Below is the example code:
import sys
from PyQt5 import QtCore, QtWidgets, QtSql, uic
class FilterProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._filter_value = None
#property
def filter_value(self):
return self._filter_value
#filter_value.setter
def filter_value(self, value):
self._filter_value = value
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow, sourceParent):
if self.filter_value is None:
return super().filterAcceptsRow(sourceRow, sourceParent)
if self.filterKeyColumn() >= 0:
value = (
self.sourceModel()
.index(sourceRow, self.filterKeyColumn(), sourceParent)
.data(self.filterRole())
)
return value == self.filter_value
for column in range(self.columnCount()):
value = (
self.sourceModel()
.index(sourceRow, column, sourceParent)
.data(self.filterRole())
)
if value == self.filter_value:
return True
return False
def setFilterRegExp(self, filter):
self.filter_value = None
super().setFilterRegExp(filter)
class UI(QtWidgets.QMainWindow):
def __init__(self):
super(UI, self).__init__()
uic.loadUi("tableview.ui", self)
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("book.db")
db.open()
self.model = QtSql.QSqlTableModel(self)
self.model.setTable("card")
self.model.select()
self.proxy = FilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.tableView.setModel(self.proxy)
self.model.select()
self.edit.clicked.connect(self.edit_items)
self.refresh.clicked.connect(self.refresh_table)
r = self.model.record()
column_names = [r.field(i).name().title() for i in range(r.count())]
self.comboBox.addItems([x for x in column_names])
self.horizontalHeader = self.tableView.horizontalHeader()
self.horizontalHeader.sectionClicked.connect(
self.tableView_horizontalHeader_sectionClicked
)
self.lineEdit.textChanged.connect(self.lineEdit_textChanged)
def tableView_horizontalHeader_sectionClicked(self, logicalIndex):
menu = QtWidgets.QMenu(self)
values = []
for row in range(self.model.rowCount()):
value = self.model.index(row, logicalIndex).data(self.proxy.filterRole())
values.append(value)
action_all = QtWidgets.QAction("All", self)
action_all.setData(None)
menu.addAction(action_all)
menu.addSeparator()
for value in sorted(list(set(values))):
action = QtWidgets.QAction(str(value), self)
action.setData(value)
menu.addAction(action)
headerPos = self.tableView.mapToGlobal(self.horizontalHeader.pos())
posY = headerPos.y() + self.horizontalHeader.height()
posX = headerPos.x() + self.horizontalHeader.sectionPosition(logicalIndex)
action = menu.exec_(QtCore.QPoint(posX, posY))
if action is not None:
self.proxy.setFilterKeyColumn(logicalIndex)
self.proxy.filter_value = action.data()
def lineEdit_textChanged(self):
search = QtCore.QRegExp(
self.lineEdit.text(), QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp
)
self.proxy.setFilterKeyColumn(self.comboBox.currentIndex())
self.proxy.setFilterRegExp(search)
def edit_items(self):
if not self.model.rowCount():
return
index = self.tableView.currentIndex()
if index.isValid():
row = index.row()
else:
row = 0
name_line = QtWidgets.QLineEdit(readOnly=True)
age_edit = QtWidgets.QSpinBox()
gender_combo = QtWidgets.QComboBox()
genders = "M", "F"
gender_combo.addItems(genders)
date_of_birth = QtWidgets.QDateEdit()
date_of_birth.setDisplayFormat("d-MMM-yyyy")
updateButton = QtWidgets.QPushButton("Update")
mapper = QtWidgets.QDataWidgetMapper()
mapper.setSubmitPolicy(QtWidgets.QDataWidgetMapper.ManualSubmit)
mapper.setModel(self.tableView.model())
mapper.addMapping(name_line, 0)
mapper.addMapping(age_edit, 1)
mapper.addMapping(gender_combo, 2)
mapper.addMapping(date_of_birth, 3)
mapper.setCurrentIndex(row)
dialog = QtWidgets.QDialog()
dialog.setWindowTitle("Edit Window")
layout = QtWidgets.QVBoxLayout(dialog)
formLayout = QtWidgets.QFormLayout()
layout.addLayout(formLayout)
formLayout.addRow("Name", name_line)
formLayout.addRow("Age", age_edit)
formLayout.addRow("Gender", gender_combo)
formLayout.addRow("Date of Birth", date_of_birth)
layout.addWidget(updateButton)
updateButton.clicked.connect(dialog.accept)
if dialog.exec_():
mapper.submit()
def refresh_table(self):
print("refresh")
def main():
app = QtWidgets.QApplication(sys.argv)
w = UI()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I am trying to edit row data from Qsqltablemodel using with QDataWidgetMapper(). The date column is in text format in My database table, I want to use date format as "d-MMM-yyyy". When i am trying to edit the row, the date column is setting the default date format as "1-1-2000".
How to do this?
Below is the example Image:
By default QtSql can handle string columns with a specific format such as QDate and QDateTime (as indicated in the sqlite docs), but in this case it does not comply with those formats so Qt does not know how to interpret them and displays them as text. So you must convert that text into QDate, and vice versa, using a delegate:
class ItemDelegate(QtWidgets.QItemDelegate):
def setEditorData(self, editor, index):
if index.column() == 3 and isinstance(editor, QtWidgets.QDateEdit):
text = index.data()
date = QtCore.QDate.fromString(text, "d-MMM-yyyy")
editor.setDate(date)
return
super().setEditorData(editor, index)
def setModelData(self, editor, model, index):
if index.column() == 3 and isinstance(editor, QtWidgets.QDateEdit):
text = editor.date().toString("d-MMM-yyyy")
model.setData(index, text)
return
super().setModelData(editor, model, index)
# ...
mapper = QtWidgets.QDataWidgetMapper()
delegate = ItemDelegate(mapper)
mapper.setItemDelegate(delegate)
# ...
Related
It is not possible to make the text crossed out in a separate cell. It turns out only to apply to everyone. Tried changing in model methods. Also applies to everyone. I looked for an example on the Internet, but it didn't work. The bottom line is for the user to select a cell, click on the button and the text in the cell becomes checked out.
My code
import sys
from PySide2.QtCore import *
from PySide2.QtGui import *
from PySide2.QtWidgets import *
STRIKE_ROLE = Qt.UserRole + 1
import pandas as pd
class StrikeDelegate(QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
font = QFont(option.font)
font.setStrikeOut(index.data(STRIKE_ROLE))
option.font = font
class MyDelegate(QItemDelegate):
def setEditorData(self, editor, index):
text = index.data(Qt.EditRole) or index.data(Qt.DisplayRole)
editor.setText(text)
# def initStyleOption(self,option,index):
#
# QtWidgets.QStyledItemDelegate.initStyleOption(option,index)
# option.font().setStrikeOut(True)
class TableModel(QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
def setData(self, index, value, role):
self._data.iloc[index.row(), index.column()] = value
self.dataChanged.emit(index, index)
return True
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsEditable |Qt.ItemIsSelectable | Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled
def data(self, index, role):
if role == Qt.DisplayRole:
value = self._data.iloc[index.row(), index.column()]
return str(value)
def rename_column(self, index, new_name):
self._data.rename(index={self._data.index[index]: new_name}, inplace=True)
return True
def rowCount(self, index):
return self._data.shape[0]
def columnCount(self, index):
return self._data.shape[1]
def headerData(self, section, orientation, role):
# section is the index of the column/row.
if role == Qt.DisplayRole:
if orientation == Qt.Horizontal:
return str(self._data.columns[section])
if orientation == Qt.Vertical:
return str(self._data.index[section])
def add_empty_row(self):
self.beginResetModel()
try:
self._data = self._data.append(
pd.DataFrame(columns=self._data.columns, data=([[0] * len(self._data.columns)]),
index=['Пусто']))
except IndexError:
self._data = self._data.append(
pd.DataFrame(columns=self._data.columns, data=([[None] * len(self._data.columns)]),
index=[0]))
self.layoutChanged.emit()
self.endResetModel()
# self.inp_.add_delete_row.emit(self.createIndex(self._data.index[-1], 0), 'add_empty_row')
def delete_row(self, index):
self._data.drop(self._data.index[index.row()], inplace=True)
# self._data.reset_index(inplace=True, drop=True)
self.layoutChanged.emit()
class TaskModel(TableModel):
def add_empty_row(self):
self.beginResetModel()
try:
self._data = self._data.append(
pd.DataFrame(columns=self._data.columns, data=([['Новая задача'] * len(self._data.columns)]),
index=[self._data.index[-1] + 1]))
except IndexError:
self._data = self._data.append(
pd.DataFrame(columns=self._data.columns, data=([[None] * len(self._data.columns)]),
index=[0]))
self.layoutChanged.emit()
self.endResetModel()
def data(self, index, role):
value = self._data.iloc[index.row(), index.column()]
if role == Qt.DisplayRole:
return str(value)
if role == STRIKE_ROLE:
font=QFont('Calibri',13)
return font
def setData(self, index, value, role):
self._data.iloc[index.row(), index.column()] = value
self.dataChanged.emit(index, index)
return True
class Ui_Form(object):
def setupUi(self, Form):
if not Form.objectName():
Form.setObjectName(u"Form")
Form.resize(586, 683)
self.Form=Form
self.horizontalLayout_2 = QHBoxLayout(Form)
self.horizontalLayout_2.setObjectName(u"horizontalLayout_2")
self.dock_task_calendar = QDockWidget(Form)
self.dock_task_calendar.setObjectName(u"dock_task_calendar")
self.dock_task_calendar.setFeatures(QDockWidget.DockWidgetClosable|QDockWidget.DockWidgetMovable)
self.dockWidgetContents_task = QWidget()
self.dockWidgetContents_task.setObjectName(u"dockWidgetContents_task")
self.verticalLayout_3 = QVBoxLayout(self.dockWidgetContents_task)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.scrollArea_task = QScrollArea(self.dockWidgetContents_task)
self.scrollArea_task.setObjectName(u"scrollArea_task")
self.scrollArea_task.setWidgetResizable(True)
self.scrollAreaWidgetContents_task_2 = QWidget()
self.scrollAreaWidgetContents_task_2.setObjectName(u"scrollAreaWidgetContents_task_2")
self.scrollAreaWidgetContents_task_2.setGeometry(QRect(0, 0, 548, 623))
self.horizontalLayout = QHBoxLayout(self.scrollAreaWidgetContents_task_2)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.gridLayout = QGridLayout()
self.gridLayout.setObjectName(u"gridLayout")
self.verticalLayout_2 = QVBoxLayout()
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.gridLayout.addLayout(self.verticalLayout_2, 4, 0, 1, 1)
self.tasklistView = QListView(self.scrollAreaWidgetContents_task_2)
self.tasklistView.setObjectName(u"tasklistView")
self.tasklistView.setDragEnabled(True)
self.tasklistView.setDragDropOverwriteMode(True)
self.tasklistView.setWordWrap(True)
self.tasklistView.setItemAlignment(Qt.AlignLeading)
self.gridLayout.addWidget(self.tasklistView, 4, 1, 1, 1)
self.model=TaskModel(pd.DataFrame([],columns=['a']))
self.delegater=MyDelegate()
self.tasklistView.setModel(self.model)
self.tasklistView.setItemDelegate(self.delegater)
self.label_task = QLabel(self.scrollAreaWidgetContents_task_2)
self.label_task.setObjectName(u"label_task")
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.label_task.sizePolicy().hasHeightForWidth())
self.label_task.setSizePolicy(sizePolicy)
self.label_task.setMaximumSize(QSize(16777215, 20))
self.label_task.setAlignment(Qt.AlignCenter)
self.gridLayout.addWidget(self.label_task, 2, 1, 1, 1)
self.calendar = QCalendarWidget(self.scrollAreaWidgetContents_task_2)
self.calendar.setObjectName(u"calendar")
sizePolicy1 = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
sizePolicy1.setHorizontalStretch(0)
sizePolicy1.setVerticalStretch(0)
sizePolicy1.setHeightForWidth(self.calendar.sizePolicy().hasHeightForWidth())
self.calendar.setSizePolicy(sizePolicy1)
self.calendar.setMaximumSize(QSize(16777215, 400))
self.gridLayout.addWidget(self.calendar, 1, 1, 1, 1)
self.verticalLayout = QVBoxLayout()
self.verticalLayout.setObjectName(u"verticalLayout")
self.add_task = QPushButton(self.scrollAreaWidgetContents_task_2)
self.add_task.setObjectName(u"add_task")
sizePolicy2 = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
sizePolicy2.setHorizontalStretch(0)
sizePolicy2.setVerticalStretch(0)
sizePolicy2.setHeightForWidth(self.add_task.sizePolicy().hasHeightForWidth())
self.add_task.setSizePolicy(sizePolicy2)
self.add_task.setMaximumSize(QSize(40, 40))
self.verticalLayout.addWidget(self.add_task)
self.del_task = QPushButton(self.scrollAreaWidgetContents_task_2)
self.del_task.setObjectName(u"del_task")
self.del_task.setMaximumSize(QSize(40, 40))
self.verticalLayout.addWidget(self.del_task)
self.check_task = QPushButton(self.scrollAreaWidgetContents_task_2)
self.check_task.setObjectName(u"check_task")
self.check_task.setMaximumSize(QSize(40, 40))
self.verticalLayout.addWidget(self.check_task)
self.verticalSpacer = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
self.verticalLayout.addItem(self.verticalSpacer)
self.gridLayout.addLayout(self.verticalLayout, 4, 2, 1, 1)
self.horizontalLayout.addLayout(self.gridLayout)
self.scrollArea_task.setWidget(self.scrollAreaWidgetContents_task_2)
self.verticalLayout_3.addWidget(self.scrollArea_task)
self.dock_task_calendar.setWidget(self.dockWidgetContents_task)
self.horizontalLayout_2.addWidget(self.dock_task_calendar)
self.add_task.pressed.connect(self.model.add_empty_row)
self.retranslateUi(Form)
self.Form.show()
# setupUi
def retranslateUi(self, Form):
Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None))
self.dock_task_calendar.setWindowTitle("")
self.label_task.setText(QCoreApplication.translate("Form", u"C\u043f\u0438\u0441\u043e\u043a \u0434\u0435\u043b/\u0437\u0430\u0434\u0430\u0447", None))
self.add_task.setText(QCoreApplication.translate("Form", u"PushButton", None))
self.del_task.setText(QCoreApplication.translate("Form", u"PushButton", None))
self.check_task.setText(QCoreApplication.translate("Form", u"PushButton", None))
# retranslateUi
if __name__ == '__main__':
app = QApplication(sys.argv)
translator = QTranslator()
if len(sys.argv) > 1:
locale = sys.argv[1]
else:
locale = QLocale.system().name()
translator.load('qt_%s' % locale,
QLibraryInfo.location(QLibraryInfo.TranslationsPath))
app.installTranslator(translator)
form=Ui_Form()
form.setupUi(QWidget())
app.exec_()
Since the OP has not provided an MRE then my solution will only limit the general logic which is:
Create a new role for each item where a boolean (or any other variable) is stored if the strike is applied.
Use a delegate to set the strike in the font.
With the clicked signal change the value stored in the role strike for selected rows.
from functools import cached_property
from PyQt5 import QtCore, QtGui, QtWidgets
STRIKE_ROLE = QtCore.Qt.UserRole + 1
class StrikeDelegate(QtWidgets.QStyledItemDelegate):
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
font = QtGui.QFont(option.font)
font.setStrikeOut(index.data(STRIKE_ROLE))
option.font = font
def createEditor(self, parent, option, index):
editor = super().createEditor(parent, option, index)
font = QtGui.QFont(editor.font())
font.setStrikeOut(index.data(STRIKE_ROLE))
editor.setFont(font)
return editor
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
hlay = QtWidgets.QHBoxLayout(central_widget)
vlay = QtWidgets.QVBoxLayout()
vlay.addWidget(self.add_button)
vlay.addWidget(self.remove_button)
vlay.addWidget(self.strike_button)
vlay.addStretch()
hlay.addWidget(self.view)
hlay.addLayout(vlay)
self.add_button.clicked.connect(self.add)
self.remove_button.clicked.connect(self.remove)
self.strike_button.clicked.connect(self.strike)
#cached_property
def model(self):
return QtGui.QStandardItemModel()
#cached_property
def view(self):
view = QtWidgets.QListView()
delegate = StrikeDelegate()
view.setItemDelegate(delegate)
view.setModel(self.model)
return view
#cached_property
def add_button(self):
return QtWidgets.QPushButton("Add")
#cached_property
def remove_button(self):
return QtWidgets.QPushButton("Remove")
#cached_property
def strike_button(self):
return QtWidgets.QPushButton("Strike")
def add(self):
text, ok = QtWidgets.QInputDialog.getText(self, "Title", "Text:")
if not ok:
return
item = QtGui.QStandardItem(text)
item.setData(True, STRIKE_ROLE)
self.model.appendRow(item)
def remove(self):
rows = [index.row() for index in self.view.selectedIndexes()]
for row in sorted(rows, reverse=True):
self.model.takeRow(row)
def strike(self):
for index in self.view.selectedIndexes():
strike = self.model.data(index, STRIKE_ROLE)
self.model.setData(index, not strike, STRIKE_ROLE)
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Below is the example code.
import sys
from PyQt5 import QtCore, QtWidgets, QtSql, uic
class FilterProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._filter_value = None
#property
def filter_value(self):
return self._filter_value
#filter_value.setter
def filter_value(self, value):
self._filter_value = value
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow, sourceParent):
if self.filter_value is None:
return super().filterAcceptsRow(sourceRow, sourceParent)
if self.filterKeyColumn() >= 0:
value = (
self.sourceModel()
.index(sourceRow, self.filterKeyColumn(), sourceParent)
.data(self.filterRole())
)
return value == self.filter_value
for column in range(self.columnCount()):
value = (
self.sourceModel()
.index(sourceRow, column, sourceParent)
.data(self.filterRole())
)
if value == self.filter_value:
return True
return False
def setFilterRegExp(self, filter):
self.filter_value = None
super().setFilterRegExp(filter)
class UI(QtWidgets.QMainWindow):
def __init__(self):
super(UI, self).__init__()
uic.loadUi("tableview.ui", self)
self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
db.setDatabaseName("book.db")
db.open()
self.model = QtSql.QSqlTableModel(self)
self.model.setTable("card")
self.model.select()
self.proxy = FilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.tableView.setModel(self.proxy)
self.model.select()
self.refresh.clicked.connect(self.refresh_table)
r = self.model.record()
column_names = [r.field(i).name().title() for i in range(r.count())]
self.comboBox.addItems([x for x in column_names])
self.horizontalHeader = self.tableView.horizontalHeader()
self.horizontalHeader.sectionClicked.connect(
self.tableView_horizontalHeader_sectionClicked
)
self.lineEdit.textChanged.connect(self.lineEdit_textChanged)
def tableView_horizontalHeader_sectionClicked(self, logicalIndex):
menu = QtWidgets.QMenu(self)
values = []
for row in range(self.model.rowCount()):
value = self.model.index(row, logicalIndex).data(self.proxy.filterRole())
values.append(value)
action_all = QtWidgets.QAction("All", self)
action_all.setData(None)
menu.addAction(action_all)
menu.addSeparator()
for value in sorted(list(set(values))):
action = QtWidgets.QAction(str(value), self)
action.setData(value)
menu.addAction(action)
headerPos = self.tableView.mapToGlobal(self.horizontalHeader.pos())
posY = headerPos.y() + self.horizontalHeader.height()
posX = headerPos.x() + self.horizontalHeader.sectionPosition(logicalIndex)
action = menu.exec_(QtCore.QPoint(posX, posY))
if action is not None:
self.proxy.setFilterKeyColumn(logicalIndex)
self.proxy.filter_value = action.data()
def lineEdit_textChanged(self):
search = QtCore.QRegExp(
self.lineEdit.text(), QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp
)
self.proxy.setFilterKeyColumn(self.comboBox.currentIndex())
self.proxy.setFilterRegExp(search)
def refresh_table(self):
print("refresh")
def main():
app = QtWidgets.QApplication(sys.argv)
w = UI()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I am using FilterProxyModel(QtCore.QSortFilterProxyModel), QTableView and QtSql.QSqlTableModel. I am able to filter rows. My question is How to refresh the proxymodel or tableview to show all rows with push button. As per above image the rows was filtered with qmenu. Now i want to refresh and show the all rows after clicked With the Qpushbutton, I want to show all rows with Not only with the menu option but also with the Qpushbutton. Is it possible to refresh, How to do?
If you want to remove the filter then you must set None to filter_value:
def refresh_table(self):
self.proxy.filter_value = None
I'm trying to update a table/table model after loading a new csv so it shows the new values. Would updating the widget also some how manage this? I've cut down the code quite a bit below. You can use any 2 column csv:
0, 0, 0, 0, 0, 0,
import pandas as pd
from PySide2 import QtWidgets, QtGui, QtCore
def read_data(fname):
df = pd.read_csv(fname, sep=',', usecols=(0, 1), header=None)
df = df.dropna(how='any')
df = df[pd.to_numeric(df[0], errors='coerce').notnull()]
data_arr = df.to_numpy(dtype='float64')
return data_arr
class CustomTableModel(QtCore.QAbstractTableModel):
def __init__(self, data=None):
QtCore.QAbstractTableModel.__init__(self)
self.load_data(data)
def load_data(self, data):
self.input_x = data[:, 0]
self.input_y = data[:, 1]
self.column_count = 2
self.row_count = len(self.input_x)
def rowCount(self, parent=QtCore.QModelIndex()):
return self.row_count
def columnCount(self, parent=QtCore.QModelIndex()):
return self.column_count
def headerData(self, section, orientation, role):
if role != QtCore.Qt.DisplayRole:
return None
if orientation == QtCore.Qt.Horizontal:
return ("x", "y")[section]
else:
return "{}".format(section)
def data(self, index, role=QtCore.Qt.DisplayRole):
column = index.column()
row = index.row()
if role == QtCore.Qt.DisplayRole:
if column == 0:
return str(self.input_x[row])
elif column == 1:
return str(self.input_y[row])
elif role == QtCore.Qt.BackgroundRole:
return QtGui.QColor(QtCore.Qt.white)
elif role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignRight
return None
class Widget(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
try:
self.data
except AttributeError:
self.open_csv()
self.model = CustomTableModel(self.data)
self.table_view = QtWidgets.QTableView()
self.table_view.setModel(self.model)
resize = QtWidgets.QHeaderView.ResizeToContents
self.horizontal_header = self.table_view.horizontalHeader()
self.vertical_header = self.table_view.verticalHeader()
self.horizontal_header.setSectionResizeMode(resize)
self.vertical_header.setSectionResizeMode(resize)
self.horizontal_header.setStretchLastSection(False)
# Creating layout
self.main_layout = QtWidgets.QVBoxLayout()
self.file_button = QtWidgets.QPushButton('CSV Import', self)
self.file_button.clicked.connect(self.open_csv)
self.main_layout.addWidget(self.table_view)
self.main_layout.addWidget(self.file_button)
self.setLayout(self.main_layout)
def open_csv(self):
filename, *_ = QtWidgets.QFileDialog.getOpenFileName(self, self.tr('Open CSV'),
self.tr("~/Desktop/"), self.tr('Files (*.csv)'))
self.data = read_data(filename)
return None
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, widget):
QtWidgets.QMainWindow.__init__(self)
self.setWindowTitle('Linear Plotter')
self.setCentralWidget(widget)
self.menu = self.menuBar()
self.file_menu = self.menu.addMenu('File')
exit_action = QtWidgets.QAction('Exit', self)
exit_action.setShortcut(QtGui.QKeySequence.Quit)
exit_action.triggered.connect(self.close)
self.file_menu.addAction(exit_action)
self.status = self.statusBar()
self.status.showMessage('Data loaded and plotted')
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
widget = Widget()
window = MainWindow(widget)
window.show()
sys.exit(app.exec_())
Not sure if QWidget.update() would work? I could not get it working.
I am trying to make a PyQt5 GUI to show a Pandas dataframe in the form of a table and provide column filtering options, similar to the Microsoft Excel filters. So far I managed to adopt a similar SO answer. Here is the picture of my table in the GUI:
As shown in the figure above, there are two ways to filter columns: the Regex Filter and clicking on each column. There is however a problem I need help to address: the currently applied filters (either regex filter or column click) disappear when I filter a second column. I want the second filter as AND, i.e. a filter that satisfies column 1 AND column 2.
Here is my code:
#!/usr/bin/env python
#-*- coding:utf-8 -*-
from PyQt5 import QtCore, QtGui, QtWidgets
import pandas as pd
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, df=pd.DataFrame(), parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self._df = df.copy()
def toDataFrame(self):
return self._df.copy()
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.iloc[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()
class myWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(myWindow, self).__init__(parent)
self.centralwidget = QtWidgets.QWidget(self)
self.lineEdit = QtWidgets.QLineEdit(self.centralwidget)
self.view = QtWidgets.QTableView(self.centralwidget)
self.comboBox = QtWidgets.QComboBox(self.centralwidget)
self.label = QtWidgets.QLabel(self.centralwidget)
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.addWidget(self.lineEdit, 0, 1, 1, 1)
self.gridLayout.addWidget(self.view, 1, 0, 1, 3)
self.gridLayout.addWidget(self.comboBox, 0, 2, 1, 1)
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.setCentralWidget(self.centralwidget)
self.label.setText("Regex Filter")
self.load_sites()
self.comboBox.addItems(["{0}".format(col) for col in self.model._df.columns])
self.lineEdit.textChanged.connect(self.on_lineEdit_textChanged)
self.comboBox.currentIndexChanged.connect(self.on_comboBox_currentIndexChanged)
self.horizontalHeader = self.view.horizontalHeader()
self.horizontalHeader.sectionClicked.connect(self.on_view_horizontalHeader_sectionClicked)
def load_sites(self):
df = pd.DataFrame({'site_codes': ['01', '02', '03', '04'],
'status': ['open', 'open', 'open', 'closed'],
'Location': ['east', 'north', 'south', 'east'],
'data_quality': ['poor', 'moderate', 'high', 'high']})
self.model = PandasModel(df)
self.proxy = QtCore.QSortFilterProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.view.resizeColumnsToContents()
#QtCore.pyqtSlot(int)
def on_view_horizontalHeader_sectionClicked(self, logicalIndex):
self.logicalIndex = logicalIndex
self.menuValues = QtWidgets.QMenu(self)
self.signalMapper = QtCore.QSignalMapper(self)
self.comboBox.blockSignals(True)
self.comboBox.setCurrentIndex(self.logicalIndex)
self.comboBox.blockSignals(True)
valuesUnique = self.model._df.iloc[:, self.logicalIndex].unique()
actionAll = QtWidgets.QAction("All", self)
actionAll.triggered.connect(self.on_actionAll_triggered)
self.menuValues.addAction(actionAll)
self.menuValues.addSeparator()
for actionNumber, actionName in enumerate(sorted(list(set(valuesUnique)))):
action = QtWidgets.QAction(actionName, self)
self.signalMapper.setMapping(action, actionNumber)
action.triggered.connect(self.signalMapper.map)
self.menuValues.addAction(action)
self.signalMapper.mapped.connect(self.on_signalMapper_mapped)
headerPos = self.view.mapToGlobal(self.horizontalHeader.pos())
posY = headerPos.y() + self.horizontalHeader.height()
posX = headerPos.x() + self.horizontalHeader.sectionPosition(self.logicalIndex)
self.menuValues.exec_(QtCore.QPoint(posX, posY))
#QtCore.pyqtSlot()
def on_actionAll_triggered(self):
filterColumn = self.logicalIndex
filterString = QtCore.QRegExp( "",
QtCore.Qt.CaseInsensitive,
QtCore.QRegExp.RegExp
)
self.proxy.setFilterRegExp(filterString)
self.proxy.setFilterKeyColumn(filterColumn)
#QtCore.pyqtSlot(int)
def on_signalMapper_mapped(self, i):
stringAction = self.signalMapper.mapping(i).text()
filterColumn = self.logicalIndex
filterString = QtCore.QRegExp( stringAction,
QtCore.Qt.CaseSensitive,
QtCore.QRegExp.FixedString
)
self.proxy.setFilterRegExp(filterString)
self.proxy.setFilterKeyColumn(filterColumn)
#QtCore.pyqtSlot(str)
def on_lineEdit_textChanged(self, text):
search = QtCore.QRegExp( text,
QtCore.Qt.CaseInsensitive,
QtCore.QRegExp.RegExp
)
self.proxy.setFilterRegExp(search)
#QtCore.pyqtSlot(int)
def on_comboBox_currentIndexChanged(self, index):
self.proxy.setFilterKeyColumn(index)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
main = myWindow()
main.show()
main.resize(800, 600)
sys.exit(app.exec_())
If you want to implement a custom filtering process then you must override the filterAcceptsRow method, obtain the texts of each column and verify if they meet the condition, if they do return True, otherwise False. To recalculate the filter you must call the invalidateFilter method:
class CustomProxyModel(QtCore.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 = QtCore.QRegExp(
expresion, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp
)
if regex.indexIn(text) == -1:
return False
return True
class myWindow(QtWidgets.QMainWindow):
# ...
def load_sites(self):
# ...
self.model = PandasModel(df)
self.proxy = CustomProxyModel(self)
self.proxy.setSourceModel(self.model)
self.view.setModel(self.proxy)
self.view.resizeColumnsToContents()
print("finished loading sites")
# ...
#QtCore.pyqtSlot()
def on_actionAll_triggered(self):
filterColumn = self.logicalIndex
self.proxy.setFilter("", filterColumn)
#QtCore.pyqtSlot(int)
def on_signalMapper_mapped(self, i):
stringAction = self.signalMapper.mapping(i).text()
filterColumn = self.logicalIndex
self.proxy.setFilter(stringAction, filterColumn)
#QtCore.pyqtSlot(str)
def on_lineEdit_textChanged(self, text):
self.proxy.setFilter(text, self.proxy.filterKeyColumn())
#QtCore.pyqtSlot(int)
def on_comboBox_currentIndexChanged(self, index):
self.proxy.setFilterKeyColumn(index)
Plus:
If you want to change the font of the QHeaderView then you must return the font in the headerData as shown below:
class PandasModel(QtCore.QAbstractTableModel):
def __init__(self, df=pd.DataFrame(), parent=None):
QtCore.QAbstractTableModel.__init__(self, parent=parent)
self._df = df.copy()
self.bolds = dict()
def toDataFrame(self):
return self._df.copy()
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal:
if role == QtCore.Qt.DisplayRole:
try:
return self._df.columns.tolist()[section]
except (IndexError,):
return QtCore.QVariant()
elif role == QtCore.Qt.FontRole:
return self.bolds.get(section, QtCore.QVariant())
elif orientation == QtCore.Qt.Vertical:
if role == QtCore.Qt.DisplayRole:
try:
# return self.df.index.tolist()
return self._df.index.tolist()[section]
except (IndexError,):
return QtCore.QVariant()
return QtCore.QVariant()
def setFont(self, section, font):
self.bolds[section] = font
self.headerDataChanged.emit(QtCore.Qt.Horizontal, 0, self.columnCount())
# ...
class myWindow(QtWidgets.QMainWindow):
# ...
#QtCore.pyqtSlot()
def on_actionAll_triggered(self):
filterColumn = self.logicalIndex
self.proxy.setFilter("", filterColumn)
font = QtGui.QFont()
self.model.setFont(filterColumn, font)
#QtCore.pyqtSlot(int)
def on_signalMapper_mapped(self, i):
stringAction = self.signalMapper.mapping(i).text()
filterColumn = self.logicalIndex
self.proxy.setFilter(stringAction, filterColumn)
font = QtGui.QFont()
font.setBold(True)
self.model.setFont(filterColumn, font)
How could I implement a QLineEdit in the column header for filtering data inside my QTreeView?
Expected Result something like this:
So far I've got this:
What I have so far is everything but the QLineEdit for the filter.
Full working code example (peewee ORM necessary):
import sys
import re
from peewee import *
from PyQt5 import QtWidgets, QtGui, QtCore
COUNT_PERS_COLS = 3
col_persID, col_persLAST_NAME, col_persFIRST_NAME = range(COUNT_PERS_COLS)
db = SqliteDatabase(':memory:')
def _human_key(key):
parts = re.split(r'(\d*\.\d+|\d+)', key)
return tuple((e.swapcase() if i % 2 == 0 else float(e))
for i, e in enumerate(parts))
class FilterHeader(QtWidgets.QHeaderView):
filterActivated = QtCore.pyqtSignal()
def __init__(self, parent):
super().__init__(QtCore.Qt.Horizontal, parent)
self._editors = []
self._padding = 4
self.setStretchLastSection(True)
# self.setResizeMode(QHeaderView.Stretch) obsolete
self.setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
self.setDefaultAlignment(
QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
self.setSortIndicatorShown(False)
self.sectionResized.connect(self.adjustPositions)
parent.horizontalScrollBar().valueChanged.connect(
self.adjustPositions)
def setFilterBoxes(self, count):
while self._editors:
editor = self._editors.pop()
editor.deleteLater()
for index in range(count):
editor = QtWidgets.QLineEdit(self.parent())
editor.setPlaceholderText('Filter')
editor.returnPressed.connect(self.filterActivated.emit)
self._editors.append(editor)
self.adjustPositions()
def sizeHint(self):
size = super().sizeHint()
if self._editors:
height = self._editors[0].sizeHint().height()
size.setHeight(size.height() + height + self._padding)
return size
def updateGeometries(self):
if self._editors:
height = self._editors[0].sizeHint().height()
self.setViewportMargins(0, 0, 0, height + self._padding)
else:
self.setViewportMargins(0, 0, 0, 0)
super().updateGeometries()
self.adjustPositions()
def adjustPositions(self):
for index, editor in enumerate(self._editors):
height = editor.sizeHint().height()
editor.move(
self.sectionPosition(index) - self.offset() + 2,
height + (self._padding // 2))
editor.resize(self.sectionSize(index), height)
def filterText(self, index):
if 0 <= index < len(self._editors):
return self._editors[index].text()
return ''
def setFilterText(self, index, text):
if 0 <= index < len(self._editors):
self._editors[index].setText(text)
def clearFilters(self):
for editor in self._editors:
editor.clear()
class HumanProxyModel(QtCore.QSortFilterProxyModel):
def lessThan(self, source_left, source_right):
data_left = source_left.data()
data_right = source_right.data()
if type(data_left) == type(data_right) == str:
return _human_key(data_left) < _human_key(data_right)
return super(HumanProxyModel, self).lessThan(source_left, source_right)
class Person(Model):
persId = CharField()
lastName = CharField()
firstName = CharField()
class Meta:
database = db
class winMain(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi()
self.setGeometry(300,200,700,500)
self.show()
def createPersonModel(self,parent):
model = QtGui.QStandardItemModel(0, COUNT_PERS_COLS, parent)
#model.setHeaderData(col_persID, QtCore.Qt.Horizontal, "ID")
#model.setHeaderData(col_persLAST_NAME, QtCore.Qt.Horizontal, "Last Name")
#model.setHeaderData(col_persFIRST_NAME, QtCore.Qt.Horizontal, "First Name")
#model.setHorizontalHeaderLabels('One Two Three' .split())
model.setHorizontalHeaderLabels(['ID', 'Last Name', 'First Name'])
return model
def addPerson(self, model, id, last_name, first_name):
model.insertRow(0)
model.setData(model.index(0, col_persID), id)
model.setData(model.index(0, col_persLAST_NAME), last_name)
model.setData(model.index(0, col_persFIRST_NAME), first_name)
def handleFilterActivated(self):
#header = self.tableView.horizontalHeader() # QTableView()-command
header = self.treeView.header()
for index in range(header.count()):
print((index, header.filterText(index)))
def setupUi(self):
self.treeView = QtWidgets.QTreeView(self)
self.treeView.setGeometry(0,0,700,500)
self.treeView.setSortingEnabled(True)
self.treeView.setAlternatingRowColors(True)
self.treeView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.treeView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.treeView.setAnimated(True)
self.treeView.setItemsExpandable(True)
#layout = QtWidgets.QVBoxLayout(self)
#layout.addWidget(self.treeView)
header = FilterHeader(self.treeView)
# self.tableView.setHorizontalHeader(header) # QTableView()-command
self.treeView.setHeader(header)
model = self.createPersonModel(self)
self.treeView.setModel(model)
proxy = HumanProxyModel(self)
proxy.setSourceModel(model)
self.treeView.setModel(proxy)
header.setFilterBoxes(model.columnCount())
header.filterActivated.connect(self.handleFilterActivated)
for rec_person in Person.select():
self.addPerson(model, rec_person.persId, rec_person.lastName, rec_person.firstName)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
# create a table for our model
db.create_tables([Person])
# create some sample data for our model
Person.create(persId='1001', lastName='Martin', firstName='Robert')
Person.create(persId='1002', lastName='Smith', firstName='Brad')
Person.create(persId='1003', lastName='Smith', firstName='Angelina')
window = winMain()
sys.exit(app.exec_())
I already use the QSortFilterProxyModel class for sorting the result by clicking into the column header.
You have to override the filterAcceptsRow method by implementing the filtering logic, in the following example it will be filtered using the following rules:
Only the columns that have a different text to the vacuum are considered.
The filtering criterion is that the text placed in the QLineEdit is contained in the corresponding row and column
class HumanProxyModel(QtCore.QSortFilterProxyModel):
# ...
#property
def filters(self):
if not hasattr(self, "_filters"):
self._filters = []
return self._filters
#filters.setter
def filters(self, filters):
self._filters = filters
self.invalidateFilter()
def filterAcceptsRow(self, sourceRow, sourceParent):
for i, text in self.filters:
if 0 <= i < self.columnCount():
ix = self.sourceModel().index(sourceRow, i, sourceParent)
data = ix.data()
if text not in data:
return False
return True
# ...
class winMain(QtWidgets.QMainWindow):
# ...
def handleFilterActivated(self):
header = self.treeView.header()
filters = []
for i in range(header.count()):
text = header.filterText(i)
if text:
filters.append((i, text))
proxy = self.treeView.model()
proxy.filters = filters