C#でガントチャートを作成する方法|WinForms・WPF対応の自作手順とサンプルコード

はじめに

C#で業務アプリやプロジェクト管理ツールを作成していると、「ガントチャートを画面上に表示したい」「タスクの開始日・終了日・進捗を横棒で見せたい」という要件がよく出てきます。

ガントチャートは、タスクの期間や進捗状況を視覚的に把握できるため、工程管理、製造管理、開発スケジュール管理、設備保全、案件管理など、さまざまなC#アプリで利用されます。

一方で、C#の標準コントロールには本格的なガントチャート専用部品が用意されているわけではありません。そのため、WinFormsやWPFでガントチャートを作る場合は、自分で描画処理を実装するか、外部ライブラリを利用する必要があります。

この記事では、C#でガントチャートを作成する方法について、WinFormsとWPFの両方に対応した自作手順を解説します。タスクデータの設計、日付からバー位置を計算する考え方、描画サンプルコード、実用機能の追加方法、ライブラリ利用との比較までまとめて紹介します。

1. C#でガントチャートを作成する前に理解すべき基本

1-1. ガントチャートとは?C#アプリで使われる主な用途

ガントチャートとは、タスクや作業工程を縦軸に並べ、日付や時間を横軸に配置し、各タスクの開始日から終了日までを横棒で表現する表・グラフです。

たとえば、次のような情報をひと目で確認できます。

  • どのタスクがいつ始まるか

  • どのタスクがいつ終わるか

  • 複数タスクの重なり具合

  • 現在の進捗状況

  • 遅延している作業

  • タスク間の依存関係

  • 今日時点での進み具合

C#アプリでは、特に次のような場面でガントチャートが使われます。

  • 生産管理システム

  • 工程管理システム

  • プロジェクト管理ツール

  • 建設・設備・保守管理アプリ

  • 社内向けスケジュール管理ソフト

  • 開発タスク管理ツール

  • 作業員や機械の稼働予定表

  • 予約・配車・リソース管理システム

Excelで作られた工程表をC#アプリ化したい場合にも、ガントチャート表示はよく求められる機能です。

1-2. C#でガントチャートを作る方法は「自作」と「ライブラリ利用」の2種類

C#でガントチャートを作成する方法は、大きく分けて次の2つです。

1つ目は、WinFormsやWPFの標準機能を使って自作する方法です。Panel、Canvas、DataGridView、Grid、Rectangleなどを組み合わせ、日付軸やタスクバーを自分で描画します。自由度が高く、学習にも向いていますが、ドラッグ編集、依存線、スクロール同期、印刷、ズームなどを本格的に実装すると手間がかかります。

2つ目は、ガントチャート対応のUIライブラリを利用する方法です。市販コンポーネントやOSSライブラリを使えば、表示、編集、スクロール、依存関係、リソース管理などを短期間で実装しやすくなります。ただし、ライセンス費用、カスタマイズ制限、導入コスト、学習コストを考慮する必要があります。

小規模な社内ツールや学習目的であれば自作でも十分対応できます。一方、業務システムとして長期運用する場合や、Excelのような高機能なガントチャートを求める場合は、ライブラリ利用も検討したほうがよいでしょう。

1-3. WinFormsとWPFでガントチャート実装方法が異なる理由

C#でデスクトップアプリを作る代表的な技術には、WinFormsとWPFがあります。どちらでもガントチャートは作成できますが、実装方法は大きく異なります。

WinFormsは、従来型のデスクトップアプリ開発に向いたUIフレームワークです。描画にはGraphicsクラスを使うことが多く、PanelやUserControlのPaintイベントで線、文字、四角形を直接描画します。シンプルに作れる反面、拡大縮小やアニメーション、データバインディングとの相性はWPFほど強くありません。

WPFは、XAML、データバインディング、テンプレート、MVVMパターンを活用しやすいUIフレームワークです。Canvas上にRectangleやTextBlockを配置したり、ItemsControlでタスクバーを動的に表示したりできます。見た目のカスタマイズやMVVM構成には向いていますが、WinFormsより構造を理解するまでに少し学習コストがあります。

そのため、既存のWinForms業務アプリにガントチャートを追加したい場合はWinFormsでの自作が現実的です。新規開発で柔軟なUIやデータバインディングを重視するなら、WPFで実装するのがおすすめです。

1-4. 本記事で作成するガントチャートの完成イメージ

本記事では、次のようなシンプルなガントチャートを作成します。

  • 左側にタスク名を表示する

  • 上部に日付ヘッダーを表示する

  • タスクごとに開始日から終了日までのバーを表示する

  • 進捗率をバー内に表示する

  • 今日の日付を縦線で表示する

  • 土日を背景色で区別する

  • スクロールに対応する

  • WinForms版とWPF版の考え方を理解できる

最初から高機能なガントチャートを作ろうとすると複雑になるため、まずは「日付からバーの位置と幅を計算して表示する」という基本を押さえることが重要です。

2. C#ガントチャート作成に必要な設計項目

2-1. タスク名・開始日・終了日・進捗率のデータ構造を決める

ガントチャートを作成するには、まず表示対象となるタスクデータの構造を決めます。最低限必要な項目は、タスク名、開始日、終了日、進捗率です。

C#では、次のようなクラスを用意すると扱いやすくなります。

C#
public class GanttTask
{
public string TaskName { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Progress { get; set; }
}

実務で使う場合は、さらに次のような項目を追加することもあります。

C#
public class GanttTask
{
public int Id { get; set; }
public string TaskName { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Progress { get; set; }
public int? ParentId { get; set; }
public bool IsMilestone { get; set; }
public string Assignee { get; set; } = string.Empty;
public string Status { get; set; } = string.Empty;
}

開始日と終了日はDateTime型で管理し、画面上の表示位置を計算するときに日数差へ変換します。進捗率は0〜100の整数で持たせると、バー内の塗り分けに利用しやすくなります。

2-2. 日付軸とタスク行の表示ルールを設計する

ガントチャートでは、横方向が日付軸、縦方向がタスク行になります。そのため、次のような表示ルールを決める必要があります。

  • 1日を何ピクセルで表示するか

  • 1タスク行の高さを何ピクセルにするか

  • 左側のタスク名エリアの幅を何ピクセルにするか

  • ヘッダー行の高さを何ピクセルにするか

  • 表示開始日と表示終了日をどう決めるか

  • 土日や祝日を背景で色分けするか

  • 今日の日付線を表示するか

たとえば、次のように定数を定義しておくと、描画処理で使い回しやすくなります。

C#
private const int DayWidth = 40;
private const int RowHeight = 32;
private const int HeaderHeight = 40;
private const int TaskNameWidth = 180;

1日あたりの幅を広くすれば見やすくなりますが、横スクロール量が増えます。逆に狭くすれば多くの日付を表示できますが、文字やバーが詰まって見えやすくなります。

2-3. バーの位置・幅を日付から計算する考え方

ガントチャート実装で最も重要なのが、日付を画面上のX座標に変換する処理です。

基本的な考え方は次のとおりです。

C#
int x = TaskNameWidth + (task.StartDate - chartStartDate).Days * DayWidth;
int width = (task.EndDate - task.StartDate).Days + 1;
int barWidth = width * DayWidth;

開始日は、チャート全体の表示開始日から何日後かを求め、その日数に1日分の幅を掛けます。終了日まで含めて表示する場合は、日数差に1を足してバーの幅を計算します。

たとえば、表示開始日が2026年1月1日、タスク開始日が2026年1月4日、1日幅が40ピクセルの場合、タスクバーのX座標は次のようになります。

C#
(2026/1/4 - 2026/1/1).Days * 40 = 120

これにタスク名エリアの幅を加えた位置からバーを描画します。

2-4. 休日・土日・今日線・進捗表示を追加するか決める

ガントチャートは、単にタスクバーを並べるだけでも使えますが、実用性を高めるには補助情報が重要です。

よく追加される機能は次のとおりです。

  • 土日の背景色を変える

  • 祝日の背景色を変える

  • 今日の日付に縦線を引く

  • 進捗率をバー内に表示する

  • 遅延タスクを赤系の色で表示する

  • 完了タスクを別色で表示する

  • マイルストーンをひし形で表示する

特に「今日線」と「進捗率」は、現在の状況を把握しやすくなるため、最初に追加したい機能です。

2-5. スクロール・ズーム・大量データ表示への対応方針

実務で使うガントチャートでは、タスク数が数百件以上になることがあります。また、表示期間が数か月から数年に及ぶ場合もあります。

そのため、次の点を事前に考えておくことが大切です。

  • 横スクロールで長期間を表示する

  • 縦スクロールで大量タスクを表示する

  • 1日単位、週単位、月単位のズームを切り替える

  • 表示範囲外のタスクは描画しない

  • 再描画処理を必要最小限にする

  • データ読み込みを非同期化する

最初は1日単位のシンプルな表示で作成し、必要に応じて週単位・月単位表示や仮想化を追加していくと実装しやすくなります。

3. C# WinFormsでガントチャートを自作する方法

3-1. WinFormsでガントチャートを作る基本方針

WinFormsでガントチャートを自作する場合、最もシンプルなのはPanelまたはUserControlに直接描画する方法です。

Paintイベント内でGraphicsオブジェクトを使い、次の要素を描画します。

  • 背景

  • 日付ヘッダー

  • タスク名

  • 行の罫線

  • タスクバー

  • 進捗バー

  • 今日線

  • 土日の背景

WinFormsでは、描画内容を変更したいときにInvalidateメソッドを呼び出し、再描画を発生させます。

C#
ganttPanel.Invalidate();

描画量が多くなる場合は、ちらつきを防ぐためにダブルバッファリングを有効にすることも重要です。

3-2. DataGridViewとPanelを使った構成例

WinFormsでガントチャートを作る構成としては、次の2パターンが考えられます。

1つ目は、DataGridViewでタスク一覧を表示し、右側のPanelにガントバーを描画する方法です。タスク名や担当者などの表形式データをDataGridViewで扱えるため、業務アプリとの相性がよい構成です。

2つ目は、UserControlまたはPanelだけで、タスク名エリアとチャートエリアをまとめて描画する方法です。自由度が高く、スクロール同期もしやすいですが、セル編集や並び替えなどは自分で実装する必要があります。

この記事では、理解しやすいようにPanelへ直接描画する方法を中心に解説します。

3-3. タスクデータ用クラスを作成する

まずは、WinFormsで使用するタスククラスを作成します。

C#
public class GanttTask
{
public string TaskName { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Progress { get; set; }

public GanttTask(string taskName, DateTime startDate, DateTime endDate, int progress)
{
TaskName = taskName;
StartDate = startDate;
EndDate = endDate;
Progress = progress;
}
}

サンプルデータは次のように作成できます。

C#
private readonly List<GanttTask> tasks = new()
{
new GanttTask("要件定義", new DateTime(2026, 1, 1), new DateTime(2026, 1, 5), 100),
new GanttTask("基本設計", new DateTime(2026, 1, 6), new DateTime(2026, 1, 12), 80),
new GanttTask("詳細設計", new DateTime(2026, 1, 10), new DateTime(2026, 1, 18), 50),
new GanttTask("実装", new DateTime(2026, 1, 15), new DateTime(2026, 1, 31), 30),
new GanttTask("テスト", new DateTime(2026, 2, 1), new DateTime(2026, 2, 10), 0)
};

3-4. 日付ヘッダーを描画するサンプルコード

次に、日付ヘッダーを描画します。

C#
private void DrawDateHeader(Graphics g)
{
using var headerBrush = new SolidBrush(Color.FromArgb(240, 240, 240));
using var linePen = new Pen(Color.LightGray);
using var textBrush = new SolidBrush(Color.Black);

g.FillRectangle(headerBrush, 0, 0, ganttPanel.Width, HeaderHeight);

int totalDays = (chartEndDate - chartStartDate).Days + 1;

for (int i = 0; i < totalDays; i++)
{
DateTime date = chartStartDate.AddDays(i);
int x = TaskNameWidth + i * DayWidth - scrollX;

if (x + DayWidth < TaskNameWidth || x > ganttPanel.Width)
{
continue;
}

Rectangle rect = new Rectangle(x, 0, DayWidth, HeaderHeight);

if (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday)
{
using var weekendBrush = new SolidBrush(Color.FromArgb(245, 245, 255));
g.FillRectangle(weekendBrush, rect);
}

g.DrawRectangle(linePen, rect);
g.DrawString(date.ToString("MM/dd"), ganttPanel.Font, textBrush, rect);
}

g.DrawString("タスク名", ganttPanel.Font, textBrush, new Rectangle(5, 10, TaskNameWidth, HeaderHeight));
}

このコードでは、chartStartDateからchartEndDateまでの日付を1日ずつ描画しています。scrollXを引くことで、横スクロールにも対応できるようにしています。

3-5. タスクバーを描画するサンプルコード

次に、タスク行とタスクバーを描画します。

C#
private void DrawTasks(Graphics g)
{
using var linePen = new Pen(Color.LightGray);
using var textBrush = new SolidBrush(Color.Black);
using var barBrush = new SolidBrush(Color.SteelBlue);
using var progressBrush = new SolidBrush(Color.SeaGreen);
using var progressTextBrush = new SolidBrush(Color.White);

for (int i = 0; i < tasks.Count; i++)
{
GanttTask task = tasks[i];

int rowY = HeaderHeight + i * RowHeight - scrollY;
if (rowY + RowHeight < HeaderHeight || rowY > ganttPanel.Height)
{
continue;
}

Rectangle nameRect = new Rectangle(0, rowY, TaskNameWidth, RowHeight);
g.DrawRectangle(linePen, nameRect);
g.DrawString(task.TaskName, ganttPanel.Font, textBrush, nameRect);

int x = TaskNameWidth + (task.StartDate.Date - chartStartDate.Date).Days * DayWidth - scrollX;
int days = (task.EndDate.Date - task.StartDate.Date).Days + 1;
int width = days * DayWidth;
int barY = rowY + 6;
int barHeight = RowHeight - 12;

Rectangle barRect = new Rectangle(x, barY, width, barHeight);
g.FillRectangle(barBrush, barRect);

int progressWidth = width * Math.Clamp(task.Progress, 0, 100) / 100;
Rectangle progressRect = new Rectangle(x, barY, progressWidth, barHeight);
g.FillRectangle(progressBrush, progressRect);

g.DrawRectangle(Pens.DarkSlateGray, barRect);

string progressText = $"{task.Progress}%";
g.DrawString(progressText, ganttPanel.Font, progressTextBrush, barRect);

g.DrawLine(linePen, 0, rowY + RowHeight, ganttPanel.Width, rowY + RowHeight);
}
}

この処理では、タスクの開始日と終了日からバーのX座標と幅を計算し、Rectangleとして描画しています。進捗率はバーの幅に対する割合として計算し、別の色で塗り分けています。

3-6. 横スクロール・縦スクロールに対応する方法

WinFormsでスクロールに対応するには、HScrollBarとVScrollBarを配置し、スクロール値を描画座標に反映します。

C#
private int scrollX = 0;
private int scrollY = 0;

private void hScrollBar1_Scroll(object sender, ScrollEventArgs e)
{
scrollX = e.NewValue;
ganttPanel.Invalidate();
}

private void vScrollBar1_Scroll(object sender, ScrollEventArgs e)
{
scrollY = e.NewValue;
ganttPanel.Invalidate();
}

フォームの初期化時にスクロールバーの最大値を設定します。

C#
private void InitializeScrollBars()
{
int totalDays = (chartEndDate - chartStartDate).Days + 1;
hScrollBar1.Minimum = 0;
hScrollBar1.Maximum = Math.Max(0, totalDays * DayWidth);

vScrollBar1.Minimum = 0;
vScrollBar1.Maximum = Math.Max(0, tasks.Count * RowHeight);
}

スクロール値を描画座標から引くことで、表示位置をずらせます。横スクロールでは日付軸とタスクバーを同じscrollXで移動させ、縦スクロールではタスク行とバーを同じscrollYで移動させるのがポイントです。

3-7. WinForms実装でよくある表示崩れと対処法

WinFormsでガントチャートを自作すると、次のような表示崩れが発生しやすくなります。

1つ目は、再描画時のちらつきです。Panelに直接描画すると、スクロールやサイズ変更時にちらつく場合があります。この場合は、ダブルバッファリングを有効にしたカスタムPanelを使うと改善できます。

C#
public class DoubleBufferedPanel : Panel
{
public DoubleBufferedPanel()
{
DoubleBuffered = true;
ResizeRedraw = true;
}
}

2つ目は、日付計算のズレです。DateTimeに時刻が含まれていると、日数差の計算が想定とずれることがあります。StartDate.DateやEndDate.DateのようにDateプロパティを使い、日付部分だけで計算するのがおすすめです。

3つ目は、スクロールバーの最大値と描画範囲の不一致です。スクロールバーのMaximumには、表示領域の幅や高さを考慮する必要があります。最大値が大きすぎると余白が出やすく、小さすぎると最後までスクロールできません。

4つ目は、文字がバーや罫線と重なる問題です。タスク名エリア、ヘッダー行、バーの上下余白を定数化し、描画位置を調整しやすくしておきましょう。

4. C# WPFでガントチャートを自作する方法

4-1. WPFでガントチャートを作る基本方針

WPFでガントチャートを作る場合は、CanvasやGridを使ってタスクバーを配置する方法が分かりやすいです。

WPFでは、RectangleやTextBlockなどのUI要素をXAMLで定義し、Bindingを使ってデータと表示を連動できます。WinFormsのようにGraphicsで直接描画するのではなく、UI要素としてバーを配置するため、スタイル変更やテンプレート化がしやすいのが特徴です。

基本構成は次のようになります。

  • ScrollViewerで全体をスクロール可能にする

  • Gridでヘッダー、タスク名、チャート領域を分ける

  • ItemsControlでタスク一覧を表示する

  • Canvas上にRectangleを配置してバーを表現する

  • Converterを使って日付をX座標や幅に変換する

MVVMで実装する場合は、タスクデータをViewModelに持たせ、XAML側はBindingで表示します。

4-2. Canvas・Grid・ItemsControlを使った構成例

WPFでシンプルなガントチャートを作る場合、次のような構成にできます。

XML
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="40"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>

<Canvas x:Name="HeaderCanvas"
Grid.Row="0"
Height="40"/>

<Canvas x:Name="TaskCanvas"
Grid.Row="1"/>
</Grid>
</ScrollViewer>

よりWPFらしく作るなら、ItemsControlとDataTemplateを使ってタスク行を表示します。

XML
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>

Canvasは、Canvas.LeftやCanvas.Topを指定して要素を自由な位置に配置できます。ガントチャートのように日付からX座標を計算してバーを置くUIと相性がよいです。

4-3. MVVM向けのタスクモデルを作成する

WPFでMVVMを意識する場合は、モデルまたはViewModelとしてタスククラスを用意します。

C#
public class GanttTaskViewModel
{
public string TaskName { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Progress { get; set; }

public double X { get; set; }
public double Y { get; set; }
public double Width { get; set; }
}

表示用のX座標、Y座標、幅をViewModel側で計算しておくと、XAML側のBindingがシンプルになります。

C#
public class MainViewModel
{
public ObservableCollection<GanttTaskViewModel> Tasks { get; } = new();

public DateTime ChartStartDate { get; set; } = new DateTime(2026, 1, 1);
public int DayWidth { get; set; } = 40;
public int RowHeight { get; set; } = 32;

public MainViewModel()
{
Tasks.Add(new GanttTaskViewModel
{
TaskName = "要件定義",
StartDate = new DateTime(2026, 1, 1),
EndDate = new DateTime(2026, 1, 5),
Progress = 100
});

Tasks.Add(new GanttTaskViewModel
{
TaskName = "基本設計",
StartDate = new DateTime(2026, 1, 6),
EndDate = new DateTime(2026, 1, 12),
Progress = 80
});

UpdateLayoutValues();
}

private void UpdateLayoutValues()
{
for (int i = 0; i < Tasks.Count; i++)
{
var task = Tasks[i];
task.X = (task.StartDate.Date - ChartStartDate.Date).Days * DayWidth + 180;
task.Y = i * RowHeight;
task.Width = ((task.EndDate.Date - task.StartDate.Date).Days + 1) * DayWidth;
}
}
}

実際のアプリでは、INotifyPropertyChangedを実装し、値の変更が画面に反映されるようにします。

4-4. 日付軸をXAMLで表示する方法

WPFで日付軸を表示する場合は、日付リストを作成し、ItemsControlで横方向に並べる方法が便利です。

C#
public class DateHeaderItem
{
public DateTime Date { get; set; }
public double X { get; set; }
public double Width { get; set; }
}

ViewModelで日付ヘッダー用データを作成します。

C#
public ObservableCollection<DateHeaderItem> DateHeaders { get; } = new();

private void CreateDateHeaders()
{
DateTime start = new DateTime(2026, 1, 1);
DateTime end = new DateTime(2026, 1, 31);

for (DateTime date = start; date <= end; date = date.AddDays(1))
{
DateHeaders.Add(new DateHeaderItem
{
Date = date,
X = (date - start).Days * DayWidth + 180,
Width = DayWidth
});
}
}

XAMLでは、Canvas上にTextBlockやBorderを配置します。

XML
<ItemsControl ItemsSource="{Binding DateHeaders}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Height="40"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
</Style>
</ItemsControl.ItemContainerStyle>

<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="{Binding Width}"
Height="40"
BorderBrush="LightGray"
BorderThickness="1"
Background="#F0F0F0">
<TextBlock Text="{Binding Date, StringFormat=MM/dd}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

この方法なら、日付範囲を変更したときもDateHeadersを再作成するだけで表示を更新できます。

4-5. タスクバーをCanvas上に配置するサンプルコード

タスクバーをCanvas上に配置するXAML例は次のとおりです。

XML
<ItemsControl ItemsSource="{Binding Tasks}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>

<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Width="{Binding Width}" Height="28">
<Border Background="SteelBlue"
BorderBrush="DarkSlateGray"
BorderThickness="1"
CornerRadius="3"/>
<Border Background="SeaGreen"
HorizontalAlignment="Left"
Width="{Binding ProgressWidth}"
CornerRadius="3"/>
<TextBlock Text="{Binding Progress, StringFormat={}{0}%}"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

この例では、ProgressWidthプロパティを用意して、進捗率に応じた幅を表示します。

C#
public double ProgressWidth
{
get
{
int progress = Math.Clamp(Progress, 0, 100);
return Width * progress / 100.0;
}
}

WPFでは、Bindingを使うことで、タスクデータを変更したときに表示も更新しやすくなります。

4-6. Bindingで開始日・終了日・進捗率を反映する方法

開始日や終了日が変更されたときに画面へ反映するには、INotifyPropertyChangedを実装します。

C#
public class GanttTaskViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

private DateTime startDate;
private DateTime endDate;
private int progress;
private double x;
private double width;

public string TaskName { get; set; } = string.Empty;

public DateTime StartDate
{
get => startDate;
set
{
startDate = value;
OnPropertyChanged(nameof(StartDate));
}
}

public DateTime EndDate
{
get => endDate;
set
{
endDate = value;
OnPropertyChanged(nameof(EndDate));
}
}

public int Progress
{
get => progress;
set
{
progress = value;
OnPropertyChanged(nameof(Progress));
OnPropertyChanged(nameof(ProgressWidth));
}
}

public double X
{
get => x;
set
{
x = value;
OnPropertyChanged(nameof(X));
}
}

public double Width
{
get => width;
set
{
width = value;
OnPropertyChanged(nameof(Width));
OnPropertyChanged(nameof(ProgressWidth));
}
}

public double Y { get; set; }

public double ProgressWidth => Width * Math.Clamp(Progress, 0, 100) / 100.0;

private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

日付を変更したら、ViewModel側でXやWidthを再計算し、OnPropertyChangedを呼び出します。これにより、Canvas上のバー位置や幅が更新されます。

4-7. WPFでドラッグ移動・サイズ変更を実装する考え方

WPFでタスクバーをドラッグ移動できるようにするには、MouseLeftButtonDown、MouseMove、MouseLeftButtonUpを使います。

基本的な流れは次のとおりです。

  • マウス押下時に対象タスクと開始位置を記録する

  • マウス移動時に移動量を計算する

  • 移動量を日数に変換する

  • StartDateとEndDateを更新する

  • X座標とWidthを再計算する

サイズ変更の場合は、バーの右端付近をドラッグしたときにEndDateを変更します。左端をドラッグする場合はStartDateを変更します。

ドラッグ中に1ピクセル単位で日付を変えると操作しづらいため、DayWidth単位で丸めるのがポイントです。

C#
int movedDays = (int)Math.Round(deltaX / DayWidth);
task.StartDate = originalStartDate.AddDays(movedDays);
task.EndDate = originalEndDate.AddDays(movedDays);

WPFでは、UI操作とViewModel更新を分離すると保守しやすくなります。最初はコードビハインドで実装しても問題ありませんが、業務アプリではBehaviorやCommandを使ってMVVMに寄せるとよいでしょう。

5. C#ガントチャートに追加したい実用機能

5-1. 進捗率をバー内に表示する

進捗率は、ガントチャートで特に重要な情報です。バー全体を予定期間、バー内の塗り分け部分を進捗として表示すると、作業がどこまで進んでいるか分かりやすくなります。

WinFormsでは、バー全体のRectangleを描画したあと、進捗率に応じた幅のRectangleを重ねて描画します。

C#
int progressWidth = barWidth * task.Progress / 100;
g.FillRectangle(progressBrush, x, y, progressWidth, barHeight);
g.DrawString($"{task.Progress}%", font, Brushes.White, x + 5, y + 4);

WPFでは、Gridの中に2つのBorderを重ね、手前のBorderのWidthを進捗率に応じて変更します。

5-2. タスクの依存関係を線で表現する

ガントチャートでは、「Aの作業が終わったらBを開始する」といった依存関係を線で表現することがあります。

依存関係を実装するには、タスクデータに先行タスクIDを持たせます。

C#
public int? PredecessorId { get; set; }

描画時には、先行タスクのバーの右端と、後続タスクのバーの左端を線で結びます。WinFormsではGraphics.DrawLineを使い、WPFではLineやPathを使います。

C#
g.DrawLine(Pens.Gray, fromX, fromY, toX, toY);

依存線は、タスクバーより後に描画するとバーの上に重なるため、見た目に応じて描画順を調整します。一般的には、背景、依存線、タスクバー、文字の順で描画すると見やすくなります。

5-3. マイルストーンをひし形アイコンで表示する

マイルストーンは、期間を持たない重要な日付や節目を表します。ガントチャートでは、ひし形で表示するのが一般的です。

WinFormsでひし形を描画する場合は、Polygonを使います。

C#
Point[] points =
{
new Point(x, y - 8),
new Point(x + 8, y),
new Point(x, y + 8),
new Point(x - 8, y)
};

g.FillPolygon(Brushes.Orange, points);
g.DrawPolygon(Pens.DarkOrange, points);

WPFでは、Polygonを使ってひし形を表現できます。

XML
<Polygon Points="10,0 20,10 10,20 0,10"
Fill="Orange"
Stroke="DarkOrange"/>

マイルストーンは、リリース日、承認日、検収日、納品日などを表示するのに向いています。

5-4. 土日・祝日・今日の日付を色分けする

土日や祝日を背景色で分けると、作業日と非作業日を視覚的に区別できます。

WinFormsでは、日付ヘッダーやチャート背景を描画するときにDayOfWeekを判定します。

C#
if (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday)
{
g.FillRectangle(weekendBrush, x, HeaderHeight, DayWidth, chartHeight);
}

今日の日付を表示するには、DateTime.Todayと表示開始日の差からX座標を計算し、縦線を描画します。

C#
int todayX = TaskNameWidth + (DateTime.Today - chartStartDate).Days * DayWidth - scrollX;
g.DrawLine(Pens.Red, todayX, 0, todayX, ganttPanel.Height);

祝日を扱う場合は、祝日リストを別途用意し、該当日かどうかを判定します。業務アプリでは、会社独自の休日カレンダーをデータベースで管理することもあります。

5-5. タスクのドラッグ移動・期間変更に対応する

タスクバーをマウスでドラッグ移動できるようにすると、Excelやプロジェクト管理ツールに近い操作感になります。

実装の考え方は、WinFormsでもWPFでもほぼ同じです。

  • マウス押下時に対象タスクを判定する

  • ドラッグ開始時のマウス位置とタスク日付を記録する

  • マウス移動量を日数に変換する

  • StartDateとEndDateを更新する

  • 再描画する

WinFormsでは、MouseDown、MouseMove、MouseUpイベントを使います。バーの当たり判定には、描画時のRectangleを保持しておくと便利です。

C#
private GanttTask? draggingTask;
private Point dragStartPoint;
private DateTime originalStartDate;
private DateTime originalEndDate;

期間変更を行う場合は、バーの左端または右端付近をドラッグしたかどうかを判定します。右端をドラッグした場合はEndDateを変更し、左端をドラッグした場合はStartDateを変更します。

5-6. CSV・Excel・画像・PDFへの出力機能を追加する

業務アプリでは、ガントチャートを画面に表示するだけでなく、外部ファイルへ出力したいケースがよくあります。

CSV出力では、タスク名、開始日、終了日、進捗率などのデータをテキストとして保存します。

C#
var lines = tasks.Select(t =>
$"{t.TaskName},{t.StartDate:yyyy/MM/dd},{t.EndDate:yyyy/MM/dd},{t.Progress}");

File.WriteAllLines("tasks.csv", lines, Encoding.UTF8);

画像出力では、Bitmapに対してガントチャートを描画し、PNGやJPEGとして保存します。WinFormsの場合は、画面描画用の処理をBitmap描画にも使い回せるように設計すると便利です。

C#
using Bitmap bitmap = new Bitmap(1200, 800);
using Graphics g = Graphics.FromImage(bitmap);

DrawGanttChart(g, bitmap.Width, bitmap.Height);

bitmap.Save("gantt.png", ImageFormat.Png);

PDF出力は、PDF生成ライブラリを利用して画像や表を配置する方法が一般的です。Excel出力では、セルの背景色や罫線を使ってガントチャート風に出力できます。

5-7. データベースからタスク情報を読み込む方法

実務アプリでは、タスク情報をデータベースから読み込むことが多くなります。SQL Serverを使う場合、テーブル例は次のようになります。

SQL
CREATE TABLE Tasks (
Id INT PRIMARY KEY,
TaskName NVARCHAR(100),
StartDate DATE,
EndDate DATE,
Progress INT
);

C#側では、取得したデータをGanttTaskクラスに変換します。

C#
var tasks = new List<GanttTask>();

using var connection = new SqlConnection(connectionString);
connection.Open();

using var command = new SqlCommand(
"SELECT TaskName, StartDate, EndDate, Progress FROM Tasks",
connection);

using var reader = command.ExecuteReader();

while (reader.Read())
{
tasks.Add(new GanttTask(
reader.GetString(0),
reader.GetDateTime(1),
reader.GetDateTime(2),
reader.GetInt32(3)
));
}

データ件数が多い場合は、表示期間に該当するタスクだけを読み込む、ページングする、非同期で読み込むなどの工夫が必要です。

6. C#ガントチャートのサンプルコード全体

6-1. WinForms版サンプルコードの全体構成

WinForms版の最小構成は、次のようになります。

C#
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

public partial class MainForm : Form
{
private const int DayWidth = 40;
private const int RowHeight = 32;
private const int HeaderHeight = 40;
private const int TaskNameWidth = 180;

private readonly Panel ganttPanel = new DoubleBufferedPanel();
private readonly HScrollBar hScrollBar = new();
private readonly VScrollBar vScrollBar = new();

private int scrollX;
private int scrollY;

private DateTime chartStartDate = new DateTime(2026, 1, 1);
private DateTime chartEndDate = new DateTime(2026, 2, 15);

private readonly List<GanttTask> tasks = new()
{
new GanttTask("要件定義", new DateTime(2026, 1, 1), new DateTime(2026, 1, 5), 100),
new GanttTask("基本設計", new DateTime(2026, 1, 6), new DateTime(2026, 1, 12), 80),
new GanttTask("詳細設計", new DateTime(2026, 1, 10), new DateTime(2026, 1, 18), 50),
new GanttTask("実装", new DateTime(2026, 1, 15), new DateTime(2026, 1, 31), 30),
new GanttTask("テスト", new DateTime(2026, 2, 1), new DateTime(2026, 2, 10), 0)
};

public MainForm()
{
InitializeComponent();
InitializeGanttChart();
}

private void InitializeGanttChart()
{
ganttPanel.Dock = DockStyle.Fill;
ganttPanel.Paint += GanttPanel_Paint;

hScrollBar.Dock = DockStyle.Bottom;
hScrollBar.Scroll += (s, e) =>
{
scrollX = e.NewValue;
ganttPanel.Invalidate();
};

vScrollBar.Dock = DockStyle.Right;
vScrollBar.Scroll += (s, e) =>
{
scrollY = e.NewValue;
ganttPanel.Invalidate();
};

Controls.Add(ganttPanel);
Controls.Add(hScrollBar);
Controls.Add(vScrollBar);

InitializeScrollBars();
}

private void InitializeScrollBars()
{
int totalDays = (chartEndDate - chartStartDate).Days + 1;
hScrollBar.Maximum = Math.Max(0, totalDays * DayWidth);
vScrollBar.Maximum = Math.Max(0, tasks.Count * RowHeight);
}

private void GanttPanel_Paint(object? sender, PaintEventArgs e)
{
e.Graphics.Clear(Color.White);
DrawDateHeader(e.Graphics);
DrawTasks(e.Graphics);
DrawTodayLine(e.Graphics);
}

private void DrawDateHeader(Graphics g)
{
using var headerBrush = new SolidBrush(Color.FromArgb(240, 240, 240));
using var linePen = new Pen(Color.LightGray);
using var textBrush = new SolidBrush(Color.Black);

g.FillRectangle(headerBrush, 0, 0, ganttPanel.Width, HeaderHeight);
g.DrawString("タスク名", ganttPanel.Font, textBrush, 8, 12);

int totalDays = (chartEndDate - chartStartDate).Days + 1;

for (int i = 0; i < totalDays; i++)
{
DateTime date = chartStartDate.AddDays(i);
int x = TaskNameWidth + i * DayWidth - scrollX;

if (x + DayWidth < TaskNameWidth || x > ganttPanel.Width)
{
continue;
}

Rectangle rect = new Rectangle(x, 0, DayWidth, HeaderHeight);

if (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday)
{
using var weekendBrush = new SolidBrush(Color.FromArgb(235, 245, 255));
g.FillRectangle(weekendBrush, rect);
}

g.DrawRectangle(linePen, rect);
g.DrawString(date.ToString("MM/dd"), ganttPanel.Font, textBrush, rect);
}
}

private void DrawTasks(Graphics g)
{
using var linePen = new Pen(Color.LightGray);
using var textBrush = new SolidBrush(Color.Black);
using var barBrush = new SolidBrush(Color.SteelBlue);
using var progressBrush = new SolidBrush(Color.SeaGreen);
using var progressTextBrush = new SolidBrush(Color.White);

for (int i = 0; i < tasks.Count; i++)
{
var task = tasks[i];

int rowY = HeaderHeight + i * RowHeight - scrollY;
if (rowY + RowHeight < HeaderHeight || rowY > ganttPanel.Height)
{
continue;
}

Rectangle nameRect = new Rectangle(0, rowY, TaskNameWidth, RowHeight);
g.DrawRectangle(linePen, nameRect);
g.DrawString(task.TaskName, ganttPanel.Font, textBrush, 8, rowY + 8);

int x = TaskNameWidth + (task.StartDate.Date - chartStartDate.Date).Days * DayWidth - scrollX;
int days = (task.EndDate.Date - task.StartDate.Date).Days + 1;
int width = days * DayWidth;

int barY = rowY + 6;
int barHeight = RowHeight - 12;

Rectangle barRect = new Rectangle(x, barY, width, barHeight);
g.FillRectangle(barBrush, barRect);

int progressWidth = width * Math.Clamp(task.Progress, 0, 100) / 100;
Rectangle progressRect = new Rectangle(x, barY, progressWidth, barHeight);
g.FillRectangle(progressBrush, progressRect);

g.DrawRectangle(Pens.DarkSlateGray, barRect);
g.DrawString($"{task.Progress}%", ganttPanel.Font, progressTextBrush, x + 5, barY + 3);
}
}

private void DrawTodayLine(Graphics g)
{
DateTime today = DateTime.Today;

if (today < chartStartDate || today > chartEndDate)
{
return;
}

int x = TaskNameWidth + (today.Date - chartStartDate.Date).Days * DayWidth - scrollX;
using var pen = new Pen(Color.Red, 2);
g.DrawLine(pen, x, 0, x, ganttPanel.Height);
}
}

public class GanttTask
{
public string TaskName { get; set; }
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Progress { get; set; }

public GanttTask(string taskName, DateTime startDate, DateTime endDate, int progress)
{
TaskName = taskName;
StartDate = startDate;
EndDate = endDate;
Progress = progress;
}
}

public class DoubleBufferedPanel : Panel
{
public DoubleBufferedPanel()
{
DoubleBuffered = true;
ResizeRedraw = true;
}
}

このコードをベースに、土日背景、ドラッグ編集、依存線、マイルストーンなどを追加していくと、実用的なWinFormsガントチャートに拡張できます。

6-2. WPF版サンプルコードの全体構成

WPF版では、ViewModelで表示位置を計算し、XAMLでCanvasに配置する構成が分かりやすいです。

MainWindow.xamlの例です。

XML
<Window x:Class="GanttSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WPF Gantt Chart"
Width="1000"
Height="600">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Grid Width="1600" Height="600">
<ItemsControl ItemsSource="{Binding DateHeaders}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Height="40"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="0"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="{Binding Width}"
Height="40"
BorderBrush="LightGray"
BorderThickness="1"
Background="#F0F0F0">
<TextBlock Text="{Binding Date, StringFormat=MM/dd}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

<ItemsControl ItemsSource="{Binding Tasks}" Margin="0,40,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding X}"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Width="{Binding Width}" Height="28">
<Border Background="SteelBlue"
BorderBrush="DarkSlateGray"
BorderThickness="1"
CornerRadius="3"/>
<Border Background="SeaGreen"
Width="{Binding ProgressWidth}"
HorizontalAlignment="Left"
CornerRadius="3"/>
<TextBlock Text="{Binding Progress, StringFormat={}{0}%}"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

<ItemsControl ItemsSource="{Binding Tasks}" Margin="0,40,0,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="0"/>
<Setter Property="Canvas.Top" Value="{Binding Y}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Width="180"
Height="28"
BorderBrush="LightGray"
BorderThickness="1">
<TextBlock Text="{Binding TaskName}"
VerticalAlignment="Center"
Margin="8,0,0,0"/>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
</Window>

MainViewModelの例です。

C#
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;

public class MainViewModel
{
public ObservableCollection<GanttTaskViewModel> Tasks { get; } = new();
public ObservableCollection<DateHeaderItem> DateHeaders { get; } = new();

private readonly DateTime chartStartDate = new DateTime(2026, 1, 1);
private readonly DateTime chartEndDate = new DateTime(2026, 2, 15);

private const int DayWidth = 40;
private const int RowHeight = 32;
private const int TaskNameWidth = 180;

public MainViewModel()
{
CreateDateHeaders();

Tasks.Add(new GanttTaskViewModel
{
TaskName = "要件定義",
StartDate = new DateTime(2026, 1, 1),
EndDate = new DateTime(2026, 1, 5),
Progress = 100
});

Tasks.Add(new GanttTaskViewModel
{
TaskName = "基本設計",
StartDate = new DateTime(2026, 1, 6),
EndDate = new DateTime(2026, 1, 12),
Progress = 80
});

Tasks.Add(new GanttTaskViewModel
{
TaskName = "詳細設計",
StartDate = new DateTime(2026, 1, 10),
EndDate = new DateTime(2026, 1, 18),
Progress = 50
});

UpdateTaskLayout();
}

private void CreateDateHeaders()
{
for (DateTime date = chartStartDate; date <= chartEndDate; date = date.AddDays(1))
{
DateHeaders.Add(new DateHeaderItem
{
Date = date,
X = TaskNameWidth + (date - chartStartDate).Days * DayWidth,
Width = DayWidth
});
}
}

private void UpdateTaskLayout()
{
for (int i = 0; i < Tasks.Count; i++)
{
var task = Tasks[i];
task.X = TaskNameWidth + (task.StartDate.Date - chartStartDate.Date).Days * DayWidth;
task.Y = i * RowHeight;
task.Width = ((task.EndDate.Date - task.StartDate.Date).Days + 1) * DayWidth;
}
}
}

public class DateHeaderItem
{
public DateTime Date { get; set; }
public double X { get; set; }
public double Width { get; set; }
}

public class GanttTaskViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;

public string TaskName { get; set; } = string.Empty;
public DateTime StartDate { get; set; }
public DateTime EndDate { get; set; }
public int Progress { get; set; }

private double x;
private double y;
private double width;

public double X
{
get => x;
set
{
x = value;
OnPropertyChanged(nameof(X));
}
}

public double Y
{
get => y;
set
{
y = value;
OnPropertyChanged(nameof(Y));
}
}

public double Width
{
get => width;
set
{
width = value;
OnPropertyChanged(nameof(Width));
OnPropertyChanged(nameof(ProgressWidth));
}
}

public double ProgressWidth => Width * Math.Clamp(Progress, 0, 100) / 100.0;

private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}

MainWindow.xaml.csでは、DataContextにViewModelを設定します。

C#
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
}

6-3. タスクデータのサンプル

ガントチャートの確認には、開始日や終了日が少しずつ重なるサンプルデータを使うと分かりやすくなります。

C#
var tasks = new List<GanttTask>
{
new GanttTask("要件定義", new DateTime(2026, 1, 1), new DateTime(2026, 1, 5), 100),
new GanttTask("基本設計", new DateTime(2026, 1, 6), new DateTime(2026, 1, 12), 80),
new GanttTask("詳細設計", new DateTime(2026, 1, 10), new DateTime(2026, 1, 18), 50),
new GanttTask("実装", new DateTime(2026, 1, 15), new DateTime(2026, 1, 31), 30),
new GanttTask("単体テスト", new DateTime(2026, 2, 1), new DateTime(2026, 2, 7), 0),
new GanttTask("結合テスト", new DateTime(2026, 2, 8), new DateTime(2026, 2, 14), 0)
};

重なりのあるタスクを入れておくと、日付計算やスクロール表示の確認がしやすくなります。

6-4. 日付計算ロジックの共通化

WinFormsでもWPFでも、日付からX座標や幅を計算する処理は共通化できます。

C#
public static class GanttDateCalculator
{
public static int GetX(DateTime chartStartDate, DateTime targetDate, int dayWidth)
{
return (targetDate.Date - chartStartDate.Date).Days * dayWidth;
}

public static int GetWidth(DateTime startDate, DateTime endDate, int dayWidth)
{
int days = (endDate.Date - startDate.Date).Days + 1;
return Math.Max(dayWidth, days * dayWidth);
}

public static DateTime GetDateFromX(DateTime chartStartDate, int x, int dayWidth)
{
int days = (int)Math.Round((double)x / dayWidth);
return chartStartDate.Date.AddDays(days);
}
}

このように共通クラスを作っておくと、WinForms版とWPF版で同じ日付計算ルールを使えます。ドラッグ移動やサイズ変更を実装するときにも便利です。

6-5. 実行結果の確認ポイント

サンプルコードを実行したら、次の点を確認します。

  • タスク名が左側に表示されているか

  • 日付ヘッダーが正しい順番で表示されているか

  • タスクバーの開始位置が開始日と一致しているか

  • タスクバーの幅が終了日まで含まれているか

  • 進捗率の塗り分けが正しいか

  • 今日線が正しい日付に表示されているか

  • スクロール時にバーと日付軸がずれていないか

  • 画面サイズ変更時に再描画されるか

特に確認すべきなのは、終了日を含めた幅になっているかどうかです。日数差だけで計算すると、1日分短く表示されることがあります。そのため、終了日を含める場合は必ず「+1日」して計算します。

7. C#でガントチャートを作るときの注意点

7-1. 日付計算のズレを防ぐ方法

C#でガントチャートを作るときは、日付計算のズレに注意が必要です。DateTimeには日付だけでなく時刻も含まれるため、時刻部分が混在しているとDaysの値が想定と異なる場合があります。

次のようにDateプロパティを使い、日付部分だけで計算するのがおすすめです。

C#
int days = (task.EndDate.Date - task.StartDate.Date).Days + 1;

また、終了日を含めるのか、終了日の前日までを表示するのかも明確にしておきましょう。業務アプリのガントチャートでは、開始日から終了日までを含めて表示することが多いため、日数差に1を足す実装が一般的です。

7-2. 大量タスク表示時の描画パフォーマンス対策

タスク数が多くなると、すべての行を毎回描画すると重くなります。数百件、数千件のタスクを扱う場合は、表示範囲外のタスクを描画しないようにします。

WinFormsでは、rowYが画面外にある場合はcontinueでスキップします。

C#
if (rowY + RowHeight < HeaderHeight || rowY > panel.Height)
{
continue;
}

WPFでは、ItemsControlよりも仮想化に対応したコントロールを検討する、表示範囲に応じてデータを絞り込む、Canvas上の要素数を減らすなどの工夫が必要です。

また、頻繁に再描画する処理では、BrushやPenをループ内で大量生成しないようにすることも大切です。

7-3. 画面サイズ変更時の再描画処理

画面サイズを変更したときは、ガントチャートの表示範囲も変わります。WinFormsではResizeイベントやResizeRedrawを使って再描画します。

C#
private void MainForm_Resize(object sender, EventArgs e)
{
ganttPanel.Invalidate();
}

WPFでは、親要素のサイズ変更に合わせてCanvasやGridのサイズを更新する必要があります。Autoレイアウトだけで対応できない場合は、ActualWidthやActualHeightを使って再計算します。

サイズ変更時には、ヘッダー、タスク名エリア、スクロールバー、チャート本体の位置関係が崩れないように注意しましょう。

7-4. UIスレッドを止めないための実装ポイント

データベースから大量のタスクを読み込む場合や、Excel・PDF出力を行う場合、UIスレッド上で重い処理を実行すると画面が固まります。

C#では、async/awaitを使って非同期処理にするのが基本です。

C#
private async Task LoadTasksAsync()
{
var loadedTasks = await Task.Run(() =>
{
return LoadTasksFromDatabase();
});

tasks.Clear();
tasks.AddRange(loadedTasks);

ganttPanel.Invalidate();
}

ただし、WinFormsやWPFのUIコントロールはUIスレッドから更新する必要があります。バックグラウンド処理でデータを取得し、画面更新だけをUIスレッドで行うようにします。

7-5. 印刷・出力時にレイアウトが崩れないようにする方法

ガントチャートを画像やPDFに出力するときは、画面表示用の座標と出力用の座標を分けて考える必要があります。

画面ではスクロール位置を反映しますが、出力では全体を描画したい場合があります。そのため、描画処理には次のような引数を持たせると便利です。

C#
private void DrawGanttChart(Graphics g, int width, int height, int offsetX, int offsetY)
{
// offsetXとoffsetYを使って描画位置を調整する
}

印刷やPDF出力では、用紙サイズに合わせて縮小する、複数ページに分割する、月単位でページを分けるなどの設計が必要になります。画面表示と出力を完全に同じロジックで処理しようとすると崩れやすいため、共通化する部分と出力専用に調整する部分を分けるのがポイントです。

8. C#ガントチャートは自作とライブラリ利用のどちらがよいか

8-1. 自作ガントチャートのメリット・デメリット

C#でガントチャートを自作するメリットは、自由にカスタマイズできることです。表示項目、色、バーの形、クリック操作、独自ルールなどをアプリに合わせて実装できます。

また、WinFormsやWPFの描画処理を学ぶうえでも、自作は非常に良い題材です。日付計算、座標変換、スクロール、イベント処理、データバインディングなど、C#アプリ開発に必要な知識をまとめて学べます。

一方で、デメリットもあります。ドラッグ編集、依存関係、ズーム、印刷、PDF出力、大量データ対応、Undo/Redoなどをすべて自作すると、開発工数が大きくなります。テストや保守の負担も増えるため、業務で長く使う場合は慎重に判断する必要があります。

8-2. 有料・無料ライブラリを使うメリット・デメリット

ガントチャート用ライブラリを使うメリットは、短期間で高機能な画面を実装できることです。スクロール、編集、依存線、マイルストーン、リソース表示、印刷などが最初から用意されている場合があります。

特に業務アプリでは、開発工数を削減できる点が大きな利点です。自作では数週間かかる機能でも、ライブラリを使えば数日で実装できることがあります。

一方で、デメリットとしては、ライセンス費用、商用利用条件、カスタマイズ制限、バージョンアップ対応があります。無料ライブラリの場合も、メンテナンス状況や対応フレームワークを確認してから導入することが大切です。

8-3. 業務アプリでライブラリ利用を検討すべきケース

次のような場合は、自作よりもライブラリ利用を検討したほうがよいでしょう。

  • タスク数が多い

  • ドラッグ編集が必須

  • 依存関係を表示したい

  • リソース管理が必要

  • 印刷やPDF出力が必要

  • Excelのような操作感が求められる

  • 開発期間が短い

  • 長期保守が前提

  • ユーザー数が多い

業務アプリでは、見た目だけでなく操作性や保守性も重要です。ガントチャートがシステムの中心機能になる場合は、ライブラリの採用によって開発リスクを下げられる可能性があります。

8-4. 学習・小規模アプリなら自作が向いているケース

次のような場合は、自作ガントチャートが向いています。

  • 表示だけできればよい

  • タスク数が少ない

  • 社内向けの簡易ツールである

  • 独自の見た目にしたい

  • C#の描画処理を学びたい

  • ライセンス費用をかけたくない

  • WinFormsやWPFの標準機能だけで作りたい

たとえば、10〜100件程度のタスクを表示するだけであれば、PanelやCanvasを使った自作でも十分実用的です。最初はシンプルに作り、必要な機能だけを段階的に追加していく方法がおすすめです。

8-5. WinForms・WPF・Webアプリで選ぶべき実装方針

既存のWinFormsアプリにガントチャートを追加するなら、PanelやUserControlに描画する方法が導入しやすいです。DataGridViewと組み合わせれば、既存の業務画面にも組み込みやすくなります。

WPFで新規開発する場合は、Canvas、ItemsControl、Binding、MVVMを活用する構成がおすすめです。見た目を柔軟に変更でき、データ更新にも対応しやすくなります。

Webアプリとして作る場合は、C#だけで描画するのではなく、ASP.NET CoreとJavaScriptのガントチャートライブラリを組み合わせる方法もあります。ブラウザ上で操作するガントチャートは、既存のWeb向けライブラリを使ったほうが実装しやすい場合があります。

9. C#ガントチャート作成でよくある質問

9-1. C#でガントチャートを無料で作成できますか?

はい、C#の標準機能だけでもガントチャートは無料で作成できます。WinFormsならPanelやGraphics、WPFならCanvasやRectangleを使って、タスクバーや日付軸を自作できます。

ただし、高度な編集機能、依存関係、印刷、PDF出力、大量データ対応まで無料で自作する場合は、それなりの開発工数が必要です。表示だけなら自作、業務レベルの高機能が必要ならライブラリ利用を検討するとよいでしょう。

9-2. WinFormsだけでガントチャートは作れますか?

はい、WinFormsだけでもガントチャートは作成できます。PanelやUserControlのPaintイベントで、日付ヘッダー、タスク名、タスクバー、進捗率、今日線などを描画できます。

既存のWinForms業務アプリに組み込む場合は、WinFormsで自作する方法が最も導入しやすいです。DataGridViewとPanelを組み合わせれば、左側にタスク一覧、右側にガントチャートを表示する構成も作れます。

9-3. WPFでExcelのようなガントチャートは作れますか?

はい、WPFでもExcelのようなガントチャートは作れます。GridやItemsControlを使って表形式の行を作り、Canvas上にタスクバーを配置すれば、Excelの工程表に近い画面を実装できます。

さらに、Binding、Style、DataTemplateを活用すれば、進捗表示、色分け、マイルストーン、ドラッグ編集なども実装できます。ただし、Excelと同じ操作感を完全に再現するには、セル選択、コピー、貼り付け、編集、印刷などの実装が必要になります。

9-4. タスクバーをマウスで編集できますか?

はい、タスクバーをマウスで編集できます。WinFormsではMouseDown、MouseMove、MouseUpイベントを使い、WPFでは同様のマウスイベントやBehaviorを使って実装します。

ドラッグ移動では、マウスの移動量を日数に変換し、StartDateとEndDateを更新します。期間変更では、バーの右端や左端をドラッグしたかどうかを判定し、終了日または開始日を変更します。

実装時は、1ピクセル単位ではなく1日単位に丸めると、ガントチャートらしい操作になります。

9-5. ガントチャートを画像やPDFに出力できますか?

はい、出力できます。画像出力では、Bitmapにガントチャートを描画してPNGやJPEGとして保存します。PDF出力では、PDF生成ライブラリを使って画像や表を配置する方法が一般的です。

WinFormsで自作している場合は、画面描画と画像出力の描画ロジックを共通化しておくと便利です。WPFの場合は、VisualBrushやRenderTargetBitmapを使って画面要素を画像化する方法があります。

9-6. C#で使えるおすすめのガントチャートライブラリはありますか?

C#でガントチャートを作る場合、WinForms向け、WPF向け、ASP.NET向けなど、用途に応じたUIコンポーネントを検討できます。市販ライブラリには、ガントチャート専用コントロールやスケジューラー機能を備えたものがあります。

ライブラリを選ぶときは、次の点を確認しましょう。

  • WinFormsに対応しているか

  • WPFに対応しているか

  • .NETの対象バージョンに対応しているか

  • 商用利用できるか

  • 日本語表示に問題がないか

  • ドラッグ編集に対応しているか

  • 依存関係やマイルストーンを表示できるか

  • 印刷やPDF出力に対応しているか

  • サポートやアップデートが継続されているか

おすすめは、最初からライブラリを決めるのではなく、必要な機能を整理してから比較することです。単純な表示だけなら自作で十分ですが、高機能な業務用ガントチャートが必要なら、ライブラリを使うほうが結果的に開発コストを抑えられる場合があります。

まとめ

C#でガントチャートを作成するには、まずタスク名、開始日、終了日、進捗率などのデータ構造を決め、日付を画面上の座標に変換する考え方を理解することが重要です。

WinFormsでは、PanelやUserControlのPaintイベントでGraphicsを使って描画する方法が基本です。既存の業務アプリに組み込みやすく、シンプルなガントチャートであれば標準機能だけでも十分作成できます。

WPFでは、Canvas、Grid、ItemsControl、Bindingを使ってガントチャートを構成できます。MVVMとの相性がよく、見た目のカスタマイズやデータ更新に強い点が特徴です。

ガントチャートを実用的にするには、進捗率、今日線、土日・祝日表示、マイルストーン、依存関係、ドラッグ編集、CSV・Excel・画像・PDF出力などの機能を必要に応じて追加していきます。

学習目的や小規模アプリであれば自作がおすすめです。一方、業務システムで高機能なガントチャートが必要な場合は、ライブラリ利用も有力な選択肢です。

C#でガントチャートを作るポイントは、最初から複雑にしすぎないことです。まずは「日付軸を表示する」「タスクバーを表示する」「進捗率を表示する」という基本機能を作り、その後にスクロール、ドラッグ編集、出力機能などを段階的に追加していくと、保守しやすく実用的なガントチャートを作成できます。