Qt 无边框窗口进阶指南

目录

一、无边框窗口基础篇

[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 设置无边框样式等)

三、总结(Conclusion)


一、无边框窗口基础篇

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 无边框窗口的实现,完成了以下核心内容阐述:

  1. 基础架构 :通过 setWindowFlags 与 Windows 原生样式(GWL_STYLE)双重设置,彻底去除系统边框,同时保留 WS_SIZEBOX 等关键属性,为后续交互奠定基础。

  2. 系统级拖拽 :利用 ReleaseCapture + SendMessage,将窗口移动任务交由 Windows 桌面管理器处理,避免了 Qt 手动移动的坐标计算复杂性与卡顿问题,且天然支持多显示器与 DPI 缩放。

  3. 精准命中测试 :通过重写 nativeEvent 拦截 WM_NCHITTEST,手动划分窗口 8 个拉伸区域(四角 + 四边),实现类似原生窗口的任意方向缩放;同时处理 WM_NCCALCSIZE 消除非客户区残留,确保客户区边界完美对齐。

  4. 特殊交互模式 :实现了"置顶全屏"功能,通过动态切换窗口标志与全屏状态,并利用 m_fullAndTop 标志屏蔽拉伸消息,满足演示或专注场景的需求。

相关推荐
点我头像干啥1 小时前
004:多个PDF文件合并
开发语言·pdf·php
En^_^Joy1 小时前
Cadence Skill脚本语言快速入门指南
开发语言
10mAh1 小时前
【Java】HashMap 的 put 到底做了什么?——冲突、扩容与树化实测
java·开发语言·hash
九九落3 小时前
JavaScript 实现北京时间精确到毫秒显示:UTC+8、Asia/Shanghai 与在线时间校准详解
开发语言·javascript·ecmascript
东华万里3 小时前
第41篇 C++类与对象核心知识梳理:从底层原理到面试实战
开发语言·c++·大学生专区
微小冷3 小时前
Python图论库NetworkX初步
开发语言·python·图论·graph·networkx·digraph
Data_Journal4 小时前
如何使用 Python 抓取 Google Flights:分步指南
大数据·开发语言·数据库·python·scrapy
一直C5 小时前
【数据结构】哈希表+算法复杂度与经典排序查找(C语言)
java·linux·开发语言·数据结构·算法·ubuntu·散列表
Kismet_nvi5 小时前
Python 基础核心知识点总结
开发语言·windows·python