英文:
Directly show the menu when its action is added to a QToolBar
问题
我有一个菜单,我想要添加到一个 QToolBar。
我知道可以将菜单的 menuAction()
添加到工具栏,但这样会正确显示其侧边的“菜单提示”,并通过单击它来弹出菜单,但单击按钮的主区域将不会产生任何效果。
当触发该操作时,该操作不应产生任何结果:该菜单用于设置文本编辑器中的字体颜色,由于它根据当前颜色自动更新其图标,使其可选(设置/取消字体颜色)是无效的。
我想要的是,无论用户在何处点击,菜单都会显示。
我知道可以添加操作,然后使用 widgetForAction()
获取实际的 QToolButton,然后更改其 popupMode
,但因为我知道将有更多类似的情况,我正在寻找更好的方法。
这个答案 建议使用 QPushButton,然后将该按钮添加到工具栏,但该解决方案并不理想:QPushButton 的样式与默认的 QToolButton 稍有不同,并且正如文档建议的,即使我使用 QToolButton,它也不会遵守 ToolButtonStyle
。
这是我当前代码的基本 MRE。请考虑 ColorMenu
类旨在通过使用子类扩展其他功能(背景文本、表边框和背景的颜色等):
# 以上是代码,无法提供翻译
英文:
I have a menu that I want to add to a QToolBar.
I know that I can add the menuAction()
of the menu to the tool bar, but while that will properly show the "menu hint" on its side and popup the menu by clicking on it, clicking the main area of the button will have no effect.
That action is not supposed to have any result when triggered: the menu is used to set the font color in a text editor, and since it automatically updates its icon based on the current color, making it checkable (to set/unset the font color) is ineffective.
What I want is that the menu will be shown, no matter where the user clicks.
I know that I can add the action, then use widgetForAction()
to get the actual QToolButton, and then change its popupMode
, but since I know that I will have more situations like this, I was looking for a better approach.
This answer suggests to use QPushButton instead, and add that button to the toolbar, but that solution is not ideal: QPushButton is styled slightly differently from the default QToolButton, and, as the documentation suggests, even if I use a QToolButton it will not respect the ToolButtonStyle
.
Here is a basic MRE of my current code. Please consider that the ColorMenu
class is intended to be extended for other features (background text, colors for table borders and backgrounds, etc) by using subclasses:
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class ColorMenu(QMenu):
def __init__(self, parent=None):
super().__init__(parent)
self.setTitle('Text color')
self.group = QActionGroup(self)
iconSize = self.style().pixelMetric(QStyle.PM_LargeIconSize)
pm = QPixmap(iconSize, iconSize)
pm.fill(self.palette().text().color())
self.defaultAction = self.addAction(QIcon(pm), 'Default color')
self.defaultAction.setCheckable(True)
self.group.addAction(self.defaultAction)
self.addSeparator()
self.customColorAction = self.addAction('Custom color')
self.customColorAction.setVisible(False)
self.customColorAction.setCheckable(True)
self.group.addAction(self.customColorAction)
self.addSeparator()
self.baseColorActions = []
colors = {}
# get valid global colors
for key, value in Qt.__dict__.items():
if (
isinstance(value, Qt.GlobalColor)
and 1 < value < 19
):
# make names more readable
if key.startswith('light'):
key = 'light {}'.format(key[5:].lower())
elif key.startswith('dark'):
key = 'dark {}'.format(key[4:].lower())
colors[value] = key.capitalize()
# more logical sorting of global colors
for i in (2, 4, 5, 6, 3, 7, 13, 8, 14, 9, 15, 10, 16, 11, 17, 12, 18):
color = QColor(Qt.GlobalColor(i))
pm = QPixmap(iconSize, iconSize)
pm.fill(color)
action = self.addAction(QIcon(pm), colors[i])
action.setData(color)
action.setCheckable(True)
self.group.addAction(action)
self.baseColorActions.append(action)
self.setColor(None)
def setColor(self, color):
if isinstance(color, QBrush) and color.style():
color = color.color()
elif isinstance(color, (Qt.GlobalColor, int):
color = QColor(color)
if instance(color, QColor) and color.isValid():
for action in self.baseColorActions:
if action.data() == color:
self.setIcon(action.icon())
action.setChecked(True)
self.customColorAction.setVisible(False)
break
else:
iconSize = self.style().pixelMetric(QStyle.PM_LargeIconSize)
pm = QPixmap(iconSize, iconSize)
pm.fill(color)
icon = QIcon(pm)
self.setIcon(icon)
self.customColorAction.setIcon(icon)
self.customColorAction.setData(color)
self.customColorAction.setVisible(True)
self.customColorAction.setChecked(True)
return
self.setIcon(self.defaultAction.icon())
self.defaultAction.setChecked(True)
self.customColorAction.setVisible(False)
class Editor(QMainWindow):
def __init__(self):
super().__init__()
self.editor = QTextEdit()
self.setCentralWidget(self.editor)
self.formatMenu = self.menuBar().addMenu('Format')
self.colorMenu = ColorMenu(self)
self.formatMenu.addMenu(self.colorMenu)
self.toolbar = QToolBar('Format')
self.addToolBar(Qt.TopToolBarArea, self.toolbar)
self.toolbar.addAction(self.colorMenu.menuAction())
self.editor.currentCharFormatChanged.connect(self.updateColorMenu)
self.colorMenu.triggered.connect(self.setTextColor)
def setTextColor(self, action):
# assume that the action.data() has a color value, if not, revert to the default
if action.data():
self.editor.setTextColor(action.data())
else:
tc = self.editor.textCursor()
fmt = tc.charFormat()
fmt.clearForeground()
tc.setCharFormat(fmt)
def updateColorMenu(self, fmt):
self.colorMenu.setColor(fmt.foreground())
app = QApplication([])
editor = Editor()
editor.show()
app.exec()
答案1
得分: 1
这是一个使用QMenu的子类并实现一个专门返回专用操作的方法的可能性。
这有点像是一个小技巧或变通方法,但在某些情况下可能是有效的。
新操作将会:
- 通过提供工具栏创建,工具栏将成为其父对象(以确保在工具栏被销毁时能正确删除);
- 当触发时强制显示菜单;
- 当菜单操作被更改时更新自身(标题和图标);
class ColorMenu(QMenu):
# ...
def toolBarAction(self, toolbar):
def triggerMenu():
try:
button = toolbar.widgetForAction(action)
if isinstance(button, QToolButton):
button.showMenu()
else:
print('Warning: action triggered from somewhere else')
except (TypeError, RuntimeError):
# 该操作已被销毁
pass
def updateAction():
try:
action.setIcon(self.icon())
action.setText(self.title())
except (TypeError, RuntimeError):
# 该操作已被销毁
pass
action = QAction(self.icon(), self.title(), toolbar)
action.triggered.connect(triggerMenu)
self.menuAction().changed.connect(updateAction)
action.setMenu(self)
return action
class Editor(QMainWindow):
def __init__(self):
# ...
# 将相关行替换为以下内容
self.toolbar.addAction(self.colorMenu.toolBarAction(self.toolbar))
英文:
A possibility is to use a subclass of QMenu and implement a specialized function that will return a dedicated action.
This is a bit of a hack/workaround, but may be effective in some situations.
That new action will:
- be created by providing the tool bar, which will become its parent (to ensure proper deletion if the tool bar is destroyed);
- forcibly show the menu when triggered;
- update itself (title and icon) whenever the menu action is changed;
class ColorMenu(QMenu):
# ...
def toolBarAction(self, toolbar):
def triggerMenu():
try:
button = toolbar.widgetForAction(action)
if isinstance(button, QToolButton):
button.showMenu()
else:
print('Warning: action triggered from somewhere else')
except (TypeError, RuntimeError):
# the action has been destroyed
pass
def updateAction():
try:
action.setIcon(self.icon())
action.setText(self.title())
except (TypeError, RuntimeError):
# the action has been destroyed
pass
action = QAction(self.icon(), self.title(), toolbar)
action.triggered.connect(triggerMenu)
self.menuAction().changed.connect(updateAction)
action.setMenu(self)
return action
class Editor(QMainWindow):
def __init__(self):
# ...
# replace the related line with the following
self.toolbar.addAction(self.colorMenu.toolBarAction(self.toolbar))
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论