自己编写甘特图的绘制程序
甘特图的绘制的数学原理
假设我有一个任务列表数组:Days = [(任务1, day_start, day_end), (任务2, day_start, day_end), ..., (任务n,day_start, day_end)]
在这个列表中:
-
一定会有一个任务的开始时间在所有其他任务开始之前(或刚好相等),设该任务为:(任务i, day_start_i, day_end_i);
-
一定会有一个任务的结束时间在所有其他任务结束之后(或刚好相同),设该任务为:(任务j,day_start_j, day_end_j);
我们需要有一个坐标轴来展示这些任务,问题是这些坐标轴的范围如何确认呢?假设我需要用以下的方式展示这个甘特图:
我们以固定甘特图的绘制的大小的方式来绘制甘特图,至于视图层要以什么样的viewport观察这个甘特图,那是视图层QGraphicsView的任务,我不想管这个,因为别人已经实现地很好了。
由于不重叠Item的Scene在视图层渲染方面的性能是足够的,这一点在官方所给的40000flips例子中已经得到验证,所以不需要担心缩放范围太大或太小,或者任务的数量太多。因为以目前人类的脑力水平来说,管理上百个任务已经是比较大的项目了,如果任务上千甚至上万,程序也能够表现良好。因此可以认为,该甘特图可以符合在大多数情况下的项目管理的甘特图编写的需求。
甘特图的总宽度
这里假设day_start_i = (year_start_i, month_start_i, day_start_i),则最左边的月份轴为:year_start_i年month_start_i月的上一个月(由于月份的计算需要进行回绕,所以数学方面不好表述出来)。
假设day_end_j = (year_end_j, month_end_j, day_end_j),则最右边的月份轴为:year_end_j年month_end_j月的下一个月。
因此,需要绘制的坐标轴的范围是:[最左月份轴,最右月份轴],假设这个序列为:[axis_0, axis_1, ..., axis_m]。
假设左上坐标为(0, 0),那么最左月份轴的x轴坐标为多少呢?不可能定义为0,因为这样的话月份轴可能渲染不到。那么究竟定义为多少呢?这里我从月份轴上的xxxx年xx月
入手,假设这个文本框的显示需要的大小为[w_month, h_month]
,如果想要这个文本框刚好位于最左上角,然后对应的月份轴居中于该文本框,则月份轴的x坐标为:w_month / 2
。
最左月份轴的x轴坐标定义完成,那么最左的下一个月份轴的x坐标是什么?下下个呢?由于我们期望这些月份轴之间间隔相同,因此我们需要定义好月份之间的间隔,假设这个间隔的距离为month_spacing
。那么最左月份的下一个月份轴的x轴坐标是x = w_month / 2 + month_spacing * i
,i是月份轴在序列中的下标。
那么,整个甘特图的最右边的x坐标(注意不是最右边月份轴的)是:w_month + month_spacing * m
。
这个图很清晰地反映了月份轴的x轴的坐标分布。只要我们定义好:
-
月份文本框的大小
(w_month, h_month)
。 -
月份轴间的间隔
month_spacing
。 -
月份轴的数量
m + 1
。
那么就可以计算得出整个甘特图的总宽度w_month + month_spacing * m
。
甘特图的总高度
我们前面假设我们具有n个任务,任务的y轴坐标应该如何定义?
时间轴t我们可以简单定义为:(x, y = h_month)
。
答案是:h_month + top + (h + spacing) * i
,这里的h_month
是月份文本框的高度,而top
则是第一个任务与时间轴t的距离(毕竟总不能贴在一起),至于h
则是甘特图中任务的范围矩形的高度,spacing
则是任务之间的间隔距离。以上这些都是程序获得任务列表之前能够定义好的,但只有获得了任务列表之后,我们才能获得i
,即任务在任务数组中的下标。
就像我们不想要第一个任务紧贴时间轴t,我们也不想要最后一个任务紧贴甘特图的底边,最后一个任务距离甘特图的底边有bottom
的距离。
最终,我们可以得出甘特图的总高度为:h_month + top + (h + spacing) * (n-1) + bottom
。
因此,我们可以确定,在预先定义好了种种常量(像h_month
top
之类的)之后,在用户输入任务列表时,我们可以马上确定甘特图的大小是多少。因此,我们可以完全可以将甘特图放入一个Item中。
月份轴的文本框坐标计算
月份轴按从小到大排序,并分配下标,下标为i的月份轴对应的文本框的坐标为:(month_spacing * i, 0)
。
任务i的坐标计算
假设任务i在任务数组中的下标为i,我们需要计算得出其左上坐标,和右下坐标,方便绘制图形。
左上坐标为:(x_i_start, h_month + top + (h + spacing) * i)
,右下坐标为:(x_i_end, h_month + top + (h + spacing) * i + h)
。
这里的重点在于求出x_i_start
和x_i_end
。
对于x_i_start
我们可以使用x_{i-start}=\frac{任务i的开始日}{任务i所在月份的天数}\times month\_spacing+任务i所在月份的月份轴的x轴坐标。
对于x_i_end
的计算其实与x_i_start
同理。
因此,我们只要有计算得出任务i所在月份的天数的方法,由于任务i的开始日已知,我们就可以很轻松地计算出这些坐标。
而某年某月的天数,可以使用Python中的calender
库来获取,只需指定年月就可以通过获取其天数。
到此为止,关于甘特图大部分内容的数学原理都已经基本完成了,接下来就是设计好代码结构,合理定义接口,把这些数学原理实现出来。
就算以后要使用以天为单位的甘特图,那我们也可以新搞一个可以显示更加详细数据的甘特图。这个月份的甘特图就可以作为概要图来显示。
软件接口定义
考虑到数据量不会太过复杂,所以这里采用所有任务都需要进行统一的运算,也就是说,我会将所有任务都打包送给某个对象进行处理,处理完成之后,外界可以从该对象获取处理完成的信息。如果原来的任务有任何一个发生变化,那么就需要对所有任务进行的坐标信息之类的进行修改,因此,任务越多,计算任务就越繁重。这是一种不好的设计,我认为不应该这样做。
关于甘特图的总高度和总宽度,我们可以通过动态统计它们,从而不需要重新计算所有任务。
对于新添加的任务,无论这个任务的范围是什么,只要我们调整了总宽度和总高度以及月份轴的位置,那么原有的任务的框的计算方式不需要变化,我们只需要为新添加的任务分配下标和坐标,其他任务的下标改变无所谓,因为不影响显示。
即新添加的任务完全可以只分配好坐标和下标即可,而无需担心原有的绘制和新的绘制出现冲突,因为间隔的距离保证了它们之间不会有冲突。但是绘制的代价不会变,每一次修改我们必然需要重新绘制一遍甘特图,这个绘制的开销与任务的数量是成正相关的,我们无法改变(只要发生了修改),但是我们可以通过缓存避免不必要的刷新,但根据官方的示例,只要不是大量重叠,大数量的Item的刷新根本不是问题。因此,这个方法很大概率是可行的。
因此,关于甘特图绘制的数学原理应该是这样一个对象:可以定义初始任务列表,并在后续动态向里面添加任务的对象,这个对象维护着月份轴的位置,总高度,总宽度,月份文本框大小,各种间隔数据供Item对象取用的对象。这是一个为甘特图Item提供坐标计算辅助的对象!
对象的数据组成
-
月份轴数组:由于月份轴数据涉及到年份,所以这里打算使用“年月”(可对Python中的
datetime
作一定的限制来使用,或者利用该对象封装一个合适的MonthTime)作为键,而值为月份轴的x轴坐标。仅存储所需要的月份轴坐标,也就是说如果任务的增减导致了有些月份轴没有必要存在,则删除对应的月份轴。 -
间隔设置:提供一些基本的间隔数据以定义好甘特图的格式,如甘特图总高度或总宽度,月份框大小,任务垂直间隔,上下间隔等等。
计算任务的左上和右下坐标时,需要通过获得月份轴坐标来实时计算,这是因为任务的位置是相对于月份轴而言的。
因此,维护月份轴数据很重要:
-
当添加新任务或删除现有任务时,需要更新月份轴数据,还有甘特图的总高度和总宽度数据。
经过试验的合适常量:
-
年月文本框的大小:
QSizeF(74.1875, 24.0)
。 -
month_spacing
,由于年月文本框的宽度为74.1875
,如果month_spacing < 74.1875
则两个相邻的文本框将会重叠,因此month_spacing >= 74.1875
,我想要这两个文本框相隔远一点,因此定义month_spacing = 180.0
。 -
bottom = top = spacing = 12.0
。 -
h = 24.0
作为任务框的高度。
现在的问题是,如何定义输入的任务列表呢?这个任务列表最好还要兼容未来的数据库。这里我的解决方案是,数据库方面的搞一个查询,查询任务和开始时间和结束时间,然后经过程序的处理变成对象可以处理的东西。因此,在这里,我们需要定义好这样一个对象。
简单搭建一番之后,可以做到这样程度的排布:
接下来是使用GanttExplainer来绘制完整的甘特图。我们就先用矩形框代替将来可能的任务条Item先,后续再加上一个文本之类的。
在经过粗糙的搭建后,达成了以下的效果:
可以看到,我们的程序已经可以粗略地将这个甘特图的数据显示出来了,虽然很简陋但至少能用不是吗?就先这样子。