【问题标题】:Painting issue in Qt custom frameless window between monitors显示器之间的 Qt 自定义无框窗口中的绘画问题
【发布时间】:2020-06-06 08:32:08
【问题描述】:

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

虽然它运行良好,但当我的自定义窗口从一个屏幕转移到另一个屏幕(我有两个屏幕)时,会出现视觉故障(如下所示)。故障出现后,调整窗口大小,纠正错误。而且,经过一些调试,我发现Qt QWidget::geometry()与出现bug时Win32 API GetWindowRect()返回的几何图形不一样。

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

没有故障的窗口屏幕截图

有故障的窗口的屏幕截图

QWidget::geometry() 最初报告的几何图形是 QRect(640,280 800x600)QWidget::frameGeometry()QRect(640,280 800x600),Win32 GetWindowRect()QRect(640,280 800x600)。因此,相同的几何形状。但是,在两个监视器之间移动窗口后,QWidget::geometry() 报告的几何图形变为QGeometry(1541,322 784x561)QWidget::frameGeometry()GetWindowRect() 报告的几何形状保持不变。当然,在那之后,当窗口被调整大小时,几何图形被重新正确报告,并且绘画问题消失了。结论,当窗口在两个监视器之间移动时,Qt 似乎假设“出现”帧。

BaseFramelessWindow 类实现可以在here 中找到。这是一个 Qt QMainWindow 子类,重新实现了一些 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;
}

【问题讨论】:

    标签: c++ windows qt winapi dwm


    【解决方案1】:

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

    一个例子是:

    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 子句,但如果没有它,我会遇到多个窗口/对话框的问题(有时它们会变得无响应)。我也不知道SetWindowPos 需要哪些标志。

    我也不能说这种方法是否是好的做法,它可能会在其他地方引起问题。希望其他人可以对此发表评论。

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

    【讨论】:

    • 谢谢,你救了我的命,从 5.12 升级到 5.15 后我遇到了同样的问题,在我的情况下,尝试调整窗口大小修复了故障,当调整 WM_NCCALCSIZE 消息发送到本机事件处理程序,当您使用SWP_FRAMECHANGED 标志时,WM_NCCALCSIZE 将发送到窗口,因此将重新计算窗口大小。无论如何,这似乎是一个与 Qt 相关的问题。
    • 谢谢 - 在错误报告中使用您的代码作为参考:bugreports.qt.io/browse/QTBUG-90701
    • 我发现此解决方案无法处理来自窗口快照的屏幕更改,例如通过使用 Win+向左箭头。更强大的解决方案是在发出QWindow::screenChanged 信号时调用SetWindowPos。这也避免了换屏后短暂出现黑框。
    【解决方案2】:

    问题是Qt在修改WM_NCCALCSIZE时没有正确处理窗口的绘制!

    我意识到您已经有一项工作正在进行中,但我开发了一项并行工作,以一种您可能感兴趣的简单方式实现您的目标,它处理窗口的所有低级 API 以实现无边框窗口,而您只需使用setCentralWidget API,您的小部件就会填满整个窗口!

    ExampleMinimal(干净的例子)的标题只是:

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

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

    【讨论】:

      【解决方案3】:

      看看这个答案:How to integrate the Qt frameless window in Windows composer? (system shortcut doesnt works)

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

      我在从一个屏幕捕捉到另一个屏幕时遇到了一些绘画问题,但使用蒙版解决了这个问题(代码 cmets 中的注释)。找到一个更干净的解决方案会很好(实际上它可能符合 Qt 错误)。

      解决方法位于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()

      【讨论】:

      • 使用 setMask() 似乎只能部分纠正问题。使用 setMash() 时,当窗口未聚焦时会出现一个非常难看的框架,并且当窗口重新打开时,AeroSnap 功能不再起作用(即使重新显示窗口也不起作用)。但这给出了一些想法。
      • 经过一些新的调试后,问题似乎是 Qt 检测到两个监视器之间不存在的框架。有关更多信息,请参阅我的问题更新。
      • @HubertGruniaux 好吧,我确实看到您使用了完全不同的一组 SetWindowLongPtr() 标志和其他功能,例如 DwmExtendFrameIntoClientArea() 等(我的示例要简单得多)。我可以看到setMask() 是如何搞砸的(尽管您可以尝试设置并删除它)。我怀疑问题源于size()frameSize() 之间的差异,触发Qt 重新绘制可能会有所帮助。将示例代码分解到重现问题所需的最低限度并发布完整代码(包括带有简单实现示例的main())会很有用。
      • 添加了最小示例代码。你认为我需要在哪里触发 Qt 重绘?
      • @HubertGruniaux 感谢您的示例,抱歉,我刚刚有时间测试它。但我根本无法重现这个问题......不是用鼠标移动窗口,也不是用 Aero-snap。这幅画每次都能完美呈现。你有什么 Qt 版本,以及哪个 Windows? (这里是 Qt 5.12.6 和 Win7。)
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-03-04
      • 1970-01-01
      • 1970-01-01
      • 2021-06-18
      相关资源
      最近更新 更多