├── README.md ├── app_nano.py ├── app_simple_pyqt5.py ├── app_simple_pyside2.py ├── app_simple_pyside6.py ├── app_table_people.py └── app_table_word_pairs.py /README.md: -------------------------------------------------------------------------------- 1 | # Snek Byte's Python Qt Cookbook 2 | 3 | This cookbook is here to provide SIMPLE examples of how to do 4 | common things when making apps with Qt in Python. Qt is extremely 5 | flexible and powerful, but that sometimes means that it can be 6 | complicated or cumbersome to do certain things. 7 | 8 | The cookbook aims to cover a lot of common tasks, and demonstrate 9 | concepts that are useful for Python Qt developers to know. This README 10 | provides descriptions of all the code in this repo (below), along with 11 | some high level overviews of Qt concepts and common gotchas/issues. 12 | 13 | There are a few different Python Qt libraries (PySide, PyQt), and 14 | while these libraries are nearly identical, some minor differences 15 | exist. The `app_simple` examples provide working sample code for all 16 | of the major libraries. The rest of the examples target PySide6. It 17 | should be easy to use or adapt any of the examples, whether you're 18 | using PySide6, PySide2, or PyQt5. 19 | 20 | # Before You Start 21 | 22 | See the setup section below if you need help getting set up running 23 | the examples here. 24 | 25 | # What's Covered In This Cookbook Project 26 | 27 | Lightning summary/bullet points: 28 | - Basic app initialization and startup 29 | - Layouts 30 | - Custom widgets 31 | - Many standard widgets and controls 32 | - Signals/slots 33 | - Model/view features (tables, lists, etc.) 34 | - Common gotchas/issues 35 | 36 | If you're looking to get a basic app up and running quickly, check the 37 | `app_nano.py` sample as it covers app startup and not much else. If you're 38 | looking for easy examples of built-in widgets, layouts and controls, check 39 | the `app_simple_*.py` samples. There are also samples of the Qt model/view 40 | features, which are typically used to display tables and lists of data 41 | (this is QT's implementation of the 42 | [MVC/Model-View-Controller design pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)). 43 | 44 | Right now, the demos in this repo use the programmatic API (meaning 45 | you write code to define the look and logic of your app). Qt also has 46 | its own declarative UI language called QML that can potentially make 47 | it easier to read and understand the layout of an application. Some 48 | QML samples should be coming in the future. 49 | 50 | # Things You Should Know 51 | 52 | ## Layouts 53 | 54 | Qt's [layout system](https://doc.qt.io/qt-6/layout.html) is unique. Qt uses a 55 | collaborative layout philosophy, where child widgets in a layout ask for space, 56 | and the layout tries to accommodate each of them as best it can. This can lead 57 | to some frustrating behaviors, as seemingly bizarre widget spacing or alignment 58 | problems can pop up if you don't have a good grasp of how things work. 59 | 60 | Widgets can compete for space, like unruly children, so a problem with one 61 | widget might actually have its root cause in another competing widget in the 62 | layout. When widgets have competing goals, the layout, like a patient parent, 63 | will try to compromise and satisfy both of them as best it can. 64 | 65 | For example, if two widgets in a layout have a "take all available vertical space" 66 | behavior (if both have a vertical `sizePolicy` of `minimumExpanding`, for example), 67 | the layout will first try to give all children at least as much space as their 68 | `sizeHint()` suggests, and any leftover space will generally be split between the 69 | two greedy widgets. 70 | 71 | ### There's too much space between my widgets! 72 | 73 | image 74 | 75 | If you find yourself with large, unwanted space between your widgets, a good way to solve that is to add a stretchable space in the layout to push your widgets up/down or left/right. 76 | 77 | Code for the left layout: 78 | 79 | ``` 80 | layout = QVBoxLayout() 81 | 82 | push_a = QPushButton('Run A') 83 | layout.addWidget(push_a) 84 | 85 | push_b = QPushButton('Run B') 86 | layout.addWidget(push_b) 87 | 88 | push_c = QPushButton('Run C') 89 | layout.addWidget(push_c) 90 | 91 | push_d = QPushButton('Run D') 92 | layout.addWidget(push_d) 93 | 94 | push_e = QPushButton('Run E') 95 | layout.addWidget(push_e) 96 | ``` 97 | 98 | Code for the middle layout: 99 | 100 | ``` 101 | layout = QVBoxLayout() 102 | layout.addStretch() 103 | 104 | push_a = QPushButton('Run A') 105 | layout.addWidget(push_a) 106 | 107 | push_b = QPushButton('Run B') 108 | layout.addWidget(push_b) 109 | 110 | push_c = QPushButton('Run C') 111 | layout.addWidget(push_c) 112 | 113 | push_d = QPushButton('Run D') 114 | layout.addWidget(push_d) 115 | 116 | push_e = QPushButton('Run E') 117 | layout.addWidget(push_e) 118 | ``` 119 | 120 | Code for the right layout: 121 | 122 | ``` 123 | layout = QVBoxLayout() 124 | 125 | push_a = QPushButton('Run A') 126 | layout.addWidget(push_a) 127 | 128 | push_b = QPushButton('Run B') 129 | layout.addWidget(push_b) 130 | 131 | push_c = QPushButton('Run C') 132 | layout.addWidget(push_c) 133 | layout.addStretch() 134 | 135 | push_d = QPushButton('Run D') 136 | layout.addWidget(push_d) 137 | 138 | push_e = QPushButton('Run E') 139 | layout.addWidget(push_e) 140 | ``` 141 | 142 | You're not really aligning your widgets to the top/bottom, in reality, 143 | `addStretch()` adds a `QSpacerItem` that requests/consumes extra vertical 144 | space from the layout in the example above. This reflects Qt's collaborative 145 | layout philosophy, where each widget tells the layout how much space it wants. 146 | The buttons above don't want any extra vertical space, but a QSpacerItem will 147 | request as much space as it can get, so it takes any extra space that's left 148 | over after the buttons take up what little vertical space they need. 149 | 150 | ### I can't shrink my window! 151 | 152 | ![resizing_gif1](https://github.com/ericsnekbytes/python_qt_cookbook/assets/104786633/cbc0b89b-02de-4fe8-9445-d8cbd1c38b82) 153 | 154 | Sometimes you'll find that your window won't shrink. Typically this is because 155 | your widgets are consuming too much space and don't have a good minimum width 156 | (or height) set. There are several ways to fix this, but perhaps the simplest 157 | way is to just set a minimum width of 1 on the widget: 158 | 159 | ``` 160 | # The top widget in the example, has a really long label with no minimum set 161 | # and prevents the window from shrinking/resizing down 162 | long_label = QLabel('A REALLY REALLY LONG PIECE OF TEXT NOT EVEN KIDDING YOU') 163 | layout.addWidget(long_label) 164 | 165 | # ---------------------------------------------- 166 | 167 | # The bottom widget in the example, has a really long label, AND a minimum width 168 | # set that allows the window to shrink 169 | better_long_label.setMinimumWidth(1) 170 | better_long_label.addWidget(better_long_label) 171 | ``` 172 | 173 | ### I want a specific width but don't know what to set for the height, what do I do? 174 | 175 | If you have a custom widget that you'd like to resize, but only in one dimension, 176 | just use the sizeHint()'s width or height in place of the value you don't care about. 177 | 178 | ``` 179 | # Resize to a width of 400, leave the height as-is 180 | self.resize(400, self.sizeHint().height()) 181 | ``` 182 | 183 | It's often helpful to resize a custom widget after all of its child widgets have been 184 | added to its layout, so a good place to resize is often at the end of the widget's 185 | `__init__` function/constructor. 186 | 187 | ## Signals and slots 188 | 189 | [Signals and slots](https://doc.qt.io/qt-6/signalsandslots.html) are used to 190 | pass data around between different places in your Qt applications. Signals are 191 | fired when something happens (like a button push) in your app, and slots are 192 | functions that get called to react and respond to them. Multiple slots can 193 | connect to a given signal, and signals can be connected to each other to form 194 | signal chains. 195 | 196 | Qt's built-in widgets provide a lot of default signals that you can connect to, 197 | and you can define your own signals and slots to pass your own data around in 198 | your application. QPushButton's `clicked` signal is probably the best example 199 | of a built-in signal, it gets called when a user clicks a button, as shown in 200 | this tiny example widget: 201 | 202 | ``` 203 | class CustomWidget(QWidget): 204 | """A very simple custom widget""" 205 | 206 | def __init__(self): 207 | super().__init__() 208 | 209 | # Set some initial properties 210 | layout = QVBoxLayout() 211 | self.setLayout(layout) 212 | 213 | # Add a text box 214 | text_area = QTextEdit() 215 | layout.addWidget(text_area) 216 | self.text_area = text_area 217 | 218 | # Add a scream button 219 | scream_btn = QPushButton('Scream') 220 | scream_btn.clicked.connect(self.handle_scream) 221 | 222 | def handle_scream(self): 223 | self.text_area.setText('AHHH!') 224 | ``` 225 | 226 | The scream button has a `clicked` signal (since it's a 227 | QPushButton), and the `connect` method (on the `clicked` 228 | signal) hooks that signal up to the widget's `handle_scream` 229 | method. 230 | 231 | To use custom signals on your own widgets, you need to define a 232 | Signal object inside the class definition, then `connect(my_handler)` 233 | the signal on your widget instance to a handler function of your choice. 234 | See the code sample/screenshot below: 235 | 236 | ``` 237 | class ChildWidget(QWidget): 238 | def __init__(self): 239 | super().__init__() 240 | 241 | layout = QVBoxLayout() 242 | self.setLayout(layout) 243 | 244 | # Add a read-only text box 245 | child_text = QTextEdit() 246 | layout.addWidget(child_text) 247 | self.child_text = child_text 248 | 249 | self.show() 250 | 251 | def handle_incoming_mood(self, mood): 252 | self.child_text.setPlainText('Mood: ' + mood) 253 | 254 | 255 | class CustomWidget(QWidget): 256 | # This is a basic custom signal 257 | mood_change = Signal(str) 258 | 259 | def __init__(self): 260 | super().__init__() 261 | 262 | # Store mood data here 263 | self.mood = '' 264 | 265 | layout = QVBoxLayout() 266 | self.setLayout(layout) 267 | 268 | # Hold a reference to a free floating child window here 269 | child_widget = ChildWidget() 270 | self.child_widget = child_widget 271 | # Connect the custom widget's mood change signal to 272 | # the child's handler function, so the child can react 273 | # to changes on the parent 274 | self.mood_change.connect(child_widget.handle_incoming_mood) 275 | 276 | # Add a 'make happy' button 277 | happy_btn = QPushButton('Make Happy') 278 | happy_btn.clicked.connect(self.handle_happy) 279 | layout.addWidget(happy_btn) 280 | 281 | # Add a 'make confused' button 282 | confused_btn = QPushButton('Make Confused') 283 | confused_btn.clicked.connect(self.handle_confused) 284 | layout.addWidget(confused_btn) 285 | 286 | self.show() 287 | 288 | def handle_happy(self): 289 | self.mood = 'HAPPY' 290 | self.mood_change.emit(self.mood) 291 | 292 | def handle_confused(self): 293 | self.mood = 'CONFUSED' 294 | self.mood_change.emit(self.mood) 295 | ``` 296 | 297 | Here's what that looks like: 298 | 299 | ![image](https://user-images.githubusercontent.com/104786633/209587808-a00aabfc-cd90-441a-b928-3f1095b5f89b.png) 300 | 301 | # Code Overview 302 | 303 | You can browse the source code files above, or clone the repository 304 | to download and run them yourself. See the Setup section if you need 305 | more details. 306 | 307 | ## `app_simple_*.py` examples 308 | 309 | image 310 | 311 | Covered in `app_simple_*.py` modules: 312 | 313 | - Basic app startup 314 | - Simple nested layouts 315 | - Custom widgets 316 | - Standard widgets/controls 317 | - A basic signal/slot example 318 | 319 | ## `app_table_word_pairs` example 320 | 321 | image 322 | 323 | Covered in `app_table_word_pairs.py`: 324 | 325 | - Basic table display (display/read-only) 326 | - QAbstractTableModel 327 | - QTableView 328 | 329 | This module provides a minimal example of how to display custom 330 | data in a table using the model/view classes QAbstractTableModel 331 | and QTableView. This simple example only covers basic display (a 332 | read-only table, no editing features). Check the other samples 333 | if you want model/view editing features. 334 | 335 | ## `app_table_people.py` example 336 | 337 | image 338 | 339 | Covered in `app_table_people.py`: 340 | 341 | - An editable table 342 | - QAbstractTableModel 343 | - QTableView 344 | - QStyledItemDelegate 345 | 346 | This module displays a list of people (Person objects) with 347 | a variety of attributes, with different types, each of which can 348 | be edited. 349 | 350 | ## `app_nano.py` example 351 | 352 | image 353 | 354 | This is a very tiny app skeleton. 355 | 356 | # Setup 357 | 358 | *Full coverage of install/setup issues is not practical here, but this should 359 | cover the basics* 360 | 361 | As noted above, you can browse the source code files on github, or clone 362 | the repository to download and run them yourself. 363 | 364 | You'll need to [install Python](https://www.python.org/downloads/) 365 | (or [install Miniconda](https://conda.io/projects/conda/en/latest/user-guide/install/index.html#regular-installation), 366 | Anaconda is not compatible with PySide6 as of this writing), 367 | download one of the Qt libraries, and clone or download this repository 368 | to run the examples yourself. 369 | 370 | To run each of the different demo modules, just `python app_table_people.py` 371 | (`python` followed by the name of the py file you want to run). 372 | 373 | ## Regular Python Setup 374 | 375 | Once you've installed Python, you can run `pip install PySide6` to install Qt. 376 | If you see an error about the command not being found, you'll need to fix your 377 | `PATH` environment variable (if install succeeded, you may just need to close and 378 | reopen your terminal), or specify the full path to pip (not preferable). 379 | 380 | ## Miniconda Setup 381 | 382 | Once you've installed Miniconda, you can `conda create -n appdemos pip` to 383 | create an environment with pip, `conda activate appdemos` to activate it, then 384 | `pip install PySide6` to install Qt. If you see an error about the command not 385 | being found, you'll need to fix your `PATH` environment variable (if install 386 | succeeded, you may just need to close and reopen your terminal), or specify 387 | the full path to the conda executable (not preferable). 388 | 389 | # Final Thoughts 390 | 391 | Submit an issue to the repo if you want to suggest a change or have 392 | a question or bug report. 393 | -------------------------------------------------------------------------------- /app_nano.py: -------------------------------------------------------------------------------- 1 | """A very tiny app skeleton""" 2 | 3 | 4 | import datetime 5 | import random 6 | import sys 7 | 8 | from PySide6.QtCore import Qt, Signal 9 | from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QTextEdit, QPushButton, 10 | QHBoxLayout) 11 | 12 | 13 | class CustomWidget(QWidget): 14 | """A very simple custom widget""" 15 | 16 | def __init__(self): 17 | super().__init__() 18 | 19 | # Set some initial properties 20 | layout = QVBoxLayout() 21 | self.setWindowTitle('Tiny sample app') 22 | self.setLayout(layout) 23 | 24 | # Add a text box 25 | text_area = QTextEdit() 26 | text_area.setAcceptRichText(False) 27 | msg = 'Hello! Type here, or hit "Time to Text"' 28 | text_area.setPlainText(msg) 29 | layout.addWidget(text_area) 30 | self.text_area = text_area 31 | 32 | # Some controls at the bottom of the window 33 | lower_row = QHBoxLayout() 34 | lower_row.setContentsMargins(0, 0, 0, 0) 35 | layout.addLayout(lower_row) 36 | 37 | # Button for showing the time/some random data 38 | time_to_text_btn = QPushButton('Time to Text') 39 | # Add a tooltip (for on-hover) 40 | time_to_text_btn.setToolTip('Show a sample message in the text box') 41 | time_to_text_btn.clicked.connect(self.handle_press_time_to_text) 42 | lower_row.addStretch(1) # Push the button to the right side 43 | lower_row.addWidget(time_to_text_btn) 44 | 45 | # Size the widget after adding stuff to the layout 46 | self.resize(750, 500) # Resize children (if needed) below this line 47 | # Make sure you show() the widget! 48 | self.show() 49 | 50 | def handle_press_time_to_text(self): 51 | timestring = datetime.datetime.now().isoformat() 52 | letters = [chr(codepoint) for codepoint in range(ord('A'), ord('A') + 26)] 53 | some_rand_letters = random.choices(population=letters, k=4) 54 | 55 | message = 'The time is: {}\nRandom letters: {}\n'.format( 56 | timestring, 57 | ''.join(some_rand_letters) 58 | ) 59 | self.text_area.setPlainText(message) 60 | 61 | 62 | def run_gui(): 63 | """Function scoped main app entrypoint""" 64 | # Initialize the QApplication! 65 | app = QApplication(sys.argv) 66 | 67 | # This widget shows itself (the main GUI entrypoint) 68 | my_widget = CustomWidget() 69 | 70 | # Run the program/start the event loop with exec() 71 | sys.exit(app.exec()) 72 | 73 | 74 | if __name__ == '__main__': 75 | run_gui() 76 | -------------------------------------------------------------------------------- /app_simple_pyqt5.py: -------------------------------------------------------------------------------- 1 | """Simple demo app for common Qt features. 2 | 3 | This demo shows basic app init and startup, custom widgets, layouts, 4 | commonly used standard widgets, signals and slots, and popup dialogs. 5 | """ 6 | 7 | 8 | import datetime 9 | import os.path 10 | import random 11 | import sys 12 | 13 | from PyQt5.QtCore import Qt, pyqtSignal 14 | from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QTextEdit, QPushButton, 15 | QHBoxLayout, QSplitter, QLabel, QMessageBox, QFileDialog, QLineEdit, 16 | QRadioButton, QGroupBox, QCheckBox) 17 | 18 | 19 | class ChildWidget(QWidget): 20 | """A simple child widget of the main custom widget""" 21 | 22 | def __init__(self): 23 | super().__init__() 24 | 25 | # Set up some basics 26 | layout = QVBoxLayout() 27 | self.setWindowTitle('Cool child window') 28 | self.setLayout(layout) 29 | 30 | # Add a basic label 31 | layout.addWidget(QLabel('Status:')) 32 | 33 | # Add a read-only text box 34 | child_text = QTextEdit() 35 | child_text.setPlainText('EMPTY') 36 | child_text.setTextInteractionFlags(Qt.TextSelectableByMouse) 37 | layout.addWidget(child_text) 38 | self.child_text = child_text 39 | 40 | # Size the widget after adding stuff to the layout 41 | self.resize(400, 300) # Resize children (if needed) below this line 42 | 43 | def handle_incoming_mood(self, mood): 44 | """This is an example slot (a function) for mood change signals""" 45 | letters = [chr(codepoint) for codepoint in range(ord('A'), ord('A') + 26)] 46 | some_rand_letters = random.choices(population=letters, k=4) 47 | 48 | # Make a message with the mood and some random letters 49 | message = 'This window is {}\n\nRandom letters: {}'.format( 50 | mood, 51 | ''.join(some_rand_letters) 52 | ) 53 | 54 | self.child_text.setPlainText(message) 55 | 56 | 57 | class CustomWidget(QWidget): 58 | """A very simple custom widget""" 59 | 60 | # This is a basic custom signal 61 | mood_change = pyqtSignal(str) 62 | 63 | def __init__(self): 64 | super().__init__() 65 | 66 | # Set some initial properties 67 | layout = QVBoxLayout() 68 | self.setWindowTitle('Basics sample app') 69 | self.setLayout(layout) 70 | 71 | # Add a main area with a draggable divider 72 | primary_area = QSplitter(orientation=Qt.Horizontal) 73 | layout.addWidget(primary_area) 74 | 75 | # Add a text box 76 | left_text_area = QTextEdit() 77 | left_text_area.setAcceptRichText(False) 78 | msg = 'Hello! Type here, or hit "Time to Text"' 79 | left_text_area.setPlainText(msg) 80 | primary_area.addWidget(left_text_area) 81 | self.left_text_area = left_text_area 82 | 83 | # Add a container widget with a horizontal layout 84 | right_control_area = QWidget() 85 | right_layout = QVBoxLayout() 86 | right_layout.setContentsMargins(0, 0, 0, 0) 87 | right_control_area.setLayout(right_layout) 88 | primary_area.addWidget(right_control_area) 89 | 90 | # "Shout" button sample 91 | shout_lbl = QLabel('Some buttons') 92 | shout_btn = QPushButton('Shout') 93 | shout_btn.clicked.connect(self.handle_press_shout) 94 | right_layout.addWidget(shout_lbl) 95 | right_layout.addWidget(shout_btn) 96 | # Add a stretchable space to consume space at 97 | # the bottom of the layout (pushes other stuff up) 98 | right_layout.addStretch(1) 99 | self.shout_btn = shout_btn 100 | 101 | # Food preference controls 102 | right_layout.addWidget(QLabel('Food Preferences'), alignment=Qt.AlignRight) 103 | food_layout = QHBoxLayout() 104 | food_layout.addStretch() 105 | right_layout.addLayout(food_layout) 106 | # .......................... 107 | breakfast_cb = QCheckBox('Breakfast') 108 | breakfast_cb.stateChanged.connect(self.handle_food_check) 109 | food_layout.addWidget(breakfast_cb) 110 | self.breakfast_cb = breakfast_cb 111 | # ........................... 112 | lunch_cb = QCheckBox('Lunch') 113 | lunch_cb.stateChanged.connect(self.handle_food_check) 114 | food_layout.addWidget(lunch_cb) 115 | self.lunch_cb = lunch_cb 116 | # ............................. 117 | dinner_cb = QCheckBox('Dinner') 118 | dinner_cb.stateChanged.connect(self.handle_food_check) 119 | food_layout.addWidget(dinner_cb) 120 | self.dinner_cb = dinner_cb 121 | 122 | # File picker controls 123 | # ..................... 124 | file_pick_lbl = QLabel('Basic file picker') 125 | right_layout.addWidget(file_pick_lbl, alignment=Qt.AlignRight) 126 | # ........................... 127 | # Put the controls together in a group box 128 | file_picker_box = QGroupBox() 129 | file_picker_layout = QVBoxLayout() 130 | file_picker_box.setLayout(file_picker_layout) 131 | right_layout.addWidget(file_picker_box) 132 | # ........................ 133 | # Use a row/stretchable space for better button sizing 134 | picker_row = QHBoxLayout() 135 | picker_row.addStretch() 136 | file_picker_layout.addLayout(picker_row) 137 | # ................................ 138 | file_picker_btn = QPushButton('Pick File') 139 | file_picker_btn.clicked.connect(self.handle_pick_file) 140 | picker_row.addWidget(file_picker_btn) 141 | # ................................... 142 | file_picker_result_field = QLineEdit() 143 | file_picker_result_field.setPlaceholderText('Pick a file...') 144 | file_picker_result_field.setAlignment(Qt.AlignRight) 145 | file_picker_result_field.setReadOnly(True) 146 | file_picker_layout.addWidget(file_picker_result_field) 147 | self.file_picker_result_field = file_picker_result_field 148 | 149 | # Fruit picker controls 150 | right_layout.addWidget(QLabel('Fruit picker'), alignment=Qt.AlignRight) 151 | fruit_choices = QGroupBox() 152 | fruit_layout = QHBoxLayout() 153 | fruit_choices.setLayout(fruit_layout) 154 | right_layout.addWidget(fruit_choices) 155 | # ............................... 156 | apple_btn = QRadioButton('Apple') 157 | apple_btn.setChecked(True) 158 | fruit_layout.addWidget(apple_btn) 159 | banana_btn = QRadioButton('Banana') 160 | fruit_layout.addWidget(banana_btn) 161 | kiwi_btn = QRadioButton('Kiwi') 162 | fruit_layout.addWidget(kiwi_btn) 163 | 164 | # Some controls at the bottom of the window 165 | lower_row = QHBoxLayout() 166 | lower_row.setContentsMargins(0, 0, 0, 0) 167 | layout.addLayout(lower_row) 168 | 169 | # Button for showing the time/some random data 170 | time_to_text_btn = QPushButton('Time to Text') 171 | # Add a tooltip (for on-hover) 172 | time_to_text_btn.setToolTip('Show a sample message in the text box') 173 | time_to_text_btn.clicked.connect(self.handle_press_time_to_text) 174 | lower_row.addWidget(time_to_text_btn) 175 | lower_row.addStretch(1) # Space the buttons apart 176 | 177 | # Hold a hidden child widget (separate window) 178 | child_widget = ChildWidget() 179 | # Connect the mood_change signal to the child's 180 | # handle_incoming_mood slot (basic signal/slot example) 181 | self.mood_change.connect(child_widget.handle_incoming_mood) 182 | self.child_widget = child_widget 183 | 184 | # Controls for the child window 185 | # ............................. 186 | show_child_btn = QPushButton('Show child') 187 | show_child_btn.setToolTip('Show a child window') 188 | show_child_btn.clicked.connect(self.handle_show_child) 189 | lower_row.addWidget(show_child_btn) 190 | self.show_child_btn = show_child_btn 191 | # .................................. 192 | child_happy_btn = QPushButton('Make child happy') 193 | child_happy_btn.clicked.connect(self.handle_child_mood) 194 | lower_row.addWidget(child_happy_btn) 195 | self.child_happy_btn = child_happy_btn 196 | # .................................. 197 | child_confused_btn = QPushButton('Make child confused') 198 | child_confused_btn.clicked.connect(self.handle_child_mood) 199 | lower_row.addWidget(child_confused_btn) 200 | self.child_confused_btn = child_confused_btn 201 | 202 | # Size the widget after adding stuff to the layout 203 | self.resize(900, 600) 204 | primary_area.setSizes([2 * self.width() // 3, self.width() // 3]) 205 | # Make sure you show() the widget! 206 | self.show() 207 | 208 | def handle_press_time_to_text(self): 209 | timestring = datetime.datetime.now().isoformat() 210 | letters = [chr(codepoint) for codepoint in range(ord('A'), ord('A') + 26)] 211 | some_rand_letters = random.choices(population=letters, k=4) 212 | 213 | message = 'The time is: {}\nRandom letters: {}\n'.format( 214 | timestring, 215 | ''.join(some_rand_letters) 216 | ) 217 | self.left_text_area.setPlainText(message) 218 | 219 | def handle_press_shout(self): 220 | # Show a box with some shout options 221 | box = QMessageBox() 222 | box.setStandardButtons( 223 | QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel 224 | ) 225 | box.setWindowTitle('Shout Options') 226 | box.setText('Pick a shout') 227 | yes_btn = box.button(QMessageBox.Yes) 228 | yes_btn.setText('Shout YAY') 229 | no_btn = box.button(QMessageBox.No) 230 | no_btn.setText('Shout NAW') 231 | 232 | # Run the dialog/get the result 233 | result = box.exec() 234 | 235 | # Show another info box with the result 236 | if result == QMessageBox.Yes: 237 | QMessageBox.information( 238 | self, 'Shouting', "I'm shouting YAY!!" 239 | ) 240 | if result == QMessageBox.No: 241 | QMessageBox.information( 242 | self, 'Shouting', "I'm shouting NAW!!" 243 | ) 244 | if result == QMessageBox.Cancel: 245 | QMessageBox.information( 246 | self, 'Canceled', 'You canceled the shout' 247 | ) 248 | 249 | def handle_pick_file(self): 250 | filepath, filefilter = QFileDialog.getOpenFileName(self) 251 | 252 | if filepath: 253 | self.file_picker_result_field.setText(os.path.basename(filepath)) 254 | else: 255 | self.file_picker_result_field.clear() 256 | 257 | def handle_food_check(self, state): 258 | meal_type = '' 259 | if self.sender() is self.breakfast_cb: 260 | meal_type = 'breakfast' 261 | if self.sender() is self.lunch_cb: 262 | meal_type = 'lunch' 263 | if self.sender() is self.dinner_cb: 264 | meal_type = 'dinner' 265 | 266 | if state: 267 | QMessageBox.information( 268 | self, 269 | 'Meal updated!', 270 | '{} will be served.'.format(meal_type.title()) 271 | ) 272 | else: 273 | QMessageBox.information( 274 | self, 275 | 'Meal updated!', 276 | 'Canceling {}.'.format(meal_type) 277 | ) 278 | 279 | def handle_show_child(self): 280 | self.child_widget.show() 281 | 282 | def handle_child_mood(self): 283 | # Determine which button was clicked using self.sender(), 284 | # then emit the mood_change signal with a string (signals 285 | # are a main way of passing information around Qt) 286 | if self.sender() is self.child_happy_btn: 287 | self.mood_change.emit('HAPPY') 288 | if self.sender() is self.child_confused_btn: 289 | self.mood_change.emit('CONFUSED') 290 | 291 | 292 | def run_gui(): 293 | """Function scoped main app entrypoint""" 294 | # Initialize the QApplication! 295 | app = QApplication(sys.argv) 296 | 297 | # This widget shows itself (the main GUI entrypoint) 298 | my_widget = CustomWidget() 299 | 300 | # Run the program/start the event loop with exec() 301 | sys.exit(app.exec()) 302 | 303 | 304 | if __name__ == '__main__': 305 | run_gui() 306 | -------------------------------------------------------------------------------- /app_simple_pyside2.py: -------------------------------------------------------------------------------- 1 | """Simple demo app for common Qt features. 2 | 3 | This demo shows basic app init and startup, custom widgets, layouts, 4 | commonly used standard widgets, signals and slots, and popup dialogs. 5 | """ 6 | 7 | 8 | import datetime 9 | import os.path 10 | import random 11 | import sys 12 | 13 | from PySide2.QtCore import Qt, Signal 14 | from PySide2.QtWidgets import (QApplication, QWidget, QVBoxLayout, QTextEdit, QPushButton, 15 | QHBoxLayout, QSplitter, QLabel, QMessageBox, QFileDialog, QLineEdit, 16 | QRadioButton, QGroupBox, QCheckBox) 17 | 18 | 19 | class ChildWidget(QWidget): 20 | """A simple child widget of the main custom widget""" 21 | 22 | def __init__(self): 23 | super().__init__() 24 | 25 | # Set up some basics 26 | layout = QVBoxLayout() 27 | self.setWindowTitle('Cool child window') 28 | self.setLayout(layout) 29 | 30 | # Add a basic label 31 | layout.addWidget(QLabel('Status:')) 32 | 33 | # Add a read-only text box 34 | child_text = QTextEdit() 35 | child_text.setPlainText('EMPTY') 36 | child_text.setTextInteractionFlags(Qt.TextSelectableByMouse) 37 | layout.addWidget(child_text) 38 | self.child_text = child_text 39 | 40 | # Size the widget after adding stuff to the layout 41 | self.resize(400, 300) # Resize children (if needed) below this line 42 | 43 | def handle_incoming_mood(self, mood): 44 | """This is an example slot (a function) for mood change signals""" 45 | letters = [chr(codepoint) for codepoint in range(ord('A'), ord('A') + 26)] 46 | some_rand_letters = random.choices(population=letters, k=4) 47 | 48 | # Make a message with the mood and some random letters 49 | message = 'This window is {}\n\nRandom letters: {}'.format( 50 | mood, 51 | ''.join(some_rand_letters) 52 | ) 53 | 54 | self.child_text.setPlainText(message) 55 | 56 | 57 | class CustomWidget(QWidget): 58 | """A very simple custom widget""" 59 | 60 | # This is a basic custom signal 61 | mood_change = Signal(str) 62 | 63 | def __init__(self): 64 | super().__init__() 65 | 66 | # Set some initial properties 67 | layout = QVBoxLayout() 68 | self.setWindowTitle('Basics sample app') 69 | self.setLayout(layout) 70 | 71 | # Add a main area with a draggable divider 72 | primary_area = QSplitter(orientation=Qt.Horizontal) 73 | layout.addWidget(primary_area) 74 | 75 | # Add a text box 76 | left_text_area = QTextEdit() 77 | left_text_area.setAcceptRichText(False) 78 | msg = 'Hello! Type here, or hit "Time to Text"' 79 | left_text_area.setPlainText(msg) 80 | primary_area.addWidget(left_text_area) 81 | self.left_text_area = left_text_area 82 | 83 | # Add a container widget with a horizontal layout 84 | right_control_area = QWidget() 85 | right_layout = QVBoxLayout() 86 | right_layout.setContentsMargins(0, 0, 0, 0) 87 | right_control_area.setLayout(right_layout) 88 | primary_area.addWidget(right_control_area) 89 | 90 | # "Shout" button sample 91 | shout_lbl = QLabel('Some buttons') 92 | shout_btn = QPushButton('Shout') 93 | shout_btn.clicked.connect(self.handle_press_shout) 94 | right_layout.addWidget(shout_lbl) 95 | right_layout.addWidget(shout_btn) 96 | # Add a stretchable space to consume space at 97 | # the bottom of the layout (pushes other stuff up) 98 | right_layout.addStretch(1) 99 | self.shout_btn = shout_btn 100 | 101 | # Food preference controls 102 | right_layout.addWidget(QLabel('Food Preferences'), alignment=Qt.AlignRight) 103 | food_layout = QHBoxLayout() 104 | food_layout.addStretch() 105 | right_layout.addLayout(food_layout) 106 | # .......................... 107 | breakfast_cb = QCheckBox('Breakfast') 108 | breakfast_cb.stateChanged.connect(self.handle_food_check) 109 | food_layout.addWidget(breakfast_cb) 110 | self.breakfast_cb = breakfast_cb 111 | # ........................... 112 | lunch_cb = QCheckBox('Lunch') 113 | lunch_cb.stateChanged.connect(self.handle_food_check) 114 | food_layout.addWidget(lunch_cb) 115 | self.lunch_cb = lunch_cb 116 | # ............................. 117 | dinner_cb = QCheckBox('Dinner') 118 | dinner_cb.stateChanged.connect(self.handle_food_check) 119 | food_layout.addWidget(dinner_cb) 120 | self.dinner_cb = dinner_cb 121 | 122 | # File picker controls 123 | # ..................... 124 | file_pick_lbl = QLabel('Basic file picker') 125 | right_layout.addWidget(file_pick_lbl, alignment=Qt.AlignRight) 126 | # ........................... 127 | # Put the controls together in a group box 128 | file_picker_box = QGroupBox() 129 | file_picker_layout = QVBoxLayout() 130 | file_picker_box.setLayout(file_picker_layout) 131 | right_layout.addWidget(file_picker_box) 132 | # ........................ 133 | # Use a row/stretchable space for better button sizing 134 | picker_row = QHBoxLayout() 135 | picker_row.addStretch() 136 | file_picker_layout.addLayout(picker_row) 137 | # ................................ 138 | file_picker_btn = QPushButton('Pick File') 139 | file_picker_btn.clicked.connect(self.handle_pick_file) 140 | picker_row.addWidget(file_picker_btn) 141 | # ................................... 142 | file_picker_result_field = QLineEdit() 143 | file_picker_result_field.setPlaceholderText('Pick a file...') 144 | file_picker_result_field.setAlignment(Qt.AlignRight) 145 | file_picker_result_field.setReadOnly(True) 146 | file_picker_layout.addWidget(file_picker_result_field) 147 | self.file_picker_result_field = file_picker_result_field 148 | 149 | # Fruit picker controls 150 | right_layout.addWidget(QLabel('Fruit picker'), alignment=Qt.AlignRight) 151 | fruit_choices = QGroupBox() 152 | fruit_layout = QHBoxLayout() 153 | fruit_choices.setLayout(fruit_layout) 154 | right_layout.addWidget(fruit_choices) 155 | # ............................... 156 | apple_btn = QRadioButton('Apple') 157 | apple_btn.setChecked(True) 158 | fruit_layout.addWidget(apple_btn) 159 | banana_btn = QRadioButton('Banana') 160 | fruit_layout.addWidget(banana_btn) 161 | kiwi_btn = QRadioButton('Kiwi') 162 | fruit_layout.addWidget(kiwi_btn) 163 | 164 | # Some controls at the bottom of the window 165 | lower_row = QHBoxLayout() 166 | lower_row.setContentsMargins(0, 0, 0, 0) 167 | layout.addLayout(lower_row) 168 | 169 | # Button for showing the time/some random data 170 | time_to_text_btn = QPushButton('Time to Text') 171 | # Add a tooltip (for on-hover) 172 | time_to_text_btn.setToolTip('Show a sample message in the text box') 173 | time_to_text_btn.clicked.connect(self.handle_press_time_to_text) 174 | lower_row.addWidget(time_to_text_btn) 175 | lower_row.addStretch(1) # Space the buttons apart 176 | 177 | # Hold a hidden child widget (separate window) 178 | child_widget = ChildWidget() 179 | # Connect the mood_change signal to the child's 180 | # handle_incoming_mood slot (basic signal/slot example) 181 | self.mood_change.connect(child_widget.handle_incoming_mood) 182 | self.child_widget = child_widget 183 | 184 | # Controls for the child window 185 | # ............................. 186 | show_child_btn = QPushButton('Show child') 187 | show_child_btn.setToolTip('Show a child window') 188 | show_child_btn.clicked.connect(self.handle_show_child) 189 | lower_row.addWidget(show_child_btn) 190 | self.show_child_btn = show_child_btn 191 | # .................................. 192 | child_happy_btn = QPushButton('Make child happy') 193 | child_happy_btn.clicked.connect(self.handle_child_mood) 194 | lower_row.addWidget(child_happy_btn) 195 | self.child_happy_btn = child_happy_btn 196 | # .................................. 197 | child_confused_btn = QPushButton('Make child confused') 198 | child_confused_btn.clicked.connect(self.handle_child_mood) 199 | lower_row.addWidget(child_confused_btn) 200 | self.child_confused_btn = child_confused_btn 201 | 202 | # Size the widget after adding stuff to the layout 203 | self.resize(900, 600) # Resize children (if needed) below this line 204 | primary_area.setSizes([2 * self.width() / 3, self.width() / 3]) 205 | # Make sure you show() the widget! 206 | self.show() 207 | 208 | def handle_press_time_to_text(self): 209 | timestring = datetime.datetime.now().isoformat() 210 | letters = [chr(codepoint) for codepoint in range(ord('A'), ord('A') + 26)] 211 | some_rand_letters = random.choices(population=letters, k=4) 212 | 213 | message = 'The time is: {}\nRandom letters: {}\n'.format( 214 | timestring, 215 | ''.join(some_rand_letters) 216 | ) 217 | self.left_text_area.setPlainText(message) 218 | 219 | def handle_press_shout(self): 220 | # Show a box with some shout options 221 | box = QMessageBox() 222 | box.setStandardButtons( 223 | QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel 224 | ) 225 | box.setWindowTitle('Shout Options') 226 | box.setText('Pick a shout') 227 | yes_btn = box.button(QMessageBox.Yes) 228 | yes_btn.setText('Shout YAY') 229 | no_btn = box.button(QMessageBox.No) 230 | no_btn.setText('Shout NAW') 231 | 232 | # Run the dialog/get the result 233 | result = box.exec_() 234 | 235 | # Show another info box with the result 236 | if result == QMessageBox.Yes: 237 | QMessageBox.information( 238 | self, 'Shouting', "I'm shouting YAY!!" 239 | ) 240 | if result == QMessageBox.No: 241 | QMessageBox.information( 242 | self, 'Shouting', "I'm shouting NAW!!" 243 | ) 244 | if result == QMessageBox.Cancel: 245 | QMessageBox.information( 246 | self, 'Canceled', 'You canceled the shout' 247 | ) 248 | 249 | def handle_pick_file(self): 250 | filepath, filefilter = QFileDialog.getOpenFileName(self) 251 | 252 | if filepath: 253 | self.file_picker_result_field.setText(os.path.basename(filepath)) 254 | else: 255 | self.file_picker_result_field.clear() 256 | 257 | def handle_food_check(self, state): 258 | meal_type = '' 259 | if self.sender() is self.breakfast_cb: 260 | meal_type = 'breakfast' 261 | if self.sender() is self.lunch_cb: 262 | meal_type = 'lunch' 263 | if self.sender() is self.dinner_cb: 264 | meal_type = 'dinner' 265 | 266 | if state: 267 | QMessageBox.information( 268 | self, 269 | 'Meal updated!', 270 | '{} will be served.'.format(meal_type.title()) 271 | ) 272 | else: 273 | QMessageBox.information( 274 | self, 275 | 'Meal updated!', 276 | 'Canceling {}.'.format(meal_type) 277 | ) 278 | 279 | def handle_show_child(self): 280 | self.child_widget.show() 281 | 282 | def handle_child_mood(self): 283 | # Determine which button was clicked using self.sender(), 284 | # then emit the mood_change signal with a string (signals 285 | # are a main way of passing information around Qt) 286 | if self.sender() is self.child_happy_btn: 287 | self.mood_change.emit('HAPPY') 288 | if self.sender() is self.child_confused_btn: 289 | self.mood_change.emit('CONFUSED') 290 | 291 | 292 | def run_gui(): 293 | """Function scoped main app entrypoint""" 294 | # Initialize the QApplication! 295 | app = QApplication(sys.argv) 296 | 297 | # This widget shows itself (the main GUI entrypoint) 298 | my_widget = CustomWidget() 299 | 300 | # Run the program/start the event loop with exec() 301 | sys.exit(app.exec_()) 302 | 303 | 304 | if __name__ == '__main__': 305 | run_gui() 306 | -------------------------------------------------------------------------------- /app_simple_pyside6.py: -------------------------------------------------------------------------------- 1 | """Simple demo app for common Qt features. 2 | 3 | This demo shows basic app init and startup, custom widgets, layouts, 4 | commonly used standard widgets, signals and slots, and popup dialogs. 5 | """ 6 | 7 | 8 | import datetime 9 | import os.path 10 | import random 11 | import sys 12 | 13 | from PySide6.QtCore import Qt, Signal 14 | from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QTextEdit, QPushButton, 15 | QHBoxLayout, QSplitter, QLabel, QMessageBox, QFileDialog, QLineEdit, 16 | QRadioButton, QGroupBox, QCheckBox) 17 | 18 | 19 | class ChildWidget(QWidget): 20 | """A simple child widget of the main custom widget""" 21 | 22 | def __init__(self): 23 | super().__init__() 24 | 25 | # Set up some basics 26 | layout = QVBoxLayout() 27 | self.setWindowTitle('Cool child window') 28 | self.setLayout(layout) 29 | 30 | # Add a basic label 31 | layout.addWidget(QLabel('Status:')) 32 | 33 | # Add a read-only text box 34 | child_text = QTextEdit() 35 | child_text.setPlainText('EMPTY') 36 | child_text.setTextInteractionFlags(Qt.TextSelectableByMouse) 37 | layout.addWidget(child_text) 38 | self.child_text = child_text 39 | 40 | # Size the widget after adding stuff to the layout 41 | self.resize(400, 300) # Resize children (if needed) below this line 42 | 43 | def handle_incoming_mood(self, mood): 44 | """This is an example slot (a function) for mood change signals""" 45 | letters = [chr(codepoint) for codepoint in range(ord('A'), ord('A') + 26)] 46 | some_rand_letters = random.choices(population=letters, k=4) 47 | 48 | # Make a message with the mood and some random letters 49 | message = 'This window is {}\n\nRandom letters: {}'.format( 50 | mood, 51 | ''.join(some_rand_letters) 52 | ) 53 | 54 | self.child_text.setPlainText(message) 55 | 56 | 57 | class CustomWidget(QWidget): 58 | """A very simple custom widget""" 59 | 60 | # This is a basic custom signal 61 | mood_change = Signal(str) 62 | 63 | def __init__(self): 64 | super().__init__() 65 | 66 | # Set some initial properties 67 | layout = QVBoxLayout() 68 | self.setWindowTitle('Basics sample app') 69 | self.setLayout(layout) 70 | 71 | # Add a main area with a draggable divider 72 | primary_area = QSplitter(orientation=Qt.Horizontal) 73 | layout.addWidget(primary_area) 74 | 75 | # Add a text box 76 | left_text_area = QTextEdit() 77 | left_text_area.setAcceptRichText(False) 78 | msg = 'Hello! Type here, or hit "Time to Text"' 79 | left_text_area.setPlainText(msg) 80 | primary_area.addWidget(left_text_area) 81 | self.left_text_area = left_text_area 82 | 83 | # Add a container widget with a horizontal layout 84 | right_control_area = QWidget() 85 | right_layout = QVBoxLayout() 86 | right_layout.setContentsMargins(0, 0, 0, 0) 87 | right_control_area.setLayout(right_layout) 88 | primary_area.addWidget(right_control_area) 89 | 90 | # "Shout" button sample 91 | shout_lbl = QLabel('Some buttons') 92 | shout_btn = QPushButton('Shout') 93 | shout_btn.clicked.connect(self.handle_press_shout) 94 | right_layout.addWidget(shout_lbl) 95 | right_layout.addWidget(shout_btn) 96 | # Add a stretchable space to consume space at 97 | # the bottom of the layout (pushes other stuff up) 98 | right_layout.addStretch(1) 99 | self.shout_btn = shout_btn 100 | 101 | # Food preference controls 102 | right_layout.addWidget(QLabel('Food Preferences'), alignment=Qt.AlignRight) 103 | food_layout = QHBoxLayout() 104 | food_layout.addStretch() 105 | right_layout.addLayout(food_layout) 106 | # .......................... 107 | breakfast_cb = QCheckBox('Breakfast') 108 | breakfast_cb.stateChanged.connect(self.handle_food_check) 109 | food_layout.addWidget(breakfast_cb) 110 | self.breakfast_cb = breakfast_cb 111 | # ........................... 112 | lunch_cb = QCheckBox('Lunch') 113 | lunch_cb.stateChanged.connect(self.handle_food_check) 114 | food_layout.addWidget(lunch_cb) 115 | self.lunch_cb = lunch_cb 116 | # ............................. 117 | dinner_cb = QCheckBox('Dinner') 118 | dinner_cb.stateChanged.connect(self.handle_food_check) 119 | food_layout.addWidget(dinner_cb) 120 | self.dinner_cb = dinner_cb 121 | 122 | # File picker controls 123 | # ..................... 124 | file_pick_lbl = QLabel('Basic file picker') 125 | right_layout.addWidget(file_pick_lbl, alignment=Qt.AlignRight) 126 | # ........................... 127 | # Put the controls together in a group box 128 | file_picker_box = QGroupBox() 129 | file_picker_layout = QVBoxLayout() 130 | file_picker_box.setLayout(file_picker_layout) 131 | right_layout.addWidget(file_picker_box) 132 | # ........................ 133 | # Use a row/stretchable space for better button sizing 134 | picker_row = QHBoxLayout() 135 | picker_row.addStretch() 136 | file_picker_layout.addLayout(picker_row) 137 | # ................................ 138 | file_picker_btn = QPushButton('Pick File') 139 | file_picker_btn.clicked.connect(self.handle_pick_file) 140 | picker_row.addWidget(file_picker_btn) 141 | # ................................... 142 | file_picker_result_field = QLineEdit() 143 | file_picker_result_field.setPlaceholderText('Pick a file...') 144 | file_picker_result_field.setAlignment(Qt.AlignRight) 145 | file_picker_result_field.setReadOnly(True) 146 | file_picker_layout.addWidget(file_picker_result_field) 147 | self.file_picker_result_field = file_picker_result_field 148 | 149 | # Fruit picker controls 150 | right_layout.addWidget(QLabel('Fruit picker'), alignment=Qt.AlignRight) 151 | fruit_choices = QGroupBox() 152 | fruit_layout = QHBoxLayout() 153 | fruit_choices.setLayout(fruit_layout) 154 | right_layout.addWidget(fruit_choices) 155 | # ............................... 156 | apple_btn = QRadioButton('Apple') 157 | apple_btn.setChecked(True) 158 | fruit_layout.addWidget(apple_btn) 159 | banana_btn = QRadioButton('Banana') 160 | fruit_layout.addWidget(banana_btn) 161 | kiwi_btn = QRadioButton('Kiwi') 162 | fruit_layout.addWidget(kiwi_btn) 163 | 164 | # Some controls at the bottom of the window 165 | lower_row = QHBoxLayout() 166 | lower_row.setContentsMargins(0, 0, 0, 0) 167 | layout.addLayout(lower_row) 168 | 169 | # Button for showing the time/some random data 170 | time_to_text_btn = QPushButton('Time to Text') 171 | # Add a tooltip (for on-hover) 172 | time_to_text_btn.setToolTip('Show a sample message in the text box') 173 | time_to_text_btn.clicked.connect(self.handle_press_time_to_text) 174 | lower_row.addWidget(time_to_text_btn) 175 | lower_row.addStretch(1) # Space the buttons apart 176 | 177 | # Hold a hidden child widget (separate window) 178 | child_widget = ChildWidget() 179 | # Connect the mood_change signal to the child's 180 | # handle_incoming_mood slot (basic signal/slot example) 181 | self.mood_change.connect(child_widget.handle_incoming_mood) 182 | self.child_widget = child_widget 183 | 184 | # Controls for the child window 185 | # ............................. 186 | show_child_btn = QPushButton('Show child') 187 | show_child_btn.setToolTip('Show a child window') 188 | show_child_btn.clicked.connect(self.handle_show_child) 189 | lower_row.addWidget(show_child_btn) 190 | self.show_child_btn = show_child_btn 191 | # .................................. 192 | child_happy_btn = QPushButton('Make child happy') 193 | child_happy_btn.clicked.connect(self.handle_child_mood) 194 | lower_row.addWidget(child_happy_btn) 195 | self.child_happy_btn = child_happy_btn 196 | # .................................. 197 | child_confused_btn = QPushButton('Make child confused') 198 | child_confused_btn.clicked.connect(self.handle_child_mood) 199 | lower_row.addWidget(child_confused_btn) 200 | self.child_confused_btn = child_confused_btn 201 | 202 | # Size the widget after adding stuff to the layout 203 | self.resize(900, 600) # Resize children (if needed) below this line 204 | primary_area.setSizes([2 * self.width() / 3, self.width() / 3]) 205 | # Make sure you show() the widget! 206 | self.show() 207 | 208 | def handle_press_time_to_text(self): 209 | timestring = datetime.datetime.now().isoformat() 210 | letters = [chr(codepoint) for codepoint in range(ord('A'), ord('A') + 26)] 211 | some_rand_letters = random.choices(population=letters, k=4) 212 | 213 | message = 'The time is: {}\nRandom letters: {}\n'.format( 214 | timestring, 215 | ''.join(some_rand_letters) 216 | ) 217 | self.left_text_area.setPlainText(message) 218 | 219 | def handle_press_shout(self): 220 | # Show a box with some shout options 221 | box = QMessageBox() 222 | box.setStandardButtons( 223 | QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel 224 | ) 225 | box.setWindowTitle('Shout Options') 226 | box.setText('Pick a shout') 227 | yes_btn = box.button(QMessageBox.Yes) 228 | yes_btn.setText('Shout YAY') 229 | no_btn = box.button(QMessageBox.No) 230 | no_btn.setText('Shout NAW') 231 | 232 | # Run the dialog/get the result 233 | result = box.exec() 234 | 235 | # Show another info box with the result 236 | if result == QMessageBox.Yes: 237 | QMessageBox.information( 238 | self, 'Shouting', "I'm shouting YAY!!" 239 | ) 240 | if result == QMessageBox.No: 241 | QMessageBox.information( 242 | self, 'Shouting', "I'm shouting NAW!!" 243 | ) 244 | if result == QMessageBox.Cancel: 245 | QMessageBox.information( 246 | self, 'Canceled', 'You canceled the shout' 247 | ) 248 | 249 | def handle_pick_file(self): 250 | filepath, filefilter = QFileDialog.getOpenFileName(self) 251 | 252 | if filepath: 253 | self.file_picker_result_field.setText(os.path.basename(filepath)) 254 | else: 255 | self.file_picker_result_field.clear() 256 | 257 | def handle_food_check(self, state): 258 | meal_type = '' 259 | if self.sender() is self.breakfast_cb: 260 | meal_type = 'breakfast' 261 | if self.sender() is self.lunch_cb: 262 | meal_type = 'lunch' 263 | if self.sender() is self.dinner_cb: 264 | meal_type = 'dinner' 265 | 266 | if state: 267 | QMessageBox.information( 268 | self, 269 | 'Meal updated!', 270 | '{} will be served.'.format(meal_type.title()) 271 | ) 272 | else: 273 | QMessageBox.information( 274 | self, 275 | 'Meal updated!', 276 | 'Canceling {}.'.format(meal_type) 277 | ) 278 | 279 | def handle_show_child(self): 280 | self.child_widget.show() 281 | 282 | def handle_child_mood(self): 283 | # Determine which button was clicked using self.sender(), 284 | # then emit the mood_change signal with a string (signals 285 | # are a main way of passing information around Qt) 286 | if self.sender() is self.child_happy_btn: 287 | self.mood_change.emit('HAPPY') 288 | if self.sender() is self.child_confused_btn: 289 | self.mood_change.emit('CONFUSED') 290 | 291 | 292 | def run_gui(): 293 | """Function scoped main app entrypoint""" 294 | # Initialize the QApplication! 295 | app = QApplication(sys.argv) 296 | 297 | # This widget shows itself (the main GUI entrypoint) 298 | my_widget = CustomWidget() 299 | 300 | # Run the program/start the event loop with exec() 301 | sys.exit(app.exec()) 302 | 303 | 304 | if __name__ == '__main__': 305 | run_gui() 306 | -------------------------------------------------------------------------------- /app_table_people.py: -------------------------------------------------------------------------------- 1 | """Shows people in a table. 2 | 3 | This demo shows a table with some editing features. This builds 4 | upon the simpler app_table_word_pairs example, and shows how 5 | to use a QAbstractTableModel to edit an underlying data source. 6 | """ 7 | 8 | 9 | import re 10 | import sys 11 | 12 | from PySide6.QtCore import Qt, QAbstractTableModel, QSortFilterProxyModel, Signal 13 | from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QTableView, QLabel, QHeaderView, 14 | QHBoxLayout, QLineEdit, QPushButton, QAbstractItemView, 15 | QStyledItemDelegate) 16 | 17 | 18 | class Person: 19 | """Simple demo class for storing person info""" 20 | 21 | def __init__(self, first, middle, last, age, height_mm): 22 | self.first = first 23 | self.middle = middle 24 | self.last = last 25 | self.age = age 26 | self.height_mm = height_mm 27 | 28 | 29 | class PeopleModel(QAbstractTableModel): 30 | """Tells Qt how our person data corresponds to different rows/columns/cells. 31 | 32 | From the Qt documentation (for editable tables): 33 | When subclassing QAbstractTableModel, you must implement rowCount(), 34 | columnCount(), and data(). Default implementations of the index() 35 | and parent() functions are provided by QAbstractTableModel. 36 | Well behaved models will also implement headerData(). 37 | 38 | Editable models need to implement setData(), and implement flags() to 39 | return a value containing Qt::ItemIsEditable. 40 | 41 | Models that provide interfaces to resizable data structures can provide 42 | implementations of insertRows(), removeRows(), insertColumns(), 43 | and removeColumns(). 44 | """ 45 | 46 | FIRST_NAME = 0 47 | MIDDLE_NAME = 1 48 | LAST_NAME = 2 49 | AGE = 3 50 | HEIGHT_MM = 4 51 | 52 | def __init__(self, user_data): 53 | super().__init__() 54 | 55 | # Store the data we're representing 56 | self.model_data = user_data 57 | 58 | # Assign numbers to Person attributes, so we can 59 | # associate them with different column numbers 60 | self.attrib_key = { 61 | # int: ['attrib_name', 'display_name'] 62 | 0: ['first', 'First Name'], 63 | 1: ['middle', 'Middle Name'], 64 | 2: ['last', 'Last Name'], 65 | 3: ['age', 'Age'], 66 | 4: ['height_mm', 'Height (mm)'], 67 | } 68 | 69 | def rowCount(self, parent): 70 | return len(self.model_data) 71 | 72 | def columnCount(self, parent): 73 | """Count how many attribs we're showing in attrib_key""" 74 | return len(self.attrib_key) 75 | 76 | def data(self, index, role): 77 | row = index.row() 78 | col = index.column() 79 | 80 | if index.isValid(): 81 | if role == Qt.DisplayRole: 82 | person = self.model_data[row] 83 | attrib_name, display_val = self.attrib_key[col] 84 | 85 | return str(getattr(person, attrib_name)) 86 | 87 | return None 88 | 89 | def headerData(self, section, orientation, role): 90 | # This is where you can name your columns, or show 91 | # some other data for the column and row headers 92 | if role == Qt.DisplayRole: 93 | # Just return a row number for the vertical header 94 | if orientation == Qt.Vertical: 95 | return str(section) 96 | 97 | # Return some column names for the horizontal header 98 | if orientation == Qt.Horizontal: 99 | attrib_name, display_val = self.attrib_key[section] 100 | 101 | return display_val 102 | 103 | def setData(self, index, value, role): 104 | row = index.row() 105 | col = index.column() 106 | 107 | if index.isValid(): 108 | if role == Qt.DisplayRole: 109 | person = self.model_data[row] 110 | attrib_name, display_val = self.attrib_key[col] 111 | stripped = value.strip() 112 | 113 | # Age and height are numbers, convert if needed 114 | if col in {PeopleModel.AGE, PeopleModel.HEIGHT_MM}: 115 | if re.match(r'[0-9]+', stripped): 116 | setattr(person, attrib_name, int(stripped)) 117 | 118 | self.dataChanged.emit(index, index, [Qt.DisplayRole]) 119 | return True 120 | else: 121 | # Names are strings, just store them 122 | setattr(person, attrib_name, stripped) 123 | 124 | self.dataChanged.emit(index, index, [Qt.DisplayRole]) 125 | return True 126 | 127 | # The item was not edited, return False 128 | return False 129 | 130 | def flags(self, index): 131 | return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable 132 | 133 | 134 | class PeopleSortFilterModel(QSortFilterProxyModel): 135 | """Lets us sort/filter a PeopleModel""" 136 | 137 | def __init__(self, user_data): 138 | super().__init__() 139 | 140 | self.model_data = user_data 141 | self.setSourceModel(user_data) 142 | self.filter_string = '' 143 | 144 | def filterAcceptsRow(self, source_row, source_parent): 145 | if self.filter_string: 146 | index = self.model_data.index(source_row, PeopleModel.FIRST_NAME) 147 | first_name = self.model_data.data(index, Qt.DisplayRole) 148 | 149 | return True if first_name.lower().startswith(self.filter_string) else False 150 | 151 | # If no filter, all rows are accepted 152 | return True 153 | 154 | def filterAcceptsColumn(self, source_column, source_parent): 155 | return True 156 | 157 | def set_filter_string(self, user_filter): 158 | self.filter_string = user_filter 159 | 160 | # This tells Qt to invalidate the model, which will cause 161 | # connected views to refresh/re-query any displayed data 162 | self.beginResetModel() 163 | self.endResetModel() 164 | 165 | def lessThan(self, source_left, source_right): 166 | # If you want to customize sort behavior, do the comparison logic here 167 | left = self.model_data.data(source_left, Qt.DisplayRole) 168 | right = self.model_data.data(source_right, Qt.DisplayRole) 169 | 170 | return left < right 171 | 172 | 173 | class PeopleFieldEditor(QLineEdit): 174 | """Provides a QLineEdit that only allows digit entry on numeric fields""" 175 | 176 | def __init__(self, parent, column): 177 | super().__init__(parent) 178 | 179 | # Store the column type (age and height are numeric columns) 180 | self.column = column 181 | 182 | def keyPressEvent(self, event): 183 | # Restrict accepted keypresses if this editor is for a numeric column 184 | if self.column in {PeopleModel.AGE, PeopleModel.HEIGHT_MM}: 185 | event.accept() 186 | 187 | accepted_keys = { 188 | # Accept digits 189 | Qt.Key_0, 190 | Qt.Key_1, 191 | Qt.Key_2, 192 | Qt.Key_3, 193 | Qt.Key_4, 194 | Qt.Key_5, 195 | Qt.Key_6, 196 | Qt.Key_7, 197 | Qt.Key_8, 198 | Qt.Key_9, 199 | # Also allow return/enter/tab and delete keys 200 | Qt.Key_Return, 201 | Qt.Key_Enter, 202 | Qt.Key_Tab, 203 | Qt.Key_Delete, 204 | Qt.Key_Backspace, 205 | } 206 | 207 | if event.key() in accepted_keys: 208 | event.ignore() 209 | super().keyPressEvent(event) 210 | else: 211 | # This editor is NOT for a numeric column, 212 | # let the editor do its normal thing 213 | event.ignore() 214 | super().keyPressEvent(event) 215 | 216 | 217 | class PeopleDelegate(QStyledItemDelegate): 218 | """Provides editor widgets for editing the people table""" 219 | 220 | def __init__(self): 221 | super().__init__() 222 | 223 | def createEditor(self, parent, option, index): 224 | # You can create different widgets per column if you want, 225 | # but here we'll just use our PeopleFieldEditor for all cells 226 | row = index.row() 227 | col = index.column() 228 | 229 | return PeopleFieldEditor(parent, col) 230 | 231 | def setEditorData(self, editor, index): 232 | # This populates the contents of the editor based on the index 233 | # (so our line editor will be pre-populated with names, for instance) 234 | editor.setText(index.model().data(index, Qt.DisplayRole)) 235 | 236 | def setModelData(self, editor, model, index): 237 | # This attempts to assign the new value given by the editor 238 | model.setData(index, editor.text(), Qt.DisplayRole) 239 | 240 | def updateEditorGeometry(self, editor, option, index): 241 | """Just call the superclass implementation here""" 242 | # Let Qt size and position the editor widget, you probably 243 | # don't want to manually size and position the widget yourself 244 | super().updateEditorGeometry(editor, option, index) 245 | 246 | 247 | class CustomWidget(QWidget): 248 | """A widget that shows people in a table""" 249 | 250 | def __init__(self): 251 | super().__init__() 252 | 253 | # Set some initial properties 254 | layout = QVBoxLayout() 255 | self.setWindowTitle('Editable table example') 256 | self.setLayout(layout) 257 | 258 | # Make a list of people, then show it in a sortable table 259 | people = [ 260 | Person('Alice', 'Lee', 'Smith', 33, 181), 261 | Person('Aaron', 'Jake', 'Bell', 29, 177), 262 | Person('Bob', 'Greg', 'Candler', 24, 193), 263 | Person('Ben', 'Joseph', 'Wicket', 34, 174), 264 | Person('William', 'Troy', 'Ackford', 49, 207), 265 | Person('Walter', 'Sam', 'Beckett', 57, 202), 266 | Person('Megan', 'Rose', 'Rust', 11, 180), 267 | Person('Finn', 'Jake', 'Beemo', 99, 230), 268 | Person('Mark', 'Charles', 'Ford', 16, 172), 269 | Person('Jeff', 'Glenn', 'Teesdale', 71, 179), 270 | Person('Jessica', 'Lala', 'Earl', 45, 212), 271 | Person('Nancy', 'Elizabeth', 'Lemon', 40, 211), 272 | ] 273 | self.people = people 274 | 275 | # Show a header for the people table area 276 | layout.addWidget(QLabel('People, in a table')) 277 | # ........................................... 278 | # Show a filter field and button for the people table 279 | people_controls = QHBoxLayout() 280 | layout.addLayout(people_controls) 281 | # ............................... 282 | people_filter_field = QLineEdit() 283 | people_filter_field.setPlaceholderText( 284 | 'First-name-starts-with' 285 | ) 286 | people_controls.addWidget(people_filter_field) 287 | self.people_filter_field = people_filter_field 288 | # ............................................ 289 | people_filter_btn = QPushButton('Filter People') 290 | people_filter_btn.clicked.connect(self.handle_apply_people_filter) 291 | people_controls.addWidget(people_filter_btn) 292 | # .......................................... 293 | clear_people_filt_btn = QPushButton('Clear Filter') 294 | clear_people_filt_btn.clicked.connect(self.handle_clear_people_filter) 295 | people_controls.addWidget(clear_people_filt_btn) 296 | 297 | # Make a model for our People 298 | people_model = PeopleModel(people) 299 | self.people_model = people_model 300 | # Make a sort/filter proxy model, it enables us to 301 | # inform Qt about which items from the original model 302 | # are filtered out and how they should be sorted 303 | people_sort_model = PeopleSortFilterModel(people_model) 304 | self.people_sort_model = people_sort_model 305 | 306 | # A table view of our people 307 | people_table = QTableView() 308 | people_table.setModel(people_sort_model) 309 | people_table.setItemDelegate(PeopleDelegate()) 310 | # Set extra table settings 311 | # .................................. 312 | people_table.setSortingEnabled(True) 313 | # Only allow single, full-row selections 314 | people_table.setSelectionBehavior(QAbstractItemView.SelectRows) 315 | people_table.setSelectionMode(QAbstractItemView.SingleSelection) 316 | # Set header behaviors 317 | # .................... 318 | # Make the last column fit the parent layout width 319 | horiz_header = people_table.horizontalHeader() 320 | horiz_header.setStretchLastSection(True) 321 | vert_header = people_table.verticalHeader() 322 | vert_header.setSectionResizeMode(QHeaderView.Fixed) 323 | # .......................... 324 | layout.addWidget(people_table) 325 | self.people_table = people_table 326 | 327 | # Size the widget after adding stuff to the layout 328 | self.resize(900, 600) 329 | self.people_table.resizeColumnsToContents() 330 | # Make sure you show() the widget! 331 | self.show() 332 | 333 | def handle_apply_people_filter(self): 334 | self.people_sort_model.set_filter_string(self.people_filter_field.text().lower()) 335 | 336 | def handle_clear_people_filter(self): 337 | self.people_filter_field.clear() 338 | self.people_sort_model.set_filter_string('') 339 | 340 | 341 | def run_gui(): 342 | """Function scoped main app entrypoint""" 343 | # Initialize the QApplication! 344 | app = QApplication(sys.argv) 345 | 346 | # This widget shows itself (the main GUI entrypoint) 347 | my_widget = CustomWidget() 348 | 349 | # Run the program/start the event loop with exec() 350 | sys.exit(app.exec()) 351 | 352 | 353 | if __name__ == '__main__': 354 | run_gui() 355 | -------------------------------------------------------------------------------- /app_table_word_pairs.py: -------------------------------------------------------------------------------- 1 | """Basic word pairs table demo. 2 | 3 | This demo shows a minimal example of how to display custom data 4 | in a table interface. This involves using a QAbstractTableModel, 5 | which tells Qt how your custom data corresponds to different 6 | rows/columns/cells in the table, and a QTableView, which connects 7 | to your table model to display your data to the user. 8 | """ 9 | 10 | 11 | import sys 12 | 13 | from PySide6.QtCore import Qt, QAbstractTableModel 14 | from PySide6.QtWidgets import (QApplication, QWidget, QVBoxLayout, QTableView, QLabel, 15 | QHeaderView, QHBoxLayout, QPushButton) 16 | 17 | 18 | class WordPairModel(QAbstractTableModel): 19 | """Tells Qt how our word pair data corresponds to different rows/columns/cells. 20 | 21 | From the Qt documentation (for display-only tables): 22 | When subclassing QAbstractTableModel, you must implement rowCount(), 23 | columnCount(), and data(). Default implementations of the index() 24 | and parent() functions are provided by QAbstractTableModel. 25 | Well behaved models will also implement headerData(). 26 | """ 27 | 28 | def __init__(self, user_data): 29 | super().__init__() 30 | 31 | # Store the data we're representing 32 | self.model_data = user_data 33 | 34 | def rowCount(self, parent): 35 | return len(self.model_data) 36 | 37 | def columnCount(self, parent): 38 | # This data (word pairs) always has 2 columns 39 | return 2 40 | 41 | def data(self, index, role): 42 | # So, data() does a lot of different things. This 43 | # function takes in a QModelIndex (which tells you 44 | # which cell/what data Qt needs info about), then 45 | # you respond by returning whatever KIND of information 46 | # Qt is looking for, determined by the role. Here are 47 | # the builtin roles Qt requests by default: 48 | # 49 | # 0) Qt::DisplayRole, 1) Qt::DecorationRole, 50 | # 2) Qt::EditRole 3) Qt::ToolTipRole, 4) Qt::StatusTipRole 51 | # 5) Qt::WhatsThisRole, 6) Qt::SizeHintRole 52 | # 53 | # Most of these you can probably ignore. Often, you 54 | # only need to provide data for the DisplayRole, which 55 | # will often just be some text representing your data... 56 | # but as you can see, for each cell, Qt also might want 57 | # to know how to size the data in that cell, or what 58 | # a good tooltip might be for the cell, etcetera. Make 59 | # SURE you specifically test for the roles that you care 60 | # about, and return None if the role isn't relevant to you. 61 | # Providing bad data/a nonsense return value for a role 62 | # you don't care about can make weird things happen. 63 | row = index.row() 64 | col = index.column() 65 | 66 | # Note that dicts are sorted in Py3.7+, so here 67 | # we just index an ordered list of our dict items 68 | if index.isValid(): 69 | if role == Qt.DisplayRole: 70 | return list(self.model_data.items())[row][col] 71 | 72 | return None 73 | 74 | def headerData(self, section, orientation, role): 75 | # This is where you can name your columns, or show 76 | # some other data for the column and row headers 77 | if role == Qt.DisplayRole: 78 | # Just return a row number for the vertical header 79 | if orientation == Qt.Vertical: 80 | return str(section) 81 | 82 | # Return some column names for the horizontal header 83 | if orientation == Qt.Horizontal: 84 | if section == 0: 85 | return "First Word" 86 | if section == 1: 87 | return "Second Word" 88 | 89 | def set_new_pair_data(self, user_data): 90 | # A custom function that clears the underlying word pair 91 | # data (and stores new data), then refreshes the model 92 | 93 | # Assign new underlying word pairs data 94 | self.model_data = user_data 95 | 96 | # This tells Qt to invalidate the model, which will cause 97 | # connected views to refresh/re-query any displayed data 98 | self.beginResetModel() 99 | self.endResetModel() 100 | 101 | 102 | class CustomWidget(QWidget): 103 | """A widget that shows a simple table of word pairs""" 104 | 105 | def __init__(self): 106 | super().__init__() 107 | 108 | # Internal data that we want to display in a table 109 | self.word_pairs = { 110 | 'fruit': 'banana', 111 | 'vegetable': 'spinach', 112 | 'animal': 'dog', 113 | 'mineral': 'quartz', 114 | 'nature': 'neat', 115 | 'plant': 'tree', 116 | 'yolk': 'yellow', 117 | 'pillows': 'soft', 118 | } 119 | 120 | # Set some initial properties 121 | layout = QVBoxLayout() 122 | self.setWindowTitle('Basic table example') 123 | self.setLayout(layout) 124 | 125 | # Start defining an area that will hold a 126 | # table layout for our word pairs 127 | upper_area = QHBoxLayout() 128 | layout.addLayout(upper_area) 129 | upper_area.addWidget(QLabel('Word pairs, in a table:')) 130 | 131 | # Make a Qt model, this lets us use Qt's 132 | # standard table interface widget 133 | word_model = WordPairModel(self.word_pairs) 134 | self.word_model = word_model 135 | 136 | # A table view of the word_pairs dict 137 | word_table = QTableView() 138 | word_table.setModel(word_model) 139 | # Set header behaviors 140 | # .................... 141 | # Make the last column fit the parent layout width 142 | horiz_header = word_table.horizontalHeader() 143 | horiz_header.setStretchLastSection(True) 144 | vert_header = word_table.verticalHeader() 145 | vert_header.setSectionResizeMode(QHeaderView.Fixed) 146 | # .......................... 147 | layout.addWidget(word_table) 148 | self.word_table = word_table 149 | 150 | # Store an alternate word pair dict, then swap 151 | # back and forth between displaying the original and 152 | # the alternate in our word pair table 153 | alternate_word_pairs = { 154 | 'sun': 'bright', 155 | 'water': 'wet', 156 | 'rocks': 'hard', 157 | 'grass': 'soft', 158 | 'planes': 'flying', 159 | 'toast': 'golden', 160 | } 161 | self.alternate_word_pairs = alternate_word_pairs 162 | # Store which word pair dict is being shown now 163 | self.current_word_pairs = self.word_pairs 164 | 165 | # Make a button for swapping the displayed pairs 166 | swap_words_btn = QPushButton('Show Other Words') 167 | swap_words_btn.clicked.connect(self.handle_swap_words) 168 | upper_area.addStretch() 169 | upper_area.addWidget(swap_words_btn) 170 | 171 | # Size the widget after adding stuff to the layout 172 | self.resize(900, 600) 173 | # Size the table columns after resizing the main widget 174 | self.word_table.resizeColumnsToContents() 175 | # Make sure you show() the widget! 176 | self.show() 177 | 178 | def handle_swap_words(self): 179 | # Make the word_table show different data 180 | if self.current_word_pairs is self.word_pairs: 181 | # We're showing the original word pairs, switch to the alternates 182 | self.current_word_pairs = self.alternate_word_pairs 183 | self.word_model.set_new_pair_data(self.alternate_word_pairs) 184 | else: 185 | # We're showing the alternates, switch back to the original pairs 186 | self.current_word_pairs = self.word_pairs 187 | self.word_model.set_new_pair_data(self.word_pairs) 188 | 189 | # Resize the columns to fit the new contents 190 | self.word_table.resizeColumnsToContents() 191 | 192 | 193 | def run_gui(): 194 | """Function scoped main app entrypoint""" 195 | # Initialize the QApplication! 196 | app = QApplication(sys.argv) 197 | 198 | # This widget shows itself (the main GUI entrypoint) 199 | my_widget = CustomWidget() 200 | 201 | # Run the program/start the event loop with exec() 202 | sys.exit(app.exec()) 203 | 204 | 205 | if __name__ == '__main__': 206 | run_gui() 207 | --------------------------------------------------------------------------------