zoukankan      html  css  js  c++  java
  • QT无边框窗体——拦截windows消息实现

    无边框其实就是去掉windows自带的标题栏,去掉标题栏之后手动实现标题栏的功能:
    1、左键按住标题栏移动窗体
    2、双击标题栏切换最大化和normal状态
    3、贴靠窗口功能
    4、四个边和四个角resize窗体大小
    5、窗体阴影

    窗体的客户区和非客户区

    用一个超级老的图(来自msdn)来介绍客户区和非客户区,简单来说就是,客户区是下图白色区域,除此之外都是非客户区。

    客户区好说,qml写的东西都是堆在客户区的,非客户区比较麻烦,但是win32提供了一些办法来自定义非客户区
    https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#related-topics
    msdn上面的这个链接提供了非常详细的介绍,简单总结一下
    1、客户区和非客户区的关系就类似于ps中的图层,客户区是在非客户的上边
    2、我们可以通过win32的api设置非客户区四个边框的大小

    这个图左右两个只有细微的差别,那就是看起来左边的客户区带边框,而右边的没有,实际上是有边的非客户区边框被重新设置过,做了加粗处理,客户区覆盖了边框(边框是非客户区的,客户区没有),如果把客户区设置为透明就能看出其中玄机:

    窗体的样式

    这个属于非客户的功能,窗体带不带标题栏、有没有最大化那几个按钮等都是Windows系统的窗体样式。
    每个窗口都有一个或多个窗口样式。 窗口样式是一个命名常量(named constant),它定义了窗口类(并不是那个结构体的window class,而是指create函数创建的一个窗体)未指定的窗口外观和行为。 应用程序通常在创建窗口时设置窗口样式(create函数),也可以在创建window后使用SetWindowLong函数设置样式。
    窗体样式的参考可以看这个链接: https://docs.microsoft.com/zh-cn/windows/win32/winmsg/window-styles
    除此之外还有扩展样式: https://docs.microsoft.com/zh-cn/windows/win32/winmsg/extended-window-styles
    setwindowlong就是设置样式的函数,比如去掉标题栏:

    效果如下图

    虽然从代码上我们是去掉了标题栏,但是很明显,标题栏还有一个高度,测量了一下是3个像素,所以直接设置窗体样式并不能实现我们的需求(需要别的操作,我看有人可以实现,我没深究这个方案)

    与窗体相关的几个Windows的消息

    WM_NCHITTEST
    鼠标在窗体上的时候Windows发送该消息,通过“窗口过程”的返回值能让Windows了解鼠标是在哪,比如返回HTCAPTION就是在标题栏中。关于窗口过程这里不细讲。
    假设我们拦截了这个消息,然后不管什么情况都返回HTCAPTION,那么Windows系统就认为你一直在标题栏上。用鼠标左键点按就可以移动窗体。
    这个消息是resize和按住标题栏移动窗体的关键。
    这个消息的详细定义可以看这里 https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest
    WM_NCCALCSIZE
    当客户区大小和坐标需要被重新计算的时候Windows会发送这个消息,默认os会自动处理该消息,但是如果我们拦截该消息并返回一些特定的值比如数字0x00,那么就能更改客户区的大小和坐标。
    实际上返回0x00就是让客户区完全覆盖窗体,效果可以看下图

    红框就是客户区(客户区做了透明处理),右边的图完全覆盖了窗体(包括非客户区)。其实这就是创造无边框窗体的核心操作
    这个消息的详细定义可以看这里 https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize
    WM_ACTIVATE
    窗口被激活的时候os会发送这条消息
    https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-activate

    qt中的消息过滤器

    win32程序中可以在WndProc函数中处理消息,这非常方便,但是qt封装了系统相关的东西做到了跨平台,要在qt中截获windows的系统消息就没有win32程序那么方便,但是qt仍开放了接口
    使用 QAbstractNativeEventFilter类 , 参考 https://doc.qt.io/qt-5/qabstractnativeeventfilter.html
    除了构造函数,就一个成员函数nativeEventFilter

    所有的native消息都会经过这个函数,如果重写该函数,抓取到某个消息后函数返回值为true,那么该消息就永远不会进入qt系统(被阻断了),如果返回false,那么这条消息就会正常进入qt系统被处理。
    该函数的第三个参数就相当于win32中WndProc函数的返回值。比如在win32中处理WM_NCCALCSIZE消息的代码

    在qt的消息过滤器中上图就等价于:

    所以:我们继承QAbstractNativeEventFilter类就可以对Windows系统消息做自定义处理,当然,我们写的消息过滤器要在qt的main函数中注册到qt系统,否则qt不会识别的:

    去掉Windows系统的非客户区

    其实就是用客户区完全覆盖整个窗体,根据msdn消息说明
    截取WM_NCCALCSIZE消息,并设置返回值为0
    就这么简单,就可以实现了。不过首先还是要写一个类,继承QAbstractNativeEventFilter类 ,代码如下:

    此时的窗口,用鼠标根本无法操作,也没有阴影,因为非客户区的所有可控因素都被客户区盖住了。

    添加阴影

    设置一下非客户区边框就可以了,利用WM_ACTIVATE消息

    resize和移动窗体

    利用WM_NCHITTEST消息
    下边代码绝大多数是参考了 https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#appendix-c-hittestnca-function msdn这个提供的代码

    case WM_NCHITTEST:
    		{
    			//处理resize
    			//标记只处理resize
    			bool isResize = false;
    
    			//鼠标点击的坐标
    			POINT ptMouse = { GET_X_LPARAM(pMsg->lParam), GET_Y_LPARAM(pMsg->lParam) };
    			//窗口矩形
    			RECT rcWindow;
    			GetWindowRect(pMsg->hwnd, &rcWindow);
    			RECT rcFrame = { 0,0,0,0 };
    			AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);
    			USHORT uRow = 1;
    			USHORT uCol = 1;
    			bool fOnResizeBorder = false;
    
    			//确认鼠标指针是否在top或者bottom
    			if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + 1)
    			{
    				fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top));
    				uRow = 0;
    				isResize = true;
    			}
    			else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - 5)
    			{
    				uRow = 2;
    				isResize = true;
    			}
    			//确认鼠标指针是否在left或者right
    			if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + 5)
    			{
    				uCol = 0; // left side
    				isResize = true;
    			}
    			else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - 5)
    			{
    				uCol = 2; // right side
    				isResize = true;
    			}
                if (ptMouse.x >= rcWindow.left && ptMouse.x <= rcWindow.right - 135 && ptMouse.y > rcWindow.top + 3 && ptMouse.y <= rcWindow.top + 30)
    			{
                    *result = HTCAPTION;
    				return true;
    			}
    
    			LRESULT hitTests[3][3] =
    			{
    				{ HTTOPLEFT,    fOnResizeBorder ? HTTOP : HTCAPTION,    HTTOPRIGHT },
    				{ HTLEFT,       HTNOWHERE,     HTRIGHT },
    				{ HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT },
    			};
    
    			if (isResize == true)
    			{
    				*result = hitTests[uRow][uCol];
    				return true;
    			}
    			else
    			{
    				return false;
    			}
    
    		}
    

    关于移动窗体,这个可选的方案有很多,qt中可以使用mousearea实现,但是使用Windows消息更好用,因为mousearea实现的办法没有“贴边停靠”,而且双击标题栏的操作也要手动实现。
    关于贴边停靠功能,我并没有找到什么可查询的资料准确定义怎么实现,我做了很多次尝试,总结一下基本上是有两个条件可以实现,首先是设置窗体样式,具体是哪几个样式我没细究,总之必须有某些样式,才能实现贴边停靠,第二个条件是鼠标按住标题栏贴近屏幕某个边缘。这两个条件缺一不可,这也是为什么移动窗体我是拦截系统消息而不是在qml中使用mousearea的原因。关于第一个条件“窗体样式”在我的代码中我猜测是在WM_ACTIVATE处理阴影的时候添加了合适的窗口样式,这里没有细究。

    完整代码

    #ifndef WINMSGFILTER_H
    #define WINMSGFILTER_H
    #include <QAbstractNativeEventFilter>
    #include <QDebug>
    #include <Windows.h>
    #pragma comment(lib, "dwmapi")
    #pragma comment(lib,"user32.lib")
    #include <dwmapi.h>
    #include <windowsx.h>
    
    class WinMsgFilter :public QAbstractNativeEventFilter
    {
    public:
    	WinMsgFilter();
    	//过滤掉消息返回true,否则返回false
    	bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override
    	{
    		MSG* pMsg = reinterpret_cast<MSG*>(message);
    
    		switch (pMsg->message)
    		{
    			//去掉边框
    		case WM_NCCALCSIZE:
    		{
    			*result = 0;
    			return true;
    			break;
    		}
    		//阴影
            case WM_ACTIVATE:
            {
                MARGINS margins = { 1,1,1,1 };
                HRESULT hr = S_OK;
                hr = DwmExtendFrameIntoClientArea(pMsg->hwnd, &margins);
                *result = hr;
                return true;
            }
    		case WM_NCHITTEST:
    		{
    			//处理resize
    			//标记只处理resize
    			bool isResize = false;
    
    			//鼠标点击的坐标
    			POINT ptMouse = { GET_X_LPARAM(pMsg->lParam), GET_Y_LPARAM(pMsg->lParam) };
    			//窗口矩形
    			RECT rcWindow;
    			GetWindowRect(pMsg->hwnd, &rcWindow);
    			RECT rcFrame = { 0,0,0,0 };
    			AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);
    			USHORT uRow = 1;
    			USHORT uCol = 1;
    			bool fOnResizeBorder = false;
    
    			//确认鼠标指针是否在top或者bottom,顺带说一下屏幕坐标原点是左上角,窗体坐标原点也是左上角
    			if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + 1)
    			{
    				fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top));
    				uRow = 0;
    				isResize = true;
    			}
    			else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - 5)
    			{
    				uRow = 2;
    				isResize = true;
    			}
    			//确认鼠标指针是否在left或者right
    			if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + 5)
    			{
    				uCol = 0; // left side
    				isResize = true;
    			}
    			else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - 5)
    			{
    				uCol = 2; // right side
    				isResize = true;
    			}
                //检测是不是在标题栏上,右边预留出了45*3 = 135的宽度,是留给关闭按钮、最大化、最小化的。
                if (ptMouse.x >= rcWindow.left && ptMouse.x <= rcWindow.right - 135 && ptMouse.y > rcWindow.top + 3 && ptMouse.y <= rcWindow.top + 30)
    			{
                    *result = HTCAPTION;
    				return true;
    			}
    
    			LRESULT hitTests[3][3] =
    			{
    				{ HTTOPLEFT,    fOnResizeBorder ? HTTOP : HTCAPTION,    HTTOPRIGHT },
    				{ HTLEFT,       HTNOWHERE,     HTRIGHT },
    				{ HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT },
    			};
    
    			if (isResize == true)
    			{
    				*result = hitTests[uRow][uCol];
    				return true;
    			}
    			else
    			{
    				return false;
    			}
    
    		}
    		}
    
    		//这里一定要返回false,否则是屏蔽所有消息了
    		return false;
    	}
    
    };
    
    #endif // WINMSGFILTER_H
    
    

    不要忘了在main函数中注册消息拦截的类

    demo演示

    demo代码
    https://gitee.com/feipeng8848/frameless-window-qml/tree/master

    瑕疵

    这并不是完美的解决方案,切换多显示器的时候会有下图这种边框出现(为了方便观察,我把window的颜色做成了红色)

    在别人实现的无边框方案中,也出现了这种问题,目前还没解决 https://github.com/qtdevs/FramelessHelper/issues/10
    测试过程中发现Window的height和width发生了改变(top和right也有变化),而qt中的window的height和width实际是“客户区”的size
    当设置qml中window的宽和高分别是300400,启动软件,实际软件宽高变成了316439,被拉长了。
    切换到另一显示器的时候,这个数字会被重置为300*400.也就是说在从一个屏幕移动到另一个屏幕的时候客户区发生了变化。

    但是,重点来了,我们设置window的颜色是红色,却是全部覆盖。window的height、width、top、right等属性发生了改变。猜测这是qt的bug。

  • 相关阅读:
    python学习之控制语句
    linux中的网络基础
    python学习之准备
    linux用户权限
    python学习之函数和函数参数
    python学习之输出与文件读写
    linux中的vim编辑器的使用
    从产品和用户角度,思考需求和用户体验
    好记性不如烂笔头
    TI DaVinci(达芬奇)入门
  • 原文地址:https://www.cnblogs.com/feipeng8848/p/14493087.html
Copyright © 2011-2022 走看看