Search code examples
pythonpyqtpyqt5qpainterqmouseevent

Drawing Line from QLabel to QLabel in PyQt


I'm fairly new to PyQt I'm trying to drawing a line from 1 QLabel to another.
My 2 QLabel are located on another QLabel which acts as an image in my GUI.
I've managed to track the mouse event and move the label around, but I cannot draw the line between them using QPainter.
Thank you in advance :)

Image

This is my MouseTracking class

class MouseTracker(QtCore.QObject):
    positionChanged = QtCore.pyqtSignal(QtCore.QPoint)

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

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

    def eventFilter(self, o, e):
        if e.type() == QtCore.QEvent.MouseMove:
            self.positionChanged.emit(e.pos())
        return super().eventFilter(o, e)

This is my DraggableLabel class:

class DraggableLabel(QLabel):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.LabelIsMoving = False
        self.setStyleSheet("border-color: rgb(238, 0, 0); border-width : 2.0px; border-style:inset; background: transparent;")
        self.origin = None
        # self.setDragEnabled(True)

    def mousePressEvent(self, event):
        if not self.origin:
            # update the origin point, we'll need that later
            self.origin = self.pos()
        if event.button() == Qt.LeftButton:
            self.LabelIsMoving = True
            self.mousePos = event.pos()
            # print(event.pos())

    def mouseMoveEvent(self, event):
        if event.buttons() == Qt.LeftButton:
            # move the box
            self.move(self.pos() + event.pos() - self.mousePos)

            # print(event.pos())

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            print(event.pos())

    def paintEvent(self, event):
        painter = QPainter()
        painter.setBrush(Qt.red)
        # painter.setPen(qRgb(200,0,0))
        painter.drawLine(10, 10, 200, 200)

This is my custom class for the QTabwigdet (since I need to control and track the position of 2 QLabels whenever the user add/insert a new Tab)

class DynamicTab(QWidget):

    def __init__(self):
        super(DynamicTab, self).__init__()
        # self.count = 0
        self.setMouseTracking(True)
        self.setAcceptDrops(True)
        self.bool = True
        self.layout = QVBoxLayout(self)
        self.label = QLabel()
        self.layout.addChildWidget(self.label)

        self.icon1 = DraggableLabel(parent=self)
        #pixmap for icon 1
        pixmap = QPixmap('icon1.png')
        # currentTab.setLayout(QVBoxLayout())
        # currentTab.layout.setWidget(QRadioButton())
        self.icon1.setPixmap(pixmap)
        self.icon1.setScaledContents(True)
        self.icon1.setFixedSize(20, 20)

        self.icon2 = DraggableLabel(parent=self)
        pixmap = QPixmap('icon1.png')
        # currentTab.setLayout(QVBoxLayout())
        # currentTab.layout.setWidget(QRadioButton())
        self.icon2.setPixmap(pixmap)
        self.icon2.setScaledContents(True)
        self.icon2.setFixedSize(20, 20)
            #self.label.move(event.x() - self.label_pos.x(), event.y() - self.label_pos.y())

MainWindow and main method:

class UI_MainWindow(QMainWindow):

    def __init__(self):
        super(UI_MainWindow, self).__init__()
        self.setWindowTitle("QHBoxLayout")
        self.PictureTab = QTabWidget

    def __setupUI__(self):
        # super(UI_MainWindow, self).__init__()
        self.setWindowTitle("QHBoxLayout")
        loadUi("IIML_test2.ui", self)
        self.tabChanged(self.PictureTab)
        # self.tabChanged(self.tabWidget)
        self.changeTabText(self.PictureTab, index=0, TabText="Patient1")
        self.Button_ImportNew.clicked.connect(lambda: self.insertTab(self.PictureTab))
        # self.PictureTab.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.PictureTab))
        # self.tabWidget.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.tabWidget))

    def tabChanged(self, QtabWidget):
        QtabWidget.currentChanged.connect(lambda : print("Tab was changed to ", QtabWidget.currentIndex()))

    def changeTabText(self, QTabWidget, index, TabText):
        QTabWidget.setTabText(index, TabText)

    def insertTab(self, QtabWidget):
        # QFileDialog.getOpenFileNames(self, 'Open File', '.')
        QtabWidget.addTab(DynamicTab(), "New Tab")
        # get number of active tab
        count = QtabWidget.count()
        # change the view to the last added tab
        currentTab = QtabWidget.widget(count-1)
        QtabWidget.setCurrentWidget(currentTab)

        pixmap = QPixmap('cat.jpg')
        #currentTab.setLayout(QVBoxLayout())
        #currentTab.layout.setWidget(QRadioButton())

        # currentTab.setImage("cat.jpg")
        currentTab.label.setPixmap(pixmap)
        currentTab.label.setScaledContents(True)
        currentTab.label.setFixedSize(self.label.width(), self.label.height())
        tracker = MouseTracker(currentTab.label)
        tracker.positionChanged.connect(self.on_positionChanged)
        self.label_position = QtWidgets.QLabel(currentTab.label, alignment=QtCore.Qt.AlignCenter)
        self.label_position.setStyleSheet('background-color: white; border: 1px solid black')
        currentTab.label.show()
        # print(currentTab.label)

    @QtCore.pyqtSlot(QtCore.QPoint)
    def on_positionChanged(self, pos):
        delta = QtCore.QPoint(30, -15)
        self.label_position.show()
        self.label_position.move(pos + delta)
        self.label_position.setText("(%d, %d)" % (pos.x(), pos.y()))
        self.label_position.adjustSize()

    # def SetupUI(self, MainWindow):
    #
    #     self.setLayout(self.MainLayout)


    if __name__ == '__main__':
        app = QApplication(sys.argv)
        UI_MainWindow = UI_MainWindow()
        UI_MainWindow.__setupUI__()
        widget = QtWidgets.QStackedWidget()
        widget.addWidget(UI_MainWindow)
        widget.setFixedHeight(900)
        widget.setFixedWidth(1173)
        widget.show()
        try:
            sys.exit(app.exec_())
        except:
            print("Exiting")

My concept: I have a DynamicTab (QTabWidget) which acts as a picture opener (whenever the user press Import Now). The child of this Widget are 3 Qlabels: self.label is the picture it self and two other Qlabels are the icon1 and icon2 which I'm trying to interact/drag with (Draggable Label)

My Problem: I'm trying to track my mouse movement and custom the painter to paint accordingly. I'm trying that out by telling the painter class to paint whenever I grab the label and move it with my mouse (Hence, draggable). However, I can only track the mouse position inside the main QLabel (the main picture) whenever I'm not holding or clicking my left mouse. Any help will be appreciated here. Thank you guys.


Solution

  • Painting can only happen within the widget rectangle, so you cannot draw outside the boundaries of DraggableLabel.

    The solution is to create a further custom widget that shares the same parent, and then draw the line that connects the center of the other two.

    In the following example I install an event filter on the two draggable labels which will update the size of the custom widget based on them (so that its geometry will always include those two geometries) and call self.update() which schedules a repainting. Note that since the widget is created above the other two, it might capture mouse events that are intended for the others; to prevent that, the Qt.WA_TransparentForMouseEvents attribute must be set.

    class Line(QWidget):
        def __init__(self, obj1, obj2, parent):
            super().__init__(parent)
            self.obj1 = obj1
            self.obj2 = obj2
    
            self.obj1.installEventFilter(self)
            self.obj2.installEventFilter(self)
    
            self.setAttribute(Qt.WA_TransparentForMouseEvents)
    
        def eventFilter(self, obj, event):
            if event.type() in (event.Move, event.Resize):
                rect = self.obj1.geometry() | self.obj2.geometry()
                corner = rect.bottomRight()
                self.resize(corner.x(), corner.y())
                self.update()
            return super().eventFilter(obj, event)
    
        def paintEvent(self, event):
            painter = QPainter(self)
            painter.setRenderHint(painter.Antialiasing)
            painter.setPen(QColor(200, 0, 0))
            painter.drawLine(
                self.obj1.geometry().center(), 
                self.obj2.geometry().center()
            )
    
    
    class DynamicTab(QWidget):
        def __init__(self):
            # ...
            self.line = Line(self.icon1, self.icon2, self)
    
    

    Notes:

    • to simplify things, I only use resize() (not setGeometry()), in this way the widget will always be placed on the top left corner of the parent and we can directly get the other widget's coordinates without any conversion;
    • the custom widget is placed above the other two because it is added after them; if you want to place it under them, use self.line.lower();
    • the painter must always be initialized with the paint device argument, either by using QPainter(obj) or painter.begin(obj), otherwise no painting will happen (and you'll get lots of errors in the output);
    • do not use layout.addChildWidget() (which is used internally by the layout), but the proper addWidget() function of the layout;
    • the stylesheet border syntax can be shortened with border: 2px inset rgb(238, 0, 0);;
    • the first lines of insertTab could be simpler: currentTab = DynamicTab() QtabWidget.addTab(currentTab, "New Tab");
    • currentTab.label.setFixedSize(self.label.size());
    • QMainWindow is generally intended as a top level widget, it's normally discouraged to add it to a QStackedWidget; note that if you did that because of a Youtube tutorial, that tutorial is known for suggesting terrible practices (like the final try/except block) which should not be followed;
    • only classes and constants should have capitalized names, not variables and functions which should always start with a lowercase letter;