Language: English | 简体中文
Project README: English | 简体中文
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 forQWidgetandQMainWindow.QWindowKit::Quick: integration forQQuickWindowand QMLWindow.
- 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 from source:
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 ReleaseOn Linux or macOS:
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 ReleaseCommon options:
-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=ONWidgets application:
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_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:
cmake -S . -B build -DQWindowKit_DIR=C:/Libraries/QWindowKit/lib/cmake/QWindowKitWidgets applications must set Qt::AA_DontCreateNativeWidgetSiblings before constructing QApplication.
main.cpp:
#include <QtCore/QCoreApplication>
#include <QtWidgets/QApplication>
#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:
#pragma once
#include <QtWidgets/QMainWindow>
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:
#include "mainwindow.h"
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QLabel>
#include <QtWidgets/QMenuBar>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QWidget>
#include <QWKWidgets/widgetwindowagent.h>
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.
Menus should be hit-test visible:
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:
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.
Connect button behavior explicitly:
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:
setWindowFlag(Qt::WindowMaximizeButtonHint, false);Create a fixed-size window:
setFixedSize(640, 420);main.cpp:
#include <QtGui/QGuiApplication>
#include <QtQml/QQmlApplicationEngine>
#include <QtQuick/QQuickWindow>
#include <QWKQuick/qwkquickglobal.h>
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.
Main.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:
Button {
id: menuButton
text: "Menu"
}
Component.onCompleted: {
windowAgent.setHitTestVisible(menuButton, true)
}Windows supports several platform attributes. Set them after setup() succeeds.
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:
windowAgent->setWindowAttribute(QStringLiteral("dwm-border-color"), QColor("#3367d6"));Disable the system menu:
windowAgent->setWindowAttribute(QStringLiteral("no-system-menu"), true);Show the system menu:
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:
#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");
#endifDo not enable QT_QPA_DISABLE_REDIRECTION_SURFACE with OpenGL or Vulkan.
QWK's dark-mode attribute configures the native Windows appearance. It does not call QStyleHints::setColorScheme() or change the application's palette, QSS, or explicit Qt Quick Controls theme settings. Changing only this attribute can leave the window border and Qt controls using different color schemes.
QStyleHints::colorScheme() can read the current color scheme since Qt 6.5; the setter and resetter require Qt 6.8 or later. After creating QGuiApplication or QApplication, set Qt's color scheme where the application switches its theme, and update each window's QWK attribute separately:
#include <QtGui/QGuiApplication>
#include <QtGui/QStyleHints>
// dark is the application's selected theme; windowAgent has completed setup().
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
QGuiApplication::styleHints()->setColorScheme(
dark ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light);
#endif
#ifdef Q_OS_WIN
windowAgent->setWindowAttribute(QStringLiteral("dark-mode"), dark);
#endifsetColorScheme() affects the entire application, so it should not be set automatically from a single window's attribute. Windows can use different appearances, and QWK also enables dark-mode internally to handle Windows borders.
When the user selects "Follow system", call QGuiApplication::styleHints()->unsetColorScheme() on Qt 6.8 or later, and update window attributes and custom application colors when the system color scheme changes. The public API in Qt 6.5–6.7 only reads the color scheme. On older versions, applications need to manage control colors through palettes, QSS, or the theme mechanism of their Qt Quick Controls style.
Color scheme overrides are not supported on all platforms, and Qt does not overwrite palette colors explicitly set by the application. Custom QSS and explicit Qt Quick Controls theme settings still need to be synchronized by the application. See the official QStyleHints documentation.
On macOS, prefer native system buttons unless you have a strong reason to draw your own.
Show native system buttons:
windowAgent->setWindowAttribute(QStringLiteral("no-system-buttons"), false);Hide native system buttons:
windowAgent->setWindowAttribute(QStringLiteral("no-system-buttons"), true);Enable blur:
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:
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.
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:
-DQWINDOWKIT_FORCE_QT_WINDOW_CONTEXT=ONMark the control or its parent container as hit-test visible:
windowAgent->setHitTestVisible(button, true);System buttons do not need an extra hit-test-visible call, but they do need a role:
windowAgent->setSystemButton(QWK::WindowAgentBase::Close, closeButton);Windows 11 Snap Layout depends on the maximize button role:
windowAgent->setSystemButton(QWK::WindowAgentBase::Maximize, maxButton);The window must also keep the maximize capability:
setWindowFlag(Qt::WindowMaximizeButtonHint, true);Call it early in the window constructor, before complex child widgets and size constraints when possible:
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:
Component.onCompleted: {
windowAgent.setup(window)
windowAgent.setTitleBar(titleBar)
window.visible = true
}Do not rely on hot switching. Once QWindowKit has taken over the window frame, recreate the window if you need a different framing mode.
No. Install WidgetWindowAgent only on top-level windows.
Widgets:
QCoreApplication::setAttribute(Qt::AA_DontCreateNativeWidgetSiblings);
QApplication app(argc, argv);Quick:
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.