Qt QWidget.move(x, y) 在 Wayland 屏幕上无法更新小部件

huangapple go评论86阅读模式
英文:

Qt QWidget.move(x,y) fails to update widget on Wayland screen

问题

我有一个名为QMovableResizableWidget的类。它是另一个名为QResizableWidget的类的子类,而QResizableWidget则是QCustomWidget的子类,而QCustomWidget则是QWidget的子类。

在初始化时,QMovableResizableWidget没有指定父窗口小部件。我一直在尝试使QMovableResizableWidget().move(x, y)起作用。setGeometry按预期工作,但是_QMovableResizableWidget_拒绝从其初始位置移动。

然而,在我移动后,小部件的(x, y)位置会更改为我移动的位置,但小部件本身没有移动(在屏幕上绘制)。 [我尝试过重新绘制,但这也失败了。]
此外,在调用move时,WA_Moved小部件属性被设置为True,正如预期的那样。

我已经广泛搜索了这个问题,但找不到任何指引。我是否遗漏了什么?

以下是QMovableResizableWidget的最简实现以及其祖先类的代码片段:

import sys
from PySide6 import QtWidgets, QtCore, QtGui

Qt = QtCore.Qt
QMouseEvent = QtGui.QMouseEvent
QWidget = QtWidgets.QWidget
QEvent = QtCore.QEvent
CursorShape = Qt.CursorShape
QApplication = QtWidgets.QApplication
WidgetAttributes = Qt.WidgetAttribute
WindowTypes = Qt.WindowType
QPoint = QtCore.QPoint


class QCustomWidget(QWidget):
    def __init__(self, p, f):
        super().__init__(p, f)
        self.setMouseTracking(True)


class QResizableWidget(QCustomWidget):
    def __init__(self, p, f):
        super().__init__(p, f)
        pass


class QMovableResizableWidget(QCustomWidget):
    def __init__(self, p, f):
        super().__init__(p, f)

        self.__moveInitLoc = None
        self.__isDragging = False
        self.setAttribute(Qt.WidgetAttribute.WA_MouseTracking, True)

    def event(self, ev):
        if isinstance(ev, QMouseEvent):
            if (
                ev.type() == QEvent.Type.MouseButtonPress
                and ev.button() == Qt.MouseButton.LeftButton
            ):
                self.__isDragging = True
                self.__moveInitLoc = ev.globalPos()

            elif (
                ev.type() == QEvent.Type.MouseMove
                and self.__isDragging
            ):
                if self.__moveInitLoc is not None:
                    self.setCursor(CursorShape.DragMoveCursor)

                    diffLoc = ev.globalPos() - self.__moveInitLoc
                    newLoc = self.mapToGlobal(self.pos()) + diffLoc
                    self.move(newLoc)  # x, y位置在对象中更新。但对象不会移动
                    return True

            elif ev.type() == QEvent.Type.MouseButtonRelease:
                self.__isDragging = False
                if self.__moveInitLoc is not None:
                    self.__moveInitLoc = None
                    self.unsetCursor()

        return super().event(ev)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")

    window = QMovableResizableWidget(
        None, WindowTypes.Window | WindowTypes.FramelessWindowHint
    )

    window.resize(300, 300)
    window.show()
    sys.exit(app.exec())

你的代码看起来几乎正确,但可能需要检查一些细节,以确保窗口小部件在移动时正确重绘。如果问题仍然存在,你可能需要查看其他可能干扰窗口小部件移动的部分。

英文:

I have a class QMovableResizableWidget. It sub-classes another class QResizableWidget, which is a subclass of QCustomWidget, a subclass of QWidget.

QMovableResizableWidget is given no parent widget, on initialization. I've been trying to get QMovableResizableWidget().move(x,y) to work. setGeometry works as intended but the QMovableResizableWidget refuses to move from its original position.

However after I move, the (x,y) position of the widget changes to the position I have moved, but not the widget itself (painted on the screen). [I've tried repainting, but this fails too.]
Also, on calling move, the WA_Moved widget attribute is set to True, as expected.

I've searched extensively for this problem but can't find any pointers. Is there something I'm missing?

Below is a bare bones implementation of QMovableResizableWidget and snippets of it's ancestor classes:

import sys
from PySide6 import QtWidgets, QtCore, QtGui

Qt = QtCore.Qt
QMouseEvent = QtGui.QMouseEvent
QWidget = QtWidgets.QWidget
QEvent = QtCore.QEvent
CursorShape = Qt.CursorShape
QApplication = QtWidgets.QApplication
WidgetAttributes = Qt.WidgetAttribute
WindowTypes = Qt.WindowType
QPoint = QtCore.QPoint


class QCustomWidget(QWidget):
    def __init__(self, p, f):
        # type: (QWidget | None, WindowTypes) -> None
        super().__init__(p, f)
        self.setMouseTracking(True)
        
        

class QResizableWidget(QCustomWidget):
    def __init__(self,p, f):
        # type: (QWidget| None, WindowTypes) -> None
        super().__init__(p, f)
        pass
        

class QMovableResizableWidget(QCustomWidget):
    def __init__(self,p, f):
        # type: (QWidget | None, WindowTypes) -> None
        super().__init__(p, f)

        self.__moveInitLoc = None
        self.__isDragging = False
        self.setAttribute(Qt.WidgetAttribute.WA_MouseTracking, True)
        
    def event(self, ev):
        # type: (QEvent | QMouseEvent) -> bool
        if isinstance(ev, QMouseEvent):
            if (
                ev.type() == QEvent.Type.MouseButtonPress
                and ev.button() == Qt.MouseButton.LeftButton
            ):
                # print("Movable widget clicked")
                self.__isDragging = True
                self.__moveInitLoc = ev.globalPos()

            elif (
                ev.type() == QEvent.Type.MouseMove
                and self.__isDragging
            ):
                # dragging
                # print("ms move")
                if self.__moveInitLoc is not None:
                    self.setCursor(CursorShape.DragMoveCursor)
                    
                    diffLoc = ev.globalPos() - self.__moveInitLoc
                    newLoc = self.mapToGlobal(self.pos()) + diffLoc
                    self.move(newLoc) # x,y location updated in object. But object doesn't move
                    return True

            elif ev.type() == QEvent.Type.MouseButtonRelease:
                # Check if we were dragging
                # print("ms Released")
                self.__isDragging = False
                if self.__moveInitLoc is not None:
                    self.__moveInitLoc = None
                    self.unsetCursor()

        return super().event(ev)
    

if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")

    window = QMovableResizableWidget(None, WindowTypes.Window | WindowTypes.FramelessWindowHint)

    window.resize(300, 300)
    window.show()
    sys.exit(app.exec())

答案1

得分: 1

你的主要问题出在这一行:

newLoc = self.mapToGlobal(self.pos()) + diffLoc

你实际上是通过其全局位置来进行小部件位置的转换:mapToGlobal 将一个点从本地坐标映射到基于小部件全局位置的全局坐标中。

如果你调用一个小部件的 self.mapToGlobal(self.pos()),而该小部件显示在 10, 10(相对于其父级,或对于顶级小部件来说是整个桌面),结果将是 20, 20,或者 10, 10 加上 该小部件的屏幕坐标。

另外,一旦小部件被移动,任何进一步的映射将基于位置进行。

以下是一个更合适的实现:

mousePos = self.mapToParent(ev.pos())
diffLoc = mousePos - self.__moveInitLoc
newLoc = self.pos() + diffLoc
self.move(newLoc)
self.__moveInitLoc = mousePos

请注意,第一行和最后一行(以及它们相对于 move() 调用的顺序)非常重要,因为它们跟踪了相对于小部件当前几何形状以及在移动之前映射的鼠标位置。

还请注意,使用 mapToParent() 是重要的,因为它考虑了小部件可能是顶级小部件的情况:mapToGlobal() 仅假定位置是在全局坐标中,而对于另一个小部件的子级来说,这显然是错误的。

关于 Wayland 的更新

Wayland 的一个限制之一是它不支持从客户端侧设置顶级窗口的几何(位置和大小)。解决方案是使用操作系统的功能来实际移动窗口:获取当前顶级小部件的 QWindow(windowHandle()) ,然后简单地调用 startSystemMove()

def event(self, ev):
    # type: (QEvent | QMouseEvent) -> bool
    if (
        ev.type() == QEvent.Type.MouseButtonPress
        and ev.button() == Qt.MouseButton.LeftButton
    ):
        self.window().windowHandle().startSystemMove()
        event.accept()
        return True
    return super().event(ev)
英文:

Your main issue is within this line:

newLoc = self.mapToGlobal(self.pos()) + diffLoc

You're practically translating the widget position by its own global position: mapToGlobal maps a point in local coordinates to a point in global coordinates based on the global position of the widget.

If you call self.mapToGlobal(self.pos()) of a widget that is shown at 10, 10 (to its parent, or to the whole desktop for top level widget), the result is 20, 20, or 10, 10 plus the screen coordinate of that widget.

Also, once the widget has been moved, any further mapping will be done based on the new position.

Here is a more appropriate implementation:

    mousePos = self.mapToParent(ev.pos())
    diffLoc = mousePos - self.__moveInitLoc
    newLoc = self.pos() + diffLoc
    self.move(newLoc)
    self.__moveInitLoc = mousePos

Note that the first and last line (and their order related to the move() call) are extremely important, because they keep track of the mapped mouse position related to the current geometry of the widget and before it was moved.

Also note that using mapToParent() is important because it considers the possibility of the widget being a top level one: mapToGlobal() only assumes that the position is in global coordinates, and that would be obviously wrong for children of another widget.

Update about Wayland

One of the limitations of Wayland is that it doesn't support setting a top level window geometry (position and size) from the client side. The solution, then, is to use the OS capabilities to actually move the window: get the QWindow (windowHandle()) of the current top level widget (window()), and simply call startSystemMove():

    def event(self, ev):
        # type: (QEvent | QMouseEvent) -> bool
        if (
            ev.type() == QEvent.Type.MouseButtonPress
            and ev.button() == Qt.MouseButton.LeftButton
        ):
            self.window().windowHandle().startSystemMove()
            event.accept()
            return True
        return super().event(ev)

答案2

得分: 0

这显然是Wayland上已知的问题。

请参阅Wayland Peculiarities。感谢@musicamante指出这一点。

另请参阅此链接,了解Wayland的此限制。

查看下面的代码以获取在Wayland上的工作实现:

import sys
from PySide6 import QtWidgets, QtCore, QtGui

Qt = QtCore.Qt
QMouseEvent = QtGui.QMouseEvent
QWidget = QtWidgets.QWidget
QEvent = QtCore.QEvent
CursorShape = Qt.CursorShape
QApplication = QtWidgets.QApplication
WidgetAttributes = Qt.WidgetAttribute
WindowTypes = Qt.WindowType
QPoint = QtCore.QPoint


class QCustomWidget(QWidget):
    def __init__(self, p, f):
        # type: (QWidget | None, WindowTypes) -> None
        super().__init__(p, f)
        self.setMouseTracking(True)
        

class QResizableWidget(QCustomWidget):
    def __init__(self,p, f):
        # type: (QWidget| None, WindowTypes) -> None
        super().__init__(p, f)
        pass
        

class QMovableResizableWidget(QCustomWidget):
    def __init__(self,p, f):
        # type: (QWidget | None, WindowTypes) -> None
        super().__init__(p, f)

        self.setAttribute(Qt.WidgetAttribute.WA_MouseTracking, True)
        self.setAttribute(Qt.WidgetAttribute.WA_NativeWindow, True)

    def event(self, ev):
        # type: (QEvent | QMouseEvent) -> bool
        if (
            ev.type() == QEvent.Type.MouseButtonPress
            and ev.button() == Qt.MouseButton.LeftButton
        ):
            self.window().windowHandle().startSystemMove()
            ev.accept()
            return True
        return super().event(ev)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")

    window = QMovableResizableWidget(None, WindowTypes.Window | WindowTypes.FramelessWindowHint)

    window.resize(300, 300)
    window.show()
    sys.exit(app.exec())
英文:

This is apparently a known problem on Wayland.

See Wayland Peculiarities. Thanks to @musicamante for pointing this out

Also, see this link on this limitation of Wayland

See the updated code below for the working implementation on Wayland

import sys
from PySide6 import QtWidgets, QtCore, QtGui

Qt = QtCore.Qt
QMouseEvent = QtGui.QMouseEvent
QWidget = QtWidgets.QWidget
QEvent = QtCore.QEvent
CursorShape = Qt.CursorShape
QApplication = QtWidgets.QApplication
WidgetAttributes = Qt.WidgetAttribute
WindowTypes = Qt.WindowType
QPoint = QtCore.QPoint


class QCustomWidget(QWidget):
    def __init__(self, p, f):
        # type: (QWidget | None, WindowTypes) -> None
        super().__init__(p, f)
        self.setMouseTracking(True)
        
        

class QResizableWidget(QCustomWidget):
    def __init__(self,p, f):
        # type: (QWidget| None, WindowTypes) -> None
        super().__init__(p, f)
        pass
        

class QMovableResizableWidget(QCustomWidget):
    def __init__(self,p, f):
        # type: (QWidget | None, WindowTypes) -> None
        super().__init__(p, f)

        self.setAttribute(Qt.WidgetAttribute.WA_MouseTracking, True)
        self.setAttribute(Qt.WidgetAttribute.WA_NativeWindow, True)

    def event(self, ev):
        # type: (QEvent | QMouseEvent) -> bool
        if (
            ev.type() == QEvent.Type.MouseButtonPress
            and ev.button() == Qt.MouseButton.LeftButton
        ):
            self.window().windowHandle().startSystemMove()
            ev.accept()
            return True
        return super().event(ev)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")

    window = QMovableResizableWidget(None, WindowTypes.Window | WindowTypes.FramelessWindowHint)

    window.resize(300, 300)
    window.show()
    sys.exit(app.exec())

huangapple
  • 本文由 发表于 2023年7月7日 03:40:43
  • 转载请务必保留本文链接:https://go.coder-hub.com/76632084.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定