输入关键词开始搜索

GUI 设计文档

07 — 云融(YunRong)· GUI 设计文档

前置文档02-系统架构设计文档05-模块详细设计文档 设计思路00-设计思路文档

版本:v0.1 状态:已发布 最后更新:2025-01


1. 设计概述

1.1 设计原则

原则说明
非阻塞 UI任何可能超过 50ms 的操作不得在主线程执行
即时反馈用户操作后 100ms 内必须有视觉反馈(骨架屏/动画/临时状态)
渐进展示默认展示最常用信息,详细内容按需展开
平台一致遵循各平台 HIG(Windows 11 Fluent / Linux GNOME Adwaita),而非强行统一
键盘可达所有功能可通过键盘操作(Tab 导航 + 快捷键)
响应式窗口宽度 ≥ 800px 时三栏,< 800px 时自动折叠为单栏 + 导航

1.2 设计尺度

参数说明
默认窗口尺寸1200 × 800 px最小 800 × 600
导航栏宽度64 px固定,图标竖排
侧边栏宽度280 px (默认)可拖拽调整 (200–400 px)
信息面板宽度320 px (默认)可折叠
字体系统默认 (Segoe UI / Noto Sans)基础大小 14px
圆角半径8px (控件) / 12px (气泡)统一圆角体系
间距单位4px 基础网格所有间距为 4 的倍数

2. 主窗口布局

2.1 三段式布局

┌──────┬──────────────────────────┬─────────────┐
│ Nav  │     Content Area          │  Info Panel │
│ Bar  │                          │  (可折叠)    │
│ 64px │                          │  320px      │
│      │                          │             │
│  ●   │  ┌────────────────────┐  │  联系人详情   │
│ 消   │  │                    │  │  文件预览     │
│ 息   │  │                    │  │  搜索面板     │
│      │  │                    │  │             │
│  ●   │  │   当前视图内容       │  │             │
│ 任   │  │                    │  │             │
│ 务   │  │                    │  │             │
│      │  │                    │  │             │
│  ●   │  │                    │  │             │
│ 文   │  └────────────────────┘  │             │
│ 件   │                          │             │
│      │                          │             │
│  ●   │                          │             │
│ 联   │                          │             │
│ 系   │                          │             │
│ 人   │                          │             │
│      │                          │             │
│  ●   │                          │             │
│ 设   │                          │             │
│ 置   │                          │             │
└──────┴──────────────────────────┴─────────────┘

2.2 导航栏设计

┌──────┐
│      │  ← 用户头像 (32×32, 圆形)
│  👤  │
│      │
│  💬  │  ← 消息 (当前选中: 高亮左侧蓝色竖条)
│  ┃   │
│      │
│  ✅  │  ← 任务 (有未读数: 红色角标 "3")
│  ③   │
│      │
│  📁  │  ← 文件
│      │
│  👥  │  ← 联系人
│      │
│  ⚙   │  ← 设置
│      │
│      │
│      │  ← 弹性空间
│      │
│  ⚡   │  ← 网络状态指示 (绿/黄/红)
│      │
└──────┘

导航项数据结构:

struct NavItem {
    QString icon;           // SVG 图标路径
    QString label;          // 工具提示文本
    Page page;              // 对应页面枚举
    int unreadCount = 0;    // 未读角标数
    bool isActive = false;
};

2.3 响应式断点

窗口宽度布局
≥ 1200 px三栏:导航 + 侧边栏 + 内容 + 信息面板
900–1199 px两栏:导航 + 侧边栏 + 内容(信息面板折叠)
700–899 px两栏:导航 + 内容(侧边栏折叠为弹出菜单)
< 700 px单栏:内容全屏 + 底部导航条
void MainWindow::resizeEvent(QResizeEvent* event) {
    int w = event->size().width();
    if (w >= 1200 && layoutMode_ != LayoutMode::ThreePanel) {
        setLayoutMode(LayoutMode::ThreePanel);
    } else if (w >= 900 && w < 1200 && layoutMode_ != LayoutMode::TwoPanel) {
        setLayoutMode(LayoutMode::TwoPanel);
    } else if (w >= 700 && w < 900 && layoutMode_ != LayoutMode::Compact) {
        setLayoutMode(LayoutMode::Compact);
    } else if (w < 700 && layoutMode_ != LayoutMode::Single) {
        setLayoutMode(LayoutMode::Single);
    }
    QMainWindow::resizeEvent(event);
}

3. 核心视图设计

3.1 聊天视图 (ChatView)

┌──────────────────────────────────────────┐
│  会话标题栏                               │
│  ┌────────────────────────────────────┐  │
│  │ 👤 李四                            │  │
│  │    在线 · 研发部                    │  │
│  │                          🔍 📞 ⋯  │  │
│  └────────────────────────────────────┘  │
│                                          │
│  消息列表 (QListView)                     │
│  ┌────────────────────────────────────┐  │
│  │         ┌──────────────────┐       │  │
│  │  09:30  │ 系统消息: 你们已加 │       │  │
│  │         └──────────────────┘       │  │
│  │                                    │  │
│  │  ┌─────────────────────┐           │  │
│  │  │ 你好,明天的会议几点?│  对方     │  │
│  │  └─────────────────────┘           │  │
│  │                                    │  │
│  │               ┌──────────────────┐ │  │
│  │     我        │ 下午 3 点,3 号   │ │  │
│  │               │ 会议室           │ │  │
│  │               └──────────────────┘ │  │
│  │                          ✓ 已读    │  │
│  │                                    │  │
│  │  ┌─────────────────────┐           │  │
│  │  │ 好的,我会准时参加    │  对方     │  │
│  │  └─────────────────────┘           │  │
│  └────────────────────────────────────┘  │
│                                          │
│  输入区域                                 │
│  ┌────────────────────────────────────┐  │
│  │ 工具栏: 😊 📎 🖼️ 📁  ...          │  │
│  ├────────────────────────────────────┤  │
│  │                                    │  │
│  │  输入消息...                        │  │
│  │                                    │  │
│  ├────────────────────────────────────┤  │
│  │                          发送  ↵   │  │
│  └────────────────────────────────────┘  │
└──────────────────────────────────────────┘

消息气泡规格

我方气泡 (右对齐, 绿色 #95EC69):
  ┌──────────────────────┐
  │ 消息文本              │
  │ 最大宽度 400px        │
  │                      │
  │              ✓ 09:30 │  ← 状态图标 + 时间
  └──────────────────────┘
  
  圆角: 左上12px, 左下12px, 右上12px, 右下4px
  内边距: 10px 14px
  字体: 14px, 行高 1.5

对方气泡 (左对齐, 白色 #FFFFFF):
  ┌──────────────────────┐
  │ 消息文本              │
  │ 最大宽度 400px        │
  │                      │
  │ 09:30                │
  └──────────────────────┘

  圆角: 左上12px, 右上12px, 右下12px, 左下4px
  内边距: 10px 14px
  阴影: 0 1px 3px rgba(0,0,0,0.08)

文件卡片规格

┌─────────────────────────────────────┐
│ 📄 Q3季度报告.pdf                    │
│     1.0 MB                         │
│ ┌─────────────────────────────┐    │
│ │████████████████░░░░░░░░░░░░░│ 45%│
│ └─────────────────────────────┘    │
│                          ⬇ 下载    │
└─────────────────────────────────────┘

3.2 会话列表 (ConversationList)

┌──────────────────────────────────┐
│ 🔍 搜索会话或消息...              │
├──────────────────────────────────┤
│                                  │
│ ┌──────────────────────────────┐ │
│ │ 👤 李四                09:30 │ │
│ │    好的,我会准时参加         │ │
│ │                        ③     │ │  ← 未读角标
│ └──────────────────────────────┘ │
│                                  │
│ ┌──────────────────────────────┐ │
│ │ 👥 项目讨论组           09:15 │ │
│ │    张三: 文档已更新到共享      │ │
│ │    [免打扰] 🔕                │ │
│ └──────────────────────────────┘ │
│                                  │
│ ┌──────────────────────────────┐ │
│ │ 👤 王五                昨天   │ │
│ │    [图片]                    │ │
│ │                         ✓    │ │  ← 已读
│ └──────────────────────────────┘ │
│                                  │
│  ─── 置顶 ────────────────────── │
│                                  │
│ ┌──────────────────────────────┐ │
│ │ 📌 全员公告群           周一   │ │
│ │    [2条公告] 新年放假通知     │ │
│ └──────────────────────────────┘ │
└──────────────────────────────────┘

每行高度:68px,含头像(40×40) + 名称(14px bold) + 摘要(12px) + 时间(11px)。

3.3 联系人视图 (ContactTree)

┌──────────────────────────────────┐
│ 🔍 搜索联系人...                  │
├──────────────────────────────────┤
│                                  │
│  ⭐ 星标联系人                    │
│  ├─ 👤 李四    研发部    🟢 在线  │
│  └─ 👤 赵六    产品部    🟡 离开  │
│                                  │
│  🏢 总公司 (1500人)               │
│  ├─ 📁 研发部 (80人)              │
│  │  ├─ 👤 张三    高级工程师 🟢   │
│  │  ├─ 👤 李四    高级工程师 🟢   │
│  │  └─ 👤 孙七    实习生     ⚫   │
│  ├─ 📁 产品部 (45人)              │
│  │  ├─ 👤 赵六    产品经理   🟡   │
│  │  └─ 👤 周八    UI设计    🟢   │
│  └─ 📁 市场部 (30人)              │
│     └─ ...                       │
│                                  │
└──────────────────────────────────┘

树节点高度 40px,缩进 20px/级。部门节点前有展开/折叠箭头。

状态指示灯:

  • 🟢 在线 (绿色 #4CAF50)
  • 🟡 离开 (黄色 #FF9800)
  • 🔴 忙碌 (红色 #F44336)
  • ⚫ 离线 (灰色 #9E9E9E)

3.4 任务视图 (TaskList)

┌──────────────────────────────────┐
│  [待处理] [已处理] [我发起的]      │
├──────────────────────────────────┤
│                                  │
│  ┌──────────────────────────────┐│
│  │ 🔴 高优先级                   ││
│  │ 请假审批                      ││
│  │ 张三申请年假 3 天              ││
│  │ 2025-01-20 → 2025-01-22      ││
│  │ 1 小时前                     ││
│  │          [驳回]  [同意]       ││
│  └──────────────────────────────┘│
│                                  │
│  ┌──────────────────────────────┐│
│  │ 🟡 普通优先级                 ││
│  │ 代码评审                      ││
│  │ 李四邀请你评审 PR #234         ││
│  │ 3 小时前                     ││
│  │          [拒绝]  [接受]       ││
│  └──────────────────────────────┘│
│                                  │
│  ┌──────────────────────────────┐│
│  │ 📢 系统公告                   ││
│  │ 服务器维护通知                 ││
│  │ 本周六 02:00-06:00 维护       ││
│  │ 2 天前 · 已读                ││
│  └──────────────────────────────┘│
└──────────────────────────────────┘

3.5 文件传输面板 (FilePanel)

┌──────────────────────────────────┐
│  传输中 (2) · 已完成 (48)         │
├──────────────────────────────────┤
│                                  │
│  上传                            │
│  ┌──────────────────────────────┐│
│  │ 📄 Q3季度报告.pdf             ││
│  │    1.0 MB                    ││
│  │ ████████████░░░░░░ 78%       ││
│  │    812 KB/s           ⏸ 取消 ││
│  └──────────────────────────────┘│
│                                  │
│  下载                            │
│  ┌──────────────────────────────┐│
│  │ 🎨 设计稿_v3.fig              ││
│  │    24.5 MB                   ││
│  │ ████░░░░░░░░░░░░░░ 22%       ││
│  │    1.2 MB/s           ⏸ 取消 ││
│  └──────────────────────────────┘│
│                                  │
│  ─── 已完成 ──────────────────── │
│  ├─ 📸 screenshot.png   2.1 MB  │
│  ├─ 📄 需求文档.docx    156 KB  │
│  └─ 🎵 录音.amr          48 KB  │
└──────────────────────────────────┘

3.6 数据统计面板 (Dashboard)

┌──────────────────────────────────┐
│  消息统计   本周 ◀ ▶              │
├──────────────────────────────────┤
│                                  │
│  总发送: 1,523    总接收: 2,105  │
│                                  │
│  📈 消息趋势 (7天)                │
│  ┌────────────────────────────┐  │
│  │     ·                      │  │
│  │    / \    ·                │  │
│  │   /   \  / \      ·       │  │
│  │  /     \/   \    / \      │  │
│  │ /            \  /   \     │  │
│  │·              \/     ·    │  │
│  └────────────────────────────┘  │
│  ── 发送  ── 接收                │
│                                  │
│  📊 会话活跃度                    │
│  ┌────────────────────────────┐  │
│  │ 项目讨论组 ████████████ 850 │  │
│  │ 李四       ██████ 300     │  │
│  │ 王五       ████ 200       │  │
│  │ 全员公告群  ███ 150        │  │
│  └────────────────────────────┘  │
│                                  │
│  📁 文件传输                      │
│  上传: 45 个 · 500 MB            │
│  下载: 23 个 · 100 MB            │
│                                  │
│  [导出 CSV] [导出 PDF] [导出 JSON]│
└──────────────────────────────────┘

4. 自定义控件规格

4.1 MessageBubble (消息气泡)

// gui/widgets/message_bubble.h
class MessageBubble : public QWidget {
    Q_OBJECT
    Q_PROPERTY(bool isMine READ isMine WRITE setIsMine)

public:
    explicit MessageBubble(QWidget* parent = nullptr);

    void setMessage(const model::Message& msg);
    void setStatus(MessageStatus status);  // Sending/Sent/Delivered/Read/Failed

    bool isMine() const;
    void setIsMine(bool mine);

protected:
    void paintEvent(QPaintEvent* event) override;
    QSize sizeHint() const override;
    void mousePressEvent(QMouseEvent* event) override;
    void contextMenuEvent(QContextMenuEvent* event) override;

private:
    QString text_;
    QString imageUrl_;
    int contentType_ = 0;
    MessageStatus status_ = MessageStatus::Sent;
    bool isMine_ = false;
    bool isSelected_ = false;

    void paintTextBubble(QPainter* p, const QRect& rect);
    void paintImageBubble(QPainter* p, const QRect& rect);
    void paintFileCard(QPainter* p, const QRect& rect);
    QRect bubbleRect() const;
    QColor bubbleColor() const;
};

绘制流程:

paintEvent:
  1. 计算气泡矩形 (bubbleRect)
     - 我方: 右对齐, 右侧距边 12px
     - 对方: 左对齐, 左侧距边 48px (头像占位)
  2. 绘制气泡背景
     - QPainterPath 圆角矩形
     - 我方: 填充 #95EC69
     - 对方: 填充 #FFFFFF, 描边 #E0E0E0
  3. 根据 contentType 绘制内容
     - 文本: QPainter::drawText (支持多行、Emoji)
     - 图片: QPainter::drawPixmap (带圆角裁剪)
     - 文件: 文件卡片 (图标 + 名称 + 大小 + 进度条)
  4. 绘制时间和状态标记
     - 我方: 气泡右下角
     - 对方: 气泡左下角

4.2 AvatarWidget (头像)

class AvatarWidget : public QWidget {
    Q_OBJECT
    Q_PROPERTY(int size READ size WRITE setSize)

public:
    explicit AvatarWidget(QWidget* parent = nullptr);

    void setImage(const QPixmap& pixmap);
    void setImageUrl(const QString& url);        // 异步加载
    void setPlaceholder(const QString& initials); // 无头像时显示首字母
    void setOnlineStatus(OnlineStatus status);

    int size() const;
    void setSize(int size);

protected:
    void paintEvent(QPaintEvent* event) override;

private:
    QPixmap image_;
    QString initials_;
    OnlineStatus status_ = OnlineStatus::Offline;
    int size_ = 40;

    void paintCircle(QPainter* p);
    void paintInitials(QPainter* p);
    void paintStatusDot(QPainter* p);
};

规格:

  • 圆形裁剪(QPainterPath::addEllipse + setClipPath
  • 默认尺寸:列表 40px,详情 64px,导航栏 32px
  • 状态角标:头像右下角 10px 圆点 + 2px 白色描边

4.3 SpeedChart (实时速度曲线)

class SpeedChart : public QWidget {
    Q_OBJECT

public:
    explicit SpeedChart(QWidget* parent = nullptr);

    void addDataPoint(double upSpeed, double downSpeed); // KB/s
    void setHistoryDuration(int seconds);                 // 默认 60s
    void clear();

protected:
    void paintEvent(QPaintEvent* event) override;

private:
    struct DataPoint {
        qint64 timestamp;
        double upSpeed;
        double downSpeed;
    };
    std::deque<DataPoint> history_; // 保留最近 ~600 个采样点 (≈60s @10Hz),超限时 pop_front
    QColor upColor_   = QColor("#4CAF50");
    QColor downColor_ = QColor("#2196F3");

    void paintGrid(QPainter* p, const QRect& rect);
    void paintCurves(QPainter* p, const QRect& rect);
    void paintLegend(QPainter* p, const QRect& rect);
};

4.4 ToastNotify (通知弹窗)

┌────────────────────────────────┐
│ 🔔  新的审批任务                │
│     张三邀请你审批「请假申请」    │
│                          ╳     │
└────────────────────────────────┘
class ToastNotify : public QWidget {
    Q_OBJECT

public:
    enum class Level { Info, Warning, Error, Success };

    static void show(QWidget* parent, const QString& title,
                     const QString& body, Level level = Level::Info,
                     int durationMs = 5000);

protected:
    void paintEvent(QPaintEvent* event) override;
    void showEvent(QShowEvent* event) override;   // 触发滑入动画

private:
    QString title_;
    QString body_;
    Level level_ = Level::Info;

    void startSlideIn();   // 从右上角滑入
    void startFadeOut();   // 淡出
};

动画规格:

  • 滑入:QPropertyAnimation 作用于 pos,从 (windowWidth, 20)(windowWidth - width - 20, 20),持续 300ms,QEasingCurve::OutCubic
  • 淡出:QPropertyAnimation 作用于 windowOpacity,从 1.0 到 0.0,持续 200ms

4.5 StatusIndicator (在线状态)

class StatusIndicator : public QWidget {
    Q_OBJECT

public:
    enum class State { Online, Away, Busy, Offline };

    void setState(State state);
    State state() const;

    // 支持闪烁动画(新消息提醒)
    void startBlink(int intervalMs = 500);
    void stopBlink();

protected:
    void paintEvent(QPaintEvent* event) override;

private:
    State state_ = State::Offline;
    bool blinkOn_ = false;
    QTimer* blinkTimer_ = nullptr;
};

5. 样式系统

5.1 设计令牌 (Design Tokens)

// gui/style/tokens.h
namespace gui::style {

// ---- 颜色 ----
namespace Color {
    inline constexpr auto Primary      = "#1677FF";  // 主色调
    inline constexpr auto PrimaryLight = "#E6F4FF";  // 主色浅底
    inline constexpr auto Success      = "#52C41A";
    inline constexpr auto Warning      = "#FAAD14";
    inline constexpr auto Error        = "#FF4D4F";
    inline constexpr auto TextPrimary  = "#1F1F1F";
    inline constexpr auto TextSecondary= "#8C8C8C";
    inline constexpr auto TextDisabled = "#BFBFBF";
    inline constexpr auto BgPrimary    = "#FFFFFF";
    inline constexpr auto BgSecondary  = "#F5F5F5";
    inline constexpr auto BgTertiary   = "#FAFAFA";
    inline constexpr auto Border       = "#E8E8E8";
    inline constexpr auto BubbleMine   = "#95EC69";
    inline constexpr auto BubbleOther  = "#FFFFFF";
}

// ---- 字体 ----
namespace Font {
    inline constexpr int SizeXS   = 11;
    inline constexpr int SizeSM   = 12;
    inline constexpr int SizeBase = 14;
    inline constexpr int SizeLG   = 16;
    inline constexpr int SizeXL   = 20;
    inline constexpr int SizeTitle= 24;
}

// ---- 间距 ----
namespace Spacing {
    inline constexpr int XS  = 4;
    inline constexpr int SM  = 8;
    inline constexpr int MD  = 12;
    inline constexpr int LG  = 16;
    inline constexpr int XL  = 24;
    inline constexpr int XXL = 32;
}

// ---- 圆角 ----
namespace Radius {
    inline constexpr int SM = 4;
    inline constexpr int MD = 8;
    inline constexpr int LG = 12;
    inline constexpr int Full = 9999;
}

// ---- 阴影 ----
namespace Shadow {
    inline constexpr auto Card = "0 1px 3px rgba(0,0,0,0.08)";
    inline constexpr auto Dropdown = "0 4px 12px rgba(0,0,0,0.12)";
    inline constexpr auto Modal = "0 8px 24px rgba(0,0,0,0.16)";
}

} // namespace gui::style

5.2 全局样式表

// gui/style/global.qss (编译为 Qt 资源)
void applyGlobalStyle(QApplication* app) {
    app->setStyleSheet(R"(
        /* ---- 全局 ---- */
        * {
            font-family: -apple-system, "Segoe UI", "Noto Sans", sans-serif;
            font-size: 14px;
            color: #1F1F1F;
        }

        /* ---- 导航栏 ---- */
        #NavBar {
            background-color: #2C2C2C;
            min-width: 64px;
            max-width: 64px;
        }
        #NavBar QPushButton {
            border: none;
            border-radius: 8px;
            padding: 8px;
            color: #A0A0A0;
        }
        #NavBar QPushButton:hover {
            background-color: #3C3C3C;
            color: #FFFFFF;
        }
        #NavBar QPushButton[active="true"] {
            color: #FFFFFF;
        }
        #NavBar QPushButton[active="true"]::before {
            /* 左侧蓝色指示条 */
            background-color: #1677FF;
        }

        /* ---- 侧边栏 ---- */
        #SideBar {
            background-color: #F5F5F5;
            border-right: 1px solid #E8E8E8;
        }

        /* ---- 会话列表 ---- */
        #ConversationList QListView {
            background-color: #F5F5F5;
            border: none;
        }
        #ConversationList QListView::item {
            padding: 12px;
            border-bottom: 1px solid #F0F0F0;
        }
        #ConversationList QListView::item:selected {
            background-color: #E6F4FF;
        }
        #ConversationList QListView::item:hover {
            background-color: #ECECEC;
        }

        /* ---- 输入框 ---- */
        #MessageInput {
            border: 1px solid #E8E8E8;
            border-radius: 8px;
            padding: 10px;
            background-color: #FFFFFF;
        }
        #MessageInput:focus {
            border-color: #1677FF;
        }

        /* ---- 滚动条 ---- */
        QScrollBar:vertical {
            background: transparent;
            width: 6px;
        }
        QScrollBar::handle:vertical {
            background: #D0D0D0;
            border-radius: 3px;
            min-height: 30px;
        }
        QScrollBar::handle:vertical:hover {
            background: #A0A0A0;
        }

        /* ---- 工具提示 ---- */
        QToolTip {
            background-color: #2C2C2C;
            color: #FFFFFF;
            border: none;
            border-radius: 4px;
            padding: 4px 8px;
            font-size: 12px;
        }
    )");
}

5.3 暗色主题

通过 ConfigManager 读取 ui.theme 切换全局样式表。暗色模式下的颜色映射:

令牌亮色暗色
BgPrimary#FFFFFF#1E1E1E
BgSecondary#F5F5F5#2D2D2D
BgTertiary#FAFAFA#252525
TextPrimary#1F1F1F#E0E0E0
TextSecondary#8C8C8C#9E9E9E
Border#E8E8E8#3A3A3A
BubbleOther#FFFFFF#3A3A3A

6. 交互设计

6.1 键盘快捷键

快捷键功能备注
Ctrl+N新建会话
Ctrl+F搜索消息/联系人根据当前视图切换搜索对象
Ctrl+Enter发送消息
Enter发送消息(可配置)
Ctrl+Tab切换到下一个会话
Ctrl+Shift+Tab切换到上一个会话
Ctrl+1..5切换到导航页 1-5消息/任务/文件/联系人/设置
Ctrl+R引用回复选中消息
Ctrl+C复制选中消息文本
Escape关闭弹窗/取消选择/清空搜索
Alt+Left返回上一个视图
Ctrl+Shift+A上传文件
Ctrl+D截图发送可选功能
Ctrl++/-缩放字体

6.2 通知交互

收到新消息 → 判断当前状态:
  ├── 聊天窗口在前台 + 当前会话
  │     → 直接追加到消息列表 + 标记已读

  ├── 聊天窗口在前台 + 其他会话
  │     → 会话列表未读角标 +1 + 系统托盘角标更新

  ├── 应用在后台(最小化/隐藏)
  │     → 系统通知弹窗 (Windows Toast / Linux D-Bus)
  │     → 系统托盘闪烁 + 角标更新
  │     → 任务栏图标高亮 (Windows) / 紧急标记 (Linux)

  └── 免打扰模式
       → 静默记录,仅角标更新,无弹窗无声音

6.3 文件拖拽上传

拖拽文件到聊天窗口:
  1. 检测 dragEnterEvent → 判断文件类型和大小
  2. 显示拖拽指示器 (半透明蓝色虚线框)
  3. dropEvent → 获取文件路径列表
  4. 弹出确认对话框 (文件名 + 大小 + 发送按钮)
  5. 确认后 → FileService::uploadFile()
  6. 上传完成后 → 自动发送文件消息

6.4 消息右键菜单

右键消息气泡 → QMenu:
  ├── 📋 复制文本
  ├── 💬 引用回复
  ├── 📌 置顶消息 (仅群聊)
  ├── 🔗 复制消息链接
  ├── ──────────────
  ├── ❌ 撤回 (仅自己的消息, 2 分钟内)
  └── 🗑️ 删除 (仅本地)

7. 平台适配

7.1 系统托盘

// platform/tray_impl.h
class IPlatformTray {
public:
    virtual ~IPlatformTray() = default;
    virtual void showMessage(const QString& title, const QString& body) = 0;
    virtual void setBadge(int count) = 0;
    virtual void setBlinking(bool blink) = 0;
};

// Windows 实现
class WinTray : public IPlatformTray {
    // 使用 QSystemTrayIcon + Windows ITaskbarList3 接口
    // 任务栏角标: ITaskbarList3::SetOverlayIcon()
};

// Linux 实现
class LinuxTray : public IPlatformTray {
    // 使用 QSystemTrayIcon + D-Bus org.freedesktop.Notifications
    // 部分 DE (KDE) 支持角标,GNOME 不支持
};

7.2 窗口行为

行为WindowsLinux
最小化到托盘✅ (点击关闭按钮最小化)
任务栏进度条ITaskbarList3::SetProgressValueUnity Launcher API
窗口阴影DWM 原生自绘 (QGraphicsDropShadowEffect)
窗口圆角Windows 11 原生自绘 (QPainterPath + setMask)
文件对话框QFileDialog (原生)QFileDialog (GTK 风格)

7.3 高 DPI 适配

// main.cpp
int main(int argc, char* argv[]) {
    // Qt 6 默认启用 High DPI
    // 手动设置策略 (Qt 5 兼容)
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
    QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps);
#endif

    QApplication app(argc, argv);

    // 设置缩放取整策略
    QApplication::setHighDpiScaleFactorRoundingPolicy(
        Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
    // ...
}

所有图标使用 SVG 格式,QIcon::fromTheme() 优先使用系统图标。


8. GUI 与核心层的连接

8.1 信号槽连接图

core::service::IMService (工作线程, QObject)

    ├── signal: newMessageReceived(Message)
    │        │ QueuedConnection
    │        ▼
    │   gui::MainWindow (GUI 线程)
    │        │
    │        ├──► gui::ChatView::appendMessage()
    │        ├──► gui::ConversationList::updateLastMessage()
    │        └──► gui::TrayIcon::notify()

core::net::ConnectionManager (Main Thread, Qt Event Loop)
    │  异步 IO 收发在主线程;重计算(编解码/加解密)
    │  通过 QueuedConnection 投递给 Worker Pool

    ├── signal: onStateChanged(ConnectionState)
    │        │ QueuedConnection
    │        ▼
    │   gui::StatusBar::updateConnectionIcon()

core::infra::WorkerPool (N workers)
    │  接收 Main Thread 投递的 RawMessage
    │  执行 Protocol 编解码、校验、解密
    │  完成后通过 Signal 回传 DecodedMsg 给 Main Thread

    ├── signal: messageDecoded(DecodedMsg)
    │        │ QueuedConnection → Main Thread → GUI / DB

core::service::FileService (Worker Thread)

    ├── signal: transferProgressChanged(id, progress, speed)
    │        │ QueuedConnection
    │        ▼
    │   gui::FilePanel::updateProgress()
    │   gui::SpeedChart::addDataPoint()

8.2 线程安全桥接

// 在工作线程中安全调用 GUI 更新
template<typename Func>
void runOnGuiThread(Func&& func) {
    QMetaObject::invokeMethod(QApplication::instance(),
        std::forward<Func>(func), Qt::QueuedConnection);
}

// 使用示例(任何工作线程)
runOnGuiThread([this, msg = std::move(message)] {
    chatView_->appendMessage(msg);
});

9. 性能考虑

9.1 消息列表虚拟化

QListView + 自定义 QAbstractListModel 天然支持视图虚拟化——只渲染可见区域的消息气泡。对于 10000 条消息的会话,实际渲染的只有 ~20 个气泡。

9.2 图片懒加载

class LazyImageLoader : public QObject {
    // 维护一个优先级队列
    // 可见区域内的图片优先加载
    // 滚出可见区域的图片取消加载
    // 内存中最多缓存 50 张解码后的缩略图
};

9.3 绘制优化

  • 消息气泡背景预渲染为 QPixmap 缓存,避免每次 paintEvent 重复计算圆角路径
  • 使用 QWidget::setAttribute(Qt::WA_OpaquePaintEvent) 减少不必要的背景擦除
  • QListView::setUniformItemSizes(true) 允许 Qt 做更多布局优化(但由于消息气泡高度可变,设为 false 更准确)

附录 A — 控件清单

控件基类是否自定义绘制所在模块
MainWindowQMainWindowgui/
NavBarQWidgetgui/widgets/
ConversationListQListView否 (自定义 Delegate)gui/views/
ChatViewQWidget (组合)gui/views/
MessageBubbleQWidgetgui/widgets/
AvatarWidgetQWidgetgui/widgets/
ContactTreeQTreeView否 (自定义 Model)gui/views/
TaskListQListView否 (自定义 Delegate)gui/views/
FilePanelQWidget (组合)gui/views/
SpeedChartQWidgetgui/widgets/
DashboardQWidget (组合)gui/views/
ToastNotifyQWidgetgui/widgets/
StatusIndicatorQWidgetgui/widgets/
MessageInputQTextEdit否 (子类化)gui/widgets/

附录 B — 文档修订记录

版本日期作者变更说明
v0.12025-01初稿:7 个视图 + 5 个自定义控件 + 样式系统 + 交互 + 平台适配