从零开始手把手教你设计自己的qt控件
- 1 说明
- 1.1 显示效果
- 1.2 控件特性
- 1.3 设计方法
- 2 控件需求分析
- 2.1 必要需求
- 2.1 顺带需求(锦上添花)
- 3 功能设计
- 3.1 设计思路(重点内容)
- 3.2 自适应大小
- 3.3 开关动画
- 3.4 控件绘制
- 4 总体代码
1 说明
1.1 显示效果
显示效果看下面的动图:
(1)普通模式,一般都是用这种模式就行
(2)等待模式,有的比较耗时且不确定是否能够开启的操作使用该模式
1.2 控件特性
具体特性如下图所示:
1.3 设计方法
实现自定义控件的方法有很多,除了自己绘制外,还可以使用qt的控件+样式表堆一个新的控件。
我现在只推荐继承qwidget类进行绘制,虽然前期可能麻烦点,原因如下:
- 需要高度自定义,很多细节把控;
- 后续功能和样式拓展方便灵活;
- 执行效率;
- 通用性和独立性;
- 项目如果非常复杂庞大的话,样式表将会是个非常忌讳的东西。
在后文提供一种控件的通用设计思路,可供参考。
2 控件需求分析
需求分析主要是要明确控件做成什么样子,从而在开发中避免做无用功,最主要的是防止设计方向出现偏差导致漏掉的需求很难添加上去。
这个步骤还是非常有必要的,在我们的工作项目中开发做需求分析后,一个是可以对工作上做风险把控,确定工作难度和影响,另外一个就是可以做好时间规划,防止规划工作的时间过长或过短,最后就是这个过程可以顺便把概要设计完成,总之益处多多。
2.1 必要需求
(1)控件开、关状态;
(2)开、关、圆形按钮颜色自定义;
(3)圆形按钮带移动动画;
(4)添加等待模式;
(5)圆形按钮阴影;
(6)可获取开关状态;
(7)状态切换后发出信号。
2.1 顺带需求(锦上添花)
(1)自适应缩放,就是可大可小;
(2)边缘颜色自定义(一般为透明就行);
(3)颜色渐变动画;
(4)添加使、失能状态。
3 功能设计
3.1 设计思路(重点内容)
对于自定义控件实现中,每个功能的支持,在代码上我们一般可总结为接口、操作、数据、绘制几个部分,关系如下图:
使用失能状态为例子:
(1)首先是数据支持,每个功能第一步应该是需要构建好支持的数据,这样就有个中心,接下来围绕这个中心进行开发,从上图可以看出来,最后数据最好有个默认初始状态。
bool mEnable{1}; //使能状态
(2)、读取、写入接口,接口定义可以优于开发的,一般在项目需求分析的时候定义好了。
void setEnabled(bool enable); /// 设置使能状态,default:1
bool getEnabled(); /// 获取使能状态
(3)操作,由于这个功能非常简单,所以基本没有什么操作,所以直接写在接口函数内部,如下:
void WBSwitchButton::setEnabled(bool enable){
QWidget::setEnabled(enable);
mEnable = enable;
emit sigEnableChanged(mEnable);
update();
}
(3-2)操作相关影响,失能状态下是不可点击切换开关状态的,所以在点击事件中进行过滤,代码如下:
if(!mEnable) return ;
注:这段代码需要写在事件函数最前面。
(4)绘制,这步主要是要在显示上区分使能和失能,我的思路是直接在按钮上面盖上一层暗色的蒙层,表示处于失能状态,代码如下所示:
/// 失能显示,添加一层暗色的蒙层
if(!mEnable){
QColor disable(Qt::black);
disable.setAlphaF(0.5);
painter.setBrush(disable);
painter.drawRoundedRect(this->rect(),mRadius,mRadius);
}
注:这部分代码是写在绘制函数最后面。
失能效果如下,可对比前面的使能状态。
3.2 自适应大小
因为控件按照上面的思路设计进行设计,所以在自适应大小的时候,不涉及到接口和操作,直接调整(·计算)背后支持的数据即可,然后直接update()重新绘制一下。
void WBSwitchButton::resizeEvent(QResizeEvent *event){
Q_UNUSED(event)
/// 更新按钮大小、圆角大小、动画两个位置
int size = qMin(this->width(),this->height());
mRadius = size/2;
float width = size * 3 / 4;
float border = (size - width) / 2;
mLeftPos = QPoint(border,border);
mRightPos = QPoint(this->width() - border - width,border);
mButtonRect.setWidth(width);
mButtonRect.setHeight(width);
mButtonRect.moveTo(mOnOff ? mRightPos : mLeftPos);
mBackColor = mOnOff ? mBackOnColor : mBackOffColor ;
update();
}
3.3 开关动画
对于动画,其实都是属于设计思路中的操作部分,此部分只要使用qt的动画对象动态修改数据即可,位置动画就修改按钮位置,颜色动画就修改颜色位置,具体代码如下:
开关按钮位置移动动画
/// 动画-开关按钮位置
QVariantAnimation* posAnimation = new QVariantAnimation(this);
posAnimation->setDuration(mAnimationPeriod);
posAnimation->setStartValue(mButtonRect.topLeft());
posAnimation->setEndValue(mOnOff ? mRightPos : mLeftPos);
connect(posAnimation,&QPropertyAnimation::valueChanged,[=](const QVariant &value){
mButtonRect.moveTo(value.toPointF());
update();
});
posAnimation->start(QAbstractAnimation::DeletionPolicy::DeleteWhenStopped); //停止后删除
开关状态颜色渐变动画
/// 动画-背景颜色
QPropertyAnimation * colorAnimation = new QPropertyAnimation(this,"pBackColor");
colorAnimation->setDuration(mAnimationPeriod);
colorAnimation->setStartValue(mBackColor);
colorAnimation->setEndValue(mOnOff ? mBackOnColor: mBackOffColor);
colorAnimation->start(QAbstractAnimation::DeletionPolicy::DeleteWhenStopped); //停止后删除
3.4 控件绘制
从设计思路中可以看出,我们的绘制主要按照外观,读取支持的数据直接绘制即可,这样的好处就是绘制中不带复杂的操作逻辑(比如动画)和计算操作,防止每次绘制都会进行没必要的重复计算。
绘制函数只拿数据在必要的时候进行绘制,这样大大提高控件的执行效率和流畅度(本控件可能看不出来,大的复杂控件就能体验出来)。
void WBSwitchButton::paintEvent(QPaintEvent *event){
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true); //抗锯齿
painter.setPen(Qt::NoPen);
/// 绘制边缘颜色
QPainterPath path;
path.addRect(this->rect());
path.addRoundedRect(this->rect(),mRadius,mRadius);
path.setFillRule(Qt::OddEvenFill);
painter.setBrush(mEdgeColor);
painter.drawPath(path);
/// 绘制背景颜色
painter.setBrush(mBackColor);
painter.drawRoundedRect(this->rect(),mRadius,mRadius);
/// 绘制圆形按钮
painter.setBrush(mButtonColor);
painter.drawEllipse(mButtonRect);
/// 绘制按钮阴影
painter.setBrush(Qt::NoBrush);
QColor color(Qt::black);
int count = (this->height() - mButtonRect.height())/2;
float stepColor = (0.15-0.0)/count;
for (int i = mButtonRect.height()/2 + 1; i < this->height()/2; i++){
color.setAlphaF(0.15 - stepColor*(i - mButtonRect.height()/2));
painter.setPen(color);
painter.drawEllipse(mButtonRect.center(),i,i);
}
/// 失能显示,添加一层蒙层
if(!mEnable){
QColor disable(Qt::black);
disable.setAlphaF(0.5);
painter.setBrush(disable);
painter.drawRoundedRect(this->rect(),mRadius,mRadius);
}
}
注:可以重点看一下上面的绘制阴影实现思路。
4 总体代码
(1)h文件
#ifndef WBSWITCHBUTTON_H
#define WBSWITCHBUTTON_H
#include <QWidget>
#include <QPropertyAnimation>
#include <QPainterPath>
#include <QPainter>
#include <QRadialGradient>
#include <QMouseEvent>
///
/// \brief 基础控件-Switch开关按钮
///
class WBSwitchButton : public QWidget
{
Q_OBJECT
public:
Q_PROPERTY(QColor pBackColor MEMBER mBackColor) //新增背景颜色属性,用于动画
explicit WBSwitchButton(QWidget *parent = nullptr);
bool getSwitch(); /// 获取开关状态
public slots:
void setSwitch(bool onoff); /// 设置开关状态,default:0
void setEnabled(bool enable); /// 设置使能状态,default:1
bool getEnabled(); /// 获取使能状态
void setAnimationPeriod(int period); /// 设置切换状态周期
void setPrecisionClick(bool flag); /// 设置精确点击,即只有点中按钮的时候才开关
void setWaitModel(bool flag); /// 设置等待模式,点击后不会主动切换开关,需要setSwitch
void setSwitchForWaitModel(bool onoff); /// 设置开关状态,default:0
void setButtonColor(QColor color); /// 设置开关(圆形按钮)颜色
void setBackOnColor(QColor color); /// 设置背景颜色-开
void setBackOffColor(QColor color); /// 设置背景颜色-关
void setEdgeColor(QColor color); /// 设置边缘颜色,默认透明
signals:
void sigEnableChanged(bool enable); /// 使能状态变化信号
void sigSwitchChanged(bool onoff); /// 开关状态变化信号
protected:
void paintEvent(QPaintEvent *event);
void resizeEvent(QResizeEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
void enterEvent(QEvent *event);
void leaveEvent(QEvent *event);
private:
bool mOnOff{0}; //开关状态
bool mEnable{1}; //使能状态
bool mPrecisionClickFlagh{0}; //精确点击标志位
bool mWaitSigModel{1}; //等待模式,点击后按钮位置会进行切换,但是颜色需要等待外部信号变动
bool mAnimationOnOff{1}; //动画开关,default:1
bool mHover{0};
QColor mButtonColor{Qt::white}; //开关(圆形按钮)颜色
QColor mBackColor{Qt::red};
QColor mEdgeColor{Qt::transparent}; //边缘颜色
QRectF mButtonRect; //开关按钮rect
int mRadius{8}; // 开关外观边缘圆角
int mAnimationPeriod{200}; //动画周期
QPointF mRightPos; // 动画位置-开
QPointF mLeftPos; // 动画位置-关
QColor mBackOnColor{Qt::green}; //背景颜色-开
QColor mBackOffColor{Qt::darkGray}; //背景颜色-关
};
#endif // WBSWITCHBUTTON_H
(2)cpp文件
#include "wbswitchbutton.h"
#include <QDebug>
WBSwitchButton::WBSwitchButton(QWidget *parent)
: QWidget{parent}
{
}
bool WBSwitchButton::getSwitch(){
return mOnOff;
}
void WBSwitchButton::setSwitch(bool onoff){
if(mWaitSigModel) return ;
/// 状态切换
mOnOff = onoff;
/// 发送信号
sigSwitchChanged(mOnOff);
/// 动画-背景颜色
QPropertyAnimation * colorAnimation = new QPropertyAnimation(this,"pBackColor");
colorAnimation->setDuration(mAnimationPeriod);
colorAnimation->setStartValue(mBackColor);
colorAnimation->setEndValue(mOnOff ? mBackOnColor: mBackOffColor);
colorAnimation->start(QAbstractAnimation::DeletionPolicy::DeleteWhenStopped); //停止后删除
/// 动画-开关按钮位置
QVariantAnimation* posAnimation = new QVariantAnimation(this);
posAnimation->setDuration(mAnimationPeriod);
posAnimation->setStartValue(mButtonRect.topLeft());
posAnimation->setEndValue(mOnOff ? mRightPos : mLeftPos);
connect(posAnimation,&QPropertyAnimation::valueChanged,[=](const QVariant &value){
mButtonRect.moveTo(value.toPointF());
update();
});
posAnimation->start(QAbstractAnimation::DeletionPolicy::DeleteWhenStopped); //停止后删除
}
void WBSwitchButton::setSwitchForWaitModel(bool onoff)
{
if(!mWaitSigModel) return ;
if(mOnOff == onoff){
/// 表示值未改变先运行按钮位置动画
QVariantAnimation* posAnimation = new QVariantAnimation(this);
posAnimation->setDuration(mAnimationPeriod);
posAnimation->setStartValue(mOnOff ? mLeftPos : mRightPos);
posAnimation->setEndValue(mOnOff ? mRightPos : mLeftPos);
connect(posAnimation,&QVariantAnimation::valueChanged,[=](const QVariant &value){
mButtonRect.moveTo(value.toPointF());
update();
});
posAnimation->start(QAbstractAnimation::DeletionPolicy::DeleteWhenStopped); //停止后删除
return ;
}
/// 状态切换
mOnOff = onoff;
/// 发送信号
sigSwitchChanged(mOnOff);
/// 后运行背景颜色动画
QPropertyAnimation * colorAnimation = new QPropertyAnimation(this,"pBackColor");
colorAnimation->setDuration(mAnimationPeriod);
colorAnimation->setStartValue(mBackColor);
colorAnimation->setEndValue(mOnOff ? mBackOnColor: mBackOffColor);
colorAnimation->start(QAbstractAnimation::DeletionPolicy::DeleteWhenStopped); //停止后删除
connect(colorAnimation,&QPropertyAnimation::valueChanged,[=](const QVariant &value){
update();
});
}
void WBSwitchButton::setEnabled(bool enable){
QWidget::setEnabled(enable);
mEnable = enable;
emit sigEnableChanged(mEnable);
update();
}
void WBSwitchButton::setAnimationPeriod(int period){
mAnimationPeriod = period;
}
void WBSwitchButton::setPrecisionClick(bool flag){
mPrecisionClickFlagh = flag;
}
void WBSwitchButton::setWaitModel(bool flag)
{
mWaitSigModel = flag;
}
void WBSwitchButton::setButtonColor(QColor color){
mButtonColor = color;
update();
}
void WBSwitchButton::setBackOnColor(QColor color){
mBackOnColor = color;
update();
}
void WBSwitchButton::setBackOffColor(QColor color){
mBackOffColor = color;
update();
}
void WBSwitchButton::setEdgeColor(QColor color){
mEdgeColor = color;
update();
}
void WBSwitchButton::paintEvent(QPaintEvent *event){
Q_UNUSED(event)
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setPen(Qt::NoPen);
/// 绘制边缘颜色
QPainterPath path;
path.addRect(this->rect());
path.addRoundedRect(this->rect(),mRadius,mRadius);
path.setFillRule(Qt::OddEvenFill);
painter.setBrush(mEdgeColor);
painter.drawPath(path);
/// 绘制背景颜色
painter.setBrush(mBackColor);
painter.drawRoundedRect(this->rect(),mRadius,mRadius);
/// 绘制圆形按钮
painter.setBrush(mButtonColor);
painter.drawEllipse(mButtonRect);
/// 绘制按钮阴影
painter.setBrush(Qt::NoBrush);
QColor color(Qt::black);
int count = (this->height() - mButtonRect.height())/2;
float stepColor = (0.15-0.0)/count;
for (int i = mButtonRect.height()/2 + 1; i < this->height()/2; i++){
color.setAlphaF(0.15 - stepColor*(i - mButtonRect.height()/2));
painter.setPen(color);
painter.drawEllipse(mButtonRect.center(),i,i);
}
/// 失能显示,添加一层蒙层
if(!mEnable){
QColor disable(Qt::black);
disable.setAlphaF(0.5);
painter.setBrush(disable);
painter.drawRoundedRect(this->rect(),mRadius,mRadius);
}
}
void WBSwitchButton::resizeEvent(QResizeEvent *event){
Q_UNUSED(event)
/// 更新按钮大小、圆角大小、动画两个位置
int size = qMin(this->width(),this->height());
mRadius = size/2;
float width = size * 3 / 4;
float border = (size - width) / 2;
mLeftPos = QPoint(border,border);
mRightPos = QPoint(this->width() - border - width,border);
mButtonRect.setWidth(width);
mButtonRect.setHeight(width);
mButtonRect.moveTo(mOnOff ? mRightPos : mLeftPos);
mBackColor = mOnOff ? mBackOnColor : mBackOffColor ;
update();
}
void WBSwitchButton::mouseReleaseEvent(QMouseEvent *event){
if(mWaitSigModel){
/// 先运行按钮位置动画
QVariantAnimation* posAnimation = new QVariantAnimation(this);
posAnimation->setDuration(mAnimationPeriod);
posAnimation->setStartValue(mOnOff ? mRightPos : mLeftPos);
posAnimation->setEndValue(mOnOff ? mLeftPos : mRightPos);
connect(posAnimation,&QVariantAnimation::valueChanged,[=](const QVariant &value){
mButtonRect.moveTo(value.toPointF());
update();
});
posAnimation->start(QAbstractAnimation::DeletionPolicy::DeleteWhenStopped); //停止后删除
return ;
}
if(!mEnable) return ;
if(mButtonRect.contains(event->pos()) || !mPrecisionClickFlagh){
setSwitch(!mOnOff);
}
}
void WBSwitchButton::enterEvent(QEvent *event){
Q_UNUSED(event)
mHover = true;
}
void WBSwitchButton::leaveEvent(QEvent *event){
Q_UNUSED(event)
mHover = false;
}