Qt实战16.构建甘特图
1 需求描述
根据不同的飞机平台,可视化展示其计划飞行时间(段)和实际飞行时间(段),同时能够展示飞行过程中人员的操作。
2 设计思路
这次我们换一种思路,站在使用者的角度去思考,如果是我,我希望这个控件具有哪些元素?再一个就是控件应该提供什么样的接口?让我用着更爽。
好了,我们简单分析下,首先控件要能展示时间(段),一般来说横轴就是时间轴,纵轴用来显示飞机型号。
既然能显示时间(段),给个起始时间和结束时间,控件能够按照一定的步长把时间轴绘制出来。时间轴有了,飞机平台是不是也可以一起指定,这个时候一个接口就出现了:
void setAxisRange(const QDateTime &start, const QDateTime &end, const QStringList &platforms);
两轴内容有了,接下来就是时间条了,即一行需要有两个时间条,一个是计划时间、一个是实际时间,时间条也需要传入起始时间和结束时间,同时还要知道对应的是哪个飞机平台:
CanttTimeBarItem *addPlanTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end);
CanttTimeBarItem *addRealTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end);
上面返回了一个CanttTimeBarItem指针,说明控件内部完成了时间条的实例化,时间条涉及到一个接口,那就是在什么时间点做了什么操作(事件),这个是需要进行标记的:
void addEvent(const QDateTime &dateTime, EventType type);
到这里,关节已经打通了,下面撸代码。
3 代码实现
没有意外的情况下,我们依然选择了图形视图框架,很简单,只需要自定义三个类,自定义QGraphicsView、QGraphicsScene、QGraphicsRectItem。
3.1 CanttChartScene
场景类主要完成网格线的绘制,同时对外提供前面提到的添加时间条的两个接口,以及时间步长设置接口,头文件代码如下:
#ifndef CANTTCHARTSCENE_H
#define CANTTCHARTSCENE_H
#include <QGraphicsScene>
#include <QDateTime>
#include <QTime>
#include <QHash>
#include <QPair>
class CanttTimeBarItem;
class CanttChartScene : public QGraphicsScene
{
Q_OBJECT
public:
explicit CanttChartScene(QObject *parent = 0);
void setAxisRange(const QDateTime &start, const QDateTime &end, const QStringList &platforms);
CanttTimeBarItem *addPlanTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end);
CanttTimeBarItem *addRealTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end);
void setStepTimeValue(const QTime &time);
private:
void drawGridLines();
void drawVerticalAxis(const QStringList &platforms);
int m_rowCount;
int m_columnCount;
QDateTime m_startDateTime;
QDateTime m_endDateTime;
QTime m_stepTimeValue;
QStringList m_platforms;
int m_firstTimeBarStartX;
int m_firstTimeBarStartY;
double m_perPixelHMsecs;
QHash<QString, double> m_platformStartYHash;
QHash<QString, QPair<QDateTime, QDateTime>> m_planTimeBarTemp;
QHash<QString, QPair<QDateTime, QDateTime>> m_realTimeBarTemp;
QMultiHash<QString, QGraphicsItem*> m_plaformTimeBarHash;
};
#endif // CANTTCHARTSCENE_H
源文件中定义了一些const常量,无非就是一些网格的高度、宽度、偏移啥的,看名字应该能看懂,也可以改改试试效果:
#include "canttchartscene.h"
#include "definition.h"
#include "cantttimebaritem.h"
#include <QBrush>
#include <QPen>
#include <QGraphicsLineItem>
#include <QGraphicsTextItem>
#include <QDebug>
#include <QCheckBox>
#include <QGraphicsProxyWidget>
#include <QCursor>
const int firstHorizantalGridWidth = 100;
const int horizontalGridWidth = 40;
const int verticalGridHeight = 40;
const int horizontalAxisTextHeight = 21;
const int horizontalAxisTextOffset = 5;
const QPoint axisStartPoint = QPoint(20, 40);
const QPoint platformHeaderOffset = QPoint(6, 10);
const QColor gridLineColor = QColor(48, 85, 93);
const QColor scaleDateColor = QColor(253, 201, 115);
const QColor scaleTimeColor = QColor(208, 216, 237);
CanttChartScene::CanttChartScene(QObject *parent) : QGraphicsScene(parent),
m_rowCount(0), m_columnCount(0), m_stepTimeValue(0, 30)
{
setBackgroundBrush(QBrush(QColor(43, 48, 54)));
m_perPixelHMsecs = m_stepTimeValue.msecsSinceStartOfDay() / (double)horizontalGridWidth;
}
void CanttChartScene::setAxisRange(const QDateTime &start, const QDateTime &end, const QStringList &platforms)
{
if (start >= end || 0 == platforms.count())
{
return;
}
m_rowCount = platforms.count();
m_startDateTime = start;
m_endDateTime = end;
m_platforms = platforms;
m_firstTimeBarStartX = axisStartPoint.x() + firstHorizantalGridWidth;
m_firstTimeBarStartY = axisStartPoint.y();
//清空现有图形项
clear();
//绘制前先预留足够空间
double sceneMiniWidth = m_firstTimeBarStartX + horizontalGridWidth
+ (end.toMSecsSinceEpoch() - start.toMSecsSinceEpoch()) / m_perPixelHMsecs;
double sceneMiniHeight = m_firstTimeBarStartY + platforms.count() * verticalGridHeight;
setSceneRect(0, 0, sceneMiniWidth, sceneMiniHeight + 800);
drawVerticalAxis(platforms);
QDateTime startDateTime = start;
QDate startDate = start.date();
double x = m_firstTimeBarStartX;
for (; x <= sceneMiniWidth; x += horizontalGridWidth)
{
QGraphicsTextItem *timeItem = new QGraphicsTextItem(startDateTime.toString("hh:mm"));
timeItem->setDefaultTextColor(scaleTimeColor);
timeItem->setZValue(std::numeric_limits<int>::min());
timeItem->setPos(x - horizontalAxisTextOffset, axisStartPoint.y() - horizontalAxisTextHeight);
addItem(timeItem);
if (x == axisStartPoint.x() + firstHorizantalGridWidth)
{
QGraphicsTextItem *dateItem = new QGraphicsTextItem(startDateTime.date().toString("yyyy-MM-dd"));
dateItem->setDefaultTextColor(scaleDateColor);
dateItem->setZValue(std::numeric_limits<int>::min());
addItem(dateItem);
dateItem->setPos(x - horizontalAxisTextOffset, axisStartPoint.y() - horizontalAxisTextHeight*2);
}
else
{
if (startDateTime.date() > startDate)
{
QGraphicsTextItem *dateItem = new QGraphicsTextItem(startDateTime.date().toString("yyyy-MM-dd"));
dateItem->setDefaultTextColor(scaleDateColor);
dateItem->setZValue(std::numeric_limits<int>::min());
addItem(dateItem);
dateItem->setPos(x - horizontalAxisTextOffset, axisStartPoint.y() - horizontalAxisTextHeight*2);
startDate = startDateTime.date();
}
}
startDateTime = startDateTime.addMSecs(m_stepTimeValue.msecsSinceStartOfDay());
m_columnCount++;
if (startDateTime > QDateTime::currentDateTime())
{
break;
}
}
drawGridLines();
QRectF rect = this->sceneRect();
setSceneRect(0, 0, rect.width() + 200, rect.height() + 200);
}
void CanttChartScene::drawVerticalAxis(const QStringList &platforms)
{
if (platforms.count() == 0)
{
return;
}
const double maxY = this->height();
//绘制垂直表头
int index = 0;
for (double y = axisStartPoint.y(); y <= maxY; y += verticalGridHeight)
{
if (index > platforms.count() - 1)
{
break;
}
QCheckBox *box = new QCheckBox;
box->setObjectName("PlatformCheckBox");
box->setStyleSheet("#PlatformCheckBox {"
"color: rgb(205, 218, 235);"
"background-color: rgb(43, 48, 54);"
"}"
"#PlatformCheckBox::indicator:unchecked {"
"border-image: url(:/img/checkbox/timebar-show.png) 0 0 0 0 stretch;"
"}"
"#PlatformCheckBox::indicator:checked {"
"border-image: url(:/img/checkbox/timebar-hide.png) 0 0 0 0 stretch;"
"}");
connect(box, &QCheckBox::clicked, [=](bool checked) {
auto list = m_plaformTimeBarHash.values(box->text());
if (checked)
{
foreach (QGraphicsItem *item, list)
{
item->hide();
}
}
else
{
foreach (QGraphicsItem *item, list)
{
item->show();
}
}
});
box->setText(platforms.at(index));
QGraphicsProxyWidget *proxy = addWidget(box);
proxy->setCursor(QCursor(Qt::PointingHandCursor));
proxy->setPos(QPoint(axisStartPoint.x(), y) + platformHeaderOffset);
m_platformStartYHash.insert(platforms.at(index), y);
index++;
}
}
CanttTimeBarItem *CanttChartScene::addPlanTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end)
{
if (!m_platformStartYHash.keys().contains(platform))
{
return nullptr;
}
//添加到缓存
auto pair = qMakePair(start, end);
m_planTimeBarTemp.insert(platform, pair);
//绘制时间条图形项
CanttTimeBarItem *item = new CanttTimeBarItem(start, end, CanttTimeBarItem::PlanTime, m_perPixelHMsecs);
double x = m_firstTimeBarStartX + (start.toMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch()) / m_perPixelHMsecs;
double y = m_platformStartYHash.value(platform) + 3;
addItem(item);
item->setPos(x, y);
m_plaformTimeBarHash.insert(platform, item);
return item;
}
CanttTimeBarItem *CanttChartScene::addRealTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end)
{
if (!m_platformStartYHash.keys().contains(platform))
{
return nullptr;
}
//添加到缓存
auto pair = qMakePair(start, end);
m_realTimeBarTemp.insert(platform, pair);
//绘制时间条图形项
CanttTimeBarItem *item = new CanttTimeBarItem(start, end, CanttTimeBarItem::RealTime, m_perPixelHMsecs);
double x = m_firstTimeBarStartX + (start.toMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch()) / m_perPixelHMsecs;
double y = m_platformStartYHash.value(platform) + canttTimeBarHeight + 6;
addItem(item);
item->setPos(x, y);
m_plaformTimeBarHash.insert(platform, item);
return item;
}
void CanttChartScene::setStepTimeValue(const QTime &time)
{
m_stepTimeValue = time;
m_perPixelHMsecs = m_stepTimeValue.msecsSinceStartOfDay() / (double)horizontalGridWidth;
#if 0
//时间步长更新后需要更新坐标轴
if (m_startDateTime.isNull() || m_endDateTime.isNull() || 0 == m_platforms.count())
{
return;
}
setAxisRange(m_startDateTime, m_endDateTime, m_platforms);
#endif
}
void CanttChartScene::drawGridLines()
{
const double maxY = this->height();
const double maxX = m_firstTimeBarStartX + m_columnCount * horizontalGridWidth;
//绘制第一条水平网格线
QGraphicsLineItem *item = new QGraphicsLineItem(axisStartPoint.x(), axisStartPoint.y(), axisStartPoint.x(), maxY);
item->setPen(QPen(gridLineColor));
item->setZValue(std::numeric_limits<int>::min());
addItem(item);
//绘制水平网格线
for (double x = axisStartPoint.x() + firstHorizantalGridWidth; x <= maxX; x += horizontalGridWidth)
{
QGraphicsLineItem *item = new QGraphicsLineItem(x, axisStartPoint.y(), x, maxY);
item->setPen(QPen(gridLineColor));
item->setZValue(std::numeric_limits<int>::min());
addItem(item);
}
//绘制垂直网格线
for (double y = axisStartPoint.y(); y <= maxY; y += verticalGridHeight)
{
QGraphicsLineItem *item = new QGraphicsLineItem(axisStartPoint.x(), y, maxX, y);
item->setPen(QPen(gridLineColor));
item->setZValue(std::numeric_limits<int>::min());
addItem(item);
}
}
3.2 CanttChartView
视图类很简单,主要就是把场景类的接口套了一下,因为视图最终会提供给外部使用,所以这里就套一下接口:
#ifndef CANTTCHARTVIEW_H
#define CANTTCHARTVIEW_H
#include <QGraphicsView>
#include <QDateTime>
class CanttChartScene;
class CanttTimeBarItem;
class CanttChartView : public QGraphicsView
{
Q_OBJECT
public:
explicit CanttChartView(QWidget *parent = 0);
void setAxisRange(const QDateTime &start, const QDateTime &end, const QStringList &platforms);
CanttTimeBarItem *addPlanTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end);
CanttTimeBarItem *addRealTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end);
void setStepTimeValue(const QTime &time);
protected:
virtual void wheelEvent(QWheelEvent *) override;
private slots:
void zoomIn();
void zoomOut();
private:
void scaleBy(double factor);
private:
CanttChartScene *m_pScene;
};
#endif // CANTTCHARTVIEW_H
#include "canttchartview.h"
#include "canttchartscene.h"
#include <QWheelEvent>
CanttChartView::CanttChartView(QWidget *parent) : QGraphicsView(parent)
{
m_pScene = new CanttChartScene(this);
setScene(m_pScene);
setAlignment(Qt::AlignLeft | Qt::AlignTop);
setDragMode(QGraphicsView::ScrollHandDrag);
setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
centerOn(0, 0);
}
void CanttChartView::setAxisRange(const QDateTime &start, const QDateTime &end, const QStringList &platforms)
{
m_pScene->setAxisRange(start, end, platforms);
}
CanttTimeBarItem *CanttChartView::addPlanTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end)
{
return m_pScene->addPlanTimeBar(platform, start, end);
}
CanttTimeBarItem *CanttChartView::addRealTimeBar(const QString &platform, const QDateTime &start, const QDateTime &end)
{
return m_pScene->addRealTimeBar(platform, start, end);
}
void CanttChartView::setStepTimeValue(const QTime &time)
{
m_pScene->setStepTimeValue(time);
}
void CanttChartView::wheelEvent(QWheelEvent *event)
{
if (event->delta() > 0)
{
zoomOut();
}
else
{
zoomIn();
}
}
void CanttChartView::zoomIn()
{
scaleBy(1.1);
}
void CanttChartView::zoomOut()
{
scaleBy(1.0 / 1.1);
}
void CanttChartView::scaleBy(double factor)
{
scale(factor, factor);
}
3.3 CanttTimeBarItem
这里要提一点就是,构造函数中factor参数,可以认为是每个像素代表多少毫秒,是一个缩放因子,由场景类中根据步长和网格宽度计算出的,从而计算出时间条对应的长度。
#ifndef CANTTTIMEBARITEM_H
#define CANTTTIMEBARITEM_H
#include <QGraphicsRectItem>
#include <QDateTime>
#include "definition.h"
class CanttTimeBarItem : public QGraphicsRectItem
{
public:
enum {Type = canttTimeBarType};
enum TimeType {
PlanTime,
RealTime
};
enum EventType {
TakeoffEvent,
RotationEvent,
SwitchChannelEvent,
LandEvent
};
explicit CanttTimeBarItem(const QDateTime &start, const QDateTime &end, TimeType type, double factor);
void addEvent(const QDateTime &dateTime, EventType type);
private:
QGraphicsItem *createEventItem(EventType type);
private:
double m_pFactor;
QDateTime m_startDateTime;
QDateTime m_endDateTime;
};
#endif // CANTTTIMEBARITEM_H
#include "cantttimebaritem.h"
#include "definition.h"
#include <QBrush>
#include <QPen>
#include <QCursor>
#include <QPoint>
#include <QLabel>
#include <QGraphicsProxyWidget>
const int eventItemYOffset = 2;
CanttTimeBarItem::CanttTimeBarItem(const QDateTime &start, const QDateTime &end, TimeType type, double factor)
: QGraphicsRectItem(nullptr),
m_pFactor(factor),
m_startDateTime(start),
m_endDateTime(end)
{
double width = (end.toMSecsSinceEpoch() - start.toMSecsSinceEpoch()) / m_pFactor;
setRect(0, 0, width, canttTimeBarHeight);
setCursor(QCursor(Qt::PointingHandCursor));
if (CanttTimeBarItem::PlanTime == type)
{
setBrush(QBrush(QColor(92, 201, 221)));
}
else
{
setBrush(QBrush(QColor(233, 252, 74)));
}
QPen pen;
pen.setStyle(Qt::NoPen);
setPen(pen);
}
void CanttTimeBarItem::addEvent(const QDateTime &dateTime, CanttTimeBarItem::EventType type)
{
if (dateTime < m_startDateTime || dateTime > m_endDateTime)
{
return;
}
QGraphicsItem *item = createEventItem(type);
double x = (dateTime.toMSecsSinceEpoch() - m_startDateTime.toMSecsSinceEpoch()) / m_pFactor;
item->setPos(x, eventItemYOffset);
}
QGraphicsItem *CanttTimeBarItem::createEventItem(CanttTimeBarItem::EventType type)
{
QLabel *label = new QLabel;
label->setStyleSheet("QLabel {"
"background-color: transparent;"
"min-height: 12px;"
"max-height: 12px;"
"font-size: 11px;"
"padding-left: -2px;"
"border-width: 0 0 0 12;"
"border-image: url(:/img/event/takeoff.png) 0 0 0 64;}");
label->setToolTip(QStringLiteral("开始起飞\n人员:张三\n地点:xxx根据地"));
switch (type)
{
case CanttTimeBarItem::TakeoffEvent:
label->setText(QStringLiteral("起飞"));
break;
case CanttTimeBarItem::RotationEvent:
label->setText(QStringLiteral("转角"));
break;
case CanttTimeBarItem::SwitchChannelEvent:
label->setText(QStringLiteral("切换频道"));
break;
case CanttTimeBarItem::LandEvent:
label->setText(QStringLiteral("降落"));
break;
default:
break;
}
QGraphicsProxyWidget *proxy = new QGraphicsProxyWidget(this);
proxy->setWidget(label);
return proxy;
}
4 总结
在开发过程中,并不一定是先设计底层接口,有时候我们应该从业务角度去思考自己需要什么样的接口,然后根据需要去开发,从上往下去想,往往会有事半功倍的效果。
5 下载
每一步踏出,都是一次探索,一次成长。