• 12358阅读
  • 11回复

更加“Native”的方式实现无边框可拖动调整大小的阴影窗口 [复制链接]

上一主题 下一主题
离线nyyzpp
 

只看楼主 倒序阅读 楼主  发表于: 2014-03-03
— 本帖被 XChinux 执行加亮操作(2014-03-04) —
Qt Version:Qt5.2.1
Platform:windows 7
实现目的:网上很多类似的教程,但最终都会产生或多或少的负面作用,例如窗口闪烁、需要自定义最大化方法显示方法(就需要使用多个变量来存储窗口状态)、计算调整窗口的边界在哪(逻辑过程复杂,并且需要和窗口拖动一起判断)等等,也许还会面临更多问题,这里不一一列出。为了能够更加“native”的方式去实现无边框可以拖动和调整大小的阴影窗口,查阅各种资料(主要参考msdn library),小有成果,切不敢独享。
实现过程:
1. 去除边框,是背景透明:
  1. this->setWindowFlags(Qt::FramelessWindowHint);
  2. this->setAttribute(Qt::WA_TranslucentBackground);


    背景透明的目的是为了能够重画阴影窗口

2. 重载paintEvent函数:
  1. void QShadowDialog::paintEvent(QPaintEvent *)
  2. {
  3.     Q_D(QShadowDialog);
  4.     QSize verticalShadowSize   = QSize(d->shadowSize, this->height() - d->shadowSize*2);
  5.     QSize horizontalShadowSize = QSize(this->width() - shadowSize()*2, d->shadowSize);
  6.     QSize cornerShadowSize     = QSize(d->shadowSize, d->shadowSize);
  7.     QRect leftRectangle        = QRect(QPoint(0, d->shadowSize), verticalShadowSize);
  8.     QRect leftTopRectangle     = QRect(QPoint(0, 0), cornerShadowSize);
  9.     QRect topRectangle         = QRect(QPoint(d->shadowSize, 0), horizontalShadowSize);
  10.     QRect rightTopRectangle    = QRect(QPoint(this->width() - d->shadowSize, 0), cornerShadowSize);
  11.     QRect rightRectangle       = QRect(QPoint(this->width() - d->shadowSize, d->shadowSize), verticalShadowSize);
  12.     QRect rightBottomRectangle = QRect(QPoint(this->width() - d->shadowSize, this->height() - d->shadowSize), cornerShadowSize);
  13.     QRect bottomRectangle      = QRect(QPoint(d->shadowSize, this->height() - d->shadowSize), horizontalShadowSize);
  14.     QRect leftBottomRectangle  = QRect(QPoint(0, this->height() - d->shadowSize), cornerShadowSize);
  15.     QPainter painter(this);
  16.     painter.drawPixmap(leftRectangle,        d->shadowPixmapList[0].scaled(verticalShadowSize));
  17.     painter.drawPixmap(leftTopRectangle,     d->shadowPixmapList[1].scaled(cornerShadowSize));
  18.     painter.drawPixmap(topRectangle,         d->shadowPixmapList[2].scaled(horizontalShadowSize));
  19.     painter.drawPixmap(rightTopRectangle,    d->shadowPixmapList[3].scaled(cornerShadowSize));
  20.     painter.drawPixmap(rightRectangle,       d->shadowPixmapList[4].scaled(verticalShadowSize));
  21.     painter.drawPixmap(rightBottomRectangle, d->shadowPixmapList[5].scaled(cornerShadowSize));
  22.     painter.drawPixmap(bottomRectangle,      d->shadowPixmapList[6].scaled(horizontalShadowSize));
  23.     painter.drawPixmap(leftBottomRectangle,  d->shadowPixmapList[7].scaled(cornerShadowSize));
  24.     painter.setPen(Qt::NoPen);
  25.     painter.setBrush(Qt::white);
  26.     painter.drawRect(d->shadowSize, d->shadowSize, this->width() - d->shadowSize*2, this->height() - d->shadowSize*2);
  27. }
    这里是为了画出阴影背景,可以参考之前360实现的代码,并非来自本人的成果。



3.响应系统消息,重载nativeEvent(Qt4.x中,应该重载winEvent)
    这里开始跟网上的大多教程不一致了,网上大多教程叫你使用mouseMoveEvent,mousePressEvent和mouseReleaseEvent,但是这样做会产生很多副作用,具体什么副作用,上面有列出。于是从其他想法入手,查找资料发现网上一个WPF的实现,于是突发奇想,Qt也可以响应WM_消息,为何不从这里入手,把窗口移动、调整大小的工作交给操作系统处理呢?于是就有了下列代码:

  1. bool QShadowDialog::nativeEvent(const QByteArray& eventType, void* message, long* result)
  2. {
  3.     Q_UNUSED(eventType)
  4.     Q_D(QShadowDialog);
  5.     MSG* param = static_cast<MSG*>(message);
  6.     switch (param->message) {
  7.     case WM_NCHITTEST: {
  8.             HWND hWnd = (HWND)this->winId();
  9.             int x = GET_X_LPARAM(param->lParam) - this->geometry().x();
  10.             int y = GET_Y_LPARAM(param->lParam) - this->geometry().y();
  11.             //! [1] 指定标题栏区域
  12.             if (this->childAt(x, y)) {
  13.                 return false;
  14.             }
  15.             else {
  16.                 *result = HTCAPTION;
  17.             }
  18.             //! [1]
  19.             //! [2] 如果窗口最大化了,则不支持resize
  20.             if (IsZoomed(hWnd)) return true;
  21.             if (!d->resizable) return true;
  22.             //! [2]
  23.             if (x > 0 && x < d->shadowSize)
  24.                 *result = HTLEFT;
  25.             if (x > this->width() - d->shadowSize && x < this->width())
  26.                 *result = HTRIGHT;
  27.             if (y > 0 && y < d->shadowSize)
  28.                 *result = HTTOP;
  29.             if (y > this->height() - d->shadowSize && y < this->height())
  30.                 *result = HTBOTTOM;
  31.             if (x > 0 && x < d->shadowSize && y > 0 && y < d->shadowSize)
  32.                 *result = HTTOPLEFT;
  33.             if (x > this->width() - d->shadowSize && x < this->width() && y > 0 && y < d->shadowSize)
  34.                 *result = HTTOPRIGHT;
  35.             if (x > 0 && x < d->shadowSize && y > this->height() - d->shadowSize && y < this->height())
  36.                 *result = HTBOTTOMLEFT;
  37.             if (x > this->width() - d->shadowSize && x < this->width() && y > this->height() - d->shadowSize && y < this->height())
  38.                 *result = HTBOTTOMRIGHT;
  39.             return true;
  40.         }
  41.     case WM_NCLBUTTONDBLCLK:
  42.         {
  43.             //! [1] 如果窗口大小不可调整,则不处理
  44.             if (!d->resizable) return false;
  45.             //! [1]
  46.             HWND hWnd = (HWND)this->winId();
  47.             if (IsZoomed(hWnd)) {
  48.                 ShowWindow(hWnd, SW_RESTORE);
  49.             }
  50.             else {
  51.                 ShowWindow(hWnd, SW_MAXIMIZE);
  52.             }
  53.             return true;
  54.         }
  55.     case WM_GETMINMAXINFO:
  56.         {
  57.             QRect rect = QApplication::desktop()->availableGeometry(this);
  58.             MINMAXINFO* info = reinterpret_cast<MINMAXINFO*>(param->lParam);
  59.             info->ptMaxPosition.x = rect.x() - d->shadowSize;
  60.             info->ptMaxPosition.y = rect.y() - d->shadowSize;
  61.             info->ptMaxSize.x = rect.width() + d->shadowSize*2;
  62.             info->ptMaxSize.y = rect.height() + d->shadowSize*2;
  63.             return true;
  64.         }
  65.     }// end switch
  66.     return false;
  67. }
    你可以在msdn library上查看这几个消息所代表的意思,凡是以NC开头的消息,都代表非客户区域的消息(not client)

    I. WM_NCHITTEST表示鼠标在非客户区域的位置,result返回具体的位置,例如HTCAPTION表示鼠标在标题栏,HTLEFT表示鼠标在窗口左边界,接下来操作系统会自己去处理先关事件,相信大家很快就会知道由操作系统去处理,比我们自己去处理的好处和代码简洁度的提高。
    II. WM_NCLBUTTONDBLCLK表示非客户区域双击事件,这段代码很容易懂,不做详细解释。
    III. WM_GETMINMAXINFO表示操作系统需要获取窗口最大化最小化的一些信息,为什么需要处理这个消息呢,事实是这样的,由于自绘了阴影部分,算在客户区域,当不处理这个消息的时候,最大化的话会发现,窗口会遮挡任务栏,并且阴影在出现在最大化的状态当中,这样就不太符合要求了。幸运的是,微软提供了响应的处理来获取最大化信息。代码很容易懂,不做详细解释。

4.支持修改阴影大小

  1. void QShadowDialog::setShadowSize(int shadowSize)
  2. {
  3.     Q_D(QShadowDialog);
  4.     if (shadowSize > 0 && shadowSize < 20) {
  5.         d->shadowSize = shadowSize;
  6.         this->update();
  7.     }
  8.     else {
  9.         qWarning() << "Shadow size bigger than 20px or less than 0px will be throw!";
  10.     }
  11. }
    实现这一功能看起来会更加的“人性化”吧。


后续:该类实现的还不是很完美,在我的计划中,我还需要支持指定背景色、支持Aero Snap功能等,关于Aero Snap功能,我怀疑这是Qt的一个bug,已经看到有人提交了Aero Snap的bug在Qt网站上,当然如果有大拿能够提供Aero Snap的实现方法,本人感激不尽。

提供附件下载:附件的名称是我要实现的一个伟大计划,相信大家根据名字也知道了: QtWidgetsExtend.rar (17 K) 下载次数:207
离线nyyzpp

只看该作者 1楼 发表于: 2014-03-03
我今天又重新实现了改类,使用了更加合理的方式,稍后贴出来
离线windywater

只看该作者 2楼 发表于: 2014-03-04
Mac和Linux下怎么使用?
离线woniu600

只看该作者 3楼 发表于: 2014-03-07
Mac和Linux下怎么使用?
离线mxcai2005

只看该作者 4楼 发表于: 2014-03-19
mark  ..............
离线gzfstudy

只看该作者 5楼 发表于: 2014-04-08
离线hcaihao

只看该作者 6楼 发表于: 2014-04-10
这种方法有个bug,困扰我很久了

http://www.qtcn.org/bbs/read-htm-tid-56224.html
离线姜小白

只看该作者 7楼 发表于: 2016-04-23
  
我整理的一些文章,持续更新中,有兴趣的小伙伴可以关注。
https://xiaozhuanlan.com/sorghum-cpp?rel=sorghum

Qt技术交流:QQ2499971906
在线tonyzhou1985

只看该作者 8楼 发表于: 2016-05-24
如果往客户区添加一个qwebview,并加载了页面的话,就无效了。
离线sunboseo

只看该作者 9楼 发表于: 2016-10-24
看起来很不错
离线qxiaoyu718

只看该作者 10楼 发表于: 2016-11-11
给力帖子 顶
离线shokokawaii

只看该作者 11楼 发表于: 2023-10-22
QFluentWidgets 是基于 Qt 的 Fluent Designer 组件库,内置超过 160 个开箱即用的 Fluent Designer 组件,支持亮暗主题无缝切换和自定义主题色。搭配所见即所得的 Fluent Designer 软件,只需拖拖拽拽,不用编写一行 QSS,就能快速搭建现代化界面。有意者可联系邮箱或者 QQ~
官网:https://qfluentwidgets.com/zh/ (需要科学上网)
邮箱:shokokawaii@foxmail.com
QQ:1953658489
快速回复
限100 字节
 
上一个 下一个