带有 Pyside/PyQt 的径向菜单

Posted

技术标签:

【中文标题】带有 Pyside/PyQt 的径向菜单【英文标题】:Radial Menu With Pyside/PyQt 【发布时间】:2020-02-19 22:22:39 【问题描述】:

基本上我正在尝试创建一个像这样的径向菜单

我使用的是 QPainter,这是我的尝试。但我不知道如何在像素图上添加点击事件。有没有可用的库?

Images Link

from PySide2 import QtWidgets, QtGui, QtCore
import sys
import os
RESPATH = "/your_folder/radialMenu/res"

class RadialMenu(QtWidgets.QGraphicsRectItem):
    addButton     = 1
    disableButton = 2
    clearButton   = 3
    exportButton  = 4
    infoButton    = 5
    runButton     = 6
    scriptsButton = 7
    def __init__(self, parent=None):
        super(RadialMenu, self).__init__(parent)

    def paint(self, painter, option, widget=None):
        painter.setRenderHint(QtGui.QPainter.Antialiasing)
        painter.setBrush(QtGui.QBrush(QtGui.QColor(71, 71, 71, 0)))
        tempPen =  QtGui.QPen(QtGui.QColor(178, 141, 58), 20.0, QtCore.Qt.CustomDashLine)
        tempPen.setDashPattern([4, 4, 4, 4])
        painter.setPen(tempPen)
        painter.drawEllipse(0, 0, 150, 150)
        topX = 0
        topY = 0
        pixmap1 =  QtGui.QPixmap(os.path.join(RESPATH, "add.png"))
        painter.drawPixmap(topX + 50, topY - 8, pixmap1)
        pixmap2 = QtGui.QPixmap(os.path.join(RESPATH, "disable.png"))
        painter.drawPixmap(topX + 90, topY - 5, pixmap2)
        pixmap3 = QtGui.QPixmap(os.path.join(RESPATH, "clear.png"))
        painter.drawPixmap(topX - 10, topY + 70, pixmap3)
        pixmap4 = QtGui.QPixmap(os.path.join(RESPATH, "export.png"))
        pixmap4 = pixmap4.transformed(QtGui.QTransform().rotate(15))
        painter.drawPixmap(topX - 2, topY + 100, pixmap4)
        pixmap5 = QtGui.QPixmap(os.path.join(RESPATH, "info.png"))
        painter.drawPixmap(topX + 20, topY + 125, pixmap5)
        pixmap6 = QtGui.QPixmap(os.path.join(RESPATH, "run.png"))
        painter.drawPixmap(topX + 113, topY + 125, pixmap6)
        pixmap6 = QtGui.QPixmap(os.path.join(RESPATH, "scripts.png"))
        painter.drawPixmap(topX + 137, topY + 85, pixmap6)

class RadialTest(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        self.scene=QtWidgets.QGraphicsScene(self)
        buttonItem = RadialMenu()
        self.scene.addItem(buttonItem)
        buttonItem.setPos(100,100)
        buttonItem.setZValue(1000)
        self.scene.update()
        self.view = QtWidgets.QGraphicsView(self.scene, self)
        self.scene.setSceneRect(0, 0, 300, 300)
        self.setGeometry(50, 50, 305, 305)
        self.show()

if __name__ == "__main__":
    app=QtWidgets.QApplication(sys.argv)
    firstScene = RadialTest()
    sys.exit(app.exec_())

这段代码会给出这个结果

【问题讨论】:

你能分享一下.png @eyllanesc 也添加了图片链接 @eyllanesc 我刚刚添加了一个完整的工作代码.. 但没有任何操作 【参考方案1】:

对于此类对象,始终建议保持分层对象结构。此外,在处理可能具有“固定”尺寸的对象(如图像,但不仅如此)时,固定定位可能会很棘手,特别是对于支持不同 DPI 屏幕值的较新系统。

通过这种方法,我根本不使用映射图像(但仍然可以设置按钮图标),而是选择使用纯几何概念,为每个按钮使用像素“半径”值和角度。

from PyQt5 import QtWidgets, QtGui, QtCore
from math import sqrt

class RadialMenu(QtWidgets.QGraphicsObject):
    buttonClicked = QtCore.pyqtSignal(int)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setAcceptHoverEvents(True)
        self.buttons = 

    def addButton(self, id, innerRadius, size, startAngle, angleSize, pen=None, 
                  brush=None, icon=None):
        # if a button already exists with the same id, remove it
        if id in self.buttons:
            oldItem = self.buttons.pop(id)
            if self.scene():
                self.scene().removeItem(oldItem)
            oldItem.setParent(None)

        # compute the extents of the inner and outer "circles"
        startRect = QtCore.QRectF(
            -innerRadius, -innerRadius, innerRadius * 2, innerRadius * 2)
        outerRadius = innerRadius + size
        endRect = QtCore.QRectF(
            -outerRadius, -outerRadius, outerRadius * 2, outerRadius * 2)

        # create the circle section path
        path = QtGui.QPainterPath()
        # move to the start angle, using the outer circle
        path.moveTo(QtCore.QLineF.fromPolar(outerRadius, startAngle).p2())
        # draw the arc to the end of the angle size
        path.arcTo(endRect, startAngle, angleSize)
        # draw a line that connects to the inner circle
        path.lineTo(QtCore.QLineF.fromPolar(innerRadius, startAngle + angleSize).p2())
        # draw the inner circle arc back to the start angle
        path.arcTo(startRect, startAngle + angleSize, -angleSize)
        # close the path back to the starting position; theoretically unnecessary,
        # but better safe than sorry
        path.closeSubpath()

        # create a child item for the "arc"
        item = QtWidgets.QGraphicsPathItem(path, self)
        item.setPen(pen if pen else (QtGui.QPen(QtCore.Qt.transparent)))
        item.setBrush(brush if brush else QtGui.QColor(180, 140, 70))
        self.buttons[id] = item

        if icon is not None:
            # the maximum available size is at 45 degrees, use the Pythagorean
            # theorem to compute it and create a new pixmap based on the icon
            iconSize = int(sqrt(size ** 2 / 2))
            pixmap = icon.pixmap(iconSize)
            # create the child icon (pixmap) item
            iconItem = QtWidgets.QGraphicsPixmapItem(pixmap, self)
            # push it above the "arc" item
            iconItem.setZValue(item.zValue() + 1)
            # find the mid of the angle and put the icon there
            midAngle = startAngle + angleSize / 2
            iconPos = QtCore.QLineF.fromPolar(innerRadius + size * .5, midAngle).p2()
            iconItem.setPos(iconPos)
            # use the center of the pixmap as the offset for centering
            iconItem.setOffset(-pixmap.rect().center())

    def itemAtPos(self, pos):
        for button in self.buttons.values():
            if button.shape().contains(pos):
                return button

    def checkHover(self, pos):
        hoverButton = self.itemAtPos(pos)
        for button in self.buttons.values():
            # set a visible border only for the hovered item
            button.setPen(QtCore.Qt.red if button == hoverButton else QtCore.Qt.transparent)

    def hoverEnterEvent(self, event):
        self.checkHover(event.pos())

    def hoverMoveEvent(self, event):
        self.checkHover(event.pos())

    def hoverLeaveEvent(self, event):
        for button in self.buttons.values():
            button.setPen(QtCore.Qt.transparent)

    def mousePressEvent(self, event):
        clickButton = self.itemAtPos(event.pos())
        if clickButton:
            for id, btn in self.buttons.items():
                if btn == clickButton:
                    self.buttonClicked.emit(id)

    def boundingRect(self):
        return self.childrenBoundingRect()

    def paint(self, qp, option, widget):
        # required for QGraphicsObject subclasses
        pass

ButtonData = [
    (50, 40, QtWidgets.QStyle.SP_MessageBoxInformation), 
    (90, 40, QtWidgets.QStyle.SP_MessageBoxQuestion), 
    (180, 20, QtWidgets.QStyle.SP_FileDialogBack), 
    (200, 20, QtWidgets.QStyle.SP_DialogOkButton), 
    (220, 20, QtWidgets.QStyle.SP_DialogOpenButton), 
    (290, 30, QtWidgets.QStyle.SP_ArrowDown), 
    (320, 30, QtWidgets.QStyle.SP_ArrowUp), 
]

class RadialTest(QtWidgets.QWidget):
    def __init__(self):
        QtWidgets.QWidget.__init__(self)
        self.scene = QtWidgets.QGraphicsScene(self)

        buttonItem = RadialMenu()
        self.scene.addItem(buttonItem)
        buttonItem.buttonClicked.connect(self.buttonClicked)
        for index, (startAngle, extent, icon) in enumerate(ButtonData):
            icon = self.style().standardIcon(icon, None, self)
            buttonItem.addButton(index, 64, 20, startAngle, extent, icon=icon)

        buttonItem.setPos(150, 150)
        buttonItem.setZValue(1000)

        self.view = QtWidgets.QGraphicsView(self.scene, self)
        self.view.setRenderHints(QtGui.QPainter.Antialiasing)
        self.scene.setSceneRect(0, 0, 300, 300)
        self.setGeometry(50, 50, 305, 305)
        self.show()

    def buttonClicked(self, id):
        print('Button id  has been clicked'.format(id))

结果如下:

【讨论】:

完美.. 非常感谢,这正是我想要的。

以上是关于带有 Pyside/PyQt 的径向菜单的主要内容,如果未能解决你的问题,请参考以下文章

CSS 径向菜单

如何创建一个新的窗口按钮 PySide/PyQt?

颤振中的侧面径向菜单

如何在 CSS 中创建径向菜单?

PySide/PyQt:PointingHandCursor 食谱?

Raphael JS 的径向饼图菜单