Borderless Window on Windows (almost have it working)



  • I'm trying to create a borderless window like Github for Windows: Github for Windows.

    I've almost got it working except for one small issue: Qt is not placing items at the correct position. The good news is everything else works: resize, aero-snap, shadows, etc..

    The purple Rectangle should be at position 0,0 with a width equivalent to that of the window. However, as the following screenshot shows, the rectangle is pushed down some and does not extend to the width of the window.
    Incorrect Behavior

    Here is the expected behavior (image created with help from photoshop):
    Correct Behavior

    Here is the complete source. There isn't much to it.

    I'm not sure if I'm going about solving this problem correctly. I'd love to hear alternative suggestions for achieving a borderless window (complete with aero-snap, resize, etc...).

    // BorderlessWindow.hpp
    #pragma once
    
    #include <QtQuick/QQuickWindow>
    
    class BorderlessWindow : public QQuickWindow
    {
        Q_OBJECT
        Q_PROPERTY(int titleHeight READ titleHeight WRITE setTitleHeight NOTIFY titleHeightChanged)
    
    public:
        explicit BorderlessWindow(QWindow *parent = nullptr);
        explicit BorderlessWindow(QQuickRenderControl *renderControl);
    
        bool nativeEvent(const QByteArray &eventType, void *message, long *result) override;
    
        void setTitleHeight(int value);
        int titleHeight() const;
    
    signals:
        void titleHeightChanged();
    
    private:
        LRESULT hit_test(POINT point) const;
        bool composition_enabled() const;
    
        int m_titleHeight = 20;
    };
    
    
    // BorderlessWindow.cpp
    
    #include "BorderlessWindow.hpp"
    
    #include <Windowsx.h>
    #include <dwmapi.h>
    #pragma comment(lib, "dwmapi.lib")
    
    BorderlessWindow::BorderlessWindow(QWindow *parent) : QQuickWindow(parent)
    {
    
    }
    
    BorderlessWindow::BorderlessWindow(QQuickRenderControl *renderControl) : QQuickWindow(renderControl)
    {
    
    }
    
    void BorderlessWindow::setTitleHeight(int value)
    {
        if (value != m_titleHeight)
        {
            m_titleHeight = value;
            emit titleHeightChanged();
        }
    }
    
    int BorderlessWindow::titleHeight() const
    {
        return m_titleHeight;
    }
    
    struct HitRegion
    {
        RECT    bounds;
        LRESULT region;
    
        bool contains(const POINT& point) const
        {
            return
                point.x >= bounds.left && point.x < bounds.right &&
                point.y >= bounds.top  && point.y < bounds.bottom;
        }
    };
    
    LRESULT BorderlessWindow::hit_test(POINT point) const
    {
        const auto border_x = ::GetSystemMetrics(SM_CYFRAME) + ::GetSystemMetrics(SM_CXPADDEDBORDER);
        const auto border_y = ::GetSystemMetrics(SM_CYFRAME) + ::GetSystemMetrics(SM_CXPADDEDBORDER);
    
        RECT winrect;
        ::GetWindowRect((HWND)this->winId(), &winrect);
    
        const HitRegion hitregions[]
        {
            { 
                { winrect.left, winrect.bottom - border_y, winrect.left + border_x, winrect.bottom }, HTBOTTOMLEFT
            },
            {
                { winrect.right - border_x, winrect.bottom - border_y, winrect.right, winrect.bottom }, HTBOTTOMRIGHT
            },
            {
                { winrect.left, winrect.top, winrect.left + border_x  , winrect.top + border_y }, HTTOPLEFT
            },
            {
                { winrect.right - border_x , winrect.top, winrect.right, winrect.top + border_y }, HTTOPRIGHT
            },
            {
                { winrect.left, winrect.top, winrect.left + border_x , winrect.bottom }, HTLEFT
            },
            {
                { winrect.right - border_x , winrect.top, winrect.right, winrect.bottom }, HTRIGHT
            },
            {
                { winrect.left, winrect.top, winrect.right, winrect.top + border_y }, HTTOP
            },
            {
                { winrect.left, winrect.bottom - border_y, winrect.right, winrect.bottom }, HTBOTTOM
            },
            {
                { winrect.left, winrect.top, winrect.right, winrect.top + this->titleHeight() }, HTCAPTION
            },
            {
                { winrect.left, winrect.top + this->titleHeight(), winrect.right, winrect.bottom }, HTCLIENT
            }
        };
    
        for (auto &&hr : hitregions)
        {
            if (hr.contains(point))
            {
                return hr.region;
            }
        }
    
        return HTNOWHERE;
    }
    
    bool BorderlessWindow::composition_enabled() const
    {
        BOOL composition_enabled = FALSE;
        bool success = ::DwmIsCompositionEnabled(&composition_enabled) == S_OK;
        return composition_enabled && success;
    }
    
    bool BorderlessWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)
    {
        MSG *msg = static_cast<MSG *>(message);
    
        switch (msg->message)
        {
            case WM_SHOWWINDOW:
                {
                    if (this->composition_enabled())
                    {
                        static const MARGINS shadow_state{ 1, 1, 1, 1 };
                        ::DwmExtendFrameIntoClientArea((HWND)this->winId(), &shadow_state);
                    }
    
                    // Do not return true: Let Qt handle this event.
                    // We just needed a hook to enable shadows.
                    return false;
                }
    
            case WM_NCCALCSIZE:
                {
                    // This is what kills the border and extends the client rect to the size of the window rect.
                    *result = 0;
                    return true;
                }
    
            case WM_NCHITTEST:
                {
                    // When we have no border or title bar, we need to perform our
                    // own hit testing to allow resizing and moving.
                    const POINT cursor{
                        GET_X_LPARAM(msg->lParam),
                        GET_Y_LPARAM(msg->lParam)
                    };
    
                    *result = this->hit_test(cursor);
                    return true;
                }
    
            case WM_NCACTIVATE:
                {
                    if (!this->composition_enabled())
                    {
                        // Prevents window frame reappearing on window activation in "basic" theme,
                        // where no aero shadow is present.
                        *result = 1;
                        return true;
                    }
                    else
                    {
                        return false;
                    }
                }
        }
    
        return QQuickWindow::nativeEvent(eventType, message, result);
    }
    
    // main.cpp
    
    #include "BorderlessWindow.hpp"
    
    #include <QtGui/QGuiApplication>
    #include <QtGui/QScreen>
    #include <QtQml/QQmlEngine>
    #include <QtQml/QQmlComponent>
    #include <QtQuick/QQuickWindow>
    #include <QtCore/QUrl>
    #include <QDebug>
    
    int main(int argc, char* argv[])
    {
        QGuiApplication app(argc, argv);
        qmlRegisterType<BorderlessWindow>("SampleApp", 1, 0, "BorderlessWindow");
    
        QQmlEngine engine;
        QQmlComponent component(&engine);
        QQuickWindow::setDefaultAlphaBuffer(true);
    
        component.loadUrl(QUrl("qrc:///window.qml"));
        if (component.isReady())
        {
            component.create();
        }
        else
        {
            qWarning() << component.errorString();
        }
    
        return app.exec();
    }
    
    // window.qml
    
    import QtQuick 2.6
    import QtQuick.Controls 2.0
    import QtQuick.Window 2.2
    import SampleApp 1.0
    
    BorderlessWindow {
        width: 400
        height: 400
        visible: true
        color: "aqua"
    
        Rectangle {
            x: 0 /* Position 0,0 should be the very top-left of the window, but its not. */
            y: 0
            width: parent.width
            height: 20
            color: "purple"
        }
    }
    
    // sample.qrc
    
    <!DOCTYPE RCC><RCC version="1.0">
    <qresource>
      <file>window.qml</file>
    </qresource>
    </RCC>
    
    // sample.pro
    
    TEMPLATE = app
    
    QT += quick qml
    SOURCES += main.cpp BorderlessWindow.cpp
    HEADERS += BorderlessWindow.hpp
    RESOURCES += sample.qrc
    


  • @Henry_S I don't really use QML as I prefer C++ to make my guis, but it looks like telling the native window not to have the border is fine but Qt doesn't realize you did that so it's imagining the windows to be quite different geometry wise. This is just a guess.

    What happens if you take QML out of it and just use BorderlessWindow directly in c++. Does it do the same thing as the QML one?



  • @ambershark It looks like this: qt_cpp_window

    That's using only C++ and Qt widgets (no QML). Both this and the QML version were based on the implementation found in this github repo. This is clearly doable, but it seems Qt is making assumptions about the geometry.

    If you notice, that repo has this blurb at the bottom:

    What this example does not do: Draw anything to the client area. You will need to fill the entire window with an opaque color, or the window frame may be visible inside your client area in borderless mode. In my use case I simply fill the D3D backbuffer covering the window's client area.

    I suspect there may be a way to hack around this without modifying Qt, but I'm not sure.

    In the case of QML, the OpenGL context is clearly filling the window (take a look at my first post, notice how the entire window is aqua). That aqua is all from OpenGL so its clearly filling the window. Why the purple bar doesn't appear in the correct position must be due to the QML implementation making geometry assumptions.

    I'm curious if Qt will ever officially support a borderless window. I've never dug into the Qt implementation before. I wonder where the changes would be needed...



  • @Henry_S I think the problem is supporting it in all the platforms. It's doable in windows, but not necessarily doable in other platforms.

    As for making a borderless window I would have to play around with it. It isn't something I could do off the top of my head. I am 95% linux/osx coder these days. I haven't coded outside Qt for windows since like 2001. So I'm a tad rusty. ;)

    I'm curious though so I might play with the idea later this weekend if I have time.


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.