wangwenx190 / framelesshelper

Project moved to: https://github.com/stdware/qwindowkit Cross-platform window customization framework for Qt Widgets and Qt Quick. Supports Windows, Linux and macOS.
MIT License
849 stars 202 forks source link
dwm frame-shadow frameless frameless-helper frameless-windows qt win32 windows

ATTENTION! THIS REPO HAS BEEN DEPRECATED!

PLEASE USE THE BRAND NEW QWINDOWKIT INSTEAD!

THIS REPO IS NO LONGER MAINTAINED.

FramelessHelper 2.x

CI: Build Test

Cross-platform window customization framework for Qt Widgets and Qt Quick. Supports Windows, Linux and macOS.

Join with Us :triangular_flag_on_post:

You can join our Discord channel to communicate with us. You can share your findings, thoughts and ideas on improving / implementing FramelessHelper functionalities on more platforms and apps!

TODO

Highlights v2.5

Highlights v2.4

Highlights v2.3

Highlights v2.2

Highlights v2.1

Highlights v2.0

Screenshots

Windows

Light

Dark

Linux

Light

Dark

macOS

Light

Dark

Use Cases

QVogenClient

QVogenClient

Vogen editor using QSynthesis framework. Repository URL: https://gitee.com/functioner/qvogenclient.

Requiredments

Supported Platforms

There are some additional restrictions for each platform, please refer to the Platform notes section below.

Build

git clone --recursive https://github.com/wangwenx190/framelesshelper.git # "--recursive" is necessary to clone the submodules.
mkdir build # Please change to your own build directory!
cd build
cmake -DCMAKE_PREFIX_PATH=<YOUR_QT_SDK_DIR_PATH> -DCMAKE_INSTALL_PREFIX=<WHERE_YOU_WANT_TO_INSTALL> -DCMAKE_BUILD_TYPE=Release -GNinja <PATH_TO_THE_REPOSITORY>
cmake --build . --config Release --target all --parallel
cmake --install . --config Release --strip # Don't add "--strip" for MSVC/Clang-CL/Intel-CL toolchains!
# YOUR_QT_SDK_DIR_PATH: the Qt SDK directory, something like "C:/Qt/6.5.1/msvc2019_64" or "/opt/Qt/6.5.1/gcc_64". Please change to your own path!
# WHERE_YOU_WANT_TO_INSTALL: the install directory of FramelessHelper, something like "../install". You can ignore this setting if you don't need to install the CMake package. Please change to your own path!
# PATH_TO_THE_REPOSITORY: the source code directory of FramelessHelper, something like "../framelesshelper". Please change to your own path!

You can also use Qt6_DIR or Qt5_DIR to replace CMAKE_PREFIX_PATH:

cmake -DQt6_DIR=C:/Qt/6.5.1/msvc2019_64/lib/cmake/Qt6 [other parameters ...]
# Or
cmake -DQt5_DIR=C:/Qt/5.15.2/msvc2019_64/lib/cmake/Qt5 [other parameters ...]

If there are any errors when cloning the submodules, try run git submodule update --init --recursive --remote in the project directory, that command will download & update all the submodules. If it fails again, try execute it multiple times until it finally succeeds.

Once the compilation and installation is done, you will be able to use the find_package(FramelessHelper REQUIRED COMPONENTS Core Widgets Quick) command to find and link to the FramelessHelper library. But before doing that, please make sure CMake knows where to find FramelessHelper, by passing the CMAKE_PREFIX_PATH or FramelessHelper_DIR variable to it. For example: -DCMAKE_PREFIX_PATH=C:/my-cmake-packages;C:/my-toolchain;etc... or -DFramelessHelper_DIR=C:/Projects/FramelessHelper/lib64/cmake/FramelessHelper. Build FramelessHelper as a sub-directory of your CMake project is of course also supported. The supported FramelessHelper target names are FramelessHelper::Core, FramelessHelper::Widgets and FramelessHelper::Quick. Example code:

# Find Qt:
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
# Find FramelessHelper:
find_package(FramelessHelper REQUIRED COMPONENTS Core Widgets)
# Create your target:
add_executable(demo)
# Add your source code:
target_sources(demo PRIVATE main.cpp)
# Link to Qt and FramelessHelper:
target_link_libraries(demo PRIVATE
    Qt${QT_VERSION_MAJOR}::Widgets
    FramelessHelper::Core
    FramelessHelper::Widgets
)

If you need the syntax highlighting of FramelessHelper's Quick module, please set up the QML_IMPORT_PATH variable. Example code:

# This is the path where you want FramelessHelper's Quick plugin (it only contains the QML meta
# info and an optional dummy library, for QtCreator's QML tooling purpose, it's not the Quick
# module) to place. Please change to your own path!
# If you are using add_subdirectory() to include FramelessHelper directly, you can change it to
# "${PROJECT_BINARY_DIR}/imports" instead of the install location.
set(FRAMELESSHELPER_IMPORT_DIR "C:/packages/FramelessHelper/qml")
list(APPEND QML_IMPORT_PATH "${FRAMELESSHELPER_IMPORT_DIR}")
list(REMOVE_DUPLICATES QML_IMPORT_PATH)
# Force cache refresh:
set(QML_IMPORT_PATH ${QML_IMPORT_PATH} CACHE STRING "Qt Creator extra QML import paths" FORCE)

Use

Qt Widgets

To customize the window frame of a QWidget, you need to instantiate a FramelessWidgetsHelper object and then attach it to the widget's top level widget, and then FramelessWidgetsHelper will do all the rest work for you: the window frame will be removed automatically once it has been attached to the top level widget successfully. In theory you can instantiate multiple FramelessWidgetsHelper objects for a same widget, in this case there will be only one object that keeps functional, all other objects will become a wrapper of that one. But to make sure everything goes smoothly and normally, you should not do that in any case. The simplest way to instantiate a FramelessWidgetsHelper object is to call the static method FramelessWidgetsHelper *FramelessWidgetsHelper::get(QObject *). It will return the handle of the previously instantiated object if any, or it will instantiate a new object if it can't find one. It's safe to call this method multiple times for a same widget, it won't instantiate any new objects if there is one already. It also does not matter when and where you call that function as long as the top level widget is the same. The internally created objects will always be parented to the top level widget. Once you get the handle of the FramelessWidgetsHelper object, you can call void FramelessWidgetsHelper::extendsContentIntoTitleBar() to let it hide the default title bar provided by the operating system. In order to make sure FramelessWidgetsHelper can find the correct top level widget, you should call the FramelessWidgetsHelper *FramelessWidgetsHelper::get(QObject *) function on a widget which has a complete parent-chain whose root parent is the top level widget. To make the frameless window draggable, you should provide a homemade title bar widget yourself, the title bar widget doesn't need to be in rectangular shape, it also doesn't need to be placed on the first row of the window. Call void FramelessWidgetsHelper::setTitleBarWidget(QWidget *) to let FramelessHelper know what's your title bar widget. By default, all the widgets in the title bar area won't be responsible to any mouse and keyboard events due to they have been intercepted by FramelessHelper. To make them recover the responsible state, you should make them visible to hit test. Call void FramelessWidgetsHelper::setHitTestVisible(QWidget* ) to do that. You can of course call it on a widget that is not inside the title bar at all, it won't have any effect though. Due to Qt's own limitations, you need to make sure your widget has a complete parent-chain whose root parent is the top level widget. Do not ever try to delete the FramelessWidgetsHelper object, it may still be monitoring and controlling your widget, and Qt will delete it for you automatically. No need to worry about memory leaks.

There are also two classes called FramelessWidget and FramelessMainWindow, they are only simple wrappers of FramelessWidgetsHelper, which just saves the call of the void FramelessWidgetsHelper::extendsContentIntoTitleBar() function for you. You can absolutely use plain QWidget instead.

Code Snippet

First of all, call void FramelessHelper::Widgets::initialize() in your main function in a very early stage (MUST before the construction of any Q(Gui|Core)Application objects):

int main(int, char **)
{
    FramelessHelper::Widgets::initialize();
    // ...
}

Then hide the standard title bar provided by the OS:

MyWidget::MyWidget(QWidget *parent) : QWidget(parent)
{
    // You should do this early enough.
    FramelessWidgetsHelper::get(this)->extendsContentIntoTitleBar();
    // ...
}

Then let FramelessHelper know what should be the title bar:

void MyWidget::myFunction()
{
    // ...
    FramelessWidgetsHelper::get(this)->setTitleBarWidget(m_myTitleBarWidget);
    // ...
}

Then make some widgets inside your title bar visible to hit test:

void MyWidget::myFunction2()
{
    // ...
    FramelessWidgetsHelper::get(this)->setHitTestVisible(m_someSearchBox);
    FramelessWidgetsHelper::get(this)->setHitTestVisible(m_someButton);
    FramelessWidgetsHelper::get(this)->setHitTestVisible(m_someMenuItem);
    // ...
}

IMPORTANT NOTE for Qt Widgets applications: Some functionalities may only be available when FramelessHelper has finished the window customization process, such as changing window geometry/flags/state. In this case you can connect to the public void ready() signal of FramelessHelper to get the accurate time point and do your rest initialization process afterwards.

Qt Quick

Code Snippet

First of all, you should call void FramelessHelper::Quick::initialize() in your main function in a very early stage (MUST before the construction of any Q(Gui|Core)Application objects):

int main(int, char **)
{
    FramelessHelper::Quick::initialize();
    // ...
}

Then you need to register the custom types provided by FramelessHelper by calling void FramelessHelper::Quick::registerTypes(QQmlEngine *), before the QML engine loads any QML documents:

int main(int, char **)
{
    // ...
    QQmlApplicationEngine engine;
    FramelessHelper::Quick::registerTypes(&engine);
    // ...
}

Now you can write your QML documents. You should import FramelessHelper from the URI org.wangwenx190.FramelessHelper. You should specify a version number right after it if you are using Qt5:

import org.wangwenx190.FramelessHelper 1.0 // You can use "auto" or omit the version number in Qt6.

And then you can use the attached properties from the QML type FramelessHelper:

Window {
    Item {
        id: myTitleBar
        Item { id: someControl1 }
        Item { id: someControl2 }
        Item { id: someControl3 }
        Component.onCompleted: {
            // Don't access FramelessHelper too early, otherwise it may not be able to find the root window!
            FramelessHelper.titleBarItem = myTitleBar;
            FramelessHelper.setHitTestVisible(someControl1);
            FramelessHelper.setHitTestVisible(someControl2);
            FramelessHelper.setHitTestVisible(someControl3);
        }
    }
}

It's the same with the FramelessWidgetsHelper interface, the QML type FramelessHelper will be instantiated only once for each Window, no matter when and where you use attached properties from it. However, due to the special design of the FramelessHelper type, you can also use it just like a normal QML type:

Window {
    Item {
        id: myTitleBar
        Item { id: someControl1 }
        Item { id: someControl2 }
        Item { id: someControl3 }
        Component.onCompleted: {
            framelessHelper.setHitTestVisible(someControl1);
            framelessHelper.setHitTestVisible(someControl2);
            framelessHelper.setHitTestVisible(someControl3);
        }
    }
    FramelessHelper {
        id: framelessHelper
        titleBarItem: myTitleBar
    }
}

In theory it's possible to instantiate multiple FramelessHelper objects for a same Window, in this case only one of them will keep functional, all other objects will become a wrapper of it, but doing so is not recommended and may cause unexpected behavior or bugs, so please avoid trying to do that in any case.

If you find any of FramelessHelper functions have no effect after calling, the most possible reason is by the time you call the function/change the property of FramelessHelper, the root window has not finished its initialization process and thus FramelessHelper can't get the handle of it, so any action from the user will be ignored until the root window finished initialization.

There's also a QML type called FramelessWindow, it's only a simple wrapper of FramelessHelper, you can absolutely use plain Window instead.

IMPORTANT NOTE for Qt Quick applications: Some functionalities may only be available when FramelessHelper has finished the window customization process, such as changing window geometry/flags/state. In this case you can connect to the public void ready() signal of FramelessHelper to get the accurate time point and do your rest initialization process afterwards:

Window {
    FramelessHelper.onReady: {
        // do something here ...
    }
}
Window {
    FramelessHelper {
        onReady: {
            // do something here ...
        }
    }
}

More

Please refer to the demo projects to see more detailed usages: examples

Title Bar Design Guidance

Platform Notes

Windows

Linux

macOS

FAQs

When running on Win10, it seems the top border is missing? But the demo applications still have it?

FramelessHelper hides the system title bar by removing the whole top part of the window frame, including the top border. There's no way to only remove the system title bar but still preserve the top border at the same time, even Microsoft themself can't do that either. The exact reason is unknown to non-Microsoft developers, and I have no interest in digging into all the magic behind it. So you'll have to draw one manually yourself to pretend the top border is still there. You can retrieve it's height and color through official DWM APIs. Please refer to the documentation of DwmGetWindowAttribute() and DwmGetColorizationColor(). The demo applications still have the top border because their main windows all inherit from FramelessWidget or FramelessMainWindow, which will draw the top border for you internally. As for Qt Quick, the QML type FramelessWindow will also draw the top border.

When running on Wayland, dragging the title bar causes crash?

You need to force Qt to use the XCB QPA when running on Wayland. Try setting the environment variable QT_QPA_PLATFORM (case sensitive) to xcb (case sensitive) before instantiating any Q(Gui)Application instances. Or just call void FramelessHelper::Widgets/Quick::initialize() in your main function, this function will take care of it for you.

I can see the black background during window resizing?

First of all, it's a Qt issue, not caused by FramelessHelper. And it should not be possible for Qt Widgets applications. It's a common issue for Qt Quick applications. Most of the time it's caused by D3D11/Vulkan/Metal because they are not good at dealing with texture resizing operations. If you really want to fix this issue, you can try to change Qt's RHI backend to OpenGL (be careful of the bug of your graphics card driver) or Software (if you don't care about performance). And please keep in mind that this issue is not fixable from outside of Qt.

Can I preserve the window frame border even on Win7? How does Google Chrome/Microsoft Edge's installer achieve that?

Short answer: it's impossible. Full explaination: of course we can use the same technique we use on Win10 to remove the whole top part of the window and preserve the other three frame borders at the same time, but on Win10 we can bring the top border back, either by doing some black magic in the WM_PAINT handler or draw a thin frame border manually ourself, however, it's impossible to do this on Win7. I've tried it on Win7 already and sadly the result is the WM_PAINT trick won't work on Win7, and we also can't draw a frame border which looks very similar to the original one (a semi-transparent rectangle, blended with system's accent color and the visual content behind the window, also with some blur effect applied). But it seems Google Chrome/Microsoft Edge's installer have achieved what we wanted to do, how? Well, their installer is open source and I've read it's code already. They achieve that by overlapping two windows, one normal window on the bottom, another border-less window on the top to cover the bottom window's title bar. They draw their homemade title bar on the border-less window and use it to emulate the standard title bar's behavior. The original title bar provided by the system is still there, but it can't be seen by anyone just because it's covered by another window. I admit it's a good solution in such cases but for our library it's not appropriate because the code complexity will blow up.

Special Thanks

Ordered by first contribution time (it may not be very accurate, sorry)

License

MIT License

Copyright (C) 2021-2023 by wangwenx190 (Yuhang Zhao)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.