2024-03-28 16:38:05 发布
网友
我想有一个可滚动的上下文菜单,以便我可以在其中放置许多操作。我在另一篇文章中看到了一个答案,设置menu.setStyleSheet('QMenu{menu-scrollable: 1;}')将启用滚动条,但它似乎不起作用
menu.setStyleSheet('QMenu{menu-scrollable: 1;}')
下面是blender软件上下文菜单的演示
如何做到这一点
为此,您必须继承QMenu并重写wheelEvent
QMenu
wheelEvent
下面是一个示例,您可以对其进行改进
import sys from PyQt5 import QtWidgets, QtCore, QtGui class CustomMenu(QtWidgets.QMenu): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet('QMenu{background: gray;} QMenu::item:selected {background: rgb(58, 145, 232);}') self.setFixedHeight(100) self.visible_lst = [] self.index = -1 self.visibleCount = None self.maxHeightOfAction = 0 # Remove Below code if you don't want the arrow -# self.topArrow = None self.bottomArrow = None self.painter = QtGui.QPainter() # # def actionEvent(self, event): if event.type() == QtCore.QEvent.ActionAdded: if self.maxHeightOfAction < self.actionGeometry(self.actions()[-1]).height(): self.maxHeightOfAction = self.actionGeometry(self.actions()[-1]).height() self.actions()[-1].setVisible(False) self.updateActions() if event.type() == QtCore.QEvent.ActionRemoved: super(CustomMenu, self).actionEvent(event) if self.index == len(self.actions()): self.index -= 1 if event.action() in self.visible_lst: self.visible_lst.remove(event.action()) self.removed() super(CustomMenu, self).actionEvent(event) def updateActions(self): if self.actions(): if self.findVisibleCount() > len(self.actions()) and self.index == -1: self.visible_lst = self.actions() self.updateVisible() elif self.findVisibleCount() < len(self.actions()) and self.index == -1: self.index += 1 self.visible_lst = self.actions()[0: self.findVisibleCount()] self.updateVisible() self.setActiveAction(self.visible_lst[0]) def removed(self): if len(self.actions()) > self.findVisibleCount(): if self.index < len(self.actions())-2: index = self.findIndex(self.visible_lst, self.activeAction()) self.visible_lst.append(self.actions()[self.index + (index-self.findVisibleCount())-1]) elif self.index == len(self.actions())-1: self.visible_lst.insert(0, self.actions()[-self.findVisibleCount()-1]) self.updateVisible() def findVisibleCount(self): # finds how many QActions will be visible visibleWidgets = 0 if self.actions(): try: visibleWidgets = self.height()//self.maxHeightOfAction except ZeroDivisionError: pass return visibleWidgets def mousePressEvent(self, event) -> None: if self.topArrow.containsPoint(event.pos(), QtCore.Qt.OddEvenFill) and self.index>0: self.scrollUp() elif self.bottomArrow.containsPoint(event.pos(), QtCore.Qt.OddEvenFill) and self.index < len(self.actions()) -1: self.scrollDown() else: super(CustomMenu, self).mousePressEvent(event) def keyPressEvent(self, event): if self.actions(): if self.activeAction() is None: self.setActiveAction(self.actions()[self.index]) if event.key() == QtCore.Qt.Key_Up: self.scrollUp() elif event.key() == QtCore.Qt.Key_Down: self.scrollDown() elif event.key() == QtCore.Qt.Key_Return: super(CustomMenu, self).keyPressEvent(event) def wheelEvent(self, event): if self.actions(): if self.activeAction() is None: self.setActiveAction(self.actions()[self.index]) delta = event.angleDelta().y() if delta < 0: # scroll down self.scrollDown() elif delta > 0: # scroll up self.scrollUp() def scrollDown(self): if self.index < len(self.actions())-1: self.index = self.findIndex(self.actions(), self.activeAction()) + 1 try: self.setActiveAction(self.actions()[self.index]) if self.activeAction() not in self.visible_lst and len(self.actions()) > self.findVisibleCount(): self.visible_lst[0].setVisible(False) self.visible_lst.pop(0) self.visible_lst.append(self.actions()[self.index]) self.visible_lst[-1].setVisible(True) except IndexError: pass def scrollUp(self): if self.findIndex(self.actions(), self.activeAction()) > 0: self.index = self.findIndex(self.actions(), self.activeAction()) - 1 try: self.setActiveAction(self.actions()[self.index]) if self.activeAction() not in self.visible_lst and len(self.actions()) > self.findVisibleCount(): self.visible_lst[-1].setVisible(False) self.visible_lst.pop() self.visible_lst.insert(0, self.actions()[self.index]) self.visible_lst[0].setVisible(True) except IndexError: pass def updateVisible(self): for item in self.visible_lst: if not item.isVisible(): item.setVisible(True) def findIndex(self, lst, element): for index, item in enumerate(lst): if item == element: return index return -1 def paintEvent(self, event): # remove this if you don't want the arrow super(CustomMenu, self).paintEvent(event) height = int(self.height()) width = self.width()//2 topPoints = [QtCore.QPoint(width-5, 7), QtCore.QPoint(width, 2), QtCore.QPoint(width+5, 7)] bottomPoints = [QtCore.QPoint(width-5, height-7), QtCore.QPoint(width, height-2), QtCore.QPoint(width+5, height-7)] self.topArrow = QtGui.QPolygon(topPoints) self.bottomArrow = QtGui.QPolygon(bottomPoints) self.painter.begin(self) self.painter.setBrush(QtGui.QBrush(QtCore.Qt.white)) self.painter.setPen(QtCore.Qt.white) if len(self.actions()) > self.findVisibleCount(): if self.index>0: self.painter.drawPolygon(self.topArrow) if self.index < len(self.actions()) -1: self.painter.drawPolygon(self.bottomArrow) self.painter.end() class ExampleWindow(QtWidgets.QWidget): def contextMenuEvent(self, event) -> None: menu = CustomMenu() menu.addAction('Hello0') menu.addAction('Hello1') menu.addAction('Hello2') menu.addAction('Hello3') menu.addAction('Hello4') menu.addAction('Hello5') menu.addAction('Hello6') menu.addAction('Hello7') menu.addAction('Hello8') menu.exec_(QtGui.QCursor.pos()) def main(): app = QtWidgets.QApplication(sys.argv) window = ExampleWindow() window.setWindowTitle('PyQt5 App') window.show() app.exec_() if __name__ == '__main__': main()
上述代码的解释:
将所有可见操作存储在一个列表中,例如visible_lst
visible_lst
向下滚动:
self.visible_lst[0].setVisible(False)
self.actions()[self.index]
向上滚动:
self.visible_lst[-1].setVisible(False)
self.actions()[index].setVisible(True)
滚动上下文菜单代码的输出:
读者,如果您有任何建议或疑问,请留言
处理菜单定制(使用任何框架)不是一项容易的任务,根据经验,我可以告诉您,尝试使用简单的方法,例如切换项目可见性,肯定会在用户体验中导致意外的行为和问题
必须牢记三个方面:
从您给出的答案中,我至少可以看到以下重要问题:
不幸的是,解决方案是正确地实现所需的一切,从绘画开始,显然,从用户交互开始
下面是一个几乎完整的可滚动菜单的实现;可以通过设置最大高度或maxItemCount关键字参数来启用滚动,该参数根据标准项猜测高度;然后通过移动箭头(和/或单击箭头)以及使用键盘箭头来激活它。 它还不完善,可能还有一些方面我没有考虑(见上面的“懒惰调试”注释),但是对于我所看到的,它应该按照预期工作。p>
maxItemCount
而且,是的,我知道,它真的扩展了;但是,如上所述,菜单并不像看上去那么简单
class ScrollableMenu(QtWidgets.QMenu): deltaY = 0 dirty = True ignoreAutoScroll = False def __init__(self, *args, **kwargs): maxItemCount = kwargs.pop('maxItemCount', 0) super().__init__(*args, **kwargs) self._maximumHeight = self.maximumHeight() self._actionRects = [] self.scrollTimer = QtCore.QTimer(self, interval=50, singleShot=True, timeout=self.checkScroll) self.scrollTimer.setProperty('defaultInterval', 50) self.delayTimer = QtCore.QTimer(self, interval=100, singleShot=True) self.setMaxItemCount(maxItemCount) @property def actionRects(self): if self.dirty or not self._actionRects: self._actionRects.clear() offset = self.offset() for action in self.actions(): geo = super().actionGeometry(action) if offset: geo.moveTop(geo.y() - offset) self._actionRects.append(geo) self.dirty = False return self._actionRects def iterActionRects(self): for action, rect in zip(self.actions(), self.actionRects): yield action, rect def setMaxItemCount(self, count): style = self.style() opt = QtWidgets.QStyleOptionMenuItem() opt.initFrom(self) a = QtWidgets.QAction('fake action', self) self.initStyleOption(opt, a) size = QtCore.QSize() fm = self.fontMetrics() qfm = opt.fontMetrics size.setWidth(fm.boundingRect(QtCore.QRect(), QtCore.Qt.TextSingleLine, a.text()).width()) size.setHeight(max(fm.height(), qfm.height())) self.defaultItemHeight = style.sizeFromContents(style.CT_MenuItem, opt, size, self).height() if not count: self.setMaximumHeight(self._maximumHeight) else: fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self) vmargin = style.pixelMetric(style.PM_MenuHMargin, opt, self) scrollHeight = self.scrollHeight(style) self.setMaximumHeight( self.defaultItemHeight * count + (fw + vmargin + scrollHeight) * 2) self.dirty = True def scrollHeight(self, style): return style.pixelMetric(style.PM_MenuScrollerHeight, None, self) * 2 def isScrollable(self): return self.height() < super().sizeHint().height() def checkScroll(self): pos = self.mapFromGlobal(QtGui.QCursor.pos()) delta = max(2, int(self.defaultItemHeight * .25)) if pos in self.scrollUpRect: delta *= -1 elif pos not in self.scrollDownRect: return if self.scrollBy(delta): self.scrollTimer.start(self.scrollTimer.property('defaultInterval')) def offset(self): if self.isScrollable(): return self.deltaY - self.scrollHeight(self.style()) return 0 def translatedActionGeometry(self, action): return self.actionRects[self.actions().index(action)] def ensureVisible(self, action): style = self.style() fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self) hmargin = style.pixelMetric(style.PM_MenuHMargin, None, self) vmargin = style.pixelMetric(style.PM_MenuVMargin, None, self) scrollHeight = self.scrollHeight(style) extent = fw + hmargin + vmargin + scrollHeight r = self.rect().adjusted(0, extent, 0, -extent) geo = self.translatedActionGeometry(action) if geo.top() < r.top(): self.scrollBy(-(r.top() - geo.top())) elif geo.bottom() > r.bottom(): self.scrollBy(geo.bottom() - r.bottom()) def scrollBy(self, step): if step < 0: newDelta = max(0, self.deltaY + step) if newDelta == self.deltaY: return False elif step > 0: newDelta = self.deltaY + step style = self.style() scrollHeight = self.scrollHeight(style) bottom = self.height() - scrollHeight for lastAction in reversed(self.actions()): if lastAction.isVisible(): break lastBottom = self.actionGeometry(lastAction).bottom() - newDelta + scrollHeight if lastBottom < bottom: newDelta -= bottom - lastBottom if newDelta == self.deltaY: return False self.deltaY = newDelta self.dirty = True self.update() return True def actionAt(self, pos): for action, rect in self.iterActionRects(): if pos in rect: return action # class methods reimplementation def sizeHint(self): hint = super().sizeHint() if hint.height() > self.maximumHeight(): hint.setHeight(self.maximumHeight()) return hint def eventFilter(self, source, event): if event.type() == event.Show: if self.isScrollable() and self.deltaY: action = source.menuAction() self.ensureVisible(action) rect = self.translatedActionGeometry(action) delta = rect.topLeft() - self.actionGeometry(action).topLeft() source.move(source.pos() + delta) return False return super().eventFilter(source, event) def event(self, event): if not self.isScrollable(): return super().event(event) if event.type() == event.KeyPress and event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down): res = super().event(event) action = self.activeAction() if action: self.ensureVisible(action) self.update() return res elif event.type() in (event.MouseButtonPress, event.MouseButtonDblClick): pos = event.pos() if pos in self.scrollUpRect or pos in self.scrollDownRect: if event.button() == QtCore.Qt.LeftButton: step = max(2, int(self.defaultItemHeight * .25)) if pos in self.scrollUpRect: step *= -1 self.scrollBy(step) self.scrollTimer.start(200) self.ignoreAutoScroll = True return True elif event.type() == event.MouseButtonRelease: pos = event.pos() self.scrollTimer.stop() if not (pos in self.scrollUpRect or pos in self.scrollDownRect): action = self.actionAt(event.pos()) if action: action.trigger() self.close() return True return super().event(event) def timerEvent(self, event): if not self.isScrollable(): # ignore internal timer event for reopening popups super().timerEvent(event) def mouseMoveEvent(self, event): if not self.isScrollable(): super().mouseMoveEvent(event) return pos = event.pos() if pos.y() < self.scrollUpRect.bottom() or pos.y() > self.scrollDownRect.top(): if not self.ignoreAutoScroll and not self.scrollTimer.isActive(): self.scrollTimer.start(200) return self.ignoreAutoScroll = False oldAction = self.activeAction() if not pos in self.rect(): action = None else: y = event.y() for action, rect in self.iterActionRects(): if rect.y() <= y <= rect.y() + rect.height(): break else: action = None self.setActiveAction(action) if action and not action.isSeparator(): def ensureVisible(): self.delayTimer.timeout.disconnect() self.ensureVisible(action) try: self.delayTimer.disconnect() except: pass self.delayTimer.timeout.connect(ensureVisible) self.delayTimer.start(150) elif oldAction and oldAction.menu() and oldAction.menu().isVisible(): def closeMenu(): self.delayTimer.timeout.disconnect() oldAction.menu().hide() self.delayTimer.timeout.connect(closeMenu) self.delayTimer.start(50) self.update() def wheelEvent(self, event): if not self.isScrollable(): return self.delayTimer.stop() if event.angleDelta().y() < 0: self.scrollBy(self.defaultItemHeight) else: self.scrollBy(-self.defaultItemHeight) def showEvent(self, event): if self.isScrollable(): self.deltaY = 0 self.dirty = True for action in self.actions(): if action.menu(): action.menu().installEventFilter(self) self.ignoreAutoScroll = False super().showEvent(event) def hideEvent(self, event): for action in self.actions(): if action.menu(): action.menu().removeEventFilter(self) super().hideEvent(event) def resizeEvent(self, event): super().resizeEvent(event) style = self.style() l, t, r, b = self.getContentsMargins() fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self) hmargin = style.pixelMetric(style.PM_MenuHMargin, None, self) vmargin = style.pixelMetric(style.PM_MenuVMargin, None, self) leftMargin = fw + hmargin + l topMargin = fw + vmargin + t bottomMargin = fw + vmargin + b contentWidth = self.width() - (fw + hmargin) * 2 - l - r scrollHeight = self.scrollHeight(style) self.scrollUpRect = QtCore.QRect(leftMargin, topMargin, contentWidth, scrollHeight) self.scrollDownRect = QtCore.QRect(leftMargin, self.height() - scrollHeight - bottomMargin, contentWidth, scrollHeight) def paintEvent(self, event): if not self.isScrollable(): super().paintEvent(event) return style = self.style() qp = QtGui.QPainter(self) rect = self.rect() emptyArea = QtGui.QRegion(rect) menuOpt = QtWidgets.QStyleOptionMenuItem() menuOpt.initFrom(self) menuOpt.state = style.State_None menuOpt.maxIconWidth = 0 menuOpt.tabWidth = 0 style.drawPrimitive(style.PE_PanelMenu, menuOpt, qp, self) fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self) topEdge = self.scrollUpRect.bottom() bottomEdge = self.scrollDownRect.top() offset = self.offset() qp.save() qp.translate(0, -offset) # offset translation is required in order to allow correct fade animations for action, actionRect in self.iterActionRects(): actionRect = self.translatedActionGeometry(action) if actionRect.bottom() < topEdge: continue if actionRect.top() > bottomEdge: continue visible = QtCore.QRect(actionRect) if actionRect.bottom() > bottomEdge: visible.setBottom(bottomEdge) elif actionRect.top() < topEdge: visible.setTop(topEdge) visible = QtGui.QRegion(visible.translated(0, offset)) qp.setClipRegion(visible) emptyArea -= visible.translated(0, -offset) opt = QtWidgets.QStyleOptionMenuItem() self.initStyleOption(opt, action) opt.rect = actionRect.translated(0, offset) style.drawControl(style.CE_MenuItem, opt, qp, self) qp.restore() cursor = self.mapFromGlobal(QtGui.QCursor.pos()) upData = ( False, self.deltaY > 0, self.scrollUpRect ) downData = ( True, actionRect.bottom() - 2 > bottomEdge, self.scrollDownRect ) for isDown, enabled, scrollRect in upData, downData: qp.setClipRect(scrollRect) scrollOpt = QtWidgets.QStyleOptionMenuItem() scrollOpt.initFrom(self) scrollOpt.state = style.State_None scrollOpt.checkType = scrollOpt.NotCheckable scrollOpt.maxIconWidth = scrollOpt.tabWidth = 0 scrollOpt.rect = scrollRect scrollOpt.menuItemType = scrollOpt.Scroller if enabled: if cursor in scrollRect: frame = QtWidgets.QStyleOptionMenuItem() frame.initFrom(self) frame.rect = scrollRect frame.state |= style.State_Selected | style.State_Enabled style.drawControl(style.CE_MenuItem, frame, qp, self) scrollOpt.state |= style.State_Enabled scrollOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active) else: scrollOpt.palette.setCurrentColorGroup(QtGui.QPalette.Disabled) if isDown: scrollOpt.state |= style.State_DownArrow style.drawControl(style.CE_MenuScroller, scrollOpt, qp, self) if fw: borderReg = QtGui.QRegion() borderReg |= QtGui.QRegion(QtCore.QRect(0, 0, fw, self.height())) borderReg |= QtGui.QRegion(QtCore.QRect(self.width() - fw, 0, fw, self.height())) borderReg |= QtGui.QRegion(QtCore.QRect(0, 0, self.width(), fw)) borderReg |= QtGui.QRegion(QtCore.QRect(0, self.height() - fw, self.width(), fw)) qp.setClipRegion(borderReg) emptyArea -= borderReg frame = QtWidgets.QStyleOptionFrame() frame.rect = rect frame.palette = self.palette() frame.state = QtWidgets.QStyle.State_None frame.lineWidth = style.pixelMetric(style.PM_MenuPanelWidth) frame.midLineWidth = 0 style.drawPrimitive(style.PE_FrameMenu, frame, qp, self) qp.setClipRegion(emptyArea) menuOpt.state = style.State_None menuOpt.menuItemType = menuOpt.EmptyArea menuOpt.checkType = menuOpt.NotCheckable menuOpt.rect = menuOpt.menuRect = rect style.drawControl(style.CE_MenuEmptyArea, menuOpt, qp, self) class Test(QtWidgets.QWidget): def __init__(self): super().__init__() self.menu = ScrollableMenu(maxItemCount=5) self.menu.addAction('test action') for i in range(10): self.menu.addAction('Action {}'.format(i + 1)) if i & 1: self.menu.addSeparator() subMenu = self.menu.addMenu('very long sub menu') subMenu.addAction('goodbye') self.menu.triggered.connect(self.menuTriggered) def menuTriggered(self, action): print(action.text()) def contextMenuEvent(self, event): self.menu.exec_(event.globalPos()) if __name__ == '__main__': import sys app = QtWidgets.QApplication(sys.argv) test = Test() test.show() sys.exit(app.exec_())
为此,您必须继承
QMenu
并重写wheelEvent
下面是一个示例,您可以对其进行改进
上述代码的解释:
将所有可见操作存储在一个列表中,例如
visible_lst
向下滚动:
self.visible_lst[0].setVisible(False)
将使该操作不可见,然后从前面弹出列表李>self.actions()[self.index]
将下一个操作附加到visible_lst
向上滚动:
self.visible_lst[-1].setVisible(False)
将隐藏列表中的最后一项,并从列表中弹出最后一个元素visible_lst
的第0个索引中,并使用self.actions()[index].setVisible(True)
使其可见滚动上下文菜单代码的输出:
读者,如果您有任何建议或疑问,请留言
处理菜单定制(使用任何框架)不是一项容易的任务,根据经验,我可以告诉您,尝试使用简单的方法,例如切换项目可见性,肯定会在用户体验中导致意外的行为和问题
必须牢记三个方面:
从您给出的答案中,我至少可以看到以下重要问题:
不幸的是,解决方案是正确地实现所需的一切,从绘画开始,显然,从用户交互开始
下面是一个几乎完整的可滚动菜单的实现;可以通过设置最大高度或
maxItemCount
关键字参数来启用滚动,该参数根据标准项猜测高度;然后通过移动箭头(和/或单击箭头)以及使用键盘箭头来激活它。它还不完善,可能还有一些方面我没有考虑(见上面的“懒惰调试”注释),但是对于我所看到的,它应该按照预期工作。p>
而且,是的,我知道,它真的扩展了;但是,如上所述,菜单并不像看上去那么简单
相关问题 更多 >
编程相关推荐