I’m trying to use either TreeView or the TreeWidget to create a hierarchy that displays just a icon/string to represent a path.
I’d like a signal/slot so that double-clicking on an item opens a new window to edit the contents of that path. Currently I’m able to lookup by index the display name, but I can’t see any way to store/link hidden data (such as a key to reference a unique folder_id or node_id)
Is the common paradigm to add the key to Model and then remove those columns from the tree display?
Example Data / reason for needing to access hidden properties.
Class: Repository
Properties: ID, Name
Class: Endpoint
Properties: ID, Name, Repository_ID, Address, Method, etc...
Class: GeneratedDiagramTree
Properties: Type, Mixed_ID, Name
I want to only see the Name in the view, but I want to be able to refer to the Type/ID in order to determine what happens when double-clicking.
When you want to save custom information for each node then you must use the roles (preferably >= Qt::UserRole since the roles can be used internally by Qt). For example, in the following code 2 roles are used to store 2 types of information.
If QTreeWidget is used then you must use the setData() method and pass it the role and value to store the information, to obtain the information you must use the data() method by passing the role.
from enum import Enum
import uuid
from PyQt5 import QtCore, QtWidgets
TypeRole = QtCore.Qt.UserRole + 1000
IdRole = QtCore.Qt.UserRole + 1001
class TypeItem(Enum):
ROOT = 0
CHILD = 1
class Dialog(QtWidgets.QDialog):
def __init__(self, name, type_, id_, parent=None):
super().__init__(parent)
self.name_le = QtWidgets.QLineEdit(name)
type_label = QtWidgets.QLabel(str(type_))
self.id_le = QtWidgets.QLineEdit(id_)
button_box = QtWidgets.QDialogButtonBox()
button_box.setOrientation(QtCore.Qt.Horizontal)
button_box.setStandardButtons(
QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok
)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.name_le)
lay.addWidget(type_label)
lay.addWidget(self.id_le)
lay.addWidget(button_box)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
#property
def name(self):
return self.name_le.text()
#property
def id_(self):
return self.id_le.text()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.view = QtWidgets.QTreeWidget()
self.view.itemDoubleClicked.connect(self.onItemDoubleClicked)
self.setCentralWidget(self.view)
for i in range(3):
root_it = QtWidgets.QTreeWidgetItem(["tlv-{}".format(i)])
root_it.setData(0, TypeRole, TypeItem.ROOT)
root_it.setData(0, IdRole, uuid.uuid4().hex)
self.view.addTopLevelItem(root_it)
for j in range(3):
child_it = QtWidgets.QTreeWidgetItem(["it-{}{}".format(i, j)])
child_it.setData(0, TypeRole, TypeItem.CHILD)
child_it.setData(0, IdRole, uuid.uuid4().hex)
root_it.addChild(child_it)
#QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem, int)
def onItemDoubleClicked(self, item, column):
name = item.text(column)
type_ = item.data(column, TypeRole)
id_ = item.data(column, IdRole)
d = Dialog(name, type_, id_)
if d.exec_() == QtWidgets.QDialog.Accepted:
item.setText(column, d.name)
item.setData(column, IdRole, d.id_)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
If you use QTreeView then the handling of that role must be implemented in the model, for the following example, QStandardItemModel is used where each QStandardItem has the setData() method and data() to store and retrieve the information.
from enum import Enum
import uuid
from PyQt5 import QtCore, QtGui, QtWidgets
TypeRole = QtCore.Qt.UserRole + 1000
IdRole = QtCore.Qt.UserRole + 1001
class TypeItem(Enum):
ROOT = 0
CHILD = 1
class Dialog(QtWidgets.QDialog):
def __init__(self, name, type_, id_, parent=None):
super().__init__(parent)
self.name_le = QtWidgets.QLineEdit(name)
type_label = QtWidgets.QLabel(str(type_))
self.id_le = QtWidgets.QLineEdit(id_)
button_box = QtWidgets.QDialogButtonBox()
button_box.setOrientation(QtCore.Qt.Horizontal)
button_box.setStandardButtons(
QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok
)
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(self.name_le)
lay.addWidget(type_label)
lay.addWidget(self.id_le)
lay.addWidget(button_box)
button_box.accepted.connect(self.accept)
button_box.rejected.connect(self.reject)
#property
def name(self):
return self.name_le.text()
#property
def id_(self):
return self.id_le.text()
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.model = QtGui.QStandardItemModel(self)
self.view = QtWidgets.QTreeView()
self.view.setModel(self.model)
self.view.doubleClicked.connect(self.onDoubleClicked)
self.setCentralWidget(self.view)
for i in range(3):
root_it = QtGui.QStandardItem("tlv-{}".format(i))
root_it.setData(TypeItem.ROOT, TypeRole)
root_it.setData(uuid.uuid4().hex, IdRole)
self.model.appendRow(root_it)
for j in range(3):
child_it = QtGui.QStandardItem("it-{}{}".format(i, j))
child_it.setData(TypeItem.CHILD, TypeRole)
child_it.setData(uuid.uuid4().hex, IdRole)
root_it.appendRow(child_it)
#QtCore.pyqtSlot(QtCore.QModelIndex)
def onDoubleClicked(self, index):
item = self.model.itemFromIndex(index)
name = item.text()
type_ = item.data(TypeRole)
id_ = item.data(IdRole)
d = Dialog(name, type_, id_)
if d.exec_() == QtWidgets.QDialog.Accepted:
item.setText(d.name)
item.setData(d.id_, IdRole)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Related
In the default behavior of editing a cell in a QtableView, when the user clicks away either to another widget or closes the form, the edits are lost. After a lot of googling, I have found a way to save the edits if the user selects another widget in the form, but if the form is closed, the edits are still lost. The blog post is here.
I attempted to call the closeEditor method from the forms closeEvent, but it requires two parameters: the editor and hint. I can provide QAbstractItemDelegate.NoHint but the editor is expecting the QlineEdit object where the editing is taking place. I am lost on how to provide this for the cell currently being edited.
Here is a gif of the current behaviour:
My question is how do I provide the QlineEdit of the cell being edited?
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtSql import *
from PyQt5.QtWidgets import *
from phones import *
class Main(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.resize(490, 998)
self.layoutWidget = QWidget(self)
self.layoutWidget.setObjectName("layoutWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.new_phone = QtWidgets.QPushButton(self.layoutWidget)
self.new_phone.setObjectName("new_phone")
self.new_phone.setText("New Phone")
self.horizontalLayout_7.addWidget(self.new_phone)
self.delete_phone = QtWidgets.QPushButton(self.layoutWidget)
self.delete_phone.setObjectName("delete_phone")
self.delete_phone.setText("Delete phone")
self.horizontalLayout_7.addWidget(self.delete_phone)
self.verticalLayout.addLayout(self.horizontalLayout_7)
self.phone_view = Syn_tableview()
self.verticalLayout.addWidget(self.phone_view)
self.cont_id = '9'
self.setCentralWidget(self.layoutWidget)
self.new_phone.clicked.connect(self.add_phone)
self.populate_phones()
def populate_phones(self):
self.phone_model = QSqlTableModel(self)
self.phone_model.setTable("contact_phones")
self.phone_model.setFilter("contact_id='{0}'".format(self.cont_id))
self.phone_model.select()
self.phone_view.setModel(self.phone_model)
self.phone_view.resizeColumnsToContents()
def add_phone(self):
self.phone_model.submitAll()
self.phone_model.setEditStrategy(QSqlTableModel.OnManualSubmit)
row = self.phone_model.rowCount()
record = self.phone_model.record()
record.setGenerated('id', False) #primary key
record.setValue('contact_id', self.cont_id) #foreign key
self.phone_model.insertRecord(row, record)
phone_index_edit = self.phone_model.index(row, self.phone_model.fieldIndex('phone_number'))
self.phone_view.edit(phone_index_edit)
def closeEvent(self, event):
submit = self.phone_model.submitAll()
#This is the problem
self.phone_view.closeEditor("QLineEdit", QAbstractItemDelegate.NoHint)
class Syn_tableview(QTableView):
def __init__(self, *args, **kwargs):
QTableView.__init__(self, *args, **kwargs)
def closeEditor(self, editor, hint):
if hint == QAbstractItemDelegate.NoHint:
QTableView.closeEditor(self, editor,
QAbstractItemDelegate.SubmitModelCache)
if __name__=="__main__":
app=QApplication(sys.argv)
db = QSqlDatabase.addDatabase("QPSQL");
db.setHostName(server)
db.setDatabaseName(database)
db.setUserName(user)
db.setPassword(pword)
myapp = Main()
myapp.show()
sys.exit(app.exec_())
The delegate editors are children of the QTableView so you can use findChildren to get them, to make sure they are not other children you can set an objectName that allows you to filter them:
import sys
from PyQt5 import QtCore, QtSql, QtWidgets
def create_connection():
db = QtSql.QSqlDatabase.addDatabase("QPSQL")
# FIXME
db.setHostName("server")
db.setDatabaseName("database")
db.setUserName("user")
db.setPassword("pword")
if not db.open():
print(db.lastError().text())
return False
return True
class Syn_Delegate(QtWidgets.QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = super(Syn_Delegate, self).createEditor(parent, option, index)
if isinstance(editor, QtWidgets.QWidget):
editor.setObjectName("syn_editor")
return editor
class Syn_Tableview(QtWidgets.QTableView):
def closeEditor(self, editor, hint):
if hint == QtWidgets.QAbstractItemDelegate.NoHint:
hint = QtWidgets.QAbstractItemDelegate.SubmitModelCache
super(Syn_Tableview, self).closeEditor(editor, hint)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.new_phone = QtWidgets.QPushButton(self.tr("New Phone"))
self.delete_phone = QtWidgets.QPushButton(self.tr("Delete phone"))
self.phone_view = Syn_Tableview()
self.phone_model = QtSql.QSqlTableModel()
self.phone_model.setEditStrategy(QtSql.QSqlTableModel.OnManualSubmit)
self.phone_view.setModel(self.phone_model)
self.phone_view.resizeColumnsToContents()
delegate = Syn_Delegate(self)
self.phone_view.setItemDelegate(delegate)
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
lay = QtWidgets.QGridLayout(central_widget)
lay.addWidget(self.new_phone, 0, 0)
lay.addWidget(self.delete_phone, 0, 1)
lay.addWidget(self.phone_view, 1, 0, 1, 2)
self._contact_id = "9"
self.populate_phones()
self.new_phone.clicked.connect(self.add_phone)
#property
def contact_id(self):
return self._contact_id
def populate_phones(self):
self.phone_model.setTable("contact_phones")
self.phone_model.setFilter("contact_id='{0}'".format(self.contact_id))
self.phone_model.select()
#QtCore.pyqtSlot()
def add_phone(self):
self.phone_model.submitAll()
row = self.phone_model.rowCount()
record = self.phone_model.record()
record.setGenerated("id", False) # primary key
record.setValue("contact_id", self.contact_id) # foreign key
self.phone_model.insertRecord(row, record)
phone_index_edit = self.phone_model.index(
row, self.phone_model.fieldIndex("phone_number")
)
if phone_index_edit.isValid():
self.phone_view.edit(phone_index_edit)
def closeEvent(self, event):
for editor in self.phone_view.findChildren(QtWidgets.QWidget, "syn_editor"):
self.phone_view.commitData(editor)
submit = self.phone_model.submitAll()
super().closeEvent(event)
def main():
app = QtWidgets.QApplication(sys.argv)
if not create_connection():
sys.exit(-1)
w = MainWindow()
w.show()
w.resize(640, 480)
ret = sys.exit(app.exec_())
sys.exit(ret)
if __name__ == "__main__":
main()
I have a QWizard that reads the column headers of a CSV file and makes the user choose what they want with each column. In the 2nd page of this wizard, I add the combo-boxes in a for loop that loops over the column names of the CSV. All combo-boxes are mandatory fields. However, another constraint is that at least one of them must be selected to choice 3 or above (c1 or c2 in my MWE), before the user can press "Next".
In addition to self.NextButton.setEnabled(False), I also tried using isComplete and completeChanged according to this example and this question, but being a beginner to PyQt and not so good at C++, I haven't been able to understand that much of the existing documentation. For now isComplete seems to be fine but the wizard isn't registering it.
Can anyone teach me how I would be able to achieve what I wanted (the text in bold)?
from PyQt5 import QtGui, QtWidgets, QtCore
import csv
class ChooseFile(QtWidgets.QWizardPage):
def __init__(self, parent=None):
super(ChooseFile, self).__init__(parent)
body = QtWidgets.QVBoxLayout()
self.filePathShow = QtWidgets.QLineEdit(self)
self.filePathShow.setReadOnly(True) # not editable
self.registerField("filePathShow*", self.filePathShow) # mandatory
body.addWidget(self.filePathShow)
browseButton = QtWidgets.QPushButton('Browse...', self)
browseButton.clicked.connect(self.browseDialog)
browseBox = QtWidgets.QHBoxLayout()
browseBox.addWidget(browseButton)
body.addLayout(browseBox)
self.setLayout(body)
def browseDialog(self):
filePath, _ = QtWidgets.QFileDialog.getOpenFileName(self, 'Open CSV', '/home', '(*.csv)')
if filePath: # only changes if valid file, stays the same if user presses cancel
self.setField("filePathShow", filePath)
class ChooseColumns(QtWidgets.QWizardPage):
def __init__(self, parent=None):
super(ChooseColumns, self).__init__(parent)
self.box = QtWidgets.QGroupBox()
body = QtWidgets.QVBoxLayout()
body.addWidget(self.box) # these are where the choices (comboboxes) go
self.setLayout(body)
def initializePage(self):
filePath2 = self.field("filePathShow")
with open(str(filePath2), 'r') as f:
reader = csv.reader(f)
self.columns = next(reader)
# make a combobox for each column
grid = QtWidgets.QGridLayout()
self.comboBoxes = [None] * len(self.columns)
for i, col in enumerate(self.columns):
grid.addWidget(QtWidgets.QLabel(col), i, 0) # printscolumn name
self.comboBoxes[i] = QtWidgets.QComboBox()
self.comboBoxes[i].addItem("") # default value since is mandatory field
self.comboBoxes[i].addItem("a")
self.comboBoxes[i].addItem("b")
self.comboBoxes[i].addItem("c1")
self.comboBoxes[i].addItem("c2")
grid.addWidget(self.comboBoxes[i], i, 1)
self.registerField("column" + str(i) + "*", self.comboBoxes[i]) # all mandatory
self.comboBoxes[i].currentIndexChanged.connect(self.isComplete)
#self.connect(self.comboBoxes[i], QtCore.SIGNAL(currentIndexChanged()),
# self, QtCore.SIGNAL(completeChanged()))
self.comboBoxes[i].currentIndexChanged.connect(self.completeChanged) # DOESN'T WORK
self.box.setLayout(grid)
def isComplete(self, other): # WORKS
self.selections = [None] * len(self.columns)
for i in range(len(self.selections)): # first fill the list
self.selections[i] = self.comboBoxes[i].currentIndex()
#print(self.selections)
for item in self.selections: # then evaluate the values
if i >= 3:
return True
return False
class Manager(QtWidgets.QWizard):
def __init__(self, parent=None):
super(Manager, self).__init__(parent)
self.resize(500, 300)
self.addPage(ChooseFile(self))
self.addPage(ChooseColumns(self))
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
w = Manager()
w.show()
sys.exit(app.exec_())
The isComplete signal must return a Boolean that indicates whether it can advance to the next page or terminate the process. This method should not be invoked directly but through the completeChanged signal.
This case is special since the amount of QComboBox is variable, so if the user goes back to the previous page and selects another .csv, the amount of QComboBox should be changed and registered would be a problem so in this case I will not do it but It is directly handled using isComplete.
Finally, as the objective (I suppose) is to obtain the selected values then I will use a property of the QWizard to store that information and be able to obtain it on the last page that I added for my test.
import csv
from PyQt5 import QtGui, QtWidgets, QtCore
class ChooseFile(QtWidgets.QWizardPage):
def __init__(self, parent=None):
super(ChooseFile, self).__init__(parent)
body = QtWidgets.QVBoxLayout(self)
self.filePathShow = QtWidgets.QLineEdit(self)
self.filePathShow.setReadOnly(True) # not editable
self.registerField("filePathShow*", self.filePathShow) # mandatory
body.addWidget(self.filePathShow)
browseButton = QtWidgets.QPushButton("Browse...", self)
browseButton.clicked.connect(self.browseDialog)
browseBox = QtWidgets.QHBoxLayout()
browseBox.addWidget(browseButton)
body.addLayout(browseBox)
def browseDialog(self):
filePath, _ = QtWidgets.QFileDialog.getOpenFileName(
self, "Open CSV", "/home", "(*.csv)"
)
if filePath:
self.setField("filePathShow", filePath)
class ChooseColumns(QtWidgets.QWizardPage):
def __init__(self, parent=None):
super(ChooseColumns, self).__init__(parent)
self.comboboxes = []
box = QtWidgets.QGroupBox()
lay = QtWidgets.QVBoxLayout(self)
lay.addWidget(box)
self.flay = QtWidgets.QFormLayout()
box.setLayout(self.flay)
def initializePage(self):
for combo in self.comboboxes:
self.flay.removeRow(combo)
self.comboboxes = []
self.wizard().setProperty("indexes_selected", [])
self.wizard().setProperty("options_selected", [])
filePath2 = self.field("filePathShow")
options = ("", "a", "b", "c1", "c2")
with open(filePath2, "r") as f:
reader = csv.reader(f)
header = next(reader)
for i, text in enumerate(header):
combo = QtWidgets.QComboBox()
combo.addItems(options)
combo.currentIndexChanged.connect(self.completeChanged)
self.flay.addRow(text, combo)
self.comboboxes.append(combo)
def isComplete(self):
indexes = [combo.currentIndex() for combo in self.comboboxes]
is_completed = all(index >= 1 for index in indexes) and any(
index >= 3 for index in indexes
)
self.wizard().setProperty("indexes_selected", indexes)
self.wizard().setProperty(
"options_selected", [combo.currentText() for combo in self.comboboxes]
)
return is_completed
class FinalPage(QtWidgets.QWizardPage):
def initializePage(self):
print(self.wizard().property("indexes_selected"))
print(self.wizard().property("options_selected"))
class Manager(QtWidgets.QWizard):
def __init__(self, parent=None):
super(Manager, self).__init__(parent)
self.resize(500, 300)
self.addPage(ChooseFile())
self.addPage(ChooseColumns())
self.addPage(FinalPage())
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Manager()
w.show()
sys.exit(app.exec_())
How does one use a QFileSystemModel to populate several QComboBox with subdirectories?
I have built a project management tool that allows me to create and manage my projects. I am currently using a combination of os.listdir and json to populate and validate my QComboboxes. But I am trying to learn a more modelview approach with QFileSystemModel.
So this is what I have:
class FileSystemModel(QW.QFileSystemModel):
def __init__(self, root, parent=None):
QW.QFileSystemModel.__init__(self, parent)
self.root = root
self.rootIndex = self.setRootPath(root)
class Window(QW.QWidget):
def __init__(self, parent=None):
super(Window, self).__init__()
self.init()
def init(self):
layout = QW.QVBoxLayout()
self.cbox = QW.QComboBox()
self.cbox2 = QW.QComboBox()
self.model = FileSystemModel("C:\\projects\\")
self.cbox.setModel(self.model)
self.cbox2.setModel(self.model)
self.cbox.setRootModelIndex(self.model.rootIndex)
self.cbox.currentIndexChanged.connect(self._indexChanged)
layout.addWidget(self.cbox)
layout.addWidget(self.cbox2)
self.setLayout(layout)
def _indexChanged(self):
row = self.sender().currentIndex()
index = self.sender().rootModelIndex().child(row, 0)
self.cbox2.setRootModelIndex(index)
def main():
app = QW.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
I was attempting to repopulate the cbox2 using the index from cbox, but with my code it doesn't seem to work - it just stays empty.
Okay here is modified version of what you had:
from sys import exit as sysExit
from PyQt5.QtCore import QDir, pyqtSlot
from PyQt5.QtWidgets import QApplication, QWidget, QFileSystemModel, QHBoxLayout, QComboBox
class SysDirModel(QFileSystemModel):
def __init__(self, DirPath):
QFileSystemModel.__init__(self)
self.setFilter(QDir.NoDotAndDotDot | QDir.AllDirs)
self.setReadOnly(True)
# Property
self.setRootPath(DirPath)
# Property
self.RootIndex = self.index(DirPath)
class SysFileModel(QFileSystemModel):
def __init__(self, DirPath):
QFileSystemModel.__init__(self)
self.setFilter(QDir.NoDotAndDotDot | QDir.Files)
self.setReadOnly(True)
# Property
self.setRootPath(DirPath)
# Property
self.RootIndex = self.index(DirPath)
def ResetPath(self, DirPath):
self.setRootPath(DirPath)
self.RootIndex = self.index(DirPath)
class MainWindow(QWidget):
def __init__(self):
QWidget.__init__(self)
self.setGeometry(150, 150, 450, 100)
# If you use forward slash this works in Windows as well and it is cleaner
self.SysDirs = SysDirModel('C:/projects/')
self.SysFils = SysFileModel('C:/projects/')
# Setup first ComboBox
self.cbxDirs = QComboBox()
self.cbxDirs.setMinimumWidth(200)
self.cbxDirs.setModel(self.SysDirs)
self.cbxDirs.setRootModelIndex(self.SysDirs.RootIndex)
# This sends a Signal to a predefined Slot
self.cbxDirs.currentIndexChanged.connect(self.IndexChanged)
self.cbxFiles = QComboBox()
self.cbxFiles.setMinimumWidth(200)
self.cbxFiles.setModel(self.SysFils)
self.cbxFiles.setRootModelIndex(self.SysFils.RootIndex)
HBox = QHBoxLayout()
HBox.addWidget(self.cbxDirs)
HBox.addStretch(1)
HBox.addWidget(self.cbxFiles)
self.setLayout(HBox)
# This is the receiver of a Signal (aka Slot) so it ought to be used as such
#pyqtSlot(int)
def IndexChanged(self, RowIdx):
# Get your Current DirPath based on the Selected Value
index = self.cbxDirs.rootModelIndex().child(RowIdx, 0)
DirPath = self.cbxDirs.model().filePath(index)
# Reset what ComboBox 2's Model and what it is looking at
self.cbxFiles.clear()
self.SysFils.ResetPath(DirPath)
self.cbxFiles.setModel(self.SysFils)
if __name__ == '__main__':
MainThred = QApplication([])
MainGui = MainWindow()
MainGui.show()
sysExit(MainThred.exec_())
I need auto completion in a table. So far, I could make it work that I get the same list for the entire table.
However, I need a dynamic list for each cell. How can I get update the list when I move to a new position in the cell?
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys
class mainWindow(QMainWindow):
def __init__(self, parent = None):
super(mainWindow, self).__init__(parent)
self.initUI()
def initUI(self):
self.center_window = centerWindow(parent=self)
self.setCentralWidget(self.center_window)
class centerWindow(QWidget):
def __init__(self, parent=None):
super(centerWindow, self).__init__(parent)
table = QTableWidget()
table.setItemDelegate(TableItemCompleter())
table.setRowCount(5)
table.setColumnCount(1)
vbox = QVBoxLayout(self)
vbox.addWidget(table)
self.setLayout(vbox)
class TableItemCompleter(QStyledItemDelegate):
def __init__(self, parent = None):
super(TableItemCompleter, self).__init__(parent)
def createEditor(self, parent, styleOption, index):
editor = QLineEdit(parent)
completion_ls = ['aaa', 'bbb', 'ccc']
autoComplete = QCompleter(completion_ls)
editor.setCompleter(autoComplete)
return editor
if __name__ == '__main__':
app = QApplication.instance()
if app is None:
app = QApplication(sys.argv)
else:
print('QApplication instance already exists: %s' % str(app))
ex = mainWindow()
ex.show()
sys.exit(app.exec_())
To make it more clear. Instead of having the completion_ls list in the TableItemCompleter, I'd like to add something like this:
table.setItem(row, column) #add my new auto completion list
QCompleter can be established a model that uses as a source for the autocomplete, we could pass the QModelIndex model that provides the method createEditor(self, parent, option, index) through index.model() but the problem is that you can only take a column and is not what is desired.
Then we must do a conversion of the tablemodel to a listmodel, and then we must filter the repeated elements so that the completer shows unique elements, one way to do them is through proxies, classes that inherit from QAbstractProxyModel, the scheme is as follows:
TableModel ----> ReadTable2ListProxyModel ----> DuplicateFilterProxyModel
En la siguiente sección muestros las clases:
class ReadTable2ListProxyModel(QIdentityProxyModel):
def columnCount(self, parent=QModelIndex()):
return 1
def rowCount(self, parent=QModelIndex()):
return self.sourceModel().rowCount() * self.sourceModel().columnCount()
def mapFromSource(self, sourceIndex):
if sourceIndex.isValid() and sourceIndex.column() == 0\
and sourceIndex.row() < self.rowCount():
r = sourceIndex.row()
c = sourceIndex.column()
row = sourceIndex.model().columnCount() * c + r
return self.index(row, 0)
return QModelIndex()
def mapToSource(self, proxyIndex):
r = proxyIndex.row() / self.sourceModel().columnCount()
c = proxyIndex.row() % self.sourceModel().columnCount()
return self.sourceModel().index(r, c)
def index(self, row, column, parent=QModelIndex()):
return self.createIndex(row, column)
class DuplicateFilterProxyModel(QSortFilterProxyModel):
def setSourceModel(self, model):
model.dataChanged.connect(lambda: self.invalidate())
QSortFilterProxyModel.setSourceModel(self, model)
def filterAcceptsRow(self, row, parent):
value = self.sourceModel().index(row, self.filterKeyColumn())\
.data(self.filterRole())
if value is None:
return False
if row == 0:
return True
for i in reversed(range(0, row)):
val = self.sourceModel().index(i, self.filterKeyColumn())\
.data(self.filterRole())
if val == value:
return False
return True
Then the conversion is established in the delegate:
class TableItemCompleter(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QLineEdit(parent)
completer = QCompleter(parent)
proxy1 = ReadTable2ListProxyModel(parent)
proxy2 = DuplicateFilterProxyModel(parent)
proxy1.setSourceModel(index.model())
proxy2.setSourceModel(proxy1)
completer.setModel(proxy2)
editor.setCompleter(completer)
return editor
In the following link you will find an example, and in the following image the operation is illustrated, in the first widget the table is observed, in the second the conversion to list and in the third the list eliminating duplicate elements.
If you want each item to have a list what can be done is to store the list in each item through a role that is not in use as Qt.UserRole through the setData() method, and in the delegate through the method data() of the QModelIndex:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import random
class TableItemCompleter(QStyledItemDelegate):
def createEditor(self, parent, option, index):
editor = QLineEdit(parent)
completion_ls = index.data(Qt.UserRole) # get list
completer = QCompleter(completion_ls, parent)
editor.setCompleter(completer)
return editor
class Widget(QWidget):
def __init__(self, *args, **kwargs):
QWidget.__init__(self, *args, **kwargs)
lay = QHBoxLayout(self)
tv = QTableWidget(3, 4, self)
lay.addWidget(tv)
l = ["AA", "AB", "AC", "AD", "BA", "BB", "BC"]
for i in range(tv.rowCount()):
for j in range(tv.columnCount()):
it = QTableWidgetItem(f"{i},{j}")
tv.setItem(i, j, it)
it.setData(Qt.UserRole, random.sample(l, 3)) # set list
tv.setItemDelegate(TableItemCompleter(tv))
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
w = Widget()
w.show()
sys.exit(app.exec_())
I have a QTreeview and based on the users selection I would like to get a unique array containing all the names of the parents of the selected items.
So if any children are selected it would return the parent and if a parent is selected it would still return the parent.
Want returned:
>> [Kevin, Michelle, Nikki, Tim]
from PySide import QtGui, QtCore
from PySide import QtSvg, QtXml
import sys
class Person:
def __init__(self, name="", children=None):
self.name = name
self.children = children if children else []
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.resize(300, 400)
self.init_ui()
def init_ui(self):
# Setup Tabs Widget
# self.treeview = QtGui.QTreeView()
self.treeview = QtGui.QTreeView()
self.treeview.setHeaderHidden(True)
self.treeview.setUniformRowHeights(True)
self.treeview.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.treeview.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)
self.model = QtGui.QStandardItemModel()
self.treeview.setModel(self.model)
self.action = QtGui.QAction('Print', self)
self.action.setShortcut('F5')
self.action.triggered.connect(self.get_checked)
fileMenu = QtGui.QMenu("&File", self)
fileMenu.addAction(self.action)
self.menuBar().addMenu(fileMenu)
# Setup central widget
self.setCentralWidget(self.treeview)
# populate data
self.populate_people()
self.treeview.expandAll()
def populate_people(self):
parents = [
Person("Kevin", [Person("Tom"), Person("Sarah"), Person("Chester")]),
Person("Michelle", [Person("James"), Person("Corey"),Person("Leslie")]),
Person("Doug", [Person("Fred"), Person("Harold"),Person("Stephen")]),
Person("Nikki", [Person("Brody"), Person("Tyson"),Person("Bella")]),
Person("Tim", [Person("Marie"), Person("Val"),Person("Ted")])
]
for p in parents:
self.create_nodes(p, self.model)
def create_nodes(self, node, parent):
tnode = QtGui.QStandardItem()
tnode.setCheckable(True)
tnode.setData(QtCore.Qt.Unchecked, role=QtCore.Qt.CheckStateRole)
tnode.setData(node.name , role=QtCore.Qt.DisplayRole)
tnode.setData(node, role=QtCore.Qt.UserRole) # store object on item
parent.appendRow(tnode)
for x in node.children:
self.create_nodes(x, tnode)
def get_checked(self):
print "collecting parents..."
def main():
app = QtGui.QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Here is a simple implementation:
def selectedParents(self):
parents = set()
for index in self.treeview.selectedIndexes():
while index.parent().isValid():
index = index.parent()
parents.add(index.sibling(index.row(), 0))
return [index.data() for index in sorted(parents)]
Note that selectedIndexes() returns the indexes in the order they were selected, and, if there are multiple columns, will include the index of every column in the selected row.
So the above method makes sure only the text from the first column is included, and also makes sure that the items are returned in the correct order.