小部件移动上的运行方法

Posted

技术标签:

【中文标题】小部件移动上的运行方法【英文标题】:Run Method on Widget Move 【发布时间】:2021-03-08 16:57:02 【问题描述】:

我正在尝试设计一个继承自 PyQt5 基 QLabel 类的标签类,该类能够跟踪另一个小部件。这是我班级的当前代码:

class AttachedLabel(QLabel):
    def __init__(self, attachedTo, *args, side="left", ** kwargs):
        super().__init__(*args, **kwargs) # Run parent initialization
        # Define instance variables
        self.attached = attachedTo
        self.side = side

        # Update label position
        self.updatePos()

    def updatePos(self):
        # Get "attached widget" position and dimensions
        x = self.attached.geometry().x()
        y = self.attached.geometry().y()
        aWidth = self.attached.geometry().width()
        aHeight = self.attached.geometry().height()

        # Get own dimensions
        width = self.geometry().width()
        height = self.geometry().height()

        if self.side == "top":  # Above of attached widget
            self.setGeometry(x, y-height, width, height)
        elif self.side == "bottom":  # Below attached widget
            self.setGeometry(x, y+height+aHeight, width, height)
        elif self.side == "right":  # Right of attached widget
            self.setGeometry(x + width + aWidth, y, width, height)
        else:  # Left of attached widget
            self.setGeometry(x - width, y, width, height)

我希望能够像这样实例化标签:

AttachedLabel(self.pushButton, self.centralwidget)

self.pushButton 是它应该关注的小部件。问题是我不知道如何检测小部件何时移动以运行我的updatePos() 函数。理想情况下,我只会在其他小部件移动时更新标签位置,但我想避免将额外代码添加到正在跟踪的小部件的类中。我尝试过覆盖paintEvent,但这只会在对象本身需要重绘时触发,因此它甚至不能作为次优解决方案。

我可以使用/覆盖一些内置方法来检测小部件何时移动或屏幕本身何时更新?

【问题讨论】:

【参考方案1】:

您必须使用与 QEvent::Move 事件相交的 eventFilter,您还应该通过 QEvent::Resize 事件跟踪调整大小。

from dataclasses import dataclass, field
import random

from PyQt5 import QtCore, QtWidgets


class GeometryTracker(QtCore.QObject):
    geometryChanged = QtCore.pyqtSignal()

    def __init__(self, widget):
        super().__init__(widget)
        self._widget = widget
        self.widget.installEventFilter(self)

    @property
    def widget(self):
        return self._widget

    def eventFilter(self, source, event):
        if self.widget is source and event.type() in (
            QtCore.QEvent.Move,
            QtCore.QEvent.Resize,
        ):
            self.geometryChanged.emit()
        return super().eventFilter(source, event)


@dataclass
class TrackerManager:
    widget1: field(default_factory=QtWidgets.QWidget)
    widget2: field(default_factory=QtWidgets.QWidget)
    alignment: QtCore.Qt.Alignment = QtCore.Qt.AlignLeft
    enabled: bool = True

    valid_alignments = (
        QtCore.Qt.AlignLeft,
        QtCore.Qt.AlignRight,
        QtCore.Qt.AlignHCenter,
        QtCore.Qt.AlignTop,
        QtCore.Qt.AlignBottom,
        QtCore.Qt.AlignVCenter,
    )

    def __post_init__(self):
        self._traker = GeometryTracker(self.widget1)
        self._traker.geometryChanged.connect(self.update)
        if not any(self.alignment & flag for flag in self.valid_alignments):
            raise ValueError("alignment is not valid")

    def update(self):
        if not self.enabled:
            return
        r = self.widget1.rect()

        p1 = r.center()
        c1 = r.center()

        if self.alignment & QtCore.Qt.AlignLeft:
            p1.setX(r.left())
        if self.alignment & QtCore.Qt.AlignRight:
            p1.setX(r.right())

        if self.alignment & QtCore.Qt.AlignTop:
            p1.setY(r.top())
        if self.alignment & QtCore.Qt.AlignBottom:
            p1.setY(r.bottom())

        p2 = self.convert_position(p1)
        c2 = self.convert_position(c1)

        g = self.widget2.geometry()
        g.moveCenter(c2)

        if self.alignment & QtCore.Qt.AlignLeft:
            g.moveRight(p2.x())
        if self.alignment & QtCore.Qt.AlignRight:
            g.moveLeft(p2.x())

        if self.alignment & QtCore.Qt.AlignTop:
            g.moveBottom(p2.y())
        if self.alignment & QtCore.Qt.AlignBottom:
            g.moveTop(p2.y())

        self.widget2.setGeometry(g)

    def convert_position(self, point):
        gp = self.widget1.mapToGlobal(point)
        if self.widget2.isWindow():
            return gp
        return self.widget2.parent().mapFromGlobal(gp)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.button = QtWidgets.QPushButton("Press me", self)

        self.label = QtWidgets.QLabel(
            "Tracker\nLabel", self, alignment=QtCore.Qt.AlignCenter
        )
        self.label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, True)
        self.label.setFixedSize(200, 200)
        self.label.setStyleSheet(
            "background-color: salmon; border: 1px solid black; font-size: 40pt;"
        )

        self.resize(640, 480)

        self.manager = TrackerManager(
            widget1=self.button,
            widget2=self.label,
            alignment=QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter,
        )

        self.move_button()

    def move_button(self):
        pos = QtCore.QPoint(*random.sample(range(400), 2))
        animation = QtCore.QPropertyAnimation(
            targetObject=self.button,
            parent=self,
            propertyName=b"pos",
            duration=1000,
            startValue=self.button.pos(),
            endValue=pos,
        )
        animation.finished.connect(self.move_button)
        animation.start(QtCore.QAbstractAnimation.DeleteWhenStopped)


def main():
    import sys

    app = QtWidgets.QApplication(sys.argv)

    w = MainWindow()
    w.show()

    sys.exit(app.exec_())


if __name__ == "__main__":
    main()

【讨论】:

完美,多么广泛的参考!谢谢!

以上是关于小部件移动上的运行方法的主要内容,如果未能解决你的问题,请参考以下文章

Qt 的鼠标事件

如何同时移动两个 tkinter 小部件但异步?

屏幕旋转时,小部件上的按钮单击丢失

Flutter 滚动视图到列上的焦点小部件

透明小部件不随其父级移动

android iFrame 不滚动:在保持移动兼容性的同时,将我们的“小部件”包含在客户端网页上的做法是啥