资讯动态

Qt分页组件实战:用QWidget+QSS复刻ElementUI翻页体验

发布时间:2026/10/13 10:52:27 来源:尧图企业网站定制
简介一份面向Qt/C开发者的分页组件源码参考了ElementUI分页控件的交互与简洁风格解决桌面应用内长列表或数据表格的页码导航问题适合有一定C/Qt基础、希望快速获得可复用分页组件的开发者。压缩包包含两个文件一个头文件负责类定义、信号与槽接口声明一个实现文件承载页面切换、页号跳转等核心逻辑整体体积仅3KB轻量紧凑便于阅读与嵌入项目。该资源已有1080人学习是小而实用的参考案例。核心价值在于完整展示了自定义分页导航器从属性设计到事件响应的开发流程包括当前页、总页数等成员变量定义上一页、下一页、直接跳转、首页末页快捷跳转等槽函数以及通过QSS样式表模拟ElementUI外观的写法。同时结构简明为后续扩展每页显示条数选择、页码按钮组等功能留出了清晰框架。1. Qt 分页组件照着 ElementUI 分页把翻页体验搬进桌面端做客户端数据列表时表格里数据一过几百条翻页就成了刚需。这套 Qt 分页组件是参考 ElementUI 分页组件交互模型实现的一套 QWidget 控件页码按钮、总条数、跳页输入、每页条数下拉一应俱全连省略号的折叠策略都跟前端版保持一致。开发语言是 C/Qt但设计思路直接移植自前端组件QSS 书写方式和 CSS 同源前端经验能无缝带过来。适合做后台管理工具、数据看板、数据导出客户端的同学新手拿过去能直接编译运行熟手可以把它当二次开发底盘改交互、换皮肤都在这一套结构上动刀。2. 先把交互模型拆干净ElementUI 属性/事件/布局三层结构与 Qt 选型2.1 先把前端分页的交互模型抽出来ElementUI 的分页组件是一个 Vue 组件逻辑由 javascript 写成DOM 操作和响应式更新混在一起。直接往 Qt 搬最忌讳照着源码一行行翻译因为 DOM 操作在 QWidget 里根本没有对应物。我一般先抽交互模型对外暴露的属性就五个total、page-size、current-page、page-count、pager-count对外发的事件就两个current-change 和 page-size-change。内部状态只有一个 current加一个由 total 和 page-size 推导出来的 totalPage。搞清楚这个模型后面每一步实现都有据可依。这个模型里有两个模式要分清。total 模式是常规做法totalPage ceil(total / pageSize)total 变了总页数跟着变适合后端告诉你一共多少条的接口page-count 模式不关心总条数直接给固定页数适合那种接口只返回页数、不返回条数的场景。前端版还用 layout 字符串控制区域排列顺序比如 total, prev, pager, next, jumper 就按这个顺序摆五个区块Qt 侧完全可以照搬这个思路用字符串映射到布局。顺序在真实产品里是有讲究的有的后台要把总数放最左边强调数据量有的要把跳页放最后方便快速定位做成可配置比写死强得多。移植顺序我建议固定成三层先写纯函数算页码序列再写渲染层把序列变成按钮最后接信号槽。这样每一步都能单独验证页码算法可以脱离 UI 跑单元测试比整个控件黑盒测试好排查得多。2.2 为什么选 QWidget 组合控件而不是 QML同类需求常被问到直接用 QML 写分页器不是更接近前端吗QML 里写 JavaScript 逻辑确实顺手代价是工程要引入 QQuickWidget老项目里一堆 QWidget 页面突然混进一个 QML 渲染上下文事件穿透、焦点切换都是麻烦。常见的做法是留在 QWidget 体系里用 QPushButton 做页码按钮、QLabel 做总条数、QLineEdit 做跳页输入、QComboBox 做每页条数选择外面套一个 QHBoxLayout。接入成本最低任何 QWidget 容器里塞进去就能用。顺带说一句QTableView 本身只有滚动没有分页概念这个组件补的正是这个缺口。类骨架我一般这样定义信号命名直接对齐前端事件方便前后端对需求时不用翻译术语class Pagination : public QWidget { Q_OBJECT public: explicit Pagination(QWidget *parent nullptr); void setTotal(int total); // total 模式总条数 void setPageSize(int pageSize); // 每页条数默认 10 void setCurrentPage(int page); // 外部强制指定当前页 void setFixedPageCount(int pages); // page-count 模式固定页数 void setPagerCount(int count); // 页码位数量默认 7 void setLayoutSpec(const QString spec); // total,prev,pager,next,jumper int total() const; int pageSize() const; int currentPage() const; int pageCount() const; signals: void currentChanged(int currentPage); // 对齐 current-change void pageSizeChanged(int pageSize); // 对齐 size-change private: void rebuild(); // 状态变化后重建页码按钮 void applyLayout(); // 按布局规格排列子控件 };信号设计上currentChanged 只在 current 真正变化时发出pageSizeChanged 在每页条数下拉切换时发出。两个信号和前端组件的两个事件一一对应后续接数据层只需 connect 这两个信号不用关心按钮内部怎么画。rebuild 和 applyLayout 是两个私有入口前者管数据、后者管布局职责切分清楚之后避坑章里很多问题都能收敛到 rebuild 一个函数里排查。2.3 状态收敛所有变更都走同一个入口一个会影响后续所有问题的细节setTotal、setPageSize、setCurrentPage 这三个 setter内部都要先做边界夹紧再统一调 rebuild任何 setter 都不能直接改成员变量就返回。举个例子total 从 1000 改成 50pageSize 还是 10totalPage 从 100 缩成 5如果 current 还停在 30页码序列里根本没有 30高亮就会悬空。所以 setTotal 内部要重新计算 totalPage再用 qBound 把 current 夹回合法区间最后走 rebuild。不收敛会引发一个很隐蔽的现象页码按钮和实际 current 对不上active 高亮停在旧页上点了新页码又跳回去。这种问题靠打断点很难定位因为每个 setter 看起来都没错。把状态收敛到一条路径上之后只需要在 rebuild 入口加一行日志看每次重建前的 current 和 totalPage 即可排查时间能省一大半。3. 核心实现页码计算、按钮渲染与信号发射的一条链路3.1 页码序列算法pagerCount 与省略号哨兵分页器最核心的不是按钮而是页码序列怎么折叠。ElementUI 默认 pagerCount 是 7意思是包括省略号在内最多显示 7 个页码位。计算出的序列我放在 QVector 里用 -1 当省略号哨兵渲染层看到 -1 就摆一个 ··· 按钮。分三种情况总页数小于等于 7直接全量显示当前页靠左显示 1 到 5 再跟省略号和最后一页当前页靠右显示第一页、省略号加最后 5 页中间位置则显示第一页、省略号、当前页前后各一页、省略号、最后一页。这个策略和前端组件在相同输入下产出的序列完全一致QVectorint Pagination::calcPageItems(int current, int totalPage, int pagerCount) { QVectorint items; if (totalPage pagerCount) { for (int i 1; i totalPage; i) items.append(i); // 全量显示 return items; } if (current pagerCount - 1) { // 靠左1 2 3 4 5 ... totalPage for (int i 1; i pagerCount - 1; i) items.append(i); items.append(kEllipsis); // -1 哨兵 items.append(totalPage); } else if (current totalPage - pagerCount 2) { // 靠右1 ... totalPage-4 ... totalPage items.append(1); items.append(kEllipsis); for (int i totalPage - (pagerCount - 3); i totalPage; i) items.append(i); } else { // 中间1 ... current-1 current current1 ... totalPage items.append(1); items.append(kEllipsis); for (int i current - 1; i current 1; i) items.append(i); items.append(kEllipsis); items.append(totalPage); } return items; }参数说明pagerCount 是含省略号在内的最大页码位数默认 7不建议改成偶数否则中间态左右页码数不对称current 在进入函数前要夹到 [1, totalPage] 区间totalPage 最小必须是 1哪怕 total 传 0 也要显示第一页。这个函数是纯计算不碰任何控件可以直接拿去做单元测试边界用例就是 totalPage1、current1、currenttotalPage 这三组。3.2 渲染层动态按钮的创建与销毁拿到页码序列后渲染层把每个数字变成 QPushButton 放进水平布局。这里有一个容易翻车的点页码按钮不能复用。同一个按钮在不同 current 下可能代表不同页码与其做原地更新不如每次 rebuild 时清掉旧的重新创建。销毁用 deleteLater不要在按钮的 clicked 信号栈里直接 delete否则触发 use-after-freevoid Pagination::rebuild() { m_totalPage (m_mode TotalMode) ? qMax(1, (m_total m_pageSize - 1) / m_pageSize) : m_fixedPageCount; m_current qBound(1, m_current, m_totalPage); // 先夹紧再渲染 QVectorint items calcPageItems(m_current, m_totalPage, m_pagerCount); for (QPushButton *btn : qAsConst(m_pageButtons)) { m_pagerLayout-removeWidget(btn); btn-deleteLater(); // 延后销毁避免悬空指针 } m_pageButtons.clear(); for (int item : items) { if (item kEllipsis) { QPushButton *dot new QPushButton(QStringLiteral(···), this); dot-setObjectName(QStringLiteral(pagerEllipsis)); // 省略号也接点击左右跳 5 页见避坑章 } else { QPushButton *btn new QPushButton(QString::number(item), this); btn-setObjectName(QStringLiteral(pagerBtn)); btn-setProperty(page, item); btn-setProperty(active, item m_current); connect(btn, QPushButton::clicked, this, [this, item] { setCurrentPage(item); }); m_pageButtons.append(btn); } m_pagerLayout-addWidget(btn); } }提示按钮销毁用 deleteLater 而不是 delete否则在点击信号的调用栈里销毁对象会触发 use-after-free表现时好时坏很难复现。重建逻辑里 qMax 的使用值得注意total 0 时(0 9) / 10 在整数除法里结果是 0qMax(1, 0) 兜底成 1页面至少保留一个占位页码m_current 用 qBound 夹紧后active 高亮永远和 current 一致。每次 rebuild 触发一次布局重排频率完全可控几千条数据的分页场景下性能没有任何压力。3.3 交互层信号只在值真正变化时发射交互链路集中在 setCurrentPage 一个方法里。点击页码按钮、点击上一页下一页、跳页输入框回车最终都走这里保证状态收敛void Pagination::setCurrentPage(int page) { int clamped qBound(1, page, m_totalPage); if (clamped m_current) return; // 值没变不发信号 m_current clamped; rebuild(); emit currentChanged(m_current); }关键约束是值没变就 return。如果不做这个判断跳页输入框输入同一个页码也会触发 currentChanged数据层会白白多一次网络请求更严重的是如果槽函数里回调 setCurrentPage会形成信号递归。加这一道判断后组件在任何路径下都不会重复发信号这个习惯建议在所有自定义控件里保持。prev 和 next 的禁用态也在这一层维护current 是第一页时上一页禁用是最后一页时下一页禁用。别小看这个状态很多移植版忘了处理第一页还能往前点点完又跳回第一页体验很粗糙。可以在 rebuild 末尾统一刷新两个按钮的 enabled不要散落在各个 slot 里。4. 样式适配用 QSS 复刻 ElementUI 主题色的三个关键点4.1 颜色规范默认、背景、禁用三套皮肤ElementUI 分页的视觉核心是颜色区分。默认模式下页码按钮透明无边框悬停和当前页都变成主题色文字背景模式下按钮带白色底和灰色边框当前页变成主题色填充、白色文字。这三个关键状态的颜色值可以直接对齐方便后续换主题色样式项默认模式背景模式禁用态页码文字#606266#606266#C0C4CC当前页文字#409EFF#FFFFFF-悬停文字#409EFF#409EFF-按钮背景透明#FFFFFF#F5F7FA边框无#DCDFE6#DCDFE6字号统一 13px页码按钮最小宽度 24px高度控制在 28px 到 32px跟跳页输入框、每页条数下拉保持同一高度。QSS 里直接写这些值和前端组件的 css 变量一一对应。基础规则长这样QPushButton#pagerBtn { background: transparent; border: none; color: #606266; font-size: 13px; min-width: 24px; padding: 0 4px; } QPushButton#pagerBtn:hover { color: #409EFF; } QPushButton#pagerBtn[activetrue] { color: #409EFF; font-weight: 500; }当前页高亮用属性选择器 [activetrue] 实现而不是给每个按钮单独 setStyleSheet。理由很直接动态属性切换比逐个改样式省事属性选择器可以让一套样式表统一管理换主题色只动 QSS 一处代码里完全不用改。4.2 动态属性切换 active 态必须手动刷新样式QSS 有一个经典坑setProperty 改完动态属性后样式不会自动重算。第一次点击页码时 active 高亮没反应要等鼠标再动一下才生效。解决方案是每次切换属性后主动 unpolish 再 polishvoid Pagination::setActiveButton(QPushButton *btn, bool active) { btn-setProperty(active, active); btn-style()-unpolish(btn); // 清除旧样式匹配 btn-style()-polish(btn); // 重新解析属性选择器 btn-update(); // 触发重绘 }unpolish 清除按钮当前的样式匹配polish 重新走一遍样式解析update 触发重绘。这三行缺一不可只调 update 不行因为 QSS 的匹配发生在 polish 阶段重绘阶段不会重新匹配动态属性。这个行为在文档里写得含蓄实际踩到一次就能记住。4.3 背景模式与整体尺寸适配背景模式的差异在边框和填充单独用一组规则通过根节点上的属性开关控制整组样式Pagination[bgModetrue] QPushButton#pagerBtn { background: #FFFFFF; border: 1px solid #DCDFE6; border-radius: 4px; } Pagination[bgModetrue] QPushButton#pagerBtn[activetrue] { background: #409EFF; border-color: #409EFF; color: #FFFFFF; }属性选择器写在 Pagination 根节点上内部所有按钮自动继承匹配比在代码里逐个 setStyleSheet 干净。尺寸方面分页器高度建议固定不要随内容撑开跳页输入框宽度 50px 左右每页条数下拉宽度 110px 左右这些都在 applyLayout 里用固定 spacing 控制避免不同系统字体渲染差异导致布局忽宽忽窄。上一页下一页如果要用 ‹ › 这类文本符号注意不同字体下符号基线不一样最好用图标资源或者干脆用文字上一页/下一页。5. 避坑与排查五个翻车场景的现象、根因与对策下面五条是从几个实际接入项目里攒下来的血泪经验每条都按现象、原因、解决三个层次记录排查时直接对号入座。5.1 快速翻页后表格显示的是旧请求的数据现象快速连续点击第 2 页、第 3 页表格最终显示的是第 2 页的数据页码却停在 3。原因异步请求返回顺序不可控第 2 页响应比第 3 页晚到到达时把新结果覆盖了。这是分页接数据层最常见的翻车点组件本身没问题问题在调用方没做过期校验。解决给每次翻页请求发一个递增序号响应回来时序号不是最新的直接丢弃connect(pagination, Pagination::currentChanged, this, [this](int page) { const int seq m_requestSeq; // 每次请求自增 fetchPage(page, [this, seq](const QVariantList rows) { if (seq ! m_requestSeq) return; // 过期响应直接丢弃 m_model-setRows(rows); }); });m_requestSeq 是成员变量lambda 按值捕获 seq回调时比对。不等说明已有更新的请求发出这条是旧货。这个套路在 C 异步编程里通用任何连续操作只认最后一个结果的场景都能用。5.2 点击省略号没反应现象页码中间出现 ···点击毫无反馈用户以为组件坏了。原因前端 ElementUI 里省略号按钮默认可点点击向前或向后跳 5 页。很多移植版把省略号做成了纯展示的 QLabel交互能力丢掉了。解决渲染省略号时也接 clicked向左的省略号跳 current - 5向右的跳 current 5再用 qBound 夹紧。跳 5 页是产品层面的设计选择如果 pagerCount 改大了步长可以对应调整但不要做成不可点否则用户只能一页页点过去体验断崖式下跌。5.3 QSS 动态属性不刷新现象第一次点击页码高亮不移动再点一次才正常。原因setProperty 修改属性后样式表没有重新匹配按钮外观停留在旧状态。解决每次切换 active 属性后调用 style()-unpolish 和 polish再加 update()。4.2 节给了完整代码凡是 QSS 用到属性选择器的控件都会遇到不只分页器。排查这类问题先在代码里搜有没有改完属性忘了刷新样式。5.4 分页器放在滚动区域里鼠标滚轮被外层吃掉现象鼠标悬停在分页器上方滚轮滚的是外层 QScrollArea 的内容分页器没反应。原因wheelEvent 默认向上传播外层滚动区域优先处理了滚轮事件分页器没有机会响应。解决分页器重写 wheelEvent把滚轮事件接收下来做翻页再调用 accept 阻断传播。同时用 QElapsedTimer 做节流500ms 内最多翻一页防止滚一下连翻好几页。如果产品不接受滚轮翻页至少 accept 掉事件避免外层列表跟着滚造成误触。5.5 total 传 0 或 pageSize 传 0 导致界面异常现象total 传 0 时页码区空白pageSize 传 0 时程序崩溃或显示 0 页。原因调用方没做输入合法性检查组件也没兜底。total0 时向上取整得到 0 页pageSize0 时除法直接溢出。解决组件内部做防御totalPage 用 qMax(1, ...) 保证至少 1 页pageSize 在 setter 里拦截非法值回退到默认 10。组件要做可靠的输入过滤别把希望寄托在调用方守规矩上。6. 进阶用法把分页组件接进 QTableView 数据层并固定验证流程组件稳定之后真正决定好不好用的是它和数据层的配合方式。我一般把分页器、表格模型、请求回调组织成一条单向链路分页器发信号控制器发请求响应回来填模型模型刷新表格。关键约束是控制器只管当前第几页表格只管显示哪些行谁都不直接碰对方的内部状态。组装时还有一个习惯值得留把页码状态存下来页面切换回来时恢复。桌面工具经常有关闭面板再打开的场景每次打开都回到第 1 页用户翻了半小时的数据就白翻了。在关闭事件里记录 currentPage 和 pageSize重建窗口时 setCurrentPage 回去请求数量也能省一半。m_pagination new Pagination(this); connect(m_pagination, Pagination::currentChanged, this, [this](int page) { fetchPage(page, m_pagination-pageSize()); }); connect(m_pagination, Pagination::pageSizeChanged, this, [this](int size) { fetchPage(1, size); }); void DemoWindow::fetchPage(int page, int pageSize) { // 按 page/pageSize 拼参数请求回调里过滤过期序号 }注意fetchPage 里务必把 pageSize 作为参数传进去不要在回调里现取分页器对象的值避免请求发出后 pageSize 已变导致响应错位。验证方法可以固定一套准备 101 条数据pageSize 设 10总页数 11。依次把 current 设为 1、5、6、7、11打印页码序列跟前端版在线 demo 逐一比对再快速连点 10 次下一页确认最终数据和页码一致。这套用例跑完边界基本就稳了。从那以后我每次接分页组件第一件事就是把 total0、currentPage 越界、快速连点翻页这三组用例在测试里跑完再进 UI 调样式。这个习惯是从翻车记录里一条一条赔出来的希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑