乐于分享
好东西不私藏

Qt QTableView 插件化实战:QVariant + Adapter 的可复用表格设计

Qt QTableView 插件化实战:QVariant + Adapter 的可复用表格设计

mainwindow8.cpp 是一个 QTableView 插件化 demo。它不是简单地在窗口里堆一个 QTableView,然后手动往 QStandardItemModel 里塞数据,而是把表格能力抽象成一个独立插件:MenuPluginTableViewWidget

这个 demo 的核心目标很明确:

  1. 表格插件负责通用能力,比如显示、筛选、拖拽、排序、冻结列、选择状态。
  2. 业务代码只负责准备数据,以及告诉插件“每一行应该怎么变成表格单元格”。
  3. 数据通过 QVariant 承载,插件不绑定具体业务结构。
  4. 行内容通过 Adapter 生成,buildRowItems 可以由使用者自定义。

这套设计和 Android 里的 ListView + AdapterRecyclerView + Adapter 思路很像:控件本身只管展示和交互,具体数据长什么样、每一行怎么画,由外部适配器决定。

效果:

一、为什么要把 QTableView 抽成插件

直接在窗口代码里写 QTableView,最常见的写法大概是这样:

auto *model = new QStandardItemModel(this);model->setHorizontalHeaderLabels({"编号""名称""状态"});model->appendRow({    new QStandardItem("TC-001"),    new QStandardItem("登录"),    new QStandardItem("已完成")});auto *tableView = new QTableView(this);tableView->setModel(model);

这种写法在小 demo 里没问题,但工程里很快会遇到几个问题:

  1. 每个页面都要重复写表格初始化代码。
  2. 每个页面都要重复处理筛选、拖拽、表头、列宽、行高、冻结列。
  3. 表格和业务数据耦合很重,换一种数据结构就要改一堆表格逻辑。
  4. 想复用时很困难,因为代码散落在 MainWindow 里。

所以 mainwindow8.cpp 的重构方向是:把表格通用能力放进 menuplugintableviewwidget,业务 demo 只负责配置它。

最终结构可以理解成三层:

MainWindow8    负责 demo 页面、按钮、配置项、业务数据 DemoTableRowDataMenuPluginVariantTableViewAdapter    负责把 QVariant 数据转成 QStandardItem 列表MenuPluginTableViewWidget    负责 QTableView、QStandardItemModel、筛选、拖拽、冻结列、信号

这样一来,后面如果有另一个页面也要用 QTableView,不需要复制 mainwindow8.cpp 的整套逻辑,只要新建一个 adapter 配置即可。

这样一来,后面如果有另一个页面也要用 QTableView,不需要复制 mainwindow8.cpp 的整套逻辑,只要新建一个 adapter 配置即可。

二、整体设计:插件不认识业务数据,只认识 QVariant

mainwindow8.cpp 里定义了一个 demo 用的数据结构:

struct DemoTableRowData{    QString id;    QString title;    QString category;    QString status;    int score = 0;    QString owner;    QString updated;    QString note;    bool checked = false;};

这是业务数据。插件本身不应该依赖它,否则插件就只能服务这个 demo。

所以代码里使用了:

Q_DECLARE_METATYPE(DemoTableRowData)

以及:

rows.append(QVariant::fromValue(buildRowData(i)));

这一步很关键。

QVariant 可以理解成 Qt 里的“通用盒子”。不同业务页面可以把自己的结构体、字符串、数字、对象包装进 QVariant,然后交给插件。插件不关心盒子里具体是什么,只把它传给 adapter。

真正知道 QVariant 里面是什么的人,是业务侧配置的 adapter。

数据流大概是这样:

DemoTableRowData    ↓ QVariant::fromValue(...)QVariant    ↓ adapter->createRowItems(...)QList<QStandardItem *>    ↓ QStandardItemModelQTableView 显示

这就是 QVariant + Adapter 的核心价值:插件负责通用表格,业务负责解释数据。

三、Adapter 接口:插件和业务之间的边界

MenuPluginTableViewAdapter 是一个抽象接口,它规定了插件需要向业务侧询问哪些信息。

核心接口包括:

class MenuPluginTableViewAdapter{public:    virtualintrowCount() const = 0;    virtual QVariant rowAt(int rowconst = 0;    virtualintcolumnCount() const = 0;    virtual QStringList headerLabels() const = 0;    virtual QList<QStandardItem *> createRowItems(const QVariant &rowData, int rowIndex) const = 0;    virtual QStringList filterTexts(const QVariant &rowData, int rowIndexconst;    virtualboolmoveRow(intfromint to);    virtualboolmoveCell(int fromRow, int fromColumn, int toRow, int toColumn);};

这里最重要的是 createRowItems

插件在重建表格时,会遍历每一行数据:

const QVariant payload = m_adapter->rowAt(row);QList<QStandardItem *> items = m_adapter->createRowItems(payload, row);

也就是说,插件并不自己决定单元格长什么样。它只问 adapter:

“这条 QVariant 数据应该生成哪些 QStandardItem?”

Adapter 返回以后,插件再把这些 item 放进 QStandardItemModel,最终由 QTableView 显示。

四、buildRowItems 能不能自定义?

可以,而且这个 demo 已经按“可自定义”的方式实现了。

在 MenuPluginVariantTableViewAdapter 里有一个配置入口:

using RowItemsBuilder = std::function<QList<QStandardItem *>(const QVariant &rowData, int rowIndex)>;voidsetRowItemsBuilder(const RowItemsBuilder &builder);
业务代码可以这样设置:
m_tableAdapter->setRowItemsBuilder([](const QVariant &item, int) {    return buildDemoRowItems(item.value<DemoTableRowData>());});

这意味着 buildRowItems 不是写死在插件里的。

默认情况下,如果没有设置 setRowItemsBuilder,adapter 可以根据列值解析器走一套通用逻辑。但只要业务传入了 RowItemsBuilder,插件就会使用业务自己的行构建函数。

这也是这个插件最重要的扩展点。

比如 demo 里的 buildDemoRowItems 做了这些事:

auto *idItem = new QStandardItem(rowData.id);idItem->setEditable(false);idItem->setTextAlignment(Qt::AlignCenter);auto *statusItem = new QStandardItem(rowData.status);statusItem->setEditable(true);statusItem->setCheckable(true);statusItem->setCheckState(rowData.checked ? Qt::Checked : Qt::Unchecked);auto *scoreItem = new QStandardItem(QString::number(rowData.score));scoreItem->setData(rowData.score, Qt::EditRole);scoreItem->setTextAlignment(Qt::AlignCenter);

这里可以看到,业务不只是设置文本,还可以设置:

  1. 是否可编辑。
  2. 是否居中。
  3. 是否带勾选框。
  4. DisplayRole
     和 EditRole 的数据。
  5. tooltip。
  6. 后续也可以扩展字体、颜色、图标、自定义 role。

所以它不是简单的“字符串表格”,而是把 QTableView 的 item 能力暴露给业务侧。

五、为什么不用 QTableWidget,而是 QTableView + QStandardItemModel

Qt 里有两个常见表格控件:

QTableWidget    使用简单,适合小型、固定结构表格QTableView    更偏 Model/View,适合工程化、复用、扩展

这个 demo 选择 QTableView + QStandardItemModel,主要原因是:

  1. 插件可以统一维护 model。
  2. 后续可以替换成自定义 model。
  3. 更适合做筛选、排序、冻结列、拖拽等高级能力。
  4. Adapter 抽象更自然。

QTableWidget 本质上是更方便的封装,但当项目需要长期维护和复用时,QTableView 更适合做插件底座。

六、MainWindow8 在 demo 中负责什么

MainWindow8 不再直接承载所有表格逻辑,它主要做四件事。

第一,创建业务数据:

QList<QVariantbuildDefaultRows(){    QList<QVariant> rows;    rows.reserve(14);    for (int i = 1; i <= 14; ++i) {        rows.append(QVariant::fromValue(buildRowData(i)));    }    return rows;}
第二,配置 adapter:
m_tableAdapter->setHeaderLabels(m_baseHeaderLabels);m_tableAdapter->setFilterTextResolver(...);m_tableAdapter->setRowItemsBuilder(...);m_tableAdapter->setCellMoveHandler(...);
第三,把 adapter 交给插件:
m_tablePluginWidget->setAdapter(m_tableAdapter);
第四,通过插件暴露出来的 tableView() 获取原生 QTableView,继续配置 Qt 自带能力:
m_tableView = m_tablePluginWidget->tableView();m_model = m_tablePluginWidget->model();m_tableView->setSortingEnabled(true);m_tableView->setAlternatingRowColors(true);m_tableView->setShowGrid(true);m_tableView->verticalHeader()->setVisible(false);
这种写法的好处是:插件提供通用能力,但不把 QTableView 锁死。业务页面仍然可以拿到原生 QTableView 做细节配置。
第五,通过QStyledItemDelegate来实现自定义QStandarditem单个表格的样式,我这里是用来实现标签功能
继承QStyledItemDelegate,来实现绘制,主要代码:
class TagListDelegate : public QStyledItemDelegate{public:    explicitTagListDelegate(QObject *parent = nullptr)        : QStyledItemDelegate(parent)    {    }    voidpaint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index)constoverride    {        const QList<TagInfo> tags = index.data(kTagListRole).value<QList<TagInfo>>();        if (tags.isEmpty()) {            QStyledItemDelegate::paint(painter, option, index);            return;        }        QStyleOptionViewItem opt(option);        initStyleOption(&opt, index);        opt.text.clear();        QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();        style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);        painter->save();        painter->setRenderHint(QPainter::Antialiasing, true);        const QRect contentRect = option.rect.adjusted(85-8-5);        const QFontMetrics metrics(option.font);        const int tagHeight = qMin(22qMax(18, contentRect.height()));        int x = contentRect.left();        const int y = contentRect.top() + (contentRect.height() - tagHeight) / 2;        for (int i = 0; i < tags.size(); ++i) {            const TagInfo &tag = tags.at(i);            const int tagWidth = metrics.horizontalAdvance(tag.text) + 18;            if (x + tagWidth > contentRect.right()) {                const QString moreText = QStringLiteral("+%1").arg(tags.size() - i);                const int moreWidth = metrics.horizontalAdvance(moreText) + 16;                if (x + moreWidth <= contentRect.right()) {                    const QRect moreRect(x, y, moreWidth, tagHeight);                    painter->setPen(Qt::NoPen);                    painter->setBrush(QColor(QStringLiteral("#CBD5E1")));                    painter->drawRoundedRect(moreRect, 88);                    painter->setPen(QColor(QStringLiteral("#334155")));                    painter->drawText(moreRect, Qt::AlignCenter, moreText);                }                break;            }            const QRect tagRect(x, y, tagWidth, tagHeight);            painter->setPen(Qt::NoPen);            painter->setBrush(tag.backgroundColor);            painter->drawRoundedRect(tagRect, 88);            painter->setPen(tag.textColor);            painter->drawText(tagRect.adjusted(90-90), Qt::AlignCenter, tag.text);            x += tagWidth + 6;        }        painter->restore();    }    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index)constoverride    {        QSize size = QStyledItemDelegate::sizeHint(option, index);        size.setHeight(qMax(size.height(), 32));        return size;    }};
使用委托
m_tableView->setItemDelegateForColumn(kTagsColumn, new TagListDelegate(m_tableView));
七、筛选功能:业务决定哪些字段参与搜索

筛选逻辑也没有写死。

插件只保存一个关键词,然后逐行判断这一行是否匹配:

QStringList filterTexts(const QVariant &rowData, int rowIndexconst;

业务侧通过 setFilterTextResolver 告诉插件:这条数据有哪些文本可以参与搜索。

demo 里大概是这样:

m_tableAdapter->setFilterTextResolver([](const QVariant &item, int) {    const DemoTableRowData rowData = item.value<DemoTableRowData>();    return QStringList{        rowData.id,        rowData.title,        rowData.category,        rowData.status,        rowData.owner,        rowData.updated,        rowData.note    };});

插件拿到这些文本以后,把它们拼起来和搜索关键词比较。

这比在插件里写死“搜索第 1 列、第 2 列、第 3 列”更灵活。不同业务页面可以决定不同字段是否参与搜索。

八、拖拽:同时支持按行拖动和按单元格拖动

QTableView 自带拖拽能力,但实际项目里常常需要更明确的业务规则。

这个插件里做了两类拖拽:

按行拖动    调用 adapter->moveRow(fromto)按单元格拖动    调用 adapter->moveCell(fromRow, fromColumn, toRow, toColumn)

判断方式和 QTableView 的选择行为有关。

如果当前是行选择模式,拖动就是行移动。

如果当前是单元格选择模式,拖动就是单元格移动。

demo 里的单元格拖动规则是:只允许同一列内交换。

bool swapDemoCellInSameColumn(QList<QVariant> &rows,                              int fromRow,                              int fromColumn,                              int toRow,                              int toColumn){    if (fromColumn != toColumn || fromRow == toRow) {        return false;    }    DemoTableRowData source = rows.at(fromRow).value<DemoTableRowData>();    DemoTableRowData target = rows.at(toRow).value<DemoTableRowData>();    const QVariant sourceValue = demoCellValue(source, fromColumn);    const QVariant targetValue = demoCellValue(target, toColumn);    setDemoCellValue(source, fromColumn, targetValue);    setDemoCellValue(target, toColumn, sourceValue);    rows[fromRow] = QVariant::fromValue(source);    rows[toRow] = QVariant::fromValue(target);    return true;}

为什么只允许同列交换?

因为不同列代表的数据类型可能不同。比如分数列是数字,负责人列是字符串,日期列是日期。如果允许跨列随便拖,业务数据会变脏。

所以插件只提供 moveCell 扩展点,具体能不能移动、移动后怎么改数据,由业务 adapter 决定。

这就是 adapter 设计的价值:插件不替业务做错误决定。

九、冻结列:用一个叠加的 QTableView 实现

用户提到一个典型需求:表格列很多时会出现横向滚动条,希望某几列冻结在左侧。

这个插件支持多列冻结:

voidsetFrozenColumnCount(int count);intfrozenColumnCount() const;
demo 里通过一个 SpinBox 控制冻结列数量:
m_tablePluginWidget->setFrozenColumnCount(m_frozenColumnSpinBox->value());
实现方式不是修改 QTableView 的内部滚动机制,而是使用两个 QTableView:
主 QTableView    显示完整表格,负责正常滚动冻结 QTableView    叠在主表格左侧,只显示前 N 列
冻结表格和主表格共享同一个 model:
m_tableView->setModel(m_model);m_frozenTableView->setModel(m_model);
并且共享 selection model:
m_frozenTableView->setSelectionModel(m_tableView->selectionModel());

这样用户选中某一行或某个单元格时,两个表格的选中状态是一致的。

冻结表格还会同步主表格的垂直滚动:

connect(m_tableView->verticalScrollBar(), &QScrollBar::valueChanged,        m_frozenTableView->verticalScrollBar(), &QScrollBar::setValue);
当列宽、行高、表头显示状态变化时,插件会重新计算冻结区域:
updateFrozenColumns();updateFrozenGeometry();syncFrozenRowHeights();

这个方案的优点是实现清晰,不需要重写复杂的 QTableView 绘制逻辑。

需要注意的是,冻结列不应该单独变色。当前设计中冻结表格同步主表格 palette,不额外设置背景样式:

m_frozenTableView->setPalette(m_tableView->palette());m_frozenTableView->viewport()->setPalette(m_tableView->viewport()->palette());
这样冻结列看起来和普通列一致,只是位置固定。
十、隐藏 1、2、3、4 这种序号列
QTableView 默认左侧有一个垂直表头,里面显示行号:
1234...

这不是业务列,而是 Qt 的 row header。

demo 中已经默认隐藏它:

m_tableView->verticalHeader()->setVisible(false);
十一、排序箭头:不用 Qt 默认箭头,避免表头文字跳动

QTableView 开启排序以后,Qt 默认会在表头显示排序指示器。

问题是:默认排序箭头会占用表头内部空间。当点击某一列时,这一列的文字可能会轻微左移;点击其他列后,原来的列又恢复居中。

这就是用户看到的“点击模块列,模块文字向左移动一点”的原因。

demo 的处理方式是:关闭 Qt 默认排序指示器。

m_tableView->horizontalHeader()->setSortIndicatorShown(false);
然后自己在表头文字里追加箭头:
void MainWindow8::updateSortHeaderLabels(int sortColumn, Qt::SortOrder order){    QStringList labels = m_baseHeaderLabels;    if (m_sortIndicatorCheckBox->isChecked()        && sortColumn >= 0        && sortColumn < labels.size()) {        labels[sortColumn] += (order == Qt::AscendingOrder)            ? QStringLiteral(" ↑")            : QStringLiteral(" ↓");    }    m_model->setHorizontalHeaderLabels(labels);}

这样箭头是文字的一部分,不再由 QHeaderView 单独绘制,表头布局就稳定了。

顺序和倒序可以通俗理解为:

↑ 顺序 / 升序    从小到大,例如 123    从 A 到 Z    从较早日期到较晚日期↓ 倒序 / 降序    从大到小,例如 321    从 Z 到 A    从较晚日期到较早日期
这个方案简单、稳定,也方便以后换成其他符号,比如  / 
十二、插件对外暴露了哪些能力
MenuPluginTableViewWidget 对外提供了一组常用接口:
voidsetAdapter(MenuPluginTableViewAdapter *adapter);MenuPluginTableViewAdapter *adapter() const;QTableView *tableView() const;QStandardItemModel *model() const;voidsetFrozenColumnCount(int count);intfrozenColumnCount() const;voidrebuild();voidsetFilterKeyword(const QString &keyword);QString filterKeyword() const;QVariant rowData(const QModelIndex &indexconst;QList<QVariant> rows() const;

这里有两个接口特别重要。

第一个是 tableView()

它允许外部继续使用 QTableView 原生能力,比如排序、选择模式、列宽策略、拖拽模式。

第二个是 model()

它允许外部在必要时读取或调整 QStandardItemModel,比如设置表头、span、item role。

这两个接口让插件既封装了通用逻辑,又没有把底层能力完全封死。

十三、信号设计:插件把交互结果通知外部
插件不是只显示数据,它还会把关键交互通知外部:
signals:    voidcellClicked(const QModelIndex &index, const QVariant &rowData);    voidcellDoubleClicked(const QModelIndex &index, const QVariant &rowData);    voidselectionChanged(int selectedRows, int selectedCells);    voidrowsReordered(const QList<QVariant> &rows);    voidcellsMoved(const QList<QVariant> &rows);
这样 MainWindow 或其他业务页面可以监听:
connect(m_tablePluginWidget, &MenuPluginTableViewWidget::cellsMoved,        this, [this](const QList<QVariant> &rows) {            // 保存拖拽后的数据        });

这里仍然保持了 QVariant 的抽象。

插件不会把数据强转成 DemoTableRowData,因为它不应该知道业务数据类型。

十四、如何在其他页面复用这个插件

如果另一个页面也要用这个表格插件,大致步骤如下。

第一步,定义自己的业务数据:

struct UserRow{    QString name;    int age;    QString role;};Q_DECLARE_METATYPE(UserRow)
第二步,把业务数据包装成 QVariant:
QList<QVariant> rows;rows.append(QVariant::fromValue(UserRow{"张三"28"管理员"}));rows.append(QVariant::fromValue(UserRow{"李四"31"普通用户"}));
第三步,创建 adapter:
auto *adapter = new MenuPluginVariantTableViewAdapter;adapter->setHeaderLabels({"姓名""年龄""角色"});adapter->setRows(rows);
第四步,自定义行构建逻辑:
adapter->setRowItemsBuilder([](const QVariant &item, int) {    const UserRow row = item.value<UserRow>();    auto *nameItem = new QStandardItem(row.name);    auto *ageItem = new QStandardItem(QString::number(row.age));    ageItem->setData(row.age, Qt::EditRole);    ageItem->setTextAlignment(Qt::AlignCenter);    auto *roleItem = new QStandardItem(row.role);    return QList<QStandardItem *>{nameItem, ageItem, roleItem};});
第五步,把 adapter 交给插件:
auto *tablePlugin = new MenuPluginTableViewWidget(this);tablePlugin->setAdapter(adapter);tablePlugin->rebuild();
第六步,如果需要冻结列:
tablePlugin->setFrozenColumnCount(1);

这样就完成了一个新的业务表格。

不需要复制 mainwindow8.cpp 里的筛选、冻结列、拖拽、信号处理底层逻辑。

十五、这个设计的优点

这个 demo 的设计有几个明显优点。

第一,业务数据和表格控件解耦。

插件不认识 DemoTableRowData,只认识 QVariant。换一个业务结构体,插件不用改。

第二,行内容可以完全自定义。

通过 setRowItemsBuilder,用户可以自己决定每一列用什么 QStandardItem,也可以设置编辑、对齐、勾选、tooltip、role。

第三,通用能力可以复用。

冻结列、筛选、拖拽、选择变化、点击信号都在插件里,多个页面可以共用。

第四,扩展点清晰。

行移动走 moveRow,单元格移动走 moveCell,筛选文本走 filterTexts,行构建走 createRowItems

第五,没有破坏 Qt 原生能力。

外部仍然能通过 tableView() 和 model() 访问原生 QTableView / QStandardItemModel。

十六、需要注意的地方
第一,自定义结构体放进 QVariant 前,要声明元类型。
Q_DECLARE_METATYPE(MyRowData)
如果还需要跨线程 signal/slot 传递,通常还要注册:
qRegisterMetaType<MyRowData>("MyRowData");

第二,createRowItems 里 new 出来的 QStandardItem 交给 model 管理,不要手动 delete。

第三,单元格拖拽不要盲目允许跨列移动。

不同列的数据含义不同,跨列移动很容易破坏业务数据。推荐像 demo 一样,由 setCellMoveHandler 自己判断是否合法。

第四,冻结列是叠加视图方案。

它实现简单、效果稳定,但要注意同步行高、列宽、隐藏行、表头显示状态。

第五,排序箭头如果使用 Qt 默认指示器,表头文字可能会轻微移动。

当前 demo 用“修改表头文本”的方式显示  / ,就是为了解决这个视觉跳动。

十七、总结

mainwindow8.cpp 这个 demo 的重点不是 QTableView 某一个 API,而是一个可复用表格插件的设计方式。

它把表格拆成了三部分:

数据    DemoTableRowData,或者其他业务结构体适配器    MenuPluginTableViewAdapter / MenuPluginVariantTableViewAdapter视图插件    MenuPluginTableViewWidget

其中最关键的是 QVariant + Adapter

QVariant 负责让插件不依赖具体业务类型,Adapter 负责把业务数据转成表格可显示的 QStandardItem

所以 buildRowItems 不但可以自定义,而且应该作为这个插件最核心的扩展点来使用。

当后续项目中再出现新的表格页面时,不需要重新写一套 QTableView 逻辑,只需要:

  1. 定义业务数据。
  2. 包装成 QVariant。
  3. 配置 adapter。
  4. 自定义 setRowItemsBuilder
  5. 把 adapter 设置给 MenuPluginTableViewWidget

这样表格插件就可以真正做到复用,而不是只服务某一个 demo 页面。

最后源码:

mainwindow8.h

#pragma once#include <QList>#include <QMainWindow>#include <QVariant>classQCheckBox;classQComboBox;classQDateEdit;classQLabel;classMenuPluginTableViewWidget;classMenuPluginVariantTableViewAdapter;classQLineEdit;classQPushButton;classQSpinBox;classQStandardItemModel;classQTableView;classMainWindow8 : publicQMainWindow{public:    explicit MainWindow8(QWidget *parent = nullptr);private:    void buildUi();    void buildControlPanel(QWidget *parent);    void buildTablePanel(QWidget *parent);    void populateModel();    void syncAdapterRows(const QList<QVariant> &rows);    void appendDemoRow(int rowIndex);    void applyViewSettings();    void applyFilter();    void updateStatusText(const QString &prefix = QString());    void updateSortHeaderLabels(int sortColumn = -1, Qt::SortOrder order = Qt::AscendingOrder);    static QString buildStyleSheet();    MenuPluginTableViewWidget *m_tablePluginWidget;    MenuPluginVariantTableViewAdapter *m_tableAdapter;    QTableView *m_tableView;    QStandardItemModel *m_model;    QLabel *m_statusLabel;    QLabel *m_selectionLabel;    QLineEdit *m_filterEdit;    QComboBox *m_selectionBehaviorCombo;    QComboBox *m_selectionModeCombo;    QComboBox *m_editTriggerCombo;    QComboBox *m_dragDropModeCombo;    QComboBox *m_horizontalResizeCombo;    QComboBox *m_verticalResizeCombo;    QComboBox *m_scrollModeCombo;    QCheckBox *m_sortCheckBox;    QCheckBox *m_alternatingColorCheckBox;    QCheckBox *m_showGridCheckBox;    QCheckBox *m_wordWrapCheckBox;    QCheckBox *m_cornerButtonCheckBox;    QCheckBox *m_rowHeaderCheckBox;    QCheckBox *m_columnHeaderCheckBox;    QCheckBox *m_stretchLastSectionCheckBox;    QCheckBox *m_sortIndicatorCheckBox;    QCheckBox *m_spanFirstRowCheckBox;    QCheckBox *m_hideLowScoreRowsCheckBox;    QSpinBox *m_rowHeightSpinBox;    QSpinBox *m_columnWidthSpinBox;    QSpinBox *m_frozenColumnSpinBox;    QPushButton *m_addRowButton;    QPushButton *m_removeRowButton;    QPushButton *m_resetButton;    QStringList m_baseHeaderLabels;    int m_nextRowNumber;};

mainwindow8.cpp

#include"mainwindow8.h"#include"menuplugintableviewwidget.h"#include<algorithm>#include<QAbstractItemView>#include<QApplication>#include<QColor>#include<QCheckBox>#include<QComboBox>#include<QDate>#include<QFontMetrics>#include<QFormLayout>#include<QFrame>#include<QHeaderView>#include<QHBoxLayout>#include<QItemSelectionModel>#include<QLabel>#include<QLineEdit>#include<QPainter>#include<QPushButton>#include<QScrollArea>#include<QSpinBox>#include<QStandardItem>#include<QStandardItemModel>#include<QStatusBar>#include<QStyle>#include<QStyledItemDelegate>#include<QStringList>#include<QTableView>#include<QVBoxLayout>#include<QWidget>// MainWindow8 是 menuplugintableviewwidget 的使用示例。// 这个文件刻意把“业务数据”和“通用表格插件”分开:// 1. DemoTableRowData 表示业务层的一行原始数据。// 2. MenuPluginVariantTableViewAdapter 负责把 QVariant 数据适配成 QStandardItem。// 3. MenuPluginTableViewWidget 负责 QTableView 的通用能力,如筛选、拖拽、冻结列和信号转发。struct TagInfo{    QString text;    QColor backgroundColor;    QColor textColor;};Q_DECLARE_METATYPE(TagInfo)Q_DECLARE_METATYPE(QList<TagInfo>)struct DemoTableRowData{    QString id;    QString title;    QString category;    QString status;    QList<TagInfo> tags;    int score = 0;    QString owner;    QString updated;    QString note;    bool checked = false;};// QVariant 要保存自定义结构体时,需要先声明 Qt 元类型。// 这样 DemoTableRowData 才能通过 QVariant::fromValue(...) 装箱,// 后续再通过 item.value<DemoTableRowData>() 还原。Q_DECLARE_METATYPE(DemoTableRowData)namespace {constexpr int kTagsColumn = 4;constexpr int kTagListRole = Qt::UserRole + 100;QStringList tagTexts(const QList<TagInfo> &tags){    QStringList texts;    texts.reserve(tags.size());    for (const TagInfo &tag : tags) {        texts.append(tag.text);    }    return texts;}class TagListDelegate : public QStyledItemDelegate{public:    explicitTagListDelegate(QObject *parent = nullptr)        : QStyledItemDelegate(parent)    {    }    voidpaint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index)constoverride    {        const QList<TagInfo> tags = index.data(kTagListRole).value<QList<TagInfo>>();        if (tags.isEmpty()) {            QStyledItemDelegate::paint(painter, option, index);            return;        }        QStyleOptionViewItem opt(option);        initStyleOption(&opt, index);        opt.text.clear();        QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();        style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);        painter->save();        painter->setRenderHint(QPainter::Antialiasing, true);        const QRect contentRect = option.rect.adjusted(85-8-5);        const QFontMetrics metrics(option.font);        const int tagHeight = qMin(22qMax(18, contentRect.height()));        int x = contentRect.left();        const int y = contentRect.top() + (contentRect.height() - tagHeight) / 2;        for (int i = 0; i < tags.size(); ++i) {            const TagInfo &tag = tags.at(i);            const int tagWidth = metrics.horizontalAdvance(tag.text) + 18;            if (x + tagWidth > contentRect.right()) {                const QString moreText = QStringLiteral("+%1").arg(tags.size() - i);                const int moreWidth = metrics.horizontalAdvance(moreText) + 16;                if (x + moreWidth <= contentRect.right()) {                    const QRect moreRect(x, y, moreWidth, tagHeight);                    painter->setPen(Qt::NoPen);                    painter->setBrush(QColor(QStringLiteral("#CBD5E1")));                    painter->drawRoundedRect(moreRect, 88);                    painter->setPen(QColor(QStringLiteral("#334155")));                    painter->drawText(moreRect, Qt::AlignCenter, moreText);                }                break;            }            const QRect tagRect(x, y, tagWidth, tagHeight);            painter->setPen(Qt::NoPen);            painter->setBrush(tag.backgroundColor);            painter->drawRoundedRect(tagRect, 88);            painter->setPen(tag.textColor);            painter->drawText(tagRect.adjusted(90-90), Qt::AlignCenter, tag.text);            x += tagWidth + 6;        }        painter->restore();    }    QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index)constoverride    {        QSize size = QStyledItemDelegate::sizeHint(option, index);        size.setHeight(qMax(size.height(), 32));        return size;    }};// 以下几个函数只服务本 demo,用来构造模拟数据。// 真实项目中可以替换成接口返回、数据库读取或本地配置文件读取。QString categoryForIndex(int index){    static const QStringList categories = {        QStringLiteral("登录"),        QStringLiteral("档案"),        QStringLiteral("设备"),        QStringLiteral("测量"),        QStringLiteral("回放"),        QStringLiteral("报告"),        QStringLiteral("系统")    };    return categories.at(index % categories.size());}QString statusForIndex(int index){    static const QStringList statuses = {        QStringLiteral("已完成"),        QStringLiteral("进行中"),        QStringLiteral("待复测"),        QStringLiteral("待确认")    };    return statuses.at(index % statuses.size());}DemoTableRowData buildRowData(int rowIndex){    DemoTableRowData rowData;    rowData.id = QStringLiteral("TC-%1").arg(rowIndex, 310QLatin1Char('0'));    rowData.title = QStringLiteral("功能项 %1").arg(rowIndex, 310QLatin1Char('0'));    rowData.category = categoryForIndex(rowIndex);    rowData.status = statusForIndex(rowIndex);    rowData.tags = {        {QStringLiteral("正常"), QColor(QStringLiteral("#22C55E")), QColor(Qt::white)},        {QStringLiteral("心电"), QColor(QStringLiteral("#3B82F6")), QColor(Qt::white)},        {rowIndex % 2 == 0 ? QStringLiteral("已审核") : QStringLiteral("待审核"),            rowIndex % 2 == 0 ? QColor(QStringLiteral("#F59E0B")) : QColor(QStringLiteral("#64748B")),            QColor(Qt::white)}    };    rowData.score = 68 + (rowIndex * 7) % 31;    rowData.owner = QStringLiteral("工程师 %1").arg((rowIndex % 5) + 1);    rowData.updated = QDate::currentDate().addDays(-rowIndex).toString(QStringLiteral("yyyy-MM-dd"));    rowData.note = QStringLiteral("支持排序、选择、编辑、拖放、表头调节、过滤和跨度显示演示。");    rowData.checked = rowIndex % 2 == 0;    return rowData;}// 插件侧统一接收 QList<QVariant>,而不是接收 QList<DemoTableRowData>。// 这样 MenuPluginTableViewWidget 不依赖任何具体业务类型,其他页面也能复用。QList<QVariant> buildDefaultRows(){    QList<QVariant> rows;    rows.reserve(14);//初始化空间长度,避免后续append时多次扩容    for (int i = 1; i <= 14; ++i) {        rows.append(QVariant::fromValue(buildRowData(i)));    }    return rows;}// 这是本 demo 最关键的“行布局构建函数”。// Adapter 会把每一条 QVariant 还原成 DemoTableRowData,然后调用这里生成一整行 QStandardItem。// 如果其他业务页面要复用插件,只需要换成自己的 RowItemsBuilder 即可。QList<QStandardItem *> buildDemoRowItems(const DemoTableRowData &rowData){    auto *idItem = new QStandardItem(rowData.id);    idItem->setEditable(false);    idItem->setTextAlignment(Qt::AlignCenter);    auto *titleItem = new QStandardItem(rowData.title);    titleItem->setEditable(true);    auto *categoryItem = new QStandardItem(rowData.category);    categoryItem->setEditable(true);    categoryItem->setTextAlignment(Qt::AlignCenter);    auto *statusItem = new QStandardItem(rowData.status);    statusItem->setEditable(true);    statusItem->setCheckable(true);    statusItem->setCheckState(rowData.checked ? Qt::Checked : Qt::Unchecked);    statusItem->setToolTip(QStringLiteral("勾选框演示:QTableView 默认委托会直接显示可勾选项。"));    auto *tagsItem = new QStandardItem(tagTexts(rowData.tags).join(QStringLiteral(",")));    tagsItem->setEditable(false);    tagsItem->setData(QVariant::fromValue(rowData.tags), kTagListRole);    tagsItem->setToolTip(QStringLiteral("标签列使用 QStyledItemDelegate 绘制,每个标签可拥有独立背景色和文字色。"));    auto *scoreItem = new QStandardItem(QString::number(rowData.score));    scoreItem->setData(rowData.score, Qt::EditRole);    scoreItem->setTextAlignment(Qt::AlignCenter);    auto *ownerItem = new QStandardItem(rowData.owner);    auto *updatedItem = new QStandardItem(rowData.updated);    updatedItem->setTextAlignment(Qt::AlignCenter);    auto *noteItem = new QStandardItem(rowData.note);    return {idItem, titleItem, categoryItem, statusItem, tagsItem, scoreItem, ownerItem, updatedItem, noteItem};}// 单元格拖动时,需要能从业务结构体中按列读出值。// 这里把“列号 -> 字段”的映射集中放在一个函数里,避免拖拽逻辑直接散落 switch。QVariant demoCellValue(const DemoTableRowData &rowData, int column){    switch (column) {    case 0:        return rowData.id;    case 1:        return rowData.title;    case 2:        return rowData.category;    case 3:        return rowData.status;    case 4:        return QVariant::fromValue(rowData.tags);    case 5:        return rowData.score;    case 6:        return rowData.owner;    case 7:        return rowData.updated;    case 8:        return rowData.note;    default:        return QVariant();    }}// 和 demoCellValue 对应,用于单元格拖动后把新值写回业务结构体。// 插件本身不会也不应该知道 DemoTableRowData 有哪些字段。voidsetDemoCellValue(DemoTableRowData &rowData, int column, const QVariant &value){    switch (column) {    case 0:        rowData.id = value.toString();        break;    case 1:        rowData.title = value.toString();        break;    case 2:        rowData.category = value.toString();        break;    case 3:        rowData.status = value.toString();        break;    case 4:        rowData.tags = value.value<QList<TagInfo>>();        break;    case 5:        rowData.score = value.toInt();        break;    case 6:        rowData.owner = value.toString();        break;    case 7:        rowData.updated = value.toString();        break;    case 8:        rowData.note = value.toString();        break;    default:        break;    }}// 单元格拖拽的业务规则:只允许同一列内交换内容。// 不允许跨列交换,是为了避免把“分数”拖到“负责人”这类数据类型不匹配的问题。// 插件只负责捕获拖拽动作,真正是否允许移动由 Adapter 的 CellMoveHandler 决定。boolswapDemoCellInSameColumn(QList<QVariant> &rows, int fromRow, int fromColumn, int toRow, int toColumn){    if (fromColumn != toColumn || fromRow == toRow) {        return false;    }    DemoTableRowData source = rows.at(fromRow).value<DemoTableRowData>();    DemoTableRowData target = rows.at(toRow).value<DemoTableRowData>();    const QVariant sourceValue = demoCellValue(source, fromColumn);    const QVariant targetValue = demoCellValue(target, toColumn);    setDemoCellValue(source, fromColumn, targetValue);    setDemoCellValue(target, toColumn, sourceValue);    rows[fromRow] = QVariant::fromValue(source);    rows[toRow] = QVariant::fromValue(target);    return true;}// namespaceMainWindow8::MainWindow8(QWidget *parent)    : QMainWindow(parent)    , m_tablePluginWidget(nullptr)    , m_tableAdapter(nullptr)    , m_tableView(nullptr)    , m_model(nullptr)    , m_statusLabel(nullptr)    , m_selectionLabel(nullptr)    , m_filterEdit(nullptr)    , m_selectionBehaviorCombo(nullptr)    , m_selectionModeCombo(nullptr)    , m_editTriggerCombo(nullptr)    , m_dragDropModeCombo(nullptr)    , m_horizontalResizeCombo(nullptr)    , m_verticalResizeCombo(nullptr)    , m_scrollModeCombo(nullptr)    , m_sortCheckBox(nullptr)    , m_alternatingColorCheckBox(nullptr)    , m_showGridCheckBox(nullptr)    , m_wordWrapCheckBox(nullptr)    , m_cornerButtonCheckBox(nullptr)    , m_rowHeaderCheckBox(nullptr)    , m_columnHeaderCheckBox(nullptr)    , m_stretchLastSectionCheckBox(nullptr)    , m_sortIndicatorCheckBox(nullptr)    , m_spanFirstRowCheckBox(nullptr)    , m_hideLowScoreRowsCheckBox(nullptr)    , m_rowHeightSpinBox(nullptr)    , m_columnWidthSpinBox(nullptr)    , m_frozenColumnSpinBox(nullptr)    , m_addRowButton(nullptr)    , m_removeRowButton(nullptr)    , m_resetButton(nullptr)    , m_baseHeaderLabels()    , m_nextRowNumber(1){    // 注册元类型后,DemoTableRowData 可以更安全地参与 QVariant 和 Qt 元对象系统。    qRegisterMetaType<TagInfo>("TagInfo");    qRegisterMetaType<QList<TagInfo>>("QList<TagInfo>");    qRegisterMetaType<DemoTableRowData>("DemoTableRowData");    // 初始化顺序:    // 1. buildUi 创建控制区和表格插件。    // 2. populateModel 准备 QVariant 行数据并交给 adapter。    // 3. applyViewSettings 把右侧控制项同步到 QTableView。    // 4. applyFilter 应用初始过滤条件。    buildUi();    setStyleSheet(buildStyleSheet());    populateModel();    applyViewSettings();    applyFilter();    updateStatusText(QStringLiteral("QTableView 插件示例已就绪"));    statusBar()->showMessage(QStringLiteral("QTableView 适配器插件示例已加载"), 3000);}voidMainWindow8::buildUi(){    // 页面分成左右两块:左侧是表格控制面板,右侧是 QTableView 插件预览区。    // MainWindow8 只做 demo 编排,不把通用表格逻辑写死在窗口里。    resize(1500920);    setWindowTitle(QStringLiteral("QTableView 插件化演示"));    auto *centralWidget = new QWidget(this);    auto *rootLayout = new QVBoxLayout(centralWidget);    rootLayout->setContentsMargins(24222424);    rootLayout->setSpacing(16);    auto *titleLabel = new QLabel(QStringLiteral("QTableView 插件化演示"), centralWidget);    titleLabel->setObjectName(QStringLiteral("titleLabel"));    auto *descLabel = new QLabel(        QStringLiteral("MainWindow8 只负责传入 QVariant 行数据和 adapter 配置,表格控件、模型重建和关键字过滤由 menuplugintableviewwidget 统一承接。"),        centralWidget);    descLabel->setObjectName(QStringLiteral("descLabel"));    descLabel->setWordWrap(true);    auto *contentLayout = new QHBoxLayout();    contentLayout->setSpacing(16);    auto *controlCard = new QFrame(centralWidget);    controlCard->setObjectName(QStringLiteral("controlCard"));    buildControlPanel(controlCard);    auto *tableCard = new QFrame(centralWidget);    tableCard->setObjectName(QStringLiteral("tableCard"));    buildTablePanel(tableCard);    contentLayout->addWidget(controlCard, 0);    contentLayout->addWidget(tableCard, 1);    rootLayout->addWidget(titleLabel);    rootLayout->addWidget(descLabel);    rootLayout->addLayout(contentLayout, 1);    setCentralWidget(centralWidget);}voidMainWindow8::buildControlPanel(QWidget *parent){    // 控制项比较多,直接塞进固定高度的 QFormLayout 容易在窗口变小时互相挤压。    // 所以这里外层使用 QScrollArea:高度不够时滚动,而不是压缩控件。    auto *layout = new QVBoxLayout(parent);    layout->setContentsMargins(16161616);    layout->setSpacing(14);    auto *panelTitle = new QLabel(QStringLiteral("表格控制"), parent);    panelTitle->setObjectName(QStringLiteral("panelTitle"));    layout->addWidget(panelTitle);    auto *scrollArea = new QScrollArea(parent);    scrollArea->setObjectName(QStringLiteral("controlScrollArea"));    scrollArea->setWidgetResizable(true);    scrollArea->setFrameShape(QFrame::NoFrame);    scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);    auto *controlContent = new QWidget(scrollArea);    auto *controlLayout = new QVBoxLayout(controlContent);    controlLayout->setContentsMargins(0060);    controlLayout->setSpacing(12);    auto *formLayout = new QFormLayout();    formLayout->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);    formLayout->setLabelAlignment(Qt::AlignLeft | Qt::AlignVCenter);    formLayout->setFormAlignment(Qt::AlignTop);    formLayout->setHorizontalSpacing(12);    formLayout->setVerticalSpacing(10);    m_selectionBehaviorCombo = new QComboBox(parent);    m_selectionBehaviorCombo->addItem(QStringLiteral("按单元格"), static_cast<int>(QAbstractItemView::SelectItems));    m_selectionBehaviorCombo->addItem(QStringLiteral("按整行"), static_cast<int>(QAbstractItemView::SelectRows));    m_selectionModeCombo = new QComboBox(parent);    m_selectionModeCombo->addItem(QStringLiteral("单选"), static_cast<int>(QAbstractItemView::SingleSelection));    m_selectionModeCombo->addItem(QStringLiteral("多选"), static_cast<int>(QAbstractItemView::MultiSelection));    m_selectionModeCombo->addItem(QStringLiteral("扩展选择"), static_cast<int>(QAbstractItemView::ExtendedSelection));    m_selectionModeCombo->addItem(QStringLiteral("连续选择"), static_cast<int>(QAbstractItemView::ContiguousSelection));    m_selectionModeCombo->addItem(QStringLiteral("不可选择"), static_cast<int>(QAbstractItemView::NoSelection));    m_editTriggerCombo = new QComboBox(parent);    m_editTriggerCombo->addItem(QStringLiteral("双击编辑"), static_cast<int>(QAbstractItemView::DoubleClicked));    m_editTriggerCombo->addItem(QStringLiteral("编辑键触发"), static_cast<int>(QAbstractItemView::EditKeyPressed));    m_editTriggerCombo->addItem(QStringLiteral("选中后单击编辑"), static_cast<int>(QAbstractItemView::SelectedClicked));    m_editTriggerCombo->addItem(QStringLiteral("任意按键编辑"), static_cast<int>(QAbstractItemView::AnyKeyPressed));    m_editTriggerCombo->addItem(QStringLiteral("全部触发"), static_cast<int>(QAbstractItemView::AllEditTriggers));    m_editTriggerCombo->addItem(QStringLiteral("禁止编辑"), static_cast<int>(QAbstractItemView::NoEditTriggers));    m_dragDropModeCombo = new QComboBox(parent);    m_dragDropModeCombo->addItem(QStringLiteral("禁止拖放"), static_cast<int>(QAbstractItemView::NoDragDrop));    m_dragDropModeCombo->addItem(QStringLiteral("仅拖动"), static_cast<int>(QAbstractItemView::DragOnly));    m_dragDropModeCombo->addItem(QStringLiteral("仅放入"), static_cast<int>(QAbstractItemView::DropOnly));    m_dragDropModeCombo->addItem(QStringLiteral("拖放"), static_cast<int>(QAbstractItemView::DragDrop));    m_dragDropModeCombo->addItem(QStringLiteral("内部移动"), static_cast<int>(QAbstractItemView::InternalMove));    m_horizontalResizeCombo = new QComboBox(parent);    m_horizontalResizeCombo->addItem(QStringLiteral("交互调整"), static_cast<int>(QHeaderView::Interactive));    m_horizontalResizeCombo->addItem(QStringLiteral("拉伸填充"), static_cast<int>(QHeaderView::Stretch));    m_horizontalResizeCombo->addItem(QStringLiteral("按内容自适应"), static_cast<int>(QHeaderView::ResizeToContents));    m_horizontalResizeCombo->addItem(QStringLiteral("固定宽度"), static_cast<int>(QHeaderView::Fixed));    m_verticalResizeCombo = new QComboBox(parent);    m_verticalResizeCombo->addItem(QStringLiteral("交互调整"), static_cast<int>(QHeaderView::Interactive));    m_verticalResizeCombo->addItem(QStringLiteral("按内容自适应"), static_cast<int>(QHeaderView::ResizeToContents));    m_verticalResizeCombo->addItem(QStringLiteral("固定高度"), static_cast<int>(QHeaderView::Fixed));    m_scrollModeCombo = new QComboBox(parent);    m_scrollModeCombo->addItem(QStringLiteral("按项滚动"), static_cast<int>(QAbstractItemView::ScrollPerItem));    m_scrollModeCombo->addItem(QStringLiteral("按像素滚动"), static_cast<int>(QAbstractItemView::ScrollPerPixel));    m_rowHeightSpinBox = new QSpinBox(parent);    m_rowHeightSpinBox->setRange(2480);    m_rowHeightSpinBox->setValue(34);    m_columnWidthSpinBox = new QSpinBox(parent);    m_columnWidthSpinBox->setRange(70220);    m_columnWidthSpinBox->setValue(120);    m_frozenColumnSpinBox = new QSpinBox(parent);    m_frozenColumnSpinBox->setRange(09);    m_frozenColumnSpinBox->setValue(0);    // 这些下拉框直接映射 Qt 原生枚举值。    // applyViewSettings() 中会读取 currentData(),再设置回 QTableView。    formLayout->addRow(QStringLiteral("选择行为"), m_selectionBehaviorCombo);    formLayout->addRow(QStringLiteral("选择模式"), m_selectionModeCombo);    formLayout->addRow(QStringLiteral("编辑触发"), m_editTriggerCombo);    formLayout->addRow(QStringLiteral("拖放模式"), m_dragDropModeCombo);    formLayout->addRow(QStringLiteral("水平表头尺寸"), m_horizontalResizeCombo);    formLayout->addRow(QStringLiteral("垂直表头尺寸"), m_verticalResizeCombo);    formLayout->addRow(QStringLiteral("滚动模式"), m_scrollModeCombo);    formLayout->addRow(QStringLiteral("默认行高"), m_rowHeightSpinBox);    formLayout->addRow(QStringLiteral("默认列宽"), m_columnWidthSpinBox);    formLayout->addRow(QStringLiteral("冻结列数"), m_frozenColumnSpinBox);    controlLayout->addLayout(formLayout);    m_sortCheckBox = new QCheckBox(QStringLiteral("启用点击表头排序"), parent);    m_alternatingColorCheckBox = new QCheckBox(QStringLiteral("交替行颜色"), parent);    m_showGridCheckBox = new QCheckBox(QStringLiteral("显示网格线"), parent);    m_wordWrapCheckBox = new QCheckBox(QStringLiteral("单元格文本自动换行"), parent);    m_cornerButtonCheckBox = new QCheckBox(QStringLiteral("显示左上角按钮"), parent);    m_rowHeaderCheckBox = new QCheckBox(QStringLiteral("显示行表头序号"), parent);    m_columnHeaderCheckBox = new QCheckBox(QStringLiteral("显示列表头"), parent);    m_stretchLastSectionCheckBox = new QCheckBox(QStringLiteral("最后一列自动拉伸"), parent);    m_sortIndicatorCheckBox = new QCheckBox(QStringLiteral("显示排序指示器"), parent);    m_spanFirstRowCheckBox = new QCheckBox(QStringLiteral("第一行演示列合并"), parent);    m_hideLowScoreRowsCheckBox = new QCheckBox(QStringLiteral("隐藏分数低于 80 的行"), parent);    // 默认配置尽量展示 QTableView 常用能力:    // 排序、隔行色、网格线、列头、左上角按钮默认开启;垂直行号默认隐藏。    m_sortCheckBox->setChecked(true);    m_alternatingColorCheckBox->setChecked(true);    m_showGridCheckBox->setChecked(true);    m_wordWrapCheckBox->setChecked(false);    m_cornerButtonCheckBox->setChecked(true);    m_rowHeaderCheckBox->setChecked(false);    m_columnHeaderCheckBox->setChecked(true);    m_stretchLastSectionCheckBox->setChecked(true);    m_sortIndicatorCheckBox->setChecked(true);    m_spanFirstRowCheckBox->setChecked(false);    m_hideLowScoreRowsCheckBox->setChecked(false);    controlLayout->addWidget(m_sortCheckBox);    controlLayout->addWidget(m_alternatingColorCheckBox);    controlLayout->addWidget(m_showGridCheckBox);    controlLayout->addWidget(m_wordWrapCheckBox);    controlLayout->addWidget(m_cornerButtonCheckBox);    controlLayout->addWidget(m_rowHeaderCheckBox);    controlLayout->addWidget(m_columnHeaderCheckBox);    controlLayout->addWidget(m_stretchLastSectionCheckBox);    controlLayout->addWidget(m_sortIndicatorCheckBox);    controlLayout->addWidget(m_spanFirstRowCheckBox);    controlLayout->addWidget(m_hideLowScoreRowsCheckBox);    auto *buttonRow = new QHBoxLayout();    buttonRow->setSpacing(8);    m_addRowButton = new QPushButton(QStringLiteral("新增行"), parent);    m_removeRowButton = new QPushButton(QStringLiteral("删除选中行"), parent);    m_resetButton = new QPushButton(QStringLiteral("重置模型"), parent);    buttonRow->addWidget(m_addRowButton);    buttonRow->addWidget(m_removeRowButton);    buttonRow->addWidget(m_resetButton);    controlLayout->addLayout(buttonRow);    controlLayout->addStretch();    scrollArea->setWidget(controlContent);    layout->addWidget(scrollArea, 1);}voidMainWindow8::buildTablePanel(QWidget *parent){    // 右侧区域才是真正演示 menuplugintableviewwidget 的地方。    // 上方是过滤输入框,中间是插件控件,底部是选择状态和操作状态。    auto *layout = new QVBoxLayout(parent);    layout->setContentsMargins(16161616);    layout->setSpacing(12);    auto *headerRow = new QHBoxLayout();    headerRow->setSpacing(10);    auto *panelTitle = new QLabel(QStringLiteral("预览区域"), parent);    panelTitle->setObjectName(QStringLiteral("panelTitle"));    m_filterEdit = new QLineEdit(parent);    m_filterEdit->setPlaceholderText(QStringLiteral("按用例编号、功能项、模块、状态或备注过滤"));    headerRow->addWidget(panelTitle);    headerRow->addStretch();    headerRow->addWidget(m_filterEdit, 0);    m_tablePluginWidget = new MenuPluginTableViewWidget(parent);    m_tableAdapter = new MenuPluginVariantTableViewAdapter();    // 表头文本保存两份:    // 1. m_baseHeaderLabels 永远保存原始表头。    // 2. updateSortHeaderLabels() 会在当前排序列后追加箭头,再写入 model。    // 这样可以避免 Qt 默认排序箭头导致表头文字左右跳动。    m_baseHeaderLabels = {        QStringLiteral("用例编号"),        QStringLiteral("功能项"),        QStringLiteral("模块"),        QStringLiteral("状态"),        QStringLiteral("标签"),        QStringLiteral("分数"),        QStringLiteral("负责人"),        QStringLiteral("更新日期"),        QStringLiteral("备注")    };    m_tableAdapter->setHeaderLabels(m_baseHeaderLabels);    // 过滤文本由业务侧决定。    // 插件只负责拿关键词匹配这些字符串,不关心 DemoTableRowData 具体有哪些字段。    m_tableAdapter->setFilterTextResolver([](const QVariant &item, int) {        const DemoTableRowData rowData = item.value<DemoTableRowData>();        return QStringList{            rowData.id,            rowData.title,            rowData.category,            rowData.status,            tagTexts(rowData.tags).join(QStringLiteral(" ")),            QString::number(rowData.score),            rowData.owner,            rowData.updated,            rowData.note        };    });    // 自定义 buildRowItems 的入口。    // 这里把 QVariant 还原成 DemoTableRowData,再生成 QStandardItem 列表。    // 其他页面复用插件时,重点就是替换这个 lambda。    m_tableAdapter->setRowItemsBuilder([](const QVariant &item, int) {        return buildDemoRowItems(item.value<DemoTableRowData>());    });    // 单元格拖动的业务规则也通过 adapter 注入。    // 当前 demo 只允许同列交换;如果业务希望跨列移动,可以在这里换成自己的处理函数。    m_tableAdapter->setCellMoveHandler([](QList<QVariant> &rows, int fromRow, int fromColumn, int toRow, int toColumn) {        return swapDemoCellInSameColumn(rows, fromRow, fromColumn, toRow, toColumn);    });    // 插件接收 adapter 后,就可以根据 adapter 的 rowCount/headerLabels/createRowItems 重建 model。    m_tablePluginWidget->setAdapter(m_tableAdapter);    // 插件仍然暴露原生 QTableView 和 QStandardItemModel。    // 这样 demo 可以继续配置 Qt 自带能力,而插件不用把每个小配置都包装一遍。    m_tableView = m_tablePluginWidget->tableView();    m_model = m_tablePluginWidget->model();    m_tableView->setObjectName(QStringLiteral("demoTableView"));    m_tableView->setSortingEnabled(true);    m_tableView->setCornerButtonEnabled(true);    m_tableView->setWordWrap(false);    m_tableView->setAlternatingRowColors(true);    m_tableView->setShowGrid(true);    m_tableView->setItemDelegateForColumn(kTagsColumn, new TagListDelegate(m_tableView));    // 这里隐藏的是 QTableView 左侧默认行号 1、2、3...,不是业务里的“用例编号”列。    m_tableView->verticalHeader()->setVisible(false);    m_selectionLabel = new QLabel(parent);    m_selectionLabel->setObjectName(QStringLiteral("selectionLabel"));    m_statusLabel = new QLabel(parent);    m_statusLabel->setObjectName(QStringLiteral("statusLabel"));    m_statusLabel->setWordWrap(true);    layout->addLayout(headerRow);    layout->addWidget(m_tablePluginWidget, 1);    layout->addWidget(m_selectionLabel);    layout->addWidget(m_statusLabel);    // 右侧控制面板的所有选项最终都汇总到 applyViewSettings()。    // 这样每个控件只负责触发刷新,具体如何设置 QTableView 保持集中管理。    connect(m_selectionBehaviorCombo, &QComboBox::currentIndexChanged, this, [this](int) { applyViewSettings(); });    connect(m_selectionModeCombo, &QComboBox::currentIndexChanged, this, [this](int) { applyViewSettings(); });    connect(m_editTriggerCombo, &QComboBox::currentIndexChanged, this, [this](int) { applyViewSettings(); });    connect(m_dragDropModeCombo, &QComboBox::currentIndexChanged, this, [this](int) { applyViewSettings(); });    connect(m_horizontalResizeCombo, &QComboBox::currentIndexChanged, this, [this](int) { applyViewSettings(); });    connect(m_verticalResizeCombo, &QComboBox::currentIndexChanged, this, [this](int) { applyViewSettings(); });    connect(m_scrollModeCombo, &QComboBox::currentIndexChanged, this, [this](int) { applyViewSettings(); });    connect(m_rowHeightSpinBox, &QSpinBox::valueChanged, this, [this](int) { applyViewSettings(); });    connect(m_columnWidthSpinBox, &QSpinBox::valueChanged, this, [this](int) { applyViewSettings(); });    connect(m_frozenColumnSpinBox, &QSpinBox::valueChanged, this, [this](int) { applyViewSettings(); });    connect(m_sortCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_alternatingColorCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_showGridCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_wordWrapCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_cornerButtonCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_rowHeaderCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_columnHeaderCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_stretchLastSectionCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_sortIndicatorCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_spanFirstRowCheckBox, &QCheckBox::toggled, this, [this](bool) { applyViewSettings(); });    connect(m_hideLowScoreRowsCheckBox, &QCheckBox::toggled, this, [this](bool) {        applyFilter();        updateStatusText(QStringLiteral("隐藏条件已更新"));    });    connect(m_filterEdit, &QLineEdit::textChanged, this, [this](const QString &) {        applyFilter();        updateStatusText(QStringLiteral("过滤条件已更新"));    });    // Qt 默认排序指示器会占用表头空间,导致文字点击后轻微偏移。    // 这里监听排序变化,改用 updateSortHeaderLabels() 在表头文本后追加箭头。    connect(m_tableView->horizontalHeader(), &QHeaderView::sortIndicatorChanged, this,        [this](int logicalIndex, Qt::SortOrder order) {            updateSortHeaderLabels(logicalIndex, order);        });    // 新增行不直接操作 QStandardItemModel,而是先更新 adapter 中的 QVariant 数据源,    // 再调用 syncAdapterRows() 让插件统一 rebuild。    connect(m_addRowButton, &QPushButton::clicked, this, [this]() {        appendDemoRow(m_nextRowNumber++);        updateStatusText(QStringLiteral("已新增一行"));    });    connect(m_removeRowButton, &QPushButton::clicked, this, [this]() {        const QModelIndexList indexes = m_tableView->selectionModel()->selectedRows();        if (indexes.isEmpty()) {            updateStatusText(QStringLiteral("没有可删除的选中行"));            return;        }        // 删除时先从选中行拿到业务 id,再从 adapter 的 QVariant 数据源中删除。        // 这样可以保持“数据源 -> adapter -> model”的单向刷新路径。        QList<QString> idsToRemove;        idsToRemove.reserve(indexes.size());        for (const QModelIndex &index : indexes) {            const DemoTableRowData rowData = m_tablePluginWidget->rowData(index).value<DemoTableRowData>();            idsToRemove.append(rowData.id);        }        QList<QVariant> rows = m_tableAdapter->rows();        rows.erase(std::remove_if(rows.begin(), rows.end(), [&idsToRemove](const QVariant &item) {            return idsToRemove.contains(item.value<DemoTableRowData>().id);        }), rows.end());        syncAdapterRows(rows);        updateStatusText(QStringLiteral("已删除选中行"));    });    connect(m_resetButton, &QPushButton::clicked, this, [this]() {        populateModel();        applyViewSettings();        applyFilter();        updateStatusText(QStringLiteral("模型已重置"));    });    // 以下信号由 MenuPluginTableViewWidget 对原生 QTableView 事件进行转发。    // 外部页面无需直接关心 clicked/doubleClicked/selectionModel 的底层细节。    connect(m_tablePluginWidget, &MenuPluginTableViewWidget::cellClicked, this,        [this](const QModelIndex &index, const QVariant &) {            updateStatusText(QStringLiteral("单击单元格:第 %1 行,第 %2 列").arg(index.row() + 1).arg(index.column() + 1));        });    connect(m_tablePluginWidget, &MenuPluginTableViewWidget::cellDoubleClicked, this,        [this](const QModelIndex &index, const QVariant &) {            updateStatusText(QStringLiteral("双击单元格:%1").arg(index.data().toString()));        });    connect(m_tablePluginWidget, &MenuPluginTableViewWidget::selectionChanged, this, [this](int rowCount, int cellCount) {        m_selectionLabel->setText(QStringLiteral("选中行数:%1,选中单元格数:%2").arg(rowCount).arg(cellCount));    });    connect(m_tablePluginWidget, &MenuPluginTableViewWidget::rowsReordered, this, [this](const QList<QVariant> &) {        updateStatusText(QStringLiteral("表格行顺序已更新"));    });    connect(m_tablePluginWidget, &MenuPluginTableViewWidget::cellsMoved, this, [this](const QList<QVariant> &) {        // 单元格移动后,adapter 数据已经变化;重新应用过滤,保证隐藏行状态同步。        applyFilter();        updateStatusText(QStringLiteral("单元格内容已移动"));    });    // itemChanged 这里只用于 demo 状态提示。    // 如果真实业务要把编辑结果写回 QVariant 数据源,需要额外做 model -> adapter 的同步策略。    connect(m_model, &QStandardItemModel::itemChanged, this, [this](QStandardItem *item) {        const QString text = item->isCheckable()            ? QStringLiteral("%1 勾选状态 -> %2").arg(item->text(), item->checkState() == Qt::Checked ? QStringLiteral("已选中") : QStringLiteral("未选中"))            : QStringLiteral("已编辑:%1").arg(item->text());        updateStatusText(text);    });}voidMainWindow8::populateModel(){    // 初始化 demo 数据:构造默认行并同步给 adapter。    // m_nextRowNumber 用于后续“新增行”继续生成不重复的用例编号。    syncAdapterRows(buildDefaultRows());    m_nextRowNumber = 15;}voidMainWindow8::syncAdapterRows(const QList<QVariant> &rows){    // 所有数据变更都走这个函数:    // 1. 更新 adapter 内部 QVariant 列表。    // 2. 让插件根据 adapter 重新构建 QStandardItemModel。    // 3. 重新应用过滤条件。    m_tableAdapter->setRows(rows);    m_tablePluginWidget->rebuild();    applyFilter();}voidMainWindow8::appendDemoRow(int rowIndex){    // 新增行仍然先改 adapter 的原始数据,而不是直接 appendRow 到 model。    // 这样能保证拖拽、过滤、重建时数据源一致。    QList<QVariant> rows = m_tableAdapter->rows();    rows.append(QVariant::fromValue(buildRowData(rowIndex)));    syncAdapterRows(rows);}voidMainWindow8::applyViewSettings(){    // 这个函数把控制面板的配置统一写回 QTableView。    // 插件不重复封装所有 QTableView API,复杂页面仍可直接使用原生能力。    m_tableView->setSelectionBehavior(static_cast<QAbstractItemView::SelectionBehavior>(m_selectionBehaviorCombo->currentData().toInt()));    m_tableView->setSelectionMode(static_cast<QAbstractItemView::SelectionMode>(m_selectionModeCombo->currentData().toInt()));    m_tableView->setEditTriggers(static_cast<QAbstractItemView::EditTriggers>(m_editTriggerCombo->currentData().toInt()));    m_tableView->setDragDropMode(static_cast<QAbstractItemView::DragDropMode>(m_dragDropModeCombo->currentData().toInt()));    m_tableView->setVerticalScrollMode(static_cast<QAbstractItemView::ScrollMode>(m_scrollModeCombo->currentData().toInt()));    m_tableView->setHorizontalScrollMode(static_cast<QAbstractItemView::ScrollMode>(m_scrollModeCombo->currentData().toInt()));    m_tableView->setSortingEnabled(m_sortCheckBox->isChecked());    m_tableView->setAlternatingRowColors(m_alternatingColorCheckBox->isChecked());    m_tableView->setShowGrid(m_showGridCheckBox->isChecked());    m_tableView->setWordWrap(m_wordWrapCheckBox->isChecked());    m_tableView->setCornerButtonEnabled(m_cornerButtonCheckBox->isChecked());    m_tableView->verticalHeader()->setVisible(m_rowHeaderCheckBox->isChecked());    m_tableView->horizontalHeader()->setVisible(m_columnHeaderCheckBox->isChecked());    m_tableView->horizontalHeader()->setStretchLastSection(m_stretchLastSectionCheckBox->isChecked());    // 关闭 Qt 默认排序箭头,避免表头文字因箭头占位产生左右跳动。    // 排序方向由 updateSortHeaderLabels() 自己追加到表头文本里。    m_tableView->horizontalHeader()->setSortIndicatorShown(false);    // QTableView 的拖放能力由 DragDropMode 决定。    // 插件内部会根据当前选择行为区分“按行拖动”和“按单元格拖动”。    m_tableView->setDragEnabled(m_tableView->dragDropMode() != QAbstractItemView::NoDragDrop        && m_tableView->dragDropMode() != QAbstractItemView::DropOnly);    m_tableView->viewport()->setAcceptDrops(m_tableView->dragDropMode() != QAbstractItemView::NoDragDrop        && m_tableView->dragDropMode() != QAbstractItemView::DragOnly);    m_tableView->setDropIndicatorShown(m_tableView->dragDropMode() != QAbstractItemView::NoDragDrop);    m_tableView->setDefaultDropAction(Qt::MoveAction);    const auto horizontalMode = static_cast<QHeaderView::ResizeMode>(m_horizontalResizeCombo->currentData().toInt());    const auto verticalMode = static_cast<QHeaderView::ResizeMode>(m_verticalResizeCombo->currentData().toInt());    m_tableView->horizontalHeader()->setSectionResizeMode(horizontalMode);    m_tableView->verticalHeader()->setSectionResizeMode(verticalMode);    m_tableView->verticalHeader()->setDefaultSectionSize(m_rowHeightSpinBox->value());    if (horizontalMode == QHeaderView::Interactive || horizontalMode == QHeaderView::Fixed) {        for (int column = 0; column < m_model->columnCount(); ++column) {            m_tableView->setColumnWidth(column, m_columnWidthSpinBox->value() + column * 8);        }    }    // 冻结列能力由插件实现:内部叠加一个共享 model 的 QTableView,只显示前 N 列。    m_tablePluginWidget->setFrozenColumnCount(m_frozenColumnSpinBox->value());    m_tableView->clearSpans();    if (m_spanFirstRowCheckBox->isChecked() && m_model->rowCount() > 0) {        // span 只是演示 QTableView 原生能力,说明插件没有封死底层 tableView。        m_tableView->setSpan(0112);    }    updateSortHeaderLabels(        m_tableView->horizontalHeader()->sortIndicatorSection(),        m_tableView->horizontalHeader()->sortIndicatorOrder());    updateStatusText(QStringLiteral("表格设置已更新"));}voidMainWindow8::updateSortHeaderLabels(int sortColumn, Qt::SortOrder order){    // 每次都从原始表头开始生成,避免多次点击后箭头重复追加。    QStringList labels = m_baseHeaderLabels;    if (m_sortIndicatorCheckBox->isChecked()        && sortColumn >= 0        && sortColumn < labels.size()) {        labels[sortColumn] = QStringLiteral("%1 %2")            .arg(labels.at(sortColumn), order == Qt::AscendingOrder ? QStringLiteral("↑") : QStringLiteral("↓"));    }    m_model->setHorizontalHeaderLabels(labels);}voidMainWindow8::applyFilter(){    // 第一层过滤交给插件:插件根据 adapter->filterTexts(...) 判断关键词是否命中。    m_tablePluginWidget->setFilterKeyword(m_filterEdit->text().trimmed());    for (int row = 0; row < m_model->rowCount(); ++row) {        bool hidden = !m_tablePluginWidget->rowMatchesFilter(row);        if (!hidden && m_hideLowScoreRowsCheckBox->isChecked()) {            // 第二层过滤是 demo 自己追加的业务规则:隐藏分数低于 80 的行。            // 分数列写入了 Qt::EditRole,所以这里按数值读取,而不是按显示字符串读取。            const int score = m_model->index(row, 5).data(Qt::EditRole).toInt();            hidden = score < 80;        }        m_tableView->setRowHidden(row, hidden);    }}voidMainWindow8::updateStatusText(const QString &prefix){    // 统计当前可见行数,用于底部状态栏说明。    // 注意这里读取的是 view 的隐藏状态,所以能同时反映关键词过滤和低分过滤。    const int total = m_model->rowCount();    int visible = 0;    for (int row = 0; row < total; ++row) {        if (!m_tableView->isRowHidden(row)) {            ++visible;        }    }    const QString selectionText = m_selectionBehaviorCombo->currentText();    const QString editText = m_editTriggerCombo->currentText();    m_selectionLabel->setText(QStringLiteral("选中行数:%1,选中单元格数:%2")        .arg(m_tableView->selectionModel()->selectedRows().size())        .arg(m_tableView->selectionModel()->selectedIndexes().size()));    m_statusLabel->setText(QStringLiteral("%1 | 选择=%2 | 编辑=%3 | 可见行 %4/%5")        .arg(prefix.isEmpty() ? QStringLiteral("已就绪") : prefix, selectionText, editText)        .arg(visible)        .arg(total));}QString MainWindow8::buildStyleSheet(){    // 样式只服务 demo 展示。    // 插件本身不强制业务页面使用这套样式,真实项目可以在外部统一接入主题。    return QStringLiteral(R"(        QMainWindow {            background: #eef3f8;        }        QLabel#titleLabel {            color: #0f172a;            font-size: 30px;            font-weight: 800;        }        QLabel#descLabel,        QLabel#statusLabel,        QLabel#selectionLabel {            color: #475569;            font-size: 14px;        }        QLabel#panelTitle {            color: #0f172a;            font-size: 18px;            font-weight: 700;        }        QFrame#controlCard,        QFrame#tableCard {            background: #ffffff;            border: 1px solid #dbe3ee;            border-radius: 18px;        }        QComboBox,        QLineEdit,        QSpinBox {            min-height: 32px;            padding: 4px 8px;            border: 1px solid #cbd5e1;            border-radius: 8px;            background: #ffffff;            color: #0f172a;        }        QCheckBox {            color: #334155;            font-size: 14px;        }        QScrollArea#controlScrollArea {            background: transparent;            border: none;        }        QScrollArea#controlScrollArea > QWidget > QWidget {            background: transparent;        }        QPushButton {            min-height: 34px;            padding: 6px 12px;            border: none;            border-radius: 8px;            background: #1d4ed8;            color: #ffffff;            font-weight: 700;        }        QPushButton:hover {            background: #2563eb;        }        QTableView#demoTableView {            border: 1px solid #dbe3ee;            border-radius: 14px;            background: #ffffff;            gridline-color: #e2e8f0;            selection-background-color: #dbeafe;            selection-color: #1d4ed8;            alternate-background-color: #f8fafc;            color: #0f172a;        }        QHeaderView::section {            background: #f8fafc;            color: #0f172a;            padding: 8px;            border: none;            border-right: 1px solid #e2e8f0;            border-bottom: 1px solid #e2e8f0;            font-weight: 700;        }        QStatusBar {            background: #ffffff;            color: #64748b;            border-top: 1px solid #dbe3ee;        }    )");}

menuplugintableviewwidget.h

#pragma once#include<functional>#include<QList>#include<QStringList>#include<QVariant>#include<QWidget>class QModelIndex;class QStandardItem;class QStandardItemModel;class QTableView;// QTableView 插件的数据适配器基类。//// 插件本身只认识 QVariant,不认识业务结构体。业务侧需要继承这个接口,// 或者直接使用下面的 MenuPluginVariantTableViewAdapter,把自己的数据转换成:// 1. 行数。// 2. 表头。// 3. 每一行对应的 QStandardItem 列表。// 4. 可参与过滤的文本。// 5. 拖拽后的数据移动规则。class MenuPluginTableViewAdapter{public:    virtual ~MenuPluginTableViewAdapter() = default;    // 返回当前数据源中的行数。    virtualintrowCount()const0;    // 返回某一行的原始业务数据。    // 这里使用 QVariant 是为了让插件和业务类型解耦。    virtual QVariant rowAt(int row)const0;    // 返回列数。通常等于表头数量,也可以由列解析器数量决定。    virtualintcolumnCount()const0;    // 返回水平表头文本。    virtual QStringList headerLabels()const0;    // 把一条 QVariant 业务数据转换成一整行 QStandardItem。    // 这是最核心的扩展点:业务可以在这里设置文本、对齐、可编辑、勾选框、tooltip、role 等。    virtual QList<QStandardItem *> createRowItems(const QVariant &rowData, int rowIndex)const0;    // 返回这一行参与关键字过滤的文本集合。    // 默认实现只使用 rowData.toString(),复杂业务建议自定义。    virtual QStringList filterTexts(const QVariant &rowData, int rowIndex)const;    // 行拖拽时调用。返回 true 表示 adapter 已完成数据源重排。    virtualboolmoveRow(int from, int to);    // 单元格拖拽时调用。是否允许移动、如何移动,由业务 adapter 决定。    virtualboolmoveCell(int fromRow, int fromColumn, int toRow, int toColumn);};// 一个通用的 QVariant adapter 实现。//// 适合大多数页面直接使用:// - setRows(...) 设置 QList<QVariant> 数据源。// - setRowItemsBuilder(...) 自定义每行 QStandardItem 的构建方式。// - setFilterTextResolver(...) 自定义过滤字段。// - setCellMoveHandler(...) 自定义单元格拖拽规则。class MenuPluginVariantTableViewAdapter : public MenuPluginTableViewAdapter{public:    // 自定义行构建函数。业务最常用的扩展点。    using RowItemsBuilder = std::function<QList<QStandardItem *>(const QVariant &rowData, int rowIndex)>;    // 如果没有提供 RowItemsBuilder,可以用列解析器把一行 QVariant 拆成多列值。    using ColumnValueResolver = std::function<QVariant(const QVariant &rowData, int rowIndex)>;    // 自定义过滤文本。比如只让编号、名称、状态参与搜索。    using FilterTextResolver = std::function<QStringList(const QVariant &rowData, int rowIndex)>;    // 在 RowItemsBuilder 或默认 item 创建之后,再统一调整整行 item。    using RowItemsConfigurator = std::function<void(QList<QStandardItem *> &items, const QVariant &rowData, int rowIndex)>;    // 自定义单元格拖拽后的数据更新规则。    using CellMoveHandler = std::function<bool(QList<QVariant> &rows, int fromRow, int fromColumn, int toRow, int toColumn)>;    // 设置和读取原始数据源。数据源中的每一项代表一行。    voidsetRows(const QList<QVariant> &rows);    QList<QVariant> rows()const;    voidclear();    // 配置 adapter 的各类扩展点。    voidsetHeaderLabels(const QStringList &headers);    voidsetColumnValueResolvers(const QList<ColumnValueResolver> &resolvers);    voidsetRowItemsBuilder(const RowItemsBuilder &builder);    voidsetFilterTextResolver(const FilterTextResolver &resolver);    voidsetRowItemsConfigurator(const RowItemsConfigurator &configurator);    voidsetCellMoveHandler(const CellMoveHandler &handler);    introwCount()constoverride;    QVariant rowAt(int row)constoverride;    intcolumnCount()constoverride;    QStringList headerLabels()constoverride;    QList<QStandardItem *> createRowItems(const QVariant &rowData, int rowIndex)constoverride;    QStringList filterTexts(const QVariant &rowData, int rowIndex)constoverride;    boolmoveRow(int from, int to)override;    boolmoveCell(int fromRow, int fromColumn, int toRow, int toColumn)override;private:    // 原始业务行数据。插件重建 model、拖拽重排都以它为准。    QList<QVariant> m_rows;    // 表头文本。    QStringList m_headers;    // 无 RowItemsBuilder 时的默认列解析方案。    QList<ColumnValueResolver> m_columnValueResolvers;    // 自定义行 item 构建器。    RowItemsBuilder m_rowItemsBuilder;    // 自定义过滤文本解析器。    FilterTextResolver m_filterTextResolver;    // 行 item 创建后的二次配置器。    RowItemsConfigurator m_rowItemsConfigurator;    // 单元格移动处理器。    CellMoveHandler m_cellMoveHandler;};// 可复用 QTableView 插件控件。//// 它内部持有:// - 一个主 QTableView。// - 一个 QStandardItemModel。// - 一个用于冻结列的叠加 QTableView。//// 业务侧通过 setAdapter(...) 提供数据和行构建方式;// 如需继续使用 Qt 原生能力,可以通过 tableView() / model() 拿到底层对象。class MenuPluginTableViewWidget : public QWidget{    Q_OBJECTpublic:    explicitMenuPluginTableViewWidget(QWidget *parent = nullptr);    // 设置数据适配器。插件不接管 adapter 生命周期,通常由外部决定释放时机。    voidsetAdapter(MenuPluginTableViewAdapter *adapter);    MenuPluginTableViewAdapter *adapter()const;    // 暴露底层 QTableView / QStandardItemModel,方便外部配置排序、选择模式、列宽、span 等 Qt 原生能力。    QTableView *tableView()const;    QStandardItemModel *model()const;    // 冻结左侧前 N 列。0 表示关闭冻结列。    voidsetFrozenColumnCount(int count);    intfrozenColumnCount()const;    // 根据 adapter 当前数据重新构建 model。    voidrebuild();    // 设置关键字过滤。过滤字段由 adapter->filterTexts(...) 提供。    voidsetFilterKeyword(const QString &keyword);    QString filterKeyword()const;    boolrowMatchesFilter(int row)const;    intvisibleRowCount()const;    // 通过 model index 取回这一行的原始 QVariant 数据。    QVariant rowData(const QModelIndex &index)const;    // 返回 adapter 中的所有原始行数据。    QList<QVariant> rows()const;signals:    // 对 QTableView 常用交互做一层转发,同时附带原始 QVariant 行数据。    voidcellClicked(const QModelIndex &index, const QVariant &rowData);    voidcellDoubleClicked(const QModelIndex &index, const QVariant &rowData);    voidselectionChanged(int selectedRows, int selectedCells);    // 行拖拽或单元格拖拽完成后发出,外部可据此保存新顺序。    voidrowsReordered(const QList<QVariant> &rows);    voidcellsMoved(const QList<QVariant> &rows);private:    // 监听主表格和表头尺寸变化,用于更新冻结列覆盖区域。    booleventFilter(QObject *watched, QEvent *event)override;    voidapplyFilter();    QVariant rowDataByRow(int row)const;    // 内部拖拽回调:主表格捕获拖拽后,交给 adapter 修改数据源,再 rebuild。    voidhandleInternalMoveRequest(int from, int to);    voidhandleInternalCellMoveRequest(int fromRow, int fromColumn, int toRow, int toColumn);    // 冻结列相关:控制哪些列显示在冻结表格中,并同步几何、行高和隐藏状态。    voidupdateFrozenColumns();    voidupdateFrozenGeometry();    voidsyncFrozenRowHeights();    // 主表格,负责正常显示、滚动、选择、排序和编辑。    QTableView *m_tableView;    // 叠加在主表格左侧的冻结表格,只显示前 N 列。    QTableView *m_frozenTableView;    // 插件内部使用的标准 item model。    QStandardItemModel *m_model;    // 外部注入的数据适配器。    MenuPluginTableViewAdapter *m_adapter;    // 当前关键字过滤条件。    QString m_filterKeyword;    // 左侧冻结列数量。    int m_frozenColumnCount;};

menuplugintableviewwidget.cpp

#include"menuplugintableviewwidget.h"#include<functional>#include<QAbstractItemView>#include<QDragEnterEvent>#include<QDragMoveEvent>#include<QDropEvent>#include<QEvent>#include<QFrame>#include<QHeaderView>#include<QItemSelectionModel>#include<QModelIndex>#include<QScrollBar>#include<QStandardItem>#include<QStandardItemModel>#include<QTableView>#include<QTimer>#include<QVBoxLayout>namespace {// 每个 QStandardItem 上都保存两份额外数据:// kRowDataRole 保存这一行的原始 QVariant,方便点击、双击、选择时把业务数据带出去。// kFilterTextRole 保存这一行拼好的过滤文本,避免每次过滤都重新问 adapter。constexpr int kRowDataRole = Qt::UserRole;constexpr int kFilterTextRole = Qt::UserRole + 1;// 默认 item 构建函数。// 当业务没有提供 RowItemsBuilder,只提供了普通列值时,adapter 会用它创建基础 QStandardItem。QStandardItem *createDefaultItem(const QVariant &value){    auto *item = new QStandardItem(value.toString());    item->setData(value, Qt::EditRole);    return item;}// 内部 QTableView 子类。//// Qt 自带 QTableView 支持拖拽,但默认行为更偏 model 层 MIME 数据处理。// 这里重写 drag/drop,是为了把“拖动了哪一行/哪一个单元格”转换成简单回调,// 再交给 MenuPluginTableViewWidget 和 adapter 处理真实数据源。class MenuPluginTableView : public QTableView{public:    explicitMenuPluginTableView(QWidget *parent = nullptr)        : QTableView(parent)    {        setAcceptDrops(true);        viewport()->setAcceptDrops(true);        setDropIndicatorShown(true);        setDragDropOverwriteMode(false);        setDefaultDropAction(Qt::MoveAction);    }    voidsetInternalMoveHandler(const std::function<void(intint)> &handler)    {        m_internalMoveHandler = handler;    }    voidsetInternalCellMoveHandler(const std::function<void(intintintint)> &handler)    {        m_internalCellMoveHandler = handler;    }protected:    voiddragEnterEvent(QDragEnterEvent *event)override    {        // 只特殊处理本表格内部拖拽;外部拖入仍交给 QTableView 默认逻辑。        if (event->source() == this) {            event->acceptProposedAction();            return;        }        QTableView::dragEnterEvent(event);    }    voiddragMoveEvent(QDragMoveEvent *event)override    {        // 内部拖拽过程中持续接受 move action,保证 dropEvent 能被触发。        if (event->source() == this) {            event->acceptProposedAction();            return;        }        QTableView::dragMoveEvent(event);    }    voiddropEvent(QDropEvent *event)override    {        // 只接管内部拖拽。外部数据拖入时仍使用 QTableView 默认实现。        if (event->source() != this || model() == nullptr || selectionModel() == nullptr) {            QTableView::dropEvent(event);            return;        }        const QModelIndexList selectedIndexes = selectionModel()->selectedIndexes();        if (selectedIndexes.isEmpty()) {            event->ignore();            return;        }#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)        const QPoint dropPos = event->position().toPoint();#else        const QPoint dropPos = event->pos();#endif        const QModelIndex targetIndex = indexAt(dropPos);        if (!targetIndex.isValid()) {            event->ignore();            return;        }        // SelectItems 表示当前是按单元格选择。        // 此时拖拽一个 cell,交给 adapter->moveCell(...) 决定是否移动以及如何修改业务数据。        if (selectionBehavior() == QAbstractItemView::SelectItems) {            const QModelIndex sourceIndex = selectedIndexes.first();            event->acceptProposedAction();            if (m_internalCellMoveHandler) {                QTimer::singleShot(0this, [handler = m_internalCellMoveHandler,                                             fromRow = sourceIndex.row(),                                             fromColumn = sourceIndex.column(),                                             toRow = targetIndex.row(),                                             toColumn = targetIndex.column()]() {                    handler(fromRow, fromColumn, toRow, toColumn);                });            }            return;        }        // 非 SelectItems 时按“行拖拽”处理。        // toRow 根据鼠标落点在目标行上半区/下半区决定插入到目标行前还是后。        const QModelIndexList selectedRows = selectionModel()->selectedRows();        const int fromRow = selectedRows.isEmpty() ? selectedIndexes.first().row() : selectedRows.first().row();        int toRow = targetIndex.isValid() ? targetIndex.row() : model()->rowCount();        const QRect rect = visualRect(targetIndex);        if (dropPos.y() > rect.center().y()) {            ++toRow;        }        event->acceptProposedAction();        if (m_internalMoveHandler) {            // 延迟到当前 drop event 结束后再重建 model,避免事件处理中直接改 model 带来视图状态异常。            QTimer::singleShot(0this, [handler = m_internalMoveHandler, fromRow, toRow]() {                handler(fromRow, toRow);            });        }    }private:    std::function<void(intint)> m_internalMoveHandler;    std::function<void(intintintint)> m_internalCellMoveHandler;};}QStringList MenuPluginTableViewAdapter::filterTexts(const QVariant &rowData, int rowIndex)const{    Q_UNUSED(rowIndex);    // 基类提供一个很保守的默认过滤方案:直接使用 rowData.toString()。    // 复杂业务通常会在具体 adapter 中返回多个字段,如编号、名称、状态、备注。    return {rowData.toString()};}boolMenuPluginTableViewAdapter::moveRow(int from, int to){    Q_UNUSED(from);    Q_UNUSED(to);    // 基类默认不支持移动。具体 adapter 返回 true 时,插件才会 rebuild 并发出 rowsReordered。    return false;}boolMenuPluginTableViewAdapter::moveCell(int fromRow, int fromColumn, int toRow, int toColumn){    Q_UNUSED(fromRow);    Q_UNUSED(fromColumn);    Q_UNUSED(toRow);    Q_UNUSED(toColumn);    // 基类默认不支持单元格移动,避免插件替业务做错误的数据交换。    return false;}voidMenuPluginVariantTableViewAdapter::setRows(const QList<QVariant> &rows){    m_rows = rows;}QList<QVariant> MenuPluginVariantTableViewAdapter::rows()const{    return m_rows;}voidMenuPluginVariantTableViewAdapter::clear(){    m_rows.clear();}voidMenuPluginVariantTableViewAdapter::setHeaderLabels(const QStringList &headers){    m_headers = headers;}voidMenuPluginVariantTableViewAdapter::setColumnValueResolvers(const QList<ColumnValueResolver> &resolvers){    m_columnValueResolvers = resolvers;}voidMenuPluginVariantTableViewAdapter::setRowItemsBuilder(const RowItemsBuilder &builder){    m_rowItemsBuilder = builder;}voidMenuPluginVariantTableViewAdapter::setFilterTextResolver(const FilterTextResolver &resolver){    m_filterTextResolver = resolver;}voidMenuPluginVariantTableViewAdapter::setRowItemsConfigurator(const RowItemsConfigurator &configurator){    m_rowItemsConfigurator = configurator;}voidMenuPluginVariantTableViewAdapter::setCellMoveHandler(const CellMoveHandler &handler){    m_cellMoveHandler = handler;}intMenuPluginVariantTableViewAdapter::rowCount()const{    return m_rows.size();}QVariant MenuPluginVariantTableViewAdapter::rowAt(int row)const{    return row >= 0 && row < m_rows.size() ? m_rows.at(row) : QVariant();}intMenuPluginVariantTableViewAdapter::columnCount()const{    // 列数可以来自表头,也可以来自默认列解析器。    // 如果业务提供 RowItemsBuilder,实际 item 数量可能在 rebuild() 时进一步扩展 model 列数。    return qMax(m_headers.size(), m_columnValueResolvers.size());}QStringList MenuPluginVariantTableViewAdapter::headerLabels()const{    return m_headers;}QList<QStandardItem *> MenuPluginVariantTableViewAdapter::createRowItems(const QVariant &rowData, int rowIndex)const{    QList<QStandardItem *> items;    // 优先使用业务自定义的 RowItemsBuilder。    // 这是最灵活的方式,可以设置每个 item 的编辑状态、对齐、勾选框、tooltip、role 等。    if (m_rowItemsBuilder) {        items = m_rowItemsBuilder(rowData, rowIndex);    } else if (!m_columnValueResolvers.isEmpty()) {        // 如果没有 RowItemsBuilder,就使用列解析器把一条 QVariant 拆成多列普通值。        items.reserve(m_columnValueResolvers.size());        for (const ColumnValueResolver &resolver : m_columnValueResolvers) {            items.append(createDefaultItem(resolver ? resolver(rowData, rowIndex) : QVariant()));        }    } else {        // 最后兜底:整条 QVariant 作为一个单元格显示。        items.append(createDefaultItem(rowData));    }    if (m_rowItemsConfigurator) {        // 二次配置器适合做整行统一处理,比如统一设置不可编辑、字体、背景、附加 role。        m_rowItemsConfigurator(items, rowData, rowIndex);    }    return items;}QStringList MenuPluginVariantTableViewAdapter::filterTexts(const QVariant &rowData, int rowIndex)const{    if (m_filterTextResolver) {        // 过滤字段由业务决定,插件只负责关键字匹配。        return m_filterTextResolver(rowData, rowIndex);    }    return MenuPluginTableViewAdapter::filterTexts(rowData, rowIndex);}boolMenuPluginVariantTableViewAdapter::moveRow(int from, int to){    if (from < 0 || from >= m_rows.size()) {        return false;    }    int normalizedTo = qBound(0, to, m_rows.size());    if (normalizedTo > from) {        // QList::move 是“移动到目标下标”,而 drop 位置是“插入到目标行前/后”。        // 从上往下移动时,源行移除后目标下标会前移一位,所以这里要减 1。        --normalizedTo;    }    if (normalizedTo == from) {        return false;    }    m_rows.move(from, normalizedTo);    return true;}boolMenuPluginVariantTableViewAdapter::moveCell(int fromRow, int fromColumn, int toRow, int toColumn){    if (!m_cellMoveHandler) {        // 未提供业务处理器时,不允许单元格移动。        return false;    }    if (fromRow < 0 || fromRow >= m_rows.size()        || toRow < 0 || toRow >= m_rows.size()        || fromColumn < 0 || fromColumn >= columnCount()        || toColumn < 0 || toColumn >= columnCount()        || (fromRow == toRow && fromColumn == toColumn)) {        return false;    }    // 真正的数据交换/移动由业务回调执行。    // 这样插件不需要知道 QVariant 里放的是哪个结构体,也不会破坏业务字段类型。    return m_cellMoveHandler(m_rows, fromRow, fromColumn, toRow, toColumn);}MenuPluginTableViewWidget::MenuPluginTableViewWidget(QWidget *parent)    : QWidget(parent)    , m_tableView(new MenuPluginTableView(this))    , m_frozenTableView(new QTableView(m_tableView))    , m_model(new QStandardItemModel(this))    , m_adapter(nullptr)    , m_frozenColumnCount(0){    // 插件自身只放一个主 QTableView。    // 冻结列用的 m_frozenTableView 不是 layout 子控件,而是叠加在主表格内部。    auto *layout = new QVBoxLayout(this);    layout->setContentsMargins(0000);    layout->addWidget(m_tableView);    m_tableView->setModel(m_model);    // 冻结表格和主表格共享同一个 model。    // 这样数据只有一份,冻结列只是“另一个视图”显示前 N 列。    m_frozenTableView->setModel(m_model);    m_frozenTableView->setFocusPolicy(Qt::NoFocus);    // 共享 selection model,保证主表格和冻结表格选中状态一致。    m_frozenTableView->setSelectionModel(m_tableView->selectionModel());    m_frozenTableView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);    m_frozenTableView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);    m_frozenTableView->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);    m_frozenTableView->setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);    m_frozenTableView->setFrameShape(QFrame::NoFrame);    m_frozenTableView->setSortingEnabled(false);    m_frozenTableView->setWordWrap(m_tableView->wordWrap());    m_frozenTableView->setAlternatingRowColors(m_tableView->alternatingRowColors());    m_frozenTableView->setShowGrid(m_tableView->showGrid());    // 冻结表格只负责显示,不抢鼠标事件。    // 用户点击冻结列区域时,事件仍由主表格处理,避免两个 view 的交互状态打架。    m_frozenTableView->setAttribute(Qt::WA_TransparentForMouseEvents, true);    m_frozenTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Fixed);    m_frozenTableView->verticalHeader()->setVisible(m_tableView->verticalHeader()->isVisible());    m_frozenTableView->hide();    // 确保主表格 viewport 在冻结表格下面,冻结表格 raise() 后可以覆盖左侧列区域。    m_tableView->viewport()->stackUnder(m_frozenTableView);    // 表格或表头尺寸变化时,需要重新计算冻结表格的位置和宽度。    m_tableView->installEventFilter(this);    m_tableView->horizontalHeader()->installEventFilter(this);    m_tableView->verticalHeader()->installEventFilter(this);    // 内部表格只负责识别拖拽动作;真正的数据移动在本类和 adapter 中完成。    static_cast<MenuPluginTableView *>(m_tableView)->setInternalMoveHandler([this](int from, int to) {        handleInternalMoveRequest(from, to);    });    static_cast<MenuPluginTableView *>(m_tableView)->setInternalCellMoveHandler(        [this](int fromRow, int fromColumn, int toRow, int toColumn) {            handleInternalCellMoveRequest(fromRow, fromColumn, toRow, toColumn);        });    connect(m_tableView, &QTableView::clicked, this, [this](const QModelIndex &index) {        emit cellClicked(index, rowData(index));    });    connect(m_tableView, &QTableView::doubleClicked, this, [this](const QModelIndex &index) {        emit cellDoubleClicked(index, rowData(index));    });    // 对外只暴露简化后的选择统计,调用方不用直接操作 selectionModel。    connect(m_tableView->selectionModel(), &QItemSelectionModel::selectionChanged, this, [this]() {        emit selectionChanged(            m_tableView->selectionModel()->selectedRows().size(),            m_tableView->selectionModel()->selectedIndexes().size());    });    // 冻结表格和主表格必须垂直同步滚动,否则左右两块行内容会错位。    connect(m_tableView->verticalScrollBar(), &QScrollBar::valueChanged,        m_frozenTableView->verticalScrollBar(), &QScrollBar::setValue);    connect(m_frozenTableView->verticalScrollBar(), &QScrollBar::valueChanged,        m_tableView->verticalScrollBar(), &QScrollBar::setValue);    connect(m_tableView->horizontalHeader(), &QHeaderView::sectionResized, this,        [this](intintint) {            // 列宽变化会影响冻结区域宽度。            updateFrozenColumns();        });    connect(m_tableView->verticalHeader(), &QHeaderView::sectionResized, this,        [this](int logicalIndex, intint newSize) {            // 行高变化时同步给冻结表格,避免同一行左右高度不同。            m_frozenTableView->setRowHeight(logicalIndex, newSize);        });}voidMenuPluginTableViewWidget::setAdapter(MenuPluginTableViewAdapter *adapter){    // 这里只保存裸指针,不接管生命周期。    // 当前项目中 adapter 通常由页面创建并随页面一起销毁。    m_adapter = adapter;    rebuild();}MenuPluginTableViewAdapter *MenuPluginTableViewWidget::adapter()const{    return m_adapter;}QTableView *MenuPluginTableViewWidget::tableView()const{    return m_tableView;}QStandardItemModel *MenuPluginTableViewWidget::model()const{    return m_model;}voidMenuPluginTableViewWidget::setFrozenColumnCount(int count){    // 负数没有意义,统一归零。    m_frozenColumnCount = qMax(0, count);    updateFrozenColumns();}intMenuPluginTableViewWidget::frozenColumnCount()const{    return m_frozenColumnCount;}voidMenuPluginTableViewWidget::rebuild(){    // rebuild 是插件的核心刷新入口。    // 它不直接关心业务类型,只通过 adapter 拉取 QVariant,再转成 QStandardItem。    m_model->clear();    if (m_adapter == nullptr) {        return;    }    QStringList headers = m_adapter->headerLabels();    int modelColumnCount = qMax(0, m_adapter->columnCount());    m_model->setColumnCount(modelColumnCount);    for (int row = 0; row < m_adapter->rowCount(); ++row) {        const QVariant payload = m_adapter->rowAt(row);        QList<QStandardItem *> items = m_adapter->createRowItems(payload, row);        const QString filterText = m_adapter->filterTexts(payload, row).join(QStringLiteral(" "));        // 如果业务返回的 item 数量超过当前列数,model 自动扩列。        if (items.size() > m_model->columnCount()) {            m_model->setColumnCount(items.size());        }        // 如果业务返回的 item 数量不足,补空 item,保证每一行列数一致。        while (items.size() < m_model->columnCount()) {            items.append(new QStandardItem);        }        for (int column = 0; column < items.size(); ++column) {            if (items.at(column) == nullptr) {                items[column] = new QStandardItem;            }            QStandardItem *item = items.at(column);            item->setDragEnabled(true);            item->setDropEnabled(true);            // 每个单元格都挂同一份行级业务数据。            // 这样任意列被点击时,都能通过 rowData(index) 取回完整 QVariant。            item->setData(payload, kRowDataRole);            item->setData(filterText, kFilterTextRole);        }        m_model->appendRow(items);    }    if (headers.size() < m_model->columnCount()) {        // 表头数量不足时补空,避免 setHorizontalHeaderLabels 少列导致显示不完整。        headers.resize(m_model->columnCount());    }    m_model->setHorizontalHeaderLabels(headers);    // model 重建后,过滤和冻结列状态都需要重新同步。    applyFilter();    syncFrozenRowHeights();    updateFrozenColumns();}voidMenuPluginTableViewWidget::setFilterKeyword(const QString &keyword){    // 保存 trimmed 后的关键字,避免用户输入前后空格影响匹配结果。    m_filterKeyword = keyword.trimmed();    applyFilter();}QString MenuPluginTableViewWidget::filterKeyword()const{    return m_filterKeyword;}boolMenuPluginTableViewWidget::rowMatchesFilter(int row)const{    if (row < 0 || row >= m_model->rowCount()) {        return false;    }    if (m_filterKeyword.isEmpty()) {        // 空关键字表示不过滤。        return true;    }    // kFilterTextRole 存在第 0 列即可,因为同一行所有 item 都保存了相同过滤文本。    const QModelIndex index = m_model->index(row, 0);    return index.data(kFilterTextRole).toString().contains(m_filterKeyword, Qt::CaseInsensitive);}intMenuPluginTableViewWidget::visibleRowCount()const{    int visibleCount = 0;    for (int row = 0; row < m_model->rowCount(); ++row) {        if (!m_tableView->isRowHidden(row)) {            ++visibleCount;        }    }    return visibleCount;}QVariant MenuPluginTableViewWidget::rowData(const QModelIndex &index)const{    // 对外按 QModelIndex 查询业务数据,但实际只需要行号。    return rowDataByRow(index.row());}QList<QVariant> MenuPluginTableViewWidget::rows()const{    QList<QVariant> result;    if (m_adapter == nullptr) {        return result;    }    for (int row = 0; row < m_adapter->rowCount(); ++row) {        result.append(m_adapter->rowAt(row));    }    return result;}voidMenuPluginTableViewWidget::applyFilter(){    // 插件同时隐藏主表格和冻结表格的行,保证冻结列与普通列可见状态一致。    for (int row = 0; row < m_model->rowCount(); ++row) {        const bool hidden = !rowMatchesFilter(row);        m_tableView->setRowHidden(row, hidden);        m_frozenTableView->setRowHidden(row, hidden);    }}QVariant MenuPluginTableViewWidget::rowDataByRow(int row)const{    if (row < 0 || row >= m_model->rowCount()) {        return QVariant();    }    // 原始 QVariant 保存在第 0 列 item 上。    return m_model->index(row, 0).data(kRowDataRole);}voidMenuPluginTableViewWidget::handleInternalMoveRequest(int from, int to){    // 行拖拽只修改 adapter 的原始数据源。    // 如果 adapter 返回 false,说明业务不允许移动或移动失败,视图不刷新。    if (m_adapter == nullptr || !m_adapter->moveRow(from, to)) {        return;    }    // adapter 数据源已变化,重建 model 让显示和数据源一致。    rebuild();    // 恢复拖拽后的当前行选择,给用户一个明确反馈。    int currentRow = qBound(0, to, m_model->rowCount());    if (currentRow > from) {        --currentRow;    }    if (currentRow >= 0 && currentRow < m_model->rowCount()) {        const QModelIndex index = m_model->index(currentRow, 0);        m_tableView->setCurrentIndex(index);        m_tableView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);    }    m_tableView->doItemsLayout();    m_tableView->viewport()->update();    // 把新的 QVariant 行顺序通知外部,外部可持久化。    emit rowsReordered(rows());}voidMenuPluginTableViewWidget::handleInternalCellMoveRequest(int fromRow, int fromColumn, int toRow, int toColumn){    // 单元格移动也必须先经过 adapter。    // 插件不知道不同列的数据语义,所以不直接交换 model item。    if (m_adapter == nullptr || !m_adapter->moveCell(fromRow, fromColumn, toRow, toColumn)) {        return;    }    // adapter 已更新 QVariant 数据源,重建 model 生成新的单元格内容。    rebuild();    // 恢复目标单元格选中状态。    const QModelIndex index = m_model->index(toRow, toColumn);    if (index.isValid()) {        m_tableView->setCurrentIndex(index);        m_tableView->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);    }    m_tableView->doItemsLayout();    m_tableView->viewport()->update();    // 通知外部单元格移动完成。    emit cellsMoved(rows());}boolMenuPluginTableViewWidget::eventFilter(QObject *watched, QEvent *event){    // 冻结列覆盖区域依赖主表格尺寸、表头显示状态和表头高度。    // 这些变化发生时需要重新计算冻结表格几何。    if ((watched == m_tableView || watched == m_tableView->horizontalHeader() || watched == m_tableView->verticalHeader())        && (event->type() == QEvent::Resize || event->type() == QEvent::Show || event->type() == QEvent::Hide)) {        updateFrozenColumns();    }    return QWidget::eventFilter(watched, event);}voidMenuPluginTableViewWidget::updateFrozenColumns(){    // frozenCount 做边界保护:不能超过当前 model 的实际列数。    const int frozenCount = qMin(m_frozenColumnCount, m_model->columnCount());    const bool enabled = frozenCount > 0;    // 冻结表格尽量镜像主表格的可视配置。    // 这样冻结列看起来和普通列一致,不额外变色、不单独使用另一套样式。    m_frozenTableView->verticalHeader()->setVisible(m_tableView->verticalHeader()->isVisible());    m_frozenTableView->horizontalHeader()->setVisible(m_tableView->horizontalHeader()->isVisible());    m_frozenTableView->setSelectionBehavior(m_tableView->selectionBehavior());    m_frozenTableView->setSelectionMode(m_tableView->selectionMode());    m_frozenTableView->setEditTriggers(m_tableView->editTriggers());    m_frozenTableView->setAlternatingRowColors(m_tableView->alternatingRowColors());    m_frozenTableView->setShowGrid(m_tableView->showGrid());    m_frozenTableView->setWordWrap(m_tableView->wordWrap());    m_frozenTableView->setPalette(m_tableView->palette());    m_frozenTableView->viewport()->setPalette(m_tableView->viewport()->palette());    for (int column = 0; column < m_model->columnCount(); ++column) {        const bool frozen = enabled && column < frozenCount;        // 冻结表格只显示前 frozenCount 列;主表格仍显示所有列。        // 两个 view 叠加后,用户看到的是左侧固定、右侧横向滚动。        m_frozenTableView->setColumnHidden(column, !frozen);        m_tableView->setColumnHidden(column, false);        if (frozen) {            // 列宽必须同步,否则冻结区与主表格表头/单元格无法对齐。            m_frozenTableView->setColumnWidth(column, m_tableView->columnWidth(column));            m_frozenTableView->setItemDelegateForColumn(column, m_tableView->itemDelegateForColumn(column));        }    }    if (!enabled) {        // 冻结列数为 0 时直接隐藏叠加表格。        m_frozenTableView->hide();        return;    }    // 显示冻结表格前,先同步行高和几何位置。    syncFrozenRowHeights();    updateFrozenGeometry();    m_frozenTableView->show();    m_frozenTableView->raise();}voidMenuPluginTableViewWidget::updateFrozenGeometry(){    // 冻结表格宽度 = 垂直表头宽度 + 前 N 列列宽之和。    int frozenWidth = 0;    const int frozenCount = qMin(m_frozenColumnCount, m_model->columnCount());    for (int column = 0; column < frozenCount; ++column) {        frozenWidth += m_tableView->columnWidth(column);    }    const int verticalHeaderWidth = m_tableView->verticalHeader()->isVisible()        ? m_tableView->verticalHeader()->width()        : 0;    const int frameWidth = m_tableView->frameWidth();    // 冻结表格作为 m_tableView 的子控件,坐标相对主表格。    // 高度覆盖 horizontalHeader + viewport,保证表头和内容都被冻结。    m_frozenTableView->setGeometry(        frameWidth,        frameWidth,        verticalHeaderWidth + frozenWidth,        m_tableView->viewport()->height() + m_tableView->horizontalHeader()->height());}voidMenuPluginTableViewWidget::syncFrozenRowHeights(){    // model rebuild、行高调整、过滤后都需要同步行高和隐藏状态。    // 否则冻结列和普通列会出现上下错位。    for (int row = 0; row < m_model->rowCount(); ++row) {        m_frozenTableView->setRowHeight(row, m_tableView->rowHeight(row));        m_frozenTableView->setRowHidden(row, m_tableView->isRowHidden(row));    }}
关注我获取更多基础编程知识 
一个热爱编程、分享的 Bug 战士

👆🏻👆🏻👆🏻扫码关注👆🏻👆🏻👆🏻

点个「」赞,是我持续更新的动力 

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-05-29 06:28:02 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/680739.html
  2. 运行时间 : 0.111306s [ 吞吐率:8.98req/s ] 内存消耗:5,303.27kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=e971ca48515646e2df3d7a8d98cc97b5
  1. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/composer/autoload_static.php ( 6.05 KB )
  7. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/ralouphie/getallheaders/src/getallheaders.php ( 1.60 KB )
  10. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  11. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  12. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  13. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  14. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  15. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  16. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  17. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  18. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  19. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions_include.php ( 0.16 KB )
  21. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/guzzlehttp/guzzle/src/functions.php ( 5.54 KB )
  22. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  23. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  24. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  25. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/provider.php ( 0.19 KB )
  26. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  27. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  28. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  29. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/common.php ( 0.03 KB )
  30. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  32. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/alipay.php ( 3.59 KB )
  33. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  34. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/app.php ( 0.95 KB )
  35. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cache.php ( 0.78 KB )
  36. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/console.php ( 0.23 KB )
  37. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/cookie.php ( 0.56 KB )
  38. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/database.php ( 2.48 KB )
  39. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/filesystem.php ( 0.61 KB )
  40. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/lang.php ( 0.91 KB )
  41. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/log.php ( 1.35 KB )
  42. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/middleware.php ( 0.19 KB )
  43. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/route.php ( 1.89 KB )
  44. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/session.php ( 0.57 KB )
  45. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/trace.php ( 0.34 KB )
  46. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/config/view.php ( 0.82 KB )
  47. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/event.php ( 0.25 KB )
  48. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  49. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/service.php ( 0.13 KB )
  50. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/AppService.php ( 0.26 KB )
  51. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  52. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  53. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  54. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  55. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  56. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/services.php ( 0.14 KB )
  57. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  58. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  59. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  60. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  61. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  62. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  63. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  64. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  65. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  66. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  67. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  68. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  69. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  70. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  71. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  72. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  73. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  74. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  75. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  76. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  77. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  78. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  79. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  80. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  81. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  82. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  83. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  84. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  85. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  86. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  87. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/Request.php ( 0.09 KB )
  88. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  89. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/middleware.php ( 0.25 KB )
  90. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  91. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  92. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  93. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  94. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  95. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  96. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  97. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  98. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  99. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  100. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  101. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  102. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  103. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/route/app.php ( 3.94 KB )
  104. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  105. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  106. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Index.php ( 9.87 KB )
  108. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/BaseController.php ( 2.05 KB )
  109. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  110. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  111. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  112. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  113. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  114. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  115. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  116. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  117. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  118. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  119. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  120. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  121. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  122. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  123. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  124. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  125. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  126. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  127. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  128. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  129. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  130. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  131. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  132. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  133. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  134. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  135. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/app/controller/Es.php ( 3.30 KB )
  136. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  137. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  138. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  139. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  140. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  141. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  142. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  143. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  144. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/runtime/temp/c935550e3e8a3a4c27dd94e439343fdf.php ( 31.50 KB )
  145. /yingpanguazai/ssd/ssd1/www/wwww.yeyulingfeng.com/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000633s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000897s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.001023s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000293s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000694s ]
  6. SELECT * FROM `set` [ RunTime:0.000252s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000720s ]
  8. SELECT * FROM `article` WHERE `id` = 680739 LIMIT 1 [ RunTime:0.001296s ]
  9. UPDATE `article` SET `lasttime` = 1780007282 WHERE `id` = 680739 [ RunTime:0.006517s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000261s ]
  11. SELECT * FROM `article` WHERE `id` < 680739 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000533s ]
  12. SELECT * FROM `article` WHERE `id` > 680739 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000452s ]
  13. SELECT * FROM `article` WHERE `id` < 680739 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.000840s ]
  14. SELECT * FROM `article` WHERE `id` < 680739 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001873s ]
  15. SELECT * FROM `article` WHERE `id` < 680739 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.000724s ]
0.113179s