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.
Related
from PyQt5.QtGui import *
from PyQt5.QtWidgets import*
from PyQt5.QtCore import *
import operator
from Calculator import Ui_MainWindow
# Calculator state.
READY = 0
INPUT = 1
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self),__init__(*args, **kwargs)
self.setupUi(self)
# setup numbers
for n in range(0, 10):
getattr(self, 'pushButton_nks' % n).pressed.connect(lambda v=n: self.input_number(v))
# setup operation
self.pushButton_add.pressed.connect(lambda: self.operation(operator.add))
self.pushButton_sub.pressed.connect(lambda: self.operation(operator.sub))
self.pushButton_mul.pressed.connect(lambda: self.operation(operator.mul))
self.pushButton_div.pressed.connect(lambda: self.operation(operator.truediv))
self.pushButton_pc.pressed.connect(self.operation_pc)
self.pushButton_eq.pressed.connect(self.equals)
# setup actions
self.actionReset.triggered.connect(self.reset)
self.pushButton_ac.pressed.connect(self.reset)
self.actionExit.triggered.connect(self.close)
self.pushButton_m.pressed.connect(self.memory_store)
self.pushButton_mr.pressed.connect(self.memory_recall)
self.memory = 0
self.reset()
self.show()
def display(self):
self.lodNumber.display(self.stack[-1])
def reset(self):
self.state = READY
self.stack = [0]
self.last_operation = None
self.current_op = None
self.display()
def memory_store(self):
self.memory = self.lodNumber.value()
def memory_recall(self):
self.state = INPUT
self.stack[-1] = self.memory
self.display()
def input_number(self, v):
if self.state == READY:
self.state = INPUT
self.stack[-1] = v
else:
self.stack[-1] = self.stack[-1] * 10 + v
self.display()
def operation(self, op):
if self.current_op: #complete the current operation
self.equals()
self.stack.append(0)
self.state = INPUT
self.current_op = op
def operation_pc(self):
self.state = INPUT
self.stack[-1] *= 0.01
self.display()
def equals(self):
#support to allow '=' tp repeat previous operation
#if no futher input has been added.
if self.state == READY and self.last_operation:
s, self.current_op = self.last_operation
self.stack.append(s)
if self.current_op:
self.last_operation = self.stack[-1], self.current_op
try:
self.stack = [self.current_op(*self.stack)]
except Exception:
self.lodNumber.display('Err')
self.stack = [0]
else:
self.current_op = None
self.state = READY
self.display()
if __name__ == '__main__':
app = QApplication([])
app.setApplicationName("calculon")
window = MainWindow()
app.exec_()
when I run this, I come across a problem that I can't solve on my own, I hope to find the answer here
Traceback (most recent call last):
File "C:/Python35/Lib/site-packages/PyQt5/qt ddesain/callKalkulator.py", line 109, in
window = MainWindow()
File "C:/Python35/Lib/site-packages/PyQt5/qt ddesain/callKalkulator.py", line 15, in init
super(MainWindow, self),init(*args, **kwargs)
NameError: name 'init' is not defined
i got this kind of problem with my code like this, is there any way to solve it
It's a clear error in syntax for calling the constructor of parent class:
super(MainWindow, self).__init__(*args, **kwargs)
Note than in python3, you can simply use super().__init__(*args, **kwargs).
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 am developing an application with PyQt5 5.7.1 on Python 3.5. I use a QTreeView to display some data with a hierachy. I would like to be able to filter this data, so I used a QSortFilterProxy model.
self.model = CustomTreeModel(data)
self.proxy = QSortFilterProxyModel(self.model)
self.ui.treeView.setModel(self.proxy)
This gives a strange behaviour:
By default, the treeView is collapsed. If I expand any item which is not the first one, and then click on any of its children, the first item of the treeView gets expanded (it shouldn't). Actually, whenever I click on any item which is not first level (a child of the root item), the first child of the root item gets expanded.
I thought there was an issue with my model, but if I don't use QSortFilterProxy model, like this :
self.model = CustomTreeModel(data)
self.ui.treeView.setModel(self.model)
The treeView behaves as expected, the first element doesn't expand when it shouldn't.
It seems to me that this is a bug in PyQt5, but I didn't find any information about it on the Internet. On the contrary, there are some examples of using QSortFilterProxyModel with a QTreeView and nobody seems to report such issue. So is it really a bug in PyQt or am I missing something ?
Here is a demo code:
from PyQt5.QtCore import QAbstractItemModel, QModelIndex, Qt
class CustomTreeModel(QAbstractItemModel):
# Based on http://trevorius.com/scrapbook/uncategorized/pyqt-custom-abstractitemmodel/
def __init__(self, headerData, nodes):
QAbstractItemModel.__init__(self)
self._headerData = headerData
self._root = CustomNode(None)
for node in nodes:
self._root.addChild(node)
def addChild(self, in_node, in_parent=QModelIndex()):
if not in_parent or not in_parent.isValid():
parent = self._root
else:
parent = in_parent.internalPointer()
parent.addChild(in_node)
def index(self, in_row, in_column, in_parent=QModelIndex()):
if not in_parent.isValid():
parent = self._root
else:
parent = in_parent.internalPointer()
if not QAbstractItemModel.hasIndex(self, in_row, in_column, in_parent):
return QModelIndex()
child = parent.child(in_row)
if child:
return QAbstractItemModel.createIndex(self, in_row, in_column, child)
else:
return QModelIndex()
def rowCount(self, in_index=QModelIndex()):
if in_index.isValid():
return in_index.internalPointer().childCount()
return self._root.childCount()
def child(self, row, index):
if index.isValid():
c = index.internalPointer().child(row)
return QAbstractItemModel.createIndex(self, row, 0, c)
return QModelIndex()
def parent(self, in_index):
if in_index.isValid():
p = in_index.internalPointer().parent()
if p:
return QAbstractItemModel.createIndex(self, p.row(),0,p)
return QModelIndex()
def columnCount(self, in_index):
if in_index.isValid():
return in_index.internalPointer().columnCount()
return self._root.columnCount()
def data(self, in_index, role):
if not in_index.isValid():
return None
node = in_index.internalPointer()
if role == Qt.DisplayRole:
return node.data(in_index.column())
return None
def headerData(self, section, orientation, role=Qt.DisplayRole):
if (section < 0) or (section > self.columnCount(QModelIndex())):
return None
if (orientation == Qt.Horizontal) and (role == Qt.DisplayRole):
return self._headerData[section]
return None
class CustomNode(object):
def __init__(self, in_data):
self._data = in_data
if type(in_data) == tuple:
self._data = list(in_data)
if type(in_data) == str or not hasattr(in_data, '__getitem__'):
self._data = [in_data]
self._columncount = len(self._data)
self._children = []
self._parent = None
self._row = 0
def data(self, in_column):
if in_column >= 0 and in_column < len(self._data):
return self._data[in_column]
return None
def columnCount(self):
return self._columncount
def childCount(self):
return len(self._children)
def child(self, in_row):
if in_row >= 0 and in_row < self.childCount():
return self._children[in_row]
return None
def parent(self):
return self._parent
def row(self):
return self._row
def addChild(self, in_child):
in_child._parent = self
in_child._row = len(self._children)
self._children.append(in_child)
self._columncount = max(in_child.columnCount(), self._columncount)
if __name__ == '__main__':
import sys
from PyQt5.QtCore import QSortFilterProxyModel
from PyQt5.QtWidgets import QApplication, QTreeView
app = QApplication(sys.argv)
header = ['Food']
fruits = CustomNode(['Fruit'])
fruits.addChild(CustomNode(['Apple']))
fruits.addChild(CustomNode(['Banana']))
fruits.addChild(CustomNode(['Orange']))
vegetables = CustomNode(['Vegetables'])
vegetables.addChild(CustomNode(['Carott']))
vegetables.addChild(CustomNode(['Potato']))
meat = CustomNode(['Meat'])
meat.addChild(CustomNode(['Beef']))
meat.addChild(CustomNode(['Chicken']))
meat.addChild(CustomNode(['Pork']))
nodes = [fruits, vegetables, meat]
v = QTreeView()
model = CustomTreeModel(header, nodes)
proxy = QSortFilterProxyModel()
proxy.setSourceModel(model)
v.setModel(proxy)
v.show()
sys.exit(app.exec_())
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()
I make a UI with PyQt4. It has a treeView and I want to deal with it.
The treeView is made up with model-base. I create a data in .py file and import it.
So, I can see the data tree in my treeView.
But I can't drag and drop it, so can't change the order.
I referred some articles so add it in my script, but they couldn't work.
I plant some "print", so I chased my problem.
I found that when drag a item, it transferred to MIME data.
But when it is dropped, I can't find any outputs.
It seems that the script doesn't call "dropMimeData" method.
How can I fix my script?
from PyQt4 import QtCore, QtGui
from setting import *
from copy import deepcopy
from cPickle import dumps, load, loads
from cStringIO import StringIO
class PyMimeData(QtCore.QMimeData):
MIME_TYPE = QtCore.QString('text/plain')
def __init__(self, data=None):
QtCore.QMimeData.__init__(self)
self._local_instance = data
if data is not None:
try:
pdata = dumps(data)
except:
return
self.setData(self.MIME_TYPE, dumps(data.__class__) + pdata)
#classmethod
def coerce(cls, md):
if isinstance(md, cls):
return md
if not md.hasFormat(cls.MIME_TYPE):
return None
nmd = cls()
nmd.setData(cls.MIME_TYPE, md.data())
return nmd
def instance(self):
if self._local_instance is not None:
return self._local_instance
io = StringIO(str(self.data(self.MIME_TYPE)))
try:
load(io)
return load(io)
except:
pass
return None
def instanceType(self):
if self._local_instance is not None:
return self._local_instance.__class__
try:
return loads(str(self.data(self.MIME_TYPE)))
except:
pass
return None
class treeItem(QtGui.QStandardItem):
def __init__(self, data, parent=None):
super(treeItem, self).__init__(data)
self.parentItem = parent
self.itemData = data
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def parent(self):
return self.parentItem
def childAtRow(self, row):
return self.childItems[row]
def rowOfChild(self, child):
for i, item in enumerate(self.childItems):
if item == child:
return i
return -1
class treeModel(QtGui.QStandardItemModel):
def __init__(self, name, parent=None):
super(treeModel, self).__init__(parent)
self.headerName = name
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def removeRowAll(self):
pass
def addItemList(self, parent, elements):
for text, children in elements:
item = treeItem(text, parent)
self.addItems(parent, item)
if children:
self.addItemList(item, children)
def addItems(self, parent, inputItem):
parent.appendRow(inputItem)
parent.appendChild(inputItem)
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.headerName
def supportedDropActions(self):
return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction
def flags(self, index):
defaultFlags = QtCore.QAbstractItemModel.flags(self, index)
if index.isValid():
return QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsDropEnabled | defaultFlags
else:
return QtCore.Qt.ItemIsDropEnabled | defaultFlags
def mimeTypes(self):
types = QtCore.QStringList()
types.append('text/plain')
return types
def mimeData(self, index):
node = self.nodeFromIndex(index[0])
mimeData = PyMimeData(node)
return mimeData
def dropMimeData(self, mimedata, action, row, column, parentIndex):
print mimedata, action, row, column, parentIndex
if action == QtCore.Qt.IgnoreAction:
return True
dragNode = mimedata.instance()
print dragNode
parentNode = self.nodeFromIndex(parentIndex)
# copy of node being moved
newNode = deepcopy(dragNode)
print newNode
newNode.setParent(parentNode)
self.insertRow(len(parentNode)-1, parentIndex)
self.emit(QtCore.SIGNAL("dataChanged(QtCore.QModelIndex,QtCore.QModelIndex)"), parentIndex, parentIndex)
return True
def nodeFromIndex(self, index):
##return index.internalPointer() if index.isValid() else self.root
return index.model() if index.isValid() else self.parent()
def insertRow(self, row, parent):
return self.insertRows(row, 1, parent)
def insertRows(self, row, count, parent):
self.beginInsertRows(parent, row, (row + (count - 1)))
self.endInsertRows()
return True
def removeRow(self, row, parentIndex):
return self.removeRows(row, 1, parentIndex)
def removeRows(self, row, count, parentIndex):
self.beginRemoveRows(parentIndex, row, row)
node = self.nodeFromIndex(parentIndex)
node.removeChild(row)
self.endRemoveRows()
return True
added script
here is ui creation (the script above is imported in this script)
class RigControlWindow(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self, parent = getMayaWindow()):
super(RigControlWindow, self).__init__(parent)
self.setupUi(self)
self.bodyrig_treelist.setDragEnabled(1)
self.bodyrig_treelist.setAcceptDrops(1)
self.bodyrig_treelist.setDropIndicatorShown(1)
self.bodyrig_treelist.setDragDropMode(QtGui.QAbstractItemView.InternalMove)
QtCore.QObject.connect(self.finalize_button, QtCore.SIGNAL("clicked()"), self.AddData_treeList)
def AddData_treeList(self):
self.localtreeModel = treeModel("objects")
self.bodyrig_treelist.setModel(self.localtreeModel)
self.localtreeModel.addItemList(self.localtreeModel, data)
and data is
data = [("root",[("upper",[("hand",[]),
("head",[])
]),
("lower",[("leg",[]),
("foot",[])
])
])
]
The QTreeView.dragMoveEvent and QTreeView.dragEnterEvent methods both check the object returned by event.mimeData() to see if it can return data for any of the formats supported by the model (i.e. those returned by model.mimeTypes()).
But your PyMimeData subclass doesn't support any formats, because it never successfully sets the data passed to its constructor.
The problem is located in PyMimeData.__init__:
...
try:
pdata = dumps(data)
except:
return
self.setData(self.MIME_TYPE, dumps(data.__class__) + pdata)
The data is passed in from the treeModel.mimeData method:
def mimeData(self, index):
node = self.nodeFromIndex(index[0])
mimeData = PyMimeData(node)
return mimeData
But if you check the type of data/node you'll see that it is a treeModel instance, and so dumps(data) will fail because data can't be pickled. As a result of this, the PyMimeData object is not initialized properly, and so it is ignored by drag events.