Search code examples
pyqt5qtableview

issue with update shown values in a pyqt5-applet (using qtableview)


I want to make a qtableview widget correctly updating. I'm working on a calibration applet, where i wanna fill cell by cell of an (e. g.) 100 x 100 x 4 array.

If my hardware reaches position 1, 2, 3, and so on, I will trigger a voltage measurement and gather those values with an i2c-read out-function.

So issues a la "my qtableview is not updating" are omnipresent.

But so far, I'm not able to adapt examples I have read, to make my code behaving as I want.

So if you look at my screenshot:

indPosApplet.png

the problem is:

  • when I'm clicking on row or col +/-, the yellow highlighting is not changing instantly
  • when I'm clicking on store i²c, which is meant to put a dummy 0.0 in/on selected cell, this is also not changing instantly

Several methods like telling the model that data has changed, I was not able to implement correctly so far.

Could some of you help me to add a few lines just to force applet to update correctly?

fillCSV_forum.py:

### libraries:
import sys # to use e. g. exit function
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtCore import Qt
import pandas as pd # to use pandas tables
import numpy as np # to use numpy arrays

### user-defined header files / modules:
from uLib_coloredWidget import Color    # import user-defined functions..                        
from rndGen import i2c_read             # .. see folder


### initial settings:
 # general
np.random.seed(4) # if using np.random, then pseudo random values will occure


### globals:
nRow = 5; nCol = 5; nSht = 4 # table dimensions
rowIdx = colIdx = shtIdx = 0 # aux vars to index array
rndArray = np.random.rand(nSht, nRow, nCol) * 4.3 # auxilliary before integrating i2c
tabNames = ["A4", "A5","A6","A7"] # array (list) with tab names
rowIdx = 1; colIdx = 1 # aux vars to index selected cell

### declarations / definitions:
class TableModel(QtCore.QAbstractTableModel):

    def __init__(self, data):
        super(TableModel, self).__init__()
        self._data = data

    def data(self, index, role):

        if role == Qt.BackgroundRole and index.column() == colIdx and index.row() == rowIdx:
            # See below for the data structure.
            return QtGui.QColor('yellow')

        if role == Qt.DisplayRole:
            value = self._data.iloc[index.row(), index.column()]
            if isinstance(value, float): # to set fixed DISPLAYED precision of floats
                return "%.4f" % value
            return str(value)

    def rowCount(self, index):
        return self._data.shape[0]

    def columnCount(self, index):
        return self._data.shape[1]

    def headerData(self, section, orientation, role):
        # section is the index of the column/row.
        if role == Qt.DisplayRole:
            if orientation == Qt.Horizontal:
                return str(self._data.columns[section])

            if orientation == Qt.Vertical:
                return str(self._data.index[section])

class App(QtWidgets.QMainWindow):
     # local variable's declarations

     # init
    def __init__(self):
        super().__init__() # default one

        self.setup_main_window() # using helper function to increase readability (function call within self scope)
                                 # setup main window
        self.createLayout() # function call to create layouts with widgets
        self.post_main_window() # pass edited layouts to main window

     # declaration / methods / helper functions
    def setup_main_window(self): # to set window's / applet's properties
        self.centralwidget = QtWidgets.QWidget()
        self.setCentralWidget(self.centralwidget)        
        self.resize( 800, 400  )
        self.setWindowTitle( "# disposition calibration #" )

    def post_main_window(self): # to publish edited layouts in app window
        self.centralwidget.setLayout(self.lyoOut)
        
    def createLayout(self): # to create layouts with widgets
        self.lyoOut = QtWidgets.QVBoxLayout() # declare different layouts
        self.lyoIn1 = QtWidgets.QHBoxLayout()
        self.lyoIn2 = QtWidgets.QGridLayout()

        self.createWidgets() # function call pass widgets to sub-layouts

        self.lyoOut.addLayout(self.lyoIn1) # inner layouts got widgets by self.createWidgets()
        self.lyoOut.addLayout(self.lyoIn2) # merge edited inner layout in/to outside layout here

    def createWidgets(self): # create master-layout's widgets (function calls)
         # fill 1st row of ouside layout
        self.lyoIn1 = self.createNestedTabs(self.lyoIn1) # function call to create master-tabs
         # fill 2nd row of outside layout
        self.lyoIn2 = self.createButtons(self.lyoIn2) # function call to create buttons 

    def createNestedTabs(self, layout2modify): # create 1st tab layer
        self.MstTabs = QtWidgets.QTabWidget() # create tabs-widget
        self.MstTabs.setTabPosition(QtWidgets.QTabWidget.North) # set it's location
        self.MstTabs.addTab(self.createChildTabs(), "data") # add several sub-tab layouts to that widget
        self.MstTabs.addTab(Color("orange"), "plot") # 

        stylesheet = """ 
            QTabBar::tab:selected {background: lightgreen;}
            QTabBar::tab:!selected {background: lightyellow;}
            """ 
        self.MstTabs.setStyleSheet(stylesheet)

        layout2modify.addWidget(self.MstTabs) # add this tabs-widget to passed-in layout
        return layout2modify # return edited layout

    def createChildTabs(self): # create 2nd tab layer
        self.ChdTabs = QtWidgets.QTabWidget() # create tabs-widget
        self.ChdTabs.setTabPosition(QtWidgets.QTabWidget.West) # set it's location
        self.ChdTabs.addTab(self.createPandasTables(0), "A4") 
        self.ChdTabs.addTab(self.createPandasTables(1), "A5") 
        self.ChdTabs.addTab(self.createPandasTables(2), "A6") 
        self.ChdTabs.addTab(self.createPandasTables(3), "A7")

        return self.ChdTabs # return created widgets

    def createPandasTables(self, shtIdx): # to creating and editing pandas tables-widgets
         # use indexed (pandas)dataframe sheet values
        Lbl = ["a","b","c","d","e"]
        self.df = pd.DataFrame(rndArray[shtIdx], columns = Lbl, index = Lbl)
         # .. to create a widget
        self.table_widget = QtWidgets.QTableView() # create QTableView-Widget
        self.model = TableModel(self.df) # make df to user defined table model to use in widgets
        self.table_widget.setModel(self.model) # pass created model to created widget

         # certain formatings
        self.table_widget.resizeColumnsToContents() # set column width to content
        self.table_widget.horizontalHeader().setStretchLastSection(True) # strech last column to frame width
        self.table_widget.verticalHeader().setStretchLastSection(True) # strech last row to frame height
        self.table_widget.setAlternatingRowColors(True) # switch on alternating row highlighting

        return self.table_widget # return created widgets

    def createButtons(self, layout2modify): # helper function - to create layout's buttons
        bStoreI2C = QtWidgets.QPushButton("Store i²c")
        bStoreI2C.clicked.connect(lambda:self.storeVal())        
        bStoreI2C.setStyleSheet("QPushButton::hover" 
                             "{"
                             "background-color : yellow;"
                             "}")
        layout2modify.addWidget(bStoreI2C, 1, 3, 2, 1)

        self.lbl_1 = QtWidgets.QLabel()
        self.lbl_1.setText(str(rowIdx))
        self.lbl_1.setAlignment(QtCore.Qt.AlignCenter)
        layout2modify.addWidget(self.lbl_1, 1, 5, 2, 1)

        bRowAdd = QtWidgets.QPushButton("row +")
        bRowAdd.clicked.connect(lambda:self.rowAdd())
        layout2modify.addWidget(bRowAdd, 2, 6)


        bRowSub = QtWidgets.QPushButton("row -")
        bRowSub.clicked.connect(lambda:self.rowSub())
        layout2modify.addWidget(bRowSub, 1, 6)

        return layout2modify # return edited layout

    def storeVal(self):

        #i2c_vals = get_i2c_values(i2c_addrs)
        for i in range (0,4):
            #self.tbData[i, rowIdx, colIdx] = i2c_vals[i] # change cell entries with imported value
            rndArray[i, rowIdx, colIdx] = 0
        #self.tbData[sht, row, col] = 99 # change cell entry with imported value

        # try 1
        #self.table_widget.update()
        #self.table_widget.repaint()
         #self.model.select()
        #self.table_widget.select()
        # try 2
        
        # self.refreshModel() # not working so far
        #self.model = TableModel(self.df) # make df to user defined table model to use in widgets
        #self.table_widget.setModel(self.model) 

        # print(rndArray)
        print('i²c-value(s) stored')

    def rowAdd(self):
        global rowIdx

        rowIdx = (rowIdx + 1) % nRow # increment and modulo to cycle
        self.lbl_1.setText(str(rowIdx)) # update label's text

        print('row is ', rowIdx)

    def rowSub(self):
        global rowIdx

        rowIdx = (rowIdx - 1) % nRow # increment and modulo to cycle
        self.lbl_1.setText(str(rowIdx)) # update label's text

        print('row is ', rowIdx)

    
### main:
def main():
    app = QtWidgets.QApplication(sys.argv) # instanciate app
    window = App() # instanciate window

    window.show() # show window 
    
    app.exec_() # stuck here 'til window is closed
    print('# window will be terminated.. #')
    time.sleep(2)
    print('# ..app execution closed #')


# make file executable
if __name__ == '__main__':
    main()

rndGen.py: (is called in fillCSV_forum.py)

import numpy as np

def i2c_read():
    floats = np.random.rand(4,1,1) * 4.3

    return floats

uLib_coloredWidget.py: (is called in fillCSV_forum.py)

from PyQt5.QtGui import QColor, QPalette
from PyQt5.QtWidgets import QWidget


class Color(QWidget):
  def __init__(self, color):
    super().__init__()
    self.setAutoFillBackground(True)
    palette = self.palette()
    palette.setColor(QPalette.Window, QColor(color))
    self.setPalette(palette)

pip freeze --local-output of virtual enviroment:

numpy==1.23.0
pandas==1.4.3
PyQt5==5.15.7
PyQt5-Qt5==5.15.2
PyQt5-sip==12.11.0
python-dateutil==2.8.2
pytz==2022.1
six==1.16.0

Solution

  • [... additionally many hours of trial and error....]

    i think i finally got a dirty solution / work around..

    the problem i could determining, was e. g. if i am clicking the col+/- or store button, the focus of recently selected tab is vanishing. first when click again into any tab region or select another tabs those values are updating.

    so i tried to look for programmatically tab swap and did this as a dirty work around because i could not find a method like "reactivate tab again"

    i added ... :

        def storeVal(self):
    
            #i2c_vals = get_i2c_values(i2c_addrs)
            for i in range (0,nSht):
                self.df[i].iat[rowIdx, colIdx] = 99
    
            print('i²c-value(s) stored')
            self.show_data()
        
        def show_data(self):
                               
            x = self.ChdTabs.currentIndex()
            print(x) # debugging
            self.ChdTabs.setCurrentIndex(1)
            self.ChdTabs.setCurrentIndex(x)
    

    ... a show method and called it at the end of the store-method.

    in this show method i programmatically swap the active tab back and forth. this is so fast, that i cannot see it

    now my values are correctly shown

    another tiny if else code is necessary to also swap if tab 1 is selected, but this is cosmetic thing