SineStriker
2025-03-14 7966371dead9076db6c56de4074b36d6f628678b
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// Copyright (C) 2023-2024 Stdware Collections (https://www.github.com/stdware)
// Copyright (C) 2021-2023 wangwenx190 (Yuhang Zhao)
// SPDX-License-Identifier: Apache-2.0
 
#include "widgetwindowagent_p.h"
 
#include <QtCore/QDebug>
#include <QtCore/QDateTime>
#include <QtGui/QPainter>
 
#include <QtCore/private/qcoreapplication_p.h>
 
#include <QWKCore/qwindowkit_windows.h>
#include <QWKCore/private/qwkglobal_p.h>
#include <QWKCore/private/windows10borderhandler_p.h>
 
namespace QWK {
 
#if QWINDOWKIT_CONFIG(ENABLE_WINDOWS_SYSTEM_BORDERS)
    // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowsbackingstore.cpp#L42
    // In QtWidgets applications, when repainting happens, QPA at the last calls
    // QWindowsBackingStore::flush() to draw the contents of the buffer to the screen, we need to
    // call GDI drawing the top border after that.
 
    // After debugging, we know that there are two situations that will lead to repaint.
    //
    // 1. Windows sends a WM_PAINT message, after which Qt immediately generates a QExposeEvent or
    // QResizeEvent and send it to the corresponding QWidgetWindow instance, calling "flush" at the
    // end of its handler.
    //
    // 2. When a timer or user input triggers Qt to repaint spontaneously, the corresponding
    // QWidget receives a QEvent::UpdateRequest event and also calls "flush" at the end of its
    // handler.
    //
    // The above two cases are mutually exclusive, so we just need to intercept the two events
    // separately and draw the border area after the "flush" is called.
 
    // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowswindow.cpp#L2440
    // Note that we can not draw the border right after WM_PAINT comes or right before the WndProc
    // returns, because Qt calls BeginPaint() and EndPaint() itself. We should make sure that we
    // draw the top border between these two calls, otherwise some display exceptions may arise.
 
    class WidgetBorderHandler : public QObject, public Windows10BorderHandler {
    public:
        explicit WidgetBorderHandler(QWidget *widget, AbstractWindowContext *ctx,
                                     QObject *parent = nullptr)
            : QObject(parent), Windows10BorderHandler(ctx), widget(widget) {
            widget->installEventFilter(this);
 
            // First update
            if (ctx->windowId()) {
                setupNecessaryAttributes();
            }
            WidgetBorderHandler::updateGeometry();
        }
 
        void updateGeometry() override {
            // The window top border is manually painted by QWK so we want to give
            // some margins to avoid it covering real window contents, however, we
            // found that there are some rounding issues for the thin border and
            // thus this small trick doesn't work very well when the DPR is not
            // integer. So far we haven't found a perfect solution, so just don't
            // set any margins. In theory the window content will only be covered
            // by 1px or so, it should not be a serious issue in the real world.
            //
            // widget->setContentsMargins(isNormalWindow() ? QMargins(0, borderThickness(), 0, 0)
            //                                             : QMargins());
        }
 
        bool isWindowActive() const override {
            return widget->isActiveWindow();
        }
 
        inline void forwardEventToWidgetAndDraw(QWidget *w, QEvent *event) {
            // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/widgets/kernel/qapplication.cpp#L3286
            // Deliver the event
            if (!forwardObjectEventFilters(this, w, event)) {
                // Let the widget paint first
                std::ignore = static_cast<QObject *>(w)->event(event);
                QCoreApplicationPrivate::setEventSpontaneous(event, false);
            }
 
            // Due to the timer or user action, Qt will repaint some regions spontaneously,
            // even if there is no WM_PAINT message, we must wait for it to finish painting
            // and then update the top border area.
            drawBorderNative();
        }
 
        inline void forwardEventToWindowAndDraw(QWindow *window, QEvent *event) {
            // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/widgets/kernel/qapplication.cpp#L3286
            // Deliver the event
            if (!forwardObjectEventFilters(ctx, window, event)) {
                // Let Qt paint first
                std::ignore = static_cast<QObject *>(window)->event(event);
                QCoreApplicationPrivate::setEventSpontaneous(event, false);
            }
 
            // Upon receiving the WM_PAINT message, Qt will repaint the entire view, and we
            // must wait for it to finish painting before drawing this top border area.
            drawBorderNative();
        }
 
    protected:
        bool sharedEventFilter(QObject *obj, QEvent *event) override {
            Q_UNUSED(obj)
 
            switch (event->type()) {
                case QEvent::Expose: {
                    // Qt will absolutely send a QExposeEvent or QResizeEvent to the QWindow when it
                    // receives a WM_PAINT message. When the control flow enters the expose handler,
                    // Qt must have already called BeginPaint() and it's the best time for us to
                    // draw the top border.
 
                    // Since a QExposeEvent will be sent immediately after the QResizeEvent, we can
                    // simply ignore it.
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
                    struct ExposeEvent : public QExposeEvent {
                        inline const QRegion &getRegion() const { return m_region; }
                    };
                    auto ee = static_cast<ExposeEvent *>(event);
                    bool exposeRegionValid = !ee->getRegion().isNull();
#else
                    auto ee = static_cast<QExposeEvent *>(event);
                    bool exposeRegionValid = !ee->region().isNull();
#endif
                    auto window = widget->windowHandle();
                    if (window->isExposed() && isNormalWindow() && exposeRegionValid) {
                        forwardEventToWindowAndDraw(window, event);
                        return true;
                    }
                    break;
                }
                default:
                    break;
            }
            return Windows10BorderHandler::sharedEventFilter(obj, event);
        }
 
        bool eventFilter(QObject *obj, QEvent *event) override {
            Q_UNUSED(obj)
 
            switch (event->type()) {
                case QEvent::UpdateRequest: {
                    if (!isNormalWindow())
                        break;
                    forwardEventToWidgetAndDraw(widget, event);
                    return true;
                }
 
                case QEvent::WindowStateChange: {
                    updateGeometry();
                    break;
                }
 
                case QEvent::WindowActivate:
                case QEvent::WindowDeactivate: {
                    widget->update();
                    break;
                }
 
                default:
                    break;
            }
            return false;
        }
 
        QWidget *widget;
    };
 
    void WidgetWindowAgentPrivate::setupWindows10BorderWorkaround() {
        // Install painting hook
        auto ctx = context.get();
        if (ctx->windowAttribute(QStringLiteral("win10-border-needed")).toBool()) {
            borderHandler = std::make_unique<WidgetBorderHandler>(hostWidget, ctx);
        }
    }
#endif
 
}