《Qt 自定义控件设计:模拟 QCheckBox 的动效开关按钮》


前言(简介)

  • 提示:本设计使用的是【Qt4 设计师自定义控件】,这样可以集成到Qt Desinger中,不知道这项目模板的可以跳转此处
  • 背景:使用自带QCheckBox时,觉得不够好看,如果直接改变qss又觉得调整不到自己想要的效果,于是有了本篇讲述的CtmSwitchButton。
  • 目标:设计出一个可以发出on/off基本信号的按钮,并且实现一些小动画和自定义属性(颜色)

实现效果

在这里插入图片描述
在这里插入图片描述

考虑到通用性,我们的颜色是可以自定义的

在这里插入图片描述

再看看我们添加颜色切换的效果

请添加图片描述

最后是槽函数
其中最重要的就是我们的checked(bool),这是我们设计的初衷。

在这里插入图片描述

设计思路

  1. 需要用到哪些类
  2. 动画效果要怎么实现
  3. 需要哪些属性

想要直观地思考这些问题,直接画一幅草图就好了。
在这里插入图片描述
这图是我们设计的原型(确实草)

看来我们需要

  • 用一个大边框包住一个小边框
  • 大边框作为按钮边界
  • 小边框的位置则代表按钮on/off状态
  • 小边框需要可以左右移动

继续,那大边框用什么做?小边框呢?

首先大边框肯定用QWidget最好,因为QWidget最干净,也适合做绘制和做父类
小边框可以有两种选择:

  1. 直接使用大边框的绘图事件画出来
  2. 再写一个QWidget类作为成员变量

权衡之下,我选择了第二种,因为它的封装性更好,低耦合。

那么现在图就是这样:
在这里插入图片描述
为了提高视觉效果,我们可以把边框变成曲线,曲线自带柔和效果,不像直角那样让人觉得锋利。

所以可以改成这样:
在这里插入图片描述
这个就是我们最终的设计效果,接下来我们只要实现:颜色过渡小球移动这两个动画,这个按钮的界面就完成了。

代码实现

由于这个控件比较小,所以只需要一个头文件和源文件就够了

头文件

  1. 我们需要两个类
class Ball : public QWidget{};
// QDESIGNER_WIDGET_EXPORT 这个宏用来标记这是一个Designer插件,可以让Qt自动识别
// 需要包含头文件 #include <QtUiPlugin/QDesignerExportWidget>
class QDESIGNER_WIDGET_EXPORT CtmSwitchButton : public QWidget{
	Ball* ball; //内部小球
};
  1. 设置属性
    • 小球(Ball
      1. 颜色color(控制动画,用户没有权限修改)
      2. 位置geometry(自带属性,控制小球移动)
    • 按钮(CtmSwitchButton
      1. 背景色(bgc, 控制动画,用户没有权限修改)
      2. 背景色Off状态(bgcOff,由用户设置动画的初态)
      3. 背景色On状态(bgcOn,由用户设置动画的终态)
      4. 小球色Off状态(ballBgcOff,由用户设置动画的初态)
      5. 小球色On状态(ballBgcOn,由用户设置动画的终态)
class Ball: public QWidget{
    Q_OBJECT

    Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
}
class QDESIGNER_WIDGET_EXPORT CtmSwitchButton:public QWidget
{
    Q_OBJECT

    // 总背景色(on状态)
    Q_PROPERTY(QColor bgcOn READ bgcOn WRITE setBgcOn NOTIFY bgcOnChanged)
    // 总背景色(off状态)
    Q_PROPERTY(QColor bgcOff READ bgcOff WRITE setBgcOff NOTIFY bgcOffChanged)
    // 小球背景色(on状态)
    Q_PROPERTY(QColor ballBgcOn READ ballBgcOn WRITE setBallBgcOn NOTIFY ballBgcOnChanged)
    // 小球背景色(off状态)
    Q_PROPERTY(QColor ballBgcOff READ ballBgcOff WRITE setBallBgcOff NOTIFY ballBgcOffChanged)


    // 总背景颜色(变化态)使用 DESIGNABLE false 不允许用户设置该值
    Q_PROPERTY(QColor bgc READ bgc WRITE setBgc NOTIFY bgcChanged DESIGNABLE false)
}
  1. 添加属性成员和动画成员和checked标记
    每设置一个属性,都需要为该属性添加一个对应的成员变量(变量名可以和属性名不同)
class Ball: public QWidget{
private:
	// checked作用:判断下一次点击时,动画是Forward还是Backword
	bool checked;
	QColor m_color;
	// 小球移动动画  、 小球背景颜色过渡动画
	QPropertyAnimation* m_slideAnimation, * m_ballBgcAnimation;
}
class QDESIGNER_WIDGET_EXPORT CtmSwitchButton : public QWidget{
private:
	Ball* ball; //内部小球
	// checked作用:1. 判断下一次点击时,动画是Forward还是Backword。
	// 				2. 点击时,发送checked(bool)信号
	bool checked;
	QColor m_bgc, m_bgcOn, m_bgcOff, m_ballBgcOn, m_ballBgcOff;
	// 背景颜色过渡动画
	QPropertyAnimation* m_bgcAnimation;
};
  1. 完善属性函数定义
class Ball : public QWidget{

public:
    QColor color(){return m_color;}
    void setColor(const QColor& color);
    
signals:
    void colorChanged(const QColor& color);
};

class QDESIGNER_WIDGET_EXPORT CtmSwitchButton : public QWidget{

public:
    QColor bgc(){return m_bgc;}
    QColor bgcOn(){return m_bgcOn;}
    QColor bgcOff(){return m_bgcOff;}
    QColor ballBgcOn(){return m_ballBgcOn;}
    QColor ballBgcOff(){return m_ballBgcOff;}

    void setBgcOn(const QColor& color);
    void setBgcOff(const QColor& color);
    void setBallBgcOn(const QColor& color);
    void setBallBgcOff(const QColor& color);
    void setBgc(const QColor& color);
    
signals:
    void bgcOnChanged(const QColor &color);
    void bgcOffChanged(const QColor &color);
    void ballBgcOnChanged(const QColor &color);
    void ballBgcOffChanged(const QColor &color);
    void bgcChanged(const QColor& color);
};
  1. 补充需要的辅助函数
class Ball : public QWidget{

public:
	// 构造函数
    Ball(QWidget* parent = nullptr);
	// 由CtmSwitchButton来设置小球动画初始颜色
    void setBallBgcOn(const QColor &color);
    // 由CtmSwitchButton来设置小球动画饥结束颜色
    void setBallBgcOff(const QColor& color);
    // 当CtmSwitchButton被点击时,触发此函数,启动动画
    void setChecked(bool checked);
    // 当CtmSwitchButton产生resizeEvent事件时,触发此函数,重置小球gemtory和移动动画属性
    void resetRect();
    
protected:
	// 绘制出小球
    void paintEvent(QPaintEvent* event) override;
};

class QDESIGNER_WIDGET_EXPORT CtmSwitchButton : public QWidget{
public:
	// 构造函数
    CtmSwitchButton(QWidget* parent = nullptr);
	// 判断按钮状态
    bool isChecked() const;
    // 按钮点击时触发:调用Ball::setChecked,发送checked(bool)信号
    void setChecked(bool checked);

signals:
	// 定义一个点击时发送的信号
    void checked(bool isChecked);

protected:
	// 绘制出按钮
    void paintEvent(QPaintEvent* e) override;
    // 强制按钮宽高比为2:1,同时调用Ball::resetRect
    void resizeEvent(QResizeEvent* e) override;
    // 模拟左键点击按钮,触发CtmSwitchButton::setChecked
    void mousePressEvent(QMouseEvent* e) override;
};
  1. 完整版头文件
#ifndef MYSWITCHBUTTON_H
#define MYSWITCHBUTTON_H


#include <QWidget>
#include <QPainter>
#include <QTimer>
#include <QMouseEvent>
#include <QPropertyAnimation>
#include <QEasingCurve>
#include <QResizeEvent>
#include <QtUiPlugin/QDesignerExportWidget>
#include <QPushButton>
// config
#define DURATION 150
#define EC QEasingCurve::OutBack
#define BG QColor(255, 217, 217)
#define BBG QColor(255, 134, 134)

class Ball: public QWidget{
    Q_OBJECT

    Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
    // Off -> On   <=>   start -> end

	// On  setDirection(QAbstractAnimation::Forward);
    // Off setDirection(QAbstractAnimation::Backward);

public:
    Ball(QWidget* parent = nullptr);

    QColor color(){return m_color;}

    void setBallBgcOn(const QColor &color);
    void setBallBgcOff(const QColor& color);
    void setColor(const QColor& color);
    void setChecked(bool checked);
    void resetRect();

signals:
    void colorChanged(const QColor& color);

private:
    bool m_checked;
    QColor m_color, m_colorOn, m_colorOff;
    QPropertyAnimation* m_slideAnimation, * m_ballBgcAnimation;

protected:
    void paintEvent(QPaintEvent* event) override;
};

class QDESIGNER_WIDGET_EXPORT CtmSwitchButton:public QWidget
{
    Q_OBJECT

    // 总背景色(on状态)
    Q_PROPERTY(QColor bgcOn READ bgcOn WRITE setBgcOn NOTIFY bgcOnChanged)
    // 总背景色(off状态)
    Q_PROPERTY(QColor bgcOff READ bgcOff WRITE setBgcOff NOTIFY bgcOffChanged)
    // 小球背景色(on状态)
    Q_PROPERTY(QColor ballBgcOn READ ballBgcOn WRITE setBallBgcOn NOTIFY ballBgcOnChanged)
    // 小球背景色(off状态)
    Q_PROPERTY(QColor ballBgcOff READ ballBgcOff WRITE setBallBgcOff NOTIFY ballBgcOffChanged)


    // 总背景颜色(变化态)使用 DESIGNABLE false 不允许用户设置该值
    Q_PROPERTY(QColor bgc READ bgc WRITE setBgc NOTIFY bgcChanged DESIGNABLE false)

public:
    CtmSwitchButton(QWidget* parent = nullptr);

    QColor bgc(){return m_bgc;}
    QColor bgcOn(){return m_bgcOn;}
    QColor bgcOff(){return m_bgcOff;}
    QColor ballBgcOn(){return m_ballBgcOn;}
    QColor ballBgcOff(){return m_ballBgcOff;}

    void setBgcOn(const QColor& color);
    void setBgcOff(const QColor& color);
    void setBallBgcOn(const QColor& color);
    void setBallBgcOff(const QColor& color);
    void setBgc(const QColor& color);

    bool isChecked() const;
    void setChecked(bool checked);

signals:
    void bgcOnChanged(const QColor &color);
    void bgcOffChanged(const QColor &color);
    void ballBgcOnChanged(const QColor &color);
    void ballBgcOffChanged(const QColor &color);
    void bgcChanged(const QColor& color);
    void checked(bool isChecked);

protected:
    void paintEvent(QPaintEvent* e) override;
    void resizeEvent(QResizeEvent* e) override;
    void mousePressEvent(QMouseEvent* e) override;

private:
    Ball* ball;
    bool m_checked;
    QPropertyAnimation* m_bgcAnimation;
    QColor m_bgc, m_bgcOn, m_bgcOff, m_ballBgcOn, m_ballBgcOff;
};

#endif // MYSWITCHBUTTON_H

源文件

如果你自己可以设计出这个头文件,那么cpp文件应该也是洒洒水啦。

cpp文件我只挑重点讲,主要有几个地方需要注意以下:

  1. 需要为我们的两个类都添加:
setAttribute(Qt::WA_TranslucentBackground, false);

这个的作用是形成一个透明底,保证绘制的时候和预期效果一致。

  1. upate()调用时机
    我们的动画是通过不断调用setAttribute()实现的,所以你需要:
void Ball::setColor(const QColor &color)
{
    m_color = color;
    update();
}

void CtmSwitchButton::setBgc(const QColor &color)
{
    m_bgc = color;
    update();
}
  1. 动画方向设置
    动画方向一定是在点击的时候设置,如果是Off → On,动画Forward,否则Backward
  2. resizeEvent写法
    这里实现了控制宽高比 2 : 1 2:1 2:1,并且注意需要调整小球的gemtory → Ball::resetRect
void CtmSwitchButton::resizeEvent(QResizeEvent *e)
{
    QSize oldSize = e->oldSize();
    QSize newSize = e->size();

    // 判断是否是高度发生变化(优先处理高度变化)
    if (oldSize.height() != newSize.height()) {
        int newHeight = newSize.height();
        int newWidth = newHeight * 2;
        this->resize(newWidth, newHeight);
    }
    // 否则,如果是宽度发生变化
    else if (oldSize.width() != newSize.width()) {
        int newWidth = newSize.width();
        int newHeight = newWidth / 2;
        this->resize(newWidth, newHeight);
    }

    ball->resetRect();
    QWidget::resizeEvent(e); // 保证父类事件被处理
}
  1. resetRect写法
    我们需要知道小球和按钮的大小关系,可以参考我这副图
    在这里插入图片描述
    【注意】最好使用我的方法计算y值,否则整数除法的误差可能会导致小球对不齐
    你可以在这个基础上添加一个鼠标悬浮时,小球会变大,移开后恢复的动画(如果你和我一样比较懒的话就算了)
void Ball::resetRect()
{
    int h = parentWidget()->height();
    int w = parentWidget()->width();

    int margin = h / 8;
    int diameter = h * 0.75;
    int y = (h - diameter) / 2;

    // 左边起点
    QRect startRect(margin, y, diameter, diameter);

    // 右边终点
    QRect endRect(w - diameter - margin, y, diameter, diameter);

    if(m_checked){// on
        setGeometry(endRect);
    }
    else {// off
        setGeometry(startRect);
    }

    m_slideAnimation->setStartValue(startRect);
    m_slideAnimation->setEndValue(endRect);
}
  1. 完整版cpp
#include "ctmswitchbutton.h"

CtmSwitchButton::CtmSwitchButton(QWidget *parent)
    :QWidget(parent)
{
    setAttribute(Qt::WA_TranslucentBackground, false);
    setCursor(Qt::PointingHandCursor);
    m_checked = false;
    resize(28, 14);
    // 初始化动画对象
    m_bgcAnimation = new QPropertyAnimation(this, "bgc");

    // 设置动画时间/曲线
    m_bgcAnimation->setDuration(DURATION);
    m_bgcAnimation->setEasingCurve(EC);

    ball = new Ball(this);

    // 初始化颜色 off
    setBgcOn(BG);
    setBgcOff(BG);
    setBallBgcOn(BBG);
    setBallBgcOff(BBG);
}

void CtmSwitchButton::setBgcOn(const QColor& color)
{
    m_bgcOn = color;
    m_bgcAnimation->setEndValue(color);
    if(m_checked)setBgc(color);
}

void CtmSwitchButton::setBgcOff(const QColor& color)
{
    m_bgcOff = color;
    m_bgcAnimation->setStartValue(color);
    if(!m_checked)setBgc(color);
}

void CtmSwitchButton::setBallBgcOn(const QColor& color)
{
    m_ballBgcOn = color;
    ball->setBallBgcOn(color);
}

void CtmSwitchButton::setBallBgcOff(const QColor& color)
{
    m_ballBgcOff = color;
    ball->setBallBgcOff(color);
}

void CtmSwitchButton::setBgc(const QColor &color)
{
    m_bgc = color;
    update();
}

bool CtmSwitchButton::isChecked() const
{
    return m_checked;
}

void CtmSwitchButton::setChecked(bool c)
{
    if(m_checked == c)return;// 冲突
    m_checked = c;
    // 保险起见,先暂停
    m_bgcAnimation->stop();

    if(c){// off -> on
        m_bgcAnimation->setDirection(QAbstractAnimation::Forward);
    }
    else{// on -> off
        m_bgcAnimation->setDirection(QAbstractAnimation::Backward);
    }
    ball->setChecked(c);
    m_bgcAnimation->start();
    emit checked(c);// 发送信号
}

void CtmSwitchButton::paintEvent(QPaintEvent *event)
{
    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing);
    p.setPen(Qt::NoPen);
    p.setBrush(m_bgc);
    p.drawRoundedRect(rect(), height() / 2.0, height() / 2.0);
    QWidget::paintEvent(event);
}

void CtmSwitchButton::resizeEvent(QResizeEvent *e)
{
    QSize oldSize = e->oldSize();
    QSize newSize = e->size();

    // 判断是否是高度发生变化(优先处理高度变化)
    if (oldSize.height() != newSize.height()) {
        int newHeight = newSize.height();
        int newWidth = newHeight * 2;
        this->resize(newWidth, newHeight);
    }
    // 否则,如果是宽度发生变化
    else if (oldSize.width() != newSize.width()) {
        int newWidth = newSize.width();
        int newHeight = newWidth / 2;
        this->resize(newWidth, newHeight);
    }

    ball->resetRect();
    QWidget::resizeEvent(e); // 保证父类事件被处理
}

void CtmSwitchButton::mousePressEvent(QMouseEvent *e)
{
    if(e->button() != Qt::LeftButton || m_bgcAnimation->state() == QAbstractAnimation::Running)return;
    setChecked(!m_checked);
}


// ================================= Ball Implementation =========================================


Ball::Ball(QWidget *parent)
    :QWidget(parent)
{
    setAttribute(Qt::WA_TranslucentBackground, false);
    m_checked = false;

    m_slideAnimation = new QPropertyAnimation(this, "geometry");// 左右移动
    m_ballBgcAnimation = new QPropertyAnimation(this, "color");// 变色

    for(auto anim: {m_slideAnimation, m_ballBgcAnimation}){
        anim->setDuration(DURATION);
        anim->setEasingCurve(EC);
    }

    resetRect();
}

void Ball::setBallBgcOn(const QColor& color)
{
    m_ballBgcAnimation->setEndValue(color);
    if(m_checked)setColor(color);
}

void Ball::setBallBgcOff(const QColor& color)
{
    m_ballBgcAnimation->setStartValue(color);
    if(!m_checked)setColor(color);
}

void Ball::setColor(const QColor &color)
{
    m_color = color;
    update();
}

void Ball::setChecked(bool checked)
{
    m_checked = checked;

    if(checked) {// off -> on
        m_slideAnimation->setDirection(QAbstractAnimation::Forward);
        m_ballBgcAnimation->setDirection(QAbstractAnimation::Forward);
    }
    else {// on -> off
        m_slideAnimation->setDirection(QAbstractAnimation::Backward);
        m_ballBgcAnimation->setDirection(QAbstractAnimation::Backward);
    }

    m_slideAnimation->start();
    m_ballBgcAnimation->start();
}

void Ball::resetRect()
{
    int h = parentWidget()->height();
    int w = parentWidget()->width();

    int margin = h / 8;
    int diameter = h * 0.75;
    int y = (h - diameter) / 2;

    // 左边起点
    QRect startRect(margin, y, diameter, diameter);

    // 右边终点
    QRect endRect(w - diameter - margin, y, diameter, diameter);

    if(m_checked){// on
        setGeometry(endRect);
    }
    else {// off
        setGeometry(startRect);
    }

    m_slideAnimation->setStartValue(startRect);
    m_slideAnimation->setEndValue(endRect);
}

void Ball::paintEvent(QPaintEvent *event)
{
    QPainter p(this);
    p.setRenderHint(QPainter::Antialiasing);
    p.setPen(Qt::NoPen);
    p.setBrush(m_color);
    p.drawEllipse(rect());
    QWidget::paintEvent(event);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

phshp

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

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

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

打赏作者

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

抵扣说明:

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

余额充值