【Qt自定义控件】思路详解(“滑动开关”为例)

先上效果:
滑动开关

一、控件功能介绍

滑动开关有以下功能:

  • 可更改背景颜色和滑块背景色;
  • 可设置是否显示提示光以及提示光颜色。
  • 可以设置圆角半径;
  • 可设置是否开启动画;
  • 可设置动画效果的速度。

好了,介绍完毕(真介绍完了啊)~~

二、实现思路

跟着我的思路来走,自定义控件不再有难度!

记得抓住这三点来想:有什么功能,怎么实现,什么时机去实现

  1. 根据功能,确定所有成员变量(例如是否开启动画是否显示提示光等等)。

  2. 根据功能,确定所有 setget 函数(注意,只是确定,写好声明,先不需要实现,当然 get 类函数如果只是返回成员变量,也可先实现),每一对函数都要对应至少一个成员变量。

  3. 确定所有静态属性(通过 Q_PROPERTY 设置)。

  4. 声明并定义 paintEvent 函数,编写最基本的函数体,就像这样:

    void SlideSwitch::paintEvent(QPaintEvent *event)
    {
        Q_UNUSED(event);
    
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing);
    	
    	// TODO: 以下分模块函数进行绘制
    }
    

    就如上面代码所示,把基本的函数体写出来,绘制部分先用一个 TODO 表示,不用着急着想怎么画。

  5. 确定绘制的模块,这里我分成了三个模块:开关背景开关滑块提示光注意,只是确定,不用着急想着所有模块怎么实现。

  6. 从第一个绘制模块开始实现。实现模块过程中,逐步丰富工具类辅助类函数(指的不是 setget 这类)。

  7. setget 函数进行实现。

  8. 加入动画,这里用定时器实现,声明和实现响应函数。

  9. 声明和实现鼠标事件函数(其实这一步通常也不用放到最后,但此处放到最后可以起到“清晰思路”的效果:先实现功能和显示,再实现操作响应)。

  10. 优化和完善。

三、详细步骤

1. 确定成员变量

根据我们制定的控件功能,可以得出需要这些成员变量:选中标志背景颜色滑块颜色是否显示提示光提示光颜色提示光选中颜色提示光当前颜色圆角半径是否开启动画动画时间间隔动画定时器

另外,我们还需要一个记录滑块位置的变量,这里我打算记录滑块的X坐标

深入思考,动画部分还需要一个步长值,这样能更精细化控制动画效果。

综上,我们可以得出以下成员变量:

bool m_isAnimated;  ///< 是否开启动画
bool m_isShowTip;   ///< 是否显示提示光

bool m_isChecked;   ///< 是否选中

QColor m_bgColor;               ///< 背景颜色
QColor m_sliderColor;           ///< 滑块颜色
QColor m_tipLightColor;         ///< 提示光颜色
QColor m_tipLightCheckColor;    ///< 提示光选中颜色
QColor m_tipLightCurrentColor;  ///< 提示光当前颜色

qreal m_radius;  ///< 圆弧半径

int m_interval;   ///< 间隔
QTimer *m_timer;  ///< 动画定时器

int m_sliderX;  ///< 滑块X坐标
int m_step;     ///< 步长

2. 确定 setget 函数

经过第1步后,我们已经对控件的属性有了大概的轮廓,这里我就直接列出一些相关的 setget 函数:

Setter functions:

/// 设置是否选中
void setChecked(bool checked);
/// 设置是否显示提示光
void setShowTip(bool showTip);
/// 设置背景色
void setBgColor(const QColor &color);
/// 设置滑块颜色
void setSliderColor(const QColor &color);
/// 设置提示光颜色
void setTipLightColor(const QColor &color);
/// 设置提示光选中颜色
void setTipLightCheckColor(const QColor &color);
/// 设置圆弧半径
void setRadius(qreal radius);
/// 设置是否开启动画
void setAnimated(bool animated);
/// 设置动画速度(毫秒)
void setAnimationInterval(int interval);

Getter Functions:

/// 获取是否选中
[[nodiscard]] bool getIsChecked() const { return m_isChecked; }
/// 获取是否开启动画
[[nodiscard]] bool getIsAnimated() const { return m_isAnimated; }
/// 获取是否显示提示光
[[nodiscard]] bool getIsShowTip() const { return m_isShowTip; }
/// 获取背景色
[[nodiscard]] QColor getBgColor() const { return m_bgColor; }
/// 获取滑块颜色
[[nodiscard]] QColor getSliderColor() const { return m_sliderColor; }
/// 获取提示光颜色
[[nodiscard]] QColor getTipLightColor() const { return m_tipLightColor; }
/// 获取提示光选中颜色
[[nodiscard]] QColor getTipLightCheckColor() const { return m_tipLightCheckColor; }
/// 获取圆弧半径
[[nodiscard]] qreal getRadius() const { return m_radius; }
/// 获取动画速度(毫秒)
[[nodiscard]] int getAnimationInterval() const { return m_interval; }

基本上 setget 函数都是一一对应的。

你可能发现了,并非所有的成员变量都需要有对应的 setget 函数对,因为有部分成员变量是辅助变量或者是不打算对外开放变量

3. 确定静态属性

这一步比较简单,其实就是把第2步的函数与属性关联起来,已实现通过Qt的 setPropertyproperty 函数进行操作。代码我直接给出:

Q_PROPERTY(bool isChecked READ getIsChecked WRITE setChecked)
Q_PROPERTY(bool isAnimated READ getIsAnimated WRITE setAnimated)
Q_PROPERTY(bool isShowTip READ getIsShowTip WRITE setShowTip)
Q_PROPERTY(QColor bgColor READ getBgColor WRITE setBgColor)
Q_PROPERTY(QColor sliderColor READ getSliderColor WRITE setSliderColor)
Q_PROPERTY(QColor tipLightColor READ getTipLightColor WRITE setTipLightColor)
Q_PROPERTY(QColor tipLightCheckColor READ getTipLightCheckColor WRITE setTipLightCheckColor)
Q_PROPERTY(qreal radius READ getRadius WRITE setRadius)
Q_PROPERTY(int animationInterval READ getAnimationInterval WRITE setAnimationInterval)

就拿 Q_PROPERTY(bool isChecked READ getIsChecked WRITE setChecked) 作为示例:bool 为数据类型,isChecked 为属性名称,READ 表示接下来的 getIsChecked 是读取操作,WRITE 表示接下来的 setChecked 是写入操作。

4. paintEvent 函数声明和定义

声明很简单:

protected:
    void paintEvent(QPaintEvent *event) override;

定义在第二大点就已经写了,这里拷贝一下:

void SlideSwitch::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);

    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    
    // TODO: 以下分模块函数进行绘制
}

5. 确定绘制模块

我们来思考一下,这个控件由什么东西组成:

  • 滑块开关背景
  • 滑块
  • 滑块上面的提示光

思考完毕,列出这三个绘制函数:

private:
    void drawBg(QPainter *painter);
    void drawSlider(QPainter *painter);
    void drawTipLight(QPainter *painter);

然后每一个函数都提供空白实现:

void SlideSwitch::drawBg(QPainter *painter)
{
}

void SlideSwitch::drawSlider(QPainter *painter)
{
}

void SlideSwitch::drawTipLight(QPainter *painter)
{
}

这里解释一下为什么提供空白实现:空白实现可以确保我们程序能成功运行,而不至于等到我们把所有功能都实现完毕才能对程序进行调试。

此时,我们回到第4点编写的 paintEvent 函数定义,对其进行完善:

void SlideSwitch::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);

    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    drawBg(&painter);
    drawSlider(&painter);
    drawTipLight(&painter);
}

好了,paintEvent 函数就实现完毕了,后面的开发管都不用管它了

6. 绘制函数逐一实现

drawBg 函数

我们能想到背景绘制有以下几点:

  • 背景颜色
  • 背景形状
    • 矩形
    • 圆角

想好了,开始实现:

void SlideSwitch::drawBg(QPainter *painter)
{
    painter->save();

    painter->setPen(Qt::NoPen);
    painter->setBrush(m_bgColor);

    painter->drawRoundedRect(rect(), m_radius, m_radius);

    painter->restore();
}

Qt 绘制中最好养成习惯,绘制函数应该使用 painter->save();painter->restore(); 对绘制操作进行包裹,以避免绘制过程中对 painter 的修改影响到其他绘制函数

没什么难度,直接用 drawRoundedRect 函数进行圆角矩形的绘制。

drawSlider 函数

我们继续思考滑块的属性:

  • 颜色
  • 尺寸(宽度、高度)
  • 位置

貌似就这么多了,开始进行实现:

  1. 颜色:

    painter->setPen(Qt::NoPen);
    painter->setBrush(m_sliderColor);
    

    没什么好说的。

  2. 得出当前滑块的尺寸,我们需要想到滑块不能跟背景的高度一致,否则滑块就完全挡住背景,就没有那种立体效果了。

    这里我们用一个宏,用来设定滑块上下边距(为什么用宏?因为我没打算让用户更改):

    #define PADDING_TOP_BOTTOM 2  ///< 上下内间距
    

    有上下,也就有左右——滑块靠到最左边或者最右边时都不应该与背景的边距重合:

    #define PADDING_LEFT_RIGHT 2  ///< 左右内间距
    

    由此我们可以得出以下尺寸获取代码:

    qreal width  = this->width() / 2.0 - PADDING_LEFT_RIGHT * 2.0;
    qreal height = this->height() - PADDING_TOP_BOTTOM * 2.0;
    
  3. 我们需要根据滑块当前所处的位置对滑块进行绘制,我发现 drawRoundedRect 函数可以设置 x、y 值,那直接用起来:

    painter->drawRoundedRect(m_sliderX, PADDING_TOP_BOTTOM, (int)width, (int)height, m_radius, m_radius);
    

完整代码如下:

void SlideSwitch::drawSlider(QPainter *painter)
{
    painter->save();

    painter->setPen(Qt::NoPen);
    painter->setBrush(m_sliderColor);

    qreal width  = this->width() / 2.0 - PADDING_LEFT_RIGHT * 2.0;
    qreal height = this->height() - PADDING_TOP_BOTTOM * 2.0;

    painter->drawRoundedRect(m_sliderX, PADDING_TOP_BOTTOM, (int)width, (int)height, m_radius, m_radius);

    painter->restore();
}

建议看了我的实现思路再去看完整代码,我们要的不是实现,而是产生这个实现的思路

drawTipLight 函数

继续思考属性:

  • 颜色
  • 形状(不管了,圆形)
  • 尺寸
  • 位置

貌似也没了,接下来开始实现:

  1. 颜色:

    painter->setPen(Qt::NoPen);
    painter->setBrush(m_tipLightCurrentColor);
    

    我们需要知道当前应该是绿色还是红色吗?不必多想,你就当作 m_tipLightCurrentColor 是别人给的就行了,我们只是负责把 m_tipLightCurrentColor 颜色画出来了。

  2. 尺寸:

    qreal width  = this->height() / 4.0 - PADDING_LEFT_RIGHT;
    qreal height = width;
    

    自己看着来写吧,我这里是通过计算得来的,你们想绘制成其他样子可以自己重写表达式。

  3. 对于提示光的位置,y 直接相对于滑块居中,x 定在了 2 3 \frac {2} {3} 32 1 3 \frac {1} {3} 31 的位置:当滑块在左边时,提示光位于滑块的 2 3 \frac {2} {3} 32 处,当滑块在右边时,提示光位于滑块的 1 3 \frac {1} {3} 31 处。

    那当滑块在中间时呢?那滑块在移动过程中呢?这就需要我们按比例进行计算了。

    既然要按照比例进行计算,我们就要得出提示光和滑块的移动宽度比例,数学思路我就不写了(这里不是算法教程),直接上代码:

    qreal xStart  = this->width() / 3.0 - width / 2 - PADDING_LEFT_RIGHT;
    qreal xEnd    = this->width() / 3.0 * 2 - width / 2 - PADDING_LEFT_RIGHT;
    qreal len     = xEnd - xStart;
    qreal percent = len / (this->width() / 2.0 - PADDING_LEFT_RIGHT * 2);
    

    大家别忘了 PADDING_LEFT_RIGHT 代表的是什么意思。

    得到比例后,获取位置就很容易了:

    qreal x = xStart + m_sliderX * percent;
    qreal y = this->height() / 2.0 - height / 2.0;
    
  4. 万事俱备,只欠东风,最后一步,画出来,我们用 drawEllipse 函数:

    painter->drawEllipse((int)x, (int)y, (int)width, (int)height);
    

完整代码:

void SlideSwitch::drawTipLight(QPainter *painter)
{
    if (!m_isShowTip) return;

    painter->save();

    painter->setPen(Qt::NoPen);
    painter->setBrush(m_tipLightCurrentColor);

    qreal width  = this->height() / 4.0 - PADDING_LEFT_RIGHT;
    qreal height = width;

    qreal xStart  = this->width() / 3.0 - width / 2 - PADDING_LEFT_RIGHT;
    qreal xEnd    = this->width() / 3.0 * 2 - width / 2 - PADDING_LEFT_RIGHT;
    qreal len     = xEnd - xStart;
    qreal percent = len / (this->width() / 2.0 - PADDING_LEFT_RIGHT * 2);

    qreal x = xStart + m_sliderX * percent;
    qreal y = this->height() / 2.0 - height / 2.0;
    painter->drawEllipse((int)x, (int)y, (int)width, (int)height);

    painter->restore();
}

7. 实现 setget 函数

对于这一步为什么放这里,可能每个人的想法有所不同,我的想法是:在实现绘制操作时,有可能会添加一些辅助性的成员变量,这下变量有可能要和某些表达属性的成员变量相关联。如果在绘制函数实现之前就实现 set 函数,可能会导致后面忘记更改 set 函数,从而导致功能异常(有些功能异常极其难发现,特别是设计框架、开发复杂程序时,所以思路明确异常重要,不要想到什么就写什么,这样很容易导致后期功能出现很多隐藏问题)。

在本控件中,get 函数基本上不用怎么管,直接是返回成员变量即可。

以下是 set 函数实现:

void SlideSwitch::setChecked(bool checked)
{
    m_isChecked = checked;
    update();
}

void SlideSwitch::setShowTip(bool showTip)
{
    m_isShowTip = showTip;
    update();
}

void SlideSwitch::setBgColor(const QColor &color)
{
    m_bgColor = color;
    update();
}

void SlideSwitch::setSliderColor(const QColor &color)
{
    m_sliderColor = color;
    update();
}

void SlideSwitch::setTipLightColor(const QColor &color)
{
    m_tipLightColor = color;
    update();
}

void SlideSwitch::setTipLightCheckColor(const QColor &color)
{
    m_tipLightCheckColor = color;
    update();
}

void SlideSwitch::setRadius(qreal radius)
{
    m_radius = radius;
    update();
}

void SlideSwitch::setAnimated(bool animated)
{
    m_isAnimated = animated;
}

void SlideSwitch::setAnimationInterval(int interval)
{
    m_interval = interval;
}

不过多解释这块,都比较简单。

8. 动画实现

我们通过定时器来实现动画。我们先假设所有功能都已经完成了,就剩下定时器函数待实现。

首先,我们要想到动画几种,这里很明显是两种:“滑块向左”和“滑块向右”。这样我们就定下了整体框架:

if (m_isChecked) {
    // 滑块向左
}
else {
    // 滑块向右
}

我们挑一个部分来实现,这里我挑“滑块向左”——

试想一下,当我们滑块向左一步,就应该加一次步数,当走到最后一步时,为确保不会走超过控件大小,因此我们只会走完实际剩下的长度,而不是固执地加一次步数。

这样,我们就能得出以下代码:

if (m_sliderX < width() / 2 + PADDING_LEFT_RIGHT) {
    m_sliderX += m_step;
}
else {
    m_sliderX = width() / 2 + PADDING_LEFT_RIGHT;
}

当走到了最后一步时,我们还需要进行提示光颜色的更改以及关闭这个定时器(一直开着定时器会非常消耗资源),因此代码需要进一步完善:

if (m_sliderX < width() / 2 + PADDING_LEFT_RIGHT) {
    m_sliderX += m_step;
}
else {
    m_sliderX              = width() / 2 + PADDING_LEFT_RIGHT;
    m_tipLightCurrentColor = m_tipLightCheckColor;
    m_timer->stop();
}

现在,我们照着写“滑块向右”部分:

if (m_sliderX > PADDING_LEFT_RIGHT) {
    m_sliderX -= m_step;
}
else {
    m_sliderX              = PADDING_LEFT_RIGHT;
    m_tipLightCurrentColor = m_tipLightColor;
    m_timer->stop();
}

整个定时器函数就设计完毕,完整代码如下:

void SlideSwitch::animateTimer()
{
    if (m_isChecked) {
        if (m_sliderX < width() / 2 + PADDING_LEFT_RIGHT) {
            m_sliderX += m_step;
        }
        else {
            m_sliderX              = width() / 2 + PADDING_LEFT_RIGHT;
            m_tipLightCurrentColor = m_tipLightCheckColor;
            m_timer->stop();
        }
    }
    else {
        if (m_sliderX > PADDING_LEFT_RIGHT) {
            m_sliderX -= m_step;
        }
        else {
            m_sliderX              = PADDING_LEFT_RIGHT;
            m_tipLightCurrentColor = m_tipLightColor;
            m_timer->stop();
        }
    }

    update();
}

9. 鼠标事件函数实现

经过了上面那些步骤,我们的控件的功能已经成型了,接下来就是实现控件功能触发部分(这里我只实现了鼠标触发,大家喜欢的话可以自己实现其他的触发)。

既然是开关,我们就要模拟“点击”操作,这里我们需要实现三个函数——mousePressEventmouseMoveEventmouseReleaseEvent

实现“点击”操作,我们需要对鼠标按下标记进行记录;实现在合适区域才响应“点击”操作,我们需要有一个是否可以选中标记(这个标记起名有点不合适,准确来说应该是是否处于响应区域才对,但我懒得改了)。

我们先来实现 mousePressEvent

void SlideSwitch::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        m_isPressed    = true;
        m_isCanChecked = true;
    }

    QWidget::mousePressEvent(event);
}

然后实现 mouseMoveEvent

void SlideSwitch::mouseMoveEvent(QMouseEvent *event)
{
    if (!m_isPressed) return;

    QRect rect = this->rect();
    rect.adjust(PADDING_LEFT_RIGHT, PADDING_TOP_BOTTOM, -PADDING_LEFT_RIGHT, -PADDING_TOP_BOTTOM);
    m_isCanChecked = rect.contains(event->pos());
}

很容易看出,5 - 7 这三句代码,目的就是判断是否处于响应区域的。

接下来是 mouseReleaseEvent 函数。这个函数的逻辑有点长,因为它是响应”点击“的最后一个流程。它需要进行以下判断:

  • 是否处于响应区域
  • 确定是左键松开
  • 确定当前是否开启了动画
  • 确定当前选中状态

我们可以得出一个大致的框架:

if (event->button() == Qt::LeftButton && m_isCanChecked)
{
    if (m_isAnimated) {
        if (m_isChecked) {
            // 有动画,并且原状态为checked
        }
        else {
            // 有动画,并且原状态为unchecked
        }
    }
    else {
        if (m_isChecked) {
            // 无动画,并且原状态为checked
        }
        else {
            // 无动画,并且原状态为unchecked
        }

        update();
    }

    emit checkedChanged(m_isChecked);
}

然后我们来一块一块编写,我们以 有动画,并且原状态为checked无动画,并且原状态为checked 为例子。

既然是有动画,那么我们就应该开启定时器,所以代码就这么简单:

m_isChecked = false;
m_timer->start(m_interval);

无动画,那么就要直接改变滑块位置以及滑块的颜色,代码如下:

m_isChecked            = false;
m_sliderX              = PADDING_LEFT_RIGHT;
m_tipLightCurrentColor = m_tipLightColor;

另外两处思路相同,这里直接给出 mouseReleaseEvent 函数完整代码:

void SlideSwitch::mouseReleaseEvent(QMouseEvent *event)
{
    m_isPressed = false;

    if (event->button() == Qt::LeftButton && m_isCanChecked)
    {
        if (m_isAnimated) {
            if (m_isChecked) {
                m_isChecked = false;
                m_timer->start(m_interval);
            }
            else {
                m_isChecked = true;
                m_timer->start(m_interval);
            }
        }
        else {
            if (m_isChecked) {
                m_isChecked            = false;
                m_sliderX              = PADDING_LEFT_RIGHT;
                m_tipLightCurrentColor = m_tipLightColor;
            }
            else {
                m_isChecked            = true;
                m_sliderX              = width() / 2 + PADDING_LEFT_RIGHT;
                m_tipLightCurrentColor = m_tipLightCheckColor;
            }

            update();
        }

        emit checkedChanged(m_isChecked);
    }

    QWidget::mouseReleaseEvent(event);
}

10. 优化和完善

至此,控件已经开发完毕,那有没有优化的地方呢?当然有!

我们可以看到 mouseReleaseEvent 函数的实现过多的参与了一些属性的改动,但同类型的属性改动又并不仅仅存在于 mouseReleaseEvent 函数中,在 animateTimer 函数中也有一部分。基于模块化思想,我们应该把那些相同功能的操作“抽离”出来(例如更改提示光的属性),这样对以后复用和维护都有非常大的帮助。

当然还有其他待优化的地方,大家可以自己去思考,去完善。

四、完整代码

slideswitch.h

#ifndef SLIDESWITCH_H
#define SLIDESWITCH_H

#include <QWidget>

/**
 * @brief   滑动开关控件
 * @details 一个自定义的实用性滑动开关,其功能如下:\n
 * -# 可设置背景颜色和滑块背景色;
 * -# 可设置是否显示提示光以及提示光颜色。
 * -# 可以设置圆角半径;
 * -# 可设置是否开启动画;
 * -# 可设置动画效果的速度;
 */
class SlideSwitch : public QWidget
{
    Q_OBJECT

    Q_PROPERTY(bool isChecked READ getIsChecked WRITE setChecked)
    Q_PROPERTY(bool isAnimated READ getIsAnimated WRITE setAnimated)
    Q_PROPERTY(bool isShowTip READ getIsShowTip WRITE setShowTip)
    Q_PROPERTY(QColor bgColor READ getBgColor WRITE setBgColor)
    Q_PROPERTY(QColor sliderColor READ getSliderColor WRITE setSliderColor)
    Q_PROPERTY(QColor tipLightColor READ getTipLightColor WRITE setTipLightColor)
    Q_PROPERTY(QColor tipLightCheckColor READ getTipLightCheckColor WRITE setTipLightCheckColor)
    Q_PROPERTY(qreal radius READ getRadius WRITE setRadius)
    Q_PROPERTY(int animationInterval READ getAnimationInterval WRITE setAnimationInterval)

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

    // Setter functions
    /// 设置是否选中
    void setChecked(bool checked);
    /// 设置是否显示提示光
    void setShowTip(bool showTip);
    /// 设置背景色
    void setBgColor(const QColor &color);
    /// 设置滑块颜色
    void setSliderColor(const QColor &color);
    /// 设置提示光颜色
    void setTipLightColor(const QColor &color);
    /// 设置提示光选中颜色
    void setTipLightCheckColor(const QColor &color);
    /// 设置圆弧半径
    void setRadius(qreal radius);
    /// 设置是否开启动画
    void setAnimated(bool animated);
    /// 设置动画速度(毫秒)
    void setAnimationInterval(int interval);

    // Getter functions
    /// 获取是否选中
    [[nodiscard]] bool getIsChecked() const { return m_isChecked; }
    /// 获取是否开启动画
    [[nodiscard]] bool getIsAnimated() const { return m_isAnimated; }
    /// 获取是否显示提示光
    [[nodiscard]] bool getIsShowTip() const { return m_isShowTip; }
    /// 获取背景色
    [[nodiscard]] QColor getBgColor() const { return m_bgColor; }
    /// 获取滑块颜色
    [[nodiscard]] QColor getSliderColor() const { return m_sliderColor; }
    /// 获取提示光颜色
    [[nodiscard]] QColor getTipLightColor() const { return m_tipLightColor; }
    /// 获取提示光选中颜色
    [[nodiscard]] QColor getTipLightCheckColor() const { return m_tipLightCheckColor; }
    /// 获取圆弧半径
    [[nodiscard]] qreal getRadius() const { return m_radius; }
    /// 获取动画速度(毫秒)
    [[nodiscard]] int getAnimationInterval() const { return m_interval; }

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

    void animateTimer();

signals:
    void checkedChanged(bool checked);

private:
    void drawBg(QPainter *painter);
    void drawSlider(QPainter *painter);
    void drawTipLight(QPainter *painter);

private:
    bool m_isAnimated;  ///< 是否开启动画
    bool m_isShowTip;   ///< 是否显示提示光

    bool m_isPressed;     ///< 是否按下
    bool m_isCanChecked;  ///< 是否可以选中
    bool m_isChecked;     ///< 是否选中

    QColor m_bgColor;               ///< 背景颜色
    QColor m_sliderColor;           ///< 滑块颜色
    QColor m_tipLightColor;         ///< 提示光颜色
    QColor m_tipLightCheckColor;    ///< 提示光选中颜色
    QColor m_tipLightCurrentColor;  ///< 提示光当前颜色

    qreal m_radius;  ///< 圆弧半径

    int m_interval;   ///< 间隔
    QTimer *m_timer;  ///< 动画定时器

    int m_sliderX;  ///< 滑块X坐标
    int m_step;     ///< 步长
};

#endif  // SLIDESWITCH_H

slideswitch.cpp

#include <QLinearGradient>
#include <QMouseEvent>
#include <QPainter>
#include <QTimer>
#include "slideswitch.h"

#define PADDING_LEFT_RIGHT 2  ///< 左右内间距
#define PADDING_TOP_BOTTOM 2  ///< 上下内间距

SlideSwitch::SlideSwitch(QWidget *parent) :
    QWidget(parent),
    m_isAnimated(true),
    m_isShowTip(true),
    m_isPressed(false),
    m_isCanChecked(true),
    m_isChecked(false),
    m_bgColor(QColor(80, 80, 80)),
    m_sliderColor(QColor(255, 255, 255)),
    m_tipLightColor(QColor(255, 0, 0)),
    m_tipLightCheckColor(QColor(0, 255, 0)),
    m_tipLightCurrentColor(m_tipLightColor),
    m_radius(0.0),
    m_interval(10),
    m_timer(new QTimer(this)),
    m_sliderX(2),
    m_step(5)
{
    connect(m_timer, &QTimer::timeout, this, &SlideSwitch::animateTimer);

    setBaseSize(50, 30);  // 设置默认大小
}


SlideSwitch::~SlideSwitch()
{
}


void SlideSwitch::setChecked(bool checked)
{
    m_isChecked = checked;
    update();
}


void SlideSwitch::setShowTip(bool showTip)
{
    m_isShowTip = showTip;
    update();
}


void SlideSwitch::setBgColor(const QColor &color)
{
    m_bgColor = color;
    update();
}


void SlideSwitch::setSliderColor(const QColor &color)
{
    m_sliderColor = color;
    update();
}


void SlideSwitch::setTipLightColor(const QColor &color)
{
    m_tipLightColor = color;
    update();
}


void SlideSwitch::setTipLightCheckColor(const QColor &color)
{
    m_tipLightCheckColor = color;
    update();
}


void SlideSwitch::setRadius(qreal radius)
{
    m_radius = radius;
    update();
}


void SlideSwitch::setAnimated(bool animated)
{
    m_isAnimated = animated;
}


void SlideSwitch::setAnimationInterval(int interval)
{
    m_interval = interval;
}


void SlideSwitch::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event);

    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);

    drawBg(&painter);
    drawSlider(&painter);
    drawTipLight(&painter);
}


void SlideSwitch::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        m_isPressed    = true;
        m_isCanChecked = true;
    }

    QWidget::mousePressEvent(event);
}


void SlideSwitch::mouseMoveEvent(QMouseEvent *event)
{
    if (!m_isPressed) return;

    QRect rect = this->rect();
    rect.adjust(PADDING_LEFT_RIGHT, PADDING_TOP_BOTTOM, -PADDING_LEFT_RIGHT, -PADDING_TOP_BOTTOM);
    m_isCanChecked = rect.contains(event->pos());
}


void SlideSwitch::mouseReleaseEvent(QMouseEvent *event)
{
    m_isPressed = false;

    if (event->button() == Qt::LeftButton && m_isCanChecked)
    {
        if (m_isAnimated) {
            if (m_isChecked) {
                m_isChecked = false;
                m_timer->start(m_interval);
            }
            else {
                m_isChecked = true;
                m_timer->start(m_interval);
            }
        }
        else {
            if (m_isChecked) {
                m_isChecked            = false;
                m_sliderX              = PADDING_LEFT_RIGHT;
                m_tipLightCurrentColor = m_tipLightColor;
            }
            else {
                m_isChecked            = true;
                m_sliderX              = width() / 2 + PADDING_LEFT_RIGHT;
                m_tipLightCurrentColor = m_tipLightCheckColor;
            }

            update();
        }

        emit checkedChanged(m_isChecked);
    }

    QWidget::mouseReleaseEvent(event);
}


void SlideSwitch::animateTimer()
{
    if (m_isChecked) {
        if (m_sliderX < width() / 2 + PADDING_LEFT_RIGHT) {
            m_sliderX += m_step;
        }
        else {
            m_sliderX              = width() / 2 + PADDING_LEFT_RIGHT;
            m_tipLightCurrentColor = m_tipLightCheckColor;
            m_timer->stop();
        }
    }
    else {
        if (m_sliderX > PADDING_LEFT_RIGHT) {
            m_sliderX -= m_step;
        }
        else {
            m_sliderX              = PADDING_LEFT_RIGHT;
            m_tipLightCurrentColor = m_tipLightColor;
            m_timer->stop();
        }
    }

    update();
}


void SlideSwitch::drawBg(QPainter *painter)
{
    painter->save();

    painter->setPen(Qt::NoPen);
    painter->setBrush(m_bgColor);

    painter->drawRoundedRect(rect(), m_radius, m_radius);

    painter->restore();
}


void SlideSwitch::drawSlider(QPainter *painter)
{
    painter->save();

    painter->setPen(Qt::NoPen);
    painter->setBrush(m_sliderColor);

    qreal width  = this->width() / 2.0 - PADDING_LEFT_RIGHT * 2.0;
    qreal height = this->height() - PADDING_TOP_BOTTOM * 2.0;

    painter->drawRoundedRect(m_sliderX, PADDING_TOP_BOTTOM, (int)width, (int)height, m_radius, m_radius);

    painter->restore();
}


void SlideSwitch::drawTipLight(QPainter *painter)
{
    if (!m_isShowTip) return;

    painter->save();

    painter->setPen(Qt::NoPen);
    painter->setBrush(m_tipLightCurrentColor);

    qreal width  = this->height() / 4.0 - PADDING_LEFT_RIGHT;
    qreal height = width;

    qreal xStart  = this->width() / 3.0 - width / 2 - PADDING_LEFT_RIGHT;
    qreal xEnd    = this->width() / 3.0 * 2 - width / 2 - PADDING_LEFT_RIGHT;
    qreal len     = xEnd - xStart;
    qreal percent = len / (this->width() / 2.0 - PADDING_LEFT_RIGHT * 2);

    qreal x = xStart + m_sliderX * percent;
    qreal y = this->height() / 2.0 - height / 2.0;
    painter->drawEllipse((int)x, (int)y, (int)width, (int)height);

    painter->restore();
}

五、个人小忠告

我理解有的小伙伴可能会认为,在日常开发中过度深入思考可能显得有些多余,甚至有时会感觉它会延缓我们的开发节奏。但大家有没有想过,真正拖慢进度的是你的思路还是你产生思路的时间呢?

对于一些较小的项目,采用深思熟虑的方法进行开发可能看似没有明显成效,甚至给人一种画蛇添足的感觉。但若没有培养这种思维习惯,当我们面对更大的挑战,如框架的构建或软件架构的设计时,缺乏周全的考虑可能会导致成果中存在诸多疏漏,进而使得后续的维护工作变得异常复杂。稳健的框架需要我们在构建时考虑周全,否则诸如遗忘初始化变量忽视对特定区域的限制未能清晰定义不同功能之间的关系等问题可能会严重影响其稳定性。

在本项目中,我并未应用复杂的设计模式或高深的软件设计概念,最多仅采用了简单的模块化开发方法。我注意到有些小伙伴热衷于追求各种设计理念,但却可能忽视了最基本的能力——形成清晰、逻辑性强的思路。这样做有时候会陷入为使用某个概念而使用它的境地。我希望大家能先培养起扎实的程序设计思维,再逐渐深入学习更多的软件设计概念,这样可能对个人技能的提升更为有益。

这些仅仅是我个人的一点建议,每个人的情况都不相同,所以大家可以根据自己的实际情况进行取舍。这只是我一家之言,未必全然正确,小伙伴们可以自行判断。如果觉得我说的有不对的地方,请多多指教,我一定虚心接受~~~~

  • 27
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Qt自定义控件大全Designer源码是一个包含了多种自定义控件的全套源码,它主要用于在Qt设计师中使用。Qt是一个跨平台的C++应用程序开发框架,提供了丰富的图形界面控件,但有时候我们可能需要自定义一些特殊的控件来满足我们的需求。 Qt自定义控件大全Designer源码包含了很多常用的自定义控件,如自定义按钮、进度条、滑块、验证码输入框等等。这些控件的设计和实现已经封装好,我们只需要将源码添加到我们的项目中,然后在Qt设计师中直接使用即可。 使用Qt自定义控件大全Designer源码有以下几个优点: 1.提供了丰富的自定义控件选择:Qt自定义控件大全Designer源码包含了多样化的控件,可以满足不同项目的需求。无论是一些简单的控件,还是一些复杂的控件,我们都可以找到合适的选择。 2.减少开发时间和工作量:使用源码中的自定义控件可以减少我们从头开始设计和实现的工作,节省了大量的开发时间和工作量。我们只需要将源码添加到项目中并正确配置,就可以直接在设计师中使用这些自定义控件。 3.提高应用程序的美观性和用户体验:Qt自定义控件大全Designer源码中的控件经过精心设计和实现,具有良好的界面效果和用户交互体验。使用这些自定义控件可以为我们的应用程序提供更加美观和友好的界面。 总之,Qt自定义控件大全Designer源码是一个提供了多种自定义控件的全套源码,使用它可以快速、方便地实现各种自定义控件,提高应用程序的开发效率和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xiao_Ley

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值