前言(简介)
- 提示:本设计使用的是【Qt4 设计师自定义控件】,这样可以集成到Qt Desinger中,不知道这项目模板的可以跳转此处。
- 背景:使用自带QCheckBox时,觉得不够好看,如果直接改变qss又觉得调整不到自己想要的效果,于是有了本篇讲述的CtmSwitchButton。
- 目标:设计出一个可以发出on/off基本信号的按钮,并且实现一些小动画和自定义属性(颜色)
实现效果
考虑到通用性,我们的颜色是可以自定义的
再看看我们添加颜色切换的效果
最后是槽函数
其中最重要的就是我们的checked(bool)
,这是我们设计的初衷。
设计思路
- 需要用到哪些类
- 动画效果要怎么实现
- 需要哪些属性
想要直观地思考这些问题,直接画一幅草图就好了。
这图是我们设计的原型(确实草)
看来我们需要
- 用一个大边框包住一个小边框
- 大边框作为按钮边界
- 小边框的位置则代表按钮on/off状态
- 小边框需要可以左右移动
继续,那大边框用什么做?小边框呢?
首先大边框肯定用QWidget
最好,因为QWidget
最干净,也适合做绘制和做父类
小边框可以有两种选择:
- 直接使用大边框的绘图事件画出来
- 再写一个
QWidget
类作为成员变量
权衡之下,我选择了第二种,因为它的封装性更好,低耦合。
那么现在图就是这样:
为了提高视觉效果,我们可以把边框变成曲线,曲线自带柔和效果,不像直角那样让人觉得锋利。
所以可以改成这样:
这个就是我们最终的设计效果,接下来我们只要实现:颜色过渡、小球移动这两个动画,这个按钮的界面就完成了。
代码实现
由于这个控件比较小,所以只需要一个头文件和源文件就够了
头文件
- 我们需要两个类
class Ball : public QWidget{};
// QDESIGNER_WIDGET_EXPORT 这个宏用来标记这是一个Designer插件,可以让Qt自动识别
// 需要包含头文件 #include <QtUiPlugin/QDesignerExportWidget>
class QDESIGNER_WIDGET_EXPORT CtmSwitchButton : public QWidget{
Ball* ball; //内部小球
};
- 设置属性
- 小球(
Ball
)- 颜色
color
(控制动画,用户没有权限修改) - 位置
geometry
(自带属性,控制小球移动)
- 颜色
- 按钮(
CtmSwitchButton
)- 背景色(
bgc
, 控制动画,用户没有权限修改) - 背景色Off状态(
bgcOff
,由用户设置动画的初态) - 背景色On状态(
bgcOn
,由用户设置动画的终态) - 小球色Off状态(
ballBgcOff
,由用户设置动画的初态) - 小球色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)
}
- 添加属性成员和动画成员和
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;
};
- 完善属性函数定义
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);
};
- 补充需要的辅助函数
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;
};
- 完整版头文件
#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文件我只挑重点讲,主要有几个地方需要注意以下:
- 需要为我们的两个类都添加:
setAttribute(Qt::WA_TranslucentBackground, false);
这个的作用是形成一个透明底,保证绘制的时候和预期效果一致。
upate()
调用时机
我们的动画是通过不断调用setAttribute()实现的,所以你需要:
void Ball::setColor(const QColor &color)
{
m_color = color;
update();
}
void CtmSwitchButton::setBgc(const QColor &color)
{
m_bgc = color;
update();
}
- 动画方向设置
动画方向一定是在点击的时候设置,如果是Off → On,动画Forward
,否则Backward
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); // 保证父类事件被处理
}
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);
}
- 完整版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);
}