├── .gitignore ├── LICENSE ├── README.md ├── qdialog ├── __init__.py └── dialog.py ├── qmainwindow ├── __init__.py └── window.py ├── qmodelproxy ├── __init__.py ├── data.json └── sort_filter_proxy.py ├── qmodelview ├── __init__.py ├── __main__.py ├── common.py ├── data.json ├── data2.json ├── item_widget.py ├── model_view.py └── model_view2.py └── qsettings ├── __init__.py ├── advanced.py └── basic.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # vscode 107 | .vscode 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ryan Porter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyqt_examples 2 | Short examples of uses of the PyQt/PySide application framework. 3 | -------------------------------------------------------------------------------- /qdialog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantor3d/pyqt_examples/a258544b69cc8cbd1638adecf1433fe6b7180101/qdialog/__init__.py -------------------------------------------------------------------------------- /qdialog/dialog.py: -------------------------------------------------------------------------------- 1 | """Dialog example.""" 2 | 3 | import sys 4 | 5 | from PySide2 import QtCore, QtGui, QtWidgets 6 | 7 | 8 | class PromptDialog(QtWidgets.QDialog): 9 | """A simple example dialog.""" 10 | 11 | def __init__(self, title='Prompt', message='Enter text:', parent=None): 12 | """Initialize. 13 | 14 | Args: 15 | parent (PySide2.QtWidgets.QWidget): Parent widget for this dialog. 16 | """ 17 | 18 | super(PromptDialog, self).__init__(parent) 19 | 20 | self.setWindowTitle(title) 21 | 22 | self._text_field = QtWidgets.QLineEdit(self) 23 | self._buttons = QtWidgets.QDialogButtonBox( 24 | QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel, 25 | parent=self 26 | ) 27 | 28 | layout = QtWidgets.QFormLayout(self) 29 | layout.addRow(message, self._text_field) 30 | layout.addRow(self._buttons) 31 | 32 | self._setup() 33 | 34 | def _setup(self): 35 | """Set up the signal/slot connections.""" 36 | 37 | self._buttons.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False) 38 | self._buttons.accepted.connect(self.accept) 39 | self._buttons.rejected.connect(self.reject) 40 | 41 | self._text_field.textChanged.connect(self._handle_text_changed) 42 | 43 | @property 44 | def text(self): 45 | """Return the text the user entered.""" 46 | 47 | return self._text_field.text() 48 | 49 | def _handle_text_changed(self, text): 50 | """Enable the OK button if the user has entered text.""" 51 | 52 | self._buttons.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(bool(text)) 53 | 54 | 55 | def main(): 56 | app = QtWidgets.QApplication([]) 57 | app.setQuitOnLastWindowClosed(True) 58 | 59 | dlg = PromptDialog() 60 | dlg.resize(240, 60) 61 | 62 | if dlg.exec_(): 63 | print("# Accepted - Result: '{}'".format(dlg.text)) 64 | else: 65 | print("# Canceled - No result") 66 | 67 | sys.exit(0) 68 | 69 | 70 | if __name__ == '__main__': 71 | main() -------------------------------------------------------------------------------- /qmainwindow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yantor3d/pyqt_examples/a258544b69cc8cbd1638adecf1433fe6b7180101/qmainwindow/__init__.py -------------------------------------------------------------------------------- /qmainwindow/window.py: -------------------------------------------------------------------------------- 1 | """Window example.""" 2 | 3 | import sys 4 | 5 | from PySide2 import QtCore, QtGui, QtWidgets 6 | 7 | 8 | class MyWidget(QtWidgets.QDialog): 9 | """A simple example widget.""" 10 | 11 | Order = QtCore.Signal(str) 12 | 13 | def __init__(self, parent=None): 14 | """Initialize. 15 | 16 | Args: 17 | parent (PySide2.QtWidgets.QWidget): Parent widget for this dialog. 18 | """ 19 | 20 | super(MyWidget, self).__init__(parent) 21 | 22 | self.option_a = QtWidgets.QCheckBox('Chips and Guac') 23 | self.option_b = QtWidgets.QCheckBox('Chips and Queso') 24 | self.option_c = QtWidgets.QCheckBox('Chips and Salsa') 25 | 26 | self.accept_btn = QtWidgets.QPushButton('Add to Order') 27 | 28 | self.button_group = QtWidgets.QButtonGroup() 29 | 30 | options_box = QtWidgets.QGroupBox('Options') 31 | options_lay = QtWidgets.QVBoxLayout(options_box) 32 | options_lay.addWidget(self.option_a) 33 | options_lay.addWidget(self.option_b) 34 | options_lay.addWidget(self.option_c) 35 | 36 | btn_layout = QtWidgets.QHBoxLayout() 37 | btn_layout.addWidget(self.accept_btn) 38 | 39 | layout = QtWidgets.QVBoxLayout(self) 40 | layout.addWidget(options_box) 41 | layout.addLayout(btn_layout) 42 | 43 | self._setup() 44 | 45 | def _setup(self): 46 | """Set up the signal/slot connections.""" 47 | 48 | self.accept_btn.clicked.connect(self._handle_accept_clicked) 49 | 50 | self.button_group.addButton(self.option_a) 51 | self.button_group.addButton(self.option_b) 52 | self.button_group.addButton(self.option_c) 53 | 54 | self.button_group.setExclusive(True) 55 | self.option_a.setChecked(True) 56 | 57 | def _handle_accept_clicked(self): 58 | """Handle the user clicking 'Accept'.""" 59 | 60 | item = self.button_group.checkedButton().text() 61 | 62 | self.Order.emit(item) 63 | 64 | 65 | def main(): 66 | app = QtWidgets.QApplication([]) 67 | app.setQuitOnLastWindowClosed(True) 68 | 69 | win = QtWidgets.QMainWindow() 70 | win.setWindowTitle('Sides') 71 | win.setCentralWidget(MyWidget()) 72 | win.show() 73 | 74 | def handle_order(item): 75 | print('# You ordered a side of {}'.format(item.lower())) 76 | 77 | win.centralWidget().Order.connect(handle_order) 78 | 79 | sys.exit(app.exec_()) 80 | 81 | 82 | if __name__ == '__main__': 83 | main() -------------------------------------------------------------------------------- /qmodelproxy/__init__.py: -------------------------------------------------------------------------------- 1 | """Sort/Filter Proxy Model example.""" -------------------------------------------------------------------------------- /qmodelproxy/data.json: -------------------------------------------------------------------------------- 1 | [ 2 | "cause", 3 | "spark", 4 | "giraffe", 5 | "attack", 6 | "scissors", 7 | "nose", 8 | "pizzas", 9 | "stone", 10 | "cover", 11 | "orange", 12 | "payment", 13 | "development", 14 | "cars", 15 | "coat", 16 | "language", 17 | "finger", 18 | "hope", 19 | "pleasure", 20 | "marble", 21 | "roll", 22 | "blood", 23 | "tub", 24 | "office", 25 | "string", 26 | "stew", 27 | "bag", 28 | "argument", 29 | "time", 30 | "income", 31 | "request", 32 | "channel", 33 | "squirrel", 34 | "muscle", 35 | "cushion", 36 | "cracker", 37 | "ladybug", 38 | "chin", 39 | "soap", 40 | "root", 41 | "arithmetic", 42 | "desk", 43 | "thread", 44 | "guitar", 45 | "pen", 46 | "whip", 47 | "agreement", 48 | "coast", 49 | "clam", 50 | "weight", 51 | "cannon", 52 | "sister", 53 | "unit", 54 | "snails", 55 | "knot", 56 | "digestion", 57 | "shade", 58 | "tooth", 59 | "butter", 60 | "addition", 61 | "sisters", 62 | "distribution", 63 | "sidewalk", 64 | "flesh", 65 | "slip", 66 | "hand", 67 | "walk", 68 | "question", 69 | "mass", 70 | "geese", 71 | "voice", 72 | "cattle", 73 | "oil", 74 | "border", 75 | "fork", 76 | "loss", 77 | "quartz", 78 | "art", 79 | "poison", 80 | "aftermath", 81 | "cakes", 82 | "relation", 83 | "curve", 84 | "card", 85 | "bone", 86 | "underwear", 87 | "amount", 88 | "destruction", 89 | "store", 90 | "meeting", 91 | "spoon", 92 | "plant", 93 | "jellyfish", 94 | "rake", 95 | "crayon", 96 | "stretch", 97 | "suggestion", 98 | "lettuce", 99 | "passenger", 100 | "shock", 101 | "vacation" 102 | ] -------------------------------------------------------------------------------- /qmodelproxy/sort_filter_proxy.py: -------------------------------------------------------------------------------- 1 | """Sort/Filter Proxy Model example.""" 2 | 3 | import json 4 | import os 5 | import random 6 | import sys 7 | import functools 8 | 9 | from PySide2 import QtCore, QtGui, QtWidgets 10 | 11 | random.seed(42) 12 | 13 | COLORS = { 14 | 'Black': QtGui.QColor(QtCore.Qt.black), 15 | 'Red': QtGui.QColor(QtCore.Qt.red), 16 | 'Dark Red': QtGui.QColor(QtCore.Qt.darkRed), 17 | 'Green': QtGui.QColor(QtCore.Qt.green), 18 | 'Dark Green': QtGui.QColor(QtCore.Qt.darkGreen), 19 | 'Blue': QtGui.QColor(QtCore.Qt.blue), 20 | 'Dark Blue': QtGui.QColor(QtCore.Qt.darkBlue), 21 | 'Cyan': QtGui.QColor(QtCore.Qt.cyan), 22 | 'Dark Cyan': QtGui.QColor(QtCore.Qt.darkCyan), 23 | } 24 | 25 | COLOR_NAMES = list(COLORS.keys()) 26 | 27 | 28 | class SourceModel(QtGui.QStandardItemModel): 29 | Changed = QtCore.Signal() 30 | 31 | def __init__(self): 32 | super(SourceModel, self).__init__() 33 | 34 | data_filepath = os.path.join(os.path.dirname(__file__), 'data.json') 35 | 36 | with open(data_filepath, 'r') as fp: 37 | self.words = json.load(fp) 38 | 39 | def refresh(self): 40 | self.clear() 41 | 42 | for i in range(1000): 43 | self._make_data_item() 44 | 45 | self.Changed.emit() 46 | 47 | def _make_data_item(self): 48 | name = ' '.join(random.choices(self.words, k=3)) 49 | color = random.choice(COLOR_NAMES) 50 | 51 | item = ColorItem(name, color) 52 | 53 | self.appendRow(item) 54 | 55 | 56 | class ProxyModel(QtCore.QSortFilterProxyModel): 57 | def __init__(self): 58 | super(ProxyModel, self).__init__() 59 | 60 | self._filter_value = None 61 | 62 | self.setFilterRole(QtCore.Qt.UserRole + 1) 63 | self.sort(0) 64 | 65 | @property 66 | def sort_role(self): 67 | return self.sortRole() 68 | 69 | @sort_role.setter 70 | def sort_role(self, value): 71 | self.setSortRole(value) 72 | 73 | @property 74 | def filter_string(self): 75 | return self.filterFixedString() 76 | 77 | @filter_string.setter 78 | def filter_string(self, value): 79 | self.setFilterFixedString(value) 80 | self.invalidate() 81 | 82 | @property 83 | def filter_value(self): 84 | return self._filter_value 85 | 86 | @filter_value.setter 87 | def filter_value(self, value): 88 | self._filter_value = value 89 | self.invalidate() 90 | 91 | def refresh(self): 92 | self.sourceModel().refresh() 93 | 94 | # Calling `invalidate` re-runs the filter and ensures a `layoutChanged` 95 | # signal is emitted by the model proxy. 96 | self.invalidate() 97 | 98 | def filterAcceptsRow(self, source_row, source_parent): 99 | # The default behavior of a sort/filter proxy model will filter 100 | # items using the filter string. Additional filters, like one to 101 | # filter by color, need to implemented on top of this behavior. 102 | 103 | if self.filter_value is not None: 104 | source_index = self.sourceModel().index( 105 | source_row, 0, source_parent 106 | ) 107 | item = self.sourceModel().itemFromIndex(source_index) 108 | result = self.filter_value == item.color 109 | else: 110 | result = True 111 | 112 | if result: 113 | result = super(ProxyModel, self).filterAcceptsRow( 114 | source_row, source_parent 115 | ) 116 | 117 | return result 118 | 119 | def item_from_index(self, index): 120 | # A sort/filter proxy model manages its own indices that must be 121 | # mapped to the indices of the source model to access the items 122 | source_index = self.mapToSource(index) 123 | return self.sourceModel().itemFromIndex(source_index) 124 | 125 | 126 | class ItemView(QtWidgets.QListView): 127 | def __init__(self, model, parent=None): 128 | super(ItemView, self).__init__(parent) 129 | 130 | self.setSelectionMode(QtWidgets.QTreeView.SingleSelection) 131 | self.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) 132 | self.setViewMode(QtWidgets.QListView.IconMode) 133 | self.setResizeMode(QtWidgets.QListView.Adjust) 134 | self.setMovement(QtWidgets.QListView.Static) 135 | self.setIconSize(QtCore.QSize(96, 96)) 136 | self.setLayoutMode(QtWidgets.QListView.Batched) 137 | self.setModel(model) 138 | 139 | def selectionChanged(self, old, new): 140 | for index in self.selectedIndexes(): 141 | item = self.model().item_from_index(index) 142 | 143 | print( 144 | '{:12} {}' 145 | .format('[{}]'.format(item.color), item.name) 146 | ) 147 | 148 | 149 | class ColorItem(QtGui.QStandardItem): 150 | """Model item for a color swatch.""" 151 | 152 | # Wrapping QStandardItem provides a pythonic API for accessing the data 153 | # (eg, item.color) instead of having to make other objects aware of the 154 | # data role values. 155 | 156 | NAME_ROLE = QtCore.Qt.UserRole + 1 157 | COLOR_ROLE = QtCore.Qt.UserRole + 2 158 | 159 | def __init__(self, name, color): 160 | display_name = name.replace(' ', '\n') 161 | 162 | super(ColorItem, self).__init__(display_name) 163 | 164 | self.setData(name, self.NAME_ROLE) 165 | self.setData(color, self.COLOR_ROLE) 166 | 167 | color_swatch = COLORS[color] 168 | self.setData(color_swatch, QtCore.Qt.DecorationRole) 169 | 170 | @property 171 | def name(self): 172 | return self.data(self.NAME_ROLE) 173 | 174 | @property 175 | def color(self): 176 | return self.data(self.COLOR_ROLE) 177 | 178 | 179 | class SimpleDataModel(QtGui.QStandardItemModel): 180 | """Simple wrapper around a QStandardItemModel. 181 | 182 | Allows construction of items with data in a fixed role. 183 | """ 184 | 185 | def __init__(self, data_role=QtCore.Qt.UserRole + 1): 186 | super(SimpleDataModel, self).__init__() 187 | self.data_role = data_role 188 | 189 | def _add_item(self, name, data): 190 | item = QtGui.QStandardItem(name) 191 | item.setData(data, self.data_role) 192 | 193 | self.appendRow(item) 194 | 195 | 196 | class Colors(SimpleDataModel): 197 | """List of color options.""" 198 | 199 | def __init__(self): 200 | super(Colors, self).__init__() 201 | 202 | self._add_item('All Colors', None) 203 | 204 | for color in sorted(COLORS): 205 | self._add_item(color, color) 206 | 207 | 208 | class SortModes(SimpleDataModel): 209 | """List of sort options.""" 210 | 211 | def __init__(self): 212 | super(SortModes, self).__init__() 213 | 214 | self._add_item('By Name', ColorItem.NAME_ROLE) 215 | self._add_item('By Color', ColorItem.COLOR_ROLE) 216 | 217 | 218 | class DataComboBox(QtWidgets.QComboBox): 219 | """Simple wrapper around a ComboBox. 220 | 221 | The `Changed` signal emits the data assigned to the selected item. 222 | """ 223 | 224 | Changed = QtCore.Signal(object) 225 | 226 | def __init__(self, model, parent=None, data_role=QtCore.Qt.UserRole + 1): 227 | self.data_role = data_role 228 | 229 | super(DataComboBox, self).__init__(parent) 230 | 231 | self.currentIndexChanged.connect(self._handle_index_changed) 232 | self.setModel(model) 233 | 234 | def _handle_index_changed(self, index): 235 | self.Changed.emit(self.itemData(index, self.data_role)) 236 | 237 | 238 | class MainWidget(QtWidgets.QWidget): 239 | """Widget for viewing a list of items, with filter/sort capabilities.""" 240 | 241 | def __init__(self, model, parent=None): 242 | super(MainWidget, self).__init__(parent) 243 | 244 | self.model = model 245 | 246 | main_layout = QtWidgets.QVBoxLayout(self) 247 | form_layout = QtWidgets.QFormLayout() 248 | 249 | self.filter_edit = QtWidgets.QLineEdit(self) 250 | self.sort_mode = DataComboBox(SortModes(), self) 251 | self.color_mode = DataComboBox(Colors(), self) 252 | self.flow_view = ItemView(model, self) 253 | self.item_count = QtWidgets.QLabel() 254 | 255 | form_layout.addRow('Search', self.filter_edit) 256 | form_layout.addRow('Sort', self.sort_mode) 257 | form_layout.addRow('Show', self.color_mode) 258 | main_layout.addLayout(form_layout) 259 | main_layout.addWidget(self.flow_view) 260 | main_layout.addWidget(self.item_count) 261 | 262 | self._connect_slots() 263 | 264 | def _connect_slots(self): 265 | """Connect signals/slots.""" 266 | 267 | self.model.layoutChanged.connect(self._update_item_count) 268 | 269 | # A partial of `setattr` gives you a callable to assign a value. 270 | # 271 | # f = partial(setattr, obj, 'foo') 272 | # f(5) 273 | # obj.foo 274 | # 5 275 | 276 | self.filter_edit.textChanged.connect( 277 | functools.partial(setattr, self.model, 'filter_string') 278 | ) 279 | 280 | self.sort_mode.Changed.connect( 281 | functools.partial(setattr, self.model, 'sort_role') 282 | ) 283 | 284 | self.color_mode.Changed.connect( 285 | functools.partial(setattr, self.model, 'filter_value') 286 | ) 287 | 288 | def _update_item_count(self): 289 | """Update the item counter.""" 290 | 291 | self.item_count.setText( 292 | 'Showing {:4d} Items' 293 | .format(self.model.rowCount()) 294 | ) 295 | 296 | 297 | class MainWindow(QtWidgets.QMainWindow): 298 | """Tool for viewing a list of items, with filter/sort capabilities.""" 299 | 300 | def __init__(self): 301 | super(MainWindow, self).__init__() 302 | 303 | self.setWindowTitle('Filter/Sort Proxy Model Example') 304 | 305 | self.model = ProxyModel() 306 | self.model.setSourceModel(SourceModel()) 307 | 308 | self.setCentralWidget(MainWidget(self.model)) 309 | 310 | self._opened = False 311 | 312 | def showEvent(self, event): 313 | super(MainWindow, self).showEvent(event) 314 | 315 | if not self._opened: 316 | self._opened = True 317 | 318 | QtCore.QTimer.singleShot(10, self.refresh) 319 | 320 | def refresh(self): 321 | """Refresh the view.""" 322 | 323 | self.model.refresh() 324 | 325 | 326 | def main(): 327 | app = QtWidgets.QApplication([]) 328 | app.setQuitOnLastWindowClosed(True) 329 | 330 | win = MainWindow() 331 | win.resize(540, 400) 332 | win.show() 333 | 334 | sys.exit(app.exec_()) 335 | 336 | 337 | if __name__ == '__main__': 338 | main() -------------------------------------------------------------------------------- /qmodelview/__init__.py: -------------------------------------------------------------------------------- 1 | """Model/View usage examples. 2 | 3 | Model/View programming (https://doc.qt.io/qt-5/model-view-programming.html) is a 4 | method of managing the separation of data persistence from rendering/editing. 5 | 6 | This examples will focus on the QStandardItemModel, a generic model for storing 7 | custom data (https://doc.qt.io/qt-5/qstandarditemmodel.html). For comparison, an 8 | example using an item/widget solution is also presented. The UI/UX of the examples 9 | are identical - a tree of items with status codes, presented with human readable 10 | names. Selecting an item prints the item name and status code. 11 | 12 | In my experience, you can do most of your basic UI/IX work - presenting structured 13 | data, showing icons, managing user selections - with this model and one of the 14 | built-in views. 15 | 16 | Advanced UI/IX work - editing per-item data, custom data rendering, etc - can 17 | be handled with delegates, which is outside of the scope of thes examples. 18 | """ 19 | 20 | __version__ = '1.1.5' -------------------------------------------------------------------------------- /qmodelview/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | 4 | import common 5 | import item_widget 6 | import model_view 7 | import model_view2 8 | 9 | from PySide2 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser() 14 | parser.add_argument( 15 | '--mv', action='store_true', help='View the Model/View example' 16 | ) 17 | parser.add_argument( 18 | '--mv2', action='store_true', help='View the Model/View example' 19 | ) 20 | 21 | args = parser.parse_args() 22 | 23 | if args.mv2: 24 | model_view2.main() 25 | elif args.mv: 26 | model_view.main() 27 | else: 28 | item_widget.main() 29 | 30 | 31 | if __name__ == '__main__': 32 | main() -------------------------------------------------------------------------------- /qmodelview/common.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import json 3 | import os 4 | import sys 5 | 6 | from PySide2 import QtCore, QtGui, QtWidgets 7 | 8 | 9 | STATUS_NAMES = { 10 | 'rdy': 'Ready to Start', 11 | 'omt': 'Omitted', 12 | 'wip': 'In Progress', 13 | 'fin': 'Final' 14 | } 15 | 16 | STATUS_COLORS = { 17 | 'rdy': QtCore.Qt.white, 18 | 'omt': QtCore.Qt.darkGray, 19 | 'wip': QtCore.Qt.yellow, 20 | 'fin': QtCore.Qt.green 21 | } 22 | 23 | ItemStatus = collections.namedtuple('ItemStatus', 'parent child status') 24 | 25 | 26 | def print_item_status(item_status): 27 | print('{parent}:{child} ({status})'.format(**item_status._asdict())) 28 | 29 | 30 | def query_db(filename='data.json'): 31 | with open(os.path.join(os.path.dirname(__file__), filename), 'r') as fp: 32 | return json.load(fp) 33 | 34 | 35 | class StatusWindow(QtWidgets.QMainWindow): 36 | """Window for a tool that displays the status of items.""" 37 | 38 | def __init__(self, widget): 39 | """Initialize.""" 40 | 41 | super(StatusWindow, self).__init__() 42 | 43 | self._status_widget = widget(self) 44 | self.setCentralWidget(self._status_widget) 45 | self.setFixedWidth(320) 46 | self.setFixedHeight(320) 47 | 48 | self._opened = False 49 | 50 | def showEvent(self, event): 51 | # Delay "querying" the DB until after the window has opened 52 | if not self._opened: 53 | self._opened = True 54 | self._handle_window_opened() 55 | 56 | super(StatusWindow, self).showEvent(event) 57 | 58 | def _handle_window_opened(self): 59 | """Handle the window opening for the first time.""" 60 | 61 | self._status_widget.refresh() 62 | 63 | 64 | def main(widget, window_name): 65 | app = QtWidgets.QApplication([]) 66 | app.setQuitOnLastWindowClosed(True) 67 | 68 | win = StatusWindow(widget) 69 | win.setWindowTitle('{} Work Status'.format(window_name)) 70 | win.show() 71 | 72 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /qmodelview/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "A": { 3 | "01": "wip", 4 | "02": "wip", 5 | "03": "fin", 6 | "04": "fin", 7 | "05": "omt" 8 | }, 9 | "B": { 10 | "01": "rdy", 11 | "02": "wip", 12 | "03": "wip" 13 | }, 14 | "C": { 15 | "01": "rdy", 16 | "02": "rdy", 17 | "03": "rdy" 18 | } 19 | } -------------------------------------------------------------------------------- /qmodelview/data2.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "B", 4 | "items": [ 5 | { 6 | "name": "01", 7 | "items": [ 8 | {"name": "foo", "status": "wip"}, 9 | {"name": "bar", "status": "wip"}, 10 | {"name": "bat", "status": "wip"}, 11 | {"name": "baz", "status": "wip"}, 12 | {"name": "qux", "status": "wip"} 13 | ] 14 | }, 15 | { 16 | "name": "02", 17 | "items": [ 18 | {"name": "foo", "status": "rdy"}, 19 | {"name": "bar", "status": "wip"}, 20 | {"name": "baz", "status": "wip"}, 21 | {"name": "qux", "status": "wip"} 22 | ] 23 | }, 24 | { 25 | "name": "03", 26 | "items": [ 27 | {"name": "bar", "status": "fin"}, 28 | {"name": "bat", "status": "fin"}, 29 | {"name": "baz", "status": "wip"}, 30 | {"name": "qux", "status": "wip"} 31 | ] 32 | } 33 | ] 34 | }, 35 | { 36 | "name": "A", 37 | "items": [ 38 | { 39 | "name": "01", 40 | "items": [ 41 | {"name": "foo", "status": "wip"}, 42 | {"name": "baz", "status": "wip"}, 43 | {"name": "qux", "status": "wip"} 44 | ] 45 | }, 46 | { 47 | "name": "02", 48 | "items": [] 49 | }, 50 | { 51 | "name": "03", 52 | "items": [ 53 | {"name": "foo", "status": "fin"}, 54 | {"name": "bar", "status": "fin"}, 55 | {"name": "bat", "status": "rdy"}, 56 | {"name": "baz", "status": "wip"}, 57 | {"name": "qux", "status": "rdy"} 58 | ] 59 | }, 60 | { 61 | "name": "04", 62 | "items": [ 63 | {"name": "foo", "status": "fin"}, 64 | {"name": "bar", "status": "wip"}, 65 | {"name": "bat", "status": "omt"}, 66 | {"name": "baz", "status": "wip"}, 67 | {"name": "qux", "status": "omt"} 68 | ] 69 | }, 70 | { 71 | "name": "05", 72 | "items": [ 73 | {"name": "foo", "status": "wip"}, 74 | {"name": "bar", "status": "rdy"}, 75 | {"name": "bat", "status": "rdy"} 76 | ] 77 | } 78 | ] 79 | }, 80 | { 81 | "name": "C", 82 | "items": [ 83 | { 84 | "name": "01", 85 | "items": [ 86 | {"name": "foo", "status": "fin"}, 87 | {"name": "bar", "status": "fin"}, 88 | {"name": "bat", "status": "fin"}, 89 | {"name": "baz", "status": "wip"}, 90 | {"name": "qux", "status": "omt"} 91 | ] 92 | }, 93 | { 94 | "name": "02", 95 | "items": [ 96 | {"name": "foo", "status": "fin"}, 97 | {"name": "bat", "status": "rdy"}, 98 | {"name": "baz", "status": "wip"}, 99 | {"name": "qux", "status": "omt"} 100 | ] 101 | }, 102 | { 103 | "name": "03", 104 | "items": [ 105 | {"name": "baz", "status": "wip"}, 106 | {"name": "qux", "status": "omt"} 107 | ] 108 | } 109 | ] 110 | } 111 | ] -------------------------------------------------------------------------------- /qmodelview/item_widget.py: -------------------------------------------------------------------------------- 1 | """Tree item/widget example. 2 | 3 | This example shows the widget approach to rendering a data in a tree structure. 4 | It uses a QTreeWidget and QTreeWidgetItems. 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | import common 11 | 12 | from PySide2 import QtCore, QtGui, QtWidgets 13 | 14 | 15 | class StatusWidget(QtWidgets.QTreeWidget): 16 | """Widget that displays the status of items.""" 17 | 18 | def __init__(self, parent=None): 19 | """Initialize. 20 | 21 | Args: 22 | parent (QtWidgets.QWidget): Parent widget for this widget. 23 | """ 24 | 25 | super(StatusWidget, self).__init__(parent) 26 | 27 | self.setColumnCount(2) 28 | self.setHeaderLabels(['Name', 'Status']) 29 | self.setSelectionMode(QtWidgets.QTreeWidget.SingleSelection) 30 | self.itemSelectionChanged.connect(self._handle_item_selection_handled) 31 | 32 | def refresh(self): 33 | """Refresh the list of status items.""" 34 | 35 | data = common.query_db() 36 | 37 | self.clear() 38 | 39 | for name, items in sorted(data.items()): 40 | self._create_top_item(name, items, self) 41 | 42 | self.expandAll() 43 | 44 | def _create_top_item(self, name, data, parent): 45 | """Create a top level item for the status list. 46 | 47 | Args: 48 | name (str): Display name of the top level item. 49 | data (list(dict]): Status item data. 50 | parent (QtWidgets.QTreeWidget): Parent for the item. 51 | """ 52 | 53 | top_item = QtWidgets.QTreeWidgetItem(parent) 54 | top_item.setFlags(top_item.flags() & ~QtCore.Qt.ItemIsSelectable) 55 | top_item.setChildIndicatorPolicy( 56 | QtWidgets.QTreeWidgetItem.DontShowIndicatorWhenChildless 57 | ) 58 | top_item.setText(0, name) 59 | 60 | for child, status in sorted(data.items()): 61 | self._create_child_item(child, status, top_item) 62 | 63 | def _create_child_item(self, name, status, parent): 64 | """Create a child item for the status list. 65 | 66 | Args: 67 | name (str): Display name of the child item. 68 | status (str): Status code for the child item. 69 | parent (QtWidgets.QTreeWidgetItem): Parent for the item. 70 | """ 71 | 72 | child_item = QtWidgets.QTreeWidgetItem(parent) 73 | brush = QtGui.QBrush(common.STATUS_COLORS[status]) 74 | child_item.setBackground(0, brush) 75 | child_item.setBackground(1, brush) 76 | child_item.setText(0, name) 77 | child_item.setText(1, common.STATUS_NAMES.get(status)) 78 | child_item.setData(1, QtCore.Qt.UserRole, status) 79 | 80 | def _handle_item_selection_handled(self): 81 | """Handle the user selecting an item in the status list.""" 82 | 83 | item, = self.selectedItems() or [None] 84 | 85 | if item is None: 86 | return 87 | 88 | item_status = self._item_status(item) 89 | common.print_item_status(item_status) 90 | 91 | def _item_status(self, item): 92 | """Return the status for the given item. 93 | 94 | Args: 95 | item (QtWidgets.QTreeWidgetItem): Status item. 96 | 97 | Returns: 98 | ItemStatus 99 | """ 100 | 101 | return common.ItemStatus( 102 | item.parent().text(0), 103 | item.text(0), 104 | item.data(1, QtCore.Qt.UserRole) 105 | ) 106 | 107 | 108 | def main(): 109 | common.main( 110 | widget=StatusWidget, 111 | window_name='Item/Widget' 112 | ) 113 | 114 | 115 | if __name__ == '__main__': 116 | main() 117 | -------------------------------------------------------------------------------- /qmodelview/model_view.py: -------------------------------------------------------------------------------- 1 | """Tree model/view example. 2 | 3 | This example shows the model/view to rendering a data in a tree structure. 4 | It uses a QTreeView and QStandardItemModel 5 | """ 6 | 7 | import os 8 | import sys 9 | 10 | import common 11 | 12 | from PySide2 import QtCore, QtGui, QtWidgets 13 | 14 | 15 | class StatusView(QtWidgets.QTreeView): 16 | """View that displays the status of items.""" 17 | 18 | def __init__(self, parent=None): 19 | """Initialize. 20 | 21 | Args: 22 | parent (QtWidgets.QWidget): Parent widget for this widget. 23 | """ 24 | 25 | super(StatusView, self).__init__(parent) 26 | 27 | self.setSelectionMode(QtWidgets.QTreeView.SingleSelection) 28 | self.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) 29 | self.setModel(StatusModel()) 30 | 31 | @property 32 | def selected_index(self): 33 | """Return the index of the selected item. 34 | 35 | Returns: 36 | QtCore.QModelIndex 37 | """ 38 | 39 | return (self.selectedIndexes() or [QtCore.QModelIndex()])[0] 40 | 41 | def refresh(self): 42 | """Refresh the status view.""" 43 | 44 | self.model().refresh() 45 | 46 | self.expandAll() 47 | self.setItemsExpandable(False) 48 | 49 | for row in self.model().rows: 50 | self.setFirstColumnSpanned(row, QtCore.QModelIndex(), True) 51 | 52 | def selectionChanged(self, selected, deselected): 53 | """Handle the user selecting an item in the status view.""" 54 | 55 | try: 56 | item_status = self.model()[self.selected_index] 57 | except IndexError: 58 | pass 59 | else: 60 | common.print_item_status(item_status) 61 | 62 | 63 | class StatusModel(QtGui.QStandardItemModel): 64 | """Provides access to the status of items.""" 65 | 66 | @property 67 | def rows(self): 68 | """Return the row indices for this model. 69 | 70 | Returns: 71 | iterable[int] 72 | """ 73 | 74 | return range(self.rowCount()) 75 | 76 | def __getitem__(self, index): 77 | """Return the status for the item at the given index. 78 | 79 | Args: 80 | index (QtCore.QModelIndex): Index of an item. 81 | 82 | Returns: 83 | ItemStatus 84 | 85 | Raises: 86 | IndexError: If the index is invalid. 87 | """ 88 | 89 | if not index.isValid(): 90 | raise IndexError() 91 | 92 | if not index.parent().isValid(): 93 | raise IndexError() 94 | 95 | return common.ItemStatus( 96 | self.itemFromIndex(index.siblingAtColumn(0)).text(), 97 | self.itemFromIndex(index.parent()).text(), 98 | self.itemFromIndex(index.siblingAtColumn(1)).data(), 99 | ) 100 | 101 | def flags(self, index): 102 | """Return the flags for the item at the given index. 103 | 104 | Args: 105 | index (QtCore.QModelIndex): Index of an item. 106 | 107 | Returns: 108 | QtCore.Qt.ItemFlags 109 | """ 110 | 111 | result = super(StatusModel, self).flags(index) 112 | 113 | is_top_level_item = index.isValid() and not index.parent().isValid() 114 | 115 | if is_top_level_item: 116 | result &= ~QtCore.Qt.ItemIsSelectable 117 | 118 | return result 119 | 120 | def refresh(self): 121 | """Refresh the list of status items in this model.""" 122 | 123 | data = common.query_db() 124 | 125 | self.clear() 126 | self.setHorizontalHeaderLabels(['Name', 'Status']) 127 | 128 | for name, items in sorted(data.items()): 129 | self._create_top_item(name, items, self) 130 | 131 | def _create_top_item(self, name, data, parent): 132 | """Create a top level item for the status list. 133 | 134 | Args: 135 | name (str): Display name of the top level item. 136 | data (list(dict]): Status item data. 137 | parent (QtGui.QStandardItemModel): Parent for the item. 138 | """ 139 | 140 | top_item = QtGui.QStandardItem(name) 141 | 142 | for child, status in sorted(data.items()): 143 | self._create_child_items(child, status, top_item) 144 | 145 | parent.appendRow(top_item) 146 | 147 | def _create_child_items(self, name, status, parent): 148 | """Create a child item for the status list. 149 | 150 | Args: 151 | name (str): Display name of the child item. 152 | status (str): Status code for the child item. 153 | parent (QtGui.QStandardItem): Parent for the item. 154 | """ 155 | 156 | child_item = QtGui.QStandardItem(name) 157 | status_item = QtGui.QStandardItem(common.STATUS_NAMES.get(status)) 158 | status_item.setData(status) 159 | 160 | brush = QtGui.QBrush(common.STATUS_COLORS[status]) 161 | child_item.setBackground(brush) 162 | status_item.setBackground(brush) 163 | 164 | parent.appendRow([child_item, status_item]) 165 | 166 | 167 | def main(): 168 | common.main( 169 | widget=StatusView, 170 | window_name='Model/View' 171 | ) 172 | 173 | 174 | if __name__ == '__main__': 175 | main() 176 | -------------------------------------------------------------------------------- /qmodelview/model_view2.py: -------------------------------------------------------------------------------- 1 | """Advanced model/view example. 2 | 3 | This example shows the model/view to rendering nested data in a tree structure, 4 | shared across multiple widgets. 5 | """ 6 | 7 | import os 8 | import operator 9 | import sys 10 | 11 | import common 12 | 13 | from PySide2 import QtCore, QtGui, QtWidgets 14 | 15 | 16 | class StatusView(QtWidgets.QTreeView): 17 | """Widget that displays the status of items.""" 18 | 19 | def __init__(self, model, parent=None): 20 | """Initialize. 21 | 22 | Args: 23 | model (QtGui.QStandardItemModel): Model for the item/status data. 24 | parent (QtWidgets.QWidget): Parent widget for this widget. 25 | """ 26 | 27 | super(StatusView, self).__init__(parent) 28 | 29 | self.setSelectionMode(QtWidgets.QTreeView.NoSelection) 30 | self.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) 31 | self.setModel(model) 32 | 33 | 34 | class RootLeafWidget(QtWidgets.QWidget): 35 | """Widget to select root/leaf items.""" 36 | 37 | IndexChanged = QtCore.Signal(QtCore.QModelIndex) 38 | 39 | def __init__(self, model, parent=None): 40 | """Initialize. 41 | 42 | Args: 43 | model (QtGui.QStandardItemModel): Model for the root/leaf data. 44 | parent (QtWidgets.QWidget): Parent widget for this widget. 45 | """ 46 | 47 | super(RootLeafWidget, self).__init__(parent) 48 | 49 | form_layout = QtWidgets.QFormLayout(self) 50 | 51 | # A QComboBox behaves like a QListView 52 | # that renders only the selected item. 53 | self.sel_root = QtWidgets.QComboBox(self) 54 | self.sel_root.setModel(model) 55 | self.sel_root.currentIndexChanged.connect(self._handle_root_changed) 56 | 57 | self.sel_leaf = QtWidgets.QComboBox(self) 58 | self.sel_leaf.setModel(model) 59 | self.sel_leaf.currentIndexChanged.connect(self._handle_leaf_changed) 60 | 61 | form_layout.addRow('Root', self.sel_root) 62 | form_layout.addRow('Leaf', self.sel_leaf) 63 | 64 | def _handle_root_changed(self, row): 65 | """Handle the user selecting a root item. 66 | 67 | Args: 68 | row (int): Selected row in the combo box. 69 | """ 70 | 71 | # When the root is selected, we need to point the leaf combo box at 72 | # the index for that root so the correct leaf items are shown. 73 | new_index = self.sel_root.model().index(row, 0) 74 | 75 | self._set_root_index(self.sel_leaf, new_index, restore_selection=True) 76 | 77 | def _handle_leaf_changed(self, row): 78 | """Handle the user selecting a leaf item. 79 | 80 | Args: 81 | row (int): Selected row in the combo box. 82 | """ 83 | 84 | # When a leaf is selected, we need to point the tree view at 85 | # the index for that root so the items are shown. Note that the 86 | # new index is a child of the leaf index, because our data model 87 | # is hierarchical. 88 | new_index = self.sel_leaf.model().index( 89 | row, 0, self.sel_leaf.rootModelIndex() 90 | ) 91 | 92 | self.IndexChanged.emit(new_index) 93 | 94 | @staticmethod 95 | def _set_root_index(combobox, index, restore_selection=True): 96 | """Set the root index of the given combobox. 97 | 98 | Args: 99 | combobox (QtWidgets.QComboBox): Combobox to edit. 100 | index (QtCore.QModelIndex): New root index for the combobox. 101 | restore_selection (bool): If True, attempt to restore the combo box 102 | to the last selected row. If the index is out of bounds, set 103 | the selection to the first row. 104 | """ 105 | 106 | # Setting a new model/root index in a combo box clears the selection. 107 | # We can either reset to the first item, or try to maintain selection. 108 | # In this example, we just re-select the same index, but in production 109 | # code, you would have to some sort of lookup to find the index of the 110 | # "same" item (eg, same name, same data, etc). 111 | row = combobox.currentIndex() 112 | combobox.setRootModelIndex(index) 113 | 114 | if restore_selection: 115 | row = max(0, row * (row < combobox.count())) 116 | combobox.setCurrentIndex(row) 117 | else: 118 | combobox.setCurrentIndex(0) 119 | 120 | 121 | class StatusWidget(QtWidgets.QWidget): 122 | """Widget that displays the status of items.""" 123 | 124 | def __init__(self, parent=None): 125 | """Initialize. 126 | 127 | Args: 128 | parent (QtWidgets.QWidget): Parent widget for this widget. 129 | """ 130 | 131 | super(StatusWidget, self).__init__(parent) 132 | 133 | # Normally, the model should be passed to the view; this is just an 134 | # artifact of how I set up the re-usable components of this example. 135 | # 136 | # Passing the model to the view means you can mock it when your doing 137 | # tests/demos if you don't want to deal with spinning up a test database 138 | # and populating test data. 139 | self.model = StatusModel() 140 | 141 | # Decomposing your view into individual widgets makes your code easier 142 | # to digest in small junks. Let the widget be responsible for its own 143 | # static configuration options. 144 | self.status_view = StatusView(self.model, self) 145 | 146 | # A GroupBox lets you organize and label your widgets. 147 | self.status_box = QtWidgets.QGroupBox('Status') 148 | self.status_lay = QtWidgets.QVBoxLayout(self.status_box) 149 | self.status_lay.addWidget(self.status_view) 150 | 151 | # Decomposing your view into individual widgets also them re-usable. 152 | # How many tools do you have that have your user select a data in a 153 | # parent/child relationship? Hint: do you group shots by sequence? 154 | self.sel_widget = RootLeafWidget(self.model, self) 155 | self.sel_widget.IndexChanged.connect(self.status_view.setRootIndex) 156 | 157 | root_layout = QtWidgets.QVBoxLayout(self) 158 | root_layout.addWidget(self.sel_widget) 159 | root_layout.addWidget(self.status_box) 160 | 161 | def refresh(self): 162 | """Refresh the UI.""" 163 | 164 | self.model.refresh() 165 | 166 | 167 | class StatusModel(QtGui.QStandardItemModel): 168 | """Provides access to the status of items.""" 169 | 170 | def refresh(self): 171 | """Refresh the list of status items in this model.""" 172 | 173 | data = common.query_db('data2.json') 174 | 175 | self.clear() 176 | 177 | # In a Model/View setup, the model is responsible for the header labels. 178 | self.setHorizontalHeaderLabels(['Name', 'Status']) 179 | 180 | # Use the operator module to make callables that behave like operators 181 | # For example, operator.itemgetter('foo')(obj) is the same as obj.foo 182 | for each in sorted(data, key=operator.itemgetter('name')): 183 | self._create_item_a(each, self) 184 | 185 | def _create_item_a(self, data, parent): 186 | """Create a item for the status list. 187 | 188 | Args: 189 | data (dict): Status data. 190 | parent (QtGui.QStandardItemModel): Parent for the model items. 191 | """ 192 | 193 | item = QtGui.QStandardItem(data['name']) 194 | 195 | for each in sorted(data.get('items', []), key=operator.itemgetter('name')): 196 | self._create_item_b(each, item) 197 | 198 | parent.appendRow(item) 199 | 200 | def _create_item_b(self, data, parent): 201 | """Create a named item for the status list. 202 | 203 | Args: 204 | data (dict): Status data. 205 | parent (QtGui.QStandardItem): Parent for the model items. 206 | """ 207 | 208 | item = QtGui.QStandardItem(data['name']) 209 | 210 | for each in data.get('items', []): 211 | self._create_item_c(each, item) 212 | 213 | parent.appendRow(item) 214 | 215 | def _create_item_c(self, data, parent): 216 | """Create a child item for the status list. 217 | 218 | Args: 219 | data (dict): Status data. 220 | parent (QtGui.QStandardItem): Parent for the model items. 221 | """ 222 | 223 | name = data['name'] 224 | status = data['status'] 225 | 226 | name_item = QtGui.QStandardItem(name) 227 | status_item = QtGui.QStandardItem(common.STATUS_NAMES.get(status)) 228 | 229 | # The constructor of QStandardItem accepts the text/display data. 230 | # You can set additional internal data on the item in any "role". 231 | status_item.setData(status) 232 | 233 | brush = QtGui.QBrush(common.STATUS_COLORS[status]) 234 | name_item.setBackground(brush) 235 | status_item.setBackground(brush) 236 | 237 | parent.appendRow([name_item, status_item]) 238 | 239 | 240 | def main(): 241 | common.main( 242 | widget=StatusWidget, 243 | window_name='Model/View+' 244 | ) 245 | 246 | 247 | if __name__ == '__main__': 248 | main() 249 | -------------------------------------------------------------------------------- /qsettings/__init__.py: -------------------------------------------------------------------------------- 1 | """Settings dialog examples. 2 | 3 | Each file in the module contains an example of a settings/preferences dialog 4 | for a tool. The UI/UX of the dialogs is identical. 5 | 6 | The basic dialog uses a straight forward approach with minimal abstraction. 7 | 8 | The advanced dialog uses descriptors and a proxy/editor pattern to implement 9 | smart widgets read/write the values they control, minimizing the boiler plate 10 | calls to .value() and .getValue() on the QSettings object. 11 | """ -------------------------------------------------------------------------------- /qsettings/advanced.py: -------------------------------------------------------------------------------- 1 | """Settings dialog advanced example. 2 | 3 | This example shows the an advanced approach to using a QSettings class for 4 | persisting user preferences and tool settings between sessions. It uses 5 | descriptors and partials to eliminate most of the boilerplate. 6 | """ 7 | 8 | import collections 9 | import sys 10 | 11 | import functools 12 | 13 | from PySide2 import QtCore, QtGui, QtWidgets 14 | 15 | SettingsEditor = collections.namedtuple('SettingsEditor', 'get set') 16 | 17 | 18 | class SettingsModelProxy(object): 19 | """Settings model proxy. 20 | 21 | This model allows edits to be made to the settings without modifying the 22 | values until the changes are comitted. 23 | """ 24 | 25 | def __init__(self, settings): 26 | """Initialize. 27 | 28 | Args: 29 | settings (QSettings): Settings to edit. 30 | """ 31 | 32 | self._settings = settings 33 | self._edits = {} 34 | 35 | def editor(self, attr): 36 | """Return an pair of editor functions (get/set) for the given attribute. 37 | 38 | Args: 39 | attr (str): Name of the attribute to editor. 40 | 41 | Returns: 42 | SettingsEditor 43 | """ 44 | 45 | assert hasattr(self._settings, attr), \ 46 | "The {} has no attribute '{}'".format(self.__class__.__name__, attr) 47 | 48 | return SettingsEditor( 49 | functools.partial(getattr, self._settings, attr), 50 | functools.partial(self._edits.__setitem__, attr) 51 | ) 52 | 53 | def sync(self): 54 | """Commit the edits to the settings model.""" 55 | 56 | for key, value in self._edits.items(): 57 | self.setValue(key, value) 58 | 59 | self._settings.sync() 60 | 61 | def setValue(self, key, value): 62 | """Set the value of the given key. 63 | 64 | Args: 65 | key (str): Key to set the value of. 66 | value (Any): New value to set the key to. 67 | """ 68 | 69 | setattr(self._settings, key, value) 70 | 71 | def value(self, key, defaultValue=None): 72 | """Return the value for the given key. 73 | 74 | Args: 75 | key (str): Key to get the value of. 76 | defaultValue (Any): Default value to return if no value is set. 77 | 78 | Returns: 79 | Any 80 | """ 81 | 82 | getattr(self._settings, key, defaultValue) 83 | 84 | 85 | class Setting(object): 86 | """QSettings value descriptor. 87 | 88 | This descriptor handles the boiler plate to get/set a value on a 89 | QSettings-like object. The bound object must have these methods: 90 | def setValue(self, key, value) 91 | def value(self, key[, defaultValue=None]) 92 | """ 93 | 94 | def __init__(self, key, default=None, get_as_type=None, set_as_type=None): 95 | """Initialize. 96 | 97 | Args: 98 | key (str): Key to access the settings in the QSettings object. 99 | default (Any): Optional default value for the settings. 100 | get_as_type (callable): Optional convert function for get. 101 | set_as_type (callable): Optional convert function for set. 102 | """ 103 | 104 | self.key = key 105 | self.default = default 106 | self.get_as_type = get_as_type 107 | self.set_as_type = set_as_type 108 | 109 | def __get__(self, obj, type=None): 110 | value = obj.value(self.key, self.default) 111 | 112 | if callable(self.get_as_type): 113 | value = self.get_as_type(value) 114 | 115 | return value 116 | 117 | def __set__(self, obj, value): 118 | if callable(self.set_as_type): 119 | value = self.set_as_type(value) 120 | 121 | obj.setValue(self.key, value) 122 | 123 | 124 | class SettingsModel(QtCore.QSettings): 125 | """Settings model. 126 | 127 | Use the Settings descriptor to create attributes to get/set the values. 128 | """ 129 | 130 | color = Setting('color', default='red') 131 | toggle = Setting('toggle', default=False, get_as_type=int, set_as_type=int) 132 | 133 | def __init__(self): 134 | super(SettingsModel, self).__init__( 135 | QtCore.QSettings.IniFormat, 136 | QtCore.QSettings.UserScope, 137 | 'Yantor3D', 138 | 'AdvancedSettingsDialog' 139 | ) 140 | 141 | def editor(self, attr): 142 | """Return an pair of editor functions (get/set) for the given attribute. 143 | 144 | Args: 145 | attr (str): Name of the attribute to editor. 146 | 147 | Returns: 148 | SettingsEditor 149 | """ 150 | 151 | assert hasattr(self._settings, attr), \ 152 | "The {} has no attribute '{}'".format(self.__class__.__name__, attr) 153 | 154 | return SettingsEditor( 155 | functools.partial(getattr, self._settings, attr), 156 | functools.partial(setattr, self._settings, attr) 157 | ) 158 | 159 | 160 | class CheckBox(QtWidgets.QCheckBox): 161 | """Checkbox widget. 162 | 163 | Presents the user with an option to toggle on/off. 164 | """ 165 | 166 | def __init__(self, label, editor, parent=None): 167 | """Initialize. 168 | 169 | Args: 170 | label (str): Label for the check box. 171 | editor (SettingsEditor): Editor for the value being changed. 172 | parent (QWidget): Optional parent for this widget. 173 | """ 174 | 175 | super(CheckBox, self).__init__(label, parent) 176 | 177 | self._editor = editor 178 | 179 | self.setChecked(self._editor.get()) 180 | self.stateChanged.connect(self._handle_change) 181 | 182 | def _handle_change(self, state): 183 | self._editor.set(self.isChecked()) 184 | 185 | 186 | class RadioButtonGroup(QtWidgets.QWidget): 187 | """Radio button group widget. 188 | 189 | Presents the user with the choice of items to choose from. Exactly one item may be chosen at a time. 190 | """ 191 | 192 | def __init__(self, label, editor, model, parent=None): 193 | """Initialize. 194 | 195 | Args: 196 | label (str): Label for the radio buttons. 197 | editor (SettingsEditor): Editor for the value being changed. 198 | model (QStandardItemModel): Items to choose from. The .text() value is displayed; the .data() value is set. 199 | parent (QWidget): Optional parent for this widget. 200 | """ 201 | 202 | super(RadioButtonGroup, self).__init__(parent) 203 | 204 | self._model = model 205 | self._editor = editor 206 | 207 | self.button_group = QtWidgets.QButtonGroup(self) 208 | 209 | box = QtWidgets.QGroupBox(label, self) 210 | lay = QtWidgets.QVBoxLayout(box) 211 | root = QtWidgets.QVBoxLayout(self) 212 | root.addWidget(box) 213 | 214 | value = self._editor.get() 215 | 216 | for index in range(self._model.rowCount()): 217 | item = self._model.item(index) 218 | 219 | button = QtWidgets.QRadioButton(item.text(), self) 220 | button.setChecked(item.data() == value) 221 | self.button_group.addButton(button, index) 222 | 223 | lay.addWidget(button) 224 | 225 | self.button_group.setExclusive(True) 226 | self.button_group.buttonClicked.connect(self._handle_change) 227 | 228 | def _handle_change(self, button): 229 | index = self.button_group.id(button) 230 | item = self._model.item(index) 231 | self._editor.set(item.data()) 232 | 233 | 234 | class SettingsDialog(QtWidgets.QDialog): 235 | """Simple dialog for editing settings.""" 236 | 237 | def __init__(self, settings): 238 | """Initialize. 239 | 240 | Args: 241 | settings (SettingsModel): Settings to edit. 242 | """ 243 | 244 | super(SettingsDialog, self).__init__() 245 | 246 | self._settings = settings 247 | 248 | self.setWindowTitle('Advanced Settings') 249 | 250 | layout = QtWidgets.QVBoxLayout(self) 251 | 252 | select_color = RadioButtonGroup('Color', self._settings.editor('color'), self.colors(), self) 253 | checkbox = CheckBox('Checkbox', self._settings.editor('toggle'), self) 254 | 255 | layout.addWidget(select_color) 256 | layout.addWidget(checkbox) 257 | 258 | buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel, self) 259 | buttons.accepted.connect(self.accept) 260 | buttons.rejected.connect(self.reject) 261 | 262 | layout.addWidget(buttons) 263 | 264 | def colors(self): 265 | model = QtGui.QStandardItemModel() 266 | 267 | for color in ['red', 'blue', 'green']: 268 | item = QtGui.QStandardItem(color.capitalize()) 269 | item.setData(color) 270 | model.appendRow(item) 271 | 272 | return model 273 | 274 | def accept(self): 275 | self._settings.sync() 276 | 277 | super(SettingsDialog, self).accept() 278 | 279 | 280 | def main(): 281 | app = QtWidgets.QApplication([]) 282 | app.setQuitOnLastWindowClosed(True) 283 | 284 | settings = SettingsModel() 285 | settings = SettingsModelProxy(settings) 286 | 287 | dialog = SettingsDialog(settings) 288 | dialog.exec_() 289 | 290 | sys.exit() 291 | 292 | 293 | if __name__ == '__main__': 294 | main() -------------------------------------------------------------------------------- /qsettings/basic.py: -------------------------------------------------------------------------------- 1 | """Settings dialog basic example. 2 | 3 | This example shows the straight forward approach to using a QSettings class for 4 | persisting user preferences and tool settings between sessions. It involves a 5 | great deal more boilerplate code. 6 | """ 7 | 8 | import sys 9 | 10 | from PySide2 import QtCore, QtGui, QtWidgets 11 | 12 | 13 | class SettingsModel(QtCore.QSettings): 14 | """Settings model. 15 | 16 | Use the Settings descriptor to create attributes to get/set the values. 17 | """ 18 | 19 | def __init__(self): 20 | super(SettingsModel, self).__init__( 21 | QtCore.QSettings.IniFormat, 22 | QtCore.QSettings.UserScope, 23 | 'Yantor3D', 24 | 'BasicSettingsDialog' 25 | ) 26 | 27 | @property 28 | def color(self): 29 | return self.value('color', default='red') 30 | 31 | @color.setter 32 | def color(self, value): 33 | self.setValue('color', value) 34 | 35 | @property 36 | def toggle(self): 37 | return int(self.value('toggle', default=0)) 38 | 39 | @toggle.setter 40 | def toggle(self, value): 41 | self.setValue('toggle', int(value)) 42 | 43 | 44 | class RadioButtonGroup(QtWidgets.QWidget): 45 | """Radio button group widget. 46 | 47 | Presents the user with the choice of items to choose from. Exactly one item may be chosen at a time. 48 | """ 49 | 50 | def __init__(self, label, value, model, parent=None): 51 | """Initialize. 52 | 53 | Args: 54 | label (str): Label for the radio button group. 55 | value (Any): The default selection for this group. 56 | model (QStandardItemModel): Items to choose from. The .text() value is displayed; the .data() value is set. 57 | parent (QWidget): Optional parent for this widget. 58 | """ 59 | 60 | super(RadioButtonGroup, self).__init__(parent) 61 | 62 | self._model = model 63 | 64 | self.button_group = QtWidgets.QButtonGroup(self) 65 | 66 | box = QtWidgets.QGroupBox(label, self) 67 | lay = QtWidgets.QVBoxLayout(box) 68 | root = QtWidgets.QVBoxLayout(self) 69 | root.addWidget(box) 70 | 71 | for index in range(self._model.rowCount()): 72 | item = self._model.item(index) 73 | 74 | button = QtWidgets.QRadioButton(item.text(), self) 75 | button.setChecked(item.data() == value) 76 | self.button_group.addButton(button, index) 77 | 78 | lay.addWidget(button) 79 | 80 | self.button_group.setExclusive(True) 81 | 82 | @property 83 | def value(self): 84 | index = self.button_group.checkedId() 85 | item = self._model.item(index) 86 | 87 | return item.data() 88 | 89 | 90 | class SettingsDialog(QtWidgets.QDialog): 91 | """Simple dialog for editing settings.""" 92 | 93 | def __init__(self, settings): 94 | """Initialize. 95 | 96 | Args: 97 | settings (SettingsModel): Settings to edit. 98 | """ 99 | 100 | super(SettingsDialog, self).__init__() 101 | 102 | self._settings = settings 103 | 104 | self.setWindowTitle('Basic Settings') 105 | 106 | layout = QtWidgets.QVBoxLayout(self) 107 | 108 | self.select_color = RadioButtonGroup('Color', self._settings.color, self.colors(), self) 109 | 110 | self.checkbox = QtWidgets.QCheckBox('Checkbox', self) 111 | self.checkbox.setChecked(self._settings.toggle) 112 | 113 | layout.addWidget(self.select_color) 114 | layout.addWidget(self.checkbox) 115 | 116 | buttons = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Save | QtWidgets.QDialogButtonBox.Cancel, self) 117 | buttons.accepted.connect(self.accept) 118 | buttons.rejected.connect(self.reject) 119 | 120 | layout.addWidget(buttons) 121 | 122 | def colors(self): 123 | model = QtGui.QStandardItemModel() 124 | 125 | for color in ['red', 'blue', 'green']: 126 | item = QtGui.QStandardItem(color.capitalize()) 127 | item.setData(color) 128 | model.appendRow(item) 129 | 130 | return model 131 | 132 | def accept(self): 133 | self._settings.color = self.select_color.value 134 | self._settings.toggle = self.checkbox.isChecked() 135 | 136 | self._settings.sync() 137 | 138 | super(SettingsDialog, self).accept() 139 | 140 | 141 | def main(): 142 | app = QtWidgets.QApplication([]) 143 | app.setQuitOnLastWindowClosed(True) 144 | 145 | settings = SettingsModel() 146 | 147 | dialog = SettingsDialog(settings) 148 | dialog.exec_() 149 | 150 | sys.exit() 151 | 152 | 153 | if __name__ == '__main__': 154 | main() --------------------------------------------------------------------------------