乐于分享
好东西不私藏

Qt 自定义网格拖拽控件实战:基于 MenuPluginGridWidget 的网格管理方案

Qt 自定义网格拖拽控件实战:基于 MenuPluginGridWidget 的网格管理方案

在 Qt 桌面项目中,如果只是做一个静态的网格布局,直接使用 QGridLayout 就已经足够。但一旦需求升级为:

  • 网格卡片可复用
  • 网格项由外部数据驱动
  • 网格项支持自定义 UI
  • 支持拖拽调整顺序
  • 支持不同的重排策略
  • 保持布局与业务解耦

那么单纯依赖 QGridLayout 就不够了。

在当前工程中,gridwidget.h 已经完成了对 MenuPluginGridWidget 的引用,而 MenuPluginGridWidget 则承担了整个 grid 场景下的核心管理职责。它并不是一个简单的“显示控件”,而是一个完整的网格拖拽容器组件。

先看看效果:

本文就结合当前代码,说明这套方案是如何工作的。

一、gridwidget 的角色:示例窗口,而不是业务耦合层

gridwidget.h

#pragma once#include <QWidget>#include "widget/gridWidget/menuplugingridwidget.h"#include "ui_gridwidget.h"classQLabel;classMenuPluginGridWidget;classQComboBox;namespace Ui {	classGridWidgetClass;}classGridWidget : publicQWidget{	Q_OBJECTpublic:	GridWidget(QWidget *parent = nullptr);	~GridWidget();private:	Ui::GridWidgetClass ui;	void updateStatusText();	QLabel* m_statusLabel;	QComboBox* m_modeComboBox;	MenuPluginGridWidget* m_gridWidget;};

gridwidget.cpp

#include "gridwidget.h"#include <QHBoxLayout>#include <QVBoxLayout>#include <QLabel>#include <QComboBox>namespace {	struct DemoGridItemData {		QString title;		QString subtitle;		QString tag;		QString footer;		QString color;	};// namespaceQ_DECLARE_METATYPE(DemoGridItemData)// //把你的自定义类型注册到 Qt 的元类型系统(Meta-Type System)中 以便它可以被 QVariant 使用。这对于在 Qt 的信号和槽机制中传递自定义类型非常有用。GridWidget::GridWidget(QWidget *parent)QWidget(parent)m_statusLabel(nullptr)m_modeComboBox(nullptr)m_gridWidget(nullptr){	qRegisterMetaType<DemoGridItemData>("DemoGridItemData"); // 把你的自定义类型注册到 Qt 的元类型系统(Meta-Type System)中 以便它可以被 QVariant 使用。	//创建布局	auto* rootLayout = new QVBoxLayout(this);	rootLayout->setContentsMargins(12121212);	rootLayout->setSpacing(8);	auto* titleLabel = new QLabel(QStringLiteral("QGridLayout定义网格拖拽插件"), this);	titleLabel->setObjectName(QStringLiteral("titleLabel"));	rootLayout->addWidget(titleLabel);	auto* descLabel = new QLabel(		QStringLiteral("插件只负责网格排版、滚动和拖拽。数据模型与 item 布局都由外部提供,外部通过 QVariant 列表和回调工厂返回完整的 item 视图。"),		this);	descLabel->setObjectName(QStringLiteral("descLabel"));	descLabel->setWordWrap(true);	rootLayout->addWidget(descLabel);	//工具栏	auto* toolPanel = new QFrame(this);	toolPanel->setObjectName(QStringLiteral("toolPanel"));	auto* toolLayout = new QHBoxLayout(toolPanel);	toolLayout->setContentsMargins(16121612);	toolLayout->setSpacing(12);	auto* modeLabel = new QLabel(QStringLiteral("拖拽模式"), toolPanel);	modeLabel->setObjectName(QStringLiteral("modeLabel"));	m_modeComboBox = new QComboBox(toolPanel);	m_modeComboBox->addItem(QStringLiteral("交换位置"), static_cast<int>(MenuPluginGridWidget::RearrangeMode::Swap));	m_modeComboBox->addItem(QStringLiteral("插入前移"), static_cast<int>(MenuPluginGridWidget::RearrangeMode::InsertShift));	m_statusLabel = new QLabel(toolPanel);	m_statusLabel->setObjectName(QStringLiteral("statusLabel"));	m_statusLabel->setText(QStringLiteral("状态说明"));	toolLayout->addWidget(modeLabel);	toolLayout->addWidget(m_modeComboBox);	toolLayout->addWidget(m_statusLabel, 1);	rootLayout->addWidget(toolPanel);	//网格内容	auto* gridCard = new QFrame(this);	gridCard->setObjectName(QStringLiteral("gridCard"));	auto* gridLayout = new QVBoxLayout(gridCard);	gridLayout->setContentsMargins(18181818);	gridLayout->setSpacing(0);	m_gridWidget = new MenuPluginGridWidget(gridCard);	m_gridWidget->setColumnCount(4);	m_gridWidget->setGridSpacing(1616);    m_gridWidget->setItemWidgetFactory([](const QVariant& item, QWidget* parent) -> QWidget* {        if (!item.canConvert<DemoGridItemData>()) {            return nullptr;        }        const DemoGridItemData data = item.value<DemoGridItemData>();        auto* content = new QWidget(parent);        auto* layout = new QVBoxLayout(content);        layout->setContentsMargins(0000);        layout->setSpacing(10);        auto* headerLayout = new QHBoxLayout();        headerLayout->setContentsMargins(0000);        headerLayout->setSpacing(8);        auto* badge = new QLabel(data.tag, content);        badge->setObjectName(QStringLiteral("gridBadge"));        auto* titleLabel = new QLabel(data.title, content);        titleLabel->setObjectName(QStringLiteral("gridCustomTitle"));        titleLabel->setWordWrap(true);        headerLayout->addWidget(badge, 0);        headerLayout->addWidget(titleLabel, 1);        auto* subtitleLabel = new QLabel(data.subtitle, content);        subtitleLabel->setObjectName(QStringLiteral("gridCustomSubtitle"));        subtitleLabel->setWordWrap(true);        auto* footerLabel = new QLabel(data.footer, content);        footerLabel->setObjectName(QStringLiteral("gridCustomFooter"));        footerLabel->setWordWrap(true);        layout->addLayout(headerLayout);        layout->addWidget(subtitleLabel);        layout->addStretch();        layout->addWidget(footerLabel);        content->setStyleSheet(QStringLiteral(R"(            QLabel#gridBadge {                min-width: 48px;                padding: 4px 10px;                color: #ffffff;                font-size: 12px;                font-weight: 700;                background: %1;            }            QLabel#gridCustomTitle {                color: #0f172a;                font-size: 17px;                font-weight: 800;            }            QLabel#gridCustomSubtitle {                color: #475569;                font-size: 13px;            }            QLabel#gridCustomFooter {                color: %1;                font-size: 12px;                font-weight: 700;            }        )").arg(data.color));        return content;        });    QList<QVariant> items;    const QList<DemoGridItemData> demoItems = {        {QStringLiteral("数据大屏"), QStringLiteral("整合今日核心指标、趋势图和异常提醒。"), QStringLiteral("监控"), QStringLiteral("实时更新 · 12个数据源"), QStringLiteral("#2563eb")},        {QStringLiteral("系统管理"), QStringLiteral("管理角色、菜单、组织结构和权限边界。"), QStringLiteral("平台"), QStringLiteral("支持多租户隔离"), QStringLiteral("#7c3aed")},        {QStringLiteral("用户中心"), QStringLiteral("查看用户档案、状态、标签与活跃行为。"), QStringLiteral("用户"), QStringLiteral("用户画像实时同步"), QStringLiteral("#0891b2")},        {QStringLiteral("订单管理"), QStringLiteral("覆盖订单检索、售后进度与履约处理。"), QStringLiteral("交易"), QStringLiteral("今日待处理 37 笔"), QStringLiteral("#ea580c")},        {QStringLiteral("内容中心"), QStringLiteral("文章、专题、素材和审核流统一管理。"), QStringLiteral("内容"), QStringLiteral("支持草稿与版本回滚"), QStringLiteral("#16a34a")},        {QStringLiteral("营销活动"), QStringLiteral("优惠券、活动页、推送计划和复盘面板。"), QStringLiteral("增长"), QStringLiteral("活动转化率 14.8%"), QStringLiteral("#dc2626")},        {QStringLiteral("监控告警"), QStringLiteral("服务状态、链路日志与故障播报统一展示。"), QStringLiteral("运维"), QStringLiteral("过去24小时告警 5 条"), QStringLiteral("#0f766e")},        {QStringLiteral("基础设置"), QStringLiteral("站点参数、主题、通知和第三方接入配置。"), QStringLiteral("配置"), QStringLiteral("最近修改于 09:20"), QStringLiteral("#4f46e5")},        {QStringLiteral("报表中心"), QStringLiteral("按日周月维度导出业务报表与统计结果。"), QStringLiteral("分析"), QStringLiteral("支持异步导出"), QStringLiteral("#9333ea")},        {QStringLiteral("设备管理"), QStringLiteral("跟踪设备在线状态、版本和升级策略。"), QStringLiteral("设备"), QStringLiteral("在线设备 124 台"), QStringLiteral("#0284c7")},        {QStringLiteral("任务中心"), QStringLiteral("统一编排定时任务、审批任务和回调任务。"), QStringLiteral("任务"), QStringLiteral("待执行 8 个任务"), QStringLiteral("#ca8a04")},        {QStringLiteral("知识库"), QStringLiteral("沉淀文档、FAQ 和内部培训资料。"), QStringLiteral("文档"), QStringLiteral("最近新增 12 篇内容"), QStringLiteral("#1d4ed8")},        {QStringLiteral("渠道管理"), QStringLiteral("维护渠道配置、分佣策略和渠道数据看板。"), QStringLiteral("渠道"), QStringLiteral("覆盖 18 个分发渠道"), QStringLiteral("#be123c")},        {QStringLiteral("财务结算"), QStringLiteral("对账、开票、付款单和结算流程闭环管理。"), QStringLiteral("财务"), QStringLiteral("本月结算中 6 单"), QStringLiteral("#0f766e")}    };    for (const DemoGridItemData& item : demoItems) {        items.append(QVariant::fromValue(item));    }    m_gridWidget->setItems(items);    gridLayout->addWidget(m_gridWidget);    connect(m_modeComboBox, &QComboBox::currentIndexChanged, this, [this](int index) {        const auto mode = static_cast<MenuPluginGridWidget::RearrangeMode>(            m_modeComboBox->itemData(index).toInt());        m_gridWidget->setRearrangeMode(mode);        updateStatusText();        });    connect(m_gridWidget, &MenuPluginGridWidget::orderChanged, this, [this](const QList<QVariant>& items) {        QString title = QStringLiteral("-");        if (!items.isEmpty() && items.first().canConvert<DemoGridItemData>()) {            title = items.first().value<DemoGridItemData>().title;        }        const QString message = QStringLiteral("当前顺序已更新,第一项为:%1").arg(title);        m_statusLabel->setText(message);    });	rootLayout->addWidget(gridCard, 1);	//设置根布局	setLayout(rootLayout);}//刷新状态栏void GridWidget::updateStatusText() {    const auto mode = static_cast<MenuPluginGridWidget::RearrangeMode>(m_modeComboBox->currentData().toInt());    m_gridWidget->setRearrangeMode(mode);    if (mode == MenuPluginGridWidget::RearrangeMode::Swap) {        m_statusLabel->setText(QStringLiteral("当前为交换模式:拖到目标网格后,两个网格直接互换位置。"));    }    else {        m_statusLabel->setText(QStringLiteral("当前为插入前移模式:拖动时会显示插入线,释放后插入到对应位置。"));    }}GridWidget::~GridWidget(){}

从 gridwidget.h 可以看到,gridwidget本身非常轻量:

  • 只持有状态文字 m_statusLabel
  • 一个模式切换框 m_modeComboBox
  • 一个核心网格控件 m_gridWidget

也就是说,gridwidget的职责并不是实现网格逻辑,而是:

  1. 初始化演示界面
  2. 准备演示数据
  3. 配置 MenuPluginGridWidget
  4. 监听排序变化并反馈状态

这种职责划分很重要。因为真正的“网格管理能力”被封装在 MenuPluginGridWidget 内部,窗口层只是使用者。

二、如何接入 MenuPluginGridWidget

在 gridwidget.cpp 中,核心接入逻辑主要分成三步。

1. 创建控件实例

m_gridWidget = new MenuPluginGridWidget(gridCard);m_gridWidget->setColumnCount(4);m_gridWidget->setGridSpacing(1616);

这里完成了三件事:

  • 创建网格控件
  • 设置每行列数为 4
  • 设置水平、垂直间距

这说明 MenuPluginGridWidget 并不是写死布局,而是允许外部控制网格参数。

2. 通过工厂回调定义每个卡片的显示方式

m_gridWidget->setItemWidgetFactory([](const QVariant &item, QWidget *parent) -> QWidget * {    ...});

这是整套方案最关键的地方。

MenuPluginGridWidget 不直接关心“每个格子长什么样”,而是把渲染责任交给外部。
也就是说:

  • 插件只负责“管理网格”
  • 外部负责“生成每个卡片内容”

这就是一个很典型的“容器控件 + 外部渲染工厂”的设计。

当前示例里,外部通过 DemoGridItemData 提供:

  • title
  • subtitle
  • tag
  • footer
  • color

然后在工厂函数中将这些数据组装成一个卡片视图。

这种做法的好处是非常明显的:

  • 网格控件可以复用在不同业务中
  • 不同页面可以复用同一个网格管理器
  • 外观变化不影响拖拽逻辑
  • 数据结构变化不影响网格核心实现

3. 通过 QVariant 传递数据

示例中用了:
Q_DECLARE_METATYPE(DemoGridItemData)qRegisterMetaType<DemoGridItemData>("DemoGridItemData");
然后把数据封装进 QVariant 列表,再传给:
m_gridWidget->setItems(items);

这说明 MenuPluginGridWidget 的数据输入模型是泛型化的。
它不依赖具体业务对象,而是统一接收 QVariant

这套设计使控件的工程化程度明显提高,因为它不需要知道:

  • 这是订单卡片
  • 这是用户卡片
  • 这是报表卡片
  • 还是监控面板卡片

它只负责把这些“项”放进网格并支持重排。

三、MenuPluginGridWidget 的核心定位:网格容器管理器

menuplugingridwidget.h

#pragma once#include <functional>#include <QFrame>#include <QList>#include <QPointer>#include <QVariant>#include <QWidget>class QGridLayout;class QLabel;class QMouseEvent;class QResizeEvent;class QScrollArea;class QScrollBar;class QVBoxLayout;class GridTileWidget : public QFrame{    Q_OBJECTpublic:    explicitGridTileWidget(QWidget* parent = nullptr);    voidsetContentWidget(QWidget* contentWidget);    QWidget* contentWidget()const;    voidsetDragging(bool dragging);    voidsetDropTarget(bool dropTarget);signals:    voidpressed(const QPoint& globalPos);    voidmoved(const QPoint& globalPos);    voidreleased(const QPoint& globalPos);protected:    voidmousePressEvent(QMouseEvent* event)override;    voidmouseMoveEvent(QMouseEvent* event)override;    voidmouseReleaseEvent(QMouseEvent* event)override;private:    voidapplyVisualState();    QVBoxLayout* m_contentLayout;    QPointer<QWidget> m_contentWidget;    bool m_dragging;    bool m_dropTarget;};class MenuPluginGridWidget : public QWidget{    Q_OBJECTpublic:    using GridItemData = QVariant;    enum class RearrangeMode {        Swap,        InsertShift    };    using ItemWidgetFactory = std::function<QWidget* (const QVariant& item, QWidget* parent)>;    using TileSizeProvider = std::function<QSize(const QSize& defaultSize, const QVariant& item, int index)>;    explicitMenuPluginGridWidget(QWidget* parent = nullptr);    voidsetItems(const QList<QVariant>& items);    QList<QVariant> items()const;    voidsetColumnCount(int columns);    intcolumnCount()const;    voidsetRearrangeMode(RearrangeMode mode);    RearrangeMode rearrangeMode()const;    voidsetGridSpacing(int horizontalSpacing, int verticalSpacing);    voidsetItemWidgetFactory(const ItemWidgetFactory& factory);    voidclearItemWidgetFactory();    voidsetTileSizeProvider(const TileSizeProvider& provider);    voidclearTileSizeProvider();    QSize defaultTileSize()const;signals:    voidorderChanged(const QList<QVariant>& items);protected:    voidresizeEvent(QResizeEvent* event)override;private:    voidrebuildGrid();    voidclearGrid();    voidupdateTileSizes();    intindexOfTile(GridTileWidget* tile)const;    GridTileWidget* tileAtGlobalPos(const QPoint& globalPos)const;    voidbeginDrag(GridTileWidget* tile, const QPoint& globalPos);    voidupdateDrag(const QPoint& globalPos);    voidfinishDrag(const QPoint& globalPos);    voidclearDragState();    voidapplyReorder(int fromIndex, int toIndex);    QWidget* createDragPreview(GridTileWidget* tile)const;    voidupdatePreviewPosition(const QPoint& globalPos);    intinsertionIndexForGlobalPos(const QPoint& globalPos)const;    voidupdateInsertIndicator(int insertIndex);    QWidget* buildItemWidget(const QVariant& item, QWidget* parent)const;    QSize tileSizeForIndex(int index)const;    voidrestoreScrollPosition(int verticalValue, int horizontalValue);    QList<QVariant> m_items;    QList<GridTileWidget*> m_tiles;    QScrollArea* m_scrollArea;    QWidget* m_contentWidget;    QGridLayout* m_gridLayout;    int m_columnCount;    RearrangeMode m_mode;    ItemWidgetFactory m_itemWidgetFactory;    TileSizeProvider m_tileSizeProvider;    QPointer<GridTileWidget> m_dragTile;    QPointer<GridTileWidget> m_dropTargetTile;    QPointer<QWidget> m_dragPreview;    QPointer<QWidget> m_insertIndicator;    QPoint m_dragOffset;    int m_dragSourceIndex;    int m_previewInsertIndex;};
menuplugingridwidget.cpp

#include "menuplugingridwidget.h"#include <QFrame>#include <QGridLayout>#include <QHBoxLayout>#include <QLabel>#include <QLayout>#include <QMouseEvent>#include <QResizeEvent>#include <QScrollArea>#include <QScrollBar>#include <QSizePolicy>#include <QTimer>#include <QVBoxLayout>#include <limits>GridTileWidget::GridTileWidget(QWidget* parent)    : QFrame(parent)    , m_contentLayout(new QVBoxLayout(this))    , m_dragging(false)    , m_dropTarget(false){    m_contentLayout->setContentsMargins(14141414);    m_contentLayout->setSpacing(0);    setObjectName(QStringLiteral("gridTile"));    setFrameShape(QFrame::NoFrame);    setSizePolicy(QSizePolicy::FixedQSizePolicy::Fixed);    setCursor(Qt::OpenHandCursor);    applyVisualState();}void GridTileWidget::setContentWidget(QWidget* contentWidget){    if (m_contentWidget == contentWidget) {        return;    }    if (m_contentWidget != nullptr) {        m_contentLayout->removeWidget(m_contentWidget);        m_contentWidget->deleteLater();        m_contentWidget.clear();    }    if (contentWidget == nullptr) {        return;    }    contentWidget->setParent(this);    m_contentLayout->addWidget(contentWidget);    m_contentWidget = contentWidget;    m_contentLayout->activate();    if (m_contentWidget->layout() != nullptr) {        m_contentWidget->layout()->activate();    }    m_contentWidget->show();    m_contentWidget->updateGeometry();    m_contentWidget->update();    updateGeometry();    update();}QWidget* GridTileWidget::contentWidget() const{    return m_contentWidget;}void GridTileWidget::setDragging(bool dragging){    m_dragging = dragging;    applyVisualState();}void GridTileWidget::setDropTarget(bool dropTarget){    m_dropTarget = dropTarget;    applyVisualState();}void GridTileWidget::mousePressEvent(QMouseEvent* event){    if (event->button() == Qt::LeftButton) {        setCursor(Qt::ClosedHandCursor);        emit pressed(event->globalPosition().toPoint());    }    QFrame::mousePressEvent(event);}void GridTileWidget::mouseMoveEvent(QMouseEvent* event){    if (event->buttons() & Qt::LeftButton) {        emit moved(event->globalPosition().toPoint());    }    QFrame::mouseMoveEvent(event);}void GridTileWidget::mouseReleaseEvent(QMouseEvent* event){    if (event->button() == Qt::LeftButton) {        setCursor(Qt::OpenHandCursor);        emit released(event->globalPosition().toPoint());    }    QFrame::mouseReleaseEvent(event);}void GridTileWidget::applyVisualState(){    if (m_dragging) {        setStyleSheet(QStringLiteral(R"(            QFrame#gridTile {                background: #eff6ff;                border: 1px solid #93c5fd;            }        )"));        return;    }    if (m_dropTarget) {        setStyleSheet(QStringLiteral(R"(            QFrame#gridTile {                background: #dbeafe;                border: 2px solid #1d4ed8;            }        )"));        return;    }    setStyleSheet(QStringLiteral(R"(        QFrame#gridTile {            background: #ffffff;            border: 1px solid #dbe3ee;        }    )"));}MenuPluginGridWidget::MenuPluginGridWidget(QWidget* parent)    : QWidget(parent)    , m_scrollArea(new QScrollArea(this))    , m_contentWidget(new QWidget(this))    , m_gridLayout(new QGridLayout(m_contentWidget))    , m_columnCount(4)    , m_mode(RearrangeMode::Swap)    , m_dragSourceIndex(-1)    , m_previewInsertIndex(-1){    auto* rootLayout = new QVBoxLayout(this);    rootLayout->setContentsMargins(0000);    rootLayout->setSpacing(0);    m_scrollArea->setFrameShape(QFrame::NoFrame);    m_scrollArea->setWidgetResizable(true);    m_scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);    m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);    m_scrollArea->setWidget(m_contentWidget);    m_contentWidget->setObjectName(QStringLiteral("gridScrollContent"));    m_contentWidget->setSizePolicy(QSizePolicy::PreferredQSizePolicy::Maximum);    m_gridLayout->setContentsMargins(0000);    m_gridLayout->setHorizontalSpacing(14);    m_gridLayout->setVerticalSpacing(14);    m_gridLayout->setSizeConstraint(QLayout::SetMinAndMaxSize);    rootLayout->addWidget(m_scrollArea);    setStyleSheet(QStringLiteral(R"(        QScrollArea {            background: transparent;            border: none;        }        QScrollArea > QWidget > QWidget#gridScrollContent {            background: transparent;        }    )"));}void MenuPluginGridWidget::setItems(const QList<GridItemData>& items){    m_items = items;    rebuildGrid();}QList<MenuPluginGridWidget::GridItemDataMenuPluginGridWidget::items() const{    return m_items;}void MenuPluginGridWidget::setColumnCount(int columns){    m_columnCount = qMax(1, columns);    rebuildGrid();}int MenuPluginGridWidget::columnCount() const{    return m_columnCount;}void MenuPluginGridWidget::setRearrangeMode(RearrangeMode mode){    m_mode = mode;}MenuPluginGridWidget::RearrangeMode MenuPluginGridWidget::rearrangeMode() const{    return m_mode;}void MenuPluginGridWidget::setGridSpacing(int horizontalSpacing, int verticalSpacing){    m_gridLayout->setHorizontalSpacing(qMax(0, horizontalSpacing));    m_gridLayout->setVerticalSpacing(qMax(0, verticalSpacing));    updateTileSizes();}void MenuPluginGridWidget::setItemWidgetFactory(const ItemWidgetFactory& factory){    m_itemWidgetFactory = factory;    rebuildGrid();}void MenuPluginGridWidget::clearItemWidgetFactory(){    m_itemWidgetFactory = ItemWidgetFactory();    rebuildGrid();}void MenuPluginGridWidget::setTileSizeProvider(const TileSizeProvider& provider){    m_tileSizeProvider = provider;    updateTileSizes();}void MenuPluginGridWidget::clearTileSizeProvider(){    m_tileSizeProvider = TileSizeProvider();    updateTileSizes();}QSize MenuPluginGridWidget::defaultTileSize() const{    const int viewportWidth = m_scrollArea->viewport() != nullptr        ? m_scrollArea->viewport()->width()        : width();    const QMargins margins = m_gridLayout->contentsMargins();    const int spacing = m_gridLayout->horizontalSpacing();    const int usableWidth = qMax(        0,        viewportWidth - margins.left() - margins.right() - spacing * qMax(0, m_columnCount - 1));    const int tileWidth = qMax(1, usableWidth / qMax(1, m_columnCount));    return QSize(tileWidth, tileWidth);}void MenuPluginGridWidget::resizeEvent(QResizeEvent* event){    QWidget::resizeEvent(event);    updateTileSizes();}void MenuPluginGridWidget::rebuildGrid(){    const int verticalScrollValue = m_scrollArea->verticalScrollBar() != nullptr        ? m_scrollArea->verticalScrollBar()->value()        : 0;    const int horizontalScrollValue = m_scrollArea->horizontalScrollBar() != nullptr        ? m_scrollArea->horizontalScrollBar()->value()        : 0;    clearGrid();    for (int index = 0; index < m_items.size(); ++index) {        const GridItemData& data = m_items.at(index);        auto* tile = new GridTileWidget(m_contentWidget);        tile->setContentWidget(buildItemWidget(data, tile));        m_tiles.append(tile);        connect(tile, &GridTileWidget::pressed, this, [this, tile](const QPoint& globalPos) {            beginDrag(tile, globalPos);            });        connect(tile, &GridTileWidget::moved, this, [this](const QPoint& globalPos) {            updateDrag(globalPos);            });        connect(tile, &GridTileWidget::released, this, [this](const QPoint& globalPos) {            finishDrag(globalPos);            });        const int row = index / m_columnCount;        const int column = index % m_columnCount;        m_gridLayout->addWidget(tile, row, column);    }    updateTileSizes();    QTimer::singleShot(0, this, [this]() {        updateTileSizes();        for (GridTileWidget* tile : m_tiles) {            if (tile == nullptr) {                continue;            }            if (tile->layout() != nullptr) {                tile->layout()->activate();            }            if (tile->contentWidget() != nullptr) {                if (tile->contentWidget()->layout() != nullptr) {                    tile->contentWidget()->layout()->activate();                }                tile->contentWidget()->updateGeometry();                tile->contentWidget()->update();            }            tile->updateGeometry();            tile->update();        }        m_contentWidget->updateGeometry();        m_contentWidget->update();        });    QTimer::singleShot(0, this, [this, verticalScrollValue, horizontalScrollValue]() {        restoreScrollPosition(verticalScrollValue, horizontalScrollValue);        });}void MenuPluginGridWidget::clearGrid(){    clearDragState();    m_tiles.clear();    while (QLayoutItem* item = m_gridLayout->takeAt(0)) {        if (QWidget* widget = item->widget()) {            widget->deleteLater();        }        delete item;    }}void MenuPluginGridWidget::updateTileSizes(){    if (m_tiles.isEmpty()) {        m_contentWidget->setMinimumHeight(0);        return;    }    for (int index = 0; index < m_tiles.size(); ++index) {        GridTileWidget* tile = m_tiles.at(index);        if (tile == nullptr) {            continue;        }        tile->setFixedSize(tileSizeForIndex(index));    }    m_contentWidget->setMinimumWidth(m_scrollArea->viewport()->width());    m_contentWidget->setMinimumHeight(m_gridLayout->sizeHint().height());    m_contentWidget->updateGeometry();}int MenuPluginGridWidget::indexOfTile(GridTileWidget* tile) const{    return m_tiles.indexOf(tile);}GridTileWidget* MenuPluginGridWidget::tileAtGlobalPos(const QPoint& globalPos) const{    for (GridTileWidget* tile : m_tiles) {        if (tile == nullptr || tile == m_dragTile) {            continue;        }        const QRect rect(tile->mapToGlobal(QPoint(00)), tile->size());        if (rect.contains(globalPos)) {            return tile;        }    }    return nullptr;}void MenuPluginGridWidget::beginDrag(GridTileWidget* tile, const QPoint& globalPos){    if (tile == nullptr || m_items.size() <= 1) {        return;    }    m_dragTile = tile;    m_dragSourceIndex = indexOfTile(tile);    m_previewInsertIndex = m_dragSourceIndex;    m_dragTile->setDragging(true);    const QPoint tileTopLeft = tile->mapToGlobal(QPoint(00));    m_dragOffset = globalPos - tileTopLeft;    m_dragPreview = createDragPreview(tile);    updatePreviewPosition(globalPos);}void MenuPluginGridWidget::updateDrag(const QPoint& globalPos){    if (m_dragTile == nullptr || m_dragPreview == nullptr) {        return;    }    updatePreviewPosition(globalPos);    GridTileWidget* target = tileAtGlobalPos(globalPos);    if (m_dropTargetTile == target) {        return;    }    if (m_dropTargetTile != nullptr) {        m_dropTargetTile->setDropTarget(false);    }    m_dropTargetTile = target;    if (m_dropTargetTile != nullptr) {        m_dropTargetTile->setDropTarget(true);    }    if (m_mode == RearrangeMode::InsertShift && m_insertIndicator != nullptr) {        m_insertIndicator->hide();    }}void MenuPluginGridWidget::finishDrag(const QPoint& globalPos){    if (m_dragTile == nullptr) {        return;    }    if (m_mode == RearrangeMode::InsertShift) {        const int fromIndex = m_dragSourceIndex;        GridTileWidget* target = tileAtGlobalPos(globalPos);        const int targetIndex = indexOfTile(target);        if (fromIndex >= 0 && fromIndex < m_items.size() && targetIndex >= 0 && fromIndex != targetIndex) {            const GridItemData moving = m_items.takeAt(fromIndex);            const int finalIndex = targetIndex > fromIndex ? targetIndex : targetIndex + 1;            m_items.insert(qBound(0, finalIndex, m_items.size()), moving);            emit orderChanged(m_items);        }        rebuildGrid();        return;    }    GridTileWidget* target = tileAtGlobalPos(globalPos);    if (target != nullptr) {        const int fromIndex = indexOfTile(m_dragTile);        const int toIndex = indexOfTile(target);        if (fromIndex >= 0 && toIndex >= 0 && fromIndex != toIndex) {            applyReorder(fromIndex, toIndex);            emit orderChanged(m_items);        }    }    else {        rebuildGrid();        return;    }    clearDragState();}void MenuPluginGridWidget::clearDragState(){    if (m_dragTile != nullptr) {        m_dragTile->setDragging(false);        m_dragTile.clear();    }    if (m_dropTargetTile != nullptr) {        m_dropTargetTile->setDropTarget(false);        m_dropTargetTile.clear();    }    if (m_dragPreview != nullptr) {        m_dragPreview->hide();        m_dragPreview->deleteLater();        m_dragPreview.clear();    }    if (m_insertIndicator != nullptr) {        m_insertIndicator->hide();        m_insertIndicator->deleteLater();        m_insertIndicator.clear();    }    m_dragSourceIndex = -1;    m_previewInsertIndex = -1;}void MenuPluginGridWidget::applyReorder(int fromIndex, int toIndex){    if (fromIndex < 0 || fromIndex >= m_items.size() || toIndex < 0 || toIndex >= m_items.size()) {        return;    }    if (m_mode == RearrangeMode::Swap) {        m_items.swapItemsAt(fromIndex, toIndex);    }    else {        const GridItemData moving = m_items.takeAt(fromIndex);        m_items.insert(toIndex, moving);    }    rebuildGrid();}QWidget* MenuPluginGridWidget::createDragPreview(GridTileWidget* tile) const{    auto* preview = new QLabel(nullptr, Qt::ToolTip | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint);    preview->setAttribute(Qt::WA_TransparentForMouseEventstrue);    const QPixmap originalPixmap = tile->grab();    const QSize originalSize = tile->size();    const QSize previewSize(qMax(96, originalSize.width() - 40), qMax(96, originalSize.height() - 40));    const QPixmap scaledPixmap = originalPixmap.scaled(        previewSize,        Qt::IgnoreAspectRatio,        Qt::SmoothTransformation);    auto* previewLabel = qobject_cast<QLabel*>(preview);    previewLabel->setPixmap(scaledPixmap);    previewLabel->setScaledContents(true);    preview->setFixedSize(previewSize);    preview->setStyleSheet(QStringLiteral(R"(        QLabel {            background: rgba(255, 255, 255, 242);            border: 1px solid #1d4ed8;        }    )"));    preview->show();    preview->raise();    return preview;}void MenuPluginGridWidget::updatePreviewPosition(const QPoint& globalPos){    if (m_dragPreview == nullptr) {        return;    }    const QPoint previewOffset(        qBound(18, m_dragOffset.x(), qMax(18, m_dragPreview->width() - 18)),        qBound(18, m_dragOffset.y(), qMax(18, m_dragPreview->height() - 18)));    m_dragPreview->move(globalPos - previewOffset);    m_dragPreview->raise();}int MenuPluginGridWidget::insertionIndexForGlobalPos(const QPoint& globalPos) const{    QList<GridTileWidget*> visibleTiles;    visibleTiles.reserve(m_tiles.size());    for (GridTileWidget* tile : m_tiles) {        if (tile != nullptr && tile != m_dragTile) {            visibleTiles.append(tile);        }    }    if (visibleTiles.isEmpty()) {        return 0;    }    GridTileWidget* nearestTile = nullptr;    int nearestVisualIndex = -1;    qint64 nearestDistance = std::numeric_limits<qint64>::max();    for (int visualIndex = 0; visualIndex < visibleTiles.size(); ++visualIndex) {        GridTileWidget* tile = visibleTiles.at(visualIndex);        const QRect rect(tile->mapToGlobal(QPoint(00)), tile->size());        const QPoint center = rect.center();        const qint64 dx = qint64(globalPos.x()) - center.x();        const qint64 dy = qint64(globalPos.y()) - center.y();        const qint64 distance = dx * dx + dy * dy;        if (distance < nearestDistance) {            nearestDistance = distance;            nearestTile = tile;            nearestVisualIndex = visualIndex;        }        if (!rect.contains(globalPos)) {            continue;        }        return globalPos.x() < rect.center().x() ? visualIndex : visualIndex + 1;    }    if (nearestTile != nullptr && nearestVisualIndex >= 0) {        const QRect nearestRect(nearestTile->mapToGlobal(QPoint(00)), nearestTile->size());        return globalPos.x() < nearestRect.center().x() ? nearestVisualIndex : nearestVisualIndex + 1;    }    return qBound(0, m_previewInsertIndex, visibleTiles.size());}void MenuPluginGridWidget::updateInsertIndicator(int insertIndex){    if (m_mode != RearrangeMode::InsertShift) {        return;    }    if (m_insertIndicator == nullptr) {        auto* line = new QFrame(m_contentWidget);        line->setAttribute(Qt::WA_TransparentForMouseEventstrue);        line->setStyleSheet(QStringLiteral(R"(            QFrame {                background: #1d4ed8;            }        )"));        line->setFixedWidth(4);        line->show();        line->raise();        m_insertIndicator = line;    }    QList<GridTileWidget*> visibleTiles;    visibleTiles.reserve(m_tiles.size());    for (GridTileWidget* tile : m_tiles) {        if (tile != nullptr && tile != m_dragTile) {            visibleTiles.append(tile);        }    }    if (visibleTiles.isEmpty()) {        m_insertIndicator->hide();        return;    }    int indicatorX = 0;    int indicatorY = 0;    int indicatorHeight = 0;    const int spacing = qMax(8, m_gridLayout->horizontalSpacing());    if (insertIndex <= 0) {        GridTileWidget* firstTile = visibleTiles.first();        indicatorX = firstTile->x() - 7;        indicatorY = firstTile->y() + 8;        indicatorHeight = qMax(40, firstTile->height() - 16);    }    else if (insertIndex >= visibleTiles.size()) {        GridTileWidget* lastTile = visibleTiles.last();        indicatorX = lastTile->x() + lastTile->width() + spacing / 2 - 2;        indicatorY = lastTile->y() + 8;        indicatorHeight = qMax(40, lastTile->height() - 16);    }    else {        GridTileWidget* targetTile = visibleTiles.at(insertIndex);        GridTileWidget* previousTile = visibleTiles.at(insertIndex - 1);        const bool targetStartsNewVisualRow =            targetTile->y() > previousTile->y() && targetTile->x() <= previousTile->x();        if (targetStartsNewVisualRow) {            indicatorX = previousTile->x() + previousTile->width() + spacing / 2 - 2;            indicatorY = previousTile->y() + 8;            indicatorHeight = qMax(40, previousTile->height() - 16);        }        else {            indicatorX = targetTile->x() - 7;            indicatorY = targetTile->y() + 8;            indicatorHeight = qMax(40, targetTile->height() - 16);        }    }    indicatorX = qBound(0, indicatorX, qMax(0, m_contentWidget->width() - 4));    m_insertIndicator->setGeometry(indicatorX, indicatorY, 4, indicatorHeight);    m_insertIndicator->show();    m_insertIndicator->raise();}QWidget* MenuPluginGridWidget::buildItemWidget(const QVariant& item, QWidget* parentconst{    if (!m_itemWidgetFactory) {        return nullptr;    }    QWidget* widget = m_itemWidgetFactory(item, parent);    if (widget != nullptr) {        widget->setParent(parent);    }    return widget;}QSize MenuPluginGridWidget::tileSizeForIndex(int index) const{    const QSize squareSize = defaultTileSize();    if (!m_tileSizeProvider || index < 0 || index >= m_items.size()) {        return squareSize;    }    const QSize customSize = m_tileSizeProvider(squareSize, m_items.at(index), index);    if (!customSize.isValid()) {        return squareSize;    }    return QSize(qMax(1, customSize.width()), qMax(1, customSize.height()));}void MenuPluginGridWidget::restoreScrollPosition(int verticalValue, int horizontalValue){    if (m_scrollArea->verticalScrollBar() != nullptr) {        m_scrollArea->verticalScrollBar()->setValue(verticalValue);    }    if (m_scrollArea->horizontalScrollBar() != nullptr) {        m_scrollArea->horizontalScrollBar()->setValue(horizontalValue);    }}

从 menuplugingridwidget.h 可以看到,MenuPluginGridWidget 对外暴露的能力包括:

  • setItems
  • setColumnCount
  • setRearrangeMode
  • setGridSpacing
  • setItemWidgetFactory
  • setTileSizeProvider
  • orderChanged
     信号

这已经说明它不是一个普通 widget,而是一个完整的网格管理组件。

它主要负责五类事情:

  1. 管理网格项数据
  2. 负责网格布局和滚动容器
  3. 负责拖拽开始、移动、释放过程
  4. 负责根据策略调整项顺序
  5. 对外发出排序变化结果

四、为什么它不是直接用 QGridLayout 就结束了

很多人第一次做这类需求时,会直接把若干 QWidget 丢进 QGridLayout
这种方式只能解决“摆放”,解决不了“管理”。

MenuPluginGridWidget 内部其实是:

  • QScrollArea
  • m_contentWidget
  • QGridLayout

也就是说,它底层仍然使用 QGridLayout 完成静态排布,但外层又加了一层管理逻辑。

这样做的原因在于:

1. QGridLayout 本身不带拖拽排序能力

Qt 的布局系统负责计算位置,但不会帮你完成拖拽重排。

2. 需要统一处理滚动区域

代码里:

m_scrollArea->setWidgetResizable(true);m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);

这让网格在内容较多时具备滚动能力,而不是简单溢出窗口。

3. 需要重建布局

当元素顺序变化后,控件通过 rebuildGrid() 整体重建网格,而不是局部硬改布局坐标。
这种方式虽然朴素,但非常稳定,也更容易维护。

五、GridTileWidget:拖拽交互的最小单元

MenuPluginGridWidget 没有直接让内容 widget 自己处理拖拽,而是包了一层 GridTileWidget

GridTileWidget 的作用有三个:

  1. 承载内容 widget
  2. 捕获鼠标按下、移动、释放事件
  3. 维护拖拽中的视觉状态

它提供了几个重要接口:

  • setContentWidget
  • setDragging
  • setDropTarget

以及三个信号:

  • pressed
  • moved
  • released

这说明它本质上是一个“可拖拽卡片容器”,而不是业务内容本身。

这个拆分非常合理,因为:

  • 内容卡片不需要知道拖拽逻辑
  • 拖拽事件统一由外层容器接管
  • 视觉状态和业务显示分离

六、拖拽流程是如何完成的

整个拖拽流程在 MenuPluginGridWidget 内部是完整闭环的。

1. 按下时开始拖拽

beginDrag(tile, globalPos);

这里会记录:

  • 当前拖拽源项
  • 拖拽源索引
  • 鼠标相对偏移
  • 生成拖拽预览层

2. 移动时更新预览和目标项

updateDrag(globalPos);

这个阶段会做两件事:

  • 移动拖拽预览层
  • 找出当前鼠标所在的目标 tile,并更新高亮状态

如果是插入模式,还会控制插入指示器。

3. 释放时完成重排

finishDrag(globalPos);

释放后根据当前模式决定如何排序:

  • Swap
    :交换位置
  • InsertShift
    :插入后整体平移

最后发出:

emit orderChanged(m_items);

由外部拿到最新排序后的数据。

七、两种重排模式是这套控件的亮点

当前控件定义了:

enum classRearrangeMode{    Swap,    InsertShift};

这意味着拖拽并不是单一行为,而是可配置的。

1. Swap 交换模式

拖到目标项上后,两个卡片直接互换位置。

适合场景:

  • 仪表盘模块重排
  • 首页卡片换位
  • 快捷入口排序

2. InsertShift 插入前移模式

拖动项插入目标位置,其余项顺序平移。

适合场景:

  • 任务卡片排序
  • 内容块列表重排
  • 类似列表插入式编辑

gridwidget 里通过 QComboBox 切换这两种模式,说明这套控件不仅支持拖拽,还支持拖拽策略切换。

八、网格项尺寸并不是固定写死的

MenuPluginGridWidget 提供了:

  • defaultTileSize()
  • setTileSizeProvider(...)

这说明它的尺寸策略也是可插拔的。

默认情况下,它根据:

  • 当前 viewport 宽度
  • 列数
  • 间距
  • layout margins

自动计算一个方形 tile 尺寸。

如果外部有更复杂需求,比如:

  • 第一张卡片更大
  • 某些卡片横向跨列
  • 不同类型卡片有不同高度

就可以通过 TileSizeProvider 扩展,而不需要修改网格内部实现。

这种设计使控件从“固定演示控件”升级成了“可配置网格框架”。

九、为什么这套方案工程上是成立的

这套实现最值得肯定的地方,不是“能拖”,而是它做到了结构分层清晰。

1. 数据与视图分离

网格接收的是 QVariant 列表,而不是具体业务控件。

2. 视图生成外包

每个卡片长什么样,由 ItemWidgetFactory 决定。

3. 拖拽逻辑内聚

拖拽开始、更新、结束都在控件内部,不污染业务页面。

4. 排序结果对外输出

通过 orderChanged 把最终数据顺序通知外部,方便保存或同步。

5. 支持后续扩展

例如继续增加:

  • 占位动画
  • 键盘重排
  • 多选拖拽
  • 删除区
  • 固定卡片不可拖动
  • 跨行跨列 tile

都具备良好的演化空间。

十、Gridwidget这个示例说明了什么

gridwidget实际上已经很好地说明了 MenuPluginGridWidget 的使用方式:

  1. 定义自己的业务数据结构 DemoGridItemData
  2. 注册为元类型并封装进 QVariant
  3. 把数据列表传给 setItems
  4. 把卡片渲染逻辑通过 setItemWidgetFactory 传进去
  5. 用 orderChanged 监听最终顺序变化
  6. 用 setRearrangeMode 控制重排策略

这是一种非常标准的“容器控件使用模式”。

换句话说,gridwidget只是 demo,但它已经可以作为真实业务页面接入该控件的模板。

十一、总结

在这个工程里,gridwiget.h/.cpp 和 MenuPluginGridWidget 的配合,本质上实现的是一个“可复用的网格拖拽容器”。

它不是简单把若干 widget 丢进 QGridLayout,而是进一步解决了以下问题:

  • 网格项如何由外部数据驱动
  • 网格内容如何自定义渲染
  • 拖拽如何统一管理
  • 排序如何支持不同策略
  • 排序结果如何回传业务层
  • 布局、滚动、拖拽如何在同一组件内闭环

因此,MenuPluginGridWidget 的价值不在于“做了一个能拖的网格”,而在于它把 Qt 中原本分散的布局能力、滚动能力、事件处理能力和业务扩展能力组织成了一个完整的组件化方案。

关注我获取更多基础编程知识 
一个热爱编程、分享的 Bug 战士

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

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

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-05-29 19:05:46 HTTP/1.1 GET : https://www.yeyulingfeng.com/a/673117.html
  2. 运行时间 : 0.105744s [ 吞吐率:9.46req/s ] 内存消耗:5,150.62kb 文件加载:145
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=0b8b512e435a2bfd3eaa9a6a5decd83c
  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.000541s ] mysql:host=127.0.0.1;port=3306;dbname=wenku;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000618s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000295s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000280s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000500s ]
  6. SELECT * FROM `set` [ RunTime:0.000187s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000599s ]
  8. SELECT * FROM `article` WHERE `id` = 673117 LIMIT 1 [ RunTime:0.000674s ]
  9. UPDATE `article` SET `lasttime` = 1780052746 WHERE `id` = 673117 [ RunTime:0.012687s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000267s ]
  11. SELECT * FROM `article` WHERE `id` < 673117 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.000462s ]
  12. SELECT * FROM `article` WHERE `id` > 673117 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000464s ]
  13. SELECT * FROM `article` WHERE `id` < 673117 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.001519s ]
  14. SELECT * FROM `article` WHERE `id` < 673117 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001055s ]
  15. SELECT * FROM `article` WHERE `id` < 673117 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.002153s ]
0.107506s