PyQt5: drawing multiple rectangles using mouseEvents by implementing QGraphicsScene, QGraphicsView and QGraphicsItem [duplicate] - pyqt5

I have a scene like this
class Scene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(Scene, self).__init__(parent)
def mousePressEvent(self, event):
print('scene pressed')
self.wid = MyRect(event.pos(), event.pos())
self.addItem(self.wid)
self.wid.show()
I would like class MyRect(QtWidgets.QGraphicsRectItem) with painter, mouse event and so on to be a draggable rectangle.
all stuff in MyRect
So then I could have many Rectangle to the scene and even after draw line between them and so on (kind of diagram app), but keeping objects related editable options in MyRect, MyLine , ....
I thought :
class MyRect(QtWidgets.QGraphicsRectItem):
def __init__(self, begin, end, parent=None):
super().__init__(parent)
self.begin = begin
self.end = end
def paintEvent(self, event):
print('painting')
qp = QtGui.QPainter(self)
qp.drawRect(QtCore.QRect(self.begin, self.end))
def mousePressEvent(self, event):
self.begin = event.pos()
self.end = event.pos()
self.update()
def mouseMoveEvent(self, event):
self.end = event.pos()
self.update()
def mouseReleaseEvent(self, event):
self.begin = event.pos()
self.end = event.pos()
self.update()
But I does not work (paint event not initiated whereas mousepressed event in scene is intiated)
I did not find what I wanted through the web so started totry do it by myself. I'm pretty sure it is a must known starting point but I cannot find it

First of all a QGraphicsItem is not a QWidget, so it has those events and does not handle them directly, that's what QGraphicsView and QGraphicsScene do. For example you say that you want to have a moveable rectangle because that task is simple is QGraphicsView, it is not necessary to overwrite:
from PyQt5 import QtCore, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
view = QtWidgets.QGraphicsView(scene)
self.setCentralWidget(view)
rect_item = QtWidgets.QGraphicsRectItem(QtCore.QRectF(0, 0, 100, 100))
rect_item.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
scene.addItem(rect_item)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
If you want to change the way you paint the rectangle you must overwrite the paint() method as shown below:
from PyQt5 import QtCore, QtGui, QtWidgets
class RectItem(QtWidgets.QGraphicsRectItem):
def paint(self, painter, option, widget=None):
super(RectItem, self).paint(painter, option, widget)
painter.save()
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setBrush(QtCore.Qt.red)
painter.drawEllipse(option.rect)
painter.restore()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
view = QtWidgets.QGraphicsView(scene)
self.setCentralWidget(view)
rect_item = RectItem(QtCore.QRectF(0, 0, 100, 100))
rect_item.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
scene.addItem(rect_item)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
Update:
from PyQt5 import QtCore, QtGui, QtWidgets
class GraphicsScene(QtWidgets.QGraphicsScene):
def __init__(self, parent=None):
super(GraphicsScene, self).__init__(QtCore.QRectF(-500, -500, 1000, 1000), parent)
self._start = QtCore.QPointF()
self._current_rect_item = None
def mousePressEvent(self, event):
if self.itemAt(event.scenePos(), QtGui.QTransform()) is None:
self._current_rect_item = QtWidgets.QGraphicsRectItem()
self._current_rect_item.setBrush(QtCore.Qt.red)
self._current_rect_item.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
self.addItem(self._current_rect_item)
self._start = event.scenePos()
r = QtCore.QRectF(self._start, self._start)
self._current_rect_item.setRect(r)
super(GraphicsScene, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self._current_rect_item is not None:
r = QtCore.QRectF(self._start, event.scenePos()).normalized()
self._current_rect_item.setRect(r)
super(GraphicsScene, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self._current_rect_item = None
super(GraphicsScene, self).mouseReleaseEvent(event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
scene =GraphicsScene(self)
view = QtWidgets.QGraphicsView(scene)
self.setCentralWidget(view)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

Related

How to get log and show it on GUI from multiprocessing work?

I try to get logs from multiprocessing work and show them on GUI.
Based on this document
gui.py:
from PyQt5 import QtCore, QtWidgets
import logging
from log_test import main
Signal = QtCore.pyqtSignal
Slot = QtCore.pyqtSlot
class Signaller(QtCore.QObject):
signal = Signal(str, logging.LogRecord)
class QtHandler(logging.Handler):
def __init__(self, slotfunc, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signaller = Signaller()
self.signaller.signal.connect(slotfunc)
def emit(self, record):
s = self.format(record)
self.signaller.signal.emit(s, record)
class Worker(QtCore.QObject):
finished = Signal()
#Slot()
def start(self):
main()
self.finished.emit()
class Ui_Dialog(QtCore.QObject):
def __init__(self):
super().__init__()
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.setEnabled(True)
Dialog.resize(530, 440)
self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
self.verticalLayout.setObjectName("verticalLayout")
self.button = QtWidgets.QPushButton(Dialog)
self.button.setText("start working")
self.verticalLayout.addWidget(self.button)
self.logWidget = QtWidgets.QPlainTextEdit(Dialog)
self.logWidget.setReadOnly(True)
self.verticalLayout.addWidget(self.logWidget)
self.handler = QtHandler(self.update_log_gui)
logging.getLogger('log').addHandler(self.handler)
self.button.clicked.connect(self.start_work)
#Slot(str, logging.LogRecord)
def update_log_gui(self, status, record):
self.logWidget.appendPlainText(status)
def config_thread(self):
self.worker_thread = QtCore.QThread()
self.worker_thread.setObjectName('WorkerThread')
self.worker = Worker()
self.worker.moveToThread(self.worker_thread)
self.worker_thread.started.connect(self.worker.start)
self.worker.finished.connect(self.worker_thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.worker_thread.finished.connect(self.worker_thread.deleteLater)
self.worker_thread.finished.connect(lambda: self.button.setEnabled(True))
pass
def start_work(self):
self.config_thread()
self.worker_thread.start()
self.button.setEnabled(False)
if __name__ == "__main__":
import sys
QtCore.QThread.currentThread().setObjectName('MainThread')
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
ui = Ui_Dialog()
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec_())
log_test.py (where multiprocessing work happens)
import logging
import time
from multiprocessing import Pool
def f(name):
logger = logging.getLogger('log.' + name)
logger.error('hello there 1')
time.sleep(0.5)
logger.error('hello there 2')
time.sleep(0.5)
logger.error('hello there 3')
time.sleep(0.5)
def main():
with Pool(5) as p:
p.map(f, ['aaa', 'bbb', 'ccc'])
At first time, I thought working in single thread causing the problem. So I added QThread to this.
Later I discovered in debug, it seems to QtHandler.emit() works fine at receiving log messages. But the connected slot function, update_log_gui() does not work somehow.
I solved it myself.
#Alexander was right. Indeed my QtHandler has a problem when multiprocessing but I don't know exactly why. Rather, you wanna implement QueueHandler. An example in this article (Written in Korean) helped me.
from PyQt5 import QtCore, QtWidgets
import logging
import multiprocessing
from log_test import main
Signal = QtCore.pyqtSignal
Slot = QtCore.pyqtSlot
QThread = QtCore.QThread
class Signaller(QtCore.QObject):
signal = Signal(logging.LogRecord)
class Worker(QtCore.QObject):
finished = Signal()
def __init__(self, q):
super().__init__()
self.q = q
#Slot()
def start(self):
main(self.q)
self.finished.emit()
class Consumer(QThread):
popped = Signaller()
def __init__(self, q):
super().__init__()
self.q = q
self.setObjectName('ConsumerThread')
def run(self):
while True:
if not self.q.empty():
record = self.q.get()
self.popped.signal.emit(record)
class Ui_Dialog(QtCore.QObject):
def __init__(self, app):
super().__init__()
self.app = app
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.setEnabled(True)
Dialog.resize(530, 440)
self.verticalLayout = QtWidgets.QVBoxLayout(Dialog)
self.verticalLayout.setObjectName("verticalLayout")
self.button = QtWidgets.QPushButton(Dialog)
self.button.setText("start working")
self.verticalLayout.addWidget(self.button)
self.logWidget = QtWidgets.QPlainTextEdit(Dialog)
self.logWidget.setReadOnly(True)
self.verticalLayout.addWidget(self.logWidget)
self.button.clicked.connect(self.start_work)
self.q = multiprocessing.Manager().Queue()
self.consumer = Consumer(self.q)
self.consumer.popped.signal.connect(self.update_log_gui)
self.consumer.start()
app.aboutToQuit.connect(self.shutdown_consumer)
#Slot(logging.LogRecord)
def update_log_gui(self, record):
self.logWidget.appendPlainText(str(record.msg))
def config_thread(self):
self.worker_thread = QtCore.QThread()
self.worker_thread.setObjectName('WorkerThread')
self.worker = Worker(self.q)
self.worker.moveToThread(self.worker_thread)
self.worker_thread.started.connect(self.worker.start)
self.worker.finished.connect(self.worker_thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.worker_thread.finished.connect(self.worker_thread.deleteLater)
self.worker_thread.finished.connect(lambda: self.button.setEnabled(True))
def start_work(self):
self.config_thread()
self.worker_thread.start()
self.button.setEnabled(False)
def shutdown_consumer(self):
if self.consumer.isRunning():
self.consumer.requestInterruption()
self.consumer.quit()
self.consumer.wait()
if __name__ == "__main__":
import sys
QtCore.QThread.currentThread().setObjectName('MainThread')
app = QtWidgets.QApplication(sys.argv)
Dialog = QtWidgets.QDialog()
ui = Ui_Dialog(app)
ui.setupUi(Dialog)
Dialog.show()
sys.exit(app.exec_())

How to add multiple scenes to a graphicsview or layers to a scene that would allow me to just hide/show certain marks in PyQt5

I want to know how to keep scene widgets after add them to scene for later.
After user's click on marker button, marker feature active and with rightclick can mark points in QGraphicscene.
Here is my code:
from PyQt5.QtWidgets import QMainWindow, QToolButton, QGraphicsScene, QGraphicsPixmapItem
from PyQt5.QtGui import QPixmap, QIcon, QPainter
from PyQt5 import uic
from PyQt5.QtCore import Qt
class MarkerButton(QToolButton):
def __init__(self, *args, **kwargs):
super(MarkerButton, self).__init__(*args, **kwargs)
def mousePressEvent(self, event):
if event.buttons() & Qt.LeftButton:
self.deleteLater()
super().mousePressEvent(event)
class MarkerScene(QGraphicsScene):
def __init__(self, *args, **kwargs):
super(MarkerScene, self).__init__(*args, **kwargs)
self.marker_widgets = dict()
def mousePressEvent(self, event):
if event.buttons() & Qt.RightButton:
self.start_point = event.scenePos()
self.update_path()
super().mousePressEvent(event)
def update_path(self):
if not self.start_point.isNull():
pushButton_icon = QIcon()
pushButton_icon.addPixmap(QPixmap('assets/mark.png'))
new_marker = MarkerButton()
new_marker.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
new_marker.move(int(self.start_point.x()), int(self.start_point.y()))
new_marker.setIcon(pushButton_icon)
new_marker.setText(str(len(self.marker_widgets)))
self.addWidget(new_marker)
self.marker_widgets[new_marker] = self.start_point
class Ui(QMainWindow):
def __init__(self):
super(Ui, self).__init__() # Call the inherited classes __init__ method
self.Window_obj= uic.loadUi('main.ui')
self.Window_obj.setWindowFlags(Qt.FramelessWindowHint)
pix = QPixmap('assets/data.png')
item = QGraphicsPixmapItem(pix)
scene = QGraphicsScene(self)
scene.addItem(item)
self.Window_obj.graphicsViewScene1.setScene(scene)
self.Window_obj.show()
def AddMarker(self):
if not self.active_toolbox_markers:
self.active_toolbox_markers = True
self.main_Window_obj.graphicsViewScene1.setRenderHint(QPainter.Antialiasing)
self.Window_obj.graphicsViewScene1.setMouseTracking(True)
scene = MarkerScene()
self.main_Window_obj.graphicsViewScene1.setScene(scene)
else:
self.active_toolbox_markers = False
I need to save this markers for later but user have to be able to hide them, the reason is because I have another tool that I want not conflict with this markers.
At the end, after add markers, the main Qpixmap removed.
my app something like this

cannot be associated with QComboBox

button of class Main don't connect with class Qcombobox of Signals
from PyQt5.QtCore import QObject, pyqtSignal
from PyQt5.QtWidgets import *
import sys
from PyQt5 import QtGui
class Signals(QWidget):
asignal = pyqtSignal(str)
def __init__(self):
super(Signals, self).__init__()
self.setGeometry(300, 250, 400, 300)
self.ii()
self.show()
def ii(self):
vbox = QVBoxLayout()
self.combo = QComboBox()
self.combo.addItem("Python")
self.combo.addItem("Java")
self.combo.addItem("C++")
self.combo.addItem("C#")
self.combo.addItem("Ruby")
self.buttom = QPushButton("Click")
self.buttom.clicked.connect(self.windown2)
vbox.addWidget(self.combo)
vbox.addWidget(self.buttom)
self.setLayout(vbox)
def do_something(self):
self.asignal.emit(self.combo.currentText())
def windown2(self):
self.ggpp = Main()
self.ggpp.show()
class Main(QWidget):
def __init__(self):
super(Main, self).__init__()
self.setGeometry(500,150, 600, 300)
vbox1 = QVBoxLayout()
self.buttom1 = QPushButton("Click")
self.buttom1.clicked.connect(self.coso1)
vbox1.addWidget(self.buttom1)
self.setLayout(vbox1)
def coso1(self):
s = Signals()
s.asignal.connect(lambda sig: print("self.combo.currentText()>>>>>" + sig))
s.do_something()
if __name__ == '__main__':
app = QApplication(sys.argv)
nals = Signals()
nals.show()
sys.exit(app.exec())
What you see happens because you're not using the existing instance of Signals, but you're creating a new one each time the button is clicked.
In your case, you could add a reference to the instance as an argument when you create the new window, so that you can correctly connect to its signal.
class Signals(QWidget):
# ...
def windown2(self):
self.ggpp = Main(self)
self.ggpp.show()
class Main(QWidget):
def __init__(self, signals):
super(Main, self).__init__()
self.signals = signals
self.signals.asignal.connect(self.coso1)
self.setGeometry(500,150, 600, 300)
vbox1 = QVBoxLayout()
self.buttom1 = QPushButton("Click")
self.buttom1.clicked.connect(self.signals.do_something)
vbox1.addWidget(self.buttom1)
self.setLayout(vbox1)
def coso1(self, sig):
print("self.combo.currentText()>>>>>" + sig)

how to transmit true value to formal parameter in decorator function(pyqtSlot())?

first, see code below:
import sys
from PyQt5.QtCore import (Qt, pyqtSignal, pyqtSlot)
from PyQt5.QtWidgets import (QWidget, QLCDNumber, QSlider,
QVBoxLayout, QApplication)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def printLabel(self, str):
print(str)
#pyqtSlot(int)
def on_sld_valueChanged(self, value):
self.lcd.display(value)
self.printLabel(value)
def initUI(self):
self.lcd = QLCDNumber(self)
self.sld = QSlider(Qt.Horizontal, self)
vbox = QVBoxLayout()
vbox.addWidget(self.lcd)
vbox.addWidget(self.sld)
self.setLayout(vbox)
self.sld.valueChanged.connect(self.on_sld_valueChanged)
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Signal & slot')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
I'm a little puzzled about how the true value in sld is transmitted to the formal parameter 'value' in the slot function : def sld_valChanged(self, value).
Because i can't see something like this: self.sld.valueChanged.connect(partial(self.sld_valChanged, self.sld.value))
Could someone explain that?

Resizing a QWindow to fit contents

The main window of my PyQt5 application is set up with a text label along the top above a custom canvas widget which displays an image:
from PyQt5 import QtCore, QtGui, QtWidgets
class Canvas(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.image = None
def paintEvent(self, event):
qp = QtGui.QPainter(self)
if self.image:
qp.drawImage(0, 0, self.image)
class Window(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.canvas = Canvas()
self.label = QtWidgets.QLabel()
self.label.setText('foobar')
self.label.setSizePolicy(QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Fixed)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.canvas)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
content = QtWidgets.QWidget()
content.setLayout(layout)
self.setCentralWidget(content)
self.load_image('a.jpg')
def load_image(self, filename):
image = QtGui.QImage(filename)
self.canvas.image = image
self.canvas.setFixedSize(image.width(), image.height())
self.update()
def keyPressEvent(self, event):
self.load_image('b.jpg')
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
This looks like this, which is what I want:
When the canvas changes to display a smaller image, I want to shrink the window to fit accordingly. However, it looks like this:
It seems that the minimum size that I can give the window if I manually drag to resize it is the size that fits the contents, but why isn't it resizing to this automatically?
When a fixed size is set, it is used as sizeHint, and the latter is used by layouts to set the widget size. So the size of the canvas depends on the size of the widget, but you want the opposite. You must scale the image size to the window size:
from PyQt5 import QtCore, QtGui, QtWidgets
class Canvas(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding
)
self.image = QtGui.QImage()
#property
def image(self):
return self._image
#image.setter
def image(self, image):
self._image = image
self.update()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
if not self.image.isNull():
image = self.image.scaled(
self.size(), QtCore.Qt.IgnoreAspectRatio, QtCore.Qt.SmoothTransformation
)
qp.drawImage(0, 0, image)
class Window(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.canvas = Canvas()
self.label = QtWidgets.QLabel("foobar")
self.label.setSizePolicy(
QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed
)
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.label)
layout.addWidget(self.canvas)
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
content = QtWidgets.QWidget()
content.setLayout(layout)
self.setCentralWidget(content)
self.load_image("a.jpg")
def load_image(self, filename):
image = QtGui.QImage(filename)
self.canvas.image = image
def keyPressEvent(self, event):
self.load_image('b.jpg')
super().keyPressEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())