QListView Complexe Delegate editor focus to parent - python

I'm trying to make a QListView where each rows are represented has a complex widget.
I want to have a QLabel and a QTableView representing some data.
All good so far.
The problem is that when i click on the QTableView (which i disabled the focus policy), it doesn't select the row of the QListView.
But it's working when i click on the QLabel (which is also disabled focus policy)
Is there anything i do wrong ? My change_line method does get called when clicking on the "This is line" label but not on the table bellow it :(
I've tried to play with the FocusPolicy and the setFocusProxy in the editor, but so far can't make it work.
Thanks for the help
import sys
from typing import Any, Dict
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt, QAbstractTableModel, QSize
from PyQt5.QtMultimedia import QMediaMetaData
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QListView, QWidget, QStyledItemDelegate, \
QTableView, QHeaderView, QAbstractScrollArea, QLabel
class TableModel(QAbstractTableModel):
def __init__(self):
super().__init__()
self._data_list = []
#property
def data_list(self):
return self._data_list
#data_list.setter
def data_list(self, data_list):
self.beginResetModel()
self._data_list = data_list
self.endResetModel()
def headerData(self, section: int, orientation: QMediaMetaData.Orientation, role: int = ...) -> Any:
if role == Qt.DisplayRole:
if section == 0:
return "header"
def data(self, index: QModelIndex, role: int = ...) -> Any:
if index.column() == 0:
if role == Qt.DisplayRole:
return self.data_list[index.row()]
def rowCount(self, parent: QModelIndex = ..., *args, **kwargs) -> int:
return len(self.data_list)
def columnCount(self, parent: QModelIndex = ..., *args, **kwargs) -> int:
return 1
class ListModel(QAbstractListModel):
def __init__(self):
super().__init__()
def rowCount(self, parent=QModelIndex(), *args, **kwargs) -> int:
return 2
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
if index.row() == 0:
if role == Qt.DisplayRole:
return ["Hello", "World!"]
elif index.row() == 1:
if role == Qt.DisplayRole:
return ["One", "Two", "Three!"]
class ItemEditor(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.verticalLayout = QVBoxLayout(self)
self.line_label = QLabel("This is a line", self)
self.verticalLayout.addWidget(self.line_label)
self.table_view = QTableView(self)
self.table_view.setFocusPolicy(QtCore.Qt.NoFocus)
self.table_view.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.table_view.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.table_view.horizontalHeader().setVisible(False)
self.table_view.verticalHeader().setVisible(False)
self.verticalLayout.addWidget(self.table_view)
class StyledItemDelegate(QStyledItemDelegate):
def __init__(self, parent):
super(StyledItemDelegate, self).__init__(parent)
self.editors: Dict[int, QTableView] = {}
def sizeHint(self, option: 'QStyleOptionViewItem', index: QModelIndex) -> QSize:
if index.row() in self.editors.keys():
return QSize(min(self.parent().width(), self.editors[index.row()].sizeHint().width()),
self.editors[index.row()].sizeHint().height())
else:
return super(StyledItemDelegate, self).sizeHint(option, index)
def createEditor(self, parent, option, index):
editor = ItemEditor(parent)
editor.setFocusProxy(parent)
editor.table_view.setModel(TableModel())
editor.table_view.setSizeAdjustPolicy(QAbstractScrollArea.AdjustToContents)
header = editor.table_view.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.Stretch)
return editor
def setEditorData(self, editor: QTableView, index):
editor.table_view.model().data_list = index.data()
self.editors[index.row()] = editor
def change_line(index: QModelIndex):
# FIXME Only working when clicking on the "editor" widget or the QLabel, not on the Table
print(f"Changing to line {index.row()}")
if __name__ == "__main__":
app = QApplication([])
main_window = QMainWindow()
centralwidget = QWidget(main_window)
verticalLayout = QVBoxLayout(centralwidget)
listView = QListView(centralwidget)
listView.clicked.connect(change_line)
verticalLayout.addWidget(listView)
main_window.setCentralWidget(centralwidget)
listModel = ListModel()
listView.setModel(listModel)
delegate = StyledItemDelegate(listView)
listView.setItemDelegate(delegate)
for i in range(listModel.rowCount()):
index = listModel.index(i, 0)
listView.openPersistentEditor(index)
main_window.resize(600,400)
main_window.show()
sys.exit(app.exec_())

Since no mouse interaction is required, a possibility is to make the table "transparent" to mouse events:
self.table_view.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
Note that by default the editor doesn't change the size hint of the cell but the other way around, so you should emit sizeHintChanged when creating the editor, and you should add the editor to the dictionary in createEditor, not in setEditorData.
self.editors[index.row()] = editor
QtCore.QTimer.singleShot(0, lambda: self.sizeHintChanged.emit(index))
return editor

Related

Change Mousepointer and font color by entering a cell in Qtableview

I tried something out with QTableview and want that the mousepointer changed to "hand" and font color changed to blue if I enter a specific cell in QTableview. Is that possible? So the cell should be use like a button without being a button.
I provide you a demo, maybe you want this.
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import numpy as np
class Model(QAbstractTableModel):
def __init__(self):
super().__init__()
self._data = np.random.randint(0, 255, (5, 10))
self._color = dict()
def rowCount(self, parent: QModelIndex = ...) -> int:
return self._data.shape[0]
def columnCount(self, parent: QModelIndex = ...) -> int:
return self._data.shape[1]
def data(self, index: QModelIndex, role: int = ...) :
if index.isValid() and role == Qt.DisplayRole:
return str(self._data[index.row(), index.column()])
if index.isValid() and role == Qt.ForegroundRole:
if f"{index.row(),index.column()}" in self._color:
return self._color[f"{index.row(),index.column()}"]
def setData(self, index: QModelIndex, value, role: int = ...) -> bool:
if index.isValid() and role == Qt.ForegroundRole:
self._color = dict()
self._color[f"{index.row(),index.column()}"] = value
self.dataChanged.emit(self.index(0,0), self.index(self.rowCount()-1, self.columnCount() -1))
return True
return super().setData(index, value, role)
class View(QTableView):
def mouseMoveEvent(self, e: QMouseEvent) -> None:
index = self.indexAt(e.pos())
if index.isValid():
self.model().setData(index, QBrush(Qt.blue), Qt.ForegroundRole)
self.setCursor(Qt.PointingHandCursor)
else:
self.setCursor(Qt.ArrowCursor)
return super().mouseMoveEvent(e)
app = QApplication([])
v = View()
v.setMouseTracking(True)
v.setModel(Model())
v.show()
app.exec()
Such "visual" hints should never modify the underlying model, and should always be managed by the item delegate instead.
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class ItemDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
self.highlightedCells = []
def setHighlightedCells(self, cells):
self.highlightedCells.clear()
for row, column in cells:
self.highlightedCells.append((row, column))
def initStyleOption(self, option, index):
super().initStyleOption(option, index)
if not (index.row(), index.column()) in self.highlightedCells:
return
if option.state & QStyle.State_MouseOver:
option.palette.setColor(QPalette.Text, Qt.blue)
def editorEvent(self, event, model, option, index):
if event.type() == event.MouseMove:
if (index.row(), index.column()) in self.highlightedCells:
option.widget.viewport().setCursor(Qt.PointingHandCursor)
else:
option.widget.viewport().unsetCursor()
return super().editorEvent(event, model, option, index)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
table = QTableWidget(5, 5)
for r in range(5):
for c in range(5):
table.setItem(r, c,
QTableWidgetItem('{} {}'.format(r + 1, c + 1)))
# required for updates on mouse movements (especially the cursor)
table.setMouseTracking(True)
delegate = ItemDelegate(table)
table.setItemDelegate(delegate)
# the following is for the fourth column of the first 3 rows
delegate.setHighlightedCells([(0, 3), (1, 3), (2, 3)])
table.show()
sys.exit(app.exec_())

PyQt5: QTableView drag and drop cells not rows

I know how to drag rows
QTableView.verticalHeader().setSectionsMovable(True)
QTableView.verticalHeader().setDragEnabled(True)
QTableView.verticalHeader().setDragDropMode(qtw.QAbstractItemView.InternalMove)
but I want to be able to drag and drop just a single (or a pair of) cell.
Can anyone point me in the right direction?
PS: my current idea would be to intercept the clicked() -> dragEnter() -> dragLeave() or fork to dragDrop() -> dragIndicatorPosition()
events, but it sounds kinda convoluted
I did read this but I am confused on how to implement it, especially the section "Enabling drag and drop for items" seems to address exactly what I need. I'll see if I can post a "slim" example.
EDIT:
here an example with some other stuff. in MyStandardItemModel I try to do the trick:
from PyQt5 import QtCore, QtWidgets, QtSql
from PyQt5.QtCore import QModelIndex
from PyQt5.QtGui import QStandardItemModel
from PyQt5.QtWidgets import QApplication, QTableView, QTableWidget
class MyStandardItemModel(QStandardItemModel):
def __init__(self, parent=None, *arg, **kwargs):
super().__init__(parent, *arg, **kwargs)
self.__readonly_cols = []
def flags(self, index: QtCore.QModelIndex) -> QtCore.Qt.ItemFlags:
try:
default_Flags = QStandardItemModel.flags(self,index)
if (index.column() in self.__readonly_cols):
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
else:
if (index.isValid()):
return default_Flags | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled
else:
return default_Flags
except Exception as ex:
print(ex)
def setReadOnly(self, columns: [int]):
for i in columns:
if i <= (self.columnCount() - 1) and i not in self.__readonly_cols:
self.__readonly_cols.append(i)
def resetReadOnly(self):
self.__readonly_cols = []
class MyTableView(QtWidgets.QTableView):
def __init__(self, parent=None, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
class CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
# signal to inform clicking
user_click = QtCore.pyqtSignal(int, int, bool)
def __init__(self, parent):
QtWidgets.QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
"""
Important, otherwise an editor is created if the user clicks in this cell.
"""
return None
def paint(self, painter, option, index):
"""
Paint a checkbox without the label.
"""
self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked)
def editorEvent(self, event, model, option, index):
'''
Change the data in the model and the state of the checkbox
if the user presses the left mousebutton and this cell is editable. Otherwise do nothing.
'''
if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0:
return False
if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton:
# Change the checkbox-state
self.setModelData(None, model, index)
return True
# if event.type() == QtCore.QEvent.MouseButtonPress or event.type() == QtCore.QEvent.MouseMove:
# return False
return False
def setModelData (self, editor, model, index):
'''
The user wanted to change the old state in the opposite.
'''
try:
if int(index.data()) == 0:
model.setData(index, 1, QtCore.Qt.EditRole)
ret = True
else:
model.setData(index, 0, QtCore.Qt.EditRole)
ret = False
# emit signal with row, col, and the status
self.user_click.emit(index.row(), index.column(), ret)
except Exception as ex:
print(ex)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
model = MyStandardItemModel(5, 5)
header_labels = ['', 'Signal', 'Type', 'Routing', 'Input']
model.setHorizontalHeaderLabels(header_labels)
tableView = MyTableView()
tableView.setModel(model)
delegate = CheckBoxDelegate(None)
tableView.setItemDelegateForColumn(0, delegate)
for row in range(5):
for column in range(4):
index = model.index(row, column, QModelIndex())
model.setData(index, 0)
model.setReadOnly([1,2])
tableView.setWindowTitle("CheckBox, readonly and drag&drop example")
tableView.show()
sys.exit(app.exec_())

Alerting QDataWidgetMapper to changes when using a custom Model & Delegate

I'm using a subclassed QAbstractTableModel with dataclasses as items. Each dataclass contains a field "field1" with a list, which I'd like to display in a listview and have it automatically change whenever I edit or add an item in the listview.
To do that I set a custom delegate to the QDataWidgetMapper which will retrieve and set the values from that dataclass. This works the way I want it to.
My problem is that I want to add additional items to that listview with the press of a button and have the QDataWidgetMapper add them automatically to the model.
This is what I have so far:
import sys
import dataclasses
from typing import List, Any
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
#dataclasses.dataclass()
class StorageItem:
field1: List[str] = dataclasses.field(default_factory=list)
class StorageModel(QAbstractTableModel):
def __init__(self, parent=None):
super().__init__(parent)
test = StorageItem()
test.field1 = ['Item °1', 'Item °2']
self._data: List[StorageItem] = [test]
def data(self, index: QModelIndex, role: int = ...) -> Any:
if not index.isValid():
return
item = self._data[index.row()]
col = index.column()
if role in {Qt.DisplayRole, Qt.EditRole}:
if col == 0:
return item.field1
else:
return None
def setData(self, index: QModelIndex, value, role: int = ...) -> bool:
if not index.isValid() or role != Qt.EditRole:
return False
item = self._data[index.row()]
col = index.column()
if col == 0:
item.field1 = value
self.dataChanged.emit(index, index)
print(self._data)
return True
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
return Qt.ItemFlags(
Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable
)
def rowCount(self, parent=None) -> int:
return len(self._data)
def columnCount(self, parent=None) -> int:
return len(dataclasses.fields(StorageItem))
class TestDelegate(QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def setEditorData(self, editor: QWidget, index: QModelIndex) -> None:
if isinstance(editor, QListView):
data = index.model().data(index, Qt.DisplayRole)
editor.model().setStringList(data)
else:
super().setEditorData(editor, index)
def setModelData(
self, editor: QWidget,
model: QAbstractItemModel,
index: QModelIndex
) -> None:
if isinstance(editor, QListView):
data = editor.model().stringList()
model.setData(index, data, Qt.EditRole)
else:
super().setModelData(editor, model, index)
class CustomListView(QListView):
item_added = pyqtSignal(name='itemAdded')
def __init__(self, parent=None):
super().__init__(parent)
self.setModel(QStringListModel())
def add_item(self, item: str):
str_list = self.model().stringList()
str_list.append(item)
self.model().setStringList(str_list)
self.item_added.emit()
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
cent_widget = QWidget()
self.setCentralWidget(cent_widget)
# Vertical Layout
v_layout = QVBoxLayout()
v_layout.setContentsMargins(10, 10, 10, 10)
# Listview
self.listview = CustomListView()
v_layout.addWidget(self.listview)
# Button
self.btn = QPushButton('Add')
self.btn.clicked.connect(lambda: self.listview.add_item('New Item'))
v_layout.addWidget(self.btn)
cent_widget.setLayout(v_layout)
# Set Mapping
self.mapper = QDataWidgetMapper()
self.mapper.setItemDelegate(TestDelegate())
self.mapper.setSubmitPolicy(QDataWidgetMapper.AutoSubmit)
self.mapper.setModel(StorageModel())
self.mapper.addMapping(self.listview, 0)
self.mapper.toFirst()
self.listview.itemAdded.connect(self.mapper.submit)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
if __name__ == '__main__':
main()
Currently, I'm using the signal itemAdded from inside the custom ListView to manually submit the QDataWidgetMapper.
Is there a way to do this within CustomListView, without using a custom signal?
Somehow the delegate knows when data in the listview has been edited. How can I trigger that same mechanism when new items are added?
TL; DR; It can not.
The submitPolicy QDataWidgetMapper::AutoSubmit indicates that the model will be updated when focus is lost. The model is also updated when the commitData or closeEditor signal of the delegate is invoked, which happens by default when some specific keys are pressed.
A better implementation would be to create a signal that is emitted every time a change is made in the QListView model and connect it to submit, not just the method of adding elements. Also it is better to use a custom qproperty.
class CustomListView(QListView):
items_changed = pyqtSignal(name="itemsChanged")
def __init__(self, parent=None):
super().__init__(parent)
self.setModel(QStringListModel())
self.model().rowsInserted.connect(self.items_changed)
self.model().rowsRemoved.connect(self.items_changed)
self.model().dataChanged.connect(self.items_changed)
self.model().layoutChanged.connect(self.items_changed)
def add_item(self, item: str):
self.items += [item]
#pyqtProperty(list, notify=items_changed)
def items(self):
return self.model().stringList()
#items.setter
def items(self, data):
if len(data) == len(self.items) and all(
x == y for x, y in zip(data, self.items)
):
return
self.model().setStringList(data)
self.items_changed.emit()
# Set Mapping
self.mapper = QDataWidgetMapper()
self.mapper.setModel(StorageModel())
self.mapper.addMapping(self.listview, 0, b"items")
self.mapper.toFirst()
self.listview.items_changed.connect(self.mapper.submit)

Why does my QStyleItemDelegate pop up in a new window instead of in my TableView?

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

How to add QTreeView in a QTableView column

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:

Categories