Change certain data in QTreeView - python

I am trying to make a virtual file system for a part of a project (since I couldn't find any libraries for this — all the ones I found were for accessing your actual file system).
I decided to store it as an XML, and then to display it by generating a QStandardItemModel, and generating a QTreeView based on that model.
I am currently trying to add the rename, move, etc options. I can perform these changes in the XML fairly easily, but I've spent hours trying to figure it out with the treeview and haven't gotten anywhere. The best thing I've been able to do is to regenerate the model from the xml file, and then make that the treeview model. However, this will probably be too slow for my end program (which will have a few thousand "files"), and this also collapses all the nodes in the treeview which seems pretty annoying to fix. Overall, this just doesn't seem like the best way to do it, especially since I know which nodes are being changed, so it would be much simpler to just edit those individual nodes in the treeview (or in the model).
The way I would want this to work, is that the user selects an element of the treeview to rename, I get the selectedItem from the treeview, then somehow lookup the corresponding item in the model and modify that.
I've also considered traversing the model to find the item I want to move/rename and then doing that within the model, but I couldn't find any good documentation for how to traverse the model (it doesn't even seem to have a method that returns the number of children of a node).
Is there a "nice" way of doing this?
Example:
def clicked():
index = list(treeView.selectedIndexes())[0]
# TODO: change text of index to "changed text"
app = QApplication([])
window = QWidget()
layout = QVBoxLayout()
treeView = QTreeView()
model = QStandardItemModel()
model.invisibleRootItem().appendRow(QStandardItem('a'))
child_node = QStandardItem('b')
child_node.appendRow(QStandardItem('c'))
model.invisibleRootItem().appendRow(child_node)
treeView.setModel(model)
treeView.clicked.connect(clicked)
layout.addWidget(treeView)
window.setLayout(layout)
window.show()
app.exec_()

The clicked signal sends the QModelIndex associated with the clicked item so it is not necessary to use selectedIndexes().
Going to the bottom problem, the logic is to use the model to get the item given the QModelIndex and then use the setText() method, another more general option is to use the setData() method:
def clicked(index):
model = index.model()
item = model.itemFromIndex(index)
item.setText("changed text")
# or
# model.setData(index, "changed text", Qt.DisplayRole)

Related

Layout in a QStandartItem for QTreeView

I am searching for a way to have a QTreeView that contains hierarchical items which themselfs have a layout that is propperly drawn.
I tried to inherit from both QStandartItem and QWidget (to have a layout) but the second i set the layout on the widget part of this class the programm is shutting down when it tries to render.
class modPackItem(qtg.QStandardItem,qtw.QWidget):
def __init__(self,txt:str='',image_path:str='./assets/defaultModPack.jpg'):
super().__init__()
fnt = qtg.QFont('Calibri',12)
fnt.setBold(True)
self.setEditable(False)
self.setForeground(qtg.QColor(0,0,0))
self.setFont(fnt)
self.setText(txt)
self.horLayout = qtw.QHBoxLayout()
self.horLayout.addWidget(qtw.QLabel("test"))
#self.setLayout(self.horLayout) #this breaks the rendering
modPack_image = qtg.QImage(image_path)
self.setData(modPack_image.scaled(64,64,qtc.Qt.AspectRatioMode.KeepAspectRatioByExpanding),qtc.Qt.ItemDataRole.DecorationRole)
Is there a possible way to have all items in the QTreeView contain layouts (For example with multiple texts[description,tag-words,etc]).
Note: I also considered switching to a simple List of widgets which have children containing the hierarchical items. But that would increase complexity of my app-structure a lot and therefore i would like to avoid that.
Edit: To clearify what i want to do:
I want to build a mod(pack) manager in the style of the technic-launcher for minecraft mods but instead for any kind of game in any kind of infrastructure(steam, local instal,etc). By clicking different buttons i add new "modpacks" or "mods" (optimally custom QStandartItem with Layout for all the data) in an hierarchical fashion (therefore treeview). Adding the items and the steam-subsrciption or filecopy logic is no problem but i would like to see all infos (Name,descritpion, custom tags) on the overview (like in the example pic). I know i could bind the QStandartItem selection method to a new popup showing all infos but that would be inconvinient.
Edit2: On terms of implementation i just add the QStandartItem-object as an additional row to the root-node before setting the model. I allready tested adding new objects to the rootnode by clicking on a button and that worked fine. Just setting the layout in the object crashes the application at start.
class SteamModManager_Dialog(qtw.QDialog):
window: Ui_SteamModManagerFrame
treeModel: qtg.QStandardItemModel
rootNode: qtg.QStandardItem
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.window = Ui_SteamModManagerFrame()
self.window.setupUi(self)
self.window.label_footer.setText("")
self.treeModel = qtg.QStandardItemModel()
self.rootNode = self.treeModel.invisibleRootItem()
modPack = modPackItem('Dont Starve Together')
testMod = modItem("TestMod")
modPack.appendRow(testMod)
self.rootNode.appendRow(modPack)
self.window.tView_modPacks.setModel(self.treeModel)
self.window.tView_modPacks.expandAll()
On the behalf of #musicamente here the solution that worked out for me:
I created a widget in the designer (as usual, not posting the full ui code here).
Then i implemented the following code into the Dialog:
self.treeModel = qtg.QStandardItemModel()
self.rootNode = self.treeModel.invisibleRootItem()
modPack = modPackItem('Dont Starve Together')
testMod = modItem("TestMod")
modPack.appendRow(testMod)
self.rootNode.appendRow(modPack)
self.window.tView_modPacks.setModel(self.treeModel)
self.window.tView_modPacks.expandAll()
modPackWidget = qtw.QWidget()
ui = Ui_modPackWidget()
ui.setupUi(modPackWidget)
self.window.tView_modPacks.setIndexWidget(self.treeModel.index(0,0),modPackWidget)
This code resulted setting the custom widget to the treeview item. Here the final look:

PyQt need to throw custom widgets into a list and be able to move them around

I have been fighting with custom widgets and lists for weeks now.
I have the ability to add a custom widget to a QListWidget. My issue is the insertItem seems to be buggy as it always drops it to the bottom of the list no matter what row I tell it to go to.
def AddToInitiative(self):
creature = self.comboBoxSelectCharacter.currentText()
if(creature):
widget = self.NewItemWidget()
customWidgetItem = QtGui.QListWidgetItem(self.initiativeList)
customWidgetItem.setSizeHint(QtCore.QSize(400,50))
self.initiativeList.addItem(customWidgetItem)
self.initiativeList.setItemWidget(customWidgetItem, widget)
widget.lineName.setText(creature)
return
def NewItemWidget(self):
customWidget = creature_initiative_object.InitCreatureObject()
customWidget.btnRemove.clicked.connect(self.ItemButtonClicked)
customWidget.btnInfo.clicked.connect(self.ItemButtonClicked)
customWidget.btnIncreaseInitOrder.clicked.connect(self.ItemButtonClicked)
customWidget.btnDecreaseInitOrder.clicked.connect(self.ItemButtonClicked)
return customWidget
def ChangeInit(self, row, direction, oldwidget):
item = self.initiativeList.takeItem(row)
widget = self.NewItemWidget()
widget.lineName.setText(oldwidget.lineName.text())
customWidgetItem = QtGui.QListWidgetItem(self.initiativeList)
customWidgetItem.setSizeHint(QtCore.QSize(400,50))
self.initiativeList.insertItem((row + direction), customWidgetItem)
self.initiativeList.setItemWidget(customWidgetItem, widget)
self.initiativeList.setCurrentRow(row+direction)
If I try to move the item up or down the list it always just goes straight to the bottom despite assigning the row number. I've output the row number and count to verify it should be saying the right row.
Do you know a way to do this with QListWidget for custom widgets that need to preserve data? Is there a better module to use?
I've looked at QListView, QBoxLayout, and a few others without being able to get it to work as well as QListWidget.

Adding/Removing QSlider widgets on PyQt5

I want to dynamically change the number of sliders on my application window, in dependence of the number of checked items in a QStandardItemModel structure.
My application window has an instance of QVBoxLayout called sliders, which I update when a button is pressed:
first removing all sliders eventually in there:
self.sliders.removeWidget(slider)
And then creating a new set.
The relevant code:
def create_sliders(self):
if len(self.sliders_list):
for sl in self.sliders_list:
self.sliders.removeWidget(sl)
self.sliders_list = []
for index in range(self.model.rowCount()):
if self.model.item(index).checkState():
slid = QSlider(Qt.Horizontal)
self.sliders.addWidget(slid)
self.sliders_list.append(slid)
The principle seems to work, however what happens is weird as the deleted sliders do not really disappear but it is as they were 'disconnected' from the underlying layout.
When created, the sliders keep their position among other elements while I resize the main window.
However, once they've been removed, they occupy a fixed position and can for instance disappear if I reduce the size of the window.
Unfortunately I'm having difficulties in linking images (it says the format is not supported when I try to link from pasteboard), so I hope this description is enough to highlight the issue.
Do I have to remove the sliders using a different procedure?
EDIT
Thanks to #eyllansec for his reply, it condenses a bunch of other replies around the topic, which I wasn't able to find as I did not know the method deleteLater() which is the key to get rid of widgets inside a QLayout.
I am marking it as my chosen (hey, it's the only one and it works, after all!), however I want to propose my own code which also works with minimal changes w.r.t. to what I proposed in the beginning.
The key point here is that I was using the metod QLayout.removeWidget(QWidget) which I was wrongly thinking, it would..er..Remove the widget! But actually what it does is (if I understood it right) remove it from the layout instance.
That is why it was still hanging in my window, although it seemed disconnected
Manual reference
Also, the proposed code is far more general than what I need, as it is a recursion over layout contents, which could in principle be both other QLayout objects or QWidgets (or even Qspacer), and be organized in a hierarchy (i.e., a QWidget QLayout within a QLayout and so on).
check this other answer
From there, the use of recursion and the series of if-then constructs.
My case is much simpler though, as I use this QVLayout instance to just place my QSliders and this will be all. So, for me, I stick to my list for now as I do not like the formalism of QLayout.TakeAt(n) and I don't need it. I was glad that the references I build in a list are absolutely fine to work with.
In the end, this is the slightly changed code that works for me in this case:
def create_sliders(self):
if len(self.sliders_list):
for sl in self.sliders_list:
sl.deleteLater()
self.sliders_list = []
for index in range(self.model.rowCount()):
if self.model.item(index).checkState():
slid = QSlider(Qt.Horizontal)
self.sliders.addWidget(slid)
self.sliders_list.append(slid)
It is not necessary to save the sliders in a list, they can be accessed through the layout where it is contained. I have implemented a function that deletes the elements within a layout. The solution is as follows:
def create_sliders(self):
self.clearLayout(self.sliders)
for index in range(self.model.rowCount()):
if self.model.item(index).checkState():
slid = QSlider(Qt.Horizontal)
self.sliders.addWidget(slid)
def clearLayout(self, layout):
if layout:
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
else :
self.clearLayout(item.layout())
layout.removeItem(item)

How to insert QWidgets in the middle of a Layout?

I'm using the Qt framework to build my graphical user interface. I use a QGridLayout to position my QWidgets neatly.
The GUI looks like this:
My application regularly adds new widgets to the GUI at runtime. These new widgets are usually not added at the end of the QLayout, but somewhere in the middle.
The procedure to do this is a bit cumbersome. Applied on the figure above, I would need to take out widg_C, widg_D, ... from the QGridLayout. Next, I add widg_xand widg_y, and finally I put the other widgets back again.
This is how I remove widgets from the QGridLayout:
for i in reversed(range(myGridLayout.count())):
self.itemAt(i).widget().setParent(None)
###
As long as you're dealing with a small amount of widgets, this procedure is not a disaster. But in my application I display a lot of small widgets - perhaps 50 or more! The application freezes a second while this procedure is ongoing, which is very annoying to the user.
Is there a way to insert widgets somewhere in a QLayout, without the need to take out other widgets?
EDIT: Apparently the solution for the QVBoxLayout is very simple. Just use the function insertWidget(..) instead of addWidget(..). The docs can be found a this link: http://doc.qt.io/qt-5/qboxlayout.html#insertWidget
Unfortunately, I couldn't find a similar function for the QGridLayout.
EDIT: Many people rightly mentioned that putting back a lot of widgets shouldn't cause a performance issue - it is very fast indeed (thank you #ekhumoro to point that out). Apparently, the performance issue I faced had to do with the algorithm putting the widgets back. It is a fairly complicated recursive algorithm that puts every widget on the right coordinates in the QGridLayout. This resulted in a "flicker" on my display. The widgets are taken out, and put back inside with some delay (due to the algorithm) - causing the flicker.
EDIT: I found a solution such that I can easily insert new rows into the QGridLayout. Inserting new rows means that I don't need to take out and replace all the widgets from scratch - hence I avoid the expensive recursive algorithm to run.
The solution can be found in my answer below.
Thank you #ekhumoro, #Stuart Fisher, #vahancho and #mbjoe for your help. I eventually found a way to solve the issue. I no longer use the QGridLayout(). Instead, I built a wrapper around the QVBoxLayout to behave as if it was a GridLayout, with an extra function to insert new rows:
class CustomGridLayout(QVBoxLayout):
def __init__(self):
super(CustomGridLayout, self).__init__()
self.setAlignment(Qt.AlignTop) # !!!
self.setSpacing(20)
def addWidget(self, widget, row, col):
# 1. How many horizontal layouts (rows) are present?
horLaysNr = self.count()
# 2. Add rows if necessary
if row < horLaysNr:
pass
else:
while row >= horLaysNr:
lyt = QHBoxLayout()
lyt.setAlignment(Qt.AlignLeft)
self.addLayout(lyt)
horLaysNr = self.count()
###
###
# 3. Insert the widget at specified column
self.itemAt(row).insertWidget(col, widget)
''''''
def insertRow(self, row):
lyt = QHBoxLayout()
lyt.setAlignment(Qt.AlignLeft)
self.insertLayout(row, lyt)
''''''
def deleteRow(self, row):
for j in reversed(range(self.itemAt(row).count())):
self.itemAt(row).itemAt(j).widget().setParent(None)
###
self.itemAt(row).setParent(None)
def clear(self):
for i in reversed(range(self.count())):
for j in reversed(range(self.itemAt(i).count())):
self.itemAt(i).itemAt(j).widget().setParent(None)
###
###
for i in reversed(range(self.count())):
self.itemAt(i).setParent(None)
###
''''''

Updating cell widgets changes in a Qtable without selecting the cell

well I'm using Pyqt4 in Maya2012 to make a reference editor alike ui. I working with a QtableWidget to make the reference list and i have subwidgets in each cell. One of the widgets is a checkbox that unload or reload the reference.
The problem i have is if a click directly in the checkbox without have the cell selected it doesn't do anything
this is my code:
def listConnections(self):
self.pos=self.sender().currentRow()
wid = self.list.ref_l.cellWidget(self.pos, 0).children()
self.text = self.list.list[self.pos]
self.ref()
for wt in wid:
if type(wt)== type(QCheckBox()):
wt.stateChanged.connect(self.changeState)
if type(wt)== type(QComboBox()):
wt.currentIndexChanged.connect(self.changeType)
I'm calling the function with a "itemSlectionChanged" signal because is the only way i knew i could detect the subwidgets.
All the subwidgets are made in the moment i fill the list.
Is there a way to make what i want?
Edit:
This is how i called the function
self.list.ref_l.itemSelectionChanged.connect(self.listConnections)
and this is how i create all the subwidgets in the cells
def fillList(self):
mayaRef = self.findRef()
if len(mayaRef)>0:
for count in range(0,len(mayaRef)):
self.ref_l.insertRow(count)
wid=QWidget()
cLayout=QHBoxLayout()
wid.setLayout(cLayout)
checkWid=QCheckBox()
nameWid=QLabel()
cLayout.addWidget(nameWid)
nameWid2=QLabel()
cLayout.addWidget(nameWid2)
comWid=QComboBox()
cLayout.addWidget(comWid)
self.ref_l.setCellWidget(count,0,wid)
self.ref_l is my QTable Widget, this is in another code that i'm calling with self.list in the original
You should set up all your connections right after you create the check boxes in fillList. Each item is associated with the path of the reference. You can use a QSignalMapper to map each check box to the path, then connect the signal mapper to your changeState slot. The signal mapper then calls that slot with the path you specified.

Categories

Resources