Qt Scale Application Resolution - qt

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

Related

Why QDialog not shows any widgets until job completed? [duplicate]

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

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

Strange painting bug in GraphicsView in PyQt 5.7

[updating with smaller example]
We just upgraded to PyQt 5.7 and we have one the last problem left to fix in our application. Here is a standalone example that I created from our application code. Run it and see how the ellipse gets drawn beyond the view borders. This did not occur in 5.5.1. Platform is Windows 7 64 bit (running in a VM). It looks like this:
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QHBoxLayout, QWidget, QLabel
from PyQt5.QtWidgets import QGraphicsProxyWidget, QGraphicsObject, QGraphicsEllipseItem
from PyQt5.QtGui import QBrush
class MyGraphicsItem(QGraphicsObject):
def __init__(self):
QGraphicsObject.__init__(self)
# next line could be any type of graphics item:
rect_item = QGraphicsEllipseItem(0, 0, 100, 100, self)
# effect easier to see if paint black:
rect_item.setBrush(QBrush(Qt.SolidPattern))
label_item = QGraphicsProxyWidget(self)
# *** Next line must be there for effect to be visible, but could be any other type of widget
label_item.setWidget(QLabel('a'*30))
def paint(self, painter, option, widget=None):
return
def boundingRect(self):
return self.childrenBoundingRect()
def show_problem():
app = QApplication([])
widget = QWidget()
layout = QHBoxLayout()
widget.setLayout(layout)
view = QGraphicsView()
scene = QGraphicsScene()
view.setScene(scene)
scene.addItem(MyGraphicsItem()) # *** effect only there if more than 1 item
scene.addItem(MyGraphicsItem())
layout.addWidget(view)
widget.setGeometry(100, 100, 50, 50)
widget.show()
app.exec()
show_problem()
So Russell confirmed that this is a regression in 5.7 (i.e., problem was not present in PyQt 5.6).
I posted a C++ version of my Python example to Qt Forum. A fellow there was kind enough to fix compilation bugs and run it, and saw the bug: so this is indeed a Qt bug.
There is a bug report at bugreports.qt.io that is almost certainly the same issue. It is not resolved, but by an incredible luck, it links to a post where there is a workaround: set the opacity of proxy widget item to just below 1. In the example app of my post, I added
label_item.setOpacity(0.999999)
after setting label_item's widget. I our real app (110k PyQt code) I set the opacity of all QGraphicsProxyWidget instance and it seems to work.
This workaround seems to not have any other side effects, time will tell if there are deeper problems. But it appears to be at least be a temporary solution until the bug gets fixed in Qt. I have submitted a bug report.

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

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?

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