Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

How to create a sliding window view over a dataframe in PyQt5 QTableView, QAbstractTableModel?



  • The following code will let me scroll down through a 200 by 4 table fetching 5 new rows each time. How do I modify this such that the table will open at a certain index (say 100) and I can scroll either up or down like a sliding window?

    user @d_stranz mentioned this idea in this thread but didnt show how to implement - https://www.qtcentre.org/threads/62807-PyQt-QTableView-displying-100-000-000-rows

    ```
    One possible solution is to implement a sliding window onto your underlying data that contains say, twice as many rows as can be viewed onscreen. When you scroll up or down, you slide the window along, moving the first and last index rows accordingly. This should also significantly improve your UI response time, since the view s only have to base their calculations on a few hundred rows instead of thousands or millions. This could probably be done with a proxy model. If having an accurate row number on the vertical header is important, you can re-implement your model's headerData() method to return a string containing the actual row number from the underlying data. "

    Here is the code I am working with

    import sys
    from PyQt5 import QtWidgets, QtCore
    import pandas as pd
    import numpy as np
    
    
    class DelayedFetchingTableModel(QtCore.QAbstractTableModel):
        def __init__(self, batch_size=5, max_num_nodes=200):
            QtCore.QAbstractTableModel.__init__(self)
            self.batch_size = batch_size
    
            self.df = pd.DataFrame(np.random.randint(0,200,size=(200, 4)))
            self.nodes = self.df[:5]
            self.max_num_nodes = max(self.batch_size, max_num_nodes)
    
        def rowCount(self, index):
            return len(self.nodes)
    
        def columnCount(self, index):
            return 4
    
        def data(self, index, role):
            if not index.isValid():
                return None
            if role != QtCore.Qt.DisplayRole:
                return None
            if index.row() < 0 or index.row() >= len(self.nodes):
                return None
            else:
                return QtCore.QVariant(str(self.nodes.iloc[index.row(), index.column()]))
    
        def canFetchMore(self, index):
            if index.isValid():
                return False
            return (len(self.nodes) < self.max_num_nodes)
    
        def fetchMore(self, index):
            print('fetch more')
            if index.isValid():
                print('not valid')
                return QtCore.QVariant()
            current_len = len(self.nodes)
            target_len = min(current_len + self.batch_size, self.max_num_nodes)
            self.beginInsertRows(index, current_len, target_len - 1)
            for i in range(current_len, target_len):
                self.nodes = self.nodes.append(self.df.iloc[i])
    
            self.endInsertRows()
    
    class MainForm(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            QtWidgets.QMainWindow.__init__(self, parent)
            self.model = DelayedFetchingTableModel(batch_size=self.batch_size, max_num_nodes=self.max_num_nodes)
    
            self.view = QtWidgets.QTableView()
            self.view.setModel(self.model)
    
            self.layout = QtWidgets.QVBoxLayout()
            self.layout.addWidget(self.view)
    
            self.window = QtWidgets.QWidget()
            self.window.setLayout(self.layout)
            self.setCentralWidget(self.window)
    
    
    def main():
        app = QtWidgets.QApplication(sys.argv)
        form = MainForm()
        form.show()
        app.exec_()
    
    if __name__ == '__main__':
        main()
    

    Cheers



  • I have tried this and can at least filter the table by the specific rows I want. How do I implement fetchMore function to insert rows when I scroll up and append rows when I scroll down. It is not obvious how fetchMore works and the conditions where the function is called. Who is calling this function?

    import sys
    from PyQt5 import QtWidgets, QtCore
    import pandas as pd
    import numpy as np
    
    class TableModel(QtCore.QAbstractTableModel):
        def __init__(self, parent=None, visible_range = [2,5]):
            super().__init__(parent)
            self.df = pd.DataFrame(np.random.randint(0,1000000,size=(200, 4)))
            print(self.df)
            self.scope = self.df[visible_range[0]:visible_range[1] + 1]
            self.scope_range = visible_range
            #print(self.df)
    
        def rowCount(self, parent=QtCore.QModelIndex()):
            return len(self.scope)
    
        def columnCount(self, parent=QtCore.QModelIndex()):
            return 4
            #return len(self.df.columns)
    
        def data(self, index, role=QtCore.Qt.DisplayRole):
    
            if not index.isValid():
                return None
            if role != QtCore.Qt.DisplayRole:
                return None
            if index.row() < 0 or index.row() >= len(self.df):
                return None
            else:
                return QtCore.QVariant(str(self.scope.iloc[index.row(), index.column()]))
            
        def canFetchMore(self, index):
            if index.isValid():
                return False
            return (len(self.scope) < len(self.df))
    
        def fetchMore(self, index):
            print('fetchmore')
            pass
            """
            if scroll up table:
                #prepend 20 rows
                self.scope_range = [max(self.scope_range[0] - 20, 0), self.scope_range[1]] 
                self.scope = self.df[self.scope_range[0]:self.scope_range[1]+1]
            elif scroll down table:
                #append 20 rows
                self.scope_range = [self.scope_range[0], min(self.scope_range[1] + 20, len(self.df))]
                self.scope = self.df[self.scope_range[0]:self.scope_range[1]+1]
            """
    
           
    class MainForm(QtWidgets.QMainWindow):
        def __init__(self, parent=None):
            QtWidgets.QMainWindow.__init__(self, parent)
            # Setup the model
            self.max_num_nodes = 200
            self.batch_size = 5
            self.model = TableModel()
            # Setup the view
            self.view = QtWidgets.QTableView()
            self.view.setModel(self.model)
            # Update the currently selected row in the spinbox
    
            # Collect all this stuff into a vertical layout
            self.layout = QtWidgets.QVBoxLayout()
            self.layout.addWidget(self.view)
    
            self.window = QtWidgets.QWidget()
            self.window.setLayout(self.layout)
            self.setCentralWidget(self.window)
    
    
    def main():
        app = QtWidgets.QApplication(sys.argv)
        form = MainForm()
        form.show()
        app.exec_()
    
    if __name__ == '__main__':
        main()
    


  • Nevermind my nonsense. QTableView does exactly what I want out of the box. It will only update the view for what is currently visible


Log in to reply