ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Qt Widgets分页组件设计:仿el-pagination的页码折叠与QSS实现

Qt Widgets分页组件设计:仿el-pagination的页码折叠与QSS实现 简介一份 Qt 分页组件实现代码参考 Element UI 分页设计面向需要为桌面应用添加类似 Web 风格分页功能的 Qt/C 开发者。压缩包共 2 个文件包含 pagenavigator.h 与 pagenavigator.cpp分别对应组件类接口声明和核心逻辑实现整体仅 3KB轻量易读。已有 1080 人学习浏览。通过定义 PageNavigator 类提供当前页、总页数、每页条数等属性并封装 pageChanged、nextPage、prevPage、jumpToPage 等信号与槽可快速集成到项目同时配合 QSS 样式定制可复现 Element UI 简洁分页外观适合中高级 Qt 开发者参考设计模式或直接改造使用。 做桌面端项目列表展示类数据基本绕不开分页。我最近要在Qt程序里加一个分页条第一反应就是照搬elementui的el-pagination交互上一页、页码、下一页、跳转输入这套组合在PC浏览器里被验证了这么多年用户几乎不需要学习成本。所以我就用Qt Widgets手写了一个PaginationWidget整体交互和视觉都向elementui看齐。这篇文章把设计思路、核心算法、关键代码和踩过的坑都整理出来适合正在用Qt做信息管理类桌面应用、又不想自己拍脑袋设计分页交互的开发者参考。先说结论这个组件只依赖Qt Widgets模块没有第三方库写完一个头文件加一个源文件就能到处复用。代码量不大难点主要在页码折叠算法、按钮状态同步以及QSS细节上。下面直接拆解。1. 为什么参考elementui而不是自己拍脑袋设计1.1 elementui分页组件久经考验的设计el-pagination的设计不是随便画出来的它把分页场景拆得很清楚。第一层是导航上一页和下一页永远固定在两侧用户随手就能点。第二层是页码数字让用户可以直接跳到指定页。第三层是省略号页数多了之后只显示当前页附近的页码避免按钮铺满整个窗口。第四层是跳转输入框给高频用户一个快速定位的入口。这四个层次覆盖了绝大多数使用场景。我自己写Qt分页组件的时候一开始也想过搞个“极简版”只放上一页和下一页代码能省一半。但后来发现列表数据一多没有数字页码的定位能力会很痛苦用户只能一页一页翻体验非常差。elementui这套方案好在它已经照顾到了低频用户和高频用户的共同需求直接照着实现是最划算的选择。1.2 从Web控件到Qt Widgets交互形态的取舍从elementui迁移到Qt Widgets不是简单的翻译HTML标签有几处差异需要处理。首先是消息机制Vue里靠props传参和事件分发Qt里对应的就是属性接口加信号槽页码变化时通过currentChanged信号通知外部。其次是数据模式el-pagination本身不管数据只负责抛出页码真正加载表格数据是外部根据页码重新请求我在Qt组件里也沿用这个思路组件只发信号具体查库还是过滤内存数据全交给调用方。还有一个细节是布局稳定性。Web端的组件宽度可以跟随内容变化但在桌面端窗口里如果每次刷新数据后按钮数量忽多忽少窗口布局会跟着抖动。所以我在组件里固定了容器宽度策略宁可按钮间距均匀也不要页码变了整体宽度就跳来跳去。2. 组件整体架构与对外接口2.1 类的职责划分我把组件拆成一个对外暴露的类PaginationWidget内部结构分三层交互层上一页按钮、下一页按钮、页码按钮区域、跳转输入框逻辑层当前页、总页数、每页条数、可见页码数量以及页码折叠算法表现层QSS样式和按钮状态切换头的接口模仿el-pagination的常用属性设计class PaginationWidget : public QWidget { Q_OBJECT public: explicit PaginationWidget(QWidget *parent nullptr); void setTotal(int total); // 数据总条数 void setPageSize(int pageSize); // 每页条数 void setCurrentPage(int page); // 设置当前页会触发刷新 void setVisiblePages(int count); // 可见页码数量建议奇数 void enableJump(bool enable); // 是否显示跳转输入框 void setPrevText(const QString text); // 自定义上一页文字 void setNextText(const QString text); // 自定义下一页文字 signals: void currentChanged(int page); // 页码变化外部据此加载数据 private slots: void onPrevClicked(); void onNextClicked(); void onPageClicked(); private: void updateButtons(); void calculateRange(); QToolButton *createButton(const QString text); QHBoxLayout *m_layout; QWidget *m_pageArea; // 中间页码区域容器 QHBoxLayout *m_pageAreaLayout; // 页码区域内部布局 QToolButton *m_prevBtn; QToolButton *m_nextBtn; QSpinBox *m_jumpSpin; int m_currentPage; int m_pageCount; int m_pageSize; int m_visiblePages; int m_beginPage; int m_endPage; bool m_showLeftEllipsis; bool m_showRightEllipsis; bool m_showJump; };按钮我选用QToolButton而不是QPushButton原因是QToolButton更轻量默认没有复杂的背景绘制逻辑配合QSS定制样式时更顺手。页码按钮区域单独用一个嵌套的QWidget加QHBoxLayout承载这样刷新页码时只需要清空这个区域不会影响外层布局上的其他控件。2.2 页码窗口算法一组公式讲清楚这是整个组件最核心的部分。el-pagination在总页数很多时只显示当前页附近的一小段页码其他部分用省略号代替。比如当前在第6页总共有50页就显示“1 ... 5 6 7 ... 50”。这个算法要处理的情况不多但边界条件必须想清楚。我的实现是维护m_beginPage和m_endPage两个成员表示当前应该显示页码区间的起止位置。计算步骤如下void PaginationWidget::calculateRange() { int window m_visiblePages; if (m_pageCount window) { m_beginPage 1; m_endPage m_pageCount; m_showLeftEllipsis false; m_showRightEllipsis false; return; } // 优先让当前页居中 m_beginPage qMax(1, m_currentPage - window / 2); m_endPage m_beginPage window - 1; // 右边界溢出时整体往左平移 if (m_endPage m_pageCount) { m_endPage m_pageCount; m_beginPage qMax(1, m_endPage - window 1); } m_showLeftEllipsis (m_beginPage 1); m_showRightEllipsis (m_endPage m_pageCount); }为什么可见页码数量建议取奇数因为奇数可以保证当前页前后页数对称比如显示5个页码时当前页正好居中前后各两个。如果取偶数当前页会偏向一侧视觉上不够稳定。默认情况下我设置m_visiblePages为5数据量小的场景也够用。这里有个很容易忽略的点当总页数小于等于窗口大小时不需要任何省略号直接把所有页码全显示出来。千万别在这个分支里硬套窗口对齐逻辑否则会出现第一页被挤到中间左边还显示一个假的省略号看起来很蠢。3. 核心实现细节与代码落地3.1 按钮生成与信号连接构造函数里先建好布局骨架上一页按钮、页码区域、下一页按钮依次加入。跳转输入框用QSpinBox实现它自带数字过滤和范围限制比用QLineEdit手动校验省太多事。生成页码按钮的逻辑集中在updateButtons()里每次刷新时先清空中间区域再根据m_beginPage到m_endPage这个范围重新生成按钮。清空比较直接void PaginationWidget::updateButtons() { // 清除页码区域的所有旧控件 while (QLayoutItem *item m_pageAreaLayout-takeAt(0)) { if (QWidget *widget item-widget()) widget-deleteLater(); delete item; } calculateRange(); if (m_showLeftEllipsis) { QToolButton *ellipsis createButton(QStringLiteral(...)); ellipsis-setEnabled(false); m_pageAreaLayout-addWidget(ellipsis); } for (int page m_beginPage; page m_endPage; page) { QToolButton *btn createButton(QString::number(page)); btn-setProperty(page, page); if (page m_currentPage) { btn-setProperty(current, true); btn-setEnabled(false); } connect(btn, QToolButton::clicked, this, PaginationWidget::onPageClicked); m_pageAreaLayout-addWidget(btn); } if (m_showRightEllipsis) { QToolButton *ellipsis createButton(QStringLiteral(...)); ellipsis-setEnabled(false); m_pageAreaLayout-addWidget(ellipsis); } m_prevBtn-setEnabled(m_currentPage 1); m_nextBtn-setEnabled(m_currentPage m_pageCount); m_jumpSpin-setRange(1, m_pageCount); }点击槽里用sender()拿按钮再从属性里取页码void PaginationWidget::onPageClicked() { QToolButton *btn qobject_castQToolButton *(sender()); if (!btn) return; bool ok false; int page btn-property(page).toInt(ok); if (ok page ! m_currentPage) setCurrentPage(page); }这里有个经验给按钮绑定页码时用setProperty比在connect的lambda里捕获页码变量更规范。因为按钮可能在后续刷新中被复用属性是一次性的而lambda捕获容易在对象池场景下出现旧页码残留。3.2 页码折叠与状态刷新有同学问过为什么每次刷新都delete再重建按钮不麻烦吗确实麻烦一点但胜在逻辑简单、不容易出错。一个分页组件每次点击最多重建五六个按钮开销可以忽略不计。真要优化可以做按钮对象池回收复用但这是后话目前没必要。setCurrentPage是关键入口所有跳转最终都会走这里void PaginationWidget::setCurrentPage(int page) { if (page 1) page 1; if (page m_pageCount) page m_pageCount; if (m_currentPage page) return; m_currentPage page; updateButtons(); emit currentChanged(m_currentPage); }注意我在m_currentPage没变化时直接return避免了重复发信号。这个细节很关键因为外部可能拿到信号后又去调用setCurrentPage如果每次都发会产生循环调用。上一页和下一页的槽函数就一行void PaginationWidget::onPrevClicked() { setCurrentPage(m_currentPage - 1); } void PaginationWidget::onNextClicked() { setCurrentPage(m_currentPage 1); }到边界时按钮已经setEnabled(false)所以不可能触发越界但保险起见setCurrentPage里还是做了上下限钳制双保险。3.3 跳转输入框的处理跳转输入框用QSpinBox范围跟随总页数联动。当跳转值和当前页不一致时只需要从valueChanged信号里触发跳转void PaginationWidget::onJumpValueChanged(int value) { if (value ! m_currentPage) setCurrentPage(value); }但要注意一个坑updateButtons()里更新QSpinBox范围时会自动触发valueChanged信号如果当时m_currentPage还没更新就可能产生一次错误的跳转。解决方法是更新范围前调用blockSignals(true)更新完再恢复。我的实际代码里是这样处理的m_jumpSpin-blockSignals(true); m_jumpSpin-setRange(1, m_pageCount); m_jumpSpin-setValue(m_currentPage); m_jumpSpin-blockSignals(false);这样范围更新和值同步都不会触发多余的信号。setTotal的实现也不复杂但边界判断很关键void PaginationWidget::setTotal(int total) { if (total 0) { m_pageCount 1; } else { m_pageCount (total m_pageSize - 1) / m_pageSize; } if (m_currentPage m_pageCount) { m_currentPage m_pageCount; emit currentChanged(m_currentPage); } updateButtons(); }当外部数据删除导致总页数变小、当前页溢出时这里主动把当前页钳制到最后一页并发出信号让外部重新加载数据。这个逻辑不做的话最容易出现的问题就是界面上停留在第8页但数据只有6页整页空白。4. 用QSS把组件做成elementui的观感4.1 QSS代码与属性选择器技巧elementui的默认主题色是#409EFF文字色是#606266边框是#DCDFE6。我在组件的构造函数里内嵌一段QSS不依赖外部样式文件复制到任何项目都能直接用。关键技巧是利用属性选择器处理当前页高亮我在生成当前页按钮时设置了setProperty(current, true)QSS里直接选中这个属性PaginationWidget QToolButton { min-width: 28px; height: 28px; border: 1px solid #DCDFE6; background-color: #FFFFFF; color: #606266; border-radius: 4px; margin: 2px; padding: 0 6px; } PaginationWidget QToolButton:hover { color: #409EFF; border-color: #C6E2FF; background-color: #ECF5FF; } PaginationWidget QToolButton[currenttrue] { background-color: #409EFF; border-color: #409EFF; color: #FFFFFF; } PaginationWidget QToolButton:disabled { color: #C0C4CC; background-color: #FFFFFF; border-color: #EBEEF5; }这个做法的好处是不需要额外维护一个“当前页按钮引用”QSS自己根据属性来匹配。需要注意disabled状态的选择器要放在最后否则会被前面的样式覆盖。省略号按钮我设置了setEnabled(false)刚好被:disabled样式接管视觉上变灰看起来就是个纯分隔符。4.2 高分屏、字体与动态样式的坑高分屏下Qt界面最容易出现的问题就是控件模糊或者尺寸过小。QSS里指定了固定像素的min-width和height在2倍缩放屏幕上虽然不会模糊但视觉上会偏小建议在main函数里先开启高分屏支持QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);然后配合QSS里的通用字体设置整体观感就会正常很多。这个在Qt 5.14以上版本尤其重要老版本默认不开启高分屏缩放按钮看起来会明显偏小。还有一个字体相关的坑不同系统下Qt默认字体不一样Windows上是微软雅黑Linux发行版可能缺字体回退到宋体样式差异很大。我一般在构造函数里不强行指定字体留给应用层统一设置避免组件写死字体反而格格不入。跳转输入框我直接用QSpinBox样式可以不加保持系统原样就行。因为QSpinBox自带上下箭头按钮硬套elementui风格反而会显得突兀。如果追求视觉效果更统一可以换成自定义QLineEdit加正则过滤器但那个成本就上去了一般项目没必要。5. 常见问题与排查技巧5.1 新手最容易遇到的几个问题我把实际使用中见过的问题整理成一个速查表照着排查效率最高问题现象可能原因解决方法点击页码没反应按钮的clicked信号没连到onPageClicked或sender()拿到空指针检查connect连接用qobject_cast时先判空页面数据只有1页时组件显示一堆按钮setTotal的分支逻辑没写好pageCount被算成很大确认total和pageSize的除法逻辑做ceil处理列表删到最后一页后整页空白当前页溢出页面数量后没有钳制回合法范围setTotal里判断m_currentPage m_pageCount后重新定位下一页按钮在最后一页还能点刷新按钮状态时机不对updateButtons末尾同步设置prev和next的enabled跳转输入框数值变了但数据没刷新QSpinBox信号被blockSignals误伤检查blockSignals(true)之后是否及时恢复false多个分页组件同时存在时信号串了连接信号时用了旧写法且没指定上下文对象推荐使用新语法connect(sender, QSpinBox::valueChanged, this, Class::onJump)当前页高亮样式不生效QSS属性选择器与setProperty的类型不匹配setProperty(current, true)后记得调style()-unpolish和polish刷新样式最后一条值得展开说。Qt有个比较隐蔽的行为给控件动态设置property后已有的样式不一定马上生效。我在生成按钮时虽然先setProperty再添加到布局按道理没问题但如果按钮在后续逻辑中重复使用或者属性值被改写就必须手动刷新样式btn-style()-unpolish(btn); btn-style()-polish(btn); btn-update();这个组合拳能强制Qt重新评估QSS规则。遇到样式不更新时先用这个办法应急。5.2 快速定位问题的排查思路如果组件行为异常我一般按三个顺序排查。第一步看信号链路确认currentChanged有没有发出来、外部槽函数有没有被调用这个可以在槽函数入口打断点或者qDebug。第二步看状态同步重点检查m_pageCount和m_currentPage的值在刷新前后是否符合预期尤其是数据变更触发setTotal之后。第三步看样式层如果按钮状态正确但显示不对问题基本出在QSS选择器或者属性刷新上。我开发时习惯在PaginationWidget里加一个调试输出的开关用qDebug打印当前页、总页数、beginPage、endPage这些关键值。分页算法的数学逻辑本身不复杂但写错一个qMax参数就会导致页码跳来跳去有个打印开关能省掉大量猜测时间。最后再说一个我踩过的坑当外部数据变化时一定要先调用setTotal更新总页数再调用setCurrentPage调整当前位置顺序反了会出现一次到两次多余的currentChanged信号。我已经把这种检查统一收敛到setTotal和setCurrentPage内部外部无论如何调用都不会出错。像这种小细节一个个处理完整个组件用起来就很顺手了。本文还有配套的精品资源点击获取
返回列表