I am trying to update my model when a combo box is changed. I have a function connected to the combobox that updates the model but from this question - "the delegates should use the base functions of the model whenever they provide standard behavior (accessing index.model()._data is not good)". I also want the change of the combobox to affect what appears in a QLineEdit.
QItemDelegate:
class Delegate(QItemDelegate):
def __init__(self):
QItemDelegate.__init__(self)
self.type_items = ["1", "2"]
def createEditor(self, parent, option, index):
if index.column() == 0:
comboBox = QComboBox(parent)
for text in self.type_items:
comboBox.addItem(text, (index.row(), index.column()))
comboBox.currentIndexChanged.connect(lambda: self.comboBoxChanged(comboBox, index))
return comboBox
return super().createEditor(parent, option, index)
# updates model but does not update view - shows what I want to happen
def comboBoxChanged(self, comboBox, index):
if comboBox.currentText() == "1":
index.model()._data[index.row()][0] = "1"
index.model()._data[index.row()][1] = "One"
elif comboBox.currentText() == "2":
index.model()._data[index.row()][0] = "2"
index.model()._data[index.row()][1] = "Two"
# self.dataChanged.emit(index, index) # can't call this here
QAbstractTableModel:
class TableModel(QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
if role in (Qt.DisplayRole, Qt.EditRole):
return self._data[index.row()][index.column()]
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._data[0])
def flags(self, index):
return super().flags(index) | Qt.ItemIsEditable
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
Main:
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
localWidget = QWidget()
self.table = QTableView()
data = [["1", "One"]]
self.model = TableModel(data)
self.table.setModel(self.model)
self.table.setItemDelegate(Delegate())
self.print_data = QPushButton("Print data")
self.print_data.clicked.connect(self.printData)
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
index = self.model.index(row, column)
self.table.openPersistentEditor(index)
layout_v = QVBoxLayout()
layout_v.addWidget(self.table)
layout_v.addWidget(self.print_data)
localWidget.setLayout(layout_v)
self.setCentralWidget(localWidget)
self.show()
def printData(self):
print(self.model._data)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
How can I register the change in the qcombobox to correctly update the model?
and
How do I make this trigger the change in qlineedit?
The data of a model should be preferably updated by using the existing setData function, which provides a centralized interface that avoids confusing bugs and mistakes.
In your case, you need to set the data of the sibling index, which is the index at the same row in the next column (no need to set the index value itself, as Qt is able to get it automatically from the combo, and it should be done in setModelData() anyway).
def comboBoxChanged(self, comboBox, index):
if comboBox.currentText() == "1":
value = "One"
elif comboBox.currentText() == "2":
value = "Two"
sibling = index.sibling(index.row(), 1)
index.model().setData(sibling, value)
Just to be clear, your code could have worked, but dataChanged is a signal of the model, not the delegate, so it should have been:
index.model().dataChanged.emit(index, index)
But, as already said, it's better to avoid using different ways of changing data in the model, especially if done from outside it. The major benefit of having a single function that takes care of that is that whenever something is going to change in the structure and implementation of the model the only thing left to check is all the occurrences of that function call (in case the column order changes, for example).
Related
I am trying to add a row to QTableView with a QAbstractTableModel and QItemDelegate where the widgets appear in the added row. From what I've read I need to call .edit(index) on each item of the added row to call createEditor where the widgets are created however I am getting edit: editing failed
QItemDelegate:
class Delegate(QItemDelegate):
def __init__(self):
QItemDelegate.__init__(self)
self.type_items = ["1", "2", "3"]
def createEditor(self, parent, option, index):
# COMBOBOX, LINEEDIT, TIMEDIT
if index.column() == 0:
comboBox = QComboBox(parent)
for text in self.type_items:
comboBox.addItem(text, (index.row(), index.column()))
return comboBox
elif index.column() == 1:
lineEdit = QLineEdit(parent)
return lineEdit
elif index.column() == 2:
timeEdit = QTimeEdit(parent)
return timeEdit
def setEditorData(self, editor, index):
value = index.model()._data[index.row()][index.column()]
if index.column() == 0:
editor.setCurrentIndex(self.type_items.index(value))
elif index.column() == 1:
editor.setText(str(value))
elif index.column() == 2:
editor.setTime(value)
QAbstractTableModel:
class TableModel(QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def data(self, index, role):
pass
def rowCount(self, index=None):
return len(self._data)
def columnCount(self, index=None):
return len(self._data[0])
Main:
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.table.setModel(self.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)
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):
new_row_data = ["3", "Howdy", QTime(9, 0)]
self.model.beginInsertRows(QModelIndex(), self.model.rowCount(), self.model.rowCount())
self.model._data.append(new_row_data)
self.model.endInsertRows()
for i in range(len(new_row_data)):
index = self.table.model().index(self.model.rowCount()-1, i)
self.table.edit(index)
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
How can I trigger the QItemDelegate's createEditor to create the widgets for the added row's items and setEditorData to populate them?
First of all, you could use openPersistentEditor as you already did in the __init__.
There are two reasons for the error in self.table.edit():
in order to allow editing of an item through edit(), the flags() function must be overridden and provide the Qt.ItemIsEditable flag;
you cannot call edit() multiple times on different indexes within the same function;
Then, there are other important problems in your code:
you're not correctly using the model, as data() is not implemented;
the delegates should use the base functions of the model whenever they provide standard behavior (accessing index.model()._data is not good);
setEditorData is unnecessary, as long as the above aspects are respected: Qt automatically sets the data based on the data type and the editor class;
setData() should be implemented in order to correctly set the data on the model, otherwise the new data won't be accessible;
changes in the model structure (like creating a new row) should be done in the model class;
Here is a revised version of your code:
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)
for text in self.type_items:
comboBox.addItem(text, (index.row(), index.column()))
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)
# no setEditorData() required
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 MainWindow(QMainWindow):
# ...
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)
I am trying to make everything shown by the current code un-editable.
Previous searches all suggest either modifying the flags() function of the model or using the setEditTriggers of the table. I do both in this code, but neither of them work.
Looking at a widget-by-widget case, I can find readonly modes for LineEdit and others, but not for ComboBox. So I can not even modify the delegate to force the readonly constraint, not that I would necessarily like to do it this way.
EDIT: to clarify, when I say I want the user to not be able to 'edit' I mean that he shouldn't be able to change the state of the widget in any way. E.g. he won't be able to click on a ComboBox (or at least changing the current selected item/index).
from PyQt5 import QtCore, QtWidgets
import sys
class MyWindow(QtWidgets.QWidget):
def __init__(self, *args):
super().__init__(*args)
tableview = TableView()
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(tableview)
self.setLayout(layout)
class Delegate(QtWidgets.QStyledItemDelegate):
def __init__(self, model):
super().__init__()
self.model = model
def createEditor(self, parent, option, index):
widget = QtWidgets.QComboBox(parent)
widget.addItems(['', 'Cat', 'Dog'])
return widget
def setModelData(self, widget, model, index):
self.model.setData(index, widget.currentIndex())
class Model(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self.value = 0
def flags(self, index):
return QtCore.Qt.ItemIsEnabled
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid() or role != QtCore.Qt.DisplayRole:
return QtCore.QVariant()
return QtCore.QVariant(self.value)
def setData(self, index, value, role=QtCore.Qt.EditRole):
self.value = value
print("data[{}][{}] = {}".format(index.row(), index.column(), value))
return True
def rowCount(self, parent=QtCore.QModelIndex()):
return 1
def columnCount(self, parent=QtCore.QModelIndex()):
return 1
class TableView(QtWidgets.QTableView):
def __init__(self, parent=None):
super().__init__(parent)
self.model = Model(self)
delegate = Delegate(self.model)
self.setItemDelegate(delegate)
self.setModel(self.model)
self.setEditTriggers(QtWidgets.QTableWidget.NoEditTriggers)
for row in range(self.model.rowCount()):
for column in range(self.model.columnCount()):
index = self.model.index(row, column)
self.openPersistentEditor(index)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
Explanation:
Some concepts must be clarified:
For Qt to disable editing that a view (QListView, QTableView, QTreeView, etc.) or item of the view implies only that the editor will not open through user events such as clicked, double-clicked, etc.
The user interaction in Qt follows the following path:
The user interacts through the OS with the mouse, keyboard, etc.
the OS notifies Qt of that interaction.
Qt creates QEvents and sends it to the widgets.
The widget analyzes what you should modify regarding the QEvent you receive.
In your case, using openPersistentEditor() shows the widgets, and so the edibility from the Qt point of view is not valid for this case.
Solution:
Considering the above a possible general methodology to make a widget not editable: block some point of the user-widget interaction path. In this case, the simplest thing is to prevent the widget from receiving the QEvents through an event filter.
Considering the above, the solution is:
class DisableEventsManager(QtCore.QObject):
def __init__(self, *, qobject, events=None, apply_childrens=False):
if not isinstance(qobject, QtCore.QObject):
raise TypeError(
f"{qobject} must belong to a class that inherits from QObject"
)
super().__init__(qobject)
self._qobject = qobject
self._events = events or []
self._qobject.installEventFilter(self)
self._children_filter = []
if apply_childrens:
for child in self._qobject.findChildren(QtWidgets.QWidget):
child_filter = DisableEventsManager(
qobject=child, events=events, apply_childrens=apply_childrens
)
self._children_filter.append(child_filter)
#property
def events(self):
return self._events
#events.setter
def events(self, events):
self._events = events
for child_filter in self._children_filter:
child_filter.events = events
def eventFilter(self, obj, event):
if self.events and self._qobject is obj:
if event.type() in self.events:
return True
return super().eventFilter(obj, event)
def createEditor(self, parent, option, index):
combo = QtWidgets.QComboBox(parent)
combo.addItems(["", "Cat", "Dog"])
combo_event_filter = DisableEventsManager(qobject=combo)
combo_event_filter.events = [
QtCore.QEvent.KeyPress,
QtCore.QEvent.FocusIn,
QtCore.QEvent.MouseButtonPress,
QtCore.QEvent.MouseButtonDblClick,
]
return combo
The QTableView was assigned QAbstractTableModel as a model. And ItemDelegate(QItemDelegate) was assigned with tableView.openPersistentEditor. Now when tableView is clicked the event does not propagate all the way to the tableView (does it get blocked by a delegated QLineEditor).
What would be a way to pass the QLineEdit'smousePressEventevent that is passed through theItemDelegatetoQTableView's modelwhen the tableView's item is clicked even while it is occupied byQItemInstance? Is there anyQItemDelegate` flags that can be used to get things done?
The code creates a single tableView with model and delegate.
The event triggered by clicking the column 0 or column 1 does not propagate to the tableView's item since the item stays deselected.
Uncommenting a line with tableView.setSelectionBehavior(QtGui.QTableView.SelectRows) fixes the problem. Does it?
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
class LineEdit(QtGui.QLineEdit):
def __init__(self, parent=None, total=20):
super(LineEdit, self).__init__(parent=parent)
def mousePressEvent(self, event):
print 'mousePressEvent', event
super(LineEdit, self).mousePressEvent(event)
class Delegate(QtGui.QItemDelegate):
def __init__(self):
QtGui.QItemDelegate.__init__(self)
def createEditor(self, parent, option, index):
if index.column()==0:
lineedit=LineEdit(parent)
return lineedit
elif index.column()==1:
combo=QtGui.QComboBox(parent)
return combo
def setEditorData(self, editor, index):
row = index.row()
column = index.column()
value = index.model().items[row][column]
if isinstance(editor, QtGui.QComboBox):
editor.addItems(['Somewhere','Over','The Rainbow'])
editor.setCurrentIndex(index.row())
if isinstance(editor, QtGui.QLineEdit):
editor.setText('Somewhere over the rainbow')
class Model(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items = [[1, 'one', 'ONE'], [2, 'two', 'TWO'], [3, 'three', 'THREE']]
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable
def rowCount(self, parent=QtCore.QModelIndex()):
return 3
def columnCount(self, parent=QtCore.QModelIndex()):
return 3
def data(self, index, role):
if not index.isValid(): return
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.items[row][column]
def tableViewClicked(index):
print 'clicked: %s'%index
tableModel=Model()
tableView=QtGui.QTableView()
tableView.setModel(tableModel)
tableView.setItemDelegate(Delegate())
# tableView.setSelectionBehavior(QtGui.QTableView.SelectRows)
tableView.clicked.connect(tableViewClicked)
for row in range(tableModel.rowCount()):
for column in range(tableModel.columnCount()):
index=tableModel.index(row, column)
tableView.openPersistentEditor(index)
tableView.show()
app.exec_()
why not use a model object pointer in your delegate item's function ? e.g connect your delegate item's signal to model's slots, or call model's function in your delegate item's event handler?
The code below creates the single QTableView. There is a QItemDelegate is assigned as a PersistentEditor. There are two kind of editors created: QLineEdit is created for the column 0 and the 'QComboBox` is created for the column 1.
When the tableView is clicked I would like to get the instance of both editors: the instance of the LineEdit and the instance of comboBox. How to achieve this?
from PyQt4 import QtCore, QtGui
app = QtGui.QApplication([])
class Delegate(QtGui.QItemDelegate):
def __init__(self):
QtGui.QItemDelegate.__init__(self)
def createEditor(self, parent, option, index):
if index.column()==0:
lineedit=QtGui.QLineEdit(parent)
return lineedit
elif index.column()==1:
combo=QtGui.QComboBox(parent)
return combo
def setEditorData(self, editor, index):
row = index.row()
column = index.column()
value = index.model().items[row][column]
if isinstance(editor, QtGui.QComboBox):
editor.addItems(['Somewhere','Over','The Rainbow'])
editor.setCurrentIndex(index.row())
if isinstance(editor, QtGui.QLineEdit):
editor.setText('Somewhere over the rainbow')
class Model(QtCore.QAbstractTableModel):
def __init__(self):
QtCore.QAbstractTableModel.__init__(self)
self.items = [[1, 'one', 'ONE'], [2, 'two', 'TWO'], [3, 'three', 'THREE']]
def flags(self, index):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable
def rowCount(self, parent=QtCore.QModelIndex()):
return 3
def columnCount(self, parent=QtCore.QModelIndex()):
return 3
def data(self, index, role):
if not index.isValid(): return
row = index.row()
column = index.column()
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.items[row][column]
def tableViewClicked(index):
print 'clicked indedx: %s'%index
tableModel=Model()
tableView=QtGui.QTableView()
tableView.setModel(tableModel)
tableView.setItemDelegate(Delegate())
tableView.clicked.connect(tableViewClicked)
for row in range(tableModel.rowCount()):
for column in range(tableModel.columnCount()):
index=tableModel.index(row, column)
tableView.openPersistentEditor(index)
tableView.show()
app.exec_()
I recently set up something similar like this. This might be what you were after.
Step 1, create the delegate to do whatever it is you need, and store the QLineEdit as a variable.
In the case below, I want a signal to emit every time the user leaves the QLineEdit.
class LineEditDelegate(QtWidgets.QStyledItemDelegate):
on_focus_out = QtCore.Signal(object)
def __init__(self, *args, **kwargs):
super(LineEditDelegate, self).__init__(*args, **kwargs)
self.line_edit = None
def createEditor(self, parent, option, index):
self.line_edit = QtWidgets.QLineEdit(parent=parent)
self.line_edit.destroyed.connect(lambda: self._line_edit_left(index=index))
return self.line_edit
def _line_edit_left(self, index):
"""
Triggers when line edit was closed / destroyed
:param index: QModelIndex, index of edited item
:return: None
"""
self.on_focus_out.emit(index)
return
Step 2, Create you QTableWidget and setup however you want, adding the delegate where you need it.
In this example, on all items in column 0
self.table = QtWidgets.QTableWidget(parent=self.base_widget)
self.table.setRowCount(2)
self.table.setColumnCount(2)
self.line_edit_delegate = LineEditDelegate(self.main_window)
self.table.setItemDelegateForColumn(0, self.line_edit_delegate)
I also connect my function to the signal, fulfilling the purpose of this delegate.
self.line_edit_delegate.on_focus_out.connect(self._check_text)
Step 3, Getting the delegate item, i.e. the QLineEdit.
First you'll need to get the item; you can do this in many ways such as:
item_0 = self.table.currentRow()
item_0 = self.table.currentItem()
or connecting through one of the QTableWidget's signals that pass through the QTableWidgetItem, row, or whatever:
self.table.itemClicked.connect(self.item_clicked)
self.table.itemDoubleClicked.connect(self.item_double_clicked)
Then with the QTableWidgetItem you can get the delegate, and then get the QLineEdit.
delegate = self.table.itemDelegate(self.table.indexFromItem(item_0))
delegate.line_edit
For my use, I wanted the user to add a new row and then have the QLineEdit get automatically selected for editing. This is what I used:
row_position = self.table.currentRow() + 1
item_0 = QtWidgets.QTableWidgetItem()
item_0.setFont(font)
self.table.setItem(row_position, 0, item_0)
self.table.openPersistentEditor(item_0)
delegate = self.table.itemDelegate(self.table.indexFromItem(item_0))
delegate.line_edit.setFocus()
The posted code creates a singe Model/Proxy QTableView. The multi-selection feature has been enabled for it.
There are four items total. Two of them include a character "A". Other two include character "B" in their "item" names.
QPushButton when pressed calls for the clicked() method.
When called this method first queries a Proxy Model connected to the QTableView:
proxyModel=self.tableview.model()
Then the method asks a proxyModel to return a total number of rows:
rows=proxyModel.rowCount()
Knowing how many rows in a QTabelView's model it iterates each row. First it is querying a row index:
index=proxyModel.index(row, 0)
Knowing index it proceeds with asking for a value stored in self.items variable by calling data() method supplying it with a queried in a previous step a QModelIndex (a variable index here) and a Role flag.
item=proxyModel.data(index, Qt.DisplayRole).toPyObject()
'toPyObject()' is used to convert the data received from .data() method to a "regular" Python variable.
Lastly it checks if the characters "B" in a received string. If so it selects QTableView row using:
self.tableview.selectRow(row)
Now what I want is to get the same selection functionality from inside of the scope of Proxy Model's filterAcceptsRow() if that is possible.
If it is not possible I would like to know if there is any other way of doing it... should be I using QItemSelectionModel? Then how?
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']
def rowCount(self, parent=QModelIndex()):
return len(self.items)
def columnCount(self, parent=QModelIndex()):
return 1
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
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)
button=QPushButton(self)
button.setText('Select Items with B')
button.clicked.connect(self.clicked)
layout = QVBoxLayout(self)
layout.addWidget(self.tableview)
layout.addWidget(button)
self.setLayout(layout)
def clicked(self, arg):
proxyModel=self.tableview.model()
self.tableview.clearSelection()
rows=proxyModel.rowCount()
for row in range(rows):
index=proxyModel.index(row, 0)
item=proxyModel.data(index, Qt.DisplayRole).toPyObject()
if '_B_' in item:
self.tableview.selectRow(row)
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
You can achieve the selection within the filterAcceptsRow() method of the proxy model, but doing so would require the following:
That your proxy model (or source model) contain a reference to the QTableView instance.
That your proxy model contain an attribute indicating whether it is active. This is because you want to only select the table rows when the button is clicked, but filterAcceptsRow() is called automatically by the proxy model. Therefore, you would want to avoid calling the view's selectRow() method until the button is clicked.
To achieve #1, you could define a simple setter method in your proxy model class:
def setView(self, view):
self._view = view
You would also need to of course invoke that setter within your MyWindow class's constructor:
proxyModel.setView(self.tableview)
Achieving #2 is a simple matter of creating this attribute in the proxy model class's constructor
self.filterActive = False
Now that your classes are prepared, you can implement your desired behavior. In your filterAcceptsRow() re-implementation, you only want to select the rows if they contain '_B_' and is the filter is active (that is, the button was clicked):
def filterAcceptsRow(self, row, parent):
if self.filterActive and '_B_' in self.sourceModel().data(self.sourceModel().index(row, 0), Qt.DisplayRole).toPyObject():
self._view.selectRow(row)
return True
Finally, you want to make sure that these conditions are met once the button is clicked, so in your clicked() method you need to set the proxyModel's filterActive attribute to True and you need to call the QSortFilterProxyModel class's invalidateFilter() method to indicate that the existing filter is invalid and therefore filterAcceptsRow() should be called again:
def clicked(self, arg):
proxyModel=self.tableview.model()
self.tableview.clearSelection()
proxyModel.filterActive = True
proxyModel.invalidateFilter()
So the new code, in full, is:
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']
def rowCount(self, parent=QModelIndex()):
return len(self.items)
def columnCount(self, parent=QModelIndex()):
return 1
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__()
self.filterActive = False
def setView(self, view):
self._view = view
def filterAcceptsRow(self, row, parent):
if self.filterActive and '_B_' in self.sourceModel().data(self.sourceModel().index(row, 0), Qt.DisplayRole).toPyObject():
self._view.selectRow(row)
return True
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)
proxyModel.setView(self.tableview)
button=QPushButton(self)
button.setText('Select Items with B')
button.clicked.connect(self.clicked)
layout = QVBoxLayout(self)
layout.addWidget(self.tableview)
layout.addWidget(button)
self.setLayout(layout)
def clicked(self, arg):
proxyModel=self.tableview.model()
self.tableview.clearSelection()
proxyModel.filterActive = True
proxyModel.invalidateFilter()
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
Having said all of that, the purpose of filterAcceptsRow() is so that you can implement your own custom filtering in a subclass of QSortFilterProxyModel. So, a more typical implementation (following your desired rule) would be:
def filterAcceptsRow(self, row, parent):
if not self.filterActive or '_B_' in self.sourceModel().data(self.sourceModel().index(row, 0), Qt.DisplayRole).toPyObject():
return True
return False
And even then, because the filtering could be done with regex, reimplementation of filterAcceptsRow() isn't even necessary. You could just call proxyModel.setFilterRegExp(QRegExp("_B_", Qt.CaseInsensitive, QRegExp.FixedString)) and proxyModel.setFilterKeyColumn(0) to achieve the same thing, filter-wise.
Hope that helps!