エクセルで工程表を自動作成する方法と、8年でわかった限界

当ページのリンクには広告 (Amazonアソシエイト含む) が含まれています。
ガントチャートをExcelで作る
サクッと結論

関数と条件付き書式だけで、開始日と終了日を入力すればバーが自動で出るところまで作れます。マクロは要りません。

同じ工程表を毎回作り直すなら、テーブル+VBAでボタン1つにできます。

ただし「何%進んだか」「実際にいつ終わったか」は、誰かが入力するか、他のシステムから持ってこない限り残りません。ここが最後まで残ります。

工場で、設備をいったん止めて行う定期メンテナンス工事の工程表を8年作ってきました。関わる会社も人も多いので、日程が1日ずれるだけで連絡が何本も飛びます。それでも道具はずっとExcelでした。専用ソフトを入れる話にはならず、関係者全員がその場で開けるものが、Excelしかなかったからです。

その工程表を、毎回こうやって作っていました。前の年のファイルをコピーして、日付を今年に直して、土日の位置がずれた分だけセルの色を塗り直す。作業を1本足すたびに、3行セットをコピーしてサイズを合わせ直す。私はこれに、毎回半日から丸1日かけていました。

やっていることは工程管理のはずなのに、実際の手の動きはイラストを描く作業でした。バーを細く見せようとすると今度はマウスカーソルが乗らないので、Ctrlとマウスホイールで拡大して、色を塗って、倍率を戻して、文字を書いて、またセルの高さを合わせる。

いまはこの作り直しがありません。独学でExcelの関数とVBAを覚えたからですが、入口は特別なものではなく、この記事に書く数式を1本入れただけでした。そのとおりに組めば、開始日と終了日を打つとバーが出ます。私はここに来るまでに3年かかりました。当時はAIに聞くこともできなかったので、遠回りした部分も含めて全部書きます。

この記事のレベル感を5角形のレーダーチャートで示した図
中心が0、外側が5。「覚えやすさ」「つまずきにくさ」は高いほど楽という意味です。

検証環境: Microsoft 365(ビジネス)/ Windows 11 / 確認日 2026年8月9日
※本記事の情報は 2026年8月時点 のものです。最新情報は公式ドキュメントをご確認ください。

Schedikaの画面。親・子・孫の階層になったタスク一覧と、計画と実績のズレを示すイナズマ線、遅れている作業の赤い表示
親・子・孫で折りたためて、イナズマ線で遅れがそのまま見えます
PR・自社製品

数式もVBAも書かずに、工程表を引き直す

工程表を引き直すたびに、行をコピーしてバーの長さを合わせ直す。あの作業をなくすために Schedika を作りました。手元のPCだけで動きます。クラウドが使えない職場を前提にしています。

  • インストール不要
  • 外部送信なし
  • CSV・PNGで持ち出せる
Schedika のページを見る →

無料で試せる版があります。この記事を読むだけでも工程表は作れます。

目次

マクロなしで、バーが自動で出るところまで作る

先に結論を書くと、検索して出てくる「工程表の自動作成」の多くは、開始日と終了日からバーを出すところまでです。そこまでならマクロは要りません

必要なのは3つだけです。日付の行、土日の色分け、そしてバーの色分け。順番に作ります。

このあと出てくる数式は、次のレイアウトを前提にしています。自分の表に合わせて列番号だけ読み替えてください。

スクロールできます
場所入れるもの
B列作業内容
C列開始日
D列終了日
2行目(E列以降)月。数式で出す
3行目(E列以降)日付そのもの。あとの数式はすべてこの行を見る
4行目(E列以降)曜日。数式で3行目に追従させる
5行目以降作業。1作業につき3行を使う

見出しを3段にしているのには理由があります。列幅を3文字ぶんまで細くしたいので、1つのセルに「9/1(火)」は入りません。月・日・曜を縦に分ければ、1列が細いままでも読めます。現場の工程表がたいていこの形をしているのも同じ理由です。

作業のほうも1行では作りません。1作業につき3行を使います。1行目に作業名と開始日・終了日、2行目にバー、3行目はすきまです。行の高さは上から17・13・5あたりが目安です。

1作業につき3行を使ったレイアウト。1行目が作業名、2行目がバー、3行目がすきま
1作業につき3行のイメージ

3行に分ける理由は3つあります。バーの行を細くすると線が引き締まって、本数が増えても潰れません。作業名とバーが同じ行にないので、名前が長くてもバーに重なりません。そしてすきまの行があると、作業と作業の切れ目が目で追えます。ここを詰めると、10本を超えたあたりから急に読めなくなります。次の章のVBA版も、同じ3行1セットで描いています。

罫線も先に入れておいてください。カレンダーが右へ長くなると、線の無い表は目で行を追えなくなります。マス目に細い線を敷いたうえで、作業のまとまりごとに1本、少し濃い横線を引くと一気に読めるようになります。月曜の左に薄い縦線を足すと、週の区切りも分かります。なお次の章のVBA版では、この線もボタンを押すたびにマクロが引き直します。

マクロなしで作った工程表の完成形。左に作業名、右に月・日・曜の3段ヘッダーとガントバーが並んでいる
この章のとおりに作ると、この形になります。1作業に3行使い、2行目にだけバーを出しています。マクロは使っていません。

ここから3つの手順に分けて作ります。このあとの3枚の画面は、仕組みが見えるように1作業1行の簡単な形で撮っています。3行1セットに広げるのは最後に数式を1か所足すだけなので、先に仕組みのほうを押さえてください。

日付の行は「開始日+1」で作る

E3に開始日、F3に=E3+1を入れて右へコピーした日付の行
手で入れるのはE3だけ。F3から先は =E3+1 でつないで右へコピーします。E4は =E3 に書式 aaa。

E3に工事の開始日を入れます。そしてF3に、こう書きます。

=E3+1

あとは右へオートフィルするだけです。日付は連番なので、足し算1つで並びます。

ここで大事なのは、日付を手で打たないことです。手で打つと、開始日が1日ずれただけで全部打ち直しになります。E3だけ直せば右が全部ついてくる形にしておくと、あとがまったく違います。

3行目の表示形式は、ユーザー定義で d にします。日にちだけが出るので、列幅3文字でも収まります。

セルの書式設定でユーザー定義に d を指定している画面
セルの書式設定で「d」を設定

曜日の行(4行目)は、真上のセルを見るだけです。E4にこう入れて、書式を aaa にします。

=E3

月の行(2行目)は、月が変わる列にだけ「2026年9月」と文字で入れます。3か月の工事でも3回だけなので、手で入れて十分です。

ここは数式にしたくなるところですが、実際にやってみるとうまくいきません。数式が空文字を返すと、Excelはそのセルを「中身がある」と判断して、隣のセルからの文字のはみ出しを止めます。列幅は3文字しかないので、はみ出せないと「202」で切れて読めなくなります。ラベルを置かない列は、数式も入れずに本当に空のままにしてください。

曜日の行は数式のままで問題ありません。3行目の日付を書き換えれば、曜日はついてきます。

土日は条件付き書式とWEEKDAY関数で自動的に塗る

条件付き書式で土日の列がグレーに塗られた工程表
WEEKDAY関数のルールを1つ入れるだけで、土日が縦に通ります。

私は最初、土日を手で塗っていました。前年の工程表をコピーすると土日の位置が変わるので、そのたびに塗り直しです。行を1本足すたびにまた塗り直しでした。

これは条件付き書式に置き換えられます。Excelで最初にやめるべき手作業がこれです。

E5から表の右下までを選択して、ホームタブの「条件付き書式」→「新しいルール」→「数式を使用して、書式設定するセルを決定」を選びます。数式欄にこう入れます。

=WEEKDAY(E$3,2)>=6

WEEKDAY は日付が何曜日かを数字で返す関数です。第2引数に 2 を指定すると、月曜が1、日曜が7になります。つまり 6以上なら土日です。この対応はMicrosoftサポートの WEEKDAY 関数のページに一覧があります。

E$3$ は行だけを固定する意味です。これを付けないと、下の行に行くほど参照する日付がずれていきます。条件付き書式でいちばん多いつまずきがここです。

同じルールを曜日の行(4行目)にも入れておくと、土日の帯が見出しまで通って読みやすくなります。ただし月の行(2行目)には入れないでください。月のラベルは月初の列にだけ文字を置いて、右隣の空セルへはみ出させて表示しています。その途中に色が入ると、文字の下だけ帯が割り込んだように見えます。日付の行(3行目)も、背景を濃い色にしているなら入れません。土日だけ地の色が変わって、見出しが途切れて見えます。

なお条件付き書式の数式は、等号で始めてTRUEかFALSEを返す形でなければ動きません。またMicrosoftサポートの条件式の解説によると、別のブックへの外部参照は条件付き書式には使えません。祝日リストを別ファイルに置きたくなりますが、同じブックの中に置いてください。

祝日も塗りたい場合は、どこかのシートに祝日の日付を並べて名前を付け(ここでは 祝日 とします)、2つ目のルールとして追加します。

=COUNTIF(祝日,E$3)>0

祝日を除いた営業日で日数を数えたい場合は、WORKDAY関数の使い方NETWORKDAYS関数の解説のほうが向いています。この記事の工程表は「暦日で並べる」前提なので、色分けだけ入れてあります。

バーは「開始日と終了日のあいだなら塗る」だけ

開始日と終了日を入力するとガントバーが自動で表示された状態
C列とD列の日付を変えるだけで、バーが動きます。

ここが自動作成の本体です。やることは1つで、その列の日付が、その行の開始日と終了日のあいだに入っていたら色を塗る。それだけです。

同じくE5から表の右下を選択して、条件付き書式に次の数式を追加します。

=AND($C5<>"",E$3>=$C5,E$3<=$D5)

読み方はこうです。

  • $C5<>"" … 開始日が入っている行だけを対象にする(空の行に色が出るのを防ぐ)
  • E$3>=$C5 … その列の日付が、開始日以降である
  • E$3<=$D5 … その列の日付が、終了日以前である

$C5 は列だけを固定、E$3 は行だけを固定です。この $ の付け方さえ合っていれば、あとは表をどれだけ広げても崩れません。

ここまで作ると、C列とD列に日付を打つだけでバーが出ます。開始日を1日ずらせば、バーも1日ずれます。塗り直しはもう発生しません。

C列とD列に期間を入れるとバーが自動で塗られた状態

ルールの適用順にだけ注意してください。土日のルールが上にあると、バーが土日で途切れて見えます。「条件付き書式ルールの管理」で、バーのルールを土日より上に置いてください。土日も含めて連続で見せたいならこの順、土日は動かない日として抜いて見せたいなら逆の順です。どちらが正しいということはなく、現場の見方に合わせます。

条件付き書式ルールの管理で、バーのルールを土日のルールより上に並べた画面
バーのルールを土日より上に置くと、土日でバーが途切れません。

ここまでの数式は、さきほど断ったとおり1作業1行で作った場合のものです。上の3枚の画面もその形です。冒頭の完成形のように1作業3行にするなら、「バーの行だけ塗る」という条件を1つ足します。E5から右下までを選んで、次の数式に差し替えてください。

=AND(MOD(ROW()-5,3)=1,$C4<>"",E$3>=$C4,E$3<=$D4)

足したのは先頭の MOD(ROW()-5,3)=1 だけです。5 は作業が始まる行、3 は1セットの行数。この計算が1になる行が、3行のうちの2行目、つまりバーの行です。参照が $C5 から $C4 に1つ上がっているのは、日付が1行上の作業名の行に入っているからです。範囲はE5から右下までのまま、ルールも2つのままで済みます。作業を足すときは3行ずつ足してください。これで、この章のはじめに出した完成形と同じ形になります。

なお、条件付き書式を大量に設定したブックは動作が重くなることがあります。Microsoftのサポート記事でも、条件付き書式が多いブックで編集が遅くなる場合があることが案内されています。ただしルール数の上限は公式に数値が示されていませんので、「何個までなら安全」という言い方はできません。私の感覚では、範囲を表の実サイズぴったりに絞っておけば実用上は困りませんでした。

VBAでボタン1つにする(テーブルから工程表を描く)

VBAで作成したガントチャートです
VBAで作成したガントチャートです

ここまでの形で、私は最初の3年をやっていました。次の1年は手塗りとVBAを行ったり来たりして、最後の4年はVBAでした。

正直に書くと、ハイブリッドの1年がいちばんしんどかったです。頭の中に「こうなってほしい」という形はあるのに、当時はAIに聞くこともできなかったので、こうやればいいのかな、いや違うな、を延々とやっていました。いま同じことをやるなら、たぶん数日で組めると思います。

では、なぜ条件付き書式で足りているのにVBAにしたのか。表示する期間を切り替えたかったからです。

条件付き書式のやり方は、カレンダーの列をあらかじめ全部並べておく必要があります。3か月の工事なら90列。ここに「今週だけ見たい」「来月だけ見たい」を足そうとすると、列を隠したりスクロールしたりで結局手が動きます。そこをボタン1つにしたかったというのが動機です。

準備:作業リストは「テーブル」にしておく

VBAにする前に、作業リストをExcelのテーブルにします。範囲を選んで、ホームタブの「テーブルとして書式設定」を選ぶだけです。

テーブルにしておく理由は、行が増えても範囲を書き直さなくていいからです。ふつうのセル範囲だと A2:E50 のように書くことになり、51行目を足した瞬間にコードが拾わなくなります。テーブルなら行を足した分だけ自動で広がります。

Microsoftサポートのテーブルの概要では、フィルタが自動で有効になること、集計列は1つのセルに数式を入れれば列全体に適用されること、テーブル名[列名] という書き方で数式が読みやすくなることが挙げられています。

私が持たせていた列はこれだけです。

スクロールできます
列名中身
作業内容工程の名前
開始日いつから(日)
開始時間いつから(時刻)
終了日いつまで(日)
終了時間いつまで(時刻)
備考短いメモ。その作業のバーの真上に出ます

進捗率の列はありません。これは後で書きますが、最後まで作りませんでした。

日付と時間は別の列にしてください。1つのセルにまとめると、日だけを見たい全体工程のほうで、毎回時刻を切り落とすことになります。分けておけば、全体工程は日の列だけを見て、あとで作る詳細工程が時間の列も見る、という形にできます。

備考は、その作業のバーの真上に出ます。1作業3行にしてあるので、タイトル行のカレンダー側は空いたままです。そこにメモを置くと、右隣の空セルへはみ出して、バーに重ならずに読めます。長い文章を入れると右へ流れていくので、10文字前後までにしておくと収まりがいいです。

あとで出てくるコードは、列を名前で探しています(ListColumns("開始日") という書き方です)。左から何番目かで数えていないので、列を足しても、順番を入れ替えても、マクロは壊れません。使わない列を置いておいても大丈夫です。

テーブルには分かりやすい名前を付けておきます。テーブル内をクリックして「テーブルデザイン」タブの左端で変更できます。名前にはスペースが使えず、ブックの中で重複できない決まりです(構造化参照の公式解説)。

1つだけ決めておくことがあります。このテーブルは、ガントチャートとは別のシートに置いてください。

同じシートに置くと、バーを描く領域とテーブルが場所を取り合います。カレンダーは右へどんどん伸びるので、テーブルをどこに逃がしても、いつか当たります。私は「作業リスト」というシートを作って、そこにはテーブルだけを置いていました。

作業内容・開始日・開始時間・終了日・終了時間・備考の6列を持つExcelのテーブル
ガントチャートの表示内容をリストで管理します

ボタンを押したら、いったん全部消してから描き直す

ここが設計の分かれ目です。私は上書きではなく、全部消してから描き直す形にしました。

理由は単純で、上書きだと消し忘れが出るからです。作業の期間を短くしたとき、前に塗ったバーの右端が残ります。それを1つずつ判定して消すコードを書くくらいなら、毎回まっさらにしたほうが確実でした。

そして表の左上にドロップダウンを置きました。そこで日付を選んでボタンを押すと、その日が表のいちばん左になります。選んだだけでは何も動きません。描き直すのはボタンのほうです。

ドロップダウンの中身は、作業リストのシートの空いているところに日付を並べておき、データタブの「データの入力規則」で種類を「リスト」にして、その範囲を指定するだけです。

候補の日付も手で打つ必要はありません。先頭に1つだけ日付を入れて、その下に =(1つ上のセル)+7 を入れて下へコピーすれば、1週間おきに並びます。日付の行とまったく同じ考え方です。週単位で並べておくと、表示する範囲を1週間ずつずらせます。

1つだけ気をつけてください。入力規則で指定する範囲は、実際に日付が入っている行数ぴったりにします。空のセルまで範囲に入れると、ドロップダウンに空行が並びます。候補を増やすときは、数式を下へコピーしてから範囲も広げてください。なお作業リストのテーブルは下へ伸びるので、候補は右か、別のシートに置いておくとぶつかりません。

コードはこうなります。標準モジュールを2つ作って貼ってください。VBAをまだ触ったことがない場合は、先にVBAの始め方でエディタの開き方と標準モジュールの作り方を確認しておくと迷いません。

この先のコードは、説明のために区切った抜粋です。そのまま動かしたい場合は、記事の最後にある貼り付け用の完成版を2つ貼れば終わります。モジュールは2つに分けます。分ける線は「最初に一度だけ触るもの」と「ずっと動き続けるもの」です。工程表設定には設定の値とシートを作る初期設定が入り、触るのはこちらだけ工程表マクロは描くところと書き出すところで、開かなくて構いません。

こう分けておくと、渡したあとが楽になります。設定を頼まれた人は工程表設定を開けば済みますし、こちらがコードを直して配り直すときも、工程表マクロだけ入れ替えてもらえば相手の設定はそのまま残ります

この記事のコードは6つに分けて出てきます。貼り先は、工程表設定が①設定の値と⑥土台づくり(記事のいちばん最後)、工程表マクロが②全体工程・③詳細工程・④書き出し・⑤フォルダを選ぶ、です。

モジュールの名前は、左のプロパティウィンドウの (Name) で変えられます。名前が違っても動きます。

まず1つ目、工程表設定です。

クリックして全てのコードを見る
Option Explicit

'============================================================
' 工程表設定
'
'   最初に一度だけ触るところです。
'
'   ① 下の設定を、自分の表に合わせる(変えなくても動きます)
'   ② SetupBook を1回実行すると、シートとボタンができあがります
'
'   もう1つの「工程表マクロ」は、描くところと書き出すところだけです。
'   開かなくて構いません。
'============================================================

'---- ① シートとテーブルの名前 ----
Public Const SHEET_NAME    As String = "全体工程"   ' ガントチャートを描くシート
Public Const DAILY_SHEET   As String = "詳細工程"   ' その日の時間割を描くシート
Public Const LIST_SHEET    As String = "作業リスト" ' 作業リストのテーブルがあるシート
Public Const TABLE_NAME    As String = "作業リスト" ' テーブルの名前

'---- ② 表のどこに何があるか ----
Public Const START_CELL    As String = "B2"  ' 全体工程で、表示開始日を選ぶセル
Public Const DAILY_CELL    As String = "B2"  ' 詳細工程で、表示する日を選ぶセル
Public Const MONTH_ROW     As Long = 2       ' 月のラベルを出す行
Public Const HEAD_ROW      As Long = 3       ' 日付を並べる行
Public Const WEEK_ROW      As Long = 4       ' 曜日の行(=E3 の数式が入っている)
Public Const FIRST_ROW     As Long = 5       ' 1件目のタイトル行
Public Const FIRST_COL     As Long = 5       ' E列からカレンダーを描く

'---- ③ どれだけ出すか ----
Public Const DAY_COUNT     As Long = 60      ' 全体工程を横に何日ぶん出すか
Public Const HOUR_COUNT    As Long = 24      ' 詳細工程を0時から何時間ぶん出すか

'---- ④ 見た目(1作業ぶんの高さ)----
Public Const ROWS_PER_JOB  As Long = 3       ' タイトル行+バー行+隙間行
Public Const H_TITLE       As Double = 17    ' タイトル行の高さ
Public Const H_BAR         As Double = 13    ' バーの行の高さ(細いほど締まって見える)
Public Const H_GAP         As Double = 5     ' すきまの行の高さ

'---- ⑤ 勤務時間(詳細工程で、人がいない時間を塗るため)----
Public Const WORK_START    As Long = 8       ' 日勤の始まり
Public Const WORK_END      As Long = 17      ' 日勤の終わり
Public Const LUNCH_HOUR    As Long = 12      ' 昼休み

'---- ⑥ 書き出し先。無ければ作ります ----
Public Const EXPORT_DIR    As String = "C:\Users\Public\工程表\"

'---- ⑦ 最初に入れておくサンプルの分量(土台づくりだけが使う)----
Public Const JOB_COUNT     As Long = 9     ' サンプルの作業の数
Public Const PICK_WEEKS    As Long = 9     ' 全体工程のドロップダウンに出す週の数
Public Const PICK_DAYS     As Long = 33    ' 詳細工程のドロップダウンに出す日数

'---- 土台づくりが「何をしたか」を覚えておく箱。あとでまとめて報告する ----
Private mMade As String   ' 作ったもの
Private mKept As String   ' もとからあったので触らなかったもの
Private mWarn As String   ' こちらでは判断できなかったもの

①と②だけ自分の表に合わせれば、あとはそのままで動きます。まず動かしてみるだけなら、どこも変えなくて大丈夫です。なおこのモジュールには、記事の最後にもう1つコードを足します。

2つ目が工程表マクロです。ここから⑤までのコードは、すべてこちらのモジュールに入ります。

クリックして全てのコードを見る
Option Explicit

'============================================================
' 工程表マクロ
'
'   描くところと、書き出すところだけが入っています。
'   触らなくて構いません。
'   設定と初期設定は「工程表設定」にまとめてあります。
'============================================================

Public Sub DrawGantt()
    Dim ws As Worksheet
    Dim lo As ListObject
    Dim baseDate As Date
    Dim i As Long, n As Long
    Dim d As Date
    Dim lastRow As Long
    Dim titleRow As Long, barRow As Long
    Dim sDate As Date, eDate As Date
    Dim sCol As Long, eCol As Long
    Dim memoCol As Long
    Dim memo As String

    Set ws = ThisWorkbook.Worksheets(SHEET_NAME)
    Set lo = ThisWorkbook.Worksheets(LIST_SHEET).ListObjects(TABLE_NAME)
    baseDate = ws.Range(START_CELL).Value
    lastRow = FIRST_ROW + lo.ListRows.Count * ROWS_PER_JOB - 1

    ' 備考の列があるかどうかを先に調べておく。無いブックでもエラーにしないため
    memoCol = ColIndex(lo, "備考")

    Application.ScreenUpdating = False

    ' 1) 前回描いた内容を全部消す
    ClearArea ws, DAY_COUNT

    ' 2) 見出しを引き直す(2行目=月・3行目=日。4行目の曜日は数式が追従する)
    ' 月のラベルは必ず「文字」として入れる。日本語のExcelは "2026年9月" を
    ' 日付だと解釈してしまい、列幅が狭いと ### になるため先に表示形式を文字列にする。
    ' 書式もここで毎回そろえる。最初の1つだけ手で色を付けたブックだと、
    ' 月が変わって出てくる2つ目以降が既定の黒のまま残ってしまうため
    With ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), _
                  ws.Cells(MONTH_ROW, FIRST_COL + DAY_COUNT - 1))
        .NumberFormat = "@"
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(22, 50, 79)
        .HorizontalAlignment = xlLeft
    End With

    For i = 0 To DAY_COUNT - 1
        d = baseDate + i
        ws.Cells(HEAD_ROW, FIRST_COL + i).Value = d
        ' 月のラベルは左端と月初だけ。隣は空のままにしないと文字がはみ出せない
        If i = 0 Or Day(d) = 1 Then
            ws.Cells(MONTH_ROW, FIRST_COL + i).Value = MonthLabel(baseDate, i)
        End If
    Next i

    ' 3) 先に土日を塗る(ここから下は塗る順番がそのまま重なる順番になる)
    '    2行目(月のラベル)は塗らない。月名は右隣の空セルへはみ出して表示するので、
    '    途中に色が入ると文字の下だけ帯が入ったように見える
    For i = 0 To DAY_COUNT - 1
        d = baseDate + i
        If Weekday(d, vbMonday) >= 6 Then
            ws.Range(ws.Cells(WEEK_ROW, FIRST_COL + i), _
                     ws.Cells(lastRow, FIRST_COL + i)).Interior.Color = RGB(231, 226, 216)
        End If
    Next i

    ' 4) その上にバーを重ねる
    For n = 1 To lo.ListRows.Count
        titleRow = FIRST_ROW + (n - 1) * ROWS_PER_JOB
        barRow = titleRow + 1

        ' 行の高さもここで揃える。揃えないと3行とも同じ高さのままで、
        ' せっかく3行1セットにしてもバーが太く見えて締まらない
        ws.Rows(titleRow).RowHeight = H_TITLE
        ws.Rows(barRow).RowHeight = H_BAR
        ws.Rows(barRow + 1).RowHeight = H_GAP

        ws.Cells(titleRow, 2).Value = _
            lo.ListColumns("作業内容").DataBodyRange.Cells(n).Value

        sDate = lo.ListColumns("開始日").DataBodyRange.Cells(n).Value
        eDate = lo.ListColumns("終了日").DataBodyRange.Cells(n).Value

        ' 表示している期間から外れている作業は描かない
        If eDate >= baseDate And sDate <= baseDate + DAY_COUNT - 1 Then

            If sDate < baseDate Then sDate = baseDate
            If eDate > baseDate + DAY_COUNT - 1 Then eDate = baseDate + DAY_COUNT - 1

            sCol = FIRST_COL + DateDiff("d", baseDate, sDate)
            eCol = FIRST_COL + DateDiff("d", baseDate, eDate)

            ws.Range(ws.Cells(barRow, sCol), ws.Cells(barRow, eCol)) _
              .Interior.Color = RGB(15, 118, 110)

            ' 備考はバーの真上(タイトル行)に置く。タイトル行のカレンダー側は
            ' 空いているので、右隣の空セルへはみ出してそのまま読める
            If memoCol > 0 Then
                memo = CStr(lo.ListColumns(memoCol).DataBodyRange.Cells(n).Value)
                If Len(memo) > 0 Then
                    With ws.Cells(titleRow, sCol)
                        .Value = memo
                        .HorizontalAlignment = xlLeft
                    End With
                End If
            End If
        End If
    Next n

    ' 5) 最後に罫線を引き直す
    DrawBorders ws, lo.ListRows.Count, baseDate

    Application.ScreenUpdating = True
End Sub

'==== 月のラベル。次の月初までに何列空いているかで、入る長さを選ぶ ====
'   列幅は3文字ぶんしかないので、月初のすぐ手前から表を始めると
'   "2026年9月" が右へはみ出しきれず "2026年" で切れてしまう
Private Function MonthLabel(baseDate As Date, i As Long) As String
    Dim gap As Long

    ' 次にラベルが出る列(次の月の1日)まで、何列あるかを数える
    gap = 1
    Do While i + gap <= DAY_COUNT - 1
        If Day(baseDate + i + gap) = 1 Then Exit Do
        gap = gap + 1
    Loop

    If gap >= 4 Then
        MonthLabel = Format$(baseDate + i, "yyyy年m月")
    Else
        MonthLabel = Format$(baseDate + i, "m月")
    End If
End Function

'==== 前回描いた内容を消す(全体工程・詳細工程で共通)====
Private Sub ClearArea(ws As Worksheet, colCount As Long)
    Dim lastRow As Long, lastCol As Long

    ' 消す範囲は「そのシートで何かが入っている最終行」まで。作業名の位置から
    ' 逆算すると、名前を手で消したときに範囲が縮んで、前のバーが残ってしまう
    lastRow = ws.UsedRange.Row + ws.UsedRange.Rows.Count - 1
    If lastRow < FIRST_ROW Then lastRow = FIRST_ROW
    lastCol = FIRST_COL + colCount - 1

    ' 2行目のラベルは値も色も消す
    With ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), ws.Cells(MONTH_ROW, lastCol))
        .ClearContents
        .Interior.ColorIndex = xlNone
    End With

    ' 3行目は「値だけ」消す(見出しの背景色は残したいため)
    ws.Range(ws.Cells(HEAD_ROW, FIRST_COL), ws.Cells(HEAD_ROW, lastCol)).ClearContents

    ws.Range(ws.Cells(WEEK_ROW, FIRST_COL), _
             ws.Cells(WEEK_ROW, lastCol)).Interior.ColorIndex = xlNone

    With ws.Range(ws.Cells(FIRST_ROW, FIRST_COL), ws.Cells(lastRow, lastCol))
        .ClearContents
        .Interior.ColorIndex = xlNone
    End With

    ' 罫線も消す。作業が減ったとき、前回の横線が下に残ってしまうため
    ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), _
             ws.Cells(lastRow, lastCol)).Borders.LineStyle = xlNone
    ws.Range(ws.Cells(WEEK_ROW, 2), ws.Cells(lastRow, 4)).Borders.LineStyle = xlNone
    ws.Range(ws.Cells(FIRST_ROW, 2), ws.Cells(lastRow, 4)).ClearContents

    ' 行の高さも標準に戻す。作業が減ったとき、前回の高さがそのまま残るため
    ws.Rows(FIRST_ROW & ":" & lastRow).UseStandardHeight = True
End Sub

'==== 縦線を1本引く ====
Private Sub VLine(ws As Worksheet, col As Long, lastRow As Long, lineColor As Long)
    With ws.Range(ws.Cells(MONTH_ROW, col), ws.Cells(lastRow, col)).Borders(xlEdgeLeft)
        .LineStyle = xlContinuous
        .Color = lineColor
        .Weight = xlThin
    End With
End Sub

'==== マス目・見出しの下線・作業の区切り線(全体工程・詳細工程で共通)====
Private Sub GridBorders(ws As Worksheet, jobCount As Long, lastCol As Long)
    Dim lastRow As Long, k As Long, n As Long, sepRow As Long
    Dim edges As Variant

    lastRow = FIRST_ROW + jobCount * ROWS_PER_JOB - 1

    ' 1) まず全体に細いマス目を敷く
    '    消す範囲と引く範囲を必ず同じにする。ここを FIRST_ROW から始めると、
    '    見出しの行の罫線を消したまま引き直さないことになる
    edges = Array(xlEdgeLeft, xlEdgeTop, xlEdgeBottom, xlEdgeRight, _
                  xlInsideVertical, xlInsideHorizontal)
    For k = 0 To UBound(edges)
        With ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), _
                      ws.Cells(lastRow, lastCol)).Borders(edges(k))
            .LineStyle = xlContinuous
            .Color = RGB(216, 222, 230)
            .Weight = xlHairline
        End With
    Next k

    ' 2) 見出しの下に1本(B列から右端まで)
    With ws.Range(ws.Cells(WEEK_ROW, 2), _
                  ws.Cells(WEEK_ROW, lastCol)).Borders(xlEdgeBottom)
        .LineStyle = xlContinuous
        .Color = RGB(22, 50, 79)
        .Weight = xlMedium
    End With

    ' 3) 作業のまとまりごとに1本、少し濃い横線
    For n = 1 To jobCount
        sepRow = FIRST_ROW + n * ROWS_PER_JOB - 1
        With ws.Range(ws.Cells(sepRow, 2), _
                      ws.Cells(sepRow, lastCol)).Borders(xlEdgeBottom)
            .LineStyle = xlContinuous
            .Color = RGB(100, 116, 139)
            .Weight = xlThin
        End With
    Next n
End Sub

'==== 全体工程の罫線を引く ====
Private Sub DrawBorders(ws As Worksheet, jobCount As Long, baseDate As Date)
    Dim lastRow As Long, i As Long

    lastRow = FIRST_ROW + jobCount * ROWS_PER_JOB - 1
    GridBorders ws, jobCount, FIRST_COL + DAY_COUNT - 1

    ' 月曜の左に縦線を入れて、週の区切りを見せる
    For i = 0 To DAY_COUNT - 1
        If Weekday(baseDate + i, vbMonday) = 1 Then
            VLine ws, FIRST_COL + i, lastRow, RGB(148, 163, 184)
        End If
    Next i

    VLine ws, FIRST_COL, lastRow, RGB(100, 116, 139)
End Sub

'==== 列の名前から位置を探す。無ければ 0 を返す ====
Private Function ColIndex(lo As ListObject, colName As String) As Long
    Dim k As Long
    ColIndex = 0
    For k = 1 To lo.ListColumns.Count
        If lo.ListColumns(k).Name = colName Then ColIndex = k
    Next k
End Function

つまずきやすいところを分解します。

  • ListObjects(TABLE_NAME)
  • ListColumns("開始日").DataBodyRange.Cells(n)
    • 「開始日」列の n番目のデータを取る書き方です。見出し行を除いた本体だけを指すのが DataBodyRange です
  • Interior.Color = RGB(15,118,110)
  • DateDiff("d", baseDate, sDate)
    • 起点の日から何日ぶん右にずらすかを出しています。日付の引き算でも動きますが、こう書いたほうが意図が読めます
  • ScreenUpdating = False
    • 描画中の画面のちらつきを止めます。これを入れるかどうかで体感速度がまったく違います
  • NumberFormat = "@zangyo-free月のラベルを入れる行を、先に「文字列」にしています。これが無いと、日本語のExcelは 2026年9月 を日付として取り込んでしまい、列幅が狭いセルが # だけの表示になります
  • 月のラベルは、色と太さも毎回ここで指定し直しています
    • 指定しないと、そのセルに前から付いていた書式がそのまま出ます。1つ目だけ手で色を付けたブックだと、月が変わって2つ目が出てきたときだけ既定の黒になり、揃いません
  • MonthLabel
    • ラベルの長さを決めるだけの関数です。列幅は3文字ぶんしかないので、2026年9月 は右の空きセルへはみ出して表示しています。月初のすぐ手前から表を始めると、はみ出す先が足りずに 2026年 で切れます。次の月初まで4列に満たないときは 9月 の形にして、切れないようにしています
  • Borders(xlEdgeBottom)

Cells(行, 列) の書き方でつまずいた場合は、RangeとCellsの使い分けを先に読んでおくと、このコードが全部読めるようになります。

そしてここが実務で効くポイントです。ClearArea が消しているのは、VBAで塗った色と、書き込んだ値と、引いた罫線だけです。前の章で設定した条件付き書式の土日の色は消えません。

罫線を毎回引き直しているのには理由があります。作業が1件増えれば、区切りの横線を引く行も3行ずれます。手で引いた線は、作業を足した瞬間に表とずれたまま残ります。色と同じで、線も描く側に任せてしまったほうが楽でした。

ここで、前の章で作った土日の条件付き書式をどうするかという話になります。

私はVBAに移行したとき、条件付き書式を全部消しました。

理由は、条件付き書式はVBAで塗った色より優先されるからです。土日の条件付き書式を残したままバーを描くと、せっかく引いたバーが土日のところだけ消えて見えます。どちらが上に出るのかを毎回考えるのが面倒でした。

だから表示を決める場所を1つにしました。先にVBAで土日を一気に塗って、その上からバーを重ねる。コードの3)と4)がその順番です。

見え方はこうなります。

土日はグレー、作業がかかっている土日はバーの色で塗られた状態
  • 土日に作業が入っていなければ、グレーのまま
  • 土日に作業がかかっていれば、バーの色がそのまま横に抜ける

マクロなしでいくなら全部条件付き書式、VBAでいくなら全部VBA。混ぜないほうが、あとで悩みません。

最後に、これをボタンにします。開発タブ → 挿入 → フォームコントロールの「ボタン」を選び、シートの空いているところへドラッグすると、マクロを選ぶ画面が出るので DrawGantt を選びます。ボタンの文字は、右クリックの「テキストの編集」であとから変えられます。開発タブが見当たらない場合は、ファイル → オプション → リボンのユーザー設定で「開発」にチェックを入れると出てきます。

フォームコントロールのボタンにマクロ DrawGantt を割り当てている画面

ボタンを作らずに、開発タブ → マクロ から実行しても結果は同じです。ただ日付を変えるたびに実行するものなので、押す場所が表のすぐ近くにあったほうが楽でした。

その日の詳細工程を、時間で出す

9月8日の詳細工程。0時から24時まで並べ、8時より前と17時以降と昼休みをグレーにした状態
前の日から続くもの、その日で終わるもの、その日から始まるものが混ざっています。

全体工程は日の単位です。ところが現場で朝いちばんに見たいのは、その日1日をどう回すかでした。何時から何が動くのか、どこが重なるのか。これは日の単位の表では出せません。

そこで、日付を選ぶとその日だけを時間で並べるシートを持たせていました。全体工程とは別のシートにしてください。同じシートに混ぜると、列の意味が「日」と「時間」で入れ替わってしまい、どちらも読みにくくなります。

ここで1つ、実際に作ってみないと気づかなかったことがあります。時間の範囲は0時から24時まで必要でした。

日勤の時間帯は8時から17時なので、最初はその範囲だけで作りました。ところが定時より早い時間から動いている作業があるので、それが表に載りません。結局0時から24時まで全部並べて、8時より前と17時より後、それと12時の昼休みをグレーで薄く塗る形に落ち着きました。「ここは人がいない時間ですよ」という意味です。

作業リストに開始時間と終了時間の列を足しておく理由もここです。日をまたぐ作業を、その日の中でどこからどこまでにするかを決めるのに要ります。

その日が開始終了
作業の初日開始時間から24時まで
途中の日0時から24時まで
最終日0時から終了時間まで
当日で終わる開始時間から終了時間まで

4パターンありますが、コードにすると3行です。

If Int(sVal) = theDate Then sh = Hour(sVal) Else sh = 0
If Int(eVal) = theDate Then eh = Hour(eVal) Else eh = HOUR_COUNT
If eh < HOUR_COUNT And Minute(eVal) > 0 Then eh = eh + 1   ' 端数は切り上げる

上の2行が「その日が初日なら開始時間から、そうでなければ0時から」「その日が最終日なら終了時間まで、そうでなければ24時まで」です。これで表の4パターンがそろいます。Int は日付から時刻を切り落として、日だけにする関数です。

3行目は端数の処理です。この表は1時間刻みなので、8時30分のような分は列がありません。Hour は分を切り捨てるので、そのままだと8時30分〜8時45分のように1時間の枠に収まる作業で sheh が同じ値になり、バーが1本も描かれません(あとで出てくる If eh > sh Then を通らないためです)。終了だけ切り上げておけば、短い作業でも必ず1マスは出ます。

そのかわり、この作例で表示される時刻は枠の時刻です。8時30分〜9時30分の作業は「8:00〜9:00」と出ます。分まで正確に見せたい表が要るなら、1時間ではなく30分刻みで列を作り直すことになります。私は1時間で足りていたので、そこまではやりませんでした。

もう1つ、順番に気をつけてください。その日にかかる作業を、塗る前に数えます。本数が決まらないと、グレーの帯をどこまで下ろすかも、罫線をどこまで引くかも決められません。

コードはこうです。ここから先は説明のための抜粋なので、実際に貼るときは記事の最後にある貼り付け用の完成版を使ってください。

クリックして全てのコードを見る
'==== 作業の開始・終了を「日付+時間」の1つの値にして返す ====
Private Function JobStart(lo As ListObject, n As Long) As Date
    JobStart = Int(lo.ListColumns("開始日").DataBodyRange.Cells(n).Value) + _
               CDbl(lo.ListColumns("開始時間").DataBodyRange.Cells(n).Value)
End Function

Private Function JobEnd(lo As ListObject, n As Long) As Date
    JobEnd = Int(lo.ListColumns("終了日").DataBodyRange.Cells(n).Value) + _
             CDbl(lo.ListColumns("終了時間").DataBodyRange.Cells(n).Value)
End Function

'==== その日の詳細工程を描く ====
Public Sub DrawDaily()
    Dim ws As Worksheet
    Dim lo As ListObject
    Dim theDate As Date
    Dim i As Long, n As Long, k As Long, cnt As Long
    Dim lastRow As Long
    Dim titleRow As Long, barRow As Long
    Dim sVal As Date, eVal As Date
    Dim sh As Long, eh As Long
    Dim idx() As Long
    Dim memoCol As Long
    Dim memo As String

    Set ws = ThisWorkbook.Worksheets(DAILY_SHEET)
    Set lo = ThisWorkbook.Worksheets(LIST_SHEET).ListObjects(TABLE_NAME)

    ' 時間の列が無いと日をまたぐ判定ができないので、ここで止める
    If ColIndex(lo, "開始時間") = 0 Or ColIndex(lo, "終了時間") = 0 Then
        MsgBox "作業リストに「開始時間」「終了時間」の列がありません。", vbExclamation
        Exit Sub
    End If

    theDate = Int(ws.Range(DAILY_CELL).Value)
    memoCol = ColIndex(lo, "備考")

    Application.ScreenUpdating = False

    ClearArea ws, HOUR_COUNT

    ' 1) その日にかかっている作業を先に数える。
    '    行の数が決まらないと、塗る範囲も罫線の範囲も決められない
    ReDim idx(1 To lo.ListRows.Count)
    cnt = 0
    For n = 1 To lo.ListRows.Count
        If Int(JobStart(lo, n)) <= theDate And Int(JobEnd(lo, n)) >= theDate Then
            cnt = cnt + 1
            idx(cnt) = n
        End If
    Next n

    lastRow = FIRST_ROW + cnt * ROWS_PER_JOB - 1
    If lastRow < FIRST_ROW Then lastRow = FIRST_ROW

    ' 2) 時刻の目盛りを並べる。日付は B2 に出ているので、ここには書かない
    '    (同じ日付が2か所に出ると、どちらを直せばいいのか分からなくなる)
    ws.Range(DAILY_CELL).NumberFormatLocal = "yyyy/m/d(aaa)"
    For i = 0 To HOUR_COUNT - 1
        ws.Cells(HEAD_ROW, FIRST_COL + i).Value = i
    Next i

    ' 3) 人がいない時間を先に塗る。8時より前・17時以降・昼休み
    For i = 0 To HOUR_COUNT - 1
        If i < WORK_START Or i >= WORK_END Or i = LUNCH_HOUR Then
            ws.Range(ws.Cells(WEEK_ROW, FIRST_COL + i), _
                     ws.Cells(lastRow, FIRST_COL + i)).Interior.Color = RGB(231, 226, 216)
        End If
    Next i

    ' 4) その上にバーを重ねる
    For k = 1 To cnt
        n = idx(k)
        titleRow = FIRST_ROW + (k - 1) * ROWS_PER_JOB
        barRow = titleRow + 1

        ws.Rows(titleRow).RowHeight = H_TITLE
        ws.Rows(barRow).RowHeight = H_BAR
        ws.Rows(barRow + 1).RowHeight = H_GAP

        sVal = JobStart(lo, n)
        eVal = JobEnd(lo, n)

        ws.Cells(titleRow, 2).Value = _
            lo.ListColumns("作業内容").DataBodyRange.Cells(n).Value

        ' その日の中での開始と終了。前の日から続いていれば0時から、
        ' 次の日へ続くなら24時まで。ここが日をまたぐ作業の扱いになる
        If Int(sVal) = theDate Then sh = Hour(sVal) Else sh = 0
        If Int(eVal) = theDate Then eh = Hour(eVal) Else eh = HOUR_COUNT
        If eh < HOUR_COUNT And Minute(eVal) > 0 Then eh = eh + 1   ' 端数は切り上げる

        ' 24:00 は時刻として解釈されると日付に化けて #### になる。文字で入れる
        ws.Range(ws.Cells(titleRow, 3), ws.Cells(titleRow, 4)).NumberFormat = "@"
        ws.Cells(titleRow, 3).Value = sh & ":00"
        ws.Cells(titleRow, 4).Value = eh & ":00"

        If eh > sh Then
            ws.Range(ws.Cells(barRow, FIRST_COL + sh), _
                     ws.Cells(barRow, FIRST_COL + eh - 1)).Interior.Color = RGB(15, 118, 110)

            If memoCol > 0 Then
                memo = CStr(lo.ListColumns(memoCol).DataBodyRange.Cells(n).Value)
                If Len(memo) > 0 Then
                    With ws.Cells(titleRow, FIRST_COL + sh)
                        .Value = memo
                        .HorizontalAlignment = xlLeft
                    End With
                End If
            End If
        End If
    Next k

    DrawDailyBorders ws, cnt

    Application.ScreenUpdating = True

    If cnt = 0 Then
        MsgBox Format$(theDate, "m月d日") & " にかかる作業はありません。", vbInformation
    End If
End Sub

'==== 詳細工程の罫線を引く ====
Private Sub DrawDailyBorders(ws As Worksheet, jobCount As Long)
    Dim lastRow As Long

    If jobCount = 0 Then Exit Sub
    lastRow = FIRST_ROW + jobCount * ROWS_PER_JOB - 1
    GridBorders ws, jobCount, FIRST_COL + HOUR_COUNT - 1

    ' 日勤の始まりと終わりに縦線。全体工程の月曜の線と同じ役目
    VLine ws, FIRST_COL + WORK_START, lastRow, RGB(148, 163, 184)
    VLine ws, FIRST_COL + WORK_END, lastRow, RGB(148, 163, 184)
    VLine ws, FIRST_COL, lastRow, RGB(100, 116, 139)
End Sub

日付を選ぶドロップダウンは、全体工程と同じ作り方です。空いているところに日付を並べて、入力規則の「リスト」でその範囲を指定します。並べ終わったら、その列は非表示にしてください。ドロップダウンは元の列を隠したままでも動きます。表の横に日付が延々と並んでいると、それだけで読みにくくなります。

日付そのものは、選ぶセルに1回出れば十分です。同じ日付を見出しにもう一度書かないでください。2か所に出ると、どちらを直せばいいのか分からなくなります。曜日も見たいなら、選ぶセルの表示形式を yyyy/m/d(aaa) にすれば1か所で済みます。

できた工程表をExcelとPDFで書き出す

PDFで書き出した工程表
PDfで出力したイメージ

ここまでで、全体工程と詳細工程の2枚ができました。出力はExcelとPDFの両方を出せるようにしていました。理由は渡す相手によって欲しい形が違うからです。

  • Excelでほしいという人がいる。自分なりに加工したいからです
  • 私が作った元の版は、変えられたくない。だから確定版はPDFで置きます

社内で共有していたのはPDFのほうでした。この2つを毎回手で作り分けるのが面倒だったので、ボタン1つで両方出す形にしました。

全体工程と詳細工程は、どちらも同じフォルダに出します。ただしボタンは別々に要ります。

ここで1つ、最初に私がつまずいたところがあります。フォームコントロールのボタンは、引数を持つマクロを呼べません。「シート名を渡して書き出すマクロ」を1本だけ書いても、ボタンからは呼べないということです。

なので、中身は1本、入口だけ2つにします。

クリックして全てのコードを見る
'==== ボタンから呼ぶ入口。全体工程を書き出す ====
Public Sub ExportGantt()
    ExportSheet SHEET_NAME
End Sub

'==== ボタンから呼ぶ入口。詳細工程(その日の時間割)を書き出す ====
Public Sub ExportDaily()
    ExportSheet DAILY_SHEET
End Sub

'==== 中身。シート名を受け取って、同じフォルダにPDFとExcelで出す ====
Private Sub ExportSheet(sheetName As String)
    ' 出力先は「工程表設定」の EXPORT_DIR を使う
    Dim ws As Worksheet
    Dim outWs As Worksheet
    Dim baseName As String
    Dim outDir As String
    Dim ans As VbMsgBoxResult
    Dim n As Long

    Set ws = ThisWorkbook.Worksheets(sheetName)
    outDir = EXPORT_DIR
    ' 末尾の \ を忘れると、フォルダ名がファイル名の頭にくっついて
    ' 1つ上のフォルダに出てしまう。忘れても動くようにここで補う
    If Right$(outDir, 1) <> "\" Then outDir = outDir & "\"
    If Dir(outDir, vbDirectory) = "" Then MkDir outDir
    ' 受け取ったシート名をそのままファイル名に使う。
    ' 全体工程なら「20260812_全体工程」、詳細工程なら「20260812_詳細工程」
    baseName = Format$(Date, "yyyymmdd") & "_" & sheetName

    ' 同じ名前がすでにあるかを先に見る。PDFとExcelのどちらか一方でもあれば聞く
    If FileExists(outDir & baseName & ".pdf") _
       Or FileExists(outDir & baseName & ".xlsx") Then

        ans = MsgBox(baseName & " はすでにあります。上書きしますか?" & vbCrLf & vbCrLf & _
                     "はい    … 上書きする" & vbCrLf & _
                     "いいえ   … _2、_3 と番号を付けて別に保存する" & vbCrLf & _
                     "キャンセル … 書き出しをやめる", _
                     vbYesNoCancel + vbQuestion, "書き出し")

        If ans = vbCancel Then Exit Sub

        If ans = vbNo Then
            ' PDFとExcelの両方が空いている番号まで進める。片方だけで判定すると
            ' PDFが_2でExcelが_3のようにずれて、どれが同じ書き出しか分からなくなる
            n = 2
            Do While FileExists(outDir & baseName & "_" & n & ".pdf") _
                     Or FileExists(outDir & baseName & "_" & n & ".xlsx")
                n = n + 1
            Loop
            baseName = baseName & "_" & n
        End If
    End If

    ' このシートだけ別ブックにコピーして、そちらから書き出す
    ws.Copy
    Set outWs = ActiveWorkbook.Worksheets(1)

    ' ボタンはコピーにも付いてくる。渡した先にはマクロが無いので押すとエラーになるし、
    ' 紙にもそのまま出てしまう。コピーのほうから消しておく
    Do While outWs.Shapes.Count > 0
        outWs.Shapes(1).Delete
    Loop

    ' PDFで出す
    outWs.ExportAsFixedFormat _
        Type:=xlTypePDF, _
        FileName:=outDir & baseName & ".pdf", _
        Quality:=xlQualityStandard, _
        IgnorePrintAreas:=False, _
        OpenAfterPublish:=False

    ' Excelで出す
    ' 上書きしていいかは上で聞いたあとなので、Excelにもう一度聞かせない
    Application.DisplayAlerts = False
    ActiveWorkbook.SaveAs _
        FileName:=outDir & baseName & ".xlsx", _
        FileFormat:=xlOpenXMLWorkbook
    ActiveWorkbook.Close SaveChanges:=False
    Application.DisplayAlerts = True

    MsgBox "書き出しました。" & vbCrLf & outDir & baseName & ".pdf / .xlsx", vbInformation
End Sub

'==== ファイルがあるかどうかを見るだけの関数 ====
Private Function FileExists(filePath As String) As Boolean
    FileExists = (Dir(filePath) <> "")
End Function

ボタンは2つ作ります。全体工程のシートに1つ置いて ExportGantt を割り当て、詳細工程のシートにもう1つ置いて ExportDaily を割り当てます。作り方はさきほどと同じです。開発タブ → 挿入 → フォームコントロールの「ボタン」で、割り当てるマクロだけを変えます。

中身の ExportSheetPrivate にしてあるので、マクロを選ぶ画面には出てきません。押せる入口が2つだけ並ぶので、間違えにくくなります。

出力先はどちらも同じ EXPORT_DIR です。ファイル名の後ろにシート名が付くので、同じフォルダに 20260812_全体工程.pdf20260812_詳細工程.pdf が並びます。

PDFもExcelも、元のシートからではなくコピーから出しています。ここは実際に渡してみて分かったことです。

ws.Copy で作ったコピーには、シートに置いたボタンもそのまま付いてきます。渡した先のブックにはマクロが入っていないので、受け取った人がボタンを押すと「マクロが見つかりません」で止まります。PDFに出せば、ボタンの絵が紙にそのまま印刷されます。どちらも、渡したあとに気づくやつです。

なので、コピーを作った直後に図形を全部消してから書き出します。

Do While outWs.Shapes.Count > 0
    outWs.Shapes(1).Delete
Loop

For Each で回さずに Do While にしているのは、消しながら回すと数がずれて消し残るからです。B2のドロップダウンは図形ではなく「データの入力規則」なので、これでは消えません。日付の選択は渡した先でも残ります。

PDF出力は ExportAsFixedFormat で、Type:=xlTypePDF を指定します。IgnorePrintAreas:=False にしておくと、シートに設定した印刷範囲がそのまま効きます(Microsoft Learnの ExportAsFixedFormat)。

Excel側の保存で使っている SaveAs は、指定する形式を間違えるとマクロが消えたり、上書き確認のダイアログで処理が止まったりします。そのあたりはWorkbooks.SaveAsの使い方にまとめてあります。

上書きの扱いは、聞いてから決める形にしました。同じ名前のファイルがすでにあるときだけ、3択が出ます。

スクロールできます
押したボタンどうなるか
はいそのまま上書きする
いいえ_2_3 と番号を付けて別ファイルにする
キャンセル書き出しをやめる(何も出さずに終わる)

MsgBoxvbYesNoCancel を渡すと、押されたボタンが戻り値で返ってきます。それを ans で受けて分岐しているだけです。

番号を探すループでPDFとExcelの両方を見ているのには理由があります。片方だけで判定すると、PDFが _2 でExcelが _3 のようにずれて、どれとどれが同じ書き出しなのか分からなくなるからです。

Application.DisplayAlerts = False はそのまま残しています。Microsoft Learn の DisplayAlerts の説明にあるとおり、これを False にすると確認を求められる場面で既定の答えが選ばれます。上書きしていいかは自分で聞いたあとなので、Excelにもう一度聞かせない、という意味です。

逆に言うと、この行だけ残して自分の確認を消すと、同じ日の2回目で1回目の出力が黙って消えます。工程表は同じ日に2回直すことがあるので、ここは省かないでください。

出力先のフォルダは、コードの中に定数として書いていました。変えたくなったらコードを直す、という運用です。乱暴に見えますが、触るのが自分しかいなかったのでこれで足りていました。

ただ実際には「今回だけ別の場所に出したい」が起きるので、フォルダを選べる形も付けていました。

'==== 保存先をその場で選びたいとき ====
Public Function AskFolder(defaultPath As String) As String
    With Application.FileDialog(msoFileDialogFolderPicker)
        .Title = "書き出し先のフォルダを選んでください"
        .InitialFileName = defaultPath
        If .Show = -1 Then
            AskFolder = .SelectedItems(1) & "\"
        Else
            AskFolder = defaultPath   ' キャンセルなら初期値のまま
        End If
    End With
End Function

この関数は置いただけでは動きません。使うときは、さきほどの ExportGantt の中の outDir = EXPORT_DIR を、次の1行に差し替えてください。

outDir = AskFolder(EXPORT_DIR)

これでボタンを押すたびにフォルダ選択のダイアログが出て、キャンセルすれば EXPORT_DIR に出ます。

自分しか使わないなら定数で十分。誰かに渡すならダイアログを付ける。この線引きだけ先に決めておくと、あとで作り直さずに済みます。

FileDialog は初期フォルダの指定やファイル種類の絞り込みもできます。使い方はVBA FileDialogの使い方にまとめています。

なお、ここまでの土台(シート・列幅・見出しの数式・ボタン)を作るところも含めてマクロにしてあります。新しいブックにゼロから作りたい場合はまっさらなブックに貼るだけで、ここまで作るを見てください。

8年使って分かった、自動化できなかったこと

ここからが、この記事でいちばん書きたかったところです。

ボタン1つで工程表が出るようになって、作る時間は確かに消えました。でも8年やって残ったものがあります。

自動化できたのは「描く」ところだけだった

私が作ったVBA版には、進捗率がありません。何パーセント終わったかを書く欄がないし、色も変わりません。

実績を記録する機能もありません。予定どおりに終わったのか、2日延びたのか、それを残す場所を作りませんでした。イナズマ線(進んでいる・遅れているを折れ線で見せるやり方)も入っていません。

できたのは、とりあえず線を引いて「こういう順番でやるんだね」が分かるところまでです。

これは技術的にできなかったわけではありません。作らなかったんです。工事が動いているあいだは翌日の打ち合わせと資料づくりで手一杯で、実績まで手が回らない。実績は未来のために使うものなので、目先は困らない。だから後回しになって、そのまま残らない。

工程に関わっていたのは4人でしたが、実績をきちんと残していたのは1人だけでした。私が残し始めたのは去年からです。8年やって、去年からです。

正直に書きますが、この部分は道具を変えても完全には消えません。自動化できるのは手の動きであって、「終わったあとに入力する」という行為そのものではないからです。ここを「ツールを入れれば解決します」と書いている記事があったら、私は少し疑います。

幅と紙の制約は、最後まで解決しなかった

もう1つ、紙に収めることから来る問題です。

工程表を横1枚に収めようとすると、日数が多いほど1列が細くなります。細くなると、バーの中に書いた作業名が読めなくなります。読めるようにすると今度は1枚に収まりません。

対処として無理やりA3にしますが、それ自体が大変です。逆に日数が少なすぎると1列が大きすぎて、スカスカで見栄えが悪い。

期間の長さが、表の読みやすさを決めてしまう。1枚に収める前提でいるかぎり、Excelが上手くなっても消えませんでした。

紙の話も残ります。共有はデータで回していても、紙で持っておきたいという人は必ずいます。そうすると「1枚に収まっていること」が要件になります。横を1枚に収めると縦がスカスカになるので、印刷プレビューを見ながら毎回合わせていました。私は結局、基本はA4に何とか収めて、無理ならA3に諦めて全部A3用に合わせるという運用にしていました。

ちなみにExcelのシートは1,048,576行 × 16,384列まで使えます。容量が足りないわけではありません。足りないのは紙のほうです。

バージョンは増える。増えても困らない置き方にする

これは私も予想が外れたところなので、正直に書きます。

工程は変わるので、直すたびに新しい版が出ます。バージョンはどんどん増えます。ボタン1つで書き出せるようになると、なおさら出しやすくなります。

「自動化したせいで管理が破綻したのでは」と思われそうですが、そうはなりませんでした。理由は単純で、置き場所を1か所に決めていたからです。共有のサーバーに置いて、「最新はここにあります。いちばん新しい番号のものを見てください」で回していました。

版が増えること自体は、実は問題ではありませんでした。問題になるのは手元にコピーが渡ったときです。紙に印刷した瞬間、その紙は更新されません。「これ最新じゃなくないですか」というやりとりは、データではなく紙で起きていました。

ついでに書くと、その日の時間割のほうは、あってもバージョン2か3程度でした。1日分しかないので、そもそも直す回数が少ないんです。版が荒れるのは全体工程のほうだけでした。

だから、対策は「バージョンを増やさない」ではありません。

  • 置き場所を1つに決める(増えてもいいので、探す場所を分散させない)
  • 番号は日付ではなく通し番号にする(同じ日に2回直すことがあるため。書き出しのコードで _2_3 と付けていたのはこれです)
  • 紙に出したものは、その場限りと割り切る

これだけで、実務上は困らなくなりました。

ここまでが、私がExcelで実用化した範囲です。この形で4年やって、それから工程表アプリを自作しました。「Excelでこれ以上やっても、幅と紙の問題は消えないな」と思ったのが理由です。

もし同じところで詰まっているなら、まずこの記事の形まで作ってみてください。それで足りるなら、それがいちばん安上がりです。足りなかったときに初めて、Excelの外を考えれば十分だと思います。

PR・自社製品

Schedika(スケディカ)

私がその後に作ったのが、HTMLファイルをChromeやEdgeで開くだけで動くガントチャートアプリです。インストールも通信も要らないので、クラウドが使えない職場を前提にしています。

  • 向いている人:毎週・毎月ダイヤを引き直す人/会議中にその場で工程を組み替えたい人/ソフトのインストールに申請が要る職場の人
  • 向いていない人:工程表を作るのが年に数回の人(この記事の形で足ります)

案件数とタスク数の制限を外した版は Schedika Standard(買い切り2,980円・2026年8月時点)です。まずLiteで足りるかを確かめてからで大丈夫です。

Schedika Lite の配布ページを開くQRコード
スマホで開く
解決できること行のドラッグでの並べ替え、バーのドラッグでの移動と伸縮ができます。Excelでいちばん重かった「3行1セットをコピーしてサイズを合わせ直す」がなくなります。親・子・孫の3階層まで持てて、折りたたみもできます。
できないこと前の作業がずれた分だけ後ろを自動でずらす機能はありません。進捗率の自動計算もありません。実績は、この記事に書いたとおり、どんな道具を使っても自動では残りません。
動作条件HTMLファイルをChromeまたはEdgeで開くだけで動きます。インストール不要・通信不要。通常は管理者権限も要りませんが、社内のセキュリティポリシーによってはファイルの実行や保存が制限される場合があります(2026年8月時点)。
無料版(Lite)の範囲1案件・タスク100件まで。動かせない日を示すピンは20本まで。PNG出力と印刷には透かしが入ります。
無料・手動でできる代替この記事の条件付き書式とVBAで、作る手間はかなり減らせます。年に数回しか作らないなら、そちらで十分です。

よくある質問とまとめ

まっさらなブックに貼るだけで、ここまで作る

ここまでのコードは、シートの並びと列の位置が合っていることを前提にしています全体工程 という名前のシートがあって、E列からカレンダーが始まって、4行目に曜日の数式が入っていて、作業リスト という名前のテーブルがある。どれか1つでも違うと、DrawGantt は最初の行で止まります。

私自身、新しいブックで作り直すたびにこの土台作りをやり直していました。面倒なのは描く処理ではなく、描かせるまでの下ごしらえのほうです。

そこで、下ごしらえだけをするコードを最後に用意しました。新しいブックに、この記事のコードを2つの標準モジュールへ貼り、最後に SetupBook を1回実行すれば、それで使える状態になります。このコードは、1つ目の「工程表設定」モジュールの続きに貼ってください。設定の値と、最初の1回だけ動かす初期設定は、同じところにまとめたいからです。

やっているのは、足りないものを足すことだけです。

スクロールできます
見るもの無いときもうあるとき
作業リストのシートサンプルの予定つきで作る何もしない
テーブル「作業リスト」シートはあってもテーブルが無ければ、テーブルだけ作る何もしない
全体工程のシート見出し・列幅・曜日の数式・印刷設定まで作る何もしない
詳細工程のシート同じ。ドロップダウンの元になる日付の候補(AD列)も作る何もしない
4つのボタン足りないものだけ置く何もしない
B2の日付とドロップダウン入れる何もしない

あるものは見るだけで飛ばします。だから2回目以降に実行しても中身は消えませんし、待たされることもありません。途中まで手で作ってあるブックに実行しても構いません。「テーブルだけ作り忘れていた」なら、テーブルだけができます。最後に、何を作って何をそのままにしたかを一覧で出します。

クリックして全てのコードを見る
'============================================================
' ここから下は「土台づくり」。
'   まだ何も無いブックに、シート・テーブル・ボタンを作る。
'   足りないものだけを作るので、2回目以降に実行しても中身は消えない。
'   SetupBook を実行するのは最初の1回だけでよい。
'============================================================
Public Sub SetupBook()
    mMade = ""
    mKept = ""
    mWarn = ""

    ' 途中で止まっても画面の更新が止まったままにならないように、
    ' エラーの行き先を先に決めてから ScreenUpdating を切る
    On Error GoTo TROUBLE
    Application.ScreenUpdating = False

    EnsureList
    EnsureGantt
    EnsureDaily

    Application.ScreenUpdating = True

    ' 何も作らなかったなら、描き直す必要もない。ここで終われば一瞬で済む
    If Len(mMade) = 0 Then
        MsgBox "そろっていました。何も変更していません。" & vbCrLf & vbCrLf & _
               "もとからあったもの" & vbCrLf & mKept & Warnings, _
               vbInformation, "工程表のセットアップ"
        Exit Sub
    End If

    ' 土台ができたので、そのまま1回描いて完成形を見せる
    DrawGantt
    DrawDaily

    ThisWorkbook.Worksheets(SHEET_NAME).Activate
    MsgBox "できました。" & vbCrLf & vbCrLf & _
           "作ったもの" & vbCrLf & mMade & _
           IIf(Len(mKept) > 0, vbCrLf & "もとからあったもの" & vbCrLf & mKept, "") & _
           Warnings & vbCrLf & _
           "このあと、必ず .xlsm(Excel マクロ有効ブック)で保存してください。" & vbCrLf & _
           ".xlsx で保存すると、貼り付けたコードが消えます。", _
           vbInformation, "工程表のセットアップ"
    Exit Sub

TROUBLE:
    Application.ScreenUpdating = True
    MsgBox "途中で止まりました。" & vbCrLf & vbCrLf & _
           "エラー " & Err.Number & ":" & Err.Description, _
           vbCritical, "工程表のセットアップ"
End Sub

'============================================================
' 作業リスト。シートとテーブルを別々に見る
'============================================================
Private Sub EnsureList()
    Dim ws As Worksheet
    Dim isNew As Boolean

    Set ws = EnsureSheet(LIST_SHEET, isNew)

    If isNew Then
        BuildList ws
        NoteMade LIST_SHEET & " シート(サンプルの予定つき)"
    Else
        NoteKept LIST_SHEET & " シート"
    End If

    ' シートがあってもテーブルが無ければ、テーブルだけ作る。
    ' 「工程表マクロ」は列を名前で探すので、テーブルでないと動かない
    If TableExists(TABLE_NAME) Then
        If Not isNew Then NoteKept "テーブル「" & TABLE_NAME & "」"
    ElseIf ws.Range("A1").Value = "作業内容" Then
        MakeTable ws
        NoteMade "テーブル「" & TABLE_NAME & "」(もとの表をテーブルにしました)"
    ElseIf IsEmptySheet(ws) Then
        BuildList ws
        MakeTable ws
        NoteMade LIST_SHEET & " の中身とテーブル「" & TABLE_NAME & "」"
    Else
        ' 人が作った別の表が入っている。上書きするとその人の予定が消えるので触らない
        NoteWarn LIST_SHEET & " のA1が「作業内容」ではありません。" & vbCrLf & _
                 "  見出しを 作業内容/開始日/開始時間/終了日/終了時間/備考 に直してから、" & vbCrLf & _
                 "  もう一度実行してください。"
    End If
End Sub

'============================================================
' 全体工程
'============================================================
Private Sub EnsureGantt()
    Dim ws As Worksheet
    Dim isNew As Boolean
    Dim lastCol As Long

    Set ws = EnsureSheet(SHEET_NAME, isNew)
    lastCol = FIRST_COL + DAY_COUNT - 1

    If isNew Then
        BuildGantt ws
        NoteMade SHEET_NAME & " シート"
    Else
        NoteKept SHEET_NAME & " シート"

        ' シートはあるけれど、動くのに要るものが欠けている場合だけ足す。
        ' 見た目(色・列幅)は直さない。手で整えた人のものを壊さないため
        If Not IsDate(ws.Range("B2").Value) Then
            ws.Range("B2").Value = SampleStart
            NoteMade SHEET_NAME & " の表示開始日(B2)"
        End If

        If Len(ws.Cells(WEEK_ROW, FIRST_COL).Formula) = 0 Then
            WeekdayRow ws, lastCol
            NoteMade SHEET_NAME & " の曜日の行(4行目の数式)"
        End If

        If Not HasValidation(ws.Range("B2")) Then
            DateDropdown ws.Range("B2"), _
                "=" & LIST_SHEET & "!$G$2:$G$" & (PICK_WEEKS + 1)
            NoteMade SHEET_NAME & " のB2のドロップダウン"
        End If
    End If

    EnsureButton ws, "btnDrawGantt", "工程表を描く", "DrawGantt", 290, 5
    EnsureButton ws, "btnExportGantt", "PDFとExcelで出す", "ExportGantt", 422, 5
End Sub

'============================================================
' 詳細工程
'============================================================
Private Sub EnsureDaily()
    Dim ws As Worksheet
    Dim isNew As Boolean

    Set ws = EnsureSheet(DAILY_SHEET, isNew)

    If isNew Then
        BuildDaily ws
        NoteMade DAILY_SHEET & " シート"
    Else
        NoteKept DAILY_SHEET & " シート"

        If Len(ws.Range("AD2").Formula) = 0 And Not IsDate(ws.Range("AD2").Value) Then
            DateCandidates ws
            NoteMade DAILY_SHEET & " の日付の候補(AD列)"
        End If

        If Not IsDate(ws.Range("B2").Value) Then
            ws.Range("B2").Value = SampleStart + 7
            NoteMade DAILY_SHEET & " の表示する日(B2)"
        End If

        If Not HasValidation(ws.Range("B2")) Then
            DateDropdown ws.Range("B2"), "=$AD$2:$AD$" & (PICK_DAYS + 1)
            NoteMade DAILY_SHEET & " のB2のドロップダウン"
        End If
    End If

    EnsureButton ws, "btnDrawDaily", "その日の工程を描く", "DrawDaily", 290, 5
    EnsureButton ws, "btnExportDaily", "PDFとExcelで出す", "ExportDaily", 422, 5
End Sub

'============================================================
' 中身を作る。ここはシートを新しく作ったときだけ通る
'============================================================
Private Sub BuildList(ws As Worksheet)
    Dim jobs As Variant
    Dim r As Variant
    Dim n As Long

    ' 見出し。この6つの名前で「工程表マクロ」が列を探すので、変えるなら両方直す
    ws.Range("A1").Value = "作業内容"
    ws.Range("B1").Value = "開始日"
    ws.Range("C1").Value = "開始時間"
    ws.Range("D1").Value = "終了日"
    ws.Range("E1").Value = "終了時間"
    ws.Range("F1").Value = "備考"

    ' サンプルの作業。日付は「今月1日から何日目か」で入れているので、
    ' いつ実行しても今月の表になる。中身は自分の予定に書き換える前提
    '           作業内容,      開始+日, 開始時, 終了+日, 終了時, 備考
    jobs = Array( _
        Array("事前準備", 0, 8, 3, 17, "手配と段取り"), _
        Array("資材手配", 2, 9, 8, 15, "納期は要確認"), _
        Array("足場組立", 6, 8, 7, 12, ""), _
        Array("現地作業A", 7, 6, 15, 18, "定時前から開始"), _
        Array("仮設電源の敷設", 7, 8, 7, 17, "当日で完了"), _
        Array("立会い確認", 7, 13, 7, 15, "社外の方が同席"), _
        Array("現地作業B", 13, 8, 23, 17, "Aと3日重なる"), _
        Array("検査", 24, 9, 28, 12, "立会いあり"), _
        Array("片付け・報告", 29, 8, 32, 15, ""))

    For n = 0 To UBound(jobs)
        r = jobs(n)
        ws.Cells(n + 2, 1).Value = r(0)
        ws.Cells(n + 2, 2).Value = SampleStart + r(1)
        ws.Cells(n + 2, 3).Value = TimeSerial(r(2), 0, 0)
        ws.Cells(n + 2, 4).Value = SampleStart + r(3)
        ws.Cells(n + 2, 5).Value = TimeSerial(r(4), 0, 0)
        ws.Cells(n + 2, 6).Value = r(5)
    Next n

    ws.Columns("A").ColumnWidth = 14.8
    ws.Columns("B").ColumnWidth = 11.8
    ws.Columns("C").ColumnWidth = 9.8
    ws.Columns("D").ColumnWidth = 11.8
    ws.Columns("E").ColumnWidth = 9.8
    ws.Columns("F").ColumnWidth = 16.8
    ws.Columns("G").ColumnWidth = 15

    ws.Range("B2:B" & (JOB_COUNT + 1)).NumberFormatLocal = "yyyy/m/d"
    ws.Range("D2:D" & (JOB_COUNT + 1)).NumberFormatLocal = "yyyy/m/d"
    ws.Range("C2:C" & (JOB_COUNT + 1)).NumberFormatLocal = "h:mm"
    ws.Range("E2:E" & (JOB_COUNT + 1)).NumberFormatLocal = "h:mm"
    ws.Range("B2:E" & (JOB_COUNT + 1)).HorizontalAlignment = xlCenter

    HeaderStyle ws.Range("A1:F1")
    With ws.Range("A2:F" & (JOB_COUNT + 1)).Font
        .Size = 9
        .Color = RGB(31, 42, 55)
    End With
    ws.Range("F2:F" & (JOB_COUNT + 1)).Font.Color = RGB(100, 116, 139)

    ' 全体工程のドロップダウンに出す候補。1週間ずつずらした日付を並べておく
    With ws.Range("G1")
        .Value = "表示開始日の候補"
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(100, 116, 139)
    End With
    ws.Range("G2").Value = SampleStart
    ws.Range("G3:G" & (PICK_WEEKS + 1)).FormulaR1C1 = "=R[-1]C+7"
    With ws.Range("G2:G" & (PICK_WEEKS + 1))
        .NumberFormatLocal = "yyyy/m/d"
        .HorizontalAlignment = xlCenter
        .Font.Size = 9
    End With

    HideGridlines ws
End Sub

'==== 表をテーブルにする。
'     G列(日付の候補)を書いたあとに呼ぶこと。先にテーブルを作ってから
'     隣のG列に書き込むと、テーブルがG列を巻き込んで7列に広がる ====
Private Sub MakeTable(ws As Worksheet)
    Dim lo As ListObject
    Dim lastRow As Long

    ' 最終行は作業内容の列から数える
    lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
    If lastRow < 2 Then lastRow = 2

    Set lo = ws.ListObjects.Add(xlSrcRange, ws.Range("A1:F" & lastRow), , xlYes)
    lo.Name = TABLE_NAME
    lo.TableStyle = "TableStyleLight1"
End Sub

Private Sub BuildGantt(ws As Worksheet)
    Dim lastCol As Long

    lastCol = FIRST_COL + DAY_COUNT - 1

    ' 列幅の数字は「標準フォントで何文字ぶんか」なので、
    ' Excelの標準フォントが違う人の画面では見た目の幅も変わる。
    ' 日付は2桁しか出さないので、3文字ぶんあれば足りる
    ws.Columns("A").ColumnWidth = 5
    ws.Columns("B").ColumnWidth = 20
    ws.Columns("C:D").ColumnWidth = 2.44
    ws.Range(ws.Columns(FIRST_COL), ws.Columns(lastCol)).ColumnWidth = 3.22

    ws.Rows(1).RowHeight = 16.2
    ws.Rows(MONTH_ROW).RowHeight = 14.4
    ws.Rows(WEEK_ROW).RowHeight = 15

    With ws.Range("B1")
        .Value = "VBA版|表示開始日を選んで[工程表を描く]"
        .Font.Bold = True
        .Font.Size = 12
        .Font.Color = RGB(22, 50, 79)
    End With

    With ws.Range("A2")
        .Value = "開始"
        .Font.Size = 8
        .Font.Color = RGB(100, 116, 139)
        .HorizontalAlignment = xlCenter
    End With

    ' 表示開始日。ここを変えて[工程表を描く]を押すと、表が丸ごとずれる
    With ws.Range("B2")
        .Value = SampleStart
        .NumberFormatLocal = "yyyy/m/d(aaa)"
        .Font.Bold = True
        .Font.Size = 10
        .Font.Color = RGB(22, 50, 79)
        .Interior.Color = RGB(233, 162, 59)
        .HorizontalAlignment = xlCenter
    End With
    DateDropdown ws.Range("B2"), "=" & LIST_SHEET & "!$G$2:$G$" & (PICK_WEEKS + 1)

    ' 見出しの帯。色はここで付けておく。
    ' DrawGantt は3行目の「値だけ」を消すので、この色は描き直しても残る
    ws.Range("B3").Value = "作業内容"
    HeaderStyle ws.Range("B3:B4")
    HeaderStyle ws.Range(ws.Cells(HEAD_ROW, FIRST_COL), ws.Cells(HEAD_ROW, lastCol))
    ws.Range(ws.Cells(HEAD_ROW, FIRST_COL), _
             ws.Cells(HEAD_ROW, lastCol)).NumberFormatLocal = "d"

    ' 月のラベルの行。左詰めにしておかないと、月名が右隣にはみ出して表示できない
    With ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), ws.Cells(MONTH_ROW, lastCol))
        .NumberFormat = "@"
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(22, 50, 79)
        .HorizontalAlignment = xlLeft
    End With

    WeekdayRow ws, lastCol

    ' 印刷。横幅を1枚に収め、縦は何枚になってもよい設定。
    ' Zoom = False にしないと FitToPagesWide が効かない(倍率指定が優先される)。
    ' ただし60日ぶんを1枚に押し込むと字が小さくなる。実際には表示日数を減らすか、
    ' 期間を区切って刷ることになる
    With ws.PageSetup
        .Orientation = xlLandscape
        .PaperSize = xlPaperA4
        .Zoom = False
        .FitToPagesWide = 1
        .FitToPagesTall = False
        .PrintTitleColumns = "$A:$D"
    End With

    FreezeAt ws, "E5"
    HideGridlines ws
End Sub

Private Sub BuildDaily(ws As Worksheet)
    Dim lastCol As Long

    lastCol = FIRST_COL + HOUR_COUNT - 1

    ws.Columns("A").ColumnWidth = 2.33
    ws.Columns("B").ColumnWidth = 16.8
    ws.Columns("C:D").ColumnWidth = 6.8
    ws.Range(ws.Columns(FIRST_COL), ws.Columns(lastCol)).ColumnWidth = 4.22

    ws.Rows(1).RowHeight = 25.95
    ws.Rows(MONTH_ROW).RowHeight = 15
    ws.Rows(HEAD_ROW).RowHeight = 15
    ws.Rows(WEEK_ROW).RowHeight = 7.95

    With ws.Range("B1")
        .Value = "詳細工程|日付を選んで[その日の工程を描く]"
        .Font.Bold = True
        .Font.Size = 12
        .Font.Color = RGB(22, 50, 79)
    End With

    DateCandidates ws

    ' 表示する日。最初は1週間後にしておく(サンプルの作業が重なっている日)
    With ws.Range("B2")
        .Value = SampleStart + 7
        .NumberFormatLocal = "yyyy/m/d(aaa)"
        .Font.Bold = True
        .Font.Size = 9
        .Interior.Color = RGB(245, 180, 76)
        .HorizontalAlignment = xlCenter
    End With
    DateDropdown ws.Range("B2"), "=$AD$2:$AD$" & (PICK_DAYS + 1)

    ws.Range("B3").Value = "作業内容"
    ws.Range("C3").Value = "開始"
    ws.Range("D3").Value = "終了"
    HeaderStyle ws.Range(ws.Cells(HEAD_ROW, 2), ws.Cells(HEAD_ROW, lastCol))

    ' こちらは24時間ぶんしかないので、幅を1枚に合わせても字がつぶれない
    With ws.PageSetup
        .Orientation = xlLandscape
        .PaperSize = xlPaperA4
        .Zoom = False
        .FitToPagesWide = 1
        .FitToPagesTall = False
        .PrintTitleColumns = "$A:$D"
    End With

    HideGridlines ws
End Sub

'==== ドロップダウンの中身。使う人には見せないのでAD列は隠す ====
Private Sub DateCandidates(ws As Worksheet)
    With ws.Range("AD1")
        .Value = "日付の候補"
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(100, 116, 139)
    End With
    ws.Columns("AD").ColumnWidth = 12.8
    ws.Range("AD2").Value = SampleStart
    ws.Range("AD3:AD" & (PICK_DAYS + 1)).FormulaR1C1 = "=R[-1]C+1"
    ws.Range("AD2:AD" & (PICK_DAYS + 1)).NumberFormatLocal = "yyyy/m/d"
    ws.Columns("AD").Hidden = True
End Sub

'============================================================
' ここから下は道具
'============================================================

'==== サンプルの起点。今月の1日。
'     DrawGantt の中の baseDate と紛らわしいので別の名前にしている ====
Private Function SampleStart() As Date
    SampleStart = DateSerial(Year(Date), Month(Date), 1)
End Function

'==== シートを返す。無ければ足して、足したことを isNew で知らせる ====
Private Function EnsureSheet(sheetName As String, isNew As Boolean) As Worksheet
    Dim ws As Worksheet

    isNew = False
    For Each ws In ThisWorkbook.Worksheets
        If ws.Name = sheetName Then
            Set EnsureSheet = ws
            Exit Function
        End If
    Next ws

    Set ws = ThisWorkbook.Worksheets.Add( _
             After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
    ws.Name = sheetName
    isNew = True
    Set EnsureSheet = ws
End Function

'==== その名前のテーブルがブックのどこかにあるか ====
Private Function TableExists(tblName As String) As Boolean
    Dim ws As Worksheet
    Dim lo As ListObject
    For Each ws In ThisWorkbook.Worksheets
        For Each lo In ws.ListObjects
            If lo.Name = tblName Then
                TableExists = True
                Exit Function
            End If
        Next lo
    Next ws
End Function

'==== その名前のボタンがそのシートにあるか ====
Private Function ShapeExists(ws As Worksheet, shapeName As String) As Boolean
    Dim k As Long
    For k = 1 To ws.Shapes.Count
        If ws.Shapes(k).Name = shapeName Then
            ShapeExists = True
            Exit Function
        End If
    Next k
End Function

'==== 入力規則が入っているか。
'     入っていないセルの .Validation.Type は
'     値を返さずエラーになるので、エラーが出るかどうかで見る ====
Private Function HasValidation(c As Range) As Boolean
    Dim t As Long
    On Error Resume Next
    t = c.Validation.Type
    HasValidation = (Err.Number = 0)
    Err.Clear
    On Error GoTo 0
End Function

'==== 何も入っていないシートか ====
Private Function IsEmptySheet(ws As Worksheet) As Boolean
    IsEmptySheet = (Application.WorksheetFunction.CountA(ws.Cells) = 0)
End Function

'==== 日付のドロップダウンを付ける ====
Private Sub DateDropdown(c As Range, listFormula As String)
    c.Validation.Delete
    c.Validation.Add Type:=xlValidateList, AlertStyle:=xlValidAlertStop, _
                     Formula1:=listFormula
End Sub

'==== 曜日の行。真上の日付を見るだけの数式を並べる。
'     これがあるので、VBA側で曜日を書かなくて済む ====
Private Sub WeekdayRow(ws As Worksheet, lastCol As Long)
    With ws.Range(ws.Cells(WEEK_ROW, FIRST_COL), ws.Cells(WEEK_ROW, lastCol))
        .FormulaR1C1 = "=R[-1]C"
        .NumberFormatLocal = "aaa"
        .Font.Size = 8
        .Font.Color = RGB(100, 116, 139)
        .HorizontalAlignment = xlCenter
    End With
End Sub

'==== 見出しの帯(濃い紺に白抜き)====
Private Sub HeaderStyle(target As Range)
    With target
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(255, 255, 255)
        .Interior.Color = RGB(22, 50, 79)
        .HorizontalAlignment = xlCenter
    End With
End Sub

'==== ボタンを1つ置く。もうあるなら何もしない ====
Private Sub EnsureButton(ws As Worksheet, btnName As String, btnCaption As String, _
                         macroName As String, L As Double, T As Double)
    Dim b As Object

    If ShapeExists(ws, btnName) Then
        NoteKept ws.Name & " の[" & btnCaption & "]ボタン"
        Exit Sub
    End If

    Set b = ws.Buttons.Add(L, T, 126, 24)
    b.Name = btnName
    b.Caption = btnCaption
    b.OnAction = macroName
    NoteMade ws.Name & " の[" & btnCaption & "]ボタン"
End Sub

'==== 見出しを固定する。画面の設定なので、ここだけシートを表に出す必要がある ====
Private Sub FreezeAt(ws As Worksheet, cellAddress As String)
    ws.Activate
    ActiveWindow.FreezePanes = False
    ws.Range(cellAddress).Select
    ActiveWindow.FreezePanes = True
End Sub

'==== 目盛線を消す。これも画面の設定 ====
Private Sub HideGridlines(ws As Worksheet)
    ws.Activate
    ActiveWindow.DisplayGridlines = False
End Sub

'==== 何をしたかを覚えておく ====
Private Sub NoteMade(msg As String)
    mMade = mMade & "・" & msg & vbCrLf
End Sub

Private Sub NoteKept(msg As String)
    mKept = mKept & "・" & msg & vbCrLf
End Sub

Private Sub NoteWarn(msg As String)
    mWarn = mWarn & "・" & msg & vbCrLf
End Sub

Private Function Warnings() As String
    If Len(mWarn) = 0 Then Exit Function
    Warnings = vbCrLf & "こちらでは決められなかったもの" & vbCrLf & mWarn
End Function
使い方
  1. 新しいブックを作る
  2. Alt + F11 でVBAの画面を開く
  3. 挿入 → 標準モジュールを2つ作る
    標準モジュールの記載方法
  4. 1つ目のモジュール名を「工程表設定」にして、下の完成版①を貼る
  5. 2つ目のモジュール名を「工程表マクロ」にして、下の完成版②を貼る
  6. SetupBook の中のどこでもいいのでカーソルを置いて F5
  7. .xlsm(Excel マクロ有効ブック)で保存する

7番だけ間違えやすいので気を付けてください。.xlsx で保存すると、貼り付けたコードが消えます。

3か所だけ、そう書いた理由を残しておきます。

  • JOB_COUNTPICK_WEEKSPICK_DAYS はモジュールのいちばん上にある … この土台づくりだけが使う値ですが、VBAはモジュール全体で使う宣言を先頭に置く決まりなので、ここには書けません。mMade などの3つの変数も同じ理由で上にあります
  • 入力規則があるかどうかを、エラーが出るかどうかで見ている … 入力規則が入っていないセルの Validation.Type は、値を返さずにエラーになります。「入っていますか」と素直に聞ける書き方が無いので、On Error Resume Next で受けて判定しています
  • テーブルを作るのは、G列を書いたあと … 先にテーブルを作ってから隣のG列に書き込むと、テーブルがG列を巻き込んで7列に広がります。順番を逆にするだけで防げます

なお列幅の数字は「標準フォントで何文字ぶんか」という意味なので、Excelの標準フォントが違う人の画面では見た目の幅も少し変わります。日付は2桁しか出さないので、3文字ぶんあれば足ります。

貼り付け用の完成版

ここまで①〜⑥に分けて出したコードを、モジュールごとに1つへまとめました。貼るのは、この2つだけで大丈夫です。上で分けて出したコードも一緒に貼ると、同じ処理が2つになって「名前が適切ではありません」というエラーになります。

完成版① 工程表設定(1つ目のモジュールに貼る)
Option Explicit
'============================================================
' 工程表設定
'
'   最初に一度だけ触るところです。
'
'   ① 下の設定を、自分の表に合わせる(変えなくても動きます)
'   ② SetupBook を1回実行すると、シートとボタンができあがります
'
'   もう1つの「工程表マクロ」は、描くところと書き出すところだけです。
'   開かなくて構いません。
'============================================================
'---- ① シートとテーブルの名前 ----
Public Const SHEET_NAME    As String = "全体工程"   ' ガントチャートを描くシート
Public Const DAILY_SHEET   As String = "詳細工程"   ' その日の時間割を描くシート
Public Const LIST_SHEET    As String = "作業リスト" ' 作業リストのテーブルがあるシート
Public Const TABLE_NAME    As String = "作業リスト" ' テーブルの名前
'---- ② 表のどこに何があるか ----
Public Const START_CELL    As String = "B2"  ' 全体工程で、表示開始日を選ぶセル
Public Const DAILY_CELL    As String = "B2"  ' 詳細工程で、表示する日を選ぶセル
Public Const MONTH_ROW     As Long = 2       ' 月のラベルを出す行
Public Const HEAD_ROW      As Long = 3       ' 日付を並べる行
Public Const WEEK_ROW      As Long = 4       ' 曜日の行(=E3 の数式が入っている)
Public Const FIRST_ROW     As Long = 5       ' 1件目のタイトル行
Public Const FIRST_COL     As Long = 5       ' E列からカレンダーを描く
'---- ③ どれだけ出すか ----
Public Const DAY_COUNT     As Long = 60      ' 全体工程を横に何日ぶん出すか
Public Const HOUR_COUNT    As Long = 24      ' 詳細工程を0時から何時間ぶん出すか
'---- ④ 見た目(1作業ぶんの高さ)----
Public Const ROWS_PER_JOB  As Long = 3       ' タイトル行+バー行+隙間行
Public Const H_TITLE       As Double = 17    ' タイトル行の高さ
Public Const H_BAR         As Double = 13    ' バーの行の高さ(細いほど締まって見える)
Public Const H_GAP         As Double = 5     ' すきまの行の高さ
'---- ⑤ 勤務時間(詳細工程で、人がいない時間を塗るため)----
Public Const WORK_START    As Long = 8       ' 日勤の始まり
Public Const WORK_END      As Long = 17      ' 日勤の終わり
Public Const LUNCH_HOUR    As Long = 12      ' 昼休み
'---- ⑥ 書き出し先。無ければ作ります ----
Public Const EXPORT_DIR    As String = "C:\Users\Public\工程表\"
'---- ⑦ 最初に入れておくサンプルの分量(土台づくりだけが使う)----
Public Const JOB_COUNT     As Long = 9     ' サンプルの作業の数
Public Const PICK_WEEKS    As Long = 9     ' 全体工程のドロップダウンに出す週の数
Public Const PICK_DAYS     As Long = 33    ' 詳細工程のドロップダウンに出す日数
'---- 土台づくりが「何をしたか」を覚えておく箱。あとでまとめて報告する ----
Private mMade As String   ' 作ったもの
Private mKept As String   ' もとからあったので触らなかったもの
Private mWarn As String   ' こちらでは判断できなかったもの
'============================================================
' ここから下は「土台づくり」。
'   まだ何も無いブックに、シート・テーブル・ボタンを作る。
'   足りないものだけを作るので、2回目以降に実行しても中身は消えない。
'   SetupBook を実行するのは最初の1回だけでよい。
'============================================================
Public Sub SetupBook()
    mMade = ""
    mKept = ""
    mWarn = ""
    ' 途中で止まっても画面の更新が止まったままにならないように、
    ' エラーの行き先を先に決めてから ScreenUpdating を切る
    On Error GoTo TROUBLE
    Application.ScreenUpdating = False
    EnsureList
    EnsureGantt
    EnsureDaily
    Application.ScreenUpdating = True
    ' 何も作らなかったなら、描き直す必要もない。ここで終われば一瞬で済む
    If Len(mMade) = 0 Then
        MsgBox "そろっていました。何も変更していません。" & vbCrLf & vbCrLf & _
               "もとからあったもの" & vbCrLf & mKept & Warnings, _
               vbInformation, "工程表のセットアップ"
        Exit Sub
    End If
    ' 土台ができたので、そのまま1回描いて完成形を見せる
    DrawGantt
    DrawDaily
    ThisWorkbook.Worksheets(SHEET_NAME).Activate
    MsgBox "できました。" & vbCrLf & vbCrLf & _
           "作ったもの" & vbCrLf & mMade & _
           IIf(Len(mKept) > 0, vbCrLf & "もとからあったもの" & vbCrLf & mKept, "") & _
           Warnings & vbCrLf & _
           "このあと、必ず .xlsm(Excel マクロ有効ブック)で保存してください。" & vbCrLf & _
           ".xlsx で保存すると、貼り付けたコードが消えます。", _
           vbInformation, "工程表のセットアップ"
    Exit Sub
TROUBLE:
    Application.ScreenUpdating = True
    MsgBox "途中で止まりました。" & vbCrLf & vbCrLf & _
           "エラー " & Err.Number & ":" & Err.Description, _
           vbCritical, "工程表のセットアップ"
End Sub
'============================================================
' 作業リスト。シートとテーブルを別々に見る
'============================================================
Private Sub EnsureList()
    Dim ws As Worksheet
    Dim isNew As Boolean
    Set ws = EnsureSheet(LIST_SHEET, isNew)
    If isNew Then
        BuildList ws
        NoteMade LIST_SHEET & " シート(サンプルの予定つき)"
    Else
        NoteKept LIST_SHEET & " シート"
    End If
    ' シートがあってもテーブルが無ければ、テーブルだけ作る。
    ' 「工程表マクロ」は列を名前で探すので、テーブルでないと動かない
    If TableExists(TABLE_NAME) Then
        If Not isNew Then NoteKept "テーブル「" & TABLE_NAME & "」"
    ElseIf ws.Range("A1").Value = "作業内容" Then
        MakeTable ws
        NoteMade "テーブル「" & TABLE_NAME & "」(もとの表をテーブルにしました)"
    ElseIf IsEmptySheet(ws) Then
        BuildList ws
        MakeTable ws
        NoteMade LIST_SHEET & " の中身とテーブル「" & TABLE_NAME & "」"
    Else
        ' 人が作った別の表が入っている。上書きするとその人の予定が消えるので触らない
        NoteWarn LIST_SHEET & " のA1が「作業内容」ではありません。" & vbCrLf & _
                 "  見出しを 作業内容/開始日/開始時間/終了日/終了時間/備考 に直してから、" & vbCrLf & _
                 "  もう一度実行してください。"
    End If
End Sub
'============================================================
' 全体工程
'============================================================
Private Sub EnsureGantt()
    Dim ws As Worksheet
    Dim isNew As Boolean
    Dim lastCol As Long
    Set ws = EnsureSheet(SHEET_NAME, isNew)
    lastCol = FIRST_COL + DAY_COUNT - 1
    If isNew Then
        BuildGantt ws
        NoteMade SHEET_NAME & " シート"
    Else
        NoteKept SHEET_NAME & " シート"
        ' シートはあるけれど、動くのに要るものが欠けている場合だけ足す。
        ' 見た目(色・列幅)は直さない。手で整えた人のものを壊さないため
        If Not IsDate(ws.Range("B2").Value) Then
            ws.Range("B2").Value = SampleStart
            NoteMade SHEET_NAME & " の表示開始日(B2)"
        End If
        If Len(ws.Cells(WEEK_ROW, FIRST_COL).Formula) = 0 Then
            WeekdayRow ws, lastCol
            NoteMade SHEET_NAME & " の曜日の行(4行目の数式)"
        End If
        If Not HasValidation(ws.Range("B2")) Then
            DateDropdown ws.Range("B2"), _
                "=" & LIST_SHEET & "!$G$2:$G$" & (PICK_WEEKS + 1)
            NoteMade SHEET_NAME & " のB2のドロップダウン"
        End If
    End If
    EnsureButton ws, "btnDrawGantt", "工程表を描く", "DrawGantt", 290, 5
    EnsureButton ws, "btnExportGantt", "PDFとExcelで出す", "ExportGantt", 422, 5
End Sub
'============================================================
' 詳細工程
'============================================================
Private Sub EnsureDaily()
    Dim ws As Worksheet
    Dim isNew As Boolean
    Set ws = EnsureSheet(DAILY_SHEET, isNew)
    If isNew Then
        BuildDaily ws
        NoteMade DAILY_SHEET & " シート"
    Else
        NoteKept DAILY_SHEET & " シート"
        If Len(ws.Range("AD2").Formula) = 0 And Not IsDate(ws.Range("AD2").Value) Then
            DateCandidates ws
            NoteMade DAILY_SHEET & " の日付の候補(AD列)"
        End If
        If Not IsDate(ws.Range("B2").Value) Then
            ws.Range("B2").Value = SampleStart + 7
            NoteMade DAILY_SHEET & " の表示する日(B2)"
        End If
        If Not HasValidation(ws.Range("B2")) Then
            DateDropdown ws.Range("B2"), "=$AD$2:$AD$" & (PICK_DAYS + 1)
            NoteMade DAILY_SHEET & " のB2のドロップダウン"
        End If
    End If
    EnsureButton ws, "btnDrawDaily", "その日の工程を描く", "DrawDaily", 290, 5
    EnsureButton ws, "btnExportDaily", "PDFとExcelで出す", "ExportDaily", 422, 5
End Sub
'============================================================
' 中身を作る。ここはシートを新しく作ったときだけ通る
'============================================================
Private Sub BuildList(ws As Worksheet)
    Dim jobs As Variant
    Dim r As Variant
    Dim n As Long
    ' 見出し。この6つの名前で「工程表マクロ」が列を探すので、変えるなら両方直す
    ws.Range("A1").Value = "作業内容"
    ws.Range("B1").Value = "開始日"
    ws.Range("C1").Value = "開始時間"
    ws.Range("D1").Value = "終了日"
    ws.Range("E1").Value = "終了時間"
    ws.Range("F1").Value = "備考"
    ' サンプルの作業。日付は「今月1日から何日目か」で入れているので、
    ' いつ実行しても今月の表になる。中身は自分の予定に書き換える前提
    '           作業内容,      開始+日, 開始時, 終了+日, 終了時, 備考
    jobs = Array( _
        Array("事前準備", 0, 8, 3, 17, "手配と段取り"), _
        Array("資材手配", 2, 9, 8, 15, "納期は要確認"), _
        Array("足場組立", 6, 8, 7, 12, ""), _
        Array("現地作業A", 7, 6, 15, 18, "定時前から開始"), _
        Array("仮設電源の敷設", 7, 8, 7, 17, "当日で完了"), _
        Array("立会い確認", 7, 13, 7, 15, "社外の方が同席"), _
        Array("現地作業B", 13, 8, 23, 17, "Aと3日重なる"), _
        Array("検査", 24, 9, 28, 12, "立会いあり"), _
        Array("片付け・報告", 29, 8, 32, 15, ""))
    For n = 0 To UBound(jobs)
        r = jobs(n)
        ws.Cells(n + 2, 1).Value = r(0)
        ws.Cells(n + 2, 2).Value = SampleStart + r(1)
        ws.Cells(n + 2, 3).Value = TimeSerial(r(2), 0, 0)
        ws.Cells(n + 2, 4).Value = SampleStart + r(3)
        ws.Cells(n + 2, 5).Value = TimeSerial(r(4), 0, 0)
        ws.Cells(n + 2, 6).Value = r(5)
    Next n
    ws.Columns("A").ColumnWidth = 14.8
    ws.Columns("B").ColumnWidth = 11.8
    ws.Columns("C").ColumnWidth = 9.8
    ws.Columns("D").ColumnWidth = 11.8
    ws.Columns("E").ColumnWidth = 9.8
    ws.Columns("F").ColumnWidth = 16.8
    ws.Columns("G").ColumnWidth = 15
    ws.Range("B2:B" & (JOB_COUNT + 1)).NumberFormatLocal = "yyyy/m/d"
    ws.Range("D2:D" & (JOB_COUNT + 1)).NumberFormatLocal = "yyyy/m/d"
    ws.Range("C2:C" & (JOB_COUNT + 1)).NumberFormatLocal = "h:mm"
    ws.Range("E2:E" & (JOB_COUNT + 1)).NumberFormatLocal = "h:mm"
    ws.Range("B2:E" & (JOB_COUNT + 1)).HorizontalAlignment = xlCenter
    HeaderStyle ws.Range("A1:F1")
    With ws.Range("A2:F" & (JOB_COUNT + 1)).Font
        .Size = 9
        .Color = RGB(31, 42, 55)
    End With
    ws.Range("F2:F" & (JOB_COUNT + 1)).Font.Color = RGB(100, 116, 139)
    ' 全体工程のドロップダウンに出す候補。1週間ずつずらした日付を並べておく
    With ws.Range("G1")
        .Value = "表示開始日の候補"
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(100, 116, 139)
    End With
    ws.Range("G2").Value = SampleStart
    ws.Range("G3:G" & (PICK_WEEKS + 1)).FormulaR1C1 = "=R[-1]C+7"
    With ws.Range("G2:G" & (PICK_WEEKS + 1))
        .NumberFormatLocal = "yyyy/m/d"
        .HorizontalAlignment = xlCenter
        .Font.Size = 9
    End With
    HideGridlines ws
End Sub
'==== 表をテーブルにする。
'     G列(日付の候補)を書いたあとに呼ぶこと。先にテーブルを作ってから
'     隣のG列に書き込むと、テーブルがG列を巻き込んで7列に広がる ====
Private Sub MakeTable(ws As Worksheet)
    Dim lo As ListObject
    Dim lastRow As Long
    ' 最終行は作業内容の列から数える
    lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row
    If lastRow < 2 Then lastRow = 2
    Set lo = ws.ListObjects.Add(xlSrcRange, ws.Range("A1:F" & lastRow), , xlYes)
    lo.Name = TABLE_NAME
    lo.TableStyle = "TableStyleLight1"
End Sub
Private Sub BuildGantt(ws As Worksheet)
    Dim lastCol As Long
    lastCol = FIRST_COL + DAY_COUNT - 1
    ' 列幅の数字は「標準フォントで何文字ぶんか」なので、
    ' Excelの標準フォントが違う人の画面では見た目の幅も変わる。
    ' 日付は2桁しか出さないので、3文字ぶんあれば足りる
    ws.Columns("A").ColumnWidth = 5
    ws.Columns("B").ColumnWidth = 20
    ws.Columns("C:D").ColumnWidth = 2.44
    ws.Range(ws.Columns(FIRST_COL), ws.Columns(lastCol)).ColumnWidth = 3.22
    ws.Rows(1).RowHeight = 16.2
    ws.Rows(MONTH_ROW).RowHeight = 14.4
    ws.Rows(WEEK_ROW).RowHeight = 15
    With ws.Range("B1")
        .Value = "VBA版|表示開始日を選んで[工程表を描く]"
        .Font.Bold = True
        .Font.Size = 12
        .Font.Color = RGB(22, 50, 79)
    End With
    With ws.Range("A2")
        .Value = "開始"
        .Font.Size = 8
        .Font.Color = RGB(100, 116, 139)
        .HorizontalAlignment = xlCenter
    End With
    ' 表示開始日。ここを変えて[工程表を描く]を押すと、表が丸ごとずれる
    With ws.Range("B2")
        .Value = SampleStart
        .NumberFormatLocal = "yyyy/m/d(aaa)"
        .Font.Bold = True
        .Font.Size = 10
        .Font.Color = RGB(22, 50, 79)
        .Interior.Color = RGB(233, 162, 59)
        .HorizontalAlignment = xlCenter
    End With
    DateDropdown ws.Range("B2"), "=" & LIST_SHEET & "!$G$2:$G$" & (PICK_WEEKS + 1)
    ' 見出しの帯。色はここで付けておく。
    ' DrawGantt は3行目の「値だけ」を消すので、この色は描き直しても残る
    ws.Range("B3").Value = "作業内容"
    HeaderStyle ws.Range("B3:B4")
    HeaderStyle ws.Range(ws.Cells(HEAD_ROW, FIRST_COL), ws.Cells(HEAD_ROW, lastCol))
    ws.Range(ws.Cells(HEAD_ROW, FIRST_COL), _
             ws.Cells(HEAD_ROW, lastCol)).NumberFormatLocal = "d"
    ' 月のラベルの行。左詰めにしておかないと、月名が右隣にはみ出して表示できない
    With ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), ws.Cells(MONTH_ROW, lastCol))
        .NumberFormat = "@"
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(22, 50, 79)
        .HorizontalAlignment = xlLeft
    End With
    WeekdayRow ws, lastCol
    ' 印刷。横幅を1枚に収め、縦は何枚になってもよい設定。
    ' Zoom = False にしないと FitToPagesWide が効かない(倍率指定が優先される)。
    ' ただし60日ぶんを1枚に押し込むと字が小さくなる。実際には表示日数を減らすか、
    ' 期間を区切って刷ることになる
    With ws.PageSetup
        .Orientation = xlLandscape
        .PaperSize = xlPaperA4
        .Zoom = False
        .FitToPagesWide = 1
        .FitToPagesTall = False
        .PrintTitleColumns = "$A:$D"
    End With
    FreezeAt ws, "E5"
    HideGridlines ws
End Sub
Private Sub BuildDaily(ws As Worksheet)
    Dim lastCol As Long
    lastCol = FIRST_COL + HOUR_COUNT - 1
    ws.Columns("A").ColumnWidth = 2.33
    ws.Columns("B").ColumnWidth = 16.8
    ws.Columns("C:D").ColumnWidth = 6.8
    ws.Range(ws.Columns(FIRST_COL), ws.Columns(lastCol)).ColumnWidth = 4.22
    ws.Rows(1).RowHeight = 25.95
    ws.Rows(MONTH_ROW).RowHeight = 15
    ws.Rows(HEAD_ROW).RowHeight = 15
    ws.Rows(WEEK_ROW).RowHeight = 7.95
    With ws.Range("B1")
        .Value = "詳細工程|日付を選んで[その日の工程を描く]"
        .Font.Bold = True
        .Font.Size = 12
        .Font.Color = RGB(22, 50, 79)
    End With
    DateCandidates ws
    ' 表示する日。最初は1週間後にしておく(サンプルの作業が重なっている日)
    With ws.Range("B2")
        .Value = SampleStart + 7
        .NumberFormatLocal = "yyyy/m/d(aaa)"
        .Font.Bold = True
        .Font.Size = 9
        .Interior.Color = RGB(245, 180, 76)
        .HorizontalAlignment = xlCenter
    End With
    DateDropdown ws.Range("B2"), "=$AD$2:$AD$" & (PICK_DAYS + 1)
    ws.Range("B3").Value = "作業内容"
    ws.Range("C3").Value = "開始"
    ws.Range("D3").Value = "終了"
    HeaderStyle ws.Range(ws.Cells(HEAD_ROW, 2), ws.Cells(HEAD_ROW, lastCol))
    ' こちらは24時間ぶんしかないので、幅を1枚に合わせても字がつぶれない
    With ws.PageSetup
        .Orientation = xlLandscape
        .PaperSize = xlPaperA4
        .Zoom = False
        .FitToPagesWide = 1
        .FitToPagesTall = False
        .PrintTitleColumns = "$A:$D"
    End With
    HideGridlines ws
End Sub
'==== ドロップダウンの中身。使う人には見せないのでAD列は隠す ====
Private Sub DateCandidates(ws As Worksheet)
    With ws.Range("AD1")
        .Value = "日付の候補"
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(100, 116, 139)
    End With
    ws.Columns("AD").ColumnWidth = 12.8
    ws.Range("AD2").Value = SampleStart
    ws.Range("AD3:AD" & (PICK_DAYS + 1)).FormulaR1C1 = "=R[-1]C+1"
    ws.Range("AD2:AD" & (PICK_DAYS + 1)).NumberFormatLocal = "yyyy/m/d"
    ws.Columns("AD").Hidden = True
End Sub
'============================================================
' ここから下は道具
'============================================================
'==== サンプルの起点。今月の1日。
'     DrawGantt の中の baseDate と紛らわしいので別の名前にしている ====
Private Function SampleStart() As Date
    SampleStart = DateSerial(Year(Date), Month(Date), 1)
End Function
'==== シートを返す。無ければ足して、足したことを isNew で知らせる ====
Private Function EnsureSheet(sheetName As String, isNew As Boolean) As Worksheet
    Dim ws As Worksheet
    isNew = False
    For Each ws In ThisWorkbook.Worksheets
        If ws.Name = sheetName Then
            Set EnsureSheet = ws
            Exit Function
        End If
    Next ws
    Set ws = ThisWorkbook.Worksheets.Add( _
             After:=ThisWorkbook.Worksheets(ThisWorkbook.Worksheets.Count))
    ws.Name = sheetName
    isNew = True
    Set EnsureSheet = ws
End Function
'==== その名前のテーブルがブックのどこかにあるか ====
Private Function TableExists(tblName As String) As Boolean
    Dim ws As Worksheet
    Dim lo As ListObject
    For Each ws In ThisWorkbook.Worksheets
        For Each lo In ws.ListObjects
            If lo.Name = tblName Then
                TableExists = True
                Exit Function
            End If
        Next lo
    Next ws
End Function
'==== その名前のボタンがそのシートにあるか ====
Private Function ShapeExists(ws As Worksheet, shapeName As String) As Boolean
    Dim k As Long
    For k = 1 To ws.Shapes.Count
        If ws.Shapes(k).Name = shapeName Then
            ShapeExists = True
            Exit Function
        End If
    Next k
End Function
'==== 入力規則が入っているか。
'     入っていないセルの .Validation.Type は
'     値を返さずエラーになるので、エラーが出るかどうかで見る ====
Private Function HasValidation(c As Range) As Boolean
    Dim t As Long
    On Error Resume Next
    t = c.Validation.Type
    HasValidation = (Err.Number = 0)
    Err.Clear
    On Error GoTo 0
End Function
'==== 何も入っていないシートか ====
Private Function IsEmptySheet(ws As Worksheet) As Boolean
    IsEmptySheet = (Application.WorksheetFunction.CountA(ws.Cells) = 0)
End Function
'==== 日付のドロップダウンを付ける ====
Private Sub DateDropdown(c As Range, listFormula As String)
    c.Validation.Delete
    c.Validation.Add Type:=xlValidateList, AlertStyle:=xlValidAlertStop, _
                     Formula1:=listFormula
End Sub
'==== 曜日の行。真上の日付を見るだけの数式を並べる。
'     これがあるので、VBA側で曜日を書かなくて済む ====
Private Sub WeekdayRow(ws As Worksheet, lastCol As Long)
    With ws.Range(ws.Cells(WEEK_ROW, FIRST_COL), ws.Cells(WEEK_ROW, lastCol))
        .FormulaR1C1 = "=R[-1]C"
        .NumberFormatLocal = "aaa"
        .Font.Size = 8
        .Font.Color = RGB(100, 116, 139)
        .HorizontalAlignment = xlCenter
    End With
End Sub
'==== 見出しの帯(濃い紺に白抜き)====
Private Sub HeaderStyle(target As Range)
    With target
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(255, 255, 255)
        .Interior.Color = RGB(22, 50, 79)
        .HorizontalAlignment = xlCenter
    End With
End Sub
'==== ボタンを1つ置く。もうあるなら何もしない ====
Private Sub EnsureButton(ws As Worksheet, btnName As String, btnCaption As String, _
                         macroName As String, L As Double, T As Double)
    Dim b As Object
    If ShapeExists(ws, btnName) Then
        NoteKept ws.Name & " の[" & btnCaption & "]ボタン"
        Exit Sub
    End If
    Set b = ws.Buttons.Add(L, T, 126, 24)
    b.Name = btnName
    b.Caption = btnCaption
    b.OnAction = macroName
    NoteMade ws.Name & " の[" & btnCaption & "]ボタン"
End Sub
'==== 見出しを固定する。画面の設定なので、ここだけシートを表に出す必要がある ====
Private Sub FreezeAt(ws As Worksheet, cellAddress As String)
    ws.Activate
    ActiveWindow.FreezePanes = False
    ws.Range(cellAddress).Select
    ActiveWindow.FreezePanes = True
End Sub
'==== 目盛線を消す。これも画面の設定 ====
Private Sub HideGridlines(ws As Worksheet)
    ws.Activate
    ActiveWindow.DisplayGridlines = False
End Sub
'==== 何をしたかを覚えておく ====
Private Sub NoteMade(msg As String)
    mMade = mMade & "・" & msg & vbCrLf
End Sub
Private Sub NoteKept(msg As String)
    mKept = mKept & "・" & msg & vbCrLf
End Sub
Private Sub NoteWarn(msg As String)
    mWarn = mWarn & "・" & msg & vbCrLf
End Sub
Private Function Warnings() As String
    If Len(mWarn) = 0 Then Exit Function
    Warnings = vbCrLf & "こちらでは決められなかったもの" & vbCrLf & mWarn
End Function
完成版② 工程表マクロ(2つ目のモジュールに貼る)
Option Explicit
'============================================================
' 工程表マクロ
'
'   描くところと、書き出すところだけが入っています。
'   触らなくて構いません。
'   設定と初期設定は「工程表設定」にまとめてあります。
'============================================================
Public Sub DrawGantt()
    Dim ws As Worksheet
    Dim lo As ListObject
    Dim baseDate As Date
    Dim i As Long, n As Long
    Dim d As Date
    Dim lastRow As Long
    Dim titleRow As Long, barRow As Long
    Dim sDate As Date, eDate As Date
    Dim sCol As Long, eCol As Long
    Dim memoCol As Long
    Dim memo As String
    Set ws = ThisWorkbook.Worksheets(SHEET_NAME)
    Set lo = ThisWorkbook.Worksheets(LIST_SHEET).ListObjects(TABLE_NAME)
    baseDate = ws.Range(START_CELL).Value
    lastRow = FIRST_ROW + lo.ListRows.Count * ROWS_PER_JOB - 1
    ' 備考の列があるかどうかを先に調べておく。無いブックでもエラーにしないため
    memoCol = ColIndex(lo, "備考")
    Application.ScreenUpdating = False
    ' 1) 前回描いた内容を全部消す
    ClearArea ws, DAY_COUNT
    ' 2) 見出しを引き直す(2行目=月・3行目=日。4行目の曜日は数式が追従する)
    ' 月のラベルは必ず「文字」として入れる。日本語のExcelは "2026年9月" を
    ' 日付だと解釈してしまい、列幅が狭いと ### になるため先に表示形式を文字列にする。
    ' 書式もここで毎回そろえる。最初の1つだけ手で色を付けたブックだと、
    ' 月が変わって出てくる2つ目以降が既定の黒のまま残ってしまうため
    With ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), _
                  ws.Cells(MONTH_ROW, FIRST_COL + DAY_COUNT - 1))
        .NumberFormat = "@"
        .Font.Bold = True
        .Font.Size = 9
        .Font.Color = RGB(22, 50, 79)
        .HorizontalAlignment = xlLeft
    End With
    For i = 0 To DAY_COUNT - 1
        d = baseDate + i
        ws.Cells(HEAD_ROW, FIRST_COL + i).Value = d
        ' 月のラベルは左端と月初だけ。隣は空のままにしないと文字がはみ出せない
        If i = 0 Or Day(d) = 1 Then
            ws.Cells(MONTH_ROW, FIRST_COL + i).Value = MonthLabel(baseDate, i)
        End If
    Next i
    ' 3) 先に土日を塗る(ここから下は塗る順番がそのまま重なる順番になる)
    '    2行目(月のラベル)は塗らない。月名は右隣の空セルへはみ出して表示するので、
    '    途中に色が入ると文字の下だけ帯が入ったように見える
    For i = 0 To DAY_COUNT - 1
        d = baseDate + i
        If Weekday(d, vbMonday) >= 6 Then
            ws.Range(ws.Cells(WEEK_ROW, FIRST_COL + i), _
                     ws.Cells(lastRow, FIRST_COL + i)).Interior.Color = RGB(231, 226, 216)
        End If
    Next i
    ' 4) その上にバーを重ねる
    For n = 1 To lo.ListRows.Count
        titleRow = FIRST_ROW + (n - 1) * ROWS_PER_JOB
        barRow = titleRow + 1
        ' 行の高さもここで揃える。揃えないと3行とも同じ高さのままで、
        ' せっかく3行1セットにしてもバーが太く見えて締まらない
        ws.Rows(titleRow).RowHeight = H_TITLE
        ws.Rows(barRow).RowHeight = H_BAR
        ws.Rows(barRow + 1).RowHeight = H_GAP
        ws.Cells(titleRow, 2).Value = _
            lo.ListColumns("作業内容").DataBodyRange.Cells(n).Value
        sDate = lo.ListColumns("開始日").DataBodyRange.Cells(n).Value
        eDate = lo.ListColumns("終了日").DataBodyRange.Cells(n).Value
        ' 表示している期間から外れている作業は描かない
        If eDate >= baseDate And sDate <= baseDate + DAY_COUNT - 1 Then
            If sDate < baseDate Then sDate = baseDate
            If eDate > baseDate + DAY_COUNT - 1 Then eDate = baseDate + DAY_COUNT - 1
            sCol = FIRST_COL + DateDiff("d", baseDate, sDate)
            eCol = FIRST_COL + DateDiff("d", baseDate, eDate)
            ws.Range(ws.Cells(barRow, sCol), ws.Cells(barRow, eCol)) _
              .Interior.Color = RGB(15, 118, 110)
            ' 備考はバーの真上(タイトル行)に置く。タイトル行のカレンダー側は
            ' 空いているので、右隣の空セルへはみ出してそのまま読める
            If memoCol > 0 Then
                memo = CStr(lo.ListColumns(memoCol).DataBodyRange.Cells(n).Value)
                If Len(memo) > 0 Then
                    With ws.Cells(titleRow, sCol)
                        .Value = memo
                        .HorizontalAlignment = xlLeft
                    End With
                End If
            End If
        End If
    Next n
    ' 5) 最後に罫線を引き直す
    DrawBorders ws, lo.ListRows.Count, baseDate
    Application.ScreenUpdating = True
End Sub
'==== 月のラベル。次の月初までに何列空いているかで、入る長さを選ぶ ====
'   列幅は3文字ぶんしかないので、月初のすぐ手前から表を始めると
'   "2026年9月" が右へはみ出しきれず "2026年" で切れてしまう
Private Function MonthLabel(baseDate As Date, i As Long) As String
    Dim gap As Long
    ' 次にラベルが出る列(次の月の1日)まで、何列あるかを数える
    gap = 1
    Do While i + gap <= DAY_COUNT - 1
        If Day(baseDate + i + gap) = 1 Then Exit Do
        gap = gap + 1
    Loop
    If gap >= 4 Then
        MonthLabel = Format$(baseDate + i, "yyyy年m月")
    Else
        MonthLabel = Format$(baseDate + i, "m月")
    End If
End Function
'==== 前回描いた内容を消す(全体工程・詳細工程で共通)====
Private Sub ClearArea(ws As Worksheet, colCount As Long)
    Dim lastRow As Long, lastCol As Long
    ' 消す範囲は「そのシートで何かが入っている最終行」まで。作業名の位置から
    ' 逆算すると、名前を手で消したときに範囲が縮んで、前のバーが残ってしまう
    lastRow = ws.UsedRange.Row + ws.UsedRange.Rows.Count - 1
    If lastRow < FIRST_ROW Then lastRow = FIRST_ROW
    lastCol = FIRST_COL + colCount - 1
    ' 2行目のラベルは値も色も消す
    With ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), ws.Cells(MONTH_ROW, lastCol))
        .ClearContents
        .Interior.ColorIndex = xlNone
    End With
    ' 3行目は「値だけ」消す(見出しの背景色は残したいため)
    ws.Range(ws.Cells(HEAD_ROW, FIRST_COL), ws.Cells(HEAD_ROW, lastCol)).ClearContents
    ws.Range(ws.Cells(WEEK_ROW, FIRST_COL), _
             ws.Cells(WEEK_ROW, lastCol)).Interior.ColorIndex = xlNone
    With ws.Range(ws.Cells(FIRST_ROW, FIRST_COL), ws.Cells(lastRow, lastCol))
        .ClearContents
        .Interior.ColorIndex = xlNone
    End With
    ' 罫線も消す。作業が減ったとき、前回の横線が下に残ってしまうため
    ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), _
             ws.Cells(lastRow, lastCol)).Borders.LineStyle = xlNone
    ws.Range(ws.Cells(WEEK_ROW, 2), ws.Cells(lastRow, 4)).Borders.LineStyle = xlNone
    ws.Range(ws.Cells(FIRST_ROW, 2), ws.Cells(lastRow, 4)).ClearContents
    ' 行の高さも標準に戻す。作業が減ったとき、前回の高さがそのまま残るため
    ws.Rows(FIRST_ROW & ":" & lastRow).UseStandardHeight = True
End Sub
'==== 縦線を1本引く ====
Private Sub VLine(ws As Worksheet, col As Long, lastRow As Long, lineColor As Long)
    With ws.Range(ws.Cells(MONTH_ROW, col), ws.Cells(lastRow, col)).Borders(xlEdgeLeft)
        .LineStyle = xlContinuous
        .Color = lineColor
        .Weight = xlThin
    End With
End Sub
'==== マス目・見出しの下線・作業の区切り線(全体工程・詳細工程で共通)====
Private Sub GridBorders(ws As Worksheet, jobCount As Long, lastCol As Long)
    Dim lastRow As Long, k As Long, n As Long, sepRow As Long
    Dim edges As Variant
    lastRow = FIRST_ROW + jobCount * ROWS_PER_JOB - 1
    ' 1) まず全体に細いマス目を敷く
    '    消す範囲と引く範囲を必ず同じにする。ここを FIRST_ROW から始めると、
    '    見出しの行の罫線を消したまま引き直さないことになる
    edges = Array(xlEdgeLeft, xlEdgeTop, xlEdgeBottom, xlEdgeRight, _
                  xlInsideVertical, xlInsideHorizontal)
    For k = 0 To UBound(edges)
        With ws.Range(ws.Cells(MONTH_ROW, FIRST_COL), _
                      ws.Cells(lastRow, lastCol)).Borders(edges(k))
            .LineStyle = xlContinuous
            .Color = RGB(216, 222, 230)
            .Weight = xlHairline
        End With
    Next k
    ' 2) 見出しの下に1本(B列から右端まで)
    With ws.Range(ws.Cells(WEEK_ROW, 2), _
                  ws.Cells(WEEK_ROW, lastCol)).Borders(xlEdgeBottom)
        .LineStyle = xlContinuous
        .Color = RGB(22, 50, 79)
        .Weight = xlMedium
    End With
    ' 3) 作業のまとまりごとに1本、少し濃い横線
    For n = 1 To jobCount
        sepRow = FIRST_ROW + n * ROWS_PER_JOB - 1
        With ws.Range(ws.Cells(sepRow, 2), _
                      ws.Cells(sepRow, lastCol)).Borders(xlEdgeBottom)
            .LineStyle = xlContinuous
            .Color = RGB(100, 116, 139)
            .Weight = xlThin
        End With
    Next n
End Sub
'==== 全体工程の罫線を引く ====
Private Sub DrawBorders(ws As Worksheet, jobCount As Long, baseDate As Date)
    Dim lastRow As Long, i As Long
    lastRow = FIRST_ROW + jobCount * ROWS_PER_JOB - 1
    GridBorders ws, jobCount, FIRST_COL + DAY_COUNT - 1
    ' 月曜の左に縦線を入れて、週の区切りを見せる
    For i = 0 To DAY_COUNT - 1
        If Weekday(baseDate + i, vbMonday) = 1 Then
            VLine ws, FIRST_COL + i, lastRow, RGB(148, 163, 184)
        End If
    Next i
    VLine ws, FIRST_COL, lastRow, RGB(100, 116, 139)
End Sub
'==== 列の名前から位置を探す。無ければ 0 を返す ====
Private Function ColIndex(lo As ListObject, colName As String) As Long
    Dim k As Long
    ColIndex = 0
    For k = 1 To lo.ListColumns.Count
        If lo.ListColumns(k).Name = colName Then ColIndex = k
    Next k
End Function
'==== 作業の開始・終了を「日付+時間」の1つの値にして返す ====
Private Function JobStart(lo As ListObject, n As Long) As Date
    JobStart = Int(lo.ListColumns("開始日").DataBodyRange.Cells(n).Value) + _
               CDbl(lo.ListColumns("開始時間").DataBodyRange.Cells(n).Value)
End Function
Private Function JobEnd(lo As ListObject, n As Long) As Date
    JobEnd = Int(lo.ListColumns("終了日").DataBodyRange.Cells(n).Value) + _
             CDbl(lo.ListColumns("終了時間").DataBodyRange.Cells(n).Value)
End Function
'==== その日の詳細工程を描く ====
Public Sub DrawDaily()
    Dim ws As Worksheet
    Dim lo As ListObject
    Dim theDate As Date
    Dim i As Long, n As Long, k As Long, cnt As Long
    Dim lastRow As Long
    Dim titleRow As Long, barRow As Long
    Dim sVal As Date, eVal As Date
    Dim sh As Long, eh As Long
    Dim idx() As Long
    Dim memoCol As Long
    Dim memo As String
    Set ws = ThisWorkbook.Worksheets(DAILY_SHEET)
    Set lo = ThisWorkbook.Worksheets(LIST_SHEET).ListObjects(TABLE_NAME)
    ' 時間の列が無いと日をまたぐ判定ができないので、ここで止める
    If ColIndex(lo, "開始時間") = 0 Or ColIndex(lo, "終了時間") = 0 Then
        MsgBox "作業リストに「開始時間」「終了時間」の列がありません。", vbExclamation
        Exit Sub
    End If
    theDate = Int(ws.Range(DAILY_CELL).Value)
    memoCol = ColIndex(lo, "備考")
    Application.ScreenUpdating = False
    ClearArea ws, HOUR_COUNT
    ' 1) その日にかかっている作業を先に数える。
    '    行の数が決まらないと、塗る範囲も罫線の範囲も決められない
    ReDim idx(1 To lo.ListRows.Count)
    cnt = 0
    For n = 1 To lo.ListRows.Count
        If Int(JobStart(lo, n)) <= theDate And Int(JobEnd(lo, n)) >= theDate Then
            cnt = cnt + 1
            idx(cnt) = n
        End If
    Next n
    lastRow = FIRST_ROW + cnt * ROWS_PER_JOB - 1
    If lastRow < FIRST_ROW Then lastRow = FIRST_ROW
    ' 2) 時刻の目盛りを並べる。日付は B2 に出ているので、ここには書かない
    '    (同じ日付が2か所に出ると、どちらを直せばいいのか分からなくなる)
    ws.Range(DAILY_CELL).NumberFormatLocal = "yyyy/m/d(aaa)"
    For i = 0 To HOUR_COUNT - 1
        ws.Cells(HEAD_ROW, FIRST_COL + i).Value = i
    Next i
    ' 3) 人がいない時間を先に塗る。8時より前・17時以降・昼休み
    For i = 0 To HOUR_COUNT - 1
        If i < WORK_START Or i >= WORK_END Or i = LUNCH_HOUR Then
            ws.Range(ws.Cells(WEEK_ROW, FIRST_COL + i), _
                     ws.Cells(lastRow, FIRST_COL + i)).Interior.Color = RGB(231, 226, 216)
        End If
    Next i
    ' 4) その上にバーを重ねる
    For k = 1 To cnt
        n = idx(k)
        titleRow = FIRST_ROW + (k - 1) * ROWS_PER_JOB
        barRow = titleRow + 1
        ws.Rows(titleRow).RowHeight = H_TITLE
        ws.Rows(barRow).RowHeight = H_BAR
        ws.Rows(barRow + 1).RowHeight = H_GAP
        sVal = JobStart(lo, n)
        eVal = JobEnd(lo, n)
        ws.Cells(titleRow, 2).Value = _
            lo.ListColumns("作業内容").DataBodyRange.Cells(n).Value
        ' その日の中での開始と終了。前の日から続いていれば0時から、
        ' 次の日へ続くなら24時まで。ここが日をまたぐ作業の扱いになる
        If Int(sVal) = theDate Then sh = Hour(sVal) Else sh = 0
        If Int(eVal) = theDate Then eh = Hour(eVal) Else eh = HOUR_COUNT
        If eh < HOUR_COUNT And Minute(eVal) > 0 Then eh = eh + 1   ' 端数は切り上げる
        ' 24:00 は時刻として解釈されると日付に化けて #### になる。文字で入れる
        ws.Range(ws.Cells(titleRow, 3), ws.Cells(titleRow, 4)).NumberFormat = "@"
        ws.Cells(titleRow, 3).Value = sh & ":00"
        ws.Cells(titleRow, 4).Value = eh & ":00"
        If eh > sh Then
            ws.Range(ws.Cells(barRow, FIRST_COL + sh), _
                     ws.Cells(barRow, FIRST_COL + eh - 1)).Interior.Color = RGB(15, 118, 110)
            If memoCol > 0 Then
                memo = CStr(lo.ListColumns(memoCol).DataBodyRange.Cells(n).Value)
                If Len(memo) > 0 Then
                    With ws.Cells(titleRow, FIRST_COL + sh)
                        .Value = memo
                        .HorizontalAlignment = xlLeft
                    End With
                End If
            End If
        End If
    Next k
    DrawDailyBorders ws, cnt
    Application.ScreenUpdating = True
    If cnt = 0 Then
        MsgBox Format$(theDate, "m月d日") & " にかかる作業はありません。", vbInformation
    End If
End Sub
'==== 詳細工程の罫線を引く ====
Private Sub DrawDailyBorders(ws As Worksheet, jobCount As Long)
    Dim lastRow As Long
    If jobCount = 0 Then Exit Sub
    lastRow = FIRST_ROW + jobCount * ROWS_PER_JOB - 1
    GridBorders ws, jobCount, FIRST_COL + HOUR_COUNT - 1
    ' 日勤の始まりと終わりに縦線。全体工程の月曜の線と同じ役目
    VLine ws, FIRST_COL + WORK_START, lastRow, RGB(148, 163, 184)
    VLine ws, FIRST_COL + WORK_END, lastRow, RGB(148, 163, 184)
    VLine ws, FIRST_COL, lastRow, RGB(100, 116, 139)
End Sub
'==== ボタンから呼ぶ入口。全体工程を書き出す ====
Public Sub ExportGantt()
    ExportSheet SHEET_NAME
End Sub
'==== ボタンから呼ぶ入口。詳細工程(その日の時間割)を書き出す ====
Public Sub ExportDaily()
    ExportSheet DAILY_SHEET
End Sub
'==== 中身。シート名を受け取って、同じフォルダにPDFとExcelで出す ====
Private Sub ExportSheet(sheetName As String)
    ' 出力先は「工程表設定」の EXPORT_DIR を使う
    Dim ws As Worksheet
    Dim outWs As Worksheet
    Dim baseName As String
    Dim outDir As String
    Dim ans As VbMsgBoxResult
    Dim n As Long
    Set ws = ThisWorkbook.Worksheets(sheetName)
    outDir = EXPORT_DIR
    ' 末尾の \ を忘れると、フォルダ名がファイル名の頭にくっついて
    ' 1つ上のフォルダに出てしまう。忘れても動くようにここで補う
    If Right$(outDir, 1) <> "\" Then outDir = outDir & "\"
    If Dir(outDir, vbDirectory) = "" Then MkDir outDir
    ' 受け取ったシート名をそのままファイル名に使う。
    ' 全体工程なら「20260812_全体工程」、詳細工程なら「20260812_詳細工程」
    baseName = Format$(Date, "yyyymmdd") & "_" & sheetName
    ' 同じ名前がすでにあるかを先に見る。PDFとExcelのどちらか一方でもあれば聞く
    If FileExists(outDir & baseName & ".pdf") _
       Or FileExists(outDir & baseName & ".xlsx") Then
        ans = MsgBox(baseName & " はすでにあります。上書きしますか?" & vbCrLf & vbCrLf & _
                     "はい    … 上書きする" & vbCrLf & _
                     "いいえ   … _2、_3 と番号を付けて別に保存する" & vbCrLf & _
                     "キャンセル … 書き出しをやめる", _
                     vbYesNoCancel + vbQuestion, "書き出し")
        If ans = vbCancel Then Exit Sub
        If ans = vbNo Then
            ' PDFとExcelの両方が空いている番号まで進める。片方だけで判定すると
            ' PDFが_2でExcelが_3のようにずれて、どれが同じ書き出しか分からなくなる
            n = 2
            Do While FileExists(outDir & baseName & "_" & n & ".pdf") _
                     Or FileExists(outDir & baseName & "_" & n & ".xlsx")
                n = n + 1
            Loop
            baseName = baseName & "_" & n
        End If
    End If
    ' このシートだけ別ブックにコピーして、そちらから書き出す
    ws.Copy
    Set outWs = ActiveWorkbook.Worksheets(1)
    ' ボタンはコピーにも付いてくる。渡した先にはマクロが無いので押すとエラーになるし、
    ' 紙にもそのまま出てしまう。コピーのほうから消しておく
    Do While outWs.Shapes.Count > 0
        outWs.Shapes(1).Delete
    Loop
    ' PDFで出す
    outWs.ExportAsFixedFormat _
        Type:=xlTypePDF, _
        FileName:=outDir & baseName & ".pdf", _
        Quality:=xlQualityStandard, _
        IgnorePrintAreas:=False, _
        OpenAfterPublish:=False
    ' Excelで出す
    ' 上書きしていいかは上で聞いたあとなので、Excelにもう一度聞かせない
    Application.DisplayAlerts = False
    ActiveWorkbook.SaveAs _
        FileName:=outDir & baseName & ".xlsx", _
        FileFormat:=xlOpenXMLWorkbook
    ActiveWorkbook.Close SaveChanges:=False
    Application.DisplayAlerts = True
    MsgBox "書き出しました。" & vbCrLf & outDir & baseName & ".pdf / .xlsx", vbInformation
End Sub
'==== ファイルがあるかどうかを見るだけの関数 ====
Private Function FileExists(filePath As String) As Boolean
    FileExists = (Dir(filePath) <> "")
End Function
'==== 保存先をその場で選びたいとき ====
Public Function AskFolder(defaultPath As String) As String
    With Application.FileDialog(msoFileDialogFolderPicker)
        .Title = "書き出し先のフォルダを選んでください"
        .InitialFileName = defaultPath
        If .Show = -1 Then
            AskFolder = .SelectedItems(1) & "\"
        Else
            AskFolder = defaultPath   ' キャンセルなら初期値のまま
        End If
    End With
End Function
マクロを有効にしても動きません。

ファイルをメールや共有リンクで受け取った場合、Officeが既定でマクロをブロックします。バージョン2206以降の動作で、インターネット由来のファイルには Windows が印(Mark of the Web)を付けるためです。この場合は「セキュリティリスク」のバナーが出て、そもそも有効化ボタンが出ません。

解除するには、ファイルを右クリック→プロパティ→全般タブの下にある「ブロックの解除」にチェックを入れます。詳しくはMicrosoft Learnの「インターネットからのマクロは、Officeで既定でブロックされます」に条件が整理されています。自分で作ったファイルなら、この現象は起きません。

保存したらマクロが消えました。

拡張子が .xlsx になっていませんか。マクロを含むブックは .xlsm で保存する必要があります。「名前を付けて保存」でファイルの種類を「Excel マクロ有効ブック」にしてください。

条件付き書式のバーが、途中で途切れます。

土日のルールがバーのルールより上にある可能性が高いです。「条件付き書式ルールの管理」でバーのルールを上に移動してください。または、数式の $ の付け方(E$3$C5)を見直してください。

マクロなしとVBA、どちらを選べばいいですか。

作る回数で決めるのがおすすめです。

マクロなしとVBAの得意分野を比べた5角形のレーダーチャート
外に伸びているほど得意。渡しやすさは差がつかないので軸に入れていません。
スクロールできます
マクロなし(条件付き書式)VBA
向いている場面工程表を作る回数が年に数回毎週・毎月作り直す
表示期間の切り替え苦手(列を隠す・スクロール)ボタン1つでできる
他の人に渡すそのまま渡せる描いたあと .xlsx で保存すれば、相手にマクロは要らない
覚えること数式と $ の付け方上記+VBAの基礎
進捗率・実績の自動記録手で入力すれば持てる。自動では残らない同じ。自動では残らない

渡すときの話だけ補足します。VBAで描いたバーは、色と値としてシートに残ります。だから描き終わったものを .xlsx で保存して渡せば、受け取る側にマクロは要りません。マクロが要るのは、作り直す側だけです。ただしボタンは消してから渡してください。上の書き出しコードが図形を消しているのはそのためです。

テンプレートは配っていないんですか。

配っていません。この記事の数式とコードをそのまま使えば同じものが作れますし、表のレイアウトは職場ごとに違うので、配ったものを直すほうが手間になると考えているためです。もし「それでもファイルが欲しい」という声が多ければ検討します。

この記事の持ち帰り
  1. コピペで使える条件付き書式の数式 … 土日 =WEEKDAY(E$3,2)>=6 / バー =AND($C5<>"",E$3>=$C5,E$3<=$D5)
  2. テーブルからバーを描くVBAコード … 全部消して描き直す形。設定は先頭の定数だけ変える
  3. 判断のものさし … 年数回ならマクロなし。毎週作り直すならVBA。幅と紙で詰まったらExcelの外

まとめ

  • 検索して出てくる「工程表の自動作成」の多くは、開始日と終了日を打てばバーが出るところまで。そこは関数と条件付き書式で足りる
  • 毎回作り直すならVBA。テーブルを作業リストにして、ボタンで全部消して描き直す形が扱いやすい
  • 条件付き書式で足りるものは、コードにしない。土日は条件付き書式のまま残していい
  • 自動化できるのは「描く」ところまで。進捗率と実績は、入力するか他のシステムから持ってこない限り残らない
  • 1枚に収める前提だと、期間の長さと読みやすさは必ずぶつかる。Excelが上手くなっても消えない
  • バージョンは増える。増やさない努力より、置き場所を1つに決めるほうが効く

私はこの形にたどり着くまでに、手で塗る3年と、試行錯誤の1年を使いました。同じ遠回りをする必要はありません。まずは土日の条件付き書式だけでも入れてみてください。あれが消えるだけで、だいぶ違います。

次にどうするか
スクロールできます
いまの状態次に見るもの
コードが読めなかったVBA(マクロ)入門ガイド。ここからで大丈夫です
コードを書かずに済ませたいSchedika Lite を無料で試す(PR・自社製品)
制限なしで使いたいSchedika Standard の販売ページ(PR・自社製品/買い切り2,980円・2026年8月時点)

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次