one model, two proxies, two views - Argg!
-
I'm trying to apply MVC in Pyside6. The problem for a hobbyist working alone like me, is that Pyside6 is so large it is very hard to get the correct model and philosophy of how it works in mind. The use case is a custom photo manager, with folders only in a treeview, and folders and thumbnails in a list view, sharing a selection model.
My code is producing just the top level in both views; it is not starting at the desired subfolder; the folder view also contains filenames, which is shouldn't, and the listview only contains folders, when it should contain filenames.and expanding the treeview to show files when it shouldn't:
Here's a simplified example which runs and shows the problem.
""" Pycasa is intended to replicate the functionality of Google's Picasa image management utility. Google abandoned Picasa in 2016. Pycasa is entirely in Python3, using PySide6 for the user interface, and Sqlite3 for storage of data. CODE CONVERTER: Many examples for Qt are in C++. Use https://www.codeconvert.ai/c++-to-python-converter to convert. DESIGN OF THE MAIN WINDOW Picasa's key feature for me was the display of the first line of caption underneath the thumbnail. Then, upon displaying the photo, editing the caption, copying the caption, and easily moving to the next photo and pasting the caption. The essence of the design is to construct a widget containing the thumbnail and the caption, then displaying that widget in a list. There are two choices of list, either a list view or a list widget. This file uses the list view. Constructing the thumbnails pane is hard. Based on my research, there appear to be these ways to approach this problem. First, use a QListWidget instead of a QListView. Create the desired display widget, namely CaptionedThumbnail Create QListItems Bind the thumbnail to the list item with item.setItemWidget() Or, Use a QListView instead of a QListWidget Create a QStandardItemModel Create the desired display widget, namely CaptionedThumbnail Create a QStandardItem to hold the widget Add the item to the model Bind the widget to the model with setIndexWidget() Or, use a QStyledItemDelegate Subclass QStyledItemDelegate Define a paint function paint(self, painter, option, index) Use paint.drawControl(element, option, ...) I have read on the web that this should be used to edit data, so I may be forced to this more complex way. """ import sys from pathlib import Path from PySide6.QtCore import (QItemSelection, QSortFilterProxyModel, QItemSelectionModel) from PySide6.QtCore import (QSize, Qt) from PySide6.QtWidgets import (QAbstractItemView, QAbstractScrollArea, QApplication, QFrame, QHBoxLayout, QLayout, QListView, QMainWindow, QSizePolicy, QTreeView, QVBoxLayout, QWidget) from PySide6.QtWidgets import (QFileSystemModel, QFileIconProvider) VALID_EXTENSIONS = (".jpg", ".jpeg", '.tif', '.tiff', '.png', ".raw", ".rw2", '.bmp', '.webp', '.heic') class CustomSortFilterProxyModel(QSortFilterProxyModel): """Filter for folders of any name, and files in VALID_EXTENSIONS, or just for folders""" def __init__(self, parent=None, filter_option: str = 'dir'): super().__init__(parent) self.filter_option = filter_option def filterAcceptsRow(self, row, parent): # The doc says to return False to filter out. Returning True accepts the row. model = self.sourceModel() # The underlying model index = model.index(row, 0) filepath = model.filePath(index) if self.filter_option.lower() == 'dir': if not Path(filepath).is_dir(): return False elif self.filter_option.lower() == 'files': if Path(filepath).is_dir() or Path(filepath).suffix in VALID_EXTENSIONS: return True else: return False else: return True class MainWindow(QMainWindow): def __init__(self, root_path: str | Path = None, test_run=False): super().__init__() self.root_path = root_path # ****** Code generated by Designer, but copied here for the bug demo self.centralwidget = QWidget(self) self.centralwidget.setObjectName(u"centralwidget") self.horizontalLayout = QHBoxLayout(self.centralwidget) self.horizontalLayout.setObjectName(u"horizontalLayout") self.fldrView = QTreeView(self.centralwidget) self.fldrView.setObjectName(u"fldrView") sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.fldrView.sizePolicy().hasHeightForWidth()) self.fldrView.setSizePolicy(sizePolicy) self.fldrView.setMaximumSize(QSize(300, 16777215)) self.fldrView.setLayoutDirection(Qt.LayoutDirection.LeftToRight) self.fldrView.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents) self.fldrView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.fldrView.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems) self.fldrView.setSortingEnabled(True) self.fldrView.setAnimated(True) self.fldrView.setWordWrap(True) self.horizontalLayout.addWidget(self.fldrView) self.verticalLayout_3 = QVBoxLayout() self.verticalLayout_3.setSpacing(0) self.verticalLayout_3.setObjectName(u"verticalLayout_3") self.verticalLayout_3.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize) self.thumbnails = QListView(self.centralwidget) self.thumbnails.setObjectName(u"thumbnails") sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) sizePolicy1.setHorizontalStretch(0) sizePolicy1.setVerticalStretch(0) sizePolicy1.setHeightForWidth(self.thumbnails.sizePolicy().hasHeightForWidth()) self.thumbnails.setSizePolicy(sizePolicy1) self.thumbnails.setAutoFillBackground(True) self.thumbnails.setFrameShape(QFrame.Shape.StyledPanel) self.thumbnails.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContentsOnFirstShow) self.thumbnails.setTabKeyNavigation(True) self.thumbnails.setDragEnabled(True) self.thumbnails.setAlternatingRowColors(True) self.thumbnails.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection) self.thumbnails.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectColumns) self.thumbnails.setMovement(QListView.Movement.Snap) self.thumbnails.setFlow(QListView.Flow.LeftToRight) self.thumbnails.setResizeMode(QListView.ResizeMode.Adjust) self.thumbnails.setSpacing(12) self.thumbnails.setViewMode(QListView.ViewMode.IconMode) self.thumbnails.setUniformItemSizes(True) self.thumbnails.setBatchSize(98) self.thumbnails.setWordWrap(True) self.thumbnails.setSelectionRectVisible(True) self.thumbnails.setItemAlignment(Qt.AlignmentFlag.AlignLeading) self.verticalLayout_3.addWidget(self.thumbnails) self.horizontalLayout.addLayout(self.verticalLayout_3) self.setCentralWidget(self.centralwidget) # ************ end of code copied from Designer self.resize(1076, 708) # In the spirit of the MVC pattern, set up one model for both views. # Use QProxyModel to provide different filtering # For the thumbnails view, customize to display a widget constructed from the model's filename # But first, just get a list of the filespecs displayed model = QFileSystemModel() self.model = model model.directoryLoaded.connect(self.on_directoryLoaded) icon_provider = QFileIconProvider() model.setIconProvider(icon_provider) model.setReadOnly(False) # True is the default # Create the proxy models, one for each view, but each sharing the same model self.folder_proxy = CustomSortFilterProxyModel(filter_option='dirs') self.folder_proxy.setSourceModel(model) self.thumbs_proxy = CustomSortFilterProxyModel(filter_option='files') self.thumbs_proxy.setSourceModel(model) # Set up the file directory pane. model.setRootPath(root_path) # ToDo: set the selection for the fldrView from the saved configuration # Set up the list view. The view is created in Designer, but was copied into this bug demo # Attach the model to the view. This must be done before setting root index and sorting. self.fldrView.setModel(self.folder_proxy) self.fldrView.setRootIndex(self.model.index(self.root_path)) self.fldrView.sortByColumn(0, Qt.AscendingOrder) self.fldrView.setIconSize(QSize(0, 0)) # Get rid of the folder icon self.fldrView.setExpanded(self.fldrView.rootIndex(), True) # Hide some columns, all but the first for i in range(1, self.model.columnCount()): self.fldrView.hideColumn(i) # Set up the thumbnails view # TODO: Set the style for the listview to have border = none. # Share the selection model with the folder view (https://doc.qt.io/qt-5/model-view-programming.html) self.thumbnails.setSelectionModel(self.fldrView.selectionModel()) self.thumbnails.setModel(self.thumbs_proxy) self.thumbnails.setRootIndex(self.model.index(self.root_path)) self.thumbnails.setIconSize(QSize(0, 0)) # Get rid of the folder icon self.thumbnails.show() self.show() # define the connections of signals to slots # Below should be unnecessary because of sharing of the selection model between the views # self.fldrView.selectionModel().selectionChanged.connect(self.fldrView_current_changed) def on_directoryLoaded(self): print('on_directoryLoaded fired') self.fldrView.setModel(self.folder_proxy) self.thumbnails.setModel(self.thumbs_proxy) self.thumbnails.show() def fldrView_current_changed(self, selected: QItemSelection, deselected: QItemSelection): # Obsolete - sharing the selection model should make this unnecessary # Synchronize selection in the thumbnails view selection_index = self.fldrView.selectionModel().selectedIndexes()[0] self.thumbnails.selectionModel().select(selection_index, QItemSelectionModel.Select) def main(): # Configure User Interface global APP APP = QApplication(sys.argv) APP.setStyle('default') global START_PATH START_PATH = "C:/Photographs/Misc" w = MainWindow(START_PATH) w.show() APP.exec() if __name__ == '__main__': main()
-
I'm trying to apply MVC in Pyside6. The problem for a hobbyist working alone like me, is that Pyside6 is so large it is very hard to get the correct model and philosophy of how it works in mind. The use case is a custom photo manager, with folders only in a treeview, and folders and thumbnails in a list view, sharing a selection model.
My code is producing just the top level in both views; it is not starting at the desired subfolder; the folder view also contains filenames, which is shouldn't, and the listview only contains folders, when it should contain filenames.and expanding the treeview to show files when it shouldn't:
Here's a simplified example which runs and shows the problem.
""" Pycasa is intended to replicate the functionality of Google's Picasa image management utility. Google abandoned Picasa in 2016. Pycasa is entirely in Python3, using PySide6 for the user interface, and Sqlite3 for storage of data. CODE CONVERTER: Many examples for Qt are in C++. Use https://www.codeconvert.ai/c++-to-python-converter to convert. DESIGN OF THE MAIN WINDOW Picasa's key feature for me was the display of the first line of caption underneath the thumbnail. Then, upon displaying the photo, editing the caption, copying the caption, and easily moving to the next photo and pasting the caption. The essence of the design is to construct a widget containing the thumbnail and the caption, then displaying that widget in a list. There are two choices of list, either a list view or a list widget. This file uses the list view. Constructing the thumbnails pane is hard. Based on my research, there appear to be these ways to approach this problem. First, use a QListWidget instead of a QListView. Create the desired display widget, namely CaptionedThumbnail Create QListItems Bind the thumbnail to the list item with item.setItemWidget() Or, Use a QListView instead of a QListWidget Create a QStandardItemModel Create the desired display widget, namely CaptionedThumbnail Create a QStandardItem to hold the widget Add the item to the model Bind the widget to the model with setIndexWidget() Or, use a QStyledItemDelegate Subclass QStyledItemDelegate Define a paint function paint(self, painter, option, index) Use paint.drawControl(element, option, ...) I have read on the web that this should be used to edit data, so I may be forced to this more complex way. """ import sys from pathlib import Path from PySide6.QtCore import (QItemSelection, QSortFilterProxyModel, QItemSelectionModel) from PySide6.QtCore import (QSize, Qt) from PySide6.QtWidgets import (QAbstractItemView, QAbstractScrollArea, QApplication, QFrame, QHBoxLayout, QLayout, QListView, QMainWindow, QSizePolicy, QTreeView, QVBoxLayout, QWidget) from PySide6.QtWidgets import (QFileSystemModel, QFileIconProvider) VALID_EXTENSIONS = (".jpg", ".jpeg", '.tif', '.tiff', '.png', ".raw", ".rw2", '.bmp', '.webp', '.heic') class CustomSortFilterProxyModel(QSortFilterProxyModel): """Filter for folders of any name, and files in VALID_EXTENSIONS, or just for folders""" def __init__(self, parent=None, filter_option: str = 'dir'): super().__init__(parent) self.filter_option = filter_option def filterAcceptsRow(self, row, parent): # The doc says to return False to filter out. Returning True accepts the row. model = self.sourceModel() # The underlying model index = model.index(row, 0) filepath = model.filePath(index) if self.filter_option.lower() == 'dir': if not Path(filepath).is_dir(): return False elif self.filter_option.lower() == 'files': if Path(filepath).is_dir() or Path(filepath).suffix in VALID_EXTENSIONS: return True else: return False else: return True class MainWindow(QMainWindow): def __init__(self, root_path: str | Path = None, test_run=False): super().__init__() self.root_path = root_path # ****** Code generated by Designer, but copied here for the bug demo self.centralwidget = QWidget(self) self.centralwidget.setObjectName(u"centralwidget") self.horizontalLayout = QHBoxLayout(self.centralwidget) self.horizontalLayout.setObjectName(u"horizontalLayout") self.fldrView = QTreeView(self.centralwidget) self.fldrView.setObjectName(u"fldrView") sizePolicy = QSizePolicy(QSizePolicy.Policy.MinimumExpanding, QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.fldrView.sizePolicy().hasHeightForWidth()) self.fldrView.setSizePolicy(sizePolicy) self.fldrView.setMaximumSize(QSize(300, 16777215)) self.fldrView.setLayoutDirection(Qt.LayoutDirection.LeftToRight) self.fldrView.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents) self.fldrView.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) self.fldrView.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems) self.fldrView.setSortingEnabled(True) self.fldrView.setAnimated(True) self.fldrView.setWordWrap(True) self.horizontalLayout.addWidget(self.fldrView) self.verticalLayout_3 = QVBoxLayout() self.verticalLayout_3.setSpacing(0) self.verticalLayout_3.setObjectName(u"verticalLayout_3") self.verticalLayout_3.setSizeConstraint(QLayout.SizeConstraint.SetFixedSize) self.thumbnails = QListView(self.centralwidget) self.thumbnails.setObjectName(u"thumbnails") sizePolicy1 = QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) sizePolicy1.setHorizontalStretch(0) sizePolicy1.setVerticalStretch(0) sizePolicy1.setHeightForWidth(self.thumbnails.sizePolicy().hasHeightForWidth()) self.thumbnails.setSizePolicy(sizePolicy1) self.thumbnails.setAutoFillBackground(True) self.thumbnails.setFrameShape(QFrame.Shape.StyledPanel) self.thumbnails.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContentsOnFirstShow) self.thumbnails.setTabKeyNavigation(True) self.thumbnails.setDragEnabled(True) self.thumbnails.setAlternatingRowColors(True) self.thumbnails.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection) self.thumbnails.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectColumns) self.thumbnails.setMovement(QListView.Movement.Snap) self.thumbnails.setFlow(QListView.Flow.LeftToRight) self.thumbnails.setResizeMode(QListView.ResizeMode.Adjust) self.thumbnails.setSpacing(12) self.thumbnails.setViewMode(QListView.ViewMode.IconMode) self.thumbnails.setUniformItemSizes(True) self.thumbnails.setBatchSize(98) self.thumbnails.setWordWrap(True) self.thumbnails.setSelectionRectVisible(True) self.thumbnails.setItemAlignment(Qt.AlignmentFlag.AlignLeading) self.verticalLayout_3.addWidget(self.thumbnails) self.horizontalLayout.addLayout(self.verticalLayout_3) self.setCentralWidget(self.centralwidget) # ************ end of code copied from Designer self.resize(1076, 708) # In the spirit of the MVC pattern, set up one model for both views. # Use QProxyModel to provide different filtering # For the thumbnails view, customize to display a widget constructed from the model's filename # But first, just get a list of the filespecs displayed model = QFileSystemModel() self.model = model model.directoryLoaded.connect(self.on_directoryLoaded) icon_provider = QFileIconProvider() model.setIconProvider(icon_provider) model.setReadOnly(False) # True is the default # Create the proxy models, one for each view, but each sharing the same model self.folder_proxy = CustomSortFilterProxyModel(filter_option='dirs') self.folder_proxy.setSourceModel(model) self.thumbs_proxy = CustomSortFilterProxyModel(filter_option='files') self.thumbs_proxy.setSourceModel(model) # Set up the file directory pane. model.setRootPath(root_path) # ToDo: set the selection for the fldrView from the saved configuration # Set up the list view. The view is created in Designer, but was copied into this bug demo # Attach the model to the view. This must be done before setting root index and sorting. self.fldrView.setModel(self.folder_proxy) self.fldrView.setRootIndex(self.model.index(self.root_path)) self.fldrView.sortByColumn(0, Qt.AscendingOrder) self.fldrView.setIconSize(QSize(0, 0)) # Get rid of the folder icon self.fldrView.setExpanded(self.fldrView.rootIndex(), True) # Hide some columns, all but the first for i in range(1, self.model.columnCount()): self.fldrView.hideColumn(i) # Set up the thumbnails view # TODO: Set the style for the listview to have border = none. # Share the selection model with the folder view (https://doc.qt.io/qt-5/model-view-programming.html) self.thumbnails.setSelectionModel(self.fldrView.selectionModel()) self.thumbnails.setModel(self.thumbs_proxy) self.thumbnails.setRootIndex(self.model.index(self.root_path)) self.thumbnails.setIconSize(QSize(0, 0)) # Get rid of the folder icon self.thumbnails.show() self.show() # define the connections of signals to slots # Below should be unnecessary because of sharing of the selection model between the views # self.fldrView.selectionModel().selectionChanged.connect(self.fldrView_current_changed) def on_directoryLoaded(self): print('on_directoryLoaded fired') self.fldrView.setModel(self.folder_proxy) self.thumbnails.setModel(self.thumbs_proxy) self.thumbnails.show() def fldrView_current_changed(self, selected: QItemSelection, deselected: QItemSelection): # Obsolete - sharing the selection model should make this unnecessary # Synchronize selection in the thumbnails view selection_index = self.fldrView.selectionModel().selectedIndexes()[0] self.thumbnails.selectionModel().select(selection_index, QItemSelectionModel.Select) def main(): # Configure User Interface global APP APP = QApplication(sys.argv) APP.setStyle('default') global START_PATH START_PATH = "C:/Photographs/Misc" w = MainWindow(START_PATH) w.show() APP.exec() if __name__ == '__main__': main()
@Raoul said in one model, two proxies, two views - Argg!:
I'm trying to apply MVC in Pyside6. The problem for a hobbyist working alone like me, is that Pyside6 is so large it is very hard to get the correct model and philosophy of how it works in mind.
https://doc.qt.io/qtforpython-6/examples/index.html is a good place to start.
Here's a simplified example which runs and shows the problem.
That's too much code to work as a simple example, at least for me. Comments and unrelated UI setup code get in the way of understanding the core issue.
This is a demonstration of a tree view and list view into the same filesystem model. It uses PyQt5, but translating to PySide 6 should be trivial.
from PyQt5 import QtCore, QtWidgets class FilterModel(QtCore.QSortFilterProxyModel): def filterAcceptsRow(self, source_row: int, source_parent: QtCore.QModelIndex): index = self.sourceModel().index(source_row, 0 , source_parent) if self.sourceModel().isDir(index): return True return False app = QtWidgets.QApplication([]) model = QtWidgets.QFileSystemModel() model.setRootPath(app.applicationDirPath()) filterModel = FilterModel() filterModel.setSourceModel(model) treeView = QtWidgets.QTreeView() listView = QtWidgets.QListView() treeView.setModel(filterModel) listView.setModel(model) treeView.clicked.connect(lambda index: listView.setRootIndex(filterModel.mapToSource(index))) splitter = QtWidgets.QSplitter() splitter.addWidget(treeView) splitter.addWidget(listView) splitter.show() app.exec()
-