目录
[1.1 项目背景](#1.1 项目背景)
[1.2 什么是无边框窗口?](#1.2 什么是无边框窗口?)
[1.3 Qt 层面的基础设置](#1.3 Qt 层面的基础设置)
[二、深入 Windows 原生消息机制](#二、深入 Windows 原生消息机制)
[2.1 为什么需要直接操作 Win32 API?](#2.1 为什么需要直接操作 Win32 API?)
[2.2 核心 API 详解](#2.2 核心 API 详解)
[2.3 实现系统级窗口拖拽](#2.3 实现系统级窗口拖拽)
[2.4 窗口置顶且全屏](#2.4 窗口置顶且全屏)
[2.5 设置无边框样式等](#2.5 设置无边框样式等)
一、无边框窗口基础篇
1.1 项目背景
在日常桌面应用开发中,实现一个既美观又功能完备的无边框窗口,往往需要深入理解 Windows 消息机制。本文将带你从零开始,全面解析无边框窗口的实现原理。
1.2 什么是无边框窗口?
无边框窗口就是移除系统自带的标题栏和边框,完全由开发者自定义界面样式的窗口。这种设计让应用更加美观现代,但也带来两个核心挑战:如何拖动窗口 和 如何调整窗口大小。
1.3 Qt 层面的基础设置
cpp
// 设置无边框标志
setWindowFlags(Qt::FramelessWindowHint);
// 设置鼠标跟踪,确保子控件不拦截鼠标移动事件
setMouseTracking(true);
ui.centralWidget->setMouseTracking(true);
二、深入 Windows 原生消息机制
2.1 为什么需要直接操作 Win32 API?
Qt 虽然封装了绝大多数窗口功能,但在无边框窗口场景下,Qt 自身无法处理"非客户区"事件------这些事件由 Windows 系统直接管理。比如:
WM_NCHITTEST:系统需要知道鼠标点击在窗口的哪个区域WM_NCCALCSIZE:系统需要计算客户区大小- 系统级的拖拽动画和 DPI 适配
2.2 核心 API 详解
GWL_STYLE 解释:
WS_CAPTION:标题栏样式WS_SYSMENU:系统菜单样式WS_SIZEBOX:可调整大小边框WS_CLIPCHILDREN:裁剪子控件区域,防止绘图闪烁
#include <windows.h>
HWND hwnd = reinterpret_cast<HWND>(winId());
// 获取当前样式
LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE);
// 修改样式:去掉标题栏和系统菜单,但保留可调整大小功能
SetWindowLongPtr(hwnd, GWL_STYLE,
style & ~WS_CAPTION & ~WS_SYSMENU
| WS_SIZEBOX | WS_CLIPCHILDREN | WS_CLIPSIBLINGS);
2.3 实现系统级窗口拖拽
系统级拖拽优势:
- 流畅性:系统级拖拽由 Windows 桌面管理器直接处理,动画流畅无卡顿
- 可靠性:自动支持多显示器、DPI 缩放等复杂场景
- 简洁性:无需自己维护坐标计算和事件循环
cpp
//鼠标事件过滤 处理标题栏的拖动事件
bool EtcTestTool::eventFilter(QObject* watched, QEvent* event) {
if (watched == ui.frmTitle) {
if (event->type() == QEvent::MouseButtonPress) {
auto* me = static_cast<QMouseEvent*>(event);
if (me->button() == Qt::LeftButton) {
#ifdef Q_OS_WIN
//系统API命令处理而非自己计算拖拽移动等坐标处理
//系统级拖拽,由Windows桌面管理器直接处理,动画流畅无卡顿;而Qt手动移动可能因消息循环延迟产生滞后。
//可靠性:系统级命令天然支持多显示器、DPII变化等复杂场景,避免自行维护坐标计算。
::ReleaseCapture();//释放鼠标捕获,防止干扰系统拖拽消息
::SendMessage(reinterpret_cast<HWND>(winId()),//将Qt的窗口句柄(WId)转换为Windows窗口句柄(HWND)。
WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
//SC_MOVE = 系统移动命令 HTCAPTION = 告诉系统鼠标点击的是标题栏区域,系统会自动处理移动逻辑。
#else
// 非 Windows:用 Qt 手动拖拽(略,需记录偏移)
#endif
return true;
}
}
if (event->type() == QEvent::MouseButtonDblClick) {
if (isMaximized()) showNormal(); else showMaximized();
return true;
}
}
return QWidget::eventFilter(watched, event);
}
2.4 实现窗口拉伸:命中测试
cpp
/重写nativeEvent函数进行Qt事件拦截 处理系统消息
bool EtcTestTool::nativeEvent(const QByteArray& eventType, void* message, long* result) {
#ifdef Q_OS_WIN
if (eventType != "windows_generic_MSG")
return QWidget::nativeEvent(eventType, message, result);
MSG* msg = static_cast<MSG*>(message);
switch (msg->message) {
//WM_NCCALCSIZE用来计算窗口客户区大小(即绘制内容区域,不含标题栏和边框)。当窗口尺寸变化时,系统会发送此消息。
// 去非客户区
case WM_NCCALCSIZE: {
if ((bool)msg->wParam) {
NCCALCSIZE_PARAMS* p = (NCCALCSIZE_PARAMS*)msg->lParam;
p->rgrc[2] = p->rgrc[1];
p->rgrc[1] = p->rgrc[0];
//lParam指向NCCALCSIZE_PARAMS结构体,其中包含三个矩形:rgrc[0]是窗口原矩形,rgrc[1]是用户期望的客户区,rgrc[2]是目标位置(一般与rgrc[1]相同)。
//把rgrc[1](期望客户区)设为rgrc[0](整个窗口),再让rgrc[2]等于rgrc[1],这样系统就会认为客户区就是整个窗口,从而消除了标题栏、系统菜单的残留空间。
}
return true;//返回true表示已处理。
}
//方向拉伸 系统会根据光标位置判断鼠标点击落在窗口哪个区域,无边框窗口必须自己处理这个逻辑,否则无法调整大小或拖动。
case WM_NCHITTEST: {
if (isMaximized()) return false;
if(m_fullAndTop) return true; //客户强制全屏且置顶时候不处理拉伸消息
RECT rc; GetWindowRect(msg->hwnd, &rc);//获取窗口在屏幕上的坐标(左上角和右下角)
long x = LOWORD(msg->lParam);//从lParam中提取鼠标指针的屏幕坐标 低16位 和 高16位
long y = HIWORD(msg->lParam);
// 4 角(优先)
if (x >= rc.left && x < rc.left + FRAME_W && y >= rc.top && y < rc.top + FRAME_W) { *result = HTTOPLEFT; return true; }
if (x >= rc.right - FRAME_W && x < rc.right && y >= rc.top && y < rc.top + FRAME_W) { *result = HTTOPRIGHT; return true; }
if (x >= rc.left && x < rc.left + FRAME_W && y >= rc.bottom - FRAME_W && y < rc.bottom) { *result = HTBOTTOMLEFT; return true; }
if (x >= rc.right - FRAME_W && x < rc.right && y >= rc.bottom - FRAME_W && y < rc.bottom) { *result = HTBOTTOMRIGHT; return true; }
// 4 边
if (x >= rc.left && x < rc.left + FRAME_W) { *result = HTLEFT; return true; }
if (x >= rc.right - FRAME_W && x < rc.right) { *result = HTRIGHT; return true; }
if (y >= rc.top && y < rc.top + FRAME_W) { *result = HTTOP; return true; }
if (y >= rc.bottom - FRAME_W && y < rc.bottom) { *result = HTBOTTOM; return true; }
return false; // 中间交给 Qt
}
}
#endif
return QWidget::nativeEvent(eventType, message, result);
}
2.4 窗口置顶且全屏
置顶后禁止拉伸放大缩小,界面占据整个桌面。
cpp
void EtcTestTool::SlotUpdateCurrentWindowLevel(int level)
{
bool bTopLevel = (1 == level);
if (bTopLevel) {
setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);//窗口置顶
ui.frmTitle->removeEventFilter(this);
showFullScreen();
m_fullAndTop = true;
}
else {
setWindowFlag(Qt::WindowStaysOnTopHint, false);
ui.frmTitle->installEventFilter(this);
// 先显示恢复,再最大化,避免状态残留
showNormal();
showMaximized();
m_fullAndTop = false;
}
ui.btnClose->setVisible(!bTopLevel);
ui.btnMin->setVisible(!bTopLevel);
ui.btnMax->setVisible(!bTopLevel);
}
2.5 设置无边框样式等
cpp
void EtcTestTool::InitialUi()
{
//mainwindow的鼠标跟踪事件被子控件遮挡拦截
this->setMouseTracking(true);
auto menuFullScreen = new QMenu(ui.btnFullScreen);
menuFullScreen->addAction(QIcon(":/Images/images/about_white.svg"), tr("置顶全屏"), this, [this]() {
SlotUpdateCurrentWindowLevel(1);
});
menuFullScreen->addAction(QIcon(":/Images/images/about_white.svg"), tr("取消全屏"), this, [this]() {
SlotUpdateCurrentWindowLevel(0);
});
ui.btnFullScreen->setMenu(menuFullScreen);
ui.btnFullScreen->setPopupMode(QToolButton::InstantPopup);
//设置无边框
setWindowFlags(Qt::FramelessWindowHint);
#ifdef Q_OS_WIN
// Windows 层:精细控制样式
HWND hwnd = reinterpret_cast<HWND>(winId());
//获取该窗口的"原生"Windows样式(GWL_STYLE)*/
LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE);
/*修改这个原生样式:去掉"标题栏"、"系统菜单",但保留"可调整大小"等关键样式*/
SetWindowLongPtr(hwnd, GWL_STYLE,
style & ~WS_CAPTION & ~WS_SYSMENU
| WS_SIZEBOX | WS_CLIPCHILDREN | WS_CLIPSIBLINGS);
#endif
// 标题栏装事件过滤器(拖拽用)
ui.frmTitle->installEventFilter(this);
}
三、总结
本文围绕 Qt 无边框窗口的实现,完成了以下核心内容阐述:
-
基础架构 :通过
setWindowFlags与 Windows 原生样式(GWL_STYLE)双重设置,彻底去除系统边框,同时保留WS_SIZEBOX等关键属性,为后续交互奠定基础。 -
系统级拖拽 :利用
ReleaseCapture+SendMessage,将窗口移动任务交由 Windows 桌面管理器处理,避免了 Qt 手动移动的坐标计算复杂性与卡顿问题,且天然支持多显示器与 DPI 缩放。 -
精准命中测试 :通过重写
nativeEvent拦截WM_NCHITTEST,手动划分窗口 8 个拉伸区域(四角 + 四边),实现类似原生窗口的任意方向缩放;同时处理WM_NCCALCSIZE消除非客户区残留,确保客户区边界完美对齐。 -
特殊交互模式 :实现了"置顶全屏"功能,通过动态切换窗口标志与全屏状态,并利用
m_fullAndTop标志屏蔽拉伸消息,满足演示或专注场景的需求。