It is now time for a more systematic introduction to Python GUI development with QT5 and PyQt5. We will split this introduction into two parts, first showing you how to create GUI programmatically from Python code and then familiarizing you with the QT Designer tool for visually building GUIs and then translating them to Python code. In the walkthrough from Section 2.7 [1], you will then work through a larger example of creating a GUI based application.
To familiarize ourselves with PyQT5 and get to know the most common QT5 widgets, let’s go through three smaller examples.
Let’s start by just producing a simple window that has a title and displays some simple text via a label widget as shown in the image below.
Thanks to PyQt5, the code for producing this window takes only a few lines:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import sys from PyQt5.QtWidgets import QWidget, QApplication, QLabel app = QApplication(sys.argv) window = QWidget() window.resize( 400 , 200 ) window.setWindowTitle( "PyQt5 example 1" ) label = QLabel( "Just a window with a label!" , window) label.move( 100 , 100 ) window.show() sys.exit(app.exec_()) |
Try out this example by typing or pasting the code into a new Python script and running it. You should get the same window as shown in the image above. Let’s briefly go through what is happening in the code:
That’s all that’s needed! You will see that even if you resize the window, the label will always remain at the same fixed position.
If you have trouble running this script (e.g., you get a "Kernel died, restarting" error), try this version of the code (modified lines highlighted):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | import sys from PyQt5.QtWidgets import QWidget, QApplication, QLabel from PyQt5.QtCore import Qt, QCoreApplication app = QCoreApplication.instance() if app is None : app = QApplication(sys.argv) app.aboutToQuit.connect(app.deleteLater) window = QWidget() window.resize( 400 , 200 ) window.setWindowTitle( "PyQt5 example 1" ) label = QLabel( "Just a window with a label!" , window) label.move( 100 , 100 ) window.show() sys.exit(app.exec_()) |
This version of the code checks to see if there's already a QApplication object left existing in the current process -- only one of these objects is allowed. If an object exists, it's used; else, a new one is created. Line 9 then ensures that the application is deleted upon quitting.
As we already pointed out in Section 2.4.2 [2], using absolute coordinates has a lot of disadvantages and rarely happens when building GUIs. So let’s adapt the example code to use relative layouts and alignment properties to keep the label always nicely centered in the middle of the window. Here is the code with the main changes highlighted:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import sys from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QGridLayout from PyQt5.QtCore import Qt app = QApplication(sys.argv) window = QWidget() window.resize( 400 , 200 ) window.setWindowTitle( "PyQt5 example 1" ) layout = QGridLayout(window) label = QLabel( "Just a window with a label (now perfectly centered!)" ) label.setAlignment(Qt.AlignCenter) layout.addWidget(label, 0 , 0 ) window.show() sys.exit(app.exec_()) |
Try out this modified version and see whether you notice the change. Here is an explanation:
If you tried out the modified example, you will have noticed that the label now always remains in the center independent of how you resize the window. That is the result of the grid layout manager working in the background to rearrange the child elements whenever the size is changed.
As a further extension of this example, let us make things a bit more interesting and bring in some interactions by adding a button that can be used to close the application as an alternative to using the close icon at the top. The widget needed to implement such a button is called QPushButton. We will add the button to cell 1,0 which is the cell in row 1 and column 0, so below the cell containing the label. That means that the grid layout will now consist of one column and two rows. Here is the modified code with the main changes highlighted:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import sys from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QGridLayout, QPushButton from PyQt5.QtCore import Qt app = QApplication(sys.argv) window = QWidget() window.resize( 400 , 200 ) window.setWindowTitle( "PyQt5 example 1" ) layout = QGridLayout(window) label = QLabel( "Just a window with a label (now perfectly centered!)" ) label.setAlignment(Qt.AlignCenter) layout.addWidget(label, 0 , 0 ) button = QPushButton( "Close me" ) button.setToolTip( 'This is a <b>QPushButton</b> widget. Clicking it will close the program!' ) layout.addWidget(button, 1 , 0 ) button.clicked.connect(app.quit) window.show() sys.exit(app.exec_()) |
Please note how the push button widget is created in line 17 providing the text it will display as a parameter. It is then added to the layout in line 19. In addition, we use the setToolTip(…) method to specify the text that should be displayed if you hover over the button with the mouse. This method can be used for pretty much any widget to provide some help text for the user. The interesting part happens in line 21: Here we specify what should actually happen when the button is pressed by, in QT terminology, “connecting a signal (button.clicked) of the button to a slot (app.quit) of the application object”. So if the button is clicked causing a “clicked” event, the method quit(…) of the application object is called and the program is terminated as a result. Give this example a try and test out the tooltip and button functionality. The produced window should look like in the image below:
As you probably noticed, the button right now only takes up a fixed small amount of space in the vertical dimension, while most of the vertical space is taken by the cell containing the label which remains centered in this area. Horizontally, the button is expanded to always cover the entire available space. This is the result of the interplay between the layout policies of the containing grid layout and the button object itself. By default, the vertical policy of the button is set to always take up a fixed amount of space but the horizontal policy allows for expanding the button. Since the default of the grid layout is to expand the contained objects to cover the entire cell space, we get this very wide button.
In the last version of this first example, we are therefore going to change things so that the button is not horizontally expanded anymore by adding a QHBoxLayout to the bottom cell of the grid layout. This is supposed to illustrate how different widgets and layouts can be nested to realize more complex arrangements of GUI elements. In addition, we change the code to not close the application anymore when the button is clicked but instead call our own function that counts how often the button has been clicked and displays the result with the help of our label widget. A screenshot of this new version and the modified code with the main changes highlighted are shown below.
Source code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | import sys from PyQt5.QtWidgets import QWidget, QApplication, QLabel, QGridLayout, QPushButton, QHBoxLayout from PyQt5.QtCore import Qt def buttonClickedHandler(c): global counter counter + = 1 label.setText( 'Thank you for clicking the button ' + str (counter) + ' times!' ) app = QApplication(sys.argv) window = QWidget() window.resize( 400 , 200 ) window.setWindowTitle( "PyQt5 example 1" ) layout = QGridLayout(window) label = QLabel( "Just a window with a label (now perfectly centered!)" ) label.setAlignment(Qt.AlignCenter) layout.addWidget(label, 0 , 0 ) button = QPushButton( "Click me" ) button.setToolTip( 'This is a <b>QPushButton</b> widget. Click it!' ) horLayout = QHBoxLayout() horLayout.addStretch( 1 ) horLayout.addWidget(button) horLayout.addStretch( 1 ) layout.addLayout(horLayout, 1 , 0 ) button.clicked.connect(buttonClickedHandler) counter = 0 window.show() sys.exit(app.exec_()) |
In addition to the highlighted changes, there are a few very minor changes to the text displayed on the button and its tooltip. Let us first look at the changes made to implement the counting when the button is pressed. Instead of directly connecting the button.clicked signal to the slot of another QT element, we are connecting it to our own function buttonClickedHandler(…) in line 32. In addition, we create a global variable counter for counting how often the button has been clicked. When it is clicked, the buttonClickedHandler(…) function defined in lines 6 to 9 will be called, which first increases the value of the global counter variable by one and then uses the setText(…) method of our label object to display a message which includes the number of button presses taken from variable counter. Very simple!
Now regarding the changes to the layout to avoid that the button is expanded horizontally: In principle, the same thing could have been achieved by modifying the horizontal layout policy of the button. Instead, we add a new layout manager object of type QHBoxLayout to the bottom cell of the grid layout that allows for arranging multiple widgets in horizontal order. This kind of layout would also be a good choice if, for instance, we wanted to have several buttons at the bottom instead of just one, all next to each other. In line 26, we create the layout object and store it in variable horLayout. Later in line 30, we add the layout to the bottom cell of the grid layout instead of adding the button directly. This is done using the addLayout(…) method rather than addWidget(…).
In between these two steps, we add the button to the new horizontal layout in horLayout in line 28. In addition, we add horizontal stretch objects to the layout before and after the button in lines 27 and 29. We can think of these objects as springs that try to take up as much space as possible without compressing other objects more than these allow. The number given to the addStretch(…) method is a weight factor that determines how multiple stretch objects split up available space between them. Since we use 1 for both calls of addStretch(…), the button will appear horizontally centered and just take up as much space as needed to display its text. If you want to have the button either centered to the left or to the right, you would have to comment out line 27 or line 29, respectively. What do you think would happen if you change the weight number in line 27 to 2, while keeping the one in line 29 as 1? Give it a try!
We are now moving on to example 2, a completely new example that focuses on the menu bar and status bar widgets as well as on defining user actions that can be associated with different input widgets and some other useful features of QT5 that are commonly used in GUI-based programs. Often GUI-based programs provide many ways in which the user can trigger a particular action, e.g. the action for saving the currently opened file can typically be performed by choosing the corresponding entry from the menu bar at the top, by a keyboard shortcut like CTRL+S for instance, and potentially also by a tool button in a toolbar and by some entry in a so-called context menu that shows up when you click the right mouse button. PyQT5 provides the QAction class for defining actions and these actions can then be associated with or added to different GUI elements that are supposed to trigger the action. For instance, in the following example we will create an action for exiting the program with the following four lines of code:
1 2 3 4 5 | # exit action exitAction = QAction(app.style().standardIcon(QStyle.SP_DialogCancelButton), '&Exit' , mainWindow) exitAction.setShortcut( 'Ctrl+Q' ) exitAction.setStatusTip( 'Exit application' ) exitAction.triggered.connect(app.quit) |
The QAction object for our exit action is created in the first line and then stored in variable exitAction. The first two parameters given to QAction(…) are an icon that will be associated with that action and the name. For the icon we use the SP_DialogCancelButton icon from the set of icons that comes with QT5. Of course, it is possible to use your own set of icons but we want to keep things simple here. The & symbol in the name of the action (&Exit) signals that it should be possible to use ALT+E as a keyboard shortcut to trigger the action when using the application’s menu bar. The last parameter is the parent object which needs be another QT object, the one for the application’s main window in this case (more on this in a moment).
In the following two lines we define the keyboard shortcut (Ctrl+Q) that can be used at any moment to trigger the action and a message that should be shown in the status bar (the bar at the bottom of the application’s main window that is typically used for showing status messages) when hovering over a GUI element that would trigger this action. Finally, in the last line we connect the event that our exit action is triggered (by whatever GUI element) to the quit slot of our application. So this is the part where we specify what the action should actually do and as we have seen before, we can either connect the signal directly to a slot of another GUI element or to a function that we defined ourselves.
We already briefly mentioned the “main window” of the application. In example 1 above, we used the QWidget object for the main window and container of the other GUI elements. In example 2, we will use the QMainWindow widget instead which represents a typical application window with potentially a menu bar and tool bar at the top, a large central area in the middle to display the main content of the app, and potentially a small status bar at the bottom. The image below shows how the main window we are going to create in example 2 will look.
Once a QMainWindow object has been created and stored in variable mainWindow, its menu bar (an object of type QMenuBar that is created automatically) can be accessed via the menuBar() method, so with the expression mainWindow.menuBar(). A menu bar consists of one or more menus (= objects of the QMenu class) which in turn consist of several menu entries. The entries can be actions or submenus which again are QMenu objects. To add a new menu to a menu bar, you call its addMenu(…) method and provide the name of the menu, for instance ‘&File’. The method returns the newly created QMenu object as a result, so that you can use it to add menu entries to it. To add an action to a menu, you invoke a method called addAction(…) of the menu object, providing the action as a parameter. With that explanation, it should be relatively easy to follow the code below. We have highlighted the important parts related to creating the main window, setting up the menu bar, and adding the exit action to it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QAction, QStyle app = QApplication(sys.argv) mainWindow = QMainWindow() mainWindow.resize( 400 , 200 ) mainWindow.setWindowTitle( "PyQt5 example 2" ) fileMenu = mainWindow.menuBar().addMenu( '&File' ) optionsMenu = mainWindow.menuBar().addMenu( '&Options' ) exitAction = QAction(app.style().standardIcon(QStyle.SP_DialogCancelButton), '&Exit' , mainWindow) exitAction.setShortcut( 'Ctrl+Q' ) exitAction.setStatusTip( 'Exit application' ) exitAction.triggered.connect(app.quit) fileMenu.addAction(exitAction) mainWindow.statusBar().showMessage( 'Waiting for your commands...' ) mainWindow.show() sys.exit(app.exec_()) |
The QMainWindow is created very similarly to the QWidget we used in example 1, meaning we can set the title and initial size of the widget (lines 8 and 9). The two menus ‘File’ and ‘Options’ are added to the menu bar of our main window in lines 11 and 12, and the QMenu objects returned are stored in variables fileMenu and optionsMenu, respectively. In line 19, we add the exit action we created with the code already discussed earlier (lines 14 to 17) to the ‘File’ menu. The icon and name we provided when creating the action will be used for the entry in the menu bar and selecting the entry will trigger the action and result in the quit() method of application being called.
Please note that in line 21, we also added a command to show a message in the status bar at the bottom of the main window when the application is started. The status bar object is accessed via the statusBar() method of the QMainWindow, and then we directly call its showMessage(…) method specifying the text that should be displayed. We suggest that you run the program a few times, trying out the different ways to exit it via the menu entry (either by clicking the entry in the ‘File’ menu or using ALT+F followed by ALT+E) and the action's keyboard shortcut CTRL+Q that we defined.
So far, our 'File' menu only has a single entry and the 'Options' menu is still completely empty. In the following, we are going to extend this example by adding ‘Open’ and ‘Save’ actions to the ‘File’ menu making it appear somewhat similar to what you often see in programs. We also add entries to the 'Options' menu, namely one with a checkbox next to it that we use for controlling whether the label displayed in our main window is shown or hidden, and one that is a QMenu object for a submenu with two additional entries. The two images below illustrate how our menu bar will look after these changes.
In addition to the changes to the menu bar, we will use the QFileDialog widget to display a dialog for selecting the file that should be opened and we use a QMessageBox widget to display a quick message to the user that the user has to confirm. Here is the code for the new version with main changes highlighted. Further explanation will follow below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QAction, QStyle, QFileDialog, QMessageBox, QWidget, QGridLayout, QLabel, QMenu from PyQt5.QtCore import Qt def openFile(): fileName, _ = QFileDialog.getOpenFileName(mainWindow, "Open file" , " ", " All files ( * . * )") if fileName: mainWindow.statusBar().showMessage( 'User has picked file ' + fileName) else : mainWindow.statusBar().showMessage( 'User canceled the file dialog.' ) def saveFile(): QMessageBox.information(mainWindow, 'Important information' , 'Save file has not been implemented yet, sorry!' , QMessageBox.Ok) def toggleLabel(state): if state: label.show() else : label.hide() app = QApplication(sys.argv) mainWindow = QMainWindow() mainWindow.resize( 400 , 200 ) mainWindow.setWindowTitle( "PyQt5 example 2" ) mainWindow.setCentralWidget(QWidget()) layout = QGridLayout(mainWindow.centralWidget()) label = QLabel( "Some text ..." ) label.setAlignment(Qt.AlignCenter) layout.addWidget(label, 0 , 0 ) fileMenu = mainWindow.menuBar().addMenu( '&File' ) optionsMenu = mainWindow.menuBar().addMenu( '&Options' ) openAction = QAction( '&Open...' , mainWindow) openAction.triggered.connect(openFile) fileMenu.addAction(openAction) saveAction = QAction( '&Save' , mainWindow) saveAction.triggered.connect(saveFile) fileMenu.addAction(saveAction) exitAction = QAction(app.style().standardIcon(QStyle.SP_DialogCancelButton), '&Exit' , mainWindow) exitAction.setShortcut( 'Ctrl+Q' ) exitAction.setStatusTip( 'Exit application' ) exitAction.triggered.connect(app.quit) fileMenu.addAction(exitAction) toggleLabelAction = QAction( '&Toggle label' , mainWindow, checkable = True ) toggleLabelAction.setChecked( True ) toggleLabelAction.triggered.connect(toggleLabel) optionsMenu.addAction(toggleLabelAction) otherOptionsSubmenu = QMenu( '&Other options' , mainWindow) otherOption1Action = QAction( 'Other option &1' , mainWindow, checkable = True ) otherOption2Action = QAction( 'Other option &2' , mainWindow, checkable = True ) otherOptionsSubmenu.addAction(otherOption1Action) otherOptionsSubmenu.addAction(otherOption2Action) optionsMenu.addMenu(otherOptionsSubmenu) mainWindow.statusBar().showMessage( 'Waiting for your commands...' ) mainWindow.show() sys.exit(app.exec_()) |
In addition to importing some more widgets and other PyQT classes that we need, we define three functions at the beginning, openFile(), saveFile(), and toggleLabel(…). These will serve as the event handler functions for some of the new actions/menu entries that we are adding, and we will discuss them in more detail later.
The next thing to note is that in lines 31 to 33 we are reintroducing our label object from the first example in the center of the main window. Since our window is based on QMainWindow, we first have to create a QWidget to fill the central area of the main window via its setCentralWidget(…) method (line 27), and then we add the needed layouts and QLabel object itself to this central widget exactly as in example 1 (lines 29 to 33).
In lines 38 and 42, we create new QAction objects for the two actions we want to add to the ‘File’ menu in variable fileMenu. To keep the code from getting too long, we don’t set up icons, keyboard shortcuts, and status bar tips for these like we did for the exit action, but these could be easily added. They are set up to call the openFile() and saveFile() functions we defined when they are triggered (lines 39 and 43), and both actions are added to fileMenu via the addAction(…) method in lines 40 and 44.
In lines 52 to 55, the action with the checkbox to toggle the label on and off is created and added to the ‘Options’ menu. The main difference to the other actions is that we use the additional keyword argument checkable=True when creating the action object in line 52, and then set the initial state to being checked in the following line. The "triggered" signal of the action is connected to the toggleLabel(…) function. Note how this function, in contrast to other event handler function we created before, has a parameter state that when called will be given the state of the action, meaning whether it is checked or unchecked, as a boolean. The code in the body of the function in lines 17 to 20 then simply checks whether this state is True for "checked" and if so, makes sure that the label is visible by calling its show() method. If state is False for "unchecked", it will call hide() instead and the label will become invisible.
In lines 57 to 63, we create a submenu for the ‘Options’ menu with two more checkable actions that we simply call ‘Other option 1’ and ‘Other option 2’. This is just for illustrating how to create a submenu in a menu bar, so we don’t bother with linking these actions to some actual functionality as we might in a real-world situation. The important part starts with line 57 where we create a QMenu object called ‘Other options’ that we then add the actions to in line 61 and 62. In line 63, we then add this new menu to our ‘Options’ menu in variable optionsMenu. Since we are not adding an action but a submenu here, we have to use the addMenu(…) method for this.
Now it is time to have a closer look at the openFile() and saveFile() functions that describe what should happen if the open or save actions are triggered. We are keeping things very simple in the saveFile() function, so let us start with that one: Since we are just creating a GUI framework here without real functionality, we only display a warning message to the user that no save file functionality has been implemented yet. We do this with the help of the QMessageBox widget that has the purpose of making, creating, and showing such message boxes as easily as possible. QMessageBox has several methods that can be invoked to display different kinds of messages such as simple information text or questions that require some user input. To just display some text and have an OK button that the user needs to click for confirmation, we use the information(…) method (line 14). We have to provide a parent QT object (mainWindow), a title, the information text, and the kind of buttons we want in the message box (QMessageBox.Ok) as parameters.
Finally, let’s look at the openFile() function in lines 6 to 11: here we illustrate what would typically happen when the action to open a file is triggered. Among other things, you typically want to provide a file browser dialog that allows the user to pick the file that should be opened. Such a dialog is much more complicated than a simple message box, so we cannot use QMessageBox for this, but fortunately QT provides the QFileDialog widget for such purposes. Like QMessageBox, QFileDialog has multiple methods that one can call depending on whether one needs a dialog for opening an existing file, selecting a folder, or saving a file under a name chosen by the user. We here use QFileDialog.getOpenFileName (…) and provide a parent object (mainWindow), a title for the dialog, and a string that specifies what files can be selected based on their file extension as parameters. For the last parameter, we use "*.*" meaning that the user can pick any file.
getOpenFileName (…) has a return value that indicates whether the user left the dialog via the Ok button or whether the user canceled the dialog. In the first case, the return value will be the name of the file selected, and in the second case it will be None. We capture this return value in variable fileName and then use an if-statement to distinguish both cases: In the first case, we use showMessage(…) of our status bar to display a message saying which file was selected. If the condition is False (so if fileName is None), we use the same message to inform that the dialog was canceled.
This second version of example 2 has already gotten quite long and the same applies for our explanation. You should take a moment to run the actual application and test out the different actions we implemented, the message box display, the open file dialog, toggling the label on and off via the entry under the ‘Options’ menu, and so on.
We are now leaving the world of menu bars behind and moving on to a third and final example of manually creating PyQT5 based programs which has the purpose of showing you the most common widgets used for getting input from the user as well as the following things:
We will keep the actual functionality we have to implement in this example to a minimum and mainly connect signals sent by the different widgets to slots of other widgets. As a result, the dialog box will operate in a somewhat weird way and we, hence, call this example “The world’s weirdest dialog box”. It still serves the purpose of illustrating the different event types and how to react to them.
To understand the example, it is important to know that dialog boxes can be invoked in two different ways, modally and modelessly (also referred to as non-modal). Modal means that when the method for displaying the dialog to the user is called, it will only return from the call once the user closes the dialog (e.g. by pressing an Ok or Cancel button). That means the user cannot interact with any other parts of the program's GUI, only the dialog box. When a dialog is invoked in the modeless approach, the method for displaying the dialog will return immediately and the dialog will essentially be displayed in addition to the other windows of the program that still can be interacted with.
The QDialog widget that we will use to build our own dialog box in this example, therefore, has two methods: exec_() for displaying the dialog modally, and show() for displaying it in the modeless way. In contrast to show(), exec_() has a return value that indicates whether the dialog was canceled or has been closed normally, e.g. by pressing an Ok button. You may wonder how our program would be informed about the fact that the dialog has been closed, and in which way, in the modeless option using show(). This happens via the signals accepted and rejected that the dialog will produce in this case and that we can connect to in the usual way. You will see an example of that later on but we first start with a modal version of our dialog box.
The final version of example 3 will be even longer than that of example 2. We, therefore, added some comments to structure the code into different parts, e.g. for setting up the application and GUI, for defining the functions that realize the main functionality, for wiring things up by connecting signals to slots or functions, and so on. In case you run into any issues while going through the following steps to produce the final code for the example, the final script file can be downloaded here [3]. In the first skeleton of the code shown below, some of the sections introduced by the comments are still empty but we will fill them while we move along. This first version only illustrates how to create an empty QDialog object for our dialog box and show it (modally) when a button located on the main window is clicked. The most important parts of the code are again highlighted.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | # imports import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QGridLayout, QWidget, QDialog # set up app and GUI app = QApplication(sys.argv) mainWindow = QMainWindow() mainWindow.resize( 400 , 200 ) mainWindow.setWindowTitle( "PyQt5 example 3" ) mainWindow.setCentralWidget(QWidget()) layout = QGridLayout(mainWindow.centralWidget()) button = QPushButton( "Open dialog ..." ) layout.addWidget(button, 0 , 0 ) dialogBox = QDialog() dialogBox.setWindowTitle( "The world's weirdest dialog box" ) # functions for interactions # functions for modal version # functions for modeless version # connect signals and other initializations button.clicked.connect(dialogBox.exec_) # invoke dialog modal version # run the program mainWindow.show() sys.exit(app.exec_()) |
The QDialog widget is created in line 21 and it is stored in variable dialogBox. We can now add content (meaning other widgets) to it in a similar way as we did with QWidget objects in previous examples using the addWidget(…) and addLayout(…) methods. In lines 17 and 18, we create a simple push button and add it to our main window. In line 31, we connect the "clicked" signal of this button with the exec_() method of our (still empty) dialog box. As a result, when the button is pressed, exec_() will be called and the dialog box will be displayed on top of the main window in a modal way blocking the rest of the GUI. Run the application now and see whether the dialog shows up as expected when the button is clicked.
We are now going to add the widgets to our dialog box in variable dialogBox. The result should look as in the image below:
Please follow the steps below to create this new version of example 3:
Step 1. Replace all the lines with import statements under the comment “# imports” and before the comment “# set up app and GUI” with the following lines.
1 2 3 4 5 6 7 | import sys, random from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton, QGridLayout, QWidget, QDialog, \ QVBoxLayout, QGroupBox, QLabel, QLineEdit, QTextEdit, QHBoxLayout, QListView, QRadioButton, \ QCheckBox, QComboBox, QDialogButtonBox from PyQt5.QtCore import Qt, QVariant from PyQt5.QtGui import QPixmap, QStandardItemModel, QStandardItem |
As you can see, we need to import quite a few more widget classes. In addition, using some of these will require additional auxiliary classes from the PyQt5.QtCore and PyQt5.QtGui modules.
Step 2. Keep the code that is currently under the comment “# set up app and GUI” as this will not change. But then add the following code directly after it, still before the “# functions for interactions” comment.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | mainVerticalLayout = QVBoxLayout(dialogBox) nameGroupBox = QGroupBox( "Name" ) # row 1 of vertical layout mainVerticalLayout.addWidget(nameGroupBox) nameGridLayout = QGridLayout(nameGroupBox) firstNameLabel = QLabel( "First name:" ) nameGridLayout.addWidget(firstNameLabel, 0 , 0 ) lastNameLabel = QLabel( "Last name:" ) nameGridLayout.addWidget(lastNameLabel, 1 , 0 ) firstNameLineEdit = QLineEdit() nameGridLayout.addWidget(firstNameLineEdit, 0 , 1 ) lastNameLineEdit = QLineEdit() nameGridLayout.addWidget(lastNameLineEdit, 1 , 1 ) imageHorizontalLayout = QHBoxLayout() # row 2 mainVerticalLayout.addLayout(imageHorizontalLayout) imageLabel = QLabel() imageLabel.setPixmap(QPixmap( "psu.PNG" ).scaledToWidth( 172 )) imageHorizontalLayout.addWidget(imageLabel) textEdit = QTextEdit() textEdit.setText( "<write whatever you want here>" ) imageHorizontalLayout.addWidget(textEdit) listGridLayout = QGridLayout() # row 3 mainVerticalLayout.addLayout(listGridLayout) listView = QListView() listGridLayout.addWidget(listView, 0 , 0 , 4 , 1 ) clearPushButton = QPushButton( "Clear" ) listGridLayout.addWidget(clearPushButton, 0 , 1 ) hidePushButton = QPushButton( "Hide" ) listGridLayout.addWidget(hidePushButton, 1 , 1 ) showPushButton = QPushButton( "Show" ) listGridLayout.addWidget(showPushButton, 2 , 1 ) listWordsPushButton = QPushButton( "List words" ) listGridLayout.addWidget(listWordsPushButton, 3 , 1 ) widgetGroupBox = QGroupBox() # row 4 mainVerticalLayout.addWidget(widgetGroupBox) widgetGridLayout = QGridLayout(widgetGroupBox) greatRadioButton = QRadioButton( "I think this dialog box is great!" ) greatRadioButton.setChecked( True ) widgetGridLayout.addWidget(greatRadioButton, 0 , 0 ) neutralRadioButton = QRadioButton( "I am neutral towards this dialog box!" ) widgetGridLayout.addWidget(neutralRadioButton, 1 , 0 ) horribleRadioButton = QRadioButton( "This dialog box is just horrible!" ) widgetGridLayout.addWidget(horribleRadioButton, 2 , 0 ) checkBox = QCheckBox( "Check me out" ) widgetGridLayout.addWidget(checkBox, 0 , 1 ) comboBox = QComboBox() widgetGridLayout.addWidget(comboBox, 0 , 2 ) widgetPushButton = QPushButton( "I am a push button spanning two columns" ) widgetGridLayout.addWidget(widgetPushButton, 2 , 1 , 1 , 2 ) buttonBox = QDialogButtonBox() # row 5 buttonBox.setStandardButtons(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) mainVerticalLayout.addWidget(buttonBox) |
This is the code for creating all the different widgets in our dialog box. You should be getting used to reading this kind of code, so we will just explain the most important points here:
At this point, you can run the script and it should produce the GUI as shown in the previous figure. You can already type things into the different edit fields and use the checkbox and radio buttons. The other elements still need to be connected to some functionality to serve a purpose, which is what we will do next.
Step 3. The next things we are going to add are two functions to put some content into the QListView widget in the third row and the QComboBox widget in the fourth row. Since we want to illustrate how different GUI elements can be connected to play together, we will use the list view to display a list of the words from the text that has been entered into the QTextEdit widget in the second row (variable textEdit). The combo box we will simply fill with a set of randomly generated numbers between 1 and 9. Then we will wire up these widgets as well as the push buttons from the third row and the QDialogButtonBox buttons from the fifth row.
The following code needs to be placed directly under the comment “# functions for interactions”, before the comment “# functions for modal version”.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | def populateListView(): words = textEdit.toPlainText().split() m = QStandardItemModel() for w in words: item = QStandardItem(w) item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) item.setData(QVariant(Qt.Checked), Qt.CheckStateRole) m.appendRow(item) listView.setModel(m) def populateComboBoxWithRandomNumbers(): comboBox.clear() for i in range ( 5 ): comboBox.addItem( str (random.randrange( 10 ))) |
The function populateListView() calls the method toPlainText() of the QTextEdit widget. The QTextEdit widget can contain rich text with styling but this method only gives us the plain text without styling markups as a string. We then use the string method split() to split this string into a list of strings at each space or other whitespace symbol. The resulting list of words is stored in variable words. The QListView is one of the widgets that needs a model behind it meaning some object that stores the actual list data to be displayed. Since we just need a list of simple string objects here, we use the QStandardItemModel class available for such cases and fill it with QStandardItem objects we create, one for each word in our words list (lines 3 to 8). The model created in this way is then given to the setModel() method of the list view, which will then display these items. In lines 6 and 7 we are setting up the list items to have a check box that is originally checked but can be unchecked by the user to only select a subset of the items.
Populating the combo box with items the user can pick from is much simpler because we can directly add string items to it with the addItem(…) method (line 14). In the populateComboBoxWithRandomNumbers() function, we first clear the current content, then use a for-loop that creates the random numbers and adds them as string items to the combo box.
In addition, you now need to place the following lines of code directly under the comment “# connect signals and other initializations”, before the line that is already there for opening the dialog when the button on the main window is clicked:
1 2 3 4 5 6 7 8 9 10 | radioButtons = [ greatRadioButton, neutralRadioButton, horribleRadioButton ] populateComboBoxWithRandomNumbers() buttonBox.accepted.connect(dialogBox.accept) buttonBox.rejected.connect(dialogBox.reject) clearPushButton.clicked.connect(textEdit.clear) hidePushButton.clicked.connect(textEdit.hide) showPushButton.clicked.connect(textEdit.show) listWordsPushButton.clicked.connect(populateListView) |
The first line will only play a role later on, so we ignore it for the moment. In line 3, we call the populateComboBoxWithRandomNumbers() function to initialize the combo box so that it contains a list of numbers immediately when the dialog box is opened for the first time. Next we wire up the “Ok” and “Cancel” buttons for exiting the dialog (lines 5 and 6). This is not done via the "clicked" signals of the button themselves but via the "accepted" and "rejected" signals of the button box that contains them. We connect these signals to the accept() and reject() methods of our dialog box, and these will take care of producing the corresponding return values or trigger the corresponding signals depending on whether we called the dialog box modally or modeless.
Finally, we connect the four push buttons from the third row (lines 7 to 10). The first three are used to invoke different methods of the text edit widget above them: The first clears the text area, the second hides the widget, and the third shows it again. The fourth button is set up to invoke our populateListView() function, so this is the button that needs to be clicked for a list of words to show up in the list view widget. Go ahead and run the script now. Enter a few lines of text into the text edit field and then click the “List words” button and observe the list of words that can now be selected via the little checkboxes. Then try out the other buttons and the combo box.
Step 4. At this point, we still have a few widgets in our dialog box that do not do anything. Let’s make things really weird by adding the following commands to the “# connect signals and other initializations” section directly following the lines you just added and still before the line for opening the dialog when the button on the main window is clicked.
1 2 3 | widgetPushButton.pressed.connect(populateComboBoxWithRandomNumbers) firstNameLineEdit.textChanged.connect(checkBox.toggle) lastNameLineEdit.editingFinished.connect(comboBox.showPopup) |
Take a brief moment to read these commands and try to understand the functionality they are adding. Do you understand what is happening here? The first line is for finally giving some functionality to the large push button labeled “I am a push button spanning two row”. We connect this button to our function for populating the combo box with random numbers. So every time you click the button, the combo box will show a different selection of random numbers to pick from. Please note that we are not connecting the "clicked" signal here as we did with the other push buttons. Instead, we connect the "pressed" signal. What is the difference? Well, the "clicked" signal will only be sent out when the mouse button is released, while "pressed" is immediately sent when you press down the mouse button. When you run the dialog again, check out whether you notice the difference.
In the second and third line, we do something that you would usually never do in a dialog box: We connect the "textChanged" signal of the line edit widget for entering your first name at the top to the "toggle" slot of our checkbox widget in the fourth row. This signal is emitted whenever the text in the field is changed, e.g. every time you press a key while editing this input field. So if you type in your first name, you will see the checkbox constantly toggle between its checked and unchecked states. We then connect the "editingFinished" signal of the line edit widget for the last name to the "showPopup" slot of our combo box for opening the drop down list with the different choices. The difference between "textChanged" and "editingFinished" is that "editingFinished" will only be emitted when you press TAB or the widget loses focus in another way, for instance when you click on a different widget. So if you enter your last name and press TAB, you will see the drop down list of the combo box appearing. Give this and the other weird things we just implemented a try by running the script!
Step 5. It’s probably best if we stop wiring up our dialog box at this point, but feel free to keep experimenting with the different signals and connecting them to different slots later on after we have completed this example. We now want to focus on what typically happens if the dialog box is closed. Right now, nothing will happen because we have been connecting the push button on our main window directly to the exec_() method, so there is no own code yet that would be executed when returning from this method. Typically, you will have your own function that calls exec_() and that contains some additional code depending on whether the user closed the dialog via the “Ok” or “Cancel” button and the state or content of the different widgets. For this purpose, please first add the following function at the end of the “# functions for interactions” section, directly before “# functions for modal version”:
1 2 3 4 5 6 | def printResults(): for rb in radioButtons: if rb.isChecked(): print ( "Selected opinion: " + rb.text()) print ( "Combo box has current value " + comboBox.currentText()) print ( "Checkbox is " + ( "checked" if checkBox.isChecked() else "unchecked" )) |
Then under "#functions for modal version" insert the following code:
1 2 3 4 5 6 | def openDialogModal(): result = dialogBox.exec_() if result = = QDialog.Accepted: printResults() else : print ( "Exited dialog via cancel button or closing window" ) |
Finally, change the line in which we set up the main window button to open the dialog from
1 | button.clicked.connect(dialogBox.exec_()) # invoke dialog modal version |
to
1 | button.clicked.connect(openDialogModal) # invoke dialog modal version |
It should be clear that this last change means that instead of opening the dialog box directly, we are now calling our own function openDialogModal() when the button on the main window is clicked. Looking at the code of that function, the first thing that will happen then is that we call dialogBox.exec_() to open the dialog box, but here we also capture its return value in variable result. When the dialog box is closed, this return value will tell us whether the user accepted the dialog (the user clicked ok) or whether the user rejected the dialog (the user clicked cancel or closed the dialog in another way). The return value is a number but instead of bothering with how accepted and rejected are encoded, we compare result to the corresponding constants QDialog.Accepted and QDialog.Rejected defined in the QDialog class. When the return value is equal to QDialog.Accepted, we call the printResults() function we defined, else we just print out a message to the console saying that the dialog was canceled.
The printResults() function illustrates how you can check the content or state of some of the widgets, once the dialog has been closed. Even though the dialog is not visible anymore, the widgets still exist and we just have to call certain methods to access the information about the widgets.
We first look at the three radio buttons to figure out which of the three is selected and print out the corresponding text. At the beginning of the section “#connect signals and other initializations” in the code, we created a list of the three buttons in variable radioButtons. So we can just loop through this list and use the isChecked() method which gives us back a boolean value. If it is True, we get the label of the radio button via its text() method and print out a message about the user’s opinion on our dialog box.
Next, we print out the item currently selected for our combo box: This is retrieved via the combo box’s currentText() method. The state of the check box widget can again be accessed via a method called isChecked(). The other widgets provide similar methods but the general idea should have gotten clear. You already saw the toPlainText() method of QTextEdit being used, and QLineEdit has a method called text() to retrieve the text the user entered into the widget. We will leave adding additional output for these and the other widgets as an “exercise for the reader”. Please run the script and open/close the dialog a few times after using the widgets in different ways and observe the output produced when dialog is closed.
Change to modeless version. The last thing we are going to do in this section is coming back to the concept of modal and modeless dialog boxes and showing what a modeless version of our dialog box would look like. Please add the following three functions to the section “# functions for modeless version”:
1 2 3 4 5 6 7 8 9 | def openDialogModeless(): dialogBox.show() print ( "We are already back from calling dialogBox.show()" ) def dialogAccepted(): printResults() def dialogRejected(): print ( "Exited dialog via cancel button or closing window" ) |
Now comment out the line
1 | button.clicked.connect(openDialogModal) # invoke dialog modal version |
by placing a # in front of it and then insert the following lines below it:
1 2 3 | dialogBox.accepted.connect(dialogAccepted) # invoke dialog modeless version dialogBox.rejected.connect(dialogRejected) button.clicked.connect(openDialogModeless) |
We suggest you try out this new version immediately and observe the change. Note how the main window still can be interacted with after the dialog box has been opened. Also note the message in the console “We are already back from calling dialogBox.show()” appearing directly after the dialog window has appeared. Looking at the code, instead of calling openDialogModal(), we are now calling openDialogModeless(). This function uses dialogBox.show() to open a modeless version of our dialog rather than dialogBox.exec_() for the modal version. The message is produced by the print statement directly after this call, illustrating that indeed we return immediately from the function call, not just when the dialog box is closed.
As a result, we need the two other functions to react when the dialog box has been closed. We connect the function dialogAccepted() to the "accepted" signal of dialogBox that is emitted when the dialog box is closed via the “Ok” button. The function simply calls printResults() and, hence, essentially corresponds to the if-case in function openDialogModal(). Similarly, the dialogRejected() function corresponds to the else-case of openDialogModal() and is connected to the "rejected" signal emitted when the dialog is canceled.
As you can see, the change from modal to modeless is straightforward and involves changing from working with a return value to working with functions for the "accepted" and "rejected" signals. Which version to use is mainly a question of whether the dialog box is supposed to get important information from the user before being able to continue, or whether the dialog is a way for the user to provide input or change parameters at any time while the program is executed.
One interesting observation if you revisit the code from the three examples in this section, in particular examples 2 and 3, is that while the script code can become rather long, most of this code is for creating the different widgets and arranging them in a nice way. Compared to that, there is not much code needed for wiring up the widgets and implementing the actual functionality. Admittedly, our toy examples didn’t have a lot of functionality included, but it still should be obvious that a lot of time and effort could be saved by using visual tools for producing the GUI layouts in an intuitive way and then automatically turning them into Python code. This is exactly what the next section will be about.
While it’s good and useful to understand how to write Python code to create a GUI directly, it’s obviously a very laborious and time consuming approach that requires writing a lot of code. So together with the advent of early GUI frameworks, people also started to look into more visual approaches in which the GUI of a new software application is clicked and dragged together from predefined building blocks. The typical approach is that first the GUI is created within the graphical GUI building tool and then the tool translates the graphical design into code of the respective programming language that is then included into the main code of the new software application.
The GUI building tool for QT that we are going to use in this lesson is called QT Designer. QT Designer is included in the PyQT5 Python package. The tool itself is platform and programming language independent. Instead of producing code from a particular programming language, it creates a .ui file with an xml description of the QT-based GUI. This .ui file can then be translated into Python code with the pyuic5 GUI compiler tool that also comes with PyQT5. There is also support for directly reading the .ui file from Python code in PyQT5 and then generating the GUI from its content, but we here will use the approach of creating the Python code with pyui5 because it is faster and allows us to see and inspect the Python code for our application. However, you will see an example of reading in the content of the .ui file directly in Lesson 4. In the following, we will take a quick look at how the QT Designer works.
Since you already installed PyQt5 from the ArcGIS Pro package manager, the QT Designer executable will be in your ArcGIS Pro default Python environment folder, either under "C:\Users\<username>\AppData\Local\ESRI\conda\envs\arcgispro-py3-clone\Library\bin\designer.exe" or under “C:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\Library\bin\designer.exe”. It might be good idea to create a shortcut to this .exe file on your desktop for the duration of the course, allowing you to directly start the application. After starting, QT Designer will greet you as shown in the figure below:
QT Designer allows for creating so-called “forms” which can be the GUI for a dialog box, a main window, or just a simple QWidget. Each form is saved as a single .ui file. To create a new form, you pick one of the templates listed in the “New Form” dialog. Go ahead and double-click the “Main Window” template. As a result, you will now see an empty main window form in the central area of the QT Designer window.
Let’s quickly go through the main windows of QT Designer. On the left side, you see the “Widget Box” pane that lists all the widgets available including layout widgets and spacers. Adding a widget to the current form can be done by simply dragging the widget from the pane and dropping it somewhere on the form. Go ahead and place a few different widgets like push buttons, labels, and line edits somewhere on the main window form. When you do a right-click in an empty part of the central area of the main window form, you can pick “Lay out” in the context menu that pops up to set the layout that should be used to arrange the child widgets. Do this and pick “Lay out horizontally” which should result in all the widgets you added being arranged in a single row. See what happens if you instead change to a grid or vertical layout. You can change the layout of any widget that contains other widgets in this way.
On the right side of QT Designer, there are three different panes. The one at the top called “Object Inspector” shows you the hierarchy of all the widgets of the current form. This currently should show you that you have a QMainWindow widget with a QWidget for its central area, which in turn has several child widgets, namely the widgets you added to it. You can pretty much perform the same set of operations that are available when interacting with a widget in the form (like changing its layout) with the corresponding entry in the “Object Inspector” hierarchy. You can also drag and drop widgets onto entries in the hierarchy to add new child widgets to these entries, which can sometimes be easier than dropping them on widgets in the form, e.g., when the parent widget is rather small.
The “Object” column lists the object name for each widget in the hierarchy. This name is important because when turning a GUI form into Python code, the object name will become the name of the variable containing that widget. So if you need to access the widget from your main code, you need to know that name and it’s a good idea to give these widgets intuitive names. To change the object name to something that is easier to recognize and remember, you can double-click the name to edit it, or you can use “Change objectName” from the context menu when you right-click on the entry in the hierarchy or the widget itself.
Below the “Object Inspector” window is the “Property Editor”. This shows you all the properties of the currently selected widget and allows you to change them. The yellow area lists properties that all widgets have, while the green and blue areas below it (you may have to scroll down to see these) list special properties of that widget class. For instance, if you select a push button you added to your main window form, you will find a property called “text” in the green area. This property specifies the text that will be displayed on the button. Click on the “Value” column for that property, enter “Push me”, and see how the text displayed on the button in the main window form changes accordingly. Some properties can also be changed by double-clicking the widget in the form. For instance, you can also change the text property of a push button or label by double-clicking it.
If you double-click where it says “Type Here” at the top, you can add a menu to the menu bar of the main window. Give this a try and call the menu “File”.
The last pane on the right side has three tabs below it. “Resource browser” allows for managing additional resources, like files containing icons to be used as part of the GUI. “Action editor” allows for creating actions for your GUI. Remember that actions are for things that can be initiated via different GUI elements. If you click the “New” button at the top, a dialog for creating a new action opens up. You can just type in “test” for the “Text” property and then press “OK”. The new action will now appear as actionTest in the list of actions. You can drag it and drop it on the File menu you created to make this action an entry in that menu.
Finally, the “Signal/Slot Editor” tab allows for connecting signals of the widgets and actions created to slots of other widgets. We will mainly connect signals with event handler functions in our own Python code rather than in QT Designer, but if some widgets’ signals should be directly connected to slots of other widgets this can already be done here.
You now know the main components of QT Designer and a bit about how to place and arrange widgets in a form. We cannot teach QT Designer in detail here but we will show you an example of creating a larger GUI in QT Designer as part of the following first walkthrough of this lesson. If you want to learn more, there exist quite a few videos explaining different aspects of how to use QT Designer on the web. For now, here is a short video (12:21min) that we recommend checking out to see a few more basic examples of the interactions we briefly described above before moving on to the walkthrough.
Once you have created the form(s) for the GUI of your program and saved them as .ui file(s), you can translate them into Python code with the help of the pyuic5 tool, e.g. by running a command like the following from the command line using the tool directly with Python :
"C:\Users\<username>\AppData\Local\ESRI\conda\envs\arcgispro-py3-clone\python.exe" –m PyQt5.uic.pyuic mainwindow.ui –o mainwindow.py
or
"C:\Program Files\ArcGIS\Pro\bin\Python\envs\arcgispro-py3\python.exe" –m PyQt5.uic.pyuic mainwindow.ui –o mainwindow.py
, depending on where your default Python environment is located (don't forget to replace <username> with your actual user name in the first version and if you get an error with this command try typing it in, not copying/pasting). mainwindow.ui, here, is the name of the file produced with QT Designer, and what follows the -o is the name of the output file that should be produced with the Python version of the GUI. If you want, you can do this for your own .ui file now and have a quick look at the produced .py file and see whether or not you understand some of the things happening in it. We will demonstrate how to use the produced .py file to create the GUI from your Python code as part of the walkthrough in the next section.
Links
[1] https://www.e-education.psu.edu/geog489/l2_p6.html
[2] https://www.e-education.psu.edu/geog489/2221
[3] https://www.e-education.psu.edu/geog489/sites/www.e-education.psu.edu.geog489/files/downloads/QT_Example3_modal.zip
[4] https://www.e-education.psu.edu/geog489/sites/www.e-education.psu.edu.geog489/files/downloads/psuPNG.zip