PyQt5: Questions about QGraphicsScene's itemAt() and focusItemChanged() - python

I have two questions:
what does QTransform() mean in itemAt()? The sentence below is what it says in Qt doc, but I can't understand:
deviceTransform is the transformation that applies to the view, and needs to be provided if the scene contains items that ignore transformations.
why focusItemChanged signal is not working?
Here is my code:
import sys
from PyQt5.QtGui import QTransform
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsScene, QGraphicsView
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 300, 300)
self.rect = self.scene.addRect(100, 30, 100, 30)
self.ellipse = self.scene.addEllipse(100, 80, 50, 40)
self.rect.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
self.ellipse.setFlags(QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable)
self.setScene(self.scene)
# Question 1
print(self.scene.itemAt(110, 40, QTransform()))
# Question 2
self.scene.focusItemChanged.connect(self.my_slot)
def my_slot(self, new_item, old_item):
print(new_item)
print(old_item)
if __name__ == '__main__':
app = QApplication(sys.argv)
demo = Demo()
demo.show()
sys.exit(app.exec_())
Any help would be appreciated.

1. what does QTransform() mean in itemAt()?
As it indicates the docs it is only necessary to pass the deviceTransform if there is an item that ignores the transformations, then how is it done so that an item does not support transformation? you must enable the flag Qt::ItemIgnoresTransformations.
With your code you can not see the difference so I have implemented the following example where there are 2 items one with the flag ItemIgnoresTransformations activated and the other not. Then when you press any of the items it is expected that the item will be printed in the console but you will see that the item that has the ItemIgnoresTransformations flag returns None if you pass QTransform (), if you press the radiobutton to pass the viewportTransform() you will see that Now both items are printed on the console. So you must pass the deviceTransform if there is any item with the flag ItemIgnoresTransformations enabled.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class Demo(QtWidgets.QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self._scene = QtWidgets.QGraphicsScene()
self._scene.setSceneRect(0, 0, 300, 300)
self.setScene(self._scene)
self.rect1 = self._scene.addRect(
100, 30, 100, 30, brush=QtGui.QBrush(QtGui.QColor("red"))
)
self.rect1.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations)
self.rect2 = self._scene.addRect(
200, 30, 100, 30, brush=QtGui.QBrush(QtGui.QColor("green"))
)
self.rotate(50)
self._use_deviceTransform = False
def mousePressEvent(self, event):
sp = self.mapToScene(event.pos())
item = self._scene.itemAt(
sp,
self.viewportTransform()
if self._use_deviceTransform
else QtGui.QTransform(),
)
print(item)
def set_use_deviceTransform(self, t):
self._use_deviceTransform = t
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
radiobutton = QtWidgets.QRadioButton("use deviceTransform")
demo = Demo()
radiobutton.toggled.connect(demo.set_use_deviceTransform)
w = QtWidgets.QWidget()
lay = QtWidgets.QVBoxLayout(w)
lay.addWidget(radiobutton)
lay.addWidget(demo)
w.show()
w.resize(640, 480)
sys.exit(app.exec_())
2. why focusItemChanged signal is not working?
The signal is triggered if the focus of an item changes, but by default the items have no focus so the signal is not emitted, the solution is to activate the flag QGraphicsItem::ItemIsFocusable:
import sys
from PyQt5.QtGui import QTransform
from PyQt5.QtWidgets import QApplication, QGraphicsItem, QGraphicsScene, QGraphicsView
from PyQt5.QtCore import pyqtSlot, Qt
class Demo(QGraphicsView):
def __init__(self):
super(Demo, self).__init__()
self.resize(300, 300)
self.scene = QGraphicsScene()
self.scene.setSceneRect(0, 0, 300, 300)
self.rect = self.scene.addRect(100, 30, 100, 30)
self.ellipse = self.scene.addEllipse(100, 80, 50, 40)
self.rect.setFlags(
QGraphicsItem.ItemIsMovable
| QGraphicsItem.ItemIsSelectable
| QGraphicsItem.ItemIsFocusable
)
self.ellipse.setFlags(
QGraphicsItem.ItemIsMovable
| QGraphicsItem.ItemIsSelectable
| QGraphicsItem.ItemIsFocusable
)
self.setScene(self.scene)
self.scene.focusItemChanged.connect(self.my_slot)
#pyqtSlot("QGraphicsItem*", "QGraphicsItem*", Qt.FocusReason)
def my_slot(self, new_item, old_item, reason):
print(old_item, new_item)

Related

Pyqt5 draw a line between two widgets

I am trying to use QPainter to draw a line between two widgets. If I use a simple function inside the first class it works. But, I want to create a separate class of a QPainter event, that I can call at the first class whenever I want. But, it is not working as expected. Can you help me to figure out why the QPainter class is not adding a line.
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.okButton = QPushButton("OK")
self.cancelButton = QPushButton("Cancel")
l1 = self.okButton.pos()
l2 = self.cancelButton.pos()
# This is to call the class to draw a line between those two widgets
a = QPaint(l1.x(), l1.y(), l2.x(), l2.y(),parent=self)
vbox = QVBoxLayout()
vbox.addWidget(self.okButton)
vbox.addWidget(self.cancelButton)
self.setLayout(vbox)
self.setGeometry(300, 300, 300, 150)
self.setWindowTitle('Buttons')
self.show()
class QPaint(QPainter):
def __init__(self, x1, y1, x2, y2, parent=None):
super().__init__()
def paintEvent(self, event):
self.setPen(Qt.red)
self.drawLine(x1,y1,x2,y2)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Widgets can only be painted in the widget's paintEvent method, so if you don't want to paint it in the same class then you can use multiple inheritance. On the other hand, the initial positions you use to paint will be the positions before showing that they are 0 making no line is painted but a point so it is better to track the positions using an event filter.
import sys
from PyQt5.QtCore import QEvent
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
class Drawer:
def paintEvent(self, event):
painter = QPainter(self)
painter.drawLine(self.p1, self.p2)
class Example(QWidget, Drawer):
def __init__(self, parent=None):
super().__init__(parent)
self.initUI()
def initUI(self):
self.okButton = QPushButton("OK")
self.cancelButton = QPushButton("Cancel")
vbox = QVBoxLayout(self)
vbox.addWidget(self.okButton)
vbox.addWidget(self.cancelButton)
self.setGeometry(300, 300, 300, 150)
self.setWindowTitle("Buttons")
self.p1, self.p2 = self.okButton.pos(), self.cancelButton.pos()
self.okButton.installEventFilter(self)
self.cancelButton.installEventFilter(self)
def eventFilter(self, o, e):
if e.type() == QEvent.Move:
if o is self.okButton:
self.p1 = self.okButton.pos()
elif o is self.cancelButton:
self.p2 = self.cancelButton.pos()
self.update()
return super().eventFilter(o, e)
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())

Embed QGraphics in widget

I have a main widget inside a window who contains a lot of widgets. How can I insert a QGraphics view and a QGraphicsScene in that widget? I have not found a direct insertion method, so I am trying using a wrapper, in this case a box layout but it is not a good solution. The QGraphicsScene stands out from the layout limits.
Code:
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(1500, 1015)
widget_central = QtWidgets.QWidget(self)
wrapper = QtWidgets.QHBoxLayout(widget_central)
scene = QtWidgets.QGraphicsScene(wrapper)
vista = QtWidgets.QGraphicsView(scene)
wrapper.addWidget(vista)
self.diedrico = Diedrico() # This is a class who draw things, not relevant
self.diedrico.setFixedSize(2000, 2000)
scene.addWidget(self.diedrico)
self.setCentralWidget(widget_central)
I would like to get this result:
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen, QColor
import sys
class Diedrico(QWidget):
def __init__(self, parent):
super().__init__(parent)
def paintEvent(self, event):
qp = QPainter(self)
qp.setPen(QPen(QColor(Qt.black), 5))
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(520, 520)
self.widget_central = QtWidgets.QWidget(self)
scrol = QtWidgets.QScrollArea(self.widget_central)
scrol.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scrol.setGeometry(QtCore.QRect(30, 30, 500, 500))
scrol.setWidgetResizable(False)
contenido = QtWidgets.QWidget()
contenido.setGeometry(QtCore.QRect(0, 0, 2000, 2000))
scrol.setWidget(contenido)
self.Diedrico = Diedrico(contenido)
self.Diedrico.setGeometry(QtCore.QRect(0, 0, 2000, 2000))
self.setCentralWidget(self.widget_central)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())
But using QGraphics instead of a scroll area
The QGraphicsProxyWidget that is created using the widget takes into account the minimum size of the widget to set the boundingRect, and the QGraphicsScene uses the boundingRect to set the initial scene rect.
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
def minimumSizeHint(self):
return QtCore.QSize(2000, 2000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(520, 520)
widget_central = QtWidgets.QWidget()
self.setCentralWidget(widget_central)
lay = QtWidgets.QVBoxLayout(widget_central)
scene = QtWidgets.QGraphicsScene(self)
view = QtWidgets.QGraphicsView(scene)
diedrico = Diedrico()
scene.addWidget(diedrico)
lay.addWidget(view)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())

Zoom in and out in widget

How can I make a zoom effect with key input on a widget? The widget is inside a scroll area and there are some drawings made with QPainter who change with user input. The zoom would affect the length of the scrolling bar, the closer you are, the smaller the bar becomes. The zoom at minimum level should make the scroll bar as big as the widget area, so all the content in the widget could be visualized.
MRE:
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPen, QColor
import sys
class Diedrico(QWidget):
def __init__(self, parent):
super().__init__(parent)
def paintEvent(self, event):
qp = QPainter(self)
qp.setPen(QPen(QColor(Qt.black), 5))
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self.resize(520, 520)
self.widget_central = QtWidgets.QWidget(self)
scrol = QtWidgets.QScrollArea(self.widget_central)
scrol.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
scrol.setGeometry(QtCore.QRect(10, 10, 500, 500))
scrol.setWidgetResizable(False)
contenido = QtWidgets.QWidget()
contenido.setGeometry(QtCore.QRect(0, 0, 2000, 2000))
scrol.setWidget(contenido)
self.Diedrico = Diedrico(contenido)
self.Diedrico.setGeometry(QtCore.QRect(0, 0, 2000, 2000))
self.setCentralWidget(self.widget_central)
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())
Why do you want to reinvent the wheel? Instead of wanting to implement the logic of the scaling feature, use the classes that already do it. In this case, a good option is to use QGraphicsView with QGraphicsScene:
Note: The shortcut standard Zoom In and Zoom Out are associated with Ctrl + + and Ctrl + -, respectively.
from PyQt5 import QtCore, QtGui, QtWidgets
class Diedrico(QtWidgets.QWidget):
def paintEvent(self, event):
qp = QtGui.QPainter(self)
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.black), 5)
qp.setPen(pen)
qp.drawRect(500, 500, 1000, 1000)
class UiVentana(QtWidgets.QMainWindow):
factor = 1.5
def __init__(self, parent=None):
super(UiVentana, self).__init__(parent)
self._scene = QtWidgets.QGraphicsScene(self)
self._view = QtWidgets.QGraphicsView(self._scene)
self._diedrico = Diedrico()
self._diedrico.setFixedSize(2000, 2000)
self._scene.addWidget(self._diedrico)
self.setCentralWidget(self._view)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn),
self._view,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_in,
)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtGui.QKeySequence.ZoomOut),
self._view,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_out,
)
#QtCore.pyqtSlot()
def zoom_in(self):
scale_tr = QtGui.QTransform()
scale_tr.scale(UiVentana.factor, UiVentana.factor)
tr = self._view.transform() * scale_tr
self._view.setTransform(tr)
#QtCore.pyqtSlot()
def zoom_out(self):
scale_tr = QtGui.QTransform()
scale_tr.scale(UiVentana.factor, UiVentana.factor)
scale_inverted, invertible = scale_tr.inverted()
if invertible:
tr = self._view.transform() * scale_inverted
self._view.setTransform(tr)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
ui = UiVentana()
ui.show()
sys.exit(app.exec_())
Update:
If you want to use + and - for ZoomIn and ZoomOut, respectively, then just change the shortcuts to:
QtWidgets.QShortcut(
QtGui.QKeySequence(QtCore.Qt.Key_Plus), # <---
self._view,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_in,
)
QtWidgets.QShortcut(
QtGui.QKeySequence(QtCore.Qt.Key_Minus), # <---
self._view,
context=QtCore.Qt.WidgetShortcut,
activated=self.zoom_out,
)

Adding custom widgets to a QScrollArea in pyqt5 does not make scrollbar appear

The example shown below adds a very simple custom pyqt5 widget to a QScrollArea. When i move the custom widget "off" the area of the scroll area I would expect that scrollbars should appear. Obviously this is not the case.
#!/usr/bin/python3
from PyQt5.QtWidgets import QMainWindow, QWidget, QApplication, QScrollArea
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QPainter, QColor, QPen
import sys
class ExampleWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent=parent)
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawWidget(qp)
qp.end()
def drawWidget(self, qp):
qp.setPen(QColor(255, 0, 0))
qp.setBrush(QColor(255, 0, 0))
qp.drawRect(0, 0, 100, 100)
class Example(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(300, 300, 300, 200)
self.scrollArea = QScrollArea(self)
self.setCentralWidget(self.scrollArea)
self.widget = ExampleWidget(self.scrollArea)
self.widget.setGeometry(250, 150, 100, 100)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
How can I make the scrollbars appear once a widget is moved so it's not visible completely anymore?
In the code that samples ExampleWidget is a child of QScrollArea: self.widget = ExampleWidget(self.scrollArea), that does not imply that the widget is handled with the properties of the QScrollArea, the QScrollArea has a viewport() that is the background widget and the QScrollArea only shows a part of it. So if you want a widget to be inside you have to use the setWidget() method. On the other hand that viewport() has the size of the content and how it calculates that size ?, by default it uses the sizeHint() of the widget, and in your case it does not have it so it will not show what you want.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class ExampleWidget(QtWidgets.QWidget):
def paintEvent(self, e):
qp = QtGui.QPainter(self)
self.drawWidget(qp)
def drawWidget(self, qp):
qp.setPen(QtGui.QColor(255, 0, 0))
qp.setBrush(QtGui.QColor(255, 0, 0))
qp.drawRect(0, 0, 100, 100)
def sizeHint(self):
return QtCore.QSize(500, 500)
class Example(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.scrollArea = QtWidgets.QScrollArea()
self.setCentralWidget(self.scrollArea)
self.widget = ExampleWidget()
self.scrollArea.setWidget(self.widget)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())

How to QPainter inside QLabel

I can not understand how to make the QPainter() draw inside a QLabel, here is the code I told would have worked:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QPainter, QColor, QBrush
class Labella(QLabel):
def __init__(self, parent):
super().__init__()
lb = QLabel('text', parent)
lb.setStyleSheet('QFrame {background-color:grey;}')
lb.resize(200, 200)
qp = QPainter(lb)
qp.begin(lb);
qp.setBrush(QColor(200, 0, 0))
qp.drawRect(0,0,20,20);
qp.end();
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawRectangles(qp)
qp.end()
def drawRectangles(self, qp):
col = QColor(0, 0, 0)
col.setNamedColor('#040404')
qp.setPen(col)
qp.setBrush(QColor(200, 0, 0))
qp.drawRect(10, 15, 200, 60)
class Example(QWidget):
def __init__(self):
super().__init__()
lb = Labella(self)
self.setGeometry(300, 300, 350, 300)
self.setWindowTitle('Colours')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
I can only find examples in C++ same as for the Qt documentation, please instruct me where I should have find the information if not here.
The documentation suggests to use QPainter inside the paintEvent.
By using the constructor like below, inside the method paintEvent, no need to call begin() and end()
(Your class Labella just miss a parameter to initialize the parent)
The method save() and restore() can be convenient to store a standard config of a QPainter, allowing to draw something different before restoring the settings.
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import QPainter, QColor, QBrush
class Labella(QLabel):
def __init__(self, parent):
super().__init__(parent=parent)
self.setStyleSheet('QFrame {background-color:grey;}')
self.resize(200, 200)
def paintEvent(self, e):
qp = QPainter(self)
self.drawRectangles(qp)
qp.setBrush(QColor(200, 0, 0))
qp.drawRect(0,0,20,20)
def drawRectangles(self, qp):
qp.setBrush(QColor(255, 0, 0, 100))
qp.save() # save the QPainter config
qp.drawRect(10, 15, 20, 20)
qp.setBrush(QColor(0, 0, 255, 100))
qp.drawRect(50, 15, 20, 20)
qp.restore() # restore the QPainter config
qp.drawRect(100, 15, 20, 20)
class Example(QWidget):
def __init__(self):
super().__init__()
lb = Labella(self)
self.setGeometry(300, 300, 350, 300)
self.setWindowTitle('Colours')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())

Categories