Qt 自定义 frameless window 显示器之间的绘画问题

Painting issue in Qt custom frameless window between monitors

对于我的一个应用程序,我想在 Windows 操作系统(如 Firefox、Avast、Microsoft Word 等)下定制 window。因此,我从 Win32 API 重新实现了一些消息处理 (QWidget::nativeEvent ()),以保留 AeroSnap 和其他功能。

虽然效果很好,但当我的自定义 window 从一个屏幕转移到另一个屏幕(我有两个屏幕)时,会出现视觉故障(如下所示)。故障出现后,调整 window 的大小,修复错误。此外,经过一些调试,我发现Qt QWidget::geometry() 与出现错误时Win32 API GetWindowRect() 返回的几何图形不一样。

我的两台显示器都是高清的(1920x1080,所以导致错误的不是分辨率差异,而是 DPI 差异?)。当 window 在两个屏幕之间移动时(当 windows 将 window 从一个屏幕转移到另一个屏幕时,似乎会出现故障?)。

window 无故障

的屏幕截图

window 故障

的屏幕截图

QWidget::geometry() 最初报告的几何形状是 QRect(640,280 800x600)QWidget::frameGeometry()QRect(640,280 800x600),Win32 GetWindowRect()QRect(640,280 800x600) .所以,相同的几何形状。但是,在 window 在两个监视器之间移动后,QWidget::geometry() 报告的几何形状变为 QGeometry(1541,322 784x561)QWidget::frameGeometry()GetWindowRect() 报告的几何图形未更改。当然,之后,当 window 调整大小时,几何重新正确报告,绘画问题消失。结论,当 window 在两个监视器之间移动时,Qt 似乎假设 "appear" 帧。

可以找到 BaseFramelessWindow class 实现 here。这是一个 Qt QMainWindow subclass 重新实现一些 Win32 API 本机事件 (QWidget::nativeEvent()).

那么有人会怎么避免这个bug呢?这是一个 Qt 错误吗?我尝试了很多东西,但没有任何效果。而且我在互联网上找不到任何提及此问题的信息(也许我看起来很糟糕?)。

最小示例代码:

// 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;
}

查看此答案:

我在屏幕之间捕捉时遇到了完全相同的问题,如该答案的第 3 段所述:

I ran into some painting issues when snapping from one screen to another, but worked around that with a mask (notes in code comments). Would be nice to find a cleaner solution (it may qualify as a Qt bug actually).

解决方法在 paintEvent() 方法中 FramelessWidget 代码块的底部,在长注释之后。我也将其粘贴在这里并附上一些上下文:

  QStyleOption opt;
  opt.initFrom(this);
  // be sure to use the full frame size, not the default rect() which is inside frame.
  opt.rect.setSize(frameSize());  

      .....

  // Setting a mask works around an issue with artifacts when switching screens with Win+arrow
  // keys. I don't think it's the actual mask which does it, rather it triggers the region 
  // around the widget to be polished but I'm not sure. As support for my theory, the mask 
  // doesn't even have to follow the border radius.
  setMask(QRegion(opt.rect));

我在你的代码中没有看到 paintEvent() 但也许你可以从其他地方做类似的事情。

顺便说一句,这也可能会提示您为什么 QWidget::geometry() 不正确...您可能想要 QWidget::frameGeometry().

问题是Qt在修改WM_NCCALCSIZE时没有正确处理window的绘图!

我意识到你已经有一项工作正在进行中,但我开发了一项并行工作,它以一种你可能感兴趣的简单方式实现了你的目标,它处理了 [=] 的所有低级别 API 28=] 用于少边框 window,而您可以只使用 setCentralWidget API 并且您的小部件将填满整个 window!

ExampleMinimal 的 header(干净的例子)只是:

#include <QGoodWindow>

class MainWindow : public QGoodWindow
{
    Q_OBJECT
public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();
};

参见:https://github.com/antonypro/QGoodWindow

之后我遇到了这个确切的错误,并通过实施 Qt 的 moveEvent 来检测屏幕变化然后调用 SetWindowPos 在相同位置重绘 window 来解决它。

例如:

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);
        }

    }
};

我不确定是否确实需要 current_screen == nullptr 子句,但如果没有它,我在处理多个 windows/dialogs 时遇到了麻烦(有时它们会变得无响应)。我也不知道 SetWindowPos 需要哪些标志。

我也不好说这个方法好不好,可能会在其他地方出问题。希望其他人可以对此发表评论。

在简单的情况下,无论哪种方式都对我有用。希望它提供了一个解决方案,或者至少是一个尝试解决这个问题的起点。