I want to display the x and y coordinates from an accelerometer on an image. I used QPainter to set up the image and the drawPoint function to draw the coordinate at the given point. When the new coordinate is drawn, I need to erase the old coordinate so that there is only one at a time. In the code below, I called drawCoordinate twice, what happens is the target image ends up with 2 coordinate points at 0,0 and 0.5,0.5 but ideally I would be left with the last drawn coordinate.
This is my first question on here so I hope my format is correct! Let me know if I need to fix anything to help with clarity.
class Target(QWidget):
def __init__(self):
super().__init__()
self.drawing = False
self.image = QPixmap(r"Pictures\target_png_300.png")
self.setGeometry(0, 0, 300, 300)
self.resize(self.image.width(), self.image.height())
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.image)
def paintCoordinate(self, x, y):
painter = QPainter(self.image)
r = QRect(-1, -1, 2, 2)
painter.setWindow(r)
pen = QPen(Qt.black, 0.06, Qt.DotLine, Qt.RoundCap)
painter.setPen(pen)
painter.drawPoint(QPointF(x, y))
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Target()
ex.paintCoordinate(0, 0)
ex.paintCoordinate(0.5, 0.5)
sys.exit(app.exec_())
If you want only one point then don't modify the QPixmap but just do the painting in the paintEvent method:
class Target(QWidget):
def __init__(self):
super().__init__()
self._pixmap = QPixmap()
self._coordinate = QPointF()
#property
def pixmap(self):
return self._pixmap
#pixmap.setter
def pixmap(self, pixmap):
self._pixmap = pixmap.copy()
self.update()
size = self.pixmap.size()
if size.isValid():
self.resize(size)
else:
self.resize(300, 300)
#property
def coordinate(self):
return self._coordinate
#coordinate.setter
def coordinate(self, point):
self._coordinate = point
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.pixmap)
r = QRect(-1, -1, 2, 2)
painter.setWindow(r)
pen = QPen(Qt.black, 0.06, Qt.DotLine, Qt.RoundCap)
painter.setPen(pen)
painter.drawPoint(self.coordinate)
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Target()
ex.pixmap = QPixmap(r"Pictures\target_png_300.png")
ex.coordinate = QPointF(0, 0)
ex.coordinate = QPointF(0.5, 0.5)
ex.show()
sys.exit(app.exec_())
Drawing on a raster image means overriding its contents, there's no "erase" nor "undo": it's like using a brush on a painting: if you try to "erase" it's like using bleach on what you're trying to "unpaint".
Keep track of what you're manually painting (the coordinates) and then implement the paintEvent accordingly.
class Target(QWidget):
def __init__(self):
super().__init__()
self.drawing = False
self.image = QPixmap(r"Pictures\target_png_300.png")
self.setGeometry(0, 0, 300, 300)
self.resize(self.image.width(), self.image.height())
self.points = []
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.image)
r = QRect(-1, -1, 2, 2)
painter.setWindow(r)
painter.setPen(QPen(Qt.black, 0.06, Qt.DotLine, Qt.RoundCap))
for point in self.points:
painter.drawPoint(point)
def paintCoordinate(self, x, y):
self.points.append(QPointF(x, y))
self.update()
def deleteLastPoint(self):
self.points.pop()
self.update()
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = Target()
ex.paintCoordinate(0, 0)
ex.paintCoordinate(0.5, 0.5)
QTimer.singleShot(1000, lambda: ex.deleteLastPoint())
sys.exit(app.exec_())
Related
I cannot figure out how to crop a QPixmap that is set on a scene with a rotated QGraphicsRectItem also placed in the same scene.
Here is the code for my QGraphicsScene and QPixmap.
class crop_pattern(QGraphicsView):
img_is_cropped = QtCore.Signal(object)
def __init__(self, path, scale):
super().__init__()
# Connect graphics scene with graphics view
self.setFixedSize(500, 500)
self.scene = QGraphicsScene(self)
self.setScene(self.scene)
self.roi_scale = scale
# Display image
self.set_image(path)
def set_image(self, path):
pixmap = QtGui.QPixmap(path)
if pixmap:
pixmap = pixmap.scaledToHeight(self.roi_scale * pixmap.height())
self.scene.clear()
self.scene.addPixmap(pixmap)
self.setAlignment(QtCore.Qt.AlignCenter)
def wheelEvent(self, event):
zoomInFactor = 1.05
zoomOutFactor = 1 / zoomInFactor
oldPos = self.mapToScene(event.pos())
if event.angleDelta().y() > 0:
zoomFactor = zoomInFactor
else:
zoomFactor = zoomOutFactor
self.scale(zoomFactor, zoomFactor)
newPos = self.mapToScene(event.pos())
delta = newPos - oldPos
self.translate(delta.x(), delta.y())
(Credits to QGraphicsView Zooming in and out under mouse position using mouse wheel for the wheelEvent function)
Here is the QGraphicsItem that is generated when the user clicks a certain button.
QtCore.Slot(bool)
def create_shape(self):
sender = self.sender()
if sender.text().lower() == "circle":
self.shape = ellipse_shape(0, 0, 100, 100)
elif sender.text().lower() == "rectangle":
self.shape = rect_shape(0, 0, 100, 100)
self.shape.setZValue(1)
self.shape.setTransformOriginPoint(50, 50)
self.crop_pattern.scene.addItem(self.shape) # added item to the same scene, which is crop_pattern.
Here is the GUI as the question suggested. (QGraphicsRectItem has been resized)
How can I crop the pixels inside the rectangle? Thanks!
One possible solution is to create a hidden QGraphicsView and use the render() method to save the image section. The objective of the hidden QGraphicsView is not to modify the existing view since the image must be rotated in addition to not being affected by the scaling.
from PyQt5 import QtCore, QtGui, QtWidgets
def crop_rect(rect_item, scene):
is_visible = rect_item.isVisible()
rect_item.hide()
hide_view = QtWidgets.QGraphicsView(scene)
hide_view.rotate(-rect_item.rotation())
polygon = rect_item.mapToScene(rect_item.rect())
pixmap = QtGui.QPixmap(rect_item.rect().size().toSize())
pixmap.fill(QtCore.Qt.transparent)
source_rect = hide_view.mapFromScene(polygon).boundingRect()
painter = QtGui.QPainter(pixmap)
hide_view.render(
painter,
target=QtCore.QRectF(pixmap.rect()),
source=source_rect,
)
painter.end()
rect_item.setVisible(is_visible)
return pixmap
def main():
app = QtWidgets.QApplication([])
scene = QtWidgets.QGraphicsScene()
view = QtWidgets.QGraphicsView(alignment=QtCore.Qt.AlignCenter)
view.setScene(scene)
# emulate wheel
view.scale(0.8, 0.8)
# create pixmap
pixmap = QtGui.QPixmap(500, 500)
pixmap.fill(QtGui.QColor("green"))
painter = QtGui.QPainter(pixmap)
painter.setBrush(QtGui.QColor("salmon"))
painter.drawEllipse(pixmap.rect().adjusted(100, 90, -80, -100))
painter.end()
pixmap_item = scene.addPixmap(pixmap)
rect = QtCore.QRectF(0, 0, 200, 300)
rect_item = scene.addRect(rect)
rect_item.setPen(QtGui.QPen(QtGui.QColor("red"), 4))
rect_item.setPos(100, 100)
rect_item.setTransformOriginPoint(50, 50)
rect_item.setRotation(10)
view.resize(640, 480)
view.show()
qpixmap = crop_rect(rect_item, scene)
label = QtWidgets.QLabel()
label.setPixmap(qpixmap)
label.show()
app.exec_()
if __name__ == "__main__":
main()
Input:
Output:
I have created a custom QGraphicsWidget with the ability to resize the widget in the scene. I can also add predefined widgets such as buttons, labels, etc. to my custom widget. I now have two problems.
The first being that the widget doesn't change the size (to re-adjust) upon inserting a new label or LineEdit widget as a result newly inserted widget stays out of the custom widget border.
The second problem is encountered when I try to change the setContentMargins of the QGraphicsLayout to something other than 0. For example QGraphicsLayout.setContentMargins(1, 1, 1, 20) will delay the cursor in the LineEdit widget.
Here is the image.
(Drag the grey triangle to change size)
import sys
from PyQt5 import QtWidgets, QtCore, QtGui, Qt
from PyQt5.QtCore import Qt, QRectF, QPointF
from PyQt5.QtGui import QBrush, QPainterPath, QPainter, QColor, QPen, QPixmap
from PyQt5.QtWidgets import QGraphicsRectItem, QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem
class Container(QtWidgets.QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.layout = QtWidgets.QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
self.setStyleSheet('Container{background:transparent;}')
class GraphicsFrame(QtWidgets.QGraphicsWidget):
def __init__(self, *args, **kwargs):
super(GraphicsFrame, self).__init__()
x, y, h, w = args
rect = QRectF(x, y, h, w)
self.setGeometry(rect)
self.setMinimumSize(150, 150)
self.setMaximumSize(400, 800)
self.setAcceptHoverEvents(True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.mousePressPos = None
self.mousePressRect = None
self.handleSelected = None
self.polygon = QtGui.QPolygon([
QtCore.QPoint(int(self.rect().width()-10), int(self.rect().height()-20)),
QtCore.QPoint(int(self.rect().width()-10), int(self.rect().height()-10)),
QtCore.QPoint(int(self.rect().width()-20), int(self.rect().height()-10))
])
graphic_layout = QtWidgets.QGraphicsLinearLayout(Qt.Vertical, self)
graphic_layout.setContentsMargins(0, 0, 0, 20) # changing this will cause the second problem
self.container = Container()
proxyWidget = QtWidgets.QGraphicsProxyWidget(self)
proxyWidget.setWidget(self.container)
graphic_layout.addItem(proxyWidget)
self.contentLayout = QtWidgets.QFormLayout()
self.contentLayout.setContentsMargins(10, 10, 20, 20)
self.contentLayout.setSpacing(5)
self.container.layout.addLayout(self.contentLayout)
self.options = []
def addOption(self, color=Qt.white, lbl=None, widget=None):
self.insertOption(-1, lbl, widget, color)
def insertOption(self, index, lbl, widget, color=Qt.white):
if index < 0:
index = self.contentLayout.count()
self.contentLayout.addRow(lbl, widget)
self.options.insert(index, (widget, color))
def update_polygon(self):
self.polygon = QtGui.QPolygon([
QtCore.QPoint(int(self.rect().width() - 10), int(self.rect().height() - 20)),
QtCore.QPoint(int(self.rect().width() - 10), int(self.rect().height() - 10)),
QtCore.QPoint(int(self.rect().width() - 20), int(self.rect().height() - 10))
])
def hoverMoveEvent(self, event):
if self.polygon.containsPoint(event.pos().toPoint(), Qt.OddEvenFill):
self.setCursor(Qt.SizeFDiagCursor)
else:
self.unsetCursor()
super(GraphicsFrame, self).hoverMoveEvent(event)
def mousePressEvent(self, event):
self.handleSelected = self.polygon.containsPoint(event.pos().toPoint(), Qt.OddEvenFill)
if self.handleSelected:
self.mousePressPos = event.pos()
self.mousePressRect = self.boundingRect()
super(GraphicsFrame, self).mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.handleSelected:
self.Resize(event.pos())
else:
super(GraphicsFrame, self).mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
super(GraphicsFrame, self).mouseReleaseEvent(event)
self.handleSelected = False
self.mousePressPos = None
self.mousePressRect = None
self.update()
def paint(self, painter, option, widget):
painter.save()
painter.setBrush(QBrush(QColor(37, 181, 247)))
pen = QPen(Qt.white)
pen.setWidth(2)
if self.isSelected():
pen.setColor(Qt.yellow)
painter.setPen(pen)
painter.drawRoundedRect(self.rect(), 4, 4)
painter.setPen(QtCore.Qt.white)
painter.setBrush(QtCore.Qt.gray)
painter.drawPolygon(self.polygon)
super().paint(painter, option, widget)
painter.restore()
def Resize(self, mousePos):
"""
Perform shape interactive resize.
"""
if self.handleSelected:
self.prepareGeometryChange()
width, height = self.geometry().width()+(mousePos.x()-self.mousePressPos.x()),\
self.geometry().height()+(mousePos.y()-self.mousePressPos.y())
self.setGeometry(QRectF(self.geometry().x(), self.geometry().y(), width, height))
self.contentLayout.setGeometry(QtCore.QRect(0, 30, width-10, height-20))
self.mousePressPos = mousePos
self.update_polygon()
self.updateGeometry()
def main():
app = QApplication(sys.argv)
grview = QGraphicsView()
scene = QGraphicsScene()
grview.setViewportUpdateMode(grview.FullViewportUpdate)
scene.addPixmap(QPixmap('01.png'))
grview.setScene(scene)
item = GraphicsFrame(0, 0, 300, 150)
scene.addItem(item)
item.addOption(Qt.green, lbl=QtWidgets.QLabel('I am a label'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('why'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('How'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('Nooo.'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('Nooo.'), widget=QtWidgets.QLineEdit())
item.addOption(lbl=QtWidgets.QLabel('Nooo.'), widget=QtWidgets.QLineEdit())
item2 = GraphicsFrame(50, 50, 300, 150)
scene.addItem(item2)
grview.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
grview.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
As already suggested to you more than once, using a QGraphicsWidget with a QGraphicsLayout is not a good idea if you are only using it to embed a QGraphicsProxyWidget, as you will certainly have to face unexpected behavior when changing geometries unless you really know what you're doing.
Then, prepareGeometryChange and updateGeometry are completely unnecessary for QGraphicsWidget, and resizing the widget using the item geometries is absolutely wrong for two reasons: first of all, it's up the graphics layout to manage the content size, then you're using scene coordinates, and since you're using scaling, those coordinates will not be correct as they should be transformed in widget's coordinate.
Since using a QSizeGrip is not doable due to the continuously changing scene rect (which, I have to say, is not always a good idea if done along with interactive resizing of contents), you can use a simple QGraphicsPathItem for it, and use that as a reference for the resizing, which is far more simple than continuously move the polygon and draw it.
class SizeGrip(QtWidgets.QGraphicsPathItem):
def __init__(self, parent):
super().__init__(parent)
path = QtGui.QPainterPath()
path.moveTo(0, 10)
path.lineTo(10, 10)
path.lineTo(10, 0)
path.closeSubpath()
self.setPath(path)
self.setPen(QtGui.QPen(Qt.white))
self.setBrush(QtGui.QBrush(Qt.white))
self.setCursor(Qt.SizeFDiagCursor)
class GraphicsFrame(QtWidgets.QGraphicsItem):
def __init__(self, *args, **kwargs):
super(GraphicsFrame, self).__init__()
x, y, w, h = args
self.setPos(x, y)
self.setAcceptHoverEvents(True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.container = Container()
self.proxy = QtWidgets.QGraphicsProxyWidget(self)
self.proxy.setWidget(self.container)
self.proxy.setMinimumSize(150, 150)
self.proxy.setMaximumSize(400, 800)
self.proxy.resize(w, h)
self.contentLayout = QtWidgets.QFormLayout()
self.contentLayout.setContentsMargins(10, 10, 20, 20)
self.contentLayout.setSpacing(5)
self.container.layout.addLayout(self.contentLayout)
self.options = []
self.sizeGrip = SizeGrip(self)
self.mousePressPos = None
self.proxy.geometryChanged.connect(self.resized)
self.resized()
def addOption(self, color=Qt.white, lbl=None, widget=None):
self.insertOption(-1, lbl, widget, color)
def insertOption(self, index, lbl, widget, color=Qt.white):
if index < 0:
index = self.contentLayout.count()
self.contentLayout.addRow(lbl, widget)
self.options.insert(index, (widget, color))
def mousePressEvent(self, event):
gripShape = self.sizeGrip.shape().translated(self.sizeGrip.pos())
if event.button() == Qt.LeftButton and gripShape.contains(event.pos()):
self.mousePressPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.mousePressPos:
delta = event.pos() - self.mousePressPos
geo = self.proxy.geometry()
bottomRight = geo.bottomRight()
geo.setBottomRight(bottomRight + delta)
self.proxy.setGeometry(geo)
diff = self.proxy.geometry().bottomRight() - bottomRight
if diff.x():
self.mousePressPos.setX(event.pos().x())
if diff.y():
self.mousePressPos.setY(event.pos().y())
else:
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
self.mousePressPos = None
super().mouseReleaseEvent(event)
def resized(self):
rect = self.boundingRect()
self.sizeGrip.setPos(rect.bottomRight() + QtCore.QPointF(-20, -20))
def boundingRect(self):
return self.proxy.boundingRect().adjusted(-11, -11, 11, 11)
def paint(self, painter, option, widget):
painter.save()
painter.setBrush(QBrush(QColor(37, 181, 247)))
painter.drawRoundedRect(self.boundingRect().adjusted(0, 0, -.5, -.5), 4, 4)
painter.restore()
Do note that using fitInView() before showing the view is not a good idea, especially if using proxy widgets and layouts.
Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 2 years ago.
Improve this question
I am trying to implement a function that will join two circles(with a line from center to center), by creating a list of lines for every pair of the circles that have to be joined. There was some error but when I passed two objects in circles list and corresponding center points to the lines list it worked.
Could someone help to solve this issue to create a function that will join the circles when join button is clicked and two circles are selected by clicking
Below is the code, which when runs have two circles with a line and a functionlity to add more circles by clicking "Add" button. Or am I taking a wrong technique, Please suggest if anything is much simpler. Struggling for a long time... I am attaching a screenshot.
import random
import sys
from PyQt5.QtWidgets import QMenu
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import QRect, QSize, QPoint, QLineF
class Window(QtWidgets.QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.rect = QtCore.QRect()
self.drag_position = QtCore.QPoint()
self.circles = [QRect(100, 200, 100, 100), QRect(100, 300, 100, 100)]
self.lines = []
self.a = [0, 0, 500, 400]
self.current_circle = None
button = QtWidgets.QPushButton("Add", self)
button.clicked.connect(self.on_clicked)
join = QtWidgets.QPushButton("Join", self)
join.clicked.connect(self.joinAction)
join.setGeometry(100, 0, 100, 30)
Delete = QtWidgets.QPushButton("Delete", self)
Delete.clicked.connect(self.DeleteItem)
Delete.setGeometry(200, 0, 100, 30)
self.resize(640, 480)
def on_clicked(self):
coor = QPoint(random.randrange(self.width() - 100), random.randrange(self.height()) - 100)
self.circles.append(QRect(coor, QSize(100, 100)))
self.update()
def joinAction(self, event):
pass
contextMenu = QMenu(self)
delAct = contextMenu.addAction("Delete Circle")
action = contextMenu.exec_(self.mapToGlobal(event.pos()))
if action == delAct:
pass
def DeleteItem(self):
pass
def paintEvent(self, event):
super().paintEvent(event)
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QPen(QtCore.Qt.black, 5, QtCore.Qt.SolidLine))
for circle in self.circles:
painter.drawEllipse(circle)
# for line in self.lines:
painter.drawLine(self.circles[0].center(), self.circles[1].center())
def mousePressEvent(self, event):
for circle in self.circles:
line = QLineF(circle.center(), event.pos())
if line.length() < circle.width() / 2:
self.current_circle = circle
self.drag_position = event.pos()
break
def mouseMoveEvent(self, event):
if self.current_circle is not None:
self.current_circle.translate(event.pos() - self.drag_position)
self.drag_position = event.pos()
self.update()
def mouseReleaseEvent(self, event):
self.current_circle = None
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
Rect = Window()
Rect.show()
sys.exit(app.exec_())
Here is one way to do it. I defined a Circle class with the attributes line_to and line_from to keep track of which circles are connected. A list is kept in the Window class, last_two_clicked, so when the join button is clicked, the two most recent circles that were pressed will be joined.
class Circle(QRect):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.line_to = self.line_from = None
def join(self, other):
self.line_to = other
other.line_from = self
if self.line_from and self.line_from.line_to == self:
self.line_from.line_to = None
self.line_from = other.line_to = None
class Window(QtWidgets.QMainWindow):
def __init__(self):
super(Window, self).__init__()
self.rect = QtCore.QRect()
self.drag_position = QtCore.QPoint()
self.circles = [Circle(100, 200, 100, 100), Circle(100, 300, 100, 100)]
self.current_circle = None
self.last_two_clicked = self.circles[:]
button = QtWidgets.QPushButton("Add", self)
button.clicked.connect(self.on_clicked)
join = QtWidgets.QPushButton("Join", self)
join.clicked.connect(self.joinAction)
join.setGeometry(100, 0, 100, 30)
Delete = QtWidgets.QPushButton("Delete", self)
Delete.clicked.connect(self.DeleteItem)
Delete.setGeometry(200, 0, 100, 30)
self.resize(640, 480)
def on_clicked(self):
coor = (random.randrange(self.width() - 100), random.randrange(self.height() - 100))
c = Circle(*coor, 100, 100)
self.circles.append(c)
self.last_two_clicked.insert(0, c)
self.last_two_clicked = self.last_two_clicked[:2]
self.update()
def joinAction(self, event):
c1, c2 = self.last_two_clicked
c1.join(c2)
self.update()
def DeleteItem(self):
pass
def paintEvent(self, event):
super().paintEvent(event)
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtGui.QPen(QtCore.Qt.black, 5, QtCore.Qt.SolidLine))
for circle in self.circles:
painter.drawEllipse(circle)
if circle.line_to:
painter.drawLine(circle.center(), circle.line_to.center())
def mousePressEvent(self, event):
for circle in self.circles:
line = QLineF(circle.center(), event.pos())
if line.length() < circle.width() / 2:
self.current_circle = circle
self.drag_position = event.pos()
self.last_two_clicked.insert(0, circle)
self.last_two_clicked = self.last_two_clicked[:2]
break
def mouseMoveEvent(self, event):
if self.current_circle is not None:
self.current_circle.translate(event.pos() - self.drag_position)
self.drag_position = event.pos()
self.update()
def mouseReleaseEvent(self, event):
self.current_circle = None
I am currently having issues understanding the behavior of QGraphicsAnchorLayout within a QGraphicsScene. I create 4 boxes and anchor the corners of each, but no anchors appear to be applied properly, or at least the way I believed they would be.
The yellow box should be on the top left of the QGraphicsScene at all times, even when the GraphicsView is expanded. The blue box is anchored to appear adjacent to the yellow box on the right, with its top the coinciding with the top of the QGraphicsScene/viewport.
The top left corner of the green box is anchored to the bottom right of the blue box and likewise for the red box to the green box. But this is what I am getting:
I expect the yellow box to be at the top of the graphics scene/viewport at all times. And I would like for it always to remain visible even when scrolled right, but I believe that probably would be a separate issue. However, when I expand the window vertically, all the boxes are centered, including the yellow box which I expected to remain at top.
The blue, green and red boxes seem to bear no resemblance to the anchors I applied.
Following is the code I used to generate this. How do these anchors work and what can I do to correct this?
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
from debug_utils import *
from PyQt5.QtWidgets import QGraphicsAnchorLayout, QGraphicsWidget, QGraphicsLayoutItem
def qp(p):
return "({}, {})".format(p.x(), p.y())
class box(QtWidgets.QGraphicsWidget):
pressed = QtCore.pyqtSignal()
def __init__(self, rect, color, parent=None):
super(box, self).__init__(parent)
self.raw_rect = rect
self.rect = QtCore.QRectF(rect[0], rect[1], rect[2], rect[3])
self.color = color
def boundingRect(self):
pen_adj = 0
return self.rect.normalized().adjusted(-pen_adj, -pen_adj, pen_adj, pen_adj)
def paint(self, painter, option, widget):
r = self.boundingRect()
brush = QtGui.QBrush()
brush.setColor(QtGui.QColor(self.color))
brush.setStyle(Qt.SolidPattern)
#rect = QtCore.QRect(0, 0, painter.device().width(), painter.device().height())
painter.fillRect(self.boundingRect(), brush)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(Qt.darkGray)
painter.drawRect(self.boundingRect())
#painter.drawRect(0, 0, max_time*char_spacing, self.bar_height)
def mousePressEvent(self, ev):
self.pressed.emit()
self.update()
def mouseReleaseEvent(self, ev):
self.update()
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self.numbers = []
self.setMouseTracking(True)
l = QGraphicsAnchorLayout()
l.setSpacing(0)
w = QGraphicsWidget()
#painter = QtGui.QPainter(self)
w.setPos(0, 0)
w.setLayout(l)
scene.addItem(w)
self.main_widget = w
self.main_layout = l
self.makeBoxs()
def makeBoxs(self):
rect = [0, 0, 600, 250]
blue_box = box(rect, QtGui.QColor(0, 0, 255, 128))
green_box = box(rect, QtGui.QColor(0, 255, 0, 128))
red_box = box([0, 0, 200, 50], QtGui.QColor(255, 0, 0, 128))
yellow_box_left = box([0, 0, 75, 600], QtGui.QColor(255, 255, 0, 128))
#self.scene().setSceneRect(blue_box.rect)
#self.scene().setSceneRect(bar_green.rect)
# Adding anchors adds the item to the layout which is part of the scene
self.main_layout.addCornerAnchors(yellow_box_left, Qt.TopLeftCorner, self.main_layout, Qt.TopLeftCorner)
self.main_layout.addCornerAnchors(blue_box, Qt.TopLeftCorner, yellow_box_left, Qt.TopRightCorner)
self.main_layout.addCornerAnchors(green_box, Qt.TopLeftCorner, blue_box, Qt.BottomRightCorner)
self.main_layout.addCornerAnchors(red_box, Qt.TopLeftCorner, green_box, Qt.BottomRightCorner)
#self.main_layout.addAnchor(bar_green, Qt.AnchorTop, blue_box, Qt.AnchorBottom)
#self.main_layout.addAnchor(bar_green, Qt.AnchorLeft, blue_box, Qt.AnchorRight)
def printStatus(self, pos):
msg = "Viewport Position: " + str(qp(pos))
v = self.mapToScene(pos)
v = QtCore.QPoint(v.x(), v.y())
msg = msg + ", Mapped to Scene: " + str(qp(v))
v = self.mapToScene(self.viewport().rect()).boundingRect()
msg = msg + ", viewport Mapped to Scene: " + str(qp(v))
v2 = self.mapToScene(QtCore.QPoint(0, 0))
msg = msg + ", (0, 0) to scene: " + qp(v2)
self.parent().statusBar().showMessage(msg)
def mouseMoveEvent(self, event):
pos = event.pos()
self.printStatus(pos)
super(GraphicsView, self).mouseMoveEvent(event)
def resizeEvent(self, event):
self.printStatus(QtGui.QCursor().pos())
h = self.mapToScene(self.viewport().rect()).boundingRect().height()
r = self.sceneRect()
r.setHeight(h)
height = self.viewport().height()
for item in self.items():
item_height = item.boundingRect().height()
super(GraphicsView, self).resizeEvent(event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
gv = GraphicsView()
self.setCentralWidget(gv)
self.setGeometry(475, 250, 600, 480)
scene = self.scene = gv.scene()
sb = self.statusBar()
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
EDIT: Adding expected output
Based on how the anchors are defined, I expect the output to look something like the following. Since I can't yet actually create what I need, I have created this in PowerPoint. But, of course, in addition to getting this to work, I'm hoping to understand how to use anchors in a more general sense as well.
EDIT 2:
Thank you again for updating. It's not quite what I was expecting, and there is a strange artifact when I scroll. Just to clarify,
I expect the yellow widget to be visible at all times, top left of viewport with the highest z-order. I think you provided that.
All the other widgets should scroll, maintaining their relative anchors. I removed the blue box's anchor to the yellow box, but then all boxes align left.
In your current implementation, the non-yellow boxes do not scroll, but when I resize or move the scroll bar right and back left I see this artifact:
You have 2 errors:
Your Box class is poorly built, instead of override boundingRect it only sets the size since the position will be handled by the layout.
The position of a layout is always relative to the widget where it is set. And in your case you want the top-left of the main_layout to match the top-left of the viewport so you must modify the top-left of the main_widget to match.
Considering the above, the solution is:
from PyQt5 import QtCore, QtGui, QtWidgets
def qp(p):
return "({}, {})".format(p.x(), p.y())
class Box(QtWidgets.QGraphicsWidget):
pressed = QtCore.pyqtSignal()
def __init__(self, size, color, parent=None):
super(Box, self).__init__(parent)
self.setMinimumSize(size)
self.setMaximumSize(size)
self.color = color
def paint(self, painter, option, widget):
brush = QtGui.QBrush()
brush.setColor(QtGui.QColor(self.color))
brush.setStyle(QtCore.Qt.SolidPattern)
painter.fillRect(self.boundingRect(), brush)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtCore.Qt.darkGray)
painter.drawRect(self.boundingRect())
def mousePressEvent(self, event):
self.pressed.emit()
super().mousePressEvent(event)
class GraphicsView(QtWidgets.QGraphicsView):
messageChanged = QtCore.pyqtSignal(str)
def __init__(self, parent=None):
super(GraphicsView, self).__init__(parent)
self.setMouseTracking(True)
self.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
l = QtWidgets.QGraphicsAnchorLayout()
l.setSpacing(0)
w = QtWidgets.QGraphicsWidget()
self.scene().sceneRectChanged.connect(self.update_widget)
self.horizontalScrollBar().valueChanged.connect(self.update_widget)
self.verticalScrollBar().valueChanged.connect(self.update_widget)
w.setLayout(l)
scene.addItem(w)
self.main_widget = w
self.main_layout = l
self.makeBoxs()
def makeBoxs(self):
blue_box = Box(QtCore.QSizeF(300, 125), QtGui.QColor(0, 0, 255, 128))
green_box = Box(QtCore.QSizeF(300, 125), QtGui.QColor(0, 255, 0, 128))
red_box = Box(QtCore.QSizeF(100, 25), QtGui.QColor(255, 0, 0, 128))
yellow_box = Box(QtCore.QSizeF(37.5, 300), QtGui.QColor(255, 255, 0, 128))
# yellow_box_left top-left
self.main_layout.addCornerAnchors(
yellow_box,
QtCore.Qt.TopLeftCorner,
self.main_layout,
QtCore.Qt.TopLeftCorner,
)
self.main_layout.addCornerAnchors(
blue_box, QtCore.Qt.TopLeftCorner, yellow_box, QtCore.Qt.TopRightCorner
)
self.main_layout.addCornerAnchors(
green_box, QtCore.Qt.TopLeftCorner, blue_box, QtCore.Qt.BottomRightCorner
)
self.main_layout.addCornerAnchors(
red_box, QtCore.Qt.TopLeftCorner, green_box, QtCore.Qt.BottomRightCorner
)
# self.setSceneRect(self.scene().itemsBoundingRect())
def update_widget(self):
vp = self.viewport().mapFromParent(QtCore.QPoint())
tl = self.mapToScene(vp)
geo = self.main_widget.geometry()
geo.setTopLeft(tl)
self.main_widget.setGeometry(geo)
def resizeEvent(self, event):
self.update_widget()
super().resizeEvent(event)
def mouseMoveEvent(self, event):
pos = event.pos()
self.printStatus(pos)
super(GraphicsView, self).mouseMoveEvent(event)
def printStatus(self, pos):
msg = "Viewport Position: " + str(qp(pos))
v = self.mapToScene(pos)
v = QtCore.QPoint(v.x(), v.y())
msg = msg + ", Mapped to Scene: " + str(qp(v))
v = self.mapToScene(self.viewport().rect()).boundingRect()
msg = msg + ", viewport Mapped to Scene: " + str(qp(v))
v2 = self.mapToScene(QtCore.QPoint(0, 0))
msg = msg + ", (0, 0) to scene: " + qp(v2)
self.messageChanged.emit(msg)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
gv = GraphicsView()
self.setCentralWidget(gv)
self.setGeometry(475, 250, 600, 480)
gv.messageChanged.connect(self.statusBar().showMessage)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
My QGraphicsView should show an image of a large resolution. The size should fit inside a resizable window. Currently, the image is viewed in a way that I want it to but only by providing some manually adjusted values to the initial view geometry. This doe not look neat. I also tried to refer to the solutions posted here: Graphics View and Pixmap Size
My current Window looks like this:
class ImageCheck(Ui_ImageCheck.Ui_MainWindow, QMainWindow):
def __init__(self, parent=None):
super(ImageCheck, self).__init__()
self.setupUi(self)
self.setWindowTitle("Image Analyzer")
self.crop_ratio_w = 1
self.crop_ratio_h = 1
self.path = None
self.scene = QGraphicsScene()
self.scene.clear()
self.image_item = QGraphicsPixmapItem()
# This is the approximate shift in coordinates of my initial view from the window
self.view.setGeometry(self.geometry().x()+ 10, self.geometry().y()+ 39,
self.geometry().width()- 55, self.geometry().height()- 110)
self.view.setAlignment(Qt.AlignCenter)
self.view.setFrameShape(QFrame.NoFrame)
def setImage(self, path):
self.path = path
self.crop_ratio_w = self.pixmap.width() / self.view.width()
self.crop_ratio_h = self.pixmap.height() / self.view.height()
pixmap = QPixmap(path)
smaller_pixmap = pixmap.scaled(self.view.width(), self.view.height(),
Qt.IgnoreAspectRatio, t.FastTransformation)
self.image_item.setPixmap(smaller_pixmap)
self.scene.addItem(self.image_item)
self.scene.setSceneRect(0, 0, self.view.width(), self.view.height())
self.view.setGeometry(0, 0, self.view.width(), self.view.height())
self.view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.view.setScene(self.scene)
self.view.setSceneSize()
def resizeEvent(self, event):
self.view.setGeometry(self.geometry().x()+ 10, self.geometry().y()+ 39,
self.geometry().width()- 55, self.geometry().height()- 110)
self.setImage(self.path)
My manual override was probably not a good idea when I tried to determine distances between two points. Even the scaled distance gives me a slightly wrong value.
I can not use your code because there are many hidden things so I will propose the next solution that is to rescale the view based on the scene each time the window changes its size. I have also implemented a signal that transports the clicked information in the image based on the coordinates of the image.
from PyQt5 import QtCore, QtGui, QtWidgets
class ClickableGraphicsView(QtWidgets.QGraphicsView):
clicked = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, parent=None):
super(ClickableGraphicsView, self).__init__(parent)
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self.pixmap_item = None
def setImage(self, path):
pixmap = QtGui.QPixmap(path)
self.pixmap_item = self.scene().addPixmap(pixmap)
self.pixmap_item.setShapeMode(
QtWidgets.QGraphicsPixmapItem.BoundingRectShape
)
def mousePressEvent(self, event):
if self.pixmap_item is not None:
if self.pixmap_item == self.itemAt(event.pos()):
sp = self.mapToScene(event.pos())
lp = self.pixmap_item.mapToItem(self.pixmap_item, sp)
p = lp.toPoint()
if self.pixmap_item.pixmap().rect().contains(p):
self.clicked.emit(p)
super(ClickableGraphicsView, self).mousePressEvent(event)
def resizeEvent(self, event):
self.fitInView(self.sceneRect(), QtCore.Qt.IgnoreAspectRatio)
super(ClickableGraphicsView, self).resizeEvent(event)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowTitle("Image Analyzer")
view = ClickableGraphicsView()
view.clicked.connect(print)
view.setImage("image.jpg")
label = QtWidgets.QLabel("Distance")
display = QtWidgets.QLCDNumber()
buttonbox = QtWidgets.QDialogButtonBox(
QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel
)
widget = QtWidgets.QWidget()
self.setCentralWidget(widget)
lay = QtWidgets.QGridLayout(widget)
lay.addWidget(view, 0, 0, 1, 2)
hlay = QtWidgets.QHBoxLayout()
hlay.addWidget(label)
hlay.addWidget(display)
hlay.addStretch()
lay.addLayout(hlay, 1, 0)
lay.addWidget(buttonbox, 1, 1)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())