QT C++ 之滑块插件设计与实现
在 Qt 应用程序中,QSlider 是一种常用的基础交互控件,广泛应用于数值调节、参数配置、进度控制等场景。Qt 自带的 QSlider 虽然功能完整,但在实际工程项目中,往往难以满足复杂交互、精细样式控制以及特定业务逻辑的需求。例如,在需要支持离散刻度、多滑块范围选择、自定义提示信息或特定交互反馈的场景下,默认控件的扩展能力显得有限。
基于上述问题,本文结合实际项目需求,对 Qt 中 Slider 滑块控件进行重新设计与封装,实现了一款高度可定制的 Slider 插件。该插件在保持原有滑块交互逻辑的基础上,支持自定义刻度规则、灵活的数值映射关系以及可扩展的交互提示机制,能够更好地适配工程化开发与复杂业务场景。
本文将围绕该 Slider 插件的设计思路、核心功能与关键实现点进行说明,重点介绍控件在交互行为、事件处理以及可扩展性方面的设计方法,为 Qt 开发者在自定义控件与插件化开发过程中提供参考。


-
自定义刻度步长 -
自定义刻度标签,可显示与隐藏 -
刻度原点显示和隐藏 -
当前拖动值气泡提示 -
有横向和竖向2中模式 -
有单把手和双把手模式,分别表示单个值或值范围
bubbletipwidget.h
#pragma once#include<QWidget>class BubbleTipWidget : public QWidget{Q_OBJECTpublic:explicitBubbleTipWidget(QWidget* parent = nullptr);voidsetText(const QString& text);voidshowAt(const QPoint& globalPos); // 三角尖的位置voidsetOrientation(constint& orientation); //设置方向protected:voidpaintEvent(QPaintEvent*)override;QSize sizeHint()constoverride;private:QString m_text;int m_margin = 10; // 矩形内部边距int m_triangleHeight = 8; // 三角形高度int m_radius = 5; // 圆角int m_orienration = 0; //orienration 0横向 1 竖向};
bubbletipwidget.cpp
#include"bubbletipwidget.h"#include<QPainter>#include<QFontMetrics>#include<QApplication>#include<QPainterPath>BubbleTipWidget::BubbleTipWidget(QWidget* parent): QWidget(parent, Qt::ToolTip | Qt::FramelessWindowHint){setAttribute(Qt::WA_TransparentForMouseEvents);setAttribute(Qt::WA_ShowWithoutActivating);setAttribute(Qt::WA_TranslucentBackground);}voidBubbleTipWidget::setText(const QString& text){m_text = text;updateGeometry();update();}voidBubbleTipWidget::setOrientation(constint & orientation){m_orienration = orientation;update();}QSize BubbleTipWidget::sizeHint()const{QFontMetrics fm(font());int textW = fm.horizontalAdvance(m_text);int textH = fm.height();int w, h;if (m_orienration == 0) {w = textW + m_margin * 2;h = textH + m_margin * 2 + m_triangleHeight;}else {w = textW + m_margin * 2 + m_triangleHeight;h = textH + m_margin * 2;}//横向和竖向切换return QSize(w, h);}voidBubbleTipWidget::showAt(const QPoint& globalPos){// globalPos = 三角尖位置QSize sz = sizeHint();// 让三角形底边中心对准 globalPosint x, y;if (m_orienration == 0) {//横向x = globalPos.x() - sz.width() / 2;y = globalPos.y() - sz.height();}else {//竖向x = globalPos.x();y = globalPos.y() - sz.height()/2;}move(x, y);resize(sz);show();}voidBubbleTipWidget::paintEvent(QPaintEvent*){QPainter p(this);p.setRenderHint(QPainter::Antialiasing);QPainterPath path;QRect rectArea;QPolygon triangle;if (m_orienration == 0) { // 横向显示// rect 区域rectArea = QRect(0, 0, width(), height() - m_triangleHeight);// 圆角矩形path.addRoundedRect(rectArea, m_radius, m_radius);// 三角形QPoint p1(width() / 2 - m_triangleHeight, rectArea.bottom() + 1);QPoint p2(width() / 2 + m_triangleHeight, rectArea.bottom() + 1);QPoint p3(width() / 2, height());triangle << p1 << p2 << p3;}else if (m_orienration == 1) { // 竖向显示// rect 区域left top width heightrectArea = QRect(m_triangleHeight, 0, width() - m_triangleHeight, height());// 圆角矩形path.addRoundedRect(rectArea, m_radius, m_radius);// 三角形QPoint p1(0, height() / 2);QPoint p2(m_triangleHeight, height() / 2 - m_triangleHeight);QPoint p3(m_triangleHeight, height() / 2 + m_triangleHeight);triangle << p1 << p2 << p3;}// 添加三角形到路径path.addPolygon(triangle);// 填充背景p.fillPath(path, QColor(50, 50, 50, 220));// 画文字p.setPen(Qt::white);p.drawText(rectArea, Qt::AlignCenter, m_text);}
插件源码如下:
discreterangeslider.h
#pragma once#include<QWidget>#include<QVector>#include<QString>#include<bubbletipwidget.h>/*DiscreteRangeSlider- 支持单滑块(Single)与范围滑块(Range,双把手)- 支持离散模式(根据 step)和连续模式- 支持横向和纵向- 支持刻度点及刻度标签显示- 拖动时显示tooltip*/class DiscreteRangeSlider : public QWidget{Q_OBJECTpublic:explicitDiscreteRangeSlider(QWidget* parent = nullptr);//样式enum Orientation { Horizontal, Vertical };//模式enum Mode { Single, RangeMode };// 属性接口voidsetOrientation(Orientation o);Orientation orientation()const{ return m_orientation; }voidsetRange(int minV, int maxV);intminimum()const{ return m_min; }intmaximum()const{ return m_max; }// 连续或离散voidsetStep(double step); // step <=0 表示连续(无离散)doublestep()const{ return m_step; }// 模式:单把手 or 双把手voidsetMode(Mode m);// 单值或区间设置/读取voidsetValue(double v);doublevalue()const{ return m_value; }voidsetRangeValues(int a, int b);voidrangeValues(int& a, int& b)const{ a = m_lower; b = m_upper; }// UI 选项voidsetShowTicks(bool show); //toptip 刻度值提示voidsetShowTickLabels(bool show);//底部刻度值标签voidsetTickLabels(const QMap<double, QString>& labels); // 可自定义刻度文字voidsetShowFill(bool show); // 填充轨迹区间颜色//轨道背景色voidsetGrooveBgColor(const QColor& c){ grooveBg = c; update();}//轨道填充色voidsetGrooveFillColor(const QColor& c){ grooveFill = c; update();}//轨道高度voidsetGrooveHeight(int h){ m_grooveThickness = h; update();}//handle原点的半径voidsetHandleRadius(int r){ m_handleRadius = r; update();}//刻度半径,默认是轨道高度的1/2voidsetTickRadius(int r){ m_tickRadius = r; update();}//刻度mark的颜色voidsetTickLabels(const QColor& c){ tickLabelColor = c; update();}//显示tooptipvoidsetShowTooptip(bool above){ m_showTooptip = above; update();}//支持的格式化voidsetFormat(int format){ m_format = format; update(); }intcountDecimalPlaces(double step){QString str = QString::number(step, 'f', 15);int dotIndex = str.indexOf('.');if (dotIndex == -1) {return 0; // 没有小数点,是整数}// 获取小数部分QString decimalPart = str.mid(dotIndex + 1);// 去除末尾的0while (decimalPart.endsWith('0')) {decimalPart.chop(1);}return decimalPart.length();}//保留小数位数,0不保留 , 1 一位 2 二位doubleformatValue(double value){int precision = qBound(0, m_format, 15);return qRound(value * pow(10, precision)) / pow(10, precision);//return qFloor(value * pow(10, precision)) / pow(10, precision);}//获取当前m_value 单把手doublegetCurrentValue(){return m_value;}//获取当前值m_lower, m_upper 2个值 并返回std::pair<double, double> getRangeValues()const{return { m_lower, m_upper };}signals:voidvalueChanged(double v);voidrangeChanged(double a, double b);protected:// 重写绘制与鼠标事件voidpaintEvent(QPaintEvent* ev)override;voidresizeEvent(QResizeEvent* ev)override;//处理voidmousePressEvent(QMouseEvent* ev)override;//移动voidmouseMoveEvent(QMouseEvent* ev)override;//释放voidmouseReleaseEvent(QMouseEvent* ev)override;// 鼠标进入voidenterEvent(QEnterEvent* event)override;//鼠标离开voidleaveEvent(QEvent* ev)override;private:// 帮助函数QRect grooveRect()const; //轨道矩形QRect handleRectForPos(int posIndex)const; //获取指定索引的滑块矩形QPoint valueToPoint(double v)const; //值转坐标doublepointToValue(const QPoint& p); //坐标转值doublesnapToStep(double v); //值 snap 到 stepvoidshowTipAtPoint(const QPoint& p, double value); //显示 tooltipvoidhideTip(); //隐藏 tooltip// 成员变量(核心)Orientation m_orientation;Mode m_mode;int m_min; //最大值int m_max; //最小值double m_step; // <=0 为连续double m_value; // 单模式当前值double m_lower, m_upper; // range 模式的两个值(保证 lower <= upper)bool m_showTicks; //显示刻度点bool m_showTickLabels; //显示刻度值标签//QVector<QString> m_tickLabels; //自定义标签QMap<double, QString> m_tickLabelMap;bool m_showTooptip = false; //显示tooltipbool m_showFill; // 填充轨迹区间是否填充// 外观参数(可调整)int m_grooveThickness; //轨道宽度int m_handleRadius; //滑块半径int m_tickRadius; //刻度值标签半径int m_margin; //外边距// 拖拽状态bool m_dragging; //是否拖动enum DragHandle { NoHandle, HandleLower, HandleUpper, HandleSingle } m_dragHandle;QPoint m_lastMousePos; //最后鼠标位置QColor grooveBg = QColor(200, 200, 200); //轨道背景颜色QColor grooveFill = QColor(50, 150, 250); //轨道填充颜色QColor tickLabelColor = QColor(0, 0, 0); //刻度mark颜色//支持格式int m_format = 0; //0:整数 ,1:一位小数 2:二位小数BubbleTipWidget* m_tip = nullptr;// 内部 tooltip 文本缓存(采用 QToolTip 显示)//tiphide 标识bool m_mouseInside = false; // 鼠标是否在控件内//鼠标是否移动到handels 点上 单把手bool m_is_selected = false;//双把手bool m_isLowerHandleHovered = false;bool m_isUpperHandleHovered = false;};
discreterangeslider.cpp
#include"discreterangeslider.h"#include<QPainter>#include<QMouseEvent>#include<QToolTip>#include<QStyleOption>#include<QDebug>DiscreteRangeSlider::DiscreteRangeSlider(QWidget* parent): QWidget(parent),m_orientation(Horizontal),m_mode(Single),m_min(0),m_max(100),m_step(0),m_value(0),m_lower(0),m_upper(100),m_showTicks(true),m_showTickLabels(true),m_showFill(true),m_grooveThickness(8),m_handleRadius(8),m_tickRadius(3),m_margin(12),m_dragging(false),m_mouseInside(false),m_showTooptip(true),m_dragHandle(NoHandle){setMinimumSize(100, 60);m_tickRadius = m_grooveThickness/2.0;//开启 mouseTracking = true: 鼠标只要在控件上移动,就会触发 mouseMoveEvent。//关闭 mouseTracking = false(默认): 必须按住鼠标左键拖动,才会触发 mouseMoveEvent。setMouseTracking(true);// 默认刻度标签为均分的数字(如果用户没有设置自定义)m_tickLabelMap.clear();//气泡m_tip = new BubbleTipWidget(this);}/*---------------- 属性设置 ----------------*///设置方向voidDiscreteRangeSlider::setOrientation(Orientation o){if (m_orientation == o) return;m_orientation = o;m_tip->setOrientation(m_orientation == Orientation::Horizontal ? 0 : 1);update();}//设置范围voidDiscreteRangeSlider::setRange(int minV, int maxV){if (minV >= maxV) return;m_min = minV;m_max = maxV;if (m_value < m_min) m_value = m_min;if (m_value > m_max) m_value = m_max;if (m_lower < m_min) m_lower = m_min;if (m_upper > m_max) m_upper = m_max;update();}//设置步长voidDiscreteRangeSlider::setStep(double step){// step <= 0 表示连续if (step <= 0) m_step = 0;else m_step = step;m_format = countDecimalPlaces(m_step);update();}//设置模式 单把手 还是双把手voidDiscreteRangeSlider::setMode(Mode m){if (m_mode == m) return;m_mode = m;update();}//设置值voidDiscreteRangeSlider::setValue(double v){v = qBound(m_min*1.0, v, m_max * 1.0);if (m_step > 0) v = snapToStep(v);if (m_value == v) return;m_value = v;update();emit valueChanged(m_value);}//设置范围值voidDiscreteRangeSlider::setRangeValues(int a, int b){if (a > b) qSwap(a, b);a = qBound(m_min, a, m_max);b = qBound(m_min, b, m_max);if (m_step > 0) { a = snapToStep(a); b = snapToStep(b); }if (m_lower == a && m_upper == b) return;m_lower = a; m_upper = b;update();emit rangeChanged(m_lower, m_upper);}//显示toptipvoidDiscreteRangeSlider::setShowTicks(bool show){ m_showTicks = show; update(); }//显示刻度voidDiscreteRangeSlider::setShowTickLabels(bool show){ m_showTickLabels = show; update(); }//刻度标签voidDiscreteRangeSlider::setTickLabels(const QMap<double,QString>& labels){m_tickLabelMap = labels;update();}//是否填充voidDiscreteRangeSlider::setShowFill(bool show){ m_showFill = show; update(); }/*---------------- 绘制相关辅助 ----------------*/// 轨道矩形:基于 orientation, margin, thicknessQRect DiscreteRangeSlider::grooveRect()const{if (m_orientation == Horizontal) {int x = m_margin;int w = width() - 2 * m_margin;int y = (height() - m_grooveThickness) / 2;return QRect(x, y, w, m_grooveThickness);}else {int y = m_margin;int h = height() - 2 * m_margin;int x = (width() - m_grooveThickness) / 2;return QRect(x, y, m_grooveThickness, h);}}// 根据 value 返回屏幕坐标点QPoint DiscreteRangeSlider::valueToPoint(double v)const{QRect g = grooveRect();double ratio = 0.0;if (m_max == m_min) ratio = 0.0;else ratio = double(v - m_min) / double(m_max - m_min);if (m_orientation == Horizontal) {double px = g.left() + int(ratio * double(g.width()));double py = g.center().y()+1;return QPoint(px, py);}else {// vertical: 上 -> m_min, 下 -> m_max; 我们希望 m_min 在上方double py = g.top() + int((1.0 - ratio) * double(g.height()));double px = g.center().x()+1;return QPoint(px, py);}}// 将鼠标点映射为 value(并把它限制在区间)doubleDiscreteRangeSlider::pointToValue(const QPoint& p){QRect g = grooveRect();double ratio = 0.0;if (m_orientation == Horizontal) {double dx = p.x() - g.left();ratio = double(dx) / double(g.width());}else {int dy = p.y() - g.top();ratio = 1.0 - double(dy) / double(g.height()); // top->max? we invert so top->max? we keep min at top}ratio = qBound(0.0, ratio, 1.0);double v = m_min + ratio * (m_max - m_min);qDebug() << "ratio1:" << ratio << "v:" << v;if (m_step > 0) v = snapToStep(v);qDebug() << "ratio2:" << ratio << "v:" << v << "===" << m_min << "==" << m_max;return qBound(m_min*1.0, v, m_max * 1.0);}// 将值 snap 到 step 网格doubleDiscreteRangeSlider::snapToStep(double v){if (m_step <= 0) return v;// 以最小值为基准划分步长double offset = v - m_min;int n = (offset + m_step / 2.0) / m_step; // 四舍五入到最近一步 (offset + m_step / 2.0) / m_stepdouble nv = m_min + n * m_step;//step 是多少位 nv 就保留几位小数nv = formatValue(nv);return qBound(m_min*1.0, nv, m_max*1.0);}// 根据 handle 的位置画 handle 用于点击/拖拽判定(返回 handle 的 bounding rect)QRect DiscreteRangeSlider::handleRectForPos(int posIndex)const{// 实际绘制时我们直接用 valueToPoint,handleRect 基本是以 handleRadius 为半径的正方形Q_UNUSED(posIndex);// 仅用于判定用,实际使用 valueToPoint() 获取中心点return QRect(0, 0, m_handleRadius * 2, m_handleRadius * 2);}/*---------------- 绘制 ----------------*/voidDiscreteRangeSlider::paintEvent(QPaintEvent* /*ev*/){QPainter p(this);p.setRenderHint(QPainter::Antialiasing, true);// 绘制背景(支持样式)QStyleOption opt;opt.initFrom(this);style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);// 轨道QRect g = grooveRect();// 绘制底轨p.setPen(Qt::NoPen);p.setBrush(grooveBg);p.drawRoundedRect(g, m_grooveThickness / 2.0, m_grooveThickness / 2.0);// 计算当前值对应的点QPoint ptA, ptB;if (m_mode == Single) {ptA = valueToPoint(m_value); //值转为坐标点}else { // RangeptA = valueToPoint(m_lower);ptB = valueToPoint(m_upper);}// 绘制填充(选中区间或从最左到 handle)if (m_showFill) {if (m_mode == Single) {// single: 从左端到 value 进行填充if (m_orientation == Horizontal) {QRect fillRect(g.left(), g.top(), ptA.x() - g.left(), g.height());p.setBrush(grooveFill);p.drawRoundedRect(fillRect, m_grooveThickness / 2.0, m_grooveThickness / 2.0);}else {// vertical: 从底部到 valueint bottom = g.bottom();int top = ptA.y();QRect fillRect(g.left(), top, g.width(), bottom - top);p.setBrush(grooveFill);p.drawRoundedRect(fillRect, 5, 5);}}else {// range: 填充 lower~upperif (m_orientation == Horizontal) {int left = qMin(ptA.x(), ptB.x());int right = qMax(ptA.x(), ptB.x());QRect fillRect(left, g.top(), right - left, g.height());p.setBrush(grooveFill);p.drawRoundedRect(fillRect, m_grooveThickness / 2.0, m_grooveThickness / 2.0);}else {int top = qMin(ptA.y(), ptB.y());int bottom = qMax(ptA.y(), ptB.y());QRect fillRect(g.left(), top, g.width(), bottom - top);p.setBrush(grooveFill);p.drawRoundedRect(fillRect, 5, 5);}}}// 绘制刻度点(如果设置 step 或者 showTicks)int tickCount = 0;if (m_step > 0) {tickCount = (m_max - m_min)*1.0 / m_step;}else {// 若没有 step,仍然显示若干默认刻度tickCount = 10;}// 计算刻度点 当存在的话if (m_showTicks) {int actualTicks = tickCount;for (int i = 0; i <= actualTicks; ++i) {// 计算值和位置double ratio = double(i) / double(actualTicks);double v;if (m_step > 0) v = m_min + i * m_step;else v = m_min + ratio * (m_max - m_min);QPoint pt = valueToPoint(v);//标签为空或者包含labelif (m_tickLabelMap.isEmpty() || m_tickLabelMap.contains(v) ) {// 小圆点p.setBrush(Qt::white);//填充色p.setPen(QPen(Qt::gray, 1));p.drawEllipse(pt, m_tickRadius, m_tickRadius);}// 可选的刻度标签if (m_showTickLabels) {QString txt;// 只在 key 存在时显示标签if (!m_tickLabelMap.isEmpty()) {// 查找是否有对应的标签auto it = m_tickLabelMap.find(v);if (it != m_tickLabelMap.end()) {txt = it.value();}else {continue; // 没有对应标签则跳过}}else {txt = QString::number(v);}//根据字体大小计算文本的宽度QFontMetrics fm(font());if (m_orientation == Horizontal) {int tx = pt.x() - fm.horizontalAdvance(txt) / 2;int ty = g.bottom() + 6 + fm.ascent();p.setPen(Qt::black);p.drawText(QPoint(tx, ty), txt);}else {// 纵向:绘制在右侧int tx = g.right() + 6;int ty = pt.y() + fm.ascent() / 2;p.setPen(tickLabelColor);p.drawText(QPoint(tx, ty), txt);}}}}// 绘制 handlesp.setPen(QPen(grooveFill, 1));p.setBrush(Qt::white);// handle style 可扩展,这里简单做圆形并画边框if (m_mode == Single) {int _handleRadius = m_is_selected ? m_handleRadius + 1 : m_handleRadius;p.drawEllipse(ptA, _handleRadius, _handleRadius);}else {int _handleRadiusL = m_isLowerHandleHovered ? m_handleRadius + 1 : m_handleRadius;int _handleRadiusU = m_isUpperHandleHovered ? m_handleRadius + 1 : m_handleRadius;p.drawEllipse(ptA, _handleRadiusL, _handleRadiusL);p.drawEllipse(ptB, _handleRadiusU, _handleRadiusU);}}/*---------------- 鼠标事件(拖拽与点击) ----------------*////鼠标按下voidDiscreteRangeSlider::mousePressEvent(QMouseEvent* ev){if (ev->button() != Qt::LeftButton) return;m_dragging = true;m_lastMousePos = ev->pos();m_dragHandle = NoHandle;if (m_mode == Single) { //单把手// single 模式:若点击在 handle 附近,则拖拽该 handle;否则直接跳到点击位置//当前点转为值double v = pointToValue(m_lastMousePos);qDebug() << "pointToValue:" << v;QPoint hv = valueToPoint(m_value);int dx = ev->pos().x() - hv.x();int dy = ev->pos().y() - hv.y();if ( dx * dx + dy * dy <= (m_handleRadius + 6) * (m_handleRadius + 6) ) {m_dragHandle = HandleSingle;qDebug() << "pointToValue2:" << v;}else {// 直接设置值并触发变化//int v = pointToValue(ev->pos());setValue(v);showTipAtPoint(valueToPoint(v), v);qDebug() << "pointToValue3:" << v;}}else { //双把手// range 模式:判断哪个 handle 更接近QPoint pL = valueToPoint(m_lower);QPoint pU = valueToPoint(m_upper);int dL = (ev->pos() - pL).manhattanLength();int dU = (ev->pos() - pU).manhattanLength();// 设定阈值,以便当点击某一把手附近时选择它int threshold = m_handleRadius + 8;if (dL <= threshold && dL <= dU) {m_dragHandle = HandleLower;}else if (dU <= threshold && dU < dL) {m_dragHandle = HandleUpper;}else {// 如果不靠近把手,则选择最近的一个并跳到该点if (dL <= dU) {double v = pointToValue(ev->pos());m_lower = qBound(m_min * 1.0, v, m_upper); // 不超出 upperif (m_step > 0) m_lower = snapToStep(m_lower);emit rangeChanged(m_lower, m_upper);update();showTipAtPoint(valueToPoint(m_lower), m_lower);}else {double v = pointToValue(ev->pos());m_upper = qBound(m_lower, v, m_max*1.0); // 不低于 lowerif (m_step > 0) m_upper = snapToStep(m_upper);emit rangeChanged(m_lower, m_upper);update();showTipAtPoint(valueToPoint(m_upper), m_upper);}}}}//鼠标移动voidDiscreteRangeSlider::mouseMoveEvent(QMouseEvent* ev){double v = pointToValue(ev->pos());//qDebug() << "mouseMoveEvent" << v << "==" << m_value << "===" ;if (!m_dragging) {//非拖动下if (m_mode == Single) {QPointF handlePos = valueToPoint(m_value);double dist = QLineF(ev->pos(), handlePos).length();// 判断是否悬停在 handle 上bool hovered = dist <= m_handleRadius;if (hovered != m_is_selected) {m_is_selected = hovered;update();}//这里计算在一定的范围内,才显示提示if (hovered)showTipAtPoint(valueToPoint(m_value), m_value);}else {QPointF lowerPos = valueToPoint(m_lower);QPointF upperPos = valueToPoint(m_upper);double distL = QLineF(ev->pos(), lowerPos).length();double distU = QLineF(ev->pos(), upperPos).length();bool hoveredL = distL <= m_handleRadius;bool hoveredU = distU <= m_handleRadius;if (hoveredL != m_isLowerHandleHovered || hoveredU != m_isUpperHandleHovered) {m_isLowerHandleHovered = hoveredL;m_isUpperHandleHovered = hoveredU;update();}if (hoveredL)showTipAtPoint(valueToPoint(m_lower), m_lower);else if (hoveredU)showTipAtPoint(valueToPoint(m_upper), m_upper);}return;}// 计算当前点对应的 value,并根据拖拽把手类型执行动作if (m_mode == Single) {if (m_dragHandle == HandleSingle) {setValue(v);showTipAtPoint(valueToPoint(m_value), m_value);}}else {if (m_dragHandle == HandleLower) {// lower <= upperdouble newLower = qBound(m_min*1.0, v, m_upper);if (m_step > 0) newLower = snapToStep(newLower);if (newLower != m_lower) {m_lower = newLower;emit rangeChanged(m_lower, m_upper);showTipAtPoint(valueToPoint(m_lower), m_lower);update();}else {showTipAtPoint(valueToPoint(m_lower), m_lower);}}else if (m_dragHandle == HandleUpper) {double newUpper = qBound(m_lower, v, m_max*1.0);if (m_step > 0) newUpper = snapToStep(newUpper);if (newUpper != m_upper) {m_upper = newUpper;emit rangeChanged(m_lower, m_upper);showTipAtPoint(valueToPoint(m_upper), m_upper);update();}else {showTipAtPoint(valueToPoint(m_upper), m_upper);}}else {// 未指定把手:不做任何事(move 可能来自 click 跳转)}}m_lastMousePos = ev->pos();}//鼠标释放voidDiscreteRangeSlider::mouseReleaseEvent(QMouseEvent* ev){Q_UNUSED(ev);m_dragging = false;m_dragHandle = NoHandle;if (!m_dragging && !m_mouseInside) {hideTip();}}//鼠标移入voidDiscreteRangeSlider::enterEvent(QEnterEvent* ev){Q_UNUSED(ev);m_mouseInside = true;}//鼠标离开voidDiscreteRangeSlider::leaveEvent(QEvent* ev){Q_UNUSED(ev);m_mouseInside = false;if (!m_dragging && !m_mouseInside) {hideTip();}}/*---------------- tooltip 控制 ----------------*/voidDiscreteRangeSlider::showTipAtPoint(const QPoint& p, double value){if (!m_showTooptip) return;if (!m_tip) return;QString txt = QString::number(formatValue(value));m_tip->setText(txt);QPoint global = mapToGlobal(p);// 让三角形尖端在滑块中心下面一点if (m_orientation == Horizontal) {global.setY(global.y() - m_handleRadius - 2);}else {// 纵向:三角形在右侧global.setX(global.x() + m_handleRadius + 2);}m_tip->showAt(global);}voidDiscreteRangeSlider::hideTip(){//QToolTip::hideText();if (!m_showTooptip) return;if (m_tip->isVisible()) m_tip->hide();}/*---------------- resizeEvent(预计算可放在这里,如需扩展) ----------------*/voidDiscreteRangeSlider::resizeEvent(QResizeEvent* ev){Q_UNUSED(ev);update();}
demo的使用源码
sliderwidgetpage.h和 sliderwidgetpage.cpp
#pragma once#include<QWidget>#include"ui_sliderwidgetpage.h"namespace Ui {class SliderWidgetPageClass;}class SliderWidgetPage : public QWidget{Q_OBJECTpublic:SliderWidgetPage(QWidget *parent = nullptr);~SliderWidgetPage();private:Ui::SliderWidgetPageClass* ui;};
#include "sliderwidgetpage.h"SliderWidgetPage::SliderWidgetPage(QWidget *parent): QWidget(parent),ui(new Ui::SliderWidgetPageClass){ui->setupUi(this);ui->widget1->setOrientation(DiscreteRangeSlider::Horizontal);ui->widget1->setMode(DiscreteRangeSlider::Single);ui->widget1->setRange(0, 1);ui->widget1->setStep(0.1); // 连续 间隔ui->widget1->setShowTicks(true);ui->widget1->setShowTickLabels(true);ui->widget1->setValue(30);//valueChanged 信号槽connect(ui->widget1, &DiscreteRangeSlider::valueChanged, [this](double currentValue) mutable {qDebug() << "currentValue:" << currentValue;});ui->widget2->setOrientation(DiscreteRangeSlider::Horizontal);ui->widget2->setMode(DiscreteRangeSlider::Single);ui->widget2->setRange(0, 50);ui->widget2->setStep(10); // 连续 间隔ui->widget2->setShowTicks(true);ui->widget2->setShowTickLabels(true);ui->widget2->setValue(30);//valueChanged 信号槽connect(ui->widget2, &DiscreteRangeSlider::valueChanged, [this](double currentValue) mutable {qDebug() << "currentValue:" << currentValue;});ui->widget2->setTickLabels(QMap<double, QString>{{10, "10米"},{40, "40米" },{50, "50米" }});//没有刻度的sliderui->widget3->setOrientation(DiscreteRangeSlider::Horizontal);ui->widget3->setMode(DiscreteRangeSlider::Single);ui->widget3->setRange(0, 100);ui->widget3->setShowTicks(false);ui->widget3->setShowTickLabels(false);ui->widget3->setValue(30);//valueChanged 信号槽connect(ui->widget3, &DiscreteRangeSlider::valueChanged, [this](double currentValue) mutable {qDebug() << "widget3 currentValue:" << currentValue;});ui->widget4->setOrientation(DiscreteRangeSlider::Horizontal);ui->widget4->setMode(DiscreteRangeSlider::RangeMode);ui->widget4->setRange(0, 100);ui->widget4->setStep(10);ui->widget4->setShowTicks(true);ui->widget4->setShowTickLabels(true);ui->widget4->setValue(30);//valueChanged 信号槽connect(ui->widget4, &DiscreteRangeSlider::rangeChanged, [this](double minValue,double maxValue) mutable {qDebug() << "widget4 minValue:" << minValue << ";maxValue:" << maxValue;});ui->widget5->setOrientation(DiscreteRangeSlider::Horizontal);ui->widget5->setMode(DiscreteRangeSlider::RangeMode);ui->widget5->setRange(30, 40);ui->widget5->setStep(0.5);ui->widget5->setShowTicks(true);ui->widget5->setShowTickLabels(true);ui->widget5->setValue(30);//valueChanged 信号槽connect(ui->widget5, &DiscreteRangeSlider::rangeChanged, [this](double minValue, double maxValue) mutable {qDebug() << "widget5 minValue:" << minValue << ";maxValue:" << maxValue;});ui->widget5->setTickLabels(QMap<double, QString>{{30.5, "30.5℃"},{37.5, "37.5℃"},{ 40, "40℃" }});//垂直1ui->widget6->setOrientation(DiscreteRangeSlider::Vertical);ui->widget6->setMode(DiscreteRangeSlider::RangeMode);ui->widget6->setRange(30, 40);ui->widget6->setStep(0.5);ui->widget6->setShowTicks(true);ui->widget6->setShowTickLabels(true);ui->widget6->setValue(30);//valueChanged 信号槽connect(ui->widget6, &DiscreteRangeSlider::rangeChanged, [this](double minValue, double maxValue) mutable {qDebug() << "widget6 minValue:" << minValue << ";maxValue:" << maxValue;});ui->widget6->setTickLabels(QMap<double, QString>{{37.5, "37.5℃"},{ 40, "40℃" }});//垂直2ui->widget7->setOrientation(DiscreteRangeSlider::Vertical);ui->widget7->setMode(DiscreteRangeSlider::Single);ui->widget7->setRange(0, 100);ui->widget7->setStep(10);ui->widget7->setShowTicks(false);ui->widget7->setShowTickLabels(false);ui->widget7->setValue(30);//valueChanged 信号槽connect(ui->widget7, &DiscreteRangeSlider::rangeChanged, [this](double minValue, double maxValue) mutable {qDebug() << "widget7 minValue:" << minValue << ";maxValue:" << maxValue;});//垂直3ui->widget8->setOrientation(DiscreteRangeSlider::Vertical);ui->widget8->setMode(DiscreteRangeSlider::Single);ui->widget8->setRange(0, 10);ui->widget8->setStep(1);ui->widget8->setShowTicks(true);ui->widget8->setShowTickLabels(true);ui->widget8->setValue(2);//valueChanged 信号槽connect(ui->widget8, &DiscreteRangeSlider::valueChanged, [this]( double current) mutable {qDebug() << "widget8 current:" << current ;});}SliderWidgetPage::~SliderWidgetPage(){}
ui文件sliderwidgetpage.ui
<?xml version="1.0" encoding="UTF-8"?><uiversion="4.0"><class>SliderWidgetPageClass</class><widgetclass="QWidget"name="SliderWidgetPageClass"><propertyname="geometry"><rect><x>0</x><y>0</y><width>430</width><height>412</height></rect></property><propertyname="windowTitle"><string>SliderWidgetPage</string></property><layoutclass="QVBoxLayout"name="verticalLayout"><item><widgetclass="QScrollArea"name="scrollArea"><propertyname="widgetResizable"><bool>true</bool></property><widgetclass="QWidget"name="scrollAreaWidgetContents"><propertyname="geometry"><rect><x>0</x><y>0</y><width>410</width><height>322</height></rect></property><propertyname="sizePolicy"><sizepolicyhsizetype="Expanding"vsizetype="Fixed"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property><layoutclass="QVBoxLayout"name="verticalLayout_2"><item><widgetclass="QLabel"name="label"><propertyname="text"><string>基础用法:</string></property></widget></item><item><widgetclass="DiscreteRangeSlider"name="widget1"native="true"/></item><item><widgetclass="DiscreteRangeSlider"name="widget2"native="true"/></item><item><widgetclass="DiscreteRangeSlider"name="widget3"native="true"/></item><item><widgetclass="DiscreteRangeSlider"name="widget4"native="true"/></item><item><widgetclass="DiscreteRangeSlider"name="widget5"native="true"/></item><item><layoutclass="QHBoxLayout"name="horizontalLayout"><item><widgetclass="DiscreteRangeSlider"name="widget7"native="true"><propertyname="sizePolicy"><sizepolicyhsizetype="Fixed"vsizetype="Preferred"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property><propertyname="minimumSize"><size><width>100</width><height>200</height></size></property><propertyname="styleSheet"><stringnotr="true"/></property></widget></item><item><widgetclass="DiscreteRangeSlider"name="widget6"native="true"><propertyname="sizePolicy"><sizepolicyhsizetype="Fixed"vsizetype="Preferred"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property><propertyname="minimumSize"><size><width>100</width><height>200</height></size></property><propertyname="styleSheet"><stringnotr="true"/></property></widget></item><item><widgetclass="DiscreteRangeSlider"name="widget8"native="true"><propertyname="sizePolicy"><sizepolicyhsizetype="Fixed"vsizetype="Preferred"><horstretch>0</horstretch><verstretch>0</verstretch></sizepolicy></property><propertyname="minimumSize"><size><width>100</width><height>200</height></size></property></widget></item><item><spacername="horizontalSpacer_2"><propertyname="orientation"><enum>Qt::Orientation::Horizontal</enum></property><propertyname="sizeHint"stdset="0"><size><width>40</width><height>20</height></size></property></spacer></item></layout></item></layout></widget></widget></item></layout></widget><layoutdefaultspacing="6"margin="11"/><customwidgets><customwidget><class>DiscreteRangeSlider</class><extends>QWidget</extends><headerlocation="global">discreterangeslider.h</header><container>1</container></customwidget></customwidgets><resources/><connections/></ui>
通过对 Qt Slider 滑块控件的重新设计与封装,本文实现了一款具有良好扩展性与工程实用价值的自定义插件。相较于 Qt 默认控件,该插件在交互表现、数值控制以及样式定制方面具备更高的灵活性,能够有效满足复杂应用场景下对精细交互与可维护性的要求。
在实际工程中,自定义控件不仅是界面美观性的体现,更是系统可用性与用户体验的重要组成部分。合理地抽象交互逻辑、清晰地区分显示与数据模型、避免控件与业务逻辑过度耦合,是保证控件长期可维护性的关键。
本文所述 Slider 插件的设计思路与实现方式,可作为 Qt 自定义控件开发的一种参考范式。后续可在此基础上进一步扩展动画效果、键盘交互支持以及无障碍特性,使其在更广泛的应用场景中发挥作用。

▼点个「
」赞,是我持续更新的动力 ▼
夜雨聆风
