123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368 |
- using System;
- using System.Collections.Generic;
- using System.Drawing;
- using System.Drawing.Imaging;
- using System.Linq;
- using System.Windows.Forms;
- namespace GanttChartPoC.GanttChart
- {
- [System.Runtime.InteropServices.Guid("84E7DA58-BE12-478F-A463-2FDA37CFD2F0")]
- internal partial class SimpleGanttChart : UserControl
- {
- private enum Layer
- {
- Background = 0,
- Top,
- MaxValue,
- }
- private static readonly StringFormat StringFormatMiddleCenter = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
- private readonly Bitmap[] _layerBitmaps = new Bitmap[(int)Layer.MaxValue];
- private GanttData _data;
- private float[] _machineRowHeights;
- private DateTime[] _timelineDays;
- private RectangleF[][] _taskRectangles;
- private RectangleF[] _dayWidthAdjustHandles;
- private Point? _mousePosition;
- private int? _hoverMachine, _hoverTask, _hoverDayWidthHandler;
- private int? _holdDayX;
- public float TimelineOffsetHours { get; set; } = 8;
- public float TaskBlockVerticalPadding { get; set; } = 4;
- public float TimelineTickerFrequencyHour { get; set; } = 4;
- public float TimelineTickerHeight { get; set; } = 4;
- public float TimelineHeight { get; set; } = 64;
- public float TimelineDayWidth { get; set; } = 360;
- public float TimelineDayAdjustHandleWidth { get; set; } = 4;
- public float MinRowHeight { get; set; } = 64;
- public float MachineCellWidth { get; set; } = 120;
- public Color TaskBlockBgColor { get; set; } = Color.FromArgb(128, 204, 255, 204);
- public SimpleGanttChart()
- {
- InitializeComponent();
- SetData(new GanttData(new GanttMachine[0]));
- }
- public void SetData(GanttData data)
- {
- _data = data ?? throw new ArgumentNullException(nameof(data));
- CalcLayoutAndCreateBitmaps();
- HorizontalScroll.Value = 0;
- VerticalScroll.Value = 0;
- ChartPictureBox.Invalidate();
- }
- private void CalcLayoutAndCreateBitmaps()
- {
- var bmpSize = new SizeF(320, 240);
- if (0 < _data?.Machines?.Length)
- {
- var bmpHeight = TimelineHeight;
- var bmpWidth = MachineCellWidth;
- var allTasks = _data.Machines.SelectMany(p => p.Tasks).ToArray();
- if (0 < allTasks.Length)
- {
- var minDate = allTasks.Min(p => p.Begin).Date;
- var maxDate = allTasks.Max(p => p.End).Date;
- var days = new List<DateTime>(7);
- var ptr = minDate;
- do
- {
- days.Add(ptr);
- ptr = ptr.AddDays(1);
- } while (ptr <= maxDate);
- var minBeg = allTasks.Min(p => p.Begin);
- if (minBeg.Hour < TimelineOffsetHours) days.Insert(0, minBeg.Date.AddDays(-1));
- _timelineDays = days.ToArray();
- }
- else
- {
- _timelineDays = new DateTime[0];
- }
- bmpWidth += _timelineDays.Length * TimelineDayWidth + TimelineDayWidth; // 追加一天宽度,字可能会超出
- _machineRowHeights = new float[_data.Machines.Length];
- _taskRectangles = new RectangleF[_data.Machines.Length][];
- var hourWidth = TimelineDayWidth / 24f;
- var machineOffsetY = TimelineHeight;
- for (var iMachine = 0; iMachine < _data.Machines.Length; iMachine++)
- {
- var machine = _data.Machines[iMachine];
- var taskOffsetY = 5f;
- _taskRectangles[iMachine] = new RectangleF[machine.Tasks.Length];
- for (var iTask = 0; iTask < machine.Tasks.Length; iTask++)
- {
- var task = machine.Tasks[iTask];
- var span = task.End - task.Begin;
- var left = (MachineCellWidth + TimelineDayWidth * Array.IndexOf(_timelineDays, task.Begin.Date)) + (float)(hourWidth * task.Begin.TimeOfDay.TotalHours) - hourWidth * TimelineOffsetHours;
- var top = machineOffsetY + taskOffsetY;
- var width = (float)(hourWidth * span.TotalHours);
- var height = Font.Height + TaskBlockVerticalPadding + TaskBlockVerticalPadding;
- _taskRectangles[iMachine][iTask] = new RectangleF(left, top, width, height);
- var taskTextSize = TextRenderer.MeasureText(task.Text, Font, Size.Empty);
- if (taskTextSize.Width > width) taskOffsetY += height;
- }
- var rowHeight = taskOffsetY + 5 < MinRowHeight ? MinRowHeight : taskOffsetY + 5;
- bmpHeight += rowHeight;
- _machineRowHeights[iMachine] = rowHeight;
- machineOffsetY += rowHeight;
- }
- bmpSize = new SizeF(bmpWidth, bmpHeight);
- _dayWidthAdjustHandles = new RectangleF[_timelineDays.Length];
- var halfHandleWidth = TimelineDayAdjustHandleWidth / 2;
- for (var iDay = 0; iDay < _timelineDays.Length; iDay++)
- {
- var left = MachineCellWidth + (iDay + 1) * TimelineDayWidth - halfHandleWidth;
- _dayWidthAdjustHandles[iDay] = new RectangleF(left, 0, TimelineDayAdjustHandleWidth, bmpHeight);
- }
- }
- ChartPictureBox.Size = bmpSize.ToSize();
- for (var i = 0; i < _layerBitmaps.Length; i++)
- {
- var toDispose = _layerBitmaps[i];
- _layerBitmaps[i] = new Bitmap((int)bmpSize.Width, (int)bmpSize.Height, PixelFormat.Format32bppArgb);
- toDispose?.Dispose();
- }
- RenderBackgroundLayer();
- RenderTopLayer();
- }
- private void RenderBackgroundLayer()
- {
- var bmp = _layerBitmaps[(int)Layer.Background];
- using (var g = Graphics.FromImage(bmp))
- using (var taskBgBrush = new SolidBrush(TaskBlockBgColor))
- {
- g.SetHighQuality();
- g.Clear(Color.White);
- var rcFull = new Rectangle(0, 0, bmp.Width - 1, bmp.Height - 1);
- g.DrawRectangle(Pens.Black, rcFull);
- if (0 == _data.Machines.Length)
- {
- g.DrawString("无数据", Font, Brushes.Black, rcFull, StringFormatMiddleCenter);
- }
- else
- {
- var yOffset = TimelineHeight;
- g.DrawLine(Pens.Black, 0, yOffset, bmp.Width, yOffset);
- for (var index = 0; index < _data.Machines.Length; index++)
- {
- var machine = _data.Machines[index];
- var height = _machineRowHeights[index];
- g.DrawString(machine.Name, Font, Brushes.Black, new RectangleF(0, yOffset, MachineCellWidth, height), StringFormatMiddleCenter);
- yOffset += height;
- g.DrawLine(Pens.Black, 0, yOffset, bmp.Width, yOffset);
- }
- var xOffset = MachineCellWidth;
- g.DrawLine(Pens.Black, xOffset, 0, xOffset, bmp.Height);
- foreach (var day in _timelineDays)
- {
- g.DrawString($"{day:yyyy-MM-dd (ddd)}", Font, Brushes.Black, new RectangleF(xOffset, 0, TimelineDayWidth, TimelineHeight), StringFormatMiddleCenter);
- for (var h = TimelineTickerFrequencyHour; h < 24; h += TimelineTickerFrequencyHour)
- {
- var left = xOffset + h / 24 * TimelineDayWidth;
- g.DrawLine(Pens.Black, left, TimelineHeight - TimelineTickerHeight, left, TimelineHeight);
- var offsetH = h + TimelineOffsetHours;
- if (offsetH >= 24) offsetH -= 24;
- var spanText = $"{offsetH:00}";
- var spanTextSize = g.MeasureString(spanText, Font);
- g.DrawString(spanText, Font, Brushes.Black, left - spanTextSize.Width / 2f, TimelineHeight - TimelineTickerHeight - spanTextSize.Height);
- }
- xOffset += TimelineDayWidth;
- g.DrawLine(Pens.Black, xOffset, 0, xOffset, bmp.Height);
- }
- for (var iMachine = 0; iMachine < _data.Machines.Length; iMachine++)
- {
- var machine = _data.Machines[iMachine];
- for (var iTask = 0; iTask < machine.Tasks.Length; iTask++)
- {
- var task = machine.Tasks[iTask];
- var rect = _taskRectangles[iMachine][iTask];
- g.FillRectangle(taskBgBrush, rect);
- g.DrawString(task.Text, Font, Brushes.Black, rect.Left, rect.Top + TaskBlockVerticalPadding + 1);
- var halfHeight = rect.Height / 2f;
- g.FillRectangle(Brushes.Red, rect.Left, rect.Top + halfHeight, 1, halfHeight);
- }
- }
- }
- }
- }
- private void RenderTopLayer()
- {
- var bmp = _layerBitmaps[(int)Layer.Top];
- using (var g = Graphics.FromImage(bmp))
- {
- g.SetHighQuality();
- g.Clear(Color.Transparent);
- if (_mousePosition.HasValue)
- {
- //g.DrawLine(Pens.Red, 0, _mousePosition.Value.Y, bmp.Width, _mousePosition.Value.Y);
- g.DrawLine(Pens.Red, _mousePosition.Value.X, 0, _mousePosition.Value.X, bmp.Height);
- _mousePosition = null;
- }
- }
- }
- private void ChartPictureBox_Paint(object sender, PaintEventArgs e)
- {
- e.Graphics.SetHighQuality();
- foreach (var bitmap in _layerBitmaps)
- {
- e.Graphics.DrawImage(bitmap, e.ClipRectangle, e.ClipRectangle, GraphicsUnit.Pixel);
- }
- }
- private void ChartPictureBox_MouseMove(object sender, MouseEventArgs e)
- {
- _mousePosition = e.Location;
- RenderTopLayer();
- ChartPictureBox.Invalidate();
- if (MouseButtons.None != e.Button) return;
- if (null != _dayWidthAdjustHandles)
- {
- for (var iDay = 0; iDay < _dayWidthAdjustHandles.Length; iDay++)
- {
- var rect = _dayWidthAdjustHandles[iDay];
- if (rect.Contains(e.Location))
- {
- ChartPictureBox.Cursor = Cursors.SizeWE;
- _hoverDayWidthHandler = iDay;
- return;
- }
- }
- if (ChartPictureBox.Cursor != DefaultCursor) ChartPictureBox.Cursor = DefaultCursor;
- _hoverDayWidthHandler = null;
- }
- for (var iMachine = 0; iMachine < _data.Machines.Length; iMachine++)
- {
- var machine = _data.Machines[iMachine];
- for (var iTask = 0; iTask < machine.Tasks.Length; iTask++)
- {
- var rectangleF = _taskRectangles[iMachine][iTask];
- if (rectangleF.Contains(e.Location))
- {
- if (_hoverMachine == iMachine && _hoverTask == iTask) return;
- TaskToolTip.Show(_data.Machines[iMachine].Tasks[iTask].Detail, ChartPictureBox, (int)rectangleF.Left, (int)rectangleF.Bottom);
- _hoverMachine = iMachine;
- _hoverTask = iTask;
- return;
- }
- }
- }
- _hoverMachine = null;
- _hoverTask = null;
- }
- private void ChartPictureBox_MouseDown(object sender, MouseEventArgs e)
- {
- if (_hoverDayWidthHandler.HasValue) _holdDayX = e.X;
- }
- private void ChartPictureBox_MouseUp(object sender, MouseEventArgs e)
- {
- if (_hoverDayWidthHandler.HasValue && _holdDayX.HasValue)
- {
- TimelineDayWidth += e.X - _holdDayX.Value;
- var ticks = TimelineDayWidth / 40;
- if (ticks < 2)
- {
- ticks = 2;
- }
- else if (ticks > 24)
- {
- ticks = 24;
- }
- else
- {
- foreach (var item in new[] { 2, 4, 6, 8, 12, 24 })
- {
- if (!(ticks < item)) continue;
- ticks = item;
- break;
- }
- }
- TimelineTickerFrequencyHour = 24f / ticks;
- CalcLayoutAndCreateBitmaps();
- ChartPictureBox.Invalidate();
- _hoverDayWidthHandler = null;
- _holdDayX = null;
- }
- }
- protected override void OnHandleDestroyed(EventArgs e)
- {
- base.OnHandleDestroyed(e);
- foreach (var bitmap in _layerBitmaps)
- {
- bitmap?.Dispose();
- }
- }
- }
- }
|