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())
Related
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!
I've seen questions similar to this one but they are aimed at QTableView. This is not using that,, this is just for a dropdown (QComboBox) with a custom QAbstractTableModel, which needs to have 2 columns.
BIG UPDATE
(Note: Legacy code has been deleted as this is a better approach on the same question, and legacy code was confusing as hell).
Okay, so trying to catch up with what #eyllanesc explained, I changed this from a QAbstractListModel to a QAbstractTableModel. The result is:
class ModelForComboboxesWithID(QAbstractTableModel):
"""Create our basic model"""
def __init__(self, program, records):
super(ModelForComboboxesWithID, self).__init__()
self._data = records
self.program = program
self.path_images = program.PATH_IMAGES
def rowCount(self, index: int = 0) -> int:
"""The length of the outer list. Structure: [row, row, row]"""
if not self._data:
return 0 # Doubt: Do we need to return this if self._data is empty?
return len(self._data)
def columnCount(self, index: int = 0) -> int:
"""The length of the sub-list inside the outer list. Meaning that Columns are inside rows
Structure: [row [column], row [column], row [column]]"""
if not self._data:
return 0 # Doubt: Do we need to return this if self._data is empty?
return len(self._data[0])
def data(self, index, role=None):
"""return the data on this index as data[row][column]"""
# 1 - Display data based on its content (this edits the text that you visually see)
if role == Qt.DisplayRole:
value = self._data[index.row()][index.column()]
return value
# 2 - Tooltip displayed when hovering on it
elif role == Qt.ToolTipRole:
return f"ID: {self._data[index.row()][1]}"
Which I set this way:
def eventFilter(self, target, event: QEvent):
if event.type() == QEvent.MouseButtonPress:
if target == self.Buscadorcombo_cliente:
records = ... # my query to the database
set_combo_records_with_ids(self.program, target, records)
target.currentIndexChanged.connect(self.test)
def set_combo_records_with_ids(program, combobox: QComboBox, records):
"""Clear combobox, set model/data and sort it"""
combobox.clear()
model = ModelForComboboxesWithID(program, records)
combobox.setModel(model)
combobox.model().sort(0, Qt.AscendingOrder)
combobox.setModelColumn(0)
The result of this works almost perfect:
On the dropdown(Combobox) it displays the name.
If you hover on an item, it displays the ID.
Now I am able to get any data of it this way.
def test(self, index):
data_id = self.Buscadorcombo_cliente.model().index(index, 1).data()
data_name = self.Buscadorcombo_cliente.model().index(index, 0).data()
print(data_id)
print(data_name)
You have to set a QTableView as a view:
from PySide2 import QtGui, QtWidgets
def main():
import sys
app = QtWidgets.QApplication(sys.argv)
w = QtWidgets.QWidget()
combo = QtWidgets.QComboBox()
model = QtGui.QStandardItemModel(0, 2)
for i in range(10):
items = []
for j in range(model.columnCount()):
it = QtGui.QStandardItem(f"it-{i}{j}")
items.append(it)
model.appendRow(items)
combo.setModel(model)
view = QtWidgets.QTableView(
combo, selectionBehavior=QtWidgets.QAbstractItemView.SelectRows
)
combo.setView(view)
view.verticalHeader().hide()
view.horizontalHeader().hide()
header = view.horizontalHeader()
for i in range(header.count()):
header.setSectionResizeMode(i, QtWidgets.QHeaderView.Stretch)
lay = QtWidgets.QVBoxLayout(w)
lay.addWidget(combo)
lay.addStretch()
w.resize(640, 480)
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I am having issues with .deleter where the values I am trying to delete only works for the first try.
To replicate:
Checked 'GrpB-Alice' and hit 'Click Me' (Do not close the window yet)
i.myDict returns {u'GrpB': set([u'Alice'])}) which is correct
Checked 'GrpB-Phan' and hit 'Click Me'
i.myDict returns {u'GrpB': set([u'Alice', 'Phan])}) which is correct
Checked 'GrpB-Alice' and hit 'Click Me'
i.myDict returns {u'GrpB': set([u'Alice', 'Phan])}) which is incorrect where I am expecting {u'GrpB': set([u'Alice'])})
If I close the tool, and redo the last step, only will i.myDict returns correctly, but should I checked on new options, at the third time, i.myDict will returns incorrect result again.
The deleter is definitely being called but it only seems to execute the real deletion once though. Am I missing something here?
class SubMenuWindow(QtGui.QWidget):
def __init__(self, menu_items, parent=None, callback=None):
super(SubMenuWindow, self).__init__(parent)
self.callback = callback
self.my_lyt = QtGui.QVBoxLayout()
self.checked_options = []
self.sel = defaultdict(set)
for menu_name, submenu_name in menu_items.items():
# Set the main menu item name
self.groupbox = QtGui.QGroupBox(self)
self.groupbox.setTitle(menu_name)
self.groupbox.setLayout(QtGui.QVBoxLayout())
self.my_lyt.addWidget(self.groupbox)
if submenu_name:
sub_txt = [action for action in submenu_name]
for s in sub_txt:
sub_chk = QtGui.QCheckBox(s)
self.checked_options.append(sub_chk)
self.groupbox.layout().addWidget(sub_chk)
apply_tag_btn = QtGui.QPushButton('Apply to selected item')
apply_tag_btn.clicked.connect(self.get_checked_options)
self.my_lyt.addWidget(apply_tag_btn)
self.my_lyt.addStretch()
self.setLayout(self.my_lyt)
self.show()
def get_checked_options(self):
for chk in self.checked_options:
if chk.isChecked():
print 'Checked - {0}, Parent - {1}'.format(chk.text(), chk.parent().title())
self.sel[chk.parent().title()].add(chk.text())
if self.callback:
self.callback()
class MainWin(QtGui.QWidget):
def __init__(self, parent=None):
super(MainWin, self).__init__(parent)
self.my_dict = {}
btnA = QtGui.QPushButton('Click Me')
btnA.clicked.connect(self.get_options)
btnB = QtGui.QPushButton('Get results')
btnB.clicked.connect(self.get_results)
layout = QtGui.QVBoxLayout()
layout.addWidget(btnA)
layout.addWidget(btnB)
self.setLayout(layout)
def get_options(self):
sample_dict = {'GrpA' : ['John', 'Zack'], 'GrpB' : ['Alice', 'Phan']}
self.subWin = SubMenuWindow(sample_dict, callback=self.get_results)
def get_results(self):
i = MyDict()
# delete any existing values
del (i.myDict)
# append the new values
i.myDict = self.subWin.sel
print 'current myDict values : ', i.myDict
# If I select 'GrpB - Alice'
# i.myDict is `{u'GrpB': set([u'Alice'])})` # correct
# If I select 'GrpB - Alice' and 'GrpB - Phan'
# i.myDict is `{u'GrpB': set([u'Alice', 'Phan'])})` # correct
# If I re-select 'GrpB - Alice'
# i.myDict is `{u'GrpB': set([u'Alice', 'Phan'])})` # in-correct
class MyDict(QtCore.QObject):
def __init__(self):
self._myDict = None
#property
def myDict(self):
return self._myDict
#myDict.setter
def myDict(self, value):
self._myDict = value
#myDict.deleter
def myDict(self):
del(self._myDict)
Code:
class Example(QtGui.QWidget):
def __init__(self, parent):
super(Accounts, self).__init__()
self.initUI()
def initUI(self):
self.table = QtGui.QTableWidget()
self.table.setColumnCount(5)
database = 'Setting.db'
connection = sqlite3.connect(database)
cursor = connection.cursor()
cursor.execute('''
SELECT ID, User, Text
FROM Data ''')
data = cursor.fetchall()
num = 0
for row in data:
self.table.setRowCount(num+2)
self.table.setItem(num, 0, QtGui.QTableWidgetItem(str(row[0])))
self.table.setItem(num, 1, QtGui.QTableWidgetItem(str(row[1])))
self.table.setItem(num, 2, QtGui.QTableWidgetItem(str(row[2])))
self.table.setCellWidget(num, 3, Perms(self, row[0]))
save_user = QtGui.QPushButton("Save")
save_user.clicked.connect(self.save)
self.table.setCellWidget(num, 5, save_user)
num= num+1
main_layout = QtGui.QGridLayout()
main_layout.addWidget(self.table)
self.setLayout(main_layout)
def save(self):
button = self.sender()
index = self.table.indexAt(button.pos())
row = index.row()
a_id = str(self.table.item(row, 0).text())
data = Permissions(self, a_id).update_database()
class Perms(QtGui.QWidget):
def __init__(self, parent, a_id):
super(Perms, self).__init__()
self.a_id = a_id
self.init_ui()
def init_ui(self):
database = 'Settings.db'
conn = sqlite3.connect(database)
cursor = conn.cursor()
cursor.execute('''
SELECT control, sub
FROM Perms
WHERE ID= ?''', (self.a_id,))
row = cursor.fetchone()
control_label = QtGui.QLabel("Control")
sub_label = QtGui.QLabel("Sub")
self.control_options = QtGui.QComboBox()
self.control_options.addItem("Y")
self.control_options.addItem("N")
if str(row[0]) == "Y":
self.control_options.setCurrentIndex(0)
else:
self.control_options.setCurrentIndex(1)
self.sub_options = QtGui.QComboBox()
self.sub_options.addItem("Y")
self.sub_options.addItem("N")
if str(row[1]) == "Y":
self.sub_options.setCurrentIndex(0)
else:
self.sub_options.setCurrentIndex(1)
layout = QtGui.QGridLayout()
layout.addWidget(full_control_label, 1, 1)
layout.addWidget(self.full_control_options, 1, 2)
layout.addWidget(tills_label, 2, 1)
layout.addWidget(self.tills_options, 2, 2)
self.setLayout(layout)
def update_database(self):
control = str(self.control_options.currentText())
sub = str(self.sub_options.currentText())
return (control, sub)
Ignore any errors I don't specify because I wrote this quickly just for an example. The problem I'm facing is: the save button is created within the loop in order to be used multiple times. It connects to a function that gets the current text of the combo-boxes. But the problem is, if you change the current text in the combo-boxes and then click Save, it gets the text from the combo-boxes, but not the right text - you always get the same result.
For example:
QComboBox1: Y
QComboBox2: N
If we click Save, it'll return ('Y', 'N')
If we change the combobox values:
QComboBox1: N
QComboBox2: N
and click save, it'll return ('Y', 'N')
But it never returns the new values from the combo-boxes.
How can I fix it so I get the updated values rather than the old values?
The save method doesn't currently make much sense. Surely you need to get the Perm widget from the table, not make a new one every time:
def save(self):
button = self.sender()
index = self.table.indexAt(button.pos())
row = index.row()
# get the new values from the table
data = self.table.cellWidget(row, 3).update_database()
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()