Binding widget properties to Python variables - python

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_())

Related

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()

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

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())

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_())

Update a QDockWidget's subwidgets which already happens naturally upon hovering over with mouse

I have a QDockWidget in a QMainWindow, the QDockWidget contains a stack of QUndoViews containing commands that I want to change the text of in the QUndoView. You accomplish this by doing command.setText(). However, the QUndoView is only showing the updated text when I hover over it with the mouse. Not even will calling the below right after setText work:
self.editor().undoView().setFocus()
self.editor().undoView().update()
self.editor().undoView().hide()
self.editor().undoView().show()
Here is the minimal code to demonstrate the problem:
from PyQt5.QtWidgets import (QMainWindow, QDockWidget, QPushButton, QLabel, \
QStackedWidget, QUndoStack, QUndoCommand, QApplication, \
QUndoView)
import sys
from PyQt5.QtCore import QObject, pyqtSignal, Qt
class Command(QUndoCommand):
def __init__(self):
super().__init__()
def undo(self):
print("Command Undone")
def redo(self):
print("Command Redone")
class CommandTimeline(QDockWidget):
def __init__(self):
super().__init__()
self.stackedWidget = QStackedWidget()
self.setWidget(self.stackedWidget)
def addUndoView(self, stack):
view = QUndoView(stack)
self.stackedWidget.addWidget(view)
return view
class Obj(QObject):
nameChanged = pyqtSignal(str)
def __init__(self, name):
super().__init__()
self._name = name
def setName(self, name):
self._name = name
self.nameChanged.emit(name)
def __str__(self):
return self._name
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.undoStack = QUndoStack()
self.commandTimeline = CommandTimeline()
self.commandTimeline.addUndoView(self.undoStack)
self.addDockWidget(Qt.LeftDockWidgetArea, self.commandTimeline)
button = QPushButton("Click")
self.setCentralWidget(button)
button.clicked.connect(self.changeObjName)
self.obj = Obj("A")
self.addCommand()
def addCommand(self):
def getText(obj):
return "The command text: " + str(obj)
command = Command()
command.setText(getText(self.obj))
self.obj.nameChanged.connect(lambda name: command.setText(getText(self.obj)))
self.undoStack.push(command)
def changeObjName(self):
self.obj.setName("B")
if __name__ == "__main__":
app = QApplication([])
window = MainWindow()
window.show()
sys.exit(app.exec_())
Run this code and notice that after clicking the button, the text in the undoview doesn't change, but after hovering over the dockwidget, the text updates immediately.
How can I trigger the update upon command.setText()?
QUndoCommand does not notify if the text is changed, it is designed to have a static text. So we must do that task, in this case use setFocus(), you signal that it does not work, but for me it works so you probably have not implemented it correctly or your MCVE is not verifiable.
import sys
from functools import partial
from PyQt5.QtWidgets import (QMainWindow, QDockWidget, QPushButton, QLabel, \
QStackedWidget, QUndoStack, QUndoCommand, QApplication, \
QUndoView)
from PyQt5.QtCore import QObject, pyqtSignal, Qt
class Command(QUndoCommand):
def undo(self):
print("Command Undone")
def redo(self):
print("Command Redone")
class CommandTimeline(QDockWidget):
def __init__(self):
super().__init__()
self.stackedWidget = QStackedWidget()
self.setWidget(self.stackedWidget)
def addUndoView(self, stack):
view = QUndoView(stack)
self.stackedWidget.addWidget(view)
return view
class Obj(QObject):
nameChanged = pyqtSignal(str)
def __init__(self, name):
super().__init__()
self._name = name
def setName(self, name):
self._name = name
self.nameChanged.emit(name)
def __str__(self):
return self._name
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.undoStack = QUndoStack()
self.commandTimeline = CommandTimeline()
self.view = self.commandTimeline.addUndoView(self.undoStack)
self.addDockWidget(Qt.LeftDockWidgetArea, self.commandTimeline)
button = QPushButton("Click")
self.setCentralWidget(button)
button.clicked.connect(self.changeObjName)
self.obj = Obj("A")
self.addCommand()
def addCommand(self):
command = Command()
command.setText("The command text: {}".format(self.obj))
self.obj.nameChanged.connect(partial(self.onNameChanged, command))
self.undoStack.push(command)
def changeObjName(self):
self.obj.setName("B")
def onNameChanged(self, command, name):
fw = QApplication.focusWidget()
command.setText("The command text: {}".format(name))
self.view.setFocus()
fw.setFocus()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())

Categories