Qt 实现动态菜单源码分享
一、效果展示 二、源码分享 1、fanexpandmenu.h 2、fanexpandmenu.cpp 3、mainWindow.cpp 三、实现原理 1、Qt6 事件系统概述 2、 截图工具中的关键事件详解 2.1 、paintEvent - 绘图事件 2.2、 鼠标事件三部曲 mousePressEvent - 鼠标按下 mouseMoveEvent - 鼠标移动 mouseReleaseEvent - 鼠标释放 3、事件与信号槽的协同 3.1、 事件处理流程 3.2 、关键技术点 3.2.1 、全屏遮罩窗口 3.2.2 、屏幕捕获 3.2.3、 图像处理 4、Qt6 事件系统的新特性 4.1、 改进的事件过滤器 4.2、 手势事件支持 4.3、 输入法事件优化 5、性能优化建议
一、效果展示



二、源码分享
1、fanexpandmenu.h
#ifndef FANEXPANDMENU_H#define FANEXPANDMENU_H#include<QWidget>#include<QPainter>#include<QTimer>#include<QPixmap>#include<QMouseEvent>#include<cmath>class FanExpandMenu : public QWidget{Q_OBJECTpublic:explicitFanExpandMenu(QWidget *parent = nullptr);protected:voidpaintEvent(QPaintEvent *event)override;voidmousePressEvent(QMouseEvent *event)override;private slots:void _on_timer_tick();private:void _calc_mid_circle();QPointF get_icon_pos(double angle);voiddraw_circle_icon(QPainter* painter, double angle, const QPixmap& pixmap);// 角度参数double collapse_angle = 150.0;double expand1 = 210.0;double expand2 = 270.0;double expand3 = 330.0;// 动画参数double angle_step = 1.2;int frame_delay = 12;int timer_interval = 5;bool is_expand = false;// 尺寸常量int icon_circle_size = 64;int icon_inner_size = 32;int bar_radius = 12;double expand_radius_ratio = 3.8;// 图标角度状态double icon1_angle;double icon2_angle;double icon3_angle;int anim_frame_count = 0;QTimer* anim_timer;// 图片资源QPixmap img_home;QPixmap img_profile;QPixmap img_add;QPixmap img_icon1;QPixmap img_icon2;QPixmap img_icon3;// 中间按钮缓存坐标double mid_circle_size;double mid_circle_x;double mid_circle_y;double mid_center_x;double mid_center_y;double expand_radius;};#endif// FANEXPANDMENU_H
2、fanexpandmenu.cpp
#include<QApplication>#include<QMainWindow>#include<QVBoxLayout>#include"FanExpandMenu.h"FanExpandMenu::FanExpandMenu(QWidget *parent): QWidget(parent), anim_timer(new QTimer(this)){setFixedSize(600, 400);setAttribute(Qt::WA_TranslucentBackground);// 图标初始折叠角度icon1_angle = collapse_angle;icon2_angle = collapse_angle;icon3_angle = collapse_angle;// 定时器配置anim_timer->setInterval(timer_interval);connect(anim_timer, &QTimer::timeout, this, &FanExpandMenu::_on_timer_tick);// 加载图片(使用资源文件建议改为 ":/image/xxx.svg")img_home.load(":/image/image/voice.svg");img_profile.load(":/image/image/user.svg");img_add.load(":/image/image/add.svg");img_icon1.load(":/image/image/icon1.svg");img_icon2.load(":/image/image/icon2.svg");img_icon3.load(":/image/image/icon3.svg");_calc_mid_circle();}void FanExpandMenu::_calc_mid_circle(){double h = height();mid_circle_size = h / 3.0;mid_circle_x = width() / 2.0 - mid_circle_size / 2.0;mid_circle_y = h - mid_circle_size - h / 12.0;mid_center_x = mid_circle_x + mid_circle_size / 2.0;mid_center_y = mid_circle_y + mid_circle_size / 2.0;expand_radius = h / expand_radius_ratio;}void FanExpandMenu::_on_timer_tick(){bool all_done = true;anim_frame_count++;if (is_expand){// 展开:3先动 → 2延迟 → 1延迟最多if (icon3_angle < expand3){icon3_angle += angle_step;all_done = false;}if (icon2_angle < expand2 && icon3_angle >= expand1){icon2_angle += angle_step;all_done = false;}if (icon1_angle < expand1 && icon3_angle >= expand2){icon1_angle += angle_step;all_done = false;}// 上限保护icon1_angle = std::min(icon1_angle, expand1);icon2_angle = std::min(icon2_angle, expand2);icon3_angle = std::min(icon3_angle, expand3);}else{// 收起:1先收,2其次,3最后if (icon1_angle > collapse_angle){icon1_angle -= angle_step;all_done = false;}if (icon2_angle > collapse_angle){icon2_angle -= angle_step;all_done = false;}if (icon3_angle > collapse_angle){icon3_angle -= angle_step;all_done = false;}// 下限保护icon1_angle = std::max(icon1_angle, collapse_angle);icon2_angle = std::max(icon2_angle, collapse_angle);icon3_angle = std::max(icon3_angle, collapse_angle);}if (all_done){anim_timer->stop();anim_frame_count = 0;}update();}voidFanExpandMenu::mousePressEvent(QMouseEvent *event){QRectF mid_rect(mid_circle_x, mid_circle_y, mid_circle_size, mid_circle_size);if (mid_rect.contains(event->position())){is_expand = !is_expand;anim_frame_count = 0;anim_timer->start();}QWidget::mousePressEvent(event);}QPointF FanExpandMenu::get_icon_pos(double angle){double rad = qDegreesToRadians(angle);double offset_x = expand_radius * std::cos(rad);double offset_y = expand_radius * std::sin(rad);double x = mid_center_x + offset_x - icon_circle_size / 2.0;double y = mid_center_y + offset_y - icon_circle_size / 2.0;return QPointF(x, y);}voidFanExpandMenu::draw_circle_icon(QPainter *painter, double angle, const QPixmap &pixmap){QPointF pos = get_icon_pos(angle);QRectF circle_rect(pos.x(), pos.y(), icon_circle_size, icon_circle_size);painter->setBrush(Qt::black);painter->drawEllipse(circle_rect);double inner_offset = (icon_circle_size - icon_inner_size) / 2.0;QRectF icon_rect(pos.x() + inner_offset,pos.y() + inner_offset,icon_inner_size,icon_inner_size);// 补齐源pixmap rect,消除drawPixmap编译报错painter->drawPixmap(icon_rect, pixmap, pixmap.rect());}voidFanExpandMenu::paintEvent(QPaintEvent *event){Q_UNUSED(event);QPainter painter(this);painter.setRenderHint(QPainter::Antialiasing);painter.setPen(Qt::NoPen);double w = width();double h = height();// 1. 绘制三个扇形悬浮图标draw_circle_icon(&painter, icon1_angle, img_icon1);draw_circle_icon(&painter, icon2_angle, img_icon2);draw_circle_icon(&painter, icon3_angle, img_icon3);// 底部浅灰白导航栏double bar_height = h * 0.2;double bar_y = h - bar_height - 5;double bar_margin = 60;QRectF bar_rect(bar_margin, bar_y, w - bar_margin * 2, bar_height);painter.setBrush(QColor("#f0f0f0"));painter.drawRoundedRect(bar_rect, bar_radius, bar_radius);// 导航栏左右图标double icon_bar_h = bar_height * 0.6;double icon_bar_y = bar_y + (bar_height - icon_bar_h) / 2.0;double icon_w = 48;// 左侧homeQRectF home_rect(bar_margin + 30, icon_bar_y, icon_w, icon_bar_h);painter.drawPixmap(home_rect, img_home, img_home.rect());// 右侧用户QRectF prof_rect(w - bar_margin - 30 - icon_w, icon_bar_y, icon_w, icon_bar_h);painter.drawPixmap(prof_rect, img_profile, img_profile.rect());// 中间紫色加号圆形按钮QRectF mid_rect(mid_circle_x, mid_circle_y + 6, mid_circle_size, mid_circle_size);painter.setBrush(QColor("#b820f0"));painter.drawEllipse(mid_rect);// 加号图标居中double add_scale = mid_circle_size * 0.6;QRectF add_rect(mid_center_x - add_scale / 2.0,mid_center_y - add_scale / 2.0 + 12,add_scale, add_scale);painter.drawPixmap(add_rect, img_add, img_add.rect());}
3、mainWindow.cpp
#include"mainwindow.h"#include<QCoreApplication>#include<QMetaObject>#include<QMetaProperty>#include<QMetaMethod>#include<QDebug>#include"fanexpandmenu.h"MainWindow::MainWindow(QWidget *parent): QMainWindow(parent){setWindowTitle("扇形展开菜单 Qt6 C++");resize(900, 650);QWidget* centralWidget = new QWidget(this);setCentralWidget(centralWidget);centralWidget->setStyleSheet("background-color: #b820f0;");QVBoxLayout* layout = new QVBoxLayout(centralWidget);layout->setAlignment(Qt::AlignCenter);layout->setContentsMargins(0, 0, 0, 0);FanExpandMenu* menu = new FanExpandMenu();layout->addWidget(menu);}MainWindow::~MainWindow(){}
三、实现原理
本截图工具的核心实现依赖于 Qt6 的事件系统(Event System)和绘图系统(Painting System)。下面详细解析关键事件的工作原理:
1、Qt6 事件系统概述
Qt 采用事件驱动的编程模型,所有用户交互(鼠标点击、键盘输入、窗口重绘等)都通过事件(Event)来传递和处理。事件处理流程如下:
- 事件产生
:由操作系统或 Qt 内部产生 - 事件派发
:通过 QApplication::notify()派发到目标对象 - 事件过滤
:可通过 installEventFilter()进行预处理 - 事件处理
:目标对象的 event()方法接收并分发给特定事件处理器
2、 截图工具中的关键事件详解
2.1 、paintEvent - 绘图事件
voidCaptureMask::paintEvent(QPaintEvent *){QPainter painter(this);painter.setRenderHint(QPainter::Antialiasing);// 1. 绘制完整屏幕截图作为背景painter.drawPixmap(rect(), m_screenBg);// 2. 全局半透明黑色遮罩(实现变暗效果)QColor darkColor(0, 0, 0, 150); // RGBA: 黑色,透明度150/255painter.fillRect(rect(), darkColor);if (m_isDragging){QRect selectRect = QRect(m_startPos, m_endPos).normalized();// 3. 选区区域重新绘制原图(挖空效果)painter.drawPixmap(selectRect, m_screenBg, selectRect);// 4. 绘制选区边框和半透明填充painter.setPen(QPen(QColor(0, 180, 255), 2)); // 蓝色边框painter.setBrush(QColor(0, 160, 255, 60)); // 浅蓝色半透明填充painter.drawRect(selectRect);}}
原理说明:
paintEvent在以下情况自动触发: 窗口首次显示 窗口被其他窗口遮挡后重新显示 调用 update()或repaint()方法窗口大小改变 本工具中,每次鼠标移动( mouseMoveEvent)都会调用update(),从而触发重绘通过分层绘制实现选区高亮效果: 底层:完整屏幕截图 中层:全局半透明黑色遮罩(变暗效果) 上层:选区区域的原图(挖空效果)+ 蓝色边框
2.2、 鼠标事件三部曲
mousePressEvent - 鼠标按下
void CaptureMask::mousePressEvent(QMouseEvent *event){m_isDragging = true; // 开始拖拽状态m_startPos = event->pos(); // 记录起始位置m_endPos = m_startPos; // 初始结束位置=起始位置update(); // 触发重绘(绘制起始点)}
mouseMoveEvent - 鼠标移动
void CaptureMask::mouseMoveEvent(QMouseEvent *event){if (!m_isDragging) return; // 非拖拽状态不处理m_endPos = event->pos(); // 更新结束位置update(); // 触发重绘(更新选区矩形)}
mouseReleaseEvent - 鼠标释放
void CaptureMask::mouseReleaseEvent(QMouseEvent *){m_isDragging = false; // 结束拖拽状态// 计算标准化矩形(确保左上角到右下角)QRect rect = QRect(m_startPos, m_endPos).normalized();// 从原图中截取选区QPixmap result = m_screenBg.copy(rect);// 发射信号通知截图完成emit captureFinished(result);// 关闭遮罩窗口this->close();}
事件传递机制:
Qt 使用 QMouseEvent封装鼠标事件信息event->pos()返回相对于当前窗口的坐标 normalized()确保矩形坐标是标准化的(左上角到右下角)
3、事件与信号槽的协同
3.1、 事件处理流程
用户按下鼠标 → mousePressEvent↓用户拖动鼠标 → mouseMoveEvent → update() → paintEvent↓用户释放鼠标 → mouseReleaseEvent↓发射 captureFinished 信号 → MainWindow::slotOnCaptureDone
3.2 、关键技术点
3.2.1 、全屏遮罩窗口
// 窗口标志设置Qt::Window | // 作为独立窗口Qt::FramelessWindowHint | // 无边框Qt::WindowStaysOnTopHint // 始终置顶// 窗口属性setAttribute(Qt::WA_DeleteOnClose); // 关闭时自动删除setAttribute(Qt::WA_TranslucentBackground, false); // 不透明背景
3.2.2 、屏幕捕获
// 获取主屏幕QScreen *screen = QApplication::primaryScreen();// 捕获整个屏幕(包括所有窗口)QPixmap fullPix = screen->grabWindow(0); // 0表示整个屏幕
3.2.3、 图像处理
QPixmap::copy(const QRect &):截取指定区域 QPixmap::save():保存到文件 QClipboard::setPixmap():复制到剪贴板
4、Qt6 事件系统的新特性
4.1、 改进的事件过滤器
Qt6 增强了事件过滤器的性能,支持更精细的事件拦截和处理。
4.2、 手势事件支持
新增对触摸屏和手势的更好支持,虽然本工具未使用,但在移动端开发中很重要。
4.3、 输入法事件优化
对多语言输入法的支持更加完善。
5、性能优化建议
减少不必要的重绘:
只在选区变化时调用 update()使用 update(QRect)只重绘脏区域内存管理:
大尺寸截图及时释放 使用 QPixmapCache缓存常用图像响应式设计:
在高DPI屏幕上使用 devicePixelRatio适配支持多屏幕截图

夜雨聆风