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(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(); } } } }