Ok so I think the best way for me to show what I'm trying to accomplish is visually, so I created a reproducible example of what I'm trying to do and strangely enough... it worked perfectly fine. Here it is
import sys
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
big_ar = ["1","2","3"]
class MyMainWindow(QWidget):
def __init__(self):
super(MyMainWindow,self).__init__()
self.setGeometry(300,300,300,300)
self.initUI()
def initUI(self):
self.lay = QVBoxLayout()
self.window_opener = WindowOpenerButton("open window",self)
self.group_box = UpdatingGroupBox(self)
self.lay.addWidget(self.window_opener)
self.lay.addWidget(self.group_box)
self.setLayout(self.lay)
def cust_update(self):
self.group_box.addButton()
self.group_box.update()
class WindowOpenerButton(QPushButton):
def __init__(self,txt,par):
super(WindowOpenerButton,self).__init__(txt,par)
self.clicked.connect(self.openWin)
def openWin(self):
self.smallWindow = MySmallWindow(psp=self.parentWidget())
self.smallWindow.show()
class MySmallWindow(QWidget):
def __init__(self,psp=None,parent=None):
super(MySmallWindow,self).__init__(parent)
self.setGeometry(100,100,100,100)
self.pseudo_parent = psp
self.populate()
def populate(self):
self.layout = QVBoxLayout()
self.line_edit = QLineEdit(self)
self.done = DoneButton("done",self,self.line_edit)
self.layout.addWidget(self.line_edit)
self.layout.addWidget(self.done)
self.setLayout(self.layout)
def closeEvent(self,event):
if self.pseudo_parent != None:
self.pseudo_parent.cust_update()
class UpdatingGroupBox(QGroupBox):
def __init__(self,par):
super(UpdatingGroupBox,self).__init__(par)
self.layout = QVBoxLayout()
self.addPreexisting()
self.setLayout(self.layout)
def addPreexisting(self):
global big_ar
for i in range(len(big_ar)):
self.layout.addWidget(QPushButton(big_ar[i],self))
def addButton(self):
global big_ar
self.layout.addWidget(QPushButton(big_ar[ len(big_ar) - 1],self))
class DoneButton(QPushButton):
def __init__(self,txt,par,src):
super(DoneButton,self).__init__(txt,par)
self.txt_source = src
self.clicked.connect(self.addToArr)
def addToArr(self):
global big_ar
big_ar.append(self.txt_source.text())
self.parentWidget().close()
print(big_ar)
def main():
app = QApplication(sys.argv)
app.setStyle("Fusion")
x = MyMainWindow()
x.show()
app.exec()
if __name__ == "__main__":
main()
Now this example outlines what I'm trying to do quite accurately and simply where it gets the string from the line edit on the smaller window and makes it into a pushbutton in the larger window's qgroupbox which is then updated immediately afterwards. The only difference between this example and my code is that instead of using a global array to add onto the qgroupbox, I use an instance variable, take a look.
def cust_update(self):
mem_f = open(self.file,"r")
raw_file_ml = mem_f.read().split("{")[1]
file_ml = raw_file_ml.split(";")
self.list.append(Member(file_ml[len(file_ml) - 2]))
mem_f.close()
self.mb_gb.addButton()
self.mb_gb.update()
This is the cust_update method for my actual program (you can disregard the first couple lines) and mb_gb is a MemberList which is this:
class MemberList(comps.MyButtonList): #list of memButtons derived from members.txt
def __init__(self,ttl,labl_ls,par,**kwargs):
super(MemberList,self).__init__(ttl,labl_ls,par,**kwargs)
self.layout = QVBoxLayout()
self.addPrexisting()
self.setLayout(self.layout)
def addPrexisting(self):
for i in range(len(self.list)):
self.layout.addWidget(QPushButton(self.list[i].fullName()))
def addButton(self):
nb = QPushButton(self.list[len(self.list) - 1])
self.layout.addWidget(nb)
the self.list represents a list of members which, as you can see in the cust_update method, is updated. The MemberList then takes the last element of the list and makes it into a button which is then added to its layout.
It's very similar to the first example's ugb but for some reason when the cust_event is called, even though it adds the button to the layout, it isn't being drawn (discovered through print debugging) and I have already tried using repaint()
EDIT:
I now see the confusion with what a Member and MyButtonList is, the reason I didn't explain them in the first place is because I didn't find it necessary as those parts of the code are working (my bad!). Here's the member class :
class Member:
def __init__(self,info_str):
info_ar = info_str.split(",")
self.first_name = info_ar[0]
self.last_name = info_ar[1]
self.idnum = info_ar[2]
self.grade_when_joined = info_ar[3]
self.gender = info_ar[4]
self.position = info_ar[5]
self.date_joined = info_ar[6][0:len(info_ar[6])]
def fullName(self):
return self.first_name.capitalize() + " " + self.last_name.capitalize()
def __str__(self):
return (self.fullName() + ": " + self.id() + " " + self.gender + " " + self.getPosition() + " " + str(self.date_joined))
def id(self):
lln = self.last_name.lower()
return lln + str(self.idnum)
def dateJoined(self):
y = int(self.date_joined[0:4])
m = int(self.date_joined[5:7])
d = int(self.date_joined[8:10])
return dt.date(y,m,d)
def sex(self):
s = "Other"
if self.gender == 'm':
s = "Male"
elif self.gender == 'f':
s = "Female"
return s
def getPosition(self):
pos = "Member"
if self.position == "o":
pos = "Officer"
elif self.position == "v":
pos = "Vice President"
elif self.position == "p":
pos = "President"
return pos
def idInt(self):
return int(self.idnum)
Prior to the closeEvent, a line is added to a txt file based on user input which follows the format
First_Name,Last_Name,ID#,Grade,Gender,Position,Date_When_Joined
when the close event happens, the cust_update method reads the file, splits it into individual members by ";" and takes the last one (the member that was just added) and makes it into a qpushbutton with its first name.
The MyButtonList class is nearly identical to the MemberList class, the only difference is in what goes on the buttons so it looks like this:
class MyButtonList(MyGroupBox):
def __init__(self,ttl,lab_ls,par,**kwargs):
super(MyButtonList,self).__init__(title=ttl,pare=par,**kwargs)
self.setUpdatesEnabled(True)
self.label_ls = lab_ls
self.list = self.parentWidget().list
self.layout = QVBoxLayout()
self.addPrexisting()
self.setLayout(self.layout)
def addPrexisting(self):
for i in range(len(self.list)):
self.layout.addWidget(QPushButton(str(i),self))
def addButton(self):
self.layout.addWidget(QPushButton("added",self))
Please let me know if there's anything I missed or I didn't communicate properly, sorry I didn't put everything here in the first place!
Related
Custom data model that I will supply below based on QAbstractListModel will be in two views: a QWidget based control interface and QML based 'dashboard'. QWidget based control side must display model items (model rows) in a custom delegate with every data point with stated custom roles in model described below and also at least provide a button for calling an editor widget for changing data points provided by roles (i.e. date or starting hour for specific items).
class ScheduleModel(QAbstractListModel):
SfiRole = Qt.UserRole + 1
NameRole = Qt.UserRole + 2
ClsRole = Qt.UserRole + 3
FlagRole = Qt.UserRole + 4
OwnrRole = Qt.UserRole + 5
RecordRole = Qt.UserRole + 6
DeptRole = Qt.UserRole + 7
DateStrRole = Qt.UserRole + 8
HourRole = Qt.UserRole + 9
EstTimeRole = Qt.UserRole + 10
StatusRole = Qt.UserRole + 11
def __init__(self, parent=None):
super().__init__(parent)
self._data = []
def rowCount(self, parent=QModelIndex()):
return len(self._data)
#Slot()
def updateSchedule(self, schedule_items: list):
self.beginResetModel()
self._data = schedule_items
self.endResetModel()
def data(self, index=QModelIndex(), role: int = Qt.DisplayRole):
if 0 <= index.row() < self.rowCount() and index.isValid():
item = self._data[index.row()]
if role == self.SfiRole:
return item.sfi
elif role == self.NameRole:
return item.item_name
elif role == self.ClsRole:
return item.class_attendance
elif role == self.FlagRole:
return item.flag_attendance
elif role == self.OwnrRole:
return item.owner_attendance
elif role == self.RecordRole:
return item.record_status
elif role == self.DeptRole:
return item.responsible_dept
elif role == self.DateStrRole:
return item.date
elif role == self.HourRole:
return item.start_hour
elif role == self.EstTimeRole:
return item.est
elif role == self.StatusRole:
return "Passive"
else:
return None
def roleNames(self):
roles = dict()
roles[self.SfiRole] = b'sfiRole'
roles[self.NameRole] = b'nameRole'
roles[self.ClsRole] = b'clsRole'
roles[self.FlagRole] = b'flagRole'
roles[self.OwnrRole] = b'ownrRole'
roles[self.RecordRole] = b'recordRole'
roles[self.DeptRole] = b'deptRole'
roles[self.DateStrRole] = b'dateStrRole'
roles[self.HourRole] = b'hourRole'
roles[self.EstTimeRole] = b'estTimeRole'
roles[self.StatusRole] = b'statusRole'
return roles
Model described above works for retaining data and calling by index and role names. I opt to use QListView and QStyledItemDelegate for displaying data in Qt based view. As far as i now, I must develop a paint method for display purposes and for basic models Qt uses DisplayRole for reading strings from data models. But as you can see from the model, I have not facilitated the DispplayRole and I have several custom roles for data points to display for each item in model.
I try to create a paint method for a delegate that inherits from QStyledItemDelegate to paint something similar to this. I marked the roles on the picture for example.
In short the question is: Is it possible to override paint method of QStyledItemDelegate for displaying strings supplied by custom data roles and also display is it possible to draw buttons for calling editor widget within a custom delegate?
I tried to create a QWidget based solution and I failed. After that Try to create and paint method to achieve look that i wanted. I am leaving my delegate class and paint method below as an example of a complex delegate.
class TestItemDelegate(QStyledItemDelegate):
"""A delegate to show test items in listview in qt side"""
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.model = self.parent().model()
def paint(self, painter: QPainter,
option: QStyleOptionViewItem, index: QModelIndex):
model_ind = index.model()
canvas = option.rect.getRect()
painter.setRenderHint(QPainter.Antialiasing, on=True)
# Supplying data from model
sfi = index.data(model_ind.SfiRole)
name = index.data(model_ind.NameRole)
cls_att = index.data(model_ind.ClsRole)
flg_att = index.data(model_ind.FlagRole)
ownr_att = index.data(model_ind.OwnrRole)
# rec_stat = index.data(model_ind.RecordRole)
resp_dept = index.data(model_ind.DeptRole)
date_str = index.data(model_ind.DateStrRole)
hour_str = index.data(model_ind.HourRole)
est_duration = index.data(model_ind.EstTimeRole)
# status = model_ind.data(index, model_ind.StatusRole)
# Frame and Background(s)
pen = QPen()
pen.setColor("Black")
pen.setWidth(2)
painter.setPen(pen)
painter.drawRoundedRect(option.rect, 10, 10)
painter.setPen(Qt.blue)
# Coordinates for delegate background
x, y, w, h = canvas
# Drawing of the texts
painter.drawText(
QRect(x + 50, y, w-200, h//2),
Qt.AlignVCenter, name)
painter.drawText(
QRect(x, y, h, h),
Qt.AlignCenter, sfi)
painter.drawText(
QRect(x+w-150, y, 50, h//3),
Qt.AlignVCenter,
"C:{}".format('-' if cls_att == '' else cls_att))
painter.drawText(
QRect(x+w-150, y+h//3, 50, h//3),
Qt.AlignVCenter,
"F:{}".format('-' if flg_att == '' else flg_att))
painter.drawText(
QRect(x+w-150, y + 2*h//3, 50, h//3),
Qt.AlignVCenter,
"O:{}".format('-' if ownr_att == '' else ownr_att))
painter.drawText(
QRect(x+50, y+h//2, w//3, h//2),
Qt.AlignVCenter, resp_dept)
painter.drawText(
QRect(x+w-100, y, 100, h//2),
Qt.AlignCenter, date_str.strftime('%d-%m-%Y')
)
painter.drawText(
QRect(x+w-100, y+h//2, 50, h//2),
Qt.AlignCenter, hour_str.strftime('%H:%M')
)
painter.drawText(
QRect(x+w-50, y+h//2, 50, h//2),
Qt.AlignCenter, est_duration
)
def createEditor(self, parent, option, index):
print(type(parent))
def sizeHint(self, option, index):
size = QSize(300, 50)
return size
I set the model as selectable and editable via flags method of the model.
def flags(self, index):
if index.isValid():
return (Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable)
return super().flags(index)
I am creating editor widget and relative methods right now. Might update this answer to leave a comprehensive answer after I achieved a feasible solution.
UPDATE: (SOLVED)
Thanks to eyllanesc for making me write a working example code of my problem. That made me find the problem. Here is a working code with a fixed problem:
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
import sys
import os
class ExtendedTreeWidgetItem(QTreeWidgetItem):
def __init__(self, *args, **kwargs):
super(ExtendedTreeWidgetItem, self).__init__(*args, **kwargs)
self.rescueFile = None
print("created first time")
def setRescueFile(self, rFile):
self.rescueFile = rFile
print("setting rescue file", self.rescueFile, "for item", self.text(0))
def getRescueFile(self):
return self.rescueFile
class RescueFile:
def __init__(self, path):
self.path = path
self.ischecked = True
def isChecked(self):
return self.ischecked
def setChecked(self, checked):
if isinstance(checked, bool):
self.ischecked = checked
elif isinstance(checked, Qt.CheckState):
self.ischecked = True if checked == Qt.Checked else False
def getPath(self):
return self.path
class Window(QMainWindow):
def __init__(self):
super(Window, self).__init__()
windowSize = (800,400)
screenSize = QDesktopWidget().availableGeometry()
self.setGeometry(screenSize.width()/2 - windowSize[0]/2, screenSize.height()/2 - windowSize[1]//2, windowSize[0], windowSize[1])
self.setWindowTitle("TreeTest")
self.buildGui()
self.show()
def buildGui(self):
wgt = QWidget()
vlayout = QVBoxLayout()
vlayout.setAlignment(Qt.AlignTop)
grid_layout = QGridLayout()
self.setupTree(grid_layout)
vlayout.addLayout(grid_layout)
wgt.setLayout(vlayout)
self.setCentralWidget(wgt)
def setupTree(self, grid_layout):
self.treeWidget = QTreeWidget()
self.treeWidget.setHeaderLabel("Results")
self.treeWidget.setColumnCount(1)
#INSERTING DATA
topItem = ExtendedTreeWidgetItem()
topItem.setText(0, "top item")
topItem.setCheckState(0, Qt.Checked)
child1 = ExtendedTreeWidgetItem(topItem)
child1.setText(0, "child 1")
child1.setCheckState(0, Qt.Checked)
topItem.addChild(child1)
deeper_child = ExtendedTreeWidgetItem(child1)
deeper_child.setText(0, "deeper child 1")
deeper_child.setCheckState(0, Qt.Checked)
r_file = RescueFile("/home/user1/Desktop")
deeper_child.setRescueFile(r_file)
child1.addChild(deeper_child)
self.treeWidget.addTopLevelItem(topItem)
self.treeWidget.expandAll()
self.treeWidget.itemChanged.connect(self.singleClickTreeWidget)
grid_layout.addWidget(self.treeWidget, 1, 1, 4, 3)
def singleClickTreeWidget(self, widgetItem, column):
parent = widgetItem.parent()
if parent and parent.checkState(0) == Qt.Unchecked:
widgetItem.setCheckState(0, Qt.Unchecked)
return
checkState = widgetItem.checkState(0)
widgetItem.setCheckState(0, checkState)
rescue_file = widgetItem.getRescueFile()
if rescue_file:
rescue_file.setChecked(checkState)
print("rescue file found, path:", rescue_file.getPath(), "checked:", rescue_file.isChecked())
self.iterateThroughChildren(widgetItem, checkState)
def iterateThroughChildren(self, item, checkState):
for i in range(item.childCount()):
child = item.child(i)
print("child:", child, ",text:", child.text(0))
child.setCheckState(0, checkState)
# HERE WAS THE MISTAKE
# -- >> rescue_file = item.getRescueFile() << --
rescue_file = child.getRescueFile() # CORRECT CODE!!
print("rescue file", rescue_file, "child", child.text(0))
if rescue_file is not None:
rescue_file.setChecked(checkState)
else:
self.iterateThroughChildren(child, checkState)
def main():
if __name__ == '__main__':
app = QApplication(sys.argv)
window = Window()
app.exec_()
main()
BEFORE UPDATE:
I hope I can explain my problem clearly enough.
I trying to create QTreeWidget that uses my extended QTreeWidgetItems in python 3.5.3. Here is my code for the item class:
class ExtendedTreeWidgetItem(QTreeWidgetItem):
def __init__(self, *args, **kwargs):
super(ExtendedTreeWidgetItem, self).__init__(*args, **kwargs)
self.rescueFile = None
def setRescueFile(self, rFile):
self.rescueFile = rFile
def getRescueFile(self):
return self.rescueFile
class RescueFile:
def __init__(self, path):
self.path = path
self.ischecked = True
def isChecked(self):
return self.ischecked
def setChecked(self, checked):
if isinstance(checked, bool):
self.ischecked = checked
elif isinstance(checked, Qt.CheckState):
self.ischecked = True if checked == Qt.Checked else False
print(self.path, self.ischecked)
I use this code to implement checking and unchecking the rescue file:
****
...
self.treeWidget.itemChanged.connect(self.singleClickTreeWidget)
...
****
def singleClickTreeWidget(self, widgetItem, column):
parent = widgetItem.parent()
if parent and parent.checkState(0) == Qt.Unchecked:
widgetItem.setCheckState(0, Qt.Unchecked)
return
checkState = widgetItem.checkState(0)
widgetItem.setCheckState(0, checkState)
rescue_file = widgetItem.getRescueFile()
**# I CAN GET THE RESCUE FILE OBJECT HERE FROM**
if rescue_file:
rescue_file.setChecked(checkState)
self.iterateThroughChildren(widgetItem, checkState)
def iterateThroughChildren(self, item, checkState):
for i in range(item.childCount()):
child = item.child(i)
child.setCheckState(0, checkState)
**# I CAN'T GET ANY FIND HERE ANYMORE**
rescue_file = item.getRescueFile()
if rescue_file:
rescue_file.setChecked(checkState)
else:
self.iterateThroughChildren(child, checkState)
What my code does is that it generates a checkable tree like in the picture:
What I'm trying to achieve is that when I deselect/select an item (ExtendedTreeWidgetItem) that has children, all children are also selected/deselected as well as RescueFile objects associated with items. Only files are associated with RescueFile objects. Directories are left with self.rescueFile = None
For example if I pressed to deselect 'icons' then magnifier.png should also be deselected as well as RescueFile associated with it. Deselection of a checkboxes works like a charm but RescueFile is not affected (NOT FOUND) if I press on a parent of a file. But it works if I press directly on a file, for example magnifier.png.
I have tried to trace if it is a pointer problem but it seems like all objects point to the objects they are supposed to. I don't understand where does a rescueFile disappear if I am comming to ExtendedTreeWidgetItem recursively through it's parent.
I have two questions:
I was wondering if this is the proper way to do a search/filter on a single column treeview. I feel like a lot of my copying/pasting could contain unnecessary stuff. Is all the code in the subclass of QSortFilterProxyModel and the code in the search_text_changed method needed? I don't feel like the regex is needed, since I set the filter-proxy to ignore case-sensitivity.
How can I make it so that when a user double-clicks a treeview item a signal emits a string list containing the string of the item clicked and all of its ancestors recursively? For example, if I double-clicked "Birds", it would return ['Birds','Animals']; and if I double-clicked "Animals", it would just return ['Animals'].
import os, sys
from PySide import QtCore, QtGui
tags = {
"Animals": [
"Birds",
"Various"
],
"Brick": [
"Blocks",
"Special"
],
"Manmade": [
"Air Conditioners",
"Audio Equipment"
],
"Food": [
"Fruit",
"Grains and Seeds"
]
}
class SearchProxyModel(QtGui.QSortFilterProxyModel):
def __init__(self, parent=None):
super(SearchProxyModel, self).__init__(parent)
self.text = ''
# Recursive search
def _accept_index(self, idx):
if idx.isValid():
text = idx.data(role=QtCore.Qt.DisplayRole).lower()
condition = text.find(self.text) >= 0
if condition:
return True
for childnum in range(idx.model().rowCount(parent=idx)):
if self._accept_index(idx.model().index(childnum, 0, parent=idx)):
return True
return False
def filterAcceptsRow(self, sourceRow, sourceParent):
# Only first column in model for search
idx = self.sourceModel().index(sourceRow, 0, sourceParent)
return self._accept_index(idx)
def lessThan(self, left, right):
leftData = self.sourceModel().data(left)
rightData = self.sourceModel().data(right)
return leftData < rightData
class TagsBrowserWidget(QtGui.QWidget):
clickedTag = QtCore.Signal(list)
def __init__(self, parent=None):
super(TagsBrowserWidget, self).__init__(parent)
self.resize(300,500)
# controls
self.ui_search = QtGui.QLineEdit()
self.ui_search.setPlaceholderText('Search...')
self.tags_model = SearchProxyModel()
self.tags_model.setSourceModel(QtGui.QStandardItemModel())
self.tags_model.setDynamicSortFilter(True)
self.tags_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
self.ui_tags = QtGui.QTreeView()
self.ui_tags.setSortingEnabled(True)
self.ui_tags.sortByColumn(0, QtCore.Qt.AscendingOrder)
self.ui_tags.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers)
self.ui_tags.setHeaderHidden(True)
self.ui_tags.setRootIsDecorated(True)
self.ui_tags.setUniformRowHeights(True)
self.ui_tags.setModel(self.tags_model)
# layout
main_layout = QtGui.QVBoxLayout()
main_layout.addWidget(self.ui_search)
main_layout.addWidget(self.ui_tags)
self.setLayout(main_layout)
# signals
self.ui_tags.doubleClicked.connect(self.tag_double_clicked)
self.ui_search.textChanged.connect(self.search_text_changed)
# init
self.create_model()
def create_model(self):
model = self.ui_tags.model().sourceModel()
self.populate_tree(tags, model.invisibleRootItem())
self.ui_tags.sortByColumn(0, QtCore.Qt.AscendingOrder)
def populate_tree(self, children, parent):
for child in sorted(children):
node = QtGui.QStandardItem(child)
parent.appendRow(node)
if isinstance(children, dict):
self.populate_tree(children[child], node)
def tag_double_clicked(self, item):
text = item.data(role=QtCore.Qt.DisplayRole)
print [text]
self.clickedTag.emit([text])
def search_text_changed(self, text=None):
regExp = QtCore.QRegExp(self.ui_search.text(), QtCore.Qt.CaseInsensitive, QtCore.QRegExp.FixedString)
self.tags_model.text = self.ui_search.text().lower()
self.tags_model.setFilterRegExp(regExp)
if len(self.ui_search.text()) >= 1 and self.tags_model.rowCount() > 0:
self.ui_tags.expandAll()
else:
self.ui_tags.collapseAll()
def main():
app = QtGui.QApplication(sys.argv)
ex = TagsBrowserWidget()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
There's no point in setting the case-sensivity of the filter-proxy at all, because you are by-passing the built-in filtering by overriding filterAcceptsRow. And even if you weren't doing that, setFilterRegExp ignores the current case sensitiviy settings anyway.
I would simplify the filter-proxy to this:
class SearchProxyModel(QtGui.QSortFilterProxyModel):
def setFilterRegExp(self, pattern):
if isinstance(pattern, str):
pattern = QtCore.QRegExp(
pattern, QtCore.Qt.CaseInsensitive,
QtCore.QRegExp.FixedString)
super(SearchProxyModel, self).setFilterRegExp(pattern)
def _accept_index(self, idx):
if idx.isValid():
text = idx.data(QtCore.Qt.DisplayRole)
if self.filterRegExp().indexIn(text) >= 0:
return True
for row in range(idx.model().rowCount(idx)):
if self._accept_index(idx.model().index(row, 0, idx)):
return True
return False
def filterAcceptsRow(self, sourceRow, sourceParent):
idx = self.sourceModel().index(sourceRow, 0, sourceParent)
return self._accept_index(idx)
and change the search method to this:
def search_text_changed(self, text=None):
self.tags_model.setFilterRegExp(self.ui_search.text())
if len(self.ui_search.text()) >= 1 and self.tags_model.rowCount() > 0:
self.ui_tags.expandAll()
else:
self.ui_tags.collapseAll()
So now the SearchProxyModel has sole responsibilty for deciding how searches are performed via its setFilterRegExp method. The case-sensitivity is handled transparently, so there is no need to pre-process the input.
The method for getting a list of descendants, can be written like this:
def tag_double_clicked(self, idx):
text = []
while idx.isValid():
text.append(idx.data(QtCore.Qt.DisplayRole))
idx = idx.parent()
text.reverse()
self.clickedTag.emit(text)
In my Gui I creats a QTableView with a QStandardItemModel and I would like to add either an additional row or column at a given position.
class Output(object):
def __init__(self):
''' '''
self.tabs = QtGui.QTabWidget()
self.group_box = QtGui.QGroupBox('Example')
def run(self):
form_layout = QtGui.QFormLayout(self.group_box)
self.tabs.addTab(self.__genTable(),"Tab 1")
self.tabs.addTab(self.__genTable(),"Tab 2")
form_layout.addWidget(self.tabs)
return self.group_box
def __genTable(self):
table_view = QtGui.QTableView()
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
table_view.customContextMenuRequested.connect( self.__openMenu )
table_view.setSelectionBehavior( QtGui.QTableView.SelectItems )
table_view.setModel(QtGui.QStandardItemModel(4, 2))
return table_view
def __openMenu(self, position):
menu = QtGui.QMenu()
sub_menu_row = QtGui.QMenu("Row")
menu.addMenu(sub_menu_row)
addRowBelowAction = sub_menu_row.addAction("add Row below")
action = menu.exec_(QtGui.QCursor.pos())
if action == addRowBelowAction:
idx = self.tabs.currentWidget().selectionModel().currentIndex()
for i in range(self.tabs.count()):
model = self.tabs.widget(i).selectionModel()
model.insertRow(idx.row(), QtCore.QModelIndex())
Unfortunately i get the following error:
model.insertRow(idx.row(), QtCore.QModelIndex())
AttributeError: 'PySide.QtGui.QItemSelectionModel' object has no attribute 'insertRow'
Untested, but try holding a reference to your model
then call the appropriate methods of the model (insertRow, insertColumn).
The effect of these methods will be apparent in the view.
E.g.:
table_view = QtGui.QTableView()
table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
table_view.setSelectionBehavior( QtGui.QTableView.SelectItems )
model = QtGui.QStandardItemModel(4, 2)
table_view.setModel(model)
model.insertRow(2, QtCore.QModelIndex())
I want to investigate how to make a small user interface in which a user can type some letters and gets some suggestions based on a given data source (list here) which makes searches easier. For this purpose i am using Qt's QCompleter class.
In the matching elements the typed letters shall be highlighted with HTML like the example in the code below: Au<b>st</b>ria.
Finally i merged some SO answers (see How to make item view render rich (html) text in Qt) and tutorials to a small standalone module:
from PySide import QtCore, QtGui
class HTMLDelegate(QtGui.QStyledItemDelegate):
""" From: https://stackoverflow.com/a/5443112/1504082 """
def paint(self, painter, option, index):
options = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(options, index)
if options.widget is None:
style = QtGui.QApplication.style()
else:
style = options.widget.style()
doc = QtGui.QTextDocument()
doc.setHtml(options.text)
doc.setTextWidth(option.rect.width())
options.text = ""
style.drawControl(QtGui.QStyle.CE_ItemViewItem, options, painter)
ctx = QtGui.QAbstractTextDocumentLayout.PaintContext()
# Highlighting text if item is selected
# if options.state & QtGui.QStyle.State_Selected:
# ctx.palette.setColor(QtGui.QPalette.Text,
# options.palette.color(QtGui.QPalette.Active,
# QtGui.QPalette.HighlightedText))
textRect = style.subElementRect(QtGui.QStyle.SE_ItemViewItemText,
options)
painter.save()
painter.translate(textRect.topLeft())
painter.setClipRect(textRect.translated(-textRect.topLeft()))
doc.documentLayout().draw(painter, ctx)
painter.restore()
def sizeHint(self, option, index):
options = QtGui.QStyleOptionViewItemV4(option)
self.initStyleOption(options, index)
doc = QtGui.QTextDocument()
doc.setHtml(options.text)
doc.setTextWidth(options.rect.width())
return QtCore.QSize(doc.size().width(), doc.size().height())
class CustomQCompleter(QtGui.QCompleter):
""" Implement "contains" filter mode as the filter mode "contains" is not
available in Qt < 5.2
From: https://stackoverflow.com/a/7767999/1504082 """
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
self.delegate = HTMLDelegate()
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
# see: http://doc.qt.io/qt-4.8/model-view-programming.html#proxy-models
class InnerProxyModel(QtGui.QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
# model index mapping by row, 1d model => column is always 0
index = self.sourceModel().index(sourceRow, 0, sourceParent)
source_data = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
# performs case insensitive matching
# return True if item shall stay in th returned filtered data
# return False to reject an item
return local_completion_prefix.lower() in source_data.lower()
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
# #todo: Why to be set here again?
self.popup().setItemDelegate(self.delegate)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
return ""
class AutoCompleteEdit(QtGui.QLineEdit):
""" Basically from:
http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
"""
def __init__(self, list_data, separator=' ', addSpaceAfterCompleting=True):
super(AutoCompleteEdit, self).__init__()
# settings
self._separator = separator
self._addSpaceAfterCompleting = addSpaceAfterCompleting
# completer
self._completer = CustomQCompleter(self)
self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._completer.setCompletionMode(QtGui.QCompleter.PopupCompletion)
self.model = QtGui.QStringListModel(list_data)
self._completer.setModel(self.model)
# connect the completer to the line edit
self._completer.setWidget(self)
# trigger insertion of the selected completion when its activated
self.connect(self._completer,
QtCore.SIGNAL('activated(QString)'),
self._insertCompletion)
self._ignored_keys = [QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return,
QtCore.Qt.Key_Escape,
QtCore.Qt.Key_Tab]
def _insertCompletion(self, completion):
"""
This is the event handler for the QCompleter.activated(QString) signal,
it is called when the user selects an item in the completer popup.
It will remove the already typed string with the one of the completion.
"""
stripped_text = self.text()[:-len(self._completer.completionPrefix())]
extra_text = completion # [-extra:]
if self._addSpaceAfterCompleting:
extra_text += ' '
self.setText(stripped_text + extra_text)
def textUnderCursor(self):
text = self.text()
textUnderCursor = ''
i = self.cursorPosition() - 1
while i >= 0 and text[i] != self._separator:
textUnderCursor = text[i] + textUnderCursor
i -= 1
return textUnderCursor
def keyPressEvent(self, event):
if self._completer.popup().isVisible():
if event.key() in self._ignored_keys:
event.ignore()
return
super(AutoCompleteEdit, self).keyPressEvent(event)
completionPrefix = self.textUnderCursor()
if completionPrefix != self._completer.completionPrefix():
self._updateCompleterPopupItems(completionPrefix)
if len(event.text()) > 0 and len(completionPrefix) > 0:
self._completer.complete()
if len(completionPrefix) == 0:
self._completer.popup().hide()
def _updateCompleterPopupItems(self, completionPrefix):
"""
Filters the completer's popup items to only show items
with the given prefix.
"""
self._completer.setCompletionPrefix(completionPrefix)
# self._completer.popup().setCurrentIndex(
# self._completer.completionModel().index(0, 0))
if __name__ == '__main__':
def demo():
import sys
app = QtGui.QApplication(sys.argv)
values = ['Germany',
'Au<b>st</b>ria',
'Switzerland',
'Hungary',
'The United Kingdom of Great Britain and Northern Ireland']
editor = AutoCompleteEdit(values)
window = QtGui.QWidget()
hbox = QtGui.QHBoxLayout()
hbox.addWidget(editor)
window.setLayout(hbox)
window.show()
sys.exit(app.exec_())
demo()
My problem is the suggestion of user Timo in the answer https://stackoverflow.com/a/5443112/1504082:
After line: 'doc.setHtml(options.text)', you need to set also doc.setTextWidth(option.rect.width()), otherwise the delegate wont render longer content correctly in respect to target drawing area. For example does not wrap words in QListView.
So i did this to avoid cropping of long text in the completer's popup. But i get the following output:
Where does this additional vertical margin come from?
I investigated this a bit and i see that the sizeHint method of HTMLDelegate is sometimes called with an options parameter which contains a rectangle with attributes (0, 0, 0, 0). And the display behaviour finally changes after the call of doc.setTextWidth(options.rect.width()). But i couldnt finally find out who calls it with this parameter and how i could properly fix this.
Can somebody explain where this comes from and how i can fix this porperly?
Finally i found another way to realize it using the idea of https://stackoverflow.com/a/8036666/1504082. Its much more forward for me without all this custom drawing things which i dont understand yet :)
from PySide import QtCore, QtGui
class TaskDelegate(QtGui.QItemDelegate):
# based on https://stackoverflow.com/a/8036666/1504082
# https://doc.qt.io/archives/qt-4.7/qitemdelegate.html#drawDisplay
# https://doc.qt.io/archives/qt-4.7/qwidget.html#render
margin_x = 5
margin_y = 3
def drawDisplay(self, painter, option, rect, text):
label = self.make_label(option, text)
# calculate render anchor point
point = rect.topLeft()
point.setX(point.x() + self.margin_x)
point.setY(point.y() + self.margin_y)
label.render(painter, point, renderFlags=QtGui.QWidget.DrawChildren)
def sizeHint(self, option, index):
# get text using model and index
text = index.model().data(index)
label = self.make_label(option, text)
return QtCore.QSize(label.width(), label.height() + self.margin_y)
def make_label(self, option, text):
label = QtGui.QLabel(text)
if option.state & QtGui.QStyle.State_Selected:
p = option.palette
p.setColor(QtGui.QPalette.WindowText,
p.color(QtGui.QPalette.Active,
QtGui.QPalette.HighlightedText)
)
label.setPalette(p)
label.setStyleSheet("border: 1px dotted black")
# adjust width according to widget's target width
label.setMinimumWidth(self.target_width - (2 * self.margin_x))
label.setMaximumWidth(self.target_width - self.margin_x)
label.setWordWrap(True)
label.adjustSize()
return label
class CustomQCompleter(QtGui.QCompleter):
""" Implement "contains" filter mode as the filter mode "contains" is not
available in Qt < 5.2
From: https://stackoverflow.com/a/7767999/1504082 """
def __init__(self, parent=None):
super(CustomQCompleter, self).__init__(parent)
self.local_completion_prefix = ""
self.source_model = None
self.delegate = TaskDelegate()
# widget not set yet
# self.delegate.target_width = self.widget().width()
def setModel(self, model):
self.source_model = model
super(CustomQCompleter, self).setModel(self.source_model)
def updateModel(self):
local_completion_prefix = self.local_completion_prefix
# see: http://doc.qt.io/qt-4.8/model-view-programming.html#proxy-models
class InnerProxyModel(QtGui.QSortFilterProxyModel):
def filterAcceptsRow(self, sourceRow, sourceParent):
# model index mapping by row, 1d model => column is always 0
index = self.sourceModel().index(sourceRow, 0, sourceParent)
source_data = self.sourceModel().data(index, QtCore.Qt.DisplayRole)
# performs case insensitive matching
# return True if item shall stay in th returned filtered data
# return False to reject an item
return local_completion_prefix.lower() in source_data.lower()
proxy_model = InnerProxyModel()
proxy_model.setSourceModel(self.source_model)
super(CustomQCompleter, self).setModel(proxy_model)
# #todo: Why to be set here again?
# -> rescale popup list items to widget width
self.delegate.target_width = self.widget().width()
self.popup().setItemDelegate(self.delegate)
def splitPath(self, path):
self.local_completion_prefix = path
self.updateModel()
return ""
class AutoCompleteEdit(QtGui.QLineEdit):
""" Basically from:
http://doc.qt.io/qt-5/qtwidgets-tools-customcompleter-example.html
"""
def __init__(self, list_data, separator=' ', addSpaceAfterCompleting=True):
super(AutoCompleteEdit, self).__init__()
# settings
self._separator = separator
self._addSpaceAfterCompleting = addSpaceAfterCompleting
# completer
self._completer = CustomQCompleter(self)
self._completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
self._completer.setCompletionMode(QtGui.QCompleter.PopupCompletion)
self.model = QtGui.QStringListModel(list_data)
self._completer.setModel(self.model)
# connect the completer to the line edit
self._completer.setWidget(self)
# trigger insertion of the selected completion when its activated
self.connect(self._completer,
QtCore.SIGNAL('activated(QString)'),
self._insertCompletion)
self._ignored_keys = [QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Return,
QtCore.Qt.Key_Escape,
QtCore.Qt.Key_Tab]
def _insertCompletion(self, completion):
"""
This is the event handler for the QCompleter.activated(QString) signal,
it is called when the user selects an item in the completer popup.
It will remove the already typed string with the one of the completion.
"""
stripped_text = self.text()[:-len(self._completer.completionPrefix())]
extra_text = completion # [-extra:]
if self._addSpaceAfterCompleting:
extra_text += ' '
self.setText(stripped_text + extra_text)
def textUnderCursor(self):
text = self.text()
textUnderCursor = ''
i = self.cursorPosition() - 1
while i >= 0 and text[i] != self._separator:
textUnderCursor = text[i] + textUnderCursor
i -= 1
return textUnderCursor
def keyPressEvent(self, event):
if self._completer.popup().isVisible():
if event.key() in self._ignored_keys:
event.ignore()
return
super(AutoCompleteEdit, self).keyPressEvent(event)
completionPrefix = self.textUnderCursor()
if completionPrefix != self._completer.completionPrefix():
self._updateCompleterPopupItems(completionPrefix)
if len(event.text()) > 0 and len(completionPrefix) > 0:
self._completer.complete()
if len(completionPrefix) == 0:
self._completer.popup().hide()
def _updateCompleterPopupItems(self, completionPrefix):
"""
Filters the completer's popup items to only show items
with the given prefix.
"""
self._completer.setCompletionPrefix(completionPrefix)
# self._completer.popup().setCurrentIndex(
# self._completer.completionModel().index(0, 0))
if __name__ == '__main__':
def demo():
import sys
app = QtGui.QApplication(sys.argv)
values = ['Germany',
'Au<b>st</b>ria',
'Switzerland',
'Hungary',
'The United Kingdom of Great Britain and Northern Ireland',
'USA']
editor = AutoCompleteEdit(values)
window = QtGui.QWidget()
hbox = QtGui.QHBoxLayout()
hbox.addWidget(editor)
window.setLayout(hbox)
window.show()
sys.exit(app.exec_())
demo()