PySide2 QGraphicsView zoom widget in scene macOS - python

Apparently, a widget inside a QGraphicsView does not scale correctly on macOS. Does anyone have a workaround?
Problem:
I have a QGraphicsView where I am zooming by calling the scale method.
I also have some QWidgets in my QGraphicScene, but their UI graphics do not scale accordingly when I zoom.
I use a QGraphicsProxyWidget from within a QGraphicsItem to display the widgets, but the scale issue persists even by using the scene.addWidget.
I tested the code on macOS 10.15.7 and 10.14.6, both Intel CPUs.
Edit: As pointed out in the comments by musicamante, the QGraphicsItem.ItemIgnoresTransformations does the exact opposite, so I am taking the reference out of the question to avoid confusion since it was only a failed attempt.
Edit2: On Windows 10 seems to work fine.
import sys
from PySide2.QtCore import Qt
from PySide2.QtGui import QPainterPath, QColor, QBrush
from PySide2.QtWidgets import (
QGraphicsItem,
QPushButton,
QGraphicsScene,
QWidget,
QVBoxLayout,
QMainWindow,
QApplication,
QGraphicsView,
QGraphicsProxyWidget
)
class NodeGraphicsContent(QGraphicsProxyWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setWidget(QPushButton('QGraphicsItem Button'))
class GraphicsItem(QGraphicsItem):
def __init__(self):
super().__init__()
self.item_body = self.draw_item()
self.draw_content()
def draw_content(self):
NodeGraphicsContent(self)
def draw_item(self):
path = QPainterPath()
path.addRect(0, 0, 200, 200)
return path
def paint(self, painter, option, widget):
painter.setPen(Qt.NoPen)
painter.setBrush(QBrush(QColor("#FF313131")))
painter.drawPath(self.item_body)
def boundingRect(self):
return self.item_body.boundingRect()
class GraphicScene(QGraphicsScene):
def __init__(self, parent=None):
super().__init__(parent)
self.scene_width = 500
self.scene_height = 500
self._grid_pattern = QBrush(QColor("#282828"), Qt.Dense7Pattern)
self.setBackgroundBrush(QColor("#393939"))
self.setSceneRect(-self.scene_width // 2, -self.scene_height // 2,
self.scene_width, self.scene_height)
def drawBackground(self, painter, rect):
super().drawBackground(painter, rect)
painter.fillRect(rect, self._grid_pattern)
class GraphicsView(QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
def wheelEvent(self, event):
"""Zoom function."""
zoom_factor = 1.25
zoom_out = 1 / zoom_factor
zoom_factor = zoom_factor if event.delta() > 0 else zoom_out
self.scale(zoom_factor, zoom_factor)
class MainWidgets(QWidget):
def __init__(self):
super().__init__()
self.scene = GraphicScene()
self.view = GraphicsView(self.scene)
_layout = QVBoxLayout()
_layout.addWidget(self.view)
self.setLayout(_layout)
self.add_widgets()
def add_widgets(self):
"""Add first a normal widget, then a QGraphicsItem,
which both dont seem to scale correctly.
"""
button = self.scene.addWidget(QPushButton('Widget Button'))
button.setPos(-50, -100)
self.scene.addItem(GraphicsItem())
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setCentralWidget(MainWidgets())
self.statusBar().showMessage('Zoom with mouse wheel')
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

Related

How can I add new ellipse while retaining the previous ellipse?

Currently, my code can paint an ellipse but every time I click the mouse for a new ellipse the previous ellipse will not retain .
I am used the **QGraphicsView** Framework because I am using its pantool and zoom function but I have a hard time adding the paint function.
import sys
from PyQt5.QtCore import Qt, QRectF, QPointF
from PyQt5.QtGui import QPixmap, QTransform, QBrush, QColor, QPen, QPainterPath
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QGraphicsView,
QGraphicsScene, QGraphicsPixmapItem, QSizePolicy, QSpacerItem, QGraphicsObject
class MouseBrushObject(QGraphicsObject):
def __init__(self):
QGraphicsObject.__init__(self)
self._size = 10
self._x = 0
self._y = 0
self._pen = None
self._brush = None
self._color = None
self.setColor(QColor(255, 0, 0, 255))
def paint(self, painter, option, widget):
rect = self.boundingRect()
painter.setPen(self._pen)
painter.setBrush(self._brush)
painter.drawEllipse(QPointF(self._x,self._y), 5,5)
def boundingRect(self):
return QRectF(self._x, self._y, self._size, self._size)
def setColor(self, color):
self._color = color
self._pen = QPen(self._color, 1)
self._brush = QBrush(QColor(self._color.red(), self._color.green(), self._color.blue(), 40))
def setSize(self, size):
self._size = size
def setPosition(self, pos):
self._x = pos.x() - pos.x()/2
self._y = pos.y() - pos.y()/2
self.setPos(QPointF(self._x, self._y))
class View(QGraphicsView):
def __init__(self, parent=None):
QGraphicsView.__init__(self, parent=parent)
self.setMouseTracking(True)
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
pixmap = QPixmap(300, 300)
self.scene.addItem(QGraphicsPixmapItem(pixmap))
#self.setTransform(QTransform().scale(1, 1).rotate(0))
self.scene.setBackgroundBrush(QBrush(Qt.lightGray))
self._brushItem = MouseBrushObject()
self.path = QPainterPath()
def mousePressEvent(self, event):
pos = event.pos()
self._brushItem.setPosition(pos)
def enterEvent(self, event):
self.scene.addItem(self._brushItem)
return super(View, self).enterEvent(event)
def leaveEvent(self, event):
self.scene.removeItem(self._brushItem)
return super(View, self).leaveEvent(event)
class Viewer(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent=parent)
layout = QVBoxLayout()
self.view = View(self)
self.setLayout(layout)
layout.addWidget(self.view)
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.viewer = Viewer(self)
layout = QVBoxLayout()
layout.addWidget(self.viewer)
centralwidget = QWidget(self)
centralwidget.setLayout(layout)
self.setCentralWidget(centralwidget)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainWindow()
main.show()
sys.exit(app.exec_())
Every time the mousePressEvent is trigger, it will paint an ellipse but the problem is that it will not retain the previous ellipse will adding the new ellipse.
My goal is for this code is two paint an ellipse when the mouse is clicked as it will serve as a point. Then The two points will be automatically connect using path. I have tried it will the qpainter function and it works but since I can't used qpaintEvent in QgraphicsView, I have a hard time executing the code.

pyqt "ItemIgnoresParentOpacity" Flag being ignored

I've taken the time to do a Minimal Reproducible Example of my issue.
Using PyQt5, I have a classic Main window, with a QGraphicsView linked to a QGraphicsScene.
On the scene, I have a NodeHover item (green circle) with a TreeNode child (square).
The NodeHover item is transparent by default, and becomes opaque when hovered.
I want its child item (TreeNode) to remain opaque all the time, no matter the parent's opacity. I've tried setting flags in both items, as you can see below, but they seem not to work.
import sys
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QMainWindow, QApplication, QToolBar, QStatusBar, QAction, QGraphicsItem, QGraphicsLineItem, QLabel, QGraphicsEllipseItem, QGraphicsTextItem
from PyQt5.QtCore import *
from PyQt5.QtGui import QPainter, QIcon, QPen, QBrush, QTransform
class NodeHover(QGraphicsItem):
def __init__(self, *args, **kwargs):
super(NodeHover, self).__init__(*args, **kwargs)
self.setOpacity(0.1)
self.setAcceptHoverEvents(True)
# Setting the flag so Item doesn't propagate its opacity to children
self.GraphicsItemFlag(QGraphicsItem.ItemDoesntPropagateOpacityToChildren)
def hoverEnterEvent(self, event):
self.setOpacity(1) # When mouse enters item, make it opaque
def hoverLeaveEvent(self, event):
self.setOpacity(0.1) # When mouse leaves item, make it transparent
def boundingRect(self):
return QRectF(0,0, 20, 20)
def paint(self, painter, option, widget):
painter.setBrush(QBrush(Qt.green))
painter.drawEllipse(QPointF(10, 10), 10, 10)
class TreeNode(QGraphicsItem):
def __init__(self, *args, **kwargs):
super(TreeNode, self).__init__(*args, **kwargs)
# Setting the flag to ignore parent's opacity
self.GraphicsItemFlag(QGraphicsItem.ItemIgnoresParentOpacity)
def boundingRect(self):
return QRectF(100, 100, 100, 100)
def paint(self, painter, option, widget):
painter.drawRect(110,110,80,80)
class GView(QGraphicsView):
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.parent = parent
self.setGeometry(100, 100, 700, 450)
self.show()
class Scene(QGraphicsScene):
def __init__(self, parent):
super().__init__(parent)
self.parent = parent
hoverItem = NodeHover() # create a NodeHover item
self.addItem(hoverItem)
nodeItem = TreeNode() # create a TreeNode item and make it hoverItem's child
nodeItem.setParentItem(hoverItem)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setGeometry(900, 70, 1000, 800)
self.createGraphicView()
self.show()
def createGraphicView(self):
self.scene = Scene(self)
gView = GView(self)
scene = Scene(gView)
gView.setScene(scene)
# Set the main window's central widget
self.setCentralWidget(gView)
# Run program
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
To set a flag to the QGraphicsItem you must use the setFlag() method:
self.setFlag(QGraphicsItem.ItemDoesntPropagateOpacityToChildren)
self.setFlag(QGraphicsItem.ItemIgnoresParentOpacity)
Note: One of those flags is enough.

pyqt5 drawing lines with a button on a QGraphicsScene

I've a problem with pyqt5. I've create a windows with a scene that has a background image, re-implementing drawBackground. I've also a button that allow me to add a line in a position on the scene. The problem is that if i click the button to draw the line, then this line is drawn in a separate scene with it's own background, instead of into the scene i have. Seems like it create a new scene to draw the line on. Here is my code:
import sys
from PyQt5 import QtGui
from PyQt5.QtGui import QImage
from PyQt5.QtWidgets import (QMainWindow, QGraphicsView, QPushButton,
QHBoxLayout, QVBoxLayout, QWidget, QApplication, QGraphicsScene)
class GraphicsScene(QGraphicsScene):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._image = QImage()
#property
def image(self):
return self._image
#image.setter
def image(self, img):
self._image = img
self.update()
def drawBackground(self, painter, rect):
if self.image.isNull():
super().drawBackground(painter, rect)
else:
painter.drawImage(rect, self._image)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.title = "parcelDeliveryIta";
self.top = 100
self.left = 100
self.width = 1500
self.height = 900
self.initUI()
def initUI(self):
self.scene = GraphicsScene(self)
self.scene._image = QImage('Italy.png')
view = QGraphicsView(self.scene, self)
self.scene.setSceneRect(0, 0, view.width(), view.height())
addLine = QPushButton('AddLine')
addLine.clicked.connect(self.addLine)
hbox = QHBoxLayout(self)
hbox.addWidget(view)
vbox = QVBoxLayout(self)
vbox.addWidget(addLine)
hbox.addLayout(vbox)
self.setWindowTitle(self.title)
self.setGeometry(self.top, self.left, self.width, self.height)
self.setFixedSize(self.width, self.height)
self.setLayout(hbox)
self.show()
def addLine(self):
self.scene.addLine(0, 0, 100, 100)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())
and this is the result when clicking the button:
As it is possible to see, the line is drawn with its own backgroung, that is image I've setted as background to the scene (the image above is cropped to show better the line)
Thanks for helping.
Explanation:
It seems that my previous solution is not suitable for your case since as the docs points out:
void QGraphicsScene::drawBackground(QPainter *painter, const
QRectF &rect) Draws the background of the scene using painter,
before any items and the foreground are drawn. Reimplement this
function to provide a custom background for the scene.
All painting is done in scene coordinates. The rect parameter is the
exposed rectangle.
If all you want is to define a color, texture, or gradient for the
background, you can call setBackgroundBrush() instead.
See also drawForeground() and drawItems().
(emphasis mine)
That paint will also be used to paint the base of the items and therefore caused that behavior.
Solution:
So you will have to resort to another solution, for example to use a QGraphicsPixmapItem as a base and readjust the size of the window with that information:
import sys
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import (
QGraphicsView,
QPushButton,
QHBoxLayout,
QVBoxLayout,
QWidget,
QApplication,
QGraphicsScene,
)
class GraphicsView(QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
scene = QGraphicsScene(self)
self.setScene(scene)
self._pixmap_item = self.scene().addPixmap(QPixmap())
self._pixmap_item.setZValue(-1)
#property
def pixmap(self):
return self._pixmap_item.pixmap()
#pixmap.setter
def pixmap(self, pixmap):
self._pixmap_item.setPixmap(pixmap)
self.scene().setSceneRect(self._pixmap_item.boundingRect())
def resizeEvent(self, event):
if not self._pixmap_item.pixmap().isNull():
self.fitInView(self._pixmap_item)
super().resizeEvent(event)
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.title = "parcelDeliveryIta"
self.top = 100
self.left = 100
self.width = 1500
self.height = 900
self.initUI()
def initUI(self):
self.view = GraphicsView(self)
self.view.pixmap = QPixmap("Italy.png")
addLine = QPushButton("AddLine")
addLine.clicked.connect(self.addLine)
hbox = QHBoxLayout(self)
hbox.addWidget(self.view)
vbox = QVBoxLayout()
vbox.addWidget(addLine)
hbox.addLayout(vbox)
self.setWindowTitle(self.title)
self.setGeometry(self.top, self.left, self.width, self.height)
self.setFixedSize(self.width, self.height)
self.show()
def addLine(self):
self.view.scene().addLine(0, 0, 100, 100)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
sys.exit(app.exec_())

How to add a menubar(QMainWindow) alongside QGraphicsView in PyQt?

A picture is displayed with scrollbars on a window. We can draw on it. I want to add a menubar to the same window. I tried the following, it didn't work. Nothing is shown on the window when I run this.
#!/usr/bin/env python
from PyQt5.QtCore import (QLineF, QPointF, QRectF, Qt)
from PyQt5.QtGui import (QIcon, QBrush, QColor, QPainter, QPixmap)
from PyQt5.QtWidgets import (QAction, QMainWindow, QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem,
QGridLayout, QVBoxLayout, QHBoxLayout,
QLabel, QLineEdit, QPushButton)
class TicTacToe(QGraphicsItem):
def __init__(self):
super(TicTacToe, self).__init__()
def paint(self, painter, option, widget):
painter.setPen(Qt.black)
painter.drawLine(0,100,300,100)
def boundingRect(self):
return QRectF(0,0,300,300)
def mousePressEvent(self, event):
pos = event.pos()
self.select(int(pos.x()/100), int(pos.y()/100))
self.update()
super(TicTacToe, self).mousePressEvent(event)
class MyGraphicsView(QGraphicsView):
def __init__(self):
super(MyGraphicsView, self).__init__()
scene = QGraphicsScene(self)
self.tic_tac_toe = TicTacToe()
scene.addItem(self.tic_tac_toe)
scene.addPixmap(QPixmap("exit.png"))
self.setScene(scene)
self.setCacheMode(QGraphicsView.CacheBackground)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
def keyPressEvent(self, event):
key = event.key()
if key == Qt.Key_R:
self.tic_tac_toe.reset()
super(MyGraphicsView, self).keyPressEvent(event)
class Example(QMainWindow):
def __init__(self):
super(Example, self).__init__()
self.y = MyGraphicsView()
self.initUI()
def initUI(self):
menubar = self.menuBar()
menu = menubar.addMenu('File')
db_action = menu.addAction("Open file")
self.setGeometry(30, 30, 30, 20)
self.setWindowTitle('Menubar')
self.show()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
mainWindow = Example()
mainWindow.showFullScreen()
sys.exit(app.exec_())
A widget is shown in a window if it is a child of some component of the window, in your case self.y is not the child of Example, but only an attribute, a possible solution is to set it as centralWidget:
class Example(QMainWindow):
def __init__(self):
super(Example, self).__init__()
self.y = MyGraphicsView()
self.setCentralWidget(self.y)
self.initUI()
def initUI(self):
menubar = self.menuBar()
menu = menubar.addMenu('File')
db_action = menu.addAction("Open file")
self.setGeometry(30, 30, 30, 20)
self.setWindowTitle('Menubar')
self.show()

PyQt5: painting using events

I am new on PyQt I am working on a project on which I should implement a feature that make the user able to draw a digit using the mouse (digit recognition system). So what I want is when the mouse button is pressed the app will start to draw till the button is released. I made this source code but it is still not working (I think I am struggling with sending a signal to PaintEvent()).
import sys
from PyQt5 import QtCore
from PyQt5 import QtGui, QtWidgets
from PyQt5.QtWidgets import QApplication, QWidget, QInputDialog, QLineEdit, QFileDialog,QGraphicsView,QGraphicsScene,QVBoxLayout
from PyQt5.QtWidgets import (QApplication, QLabel, QWidget)
from PyQt5.QtGui import QPainter, QColor, QPen
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter
from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtWidgets import QMainWindow, QApplication
class Communicate(QObject):
drawApp = pyqtSignal()
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 300, 220)
self.setWindowTitle('Simple')
self.setMouseTracking(True)
self.label = QLabel(self)
self.label.resize(500, 40)
self.c = Communicate()
self.c.drawApp.connect(self.PaintEvent())
self.show()
def mousePressEvent(self, event):
self.c.drawApp.emit()
self.startingposx = event.x()
self.startingposy = event.y()
#super().mousePressEvent(event)
print ("mouse pressed")
def mouseMoveEvent(self, event):
self.label.setText('Coordinates: ( %d : %d )' % (event.x(), event.y()) )
self.y = event.y()
self.x=event.x()
def PaintEvent(self,event):
qp = QPainter()
qp.begin(self)
#qp.setPen(Qt.red)
qp.drawPoints(self,qp)
qp.end()
self.update()
def mouseReleaseEvent(self,event):
self.endingposx = event.x()
self.endingposy = event.y()
super().mouseReleaseEvent(event)
print("starting point was",self.startingposx)
print("starting point y was ",self.startingposy)
print("ending point was ",self.endingposx)
print("ending point was y ",self.endingposy)
print("released")
def drawPoints(self,qp):
qp.setPen(Qt.red)
size = self.size()
x=self.x
y=self.y
qp.drawPoint(x,y)
app = QApplication(sys.argv)
widget = MyWidget()
widget.show()
app.exec_()
Python is sensitive to uppercase and lowercase so be more careful, the method is called paintEvent.
Also you should not call paintEvent directly, you must use the function update(), this method will internally call paintEvent().
But even correcting that error your problem is not solved, if you want to draw a path it is advisable to use QPainterPath as this stores the strokes.
class Drawer(QWidget):
newPoint = pyqtSignal(QPoint)
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.path = QPainterPath()
def paintEvent(self, event):
painter = QPainter(self)
painter.drawPath(self.path)
def mousePressEvent(self, event):
self.path.moveTo(event.pos())
self.update()
def mouseMoveEvent(self, event):
self.path.lineTo(event.pos())
self.newPoint.emit(event.pos())
self.update()
def sizeHint(self):
return QSize(400, 400)
class MyWidget(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setLayout(QVBoxLayout())
label = QLabel(self)
drawer = Drawer(self)
drawer.newPoint.connect(lambda p: label.setText('Coordinates: ( %d : %d )' % (p.x(), p.y())))
self.layout().addWidget(label)
self.layout().addWidget(drawer)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MyWidget()
w.show()
sys.exit(app.exec_())
Screenshot:
I don't have the reputation to reply to eyllanesc's solution, but in case others are having issues, the imports were not complete. Here's is eyllanesc's solution with the imports. It will execute without error:
import sys
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import (QLabel, QWidget)
from PyQt5.QtGui import QPainter, QPainterPath
from PyQt5.QtCore import pyqtSignal, QPoint, QSize
from PyQt5.QtWidgets import QApplication
class Drawer(QWidget):
newPoint = pyqtSignal(QPoint)
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.path = QPainterPath()
def paintEvent(self, event):
painter = QPainter(self)
painter.drawPath(self.path)
def mousePressEvent(self, event):
self.path.moveTo(event.pos())
self.update()
def mouseMoveEvent(self, event):
self.path.lineTo(event.pos())
self.newPoint.emit(event.pos())
self.update()
def sizeHint(self):
return QSize(400, 400)
class MyWidget(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setLayout(QVBoxLayout())
label = QLabel(self)
drawer = Drawer(self)
drawer.newPoint.connect(lambda p: label.setText('Coordinates: ( %d : %d )' % (p.x(), p.y())))
self.layout().addWidget(label)
self.layout().addWidget(drawer)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MyWidget()
w.show()
sys.exit(app.exec_())

Categories

Resources