Painting on a widget that contains a QGridLayout in PySide/PyQt - python

I am making a custom QWidget in which I have a QGridLayout, and draw a rectangle on a particular element in the grid. I also manually draw lines to delineate the location of the grid elements (with QPainter.DrawLines).
After drawing the lines, I then paint the rectangle within one of the grid elements, with its location specified using the QGridLayout coordinate system .
The problem is, the rectangle does not stay confined to its grid element. For instance, in the example below, the blue rectangle and black grid lines get out of alignment, so I end up with a blue box floating around in space.
I have not found explicit discussion of this issue via Google or SO.
Edit:
Note as pointed out in the accepted answer, the mistake was using grid coordinates to draw on the grid, when I should have been using point coordinates (i.e., column, row). That is, the mistake in the code below is that the element in the grid has its x- and y- coordinates reversed.
from PySide import QtGui, QtCore
class HighlightSquare(QtGui.QWidget):
def __init__(self, parent = None):
QtGui.QWidget.__init__(self, parent=None)
self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Expanding))
self.setMinimumSize(self.minimumSizeHint())
layout = QtGui.QGridLayout()
layout.addItem(QtGui.QSpacerItem(10,10), 0, 0)
layout.addItem(QtGui.QSpacerItem(10,10), 0, 1)
layout.addItem(QtGui.QSpacerItem(10,10), 1, 0)
layout.addItem(QtGui.QSpacerItem(10,10), 1, 1)
self.setLayout(layout)
self.resize(150, 150)
self.update()
def paintEvent(self, event = None):
painter = QtGui.QPainter(self)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
winHeight=self.size().height(); heightStep=winHeight/2
winWidth=self.size().width(); widthStep=winWidth/2
#Draw lines
painter.setPen(QtCore.Qt.black)
for i in range(4):
#vertical lines
painter.drawLine(QtCore.QPoint(i*widthStep,0), QtCore.QPoint(i*widthStep, winHeight))
#horizontal lines
painter.drawLine(QtCore.QPoint(0,heightStep*i), QtCore.QPoint(winWidth, heightStep*i))
#Draw blue outline around box 1,1
highlightCoordinate=(1,1)
pen=QtGui.QPen(QtCore.Qt.blue, 3)
painter.setPen(pen)
coordHighlight=[QtCore.QPoint(highlightCoordinate[1]*heightStep, highlightCoordinate[0]*widthStep),\
QtCore.QPoint(highlightCoordinate[1]*heightStep, (highlightCoordinate[0]+1)*widthStep),\
QtCore.QPoint((highlightCoordinate[1]+1)*heightStep, (highlightCoordinate[0]+1)*widthStep),\
QtCore.QPoint((highlightCoordinate[1]+1)*heightStep, highlightCoordinate[0]*widthStep),\
QtCore.QPoint(highlightCoordinate[1]*heightStep, highlightCoordinate[0]*widthStep)]
#print coordHighlight
painter.drawPolyline(coordHighlight)
def minimumSizeHint(self):
return QtCore.QSize(120,120)
if __name__=="__main__":
import sys
app=QtGui.QApplication(sys.argv)
myLight = HighlightSquare()
myLight.show()
sys.exit(app.exec_())

Have you read the definition of the constructor of class QtCore.QPoint? At method QPoint.__init__ (self, int xpos, int ypos) your code is reversed (ypos, xpos). I fixed it.
import sys
from PyQt4 import QtGui, QtCore
class QHighlightSquareWidget (QtGui.QWidget):
def __init__ (self, parent = None):
QtGui.QWidget.__init__(self, parent = None)
self.setSizePolicy (
QtGui.QSizePolicy (
QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Expanding))
self.setMinimumSize(self.minimumSizeHint())
allQGridLayout = QtGui.QGridLayout()
allQGridLayout.addItem(QtGui.QSpacerItem(10,10), 0, 0)
allQGridLayout.addItem(QtGui.QSpacerItem(10,10), 0, 1)
allQGridLayout.addItem(QtGui.QSpacerItem(10,10), 1, 0)
allQGridLayout.addItem(QtGui.QSpacerItem(10,10), 1, 1)
self.setLayout(allQGridLayout)
self.resize(150, 150)
self.update()
def paintEvent (self, eventQPaintEvent):
myQPainter = QtGui.QPainter(self)
myQPainter.setRenderHint(QtGui.QPainter.Antialiasing)
winHeight = self.size().height()
heightStep = winHeight / 2
winWidth = self.size().width()
widthStep = winWidth / 2
myQPainter.setPen(QtCore.Qt.black)
for i in range(4):
myQPainter.drawLine(QtCore.QPoint(i * widthStep, 0 ), QtCore.QPoint(i * widthStep, winHeight ))
myQPainter.drawLine(QtCore.QPoint(0, heightStep * i), QtCore.QPoint(winWidth, heightStep * i))
highlightCoordinate = (1, 1)
myQPen = QtGui.QPen(QtCore.Qt.blue, 3)
myQPainter.setPen(myQPen)
coordHighlight = [
QtCore.QPoint( highlightCoordinate[0] * widthStep, highlightCoordinate[1] * heightStep),
QtCore.QPoint((highlightCoordinate[0] + 1) * widthStep, highlightCoordinate[1] * heightStep),
QtCore.QPoint((highlightCoordinate[0] + 1) * widthStep, (highlightCoordinate[1] + 1) * heightStep),
QtCore.QPoint( highlightCoordinate[0] * widthStep, (highlightCoordinate[1] + 1) * heightStep),
QtCore.QPoint( highlightCoordinate[0] * widthStep, highlightCoordinate[1] * heightStep)]
myQPainter.drawPolyline(*coordHighlight)
def minimumSizeHint (self):
return QtCore.QSize(120, 120)
if __name__=="__main__":
myQApplication = QtGui.QApplication(sys.argv)
myQHighlightSquareWidget = QHighlightSquareWidget()
myQHighlightSquareWidget.show()
sys.exit(myQApplication.exec_())

Related

Catch events with QRectF

I have a QGraphicsScene where I add QRectF objects anchored to QWidget objects in order to move them. I'd need to capture events or signals from the QRectF but the mousePressEvent method never runs.
These objects have a sort of balance and it would be hard to replace the QRectF with a QRect or a QGraphicsRectItem, because drawing the base rect in the scene only that class is accepted.
I also tried to implement the mousePressEvent method is GraphicBlock class (which is a QWidget) but nothing happens.
This is my QRectF
class BlockRect(QRectF):
def __init__(self, x, y, dim1, dim2, block_type):
super(QRectF, self).__init__(x, y, dim1, dim2)
self.block_type = block_type
def contains(self, point):
if self.x() + self.width() \
> point.x() > self.x() - self.width()/2:
if self.y() + self.height() \
> point.y() > self.y() - self.height()/2:
return True
return False
# Never called
def mousePressEvent(self, event):
print("click!")
dialog = MyDialog(self.block_type)
dialog.exec()
super(BlockRect, self).mouseDoubleClickEvent(event)
And this is the method where I draw it:
def draw_block(self, block_type):
"""Drawing a graphic clock with its properties"""
# Setting the visible scene
viewport_rect = QRect(0, 0, self.view.viewport().width(),
self.view.viewport().height())
viewport = self.view.mapToScene(viewport_rect).boundingRect()
start_x = viewport.x()
start_y = viewport.y()
# The initial point of each block is translated of 20px in order not to
# overlap them (always in the visible area)
point = QPoint(start_x + 20*(self.numBlocks % 20) + 20,
start_y + 20*(self.numBlocks % 20) + 20)
transparent = QColor(0, 0, 0, 0)
# Creation of the graphic block
block = GraphicBlock(self.numBlocks, block_type, 0, self.scene)
# Positioning the block
proxy = self.scene.addWidget(block)
proxy.setPos(point.x(), point.y())
# Creation of the rect that will be parent of the QWidget GraphicBlock
# in order to move it in the QGraphicsScene
rect = BlockRect(point.x() + 10, point.y() + 10,
block.width() - 20, block.height() - 20,
block_type)
# The rect is added to the scene and becomes the block's parent
proxy_control = self.scene.addRect(rect, QPen(transparent), QBrush(transparent))
proxy_control.setFlag(QGraphicsItem.ItemIsMovable, True)
proxy_control.setFlag(QGraphicsItem.ItemIsSelectable, True)
proxy.setParentItem(proxy_control)
block.set_rect(rect)
self.blocks[self.numBlocks] = block
self.numBlocks += 1
self.update()
I really don't know or understand how i could capture events in some way.
Here it is my QWidget class, i.e. GraphicBlock, which do have event methods but doesn't execute them. I think I should control events from the QGraphicsScene object.
class GraphicBlock(QWidget):
"""QWidget that carries both graphical and logical information about the
layer node
"""
def __init__(self, block_id, block_type, block_data, scene):
super(GraphicBlock, self).__init__()
self.block_id = block_id
self.block_type = block_type
self.block_data = block_data # Just to try
self.scene = scene
self.rect = None
# Setting style and transparent background for the rounded corners
self.setAttribute(Qt.WA_TranslucentBackground)
self.setStyleSheet(GRAPHIC_BLOCK_STYLE)
# Block title label
type_label = QLabel(block_type.name)
type_label.setStyleSheet(BLOCK_TITLE_STYLE)
# Main vertical layout: it contains the label title and grid
layout = QVBoxLayout()
layout.setSpacing(0)
layout.addWidget(type_label)
self.setLayout(layout)
if block_type.parameters:
# Creating the grid for parameters
grid = QWidget()
grid_layout = QGridLayout()
grid.setLayout(grid_layout)
layout.addWidget(grid)
# Iterating and displaying parameters
par_labels = dict()
count = 1
for par in block_type.parameters:
par_labels[par] = QLabel(par)
par_labels[par].setAlignment(Qt.AlignLeft)
par_labels[par].setStyleSheet(PAR_BLOCK_STYLE)
dim = QLabel("<dim>")
dim.setAlignment(Qt.AlignRight)
dim.setStyleSheet(DIM_BLOCK_STYLE)
grid_layout.addWidget(par_labels[par], count, 1)
grid_layout.addWidget(dim, count, 0)
count += 1
else:
type_label.setStyleSheet(ZERO_PARS_BLOCK_TITLE)
def set_rect(self, rect):
self.rect = rect

Different colours in an arc

Consider the following toy example:
from PyQt5 import QtWidgets, QtGui, QtCore
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
w = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
w.setLayout(layout)
self.setCentralWidget(w)
label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(400, 300)
label.setPixmap(canvas)
layout.addWidget(label)
def paintEvent():
painter = QtGui.QPainter(label.pixmap())
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.setPen(QtCore.Qt.red)
painter.drawArc(0, 0, 100, 100, 1440, -2880)
painter.end()
paintEvent()
self.show()
app = QtWidgets.QApplication([])
window = MainWindow()
app.exec_()
How can I paint the arc using an arbitrary number of colours ideally of varying lengths?
I tried to do it with gradients (linear and conical) but I have been unable to obtain accurate results.
I suppose the broader question is can I somehow have different pen colours when painting an arc? Note that the arc can be a half circle, a full circle or anything in between.
The colours are to be distributed using percentages. Each colour is a fraction of the arc's length. But I am content with a solution where all colours are equally spaced.
A possible solution is to paint the arc in parts:
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
w = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
w.setLayout(layout)
self.setCentralWidget(w)
label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(400, 300)
canvas.fill(QtGui.QColor("white"))
label.setPixmap(canvas)
layout.addWidget(label)
def paint_label():
painter = QtGui.QPainter(label.pixmap())
painter.setRenderHint(QtGui.QPainter.Antialiasing)
r = QtCore.QRect(0, 0, 100, 100)
delta_angle = -180 * 16
start_angle = 90 * 16
values = (1, 2, 3, 4)
colors = (
QtGui.QColor("red"),
QtGui.QColor("blue"),
QtGui.QColor("green"),
QtGui.QColor("yellow"),
)
sum_of_values = sum(values)
for value, color in zip(values, colors):
end_angle = start_angle + int((value/sum_of_values) * delta_angle)
painter.setPen(color)
painter.drawArc(r, start_angle, end_angle - start_angle)
start_angle = end_angle
painter.end()
paint_label()
self.show()
def main():
app = QtWidgets.QApplication([])
window = MainWindow()
app.exec_()
if __name__ == "__main__":
main()
The solution provided by eyllanesc is perfectly fine, but I wanted to show the possibility of achieving the same result using a conical gradient instead of drawing single arcs.
Since we want actual arcs to be drawn, the trick is to use "ranges" of colors with very narrow margins.
For example, to get a conical gradient that is half red and half blue, we'll use something like this:
gradient.setColorAt(.5, QtCore.Qt.red)
# set the next color with a stop very close to the previous
gradient.setColorAt(.500001, QtCore.Qt.blue)
I prepared an example with a small interface to test its possibilities out.
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
w = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
w.setLayout(layout)
self.setCentralWidget(w)
panelLayout = QtWidgets.QHBoxLayout()
layout.addLayout(panelLayout)
panelLayout.addWidget(QtWidgets.QLabel('Start'))
self.startSpin = QtWidgets.QSpinBox(maximum=360, suffix='°')
self.startSpin.setValue(90)
panelLayout.addWidget(self.startSpin)
panelLayout.addWidget(QtWidgets.QLabel('Extent'))
self.extentSpin = QtWidgets.QSpinBox(maximum=360, suffix='°')
self.extentSpin.setValue(180)
panelLayout.addWidget(self.extentSpin)
panelLayout.addWidget(QtWidgets.QLabel('Width'))
self.penSpin = QtWidgets.QSpinBox(minimum=1, maximum=20, suffix='px')
self.penSpin.setValue(3)
panelLayout.addWidget(self.penSpin)
self.startSpin.valueChanged.connect(self.updateCanvas)
self.extentSpin.valueChanged.connect(self.updateCanvas)
self.penSpin.valueChanged.connect(self.updateCanvas)
self.colors = []
self.colorSpins = []
colorLayout = QtWidgets.QHBoxLayout()
layout.addLayout(colorLayout)
for color in ('red', 'green', 'blue', 'yellow'):
colorLayout.addWidget(QtWidgets.QLabel(color))
self.colors.append(QtGui.QColor(color))
colorSpin = QtWidgets.QSpinBox(minimum=1, maximum=50, value=25)
colorLayout.addWidget(colorSpin)
colorSpin.valueChanged.connect(self.updateCanvas)
self.colorSpins.append(colorSpin)
self.label = QtWidgets.QLabel()
canvas = QtGui.QPixmap(400, 300)
self.label.setPixmap(canvas)
layout.addWidget(self.label)
self.updateCanvas()
self.show()
def updateCanvas(self):
pm = QtGui.QPixmap(self.label.pixmap().size())
pm.fill(QtCore.Qt.transparent)
painter = QtGui.QPainter(pm)
painter.setRenderHint(QtGui.QPainter.Antialiasing)
painter.translate(.5, .5)
sizes = [spin.value() for spin in self.colorSpins]
total = sum(sizes)
extent = self.extentSpin.value() / 360
grad = QtGui.QConicalGradient(50, 50, self.startSpin.value())
gradPos = 1
# set colors starting from stop 1.0 to (1.0 - extent), since
# conical gradients are always counter-clockwise and the actual arc
# is negative, so it is drawn clockwise
for i, (size, color) in enumerate(zip(sizes, self.colors)):
grad.setColorAt(gradPos, color)
gradPos -= size / total * extent
if i < len(self.colors) - 1:
# extend the color right next to the next value
grad.setColorAt(gradPos + .000001, color)
if extent != 1:
# ensure that the first color is not painted at the edget of the
# last due to antialiasing
grad.setColorAt(0, self.colors[0])
grad.setColorAt(1 - extent, self.colors[-1])
offset = self.penSpin.maximum()
pen = QtGui.QPen(grad, self.penSpin.value(), cap=QtCore.Qt.FlatCap)
painter.setPen(pen)
# move the brush origin so that the conical gradient correctly centered
# in the middle of the ellipse
painter.setBrushOrigin(offset, offset)
painter.drawArc(offset, offset, 100, 100, self.startSpin.value() * 16, -self.extentSpin.value() * 16)
painter.end()
self.label.setPixmap(pm)

Draw a correct grid with PyQt5

I'm struggling a bit with PyQt5: I have to implement Conway's Game of Life and I started out with the GUI general setup. I thought about stacking (vertically) two widgets, one aimed at displaying the game board and another one containing the buttons and sliders.
This is what I came up with (I'm a total noob)
I'd like to fit the grid correctly with respect to the edges. It looks like it builds the grid underneath the dedicated canvas: it would be great to fix the canvas first and then paint on it but this whole thing of layouts, widgets and all that blows my mind.
This is my (fastly and poorly written) code
import sys
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QLabel, QSlider, QPushButton, QWidget
from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QPixmap, QColor, QPainter
WINDOW_WIDTH, WINDOW_HEIGHT = 800, 600
SQUARE_SIDE = 20
ROWS, COLS = int(WINDOW_HEIGHT/SQUARE_SIDE), int(WINDOW_WIDTH/2*SQUARE_SIDE)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
buttons_layout = QHBoxLayout()
self.label = QLabel()
self.label.setContentsMargins(0,0,0,0)
self.label.setStyleSheet('background-color: white; ')
self.label.setAlignment(Qt.AlignCenter)
slider = QSlider(Qt.Horizontal)
start_button = QPushButton('Start')
pause_button = QPushButton('Pause')
reset_button = QPushButton('Reset')
load_button = QPushButton('Load')
save_button = QPushButton('Save')
layout.addWidget(self.label)
buttons_layout.addWidget(start_button)
buttons_layout.addWidget(pause_button)
buttons_layout.addWidget(reset_button)
buttons_layout.addWidget(load_button)
buttons_layout.addWidget(save_button)
buttons_layout.addWidget(slider)
layout.addLayout(buttons_layout)
widget = QWidget()
widget.setLayout(layout)
self.setCentralWidget(widget)
self.make_grid()
def make_grid(self):
_canvas = QPixmap(WINDOW_WIDTH, WINDOW_HEIGHT)
_canvas.fill(QColor("#ffffff"))
self.label.setPixmap(_canvas)
painter = QPainter(self.label.pixmap())
for c in range(COLS):
painter.drawLine(SQUARE_SIDE*c, WINDOW_HEIGHT, SQUARE_SIDE*c, 0)
for r in range(ROWS):
painter.drawLine(0, SQUARE_SIDE*r, WINDOW_WIDTH, SQUARE_SIDE*r)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.setFixedSize(WINDOW_WIDTH, WINDOW_HEIGHT)
window.setWindowTitle("Conway's Game of Life")
window.show()
app.exec_()
Thank you for your help, have a nice day!
The reason for the pixmap not being show at its full size is because you're using WINDOW_WIDTH and WINDOW_HEIGHT for both the window and the pixmap. Since the window also contains the toolbar and its own margins, you're forcing it to be smaller than it should, hence the "clipping out" of the board.
The simpler solution would be to set the scaledContents property of the label:
self.label.setScaledContents(True)
But the result would be a bit ugly, as the label will have a size slightly smaller than the pixmap you drawn upon, making it blurry.
Another (and better) possibility would be to set the fixed size after the window has been shown, so that Qt will take care of the required size of all objects:
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
# window.setFixedSize(WINDOW_WIDTH, WINDOW_HEIGHT)
window.setWindowTitle("Conway's Game of Life")
window.show()
window.setFixedSize(window.size())
app.exec_()
Even if it's not part of your question, I'm going to suggest you a slightly different concept, that doesn't involve a QLabel.
With your approach, you'll face two possibilities:
continuous repainting of the whole QPixmap: you cannot easily "clear" something from an already painted surface, and if you'll have objects that move or disappear, you will need that
adding custom widgets that will have to be manually moved (and computing their position relative to the pixmap will be a serious PITA)
A better solution would be to avoid at all the QLabel, and implement your own widget with custom painting.
Here's a simple example:
class Grid(QWidget):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setMinimumSize(800, 600)
self.columns = 40
self.rows = 30
# some random objects
self.objects = [
(10, 20),
(11, 21),
(12, 20),
(12, 22),
]
def resizeEvent(self, event):
# compute the square size based on the aspect ratio, assuming that the
# column and row numbers are fixed
reference = self.width() * self.rows / self.columns
if reference > self.height():
# the window is larger than the aspect ratio
# use the height as a reference (minus 1 pixel)
self.squareSize = (self.height() - 1) / self.rows
else:
# the opposite
self.squareSize = (self.width() - 1) / self.columns
def paintEvent(self, event):
qp = QPainter(self)
# translate the painter by half a pixel to ensure correct line painting
qp.translate(.5, .5)
qp.setRenderHints(qp.Antialiasing)
width = self.squareSize * self.columns
height = self.squareSize * self.rows
# center the grid
left = (self.width() - width) / 2
top = (self.height() - height) / 2
y = top
# we need to add 1 to draw the topmost right/bottom lines too
for row in range(self.rows + 1):
qp.drawLine(left, y, left + width, y)
y += self.squareSize
x = left
for column in range(self.columns + 1):
qp.drawLine(x, top, x, top + height)
x += self.squareSize
# create a smaller rectangle
objectSize = self.squareSize * .8
margin = self.squareSize* .1
objectRect = QRectF(margin, margin, objectSize, objectSize)
qp.setBrush(Qt.blue)
for col, row in self.objects:
qp.drawEllipse(objectRect.translated(
left + col * self.squareSize, top + row * self.squareSize))
Now you don't need make_grid anymore, and you can use Grid instead of the QLabel.
Note that I removed one pixel to compute the square size, otherwise the last row/column lines won't be shown, as happened in your pixmap (consider that in a 20x20 sided square, a 20px line starting from 0.5 would be clipped at pixel 19.5).

How to resize square children widgets after parent resize in Qt5?

I want to do board with square widgets. When I run code it creates nice board but after resize it become looks ugly. I am trying resize it with resize Event but it exists (probably some errors). I have no idea how to resize children after resize of parent.
Children widgets must be squares so it is also problem since I can not use auto expand. Maybe it is simple problem but I can not find solution. I spend hours testing different ideas but it now works as it should.
This what I want resize (click maximize):
After maximize it looks ugly (I should change children widget but on what event (I think on resizeEvent but it is not works) and how (set from parent or children cause program exit).
This is my minimize code:
import logging
import sys
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QFont, QPaintEvent, QPainter
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout
class Application(QApplication):
pass
class Board(QWidget):
def square_size(self):
size = self.size()
min_size = min(size.height(), size.width())
min_size_1_8 = min_size // 8
square_size = QSize(min_size_1_8, min_size_1_8)
logging.debug(square_size)
return square_size
def __init__(self, parent=None):
super().__init__(parent=parent)
square_size = self.square_size()
grid = QGridLayout()
grid.setSpacing(0)
squares = []
for x in range(8):
for y in range(8):
square = Square(self, (x + y - 1) % 2)
squares.append(squares)
square.setFixedSize(square_size)
grid.addWidget(square, x, y)
self.squares = squares
self.setLayout(grid)
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
# how to resize children?
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
class Square(QWidget):
def __init__(self, parent, color):
super().__init__(parent=parent)
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
def paintEvent(self, event: QPaintEvent) -> None:
painter = QPainter()
painter.begin(self)
painter.fillRect(self.rect(), self.color)
painter.end()
def main():
logging.basicConfig(level=logging.DEBUG)
app = Application(sys.argv)
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
default_font = QFont()
default_font.setPointSize(12)
app.setFont(default_font)
board = Board()
board.setWindowTitle('Board')
# ugly look
# chessboard.showMaximized()
# looks nize but resize not works
board.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
How should I do resize of square children to avoid holes?
2nd try - improved code but still I have not idea how to resize children
Some new idea with centering it works better (no gaps now) but still I do not know how to resize children (without crash).
After show():
Too wide (it keeps proportions):
Too tall (it keeps proportions):
Larger (it keeps proportions but children is not scaled to free space - I do not know how to resize children still?):
Improved code:
import logging
import sys
from PyQt5 import QtCore, QtGui
from PyQt5.QtCore import QSize
from PyQt5.QtGui import QFont, QPaintEvent, QPainter
from PyQt5.QtWidgets import QApplication, QWidget, QGridLayout, QHBoxLayout, QVBoxLayout
class Application(QApplication):
pass
class Board(QWidget):
def square_size(self):
size = self.size()
min_size = min(size.height(), size.width())
min_size_1_8 = min_size // 8
square_size = QSize(min_size_1_8, min_size_1_8)
logging.debug(square_size)
return square_size
def __init__(self, parent=None):
super().__init__(parent=parent)
square_size = self.square_size()
vertical = QVBoxLayout()
horizontal = QHBoxLayout()
grid = QGridLayout()
grid.setSpacing(0)
squares = []
for x in range(8):
for y in range(8):
square = Square(self, (x + y - 1) % 2)
squares.append(squares)
square.setFixedSize(square_size)
grid.addWidget(square, x, y)
self.squares = squares
horizontal.addStretch()
horizontal.addLayout(grid)
horizontal.addStretch()
vertical.addStretch()
vertical.addLayout(horizontal)
vertical.addStretch()
self.setLayout(vertical)
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
# how to resize children?
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
class Square(QWidget):
def __init__(self, parent, color):
super().__init__(parent=parent)
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
def resizeEvent(self, event: QtGui.QResizeEvent) -> None:
logging.debug('Resize %s.', self.__class__.__name__)
logging.debug('Size %s.', event.size())
super().resizeEvent(event)
def paintEvent(self, event: QPaintEvent) -> None:
painter = QPainter()
painter.begin(self)
painter.fillRect(self.rect(), self.color)
painter.end()
def main():
logging.basicConfig(level=logging.DEBUG)
app = Application(sys.argv)
app.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
default_font = QFont()
default_font.setPointSize(12)
app.setFont(default_font)
board = Board()
board.setWindowTitle('Board')
# ugly look
# chessboard.showMaximized()
# looks nice but resize not works
board.show()
sys.exit(app.exec())
if __name__ == '__main__':
main()
How should I resize square children without crash?
There are two possible solution.
You can use the Graphics View framework, which is intended exactly for this kind of applications where custom/specific graphics and positioning have to be taken into account, otherwise create a layout subclass.
While reimplementing a layout is slightly simple in this case, you might face some issues as soon as the application becomes more complex. On the other hand, the Graphics View framework has a steep learning curve, as you'll need to understand how it works and how object interaction behaves.
Subclass the layout
Assuming that the square count is always the same, you can reimplement your own layout that will set the correct geometry based on its contents.
In this example I also created a "container" with other widgets to show the resizing in action.
When the window width is very high, it will use the height as a reference and center it horizontally:
On the contrary, when the height is bigger, it will be centered vertically:
Keep in mind that you should not add other widgets to the board, otherwise you'll get into serious issues.
This would not be impossible, but its implementation might be much more complex, as the layout would need to take into account the other widgets positions, size hints and possible expanding directions in order to correctly compute the new geometry.
from PyQt5 import QtCore, QtGui, QtWidgets
class Square(QtWidgets.QWidget):
def __init__(self, parent, color):
super().__init__(parent=parent)
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
self.setMinimumSize(50, 50)
def paintEvent(self, event: QtGui.QPaintEvent) -> None:
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), self.color)
class EvenLayout(QtWidgets.QGridLayout):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setSpacing(0)
def setGeometry(self, oldRect):
# assuming that the minimum size is 50 pixel, find the minimum possible
# "extent" based on the geometry provided
minSize = max(50 * 8, min(oldRect.width(), oldRect.height()))
# create a new squared rectangle based on that size
newRect = QtCore.QRect(0, 0, minSize, minSize)
# move it to the center of the old one
newRect.moveCenter(oldRect.center())
super().setGeometry(newRect)
class Board(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
layout = EvenLayout(self)
self.squares = []
for row in range(8):
for column in range(8):
square = Square(self, not (row + column) & 1)
self.squares.append(square)
layout.addWidget(square, row, column)
class Chess(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
header = QtWidgets.QLabel('Some {}long label'.format('very ' * 20))
layout.addWidget(header, 0, 0, 1, 3, QtCore.Qt.AlignCenter)
self.board = Board()
layout.addWidget(self.board, 1, 1)
leftLayout = QtWidgets.QVBoxLayout()
layout.addLayout(leftLayout, 1, 0)
rightLayout = QtWidgets.QVBoxLayout()
layout.addLayout(rightLayout, 1, 2)
for b in range(1, 9):
leftLayout.addWidget(QtWidgets.QPushButton('Left Btn {}'.format(b)))
rightLayout.addWidget(QtWidgets.QPushButton('Right Btn {}'.format(b)))
footer = QtWidgets.QLabel('Another {}long label'.format('very ' * 18))
layout.addWidget(footer, 2, 0, 1, 3, QtCore.Qt.AlignCenter)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Chess()
w.show()
sys.exit(app.exec_())
Using the Graphics View
The result will be visually identical to the previous one, but while the overall positioning, drawing and interaction would be conceptually a bit easier, understanding how Graphics Views, Scenes and objects work might require you some time to get the hang of it.
from PyQt5 import QtCore, QtGui, QtWidgets
class Square(QtWidgets.QGraphicsWidget):
def __init__(self, color):
super().__init__()
if color:
self.color = QtCore.Qt.white
else:
self.color = QtCore.Qt.black
def paint(self, qp, option, widget):
qp.fillRect(option.rect, self.color)
class Scene(QtWidgets.QGraphicsScene):
def __init__(self):
super().__init__()
self.container = QtWidgets.QGraphicsWidget()
layout = QtWidgets.QGraphicsGridLayout(self.container)
layout.setSpacing(0)
self.container.setContentsMargins(0, 0, 0, 0)
layout.setContentsMargins(0, 0, 0, 0)
self.addItem(self.container)
for row in range(8):
for column in range(8):
square = Square(not (row + column) & 1)
layout.addItem(square, row, column, 1, 1)
class Board(QtWidgets.QGraphicsView):
def __init__(self):
super().__init__()
scene = Scene()
self.setScene(scene)
self.setAlignment(QtCore.Qt.AlignCenter)
# by default a graphics view has a border frame, disable it
self.setFrameShape(0)
# make it transparent
self.setStyleSheet('QGraphicsView {background: transparent;}')
def resizeEvent(self, event):
super().resizeEvent(event)
# zoom the contents keeping the ratio
self.fitInView(self.scene().container, QtCore.Qt.KeepAspectRatio)
class Chess(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
header = QtWidgets.QLabel('Some {}long label'.format('very ' * 20))
layout.addWidget(header, 0, 0, 1, 3, QtCore.Qt.AlignCenter)
self.board = Board()
layout.addWidget(self.board, 1, 1)
leftLayout = QtWidgets.QVBoxLayout()
layout.addLayout(leftLayout, 1, 0)
rightLayout = QtWidgets.QVBoxLayout()
layout.addLayout(rightLayout, 1, 2)
for b in range(1, 9):
leftLayout.addWidget(QtWidgets.QPushButton('Left Btn {}'.format(b)))
rightLayout.addWidget(QtWidgets.QPushButton('Right Btn {}'.format(b)))
footer = QtWidgets.QLabel('Another {}long label'.format('very ' * 18))
layout.addWidget(footer, 2, 0, 1, 3, QtCore.Qt.AlignCenter)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Chess()
w.show()
sys.exit(app.exec_())

paint method doesn't paint whole widget when resized

I have a PyQt window built in Qt Designer and I am writing a custom paint method. The main window creates a label and sets it as the central widget. Then I override the paint method to draw a simple column chart.
The widget works well until it is resized. The widget calls the resize method and repaints as expected, but it uses the same size rectangle as before it was resized. There's a big black area -- the resized part -- that's not being painted on.
To test this out I grab the rectangle of the widget and draw a big rectangle with a light blue fill and red line outside. When the window is resized part of the outer rectangle is missing too.
Debugging statements show that the new rectangle is the correct size and the width and height values are fed properly into the paint event.
But when I resize, this is what I see. Why is paint not painting in the black area? I've checked my code and there are no hard coded limits on the paint. Is there some hidden clipping that occurs?
I couldn't find any questions with exactly this problem, so it would seem I'm missing something. This similar question says to invalidate the window before repaint, but that's for C++:
Graphics.DrawImage Doesn't Always Paint The Whole Bitmap?
Do I need to invalidate the widget somehow? I couldn't find the PyQt method to do that.
import sys
from PyQt5 import QtCore, QtGui, QtWidgets, uic
import PyQt5 as qt
import numpy as np
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel()
self.max_x = 600
self.max_y = 400
canvas = QtGui.QPixmap(self.max_x, self.max_y)
self.label.setPixmap(canvas)
self.setCentralWidget(self.label)
np.random.seed(777)
self.x_time = np.linspace(0, 12.56, 3000)
rand_data = np.random.uniform(0.0, 1.0, 3000)
self.data = np.sin(self.x_time) + rand_data
pal = self.palette()
pal.setColor(self.backgroundRole(), QtGui.QColor('black'))
self.setPalette(pal)
self.setAutoFillBackground(True)
def resizeEvent(self, a0: QtGui.QResizeEvent):
print("resizeEvent")
max_x = self.size().width()
max_y = self.size().height()
self.draw(max_x, max_y)
def mousePressEvent(self, a0: QtGui.QMouseEvent):
print("mousePressEvent")
def paintEvent(self, a0: QtGui.QPaintEvent):
print("New window size = ", self.size())
print("canvas size = ", self.label.size())
max_x = self.label.size().width()
max_y = self.label.size().height()
self.draw(max_x, max_y)
def draw(self, max_x, max_y):
x_final = self.x_time[-1]
data = self.data/np.max(np.abs(self.data))
data = [abs(int(k*max_y)) for k in self.data]
x_pos_all = [int(self.x_time[i]*max_x / x_final) for i in range(len(data))]
# Find and use only the max y value for each x pixel location
y_pos = []
x_pos = list(range(max_x))
cnt = 0
for x_pixel in range(max_x):
mx = 0.0
v = x_pos_all[cnt]
while cnt < len(x_pos_all) and x_pos_all[cnt] == x_pixel:
if data[cnt] > mx:
mx = data[cnt]
cnt += 1
y_pos.append(mx)
print("data = ")
dat = np.array(data)
print(dat[dat > 0].shape[0])
painter = QtGui.QPainter(self.label.pixmap()) # takes care of painter.begin(self)
pen = QtGui.QPen()
rect = self.label.rect()
print("rect = {}".format(rect))
painter.fillRect(rect, QtGui.QColor('lightblue'))
pen.setWidth(2)
pen.setColor(QtGui.QColor('green'))
for i in range(len(x_pos)):
painter.setPen(pen)
painter.drawLine(x_pos[i], max_y, x_pos[i], max_y - y_pos[i])
pen.setWidth(5)
pen.setColor(QtGui.QColor('red'))
painter.setPen(pen)
painter.drawRect(rect)
painter.end()
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
I expect that as the widget is resized, the paint event will repaint over the entire new size of the widget, not just the original size. Curiously, the graph part (green lines) looks like it is scaling as I resize, but everything's just cut off at the original widget size.
How do I fix this?
If you are using a QLabel then it is not necessary to override paintEvent since it is enough to create a new QPixmap and set it in the QLabel.
import sys
import numpy as np
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = QtWidgets.QLabel()
self.setCentralWidget(self.label)
np.random.seed(777)
self.x_time = np.linspace(0, 12.56, 3000)
rand_data = np.random.uniform(0.0, 1.0, 3000)
self.data = np.sin(self.x_time) + rand_data
pal = self.palette()
pal.setColor(self.backgroundRole(), QtGui.QColor("black"))
self.setPalette(pal)
self.setAutoFillBackground(True)
def resizeEvent(self, a0: QtGui.QResizeEvent):
self.draw()
super().resizeEvent(a0)
def draw(self):
max_x, max_y = self.label.width(), self.label.height()
x_final = self.x_time[-1]
data = self.data / np.max(np.abs(self.data))
data = [abs(int(k * max_y)) for k in self.data]
x_pos_all = [int(self.x_time[i] * max_x / x_final) for i in range(len(data))]
y_pos = []
x_pos = list(range(max_x))
cnt = 0
for x_pixel in range(max_x):
mx = 0.0
v = x_pos_all[cnt]
while cnt < len(x_pos_all) and x_pos_all[cnt] == x_pixel:
if data[cnt] > mx:
mx = data[cnt]
cnt += 1
y_pos.append(mx)
print("data = ")
dat = np.array(data)
print(dat[dat > 0].shape[0])
pixmap = QtGui.QPixmap(self.size())
painter = QtGui.QPainter(pixmap)
pen = QtGui.QPen()
rect = self.label.rect()
print("rect = {}".format(rect))
painter.fillRect(rect, QtGui.QColor("lightblue"))
pen.setWidth(2)
pen.setColor(QtGui.QColor("green"))
painter.setPen(pen)
for x, y in zip(x_pos, y_pos):
painter.drawLine(x, max_y, x, max_y - y)
pen.setWidth(5)
pen.setColor(QtGui.QColor("red"))
painter.setPen(pen)
painter.drawRect(rect)
painter.end()
self.label.setPixmap(pixmap)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
Update:
Why can I not shrink the window after enlarging it? The layout of the QMainWindow takes as a reference the minimum size of the QMainWindow to the minimumSizeHint of the centralWidget, and in your case it is the QLabel that takes as minimumSizeHint the size of the QPixmap. If you want to be able to reduce the size you must override that method:
class Label(QtWidgets.QLabel):
def minimumSizeHint(self):
return QtCore.QSize(1, 1)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.label = Label()
self.setCentralWidget(self.label)
# ...
Why was the whole area not being painted before? Because you were painting a copy of the QPixmap: painter = QtGui.QPainter(self.label.pixmap()), not the stored QPixmap of the QLabel so nothing has been modified.

Categories