Sorting case insensitively in QAbstractItemModel - python

I have trouble with trying create my own sorting function with QAbstractItemModel. It works but not case insensitive. I have tried to use QSortFilterProxyModel, but any success. My sort function:
def sort(self, col, order):
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.tableData = sorted(self.tableData, key=operator.itemgetter(col))
if order == Qt.AscendingOrder:
self.tableData.reverse()
self.emit(SIGNAL("layoutChanged()"))
I am using QTableView. How I could make it case insensitive?
Full example:
from PyQt4.QtCore import *
from PyQt4.QtGui import *
import operator
import sys
class Window(QWidget):
def __init__(self):
super(Window, self).__init__()
header = ["one", "two"]
tableDict = [["abcdef", "tuvwx"], ["aBcde", "TUvWx"], ["acdef","tUvWX"], ["Acdef", "TUVwx"], ["ACdef", "TUVwx"]]
self.myTable = newTableModel(header, tableDict)
mainLayout = QHBoxLayout()
mainLayout.addWidget(self.myTable.tableView)
self.setLayout(mainLayout)
self.setWindowTitle("Test table")
class newTableModel(QAbstractTableModel):
def __init__(self, header, data, parent=None, *args):
super(newTableModel, self).__init__(parent)
self.tableView = QTableView()
self.tableData = data
self.header = header
self.tableView.setShowGrid(True)
self.tableView.setFrameStyle( QFrame.NoFrame )
self.tableView.setFocusPolicy( Qt.NoFocus )
self.tableView.setSelectionMode( QAbstractItemView.NoSelection )
vHeader = self.tableView.verticalHeader()
vHeader.setVisible(False)
vHeader.setStretchLastSection(False)
hHeader = self.tableView.horizontalHeader()
hHeader.setVisible(True)
hHeader.setStretchLastSection(False)
self.tableView.setSortingEnabled(True)
self.tableView.setModel(self)
self.tableView.resizeRowsToContents()
self.tableView.resizeColumnsToContents()
vHeader.setResizeMode(QHeaderView.ResizeToContents)
self.tableView.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
self.tableView.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
def rowCount(self, parent):
return len(self.tableData)
def columnCount(self, parent):
return len(self.tableData[0])
def data(self, index, role=Qt.DisplayRole):
row = index.row()
col = index.column()
if role == Qt.DisplayRole:
return "{0}".format(self.tableData[row][col])
return None
def setData(self, index, value, role):
if index.isValid():
return True
return False
def flags(self, index):
fl = QAbstractTableModel.flags(self, index)
if index.column() == 0:
fl |= Qt.ItemIsUserCheckable
return fl
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.header[col]
def sort(self, col, order):
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.tableData = sorted(self.tableData, key=operator.itemgetter(col))
if order == Qt.AscendingOrder:
self.tableData.reverse()
self.emit(SIGNAL("layoutChanged()"))
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
This table data in example shows sorting - non case sensitive.

You simply need to convert the values to lower (or upper) case in the sort-key that is passed to the sorted function. For improved efficiency, you can also use the reverse argument to avoid doing that in a separate step:
def sort(self, col, order):
self.layoutAboutToBeChanged.emit()
self.tableData = sorted(
self.tableData,
key=lambda row: row[col].lower(),
reverse=(order != Qt.AscendingOrder),
)
self.layoutChanged.emit()
Note that sorted does a stable sort, so equal values (after the key has been applied), will keep their original places. Thus, the second column in your example won't show any changes when it's sorted, since the values are all the "same" (if you ignore case).
UPDATE:
Here's a solution that will work for strings and numbers. It assumes the columns aren't a mixture of the two types:
def sort(self, col, order):
if self.tableData and self.tableData[0]:
self.layoutAboutToBeChanged.emit()
if isinstance(self.tableData[0][col], str):
sortkey = lambda row: row[col].lower()
else:
sortkey = operator.itemgetter(col)
self.tableData = sorted(
self.tableData, key=sortkey,
reverse=(order != Qt.AscendingOrder))
self.layoutChanged.emit()

Related

Re-ordering QTableView rows that include spanned columns

I have a working drag and drop example below for reordering rows of the same column length for a qtableview using PyQt5 (with help from this StackOverflow question here). However I am looking to perform the same operation on a qtableview table where one or two rows have merged cells spanning the total number of columns (like the second row in the picture below).
How would be the best way to go about this? Should I remove the merge (clearSpans) at the point of drag/drop and then do a remerge based on the cell value (though when I tried this it did not work), or is there a way to drag/drop reorder with the cell merging intact?
Here's the code which works for row data of equal columns, but fails when a row is merged
from PyQt5.QtGui import QBrush
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex
class myModel(QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data or []
self._headers = ['Type', 'result', 'count']
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
if section < 0 or section >= len(self._headers):
return ""
else:
return self._headers[section]
return None
def data(self, index, role=None):
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter
if role == Qt.ForegroundRole:
return QBrush(Qt.black)
if role == Qt.BackgroundRole:
if (self.index(index.row(), 0).data().startswith('second')):
return QBrush(Qt.green)
else:
if (self.index(index.row(), 1).data()) == 'abc':
return QBrush(Qt.yellow)
if (self.index(index.row(), 1).data()) == 'def':
return QBrush(Qt.blue)
if (self.index(index.row(), 1).data()) == 'ghi':
return QBrush(Qt.magenta)
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemIsDropEnabled | Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
def supportedDropActions(self) -> bool:
return Qt.MoveAction | Qt.CopyAction
def relocateRow(self, row_source, row_target) -> None:
row_a, row_b = max(row_source, row_target), min(row_source, row_target)
self.beginMoveRows(QModelIndex(), row_a, row_a, QModelIndex(), row_b)
self._data.insert(row_target, self._data.pop(row_source))
self.endMoveRows()
class myTableView(QTableView):
def __init__(self, parent):
super().__init__(parent)
self.verticalHeader().hide()
self.setSelectionBehavior(self.SelectRows)
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(self.InternalMove)
self.setDragDropOverwriteMode(False)
def dropEvent(self, event):
if (event.source() is not self or
(event.dropAction() != Qt.MoveAction and
self.dragDropMode() != QAbstractItemView.InternalMove)):
super().dropEvent(event)
selection = self.selectedIndexes()
#self.clearSpans()
from_index = selection[0].row() if selection else -1
to_index = self.indexAt(event.pos()).row()
if (0 <= from_index < self.model().rowCount() and
0 <= to_index < self.model().rowCount() and
from_index != to_index):
self.model().relocateRow(from_index, to_index)
event.accept()
super().dropEvent(event)
class sample_data(QMainWindow):
def __init__(self):
super().__init__()
tv = myTableView(self)
tv.setModel(myModel([
["first", 'abc', 123],
["second"],
["third", 'def', 456],
["fourth", 'ghi', 789],
]))
self.setCentralWidget(tv)
tv.setSpan(1, 0, 1, 3)
self.show()
if __name__ == '__main__':
app = QApplication([])
test = sample_data()
raise SystemExit(app.exec_())
The sections of the vertical header can be made movable, so there's no need to implement this functionality yourself. It obviously means the vertical header will be visible, but that can be mitigated by making the sections blank, which will result in a relatively narrow header:
Note that moving sections around (rather than rows) is purely visual - the underlying model is never modified. That shouldn't really matter in practice, though, since the header provides methods to translate from logical to visual indices. And it does bring some additional benefits - for example, it's very easy to return to a previous state (i.e. by using the header's saveState and restoreState methods).
Below is a working demo based on your example. The rows can be re-ordered by dragging and dropping the section headers, or by pressing Alt+Up / Alt+Down when a row is selected. The vertical header can be toggled by pressing F6. The logical rows can be printed by pressing F7.
UPDATE:
I also added support for moving sections around by dragging and dropping the rows themselves.
from PyQt5.QtGui import QBrush
from PyQt5.QtWidgets import *
from PyQt5.QtCore import QAbstractTableModel, Qt, QModelIndex
class myModel(QAbstractTableModel):
def __init__(self, data, parent=None, *args):
super().__init__(parent, *args)
self._data = data or []
self._headers = ['Type', 'result', 'count']
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._headers)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
if section < 0 or section >= len(self._headers):
return ""
else:
return self._headers[section]
else:
return ''
return None
def data(self, index, role=None):
if role == Qt.TextAlignmentRole:
return Qt.AlignHCenter
if role == Qt.ForegroundRole:
return QBrush(Qt.black)
if role == Qt.BackgroundRole:
if (self.index(index.row(), 0).data().startswith('second')):
return QBrush(Qt.green)
else:
if (self.index(index.row(), 1).data()) == 'abc':
return QBrush(Qt.yellow)
if (self.index(index.row(), 1).data()) == 'def':
return QBrush(Qt.blue)
if (self.index(index.row(), 1).data()) == 'ghi':
return QBrush(Qt.magenta)
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemIsDropEnabled | Qt.ItemIsEnabled | Qt.ItemIsEditable | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled
def supportedDropActions(self) -> bool:
return Qt.MoveAction | Qt.CopyAction
class myTableView(QTableView):
def __init__(self, parent):
super().__init__(parent)
header = self.verticalHeader()
header.setSectionsMovable(True)
header.setSectionResizeMode(QHeaderView.Fixed)
header.setFixedWidth(10)
QShortcut('F7', self, self.getLogicalRows)
QShortcut('F6', self, self.toggleVerticalHeader)
QShortcut('Alt+Up', self, lambda: self.moveRow(True))
QShortcut('Alt+Down', self, lambda: self.moveRow(False))
self.setSelectionBehavior(self.SelectRows)
self.setSelectionMode(self.SingleSelection)
self.setDragDropMode(self.InternalMove)
self.setDragDropOverwriteMode(False)
def dropEvent(self, event):
if (event.source() is not self or
(event.dropAction() != Qt.MoveAction and
self.dragDropMode() != QAbstractItemView.InternalMove)):
super().dropEvent(event)
selection = self.selectedIndexes()
from_index = selection[0].row() if selection else -1
to_index = self.indexAt(event.pos()).row()
if (0 <= from_index < self.model().rowCount() and
0 <= to_index < self.model().rowCount() and
from_index != to_index):
header = self.verticalHeader()
from_index = header.visualIndex(from_index)
to_index = header.visualIndex(to_index)
header.moveSection(from_index, to_index)
event.accept()
super().dropEvent(event)
def toggleVerticalHeader(self):
self.verticalHeader().setHidden(self.verticalHeader().isVisible())
def moveRow(self, up=True):
selection = self.selectedIndexes()
if selection:
header = self.verticalHeader()
row = header.visualIndex(selection[0].row())
if up and row > 0:
header.moveSection(row, row - 1)
elif not up and row < header.count() - 1:
header.moveSection(row, row + 1)
def getLogicalRows(self):
header = self.verticalHeader()
for vrow in range(header.count()):
lrow = header.logicalIndex(vrow)
index = self.model().index(lrow, 0)
print(index.data())
class sample_data(QMainWindow):
def __init__(self):
super().__init__()
tv = myTableView(self)
tv.setModel(myModel([
["first", 'abc', 123],
["second"],
["third", 'def', 456],
["fourth", 'ghi', 789],
]))
self.setCentralWidget(tv)
tv.setSpan(1, 0, 1, 3)
if __name__ == '__main__':
app = QApplication(['Test'])
test = sample_data()
test.setGeometry(600, 100, 350, 185)
test.show()
app.exec_()

Sort on Multiple columns in QTableView

I'm using Pyside2, Python 3.8
I have a QTableView, with a QSortFilterProxyModel Model. I managed to sort my rows on a single column. What I want to achieve is sort myTableView on the column 3 (contains String data), then on column 2 (contains Bool data) and then on Column 4 (contains integer data). See the below picture for an example
I've been trying about different why to do this, It seems that the hack my be in the lessThan() method, but It's very confusing to me.
Can someone give me a hint on how should I proceed?
Here's some samples of my code, If it helps any one.
class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
def __init__(self, parent=None):
ProxyModel = ProxyModel()
TableModel = TableModel()
ProxyModel.setSourceModel(TableModel)
self.MyTableView.setModel(ProxyModel)
class ProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self,parent=None):
super(ProxyModel, self).__init__()
self._filter = "Aucun"
def filterAcceptsRow(self, sourceRow, sourceParent):
if self._filter == "Aucun": return True
sourceModel = self.sourceModel()
id = sourceModel.index(sourceRow, self.filterKeyColumn(), sourceParent)
if sourceModel.data(id) == self._filter:
return True
return False
def lessThan(self, left, right):
print(left.row(), ' vs ',right.row())
if left.column() == 3:
leftData = int(self.sourceModel().data(left))
rightData = int(self.sourceModel().data(right))
if left.column() == 2:
leftData = str(self.sourceModel().data(left))
rightData = str(self.sourceModel().data(right))
return leftData < rightData
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, mlist=None):
super(TableModel, self).__init__()
self._items = [] if mlist == None else mlist
self._header = []
def rowCount(self, parent = QtCore.QModelIndex):
return len(self._items)
def columnCount(self, parent = QtCore.QModelIndex):
return len(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()-1][index.column()] = value
self.dataChanged.emit(index, index)
return True
return False
def addRow(self, rowObject):
row = self.rowCount()
self.beginInsertRows(QtCore.QModelIndex(), row, row)
self._items.append(rowObject)
self.endInsertRows()
self.layoutChanged.emit()
QSortFilterProxyModel calls lessThan only once for each row pair with indexes set to index(left_row, sort_column) and index(right_row, sort_column), so implementation must take this into account. Ignore column part and access columns you interested in.
def lessThan(self, left, right):
row1 = left.row()
row2 = right.row()
model = left.model()
for col in [3,2,4]:
a = model.data(model.index(row1, col))
b = model.data(model.index(row2, col))
if a < b:
return True
elif a > b:
return False
return True

background color of cells in QTableview, PySide2

Is it possible to conditionally change the background color of items in a QTableView, using PySide2?
I've read a lot on the model view framework . I cannot figure out if it is necessary to use a Delegate or not. Recently I was able to get a column of checkboxes without a Delegate. I believe that the virtual methods setItemData(index, roles) and itemData(index) could be what I need. However, there is no QMap in PySide2. My model must need somewhere to store the extra information to be used by QtCore.Qt.BackgroundRole (that enum, btw, says "the background brush used for items rendered with the default delegate") If I don't specify a delegate, is the "default delegate" used?. Should I be using QStandardItemModel instead?
In the example code below, how would I get a particular column's background color to be red based on some thresholds (the min and max column are the thresholds?
from PySide2.QtWidgets import (QWidget, QApplication, QTableView,QVBoxLayout)
import sys
from PandasModel2 import PandasModel2
import numpy as np
import pandas as pd
class Example(QWidget):
def __init__(self):
super().__init__()
self.setGeometry(300, 300, 700, 300)
self.setWindowTitle("QTableView")
self.initData()
self.initUI()
def initData(self):
data = pd.DataFrame(np.random.randint(1,10,size=(6,4)), columns=['Test#','MIN', 'MAX','MEASURED'])
data['Test#'] = [1,2,3,4,5,6]
#add the checkable column to the DataFrame
data['Check'] = True
self.model = PandasModel2(data)
def initUI(self):
self.tv = QTableView(self)
self.tv.setModel(self.model)
vbox = QVBoxLayout()
vbox.addWidget(self.tv)
self.setLayout(vbox)
app = QApplication([])
ex = Example()
ex.show()
sys.exit(app.exec_())
And I have a custom model using a pandas dataFrame:
import PySide2.QtCore as QtCore
class PandasModel2(QtCore.QAbstractTableModel):
"""
Class to populate a table view with a pandas dataframe.
This model is non-hierachical.
"""
def __init__(self, data, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self._data = data
def rowCount(self, parent=None):
return self._data.shape[0]
def columnCount(self, parent=None):
return self._data.shape[1]
def data(self, index, role=QtCore.Qt.DisplayRole):
if role==QtCore.Qt.DisplayRole:
if index.column() != 4:
#don't want what determines check state to be shown as a string
if index.isValid():
if index.column() in [1,2,3]:
return '{:.3f}'.format(self._data.iloc[index.row(), index.column()])
if index.column() == 0:
return '{:.2f}'.format(self._data.iloc[index.row(), index.column()])
return str(self._data.iloc[index.row(), index.column()])
if role==QtCore.Qt.CheckStateRole:
if index.column()==4:#had to add this check to get the check boxes only in column 10
if self._data.iloc[index.row(), index.column()] == True:
return QtCore.Qt.Checked
else:
return QtCore.Qt.Unchecked
def getMinimum(self, row):
return self._data.iloc[row, self.getColumnNumber('MIN')]
def getMaximum(self, row):
return self._data.iloc[row, self.getColumnNumber('MAX')]
def getColumnNumber(self, string):
'''
Given a string that identifies a label/column,
return the location of that label/column.
This enables the config file columns to be moved around.
'''
return self._data.columns.get_loc(string)
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._data.columns[col]
return None
def flags(self, index):
'''
The returned enums indicate which columns are editable, selectable,
checkable, etc.
The index is a QModelIndex.
'''
if index.column() == self.getColumnNumber('Check'):
#print(index.column())
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsUserCheckable
else:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
return QtCore.Qt.ItemIsEnabled
def setData(self, index, value, role=QtCore.Qt.DisplayRole):
"""Set the value to the index position depending on Qt::ItemDataRole and data type of the column
Args:
index (QtCore.QModelIndex): Index to define column and row.
value (object): new value.
role (Qt::ItemDataRole): Use this role to specify what you want to do.
Raises:
TypeError: If the value could not be converted to a known datatype.
Returns:
True if value is changed. Calls layoutChanged after update.
False if value is not different from original value.
"""
if not index.isValid():
return False
if role == QtCore.Qt.DisplayRole: #why not edit role?
self._data.iat[index.row(),index.column()]= value
self.layoutChanged.emit()
return True
elif role == (QtCore.Qt.CheckStateRole | QtCore.Qt.DisplayRole):
#this block does get executed when toggling the check boxes,
#verified with debugger. Although the action is the same
#as the block above!
self._data.iat[index.row(),index.column()]= value
self.layoutChanged.emit()
return True
else:
return False
The delegate by default uses the BackgroundRole information if it is available so the solution is just to return a QColor, QBrush or similar.
from PySide2 import QtCore, QtGui
class PandasModel2(QtCore.QAbstractTableModel):
# ...
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return
if not (0 <= index.row() < self.rowCount() and 0 <= index.column() <= self.columnCount()):
return
value = self._data.iloc[index.row(), index.column()]
if role == QtCore.Qt.DisplayRole:
if index.column() != 4:
if index.column() in [1,2,3]:
return '{:.3f}'.format(value)
if index.column() == 0:
return '{:.2f}'.format(value)
return str(value)
elif role == QtCore.Qt.CheckStateRole:
if index.column() == 4:
return QtCore.Qt.Checked if value else QtCore.Qt.Unchecked
elif index.column() == self.getColumnNumber('MEASURED'):
if role == QtCore.Qt.BackgroundRole:
if self.getMinimum(index.row()) <= value <= self.getMaximum(index.row()):
return QtGui.QColor("red")

Find index of value in Qlistview

I'am using PySide2 and want to search a QListView for a value and have that row selected. Like you can with .findText(string_to_search_for) on a QComboBox.
How can i search for a value in a Qlistview and have the index returned?
some additional info:
The model of my QListView is implementation of QAbstractTableModel i've written.
The model is filled with data from a database, in the first column the id and 2nd column the name of the Database item. The QListView is only showing the 2nd column. This is my code for the QTableModel.
from PySide2 import QtGui,QtCore
class TwoColumnTableModel(QtCore.QAbstractTableModel):
def __init__(self, row_data=[], column_data=[], parent=None):
QtCore.QAbstractTableModel.__init__(self, parent)
self.row_data = row_data
self.column_data = column_data
def rowCount(self, parent):
return len(self.row_data)
def columnCount(self, parent):
return len(self.column_data)
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
row = index.row()
column = index.column()
value = self.row_data[row][column]
self.dataChanged.emit(row, column, [])
return value
def headerData(self, section, orientation, role):
if role == QtCore.Qt.DisplayRole:
if orientation == QtCore.Qt.Horizontal:
if section < len(self.column_data):
return self.column_data[section]
else:
return "TEMP COL"
def insertRows(self, position, rows, data=[], parent=QtCore.QModelIndex()):
self.beginInsertRows(parent, position, position + rows - 1)
for i in range(len(data)):
columns = []
row_column1 = data[i][0]
row_column2 = data[i][1]
columns.insert(0, row_column1)
columns.insert(1, row_column2)
self.row_data.insert(position, columns)
self.endInsertRows()
return True
def removeRows(self, position, rows, parent=QtCore.QModelIndex()):
self.beginRemoveRows()
for i in range(rows):
value = self.row_data[position]
self.row_data.remove(value)
self.endRemoveRows()
return True
I ended up creating the following function in QTableModel class:
def find_index_of_value(self, search_string, search_column):
for index in range(self.rowCount(None)):
model_index = self.index(index, 1)
index_value = self.data(model_index, search_column)
if index_value == search_string:
return model_index
The "search_string" being the string i'm looking for and the "search_column" being the column of the model where i want to search for that string. With the return index i can use the setCurrentIndex(index) on my QListView and that's it.

How to insert and remove row from model linked to QTableView

The removeRows() works as intended by deleting the selected row.
But there is a problem with insertRows(). By some reason the new items do not appear at the index-number selected. What causes this problem?
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_003','Item_000','Item_005','Item_004','Item_001']
self.numbers=[20,10,30,50,40]
self.added=0
def rowCount(self, parent=QModelIndex()):
return len(self.items)
def columnCount(self, parent=QModelIndex()):
return 2
def data(self, index, role):
if not index.isValid(): return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
row=index.row()
column=index.column()
if column==0:
if row<len(self.items):
return QVariant(self.items[row])
elif column==1:
if row<len(self.numbers):
return QVariant( self.numbers[row] )
else:
return QVariant()
def removeRows(self, row, rows=1, index=QModelIndex()):
print "Removing at row: %s"%row
self.beginRemoveRows(QModelIndex(), row, row + rows - 1)
self.items = self.items[:row] + self.items[row + rows:]
self.endRemoveRows()
return True
def insertRows(self, row, rows=1, index=QModelIndex()):
print "Inserting at row: %s"%row
self.beginInsertRows(QModelIndex(), row, row + rows - 1)
for row in range(rows):
self.items.insert(row + row, "New Item %s"%self.added)
self.added+=1
self.endInsertRows()
return True
class Proxy(QSortFilterProxyModel):
def __init__(self):
super(Proxy, self).__init__()
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
vLayout=QVBoxLayout(self)
self.setLayout(vLayout)
hLayout=QHBoxLayout()
vLayout.insertLayout(0, hLayout)
tableModel=Model(self)
proxyA=Proxy()
proxyA.setSourceModel(tableModel)
proxyB=Proxy()
proxyB.setSourceModel(tableModel)
self.ViewA=QTableView(self)
self.ViewA.setModel(proxyA)
self.ViewA.clicked.connect(self.viewClicked)
self.ViewA.setSortingEnabled(True)
self.ViewA.sortByColumn(0, Qt.AscendingOrder)
self.ViewB=QTableView(self)
self.ViewB.setModel(proxyB)
self.ViewB.clicked.connect(self.viewClicked)
self.ViewB.setSortingEnabled(True)
self.ViewB.sortByColumn(0, Qt.AscendingOrder)
hLayout.addWidget(self.ViewA)
hLayout.addWidget(self.ViewB)
insertButton=QPushButton('Insert Row Above Selection')
insertButton.setObjectName('insertButton')
insertButton.clicked.connect(self.buttonClicked)
removeButton=QPushButton('Remove Selected Item')
removeButton.setObjectName('removeButton')
removeButton.clicked.connect(self.buttonClicked)
vLayout.addWidget(insertButton)
vLayout.addWidget(removeButton)
def viewClicked(self, indexClicked):
print 'indexClicked() row: %s column: %s'%(indexClicked.row(), indexClicked.column() )
proxy=indexClicked.model()
def buttonClicked(self):
button=self.sender()
if not button: return
tableView=None
if self.ViewA.hasFocus(): tableView=self.ViewA
elif self.ViewB.hasFocus(): tableView=self.ViewB
if not tableView: return
indexes=tableView.selectionModel().selectedIndexes()
for index in indexes:
if not index.isValid(): continue
if button.objectName()=='removeButton':
tableView.model().removeRows(index.row(), 1, QModelIndex())
elif button.objectName()=='insertButton':
tableView.model().insertRows(index.row(), 1, QModelIndex())
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
Mistake was in insertRows() method. The incoming row argument variable (which is selected QModelIndex.row() number) was accidentally re-declared in for loop:
for row in range(rows):
I've renamed the argument row variable to postion and the fixed working code is posted below (the proxyB sorting attribute was commented out. It displays the ViewB items as unsorted. This way it is easier to see a "real" items order:
Edited later:
There was a few more tweaks made to the code. There is a lot happening "behind of scenes": before or after insertRows() and removeRows() do their job.
For example:
When we call these methods the first argument supplied must be a QModelIndex's row number. It is going to be used by the methods as a "starting point" or "starting row number" from where the indexes will be added (inserted) or removed: as many as the second integer argument says.
As we know the proxy model's modelIndexess row and column numbers do not match to the associated with them the sourceModel's modelIndexes. Interesting but there is a translation from proxy's row numbers to sourceModel's ones before the row-number-argument is even received by those two methods. It can be seen from a print out: the buttonClicked() method "sends" row 0 while the insertRows() method prints out that it received other than 0 row number (if it was supplied an modelIndex "taken" from by-Proxy-driven TableView with sorting or filtering enabled and active).
Aside from it there is some "intricate" mechanism happening on how these two methods remove or pop the data from the model's self.items variable. removeRows() method kind of "returns" back to itself to finish a job if the row numbers do not follow in a sequence.
The fully working code is posted below:
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_A000','Item_B001','Item_A002','Item_B003','Item_B004']
self.numbers=[20,10,30,50,40]
self.added=0
def rowCount(self, parent=QModelIndex()):
return len(self.items)
def columnCount(self, parent=QModelIndex()):
return 2
def data(self, index, role):
if not index.isValid(): return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
row=index.row()
column=index.column()
if column==0:
if row<len(self.items):
return QVariant(self.items[row])
elif column==1:
if row<len(self.numbers):
return QVariant( self.numbers[row] )
else:
return QVariant()
def removeRows(self, position, rows=1, index=QModelIndex()):
print "\n\t\t ...removeRows() Starting position: '%s'"%position, 'with the total rows to be deleted: ', rows
self.beginRemoveRows(QModelIndex(), position, position + rows - 1)
self.items = self.items[:position] + self.items[position + rows:]
self.endRemoveRows()
return True
def insertRows(self, position, rows=1, index=QModelIndex()):
print "\n\t\t ...insertRows() Starting position: '%s'"%position, 'with the total rows to be inserted: ', rows
indexSelected=self.index(position, 0)
itemSelected=indexSelected.data().toPyObject()
self.beginInsertRows(QModelIndex(), position, position + rows - 1)
for row in range(rows):
self.items.insert(position + row, "%s_%s"% (itemSelected, self.added))
self.added+=1
self.endInsertRows()
return True
class Proxy(QSortFilterProxyModel):
def __init__(self):
super(Proxy, self).__init__()
def filterAcceptsRow(self, rowProc, parentProc):
modelIndex=self.sourceModel().index(rowProc, 0, parentProc)
item=self.sourceModel().data(modelIndex, Qt.DisplayRole).toPyObject()
if item and 'B' in item:
return True
else: return False
class MyWindow(QWidget):
def __init__(self, *args):
QWidget.__init__(self, *args)
vLayout=QVBoxLayout(self)
self.setLayout(vLayout)
hLayout=QHBoxLayout()
vLayout.insertLayout(0, hLayout)
tableModel=Model(self)
proxyB=Proxy()
proxyB.setSourceModel(tableModel)
self.ViewA=QTableView(self)
self.ViewA.setModel(tableModel)
self.ViewA.clicked.connect(self.viewClicked)
self.ViewB=QTableView(self)
self.ViewB.setModel(proxyB)
self.ViewB.clicked.connect(self.viewClicked)
self.ViewB.setSortingEnabled(True)
self.ViewB.sortByColumn(0, Qt.AscendingOrder)
self.ViewB.setSelectionBehavior(QTableView.SelectRows)
hLayout.addWidget(self.ViewA)
hLayout.addWidget(self.ViewB)
insertButton=QPushButton('Insert Row Above Selection')
insertButton.setObjectName('insertButton')
insertButton.clicked.connect(self.buttonClicked)
removeButton=QPushButton('Remove Selected Item')
removeButton.setObjectName('removeButton')
removeButton.clicked.connect(self.buttonClicked)
vLayout.addWidget(insertButton)
vLayout.addWidget(removeButton)
def getZeroColumnSelectedIndexes(self, tableView=None):
if not tableView: return
selectedIndexes=tableView.selectedIndexes()
if not selectedIndexes: return
return [index for index in selectedIndexes if not index.column()]
def viewClicked(self, indexClicked):
print 'indexClicked() row: %s column: %s'%(indexClicked.row(), indexClicked.column() )
proxy=indexClicked.model()
def buttonClicked(self):
button=self.sender()
if not button: return
tableView=None
if self.ViewA.hasFocus(): tableView=self.ViewA
elif self.ViewB.hasFocus(): tableView=self.ViewB
if not tableView: print 'buttonClicked(): not tableView'; return
zeroColumnSelectedIndexes=self.getZeroColumnSelectedIndexes(tableView)
if not zeroColumnSelectedIndexes: print 'not zeroColumnSelectedIndexes'; return
firstZeroColumnSelectedIndex=zeroColumnSelectedIndexes[0]
if not firstZeroColumnSelectedIndex or not firstZeroColumnSelectedIndex.isValid():
print 'buttonClicked(): not firstZeroColumnSelectedIndex.isValid()'; return
startingRow=firstZeroColumnSelectedIndex.row()
print '\n buttonClicked() startingRow =', startingRow
if button.objectName()=='removeButton':
tableView.model().removeRows(startingRow, len(zeroColumnSelectedIndexes), QModelIndex())
elif button.objectName()=='insertButton':
tableView.model().insertRows(startingRow, len(zeroColumnSelectedIndexes), QModelIndex())
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())

Categories