diff --git a/README.md b/README.md index a2b4f9b..f929995 100644 --- a/README.md +++ b/README.md @@ -1,328 +1,383 @@ # QWindowKit -Cross-platform window customization framework for Qt Widgets and Qt Quick. +**Language:** English | [简体中文](./README.zh-CN.md) -This project inherited the major implementations from [wangwenx190 FramelessHelper](https://github.com/wangwenx190/framelesshelper), with a complete refactoring and upgrading of the architecture. +**Tutorials:** [English tutorial](./docs/tutorial.en-US.md) | [简体中文教程](./docs/tutorial.zh-CN.md) -Feature requests are welcome. +QWindowKit is a cross-platform window customization framework for Qt Widgets and Qt Quick. It helps applications replace the native title bar with a custom title bar while preserving important system behaviors such as window moving, resizing, system menus, and Windows Snap Layout. -## Stay In Touch :triangular_flag_on_post: +The project inherits the core ideas and part of the implementation lineage from [wangwenx190 FramelessHelper](https://github.com/wangwenx190/framelesshelper), with a refactored architecture and a smaller API surface. -You can share your findings, thoughts and ideas on improving / implementing QWindowKit functionalities on more platforms and apps! +## Features -- Chat with us on [Discord](https://discord.gg/grrM4Tmesy) -- 中文用户可加入 QQ 群 876419693 +- Custom title bar support for Qt Widgets and Qt Quick. +- Windows 11 Snap Layout support when system button roles are configured. +- Windows 10/11 frameless window handling with optional system border workarounds. +- macOS native system button positioning and optional blur or glass-style effects. +- Linux fallback implementation with partial native system menu support on Qt 6 Wayland/X11. +- CMake package exports, qmake integration files, and Visual Studio property files. ## Supported Platforms -- Microsoft Windows -- Apple macOS (11+) -- GNU/Linux - -## Features +- Microsoft Windows. +- Apple macOS 11 or later. +- GNU/Linux. -- Full support of Windows 11 Snap Layout -- Better workaround to handle Windows 10 top border issue -- Support Mac system buttons geometry customization -- Simpler APIs, more detailed documentations and comments +## Screenshots -## Gallery +### Windows 11 -### Windows 11 (With Snap Layout) +![Windows 11](./docs/images/win11.png) -![image](./docs/images/win11.png) +### Windows 10 -### Windows 10 (And 7, Vista) - -![image](./docs/images/win10.png) +![Windows 10](./docs/images/win10.png) ### macOS -![image](./docs/images/mac.png) +![macOS](./docs/images/mac.png) -| default | glass - regular | -|:---------------------------------------------------------------------:|:---------------------------------------------------------------------------------:| -| ![default](./docs/images/macos/01%20default.png) | ![glass - regular](./docs/images/macos/02%20glass%20-%20regular.png) | -| glass - clear | glass - regular, rounded | -| ![glass - clear](./docs/images/macos/03%20glass%20-%20clear.png) | ![glass - regular, rounded](./docs/images/macos/04%20glass%20-%20regular,%20rounded.png) | -| glass - regular, dark tint | glass - regular, light tint | -| ![glass - regular, dark tint](./docs/images/macos/05%20glass%20-%20regular,%20dark%20tint.png) | ![glass - regular, light tint](./docs/images/macos/06%20glass%20-%20regular,%20light%20tint.png) | -| legacy - dark blur | legacy - light blur | -| ![legacy - dark blur](./docs/images/macos/07%20legacy%20-%20dark%20blur.png) | ![legacy - light blur](./docs/images/macos/08%20legacy%20-%20light%20blur.png) | +| Default | Glass - regular | +|:--:|:--:| +| ![Default](./docs/images/macos/01%20default.png) | ![Glass regular](./docs/images/macos/02%20glass%20-%20regular.png) | +| Glass - clear | Glass - regular, rounded | +| ![Glass clear](./docs/images/macos/03%20glass%20-%20clear.png) | ![Glass regular rounded](./docs/images/macos/04%20glass%20-%20regular,%20rounded.png) | +| Glass - regular, dark tint | Glass - regular, light tint | +| ![Glass dark tint](./docs/images/macos/05%20glass%20-%20regular,%20dark%20tint.png) | ![Glass light tint](./docs/images/macos/06%20glass%20-%20regular,%20light%20tint.png) | +| Legacy - dark blur | Legacy - light blur | +| ![Legacy dark blur](./docs/images/macos/07%20legacy%20-%20dark%20blur.png) | ![Legacy light blur](./docs/images/macos/08%20legacy%20-%20light%20blur.png) | ### Linux -![image](./docs/images/linux.png) +![Linux](./docs/images/linux.png) ## Requirements -| Component | Requirement | Details | -|:---------:|:-----------:|:-------------------------:| -| Qt | \>=5.12 | Core, Gui, Widgets, Quick | -| Compiler | \>=C++17 | MSVC 2019, GCC, Clang | -| CMake | \>=3.19 | >=3.20 is recommended | +| Component | Requirement | Notes | +|:--|:--|:--| +| Qt | 5.12 or later | Qt Core and Gui are required. Widgets and Quick are optional modules. | +| C++ | C++17 or later | Tested with MSVC, GCC, and Clang. | +| CMake | 3.19 or later | CMake 3.20 or later is recommended. | + +Recommended Qt versions: -Please read [Vulnerabilities](#Vulnerabilities) carefully to acquire detailed requirements. +- Qt 5: 5.15.2 or later. +- Qt 6: 6.6.2 or later. -### Tested Compilers +QWindowKit uses Qt private APIs and platform-specific windowing behavior. Older Qt releases may compile but can have known or unknown runtime issues. -- Windows - - MSVC: 2019, 2022 - - MinGW (GCC): 13.2.0 -- macOS - - Clang 14.0.3 -- Ubuntu - - GCC: 9.4.0 +Tested compilers: -## Dependencies +- Windows: MSVC 2019, MSVC 2022, MinGW GCC 13.2.0. +- macOS: Clang 14.0.3. +- Ubuntu: GCC 9.4.0. -- Qt 5.12 or higher -- [qmsetup](https://github.com/stdware/qmsetup) +## Modules -## Integrate +| Module | Target | Purpose | +|:--|:--|:--| +| Core | `QWindowKit::Core` | Shared platform window infrastructure. | +| Widgets | `QWindowKit::Widgets` | Integration for `QWidget` and `QMainWindow`. | +| Quick | `QWindowKit::Quick` | Integration for `QQuickWindow` and QML `Window`. | -### Build & Install +`QWINDOWKIT_BUILD_WIDGETS` is enabled by default. `QWINDOWKIT_BUILD_QUICK` is disabled by default and must be enabled explicitly when you need Qt Quick support. + +## Build And Install + +Clone the repository with submodules: ```sh git clone --recursive https://github.com/stdware/qwindowkit cd qwindowkit +``` -cmake -B build -S . \ +Configure, build, and install: + +```sh +cmake -S . -B build \ -DCMAKE_PREFIX_PATH= \ - -Dqmsetup_DIR= \ # Optional - -DQWINDOWKIT_BUILD_QUICK=TRUE \ # Optional - -DCMAKE_INSTALL_PREFIX=/path/install \ - -G "Ninja Multi-Config" + -DCMAKE_INSTALL_PREFIX= \ + -DQWINDOWKIT_BUILD_WIDGETS=ON \ + -DQWINDOWKIT_BUILD_QUICK=ON -cmake --build build --target install --config Debug -cmake --build build --target install --config Release +cmake --build build --config Release +cmake --install build --config Release ``` -Read the root `CMakeLists.txt` for more build options. -You can also include this directory as a subproject if you choose CMake as your build system. +Common build options: -For other build systems, you need to install with CMake first and include the corresponding configuration files in your project. +| Option | Default | Description | +|:--|:--|:--| +| `QWINDOWKIT_BUILD_STATIC` | `OFF` | Build static libraries instead of shared libraries. | +| `QWINDOWKIT_BUILD_WIDGETS` | `ON` | Build the Widgets module. | +| `QWINDOWKIT_BUILD_QUICK` | `OFF` | Build the Quick module. | +| `QWINDOWKIT_BUILD_EXAMPLES` | `OFF` | Build examples. | +| `QWINDOWKIT_BUILD_DOCUMENTATIONS` | `OFF` | Build Doxygen documentation. | +| `QWINDOWKIT_INSTALL` | `ON` | Generate install targets and package files. | +| `QWINDOWKIT_FORCE_QT_WINDOW_CONTEXT` | `OFF` | Force the pure Qt fallback implementation. | +| `QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS` | `ON` | Enable Windows system border workarounds. | +| `QWINDOWKIT_ENABLE_STYLE_AGENT` | `ON` | Build the style/theme helper. | -### Import +`qmsetup` is required. If CMake cannot find an installed `qmsetup` package, the bundled submodule is used automatically. -#### CMake Project +## Integrate With Your Project + +### CMake + +Configure your application with the installed package path: ```sh -cmake -B build -DQWindowKit_DIR=/path/install/lib/cmake/QWindowKit +cmake -S . -B build -DQWindowKit_DIR=/lib/cmake/QWindowKit ``` +Widgets application: + ```cmake -find_package(QWindowKit COMPONENTS Core Widgets Quick REQUIRED) -target_link_libraries(widgets_app PUBLIC QWindowKit::Widgets) -target_link_libraries(quick_app PUBLIC QWindowKit::Quick) +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) +find_package(QWindowKit REQUIRED COMPONENTS Widgets) + +target_link_libraries(my_widgets_app PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + QWindowKit::Widgets +) ``` -#### QMake Project +Quick application: ```cmake -# WidgetsApp.pro -include("/path/install/share/QWindowKit/qmake/QWKWidgets.pri") - -# QuickApp.pro -include("/path/install/share/QWindowKit/qmake/QWKQuick.pri") +find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick Qml) +find_package(QWindowKit REQUIRED COMPONENTS Quick) + +target_link_libraries(my_quick_app PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Quick + Qt6::Qml + QWindowKit::Quick +) ``` -#### Visual Studio Project +QWindowKit can also be added as a CMake subdirectory when it is vendored into your project. -See [Visual Studio Guide](./docs/visual-studio-guide.md) for detailed usages. +### qmake -## Quick Start +After installing QWindowKit with CMake, include the generated `.pri` file: -### Qt Widgets Application +```qmake +# Widgets +include("/share/QWindowKit/qmake/QWKWidgets.pri") -#### Initialization +# Quick +include("/share/QWindowKit/qmake/QWKQuick.pri") +``` -The following initialization should be done before any widget constructs. +### Visual Studio + +For MSBuild projects, see [Visual Studio Guide](./docs/visual-studio-guide.md). + +## Quick Start: Qt Widgets + +Set `Qt::AA_DontCreateNativeWidgetSiblings` before constructing `QApplication`: ```cpp +#include #include int main(int argc, char *argv[]) { - QGuiApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings) - - // ... + QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + + QApplication app(argc, argv); + // Create and show your main window. + return app.exec(); } ``` -#### Setup Window Agent +Install `WidgetWindowAgent` on each top-level widget that needs a custom frame: -First, setup `WidgetWindowAgent` for your top `QWidget` instance. (Each window needs its own agent.) - -```c++ +```cpp #include -MyWidget::MyWidget(QWidget *parent) { - // ... - auto agent = new QWK::WidgetWindowAgent(this); +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + auto *agent = new QWK::WidgetWindowAgent(this); agent->setup(this); - // ... -} -``` - -If you don't want to derive a new widget class or change the constructor, you can initialize the agent after the window -constructs. - -```c++ -auto w = new MyWidget(); -auto agent = new QWK::WidgetWindowAgent(w); -agent->setup(w); -``` - -You should call `QWK::WidgetWindowAgent::setup()` as early as possible, especially when you need to set the size constrains. QWindowKit will change some Qt internal data which will affect how Qt calculates the window size, and thus you need to let QWindowKit initialize at the very beginning. - -#### Construct Title bar -Then, construct your title bar widget, without which the window lacks the basic interaction feature, and it's better to -put it into the window's layout. + auto *titleBar = new QWidget(this); + setMenuWidget(titleBar); -You can use the [`WindowBar`](examples/shared/widgetframe/windowbar.h) provided by `WidgetFrame` in the examples as the -container of your title bar components. - -Let `WidgetWindowAgent` know which widget the title bar is. - -```c++ -agent->setTitleBar(myTitleBar); + agent->setTitleBar(titleBar); +} ``` -Next, set system button hints to let `WidgetWindowAgent` know the role of the child widgets, which is important for the -Snap Layout to work. +Register system button roles so that native behaviors such as Windows Snap Layout can work: -```c++ -agent->setSystemButton(QWK::WindowAgentBase::WindowIcon, myTitleBar->iconButton()); -agent->setSystemButton(QWK::WindowAgentBase::Minimize, myTitleBar->minButton()); -agent->setSystemButton(QWK::WindowAgentBase::Maximize, myTitleBar->maxButton()); -agent->setSystemButton(QWK::WindowAgentBase::Close, myTitleBar->closeButton()); +```cpp +agent->setSystemButton(QWK::WindowAgentBase::WindowIcon, iconButton); +agent->setSystemButton(QWK::WindowAgentBase::Minimize, minimizeButton); +agent->setSystemButton(QWK::WindowAgentBase::Maximize, maximizeButton); +agent->setSystemButton(QWK::WindowAgentBase::Close, closeButton); ``` -Doing this does not mean that these buttons' click events are automatically associated with window actions, you still need to manually connect the signals and slots to emulate the native window behaviors. - -On macOS, this step can be skipped because it is better to use the buttons provided by the system. +Connect button behavior yourself: -Last but not least, set hit-test visible hint to let `WidgetWindowAgent` know which widgets are willing to receive mouse events. - -```c++ -agent->setHitTestVisible(myTitleBar->menuBar(), true); +```cpp +connect(minimizeButton, &QPushButton::clicked, this, &QWidget::showMinimized); +connect(maximizeButton, &QPushButton::clicked, this, [this] { + isMaximized() ? showNormal() : showMaximized(); +}); +connect(closeButton, &QPushButton::clicked, this, &QWidget::close); ``` -The rest region within the title bar will be regarded as the draggable area for the user to move the window, and thus any QWidgets inside it will not receive any user interaction events such as mouse events/focus events/etc anymore, but you can still send/post such events to these widgets manually, either through Qt API or system API. - -- If you want to disable window maximization, you can remove the `Qt::WindowMaximizeButtonHint` flag from the window. - - - -### Qt Quick Application +Areas inside the title bar that are not hit-test visible are treated as draggable window space. -#### Initialization +## Quick Start: Qt Quick -Make sure you have registered `QWindowKit` into QtQuick: +Register the QML types before loading your QML: ```cpp +#include +#include +#include + #include int main(int argc, char *argv[]) { - // ... + QQuickWindow::setDefaultAlphaBuffer(true); + + QGuiApplication app(argc, argv); + QQmlApplicationEngine engine; - // ... QWK::registerTypes(&engine); - // ... + engine.load(QUrl(QStringLiteral("qrc:/Main.qml"))); + + return app.exec(); } ``` -#### Setup Window Components - -Then you can use `QWindowKit` data types and classes by importing its URI: +Use `WindowAgent` from QML: ```qml -import QtQuick 2.15 -import QtQuick.Window 2.15 +import QtQuick +import QtQuick.Controls +import QtQuick.Window import QWindowKit 1.0 Window { id: window - visible: false // We hide it first, so we can move the window to our desired position silently. + width: 900 + height: 600 + visible: false + + WindowAgent { + id: windowAgent + } + + Rectangle { + id: titleBar + anchors.top: parent.top + width: parent.width + height: 40 + } + Component.onCompleted: { windowAgent.setup(window) + windowAgent.setTitleBar(titleBar) window.visible = true } - WindowAgent { - id: windowAgent - // ... - } } ``` -You can omit the version number or use "auto" instead of "1.0" for the module URI if you are using Qt6. +In Qt 6, the import version can be omitted if your QML style allows unversioned imports. -As we just mentioned above, if you are going to set the size constrains, please do it after `windowAgent.setup()` is called. +## Platform Attributes -### Learn More +`WindowAgentBase::setWindowAttribute()` exposes platform-specific features. Unsupported attributes return `false`. -See [examples](examples) for more demo use cases. The examples have no High DPI support. +Windows examples: -- QWindowKit Internals [TODO] -- [FramelessHelper Related](docs/framelesshelper-related.md) +```cpp +agent->setWindowAttribute(QStringLiteral("dark-mode"), true); +agent->setWindowAttribute(QStringLiteral("mica"), true); +agent->setWindowAttribute(QStringLiteral("mica-alt"), false); +agent->setWindowAttribute(QStringLiteral("acrylic-material"), false); +agent->setWindowAttribute(QStringLiteral("dwm-blur"), false); +agent->setWindowAttribute(QStringLiteral("dwm-border-color"), QColor("#3367d6")); +``` +macOS examples: -### Vulnerabilities +```cpp +agent->setWindowAttribute(QStringLiteral("no-system-buttons"), false); +agent->setWindowAttribute(QStringLiteral("blur-effect"), QStringLiteral("dark")); +agent->setWindowAttribute(QStringLiteral("glass-effect"), QStringLiteral("regular")); +agent->setWindowAttribute(QStringLiteral("glass-corner-radius"), 24.0); +agent->setWindowAttribute(QStringLiteral("glass-tint-color"), QColor(255, 255, 255, 46)); +``` -#### Qt Version -- To achieve better frameless functionality, QWindowKit depends heavily on Qt's internal implementation. However, there are many differences in different versions of Qt, and earlier versions of Qt5 and Qt6 have many bugs which make it extremely difficult for QWindowKit to workaround without changing the Qt source code. -- And also due to limited manpower, although QWindowKit can be successfully compiled on Qt 5.12 or later, it can hardly work perfectly on all Qt versions. -- Therefore, the following Qt version ranges are recommended, if there are any exceptions with QWindowKit in your application, make sure the Qt version you use is in the ranges before raising the issue. - - Qt 5: 5.15.2 or higher (you may be able to build QWK on top of older Qt versions, however, QWK may not behave well and we won't accept bug reports from these unsupported versions) - - Qt 6: 6.6.2 or higher (the newer, the better) +See [the English tutorial](./docs/tutorial.en-US.md) for more complete examples. -#### Hot Switch -- Once you have made the window frameless, it will not be able to switch back to the system frame again unless you destroy your window and recreate it with different settings. +## Important Notes -- Do not use `QWidget::setWindowFlags` to change the window flags after `windowAgent.setup()` is called. As a result, `QWK::WidgetWindowAgent` can not be setup on `QDockWidget`. +### Qt Version Compatibility -#### Native Child Widget -- If you are about to add a widget with `Qt::WA_NativeWindow` property enabled as a descendent of the frameless window, you should enable `Qt::WA_DontCreateNativeAncestors` of it in advance. +QWindowKit relies on Qt private implementation details to provide better frameless behavior. The project may compile with older Qt versions, but unsupported Qt versions can still have runtime issues caused by Qt internals or platform plugins. -#### Size Constrains -- If you want to disable window resizing, you can set a fixed size, which is officially supported by QWindowKit. If you use other special means to achieve this (eg. hook Win32 messages), QWK doesn't guarantee everything can still be fully functional. -- If you set a maximized width or height, the window should not be maximized because you cannot get the correct window size through Qt APIs. You may workaround this by using system APIs such as `GetWindowRect` or `GetClientRect`. The root cause lies deep in Qt QPA implementations and currently we don't know how to fix it without modifying Qt itself. +Use Qt 5.15.2+ or Qt 6.6.2+ when possible. -#### Windows 10 -- Due to the inherent defects in the Windows 10 window system, the top border will disappear when the system title bar is removed. We have filtered Qt's event and perfectly reshown the system top border, thanks to the implementation of Windows Terminal for our reference. However, this workaround only works with QtWidgets and QtQuick (**only when rendering through OpenGL/D3D11/D3D12, not Vulkan**) applications. -- For QtQuick applications, when rendering through Vulkan, the top border will become a solid black line, that's a known issue and currently we are not able to fix it. Please use QWK's borderless version if you can't change your graphics backend. -- For QtQuick applications, when rendering through D3D11/D3D12, you may see a strange white line on window top, it may disappear if you resize the window. Currently it's a bug and we are working hard to find a suitable solution for it, for now you can set the environment variable `QT_QPA_DISABLE_REDIRECTION_SURFACE` to a non-zero value in your `main` function before any `QCoreApplication` instance is created to workaround this issue. This environment variable is first introduced in Qt 6.7.0 (qtbase/838fc606c170fac112f7bb5971c2507b7b56d08a). You must **NOT** enable this feature for OpenGL/Vulkan because their rendering will be totally broken. +### Setup Timing -## TODO +Call `setup()` as early as possible, preferably near the beginning of the top-level window constructor. If you need minimum, maximum, or fixed size constraints, apply them after QWindowKit has been set up. -- Fix mouse cursor mapping issues -- More documentations -- When do we support Linux native features? +### Runtime Frame Switching -## Special Thanks +Do not switch back to the native system frame at runtime. Recreate the window if your application needs to change between native and custom frames. -- [Maplespe](https://github.com/Maplespe) -- [zhiyiYo](https://github.com/zhiyiYo) +Do not call `QWidget::setWindowFlags()` to change window flags after `WidgetWindowAgent::setup()`. -## License +### Native Child Widgets + +If a child widget uses `Qt::WA_NativeWindow`, enable `Qt::WA_DontCreateNativeAncestors` before creating native ancestors. + +### Size Constraints + +Fixed-size windows are supported. Avoid maximizing a window with a constrained maximum width or height, because Qt may not report the native maximized geometry correctly. -QWindowKit is licensed under the [Apache 2.0 License](./LICENSE). +### Windows 10 And Qt Quick - +QWindowKit is licensed under the [Apache License 2.0](./LICENSE). diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..95c27a2 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,383 @@ +# QWindowKit + +**语言:** 简体中文 | [English](./README.md) + +**教程:** [简体中文教程](./docs/tutorial.zh-CN.md) | [English tutorial](./docs/tutorial.en-US.md) + +QWindowKit 是一个面向 Qt Widgets 和 Qt Quick 的跨平台窗口定制框架。它可以帮助应用使用自定义标题栏替换系统标题栏,同时尽量保留窗口移动、缩放、系统菜单、Windows Snap Layout 等重要系统行为。 + +本项目继承了 [wangwenx190 FramelessHelper](https://github.com/wangwenx190/framelesshelper) 的核心思路和部分实现脉络,并在架构和 API 设计上进行了重构。 + +## 功能特性 + +- 支持 Qt Widgets 和 Qt Quick 自定义标题栏。 +- 配置系统按钮角色后,支持 Windows 11 Snap Layout。 +- 支持 Windows 10/11 无边框窗口处理,并提供可选的系统边框 workaround。 +- 支持 macOS 原生系统按钮位置控制,以及可选的模糊和玻璃效果。 +- 支持 Linux fallback 实现,并在 Qt 6 Wayland/X11 环境中提供部分原生系统菜单能力。 +- 提供 CMake package、qmake 集成文件和 Visual Studio property 文件。 + +## 支持平台 + +- Microsoft Windows。 +- Apple macOS 11 或更高版本。 +- GNU/Linux。 + +## 截图 + +### Windows 11 + +![Windows 11](./docs/images/win11.png) + +### Windows 10 + +![Windows 10](./docs/images/win10.png) + +### macOS + +![macOS](./docs/images/mac.png) + +| 默认 | 玻璃效果 - regular | +|:--:|:--:| +| ![Default](./docs/images/macos/01%20default.png) | ![Glass regular](./docs/images/macos/02%20glass%20-%20regular.png) | +| 玻璃效果 - clear | 玻璃效果 - regular, rounded | +| ![Glass clear](./docs/images/macos/03%20glass%20-%20clear.png) | ![Glass regular rounded](./docs/images/macos/04%20glass%20-%20regular,%20rounded.png) | +| 玻璃效果 - regular, dark tint | 玻璃效果 - regular, light tint | +| ![Glass dark tint](./docs/images/macos/05%20glass%20-%20regular,%20dark%20tint.png) | ![Glass light tint](./docs/images/macos/06%20glass%20-%20regular,%20light%20tint.png) | +| 旧版效果 - dark blur | 旧版效果 - light blur | +| ![Legacy dark blur](./docs/images/macos/07%20legacy%20-%20dark%20blur.png) | ![Legacy light blur](./docs/images/macos/08%20legacy%20-%20light%20blur.png) | + +### Linux + +![Linux](./docs/images/linux.png) + +## 环境要求 + +| 组件 | 要求 | 说明 | +|:--|:--|:--| +| Qt | 5.12 或更高版本 | 必需 Qt Core 和 Gui。Widgets 与 Quick 是可选模块。 | +| C++ | C++17 或更高版本 | 已测试 MSVC、GCC 和 Clang。 | +| CMake | 3.19 或更高版本 | 推荐 CMake 3.20 或更高版本。 | + +推荐 Qt 版本: + +- Qt 5:5.15.2 或更高版本。 +- Qt 6:6.6.2 或更高版本。 + +QWindowKit 使用了 Qt 私有 API 和平台相关窗口行为。较旧 Qt 版本可能可以编译,但运行时仍可能受到 Qt 内部实现或平台插件问题影响。 + +已测试编译器: + +- Windows:MSVC 2019、MSVC 2022、MinGW GCC 13.2.0。 +- macOS:Clang 14.0.3。 +- Ubuntu:GCC 9.4.0。 + +## 模块 + +| 模块 | Target | 用途 | +|:--|:--|:--| +| Core | `QWindowKit::Core` | 共享的平台窗口基础设施。 | +| Widgets | `QWindowKit::Widgets` | 用于 `QWidget` 和 `QMainWindow`。 | +| Quick | `QWindowKit::Quick` | 用于 `QQuickWindow` 和 QML `Window`。 | + +`QWINDOWKIT_BUILD_WIDGETS` 默认启用。`QWINDOWKIT_BUILD_QUICK` 默认关闭,如果需要 Qt Quick 支持,需要显式启用。 + +## 构建和安装 + +克隆仓库及 submodule: + +```sh +git clone --recursive https://github.com/stdware/qwindowkit +cd qwindowkit +``` + +配置、构建并安装: + +```sh +cmake -S . -B build \ + -DCMAKE_PREFIX_PATH= \ + -DCMAKE_INSTALL_PREFIX= \ + -DQWINDOWKIT_BUILD_WIDGETS=ON \ + -DQWINDOWKIT_BUILD_QUICK=ON + +cmake --build build --config Release +cmake --install build --config Release +``` + +常用构建选项: + +| 选项 | 默认值 | 说明 | +|:--|:--|:--| +| `QWINDOWKIT_BUILD_STATIC` | `OFF` | 构建静态库而不是动态库。 | +| `QWINDOWKIT_BUILD_WIDGETS` | `ON` | 构建 Widgets 模块。 | +| `QWINDOWKIT_BUILD_QUICK` | `OFF` | 构建 Quick 模块。 | +| `QWINDOWKIT_BUILD_EXAMPLES` | `OFF` | 构建示例。 | +| `QWINDOWKIT_BUILD_DOCUMENTATIONS` | `OFF` | 构建 Doxygen 文档。 | +| `QWINDOWKIT_INSTALL` | `ON` | 生成安装目标和 package 文件。 | +| `QWINDOWKIT_FORCE_QT_WINDOW_CONTEXT` | `OFF` | 强制使用纯 Qt fallback 实现。 | +| `QWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS` | `ON` | 启用 Windows 系统边框 workaround。 | +| `QWINDOWKIT_ENABLE_STYLE_AGENT` | `ON` | 构建样式和主题辅助组件。 | + +项目依赖 `qmsetup`。如果 CMake 找不到已安装的 `qmsetup` package,会自动使用仓库中的 submodule。 + +## 集成到你的项目 + +### CMake + +配置应用时指定安装后的 package 路径: + +```sh +cmake -S . -B build -DQWindowKit_DIR=/lib/cmake/QWindowKit +``` + +Widgets 应用: + +```cmake +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) +find_package(QWindowKit REQUIRED COMPONENTS Widgets) + +target_link_libraries(my_widgets_app PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + QWindowKit::Widgets +) +``` + +Quick 应用: + +```cmake +find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick Qml) +find_package(QWindowKit REQUIRED COMPONENTS Quick) + +target_link_libraries(my_quick_app PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Quick + Qt6::Qml + QWindowKit::Quick +) +``` + +如果你将 QWindowKit 作为源码依赖放入项目,也可以通过 CMake `add_subdirectory()` 集成。 + +### qmake + +先使用 CMake 安装 QWindowKit,然后包含生成的 `.pri` 文件: + +```qmake +# Widgets +include("/share/QWindowKit/qmake/QWKWidgets.pri") + +# Quick +include("/share/QWindowKit/qmake/QWKQuick.pri") +``` + +### Visual Studio + +MSBuild 项目请参考 [Visual Studio Guide](./docs/visual-studio-guide.md)。 + +## 快速开始:Qt Widgets + +在构造 `QApplication` 前设置 `Qt::AA_DontCreateNativeWidgetSiblings`: + +```cpp +#include +#include + +int main(int argc, char *argv[]) +{ + QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + + QApplication app(argc, argv); + // 创建并显示主窗口。 + return app.exec(); +} +``` + +为每个需要自定义边框的顶层 widget 安装 `WidgetWindowAgent`: + +```cpp +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + auto *agent = new QWK::WidgetWindowAgent(this); + agent->setup(this); + + auto *titleBar = new QWidget(this); + setMenuWidget(titleBar); + + agent->setTitleBar(titleBar); +} +``` + +注册系统按钮角色,使 Windows Snap Layout 等原生能力可以正常工作: + +```cpp +agent->setSystemButton(QWK::WindowAgentBase::WindowIcon, iconButton); +agent->setSystemButton(QWK::WindowAgentBase::Minimize, minimizeButton); +agent->setSystemButton(QWK::WindowAgentBase::Maximize, maximizeButton); +agent->setSystemButton(QWK::WindowAgentBase::Close, closeButton); +``` + +按钮点击行为需要自行连接: + +```cpp +connect(minimizeButton, &QPushButton::clicked, this, &QWidget::showMinimized); +connect(maximizeButton, &QPushButton::clicked, this, [this] { + isMaximized() ? showNormal() : showMaximized(); +}); +connect(closeButton, &QPushButton::clicked, this, &QWidget::close); +``` + +标题栏中的交互控件需要标记为 hit-test visible: + +```cpp +agent->setHitTestVisible(menuBar, true); +agent->setHitTestVisible(searchBox, true); +``` + +标题栏中未标记为 hit-test visible 的区域会被视为窗口拖拽区域。 + +## 快速开始:Qt Quick + +加载 QML 前注册 QML 类型: + +```cpp +#include +#include +#include + +#include + +int main(int argc, char *argv[]) +{ + QQuickWindow::setDefaultAlphaBuffer(true); + + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + QWK::registerTypes(&engine); + engine.load(QUrl(QStringLiteral("qrc:/Main.qml"))); + + return app.exec(); +} +``` + +在 QML 中使用 `WindowAgent`: + +```qml +import QtQuick +import QtQuick.Controls +import QtQuick.Window +import QWindowKit 1.0 + +Window { + id: window + width: 900 + height: 600 + visible: false + + WindowAgent { + id: windowAgent + } + + Rectangle { + id: titleBar + anchors.top: parent.top + width: parent.width + height: 40 + } + + Component.onCompleted: { + windowAgent.setup(window) + windowAgent.setTitleBar(titleBar) + window.visible = true + } +} +``` + +在 Qt 6 中,如果你的 QML 风格允许无版本 import,可以省略 import 版本号。 + +## 平台属性 + +`WindowAgentBase::setWindowAttribute()` 提供平台相关能力。不受支持的属性会返回 `false`。 + +Windows 示例: + +```cpp +agent->setWindowAttribute(QStringLiteral("dark-mode"), true); +agent->setWindowAttribute(QStringLiteral("mica"), true); +agent->setWindowAttribute(QStringLiteral("mica-alt"), false); +agent->setWindowAttribute(QStringLiteral("acrylic-material"), false); +agent->setWindowAttribute(QStringLiteral("dwm-blur"), false); +agent->setWindowAttribute(QStringLiteral("dwm-border-color"), QColor("#3367d6")); +``` + +macOS 示例: + +```cpp +agent->setWindowAttribute(QStringLiteral("no-system-buttons"), false); +agent->setWindowAttribute(QStringLiteral("blur-effect"), QStringLiteral("dark")); +agent->setWindowAttribute(QStringLiteral("glass-effect"), QStringLiteral("regular")); +agent->setWindowAttribute(QStringLiteral("glass-corner-radius"), 24.0); +agent->setWindowAttribute(QStringLiteral("glass-tint-color"), QColor(255, 255, 255, 46)); +``` + +更完整的示例请参考[简体中文教程](./docs/tutorial.zh-CN.md)。 + +## 重要说明 + +### Qt 版本兼容性 + +QWindowKit 依赖 Qt 私有实现细节来提供更好的无边框窗口行为。项目可能可以在较旧 Qt 版本上编译,但不受支持的 Qt 版本仍可能因为 Qt 内部实现或平台插件问题导致运行时异常。 + +建议尽量使用 Qt 5.15.2+ 或 Qt 6.6.2+。 + +### setup 调用时机 + +尽量尽早调用 `setup()`,通常应放在顶层窗口构造函数的前半段。如果窗口需要最小尺寸、最大尺寸或固定尺寸约束,建议在 QWindowKit 初始化后再设置。 + +### 运行时切换窗口边框 + +不要在运行时切回系统原生边框。如果应用需要在原生边框和自定义边框之间切换,应销毁并重建窗口。 + +`WidgetWindowAgent::setup()` 之后不要再通过 `QWidget::setWindowFlags()` 修改窗口 flags。 + +### 原生子控件 + +如果子 widget 使用 `Qt::WA_NativeWindow`,应在创建原生祖先窗口前启用 `Qt::WA_DontCreateNativeAncestors`。 + +### 尺寸约束 + +固定尺寸窗口是受支持的。避免最大化设置了最大宽度或最大高度约束的窗口,因为 Qt 可能无法正确报告原生最大化几何信息。 + +### Windows 10 和 Qt Quick + +Windows 10 顶部边框 workaround 适用于 Qt Widgets,也适用于使用 OpenGL、D3D11 或 D3D12 渲染的 Qt Quick。Vulkan 存在已知限制。 + +对于使用 D3D11/D3D12 的 Qt Quick,部分环境中窗口顶部可能出现白线。在 Qt 6.7 或更高版本中,可以在创建 `QCoreApplication` 前设置 `QT_QPA_DISABLE_REDIRECTION_SURFACE=1` 尝试规避。不要在 OpenGL 或 Vulkan 后端启用该变量。 + +## 文档 + +- [English tutorial](./docs/tutorial.en-US.md) +- [简体中文教程](./docs/tutorial.zh-CN.md) +- [Visual Studio Guide](./docs/visual-studio-guide.md) +- [FramelessHelper Related](./docs/framelesshelper-related.md) +- [示例](./examples) + +## 社区 + +- Discord: +- 中文用户 QQ 群:876419693 + +## 鸣谢 + +- [Maplespe](https://github.com/Maplespe) +- [zhiyiYo](https://github.com/zhiyiYo) + +## 许可证 + +QWindowKit 使用 [Apache License 2.0](./LICENSE) 许可证。 diff --git a/docs/tutorial.en-US.md b/docs/tutorial.en-US.md new file mode 100644 index 0000000..372c910 --- /dev/null +++ b/docs/tutorial.en-US.md @@ -0,0 +1,617 @@ +# QWindowKit Tutorial + +**Language:** English | [简体中文](./tutorial.zh-CN.md) + +**Project README:** [English](../README.md) | [简体中文](../README.zh-CN.md) + +This tutorial shows how to use QWindowKit in Qt Widgets and Qt Quick applications to build a custom title bar while keeping native behaviors such as moving, resizing, system menus, and Windows Snap Layout. + +QWindowKit provides three modules: + +- `QWindowKit::Core`: shared platform window infrastructure. +- `QWindowKit::Widgets`: integration for `QWidget` and `QMainWindow`. +- `QWindowKit::Quick`: integration for `QQuickWindow` and QML `Window`. + +## Requirements + +- Qt 5.12 or later. For real applications, Qt 5.15.2+ or Qt 6.6.2+ is recommended. +- A C++17 compiler. +- CMake 3.19 or later. +- Windows, macOS, or Linux. + +## Build And Install + +Build from source: + +```sh +git clone --recursive https://github.com/stdware/qwindowkit +cd qwindowkit + +cmake -S . -B build ^ + -DCMAKE_PREFIX_PATH=C:/Qt/6.7.3/msvc2019_64 ^ + -DCMAKE_INSTALL_PREFIX=C:/Libraries/QWindowKit ^ + -DQWINDOWKIT_BUILD_WIDGETS=ON ^ + -DQWINDOWKIT_BUILD_QUICK=ON ^ + -DQWINDOWKIT_BUILD_EXAMPLES=OFF + +cmake --build build --config Release +cmake --install build --config Release +``` + +On Linux or macOS: + +```sh +cmake -S . -B build \ + -DCMAKE_PREFIX_PATH=/path/to/Qt \ + -DCMAKE_INSTALL_PREFIX=/opt/qwindowkit \ + -DQWINDOWKIT_BUILD_WIDGETS=ON \ + -DQWINDOWKIT_BUILD_QUICK=ON + +cmake --build build --config Release +cmake --install build --config Release +``` + +Common options: + +```cmake +-DQWINDOWKIT_BUILD_WIDGETS=ON +-DQWINDOWKIT_BUILD_QUICK=ON +-DQWINDOWKIT_BUILD_EXAMPLES=ON +-DQWINDOWKIT_BUILD_STATIC=OFF +-DQWINDOWKIT_FORCE_QT_WINDOW_CONTEXT=OFF +-DQWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS=ON +``` + +## Use From CMake + +Widgets application: + +```cmake +cmake_minimum_required(VERSION 3.19) +project(MyWidgetsApp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) +find_package(QWindowKit REQUIRED COMPONENTS Widgets) + +qt_add_executable(MyWidgetsApp + main.cpp + mainwindow.cpp + mainwindow.h +) + +target_link_libraries(MyWidgetsApp PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + QWindowKit::Widgets +) +``` + +Quick application: + +```cmake +cmake_minimum_required(VERSION 3.19) +project(MyQuickApp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick Qml) +find_package(QWindowKit REQUIRED COMPONENTS Quick) + +qt_add_executable(MyQuickApp + main.cpp +) + +qt_add_qml_module(MyQuickApp + URI MyQuickApp + VERSION 1.0 + QML_FILES Main.qml +) + +target_link_libraries(MyQuickApp PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Quick + Qt6::Qml + QWindowKit::Quick +) +``` + +Configure your downstream project with the installed package path: + +```sh +cmake -S . -B build -DQWindowKit_DIR=C:/Libraries/QWindowKit/lib/cmake/QWindowKit +``` + +## Widgets: Minimal Window + +Widgets applications must set `Qt::AA_DontCreateNativeWidgetSiblings` before constructing `QApplication`. + +`main.cpp`: + +```cpp +#include +#include + +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + + QApplication app(argc, argv); + + MainWindow window; + window.resize(900, 600); + window.show(); + + return app.exec(); +} +``` + +`mainwindow.h`: + +```cpp +#pragma once + +#include + +namespace QWK { +class WidgetWindowAgent; +} + +class QWidget; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +private: + void setupWindowAgent(); + + QWK::WidgetWindowAgent *m_windowAgent = nullptr; +}; +``` + +`mainwindow.cpp`: + +```cpp +#include "mainwindow.h" + +#include +#include +#include +#include +#include + +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + setupWindowAgent(); + + auto *content = new QLabel(tr("Application content"), this); + content->setAlignment(Qt::AlignCenter); + setCentralWidget(content); +} + +MainWindow::~MainWindow() = default; + +void MainWindow::setupWindowAgent() +{ + m_windowAgent = new QWK::WidgetWindowAgent(this); + m_windowAgent->setup(this); + + auto *titleBar = new QWidget(this); + titleBar->setFixedHeight(40); + + auto *iconButton = new QPushButton(QStringLiteral("App"), this); + auto *menuBar = new QMenuBar(this); + menuBar->addMenu(tr("&File"))->addAction(tr("E&xit"), this, &QWidget::close); + + auto *titleLabel = new QLabel(windowTitle(), this); + titleLabel->setAlignment(Qt::AlignCenter); + + auto *minButton = new QPushButton(QStringLiteral("-"), this); + auto *maxButton = new QPushButton(QStringLiteral("□"), this); + auto *closeButton = new QPushButton(QStringLiteral("x"), this); + + auto *layout = new QHBoxLayout(titleBar); + layout->setContentsMargins(8, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(iconButton); + layout->addWidget(menuBar); + layout->addWidget(titleLabel, 1); + layout->addWidget(minButton); + layout->addWidget(maxButton); + layout->addWidget(closeButton); + + setMenuWidget(titleBar); + + m_windowAgent->setTitleBar(titleBar); + m_windowAgent->setSystemButton(QWK::WindowAgentBase::WindowIcon, iconButton); + m_windowAgent->setSystemButton(QWK::WindowAgentBase::Minimize, minButton); + m_windowAgent->setSystemButton(QWK::WindowAgentBase::Maximize, maxButton); + m_windowAgent->setSystemButton(QWK::WindowAgentBase::Close, closeButton); + m_windowAgent->setHitTestVisible(menuBar, true); + + connect(minButton, &QPushButton::clicked, this, &QWidget::showMinimized); + connect(maxButton, &QPushButton::clicked, this, [this] { + isMaximized() ? showNormal() : showMaximized(); + }); + connect(closeButton, &QPushButton::clicked, this, &QWidget::close); +} +``` + +Key points: + +- Each top-level window needs its own `WidgetWindowAgent`. +- Call `setup(this)` as early as possible, especially before applying size constraints. +- `setTitleBar()` marks the draggable title bar area. +- Controls inside the title bar that need mouse input must be registered with `setHitTestVisible(widget, true)`. +- System button hints do not connect click behavior. You still connect the buttons yourself. + +## Widgets: Interactive Title Bar Controls + +Menus should be hit-test visible: + +```cpp +auto *menuBar = new QMenuBar(this); +menuBar->addMenu(tr("&File")); + +windowAgent->setHitTestVisible(menuBar, true); +``` + +Use the same rule for search boxes, tab bars, and tool buttons: + +```cpp +windowAgent->setHitTestVisible(searchEdit, true); +windowAgent->setHitTestVisible(tabBar, true); +windowAgent->setHitTestVisible(settingsButton, true); +``` + +Without this, QWindowKit treats those areas as draggable title bar space, so the controls will not receive normal mouse interaction. + +## Widgets: Window Buttons + +Connect button behavior explicitly: + +```cpp +connect(minimizeButton, &QPushButton::clicked, window, &QWidget::showMinimized); + +connect(maximizeButton, &QPushButton::clicked, window, [window] { + if (window->isMaximized()) { + window->showNormal(); + } else { + window->showMaximized(); + } +}); + +connect(closeButton, &QPushButton::clicked, window, &QWidget::close); +``` + +Disable maximization: + +```cpp +setWindowFlag(Qt::WindowMaximizeButtonHint, false); +``` + +Create a fixed-size window: + +```cpp +setFixedSize(640, 420); +``` + +## Quick: Register The QML Types + +`main.cpp`: + +```cpp +#include +#include +#include + +#include + +int main(int argc, char *argv[]) +{ + QQuickWindow::setDefaultAlphaBuffer(true); + + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + QWK::registerTypes(&engine); + + engine.load(QUrl(QStringLiteral("qrc:/Main.qml"))); + if (engine.rootObjects().isEmpty()) { + return 1; + } + + return app.exec(); +} +``` + +`QQuickWindow::setDefaultAlphaBuffer(true)` is usually required for transparency, blur, Mica, and glass effects. Call it before creating Quick windows. + +## Quick: Minimal QML Window + +`Main.qml`: + +```qml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import QWindowKit 1.0 + +Window { + id: window + width: 900 + height: 600 + visible: false + title: "QWindowKit Quick Window" + + WindowAgent { + id: windowAgent + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + id: titleBar + Layout.fillWidth: true + height: 42 + color: "#242936" + + RowLayout { + anchors.fill: parent + spacing: 0 + + Text { + text: window.title + color: "white" + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + Layout.leftMargin: 12 + Layout.fillWidth: true + } + + Button { + id: minButton + text: "-" + onClicked: window.showMinimized() + } + + Button { + id: maxButton + text: "□" + onClicked: { + if (window.visibility === Window.Maximized) + window.showNormal() + else + window.showMaximized() + } + } + + Button { + id: closeButton + text: "x" + onClicked: window.close() + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "#f5f6f8" + + Text { + anchors.centerIn: parent + text: "Application content" + } + } + } + + Component.onCompleted: { + windowAgent.setup(window) + windowAgent.setTitleBar(titleBar) + windowAgent.setSystemButton(WindowAgent.Minimize, minButton) + windowAgent.setSystemButton(WindowAgent.Maximize, maxButton) + windowAgent.setSystemButton(WindowAgent.Close, closeButton) + window.visible = true + } +} +``` + +If a control in the title bar needs mouse input, mark it as hit-test visible: + +```qml +Button { + id: menuButton + text: "Menu" +} + +Component.onCompleted: { + windowAgent.setHitTestVisible(menuButton, true) +} +``` + +## Windows Platform Effects + +Windows supports several platform attributes. Set them after `setup()` succeeds. + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("dark-mode"), true); +windowAgent->setWindowAttribute(QStringLiteral("mica"), true); +windowAgent->setWindowAttribute(QStringLiteral("mica-alt"), false); +windowAgent->setWindowAttribute(QStringLiteral("acrylic-material"), false); +windowAgent->setWindowAttribute(QStringLiteral("dwm-blur"), false); +``` + +Set the Windows 11 border color: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("dwm-border-color"), QColor("#3367d6")); +``` + +Disable the system menu: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("no-system-menu"), true); +``` + +Show the system menu: + +```cpp +windowAgent->showSystemMenu(QCursor::pos()); +``` + +For Qt Quick on Windows 10/11 with D3D11 or D3D12, this workaround may help with a top white line. Keep it tied to the documented backend and Qt version constraints: + +```cpp +#if defined(Q_OS_WIN) && QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) +qputenv("QSG_RHI_BACKEND", "d3d11"); +qputenv("QT_QPA_DISABLE_REDIRECTION_SURFACE", "1"); +#endif +``` + +Do not enable `QT_QPA_DISABLE_REDIRECTION_SURFACE` with OpenGL or Vulkan. + +## macOS Platform Effects + +On macOS, prefer native system buttons unless you have a strong reason to draw your own. + +Show native system buttons: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("no-system-buttons"), false); +``` + +Hide native system buttons: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("no-system-buttons"), true); +``` + +Enable blur: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("blur-effect"), QStringLiteral("dark")); +windowAgent->setWindowAttribute(QStringLiteral("blur-effect"), QStringLiteral("light")); +windowAgent->setWindowAttribute(QStringLiteral("blur-effect"), QStringLiteral("none")); +``` + +On macOS 26 and later, Liquid Glass can be enabled when available: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("glass-effect"), QStringLiteral("regular")); +windowAgent->setWindowAttribute(QStringLiteral("glass-corner-radius"), 24.0); +windowAgent->setWindowAttribute(QStringLiteral("glass-tint-color"), QColor(255, 255, 255, 46)); +``` + +If the platform does not support an attribute, `setWindowAttribute()` returns `false`. + +## Linux Notes + +On Linux, QWindowKit can use the Qt fallback window context and provides partial system menu support on Qt 6 Wayland/X11. Behavior varies by desktop environment and window manager. + +For compatibility-first applications, consider: + +```cmake +-DQWINDOWKIT_FORCE_QT_WINDOW_CONTEXT=ON +``` + +## Troubleshooting + +### Title bar controls cannot be clicked + +Mark the control or its parent container as hit-test visible: + +```cpp +windowAgent->setHitTestVisible(button, true); +``` + +System buttons do not need an extra hit-test-visible call, but they do need a role: + +```cpp +windowAgent->setSystemButton(QWK::WindowAgentBase::Close, closeButton); +``` + +### Windows Snap Layout does not appear + +Windows 11 Snap Layout depends on the maximize button role: + +```cpp +windowAgent->setSystemButton(QWK::WindowAgentBase::Maximize, maxButton); +``` + +The window must also keep the maximize capability: + +```cpp +setWindowFlag(Qt::WindowMaximizeButtonHint, true); +``` + +### When should `setup()` be called? + +Call it early in the window constructor, before complex child widgets and size constraints when possible: + +```cpp +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + m_agent = new QWK::WidgetWindowAgent(this); + m_agent->setup(this); + + setMinimumSize(640, 480); + setupUi(); +} +``` + +In QML, call it in `Component.onCompleted`: + +```qml +Component.onCompleted: { + windowAgent.setup(window) + windowAgent.setTitleBar(titleBar) + window.visible = true +} +``` + +### Can an application switch back to the native frame at runtime? + +Do not rely on hot switching. Once QWindowKit has taken over the window frame, recreate the window if you need a different framing mode. + +### Can `QDockWidget` use `WidgetWindowAgent`? + +No. Install `WidgetWindowAgent` only on top-level windows. + +## Recommended Minimal Production Setup + +Widgets: + +```cpp +QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); +QApplication app(argc, argv); +``` + +Quick: + +```cpp +QQuickWindow::setDefaultAlphaBuffer(true); +QGuiApplication app(argc, argv); +QWK::registerTypes(&engine); +``` + +Do not copy debug environment variables from the examples into production code unless you explicitly need them. diff --git a/docs/tutorial.zh-CN.md b/docs/tutorial.zh-CN.md new file mode 100644 index 0000000..0ff4169 --- /dev/null +++ b/docs/tutorial.zh-CN.md @@ -0,0 +1,623 @@ +# QWindowKit 使用教程 + +**语言:** 简体中文 | [English](./tutorial.en-US.md) + +**项目 README:** [简体中文](../README.zh-CN.md) | [English](../README.md) + +本文面向希望在 Qt Widgets 或 Qt Quick 应用中自定义系统标题栏、实现无边框窗口、保留拖拽/缩放/系统菜单等原生行为的用户。 + +QWindowKit 分为三个模块: + +- `QWindowKit::Core`:公共基础和平台窗口上下文。 +- `QWindowKit::Widgets`:用于 `QWidget`/`QMainWindow`。 +- `QWindowKit::Quick`:用于 `QQuickWindow`/QML `Window`。 + +## 环境要求 + +- Qt 5.12 或更高版本。实际项目建议 Qt 5.15.2+ 或 Qt 6.6.2+。 +- C++17 编译器。 +- CMake 3.19 或更高版本。 +- Windows、macOS 或 Linux。 + +## 构建和安装 + +从源码构建并安装: + +```sh +git clone --recursive https://github.com/stdware/qwindowkit +cd qwindowkit + +cmake -S . -B build ^ + -DCMAKE_PREFIX_PATH=C:/Qt/6.7.3/msvc2019_64 ^ + -DCMAKE_INSTALL_PREFIX=C:/Libraries/QWindowKit ^ + -DQWINDOWKIT_BUILD_WIDGETS=ON ^ + -DQWINDOWKIT_BUILD_QUICK=ON ^ + -DQWINDOWKIT_BUILD_EXAMPLES=OFF + +cmake --build build --config Release +cmake --install build --config Release +``` + +Linux/macOS 写法通常是: + +```sh +cmake -S . -B build \ + -DCMAKE_PREFIX_PATH=/path/to/Qt \ + -DCMAKE_INSTALL_PREFIX=/opt/qwindowkit \ + -DQWINDOWKIT_BUILD_WIDGETS=ON \ + -DQWINDOWKIT_BUILD_QUICK=ON + +cmake --build build --config Release +cmake --install build --config Release +``` + +常用构建选项: + +```cmake +-DQWINDOWKIT_BUILD_WIDGETS=ON +-DQWINDOWKIT_BUILD_QUICK=ON +-DQWINDOWKIT_BUILD_EXAMPLES=ON +-DQWINDOWKIT_BUILD_STATIC=OFF +-DQWINDOWKIT_FORCE_QT_WINDOW_CONTEXT=OFF +-DQWINDOWKIT_ENABLE_WINDOWS_SYSTEM_BORDERS=ON +``` + +## 在 CMake 项目中使用 + +Widgets 应用: + +```cmake +cmake_minimum_required(VERSION 3.19) +project(MyWidgetsApp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) +find_package(QWindowKit REQUIRED COMPONENTS Widgets) + +qt_add_executable(MyWidgetsApp + main.cpp + mainwindow.cpp + mainwindow.h +) + +target_link_libraries(MyWidgetsApp PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Widgets + QWindowKit::Widgets +) +``` + +Quick 应用: + +```cmake +cmake_minimum_required(VERSION 3.19) +project(MyQuickApp LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Quick Qml) +find_package(QWindowKit REQUIRED COMPONENTS Quick) + +qt_add_executable(MyQuickApp + main.cpp +) + +qt_add_qml_module(MyQuickApp + URI MyQuickApp + VERSION 1.0 + QML_FILES Main.qml +) + +target_link_libraries(MyQuickApp PRIVATE + Qt6::Core + Qt6::Gui + Qt6::Quick + Qt6::Qml + QWindowKit::Quick +) +``` + +配置下游项目时指定安装路径: + +```sh +cmake -S . -B build -DQWindowKit_DIR=C:/Libraries/QWindowKit/lib/cmake/QWindowKit +``` + +## Widgets:最小可用窗口 + +Widgets 应用必须在创建 `QApplication` 前设置 `Qt::AA_DontCreateNativeWidgetSiblings`。 + +`main.cpp`: + +```cpp +#include +#include + +#include "mainwindow.h" + +int main(int argc, char *argv[]) +{ + QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + + QApplication app(argc, argv); + + MainWindow window; + window.resize(900, 600); + window.show(); + + return app.exec(); +} +``` + +`mainwindow.h`: + +```cpp +#pragma once + +#include + +namespace QWK { +class WidgetWindowAgent; +} + +class QPushButton; +class QWidget; + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + explicit MainWindow(QWidget *parent = nullptr); + ~MainWindow() override; + +private: + void setupWindowAgent(); + + QWK::WidgetWindowAgent *m_windowAgent = nullptr; +}; +``` + +`mainwindow.cpp`: + +```cpp +#include "mainwindow.h" + +#include +#include +#include +#include +#include +#include + +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + setupWindowAgent(); + + auto *content = new QLabel(tr("Application content"), this); + content->setAlignment(Qt::AlignCenter); + setCentralWidget(content); +} + +MainWindow::~MainWindow() = default; + +void MainWindow::setupWindowAgent() +{ + m_windowAgent = new QWK::WidgetWindowAgent(this); + m_windowAgent->setup(this); + + auto *titleBar = new QWidget(this); + titleBar->setObjectName(QStringLiteral("TitleBar")); + titleBar->setFixedHeight(40); + + auto *iconButton = new QPushButton(this); + iconButton->setText(QStringLiteral("App")); + + auto *titleLabel = new QLabel(windowTitle(), this); + titleLabel->setAlignment(Qt::AlignCenter); + + auto *menuBar = new QMenuBar(this); + auto *fileMenu = menuBar->addMenu(tr("&File")); + fileMenu->addAction(tr("E&xit"), this, &QWidget::close); + + auto *minButton = new QPushButton(QStringLiteral("-"), this); + auto *maxButton = new QPushButton(QStringLiteral("□"), this); + auto *closeButton = new QPushButton(QStringLiteral("x"), this); + + auto *layout = new QHBoxLayout(titleBar); + layout->setContentsMargins(8, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(iconButton); + layout->addWidget(menuBar); + layout->addWidget(titleLabel, 1); + layout->addWidget(minButton); + layout->addWidget(maxButton); + layout->addWidget(closeButton); + + setMenuWidget(titleBar); + + m_windowAgent->setTitleBar(titleBar); + m_windowAgent->setSystemButton(QWK::WindowAgentBase::WindowIcon, iconButton); + m_windowAgent->setSystemButton(QWK::WindowAgentBase::Minimize, minButton); + m_windowAgent->setSystemButton(QWK::WindowAgentBase::Maximize, maxButton); + m_windowAgent->setSystemButton(QWK::WindowAgentBase::Close, closeButton); + m_windowAgent->setHitTestVisible(menuBar, true); + + connect(minButton, &QPushButton::clicked, this, &QWidget::showMinimized); + connect(maxButton, &QPushButton::clicked, this, [this] { + isMaximized() ? showNormal() : showMaximized(); + }); + connect(closeButton, &QPushButton::clicked, this, &QWidget::close); +} +``` + +要点: + +- 每个顶层窗口都需要自己的 `WidgetWindowAgent`。 +- `setup(this)` 越早调用越好,尤其是窗口有最小/最大尺寸约束时。 +- `setTitleBar()` 指定可拖拽标题栏区域。 +- 标题栏内需要接收鼠标事件的控件必须调用 `setHitTestVisible(widget, true)`。 +- 系统按钮提示只告诉 QWindowKit 这些控件的角色;按钮点击行为仍需要自己连接。 + +## Widgets:常见标题栏控件 + +菜单栏应设置为 hit-test visible: + +```cpp +auto *menuBar = new QMenuBar(this); +menuBar->addMenu(tr("&File")); + +windowAgent->setHitTestVisible(menuBar, true); +``` + +搜索框、标签页、工具按钮也一样: + +```cpp +windowAgent->setHitTestVisible(searchEdit, true); +windowAgent->setHitTestVisible(tabBar, true); +windowAgent->setHitTestVisible(settingsButton, true); +``` + +如果不这样做,这些控件所在区域会被当成拖拽区域,用户无法正常点击控件。 + +## Widgets:窗口按钮行为 + +QWindowKit 不会自动连接按钮行为。建议显式连接: + +```cpp +connect(minimizeButton, &QPushButton::clicked, window, &QWidget::showMinimized); + +connect(maximizeButton, &QPushButton::clicked, window, [window] { + if (window->isMaximized()) { + window->showNormal(); + } else { + window->showMaximized(); + } +}); + +connect(closeButton, &QPushButton::clicked, window, &QWidget::close); +``` + +禁用最大化: + +```cpp +setWindowFlag(Qt::WindowMaximizeButtonHint, false); +``` + +固定大小窗口: + +```cpp +setFixedSize(640, 420); +``` + +## Quick:注册 QML 类型 + +`main.cpp`: + +```cpp +#include +#include +#include + +#include + +int main(int argc, char *argv[]) +{ + QQuickWindow::setDefaultAlphaBuffer(true); + + QGuiApplication app(argc, argv); + + QQmlApplicationEngine engine; + QWK::registerTypes(&engine); + + engine.load(QUrl(QStringLiteral("qrc:/Main.qml"))); + if (engine.rootObjects().isEmpty()) { + return 1; + } + + return app.exec(); +} +``` + +`QQuickWindow::setDefaultAlphaBuffer(true)` 对 Windows/macOS 的透明、模糊、Mica、玻璃等效果通常是必要的,应该在创建 Quick 窗口前调用。 + +## Quick:最小 QML 窗口 + +`Main.qml`: + +```qml +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import QWindowKit 1.0 + +Window { + id: window + width: 900 + height: 600 + visible: false + title: "QWindowKit Quick Window" + + WindowAgent { + id: windowAgent + } + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + Rectangle { + id: titleBar + Layout.fillWidth: true + height: 42 + color: "#242936" + + RowLayout { + anchors.fill: parent + spacing: 0 + + Text { + text: window.title + color: "white" + elide: Text.ElideRight + verticalAlignment: Text.AlignVCenter + Layout.leftMargin: 12 + Layout.fillWidth: true + } + + Button { + id: minButton + text: "-" + onClicked: window.showMinimized() + } + + Button { + id: maxButton + text: window.visibility === Window.Maximized ? "□" : "□" + onClicked: { + if (window.visibility === Window.Maximized) + window.showNormal() + else + window.showMaximized() + } + } + + Button { + id: closeButton + text: "x" + onClicked: window.close() + } + } + } + + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + color: "#f5f6f8" + + Text { + anchors.centerIn: parent + text: "Application content" + } + } + } + + Component.onCompleted: { + windowAgent.setup(window) + windowAgent.setTitleBar(titleBar) + windowAgent.setSystemButton(WindowAgent.Minimize, minButton) + windowAgent.setSystemButton(WindowAgent.Maximize, maxButton) + windowAgent.setSystemButton(WindowAgent.Close, closeButton) + window.visible = true + } +} +``` + +如果标题栏内有可点击控件,例如菜单按钮或搜索框,需要设置 hit-test visible: + +```qml +Button { + id: menuButton + text: "Menu" +} + +Component.onCompleted: { + windowAgent.setHitTestVisible(menuButton, true) +} +``` + +## Windows 平台效果 + +Windows 支持一些平台属性。使用前应先完成 `setup()`。 + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("dark-mode"), true); +windowAgent->setWindowAttribute(QStringLiteral("mica"), true); +windowAgent->setWindowAttribute(QStringLiteral("mica-alt"), false); +windowAgent->setWindowAttribute(QStringLiteral("acrylic-material"), false); +windowAgent->setWindowAttribute(QStringLiteral("dwm-blur"), false); +``` + +设置 Windows 11 边框颜色: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("dwm-border-color"), QColor("#3367d6")); +``` + +关闭系统菜单: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("no-system-menu"), true); +``` + +显示系统菜单: + +```cpp +windowAgent->showSystemMenu(QCursor::pos()); +``` + +Qt Quick 在 Windows 10/11 使用 D3D11/D3D12 时,如果遇到顶部白线,可按 README 的限制条件设置: + +```cpp +#if defined(Q_OS_WIN) && QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) +qputenv("QSG_RHI_BACKEND", "d3d11"); +qputenv("QT_QPA_DISABLE_REDIRECTION_SURFACE", "1"); +#endif +``` + +不要在 OpenGL 或 Vulkan 后端下启用 `QT_QPA_DISABLE_REDIRECTION_SURFACE`。 + +## macOS 平台效果 + +默认建议使用 macOS 系统按钮,不必设置自绘关闭/最小化/缩放按钮。 + +显示系统按钮: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("no-system-buttons"), false); +``` + +隐藏系统按钮: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("no-system-buttons"), true); +``` + +启用模糊: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("blur-effect"), QStringLiteral("dark")); +windowAgent->setWindowAttribute(QStringLiteral("blur-effect"), QStringLiteral("light")); +windowAgent->setWindowAttribute(QStringLiteral("blur-effect"), QStringLiteral("none")); +``` + +macOS 26 及更高版本可尝试 Liquid Glass: + +```cpp +windowAgent->setWindowAttribute(QStringLiteral("glass-effect"), QStringLiteral("regular")); +windowAgent->setWindowAttribute(QStringLiteral("glass-corner-radius"), 24.0); +windowAgent->setWindowAttribute(QStringLiteral("glass-tint-color"), QColor(255, 255, 255, 46)); +``` + +如果平台不支持,`setWindowAttribute()` 会返回 `false`。 + +## Linux 说明 + +Linux 下 QWindowKit 优先使用 Qt 窗口上下文,并在 Qt 6 的 Wayland/X11 环境中提供部分系统菜单支持。不同桌面环境和窗口管理器行为差异较大,建议: + +```cmake +-DQWINDOWKIT_FORCE_QT_WINDOW_CONTEXT=ON +``` + +在兼容性优先的应用中可以强制使用 Qt fallback 实现。 + +## 常见问题 + +### 标题栏按钮无法点击 + +把按钮或其父容器设置为 hit-test visible: + +```cpp +windowAgent->setHitTestVisible(button, true); +``` + +系统按钮不需要额外设置 hit-test visible,但需要设置 system button 角色: + +```cpp +windowAgent->setSystemButton(QWK::WindowAgentBase::Close, closeButton); +``` + +### Snap Layout 不显示 + +Windows 11 的 Snap Layout 依赖最大化按钮的 system button 提示: + +```cpp +windowAgent->setSystemButton(QWK::WindowAgentBase::Maximize, maxButton); +``` + +同时窗口必须保留最大化能力: + +```cpp +setWindowFlag(Qt::WindowMaximizeButtonHint, true); +``` + +### 什么时候调用 `setup()` + +尽量在窗口构造函数前半段调用,早于尺寸约束和复杂子控件初始化: + +```cpp +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) +{ + m_agent = new QWK::WidgetWindowAgent(this); + m_agent->setup(this); + + setMinimumSize(640, 480); + setupUi(); +} +``` + +Quick 中通常在 `Component.onCompleted` 调用: + +```qml +Component.onCompleted: { + windowAgent.setup(window) + windowAgent.setTitleBar(titleBar) + window.visible = true +} +``` + +### 可以在运行时切回系统标题栏吗 + +不建议。QWindowKit 接管窗口后,不支持无损热切换回系统标题栏。需要销毁并重建窗口。 + +### 可以对 `QDockWidget` 使用吗 + +不建议。`WidgetWindowAgent` 只应安装在顶层窗口上。 + +## 推荐的最小生产配置 + +Widgets: + +```cpp +QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); +QApplication app(argc, argv); +``` + +Quick: + +```cpp +QQuickWindow::setDefaultAlphaBuffer(true); +QGuiApplication app(argc, argv); +QWK::registerTypes(&engine); +``` + +不要把示例中的调试环境变量直接复制到生产应用,除非你明确需要它们。