How to force screen-snip size ratio. PyQt5 - python

I want to modify Screen-Snip code from GitHub/harupy/snipping-tool so that every screen-snip has a ratio of 3 x 2. (I will save as 600 x 400 px image later)
I'm not sure how to modify self.end dynamically so that the user clicks and drags with a 3 x 2 ratio. The mouse position will define the x coordinate, and the y coordinate will be int(x * 2/3)
Any suggestions on how to do this? I promise I've been researching this, and I just can't seem to "crack the code" of modifying only the y coordinate of self.end
Here is the code:
import sys
import PyQt5
from PyQt5 import QtWidgets, QtCore, QtGui
import tkinter as tk
from PIL import ImageGrab
import numpy as np
import cv2 # package is officially called opencv-python
class MyWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
root = tk.Tk()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
self.setGeometry(0, 0, screen_width, screen_height)
self.setWindowTitle(' ')
self.begin = QtCore.QPoint()
self.end = QtCore.QPoint()
self.setWindowOpacity(0.3)
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.CrossCursor)
)
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
print('Capture the screen...')
self.show()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setPen(QtGui.QPen(QtGui.QColor('black'), 3))
qp.setBrush(QtGui.QColor(128, 128, 255, 128))
qp.drawRect(QtCore.QRect(self.begin, self.end)) ##### This seems like the place I should modify. #########
def mousePressEvent(self, event):
self.begin = event.pos()
self.end = self.begin
self.update()
def mouseMoveEvent(self, event):
self.end = event.pos()
self.update()
def mouseReleaseEvent(self, event):
self.close()
x1 = min(self.begin.x(), self.end.x())
y1 = min(self.begin.y(), self.end.y())
x2 = max(self.begin.x(), self.end.x())
y2 = max(self.begin.y(), self.end.y())
img = ImageGrab.grab(bbox=(x1, y1, x2, y2))
img.save('capture.png')
img = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB)
cv2.imshow('Captured Image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MyWidget()
window.show()
app.aboutToQuit.connect(app.deleteLater)
sys.exit(app.exec_())

You don't need to "change the y coordinate", you just need to use the correct arguments to create the rectangle.
There are various ways to initialize a QRect, you are using the two points, another one (and more common) is to use the coordinates of the origin and the size of the rectangle.
Once you know the width, you can compute the height, and make it negative if the y of the end point is above the begin.
Note that in this way you could get a "negative" rectangle (negative width, with the "right" edge actually at the left, the same for the height/bottom), so it's usually better to use normalized, which also allows you to get the correct coordinates of the rectangle for screen grabbing.
class MyWidget(QtWidgets.QWidget):
# ...
def getRect(self):
# a commodity function that always return a correctly sized
# rectangle, with normalized coordinates
width = self.end.x() - self.begin.x()
height = abs(width * 2 / 3)
if self.end.y() < self.begin.y():
height *= -1
return QtCore.QRect(self.begin.x(), self.begin.y(),
width, height).normalized()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setPen(QtGui.QPen(QtGui.QColor('black'), 3))
qp.setBrush(QtGui.QColor(128, 128, 255, 128))
qp.drawRect(self.getRect())
def mouseReleaseEvent(self, event):
self.close()
rect = self.getRect()
img = ImageGrab.grab(bbox=(
rect.topLeft().x(),
rect.topLeft().y(),
rect.bottomRight().x(),
rect.bottomRight().y()
))
# ...
I suggest you to use a delayed setGeometry as in some systems (specifically Linux), the "final" geometry is actually applied only as soon as the window is correctly mapped from the window manager, especially if the window manager tends to apply a geometry on its own when the window is shown the first time. For example, I have two screens, and your window got "centered" on my main screen, making it shifted by half width of the other screen.
Also consider that importing Tk just for the screen size doesn't make much sense, since Qt already provides all necessary tools.
You can use something like that:
class MyWidget(QtWidgets.QWidget):
# ...
def showEvent(self, event):
if not event.spontaneous():
# delay the geometry on the "next" cycle of the Qt event loop;
# this should take care of positioning issues for systems that
# try to move newly created windows on their own
QtCore.QTimer.singleShot(0, self.resetPos)
def resetPos(self):
rect = QtCore.QRect()
# create a rectangle that is the sum of the geometries of all available
# screens; the |= operator acts as `rect = rect.united(screen.geometry())`
for screen in QtWidgets.QApplication.screens():
rect |= screen.geometry()
self.setGeometry(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;

How to make same event mean different things for different functions in tkinter?

I was wondering if it was possible to have the same event(like-R-click/R-mouse-press), mean different things for different functions :
For example : Moving an object on R-mouse press and also having the same event used for changing the shape of an object(by mouse press and drag).
My requirement :
I am using a code which is used for snipping the area within, so basically it is a canvas which is being used as an overlay....
The problem i'm facing is that the overlay/snipping area never matches the image/pdf area. So currently i'm manually changing the canvas coordinates /Set_Geometry coordinates ,so that it matches the image/pdf (completely).
This process takes too long.
So i wanted to automate this process of changing and moving this overly /canvas by mouse.
There are 2 specific operations i want :
Being able to stretch the overlay/ canvas to match the image.
Move this canvas around in the screen to place it exactly where the image is present.
Both of these operations i wanted to do using same the "b3-move" button.
Here is the code :
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
import tkinter as tk
from PIL import ImageGrab
import numpy as np
import cv2
class MyWidget(QtWidgets.QWidget):
def __init__(self):
super().__init__()
root = tk.Tk()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
x = screen_width/2 - dim[0]/2
y = screen_height/2 - dim[1]/2
self.setGeometry(x, y, dim[0], dim[1])
self.setWindowTitle('Snipping')
self.begin = QtCore.QPoint()
self.end = QtCore.QPoint()
self.setWindowOpacity(0.8)
QtWidgets.QApplication.setOverrideCursor(
QtGui.QCursor(QtCore.Qt.CrossCursor))
self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
print('Capture the screen...')
self.show()
def paintEvent(self, event):
qp = QtGui.QPainter(self)
qp.setPen(QtGui.QPen(QtGui.QColor('black'), 3))
qp.setBrush(QtGui.QColor(128, 128, 255, 128))
qp.drawRect(QtCore.QRect(self.begin, self.end))
def mousePressEvent(self, event):
self.begin = event.pos()
self.end = self.begin
self.update()
def mouseMoveEvent(self, event):
self.end = event.pos()
self.update()
def mouseReleaseEvent(self, event):
self.close()
x1 = min(self.begin.x(), self.end.x())
y1 = min(self.begin.y(), self.end.y())
x2 = max(self.begin.x(), self.end.x())
y2 = max(self.begin.y(), self.end.y())
img = ImageGrab.grab(bbox=(x1, y1, x2, y2))
img = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB)
cv2.imshow('Captured Image', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
global s
s = str(x1) +","+str(dim[1] - y1)+","+str(x2)+","+str(dim[1]- y2)
print(s)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MyWidget()
window.show()
app.aboutToQuit.connect(app.deleteLater)
sys.exit(app.exec_())
How can i do this?
You can define event pattern in tkinter and they have the form of <modifier-modifier-type-detail>.
So what you can do is to combine event modifier such as Button3 and types as Motion with each other, (e.g.) root.bind('<Button3-Motion>',button3motion)
An example can be found below:
import tkinter as tk
def button3press(event):
print('right click')
def button3release(event):
print('right click ended')
def button3motion(event):
print('mouse move while right click')
root = tk.Tk()
root.bind('<ButtonPress-3>',button3press)
root.bind('<ButtonRelease-3>',button3release)
root.bind('<B3-Motion>',button3motion)
root.mainloop()

How to draw a figure on mouse click in qtapplication

I'm new to Pyqt5 and writing applications with it in Python so forgive me if this is a very simple question but I'm having trouble drawing ellipses in my program. I want to draw one by wherever a click occurs. Here is my code:
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *
class Window(QGraphicsView):
def __init__(self):
super().__init__()
self.scene = QGraphicsScene()
self.setScene(self.scene)
# p.setTransform(transform)
self.button = QPushButton("Draw")
self.button.setCheckable(True)
self.button.setGeometry(0, 0, 100, 30)
self.scene.addWidget(self.button)
# self.setMouseTracking(True)
width, height = 1000, 1000
self.setFixedSize(width, height);
self.setSceneRect(0, 0, width, height);
self.fitInView(0, 0, width, height, Qt.KeepAspectRatio);
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.show()
def mousePressEvent(self, event):
if self.button.isChecked():
x = event.x()
y = event.y()
print(x, y)
ellipse = QGraphicsEllipseItem(x, y, 50, 20)
self.scene.addItem(ellipse)
The issue I'm having is I think the mousePressEvent function isn't allowing me to click on my button to enable drawing but the part I'm really not sure about is what is going on in the mousePressEvent. It seems as though it's getting the (x, y) coordinates within the QGraphicsView object but my ellipses are getting drawn in strange spots far away from wherever is clicked in my application when it's open.
You should not override the mousePressEvent as you remove the default behavior such as sending the event to the button. On the other hand you have to convert the coordinates of the view to the coordinates of the scene.
self.proxy_widget = self.scene.addWidget(self.button)
def mousePressEvent(self, event):
super().mousePressEvent(event)
vp = event.pos()
if self.proxy_widget in self.items(vp):
return
if self.button.isChecked():
ellipse = QGraphicsEllipseItem(0, 0, 50, 20)
self.scene.addItem(ellipse)
sp = self.mapToScene(vp)
ellipse.setPos(sp)

Draw centered circle and implement an object (bitmp) that moves along the circle

I created a new UI window and drew a circle. I would like to have a small figure (think small stick figure) move along the perimeter of the circle at a certain rate. The rate of movement is probably easy enough to change. How do I have an object move along the perimeter of the circle?
from PyQt5 import QtGui
from PyQt5.QtWidgets import QApplication, QMainWindow
import sys
from PyQt5.QtGui import QPainter, QPen, QBrush
from PyQt5.QtCore import Qt
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.title = "Drawing Ellipse"
self.top = 200
self.left = 200
self.width = 750
self.height = 750
self.InitWindow()
def InitWindow(self):
self.setWindowIcon(QtGui.QIcon("icon.png"))
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.drawEllipse(200,200, 400,400)
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
First of all, there are some issues with your code:
the circle is not correctly centered: if a centered circle positioned at (200, 200) has a 400 diameter, the window should have a width and height equal to 800; but this is not a good choice, as the window could be resized (by the user or the OS), and if you want to ensure that it is actually centered, you need to adapt the rect used for the painting to the current size;
no painting should ever happen in a QMainWindow unless absolutely required: a Qt main window uses a central widget to show its main contents, and if you want to do any "main" custom painting, it should happen in that central widget; there are very few cases for which a paintEvent should be implemented in a QMainWindow, but they are very only for specific and very advanced scenarios;
a QMainWindow has an internal layout that takes into account possible features such as a menu bar, a status bar, and possibly some amount of toolbars and dock widgets, so you cannot rely on a fixed geometry for the painting, as you risk that the painting would be partially hidden by other objects;
existing class properties should never, ever be overwritten; I'm specifically referring to self.width and self.height, which are existing dynamic properties of any QWidget subclass, and should never be overwritten; I know that there are some tutorials that use that pattern (I've already warned some bloggers about this), but you should not follow those suggestions;
Considering the above, the solution is to create a QWidget subclass, and implement the paintEvent of that class. Then, using a QVariantAnimation allows to create a smooth animation that can be used to correctly position the moving image on the perimeter.
In the following example I'm using a QPainterPath with a simple ellipse, which allows me to get the position using the pointAtPercent() function.
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtGui import QIcon, QPainter, QPen, QPixmap, QPainterPath
from PyQt5.QtCore import Qt, QRectF, QPoint, QPointF, QVariantAnimation
class CirclingWidget(QWidget):
offset = QPoint()
def __init__(self, diameter=1, relative=True, sprite=None,
anglesPerSecond=360, clockwise=True, offset=None, alignment=None):
super().__init__()
self.diameter = diameter
self.relative = relative
self.clockwise = clockwise
self.setSprite(sprite, offset or alignment)
self.animation = QVariantAnimation(startValue=0., endValue=1.,
duration=360000 / anglesPerSecond, loopCount=-1)
self.animation.valueChanged.connect(self.update)
def setSprite(self, sprite, arg):
if isinstance(sprite, str):
sprite = QPixmap(sprite)
if isinstance(sprite, QPixmap) and not sprite.isNull():
self.sprite = sprite
if isinstance(arg, Qt.AlignmentFlag):
self.setAlignment(arg)
else:
self.setOffset(arg)
else:
self.sprite = None
self.update()
def setAlignment(self, alignment=None):
if self.sprite:
x = y = 0
if alignment is not None:
if alignment & Qt.AlignHCenter:
x = -self.sprite.width() / 2
elif alignment & Qt.AlignRight:
x = -self.sprite.width()
if alignment & Qt.AlignVCenter:
y = -self.sprite.height() / 2
elif alignment & Qt.AlignBottom:
y = -self.sprite.height()
self.offset = QPointF(x, y)
self.update()
def setOffset(self, offset=None):
if self.sprite:
x = y = 0
if isinstance(offset, int):
x = y = offset
elif isinstance(offset, float):
x = self.sprite.width() * offset
y = self.sprite.height() * offset
elif isinstance(offset, (QPoint, QPointF)):
x = offset.x()
y = offset.y()
elif isinstance(offset, (tuple, list)):
x, y = offset
self.offset = QPointF(x, y)
self.update()
def setAnglesPerSecond(self, ratio):
self.animation.setDuration(360000 / ratio)
def setClockwise(self, clockwise=True):
if self.clockwise != clockwise:
self.clockwise = clockwise
self.animation.setCurrentTime(
self.animation.duration() - self.animation.currentTime())
self.update()
def start(self, angle=0):
self.animation.start()
# the angle is considered clockwise, starting from 12h
start = (angle - 90) / 360 % 1
self.animation.setCurrentTime(start * self.animation.duration())
self.update()
def stop(self):
self.animation.stop()
self.update()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHints(painter.Antialiasing)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
size = self.diameter
if self.relative:
size *= min(self.width(), self.height())
rect = QRectF(0, 0, size, size)
rect.moveCenter(QRectF(self.rect()).center())
path = QPainterPath()
path.addEllipse(rect)
painter.drawPath(path)
if self.sprite:
pos = self.animation.currentValue()
if not self.clockwise:
pos = 1 - pos
pos = path.pointAtPercent(pos)
painter.drawPixmap(pos + self.offset, self.sprite)
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.title = "Drawing Ellipse"
self.top = 200
self.left = 200
self._width = 800
self._height = 800
self.InitWindow()
def InitWindow(self):
self.setWindowIcon(QIcon("icon.png"))
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self._width, self._height)
self.circleWidget = CirclingWidget(.5, sprite='image.png',
anglesPerSecond=60, alignment=Qt.AlignCenter)
self.setCentralWidget(self.circleWidget)
self.circleWidget.start(90)
self.show()
if __name__ == '__main__':
import sys
App = QApplication(sys.argv)
window = Window()
sys.exit(App.exec())
Finally, avoid mixed import styles. The following lines might lead to confusion:
from PyQt5 import QtGui
from PyQt5.QtGui import QPainter, QPen, QBrush
You either import QtGui, or you import the individual classes of that submodule.

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