I have found that QTabWidget exhibits a really strange behaviour when it comes to repainting its children. The following code is the minimal example that I created to observe this problem.
Suppose I have a custom widget (a drawing) that has costly paint event hadler. So I need to minimize the number of times it is repainted. But the problem is that if this drawing widget is inside QTabWidget it receives many unnecesary repaint requests as a response to changes in some other totally unrelated widgets (in the example below - the button and label).
At the end of the example you can change variable withTabWidget. If it is set to True, the drawing is repainted (i.e. the number in the drawing is increased) every time the button is pressed. If it is set to False, the drawing is not repainted (the number in the drawing stays the same) when the button is pressed, which is the correct behaviour I need. But obviously, I also need to use the tab widget in my app layout...
Any idea how to make the tab widget work correctly, without unnecessary repaints?
Note: Qt 4.8.4 (tested on both 32 and 64-bits), PySide 1.2.1., Windows 7
import sys
from PySide import QtGui
class MyDrawing(QtGui.QLabel):
# drawing is a widget which has user defined paint event handler
# the painting can be costly so we must avoid unnecessary repainting
repaintCount = 0
def paintEvent(self, event):
# a very simple example which count ...
# the number of times the paintEvent was executed
self.repaintCount += 1
painter = QtGui.QPainter(self)
painter.drawText(10, 10, str(self.repaintCount))
class MyContainer(QtGui.QWidget):
def __init__(self, parent=None):
super(MyContainer, self).__init__(parent)
layout = QtGui.QHBoxLayout(self)
# there is a button ...
self.button = QtGui.QPushButton("Press me.")
self.button.setCheckable(True)
self.button.pressed.connect(self.onButtonPressed)
layout.addWidget(self.button)
# .. and a label ...
self.label = QtGui.QLabel()
layout.addWidget(self.label)
# ... and a drawing
self.drawing = MyDrawing()
layout.addWidget(self.drawing)
def onButtonPressed(self):
# pressing button only changes the label
# it does not change the drawing
# so drawing should not be repainted
# but when the container is inside QTabWidget it IS repainted
self.label.setText(str(self.button.isChecked()))
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
form = QtGui.QMainWindow()
# change this True/False!!!
withTabWidget = True
# True = problem: MyDrawing IS repainted when button1 is pressed
# False = OK: MyDrawing IS NOT repainted when button1 is pressed
if withTabWidget:
tabWidget = QtGui.QTabWidget()
tabWidget.addTab(MyContainer(), "Tab1")
form.setCentralWidget(tabWidget)
else:
form.setCentralWidget(MyContainer())
form.show()
result = app.exec_()
sys.exit(result)
Update: By tracking the events on QTabWidget, I discovered, that when the label is changed, some animation object is inserted (child is added) into the QTabWidget. And the QTabWidget responds by repainting the whole area it covers. This is different with other widget types, when the animation object is inserted, it does not repaint its whole area. Unfortunately I still have no workaround to solve it. Maybe will have make my own TabWidget which I would like to avoid at all costs.
Update2: another interesting aspect is that when I start the example application above with the QTabWidget, the number which is drawn in the drawing is 2. When I start it without QTabWidget, the number is 1. So it is another example of unnecessary repainting.
Related
I'm working on a desktop application for windows using PyQt and Qt creator.
What I want
I want to display messages to the user only when the user gave an input. I also wanted the message to draw the eye, so I'm going for the following animated solution:
A frame that's hidden when not required (with height = 0 and width = the app's width), 'grows' from the bottom of the app when needed, stays visible for 5-6 seconds, then retracts back to the bottom.
The app kind of looks like this without the message:
And kind of like this when the message IS displayed (note how the bottom gray element is 'covered' by the message):
What I tried
So the way I did this was to create what I called "footer frame", which contains another frame that I call "message frame". The message frame contains a label that will hold, in time, the message for the user. Everything has pre-determined height, so to hide the whole thing I set the message frame to have a maximum height of 0.
So for the 'growing' animation I animated the message frame's maximumHeight property.
The current problem
THING IS - since I wanted the app to be responsive I put everything in layouts... and because of that, whenever the message is displayed, the rest of the components are 'compressed' in height.
kind of like this (note how the bottom gray element is not covered by the message, but all the elements' heights shrink a little):
Instead, I wanted the messsage to 'cover' whatever is located under the message's coordinates.
I tried to animate the geometry of the message frame, but nothing really happened - probably because the minimum height is still 0. So I tried to change the minimum height right before the animation begins; But that led to that compression again.
Tried to do the same with the footer frame, with the same results.
My question is : What is the best / preferred way of achieving the result I intend with Qt?
Layout managers always try to show all widgets they're managing. If you want a widget to overlap others, you cannot put it inside a layout, you just create the widget with a parent, and that parent will probably be the widget containing the layout above or the top level window.
This cannot be done in Designer/Creator, as it's assumed that once a layout has been set for a parent widget, all child widgets will be managed by that layout. The only solution is to do this programmatically.
In the following example I'm assuming a QMainWindow is used, so the reference parent widget is actually the central widget, not the QMainWindow: that's because the alert should not cover other widgets that are part of a main window's layout, like the status bar or a bottom placed tool bar or dock).
The animation is actually a QSequentialAnimationGroup that shows the rectangle, waits a few seconds, and hides it again. Since the window could be resized while the animation is running, a helper function is used to properly update the start and end values of the warning and eventually update the geometry when in the "paused" state (which is actually a QPauseAnimation); in order to do so, an event filter is installed on the central widget.
from random import randrange
from PyQt5 import QtCore, QtWidgets, uic
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
uic.loadUi('overlay.ui', self)
self.alerts = []
self.centralWidget().installEventFilter(self)
self.pushButton.clicked.connect(self.showAlert)
QtCore.QTimer.singleShot(2000, self.showAlert)
def showAlert(self, message=None, timeout=250):
# create an alert that is a child of the central widget
alert = QtWidgets.QLabel(message or 'Some message to the user',
self.centralWidget(), wordWrap=True,
alignment=QtCore.Qt.AlignCenter,
styleSheet='background: rgb({}, {}, {});'.format(
randrange(192, 255), randrange(192, 255), randrange(192, 255)))
self.alerts.append(alert)
alert.animation = QtCore.QSequentialAnimationGroup(alert)
alert.animation.addAnimation(QtCore.QPropertyAnimation(
alert, b'geometry', duration=timeout))
alert.animation.addAnimation(QtCore.QPauseAnimation(3000))
alert.animation.addAnimation(QtCore.QPropertyAnimation(
alert, b'geometry', duration=timeout))
# delete the alert when the animation finishes
def deleteLater():
self.alerts.remove(alert)
alert.deleteLater()
alert.animation.finished.connect(deleteLater)
# update all animations, including the new one; this is not very
# performant, as it also updates all existing alerts; it is
# just done for simplicity;
self.updateAnimations()
# set the start geometry of the alert, show it, and start
# the new animation
alert.setGeometry(alert.animation.animationAt(0).startValue())
alert.show()
alert.animation.start()
def updateAnimations(self):
width = self.centralWidget().width() - 20
y = self.centralWidget().height()
margin = self.fontMetrics().height() * 2
for alert in self.alerts:
height = alert.heightForWidth(width) + margin
startRect = QtCore.QRect(10, y, width, height)
endRect = startRect.translated(0, -height)
alert.animation.animationAt(0).setStartValue(startRect)
alert.animation.animationAt(0).setEndValue(endRect)
alert.animation.animationAt(2).setStartValue(endRect)
alert.animation.animationAt(2).setEndValue(startRect)
def eventFilter(self, obj, event):
if obj == self.centralWidget() and event.type() == event.Resize and self.alerts:
self.updateAnimations()
for alert in self.alerts:
ani = alert.animation
# if the animation is "paused", update the geometry
if isinstance(ani.currentAnimation(), QtCore.QPauseAnimation):
alert.setGeometry(ani.animationAt(0).endValue())
return super().eventFilter(obj, event)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec())
i have started a new project in python using pygame and for the background i want the bottom half filled with gray and the top black. i have used rect drawing in projects before but for some reason it seems to be broken? i don't know what i am doing wrong. the weirdest thing is that the result is different every time i run the program. sometimes there is only a black screen and sometimes a gray rectangle covers part of the screen, but never half of the screen.
import pygame, sys
from pygame.locals import *
pygame.init()
DISPLAY=pygame.display.set_mode((800,800))
pygame.display.set_caption("thing")
pygame.draw.rect(DISPLAY, (200,200,200), pygame.Rect(0,400,800,400))
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
You need to update the display.
You are actually drawing on a Surface object. If you draw on the Surface associated to the PyGame display, this is not immediately visible in the display. The changes become visibel, when the display is updated with either pygame.display.update() or pygame.display.flip().
See pygame.display.flip():
This will update the contents of the entire display.
While pygame.display.flip() will update the contents of the entire display, pygame.display.update() allows updating only a portion of the screen to updated, instead of the entire area. pygame.display.update() is an optimized version of pygame.display.flip() for software displays, but doesn't work for hardware accelerated displays.
The typical PyGame application loop has to:
handle the events by calling either pygame.event.pump() or pygame.event.get().
update the game states and positions of objects dependent on the input events and time (respectively frames)
clear the entire display or draw the background
draw the entire scene (draw all the objects)
update the display by calling either pygame.display.update() or pygame.display.flip()
limit frames per second to limit CPU usage with pygame.time.Clock.tick
import pygame
from pygame.locals import *
pygame.init()
DISPLAY = pygame.display.set_mode((800,800))
pygame.display.set_caption("thing")
clock = pygame.time.Clock()
run = True
while run:
# handle events
for event in pygame.event.get():
if event.type == QUIT:
run = False
# clear display
DISPLAY.fill(0)
# draw scene
pygame.draw.rect(DISPLAY, (200,200,200), pygame.Rect(0,400,800,400))
# update display
pygame.display.flip()
# limit frames per second
clock.tick(60)
pygame.quit()
exit()
repl.it/#Rabbid76/PyGame-MinimalApplicationLoop See also Event and application loop
simply change your code to:
import pygame, sys
from pygame.locals import *
pygame.init()
DISPLAY=pygame.display.set_mode((800,800))
pygame.display.set_caption("thing")
pygame.draw.rect(DISPLAY, (200,200,200), pygame.Rect(0,400,800,400))
pygame.display.flip() #Refreshing screen
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
it should help
This question already has answers here:
Equivalent to time.sleep for a PyQt application
(5 answers)
Closed 2 years ago.
im new to pyqt5,i tried to open dialog and push some text into that dialog
my dialog contain one plaintext ,progressbar and pushbutton
when i run the code its popup the dialog but not shown any thing ,after code execution completes its showing all the widgets and with text
but i need to open the dialog and i want update progress bar
My code
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import (QDialog,QPlainTextEdit,QScrollArea,QProgressBar,QPushButton)
import sys
import time
class PrograssDialog():
def ShowDialog(self,Dialogs):
try:
self.Pd=Dialogs
self.Pd.setWindowTitle("Script Excution... ")
self.Pd.resize(500,500)
self.ScrArea=QScrollArea(self.Pd)
self.ScrArea.move(0,0)
self.ScrArea.resize(500,300)
self.TextArea=QPlainTextEdit(self.Pd)
self.TextArea.move(0,0)
self.TextArea.resize(500,300)
self.TextArea.insertPlainText(str("Start : %s" % time.ctime())+"\n")
self.Prograssbar=QProgressBar(self.Pd)
self.Prograssbar.setGeometry(QtCore.QRect(0, 350, 450, 23))
self.Prograssbar.setMaximum(100)
self.Cancelbutton=QPushButton("Cancel",self.Pd)
self.Cancelbutton.setGeometry(QtCore.QRect(360, 400, 93, 28))
self.Cancelbutton.clicked.connect(self.StopExcution)
self.Pd.show()
except Exception as msg:
import sys
tb = sys.exc_info()[2]
print("Error_analysis " + str(msg)+ str(tb.tb_lineno))
def AddMessage(self,Message):
self.TextArea.insertPlainText(str(Message)+"\n")
# print("message added")
def SetPercentage(self,Number):
self.Prograssbar.setValue(Number)
# print("percent added")
def StopExcution(self):
sys.exit()
app = QApplication(sys.argv)
ui=PrograssDialog()
ui.ShowDialog(QDialog())
for i in range(100):
ui.AddMessage("Hello")
ui.SetPercentage(i)
time.sleep(0.5)
sys.exit(app.exec_())
There are various problems with your code, I'll try to address all of them.
The main reason for the issue you are facing is that no blocking functions (like time.sleep) should happen in the main Qt thread (which is the thread that shows the GUI elements and allow interactions with them); blocking functions prevent the UI to correctly draw and refresh its contents, if you want to do an operation at specific intervals, you have to use a QTimer;
You should not use a basic python object subclass for this kind of situations, especially since you're only using just one dialog; you should subclass from QDialog instead and implement
To "exit" your program you should not use sys.exit (you are already using it), but use QApplication.quit() instead; also, since you already imported sys at the beginning, there's no need to import it again in the exception;
Function and variable names should not be capitalized; while you can use any casing style you want for your own code, it's common (and highly suggested) practice to always use lowercase initials, and it's also a convention you should stick to when sharing code with others, especially on Q&A sites like StackOverflow; read more on the official Style Guide for Python Code;
Always avoid fixed geometries for children widgets: what others see on their computers will probably be very different from what you see on yours, and you might end up with an unusable interface; use layout managers instead, so that the widgets can resize themselves if required;
You added a scroll area but you never use it; since you're using the same geometry for the text area I believe that you thought you were using for that, but there's no need as the text area already is a scroll area;
Here is how the code could look like in order to achieve what you want:
import time
from PyQt5 import QtCore, QtWidgets
class ProgressDialog(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
layout = QtWidgets.QVBoxLayout(self)
self.textArea = QtWidgets.QPlainTextEdit()
layout.addWidget(self.textArea)
self.textArea.insertPlainText(str("Start : %s" % time.ctime())+"\n")
self.textArea.setReadOnly(True)
self.progressBar = QtWidgets.QProgressBar()
layout.addWidget(self.progressBar)
self.cancelButton = QtWidgets.QPushButton('Cancel')
layout.addWidget(self.cancelButton)
self.cancelButton.clicked.connect(QtWidgets.QApplication.quit)
self.countTimer = QtCore.QTimer()
self.countTimer.timeout.connect(self.timeout)
def startCounter(self, maximum, sleepSeconds):
self.progressBar.reset()
self.progressBar.setMaximum(maximum)
# QTimer interval is in milliseconds
self.countTimer.setInterval(sleepSeconds * 1000)
self.countTimer.start()
def timeout(self):
if self.progressBar.value() == self.progressBar.maximum():
self.countTimer.stop()
return
self.setPercentage(self.progressBar.value() + 1)
self.addMessage('Hello')
def setPercentage(self, value):
self.progressBar.setValue(value)
def addMessage(self, message):
self.textArea.insertPlainText(str(message) + '\n')
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
dialog = ProgressDialog()
dialog.show()
dialog.startCounter(100, .5)
sys.exit(app.exec_())
I am developing a PyQt application for a Windows environment (no cross platform required), in which I need to create custom title bar for a QMainWindow instance. There are suggestion to use self.setWindowFlags(Qt.FramelessWindowHint), but it creates undesired effect of application goes to fullscreen when maximizing it. Actually, there is solution which I believe to be the perfect solution for this case, that is drawing the NCA (Non Client Area) as describe in this page.
At the moment, this is my code:
class MainWindow(QMainWindow, Ui_MainWindow):
def winEvent(self, msg):
if msg.message == win32con.WM_NCPAINT:
self.decorate_window(msg)
return True, 0
return super(MainWindow, self).winEvent(msg)
def decorate_window(self, msg):
painter = QPainter(self)
painter.fillRect(self.rect(), QColor(255,0,0))
But it gives this warning, QPainter::begin: Paint device returned engine == 0, type: 1, and there is no drawing occur except of a white titlebar and border.
Any suggestion?
I want to unittest drag and drop for our widgets. At the moment, I instantiate a QDragEnterEvent, but this is discouraged with a big warning on the Qt documentation, because it relies on the Qt library internal state. In fact, I get segfaults that appear to be due to a violation of this Warning.
Given this premise, how can one test drag and drop behavior?
If using Unix we can use QTest, however to get a cross-platform solution, we can implement a solution where we circumvent Qt.
Using QTest
Although the Qt documentation for drag and drop says that it will not block the main event loop, a closer look at QDrag.exec will reveal that this is not true for Windows.
The call to QTest.mousePress causes the test to block until the mouse is physically moved by the user.
I got around this in Linux by using a timer to schedule the mouse move and release:
def testDragAndDrop(self):
QtCore.QTimer.singleShot(100, self.dropIt)
QtTest.QTest.mousePress(dragFromWidget, QtCore.Qt.LeftButton)
# check for desired behaviour after drop
assert something
def dropIt(self):
QtTest.QTest.mouseMove(dropToWidget)
QtTest.QTest.mouseRelease(dropToWidget, QtCore.Qt.LeftButton, delay=15)
For this solution, it is necessary to include a delay in the mouseRelease call, and to have called show on your widget.
Note that I have verified this works using pyqt4 and Python 2.7 on Fedora 20
Cross-Platform
You can use the mouse manipulation methods from the PyUserInput package. Put the mouse interaction in separate thread to avoid the locking up of the Qt main event loop. We can do this since we are not using Qt at all in our mouse control. Make sure that you have called show on the widgets you are dragging to/from.
from __future__ import division
import sys, time, threading
import numpy as np
from PyQt4 import QtGui, QtCore, QtTest
from pymouse import PyMouse
...
def mouseDrag(source, dest, rate=1000):
"""Simulate a mouse visible mouse drag from source to dest, rate is pixels/second"""
mouse = PyMouse()
mouse.press(*source)
# smooth move from source to dest
npoints = int(np.sqrt((dest[0]-source[0])**2 + (dest[1]-source[1])**2 ) / (rate/1000))
for i in range(npoints):
x = int(source[0] + ((dest[0]-source[0])/npoints)*i)
y = int(source[1] + ((dest[1]-source[1])/npoints)*i)
mouse.move(x,y)
time.sleep(0.001)
mouse.release(*dest)
def center(widget):
midpoint = QtCore.QPoint(widget.width()/2, widget.height()/2)
return widget.mapToGlobal(midpoint)
def testDragAndDrop(self):
# grab the center of the widgets
fromPos = center(dragFromWidget)
toPos = center(dropToWidget)
dragThread = threading.Thread(target=mouseDrag, args=((fromPos.x(),fromPos.y()), (toPos.x(), toPos.y())))
dragThread.start()
# cannot join, use non-blocking wait
while dragThread.is_alive():
QtTest.QTest.qWait(1000)
# check that the drop had the desired effect
assert dropToWidget.hasItemCount() > 0
Note I have tested this using PyQt4 and Python 2.7 on Fedora and Windows 7
Haven't tried, but if your drag & drop process is Qt internal (meaning, you're dragging from and to a Qt widget), QTest might help.
Basically by doing something along the lines:
QTest.mousePress(drag_widget, Qt.LeftButton) # simulate mouse press on whatever you want to drag
QTest.mouseMove(drop_widget) # move the mouse to the target - maybe this can be skipped
QTest.mouseRelease(drop_widget, Qt.LeftButton) # simulate mouse release where you want to drop
All functions may be supplied with further positional information (e.g. to click a list item within a widget) and with optional delays to emulate a human user.
Not a copy-pasteable answer, but maybe it serves as a starter...