PDA

View Full Version : Tabview update after the model data changes



drmacro
3rd April 2017, 18:14
I have searched and not found an answer that works. That may be because I still don't get the tableview --> model connection... :p

I have a QTableView and it's associated model and the model data starts out empty.

At some point the data is changed (in this case the data starts out empty and is added to) (I can see the data in the model has changed in the debugger)

But, the table view doesn't update.

The code that does the update:


self.tablemodel.setData(self.tablemodel.rowCount(N one), newitemindex,0)
self.tableView_ControlsInStrip.resizeColumnsToCont ents()
self.tableView_ControlsInStrip.selectRow(0)


The table model looks like this:

class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self, datain, headerdata, parent=None):
"""
Args:
datain: a list of lists\n
headerdata: a list of strings
"""
QtCore.QAbstractTableModel.__init__(self, parent)
self.arraydata = datain
self.headerdata = headerdata

def rowCount(self, parent):
return len(self.arraydata)

def columnCount(self, parent):
if len(self.arraydata) > 0:
return len(self.arraydata[0])
return 0

def data(self, index, role):
if not index.isValid():
return QVariant()
# elif role == Qt.BackgroundColorRole:
# #print (self.arraydata[index.row()][7])
# if self.arraydata[index.row()][7] == 'Stage':
# return QBrush(Qt.blue)
# elif self.arraydata[index.row()][7] == 'Sound':
# return QBrush(Qt.yellow)
# elif self.arraydata[index.row()][7] == 'Light':
# return QBrush(Qt.darkGreen)
# elif self.arraydata[index.row()][7] == 'Mixer':
# return QBrush(Qt.darkYellow)
# else:
# return QBrush(Qt.darkMagenta)
#
elif role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
return QtCore.QVariant(self.arraydata[index.row()][index.column()])

def setData(self, index, value, role):
self.arraydata.extend([supportedcontroltypes[index]])
self.dataChanged.emit(self.createIndex(0,0),
self.createIndex(self.rowCount(None),self.columnCo unt(None)))

def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return QtCore.QVariant(self.headerdata[col])
return QtCore.QVariant()

d_stranz
3rd April 2017, 18:55
Why are you ignoring the "role" in the setData() call? Who is calling setData(), and with what value(s)? Is it always your code, and is it always with a Qt.DisplayRole value for the role? If not, then you need to check that the role == Qt.DisplayRole and if not, do nothing.

Likewise, you do not check that "index" is valid; indeed, you ignore index entirely - what is index is invalid or pointing to an already existing element of arrayData? Your code seems to assume that it's always going to point beyond the end of the array, so you have to extend it.

And you ignore the "value" argument as well. Which begs the question - if your code is calling setData() and then ignoring everything that is passed as argument except self, why is your code bothering to call it at all? If what you want to do is to extend the array, then write yourself a custom method for your model that does that, don't override and misuse a standard model method. With the code as you've written it, you have no way to change existing elements in the model.

In C++, the setData() method is supposed to return "true" if the update was successful, "false" if not. Your implementation doesn't return anything. The default implementation returns false, so perhaps that is why the view update isn't occurring.

drmacro
3rd April 2017, 20:12
Why are you ignoring the "role" in the setData() call? Who is calling setData(), and with what value(s)? Is it always your code, and is it always with a Qt.DisplayRole value for the role? If not, then you need to check that the role == Qt.DisplayRole and if not, do nothing.

Likewise, you do not check that "index" is valid; indeed, you ignore index entirely - what is index is invalid or pointing to an already existing element of arrayData? Your code seems to assume that it's always going to point beyond the end of the array, so you have to extend it.

And you ignore the "value" argument as well. Which begs the question - if your code is calling setData() and then ignoring everything that is passed as argument except self, why is your code bothering to call it at all? If what you want to do is to extend the array, then write yourself a custom method for your model that does that, don't override and misuse a standard model method. With the code as you've written it, you have no way to change existing elements in the model.

In C++, the setData() method is supposed to return "true" if the update was successful, "false" if not. Your implementation doesn't return anything. The default implementation returns false, so perhaps that is why the view update isn't occurring.

Well, I'll reply, not necessarily in order.

"why is your code bothering to call it at all?"
Because I'm trying to figure out how to call it correctly and have had little success after reading docs, trying examples, and searching the web until my eyes are dry and blurry. Hence a blundering attempt at something.

"Who is calling setData()"
I'm calling it in this case. The doc ( https://doc.qt.io/qt-5/qabstractitemmodel.html#setData ) doesn't, at least to me, indicate that it gets called by native code.

"is it always with a Qt.DisplayRole value for the role"
in this case, I didn't bother checking it because I'm calling and it is set to 0. (Obviously not what I'd set it to for REAL use, though, here again, I'm not sure what other role I might want.)

"With the code as you've written it, you have no way to change existing elements in the model."
So, self.arraydata is not the model data that is displayed in the table?

"Your implementation doesn't return anything."
I changed it to return true. The table is still blank.

d_stranz
3rd April 2017, 21:41
I'm calling it in this case. The doc ( https://doc.qt.io/qt-5/qabstractitemmodel.html#setData ) doesn't, at least to me, indicate that it gets called by native code.

Views that are editable call it. The Qt Model / View architecture works roughly like this when the view is some sort of table view:

- the user clicks in the cell. This causes three things to happen: the table instantiates a widget for editing the value and places it in the space occupied by the cell, calls data() on the model using the EditRole to get the cell's value in editable form, then sets that value into the editor widget and sets input focus to it.

- the user edits the value in the editor.

- when editing concludes (user hits "Return", focus leaves the cell, whatever), the view copies the new value from the editor, destroys the edit widget, and then calls setData() on the model with the cell's index, new value, and EditRole role.

- the model replaces the current value at "index" with the new value and emits dataChanged().

- the view asks the model for the new formatted values to display in the range that has changed, using their indexes and DisplayRole.


"With the code as you've written it, you have no way to change existing elements in the model."
So, self.arraydata is not the model data that is displayed in the table?

If your table allows editing, then what your setData() method does (no matter what cell is edited), is extend the array and emit dataChanged() with the coordinates of the entire data array as what has changed. Your arrayData -is- what's being displayed in the table, because that is what the data() method uses when asked to return something for Qt.DisplayRole.

Your call to self.tablemodel.setData() also has an incorrect form for "index". It should be of the type "QModelIndex", not "int". This is an unfortunate side-effect of Python's lack of strict typing that it will allow any argument of any type in a method call, correct or not. A QModelIndex specifies both a row and a column for the model element. Your code assumes (I guess) a row only. Your code works because you allow the wrong type of argument, and because Python doesn't call you out for not following the rules. We C++ programmers can't get away with that.

You haven't posted the code that shows how you create the model in the first place or how you tell the view about it, so the failure to update the view when changes are made could be due to something wrong there.

drmacro
3rd April 2017, 21:51
The code that creates the tableview in this case:



self.self.tabledata = []
self.tablemodel = MyTableModel(self.tabledata, ['Controls'], self)
self.tableView_ControlsInStrip.setModel(self.table model)
self.tableView_ControlsInStrip.resizeColumnsToCont ents()
self.control_index = 0
self.tableView_ControlsInStrip.selectRow(self.cont rol_index)

d_stranz
4th April 2017, 22:30
Dunno. Looks OK to me, except that lines 4 - 6 won't really have any effect if the table has not yet been "shown" (eg. this code is executed in an __init__ method prior to a showEvent() on the table view). Until the table is actually visible on screen, its size is indeterminate, so calling a method like resizeColumnsToContent() has no effect. That particular method is not setting a flag, it is dynamically resizing the table view. If you call it once, then change the table values such that the width of the column(s) is different no resizing takes place until you call it again.

drmacro
5th April 2017, 16:34
The short answer is it turns out to be that I was not getting arraydata to be a list of lists.
(If I understand correctly, the list should look like: [[col1, col2, col3],[col1, col2, col3],...]
so each list is the columns of the row.)

As I often do when I'm confused, I make a minimum version of the code and figure out how to get that working.
This often helps enlighten me...
I did and it is shown below.

I was not able to grok a few things but this does work.

The docs for rowCount() and columnCount() say they should return 0 if it is a table. (it's not clear to me from the
docs whether, for a table it should ever return the actual count of rows/columns...) It shows the parent argument
to be a QModelIndex. What I gleaned is that a parent for trees would have children, where a table cell does not.
And, my experiments have always resulted in a non-valid parent when the selected cell of the table is sent as this argument.

I was also never able to get insertRow() to insert a row.
(Hence this example modifies the arraydata, then emits layoutChanged. Is this a valid or proper approach?)
The docs indicate that insertRow() calls insertRows() which is virtual. But it is not clear to me from the docs whether
my implementation of insertRows() MUST do all the actual inserting (is insertRow/s actually inserting a row in the
model's modelindex and not the model data?)

The docs also say:
"If row is 0, the rows are prepended to any existing rows in the parent.

If row is rowCount(), the rows are appended to any existing rows in the parent."

So, but what if the current model data is empty? i.e. arraydata = []
And, this appears to imply that rowCount() could return the actual number of rows...not 0 as noted above.

It also says: "If parent has no children, a single column with count rows is inserted."
But the parent ( a QModelIndex) for a table never has children.

And what does the view actually do when beginInsertRows/endInsertRows are called? Does this allow the view to manage if the
view needs to scroll or resize? Wouldn't this happen when layoutChanged is emitted anyway?

Sorry for the long post.


import sys

from PyQt5 import Qt, QtCore, QtGui, QtWidgets
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *

import TableUpdate_ui

class TblUpd(QtWidgets.QDialog, TableUpdate_ui.Ui_Dialog):
def __init__(self, parent=None):
QDialog.__init__(self, parent)
self.setupUi(self)
self.pushButton_update.clicked.connect(self.on_upd ate_btn_click)
self.pushButton_add.clicked.connect(self.on_add_bt n_click)
self.pushButton_insert.clicked.connect(self.on_ins ert_btn_click)
self.tabledata = []
# set the table model
self.tablemodel = MyTableModel(self.tabledata, ['Controls'], self)
self.tableView.horizontalHeader().setVisible(True)
self.tableView.setModel(self.tablemodel)
self.tableView.resizeColumnsToContents()
self.tableView.selectRow(0)

def on_update_btn_click(self):
print('Update button clicked.')
print('lineEdit.text = {0}'.format(self.lineEdit.text()))
if self.lineEdit.text() == '':
value = 'blank'
else:
value = self.lineEdit.text()
print('value = {0}'.format(value))
selectedindex = self.tableView.currentIndex()
print(self.tablemodel.setData(selectedindex, value,
QtCore.Qt.EditRole))
self.tableView.resizeColumnsToContents()
pass

def on_add_btn_click(self):
print('Insert button clicked.')
print('lineEdit.text = {0}'.format(self.lineEdit.text()))
if self.lineEdit.text() == '':
value = 'blank'
else:
value = self.lineEdit.text()
print('value = {0}'.format(value))
self.tablemodel.arraydata.extend([[value]])
self.tablemodel.layoutChanged.emit()
self.tableView.resizeColumnsToContents()

def on_insert_btn_click(self):
print('Insert clicked.')
if self.lineEdit.text() == '':
value = 'blank'
else:
value = self.lineEdit.text()
print('value = {0}'.format(value))
selectedindex = self.tableView.currentIndex()
self.tablemodel.arraydata.insert(selectedindex.row (), [value])
self.tablemodel.layoutChanged.emit()
self.tableView.resizeColumnsToContents()
pass

class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self, datain, headerdata, parent=None):
"""
Args:
datain: a list of lists\n
headerdata: a list of strings
"""
QtCore.QAbstractTableModel.__init__(self, parent)
self.arraydata = datain
self.headerdata = headerdata

def rowCount(self, parent):
if parent.isValid():
return 0
return len(self.arraydata)

def columnCount(self, parent):
if parent.isValid():
return 0
else:
return 1

def data(self, index, role):
print('In data()')
if not index.isValid():
print('Invalid index in MyModel>data')
retval = QtCore.QVariant()
elif role != QtCore.Qt.DisplayRole:
retval = QtCore.QVariant()
else:
retval = QtCore.QVariant(self.arraydata[index.row()][index.column()])
print(self.arraydata[index.row()][index.column()])
print(retval)
return retval

def setData(self, index, value, role):
if role == QtCore.Qt.EditRole and index.isValid():
print(index.row())
self.arraydata[index.row()] = [value]
print('Return from rowCount: {0}'.format(self.rowCount(index)))
self.dataChanged.emit(index, index, [QtCore.Qt.DisplayRole])
return True
return False

def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return QtCore.QVariant(self.headerdata[col])
return QtCore.QVariant()

def main():
app = QtWidgets.QApplication(sys.argv)
tblupd = TblUpd()
tblupd.show()
sys.exit(app.exec_())

if __name__ == '__main__':
main()

d_stranz
5th April 2017, 19:14
The docs for rowCount() and columnCount() say they should return 0 if it is a table.

Where did you get that idea? Are you maybe misinterpreting the C++ syntax in the documentation of QAbstractItemModel::columnCount()?



virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;


This means that columnCount() is a pure virtual method that has no implementation in this base class ("= 0") and must be overridden and implemented in a derived class.

A table model is basically a flat (row, column) matrix. For this type of model, if you are asked for the row or column count of an invalid QModelIndex (i.e. the "root" of the model), then you return the actual number of rows or columns in the matrix. For any other valid QModelIndex, you return zero because that implies that you are being asked for the number of child rows or columns of a given cell, which a flat matrix doesn't have. Only tree-structured models can have children within children.


But it is not clear to me from the docs whether my implementation of insertRows() MUST do all the actual inserting (is insertRow/s actually inserting a row in the model's modelindex and not the model data?)


For any method that modifies the table model, -you- must implement the code to modify the data array (list of lists) that underlies the model. You need to look at it this way: your list of lists is -your- representation of the data you want to display and modify. MyTableModel is the "glue" that connects your internal model to Qt's world of table views. So everything that the table view does to request data from or modify data in your list of lists goes through the MyTableModel glue. QAbstractTableModel, the base class, stores absolutely nothing. It is merely the definition of the interface that specifies the rules by which views interact with your model and how the model informs the views that its content is or has changed.


The docs also say:
"If row is 0, the rows are prepended to any existing rows in the parent.

If row is rowCount(), the rows are appended to any existing rows in the parent."

So your implementation has to do that. In the first case, you create a new row with the right number of empty column cells (i.e. a one element list containing a list of columns), append your existing list of lists to it, then assign the result back to your list of lists variable. In the second case, you need to create the same one element list of lists, then stick it on the end of the existing list. For a row in between you need to copy the existing list up to just before the indicated row, stick the new list onto that, then copy the rest of your existing list after that. The "insert" semantics imply "insert before" except when the insertion point is greater or equal to the number of rows.


And what does the view actually do when beginInsertRows/endInsertRows are called?

beginInsertRows() (or any of the "begin...() methods) emits a signal that the view listens for. When it gets that signal, it suspends any operations on the model, because the model has just told it it is changing and any model indexes the view has could become invalid or point to something else. endInsertRows() (or any end...() method) emits another signal that says, OK, I'm done making changes, you can now update that part of the view that was affected. modelAboutToBeReset() and modelReset() are the "nuclear option" - they tell the view that everything you know is wrong and that when it is all over you'll have to retrieve everything. These are useful when some external calculation updates the whole data set that the model wraps and there isn't really a way to do a partial update.

drmacro
6th April 2017, 15:47
d_stranz, as usual, your explanations are thorough and make it clear how things work! :)


Where did you get that idea?

Your explaination makes it very clear:

A table model is basically a flat (row, column) matrix. For this type of model,
if you are asked for the row or column count of an invalid QModelIndex (i.e. the "root" of the model),
then you return the actual number of rows or columns in the matrix. For any other valid QModelIndex,
you return zero because that implies that you are being asked for the number of child rows or columns
of a given cell, which a flat matrix doesn't have. Only tree-structured models can have children within children.

This from the doc page: https://doc.qt.io/qt-5/qabstractitemmodel.html#rowCount does not:
Note: When implementing a table based model, rowCount() should return 0 when the parent is valid.

In fact, the rest of that doc on rowCount() never mentions tree. After your explanation, it's clear what to do with a tree and why (it's the why that I find missing
often when reading doc...but, might be my issue.) :p

I'm slowly learning how to extrapolate what the QT docs say and what they imply.
For me, in many cases, it's sort of:
Me:"How do you make bread?"
Docs: "You use yeast and flower."
Me: "And?..."
:confused:

d_stranz
6th April 2017, 19:05
For me, in many cases, it's sort of:
Me:"How do you make bread?"
Docs: "You use yeast and flower."
Me: "And?..."


Funny language, English... most bakers would use "flour" when making bread, but I am always interested in hearing new recipes.


Note: When implementing a table based model, rowCount() should return 0 when the parent is valid.


As I said in my last post.

I agree, sometimes the only thing that makes sense in the Qt documentation are the tutorials and examples. The class documentation often leaves out important details that you don't understand until you look at an example. If you haven't looked at the Qt tutorials and examples, you should. Even if you aren't a C++ programmer, the language is similar enough to Python that I think you can follow them.

drmacro
6th April 2017, 21:53
Funny language, English... most bakers would use "flour" when making bread, but I am always interested in hearing new recipes.



As I said in my last post.

I agree, sometimes the only thing that makes sense in the Qt documentation are the tutorials and examples. The class documentation often leaves out important details that you don't understand until you look at an example. If you haven't looked at the Qt tutorials and examples, you should. Even if you aren't a C++ programmer, the language is similar enough to Python that I think you can follow them.

Yeah, that should have been flour...but, I'll bet you could dry flowers and make a flour. :o

In a past life I did plenty of C/C++ so the examples aren't foreign to me. That said, sometimes I still seem to be left wanting details. And, sometimes there don't seem to be examples that cover what I'm attempting (but, that could be I just don't know where to look.)

In any case, I appreciate you taking the time to explain!


Funny language, English... most bakers would use "flour" when making bread, but I am always interested in hearing new recipes.



As I said in my last post.

I agree, sometimes the only thing that makes sense in the Qt documentation are the tutorials and examples. The class documentation often leaves out important details that you don't understand until you look at an example. If you haven't looked at the Qt tutorials and examples, you should. Even if you aren't a C++ programmer, the language is similar enough to Python that I think you can follow them.

Yeah, that should have been flour...but, I'll bet you could dry flowers and make a flour. :o

In a past life I did plenty of C/C++ so the examples aren't foreign to me. That said, sometimes I still seem to be left wanting details. And, sometimes there don't seem to be examples that cover what I'm attempting (but, that could be I just don't know where to look.)

In any case, I appreciate you taking the time to explain!

Added after 1 25 minutes:

Ok, so if I want to use insertRow(), I need to implement insertRows().

InsertRows() should start with beginInsertRow(), insert the row/s, then endInsertRow().

InsertRow() and insertRows() don't have the new data for the row, thus insert empty rows in the model data.

So, I'm missing how the new data gets into the the newly created row.

If the insertRow() caller puts the data in, doesn't that potentially mess with the view, since the data will actually update after the endinsert has been called?

I'm confused again... :confused:

d_stranz
6th April 2017, 23:16
So, I'm missing how the new data gets into the the newly created row.


That's what setData() is for. Your UI calls insertRow() / insertRows(), which do indeed insert a row / rows of empty lists of lists into your data array (and call beginInsertRows() before and endInsertRows() after inserting). Your UI can then optionally call setData() on each cell in each row to add actual data to the elements of the data array. For efficiency, you might want to implement custom model method(s), setRow() or setRows(), which take a row index and list of lists or list of row indexes and lists of lists, respectively, and update entire row(s) of your data array in one go. In such a case, instead of emitting dataChanged() for each cell (and causing a table view refresh for each cell), you change everything first, then emit dataChanged() for the entire set of cells. This results in one view refresh versus many.

A really good book for understanding the model / view architecture is "Advanced Qt Programming" by Mark Summerfield. It is based on Qt4 and C++, but the M/V architecture hasn't changed all that much in Qt5 so almost everything still applies. Covers custom models, custom views, proxies, and more, with non-trivial examples.

Edit: I say "empty list of lists", but actually this would be more like a list of empty strings: [ "", "", "", "", ... ] (size equal to the column count) if that's what your data array contains.


Yeah, that should have been flour...but, I'll bet you could dry flowers and make a flour.

Heh. I saw an episode of the TV show "Shark Tank" a week or so ago where someone was trying to raise money for making energy bars from flour made from dried crickets.