【Qt】自定义标题栏 Title Bar的两种方案

本方案以Windows平台为例

在Windows上,标题栏是由操作系统管理的,Qt只能设置标题和icon,无法深度定制UI ,比如设置一个非正方形的icon。

如果想要实现标题栏的深度定制,只能隐藏原生标题栏后自己实现一个新的标题栏,当然还要实现原生标题栏自带的一些功能,诸如:

  • 拖动窗口
  • 最小化、最大化、关闭3个按钮
  • 双击全屏/取消全屏
  • 通过拖拽调整窗口大小

通过无边框窗口实现标题栏定制化

隐藏原生标题栏的方式有很多,直接隐藏边框最干净

  • 在Qt中可以通过设置windowFlags

    c++ 复制代码
    setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint);
  • 通过WinUser.hSetWindowLongPtr

    c++ 复制代码
    LONG_PTR style = GetWindowLongPtr(hwnd, GWL_STYLE);
    style &= ~(WS_CAPTION | WS_THICKFRAME | WS_SYSMENU); // 移除标题栏、边框和系统菜单
    SetWindowLongPtr(hwnd, GWL_STYLE, style);

有一些开源项目给出了具体实现:

将窗口设置为无边框后,只需要在窗口最上方添加一个自定义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专业版)上实现了色带与自定义标题栏的颜色融合。

简单介绍解决方案就是:

  1. 强制关闭用户的"透明效果"
  2. 如果用户打开了"在标题栏和窗口边框上显示强调色",就利用UISettingsGetColorValue(UIColorType::Accent)获取到Accent颜色,这个颜色就是用户设置的主题色。
  3. 如果用户没有打开"在标题栏和窗口边框上显示强调色",就判断是否为家庭版,是家庭版就返回#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消除色带

希望调整窗口的实际大小/位置,进而消除窗口顶部多出来的色带。尝试无果,无法消除色带。

相关推荐
island13146 分钟前
【QT】系统事件入门 -- 文件 QFile基础和示例
开发语言·qt
珹洺28 分钟前
C++从入门到实战(五)类和对象(第一部分)为什么有类,及怎么使用类,类域概念详解(附带图谱等更好对比理解)
java·c语言·开发语言·数据结构·c++·redis·缓存
Evand J38 分钟前
GNSS(GPS、北斗等)与UWB的融合定位例程,matlab,二维平面,使用卡尔曼滤波
开发语言·matlab·平面
笑口常开xpr1 小时前
C 语 言 --- 扫 雷 游 戏(初 阶 版)
c语言·开发语言
Cindy辛蒂1 小时前
C语言:穷举法编程韩信点兵问题四种做法
c语言·开发语言·算法
为美好的生活献上中指1 小时前
java每日精进 3.21 【SpringBoot规范2.0】
java·开发语言·spring boot·log4j·async·mail
繁缕怀夕1 小时前
QT笔记----QCheckBox
数据库·笔记·qt
Antonio9151 小时前
【Q&A】装饰模式在Qt中有哪些运用?
服务器·数据库·qt
碧海饮冰1 小时前
招聘面试季--一文顿悟,Java中字节流和字符流的区别及使用场景上的差异
java·开发语言·面试
重生成为码农‍2 小时前
类加载机制
java·开发语言·jvm