Related
I have an application which contains a QTableView for which I would like to have the possibility to sort its contents but also to remove one or more rows. Below is an example code that implements this.
from PyQt5 import QtCore, QtWidgets
class NeXuSFilesModel(QtCore.QAbstractTableModel):
fields = ['col1','col2']
def __init__(self,parent):
super(NeXuSFilesModel,self).__init__(parent)
self._nexus_contents = [['aaa','111'],['bbb','222'],['ccc','333']]
def columnCount(self, parent=None):
return 2
def data(self,index,role):
if not index.isValid():
return QtCore.QVariant()
row = index.row()
col = index.column()
if role == QtCore.Qt.DisplayRole:
return str(self._nexus_contents[row][col])
else:
return QtCore.QVariant()
def headerData(self, index, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return NeXuSFilesModel.fields[index]
else:
return index + 1
return None
def removeRow(self, row, parent):
self.beginRemoveRows(QtCore.QModelIndex(),row,row+1)
del self._nexus_contents[row]
self.endRemoveRows()
return True
def rowCount(self, parent=None):
return len(self._nexus_contents)
class NeXuSDataTableView(QtWidgets.QTableView):
def __init__(self,parent):
super(NeXuSDataTableView,self).__init__(parent)
self.horizontalHeader().setStretchLastSection(True)
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Delete:
model = self.model()
selected_indexes = self.selectionModel().selectedRows()
source_indexes_rows = sorted([model.mapToSource(index).row() for index in selected_indexes],reverse=True)
for row in source_indexes_rows:
model.sourceModel().removeRow(row,QtCore.QModelIndex())
super(NeXuSDataTableView, self).keyPressEvent(event)
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super(MainWindow,self).__init__()
self._table = NeXuSDataTableView(self)
model = NeXuSFilesModel(self)
proxy_model = QtCore.QSortFilterProxyModel()
proxy_model.setSourceModel(model)
self._table.setModel(proxy_model)
self._table.setSortingEnabled(True)
mainLayout = QtWidgets.QVBoxLayout()
mainLayout.addWidget(self._table)
self.setLayout(mainLayout)
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication([])
_ = MainWindow()
app.exec_()
When I run that code, I fall into several problems for which I could not find the solution or could not understand the explanations given by the various sources I could find.
When the program starts, the data is showed initially in the wrong order. Indeed, it is displayed in descending order whereas I would like to display it in ascending order
When I remove one item, it removes actually two items !
Would you have any idea about what is wrong with my implementation ?
About both your questions:
proxy_model.sort(0, QtCore.Qt.AscendingOrder) after self._table.sortSortingEnabled(True) results in an ascending order:
self._table.setSortingEnabled(True)
proxy_model.sort(0, QtCore.Qt.AscendingOrder)
mainLayout = QtWidgets.QVBoxLayout()
mainLayout.addWidget(self._table)
self.setLayout(mainLayout)
self.show()
Using self.beginRemoveRows(QtCore.QModelIndex(),row,row) will remove only one row.
I'm having problem to show the Editor Widget when Delegate is applied with the Proxy situation.
-> self.table.setModel(self.proxy)
If the Delegate is applied to the View/Model structure, then there is no problem at all.
-> #self.table.setModel(self.model)
Refer to: https://www.pythonfixing.com/2021/10/fixed-adding-row-to-qtableview-with.html
See the code below:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class Delegate(QItemDelegate):
def __init__(self):
QItemDelegate.__init__(self)
self.type_items = ["1", "2", "3"]
def createEditor(self, parent, option, index):
if index.column() == 0:
comboBox = QComboBox(parent)
comboBox.addItems(self.type_items)
return comboBox
# no need to check for the other columns, as Qt automatically creates a
# QLineEdit for string values and QTimeEdit for QTime values;
return super().createEditor(parent, option, index)
class TableModel(QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def appendRowData(self, data):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self._data.append(data)
self.endInsertRows()
def data(self, index, role=Qt.DisplayRole):
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
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
return False
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._data[0])
def flags(self, index):
# allow editing of the index
return super().flags(index) | Qt.ItemIsEditable
class CustomProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._filters = dict()
#property
def filters(self):
return self._filters
def setFilter(self, expresion, column):
if expresion:
self.filters[column] = expresion
elif column in self.filters:
del self.filters[column]
self.invalidateFilter()
def filterAcceptsRow(self, source_row, source_parent):
for column, expresion in self.filters.items():
text = self.sourceModel().index(source_row, column, source_parent).data()
regex = QRegExp(
expresion, Qt.CaseInsensitive, QRegExp.RegExp
)
if regex.indexIn(text) == -1:
return False
return True
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
localWidget = QWidget()
self.table = QTableView(localWidget)
data = [["1", "Hi", QTime(2, 1)], ["2", "Hello", QTime(3, 0)]]
self.model = TableModel(data)
self.proxy = CustomProxyModel() # Customized Filter
self.proxy.setSourceModel(self.model)
#self.table.setModel(self.model) # Original code, for View/Model
self.table.setModel(self.proxy) # Revised code, for View/Proxy/Model
self.table.setItemDelegate(Delegate())
self.add_row = QPushButton("Add Row", localWidget)
self.add_row.clicked.connect(self.addRow)
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
index = self.model.index(row, column)
self.table.openPersistentEditor(index) # openPersistentEditor for createEditor
layout_v = QVBoxLayout()
layout_v.addWidget(self.table)
layout_v.addWidget(self.add_row)
localWidget.setLayout(layout_v)
self.setCentralWidget(localWidget)
self.show()
def addRow(self):
row = self.model.rowCount()
new_row_data = ["3", "Howdy", QTime(9, 0)]
self.model.appendRowData(new_row_data)
for i in range(self.model.columnCount()):
index = self.model.index(row, i)
self.table.openPersistentEditor(index) # openPersistentEditor for createEditor
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Test with View/Model, Widget Editor display.
Test with View/Proxy/Model, Widget Editor not display.
Any attempt to access the view indexes must use the view's model.
Your code doesn't work because the index you are providing belongs to another model, so the editor cannot be created because the view doesn't recognize the model of the index as its own: the view uses the proxy model, while you're trying to open an editor for the source model.
While in this case the simplest solution would be to use self.proxy.index(), the proper solution is to always refer to the view's model.
Change both self.model.index(...) to self.table.model().index(...).
Yes, thanks you very much.
Revised the code as below, now the Widget Editor display accordingly.
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
#index = self.model.index(row, column) # original code, which get the wrong index from the model
index = self.proxy.index(row, column) # revised code, get the correct index from the proxy
self.table.openPersistentEditor(index) # openPersistentEditor for createEditor
I am not finding any documentation showing which widgets QDataWidgetMapper actually works for and have not found any implementation of mapping with a QTableWidget.
It definitely works for QLineEdit's and QComboBoxes, which are input widgets, but is it possible to map to a QTableWidget?
Goal is to use QUndoStack to undo/redo text change in each widget when added to the QUndostack. I want to be able to undo/redo text changes for the items in the QTableWidget as well as the QLineEdits and QComboBoxes.
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
class CommandTextEdit(QtWidgets.QUndoCommand):
def __init__(self, window, index, oldText, newText, description):
super(CommandTextEdit,self).__init__()
self.index = index
self.window = window
self.oldText = oldText
self.newText = newText
def redo(self):
self.index.model().itemDataChanged.disconnect(self.window.itemDataChangedSlot)
self.index.model().setData(self.index, self.newText, QtCore.Qt.EditRole)
self.index.model().itemDataChanged.connect(self.window.itemDataChangedSlot)
def undo(self):
self.index.model().itemDataChanged.disconnect(self.window.itemDataChangedSlot)
self.index.model().setData(self.index, self.oldText, QtCore.Qt.EditRole)
self.index.model().itemDataChanged.connect(self.window.itemDataChangedSlot)
class Model(QtCore.QAbstractListModel):
itemDataChanged = QtCore.pyqtSignal(object,object, object, object)
def __init__(self, text = [], parent = None):
super(Model,self).__init__(parent)
self._text = text
def rowCount(self,parent=QtCore.QModelIndex()):
return len(self._text)
def data(self,index,role):
row = index.row()
if role == QtCore.Qt.EditRole:
return self._text[row]
if role == QtCore.Qt.DisplayRole:
value = self._text[row]
return self._text[row]
def model(self):
return self
def flags(self, index):
return QtCore.Qt.ItemIsEditable |QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def setData(self, index, value, role = QtCore.Qt.UserRole + 1):
if index.isValid():
if role == QtCore.Qt.EditRole:
oldValue = self.data(index,role)
self._text[index.row()] = value
self.dataChanged.emit(index, index)
if oldValue != value:
self.itemDataChanged.emit(index, oldValue, value, role)
return True
return False
class Window(QtWidgets.QMainWindow):
def __init__(self, parent = None):
super(Window,self).__init__(parent)
self.setCentralWidget(QtWidgets.QWidget(self))
self.setWindowTitle('Widget Mapping GUI')
mainlayout = QtWidgets.QVBoxLayout()
#Information for widgets
items = ["","H","N","M"]
#LineEdit1
self.labelLineEdit1 = QtWidgets.QLabel()
self.labelLineEdit1.setText('LineEdit1')
self.LineEdit1 = QtWidgets.QLineEdit()
firstBox = QtWidgets.QHBoxLayout()
firstBox.addWidget(self.labelLineEdit1)
firstBox.addWidget(self.LineEdit1)
#LineEdit2
self.labelLineEdit2 = QtWidgets.QLabel()
self.labelLineEdit2.setText('LineEdit2')
self.LineEdit2 = QtWidgets.QLineEdit()
secondBox = QtWidgets.QHBoxLayout()
secondBox.addWidget(self.labelLineEdit2)
secondBox.addWidget(self.LineEdit2)
#ComboBox1
self.labelComboBox1 = QtWidgets.QLabel()
self.labelComboBox1.setText('ComboBox1')
self.ComboBox1 = QtWidgets.QComboBox()
self.ComboBox1.addItems(items)
thirdBox = QtWidgets.QHBoxLayout()
thirdBox.addWidget(self.labelComboBox1)
thirdBox.addWidget(self.ComboBox1)
#ComboBox2
self.labelComboBox2 = QtWidgets.QLabel()
self.labelComboBox2.setText('ComboBox2')
self.ComboBox2 = QtWidgets.QComboBox()
self.ComboBox2.addItems(items)
fourthBox = QtWidgets.QHBoxLayout()
fourthBox.addWidget(self.labelComboBox2)
fourthBox.addWidget(self.ComboBox2)
#TableWidget
self.TableWidget = QtWidgets.QTableWidget(5,1)
#Set header labels
self.TableWidget.setHorizontalHeaderLabels(["Dimensions"])
self.TableWidget.setVerticalHeaderLabels(["A","B","C","D","E"])
self.TableWidget.setItem(0,1,QtWidgets.QTableWidgetItem('1'))
fifthBox = QtWidgets.QVBoxLayout()
fifthBox.addWidget(self.TableWidget)
#Add Layouts
mainlayout.addLayout(firstBox)
mainlayout.addLayout(secondBox)
mainlayout.addLayout(thirdBox)
mainlayout.addLayout(fourthBox)
mainlayout.addLayout(fifthBox)
self.centralWidget().setLayout(mainlayout)
#Model
self.mapper = None
self.model = Model(['','','','',''])
self.setModel(self.model)
#QUndoStack
self.undoStack = QtWidgets.QUndoStack()
self.stackView = QtWidgets.QUndoView(self.undoStack)
self.stackView.setWindowTitle('StackView')
self.stackView.show()
#Run init methods
self.createActions()
self.makeConnections()
self.listViewMethod()
def createActions(self):
self.UndoAct = QtWidgets.QAction("Undo", self)
self.UndoAct.setShortcut(QtGui.QKeySequence.Undo)
self.UndoAct.triggered.connect(self.undoStack.undo)
self.RedoAct = QtWidgets.QAction("Redo", self)
self.RedoAct.setShortcut(QtGui.QKeySequence.Redo)
self.RedoAct.triggered.connect(self.undoStack.redo)
self.toolbar = self.addToolBar('Exit')
self.toolbar.addAction(self.UndoAct)
self.toolbar.addAction(self.RedoAct)
def listViewMethod(self):
self.listView = QtWidgets.QListView()
self.listView.setModel(self.model)
self.listView.setWindowTitle('ListView')
self.listView.show()
def makeConnections(self):
self.model.itemDataChanged.connect(self.itemDataChangedSlot)
def setModel(self, model):
self.mapper = QtWidgets.QDataWidgetMapper(self)
self.mapper.setOrientation(QtCore.Qt.Vertical)
self.mapper.setModel(self.model)
self.mapper.addMapping(self.LineEdit1, 0)
self.mapper.addMapping(self.LineEdit2, 1)
self.mapper.addMapping(self.ComboBox1, 2)
self.mapper.addMapping(self.ComboBox2, 3)
self.mapper.addMapping(self.TableWidget,4)
self.mapper.toFirst()
def itemDataChangedSlot(self, index, oldValue, value, role):
if role == QtCore.Qt.EditRole:
command = CommandTextEdit(self, index, oldValue, value, "Text changed from '{0}' to '{1}'".format(oldValue, value))
self.undoStack.push(command)
def main():
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
To map the items of a QTableWidget, I used the setCellWidget method to map each cell to the model using a custom QLineEdit class.
Updated Code:
from PyQt5 import QtCore, QtGui, QtWidgets
import sys
#implemented stylesheets for QLineEdit to imitate a QTableWidgetItem
class CustomLineEdit(QtWidgets.QLineEdit):
def __init__(self):
super(CustomLineEdit,self).__init__()
self.setReadOnly(True)
self.setFrame(False)
self.setStyleSheet("QLineEdit { border: none; } \n" \
"QLineEdit::focus {background-color: #3daee9;} \n"\
"QLineEdit::focus::pressed {background-color: none;} \n"\
"QLineEdit::hover {background-color: #3daee9;}"
)
def mouseDoubleClickEvent(self,event):
self.setReadOnly(False)
self.setStyleSheet("QLineEdit { border: none; \nbackground-color: none;} \n" \
)
def focusOutEvent(self,event):
self.setReadOnly(True)
self.setStyleSheet("QLineEdit { border: none; } \n" \
"QLineEdit::focus {background-color: #3daee9;} \n"\
"QLineEdit::focus::pressed {background-color: none;} \n"\
"QLineEdit::hover {background-color: #3daee9;}"
)
class CommandTextEdit(QtWidgets.QUndoCommand):
def __init__(self, window, index, oldText, newText, description):
super(CommandTextEdit,self).__init__()
self.index = index
self.window = window
self.oldText = oldText
self.newText = newText
def redo(self):
self.index.model().itemDataChanged.disconnect(self.window.itemDataChangedSlot)
self.index.model().setData(self.index, self.newText, QtCore.Qt.EditRole)
self.index.model().itemDataChanged.connect(self.window.itemDataChangedSlot)
def undo(self):
self.index.model().itemDataChanged.disconnect(self.window.itemDataChangedSlot)
self.index.model().setData(self.index, self.oldText, QtCore.Qt.EditRole)
self.index.model().itemDataChanged.connect(self.window.itemDataChangedSlot)
class Model(QtCore.QAbstractListModel):
itemDataChanged = QtCore.pyqtSignal(object,object, object, object)
def __init__(self, text = [], parent = None):
super(Model,self).__init__(parent)
self._text = text
def rowCount(self,parent=QtCore.QModelIndex()):
return len(self._text)
def data(self,index,role):
row = index.row()
if role == QtCore.Qt.EditRole:
return self._text[row]
if role == QtCore.Qt.DisplayRole:
value = self._text[row]
return self._text[row]
def model(self):
return self
def flags(self, index):
return QtCore.Qt.ItemIsEditable |QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def setData(self, index, value, role = QtCore.Qt.UserRole + 1):
if index.isValid():
if role == QtCore.Qt.EditRole:
oldValue = self.data(index,role)
self._text[index.row()] = value
self.dataChanged.emit(index, index)
if oldValue != value:
self.itemDataChanged.emit(index, oldValue, value, role)
return True
return False
class Window(QtWidgets.QMainWindow):
def __init__(self, parent = None):
super(Window,self).__init__(parent)
self.setCentralWidget(QtWidgets.QWidget(self))
self.setWindowTitle('Widget Mapping GUI')
mainlayout = QtWidgets.QVBoxLayout()
#Information for widgets
items = ["","H","N","M"]
#LineEdit1
self.labelLineEdit1 = QtWidgets.QLabel()
self.labelLineEdit1.setText('LineEdit1')
self.LineEdit1 = QtWidgets.QLineEdit()
firstBox = QtWidgets.QHBoxLayout()
firstBox.addWidget(self.labelLineEdit1)
firstBox.addWidget(self.LineEdit1)
#LineEdit2
self.labelLineEdit2 = QtWidgets.QLabel()
self.labelLineEdit2.setText('LineEdit2')
self.LineEdit2 = QtWidgets.QLineEdit()
secondBox = QtWidgets.QHBoxLayout()
secondBox.addWidget(self.labelLineEdit2)
secondBox.addWidget(self.LineEdit2)
#ComboBox1
self.labelComboBox1 = QtWidgets.QLabel()
self.labelComboBox1.setText('ComboBox1')
self.ComboBox1 = QtWidgets.QComboBox()
self.ComboBox1.addItems(items)
thirdBox = QtWidgets.QHBoxLayout()
thirdBox.addWidget(self.labelComboBox1)
thirdBox.addWidget(self.ComboBox1)
#ComboBox2
self.labelComboBox2 = QtWidgets.QLabel()
self.labelComboBox2.setText('ComboBox2')
self.ComboBox2 = QtWidgets.QComboBox()
self.ComboBox2.addItems(items)
fourthBox = QtWidgets.QHBoxLayout()
fourthBox.addWidget(self.labelComboBox2)
fourthBox.addWidget(self.ComboBox2)
#TableWidget
self.TableWidget = QtWidgets.QTableWidget(5,1)
#Set header labels
self.TableWidget.setHorizontalHeaderLabels(["Dimensions"])
self.TableWidget.setVerticalHeaderLabels(["A","B","C","D","E"])
self.TableWidget.setCellWidget(0,0,CustomLineEdit())
self.TableWidget.setCellWidget(1,0,CustomLineEdit())
self.TableWidget.setCellWidget(2,0,CustomLineEdit())
self.TableWidget.setCellWidget(3,0,CustomLineEdit())
self.TableWidget.setCellWidget(4,0,CustomLineEdit())
self.TableWidget.setItem(1,0,QtWidgets.QTableWidgetItem('1'))
fifthBox = QtWidgets.QVBoxLayout()
fifthBox.addWidget(self.TableWidget)
#Add Layouts
mainlayout.addLayout(firstBox)
mainlayout.addLayout(secondBox)
mainlayout.addLayout(thirdBox)
mainlayout.addLayout(fourthBox)
mainlayout.addLayout(fifthBox)
self.centralWidget().setLayout(mainlayout)
#Model
self.mapper = None
self.model = Model(['' for i in range(9)])
self.setModel(self.model)
#QUndoStack
self.undoStack = QtWidgets.QUndoStack()
self.stackView = QtWidgets.QUndoView(self.undoStack)
self.stackView.setWindowTitle('StackView')
self.stackView.show()
#Run init methods
self.createActions()
self.makeConnections()
self.listViewMethod()
def createActions(self):
self.UndoAct = QtWidgets.QAction("Undo", self)
self.UndoAct.setShortcut(QtGui.QKeySequence.Undo)
self.UndoAct.triggered.connect(self.undoStack.undo)
self.RedoAct = QtWidgets.QAction("Redo", self)
self.RedoAct.setShortcut(QtGui.QKeySequence.Redo)
self.RedoAct.triggered.connect(self.undoStack.redo)
self.toolbar = self.addToolBar('Exit')
self.toolbar.addAction(self.UndoAct)
self.toolbar.addAction(self.RedoAct)
def listViewMethod(self):
self.listView = QtWidgets.QListView()
self.listView.setModel(self.model)
self.listView.setWindowTitle('ListView')
self.listView.show()
def makeConnections(self):
self.model.itemDataChanged.connect(self.itemDataChangedSlot)
def setModel(self, model):
self.mapper = QtWidgets.QDataWidgetMapper(self)
self.mapper.setOrientation(QtCore.Qt.Vertical)
self.mapper.setModel(self.model)
self.mapper.addMapping(self.LineEdit1, 0)
self.mapper.addMapping(self.LineEdit2, 1)
self.mapper.addMapping(self.ComboBox1, 2)
self.mapper.addMapping(self.ComboBox2, 3)
self.mapper.addMapping(self.TableWidget.cellWidget(0,0), 4)
self.mapper.addMapping(self.TableWidget.cellWidget(1,0), 5)
self.mapper.addMapping(self.TableWidget.cellWidget(2,0), 6)
self.mapper.addMapping(self.TableWidget.cellWidget(3,0), 7)
self.mapper.addMapping(self.TableWidget.cellWidget(4,0), 8)
self.mapper.toFirst()
def itemDataChangedSlot(self, index, oldValue, value, role):
if role == QtCore.Qt.EditRole:
command = CommandTextEdit(self, index, oldValue, value, "Text changed from '{0}' to '{1}'".format(oldValue, value))
self.undoStack.push(command)
def main():
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
My database application uses the Pandas library. I can display the excel file into my tableView but anytime I remove data from the mainframe and try to refresh the tableView. It gives me a keyError.
I'm trying to get it to display the refreshed table. I am attempting to drop the row that a user asks for. It works when it drops because I outputted the information but the tableView itself won't refresh and gives an error.
df = pd.read_excel("filename")
model = PandasModel(df)
self.tableView.setModel(model)
self.tableView.resizeColumnsToContents()
def DeletePlayer(self):
global df
choose = self.removePlayerEdit.text()
if(choose == '0'):
df = df.drop([0])
print("Player deleted")
print(df)
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, df = pd.DataFrame(), parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self._df = df
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if orientation == QtCore.Qt.Horizontal:
try:
return self._df.columns.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
elif orientation == QtCore.Qt.Vertical:
try:
# return self.df.index.tolist()
return self._df.index.tolist()[section]
except (IndexError, ):
return QtCore.QVariant()
def data(self, index, role=QtCore.Qt.DisplayRole):
if role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
if not index.isValid():
return QtCore.QVariant()
return QtCore.QVariant(str(self._df.ix[index.row(), index.column()]))
def setData(self, index, value, role):
row = self._df.index[index.row()]
col = self._df.columns[index.column()]
if hasattr(value, 'toPyObject'):
# PyQt4 gets a QVariant
value = value.toPyObject()
else:
# PySide gets an unicode
dtype = self._df[col].dtype
if dtype != object:
value = None if value == '' else dtype.type(value)
self._df.set_value(row, col, value)
return True
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self._df.index)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self._df.columns)
def sort(self, column, order):
colname = self._df.columns.tolist()[column]
self.layoutAboutToBeChanged.emit()
self._df.sort_values(colname, ascending= order == QtCore.Qt.AscendingOrder, inplace=True)
self._df.reset_index(inplace=True, drop=True)
self.layoutChanged.emit()
When implementing a model, you should not access the element that stores the data (dataframe) directly, because if you modify it, the model will not know what is going to generate problems, instead you should create methods that modify the internal data but use the methods as beginRemoveRows and endRemoveColumns that will notify the model of the change.
def removeColumn(self, col):
if 0 <= col < self.columnCount():
self.beginRemoveRows(QtCore.QModelIndex(), col, col)
self._df.drop(
self._df.columns[[col]], axis=1, inplace=True
)
self._df.reset_index(inplace=True, drop=True)
self.endRemoveColumns()
I have improved my initial model to the following:
from PyQt5 import QtCore, QtGui, QtWidgets
import pandas as pd
import numpy as np
class FloatDelegate(QtWidgets.QStyledItemDelegate):
#property
def decimals(self):
if not hasattr(self, "_decimals"):
self._decimals = 2
return self._decimals
#decimals.setter
def decimals(self, decimals):
self._decimals = decimals
def createEditor(self, parent, option, index):
DBL_MAX = 1.7976931348623157e308
editor = QtWidgets.QDoubleSpinBox(
parent, minimum=-DBL_MAX, maximum=DBL_MAX, decimals=self.decimals
)
return editor
def setEditorData(self, editor, index):
editor.setValue(index.data())
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), QtCore.Qt.DisplayRole)
def displayText(self, value, locale):
return "{}".format(value)
class DataFrameModel(QtCore.QAbstractTableModel):
DtypeRole = QtCore.Qt.UserRole + 1000
ValueRole = QtCore.Qt.UserRole + 1001
def __init__(self, df=pd.DataFrame(), parent=None):
super(DataFrameModel, self).__init__(parent)
self._dataframe = df
def setDataFrame(self, dataframe):
self.beginResetModel()
self._dataframe = dataframe.copy()
self.endResetModel()
def dataFrame(self):
return self._dataframe
dataFrame = QtCore.pyqtProperty(
pd.DataFrame, fget=dataFrame, fset=setDataFrame
)
#QtCore.pyqtSlot(int, QtCore.Qt.Orientation, result=str)
def headerData(
self,
section: int,
orientation: QtCore.Qt.Orientation,
role: int = QtCore.Qt.DisplayRole,
):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
return self._dataframe.columns[section]
else:
return str(self._dataframe.index[section])
return QtCore.QVariant()
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return len(self._dataframe.index)
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return 0
return self._dataframe.columns.size
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid() or not (
0 <= index.row() < self.rowCount()
and 0 <= index.column() < self.columnCount()
):
return QtCore.QVariant()
row = self._dataframe.index[index.row()]
col = self._dataframe.columns[index.column()]
dt = self._dataframe[col].dtype
val = self._dataframe.iloc[row][col]
if role == QtCore.Qt.DisplayRole:
return val
elif role == DataFrameModel.ValueRole:
return val
if role == DataFrameModel.DtypeRole:
return dt
return QtCore.QVariant()
def setData(self, index, value, role):
row = self._dataframe.index[index.row()]
col = self._dataframe.columns[index.column()]
if hasattr(value, "toPyObject"):
# PyQt4 gets a QVariant
value = value.toPyObject()
else:
# PySide gets an unicode
dtype = self._dataframe[col].dtype
if dtype != object:
value = None if value == "" else dtype.type(value)
self._dataframe.at[row, col] = value
return True
def flags(self, index):
flags = (
QtCore.Qt.ItemIsSelectable
| QtCore.Qt.ItemIsDragEnabled
| QtCore.Qt.ItemIsEditable
| QtCore.Qt.ItemIsEnabled
)
return flags
def roleNames(self):
roles = {
QtCore.Qt.DisplayRole: b"display",
DataFrameModel.DtypeRole: b"dtype",
DataFrameModel.ValueRole: b"value",
}
return roles
def removeRow(self, row):
if 0 <= row < self.rowCount():
self.beginRemoveRows(QtCore.QModelIndex(), row, row)
self._dataframe.drop([row], inplace=True)
self._dataframe.reset_index(inplace=True, drop=True)
self.endRemoveRows()
def removeColumn(self, col):
if 0 <= col < self.columnCount():
self.beginRemoveRows(QtCore.QModelIndex(), col, col)
self._dataframe.drop(
self._dataframe.columns[[col]], axis=1, inplace=True
)
self._dataframe.reset_index(inplace=True, drop=True)
self.endRemoveColumns()
def sort(self, column, order):
colname = self._dataframe.columns[column]
self.layoutAboutToBeChanged.emit()
self._dataframe.sort_values(
colname, ascending=order == QtCore.Qt.AscendingOrder, inplace=True
)
self._dataframe.reset_index(inplace=True, drop=True)
self.layoutChanged.emit()
class Widget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Widget, self).__init__(parent)
tableview = QtWidgets.QTableView()
tableview.setSortingEnabled(True)
delegate = FloatDelegate(tableview)
tableview.setItemDelegate(delegate)
delegate.decimals = 4
self.spinbox_row = QtWidgets.QSpinBox()
self.button_row = QtWidgets.QPushButton(
"Delete Row", clicked=self.remove_row
)
self.spinbox_col = QtWidgets.QSpinBox()
self.button_col = QtWidgets.QPushButton(
"Delete Column", clicked=self.remove_col
)
df = pd.DataFrame(
np.random.uniform(0, 100, size=(100, 4)), columns=list("ABCD")
)
self._model = DataFrameModel(df)
tableview.setModel(self._model)
grid = QtWidgets.QGridLayout(self)
grid.addWidget(tableview, 0, 0, 1, 4)
grid.addWidget(self.spinbox_row, 1, 0)
grid.addWidget(self.button_row, 1, 1)
grid.addWidget(self.spinbox_col, 1, 2)
grid.addWidget(self.button_col, 1, 3)
self.on_rowChanged()
self.on_columnChanged()
self._model.rowsInserted.connect(self.on_rowChanged)
self._model.rowsRemoved.connect(self.on_rowChanged)
self._model.columnsInserted.connect(self.on_columnChanged)
self._model.columnsRemoved.connect(self.on_columnChanged)
#QtCore.pyqtSlot()
def on_rowChanged(self):
self.spinbox_row.setMaximum(self._model.rowCount() - 1)
#QtCore.pyqtSlot()
def on_columnChanged(self):
self.spinbox_col.setMaximum(self._model.columnCount() - 1)
#QtCore.pyqtSlot()
def remove_row(self):
row = self.spinbox_row.value()
self._model.removeRow(row)
#QtCore.pyqtSlot()
def remove_col(self):
col = self.spinbox_col.value()
self._model.removeColumn(col)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Widget()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
I'm trying to implement a TreeView using QAbstractItemModel. I want to set up an empty model and then add an item to the model using a button.
This appears to be a lot more complex than I'd realised! I've modified a custom instance of the QAbstractItemModel example found in the Lib\site-packages\PyQt4\examples\itemviews\simpletreemodel\simpletreemodel.pyw.
Rather than setting up the model all at once, I want o build it incrementally.
When I click the button, I get the message showing that the process method is executing, but the model isn't updated.
Any pointers as to why it's not working would be greatly appreciated!
from PyQt4 import QtGui, QtCore
import sys
class myWindow(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.resize(500, 400)
#Tree Widget
self.treeView = QtGui.QTreeView(self)
#Model
self.model = TreeModel()
self.treeView.setModel(self.model)
# Button
self.button = QtGui.QPushButton(self)
self.button.setText('Add Item')
self.button.clicked.connect(self.process)
# Add to Layout
self.gridLayout = QtGui.QGridLayout(self)
self.gridLayout.addWidget(self.button,0,0)
self.gridLayout.addWidget(self.treeView,1,0)
def process(self):
print "here"
newItem = TreeItem(["bob","bob","bob"],self.model.rootItem)
self.model.rootItem.appendChild(newItem)
class TreeItem(object):
def __init__(self, data, parent=None):
self.parentItem = parent
self.itemData = data
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def columnCount(self):
return len(self.itemData)
def data(self, column):
try:
return self.itemData[column]
except IndexError:
return None
def parent(self):
return self.parentItem
def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, parent=None):
super(TreeModel, self).__init__(parent)
self.rootItem = TreeItem(("Model", "Status","Location"))
def columnCount(self, parent):
if parent.isValid():
return parent.internalPointer().columnCount()
else:
return self.rootItem.columnCount()
def data(self, index, role):
if not index.isValid():
return None
if role != QtCore.Qt.DisplayRole:
return None
item = index.internalPointer()
return item.data(index.column())
def flags(self, index):
if not index.isValid():
return QtCore.Qt.NoItemFlags
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.rootItem.data(section)
return None
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return parentItem.childCount()
if __name__ == "__main__":
app = QtGui.QApplication.instance() # checks if QApplication already exists
if not app: # create QApplication if it doesnt exist
app = QtGui.QApplication(sys.argv)
# Window
window = myWindow()
window.show()
sys.exit(app.exec_())
Using a QStandardItemModel has worked a treat!
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class Window(QWidget):
def __init__(self):
QWidget.__init__(self)
self.resize(500, 400)
#Tree Widget
self.treeView = QTreeView()
#Model
self.model = QStandardItemModel()
self.treeView.setModel(self.model)
# Button
self.button = QPushButton()
self.button.setText('Add Item')
self.button.clicked.connect(self.addChildClick)
# Add to Layout
self.gridLayout = QGridLayout(self)
self.gridLayout.addWidget(self.button,0,0)
self.gridLayout.addWidget(self.treeView,1,0)
def addChildClick(self):
selection = self.treeView.selectedIndexes()
text = "bob"
item = QStandardItem(text)
# if nothing selected parent is model
if selection == []:
parent = self.model
else: # Otherwise parent is what is selected
s = selection[0] # Handling multiple selectons
parent = self.model.itemFromIndex(s)
parent.appendRow(item)
#cleanup
self.treeView.expandAll()
self.treeView.clearSelection()
if __name__ == "__main__":
app = QApplication.instance() # checks if QApplication already exists
if not app: # create QApplication if it doesnt exist
app = QApplication(sys.argv)
# Window
window = Window()
window.show()
app.exec_()