Home > database >  PyQt/PySide, QGraphicsView, Draw Line (Pipe) with mouse with ports on each side to attach other elem
PyQt/PySide, QGraphicsView, Draw Line (Pipe) with mouse with ports on each side to attach other elem

Time:06-19

I am trying to use PyQt (PySide6) to draw a Pipeline network like in this example gif [1]: https://i.stack.imgur.com/uQsRg.gif

I know i have to use the QGraphicsView class with QGraphicsScene to draw elements in the screen.

What i dont know how to do is how to handle all the mouse click and move events as well as having Ports on each side of the pipe to be able to attach other pipes/elements to pipes.

i also have to be able to double click on elements to configure them.

Is there any good documentation where i can learn how to achieve this ? or any tutorials ?

Thank you.

import sys
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QGraphicsView,
    QGraphicsScene,
)
from PySide6.QtGui import QAction
from PySide6.QtCore import Qt


class GraphicsScene(QGraphicsScene):
    def __init__(self):
        super().__init__()

    def mousePressEvent(self, event) -> None:
        if event.button() == Qt.LeftButton:
            print("Left button pressed")
            pos_x = event.scenePos().x()
            pos_y = event.scenePos().y()
            print(f"Position: {pos_x}, {pos_y}")


class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.scene = self.setScene(GraphicsScene())


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(450, 350)
        self.setWindowTitle("Main Window")
        self.setup_main_window()
        self.create_actions()
        self.create_menu()
        self.show()

    def setup_main_window(self):
        """Create and arrange widgets in the main window."""
        self.setCentralWidget(GraphicsView())

    def create_actions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.quit_act = QAction("&Quit")
        self.quit_act.setShortcut("Ctrl Q")
        self.quit_act.triggered.connect(self.close)

    def create_menu(self):
        """Create the application's menu bar."""
        self.menuBar().setNativeMenuBar(False)
        # Create file menu and add actions
        file_menu = self.menuBar().addMenu("File")
        file_menu.addAction(self.quit_act)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())

CodePudding user response:

It may need to write all code on your own - check button, check if there is object in small distance, remeber this object, draw this object as selected (with some extra color), update object position when move mouse, redraw all objects, etc. So it may need some list to keep all objects, search if mouse is near of one of object on list, update objects on list, use list to redraw objects on screen (in new positions)


Minimal code which use left click to add item (rectangle), and right click to delete it.

EDIT:

I found out that scene has function .items() to access all items and I don't have to use own list objects for this.

import sys
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QGraphicsView,
    QGraphicsScene,
    QGraphicsRectItem,
)
from PySide6.QtGui import QAction
from PySide6.QtCore import Qt

#objects = []

class GraphicsScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setSceneRect(-100, -100, 200, 200)
        
    def mousePressEvent(self, event) -> None:
        if event.button() == Qt.LeftButton:
            #print("Left button pressed")
            x = event.scenePos().x()
            y = event.scenePos().y()
            #print(f"Position: {x}, {y}")

            rectitem = QGraphicsRectItem(0, 0, 10, 10)
            # set center of rectangle in mouse position
            rectitem.setPos(x-5, y-5)
            self.addItem(rectitem)

            #objects.append(rectitem)
            
        elif event.button() == Qt.RightButton:
            #print("Right button pressed")
            x = event.scenePos().x()
            y = event.scenePos().y()
            #print(f"Position: {x}, {y}")

            #for item in objects:
            for item in self.items():
                pos = item.pos() 
                if abs(x-pos.x()) < 10 and abs(y-pos.y()) < 10:
                    print('selected:', item)
                    self.removeItem(item)
                    #objects.remove(item)
                    break

                
class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.scene = self.setScene(GraphicsScene())


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(450, 350)
        self.setWindowTitle("Main Window")
        self.setup_main_window()
        self.create_actions()
        self.create_menu()
        self.show()

    def setup_main_window(self):
        """Create and arrange widgets in the main window."""
        self.setCentralWidget(GraphicsView())

    def create_actions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.quit_act = QAction("&Quit")
        self.quit_act.setShortcut("Ctrl Q")
        self.quit_act.triggered.connect(self.close)

    def create_menu(self):
        """Create the application's menu bar."""
        self.menuBar().setNativeMenuBar(False)
        # Create file menu and add actions
        file_menu = self.menuBar().addMenu("File")
        file_menu.addAction(self.quit_act)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())

EDIT:

Example which use

  • left click to add rectangle (white background, black border)
  • first right click to select rectangle (red background)
  • second right click to put selected rectangle in new place (white background, black border)
import sys
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QGraphicsView,
    QGraphicsScene,
    QGraphicsRectItem,
)
from PySide6.QtGui import QAction
from PySide6.QtCore import Qt

from PySide6.QtGui import QColor, QPen, QBrush


class GraphicsScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setSceneRect(-100, -100, 200, 200)
        self.selected = []
        
    def mousePressEvent(self, event) -> None:
        if event.button() == Qt.LeftButton:
            #print("Left button pressed")
            x = event.scenePos().x()
            y = event.scenePos().y()
            #print(f"Position: {x}, {y}")

            rectitem = QGraphicsRectItem(0, 0, 10, 10)
            # set center of rectangle in mouse position
            rectitem.setPos(x-5, y-5)
            rectitem.setPen(QPen(QColor(0, 0, 0), 1.0, Qt.SolidLine))
            rectitem.setBrush(QBrush(QColor(255, 255, 255, 255)))

            self.addItem(rectitem)
            
        elif event.button() == Qt.RightButton:
            #print("Right button pressed")
            x = event.scenePos().x()
            y = event.scenePos().y()
            #print(f"Position: {x}, {y}")

            if self.selected:
                print('moved')
                for item in self.selected:
                    item.setPos(x-5, y-5)
                    item.setPen(QPen(QColor(0, 0, 0), 1.0, Qt.SolidLine))
                    item.setBrush(QBrush(QColor(255, 255, 255, 255)))
                self.selected.clear()
                
            else:
                for item in self.items():
                    pos = item.pos() 
                    if abs(x-pos.x()) < 10 and abs(y-pos.y()) < 10:
                        print('selected:', item)
                        self.selected.append(item)
                        item.setPen(QPen(QColor(255, 0, 0), 1.0, Qt.SolidLine))
                        item.setBrush(QBrush(QColor(255, 0, 0, 255)))
                        
class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.scene = self.setScene(GraphicsScene())


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(450, 350)
        self.setWindowTitle("Main Window")
        self.setup_main_window()
        self.create_actions()
        self.create_menu()
        self.show()

    def setup_main_window(self):
        """Create and arrange widgets in the main window."""
        self.setCentralWidget(GraphicsView())

    def create_actions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.quit_act = QAction("&Quit")
        self.quit_act.setShortcut("Ctrl Q")
        self.quit_act.triggered.connect(self.close)

    def create_menu(self):
        """Create the application's menu bar."""
        self.menuBar().setNativeMenuBar(False)
        # Create file menu and add actions
        file_menu = self.menuBar().addMenu("File")
        file_menu.addAction(self.quit_act)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())

EDIT:

Version which uses mouseMoveEvent and mouseReleaseEvent to drag rect (keeping right click)

Based on code in answer to pyqt add rectangle in Qgraphicsscene

import sys
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QGraphicsView,
    QGraphicsScene,
    QGraphicsRectItem,
    QGraphicsItem,
)
from PySide6.QtGui import QAction
from PySide6.QtCore import Qt

from PySide6.QtGui import QColor, QPen, QBrush, QTransform


class GraphicsScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setSceneRect(-100, -100, 200, 200)
        self.selected = None
        self.selected_offset_x = 0
        self.selected_offset_y = 0
        
    def mousePressEvent(self, event) -> None:
        if event.button() == Qt.LeftButton:
            x = event.scenePos().x()
            y = event.scenePos().y()

            rectitem = QGraphicsRectItem(0, 0, 10, 10)
            rectitem.setPos(x-5, y-5)
            rectitem.setPen(QPen(QColor(0, 0, 0), 1.0, Qt.SolidLine))
            rectitem.setBrush(QBrush(QColor(255, 255, 255, 255)))
            #rectitem.setFlag(QGraphicsItem.ItemIsMovable, True)
            
            self.addItem(rectitem)
            
        elif event.button() == Qt.RightButton:
            x = event.scenePos().x()
            y = event.scenePos().y()

            if not self.selected:
                item = self.itemAt(event.scenePos(), QTransform())
                #print(item)
    
                if item:
                    print('selected:', item)
                    self.selected = item
                    self.selected.setBrush(QBrush(QColor(255, 0, 0, 255)))
                    self.selected_offset_x = x - item.pos().x()
                    self.selected_offset_y = y - item.pos().y()
                    #self.selected_offset_x = 5  # rect_width/2   # to keep center of rect
                    #self.selected_offset_y = 5  # rect_height/2  # to keep center of rect
        #super().mousePressEvent(event)
                    
    def mouseMoveEvent(self, event):
        #print('move:', event.button())
        #print('move:', event.buttons())
        if event.buttons() == Qt.RightButton:  # `buttons()` instead of `button()`
            if self.selected:
                print('moved')
                x = event.scenePos().x()
                y = event.scenePos().y()
                self.selected.setPos(x-self.selected_offset_x, y-self.selected_offset_y)
        #super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        #print('release:', event.button())
        #print('release:', event.buttons())
        if event.button() == Qt.RightButton:
            if self.selected:
                print('released')
                self.selected.setBrush(QBrush(QColor(255, 255, 255, 255)))
                self.selected = None
        #super().mouseReleaseEvent(event)
        
class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.scene = self.setScene(GraphicsScene())


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(450, 350)
        self.setWindowTitle("Main Window")
        self.setup_main_window()
        self.create_actions()
        self.create_menu()
        self.show()

    def setup_main_window(self):
        """Create and arrange widgets in the main window."""
        self.setCentralWidget(GraphicsView())

    def create_actions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.quit_act = QAction("&Quit")
        self.quit_act.setShortcut("Ctrl Q")
        self.quit_act.triggered.connect(self.close)

    def create_menu(self):
        """Create the application's menu bar."""
        self.menuBar().setNativeMenuBar(False)
        # Create file menu and add actions
        file_menu = self.menuBar().addMenu("File")
        file_menu.addAction(self.quit_act)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())

I was thinking about changing color when mouse hover item (using mouseMoveEvent) but at this moment I don't have it.

CodePudding user response:

I found this code on here: https://python.tutorialink.com/drawing-straight-line-between-two-points-using-qpainterpath/ which lets you create a line with mousePress and move events

import sys
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QGraphicsView,
    QGraphicsScene,
)
from PySide6.QtGui import QAction, QPainterPath
from PySide6.QtCore import Qt, QPointF


class GraphicsScene(QGraphicsScene):
    def __init__(self, *args, **kwargs):
        super(GraphicsScene, self).__init__(*args, **kwargs)

        self.path_item = self.addPath(QPainterPath())

        self.start_point = QPointF()
        self.end_point = QPointF()

    def mousePressEvent(self, event):
        self.start_point = event.scenePos()
        self.end_point = self.start_point
        self.update_path()
        super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        if event.buttons() & Qt.LeftButton:
            self.end_point = event.scenePos()
            self.update_path()
        super(GraphicsScene, self).mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        self.end_point = event.scenePos()
        self.update_path()
        super(GraphicsScene, self).mouseReleaseEvent(event)

    def update_path(self):
        if not self.start_point.isNull() and not self.end_point.isNull():
            path = QPainterPath()
            path.moveTo(self.start_point)
            path.lineTo(self.end_point)
            self.path_item.setPath(path)


class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.scene = self.setScene(GraphicsScene())


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(450, 350)
        self.setWindowTitle("Main Window")
        self.setup_main_window()
        self.create_actions()
        self.create_menu()
        self.show()

    def setup_main_window(self):
        """Create and arrange widgets in the main window."""
        self.setCentralWidget(GraphicsView())

    def create_actions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.quit_act = QAction("&Quit")
        self.quit_act.setShortcut("Ctrl Q")
        self.quit_act.triggered.connect(self.close)

    def create_menu(self):
        """Create the application's menu bar."""
        self.menuBar().setNativeMenuBar(False)
        # Create file menu and add actions
        file_menu = self.menuBar().addMenu("File")
        file_menu.addAction(self.quit_act)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())

EDIT :

I combined it with Furas's code and got this

import sys
from PySide6.QtWidgets import (
    QApplication,
    QMainWindow,
    QGraphicsView,
    QGraphicsScene,
    QGraphicsRectItem,
)
from PySide6.QtGui import QAction, QPainterPath
from PySide6.QtCore import Qt

from PySide6.QtGui import QColor, QPen, QBrush, QTransform


class GraphicsScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setSceneRect(-100, -100, 200, 200)
        self.selected = None
        self.selected_offset_x = 0
        self.selected_offset_y = 0

    def mousePressEvent(self, event) -> None:
        if event.button() == Qt.LeftButton:
            x = event.scenePos().x()
            y = event.scenePos().y()

            # rectangle
            rectitem = QGraphicsRectItem(0, 0, 10, 10)
            rectitem.setPos(x - 5, y - 5)
            rectitem.setPen(QPen(QColor(0, 0, 0), 1.0, Qt.SolidLine))
            rectitem.setBrush(QBrush(QColor(255, 255, 255, 255)))
            # rectitem.setFlag(QGraphicsItem.ItemIsMovable, True)

            # Line
            self.path_item = self.addPath(QPainterPath())
            self.start_point = event.scenePos()
            self.end_point = self.start_point
            self.update_path()
            self.addItem(rectitem)

        elif event.button() == Qt.RightButton:
            x = event.scenePos().x()
            y = event.scenePos().y()

            if not self.selected:
                item = self.itemAt(event.scenePos(), QTransform())
                # print(item)

                if item:
                    print("selected:", item)
                    self.selected = item
                    self.selected.setBrush(QBrush(QColor(255, 0, 0, 255)))
                    self.selected_offset_x = x - item.pos().x()
                    self.selected_offset_y = y - item.pos().y()
                    # self.selected_offset_x = 5  # rect_width/2   # to keep center of rect
                    # self.selected_offset_y = 5  # rect_height/2  # to keep center of rect
        # super().mousePressEvent(event)

    def mouseMoveEvent(self, event):
        # print('move:', event.button())
        # print('move:', event.buttons())
        if event.buttons() == Qt.RightButton:  # `buttons()` instead of `button()`
            if self.selected:
                print("moved")
                x = event.scenePos().x()
                y = event.scenePos().y()
                self.selected.setPos(
                    x - self.selected_offset_x, y - self.selected_offset_y
                )
        elif event.buttons() == Qt.LeftButton:
            self.end_point = event.scenePos()
            self.update_path()
        # super().mouseMoveEvent(event)

    def mouseReleaseEvent(self, event):
        # print('release:', event.button())
        # print('release:', event.buttons())
        if event.button() == Qt.RightButton:
            if self.selected:
                print("released")
                self.selected.setBrush(QBrush(QColor(255, 255, 255, 255)))
                self.selected = None
        elif event.button() == Qt.LeftButton:
            self.end_point = event.scenePos()

            x = event.scenePos().x()
            y = event.scenePos().y()
            rectitem = QGraphicsRectItem(0, 0, 10, 10)
            rectitem.setPos(x - 5, y - 5)
            rectitem.setPen(QPen(QColor(0, 0, 0), 1.0, Qt.SolidLine))
            rectitem.setBrush(QBrush(QColor(255, 255, 255, 255)))
            self.addItem(rectitem)
            self.update_path()

        # super().mouseReleaseEvent(event)

    def update_path(self):
        if not self.start_point.isNull() and not self.end_point.isNull():
            path = QPainterPath()
            path.moveTo(self.start_point)
            path.lineTo(self.end_point)
            self.path_item.setPath(path)


class GraphicsView(QGraphicsView):
    def __init__(self):
        super().__init__()
        self.scene = self.setScene(GraphicsScene())


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        """Set up the application's GUI."""
        self.setMinimumSize(450, 350)
        self.setWindowTitle("Main Window")
        self.setup_main_window()
        self.create_actions()
        self.create_menu()
        self.show()

    def setup_main_window(self):
        """Create and arrange widgets in the main window."""
        self.setCentralWidget(GraphicsView())

    def create_actions(self):
        """Create the application's menu actions."""
        # Create actions for File menu
        self.quit_act = QAction("&Quit")
        self.quit_act.setShortcut("Ctrl Q")
        self.quit_act.triggered.connect(self.close)

    def create_menu(self):
        """Create the application's menu bar."""
        self.menuBar().setNativeMenuBar(False)
        # Create file menu and add actions
        file_menu = self.menuBar().addMenu("File")
        file_menu.addAction(self.quit_act)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec_())

  • Related