PyQt
To install PyQt5 locally:
pipenv install pyqt5
pipenv install PyQt5==5.9.2 ## prevents win8 bugs
Qt Designer (40 MB) ships as a part of Qt Creator (IDE, 45 GB), or individually through FMan.
To install Qt Designer:
QDialog
.QDialog
.QDialog
.QMainWindow
.QWidget
.Select a template, drag and drop UI elements onto the canvas, edit their objectName
properties and save the file with a .ui
extension.
To use our .ui
file with Python, you can load the .ui
file directly:
import sys
from PyQt5 import QtWidgets, uic
app = QtWidgets.QApplication(sys.argv)
window = uic.loadUI("demo.ui")
window.show()
app.exec()
Or convert the .ui
to Python and couple it.
pipenv run pyuic5 demo.ui -o demo.py ## inside project env
You can open the resulting Python file in an editor to take a look, although you should not edit this file. The power of using Qt Creator is being able to edit, tweak and update your application while you develop. Any changes made to this file will be lost when you update it. However, you can override and tweak anything you like when you import and use the file in your applications.
import sys
from PyQt5 import QtWidgets, uic
from MainWindow import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow): subclass from both
def __init__(self, *args, obj=None, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setupUi(self) call this
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
Another coupling example:
import sys
from PyQt5.QtWidgets import QDialog, QApplication
from demoLineEdit import *
class MyForm(QDialog):
def __init__(self):
super().__init__()
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.ui.ButtonClickMe.clicked.connect(self.dispmessage)
self.show()
def dispmessage(self):
self.ui.labelResponse.setText("Hello "
self.ui.lineEditName.text())
if __name__== "__main__":
app = QApplication(sys.argv)
w = MyForm()
w.show()
sys.exit(app.exec_())
Create an app and a window. There must be only one app, but there may be many windows.
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
import sys
class MyMainWindow(QMainWindow): ## always needs a central widget
## subclassed QMainWindow because windows are hidden by default
def __init__(self, *args, **kwargs):
super(MyMainWindow, self).__init__(*args, **kwargs)
self.setWindowTitle("My app")
label = QLabel("This is a label")
label.setAlignment(Qt.AlignCenter)
self.setCentralWidget(label)
Set the central widget of the Window. Widget will expand
to take up all the space in the window by default.
app = QApplication(sys.argv)
window = MyMainWindow()
window.show()
app.exec_() ## enter the event loop
Running the basic application:
python myApp.py
sys.argv
stores CLI arguments. If you know you will not use them:
app = QApplication([]) ## empty list instead of `sys.argv`
Any QWidget without a parent is its own window:
second_window = QWidget()
If you do not create a QMainWindow
, any widget without a parent will be a window in its own right. Our custom PowerBar widget will appear as any normal window.
A signal is an event, and a slot is a method that is executed on the occurrence of the event.
Signal example:
button.clicked.connect(on_button_clicked)
button.clicked
is a signal, .connect()
lets us couple it to slot, which is simply a function that us called whenever the signal is emitted. In Python, any function can be a slot.
An application is constantly listening for events, i.e. interactions such as a mouse click, keyboard input, etc.
Whenever a widget detects an event, it will emit a predefined signal for the event, passing along any related data as a default argument.
This signal is connected, via .connect()
on a class property inside the __init__
constructor, to a slot, which serves as an event handler class method.
Signal | Slot |
---|---|
class property inside constructor | class method |
def __init__(self, *args, **kwargs):
this.eventHappened.connect(eventHandler)
def eventHandler(self):
print("event handler called!")
There are many ways to connect signals and slots:
self.eventHappened(self.eventHandler)
For example, QWidget
and its child QMainWindow
can emit the signal windowTitleChanged
for any window title change event. If this event occurs, the widgets will emit the default signal windowTitleChanged
along with the new title as an argument in a QString
, to be handled in a method connected to the signal.
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
signal connected to method on widget itself (class property)
self.wTitleChanged.connect(self.handleWindowTitleChange)
## slot to handle event (class method)
def handleWindowTitleChange(self, new_title):
print(f"New Window Title is: {new_title}")
Note: wTitleChanged
should be read as windowTitleChanged
. A bug in VS Code prevents correct indentation with the full name.
self.widget.eventHappened(self.eventHandler)
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
signal on widget is widget connected to method on widget itself
self.addButton.pressed.connect(self.add)
self.widget1.eventHappened(self.widget2.eventHandler)
A signal from one attached widget can also be connected to a method in a different attached widget. For example, the custom PowerBar widget below has a QDial
whose signal is connected to the method trigger_refresh()
belonging to another child Bar
.
class PowerBar(QtWidgets.QWidget):
def __init__(self, steps=5, *args, **kwargs):
super(PowerBar, self).__init__(*args, **kwargs)
PowerBar signal connected to child Bar method
self.dial.valueChanged.connect(self.Bar.trigger_refresh)
class MyClass(QObject):
signal = pyqtSignal(int)
instance = MyClass()
instance.signal.connect(lambda x: print(x * 2))
instance.signal.emit(7)
Whenever a widget detects an event, it emits the event-specific signal automatically, through a pre-coded method. Since the emission of signals is pre-coded in the framework, you usually simply specify which signals are connected to which event handlers.
However, you can also intercept events manually, i.e. you can override the method that emits the signal in a class, by creating a child that inherits from that class and overwriting the parent is method in the child class. By overwriting the method, you can (A) replace the signal emission with a different action, or (B) allow for signal emission and also take further action.
For example, the class QMainWindow
has a method contextMenuEvent()
that is emitted whenever a context menu (i.e., the right-click menu) is about to be shown. To intercept this event, you can override this method by subclassing QMainWindow
and creating a method with the same name.
A. Replace signal emission with a different action:
class MyMainWindow(QMainWindow):
## ...
def contextMenuEvent(self, event):
new action, with no signal emitted
print("context menu event occurred!")
B. Allow for signal emission and take further action:
class MyMainWindow(QMainWindow):
## ...
def contextMenuEvent(self, event):
print("Context menu event!")
super(QMainWindow, self).contextMenuEvent(event)
Super()
calls the parent is constructor and contextMenuEvent(event)
calls the parent is method passing in the child is event.
Events in Qt are represented by instances of the QEvent
class (or one of its subclasses). For example, QKeyEvent
contains information about which key was pressed, and whether a modifier such as Ctrl
was pressed along with it.
To process an event, Qt delivers it to one or more objects. For example: Suppose the user presses Esc
while the dialog on page 38 is open. Qt first delivers the corresponding QKeyEvent
to the Save button, because it currently has the keyboard focus. However, this button does not know how to handle Esc
. So Qt next notices the button's parent, which in this case is the containing dialog window. This knows how to react to Esc
and closes itself.
In addition to the very general event(...)
, which handles all events, there are also more specialised event handlers. A good example is keyPressEvent(e)
, which is only called for key presses. Its parameter e
is always a QKeyEvent
.
Example of intercepting a widget's default event:
class MainWindow(QMainWindow):
def closeEvent(self, e):
if not text.document().isModified():
return
answer = QMessageBox.question(
window,
None,
"You have unsaved changes. Save before closing?",
QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel
)
if answer & QMessageBox.Save:
save()
elif answer & QMessageBox.Cancel:
e.ignore()
print("button pressed!", flush=True)
def except_hook(cls, exception, traceback):
sys.__excepthook__(cls, exception, traceback)
sys.stderr.flush()
if __name__ == "__main__":
sys.excepthook = except_hook
To install fbs
locally:
pipenv install fbs ## at project root dir
To install PyInstaller
(required by fbs
) locally:
pipenv install PyInstaller ## at project root dir
To start a project:
pipenv run fbs startproject ## at project's root dir
The fbs startproject
command generates an src
dir.
To run a project:
pipenv run fbs run ## at project root dir
fbs
will look for build/settings/base.json
for entry point
The entry point is at: src/main/python/main.py
When building PyQt5 applications, there are typically a number of components or resources that are used throughout your app. These are commonly stored in the QMainWindow
or as global vars which can get a bit messy as your application grows. The ApplicationContext
provides a central location for initialising and storing these components, as well as providing access to some core fbs
features. The ApplicationContext
object also creates and holds a reference to a global QApplication
object, available under ApplicationContext.app
.
(1) To create executable (i.e., to freeze):
pipenv run fbs freeze
(2) To create installer:
pipenv run fbs installer
If freezing fails, get pypiwin32
:
pipenv install pypiwin32
Afterwards, executing the freeze might fail. If running target/admin_stock/admin_stock.exe
causes a Module not found: distutils
error, then find this file:
C:\Users\IvanOv\.virtualenvs\admin-stock-yFPjNmVr\Lib\site-packages\PyInstaller\hooks\pre_find_module_path\hook-distutils.py
and patch this method:
def pre_find_module_path(api):
## Absolute path of the system-wide "distutils" package
## when run from within a venv or None otherwise.
distutils_dir = getattr(distutils, 'distutils_path', None)
if distutils_dir is not None:
## PATCH, see https://github.com/pyinstaller/pyinstaller/issues/4064
if distutils_dir.endswith('__init__.py'):
distutils_dir = os.path.dirname(distutils_dir)
## Find this package in its parent directory.
api.search_dirs = [os.path.dirname(distutils_dir)]
super(CustomDialog, self).__init__(*args, **kwargs)
event.accept()
/ event.ignore()
QtCore.pyqtSignal()
/ .emit()
In PyQt there is a difference between how a dialog is made: using exec_()
means it is a modal dialog (won't allow you to touch the mainwindow until it is closed) and once closed, it is set for garbage collection.
So the layout does not become the parent of the widget. This makes sense, because widgets can only have other widgets as parents, and layouts are not widgets. All widgets added to a layout will eventually have their parents reset to the parent of the layout (whenever it gets one).
https://stackoverflow.com/questions/30241684/pyqt4-setparent-vs-deletelater
There are two types of dialogs:
The class that is being inherited is called the super class or base class, and the inheriting class is called a derived class or subclass.
One mandatory first parameter is always defined in a method, and that first parameter is usually named self (though you can give any name to this parameter). The self parameter refers to the instance of the class that calls the method.
The following question arises: how does the layout know what the recommended size of the widget is? Basically, each widget has a property called sizeHint
that contains the widget's recommended size. When the window is resized and the layout size also changes, it is through the sizeHint
property of the widget that the layout managers know the size requirement of the widget.
In order to apply the size constraints on the widgets, you can make use of the following two properties:
minimumSize
: If the window size is decreased, the widget will still not become smaller than the size specified in the minimumSize
property.maximumSize
: Similarly, if the window is increased, the widget will not become larger than the size specified in the maximumSize
property.
When the preceding properties are set, the value specified in the sizeHint
property will be overridden.A dockable form. That is, you can dock this sign-in form to any of the four sides of the window—top, left, right, and bottom and can even use it as a floatable form
A thread is a small process created for executing a task independently.
def show_about_dialog():
text = "<center>" \
"<h1>Text Editor</h1>" \
"⁣" \
"<i mg src=icon.svg>" \
"</center>" \
"<p>Version 31.4.159.265358<br/>" \
"Copyright © Company Inc.</p>"
QMessageBox.about(window, "About Text Editor", text)
This a Qt dialect called "rich text". It sometimes behaves differently from what you would expect in a browser. For example: Browsers display an <i mg>
after a <h1>
on a new line. Qt doesn't. That's why our code needs the special character #8291
("invisible separator") to break the two apart. (We could have also used <br/>
or <p>...</p>
but they add too much space.)
Built-in styles The coarsest way to change the appearance of your GUI is to set the global style.
app.setStyle("Fusion")
If you like a style but want to change its colors (eg. to a dark theme) then you can use QPalette
and app.setPalette(...)
.
palette = QPalette()
palette.setColor(QPalette.ButtonText, Qt.red)
app.setPalette(palette)
In addition to the above, you can change the appearance of your application via Qt style sheets (QSS). This is Qt's analogue to CSS.
app.setStyleSheet("QPushButton { margin: 10ex; }")
Once your stylesheet reaches a certain size, it makes sense to put it into a separate file.
with open(appctxt.get_resource("styles.qss")) as f:
appctxt.app.setStyleSheet(f.read())
In CSS, you can use #main-button
to select an element by its ID. QSS has a similar feature: if you call .setObjectName("main-button")
on a widget, then you can select it with #main-button
. This is very useful for styling individual GUI elements.
Qt's support for High DPI Displays is not entirely seamless. In particular, margins and font sizes can vary wildly across displays. A way to alleviate this is to never use pixel measurements in stylesheets. Instead, use units that are independent of monitor resolution. These include pt
, em
and ex
.
The standard way to implement custom rendering in Qt is to override paintEvent()
and use a QPainter
to perform the actual drawing:
def mousePressEvent(self, e):
self._holes.append(e.pos())
super().mousePressEvent(e)
As your usage of Qt becomes more advanced, you may encounter the need to read Qt's C++ source code. This too is nothing to be afraid of. Simply download the source archive called "single" from Qt's home page. Unpack and open the entire folder (!) with an editor that lets you navigate to files by name. (A great choice is Sublime Text with its shortcut Ctrl + P
.) Then, you can find the implementation of any class such as QWidget by opening qwidget.cpp.
A somewhat more recent technology is Qt Quick: It offers highly customizable graphics, fluid animations and is more targeted at mobile devices and touch displays. Qt Quick uses a markup language called QML for laying out UIs. It is functionally similar to HTML, CSS and JavaScript. In fact, it supports JavaScript as a scripting language. Its overall syntax is similar to JSON.
In general, you should use Widgets if you want a pretty standard desktop GUI. Otherwise, if you want highly customizable graphics and / or support for mobile devices and touch displays, use Qt Quick.
The ampersand in "&File"
gives it the shortcut Alt + F
on Windows and Linux. The character following the ampersand defines the shortcut.
The equal sign =
in Qt docs for C++.
QString QFileDialog::getOpenFileName(QWidget *parent = nullptr, const QString &caption = QString(), const QString &dir = QString(), const QString &filter = QString(), QString *selectedFilter = nullptr, QFileDialog::Options options = Options())
Fortunately, all parameters are optional, as indicated by the =
after each one.
The double colon ::
in Qt docs for C++. You can generally replace these by single dots in Python. We did this when QFileDialog::getOpenFileName
became QFileDialog.getOpenFileName
.
Qt's Model/View classes, which we used above, separate the logic for loading data from the logic that displays it.x
from PyQt5.QtSql import *
app = QApplication([])
db = QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("projects.db")
db.open()
model = QSqlTableModel(None, db)
model.setTable("projects")
model.select()
view = QTableView()
view.setModel(model)
view.show()
app.exec_()
As your application matures, it is not unlikely that you will run into performance issues at some point. Here is a simple, yet very effective way to debug them.
from my_implementation import run_app
import cProfile
cProfile.run("run_app()", sort="cumtime")
Once your app closes, this prints detailed statistics about how much time was spent in each function.
Errors that occur in background threads are often not as visible as those in the main thread. If your app is not working as expected and you are using threads, maybe there are some errors you are not being shown.
There are some obligations you do need to fulfill when using Qt under the LGPL.
You need to provide users with the license texts of the GPL and the LGPL, as well as the source code of Qt. Furthermore, you need to indicate that Qt is used in your application. And finally, you need to include Qt’s copyright notice.
See the last chapter in "Python and Qt: The Best Parts" by Michael Herrmann.