Why QTreeview Item class changed after drag & drop Event? [duplicate] - python

This question already has answers here:
How to drop a custom QStandardItem into a QListView
(1 answer)
Preserve QStandardItem subclasses in drag and drop
(1 answer)
Closed 1 year ago.
Run code below, when drag & drop an item and then click on the moved one, I expect a console message but receive an error message about :AttributeError: 'QStandardItem' object has no attribute 'say'
Why qt change the class of the custom Item?
import sys
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QMainWindow, QTreeView, QApplication
class DemoTreeView(QMainWindow):
class Item(QStandardItem):
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
self.original_text = self.text()
def say(self):
print(self.original_text)
class View(QTreeView):
def __init__(self,parent):
super().__init__(parent)
self.setDragDropMode(self.InternalMove)
self.clicked.connect(self.on_clicked_handle)
def on_clicked_handle(self,index):
item= self.model().itemFromIndex(index)
item.say()
def __init__(self, parent=None):
super(DemoTreeView, self).__init__(parent)
self.initUi()
def initUi(self):
self.model = QStandardItemModel(self)
self.model.setHorizontalHeaderLabels(['test'])
self.model.appendRow([self.Item("A")])
self.model.appendRow([self.Item("B")])
self.treeView = self.View(self)
self.treeView.setModel(self.model)
self.treeView.expandAll()
self.setCentralWidget(self.treeView)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = DemoTreeView()
window.show()
sys.exit(app.exec())

If you are going to subclass QStandardItem then you have to implement the clone() method and pass a custom item to the model through the setItemPrototype() method.
On the other hand, when an item is copied by drag-and-drop, what is done is creating an empty item and then adding the roles using QDataStream, something like:
# ds: QDataStream with the information of the roles of the original item
item = FooItem()
ds >> item
So original_text will have an empty text so a possible solution is to say() return the text.
import sys
from PyQt5.QtGui import QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import QMainWindow, QTreeView, QApplication
class Item(QStandardItem):
def say(self):
print(self.text())
def clone(self):
return Item()
class View(QTreeView):
def __init__(self, parent):
super().__init__(parent)
self.setDragDropMode(self.InternalMove)
self.clicked.connect(self.on_clicked_handle)
def on_clicked_handle(self, index):
item = self.model().itemFromIndex(index)
item.say()
class DemoTreeView(QMainWindow):
def __init__(self, parent=None):
super(DemoTreeView, self).__init__(parent)
self.initUi()
def initUi(self):
self.model = QStandardItemModel(self)
item = Item()
self.model.setItemPrototype(item)
self.model.setHorizontalHeaderLabels(["test"])
self.model.appendRow([Item("A")])
self.model.appendRow([Item("B")])
self.treeView = View(self)
self.treeView.setModel(self.model)
self.treeView.expandAll()
self.setCentralWidget(self.treeView)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = DemoTreeView()
window.show()
sys.exit(app.exec())

Related

Binding widget properties to Python variables

was trying to understand https://wiki.python.org/moin/PyQt/Binding%20widget%20properties%20to%20Python%20variables :
"Binding widget properties to Python variables"
down below my modified code that took a while to me, but kind of visualize better what bind, example code, here, does:
def bind(objectName, propertyName, type):
def getter(self):
return type(self.findChild(QObject, objectName).property(propertyName).toPyObject())
def setter(self, value):
self.findChild(QObject, objectName).setProperty(propertyName, QVariant(value))
return property(getter, setter)
my complete code is:
import sys
from PyQt5.QtWidgets import QWidget, QLineEdit, QTextEdit, QCheckBox, QFormLayout, QPushButton
from PyQt5 import QtWidgets
def bind(objectName, propertyName, type):
def getter(self):
return type(self.findChild(QObject, objectName).property(propertyName).toPyObject())
def setter(self, value):
self.findChild(QObject, objectName).setProperty(propertyName, QVariant(value))
return property(getter, setter)
class Window(QWidget):
def __init__(self, parent = None):
super().__init__(parent)
self.nameEdit = QLineEdit()
self.nameEdit.setObjectName("nameEdit")
self.addressEdit = QTextEdit()
self.addressEdit.setObjectName("addressEdit")
self.contactCheckBox = QCheckBox()
self.contactCheckBox.setObjectName("contactCheckBox")
self.button_1 = QPushButton('press me !!!')
self.button_1.clicked.connect(self.pushButton_1_Pressed)
self.button_2 = QPushButton('press me !!! second')
self.button_2.clicked.connect(self.pushButton_2_Pressed)
self.layout = QFormLayout(self)
self.layout.addRow(self.tr("Name:"), self.nameEdit)
self.layout.addRow(self.tr("Address:"), self.addressEdit)
self.layout.addRow(self.tr("Receive extra information:"), self.contactCheckBox)
self.layout.addWidget(self.button_1)
self.layout.addWidget(self.button_2)
self.setLayout(self.layout)
self.name = bind('nameEdit', 'text', str)
self.address = bind("addressEdit", "plainText", str)
self.contact = bind("contactCheckBox", "checked", bool)
def pushButton_1_Pressed(self):
print(self.nameEdit.text())
print(self.addressEdit.toPlainText())
print(self.contactCheckBox.isChecked())
def pushButton_2_Pressed(self):
self.nameEdit.setText('pippo')
self.addressEdit.clear()
self.addressEdit.insertPlainText('papero')
self.contactCheckBox.setChecked(True)
print(self.nameEdit.text())
print(self.addressEdit.toPlainText())
print(self.contactCheckBox.isChecked())
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())
after you insert text into a QLineEdit or QTextEdit or check a QCheckBox widget
you can print the variables defined by bind and pressing the second button you change the variable values and the text/values of the widgets at the same time (got some insight from Binding a PyQT/PySide widget to a local variable in Python.
since the inners of Python and PyQt5 are to hard to me right know, is about a dumb-proof descriprion about how bind works in the code on the three widgets.
The article you are referring to tries to implement the python properties using the QObjects properties.
Since it is a property it should not be declared within the class but at the method level as class attribute. On the other hand, the code must be updated since it is written for PyQt4 where the conversion between objects from python to Qt was not implicit, considering the above, the solution is:
import sys
from PyQt5.QtCore import QObject
from PyQt5.QtWidgets import (
QApplication,
QWidget,
QLineEdit,
QTextEdit,
QCheckBox,
QFormLayout,
QPushButton,
)
def bind(objectName, propertyName):
def getter(self):
return self.findChild(QObject, objectName).property(propertyName)
def setter(self, value):
self.findChild(QObject, objectName).setProperty(propertyName, value)
return property(getter, setter)
class Window(QWidget):
name = bind("nameEdit", "text")
address = bind("addressEdit", "plainText")
contact = bind("contactCheckBox", "checked")
def __init__(self, parent=None):
super().__init__(parent)
self.nameEdit = QLineEdit()
self.nameEdit.setObjectName("nameEdit")
self.addressEdit = QTextEdit()
self.addressEdit.setObjectName("addressEdit")
self.contactCheckBox = QCheckBox()
self.contactCheckBox.setObjectName("contactCheckBox")
self.button_1 = QPushButton("press me !!!")
self.button_1.clicked.connect(self.pushButton_1_Pressed)
self.button_2 = QPushButton("press me !!! second")
self.button_2.clicked.connect(self.pushButton_2_Pressed)
layout = QFormLayout(self)
layout.addRow(self.tr("Name:"), self.nameEdit)
layout.addRow(self.tr("Address:"), self.addressEdit)
layout.addRow(self.tr("Receive extra information:"), self.contactCheckBox)
layout.addWidget(self.button_1)
layout.addWidget(self.button_2)
def pushButton_1_Pressed(self):
print(self.name)
print(self.address)
print(self.contact)
def pushButton_2_Pressed(self):
self.name = "pippo"
self.address = ""
self.address += "papero"
self.contact = True
print(self.address)
print(self.contact)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

How to use eventfilter and installeventfilter methods?

I am using python 3.8 and qt5 for the desktop app.
I want to delete selected QTreeWidgetItem when the Delete key is pressed.
I tried the below code, but it did not work:
class EventFilter(QObject):
def __init__(self):
super().__init__()
pass
def eventFilter(self, a0: 'QObject', a1: 'QEvent') -> bool:
if isinstance(a1, QKeyEvent) and a1.matches(QKeySequence_StandardKey=QKeySequence.Delete):
print("event filter")
pass
return super().eventFilter(a0, a1)
treeWidget.installEventFilter(EventFilter())
# treeWidget.viewport().installEventFilter(EventFilter())
Some posts said to install on viewport, but that did not work either.
This is my app's structure:
QMainWindow
-- QTabWidget
---- QTreeWidget
Can you give me some hints on how to use eventfilter and installeventfilter, or any other recommended way?
Probably the cause of the problem is the scope of the EventFilter object since not being assigned to a variable it will be eliminated instantly. A possible solution is to assign a variable that has sufficient scope, another option is to pass it a parent since it is a QObject, in this case I will use the second.
import sys
from PyQt5.QtCore import QEvent, QObject, pyqtSignal
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QApplication, QMainWindow, QTabWidget, QTreeWidget
class EventFilter(QObject):
delete_pressed = pyqtSignal()
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
if obj is self.widget and event.type() == QEvent.KeyPress:
if event.matches(QKeySequence.Delete):
self.delete_pressed.emit()
return super().eventFilter(obj, event)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
tabWidget = QTabWidget()
self.setCentralWidget(tabWidget)
treeWidget = QTreeWidget()
tabWidget.addTab(treeWidget, "Tree")
eventFilter = EventFilter(treeWidget)
eventFilter.delete_pressed.connect(self.handle_delete_pressed)
def handle_delete_pressed(self):
print("delete pressed")
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
On the other hand, it is not neccesary to reinvent the wheel since there are already QShortcuts that serve to detect when the user presses key combinations
import sys
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QShortcut,
QTabWidget,
QTreeWidget,
)
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
tabWidget = QTabWidget()
self.setCentralWidget(tabWidget)
treeWidget = QTreeWidget()
tabWidget.addTab(treeWidget, "Tree")
shortcut = QShortcut(QKeySequence.Delete, treeWidget)
shortcut.activated.connect(self.handle_delete_pressed)
def handle_delete_pressed(self):
print("delete pressed")
def main():
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()

PyQT5 Data/View model representation with a complex widget

I'm trying to wrap my head around some ModelView on PyQT5.
I have a QListView, which can display data stored in an QAbstractListModel.
But i'd like to have each line of my QListView displaying a complex Widget created in QDesigner.
I've created a widget with a QLabel, a spacer, and a QPushButton.
I'd like to have each elements of my QListView using this widget to display my model data
Here is the basic code with a simple ModelView QListView
import typing
from PyQt5 import QtCore
from PyQt5.QtCore import QModelIndex, Qt
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QListView
class MyListModel(QtCore.QAbstractListModel):
def __init__(self, parent=None):
super().__init__(parent)
self.data_list = []
def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
if role == Qt.DisplayRole:
return self.data_list[index.row()]
def rowCount(self, parent: QModelIndex = ...) -> int:
return len(self.data_list)
data = ["emotion", "unliving", "brutally", "torch", "donut", "comet"]
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
layout = QWidget()
layout.setLayout(QVBoxLayout())
list_view = QListView()
model = MyListModel()
model.data_list = data
list_view.setModel(model)
layout.layout().addWidget(list_view)
layout.resize(640, 480)
layout.show()
sys.exit(app.exec_())
I've tried to create a QStyledItemDelegate, and set the delegate on the model, but i can't make it work.
class LineDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
self.line = Ui_Form()
self.line.setupUi(parent)
def paint(self, painter: QtGui.QPainter, option: 'QStyleOptionViewItem', index: QtCore.QModelIndex) -> None:
# Not sure what to put here
The doc seems to said that a Delegate should be used to edit data inside the view, not having a complex widget view.
https://doc.qt.io/qtforpython/overviews/model-view-programming.html#a-simple-delegate
Is there anything possible to have what i want ?
I'm really not sure how to approch this.
You must use a delegate so that you create an editor for each item, and then use the openPersistentEditor method to open the editors, it is not necessary to override the paint method.
import typing
from PyQt5.QtCore import QAbstractListModel, QModelIndex, Qt
from PyQt5.QtWidgets import (
QApplication,
QListView,
QStyledItemDelegate,
QVBoxLayout,
QWidget,
)
class Form(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_Form()
self.ui.setupUi(self)
class MyListModel(QAbstractListModel):
def __init__(self, parent=None):
super().__init__(parent)
self._data_list = []
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> typing.Any:
if role == Qt.DisplayRole:
return self._data_list[index.row()]
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
return len(self._data_list)
#property
def data_list(self):
return self._data_list
#data_list.setter
def data_list(self, data_list):
self.beginResetModel()
self._data_list = data_list.copy()
self.endResetModel()
class Delegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
return Form(parent)
"""def setEditorData(self, editor, index):
editor.label.setText(index.data())"""
data = ["emotion", "unliving", "brutally", "torch", "donut", "comet"]
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
list_view = QListView()
model = MyListModel()
model.data_list = data
list_view.setModel(model)
delegate = Delegate(list_view)
list_view.setItemDelegate(delegate)
for i in range(model.rowCount()):
index = model.index(i, 0)
list_view.openPersistentEditor(index)
container = QWidget()
layout = QVBoxLayout(container)
layout.addWidget(list_view)
container.resize(640, 480)
container.show()
sys.exit(app.exec_())

PyQt5 "Ghost" of QIcon appears in QLineEdit of a QComboBox

I have a QComboBox and I want each item to have its own QIcon, but the QIcon should only be visible in the dropdown. I took an answer from a previous question and it works pretty well:
As you can see, the Icon only appears in the dropdown. The problem arises when I set the QComboBox to be editable and this happens:
Here a "ghost" of the QIcon is still there which in turn displaces the text.
My question is: What causes this and how can I remove this "ghost" so that the text appears normally?
My code:
from PyQt5.QtWidgets import (
QApplication,
QComboBox,
QHBoxLayout,
QStyle,
QStyleOptionComboBox,
QStylePainter,
QWidget,
)
from PyQt5.QtGui import QIcon, QPalette
from PyQt5.QtCore import QSize
class EditCombo(QComboBox):
def __init__(self, parent=None):
super(EditCombo, self).__init__(parent)
self.editable_index = 99
self.currentIndexChanged.connect(self.set_editable)
def setEditableAfterIndex(self, index):
self.editable_index = index
def set_editable(self, index: int) -> None:
if index >= self.editable_index:
self.setEditable(True)
else:
self.setEditable(False)
self.update()
def paintEvent(self, event):
painter = QStylePainter(self)
painter.setPen(self.palette().color(QPalette.Text))
opt = QStyleOptionComboBox()
self.initStyleOption(opt)
opt.currentIcon = QIcon()
opt.iconSize = QSize()
painter.drawComplexControl(QStyle.CC_ComboBox, opt)
painter.drawControl(QStyle.CE_ComboBoxLabel, opt)
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
hbox = QHBoxLayout()
edit_ico = QIcon("edit.png")
empty_ico = QIcon("empty.png") # For margin
combo = EditCombo(self)
combo.setEditableAfterIndex(2)
combo.addItem(empty_ico, "Foo 1")
combo.addItem(edit_ico, "Foo 2")
combo.addItem(edit_ico, "Bar 1")
combo.addItem(edit_ico, "Bar 2")
hbox.addWidget(combo)
self.setLayout(hbox)
self.show()
def main():
import sys
app = QApplication(sys.argv)
ex = Example()
ex.setFixedWidth(300)
sys.exit(app.exec_())
if __name__ == "__main__":
main()
QComboBox also uses the icon to set the position of the QLineEdit that is used when the QComboBox is editable so you see that displacement, if you don't want to observe that then you have to recalculate the geometry. The following code does it through a QProxyStyle:
class ProxyStyle(QProxyStyle):
def subControlRect(self, control, option, subControl, widget=None):
r = super().subControlRect(control, option, subControl, widget)
if control == QStyle.CC_ComboBox and subControl == QStyle.SC_ComboBoxEditField:
if widget.isEditable():
widget.lineEdit().setGeometry(r)
return r
class EditCombo(QComboBox):
def __init__(self, parent=None):
super(EditCombo, self).__init__(parent)
self._style = ProxyStyle(self.style())
self.setStyle(self._style)
self.editable_index = 99
self.currentIndexChanged.connect(self.set_editable)
# ...

Add more than one Qmenu from other classes in MainWindow

I want a single menubar in my main window and be able to set the menus in the menubar from additional classes. Using the setMenuWidget command will overwrite the first menu option as shown in the code. In the classes where I set up the menu I think I may need to just set up a menu rather than a menubar, then set up the menubar in the main window.
This is what I would l like, which can be achieved by populating a single menubar in a class, though I am trying to avoid this method.
Instead only the second menu is show
import sys
from PyQt5.QtWidgets import QAction, QApplication, QMainWindow
from PyQt5 import QtCore, QtGui, QtWidgets
class ToolBar0(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self)
bar = self.menuBar() # don't think I need a menubar here
file_menu = bar.addMenu('menu1')
one = QAction('one', self)
two = QAction('two', self)
file_menu.addAction(one)
file_menu.addAction(two)
class ToolBar1(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self)
bar = self.menuBar() # don't think I need a menubar here
file_menu = bar.addMenu('menu2')
one = QAction('one', self)
two = QAction('two', self)
file_menu.addAction(one)
file_menu.addAction(two)
class MainWindow(QMainWindow):
def __init__(self):
QMainWindow.__init__(self, parent=None)
#should a menubar be set up here?
#For seting widgets in main window
self.Tool_Bar0 = ToolBar0(self)
self.setMenuWidget(self.Tool_Bar0)
###menu_bar0 is over written
self.Tool_Bar1 = ToolBar1(self)
#self.setMenuWidget(self.Tool_Bar1)
if __name__ == '__main__':
app = QApplication(sys.argv)
# creating main window
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
You could use a base class with a method to return either a list of QMenu items containing QAction items or a list of QAction items and then render them in your QMainWindow toolbar in whichever way you want, here is an example:
import sys
from PyQt5.QtWidgets import QAction, QApplication, QMainWindow, QMenu
class WindowWithToolbar:
def __init__(self):
super().__init__()
def menu_items(self)->list:
pass
class Window1(WindowWithToolbar, QMainWindow):
def __init__(self):
WindowWithToolbar.__init__(self)
QMainWindow.__init__(self)
# New menu with actions
self.menu = QMenu('one')
self.menu.addActions([QAction('two', self), QAction('three', self)])
def menu_items(self):
return [self.menu]
class Window2(WindowWithToolbar, QMainWindow):
def __init__(self):
WindowWithToolbar.__init__(self)
QMainWindow.__init__(self)
def menu_items(self):
# Only actions
return [QAction('three', self), QAction('four', self)]
class MainWindow(WindowWithToolbar, QMainWindow):
def __init__(self):
QMainWindow.__init__(self, parent=None)
self.window1 = Window1()
self.window2 = Window2()
self.menu = QMenu('File')
self.helloAction = QAction('Hello')
self.menu.addAction(self.helloAction)
self._build_menu()
def menu_items(self)->list:
return [self.menu]
def _build_menu(self):
self._add_menu_items(self)
self._add_menu_items(self.window1)
self._add_menu_items(self.window2)
def _add_menu_items(self, windowWithToolbar: WindowWithToolbar):
for menu_item in windowWithToolbar.menu_items():
if isinstance(menu_item, QMenu):
self.menuBar().addMenu(menu_item)
elif isinstance(menu_item, QAction):
self.menuBar().addAction(menu_item)
if __name__ == '__main__':
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())

Categories