subclassing QGroupBox so that it can be member of QButtonGroup - python

QButtonGroups can have checkboxes. But you cannot add them to a QButtonGroup because they do not inherit QAbstractButton.
It would be really nice for some UIs to be able to have a few QGroupBoxes with exclusive checkboxes. That is, you check one and the other QGroupBoxes are automatically unchecked.
In an ideal world, I could do something like this:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QGroupBox, QWidget, QApplication,
QAbstractButton, QButtonGroup)
class SuperGroup(QGroupBox, QAbstractButton):
def __init__(self, title, parent=None):
super(SuperGroup, self).__init__(title, parent)
self.setCheckable(True)
self.setChecked(False)
class Example(QWidget):
def __init__(self):
super().__init__()
sg1 = SuperGroup(title = 'Super Group 1', parent = self)
sg1.resize(200,200)
sg1.move(20,20)
sg2 = SuperGroup(title = 'Super Group 2', parent = self)
sg2.resize(200,200)
sg2.move(300,20)
self.bgrp = QButtonGroup()
self.bgrp.addButton(sg1)
self.bgrp.addButton(sg2)
self.setGeometry(300, 300, 650, 500)
self.setWindowTitle('SuperGroups!')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
This code fails as soon as you try to add a SuperGroup to the button group. PyQt5 explicitly does not support multiple inheritance. But there are some examples out in the wild, like from this blog.
In this simple example, it would be easy to manage the clicks programmatically. But as you add more group boxes, it gets more messy. Or what if you want a QButtonGroup with buttons, check boxes, and group boxes? Ugh.

It is not necessary to create a class that inherits from QGroupBox and QAbstractButton (plus it is not possible in pyqt or Qt/C++). The solution is to create a QObject that handles the states of the other QGroupBox when any QGroupBox is checked, and I implemented that for an old answer for Qt/C++ so this answer is just a translation:
import sys
from PyQt5.QtCore import pyqtSlot, QObject, Qt
from PyQt5.QtWidgets import QGroupBox, QWidget, QApplication, QButtonGroup
class GroupBoxManager(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self._groups = []
#property
def groups(self):
return self._groups
def add_group(self, group):
if isinstance(group, QGroupBox):
group.toggled.connect(self.on_toggled)
self.groups.append(group)
#pyqtSlot(bool)
def on_toggled(self, state):
group = self.sender()
if state:
for g in self.groups:
if g != group and g.isChecked():
g.blockSignals(True)
g.setChecked(False)
g.blockSignals(False)
else:
group.blockSignals(True)
group.setChecked(False)
group.blockSignals(False)
class Example(QWidget):
def __init__(self):
super().__init__()
sg1 = QGroupBox(
title="Super Group 1", parent=self, checkable=True, checked=False
)
sg1.resize(200, 200)
sg1.move(20, 20)
sg2 = QGroupBox(
title="Super Group 2", parent=self, checkable=True, checked=False
)
sg2.resize(200, 200)
sg2.move(300, 20)
self.bgrp = GroupBoxManager()
self.bgrp.add_group(sg1)
self.bgrp.add_group(sg2)
self.setGeometry(300, 300, 650, 500)
self.setWindowTitle("SuperGroups!")
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())

Related

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)
# ...

Enable/Disable QTreeWidget from updating

Is it possible to enable/disable a QTreeWidget from updating?
I want to add rows to my tree and update it manually with a button. Obviously when i add a row to the TreeWidget it will be shown in the table. Is there a way to disable this so i can add like 100 rows and than update it once? If not is there a solution with a TreeView?
import sys
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QApplication, QMainWindow, QToolBar, QAction, QTreeWidget, QTreeWidgetItem
class Table(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.resize(800, 600)
self.setMinimumSize(QSize(800, 600))
self.table = QTreeWidget()
self.table.setHeaderLabels(["Description", "Price"])
self.setCentralWidget(self.table)
self.car = QTreeWidgetItem(["Car"])
self.house = QTreeWidgetItem(["House"])
self.table.addTopLevelItem(self.car)
self.table.addTopLevelItem(self.house)
toolbar = QToolBar("Toolbar")
carAction = QAction("Car", self)
carAction.triggered.connect(self.addToCar)
houseAction = QAction("House", self)
houseAction.triggered.connect(self.addToHouse)
updateAction = QAction("Update", self)
updateAction.triggered.connect(self.update)
toolbar.addAction(carAction)
toolbar.addAction(houseAction)
toolbar.addAction(updateAction)
self.addToolBar(toolbar)
def addToCar(self):
child = QTreeWidgetItem(["Audi", str(25000)])
self.car.addChild(child)
def addToHouse(self):
child = QTreeWidgetItem(["Villa", str(500000)])
self.house.addChild(child)
def update(self):
pass
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Table()
win.show()
sys.exit( app.exec_() )
I do not know of a way to "suspend" tree updates, but what about using lists to store the children until one decides to update? See the modified example below (modifications are commented):
import sys
from PyQt5.QtCore import QSize
from PyQt5.QtWidgets import QApplication, QMainWindow, QToolBar, QAction, QTreeWidget, QTreeWidgetItem
class Table(QMainWindow):
def __init__(self):
QMainWindow.__init__(self)
self.resize(800, 600)
self.setMinimumSize(QSize(800, 600))
self.table = QTreeWidget()
self.table.setHeaderLabels(["Description", "Price"])
self.setCentralWidget(self.table)
self.car = QTreeWidgetItem(["Car"])
self.house = QTreeWidgetItem(["House"])
self.table.addTopLevelItem(self.car)
self.table.addTopLevelItem(self.house)
# initialize empty lists which will keep track of generated children
self.car_list, self.house_list = [], []
toolbar = QToolBar("Toolbar")
carAction = QAction("Car", self)
carAction.triggered.connect(self.addToCar)
houseAction = QAction("House", self)
houseAction.triggered.connect(self.addToHouse)
updateAction = QAction("Update", self)
updateAction.triggered.connect(self.update)
toolbar.addAction(carAction)
toolbar.addAction(houseAction)
toolbar.addAction(updateAction)
self.addToolBar(toolbar)
def addToCar(self):
child = QTreeWidgetItem(["Audi", str(25000)])
# instead of adding them directly to the tree, keep them in the list
self.car_list.append(child)
def addToHouse(self):
child = QTreeWidgetItem(["Villa", str(500000)])
# instead of adding them directly to the tree, keep them in the list
self.house.addChild(child)
def update(self):
# update the tree and reset the lists
self.car.addChildren(self.car_list)
self.house.addChildren(self.house_list)
self.car_list, self.house_list = [], []
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Table()
win.show()
sys.exit( app.exec_() )

How to receive hover events for child widgets?

I have a QWidget containing another (child) widget for which I'd like to process hoverEnterEvent and hoverLeaveEvent. The documentation mentions that
Mouse events occur when a mouse cursor is moved into, out of, or within a widget, and if the widget has the Qt::WA_Hover attribute.
So I tried to receive the hover events by setting this attribute and implementing the corresponding event handlers:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
class TestWidget(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
layout.addWidget(TestLabel('Test 1'))
layout.addWidget(TestLabel('Test 2'))
self.setLayout(layout)
self.setAttribute(Qt.WA_Hover)
class TestLabel(QLabel):
def __init__(self, text):
super().__init__(text)
self.setAttribute(Qt.WA_Hover)
def hoverEnterEvent(self, event): # this is never invoked
print(f'{self.text()} hover enter')
def hoverLeaveEvent(self, event): # this is never invoked
print(f'{self.text()} hover leave')
def mousePressEvent(self, event):
print(f'{self.text()} mouse press')
app = QApplication([])
window = TestWidget()
window.show()
sys.exit(app.exec_())
However it doesn't seem to work, no hover events are received. The mousePressEvent on the other hand does work.
In addition I tried also the following things:
Set self.setMouseTracking(True) for all widgets,
Wrap the TestWidget in a QMainWindow (though that's not what I want to do for the real application),
Implement event handlers on parent widgets and event.accept() (though as I understand it, events propagate from inside out, so this shouldn't be required).
How can I receive hover events on my custom QWidgets?
The QWidget like the QLabel do not have the hoverEnterEvent and hoverLeaveEvent methods, those methods are from the QGraphicsItem so your code doesn't work.
If you want to listen to the hover events of the type you must override the event() method:
import sys
from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout
class TestWidget(QWidget):
def __init__(self):
super().__init__()
layout = QVBoxLayout(self)
layout.addWidget(TestLabel("Test 1"))
layout.addWidget(TestLabel("Test 2"))
class TestLabel(QLabel):
def __init__(self, text):
super().__init__(text)
self.setAttribute(Qt.WA_Hover)
def event(self, event):
if event.type() == QEvent.HoverEnter:
print("enter")
elif event.type() == QEvent.HoverLeave:
print("leave")
return super().event(event)
def main():
app = QApplication(sys.argv)
window = TestWidget()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Did you know that you can do this with QWidget's enterEvent and leaveEvent? All you need to do is change the method names. You won't even need to set the Hover attribute on the label.
from PyQt5.QtWidgets import QApplication, QGridLayout, QLabel, QWidget
class Window(QWidget):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
layout = QGridLayout()
self.label = MyLabel(self)
layout.addWidget(self.label)
self.setLayout(layout)
text = "hover label"
self.label.setText(text)
class MyLabel(QLabel):
def __init__(self, parent=None):
super(MyLabel, self).__init__(parent)
self.setParent(parent)
def enterEvent(self, event):
self.prev_text = self.text()
self.setText('hovering')
def leaveEvent(self, event):
self.setText(self.prev_text)
if __name__ == "__main__":
app = QApplication([])
w = Window()
w.show()
app.exit(app.exec_())

Pyqt5 draw a line between two widgets

I am trying to use QPainter to draw a line between two widgets. If I use a simple function inside the first class it works. But, I want to create a separate class of a QPainter event, that I can call at the first class whenever I want. But, it is not working as expected. Can you help me to figure out why the QPainter class is not adding a line.
import sys
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
class Example(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.okButton = QPushButton("OK")
self.cancelButton = QPushButton("Cancel")
l1 = self.okButton.pos()
l2 = self.cancelButton.pos()
# This is to call the class to draw a line between those two widgets
a = QPaint(l1.x(), l1.y(), l2.x(), l2.y(),parent=self)
vbox = QVBoxLayout()
vbox.addWidget(self.okButton)
vbox.addWidget(self.cancelButton)
self.setLayout(vbox)
self.setGeometry(300, 300, 300, 150)
self.setWindowTitle('Buttons')
self.show()
class QPaint(QPainter):
def __init__(self, x1, y1, x2, y2, parent=None):
super().__init__()
def paintEvent(self, event):
self.setPen(Qt.red)
self.drawLine(x1,y1,x2,y2)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
Widgets can only be painted in the widget's paintEvent method, so if you don't want to paint it in the same class then you can use multiple inheritance. On the other hand, the initial positions you use to paint will be the positions before showing that they are 0 making no line is painted but a point so it is better to track the positions using an event filter.
import sys
from PyQt5.QtCore import QEvent
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QApplication, QPushButton, QVBoxLayout, QWidget
class Drawer:
def paintEvent(self, event):
painter = QPainter(self)
painter.drawLine(self.p1, self.p2)
class Example(QWidget, Drawer):
def __init__(self, parent=None):
super().__init__(parent)
self.initUI()
def initUI(self):
self.okButton = QPushButton("OK")
self.cancelButton = QPushButton("Cancel")
vbox = QVBoxLayout(self)
vbox.addWidget(self.okButton)
vbox.addWidget(self.cancelButton)
self.setGeometry(300, 300, 300, 150)
self.setWindowTitle("Buttons")
self.p1, self.p2 = self.okButton.pos(), self.cancelButton.pos()
self.okButton.installEventFilter(self)
self.cancelButton.installEventFilter(self)
def eventFilter(self, o, e):
if e.type() == QEvent.Move:
if o is self.okButton:
self.p1 = self.okButton.pos()
elif o is self.cancelButton:
self.p2 = self.cancelButton.pos()
self.update()
return super().eventFilter(o, e)
if __name__ == "__main__":
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())

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