Search code examples
pythonpyqtpyqt5qstackedwidget

Updating QStackedWidget content at Runtime


I'm creating a GUI in PyQT5 that should provide multiple views on data from multiple related SQLite tables. I have implemented these views to be displayed via a QStackedWidget.

Now, some of these views are for overview purposes, others for more detailled views on a subset of the data displayed in the overviews. I want to access the detailled views from the overviews via rightclick.

I have included a minimal example with cars below. (Sorry, it's a bit long, but that was needed to provide a full working example. I have reduced it as much as I could.) The goal is to show the DetailledView with the cars of the company selected in the Overview.

This already provides access from the Overview to the DetailledView on Rightclick, but the company information is not passed along. So even when accessing the DetailledView from 'VW', self.mycompany gets updated but car_widget doesn't, so the DetailledView shows the info about 'Honda' cars (the default).

Is there a way to update car_widgit with the right company? Or do I need to create the DetailledView at runtime? (Does that even work? But I would prefer not to, anyway, as it could make the index of the Stack unreliable...)

How can I update the QTableModel of one view in a QStackedWidget according to what was selected in another view?

Here's the code:

#!/usr/bin/python3

from PyQt5 import QtSql
from PyQt5.QtWidgets import (QMainWindow, QWidget, QApplication, QGridLayout, 
                             QStackedWidget, QTableView, QMenu)
from PyQt5.QtCore import Qt, QModelIndex
import sys

class MainGUI(QMainWindow):
    def __init__(self):
        super().__init__()
        self.mycompany = "Honda"
        self.create_connection()
        self.fill_tables()

        self.init_UI()

    def create_connection(self):
        self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
        self.db.setDatabaseName("test.db")
        if not self.db.open():
            print("Cannot establish a database connection to {}!".format(self.db_file))
            return False

    def fill_tables(self):
        self.db.transaction()
        q = QtSql.QSqlQuery()
        q.exec_("DROP TABLE IF EXISTS Manufacturers;")
        q.exec_("CREATE TABLE Manufacturers (CompanyId INT PRIMARY KEY, Name TEXT, Country TEXT);")
        q.exec_("INSERT INTO Manufacturers VALUES (1, 'VW', 'Germany');")
        q.exec_("INSERT INTO Manufacturers VALUES (2, 'Honda' , 'Japan');")

        q.exec_("DROP TABLE IF EXISTS Cars;")
        q.exec_("CREATE TABLE Cars (Model TEXT, Year INT, Company INT);")
        q.exec_("INSERT INTO Cars VALUES ('Civic', 2009, 'Honda');")
        q.exec_("INSERT INTO Cars VALUES ('Golf', 2013, 'VW');")
        q.exec_("INSERT INTO Cars VALUES ('Polo', 1999, 'VW');")
        self.db.commit()

    def init_UI(self):
        self.central_widget = QWidget()    
        self.setCentralWidget(self.central_widget)
        self.grid = QGridLayout()
        self.central_widget.setLayout(self.grid)
        self.setLayout(self.grid)
        self.make_stack()
        self.show()

    def make_stack(self):
        self.Stack = QStackedWidget(self)

        company_view = QWidget()
        layout = QGridLayout()
        company_view.setLayout(layout)
        self.company_widget = Overview()
        layout.addWidget(self.company_widget, 0, 0)
        self.company_widget.table.customContextMenuRequested.connect(self.open_menu)
        self.Stack.addWidget(company_view)

        car_view = QWidget()
        layout2 = QGridLayout()
        car_view.setLayout(layout2)
        car_widget = DetailedView(self.mycompany)
        layout2.addWidget(car_widget, 0, 0)
        self.Stack.addWidget(car_view)

        self.grid.addWidget(self.Stack, 0,1)

    def open_menu(self, pos):
        menu = QMenu()
        show_act = menu.addAction("Show cars")
        action = menu.exec_(self.company_widget.table.mapToGlobal(pos))
        if action == show_act:
            row = self.company_widget.table.indexAt(pos).row()
            myindex = self.company_widget.model.index(row, 1, QModelIndex())
            company = self.company_widget.model.data(myindex)
            self.mycompany = company
            self.Stack.setCurrentIndex(1)



class MyTable(QWidget):
    def __init__(self):
        super().__init__()
        self.create_connection()
        self.create_model()
        self.init_UI()

    def create_connection(self):
        self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
        self.db.setDatabaseName("test.db")
        if not self.db.open():
            print("Cannot establish a database connection to {}!".format(self.db_file))
            return False

    def create_model(self):
        self.model = None

    def init_UI(self):
        self.grid = QGridLayout()
        self.setLayout(self.grid)
        self.table = QTableView()
        self.table.setModel(self.model)
        self.table.setContextMenuPolicy(Qt.CustomContextMenu)

        self.grid.addWidget(self.table, 0, 0)

    def closeEvent(self, e):
        if (self.db.open()):
            self.db.close()

    def check_error(self, q):
        lasterr = q.lastError()
        if lasterr.isValid():
            print(lasterr.text())
            self.db.close()
            exit(1)


class Overview(MyTable):
    def __init__(self):
        super().__init__()

    def create_model(self):
        self.model = QtSql.QSqlTableModel()
        q = QtSql.QSqlQuery()
        query = "SELECT * from Manufacturers"
        q.exec_(query)
        self.model.setQuery(q)


class DetailedView(MyTable):
    def __init__(self, company):
        self.company = company
        super().__init__()

    def create_model(self):
        self.model = QtSql.QSqlTableModel()
        q = QtSql.QSqlQuery()
        query = "SELECT * from cars where company = '{}'".format(self.company)
        q.exec_(query)
        self.model.setQuery(q)



def main():
    app = QApplication(sys.argv)
    ex = MainGUI()
    ex.show()

    result = app.exec_()
    sys.exit(result)


if __name__ == '__main__':
    main()      

Solution

  • One of the objectives of the inheritance is that the class implements the common tasks that the children can do, and in your case you do not observe those tasks, for example the creation of the model must be done in the father since all the children will do it.

    On the other hand the goal of using QSqlTableModel is not to use the QSqlQuery but more friendly requests like setTable(), select() and setFilter(), but it is simply a waste because you could use QSqlQueryModel.

    On the other hand I see that you are assuming that the self.mycompany of MainGui is the same as the self.company of DetailedView for what you are going through in the creation of the DetailView object, and the truth is that they are not the same, in the creation of the object only the value of that moment has been copied, so if you change the self.company of MainGui it will not change the self.company of DetailView.

    Restructuring your project you get the following:

    #!/usr/bin/python3
    
    from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel
    from PyQt5.QtWidgets import (QMainWindow, QWidget, QApplication, QVBoxLayout, 
                                 QStackedWidget, QTableView, QMenu)
    from PyQt5.QtCore import Qt, QModelIndex, pyqtSignal
    import sys
    
    DB_PATH = "test.db"
    
    def create_connection():
        db = QSqlDatabase.addDatabase("QSQLITE")
        db.setDatabaseName(DB_PATH)
        if not db.open():
            print("Cannot establish a database connection to {}!".format(DB_PATH))
            return False
        return True
    
    def fill_tables():
        q = QSqlQuery()
        q.exec_("DROP TABLE IF EXISTS Manufacturers;")
        q.exec_("CREATE TABLE Manufacturers (CompanyId INT PRIMARY KEY, Name TEXT, Country TEXT);")
        q.exec_("INSERT INTO Manufacturers VALUES (1, 'VW', 'Germany');")
        q.exec_("INSERT INTO Manufacturers VALUES (2, 'Honda' , 'Japan');")
    
        q.exec_("DROP TABLE IF EXISTS Cars;")
        q.exec_("CREATE TABLE Cars (Model TEXT, Year INT, Company INT);")
        q.exec_("INSERT INTO Cars VALUES ('Civic', 2009, 'Honda');")
        q.exec_("INSERT INTO Cars VALUES ('Golf', 2013, 'VW');")
        q.exec_("INSERT INTO Cars VALUES ('Polo', 1999, 'VW');")
    
    class MainGUI(QMainWindow):
        def __init__(self):
            super().__init__()
            self.init_UI()
    
        def init_UI(self):
            self.central_widget = QWidget()    
            self.setCentralWidget(self.central_widget)
            self.lay = QVBoxLayout(self.central_widget)
            self.make_stack()
    
        def make_stack(self):
            self.stack = QStackedWidget()
            self.lay.addWidget(self.stack)
    
            self.company_widget = Overview()
            self.car_widget = DetailedView()
            self.stack.addWidget(self.company_widget)
            self.stack.addWidget(self.car_widget)
            self.company_widget.changedCompany.connect(self.changedCompany)
            self.car_widget.backSignal.connect(lambda: self.stack.setCurrentIndex(0))
    
        def changedCompany(self, company):
            self.car_widget.filter(company)
            self.stack.setCurrentIndex(1)
    
    class SQLTable(QTableView):
        def __init__(self, table):
            super().__init__()
            self.init_UI()
            self.create_model(table)
    
        def create_model(self, table):
            self.model.setTable(table)
            self.model.select()
    
        def init_UI(self):
            self.model = QSqlTableModel()
            self.setModel(self.model)
            self.setContextMenuPolicy(Qt.CustomContextMenu)
    
    class Overview(SQLTable):
        changedCompany = pyqtSignal(str)
        def __init__(self):
            SQLTable.__init__(self, "Manufacturers")
            self.customContextMenuRequested.connect(self.open_menu)
    
        def open_menu(self, pos):
            menu = QMenu()
            show_act = menu.addAction("Show cars")
            action = menu.exec_(self.mapToGlobal(pos))
            if action == show_act:
                row = self.indexAt(pos).row()
                ix = myindex = self.model.index(row, 1)
                company = self.model.data(ix)
                self.changedCompany.emit(company)
    
    class DetailedView(SQLTable):
        backSignal = pyqtSignal()
        def __init__(self):
            SQLTable.__init__(self, "cars")
            self.customContextMenuRequested.connect(self.open_menu)
    
        def open_menu(self, pos):
            menu = QMenu()
            back_act = menu.addAction("Show Manufacturers")
            action = menu.exec_(self.mapToGlobal(pos))
            if action == back_act:
                self.backSignal.emit()
    
        def filter(self, company):
            self.model.setFilter("company='{}'".format(company))
    
    
    def main():
        app = QApplication(sys.argv)
        if not create_connection():
            sys.exit(-1)
        fill_tables()
        ex = MainGUI()
        ex.show()
    
        result = app.exec_()
        sys.exit(result)
    
    if __name__ == '__main__':
        main()