简介基于Element UI设计理念打造的Qt分页组件源码面向需要在桌面应用中实现清晰翻页交互的Qt/C开发者。资源压缩包共2个文件含pagenavigator.cpp与pagenavigator.h分别承载组件逻辑实现与接口声明整体仅3KB轻量易读、可直接嵌入项目。组件以Qt Widget为基础定义PageNavigator类并继承自QWidget封装当前页、总页数等核心属性通过nextPage、prevPage、jumpToPage等槽函数处理翻页逻辑同时对外发出pageChanged信号便于父窗口或业务模块实时响应。界面采用Qt样式表QSS模拟Element UI外观支持跳转输入框、首页/末页快捷操作并可扩展每页显示条数选择满足常见列表分页需求。已有1080人学习使用适合希望将前端分页设计迁移到Qt环境、快速获得成熟交互范式的开发者直接复用或二次改造。 以前我在做Qt客户端的时候最怕遇到那种“数据涨得飞快”的表格页面。QTableWidget一次性塞几千行滚动起来已经有点吃力再多一点界面直接没脾气。更麻烦的是产品经理只会问“翻页能不能做得好看一点”“每页条数能不能让用户自己选”“当前页能不能高亮”而我打开浏览器看着ElementUI的分页组件心里只有一个想法要是Qt里也能有一个这样的分页组件就好了。后来我真的动手做完了这件事参考ElementUI的el-pagination基于Qt Widgets实现了一个纯QWidget分页组件。没有引入任何第三方库全部使用QWidget、QHBoxLayout、QSS这些基础能力写出来可以在QTableView、QTableWidget、QListView等任何控件下方直接加。它能显示总条数、页码按钮、快速跳转框、每页条数下拉框也可以只显示最基础的上一页/下一页。这篇文章就把它拆开讲一遍从接口设计、页码算法、样式还原到实际项目里踩过的坑适合正在给Qt项目找分页方案的人参考。1. 为什么我在Qt里复刻ElementUI分页组件桌面端的翻页方案其实不少。最原始的是自己摆四个按钮首页、上一页、下一页、尾页旁边再加一个QLabel显示“第X页/共Y页”。功能上完全能用但交互体验和ElementUI一比就差了不少。ElementUI的el-pagination有几个点确实做得好值得QWidget复刻。第一是功能整合度高。一条分页条里可以同时容纳总条数、页码按钮、快速跳转、每页条数选择使用者按需开启或关闭不会逼用户到处找入口。第二是页码数量可控默认只显示7个页码按钮超出部分用省略号过渡。这个设计非常聪明哪怕总共有一万页分页条宽度也基本不会变化。第三是视觉状态明确当前页是纯色高亮hover有主题色反馈禁用按钮也有自己的一套灰化样式用户不需要思考就能知道当前位置。参考ElementUI并不是为了“抄个样子”而是要把它背后的交互设计逻辑搬到Qt里。这中间有一个很关键的思维转换Web组件的props需要翻译成Qt属性与方法Vue的事件需要翻译成Qt信号槽样式系统则需要用QSS来模拟。等这些都想明白之后实现本身就不复杂了。做之前我给自己定了三个目标API尽量贴近el-pagination了解Web前端的人拿到手能直接上手外观尽可能还原不做原创瞎设计内部结构保持简单方便以后按业务需求改。事实证明这三个目标在整个开发过程里省了非常多纠结遇到问题时照着ElementUI的行为去对齐就行。2. 接口与事件从el-pagination的props到Qt信号槽动手写代码前先把接口契约定清楚。ElementUI的el-pagination核心props有current-page、page-size、page-sizes、total、pager-count、layout、disabled。我按Qt的习惯把它们翻译成了setter方法并对外只暴露两个信号当前页变化和每页条数变化。el-pagination属性Qt侧对应设计current-pagesetCurrentPage(int page)page-sizesetPageSize(int size)page-sizessetPageSizes(QListint)totalsetTotal(int total)pager-countsetPagerCount(int count)disabledsetEnabled(bool)layoutsetShowTotal / setShowPageSize / setShowJumpercurrent-change事件currentChanged(int currentPage)size-change事件pageSizeChanged(int pageSize)头文件里的核心接口我设计成这样class Pagination : public QWidget { Q_OBJECT public: explicit Pagination(QWidget *parent nullptr); void setTotal(int total); void setPageSize(int pageSize); void setCurrentPage(int page); void setPageSizes(const QListint sizes); void setPagerCount(int count); void setShowTotal(bool show); void setShowPageSize(bool show); void setShowJumper(bool show); int total() const; int pageSize() const; int currentPage() const; int pageCount() const; signals: void currentChanged(int page); void pageSizeChanged(int size); private: void refresh(); void renderButtons(); QListint calcVisibleItems() const; };有几个设计决策值得说明一下。第一个是currentChanged信号的触发时机。我采用的是“只有用户点击页码、输入跳转、切换每页条数时才发信号外部代码调用setCurrentPage时不发信号”。为什么这样设计因为在构造函数里初始化组件时如果没有这个约定setTotal、setPageSize这些方法都会连带触发一次currentChanged外部再接到信号去查数据会出现一打开页面就连查好几次的尴尬情况。第二个是pageSize变化后的越界处理。比如当前在第5页每页20条用户把每页条数改成10条总页数变大了当前页如果还停在第5页可能已经超出新的最大页数。所以在setPageSize里必须对当前页做clamp处理void Pagination::setPageSize(int size) { if (m_pageSize size) return; m_pageSize size; const int maxPage pageCount(); if (m_currentPage maxPage) { m_currentPage maxPage; } refresh(); }第三是每页条数下拉框的信号处理。QComboBox有currentIndexChanged但直接把选中的索引映射成pageSize会分不清到底是用户主动改的还是代码初始化时误触发的。稳妥的做法是等布局全部构建完成后再连接信号或者在当前索引变化时再判断是否真的是items内容发生变化。connect(m_pageSizeBox, QOverloadint::of(QComboBox::currentIndexChanged), this, [this](int index) { int size m_pageSizes.value(index, 20); if (size m_pageSize) return; setPageSize(size); emit pageSizeChanged(m_pageSize); });3. 页码序列计算省略号是怎么生成和收放的分页组件的核心不在控件拼接而在页码序列的生成。ElementUI的精髓是“在最多pagerCount个页码按钮里同时展示首尾页码和当前页附近页码中间用省略号兜底”。这个逻辑我单独抽成了calcVisibleItems()和UI完全解耦。先明确规则。总页数小于等于pagerCount时直接把所有页码显示出来没有省略号。总页数大于pagerCount时按当前页位置分成三种情况。第一种当前页靠前只显示右侧省略号例如“1 2 3 4 5 ... 20”。第二种当前页靠后只显示左侧省略号例如“1 ... 16 17 18 19 20”。第三种当前页在中间两侧都有省略号例如“1 ... 9 10 11 ... 20”。这里我用-1表示省略号占位。这样返回的QListint里正数就是页码-1就是省略按钮渲染层拿到这个列表后逐个生成按钮即可。QListint Pagination::calcVisibleItems() const { QListint items; const int totalPage pageCount(); if (totalPage m_pagerCount) { for (int i 1; i totalPage; i) { items.append(i); } return items; } const int half (m_pagerCount - 2) / 2; const bool showLeftEllipsis (m_currentPage - 1) half 1; const bool showRightEllipsis (totalPage - m_currentPage) half 1; if (showLeftEllipsis showRightEllipsis) { items 1 -1; for (int i m_currentPage - 1; i m_currentPage 1; i) { items.append(i); } items -1 totalPage; } else if (showLeftEllipsis) { items 1 -1; for (int i totalPage - (m_pagerCount - 3); i totalPage; i) { items.append(i); } } else if (showRightEllipsis) { for (int i 1; i m_pagerCount - 2; i) { items.append(i); } items -1 totalPage; } else { for (int i 1; i totalPage; i) { items.append(i); } } return items; }这个算法的一个关键点在于无论哪种情况生成的元素数量都等于pagerCount。确保这一点非常重要因为分页条在切换页码时不希望宽度乱跳。ElementUI默认pagerCount是7我实测下来7个按钮也是观感最舒适的太少了显示不全太多了会挤压后面跳转框的位置。拿到页码序列之后就是渲染。这里有一个性能上的坑不要每次刷新都delete掉旧按钮再new一批新按钮。正确的做法是在构造函数里一次性创建pagerCount个QPushButton之后只更新文本、显隐和属性。void Pagination::renderButtons() { const QListint items calcVisibleItems(); for (int i 0; i m_pageButtons.size(); i) { QPushButton *btn m_pageButtons.at(i); if (i items.size()) { btn-hide(); continue; } const int value items.at(i); btn-show(); btn-setProperty(pageValue, value); if (value -1) { btn-setText(...); btn-setEnabled(false); btn-setProperty(ellipsis, true); btn-setProperty(active, false); } else { btn-setText(QString::number(value)); btn-setEnabled(true); btn-setProperty(ellipsis, false); btn-setProperty(active, value m_currentPage); } } }按钮的点击事件在构造函数里统一连接点击后从动态属性pageValue里读取页码。如果是正数就直接跳转如果是-1还可以做成ElementUI那种“点击省略号快进一段”的效果后面我会单独说。connect(btn, QPushButton::clicked, this, [this, btn]() { const int value btn-property(pageValue).toInt(); if (value 0 value ! m_currentPage) { jumpToPage(value); } });4. 样式还原用QSS给QWidget穿上一件ElementUI皮囊ElementUI分页条的视觉核心元素是白底、淡灰色边框、4px圆角、hover时变主题蓝、当前页深蓝底白字。这些用QSS还原基本没有难度关键在于选择器怎么写。我给所有页码按钮统一设置了pageButtontrue这个动态属性不管是上一页、下一页还是数字按钮都打上同样的标记。这样一组QSS规则就能覆盖全部按钮。Pagination QPushButton[pageButtontrue] { min-width: 32px; height: 32px; margin-right: 4px; border: 1px solid #DCDFE6; border-radius: 4px; background: #FFFFFF; color: #606266; font-size: 13px; font-family: Microsoft YaHei, PingFang SC, sans-serif; padding: 0 4px; } Pagination QPushButton[pageButtontrue]:hover { color: #409EFF; border-color: #409EFF; } Pagination QPushButton[pageButtontrue][activetrue] { background: #409EFF; border-color: #409EFF; color: #FFFFFF; } Pagination QPushButton[pageButtontrue][activetrue]:hover { background: #66B1FF; border-color: #66B1FF; } Pagination QPushButton[pageButtontrue]:disabled { color: #C0C4CC; background: #FFFFFF; border-color: #E4E7ED; }这里有个容易踩的坑必须提醒QSS对动态属性的识别不是“实时”的。你给按钮setProperty之后样式表不会自动刷新直接肉眼看到的结果可能是按钮样式没变。解决办法是对控件手动触发polishbtn-style()-unpolish(btn); btn-style()-polish(btn);每次点击页码切换active属性时都需要这样处理否则会出现当前页高亮还停在老按钮上的诡异现象。如果整个分页组件用到的样式比较多也可以对整个Pagination调用unpolish/polish但粒度太粗没必要。上一页和下一页按钮我直接用文本箭头实现。上一页显示“”下一页显示“”。这样不需要贴图也不依赖字体图标库。边界情况下当前页是第一页时上一页按钮禁用当前页是最后一页时下一页按钮禁用。QSS里的:disabled状态自动接管灰色样式。快速跳转框用QSpinBox做两侧自带的上下箭头需要隐藏否则很难看Pagination QSpinBox { border: 1px solid #DCDFE6; border-radius: 4px; min-height: 30px; min-width: 50px; background: #FFFFFF; padding: 0 6px; color: #606266; font-size: 13px; } Pagination QSpinBox::up-button, Pagination QSpinBox::down-button { width: 0; height: 0; border: none; }每页条数下拉框QComboBox也需要单独定制尤其是下拉面板默认样式和ElementUI差别很大。列表里的item加了hover高亮之后交互体验会贴近很多Pagination QComboBox { border: 1px solid #DCDFE6; border-radius: 4px; min-height: 30px; padding: 0 8px; background: #FFFFFF; color: #606266; font-size: 13px; } Pagination QComboBox::drop-down { border: none; width: 20px; } Pagination QComboBox QAbstractItemView { border: 1px solid #E4E7ED; outline: none; background: #FFFFFF; } Pagination QComboBox QAbstractItemView::item { height: 30px; padding-left: 8px; } Pagination QComboBox QAbstractItemView::item:hover { background: #F5F7FA; color: #409EFF; }到这里组件的外观基本可以达到“一眼看去就是ElementUI”的程度。剩下一些细节比如总条数Label用灰色文字字体统一为13px按钮之间的间距用margin控制而不是layout spacing都是让观感更贴近原版的小技巧。5. 接入真实项目后踩过的坑代码写完之后接入真实项目的过程中又遇到了一堆问题。有些问题不大但不处理就是很难受这里挑几个典型的记录一下。第一个坑是焦点虚线。Qt里的QPushButton点击后默认会获得焦点在很多样式下会显示一圈虚线边框非常破坏观感。ElementUI里不存在这个问题但在QWidget里必须手动处理。最省事的办法是在构造函数里给所有按钮统一设置setFocusPolicy(Qt::NoFocus);每个子控件都要设包括页码按钮、上一页下一页按钮、跳转输入框。跳转输入框如果不设NoFocus也能用但点击后同样会出现输入框焦点样式。根据实测我建议除了真正需要文字输入的QSpinBox其他控件全部设置NoFocus。第二个坑是跳转输入框的信号时机。一开始我用QSpinBox的valueChanged信号来做跳转结果发现用户在输入框里按上下键、滚动鼠标滚轮、或者输入过程中值变化时都会触发跳转完全不符合“输完再跳”的预期。ElementUI的行为是用户输入页码后按回车才跳转。我最后改成监听输入框内部QLineEdit的returnPressed信号connect(m_jumperEdit-lineEdit(), QLineEdit::returnPressed, this, [this]() { int page m_jumperEdit-value(); if (page 1 page pageCount() page ! m_currentPage) { jumpToPage(page); } m_jumperEdit-blockSignals(true); m_jumperEdit-setValue(m_currentPage); m_jumperEdit-blockSignals(false); });注意跳转成功之后把输入框的值重置回当前页码不然输入框里会保留上一次输入的数字下次打开会误以为当前页已经跳过去了。第三个坑是页码按钮的动态创建与回收。早期版本我图省事每次刷新页面都重新创建QPushButton再addWidget结果在快速翻页的时候明显感觉闪烁还伴随内存碎片。改成预创建pagerCount个按钮缓存起来、只更新文本和属性之后肉眼可见顺滑了很多。如果你要在超大数据量下高频刷新这个经验能直接省掉一次性能排查。第四个坑是省略号的处理。按照前面的算法省略号的value是-1如果直接把按钮setEnabled(false)并禁用它的文字会变成灰色。但ElementUI其实是支持点击省略号快速跳页的点击左侧省略号向前跳pagerCount-2页点击右侧省略号向后跳pagerCount-2页。如果想还原这个交互不能禁用按钮而是要在点击时单独判断if (value -1) { const int step m_pagerCount - 2; if (btn m_leftEllipsisBtn) { jumpToPage(qMax(1, m_currentPage - step)); } else if (btn m_rightEllipsisBtn) { jumpToPage(qMin(pageCount(), m_currentPage step)); } return; }这里还需要在renderButtons时记录当前渲染的是左省略还是右省略用两个专门的成员变量保存对应按钮即可。第五个坑是每页条数变化后表格顶部的对齐问题。组件本身只管发pageSizeChanged信号但实际业务中表格翻到第5页后用户把pageSize改成50这时候往往需要同时把currentPage重置回第一页。我的建议是在业务侧统一处理而不是让组件内部自动重置否则用户改完条数还想翻回去就变得很别扭。我把这些常见问题的排错顺序整理成了一个表方便后续排查现象原因解决方式点击页码高亮不动动态属性变化后样式未刷新unpolish/polish对应按钮点击按钮出现虚线环按钮获得焦点统一setFocusPolicy(Qt::NoFocus)跳转框输入值就跳页用了valueChanged信号改用lineEdit的returnPressed快速翻页闪烁每次刷新删除重建按钮预创建按钮只更新属性省略号不可点击直接setEnabled(false)保留按钮状态点击时按步长跳页接入业务代码的基本姿势是这样的auto *pagination new Pagination(this); pagination-setTotal(model-rowCount()); pagination-setPageSize(20); pagination-setPageSizes({10, 20, 50, 100}); connect(pagination, Pagination::currentChanged, this, [this](int page) { loadPageData(page); table-scrollToTop(); });所有数据查询都在currentChanged信号里进行组件内部不关心数据从哪来只负责把页码事件发出来。这和el-pagination在Vue里的角色完全一致。最后分享一个我在这个项目里收获最大的经验先把页码算法calcVisibleItems当成纯函数写好用几组边界数据测一遍再开始写界面。不要觉得分页组件简单就直接写UI像“总页数正好等于pagerCount”“当前页在临界点差一页”“总页数只有一页”这些情况靠手点界面很难完整覆盖。我是把QListint直接打到控制台里对比预期的确认无误后才放心连按钮。这样写出来的组件后面再改样式、加功能都会轻松很多。本文还有配套的精品资源点击获取
