2021/12/28

ToDoリストで個人タスク管理



1.背景

人によって多い少ないはあるにしても、やらなければいけない事は誰でも持っています。しかし、それを忘れずに実行するのは至難の業です。多くの人は、カレンダーに予定を書き込んだりタスク管理アプリなどを利用したりと色々な道具を使って管理していると思います。
私もカレンダーに書いておくことが多いのですが、複数のカレンダーに記入したりするので時々抜けがあったり、また気まぐれにメモ紙に書いて机の前に貼ってみたりと情報がバラバラです。たぶん、常に使う道具に情報を集約しておくのが良い方法なのだと思います。

今回はExcelのアドインとして、やらなければいけない情報を保存するアプリを紹介します。Excelを毎日使っている方であれば、有効な手段かと思います。専用のアプリにはとても太刀打ちできませんが、基本機能である「登録」→「確認」→「完了処理」は出来ますので、参考として下さい。

なお今回システムでは、データはテーブル化(ListObject)した上で操作を行っています。ListObjectを使ったものとして、今回以外にも以下のものが有りますので、そちらも参照下さい。
 ・DVD等の内容・保管場所等管理システム
 ・先行予約可能な備品予約・貸出システム
 ・会社番号検索システム

2.システム概要

今回のToDoリストは、Excelにアドイン登録して使うことを想定しています。アドインとして登録されたマクロ(図2-1の①)を実行すると、操作ダイアログ②が起動します。起動直後は、ToDoリストが表示されます。
なお「サンプルファイル」では、データテーブル横に起動用ボタンを置きました。そのボタンクリックでシステム起動します。
システム起動とToDoリスト
図2-1

表示されるToDoリストは、期限が1週間以内に迫っているタスク項目が表示されます(マクロの定数設定で変更可)。そしてタスクが完了したらリストの対象項目を選択し、上部の「完了」ボタンをクリックすることで完了処置され、ToDoリストから削除されます(項目は「完了一覧」ページへ移動)。
またタスクが不要になった場合には、「中止」ボタンをクリックすることで中止扱いにすることが可能です(この場合も、項目は「完了一覧」ページへ移動)。

タスク項目の登録は、ダイアログ上の「新規/更新」ページから行います。図2-2の左側ように必要項目を入力後、「登録③」ボタンをクリックすることで登録されます(期限が近ければ、ToDoリストに表示④される)。
タスク項目の新規作成
図2-2

なお新規/更新ページで「定期的」のチェックボックスにレ点を入れ、「毎日・毎週・毎月・毎年」の何れかを選択して登録を行うと、タスクを自動的に繰返し発生させることが出来ます。(正確には、定期的タスクを終了させた時に、次のタスクを自動的に作成します。)

タスク項目の修正を行う場合は、図2-3の左側のようにToDoリストで対象項目を選択し「編集」ボタン⑤をクリックします。するとデータは「新規/更新」ページに移動しますので、修正の上「登録」ボタン⑥をクリックすると編集が完了します。
項目の修正
図2-3

ToDoリストで「完了」または「中止」した項目は、「完了一覧」ページのリストに移動します。完了一覧のリストは、完了・中止後、約1ヶ月分をリストボックスに表示しています(マクロの定数設定で変更可)。
完了・中止した項目は、復活も可能です。図2-4のように対象項目を選択し「復活⑦」ボタンをクリックすることで、データが「新規/更新」ページに移動します。そこで内容を修正し「登録⑧」ボタンをクリックするとデータが復活します。
中止・完了項目の復活
図2-4

なお、中止した項目は文字通り復活して「完了一覧」から「ToDoリスト」へ移動しますが、完了した項目は「完了データを完了一覧に残し」たまま、新たに「ToDoリスト」にデータを作ります。

3.プログラムの流れ

操作ダイアログ上にはMultiPageコントロールを置き、「ToDoリスト」「新規/更新」「完了一覧」の3ページを作っています。また、Sheet1にテーブル(ListObject)を作り、データを保管します。

プログラムの流れ
図3-1

「ToDo」ページと「完了一覧」ページのリストには、テーブルからデータを引っ張ってきて貼り付けます。当然データを引っ張る前のデータの並べ方と絞り込み条件は、それぞれのリストで異なります。また「新規/更新」ページでは、入力された値を配列にし、テーブル(ListObject)に新規追加・データ行更新を行います。
「ToDoリスト」のデータに対し「完了処置」「中止処置」をする際は、対象データの「FinD(実行日)」列、「Status(状態)」列に日付と処置内容を記入します。
「ToDoリスト」の編集、「完了一覧」の復活ボタンをクリックした際には、その対象のデータを「新規/更新」ページに移動させ、ユーザーがデータを修正後、新規データ追加と同様にデータの新規追加・データ行更新を行います。追加になるか更新になるかは、データの状態(Status列の値)により異なります。

4.データ用シート(Sheet1)

データは、Sheet1に置いています。図4-1のように7列のテーブル(ListObject)で、テーブル名「ToDoT」をあらかじめ設定済みです。
データテーブル
図4-1

各列の内容は、以下の通りになります。
列名内容
ANumタスク番号(連番)
BTaskタスク名
CPriority優先度(A・B・C)
DEndD期限日
EFinD完了・中止日(無印は、未完)
FEvery定期の内容(無印は、単発)
GStatus状態:完了or中止(無印は、未完)
図4-2

なおサンプルファイルでは、わざわざアドイン登録しなくても動作確認が出来るように、シート上に起動ボタン(標準モジュールのToDoStartプロシージャを登録)を設けています。

5.標準モジュール(Module1)

標準モジュールでは、システムの起動関係と、テーブル(ListObject)に保管したデータとの直接的なやり取りを行うプロシージャを置いています。

5-1.システムの起動

宣言部では、システムとして使用する変数・定数の宣言と、標準モジュール内で使用する定数の宣言を行っています。
  1. '========== ⇩(1) 定数・変数の宣言 ============
  2. Public T1 As ListObject         '←保存データのテーブル
  3. Const Sh As String = "Sheet1"      '←テーブルの存在するシート名
  4. Const Tbl As String = "ToDoT"      '←テーブルの名前
  5. Public Const Term1 As Integer = 7    '←ToDoリストに載せる期間
  6. Public Const Term2 As Integer = 30   '←完了リストに載せる期間
図5-1

2行目「Public T1 As ListObject」は、保存データのテーブルをListObjectとして宣言しています。今回システムではテーブルは1つしか使用していないため、テーブルへ直接アクセスするプロシージャ内(標準プロシージャ内)で共有すれば充分なのですが、複数テーブルを操作する時の汎用性を考えてプロジェクトレベル(Public)の変数としています。
3行目「Const Sh As String = "Sheet1"」は、データテーブルのシートを定数宣言しています。
4行目「Const Tbl As String = "ToDoT"」は、データテーブルのテーブル名を定数宣言しています。なお、テーブルのListObject設定は図5-2の10行目で行っており、3・4行目は図5-2のToDoStartプロシージャ内で宣言することで充分です。しかし、定数値を変更する可能性のある値については「宣言部」で宣言した方が分かり易いと考え、宣言部での宣言を行っています。
5行目「Public Const Term1 As Integer = 7」は、「ToDoリスト」に表示する「期限の〇日前までを載せる」かを宣言しています。サンプルファイルでは「7」としていますので、「7日間(1週間)前になったらToDoリストに表示」しています。
6行目「Public Const Term2 As Integer = 30」は、「完了一覧」に表示する「完了・中止処理から〇日後までを載せる」かを宣言しています。サンプルファイルでは「30」としていますので、「完了・中止してから30日間(約1ヶ月)は完了一覧のリストに表示」しています。

システムを起動するプロシージャが図5-2です。Excelのアドインにサンプルファイルを登録後、起動マクロにはこのプロシージャを登録して下さい。
  1. '========== ⇩(2) システムの起動 ============
  2. Sub ToDoStart()
  3.  Set T1 = ThisWorkbook.Sheets(Sh).ListObjects(Tbl)
  4.  UserForm1.Show 0
  5. End Sub
図5-2

10行目「Set T1 = ThisWorkbook.Sheets(Sh).ListObjects(Tbl)」で、データのテーブルをObject設定しています。定数である「Sh」「Tbl」は、図5-1の宣言部で宣言しています。
12行目「UserForm1.Show 0」では、操作ダイアログ(UserForm1)をモードレス(フォームが表示された状態でもExcelシートを操作できる状態)で表示しています。

5-2.テーブルの並べ替え

これ以降は、テーブルへ直接操作するプロシージャ類です。フォーム内から呼び出されるため、全てPublic宣言になっています。

まず図5-3は、テーブルのデータを並べ替えるプロシージャです。引数として、対象のListObjectオブジェクト(T)、並べ替える列名(col)、並べ替えの方向(Sorder:文字列(String)のOrderという意味のつもり)を受取ります。
  1. '========== ⇩(3) テーブルの並べ替え ============
  2. Public Sub TableSort(T As ListObject, col As Variant, Sorder As Long)
  3.  With T.Sort
  4.   .SortFields.Clear
  5.   .SortFields.Add key:=T.ListColumns(col).Range, Order:=Sorder
  6.   .Apply
  7.  End With
  8. End Sub
図5-3

17行目「With T.Sort」で、Sortオブジェクトの設定のベースを作っています。
18行目「.SortFields.Clear」では、Clearメソッドで並び替えレベルの初期化を行います。以前実行した並び替え条件がもし残っていると正しく並び替えができません。

19行目「.SortFields.Add key:=T.ListColumns(col).Range, Order:=Sorder」で、新たな並び替え条件を作成します。
Addメソッドのパラメータとして、まず「key:=T.ListColumns(col).Range」で、「並び替えの列のRangeオブジェクト」を第二引数で得た「列名col」を使って列を指定します。なお引数colは「列名」でも「列位置(整数)」でもOKですが、今回システムのフォーム内では、分かり易い「列名」を使用しています。
もう一つのパラメータ「Order:=Sorder」では、並び替えの方向を指定します。第三引数で得た「並び順Sorder」を使用します。
パラメータOrderに設定する値は図5-4のようになっており、今回システムでは「値」を使って指定していますが、定数を指定してもOKです。
Orderに設定する値
定数内容
xlAscending1昇順で並べ替え(既定値)
xlDescending2降順で並べ替え
図5-4

なお、Sort.Addのパラメータは図5-5のように5つありますが、今回はKey(必須)とOrderのみを使用しました。他のパラメータは既定値でOKと判断したためです。
Sort.Addのパラメータ
名前内容
KeyRange並び替えの列(必須)
SortOnVariant並び替えのキー
OrderVariant並び替え順序
CustomOrderVariantユーザー指定の並び替え順序
DataOptionVariantデータオプション
図5-5

また、Sortオブジェクトに設定できるプロパティは、SortFields以外にもいくつかあります(図5-6)。こちらも既定値でOKと判断し省略しています。今回使用しなかったパラメータ・プロパティについては「DVD等の内容・保管場所等管理システム」でもう少し詳しく説明しています。
Sortの設定可能プロパティ
プロパティ内容
Header先頭行を見出しとするか
MatchCase大文字小文字の区別
Orientation並べ替えの方向
SortFields並び替えのキー値など
SortMethodふりがなを使うか
図5-6

並び替えの設定が完了したら、20行目「.Apply」で「並び替えを実行」します。

5-3.テーブルの絞り込み

テーブルの絞り込みを行うのが図5-7です。
引数として、対象のListObjectオブジェクト(T)、絞り込む列名(col)、絞り込み条件(word)を受取ります。
  1. '========== ⇩(4) テーブルの絞り込み ============
  2. Public Sub TableFilter(T As ListObject, col As Variant, word As String)
  3.  T.Range.AutoFilter Field:=T.ListColumns(col).Index, Criteria1:=word
  4. End Sub
図5-7

26行目「T.Range.AutoFilter Field:=T.ListColumns(col).Index, Criteria1:=word」では、見出しを含めたテーブル全体「T.Range」に対してフィルターを実行しています。
1つ目のパラメータ「Field:=T.ListColumns(col).Index」では、テーブルの左側から数える「列番号」を整数で指定します。第二引数として得ているcolは「列名」または「列番号」のどちらかですので、整数の列番号に揃えるために「T.ListColumns(col).Index」を使用します。なお、引数を「列番号のみ」とするのであれば直接「 Field:=col」と出来ますが、呼出し側のコードが読み難くなってしまうのと、列の順番を入れ替えたり、列を追加したり出来なくなるデメリットも生じます。

2つ目のパラメータ「Criteria1:=word」は、第三引数の「絞り込み条件」をCriteria1に設定しています。今回システムでは絞り込み条件は「日付列データ <= ある日付」のように「条件は1つ」しか使用していませんが、「ある日付 <= 日付列データ <= ある日付」のように挟み込む場合には、図5-8のように他のパラメータも使って絞り込む必要があります。
AutoFilterメソッドのパラメータ
名前内容
FieldVariantフィールド番号
Criteria1Variant抽出条件
OperatorXlAutoFilterOperator2つの条件を関連付ける演算子
xlAnd、xlOrなど
Criteria2Variant2番目の抽出条件
SubFieldVariant抽出条件を適用するデータ型のフィールド
VisibleDropDownVariantオートフィルタのドロップダウン矢印の表示非表示
図5-8

絞り込んだ後は、絞り込みを解除する必要がありますが、そのプロシージャが図5-9です。引数として対象のListObjectオブジェクト(T)を受取ります。
  1. '========== ⇩(5) テーブルの絞り込み解除 ============
  2. Public Sub TableFilterOff(T As ListObject)
  3.  T.ShowAutoFilter = False
  4.  T.ShowAutoFilter = True
  5.  On Error Resume Next
  6.   T.DataBodyRange.Rows.UseStandardHeight = True
  7.  On Error GoTo 0
  8. End Sub
図5-9

絞り込みを解除するには、「T.AutoFilter Field:=i」というCriteria1パラメータが無いコードを列数(i)だけ繰り返す方法が一般的なようですが、今回は「フィルターを一旦非表示にし、その後で再表示」させる方法としました。この方法によるメリットは 「先行予約可能な備品予約・貸出システム」でも説明していますが、処理速度が早くなる事です。

31行目「T.ShowAutoFilter = False」で、フィルターを一旦非表示にし、32行目「T.ShowAutoFilter = True」で再表示しています。
その後の34行目「T.DataBodyRange.Rows.UseStandardHeight = True」で、「テーブルの行高さを標準高さに設定」していますが、これは、図5-10の「絞り込みデータの配列化」の中で、「絞りこまれている行か否か」を判断するために「行の高さを調べる」必要が出て来たため、「フィルター解除の際には、行高さを標準に戻す」ようにしています。
なお、データが1つも無い場合には「DataBodyRange」オブジェクトが存在しませんので、エラー停止防止として33行目「On Error Resume Next」でエラースルーをさせています。

5-4.絞り込みデータの配列化

絞り込まれたテーブルのデータを配列の形にするのが図5-10です。引数として対象ListObjectオブジェクト(T)を受取ります。
  1. '========== ⇩(6) 絞り込みデータの配列化 ============
  2. Public Function SearchList(T As ListObject) As Variant
  3.  Dim buf1 As Variant    '←テーブルの全データの配列
  4.  Dim buf2 As Variant    '←抽出するデータの配列
  5.  Dim i As Long       '←テーブルの全データ行数
  6.  Dim j As Integer      '←テーブルの列数
  7.  Dim k As Long       '←抽出データの行数
  8.  Dim wcnt As Long     '←テーブルの列数
  9.  Dim icnt As Long     '←抽出テーブルの要素数
  10.  On Error Resume Next
  11.   buf1 = T.DataBodyRange
  12.   wcnt = T.HeaderRowRange.Count
  13.   icnt = T.DataBodyRange.SpecialCells(xlCellTypeVisible).Count
  14.   ReDim buf2(1 To Int(icnt / wcnt), 1 To wcnt)
  15.   For i = 1 To T.DataBodyRange.Rows.Count
  16.    If Not T.DataBodyRange.Rows(i).Height = 0 Then
  17.     k = k + 1
  18.     For j = 1 To wcnt
  19.      buf2(k, j) = buf1(i, j)
  20.     Next j
  21.    End If
  22.   Next i
  23.   SearchList = buf2
  24.  On Error GoTo 0
  25. End Function
図5-10

50行目「buf1 = T.DataBodyRange」では、まずテーブルの全てのデータを配列buf1に取り込みます。
従来「先行予約可能な備品予約・貸出システム」は、「処理をする行の絞り込み有無を確認し、絞り込まれた行であればその行の各セル値を配列に代入」するという手法だったのですが、今回は、まず「全データを配列に格納」した後、処理する行の絞り込み有無を確かめ、絞り込まれていれば全データ配列のデータから別な配列へデータをコピーするという手法にしてみました。

検証のため適当なデータを作って試してみました。1000行のデータ処理に掛かる時間を図ってみると、約0.25秒→約0.094秒と1/3に短縮します。また100行の処理でも約0.03秒→0.016秒と半減するので、処理時間の点から有利だと思います。但し、行高さを取得する必要があるため、図5-9のフィルターOffをする際に、行高さを標準に戻す作業は必須となります。(アドインなのでデータは改ざんされる事はない と判断できるのであれば、行高さを戻すコードは不要かもしれません。)

51行目「wcnt = T.HeaderRowRange.Count」は、テーブルの列数を取得しています。また52行目「icnt = T.DataBodyRange.SpecialCells(xlCellTypeVisible).Count」は、絞り込まれたデータの総数(絞り込まれた行数 × テーブルの列数)を取得します。
このデータを使って、54行目「ReDim buf2(1 To Int(icnt / wcnt), 1 To wcnt)」で、絞り込まれたデータの配列の大きさを設定しています。
処理行が絞りこまれている場合にだけ「ReDim Preserve buf2(・・・)」で1行ずつ格納する配列を広げていく手法もありますが、「絞り込みの結果が1行だった」場合には配列が「1次元配列」になってしまうため、後の処理が面倒になるために「最初に2次元配列を作って置き、そこにデータを格納していく」手法としました。

56~63行目では54行目で作成した配列buf2に、絞り込んだテーブルのデータを代入しています。
56行目「For i = 1 To T.DataBodyRange.Rows.Count」で、カウンタ変数iをテーブルの全データ行分だけ回します。
57行目「If Not T.DataBodyRange.Rows(i).Height = 0 Then」では、テーブルのその対象行の高さを調べ、「高さゼロでは無い(=絞り込まれた行)」場合に58~61行目を実行させます。ここで「行高さゼロ=絞り込みから外れた」の判断をしているため、図5-9の34行目の処理(行高さを標準にする)が必要になります。
58行目「k = k + 1」で、配列buf2の代入する行位置(k)を設定します。
59行目「For j = 1 To wcnt」で、配列buf2・配列buf1の列位置(j)を設定します。
60行目「buf2(k, j) = buf1(i, j)」で、配列buf1(=全データ)のデータを配列buf2(絞り込んだデータ)に代入します。

配列buf2への代入が完了しましたら、65行目「SearchList = buf2」で、この関数の戻り値にbuf2を設定します。
なお、テーブルにデータが1つも無い場合には「DataBodyRangeが存在しない」ためにエラーが発生します。または絞りこまれたデータが1行も無い(=全行が高さゼロ)場合には「52行目のicntがゼロ」となり、54行目で配列bufのサイズを決める時にエラーが発生します。
ですので、48行目「On Error Resume Next」で、エラーが出てもそのまま実行させることで、buf2は初期値のEmptyのままとなり、Emptyが関数SearchListの戻り値となります。
また初期値をEmptyにするため、41行目でのbuf2の宣言は「Dim buf2() As Variant」では無く「Dim buf2 As Variant」と配列では無い型での宣言が必要です。

5-5.データの最大値取得

重複しないユニーク番号を得る為に、テーブルの列内の最大値を取得する関数が図5-11です。引数として、対象のListObjectオブジェクト(T)、最大値を取得する列名(col)を受取ります。
  1. '========== ⇩(7) データの最大値取得 ============
  2. Public Function TableMax(T As ListObject, col As Variant) As Long
  3.  If Not T.ListRows.Count = 0 Then
  4.   TableMax = WorksheetFunction.Max(T.ListColumns(col).DataBodyRange)
  5.  End If
  6. End Function
図5-11

まず、72行目「If Not T.ListRows.Count = 0 Then」で、データが1つ以上存在する時に73行目で最大値を取得します。逆に言うとデータが一つも無い場合は、Long型の初期値のゼロを関数TableMaxは戻すことになります。
また「If Not T.ListRows.Count = 0 Then」とせずに「On Error Resume Next」として、73行目でエラーが出る(=データが1つも無く、DateBodyRangeが存在しない)場合にゼロを戻す方法もOKと思います。

73行目「TableMax = WorksheetFunction.Max(T.ListColumns(col).DataBodyRange)」では、後半「T.ListColumns(col).DataBodyRange」で「対象列のデータ範囲」に対してワークシート関数Maxを使って最大値を取得しています。

5-6.新規データ挿入(Insert文)

データ行を挿入(SQLのInsert文相当)するのが図5-12です。引数として、対象のListObjectオブジェクト(T)、挿入するデータの配列(Data)を受取ります。
79行目「With T.ListRows.Add」で、テーブルに新たな行を追加し、80行目「.Range = Data」でその追加行に配列Dataを貼り付けます。
  1. '========== ⇩(8) 新規データ挿入 ============
  2. Public Sub TableDataIn(T As ListObject, Data As Variant)
  3.  With T.ListRows.Add
  4.   .Range = Data
  5.  End With
  6. End Sub
図5-12

貼り付けるデータ「配列Data」は、テーブル(T)の列数と同じ1次元配列である必要があります。しかし、列数の不足や超過があっても、また2次元配列(=複数行のデータ)であってもエラーで止まる事はありません。
但し列が不足した場合には、不足したセルに「#N/A」が入ってしまいますので、その後思わぬ動作をする可能性が出てきます。列数が超過の場合は、超過分が単に無視されます。
また2次元配列を貼り付けようとしても、79行目で挿入される新たな行は1行のみですので、2次元配列の内の1行目のデータだけが貼り付くことになります。

なおAddメソッドは、追加したListRowオブジェクトを戻しているので、図5-13のように一旦変数で受け取ってからデータを貼り付ける書き方でもOKです。やっている事は同じなので、分かり易い方を使って下さい。
  1. '========== ⇩(9) 新規データ挿入(別の書き方) ============
  2. Public Sub TableDataIn2(T As ListObject, Data As Variant)
  3.  Dim newRow As ListRow    '←追加するテーブルの行
  4.  Set newRow = T.ListRows.Add
  5.  newRow.Range = Data
  6. End Sub
図5-13

5-7.データの変更(Update文)

データを変更(SQLのUpdate文相当)するのが図5-14・図5-15です。
SQLのUpdate文は、列を指定してデータを上書きしますが、今回システムでは「行のデータをまとめて書き換え」するプロシージャ(図5-15)も作成しました。まとめて書き換える理由は「どの列もデータが変更された可能性があるので、全列のデータを上書きをする」ためです。

まず、列指定でデータ変更するのが図5-14で、引数として、対象のListObjectオブジェクト(T)、変更する列名(col)、上書きするデータ(Data)を受取ります。ここでの引数Dataは配列では無く、単一データです。
  1. '========== ⇩(10) データの変更(1列のみ変更) ============
  2. Public Sub TableDataUp(T As ListObject, col As Variant, Data As Variant)
  3.  Dim icnt As Long    '←絞りこまれたデータ総数
  4.  On Error Resume Next
  5.   icnt = T.DataBodyRange.SpecialCells(xlCellTypeVisible).Count
  6.   If icnt = 0 Then Exit Sub
  7.  On Error GoTo 0
  8.  T.ListColumns(col).DataBodyRange = Data
  9. End Sub
図5-14

先に、本体である101行目を先に説明します。
101行目「T.ListColumns(col).DataBodyRange = Data」では、テーブル(T)の指定列(col)の、絞りこまれた(=行高さがゼロではない)行のデータを「引数Data」の値に置き換えます。

この時、もし絞り込まれた行が複数行であれば、複数行分だけ置き換えます。また絞り込まれた行が1つも無い場合には、テーブルの全ての行のデータが置き換わります(=テーブルを全く絞り込まずに、全行に対してデータを置き換えたのと同一)。
寄り道
この「絞り込まれた行が1つも無いのに、テーブルの全てのデータを書き換えてしまう」という現象には、今回初めて気が付きました。それまでは「絞り込まれた行が1つも無い場合は、1つもデータを書き換えない」だろう という先入観があり、確認を怠っていました。

これはSQLで言えば「where句で何も抽出されなかった時には、where句は無いものと見なす」のと同じです。この現象は異常だと思うのですが、今のところ受け入れるしかありません。これを防ぐには以下の2つの方法が考えられます。
 ・絞り込んだ時に1行も抽出されていない場合には、TableDataUp(Update相当)プロシージャを実行しない。
 ・TableDataUpプロシージャの中で、1行も抽出されていない場合はUpdate相当を実行しない。
今回は、最後の砦で防ぐ方が安心だと考え、2番目の方法を盛り込むことにしました。

なお、上記はマクロを使って絞り込みを行った場合のことです。手動で絞り込みを行うともっと不思議なことが起こります。
手動で何らかのデータ行を絞り込んだ(ゼロ行では無い)後、101行目「T.ListColumns(col).DataBodyRange = Data」のようなコードでデータを書き込むと、絞り込まれた行(可視されている行)に加えて絞り込まれなかった行(高さゼロの行)にもデータが書き込まれてしまうのです(つまり、全行が書き換えられてしまう)。
最初Excelの挙動がおかしくなったのかと思い再起動したりしたのですが、同じでした。ListObjectにはまだまだバグが潜んでいるのかもしれません。

少し大きなシステムにListObjectを使用する場合は、上記に充分注意する必要があると思います。今回システムではとりあえず正常に動きそうですが、絞り込んだテーブルに対して「テーブル名.DataBodyRange = 貼り付けるデータ」とSQLのUpdate文のように使うのでは無く、絞り込まれた行をまず検出して行位置を特定し、その行に対してデータ貼付けを実行する という様な方法を取るのが安心かと思います。
テーブル操作の旨味が失われてしまいますが、データがおかしくなるのでは元も子もありませんので、より確実なアルゴリズムの使用と相当の動作チェックが重要と思います。

97行目「icnt = T.DataBodyRange.SpecialCells(xlCellTypeVisible).Count」では、絞りこまれた(=行高さがゼロになっていない)行×データ列数=「絞りこまれたデータ総数」を取得します。もしそのデータ総数がゼロの場合には、97行目はエラーとなりますので、変数icntは初期値のゼロのままと言うことになります。
98行目「icnt = 0 Then Exit Sub」で、ゼロ(=絞り込まれたデータ行が無い)の場合は、プロシージャを抜け出して101行目を実行しないようにします。
なおテーブルにデータが1行も存在しない場合も101行目を実行させる訳にはいきません。データ行が空の場合は、97行目のDataBodyRangeの部分でエラーが発生しますので、96行目「On Error Resume Next」でエラーをスルーさせる必要があります。

一方、行のデータをまとめて書き換えるのが図5-15です。引数として、対象のListObjectオブジェクト(T)、上書きするデータ配列(Data)を受取ります。配列Dataは、テーブル(T)の列数分だけある1次元配列であることが前提です。
  1. '========== ⇩(11) データの変更(全列変更) ============
  2. Public Sub TableDataUpAll(T As ListObject, Data As Variant)
  3.  Dim icnt As Long    '←絞りこまれたデータ総数
  4.  On Error Resume Next
  5.   icnt = T.DataBodyRange.SpecialCells(xlCellTypeVisible).Count
  6.   If icnt = 0 Then Exit Sub
  7.  On Error GoTo 0
  8.  T.DataBodyRange = Data
  9. End Sub
図5-15

113行目「T.DataBodyRange = Data」では、テーブル(T)の絞りこまれた(=行高さがゼロではない)行の1行分のデータを「配列Data」の値に置き換えます。
イメージとしては図5-12の「TableDataIn」と似ていますが、異なるのは新たに行を追加することなく、既存のデータ行を置き換えることになります。
なお、絞り込まれた行が複数行であるなら複数行分だけ置き換えます。また絞り込まれた行が1つも無ければ、「よりみち」でも説明したように「テーブルの全データを書き換え」てしまいます。そのため図5-14と同等に、109行目「icnt = T.DataBodyRange.SpecialCells(xlCellTypeVisible).Count」で、絞りこまれたデータ総数を取得し、エラーが出た場合には110行目「icnt = 0 Then Exit Sub」で113行目を実行しないようにしています。

また、引数である配列Dataの列数の過不足についてはTableDataIn(図5-12)の時と同じです。
但し2次元配列の時は注意が必要です。もし絞り込まれたデータ行が2行あったとして、貼り付けるデータも2次元配列だとすると、テーブルの1行目には配列の1行目のデータが、テーブルの2行目には配列の2行目のデータが書き込まれることになり「各行で同じデータが上書きされない」可能性があります。

6.操作用フォーム(UserForm1)

6-1.フォーム上レイアウト

今回システムは、「ToDoリストの閲覧と処理(Page1)」「新規タスク追加・既存タスク更新(Page2)」「完了・中止項目の閲覧と処理(Page3)」の3つの機能を1つのフォーム内に収めています。その切替にはMultiPageコントロールを使用します。

まず図6-1のように、フォーム一杯にMultiPageコントロールを配置します。そのMultiPageのPage1(ToDoリスト)の中にListBox1を配置し、その上側に操作ボタンを3つ配置しています。リストは表面的には4列(非表示列があるため、本当は7列)にデータを表示し、その列に対応するようにタイトルLabelを配置しています。Labelの配置位置は、適当な位置に手動で合わせました。
1ページのレイアウト
図6-1

Page2の「新規/更新」は図6-2のように、ユーザーが手入力する「タスク名」「完了日」のTextBoxを置き、優先度を選択するようにComboBoxを置いています。また下部には定期的なタスクをON-OFFするCheckBoxと、定期の内容をOptionButtonを使って指定できるようにしました。
また上部には操作ボタンを2つ置き、タスク修正の場合の内部的処理のためにタスクNoを書き込むLabelを置いています。
2ページのレイアウト
図6-2

Page3の「完了一覧」は、完了・中止タスク項目を表示するためのListBoxを配置し、その上部に操作ボタンを置いています。このListBoxも表面的には4列表示ですが、非表示も含めて7列のデータが入ります。
3ページのレイアウト
図6-3

タスク名の文字列長さを「それほど長くは無い」と仮定して、今回はこのサイズにしています。もしタスク名が長い場合はListBox等のWidthを調整して下さい。

6-2.フォームモジュール

6-2-1.フォーム起動時

フォーム起動時(=システム起動時)には、まず図6-4のInitializeイベントが発生します。その中でフォーム内の固定的な表示等の設定をしています。
  1. '========== ⇩(12) フォーム起動時の設定 ============
  2. Private Sub UserForm_Initialize()
  3.  Me.Caption = "ToDoリスト"
  4.  Me.MultiPage1.Pages(0).Caption = "ToDo"
  5.  Me.MultiPage1.Pages(1).Caption = "新規/更新"
  6.  Me.MultiPage1.Pages(2).Caption = "完了一覧"
  7.  Me.CommandButton1.Caption = "完了"
  8.  Me.CommandButton2.Caption = "編集"
  9.  Me.CommandButton3.Caption = "中止"
  10.  Me.CommandButton4.Caption = "登録"
  11.  Me.CommandButton5.Caption = "クリア"
  12.  Me.CommandButton6.Caption = "復活"
  13.  Me.ListBox1.ColumnCount = 7
  14.  Me.ListBox1.ColumnWidths = "0;105;20;55;0;40;0"
  15.  Me.TextBox2.IMEMode = fmIMEModeDisable
  16.  Me.ListBox2.ColumnCount = 7
  17.  Me.ListBox2.ColumnWidths = "0;70;0;55;55;0;40"
  18. End Sub
図6-4

119~122行目では、フォーム名称・マルチページのページタブ名を設定しています。
119行目「Me.Caption = "ToDoリスト"」は、フォームのタイトル設定。
120行目「Me.MultiPage1.Pages(0).Caption = "ToDo"」は、マルチページ内のPage1のタブ名の設定。
121行目「Me.MultiPage1.Pages(1).Caption = "新規/更新"」は、Page2のタブ名の設定。
122行目「Me.MultiPage1.Pages(2).Caption = "完了一覧"」は、Page3のタブ名の設定をします。

124~131行目では、各ページのボタンの名称を設定しています。
124行目「Me.CommandButton1.Caption = "完了"」は、Page1のボタン名設定です。ToDoリストの項目を選択した後、このボタンをクリックすることで「項目の完了」処置をします。
125行目「Me.CommandButton2.Caption = "編集"」もPage1のボタン名設定です。ToDoリストの項目を選択した後、このボタンをクリックすることで、データをPage2に送付し、項目の編集をします。
126行目「Me.CommandButton3.Caption = "中止"」もPage1のボタン名設定です。ToDoリストの項目を選択した後、このボタンをクリックすることで「項目の中止」処置をします。
128行目「Me.CommandButton4.Caption = "登録"」は、Page2のボタン名設定です。Page2に新規入力された内容、及びPage1・Page3から送られてきたデータを修正した内容を、データとして新規登録・修正上書きをします。
129行目「Me.CommandButton5.Caption = "クリア"」もPage2のボタン名設定です。このボタンをクリックすることで、Page2の各入力欄をクリアします。
131行目「Me.CommandButton6.Caption = "復活"」は、Page3のボタン名設定です。完了一覧の項目を選択した後、このボタンをクリックすることで、データをPage2に送付し、項目の編集・新規作成を行います。

133~134行目は、Page1(ToDoリスト)のListBox1の列設定です。
133行目「Me.ListBox1.ColumnCount = 7」は、7列のListBoxに設定しています。データ用シート(図4-1)のデータ列数と同じ7列にすることで「全データをListBoxデータに格納」し、「処理時に再度データ用シートからデータを呼び出さなくても良い」ようにしています。
134行目「Me.ListBox1.ColumnWidths = "0;105;20;55;0;40;0"」は、7列のListBoxの列幅を調整しています。非表示列はゼロとし、その合計値を「ListBoxのWidth値(今回は223ポイント)-3ポイント」とすることで「ListBoxの横スクロールバーを表示させない」ようにしています。この計算の詳細は「先入先出の入出庫管理システム」を参照下さい。

136行目「Me.TextBox2.IMEMode = fmIMEModeDisable」では、Page2(新規/更新)の日付欄(TextBox2)を半角のみに設定しています。

138~139行目は、Page3(完了一覧)のListBox2の列設定です。
138行目「Me.ListBox2.ColumnCount = 7」は、7列のListBoxに設定しています。ToDoリスト(Page1)のListBoxと同じく、データ用シートのデータ列数と同じ7列にしています。
139行目「Me.ListBox2.ColumnWidths = "0;70;0;55;55;0;40"」は7列のListBoxの列幅を調整しています。

フォーム起動時にInitializeイベントの次に呼び出されるのがActivateイベント(図6-5)です。今回システムはフォームをShow ⇔ Hideを繰り返すものでは無いので、図6-5の内容は図6-4(Initialize)の中に記しても良いのですが、ボタン名など固定している設定では無く、都度変化する内容であるため、Activateイベント内で実行することにしました。
  1. '========== ⇩(13) フォーム表示時の設定 ============
  2. Private Sub UserForm_Activate()
  3.  Call make_List1
  4.  Call make_List2
  5.  Call make_Combo1
  6.  Me.MultiPage1.Value = 0
  7.  Call CheckBox1_Click
  8. End Sub
図6-5

146行目「Call make_List1」は図6-6を呼出し、Page1(ToDoリスト)のリストボックスにToDoリストを並べます。
147行目「Call make_List2」は図6-18を呼出し、Page3(完了一覧)のリストボックスに完了・中止リストを並べます。
148行目「Call make_Combo1」は図6-10を呼出し、Page2(新規/更新)の優先度選択欄のコンボボックスにA~Cの優先度リストを作成しています。

150行目「Me.MultiPage1.Value = 0」は、システム起動後、初めに表示するページをPage1(ToDoリスト)とします。
152行目「Call CheckBox1_Click」は図6-16を呼出します。CheckBox1は初期状態をOFFにしてあるため、Clickイベントを呼び出すことで「定期的チェックボックスのレ点を外した動作を再現」させ、起動初期状態にはオプションボタン(毎日・毎週・毎月・毎年)を全て無効状態にしています。

6-2-2.Page1(ToDoリスト)

6-2-2-1.ToDoリスト作成
フォーム起動時、およびデータを追加・変更した時に呼び出される「ListBox1のリストを作成」するのが図6-6です。
  1. '========== ⇩(14) ToDoリスト作成 ============
  2. Private Sub make_List1()
  3.  Dim buf As Variant   '←絞りこまれたデータの配列
  4.  Dim i As Long      '←データの行数
  5.  Dim j As Integer    '←データの列数
  6.  Call TableSort(T1, "Priority", 1)
  7.  Call TableSort(T1, "EndD", 1)
  8.  Call TableFilter(T1, "Status", "=")
  9.  Call TableFilter(T1, "EndD", "<=" & Date + Term1)
  10.  buf = SearchList(T1)
  11.  Call TableFilterOff(T1)
  12.  Me.ListBox1.Clear
  13.  If IsEmpty(buf) = True Then Exit Sub
  14.  For i = 1 To UBound(buf, 1)
  15.   Me.ListBox1.AddItem ""
  16.   For j = 1 To UBound(buf, 2)
  17.    Me.ListBox1.List(i - 1, j - 1) = buf(i, j)
  18.   Next j
  19.  Next i
  20. End Sub
図6-6

161~164行目では、テーブルのデータを「ToDoリストとして表示するデータ」に並べ替え・絞り込みを行っています。
まずToDoリストに表示するのは①「完了・中止されていないもの」であり、且つ②「ToDoリストに載せる期間内」の項目です。②掲載期間は、図5-1の5行目で設定した「定数Term1(サンプルファイルでは7日間)」を使用します。また項目が完了・中止されると、図4-1のようにFinD列に日付を、Status列に完了か中止の文字列を入れますので、これで判断していきます。
また表示する順序は、どの項目を優先にするかで決めます。今回は、③「期限(EndD列)」、④「優先度(Priority列)」の優先順で表示することとします。

この①~④をどの順で実行していくか ですが、まず表示順序の並べ替えを行います。③と④が順序ですが「最後に並べ替えた方が優先される」ので、まず優先度の低い④で並べ替えを行います。これが161行目「Call TableSort(T1, "Priority", 1)」です。
図5-3のTableSortを呼び出し、Priority列を昇順(A・B・C順)で並べています。並べ順序は図5-4の表に従って「1(またはxlAscending)」を指定します。
次に優先度の高い③で並べ替えを行うのが、162行目「Call TableSort(T1, "EndD", 1)」です。EndD列を昇順(日付の若い順)で並べます。

次に①②の絞り込みを行います。この絞り込み順番はどちらでも良いので、163行目「Call TableFilter(T1, "Status", "=")」でStatus列が空のもの(=完了・中止されていないもの)を絞り込み、次に164行目「Call TableFilter(T1, "EndD", "<=" & Date + Term1)」で「期限(EndD)が、今日+設定期間よりも前のもの」を絞りこみます。
「今日+設定期間よりも前のもの」ですので、「今日の期限を過ぎているのに未完了のタスク」も表示されることになります。

データの並び替え、および絞り込みが完了しましたので、166行目「buf = SearchList(T1)」で図5-10のSearchListを呼び出し、データを配列化し変数bufに代入します。配列bufは必要なデータが必要な順序で並んでいます。もし対象となるデータが一つもない場合には、Emptyが戻ってきます。
データの取り込みが完了しましたので、168行目「Call TableFilterOff(T1)」で、図5-9を呼出し、絞り込みを解除します。この際に並び替えは特に戻す必要が無いので、そのままの状態としています。

170~178行目では、取得したデータ配列bufを使って、リストボックスを作成します。
170行目「Me.ListBox1.Clear」で、リストボックスのデータをクリアします。
171行目「If IsEmpty(buf) = True Then Exit Sub」では、データが1行も無かった(Empty)場合に、プロシージャを抜け出します。170行目で既にリストボックスをクリアしていますので、データが無い場合は空のリストボックスになります。

173行目「For i = 1 To UBound(buf, 1)」は、カウンタ変数iで、絞り込んだデータbufの行位置を動かします。
174行目「Me.ListBox1.AddItem ""」で、データの各行ごとにリストボックスの行を作ります。
175~177行目で、リストボックスの各列にデータを収めていきます。まず175行目「For j = 1 To UBound(buf, 2)」では、カウンタ変数jを配列bufの列数(=データシートの列数)分だけ回します。そして176行目「Me.ListBox1.List(i - 1, j - 1) = buf(i, j)」で、各列の値をリストボックスに入れていきます。

6-2-2-2.ToDoページの各ボタンの動作
Page1(ToDo)には3つのボタンを設けました。機能は以下の通りです。
「完了ボタン」は、ToDoリスト上の選択した項目に対して完了処置を行います。
「編集ボタン」は、ToDoリスト上の選択した項目の内容を編集します。
「中止ボタン」は、ToDoリスト上の選択した項目に対して中止処置を行います。

図6-7は、完了ボタンをクリックした時に実行されるClickイベントです。
184行目「Call FinStop("完了")」は、図6-21を呼出し、完了処置を行います。引数としてデータテーブルのStatus列に記入する「完了」の文字列を渡します。
  1. '========== ⇩(15) 完了ボタン ============
  2. Private Sub CommandButton1_Click()
  3.  Call FinStop("完了")
  4. End Sub
図6-7

図6-8は、編集ボタンをクリックした時に実行されるClickイベントです。
189行目「Call Edit(Me.ListBox1)」は、図6-23を呼出し、編集のモード(登録/更新ページ)に移動します。引数として選択しているToDoリスト(ListBox1オブジェクト)を渡します。
  1. '========== ⇩(16) 編集ボタン ============
  2. Private Sub CommandButton2_Click()
  3.  Call Edit(Me.ListBox1)
  4. End Sub
図6-8

図6-9は、中止ボタンをクリックした時に実行されるClickイベントです。
194行目「Call FinStop("中止")」は、完了の時と同じ図6-21を呼出します。但し引数としてデータテーブルのStatus列に記入する「中止」の文字列を渡して、中止処置を行います。
  1. '========== ⇩(17) 中止ボタン ============
  2. Private Sub CommandButton3_Click()
  3.  Call FinStop("中止")
  4. End Sub
図6-9

6-2-3.Page2(新規/更新)

6-2-3-1.優先度コンボボックスの設定
新規/更新ページでは、タスクや期限日が必須入力項目ですが、同時に優先度も必須としています。その優先度は今回コンボボックスから選択する方式とし、その選択リストを設定するのが図6-10です。図6-5(Activateイベント)の148行目から呼び出されて実行します。
  1. '========== ⇩(18) 優先度コンボボックスの作成 ============
  2. Private Sub make_Combo1()
  3.  Me.ComboBox1.AddItem "A"
  4.  Me.ComboBox1.AddItem "B"
  5.  Me.ComboBox1.AddItem "C"
  6.  Me.ComboBox1.ListIndex = 0
  7. End Sub
図6-10

199行目「Me.ComboBox1.AddItem "A"」でコンボボックスのリストに、優先度「A」を設定します。その後、200行目で「B」、201行目で「C」を設定しますので、リストとしては、上からA・B・Cと並びます。
203行目「Me.ComboBox1.ListIndex = 0」では、コンボボックスの1番目(リストはゼロ始まり)を選択していますので、起動直後は一番上の「A」が表示されていることになります。

6-2-3-2.新規/更新の実行
新規/更新ページの各項目を入力後、「登録」ボタンをクリックした時に呼び出されるのが図6-11です。
  1. '========== ⇩(19) 登録ボタン ============
  2. Private Sub CommandButton4_Click()
  3.  Dim buf(1 To 7) As Variant    '←テーブルへ貼り付けるデータ配列
  4.  buf(2) = Me.TextBox1.Text
  5.  buf(4) = Me.TextBox2.Text
  6.  If Not Me.ComboBox1.ListIndex = -1 Then
  7.   buf(3) = Me.ComboBox1.List(Me.ComboBox1.ListIndex)
  8.  End If
  9.  If Trim(buf(2)) = "" Or Trim(buf(3)) = "" Or Trim(buf(4)) = "" Then
  10.   MsgBox "「タスク・優先度・完了日」は必須入力です"
  11.   Exit Sub
  12.  End If
  13.  If Me.CheckBox1.Value = True Then
  14.   Select Case True
  15.    Case Me.OptionButton1.Value
  16.     buf(6) = "毎日"
  17.    Case Me.OptionButton2.Value
  18.     buf(6) = "毎週"
  19.    Case Me.OptionButton3.Value
  20.     buf(6) = "毎月"
  21.    Case Me.OptionButton4.Value
  22.     buf(6) = "毎年"
  23.   End Select
  24.  End If
  25.  If Me.Label1.Caption = "" Then
  26.   buf(1) = TableMax(T1, "Num") + 1
  27.   Call TableDataIn(T1, buf)
  28.  Else
  29.   buf(1) = Me.Label1.Caption
  30.   Call TableFilter(T1, "Num", CStr(buf(1)))
  31.   Call TableDataUpAll(T1, buf)
  32.   Call TableFilterOff(T1)
  33.   Me.MultiPage1.Value = 0
  34.  End If
  35.  Call CommandButton5_Click
  36.  Call make_List1
  37.  Call make_List2
  38.  ThisWorkbook.Save
  39. End Sub
図6-11

図6-12で、データを保管するテーブルの内容を再確認しておきます。
列名内容Page2の入力コントロール
1Num連番Label1
2Taskタスク* TextBox1
3Priority優先度(A・B・C)* ComboBox1
4EndD期限* TextBox2
5FinD完了・中止日(無印は、未完)-
6Every定期の内容(無印は、1回のみ)OptionButton1~4
7Status状態(無印は、未完)-
図6-12

新規/更新ページでは、2・3・4の項目を入力必須としていますので、まず2・3・4にデータを入れてみてから、入っているか否かを確認していきます。
210行目「buf(2) = Me.TextBox1.Text」で、タスク名を配列bufに代入します。
211行目「buf(4) = Me.TextBox2.Text」で、期限日を配列bufに代入します。

213行目「If Not Me.ComboBox1.ListIndex = -1 Then」は、「正しくリストを選択した時」という意味になります。コンボボックスのテキストボックス部をDelete等で消したり、A・B・C以外の文字列を入力した場合には「ListIndex = -1(=リストからは何も選んでいない)」ことになります。
正しく選択した時は、214行目「buf(3) = Me.ComboBox1.List(Me.ComboBox1.ListIndex)」で、選択した文字列(A・B・Cのどれか)を配列bufに代入します。

なお、Initialize(図6-4)などで「Me.ComboBox1.MatchRequired = True」と設定し、「リスト内にある項目しか、テキストボックス部に入力できない」ようにすると、コンボボックスを出る時に「プロパティの値が無効です」とExcelが代わりに指摘してくれます(エラーのコメント内容が、ちょっと素人っぽくありませんが)。
また、「Me.ComboBox1.Style = fmStyleDropDownList」と設定することで、「リスト項目からしか選択できない」ようにもできます。

ここまでで入力必須項目は取得できましたので、217行目「If Trim(buf(2)) = "" Or Trim(buf(3)) = "" Or Trim(buf(4)) = "" Then」で、各値が空か否かを調べます。スペースだけ入れる人もいるため、Trim関数で両端のスペースは削除してから「""(長さゼロの文字列)」と比較を行っています。
もしどれかが成立(値が空)する時は、218行目「MsgBox "「タスク・優先度・完了日」は必須入力です"」でコメントを出し、219行目「Exit Sub」で登録処理を中止します。

222~233行目は、定期的な項目として指定しているか否かをチェックしています。
まず222行目「If Me.CheckBox1.Value = True Then」で、「定期的」チェックボックスにレ点が付いているか否かを確認します。レ点がついている場合には223~232行目を実行します。
223行目「Select Case True」は、以下のCase文の式(4つのオプションボタンの値)がTrueである場合に、それ以下を実行します。例えば224行目は「Case Me.OptionButton1.Value」ですので、OptionButton1(=毎日)がTrue(=ONになっている) の場合に225行目「buf(6) = "毎日"」が実行され、配列bufに「毎日」が代入されることになります。
4つのOptionButtonはどれかがOnになっています(図6-16の274行目で、起動時に「毎年」をONにしているため)ので、チェックボックスにレ点がついている場合は、どれかがbuf(6)に入ることになります。
(チェックボックスにレ点が付いていない場合でも「毎年」がONになっていますが、222行目のIF文で対象となりませんので、buf(6)は初期の空の状態になります。)

235行目「If Me.Label1.Caption = "" Then」では、Page2(新規/更新ページ)のLabel1の値を確認しています。
まず新規/更新ページに移動してくるには、図6-13のように4つの経路があります。
編集時のLabel1の値
図6-13

1つ目は、ユーザーがMultiPageのタブをクリックして新規/更新ページに移動してきた場合①です。この時には、図6-20のMultiPage1_Changeイベントが呼び出され、その中で図6-15のCommandButton5_Clickを実行することで、Label1に「""(長さゼロの文字列)」を入力(256行目)しています。
2つ目は、ToDoページで項目選択後に「編集ボタン」をクリックすることで移動してきた場合②です。この時はEditプロシージャ内でLabel1に選択項目の番号を記入(図6-23の386行目)します。
3つ目・4つ目は、完了一覧ページで項目選択後に「復活ボタン」で移動してきた場合です。復活ボタンをクリックした後のEditプロシージャ(図6-23)の中で、選択した項目が完了だった場合③にはLabel1に「""(長さゼロの文字列)」を入力(384行目)しています。また中止だった場合④には、Label1に選択項目の番号を記入(386行目)します。
以上をまとめると、図6-14のようになります。
移動前のページ条件Label1の値理由
ToDo / 完了一覧手動で移動""ユーザー意思による新規追加
ToDo項目選択後、編集ボタン選択項目のNo編集目的
完了一覧完了項目を選択後、復活ボタン""完了実績は保管の必要がある為、内容コピーし新規処理
中止項目を選択後、復活ボタン選択項目のNo中止を復活させる意思
図6-14

繰返しになりますが、235行目「If Me.Label1.Caption = "" Then」では、Label1の値を確認し、Label1の値が無い場合(図6-13、図6-14の①③)は、236~237行目を実行します。
またそれ以外(Label1の値がある)の場合(図6-13、図6-14の②④)は、239~243行目を実行します。

まずLabel1の値が無い場合(図6-13、図6-14の①③)は、新規に番号を取得する必要があります(今回のデータテーブルでは、タスク番号はユニーク番号である必要があるため)ので、236行目「buf(1) = TableMax(T1, "Num") + 1」で、図5-11を呼出し「Num列の最大値」に+1を足してユニーク番号とし、bufに代入しています。
そして237行目「Call TableDataIn(T1, buf)」で、配列bufを図5-12のTableDataInに渡して、データをテーブルに新規追加します。
なお、buf(5)・buf(7)には何も代入していませんので、FinD列とStatus列は空白セルとなるため未完項目として登録されることになります。

一方Label1の値がある場合(図6-13、図6-14の②④)は、239行目「buf(1) = Me.Label1.Caption」でその値(タスク番号)をbufに代入します。
このデータをテーブルの「同じタスク番号の行」に上書きしなければなりませんので、まず240行目「Call TableFilter(T1, "Num", CStr(buf(1)))」を使ってテーブルをタスク番号の行だけに絞り込みます。
絞り込んだ後、241行目「Call TableDataUpAll(T1, buf)」で、その行のデータ全てを配列bufのデータで置き換えます。なお絞り込んだNum列の値と貼り付けるNum列の値は同じですので、「タスク番号以外のデータは置き換えられた」という形になります。

最後に242行目「Call TableFilterOff(T1)」でテーブルの絞り込みを解除し、243行目「Me.MultiPage1.Value = 0」で「ToDoページに移動」しています。
寄り道
この「新規/更新を実行した後に、どのページに移動するか。またはそのページに留まるのか」は、作る人の考え方次第です。今回Label1に値が無い(=新規に項目を追加したい等)場合は移動せず、値がある場合はToDoページに移動させています。

私としては、まず「新規に項目を起こす場面では、複数の項目を作成する場合が多いだろう」と考え、新規作成の場合にはページ移動をさせずに連続入力が出来るようにしました。
一方「修正(Label1が有る)の場面は、それほど頻度が多く無いだろう。まして完了や中止を復活させる事は少ないだろう」と考え、修正後はToDoページに移動することとしました。このような仕様は、考えれば考えるほど難しいです。

多機能のシステムであれば、ユーザーが使い易いような詳細設定を出来るようにするのでしょうが、このような小さなシステム・個人しか使用しないようなシステムでは、使用環境に合わせてコード修正する方が良いのかもしれません。

246行目「Call CommandButton5_Click」は、図6-15を呼出し、新規/更新ページの各入力欄をクリアしています。これは新規に項目を起こしている(Label1が空欄)場合に、次の項目を入れ易くしているつもりなのと併せて、ページが移動しない+手入力欄をクリアしないと「処理が失敗したのでは?」との不安をユーザーが抱くと思い、クリアしています。

248行目「Call make_List1」は、図6-6を呼出し、ToDoリストを更新しています。
249行目「Call make_List2」は、図6-18を呼出し、完了一覧を更新しています。
250行目「ThisWorkbook.Save」は、更新したデータを保存しています。
寄り道
今回システムではデータをアドインファイルのワークシートに保存しています。システムが起動している時はもちろん、Excelが起動している最中はデータは残っていますが、Excelを閉じてしまうと、保存しておかない限りデータは消えてしまいます。
これは、Excelで資料を作ったのに保存せずにそのまま閉じてしまったのと同じです。しかしアドインの場合はもっと悪いことに、アドインのワークシートに変更を加えても、Excelを閉じる時に「このファイルの変更内容を保存しますか?」と訊いてきてくれないのです。

そのため変更を加えたタイミングでデータ保存しているのですが、もう一つ思いつくタイミングがフォームの「UserForm_QueryClose」イベントと「UserForm_Terminate」イベントです。この2つのイベントは、フォームを閉じる(例えば右上×印など)ときにはもちろん発生しますが、今回のようにフォームをモードレスで起動している時にExcelを終了されるとイベントは発生してくれません。

そのため、データテーブルに変更を加えた直後の250行目で「ThisWorkbook.Save」と、データ保存をしています。もう一箇所、タスク項目に対して完了・中止の処理をしている図6-21の367行目の、合計2箇所で保存実行しています。

6-2-3-3.入力欄のクリア
新規/更新ページの「クリア」ボタンをクリックした時に実行されるのが図6-15です。なお、手動・自動に関わらずマルチページのページが変更された時(図6-20の329行目)、及び登録ボタンでデータ登録した時(図6-11の246行目)にも呼び出されます。
  1. '========== ⇩(20) クリアボタン ============
  2. Private Sub CommandButton5_Click()
  3.  Me.Label1.Caption = ""
  4.  Me.TextBox1.Text = ""
  5.  Me.ComboBox1.ListIndex = 0
  6.  Me.TextBox2.Text = ""
  7.  Me.CheckBox1.Value = False
  8. End Sub
図6-15

256行目「Me.Label1.Caption = ""」は、タスク番号を表示しているLabel1をクリアしています。
257行目「Me.TextBox1.Text = ""」は、タスク名の入力欄をクリアしています。
258行目「Me.ComboBox1.ListIndex = 0」は、優先度を既定の「A」に戻しています。
259行目「Me.TextBox2.Text = ""」は、期限日の入力欄をクリアしています。
260行目「Me.CheckBox1.Value = False」は、定期的タスクのチェックボックスのレ点を消しています。もしチェックボックスが「レ点有→レ点無」に変更された場合は、図6-16のCheckBox1_Clickイベントが呼び出され、チェックボックスのレ点が消えるのと併せて、4つのオプションボタンも無効状態にします。

6-2-3-4.定期タスクの設定
定期的タスクのチェックボックスが「レ点無→レ点有」または「レ点有→レ点無」と、値が変わったときに呼び出されるのが図6-16です。また、フォームが起動する時にも初期設定として呼び出されます(図6-5の152行目)。
  1. '========== ⇩(21) 定期タスクの設定 ============
  2. Private Sub CheckBox1_Click()
  3.  Dim CKB As Boolean    '←定期ON-OFFのチェックボックスの値
  4.  CKB = Me.CheckBox1.Value
  5.  Me.OptionButton1.Enabled = CKB
  6.  Me.OptionButton2.Enabled = CKB
  7.  Me.OptionButton3.Enabled = CKB
  8.  Me.OptionButton4.Enabled = CKB
  9.  Me.OptionButton4.Value = True
  10. End Sub
図6-16

267行目「CKB = Me.CheckBox1.Value」では、チェックボックスの値(False または True)を取得し、変数CKBに代入します。269~272行目では、4つのオプションボタンの有効・無効を切り替えています。
代表して269行目「Me.OptionButton1.Enabled = CKB」で説明すると、OptionButton1(毎日のボタン)のEnabledプロパティをチェックボックスの値と同じに設定しています。つまり、チェックボックスがON(レ点有り)の時にはオプションボタンを有効にし、チェックボックスがOFF(レ点無し)の時にはオプションボタンを無効にしています。
他のオプションボタン(270~272行目)も同様に、チェックボックスのON-OFFに従って有効・無効を切り替えます。

274行目「Me.OptionButton4.Value = True」は、4つのオプションボタンの内、「毎年」のボタンだけをONにしています。4つのオプションボタンのGroupNameプロパティは全て未設定(作ったまま)なので4つのボタンは1つのグループになっています。ですのでOptionButton4をONにすれば、他の3つのOptionButton1~3はOFFになります。
「毎年」を既定のONにしたのは、間違えてチェックボックスをONにしてしまって登録をしても、最もスパンの長い「毎年」であれば定期的にToDoリストに表示されても被害が少ないと考えたからです。

6-2-3-5.期限日の入力欄の制限
期限の日付欄(TextBox2)には、yyyy/mm/ddのような半角数字を「/」や「-」で区切った日付を入力します。ユーザーに全角で入力されないように「半角Only」に制限するのは、図6-4の136行目で設定していますが、入力された文字列が日付か否かを判断し、かつデータテーブルに同一の形でデータ保管するために、書式を整えるのが図6-17です。
このイベントは、日付欄を抜け出す(Exit)ときに発生します。
  1. '========== ⇩(22) 日付チェックと成形 ============
  2. Private Sub TextBox2_Exit(ByVal Cancel As MSForms.ReturnBoolean)
  3.  Dim TXB As String    '←ユーザーが入力した文字列
  4.  TXB = Trim(Me.TextBox2.Value)
  5.  If IsDate(TXB) = True Then
  6.   Me.TextBox2.Value = Format(TXB, "yyyy/mm/dd")
  7.  ElseIf TXB = "" Then
  8.   Me.TextBox2.Value = ""
  9.  Else
  10.   Cancel = True
  11.   MsgBox "日付を入力して下さい"
  12.  End If
  13. End Sub
図6-17

282行目「TXB = Trim(Me.TextBox2.Value)」は、日付入力欄の値を取得し変数TXBに代入します。その際、Trim関数を使って両端のスペースを削除しています。
284行目「If IsDate(TXB) = True Then」は、その入力値が日付の形であった場合には、285行目「Me.TextBox2.Value = Format(TXB, "yyyy/mm/dd")」で日付をyyyy/mm/ddの形に整え、入力欄に戻します。例えば「12/2」などと入力した場合に、ちゃんと年も含めて「2021/12/02」という形にします。これにより、ユーザーも年月日を再チェックすることが出来ます。

286行目「ElseIf TXB = "" Then」は、入力値が日付の形では無くスペースだけだった、または一旦入力欄に入った後、何も入力せずに抜け出した時には、287行目「Me.TextBox2.Value = ""」で「入力したスペースを削除」した「""(長さゼロの文字列)」を入力欄に戻します。これは、データを「登録」の際のチェック(図6-11の217行目)として「空か、空では無いか」だけを判断材料にしているためです。

288行目「Else」は、日付の形でも無く、空やスペースだけでも無い場合です。この場合は「ユーザーは日付と思って入力したけど、日付の形になっていなかった」ことになりますので、289行目「Cancel = True」で入力欄を抜け出す(Exit)のを中止(Cancel)させて入力欄に留まります。そして290行目「MsgBox "日付を入力して下さい"」でコメントを出します。
ですので正しい日付を入力し直すか、または入力値を消すかしないと、入力欄を抜け出せなくなります。

6-2-4.Page3(完了一覧)

フォーム起動時、およびデータを追加・変更した時に呼び出される「ListBox2のリスト(完了一覧リスト)を作成」するのが図6-18です。
  1. '========== ⇩(23) 完了一覧リストの作成 ============
  2. Private Sub make_List2()
  3.  Dim buf As Variant   '←絞りこまれたデータの配列
  4.  Dim i As Long      '←データの行数
  5.  Dim j As Integer    '←データの列数
  6.  Call TableSort(T1, "Priority", 1)
  7.  Call TableSort(T1, "EndD", 2)
  8.  Call TableSort(T1, "FinD", 2)
  9.  Call TableFilter(T1, "Status", "<>")
  10.  Call TableFilter(T1, "FinD", ">=" & Date - Term2)
  11.  buf = SearchList(T1)
  12.  Call TableFilterOff(T1)
  13.  Me.ListBox2.Clear
  14.  If IsEmpty(buf) = True Then Exit Sub
  15.  For i = 1 To UBound(buf, 1)
  16.   Me.ListBox2.AddItem ""
  17.   For j = 1 To UBound(buf, 2)
  18.    Me.ListBox2.List(i - 1, j - 1) = buf(i, j)
  19.   Next j
  20.  Next i
  21. End Sub
図6-18

コードの流れは、図6-6(make_List1)とほぼ一緒です。
300~304目では、テーブルのデータを「完了一覧のリストとして表示するデータ」に並べ替え・絞り込みを行います。
まず完了一覧に表示するのは①「完了・中止したもの」であり、且つ②「完了一覧リストに載せる期間内」の項目です。②掲載期間は、図5-1の6行目で設定した「定数Term2(サンプルファイルでは30日=約1か月)」を使用します。また項目が完了・中止すると、図4-1のようにFinD列に日付を、Status列に完了か中止の文字列が入りますので、これで判断をしていきます。
また表示する順序は、どの項目を優先にするかで決めます。今回は、③「完了・中止日(FinD列)」、④「期限(EndD列)」、⑤「優先度(Priority列)」の優先順で表示することとします。

まず表示順序の並べ替えを行います。「最後に並べ替えた方が優先される」ので、優先度の低い順に、⑤→④→③の順番で並べ替えを行います。
300行目「Call TableSort(T1, "Priority", 1)」で、図5-3のTableSortを呼び出し、Priority列を昇順(A・B・C順)で並べています。並べ順序は図5-4の表に従って「1(またはxlAscending)」を指定します。
301行目「Call TableSort(T1, "EndD", 2)」で、EndD列を降順(日付の大きい順)で並べます。これは「最近完了や中止した項目がリストの上の方に並ぶ」ようにするためです。
302行目「Call TableSort(T1, "FinD", 2)」で、FinD列を降順(日付の大きい順)で並べます。この並び順もEndD列と同じです。

次に①②の絞り込みを行います。この順番は気にする必要はありませんので、303行目「Call TableFilter(T1, "Status", "<>")」でStatus列が空では無いもの(=完了・中止のもの)を絞り込み、次に304行目「Call TableFilter(T1, "FinD", ">=" & Date - Term2)」で「期限(FinD)が、今日ー設定期間よりも後のもの」を絞りこみます。

データの並び替え、および絞り込みが完了しましたので、306行目「buf = SearchList(T1)」で図5-10のSearchListを呼び出し、データを配列化し変数bufに代入します。もし対象となるデータが一つもない場合には、Emptyが戻ってきます。
データの取り込みが完了しましたので、308行目「Call TableFilterOff(T1)」で、絞り込みを解除します。この際に並び替えは特に戻す必要が無いので、そのままの状態としています。

310~318行目では、取得したデータ配列bufを使って、リストボックスを作成します。
310行目「Me.ListBox2.Clear」で、リストボックスのデータをクリアします。
311行目「If IsEmpty(buf) = True Then Exit Sub」では、データが1行も無かった(Empty)場合に、プロシージャを抜け出します。310行目でリストボックスをクリアしていますので、データが無い場合は空のリストボックスになります。

313行目「For i = 1 To UBound(buf, 1)」は、カウンタ変数iで、絞り込んだデータ配列bufの行位置を動かします。
314行目「Me.ListBox2.AddItem ""」で、データの各行ごとにリストボックスの行を作ります。
315~317行目で、リストボックスの各列にデータを収めていきます。まず315行目「For j = 1 To UBound(buf, 2)」では、カウンタ変数jを配列bufの列数(=データシートの列数)分だけ回します。そして316行目「Me.ListBox2.List(i - 1, j - 1) = buf(i, j)」で、各列の値をリストボックスに入れていきます。
以上で完了一覧のリストが完成します。

図6-19は、復活ボタンをクリックした時に実行されるClickイベントです。
324行目「Call Edit(Me.ListBox2)」は、図6-23を呼出し、編集のモード(登録/更新ページ)に移動します。引数として選択している完成一覧リスト(ListBox2オブジェクト)を渡します。
  1. '========== ⇩(24) 復活ボタン ============
  2. Private Sub CommandButton6_Click()
  3.  Call Edit(Me.ListBox2)
  4. End Sub
図6-19

6-2-5.共通プロシージャ

6-2-5-1.ページの変更
マルチページのページを変更した時に発生するChangeイベントが図6-20です。ユーザーが手動でページを移動した時のほか、マクロ側からページ移動(例えば、図6-23の379行目)を実行した時にも発生します。
内容は329行目「Call CommandButton5_Click」で図6-15を呼び出し、「新規/更新ページの各入力欄をクリア」します。
  1. '========== ⇩(25) ページ移動のイベント ============
  2. Private Sub MultiPage1_Change()
  3.  Call CommandButton5_Click
  4. End Sub
図6-20

6-2-5-2.完了・中止処置
ToDoリストページの完了ボタン(図6-7の184行目)、中止ボタン(図6-9の194行目)から呼び出されるのが図6-21です。引数として、「完了」または「中止」の文字列を受け取ります。(引数値FSは、Fin Stopの略)
  1. '========== ⇩(26) 完了・中止処理 ============
  2. Private Sub FinStop(FS As String)
  3.  Dim buf As Variant    '←絞り込みデータの配列
  4.  If Me.ListBox1.ListIndex = -1 Then
  5.   MsgBox "タスクを選択して下さい"
  6.   Exit Sub
  7.  End If
  8.  Call TableFilter(T1, "Num", Me.ListBox1.List(Me.ListBox1.ListIndex, 0))
  9.  buf = SearchList(T1)
  10.  Call TableDataUp(T1, "Status", FS)
  11.  Call TableDataUp(T1, "FinD", Date)
  12.  Call TableFilterOff(T1)
  13.  buf(1, 1) = TableMax(T1, "Num") + 1
  14.  Select Case buf(1, 6)
  15.   Case "毎日"
  16.    buf(1, 4) = DateAdd("d", 1, WorksheetFunction.Max(Date, buf(1, 4)))
  17.    Call TableDataIn(T1, buf)
  18.   Case "毎週"
  19.    buf(1, 4) = DateAdd("ww", 1, buf(1, 4))
  20.    Call TableDataIn(T1, buf)
  21.   Case "毎月"
  22.    buf(1, 4) = DateAdd("m", 1, buf(1, 4))
  23.    Call TableDataIn(T1, buf)
  24.   Case "毎年"
  25.    buf(1, 4) = DateAdd("yyyy", 1, buf(1, 4))
  26.    Call TableDataIn(T1, buf)
  27.  End Select
  28.  Call make_List1
  29.  Call make_List2
  30.  ThisWorkbook.Save
  31. End Sub
図6-21

336行目「If Me.ListBox1.ListIndex = -1 Then」は、リストボックスの項目が選択されていない場合に、337行目「MsgBox "タスクを選択して下さい"」でコメントを出し、338行目「Exit Sub」で処理を中止します。
このFinStopプロシージャは「ToDoリストページ」のみから呼び出されますので、確認するリストボックスは「ListBox1」で固定しています。

341行目「Call TableFilter(T1, "Num", Me.ListBox1.List(Me.ListBox1.ListIndex, 0))」では図5-7を呼出し、Num列の中の「ToDoリストの選択行のタスク番号」で絞り込みを行っています。タスク番号はListBox1では非表示となっていますが、1列目(インデックスとしては「ゼロ」)に収められていますので、値を取得するには「Me.ListBox1.List(Me.ListBox1.ListIndex, 0)」とします。

絞り込んだら、343行目「buf = SearchList(T1)」で、絞り込みデータを配列bufとして取得しておきます。この配列bufは、定期的タスクである場合に、新たなタスクとしてデータ追加する際に使用します。

この時点では、対象のタスク番号で絞り込まれた状態ですので、345~346行目で完了・中止の処理をします。
345行目「Call TableDataUp(T1, "Status", FS)」で、Status列に引数FS(完了処理の場合は「完了」、中止処理の場合は「中止」)を貼り付けます。
346行目「Call TableDataUp(T1, "FinD", Date)」で、FinD列に「今日の日付」を貼り付けます。
347行目「Call TableFilterOff(T1)」で、テーブルの絞り込みを解除します。

ここまでの処理で、一応「完了・中止処理は完成」です。349~363行目は「処理タスクが定期的タスクである場合、新たなタスクをデータ追加」する部分です。新たなタスクの内容は、タスク番号はもちろん異なる必要がありますが、それ以外は「期限日」が異なるだけです。新しい期限日は、定期的の内容(毎日・毎週・毎月・毎年)により異なります。

まず、新たなタスク番号を取得するために349行目「buf(1, 1) = TableMax(T1, "Num") + 1」で図5-11を呼出し、テーブルのNum列の最大値+1(ユニーク番号)を343行目で取得した「変更前(FinD、Status列は空の状態)の行データ」の先頭(buf(1, 1))に代入します。

350~363行目で「定期的タスクだった場合に、新たな期限日を計算し、タスクを新規作成」しています。
350行目「Select Case buf(1, 6)」では、343行目で取得した行データの6列目(Every列)の値を調べます。

351行目「Case "毎日"」は、その値が「毎日」だった場合には352~353行目を実行します。
352行目「buf(1, 4) = DateAdd("d", 1, WorksheetFunction.Max(Date, buf(1, 4)))」では、完了・中止処理をしたタスクのEndD列の値(期限日)と今日の日付を比べ、その大きい方(後の日付)に対して1日後の日付を配列bufのEndD列位置に戻します。
少し分かり難いので実例で説明すると、毎日行う散歩を12/20には実行できず12/21にやっと実行できたとします。12/21には散歩を実行したので完了処理をした場合、単に期限日を基準にして「buf(1, 4) = DateAdd("d", 1, buf(1, 4))」とすると、「12/20の散歩が完了」し「12/21の散歩が新規作成」され、12/21にもう一回散歩に行かなければならなくなってしまいます。
日記みたいに毎日確実にやり遂げなければならないタスクでしたら、そのような処理も必要でしょうが、都合で出来なかったらそれは無かった事にしても良いタスクの方が「毎日行うタスク」には多い気がするので、352行目のような式としました。

同様に354行目「Case "毎週"」では毎週の場合、355行目「buf(1, 4) = DateAdd("ww", 1, buf(1, 4))」で1週間後の日付を配列bufに代入します。「毎週」以上では、「毎日」のように「今日の日付と比較」するような式にはせず、単に「期限日の次にタスクを新設」しています。「毎週」と設定したような予定は、遅くなっても挽回しないといけない内容が多いと考えたためです。
357行目「Case "毎月"」では毎月の場合、358行目「buf(1, 4) = DateAdd("m", 1, buf(1, 4))」で1ヶ月後の日付を配列bufに代入します。DateAdd関数で月の計算をする場合は、例えば10/31の翌月は11/30と、月末を跨いでしまう時は月末まで戻してくれるようです。
360行目「Case "毎年"」では毎年の場合、361行目「buf(1, 4) = DateAdd("yyyy", 1, buf(1, 4))」で1年後の 日付を配列bufに代入します。

毎日~毎年まで、それぞれ新しい期限日を配列bufに代入したら、それぞれ「Call TableDataIn(T1, buf)」で図5-12を呼出し、テーブルに新規データとして挿入します。
なお、DateAdd関数に使用する「単位」は、図6-22のようになっています。
 
単位の文字内容
yyyy
m
d
h
n
単位の文字内容
s
q四半期
y通年での日数
w平日
ww
図6-22

この図6-22の中で、実に紛らわしいのが「w」と「y」です。色々なサイトでは以下のように説明されています。
 「w」指定した日付の曜日がいくつあるか。平日。
 「y」通年での日数。年間通算日。
しかし「w」の前半の説明は意味がわかりませんし、後半の平日という説も計算してみると「土日や日曜を抜いて計算する訳でも無い」のです。「y」も意味がわかりません。単純には「w」「y」は「d」と同じ動作になるようですが、とは言え天下のMicrosoftが設定するからには何か意味があると思うので、「何か」が分かったらまた報告します。

また、定期的タスクを追加する「Call TableDataIn(T1, buf)」が、「毎日」~「毎年」まで4回同じコードとして出てくるのは、なんともカッコ悪いです。もう少し工夫の必要があります。

定期的タスクの新たなタスクが追加されましたら、365行目「Call make_List1」でToDoリストを更新し、366行目「Call make_List2」で完了一覧リストを更新します。
最後に、データテーブルが書き換わりましたので、367行目「ThisWorkbook.Save」でデータ保存します。

6-2-5-3.データを編集「新規/更新」ページへ送付
ToDoページの「編集ボタン(図6-8の189行目)」、完了一覧ページの「復活ボタン(図6-19の324行目)」から呼び出されるのが図6-23です。引数として、データを取得するListBoxオブジェクトを受け取ります。(LBはListBoxの略)
  1. '========== ⇩(27) データを編集「新規/更新」ページへ送付 ============
  2. Private Sub Edit(LB As Control)
  3.  If LB.ListIndex = -1 Then
  4.   MsgBox "タスクを選択して下さい"
  5.   Exit Sub
  6.  End If
  7.  Me.MultiPage1.Value = 1
  8.  With LB
  9.   If .List(.ListIndex, 6) = "完了" Then
  10.    Me.Label1.Caption = ""
  11.   Else
  12.    Me.Label1.Caption = .List(.ListIndex, 0)
  13.   End If
  14.   Me.TextBox1.Text = .List(.ListIndex, 1)
  15.   Me.ComboBox1.Value = .List(.ListIndex, 2)
  16.   Me.TextBox2.Text = .List(.ListIndex, 3)
  17.   If Not .List(.ListIndex, 5) = "" Then
  18.    Me.CheckBox1.Value = True
  19.    Select Case .List(.ListIndex, 5)
  20.     Case "毎日"
  21.      Me.OptionButton1.Value = True
  22.     Case "毎週"
  23.      Me.OptionButton2.Value = True
  24.     Case "毎月"
  25.      Me.OptionButton3.Value = True
  26.     Case "毎年"
  27.      Me.OptionButton4.Value = True
  28.    End Select
  29.   Else
  30.    Me.CheckBox1.Value = False
  31.   End If
  32.  End With
  33. End Sub
図6-23

「編集」ボタン・「復活」ボタンとも、「リストボックスの項目を選択した後に実行」するものです。ですので374行目「If LB.ListIndex = -1 Then」で「リストボックスの項目を選択していない」場合は、375行目「MsgBox "タスクを選択して下さい"」でコメントを出し、376行目「Exit Sub」で処理を中止します。

379行目「Me.MultiPage1.Value = 1」は、(Pageのインデックスはゼロ始まりのため)Page2の「新規/更新」ページに移動をします。このページ移動の時に図6-20のMultiPage1_Changeイベントが発生し、図6-15のCommandButton5_Clickを呼出し、「新規/更新」ページの各入力欄をクリアします。

381行目「With LB」は、以下のコードを引数で得たListBoxについて処理をしていきます。
まず383行目「If .List(.ListIndex, 6) = "完了" Then」では、操作している項目が「完了」の場合に、384行目「Me.Label1.Caption = ""」で、タスク番号のLabel1をクリアします。これは、図6-13で説明した③の部分になります。
それ以外(中止④、また未完②(空データ))の場合は、386行目「Me.Label1.Caption = .List(.ListIndex, 0)」で、リストボックスに保存されているタスク番号をLabel1に記入します。

389行目「Me.TextBox1.Text = .List(.ListIndex, 1)」では「タスク名」を、390行目「Me.ComboBox1.Value = .List(.ListIndex, 2)」では「優先度」を、391行目「Me.TextBox2.Text = .List(.ListIndex, 3)」では期限日を貼り付けます。

393~407行目は、定期的タスクの情報をチェックボックス・オプションボタンに表します。
393行目「If Not .List(.ListIndex, 5) = "" Then」では、Every列に何か(「毎日」「毎週」「毎月」「毎年」)が入っている場合に394~404行目を実行し、何も入っていない(一回限りのタスク)場合に406行目を実行します。

Every列に何かが入っている場合は、394行目「Me.CheckBox1.Value = True」で、まず「定期的」のチェックボックスにレ点を入れます。
395行目「Select Case .List(.ListIndex, 5)」では、Every列の文字列を確認し、396行目「Case "毎日"」の場合は、397行目「Me.OptionButton1.Value = True」で、毎日のオプションボタン(OptionButton1)をONにします。
以下同様に、「毎週」の場合はOptionButton2を、「毎月」の場合はOptionButton3を、「毎年」の場合はOptionButton4をONにします。

Every列に何も入っていない場合は、406行目「Me.CheckBox1.Value = False」で定期的のチェックボックスのレ点を外します。但し、379行目「Me.MultiPage1.Value = 1」で「新規/更新ページ」に移動した際、図6-20のMultiPage_Changeイベントが発生し、CommandButton5_Clickプロシージャ(図6-15)が呼び出されることで、図6-15の260行目「Me.CheckBox1.Value = False」で既にOFFに設定済みです。ですので、実質このコードは意味がありません(理解し易いように、406行目を付けています)。

7.アドインとしてExcelにマクロを登録

このマクロ付ファイル(サンプルファイル)をExcelのアドインに登録することで、今回の「ToDoリスト」を利用することが出来ます。アドイン方法については「年賀状リスト等の宛名検索と追記 アドイン登録」を参照下さい。
また、リボン上のボタンには、図5-2の「ToDoStart」プロシージャをマクロ登録して下さい。

なおファイルをアドインしてしまうと、マクロの内容は確認できますが、ワークシートの確認が出来ません。
確認が必要な時には図7-1のように、「VBEを開く」→「アドインファイル①のThisWorkbook ②をクリック」→「プロパティウィンドウのIsAddinプロパティ③の値をTrueからFalseに変更④」することで、アドインのワークシート⑤が開きます。
アドインファイルのワークシート閲覧方法
図7-1

確認・操作の後、IsAddinプロパティ③の値をTrueに戻すと、アドインファイルのワークシートが非表示になります。

また、このサンプルファイルを実行しデータ変更(新規タスク追加、完了・中止処置など)を行うと、図7-2のように「ドキュメント検査機能では削除できない個人情報がドキュメントに含まれていることがありますので、ご注意ください」という注意コメントが出る場合があります。
ドキュメント検査の注意コメント
図7-2

これは、私がExcelファイルを作った時の個人情報を削除してからデータをアップしている+マクロ側から保存を実行させているためです。OKをクリックすればデータが保存されますが、キャンセルをクリックするとエラーが発生しマクロ停止してしまいます。

これを解除するには図7-3のように、「ファイル」→「情報」の「ブックの検査」のところで、「これらの情報をファイルに保存できるようにする」をクリックしておけば、その後エラーは発生しなくなります。
検査の解除
図7-3

もう少しスマートな「ファイルのアップ方法」がある気がしますが、とりあえず今回はこの方法で対応をお願いします。

8.最後に

今回のように「期限日」だけを登録するタイプでは、1日で開始から完成まで進むようなタスク項目だと問題無いのですが、例えば10日間かかるような大きなタスクの場合には、うまくスタートが出来ないかもしれません。
そのため「タスク達成に必要な期間」みたいなものも登録して、「スタートしなければいけない時期に、リストの先頭に表示される」ようにしたいと当初考えていたのですが、分かり易い表示方法に辿り着かず今回はあきらめました。

また、リストボックスに表示する項目数は今回「日数(定数Term1、Term2)」で設定しましたが、人によりタスク項目の多い少ないがあるので、日数だけで無く表示項目数も含める考え方もあると思います。
何れにしても、システムを作ったらしばらく使ってみて、バグだけでなく要望・使い勝手などを聞き取り、その内容で修正していくことは必須です。作り上げた時がスタート地点だと思います。


ToDoリストで個人タスク管理(it-071.xlsm)
セキュリティ向上を目的として「インターネット経由でダウンロードしたOfficeファイル(Excel等)のマクロは、既定でブロック」されるようにOfficeアプリケーションの既定動作が変更になりました。(2022年4月より切替開始)
解除の方法については「ダウンロードファイルのブロック解除方法」を参照下さい。