Qt 实现魔法导航菜单源码分享

Qt 实现魔法导航菜单源码分享

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

一、效果展示

二、源码分享

1、bottomnavwidget.h

cpp 复制代码
#ifndef BOTTOMNAVWIDGET_H
#define BOTTOMNAVWIDGET_H

#include <QWidget>
#include <QPainter>
#include <QTimer>
#include <QPixmap>
#include <QFont>

struct NavItem
{
    QString iconPath;
    QString text;
};

class BottomNavWidget : public QWidget
{
    Q_OBJECT
public:
    explicit BottomNavWidget(QWidget *parent = nullptr);

protected:
    void paintEvent(QPaintEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;

private slots:
    void _anim_tick();

private:
    void _calc_layout();
    double get_progress() const;
    double lerp(double a, double b, double t) const;

    // 配色
    QColor root_bg;
    QColor bar_bg;
    int bar_radius;
    QColor indicator_inner_color;
    QColor font_color;

    // 动画参数
    const int anim_duration = 200;
    const int frame_interval = 8;
    const int step_total = anim_duration / frame_interval;

    // 导航数据
    QVector<NavItem> nav_data;
    int count = 0;
    QVector<QPixmap> icon_pixmaps;

    // 选中索引
    int index = 2;
    int target_index = 2;
    int anim_frame = 0;
    bool is_animating = false;

    // 布局常量
    int bar_side_margin = 20;
    double bar_h = 0.0;
    double bar_y = 0.0;
    double item_w = 0.0;
    double indicator_diameter = 0.0;
    double icon_up_offset = 0.0;
    double text_h = 0.0;
    double text_hidden_y = 0.0;
    double text_show_y = 0.0;

    // 动画定时器
    QTimer* anim_timer;
};

#endif // BOTTOMNAVWIDGET_H

2、bottomnavwidget.cpp

cpp 复制代码
#include <QApplication>
#include <QMainWindow>
#include <QVBoxLayout>
#include <QMouseEvent>
#include "BottomNavWidget.h"

BottomNavWidget::BottomNavWidget(QWidget *parent)
    : QWidget(parent), anim_timer(new QTimer(this))
{
    setFixedSize(460, 130);
    setMouseTracking(true);

    // 配色初始化
    root_bg = QColor("#020414");
    bar_bg = QColor("#ffffff");
    bar_radius = 15;
    indicator_inner_color = QColor("#2AFC55");
    font_color = QColor("#000000");

    // 导航数据
    nav_data = {
                 {":/image/image/11.svg", "记事本"},
                 {":/image/image/22.svg", "统计"},
                 {":/image/image/33.svg", "导航"},
                 {":/image/image/44.svg", "QQ"},
                 {":/image/image/55.svg", "微信"},
                 };
    count = nav_data.size();

    // 加载图标
    for (const auto& item : nav_data)
    {
        QPixmap pix(item.iconPath);
        icon_pixmaps.append(pix);
    }

    // 默认选中第三个导航
    index = 2;
    target_index = 2;
    anim_frame = 0;
    is_animating = false;

    bar_side_margin = 20;

    // 定时器配置
    anim_timer->setInterval(frame_interval);
    connect(anim_timer, &QTimer::timeout, this, &BottomNavWidget::_anim_tick);

    _calc_layout();
}

void BottomNavWidget::_calc_layout()
{
    double w = width();
    double h = height();
    // 底部白色栏高度
    bar_h = h / 3 * 2;
    bar_y = h - bar_h;
    item_w = (w - bar_side_margin * 2) / count;
    // 绿色大圆尺寸
    indicator_diameter = bar_h * 1.15 - 10;
    // 图标上浮高度
    icon_up_offset = h / 3.5;
    // 文字尺寸
    text_h = h / 8;
    // 文字初始位置:控件底部外面,完全看不见
    text_hidden_y = h;
    // 文字弹出目标位置:白色栏下方、绿色大圆底部
    text_show_y = h - 20;
}

void BottomNavWidget::_anim_tick()
{
    anim_frame++;
    if (anim_frame >= step_total)
    {
        index = target_index;
        is_animating = false;
        anim_timer->stop();
        anim_frame = 0;
    }
    update();
}

void BottomNavWidget::mousePressEvent(QMouseEvent *event)
{
    double x = event->position().x() - bar_side_margin;
    int click_idx = static_cast<int>(x / item_w);
    if (click_idx >= 0 && click_idx < count && !is_animating)
    {
        if (target_index != click_idx)
        {
            target_index = click_idx;
            is_animating = true;
            anim_frame = 0;
            anim_timer->start();
        }
    }
    QWidget::mousePressEvent(event);
}

double BottomNavWidget::get_progress() const
{
    if (!is_animating)
        return 1.0;
    return static_cast<double>(anim_frame) / step_total;
}

double BottomNavWidget::lerp(double a, double b, double t) const
{
    return a + (b - a) * t;
}

void BottomNavWidget::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);
    QPainter painter(this);
    // 抗锯齿渲染
    painter.setRenderHints(QPainter::Antialiasing
                           | QPainter::TextAntialiasing
                           | QPainter::SmoothPixmapTransform);

    double w = width();
    double h = height();
    painter.setPen(Qt::NoPen);

    // 1. 全局黑色背景
    painter.setBrush(root_bg);
    painter.drawRect(rect());

    // 2. 底部白色圆角导航栏
    QRectF bar_rect(0, bar_y, w, bar_h);
    painter.setBrush(bar_bg);
    painter.drawRoundedRect(bar_rect, bar_radius, bar_radius);

    double t = get_progress();
    int curr_idx = index;
    int target_idx = target_index;

    double icon_size = item_w * 0.42;
    // 绘制所有图标
    for (int i = 0; i < count; i++)
    {
        double left_x = i * item_w;
        double base_icon_y = bar_y + (bar_h - icon_size) / 2.0;
        double up_icon_y = base_icon_y - icon_up_offset;
        double draw_icon_y;

        // 图标上浮插值逻辑
        if (i == curr_idx && is_animating)
        {
            draw_icon_y = lerp(up_icon_y, base_icon_y, t);
        }
        else if (i == target_idx && is_animating)
        {
            draw_icon_y = lerp(base_icon_y, up_icon_y, t);
        }
        else if (i == index && !is_animating)
        {
            draw_icon_y = up_icon_y;
        }
        else
        {
            draw_icon_y = base_icon_y;
        }

        QRectF icon_rect(
            left_x + (item_w - icon_size) / 2.0 + bar_side_margin,
            draw_icon_y,
            icon_size,
            icon_size
            );
        // 修复:补充源rect
        painter.drawPixmap(icon_rect, icon_pixmaps[i], icon_pixmaps[i].rect());
    }

    // 3. 绘制绿色悬浮大圆指示器
    double curr_ind_x = curr_idx * item_w + (item_w - indicator_diameter) / 2.0;
    double target_ind_x = target_idx * item_w + (item_w - indicator_diameter) / 2.0;
    double ind_x = lerp(curr_ind_x, target_ind_x, t);
    double ind_y = bar_y - indicator_diameter * 0.35;

    QRectF ind_circle(ind_x + bar_side_margin, ind_y, indicator_diameter, indicator_diameter);
    painter.setBrush(indicator_inner_color);
    QPen pen;
    pen.setColor(root_bg);
    pen.setWidth(10);
    painter.setPen(pen);
    painter.drawEllipse(ind_circle);

    // 大圆内部图标
    double sel_icon_size = indicator_diameter * 0.45;
    int sel_idx = qRound(lerp(curr_idx, target_idx, t));
    QRectF sel_icon_rect(
        ind_x + (indicator_diameter - sel_icon_size) / 2.0 + bar_side_margin,
        ind_y + (indicator_diameter - sel_icon_size) / 2.0,
        sel_icon_size,
        sel_icon_size
        );
    // 修复:补充源rect
    painter.drawPixmap(sel_icon_rect, icon_pixmaps[sel_idx], icon_pixmaps[sel_idx].rect());

    // 绘制底部文字(顶层)
    QFont font;
    font.setPixelSize(14);
    font.setFamily("SimHei");
    font.setBold(true);
    painter.setFont(font);
    painter.setPen(font_color);

    for (int i = 0; i < count; i++)
    {
        double left_x = i * item_w;
        double text_y;
        // 文字Y插值逻辑
        if (i == curr_idx && is_animating)
        {
            text_y = lerp(text_show_y, text_hidden_y, t);
        }
        else if (i == target_idx && is_animating)
        {
            text_y = lerp(text_hidden_y, text_show_y, t);
        }
        else if (i == index && !is_animating)
        {
            text_y = text_show_y;
        }
        else
        {
            text_y = text_hidden_y;
        }

        QRectF text_rect(left_x + bar_side_margin, text_y, item_w, text_h);
        painter.drawText(text_rect, Qt::AlignHCenter | Qt::AlignVCenter, nav_data[i].text);
    }
}

3、mainWindow.cpp

cpp 复制代码
#include "mainwindow.h"
#include <QCoreApplication>
#include <QMetaObject>
#include <QMetaProperty>
#include <QMetaMethod>
#include <QDebug>
#include "bottomnavwidget.h"


MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    this->resize(800,600);
    QWidget* centralWidget = new QWidget(this);
    setCentralWidget(centralWidget);
    centralWidget->setStyleSheet("background-color: #020414;");

    QVBoxLayout* layout = new QVBoxLayout(centralWidget);
    layout->setAlignment(Qt::AlignCenter);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->addWidget(new BottomNavWidget());
}

MainWindow::~MainWindow()
{

}

三、实现原理

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

1、Qt6 事件系统概述

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

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

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

2.1 、paintEvent - 绘图事件

cpp 复制代码
void CaptureMask::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    // 1. 绘制完整屏幕截图作为背景
    painter.drawPixmap(rect(), m_screenBg);

    // 2. 全局半透明黑色遮罩(实现变暗效果)
    QColor darkColor(0, 0, 0, 150);  // 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(0, 180, 255), 2));      // 蓝色边框
        painter.setBrush(QColor(0, 160, 255, 60));        // 浅蓝色半透明填充
        painter.drawRect(selectRect);
    }
}

原理说明

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

2.2、 鼠标事件三部曲

mousePressEvent - 鼠标按下
cpp 复制代码
void CaptureMask::mousePressEvent(QMouseEvent *event)
{
    m_isDragging = true;                    // 开始拖拽状态
    m_startPos = event->pos();              // 记录起始位置
    m_endPos = m_startPos;                  // 初始结束位置=起始位置
    update();                               // 触发重绘(绘制起始点)
}
mouseMoveEvent - 鼠标移动
cpp 复制代码
void CaptureMask::mouseMoveEvent(QMouseEvent *event)
{
    if (!m_isDragging) return;              // 非拖拽状态不处理
    m_endPos = event->pos();                // 更新结束位置
    update();                               // 触发重绘(更新选区矩形)
}
mouseReleaseEvent - 鼠标释放
cpp 复制代码
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 、全屏遮罩窗口
cpp 复制代码
// 窗口标志设置
Qt::Window |                    // 作为独立窗口
Qt::FramelessWindowHint |       // 无边框
Qt::WindowStaysOnTopHint        // 始终置顶

// 窗口属性
setAttribute(Qt::WA_DeleteOnClose);         // 关闭时自动删除
setAttribute(Qt::WA_TranslucentBackground, false); // 不透明背景
3.2.2 、屏幕捕获
cpp 复制代码
// 获取主屏幕
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 适配
    • 支持多屏幕截图
相关推荐
持敬chijing2 小时前
Python概述
开发语言·python
Quz4 小时前
QML SpinBox 基础与步长控制:可编辑输入与自定义步长
qt
lpfasd1235 小时前
编程语言榜单变迁分析
开发语言·后端·scala
名字还没想好☜5 小时前
Go 用 bufio.Scanner 读大文件踩坑:默认 64KB 行上限、Buffer 扩容与按 Token 切分
开发语言·后端·golang·go
隐积6 小时前
Qt魔法之旅 · 全系列课程导航
qt
Byron Loong6 小时前
【C++】重定向是什么
开发语言·c++
techdashen7 小时前
Go设计取舍之六: sync.Mutex正常模式与饥饿模式
开发语言·后端·golang
TunerT_TQ7 小时前
【智能体安全治理|专栏第9期】从“一堆规则”到“数字宪法”:智能体治理的下一个阶段
java·开发语言·安全·开源治理·大模型安全·ai基础设施·智能体安全
Aurorar0rua7 小时前
CS50 x 2024 Notes Algorithms - 07
c语言·开发语言·学习方法