0

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())
razor_chk
  • 77
  • 2
  • 9
  • 1
    We don't know what you may be missing, but we are certainly missing a valid [mre]. – musicamante Jul 06 '23 at 19:50
  • You're right @musicamante. I updated the post with a somewhat reproducible example – razor_chk Jul 06 '23 at 23:03
  • 1
    I have to ask you to take a moment to put yourself in our shoes. You see a post from somebody, you know nothing about their program or the problem at hand. They tell you that the program doesn't work as expected, and you ask to see some code that allows you to *understand* the problem. Then you see the code you actually posted a while ago. Remember: you know **nothing** about the problem except from what they explained; you're here to help them, voluntarily (and for free, btw). Now, *sincerely* look at the code with *those* eyes. Do you really think you could do anything with *that* code? – musicamante Jul 07 '23 at 00:25
  • Ok @musicamante. I intentionally left out the implementations of the ancestor classes because they don't matter for the problem at hand. I have updated the snippet with a version which is ready to run – razor_chk Jul 07 '23 at 00:47
  • 1
    Actually, they *did* matter: the basic requirement of a MRE is that it allows us to *reproduce* the issue. Besides, in your previous edit there were no classes at all, so the code would have just raised exceptions. We're here to help and we're eager to do it, but since we're doing it voluntarily (and for free) you should at least try to make our work as easy as possible, so that we can actually focus on the problem without being distracted by poorly written "pseudo-code" that is just annoying to read or understand. An important rule: imagine that you want to answer the question you're asking. – musicamante Jul 07 '23 at 01:21

2 Answers2

1

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)
musicamante
  • 41,230
  • 6
  • 33
  • 58
  • Thanks for the explanation. But the widget still doesn't move when I use `mapToParent`. In fact, calling `move` explicitly doesn't move the widget at all, despite the object getting updated with the new coordinates. I am using Ubuntu 22.04. Is your system any similar? And does the widget move visibly on drag on your system? – razor_chk Jul 07 '23 at 01:31
  • @razor_chk I'm on Linux using FluxBox (a BlackBox fork) and the code works as expected. It's possible that newer and more advanced window manager implementations have some fail safes or issues. The first thing to do is to try to debug the whole thing: try to put `print(self.pos(), newLoc)` before *and* after calling `self.move()`. Also, if you're using Wayland, that could also be a source of issues, since window management on that system is quite tricky, since it works on multiple abstract layers. Finally, if you *are* using Wayland, updating your OS might help, as that system is quite new. – musicamante Jul 07 '23 at 01:36
  • You are right. It would seem that this is a Wayland problem because the position (`self.pos()`) of the object does update after `self.move()` but the widget is still stuck in its location on screen. Interestingly enough, on Wayland, the widget moves perfectly fine when it's a child widget of another parent widget, but not when it has no parent. – razor_chk Jul 07 '23 at 01:53
  • @razor_chk When the widget is a child, Wayland can do nothing with it: it basically is a contained object of the top level window; conceptually speaking, it's like "drawing" the widget within the `paintEvent()` of the parent and managing its geometry within that parent. The "magic" (and mystery) only happens whenever the widget is a top level window, because it has to deal with the OS window manager. Now that you mentioned it, I realized that this is also documented in the notes about [Wayland Peculiarities](//doc.qt.io/qt-6/application-windows.html#wayland-peculiarities). – musicamante Jul 07 '23 at 02:13
  • @razor_chk "On Wayland, programmatically setting or getting the position of a top-level window from the client-side is typically not supported". This is a known limitation of Wayland, even if, in reality, it is a "feature": the basic concept is that the current Wayland system *can* allow window position as long as its implementation allows it; that's the drawback of highly customizable and extensible environments: you have to consider all their implications. That said, you should consider the [QWindow](//doc.qt.io/qt-6/qwindow.html) API (specifically, `startSystemMove()`). – musicamante Jul 07 '23 at 02:20
  • [@musicamante] I've come across the QWindow's API in previous searches. The main drawback I had was we couldn't really render QWidgets in a QWindow (only the other way around). I'd need to code the application interface from the ground up if I were to go with QWindow and either OpenGL or Raster graphics. At the moment, it's looking like this is my only option if I want my app to be usable across different window managers – razor_chk Jul 07 '23 at 17:06
  • @razor_chk You don't need to do any of that, because you just have to get the QWindow of the current top level widget. See the update. – musicamante Jul 07 '23 at 17:45
  • [@musicamante] That worked! I'll update the answer – razor_chk Jul 07 '23 at 18:08
0

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())
razor_chk
  • 77
  • 2
  • 9
  • 1
    The `w is not None` check is unnecessary: just verify that the widget is the top level one (`self == self.window()`) if you want to provide the functionality for top level widgets only. In fact, `windowHandle()` would return `None` for non-top level widgets, and it couldn't be an alien widget because that could only be created by `createWindowContainer()` calls, and they cannot be subclassed from there. Note that the `isinstance` check is also redundant, especially if you only need to check mouse events to move the window. In fact, you can just do that by overriding `mousePressEvent()`. – musicamante Jul 07 '23 at 18:29