I'm currently developing plugin for QGIS with Python 2.7 and PyQt 4. My plugin should have checkable list of map layers (could be list of items, irrelevant) that would be regenerated each time user opens Settings tab. Although I managed to create checkable list of QCheckBoxes which regenerates each time I click Settings button, it's still far from good and functional one. As I figured out, my problem is mostly parent-child relationship and layout deletion.
self.layers = qgis_api.get_layers()
#ScrollArea setup
if (api.selected_upload_layers == [] and
api.project.layerTreeRoot().children() != []):
self.tmp_layers = qgis_api.get_layers()
self.layout = QVBoxLayout(self.settingsDock.groupBox)
self.scroll = QScrollArea()
self.layout.addWidget(self.scroll)
self.scroll.setWidgetResizable(True)
self.scroll.setFixedHeight(111)
self.scrollContent = QWidget(self.scroll)
self.scrollLayout = QVBoxLayout(self.scrollContent)
self.scrollContent.setLayout(self.scrollLayout)
self.scroll.setWidget(self.scrollContent)
i = 0
self.checkboxes = []
for layer in self.layers:
self.checkboxes.append(QCheckBox("{0}".format(layer.name())))
self.checkboxes[i].stateChanged.connect(lambda checked, i = i : self.cBoxChecked(self.checkboxes[i])) #inverts logic if run every time
# check logic
if i < len(self.layers)-1:
i += 1
# Create checkboxes first time
if not api.upload: #api.upload becomes true when clicked save in settings
for i in range(0, len(self.layers)):
try:
self.scrollLayout.addWidget(self.checkboxes[i])
self.checkboxes[i].setChecked(True)
except Exception as e:
print str(e)
# compare layer list at creation and now to notice difference
elif self.tmp_layers != self.layers:
for i in range(0, self.scrollLayout.count()):
self.scrollLayout.removeItem(self.scrollLayout.itemAt(0))
try: # delete old layer items
for i in range(0, len(self.layers)):
self.scrollLayout.addWidget(self.checkboxes[i])
self.checkboxes[i].setChecked(True)
except Exception as e:
print str(e)
Function cBoxChecked() is as follows:
def cBoxChecked(self, cBox):
""" Add functionality to ScrollArea checkboxes."""
if cBox.isChecked():
if cBox.text() not in api.selected_upload_layers:
api.selected_upload_layers.append(cBox.text())
else:
try:
api.selected_upload_layers.remove(cBox.text())
except Exception as e:
print str(e)
Alhtough now i don't get any exceptions, and the list is regenerated. I'm noticing old list underneath the new one, which means that I'm not deleting layout correctly (there are various questions for layout deletion) but I couldn't figure it out completely. To sum it up. What is the most convenient way of destroying and recreating layout and how to figure out parent-child QObject relationships in this concrete example? And one more question that bothers me is that with every new opening of settings tab, there seems to be logic revertion in lambda function that does all the heavy lifting for selecting certain CheckBox. How to fix that?
Thank you for your time :)
I fixed it. The main problem was removeItem method which didn't worked as planned. Although the whole thing was a mess. What I did now? I created checkboxes list at the class initialization as empty list and with correct conditions I'm checking is it first time the SettingsDock is invoked, or is it invoked after something changed in layer interface list. The code is as folows.If something is not explained properly in comments, feel free to ask why i did it that way. Cheers
self.layers = qgis_api.get_layers()
reduced_layers = []
# reduced_layers is list variable which is populated with all layers
# without redundant ones (same names of multiple files .shp/.shx/.dbf)
# shapefile file format
for layer in self.layers:
if layer.name() not in reduced_layers:
reduced_layers.append(layer.name())
# ScrollArea setup
# Set up settingsDock.groupBox as a parent of Vertical layout
# Check if Settings was clicked before api.upload would be handy for
# that, scroll is QScrollArea and is added as widget with layout as
# parent, after that I set up Scroll Content as widget with scroll
# as parent, while scroll Layout is Vertical layout with scrollContent
# as parent, but then i use scroll.setWidget method to define it as
# a parent to scrollContent
if (api.selected_upload_layers == [] and
api.project.layerTreeRoot().children() != []):
self.tmp_layers = qgis_api.get_layers()
self.layout = QVBoxLayout(self.settingsDock.groupBox)
self.scroll = QScrollArea()
# self.layout.addWidget(self.scroll)
self.scroll.setWidgetResizable(True)
self.scroll.setFixedHeight(111)
self.layout.addWidget(self.scroll)
self.scrollContent = QWidget(self.scroll)
self.scrollLayout = QVBoxLayout(self.scrollContent)
self.scroll.setWidget(self.scrollContent)
# As self.checkboxes are initialized as empty list, here are we
# generating a list of QCheckBox items with layer names(if there are
# multiple layers with same name(shapefile format), only one will be
# added as checkbox
# After generating checkboxes list we use it to populate widgets,
# QCheckBoxes in ScrollLayout, and set it in default as Checked
# This is basically 1st time initialization
if self.checkboxes == []:
try:
for i in range(0, len(self.layers)):
if self.layers[i].name() not in map(lambda x: x.text(),
self.checkboxes):
self.checkboxes.append(QCheckBox('{}'.format(
self.layers[i].name())))
for i in range(0, len(self.checkboxes)):
self.scrollLayout.addWidget(self.checkboxes[i])
self.checkboxes[i].setChecked(True)
api.selected_upload_layers = map(lambda x: x.text(),
self.checkboxes)
except Exception as e:
print str(e)
# if checkboxes are different from layer list (that is generated) in
# the moment of clicking show settings which basically indicates that
# interface layer list has been chaged, we must update checkboxes
# To update checkboxes list it's firstly deleted with ScrollContent
elif map(lambda x: x.text(), self.checkboxes) != reduced_layers:
num = self.scrollLayout.count()
self.scrollLayout.removeWidget(self.scrollContent)
self.scrollContent.deleteLater()
self.scrollContent = QWidget(self.scroll)
self.scrollLayout = QVBoxLayout(self.scrollContent)
self.scroll.setWidget(self.scrollContent)
try:
self.checkboxes = []
for i in range(0, len(self.layers)):
if self.layers[i].name() not in map(lambda x: x.text(), self.checkboxes):
self.checkboxes.append(QCheckBox('{}'.format(
self.layers[i].name())))
for i in range(0, len(self.checkboxes)):
self.scrollLayout.addWidget(self.checkboxes[i])
self.checkboxes[i].setChecked(True)
except Exception as e:
print (e)
for i in range(0, len(self.checkboxes)):
self.checkboxes[i].stateChanged.connect(lambda checked, i=i:
self.checkBoxChecked())
Function checkBoxChecked() is as follows:
def checkBoxChecked(self):
"""Add functionality to ScrollArea checkboxes."""
#print api.selected_upload_layers
indices = []
for i in range(0, len(self.checkboxes)):
if self.checkboxes[i].isChecked():
# print cBox.isChecked()
print self.checkboxes[i].text() + " is selected"
indices.append(i)
else:
print self.checkboxes[i].text() + " is deselected"
api.selected_upload_layers = [map(lambda x: x.text(), self.checkboxes)[i] for i in indices]
Related
I am trying to have a series of QTableView created at runtime and added to newly created pages of a multipage QTabWidget.
All seems to go fine, but the QTableView don't show up.
The QTabWidget gets zeroed (reset to no pages) and refurbished (...) flawlessly (at least it looks like so) depending on the selection of a combobox (and the dictionaries therein related).
I am also using a delegate callback to include a column of checkboxes to the QTableView (thanks to https://stackoverflow.com/a/50314085/7710452), which works fine stand alone.
Here is the code.
Main Window
EDIT
as recommended by eyllanesc, here is the standalone module (jump to the end of the post for details on the part I think is problematic):
"""
qt5 template
"""
import os
import sys
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtGui as qtg
from PyQt5 import uic
from configparser import ConfigParser, ExtendedInterpolation
from lib.SearchControllers import findGuis, get_controller_dict, show_critical, show_exception
import resources.resources
from lib.CheckBoxesDelegate import CheckBoxDelegate
myForm_2, baseClass = uic.loadUiType('./forms/setup.ui')
class MainWindow(baseClass):
def __init__(self, config_obj: ConfigParser,
config_name: str,
proj_name: str,
*args,
**kwargs):
super().__init__(*args, **kwargs)
self.ui = myForm_2()
self.ui.setupUi(self)
# your code begins here
self.setWindowTitle(proj_name + " Setup")
self.ui.logo_lbl.setPixmap(qtg.QPixmap(':/logo_Small.png'))
self.config_obj = config_obj
self.config_name = config_name
self.proj_filename = proj_name
self.proj_config = ConfigParser(interpolation=ExtendedInterpolation())
self.proj_config.read(proj_name)
self.guis_dict = {}
self.components = {}
self.cdp_signals = {}
self.root_path = self.config_obj['active']['controllers']
self.tableViews = []
self.tabs = []
self.iniControllersBox()
self.setActSignals()
self.load_bulk()
self.set_signals_table()
self.update_CurController_lbl()
self.update_ControllersTab() # here is where the action gets hot
# your code ends here
self.show() # here crashes if I passed the new tab to the instance of
# QTabView. otherwise it shows empty tabs
#########################################################
def load_bulk(self):
# get the list of running components into a dictionary
for i in self.list_controllers:
i_path = os.path.join(self.root_path, i)
print(i)
self.components[i] = get_controller_dict(i_path,
self.config_obj,
'Application.xml',
'Subcomponents/Subcomponent',
'Name',
'src')
for j in self.components[i]:
print(j)
signals_key = (i , j)
tgt = os.path.join(self.root_path, self.components[i][j])
self.cdp_signals[signals_key] = get_controller_dict(i_path,
self.config_obj,
self.components[i][j],
'Signals/Signal',
'Name',
'Type',
'Routing')
def set_signals_table(self):
self.ui.MonitoredDevicesTable.setHorizontalHeaderItem(0, qtw.QTableWidgetItem('GUI caption'))
self.ui.MonitoredDevicesTable.setHorizontalHeaderItem(1, qtw.QTableWidgetItem('Monitored Signal'))
def setActSignals(self):
self.ui.controllersBox.currentIndexChanged.connect(self.update_guis_list)
self.ui.controllersBox.currentIndexChanged.connect(self.update_CurController_lbl)
self.ui.controllersBox.currentIndexChanged.connect(self.update_ControllersTab)
def update_ControllersTab(self):
self.ui.componentsTab.clear() # this is the QTabWidget
self.tabs = []
self.tableViews = []
curr_controller = self.ui.controllersBox.currentText()
for i in self.components[curr_controller]:
if len(self.cdp_signals[curr_controller, i]) == 0:
continue
self.tabs.append(qtw.QWidget())
tabs_index = len(self.tabs) - 1
header_labels = ['', 'Signal', 'Type', 'Routing', 'Input']
model = qtg.QStandardItemModel(len(self.cdp_signals[curr_controller, i]), 5)
model.setHorizontalHeaderLabels(header_labels)
# in the next line I try to create a new QTableView passing
# the last tab as parameter, in the attempt to embed the QTableView
# into the QWidget Tab
self.tableViews.append(qtw.QTableView(self.tabs[tabs_index]))
tbw_Index = len(self.tableViews) - 1
self.tableViews[tbw_Index].setModel(model)
delegate = CheckBoxDelegate(None)
self.tableViews[tbw_Index].setItemDelegateForColumn(0, delegate)
rowCount = 0
for row in self.cdp_signals[curr_controller, i]:
for col in range(len(self.cdp_signals[curr_controller, i][row])):
index = model.index(rowCount, col, qtc.QModelIndex())
model.setData(index, self.cdp_signals[curr_controller, i][row][col])
try:
self.ui.componentsTab.addTab(self.tabs[tabs_index], i) # no problems, some controllers ask up to
except Exception as ex:
print(ex)
def update_CurController_lbl(self):
self.ui.active_controller_lbl.setText(self.ui.controllersBox.currentText())
def iniControllersBox(self):
self.list_controllers = [os.path.basename(f.path) for f in os.scandir(self.root_path) if f.is_dir() and str(
f.path).upper().endswith('NC')]
self.ui.controllersBox.addItems(self.list_controllers)
for i in range(self.ui.controllersBox.count()):
self.ui.controllersBox.setCurrentIndex(i)
newKey = self.ui.controllersBox.currentText()
cur_cntrlr = os.path.join(self.config_obj['active']['controllers'], self.ui.controllersBox.currentText())
self.guis_dict[newKey] = findGuis(cur_cntrlr, self.config_obj)
self.ui.controllersBox.setCurrentIndex(0)
self.update_guis_list()
def update_guis_list(self, index=0):
self.ui.GuisListBox.clear()
self.ui.GuisListBox.addItems(self.guis_dict[self.ui.controllersBox.currentText()])
if __name__ == '__main__':
config = ConfigParser()
config.read('./config.ini')
app = qtw.QApplication([sys.argv])
w = MainWindow(config, './config.ini',
'./test_setup_1.proj')
sys.exit(app.exec_())
and here the external to add the checkboxes column:
class CheckBoxDelegate(QtWidgets.QItemDelegate):
"""
A delegate that places a fully functioning QCheckBox cell of the column to which it's applied.
"""
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.
'''
model.setData(index, 1 if int(index.data()) == 0 else 0, QtCore.Qt.EditRole)
The 1st picture shows the layout in QTDesigner, the 2nd the result (emtpy tabs) when avoiding the crashing.
the QTabWidget has no problems in zeroing, or scale up, back to as many tab as I need, it's just that I have no clue on how to show the QTabview. My approach was to try to embed the QTabView in the tabpage passing it as parameter to the line creating the new QTabView.
Since I am using rather convoluted dictionaries, calling an XML parser to fill them up, not to mention the config files, I know even this version of my script is hardly reproduceable/runnable.
If someone had the patience of focusing on the update_ControllersTab method though, and tell me what I am doing wrong handling the QWidgets, it'd be great.
Again the basic idea is to clear the QTabWidget any time the user selects a different controller (combo box on the left):
self.ui.componentsTab.clear() # this is the QTabWidget
self.tabs = [] # list to hold QTabView QWidgets (pages) throughout the scope
self.tableViews = [] # list to hold QTabView(s) thorughout the scope
count how many tabs (pages) and hence embedded TabViews I need with the new controllers selected.
and then for each tab needed:
create a new tab (page)
self.tabs.append(qtw.QWidget())
tabs_index = len(self.tabs) - 1
create a new QTabView using a model:
header_labels = ['', 'Signal', 'Type', 'Routing', 'Input']
model = qtg.QStandardItemModel(len(self.cdp_signals[curr_controller, i]), 5)
model.setHorizontalHeaderLabels(header_labels)
self.tableViews.append(qtw.QTableView(self.tabs[tabs_index]))
tbw_Index = len(self.tableViews) - 1
self.tableViews[tbw_Index].setModel(model)
populate the TableView with data, and then finally add the tab widget (with the suppposedly embedded QTableView to the QTabWidget (the i argument is a string from my dbases Names:
self.ui.componentsTab.addTab(self.tabs[tabs_index], i)
This method is called also by the __init__ to initialize and apparently all goes error free, until the last 'init' statement:
`self.show()`
at which point the app crashes with:
Process finished with exit code 1073741845
on the other hand, if here instead of trying to embed the QTableView:
self.tableViews.append(qtw.QTableView(self.tabs[tabs_index]))
I omit the parameter, that is:
self.tableViews.append(qtw.QTableView())
the app doesn't crash anymore, but of course no QtableViews are shown, only empty tabpages:
As stupid as this may sound the problem is in... the delegate class that creates the checkboxes in the first column (see https://stackoverflow.com/a/50314085/7710452)
I commented out those two lines:
delegate = CheckBoxDelegate(None)
self.tableViews[tbw_Index].setItemDelegateForColumn(0, delegate)
and... bingo!
the CheckBoxDelegate works fine in the example shown in the post (a single QTableView form). I also tinkered around adding columns and rows, and moving the checkbox column back and forth with no problems. In that standalone. But as soon as I add the class and set the delegate, i am back at square zero, app crashing with:
Process finished with exit code 1073741845
so I am left with this problem now. Thnx to whomever read this.
Problem solved, see comment to post above.
I have an interface with two tab: in the first one i ask the user to enter parameters and in the second one i want to print the following QTableWidget.
So basically on the first tab i have a QPushButton that i called process and normally, when i push on it , i want to send the information to the second Tab.
Right now i just tried to show a new window with the QTableWidget and the good parameters :
class Parameters(QWidget):
def __init__(self):
super(Parameters, self).__init__()
self.matrixsize = QLineEdit()
bouton = QPushButton("define matrix_size")
bouton.clicked.connect(self.appui_bouton)
self.halfmatrix = QCheckBox()
self.halfmatrix.toggled.connect(self.on_checked)
self.define_matrix_size = QGroupBox('Define Parameters')
layout = QGridLayout()
layout.addWidget(self.matrixsize, 0, 0, 1, 1, )
layout.addWidget(bouton, 0, 1, 1, 1)
layout.addWidget(QLabel('select half size mode'
), 1, 0, 1, 1)
layout.addWidget(self.halfmatrix, 1, 1, 1, 1)
self.define_matrix_size.setLayout(layout)
process = QPushButton('process')
process.clicked.connect(self.process)
self.matrix = QTableWidget()
self.layout = QGridLayout()
self.layout.addWidget(self.define_matrix_size)
self.layout.addWidget(matrix)
self.layout.addWidget(process)
self.setLayout(self.layout)
def matrix_size(self):
if self.matrixsize.text() == "":
return 0
else:
return int(self.matrixsize.text())
def appui_bouton(self):
taille = self.matrixsize()
self.matrix.deleteLater()
if self.halfmatrix.isChecked():
self.on_checked()
else:
self.matrix = QTableWidget()
self.matrix.setColumnCount(taille)
self.matrix.setRowCount(taille)
self.layout.addWidget(self.matrix)
self.update()
self.setLayout(self.layout)
def keyPressEvent(self, qKeyEvent):
print(qKeyEvent.key())
if qKeyEvent.key() == Qt.Key_Return or qKeyEvent.key() == Qt.Key_Enter:
self.appui_bouton()
else:
super().keyPressEvent(qKeyEvent)
def on_checked(self):
taille = self.matrixsize()
if taille == 0:
pass
else:
if self.halfmatrix.isChecked():
size = int(taille / 2)
self.matrix.deleteLater()
self.matrix = QTableWidget()
self.matrix.setColumnCount(size)
self.matrix.setRowCount(size)
self.layout.addWidget(self.matrix, 3, 0, 20, 4)
self.update()
self.setLayout(self.layout)
else:
self.appui_bouton()
def process (self):
layout = QHBoxLayout()
test = self.matrix
test.setLayout(layout)
test.show()
So in order to clarify what i said: i have a Window on which you get some parameters (size,...) , when you select those parameters, let's say you take matrixsize==5, then a 5x5 table is added to the window. This table can be after this fill by others parameters (i cut them on the code) by a system of drag and drop.
So now that i got a built table, i want to be able to open a new window with just the table by clicking on the ''process'' button.
So i don't want a dynamical table, i just want a table that keeps the same property (for instance if the matrix has dragonly enable then the new matrix should have the same) . I want to keep every information containing in the cells
I hope i am enoughly clear that is my first time asking questions (after many times reading some answers of course^^)
thanks for your answer and advice !
You can just create a new QTableWidget with no parent (which makes it a top level window), and then show it:
class Parameters(QWidget):
# ...
def process(self):
rows = self.matrix.rowCount()
columns = self.matrix.columnCount()
self.newTable = QTableWidget(rows, columns)
for row in range(rows):
for column in range(columns):
source = self.matrix.item(row, column)
if source:
self.newTable.setItem(row, column, QTableWidgetItem(source))
self.newTable.show()
Note that I created the new table as an instance attribute. This allows to avoid the garbage collection in case it was a local variable (resulting in the widget showing and disappearing right after), but has the unfortunate effect that if you click on the process button again and a window already exists, it gets deleted and "overwritten" with a new window. If you want to have more process windows at the same time, you could add them to a list:
class Parameters(QWidget):
def __init__(self):
super(Parameters, self).__init__()
# ...
self.processTables = []
def process(self):
rows = self.matrix.rowCount()
columns = self.matrix.columnCount()
# note that now "newTable" is *local*
newTable = QTableWidget(rows, columns)
self.processTables.append(newTable)
# ...
Some suggestions about your code:
there's absolutely no need to create a new table each time you want to change its size; just use setRowCount and setColumnCount on the existing one, and if you don't want to keep previous values, use clear();
don't use two functions that do almost the same things (appui_bouton and on_checked) and call each other, just use one function that checks for both aspects;
don't call update() unnecessarily: when you change the properties of a widget (or add a new widget to a layout) update is called already; while it's not an actual issue (Qt automatically manages when updates actually happen, avoiding repainting if not necessary), calling it just adds unnecessary noise to your code;
be more careful when adding widgets to a grid layout (I'm referring to the code on on_checked): don't use the rowSpan and columnSpan if not required; also, using a value that high is completely useless, as there are no other widgets in that row, and there's actually only one column in that layout; also, don't call setLayout() again;
if you need a numerical value, then use a QSpinBox, not a QLineEdit.
The function to update the existing table can be rewritten more easily, and you should connect both the button and the checkbox to it:
class Parameters(QWidget):
def __init__(self):
super(Parameters, self).__init__()
self.matrixsize = QSpinBox()
bouton = QPushButton("define matrix_size")
bouton.clicked.connect(self.appui_bouton)
self.halfmatrix = QCheckBox()
self.halfmatrix.toggled.connect(self.appui_bouton)
# ...
def appui_bouton(self):
taille = self.matrixsize.value()
if self.halfmatrix.isChecked():
taille //= 2
if not taille:
return
self.matrix.setColumnCount(taille)
self.matrix.setRowCount(taille)
I have a python tkinter application that contains a ttk.treeview widget.
The widget displays a list of files found on a specific directory tree with a specific extension - this was trivial to build with tt.treeview widget.
There is a request to enable "on-the-fly" filtering of the tree - e.g., the user types in an Entry some string, and as he/she types, the tree removes elements that don't match the typed string so far.
I was exploring the Treeview documentation, tried the detach and reattach methods but with no luck.
detach indeed removes the non-matched elements from the tree, but if the user hit Backspace, I can no longer iterate correctly on the tree to restore those detached elements as get_children method will not return them.
def filter_tree(self):
search_by = self.search_entry.get()
self.tree_detach_leaf_by_regex(self.current_loaded_folder, search_by, "")
def tree_detach_leaf_by_regex(self, root, regex, parent):
if self.treeview.get_children(root):
for child in self.treeview.get_children(root):
self.tree_detach_leaf_by_regex(child, regex, root)
else:
if not re.match(regex, self.treeview.item(root)["text"]):
self.elements_index_within_parent[root] = self.treeview.index(root)
self.elements_parents[parent] = 1
self.treeview.detach(root)
else:
self.treeview.reattach(root, parent, self.elements_index_within_parent[root])
Looking forward to read your advice.
To make my answer reusable by anybody, I have to tell more than directly answering your question. If you directly want to see how I do to get detached items (thus without using the method get_children which cannot get detached items' id), jump to the definition of the method whose name is _columns_searcher.
Introduction
Let's first define some attributes.
#property
def _to_search(self):
key = 'to_search'
if key not in self._cache:
self._cache[key] = tk.StringVar()
return self._cache[key]
def _set_search_entry(self):
ent = ttk.Entry(
self.root, # or canvas, or frame ...
#...
textvariable=self._to_search
)
ent.grid(
#...
)
ent.bind(
'<Return>',
self._columns_searcher
)
return ent
#property
def search_entry(self):
key = 'search_entry'
if key not in self._cache:
self._cache[key] = self._set_search_entry()
return self._cache[key]
Core answer
What follows is the part which directly show how to re-attach user-detached items. First note that, as the OP mentions, get_children only return ids of attached items. Second note that the only thing you need to re-attach detached items is their id. Which implies thus to trace/save them when they are detached so as to be able to re-attach them.
_detached = set()
def _columns_searcher(self, event):
# originally a set returns a tuple
children = list(self._detached) + list(self.tree.get_children())
self._detached = set()
query = self._to_search.get()
self._brut_searcher(children, query.lower())
Note that children above contains all items, be them detached.
def _brut_searcher(self, children, query):
i_r = -1
for item_id in children:
text = self.tree.item(item_id)['text'] # already contains the strin-concatenation (over columns) of the row's values
if query in text:
i_r += 1
self.tree.reattach(item_id, '', i_r)
else:
self._detached.add(item_id)
self.tree.detach(item_id)
From what I see, detach is almost same as delete.
row is gone and you have no access to it.
You have to make a copy of "detached" items, just id, name or more, if you have advance treeview structure, and then go over elements in both lists and sort it out.
Difference is that if you detach item and check it's id with "exists" function, it should return true
This question has been asked before at:
https://stackoverflow.com/questions/26538667/pyqt-populate-qtreeview-from-txt-file-that-contains-file-paths
But didn't seem to get a response.
I have a dataset of file paths that are formatted, like so:
hon_dev/Bob Dylan/Concept
hon_dev/Andromeda/Modeling
hon_dev/Andromeda/Lookdev
hon_dev/Andromeda/Rigging
hon_dev/Andromeda/Animation
hon_dev/Andromeda/FX
hon_dev/fsafasfas/production
hon_dev/Magebane: Acheron of Mana Aeacus/Model
hon_dev/Magebane: Acheron of Mana Aeacus/Concept
hon_dev/Magebane: Acheron of Mana Aeacus/Texture
hon_dev/Skrull/Modeling
hon_dev/Skrull/Lookdev
hon_dev/Skrull/Rigging
hon_dev/Skrull/Animation
hon_dev/Skrull/FX
hon_dev/Bob Mylan/Modeling
hon_dev/Bob Mylan/Lookdev
hon_dev/Bob Mylan/Rigging
hon_dev/Bob Mylan/Animation
hon_dev/Bob Mylan/FX
hon_dev/Handsome Man/Concept
hon_dev/Handsome Man/Modeling
hon_dev/Handsome Man/Lookdev
hon_dev/Handsome Man/Rigging
hon_dev/Handsome Man/Animation
hon_dev/Handsome Man/FX
demo-sync/Drone Craft/Modelling Drone Craft
demo-sync/Drone Craft/Texturing and Shading of Drone Craft
demo-sync/Drone Craft/Rigging Drone Parts
And I'm trying to get them to fill up a QTreeView (PySide). The current code I have is as such, with a simple recursive function:
def doIt(self):
self.model = QtGui.QStandardItemModel()
# self.model.setHorizontalHeaderLabels = ['test']
topLevelParentItem = self.model.invisibleRootItem()
# create all itewms first
# iterate over each string url
for item in data:
splitName = item.split('/')
# first part of string is defo parent item
# check to make sure not to add duplicate
if len(self.model.findItems(splitName[0], flags=QtCore.Qt.MatchFixedString)) == 0:
parItem = QtGui.QStandardItem(splitName[0])
topLevelParentItem.appendRow(parItem)
def addItems(parent, elements):
# check if not reached last item in the list of items to add
if len(elements) != 0:
print "currently eval addItems({0}, {1}".format(parent.text(), elements)
# check if item already exists, if so do not create
# new item and use existing item as parent
if len(self.model.findItems(elements[0], flags=QtCore.Qt.MatchFixedString)) == 0:
print "item being created for {0}".format(elements[0])
item = QtGui.QStandardItem(elements[0])
else:
print "not adding duplicate of: {0}".format(elements[0])
item = self.model.findItems(elements[0], flags=QtCore.Qt.MatchFixedString)[0]
print "the item to act as non-duplicate is: {0}".format(item.text())
child = elements[1:]
print "child is {0}".format(child)
# call recursive function to add
addItems(item, child)
print "parenting: {0} to {1}".format(item.text(), parent.text())
parent.appendRow(item)
addItems(parItem, splitName[1:])
print 'done: ' + item + '\n'
self.inst.col_taskList.setModel(self.model)
However, because I can't find any way to look through a QStandardItem for existing rows, I'm getting this in the UI as a result:
Is there a way to find duplicates rows in a QStandardItem or traverse the QStandardItemModel to find the existing QStandardItem? I've been struggling with this problem for the past 2 days and trying to find an existing example, and I can't really wrap my head around how this could be such a complication...
Any help/advice on this would be appreciated! Thanks!
Hmm, after a bit of faffing about, I've come up with something that works for now, though the file paths must be in order for this to work:
def doIt(self):
print "\n\n\n\n"
self.model = QtGui.QStandardItemModel()
topLevelParentItem = self.model.invisibleRootItem()
# iterate over each string url
for item in data:
splitName = item.split('/')
# first part of string is defo parent item
# check to make sure not to add duplicate
if len(self.model.findItems(splitName[0], flags=QtCore.Qt.MatchFixedString)) == 0:
parItem = QtGui.QStandardItem(splitName[0])
topLevelParentItem.appendRow(parItem)
def addItems(parent, elements):
"""
This method recursively adds items to a QStandardItemModel from a list of paths.
:param parent:
:param elements:
:return:
"""
for element in elements:
# first check if this element already exists in the hierarchy
noOfChildren = parent.rowCount()
# if there are child objects under specified parent
if noOfChildren != 0:
# create dict to store all child objects under parent for testing against
childObjsList = {}
# iterate over indexes and get names of all child objects
for c in range(noOfChildren):
childObj = parent.child(c)
childObjsList[childObj.text()] = childObj
if element in childObjsList.keys():
# only run recursive function if there are still elements to work on
if elements[1:]:
addItems(childObjsList[element], elements[1:])
return
else:
# item does not exist yet, create it and parent
newObj = QtGui.QStandardItem(element)
parent.appendRow(newObj)
# only run recursive function if there are still elements to work on
if elements[1:]:
addItems(newObj, elements[1:])
return
else:
# if there are no existing child objects, it's safe to create the item and parent it
newObj = QtGui.QStandardItem(element)
parent.appendRow(newObj)
# only run recursive function if there are still elements to work on
if elements[1:]:
# now run the recursive function again with the latest object as the parent and
# the rest of the elements as children
addItems(newObj, elements[1:])
return
# call proc to add remaining items after toplevel item to the hierarchy
print "### calling addItems({0}, {1})".format(parItem.text(), splitName[1:])
addItems(parItem, splitName[1:])
print 'done: ' + item + '\n'
self.inst.col_taskList.setModel(self.model)
I am new to PySide. In my program, I encountered a problem that when I click one button, it triggers other button later added. Thanks!
self.addContentButton = QtGui.QPushButton('Add')
self.addContentButton.clicked.connect(self.addContent)
def addContent(self):
'''
slot to add a row that include a lineedit, combobox, two buttons
'''
self.contentTabHBoxWdgt = QtGui.QWidget()
self.contentName = QtGui.QLineEdit('line edit')
self.conetentTypeBox = QtGui.QComboBox()
self.conetentTypeBox.addItem('elem1')
self.conetentTypeBox.addItem('elem2')
self.contentSave = QtGui.QPushButton('save',parent = self.contentTabHBoxWdgt)
self.contentSave.clicked.connect(self.contntSaveAct)
self.contentDelete = QtGui.QPushButton('delete',parent=self.contentTabHBoxWdgt)
self.contentDelete.clicked.connect(self.contntDel)
self.contentTabHBox = QtGui.QHBoxLayout()
self.contentTabHBox.addWidget(self.contentName)
self.contentTabHBox.addWidget(self.conetentTypeBox)
self.contentTabHBox.addWidget(self.contentSave)
self.contentTabHBox.addWidget(self.contentDelete)
self.contentTabHBoxWdgt.setLayout(self.contentTabHBox)
self.contentTabVBox.addWidget(self.contentTabHBoxWdgt)
def contntDel(self):
'''
slot to delete a row
'''
msgBox = QtGui.QMessageBox(QtGui.QMessageBox.Warning, '', 'Be sure to delete')
okBttn = msgBox.addButton('Yes', QtGui.QMessageBox.AcceptRole)
noBttn = msgBox.addButton('Cancel', QtGui.QMessageBox.RejectRole)
ret = msgBox.exec_()
if msgBox.clickedButton() == okBttn:
self.contentTabVBox.removeWidget(self.contentDelete.parentWidget());
When I Add one row and click its delete button, it does not work as expected.While I add two or three row , I click one delete button , it remove one row that is not the clicked delete button belong to. How could I achieve this function. Ths!
Your problem is because you aren't really taking advantage of object oriented programming properly.
All rows in your example call the same instance of the method contntDel. This method uses self.contentDelete which always contains a reference to the last row added.
What you need to do is separate out everything related to a row to a new class. When you add a row, create a new instance of this class and pass in the contentTabVBox. That way each row (or instance of the new class you will write) will have it's own delete method.
Without a complete code example, I can't provide a complete solution, but this should give you a rough idea:
class MyRow(object):
def __init__(self,contentTabVBox, rows):
self.contentTabVBox = contentTabVBox
self.my_list_of_rows = rows
self.addContent()
def addContent(self):
# The code for your existing addContent method here
def contntDel(self):
# code from your existing contntDel function here
# also add (if Ok button clicked):
self.my_list_of_rows.remove(self)
class MyExistingClass(??whatever you have here normally??):
def __init__(....):
self.addContentButton = QtGui.QPushButton('Add')
self.addContentButton.clicked.connect(self.addContent)
self.my_list_of_rows = []
def addContent(self):
my_new_row = MyRow(self.contentTabVBox,self.my_list_of_rows)
# You mustsave a reference to my_new_row in a list or else it will get garbage collected.
self.my_list_of_rows.append(my_new_row)
Hope that helps!