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 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 總結

在開發過程中,並不一定是先設計底層接口,有時候我們應該從業務角度去思考自己需要什麼樣的接口,然後根據需要去開發,從上往下去想,往往會有事半功倍的效果。

5 下載

示例代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章