To draw rectangles inside label using Qpainter in python - python

I am trying to draw rectangles according to a percentage value inside my labels. So it is important for me to know the exact coordinates of my labels. Since I am using multiple windows in cascade, the global positions of the current window are not retrieved accurately. Is there an easy workaround to this problem without changing my entire coding layout? form2 is my current view which is displayed as part of form1.
It could be helpful if I can map the coordinates of my label by using mapTo some widget inside my current window. My code seems to work except for the position of my rectangle. I have tried maptToGlobal() and mapToParent() and
self.label_pos = self.mapTo(self.frame, self.label_1.geometry().bottomLeft()) I cannot seem to access the widgets written inside my parent class using self. Or is there something wrong with my syntax?
import Ui_ImageCrop # design of my window using Qt Designer
import math
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMainWindow, QLabel
from PyQt5.QtGui import QPainter, QPen
class ImageCrop(Ui_ImageCrop.Ui_MainWindow, QMainWindow):
def __init__(self, parent=None):
super(ImageCrop,self ).__init__()
self.setupUi(self)
def paintEvent(self,e):
painter = QPainter(self)
self.square1 = Show_Square()
if self.lineEdit_1.text() == "":
self.square1.percent = 0
else:
self.square1.percent = float(self.lineEdit_1.text())
self.label_pos = self.mapTo(self, self.label_1.geometry().bottomLeft())
self.square1.x = self.label_pos.x()
self.square1.y = self.label_pos.y()
self.square1.w = self.label_1.width()
self.square1.h = self.label_1.height()
self.square1.drawRectangles(painter)
self.update()
class Show_Square(QLabel):
def __init__(self):
super(Show_Square,self).__init__()
self.percent = 50
self.x = 0
self.y = 0
self.w = 10
self.h = 10
def drawRectangles(self, painter):
center_x = float(self.x+self.w/2)
center_y = float(self.y+self.h/2)
rect_crop = float(self.percent*self.w*self.h/100)
k = float(self.h/self.w)
rect_w = int(math.sqrt(rect_crop/k))
rect_h = int(k*rect_w)
rect_x = int(center_x-rect_h/2)
rect_y = int(center_y-rect_w/2)
painter.setPen(QPen(Qt.black, 1, Qt.SolidLine))
painter.drawRect(rect_x,rect_y,rect_w,rect_h)

This is a solution that I could come out with. Total three frames were used in my layout for each label. So each label positions were found after adding the enclosing frame geometries.
frame1 = self.frame_1.geometry().topLeft()
frame2 = self.frame_2.geometry().topLeft()
frame3 = self.frame_3.geometry().topLeft()
label1_topLeft = frame1 + frame2 + frame3 + self.label_1.geometry().topLeft()
label1_bottomRight = frame1 + frame2 + frame3 + self.label_1.geometry().bottomRight()
Also, instead of placing the rectangles by drawing over the pixmap each time in paintEvent, it seems to work faster when the rectangles are drawn inside the labels. One could leave the images partly transparent.

Related

How can I limit the GraphicsScene in order to only be able to draw on the image loaded?

I have a script that is loading an image and displaying it in a QGraphicsView, it also sets the size of the GraphicsView to the dimensions of the image.
I need to be able to zoom in / out while only being able to draw on the image. Currently I can zoom in / out but can draw anywhere on the GraphicsView, meaning that when the image is zoomed out I can draw outside of it as seen below.
What can I do to make it to where the RubberBand can only be drawn on the image?
Here is my GraphicsView:
from PyQt5.QtWidgets import QGraphicsScene, QGraphicsView, QGraphicsItem
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBrush, QPen
from SelectionBand import *
from time import sleep
class CustomGraphicsView(QGraphicsView):
def __init__(self, parent=None):
super(CustomGraphicsView, self).__init__()
self.setScene(QGraphicsScene(self))
self.setAlignment(Qt.AlignLeft | Qt.AlignTop)
self.scene().setBackgroundBrush(Qt.gray)
self.brush = QBrush(Qt.green)
self.pen = QPen(Qt.blue)
def setImage(self, image):
self.img = self.scene().addPixmap(image)
self.resize(self.img.boundingRect().width(),self.img.boundingRect().height())
self.scene().setSceneRect(self.img.boundingRect())
def wheelEvent(self, event):
if event.modifiers() & Qt.ControlModifier:
x = event.angleDelta().y() / 120
if x > 0:
self.scale(1.05, 1.05)
elif x < 0:
self.scale(.95, .95)
else:
super().wheelEvent(event)
def mousePressEvent(self, event):
pos = self.mapToScene(event.pos())
self.xPos = pos.x()
self.yPos = pos.y()
self.band = SelectionBand(self)
self.band.setGeometry(pos.x(), pos.y(), 0, 0)
item = self.scene().addWidget(self.band)
item.setFlag(QGraphicsItem.ItemIsMovable)
item.setZValue(1)
def mouseMoveEvent(self, event):
pos = self.mapToScene(event.pos())
self.width = pos.x() - self.xPos
self.height = pos.y() - self.yPos
if self.width < 0 and self.height < 0:
self.band.setGeometry(pos.x(), pos.y(), abs(self.width), abs(self.height))
elif self.width < 0:
self.band.setGeometry(pos.x(), self.yPos, abs(self.width), abs(self.height))
elif self.height < 0:
self.band.setGeometry(self.xPos, pos.y(), abs(self.width), abs(self.height))
else: self.band.setGeometry(self.xPos, self.yPos, abs(self.width), abs(self.height))
Here is the RubberBand code:
from typing import ItemsView
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QApplication, QHBoxLayout, QRubberBand, QWidget,QSizeGrip
from PyQt5.QtCore import QEasingCurve, Qt, QEvent
from PyQt5.QtGui import QCursor, QHoverEvent, QPixmap
import sys
# Functionality:
# - On initial mouse click
# Create new RubberBand, select area until mouse is released
# - Allow for resizing when selected
class SelectionBand(QWidget):
def __init__(self, parent=None):
super(SelectionBand, self).__init__()
# self.setMouseTracking=True
self.draggable = True
self.dragging_threshold = 5
self.borderRadius = 5
self.setWindowFlags(Qt.SubWindow)
layout = QHBoxLayout(self)
layout.setContentsMargins(0,0,0,0)
layout.addWidget(
QSizeGrip(self), 0,
Qt.AlignRight | Qt.AlignBottom)
self._band = QtWidgets.QRubberBand(
QtWidgets.QRubberBand.Rectangle, self)
self._band.show()
self.show()
def resizeEvent(self, event):
self._band.resize(self.size())
Since you're using the scene rect as a reference, you must check that the geometry of the selection is within the scene margins, and you should also consider the minimum size of the rubber band widget.
Also, instead of making complex computations to check if the width or height are "negative", you can create a QRectF using the two points (the clicked position and that received from the mouse movement), then use the normalized() function to get the same rectangle but with positive width and height.
class CustomGraphicsView(QGraphicsView):
def mousePressEvent(self, event):
self.startPos = self.mapToScene(event.pos())
if not self.band:
self.band = SelectionBand(self)
self.bandItem = self.scene().addWidget(self.band)
self.bandItem.setFlag(QGraphicsItem.ItemIsMovable)
self.bandItem.setZValue(1)
sceneRect = self.sceneRect()
if self.startPos.x() < sceneRect.x():
self.startPos.setX(sceneRect.x())
elif self.startPos.x() > sceneRect.right() - self.band.minimumWidth():
self.startPos.setX(sceneRect.right() - self.band.minimumWidth())
if self.startPos.y() < sceneRect.y():
self.startPos.setY(sceneRect.y())
elif self.startPos.y() > sceneRect.bottom() - self.band.minimumHeight():
self.startPos.setY(sceneRect.bottom() - self.band.minimumHeight())
self.bandItem.setPos(self.startPos)
def mouseMoveEvent(self, event):
mousePos = self.mapToScene(event.pos())
# create a normalized rectangle based on the two points
rect = QRectF(self.startPos, mousePos).normalized()
if rect.width() < self.band.minimumWidth():
rect.setWidth(self.band.minimumWidth())
if rect.x() < self.startPos.x():
rect.moveLeft(self.startPos.x())
if rect.height() < self.band.minimumHeight():
rect.setHeight(self.band.minimumHeight())
if rect.y() < self.startPos.y():
rect.moveBottom(self.startPos.y())
sceneRect = self.sceneRect()
if rect.x() < sceneRect.x():
rect.setX(sceneRect.x())
elif rect.x() > sceneRect.right() - self.band.minimumWidth():
rect.setX(sceneRect.right() - self.band.minimumWidth())
if rect.right() > sceneRect.right():
rect.setRight(sceneRect.right())
if rect.y() < sceneRect.y():
rect.setY(sceneRect.y())
elif rect.y() > sceneRect.bottom() - self.band.minimumHeight():
rect.setY(sceneRect.bottom() - self.band.minimumHeight())
if rect.bottom() > sceneRect.bottom():
rect.setBottom(sceneRect.bottom())
self.bandItem.setGeometry(rect)
Consider that using a QWidget for the selection is not a good idea (and your implementation is a bit flawed anyway): you're not using most of the features of a QWidget (except for the size grip, which is not used anyway), so using it is a bit pointless.
You could instead use a subclass of QGraphicsRectItem. This has many advantages: not only it is more coherent for its usage and for the graphics view framework, but, for instance, you can use a cosmetic pen for the border, which will always be clearly shown even when zooming out.
class SelectionBandItem(QGraphicsRectItem):
_minimumWidth = _minimumHeight = 1
def __init__(self):
super().__init__()
color = QApplication.palette().highlight().color()
pen = QPen(color, 1)
pen.setCosmetic(True)
self.setPen(pen)
color.setAlpha(128)
self.setBrush(color)
self.setFlag(QGraphicsItem.ItemIsMovable)
def minimumWidth(self):
return self._minimumWidth
def minimumHeight(self):
return self._minimumHeight
def setGeometry(self, rect):
# we cannot use setRect, as it will only set the drawn rectangle but
# not the position of the item, which makes it less intuitive; it's
# better to use the given rectangle to set the position and then
# resize the item's rectangle
self.setPos(rect.topLeft())
current = self.rect()
current.setSize(rect.size())
self.setRect(current)
def geometry(self):
return self.rect().translated(self.pos())
class CustomGraphicsView(QGraphicsView):
# ...
def mousePressEvent(self, event):
self.startPos = self.mapToScene(event.pos())
if not self.band:
self.band = SelectionBandItem()
self.scene().addItem(self.band)
sceneRect = self.sceneRect()
if self.startPos.x() < sceneRect.x():
self.startPos.setX(sceneRect.x())
elif self.startPos.x() > sceneRect.right():
self.startPos.setX(sceneRect.right())
if self.startPos.y() < sceneRect.y():
self.startPos.setY(sceneRect.y())
elif self.startPos.y() > sceneRect.bottom():
self.startPos.setY(sceneRect.bottom())
self.band.setPos(self.startPos)
def mouseMoveEvent(self, event):
# all of the above, except for this:
self.band.setGeometry(rect)
Then, if you want to add support for the size grip, implement that in the mouseMoveEvent of the SelectionBandItem class.
Unrelated notes:
self.width and self.height are properties of all Qt widgets, you should not overwrite them with other values, especially if those values are not related to self; also, since you're continuously changing those values in the mouseMoveEvent, there's really no point in making them instance attributes: if you need access to the size of the band from other functions, just use self.band.width() or self.band.height();
there's no point in dividing the angle delta by 120 if you're just checking if it's greater or less than 0;
new items are always added on top of the item stack at 0 z-level, so there's little use in setting it to 1;
setMouseTracking is a function and not a variable;
when adding new widgets to a scene, you should not call show in their __init__, as it causes them to show as a normal window for an instant;
the stretch argument of QBoxLayout.addWidget already defaults to zero, if you want to specify the alignment it's better to explicitly use the keyword; also, if you add a widget to a layout there's no need for the parent argument: layout.addWidget(QSizeGrip(), alignment=Qt.AlignRight|Qt.AlignBottom);
if you're adding the graphics view to a layout, calling its resize() is completely useless; the scope of a layout manager is to manage the layout by setting the position and size of all its items, so manually requesting any geometry change is conceptually wrong; while it can resize the widget, as soon as any of the parent or sibling is resized, the layout will override that request;

Create an image in a canvas inside a class [duplicate]

This question already has answers here:
Why does Tkinter image not show up if created in a function?
(5 answers)
Closed 1 year ago.
I wanted to create a chess program using OOP. So I made a superclass Pieces, a subclass Bishop, and a UI class GameUI. I created a canvas in the class GameUI. I wanted, that when I instantiate an object bishop in the class GameUI, it shows an Image from a bishop, on the canvas.
The problem is, when I instantiate the Bishop, I don't see any image. So I tried to do the same with a text : instead of using the method create_image from the class Canvas, I used the method create_text, and it worked : I saw a text on the canvas. That means, the problem comes from the method create_image, and I don't understand it.
If I create an Image directly in the class GameUi, it works! but that's not what I want...
So I don't have any error message. I see the canvas (with a blue background), but no image on it.
Here's the code :
from tkinter import PhotoImage, Tk, Canvas
class Pieces:
def __init__(self, can, color, x_position, y_position):
self.color = color
self.x_position = x_position
self.y_position = y_position
class Bishop(Pieces):
def __init__(self, can, color, x_position, y_position):
super().__init__(can, color, x_position, y_position)
if color == "black":
icon_path = 'black_bishop.png'
elif color == "white":
icon_path = 'white_bishop.png'
icon = PhotoImage(file=icon_path) # doesn't see the image
can.create_image(x, y, image=icon)
class GameUI:
def __init__(self):
self.windows = Tk()
self.windows.title("My chess game")
self.windows.geometry("1080x720")
self.windows.minsize(300, 420)
self.can = Canvas(self.windows, width=1000, height=600, bg='skyblue')
icon = PhotoImage(file=icon_path) # here I create the image in this class, and
can.create_image(x, y, image=icon) # we can see it very well
self.bishop = Bishop(self.can, "black", 50, 50)
self.can.pack()
self.windows.mainloop()
app = GameUI()
To make your code work, I decided to sort of rewrite it based on this answer. It works now, but really the only thing that you needed to add was self.icon instead of icon. icon gets garbage collected since there is no further reference to it, while self.icon remains. Also, it's not entirely the same as yours was, so it probably needs a bit of rewriting too.
from tkinter import *
from random import randint
class Piece:
def __init__(self, canvas, x1, y1):
self.x1 = x1
self.y1 = y1
self.canvas = canvas
class Bishop(Piece):
def __init__(self, canvas, x1, y1, color):
super().__init__(canvas, x1, y1)
if color == "black":
icon_path = 'black_bishop.png'
elif color == "white":
icon_path = 'white_bishop.png'
self.icon = PhotoImage(file=icon_path)
self.ball = canvas.create_image(self.x1, self.y1, image=self.icon)
def move_piece(self):
deltax = randint(0,5)
deltay = randint(0,5)
self.canvas.move(self.ball, deltax, deltay)
self.canvas.after(50, self.move_piece)
class GameUI:
def __init__(self):
# initialize root Window and canvas
root = Tk()
root.title("Chess")
root.resizable(False,False)
canvas = Canvas(root, width = 300, height = 300)
canvas.pack()
# create two ball objects and animate them
bishop1 = Bishop(canvas, 10, 10, 'white')
bishop2 = Bishop(canvas, 60, 60, 'black')
bishop1.move_piece()
bishop2.move_piece()
root.mainloop()
app = GameUI()

Print items to pdf

I have a window with a QGraphicsScene as painter, and i want to render its elements to a pdf file on press of a button.
def generateReport(self):
lineList = {}
for i in self.circleList:
for j,k in i.lineItems:
if j not in lineList:
lineList[j] = [i, k]
printed = QPdfWriter("Output.pdf")
printed.setPageSize(QPagedPaintDevice.A4)
printer = QPainter(printed)
self.painter.render(printer)
for i,j in enumerate(lineList):
# j.scene().render(printer)
# lineList[j][0].scene().render(printer)
# lineList[j][1].scene().render(printer)
printer.drawText(0, self.painter.height() + i*200, f'{j.nameItem.toPlainText()}: {lineList[j][0].m_items[4].toPlainText()}, {lineList[j][1].m_items[4].toPlainText()}')
printer.end()
nameItem on j is the name label for the line, m_items[4] is the name label for each circle.
My issue is that i cant seem to get the exact height of the rendered scene, moreover I have zero clue as to how i could overflow the text to the next page should the contents not fit in one.
it would be lovely if i could somehow render every line and its corresponding circles seperately for each connection, stored in lineList
note: the line is a child of every circle , and the names of every line and circle are children of theirs, implemented much in the same way as the answer to my previous question where in lies my final issue of the grip items also being rendered.
I have discovered that I can create a new scene, move each item one by one and render it out to the pdf but this raises two separate issues
I cant add a line break and avoid overdrawing the new render over the previous one, and
I cant position the text as addText doesnt take positional arguments.
MRE:
import random
from fbs_runtime.application_context.PyQt5 import ApplicationContext
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QPainter, QPdfWriter, QBrush, QPagedPaintDevice
from PyQt5.QtWidgets import (QDialog, QGraphicsScene,
QGraphicsView, QGridLayout,
QPushButton, QGraphicsEllipseItem)
class gui(QDialog):
def __init__(self, parent=None):
super(gui, self).__init__(parent)
self.resize(1280, 720)
self.painter = QGraphicsScene(0, 0, self.width() - 50, self.height() - 70)
self.painter.setBackgroundBrush(QBrush(Qt.white))
self.canvas = QGraphicsView(self.painter)
mainLayout = QGridLayout()
mainLayout.addWidget(self.canvas, 0, 0, -1, -1)
self.setLayout(mainLayout)
#property
def circleList(self):
return [item for item in self.painter.items() if isinstance(item, QGraphicsEllipseItem)]
def newCircle(self):
self.painter.addEllipse( random.randint(100, 400), random.randint(100, 400), 50 + random.random() * 200, 50 + random.random() * 200)
def generateReport(self):
printed = QPdfWriter("Output.pdf")
printed.setPageSize(QPagedPaintDevice.A4)
printer = QPainter(printed)
self.painter.render(printer)
for i,j in enumerate(self.circleList):
printer.drawText(0, printer.viewport().height() + i*200, 'test')
printer.end()
if __name__ == "__main__":
app = ApplicationContext()
test = gui()
test.newCircle()
test.newCircle()
test.newCircle()
test.generateReport()
test.show()
exit(app.app.exec_())
if possible , the ability to print, test then circle for all circles would be decent enough for me.
Incorrect output example:
To understand what painting is like, you have to understand how QGraphicsScene::render() method works:
void QGraphicsScene::render(QPainter *painter, const QRectF &target = QRectF(), const QRectF &source = QRectF(), Qt::AspectRatioMode aspectRatioMode = Qt::KeepAspectRatio)
Renders the source rect from scene into target, using painter. This
function is useful for capturing the contents of the scene onto a
paint device, such as a QImage (e.g., to take a screenshot), or for
printing with QPrinter. For example:
QGraphicsScene scene;
scene.addItem(...
...
QPrinter printer(QPrinter::HighResolution);
printer.setPaperSize(QPrinter::A4);
QPainter painter(&printer);
scene.render(&painter);
If source is a null rect, this function will use sceneRect() to
determine what to render. If target is a null rect, the dimensions of
painter's paint device will be used.
The source rect contents will be transformed according to
aspectRatioMode to fit into the target rect. By default, the aspect
ratio is kept, and source is scaled to fit in target.
See also QGraphicsView::render().
In your case, if the source is not passed, the entire sceneRect (0, 0, 1230, 650) will be copied and painted on the pdf page, if the sizes do not match, the sizes will be scaled. So from the above it follows that if you want to print an item then you must pass as source the space it occupies in the scene and hide the other items, and the target is the place where you want to paint, which involves calculating the new position based on where the previous item was printed.
Considering the above, a possible solution is the following:
import random
from PyQt5 import QtCore, QtGui, QtWidgets
class Gui(QtWidgets.QDialog):
def __init__(self, parent=None):
super(Gui, self).__init__(parent)
self.resize(1280, 720)
self.scene = QtWidgets.QGraphicsScene(
0, 0, self.width() - 50, self.height() - 70
)
self.scene.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.white))
self.canvas = QtWidgets.QGraphicsView(self.scene)
mainLayout = QtWidgets.QGridLayout(self)
mainLayout.addWidget(self.canvas)
#property
def circleList(self):
return [
item
for item in self.scene.items()
if isinstance(item, QtWidgets.QGraphicsEllipseItem)
]
def newCircle(self):
self.scene.addEllipse(
random.randint(100, 400),
random.randint(100, 400),
50 + random.random() * 200,
50 + random.random() * 200,
)
def generateReport(self):
printer = QtGui.QPdfWriter("Output.pdf")
printer.setPageSize(QtGui.QPagedPaintDevice.A4)
printer.setResolution(100)
painter = QtGui.QPainter(printer)
delta = 20
f = painter.font()
f.setPixelSize(delta)
painter.setFont(f)
# hide all items
last_states = []
for item in self.scene.items():
last_states.append(item.isVisible())
item.setVisible(False)
target = QtCore.QRectF(0, 0, printer.width(), 0)
for i, item in enumerate(self.circleList):
item.setVisible(True)
source = item.mapToScene(item.boundingRect()).boundingRect()
target.setHeight(source.height())
if target.bottom() > printer.height():
printer.newPage()
target.moveTop(0)
self.scene.render(painter, target, source)
f = painter.font()
f.setPixelSize(delta)
painter.drawText(
QtCore.QRectF(
target.bottomLeft(), QtCore.QSizeF(printer.width(), delta + 5)
),
"test",
)
item.setVisible(False)
target.setTop(target.bottom() + delta + 20)
# restore visibility
for item, state in zip(self.scene.items(), last_states):
item.setVisible(state)
painter.end()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Gui()
for _ in range(200):
w.newCircle()
w.generateReport()
w.show()
sys.exit(app.exec_())

Changing a rectangle's X value does not actually move it in the GUI

I'm working on a GUI in Python with PySide2. I have a GraphicsView, where I'll put an image, and I'd like to draw and move a polygon around on that image. I've found many examples of simply drawing polygons, circles, etc. in PySide, PySide2, or PyQt 4/5 in Python. However, I haven't been able to figure out why my graphics items do not move on an event without deleting and redrawing.
I'm using the keyboard to change the X value on a PySide2 QRectF. The X value is clearly changing, but the rectangle does not actually move.
Here is a minimal example:
from PySide2 import QtCore, QtGui, QtWidgets
from functools import partial
class DebuggingDrawing(QtWidgets.QGraphicsView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# initialize the scene and set the size
self._scene = QtWidgets.QGraphicsScene(self)
self._scene.setSceneRect(0,0,500,500)
self.setScene(self._scene)
# make a green pen and draw a 10 wide, 20 high rectangle at x=20, y=30
self.pen = QtGui.QPen(QtCore.Qt.green, 0)
self.draw_rect = QtCore.QRectF(20, 30, 10, 20)
# add the rectangle to our scene
self._scene.addRect(self.draw_rect, self.pen)
def move_rect(self, dx: int):
# method for moving the existing rectangle
# get the x value
x = self.draw_rect.x()
print('x: {} dx: {}'.format(x, dx))
# use the moveLeft method of QRectF to change the rectangle's left side x value
self.draw_rect.moveLeft(x + dx)
self.update()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.labelImg = DebuggingDrawing()
# Get a keyboard shortcut and hook it up to the move_rect method
next_shortcut = QtWidgets.QShortcut(QtGui.QKeySequence('Right'), self)
next_shortcut.activated.connect(partial(self.labelImg.move_rect, 1))
# get the left key shortcut, move_rect one pixel left
back_shortcut = QtWidgets.QShortcut(QtGui.QKeySequence('Left'), self)
back_shortcut.activated.connect(partial(self.labelImg.move_rect, -1))
self.setCentralWidget(self.labelImg)
self.setMaximumHeight(480)
self.update()
if __name__ == '__main__':
app = QtWidgets.QApplication([])
testing = MainWindow()
testing.show()
app.exec_()
Here's what the output looks like:
You clearly can't see in the image, but even though the rectangle's x value is changing according to our print calls, nothing moves around in the image. I've confirmed it's not just my eyes, because if I draw new rectangles in move_rect, they clearly show up.
draw_rect is a QRectF is an input to create an item(QGraphicsRectItem) that is returned by the addRect() method similar to pen, that is, it takes the information but then no longer uses it. The idea is to move the item using setPos():
class DebuggingDrawing(QtWidgets.QGraphicsView):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# initialize the scene and set the size
self._scene = QtWidgets.QGraphicsScene(self)
self._scene.setSceneRect(0, 0, 500, 500)
self.setScene(self._scene)
# make a green pen and draw a 10 wide, 20 high rectangle at x=20, y=30
pen = QtGui.QPen(QtCore.Qt.green, 0)
draw_rect = QtCore.QRectF(20, 30, 10, 20)
# add the rectangle to our scene
self.item_rect = self._scene.addRect(draw_rect, pen)
def move_rect(self, dx: int):
p = self.item_rect.pos()
p += QtCore.QPointF(dx, 0)
self.item_rect.setPos(p)
If you still want to use draw_rect then you have to set it again in the item:
self.pen = QtGui.QPen(QtCore.Qt.green, 0)
self.draw_rect = QtCore.QRectF(20, 30, 10, 20)
# add the rectangle to our scene
self.item_rect = self._scene.addRect(self.draw_rect, self.pen)
def move_rect(self, dx: int):
# method for moving the existing rectangle
# get the x value
x = self.draw_rect.x()
print('x: {} dx: {}'.format(x, dx))
# use the moveLeft method of QRectF to change the rectangle's left side x value
self.draw_rect.moveLeft(x + dx)
self.item_rect.setRect(self.draw_rect)
It is recommended that "Graphics View Framework" be read so that the QGraphicsItems, QGraphicsView and QGraphicsScene work.

QSplitter, QWidget resizing, setSizes(), setStretchFactor(), and sizeHint() - how to make it all work together?

I'm struggling with working out how to make all the stuff in the title work together in a certain situation. I'm using PyQt5 here, but feel free to respond with regular C++ Qt as I can translate pretty easily.
I'm attempting to make a UI with the following:
A main form (inherits from QWidget, could just as well use QMainWindow)
The main form should contain a QSplitter oriented vertically containing a QTextEdit at the top and containing a custom class (inheriting from QLabel) to show an image taking up the rest of the space.
The QTextEdit at the top should default to about 3 lines of text high, but this should be resizable to any reasonable extreme via the QSplitter.
The custom class should resize the image to be as big as possible given the available space while maintaining the aspect ratio.
Of course the tricky part is getting everything to resize correctly depending on how big a monitor the user has and how the move the form around. I need this to run on screens as small as about 1,000 px width and perhaps as big as 3,000+ px width.
Here is what I have so far:
# QSplitter3.py
import cv2
import numpy as np
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QSizePolicy, \
QFrame, QTabWidget, QTextEdit, QSplitter
from PyQt5.QtGui import QImage, QPixmap, QPainter
from PyQt5.Qt import Qt
from PyQt5.Qt import QPoint
def main():
app = QApplication([])
screenSize = app.primaryScreen().size()
print('screenSize = ' + str(screenSize.width()) + ', ' + str(screenSize.height()))
mainForm = MainForm(screenSize)
mainForm.show()
app.exec()
# end function
class MainForm(QWidget):
def __init__(self, screenSize):
super().__init__()
# set the title and size of the Qt QWidget window
self.setWindowTitle('Qt Window')
self.setGeometry(screenSize.width() * 0.2, screenSize.height() * 0.2,
screenSize.width() * 0.5 , screenSize.height() * 0.7)
# declare a QTextEdit to show user messages at the top, set the font size, height, and read only property
self.txtUserMessages = QTextEdit()
self.setFontSize(self.txtUserMessages, 14)
self.txtUserMessages.setReadOnly(True)
# make the min height of the text box about 2 lines of text high
self.txtUserMessages.setMinimumHeight(self.getTextEditHeightForNLines(self.txtUserMessages, 2))
# populate the user messages text box with some example text
self.txtUserMessages.append('message 1')
self.txtUserMessages.append('message 2')
self.txtUserMessages.append('message 3')
self.txtUserMessages.append('stuff here')
self.txtUserMessages.append('bla bla bla')
self.txtUserMessages.append('asdasdsadds')
# instantiate the custom ImageWidget class below to show the image
self.imageWidget = ImageWidget()
self.imageWidget.setMargin(0)
self.imageWidget.setContentsMargins(0, 0, 0, 0)
self.imageWidget.setScaledContents(True)
self.imageWidget.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
self.imageWidget.setAlignment(Qt.AlignCenter)
# declare the splitter, then add the user message text box and tab widget
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.txtUserMessages)
self.splitter.addWidget(self.imageWidget)
defaultTextEditHeight = self.getTextEditHeightForNLines(self.txtUserMessages, 3)
print('defaultTextEditHeight = ' + str(defaultTextEditHeight))
# How can I use defaultTextEditHeight height here, but still allow resizing ??
# I really don't like this line, the 1000 is a guess and check that may only work with one screen size !!!
self.splitter.setSizes([defaultTextEditHeight, 1000])
# Should setStretchFactor be used here ?? This does not seem to work
# self.splitter.setStretchFactor(0, 0)
# self.splitter.setStretchFactor(1, 1)
# What about sizeHint() ?? Should that be used here, and if so, how ??
# set the main form's layout to the QGridLayout
self.gridLayout = QGridLayout()
self.gridLayout.addWidget(self.splitter)
self.setLayout(self.gridLayout)
# open the two images in OpenCV format
self.openCvImage = cv2.imread('image.jpg')
if self.openCvImage is None:
print('error opening image')
return
# end if
# convert the OpenCV image to QImage
self.qtImage = openCvImageToQImage(self.openCvImage)
# show the QImage on the ImageWidget
self.imageWidget.setPixmap(QPixmap.fromImage(self.qtImage))
# end function
def setFontSize(self, widget, fontSize):
font = widget.font()
font.setPointSize(fontSize)
widget.setFont(font)
# end function
def getTextEditHeightForNLines(self, textEdit, numLines):
fontMetrics = textEdit.fontMetrics()
rowHeight = fontMetrics.lineSpacing()
rowHeight = rowHeight * 1.21
textEditHeight = int(numLines * rowHeight)
return textEditHeight
# end function
# end class
def openCvImageToQImage(openCvImage):
# get the height, width, and num channels of the OpenCV image, then compute the byte value
height, width, numChannels = openCvImage.shape
byteValue = numChannels * width
# make the QImage from the OpenCV image
qtImage = QImage(openCvImage.data, width, height, byteValue, QImage.Format_RGB888).rgbSwapped()
return qtImage
# end function
class ImageWidget(QLabel):
def __init__(self):
super(QLabel, self).__init__()
# end function
def setPixmap(self, pixmap):
self.pixmap = pixmap
# end function
def paintEvent(self, event):
size = self.size()
painter = QPainter(self)
point = QPoint(0, 0)
scaledPixmap = self.pixmap.scaled(size, Qt.KeepAspectRatio, transformMode=Qt.SmoothTransformation)
point.setX((size.width() - scaledPixmap.width()) / 2)
point.setY((size.height() - scaledPixmap.height()) / 2)
painter.drawPixmap(point, scaledPixmap)
# end function
# end class
if __name__ == '__main__':
main()
Currently I'm testing on a 2560x1440 screen and with the magic 1000 entered it works on this screen size, but I really don't like the hard-coded 1000. I suspect the area of the code where I'm missing something is this part:
# declare the splitter, then add the user message text box and tab widget
self.splitter = QSplitter(Qt.Vertical)
self.splitter.addWidget(self.txtUserMessages)
self.splitter.addWidget(self.imageWidget)
defaultTextEditHeight = self.getTextEditHeightForNLines(self.txtUserMessages, 3)
print('defaultTextEditHeight = ' + str(defaultTextEditHeight))
# How can I use defaultTextEditHeight height here, but still allow resizing ??
# I really don't like this line, the 1000 is a guess and check that may only work with one screen size !!!
self.splitter.setSizes([defaultTextEditHeight, 1000])
# Should setStretchFactor be used here ?? This does not seem to work
# self.splitter.setStretchFactor(0, 0)
# self.splitter.setStretchFactor(1, 1)
# What about sizeHint() ?? Should that be used here, and if so, how ??
# set the main form's layout to the QGridLayout
self.gridLayout = QGridLayout()
self.gridLayout.addWidget(self.splitter)
With the hard coded 1000 and on this particular screen it works pretty well:
To reiterate (hopefully more clearly) I'm attempting to be able to remove the hard-coded 1000 and command Qt as follows:
Initially make the form take up about 2/3 of the screen
Initially make the text box about 3 lines of text high (min of 2 lines of text high)
Allow the user to use the QSplitter to resize the text box and image at any time and without limit
When the form is resized (or minimized or maximized), resize the text box and image proportionally per how the user had them at the time of the resize
I've tried about every combination of the stuff mentioned in the title and so far in this post but I've not been able to get this functionality, except with the hard-coded 1000 that probably won't work with a different screen size.
How can I remove the hard-coded 1000 and modify the above to achieve the intended functionality?
In my solution I will not take into account the part of opencv since it adds unnecessary complexity.
The solution is to use the setStretchFactor() method, in this case override the sizeHint() method of the QTextEdit to set the initial size and setMinimumHeight() for the minimum height. To show the image I use a QGraphicsView instead of the QLabel since the logic is easier.
from PyQt5 import QtCore, QtGui, QtWidgets
class TextEdit(QtWidgets.QTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setReadOnly(True)
font = self.font()
font.setPointSize(14)
self.setFont(font)
self.setMinimumHeight(self.heightForLines(2))
def heightForLines(self, n):
return (
n * self.fontMetrics().lineSpacing() + 2 * self.document().documentMargin()
)
def showEvent(self, event):
self.verticalScrollBar().setValue(self.verticalScrollBar().minimum())
def sizeHint(self):
s = super().sizeHint()
s.setHeight(self.heightForLines(3))
return s
class GraphicsView(QtWidgets.QGraphicsView):
def __init__(self, parent=None):
super().__init__(parent)
self.setFrameShape(QtWidgets.QFrame.NoFrame)
self.setBackgroundBrush(self.palette().brush(QtGui.QPalette.Window))
scene = QtWidgets.QGraphicsScene(self)
self.setScene(scene)
self._pixmap_item = QtWidgets.QGraphicsPixmapItem()
scene.addItem(self._pixmap_item)
def setPixmap(self, pixmap):
self._pixmap_item.setPixmap(pixmap)
def resizeEvent(self, event):
self.fitInView(self._pixmap_item, QtCore.Qt.KeepAspectRatio)
self.centerOn(self._pixmap_item)
super().resizeEvent(event)
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.textedit = TextEdit()
for i in range(10):
self.textedit.append("Message {}".format(i))
self.graphicsview = GraphicsView()
self.graphicsview.setPixmap(QtGui.QPixmap("image.jpg"))
splitter = QtWidgets.QSplitter(QtCore.Qt.Vertical)
splitter.addWidget(self.textedit)
splitter.addWidget(self.graphicsview)
splitter.setStretchFactor(1, 1)
lay = QtWidgets.QGridLayout(self)
lay.addWidget(splitter)
screenSize = QtWidgets.QApplication.primaryScreen().size()
self.setGeometry(
screenSize.width() * 0.2,
screenSize.height() * 0.2,
screenSize.width() * 0.5,
screenSize.height() * 0.7,
)
def main():
app = QtWidgets.QApplication([])
w = Widget()
w.resize(640, 480)
w.show()
app.exec_()
if __name__ == "__main__":
main()

Categories

Resources