I want to create visual hints for users on their screens, but I got struck finding a simple solution how to do basic drawings straight on the screen without limiting the user actions (under Windows with Python 3.x).
After a search the only - not properly working - solution I found was using wxPython. Here's the code:
import wx
# init
app=wx.App()
dc=wx.ScreenDC()
# set line and fill style
dc.SetBrush(wx.TRANSPARENT_BRUSH)
dc.SetPen(wx.Pen((255, 0, 0), width=3, style=wx.PENSTYLE_SOLID))
# draw (x, y, width, height)
dc.DrawRectangle(100, 100, 200, 100)
The code draws on the screen, but the result barely becomes visible as the screen is redrawn (by Windows) very quick. I tried a work-around in repeating the drawing command with a for-loop, but also the flickering rectangle is barely visible (and this is nothing that I like to show to my clients).
A bit better (close to sufficient) is using a transparent TKinter window (without header) and display it for a - shorter - period of time. Here's the WORKING code of that (with one downside that is explained below the code):
from tkinter import *
def HighlightSection(Rect=(100,100,300,200), Color = 'red', Duration = 3):
win= Tk()
GeometryString = str(Rect[0])+'x'+str(Rect[1])+'+' \
+str(Rect[2])+'+'+str(Rect[3])
win.geometry(GeometryString) # "200x100+300+250" # breite, höhe, x, y #
win.configure(background=Color)
win.overrideredirect(1)
win.attributes('-alpha', 0.3)
win.wm_attributes('-topmost', 1)
win.after(Duration * 1000, lambda: win.destroy())
win.mainloop()
One thing here I could not make working: Any chance to make this TKinter "window" click-trough? Then this would be sufficient (close to quite good). As long as it is not click-trough the user cannot act in/under the highlighted area!
Is there a simple, solid solution to make draws (line, rectangles, text) on the screen and take it off again after a defined period of time? Any help is appreciated! Thanks in advance, Ulrich!
I believe drawing anything to the screen requires a window. The window can be partially transparent, but it needs to exist.
Something like this with PyQt5 may work for you. This will give you a transparent main window and draws a couple of lines:
import sys
from PyQt5.QtGui import QPainter, QPen
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtCore import Qt
class Clear(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setGeometry(300, 300, 280, 270)
self.setStyleSheet("background:transparent")
self.setAttribute(Qt.WA_TranslucentBackground)
self.show()
def paintEvent(self, e):
qp = QPainter()
qp.begin(self)
self.drawLines(qp)
qp.end()
def drawLines(self, qp):
pen = QPen(Qt.blue, 2, Qt.SolidLine)
qp.setPen(pen)
qp.drawLine(20, 40, 250, 40)
pen.setStyle(Qt.DashLine)
qp.setPen(pen)
qp.drawLine(20, 80, 250, 80)
pen.setStyle(Qt.DashDotLine)
qp.setPen(pen)
qp.drawLine(20, 120, 250, 120)
pen.setStyle(Qt.DotLine)
qp.setPen(pen)
qp.drawLine(20, 160, 250, 160)
pen.setStyle(Qt.DashDotDotLine)
qp.setPen(pen)
qp.drawLine(20, 200, 250, 200)
pen.setStyle(Qt.CustomDashLine)
pen.setDashPattern([1, 4, 5, 4])
qp.setPen(pen)
qp.drawLine(20, 240, 250, 240)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Clear()
sys.exit(app.exec_())
Reference Source: http://zetcode.com/gui/pyqt5/painting/ - I modified it a bit to adjust the transparency.
This works, tested on macOS with Python 3.x.
from PyQt5.QtGui import (QPainter,
QPen,
QColor)
from PyQt5.QtWidgets import (QMainWindow,
QApplication)
from PyQt5.QtCore import (Qt,
QCoreApplication,
QTimer)
class TransparentWindow(QMainWindow):
def __init__(
self,
x: int,
y: int,
width: int,
height: int,
pen_color: str,
pen_size: int):
super().__init__()
self.highlight_x = x
self.highlight_y = y
self.highlight_width = width
self.highlight_height = height
self.pen_color = pen_color
self.pen_size = pen_size
self.initUI()
def initUI(self):
"""Initialize the user interface of the window."""
self.setGeometry(
self.highlight_x,
self.highlight_y,
self.highlight_width + self.pen_size,
self.highlight_height + self.pen_size)
self.setStyleSheet('background: transparent')
self.setWindowFlag(Qt.FramelessWindowHint)
def paintEvent(self, event):
"""Paint the user interface."""
painter = QPainter()
painter.begin(self)
painter.setPen(QPen(QColor(self.pen_color), self.pen_size))
painter.drawRect(
self.pen_size - 1,
self.pen_size - 1,
self.width() - 2 * self.pen_size,
self. height() - 2 * self.pen_size)
painter.end()
def highlight_on_screen(
x: int,
y: int,
width: int,
height: int,
pen_color: str = '#aaaa00',
pen_size: int = 2,
timeout: int = 2):
"""Highlights an area as a rectangle on the main screen.
`x` x position of the rectangle
`y` y position of the rectangle
`width` width of the rectangle
`height` height of the rectangle
`pen_color` Optional: color of the rectangle as a hex value;
defaults to `#aaaa00`
`pen_size` Optional: border size of the rectangle; defaults to 2
`timeout` Optional: time in seconds the rectangle
disappears; defaults to 2 seconds
"""
app = QApplication([])
window = TransparentWindow(x, y, width, height, pen_color, pen_size)
window.show()
QTimer.singleShot(timeout * 1000, QCoreApplication.quit)
app.exec_()
highlight_on_screen(0, 0, 100, 100)
Resulting rectangle on screen
However, on macOS it is not possible to draw a window over the app bar. That shouldn't be an issue on Windows.
Related
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)
I'm looking for a solution, to animate this arc from 0 - 360°. I'm relative new to Pyside/Pyqt and I don't find such a simple solution (only beginner "unfriedly"). I tried it with while loops aswell, but it doesn't works. At the moment I don't understand this animation system, but I want to work on it.
import sys
from PySide6 import QtCore
from PySide6.QtWidgets import QApplication, QMainWindow
from PySide6.QtCore import Qt
from PySide6.QtGui import QBrush, QPen, QPainter
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("AnimateArc")
self.setGeometry(100, 100, 600, 600)
def paintEvent(self, event):
self.anim = QtCore.QPropertyAnimation(self, b"width", duration=1000) #<- is there a documentation for b"width", b"geometry"?
self.anim.setStartValue(0)
start = 0
painter = QPainter(self)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.drawArc(100, 100, 400, 400, 90 * 16, start * 16) # I want to make the change dynamicly
self.anim.setEndValue(360)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = MainWindow()
w.show()
app.exec()
QPropertyAnimation is used to animate Qt properties of any QObject. If you refer to self (the current instance of QMainWindow), then you can animate all properties of a QMainWindow and all inherited properties (QMainWindow inherits from QWidget, so you can animate all the QWidget properties as well).
In your case, you're trying to animate the width property of the window, and that's certainly not what you want to do.
Since what you want to change is a value that is not a property of the window, you cannot use QPropertyAnimation (unless you create a Qt property using the #Property decorator), and you should use a QVariantAnimation instead.
Then, a paintEvent is called by Qt every time the widget is going to be drawn (which can happen very often), so you cannot create the animation there, otherwise you could end up with a recursion: since the animation would require a repaint, you would create a new animation everytime the previous requires an update.
Finally, consider that painting on a QMainWindow is normally discouraged, as a Qt main window is a special kind of QWidget intended for advanced features (menus, status bar, etc) and uses a central widget to show the actual contents.
The correct approach is to create and set a central widget, and implement the painting on that widget instead.
Here is a revised and working version of your code:
class ArcWidget(QWidget):
def __init__(self):
super().__init__()
self.anim = QtCore.QVariantAnimation(self, duration=1000)
self.anim.setStartValue(0)
self.anim.setEndValue(360)
self.anim.valueChanged.connect(self.update)
self.anim.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setPen(QPen(Qt.black, 5, Qt.SolidLine))
painter.drawArc(
100, 100, 400, 400, 90 * 16, self.anim.currentValue() * 16)
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle("AnimateArc")
self.setGeometry(100, 100, 600, 600)
self.arcWidget = ArcWidget()
self.setCentralWidget(self.arcWidget)
The valueChanged connection ensures that everytime the value changes the widget schedules an update (thus calling a paintEvent as soon as the event queue allows it), then you can use the current value of the animation to draw the actual arc.
Thanks #musicamante for the solution regarding animating arc using QAnimationProperty
Modified #musicmante code to create a loading effect guess it will help developers and might save their time who are trying to make loading effect using Qt
Source
#!/usr/bin/env python3.10
import sys
import string
import random
from PySide6.QtWidgets import (QMainWindow, QPushButton, QVBoxLayout,
QApplication, QWidget)
from PySide6.QtCore import (Qt, QVariantAnimation)
from PySide6.QtGui import (QPen, QPainter, QColor)
class Arc:
colors = list(string.ascii_lowercase[0:6]+string.digits)
shades_of_blue = ["#7CB9E8","#00308F","#72A0C1", "#F0F8FF",
"#007FFF", "#6CB4EE", "#002D62", "#5072A7",
"#002244", "#B2FFFF", "#6F00FF", "#7DF9FF","#007791",
"#ADD8E6", "#E0FFFF", "#005f69", "#76ABDF",
"#6A5ACD", "#008080", "#1da1f2", "#1a1f71", "#0C2340"]
shades_of_green = ['#32CD32', '#CAE00D', '#9EFD38', '#568203', '#93C572',
'#8DB600', '#708238', '#556B2F', '#014421', '#98FB98', '#7CFC00',
'#4F7942', '#009E60', '#00FF7F', '#00FA9A', '#177245', '#2E8B57',
'#3CB371', '#A7F432', '#123524', '#5E8C31', '#90EE90', '#03C03C',
'#66FF00', '#006600', '#D9E650']
def __init__(self):
self.diameter = random.randint(100, 600)
#cols = list(Arc.colors)
#random.shuffle(cols)
#_col = "#"+''.join(cols[:6])
#print(f"{_col=}")
#self.color = QColor(_col)
#self.color = QColor(Arc.shades_of_blue[random.randint(0, len(Arc.shades_of_blue)-1)])
self.color = QColor(Arc.shades_of_green[random.randint(0, len(Arc.shades_of_green)-1)])
#print(f"{self.color=}")
self.span = random.randint(40, 150)
self.direction = 1 if random.randint(10, 15)%2 == 0 else -1
self.startAngle = random.randint(40, 200)
self.step = random.randint(100, 300)
class ArcWidget(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.arcs = [Arc() for i in range(random.randint(10, 20))]
self.startAnime()
def initUI(self):
#self.setAutoFillBackground(True)
self.setAttribute(Qt.WA_StyledBackground, True)
self.setStyleSheet("background-color:black;")
def startAnime(self):
self.anim = QVariantAnimation(self, duration = 2000)
self.anim.setStartValue(0)
self.anim.setEndValue(360)
self.anim.valueChanged.connect(self.update)
self.anim.start()
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
#painter.setPen(QPen(QColor("#b6faec"), 5, Qt.SolidLine))
#painter.drawArc(
# 100, 100, 400, 400, 90*16,
# self.anim.currentValue() * 16)
#width = 400
#height = 400
#painter.drawArc(self.width()/2 -width/2, self.height()/2 - height/2, 400, 400, self.anim.currentValue()*16, 45*16)
for arc in self.arcs:
painter.setPen(QPen(arc.color, 6, Qt.SolidLine))
painter.drawArc(self.width()/2 - arc.diameter/2,
self.height()/2 - arc.diameter/2, arc.diameter,
arc.diameter, self.anim.currentValue()*16*arc.direction+arc.startAngle*100, arc.span*16)
#print(f"currentValue : {self.anim.currentValue()}")
#arc.startAngle = random.randint(50, 200)
if self.anim.currentValue() == 360:
#print("360")
self.startAnime()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Animate Arc")
self.setGeometry(100, 100, 600, 600)
self.arcWidget = ArcWidget()
self.setCentralWidget(self.arcWidget)
if __name__ == "__main__":
app = QApplication(sys.argv)
mainWindow = MainWindow()
mainWindow.show()
app.exec()
output:
$ ./arc_widget.py
I have a custom LED indicator widget(from github), which looks like this on QMainWindow:
I have an image which I can put in the backgroud of the QMainWindow like this:
Now my quesion is, How do I put the LED indicator widget OVER the background image(I want to put them in all the boxes)? The LED indicator widget is not showing up at all when I put a background image in the program.
This is the code:
import sys
from LedIndicatorWidget import *
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import QMainWindow, QLabel, QCheckBox, QWidget
from PyQt5.QtCore import QSize
class ExampleWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.setMinimumSize(QSize(300, 300))
self.setWindowTitle("Checkbox")
oImage = QImage("map.png")
sImage = oImage.scaled(QSize(800,277))
palette = QPalette()
palette.setBrush(QPalette.Window, QBrush(sImage))
self.setPalette(palette)
self.show()
self.led = LedIndicator(self)
self.led.setDisabled(True)
self.led.move(10,20)
self.led.resize(100,100)
self.led1 = LedIndicator(self)
self.led1.setDisabled(True)
self.led1.move(150,20)
self.led1.resize(100,100)
self.led2 = LedIndicator(self)
self.led2.setDisabled(True)
self.led2.move(300,20)
self.led2.resize(100,100)
self.timer = QtCore.QTimer()
self.timer.timeout.connect(self.onPressButton)
self.timer.start()
def onPressButton(self):
self.led.setChecked(not self.led.isChecked())
self.timer.stop()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
mainWin = ExampleWindow()
mainWin.show()
sys.exit( app.exec_() )
I've used Qpalette for the background image, if you have a better idea to add the image and make it work, feel free to do so, because I couldn't find one.
To use the LED indicator widget, make a file "LedIndicatorWidget.py" and copy this code:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class LedIndicator(QAbstractButton):
scaledSize = 1000.0
def __init__(self, parent=None):
QAbstractButton.__init__(self, parent)
self.setMinimumSize(24, 24)
self.setCheckable(True)
# Green
self.on_color_1 = QColor(0, 255, 0)
self.on_color_2 = QColor(0, 192, 0)
self.off_color_1 = QColor(0, 28, 0)
self.off_color_2 = QColor(0, 128, 0)
def resizeEvent(self, QResizeEvent):
self.update()
def paintEvent(self, QPaintEvent):
realSize = min(self.width(), self.height())
painter = QPainter(self)
pen = QPen(Qt.black)
pen.setWidth(1)
painter.setRenderHint(QPainter.Antialiasing)
painter.translate(self.width() / 2, self.height() / 2)
painter.scale(realSize / self.scaledSize, realSize / self.scaledSize)
gradient = QRadialGradient(QPointF(-500, -500), 1500, QPointF(-500, -500))
gradient.setColorAt(0, QColor(224, 224, 224))
gradient.setColorAt(1, QColor(28, 28, 28))
painter.setPen(pen)
painter.setBrush(QBrush(gradient))
painter.drawEllipse(QPointF(0, 0), 500, 500)
gradient = QRadialGradient(QPointF(500, 500), 1500, QPointF(500, 500))
gradient.setColorAt(0, QColor(224, 224, 224))
gradient.setColorAt(1, QColor(28, 28, 28))
painter.setPen(pen)
painter.setBrush(QBrush(gradient))
painter.drawEllipse(QPointF(0, 0), 450, 450)
painter.setPen(pen)
if self.isChecked():
gradient = QRadialGradient(QPointF(-500, -500), 1500, QPointF(-500, -500))
gradient.setColorAt(0, self.on_color_1)
gradient.setColorAt(1, self.on_color_2)
else:
gradient = QRadialGradient(QPointF(500, 500), 1500, QPointF(500, 500))
gradient.setColorAt(0, self.off_color_1)
gradient.setColorAt(1, self.off_color_2)
painter.setBrush(gradient)
painter.drawEllipse(QPointF(0, 0), 400, 400)
#pyqtProperty(QColor)
def onColor1(self):
return self.on_color_1
#onColor1.setter
def onColor1(self, color):
self.on_color_1 = color
#pyqtProperty(QColor)
def onColor2(self):
return self.on_color_2
#onColor2.setter
def onColor2(self, color):
self.on_color_2 = color
#pyqtProperty(QColor)
def offColor1(self):
return self.off_color_1
#offColor1.setter
def offColor1(self, color):
self.off_color_1 = color
#pyqtProperty(QColor)
def offColor2(self):
return self.off_color_2
#offColor2.setter
def offColor2(self, color):
self.off_color_2 = color
The issue is not related to the background: the LED widgets are there, the problem is that widgets added to a parent that is already shown (and without using a layout manager) does not make them visible, and they must be explicitly shown by calling show() or setVisible(True).
You can see the difference if you remove the self.show() line after setting the palette (but leaving the mainWin.show() at the end): in that case, the leds become automatically visible.
The solution is to either show the child widgets explicitly, or call show()/setVisible(True) on the parent after adding them.
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).
I am totally new to PyQt. I want to do animation using PyQt5 .This is a simple test I am doing , so I am just trying to move a rectangle from top to the bottom of the window. Here's a gist of what I am doing to achieve this.
1. I have put whatever I wanted to paint inside paintEvent() method. I have painted the rectangle using variables not constant values
2. I have also created a update() function to update all the variables
3. I have created a loop function which calls self.update() and self.repaint() every 100 milliseconds
import sys
import random
from PyQt5.QtWidgets import ( QApplication, QWidget, QToolTip, QMainWindow)
from PyQt5.QtGui import QPainter, QBrush, QPen, QColor, QFont
from PyQt5.QtCore import Qt, QDateTime
class rain_animation(QMainWindow):
def __init__(self):
super().__init__()
self.painter = QPainter()
""" Variables for the Window """
self.x = 50
self.y = 50
self.width = 500
self.height = 500
"""Variables for the rain"""
self.rain_x = self.width/2
self.rain_y = 0
self.rain_width = 5
self.rain_height = 30
self.rain_vel_x = 0
self.rain_vel_y = 5
self.start()
self.loop()
def paintEvent(self, a0):
self.painter.begin(self)
# Draw a White Background
self.painter.setPen(QPen(Qt.white, 5, Qt.SolidLine))
self.painter.setBrush(QBrush(Qt.white, Qt.SolidPattern))
self.painter.drawRect(0, 0, self.width, self.height)
#Draw the rain
self.painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine))
self.painter.setBrush(QBrush(Qt.blue, Qt.SolidPattern))
self.painter.drawRect(self.rain_x, self.rain_y, self.rain_width, self.rain_height)
self.painter.end(self)
def update(self, diff):
self.rain_x += self.rain_vel_x
self.rain_y += self.rain_vel_y
def start(self):
self.setWindowTitle("Rain Animation")
self.setGeometry(self.x, self.y, self.width, self.height)
self.show()
def loop(self):
start = QDateTime.currentDateTime()
while True :
diff = start.msecsTo(QDateTime.currentDateTime())
if diff >= 100 :
print("time : {0} ms rain_x : {1} rain_y : {2}".format(diff, self.rain_x, self.rain_y))
start = QDateTime.currentDateTime()
self.update(diff)
self.repaint()
if __name__ == "__main__":
app = QApplication(sys.argv)
animation = rain_animation()
sys.exit(app.exec_())
What I should see is a rectangle moving from the top of window to the bottom of the screen but all I see is a window with a black background.
The loop() function seems working properly since the data I am printing shows that the variables are being updated every 100 milliseconds.
Though the problem seems to be something in the loop() function since after removing the self.loop() I can see a static picture of the blue box with a white background at the top of the window.
Problem:
Having a continuous loop does not allow the GUI to perform tasks such as painting, interaction with the OS, etc. Each GUI provides a way to make animations in a way that does not block the window.
Qt provides various classes that allow you to implement the animation as:
QTimer,
QTimeLine,
QVariantAnimation,
QPropertyAnimation.
On the other hand it is recommended that:
Do not create a QPainter outside of paintEvent if it is going to be responsible for the GUI painting.
Use the update() method (not your method but the one that provides Qt) instead of repaint, in case repaint will force the window to paint sometimes unnecessarily, instead update() will do it when necessary, remember that the painted is done with the refresh rate of the screen (60 Hz). For example, if you call repaint 5 times in 20 ms then paintEvent() will be called 3 times but the painting on the screen is every 16.6ms so you only need 1 paint, in the case of update() if you consider it.
Considering the above, it is best to use a QPropertyAnimation:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class RainAnimation(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Rain Animation")
self.setGeometry(50, 50, 500, 500)
self.m_rect_rain = QtCore.QRect()
animation = QtCore.QPropertyAnimation(
self,
b"rect_rain",
parent=self,
startValue=QtCore.QRect(self.width() / 2, 0, 5, 30),
endValue=QtCore.QRect(self.width() / 2, self.height() - 30, 5, 30),
duration=5 * 1000,
)
animation.start()
def paintEvent(self, a0):
painter = QtGui.QPainter(self)
# Draw a White Background
painter.setPen(QtGui.QPen(QtCore.Qt.white, 5, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect())
#Draw the rain
painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect_rain)
#QtCore.pyqtProperty(QtCore.QRect)
def rect_rain(self):
return self.m_rect_rain
#rect_rain.setter
def rect_rain(self, r):
self.m_rect_rain = r
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = RainAnimation()
w.show()
sys.exit(app.exec_())
Another option is use QVarianAnimation:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class RainAnimation(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Rain Animation")
self.setGeometry(50, 50, 500, 500)
self.m_rect_rain = QtCore.QRect()
animation = QtCore.QVariantAnimation(
parent=self,
startValue=QtCore.QRect(self.width() / 2, 0, 5, 30),
endValue=QtCore.QRect(self.width() / 2, self.height() - 30, 5, 30),
duration=5 * 1000,
valueChanged=self.set_rect_rain,
)
animation.start()
def paintEvent(self, a0):
painter = QtGui.QPainter(self)
# Draw a White Background
painter.setPen(QtGui.QPen(QtCore.Qt.white, 5, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect())
# Draw the rain
painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue, QtCore.Qt.SolidPattern))
painter.drawRect(self.m_rect_rain)
#QtCore.pyqtSlot(QtCore.QVariant)
def set_rect_rain(self, r):
self.m_rect_rain = r
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = RainAnimation()
w.show()
sys.exit(app.exec_())
The following example is using your logic but with a QTimer:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class RainAnimation(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Rain Animation")
self.setGeometry(50, 50, 500, 500)
self.m_rect_rain = QtCore.QRect(self.width() / 2, 0, 5, 30)
timer = QtCore.QTimer(self, timeout=self.update_rain, interval=100)
timer.start()
def paintEvent(self, a0):
painter = QtGui.QPainter(self)
# Draw a White Background
painter.setPen(QtGui.QPen(QtCore.Qt.white, 5, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
painter.drawRect(self.rect())
# Draw the rain
painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
painter.setBrush(QtGui.QBrush(QtCore.Qt.blue, QtCore.Qt.SolidPattern))
painter.drawRect(self.m_rect_rain)
#QtCore.pyqtSlot()
def update_rain(self):
self.m_rect_rain.moveTop(self.m_rect_rain.top() + 5)
self.update()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
w = RainAnimation()
w.show()
sys.exit(app.exec_())