乐于分享
好东西不私藏

QT C++ 之滑块插件设计与实现

QT C++ 之滑块插件设计与实现

在 Qt 应用程序中,QSlider 是一种常用的基础交互控件,广泛应用于数值调节、参数配置、进度控制等场景。Qt 自带的 QSlider 虽然功能完整,但在实际工程项目中,往往难以满足复杂交互、精细样式控制以及特定业务逻辑的需求。例如,在需要支持离散刻度、多滑块范围选择、自定义提示信息或特定交互反馈的场景下,默认控件的扩展能力显得有限。

基于上述问题,本文结合实际项目需求,对 Qt 中 Slider 滑块控件进行重新设计与封装,实现了一款高度可定制的 Slider 插件。该插件在保持原有滑块交互逻辑的基础上,支持自定义刻度规则、灵活的数值映射关系以及可扩展的交互提示机制,能够更好地适配工程化开发与复杂业务场景。

本文将围绕该 Slider 插件的设计思路、核心功能与关键实现点进行说明,重点介绍控件在交互行为、事件处理以及可扩展性方面的设计方法,为 Qt 开发者在自定义控件与插件化开发过程中提供参考。

功能点:
效果图:
功能描述:
  1. 自定义刻度步长
  2. 自定义刻度标签,可显示与隐藏
  3. 刻度原点显示和隐藏
  4. 当前拖动值气泡提示
  5. 有横向和竖向2中模式
  6. 有单把手和双把手模式,分别表示单个值或值范围
其它需求,请看源码做修改即可。
源码如下:
气泡提示源码文件:

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();    // 让三角形底边中心对准 globalPos    int 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(00width(), 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 height            rectArea = QRect(m_triangleHeight, 0width() - 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(505050220));        // 画文字        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()constreturn m_orientation; }    voidsetRange(int minV, int maxV);    intminimum()constreturn m_min; }    intmaximum()constreturn m_max; }    // 连续或离散    voidsetStep(double step)// step <=0 表示连续(无离散)    doublestep()constreturn m_step; }    // 模式:单把手 or 双把手    voidsetMode(Mode m);    // 单值或区间设置/读取    voidsetValue(double v);    doublevalue()constreturn 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/2    voidsetTickRadius(int r){ m_tickRadius = r; update();}    //刻度mark的颜色    voidsetTickLabels(const QColor& c){ tickLabelColor = c; update();}    //显示tooptip    voidsetShowTooptip(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);        // 去除末尾的0        while (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<doubledoublegetRangeValues()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 到 step    voidshowTipAtPoint(const QPoint& p, double value)//显示 tooltip    voidhideTip()//隐藏 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//显示tooltip    bool 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(200200200); //轨道背景颜色    QColor grooveFill = QColor(50150250); //轨道填充颜色    QColor tickLabelColor = QColor(000); //刻度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(10060);    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 <= 0return v;    // 以最小值为基准划分步长    double offset = v - m_min;    int n = (offset + m_step / 2.0) / m_step; // 四舍五入到最近一步 (offset + m_step / 2.0) / m_step     double 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(00, 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 { // Range        ptA = 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: 从底部到 value                int bottom = g.bottom();                int top = ptA.y();                QRect fillRect(g.left(), top, g.width(), bottom - top);                p.setBrush(grooveFill);                p.drawRoundedRect(fillRect, 55);            }        }        else {            // range: 填充 lower~upper            if (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, 55);            }        }    }    // 绘制刻度点(如果设置 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);            //标签为空或者包含label            if (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);                }            }        }    }    // 绘制 handles    p.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); // 不超出 upper                if (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); // 不低于 lower                if (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 <= upper            double 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(01);    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(050);    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米" }    }    );    //没有刻度的slider    ui->widget3->setOrientation(DiscreteRangeSlider::Horizontal);    ui->widget3->setMode(DiscreteRangeSlider::Single);    ui->widget3->setRange(0100);    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(0100);    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(3040);    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℃" }}    );    //垂直1    ui->widget6->setOrientation(DiscreteRangeSlider::Vertical);    ui->widget6->setMode(DiscreteRangeSlider::RangeMode);    ui->widget6->setRange(3040);    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℃" }}    );    //垂直2    ui->widget7->setOrientation(DiscreteRangeSlider::Vertical);    ui->widget7->setMode(DiscreteRangeSlider::Single);    ui->widget7->setRange(0100);    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;        });    //垂直3    ui->widget8->setOrientation(DiscreteRangeSlider::Vertical);    ui->widget8->setMode(DiscreteRangeSlider::Single);    ui->widget8->setRange(010);    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 自定义控件开发的一种参考范式。后续可在此基础上进一步扩展动画效果、键盘交互支持以及无障碍特性,使其在更广泛的应用场景中发挥作用。

关注我获取更多基础编程知识 

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

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

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » QT C++ 之滑块插件设计与实现

评论 抢沙发

2 + 5 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮