Image viewer using Qt Model View architecture

Heads up! You've already completed this tutorial.

fjp | 2020-12-07 09:14:44 UTC | #1

Hi, I followed this https://www.pythonguis.com/tutorials/modelview-architecture/ tutorial, and I am trying to display an image depending on the currently selected image path from the QTableView.

I think a possible approach is to create a custom QAbstractItemView to display the currently selected image.

The ImageModel and main window class (in image_app.py) look like this

python
import sys
from PySide2 import QtCore, QtGui, QtWidgets
from PySide2.QtWidgets import QFileDialog
from PySide2.QtCore import Qt

from MainWindow import Ui_MainWindow

class ImageModel(QtCore.QAbstractListModel):
    def __init__(self, images=None):
        super().__init__()
        self.images = images or []

    def data(self, index, role):
        if role == Qt.DisplayRole:
            _, text = self.images[index.row()]
            return text

    def rowCount(self, index):
        return len(self.images)


class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self):
        super().__init__()

        self.setupUi(self)
        self.model = ImageModel()
        self.tableView.setModel(self.model)
        self.imageView.setModel(self.model)
        self.tableView.selectionModel().selectionChanged.connect(self.imageView.selectionChanged)

        self.actionImport.triggered.connect(self.onImportImageClicked)


    def onImportImageClicked(self, s):
        self.open_file()

    def open_file(self):
        filename, _ = QFileDialog.getOpenFileName(
            self,
            "Open file",
            "",
            "Ok Image (*.png *.jpg *.bmp *.jpeg);;" "All files(*.*)",
        )
        if filename:
            self.add(filename)

    def add(self, image_filename):
        # Access the list via the model.
        self.model.images.append((False, image_filename))
        # Trigger refresh.
        self.model.layoutChanged.emit()


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = MainWindow()
    window.show()
    app.exec_()

And my custom QAbstractItemView should display the image using a QLabel (defined in image_view.py):

python
class ImageView(QtWidgets.QLabel, QtWidgets.QAbstractItemView):
    def __init__(self, parent) -> None:
        print(parent)
        QtWidgets.QLabel.__init__(self, parent)
        QtWidgets.QAbstractItemView.__init__(self, parent)
        #super(ImageView, self).__init__()
        #self.label = QtWidgets.QLabel()

    def selectionChanged(self, selected, deselected):
        print("selectionChanged", type(selected))
        indexes = selected.indexes()
        print(indexes)
        if not indexes:
            return

        image_filename = self.model().data(indexes[0], Qt.DisplayRole)
        print(image_filename)

        pixmap = QtGui.QPixmap(image_filename).scaled(128, 128, QtCore.Qt.KeepAspectRatio)

        self.setPixmap(pixmap)

The GUI is designed with Qt Designer and looks like this

enter image description here

I have promoted a QWidget in the Input tab to the ImageView class in the image_view.py

It can display images when one is selected from the QTableListView, but after I run the MainWindow I get this console output:

python
<PySide2.QtWidgets.QWidget(0x7f8cd002cad0, name="tabInput") at 0x7f8ceaebc980>
QObject::connect: No such slot ImageView::_q_modelDestroyed()
QObject::connect: No such slot ImageView::dataChanged(QModelIndex,QModelIndex,QVector<int>)
QObject::connect: No such slot ImageView::_q_headerDataChanged()
QObject::connect: No such slot ImageView::rowsInserted(QModelIndex,int,int)
QObject::connect: No such slot ImageView::_q_rowsInserted(QModelIndex,int,int)
QObject::connect: No such slot ImageView::rowsAboutToBeRemoved(QModelIndex,int,int)
QObject::connect: No such slot ImageView::_q_rowsRemoved(QModelIndex,int,int)
QObject::connect: No such slot ImageView::_q_rowsMoved(QModelIndex,int,int,QModelIndex,int)
QObject::connect: No such slot ImageView::_q_columnsAboutToBeRemoved(QModelIndex,int,int)
QObject::connect: No such slot ImageView::_q_columnsRemoved(QModelIndex,int,int)
QObject::connect: No such slot ImageView::_q_columnsInserted(QModelIndex,int,int)
QObject::connect: No such slot ImageView::_q_columnsMoved(QModelIndex,int,int,QModelIndex,int)
QObject::connect: No such slot ImageView::reset()
QObject::connect: No such slot ImageView::_q_layoutChanged()
QObject::connect: No such slot ImageView::selectionChanged(QItemSelection,QItemSelection)
QObject::connect: No such slot ImageView::currentChanged(QModelIndex,QModelIndex)
selectionChanged <class 'PySide2.QtCore.QItemSelection'>
[]
selectionChanged <class 'PySide2.QtCore.QItemSelection'>
[<PySide2.QtCore.QModelIndex(0,0,0x0,TodoModel(0x1b10c90)) at 0x7f8ceaec24c0>]
/home/fjp/Pictures/motor.png

It seems also that the QLabel is displayed above the pixmap and when I click it, the app crashes:

python
[1]    150858 segmentation fault (core dumped)  python image_app.py

How to avoid these warnings and crashing the app? Whats the correct way to display an image with the currently selected file name in the QTableView? Should I avoid the QAbstractItemView and just use a QLabel to update its pixmap when the selection changes?

Edit

Here is the pyside2 generated gui MainWindow.py, used in image_app.py to better reproduce this issue:

python
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *

from image_view import ImageView


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        if not MainWindow.objectName():
            MainWindow.setObjectName(u"MainWindow")
        MainWindow.resize(800, 600)
        self.actionImport = QAction(MainWindow)
        self.actionImport.setObjectName(u"actionImport")
        self.centralwidget = QWidget(MainWindow)
        self.centralwidget.setObjectName(u"centralwidget")
        self.verticalLayout = QVBoxLayout(self.centralwidget)
        self.verticalLayout.setObjectName(u"verticalLayout")
        self.splitter = QSplitter(self.centralwidget)
        self.splitter.setObjectName(u"splitter")
        self.splitter.setOrientation(Qt.Vertical)
        self.tabWidget = QTabWidget(self.splitter)
        self.tabWidget.setObjectName(u"tabWidget")
        self.tabInput = QWidget()
        self.tabInput.setObjectName(u"tabInput")
        self.verticalLayout_2 = QVBoxLayout(self.tabInput)
        self.verticalLayout_2.setObjectName(u"verticalLayout_2")
        self.imageView = ImageView(self.tabInput)
        self.imageView.setObjectName(u"imageView")

        self.verticalLayout_2.addWidget(self.imageView)

        self.tabWidget.addTab(self.tabInput, "")
        self.splitter.addWidget(self.tabWidget)
        self.tableView = QTableView(self.splitter)
        self.tableView.setObjectName(u"tableView")
        self.splitter.addWidget(self.tableView)

        self.verticalLayout.addWidget(self.splitter)

        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QMenuBar(MainWindow)
        self.menubar.setObjectName(u"menubar")
        self.menubar.setGeometry(QRect(0, 0, 800, 22))
        self.menuFile = QMenu(self.menubar)
        self.menuFile.setObjectName(u"menuFile")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QStatusBar(MainWindow)
        self.statusbar.setObjectName(u"statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.menubar.addAction(self.menuFile.menuAction())
        self.menuFile.addAction(self.actionImport)
        self.retranslateUi(MainWindow)
        self.tabWidget.setCurrentIndex(0)
        QMetaObject.connectSlotsByName(MainWindow)
    # setupUi

    def retranslateUi(self, MainWindow):
        MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None))
        self.actionImport.setText(QCoreApplication.translate("MainWindow", u"Import", None))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.tabInput), QCoreApplication.translate("MainWindow", u"Input", None))
        self.menuFile.setTitle(QCoreApplication.translate("MainWindow", u"File", None))
    # retranslateUi

PyQt/PySide 1:1 Coaching with Martin Fitzpatrick — Get one on one help with your Python GUI projects. Working together with you I'll identify issues and suggest fixes, from bugs and usability to architecture and maintainability.

More info 60 mins ($195)

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Image viewer using Qt Model View architecture was written by Martin Fitzpatrick .

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt.