我有一个辐射菜单的脚本,旨在当用户按下鼠标右键时出现。按住按钮时,用户可以选择“工具”。辐射菜单设计为当用户释放鼠标按钮时关闭。
但是,我的脚本遇到了问题。当我第一次按下右键单击按钮时,与辐射按钮相关的事件似乎都没有运行。为了排除故障,我决定删除按钮释放时的关闭功能。令人惊讶的是,我发现只有当我最初按下右键按钮时,我的事件才无法执行。奇怪的是,当我松开按钮时,一切都开始正常工作。我怀疑这种行为与 Qt 处理事件的方式有关。
我尝试通过实现 eventFilter 来解决此问题,但似乎没有帮助。有人可以解释一下我如何实现所需的功能吗?我希望在按下右键按钮时出现辐射菜单,允许用户选择一个“工具”,然后在释放按钮时自动关闭。
这是我迄今为止编写的脚本的示例:
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)
self.setWindowFlags(Qt.FramelessWindowHint)
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()
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()
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):
print('check')
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_menus = []
radial_menu = RadialMenu(Qt.RightButton)
for i in range(0, RadialMenu.NUM_ZONES):
radial_menu.set_tool(i, "Tool {0}".format(i), lambda index=i: self.activate_tool(index))
self.radial_menus.append(radial_menu)
def mousePressEvent(self, event):
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.RightButton:
for radial_menu in self.radial_menus:
if event.button() == radial_menu.hotkey:
radial_menu.show()
def mouseReleaseEvent(self, event):
if event.type() == QEvent.MouseButtonRelease:
if event.button() == Qt.RightButton:
for radial_menu in self.radial_menus:
if event.button() == radial_menu.hotkey:
pass
#radial_menu.close()
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_()
小部件只有在成为“鼠标抓取器”时才能获取鼠标移动事件,只有当它本身接收到鼠标事件时才会发生或
grabMouse()
被调用。
由于合成进一步的鼠标按下并将其发布到事件队列并不能保证小部件会在实际显示后之后收到它,一种解决方案可能是在 grabMouse()
覆盖中调用
show()
(或
showEvent()
一个)。请注意,一旦小部件隐藏,您
必须也调用releaseMouse()
def show(self):
...
self.grabMouse()
def hideEvent(self, event):
self.releaseMouse()
不过,更合适的方法可能是使用 Qt.Popup
window flag,它间接捕获窗口上的鼠标,类似于 QComboBox 弹出窗口的情况。请注意,可能需要在
self.update()
内显式调用
show()
。
class RadialMenu(QWidget):
...
def __init__(self, hotkey, parent=None):
...
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup)
def show(self):
...
self.update()