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

发布于:2025-03-21 ⋅ 阅读:(44) ⋅ 点赞:(0)

本方案以Windows平台为例

在Windows上,标题栏是由操作系统管理的,Qt只能设置标题和icon,无法深度定制UI,比如设置一个非正方形的icon。
如果想要实现标题栏的深度定制,只能隐藏原生标题栏后自己实现一个新的标题栏,当然还要实现原生标题栏自带的一些功能,诸如:

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

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

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

  • 在Qt中可以通过设置windowFlags
    setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint);
    
  • 通过WinUser.hSetWindowLongPtr
    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中
    setWindowFlags(Qt::Window | Qt::CustomizeWindowHint);
    
  • WinUser.h
    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,否则返回白色

具体实现如下:

判断是否为家庭版本:

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;
}

判断是否打开了“在标题栏和窗口边框上显示强调色”

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; // 默认视为未启用
}

透明效果是否打开

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颜色

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;
}

强制打开“透明效果”

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;
}

最后设置

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消除色带

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


网站公告

今日签到

点亮在社区的每一天
去签到