2021/03/04

共有資料の登録と閲覧ができるサーバーシステム




1.背景

社内では様々なプロジェクトが同時並行で流れており、その1つ1つのプロジェクトには様々な書類やデータが結びつけられていると思います。その結び付け方として、紙をファイリングで整理したり、ファイル名でプロジェクトが分かるように記名方法の取り決めをしたり、共有サーバー内をプロジェクトごとにフォルダーを分けたりしていることと思います。
また、作成した資料をプロジェクトに関連付けるシステムも世の中にはあります。

今回は、Excelと共有のファイルサーバーのみで組み立てる「資料の登録・閲覧システム」を紹介します。
なお社内での同時作業を可能とするため、サーバー上にExcelシートDB(データベース)を作り、SQL文を使ってデータのやり取りをしています。また関連資料は、ファイルのコピーをサーバー内で一括保存しています。

なお、今までSQL文を使った項は以下の通りです。参考にして下さい。
 「Excelシート上にDBを作り、SQLを使ってデータを入出力する
 「ExcelシートDBを使った会議室予約システム
 「ExcelシートDBとSQLを使った倉庫管理システム
 「複数の備品を同時予約可能な貸出台帳」

なお、今回はExcelのシートをデータベーステーブルにしていますが、Accessのデータベースファイルを使ったシステム等については、下記を参照して下さい。
ExcelからAccessデータベースを作成・操作
Accessデータベースを使用した売上台帳

2.システム概要

本システムは基本的にExcelアドインに登録することを考えています。
もちろん通常のワークシート(拡張子.xlsm)で使用してもOKで、一番下のサンプルファイルではSheet1の上部に起動ボタン「資料閲覧システム」からも起動できます。

システムは図2-1のように、Excelアドインにした場合はリボン上のボタンから、サンプルファイルではSheet1の上部ボタンから起動①します。起動すると、メニューダイアログ②(UserForm1)が表示されます。
メニューダイアログ②には2つのボタンがあり、上側が「プロジェクト登録(+資料登録)」、下側が「資料閲覧」になります。

上側の「プロジェクト登録」をクリックすると③の「プロジェクト登録」+「資料登録」ダイアログ(UserForm2)が、また下側の「資料閲覧」をクリックすると④の「資料閲覧」ダイアログ(UserForm3)が表示されます。
起動と各ダイアログの役割
図2-1

③のダイアログは2つの機能を持っています。1つ目は「プロジェクト登録」で、図2-2の左側(ダイアログの上側)になります。
TextBoxにプロジェクト名を記入⑤し「登録ボタン」をクリック⑥することで、新たなプロジェクトが登録されます。通常プロジェクト登録などは管理者の業務ですので、誰もが登録できてしまうのはおかしいのですが、今回システムではパスワード等での登録制限はかけていません。
これは、今回システムは「個人的な仕事の分類」や「少グループ内での作業」みたいなものにも有用そうだ、という思いがあるからです。パスワードを付けたり、ダイアログを分けたりすることは可能ですので、必要に応じて改造してもらって構いません。

プロジェクト登録と関連資料登録
図2-2

一方、図2-2の右側(ダイアログの下側)では「プロジェクトに対応した関連ファイルを登録」します。
まずは「Project」と書いてある横のコンボボックスをクリックし「登録されているプロジェクトの1つを選択」⑦します。そしてその下の「添付ファイル」と書いてある横の枠内に「ファイルをドラッグ&ドロップ」することで、ファイルが決められたサーバー等に登録されます。

どんなアプリのファイルでもOKですが、「1ファイルずつ登録」することと「フォルダーごとはダメ」なことが制限事項です。またファイル属性として「隠しファイル」「システムファイル」は対象外です。「隠しファイル」についてはHidden属性を外してから使用して下さい。
なおショートカットも登録でき、保存されるファイルは「ショートカットでは無く実体ファイル」です。ですので、ドラッグ&ドロップする時点でリンクが繋がっているものに限られます。

またファイルを枠内にドロップした際、図2-3のように「ドロップが成功すると、枠色が赤色→緑色に変化」します。これは、ドロップした時に何も反応しないと、ユーザーは不安になるだろうと考えたためです。また、ダイアログを起動して初めてファイルをドロップした時には処理に多くの時間がかかる(数秒)ようなので、「次々にドロップして欲しくない」という意味も含んでいます。
また最初にファイルをドロップした時には、内部処理に時間がかかるようで、枠色が反応するのに数秒かかります。ご了承下さい。
ファイルをドラッグ&ドロップした時の動作
図2-3

資料閲覧のダイアログ④では図2-4のように、左側のリストボックスからプロジェクトを選択⑨します。すると、そのプロジェクトに登録されているファイルのリストが右側のリストボックスに表示⑩されます。
そのファイルのリストから閲覧したいファイル名をダブルクリック⑪すると、登録されているファイルが表示⑫されます。
閲覧が終わったら、それぞれのアプリの終了方法で閉じてください。
関連資料の閲覧
図2-4

なお、登録・閲覧時のプロジェクトや登録ファイルのリストの並び順は、上から新しい順に並べています。
また閲覧しているファイルは「サーバーに保存してあるファイルそのもの」を開いています。ですので、内容変更後に上書き保存も可能な状態ですので注意願います。
(内容を変更させたく無い書類は、PDF化+パスワード設定などの処理後にファイル登録するなどの対策が必要です。)

3.プログラムの流れ

図3-1のように、システムが起動されると「登録と閲覧を分岐するダイアログ(UserForm1)」が表示され、そのダイアログの「プロジェクト登録」ボタン(上側)をクリックするとUserForm2が、「資料閲覧」ボタン(下側)をクリックするとUserForm3が起動します。
各ダイアログの表示の流れ
図3-1

UserForm2(プロジェクト登録)の下半分の「ファイルの登録」部分にはコンボボックスを配置しています。ファイルを登録(ファイルをドロップ)する時に、そのファイルが「何のプロジェクトのファイルなのか」を判別するため、そのコンボボックスには「プロジェクトのリスト」を入れておく必要があります。
そのためUserForm2の起動時に「プロジェクトが登録されたDBテーブル」からデータを取り出し、コンボボックスにリストとして出力します。なおデータの取り出しには、SQL文を使用しています。

また、UserForm3(資料閲覧)にも「何のプロジェクトの資料(ファイル)なのか」を選択するためのリストボックスを配置しています。UserForm3起動時にも「プロジェクトが登録されたDBテーブル」からデータを取り出し、左側のリストボックスにリストとして出力します。なおデータの取り出しには、同じくSQL文を使用しています。

3ー1.プロジェクトの登録

「プロジェクトの登録」には、UserForm2の上側TextBoxにプロジェクト名を記入し、その左側の「登録」ボタンをクリックします。
ボタンがクリックされると、図3-2のようにTextBoxの値を確認し空白(何も入力されていない)だったら処理を終了します。
プロジェクト名が記入されていたら次に進み、SQL文を使用して「プロジェクトのテーブル(DB01)」から「最大のプロジェクトNo」を取得します。
続いて今回記入したプロジェクト名を登録しますが、その登録するプロジェクトNoは、先ほど取得した「最大プロジェクトNo」に対して1つ増やした値をプロジェクトNoとします。
プロジェクト登録の流れ
図3-2

3ー2.ファイルの登録

関連ファイルは、UserForm2のコンボボックスで「登録するプロジェクト」を選択した後、ファイルを枠内にドラッグ&ドロップします。
ドロップされたファイルは「WebBrowserコントロール」で受け取ります。その「ドロップされたファイル」をサーバーに保存するのですが、サーバー内に既に「ドロップされたファイル名」と同じファイルが存在する可能性があります。ですので、とりあえず「ドロップされたファイル名」を「保存ファイル名」と仮決めします。

次に、仮決めした「保存ファイル名」と同名のファイルがサーバー内に存在するかを確認し、存在する場合は「保存ファイル名」に連番を追加します。例えば、元のファイル名がABCD.pdfだったとするとABCD-1.pdfという具合です。
変更された「保存ファイル名」で再度サーバー内に同名ファイルが存在するかを確認します。また存在するようでしたら、ABCD-2.pdfというようにまた保存ファイル名を変更します。
サーバー内に存在しないような「保存ファイル名」になったら、ドロップされたファイルを「保存ファイル名」としてサーバーにコピーします。
ファイル登録の流れ
図3-3

ファイルがコピーされたら、その「保存ファイル名」を「ドキュメントテーブル」に登録します。同時に「何のプロジェクトのファイルか」が分かるように、UserForm2のコンボボックスの番号(=プロジェクトNo)を書き込みます。

3ー3.ファイルの閲覧

ファイル閲覧のためのUserForm3の左側のListBox1には、「プロジェクトNo」と「プロジェクト名」が並んでリスト化されています。ユーザーが、そのどれかのプロジェクトを選択すると、「ドキュメントテーブル」から「プロジェクトNo」に対応している「ファイル名」を取得し、右側のListBox2でリスト化します。
ファイル閲覧の流れ
図3-4

ユーザーが右側のListBox2の中から「閲覧したいファイル名」をダブルクリックすると、コマンドプロンプトを使って「閲覧したいファイル名」を実行します。
形としては図3-5のように「コマンドプロンプト」から「PDDなどのファイル名を直接実行」するような感じです。または、エクスプローラ上のPDF等のファイルをダブルクリックすることと同じです。
通常は、ファイルに対応したアプリを起動したのちファイルを開くのですが、直接ファイル名を実行することで「ファイル名の拡張子に対応しているアプリでファイル名を開く」ことになります。
コマンドプロンプトからファイル名を実行
図3-5

ですので、AさんのPCに入っている「特殊なアプリ」で作成したファイルを登録しても、特殊なアプリの入っていないBさんはファイルを開けないことはもちろん、Cさんは「*.txt」をメモ帳で開くのにDさんはWordで開く可能性もあります。
また「直接実行」方式にしているため、「拡張子がEXEのような実行ファイル」を登録すれば実行されてしまう可能性があり、悪意のある人にとっては都合の良いシステムということになります。またEXEファイルでなくても、Excelファイル(拡張子.xlsm)のマクロに悪意のあるコードを仕込むことも可能ですので、運用するときには注意が必要です。
その意味も含め、今回は「拡張子まで含めてファイル名を表示」することにしました。

4.データベーステーブル

今回のシステムは、データを「データベース」という形で操作場所とは異なるサーバー等に置くことで、様々な人が同時に操作できるようにしています。そしてそのデータの操作には、一般的なデータベース「OracleやAccess」と同じSQL文を使っています。

今回使用するテーブル(表)は2つで、「プロジェクト」を管理するテーブル(シート名:DB01)と、「ファイル(ドキュメント)」を管理するテーブル(シート名:DB02)です。2つのテーブルをつないでいるのは「プロジェクト番号(ProNo)」になります。
データベースのシート名をDB01等としましたが、Sheet1等と初期の名前でもOKです。どちらにしても図5-1の8~9行目で設定しているように「シート名」を「テーブル名」に置き換えてSQL文に使用した方が、後に本格的データベースに置き換える場合にも楽になります。

4ー1.プロジェクトのDBテーブル(ExcelのワークシートDB01)

「プロジェクト」を管理するテーブルは、図4-1のように3列分のデータを持っています。
1列目の「ProNo」は「プロジェクト番号」と呼んでいますが、いわゆる「プロジェクトを作った順番」で、ユニーク(番号が重ならない)な番号になります。もし社内で独自の「プロジェクトコード(A-050など)」が必要な場合は、ProNoとは別の新しいカラム(列)が必要になります。
2列目の「ProName」は、「プロジェクト名」です。図2-2左側の「プロジェクト登録」の操作で作られる名前です。この項目の重複は許可しています。
3列目の「Del」は「プロジェクトが無くなった」とか「プロジェクトが完了した」のように、「表示の必要が無くなった」時に印(完了日など)を入力する列です。この列に「何か」が入っている時には、プロジェクトとして表示されません。
データベーステーブル(プロジェクトのDBテーブル)
図4-1

なお今回システムでは、「Del列」への値の入力工程は設けていません。DB02シート(図4-2)の方にも同じようなDel列がありますが、Del列への入力をするフォームを新規作成するか、データベース(DB01、DB02シート)に値を直接入力するか(こちらの方法は、あまりお勧めしません)の方法が必要です。
また、もしシートの値を直接操作する場合、データ行を削除する際は「行削除」を使用して下さい。セルの値だけ消去すると「空白のデータがある」と判断されてしまい、思わぬエラーが発生することがあります。

4ー2.ドキュメントのDBテーブル(ExcelのワークシートDB02)

「ドキュメント(ファイル)」を管理するテーブルは、図4-2のように4列分のデータを持っています。
1列目の「ProNo」は「プロジェクト番号」で、その行のデータが図4-1の「プロジェクトテーブル」のどのプロジェクトに対応しているかを示しています。この列は、データが重複する可能性はあります。
データベーステーブル(ドキュメントのDBテーブル)
図4-2

2列目の「Fname」は、サーバーのファイルを保存しているフォルダー内に入っている「ファイル名」になります。重複するデータは無く、ユニークな値の列です(ファイル本体を削除すれば、重複可能です)。
当初、この列には「フルパス名+ファイル名」を入れることで、コマンドプロンプトでそのまま実行していたのですが、以下2つの理由で「ファイル名のみ」にしました。
1つは、なんらかの理由で「ファイルの置き場所(サーバー等)を変更しなければならなくなった」場合に、データを全て書き換える必要が出てくることです。手作業で変更するとは思えませんが、変更後のチェックなども考えると大変です。
もう一つは、図2-4のようにUserForm3で閲覧したいファイル名一覧を表示した時、フルパス付きだと非常に長くなってしまうために、わざわざ「フルパスを削除してから表示」という手間が増えます。
天秤に掛けて、主には前者の「データベースを後から修正するのは良くない」を優先し、「ファイル名のみ」としました。

3列目の「UpD」は「ファイルを登録した日付」です。今回のようなシステムだと、1つのプロジェクトに対して上流側(企画→設計→評価→生産 だったら企画の方を上流として)のファイルから順番にいれていく形になると考え、ファイル名一覧が表示された時に「最新のファイル(情報)が一番上に来ている」ことが望ましいと思います。そのため登録した日付で降順に並べるために「登録日」を列としました。
ファイル名の付け方を工夫し「ファイル種類の順」に表示させたり、ファイル種の列を増設したりする方法もあると思います。

4列目の「Del」は「プロジェクトテーブル」のDel列と同様で、「ファイルが必要無くなった」時に印(完了日など)を入力する列ですが、今回システムとしてDel列に印をつける機能はありません。
この列に「何か」が入っている時には、ファイル一覧には表示されません。

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

標準モジュールには、システム起動のためのプロシージャ(ProDoc_Start)と、ExcelシートDBと接続しSQLでデータをやり取りするプロシージャ(SQL_exec3)を置いています。

5ー1.定数・変数宣言とシステム起動

まず宣言部では、「ファイル保存のフォルダー名」、「データベースファイル名」、「データベースシート名」を指定します。
  1. '========== ⇩(1) 宣言部 ============
  2. Public Const FILE_BOX As String = "¥¥Server¥Excel¥FileBox¥"    '保存フォルダ―名(最後に¥印を付けて下さい)
  3. Public FILE_BOX As String         'サンプルファイルでは、こちらを生かしています。
  4. Public Const DB_FILE As String = "¥¥Server¥DB¥it-051.xlsm"    'データベースファイル名
  5. Public DB_FILE As String         'サンプルファイルでは、こちらを生かしています。
  6. Public Const ProList As String = " [DB01$] "
  7. Public Const DocList As String = " [DB02$] "
図5-1

2~3行目が「ファイル保存のフォルダー名」の設定で、定数・変数名は「FILE_BOX」になります。
通常は、皆がアクセス可能な「サーバー名+フォルダー名」を定数設定する2行目の書き方になります。パスの最後には「¥」印を付けて下さい。(「¥」印を付ける理由:プログラム中では「FILE_BOX & ファイル名」という計算式にしているため)

なお、一番下のサンプルファイルでは、ファイル単体で試行できるようにするため、3行目で「FILE_BOX」を変数として宣言し、図5-3の13行目で「ファイルをダウンロードしたフォルダをFile_Box」とするようにしています。

また、5~6行目は「プロジェクトテーブル」と「ドキュメントテーブル」が入っているExcelファイルを設定します。定数・変数名は「DB_FILE」です。ファイル保存先(FILE_BOX)と同様、皆がアクセス可能なファイルサーバーなどに置くことになりますので、5行目の定数設定の方法になります。ファイル名は「.xlsx」でも「.xlsm」でもOKです。

なおサンプルファイルでは、ファイル保存先(FILE_BOX)の時と同じ理由で、サンプルファイル上にDBテーブルを置いていますので、図5-3の14行目で「自分自身」を指定しています。
本来の設定方法とサンプルファイルでの設定の違いについて図5-2にまとめましたので、運用する時には切り替えて下さい。
データベーステーブル(ドキュメントのDBテーブル)
図5-2

システムを起動するには、図5-3のプロシージャを実行します。
  1. '========== ⇩(2) システムの起動プロシージャ =============
  2. Public Sub ProDoc_Start()
  3. ' FILE_BOX = ThisWorkbook.Path & "¥"     '定数設定する場合は消す
  4. ' DB_FILE = ThisWorkbook.Path & "¥" & ThisWorkbook.Name     '定数設定する場合は消す
  5.  UserForm1.Show 0
  6. End Sub
図5-3

上述しましたが、13~14行目の見え消し部はサンプルファイルでの記述です。宣言部で定数宣言した場合には削除して下さい。

16行目では、UserForm1をモードレスで実行します。この「モードレスで起動」しなければならない理由を説明します。
まず、ダイアログからダイアログを起動する際は「モーダルで起動したダイアログから、モードレスでダイアログを起動することは不可」です。ですので、今回システムのようにUserForm1からUserForm2とUserForm3を起動する種類としては図5-4のように6種類が考えられることになります。
各UserForm起動のモーダル・モードレスの可否
図5-4

今回のシステムではUserForm2でファイルを登録し、UserForm3でそのファイルを閲覧する流れになります。登録できるファイル種は何でもOKで、もちろんExcelファイルも登録可能です。UserForm3ではそのExcelファイルを開くことはできるのですが、フォームがモーダルの状態で開かれている時にExcelファイルを開くと「Excelのメニュー操作不可」となり「閲覧したExcelファイルを閉じる事が出来ない」状態になります。(Excel以外の場合は閉じることが出来ます)
「閲覧ファイルを閉じるのに、システムのダイアログを全て閉じなければならない」のは変ですので、UserForm3はモードレスで起動させることにしました。

そうなると、図5-4のNo.6の状態となり、つまりは「UserForm1もモードレスで起動」する必要があるのです。
なおUserForm2には、「Excelを閉じられない」のような不具合は発生しないようなので「モーダルでもモードレスでもOK」です。しかし、状態をUserForm2とUserForm3で統一するため、今回システムではフォームは全てモードレスで起動させました。

5ー2.データベース接続とSQL実行

データベースに接続し、SQL文を使ってデータの処理をするプロシージャが図5-5です。今回は2つのダイアログの中から呼び出されるため、標準モジュールに置いています(単一のダイアログで使うのでしたら、フォーム内に置いても良いと思います)。

引数は3つで、第一引数の「SQL」は、実行するSQL文です。
第二引数の「Fld」はFieldの略で「SQLのSelect文で抽出した列名」を配列の形で戻してくれます。この値は今回システムでは使用しませんが、「ExcelシートDBとSQLを使った倉庫管理システム」のような「種類の異なるSQLデータを1つのワークシートに貼り付ける」場合には、データの列名を明確にする必要があるので有用だと思います。
第三引数の「ReadOnly」は、「データの追加・修正を行うか否か」を指定するものです。オプション設定のため指定しない場合は「True=データは読み出しのみ」になりますので、「Insert や Update」文を使用する時だけ「False」を指定します。

また、関数プロシージャとしての戻り値は、Select文であれば「抽出されたデータ配列(抽出されなかった時には空の二次元配列)」を、Insert文・Update文の場合は「成功すればEmpty」「失敗すればFalse」を返します。
なお、プロシージャの機能は「ExcelシートDBとSQLを使った倉庫管理システム」の項と似ていますが、今回は「戻り値である取得データ配列の行と列を逆転」していることと「エラー処理を一部追加している」ことから一応互換性はありません(ワークシートに貼り付けるコードを修正すれば使用可能です)。
ですので、プロシージャ名を前回とは異なる「SQL_exec3」としました。
  1. '========== ⇩(3) データベース接続とSQL実行 =============
  2. Public Function SQL_exec3(SQL As String, Fld As Variant, Optional ReadOnly As Boolean = True) As Variant
  3.  Dim cn As Object        '←コネクションオブジェクト変数
  4.  Dim rs As Object        '←レコードセットオブジェクト変数
  5.  Dim temp As Variant      '←全抽出データの配列
  6.  Dim tempR As Variant      '←全抽出データの行列逆転の配列
  7.  Dim i As Long         '←カウンタ変数(temp配列の行数)
  8.  Dim j As Long         '←カウンタ変数(temp配列の列数)
  9.  Set cn = CreateObject("ADODB.Connection")
  10.  Set rs = CreateObject("ADODB.Recordset")
  11.  cn.Provider = "MSDASQL"
  12.  cn.ConnectionString = "Driver={Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)};" _
  13.             & "DBQ=" & DB_FILE & ";" _
  14.             & "ReadOnly=" & ReadOnly & ";"
  15. ' cn.Provider = "Microsoft.ACE.OLEDB.12.0"
  16. ' cn.ConnectionString = "Data Source=" & FILE_NAME & ";" _
  17. '           & "Extended Properties=""Excel 12.0;"""
  18.  cn.Open
  19.   If ReadOnly = True Then
  20.    rs.Open SQL, cn
  21.     If rs.EOF = False Then
  22.      temp = rs.GetRows         '←全行データを配列にまとめて代入
  23.      ReDim Fld(0 To UBound(temp, 1))
  24.      ReDim tempR(0 To UBound(temp, 2), 0 To UBound(temp, 1))
  25.      For i = 0 To UBound(temp, 1)
  26.       Fld(i) = rs.Fields(i).Name
  27.       For j = 0 To UBound(temp, 2)
  28.        If IsNull(temp(i, j)) Then
  29.         tempR(j, i) = ""     '←配列内にNullが存在するとエラーが出る為空文字に変更
  30.        Else
  31.         tempR(j, i) = temp(i, j)    '←行と列を入替え
  32.        End If
  33.       Next j
  34.      Next i
  35.     Else                  '←摘出データが無いの場合
  36.      ReDim Fld(0 To 0)
  37.      ReDim tempR(0 To 0, 0 To 0)    '←他の場合と同様に2次元配列を作る(各要素は空)
  38.     End If
  39.     SQL_exec3 = tempR         '←配列を戻り値とする
  40.    rs.Close
  41.   Else
  42.    On Error Resume Next
  43.     cn.Execute SQL
  44.     If Not Err.Number = 0 Then SQL_exec3 = False
  45.    On Error GoTo 0
  46.   End If
  47.  cn.Close
  48.  Set rs = Nothing
  49.  Set cn = Nothing
  50. End Function
図5-5

このプロシージャについては「ExcelシートDBとSQLを使った倉庫管理システム」の中で割と詳しく説明していますので、本項では「変更した部分」を重点にし、その他は簡単に説明します。

まず29~30行目でADODB(Microsoftが提供するデータベースアクセスのためのソフト:ActiveX Data Objects)のConnectonとRecordsetオブジェクトを生成します。
32~35行目では、図5-1の5行目で設定した「データベースファイル」に接続する準備をします。
なお接続準備の方法には、見え消しにしている37~39行目のような記述もあります。

41行目でデータベースに接続しますが、そこから先は引数ReadOnlyの値により2つに分岐します。
Select文の場合(ReadOnly=True)は、43~64行目を実行します。Insert文やUpdate文の場合(ReadOnly=False)は、67~70行目を実行します。

Select文の場合には、43行目でSQL文をデータベース側に渡すと、オブジェクト変数rsにその抽出結果が帰ってきます。
44行目ではそのデータが空では無い(rs.EOF=False)場合には45~57行目を実行します。空の場合(rs.EOF=True)は59~60行目を実行します。

データがある場合は、45行目で全データを配列形式で変数tempに格納します。そのデータ内には「値の無いもの=Null値」が含まれていることがありますが、このNull値があると後の処理でエラーが出る可能性がありますので取り除きます(Null値→長さゼロの文字列)。
また、45行目で受け取るデータは「行列が逆転」している形ですので、今回は行列を修正しています(修正する理由は、「5ー2ー1.取り出したデータの行列入れ替えの必要性」で説明します)。
行列を修正したデータは変数tempRに入れていきますので、47行目で変数tempの行列を入れ替えた大きさでRedimします。

45行目で取得したデータ配列の処理内容としては、46~57行目でtemp配列のデータを1つ1つ確認しながら「Null値を除去(長さゼロの文字列に変換)(52行目)」すると共に「行列を入れ替え(54行目)」、同時に「Fldに列名を代入(49行目)」していきます。

一方データが無かった場合は、データがある場合の戻り値の形(二次元の配列形式)と合わせるために、60行目で各要素が空の二次元配列tempを作成します。

Select文の場合は、データがあった場合も無かった場合も、63行目で配列tempをプロシージャの戻り値にします。

Insert文やUpdate文の場合はデータを受け取りませんので、68行目でSQL文を実行します。しかし、もしエラーが発生(データが追加できなかった、変更できなかった)した場合を考え、67行目の「On Error Resume Next」で68行目をスルーさせ、69行目のErr.Numberを確認しゼロで無かった(=エラーが発生している)時には、プロシージャの戻り値としてFalseを戻すようにしています。
従来(ExcelシートDBとSQLを使った倉庫管理システム)は、68行目のみでしたが、エラーを戻すために67~70行目のようにエラー処理を加えました。

寄り道
今回追加したエラー処理は「SQL文が間違っていた」ことのみの想定です。データベースに接続できなかった時には41行目の時点でエラーが発生するはずですし、その他のエラー(例えば、2箇所変更するはずが1箇所しか出来なかった)も考えられますが今回は対応できていません。
一般的なデータベースにはRDBMSという管理機能がありますが、ExcelシートDBには存在しませんので、エラー処理や(自力では難しいそうな)ロールバックはある程度作り込まないといけなくなります。
もし一般的なデータベースが利用可能であるならば、少しハードルが高くてもそちらの方をお勧めします。

5ー2ー1.取り出したデータの行列入れ替えの必要性

今回の「SQL文処理プロシージャ(SQL_exec → SQL_exec3)」の大きな変更点は、従来の「取得したデータを行列入れ替わった状態で返す」方式だったものを「行列を正しい状態にして返す」方式に変更するものです。

データベースからデータを取り出し変数に格納するには、1行ずつ渡す方法もありますが今回は図5-5の45行目のように「rs.GetRows」を使って一括で変数に格納しています。格納した状態は「行と列が入れ替わった形で格納」されますので、前回まではそのまま(Null値だけは、長さゼロの文字列に変更)出力していました(図5-6)。ですので「SQL_execプロシージャ」を出た時には「行と列が入れ替わった配列」のデータとなっているのです。
そのデータをTranspose関数で行列変換をした後、ワークシートやリストボックスに一括貼付けして使用していました。
データベースから取り出したデータ配列の処理の仕方(従来)
図5-6

しかし抽出したデータが1行だった場合には、図5-7のようにTransposeで行列変換した後「リストボックス等に一括貼付け」すると、縦方向にデータが並んでしまう不具合があることが分かりました。
原因は「1行のみ抽出されたデータをTransposeで変換すると『1次元配列』になる」ことと、「リストボックス等にとっての1次元配列の方向は縦方向(推測)」であることが重なったためと思われます。
なおワークシートの場合には1次元配列の方向は横方向」なので不具合は発生しません。
取り出したデータが1行だった場合の不具合(従来)
図5-7

今まで「SQL_execプロシージャ」の中で「行列の入替え」を行わなかった理由は、プロシージャ内部でTranspose関数を使うと、1行しか抽出されなかった時は前述の通り「1次元配列データに変更」されてしまい、1行データと複数行データで処理を分岐させる手間が発生してしまうためでした。
加えて、データの貼付け先がワークシートであった事、またリストボックスに貼り付けるとしても単列データのみだった事もあり、複数列リストボックスでの不具合に気づきませんでした。面目有りません。

しかし、今回システムでは「複数列のリストボックス・コンボボックス」を使う必要があるため、1行データのための別処理を行うのもイヤなので、「SQL_execプロシージャ」の中でTransposeを使わずに配列を入れ替える」ことにしたのです。
そのように改造することで、データ抽出後の流れは図5-8のようになります。
今回の考え方
図5-8

このような方法にするとTranspose関数を使わなくて済み、「二次元配列のまま」でリストボックス等に一括貼付けできますので、図5-8の一番右側のようにデータが正しい向きに収まるようになります。

また「Nullが存在するデータをTransposeで行列変換するとエラーが発生」するのですが、その意味もあってSQL_execプロシージャ内で「Nullを『""(長さゼロの文字列)』に置換」していました。しかし、Transposeを使わなくて良くなると「Nullのままでも良いのではないか」とも思い、その検討をしてみました。
確かに、問題無くワークシートにもリストボックス等にも一括貼付けは可能でした。しかし図5-9のようにリストボックス等では「Null値のまま」の項目が出来てしまうので、「リストの値を使って処理する」ような場合には「Nullが存在することを意識した処理が必要」となってしまいます(例えば、IsNull関数で処理を分けたり、数式を作ったり)。
Null値の処理の必要性
図5-9

ですので、今回も「SQL_execプロシージャ」の中で「Nullを『""(長さゼロの文字列)』に置換」を行い、出力される配列内にはNull値が存在しないようにしています。

6.メニューダイアログ(UserForm1)

6ー1.コントロールの配置

フォーム内の各コントロールの配置は、図6-1のようにしました。
UserForm1でのコントロール配置
図6-1

このメニューダイアログはプログラムの分岐のみの役目ですので、CommandButtonの1と2がそれぞれのフォームの起動用とし、CommandButton3はシステム終了用です。

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

フォーム起動時に最初に実行されるのが図6-2のInitializeイベントです。その中の80~82行目で、3つのCommandButtonに表示文字を設定しています。
  1. '========== ⇩(4) データベース接続とSQL実行 =============
  2. Private Sub UserForm_Initialize()
  3.  Me.CommandButton1.Caption = "プロジェクト登録"
  4.  Me.CommandButton2.Caption = "資料閲覧"
  5.  Me.CommandButton3.Caption = "終了"
  6. End Sub
図6-2

3つのボタンをクリックした時の動作を、図6-3のように各ボタンのClickイベントに記しています。
  1. '========== ⇩(5) UserForm2の起動 =============
  2. Private Sub CommandButton1_Click()
  3.  UserForm2.Show 0
  4. End Sub
  5. '========== ⇩(6) UserForm3の起動 =============
  6. Private Sub CommandButton2_Click()
  7.  UserForm3.Show 0
  8. End Sub
  9. '========== ⇩(7) システムの終了 =============
  10. Private Sub CommandButton3_Click()
  11.  Unload Me
  12. End Sub
図6-3

87行目、92行目の各UserFormはモードレスで起動しています。モードレスで起動する理由は図5-4で説明したように、「UserForm3はモードレスで起動しないと、Excelファイル閲覧後に閉じることが不可」+「UserForm2と3で状態を合わせる」ことに寄ります。
また、97行目では自分(UserForm1)を閉じています。

寄り道
97行目の「Unload Me」は「Me.Hide」でも良いのですが、「Me.Hide」を使用すると以下の様な不具合が発生します。
例えばAとBという2つのブックを開いている時、「Aをアクティブにした状態」でシステムを起動し、作業後「Me.Hide」で閉じます。その後「Bをアクティブにした状態」でシステムを起動すると、ダイアログ起動と同時に「Aのブックがアクティブになる」のです。

これは、ダイアログをLoadした時のアクティブブックを記憶しており、Hideメソッドでは消えない為のようです。最悪の場合、初回起動したブックを閉じてしまうと、ダイアログが起動できなくなる可能性もありそうです。(今回システムでは、起動できなくなることは無さそうです)

いままで「効率の為にも、ダイアログはHideで閉じるものだ」と思っていましたが、複数のブックを切り替えながら作業している方も多いと思いますので、「ダイアログは可能な限りUnloadで閉じる」方が確かだと思います。

7.プロジェクト登録・ドキュメント登録(UserForm2)

7ー1.コントロールの配置

登録のフォーム上の各コントロールの配置は、図7-1のようにしました。
この中で「ListBox1」と「Frame1」の位置と大きさはマクロで調整しますので、レイアウトを考えず存在さえしていればOKです。
UserForm1でのコントロール配置
図7-1

TextBox1は「新たなプロジェクト」を記入する枠、ComboBox1は「ファイルを登録する際のプロジェクトを選択」するためのドロップダウンリスト、WebBrowser1は「ファイルを登録(ドラッグ&ドロップ)する枠」となります。

次に、マクロで位置調整をする「ListBox1」についてです。
まずComboBoxは、下三角マークをクリックする事で下方に表示されるリストボックス部と、一番上のテキストボックス部で出来ています。テキストボックス部はユーザーが「検索文字列を記入」することで、それに合致するリストが絞り込むことができる、という機能を持っています。
そのためComboBoxを複数列設定させても、複数列になるのはリストボックス部のみで、テキストボックス部は単列のままです。(リストボックス部のどの列の値をテキストボックス部に持ってくるかはTextColumnプロパティで設定できます)
コンボボックスの構造
図7-2

ですので、今回ComboBoxだけで「ドロップさせるファイルのプロジェクト」を表現しようとすれば、ドロップさせる段階でのComboBoxのテキストボックス部は「プロジェクト番号」か「プロジェクト名」かどちらか一方のみ、ということになってしまいます。
今回のプロジェクト番号は単なる連番ですが、意味のある番号をプロジェクト番号とする場合もあると思います。その時にはプロジェクト番号とプロジェクト名の両方を確認しながら作業すると思いますので、今回は「通常表示(テキストボックス部)も複数列」になるように、「ComboBoxの上にListBoxを重ねる」というやり方にしました。

ListBoxが「通常は1行表示で、クリックすれば下にリストが延びる」というような機能を持っていれば、このような小細工はしなくて済むのですが、今のところコンボボックスのテキストボックス部に複数列表示をさせるには「ComboBox+ListBox」または「ComboBox+Label」のような方法しかなさそうです。

またマクロで位置調整をするもう一つの「Frame1」は、WebBrowserコントロールの外枠用です。これはWebBrowserの外枠が明確で無い(Borderを設定できない)ため、枠の場所を明確にするために「FrameのBorderを重ね合わせる」という小細工をしています。
なおフォーム上のコントロールは、後から作ったコントロールがイベントを受け付けるようなので、図7-3のように「Frame1を作った後にWebBrowser1を作る」必要があります。
WebBrowserとFrameの上下関係
図7-3

7ー2.フォームモジュール

7ー2ー1.フォーム起動時(Initializeイベント)

登録ダイアログ起動時のInitializeイベントは図7-4のようになります。
  1. '========== ⇩(8) フォーム起動時の各コントロールの設定 =============
  2. Private Sub UserForm_Initialize()
  3.  Dim FrameBorder As Double       'Franeの枠線の太さ
  4.  Me.CommandButton1.Caption = "登録"
  5.  Me.CommandButton2.Caption = "戻る"
  6.  Me.ComboBox1.Style = fmStyleDropDownList
  7.  Me.ComboBox1.ColumnCount = 2
  8.  Me.ComboBox1.ColumnWidths = "20;"
  9.  Me.ListBox1.ColumnCount = 2
  10.  Me.ListBox1.ColumnWidths = "20;"
  11.  Me.ListBox1.Left = Me.ComboBox1.Left
  12.  Me.ListBox1.Top = Me.ComboBox1.Top
  13.  Me.ListBox1.Height = Me.ComboBox1.Height
  14.  Me.ListBox1.Width = Me.ComboBox1.Width - Me.ComboBox1.Height
  15.  Me.ListBox1.Enabled = False
  16.  Me.Frame1.Caption = ""
  17.  FrameBorder = (Me.Frame1.Width - Me.Frame1.InsideWidth) / 2
  18.  Me.Frame1.Left = Me.WebBrowser1.Left - FrameBorder
  19.  Me.Frame1.Top = Me.WebBrowser1.Top - FrameBorder
  20.  Me.Frame1.Width = Me.WebBrowser1.Width + FrameBorder * 2
  21.  Me.Frame1.Height = Me.WebBrowser1.Height + FrameBorder * 2
  22.  Me.Frame1.BorderStyle = fmBorderStyleSingle
  23. End Sub
図7-4

104~105行目は、ボタン表面に文字列を表示しています。

107~109行目はComboBoxの設定です。まずは107行目でComboBoxのスタイルを設定しています。設定値は図7-5の様に2種類あり、今回は「テキストボックス部の検索機能を無効」にして「リスト選択機能のみ」にしています。
定数説明
fmStyleDropDownCombo0
(既定)
ドロップダウンコンボボックスとして動作
テキストボックス部への値の入力 又は
ドロップダウンリストからの値の選択が可能
fmStyleDropDownList2リストボックスとして動作
リストから値を選択
図7-5

108行目では、リストボックス部の列数を2列(プロジェクト番号/プロジェクト名)に設定し、その幅を109行目で設定します。109行目の設定値が「20;」となっているのは「1列目が20ポイントで残りが2列目」という意味になります。

111~112行目は、「ComboBoxのテキストボックス部の上に重ねるListBox」の設定です。ComboBoxのリストボックス部と同様の設定とするため、設定値は108~109行目と同じにしています。

114~117行目は、「上に重ねるListBox」の位置を指定しています。重ね方は、図7-6のようにComboBoxのテキストボックス部(リスト操作をしない時に表示されている部分)に重ねます。但し下三角ボタンは見えるように、117行目でListBoxの表示幅については調整をしています。
ComboBoxとListBoxを重ねる
図7-6

118行目は、重ねたListBoxをクリックするとComboBoxのリスト部がドロップダウンするようにするためです。既定の「Me.ListBox1.Enabled = True」のままだと、クリックするとListBox自体が選択されてしまい「ComboBoxと一体では無い」ことがバレてしまいます。

120行目は、Frame1の表示文字を消して「フレームの枠のみ」の状態にします。
122行目では「フレームの枠線の太さ」を計算しています。フレームには図7-6のように「外側の幅・高さ(Width/Height)」と「内側の幅・高さ(InsideWidth/InsideHeight)」がありますので、その差の半分が枠線の太さ(FrameBorder)となります。
フレームの枠の太さの求め方
図7-7

なお、122行目では幅の差(Width - InsideWidth)から太さを求めていますが、試してみたところ高さの差(Height - InsideHeight)で求めても同じ値が得られるようです。

124~127行目では図7-8のように、WebBrowserの外側をフレームの枠線が取り囲むように2つを重ねます。
ComboBoxとListBoxを重ねる寸法
図7-8

128行目では、Frame1の枠線を表示させています。図7-9のように、既定のBorderStyleでは枠線が無い(薄い色の凹みの様に見える)ので、実線にしています。
定数説明
BorderStyleNone0(既定)枠線表示なし
BorderStyleSingle1枠線表示あり
図7-9

7ー2ー2.フォーム表示時(Activateイベント)

登録ダイアログを表示させる際には、図7-10のActivateイベントが発生します。
  1. '========== ⇩(9) フォーム表示時のList1のへのデータ表示 =============
  2. Private Sub UserForm_Activate()
  3.  Me.TextBox1.Value = ""
  4.  Call ProListMake
  5. End Sub
図7-10

134行目では、「新規プロジェクト名」を入力するTextBox1を初期化します。(今回システムではUserForm2を終了する際には「Unload」で閉じますので、TextBox1の初期化は必須では無いことになります。もしHideで閉じるように改造された時の保険と考えています。)
また136行目では、別プロシージャ(ProListMake 図7-11)を呼び出し、「プロジェクトリスト」を作成しています。

7ー2ー3.プロジェクトリスト作成

図7-10の136行目、および図7-13の190行目から呼び出される「UserForm2のプロジェクトリスト作成」プロシージャが図7-11です。現在登録されているプロジェクト(削除されたものは除く)を全てComboBoxにリスト出力するものです。
  1. '========== ⇩(10) データベース接続とSQL実行 =============
  2. Private Sub ProListMake()
  3.  Dim SQL As String       '←SQL文
  4.  Dim Fld As Variant      '←抽出列名の戻り値
  5.  Dim temp As Variant      '←抽出データの配列
  6.  SQL = "Select ProNo,ProName from " & ProList & _
  7.     " where Del is Null " & " order by ProNo * 1 Desc"
  8.  temp = SQL_exec(SQL, Fld)
  9.  If IsEmpty(temp(0, 0)) = False Then
  10.   Me.ComboBox1.List = temp
  11.   Me.ComboBox1.ListIndex = 0
  12.  End If
  13. End Sub
図7-11

146~147行目のSQL文は、プロジェクトテーブル(ProList)からプロジェクト番号(ProNo)とプロジェクト名(ProName)を抽出する式です。また、抽出条件としては「where Del is Null」と「Del列に何も入っていない行」としており、今回システムの機能にはありませんが「不要になったプロジェクトにはDel列に『何か』を入れることで出力されない」ようにします。

なお、並べる順番は「order by ProNo * 1 Desc」と「プロジェクト番号の降順」とすることで「最新のプロジェクトを選択し易く」しています。
この式でプロジェクト番号を「ProNo * 1」と計算しているのは、「文字列→数値」に型変換するためです。図4-1を見てもらうと分かるように「ProNo列」のセルの左肩に「緑色の三角印」がついており、データベース上は数値とは見なされていないようなので「型変換」をしてから順番を決めています(この処理をしない場合、1~10までの文字列型数値の昇順での順序は「1,10,2,3,4,・・・」となります)。

149行目では、146~147行目のSQL文を実行しています。実行した結果(配列)は変数tempに代入されます。
151行目では「データが抽出されたか否か」を調べています。図5-5のプロシージャ内でデータが抽出されなかった時は、59~60行目が実行されますので、戻り値の配列はVariant型の規定値のEmptyとなっています。ですのでIsEmpty関数で調べFalseの場合は「データは抽出された」ということになり、152~153行目を実行します。
なお、もし「データは抽出されているのに、たまたまtemp(0,0)の場所が空だった」ことも考えられますが、図5-5の522行目で「空(Null)の場合は『""(長さゼロの文字列)』」に置換していますので、IsEmpty関数の結果はFalseとなるため「データが抽出されたものとして処理」が行われます。

今回はデータ処理プロシージャ(図5-5)の中でデータを「行列を修正」していますので、152行目ではそのままComboBoxのリストにしています。また153行目では、その内の1番目(ListIndex=0)のデータを最初の表示値にしています。
なお「データが抽出されなかった」場合にはListIndexは初期の「-1(選択されていない状態)」です。

153行目が実行された場合ComboBox1の値が変更されるわけですから、図7-15の「ComboBox1_Change」イベントが発生し、その中でComboBox1の上に重ねたListBox1にデータが入る(ユーザーには、2列のデータとして見える)ことになります。

7ー2ー4.プロジェクトの登録

CommandButton1(登録ボタン)をクリックした時に動作するのが図7-12です。
  1. '========== ⇩(11) プロジェクト登録ボタンをクリックした時 =============
  2. Private Sub CommandButton1_Click()
  3.  Dim ProName As String      '←新規プロジェクト名
  4.  ProName = Trim(Me.TextBox1.Value)
  5.  If ProName = "" Then
  6.   MsgBox "プロジェクト名を記入してから登録して下さい。"
  7.   Exit Sub
  8.  End If
  9.  Call ProMake(ProName)
  10. End Sub
図7-12

「新規プロジェクト名」は、ユーザーがTextBox1に手入力します。スペースだけ入力されても困りますので162行目ではTrim関数を使って「両端のスペースを削除」した後の文字列を変数ProNameとします。
なお、プロジェクト名に適さない文字列もチェックするのは人間にしか出来ないワザだと思いますので、事前に上長の承認を取ったプロジェクト名を担当が登録するとか、一旦登録した後に上長が正式許可したものしか読み込まない、などの面倒な仕組みが必要だと思います。

そのプロジェクト名を163行目でチェックし、空だったら164行目でコメントを出し165行目で終了します。
もし空でなかったら、168行目で「ProMakeプロシージャ(図7-13)」を呼出し、プロジェクトを登録します。

「ProMakeプロシージャ」は引数ProNameとして「プロジェクト名」を受け取ります。
  1. '========== ⇩(12) 新規プロジェクトの作成 =============
  2. Private Sub ProMake(ProName As String)
  3.  Dim SQL As String     '←SQL文
  4.  Dim Fld As Variant     '←抽出列名の戻り値
  5.  Dim temp As Variant     '←抽出データの配列
  6.  SQL = "Select max(ProNo) from " & ProList
  7.  temp = SQL_exec3(SQL, Fld)
  8.  SQL = "Insert into " & ProList & " (ProNo,ProName) " & _
  9.      " values (" & Val(temp(0, 0)) + 1 & ",'" & ProName & "')"
  10.  temp = SQL_exec3(SQL, Fld, False)
  11.  If IsEmpty(temp) = False Then
  12.   MsgBox "登録できませんでした。"
  13.  Else
  14.   Me.TextBox1.Value = ""
  15.   Call ProListMake
  16.  End If
  17. End Sub
図7-13

178~179行目は「現在のプロジェクトテーブル内の最大のプロジェクト番号」を取得し、その値を変数tempに代入しています。何らかプロジェクトが登録されている場合は、文字列としてその番号(例えば、"7")がtemp(0,0)に入り、もし新品のテーブルでしたら長さゼロの文字列("")がtemp(0,0)に入ることになります。(データが抽出されていない訳では無いので、temp(0,0)はEmptyではありません)

そのtemp(0,0)を使って181~182行目で「データを挿入するためのSQL文」を組み立てます。
テーブルの列ProNoにはtemp(0,0)で得られた最大プロジェクト番号+1の値を入力するのですが、temp(0,0)が長さゼロの文字列("")の場合は「"" + 1」という計算式になり、図7-14のようなエラーが発生してしまいます。
長さゼロの文字列と数値の足し算時のエラー
図7-14

そのため、Val関数を使って「長さゼロの文字列をゼロに変換」したのち足し算をさせています。
なお『"7" + 1』のように、1以上の文字列と数値の足し算では特にエラーは出ませんが、Val関数で変換しても悪さはしません。ですので、このVal関数は「初めてプロジェクトを登録する時だけの対応」ということになります。

テーブルの列ProNameには、引数のProNameを割り当てます。
(両方とも同じProNameという名前ですが問題はありません。もし紛らわしかったら異なる命名にして下さい。)
このInsert文を184行目で実行させます。その際Insert文はReadOnlyではありませんので、SQL_exec3の第三引数にはFalseを設定します。

SQLのInsert文が成功した(=テーブルにデータが入った)時には、SQL_exec3プロシージャはEmptyを戻してきますので、186行目でチェックし、Emptyで無い時(=テーブルへの書込み失敗)は187行目でコメントを出します。
なお、エラーの場合はFalseが戻すことになっていますので「If temp = False Then」という条件式でも良さそうですが、「Empty = False」は成立してしまうので、成功しても失敗してもコメントが出てしまうことになるので使えません。

Insert文が成功した時は189~190行目を実行します。
189行目はTextBox1内の文字列を初期化しています。つまり「プロジェクトの登録が成功したら、入力した文字列が消える」ことになります。
190行目では「ProListMake(図7-11)」を呼び出し、ComboBox1のプロジェクトリストを更新し、且つListBox1も「最も新しいプロジェクト(=今登録し終わったプロジェクト)が表示」されますので、「プロジェクトの登録が成功したら、プロジェクト名が下のコンボボックスに表示される」ことになります。

7ー2ー5.プロジェクトの選択

コンボボックスでプロジェクトの選択を変更した時に発生するイベントプロシージャが図7-15です。
  1. '========== ⇩(13) プロジェクトのコンボボックスの操作 =============
  2. Private Sub ComboBox1_Change()
  3.  If Not Me.ComboBox1.ListCount = 0 Then
  4.   Me.ListBox1.Clear
  5.   Me.ListBox1.AddItem ""
  6.   Me.ListBox1.List(0, 0) = Me.ComboBox1.List(Me.ComboBox1.ListIndex, 0)
  7.   Me.ListBox1.List(0, 1) = Me.ComboBox1.List(Me.ComboBox1.ListIndex, 1)
  8.  End If
  9. End Sub
図7-15

198行目の「プロジェクトが登録されていない時(ListCount = 0)以外」というIf文ですが、そもそもComboBox1にデータリストが無い(プロジェクトが登録されていない)場合は、データ変更をしようとしても出来ないのでChangeイベントは発生しないはずなのですが、トライで動かしている時に何度か反応した(勘違いかもしれません)ので、あえてIf文を付けました。(悪さはしないと思います)
なお、データが無い時に動いてしまうと、201~202行目のコードでエラーが出ることになります。

ComboBoxの選択行を変更すると、ComboBoxの表示部であるテキストボックス部に勝手に値が入ります。しかしリストボックス部が複数列の場合はテキストボックス部には「どれか1つの列の値」しか表示することが出来ません。ですので今回システムでは、図7-6のようにComboBoxの上にListBoxを重ね、あたかも複数列表示が出来るComboBoxのように作っています。
ですので自動的にテキストボックス部に選択行の値が入る訳ではないので、自分でListBoxに値を入れてあげる必要があります。

199行目で、まずはListBoxの値をクリアします。次に200行目で新たなリストの空枠を作ります。複数列のリストを作るには1行ずつ①~③のような順序でリストを作る必要があります。
 ①リスト行の空枠を作る(200行目)
 ②1列目に値を入れる(201行目)
 ③2列目に値を入れる(202行目)
リストボックスの行方向はゼロから始まりますが、列方向もゼロから始まりますので、List(0,0)がリストの左側、List(0,1)がリストの右側を表すことになります。

なおComboBoxへの登録と同様に、二次元配列のデータを一括登録してリスト化する方法も考えられますが、いちいち二次元配列を作るのが面倒なので、上記の様なベーシックな方法としました。

7ー2ー6.ドロップされたファイルの処理

プロジェクトを選択したら、次はファイルをドラッグ&ドロップで登録します。ドロップする枠には、今回WebBrowserコントロールを使っており、ファイルをドロップした時に発生するのが図7-16のBeforeNavigate2イベントです。
BeforeNavigate2イベントには多くの引数がありますが、今回使うのは「URL」と「Cancel」です。「URL」にはドロップしたファイルのフルパス+ファイル名、「Cancel」は操作の取り消しに使用します。

なお、以下の項でもWebBrowserコントロールを使ったアプリの紹介をしていますので参考にして下さい。
 「画像を直接シートに貼り付ける
 「あらゆるデータファイルをシートに貼り付ける
 「回転させた画像をシートに貼り付ける
 「CSVファイルの読み込み
  1. '========== ⇩(14) ドロップされたファイルの処理 =============
  2. Private Sub WebBrowser1_BeforeNavigate2(ByVal pDisp As Object, URL As Variant, Flags As Variant, _
  3.            TargetFrameName As Variant, PostData As Variant, Headers As Variant, Cancel As Boolean)
  4.  Dim NowTime As Single     '←枠を点滅させるための現在時刻
  5.  Dim Fso As Object        'FileSystemObjectオブジェクト
  6.  Dim FileBody As String     '←保存に使ったファイル名(Pathを除く。拡張子含む)
  7.  Cancel = True
  8.  If Me.ComboBox1.ListIndex = -1 Then
  9.   MsgBox "プロジェクトを選択して下さい"
  10.   Exit Sub
  11.  End If
  12.  Set Fso = CreateObject("Scripting.FileSystemObject")
  13.   If Fso.folderexists(URL) = True Then
  14.    MsgBox "フォルダーごとは登録できません"
  15.    Exit Sub         '←URLがディレクトリだった場合にはExit Sub
  16.   End If
  17.  Set Fso = Nothing
  18.  If FCopy(URL, FileBody) = True Then
  19.   Me.Frame1.BorderColor = RGB(255, 0, 0)
  20.   NowTime = Timer()
  21.   Do While (NowTime + 0.5) > Timer()
  22.    DoEvents: DoEvents
  23.   Loop
  24.   Me.Frame1.BorderColor = RGB(0, 0, 0)
  25.   If DCopy(Me.ComboBox1.List (Me.ComboBox1.ListIndex, 0), FileBody) = True Then
  26.    Me.Frame1.BorderColor = RGB(0, 255, 0)
  27.    NowTime = Timer()
  28.    Do While (NowTime + 0.5) > Timer()
  29.     DoEvents: DoEvents
  30.    Loop
  31.    Me.Frame1.BorderColor = RGB(0, 0, 0)
  32.   End If
  33.  End If
  34. End Sub
図7-16

このWebBrowserコントロールの本来の機能は「サイト等のURLをドラッグ&ドロップでコントロール上に落とすと、そのURLのサイトを開く」というものです。しかし今回システムではドロップしたファイルを開く訳ではないので、まず215行目の「Cancel = True」で「開く操作の取り消し」を行います。

217~220行目は「プロジェクトが登録されていない時」の処理で、218行目でコメントを出し、219行目で中止します。

222~227行目では、ドロップしたものが「フォルダー」の場合は中止させています。
ここで、今回システムにはどのようなものが登録可能かを整理しておきます。

FileAttribute
列挙型の要素名
属性(特徴)今回の
登録可否
0Normal標準ファイル
1ReadOnly読取専用ファイル
2Hidden隠しファイル××
4Systemシステムファイル××
8Volumeドライブ、ボリュームラベル
16Directoryフォルダ、ディレクトリ×
32Archiveアーカイブファイル
1024Aliasリンク、ショートカット
2048Compressed圧縮ファイル
複数ファイル
図7-17

図7-17は「指定フォルダ配下のファイル情報を取得」でも紹介したファイル種類ですが、この種類1つ1つが今回システム内でどの様な挙動をするか確認してみました。

すると、××のついた「隠しファイル」と「システムファイル」は、図7-18内のFileSystemObject.copyfileメソッドでのファイルコピーが(エラー無しで)行われないことが分かりました。もちろんエクスプローラ上でのコピーは可能です。
実用上「隠しファイル」くらいは登録可能対象にしたいのですが、今回は断念しました。コピーできるようにするには、FileSystemObjectでは無いコピーメソッドでトライしてみるか、またはファイル属性を変更してコピーするしかなさそうです。今後の課題とさせて下さい。

また「ドライブ・ボリュームラベル」は、エクスプローラ等でドラッグできないと考え無視しました。一番下の「複数のもの」については「最初に選択したファイル名のみを、WebBrowserのBeforeNavigate2のURLとして受け取る」ために、「最初に選択したファイルのみがコピーされる」ことになります。
使用するコントロールをWebBrowserでは無くListViewコントロール(どのPCのExcelにも入っている訳では無い)を使うと可能になります。使用例については「画像を直接シートに貼り付ける」を参照下さい。

「フォルダ、ディレクトリ」については、工夫すれば内部ファイルのコピーは可能なのでしょうが、今回は対象外としました。
第一の理由はcopyfileメソッドがフォルダーに対応していないためですが、もし実現するのであれば「フォルダー内を分解しファイル単位にしてからコピー」するか、フォルダー単位でコピーできるようにした後「ファイルリストでフォルダーをクリックすると、エクスプローラの様にその中のファイルが選択できる」ような窓を作る ことが必要になると思います。前者の方がプログラムとしては楽そうです。

以上の理由から222~227行目では「フォルダーは対象外」としての処理をしています。
まず、ドロップされたURLがフォルダーか否かを確認するために「FileSystemObject」の「folderexists」を使い、True(=URLはフォルダー)であればコメントを出してから終了しています。
このプロシージャ内で「FileSystemObject」を使うのは、この部分だけですので、222行目でオブジェクト生成をし、終了した227行目で解放しています。

229行目では、図7-18の「FCopy関数プロシージャ(File Copyの略)」を呼び出し、ファイル本体をコピー(ファイル元 → 図5-1のFile_Box定数で定めたフォルダー)します。コピーが成功したらTrueを戻すことになっていますので、成功時は230~249行目を実行することになります。

230~237行目は、図2-3で説明した「ファイルがサーバーに入った印」の一連の動きになります。
まず、230行目でFrame1(ドロップするWebBrowserの外枠部分)の枠色を赤色にします。
次に232~235行目で「0.5秒間 待つ」ことをします。232行目で現在時刻をTimer関数を使ってミリ秒単位で取得し、233行目で0.5秒間過ぎるまでDo~Loopで回します。0.5秒間が過ぎたら237行目ででFrame1の枠色を黒色に戻します。
これにより「枠が0.5秒間赤色になる」ことになります。

239行目は、図7-20の「DCopy関数プロシージャ(Data Copyの略)」を呼び出し、ファイル情報(どのプロジェクトのファイルか、コピー先のファイル名、コピー日)をドキュメントテーブルに記入します。
ですので「ファイルのコピーが成功しないと、テーブルには情報が書き込まれない」逆に言うと「テーブルに書き込まれたファイルは、必ずフォルダーに存在する」ことになります。
DCopyプロシージャには2つの引数を渡します。第一引数は「Me.ComboBox1.ListIndex, 0)」で、コンボボックスで選択した「プロジェクト番号」です。また第二引数の「FileBody」は「サーバーへCopyした時の(パスを除いた)ファイル名」で、229行目のFCopyプロシージャの引数として戻された保存ファイル名となります。

ドキュメントテーブルへの書込みが成功するとTrueが返りますので、241~248行目でFrame1の枠色を「緑色→0.5秒保持→黒色」に点滅させます。プログラムは230~237行目と同じで色のみが異なります。

7ー2ー7.ファイル本体の保存

図7-16の229行目から呼び出されるFCopy関数プロシージャが図7-18です。
第一引数として、コピー元のフルパス+ファイル名であるURL、第二引数のFileBodyは「ドキュメントテーブルに登録するコピー先のファイル名」で、ファイルコピーが成功した後のDCopy関数プロシージャへ渡すデータとなります。
  1. '========== ⇩(15) ファイル本体の保存 =============
  2. Private Function FCopy(URL As Variant, FileBody As String) As Boolean
  3.  Dim FileName As String        '←ファイル名(フルパス+フルファイル名)
  4.  Dim FileTitle As String        '←ファイル名本体(ピリオド+拡張子を含まず)
  5.  Dim FileTitleNew As String      '←ファイル名本体(サーバーに保存する為に加工した名前)
  6.  Dim FileExt As String         '←拡張子(ピリオドを含む)
  7.  Dim i As Long             '←保存ファイル名の変更回数
  8.  Dim Fso As Object           '←FileSystemObjectオブジェクト
  9.  FileBody = Right(URL, Len(URL) - InStrRev(URL, "¥"))
  10.  If InStr(FileBody, ".") = 0 Then
  11.   FileTitle = FileBody
  12.   FileExt = ""
  13.  Else
  14.   FileTitle = Left(FileBody, InStrRev(FileBody, ".") - 1)
  15.   FileExt = Right(FileBody, Len(FileBody) - InStrRev(FileBody, ".") + 1)
  16.  End If
  17.  FileTitle = Replace(Replace(FileTitle, ")", ""), "(", "")     '←ファイル名の中の'括弧を消去
  18.  FileTitleNew = FileTitle
  19.  FileName = FILE_BOX & FileTitleNew & FileExt
  20.  Do Until Dir(FileName) = ""
  21.   i = i + 1
  22.   FileTitleNew = FileTitle & "-" & i
  23.   FileName = FILE_BOX & FileTitleNew & FileExt
  24.   DoEvents: DoEvents
  25.  Loop
  26.  Set Fso = CreateObject("Scripting.FileSystemObject")
  27.   Fso.copyfile URL, FileName
  28.  Set Fso = Nothing
  29.  If Not Dir(FileName) = "" Then
  30.   FCopy = True
  31.  End If
  32.  FileBody = FileTitleNew & FileExt
  33. End Function
図7-18

263行目は引数で受け取ったURL(ドロップされたファイルのフルパス+ファイル名)からファイル名部分(FileBody)を切り出しています。方法として、今回は図7-19のように「後ろから「¥」印を探索」し、全長との差から切り出し文字数を決めています。

ファイルのURLからファイル本体部分を切り出し
図7-19

265~271行目は、ファイル名(FileBody)から拡張子部分を切り出しています。ただし拡張子の無いファイル名もありますので、265行目の「InStr(FileBody, ".")」の結果の「ピリオドの有無」で「拡張子の有無」を判断しています。
拡張子が無い(ピリオドが無い)場合は、拡張子の変数FileExtに長さゼロの文字列を代入します。
一方拡張子が有る(ピリオドが有る)場合は、図7-19と同じ要領で後ろ側からピリオドの位置を特定し、拡張子の無いファイル名本体(FileTitle)と拡張子(FileExt)に分けます。なおピリオドは、今回の都合上、拡張子(FileExt)側に入れます。

273行目では、ファイル名本体(FileTitle)に付いているカッコを消去しています。
例えば「sample.pdf」というファイルをコピー等したとき、コピー先に同名ファイルがあると「sample(1).pdf」という様なファイル名になることがあります。このようなカッコ付ファイルを今回システムでトライしてみると「カッコのあるファイル名は、cmd.exe で自動実行出来ない」ため、カッコを消去しています。
もちろん、実行時には一連の文字列にする為にファイル名の両端を「"(ダブルクォーテーション)」で囲っていても実行できませんし、且つコマンドプロンプト上で同じ動作を手動で実行すればファイルを開くことができますので、ますます原因が分かりません。
(もしかしたらPCの環境によるのかもしれませんが、追い切れていません)
ですので273行目では、Replace関数を使って「カッコを長さゼロの文字列に置換」して、カッコを消去しています。

275行目は、カッコを消去したファイル名本体(FileTitle)を「FileTitleNew」という変数に代入しています。「FileTitleNew」はサーバーのフォルダー内に保管する際のファイル名本体になります。
FileTitleNewは、「ファイル本体コピー(FCopy)」の次の処理である「ファイル情報をテーブルに記入(DCopy)」に渡す値に使用するものですが、この時点でFileTitleNewという変数にファイル名本体を代入している理由は、2つあります。

1つは、コピー元のファイル名がそのままコピー先ファイル名に使える(フォルダー内に同名ファイルが無い)時には279~284行目の「Do~Loopを通過しないために、変数FileTitleNewに値を入れるにはここしか無い」ことです。
2つ目は、279~284行目のDo~Loop内でファイル名に追番(『-1』など)を追加するとき、275行目の位置で「FileTitle値を保存」しておかないと、281行目で「追番に追番をつける(例えば3回Do~Loopを回ると、ABC.pdf→ABC-1-1-1.pdf)」というような追番の付け方になってしまうからです。そのため281行目は「FileTitleNew = FileTitle & "-" & i」と、元のFileTitleをベースにして追番を付けています。
(2番目の理由は、あまり理由になっていない気もしますが)

今回は1つのフォルダーに大勢の人がファイルを登録することになりますので、同じファイル名が重複する可能性があります。ですので後から登録されるファイルは、先に登録されたファイル名とは重ならないように、279~284行目で「ファイル名本体の後ろに『-1』のように追番をつける」ことにしました。
重複しないファイル名にするためには、一般的にはファイル名本体の最後に『(1)』のようにカッコ付きで追番を付ける という印象がありますが、上記の「cmd.exeでカッコ付が自動実行できない」ことから、今回『-1』のような形にしました。

277行目では、一旦「フルパス+ファイル名本体+拡張子」を結合し、変数「FileName」に代入します。
279~284行目のDo~Loopの継続条件は、279行目に記述されている「Until Dir(FileName) = ""」になります。これはDir関数を使って「変数FileNameというファイルが存在しなくなるまで」という条件式になります。

ですので、277行目で組み立てた変数「FileName」が存在しなければ、Do~Loop内を実行せずに286行目に進みます。
もし「FileName」が同じフォルダー内に存在した場合は、280行目でカウンタ変数iを1つ増やし、281行目でファイル名本体の後ろに『-1』と追番を追加します。
そして追番を付けたファイル名本体(FileTitleNew)を使って277行目と同様に「フルパス+ファイル名本体+拡張子」を結合し、Do~Loopは一周して再び「Until Dir(FileName) = ""」で変数FileNameの存在を確認します。

追番を追加したファイル名でOK(=フォルダー内には、そのファイル名は無い)であれば286行目に移行します。まだフォルダー内に同名ファイルが存在すれば、Do~Loop内をもう一周し『-2』という追番を追加して再チャレンジします。フォルダー内には有限数のファイルしかありませんので、追番を変えていけばいつかはOKになるはずです。

286行目では、FileSystemObjectオブジェクトを生成し、287行目の「copyfileメソッド」を使って「元ファイル → サーバー」へファイルコピーを行っています。
(このcopyfileメソッドが、図7-17で説明したように「隠しファイル」や「システムファイル」に対応していない様です。)

290行目では、copyfileメソッドで「コピーしたつもり」のFileNameが「フォルダーに本当に保存されているか」を確認します。保存されている「Not Dir(FileName) = ""」ならば、291行目でFCopy関数プロシージャとしてTrueを戻します(FCopyプロシージャはBoolean型ですので、Trueを設定するまでは既定値のFalse(=保存失敗)になっています)。

最後に294行目で、サーバー保存した時のファイル名(FileTitleNew & FileExt)を引数FileBodyに代入してから、FCopy関数プロシージャを終了します。

7ー2ー8.ファイル情報の保存

図7-16の239行目から呼び出される「サーバーに保存したファイル情報をテーブルに書き込む」関数プロシージャが図7-20です。
引数は2つあり、第一引数は「プロジェクト番号」、第二引数はサーバーに保存された時のファイル名(パス名を除いたファイル名)を受け取ります。
  1. '========== ⇩(16) ファイル情報をテーブルに保存 =============
  2. Private Function DCopy(ProNo As Long, FileBody As String) As Boolean
  3.  Dim SQL As String    '←SQL文
  4.  Dim Fld As Variant    '←抽出データのカラム名配列(今回はダミー)
  5.  Dim temp As Variant    '←SQL実行プロシージャからの戻り値(今回は)
  6.  SQL = "Insert into " & DocList & "(ProNo,Fname,UpD) " & _
  7.            " values ( " & ProNo & ",'" & FileBody & "','" & Date & "')"
  8.  temp = SQL_exec3(SQL, Fld, False)
  9.  If IsEmpty(temp) = True Then
  10.   DCopy = True
  11.  End If
  12. End Function
図7-20

304~305行目のSQL文は、Insert文でDocList(ドキュメントテーブル)に「ProNo(プロジェクト番号)」と「Fname(ファイル名)」および「UpD(コピーした日付)」をデータ挿入するものです。

307行目では、そのSQL文を実行(Insert文ですので、第三引数ReadOnlyにFalseを設定しています)し、成功したか否かの情報を変数tempで受け取ります。
temp値を309行目で確認し、成功(True)してれば、310行目でDCopy関数プロシージャとしてTrueを戻します。

7ー2ー9.登録ダイアログの終了

登録ダイアログの「戻る」ボタン(CommandButton2)をクリックした時のイベントプロシージャが図7-21です。
  1. '========== ⇩(17) 登録ダイアログの「戻る」ボタン =============
  2. Private Sub CommandButton2_Click()
  3.  Unload Me
  4. End Sub
図7-21

317行目でダイアログを閉じています。通常の操作であれば、登録ダイアログを閉じることで「メニューダイアログ(UserForm1)」に戻ることになります。(UserForm1を先に閉じてしまう事も可能です。)

8.ドキュメントの閲覧(UserForm3)

登録されたドキュメント(ファイル)を閲覧するのが閲覧ダイアログ(UserForm3)です。

8ー1.コントロールの配置

閲覧のフォーム上の各コントロールの配置は、図8-1のようにしました。
左側にプロジェクトのリスト、右側にファイルのリストをListBoxに表示させます。CommandButton1はダイアログを終了させるためのものです。
UserForm1でのコントロール配置
図8-1

8ー2.フォームモジュール

8ー2ー1.ダイアログ起動時

ダイアログの起動時に呼び出されるInitializeイベントプロシージャが図8-2です。
  1. '========== ⇩(18) ダイアログ起動時の初期設定 =============
  2. Private Sub UserForm_Initialize()
  3.  Me.CommandButton1.Caption = "戻る"
  4.  Me.ListBox1.ColumnCount = 2
  5.  Me.ListBox1.ColumnWidths = "20;"
  6. End Sub
図8-2

322行目は「戻る」ボタン(CommandButton1)の表面に文字を記入しています。
324行目は、左側の「プロジェクトリスト(ListBox1)」を「プロジェクト番号」と「プロジェクト名」の2列を表示させるため、2列表示にしています。またListBox1の1列目の幅を20ポイント、残りの幅を2列目用にしています。
なお、この設定をせずに図8-4で二次元配列データを貼り付けると、2列のリストにはなりません。

8ー2ー2.ダイアログ表示時にプロジェクトリストを表示

ダイアログを表示する時に実行するActivateイベントプロシージャが図8-3です。
  1. '========== ⇩(19) ダイアログ表示時の設定 =============
  2. Private Sub UserForm_Activate()
  3.  Call ProListMake
  4. End Sub
図8-3

331行目で、図8-4の「ProListMake」プロシージャを呼び出します。
  1. '========== ⇩(20) プロジェクトリストの作成 =============
  2. Private Sub ProListMake()
  3.  Dim SQL As String    '←SQL文
  4.  Dim Fld As Variant    '←抽出データのカラム名配列(今回はダミー)
  5.  Dim temp As Variant    '←SQL実行結果(プロジェクトリストの配列)
  6.  SQL = "Select ProNo,ProName from " & ProList & _
  7.          " where Del is Null " & " order by ProNo*1 Desc"
  8.  temp = SQL_exec3(SQL, Fld)
  9.  If IsEmpty(temp(0, 0)) = False Then
  10.   Me.ListBox1.List = temp
  11.  End If
  12. End Sub
図8-4

340~341行目のSQL文は、「ProNo(プロジェクト番号)」と「ProName(プロジェクト名)」を「ProList(プロジェクトテーブル)」から取り出します。なお削除項目に印の付いているものは除き(where Del is Null)、また「order by ProNo*1 Desc」はプロジェクト番号を降順(新しいプロジェクトが上になる)で並べます。

343行目でSQL文を実行し、戻されたデータ配列を変数tempに代入します。
そのtempの値を345行目のIf文で調べます。SQL_exec3プロシージャは、抽出されたデータが1つも無かった時にtemp(0,0)は「Empty」となり、データが有った場合には「数値や文字列など」または「長さゼロの文字列」が入ります。
ですので「IsEmpty(temp(0, 0)) = False」の時には、346行目でデータ配列をListBoxに貼り付けます。

8ー2ー3.プロジェクトリストのクリック時のファイルリスト表示

左側ListBox1(プロジェクトリスト)のどれかをクリックした時のイベントプロシージャが図8-5です。
  1. '========== ⇩(21) プロジェクトリストをクリックした時 =============
  2. Private Sub ListBox1_Click()
  3.  Call ProDocMake(Me.ListBox1.List (Me.ListBox1.ListIndex, 0))
  4. End Sub
図8-5

クリックした行は「Me.ListBox1.ListIndex」で得られます(上から0,1,2・・・の番号)。またListBox1の1列目の値を取得するには「Me.ListBox1.List(選択行位置,0)」ですので、353行目の「ProDocMakeプロシージャ」に引数として渡す「Me.ListBox1.List(Me.ListBox1.ListIndex, 0)」は「ListBox1の選択した行の1列目の値」となります。

その呼び出される「ProDocMakeプロシージャ」が図8-6で、引数として「ListBox1の選択した行のプロジェクト番号」を受取ります。プロシージャ内では「ProNo」という変数になります。
  1. '========== ⇩(22) プロジェクト別のファイルリストを表示 =============
  2. Private Sub ProDocMake(ProNo As Long)
  3.  Dim SQL As String    '←SQL文
  4.  Dim Fld As Variant    '←抽出データのカラム名配列(今回はダミー)
  5.  Dim temp As Variant    '←SQL実行結果(ドキュメントリストの配列)
  6.  SQL = "Select FName from " & DocList & _
  7.        " where ProNo*1 = " & ProNo & " and Del is Null " & " order by UpD Desc"
  8.  temp = SQL_exec3(SQL, Fld)
  9.  If IsEmpty(temp(0, 0)) = False Then
  10.   Me.ListBox2.List = temp
  11.  End If
  12.  Me.ListBox2.SetFocus      'ListBox2起動後の初回もダブルクリックが効くようにする。
  13. End Sub
図8-6

362~363行目のSQL文は、「DocList(ドキュメントテーブル)」から「FName(ファイル名)」を取り出します。絞り込む条件は「ProNo*1 =『LisbBox1で選択したプロジェクト番号』」、及び削除項目に印の付いているものは除いて(Del is Null)いるので、「指定したプロジェクトのドキュメントとして登録されたファイルで、且つ削除されていないもの」が取り出されます。
出力する順番は「登録日付(UpD)」の降順(Desc)で、新しいものが上に来ることを表すSelect文になります。

365行目でそのSQL文を実行し、ファイル名の配列が変数tempに代入されます。
367行目では「temp(0,0)」の値を調べ、Emptyでない(=抽出データ有り)場合は、368行目でListBox2にデータを入れます。

以上で一応ファイル一覧をリスト化できるのですが、少し不具合があったので371行目の「Me.ListBox2.SetFocus」を追加しました。
369行目まででListBox2にはファイルのリストが完成しており、次の工程「任意のファイルの行をダブルクリック」を待つ状態になるのですが、「ダブルクリックをしても、ファイルが開かない」不具合が何回か発生したのです。調べてみると「ダブルクリックしてもDblClickイベントが発生しない」場合があるようなのです。

私が考えた原因は、「1回目のクリックではListBox2をActiveにするだけ(この時は、まだListIndex=-1)」「2回目のクリックで、目的のファイル行を選択」ということが起こっているのではないか、ということです。100%発生する訳でもないので明言はできませんが、そうだとすれば「先にListBox2をActive」にしておけば「ダブルクリックが違和感なく反応する」のでは、と考え371行目を追加した次第です。
もしかしたら的外れな対応なのかもしれませんが、今のところ起動直後でもダブルクリックでファイルが開いているので、この仕様としています。

8ー2ー4.ファイルリストのダブルクリック時

ファイルリスト(ListBox2)をダブルクリックしたときに動作するプロシージャが図8-7です。
  1. '========== ⇩(23) ダブルクリックによるファイルの表示 =============
  2. Private Sub ListBox2_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
  3.  Dim cmd As String     '←実行するコマンド(=表示するファイル名)
  4.  If Me.ListBox2.ListIndex = -1 Then Exit Sub
  5.  cmd = Chr(34) & FILE_BOX & Me.ListBox2.List(Me.ListBox2.ListIndex, 0) & Chr(34)
  6.  Shell Environ("ComSpec") & " /c " & cmd
  7. ' Dim result As Object
  8. ' Dim wsh As Object
  9. ' Set wsh = CreateObject("WScript.Shell")
  10. ' wsh.exec "%ComSpec% /c " & cmd
  11. ' Set wsh = Nothing
  12. End Sub
図8-7

379行目の「If Me.ListBox2.ListIndex = -1」は、「ファイルリストのどれも選択状態で無い時」に処理を中止するものです。
通常「ダブルクリックするのであれば、どれかを選択している」と思いそうですが、例えば「ファイルリストに1件も表示されて無いのに、ListBox2をダブルクリック」とか「ファイルリストが数件しか無く、ListBoxの下の余白部分をダブルクリック」した時にも「DblClick」イベントは発生します。
ですので、どれも選択していない時は中止をさせています(どれも選択されていないので、ファイルを表示のしようがありません)。

381行目は、表示をするファイル名を「フルパス(FILE_BOX)」+「選択しているファイル名(Me.ListBox2.List(Me.ListBox2.ListIndex, 0))」という形にしています。且つ「フォルダー名やファイル名の中にスペース」があると、実行する時に「コマンドとして、そこで切れている」と判断されてしまうため、一連の文字列にするため「フルパス+ファイル名」の両端を「"(ダブルクォーテーション)」で囲みます。

381行目では「Chr(34)」という「34という文字コードを文字列として表示」する関数を使用しています。
もし「"(ダブルクォーテーション)」を直接使うのであれば「ダブルクォーテーションという文字列を表すために、その前にダブルクォーテーション」をつけ、「パス+ファイル名」全体を文字列扱いするために、全体をダブルクォーテーションで囲む必要があります。
ですので、ダブルクォーテーションを3連にして、「パス+ファイル名」の前後に付けることになります。
なお381行目の「Chr(34)」の代わりにダブルクォーテーションを使う場合は、ダブルクォーテーションを4連にします。

383行目では、コマンドプロンプト上でファイル名を実行させています。
Environ関数の引数にコマンドプロンプトの環境変数を指定することで、「コマンドプロンプトの実行ファイル」を表すことになります。 また、Shell関数は「外部の実行可能プログラムを実行」するものなので、383行目としては「コマンドプロンプトを実行する」ことになります。
そのコマンドプロンプトに渡すのが381行目で組み立てたファイル名ですので、「ファイル名の拡張子をWindowsが判断してアプリケーションを起動しファイルを開く」ことになります。

なお、Shell関数やEnviron関数を使わない方法としては、385~389行目で見え消しにしてある「Shellオブジェクトのexecメソッド」を使う方法もあります。但し、このままだと「裏側でコマンドプロンプトが動いている」のが丸見えになってしまいます。
Shellオブジェクトを使った他のアプリとして「アンケートの回収と集計方法」も参照下さい。

8ー2ー5.ダイアログ終了ボタン

ダイアログの「戻る」ボタンをクリックした時に動作するのが図8-8です。Unloadでフォームを閉じます。
  1. '========== ⇩(24) 「戻る」ボタンをクリック =============
  2. Private Sub CommandButton1_Click()
  3.  Unload Me
  4. End Sub
図8-8

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

このマクロをExcelの機能の1つに登録し、Excel上部のリボンのボタンを押せばシステムを起動できるようにできます。
その方法については「年賀状リスト等の宛名検索と追記 アドイン登録」を参照下さい。

10.最後に

本システムは、資料共有といっても「1つのサーバーフォルダーに資料ファイルのコピーを集めている」だけですので、閲覧しているファイルの内容を書き換えて上書き保存することも可能です。ファイルの属性を読み取り専用にしておけば、編集後の保存時に同名で保存が難しくなりますが完璧ではありません。
ですので、編集されて困る資料はPDFで作りパスワード等で編集不可の状態にしてからファイル登録する必要もあるかもしれません。また逆に「書き換え可能」を利用して、各人が1つのファイルに対して書込みを行うような使い方も可能だと思います(同時書込み防止をどうするか考える必要はありますが)。

また「社内にサーバーなんか無い」ような環境の場合は、ネットワークさえつながっていれば誰かのPC内のフォルダーを共有に設定し、皆でそこにアクセスする方法もあります。そういう小さな改善から「サーバーでファイルを共有することのメリット」が出てくれば、サーバー導入などの動きにも広がると思います。


共有資料の登録と閲覧ができるサーバーシステム(it-051.xlsm)

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