Related
This is a small file manager, you can right-click the tree items to rename them. after renaming the first-level folder, clicking the folder below it will get the wrong path. you can see it at the top of the window, or use this code to check.
index = widget.treeView.currentIndex()
model = index.model()
path = model.fileInfo(index).absoluteFilePath()
At the same time, if you rename the second-level folder in that first-level folder multiple times, it will cause strange problems. for example, rename fails or that folder disappears. I think QFileSystemModel is not refreshed after the name change!
The following is the complete code.
import sys
from PySide2.QtWidgets import QApplication, QWidget, QFileSystemModel, QTreeView, QPushButton, QLabel, QVBoxLayout, QHBoxLayout, QGridLayout, QMenu, QInputDialog, QLineEdit
from PySide2.QtCore import Qt, QEvent
class MyWidget(QWidget):
def __init__(self, parent=None):
super(MyWidget, self).__init__(parent)
ROOT = "F:/New Folder"
self.treeModel = QFileSystemModel()
self.treeModel.setRootPath(ROOT)
self.treeView = QTreeView()
self.treeView.setModel(self.treeModel)
self.treeView.setRootIndex(self.treeModel.index(ROOT))
self.treeView.setColumnHidden(1,True)
self.treeView.setColumnHidden(2,True)
self.treeView.setColumnHidden(3,True)
self.treeView.installEventFilter(self) # QEvent.ContextMenu
# for test -----------------------------------
self.treeView.clicked.connect(lambda index: self.show_path(index))
treeSelection = self.treeView.selectionModel()
treeSelection.currentChanged.connect(lambda index, pre_index: self.tree_selection_slot(index, pre_index))
labelA = QLabel("model path:")
self.labelA2 = QLabel()
labelB = QLabel("treeView clicked:")
self.labelB2 = QLabel()
labelC = QLabel("tree selection changed:")
self.labelC2 = QLabel()
grid = QGridLayout()
grid.addWidget(labelA, 0, 0)
grid.addWidget(self.labelA2, 0, 1)
grid.addWidget(labelB, 1, 0)
grid.addWidget(self.labelB2, 1, 1)
grid.addWidget(labelC, 2, 0)
grid.addWidget(self.labelC2, 2, 1)
# for test -------------------------------END.
layout = QVBoxLayout()
layout.addLayout(grid)
layout.addWidget(self.treeView)
self.setLayout(layout)
def eventFilter(self, source, event):
""" mouse right click rename menu """
if event.type() == QEvent.ContextMenu and source is self.treeView:
gp = event.globalPos()
lp = self.treeView.viewport().mapFromGlobal(gp)
index = self.treeView.indexAt(lp)
if not index.isValid():
return
menu = QMenu()
rename_act = menu.addAction("rename folder")
rename_act.triggered.connect(lambda: self.change_name(index))
menu.exec_(gp)
return True
return super(MyWidget, self).eventFilter(source, event)
def change_name(self, index):
""" rename """
if not index.isValid():
return
model = index.model()
old_name = model.fileName(index)
path = model.fileInfo(index).absoluteFilePath()
# ask new name
name, ok = QInputDialog.getText(self, "New Name", "Enter a name", QLineEdit.Normal, old_name)
if not ok or not name:
return
# rename
model = index.model()
wasReadOnly = model.isReadOnly()
model.setReadOnly(False)
model.setData(index, name)
model.setReadOnly(wasReadOnly)
def show_path(self, index):
""" for test """
if not index.isValid():
return
model = index.model()
path = model.fileInfo(index).absoluteFilePath()
self.labelB2.setText(path)
def tree_selection_slot(self, index, pre_index):
""" for test """
if not index.isValid():
return
model = index.model()
path = model.fileInfo(index).absoluteFilePath()
self.labelC2.setText(path)
if not QApplication.instance():
app = QApplication(sys.argv)
else:
app = QApplication.instance()
widget = MyWidget()
widget.show()
sys.exit(app.exec_())
UPDATE 1: new code failed.
I tried to "rename" several times with the new code, but the problem still exists.
my operation is:
rename "Fallout4" to "mass effects". # success
then "New Folder-01" to "vol.2". # success
"New Folder" to "vol.3". # success
"AB" to "BioShock". # success
"New Folder" to "bs2" to. # the folder is actually renamed. but the icon of "bs2" became "blank" and can't rename anymore.
all operations are separated by 2 to 5 seconds.
There are two problems, both related to the way QFileSystemModel is internally implemented, which preserves a cache of the loaded indexes and node functions (including QFileInfo), adds a QFileSystemWatcher for each visited directory and, most importantly, uses a separate thread.
The path issue is related to QFileInfo (returned by fileInfo(index)), which by its nature does not automatically update itself: once a QFileInfo instance is created (and cached, in the case of QFileSystemModel), it doesn't update its data. Since the model presumably associates the index with the QFileInfo, it returns the same (not updated) object. This is also probably related to QTBUG-33720).
To get the actual path, I suggest you to use filePath() instead (you can use QFileInfo(model.filePath(index)), which will create a new and updated instance in order to get the correct name).
The renaming issue depends on the thread part: when setting the `readOnly` property, it takes some time for the "flag to take effect", and doing it right after the QInputDialog doesn't leave Qt enough time for that.
Note: I'm not really sure about what actually happens and why, but you can check that for yourself: if you use `setReadOnly` *before* showing the dialog, it works.
If somebody with more experience on Qt and C++ might shed some light on this, I'll be glad to know about it and update this answer.
So, as a "workaround", you can change your implementation to the following:
def change_name(self, index):
if not index.isValid():
return
model = index.model()
old_name = model.fileName(index)
wasReadOnly = model.isReadOnly()
model.setReadOnly(False)
name, ok = QInputDialog.getText(self, "New Name", "Enter a name", QLineEdit.Normal, old_name)
if ok and name and name != old_name:
model.setData(index, name)
model.setReadOnly(wasReadOnly)
there are 3 folders in one directory:
f:/root_folder/folder1
f:/root_folder/_folder2
f:/root_folder/folder3.asset
i try to customize QSortFilterProxyModel.filterAcceptsRow() for only show folders that with the .asset suffix.
** i know i can use QFileSystemModel.setNameFilters([*.asset]) do that. but sometimes it doesn't work.
i have Python3.7 + PySide2 5.13.0.
# first inherit a QFileSystemModel instance:
listModel = QFileSystemModel()
# let the instance display only folders, except for '.' and '..':
listModel.setFilter(QDir.NoDotAndDotDot | QDir.Dirs)
# assgin a root path. i just want the model to search the 'f:/root_folder':
listModel.setRootPath("f:/root_folder")
# add a custom QSortFilterProxyModel:
myProxy = myProxyModel()
myProxy.setSourceModel(listModel)
# finally show result in a QListView:
# 'ui' is a QWidget object that contain a listView widget.
ui.listView.setModel(myProxy)
ui.listView.setRootIndex(myProxy.mapFromSource(listModel.index("f:/root_folder")))
here is the custom QSortFilterProxyModel:
# test:
class myProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, source_row, source_parent):
return True
at this point, the script works as expected: with 3 folders in the list and no filters.
if i understand correctly, the 'source_parent' should be a QModelIndex to the 'listModel', and it pointing to directory 'f:/root_folder'. and the 'source_row' should be the "ordinal number" of an item in the 'f:/root_folder', one of three folders. right?
then i added my own filters:
# first try:
class myProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, source_row, source_parent):
source_model = self.sourceModel()
# 'source_model' should be the 'listModel', right?
source_index = source_model.index(source_row, 0, source_parent)
# 'source_index' is a QModelIndex, pointing to 'folder1' or '_folder2' or 'folder3.asset'.
# start filtering
filename = source_index.data(Qt.DisplayRole)
print(filename) # check
if filename[-6:] == ".asset": return True
else: return False
it should display 3 folder names on the console, display 1 folder(folder3.asset) in the list. but i got very strange result! here is the result of the console: ** it lists all my hard drives several times
HDD (F:)
root_folder
HDD (F:)
HDD (E:)
HDD (D:)
C:
HDD (F:)
HDD (E:)
HDD (D:)
C:
and the listView is empty.
is the 'source_parent' invalid? then i try this:
class myProxyModel(QSortFilterProxyModel):
def filterAcceptsRow(self, source_row, source_parent):
if not source_parent.isValid():
print("index invalid")
return False
else: return True
got this on console:
index invalid
index invalid
and 3 folders in listView:
now i'm totally confused.
what does filterAcceptsRow() exactly do?
You just have to filter the children of the inde associated with the rootPath() of QFileSystemModel:
from PySide2 import QtCore, QtGui, QtWidgets
class SuffixDirProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None):
super().__init__(parent)
self._suffix = ""
def filterAcceptsRow(self, source_row, source_parent):
source_model = self.sourceModel()
if (
self._suffix
and isinstance(source_model, QtWidgets.QFileSystemModel)
and source_parent == source_model.index(source_model.rootPath())
):
index = source_model.index(source_row, 0, source_parent)
name = index.data(QtWidgets.QFileSystemModel.FileNameRole)
file_info = source_model.fileInfo(index)
return name.split(".")[-1] == self._suffix and file_info.isDir()
return True
#property
def suffix(self):
return self._suffix
#suffix.setter
def suffix(self, s):
self._suffix = s
self.invalidateFilter()
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
model = QtWidgets.QFileSystemModel()
model.setFilter(QtCore.QDir.NoDotAndDotDot | QtCore.QDir.Dirs)
path = # "f:/root_folder"
model.setRootPath(path)
proxy = SuffixDirProxyModel()
proxy.suffix = "asset"
proxy.setSourceModel(model)
w = QtWidgets.QListView()
w.setViewMode(QtWidgets.QListView.IconMode)
w.setModel(proxy)
w.setRootIndex(proxy.mapFromSource(model.index(path)))
w.show()
sys.exit(app.exec_())
Nothing if you don't overload it.
And you only need to overload it if the filter provided in the class you use, usually QSortFilterProxyModel, does not meet your needs.
I would like to render each item in a QTreeView differently based on a number of attributes stored in a database and based on whether the item is a folder or a file. However, I don't understand how the QTreeView or QFileSystemModel communicate with the delegate. Whenever an item must be drawn, including during initialization, I'd expect to provide the delegate with all the parameters it requires and then use a series of if statements within the delegate to set how the particular item is drawn. I've only found the .setItemDelegate method and don't know when or how the delegate is actually called or how it loops through all the items in the model. Below is an example based on material online. There are two problems:
I placed code in comments that I was unable to get working. Once I understand how the delegate can receive information from the QTreeView (or calling class), I believe I can do the rest.
I was unable to get this subclass of the QTreeView to display the folder and file icons.
Code:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
class fileSystemDelegate(QItemDelegate):
def __init__(self, parent=None):
QItemDelegate.__init__(self, parent) #shouldn't this insure the icons are drawn?
def paint(self, painter, option, index):
painter.save()
# set background
painter.setPen(QPen(Qt.NoPen))
if option.state & QStyle.State_Selected: #DURING DRAW LOOP: idx = self.currentIndex(); if self.fileSystemModel.isDir(idx): PAINT RED
painter.setBrush(QBrush(Qt.red))
else:
painter.setBrush(QBrush(Qt.white)) #ELSE PAINT WHITE
painter.drawRect(option.rect)
# draw item
painter.setPen(QPen(Qt.black))
text = index.data(Qt.DisplayRole)
painter.drawText(option.rect, Qt.AlignLeft, text) #there is no painter.drawIcon?
painter.restore()
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
delegate = fileSystemDelegate()
self.setItemDelegate(delegate) # how to provide delegate with additional info about the item to be drawn ?
self.fileSystemModel = QFileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT 1:
I've added an example "database" in the form of a dictionary and changed the approach to rely on the data method rather than the delegate. I would expect this code to perform the dictionary lookup whenever information is displayed in the tree and therefore print to the terminal when the user enters C:\Program Files\Internet Explorer\ on a Microsoft Windows computer. However, it just displays the directory without printing anything to the terminal. I'd like to know:
How do I get if statements in the data method to trigger for every item in the display as they are being drawn?
How can I display an icon after the default icon is displayed, on the same row?
Code:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = QFileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
path = self.fileSystemModel.filePath(index)
if self.fileSystemModel.isDir(index):
if database.get(path) != None:
if database[path][0] > 0:
print("Acting on custom data 0.") # add another icon after the regular folder icon
if database[path][1] > 0:
print("Acting on custom data 1.") # add another (different) icon after the regular folder or previous icon
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT 2:
Subclassing the model definitely did make a difference. Now the script appears to be calling my new data method on every item. Unfortunately, the data method doesn't work yet so the result is a treeview without icons or text. Sometimes I receive the error: "QFileSystemWatcher: failed to add paths: C:/PerfLogs". Based on examples online, I've commented where I think my errors may be, but I cannot yet get this to work. What am I doing wrong?
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {'C:\Program Files\Internet Explorer\ExtExport.exe':(1,3), 'C:\Program Files\Internet Explorer\iexplore.exe':(0,0)}
class newFileModel(QFileSystemModel):
def __init__(self, parent=None):
QFileSystemModel.__init__(self, parent)
#self.elements = [[Do I need this? What should go here?]]
def data(self, index, role=Qt.DisplayRole):
if index.isValid():
path = self.filePath(index)
if self.isDir(index):
if database.get(path) != None:
if database[path][0] > 0:
print("Acting on custom data 0.") # I can add code here for different color text, etc.
if database[path][1] > 0:
print("Acting on custom data 1.") # I'll add code later
#return self.data(index, role) # Do I need this about here?
class fileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = newFileModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = fileSystemBrowser()
window.show()
sys.exit(app.exec_())
Here is a basic demo that shows how to add an extra column with icons and other formatting. Note that an attempt is made to normalise the file-paths so that comparisons and dictionary look-ups should be more reliable:
import sys
from PySide.QtCore import *
from PySide.QtGui import *
database = {
QFileInfo('C:\Program Files\Internet Explorer\ExtExport.exe').absoluteFilePath(): (1, 3),
QFileInfo('C:\Program Files\Internet Explorer\iexplore.exe').absoluteFilePath(): (0, 0),
}
class FileSystemModel(QFileSystemModel):
def __init__(self, parent=None):
super().__init__(parent)
style = qApp.style()
self.icons = [
style.standardIcon(QStyle.SP_MessageBoxInformation),
style.standardIcon(QStyle.SP_MessageBoxWarning),
]
def columnCount(self, parent=QModelIndex()):
return super().columnCount(parent) + 1
def data(self, index, role=Qt.DisplayRole):
extra = False
if index.isValid():
extra = index.column() == self.columnCount(index.parent()) - 1
info = self.fileInfo(index)
path = info.absoluteFilePath()
if path in database:
major, minor = database[path]
print('found:', (major, minor), path)
if extra:
if role == Qt.DecorationRole:
if major > 0:
return self.icons[0]
else:
return self.icons[1]
elif role == Qt.DisplayRole:
return '%s/%s' % (major, minor)
elif role == Qt.ForegroundRole:
if minor > 2:
return QColor('red')
if not extra:
return super().data(index, role)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if (orientation == Qt.Horizontal and
role == Qt.DisplayRole and
section == self.columnCount() - 1):
return 'Extra'
return super().headerData(section, orientation, role)
class FileSystemBrowser(QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
self.fileSystemModel = FileSystemModel()
self.fileSystemModel.setRootPath(QDir.currentPath())
self.setModel(self.fileSystemModel)
self.header().moveSection(self.fileSystemModel.columnCount() - 1, 1)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = FileSystemBrowser()
window.show()
sys.exit(app.exec_())
EDIT:
The roles used in the data method are all documented under the ItemDataRole enum, and are introduced as follows:
Each item in the model has a set of data elements associated with it,
each with its own role. The roles are used by the view to indicate to
the model which type of data it needs. Custom models should return
data in these types.
For the extra column that has been added, it is necessary to supply everything, because it is a virtual column that is not part of the underlying model. But for the other columns, we can just call the base-class implementation to get the default values (of course, if desired, we could also return custom values for these columns to modify the existing behaviour).
To start, I've already looked at ekhumoro's code on a nearly similar subject Here. However, when I attempt to implement this code, I am getting a different result. Instead of copying and pasting all the I selected, it only copies the first cell of the selected row or rows respectively. I need users to be able to select multiple rows or columns and paste those values in either excel or notepad. Any ideas?
GUI:
Code:
from PyQt4 import QtCore, QtGui, QtSql
import sys
import sqlite3
import time
import csv
import Search # This file holds our MainWindow and all design related things
# it also keeps events etc that we defined in Qt Designer
import os
try:
from PyQt4.QtCore import QString
except ImportError:
QString = str
class TableEditor(QtGui.QMainWindow, Search.Search_MainWindow):
def __init__(self, tableName, parent=None):
# Explaining super is out of the scope of this article
# So please google it if you're not familar with it
# Simple reason why we use it here is that it allows us to
# access variables, methods etc in the design.py file
super(self.__class__, self).__init__()
self.setupUi(self) # This is defined in design.py file automatically
# It sets up layout and widgets that are defined
self.model = QtSql.QSqlTableModel(self)
self.model.setTable('CAUTI')
self.model.setEditStrategy(QtSql.QSqlTableModel.OnManualSubmit)
self.model.select()
self.model.setHeaderData(0, QtCore.Qt.Horizontal, "MRN")
self.model.setHeaderData(1, QtCore.Qt.Horizontal, "Last Name")
self.model.setHeaderData(2, QtCore.Qt.Horizontal, "First Name")
self.model.setHeaderData(3, QtCore.Qt.Horizontal, "Date of Event")
self.model.setHeaderData(4, QtCore.Qt.Horizontal, "Facility")
self.model.setHeaderData(5, QtCore.Qt.Horizontal, "Unit")
self.model.setHeaderData(6, QtCore.Qt.Horizontal, "User")
#self.tableView.verticalHeader().setVisible(False)
self.tableView.setModel(self.model)
self.setWindowTitle("HAI Table")
self.tableView.setColumnWidth(0,100)
self.tableView.setColumnWidth(1,100)
self.tableView.setColumnWidth(2,100)
self.tableView.setColumnWidth(3,100)
self.tableView.setColumnWidth(4,100)
self.tableView.setColumnWidth(5,100)
self.tableView.setColumnWidth(6,83)
self.tableView.setSortingEnabled(True)
self.tableView.setDropIndicatorShown(True)
self.tableView.setAcceptDrops(True)
self.tableView.setDragEnabled(True)
self.tableView.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.tableView.horizontalHeader().setMovable(True)
self.tableView.horizontalHeader().setDragEnabled(True)
self.clip = QtGui.QApplication.clipboard()
## Note: ** When I unblock this line of code, I get an error.
#self.tableView.installEventFilters(self)
self.tableView.horizontalHeader().setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.tableView.verticalHeader().setMovable(True)
self.tableView.verticalHeader().setDragEnabled(True)
self.tableView.verticalHeader().setDragDropMode(QtGui.QAbstractItemView.InternalMove)
self.tableView.setSelectionBehavior(QtGui.QTableView.SelectRows)
self.submitButton.clicked.connect(self.submit)
self.revertButton.clicked.connect(self.model.revertAll)
self.quitButton.clicked.connect(self.close)
self.tableView.horizontalHeader().setSortIndicatorShown(True)
self.tableView.horizontalHeader().setClickable(True)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.KeyPress and
event.matches(QtGui.QKeySequence.Copy)):
self.copySelection()
return True
return super(Window, self).eventFilter(source, event)
def copySelection(self):
selection = self.tableView.selectedIndexes()
if selection:
rows = sorted(index.row() for index in selection)
columns = sorted(index.column() for index in selection)
rowcount = rows[-1] - rows[0] + 1
colcount = columns[-1] - columns[0] + 1
table = [[''] * colcount for _ in range(rowcount)]
for index in selection:
row = index.row() - rows[0]
column = index.column() - columns[0]
table[row][column] = index.data()
stream = io.StringIO()
csv.writer(stream).writerows(table)
QtGui.qApp.clipboard().setText(stream.getvalue())
def flags(self, index):
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsDropEnabled
def main():
app = QtGui.QApplication(sys.argv)
#app.setStyle( "Plastique" )
db = QtSql.QSqlDatabase.addDatabase('QSQLITE')
db.setDatabaseName('HAI.db')
editor = TableEditor('CAUTI')
editor.show()
app.exec_()
if __name__ == '__main__':
main()
Your implementation is broken in several ways, so the code you copied never gets executed. As a result, the table's built-in copy function is used, which does not handle multiple selected items.
My original code for copying multiple table items is here. I think you should be able to get your example working by changing the following lines:
from PyQt4 import QtCore, QtGui, QtSql
# fix imports
import sys, io
...
class TableEditor(QtGui.QMainWindow, Search.Search_MainWindow):
def __init__(self, tableName, parent=None):
# do not ever use self.__class__ here
super(TableEditor, self).__init__()
...
# install event-filter properly
self.tableView.installEventFilter(self)
def eventFilter(self, source, event):
...
# call super properly
return super(TableEditor, self).eventFilter(source, event)
Obviously, I cannot run your actual example code, so there may be other errors that I haven't spotted.
This is much faster :
def copySelection(self):
clipboardString = StringIO()
selectedIndexes = self.ui.tableView.selectedIndexes()
if selectedIndexes:
countList = len(selectedIndexes)
for r in range(countList):
current = selectedIndexes[r]
displayText = current.data(QtCore.Qt.DisplayRole)
if r+1 < countList:
next_ = selectedIndexes[r+1]
if next_.row() != current.row():
displayText += ("\n")
else:
displayText += ("\t")
clipboardString.write(displayText)
pyperclip.copy(clipboardString.getvalue())
Check the edit for final code!
So...I'll admit that I'm drawing an absolute blank here due to lack of knowledget and will just present my code and pray a little bit.
Using this fantastic xml to QTreeWidget generator that ekhumoro coded I've added in checkboxes (tristate for parent nodes) and now I'm attempting to iterate over these checkboxes and return a dictionary or a list of lists or...something with a parent:children (server:services in this tree) relationship so that I can build paths to folders out of the tree's checked results. This would occur on a 'Start' button press as the first function in a series.
The end goal is to have a tool that pulls logs from multiple servers and services-per-server on a platform based on a user provided start and stop time, and deliver these logs to an admin box with identical folder structure.
I've successfully navigated through a test dictionary, searched for and located files based on os.path.getctime and getmtime, zipped them, then copied them to a seperate drive with identical folder structure that's created as part of a function...
However, I've found literally almost no documentation on the TreeWidgetItemIterator (only http://pyqt.sourceforge.net/Docs/PyQt4/qtreewidgetitemiterator.html#details) which wasn't very helpful to me. So the integral (and final) piece to this puzzle has me lost!
The XMLHandler:
from PyQt4 import QtCore, QtGui, QtXml
from PyQt4.QtXml import *
class XmlHandler(QXmlDefaultHandler):
def __init__(self, root):
QtXml.QXmlDefaultHandler.__init__(self)
self._root = root
self._item = None
self._text = ''
self._error = ''
def startElement(self, namespace, name, qname, attributes):
if qname == 'Machine' or qname == 'Feature':
if self._item is not None:
self._item = QtGui.QTreeWidgetItem(self._item)
else:
self._item = QtGui.QTreeWidgetItem(self._root)
self._item.setData(0, QtCore.Qt.UserRole, qname)
self._item.setText(0, 'Unknown Machine')
if qname == 'Machine':
self._item.setExpanded(False)
self._item.setCheckState(0, QtCore.Qt.Unchecked)
self._item.setFlags(QtCore.Qt.ItemIsSelectable|QtCore.Qt.ItemIsDragEnabled|QtCore.Qt.ItemIsUserCheckable|QtCore.Qt.ItemIsEnabled|QtCore.Qt.ItemIsTristate)
elif qname == 'FeatureName':
self._item.setText(1, self._text)
self._text = ''
return True
def endElement(self, namespace, name, qname):
if qname == 'FeatureName' or qname == 'MachineName':
if self._item is not None:
self._item.setText(0, self._text)
self._item.setCheckState(0, QtCore.Qt.Unchecked)
elif qname == 'Feature' or qname == 'Machine':
self._item = self._item.parent()
return True
def characters(self, text):
self._text += text
return True
def fatalError(self, exception):
print('Parse Error: line %d, column %d:\n %s' % (
exception.lineNumber(),
exception.columnNumber(),
exception.message(),
))
return False
The Class that uses XmlHandler to make the widget:
class MakeWidget(QtGui.QTreeWidget):
def __init__(self):
QtGui.QTreeWidget.__init__(self)
self.header().setResizeMode(QtGui.QHeaderView.Stretch)
self.setHeaderLabels(['Servers and Services'])
source = QtXml.QXmlInputSource()
source.setData(xml)
handler = XmlHandler(self)
reader = QtXml.QXmlSimpleReader()
reader.setContentHandler(handler)
reader.setErrorHandler(handler)
reader.parse(source)
Creation of the widget in my GUI:
self.treeServiceSelection = MakeWidget(xml, parent=self.ui.groupBoxServiceSelection)
The widget is two level with parents and children, and that's all:
http://i.imgur.com/npPhG41.jpg
And now we've come to where I'm stuck. I can tie the signal to the button press, I can do everything else but get the dang checked items from the QTreeWidget. It seems that I would need to construct an iterator and have the Checked flag in it's init but anything that I've tried has come up with bupkiss. I'm more than happy to work to a solution rather than have one provided to me, but not having a starting point has been depressing.
Edit: So none of what I actually posted was of any use, but ekhumoro understood the crux of my question and provided his solution (the accepted answer). I added one an elif to handle anything where a parent was checked (as it didn't seem to read that the children were checked as an effect of this):
def exportTree(self):
mapping = defaultdict(list)
root = self.tree.invisibleRootItem()
for index in range(root.childCount()):
parent = root.child(index)
if parent.checkState(0) == QtCore.Qt.PartiallyChecked:
features = mapping[parent.text(0)]
for row in range(parent.childCount()):
child = parent.child(row)
if child.checkState(0) == QtCore.Qt.Checked:
features.append(child.text(0))
elif parent.checkState(0) == QtCore.Qt.Checked:
features = mapping[parent.text(0)]
for row in range(parent.childCount()):
features.append((parent.child(row)).text(0))
return mapping
and then was able to use this example to select specific children based on another comboBox elsewhere in the gui:
def checkbox2mods(self):
d = {'DMD2K8COR2VM': ['CC2DCP', 'Centercord'],
'DMD2K8COR1VM': ['CC2DCP', 'Centercord']}
root = self.tree.invisibleRootItem()
if self.checkBox2.checkState() == QtCore.Qt.Checked:
for index in range(root.childCount()):
parent = root.child(index)
for row in range(parent.childCount()):
child = parent.child(row)
x = d.values()
y = d.keys()
for _ in x:
if child.text(0) in _:
if parent.text(0) in y:
child.setCheckState(0, QtCore.Qt.Checked)
parent.setExpanded(True)
I'm sure the code is nasty, but by only selecting the children it partial checks the parents, which allows for the export function to work correctly.
Im assuming that the core of your question is that you want to iterate over a tree widget and build a dictionary containing the checked items.
The example below assumes the tree is two levels deep, and doesn't attempt any management of check-state:
# not needed for python 3
import sip
sip.setapi('QString', 2)
from collections import defaultdict
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.tree = QtGui.QTreeWidget(self)
self.tree.header().hide()
for index in range(5):
parent = QtGui.QTreeWidgetItem(self.tree, ['NUS2K%s' % index])
if index % 3:
parent.setCheckState(0, QtCore.Qt.PartiallyChecked)
else:
parent.setCheckState(0, QtCore.Qt.Unchecked)
features = 'Loader Reports Logging'.split()
for count, item in enumerate(features):
child = QtGui.QTreeWidgetItem(parent, [item])
if index % 3 and count % 3:
child.setCheckState(0, QtCore.Qt.Checked)
else:
child.setCheckState(0, QtCore.Qt.Unchecked)
parent.setExpanded(True)
self.button = QtGui.QPushButton('Export', self)
self.button.clicked.connect(self.handleExport)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.tree)
layout.addWidget(self.button)
def handleExport(self):
mapping = self.exportTree()
for machine, features in mapping.items():
print('%s:' % machine)
for feature in features:
print(' %s' % feature)
def exportTree(self):
mapping = defaultdict(list)
root = self.tree.invisibleRootItem()
for index in range(root.childCount()):
parent = root.child(index)
if parent.checkState(0) == QtCore.Qt.PartiallyChecked:
features = mapping[parent.text(0)]
for row in range(parent.childCount()):
child = parent.child(row)
if child.checkState(0) == QtCore.Qt.Checked:
features.append(child.text(0))
return mapping
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.setGeometry(800, 300, 300, 300)
window.show()
sys.exit(app.exec_())
I use the below code no iterate through a tree
def updateTreeItems(self):
for x in range(0,self.ConfigTree.topLevelItemCount()):
topLvlItem = self.ConfigTree.topLevelItem(x)
for y in range(numColums):
self.updateHighlight(topLvlItem,y)
if self.ConfigTree.isItemExpanded(topLvlItem):
for i in range(0,topLvlItem.childCount()):
childItem = topLvlItem.child(i)
for y in range(numColums):
self.updateHighlight(childItem,y)
I use the self.updateHighlight(childItem,y), to highlight a cell in the tree using, but you could add some data to a list or something - if it is checked.
I did have a little trouble reading what your question was, but please let me know if I'm way off? - I think I can help, as I have used some trees by now :-)