0

I am updating the color of the pen or even the pen of a QGraphicsLine object through a method programmed within a child class of a QGraphicsLine. The problem consists in when I set the new pen the line disappears when I call it from a thread.

import sys
from PyQt5.QtGui import QColor, QBrush, QPen, QPainter
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsEllipseItem, QGraphicsSceneMouseEvent, \
    QGraphicsSceneHoverEvent, QGraphicsLineItem, QApplication, QMainWindow, QGraphicsView, QToolBar, QAction
from threading import Thread
from time import sleep
from random import randint


class GraphicsLine(QGraphicsLineItem):
    def __init__(self, x1: float, y1: float, x2: float, y2: float):
        super(GraphicsLine, self).__init__(x1, y1, x2, y2)

        pen = QPen(QColor(0, 0, 0))
        pen.setWidth(5)
        self.setPen(pen)
        self.setZValue(5)

    def change_color(self):
        pen = QPen(QColor(randint(0, 255), randint(0, 255), randint(0, 255)))
        pen.setWidth(5)
        self.setPen(pen)


class GraphicsNode(QGraphicsEllipseItem):
    def __init__(self, x: float, y: float, size: int):
        super(GraphicsNode, self).__init__(x - size/2, y - size/2, size, size)
        self.setAcceptHoverEvents(True)
        self.setZValue(10)
        brush = QBrush(QColor(0, 0, 0))
        pen = QPen(QColor(0, 0, 0))
        pen.setWidth(0)
        self.setBrush(brush)
        self.setPen(pen)

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        pass

    def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent) -> None:
        self.setBrush(QColor(0, 255, 0))

    def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent) -> None:
        self.setBrush(QColor(0, 0, 0))


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.width = 600
        self.height = 500
        self.thread_stop = False

        self.toolbar = QToolBar()
        self.action_color = QAction('Change Color', self)
        self.action_color.triggered.connect(self.__change_color)
        self.toolbar.addAction(self.action_color)
        self.action_stop = QAction('Stop Thread', self)
        self.action_stop.triggered.connect(self.__stop_thread)
        self.toolbar.addAction(self.action_stop)
        self.addToolBar(self.toolbar)

        self.scene = QGraphicsScene()
        self.graphics_view = QGraphicsView(self.scene, self)
        self.graphics_view.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing)
        self.graphics_view.setGeometry(0, 20, 600, 500)
        self.__create_graphs()
        self.show()

    def __create_graphs(self):
        self.nodes = [GraphicsNode(0, 0, 20), GraphicsNode(0, 100, 20)]
        self.lines = [GraphicsLine(0, 0, 0, 100)]
        for node in self.nodes:
            self.scene.addItem(node)
        for line in self.lines:
            self.scene.addItem(line)
        self.thread = Thread(target=self.__color_changer_thread)
        self.thread.start()

    def __color_changer_thread(self):
        while not self.thread_stop:
            for line in self.lines:
                line.change_color()
            sleep(5)

    def __change_color(self):
        for line in self.lines:
            line.change_color()

    def __stop_thread(self):
        self.thread_stop = True


if __name__ == '__main__':
    app = QApplication(sys.argv)

    window = MainWindow()
    app.exec_()

The color of the object GraphicsLine is changed when clicking Action in ToolBar but, the thread makes the item not visible. If the thread is commented, then the Action bottom changes the color without trouble.

Thank you in advance.

Kayzh3r
  • 3
  • 4
  • 1
    Please provide a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) (for instance, what are `List`, `Optional` and `GraphicsNode`?). Also, you call `self.hide()` in `set_linked`, and you shouldn't to call `self.update(self.boundingRect())` after setting the pen, as the item will automatically update itself. – musicamante Sep 04 '20 at 11:32
  • @musicamante Sorry, I made a mistake copying the code, self.hide() was for debugging after when debugger stopped I call self.show(), but the result was the same. The behavior in all method is the same (set_linked, set_frozen and set_disabled). Also List, Optional are from typing (type hints) and GraphicsNode is a class children from QGraphicsEllipseItem. – Kayzh3r Sep 04 '20 at 12:49
  • Please, carefully read the link in my first comment. Your example has to be **both** minimal **and** reproducible. What are `Callout` and `NetworkNode`? Where is the part where those graphics items are created? Don't just copy&paste your code, ensure that it is *reproducible*, as *we* must be able to copy, paste and run it, possibly with minimal modifications (or none at all); you can't expect us to edit a whole 150 lines example to make it runnable. Help us to help you. – musicamante Sep 04 '20 at 13:30
  • @musicamante I am so sorry for the code You were right, the code was not clear. I hope this new version to be easier to understand – Kayzh3r Sep 04 '20 at 16:09
  • 1
    You should have specified from the beginning that you were trying to do that using threads. Accessing widgets is only allowed from the *main* Qt thread, any other way is highly discouraged as it usually leads to bugs or unexpected behavior, exactly like in your case. If you want to modify anything in the GUI you need to use a subclass of QThread and communicate with the main thread using signals and slots. Look up for it, there's plenty of questions and answers even here on SO. – musicamante Sep 04 '20 at 16:14

1 Answers1

0

Many thanks to @musicamante.

Solution The solution consists in creating a QThread instead of a POSIX thread and inside the QThread subclass create a signal and connect it to a slot created within the Widget. (ref: Modify Qt GUI from background worker thread)

import sys
from PyQt5.QtCore import QThread, pyqtSlot, pyqtSignal
from PyQt5.QtGui import QColor, QBrush, QPen, QPainter
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsEllipseItem, QGraphicsSceneMouseEvent, \
    QGraphicsSceneHoverEvent, QGraphicsLineItem, QApplication, QMainWindow, QGraphicsView, QToolBar, QAction
from threading import Thread
from time import sleep
from random import randint


class ColorChangingThread(QThread):
    signal_color_change = pyqtSignal(int, name='Scheduled color change')

    def __init__(self):
        super(ColorChangingThread, self).__init__()
        self.stop_thread = False

    def run(self) -> None:
        while not self.stop_thread:
            self.signal_color_change.emit(0)
            self.sleep(5)


class GraphicsLine(QGraphicsLineItem):
    def __init__(self, x1: float, y1: float, x2: float, y2: float):
        super(GraphicsLine, self).__init__(x1, y1, x2, y2)

        pen = QPen(QColor(0, 0, 0))
        pen.setWidth(5)
        self.setPen(pen)
        self.setZValue(5)

    def change_color(self):
        pen = QPen(QColor(randint(0, 255), randint(0, 255), randint(0, 255)))
        pen.setWidth(5)
        self.setPen(pen)


class GraphicsNode(QGraphicsEllipseItem):
    def __init__(self, x: float, y: float, size: int):
        super(GraphicsNode, self).__init__(x - size/2, y - size/2, size, size)
        self.setAcceptHoverEvents(True)
        self.setZValue(10)
        brush = QBrush(QColor(0, 0, 0))
        pen = QPen(QColor(0, 0, 0))
        pen.setWidth(0)
        self.setBrush(brush)
        self.setPen(pen)

    def mousePressEvent(self, event: QGraphicsSceneMouseEvent) -> None:
        pass

    def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent) -> None:
        self.setBrush(QColor(0, 255, 0))

    def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent) -> None:
        self.setBrush(QColor(0, 0, 0))


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)
        self.width = 600
        self.height = 500
        self.thread = ColorChangingThread()
        self.thread.signal_color_change.connect(self.slot_color_change)

        self.toolbar = QToolBar()
        self.action_color = QAction('Change Color', self)
        self.action_color.triggered.connect(self.__change_color)
        self.toolbar.addAction(self.action_color)
        self.action_stop = QAction('Stop Thread', self)
        self.action_stop.triggered.connect(self.__stop_thread)
        self.toolbar.addAction(self.action_stop)
        self.addToolBar(self.toolbar)

        self.scene = QGraphicsScene()
        self.graphics_view = QGraphicsView(self.scene, self)
        self.graphics_view.setRenderHints(QPainter.Antialiasing | QPainter.HighQualityAntialiasing)
        self.graphics_view.setGeometry(0, 20, 600, 500)
        self.__create_graphs()
        self.show()

    def __create_graphs(self):
        self.nodes = [GraphicsNode(0, 0, 20), GraphicsNode(0, 100, 20)]
        self.lines = [GraphicsLine(0, 0, 0, 100)]
        for node in self.nodes:
            self.scene.addItem(node)
        for line in self.lines:
            self.scene.addItem(line)
        self.thread.start()

    @pyqtSlot(int)
    def slot_color_change(self, value):
        print('Received signal with value %d' % value)
        self.__change_color()

    def __change_color(self):
        for line in self.lines:
            line.change_color()

    def __stop_thread(self):
        self.thread.stop_thread = True


if __name__ == '__main__':
    app = QApplication(sys.argv)

    window = MainWindow()
    app.exec_()
Kayzh3r
  • 3
  • 4