[Python] How to avoid circular imports with PySide6

Discussão em 'Python' iniciado por Stack, Novembro 25, 2024.

  1. Stack

    Stack Membro Participativo

    Original file, mre.py


    import sys

    from PySide6.QtCore import (Qt, QSize, QPoint, QRect,
    QEvent, QObject,
    Signal, SignalInstance, Slot,
    )

    from PySide6.QtGui import (QResizeEvent, QMoveEvent,
    QFocusEvent, )

    from PySide6.QtWidgets import (QWidget, QPushButton, QLabel,
    QHBoxLayout, QVBoxLayout,
    QMainWindow, QApplication,
    QSizePolicy, QLayout, QLayoutItem
    )

    class CustomEventFilter(QObject):
    """Installs eventFilter obj on a widget.

    Args:
    widget: QWidget instance
    """
    def __init__(self, widget: QWidget):
    # if you don't pass widget to be a parent of this evFilter,
    # then the event filter will most likely be destroyed.
    super().__init__(widget)

    # set protected variable
    self._widget = widget

    # Install this filterObj instance on the given widget
    self.widget.installEventFilter(self)

    # Encapsulation stuff
    @property
    def widget(self):
    return self._widget

    def eventFilter(self, obj: QWidget, event: QEvent):
    match (event.type()):
    case _:
    # For all events other than move and resize, handle according to QObject built-in eventFilter().
    return super().eventFilter(obj, event)

    class MeatballEventFilter(CustomEventFilter):
    def __init__(self, widget, sig: Signal = None):
    super().__init__(widget)
    self._sig = sig

    @property
    def widget(self) -> QWidget:
    return self._widget

    @property
    def sig(self) -> SignalInstance:
    return self._sig

    def eventFilter(self, obj: QWidget, event: QEvent):
    match (event.type()):
    case QEvent.FocusOut:
    # print("Lost focus!\nReason: {}".format(event.reason()))
    # if event.reason() == Qt.FocusReason.ActiveWindowFocusReason:
    # return True

    # self.sig.emit(self.widget)
    return True
    case QEvent.FocusIn:
    # print("Gained focus!")
    return True
    case _:
    return super().eventFilter(obj, event)

    class WindowEventFilter(CustomEventFilter):
    def __init__(self, widget):
    super().__init__(widget)

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

    def eventFilter(self, obj: QWidget, event:QEvent):
    match (event.type()):
    case QEvent.MouseButtonRelease:
    #TODO
    print("mouse window!")
    sub: SubBox = obj.findChild(SubBox)

    evFilter: SubBoxEventFilter = sub.findChild(SubBoxEventFilter)
    return evFilter.eventFilter(sub, event)

    # return sub.eventFilter(sub, event) # this doesn't work.
    case _:
    return super().eventFilter(obj, event)

    class SubBoxEventFilter(CustomEventFilter):
    """blah blah blah
    """
    def __init__(self, widget: QWidget):
    super().__init__(widget)

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

    def eventFilter(self, obj: QObject, event:QEvent):
    # print(type(obj))
    match (event.type()):
    case QEvent.Resize: # TODO
    # Won't matter for final product(I think?) but for test purposes will be implemented here

    return True

    case QEvent.Move:
    # Check meatball
    print("Sub moving meatball!")
    if obj.findChild(TestSongMeatball):
    meatball: TestSongMeatball = obj.findChild(TestSongMeatball)
    meatball.findPos()
    return True

    # Return True to notify app that moving took place.
    return True

    case _:
    # For all events other than move and resize, handle according to QObject built-in eventFilter().
    return super().eventFilter(obj, event)


    class TestSong(QWidget):

    # Create signals to relay info about meatball
    meatballCreated = Signal(QWidget, name="meatballCreated")
    meatballDestroyed = Signal(QWidget, name="meatballDestroyed")

    def __init__(self):
    super().__init__()

    self.setStyleSheet("background-color: blue")

    # Init layout on widget
    layout = QHBoxLayout(self)

    # Crate widgets
    label = QLabel("Label")
    meatball = TestSongButton("Meatball button")

    layout.addWidget(label)
    layout.addWidget(meatball)

    # connect clicked signal to relay into a meatballCreate signal, passing this widget
    # as a param
    meatball.clicked.connect(
    lambda checked: self.meatballCreated.emit(self))

    class TestSongButton(QPushButton):
    def __init__(self, text):
    super().__init__(text)

    self.setStyleSheet("background-color: purple")

    class TestSongMeatball(QWidget):
    def __init__(self, songWidget: TestSong, parent=None):
    super().__init__(parent)

    # Set reference to song widget
    self._songWidget = songWidget

    self.setStyleSheet("background-color: red")

    # Create sizePolicy obj, init with horizontal and vertical preferences
    policy = QSizePolicy()
    policy.setHorizontalPolicy(QSizePolicy.Policy.Preferred)
    policy.setVerticalPolicy(QSizePolicy.Policy.Preferred)

    # Set policy, min and max size of the widget.
    self.setSizePolicy(policy)
    self.setMinimumSize(QSize(70, 50))
    self.setMaximumSize(QSize(160, 80))

    fPolicy = Qt.FocusPolicy(Qt.StrongFocus)
    self.setFocusPolicy(fPolicy)

    # Assign layout to widget
    layout = QVBoxLayout(self)

    # Add demo label for clarity
    text = QLabel("Meatball Menu")
    layout.addWidget(text)

    @property
    def songWidget(self):
    return self._songWidget

    def findPos(self):
    p = self.parent()
    # First check that there is a song meatball created as a child of monitorObj (_widget)
    if p.findChild(TestSongMeatball):
    # find song item and store its position
    item_pos = self.songWidget.pos()

    # Type hint for Python intellisense purposes
    # find button within song item and store its position
    button: TestSongButton = self.songWidget.findChild(TestSongButton)
    button_pos = button.pos()

    # since item sits inside SubBox, the meatball will have the same geometry starting point (I think?)
    # So to find the button we need to add them
    x,y = item_pos.x(), item_pos.y()
    nx,ny, = button_pos.x(), button_pos.y()

    # Add co-ordinates to get button's top left corner
    meatball_pos = QPoint(x+nx,y+ny)

    # Move to new position, then allow QT to find correct size, using SizePolicy.
    self.move(meatball_pos)
    self.adjustSize()

    return

    def sizeHint(self):
    return QSize(80,60)

    class SubBox(QWidget):
    def __init__(self):
    super().__init__()

    self.setStyleSheet("background-color: green")

    # Create and apply layout
    layout = QVBoxLayout(self)

    @Slot()
    def addWid(self, item: TestSong):
    if self.findChild(TestSongMeatball):
    print("Meatball already present.")
    return

    # Create meatball widget
    meatball = TestSongMeatball(item, self)
    evFilter = MeatballEventFilter(meatball, item.meatballDestroyed)

    meatball.findPos()
    meatball.show() # show meatball
    meatball.setFocus()

    return

    @Slot()
    def remWid(self, meatball: TestSongMeatball):
    meatball.deleteLater()
    meatball = None

    return

    class TestWindow(QMainWindow):
    def __init__(self):
    super().__init__()

    window_widget = QWidget()
    window_layout = QVBoxLayout(window_widget)

    winFilter = WindowEventFilter(window_widget)

    fPolicy = Qt.FocusPolicy(Qt.StrongFocus)
    self.setFocusPolicy(fPolicy)

    #TODO: put this into a function
    # Give total layout a label
    window_layout.addWidget(QLabel("Window"))

    # Create sub-box
    sub = SubBox()

    # Install event filter to SubBox
    subFilter = SubBoxEventFilter(sub)

    test_widget = TestSong()

    sub.layout().addWidget(test_widget)

    window_layout.addWidget(sub)

    test_widget.meatballCreated.connect(
    lambda item: sub.addWid(item))

    test_widget.meatballDestroyed.connect(
    lambda meatball : sub.remWid(meatball))

    # set window as central
    self.setCentralWidget(window_widget)

    def main():
    # Create app
    app = QApplication(sys.argv)
    window = TestWindow()
    window.show()
    app.exec()

    if __name__ == '__main__':
    main()


    I am wondering the best way to split this code up. I have so far tried to move the CustomEventFilter, MeatballEventFilter, SubBoxEventFilter, WindowEventFilter classes into their own file, called EvFilter.py. Then importing SubBox, TestSongMeatball into EvFilter.py to allow QWidget.findChild() to find the widget in question.

    This left me with two files, EvFilter.py and mre.py, the former containing all subclasses of CustomEventFilter and importing SubBox, TestSongMeatball to it; and the latter containing TestSong, TestSongButton, TestSongMeatball, SubBox, TestWindow and main(), then importing all subclasses of CustomEventFilter.

    I can see how this would create a circular import clearly, and tried to remedy the issue with fixes I found online.

    When trying to import the whole file and then access the individual classes, something like:

    import mre

    sub = obj.findChild(mre.SubBox)


    The findChild() function would return None.

    Clearly I need to restructure the program to avoid this flaw, but don't know the best way of doing so. That, or I need to find the child of a QWidget without using findChild().

    EvFilter.py


    from PySide6.QtCore import (QEvent, QObject,
    Signal, SignalInstance,
    )

    from PySide6.QtWidgets import (QWidget, )

    from mre import (TestSongMeatball, SubBox, )

    class CustomEventFilter(QObject):
    """Installs eventFilter obj on a widget.

    Args:
    widget: QWidget instance
    """
    def __init__(self, widget: QWidget):
    # if you don't pass widget to be a parent of this evFilter,
    # then the event filter will most likely be destroyed.
    super().__init__(widget)

    # set protected variable
    self._widget = widget

    # Install this filterObj instance on the given widget
    self.widget.installEventFilter(self)

    # Encapsulation stuff
    @property
    def widget(self):
    return self._widget

    def eventFilter(self, obj: QWidget, event: QEvent):
    match (event.type()):
    case _:
    # For all events other than move and resize, handle according to QObject built-in eventFilter().
    return super().eventFilter(obj, event)

    class MeatballEventFilter(CustomEventFilter):
    def __init__(self, widget, sig: Signal = None):
    super().__init__(widget)
    self._sig = sig

    @property
    def widget(self) -> QWidget:
    return self._widget

    @property
    def sig(self) -> SignalInstance:
    return self._sig

    def eventFilter(self, obj: QWidget, event: QEvent):
    match (event.type()):
    case QEvent.FocusOut:
    # print("Lost focus!\nReason: {}".format(event.reason()))
    # if event.reason() == Qt.FocusReason.ActiveWindowFocusReason:
    # return True

    # self.sig.emit(self.widget)
    return True
    case QEvent.FocusIn:
    # print("Gained focus!")
    return True
    case _:
    return super().eventFilter(obj, event)

    class WindowEventFilter(CustomEventFilter):
    def __init__(self, widget):
    super().__init__(widget)

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

    def eventFilter(self, obj: QWidget, event:QEvent):
    match (event.type()):
    case QEvent.MouseButtonRelease:
    #TODO
    print("mouse window!")
    sub: SubBox = obj.findChild(SubBox)

    evFilter: SubBoxEventFilter = sub.findChild(SubBoxEventFilter)
    return evFilter.eventFilter(sub, event)

    # return sub.eventFilter(sub, event) # this doesn't work.
    case _:
    return super().eventFilter(obj, event)

    class SubBoxEventFilter(CustomEventFilter):
    """blah blah blah
    """
    def __init__(self, widget: QWidget):
    super().__init__(widget)

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

    # Actual eventFilter function. Obj will be the widget this filterObj is installed on,
    # Event is a subclass of QEvent and can be accessed through its enum:
    # QEvent.Resize is similar to QResizeEvent
    def eventFilter(self, obj: QObject, event:QEvent):
    # print(type(obj))
    match (event.type()):
    case QEvent.Resize: # TODO
    # Won't matter for final product(I think?) but for test purposes will be implemented here

    return True

    case QEvent.Move:
    # Check meatball
    print("Sub moving meatball!")
    if obj.findChild(TestSongMeatball):
    meatball: TestSongMeatball = obj.findChild(TestSongMeatball)
    meatball.findPos()
    return True

    # Return True to notify app that moving took place.
    return True

    case _:
    # For all events other than move and resize, handle according to QObject built-in eventFilter().
    return super().eventFilter(obj, event)


    mre.py, without event filters


    import sys

    from PySide6.QtCore import (Qt, QSize, QPoint, QRect,
    QEvent, QObject,
    Signal, SignalInstance, Slot,
    )

    from PySide6.QtGui import (QResizeEvent, QMoveEvent,
    QFocusEvent, )

    from PySide6.QtWidgets import (QWidget, QPushButton, QLabel,
    QHBoxLayout, QVBoxLayout,
    QMainWindow, QApplication,
    QSizePolicy, QLayout, QLayoutItem
    )

    import EvFilter

    class TestSong(QWidget):

    # Create signals to relay info about meatball
    meatballCreated = Signal(QWidget, name="meatballCreated")
    meatballDestroyed = Signal(QWidget, name="meatballDestroyed")

    def __init__(self):
    super().__init__()

    self.setStyleSheet("background-color: blue")

    # Init layout on widget
    layout = QHBoxLayout(self)

    # Crate widgets
    label = QLabel("Label")
    meatball = TestSongButton("Meatball button")

    layout.addWidget(label)
    layout.addWidget(meatball)

    # connect clicked signal to relay into a meatballCreate signal, passing this widget
    # as a param
    meatball.clicked.connect(
    lambda checked: self.meatballCreated.emit(self))

    class TestSongButton(QPushButton):
    def __init__(self, text):
    super().__init__(text)

    self.setStyleSheet("background-color: purple")

    class TestSongMeatball(QWidget):
    def __init__(self, songWidget: TestSong, parent=None):
    super().__init__(parent)

    # Set reference to song widget
    self._songWidget = songWidget

    self.setStyleSheet("background-color: red")

    # Create sizePolicy obj, init with horizontal and vertical preferences
    policy = QSizePolicy()
    policy.setHorizontalPolicy(QSizePolicy.Policy.Preferred)
    policy.setVerticalPolicy(QSizePolicy.Policy.Preferred)

    # Set policy, min and max size of the widget.
    self.setSizePolicy(policy)
    self.setMinimumSize(QSize(70, 50))
    self.setMaximumSize(QSize(160, 80))

    fPolicy = Qt.FocusPolicy(Qt.StrongFocus)
    self.setFocusPolicy(fPolicy)

    # Assign layout to widget
    layout = QVBoxLayout(self)

    # Add demo label for clarity
    text = QLabel("Meatball Menu")
    layout.addWidget(text)

    @property
    def songWidget(self):
    return self._songWidget

    def findPos(self):
    p = self.parent()
    # First check that there is a song meatball created as a child of monitorObj (_widget)
    if p.findChild(TestSongMeatball):
    # find song item and store its position
    item_pos = self.songWidget.pos()

    # Type hint for Python intellisense purposes
    # find button within song item and store its position
    button: TestSongButton = self.songWidget.findChild(TestSongButton)
    button_pos = button.pos()

    # since item sits inside SubBox, the meatball will have the same geometry starting point (I think?)
    # So to find the button we need to add them
    x,y = item_pos.x(), item_pos.y()
    nx,ny, = button_pos.x(), button_pos.y()

    # Add co-ordinates to get button's top left corner
    meatball_pos = QPoint(x+nx,y+ny)

    # Move to new position, then allow QT to find correct size, using SizePolicy.
    self.move(meatball_pos)
    self.adjustSize()

    return

    def sizeHint(self):
    return QSize(80,60)

    class SubBox(QWidget):
    def __init__(self):
    super().__init__()

    self.setStyleSheet("background-color: green")

    # Create and apply layout
    layout = QVBoxLayout(self)

    @Slot()
    def addWid(self, item: TestSong):
    if self.findChild(TestSongMeatball):
    print("Meatball already present.")
    return

    # Create meatball widget
    meatball = TestSongMeatball(item, self)
    evFilter = EvFilter.MeatballEventFilter(meatball, item.meatballDestroyed)

    meatball.findPos()
    meatball.show() # show meatball
    meatball.setFocus()

    return

    @Slot()
    def remWid(self, meatball: TestSongMeatball):
    meatball.deleteLater()
    meatball = None

    return

    class TestWindow(QMainWindow):
    def __init__(self):
    super().__init__()

    window_widget = QWidget()
    window_layout = QVBoxLayout(window_widget)

    winFilter = EvFilter.WindowEventFilter(window_widget)

    fPolicy = Qt.FocusPolicy(Qt.StrongFocus)
    self.setFocusPolicy(fPolicy)

    #TODO: put this into a function
    # Give total layout a label
    window_layout.addWidget(QLabel("Window"))

    # Create sub-box
    sub = SubBox()

    # Install event filter to SubBox
    subFilter = EvFilter.SubBoxEventFilter(sub)

    test_widget = TestSong()

    sub.layout().addWidget(test_widget)

    window_layout.addWidget(sub)

    test_widget.meatballCreated.connect(
    lambda item: sub.addWid(item))

    test_widget.meatballDestroyed.connect(
    lambda meatball : sub.remWid(meatball))

    # set window as central
    self.setCentralWidget(window_widget)

    def main():
    # Create app
    app = QApplication(sys.argv)
    window = TestWindow()
    window.show()
    app.exec()

    if __name__ == '__main__':
    main()

    Continue reading...

Compartilhe esta Página