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 CattChartScene
場景類主要完成網格線的繪製,同時對外提供前面提到的添加時間條的兩個接口,以及時間步長設置接口,頭文件代碼如下:
#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 總結
在開發過程中,並不一定是先設計底層接口,有時候我們應該從業務角度去思考自己需要什麼樣的接口,然後根據需要去開發,從上往下去想,往往會有事半功倍的效果。