ガントチャートとはプロジェクトの進捗を管理するためのスケジュール表のことでであり、機械工学者で経営コンサルタントのヘンリー・ガントによって考案されました。縦軸に作業項目、横軸に時間軸を棒グラフで表すことにより、視覚的に全体像を掴めるようになります。
Excelなどの表計算ソフトやプロジェクト管理用のソフトも数多く存在するのですが、ここではあえてC#でつくることにします。このブログには「鳩でもわかるC#」という名前をつけてしまった以上、多少無理矢理感があってもやります。そうでなくてもNode.jsネタが増えてきてC#がおろそかになっているので・・・。
最初にデザイナで以下のようなものをつくります。フォームにPanelを貼り付けているだけです。最上部に日付を表示させたいのですが、チャートが縦に長くなってスクロールしなければならなくなったときに日付は見えるようにしておきたいからです。
TaskUserControlクラス
UserControlからTaskUserControlクラスをつくります。
上のTextBoxがタスク名、下が担当者の名前です。その横に開始日、終了日がグラフのように表示されます。開始日と終了日はタスクのそれとチャート全体のそれがあります。チャート全体の開始日、終了日はすべて同じになるので静的なフィールド変数にします。タスクの開始日、終了日はタスクによって違うので個別のフィールド変数をもちます。
それからタスクの開始日、終了日の表示色と日付の境界線を変更することができるようにします。今日がチャート全体のどの部分かも一目でわかるように別の色で表示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public partial class TaskUserControl : UserControl { public TaskUserControl() { InitializeComponent(); } // 表示されるチャート全体の開始日、終了日 static DateTime BandStartDay = DateTime.Today - new TimeSpan(10, 0, 0, 0); static DateTime BandEndDay = DateTime.Today + new TimeSpan(20, 0, 0, 0); // 各タスクの開始日、終了日、表示色 DateTime TaskStartDay = DateTime.Today + new TimeSpan(1, 0, 0, 0); DateTime TaskEndDay = DateTime.Today + new TimeSpan(2, 0, 0, 0); Color TaskColor = Color.Red; // チャート全体の背景色と日付の境界線 public static Color DateBorderColor = Color.LightGray; public static Color BandBackColor = Color.White; } |
以下はチャート全体の開始日、終了日を変更するためのメソッドです。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class TaskUserControl : UserControl { public static void SetBandStartDay(int year, int month, int day) { BandStartDay = new DateTime(year, month, day); } public static void SetBandEndDay(int year, int month, int day) { BandEndDay = new DateTime(year, month, day); } } |
描画処理
あとは各フィールド変数をセットして更新すればチャートが描画されます。
チャートを描画するにあたってチャートの開始位置、各日付間の幅、タスクの期間として塗りつぶす開始位置と終了位置を求めなければなりません。
チャートの開始位置はTextBoxの右端よりもちょっとだけ右側とします。
1 2 3 4 5 6 7 |
public partial class TaskUserControl : UserControl { int GetChartLeftX() { return TextBoxTaskName.Location.X + TextBoxTaskName.Size.Width + 10; } } |
これがチャートを描画するためのメソッドです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
public partial class TaskUserControl : UserControl { protected override void OnPaint(PaintEventArgs e) { // チャートの背景色が変更されている場合があるので最初に設定 this.BackColor = BandBackColor; // チャートの最初の日と最後の日の日数を求める TimeSpan span = BandEndDay - BandStartDay; int days = span.Days; // チャートの開始位置を求め、1日分の幅を求める int startX = GetChartLeftX(); float dayWidth = 1f * (this.Width - startX) / days; // タスクとして塗りつぶす開始位置と終了位置を求める int fillLeft = (int)(dayWidth*(TaskStartDay - BandStartDay).Days) + startX; int fillRight = (int)(dayWidth * ((TaskEndDay - BandStartDay).Days +1)) + startX; // 今日にあたる部分として塗りつぶす開始位置と終了位置を求める int fillLeftToday = (int)(dayWidth * (DateTime.Today - BandStartDay).Days) + startX; int fillRightToday = (int)(dayWidth * ((DateTime.Today - BandStartDay).Days + 1)) + startX; // 実際に塗りつぶす SolidBrush solidBrushToday = new SolidBrush(Color.Yellow); SolidBrush solidBrushTask = new SolidBrush(TaskColor); e.Graphics.FillRectangle(solidBrushToday, new RectangleF(fillLeftToday, 0, fillRightToday - fillLeftToday, this.Height)); e.Graphics.FillRectangle(solidBrushTask, new RectangleF(fillLeft, 10, fillRight - fillLeft, this.Height - 20)); solidBrushToday.Dispose(); solidBrushTask.Dispose(); // 日付の境界線に線を引く _dateBordersX.Clear(); Pen pen = new Pen(DateBorderColor); for (int i = 0; i < days; i++) { int dayPosX = startX + (int)(dayWidth * i); e.Graphics.DrawLine(pen, new PointF(dayPosX, 0), new PointF(dayPosX, this.Height)); _dateBordersX.Add(dayPosX); } pen.Dispose(); base.OnPaint(e); } // 日付の境界線の座標を取得できるようにする List<int> _dateBordersX = new List<int>(); public List<int> DateBordersX { get { return _dateBordersX.ToList(); } } } |
チャートの再描画をする処理を外部から実行できるように更新のためのメソッドを定義します。フォームのサイズが変更されたことにともなってチャートの幅が変更された場合はチャートの再描画が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public partial class TaskUserControl : UserControl { public void UpdateBand() { Invalidate(); } protected override void OnResize(EventArgs e) { UpdateBand(); base.OnResize(e); } } |
チャート全体の開始日、終了日は今日の日付との相対位置で設定したいので、BandStartDayとBandEndDayと今日の日付との関係がわかるようなメソッドを準備しておきます。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class TaskUserControl : UserControl { public static int GetPrevDays() { return (DateTime.Today - BandStartDay).Days; } public static int GetAfterDays() { return (BandEndDay - DateTime.Today).Days; } } |
Form1クラスにおける処理
作成したTaskUserControlをメインフォームに表示させます。
Form1クラス内にPanelMain(Panel)を貼り付け、フォームのサイズが変わると同じようにサイズ変更がおこなわれるようにします。また隅にチャート設定用のボタンも貼り付けておきます。
1 2 3 4 5 6 7 8 9 10 11 |
public partial class Form1 : Form { public Form1() { InitializeComponent(); PanelMain.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom; ButtonConfigChart.Anchor = AnchorStyles.Left | AnchorStyles.Bottom; InitPanelMainContextMenu(); } } |
タスクを追加するための処理
PanelMainを右クリックすると「タスクを追加」というメニューが表示されるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public partial class Form1 : Form { ContextMenuStrip panelMainContextMenuStrip = new ContextMenuStrip(); ToolStripMenuItem AddTaskMenuItem = new ToolStripMenuItem(); void InitPanelMainContextMenu() { this.PanelMain.ContextMenuStrip = this.panelMainContextMenuStrip; // メニュー「タスクを追加」を追加 this.AddTaskMenuItem.Text = "タスクを追加"; this.AddTaskMenuItem.Click += AddTaskMenuItem_Click; this.panelMainContextMenuStrip.Items.AddRange(new ToolStripItem[] { this.AddTaskMenuItem, }); } } |
「タスクを追加」が選択されるとTaskUserControlのインスタンスを生成してPanelMainに貼り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { int NextPosY = 0; private void AddTaskMenuItem_Click(object sender, EventArgs e) { TaskUserControl taskUserControl = new TaskUserControl(); taskUserControl.Location = new Point(0, NextPosY); taskUserControl.Size = new Size(PanelMain.Width, 50); taskUserControl.Anchor = AnchorStyles.Left | AnchorStyles.Right| AnchorStyles.Top; NextPosY += 50; PanelMain.Controls.Add(taskUserControl); this.Invalidate(); } } |
日付と境界線の描画処理
タスクが追加されたりフォームの大きさが変更された場合は一番上に日付の境界線を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
public partial class Form1 : Form { protected override void OnPaint(PaintEventArgs e) { DrawDateBorders(e.Graphics); base.OnPaint(e); } void DrawDateBorders(Graphics graphics) { // 日付の境界線を描画すべきX座標を取得する List<int> posXs = GetPosDateBorders(); if (posXs.Count < 2) return; // 日付の境界線同士の幅を取得する int cellWidth = posXs[1] - posXs[0]; int prevDays = TaskUserControl.GetPrevDays(); DateTime date = DateTime.Today - new TimeSpan(prevDays, 0, 0, 0); foreach (int x in posXs) { // 日付の境界線を描画する graphics.DrawLine(Pens.Black, x, 20, x, 100); string day = date.Day.ToString(); // 日付の境界線と境界線のあいだに日付を描画する if (cellWidth > 0) { Size size = TextRenderer.MeasureText(day, this.Font); int textLeft = x + (cellWidth - size.Width) / 2; TextRenderer.DrawText(graphics, day, this.Font, new Point(textLeft, 20), Color.Black); } date += new TimeSpan(1, 0, 0, 0); } } } |
日付の境界線を描画すべきX座標を取得する処理を示します。TaskUserControlが存在するなら取得し、そのDateBordersXを調べます。ここに格納されている値はTaskUserControl上のX座標なのでForm1上のX座標に変換します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public partial class Form1 : Form { List<int> GetPosDateBorders() { // TaskUserControlが存在するなら取得する TaskUserControl cont = GetFirstTaskUserControl(); if (cont == null) return new List<int>(); List<int> vs = cont.DateBordersX; return vs.Select(x => { Point pt1 = cont.PointToScreen(new Point(x, 0)); Point pt2 = this.PointToClient(pt1); return pt2.X; } ).ToList(); } // TaskUserControlが存在するなら取得する TaskUserControl GetFirstTaskUserControl() { foreach (var control in PanelMain.Controls) { if (control.GetType() == typeof(TaskUserControl)) return (TaskUserControl)control; } return null; } } |
フォームの大きさが変更されたら日付の境界線の位置が変わるので再描画が必要です。このときTaskUserControlが再描画されてからでないと日付の境界線の位置を取得することができないので、0.1秒待機してからInvalidateメソッドを呼び出します。
1 2 3 4 5 6 7 8 9 |
public partial class Form1 : Form { protected override async void OnResize(EventArgs e) { base.OnResize(e); await Task.Delay(100); this.Invalidate(); } } |
はじめまして。
私も以前に上司に社内の工程管理用ソフトを自社で作成できないかと
相談されたことがありました。
その時の私では、スキルも無かったので断ることになりましたが、見返してやろうと思い、
参考になりそうなサイトを探していてこちらに辿り着きました。
早速質問なのですが、管理人様のコードをマネしながらソフトをVisualStudioで作成
しているのですが、
PanelMain.Resize += PanelMain_Resize;
の部分で躓いてしまい、PanelMain_Resize とはどこで作成されているか気になりました。
問題なければ、教えて頂けませんか?
PanelMain.Resize += PanelMain_Resize;の行は不要です。
かわりにAddTaskMenuItem_Clickメソッドが一部変更します。
記事は修正しましたが、
× taskUserControl.Anchor = AnchorStyles.Left | AnchorStyles.Right;
○ taskUserControl.Anchor = AnchorStyles.Left | AnchorStyles.Right| AnchorStyles.Top;
これがないとウィンドウが通常表示の状態でタスクを追加したあと最大化すると位置がずれてしまう。
ありがとうございました。