import random import time from sortedcontainers import SortedDict # Define the database. At this point it comprises a single SortedDict in # which the keys are rwt(8) "word" tokens, and the values are tuples # comprising ( word, int in 0-99, rft() flag string). Yes this is # redundant in having the word as both key and value but it simplifies # coding the data() method of the model. DBM = SortedDict() DBV = None # valuesview DBS = 10000 # how many rows # These functions are used to populate the database with # values that are kinda-sorta like the ones in my app. def xdi(B,N,M): ''' eXponential Distribution of Integers, XDI Return a list of length N of integers with max value M, from an exponential distribution with beta B. ''' M = int(M) # just in case # pre-allocate the list rather than building it with .append() r = [0]*N for j in range(N) : r[j] = int( random.expovariate(B) ) % M return r RWTALPH = 'aeiouåáïbcdfghjklmnpqrstvwxyz_BCDFGHJKLMNPQRSTVWZ' def rwt(N): ''' Random Word-like Token, RWT Return a word-like token comprising N Latin-1 letters with an emphasis on vowels. ''' ii = xdi( 0.1, N, len(RWTALPH)-1 ) return ''.join( [ RWTALPH[i] for i in ii ] ) def rft(): ''' Random Flag Token Return an 8-char token composed mostly of dashes with a sprinkle of Xs, e.g. "---X--X-" or "XX------". ''' ii = xdi( 2, 8, 2 ) return ''.join( ['-X'[i] for i in ii ] ) # Clear the database, called before Refresh def clear_the_db() : global DBM, DBV DBM.clear() DBV = None # Rebuild the database, called on a Refresh. def rebuild_the_db(): global DBM, DBV, DBS ''' [re]populate the fake database: * Create a vector of random numbers which will constitute Column 2 and also pace the creation loop * Clear the SortedDict and load it with DBS rows of fake data * Recreate the keysview and valuesview ''' col2 = xdi( 0.05, DBS, 100 ) assert DBS == len(col2) # once in a blue moon... clear_the_db() for k in col2 : w = rwt(8) f = rft() DBM[ w ] = ( w, k, f ) DBV = DBM.values() # Define the Table Model, instrumented to count certain calls from PyQt5.QtCore import Qt, QAbstractTableModel class Model( QAbstractTableModel ): def __init__ ( self, parent=None ) : super().__init__( parent ) self.access_counts = [0, 0, 0] def rowCount( self, index ) : global DBM if index.isValid() : return 0 return len(DBM) def columnCount( self, index ) : if index.isValid() : return 0 return 3 def data(self, index, role ) : global DBV if role != Qt.DisplayRole : return None row = index.row() col = index.column() self.access_counts[col] += 1 return DBV[ row ][ col ] def clear_counts( self ) : self.access_counts = [0, 0, 0] def counts( self ) : return list( self.access_counts ) # Define the Table View, which at this point is quite minimal. # The View is instantiated from MainWindow, which also connects # the model to it. from PyQt5.QtWidgets import QTableView class View( QTableView ): def __init__ ( self, parent=None ) : super().__init__( parent ) self.setSortingEnabled( True ) self.sortByColumn( 0, Qt.AscendingOrder ) # Define the SortFilterProxy, which at this point is quite minimal. # It too is instantiated by the Mainwindow. from PyQt5.QtCore import QSortFilterProxyModel class Proxy( QSortFilterProxyModel ) : def __init__ ( self, parent=None ) : super().__init__( parent ) self.setSortCaseSensitivity( True ) self.setSortLocaleAware( True ) # Define the main window which is the visual face of this app. from PyQt5.QtWidgets import ( QLabel, QMainWindow, QPushButton, QVBoxLayout, QHBoxLayout, QWidget ) class Main( QMainWindow ) : def __init__ ( self ) : super().__init__( ) self.times = [0, 0, 0] clear_the_db() # make sure to start with 0 rows self._uic() # all the layout stuff out of line self.refresh_button.clicked.connect( self.do_refresh ) # Slot called when Refresh is clicked: # * clear the counts # * start model reset # * rebuild the DBM, timing it # * end the model reset, timing that # * update the labels displaying call counts and times def do_refresh( self ) : self.table_model.clear_counts() self.table_model.beginResetModel() self.times[0] = time.process_time() rebuild_the_db() self.times[1] = time.process_time() self.table_model.endResetModel() QApplication.processEvents() self.times[2] = time.process_time() self.update_labels() def update_labels(self) : [c0, c1, c2] = self.table_model.counts() [t0, t1, t2] = self.times self.c0_label.setText( str(c0) ) self.c1_label.setText( str(c1) ) self.c2_label.setText( str(c2) ) self.td_label.setText( '{:02.5f}'.format(t1-t0) ) self.tr_label.setText( '{:02.5f}'.format(t2-t1) ) def _make_label( self, text='0' ) : # just make a right-aligned label out of line L = QLabel(text) L.setAlignment( Qt.AlignRight | Qt.AlignVCenter ) return L def _uic( self ) : # create the table model, view and proxy self.table_view = View( parent=self ) self.table_model = Model( parent=self ) self.table_proxy = Proxy( parent=self ) self.table_proxy.setSourceModel( self.table_model ) self.table_view.setModel( self.table_proxy ) # create the refresh button, put it in an hbox by itself, for now self.refresh_button = QPushButton( "Refresh" ) hb0 = QHBoxLayout() hb0.addWidget(self.refresh_button, 0) hb0.addStretch(1) # create a set of labels to display counts of # entry to the model.data() method and times self.c0_label = self._make_label() # display role calls to column 0 self.c1_label = self._make_label() # 1 self.c2_label = self._make_label() # 2 self.tr_label = self._make_label() # time to reset the model self.td_label = self._make_label() # time to rebuild the db # build the row of call numbers hb1 = QHBoxLayout() hb1.addStretch(1) # push this row to the right hb1.addWidget( self._make_label( 'Display role calls col 0:' ) ) hb1.addWidget( self.c0_label ) hb1.addStretch(0) hb1.addWidget( self._make_label( 'col 1:' ) ) hb1.addWidget( self.c1_label ) hb1.addStretch(0) hb1.addWidget( self._make_label( 'col 2:' ) ) hb1.addWidget( self.c2_label ) # build the row of times hb2 = QHBoxLayout() hb2.addStretch(1) hb2.addWidget( self._make_label( 'Seconds to build DB:' ) ) hb2.addWidget( self.td_label ) hb2.addStretch(0) hb2.addWidget( self._make_label( 'to reset model:' ) ) hb2.addWidget( self.tr_label ) # stack up the central layout vb = QVBoxLayout() vb.addLayout( hb0, 0 ) vb.addWidget( self.table_view, 1 ) vb.addLayout( hb1, 0 ) vb.addLayout( hb2, 0 ) # put all that in a widget and make the widget our central layout wij = QWidget() wij.setLayout( vb ) wij.setMinimumSize( 500, 500 ) self.setCentralWidget( wij ) if __name__ == '__main__' : from PyQt5.QtWidgets import QApplication the_app = QApplication([]) main_window = Main() main_window.show() the_app.exec_()