整体效果
使用Qt属性动画实现了一种动态饼图展开效果。效果图如下图:
Qt属性动画
Qt属性动画QPropertyAnimation类动是基于Qt属性(Qt properties)的属性修改,继承于QVariantAnimation,支持与父类相同的动画属性。
Qt的整个动画框架类继承关系如下图:
1、属性动画QPropertyAnimation可以理解为对单个对象Object的单帧动画
2、组动画是对多个属性动画或者其他组动画的一种串行或并行的组织关系
这里引用Qt Assistant中的demo code,表示一个属性动画的创建
QPropertyAnimation *animation = new QPropertyAnimation(myWidget, "geometry");
animation->setDuration(10000);
animation->setStartValue(QRect(0, 0, 100, 30));
animation->setEndValue(QRect(250, 250, 100, 30));
animation->start();
其中 new QPropertyAnimation(myWidget, “geometry”) geometry就是Qt properties。这是Qt内置属性。这个属性动画控制窗口在10s内的几何尺寸的变化,从(0, 0, 100, 30)变换到(250, 250, 100, 30)。
Qt属性系统(The Property System )
Qt properties依赖于Qt属性系统,Qt属性基于Qt的元对象系统(Meta-Object System),通过Qt信号槽极值进行数据传递,属性属性可以从qml脚本文件向cpp文件中传递。
属性的代码行为更像一个类的数据成员,但为了对接元对象系统,附加了一些特性声明(具体参看Qt Assistant),属性的值任何代码块中的变化都会触发其他代码块中同属性同步数值更新(如果有信号,发送信号)。
属性宏的声明类似如下:
Q_PROPERTY(QColor color MEMBER m_color NOTIFY colorChanged)
Q_PROPERTY(qreal spacing MEMBER m_spacing NOTIFY spacingChanged)
Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged)
...
signals:
void colorChanged();
void spacingChanged();
void textChanged(const QString &newText);
private:
QColor m_color;
qreal m_spacing;
QString m_text;
本例中圆环展开的动画实现
1、定义饼图Item(PieFigureItem)继承于QWidget
2、声明角度(angle)为Qt属性,并增加属性声明和信号声明
3、在PieFigureItem::paint() 函数里,更根据angle的值绘制出分段圆环;在根据angle的值比例计算中间数值的显示值
4、属性动画触发的角度槽函数onAngleChanged(newAnlge) 重新paint()饼图
PieFigureItem.h代码如下:
#pragma once
#include <QWidget>
#include "StatusPieFigureDef.h"
class QPropertyAnimation;
namespace commonui
{
/**
* 饼图item
*/
class PieFigureItem : public QWidget
{
Q_OBJECT
// 声明角度(angle)为Qt属性
Q_PROPERTY(int angle MEMBER m_angle NOTIFY signalAngleChanged)
public:
PieFigureItem(int duration = 6000, QWidget *parent = Q_NULLPTR);
~PieFigureItem();
// 初始区段数据
void initData(PtrFigureData pData);
private:
void initConnect();
// 初始动画
void initAnimation(int duration);
// 绘制基础环段
void drawCircleSegment(QColor color, int innerRa
, int outerRa, qreal startAngle, qreal spanAngle, QPainter& painter);
signals:
// 角度动画改变信号
void signalAngleChanged(int newAngle);
protected:
void paintEvent(QPaintEvent* event) override;
void showEvent(QShowEvent* event) override;
private slots:
// 响应角度改变
void onAngleChanged(int newAngle);
private:
int m_angle{ 0 }; // 动画角度
int m_totalValue{ 0 }; // 数据总值
PtrFigureData m_pFigureData{ nullptr }; // 图标数据
QPropertyAnimation* m_pAnimation{ nullptr };// 属性动画
};
}
PieFigureItem.cpp代码如下:
#include "stdafx.h"
#include "PieFigureItem.h"
#include <QPropertyAnimation>
#include <QPainter>
#include <QtMath>
namespace commonui
{
namespace {
const qreal kInitInnerRad = 0.25; // 初始内径(归一化)
const qreal kInitOuterRad = 0.49; // 初始外径(归一化)
const qreal kRadDiff = 0.03; // 外径梯度下降差(归一化)
const int kValueMaxStringWidth = 115; // 数值显示的最大宽度为115px,小于就要缩小尺寸
const int kValueMaxStringHeght = 60; // 数值显示的最大高度为60px,小于就要缩小尺寸
const int kDefautFontSize = 45; // 初始字体大小45pt
QFont gTextFont("Microsoft YaHei"); // 字体
const QColor kTextColor = QColor("#4D4D4D"); // 字体颜色
}
/** @fn calculateDrawCircleRingPath
* @brief 绘制圆环路径
* @param(IN) pCenter 中心点
* @param(IN) qRadiusMin 内径
* @param(IN) qRadiusMax 外径
* @param(IN) startAngle 初始角
* @param(IN) spanAngle 环圆心角
* @author wangxiaodong8
* @return QPainterPath
*/
QPainterPath calculateDrawCircleRingPath(QPointF pCenter
, qreal qRadiusMin, qreal qRadiusMax, qreal startAngle, qreal spanAngle)
{
QPainterPath path;
qreal v1PointX = qRadiusMin * qCos(qDegreesToRadians(startAngle))
+ pCenter.x();
qreal v1PointY = -qRadiusMin * qSin(qDegreesToRadians(startAngle))
+ pCenter.y();
QRectF radiusMaxRect = QRectF(pCenter - QPointF(qRadiusMax, qRadiusMax), QSize(qRadiusMax * 2, qRadiusMax * 2));
QRectF radiusMinRect = QRectF(pCenter - QPointF(qRadiusMin, qRadiusMin), QSize(qRadiusMin * 2, qRadiusMin * 2));
// 构造扇形路径
path.moveTo(QPointF(v1PointX, v1PointY));
path.arcTo(radiusMinRect, startAngle, spanAngle);
path.arcTo(radiusMaxRect, startAngle + spanAngle, -spanAngle);
path.closeSubpath();
return path;
}
/** @fn computeAdustedFontSize
* @brief 计算文本的合适字体大小
* @author wangxiaodong8
* @return int
*/
int computeAdustedFontSize(QString text, QSize& textRectsize)
{
int retFontPt = kDefautFontSize;
do
{
gTextFont.setPointSizeF(retFontPt);
QFontMetrics metric(gTextFont);
textRectsize = metric.boundingRect(QRect(0, 0, kValueMaxStringWidth, kValueMaxStringHeght)
, Qt::AlignCenter, text).size();
if (textRectsize.width() < kValueMaxStringWidth
&& textRectsize.height() < kValueMaxStringHeght)
{
break;
}
retFontPt -= 2;
} while (retFontPt > kDefautFontSize / 2);
return retFontPt;
}
PieFigureItem::PieFigureItem(int duration /*= 6000*/, QWidget *parent /*= Q_NULLPTR*/)
:QWidget(parent)
{
// 初始动画
initAnimation(duration);
initConnect();
}
PieFigureItem::~PieFigureItem()
{
}
/** @fn PieFigureItem::initData
* @brief
* @param(IN)
* @param(OUT)
* @author wangxiaodong8
* @return void
*/
void PieFigureItem::initData(PtrFigureData pData)
{
m_pFigureData = pData;
// 统计总数
m_totalValue = 0;
for (const auto& segData : *m_pFigureData)
{
m_totalValue += segData.segmentValue.toInt();
}
update();
}
/** @fn PieFigureItem::initConnect
* @brief initConnect
* @author wangxiaodong8
* @return void
*/
void PieFigureItem::initConnect()
{
connect(this, &PieFigureItem::signalAngleChanged, this, &PieFigureItem::onAngleChanged);
}
/** @fn PieFigureItem::initAnimation
* @brief 初始动画
* @param(IN) duration
* @author wangxiaodong8
* @return void
*/
void PieFigureItem::initAnimation(int duration)
{
m_pAnimation = new QPropertyAnimation(this, "angle");
m_pAnimation->setDuration(duration);
m_pAnimation->setStartValue(0);
m_pAnimation->setEndValue(360);
}
/** @fn PieFigureItem::drawCircleSegment
* @brief 绘制基础环段
* @param(IN) color 环颜色
* @param(IN) innerRa 内径
* @param(IN) outerRa 外径
* @param(IN) startAngle 初始角
* @param(IN) spanAngle 环圆心角
* @param(IN) painter
* @author wangxiaodong8
* @return void
*/
void PieFigureItem::drawCircleSegment(QColor color, int innerRa
, int outerRa, qreal startAngle, qreal spanAngle, QPainter& painter)
{
painter.save();
// 圆心
QPoint centerPoint = this->rect().center();
// 计算环路径
auto path = calculateDrawCircleRingPath(QPointF(centerPoint.x(), centerPoint.y()),
innerRa, outerRa, startAngle, spanAngle);
// 绘制
painter.setPen(Qt::NoPen);
painter.setBrush(color);
painter.drawPath(path);
painter.restore();
}
/** @fn PieFigureItem::paintEvent
* @brief paintEvent
* @param(IN) event
* @author wangxiaodong8
* @return void
*/
void PieFigureItem::paintEvent(QPaintEvent* event)
{
if (m_pFigureData && m_pFigureData->count() > 0)
{
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
绘制圆环
// 最大半径
qreal radius = (this->width() < this->height()) ? this->width() : this->height();
qreal radDiff = radius * kRadDiff;
auto index = 0;
qreal startAngle = 90;
for (const auto& segData : *m_pFigureData)
{
qreal spanAngle = -segData.segmentValue.toDouble() / m_totalValue * m_angle;
qreal outerRad = radius * kInitOuterRad - index * radDiff;
qreal innterRad = radius * kInitInnerRad;
drawCircleSegment(segData.segmentColor, innterRad, outerRad, startAngle, spanAngle, painter);
startAngle += spanAngle;
++index;
}
绘制数字
// 找到合适字体大小
int midValue = m_angle / 360.0 * m_totalValue;
QString textValue = QString::number(midValue);
QSize textRectSize;
auto fontSize = computeAdustedFontSize(textValue, textRectSize);
// 绘制中间数值
gTextFont.setPointSizeF(fontSize);
gTextFont.setBold(true);
painter.setFont(gTextFont);
painter.setPen(kTextColor);
QPoint centerPoint = this->rect().center();
QPoint topleft = centerPoint - QPoint(textRectSize.width() / 2, textRectSize.height() / 2);
QRect textRect(topleft, QSize(textRectSize.width(), textRectSize.height()));
painter.drawText(textRect, textValue);
}
QWidget::paintEvent(event);
}
/** @fn PieFigureItem::showEvent
* @brief showEvent
* @param(IN) event
* @author wangxiaodong8
* @return void
*/
void PieFigureItem::showEvent(QShowEvent* event)
{
m_pAnimation->start();
}
/** @fn PieFigureItem::onAngleChanged
* @brief 响应角度改变
* @param(IN) newAngle
* @author wangxiaodong8
* @return void
*/
void PieFigureItem::onAngleChanged(int newAngle)
{
update();
}
}