Catch events with QRectF - python

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

Related

How to continuously delete a rectangle from a previously drawn rectangle while drawing the new rectangle in PyQT5?

I am trying to code a gui for highlighting areas of a screen (specifically, greying out areas of an image surrounding a clear rectangle).
I have implemented the generation of a fullscreen transparent widget created after a button press. The widget is covered by a translucent grey rectangle. The user can still see the underlying active screen image which allows them to select a starting point for drawing a rectangle.
The mouse move event after a click event triggers the Update() function which allows the drawing of a new red rectangle.
The problem here is the previously drawn overlay rectangle is disappearing.
How do I fix the following code to draw the red rectangle over the translucent overlay and continually cut the area of the new rectangle from the previous overlay while drawing the rectangle?
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBrush, QColor, QPainter, QPen
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QDesktopWidget
class MainWidget(QWidget):
def __init__(self):
super().__init__()
# Set the window properties
self.setWindowTitle("Main Widget")
self.setGeometry(100, 100, 200, 200)
# Create a button
self.screenshotButton = QPushButton("Start", self)
self.screenshotButton.move(50, 50)
# Connect the button's clicked signal to the showTransparentWidget slot
self.screenshotButton.clicked.connect(self.openTransparentWidget)
def openTransparentWidget(self):
# Close the main widget
self.close()
# Create and show the transparent widget
self.transparentWidget = TransparentWidget()
self.transparentWidget.show()
class TransparentWidget(QWidget):
def __init__(self):
super().__init__()
# Get the screen dimensions
desktop = QDesktopWidget()
screenWidth = desktop.screenGeometry().width()
screenHeight = desktop.screenGeometry().height()
# Set the size of the widget to the screen dimensions
self.setGeometry(0, 0, screenWidth, screenHeight)
# Set the window flags to make the widget borderless and topmost
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
# Set the window transparency
self.setAttribute(Qt.WA_TranslucentBackground)
# Initialize the starting and ending positions of the box to -1
self.startX = -1
self.startY = -1
self.endX = -1
self.endY = -1
#call the paintEvent to generate an overlay
self.update()
def mousePressEvent(self, event):
# Store the starting position of the mouse when it is clicked
# Set the flag to True
self.mouseClicked = True
self.startX = event.x()
self.startY = event.y()
print(self.startX, self.startY)
def mouseMoveEvent(self, event):
if self.mouseClicked:
# Store the current position of the mouse as it is being dragged
self.endX = event.x()
self.endY = event.y()
# Redraw the widget to update the box
self.update()
def mouseReleaseEvent(self, event):
# Set the flag to False
self.mouseClicked = False
def paintEvent(self, event):
# Create a QPainter object and set it up for drawing
painter = QPainter(self)
# Draw translucent overlay over the transparent widget
if self.startX == -1 and self.endX == -1:
brush = QBrush(QColor(200, 200, 200, 128))
painter.setBrush(brush)
painter.drawRect(0, 0, self.width(), self.height())
# Set the composition mode to clear
#painter.setCompositionMode(QPainter.CompositionMode_Clear)
# Draw the box if the starting and ending positions are valid
if self.startX != -1 and self.endX != -1:
# Calculate the top-left and bottom-right corners of the box
topLeftX = min(self.startX, self.endX)
topLeftY = min(self.startY, self.endY)
bottomRightX = max(self.startX, self.endX)
bottomRightY = max(self.startY, self.endY)
# Set the composition mode to source over - these options seem to have no effect
#painter.setCompositionMode(QPainter.CompositionMode_SourceOver)
#painter.setCompositionMode(QPainter.CompositionMode_Clear)
#painter.setCompositionMode(QPainter.CompositionMode_DestinationOut)
pen = QPen(QColor(255 ,0, 0))
brush = QBrush(QColor(255, 255, 255, 0))
painter.setPen(pen)
painter.setBrush(brush)
# Draw the empty box (eraseRect also not working)
painter.drawRect(topLeftX, topLeftY, bottomRightX - topLeftX, bottomRightY - topLeftY)
app = QApplication(sys.argv)
mainWidget = MainWidget()
mainWidget.show()
sys.exit(app.exec_())
Edit: Here's a sample image I found that shows what I am trying to achieve. (It's actually from a snipping tool which is very similar to what I am trying to achieve)
Whenever paintEvent is called the entire widget is redrawn.
To overcome this, when drawing anything new, also re-draw the previous item.
The short solution is to update paintEventto draw the overlay and clear the new rectangle in the same call.
brush = QBrush(QColor(200, 200, 200, 128))
painter.setBrush(brush)
painter.drawRect(0, 0, self.width(), self.height())
painter.setCompositionMode(QPainter.CompositionMode_Clear)
Thanks to #musicamante for your support via the comments section.
Here is the full code:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QBrush, QColor, QPainter, QPen
from PyQt5.QtWidgets import QApplication, QPushButton, QWidget, QDesktopWidget
class MainWidget(QWidget):
def __init__(self):
super().__init__()
# Set the window properties
self.setWindowTitle("Main Widget")
self.setGeometry(100, 100, 200, 200)
# Create a button
self.screenshotButton = QPushButton("Start", self)
self.screenshotButton.move(50, 50)
# Connect the button's clicked signal to the showTransparentWidget slot
self.screenshotButton.clicked.connect(self.openTransparentWidget)
def openTransparentWidget(self):
# Close the main widget
self.close()
# Create and show the transparent widget
self.transparentWidget = TransparentWidget()
self.transparentWidget.show()
class TransparentWidget(QWidget):
def __init__(self):
super().__init__()
# Get the screen dimensions
desktop = QDesktopWidget()
screenWidth = desktop.screenGeometry().width()
screenHeight = desktop.screenGeometry().height()
# Set the size of the widget to the screen dimensions
self.setGeometry(0, 0, screenWidth, screenHeight)
# Set the window flags to make the widget borderless and topmost
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
# Set the window transparency
self.setAttribute(Qt.WA_TranslucentBackground)
# Initialize the starting and ending positions of the box to -1
self.startX = -1
self.startY = -1
self.endX = -1
self.endY = -1
#call the paintEvent to generate an overlay
self.update()
def mousePressEvent(self, event):
# Store the starting position of the mouse when it is clicked
# Set the flag to True
self.mouseClicked = True
self.startX = event.x()
self.startY = event.y()
print(self.startX, self.startY)
def mouseMoveEvent(self, event):
if self.mouseClicked:
# Store the current position of the mouse as it is being dragged
self.endX = event.x()
self.endY = event.y()
# Redraw the widget to update the box
self.update()
def mouseReleaseEvent(self, event):
# Set the flag to False
self.mouseClicked = False
def paintEvent(self, event):
# Create a QPainter object and set it up for drawing
painter = QPainter(self)
# Draw translucent overlay over the transparent widget
if self.startX == -1 and self.endX == -1:
brush = QBrush(QColor(200, 200, 200, 128))
painter.setBrush(brush)
painter.drawRect(0, 0, self.width(), self.height())
# Set the composition mode to clear
#painter.setCompositionMode(QPainter.CompositionMode_Clear)
# Draw the box if the starting and ending positions are valid
if self.startX != -1 and self.endX != -1:
# Calculate the top-left and bottom-right corners of the box
topLeftX = min(self.startX, self.endX)
topLeftY = min(self.startY, self.endY)
bottomRightX = max(self.startX, self.endX)
bottomRightY = max(self.startY, self.endY)
brush = QBrush(QColor(200, 200, 200, 128))
painter.setBrush(brush)
painter.drawRect(0, 0, self.width(), self.height())
painter.setCompositionMode(QPainter.CompositionMode_Clear)
pen = QPen(QColor(255 ,0, 0))
brush = QBrush(QColor(0, 0, 0, 0))
painter.setPen(pen)
painter.setBrush(brush)
# Draw the empty box
painter.drawRect(topLeftX, topLeftY, bottomRightX - topLeftX, bottomRightY - topLeftY)
app = QApplication(sys.argv)
mainWidget = MainWidget()
mainWidget.show()
sys.exit(app.exec_())
UI drawing (at the low level) normally happens using a frame buffer, which is eventually cleared in a specific area in which new painting is going to happen.
This means that you cannot rely on contents previously drawn in another paint event: even when requesting to update a specific region of the widget (ie: using update(QRect)), that region will be cleared from the buffer, and previous contents doesn't exist any more, and the buffer is also cleared anyway whenever the window is hidden and shown again, like after minimizing and restoring it, or after switching virtual desktop.
In your case, it means that the "background" rectangle will only be painted at start up (when the coordinates are -1), not after that.
The solution is to always draw all the contents, and eventually cut out the area using setClipRegion().
class TransparentWidget(QWidget):
area = reference = None
def __init__(self):
super().__init__()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
self.setAttribute(Qt.WA_TranslucentBackground)
self.setMouseTracking(True)
screenArea = QRect()
for screen in QApplication.screens():
screenArea |= screen.geometry()
self.setGeometry(screenArea)
def mousePressEvent(self, event):
if event.button() != Qt.LeftButton:
return
pos = event.pos()
if self.area:
span = QRect(-5, -5, 10, 10)
if span.translated(self.area.topLeft()).contains(pos):
self.reference = self.area.setTopLeft
elif span.translated(self.area.topRight()).contains(pos):
self.reference = self.area.setTopRight
elif span.translated(self.area.bottomRight()).contains(pos):
self.reference = self.area.setBottomRight
elif span.translated(self.area.bottomLeft()).contains(pos):
self.reference = self.area.setBottomLeft
else:
self.reference = None
if not self.reference:
self.area = QRect(pos, QSize(1, 1))
self.reference = self.area.setBottomRight
self.update()
def mouseMoveEvent(self, event):
if self.reference:
self.reference(event.pos())
self.update()
elif self.area:
pos = event.pos()
span = QRect(-5, -5, 10, 10)
cursor = None
if span.translated(self.area.topLeft()).contains(pos):
cursor = Qt.SizeFDiagCursor
elif span.translated(self.area.topRight()).contains(pos):
cursor = Qt.SizeBDiagCursor
elif span.translated(self.area.bottomRight()).contains(pos):
cursor = Qt.SizeFDiagCursor
elif span.translated(self.area.bottomLeft()).contains(pos):
cursor = Qt.SizeBDiagCursor
if cursor is not None:
self.setCursor(cursor)
else:
self.unsetCursor()
def mouseReleaseEvent(self, event):
self.reference = None
if self.area is not None:
self.area = self.area.normalized()
self.update()
def paintEvent(self, event):
painter = QPainter(self)
if self.area is not None:
r = QRegion(self.rect())
r ^= QRegion(self.area.normalized().adjusted(1, 1, 0, 0))
painter.setClipRegion(r)
painter.fillRect(self.rect(), QColor(200, 200, 200, 128))
if self.area is not None:
painter.setPen(QColor(255 ,0, 0))
painter.drawRect(self.area.normalized())
Notes:
QDesktopWidget is obsolete in Qt5, use QScreen instead;
you should always consider the case of multiple screen computers; if you specifically do not want to show your widget in all screens, then just use `showFullScreen();
whenever possible and it makes sense, use Qt objects functions, which are normally quite fast and provide better readability (for instance, using QPoint, QRect and functions like QRect.normalized());
calling self.update() in the __init__ is pointless: update() doesn't immediately redraw the widget, it only schedules an update, and since the first painting will happen anyway as soon as the widget is shown, there's no point in doing it;

Qt - Show widget or label above all widget

I want to display a loading screen every time a user presses a button (a process that takes a few seconds runs).
I want something like this
QSplashScreen does not help me because that is only used before opening the application and a QDialog is not useful for me because I want that by dragging the window the application will move along with the message Loading...
What do I have to use?
The only (safe) way to achieve this is to add a child widget without adding it to any layout manager.
The only things you have to care about is that the widget is always raised as soon as it's shown, and that the geometry is always updated to the parent widget (or, better, the top level window).
This is a slightly more advanced example, but it has the benefit that you can just subclass any widget adding the LoadingWidget class to the base classes in order to implement a loading mechanism.
from random import randrange
from PyQt5 import QtCore, QtGui, QtWidgets
class Loader(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
self.gradient = QtGui.QConicalGradient(.5, .5, 0)
self.gradient.setCoordinateMode(self.gradient.ObjectBoundingMode)
self.gradient.setColorAt(.25, QtCore.Qt.transparent)
self.gradient.setColorAt(.75, QtCore.Qt.transparent)
self.animation = QtCore.QVariantAnimation(
startValue=0., endValue=1.,
duration=1000, loopCount=-1,
valueChanged=self.updateGradient
)
self.stopTimer = QtCore.QTimer(singleShot=True, timeout=self.stop)
self.focusWidget = None
self.hide()
parent.installEventFilter(self)
def start(self, timeout=None):
self.show()
self.raise_()
self.focusWidget = QtWidgets.QApplication.focusWidget()
self.setFocus()
if timeout:
self.stopTimer.start(timeout)
else:
self.stopTimer.setInterval(0)
def stop(self):
self.hide()
self.stopTimer.stop()
if self.focusWidget:
self.focusWidget.setFocus()
self.focusWidget = None
def updateGradient(self, value):
self.gradient.setAngle(-value * 360)
self.update()
def eventFilter(self, source, event):
# ensure that we always cover the whole parent area
if event.type() == QtCore.QEvent.Resize:
self.setGeometry(source.rect())
return super().eventFilter(source, event)
def showEvent(self, event):
self.setGeometry(self.parent().rect())
self.animation.start()
def hideEvent(self, event):
# stop the animation when hidden, just for performance
self.animation.stop()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setRenderHints(qp.Antialiasing)
color = self.palette().window().color()
color.setAlpha(max(color.alpha() * .5, 128))
qp.fillRect(self.rect(), color)
text = 'Loading...'
interval = self.stopTimer.interval()
if interval:
remaining = int(max(0, interval - self.stopTimer.remainingTime()) / interval * 100)
textWidth = self.fontMetrics().width(text + ' 000%')
text += ' {}%'.format(remaining)
else:
textWidth = self.fontMetrics().width(text)
textHeight = self.fontMetrics().height()
# ensure that there's enough space for the text
if textWidth > self.width() or textHeight * 3 > self.height():
drawText = False
size = max(0, min(self.width(), self.height()) - textHeight * 2)
else:
size = size = min(self.height() / 3, max(textWidth, textHeight))
drawText = True
circleRect = QtCore.QRect(0, 0, size, size)
circleRect.moveCenter(self.rect().center())
if drawText:
# text is going to be drawn, move the circle rect higher
circleRect.moveTop(circleRect.top() - textHeight)
middle = circleRect.center().x()
qp.drawText(
middle - textWidth / 2, circleRect.bottom() + textHeight,
textWidth, textHeight,
QtCore.Qt.AlignCenter, text)
self.gradient.setColorAt(.5, self.palette().windowText().color())
qp.setPen(QtGui.QPen(self.gradient, textHeight))
qp.drawEllipse(circleRect)
class LoadingExtension(object):
# a base class to extend any QWidget subclass's top level window with a loader
def startLoading(self, timeout=0):
window = self.window()
if not hasattr(window, '_loader'):
window._loader = Loader(window)
window._loader.start(timeout)
# this is just for testing purposes
if not timeout:
QtCore.QTimer.singleShot(randrange(1000, 5000), window._loader.stop)
def loadingFinished(self):
if hasattr(self.window(), '_loader'):
self.window()._loader.stop()
class Test(QtWidgets.QWidget, LoadingExtension):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
# just a test widget
textEdit = QtWidgets.QTextEdit()
layout.addWidget(textEdit, 0, 0, 1, 2)
textEdit.setMinimumHeight(20)
layout.addWidget(QtWidgets.QLabel('Timeout:'))
self.timeoutSpin = QtWidgets.QSpinBox(maximum=5000, singleStep=250, specialValueText='Random')
layout.addWidget(self.timeoutSpin, 1, 1)
self.timeoutSpin.setValue(2000)
btn = QtWidgets.QPushButton('Start loading...')
layout.addWidget(btn, 2, 0, 1, 2)
btn.clicked.connect(lambda: self.startLoading(self.timeoutSpin.value()))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
Please check Qt::WindowFlags. The Qt::SplashScreen flag will give you splash screen experience without usage QSplashScreen (you can use it with all widget as show) or, better, use QDialog with this flag.
For moving, probably fine solution is not available but you can just use parent moveEvent to emmit signal. For example:
Main window:
moveEvent -> signal moved
Dialog:
signal move -> re-center window.
Its look as not hard.
By the way, I think block all GUI during application run is not the best solution. You you think use QProgressBar?
You can use this slot: void QWidget::raise().
Raises this widget to the top of the parent widget's stack.
After this call the widget will be visually in front of any overlapping sibling widgets.

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.

PyQt keep aspect ratio fixed

I'm working on a PyQt5 GUI, so far, I've just had experience with python scripts and did not delve into creating user interfaces.
The GUI will have to be used on different screens (maybe also some old 4:3 ratio screens) and will need to look nice in different sizes.
Now, my approach to make my life easier was to enforce a fixed aspect ratio of the window and resize the different elements according to window size.
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent= None):
super().__init__(parent)
self.form_widget = FormWidget(self)
self.setCentralWidget(self.form_widget)
self.resize(200, 400)
self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
self.sizePolicy.setHeightForWidth(True)
self.setSizePolicy(self.sizePolicy)
def heightForWidth(self, width):
return width * 2
class FormWidget(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
def resizeEvent(self, event):
f = self.font()
temp = event.size().height()
f.setPixelSize(temp / 16)
self.setFont(f)
return super().resizeEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Resizing the elements according to window size works fine, but window aspect ratio is not kept at all.
I copied this approach with heightForWidth from old PyQt4 threads. Doesn't this approach work anymore in PyQt5? Am I missing something?
If I understood your question, you should try using a layout inside the main window.
I did this:
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent= None):
super().__init__(parent)
self.central_widget = QtWidgets.QWidget()
self.central_layout = QtWidgets.QVBoxLayout()
self.setCentralWidget(self.central_widget)
self.central_widget.setLayout(self.central_layout)
# Lets create some widgets inside
self.label = QtWidgets.QLabel()
self.list_view = QtWidgets.QListView()
self.push_button = QtWidgets.QPushButton()
self.label.setText('Hi, this is a label. And the next one is a List View :')
self.push_button.setText('Push Button Here')
# Lets add the widgets
self.central_layout.addWidget(self.label)
self.central_layout.addWidget(self.list_view)
self.central_layout.addWidget(self.push_button)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
If you resize the window, the widgets inside it get resized.
First, answered by Marc and codeling in this question, heightForWidth is only supported for QGraphicsLayout's subclasses.
Second, how to make a fixed aspect ratio window (or top-level widget) in qt (or pyqt) is a question that have been asked for years. However, as far as I know, there is no standard way of doing so, and it is something surprisingly hard to achieve. In short, my way of doing this is use Qt.FramelessWindowHint to create a frameless window without system move and resize function, and implement custom move and resize.
Explain important mechanism:
move:
In mousePressEvent, keep the place where we last clicked on the widget(the draggable area).
In mouseMoveEvent, calculate the distance between the last clicked point and the current mouse location. Move the window according to this distance.
resize:
Find the increase or decrease step size of width and height by dividing the minimum width and height of the window by their highest common factor.
Use the step size to increase or decrease the window size to keep the aspect ratio.
A screenshot to show that it can resize according to the aspect ratio.
The following code should works with both PyQt5 and Pyside2.
from PyQt5.QtCore import Qt, QRect, QPoint, QEvent
from PyQt5.QtWidgets import (QLabel, QMainWindow, QApplication, QSizePolicy,
QVBoxLayout, QWidget, QHBoxLayout, QPushButton)
from enum import Enum
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowFlags(Qt.FramelessWindowHint)
self.createCostumTitleBar()
self.setContentsMargins(0, 0, 0, 0)
self.central = QWidget()
self.central.setStyleSheet("background-color: #f8ecdf")
self.centralLayout = QVBoxLayout()
self.central.setLayout(self.centralLayout)
self.centralLayout.addWidget(
self.costumsystemmenu, alignment=Qt.AlignTop)
self.centralLayout.setContentsMargins(0, 0, 0, 0)
self.setCentralWidget(self.central)
# Set the minimum size to avoid window being resized too small.
self.setMinimumSize(300, 400)
self.minheight = self.minimumHeight()
self.minwidth = self.minimumWidth()
self.resize(300, 400)
# make sure your minium size have the same aspect ratio as the step.
self.stepY = 4
self.stepX = 3
# install the event filter on this window.
self.installEventFilter(self)
self.grabarea.installEventFilter(self)
self.cursorpos = CursorPos.DEFAULT
self.iswindowpress = False
def createCostumTitleBar(self):
self.costumsystemmenu = QWidget()
self.costumsystemmenu.setStyleSheet("background-color: #ccc")
self.costumsystemmenu.setContentsMargins(0, 0, 0, 0)
self.costumsystemmenu.setMinimumHeight(30)
self.grabarea = QLabel("")
self.grabarea.setStyleSheet("background-color: #ccc")
self.grabarea.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Preferred)
titlebarlayout = QHBoxLayout()
titlebarlayout.setContentsMargins(11, 11, 11, 11)
titlebarlayout.setSpacing(0)
self.closeButton = QPushButton("X")
self.closeButton.setSizePolicy(
QSizePolicy.Minimum, QSizePolicy.Preferred)
self.closeButton.clicked.connect(self.close)
self.costumsystemmenu.setLayout(titlebarlayout)
titlebarlayout.addWidget(self.grabarea)
titlebarlayout.addWidget(self.closeButton, alignment=Qt.AlignRight)
self.istitlebarpress = False
def eventFilter(self, object, event):
# The eventFilter() function must return true if the event
# should be filtered, (i.e. stopped); otherwise it must return false.
# https://doc.qt.io/qt-5/qobject.html#eventFilter
# check if the object is the mainwindow.
if object == self:
if event.type() == QEvent.HoverMove:
if not self.iswindowpress:
self.setCursorShape(event)
return True
elif event.type() == QEvent.MouseButtonPress:
self.iswindowpress = True
# Get the position of the cursor and map to the global coordinate of the widget.
self.globalpos = self.mapToGlobal(event.pos())
self.origingeometry = self.geometry()
return True
elif event.type() == QEvent.MouseButtonRelease:
self.iswindowpress = False
return True
elif event.type() == QEvent.MouseMove:
if self.cursorpos != CursorPos.DEFAULT and self.iswindowpress:
self.resizing(self.globalpos, event,
self.origingeometry, self.cursorpos)
return True
else:
return False
elif object == self.grabarea:
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.LeftButton and self.iswindowpress == False:
self.oldpos = event.globalPos()
self.oldwindowpos = self.pos()
self.istitlebarpress = True
return True
elif event.type() == QEvent.MouseButtonRelease:
self.istitlebarpress = False
return True
elif event.type() == QEvent.MouseMove:
if (self.istitlebarpress):
distance = event.globalPos()-self.oldpos
newwindowpos = self.oldwindowpos + distance
self.move(newwindowpos)
return True
else:
return False
else:
return False
# Change the cursor shape when the cursor is over different part of the window.
def setCursorShape(self, event, handlersize=11):
rect = self.rect()
topLeft = rect.topLeft()
topRight = rect.topRight()
bottomLeft = rect.bottomLeft()
bottomRight = rect.bottomRight()
# get the position of the cursor
pos = event.pos()
# make the resize handle include some space outside the window,
# can avoid user move too fast and loss the handle.
# top handle
if pos in QRect(QPoint(topLeft.x()+handlersize, topLeft.y()-2*handlersize),
QPoint(topRight.x()-handlersize, topRight.y()+handlersize)):
self.setCursor(Qt.SizeVerCursor)
self.cursorpos = CursorPos.TOP
# bottom handle
elif pos in QRect(QPoint(bottomLeft.x()+handlersize, bottomLeft.y()-handlersize),
QPoint(bottomRight.x()-handlersize, bottomRight.y()+2*handlersize)):
self.setCursor(Qt.SizeVerCursor)
self.cursorpos = CursorPos.BOTTOM
# right handle
elif pos in QRect(QPoint(topRight.x()-handlersize, topRight.y()+handlersize),
QPoint(bottomRight.x()+2*handlersize, bottomRight.y()-handlersize)):
self.setCursor(Qt.SizeHorCursor)
self.cursorpos = CursorPos.RIGHT
# left handle
elif pos in QRect(QPoint(topLeft.x()-2*handlersize, topLeft.y()+handlersize),
QPoint(bottomLeft.x()+handlersize, bottomLeft.y()-handlersize)):
self.setCursor(Qt.SizeHorCursor)
self.cursorpos = CursorPos.LEFT
# topRight handle
elif pos in QRect(QPoint(topRight.x()-handlersize, topRight.y()-2*handlersize),
QPoint(topRight.x()+2*handlersize, topRight.y()+handlersize)):
self.setCursor(Qt.SizeBDiagCursor)
self.cursorpos = CursorPos.TOPRIGHT
# topLeft handle
elif pos in QRect(QPoint(topLeft.x()-2*handlersize, topLeft.y()-2*handlersize),
QPoint(topLeft.x()+handlersize, topLeft.y()+handlersize)):
self.setCursor(Qt.SizeFDiagCursor)
self.cursorpos = CursorPos.TOPLEFT
# bottomRight handle
elif pos in QRect(QPoint(bottomRight.x()-handlersize, bottomRight.y()-handlersize),
QPoint(bottomRight.x()+2*handlersize, bottomRight.y()+2*handlersize)):
self.setCursor(Qt.SizeFDiagCursor)
self.cursorpos = CursorPos.BOTTOMRIGHT
# bottomLeft handle
elif pos in QRect(QPoint(bottomLeft.x()-2*handlersize, bottomLeft.y()-handlersize),
QPoint(bottomLeft.x()+handlersize, bottomLeft.y()+2*handlersize)):
self.setCursor(Qt.SizeBDiagCursor)
self.cursorpos = CursorPos.BOTTOMLEFT
# Default is the arrow cursor.
else:
self.setCursor(Qt.ArrowCursor)
self.cursorpos = CursorPos.DEFAULT
def resizing(self, originpos, event, geo, cursorpos):
newpos = self.mapToGlobal(event.pos())
# find the distance between new and old cursor position.
dist = newpos - originpos
# calculate the steps to grow or srink.
if cursorpos in [CursorPos.TOP, CursorPos.BOTTOM,
CursorPos.TOPRIGHT,
CursorPos.BOTTOMLEFT, CursorPos.BOTTOMRIGHT]:
steps = dist.y()//self.stepY
elif cursorpos in [CursorPos.LEFT, CursorPos.TOPLEFT, CursorPos.RIGHT]:
steps = dist.x()//self.stepX
# if the distance moved is too stort, grow or srink by 1 step.
if steps == 0:
steps = -1 if dist.y() < 0 or dist.x() < 0 else 1
oldwidth = geo.width()
oldheight = geo.height()
oldX = geo.x()
oldY = geo.y()
if cursorpos in [CursorPos.TOP, CursorPos.TOPRIGHT]:
width = oldwidth - steps * self.stepX
height = oldheight - steps * self.stepY
newX = oldX
newY = oldY + (steps * self.stepY)
# check if the new size is within the size limit.
if height >= self.minheight and width >= self.minwidth:
self.setGeometry(newX, newY, width, height)
elif cursorpos in [CursorPos.BOTTOM, CursorPos.RIGHT, CursorPos.BOTTOMRIGHT]:
width = oldwidth + steps * self.stepX
height = oldheight + steps * self.stepY
self.resize(width, height)
elif cursorpos in [CursorPos.LEFT, CursorPos.BOTTOMLEFT]:
width = oldwidth - steps * self.stepX
height = oldheight - steps * self.stepY
newX = oldX + steps * self.stepX
newY = oldY
# check if the new size is within the size limit.
if height >= self.minheight and width >= self.minwidth:
self.setGeometry(newX, newY, width, height)
elif cursorpos == CursorPos.TOPLEFT:
width = oldwidth - steps * self.stepX
height = oldheight - steps * self.stepY
newX = oldX + steps * self.stepX
newY = oldY + steps * self.stepY
# check if the new size is within the size limit.
if height >= self.minheight and width >= self.minwidth:
self.setGeometry(newX, newY, width, height)
else:
pass
# cursor position
class CursorPos(Enum):
TOP = 1
BOTTOM = 2
RIGHT = 3
LEFT = 4
TOPRIGHT = 5
TOPLEFT = 6
BOTTOMRIGHT = 7
BOTTOMLEFT = 8
DEFAULT = 9
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Finally, I'd like to give special thanks to the authors and editors of this question, GLHF, DRPK, Elad Joseph, and SimoN SavioR. Without their contribution to the community, it wouldn't be possible to come up with this answer.

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

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_())

Categories