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

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

Related

PyQt5 POO Call instance of Class in another class

I would like when I click on a buttom from a toolbar created with PyQt get the selected items in a QListWidget created in other class (LisWorkDirectory class).
In the ToolBar.py in the compilation_ function, I would like to get all selected items. I want to get the instance of the ListWorkDirectory class to get the QListWidget that I created the first time I have launched my app.
If I instanciate ListWorkDirectory, I get a new instance, and the selectedItems return a empty list, it is a normal behaviour.
Maybe my architecture is not correct, so if you have any suggestion or remarks don't' hesitate to learn me.
Below my code so that you understand my request :
main.py
from MainWindow import MainWindow
from MenuBar import MenuBar
from ToolBar import ToolBar
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
#Window
windowApp = MainWindow("pyCompile")
#MenuBar
menuBar = MenuBar()
#ToolBar
toolBar = ToolBar()
windowApp.setMenuBar(menuBar)
windowApp.addToolBar(toolBar)
windowApp.show()
sys.exit(app.exec_())
MainWindow.py
from PyQt5.QtWidgets import QMainWindow, QWidget, QLineEdit, QPushButton, QListWidget,QListWidgetItem,QPlainTextEdit, QFileDialog, QStatusBar ,QVBoxLayout, QHBoxLayout
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt
from ListWorkDirectory import ListWorkDirectory
from Logger import MyDialog
import logging
import os
class MainWindow(QMainWindow):
def __init__(self, windowTitle):
super().__init__()
#Logger
self.logger = MyDialog()
logging.info("pyCompile version 0.1")
self.setGeometry(150,250,600,350)
self.setWindowTitle(windowTitle)
self.workDirectoryField = QLineEdit()
self.workDirectoryField.setPlaceholderText("Select your work directory ...")
self.workDirectoryField.setText("F:/WORKSPACE/Projects")
self.workDirectoryButton = QPushButton()
self.workDirectoryButton.setIcon(QIcon(":folder.svg"))
self.workDirectoryButton.clicked.connect(self.launchDialog)
self.hBoxLayout = QHBoxLayout()
self.hBoxLayout.addWidget(self.workDirectoryField)
self.hBoxLayout.addWidget(self.workDirectoryButton)
#List folder in work directory
self.myListFolder = ListWorkDirectory()
print(self.myListFolder)
self.workDirectoryField.textChanged[str].connect(self.myListFolder.update)
self.hBoxLayoutLogger = QHBoxLayout()
self.hBoxLayoutLogger.addWidget(self.myListFolder)
self.hBoxLayoutLogger2 = QHBoxLayout()
self.hBoxLayoutLogger2.addWidget(self.logger)
self.centralWidget = QWidget(self)
self.setCentralWidget(self.centralWidget)
self.vBoxLayout = QVBoxLayout(self.centralWidget)
self.vBoxLayout.addLayout(self.hBoxLayout)
self.vBoxLayout.addLayout(self.hBoxLayoutLogger)
self.vBoxLayout.addLayout(self.hBoxLayoutLogger2)
#Status Bar
self.statusBar = QStatusBar()
self.statusBar.showMessage("Welcome in pyCompile", 5000)
self.setStatusBar(self.statusBar)
def launchDialog(self):
workDirectory = QFileDialog.getExistingDirectory(self, caption="Select work directory")
print(workDirectory)
self.workDirectoryField.setText(workDirectory)
MenuBar.py
from PyQt5.QtWidgets import QMenuBar
class MenuBar(QMenuBar):
def __init__(self):
super().__init__()
self.fileMenu = "&File"
self.editMenu = "&Edit"
self.helpMenu = "&Help"
self.initUI()
def initUI(self):
self.addMenu(self.fileMenu)
self.addMenu(self.editMenu)
self.addMenu(self.helpMenu)
ToolBar.py
from PyQt5.QtWidgets import QMainWindow, QToolBar, QAction
from PyQt5.QtGui import QIcon
import qrc_resources
from ListWorkDirectory import ListWorkDirectory
class ToolBar(QToolBar, ListWorkDirectory):
def __init__(self):
super().__init__()
self._createActions()
self.initUI()
def initUI(self):
self.setMovable(False)
self.addAction(self.compileAction)
self.addAction(self.settingsAction)
self.addAction(self.quitAction)
def _createActions(self):
self.compileAction = QAction(self)
self.compileAction.setStatusTip("Launch compilation")
self.compileAction.setText("&Compile")
self.compileAction.setIcon(QIcon(":compile.svg"))
self.compileAction.triggered.connect(self.compilation_)
self.settingsAction = QAction(self)
self.settingsAction.setText("&Settings")
self.settingsAction.setIcon(QIcon(":settings.svg"))
self.quitAction = QAction(self)
self.quitAction.setText("&Quit")
self.quitAction.setIcon(QIcon(":quit.svg"))
def compilation_(self):
"""
Get the instance of ListWorkDirectory to get selected items and launch the
compilation
"""
ListWorkDirectory.py
from PyQt5.QtWidgets import QListWidget,QListWidgetItem
from PyQt5.QtCore import Qt, QDir
import os
class ListWorkDirectory(QListWidget):
def __init__(self):
super().__init__()
self.clear()
def update(self, workDirectoryField):
isPathCorrect = self.checkPath(workDirectoryField)
if(isPathCorrect):
listOfDirectory = self.getFolderList(workDirectoryField, os.listdir(workDirectoryField))
for folder in listOfDirectory:
self.item = QListWidgetItem(folder)
self.item.setCheckState(Qt.Unchecked)
self.addItem(self.item)
else:
self.clear()
def checkPath(self, path):
QPath = QDir(path)
isQPathExist = QPath.exists()
isPathEmpty = self.isPathEmpty(path)
if(isQPathExist and not isPathEmpty):
return True
else:
return False
def getFolderList(self, path, listOfFiles):
listOfFolders=[]
for file_ in listOfFiles:
if(os.path.isdir(os.path.join(path, file_))):
listOfFolders.append(file_)
else:
pass
return listOfFolders
def isPathEmpty(self, path):
if(path != ""):
return False
else:
return True
Thank you for your help.
When you add widget to window (or to other widget) then this window (or widget) is its parent and you can use self.parent() to access element in window (or widget). When widgets are nested then you may even use self.parent().parent()
def compilation_(self):
"""
Get the instance of ListWorkDirectory to get selected items and launch the
compilation
"""
print(self.parent().myListFolder)
EDIT:
Class ListWorkDirectory has function item(number) to get item from list - but you overwrite it with line self.item = QListWidgetItem(folder). If you remove self. and use
item = QListWidgetItem(folder)
item.setCheckState(Qt.Unchecked)
self.addItem(item)
then this will show only checked items
def compilation_(self):
"""
Get the instance of ListWorkDirectory to get selected items and launch the
compilation
"""
lst = self.parent().myListFolder
for x in range(lst.count()):
item = lst.item(x)
#print(item.checkState(), item.text())
if item.checkState() :
print(item.text())
Full working code - everyone can simply copy all to one file and run it.
from PyQt5.QtWidgets import QMainWindow, QWidget, QLineEdit, QPushButton, QListWidget,QListWidgetItem,QPlainTextEdit, QFileDialog, QStatusBar ,QVBoxLayout, QHBoxLayout
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import Qt
import logging
import os
# ---
from PyQt5.QtWidgets import QMenuBar
class MenuBar(QMenuBar):
def __init__(self):
super().__init__()
self.fileMenu = "&File"
self.editMenu = "&Edit"
self.helpMenu = "&Help"
self.initUI()
def initUI(self):
self.addMenu(self.fileMenu)
self.addMenu(self.editMenu)
self.addMenu(self.helpMenu)
# ---
from PyQt5.QtWidgets import QMainWindow, QToolBar, QAction
from PyQt5.QtGui import QIcon
class ToolBar(QToolBar):
def __init__(self):
super().__init__()
self._createActions()
self.initUI()
def initUI(self):
self.setMovable(False)
self.addAction(self.compileAction)
self.addAction(self.settingsAction)
self.addAction(self.quitAction)
def _createActions(self):
self.compileAction = QAction(self)
self.compileAction.setStatusTip("Launch compilation")
self.compileAction.setText("&Compile")
self.compileAction.setIcon(QIcon(":compile.svg"))
self.compileAction.triggered.connect(self.compilation_)
self.settingsAction = QAction(self)
self.settingsAction.setText("&Settings")
self.settingsAction.setIcon(QIcon(":settings.svg"))
self.quitAction = QAction(self)
self.quitAction.setText("&Quit")
self.quitAction.setIcon(QIcon(":quit.svg"))
def compilation_(self):
"""
Get the instance of ListWorkDirectory to get selected items and launch the
compilation
"""
lst = self.parent().myListFolder
for x in range(lst.count()):
item = lst.item(x)
#print(item.checkState(), item.text())
if item.checkState() :
print(item.text())
# ---
from PyQt5.QtWidgets import QListWidget, QListWidgetItem
from PyQt5.QtCore import Qt, QDir
import os
class ListWorkDirectory(QListWidget):
def __init__(self):
super().__init__()
self.clear()
def update(self, workDirectoryField):
isPathCorrect = self.checkPath(workDirectoryField)
if(isPathCorrect):
listOfDirectory = self.getFolderList(workDirectoryField, os.listdir(workDirectoryField))
for folder in listOfDirectory:
item = QListWidgetItem(folder)
item.setCheckState(Qt.Unchecked)
self.addItem(item)
else:
self.clear()
def checkPath(self, path):
QPath = QDir(path)
isQPathExist = QPath.exists()
isPathEmpty = self.isPathEmpty(path)
if(isQPathExist and not isPathEmpty):
return True
else:
return False
def getFolderList(self, path, listOfFiles):
listOfFolders=[]
for file_ in listOfFiles:
if(os.path.isdir(os.path.join(path, file_))):
listOfFolders.append(file_)
else:
pass
return listOfFolders
def isPathEmpty(self, path):
if(path != ""):
return False
else:
return True
class MainWindow(QMainWindow):
def __init__(self, windowTitle):
super().__init__()
#Logger
#self.logger = MyDialog()
logging.info("pyCompile version 0.1")
self.setGeometry(150,250,600,350)
self.setWindowTitle(windowTitle)
self.workDirectoryField = QLineEdit()
self.workDirectoryField.setPlaceholderText("Select your work directory ...")
self.workDirectoryField.setText("F:/WORKSPACE/Projects")
self.workDirectoryButton = QPushButton()
self.workDirectoryButton.setIcon(QIcon(":folder.svg"))
self.workDirectoryButton.clicked.connect(self.launchDialog)
self.hBoxLayout = QHBoxLayout()
self.hBoxLayout.addWidget(self.workDirectoryField)
self.hBoxLayout.addWidget(self.workDirectoryButton)
#List folder in work directory
self.myListFolder = ListWorkDirectory()
print(self.myListFolder)
self.workDirectoryField.textChanged[str].connect(self.myListFolder.update)
self.hBoxLayoutLogger = QHBoxLayout()
self.hBoxLayoutLogger.addWidget(self.myListFolder)
self.hBoxLayoutLogger2 = QHBoxLayout()
#self.hBoxLayoutLogger2.addWidget(self.logger)
self.centralWidget = QWidget(self)
self.setCentralWidget(self.centralWidget)
self.vBoxLayout = QVBoxLayout(self.centralWidget)
self.vBoxLayout.addLayout(self.hBoxLayout)
self.vBoxLayout.addLayout(self.hBoxLayoutLogger)
self.vBoxLayout.addLayout(self.hBoxLayoutLogger2)
#Status Bar
self.statusBar = QStatusBar()
self.statusBar.showMessage("Welcome in pyCompile", 5000)
self.setStatusBar(self.statusBar)
def launchDialog(self):
workDirectory = QFileDialog.getExistingDirectory(self, caption="Select work directory")
print(workDirectory)
self.workDirectoryField.setText(workDirectory)
# ---
from PyQt5.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
#Window
windowApp = MainWindow("pyCompile")
#MenuBar
menuBar = MenuBar()
#ToolBar
toolBar = ToolBar()
windowApp.setMenuBar(menuBar)
windowApp.addToolBar(toolBar)
windowApp.show()
sys.exit(app.exec_())

PyQT5 QTabbar Expand tab header

I am writing a QTabwidget with only two tabs. But the tab headers (name) are not fitting the QTabwidget width. I want to fit the length of the tab bar (two tab headers)
import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, QWidget, QAction, QTabWidget,QVBoxLayout
from PyQt5.QtGui import QIcon
from PyQt5.QtCore import pyqtSlot
class App(QMainWindow):
def __init__(self):
super().__init__()
self.table_widget = MyTableWidget(self)
self.setCentralWidget(self.table_widget)
self.show()
class MyTableWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
self.tabs = QTabWidget()
""" Here I want to fit the two tab
headers withthe QTabwidget width
"""
self.tab1 = QWidget()
self.tab2 = QWidget()
self.tabs.resize(300,200)
self.tabs.addTab(self.tab1,"Tab 1")
self.tabs.addTab(self.tab2,"Tab 2")
# Create first tab
self.tab1.layout = QVBoxLayout(self)
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
sys.exit(app.exec_())
The size of tabs is computed using the hints given by the current QStyle.
Since QTabWidget uses the sizeHint of the tab bar to set the tab bar size and the sizeHint is usually based on the tabSizeHint(), you have to reimplement both:
sizeHint() is required in order to provide a width (or height) that is the same as the parent;
tabSizeHint() takes into account the base implementation of sizeHint() to compute the hint based on the contents of the tabs, and if it's less than the current size it suggests a size based on the available space divided by the tab count;
class TabBar(QtWidgets.QTabBar):
def sizeHint(self):
hint = super().sizeHint()
if self.isVisible() and self.parent():
if not self.shape() & self.RoundedEast:
# horizontal
hint.setWidth(self.parent().width())
else:
# vertical
hint.setHeight(self.parent().height())
return hint
def tabSizeHint(self, index):
hint = super().tabSizeHint(index)
if not self.shape() & self.RoundedEast:
averageSize = self.width() / self.count()
if super().sizeHint().width() < self.width() and hint.width() < averageSize:
hint.setWidth(averageSize)
else:
averageSize = self.height() / self.count()
if super().sizeHint().height() < self.height() and hint.height() < averageSize:
hint.setHeight(averageSize)
return hint
# ...
self.tabWidget = QtWidgets.QTabWidget()
self.tabWidget.setTabBar(TabBar(self.tabWidget))
Do note that this is a very basic implementation, there are some situations for which you might see the scroll buttons with very long tab names, even if theoretically there should be enough space to see them.
Inspired by this answer, I think you can override showEvent (and even resizeEvent) to calculate the new width and set it through stylesheets.
It is not canonical but it does the job.
import sys
from PyQt5.QtWidgets import QMainWindow, QApplication, QWidget, QTabWidget, QVBoxLayout
class App(QMainWindow):
def __init__(self):
super().__init__()
self.table_widget = MyTableWidget(self)
self.setCentralWidget(self.table_widget)
self.show()
class MyTableWidget(QWidget):
def __init__(self, parent):
super(QWidget, self).__init__(parent)
self.layout = QVBoxLayout(self)
self.tabs = QTabWidget()
self.tabs.tabBar().setExpanding(True)
self.tab1 = QWidget()
self.tab2 = QWidget()
self.tabs.resize(300, 200)
self.tabs.addTab(self.tab1, "Tab 1")
self.tabs.addTab(self.tab2, "Tab 2")
# Create first tab
self.tab1.layout = QVBoxLayout(self)
self.layout.addWidget(self.tabs)
self.setLayout(self.layout)
def resizeEvent(self, event):
super().resizeEvent(event)
self._set_tabs_width()
def showEvent(self, event):
super().showEvent(event)
self._set_tabs_width()
def _set_tabs_width(self):
tabs_count = self.tabs.count()
tabs_width = self.tabs.width()
tab_width = tabs_width / tabs_count
css = "QTabBar::tab {width: %spx;}" % tab_width
self.tabs.setStyleSheet(css)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = App()
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_())

subclassing QGroupBox so that it can be member of QButtonGroup

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

How to show new QMainWindow in every loop in PyQT5?

I'm trying to write a Python program using PyQt5 that will display a window in each iteration of the for loop. I would like to close after incrementing and displaying the next window. However, I do not know how to stop the loop every iteration and at the moment I am getting 6 windows at once.
main.py
import sys
from PyQt5.QtWidgets import (QLineEdit, QVBoxLayout, QMainWindow,
QWidget, QDesktopWidget, QApplication, QPushButton, QLabel,
QComboBox, QFileDialog, QRadioButton)
from PyQt5.QtCore import pyqtSlot, QByteArray
from alert import Window2
from test import test
class SG(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.resize(300, 150)
self.setWindowTitle('TEST')
self.resultsGen = QPushButton('TEST', self)
self.resultsGen.clicked.connect(lambda: self.on_click())
self.show()
#pyqtSlot()
def on_click(self):
test(self)
if __name__ == '__main__':
app = QApplication(sys.argv)
sg = SG()
sys.exit(app.exec_())
alert.py
from PyQt5.QtWidgets import (QLineEdit, QVBoxLayout, QMainWindow,
QWidget, QDesktopWidget, QApplication, QPushButton, QLabel,
QComboBox, QFileDialog, QRadioButton)
from PyQt5.QtCore import pyqtSlot, QByteArray
from PyQt5.QtGui import QPixmap
from PyQt5 import QtGui, QtCore
class Window2(QMainWindow):
def __init__(self):
super().__init__()
self.initPopup()
def initPopup(self):
self.resize(500, 500)
self.setWindowTitle("Window22222")
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
lay = QVBoxLayout(self.central_widget)
label = QLabel(self)
pixmap = QPixmap('cropped/8.png')
label.setPixmap(pixmap)
self.resize(pixmap.width(), pixmap.height())
lay.addWidget(label)
self.textbox = QLineEdit(self)
self.textbox.move(20, 20)
self.textbox.resize(280, 40)
# Create a button in the window
self.button = QPushButton('Show text', self)
self.button.move(20, 80)
# connect button to function on_click
self.button.clicked.connect(lambda: self.on_clickX())
self.show()
#pyqtSlot()
def on_clickX(self):
textboxValue = self.textbox.text()
print(textboxValue)
self.textbox.setText("")
self.hide()
test.py
from alert import Window2
def test(self):
for x in range(6):
w = Window2()
As soon as you run the for cycle, all the code of the initialization will be executed, which includes the show() call you used at the end of initPopup().
A simple solution is to create a new signal that is emitted whenever you hide a window, and connect that signal to a function that creates a new one until it reaches the maximum number.
main.py:
import sys
from PyQt5.QtWidgets import QWidget, QApplication, QPushButton
from alert import Window2
class SG(QWidget):
def __init__(self):
super().__init__()
self.initUI()
self.alerts = []
def initUI(self):
self.resize(300, 150)
self.setWindowTitle('TEST')
self.resultsGen = QPushButton('TEST', self)
self.resultsGen.clicked.connect(self.nextAlert)
self.show()
def nextAlert(self):
if len(self.alerts) >= 6:
return
alert = Window2()
self.alerts.append(alert)
alert.setWindowTitle('Window {}'.format(len(self.alerts)))
alert.closed.connect(self.nextAlert)
alert.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
sg = SG()
sys.exit(app.exec_())
alert.py:
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
class Window2(QMainWindow):
closed = pyqtSignal()
def __init__(self):
super().__init__()
self.initPopup()
def initPopup(self):
self.resize(500, 500)
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
lay = QVBoxLayout(self.central_widget)
label = QLabel(self)
pixmap = QPixmap('cropped/8.png')
label.setPixmap(pixmap)
self.resize(pixmap.width(), pixmap.height())
lay.addWidget(label)
self.textbox = QLineEdit(self)
lay.addWidget(self.textbox)
# Create a button in the window
self.button = QPushButton('Show text', self)
lay.addWidget(self.button)
# connect button to function on_click
self.button.clicked.connect(lambda: self.on_clickX())
self.show()
#pyqtSlot()
def on_clickX(self):
textboxValue = self.textbox.text()
print(textboxValue)
self.textbox.setText("")
self.hide()
self.closed.emit()
Just note that with this very simplified example the user might click on the button of the "SG" widget even if an "alert" window is visibile. You might prefer to use a QDialog instead of a QMainWindow and make the main widget a parent of that dialog.
main.py:
class SG(QWidget):
# ...
def nextAlert(self):
if len(self.alerts) >= 6:
return
alert = Window2(self)
# ...
alert.py:
class Window2(QDialog):
closed = pyqtSignal()
def __init__(self, parent):
super().__init__()
self.initPopup()
def initPopup(self):
self.resize(500, 500)
# a QDialog doesn't need a central widget
lay = QVBoxLayout(self)
# ...
Also, if an alert window is closed using the "X" button the new one will not be shown automatically. To avoid that, you can implement the "closeEvent" and ignore the event, so that the user will not be able to close the window until the button is clicked. As QDialogs can close themself when pressing the escape key, I'm also ignoring that situation.
alert.py:
class Window2(QMainWindow):
# ...
def closeEvent(self, event):
event.ignore()
def keyPressEvent(self, event):
if event.key() != Qt.Key_Escape:
super().keyPressEvent(event)

Categories