乐于分享
好东西不私藏

Qt 实现动态菜单源码分享

Qt 实现动态菜单源码分享

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(600400);    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;    // 左侧home    QRectF 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(900650);    QWidget* centralWidget = new QWidget(this);    setCentralWidget(centralWidget);    centralWidget->setStyleSheet("background-color: #b820f0;");    QVBoxLayout* layout = new QVBoxLayout(centralWidget);    layout->setAlignment(Qt::AlignCenter);    layout->setContentsMargins(0000);    FanExpandMenu* menu = new FanExpandMenu();    layout->addWidget(menu);}MainWindow::~MainWindow(){}

三、实现原理

本截图工具的核心实现依赖于 Qt6 的事件系统(Event System)和绘图系统(Painting System)。下面详细解析关键事件的工作原理:

1、Qt6 事件系统概述

Qt 采用事件驱动的编程模型,所有用户交互(鼠标点击、键盘输入、窗口重绘等)都通过事件(Event)来传递和处理。事件处理流程如下:

  1. 事件产生
    :由操作系统或 Qt 内部产生
  2. 事件派发
    :通过 QApplication::notify() 派发到目标对象
  3. 事件过滤
    :可通过 installEventFilter() 进行预处理
  4. 事件处理
    :目标对象的 event() 方法接收并分发给特定事件处理器

2、 截图工具中的关键事件详解

2.1 、paintEvent - 绘图事件

voidCaptureMask::paintEvent(QPaintEvent *){    QPainter painter(this);    painter.setRenderHint(QPainter::Antialiasing);    // 1. 绘制完整屏幕截图作为背景    painter.drawPixmap(rect(), m_screenBg);    // 2. 全局半透明黑色遮罩(实现变暗效果)    QColor darkColor(000150);  // RGBA: 黑色,透明度150/255    painter.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(0180255), 2));      // 蓝色边框        painter.setBrush(QColor(016025560));        // 浅蓝色半透明填充        painter.drawRect(selectRect);    }}

原理说明

  • paintEvent
     在以下情况自动触发: 
    • 窗口首次显示
    • 窗口被其他窗口遮挡后重新显示
    • 调用 update() 或 repaint() 方法
    • 窗口大小改变
  • 本工具中,每次鼠标移动(mouseMoveEvent)都会调用 update(),从而触发重绘
  • 通过分层绘制实现选区高亮效果: 
    1. 底层:完整屏幕截图
    2. 中层:全局半透明黑色遮罩(变暗效果)
    3. 上层:选区区域的原图(挖空效果)+ 蓝色边框

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_TranslucentBackgroundfalse); // 不透明背景

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、性能优化建议

  1. 减少不必要的重绘

    • 只在选区变化时调用 update()
    • 使用 update(QRect) 只重绘脏区域
  2. 内存管理

    • 大尺寸截图及时释放
    • 使用 QPixmapCache 缓存常用图像
  3. 响应式设计

    • 在高DPI屏幕上使用 devicePixelRatio 适配
    • 支持多屏幕截图