在多个显示器之间,Qt自定义无边框窗口的绘制问题

3
对于我的一个应用程序,我想要在Windows操作系统下定制一个窗口(如Firefox,Avast,Microsoft Word等)。因此,我重新实现了一些消息处理(QWidget::nativeEvent())来自Win32 API,以保留AeroSnap和其他功能。
虽然它完美地工作,但当我的自定义窗口从一个屏幕转移到另一个屏幕时(我有两个屏幕),会出现视觉故障(如下图所示)。出现故障后,调整窗口大小可以修复该错误。此外,在进行一些调试后,我注意到Qt的QWidget::geometry()与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类实现。这是一个Qt QMainWindow子类,重新实现了一些Win32 API本机事件(QWidget::nativeEvent())。
因此,有人想知道如何避免这个错误吗?这是一个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个回答

4

我曾经遇到过这个错误,通过实现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相关的问题。 - HojjatJafary
谢谢 - 我在错误报告 https://bugreports.qt.io/browse/QTBUG-90701 中使用了您的代码作为参考。 - Pascal S.
我发现这个解决方案无法处理来自窗口捕捉的屏幕更改,例如使用Win+左箭头。更健壮的解决方案是在发出QWindow::screenChanged信号时调用SetWindowPos。这也避免了屏幕更改后短暂显示黑色边框的问题。 - Everlight

0
问题在于当修改WM_NCCALCSIZE时,Qt无法正确处理窗口的绘制!
我意识到您已经有一个正在进行中的工作,但我开发了一个并行工作,以简单的方式实现了您的目标,它处理了窗口的所有低级API,使其成为一个无边框窗口,同时您只需使用setCentralWidget API,您的小部件将填充整个窗口!

QGoodWindow

ExampleMinimal 的头部(干净的示例)只有:

#include <QGoodWindow>

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

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


0

看看这个答案:如何在Windows composer中集成Qt无框窗口?(系统快捷方式不起作用)

我遇到了与该答案第三段提到的屏幕之间的捕捉问题完全相同的问题:

当从一个屏幕捕捉到另一个屏幕时,我遇到了一些绘图问题,但通过使用掩码(代码注释中的注释)解决了这个问题。找到一个更清晰的解决方案会很好(实际上可能符合Qt bug)。

解决方法位于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在两个显示器之间检测到了一个不存在的框架。请查看我的问题更新获取更多信息。 - Hubert Gruniaux
@HubertGruniaux,我看你使用了一组非常不同的SetWindowLongPtr()标志和其他特性,比如DwmExtendFrameIntoClientArea()等(我的示例要简单得多)。我可以理解setMask()可能会弄乱它(尽管您可以尝试设置它并删除它)。我怀疑问题源于size()frameSize()之间的差异,触发Qt重新绘制可能会有所帮助。将您的示例代码分解为最小化可复现该问题的代码,并发布该完整代码(包括具有简单实现示例的main())。 - Maxim Paperno
最小示例代码已添加。您认为我需要在哪里触发Qt重新绘制? - Hubert Gruniaux
@HubertGruniaux 感谢您的示例,很抱歉我现在才有时间测试。但是我无法复制任何问题...无论是用鼠标移动窗口还是使用Aero-snap。每次绘画都完美呈现。您使用的Qt版本和Windows版本是什么?(此处为Qt 5.12.6与Win7) - Maxim Paperno
此外,还有 https://bugreports.qt.io/browse/QTBUG-81695 -- 我不知道它是否相关,但可能是。您正在使用高清晰度显示器吗?同时,使用 Qt 示例之一重现了此问题,也许您想尝试一下:https://lists.qt-project.org/pipermail/development/2020-February/038809.html - Maxim Paperno
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接