How to get movable graphical items inside a QGraphicsItemGroup? - python

My goal is to write a software that displays two movable disks that live inside the same QGraphicsItemGroup. I'd like to use QGraphicsItemGroup because in this way each disk can access the position of the other. To make the objects movable, I use the flag ItemIsMovable which unfortunately doesn't seem to work inside a QGraphicsItemGroup The following program exemplifies my issue:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsItem,\
QApplication, QGraphicsItemGroup, QGraphicsEllipseItem
class MyDisk(QGraphicsEllipseItem):
def __init__(self, top_left_x, top_left_y, radius, color):
super().__init__(top_left_x, top_left_y, radius, radius)
self.setBrush(color)
self.setFlag(QGraphicsItem.ItemIsMovable)
class MyGroup(QGraphicsItemGroup):
def __init__(self):
super().__init__()
self.disk1 = MyDisk(50, 50, 20, Qt.red)
self.disk2 = MyDisk(150, 150, 20, Qt.red)
self.addToGroup(self.disk1)
self.addToGroup(self.disk2)
# self.setFlag(QGraphicsItemGroup.ItemIsMovable)
class MyView(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
self.setWindowTitle('Red disks are not movable')
self.setSceneRect(0, 0, 250, 250)
self.group = MyGroup()
self.scene.addItem(self.group)
self.scene.addItem(MyDisk(150, 50, 20, Qt.green))
if __name__ == '__main__':
app = QApplication([])
f = MyView()
f.show()
sys.exit(app.exec_())
My problem is that the green disk (not in the group) is movable but the two red disks (in the group) are not. How can I make the two red disks movable? Note that setting the flag ItemIsMovable inside MyGroup doesn't solve the problem because the red disks would then move together (you can try this by uncomment the comment in the code).

Related

How to know the exact position of a camera viewbox in Qt?

I am working with OpenGL in python and trying to attach 2d images to a canvas (the images will change according to a certain frequence).
I managed to achieve that but to continue my task i need two things:
the major problem: I need to get the image position (or bounds), sorry if i don't have the correct term, i am new to this. basically i just need to have some kind of positions to know where my picture is in the canvas. i tried to look into the methods and attributes of self.view.camera I could not find anything to help.
one minor problem: i can move the image with the mouse along the canvas and i zoom it. i wonder if it is possible to only allow the zoom but not allow the right/left move [this is resolved in the comments section]
here is my code:
import sys
from PySide2 import QtWidgets, QtCore
from vispy import scene
from PySide2.QtCore import QMetaObject
from PySide2.QtWidgets import *
import numpy as np
import dog
import time
import imageio as iio
class CameraThread(QtCore.QThread):
new_image = QtCore.Signal(object)
def __init__(self, parent=None):
QtCore.QThread.__init__(self, parent)
def run(self):
try:
while True:
frame = iio.imread(dog.getDog(filename='randog'))
self.new_image.emit(frame.data)
time.sleep(10.0)
finally:
print('end!')
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
if not MainWindow.objectName():
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 400)
self.centralwidget = QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QGridLayout(self.centralwidget)
self.gridLayout.setObjectName("gridLayout")
self.groupBox = QGroupBox(self.centralwidget)
self.groupBox.setObjectName("groupBox")
self.gridLayout.addWidget(self.groupBox, 0, 0, 1, 1)
MainWindow.setCentralWidget(self.centralwidget)
QMetaObject.connectSlotsByName(MainWindow)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# OpenGL drawing surface
self.canvas = scene.SceneCanvas(keys='interactive')
self.canvas.create_native()
self.canvas.native.setParent(self)
self.setWindowTitle('MyApp')
self.view = self.canvas.central_widget.add_view()
self.view.bgcolor = '#ffffff' # set the canvas to a white background
self.image = scene.visuals.Image(np.zeros((1, 1)),
interpolation='nearest',
parent= self.view.scene,
cmap='grays',
clim=(0, 2 ** 8 - 1))
self.view.camera = scene.PanZoomCamera(aspect=1)
self.view.camera.flip = (0, 1, 0)
self.view.camera.set_range()
self.view.camera.zoom(1000, (0, 0))
self._camera_runner = CameraThread(parent=self)
self._camera_runner.new_image.connect(self.new_image, type=QtCore.Qt.BlockingQueuedConnection)
self._camera_runner.start()
#QtCore.Slot(object)
def new_image(self, img):
try:
self.image.set_data(img)
self.image.update()
except Exception as e:
print(f"problem sending image: {e}")
def main():
import ctypes
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID('my_gui')
app = QtWidgets.QApplication([])
main_window = MainWindow()
main_window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Do you want to know the coordinates of the picture in the viewport (the window), or do you want the coordinates of the picture on the canvas? Vispy actually puts the image at (0,0) by default inside the Vispy canvas. When you move around the canvas you actually aren't moving the canvas around, you are just moving the camera which is looking at the canvas so the coordinates of the picture stay at (0,0) regardless if you move around the viewport or the camera or not. Also the coordinates of the Vispy canvas correspond one to one with the pixel length and width of your image. One pixel is one unit in Vispy. You can check this by adding this method to your MainWindow class:
def my_handler(self,event):
transform = self.image.transforms.get_transform(map_to="canvas")
img_x, img_y = transform.imap(event.pos)[:2]
print(img_x, img_y)
# optionally do the below to tell other handlers not to look at this event:
event.handled = True
and adding this to your __init__ method:
self.canvas.events.mouse_move.connect(self.my_handler)
You can see that when you hover over the top left corner of your image, it should print roughly (0,0).
def my_handler(self,event):
transform = self.image.transforms.get_transform(map_to="canvas")
img_x, img_y = transform.imap(event.pos)[:2]
print(img_x, img_y)
# optionally do the below to tell other handlers not to look at this event:
event.handled = True

Mechanics behind the synchronisation of QPainters

Context
I have a custom Widget which is supposed to make an animation of dots moving in order to make a kind of loading widget. To acheive that goal, I started using QPainter and QVariantAnimation objects, which seemed like a decent tools to do the job. The problem is that I think that the QPainters I initialise when drawing come in conflict with each other.
Technique
To acheive that, I initialize multiple QVariantAnimation, which signal .valueChanged() I connect to a function update(), which is supposed to launch the painEvent(), such as written in the docs
A paint event is a request to repaint all or part of a widget. It can happen for one of the following reasons:repaint() or update() was invoked,
the widget was obscured and has now been uncovered, or
many other reasons.
Since I start different animation at different times, I suppose that the update() is called many times, thus interfering with another QPainter already working. But, as I read in the docs,
When update() is called several times or the window system sends several paint events, Qt merges these events into one event with a larger region.
But it specifies nothing id the QPainter has the same region, which is why I suposse it crashes. It logs messages such as:
QBackingStore::endPaint() called with active painter on backingstore paint device
Minimal Working Example
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QDialog, QPushButton
from PyQt5.QtCore import Qt, pyqtSlot, QVariantAnimation, QVariant, QTimer
from PyQt5.QtGui import QColor, QPainter, QBrush
import time
class Dialog(QDialog):
def __init__(self, *args, **kwargs):
QDialog.__init__(self, *args, **kwargs)
self.resize(500, 500)
self.setLayout(QVBoxLayout())
self.button = QPushButton()
self.layout().addWidget(self.button)
self.paintWidget = PaintWidget()
self.layout().addWidget(self.paintWidget)
self.button.clicked.connect(self.paintWidget.startPainting)
self.button.clicked.connect(self.reverse)
def reverse(self):
if self.paintWidget.isMoving:
self.paintWidget.stopPainting()
class PaintWidget(QWidget):
def __init__(self):
super(PaintWidget, self).__init__()
self.dotRadius = 10
self.dotColor = QColor(255, 100, 100)
self.numberOfDots = 3
self.isMoving = False
self.animation = []
self.createAnimation()
self.dotPosition = [[0, 0], [0, 0], [0, 0]]
def startPainting(self):
for i in range(self.numberOfDots):
self.animation[i].start()
time.sleep(200)
self.isActive = True
def createAnimation(self):
for i in range(self.numberOfDots):
self.animation.append(QVariantAnimation(self, startValue=0, endValue=500, duration=3000))
self.animation[i].valueChanged.connect(self.updatePosition)
#pyqtSlot(QVariant)
def updatePosition(self, position):
self.dotPosition = [position, 0]
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.fillRect(self.rect(), Qt.transparent)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setPen(Qt.NoPen)
for i in range(self.numberOfDots):
painter.save()
painter.translate(0, 0)
position = (self.dotPosition[i][0], self.dotPosition[i][1])
color = self.dotColor
painter.setBrush(QBrush(color, Qt.SolidPattern))
painter.drawEllipse(position[0], position[1], self.dotRadius, self.dotRadius)
painter.restore()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
dial = Dialog()
dial.show()
sys.exit(app.exec_())
Results
I know that for now this code wouldn't work because I can't retreive which dot's animation got updated, but I believe the major problem here is the interference between the painters. Thus, could anyone tell me why this is hapenning and point me in a potential solution? Also, For knowing the dot that got updated and chose the good position, I'm really unsure of how to do this as well.
Your code has the following errors:
Never use time.sleep() in the main GUI thread since it blocks the event loop generating the freezing of the application.
the variable dotPosition that must store all the positions you are replacing it with only one position in the updatePosition method.
You should use QPoint if you are going to store a position instead of a list, use a list is not bad but using QPoint makes your code more readable.
Do not use painter.save() and painter.restore() unnecessarily, neither painter.translate().
Considering the above, the solution is as follows:
from functools import partial
from PyQt5 import QtCore, QtGui, QtWidgets
class Dialog(QtWidgets.QDialog):
def __init__(self, *args, **kwargs):
super(Dialog, self).__init__(*args, **kwargs)
self.resize(500, 500)
self.button = QtWidgets.QPushButton()
self.paintWidget = PaintWidget()
self.button.clicked.connect(self.paintWidget.startPainting)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.button)
lay.addWidget(self.paintWidget)
class PaintWidget(QtWidgets.QWidget):
def __init__(self):
super(PaintWidget, self).__init__()
self.dotRadius = 10
self.dotColor = QtGui.QColor(255, 100, 100)
self.animations = []
self.dotPosition = [
QtCore.QPoint(0, 0),
QtCore.QPoint(0, 0),
QtCore.QPoint(0, 0),
]
self.createAnimation()
def startPainting(self):
for i, animation in enumerate(self.animations):
QtCore.QTimer.singleShot(i * 200, animation.start)
def createAnimation(self):
for i, _ in enumerate(self.dotPosition):
wrapper = partial(self.updatePosition, i)
animation = QtCore.QVariantAnimation(
self,
startValue=0,
endValue=500,
duration=3000,
valueChanged=wrapper,
)
self.animations.append(animation)
#QtCore.pyqtSlot(int, QtCore.QVariant)
def updatePosition(self, i, position):
self.dotPosition[i] = QtCore.QPoint(position, 0)
self.update()
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), QtCore.Qt.transparent)
painter.setRenderHint(QtGui.QPainter.Antialiasing, True)
painter.setPen(QtCore.Qt.NoPen)
painter.setBrush(
QtGui.QBrush(self.dotColor, QtCore.Qt.SolidPattern)
)
for position in self.dotPosition:
painter.drawEllipse(position, self.dotRadius, self.dotRadius)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
dial = Dialog()
dial.show()
sys.exit(app.exec_())

Possible rendering issue with QScrollArea and QPainter

I'm trying to create a character map visualization tool with PyQt5. With fontTools library, I'm extracting the UNICODE code points supported in a given ttf file. Then using QPainter.drawText I'm drawing the glyphs on labels. The labels are stored in a QGridLayout and the layout is in a QScrollArea
Everything works fine, except when I try to scroll. The drawn images are overlapped whenever I try to scroll too fast. It looks like this.
The labels are redrawn properly the moment the window loses focus.
Here's an MWE of what I've so far.
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFontDatabase, QFont, QColor, QPainter
from fontTools.ttLib import TTFont
class App(QWidget):
def __init__(self):
super().__init__()
self.fileName = "mukti.ttf" #the ttf file is located in the same path as the script
self.initUI()
def initUI(self):
self.setWindowTitle("Glyph Viewer")
self.setFixedSize(640, 480)
self.move(100, 100)
vBox = QtWidgets.QVBoxLayout()
self.glyphView = GlyphView()
vBox.addWidget(self.glyphView)
self.setLayout(vBox)
self.showGlyphs()
self.show()
def showGlyphs(self):
#Using the fontTools libray, the unicode blocks are obtained from the ttf file
font = TTFont(self.fileName)
charMaps = list()
for cmap in font['cmap'].tables:
charMaps.append(cmap.cmap)
charMap = charMaps[0]
fontDB = QFontDatabase()
fontID = fontDB.addApplicationFont("mukti.ttf")
fonts = fontDB.applicationFontFamilies(fontID)
qFont = QFont(fonts[0])
qFont.setPointSize(28)
self.glyphView.populateGrid(charMap, qFont)
class GlyphView(QtWidgets.QScrollArea):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setWidgetResizable(True)
def populateGrid(self, charMap, qFont):
glyphArea = QtWidgets.QWidget(self)
gridLayout = QtWidgets.QGridLayout()
glyphArea.setLayout(gridLayout)
row, col = 1, 1
for char in charMap:
uni = charMap[char]
gridLayout.addWidget(Glyph(qFont, chr(char)), row, col)
if not col % 4:
col = 1
row += 1
else:
col += 1
self.setWidget(glyphArea)
class Glyph(QtWidgets.QLabel):
def __init__(self, font, char):
super().__init__()
self.font = font
self.char = char
self.initUI()
def initUI(self):
self.setFixedSize(48, 48)
self.setToolTip(self.char)
def paintEvent(self, event):
qp = QPainter(self)
qp.setBrush(QColor(0,0,0))
qp.drawRect(0, 0, 48, 48)
qp.setFont(self.font)
qp.setPen(QColor(255, 255, 255))
qp.drawText(event.rect(), Qt.AlignCenter, self.char)
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
I'm not sure what is causing this. Any help is appreciated!
The paintEvent() method gives us an object that belongs to QPaintEvent, that object provides a QRect through the rect() method, that QRect is the area that is currently visible, and that information could be used to optimize the painting, for example let's say we have a widget that shows texts in several lines, if the text is large few lines will look so painting all is a waste of resources, so with a proper calculation using the aforementioned QRect we could get those lines and paint that part spending few resources. In this answer I show an example of the use of event.rect().
In your case there is no need to use event.rect() since you would be painting the text in a part of the widget widget. what you should use is self.rect():
def paintEvent(self, event):
qp = QPainter(self)
qp.setBrush(QColor(0,0,0))
qp.drawRect(0, 0, 48, 48)
qp.setFont(self.font())
qp.setPen(QColor(255, 255, 255))
# change event.rect() to self.rect()
qp.drawText(self.rect(), Qt.AlignCenter, self.text())
I also see unnecessary to overwrite paintEvent() method since you can point directly to the QLabel the font, the text and the alignment:
class Glyph(QtWidgets.QLabel):
def __init__(self, font, char):
super().__init__(font=font, text=char, alignment=Qt.AlignCenter, toolTip=char)

Moving a QGraphicsItem around a central point in PyQt4

I'm using Python 2.7 and PyQt4. I am trying to have a half-circle object that is a QGraphicsItem. I want to be able to move it using the mouse, by clicking and dragging. I can create the object and move it around with the mouse by setting the flag ItemIsMovable. Now the half-circle moves around freely but I want it to move just around the fixed central point. It is difficult to describe, but it should be something similar to a dial. How can I accomplish this?
you can use QGraphicsItem::mouseMoveEvent event to track item's movements within the scene and correct its position once it's moved off the restricted area. Pls, check if an example below would work for you:
import sys
from PyQt4 import QtGui, QtCore
class TestEclipseItem(QtGui.QGraphicsEllipseItem):
def __init__(self, parent=None):
QtGui.QGraphicsPixmapItem.__init__(self, parent)
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, True)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
# set move restriction rect for the item
self.move_restrict_rect = QtCore.QRectF(20, 20, 200, 200)
# set item's rectangle
self.setRect(QtCore.QRectF(50, 50, 50, 50))
def mouseMoveEvent(self, event):
# check of mouse moved within the restricted area for the item
if self.move_restrict_rect.contains(event.scenePos()):
QtGui.QGraphicsEllipseItem.mouseMoveEvent(self, event)
class MainForm(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainForm, self).__init__(parent)
scene = QtGui.QGraphicsScene(-50, -50, 600, 600)
ellipseItem = TestEclipseItem()
scene.addItem(ellipseItem)
view = QtGui.QGraphicsView()
view.setScene(scene)
view.setGeometry(QtCore.QRect(0, 0, 400, 200))
self.setCentralWidget(view)
def main():
app = QtGui.QApplication(sys.argv)
form = MainForm()
form.show()
app.exec_()
if __name__ == '__main__':
main()
hope this helps, regards

Returning handles of elements using QPainter

I am developing a GUI based on PyQt5, and I am wondering if I can get the handles of individual elements that have been created using the QPainter class.
I mean, supose that we have three rectangles that have been painted using that class with the following code:
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPainter, QColor, QBrush
import sys
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 350, 100)
self.setWindowTitle('Colours')
self.show()
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('#d4d4d4')
qp.setPen(col)
qp.setBrush(QColor(200, 0, 0))
qp.drawRect(10, 15, 90, 60)
qp.setBrush(QColor(255, 80, 0, 160))
qp.drawRect(130, 15, 90, 60)
qp.setBrush(QColor(25, 0, 90, 200))
qp.drawRect(250, 15, 90, 60)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
I want to retrieve a kind of handle of each rectangle, for instance, handle_1, handle_2 and handle_3, in order to modify the properties of one of them in another thread without plotting the rest of them.
handle_3.setColor(...)
If this is not possible, I was wondering if I could create a kind of transparent containter with Qt (which effectively has a handle to modify the stylesheet) and put a QLabel inside it. If so, what container would be the best choice?
As #ekumoro says, the paint-event does not create new elements, so updating one of the rectangles without re-painting the rest of them is impossible.
The only way to achieve this is using containers. Attending to the second question, I found that the best one is the QFrame.

Categories

Resources