用一个超级老的图(来自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;
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;
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的宽和高分别是300*400
,启动软件,实际软件宽高变成了316*439
,被拉长了。
切换到另一显示器的时候,这个数字会被重置为300*400
.也就是说在从一个屏幕移动到另一个屏幕的时候客户区发生了变化。
但是,重点来了,我们设置window的颜色是红色,却是全部覆盖。window的height、width、top、right等属性发生了改变。猜测这是qt的bug。
收到了上边提到的项目(https://github.com/qtdevs/FramelessHelper/issues/10)作者回复,内容如下:
我尝试在nativeEventFilter函数中处理WM_MOVE消息,执行如下代码
SetWindowPos(msg->hwnd, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOOWNERZORDER); RedrawWindow(msg->hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_NOCHILDREN);
即可解决多个显示器之间切换问题,但是程序还是会有其他问题。
这个方法我在VS调试诊断中看到,使用这个方法CPU性能前后都是在25左右。
感觉是歪门邪道,但是目前确实解决了我的问题,大家可以参考一下。
确实解决了,还可以精简下只设置SetWindowPos就行:
RECT rcClient;
GetWindowRect(msg->hwnd, &rcClient);
SetWindowPos(msg->hwnd, NULL, rcClient.left, rcClient.top, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top, SWP_FRAMECHANGED);