SimpleGanttChartView.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Drawing.Imaging;
  5. using System.Linq;
  6. using System.Windows.Forms;
  7. namespace GanttChartPoC.GanttChart
  8. {
  9. [System.Runtime.InteropServices.Guid("84E7DA58-BE12-478F-A463-2FDA37CFD2F0")]
  10. internal partial class SimpleGanttChart : UserControl
  11. {
  12. private enum Layer
  13. {
  14. Background = 0,
  15. Top,
  16. MaxValue,
  17. }
  18. private static readonly StringFormat StringFormatMiddleCenter = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
  19. private readonly Bitmap[] _layerBitmaps = new Bitmap[(int)Layer.MaxValue];
  20. private GanttData _data;
  21. private float[] _machineRowHeights;
  22. private DateTime[] _timelineDays;
  23. private RectangleF[][] _taskRectangles;
  24. private RectangleF[] _dayWidthAdjustHandles;
  25. private Point? _mousePosition;
  26. private int? _hoverMachine, _hoverTask, _hoverDayWidthHandler;
  27. private int? _holdDayX;
  28. public float TimelineOffsetHours { get; set; } = 8;
  29. public float TaskBlockVerticalPadding { get; set; } = 4;
  30. public float TimelineTickerFrequencyHour { get; set; } = 4;
  31. public float TimelineTickerHeight { get; set; } = 4;
  32. public float TimelineHeight { get; set; } = 64;
  33. public float TimelineDayWidth { get; set; } = 360;
  34. public float TimelineDayAdjustHandleWidth { get; set; } = 4;
  35. public float MinRowHeight { get; set; } = 64;
  36. public float MachineCellWidth { get; set; } = 120;
  37. public Color TaskBlockBgColor { get; set; } = Color.FromArgb(128, 204, 255, 204);
  38. public SimpleGanttChart()
  39. {
  40. InitializeComponent();
  41. SetData(new GanttData(new GanttMachine[0]));
  42. }
  43. public void SetData(GanttData data)
  44. {
  45. _data = data ?? throw new ArgumentNullException(nameof(data));
  46. CalcLayoutAndCreateBitmaps();
  47. HorizontalScroll.Value = 0;
  48. VerticalScroll.Value = 0;
  49. ChartPictureBox.Invalidate();
  50. }
  51. private void CalcLayoutAndCreateBitmaps()
  52. {
  53. var bmpSize = new SizeF(320, 240);
  54. if (0 < _data?.Machines?.Length)
  55. {
  56. var bmpHeight = TimelineHeight;
  57. var bmpWidth = MachineCellWidth;
  58. var allTasks = _data.Machines.SelectMany(p => p.Tasks).ToArray();
  59. if (0 < allTasks.Length)
  60. {
  61. var minDate = allTasks.Min(p => p.Begin).Date;
  62. var maxDate = allTasks.Max(p => p.End).Date;
  63. var days = new List<DateTime>(7);
  64. var ptr = minDate;
  65. do
  66. {
  67. days.Add(ptr);
  68. ptr = ptr.AddDays(1);
  69. } while (ptr <= maxDate);
  70. var minBeg = allTasks.Min(p => p.Begin);
  71. if (minBeg.Hour < TimelineOffsetHours) days.Insert(0, minBeg.Date.AddDays(-1));
  72. _timelineDays = days.ToArray();
  73. }
  74. else
  75. {
  76. _timelineDays = new DateTime[0];
  77. }
  78. bmpWidth += _timelineDays.Length * TimelineDayWidth + TimelineDayWidth; // 追加一天宽度,字可能会超出
  79. _machineRowHeights = new float[_data.Machines.Length];
  80. _taskRectangles = new RectangleF[_data.Machines.Length][];
  81. var hourWidth = TimelineDayWidth / 24f;
  82. var machineOffsetY = TimelineHeight;
  83. for (var iMachine = 0; iMachine < _data.Machines.Length; iMachine++)
  84. {
  85. var machine = _data.Machines[iMachine];
  86. var taskOffsetY = 5f;
  87. _taskRectangles[iMachine] = new RectangleF[machine.Tasks.Length];
  88. for (var iTask = 0; iTask < machine.Tasks.Length; iTask++)
  89. {
  90. var task = machine.Tasks[iTask];
  91. var span = task.End - task.Begin;
  92. var left = (MachineCellWidth + TimelineDayWidth * Array.IndexOf(_timelineDays, task.Begin.Date)) + (float)(hourWidth * task.Begin.TimeOfDay.TotalHours) - hourWidth * TimelineOffsetHours;
  93. var top = machineOffsetY + taskOffsetY;
  94. var width = (float)(hourWidth * span.TotalHours);
  95. var height = Font.Height + TaskBlockVerticalPadding + TaskBlockVerticalPadding;
  96. _taskRectangles[iMachine][iTask] = new RectangleF(left, top, width, height);
  97. var taskTextSize = TextRenderer.MeasureText(task.Text, Font, Size.Empty);
  98. if (taskTextSize.Width > width) taskOffsetY += height;
  99. }
  100. var rowHeight = taskOffsetY + 5 < MinRowHeight ? MinRowHeight : taskOffsetY + 5;
  101. bmpHeight += rowHeight;
  102. _machineRowHeights[iMachine] = rowHeight;
  103. machineOffsetY += rowHeight;
  104. }
  105. bmpSize = new SizeF(bmpWidth, bmpHeight);
  106. _dayWidthAdjustHandles = new RectangleF[_timelineDays.Length];
  107. var halfHandleWidth = TimelineDayAdjustHandleWidth / 2;
  108. for (var iDay = 0; iDay < _timelineDays.Length; iDay++)
  109. {
  110. var left = MachineCellWidth + (iDay + 1) * TimelineDayWidth - halfHandleWidth;
  111. _dayWidthAdjustHandles[iDay] = new RectangleF(left, 0, TimelineDayAdjustHandleWidth, bmpHeight);
  112. }
  113. }
  114. ChartPictureBox.Size = bmpSize.ToSize();
  115. for (var i = 0; i < _layerBitmaps.Length; i++)
  116. {
  117. var toDispose = _layerBitmaps[i];
  118. _layerBitmaps[i] = new Bitmap((int)bmpSize.Width, (int)bmpSize.Height, PixelFormat.Format32bppArgb);
  119. toDispose?.Dispose();
  120. }
  121. RenderBackgroundLayer();
  122. RenderTopLayer();
  123. }
  124. private void RenderBackgroundLayer()
  125. {
  126. var bmp = _layerBitmaps[(int)Layer.Background];
  127. using (var g = Graphics.FromImage(bmp))
  128. using (var taskBgBrush = new SolidBrush(TaskBlockBgColor))
  129. {
  130. g.SetHighQuality();
  131. g.Clear(Color.White);
  132. var rcFull = new Rectangle(0, 0, bmp.Width - 1, bmp.Height - 1);
  133. g.DrawRectangle(Pens.Black, rcFull);
  134. if (0 == _data.Machines.Length)
  135. {
  136. g.DrawString("无数据", Font, Brushes.Black, rcFull, StringFormatMiddleCenter);
  137. }
  138. else
  139. {
  140. var yOffset = TimelineHeight;
  141. g.DrawLine(Pens.Black, 0, yOffset, bmp.Width, yOffset);
  142. for (var index = 0; index < _data.Machines.Length; index++)
  143. {
  144. var machine = _data.Machines[index];
  145. var height = _machineRowHeights[index];
  146. g.DrawString(machine.Name, Font, Brushes.Black, new RectangleF(0, yOffset, MachineCellWidth, height), StringFormatMiddleCenter);
  147. yOffset += height;
  148. g.DrawLine(Pens.Black, 0, yOffset, bmp.Width, yOffset);
  149. }
  150. var xOffset = MachineCellWidth;
  151. g.DrawLine(Pens.Black, xOffset, 0, xOffset, bmp.Height);
  152. foreach (var day in _timelineDays)
  153. {
  154. g.DrawString($"{day:yyyy-MM-dd (ddd)}", Font, Brushes.Black, new RectangleF(xOffset, 0, TimelineDayWidth, TimelineHeight), StringFormatMiddleCenter);
  155. for (var h = TimelineTickerFrequencyHour; h < 24; h += TimelineTickerFrequencyHour)
  156. {
  157. var left = xOffset + h / 24 * TimelineDayWidth;
  158. g.DrawLine(Pens.Black, left, TimelineHeight - TimelineTickerHeight, left, TimelineHeight);
  159. var offsetH = h + TimelineOffsetHours;
  160. if (offsetH >= 24) offsetH -= 24;
  161. var spanText = $"{offsetH:00}";
  162. var spanTextSize = g.MeasureString(spanText, Font);
  163. g.DrawString(spanText, Font, Brushes.Black, left - spanTextSize.Width / 2f, TimelineHeight - TimelineTickerHeight - spanTextSize.Height);
  164. }
  165. xOffset += TimelineDayWidth;
  166. g.DrawLine(Pens.Black, xOffset, 0, xOffset, bmp.Height);
  167. }
  168. for (var iMachine = 0; iMachine < _data.Machines.Length; iMachine++)
  169. {
  170. var machine = _data.Machines[iMachine];
  171. for (var iTask = 0; iTask < machine.Tasks.Length; iTask++)
  172. {
  173. var task = machine.Tasks[iTask];
  174. var rect = _taskRectangles[iMachine][iTask];
  175. g.FillRectangle(taskBgBrush, rect);
  176. g.DrawString(task.Text, Font, Brushes.Black, rect.Left, rect.Top + TaskBlockVerticalPadding + 1);
  177. var halfHeight = rect.Height / 2f;
  178. g.FillRectangle(Brushes.Red, rect.Left, rect.Top + halfHeight, 1, halfHeight);
  179. }
  180. }
  181. }
  182. }
  183. }
  184. private void RenderTopLayer()
  185. {
  186. var bmp = _layerBitmaps[(int)Layer.Top];
  187. using (var g = Graphics.FromImage(bmp))
  188. {
  189. g.SetHighQuality();
  190. g.Clear(Color.Transparent);
  191. if (_mousePosition.HasValue)
  192. {
  193. //g.DrawLine(Pens.Red, 0, _mousePosition.Value.Y, bmp.Width, _mousePosition.Value.Y);
  194. g.DrawLine(Pens.Red, _mousePosition.Value.X, 0, _mousePosition.Value.X, bmp.Height);
  195. _mousePosition = null;
  196. }
  197. }
  198. }
  199. private void ChartPictureBox_Paint(object sender, PaintEventArgs e)
  200. {
  201. e.Graphics.SetHighQuality();
  202. foreach (var bitmap in _layerBitmaps)
  203. {
  204. e.Graphics.DrawImage(bitmap, e.ClipRectangle, e.ClipRectangle, GraphicsUnit.Pixel);
  205. }
  206. }
  207. private void ChartPictureBox_MouseMove(object sender, MouseEventArgs e)
  208. {
  209. _mousePosition = e.Location;
  210. RenderTopLayer();
  211. ChartPictureBox.Invalidate();
  212. if (MouseButtons.None != e.Button) return;
  213. if (null != _dayWidthAdjustHandles)
  214. {
  215. for (var iDay = 0; iDay < _dayWidthAdjustHandles.Length; iDay++)
  216. {
  217. var rect = _dayWidthAdjustHandles[iDay];
  218. if (rect.Contains(e.Location))
  219. {
  220. ChartPictureBox.Cursor = Cursors.SizeWE;
  221. _hoverDayWidthHandler = iDay;
  222. return;
  223. }
  224. }
  225. if (ChartPictureBox.Cursor != DefaultCursor) ChartPictureBox.Cursor = DefaultCursor;
  226. _hoverDayWidthHandler = null;
  227. }
  228. for (var iMachine = 0; iMachine < _data.Machines.Length; iMachine++)
  229. {
  230. var machine = _data.Machines[iMachine];
  231. for (var iTask = 0; iTask < machine.Tasks.Length; iTask++)
  232. {
  233. var rectangleF = _taskRectangles[iMachine][iTask];
  234. if (rectangleF.Contains(e.Location))
  235. {
  236. if (_hoverMachine == iMachine && _hoverTask == iTask) return;
  237. TaskToolTip.Show(_data.Machines[iMachine].Tasks[iTask].Detail, ChartPictureBox, (int)rectangleF.Left, (int)rectangleF.Bottom);
  238. _hoverMachine = iMachine;
  239. _hoverTask = iTask;
  240. return;
  241. }
  242. }
  243. }
  244. _hoverMachine = null;
  245. _hoverTask = null;
  246. }
  247. private void ChartPictureBox_MouseDown(object sender, MouseEventArgs e)
  248. {
  249. if (_hoverDayWidthHandler.HasValue) _holdDayX = e.X;
  250. }
  251. private void ChartPictureBox_MouseUp(object sender, MouseEventArgs e)
  252. {
  253. if (_hoverDayWidthHandler.HasValue && _holdDayX.HasValue)
  254. {
  255. TimelineDayWidth += e.X - _holdDayX.Value;
  256. var ticks = TimelineDayWidth / 40;
  257. if (ticks < 2)
  258. {
  259. ticks = 2;
  260. }
  261. else if (ticks > 24)
  262. {
  263. ticks = 24;
  264. }
  265. else
  266. {
  267. foreach (var item in new[] { 2, 4, 6, 8, 12, 24 })
  268. {
  269. if (!(ticks < item)) continue;
  270. ticks = item;
  271. break;
  272. }
  273. }
  274. TimelineTickerFrequencyHour = 24f / ticks;
  275. CalcLayoutAndCreateBitmaps();
  276. ChartPictureBox.Invalidate();
  277. _hoverDayWidthHandler = null;
  278. _holdDayX = null;
  279. }
  280. }
  281. protected override void OnHandleDestroyed(EventArgs e)
  282. {
  283. base.OnHandleDestroyed(e);
  284. foreach (var bitmap in _layerBitmaps)
  285. {
  286. bitmap?.Dispose();
  287. }
  288. }
  289. }
  290. }