├── 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 |
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 | 
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 | 
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------