You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让PyQt部件事件提前触发?径向菜单右键交互异常排查

径向菜单首次右键按下无响应的解决方案

问题描述

编写了一个PyQt5径向菜单脚本,预期右键按下时菜单弹出,按住按键可选择工具,松开按键时菜单关闭。但遇到问题:首次右键按下时,菜单相关事件均未触发;移除松开关闭功能后,仍仅在首次按下时失效,松开后恢复正常。尝试过eventFilter但无效。

问题原因

首次右键按下时,主窗口TestWindow的mousePressEvent处理事件后未正确传递事件,导致径向菜单无法捕获后续鼠标移动事件;同时径向菜单的窗口标志缺少Qt.Popup,无法获得足够的事件优先级,导致首次按下时无法响应鼠标操作。

解决步骤及修改后的代码

关键修改点

  • 给RadialMenu添加Qt.Popup窗口标志,确保弹出窗口的事件优先级。
  • 在TestWindow的mousePressEvent中调用event.accept(),避免事件被拦截。
  • 菜单显示后主动捕获当前鼠标位置,更新激活区域。
  • 调整右键释放逻辑,确保菜单能正确响应松开动作。

修改后的完整代码

import math
import sys
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *


class RadialMenu(QWidget):
    WINDOW_WIDTH = 320
    WINDOW_HEIGHT = 200

    OUTER_CIRCLE_RADIUS = 50
    OUTER_CIRCLE_DIAMETER = 2 * OUTER_CIRCLE_RADIUS

    INNER_CIRCLE_RADIUS = 10
    INNER_CIRCLE_DIAMETER = 2 * INNER_CIRCLE_RADIUS

    TEXT_WIDTH = 80
    TEXT_HEIGHT = 24
    TEXT_RADIUS = 0.5 * TEXT_HEIGHT

    NUM_ZONES = 8
    ZONE_SPAN_ANGLE = 360 / NUM_ZONES

    def __init__(self, hotkey, parent=None):
        super(RadialMenu, self).__init__(parent)
        self.setFixedSize(self.WINDOW_WIDTH, self.WINDOW_HEIGHT)
        # 添加Qt.Popup标志,提升弹出窗口事件优先级
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup)
        self.setAttribute(Qt.WA_TranslucentBackground, True)
        self.setMouseTracking(True)
        self.hotkey = hotkey
        self.tools = [None] * self.NUM_ZONES

        self.center_point = QPoint(int(0.5 * self.width()), int(0.5 * self.height()))
        self.outer_circle_rect = QRect(self.center_point.x() - self.OUTER_CIRCLE_RADIUS,
                                              self.center_point.y() - self.OUTER_CIRCLE_RADIUS,
                                              self.OUTER_CIRCLE_DIAMETER, self.OUTER_CIRCLE_DIAMETER)
        self.inner_circle_rect = QRect(self.center_point.x() - self.INNER_CIRCLE_RADIUS,
                                              self.center_point.y() - self.INNER_CIRCLE_RADIUS,
                                              self.INNER_CIRCLE_DIAMETER, self.INNER_CIRCLE_DIAMETER)

        self.selected_section = -1
        self.active_zone = -1
        self.highlight_bounds = QRect(int(self.center_point.x() - 0.5 * self.height()), 0, int(self.height()),
                                             int(self.height()))
        self.highlight_start_angle = 0

        self.hotkey_down = False
        self.mouse_moved = False

    def set_tool(self, index, label, func):
        if index < 0 or index >= self.NUM_ZONES:
            return
        self.tools[index] = (label, func)

    def distance_from_center(self, x, y):
        return math.hypot(x - self.center_point.x(), y - self.center_point.y())

    def point_from_center(self, zone, distance):
        degrees = zone * self.ZONE_SPAN_ANGLE
        radians = math.radians(degrees)

        x1 = self.center_point.x() + (distance * math.cos(radians))
        y1 = self.center_point.y() + (distance * math.sin(radians))

        return x1, y1

    def label_point(self, zone):
        distance = self.OUTER_CIRCLE_RADIUS + self.TEXT_RADIUS
        x, y = self.point_from_center(zone, distance)

        if zone in [0, 1, 7]:
            x += 2
        elif zone in [2, 6]:
            x -= 0.5 * self.TEXT_WIDTH
        elif zone in [3, 4, 5]:
            x -= 2 + self.TEXT_WIDTH

        if zone in [0, 4]:
            y -= 0.5 * self.TEXT_HEIGHT
        elif zone in [1, 3]:
            y -= 0.3 * self.TEXT_HEIGHT
        elif zone in [5, 7]:
            y -= 0.7 * self.TEXT_HEIGHT
        elif zone == 2:
            y += 2
        elif zone == 6:
            y -= 2 + self.TEXT_HEIGHT

        return x, y

    def update_active_zone(self, pos):
        if self.distance_from_center(pos.x(), pos.y()) <= self.INNER_CIRCLE_RADIUS:
            self.clear_active_zone()
            return

        point = pos - self.center_point
        degrees = math.degrees(math.atan2(point.y(), point.x())) % 360

        start_angle_offset = -0.5 * self.ZONE_SPAN_ANGLE
        degrees = (degrees + start_angle_offset) % 360

        temp_section = math.floor(degrees / self.ZONE_SPAN_ANGLE)

        if self.selected_section != temp_section:
            self.selected_section = temp_section

            self.highlight_start_angle = (self.selected_section * self.ZONE_SPAN_ANGLE) - start_angle_offset
            self.active_zone = (self.selected_section + 1) % 8

            self.update()

    def clear_active_zone(self):
        self.active_zone = -1
        self.selected_section = -1

        self.update()

    def activate_selection(self):
        if self.active_zone >= 0 and self.tools[self.active_zone]:
            self.tools[self.active_zone][1]()
        self.hide()

    def show(self):
        pop_up_pos = QCursor.pos() - self.center_point
        self.move(pop_up_pos)
        super(RadialMenu, self).show()
        # 显示后立即捕获鼠标位置,更新激活区域
        self.update_active_zone(self.mapFromGlobal(QCursor.pos()))

    def showEvent(self, show_event):
        self.setFocus(Qt.PopupFocusReason)
        self.mouse_moved = False
        self.hotkey_down = True

    def keyPressEvent(self, key_event):
        key = key_event.key()
        if key == self.hotkey and not key_event.isAutoRepeat():
            self.hide()
        elif key == Qt.Key_Escape:
            self.hide()

    def keyReleaseEvent(self, key_event):
        if key_event.key() == self.hotkey and not key_event.isAutoRepeat():
            self.hotkey_down = False
            if self.mouse_moved:
                self.activate_selection()
            else:
                self.hide()

    def mousePressEvent(self, mouse_event):
        if self.mouse_moved or not self.hotkey_down:
            self.activate_selection()

    def mouseReleaseEvent(self, mouse_event):
        if self.mouse_moved:
            self.activate_selection()

    def mouseMoveEvent(self, mouse_event):
        self.update_active_zone(mouse_event.pos())
        if (mouse_event.pos() - self.center_point).manhattanLength() > self.INNER_CIRCLE_RADIUS:
            self.mouse_moved = True

    def leaveEvent(self, leave_event):
        self.clear_active_zone()

    def paintEvent(self, paint_event):
        painter = QPainter(self)
        painter.setRenderHints(QPainter.Antialiasing)

        # Clickable area
        painter.setPen(Qt.transparent)
        painter.setBrush(QColor(0, 0, 0, 1))
        painter.drawRect(self.rect())

        # Outer Circle
        radial_grad = QRadialGradient(self.center_point, 50)
        radial_grad.setColorAt(.1, QColor(0, 0, 0, 255))
        radial_grad.setColorAt(1, QColor(0, 0, 0, 1))
        painter.setBrush(radial_grad)

        circle_pen = QPen(Qt.cyan, 2)
        painter.setPen(circle_pen)
        painter.drawEllipse(self.outer_circle_rect)

        # Zone Highlight
        if self.active_zone >= 0:
            radial_grad2 = QRadialGradient(self.center_point, 80)
            radial_grad2.setColorAt(0, QColor(255, 255, 255, 255))
            radial_grad2.setColorAt(0.9, QColor(0, 0, 0, 1))
            painter.setBrush(radial_grad2)

            painter.setPen(Qt.transparent)

            painter.drawPie(self.highlight_bounds, int(-self.highlight_start_angle * 16),
                            int(-self.ZONE_SPAN_ANGLE * 16))

        # Inner Circle
        painter.setBrush(Qt.black)
        painter.setPen(circle_pen)
        painter.drawEllipse(self.inner_circle_rect)

        for i in range(self.NUM_ZONES):
            if self.tools[i]:
                if i == self.active_zone:
                    painter.setBrush(QColor(255, 255, 0, 127))
                else:
                    painter.setBrush(QColor(0, 0, 0, 127))

                label_x, label_y = self.label_point(i)

                painter.setPen(Qt.transparent)
                painter.drawRoundedRect(int(label_x), int(label_y), int(self.TEXT_WIDTH), int(self.TEXT_HEIGHT),
                                        int(self.TEXT_RADIUS), int(self.TEXT_RADIUS))

                painter.setPen(Qt.white)
                painter.drawText(int(label_x), int(label_y), int(self.TEXT_WIDTH), int(self.TEXT_HEIGHT),
                                 Qt.AlignCenter, self.tools[i][0])

    def focusOutEvent(self, focus_event):
        self.hide()


class TestWindow(QMainWindow):
    def __init__(self, parent=None):
        super(TestWindow, self).__init__(parent=None)
        self.setWindowTitle("Radial Menu Example")
        self.setFixedSize(960, 540)
        self.setStyleSheet("background-color: #2c2f33;")
        self.radial_menu = RadialMenu(Qt.RightButton)
        for i in range(0, RadialMenu.NUM_ZONES):
            self.radial_menu.set_tool(i, "Tool {0}".format(i), lambda index=i: self.activate_tool(index))

    def mousePressEvent(self, event):
        if event.button() == Qt.RightButton:
            self.radial_menu.show()
            # 接受事件,避免被其他控件拦截
            event.accept()

    def mouseReleaseEvent(self, event):
        if event.button() == Qt.RightButton:
            # 触发菜单的释放逻辑
            self.radial_menu.keyReleaseEvent(QKeyEvent(QEvent.KeyRelease, Qt.RightButton, Qt.NoModifier))

    def activate_tool(self, tool_index):
        print("Activate tool for index: {0}".format(tool_index))


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = TestWindow()
    window.show()
    app.exec_()

说明

  • 添加Qt.Popup标志后,径向菜单会被系统识别为弹出式窗口,自动获得焦点并拦截鼠标事件,解决首次按下时事件无法传递的问题。
  • 菜单显示后主动捕获鼠标位置,确保激活区域能实时更新。
  • 主窗口的右键释放事件主动调用菜单的释放逻辑,保证松开右键时菜单能正确执行选中操作或关闭。

内容的提问来源于stack exchange,提问作者Vlad

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 10:17:02