本系列预计实现
①刻度上方文字显示,
②时间轴拖动效果,
③时间轴刻度缩放,
④时间轴和其他控件联动显示,
⑤鼠标放置到时间轴,显示具体时间。
完整代码可见GitHub - 754816/QT_TimeLine: qt时间轴实现效果
1、基础思路
当按下鼠标时,时间轴的会随着鼠标的移动,而水平跟随移动等量像素位置,视觉上看起来,时间轴被拖动了一样,实际上则是时间轴上每根竖线,都根据鼠标移动的位置,而进行了偏移,所以在计算竖线的x坐标时,需要加上偏移量
2、先添加鼠标事件,获取到鼠标移动位置
这里运用了鼠标的按下,移动,释放三个事件
在MyTimeLine.hpp中添加上三个虚函数
//MyTimeLine.hpp
void mousePressEvent(QMouseEvent *event);
void mouseMoveEvent(QMouseEvent *event);
void mouseReleaseEvent(QMouseEvent *event);
然后在cpp中实现,设定当点击的为鼠标左键时,才继续处理
if (event->button() == Qt::LeftButton) {
//do something
}
3、继续拆解鼠标移动逻辑
鼠标的移动可以看成两次鼠标位置的差异,在处理中,可以选择在mousePressEvent函数中获取一次初始鼠标位置(m_BeginX),然后在mouseMoveEvent中反复获取鼠标位置(CurMoveX),在mouseReleaseEvent中记录下本次总共的移动位置(m_totalX)。由于拖动是一个持续的过程,所以会不断的触发mouseMoveEvent函数,所以需要计算mouseMoveEvent中每次鼠标位置相对于开始时的偏移量(m_MoveX)
当然也有不断的获取mouseMoveEvent中,然后每次记录鼠标移动差异的方法,只是并不直观,并且每次都只大约移动了1个像素,就会触发一次mouseMoveEvent,这种像素差距较小的情况,并不利于时间轴的计算,容易导致拖动距离过长时,产生绘制误差。
那么当第一次拖动时,按照CurMoveX绘制即可,拖动完成后得到一个m_totalX,但是由于存在多次的拖动,第二次绘制时,则是本次移动的而加上之前累计移动的像素,得到综合的时间轴偏移量。所以m_MoveX实际上等于m_totalX + curMoveX
void MyTimeLine::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
//记录鼠标的按下位置,作为初始位置,存储到全局变量m_BeginX中
m_BeginX = event->pos().x();
}
}
void MyTimeLine::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
//获取鼠标的本次移动位置
int curMoveX = event->pos().x() - m_BeginX;
m_MoveX = m_totalX + curMoveX;
update();
}
}
void MyTimeLine::mouseReleaseEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
//统计本次一共移动了多少位置,并且将其累计起来
m_totalX = m_totalX + event->pos().x() - m_BeginX;
m_MoveX = m_totalX;
}
}
4、在计算位置中,每次都添加上偏移
设定时间轴的中间为当前时间,当鼠标向左拖动时,时间轴也跟着向左移动,计算的像素为负,当前时间实际为增加,鼠标向右拖,时间为减小,计算的像素为正。
那么计算时,带入鼠标的偏移。
//添加长竖线, 需要添加13根长竖线
for(int i = 0; i <= 12; i++)
{ //第i根长竖线w = i*单个块长度
int LongW = i * m_blockWidth + mouseMoveX;
lines.push_back(QLine(LongW, TimeLineHeight / 2, LongW, TimeLineHeight));
}
结合上一节初始时间的偏移,时间轴拖动后是有两个偏移量的,把鼠标的偏移量定为 t1,把初始时间的偏移量定为 t2, 把综合偏移量定为 moveRatio,每小时间隔像素为m_blockWidth,那么最终时间轴则是偏移了 moveRatio * m_blockWidth 个像素。
double moveRatio = CalMoveRatio(m_MoveX, m_dateTime, m_blockWidth);
int MovePixMod = (int)moveRatio * m_blockWidth;
//添加长竖线, 为了展示效果合理性,需要添加13+2根长竖线,+2表示余出左右两边,以应对拖动
for(int i = -1; i <= TimeLineStyle::TimeCount + 1; i++)
{ //第i根长竖线w = 左边Margin + i*单个块长度 - 像素偏移量
int LongW = LeftMargin + i * m_blockWidth - MovePixMod;
lines.push_back(QLine(LongW, TimeLineHeight / 2, LongW, TimeLineHeight));
}
double MyTimeLine::CalMoveRatio(int movepix, QDateTime datetime, double moveRatio)
{
int interval = 3600;
//t1:鼠标累计的偏移像素; t2:非整点时间的偏移位置
double t1, t2;
if(0 == moveRatio) t1 = 0;
else t1 = (double)movepix / moveRatio;
int ss = 0;
if(_OneHour == interval)
{
ss = datetime.time().minute() * 60 + datetime.time().second();
}
t2 = (double)ss / (double)interval;
//t > 0,表示实际时间增大,鼠标是执行了向左拖动,t < 0,时间减小,鼠标向右拖
double t = t2 - t1;
return t;
}
以上是拖动的原理,但是在实际应用中,存在着时间轴拖动的距离比较长,导致拖动的区域出现了一片空白的情况,这里就需要补充绘制空白的区域,经过观察可以看出,其实时间轴的竖线都是重复的块,所以补充绘制块即可,后面再单独计算文字信息。
15:00:00-16:00:00是一个块,16:00:00-17:00:00是重复的块,17:00:00-18:00:00是重复的块,它们的竖线绘制都是一样的,只是文字不一样。
所以上面计算偏移量的实际,实际上只需要对结果取模,然后将除法运算结果带入到时间文字信息中
int MovePixMod = (int)((double)moveRatio * (double)m_blockWidth) % m_blockWidth;
5、计算文字信息
首先从中间的时间开始,中间假设为12:34:56,左边长竖线有7个,最近的为12:00:00,可以计算出最左边的则为06:00:00,右边的长竖线有6个,最右边为18:00:00。计算时,先求出最左边的时间,然后后面的按照间隔,每次加1小时,就得到刻度时间。
QString MyTimeLine::CalText(const int index, const QDateTime midDateTime, const int moveRatio)
{
QTime time(0,0,0);
//STEP 1: 将时间按照间隔进行取模处理,例如间隔为1h,12:34:56处理后为12:00:00
QDateTime IntegerTime = midDateTime;
IntegerTime.setTime(QTime(midDateTime.time().hour(), 0, 0));
//STEP 2: 根据中间的时间和Interval,计算出最左边的时间
QDateTime LeftTime = DateTimeUtil::sub(IntegerTime, 6 * 3600);
//STEP 3: 根据偏移量,更新最左边的时间
QDateTime MoveLeftTime = DateTimeUtil::sub(LeftTime, -1 * moveRatio * 3600);
//STEP 4: 根据index,计算i个间隔后的时间
time = DateTimeUtil::sub(MoveLeftTime, -1 * index * m_IntervalType).time();
return time.toString("hh:mm:ss");
}
由于Qt没有合适的时间减法,所以可以将QDateTime转为时间戳,计算后,再由时间戳转为QDateTime。这里给出封装一层的时间减法函数,注意当拖动的时间,大于了当前时间时,则这个不再适用。
QDateTime DateTimeUtil::sub(const QDateTime& time1, const QDateTime& time2)
{
qint64 t = time1.toSecsSinceEpoch() - time2.toSecsSinceEpoch();
if(t < 0) t = 0;
QDateTime dt = QDateTime::fromSecsSinceEpoch(t);
return dt;
}
QDateTime DateTimeUtil::sub(const QDateTime& time1, const qint64& seconds)
{
qint64 t = time1.toSecsSinceEpoch() - seconds;
if(t < 0) t = 0;
QDateTime dt = QDateTime::fromSecsSinceEpoch(t);
return dt;
}