Calling a model method through a subclass - python

What I'm trying to do, is to create a subclass of QTableView that will accept a standardized model (which is the result of a mysql query), and populate it. With it, I'd like to define a custom context menu which allows me to add or remove a row. This approach allows me to define the model and subclass once, and then call them both to populate them with a number of different queries without duplicating code.
The add_row and remove_row QActions would essentially call the model methods insertRows and removeRows.
Here's a functioning example (apart from the mariadb module which is separate):
from PySide2 import QtWidgets, QtCore, Qt, QtGui
import maria_db
import numpy
app = QtWidgets.QApplication([])
#Define model for tablePersonnel
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, db,query,tableName,id):
super(TableModel, self).__init__()
self.db=db
self.query=query
self.tableName=tableName
self.id=id
self.pullData()
def pullData(self):
self.datasheet=self.db.RunQuery(self.query)
self.rows = numpy.array(self.datasheet.fetchall())
self.headers = numpy.array(self.datasheet.column_names)
self.idIndex=int(numpy.where(self.headers==self.id)[0])
def data(self, index, role):
if role == QtCore.Qt.DisplayRole:
return str(self.rows[index.row()][index.column()])
def rowCount(self, parent):
return len(self.rows)
def columnCount(self, parent):
return len(self.headers)
def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.headers[section]
def flags(self,index):
if index.column()!=self.idIndex:
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsSelectable
else:
return QtCore.Qt.ItemIsSelectable
def setData(self, index,value,role=QtCore.Qt.EditRole):
if index.isValid():
sel_id=self.rows[index.row()][0]
sel_column=self.headers[index.column()]
if self.db.UpdateCell(self.tableName,sel_id,self.id,sel_column,value)==True:
self.rows[self.idFinder(self.rows,sel_id)][1]=value
self.dataChanged.emit(index,index)
return True
def insertRows(self):
print("inserting row!!!")
def removeRows(self):
pass
#find index of selected ID
def idFinder(self,data, search):
for i in range(len(data)):
for j in range(len(data[i])):
if data[i][j] == search:
return i
else:
return False
class EditTable(QtWidgets.QTableView):
def __init__(self):
super(EditTable, self).__init__()
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self.context_menu)
def context_menu(self):
context=QtWidgets.QMenu()
add_row=context.addAction("Add row")
add_row.triggered.connect(lambda: self.personnelTableModel.insertRows())
rem_row=context.addAction("Remove row")
rem_row.triggered.connect(lambda: self.personnelTableModel.removeRows())
cursor=QtGui.QCursor()
context.exec_(cursor.pos())
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
widget.setLayout(layout)
button = QtWidgets.QPushButton("Push me")
self.db = maria_db.ConnectDb("localhost", "root", "1234", "test")
self.personnelTableModel = TableModel(self.db,"SELECT * FROM personnel","personnel","personnel_id")
table = EditTable()
table.setModel(self.personnelTableModel)
layout.addWidget(table)
layout.addWidget(button)
self.setCentralWidget(widget)
window = MainWindow()
window.show()
app.exec_()
So my problem is that when I call add_row.triggered.connect(lambda: self.personnelTableModel.insertRows()), I get a 'EditTable' object has no attribute 'personnelTableModel' which essentially means that there is no method insertRows in self. How can I refer to the model assigned to the object that is constructed using the subclass of the QTableView EditTable, through the subclass itself? Am I going about this the wrong way?

What is personnelTableModel for EditTable? the model, and how can I access the model from the view? Using the model() method. On the other hand, a lambda method is not necessary.
def context_menu(self):
context=QtWidgets.QMenu()
add_row=context.addAction("Add row")
add_row.triggered.connect(self.model().insertRows)
rem_row=context.addAction("Remove row")
rem_row.triggered.connect(self.model().removeRows)
cursor=QtGui.QCursor()
context.exec_(cursor.pos())

Related

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

Unable to emit signal from QtGui.QStandardItem

I'm attempting to override QTreeView to handle adjusting parents and children if the checkbox is modified. I'm not able to emit a signal however, and I'm not sure if it's because I'm trying to subclass QtGui and not QtWidgets.
Here is the code that will trigger the error:
class QStandardItem(QtGui.QStandardItem):
someSignal = QtCore.Signal()
def __init__(self, *args, **kwargs):
QtGui.QStandardItem.__init__(self, *args, **kwargs)
self.someSignal.emit()
>>> QStandardItem()
# AttributeError: 'PySide2.QtCore.Signal' object has no attribute 'emit' #
Here's my current code just for reference:
class QStandardItem(QtGui.QStandardItem):
checkStateChanged = QtCore.Signal(object)
def __init__(self, *args, **kwargs):
QtGui.QStandardItem.__init__(self, *args, **kwargs)
def setData(self, data, role):
if role == QtCore.Qt.CheckStateRole:
self.checkStateChanged.emit(self)
QtGui.QStandardItem.setData(self, data, role)
class QTreeView(QtWidgets.QTreeView):
def __init__(self, *args, **kwargs):
QtWidgets.QTreeView.__init__(self, *args, **kwargs)
#I need to know when to trigger this as it edits other nodes
def checkStateChanged(self, model_index):
selected_item = self.model().itemFromIndex(model_index)
check_state = selected_item.checkState()
#Handle child nodes
for i in range(selected_item.rowCount()):
child_item = selected_item.child(i)
if child_item.isCheckable():
child_item.setCheckState(check_state)
#Handle parent nodes
parent_item = selected_item.parent()
check_states = {QtCore.Qt.Checked: 0,
QtCore.Qt.PartiallyChecked: 1,
QtCore.Qt.Unchecked: 2}
counts = [0, 0, 0]
if parent_item is not None:
for i in range(parent_item.rowCount()):
child_item = parent_item.child(i)
if child_item.isCheckable():
counts[check_states[child_item.checkState()]] += 1
if counts[0] and not counts[1] and not counts[2]:
parent_item.setCheckState(QtCore.Qt.Checked)
elif not counts[0] and not counts[1] and counts[2]:
parent_item.setCheckState(QtCore.Qt.Unchecked)
else:
parent_item.setCheckState(QtCore.Qt.PartiallyChecked)
As you have pointed out only the classes that inherit from QObject can emit signals, QStandardItem is not a QObject and therefore generates that problem. The appropriate option is to use QStandardItemModel, for this we overwrite the setData() method and establish a logic to verify if the state has changed and then the QStandardItem is issued using the itemFromIndex() method that returns a QStandardItem given a QModelIndex.
Example:
from PySide2 import QtCore, QtGui, QtWidgets
class StandardItemModel(QtGui.QStandardItemModel):
checkStateChanged = QtCore.Signal(QtGui.QStandardItem)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.CheckStateRole:
last_value = self.data(index, role)
val = super(StandardItemModel, self).setData(index, value, role)
if role == QtCore.Qt.CheckStateRole and val:
current_value = self.data(index, role)
if last_value != current_value:
it = self.itemFromIndex(index)
self.checkStateChanged.emit(it)
return val
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
w = QtWidgets.QTreeView()
model = StandardItemModel(w)
w.setModel(model)
model.checkStateChanged.connect(self.foo_slot)
for i in range(4):
top_level = QtGui.QStandardItem("{}".format(i))
top_level.setCheckable(True)
model.appendRow(top_level)
for j in range(5):
it = QtGui.QStandardItem("{}-{}".format(i, j))
it.setCheckable(True)
top_level.appendRow(it)
w.expandAll()
self.setCentralWidget(w)
#QtCore.Slot(QtGui.QStandardItem)
def foo_slot(self, item):
print(item.text(), item.checkState())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
not 100% sure but for Python in order to define your own signal you have to use:
QtCore.pyqtSignal()
Also you might need to also inherint QtCore.QObject in order to use signals
Edit: Just saw your using pyside so scrap the first solution but inherting QObject is your solution than

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:

Creating a model/view interface with sliders using PyQt

I have a GUI that consists of a number of sliders, and instead of updating the sliders manually when the underlying data changes, I'd like to store the data in a subclass of QAbstractListModel and have the slider positions update automatically. My subclass looks like this:
from PyQt4 import QtCore
class myDataModel(QtCore.QAbstractListModel):
def __init__(self, initData, parent=None):
super(myDataModel, self).__init__(parent)
self.__data = initData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if index.row() > len(self.__data):
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.__data[index.row()]
return None
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__data)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid() or role != QtCore.Qt.EditRole:
return False
self.__data[index.row()] = value
self.dataChanged.emit(index, index)
return True
How can I connect this model to the sliders in my GUI so that when the data in the model is changed, the sliders change, and vice versa?
Edit: Here is a mockup of the basic interface I have been working on:
Edit: I still haven't been able to get this to work. Here is my model class:
class dataModel(QtCore.QAbstractListModel):
def __init__(self, initData, parent=None):
super(dataModel, self).__init__(parent)
self.__data = initData
def data(self, index, role=QtCore.Qt.DisplayRole):
if not index.isValid():
return None
if index.row() > len(self.__data):
return None
if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
return self.__data[index.row()]
return None
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self.__data)
def setData(self, index, value, role=QtCore.Qt.EditRole):
if not index.isValid() or role != QtCore.Qt.EditRole:
return False
self.__data[index.row()] = value
self.dataChanged.emit(index, index)
return True
Here is the Delegate class:
class sliderDelegate(QtGui.QItemDelegate):
'''
classdocs
'''
def __init__(self, parent=None):
'''
Constructor
'''
super(sliderDelegate, self).__init__(parent)
def setEditorData(self, editor, index):
editor.setValue(index.model().data(index, QtCore.Qt.EditRole))
def setModelData(self, editor, model, index):
model.setData(index, editor.value(), QtCore.Qt.EditRole)
And here is the setup code:
self._model = dataModel([0 for i in xrange(20)])
self._parameterMapper = QtGui.QDataWidgetMapper(mainWindowInstance)
self._parameterMapper.setModel(self._model)
self._parameterMapper.setItemDelegate(sliderDelegate(mainWindowInstance))
self._parameterMapper.addMapping(self._mainWindowInstance.ui.mySlider, 0)
self._parameterMapper.toFirst()
Unfortunately I get the following error when toFirst() is called:
editor.setValue(index.model().data(index, QtCore.Qt.EditRole))
AttributeError: 'NoneType' object has no attribute 'data'
Any help would be appreciated.
So I haven't used QDataWidgetMapper. It does look interesting, but looks more useful for when you want to have multiple widgets updated to a particular row in a model (and be able to switch between rows easily), rather than each row of a model corresponding to the value of a widget (which I think is what you are after).
So this is my rather rough implementation. Hopefully you'll be able to extend it to your application (might need a bit more error checking added, and maybe the ability to link multiple sliders to a single model row, and possibly then extending to other types of widgets)
When you drag the slider, the model is updated to the sliders new value. I've also added a text box where you can type in a number, and click the button, which will set the model to a specific value. You will notice the slider will update to this value!
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
main_layout = QtGui.QVBoxLayout()
# Create the model
self.model = MyModel()
# Create a slider and link it to the model
self.slider1 = QtGui.QSlider()
self.model.add_slider(self.slider1)
main_layout.addWidget(self.slider1)
# Add a lineEdit and button to force update the model
# Note that the LineEdit is not linked to the model, so won't update with the slider
self.edit = QtGui.QLineEdit()
button = QtGui.QPushButton('update model')
button.clicked.connect(self.on_clicked)
main_layout.addWidget(self.edit)
main_layout.addWidget(button)
self.setLayout(main_layout)
def on_clicked(self):
self.model.update_model(int(self.edit.text()),self.slider1)
class MyModel(QtGui.QStandardItemModel):
def __init__(self,*args,**kwargs):
super(MyModel,self).__init__(*args,**kwargs)
self._slider_list = {}
self.itemChanged.connect(self.on_item_changed)
def add_slider(self,slider):
if slider in self._slider_list:
raise Exception('You cannot link a slider to the model twice')
item = QtGui.QStandardItem(str(slider.value()))
self._slider_list[slider] = item
self.appendRow(item)
slider.valueChanged.connect(lambda value: self.update_model(value,slider))
def update_model(self,value,slider):
if str(value) != self._slider_list[slider].text():
self._slider_list[slider].setText(str(value))
print 'update_model: %d'%value
def on_item_changed(self,item):
slider = self._slider_list.keys()[self._slider_list.values().index(item)]
if slider.value() != int(item.text()):
slider.setValue(int(item.text()))
print 'on_item_changed: %s'%item.text()
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
Hope that helps!

Categories