Qt 模型/视图框架(Model/View)深度解析:从 MVC 演化到数据展示架构的王者

1. 背景与设计动机:为什么需要 Model/View?

1.1 传统 MVC 与 Qt 的取舍

经典 MVC(Model-View-Controller)由 Smalltalk-80 引入,将应用拆为三部分:

角色职责
Model(模型)管理数据与业务规则,不关心如何展示
View(视图)渲染数据,把模型状态呈现给用户
Controller(控制器)接收用户输入,协调 Model 与 View

MVC 的核心价值是解耦:数据变了界面能自动更新,界面交互不影响数据层逻辑,同一份数据可以被多个视图同时展示。

但 Qt 在继承这一思想时做了务实调整:将 Controller 合并进了 View 与 Delegate。原因有二:

  • Qt 本身已有强大的信号槽机制(见姊妹篇),用户输入的分发完全可以用信号槽在 View 内部消化,无需独立的 Controller 类;
  • 桌面 GUI 的"控制器"职责往往很薄,单独拆出反而增加类数量和维护成本。

于是 Qt 提出了 Model/View 框架:Model(数据) + View(展示) + Delegate(编辑渲染) 三角色,配合**索引(QModelIndex)**通信,构成了 Qt 数据展示的基石。

1.2 解耦思想的核心价值

没有 Model/View 时,一个典型的"表格程序"长这样:界面代码里直接操作 QTableWidget 的单元格,数据逻辑与 UI 代码纠缠在一起,换一种展示方式(表格变树形)就要重写全部逻辑。

有了 Model/View 之后:

┌────────────┐    data()/setData()    ┌────────────┐
│   Model    │ ◄────────────────────► │    View    │
│ 数据 + 规则│      QModelIndex       │展示 + 交互 │
└────────────┘                        └─────┬──────┘
                                            │ setEditorData()/setModelData()
                                     ┌──────▼──────┐
                                     │  Delegate   │
                                     │ 编辑渲染器  │
                                     └─────────────┘
  • 同一模型可挂多个视图:一份数据表格,左边用 QTableView,右边用 QChart(基于模型的图表库),数据变动两处同步刷新;
  • 数据与 UI 独立演进:数据源从 QVector 换成数据库查询结果,Model 内部改即可,View 一行不用动;
  • 复用性强:QFileSystemModel、QSqlQueryModel 等现成模型开箱即用。

1.3 与 QTableWidget 等"Item Widget"类的区别

Qt 提供了两套方案:QTableWidget / QListWidget / QTreeWidget(Item Widget 家族)与 QTableView / QListView / QTreeView(View 家族)。

维度Item Widget(QTableWidget)View + Model(QTableView)
数据存储Widget 内部持有数据Model 持有数据,View 只负责展示
耦合度高,数据与控件绑定低,完全解耦
自定义数据形态固定,难以定制子类化 Model 即可实现任意数据形态
性能万级数据卡顿配合数据分块/懒加载可支撑海量数据
适用小型静态数据、Demo正式项目、大数据、多视图复用

一句话:Item Widget 是"玩具版",Model/View 才是 Qt 推荐的生产级方案。


2. 核心架构总览:Model、View、Delegate 三角色

2.1 三个核心类的分工

  • Model(模型):QAbstractItemModel 及其子类。负责向 View 描述"数据长什么样"——有多少行、多少列、每个单元格是什么值、能否编辑、能否拖动等。
  • View(视图):QAbstractItemView 及其子类。负责把 Model 描述的数据画到屏幕上,并处理鼠标键盘交互。常见三兄弟:
    • QListView:一维列表(行)
    • QTableView:二维表格(行 × 列)
    • QTreeView:树形层次结构
  • Delegate(委托):QStyledItemDelegate。负责"单元格怎么画、编辑时用什么控件、编辑结果怎么写回"。

2.2 模型内部结构:一维 / 二维 / 树形

Model 对 View 暴露的数据形态有三种:

  • 列表(List):只有行,没有列,columnCount() 返回 1;
  • 表格(Table):行 + 列,扁平二维;
  • 树(Tree):行 + 列 + 父子层次,通过 QModelIndex::parent() 表达层级。

2.3 索引(QModelIndex)——解耦的钥匙

View 与 Model 通信不直接传"行号列号"或"指针",而是传 QModelIndex:

index(row, column, parent)
      │      │       └── 父索引(树形结构中的上级,表格/列表为无效索引)
      │      └────────── 列号
      └───────────────── 行号

QModelIndex 本质是一个轻量结构体,内部持有:行号、列号、指向 Model 的指针、以及一个 internalPointer(或 internalId),用于让 Model 快速定位到真正的数据节点。

为什么用索引而非裸指针? 因为 Model 内部数据可能随时被增删改,裸指针容易悬空;而索引是"按位置寻址 + 内部指针加速"的组合,Model 通过重写 index() 把 (row, column, parent) 映射回自己的数据结构,安全性更高,也更容易支持代理模型(QSortFilterProxyModel)在映射层做手脚。


3. 使用方式一:QStandardItemModel + 三种视图绑定(可运行示例)

对于中小型数据,无需子类化 Model,直接用 QStandardItemModel 即可。下面是一个完整的可运行示例(Qt 5.15+ / Qt 6.x,Widgets 工程):

// main.cpp —— QStandardItemModel 绑定三种视图
#include <QApplication>
#include <QStandardItemModel>
#include <QTableView>
#include <QListView>
#include <QTreeView>
#include <QSplitter>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    // 1. 创建模型:3 行 2 列
    QStandardItemModel model(3, 2);

    // 2. 设置表头
    model.setHorizontalHeaderLabels({QStringLiteral("姓名"), QStringLiteral("部门")});

    // 3. 填充数据(appendRow 可同时给多列,setItem 按行列填)
    for (int row = 0; row < 3; ++row) {
        model.setItem(row, 0, new QStandardItem(QStringLiteral("员工%1").arg(row + 1)));
        model.setItem(row, 1, new QStandardItem(QStringLiteral("研发部")));
    }

    // 4. 创建三个视图,绑定同一个模型
    QTableView  tableView;
    QListView   listView;
    QTreeView   treeView;

    tableView.setModel(&model);
    listView.setModel(&model);
    treeView.setModel(&model);

    // 5. 布局展示
    QSplitter splitter;
    splitter.addWidget(&tableView);
    splitter.addWidget(&listView);
    splitter.addWidget(&treeView);
    splitter.show();

    // 6. 数据修改,所有视图自动刷新
    model.setItem(0, 1, new QStandardItem(QStringLiteral("测试部")));

    return app.exec();
}

关键点

  • 一个 Model 挂多个 View,模型一变,所有视图同步刷新;
  • QStandardItemModel 内部每个单元格是一个 QStandardItem,模型自动为其分配 internalPointer;
  • 适合数据量在几万行以内的场景,再大就应考虑自定义 Model。

工程文件(CMakeLists.txt)供参考:

cmake_minimum_required(VERSION 3.16)
project(model_view_demo)

find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_standard_project_setup()

qt_add_executable(model_view_demo main.cpp)
target_link_libraries(model_view_demo PRIVATE Qt6::Widgets)

4. 使用方式二:自定义 QAbstractItemModel 子类(文件浏览器实战)

真实项目中数据往往来自数据库、文件系统、网络或自定义结构,此时需要子类化 QAbstractItemModel(或更方便的 QAbstractListModel / QAbstractTableModel)。

4.1 必须重写的纯虚函数

以 QAbstractTableModel 为例,至少需要实现 4 个纯虚函数:

函数作用
rowCount()返回行数
columnCount()返回列数
data()返回指定索引在指定 Role 下的数据
headerData()返回表头文本(可选,但推荐)

若要支持编辑,还需重写 flags() 与 setData()。

4.2 完整示例:自定义表格模型 + 编辑支持

// mytablemodel.h
#pragma once
#include <QAbstractTableModel>
#include <QVector>

struct Person {
    QString name;
    int     age = 0;
};

class MyTableModel : public QAbstractTableModel
{
    Q_OBJECT
public:
    explicit MyTableModel(QObject *parent = nullptr);

    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role) const override;
    QVariant headerData(int section, Qt::Orientation orientation,
                        int role) const override;
    bool setData(const QModelIndex &index, const QVariant &value,
                 int role = Qt::EditRole) override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;

    void addPerson(const Person &p);

private:
    QVector<Person> m_data;
};
// mytablemodel.cpp
#include "mytablemodel.h"

MyTableModel::MyTableModel(QObject *parent)
    : QAbstractTableModel(parent) {}

int MyTableModel::rowCount(const QModelIndex &parent) const
{
    // 顶层模型 parent 无效;避免子节点被当成行列
    return parent.isValid() ? 0 : m_data.size();
}

int MyTableModel::columnCount(const QModelIndex &parent) const
{
    return parent.isValid() ? 0 : 2;
}

QVariant MyTableModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || index.row() >= m_data.size())
        return QVariant();

    const Person &p = m_data.at(index.row());

    switch (role) {
    case Qt::DisplayRole:                       // 展示角色
        return index.column() == 0 ? p.name : p.age;
    case Qt::TextAlignmentRole:                 // 对齐角色
        return index.column() == 0
                   ? QVariant(Qt::AlignLeft | Qt::AlignVCenter)
                   : QVariant(Qt::AlignCenter);
    case Qt::EditRole:                          // 编辑角色(编辑器取值)
        return index.column() == 0 ? p.name : p.age;
    default:
        return QVariant();
    }
}

QVariant MyTableModel::headerData(int section, Qt::Orientation orientation,
                                  int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();
    if (orientation == Qt::Horizontal)
        return section == 0 ? QStringLiteral("姓名") : QStringLiteral("年龄");
    return section + 1;                          // 行号从 1 开始
}

bool MyTableModel::setData(const QModelIndex &index, const QVariant &value,
                           int role)
{
    if (!index.isValid() || role != Qt::EditRole)
        return false;

    Person &p = m_data[index.row()];
    if (index.column() == 0)
        p.name = value.toString();
    else if (index.column() == 1)
        p.age = value.toInt();
    else
        return false;

    emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
    return true;
}

Qt::ItemFlags MyTableModel::flags(const QModelIndex &index) const
{
    if (!index.isValid())
        return Qt::NoItemFlags;
    // 默认可选中、可启用,再加上可编辑
    return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable;
}

void MyTableModel::addPerson(const Person &p)
{
    // 先通知"即将插入",再改数据,最后通知"已插入"
    beginInsertRows(QModelIndex(), m_data.size(), m_data.size());
    m_data.append(p);
    endInsertRows();
}
// main.cpp —— 绑定自定义模型
#include <QApplication>
#include <QTableView>
#include "mytablemodel.h"

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    MyTableModel model;
    model.addPerson({"张三", 28});
    model.addPerson({"李四", 32});

    QTableView view;
    view.setModel(&model);
    view.show();

    return app.exec();
}

4.3 树形模型的 parent() / index() 约定

自定义树形模型还需要重写:

QModelIndex index(int row, int column,
                  const QModelIndex &parent) const override;
QModelIndex parent(const QModelIndex &child) const override;

约定的核心是:index() 把 (row, column, parent) 映射到内部节点,parent() 把子索引映射回父索引。顶层节点的 parent 返回无效索引 QModelIndex()。初学者最容易在这里绕晕,记住"index() 与 parent() 是一对互逆函数"即可。


5. 核心 API 深度解释

5.1 QModelIndex:位置寻址的轻量载体

成员含义
row() / column()行号 / 列号
parent()父索引;顶层数据返回无效索引
isValid()索引是否有效(row>=0 && column>=0 && model 指针非空)
internalPointer()返回构造索引时 Model 存入的 void*(自定义模型定位内部节点)
internalId()返回构造索引时 Model 存入的 quintptr(适合存 ID)
model()返回所属 Model 指针

常见坑:isValid() 只检查行列非负与 model 指针,不保证"索引对应的数据仍存在"。索引缓存后数据被删除,索引会"悬空",必须通过 QItemSelectionModel 或重新 model->index() 获取最新索引。

5.2 模型端核心虚函数

  • data(index, role):Model 对 View 的"回答",给定位置 + 角色返回对应数据。这是 Model 被调用最频繁的函数,必须高效。
  • setData(index, value, role):写回数据。成功必须 emit dataChanged(index, index) 通知视图刷新,否则界面不更新(详见避坑 7.2)。
  • rowCount(parent) / columnCount(parent):返回子节点数量。parent 无效时返回顶层数量。
  • headerData(section, orientation, role):返回表头/行头文本。
  • flags(index):声明该单元格支持哪些能力(可选中、可编辑、可拖动、可放置等)。
  • index(row, column, parent) / parent(child):树形模型的位置映射函数。

5.3 Qt::ItemDataRole 枚举:一列数据,多种"身份"

同一单元格在不同场景下要提供不同形态的数据,用 Role(角色) 区分:

Role典型用途返回值示例
Qt::DisplayRole界面显示文本"张三"
Qt::EditRole编辑器初始值"张三"(可为原始类型)
Qt::ToolTipRole悬停提示"点击可编辑"
Qt::DecorationRole图标QIcon(":/user.png")
Qt::TextAlignmentRole对齐方式Qt::AlignCenter
Qt::FontRole字体QFont
Qt::BackgroundRole / Qt::ForegroundRole背景色 / 前景色QColor
Qt::CheckStateRole复选框状态Qt::Checked
Qt::UserRole自定义业务数据起点任意 QVariant

使用技巧:一个单元格 = 一个位置 + N 种角色。DisplayRole 给"人看的",EditRole 给"编辑器用的",UserRole 给"代码用的"。典型错误是只实现 DisplayRole 却发现双击编辑时编辑器是空的——因为编辑器取的是 EditRole

5.4 QStandardItemModel:开箱即用的通用模型

  • setItem(row, col, item):设置单元格;
  • appendRow(list):追加一行(可同时给出多列);
  • item(row, col):获取 QStandardItem 指针;
  • setHorizontalHeaderLabels(list):批量设置表头;
  • indexFromItem(item) / itemFromIndex(index):在 QStandardItem 与 QModelIndex 之间互转;
  • findItems(text):按文本查找。

modelIndex() / index() 澄清:QStandardItemModel::index() 是标准模型自己的索引构造(从 QAbstractItemModel 继承,返回 QModelIndex),而 QStandardItem::index() 则是从 Item 反向获取其所在索引。两者方向相反,容易混淆。

5.5 模型变更信号三兄弟

模型数据变化必须通过信号通知视图,Qt 约定:

信号触发时机参数
dataChanged(topLeft, bottomRight, roles)已有数据被修改变更区域左上/右下索引 + 受影响角色
rowsInserted(parent, first, last)插入行父索引 + 首末行号
rowsRemoved(parent, first, last)删除行父索引 + 首末行号
modelReset()模型整体重建

自定义模型规则:结构性变化(插入/删除行)必须用 beginInsertRows()/endInsertRows()、beginRemoveRows()/endRemoveRows() 包裹数据操作;内容变化调用 emit dataChanged(...)。漏发信号是"界面不刷新"的头号元凶。


6. 高级组件:QSortFilterProxyModel 与 QItemSelectionModel

6.1 QSortFilterProxyModel:不改数据源,排序过滤两开花

代理模型位于源模型与视图之间,把"排序/过滤"逻辑从业务代码中剥离:

View ──► QSortFilterProxyModel ──► SourceModel(源数据)
        (排序 + 过滤 + 列映射)

// 为表格模型挂代理:支持按列排序 + 关键字过滤
QSortFilterProxyModel *proxy = new QSortFilterProxyModel(&view);
proxy->setSourceModel(&myModel);
proxy->setFilterKeyColumn(0);                 // 只过滤第 0 列
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);

view.setModel(proxy);                          // 视图绑定代理而非源模型
view.setSortingEnabled(true);                  // 点击表头排序
proxy->setSortRole(Qt::DisplayRole);

// 动态过滤:搜索框输入即过滤
QLineEdit *searchBox = new QLineEdit;
QObject::connect(searchBox, &QLineEdit::textChanged,
                 proxy, [proxy](const QString &t) {
                     proxy->setFilterFixedString(t);
                 });

核心概念:sourceIndex ↔ proxyIndex 映射

  • proxy->mapToSource(proxyIndex):代理索引 → 源模型索引;
  • proxy->mapFromSource(sourceIndex):源模型索引 → 代理索引。

视图上所有点击、选中拿到的都是代理索引,要用它操作源数据必须 mapToSource。

6.2 QItemSelectionModel:选中状态的管理者

每个 View 内部都有一个 QItemSelectionModel,负责记录"哪些单元格被选中":

// 获取视图的选中模型
QItemSelectionModel *selModel = view.selectionModel();

// 常用查询
bool selected = selModel->isSelected(index);
QModelIndex current = selModel->currentIndex();
QModelIndexList selectedRows = selModel->selectedRows();  // 所有选中行

// 常用信号
connect(selModel, &QItemSelectionModel::currentChanged,
        this, [](const QModelIndex &cur, const QModelIndex &prev) {
            qDebug() << "current changed to row" << cur.row();
        });
connect(selModel, &QItemSelectionModel::selectionChanged, ...);

多视图联动技巧:多个视图可以共享同一个 selectionModel,实现"一个视图选中、其他视图同步高亮":

QItemSelectionModel *shared = new QItemSelectionModel(&model, &app);
tableView.setSelectionModel(shared);
listView.setSelectionModel(shared);

注意:共享选中模型要求两个视图绑定同一个 Model,否则索引不互通。


7. 典型使用场景

7.1 文件浏览器(QFileSystemModel + QTreeView)

QFileSystemModel *fsModel = new QFileSystemModel;
fsModel->setRootPath(QDir::homePath());

QTreeView *tree = new QTreeView;
tree->setModel(fsModel);
tree->setRootIndex(fsModel->index(QDir::homePath()));

Qt 自带的 QFileSystemModel 已实现目录树 + 文件属性列,内部使用懒加载(展开时才读取子目录),是"现成模型"的最佳范例。

7.2 数据库结果集展示(QSqlQueryModel / QSqlTableModel)

QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName("app.db");
db.open();

QSqlTableModel *tableModel = new QSqlTableModel;
tableModel->setTable("users");
tableModel->select();

QTableView *view = new QTableView;
view->setModel(tableModel);          // 只读展示

需要"数据库 + 表格 + 搜索"三合一,组合 QSqlTableModel + QSortFilterProxyModel + QTableView 即可。注意:数据库模型默认是只读的,要支持编辑需 setEditStrategy(QSqlTableModel::OnFieldChange)。

7.3 日志列表(QListView / QTableView + 追加模型)

日志系统数据不断追加,自定义 QAbstractListModel 时插入行用 beginInsertRows/endInsertRows 包裹,并可用 view->scrollToBottom() 让新日志自动滚到底部。大数据量下建议配合"环形缓冲 + 只保留最近 N 条"避免界面卡顿。

7.4 多视图联动仪表盘

股票行情:一个数据模型同时驱动 K 线表格、分时图、自选列表三个视图,数据线程更新模型 → 三个视图同步刷新,这正是 Model/View 解耦的杀手锏场景。


8. 常见问题与避坑指南

8.1 索引失效:缓存 QModelIndex 后数据被增删

现象:保存的索引在插入/删除行后指向错误数据或崩溃。

原因:QModelIndex 是"位置 + 内部指针"的组合,行插入后位置偏移。

对策

  • 不要长期缓存索引,使用时通过 model->index(row, col) 现取;
  • 必须缓存时监听 rowsInserted/rowsRemoved 并在回调里更新;
  • 拿到 view 的选中项后尽快使用,避免跨事件处理。

8.2 修改了数据却不通知,界面不刷新

现象:Model 数据变了,视图纹丝不动。

原因:漏发 dataChanged,或结构变化没用 begin/end 包裹。

对策

// 内容修改:必发 dataChanged
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});

// 插入行:beginInsertRows/endInsertRows 包裹
beginInsertRows(QModelIndex(), row, row);
m_data.insert(row, item);
endInsertRows();

8.3 编辑后数据没写回(编辑未提交)

现象:双击单元格改了内容,失焦后恢复原样。

原因:常见三种——① flags() 没返回 Qt::ItemIsEditable;② setData() 没实现或返回 false;③ setData() 内部没发 dataChanged。

对策:三处缺一不可;另外注意默认提交策略是"编辑完成即写回",如需手动提交可设置 view->setItemDelegate 配合 setAutoEdit 调整交互。

8.4 角色误用:DisplayRole / EditRole / UserRole 混为一谈

现象:双击编辑框里是空/显示的不是原始值;自定义数据存错角色取不到。

对策

  • 展示用 Qt::DisplayRole;
  • 编辑器取值用 Qt::EditRole(应返回"原始类型"如 int/double,而非格式化后的字符串);
  • 业务 ID、排序键用 Qt::UserRole 及之后的自定义角色;
  • 排序/过滤通过 QSortFilterProxyModel::setSortRole() 指定角色,避免对显示文本排序出错(如按"金额 ¥1,234"字符串排序会乱)。

8.5 大数据量性能优化

手段说明
自定义 Model 而非 QStandardItemModel避免每格建 QStandardItem 的对象开销
data() 保持 O(1)用 QVector 等随机访问容器,勿线性遍历
懒加载 / 分页QFileSystemModel 模式:展开才取子节点;或 QSqlQueryModel 只取当前页
批量更新合并信号大量连续修改时只发一次 dataChanged(整体区域),避免视图逐格重绘
setUniformRowHeights(true)等高行开启统一行高,滚动性能显著提升
关闭不必要的 Role 计算data() 中未用到 ToolTip/Font 等角色时尽早 return

8.6 线程安全:Model 只能在主线程改

现象:工作线程直接改 Model,界面闪烁、崩溃或更新不及时。

原因:Model/View 不是线程安全的,视图的事件循环在主线程。

对策

  • 工作线程准备数据,通过信号槽(QueuedConnection)把"数据变更"投递回主线程,在主线程更新 Model;
  • 或使用 QMetaObject::invokeMethod(obj, ..., Qt::QueuedConnection);
  • 记住姊妹篇讲的信号槽线程亲和性:跨线程 emit 默认走队列连接,回调在主线程执行——这正是 Model 更新的正确姿势。

8.7 代理模型索引忘了 mapToSource

现象:视图绑了 QSortFilterProxyModel 后,点击"删除选中行"删错了行。

原因:视图索引是代理索引,直接按 row 去删源数据行,排序后行号对不上。

对策

QModelIndex proxyIndex = selModel->currentIndex();
QModelIndex srcIndex = proxy->mapToSource(proxyIndex);
// 用 srcIndex.row() 操作源模型

8.8 headerData 忘记判断 role

现象:表头出现"0、1、2"而不是自定义文本。

对策:headerData 里必须 if (role != Qt::DisplayRole) return QVariant();,否则其他角色(如对齐)查询也会命中你的分支。


9. 总结

  • 设计动机:Model/View 是 Qt 对 MVC 的实用化改造,把 Controller 并入 View/Delegate,用 QModelIndex 完成"位置寻址式"解耦通信;
  • 使用路径:小数据用 QStandardItemModel 即插即用;正式项目自定义 QAbstractTableModel/ListModel;树形结构重写 index()/parent();
  • 核心 API:data()/setData()/rowCount()/columnCount()/headerData() 是模型五虎将,Qt::ItemDataRole 让"一列多态",dataChanged/rowsInserted/rowsRemoved 是刷新三信号;
  • 进阶组合:QSortFilterProxyModel 实现排序过滤不污染源数据,QItemSelectionModel 管理选中并支持多视图联动;
  • 避坑主线:索引失效别缓存、改数据必通知、编辑三件套(flags/setData/dataChanged)、角色要分清、大模型要优化、跨线程改模型必须回主线程。

希望本文能帮你从"会用 QTableWidget"进阶到"懂 Model/View 架构"。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

翎_鸢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值