QSortFilterProxyModel Filter Issue - python

Using a QSortFilterProxyModel I need to filter the data based on the value of a specific column; however, the column may contain multiple values. I need to NOT show the row if the column contains a specific value. Do I need to subclass the QSortFilterProxyModel and override the filterAcceptsRow() method or should I use a setFilterRegExp?
The column can contain integers: 0,1,2,3. If the column contains a 2 then I need to not show the row.

If you store data as a QList or list you can easily subclass QSortFilterProxyModel to check this list in every row
Here is a simple example:
import sys
from PyQt5.QtCore import QSortFilterProxyModel,Qt
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QListView, QApplication
app = QApplication(sys.argv)
list = QListView()
list.setWindowTitle('sample')
list.setMinimumSize(600, 400)
model = QStandardItemModel(list)
for i in range(1, 10):
# create each item with a list attached to the Qt::UserRole + 1
item = QStandardItem(str(i))
item.setData([i, i*2], Qt.UserRole + 1)
model.appendRow(item)
class MyFilterModel(QSortFilterProxyModel):
def filterAcceptsRow(self, source_row, source_parent):
i = self.sourceModel().index(source_row, 0, source_parent)
data = self.sourceModel().data(i, Qt.UserRole + 1)
print(data)
return 2 not in data
filter_model = MyFilterModel()
filter_model.setSourceModel(model)
list.setModel(filter_model)
list.show()
app.exec()
You can even customize your filter model to accepts a lambda filter function

According to your description, you should subclass the QSortFilterProxyModel and override the filterAcceptsRow(), here bellow is a simple proxy model for table model:
class DemoProxyModel(QSortFilterProxyModel):
"""
"""
def __init__(self, parent=None):
super().__init__(parent)
def filterAcceptsRow(self, sourceRow, sourceParent):
"""custom filtering"""
if not self.filterRegExp().isEmpty():
model_source = self.sourceModel()
if model_source is None:
return False
# pick up the column that you want to filter, e.g., column 3
index0 = model_source.index(sourceRow, 3, sourceParent)
data0 = model_source.data(index0)
# return self.filterRegExp() == data_type # equal match, the tight binding
return self.filterRegExp().indexIn(data0) >= 0 # include match, the loose binding
# return data_type.__contains__(self.filterRegExp()) # include match, the loose binding
# parent call for initial behaviour
return super().filterAcceptsRow(sourceRow, sourceParent)

Related

How to show specific row after apply filter in QTableView

How to show hidden row after applying filter in QTableView. I attached the code below and I applied filter for second column for filter value '2'. it is working as required. if want to show hidden row which contain value '3' in second column. it is not showing the row. I used match command to find row. everything working fine. but row not showing. please help me to resolve this.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class TableModel(QAbstractTableModel):
def __init__(self, data):super().__init__();self._data = data
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole or role == Qt.EditRole :return self._data[index.row()][index.column()]
def rowCount(self, index):return len(self._data)
def columnCount(self, index):return len(self._data[0])
class tableview(QTableView):
def __init__(self):
super().__init__()
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.horizontalHeader().setStyleSheet("::section{Background-color:lightgray;border-radius:10px;}")
self.smodel = QSortFilterProxyModel()
self.smodel.setFilterKeyColumn(1)
self.setModel(self.smodel)
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.smodel.setSourceModel(TableModel([[1,2],[1,2],[1,3],[1,4]]))
self.smodel.setFilterFixedString('2')
def find(self,key):
start = self.smodel.index(0, 1)
matches = self.smodel.sourceModel().match(start,Qt.DisplayRole,key,hits=-1,flags=Qt.MatchExactly)
for match in matches:self.showRow(match.row())
app = QApplication([])
table=tableview()
table.show()
b=QPushButton();b.clicked.connect(lambda:table.find('3'))
b.show()
app.exec_()
Current result
Required result on button press
I think filter and showRow()/hideRow() can work in different way - so they may have problem to work together.
Filter removes data before sending to TableView, showRow()/hideRow() removes row directly in TableView. If you want to use showRow then you may need to clear filter, hide all rows and show rows with 2 and 3
But it may be simpler to use filter
To show only rows with selected value (key = "3")
smodel.setFilterFixedString(key)
To clear filter and show all rows
smodel.setFilterFixedString("")
To filter few values you can use regex
self.smodel.setFilterRegExp("2|3")
or you could keep values on list
filtered = ["2", "3"]
self.smodel.setFilterRegExp( "|".join(filtered) )
Minimal working code.
My button toggles row "3" - first click shows row, second click hides row, etc.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class TableModel(QAbstractTableModel):
def __init__(self, data):
super().__init__()
self._data = data
def data(self, index, role):
if role == Qt.ItemDataRole.DisplayRole or role == Qt.EditRole :
return self._data[index.row()][index.column()]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data[0])
class tableview(QTableView):
def __init__(self):
super().__init__()
self.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.horizontalHeader().setStyleSheet("::section{Background-color:lightgray;border-radius:10px;}")
self.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
self.smodel = QSortFilterProxyModel()
self.setModel(self.smodel)
self.smodel.setSourceModel(TableModel([[1,2],[1,2],[1,3],[1,4]]))
self.smodel.setFilterKeyColumn(1)
self.filtered = ["2"]
#self.smodel.setFilterFixedString("2")
self.smodel.setFilterRegExp( "|".join(self.filtered) )
def find(self, key):
print('find:', key)
if key in self.filtered:
self.filtered.remove(key)
else:
self.filtered.append(key)
#self.smodel.setFilterFixedString("") # clear filter - show all rows
#self.smodel.setFilterFixedString(key)
#self.smodel.setFilterRegExp("2|3")
self.smodel.setFilterRegExp( "|".join(self.filtered) )
# --- main ---
app = QApplication([])
table = tableview()
table.show()
button = QPushButton(text="Toggle: 3")
button.clicked.connect(lambda:table.find('3'))
button.show()
app.exec()
BTW:
I see only one problem: some chars have special meaning in regex so adding ie. dot . to filtered may hide all rows, so it may need use \..
The same problem can be with | ( ) [ ] ^ $, etc.

Get QTreeview index from text slug

I'm trying to retrieve the model index for the QTreeView item using the given slug - which is a single string representing the treeview item's hierarchy separated by hyphens. In this case, I want to get the model index for the given slug 'Vegetable-Carrot-Blackbean':
My current function always returns "Vegetable" and I feel like the way it's written I'd expect it to continually loop through a given index's children until it fails, returning the last found tree item:
import os, sys
from Qt import QtWidgets, QtGui, QtCore
class CategoryView(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.resize(250,400)
self.categoryModel = QtGui.QStandardItemModel()
self.categoryModel.setHorizontalHeaderLabels(['Items'])
self.categoryProxyModel = QtCore.QSortFilterProxyModel()
self.categoryProxyModel.setSourceModel(self.categoryModel)
self.categoryProxyModel.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.categoryProxyModel.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.categoryProxyModel.setDynamicSortFilter(True)
self.uiTreeView = QtWidgets.QTreeView()
self.uiTreeView.setModel(self.categoryProxyModel)
self.uiTreeView.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.layout = QtWidgets.QVBoxLayout()
self.layout.addWidget(self.uiTreeView)
self.setLayout(self.layout)
def appendCategorySlug(self, slug):
parts = slug.split('-')
parent = self.categoryModel.invisibleRootItem()
for name in parts:
for row in range(parent.rowCount()):
child = parent.child(row)
if child.text() == name:
parent = child
break
else:
item = QtGui.QStandardItem(name)
parent.appendRow(item)
parent = item
def getIndexBySlug(self, slug):
parts = slug.split('-')
index = QtCore.QModelIndex()
if not parts:
return index
root = self.categoryModel.index(0, 0)
for x in parts:
indexes = self.categoryModel.match(root, QtCore.Qt.DisplayRole, x, 1, QtCore.Qt.MatchExactly)
if indexes:
index = indexes[0]
root = index
print index, index.data()
return index
def test_CategoryView():
app = QtWidgets.QApplication(sys.argv)
ex = CategoryView()
ex.appendCategorySlug('Fruit-Apple')
ex.appendCategorySlug('Fruit-Orange')
ex.appendCategorySlug('Vegetable-Lettuce')
ex.appendCategorySlug('Fruit-Kiwi')
ex.appendCategorySlug('Vegetable-Carrot')
ex.appendCategorySlug('Vegetable-Carrot-Blackbean')
ex.appendCategorySlug('Vegan-Meat-Blackbean')
ex.getIndexBySlug('Vegetable-Carrot-Blackbean')
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
pass
test_CategoryView()
The reason your current implementation doesn't work is that the start argument of match() needs to be valid index. The invisisble root item can never be valid because its row and column will always be -1. So, instead, you must use the the model's index() function to try to get the first child index of the current parent. You also need to make sure that an invalid index is returned when any part of the slug cannot be matched, otherwise you could wrongly end up returning an ancestor index.
Here's a method that implements all that:
def getIndexBySlug(self, slug):
parts = slug.split('-')
indexes = [self.categoryModel.invisibleRootItem().index()]
for name in parts:
indexes = self.categoryModel.match(
self.categoryModel.index(0, 0, indexes[0]),
QtCore.Qt.DisplayRole, name, 1,
QtCore.Qt.MatchExactly)
if not indexes:
return QtCore.QModelIndex()
return indexes[0]
As an alternative, you might want to consider returning an item instead, as that will then give you access to the whole QStandardItem API (and the index can be still be easily obtained via item.index()):
def itemFromSlug(self, slug):
item = None
parent = self.categoryModel.invisibleRootItem()
for name in slug.split('-'):
for row in range(parent.rowCount()):
item = parent.child(row)
if item.text() == name:
parent = item
break
else:
item = None
break
return item
But note that this returns None if the slug cannot be found (although it could easily be tweaked to return the invisible root item instead).
In this case the convenient way is to iterate over the children recursively:
def getIndexBySlug(self, slug):
parts = slug.split("-")
index = QtCore.QModelIndex()
if not parts:
return index
for part in parts:
found = False
for i in range(self.categoryModel.rowCount(index)):
ix = self.categoryModel.index(i, 0, index)
if ix.data() == part:
index = ix
found = True
if not found:
return QtCore.QModelIndex()
return index

How to select cells in QtableWidget from list of indexes

In a QtableWidget, I would like to store the selected cells while I query a database and return the previously selected cells back to being selected. My refresh of items on the QtableWidget clears the selection. The user can select non-contiguous ranges of cells.
I have no problem getting the selected cells before I refresh the data with QtableWidget.selectedIndexes().
I have tried looping through the list of indexes and using setCurrentIndex but that only leaves me with the last index. I have run out of ideas. How can I restore the selected ranges of cells based on the stored indexes?
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from room_chart import *
from datetime import datetime, timedelta
class Guest_form(QDialog):
def __init__(self, parent=None):
QDialog.__init__(self)
self.ui = Ui_rooms_chart()
self.ui.setupUi(self)
self.build_chart()
self.ui.book.clicked.connect(self.book)
def book(self):
self.indexes = self.ui.room_chart.selectedIndexes()
#Do stuff
self.build_chart()
#This has the right behaviour but only selects the last index
for x in range(len(self.indexes)):
self.ui.room_chart.setCurrentIndex(self.indexes[x])
self.ui.room_chart.setFocus()
def build_chart(self):
self.ui.room_chart.setRowCount(0)
self.ui.room_chart.setColumnCount(0)
col_labels = []
for x in range(8):
current_day = datetime.now() + timedelta(days=x)
col_labels.append(current_day.strftime('%a') + '\n' + current_day.strftime('%d/%m/%y'))
self.ui.room_chart.setColumnCount(len(col_labels))
self.ui.room_chart.setHorizontalHeaderLabels(col_labels)
row_labels = []
for x in range(8):
row_labels.append(str(x))
self.ui.room_chart.setRowCount(len(row_labels))
self.ui.room_chart.setVerticalHeaderLabels(row_labels)
self.button = QPushButton(self.ui.room_chart)
self.button.setText("Push me")
self.ui.room_chart.setCellWidget(0 , 0, self.button)
if __name__=="__main__":
app=QApplication(sys.argv)
myapp = Guest_form()
myapp.show()
sys.exit(app.exec_())
You have to use the select() method of QItemSelectionModel:
def book(self):
persistenIndex = map(QPersistentModelIndex, self.ui.room_chart.selectedIndexes())
#Do stuff
self.build_chart()
for pix in persistenIndex:
ix = QModelIndex(pix)
self.ui.room_chart.selectionModel().select(ix, QItemSelectionModel.Select)
self.ui.room_chart.setFocus()
Note: It converts the QModelIndex to QPersistentModelIndex to avoid problems since it is not known if build_chart() deletes, moves or performs any other action that changes the position of the items.

Strange behaviour when appending columns in QStandardItemModel with nested QStandardItems

I am having a setup, where I need a QStandardItemModel with nested QStandardItems to be displayed via a QTreeView. I need to add and remove columns which I cannot seem to figure out how to do properly.
To insert the initial nested rows I use appendRow([item, item, ...]) and always use the previous item as new parent.
I get an unexpected row and column count of 0 for the last QStandardItem as can be seen in the logging output.
After an appendColumn() the column count of the items does not increase from 3 to 4. Instead a new item is inserted at an index with row 0 and column 1.
Before appendColumn() - column count 3
After appendColumn() - new column appears but column count is still 3
Example Code
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import logging
import sys
import uuid
from PySide import QtCore
from PySide import QtGui
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
class TestModel(QtGui.QStandardItemModel):
def __init__(self, *args, **kwargs):
super(TestModel, self).__init__(*args, **kwargs)
self.logger = logging.getLogger(type(self).__name__)
def set_data(self):
"""Fill with 3 nested rows with 3 columns each for testing."""
# cleanup
self.clear()
self.setHorizontalHeaderLabels(["header_1", "header_2", "header_3"])
# fill nested rows
parent = None
for index in range(3):
row = [QtGui.QStandardItem(str(uuid.uuid4().hex[:4])) for _ in range(3)]
self.appendRow(row) if (parent is None) else parent.appendRow(row)
parent = row[0]
def print_rows(self):
"""Print information about all rows of the model."""
for index in self.all_indices():
item = self.itemFromIndex(index)
msg = "QModelIndex: row {0} column {1} "
msg += "QStandardItem: row count {2} column count {3} "
msg += "value: {4}"
msg = msg.format(
index.row(), index.column(),
item.rowCount(), item.columnCount(), item.text())
self.logger.info(msg)
def all_indices(self, indices=None, index=None):
"""Recurse all indices under index in the first column and return
them as flat list.
"""
indices = indices if (isinstance(indices, list)) else []
index = index if (isinstance(index, QtCore.QModelIndex)) else QtCore.QModelIndex()
for row in range(self.rowCount(index)):
current_index = self.index(row, 0, index)
indices.append(current_index)
if (self.hasChildren(current_index)):
self.all_indices(indices, current_index)
return indices
def append_column(self):
self.logger.info("appendColumn()")
self.appendColumn([QtGui.QStandardItem(str(uuid.uuid4().hex[:4])),])
# test
if (__name__ == "__main__"):
app = QtGui.QApplication(sys.argv)
# widget
widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout()
widget.setLayout(layout)
widget.show()
# model
model = TestModel()
model.set_data()
# view
view = QtGui.QTreeView()
view.setModel(model)
layout.addWidget(view)
# btn_print_rows
btn_print_rows = QtGui.QPushButton("Prints rows")
btn_print_rows.clicked.connect(model.print_rows)
layout.addWidget(btn_print_rows)
# btn_append_column
btn_append_column = QtGui.QPushButton("Append column")
btn_append_column.clicked.connect(model.append_column)
layout.addWidget(btn_append_column)
sys.exit(app.exec_())
Questions
I get an unexpected row and column count for the last QStandardItem as can be seen in the logging output. Why has the last item a row and column count of 0 when the model clearly displays what seems to be correct data?
How can i correctly facilitate appending/removing of columns in a QStandardItemModel with nested QStandardItems (displayed in a QTreeView)?
Should I have QStandardItems with a column count equal to the header item count? (de/incrementing item.columnCount() for each new column). Or should all QStandardItems have a column count of 1?

PyQt: Adding rows to QTableView using QAbstractTableModel

I am super new to Qt programming. I am trying to make a simple table that can have rows added by clicking a button. I can implement the table fine but can't seem to get the updated data to show on the table. I believe my problem stems from the fact that I can't seem to properly call any sort of "change data" method using the button. I've tried several different solutions online all of which have lead to 4 year old, dead-end posts. What I have so far is the basic structure, I just can't figure out how to make the table update with new data.
This is the basic view
I have set up with some test data.
In the final implementation, the table will start empty and I would like to append rows and have them displayed in the table view.
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyWindow(QWidget):
def __init__(self):
QWidget.__init__(self)
# create table
self.get_table_data()
self.table = self.createTable()
# layout
self.layout = QVBoxLayout()
self.testButton = QPushButton("test")
self.connect(self.testButton, SIGNAL("released()"), self.test)
self.layout.addWidget(self.testButton)
self.layout.addWidget(self.table)
self.setLayout(self.layout)
def get_table_data(self):
self.tabledata = [[1234567890,2,3,4,5],
[6,7,8,9,10],
[11,12,13,14,15],
[16,17,18,19,20]]
def createTable(self):
# create the view
tv = QTableView()
# set the table model
header = ['col_0', 'col_1', 'col_2', 'col_3', 'col_4']
tablemodel = MyTableModel(self.tabledata, header, self)
tv.setModel(tablemodel)
# set the minimum size
tv.setMinimumSize(400, 300)
# hide grid
tv.setShowGrid(False)
# hide vertical header
vh = tv.verticalHeader()
vh.setVisible(False)
# set horizontal header properties
hh = tv.horizontalHeader()
hh.setStretchLastSection(True)
# set column width to fit contents
tv.resizeColumnsToContents()
# set row height
tv.resizeRowsToContents()
# enable sorting
tv.setSortingEnabled(False)
return tv
def test(self):
self.tabledata.append([1,1,1,1,1])
self.emit(SIGNAL('dataChanged()'))
print 'success'
class MyTableModel(QAbstractTableModel):
def __init__(self, datain, headerdata, parent=None):
"""
Args:
datain: a list of lists\n
headerdata: a list of strings
"""
QAbstractTableModel.__init__(self, parent)
self.arraydata = datain
self.headerdata = headerdata
def rowCount(self, parent):
return len(self.arraydata)
def columnCount(self, parent):
if len(self.arraydata) > 0:
return len(self.arraydata[0])
return 0
def data(self, index, role):
if not index.isValid():
return QVariant()
elif role != Qt.DisplayRole:
return QVariant()
return QVariant(self.arraydata[index.row()][index.column()])
def setData(self, index, value, role):
pass # not sure what to put here
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return QVariant(self.headerdata[col])
return QVariant()
def sort(self, Ncol, order):
"""
Sort table by given column number.
"""
self.emit(SIGNAL("layoutAboutToBeChanged()"))
self.arraydata = sorted(self.arraydata, key=operator.itemgetter(Ncol))
if order == Qt.DescendingOrder:
self.arraydata.reverse()
self.emit(SIGNAL("layoutChanged()"))
if __name__ == "__main__":
app = QApplication(sys.argv)
w = MyWindow()
w.show()
sys.exit(app.exec_())
When the underlying data of the model changes, the model should emit either layoutChanged or layoutAboutToBeChanged, so that view updates properly (there's also dataChanged, if you want to update a specific range of cells).
So you just need something like this:
def test(self):
self.tabledata.append([1,1,1,1,1])
self.table.model().layoutChanged.emit()
print 'success'
QAbstractTableModel have two special methods for that ( beginInsertRows() and endInsertRows()).
You can add api-point in your custom model. For example:
def insertGuest(self, guest):
self.beginInsertRows(QtCore.QModelIndex(), self.rowCount(), self.rowCount())
self.guestsTableData.append(guest)
self.endInsertRows()
I've made your table reference a class variable instead of an instance variable, so you could edit the data for the table from virtually anywhere in your code.
# First access the data of the table
self.tv_model = self.tv.model()
Secondly, I use the sort of pandas-dataframe-editing type approach.
Lets say your data that you want to add is stored in a variable on its own:
# These can be whatever, but for consistency,
# I used the data in the OP's example
new_values = [1, 1, 1, 1, 1]
There are different ways the next step can be approached, depending on whether the data is being added to the table, or updating existing values. Adding the data as a new row would be as follows.
# The headers should also be a class variable,
# but I left it as the OP had it
header = ['col_0', 'col_1', 'col_2', 'col_3', 'col_4']
# There are multiple ways of establishing what the row reference should be,
# this is just one example how to add a new row
new_row = len(self.tv_model.dataFrame.index)
for i, col in enumerate(header):
self.tv_model.dataFrame.loc[new_row, col] = new_values[i]
Since self.tv_model is a reference to the actual data of the table,
emitting the following signal will update the data, or 'commit' it to the model,
so to speak.
self.tv_model.layoutChanged.emit()

Categories