I have a question since I read a lot of tutorial but can't figured how to make my program work properly.
I want to subclass QAbtractTableModel and set this model to a QTableView. Then I want to change some items in the view. I've create a simple program to experiment but I can't manage to get it work without creating a new model and set the new model each time.
What I ask is: can I change the items of the model and then automatically update the view ? I know it is possible with dataChanged() but I don't understand how it works.
The perfect solution would be to "bind" a variable (list1) with the data of the table model and any time the variable is modify the model and the view are updated. But I don't know if it is possible.
I read a lot about signals/slots, delegates, model/view and I'm lost.
class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self, symbList=[[]], parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.header = ['Char', 'Dec', 'Hex']
self.symbList = symbList
def rowCount(self, parent):
return len(self.symbList)
def columnCount(self, parent):
if len(self.symbList) > 0:
return len(self.symbList[0])
return 0
def data(self, index, role):
if not index.isValid():
return None
elif role != QtCore.Qt.DisplayRole:
return None
return self.symbList[index.row()][index.column()]
"""
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable
def setData(self, index, value, role=QtCore.Qt.EditRole):
self.symbList[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
"""
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and \
role == QtCore.Qt.DisplayRole:
return self.header[col]
return None
def sort(self, col, order):
"""sort table by given column number col"""
self.layoutAboutToBeChanged.emit()
self.symbList = sorted(self.symbList,
key=operator.itemgetter(col))
if order == QtCore.Qt.DescendingOrder:
self.symbList.reverse()
self.layoutChanged.emit()
if __name__ == '__main__':
list1 = [['Z', '90', '5A'], ['R', '82', '52']]
list2 = [['G', '71', '47'], ['O', '79', '4F']]
app = QtGui.QApplication([])
table = QtGui.QTableView() # the view
table.show()
tableModel = MyTableModel(list1) # create a model with list1
table.setModel(tableModel)
tableModel = MyTableModel(list2) # override the table with list2
table.setModel(tableModel)
sys.exit(app.exec_())
Related
I would like to change the background color of specific index on my table, but only after a specific task is completed.
I know that I can use the Background role to change the color in my Table model, but I want to change the background color on external factors and not based on changes to the table itself. For example, the code below shows a basic example of a QTableView with 6 rows displayed in a QWidget. Inside the main app I am able to change the text of specific indexes using setData as seen below.
model.setData(model.index(2, 0), "Task Complete")
Here is the full code:
import sys
from PySide6.QtWidgets import (
QApplication, QWidget, QTableView, QVBoxLayout
)
from PySide6.QtCore import Qt, QAbstractTableModel
from PySide6.QtGui import QBrush
class TableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role=Qt.DisplayRole):
# display data
if role == Qt.DisplayRole:
try:
return self._data[index.row()][index.column()]
except IndexError:
return ''
def setData(self, index, value, role=Qt.EditRole):
if role in (Qt.DisplayRole, Qt.EditRole):
# if value is blank
if not value:
return False
self._data[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
def flags(self, index):
return super().flags(index) | Qt.ItemIsEditable
class MainApp(QWidget):
def __init__(self):
super().__init__()
self.window_width, self.window_height = 200, 250
self.setMinimumSize(self.window_width, self.window_height)
self.layout = {}
self.layout['main'] = QVBoxLayout()
self.setLayout(self.layout['main'])
self.table = QTableView()
self.layout['main'].addWidget(self.table)
model = TableModel(data)
self.table.setModel(model)
# THIS IS WHERE THE QUESTION IS
model.setData(model.index(2, 0), "Task Complete") # Change background color instead of text
model.setData(model.index(5, 0), "Task Complete") # Change background color instead of text
if __name__ == '__main__':
data = [
["Task 1"],
["Task 2"],
["Task 3"],
["Task 4"],
["Task 5"],
["Task 6"],
]
app = QApplication(sys.argv)
myApp = MainApp()
myApp.show()
try:
sys.exit(app.exec())
except SystemExit:
print('Closing Window...')
I have tried to change the setData function to use the Qt.BackgroundRole instead of Qt.EditRole, but that does not work for changing the color. The result is that the code runs, but nothing happens.
I want to be able to fill the background with whatever color I choose based on the specific index I pick. However, I want this code to reside inside the MainApp class and not in the TableModel Class.
Suggestions Tried
Added code to data()
if role == Qt.BackgroundRole:
return QBrush(Qt.green)
Changed setData()
def setData(self, index, value, role=Qt.BackgroundRole):
if role in (Qt.DisplayRole, Qt.BackgroundRole):
# if value is blank
if not value:
return False
self._data[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
Changed setData in MainApp too
model.setData(model.index(5, 0), QBrush(Qt.green))
This resulted in highlighting the entire table in green instead of specific index.
If you want to set different colors for each index, you must store the color information in another data structure and return the corresponding value for the index.
Both data() and setData() must access different values depending on the role (see the documentation about item roles), meaning that you must not use self._data indiscriminately for anything role. If you set the color for a row/column in the same data structure you use for the text, then the text is lost.
A simple solution is to create a list of lists that has the same size of the source data, using None as default value.
class TableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
rows = len(data)
cols = len(data[0])
self._backgrounds = [[None] * cols for _ in range(rows)]
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return
elif role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
elif role == Qt.BackgroundRole:
return self._backgrounds[index.row()][index.column()]
def setData(self, index, value, role=Qt.EditRole):
if (
not index.isValid()
or index.row() >= len(self._data)
or index.column() >= len(self._data[0])
):
return False
if role == Qt.EditRole:
self._data[index.row()][index.column()] = value
elif role == Qt.BackgroundRole:
self._backgrounds[index.row()][index.column()] = value
else:
return False
self.dataChanged.emit(index, index, [role])
return True
Note: you should always ensure that data has at least one row, otherwise columnCount() will raise an exception.
Then, to update the color, you must also use the proper role:
model.setData(model.index(5, 0), QBrush(Qt.green), Qt.BackgroundRole)
Note that if you don't need to keep the data structure intact (containing only the displayed values), a common solution is to use dictionaries.
You could use common dictionary that has the role as key and the data structure as value:
class TableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
rows = len(data)
cols = len(data[0])
self._data = {
Qt.DisplayRole: data,
Qt.BackgroundRole: [[None] * cols for _ in range(rows)]
}
# implement the other functions accordingly
Otherwise, use a single structure that uses unique dictionaries for each item:
class TableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = []
for rowData in data:
self._data.append([
{Qt.DisplayRole: item} for item in rowData
])
def data(self, index, role=Qt.DisplayRole):
if not index.isValid():
return
data = self._data[index.row()][index.column()]
if role == Qt.EditRole:
role = Qt.DisplayRole
return data.get(role)
def setData(self, index, value, role=Qt.EditRole):
if (
not index.isValid()
or role not in (Qt.EditRole, Qt.BackgroundRole)
):
return False
self._data[index.row()][index.column()][role] = value
self.dataChanged.emit(index, index, [role])
return True
I am writing a gui application for processing multiple Excel files into a single file. The main window displays a table to which the user can add or remove files to be collected in the larger file. Each row will represent a file with columns for different parameters (data to be pulled from that file.) I've been working on implementing a QAbstractTableModel for this purpose, which works great, although I've not been able to update the table view. When a new row is added to my array of data, a new row is added to the table view but each column is empty. I'm not sure why this is as I've confirmed that the data array is updating as it should. Example:
class _tableModel(QAbstractTableModel):
def __init__(self, data=None):
QAbstractTableModel.__init__(self)
self.data = data
self.load_data(data)
def load_data(self, data):
self.input_files = data[:,0]
self.input_sheets = data[:,1]
self.column_count = 2
self.row_count = len(self.input_sheets)
def rowCount(self, parent=QModelIndex()):
return self.row_count
def columnCount(self, parent=QModelIndex()):
return self.column_count
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return None
if orientation == Qt.Horizontal:
return ("File", "Sheet")[section]
else:
return "{}".format(section)
def data(self, index, role=Qt.DisplayRole):
column = index.column()
row = index.row()
if role == Qt.DisplayRole:
if column == 0:
file = str(self.input_files[row])
return file
elif column == 1:
return str(self.input_sheets[row])
elif role == Qt.BackgroundRole:
return QColor(Qt.white)
elif role == Qt.TextAlignmentRole:
return Qt.AlignRight
return None
def appendRowData(self, data):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.data = np.concatenate((self.data, data), axis=0)
self.endInsertRows()
def setData(self, index, value, role=Qt.EditRole):
if role == Qt.EditRole:
self.data[index.row()][index.column()] = value
self.dataChanged.emit(index, index)
return True
class Widget(QWidget):
def __init__(self, data=None):
QWidget.__init__(self)
self.main_layout = QVBoxLayout()
self.addFileButton = QPushButton('Add File')
self.addFileButton.clicked.connect(self.addFileDialog)
self.main_layout.addWidget(self.addFileButton)
self.model = _tableModel(data)
self.table_view = QTableView()
self.table_view.setModel(self.model)
self.horizontal_header = self.table_view.horizontalHeader()
self.vertical_header = self.table_view.verticalHeader()
self.horizontal_header.setSectionResizeMode(
QHeaderView.ResizeToContents
)
self.vertical_header.setSectionResizeMode(
QHeaderView.ResizeToContents
)
self.horizontal_header.setStretchLastSection(True)
size = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
size.setHorizontalStretch(1)
self.table_view.setSizePolicy(size)
self.main_layout.addWidget(self.table_view)
self.setLayout(self.main_layout)
def addFileDialog(self):
self.fileWizard = QDialog()
self.wiz_layout = QFormLayout()
self.fileWizard.setLayout(self.wiz_layout)
self.selectExcelFile = QPushButton('Select Excel File')
self.selectExcelFile.clicked.connect(self.selectexcel)
self.wiz_layout.addRow(self.selectExcelFile)
self.selectedFileDisplay = QTextEdit()
self.wiz_layout.addRow(self.selectedFileDisplay)
self.sheet = QLineEdit()
self.wiz_layout.addRow('Sheet Name: ', self.sheet)
self.add_to_table = QPushButton('Add to File Table')
self.wiz_layout.addWidget(self.add_to_table)
self.add_to_table.clicked.connect(self._addTableEntry)
self.fileWizard.show()
def selectexcel(self):
self.filename = QFileDialog.getOpenFileName(self)
self.filename = self.filename[0]
self.selectedFileDisplay.setText(self.filename)
pass
def _addTableEntry(self):
row = self.model.rowCount()
data = np.array([[self.filename, self.sheet.text()]])
self.model.appendRowData(data)
class _mainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.generalLayout = QGridLayout()
data = np.array([['File name here','Sheet name here']])
self._centralWidget = Widget(data)
self.setCentralWidget(self._centralWidget)
self._centralWidget.setLayout(self.generalLayout)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = _mainWindow()
window.show()
sys.exit(app.exec_())
Both rowCount and columnCount must be dynamic, since they return the current extent of the model.
You're setting those values as static, so, while the model "accepts" the insertion of rows (as shown in the view), it's not able to access them because the row and column count don't reflect the updated model size.
def rowCount(self, parent=QModelIndex()):
return len(self.data[:,1])
Note that you're also not updating the self.data but in fact replacing it. Since you're using self.input_files for data(), it will probably result in an exception, as those arrays are not actually updated.
So, you either always use self.data anywhere with the correct slices, or use python properties to access input_files and input_sheets.
class _tableModel(QAbstractTableModel):
# ...
#property
def input_files(self):
return self.data[:,0]
# ...
I'm trying to set a value in a cell in my QtableView. I'm using Pyside2.
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, mlist=None):
super(TableModel, self).__init__()
self._items = [] if mlist == None else mlist
self._header = []
def data(self, index, role = QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self._items[index.row()][index.column()]
return None
def setData(self, index, value, role = QtCore.Qt.EditRole):
if value is not None and role == QtCore.Qt.EditRole:
self._items[index.row()][index.column()] = value
return True
return False
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
# skip the irrelevant code #
Table = QTableView()
model = TableModel()
Table.setModel(model)
row, column = a_function_that_outputs_the_row_and_column_of_the_cell_to_edit()
self.Table.model().setData(index ?, value)
What I'm struggling with is getting to understand how to pass the index as an argument, I have the row and column, any help on how to do that properly?
You need to use model.index(row, column, parent=None) (see the documentation) to get the appropriate QModelIndex.
If the model is two dimensional (not a tree view), the parent is not required:
model.setData(model.index(row, column), value)
I have this simple example: a value in last column of my QAbstractTableModel equals value in column 1 multiplied by 2. So every time a change is made to value in column 1 - it results to a change in column 2.
When the value in the last column has changed - a message box is shown asking whether user wishes confirm action.
Imagine user have changed value in column 1 and sees this message: I wish my app to undo changes if cancel is clicked (return both old values in column 1 and 2), can you help me with that? I need to define my 'go_back()' function:
class Mainwindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table = QtWidgets.QTableView()
self.setCentralWidget(self.table)
self.data = [
[1, 0.18, 0.36],
[2, 0.25, 0.50],
[3, 0.43, 0.86],
]
self.model = MyModel(self.data)
self.table.setModel(self.model)
self.table.setSelectionBehavior(self.table.SelectRows)
self.table.setSelectionMode(self.table.SingleSelection)
self.model.dataChanged.connect(lambda index: self.count_last_column(index))
self.model.dataChanged.connect(lambda index: self.if_last_column_changed(index))
def calculations(self, position):
value = self.model.list_data[position][1] * 2
return value
def count_last_column(self, index):
if index.column() == 1:
position = index.row()
self.model.setData(self.model.index(position,2), self.calculations(position))
def if_last_column_changed(self, index):
if index.column() == 2:
message_box, message_box_button = self.show_message_box()
if message_box_button == 'Ok':
pass
elif message_box_button == 'Cancel':
self.go_back()
def show_message_box(self):
self.message_box = QtWidgets.QMessageBox(QtWidgets.QMessageBox.Warning, 'Action', 'Value in column 3 has changed, confirm action?')
self.message_box.Ok = self.message_box.addButton(QtWidgets.QMessageBox.Ok)
self.message_box.Cancel = self.message_box.addButton(QtWidgets.QMessageBox.Cancel)
self.message_box.exec()
if self.message_box.clickedButton() == self.message_box.Ok:
return (self.message_box, 'Ok')
elif self.message_box.clickedButton() == self.message_box.Cancel:
return (self.message_box, 'Cancel')
def go_back(self):
pass #################
class MyModel(QtCore.QAbstractTableModel):
def __init__(self, list_data = [[]], parent = None):
super(MyModel, self).__init__()
self.list_data = list_data
def rowCount(self, parent):
return len(self.list_data)
def columnCount(self, parent):
return len(self.list_data[0])
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = self.list_data[row][column]
return value
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
value = self.list_data[row][column]
return value
def setData(self, index, value, role = QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.list_data[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable
if __name__ == '__main__':
app = QtWidgets.QApplication([])
application = Mainwindow()
application.show()
sys.exit(app.exec())
One option is to use a QUndoStack with the item model, and push QUndoCommand objects onto the stack in setData. The benefit of this approach is that it makes it simple to implement more undo/redo controls going forward, if you want to.
In MyModel, just create a stack in the constructor and add a line to push a command onto the stack right before you modify the list data (so the previous value can be stored in the command). The rest of the class is unchanged.
class MyModel(QtCore.QAbstractTableModel):
def __init__(self, list_data = [[]], parent = None):
super(MyModel, self).__init__()
self.list_data = list_data
self.stack = QtWidgets.QUndoStack()
def setData(self, index, value, role = QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
row = index.row()
column = index.column()
self.stack.push(CellEdit(index, value, self))
self.list_data[row][column] = value
self.dataChanged.emit(index, index)
return True
return False
Create the QUndoCommand with the index, value, and model passed to the constructor so the desired cell can be modified with calls to undo or redo.
class CellEdit(QtWidgets.QUndoCommand):
def __init__(self, index, value, model, *args, **kwargs):
super().__init__(*args, **kwargs)
self.index = index
self.value = value
self.prev = model.list_data[index.row()][index.column()]
self.model = model
def undo(self):
self.model.list_data[self.index.row()][self.index.column()] = self.prev
def redo(self):
self.model.list_data[self.index.row()][self.index.column()] = self.value
And now all that needs to be done in go_back is to call the undo method twice, for both cells that were modified.
def go_back(self):
self.model.stack.undo()
self.model.stack.undo()
The code below creates a single QTableView driven by Model/Proxy framework.
The self.headerNames list-variable declared in source-model stores the names of the Header's Columns. The number of names in this list is used by the same source model's columnCount() method to return the number of column in a view:
def columnCount(self, parent=QModelIndex()):
return len(self.headerNames)
The Proxy model's headerData() access this self.headerNames variable via source model:
sourceModel=self.sourceModel()
On if role==Qt.DisplayRole the Proxy retrieves and returns the name of the column to QTableView:
return QVariant( sourceModel.headerNames[column] )
There is a right-click menu implemented on header-column right-click.
That portion is working fine. But since I could not find any examples on how others do it I had to design how it would work myself. I would appreciate if you find it could be improved.
What I want to implement next is the ability to re-arrange the column in arbitrary order. But I am not sure where to start.
P.s. Please disregard the names of the Items displayed in QTableView.
I wanted to keep the code as simple as possible focusing on Header/Column subject only here.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys
class Model(QAbstractTableModel):
def __init__(self, parent=None, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.items = ['Item_A_001','Item_A_002','Item_B_001','Item_B_002']
self.headerNames=['Column 0','Column 1','Column 2','Column 3','Column 4','Column 5','Column 6','Column 7']
def rowCount(self, parent=QModelIndex()):
return len(self.items)
def columnCount(self, parent=QModelIndex()):
return len(self.headerNames)
def data(self, index, role):
if not index.isValid(): return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
row=index.row()
if row<len(self.items):
return QVariant(self.items[row])
else:
return QVariant()
class Proxy(QSortFilterProxyModel):
def __init__(self):
super(Proxy, self).__init__()
def filterAcceptsRow(self, row, parent):
return True
def headerData(self, column, orientation, role=Qt.DisplayRole):
sourceModel=self.sourceModel()
if role==Qt.TextAlignmentRole:
if orientation==Qt.Horizontal:
return QVariant(int(Qt.AlignHCenter|Qt.AlignVCenter))
return QVariant(int(Qt.AlignHCenter|Qt.AlignVCenter))
if role==Qt.DisplayRole:
if orientation==Qt.Horizontal:
return QVariant( sourceModel.headerNames[column] )
else:
return QVariant()
else:
return QVariant()
return QVariant(int(column+1))
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
tableModel=Model(self)
proxyModel=Proxy()
proxyModel.setSourceModel(tableModel)
self.tableview=QTableView(self)
self.tableview.setModel(proxyModel)
self.tableview.horizontalHeader().setStretchLastSection(True)
self.tableview.setSelectionMode(QAbstractItemView.MultiSelection)
header=self.tableview.horizontalHeader()
header.setContextMenuPolicy(Qt.CustomContextMenu)
header.connect(header, SIGNAL("customContextMenuRequested(QPoint)" ), self.headerRightClicked)
self.resHeaderMenu=QMenu(self)
for column in range(proxyModel.columnCount()):
columnName=proxyModel.headerData(column, Qt.Horizontal).toPyObject()
actn=QAction('%s'%columnName, self.resHeaderMenu, checkable=True)
actn.setChecked(True)
actn.triggered.connect(self.resHeaderMenuTriggered)
self.resHeaderMenu.addAction(actn)
layout = QVBoxLayout(self)
layout.addWidget(self.tableview)
self.setLayout(layout)
def headerRightClicked(self, QPos):
parentPosition=self.tableview.mapToGlobal(QPoint(0, 0))
menuPosition=parentPosition + QPos
self.resHeaderMenu.move(menuPosition)
self.resHeaderMenu.show()
def resHeaderMenuTriggered(self, arg):
print 'resHeaderMenuTriggered', arg
for i, actn in enumerate(self.resHeaderMenu.actions()):
if not actn.isChecked():
self.tableview.setColumnHidden(i, True)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
In case you really want to re-arrange the columns yourself:
You can move or swap sections via the moveSection and swapSections methods of the horizontalHeader.
headerView.moveSection(x, y)
will move column x such that it's column y afterwards, while
headerView.swapSections(x,y)
will - obviously - swap the positions of the two columns.
Thank you for the right-click menu implemented on header-column right-click example. Right looking for the same function as you were. Just one thing that the column should be shown when the tick is re-checked, i.e. your resHeaderMenuTriggered() should be written as:
def resHeaderMenuTriggered(self, arg):
print('resHeaderMenuTriggered', arg)
for i, actn in enumerate(self.resHeaderMenu.actions()):
if not actn.isChecked():
self.proxyView.setColumnHidden(i, True)
else:
self.proxyView.setColumnHidden(i, False)