pyqt5: Properly update QTreeView after data change

2019-08-17 05:15发布

问题:

I have a model-based QTreeView which is fed by a QSortFilterProxyModel.

I am displaying rows from the database here. With a double-click I am executing a modal QDialog to edit the data (the user can do CRUD operations like create new rows into my SQLite data base, updates, delete etc.).

On closing the edit-dialog the focus gets back to the underlying QTreeView. Now I would like to refresh the changed data from the database. One way to do this would be to build up my complete model and refetch all rows, but then the user-selected cell disappears and it is very slow and also the TreeView flickers (because I do some column adjustings concerning the width.)

Emiting dataChanged signal on the QSortFilterProxyModel somehow seems to do ... nothing.

Can I somehow tell my model to refresh or update the data, without fetching alls rows again?

full working code example:

import sys
from PyQt5 import QtWidgets, QtCore, QtGui
import sqlite3
import re

def _human_key(key):
    key = key.lower()
    key = key.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
    parts = re.split(r'(\d*\.\d+|\d+)', key, re.IGNORECASE)
    return parts

class HumanProxyModel(QtCore.QSortFilterProxyModel):
    def lessThan(self, source_left, source_right):
        data_left = source_left.data()
        data_right = source_right.data()
        if type(data_left) == type(data_right) == str:
            return _human_key(data_left) < _human_key(data_right)            
        return super(HumanProxyModel, self).lessThan(source_left, source_right)

class winMain(QtWidgets.QMainWindow):
    COL_ID, COL_NAME = range(2)    

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi()        

        # create and assign model
        self.model = self.createModel(self)

        proxy = HumanProxyModel(self)
        proxy.setSourceModel(self.model)
        self.treeView.setModel(proxy)

        # create db with some data                
        self.conn = self.setupDb()

        # format and sort the treeView
        self.treeView.setRootIsDecorated(False)
        self.treeView.setSortingEnabled(True)
        self.treeView.sortByColumn(self.COL_ID, QtCore.Qt.AscendingOrder)
        self.treeView.setAlternatingRowColors(True)
        self.treeView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.treeView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)

        # display the main window
        self.show()

    def setupUi(self):
        self.setWindowTitle("PyQt5 QTreeView refresh problem demo")

        # add a data fetch button
        pbFetch = QtWidgets.QPushButton(self)
        pbFetch.setText("Fetch data")
        pbFetch.move(350, 20)
        pbFetch.clicked.connect(self.on_pbFetch_clicked)

        # add a reset button
        pbReset = QtWidgets.QPushButton(self)
        pbReset.setText("Reset")
        pbReset.move(450, 20)
        pbReset.clicked.connect(self.on_pbReset_clicked)

        # add QTreeView-Control
        self.treeView = QtWidgets.QTreeView(self)
        self.treeView.move(50, 100)
        self.treeView.resize(700, 300)
        self.treeView.doubleClicked.connect(self.on_treeView_doubleClicked)        

        self.resize(800,450)  # resize main window                       

    def on_pbFetch_clicked(self):        
        self.fetchData()

    def on_pbReset_clicked(self):
        print("on_pbReset_clicked() called.")
        self.reset()

    def setupDb(self):
        # create db and table
        con = sqlite3.connect(":memory:")
        con.execute("create table person(id int, name text)")                        
        # insert some example data
        con.execute("insert into person(id, name) values (1, 'anders')");
        con.execute("insert into person(id, name) values (2, 'Arachno')");
        con.execute("insert into person(id, name) values (3, 'Zabel')");
        con.execute("insert into person(id, name) values (4, 'Ötztürk')");
        con.execute("insert into person(id, name) values (5, 'de Hahn')");
        # commit the transaction
        con.commit()
        return con

    def reset(self):
        print("reset called.")
        self.model.beginResetModel()
        self.model.endResetModel()

    def fetchData(self):
        self.reset()

        cur = self.conn.cursor()
        sSql = "SELECT id, name FROM person"        
        rows = cur.execute(sSql)

        for row in rows:
            self.addPerson(self.model, row[0], row[1])

    def refresh_treeView(self):
        print("refresh_treeView() was called.")
        self.model.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())

    def createModel(self, parent):
        model = QtGui.QStandardItemModel(0, 2, parent)
        model.setHeaderData(self.COL_ID, QtCore.Qt.Horizontal, "ID")        
        model.setHeaderData(self.COL_NAME, QtCore.Qt.Horizontal, "Name")        
        return model

    def addPerson(self, model, personID, personName):
        model.insertRow(0)
        model.setData(model.index(0, self.COL_ID), personID)
        model.setData(model.index(0, self.COL_NAME), personName)

    def on_treeView_doubleClicked(self,index):
       row = index.row()
       ix = self.treeView.model().index(row, self.COL_ID)
       person_id = ix.data()
       print(f"id of double_clicked person: {person_id}")

       # create CRUD dialog for selected person
       self.disp_dlg_edit(person_id)       

    def disp_dlg_edit(self, person):        
       dlgEdit = QtWidgets.QDialog(self)

       lblId = QtWidgets.QLabel(dlgEdit)
       lblId.setText("ID:")       
       efId = QtWidgets.QLineEdit(dlgEdit)
       efId.setReadOnly(True)
       efId.setEnabled(False)
       efId.move(50, 0)

       lblName = QtWidgets.QLabel(dlgEdit)       
       lblName.setText("Name:")
       lblName.move(0, 25)
       efName = QtWidgets.QLineEdit(dlgEdit)       
       efName.move(50, 25)

       pbUpdate = QtWidgets.QPushButton(dlgEdit)
       pbUpdate.setText("Update")
       pbUpdate.move(200,0)
       pbUpdate.resize(100,50)              

       # fetch the data of given person
       cur = self.conn.cursor()
       sSql = "SELECT id, name FROM person WHERE id = ?"       
       cur.execute(sSql, (person,))
       result = cur.fetchone()
       person_id = result[0]
       efId.setText(str(person_id))
       person_name = result[1]
       efName.setText(person_name)

       pbUpdate.clicked.connect(lambda: self.update_person(person_id, efName.text()))

       dlgEdit.resize(300, 50)
       dlgEdit.exec_()

       self.refresh_treeView()

    def update_person(self, person_id, person_name):
        print(f"update_person({person_id}, {person_name}) called.")
        cur = self.conn.cursor()
        sSql = "UPDATE person set name=? WHERE ID=?"        
        cur.execute(sSql, (person_name,person_id,))
        self.conn.commit()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    window = winMain()
    sys.exit(app.exec_())