要想功能完善,兼容多平台,建议你使用这个开源库:GitHub - stdware/qwindowkit: Cross-platform frameless window framework for Qt. Support Windows, macOS, Linux.
我这里介绍的是一个仅支持Windows的轻量级方案:
构造函数
首先自定义一个 QWidget ,在做好布局,添加好子控件之后,执行如下三段代码:
setAttribute(Qt::WA_DontCreateNativeAncestors);
HWND hwnd = reinterpret_cast<HWND>(winId());
MARGINS margins = { 1, 1, 1, 1 };
DwmExtendFrameIntoClientArea(hwnd, &margins);
int value = 2;
DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, &value, sizeof(value));
DwmSetWindowAttribute(hwnd, DWMWA_ALLOW_NCPAINT, &value, sizeof(value));
上面这段代码用于为窗口添加阴影效果
LONG style = GetWindowLongPtr(hwnd, GWL_STYLE);
style &= ~(WS_CAPTION | WS_THICKFRAME | WS_BORDER | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
SetWindowLongPtr(hwnd, GWL_STYLE, style);
SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
上面这段代码用于移除窗口的标题栏和边框,注意此处不能使用 setWindowFlags(Qt::FramelessWindowHint | Qt::Window); 来设置无边框窗口,不然窗口最大化之后无法还原,参见我之前的文章。
setGeometry(0, 0, 1200, 800);
//show();
showMaximized();
这段代码用于设置窗口位置、大小,并显示窗口,注意,必须等设置完阴影效果,移除边框之后,才能设置窗口大小并显示。不然窗口内容大小和定位会出问题。
处理系统消息
现在来处理窗口的原生事件
bool nativeEvent(const QByteArray& eventType, void* message, qintptr* result) override {
MSG* msg = static_cast<MSG*>(message);
switch (msg->message) {
//......
}
return QWidget::nativeEvent(eventType, message, result);
}
第一个原生消息:
case WM_GETMINMAXINFO: {
MINMAXINFO* mmi = reinterpret_cast<MINMAXINFO*>(msg->lParam);
RECT workArea;
SystemParametersInfo(SPI_GETWORKAREA, 0, &workArea, 0);
mmi->ptMaxPosition.x = workArea.left;
mmi->ptMaxPosition.y = workArea.top;
mmi->ptMaxSize.x = workArea.right - workArea.left;
mmi->ptMaxSize.y = workArea.bottom - workArea.top;
*result = 0;
return true;
}
这段代码用于设置窗口最大化的尺寸,避免窗口最大化时,把任务栏都盖住了。
第二个原生消息:
case WM_NCHITTEST: {
const static int borderWidth = 8;
QPoint pos = mapFromGlobal(QCursor::pos());
bool left = pos.x() < borderWidth;
bool right = pos.x() > width() - borderWidth;
bool top = pos.y() < borderWidth;
bool bottom = pos.y() > height() - borderWidth;
if (left && top) *result = HTTOPLEFT;
else if (right && top) *result = HTTOPRIGHT;
else if (left && bottom) *result = HTBOTTOMLEFT;
else if (right && bottom) *result = HTBOTTOMRIGHT;
else if (left) *result = HTLEFT;
else if (right) *result = HTRIGHT;
else if (top) *result = HTTOP;
else if (bottom) *result = HTBOTTOM;
else if (pos.x() < titleBar->btnMin->pos().x() && pos.y() < 34) {
*result = HTCAPTION;
}
else {
*result = HTCLIENT;
}
return true;
}
这段代码用于配置窗口的自定义边框和标题栏可拖拽区域,用于拖拽改变窗口大小和位置
第三个原生消息:
case WM_NCLBUTTONDBLCLK: {
QPoint pos = mapFromGlobal(QCursor::pos());
if (pos.x() < titleBar->btnMin->pos().x() && pos.y() < 34) {
if (windowState() & Qt::WindowMaximized) {
showNormal();
}
else {
showMaximized();
}
return true;
}
break;
}
这段代码鼠标双击自定义标题栏区域时,切换窗口最大化,最小化的状态。
窗口显示时发送鼠标点击消息
接下来在窗口显示时发送鼠标点击消息
void showEvent(QShowEvent* event) override
{
QWidget::showEvent(event);
HWND hwnd = reinterpret_cast<HWND>(winId());
LPARAM pos = MAKELPARAM(1, 1);
PostMessage(hwnd, WM_LBUTTONDOWN, MK_LBUTTON, pos);
PostMessage(hwnd, WM_LBUTTONUP, 0, pos);
}
这个消息主要是用于让窗口聚焦,不然 在最小化窗口后再通过点击任务栏,显示窗口,此时鼠标移入 setSystemButton 设置的组件(自定义 QWidget ),无法触发此组件的 enterEvent
鼠标移出组件时,也无法触发此组件的 leaveEvent。
这是正常状态
这是异常状态
有了上面一段代码,就不会再有这个问题
最大化状态切换时改变最大化按钮图标
void changeEvent(QEvent* event) override
{
if (event->type() == QEvent::WindowStateChange) {
if (windowState() & Qt::WindowMaximized) {
titleBar->btnMax->code = 0xe6ea;
titleBar->update();
}
else {
titleBar->btnMax->code = 0xe6e5;
titleBar->update();
}
}
QWidget::changeEvent(event);
}
这段代码用于在窗口最大化状态切换时改变最大化按钮图标