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().__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

Create GUI Applications with Python & Qt5 by Martin Fitzpatrick — (PySide2 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!

More info Get the book

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:

Create GUI Applications with Python & Qt6 by Martin Fitzpatrick — (PySide6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!

More info Get the book

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:

1:1 Coaching & Tutoring for your Python GUIs project
Martin Fitzpatrick Python GUIs Coaching & Training
60 mins ($195) Book Now

1:1 Python GUIs Coaching & Training

Comprehensive code reviewBugfixes & improvements • Maintainability advice and architecture improvements • Design and usability assessment • Suggestions and tips to expand your knowledgePackaging and distribution help for Windows, Mac & Linux • Find out more.

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

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

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. Martin founded PythonGUIs to provide easy to follow GUI programming tutorials to the Python community. He has written a number of popular Python books on the subject.