本方案以Windows平台为例
在Windows上,标题栏是由操作系统管理的,Qt只能设置标题和icon,无法深度定制UI ,比如设置一个非正方形的icon。
如果想要实现标题栏的深度定制,只能隐藏原生标题栏后自己实现一个新的标题栏,当然还要实现原生标题栏自带的一些功能,诸如:
- 拖动窗口
- 最小化、最大化、关闭3个按钮
- 双击全屏/取消全屏
- 通过拖拽调整窗口大小
通过无边框窗口实现标题栏定制化
隐藏原生标题栏的方式有很多,直接隐藏边框最干净
-
在Qt中可以通过设置
windowFlags
:c++setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint);
-
通过
WinUser.h
的SetWindowLongPtr
c++LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE); style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU); // 移除标题栏、边框和系统菜单 SetWindowLongPtr(hwnd, GWL_STYLE, style);
有一些开源项目给出了具体实现:
- https://github.com/Jorgen-VikingGod/Qt-Frameless-Window-DarkStyle
- https://github.com/imitatehappiness/QtCustomTitleBar
将窗口设置为无边框后,只需要在窗口最上方添加一个自定义UI的标题栏,实现相关功能就可以实现标题栏的深度定制了......吗?
这种方式有一个比较大的问题,就是无法完全复现通过拖拽调整窗口大小的功能。
下图就是原生的窗口调整功能:
窗口的拖拽、最大化、最小化、关闭以及双击全屏这些功能都好实现,唯独通过拖拽方式调整窗口大小这个功能无法完全复现。
因为原生功能是鼠标靠近窗口边缘时会变成双向箭头,按下鼠标拖拽即可调整窗口大小,不管鼠标从外部靠近窗口边缘还是从内部接近窗口边缘都可以。
由于在Qt层面,只能检测到鼠标在窗口内部时的移动、位置,也就无法检测到从外部靠近边缘(但还未进入窗口内)的情况,最多只能实现成鼠标在窗口(内部接近)边缘位置时变成双向箭头,进而实现拖拽调整窗口大小 的功能,这与原生功能存在差异,用起来也不够舒服。
保留边框,隐藏原生标题栏
失去原生尺寸调整功能不是因为隐藏了标题栏 ,而是因为隐藏了边框。
我们本身的目的并非隐藏边框,那么是否可以在保留边框的情况下隐藏原生标题栏?
答案是可以,并且也有Qt和原生两种方案:
-
Qt中
c++setWindowFlags(Qt::Window | Qt::CustomizeWindowHint);
-
WinUser.h
c++LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE); style &= ~(WS_CAPTION); // 移除标题栏 SetWindowLongPtr(hwnd, GWL_STYLE, style);
然后在自定义标题栏中实现除尺寸调整功能外的其他功能即可。
这种方案虽然保留了原生窗口尺寸调整的能力,但也有它的问题,具体来说就是窗口最上方可能出现一个色带 (如下图),并且这个色带的颜色是无法控制的。

既然无法控制,那么隐藏它的唯一方法就是将自定义标题栏设置为相同颜色 ,这样在视觉上色带和自定义title bar就融为一体了,那解决问题的关键就变成了如何获得色带的颜色。
这个色带的颜色是操作系统控制的,获取这个色带的准确颜色是相当有挑战的,因为用户在操作系统上的个性化设置会影响这个色带的颜色,比如:
- 设置-> 个性化 -> 颜色 -> 主题模式
- 设置-> 个性化 -> 颜色 -> 透明效果
- 设置-> 个性化 -> 颜色 -> 主题颜色
- 设置-> 个性化 -> 颜色 -> 在标题栏和窗口边框上显示强调色
- 其他没测试过的设置项
更糟糕的是,在不同版本的Windows,甚至同一版本不同分支(专业版和家庭版)上,相同设置的效果可能不同,比如:
- 打开透明效果在Windows11 专业版上不会有什么效果,但在Windows11 家庭版却会使色带变成和桌面背景相似的颜色
- 设置采用默认标题栏颜色(关闭"透明效果",关闭"在标题栏和窗口边框上显示强调色"),Windows 11专业版的色带颜色为白色,而Windows 11家庭版的色带颜色为
#f3f3f3
。
所以这种方案可能并不是一个好的解决方案,可能无法从根本上解决问题。
但笔者还是尝试找出了一些解决方案,并在能找的测试机器(Windows 11家庭版 + Windows 11专业版)上实现了色带与自定义标题栏的颜色融合。
简单介绍解决方案就是:
- 强制关闭用户的"透明效果"
- 如果用户打开了"在标题栏和窗口边框上显示强调色",就利用
UISettings
的GetColorValue(UIColorType::Accent)
获取到Accent颜色,这个颜色就是用户设置的主题色。 - 如果用户没有打开"在标题栏和窗口边框上显示强调色",就判断是否为家庭版,是家庭版就返回
#f3f3f3
,否则返回白色
具体实现如下:
判断是否为家庭版本:
c++
bool IsWindowsHomeEdition() {
HKEY hKey;
wchar_t productName[255];
DWORD size = sizeof(productName);
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", 0, KEY_READ, &hKey) == ERROR_SUCCESS) {
if (RegQueryValueEx(hKey, L"ProductName", nullptr, nullptr, (LPBYTE)productName, &size) == ERROR_SUCCESS) {
RegCloseKey(hKey);
return (wcsstr(productName, L"Home") != nullptr);
}
RegCloseKey(hKey);
}
return false;
}
判断是否打开了"在标题栏和窗口边框上显示强调色"
c++
bool IsAccentColorOnTitleBarEnabled() {
HKEY hKey;
DWORD value = 0;
DWORD dataSize = sizeof(DWORD);
if (RegOpenKeyEx(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\DWM", 0, KEY_READ, &hKey) == ERROR_SUCCESS) {
// 读取 ColorPrevalence 值
if (RegQueryValueEx(hKey, L"ColorPrevalence", nullptr, nullptr, reinterpret_cast<LPBYTE>(&value), &dataSize) == ERROR_SUCCESS) {
RegCloseKey(hKey);
return (value == 1);
}
RegCloseKey(hKey);
}
return false; // 默认视为未启用
}
透明效果是否打开
c++
bool IsTransparencyEnabled() {
DWORD value = 0;
DWORD dataSize = sizeof(DWORD);
HKEY hKey;
if (RegOpenKeyEx(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", 0, KEY_READ, &hKey) == ERROR_SUCCESS) {
if (RegQueryValueEx(hKey, L"EnableTransparency", nullptr, nullptr, (LPBYTE)&value, &dataSize) == ERROR_SUCCESS) {
RegCloseKey(hKey);
return (value == 1);
}
RegCloseKey(hKey);
}
return true; // 默认视为启用
}
获取accent颜色
c++
QColor getWindowsAccentColor()
{
UISettings const ui_settings{};
auto const accent_color{ui_settings.GetColorValue(UIColorType::Accent)};
return QColor(accent_color.R, accent_color.G, accent_color.B, accent_color.A);
return Qt::white;
}
强制打开"透明效果"
c++
bool SetTransparencyEffect(bool enable) {
HKEY hKey;
DWORD value = enable ? 1 : 0;
// 打开注册表项
LSTATUS status = RegOpenKeyEx(
HKEY_CURRENT_USER,
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
0,
KEY_WRITE,
&hKey
);
if (status != ERROR_SUCCESS) {
return false;
}
// 修改键值
status = RegSetValueEx(
hKey,
L"EnableTransparency",
0,
REG_DWORD,
reinterpret_cast<const BYTE*>(&value),
sizeof(DWORD)
);
RegCloseKey(hKey);
if (status != ERROR_SUCCESS) {
return false;
}
// 通知系统设置已更改
SendMessageTimeout(
HWND_BROADCAST,
WM_SETTINGCHANGE,
0,
(LPARAM)L"ImmersiveColorSet", // 触发颜色设置刷新
SMTO_ABORTIFHUNG,
1000,
nullptr
);
return true;
}
最后设置
c++
QColor GetTitleBarColor() {
bool isHomeEdition = IsWindowsHomeEdition();
bool transparencyEnabled = IsTransparencyEnabled();
bool accentEnabled = IsAccentColorOnTitleBarEnabled();
if (transparencyEnabled) {
SetTransparencyEffect(false);
}
if(accentEnabled) {
return getWindowsAccentColor();
} else {
if (isHomeEdition) {
return QColor("#f3f3f3");
} else {
return QColor("#ffffff");
}
}
}
另外笔者还尝试过其他一些方案,下面是笔者尝试过的一些方案:
尝试用GetSysColor获取原生标题栏颜色
你可能会认为直接用Windows提供的接口GetSysColor(COLOR_WINDOWFRAME)
就可以获取到窗口的边框颜色了,这个色条估计适合边框颜色一样的,但在Windows 11 专业版的结果如下图:
另外GetSysColor(COLOR_ACTIVECAPTION)
之类的我也试过了,结果也都不对。
尝试用SetWindowPos消除色带
希望调整窗口的实际大小/位置,进而消除窗口顶部多出来的色带。尝试无果,无法消除色带。