先上效果:
一、控件功能介绍
滑动开关有以下功能:
- 可更改背景颜色和滑块背景色;
- 可设置是否显示提示光以及提示光颜色。
- 可以设置圆角半径;
- 可设置是否开启动画;
- 可设置动画效果的速度。
好了,介绍完毕(真介绍完了啊)~~
二、实现思路
跟着我的思路来走,自定义控件不再有难度!
记得抓住这三点来想:有什么功能,怎么实现,什么时机去实现。
-
根据功能,确定所有成员变量(例如是否开启动画、是否显示提示光等等)。
-
根据功能,确定所有
set
和get
函数(注意,只是确定,写好声明,先不需要实现,当然get
类函数如果只是返回成员变量,也可先实现),每一对函数都要对应至少一个成员变量。 -
确定所有静态属性(通过
Q_PROPERTY
设置)。 -
声明并定义
paintEvent
函数,编写最基本的函数体,就像这样:void SlideSwitch::paintEvent(QPaintEvent *event) { Q_UNUSED(event); QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing); // TODO: 以下分模块函数进行绘制 }
就如上面代码所示,把基本的函数体写出来,绘制部分先用一个
TODO
表示,不用着急着想怎么画。 -
确定绘制的模块,这里我分成了三个模块:开关背景、开关滑块、提示光。注意,只是确定,不用着急想着所有模块怎么实现。
-
从第一个绘制模块开始实现。实现模块过程中,逐步丰富工具类或辅助类函数(指的不是
set
和get
这类)。 -
对
set
和get
函数进行实现。 -
加入动画,这里用定时器实现,声明和实现响应函数。
-
声明和实现鼠标事件函数(其实这一步通常也不用放到最后,但此处放到最后可以起到“清晰思路”的效果:先实现功能和显示,再实现操作响应)。
-
优化和完善。
三、详细步骤
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. 确定 set
和 get
函数
经过第1步后,我们已经对控件的属性有了大概的轮廓,这里我就直接列出一些相关的 set
和 get
函数:
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; }
基本上 set
和 get
函数都是一一对应的。
你可能发现了,并非所有的成员变量都需要有对应的 set
和 get
函数对,因为有部分成员变量是辅助变量或者是不打算对外开放变量。
3. 确定静态属性
这一步比较简单,其实就是把第2步的函数与属性关联起来,已实现通过Qt的 setProperty
和 property
函数进行操作。代码我直接给出:
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
函数
我们继续思考滑块的属性:
- 颜色
- 尺寸(宽度、高度)
- 位置
貌似就这么多了,开始进行实现:
-
颜色:
painter->setPen(Qt::NoPen); painter->setBrush(m_sliderColor);
没什么好说的。
-
得出当前滑块的尺寸,我们需要想到滑块不能跟背景的高度一致,否则滑块就完全挡住背景,就没有那种立体效果了。
这里我们用一个宏,用来设定滑块上下边距(为什么用宏?因为我没打算让用户更改):
#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;
-
我们需要根据滑块当前所处的位置对滑块进行绘制,我发现
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
函数
继续思考属性:
- 颜色
- 形状(不管了,圆形)
- 尺寸
- 位置
貌似也没了,接下来开始实现:
-
颜色:
painter->setPen(Qt::NoPen); painter->setBrush(m_tipLightCurrentColor);
我们需要知道当前应该是绿色还是红色吗?不必多想,你就当作
m_tipLightCurrentColor
是别人给的就行了,我们只是负责把m_tipLightCurrentColor
颜色画出来了。 -
尺寸:
qreal width = this->height() / 4.0 - PADDING_LEFT_RIGHT; qreal height = width;
自己看着来写吧,我这里是通过计算得来的,你们想绘制成其他样子可以自己重写表达式。
-
对于提示光的位置,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;
-
万事俱备,只欠东风,最后一步,画出来,我们用
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. 实现 set
和 get
函数
对于这一步为什么放这里,可能每个人的想法有所不同,我的想法是:在实现绘制操作时,有可能会添加一些辅助性的成员变量,这下变量有可能要和某些表达属性的成员变量相关联。如果在绘制函数实现之前就实现 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. 鼠标事件函数实现
经过了上面那些步骤,我们的控件的功能已经成型了,接下来就是实现控件功能触发部分(这里我只实现了鼠标触发,大家喜欢的话可以自己实现其他的触发)。
既然是开关,我们就要模拟“点击”操作,这里我们需要实现三个函数——mousePressEvent
、mouseMoveEvent
、mouseReleaseEvent
。
实现“点击”操作,我们需要对鼠标按下标记进行记录;实现在合适区域才响应“点击”操作,我们需要有一个是否可以选中标记(这个标记起名有点不合适,准确来说应该是是否处于响应区域才对,但我懒得改了)。
我们先来实现 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();
}
五、个人小忠告
我理解有的小伙伴可能会认为,在日常开发中过度深入思考可能显得有些多余,甚至有时会感觉它会延缓我们的开发节奏。但大家有没有想过,真正拖慢进度的是你的思路还是你产生思路的时间呢?
对于一些较小的项目,采用深思熟虑的方法进行开发可能看似没有明显成效,甚至给人一种画蛇添足的感觉。但若没有培养这种思维习惯,当我们面对更大的挑战,如框架的构建或软件架构的设计时,缺乏周全的考虑可能会导致成果中存在诸多疏漏,进而使得后续的维护工作变得异常复杂。稳健的框架需要我们在构建时考虑周全,否则诸如遗忘初始化变量、忽视对特定区域的限制、未能清晰定义不同功能之间的关系等问题可能会严重影响其稳定性。
在本项目中,我并未应用复杂的设计模式或高深的软件设计概念,最多仅采用了简单的模块化开发方法。我注意到有些小伙伴热衷于追求各种设计理念,但却可能忽视了最基本的能力——形成清晰、逻辑性强的思路。这样做有时候会陷入为使用某个概念而使用它的境地。我希望大家能先培养起扎实的程序设计思维,再逐渐深入学习更多的软件设计概念,这样可能对个人技能的提升更为有益。
这些仅仅是我个人的一点建议,每个人的情况都不相同,所以大家可以根据自己的实际情况进行取舍。这只是我一家之言,未必全然正确,小伙伴们可以自行判断。如果觉得我说的有不对的地方,请多多指教,我一定虚心接受~~~~