Issue
How can I go about creating sub-menus in a QComboBox? I am currently using the default layout offered by the widget but this creates a lengthy pull-down list as seen in the attached image.
Solution
QComboBox normally uses a QListView as its popup. While it is possible to change it to a QTreeView by calling setView()
, its result is sometimes cumbersome and often requires further adjustments to make it usable.
Most importantly, the view will not use furhter popups, which can become an issue if the whole structure requires too space horizontally or vertically, or the hierarchy is too complex.
A simpler solution for such cases is to use a QToolButton with actual menus.
Using some custom functions and signals, you can get a behavior similar to that of a QComboBox, getting the text of the currently selected option (even in its full hierarchy).
class FakeCombo(QToolButton):
currentItemChanged = pyqtSignal(str)
currentItemPathChanged = pyqtSignal(list)
_currentAction = None
def __init__(self, data=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setPopupMode(self.MenuButtonPopup)
menu = QMenu(self)
self.setMenu(menu)
menu.triggered.connect(self.setCurrentAction)
self.pressed.connect(self.showMenu)
if data:
self.setData(data)
def _currentActionPath(self):
if not self._currentAction:
return []
action = self._currentAction
path = [action]
while action.parent() != self.menu():
action = action.parent().menuAction()
path.append(action)
return reversed(path)
def _getActionsRecursive(self, parent):
for action in parent.actions():
if action.menu():
yield from self._getActionsRecursive(action.menu())
else:
yield action
def _rebuildList(self):
self._actions = tuple(self._getActionsRecursive(self.menu()))
def currentItem(self):
if not self._currentAction:
return ''
return self._currentAction.text()
def currentItemPath(self):
return [a.text() for a in self._currentActionPath()]
def setCurrentAction(self, action):
if self._currentAction == action:
return
if not isinstance(action, QAction):
action = None
self._currentAction = action
if action is None:
self.currentItemChanged.emit('')
self.currentItemPathChanged.emit([])
return
path = self.currentItemPath()
self.setText(': '.join(path))
self.currentItemChanged.emit(path[-1])
self.currentItemPathChanged.emit(path)
def setData(self, data):
menu = self.menu()
menu.clear()
if not data:
self.setCurrentAction(None)
return
for item in data:
self.addItem(item, menu)
self._rebuildList()
self.setCurrentAction(self._actions[0])
def addItem(self, item, parent):
if isinstance(item, str):
action = QAction(item, parent)
elif isinstance(item, (tuple, list)):
main, subitems = item
action = parent.addAction(main)
menu = QMenu()
action.setMenu(menu)
for other in subitems:
self.addItem(other, menu)
action.destroyed.connect(menu.clear)
parent.addAction(action)
return action
def mousePressEvent(self, event):
if self.menu().actions():
QAbstractButton.mousePressEvent(self, event)
def keyPressEvent(self, event):
if self.menu().actions() or event.key() != Qt.Key_Space:
super().keyPressEvent(event)
# simple example of data structure made of tuples:
# - "final" items are simple strings
# - groups are tuples made of a string and another tuple
DATA = (
'Top level item',
('Group #1', (
'sub item #1 ',
'sub item #2 ',
('Sub group', (
'sub-sub item #1',
'sub-sub item #2',
)),
)),
('Group #2', (
'sub item #3',
)),
)
app = QApplication([])
win = QWidget()
box = FakeCombo(DATA)
itemField = QLineEdit(readOnly=True)
pathField = QLineEdit(readOnly=True)
layout = QFormLayout(win)
layout.addRow('Options:', box)
layout.addRow('Current item:', itemField)
layout.addRow('Current path:', pathField)
def updateCurrent(item):
itemField.setText(item)
pathField.setText(', '.join(box.currentItemPath()))
box.currentItemChanged.connect(updateCurrent)
updateCurrent(box.currentItem())
win.show()
app.exec()
There's obviously some margin of improvement, for instance allowing wheel and arrow key navigation, highlight of the current item on popup etc.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.