Editing a dictionary with a QTreeView - python

I have a reduced code sample in which a window is created with just one Button.
Pressing it will pop a Qt dialog TestDialog which takes as parameter a Python dictionary. This dictionary is displayed in an editable QTreeView inside the dialog.
After changing some values you can click on Ok or Cancel to either accept or discard the changes. Once the dialog is closed my intention is to retrieve from the main window the modified dictionary calling dialog.get_data() which, right now only returns the original unmodified dictionary. After clicking the Ok button, the retrieved dictionary is printed to stdout.
My question is, how can I modify the dictionary when the tree view is modified? Is there a way to automatically attach a function to be executed on each item modification? So that when editing, for example, a float in the tree view, then the corresponding value will be updated as float in the dictionary?
The dictionary does not have a fixed size and the types on it may change. The list of types is limited and known though and, for this example, could be reduced to {int, str, float, Other}. It can be assumed as well that the parents are not supposed to be editable and the children are only editable in the second column, just as it is shown in the example bellow.
Here is the code I have:
import sys
from PyQt4 import QtGui, QtCore, uic
from copy import deepcopy
class TestDialog(QtGui.QDialog):
def __init__(self, data):
super(TestDialog, self).__init__()
self.data = deepcopy(data)
# Layout
btOk = QtGui.QPushButton("OK")
btCancel = QtGui.QPushButton("Cancel")
self.tree = QtGui.QTreeView()
hbox = QtGui.QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(btOk)
hbox.addWidget(btCancel)
vbox = QtGui.QVBoxLayout()
vbox.addLayout(hbox)
vbox.addWidget(self.tree)
self.setLayout(vbox)
self.setGeometry(300, 300, 600, 400)
# Button signals
btCancel.clicked.connect(self.reject)
btOk.clicked.connect(self.accept)
# Tree view
self.tree.setModel(QtGui.QStandardItemModel())
self.tree.setAlternatingRowColors(True)
self.tree.setSortingEnabled(True)
self.tree.setHeaderHidden(False)
self.tree.setSelectionBehavior(QtGui.QAbstractItemView.SelectItems)
self.tree.model().setHorizontalHeaderLabels(['Parameter', 'Value'])
for x in self.data:
if not self.data[x]:
continue
parent = QtGui.QStandardItem(x)
parent.setFlags(QtCore.Qt.NoItemFlags)
for y in self.data[x]:
value = self.data[x][y]
child0 = QtGui.QStandardItem(y)
child0.setFlags(QtCore.Qt.NoItemFlags |
QtCore.Qt.ItemIsEnabled)
child1 = QtGui.QStandardItem(str(value))
child1.setFlags(QtCore.Qt.ItemIsEnabled |
QtCore.Qt.ItemIsEditable |
~ QtCore.Qt.ItemIsSelectable)
parent.appendRow([child0, child1])
self.tree.model().appendRow(parent)
self.tree.expandAll()
def get_data(self):
return self.data
class Other(object):
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return '(%s, %s)' % (self.x, self.y)
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
btn = QtGui.QPushButton('Button', self)
btn.resize(btn.sizeHint())
btn.clicked.connect(self.show_dialog)
self.data = {}
# This example will be hidden (has no parameter-value pair)
self.data['example0'] = {}
# A set example with an integer and a string parameters
self.data['example1'] = {}
self.data['example1']['int'] = 14
self.data['example1']['str'] = 'asdf'
# A set example with a float and other non-conventional type
self.data['example2'] = {}
self.data['example2']['float'] = 1.2
self.data['example2']['other'] = Other(4, 8)
def show_dialog(self):
dialog = TestDialog(self.data)
accepted = dialog.exec_()
if not accepted:
return
self.data = deepcopy(dialog.get_data())
print self.data
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())

You can connect to the model's itemChanged signal:
self.tree.model().itemChanged.connect(self.handleItemChanged)
The handler would look something like this:
def handleItemChanged(self, item):
parent = self.data[item.parent().text()]
key = item.parent().child(item.row(), 0).text()
parent[key] = type(parent[key])(item.text())
Note that the conversion of values using type won't necessarily work for custom classes like Other. So you will have to either ensure that the constructor for such classes can convert a string representation, or parse the string into the appropriate arguments before passing them to the constructor.
Also note that I haven't bothered to deal with QString values in the above example code. If you use Python 3, this is not an issue, because they are automatically converted to/from Python strings by default. But for Python 2, you can switch this behaviour on by doing the following:
import sip
sip.setapi('QString', 2)
from PyQt4 import QtGui, QtCore, uic
For more details on this, see Selecting Incompatible APIs in the PyQt4 Docs.

Related

Why can't I sort columns in my PyQt5 QTableWidget using UserRole data? [duplicate]

This question already has answers here:
How to customize sorting behaviour in QTableWidget
(2 answers)
Closed 9 days ago.
I am trying to sort my QTableWidget columns by the values stored in the user role of each QTableWidgetItem, but I am unable to do so. I have enabled sorting with self.setSortingEnabled(True), and I have set the data in each QTableWidgetItem with item.setData(Qt.DisplayRole, f'M - {r}') and item.setData(Qt.UserRole, r). However, when I try to sort the columns by the values stored in the user role, it sorts the columns by the values stored in the display role instead.
Here is a minimal working example of my code:
from random import randint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableWidget, QWidget, QGridLayout, \
QTableWidgetItem, QPushButton
class Table(QTableWidget):
def __init__(self):
super().__init__()
self.setSortingEnabled(True)
def populate(self):
self.clear()
self.setColumnCount(3)
self.setRowCount(200)
for row in range(500):
for column in range(3):
r = randint(0, 1000)
item = QTableWidgetItem()
item.setData(Qt.DisplayRole, f'M - {r}')
item.setData(Qt.UserRole, r)
self.setItem(row, column, item)
class MainApp(QMainWindow):
def __init__(self):
super().__init__()
self.table = Table()
self.button = QPushButton('Roll')
self.button.clicked.connect(self.table.populate)
layout = QWidget()
self.setCentralWidget(layout)
grid = QGridLayout()
layout.setLayout(grid)
grid.addWidget(self.button)
grid.addWidget(self.table)
if __name__ == '__main__':
app = QApplication([])
main_app = MainApp()
main_app.showMaximized()
app.exec()
Additionally, I tried using EditRole, but the values that appear in the table are not the values from DisplayRole. For example, in the code below, I set item.setData(Qt.DisplayRole, f'M - {r}'), but even though r is an integer, the display role value is a string ('M - {r}'). I was hoping that sorting by UserRole or EditRole would sort based on the integer value of r, but that doesn't seem to be the case.
item.setData(Qt.DisplayRole, f'M - {r}')
item.setData(Qt.EditRole, int(r))
Sorting is always based on Qt.DisplayRole.
Trying to use the EditRole is pointless, as the setData() documentation points out:
Note: The default implementation treats Qt::EditRole and Qt::DisplayRole as referring to the same data.
The Qt.UserRole is a custom role that could be used for anything (and containing any type) and by default is not used for anything in Qt. Setting a value with the UserRole doesn't change the sorting, because Qt knows nothing about its existence or how the value should be used.
Since you are using strings for the sorting, the result is that numbers are not sorted as you may think: for instance "120" is smaller than "13", because "12" comes before "13".
The only occurrence in which the DisplayRole properly sorts number values is when it is explicitly set with a number:
item.setData(Qt.DisplayRole, r)
Which will not work for you, as you want to show the "M - " prefix. Also, a common mistake is trying to use that in the constructor:
item = QTableWidgetItem(r)
And while the syntax is correct, its usage is wrong, as the integer constructor of QTableWidgetItem is used for other purposes.
If you want to support custom sorting, you must create a QTableWidgetItem subclass and implement the < operator, which, in Python, is the __lt__() magic method:
class SortUserRoleItem(QTableWidgetItem):
def __lt__(self, other):
return self.data(Qt.UserRole) < other.data(Qt.UserRole)
Then you have to create new items using that class. Note that:
you should always try to use existing items, instead of continuously creating new ones;
as explained in the setItem() documentation, you should always disable sorting before adding new items, especially when using a loop, otherwise the table will be constantly sorted at each insertion (thus making further insertion inconsistent);
you're using the a range (500) inconsistent with the row count (200);
you should also set an item prototype based on the subclass above;
class Table(QTableWidget):
def __init__(self):
super().__init__()
self.setSortingEnabled(True)
self.setItemPrototype(SortUserRoleItem())
def populate(self):
self.setSortingEnabled(False)
self.setColumnCount(3)
self.setRowCount(200)
for row in range(200):
for column in range(3):
r = randint(0, 1000)
item = self.item(row, column)
if not item:
item = SortUserRoleItem()
self.setItem(row, column, item)
item.setData(Qt.DisplayRole, 'M - {}'.format(r))
item.setData(Qt.UserRole, r)
self.setSortingEnabled(True)
Note that, as an alternative, you could use a custom delegate, then just set the value of the item as an integer (as shown above) and override the displayText():
class PrefixDelegate(QStyledItemDelegate):
def displayText(self, text, locale):
if isinstance(text, int):
text = f'M - {text}'
return text
class Table(QTableWidget):
def __init__(self):
super().__init__()
self.setItemDelegate(PrefixDelegate(self))
# ...
def populate(self):
# ...
item = self.item(row, column)
if not item:
item = QTableWidgetItem()
self.setItem(row, column, item)
item.setData(Qt.DisplayRole, r)
Use a QTableView instead. Widgets are meant for very basic use cases. It's important to invoke setSortRole on the model. Also, your setData arguments were in reverse order, correct is data, role.
from random import randint
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets
class MainApp(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.table_view = QtWidgets.QTableView()
self.table_view.setSortingEnabled(True)
self.model = QtGui.QStandardItemModel()
self.model.setSortRole(QtCore.Qt.UserRole)
self.table_view.setModel(self.model)
self.button = QtWidgets.QPushButton('Roll')
layout = QtWidgets.QWidget()
self.setCentralWidget(layout)
grid = QtWidgets.QGridLayout()
layout.setLayout(grid)
grid.addWidget(self.button)
grid.addWidget(self.table_view)
self.button.clicked.connect(
self.populate
)
def populate(self):
self.table_view.model().clear()
for _ in range(500):
r = randint(0, 1000)
item = QtGui.QStandardItem()
item.setData(f'M - {r}', QtCore.Qt.DisplayRole)
item.setData(r, QtCore.Qt.UserRole)
self.table_view.model().appendRow(item)
if __name__ == '__main__':
app = QtWidgets.QApplication([])
main_app = MainApp()
main_app.showMaximized()
app.exec()

Access 'isChecked' property from QWidgetItem class object

I'm dynamically creating UI elements in layers of a QStackedLayout widget. I have created a generator that stores the objects for a targeted stacked widget layer so that I can collect settings made with UI elements contained in the layer when needed. I'm certain that I have isolated a radio button, but I do not know how to access the 'isChecked()' property. If I have this item isolated and stored as a variable using this line ('items' is the Generator):
target_radio_button = items.__next__()
how can I get to the 'isChecked' property to query whether or not it is checked, knowing that this QWidgetItem is a QRadioButton? If I print the type of the isolated object using:
print (type(target_radio_button))
This is what is returned:
<class 'PySide2.QtWidgets.QWidgetItem'>
Here is the minimal code requested:
from PySide2 import QtWidgets
import sys
class Test(QtWidgets.QWidget):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
self.setWindowTitle("Test")
self.setGeometry(50, 50, 200, 200)
self.test_layout = QtWidgets.QVBoxLayout()
self.setLayout(self.test_layout)
self.main_stacked_layout = QtWidgets.QStackedLayout()
self.test_layout.addLayout(self.main_stacked_layout)
self.widget = QtWidgets.QWidget()
self.widget_layout = QtWidgets.QVBoxLayout()
self.widget.setLayout(self.widget_layout)
self.radio = QtWidgets.QRadioButton('Sample Radio Button')
self.h_layout = QtWidgets.QHBoxLayout()
self.h_layout.addWidget(self.radio)
self.widget_layout.addLayout(self.h_layout)
self.layout_container = [self.widget]
self.main_stacked_layout.addWidget(self.widget)
self.demo()
def demo(self):
target_widget = self.layout_container[0]
target_layout = target_widget.layout()
for layout_item in target_layout.findChildren(QtWidgets.QHBoxLayout):
items = (layout_item.itemAt(i) for i in range(layout_item.count()))
self.get_settings_value(items, 'Boolean')
# for item in target_layout.findChildren(QtWidgets.QRadioButton):
# print (item.text())
# print (item.isChecked())
def get_settings_value(self, items, value_type):
if value_type == 'Boolean':
target_radio_button = items.__next__()
print (type(target_radio_button))
#print (target_radio_button.isChecked())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
form = Test()
form.show()
sys.exit(app.exec_())
There are two areas I commented out- the first block I disabled demonstrates a way that I can see to get the information I'm looking for, but its not versatile enough. What I need is to capture the children in the target layout without filtering for a specific type (in the case of the sample code this would be for a QRadioButton). I want to send that group of found children to the simplified "get_settings_value" function, and from there break out instructions on what to look for. I intend to have multiple different "value_type" arguments to extract values depending on the UI widget. The second commented out area is the print statement checking if the element 'isChecked()'. This throws the error, and I'm trying to understand how to gain access to this property.
The itemAt() method returns a QLayoutItem that is a generic container and its derived classes are QLayout, QSpacerItem, and QWidgetItem. So if you want to get the widget of a QLayoutItem then you must use the widget() method:
def get_settings_value(self, items, value_type):
if value_type == 'Boolean':
layoutitem = next(items)
target_radio_button = layoutitem.widget()
print(type(target_radio_button))
#print (target_radio_button.isChecked())

How to connect QTreeWidget and QStackedWidget in PyQt4?

I'm sorry but just a beginner of Python.
I just want to change index of QStackedWidget by the item click of QTreeWidget. I searched for the tutorials of SIGNAL and SLOT online, but just cannot solve the problem.
The parameters in QTreeWidget signal and QStackedWidget slot are not fitted.
self.connect(qtree, QtCore.SIGNAL("itemClicked(QTreeWidgetItem*,int)"), stack, QtCore.SLOT("setCurrentIndex(int)"))
And I tried this:
qtree.itemClicked.connect(stack.setCurrentIndex)
It just showed the error:
TypeError: setCurrentIndex(self, int): argument 1 has unexpected type 'QTreeWidgetItem'
I think there may be a method, but I cannot find on the network.
Like this:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
import sys
class StockDialog(QDialog):
def __init__(self,parent=None):
super(StockDialog,self).__init__(parent)
mainSplitter=QSplitter(Qt.Horizontal)
treewidget = QTreeWidget(mainSplitter)
treewidget.setHeaderLabels(["Tree"])
treeroot = QTreeWidgetItem(treewidget, ["Stack"])
treeitem1 = QTreeWidgetItem(["WorkSpace"])
treeitem2 = QTreeWidgetItem(["About"])
treeroot.addChild(treeitem1)
treeroot.addChild(treeitem2)
stack=QStackedWidget(mainSplitter)
stack.setFrameStyle(QFrame.Panel|QFrame.Raised)
stackworkspace=StackWorkSpace()
stackabout=StackAbout()
stack.addWidget(stackworkspace)
stack.addWidget(stackabout)
closePushButton=QPushButton(self.tr("Close"))
self.connect(treewidget,
SIGNAL("itemClicked(int)"),
stack,SLOT("setCurrentIndex(int)"))
self.connect(closePushButton,
SIGNAL("clicked()"),
self,SLOT("close()"))
layout=QVBoxLayout(self)
layout.addWidget(mainSplitter)
layout.addWidget(closePushButton)
self.setLayout(layout)
class StackWorkSpace(QWidget):
def __init__(self,parent=None):
super(StackWorkSpace,self).__init__(parent)
widget1=QTextEdit(self.tr("WorkSpace"))
widget2=QTextEdit(self.tr("WorkSpace"))
layout=QGridLayout(self)
layout.addWidget(widget1,0,0)
layout.addWidget(widget2,0,1)
class StackAbout(QDialog):
def __init__(self,parent=None):
super(StackAbout,self).__init__(parent)
self.setStyleSheet("background: red")
app=QApplication(sys.argv)
main=StockDialog()
main.show()
app.exec_()
When change the QTreeWidget to the QListWidget in StockDialog class, it works.
class StockDialog(QDialog):
def __init__(self,parent=None):
super(StockDialog,self).__init__(parent)
mainSplitter=QSplitter(Qt.Horizontal)
listwidget=QListWidget(mainSplitter)
listwidget.insertItem(0,self.tr("WorkSpace"))
listwidget.insertItem(1,self.tr("About"))
stack=QStackedWidget(mainSplitter)
stack.setFrameStyle(QFrame.Panel|QFrame.Raised)
stackworkspace=StackWorkSpace()
stackabout=StackAbout()
stack.addWidget(stackworkspace)
stack.addWidget(stackabout)
closePushButton=QPushButton(self.tr("Close"))
self.connect(listwidget,
SIGNAL("currentRowChanged(int)"),
stack,SLOT("setCurrentIndex(int)"))
self.connect(closePushButton,
SIGNAL("clicked()"),
self,SLOT("close()"))
layout=QVBoxLayout(self)
layout.addWidget(mainSplitter)
layout.addWidget(closePushButton)
self.setLayout(layout)
Now, I want to do this with QTreeWidget, how can I do?
The strategy to solve this problem is to save the index information associated with each widget in the QTreeWidgetItem. QTreeWidgetItem has the setData() method that allows us to save information in the item and in this case we will save the index. The index is returned every time you add a widget to QStackedWidget through addWidget(), so in summary we will do the following:
treeitem1.setData(0, Qt.UserRole, stack.addWidget(stackworkspace))
treeitem2.setData(0, Qt.UserRole, stack.addWidget(stackabout))
After connecting the itemClicked signal of QTreeWidget, this returns the column and the item pressed, with this information we obtain the QStackedWidget index for it we recover the data saved through the function data():
treewidget.itemClicked.connect(lambda item, column: stack.setCurrentIndex(item.data(column, Qt.UserRole))
if item.data(column, Qt.UserRole) is not None else None)
The necessary code can be found in the following section:
class StockDialog(QDialog):
def __init__(self, parent=None):
super(StockDialog, self).__init__(parent)
mainSplitter = QSplitter(Qt.Horizontal)
treewidget = QTreeWidget(mainSplitter)
treewidget.setHeaderLabels(["Tree"])
treeroot = QTreeWidgetItem(treewidget, ["Stack"])
treeitem1 = QTreeWidgetItem(["WorkSpace"])
treeitem2 = QTreeWidgetItem(["About"])
treeroot.addChild(treeitem1)
treeroot.addChild(treeitem2)
stack = QStackedWidget(mainSplitter)
stack.setFrameStyle(QFrame.Panel | QFrame.Raised)
stackworkspace = StackWorkSpace()
stackabout = StackAbout()
treeitem1.setData(0, Qt.UserRole, stack.addWidget(stackworkspace))
treeitem2.setData(0, Qt.UserRole, stack.addWidget(stackabout))
closePushButton = QPushButton(self.tr("Close"))
treewidget.itemClicked.connect(lambda item, column: stack.setCurrentIndex(item.data(column, Qt.UserRole))
if item.data(column, Qt.UserRole) is not None else None)
layout = QVBoxLayout(self)
layout.addWidget(mainSplitter)
layout.addWidget(closePushButton)
self.setLayout(layout)

PyQt Get specific value in ListWidget

I am new to python and pyqt/pyside ...
i make customwidget class consist of 2 label (title & desc) which is example instance to add to Listwidget later ...
here is the complete clean code (pyside maya)
import PySide.QtCore as qc
import PySide.QtGui as qg
class CustomQWidget (qg.QWidget):
def __init__ (self, parent = None):
super(CustomQWidget, self).__init__(parent)
self.textQVBoxLayout = qg.QVBoxLayout()
self.titleLabel = qg.QLabel()
self.description = qg.QLabel()
self.textQVBoxLayout.addWidget(self.titleLabel)
self.textQVBoxLayout.addWidget(self.description)
self.setLayout(self.textQVBoxLayout)
def setTitle (self, text):
self.titleLabel.setText(text)
def setDescription (self, text):
self.description.setText(text)
class example_ui(qg.QDialog):
def __init__(self):
qg.QDialog.__init__(self)
self.myQListWidget = qg.QListWidget(self)
self.myQListWidget.currentItemChanged.connect(self.getTitleValue)
self.myQListWidget.setGeometry(qc.QRect(0,0,200,300))
# make instance customwidget item (just one)------
instance_1 = CustomQWidget()
instance_1.setTitle('First title')
instance_1.setDescription('this is a sample desc')
myQListWidgetItem = qg.QListWidgetItem(self.myQListWidget)
myQListWidgetItem.setSizeHint(instance_1.sizeHint())
self.myQListWidget.addItem(myQListWidgetItem)
self.myQListWidget.setItemWidget(myQListWidgetItem, instance_1)
def getTitleValue(self,val):
# i make assume something like below but didnt work
# print (self.myQListWidget.currentItem.titleLabel.text()
return 0
dialog = example_ui()
dialog.show()
now at getTitleValue function how do i get Title and desc value when i change selection ?
You should remember that the list items and corresponding widgets are not the same. Luckily, QListWidget tracks them and gives you access to the displayed widget if you provide the list item:
class example_ui(qg.QDialog):
def getTitleValue(self,val):
# parameter val is actually the same as self.myQListWidget.currentItem
selected_widget = self.myQListWidget.itemWidget(val)
print selected_widget.titleLabel.text()
return 0
Side note: I had to add a main loop in order for the app to be executed at all:
import sys # to give Qt access to parameters
# ... class definitions etc. ...
app = qg.QApplication(sys.argv)
dialog = example_ui()
dialog.show()
exec_status = app.exec_() # main loop

Accessing property of dynamically created QStandardItems in PyQt5

I'm having a problem determining whether or not the checkboxes that are dynamically created have been checked or unchecked by the user in a simple GUI I've created.
I've adapted the relevant code and pasted it below. Although it may be easy to just create and name 4 QStandardItems, I'm dealing with many lists containing many different items that change quite a lot, so it isn't really feasible to create them myself.
Any help finding out how to access these properties would be much appreciated.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
class Splash(QWidget):
def __init__(self):
super().__init__()
# imagine this is a very long list...
self.seasons = ['summer','autumn','winter','spring']
self.initUI()
def initUI(self):
layout = QVBoxLayout()
list = QListView()
model = QStandardItemModel()
list.setModel(model)
printbtn = QPushButton('print values')
printbtn.clicked.connect(self.print_action)
for season in self.seasons:
item = QStandardItem(season)
item.setCheckable(True)
model.appendRow(item)
model.dataChanged.connect(lambda: self.print_action(item.text()))
layout.addWidget(printbtn)
layout.addWidget(list)
self.setLayout(layout)
self.show()
def print_action(self, item):
print('changed', item)
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
ex = Splash()
sys.exit(app.exec_())
In short - I can detect when an item has been checked using model.dataChanged and connecting that to a function, but it cannot differentiate between the seasons.
If you keep a reference to the list (or the model), you can search for the items by their text, and then get their check-state:
def print_action(self):
model = self.list.model()
for text in 'summer', 'autumn', 'winter', 'spring':
items = model.findItems(text)
if items:
checked = items[0].checkState() == Qt.Checked
print('%s = %s' % (text, checked))
It seems you want to get notified when the checkState of a item has been changed.
In my opinion, there are possible two ways.
First way, QModel will emit "dataChanged" to refresh the view, so you can connect the signal which means the checkState of a item might be changed.
model.dataChanged.connect(self.test)
def test(self):
pass
Second way, use a timer to notify yourself and you check it by yourselves.
timer = QTimer()
timer.timeout.connect(self.test)
timer.start(1000)

Categories

Resources