QT入门记录1:自定义一个RangeSlider

前言

项目搭搭叠叠,历时一年半,终于出货,进入维护优化阶段,工作又开始缓松下来,有不少的时间去思考未来。

鉴于过去一年半开发的各种踩坑,老板在年终总结上明确跟我说明,后面开发新的项目GUI不再用AWTK了,和其他人一样用QT开发,预期几个月后可能就会接到QT的项目,因此今年开始计划重拾QT的学习。

虽然前一年基本没有接触QT,但是经过一年的AWTK GUI开发经历,很多相关知识都可以迁移到现在的学习上,因此倒不用担心从0开始,不过QT和AWTK开发仍旧有一些本质的不同,因此本次学习的目的就是弄熟相关的开发流程,记录踩坑,找出不同,并予以记录。

个人认为学习GUI,除了通过源码学习思路和解决方案,最综合的一种实践检验方法就是去做该GUI框架下的自定义控件,因此这次就将以自定义一个范围滑动条来开头(Range Slider)。

实战

在一些区间类的设置场景下经常能看到这样一类的滑动条,和一般的slider不同的是它有两个滑块用来控制。

image-20260320213627016

实现这个控件的主要难点在于两个滑块的业务逻辑,首先是按键状态机的控制,RangeSlider是少见的有多个焦点的控件之一,为了两个滑块都能控制移动,两个滑块都要设置焦点;其次,滑块的正常,按下,聚焦状态都有不同的样式,因此滑块的业务处理会占控件业务很大一部分。

自然而然的想法就是用QCheckButton作为子控件来做滑块的逻辑了(QRadioButton虽然也能做,但是选中有些情况下没法取消,就没用),QCheckButton本身就有正常,按下,聚焦三种状态,最适合用来做滑块,我在AWTK就是这么干的。

不过QT上我一开始并没有直接上子控件来学习,而是用的自定义class+QPainter的形式来做,原因是我当时对QT如何处理子控件并不太熟悉,再者就是AWTK试过了子控件法,我想看看如果用原生方法来做可以达到什么样的水平。

原生绘制法

初级

myrangeslider.h

#ifndef MYRANGESLIDER_H
#define MYRANGESLIDER_H

#include <QWidget>
#include <QRect>

namespace Ui {
class MyRangeSlider;
}

class MyRangeSlider : public QWidget
{
    Q_OBJECT

    Q_PROPERTY(QColor bgColor READ bgColor WRITE setBgColor)
    Q_PROPERTY(QColor fgColor READ fgColor WRITE setFgColor)
    Q_PROPERTY(QColor draggerColorNormal READ draggerColorNormal WRITE setDraggerColorNormal)
    Q_PROPERTY(QColor draggerColorPressed READ draggerColorPressed WRITE setDraggerColorPressed)
    Q_PROPERTY(double value1 READ value1 WRITE setValue1)
    Q_PROPERTY(double value2 READ value2 WRITE setValue2)
    Q_PROPERTY(double valueMin READ valueMin WRITE setValueMin)
    Q_PROPERTY(double valueMax READ valueMax WRITE setValueMax)
    Q_PROPERTY(int barWidth READ barWidth WRITE setBarWidth)
    Q_PROPERTY(int draggerWidth READ draggerWidth WRITE setDraggerWidth)
    Q_PROPERTY(int draggerMininumValueDiff READ draggerMininumValueDiff WRITE setDraggerMininumValueDiff)

public:
    explicit MyRangeSlider(QWidget *parent = nullptr);
    ~MyRangeSlider();
    void setValue1(double value);
    void setValue2(double value);

    void setBgColor(const QColor &color);
    void setFgColor(const QColor &color);
    void setDraggerColorNormal(const QColor &color);
    void setDraggerColorPressed(const QColor &color);
    void setValueMin(double value);
    void setValueMax(double value);
    void setBarWidth(int width);
    void setDraggerWidth(int width);
    void setDraggerMininumValueDiff(int diff);

    QColor bgColor() const { return mBgColor; }
    QColor fgColor() const { return mFgColor; }
    QColor draggerColorNormal() const { return mDraggerColorNormal; }
    QColor draggerColorPressed() const { return mDraggerColorPressed; }
    double value1() const { return mValue1; }
    double value2() const { return mValue2; }
    double valueMin() const { return mValueMin; }
    double valueMax() const { return mValueMax; }
    int barWidth() const { return mBarWidth; }
    int draggerWidth() const { return mDraggerWidth; }
    int draggerMininumValueDiff() const { return mDraggerMininumValueDiff; }

    void paintEvent(QPaintEvent *event);
    void mousePressEvent(QMouseEvent *event);
    void mouseReleaseEvent(QMouseEvent *event);
    void mouseMoveEvent(QMouseEvent *event);

    // Settable
    QColor mBgColor;
    QColor mFgColor;
    QColor mDraggerColorNormal;
    QColor mDraggerColorPressed;
    double mValue1, mValue2, mValueMin, mValueMax;
    int mBarWidth;
    int mDraggerWidth;
    int mDraggerMininumValueDiff;
private:
    // Compute
    bool mDragger1Pressed;
    bool mDragger2Pressed;
    QRectF mDraggerRect1;
    QRectF mDraggerRect2;

    Ui::MyRangeSlider *ui;
};

#endif // MYRANGESLIDER_H

myrangeslider.cpp

#include "myrangeslider.h"
#include "ui_myrangeslider.h"
#include <QPainter>
#include <QDebug>
#include <QMouseEvent>

MyRangeSlider::MyRangeSlider(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MyRangeSlider)
{
    ui->setupUi(this);
    mValueMin = 0;
    mValueMax = 100;
    mValue1 = 40;
    mValue2 = 50;
    mBarWidth = 10;
    mDraggerWidth = 10;
    mFgColor = QColor("#00c2ff");
    mBgColor = QColor("#8b8b8b");
    mDraggerColorNormal = QColor("#639dff");
    mDraggerColorPressed = QColor("#00c2ff");
    mDragger1Pressed = false;
    mDragger2Pressed = false;
    mDraggerMininumValueDiff = 1;
    this->setAttribute(Qt::WA_StyledBackground);
}

MyRangeSlider::~MyRangeSlider()
{
    delete ui;
}

void MyRangeSlider::setValue1(double value)
{
    double value2 = (mValue2 - mDraggerMininumValueDiff > 0) ? mValue2 - mDraggerMininumValueDiff : 0;
    value = qBound(static_cast<double>(0), value, value2);
    mValue1 = value;
    repaint();
}


void MyRangeSlider::setValue2(double value)
{
    double value1 = (mValue1 + mDraggerMininumValueDiff <= mValueMax) ? mValue1 + mDraggerMininumValueDiff : mValueMax;
    value = qBound(value1, value, mValueMax);
    mValue2 = value;
    repaint();
}

void MyRangeSlider::setBgColor(const QColor &color)
{
    mBgColor = color;
    repaint();
}

void MyRangeSlider::setFgColor(const QColor &color)
{
    mFgColor = color;
    repaint();
}

void MyRangeSlider::setDraggerColorNormal(const QColor &color)
{
    mDraggerColorNormal = color;
    repaint();
}

void MyRangeSlider::setDraggerColorPressed(const QColor &color)
{
    mDraggerColorPressed = color;
    repaint();
}

void MyRangeSlider::setValueMin(double value)
{
    mValueMin = value;
    repaint();
}

void MyRangeSlider::setValueMax(double value)
{
    mValueMax = value;
    repaint();
}

void MyRangeSlider::setBarWidth(int width)
{
    mBarWidth = width;
    repaint();
}

void MyRangeSlider::setDraggerWidth(int width)
{
    mDraggerWidth = width;
    repaint();
}

void MyRangeSlider::setDraggerMininumValueDiff(int diff)
{
    mDraggerMininumValueDiff = diff;
    repaint();
}

void MyRangeSlider::paintEvent(QPaintEvent *event)
{
    // draw bar
    QPainter painter(this);

    painter.setPen(Qt::NoPen);
    painter.setBrush(mBgColor);
    QRectF barRect(0, (height() - mBarWidth) / 2.0, width(), mBarWidth);
    painter.drawRect(barRect);

    double pxValue1 = width() * mValue1 / (mValueMax - mValueMin);
    double pxValue2 = width() * mValue2 / (mValueMax - mValueMin);
    QRectF dragRect1(pxValue1 - mDraggerWidth / 2.0, 0, mDraggerWidth, height());
    QRectF dragRect2(pxValue2 - mDraggerWidth / 2.0, 0, mDraggerWidth, height());

    painter.setBrush(mFgColor);
    int fgWidth = (dragRect2.x() + dragRect2.width() / 2) - (dragRect1.x() + dragRect1.width() / 2);
    QRectF fgRect(dragRect1.x() + dragRect1.width() / 2, barRect.y(), fgWidth, mBarWidth);
    painter.drawRect(fgRect);

    // draw dragger1, dragger2
    painter.setBrush(mDragger1Pressed ? mDraggerColorPressed : mDraggerColorNormal);
    painter.drawRect(dragRect1);
    painter.setBrush(mDragger2Pressed ? mDraggerColorPressed : mDraggerColorNormal);
    painter.drawRect(dragRect2);

    mDraggerRect1 = dragRect1;
    mDraggerRect2 = dragRect2;
}


void MyRangeSlider::mousePressEvent(QMouseEvent *event){
    QPoint clickPos = event->pos();
    printf("event press: (%d %d)\r\n", clickPos.x(), clickPos.y());

    bool needRepaint = false;
    if(mDraggerRect1.contains(clickPos)
       && mDraggerRect2.contains(clickPos)){
        if(mValue1 == mValueMin){
            mDragger1Pressed = false;
            mDragger2Pressed = true;
            needRepaint = true;
        }
        else if(mValue2 == mValueMax){
            mDragger1Pressed = true;
            mDragger2Pressed = false;
            needRepaint = true;
        }
        else{
            // 选择距离中心最近的dragger
            QPointF p1 = mDraggerRect1.center();
            QPointF p2 = mDraggerRect2.center();
            double dist1 = QLineF(p1, clickPos).length();
            double dist2 = QLineF(p2, clickPos).length();
            if(dist1 > dist2){
                mDragger1Pressed = false;
                mDragger2Pressed = true;
                needRepaint = true;
            }
            else{
                mDragger1Pressed = true;
                mDragger2Pressed = false;
                needRepaint = true;
            }
        }
    }
    else if(mDraggerRect1.contains(clickPos)){
        mDragger1Pressed = true;
        mDragger2Pressed = false;
        needRepaint = true;
    }
    else if(mDraggerRect2.contains(clickPos)){
        mDragger1Pressed = false;
        mDragger2Pressed = true;
        needRepaint = true;
    }
    printf("mDragger1Pressed: %d mDragger1Pressed: %d\r\n", mDragger1Pressed, mDragger2Pressed);

    if(needRepaint){
        repaint();
    }
}

void MyRangeSlider::mouseReleaseEvent(QMouseEvent *event){
    mDragger1Pressed = false;
    mDragger2Pressed = false;
    repaint();
}

void MyRangeSlider::mouseMoveEvent(QMouseEvent *event){
    QPoint clickPos = event->pos();
    double newValue1 = (mValueMax - mValueMin) * clickPos.x() / width();
    double newValue2 = (mValueMax - mValueMin) * clickPos.x() / width();
    if(mDragger1Pressed){
        setValue1(newValue1);
    }
    else if(mDragger2Pressed){
        setValue2(newValue2);
    }

}

第一版练手的版本,只考虑了控件的鼠标事件,滑块就是简单绘制矩形,样式只有mDraggerColorNormal和mDraggerColorPressed两个滑块不同状态下背景色的绘制,两个颜色通过设置成qProperty的形式在design上设置,qProperty的作用类似于AWTK的属性注释,被声明的属性经过MOC解析之后在designer上可以显示属性并编辑,这种做法的优点就是能够自定义控件属性和易于理解。

这版功能上已经很完善了,毕竟滑动条绘制和值计算逻辑都比较简单,qProperty也可以接入qss做一些诸如主题切换的场景:

theme_light.qss

MyRangeSlider {
    qProperty-draggerColorNormal: #ffffff;
    qProperty-draggerColorPressed: #ff0000;
}

theme_dark.qss

MyRangeSlider {
    qProperty-draggerColorNormal: #000000;
    qProperty-draggerColorPressed: #00ff00;
}

效果如下:

![GIF 2026-3-21 15-04-56](./assets/GIF 2026-3-21 15-04-56.gif)

不过还是比较简陋,没有按键控制,滑块样式单一只有背景色且不支持图标,状态只支持normal和pressed两种状态,如果想新增样式/状态的话就又得增加property,且不管样式还是状态,随着变量的增多势必得封装起来。

如果只想增加状态而不想新增property setter和getter,比如新增一个滑块hover时的样式,要怎么办?

踩坑1:伪状态无法重置QSS

这里就踩了一个坑,开始以为QWidget不同伪状态时能够触发不同的样式属性,写成了这样:

MyRangeSlider {
    qProperty-draggerColorNormal: #ff0000;
}

MyRangeSlider::hover {
    qProperty-draggerColorNormal: #00ff00;
}

结果发现鼠标光标移到滑块上没有作用,于是问AI, 回答如下:

qproperty-xxx 在 Qt 样式表里通常只在 polish 时赋值一次,不会随着 :hover 这种伪状态自动重算。所以你会看到“无状态生效,hover 不生效

好吧,看起来还是得走QT属性系统,好在AI又给出了解决方案:

  1. QSS 改成属性选择器
MyRangeSlider[hovered="false"] {
    qproperty-draggerColorNormal: #ff0000;
}
MyRangeSlider[hovered="true"] {
    qproperty-draggerColorNormal: #00ff00;
}
  1. 在 enterEvent/leaveEvent 切换属性并 repolish
void MyRangeSlider::enterEvent(QEvent *event)
{
    setProperty("hovered", true);
    style()->unpolish(this);
    style()->polish(this);
    update();
    QWidget::enterEvent(event);
}

void MyRangeSlider::leaveEvent(QEvent *event)
{
    setProperty("hovered", false);
    style()->unpolish(this);
    style()->polish(this);
    update();
    QWidget::leaveEvent(event);
}

试了下有用,但是感觉太丑了,不符合qss的正规用法,而且每个事件都要重复走一段setProperty()+unpolish()+polish()函数,仍旧不太优雅。

状态样式探索一番后,我决定继续走状态封装+样式封装的思路。

进阶

myrangeslider.h

#ifndef MYRANGESLIDER_H
#define MYRANGESLIDER_H

#include <QWidget>
#include <QRadioButton>
#include <QRect>
#include <QPixmap>

namespace Ui {
class MyRangeSlider;
}

struct MyRangeSliderDraggerStyle {
    QColor mDraggerColor;
    QPixmap mDraggerIcon;
};

enum MyRangeSliderDraggerState{
    kDraggerNormal = 0,
    kDraggerPressed,
    kDraggerFocused
};

enum MyRangeSliderState {
    kRangerSliderNormal = 0,
    kRangeSliderDragger1Pressed,
    kRangeSliderDragger2Pressed,
    kRangeSliderDragger1Focused,
    kRangeSliderDragger2Focused
};

class MyRangeSliderDragger {
public:
    void setState(MyRangeSliderDraggerState state) { mState = state; }
    void setStyle(MyRangeSliderDraggerStyle *pstyle) { mStyle = pstyle; }
    void setStateAndStyle(MyRangeSliderDraggerState state, MyRangeSliderDraggerStyle *pstyle) {
        mState = state;
        mStyle = pstyle;
    }

    bool pressed(){ return mState == kDraggerPressed || mState == kDraggerFocused; }
    MyRangeSliderDraggerStyle *mStyle;
    MyRangeSliderDraggerState mState;
    QRectF mDraggerRect;
};

class MyRangeSlider : public QWidget
{
    Q_OBJECT

    Q_PROPERTY(QColor bgColor READ bgColor WRITE setBgColor)
    Q_PROPERTY(QColor fgColor READ fgColor WRITE setFgColor)
    Q_PROPERTY(QColor draggerColorNormal READ draggerColorNormal WRITE setDraggerColorNormal)
    Q_PROPERTY(QColor draggerColorPressed READ draggerColorPressed WRITE setDraggerColorPressed)
    Q_PROPERTY(QColor draggerColorFocused READ draggerColorFocused WRITE setDraggerColorPressed)

    Q_PROPERTY(QPixmap draggerIconNormal READ draggerIconNormal WRITE setDraggerIconNormal)
    Q_PROPERTY(QPixmap draggerIconPressed READ draggerIconPressed WRITE setDraggerIconPressed)
    Q_PROPERTY(QPixmap draggerIconFocused READ draggerIconFocused WRITE setDraggerIconFocused)

    Q_PROPERTY(double value1 READ value1 WRITE setValue1)
    Q_PROPERTY(double value2 READ value2 WRITE setValue2)
    Q_PROPERTY(double valueMin READ valueMin WRITE setValueMin)
    Q_PROPERTY(double valueMax READ valueMax WRITE setValueMax)
    Q_PROPERTY(int barWidth READ barWidth WRITE setBarWidth)
    Q_PROPERTY(int barHeight READ barHeight WRITE setBarHeight)
    Q_PROPERTY(int draggerWidth READ draggerWidth WRITE setDraggerWidth)
    Q_PROPERTY(int draggerHeight READ draggerHeight WRITE setDraggerHeight)
    Q_PROPERTY(int draggerMininumValueDiff READ draggerMininumValueDiff WRITE setDraggerMininumValueDiff)
    Q_PROPERTY(bool draggerUseIcon READ draggerUseIcon WRITE setDraggerUseIcon)
    Q_PROPERTY(int sliderStep READ sliderStep WRITE setSliderStep)
public:
    explicit MyRangeSlider(QWidget *parent = nullptr);
    ~MyRangeSlider();

    void initDraggerStyle();
    void setValue1(double value);
    void setValue2(double value);

    void setBgColor(const QColor &color);
    void setFgColor(const QColor &color);
    void setDraggerColorNormal(const QColor &color);
    void setDraggerColorPressed(const QColor &color);
    void setDraggerColorFocused(const QColor &color);
    void setDraggerIconNormal(const QPixmap &img);
    void setDraggerIconPressed(const QPixmap &img);
    void setDraggerIconFocused(const QPixmap &img);
    void setValueMin(double value);
    void setValueMax(double value);
    void setBarWidth(int width);
    void setBarHeight(int height);
    void setDraggerWidth(int width);
    void setDraggerHeight(int height);
    void setDraggerMininumValueDiff(int diff);
    void setDraggerUseIcon(bool flag);
    void setSliderStep(int step);

    QColor bgColor() const { return mBgColor; }
    QColor fgColor() const { return mFgColor; }
    QColor draggerColorNormal() const { return mDraggerStyleNormal.mDraggerColor; }
    QColor draggerColorPressed() const { return mDraggerStylePressed.mDraggerColor; }
    QColor draggerColorFocused() const { return mDraggerStyleFocused.mDraggerColor; }
    QPixmap draggerIconNormal() const { return mDraggerStyleNormal.mDraggerIcon; }
    QPixmap draggerIconPressed() const { return mDraggerStylePressed.mDraggerIcon; }
    QPixmap draggerIconFocused() const { return mDraggerStyleFocused.mDraggerIcon; }

    double value1() const { return mValue1; }
    double value2() const { return mValue2; }
    double valueMin() const { return mValueMin; }
    double valueMax() const { return mValueMax; }
    int barWidth() const { return mBarWidth; }
    int barHeight() const { return mBarHeight; }
    int draggerWidth() const { return mDraggerWidth; }
    int draggerHeight() const { return mDraggerHeight; }
    int draggerMininumValueDiff() const { return mDraggerMininumValueDiff; }
    int draggerUseIcon() const { return mDraggerUseIcon; }
    int sliderStep() const { return mSliderStep; }


    void setState(MyRangeSliderState state);
    void paintEvent(QPaintEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void enterEvent(QEvent *event) override;
    void leaveEvent(QEvent *event) override;
    void keyPressEvent(QKeyEvent *e) override;

    void focusOutEvent(QFocusEvent* e) override;
    void focusInEvent(QFocusEvent* e) override;

    // Settable
    bool mDraggerUseIcon;
    QColor mBgColor;
    QColor mFgColor;
    double mValue1, mValue2, mValueMin, mValueMax;
    int mBarWidth;
    int mBarHeight;
    int mDraggerWidth;
    int mDraggerHeight;
    int mDraggerMininumValueDiff;
    int mSliderStep;
    MyRangeSliderDraggerStyle mDraggerStyleNormal;
    MyRangeSliderDraggerStyle mDraggerStylePressed;
    MyRangeSliderDraggerStyle mDraggerStyleFocused;
private:
    // Compute
    MyRangeSliderDragger mDragger1;
    MyRangeSliderDragger mDragger2;
    MyRangeSliderState mState;
    Ui::MyRangeSlider *ui;
};

#endif // MYRANGESLIDER_H

myrangeslider.cpp

#include "myrangeslider.h"
#include "ui_myrangeslider.h"
#include <QPainter>
#include <QDebug>
#include <QMouseEvent>
#include <QStyle>
#include <QAbstractButton>

MyRangeSlider::MyRangeSlider(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MyRangeSlider)
{
    ui->setupUi(this);
    mValueMin = 0;
    mValueMax = 100;
    mValue1 = 40;
    mValue2 = 50;
    mBarWidth = 5;
    mDraggerWidth = 20;
    mDraggerHeight = 20;
    mFgColor = QColor("#00c2ff");
    mBgColor = QColor("#8b8b8b");
    mDraggerMininumValueDiff = 1;
    mDraggerUseIcon = false;
    mSliderStep = 1;
    initDraggerStyle();
    setState(kRangerSliderNormal);
    this->setAttribute(Qt::WA_StyledBackground);
}

MyRangeSlider::~MyRangeSlider()
{
    delete ui;
}

void MyRangeSlider::initDraggerStyle()
{
    //normal
    mDraggerStyleNormal.mDraggerColor = QColor("#ff0000");
    mDraggerStyleNormal.mDraggerIcon = QPixmap(":/images/range_silder_dragger0.png");

    //pressed
    mDraggerStylePressed.mDraggerColor = QColor("#00ff00");
    mDraggerStylePressed.mDraggerIcon = QPixmap(":/images/range_silder_dragger1.png");

    //focused
    mDraggerStyleFocused.mDraggerColor = QColor("#0000ff");
    mDraggerStyleFocused.mDraggerIcon = QPixmap(":/images/range_silder_dragger1.png");

}

void MyRangeSlider::setValue1(double value)
{
    double value2 = (mValue2 - mDraggerMininumValueDiff > 0) ? mValue2 - mDraggerMininumValueDiff : 0;
    value = qBound(static_cast<double>(0), value, value2);
    mValue1 = value;
    repaint();
}


void MyRangeSlider::setValue2(double value)
{
    double value1 = (mValue1 + mDraggerMininumValueDiff <= mValueMax) ? mValue1 + mDraggerMininumValueDiff : mValueMax;
    value = qBound(value1, value, mValueMax);
    mValue2 = value;
    repaint();
}

void MyRangeSlider::setBgColor(const QColor &color)
{
    mBgColor = color;
    repaint();
}

void MyRangeSlider::setFgColor(const QColor &color)
{
    mFgColor = color;
    repaint();
}

void MyRangeSlider::setDraggerColorNormal(const QColor &color)
{
    mDraggerStyleNormal.mDraggerColor = color;
    repaint();
}

void MyRangeSlider::setDraggerColorPressed(const QColor &color)
{
    mDraggerStylePressed.mDraggerColor = color;
    repaint();
}

void MyRangeSlider::setDraggerColorFocused(const QColor &color)
{
    mDraggerStylePressed.mDraggerColor = color;
    repaint();
}

void MyRangeSlider::setDraggerIconNormal(const QPixmap &img)
{
    mDraggerStyleNormal.mDraggerIcon = img;
    repaint();
}

void MyRangeSlider::setDraggerIconPressed(const QPixmap &img)
{
    mDraggerStylePressed.mDraggerIcon = img;
    repaint();
}

void MyRangeSlider::setDraggerIconFocused(const QPixmap &img)
{
    mDraggerStyleFocused.mDraggerIcon = img;
    repaint();
}

void MyRangeSlider::setValueMin(double value)
{
    mValueMin = value;
    repaint();
}

void MyRangeSlider::setValueMax(double value)
{
    mValueMax = value;
    repaint();
}

void MyRangeSlider::setBarWidth(int width)
{
    mBarWidth = width;
    repaint();
}

void MyRangeSlider::setBarHeight(int height)
{
    mBarHeight = height;
    repaint();
}

void MyRangeSlider::setDraggerWidth(int width)
{
    mDraggerWidth = width;
    repaint();
}

void MyRangeSlider::setDraggerHeight(int height)
{
    mDraggerHeight = height;
    repaint();
}

void MyRangeSlider::setDraggerMininumValueDiff(int diff)
{
    mDraggerMininumValueDiff = diff;
    repaint();
}

void MyRangeSlider::setDraggerUseIcon(bool flag)
{
    mDraggerUseIcon = flag;
    repaint();
}

void MyRangeSlider::setSliderStep(int step)
{
    mSliderStep = step;
}

void MyRangeSlider::setState(MyRangeSliderState state)
{
    MyRangeSliderState oldState = mState;
    mState = state;
    switch(state){
        case kRangerSliderNormal:{
            mDragger1.setStateAndStyle(kDraggerNormal, &mDraggerStyleNormal);
            mDragger2.setStateAndStyle(kDraggerNormal, &mDraggerStyleNormal);
            break;
        }
        case kRangeSliderDragger1Focused:{
            mDragger1.setStateAndStyle(kDraggerFocused, &mDraggerStyleFocused);
            mDragger2.setStateAndStyle(kDraggerNormal, &mDraggerStyleNormal);
            break;
        }
        case kRangeSliderDragger2Focused:{
            mDragger1.setStateAndStyle(kDraggerNormal, &mDraggerStyleNormal);
            mDragger2.setStateAndStyle(kDraggerFocused, &mDraggerStyleFocused);
            break;
        }
        case kRangeSliderDragger1Pressed:{
            mDragger1.setStateAndStyle(kDraggerPressed, &mDraggerStylePressed);
            mDragger2.setStateAndStyle(kDraggerNormal, &mDraggerStyleNormal);
            break;
        }
        case kRangeSliderDragger2Pressed:{
            mDragger1.setStateAndStyle(kDraggerNormal, &mDraggerStyleNormal);
            mDragger2.setStateAndStyle(kDraggerPressed, &mDraggerStylePressed);
            break;
        }
    }

    if(oldState != state){
        repaint();
    }
}

void MyRangeSlider::paintEvent(QPaintEvent *event)
{
    printf("mState: %d\r\n", mState);
    // draw bar
    QPainter painter(this);
    painter.save();
    painter.setPen(Qt::NoPen);
    painter.setBrush(mBgColor);

    int padding = mDraggerWidth;
    QRectF barRect(padding, (height() - mBarWidth) / 2.0, width() - padding * 2, mBarWidth);
    painter.drawRect(barRect);

    double pxValue1 = (barRect.width()) * mValue1 / (mValueMax - mValueMin) + padding / 2;
    double pxValue2 = (barRect.width()) * mValue2 / (mValueMax - mValueMin) + padding / 2;
    QRectF dragRect1(pxValue1, barRect.y() + barRect.height() / 2 - mDraggerHeight / 2, mDraggerWidth, mDraggerHeight);
    QRectF dragRect2(pxValue2, barRect.y() + barRect.height() / 2 - mDraggerHeight / 2, mDraggerWidth, mDraggerHeight);

    painter.setBrush(mFgColor);
    int fgWidth = (dragRect2.x() + dragRect2.width() / 2) - (dragRect1.x() + dragRect1.width() / 2);
    QRectF fgRect(dragRect1.x() + dragRect1.width() / 2, barRect.y(), fgWidth, mBarWidth);
    painter.drawRect(fgRect);
    painter.restore();

    // draw dragger1, dragger2
    if(!mDraggerUseIcon){
        painter.setBrush(mDragger1.mStyle->mDraggerColor);
        painter.drawRect(dragRect1);
        painter.setBrush(mDragger2.mStyle->mDraggerColor);
        painter.drawRect(dragRect2);
    }
    else{
        int draggerWidth = mDragger1.mStyle->mDraggerIcon.rect().width();
        int draggerHeight = mDragger1.mStyle->mDraggerIcon.rect().height();
        dragRect1 = QRectF(pxValue1,
                           barRect.y() - draggerHeight / 2.0 + barRect.height() / 2,
                           draggerWidth,
                           draggerHeight
                           );
        dragRect2 = QRectF(pxValue2,
                           barRect.y() - draggerHeight / 2.0 + barRect.height() / 2,
                           draggerWidth,
                           draggerHeight
                           );
        painter.drawPixmap(dragRect1, mDragger1.mStyle->mDraggerIcon, mDragger1.mStyle->mDraggerIcon.rect());
        painter.drawPixmap(dragRect2, mDragger2.mStyle->mDraggerIcon, mDragger2.mStyle->mDraggerIcon.rect());
    }

    mDragger1.mDraggerRect = dragRect1;
    mDragger2.mDraggerRect = dragRect2;
}


void MyRangeSlider::mousePressEvent(QMouseEvent *event){
    QPoint clickPos = event->pos();
    printf("event press: (%d %d)\r\n", clickPos.x(), clickPos.y());
    bool needRepaint = false;

    QRectF mDraggerRect1 = mDragger1.mDraggerRect;
    QRectF mDraggerRect2 = mDragger2.mDraggerRect;
    if(mDraggerRect1.contains(clickPos)
       && mDraggerRect2.contains(clickPos)){
        if(mValue1 == mValueMin){
            setState(kRangeSliderDragger2Pressed);
            needRepaint = true;
        }
        else if(mValue2 == mValueMax){
            setState(kRangeSliderDragger1Pressed);
            needRepaint = true;
        }
        else{
            // 选择距离中心最近的dragger
            QPointF p1 = mDraggerRect1.center();
            QPointF p2 = mDraggerRect2.center();
            double dist1 = QLineF(p1, clickPos).length();
            double dist2 = QLineF(p2, clickPos).length();
            if(dist1 > dist2){
                setState(kRangeSliderDragger2Pressed);
                needRepaint = true;
            }
            else{
                setState(kRangeSliderDragger1Pressed);
                needRepaint = true;
            }
        }
    }
    else if(mDraggerRect1.contains(clickPos)){
        setState(kRangeSliderDragger1Pressed);
        needRepaint = true;
    }
    else if(mDraggerRect2.contains(clickPos)){
        setState(kRangeSliderDragger2Pressed);
        needRepaint = true;
    }
    printf("mDragger1Pressed: %d mDragger2Pressed: %d\r\n", mDragger1.pressed(), mDragger2.pressed());

    if(needRepaint){
        repaint();
    }
}

void MyRangeSlider::mouseReleaseEvent(QMouseEvent *event){
    if(mDragger1.pressed()){
        setState(kRangeSliderDragger1Focused);
    }
    else if(mDragger2.pressed()){
        setState(kRangeSliderDragger2Focused);
    }
    else{
        setState(kRangerSliderNormal);
    }

    repaint();
}

void MyRangeSlider::mouseMoveEvent(QMouseEvent *event){
    QPoint clickPos = event->pos();
    double newValue1 = (mValueMax - mValueMin) * clickPos.x() / width();
    double newValue2 = (mValueMax - mValueMin) * clickPos.x() / width();
    if(mDragger1.pressed()){
        setValue1(newValue1);
    }
    else if(mDragger2.pressed()){
        setValue2(newValue2);
    }
}


void MyRangeSlider::enterEvent(QEvent *event){
    repaint();
}

void MyRangeSlider::leaveEvent(QEvent *event){
    repaint();
}

void MyRangeSlider::keyPressEvent(QKeyEvent *e){
    QWidget::keyPressEvent(e);
    switch (e->key()) {
        case Qt::Key_Return:{
            if(mState == kRangerSliderNormal){
                setState(kRangeSliderDragger1Focused);
            }
            else if(mState == kRangeSliderDragger1Focused){
                setState(kRangeSliderDragger1Pressed);
            }
            else if(mState == kRangeSliderDragger2Focused){
                setState(kRangeSliderDragger2Pressed);
            }
            break;
        }
        case Qt::Key_Escape:{
            if(mState == kRangeSliderDragger1Pressed){
                setState(kRangeSliderDragger1Focused);
            }
            else if(mState == kRangeSliderDragger2Pressed){
                setState(kRangeSliderDragger2Focused);
            }
            break;
        }
        case Qt::Key_Up:{
            focusPreviousChild();
            break;
        }
        case Qt::Key_Down:
        {
             focusNextChild();
             break;
        }
        case Qt::Key_Left:
        {
            if(mState == kRangeSliderDragger2Focused){
                setState(kRangeSliderDragger1Focused);
            }
            else if(mState == kRangeSliderDragger1Focused){
                setState(kRangeSliderDragger2Focused);
            }
            else if(mState == kRangeSliderDragger1Pressed){
                setValue1(mValue1 - mSliderStep);
            }
            else if(mState == kRangeSliderDragger2Pressed){
                setValue2(mValue2 - mSliderStep);
            }
            break;
        }
        case Qt::Key_Right:
        {
            if(mState == kRangeSliderDragger1Focused){
                setState(kRangeSliderDragger2Focused);
            }
            else if(mState == kRangeSliderDragger2Focused){
                setState(kRangeSliderDragger1Focused);
            }
            else if(mState == kRangeSliderDragger1Pressed){
                setValue1(mValue1 + mSliderStep);
            }
            else if(mState == kRangeSliderDragger2Pressed){
                setValue2(mValue2 + mSliderStep);
            }
            break;
        }
       
    }
}

void MyRangeSlider::focusOutEvent(QFocusEvent* e){
    setState(kRangerSliderNormal);
    repaint();
}

void MyRangeSlider::focusInEvent(QFocusEvent* e){
    setState(kRangerSliderNormal);
    repaint();
}

最终的完成版本如上,把滑块脱离出来做成了一个独立的类MyRangeSliderDragger,并把滑块的样式独立建类为MyRangeSliderDraggerStyle,滑块状态建立枚举为MyRangeSliderDraggerState, 主控件也有自己的状态MyRangeSliderState,用于控制两个dragger类的状态,这样设计便于实现按键状态机。

设置滑块正常,聚焦, 按下状态下背景色分别为红,蓝,绿,效果如下:

![GIF 2026-3-21 16-12-48](./assets/GIF 2026-3-21 16-12-48.gif)

也可以改为图标,这里就不放了。

到了这里,我对于这个控件的要求就基本完善了,但是实现起来明显繁琐了,样式和状态定义都得自己去做,而且有qProperty覆盖状态x样式导致方法数爆炸的问题,给dragger的三个样式属性draggerIcon, draggerColor新增一个focused的状态就要新增2(属性数)x2(setter及getter)=4个方法,扩展效率很差。

而且,既然已经考虑封装滑块的样式和状态了,QT的那些现成的Widget类封装不能直接拿来用?

山重水复,终于又回来了。

子控件法

myrangeslider.h

#ifndef MYRANGESLIDER_H
#define MYRANGESLIDER_H

#include <QWidget>
#include <QRadioButton>
#include <QCheckBox>
#include <QRect>
#include <QPixmap>
#include <QLabel>

namespace Ui {
class MyRangeSlider;
}


class MyRangeSliderDragger : public QCheckBox{
public:
    void setPressed(bool flag) { setDown(flag); }
    bool pressed(){ return isDown(); }
};

class MyRangeSlider : public QWidget
{
    Q_OBJECT

    Q_PROPERTY(QColor bgColor READ bgColor WRITE setBgColor)
    Q_PROPERTY(QColor fgColor READ fgColor WRITE setFgColor)

    Q_PROPERTY(double value1 READ value1 WRITE setValue1)
    Q_PROPERTY(double value2 READ value2 WRITE setValue2)
    Q_PROPERTY(double valueMin READ valueMin WRITE setValueMin)
    Q_PROPERTY(double valueMax READ valueMax WRITE setValueMax)
    Q_PROPERTY(int barHeight READ barHeight WRITE setBarHeight)
    Q_PROPERTY(int draggerWidth READ draggerWidth WRITE setDraggerWidth)
    Q_PROPERTY(int draggerHeight READ draggerHeight WRITE setDraggerHeight)
    Q_PROPERTY(int draggerMininumValueDiff READ draggerMininumValueDiff WRITE setDraggerMininumValueDiff)
    Q_PROPERTY(bool draggerUseIcon READ draggerUseIcon WRITE setDraggerUseIcon)
    Q_PROPERTY(int sliderStep READ sliderStep WRITE setSliderStep)

public:
    explicit MyRangeSlider(QWidget *parent = nullptr);
    ~MyRangeSlider();

    void initDraggerStyle();
    void initLabelStyle();
    void setValue1(double value);
    void setValue2(double value);

    void setBgColor(const QColor &color);
    void setFgColor(const QColor &color);
    void setValueMin(double value);
    void setValueMax(double value);
    void setBarHeight(int height);
    void setDraggerWidth(int width);
    void setDraggerHeight(int height);
    void setDraggerMininumValueDiff(int diff);
    void setDraggerUseIcon(bool flag);
    void setSliderStep(int step);
    void setValueLabelWidth(int width);
    void setValueLabelHeight(int height);
    void setValueLabelFontPointSize(int size);

    QColor bgColor() const { return mBgColor; }
    QColor fgColor() const { return mFgColor; }

    double value1() const { return mValue1; }
    double value2() const { return mValue2; }
    double valueMin() const { return mValueMin; }
    double valueMax() const { return mValueMax; }
    int barHeight() const { return mBarHeight; }
    int draggerWidth() const { return mDraggerWidth; }
    int draggerHeight() const { return mDraggerHeight; }
    int draggerMininumValueDiff() const { return mDraggerMininumValueDiff; }
    int draggerUseIcon() const { return mDraggerUseIcon; }
    int sliderStep() const { return mSliderStep; }

    void clearAllDraggerFocus();

    bool eventFilter(QObject *watched, QEvent *event);
    void paintEvent(QPaintEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void enterEvent(QEvent *event) override;
    void leaveEvent(QEvent *event) override;
    void keyPressEvent(QKeyEvent *e) override;

    void focusOutEvent(QFocusEvent* e) override;
    void focusInEvent(QFocusEvent* e) override;
    

    // Settable
    bool mDraggerUseIcon;
    QColor mBgColor;
    QColor mFgColor;
    double mValue1, mValue2, mValueMin, mValueMax;
    int mBarHeight;
    int mDraggerWidth;
    int mDraggerHeight;
    int mDraggerMininumValueDiff;
    int mSliderStep;
private:
    // Compute
    QRectF mBarRect;
    MyRangeSliderDragger mDragger1;
    MyRangeSliderDragger mDragger2;
    Ui::MyRangeSlider *ui;
};

#endif // MYRANGESLIDER_H

myrangeslider.cpp

#include "myrangeslider.h"
#include "ui_myrangeslider.h"
#include <QPainter>
#include <QDebug>
#include <QMouseEvent>
#include <QStyle>
#include <QAbstractButton>
#include <QFont>

MyRangeSlider::MyRangeSlider(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MyRangeSlider)
{
    ui->setupUi(this);
    mValueMin = 0;
    mValueMax = 100;
    mValue1 = 40;
    mValue2 = 50;
    mDraggerWidth = 30;
    mDraggerHeight = 30;
    mBarHeight = 5;
    mFgColor = QColor("#00c2ff");
    mBgColor = QColor("#8b8b8b");
    mDraggerMininumValueDiff = 1;
    mDraggerUseIcon = false;
    mSliderStep = 1;
    this->setFocusPolicy(Qt::NoFocus);
    initDraggerStyle();
    initLabelStyle();
}

MyRangeSlider::~MyRangeSlider()
{
    delete ui;
}

void MyRangeSlider::initDraggerStyle()
{
    mDragger1.setParent(this);
    mDragger1.setObjectName("dragger1");
    // 用于禁用子控件的鼠标事件
    mDragger1.setAttribute(Qt::WA_TransparentForMouseEvents);
    mDragger1.installEventFilter(this);

    mDragger2.setParent(this);
    mDragger2.setObjectName("dragger2");
    // 用于禁用子控件的鼠标事件
    mDragger2.setAttribute(Qt::WA_TransparentForMouseEvents);
    mDragger2.installEventFilter(this);
}


void MyRangeSlider::setValue1(double value)
{
    double value2 = (mValue2 - mDraggerMininumValueDiff > 0) ? mValue2 - mDraggerMininumValueDiff : 0;
    value = qBound(static_cast<double>(0), value, value2);
    mValue1 = value;
    repaint();
}


void MyRangeSlider::setValue2(double value)
{
    double value1 = (mValue1 + mDraggerMininumValueDiff <= mValueMax) ? mValue1 + mDraggerMininumValueDiff : mValueMax;
    value = qBound(value1, value, mValueMax);
    mValue2 = value;
    repaint();
}

void MyRangeSlider::setBgColor(const QColor &color)
{
    mBgColor = color;
    repaint();
}

void MyRangeSlider::setFgColor(const QColor &color)
{
    mFgColor = color;
    repaint();
}

void MyRangeSlider::setValueMin(double value)
{
    mValueMin = value;
    repaint();
}

void MyRangeSlider::setValueMax(double value)
{
    mValueMax = value;
    repaint();
}


void MyRangeSlider::setBarHeight(int height)
{
    mBarHeight = height;
    repaint();
}

void MyRangeSlider::setDraggerWidth(int width)
{
    mDraggerWidth = width;
    repaint();
}

void MyRangeSlider::setDraggerHeight(int height)
{
    mDraggerHeight = height;
    repaint();
}

void MyRangeSlider::setDraggerMininumValueDiff(int diff)
{
    mDraggerMininumValueDiff = diff;
    repaint();
}

void MyRangeSlider::setDraggerUseIcon(bool flag)
{
    mDraggerUseIcon = flag;
    repaint();
}

void MyRangeSlider::setSliderStep(int step)
{
    mSliderStep = step;
}



void MyRangeSlider::clearAllDraggerFocus()
{
    mDragger1.setChecked(false);
    mDragger2.setChecked(false);
    repaint();
}

bool MyRangeSlider::eventFilter(QObject *watched, QEvent *event)
{
    if(event->type() == QEvent::FocusOut){
       bool isDragger = (watched == &mDragger1 || watched == &mDragger2);
       if(isDragger){
            MyRangeSliderDragger *dragger = static_cast<MyRangeSliderDragger*>(watched);
            dragger->setChecked(false);
       } 
    }
    else if(event->type() == QEvent::KeyPress){
        QKeyEvent *ke = static_cast<QKeyEvent *>(event);
        switch (ke->key()) {
            case Qt::Key_Escape:{
                clearAllDraggerFocus();
                break;
            }
            case Qt::Key_Return:{
                if(mDragger1.hasFocus()){
                    mDragger1.setChecked(!mDragger1.isChecked());
                }
                if(mDragger2.hasFocus()){
                    mDragger2.setChecked(!mDragger2.isChecked());
                }
                break;
            }
            case Qt::Key_Up:{
                focusPreviousChild();
                break;
            }
            case Qt::Key_Down:
            {
                focusNextChild();
                break;
            }
            case Qt::Key_Left:
            {
                if(mDragger1.hasFocus() && !mDragger1.isChecked()){
                    focusNextChild();
                }
                else if(mDragger2.hasFocus() && !mDragger2.isChecked()){
                    focusPreviousChild();
                }
                else if(mDragger1.hasFocus() && mDragger1.isChecked()){
                    setValue1(mValue1 - mSliderStep);
                }
                else if(mDragger2.hasFocus() && mDragger2.isChecked()){
                    setValue2(mValue2 - mSliderStep);
                }
                
                break;
            }
            case Qt::Key_Right:
            {
                if(mDragger1.hasFocus() && !mDragger1.isChecked()){
                    focusNextChild();
                }
                else if(mDragger2.hasFocus() && !mDragger2.isChecked()){
                    focusPreviousChild();
                }
                if(mDragger1.hasFocus() && mDragger1.isChecked()){
                    setValue1(mValue1 + mSliderStep);
                }
                if(mDragger2.hasFocus() && mDragger2.isChecked()){
                    setValue2(mValue2 + mSliderStep);
                }
                break;
            }

        }
        return true;
    }
    else if(event->type() == QEvent::FocusIn){
        repaint();
    }
    return false;
}

void MyRangeSlider::paintEvent(QPaintEvent *event)
{
    // draw bar
    QPainter painter(this);
    painter.save();
    painter.setPen(Qt::NoPen);
    painter.setBrush(mBgColor);

    int padding = mDraggerWidth;
    QRectF barRect(padding, (height() - mBarHeight) / 2.0, width() - padding * 2,  mBarHeight);
    painter.drawRect(barRect);
    mBarRect = barRect;

    double pxValue1 = (barRect.width()) * mValue1 / (mValueMax - mValueMin) + padding / 2;
    double pxValue2 = (barRect.width()) * mValue2 / (mValueMax - mValueMin) + padding / 2;
    QRectF dragRect1(pxValue1, barRect.y() + (barRect.height() - mDraggerHeight) / 2, mDraggerWidth, mDraggerHeight);
    QRectF dragRect2(pxValue2, barRect.y() + (barRect.height() - mDraggerHeight) / 2, mDraggerWidth, mDraggerHeight);

    painter.setBrush(mFgColor);
    int fgWidth = (dragRect2.x() + dragRect2.width() / 2) - (dragRect1.x() + dragRect1.width() / 2);
    QRectF fgRect(dragRect1.x() + dragRect1.width() / 2, barRect.y(), fgWidth, barRect.height());
    painter.drawRect(fgRect);
    painter.restore();

    mDragger1.setGeometry(pxValue1, barRect.y() + barRect.height() / 2 - mDraggerHeight / 2, mDraggerWidth, mDraggerHeight);
    mDragger2.setGeometry(pxValue2, barRect.y() + barRect.height() / 2 - mDraggerHeight / 2, mDraggerWidth, mDraggerHeight);


}


void MyRangeSlider::mousePressEvent(QMouseEvent *event){
    QPoint clickPos = event->pos();
    printf("event press: (%d %d)\r\n", clickPos.x(), clickPos.y());
    bool needRepaint = false;

    QRectF mDraggerRect1 = mDragger1.geometry();
    QRectF mDraggerRect2 = mDragger2.geometry();
    if(mDraggerRect1.contains(clickPos)
       && mDraggerRect2.contains(clickPos)){
        if(mValue1 == mValueMin){
            mDragger2.setPressed(false);
            mDragger1.setPressed(true); mDragger1.setFocus();
            needRepaint = true;
        }
        else if(mValue2 == mValueMax){
            mDragger1.setPressed(false);
            mDragger2.setPressed(true); mDragger2.setFocus();
            needRepaint = true;
        }
        else{
            // 选择距离中心最近的dragger
            QPointF p1 = mDraggerRect1.center();
            QPointF p2 = mDraggerRect2.center();
            double dist1 = QLineF(p1, clickPos).length();
            double dist2 = QLineF(p2, clickPos).length();
            if(dist1 > dist2){
                mDragger1.setPressed(false);
                mDragger2.setPressed(true); mDragger2.setFocus();
                needRepaint = true;
            }
            else{
                mDragger2.setPressed(false);
                mDragger1.setPressed(true); mDragger1.setFocus();
                needRepaint = true;
            }
        }
    }
    else if(mDraggerRect1.contains(clickPos)){
        mDragger2.setPressed(false);
        mDragger1.setPressed(true); mDragger1.setFocus();
        needRepaint = true;
    }
    else if(mDraggerRect2.contains(clickPos)){
        mDragger1.setPressed(false);
        mDragger2.setPressed(true); mDragger2.setFocus();
        needRepaint = true;
    }
    printf("mDragger1Pressed: %d mDragger2Pressed: %d\r\n", mDragger1.pressed(), mDragger2.pressed());

    if(needRepaint){
        repaint();
    }
}

void MyRangeSlider::mouseReleaseEvent(QMouseEvent *event){
    mDragger1.setPressed(false);
    mDragger2.setPressed(false);
    repaint();
}

void MyRangeSlider::mouseMoveEvent(QMouseEvent *event){
    QPoint clickPos = event->pos();
    double newValue1 = (mValueMax - mValueMin) * (clickPos.x() - mBarRect.x()) / mBarRect.width();
    double newValue2 = (mValueMax - mValueMin) * (clickPos.x() - mBarRect.x()) / mBarRect.width();
    if(mDragger1.pressed()){
        setValue1(newValue1);
    }
    if(mDragger2.pressed()){
        setValue2(newValue2);
    }
}


void MyRangeSlider::enterEvent(QEvent *event){
    repaint();
}

void MyRangeSlider::leaveEvent(QEvent *event){
    repaint();
}

void MyRangeSlider::keyPressEvent(QKeyEvent *e){
    
}

void MyRangeSlider::focusOutEvent(QFocusEvent* e){
    qDebug()<<"Out="<<objectName(); //当失去焦点时输出失去焦点的部件名称
    clearAllDraggerFocus();
    repaint();
}

//必须调用 update()函数,否则焦点不会被更新
void MyRangeSlider::focusInEvent(QFocusEvent* e){
    repaint();
}

最终版本,回到当时AWTK开发的思路,用QCheckButton来做滑块控件,由于QCheckButton自身就有状态和样式相关的API,因此之前的各种style和state封装都可以直接删掉了,整个cpp文件减少了一百多行。

而且设置qss样式也很容易,直接父控件后面指定子控件名称就行了,qss固有的属性正好满足要求,不需要再自定义属性然后搞paintEvent逻辑。

MyRangeSlider QCheckBox::focus{
   outline: none;
}

MyRangeSlider QCheckBox::indicator {
    width: 20px;
    height: 20px;
    border: 2px solid transparent;
    image: url(:/images/range_silder_dragger0.png);
}

MyRangeSlider QCheckBox::indicator::focus {
    width: 20px;
    height: 20px;
    border: 2px solid #00ffff;
    outline: none;
}

MyRangeSlider QCheckBox::indicator::pressed, MyRangeSlider QCheckBox::indicator::checked {
    width: 20px;
    height: 20px;
    image: url(:/images/range_silder_dragger1.png);
}

效果图(这一次加上了图标):

![GIF 2026-3-21 17-17-21](./assets/GIF 2026-3-21 17-17-21.gif)

另外补充一段细节,我在两个dragger的样式控制上卡了很久,总结出来,QT给控件做样式有几种方法:QPainter, QPalette, QSS, QStyle, 几种是相互补充配合而不是替代的关系。其中最能深入QT本质的就是自己写控件的Style,继承CommonStyle,但是那个十分繁琐,需要对QWidget的元素绘制(PE, CE, CC三大类元素)以及QStyle继承体系有一定了解,要自己实现drawPrimitve/drawComplexControl, polish/unpolish, pixelMetric等等一系列方法, 本质还是封装QPainter干活,实践成本很高。

我看了下QPushButton和QSlider的paintEvent源码,看得云里雾里,深感复杂度非自己目前功能所能应对,最后只得放弃了事。

踩坑2:事件过滤器

一开始的时候,思路没有转变过来,想当然继续在MyRangeSlider::keyEvent方法上去控制dragger的相关事件:

void MyRangeSlider::keyPressEvent(QKeyEvent *e){
    QKeyEvent *ke = static_cast<QKeyEvent *>(e);
    switch (ke->key()) {
        case Qt::Key_Escape:{
            clearAllDraggerFocus();
            break;
        }
        case Qt::Key_Return:{
            if(mDragger1.hasFocus()){
                mDragger1.setChecked(!mDragger1.isChecked());
            }
            if(mDragger2.hasFocus()){
                mDragger2.setChecked(!mDragger2.isChecked());
            }
            break;
        }
        case Qt::Key_Up:{
            focusPreviousChild();
            break;
        }
        case Qt::Key_Down:
        {
            focusNextChild();
            break;
        }
        case Qt::Key_Left:
        {
            if(mDragger1.hasFocus() && !mDragger1.isChecked()){
                focusNextChild();
            }
            else if(mDragger2.hasFocus() && !mDragger2.isChecked()){
                focusPreviousChild();
            }
            else if(mDragger1.hasFocus() && mDragger1.isChecked()){
                setValue1(mValue1 - mSliderStep);
            }
            else if(mDragger2.hasFocus() && mDragger2.isChecked()){
                setValue2(mValue2 - mSliderStep);
            }
            
            break;
        }
        case Qt::Key_Right:
        {
            if(mDragger1.hasFocus() && !mDragger1.isChecked()){
                focusNextChild();
            }
            else if(mDragger2.hasFocus() && !mDragger2.isChecked()){
                focusPreviousChild();
            }
            if(mDragger1.hasFocus() && mDragger1.isChecked()){
                setValue1(mValue1 + mSliderStep);
            }
            if(mDragger2.hasFocus() && mDragger2.isChecked()){
                setValue2(mValue2 + mSliderStep);
            }
            break;
        }

    }
}

结果发现:怎么按键按下了滑块之后按左右还是焦点切换?(预期应该移动滑块)

后面检查才发现MyRangeSliderDragger拿到 Left/Right 后,走了它还是自己的QCheckBox默认按键导航逻辑,导致焦点切换。

对于这种情况的一个默认思路就是重载MyRangeSliderDragger的keyEvent事件去自己实现逻辑,但是如果后面子控件一多,重载keyEvent的方法就显得繁琐了,有没有统一集中的方法?

很幸运, QT考虑到了此番场景,提供了一个叫事件过滤器的特性,事件过滤器允许事件在走控件相关处理逻辑时,先走设置事件过滤器的逻辑,再根据事件过滤器的结果来确定继不继续走控件相关处理逻辑。

在MyRangeSlider上重载eventFilter,把KeyEvent的事件迁移过去并指定判断watched对象为dragger时处理,问题解决。

bool MyRangeSlider::eventFilter(QObject *watched, QEvent *event)
{
    if(event->type() == QEvent::FocusOut){
       bool isDragger = (watched == &mDragger1 || watched == &mDragger2);
       if(isDragger){
            MyRangeSliderDragger *dragger = static_cast<MyRangeSliderDragger*>(watched);
            dragger->setChecked(false);
       } 
    }
    else if(event->type() == QEvent::KeyPress){
        QKeyEvent *ke = static_cast<QKeyEvent *>(event);
        switch (ke->key()) {
            case Qt::Key_Escape:{
                clearAllDraggerFocus();
                break;
            }
            case Qt::Key_Return:{
                if(mDragger1.hasFocus()){
                    mDragger1.setChecked(!mDragger1.isChecked());
                }
                if(mDragger2.hasFocus()){
                    mDragger2.setChecked(!mDragger2.isChecked());
                }
                break;
            }
            case Qt::Key_Up:{
                focusPreviousChild();
                break;
            }
            case Qt::Key_Down:
            {
                focusNextChild();
                break;
            }
            case Qt::Key_Left:
            {
                if(mDragger1.hasFocus() && !mDragger1.isChecked()){
                    focusNextChild();
                }
                else if(mDragger2.hasFocus() && !mDragger2.isChecked()){
                    focusPreviousChild();
                }
                else if(mDragger1.hasFocus() && mDragger1.isChecked()){
                    setValue1(mValue1 - mSliderStep);
                }
                else if(mDragger2.hasFocus() && mDragger2.isChecked()){
                    setValue2(mValue2 - mSliderStep);
                }
                
                break;
            }
            case Qt::Key_Right:
            {
                if(mDragger1.hasFocus() && !mDragger1.isChecked()){
                    focusNextChild();
                }
                else if(mDragger2.hasFocus() && !mDragger2.isChecked()){
                    focusPreviousChild();
                }
                if(mDragger1.hasFocus() && mDragger1.isChecked()){
                    setValue1(mValue1 + mSliderStep);
                }
                if(mDragger2.hasFocus() && mDragger2.isChecked()){
                    setValue2(mValue2 + mSliderStep);
                }
                break;
            }

        }
        return true; // true表示继续不传递控件给事件
    }
    else if(event->type() == QEvent::FocusIn){
        repaint();
    }
    return false;	// false表示继续传递控件给事件
}

事件过滤器也是QT的一个独特的特性了,一个控件可以安装多个事件过滤器,本质和Single/Slot一样都是基于观察者模式的实现方式:事件过滤器回调类似链表的方式组织,集体观察指定的控件,通过回调结果决定是否把事件继续传递给控件。

事件过滤器函数a------->事件过滤器函数b------->事件过滤器函数c
|                        |                 |    
|                        |                 |
____________________________________________
                         |
                         |
                         V
                        控件

在AWTK上,没有类似事件过滤器的特性,只能用widget_on在控件上做事后处理,有点遗憾。

踩坑3:代码无法从QSS获取属性设置值

到了这里代码的功能和可扩展性都得到了很大提升,但是还没有做显示值的表框,一开始的想法是直接在代码上获取滑块focus状态下的聚焦框颜色,也就是border的设置,然后提取出颜色给画布绘制,于是这里又发现了一个大坑,上网搜索才知道QT的QSS并没有直接的像JSON那样能够获取属性key和value一类的API,唯一的方式就是通过styleSheet()方法获取设置样式然后正则暴力解析,效率不高,适用场景也很窄(比如一个控件一个qss),类似的能获取样式颜色的方法是QPalette,但是QPalette和QSS居然是两套独立的系统,实在令人无语,估计是qss脱胎于css, 太复杂不好解析的缘故?

这如果是在AWTK上就很好办了,AWTK的控件样式也是属性系统的一部分,而且样式属性类型定死了只有int, str, color等几种形式,要获取相关样式就有专门的API:

  style_t* style = widget->astyle;
  color_t trans = color_init(0, 0, 0, 0xd0);
  color_t bg_color = style_get_color(style, STYLE_ID_BG_COLOR, trans);
  color_t border_color = style_get_color(style, STYLE_ID_BORDER_COLOR, trans);
  color_t text_color = style_get_color(style, STYLE_ID_TEXT_COLOR, trans);
  int32_t font_size = style_get_int(style, STYLE_ID_FONT_SIZE, TK_DEFAULT_FONT_SIZE);
  const char* font = style_get_str(style, STYLE_ID_FONT_NAME, NULL);
  int32_t wox = style_get_int(style, STYLE_ID_X_OFFSET, 0);
  int32_t woy = style_get_int(style, STYLE_ID_Y_OFFSET, 0);
  int32_t round_r = style_get_int(style, STYLE_ID_ROUND_RADIUS, 0);
  ...

样式处理方面,AWTK略胜一筹。

看了下公司几个同事的QT项目,是直接自定义控件内setStyle定死字符串大法上去的,最后我也只好QLabel子控件+setStyleSheet大法了,没办法,到时候全局改样式时IDE能搜索到相关字符就行。

void MyRangeSlider::paintEvent(QPaintEvent *event)
{
	...
	mValueLabel1.setGeometry(mDragger1.x() + (mDraggerWidth - mValueLabelWidth) / 2,
                             mDragger1.y() + mDraggerHeight,
                             mValueLabelWidth,
                             mValueLabelHeight);
    mValueLabel2.setGeometry(mDragger2.x() + (mDraggerWidth - mValueLabelWidth) / 2,
                             mDragger2.y() + mDraggerHeight,
                             mValueLabelWidth,
                             mValueLabelHeight);

    mValueLabel1.setText(QString::number(mValue1, 'f', 0));
    mValueLabel2.setText(QString::number(mValue2, 'f', 0));

    const QString defaultLabelStyle = "QLabel{border: 2px solid #000000;}";
    const QString focusedLabelStyle = "QLabel{border: 2px solid #00ffff;}";
    mValueLabel1.setStyleSheet(mDragger1.hasFocus() ? focusedLabelStyle : defaultLabelStyle);
    mValueLabel2.setStyleSheet(mDragger2.hasFocus() ? focusedLabelStyle : defaultLabelStyle);
}

综上,这个控件迭代三次,踩了3个坑,这些坑如果我没有动手去综合实践,就不会知道,就算有AI能够立刻解答,但是由于对QT框架本身的不熟悉,可能我会在当中卡几个小时才会发现问题的本源在哪,这也是我写此篇学习记录的意义。

参考

Cursor GPT-5.3 Codex [AI]

Qt5.10 GUI完全参考手册

QT事件过滤器(QT事件处理的5个层次:自己覆盖或过滤,父窗口过滤,Application过滤与通知)

posted @ 2026-03-24 22:01  另一种开始  阅读(1)  评论(0)    收藏  举报