c++windowsqtwinapidwm

Painting issue in Qt custom frameless window between monitors


For one of my applications, I wanted to have a window customized under the Windows operating system (like Firefox, Avast, Microsoft Word, etc.). So I reimplemented some messages handling (QWidget::nativeEvent ()) from Win32 API, in order to preserve the functionality of AeroSnap, and others.

Although it works perfectly, when my custom window is transferred from one screen to another (I have two screens), a visual glitch appears (as you can see below). After the glitch appears, resizing the window, correct the bug. Moreover, after some debugging, I noticed that the Qt QWidget::geometry() is not the same as the geometry returned by Win32 API GetWindowRect() when the bug appears.

Both of my monitors are HD (1920x1080, so is not the difference of resolutions which cause the bug, but maybe the DPI difference ?). The glitch seems to appear when the window is moving between the two screens (when windows transfer the window to one screen to another ?).

Screenshot of the window without the glitch

Screenshot of the window with the glitch

The geometry reported initially by QWidget::geometry() is QRect(640,280 800x600), by QWidget::frameGeometry() is QRect(640,280 800x600), and by the Win32 GetWindowRect() is QRect(640,280 800x600). So, the same geometry. But, after the window is moved between the two monitors, the geometry reported by QWidget::geometry() becomes QGeometry(1541,322 784x561). The geometry reported by QWidget::frameGeometry() or GetWindowRect() is unchanged. Of course, after that, when the window is resized, the geometry is re correctly reported, and the painting issue dissapears. Conclusion, Qt seems to assume that a "appear" frame when the window is moved between two monitors.

The BaseFramelessWindow class implementation can be found here. This is a Qt QMainWindow subclass re-implementing some Win32 API native events (QWidget::nativeEvent()).

So someone would be how to avoid this bug? Is this a Qt bug? I have tried many things, and nothing has really worked. And I can't find any mention of this problem on the internet (maybe I'm looking badly?).

Minimal example code:

// main.cpp
#include "MainWindow.h"

#include <QtWidgets/qapplication.h>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show();
    return a.exec();
}
// MainWindow.h
#pragma once

#include <QtWidgets/qmainwindow.h>

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = Q_NULLPTR);

protected:
    void paintEvent(QPaintEvent* event) override;
    bool nativeEvent(const QByteArray& eventType, void* message, long* result) override;
};
// MainWindow.cpp
#include "MainWindow.h"

#include <QtGui/qpainter.h>

#include <Windows.h>
#include <Windowsx.h>

MainWindow::MainWindow(QWidget* parent)
    : QMainWindow(parent)
{
    ::SetWindowLongPtr((HWND)winId(), GWL_STYLE, WS_POPUP | WS_THICKFRAME | WS_CAPTION | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX);
}

void MainWindow::paintEvent(QPaintEvent* event)
{
    QPainter painter(this);

    // Using GetWindowRect() instead of rect() seems to be a valid
    // workaround for the painting issue. However many other things
    // do not work cause of the bad geometry reported by Qt.
    RECT winrect;
    GetWindowRect(reinterpret_cast<HWND>(winId()), &winrect);
    QRect rect = QRect(0, 0, winrect.right - winrect.left, winrect.bottom - winrect.top);

    // Background
    painter.fillRect(rect, Qt::red);

    // Border
    painter.setBrush(Qt::NoBrush);
    painter.setPen(QPen(Qt::blue, 1));
    painter.drawRect(rect.adjusted(0, 0, -1, -1));

    // Title bar
    painter.fillRect(QRect(1, 1, rect.width() - 2, 19), Qt::yellow);
}
bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, long* result)
{
    MSG* msg = reinterpret_cast<MSG*>(message);

    switch (msg->message)
    {
    case WM_NCCALCSIZE:
        *result = 0;
        return true;
    case WM_NCHITTEST: {
        *result = 0;

        RECT winrect;
        GetWindowRect(reinterpret_cast<HWND>(winId()), &winrect);

        // Code allowing to resize the window with the mouse is omitted.

        long x = GET_X_LPARAM(msg->lParam);
        long y = GET_Y_LPARAM(msg->lParam);
        if (x > winrect.left&& x < winrect.right && y > winrect.top&& y < winrect.top + 20) {
            // To allow moving the window.
            *result = HTCAPTION;
            return true;
        }

        repaint();
        return false;
    }
    default:
        break;
    }

    return false;
}

Solution

  • I had this exact error after and solved it by implementing Qt's moveEvent to detect screen changes and then call SetWindowPos to redraw the window in the same position.

    An example would be:

    class MainWindow : public QMainWindow
    {
        // rest of the class
    
    private:
    
        QScreen* current_screen = nullptr;
    
        void moveEvent(QMoveEvent* event)
        {
            if (current_screen == nullptr)
            {
                current_screen = screen();
            }
            else if (current_screen != screen())
            {
                current_screen = screen();
    
                SetWindowPos((HWND) winId(), NULL, 0, 0, 0, 0,
                             SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER |
                             SWP_NOOWNERZORDER | SWP_FRAMECHANGED | SWP_NOACTIVATE);
            }
    
        }
    };
    

    I'm not sure if the current_screen == nullptr clause is strictly needed, but I had trouble with multiple windows/dialogs without it (sometimes they would become unresponsive). I also do not know which flags for SetWindowPos are necessary.

    I also cannot say whether this method is good practice, and it may cause problems elsewhere. Hopefully someone else can comment on this.

    Either way it worked for me in a simple situation. Hopefully it provides a solution, or at least a starting point to try and solve this.