Having trouble trying to figure out how to catch a row in a QTableView that's being edited has been canceled. For example, if I am editing a newly inserted row in a QTableView and the ESC, up/down arrows keys have been pressed, I need to remove the row because (in my mind) has been cancelled. Also holds true if the user clicks away from the row. I can't really post any code as I have no idea how to implement something like this. Any ideas?
I have a an example of, what I believe, is what you want (at least for key pressed issue). From there you can do something similar for the clicking issue.
My solution uses a custom QItemDelegate that overrides the eventFilter method. Also it uses a naive model (because you want to use QTableView), the use of the layoutChanged signal on the model is due to functionality of the example, read the docs for more suitable add/delete data features according to your needs.
Hope it helps.
The sample ui:
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'test.ui',
# licensing of 'test.ui' applies.
#
# Created: Wed Nov 7 16:10:12 2018
# by: pyside2-uic running on PySide2 5.11.0
#
# WARNING! All changes made in this file will be lost!
from PySide2 import QtCore, QtGui, QtWidgets
class Ui_Test(object):
def setupUi(self, Test):
Test.setObjectName("Test")
Test.resize(538, 234)
self.horizontalLayout = QtWidgets.QHBoxLayout(Test)
self.horizontalLayout.setObjectName("horizontalLayout")
self.gridLayout = QtWidgets.QGridLayout()
self.gridLayout.setObjectName("gridLayout")
self.tableView = QtWidgets.QTableView(Test)
self.tableView.setObjectName("tableView")
self.gridLayout.addWidget(self.tableView, 0, 0, 1, 1)
self.addRow = QtWidgets.QPushButton(Test)
self.addRow.setObjectName("addRow")
self.gridLayout.addWidget(self.addRow, 0, 1, 1, 1)
self.horizontalLayout.addLayout(self.gridLayout)
self.retranslateUi(Test)
QtCore.QMetaObject.connectSlotsByName(Test)
def retranslateUi(self, Test):
Test.setWindowTitle(QtWidgets.QApplication.translate("Test", "Dialog", None, -1))
self.addRow.setText(QtWidgets.QApplication.translate("Test", "add row", None, -1))
The actual classes involved (I use PySide2):
from PySide2 import QtWidgets, QtCore, QtGui
from _test import Ui_Test
class MyDialog(QtWidgets.QDialog):
def __init__(self, parent = None):
super(MyDialog, self).__init__(parent = parent)
self.ui = Ui_Test()
self.ui.setupUi(self)
self._model = MyModel([["first row 1 col", "first row 2"],["second row 1", "second row 2"]])
self.ui.tableView.setModel(self._model)
self.ui.addRow.clicked.connect(self._model.addRow)
self.ui.tableView.setItemDelegate(MyDelegate(self.ui.tableView))
# this is crucial: we need to be sure that the selection is single on the view
self.ui.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectItems)
self.ui.tableView.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
class MyModel(QtCore.QAbstractTableModel):
def __init__(self, table_data = None, parent = None):
super(MyModel, self).__init__(parent = parent)
if not table_data: self._data = []
self._data = table_data
def rowCount(self, parent = None):
return len(self._data)
def columnCount(self, parent = None):
return 2
def addRow(self):
self._data.append(["new item", "new item"])
self.layoutChanged.emit()
def removeRow(self, row):
if 0 <= row < self.rowCount():
del self._data[row]
self.layoutChanged.emit()
def data(self, index, role = QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
row = index.row()
col = index.column()
return self._data[row][col]
def setData(self, index, value, role = QtCore.Qt.EditRole):
if index.isValid():
if role == QtCore.Qt.EditRole:
row = index.row()
col = index.column()
self._data[row][col] = str(value)
return True
else:
return False
else:
return False
def flags(self, index):
return QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsEditable|QtCore.Qt.ItemIsEnabled
class MyDelegate(QtWidgets.QItemDelegate):
def __init__(self, parent = None):
super(MyDelegate, self).__init__(parent)
self.view = parent
def eventFilter(self, editor, event):
# there is a lot of checking in order to identify the desired situation
# and avoid errors
if isinstance(event, QtGui.QKeyEvent):
if event.type() == QtCore.QEvent.KeyPress:
if event.key() == QtCore.Qt.Key_Escape:
# we should have a list here of length one (due to selection restrictions on the view)
index = self.view.selectedIndexes()
if index:
if index[0].isValid():
row = index[0].row()
self.view.model().removeRow(row)
return super(MyDelegate, self).eventFilter(editor, event)
if __name__ == '__main__':
app = QtWidgets.QApplication()
diag = MyDialog()
diag.show()
app.exec_()
Related
I'm trying to create a pretty simple app with Pyside2 that displays some data in a table that the user can edit. I want the cells of the table to autocomplete based on other values in the table, so I implemented a custom delegate following this tutorial. It was pretty straightforward and the delegate works as expected except for some reason the delegate pops up in a new window instead of being attached to the cell in the table, illustrated by this example (the custom delegate is defined at the end):
import pandas as pd
from PySide2 import QtWidgets, QtCore
from PySide2.QtCore import Qt
class TableView(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setObjectName(u"TableView")
self.centralwidget = QtWidgets.QWidget(self)
self.centralwidget.setObjectName(u"centralwidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setObjectName(u"verticalLayout")
self.frame = QtWidgets.QFrame(self.centralwidget)
self.frame.setObjectName(u"frame")
self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
self.frame.setFrameShadow(QtWidgets.QFrame.Raised)
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.frame)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.tableView = QtWidgets.QTableView(self.frame)
self.tableView.setObjectName(u"tableView")
delegate = QAutoCompleteDelegate(self.tableView)
self.tableView.setItemDelegate(delegate)
self.tableModel = TableModel(self.tableView)
self.tableView.setModel(self.tableModel)
self.verticalLayout_2.addWidget(self.tableView)
self.verticalLayout.addWidget(self.frame)
self.setCentralWidget(self.centralwidget)
self.statusbar = QtWidgets.QStatusBar(self)
self.statusbar.setObjectName(u"statusbar")
self.setStatusBar(self.statusbar)
QtCore.QMetaObject.connectSlotsByName(self)
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, parent=None):
super(TableModel, self).__init__(parent=parent)
self._data = pd.DataFrame([["A0", "B0", "C0"], ["A1", "B1", "C1"], ["A2", "B2", "C2"]], columns=["A", "B", "C"])
def data(self, index, role):
# Should only ever refer to the data by iloc in this method, unless you
# go specifically fetch the correct loc based on the iloc
row = index.row()
col = index.column()
if role == Qt.DisplayRole or role == Qt.EditRole:
return self._data.iloc[row, col]
def rowCount(self, index):
return len(self._data)
def columnCount(self, index):
return len(self._data.columns)
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self._data.columns[col]
return None
def unique_vals(self, index):
""" Identify the values to include in an autocompletion delegate for the given index """
column = index.column()
col_name = self._data.columns[column]
return list(self._data[col_name].unique())
def setData(self, index, value, role):
if role != Qt.EditRole:
return False
row = self._data.index[index.row()]
column = self._data.columns[index.column()]
self._data.loc[row, column] = value
self.dataChanged.emit(index, index)
return True
def flags(self, index):
flags = super(self.__class__, self).flags(index)
flags |= Qt.ItemIsEditable
flags |= Qt.ItemIsSelectable
flags |= Qt.ItemIsEnabled
flags |= Qt.ItemIsDragEnabled
flags |= Qt.ItemIsDropEnabled
return flags
class QAutoCompleteDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex) -> QtWidgets.QWidget:
le = QtWidgets.QLineEdit()
test = ["option1", "option2", "option3"]
complete = QtWidgets.QCompleter(test)
le.setCompleter(complete)
return le
def setEditorData(self, editor:QtWidgets.QLineEdit, index:QtCore.QModelIndex):
val = index.model().data(index, Qt.EditRole)
options = index.model().unique_vals(index)
editor.setText(val)
completer = QtWidgets.QCompleter(options)
editor.setCompleter(completer)
def updateEditorGeometry(self, editor: QtWidgets.QWidget, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex):
editor.setGeometry(option.rect)
app = QtWidgets.QApplication()
tv = TableView()
tv.show()
app.exec_()
Image showing table behavior when a cell is active
From everything I've read, the updateEditorGeometry method on the custom delegate should anchor it to the active cell, but something clearly isn't working as expected in my case. And I am still learning Qt so I can't claim to understand what it is doing. Is there anyone out there who might be able to explain what is going on here? Much appreciated.
OS: Ubuntu 20.04
Python 3.8.5
PySide2 5.15.0
Qt 5.12.9
Any widget that does not have a parent will be a window and since the QLineEdit has no parent then you observe the behavior that the OP indicates, for that reason the createEditor method has "parent" as an argument.
On the other hand, it is better to create a custom QLineEdit that easily implements the handling of adding suggestions to the QCompleter through a model.
class AutoCompleteLineEdit(QtWidgets.QLineEdit):
def __init__(self, parent=None):
super().__init__(parent)
self._completer_model = QtGui.QStandardItemModel()
completer = QtWidgets.QCompleter()
completer.setModel(self._completer_model)
self.setCompleter(completer)
#property
def suggestions(self):
return [
self._completer_model.item(i).text()
for i in range(self._completer_model.rowCount())
]
#suggestions.setter
def suggestions(self, suggestions):
self._completer_model.clear()
for suggestion in suggestions:
item = QtGui.QStandardItem(suggestion)
self._completer_model.appendRow(item)
class QAutoCompleteDelegate(QtWidgets.QStyledItemDelegate):
def createEditor(
self,
parent: QtWidgets.QWidget,
option: QtWidgets.QStyleOptionViewItem,
index: QtCore.QModelIndex,
) -> QtWidgets.QWidget:
le = AutoCompleteLineEdit(parent)
return le
def setEditorData(self, editor: QtWidgets.QWidget, index: QtCore.QModelIndex):
val = index.model().data(index, Qt.EditRole)
options = index.model().unique_vals(index)
editor.setText(val)
editor.suggestions = options
I am new to Qt. Currently I am trying to learn how to update a table model from a different thread and then how to get an immediate display update for it. I read the documentation and found the dataChanged() and layoutChanged() signals. While dataChanged() works fine, any attempt to emit layoutChanged() fails with:
'QObject::connect: Cannot queue arguments of type 'QList<QPersistentModelIndex>' (Make sure 'QList<QPersistentModelIndex>' is registered using qRegisterMetaType().)
Searching for this particular error didn't give me anything that I could turn into working code. I am not using any QList or QPersistentModelIndex explicitly, but of course that can be implicitly used due to the constructs that I chose.
What am I doing wrong?
class TimedModel(QtCore.QAbstractTableModel):
def __init__(self, table, view):
super(TimedModel, self).__init__()
self.table = table
self.view = view
self.setHeaderData(0, Qt.Horizontal, Qt.AlignLeft, Qt.TextAlignmentRole)
self.rows = 6
self.columns = 4
self.step = 5
self.timer = Thread(
name = "Timer",
target = self.tableTimer,
daemon = True)
self.timer.start()
self.random = Random()
self.updated = set()
#staticmethod
def encode(row, column):
return row << 32 | column
def data(self, index, role):
if role == Qt.DisplayRole or role == Qt.EditRole:
return f'Data-{index.row()}-{index.column()}'
if role == Qt.ForegroundRole:
encoded = TimedModel.encode(index.row(), index.column())
return QBrush(Qt.red if encoded in self.updated else Qt.black)
return None
def rowCount(self, index):
return self.rows
def columnCount(self, index):
return self.columns
def headerData(self, col, orientation, role):
if orientation == Qt.Vertical:
# Vertical
return super().headerData(col, orientation, role)
# Horizontal
if not 0 <= col < self.columns:
return None
if role == Qt.DisplayRole:
return f'Data-{col}'
if role == Qt.TextAlignmentRole:
return int(Qt.AlignLeft | Qt.AlignVCenter)
return super().headerData(col, orientation, role)
def tableTimer(self):
while True:
time.sleep(5.0)
randomRow = self.random.randint(0, self.rows)
randomColumn = self.random.randint(0, self.columns)
encodedRandom = TimedModel.encode(randomRow, randomColumn)
if encodedRandom in self.updated:
self.updated.remove(encodedRandom)
else:
self.updated.add(encodedRandom)
updatedIndex = self.createIndex(randomRow, randomColumn)
self.dataChanged.emit(updatedIndex, updatedIndex)
'''this here does not work:'''
self.layoutAboutToBeChanged.emit()
self.rows += self.step
self.layoutChanged.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
QtWidgets.QMainWindow.__init__(self)
self.timedTable = QTableView()
self.model = TimedModel(self.timedTable, self)
self.timedTable.setModel(self.model)
headerView = self.timedTable.horizontalHeader()
headerView.setStretchLastSection(True)
self.setCentralWidget(self.timedTable)
self.setGeometry(300, 300, 1000, 600)
self.setWindowTitle('Timed Table')
self.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.name = "Timed Table Application"
window = MainWindow()
window.show()
app.exec_()
The following code:
self.layoutAboutToBeChanged.emit()
self.rows += self.step
self.layoutChanged.emit()
create new model elements that have QPersistentModelIndex associated that are not thread-safe and that Qt monitors its creation to warn its misuse as in this case since modifying that element is unsafe since it implies modifying the GUI from another thread (Read here for more information).
So you see that message warning that what you are trying to do is unsafe.
Instead dataChanged only emits a signal, does not create any element belonging to Qt, and you have been lucky that the modification of "self.updated" has not generated bottlenecks since you modify a property that belongs to the main thread from a secondary thread without use guards as mutexes.
Qt points out that the GUI and the elements that the GUI uses should only be updated in the GUI thread, and if you want to modify the GUI with information from another thread, then you must send that information, for example, using the signals that are thread- safe:
import random
import sys
import threading
import time
from PySide2 import QtCore, QtGui, QtWidgets
class TimedModel(QtCore.QAbstractTableModel):
random_signal = QtCore.Signal(object)
def __init__(self, table, view):
super(TimedModel, self).__init__()
self.table = table
self.view = view
self.setHeaderData(
0, QtCore.Qt.Horizontal, QtCore.Qt.AlignLeft, QtCore.Qt.TextAlignmentRole
)
self.rows = 6
self.columns = 4
self.step = 5
self.updated = set()
self.random_signal.connect(self.random_slot)
self.timer = threading.Thread(name="Timer", target=self.tableTimer, daemon=True)
self.timer.start()
#staticmethod
def encode(row, column):
return row << 32 | column
def data(self, index, role):
if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole):
return f"Data-{index.row()}-{index.column()}"
if role == QtCore.Qt.ForegroundRole:
encoded = TimedModel.encode(index.row(), index.column())
return QtGui.QBrush(
QtCore.Qt.red if encoded in self.updated else QtCore.Qt.black
)
return None
def rowCount(self, index):
return self.rows
def columnCount(self, index):
return self.columns
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Vertical:
# Vertical
return super().headerData(col, orientation, role)
# Horizontal
if not 0 <= col < self.columns:
return None
if role == QtCore.Qt.DisplayRole:
return f"Data-{col}"
if role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
return super().headerData(col, orientation, role)
def tableTimer(self):
while True:
time.sleep(5.0)
randomRow = random.randint(0, self.rows)
randomColumn = random.randint(0, self.columns)
encodedRandom = TimedModel.encode(randomRow, randomColumn)
self.random_signal.emit(encodedRandom)
#QtCore.Slot(object)
def random_slot(self, encodedRandom):
if encodedRandom in self.updated:
self.updated.remove(encodedRandom)
else:
self.updated.add(encodedRandom)
self.layoutAboutToBeChanged.emit()
self.rows += self.step
self.layoutChanged.emit()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.timedTable = QtWidgets.QTableView()
self.model = TimedModel(self.timedTable, self)
self.timedTable.setModel(self.model)
headerView = self.timedTable.horizontalHeader()
headerView.setStretchLastSection(True)
self.setCentralWidget(self.timedTable)
self.setGeometry(300, 300, 1000, 600)
self.setWindowTitle("Timed Table")
self.show()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
app.name = "Timed Table Application"
window = MainWindow()
window.show()
app.exec_()
I'm trying to implement the QAbstractListModel class in order to display several similar widgets.
The following code shows my problem:
import sys
from PyQt4 import QtCore
from PyQt4 import QtGui
class Model(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super(QtCore.QAbstractListModel, self).__init__(parent)
self._widgets = []
def headerData(self, section, orientation, role):
""" Returns header for columns """
return "bla"
def rowCount(self, parentIndex=QtCore.QModelIndex()):
""" Returns number of interfaces """
return len(self._widgets)
def data(self, index, role):
""" Returns the data to be displayed """
if role == QtCore.Qt.DisplayRole:
row = index.row()
return self._widgets[row]
def insertRow(self, widget, parentIndex=QtCore.QModelIndex()):
""" Inserts a row into the model """
self.beginInsertRows(parentIndex, 0, 1)
self._widgets.append(widget)
self.endInsertRows()
class Widget(QtGui.QWidget):
def __init__(self, parent=None, name="None"):
super(QtGui.QWidget, self).__init__(parent)
self.layout = QtGui.QHBoxLayout()
self.setLayout(self.layout)
self.checkbox = QtGui.QCheckBox()
self.button = QtGui.QPushButton(self)
self.label = QtGui.QLabel(self)
self.label.setText(name)
self.layout.addWidget(self.checkbox)
self.layout.addWidget(self.button)
self.layout.addWidget(self.label)
class Window(QtGui.QMainWindow):
def __init__(self, parent=None):
super(QtGui.QMainWindow, self).__init__(parent)
self.view = QtGui.QListView(self)
self.model = Model()
self.view.setModel(self.model)
self.setCentralWidget(self.view)
self.model.insertRow(
widget=Widget(self)
)
self.model.insertRow(
widget=Widget(self)
)
self.model.insertRow(
widget=Widget(self)
)
self.model.insertRow(
widget=Widget(self)
)
self.show()
app = QtGui.QApplication(sys.argv)
window = Window()
sys.exit(app.exec_())
It works, there are four clickable entries in the list, but in only one of them the widget is actually displayed. Why is that?
I guess this behaviour is either caused by data() or how I'm using beginInsertRows(), but I can't figure out where the error is.
ìnsertRow() function now looks like that
def insertRow(self, widget, parentIndex=QtCore.QModelIndex()):
""" Inserts a row into the model """
self.beginInsertRows(parentIndex, len(self._widgets), len(self._widgets))
self._widgets.append(widget)
self.endInsertRows()
but it still does not work.
Actually, the four widgets are displayed. The issue is, they're all displayed in the top left corner. You can see they overlap if you put names, here "1", "2", "3", "4":
Fixing the row numbers doesn't solve the issue. The widgets will be in the correct rows but will still be displayed on the top left. It is because data is supposed to return text for the display role.
To display simple widgets, I suggest using QListWidget and the setItemWidget method. Otherwise you'll have to use delegates.
self.beginInsertRows(parentIndex, 0, 1)
0 - start
1 - end
You refresh only first row during insert
self.beginInsertRows(parentIndex, len(self._widgets), len(self._widgets))
must be work. Don't remember this method
self.model.reset()
this refresh all list, without specific rows
I am fairly new to PyQt, I'm working on a project that contains a QTableView, with one of its columns displaying system paths. I would like to add a QTreeView so users can click the + or > buttons to expand what is underneath the paths.
Here is my basic implementation:
from PyQt4 import QtGui
from PyQt4 import QtCore
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.resize(600,400)
self.setWindowTitle("My Basic Treeview")
self.treeview = QtGui.QTreeView(self)
self.treeview.model = QtGui.QFileSystemModel()
self.treeview.model.setRootPath('/opt')
self.treeview.setModel(self.treeview.model)
self.treeview.setColumnWidth(0, 200)
self.setCentralWidget(self.treeview)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Although, in the above case, I get all folders but I just want the /opt path and its underneath folders.
import operator
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class MyWindow(QWidget):
def __init__(self, data_list, header, *args):
QWidget.__init__(self, *args)
# setGeometry(x_pos, y_pos, width, height)
self.setGeometry(300, 200, 570, 450)
self.setWindowTitle("Click on column title to sort")
table_model = MyTableModel(self, data_list, header)
table_view = QTableView()
table_view.setModel(table_model)
# set font
font = QFont("Courier New", 14)
table_view.setFont(font)
# set column width to fit contents (set font first!)
table_view.resizeColumnsToContents()
# enable sorting
table_view.setSortingEnabled(True)
layout = QVBoxLayout(self)
layout.addWidget(table_view)
self.setLayout(layout)
class MyTableModel(QAbstractTableModel):
def __init__(self, parent, mylist, header, *args):
QAbstractTableModel.__init__(self, parent, *args)
self.mylist = mylist
self.header = header
def rowCount(self, parent):
return len(self.mylist)
def columnCount(self, parent):
return len(self.mylist[0])
def data(self, index, role):
if not index.isValid():
return None
elif role != Qt.DisplayRole:
return None
return self.mylist[index.row()][index.column()]
def headerData(self, col, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
return self.header[col]
return None
# the solvent data ...
header = ['Name', ' Email', ' Status', ' Path']
# use numbers for numeric data to sort properly
data_list = [
('option_A', 'zyro#email.com', 'Not Copied', '/Opt'),
('option_B', 'zyro#email.com', 'Not Copied', '/Users'),
]
app = QApplication([])
win = MyWindow(data_list, header)
win.show()
app.exec_()
Visual example :
I think your question can be divided in two parts:
how, in a QTreeView, the /opt path and its children can be shown, but without showing its siblings. In other words, how is it possible to show the root directory in a QTreeView ;
how can a QTreeView be added to a QTableView.
1. How to include the root directory in a QTreeView :
The root of a QTreeView is the directory for which the content is shown in the view. It is set when calling the method setRootIndex. According to a post by wysota on Qt Centre:
You can't display the invisibleRootItem because it is a fake item used only to have an equivalent of empty QModelIndex.
A workaround would be to set the root directory to the parent of /opt and filtering out the siblings of /opt with a subclass of a QSortFilterProxyModel. Note that I've also reimplemented the sizeHint method which will be necessary for the resizing of the rows of the QTableView:
from PyQt4 import QtGui, QtCore
import os
class MyQTreeView(QtGui.QTreeView):
def __init__(self, path, parent=None):
super(MyQTreeView, self).__init__(parent)
ppath = os.path.dirname(path) # parent of path
self.setFrameStyle(0)
#---- File System Model ----
sourceModel = QtGui.QFileSystemModel()
sourceModel.setRootPath(ppath)
#---- Filter Proxy Model ----
proxyModel = MyQSortFilterProxyModel(path)
proxyModel.setSourceModel(sourceModel)
#---- Filter Proxy Model ----
self.setModel(proxyModel)
self.setHeaderHidden(True)
self.setRootIndex(proxyModel.mapFromSource(sourceModel.index(ppath)))
#--- Hide All Header Sections Except First ----
header = self.header()
for sec in range(1, header.count()):
header.setSectionHidden(sec, True)
def sizeHint(self):
baseSize = super(MyQTreeView,self).sizeHint()
#---- get model index of "path" ----
qindx = self.rootIndex().child(0, 0)
if self.isExpanded(qindx): # default baseSize height will be used
pass
else: # shrink baseShize height to the height of the row
baseSize.setHeight(self.rowHeight(qindx))
return baseSize
class MyQSortFilterProxyModel(QtGui.QSortFilterProxyModel):
def __init__(self, path, parent=None):
super(MyQSortFilterProxyModel, self).__init__(parent)
self.path = path
def filterAcceptsRow(self, row, parent):
model = self.sourceModel()
path_dta = model.index(self.path).data()
ppath_dta = model.index(os.path.dirname(self.path)).data()
if parent.data() == ppath_dta:
if parent.child(row, 0).data() == path_dta:
return True
else:
return False
else:
return True
2. How to add a *QTreeView* to a *QTableView* :
It is possible to add a QTreeView to a QTableView by using a QItemDelegate. The post by Pavel Strakhov greatly helped me for this, since I had never used QTableView in combination with delegates before answering to this question. I always used QTableWidget instead with the setCellWidget method.
Note that I've setup a signal in the MyDelegate class which call the method resizeRowsToContents in the MyTableView class. This way, the height of the rows resize according the the reimplementation of the sizeHint method of the MyQTreeView class.
class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self, parent, mylist, header, *args):
super(MyTableModel, self).__init__(parent, *args)
self.mylist = mylist
self.header = header
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.mylist)
def columnCount(self, parent=QtCore.QModelIndex()):
return len(self.mylist[0])
def data(self, index, role):
if not index.isValid():
return None
elif role != QtCore.Qt.DisplayRole:
return None
return self.mylist[index.row()][index.column()]
def headerData(self, col, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.header[col]
return None
class MyDelegate(QtGui.QItemDelegate):
treeViewHeightChanged = QtCore.pyqtSignal(QtGui.QWidget)
def createEditor(self, parent, option, index):
editor = MyQTreeView(index.data(), parent)
editor.collapsed.connect(self.sizeChanged)
editor.expanded.connect(self.sizeChanged)
return editor
def sizeChanged(self):
self.treeViewHeightChanged.emit(self.sender())
class MyTableView(QtGui.QTableView):
def __init__(self, data_list, header, *args):
super(MyTableView, self).__init__(*args)
#---- set up model ----
model = MyTableModel(self, data_list, header)
self.setModel(model)
#---- set up delegate in last column ----
delegate = MyDelegate()
self.setItemDelegateForColumn(3, delegate)
for row in range(model.rowCount()):
self.openPersistentEditor(model.index(row, 3))
#---- set up font and resize calls ----
self.setFont(QtGui.QFont("Courier New", 14))
self.resizeColumnsToContents()
delegate.treeViewHeightChanged.connect(self.resizeRowsToContents)
3. Basic application :
Here is a basic application based on the code you provided in your OP:
if __name__ == '__main__':
header = ['Name', ' Email', ' Status', ' Path']
data_list = [('option_A', 'zyro#email.com', 'Not Copied', '/opt'),
('option_B', 'zyro#email.com', 'Not Copied', '/usr')]
app = QtGui.QApplication([])
win = MyTableView(data_list, header)
win.setGeometry(300, 200, 570, 450)
win.show()
app.exec_()
Which results in:
I can find some examples of how to use standard model with standard view.
http://doc.qt.io/qt-5/modelview.html
http://doc.qt.io/qt-5/qtwidgets-itemviews-simplewidgetmapper-example.html
but I just can not find ONE example of how to use QAbstractModel make my own model and to use it on my own view/widget.
After I update the model the stand view will update but not my own view.
Full code:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from PyQt5.QtWidgets import (QWidget, QLabel, QDataWidgetMapper,
QLineEdit, QApplication, QGridLayout)
from PyQt5.QtCore import QAbstractListModel, Qt
from PyQt5.QtWidgets import QListView
class Window(QWidget):
def __init__(self, model, parent=None):
super(Window, self).__init__(parent)
self.model = model
# Set up the widgets.
nameLabel = QLabel("Na&me:")
nameEdit = QLineEdit()
# Set up the mapper.
self.mapper = QDataWidgetMapper(self)
self.mapper.setModel(self.model)
self.mapper.addMapping(nameEdit, 0)
layout = QGridLayout()
layout.addWidget(nameLabel, 0, 0, 1, 1)
layout.addWidget(nameEdit, 0, 1, 1, 1)
self.setLayout(layout)
self.mapper.toFirst()
class MyModel(QAbstractListModel):
def __init__(self, status=[], parent=None):
super().__init__(parent)
self.status = status
def rowCount(self, index_parent=None, *args, **kwargs):
return len(self.status)
def data(self, index, role=Qt.DisplayRole, parent=None):
if not index.isValid():
return None
row = index.row()
if row == 0:
print(index)
if role == Qt.DisplayRole:
return self.status[row]
elif role == Qt.EditRole: # if it's editing mode, return value for editing
return self.status[row]
def flags(self, index):
return Qt.ItemIsEnabled | Qt.ItemIsEditable
def setData(self, index, value='', role=Qt.EditRole):
row = index.row()
if role == Qt.EditRole:
self.status[row] = value
self.dataChanged.emit(index, index) # inform the other view to request new data
return True
else:
return False
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
myModel_on_mywindow = MyModel([1, 2, 3])
mywindow = Window(myModel_on_mywindow)
mywindow.setWindowTitle('myModel_on_mywindow')
mywindow.show()
myModel_on_mywindow.status[0] = 2
myModel_on_qlistview = MyModel([1, 2, 3])
qlistview = QListView()
qlistview.show()
qlistview.setModel(myModel_on_qlistview)
qlistview.setWindowTitle('myModel_on_qlistview')
myModel_on_qlistview.status[0] = 2
sys.exit(app.exec_())
Your custom model needs to override some functions of the abstract model, depending on which one you subclass. This is a small example for subclassing the QAbstractListModel. You can read more about this in the Qt documentation: QAbstractListModel Details
class MyModel(QAbstractListModel):
def __init__(parent=None):
QAbstractListModel.__init__(parent)
self.content = [] # holds the data, you want to present
def rowCount(index):
return len(self.content)
# This defines what the view should present at given index
def data(index, role):
if index.isValid() and role == Qt.DisplayRole):
dataElement = self.content[index.row()]
if index.colum() == 0:
return dataElement.someBoolField
if index.colum() == 1:
return dataElement.someIntField
# ...
}
return QVariant() # this is like returning nothing
}
# If your items should be editable. Automatically called, when user changes the item.
def setData(index, value, role):
if index.isValid():
dataElement = self.content[index.row()].get()
if index.column() == 0:
return dataElement.tryToSetBoolField(value.toBool())
if index.column() == 1:
return dataElement.tryToSetIntField(value.toInt())
# ...
}
return True
}
};
# connecting your view with your model
model = MyModel()
myView = QTreeView() # or something else
myView.setModel(model)
EDIT
To update your view when using a custom Model in combination with a QDataMapper, call setCurrentIndex(changedIndex) or its wrappers after changing the model, like so in your case:
myModel_on_mywindow.status[0] = 2
myModel.mapper.toFirst()
The Qt documentation on QDataMapper mentions this in the detailed description:
The navigational functions toFirst(), toNext(), toPrevious(), toLast() and setCurrentIndex() can be used to navigate in the model and update the widgets with contents from the model.