How to properly draw Non Client Area (such as TitleBar) in PyQt? - qt

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?

Related

Animating a frame to pop out of the bottom of the app without shrinking the height of other elements

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

Qt Scale Application Resolution

I created a Qt Application that is guaranteed to always run on a 4:3 monitor. I hardcoded 320x240 as the resolution and designed everything to look great.
Now the requirements changed and the app will run on 640x480 resolution (still 4:3).
Is there an easy way to scale up the entire application, such that everything doubles in size. In particular I would like to avoid having to manually change all button sizes, fonts, absolute positioning for overlays, etc.
I found a lot of answers about high DPI settings, but this has little to do with high DPI.
You could use a QGraphicsView and embed your main widget in it. But your whole application has to apply the same factor to all the widgets.
A QGraphicsView allows you to zoom in.
For example:
class Widget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(320, 420)
layout = QFormLayout(self)
btn = QPushButton("Button")
btn.setFixedSize(100, 40)
lineEdit = QLineEdit("Button")
btn.setFixedSize(150, 20)
layout.addRow("A button", btn)
layout.addRow("A Line Edit", lineEdit)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = Widget()
view = QGraphicsView()
scene = QGraphicsScene(0, 0, 1000, 1000)
view.setFixedSize(320 * 2, 420 * 2)
view.setScene(scene)
view.scale(2, 2)
proxy = scene.addWidget(w)
view.centerOn(proxy)
#w = Widget()
#w.show()
view.show()
sys.exit(app.exec_())
That's the quickiest way to proceed. The best way would be to refactor your whole code to be independent of the resolution (by using a configuration class, for example).

Resize Main Window

I have used the setGeometry method in order to give a size to all my widgets. I know it wasn't the best thing to do but this is the first time I use PyQt.
The problem now is that when I click on the Maximize button, all my widgets stay the same...
I have tried to use the resizeEvent, but PyQt doesn't make any difference among this event's senders : indeed this event is sent when the window in being resized with the maximize button, or with the mouse by stretching the window.
What I am looking for :
a particular signal sent when the Maximize button is pressed, so that I can catch this signal and apply my setGeometry() methods, but this time in order to fit the full screen mode.
But I don't know if such a thing exists...
If no, is there another way to do what I need?
Thank you for Reading !
You're going about this in completely the wrong way. Qt already handles all this kind of stuff for you automatically (so long as you use the right APIs).
I suggest you do the following:
Read Qt's Layout Management Overview.
Learn how to use Qt Designer (especially the part about Using Layouts).
Read the Using Qt Designer Guide in the PyQt Documentation.
Meanwhile, here's a simple demo script:
from PyQt4 import QtCore, QtGui
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
self.label = QtGui.QLabel('Layout Management Example', self)
self.label.setAlignment(QtCore.Qt.AlignCenter)
self.edit1 = QtGui.QLineEdit(self)
self.edit2 = QtGui.QLineEdit(self)
self.button = QtGui.QPushButton('Maximize', self)
self.button.clicked.connect(self.handleButton)
layout = QtGui.QGridLayout(self)
layout.addWidget(self.label, 0, 0, 1, 2)
layout.addWidget(self.edit1, 1, 0)
layout.addWidget(self.edit2, 1, 1)
layout.addWidget(self.button, 2, 0, 1, 2)
def handleButton(self):
if self.button.text() == 'Maximize':
self.button.setText('Restore')
self.showMaximized()
else:
self.button.setText('Maximize')
self.showNormal()
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(500, 150, 500, 300)
window.show()
sys.exit(app.exec_())

How to test drag and drop behavior in PyQt?

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...

Qt: QTabWidget causes unnecessary repaints

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.

Resources