How to refresh QTableView when it is driven by model - python

The code below creates QTableView driven by self.myModel (QAbstractTableModel).
'Show All' self.checkBox is linked to self.myModel.cbChanged() method.
Question: How to modify this code so 'QTableView' gets refreshed as soon as checkbox is checked?
The goal: when the checkbox is checked we want the odd numbered items to be displayed. And the even numbered items to be hidden.
When the checkbox is off (unchecked) we want the even numbered items to be displayed. All the odd numbered items are hidden.
import sys, os
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items=['One','Two','Three','Four','Five','Six','Seven']
self.cb_status=True
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def data(self, index, role):
if not index.isValid() or not (0<=index.row()<len(self.items)):
return QtCore.QVariant()
item=str(self.items[index.row()])
if role==QtCore.Qt.DisplayRole and self.cb_status:
return item
else:
return QtCore.QVariant()
def cbChanged(self, arg=None):
self.cb_status=arg
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
mainLayout=QtGui.QHBoxLayout()
self.setLayout(mainLayout)
self.viewA=QtGui.QTableView()
self.viewA.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch)
self.myModel=TableModel()
self.viewA.setModel(self.myModel)
self.checkBox=QtGui.QCheckBox("Show All")
self.checkBox.stateChanged.connect(self.myModel.cbChanged)
self.checkBox.setChecked(self.myModel.cb_status)
mainLayout.addWidget(self.viewA)
mainLayout.addWidget(self.checkBox)
self.show()
view=Window()
sys.exit(app.exec_())

For this purpose, you can use the QSortFilterProxyModel class. This way, we don't tamper with the actual source model's structure or it's data. We just map the main source to this proxy model, which the view uses to display filtered/sorted data. We can affect the data in the proxy model as we please, without the risk of tampering the source model.
Here is your source code modified to use this:
import sys, os
from PyQt4 import QtCore, QtGui
app=QtGui.QApplication(sys.argv)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items=['One','Two','Three','Four','Five','Six','Seven']
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.items)
def columnCount(self, index=QtCore.QModelIndex()):
return 1
def data(self, index, role):
if not index.isValid() or not (0<=index.row()<len(self.items)):
return QtCore.QVariant()
item=str(self.items[index.row()])
if role==QtCore.Qt.DisplayRole:
return item
else:
return QtCore.QVariant()
class MySortFilterProxyModel(QtGui.QSortFilterProxyModel):
def __init__(self):
super(MySortFilterProxyModel, self).__init__()
self.cb_status=True
def cbChanged(self, arg=None):
self.cb_status=arg
print self.cb_status
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow, sourceParent):
print_when_odd_flag = self.cb_status
is_odd = True
index = self.sourceModel().index(sourceRow, 0, sourceParent)
print "My Row Data: %s" % self.sourceModel().data(index, role=QtCore.Qt.DisplayRole)
if (sourceRow + 1) % 2 == 0:
is_odd = False
if print_when_odd_flag:
if is_odd:
return True
else:
return False
else:
if not is_odd:
return True
else:
return False
class Window(QtGui.QWidget):
def __init__(self):
super(Window, self).__init__()
mainLayout=QtGui.QHBoxLayout()
self.setLayout(mainLayout)
self.viewA=QtGui.QTableView()
self.viewA.horizontalHeader().setResizeMode(QtGui.QHeaderView.Stretch)
self.myModel=TableModel()
self.sortModel = MySortFilterProxyModel()
self.sortModel.setSourceModel(self.myModel)
self.viewA.setModel(self.sortModel)
self.checkBox=QtGui.QCheckBox("Show All")
self.checkBox.stateChanged.connect(self.sortModel.cbChanged)
self.checkBox.setChecked(self.sortModel.cb_status)
mainLayout.addWidget(self.viewA)
mainLayout.addWidget(self.checkBox)
self.show()
view=Window()
sys.exit(app.exec_())
As you can see, I have removed all connection from UI and the main source model. The main source model does not care about whether the checkbox is set or not. This keeps it decoupled. It's cleaner. The proxy model has been given this responsibility now. The filterAcceptsRow() does the main heavy lifting of displaying the right row based on whether the index of the row shown is odd or even based on the checkbox status.
I have added a few print statements to it, just in case you want to alter the logic based on the data and not the index.
Check out the docs on QSortFilterProxyModel and some examples here.
Hope this was useful.

QTableView instance has viewport which has update method through that we can update the table content
Use the update method of viewport to update the content of a tableview
viewport().update()
try using :
self.viewA.viewport().update()

Related

PySide6: column of checkboxes for a boolean variable in a data table [duplicate]

How to make QAbstractTableModel 's data checkable
I want to make each cell in the following code can be checked or unchecked by the user ,how to modify the code ?
according to the Qt documentation :Qt::CheckStateRole and set the Qt::ItemIsUserCheckable might be used ,so anyone can give a little sample ?
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class MyModel(QAbstractTableModel):
def __init__(self, parent=None):
super(MyModel, self).__init__(parent)
def rowCount(self, parent = QModelIndex()):
return 2
def columnCount(self,parent = QModelIndex()) :
return 3
def data(self,index, role = Qt.DisplayRole) :
if (role == Qt.DisplayRole):
return "Row{}, Column{}".format(index.row() + 1, index.column() +1)
return None
if __name__ == '__main__':
app =QApplication(sys.argv)
tableView=QTableView()
myModel = MyModel (None);
tableView.setModel( myModel );
tableView.show();
sys.exit(app.exec_())
Override the flags function in MyModel.
def flags(self, index)
return super(MyModel, self).flags(index)|QtCore.Qt.ItemIsUserCheckable
This says that the index in your model is checkable.
Then override the data function.
def data(self,index, role = Qt.DisplayRole) :
if (role == Qt.DisplayRole):
return "Row{}, Column{}".format(index.row() + 1, index.column() +1)
elif (role==Qt.CheckStateRole):
# read from your data and return Qt.Checked or Unchecked
return None
Finally, you need to implement the setData function.
def setData(self, index, value, role = Qt.EditRole):
if (role==Qt.CheckStateRole):
# Modify your data.

QTreeView requests index for invalid row

Have a look at the following MWE.
It is a simple QAbstractItemModel with only a single level, storing its items in a list. I create a QTreeView to display the model, and a button to remove the 2nd item.
from PyQt5.QtCore import QModelIndex, QAbstractItemModel, Qt
from PyQt5.QtWidgets import QTreeView, QApplication, QPushButton
class Item:
def __init__(self, title):
self.title = title
class TreeModel(QAbstractItemModel):
def __init__(self, parent=None):
super().__init__(parent)
self._items = [] # typing.List[Item]
def addItem(self, item: Item):
self.beginInsertRows(QModelIndex(), len(self._items), len(self._items))
self._items.append(item)
self.endInsertRows()
def removeItem(self, item: Item):
index = self._items.index(item)
self.beginRemoveRows(QModelIndex(), index, index)
self._items.remove(item)
self.endRemoveRows()
# ----- overridden methods from QAbstractItemModel -----
# noinspection PyMethodOverriding
def data(self, index: QModelIndex, role):
item = index.internalPointer()
if role == Qt.DisplayRole:
return item.title
# noinspection PyMethodOverriding
def rowCount(self, parent=QModelIndex()):
if not parent.isValid():
return len(self._items)
return 0
# noinspection PyMethodOverriding
def columnCount(self, parent=QModelIndex()):
return 1
# noinspection PyMethodOverriding
def index(self, row: int, col: int, parent=QModelIndex()):
assert not parent.isValid()
return self.createIndex(row, 0, self._items[row])
def parent(self, index=QModelIndex()):
return QModelIndex()
def removeItem():
model.removeItem(item2)
if __name__ == '__main__':
app = QApplication([])
model = TreeModel()
button = QPushButton('Delete')
button.clicked.connect(removeItem)
button.show()
item1 = Item('Item 1')
model.addItem(item1)
item2 = Item('Item 2')
model.addItem(item2)
treeView = QTreeView()
treeView.setModel(model)
treeView.show()
app.exec()
As far as I can tell, the implementation of my model is correct (though very basic). In particular, the row, and column counts it reports are correct, and it never creates indices for data that would not be valid.
Steps to reproduce my issue:
Run the code above.
In the tree view, select Item 2.
Press the Delete button.
On my system, the application crashes in beginRemoveRows(), because the view requests a QModelIndex for row 2. Naturally, row 2 does not exist.
Any idea why the QTreeView would think there would be 3 rows, when the model explicitly reports there are only 2?
When an item is added, moved, removed, etc, what the model does is verify the QPersistentModelIndex are valid or not, so it calls the index() method of QAbstractItemModel. And in that method it is the developer's responsibility to verify if the row or column is valid, and for that the model provides the hasIndex() method that you do not use causing the error you point out, so the solution is:
def index(self, row: int, col: int, parent=QModelIndex()):
if not self.hasIndex(row, col, parent):
return QModelIndex()
assert not parent.isValid()
return self.createIndex(row, 0, self._items[row])

QTableView model crash when calling endInsertRows()

I have been trying to update a QTableViewModel when inserting a new object that represents a row. I did follow the advise of several question in SO, but I cannot get an example to work.
After debugging, I found that the call to self.endInsertRows() produces the crash.
This is a minimal example:
import sys
from PyQt5.QtWidgets import *
from PyQt5 import QtCore, QtGui, QtWidgets
class Wire:
def __init__(self, name, x, y, gmr, r):
self.name = name
self.x = x
self.y = y
self.r = r
self.gmr = gmr
class WiresCollection(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.header = ['Name', 'R (Ohm/km)', 'GMR (m)']
self.index_prop = {0: 'name', 1: 'r', 2: 'gmr'}
self.wires = list()
def add(self, wire: Wire):
"""
Add wire
:param wire:
:return:
"""
row = len(self.wires)
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self.wires.append(wire)
self.endInsertRows()
def delete(self, index):
"""
Delete wire
:param index:
:return:
"""
row = len(self.wires)
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self.wires.pop(index)
self.endRemoveRows()
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.wires)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self.header)
def parent(self, index=None):
return QtCore.QModelIndex()
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
val = getattr(self.wires[index.row()], self.index_prop(index.column()))
return str(val)
return None
def headerData(self, p_int, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self.header[p_int]
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
"""
Set data by simple editor (whatever text)
:param index:
:param value:
:param role:
"""
wire = self.wires[index.row()]
attr = self.index_prop[index.column()]
setattr(wire, attr, value)
class TowerBuilderGUI(QtWidgets.QDialog):
def __init__(self, parent=None):
"""
Constructor
Args:
parent:
"""
QtWidgets.QDialog.__init__(self, parent)
self.setWindowTitle('Tower builder')
# GUI objects
self.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
self.layout = QVBoxLayout(self)
self.wires_tableView = QTableView()
self.add_wire_pushButton = QPushButton()
self.add_wire_pushButton.setText('Add')
self.delete_wire_pushButton = QPushButton()
self.delete_wire_pushButton.setText('Delete')
self.layout.addWidget(self.wires_tableView)
self.layout.addWidget(self.add_wire_pushButton)
self.layout.addWidget(self.delete_wire_pushButton)
self.setLayout(self.layout)
# Model
self.wire_collection = WiresCollection(self)
# set models
self.wires_tableView.setModel(self.wire_collection)
# button clicks
self.add_wire_pushButton.clicked.connect(self.add_wire_to_collection)
self.delete_wire_pushButton.clicked.connect(self.delete_wire_from_collection)
def msg(self, text, title="Warning"):
"""
Message box
:param text: Text to display
:param title: Name of the window
"""
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText(text)
# msg.setInformativeText("This is additional information")
msg.setWindowTitle(title)
# msg.setDetailedText("The details are as follows:")
msg.setStandardButtons(QMessageBox.Ok)
retval = msg.exec_()
def add_wire_to_collection(self):
"""
Add new wire to collection
:return:
"""
name = 'Wire_' + str(len(self.wire_collection.wires) + 1)
wire = Wire(name, x=0, y=0, gmr=0, r=0.01)
self.wire_collection.add(wire)
def delete_wire_from_collection(self):
"""
Delete wire from the collection
:return:
"""
idx = self.ui.wires_tableView.currentIndex()
sel_idx = idx.row()
if sel_idx > -1:
self.wire_collection.delete(sel_idx)
else:
self.msg('Select a wire in the wires collection')
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = TowerBuilderGUI()
window.show()
sys.exit(app.exec_())
As indicated in the comments you have 2 errors:
The first is when you press add because when you add a new item you have to refresh the view and that's why it's called data() method and it's where the error is shown in self.index_prop(index.column()), index_pro is a dictionary so you should use [] instead of ().
val = getattr(self.wires[index.row()], self.index_prop[index.column()])
Another error is generated by the line idx = self.ui.wires_tableView.currentIndex(), the ui does not exist and it is not necessary, sure it is a remnant of a previous code, to access wires_tableView as this is a member of the class not it is necessary to use an intermediary, you must access directly with self: idx = self.wires_tableView.currentIndex()
The above are typos and probably mark it so that the question is closed there is another error that is not, and that is the reason for my answer.
In the line self.beginRemoveRows(...) you must pass the row that you are going to remove but you are passing the row that does not exist:
row = len(self.wires)
self.beginRemoveRows(QtCore.QModelIndex(), row, row) # <---- row does not exist in the table
The solution is simple, change it by index:
def delete(self, index):
"""
Delete wire
:param index:
:return:
"""
self.beginRemoveRows(QtCore.QModelIndex(), index, index)
self.wires.pop(index)
self.endRemoveRows()

Wrong rowCount when TableView is empty in PySIde

I am using PySide, with a tableview and a model as shown below:
# -*- coding: utf-8 -*-
from PySide import QtCore, QtGui
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, items, headers, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.items = items
self.headers = headers
def rowCount(self, parent):
return len(self.items)
def columnCount(self, parent):
return len(self.items[0])
def data(self, index, role):
if not index.isValid():
return None
elif role != QtCore.Qt.DisplayRole:
return None
return self.items[index.row()][index.column()]
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.headers[section]
return None
This is how I set the model, importing from models.py:
table_model = models.TableModel(data, headers)
self.tableView.setModel(table_model)
I get the tableview total rows:
table_model = tableview.model()
total_rows = table_model.rowCount(tableview)
If there is no data to display in the tableview, I set data = [[]].
If I try to get the rowCount, I get 1 instead of O.
I realize it probably counts the nested list set as data.
So, what should I do to get the right rou count?
Use data = [] when there's no data, and then implement columnCount like this:
def columnCount(self, parent):
return len(self.items[0]) if self.items else 0

Why is my QListView empty in IconMode?

I have a QListView displaying data from a custom ListModel. Everything seems to be working fine in the "regular" view mode (ListMode) -- icons, labels, drag/drop, etc. As soon as I change it to IconMode nothing displays.
Here's the relevant code. I've left out the main window and any other cruft, but if it helps I'll include it.
# Model
class TheModel(QtCore.QAbstractListModel):
def __init__(self, items = [], parent = None):
QtCore.QAbstractListModel.__init__(self, parent)
self.__items = items
def appendItem(self, item):
self.__items.append(item)
# item was added to end of list, so get that index
index = len(self.__items) - 1
# data was changed, so notify
self.dataChanged.emit(index, index)
def rowCount(self, parent):
return len(self.__items)
def data(self, index, role):
image = self.__items[index.row()]
if role == QtCore.Qt.DisplayRole:
# name
return image.name
if role == QtCore.Qt.DecorationRole:
# icon
return QtGui.QIcon(image.path)
return None
# ListView
class TheListView(QtGui.QListView):
def __init__(self, parent=None):
super(Ui_DragDropListView, self).__init__(parent)
self.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.setIconSize(QtCore.QSize(48, 48))
self.setViewMode(QtGui.QListView.IconMode)
# ...
After some heavy debugging, I found that data() was never getting called. The problem was with the way I was inserting data into the model: beginInsertRows() and endInsertRows() should be called. The new method resembles the following:
def appendItem(self, item):
index = len(self.__items)
self.beginInsertRows(QtCore.QModelIndex(), index, index)
self.__items.append(item)
self.endInsertRows()
Despite the old method not using using beginInsertRows() and endInsertRows(), ListMode worked just fine. That's what threw me off: I still don't think it should have worked. Quirk?

Categories