Qt-时间轴2插件开发

这篇文章介绍的自定义插件比较简单,主要都是通过绘图完成的。 特点如下: 1. 精度是可以放缩 2. 时间轴时间连续 3. 可以通过鼠标左右滑动改变时间轴显示时间。

思路逻辑

基本需求

timeline2_overview

  • 时间轴时间连续
  • 时间轴精度可以放缩
  • 时间轴可以左右滑动

简单的逻辑

  • 考虑到时间轴精度放缩的时候是以时间轴中间的时间为基本(中间的时间位置不变,一直在视图中间),所以所有的刻度生成都是基于时间轴中间的时间来计算
  • 假设提供一个中间时间,每次通过绘制当前中间时间在一定的时间精度等级下的图,当左右鼠标拖动时,改变中间时间值,再次重新绘制,这样基本就可以满足基本需求了

绘图

由于全是绘图事件。首先需要知道需要绘制哪些内容

  1. 最简单的背景
  2. 时间刻度
  3. 中间的游标
  4. 需要显示的内容

计算刻度数值

计算具体的时间刻度数值, 这一部分稍微麻烦一点。在已知中间的时间,通过像素和时间之间的比例,求出时间轴最左侧的起点时间,根据不同的精度,绘制完整的时间轴

代码逻辑

整体逻辑

根据上面的逻辑, 在 paintEvent(QPaintEvent *) 函数中对每一部分需要做的做一个简单说明

void DrawTimeline::paintEvent(QPaintEvent *)
{
    // 绘制背景
    QPainter painter(this);
    painter.setBrush(QColor{50,50,50});
    painter.drawRect(rect());

    // 绘制时间刻度
    drawTimescale(painter);

    // 显示中间位置时间
    m_midDisplayLabel->setText(m_currentDateTime.toString("yyyy-MM-dd hh:mm:ss"));
    m_midDisplayLabel->move(this->size().width()/2-m_midDisplayLabel->width()/2-5,10);

    // 绘制具体的时间数据
    drawTimeRangeInfo(painter);

    // 绘制中间的游标
    drawTimeCursor(painter);
}

接下来主要先把简单的内容先说明以下

中间位置时间

m_currentDateTime 记录当前的时间
m_midDisplayLabel 是一个 QLabel, 主要就是显示 m_currentDateTime 时间, 位置在视图的中央

绘制中间游标

timeline2_cursor

中间的游标分为2部分
第一部分是一个红色的竖线,位置在视图中间位置,只需要确定y方向的高度
第二部分是一个类似标签一样的5边形,可以通过提供5个点的位置, 使用 drawPolygon() 函数直接绘制得到

void DrawTimeline::drawTimeCursor(QPainter &painter)
{
    painter.setPen( QPen{QColor{255,0,0,200},2});
    painter.drawLine(this->width()/2+1, CURSORHEIGTH, this->width()/2+1, 105);
    QPolygonF cursorPolygon;
    cursorPolygon << QPointF(this->size().width()/2,   CURSORHEIGTH)
                << QPointF(this->size().width()/2+4, CURSORHEIGTH-4)
                << QPointF(this->size().width()/2+4, CURSORHEIGTH-12)
                << QPointF(this->size().width()/2-4, CURSORHEIGTH-12)
                << QPointF(this->size().width()/2-4, CURSORHEIGTH-4) ;
    painter.setBrush(QColor{200,200,200});
    painter.setPen(QColor{200,200,200});
    painter.drawPolygon(cursorPolygon);
}

绘制时间刻度

在绘制时间刻度之前,首先需要总结大概需要的时间和距离的关系。
我的代码中,目前分为8个精度 12个小时,6个小时,2个小时, 1个小时, 30分钟, 10分钟, 5分钟, 1分钟
同时每个小的时间段(时间步长)对应的像素(像素步长),我设置的比较随意, 50像素开始到120像素,每1级加10像素,这样在调整精确度的时候,有放缩的感觉。

定义结构体 TimeForDistance, 存储时间步长和像素步长的关系
QVector<TimeForDistance> m_vecStep 存储我所有的精度信息
int m_sizeLevel 存储当前绘图使用的精度等级(0-7),对应 m_vecStep 中的信息

struct TimeForDistance
{
    int timestep;
    float pixelstep;
};
QVector<TimeForDistance> m_vecStep;   这个变量存储所有的像素和时间的关系
int m_sizeLevel;

所有的准备工作都做好了,接下来就是绘制时间刻度
这边需要对刻度做一下简单的说明, 我以一小时的时间步长为例,时间刻度显示应该是完整的小时, 比如00:00, 01:00, 02:00,等等
总结以下 如果已知中间时间,如何绘制完整的时间轴

  • 已知中间时间,可以根据对应精度中每个像素代表多少时间,求出时间轴上起点的时间
  • 因为起点时间并不能作为时间轴上刻度,我们需要算出起点时间右侧的第一个刻度
    • 我将起点时间全部换成秒为单位的数值,该数值除以步长的整数部分+1之后乘以步长就能得到右侧第一个刻度S的数值
    • 实际上我们只关心起点的位置信息, 计算起点时间和右侧第一个刻度的时间差值,在换成位置信息即可
  • 在算出第一个刻度位置后,之后的时间刻度累加像素步长,可以得到完整的时间轴

接下来简单看一下代码,代码就是按照这个逻辑实现的

void DrawTimeline::drawTimescale(QPainter &painter)
{
    // 绘制容纳时间的矩形
    painter.setPen( QColor{150,150,150});
    painter.drawRect(-1,RECTY,this->width()+40,RECTHEIGH);

    float pixelstep = m_vecStep.at(m_sizeLevel).pixelstep;
    int timestep = m_vecStep.at(m_sizeLevel).timestep;
    float secsper = timestep/pixelstep;

    QDateTime timeline_starttime =  m_currentDateTime.addSecs(-( this->width()/2)*secsper);
    int timeH = timeline_starttime.time().hour();
    int timeM = timeline_starttime.time().minute();
    int timeS = timeline_starttime.time().second();
    int second = timeH*3600+timeM*60+timeS;

    int drawstarttime = (second/timestep+1)*timestep - second;
    int startPosition = drawstarttime / secsper;

    QDateTime show_starttime = timeline_starttime.addSecs(drawstarttime);
    QFont font{"\"Microsoft YaHei\"",8,57};
    int count  = this->size().width() / pixelstep;
    for (int i = 0; i <= count; ++i) {
        float timeX = pixelstep*i+startPosition -10;
        painter.setFont(font);
        painter.drawText(timeX, RECTY-5, show_starttime.toString("hh:mm"));
        painter.drawLine(pixelstep*i+startPosition, RECTY-1, pixelstep*i+startPosition, RECTY+25);
        show_starttime = show_starttime.addSecs(timestep);
    }
}

绘制具体的时间数据

QVector<TimeRange> m_vecTimeRangeInfo 存储所有的时间信息
结构体 TimeRange 中只记录每个时间信息的开始时间和结束时间

struct TimeRange
{
    QDateTime starttime;
    QDateTime endtime;
};

在得到开始时间和结束时间之后, 通过计算开始时间的位置,以及时间的长度,在时间轴中绘制一个矩形

void DrawTimeline::drawTimeRangeInfo(QPainter &paint)
{
    for (int i=0; i<m_vecTimeRangeInfo.count(); ++i) {
        QDateTime stime = m_vecTimeRangeInfo.at(i).starttime;
        QDateTime etime = m_vecTimeRangeInfo.at(i).endtime;

        float pixelstep = m_vecStep.at(m_sizeLevel).pixelstep;
        int timestep = m_vecStep.at(m_sizeLevel).timestep;
        float secsper = timestep/pixelstep;
        float sToCurrent = m_currentDateTime.secsTo(stime);
        float eToCurrent = m_currentDateTime.secsTo(etime);
        float length = eToCurrent - sToCurrent;

        int timeX = sToCurrent/secsper + this->width()/2;
        int timeLen = length/secsper;

        paint.setPen(QPen(Qt::darkYellow,1,Qt::NoPen));
        paint.setBrush(QBrush(Qt::cyan,Qt::SolidPattern));
        paint.drawRect(timeX,RECTY+1,timeLen,RECTHEIGH-1);
    }
}

左右滑动

这一部分比较简单主要重新实现以下几个函数

virtual void mousePressEvent(QMouseEvent *event);
virtual void mouseReleaseEvent(QMouseEvent *event);
virtual void mouseMoveEvent(QMouseEvent *event);

在鼠标点击的时候记录一个位置信息,和点击时的 m_currentDateTime 时间
在鼠标移动过程中计算移动的距离,通过当前的精度, 换算出新的 m_currentDateTime 值, 之后调用 update() 刷新绘图即可

精度的切换

之前通过 m_sizeLevel 来记录当前的精度等级,在放大和缩小精度的时候,对应改变它的值,之后调用 update() 刷新绘图即可

扩展

  • 放大缩小可以引入鼠标滚轮事件 void wheelEvent(QWheelEvent *event)

代码地址

github 地址 :https://github.com/catcheroftime/DrawTimelinePlugin