Issue
I want to make a QComboBox automatically open its dropdown menu at program start-up and without mouse clicking, and the menu should stay open indefinitely, with a cell being highlighted.
I want to do this because I am writing a widget to customize the stylesheets of every single GUI component of an application I am writing, and there are hundreds of lines of stylesheets alone. I want to make a dummy QComboBox stay open so as to let the user preview the styled result. And the dummy QComboBox should be disabled.
I have searched ways to open the dropdown programmatically and haven't found a method that works with PyQt6. I have tried a few methods myself and they don't work.
Minimal reproducible example:
import sys
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
app = QApplication([])
app.setStyle("Fusion")
class ComboBox(QComboBox):
def __init__(self, texts):
super().__init__()
self.addItems(texts)
self.setContentsMargins(3, 3, 3, 3)
self.setFixedWidth(100)
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.centralwidget = QWidget(self)
self.setCentralWidget(self.centralwidget)
self.vbox = QVBoxLayout(self.centralwidget)
box = ComboBox(["Lorem", "Ipsum", "Dolor"])
self.vbox.addWidget(box)
box.showPopup()
box.DroppedDown = True
box.setProperty("DroppedDown", True)
box.raise_()
self.setStyleSheet("""
QMainWindow {
background: #201a33;
}
QComboBox {
border: 3px outset #552b80;
border-radius: 8px;
background: #422e80;
color: #c000ff;
selection-background-color: #7800d7;
selection-color: #ffb2ff;
}
QComboBox::drop-down {
border: 0px;
padding: 0px 0px 0px 0px;
}
QComboBox::down-arrow {
image: url(D:/Asceteria/downarrow.png);
width: 10px;
height: 10px;
}
QComboBox QAbstractItemView {
border: 2px groove #8000c0;
border-radius: 6px;
background: #422e80;
}""")
window = Window()
window.show()
sys.exit(app.exec())
The combobox only opens its dropdown menu when it is clicked, I want it to open automatically without needing to click it. How do I do this?
I am perfectly capable of cheating and using a combination of QGroupBox and QLabels to make a mock-up, but I want to know if there is a proper way to do this.
Solution
Calling showPopup()
just when the combobox is initialized is inappropriate, especially considering that the combo is being added to a container that is yet to be shown.
Even ignoring that, there are two other issues with that attempt:
- when a widget has just been created, it has a default geometry based on a 0-point position (
QPoint(0, 0)
) and size set depending on its window level: if it has a parent, it's100x30
, otherwise it's considered a top level window with640x480
; since QComboBox uses its current visible size to adjust the popup geometry, this means that a popup shown when the combo is not yet visible (and positioned in window and screen coordinates) would have an inconsistent geometry; - by default, the view is shown within a private QFrame created by QComboBox, which uses the
Popup
window flag; that flag affects the whole system keyboard and mouse management, so that all those events will be received by it, preventing interaction with any other widget, including those belonging to other programs and even the OS;
Ignoring the above, the simplest solution could be to override hidePopup()
, which is normally called from the combo when it needs to hide the view. Unfortunately this won't cover all the above problems. For example, a Popup
flagged window will be potentially closed when clicking outside of it or when it loses focus (depending on the OS).
While it would be possible to prevent those issues by using properly set window flags and attributes, doing it may not be very reliable unless done with extreme care, and different OS versions/features may still give unexpected results, so covering all these cases would be painful other than unreliable.
Luckily, there is a simpler alternative: using the Graphics View framework.
That framework allows to "embed" standard QWidgets inside a QGraphicsScene using a "proxy" graphics item. When a QComboBox is added to a scene, its popup view is also shown within the scene.
This is extremely useful for this case, since we can completely ignore problems related to OS focus/input events and simplify everything: we just need to properly ignore all events and event handlers that would cause the popup to hide. This is achieved by:
- ignoring the
hidePopup()
call; - ignoring keyboard and wheel events on the combo box;
- installing an event filter on both the view and its container (the QFrame mentioned above) that will filter out the following event types of the view:
- keyboard press/release;
- mouse press/move/release;
- wheel events;
- hide events (by forcing the view to show again);
- focus out events (by forcing the focus to ensure proper QStyle painting);
Here is a basic example that shows what explained above:
QSS = '''
QComboBox {
border: 3px outset #552b80;
border-radius: 8px;
background: #422e80;
color: #c000ff;
selection-background-color: #7800d7;
selection-color: #ffb2ff;
}
QComboBox::drop-down {
border: 0px;
padding: 0px 0px 0px 0px;
}
QComboBox::down-arrow {
image: url(D:/Asceteria/downarrow.png);
width: 10px;
height: 10px;
}
QComboBox QAbstractItemView {
border: 2px groove #8000c0;
border-radius: 6px;
background: #422e80;
}
'''.strip()
class ComboWithFixedPopup(QComboBox):
def __init__(self):
super().__init__()
self.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.addItems(('Item 1', 'Item 2', 'Item 3'))
self.setCurrentIndex(1)
def eventFilter(self, obj, event):
eventType = event.type()
if eventType in (
event.Type.KeyPress, event.Type.KeyRelease,
event.Type.MouseButtonPress, event.Type.MouseMove,
event.Type.MouseButtonRelease,
event.Type.Wheel,
):
return True
if eventType == event.Type.Hide:
obj.show()
return True
elif eventType == event.Type.FocusOut:
QTimer.singleShot(0, obj.setFocus)
return True
return super().eventFilter(obj, event)
def hidePopup(self):
pass
def keyPressEvent(self, event):
pass
def showPopup(self):
super().showPopup()
view = self.view()
container = view.parent()
proxy = container.graphicsProxyWidget()
if not proxy:
return
view.installEventFilter(self)
view.viewport().installEventFilter(self)
proxy.installEventFilter(self)
QApplication.processEvents()
# Some styles show the popup *over* the combo, but some
# implementations may override this behavior. If you want to follow
# the current style, ignore the following, which instead always shows
# the popup above or below the combobox in order to always show it.
# comboRect = self.rect().translated(
# self.mapTo(self.window(), QPoint()))
# windowProxy = self.window().graphicsProxyWidget()
# comboRect = QRectF(comboRect).translated(windowProxy.scenePos())
# viewRect = container.graphicsProxyWidget().geometry()
# if viewRect.intersects(comboRect):
# if comboRect.y() > viewRect.y():
# print('cippo', comboRect.y(), viewRect.y())
# viewRect.moveBottom(comboRect.y())
# else:
# viewRect.moveTop(comboRect.bottom())
# proxy.setGeometry(viewRect)
def wheelEvent(self, event):
pass
class GraphicsPreview(QGraphicsView):
def __init__(self):
scene = QGraphicsScene()
super().__init__(scene)
self.combo = ComboWithFixedPopup()
self.container = QWidget()
self.container.resize(400, 400)
layout = QVBoxLayout(self.container)
layout.addWidget(self.combo)
scene.addWidget(self.container)
self.setSceneRect(scene.sceneRect())
def showEvent(self, event):
super().showEvent(event)
self.combo.showPopup()
def updateQSS(self, qss):
self.container.setStyleSheet(qss)
class QSSEditor(QPlainTextEdit):
_shown = False
def __init__(self):
super().__init__()
font = QFont()
font.setFamily('monospace')
self.document().setDefaultFont(font)
def sizeHint(self):
# try to show the QSS without word wrapping by using an
# appropriate width for the size hint
hint = super().sizeHint()
if self.toPlainText().strip():
fm = QFontMetrics(self.document().defaultFont())
width = fm.size(0, self.toPlainText()).width()
hint.setWidth(int(
width + self.document().documentMargin() * 2
+ self.frameWidth() * 2
+ self.verticalScrollBar().sizeHint().width()
))
return hint
class QSSPreview(QWidget):
def __init__(self):
super().__init__()
self.editor = QSSEditor()
self.graphicsPreview = GraphicsPreview()
layout = QHBoxLayout(self)
layout.addWidget(self.editor)
layout.addWidget(self.graphicsPreview)
self.editor.textChanged.connect(lambda:
self.graphicsPreview.updateQSS(self.editor.toPlainText()))
self.editor.setPlainText(QSS)
if __name__ == '__main__':
app = QApplication(sys.argv)
window = QSSPreview()
window.show()
sys.exit(app.exec())
Note that some styles may show the popup over the combo, thus making it invisible.
It is still possible to fix that by checking both the geometries to scene coordinates and eventually show the popup above or below the combo, but that may be unnecessary if you plan to display the possible appearance with the current configuration.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.