2022/06/08

Accessデータベースを使用した売上台帳




1.背景

前回「ExcelからAccessデータベースを作成・操作」では、AccessのデータベースファイルをExcel側から作成・操作する方法について説明しました。今回は、そのAccessデータベースを実際に活用する事例として、簡単な「売上台帳」を作成しましたので紹介します。

以前「ExcelシートDBとSQLを使った倉庫管理システム」等でExcelシートをデータベースとしたものを紹介してきましたが、それらとプログラムの流れ自体はほぼ同じです。しかしAccessデータベースを利用した場合は以下のような特徴を活かせるため、より厳密で安心なデータ管理が可能となります。今回は、この特徴を盛り込んで作っています。
 ・テーブルの列のデータ型が定められる
 ・テーブルの列に対しNot NullやUnique等の制約を設定できる
 ・自動連番が可能
 ・SQLのDelete文が使用できる

なお、Accessデータベースを使ったものとしては、以下のものも有りますので併せて参照下さい。
設備の稼働状態を入力し、グラフで確認

2.システム概要

2-1.全体の流れ

今回の「サンプルファイル」を開くと、図2-1の様な作業シートになります。
シート上には3つのボタンがあり、通常は「入力」ボタンから売上情報の入力をしますが、事前に取引先データ、商品データを登録しておく必要があります。取引先データの作成・メンテには「取引先」ボタンを、商品データには「商品」ボタンをクリックします。
またシート上には各項目が並んでいますが、データを出力した際のタイトルになるようにしています。
作業シート
図2-1

なおシートが開く際に、使用するデータベースファイルとテーブルの状態を調べ、正常な状態であれば図2-2の左側のようなメッセージを、データベースファイルが存在しなければ自動的に作成した後に図2-2の右側のようなメッセージを出します。
ですので、データベースファイルが無い状態でサンプルファイルを開いた際には、サンプルファイルと同じフォルダーに「データベースファイル(ファイル名:it-082.accdb)」が作成されます。
データベースの確認
図2-2

今回は3つのダイアログを使用しますが、その関係について説明します。
売上が発生した時点で入力するデータは、「売上台帳ダイアログ(図2-3の中央)」に入力をします。
データの流れ
図2-3

そのダイアログ内の「取引先」と「商品」は毎回異なる事は少ない為「選択式」とし、そのリストは「別なテーブル」からデータを持ってきています。別なテーブルのデータを管理しているのが、取引先・商品のダイアログという事になります。

2-2.取引先データの登録

売上データを入力する前に、取引先データ・商品データを作成しておく必要がありますので、そちらを先に説明します。
シート上の「取引先」ボタンをクリックし、表示されるダイアログ上で取引先のデータを入力します。
まず取引先の情報として「取引先番号」「取引先名」を入力(図2-4の左側)し、下部の「登録/更新」ボタンをクリックします。するとリストに項目が登録(図2-4の右側)されます。
取引先データの登録
図2-4

今回、取引先番号は「文字列」型としていますので、アルファベット(大文字のみ)が付いていても数字だけでもOKです。また、取引先番号はUnique制約を付けていますので重複はNGですが、取引先名は重複可です。
またリストは取引先番号順で表示しています。但し取引先番号が文字列のため、文字コード順に並びます。そのため「10」の後に「3」が来るような、数値としては不自然な並びになることがあります。

登録先情報を変更する際は、リストボックス内の該当する項目を選択(図2-5の左側)し、上部のテキストボックス内で文字列を変更(図2-5の中央)します。その後「登録/更新」ボタンをクリックすることで情報が書き換わり(図2-5の右側)ます。
取引先データの変更
図2-5

なお図2-5では、Uniqueでは無い「取引先名」を変更しているため「既データの変更」になりましたが、取引先番号を「リストには無い番号」に変更した場合は「データの新規追加」という扱いになります。また、取引先番号を「リスト内の異なる番号」にした場合は、その異なる番号の取引先名を変更することになりますので、注意が必要です。

登録先情報を削除する際は、リストボックス内の該当する項目を選択(図2-6の左側)し、「削除」ボタンをクリックします。
取引先データの削除
図2-6

削除しようとする取引先が、売上台帳のデータに使われていない場合は、素直に「データから削除」されます。しかし売上台帳のデータに使われている場合は、図2-6の右側の様にデータに「レ点」を付け、売上台帳のコンボボックスには表示されないようにしています。これは「売上台帳のデータの一部に、取引先データを使用」しているので「削除してしまうと、データの整合性が崩れる」ため、「見掛け上、削除相当」にしています。
なお、レ点の付いた項目を図2-5の手順で変更する際、「非表示のチェックボックスを消す」ことで、データ復活させることが出来ます。

2-3.商品データの登録

基本的には「取引先」データの操作と同じですが、商品情報には「単価」が必要なため1つ入力項目が増えます。
商品データの登録
図2-7

但し、同じ商品であっても「単価」は変化する可能性があります。商品データ側の単価は最新にしておく事はもちろん必要な事ですが、売上台帳に残す単価は「売り上げた時の単価」にする必要があります。ですので売上台帳側のデータには、商品データに結び付いた単価では無く、「売り上げた時の単価」を保存しておきます。

2-4.売上データの登録

シート上の「入力」ボタンのクリックにより表示される売上台帳には「伝票番号」「売上の日付」「取引先」「商品」「売上個数」を入力します。
「伝票番号」は「文字列」型としているため、アルファベット(大文字のみ)・数字が使用できます。「売上の日付」は、今回はテキストボックスへの手入力としてあります。「ボタンを自動生成するフォームカレンダー」等を参考にしてカレンダー入力方式とすることは可能です。
「取引先」「商品」は、図2-8のようにコンボボックス内から選択します。「取引先」のリストには取引先番号・取引先名を表示しています(選択後は、取引先名のみになります)。また「商品」のリストには商品番号・商品名・最新の単価を表示し、選択後は「単価の値は単価のラベルにデータを移動」し、入力した「売上個数」と掛け合わせて合計値としています。
売上データ入力は取引先・商品を選択
図2-8

入力を全て完了したら図2-9のように、上部左端の「登録/更新」ボタンをクリックすることで「入力情報がリストへ登録」されます。
売上データの登録
図2-9

ダイアログ下部のリストには図2-10の様に「売り上げ日付に対して降順に表示」し、「最新のものが一番上に来る」ようにしています。日付が同じ場合は、SNo(=売上台帳番号)の降順になります。
売上リストは降順
図2-10

ダイアログの上部の「リスト出力」ボタンをクリックすることで、売上リストはワークシート上に「昇順(=日付の若いデータを前)」で書き出します。書き出すデータ量は「年単位」で、ダイアログ上部の「表示年」のテキストボックスの年のデータが対象となります。なお書き出す直前に、以前のデータはクリアされます。
売上データのシートへの出力
図2-11

ダイアログ上部の「表示年」は、起動直後は「今年」の値が表示(=ダイアログのリストも今年の売上データが表示)されます。その表示年のテキストボックスの値を手動で変更することで、リストデータ及びワークシートに出力されるデータを「表示年のデータ」にすることが出来ます。なお表示年のテキストボックスを変更しただけではリストは変更されませんので、横の「リスト再表示」ボタンをクリックして下さい。
リスト・出力データの表示年を変更可
図2-12

売上台帳のデータを削除するには、図2-13のように削除するデータを選択し、上部の「削除」ボタンをクリックします。
売上データの削除
図2-13

「取引先データ」や「商品データ」を削除する時には「売上台帳で使用中の場合はエラーが出てしまう為にレ点を付ける」と 説明しましたが、売上台帳のデータを削除する時には「エラーが出ない(=他のテーブルに悪影響を与えることは無い)」ために削除可能です。(元には戻せないので、注意は必要です。)

3.プログラムの流れ

3-1.テーブルの構造

まず今回システムのテーブルは、図3-1のように「Sale」「Cust(Customerの略)」「Pdct(Productの略)」の3つに分けています。それぞれのテーブルにはPrimary Keyを設けており、メインである売上台帳のSaleテーブルの「CustNo」列~「Cust」テーブル、また「PdctNo」列~「Pdct」テーブルを参照制約(Foreign Key)で結び付けています。

テーブルの仕様
図3-1

テーブルの列には、各々データ型を設定する必要があります。Accessの主なデータ型を図3-2に示しましたが、「伝票番号」「取引先番号」「商品番号」の「人間が設定した番号」については、アルファベットを頭番号とする場合もあると考え、VBAで言うとString型(SQLでSTRING型など)としました。また、数値を扱う「UnitPrice」「Uprice」「Quant」は、VBAでのLong型としました。
売上番号(SNo)は、自動で追番が振られるようにCOUNTER型とし、データ削除相当の非表示列「Del列」はTrueまたはFalseのみで良いためBIT型(VBAで言うBoolean型)としました。
データ型一覧
図3-2

3-2.データベースの存在確認と作成

今回システムにはデータベースファイルとテーブルが必須ですので、サンプルファイルを開く際に存在するか否かのチェックを行っています。そして存在していない場合には、新たに作成します。
起動時のデータベースチェック
図3-3

データベースファイルは、定められたファイル(既定は it-082.accdb)がExcelファイルと同じフォルダーに存在しているかを調べます。
テーブルとしては「Sale」「Cust」「Pdct」の3つを今回使用しますので、その名前のテーブルが存在するかを調べます。 3つ揃っていない場合は、一旦全てのテーブルを削除し、新たに3つのテーブルを新作します。
なお、テーブル内の列名が揃っているか、データ型が正しいか等のチェックは今回行っていません。

3-3.取引先データの操作

シート上の「取引先」ボタンをクリックして表示されるダイアログ(UserForm2)上には、データを操作するボタンとして「登録/更新」と「削除」の2つがあります。(「終了ボタン」はダイアログを終了させるだけです。)
取引先データ操作の流れ
図3-4

上部にあるテキストボックス等にデータを入力するか、又はリストボックスの項目を選択することで上部テキストボックスにそのデータが転記されるデータを手修正した後に「登録/更新」ボタンをクリックすると、Primary Keyである取引先番号(CNo)がCustテーブル内に存在するか否かを調べます。
もし存在しない場合は「データ追加(SQL文ではInsert)」となり、存在する場合は「データ修正(Update)」となります。

また、リスト内の項目を選択した上で「削除」ボタンをクリックすると、選択している取引先番号が「売上台帳(テーブルSale)」で使われているか否かを調べます。
もし取引先番号が使われている場合には、売上台帳から「取引先の情報元が消えてしまう」ことになりますので、削除できません。かと言ってそのまま消さずに残しておく訳にもいかないので、「Del列をTrue」にして「売上台帳のダイアログ上で選択できない(コンボボックスのリストから外す)」ようにしています。
一方、売上台帳上で取引先番号が使われていない場合は、テーブルCustからデータ削除しています。

3-4.商品データの操作

商品データダイアログ(UserForm3)上にも、取引先ダイアログと同様に「登録/更新」と「削除」の2つのボタンがあります。
商品データ操作の流れ
図3-5

上部のテキストボックス等でデータを入力・修正後に「登録/更新」ボタンをクリックすると、Primary Keyである商品番号(PNo)がテーブルPdct内に存在するか否かを調べます。
もし存在しない場合は「データ追加(SQL文ではInsert)」となり、存在する場合は「データ修正(Update)」となります。

また、リスト内の項目を選択した上で「削除」ボタンをクリックすると、選択している商品番号が「売上台帳(テーブルSale)」で使われているか否かを調べます。
もし商品番号が使われている場合には、売上台帳から「商品の情報元が消えてしまう」ことになりますので、削除できません。「Del列をTrue」にして「売上台帳のダイアログ上で選択できない」ようにしています。
一方、売上台帳上で商品番号が使われていない場合は、テーブルPdctからデータ削除しています。

3-5.売上データの操作

売上データダイアログ(UserForm1)上にも、取引先ダイアログ・商品ダイアログと同様の「登録/更新」と「削除」のボタンがあります。
売上データ操作の流れ
図3-6

上部のテキストボックス等でデータを入力・修正後に「登録/更新」ボタンをクリックすると、Primary Keyである商品番号(SNo)がテーブルSale内に存在するか否かを調べます。
もし存在しない場合は「データ追加(SQL文ではInsert)」となり、存在する場合は「データ修正(Update)」となります。

また、リスト内の項目を選択した上で「削除」ボタンをクリックすると、選択している売上番号のデータをテーブルSaleから削除します。取引先ダイアログ・商品ダイアログとは異なり、Saleテーブルからレコードを削除しても整合性は崩れませんので削除可能です。

ダイアログ下部のリストボックスには、テーブルSaleのデータ全てを表示する訳では無く、データ内の売上日付(SaleD列)が上部の「表示年」に該当するデータのみを出力するようにしています。また「リスト出力」ボタンは、「表示年で絞り込んだデータ」をリストボックスでは無く、ワークシート側へ出力させる事になります。

4.操作用シート(Sheet1)

今回は、ワークシートSheet1上に操作用ボタンを3つ配置し、また出力用に項目タイトルを置いています。
操作用シート
図4-1

ボタンは「入力」「取引先」「商品」とのCaptionにし、それぞれ「DataInput」「CustomerMgt」「ProductMgt」という標準モジュールに置いた「フォームを起動」するマクロを登録しています。
また、データ出力は10項目としましたので、B2セル~K2セルにタイトルを手書きし、その下(3行目以降)にデータを書き込みます。そのデータ出力時に「表示年」も同時に出力し、その出力先をC1セルとしました。

5.ワークブックモジュール(ThisWorkbook)

ブックを開いた時に実行されるWorkbook_Openイベントが図5-1です。02行目「Call SystemStart」で図6-2を呼び出し、データベースファイル及びテーブルの状態を調べ、存在しない場合には作成をします。
  1. '========== ⇩(1) Excelを開く際に実行 ============
  2. Private Sub Workbook_Open()
  3.  Call SystemStart
  4. End Sub
図5-1

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

標準モジュールの宣言部で、標準モジュール内、及びシステム全体で使用する定数・変数の宣言をします。
  1. '========== ⇩(2) 定数・変数宣言 ============
  2. Private DBpath As String      '←データベースファイルのパス
  3. Private Const DBname As String = "it-082.accdb"    '←データベースファイルのファイル名
  4. Public Const TBL0 As String = "Sale"    '←売上データのテーブル名
  5. Public Const TBL1 As String = "Cust"    '←取引先データのテーブル名
  6. Public Const TBL2 As String = "Pdct"    '←商品データのテーブル名
図6-1

11行目「DBpath」は、データベースファイルのパスです。値は図6-2の25行目で代入しています。なお、実際に運用する時には「データベースファイルは、共有ファイルサーバー等」に置く事が多いと考えられますので、定数として宣言部で宣言することになると思います。
12行目「DBname」は、データベースファイルのファイル名です。今回はサンプルファイルと同じ名前にしています。
14~16行目はテーブルの名前です。テーブル名を変更した時でも、各SQL文を1つ1つチェック・変更しなくても良いように定数化しています。

6-1.データベース・テーブルの準備

Excelを開いた時に、図5-1「Workbook_Open」から呼び出されるのが図6-2です。
  1. '========== ⇩(3) データベース・テーブルの準備 ============
  2. Public Sub SystemStart()
  3.  Dim chk As Variant    '←ファイル・テーブルのチェック結果
  4.  Dim TBL As Variant    '←テーブル名の配列変数
  5.  DBpath = ThisWorkbook.Path & "¥"    '←データベースファイルのパス設定
  6.  TBL = Array(TBL0, TBL1, TBL2)
  7.  chk = isMissingDB(TBL)
  8.  Select Case chk
  9.   Case 1
  10.    Call makeDB
  11.    Call makeTable
  12.    MsgBox "DB,Tableを新作しました"
  13.   Case 2
  14.    Call deleteTable(TBL)
  15.    Call makeTable
  16.    MsgBox "Tableを新作しました"
  17.   Case False
  18.    MsgBox "DB、TableともOK"
  19.  End Select
  20. End Sub
図6-2

25行目「DBpath = ThisWorkbook.Path & "¥"」では、データベースファイルの置いてあるパスを設定しています。パスが固定していて、そのパス先を宣言部で定数宣言する場合には、25行目は不要になります。
26行目「TBL = Array(TBL0, TBL1, TBL2)」では、3つのテーブル名を配列化しています。これは、各テーブルの存在を調べている図6-4「isMissingDB」、及び図6-12「deleteTable」の独立性を高くするためです。

28行目「chk = isMissingDB(TBL)」では、図6-4に検査するテーブル名の配列を渡し、その検査結果を変数chkで受け取ります。戻り値は以下の通りです。
状態戻り値
DBファイルテーブル
×(×)1
×~△2
False
図6-3

表中の〇は「正しく存在する」、×は「存在しない」、△は「存在はするが不足している」を表しています。
DBファイル・テーブルとも正常(図6-3で、両方とも〇)な状態でなければシステムは動かせませんので、30~41行目で状態に従ってDBファイル・テーブルを作り直します。

30行目「Select Case chk」で検査結果を選別し、「1(=DBファイルが無い)」の場合は32行目「Call makeDB」で図6-7を呼出して「新たにDBファイルを作成」し、33行目「Call makeTable」で図6-8を呼出し「テーブルを作成」します。
作成が完了したら、34行目「MsgBox "DB,Tableを新作しました"」でコメントを出します。

「2(=テーブルが無い、又は不足)」の場合は、36行目「Call deleteTable(TBL)」で図6-12を呼出し「作ろうとするテーブルを一旦削除」します。その後の37行目「Call makeTable」で図6-8を呼出し「テーブルを作成」します。
作成が完了したら、38行目「MsgBox "Tableを新作しました"」でコメントを出します。

「False(=ファイルとテーブルが正常に存在)」の場合は、40行目「MsgBox "DB、TableともOK"」でコメントを出します。

図6-2の28行目から呼び出される「DBファイル・テーブルを調べる」のが図6-4です。引数として、調べるテーブル名の配列TBLを受け取ります。
  1. '========== ⇩(4) DBファイル・テーブルの存在チェック ============
  2. Private Function isMissingDB(ByVal TBL As Variant) As Variant
  3.  Dim Cat As Object    '←Catalogオブジェクト
  4.  Dim T As Object    '←Tableオブジェクト
  5.  Dim i As Integer    '←カウンタ変数(テーブルの数)
  6.  If Dir(DBpath & DBname) = "" Then
  7.   isMissingDB = 1    '←DBファイルが無い
  8.   Exit Function
  9.  End If
  10.  Set Cat = CreateObject("ADOX.Catalog")
  11.  Cat.ActiveConnection = "Provider=Microsoft.Ace.OLEDB.12.0;" & _
  12.               "Data Source=" & DBpath & DBname
  13.  For Each T In Cat.Tables
  14.   If T.Type = "TABLE" Then
  15.    For i = 0 To UBound(TBL, 1)
  16.     If T.Name = TBL(i) Then TBL(i) = 1
  17.    Next i
  18.   End If
  19.  Next T
  20.  If WorksheetFunction.Sum(TBL) = UBound(TBL, 1) - LBound(TBL, 1) + 1 Then
  21.   isMissingDB = False    '←テーブルが揃っている
  22.  Else
  23.   isMissingDB = 2    '←テーブルが無い。又は不足
  24.  End If
  25.  Set Cat = Nothing
  26. End Function
図6-4

56~59行目では、DBファイルが存在するか否かをチェックしています。
56行目「If Dir(DBpath & DBname) = "" Then」では、Dir関数の引数に「DBファイルのパス+DBファイル名」を渡しています。Dir関数はファイルが存在する時には「ファイル名」を戻してきますので、「 = ""」と比較している事から「DBファイルが存在しない」場合に、57行目「isMissingDB = 1」で関数プロシージャの戻り値を1としています。そして58行目「Exit Function」でプロシージャを抜け出します。

61行目「Set Cat = CreateObject("ADOX.Catalog")」では、ADOX(ActiveX Data Objects Extensions for Data Definition Language and Security = データ定義とセキュリティのために拡張したADO)のCatalogオブジェクトを生成します。
62~63行目「Cat.ActiveConnection = "Provider=Microsoft.Ace.OLEDB.12.0;" & "Data Source=" & DBpath & DBname」では、DBファイルに接続しています。Providerの種類等については「ExcelからAccessデータベースを作成・操作」を参照して下さい。

65~71行目では、引数で得た配列内のテーブル名が存在するかをつぶし込んでいます。
65行目「For Each T In Cat.Tables」では、接続しているDBファイル内のテーブルを1つ1つ抽出しています。DBファイル内には、ユーザーが作成する「データを収めるテーブル」の他にも「システムテーブル(SYSTEM TABLE)」などが含まれています。そのため66行目「If T.Type = "TABLE" Then」でデータテーブルのみに絞り込みます。
67行目「For i = 0 To UBound(TBL, 1)」では、カウンタ変数iを引数で得た配列のテーブルの数分だけ回し、68行目「If T.Name = TBL(i) Then TBL(i) = 1」のIf文で、テーブル名配列の1つと合致した時に「TBL(i) = 1」で「テーブル名を数値の1と置換」します。

分かり難いので図6-5で説明すると、引数として得た時には配列TBLの各要素にはテーブル名が書き込まれていますが、65~71行目のFor Each~Next を繰返してデータテーブルをチェックするたびに配列内の要素が数値「1」に書き換えられていき、全てのテーブルが存在すれば「配列内の全要素が1」になります。
テーブル名配列を数値で置き換え
図6-5

全テーブルのチェックが完了したら、73行目「If WorksheetFunction.Sum(TBL) = UBound(TBL, 1) - LBound(TBL, 1) + 1 Then」の前半「WorksheetFunction.Sum(TBL)」で配列内の各要素を足していきます。存在したテーブル名は数値1に置き換わっていますし、存在しなかったテーブル名は「テーブル名のまま(=文字列 → 数値ゼロ相当)」となります。
ですので、足し算の結果が「配列の要素数」であれば「配列内の要素は全て数値1に置換(=テーブルは全て存在した)」され、それより少なければ「存在しないテーブルがあった」ことになります。要素数は最大インデックスと最小インデックスの差である「 UBound(TBL, 1) - LBound(TBL, 1) + 1」で表されます。

テーブルが全て存在した場合は、74行目「isMissingDB = False」で関数プロシージャの戻り値をFalseにし、テーブルが無い又は不足していた場合は76行目「isMissingDB = 2」で戻り値を「2」に設定します。

異なる手法として、配列内のテーブル名を数値に変換するのでは無く、別な数値型変数にヒットの都度1を加えていくことで、最終的に全てがヒットしたか否かが計算できます。

寄り道
今回はテーブルが3つと少ない事が分かっているため、テーブルが見つかった後でもFor~Nextを回し続けていますが、「用が済んだらExit Forで抜けて次のTABLEに移る」という図6-6のコード(図6-4の65~71行目の代替)でも良いと思います。
但し配列TBL内に同じテーブル名を複数個入れられると、計算対象から漏れたテーブル名が残ってしまうことになり、常にテーブルが新しく作り直されることになるという危険がありますので注意が必要です。
  •  For Each T In Cat.Tables
  •   If T.Type = "TABLE" Then
  •    For i = 0 To UBound(TBL, 1)
  •     If T.Name = TBL(i) Then
  •      TBL(i) = 1
  •      Exit For
  •     End If
  •    Next i
  •   End If
  •  Next T
図6-6

6-2.データベースファイル作成

図6-2の32行目から呼び出される「データベースファイルを作成」するのが図6-7です。
  1. '========== ⇩(5) データベースファイル作成 ============
  2. Private Sub makeDB()
  3.  Dim Cat As Object    '←Catalogオブジェクト
  4.  Set Cat = CreateObject("ADOX.Catalog")
  5.  Cat.Create "Provider=Microsoft.Ace.OLEDB.12.0;" & _
  6.        "Data Source=" & DBpath & DBname
  7.  Set Cat = Nothing
  8. End Sub
図6-7

94行目「Set Cat = CreateObject("ADOX.Catalog")」でADOXのCatalogオブジェクトを生成し、95~96行目でデータベースファイルを作成します。
95行目の「Cat.Create」のCreateメソッドの引数に「データベースファイルへの接続文字列」を設定することでデータベースファイルが作られます。今回はプロバイダとして「"Provider=Microsoft.Ace.OLEDB.12.0;"」を使用し、接続先は96行目「"Data Source=" & DBpath & DBname」となります。

6-3.テーブルの作成と削除

図6-2の33行目、及び37行目から呼び出される「テーブルを作成」するのが図6-8です。テーブルは今回3つ作成するのですが、作る順番には注意が必要です。図3-1で分かるように「Saleテーブルには参照制約(Foreign Key)を設定」するため、その設定よりも前に「参照される側のテーブルにPrimary Keyを設定」する必要があります。
ですので、CustとPdctテーブルの設定を先に行い(この順番はどちらが先でもOK)、最後にSaleテーブルの設定を行います。
  1. '========== ⇩(6) テーブルの作成 ============
  2. Private Sub makeTable()
  3.  Dim sql As String    '←SQL文
  4.  sql = "Create Table " & TBL2 & " (" & _
  5.     "PNo STRING PRIMARY KEY," & _
  6.     "Pname STRING," & _
  7.     "Uprice LONG," & _
  8.     "Del Bit" & _
  9.     ")"
  10.  Call SQL_exec5(sql)
  11.  sql = "Create Table " & TBL1 & " (" & _
  12.    "CNo STRING PRIMARY KEY," & _
  13.     "Cname STRING," & _
  14.     "Del Bit" & _
  15.     ")"
  16.  Call SQL_exec5(sql)
  17.  sql = "Create Table " & TBL0 & " (" & _
  18.     "SNo COUNTER PRIMARY KEY," & _
  19.     "Slip STRING," & _
  20.     "SaleD DATETIME," & _
  21.     "CustNo STRING," & _
  22.     "PdctNo STRING," & _
  23.     "UnitPrice LONG," & _
  24.     "Quant LONG," & _
  25.     "FOREIGN KEY(CustNo) REFERENCES " & TBL1 & " (CNo) ," & _
  26.     "FOREIGN KEY(PdctNo) REFERENCES " & TBL2 & " (PNo) " & _
  27.     ")"
  28.  Call SQL_exec5(sql)
  29. End Sub
図6-8

テーブルを作成するSQL構文は以下の通りです。制約は省略可能です。
 Create Table テーブル名 ( 列名 データ型 [制約] ,  列名 データ型 [制約] , ・・・)

114~119行目は、商品データを格納するPdctテーブルを作成するSQL文です。テーブル構造は図6-9です。
Pdctテーブルの構造
図6-9

114行目「sql = "Create Table " & TBL2 & " (" & _」で、テーブル名「Pdct(=TBL2)」を作成します。
115行目「"PNo STRING PRIMARY KEY," & _」で、PNo列をString型で作成します。制約としてPrimary Key(Not Null + Unique)を設定します。
116行目「"Pname STRING," & _」で、Pname列をString型で作成します。
117行目「"Uprice LONG," & _」で、Uprice列をLong型で作成します。
118行目「"Del Bit" & _」で、Del列をBit型(VBAでBoolean型)で作成します。
120行目「Call SQL_exec5(sql)」で、上記で作成したSQL文を図6-16の「SQL文を実行するSQL_exec5関数プロシージャ」に渡して、データベース内にPdctテーブルを作成します。

122~126行目は、取引先データを格納するCustテーブルを作成するSQL文です。テーブル構造は図6-10です。
Custテーブルの構造
図6-10

122行目「sql = "Create Table " & TBL1 & " (" & _」で、テーブル名「Cust(=TBL1)」を作成します。
123行目「"CNo STRING PRIMARY KEY," & _」で、CNo列をString型で作成します。制約としてPrimary Key(Not Null + Unique)を設定します。
124行目「"Cname STRING," & _」で、Cname列をString型で作成します。
125行目「"Del Bit" & _」で、Del列をBit型(VBAでBoolean型)で作成します。
127行目「Call SQL_exec5(sql)」で、上記で作成したSQL文を実行してCustテーブルを作成します。

129~139行目は、売上データを格納するSaleテーブルを作成するSQL文です。テーブル構造は図6-11です。
Saleテーブルの構造
図6-11

129行目「sql = "Create Table " & TBL0 & " (" & _」で、テーブル名「Sale(=TBL0)」を作成します。
130行目「"SNo STRING PRIMARY KEY," & _」で、SNo列をCOUNTER型で作成します。制約としてPrimary Key(Not Null + Unique)を設定します。COUNTER型はデータ挿入時に指定しなくても「自動的に最大値+1」の値としてくれる型です。
131行目「"Slip STRING," & _」で、Slip列をString型で作成します。
132行目「"SaleD DATETIME," & _」で、SaleD列をDATETIME型で作成します。
133行目「"CustNo STRING," & _」で、CustNo列をString型で作成します。この列は参照制約(FOREIGN KEY)を付けますが、列設定時では無く、後から137行目で設定します。参照制約時の構文は以下になります。
 Create Table テーブル名 (列の設定,・・・,FOREIGN KEY(列名) REFERENCES 相手のテーブル名(相手の列名))

134行目「"PdctNo STRING," & _」で、PdctNo列をString型で作成します。この列も参照制約を138行目で設定します。
135行目「"UnitPrice LONG," & _」で、UnitPrice列をLONG型で作成します。
136行目「"Quant LONG," & _」で、Quant列をLONG型で作成します。
137行目「"FOREIGN KEY(CustNo) REFERENCES " & TBL1 & " (CNo) ," & _」で、CustNo列をTBL1(=テーブルCust)のCNo列に対して参照制約(FOREIGN KEY)を設定します。参照制約先はPrimary Keyである必要があります。
138行目「"FOREIGN KEY(PdctNo) REFERENCES " & TBL2 & " (PNo) " & _」で、PdctNo列をTBL2(=テーブルPdct)のPNo列に対して参照制約(FOREIGN KEY)を設定します。

140行目「Call SQL_exec5(sql)」で、上記で作成したSQL文を実行してSaleテーブルを作成します。

図6-2の36行目から呼び出される「指定したテーブルを削除」するのが図6-12です。引数として削除するテーブル名の配列を受け取ります。
  1. '========== ⇩(7) テーブルの削除 ============
  2. Private Sub deleteTable(ByVal TBL As Variant)
  3.  Dim sql As String    '←SQL文
  4.  Dim cn As Object    '←Connectionオブジェクト
  5.  Dim i As Integer    '←カウンタ変数(削除するテーブルの数)
  6.  Set cn = CreateObject("ADODB.Connection")
  7.  cn.Provider = "Microsoft.ACE.OLEDB.12.0"
  8.  cn.Open DBpath & DBname
  9.  For i = 0 To UBound(TBL, 1)
  10.   On Error Resume Next
  11.    sql = "drop Table " & TBL(i)
  12.    cn.Execute sql
  13.   On Error GoTo 0
  14.  Next i
  15.  cn.Close
  16.  Set cn = Nothing
  17. End Sub
図6-12

156行目「Set cn = CreateObject("ADODB.Connection")」で、ADOのConnectionオブジェクトを生成します。
157行目「cn.Provider = "Microsoft.ACE.OLEDB.12.0"」で、ConnectionのプロバイダをACE系に設定し、158行目「cn.Open DBpath & DBname」でデータベースファイルに接続します。

160行目「For i = 0 To UBound(TBL, 1)」で、カウンタ変数iを引数で得たテーブル名配列の要素数分だけ回します。
テーブルを削除するSQL構文は、「Drop Table テーブル名 」 ですので、
162行目「sql = "drop Table " & TBL(i)」でテーブル名を1つずつ構文にあてはめ、163行目「cn.Execute sql」で削除を実行します。
但し、今回は「テーブルが不足」している場合にもこの削除プロシージャを実行しています。削除しようとするテーブルが存在しなかった場合にはエラーが発生しますので、161行目「On Error Resume Next」でエラーをスルーさせます。

なお、図6-8と同様に「Drop Table のSQL文を作成した後、SQL_exec5プロシージャに渡してテーブル削除」することも可能です。しかし今回のSQL_exec5関数プロシージャは、テーブルが無いのに削除実行した時のエラー等には対応させていないため、自プロシージャ内でConnection.Open → SQL実行 → Connection.Close までの処理を行い、途中に「On Error Resume Next」を挟み込むことで成立させています。

6-4.フォームの起動

ワークシート上のボタンをクリックすることで呼び出されるのが、以下のプロシージャです。ボタンは今回3つあります。
 ・「入 力」ボタン → 図6-13の「DataInputプロシージャ」 → UserForm1を起動
 ・「取引先」ボタン → 図6-14の「CustomerMgtプロシージャ」 → UserForm2を起動
 ・「商 品」ボタン → 図6-15の「ProductMgtプロシージャ」 → UserForm3を起動
各フォームは、モーダル状態(フォームの操作しかできない)で起動しています。
  1. '========== ⇩(8) 売上台帳操作 ============
  2. Public Sub DataInput()
  3.  UserForm1.Show
  4. End Sub
図6-13

  1. '========== ⇩(9) 取引先データ操作 ============
  2. Public Sub CustomerMgt()
  3.  UserForm2.Show
  4. End Sub
図6-14

  1. '========== ⇩(10) 商品データ操作 ============
  2. Public Sub ProductMgt()
  3.  UserForm3.Show
  4. End Sub
図6-15

6-5.SQL文の実行

各ユーザーフォームから、また図6-8(テーブル作成)から呼び出されるのが、「SQL文を実行」する図6-16です。
引数としてSQL文を受け取り、Select文のようにデータを受取るSQLの場合には引数としてデータを格納した配列を戻します。
なお、Select文以外は戻り値を必要としていませんが、Variant型の初期値であるEmptyを戻す事になります。
  1. '========== ⇩(11) SQL文の実行 ============
  2. Public Function SQL_exec5(sql As String) As Variant
  3.  Dim cn As Object      '←Connectionオブジェクト
  4.  Dim rs As Object      '←Recordsetオブジェクト
  5.  Dim i As Long        '←カウンタ変数(行数)
  6.  Dim j As Integer      '←カウンタ変数(列数)
  7.  Const adOpenStatic = 3   '←カーソル状態の定数値
  8.  Dim buf() As Variant    '←戻り値の配列
  9.  Set cn = CreateObject("ADODB.Connection")
  10.  Set rs = CreateObject("ADODB.Recordset")
  11.  cn.Provider = "Microsoft.ACE.OLEDB.12.0"
  12.  cn.Open DBpath & DBname
  13.  rs.Open sql, cn, adOpenStatic
  14.  If rs.State = 0 Then
  15.   Select Case Left(LTrim(sql), 1)
  16.    Case "i", "I"
  17.     MsgBox "データ追加しました"
  18.    Case "u", "U"
  19.     MsgBox "データ更新しました"
  20.    Case "d", "D"
  21.     MsgBox "データ削除しました"
  22.   End Select
  23.  Else
  24.   ReDim buf(0 To rs.RecordCount, 1 To rs.Fields.Count)
  25.   For j = 1 To rs.Fields.Count
  26.    buf(0, j) = rs.Fields(j - 1).Name
  27.   Next j
  28.   Do Until rs.EOF
  29.    i = i + 1
  30.    For j = 1 To rs.Fields.Count
  31.     buf(i, j) = IIf(IsNull(rs.Fields(j - 1).Value), "", rs.Fields(j - 1).Value)
  32.    Next j
  33.    rs.MoveNext
  34.   Loop
  35.   SQL_exec5 = buf
  36.   rs.Close
  37.  End If
  38.  cn.Close
  39.  Set rs = Nothing
  40.  Set cn = Nothing
  41. End Function
図6-16

209行目「Set cn = CreateObject("ADODB.Connection")」で、ADOのConnectionオブジェクトを生成します。
210行目「Set rs = CreateObject("ADODB.Recordset")」で、Recordsetオブジェクトを生成します。
212行目「cn.Provider = "Microsoft.ACE.OLEDB.12.0"」で、ConnectionのプロバイダをACE系で設定します。
213行目「cn.Open DBpath & DBname」で、データベースファイルに接続します。

215行目「rs.Open sql, cn, adOpenStatic」で、引数で得たSQL文を実行します。その際、省略可能な第三引数として図6-17のカーソルから「静的カーソル」を指定します。
なお、215行目ではadOpenStaticという定数値で指定していますが、今回ADOを実行時バインディングでセットしていますので「ADOの定数は認識されない」状態です。ですので206行目「Const adOpenStatic = 3」とコード内で定数値を宣言する必要があります。
定数内容他ユーザーが操作したデータの反映RecordCountの戻り値
adOpenUnspecified-1カーソル種類を指定せず(=順方向専用カーソル?)-1
adOpenForwardOnly0順方向専用カーソル(既定)
開いた時点のレコードのみ-1
adOpenKeyset1キーセットカーソル
他ユーザーによるレコードの更新を参照可レコード数
adOpenDynamic2動的カーソル
他ユーザーの全操作を参照可-1
adOpenStatic3静的カーソル
開いた時点のレコードのみレコード数
図6-17

通常は既定である「ForwardOnly」を使い、Do~Loopで1つずつカーソルを移動するのと同時にReDimで配列を広げながら値を格納する事が多いと思います。しかし完成した配列は、Redimが列方向にしか増やせない事から「行と列が逆転」してしまうため、最後にTranspose関数で元に戻す必要が出てきます。
今回は最初から「行と列が正しい姿」の配列にデータを格納するため、rs.Open直後に「取得したレコード数を把握」できる「静的カーソル(adOpenStatic)」を使用しました。(キーセットカーソルを使用しなかった理由は「よりみち」で説明します。)

215行目「rs.Open sql, cn, adOpenStatic」を実行した時点で、Insert文のように取得データの無いSQL文では、Recordsetオブジェクトが閉じる(Recordset.State = 0 )ことになります。一方、Select文のように取得データがあるSQL文は開いて(Recordset.State = 1 )います。
ですので、217行目「If rs.State = 0 Then」でRecordsetオブジェクトの状態を調べ、閉じている場合は218~225行目を実行し、開ている場合は228~242行目で「取得したレコードを配列に格納し、戻り値に設定」します。

まず閉じている場合は218行目「Select Case Left(LTrim(sql), 1)」で、SQL文の先頭文字列を調べます。データを操作するSQL文には「Select」「Insert」「Update」「Delete」の4つがあり、それぞれの先頭文字列は異なりますので「どんな操作をするSQLが来たか分かる」ことになります。219行目「Case "i", "I"」は「Insert文」の場合で、220行目「MsgBox "データ追加しました"」というコメントを出しています。以下、「Update」「Delete」文に対応したコメントを表します。
なお、今回は図6-8から「Create Table ・・・」というSQL文もこのSQL_exec5関数プロシージャで処理を行うため、「C」という先頭文字列も流れますが、仕訳けのCase文が無いため特にコメントなどは表示されません。

次に開いている場合は、まず228行目「ReDim buf(0 To rs.RecordCount, 1 To rs.Fields.Count)」でデータを格納する配列の最終的なサイズを決めてしまいます。行方向は「rs.RecordCount」行、列方向は「rs.Fields.Count」列として得られます。
寄り道
今回、SQL_exec5関数プロシージャが戻す配列のインデックスは、図6-18のように「タイトルはゼロ行目」に、また「データはインデックス1から」としています。タイトル行には列名を記入しています。
データ格納する配列のサイズ
図6-18

この「タイトル+データ」の配列にするメリットは2つあります。
1つ目は「データ配列を受取った側での処理が、全て二次元配列の処理に単純化できる」ことです。Select文で得られるレコード数は色々ですが、ゼロ行となる場合もあります。もしタイトル行が無くデータ行のみの配列とした場合、2行取得なら「2行x列数」の配列、1行取得なら「1行x列数」の配列となります。1行x列数ならば「二次元配列」にすることは可能です。
もし取得したレコードがゼロ行の場合は「ゼロ行x列数」となりますが、これは二次元配列にすることは出来ません。
そのため先頭行(ゼロ行目)にタイトル行を設け、図6-19のようにゼロ行でも「1行x列数(=二次元配列可)」の状態にしています。
データが1行も無かった場合の配列
図6-19

2つ目は「Select * from ・・・」と、アスタリスクを使って全列のデータを取得した場合、タイトル行があれば、そのままワークシートに貼り付けても「どの列のデータかが分かる」ようになります。以前紹介した「ExcelシートDBとSQLを使った倉庫管理システム」のようなシステムにも対応可能です。

229~231行目ではタイトル行に列名を記入しています。
229行目「For j = 1 To rs.Fields.Count」で、カウンタ変数jを列数分だけ回し、230行目「buf(0, j) = rs.Fields(j - 1).Name」で配列の0行目に列名を1つずつ格納していきます。

タイトル行が完了したら、233~239行目データを配列内に格納していきます。
233行目「Do Until rs.EOF」で、Do~Loopを取得レコードの最後(EOF:End of File)が来るまで回します。
234行目「i = i + 1」は、配列の行位置を1つずつ移動しています。カウンタ変数iは204行目でLong型として宣言しただけですので、初期値のゼロとなっています。ですので最初に234行目を通過する時にはi=1 となり、配列のデータを入れる1行目にデータを書き込むことになります。
235行目「For j = 1 To rs.Fields.Count」では、カウンタ変数jを列数分だけ回します。
236行目「buf(i, j) = IIf(IsNull(rs.Fields(j - 1).Value), "", rs.Fields(j - 1).Value)」で、データを書き込みますが、その際に「Nullデータは、長さゼロの文字列に変換」してから配列に格納します。ここではIIF関数を使ってワークシート関数のような使い方をしました。
なおNullデータを嫌うのは、今回のようにリストボックスやコンボボックスのリストにデータを貼り付ける際、Null文字だとエラーが出てしまう為です。1つ1つのリスト貼付け時にNullを""(長さゼロの文字列)に変換するよりは、データ供給時点でまとめて変換した方がコードも少なくなると考えました。

1行分のデータを配列に格納したら、238行目「rs.MoveNext」で「カーソルを1行移動」し、次の行の処理をします。
全行のデータを配列に格納し終わったら、241行目「SQL_exec5 = buf」で「配列を関数プロシージャの戻り値に設定」し、242行目「rs.Close」でRecordsetを閉じます。

寄り道
rs.Open直後に取得したレコード数が分かるのは、図6-17で分かる通りキーセットカーソルと静的カーソルです。今回、キーセットカーソル(adOpenKeyset)では無く「静的カーソル(adOpenStatic)」を使った理由は以下になります。この2つの違いは「他のユーザーが操作したデータが反映されるか否か」です。
今回システムでは「同時に複数のユーザーがデータ操作」する可能性がありますので、例えば215行目「rs.Open sql, cn, adOpenStatic」と233行目「Do Until rs.EOF」の間で、別ユーザーが同じテーブルのデータを1行削除した時のことを考えてみます。RecordsetをOpenした時には10行のレコードだったものが、Do~Loopで処理をする時には9行になるのです。

カーソルとして「静的カーソル(adOpenStatic)」を使用していれば、取得するのは「rs.Open の時点」のデータで固定されているためにDo~Loop処理時点でも10行(言ってみれば、古新聞の状態)が存在するため、食い違いは発生しません。
しかし、もし「キーセットカーソル(adOpenKeyset)」を使用していれば、他ユーザーに削除されてしまったレコードのデータを取得しようとした時に、データにアクセスできずエラーが発生します。反映される操作は図6-20の通りです。
定数反映される他ユーザーの操作
InsertDeleteUpdate
adOpenUnspecified-1×××
adOpenForwardOnly0×
××
adOpenKeyset1×
adOpenDynamic2×
××
adOpenStatic3×
××
図6-20

表からも分かるように、キーセットカーソル(adOpenKeyset)を使った場合でも、他ユーザーが行挿入してもデータは反映されずにエラーは出ません。
また動的カーソル(adOpenDynamic)は、図6-17でも「他ユーザーの全ての操作を反映」と記しましたが、結果は全て古新聞状態でした。これは、rs.Open時にadOpenDynamicを指定しても、(ロック指定が既定のadLockReadOnlyの時には)adOpenDynamic → adOpenStaticに強制的に変更されてしまうのが原因のようです。
カーソル種類を選ぶ時「最新状態の方がより良いだろう」と考えがちですが、このような不具合も考えられますので注意が必要です。

なお今回のカーソル(静的カーソル)では、古新聞のデータリストとなり、例えば「別ユーザーが削除済みのデータが表示されている」のような状態も考えられます。その(既に削除済みの)データを再び削除処理をすると、Where句で抽出されずにDelete文を実行することになり、結果として「削除しましたコメントは出るが、実際には(既に削除されてるので)削除されない」ことになります。結果的には異常は無い形になっています。

また、SQL文を実行するのに「cn.Execute sql」というコードが使えますが、Executeメソッドのカーソルは前方スクロールカーソル(adOpenForwardOnly相当)固定です。取得したレコード行数を取得するカーソルはExcecuteメソッドには指定できないため、今回は「Recordset.Open」を使用しました。

7.売上ダイアログ(UserForm1)

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

売上データを操作するダイアログがUserForm1で、そのフォーム上のコントロール類の配置は図7-1の様にしました。
売上ダイアログ
図7-1

操作を実行するCommandButtonをフォーム上部に5つ並べます。また表示するデータの年を入力するTextBox4も上部に配置しています。
ダイアログ下部にデータ一覧を表示するListBox1を配置し、中央部にユーザーが入力するTextBox、ComboBoxを配置します。
Label1~3は、ユーザーが入力するものではありませんが、リストの選択や、単価・合計値の計算結果を表示するため、ユーザー入力部と同じ段に配置しました。

7-2.フォームモジュール(UserForm1)

7-2-1.起動時準備

フォームが呼び出された際に最初に実行されるのが図7-2のInitializeイベントです。ここではコントロール類の静的なプロパティをセットします。
  1. '========== ⇩(12) フォーム起動時 ============
  2. Private Sub UserForm_Initialize()
  3.  With Me.ComboBox1
  4.   .ColumnCount = 2
  5.   .ColumnWidths = "40;50"
  6.   .MatchRequired = True
  7.   .TextAlign = fmTextAlignLeft
  8.   .TextColumn = 2
  9.  End With
  10.  With Me.ComboBox2
  11.   .ColumnCount = 3
  12.   .ColumnWidths = "40;50;20"
  13.   .MatchRequired = True
  14.   .TextAlign = fmTextAlignLeft
  15.   .TextColumn = 2
  16.  End With
  17.  Me.Label1.TextAlign = fmTextAlignRight
  18.  Me.Label2.TextAlign = fmTextAlignRight
  19.  Me.Label3.TextAlign = fmTextAlignRight
  20.  With Me.ListBox1
  21.   .ColumnCount = 10
  22.   .ColumnWidths = "25;50;65;40;75;40;110;50;30;30"
  23.   .TextAlign = fmTextAlignLeft
  24.  End With
  25.  Me.TextBox1.IMEMode = fmIMEModeOff
  26.  Me.TextBox2.IMEMode = fmIMEModeDisable
  27.  Me.TextBox3.IMEMode = fmIMEModeDisable
  28.  Me.TextBox4.IMEMode = fmIMEModeDisable
  29. End Sub
図7-2

263~269行目は、取引先を選択するComboBox1の設定です。取引先は「取引先番号」と「取引先名」の2つのデータで出来ていますので、264行目「.ColumnCount = 2」で2列表示とし、265行目「.ColumnWidths = "40;50"」で、表示列の幅を設定しています。なおコンボボックスを複数列表示にした時に、横スクロールバーが出ないようにするには「先入先出の入出庫管理システム」を参照願います。

266行目「.MatchRequired = True」は、リストにある値と同じ値のみを許可する設定をしています。268行目で2列目を表示列にしているため、手入力の場合は「取引先名」のみが入力可能となります。もし、それ以外を入力すると、コンボボックスを抜け出る時に「プロパティの値が無効です」と怒られます。なお、一旦コンボボックスに入り「何も選択せずに抜け出す」と「リスト内には、""(=無選択)は無い」ために、やはり「プロパティの値が無効です」と怒られます。
267行目「.TextAlign = fmTextAlignLeft」は、各列の値を左寄せで表示します。
268行目「.TextColumn = 2」は、選択後は「2列目の取引先名」を表示するようにしています。

271~277行目は、商品を選択するComboBox2の設定です。商品は「商品番号」「商品名」「単価」の3つのデータで出来ていますので、272行目「.ColumnCount = 3」で3列表示とし、273行目「.ColumnWidths = "40;50;20"」で、表示列の幅を設定しています。
274行目「.MatchRequired = True」は、リストにある値と同じ値のみを許可する設定をし、275行目「.TextAlign = fmTextAlignLeft」は、各列の値を左寄せで表示します。
276行目「.TextColumn = 2」は、選択後は「2列目の商品名」を表示するようにしています。

279行目「Me.Label1.TextAlign = fmTextAlignRight」は、SNoを表示するLabel1を右寄せにしています。理由はSNoが「数値」だからです。280行目(単価のLabel2)、281行目(合計値のLabel3)も数値のため右寄せにします。

283~287行目は、売上データ一覧を表示するListBox1の設定です。
リストボックスに表示するデータは、「SNo」「伝票番号」「売上日付」「取引先番号」「取引先名」「商品番号」「商品名」「単価」「個数」「合計値」の10項目ですので、284行目「.ColumnCount = 10」で10列表示とし、その列幅を285行目「.ColumnWidths = "25;50;65;40;75;40;110;50;30;30"」で設定します。この列幅はTry & Error で調整しました。
また、286行目「.TextAlign = fmTextAlignLeft」でリストは左寄せにしています。本当でしたら「文字列は左寄せ」「数値は右寄せ」にしたいのですが、各列毎の設定は出来ないため左側に統一しました。

289行目「Me.TextBox1.IMEMode = fmIMEModeOff」は、伝票番号を入力するTextBox1に「fmIMEModeOff」を設定し、既定は半角入力にしています。既定ですので、「漢字キーを押せば、全角入力が可能」となります。
290~292行目は「売上日付」「売上個数」「表示年」のTextBox2~4のIME設定です。この3つは全角は有り得ないため「fmIMEModeDisable」と「半角のみ」としています。ですので漢字キーを押しても全角モードにはなりません。

Initializeイベントで静的なプロパティをセットした後、ダイアログが表示された直後に実行されるのが図7-3のActivateイベントです。ここではコントロールの動的なプロパティ等をセットします。
  1. '========== ⇩(13) フォーム表示時 ============
  2. Private Sub UserForm_Activate()
  3.  Me.TextBox4.Value = Year(Date)
  4.  Me.Label1.Caption = ""
  5.  Me.Label2.Caption = "0"
  6.  Me.Label3.Caption = "0"
  7.  Call makeCombo1
  8.  Call makeCombo2
  9.  Call makeListBox
  10. End Sub
図7-3

303行目「Me.TextBox4.Value = Year(Date)」は、表示するリスト、及び出力するリストの「表示年」のTextBox4に、起動直後は「今年」を表示しています。フォーム起動後、手入力で表示年を変更することが可能です。
305行目「Me.Label1.Caption = ""」は、SNo(売上番号)のLabel1を起動直後は空にしています。このLabel1は、リストボックスの項目を選択することで、その行のSNoが転記されることになります。
306行目「Me.Label2.Caption = "0"」は、単価のLable2です。初期値はゼロ(円)としています。
307行目「Me.Label3.Caption = "0"」は、合計金額のLable3です。初期値はゼロ(円)としています。

309行目「Call makeCombo1」では、図7-4を呼出し、取引先コンボボックスのリストを作成しています。
310行目「Call makeCombo2」では、図7-5を呼出し、商品コンボボックスのリストを作成しています。
311行目「Call makeListBox」では、図7-6を呼出し、売上データをリストボックスに表示します。その際、表示年はTextBox4の値で絞り込みを行っています。

7-2-2.コンボボックス作成

図7-3の309行目から呼び出されるのが「取引先コンボボックスを作成」する図7-4です。
  1. '========== ⇩(14) 取引先コンボボックスの作成 ============
  2. Sub makeCombo1()
  3.  Dim sql As String    '←SQL文
  4.  Dim i As Long      '←カウンタ変数(行数)
  5.  Dim buf As Variant   '←取引先データの配列
  6.  Me.ComboBox1.Clear
  7.  sql = "SELECT CNo,Cname FROM " & TBL1 & " where Del = false order by CNo"
  8.  buf = SQL_exec5(sql)
  9.  For i = 1 To UBound(buf, 1)
  10.   Me.ComboBox1.AddItem ""
  11.   Me.ComboBox1.List(i - 1, 0) = buf(i, 1)
  12.   Me.ComboBox1.List(i - 1, 1) = buf(i, 2)
  13.  Next i
  14. End Sub
図7-4

326行目「Me.ComboBox1.Clear」で、コンボボックスのリストをクリアしています。但し今回システムでは「フォームを閉じる時にUnLoadを使用」しているため、リストを二重に作成することは有りませんが、プログラムを改造された時も考え念の為クリアさせています。

328行目「sql = "SELECT CNo,Cname FROM " & TBL1 & " where Del = false order by CNo"」は、取引先データを取得するSQL文です。データのあるTBL1(=Custテーブル)から2つの列(CNo列、Cname列)のデータを取り出します。但し、削除相当のデータ(Del列がTrue)は除き、表示する順序はCNo(取引先番号)の順番にしています。
作成したSQL文を引数にして、329行目「buf = SQL_exec5(sql)」で図6-16を呼出し、データが格納された戻り値を受け取り、変数bufに代入します。

331~335行目では、コンボボックスのリスト上に取引先データを書き込みます。
331行目「For i = 1 To UBound(buf, 1)」で取引先データの行数分だけカウンタ変数iを回します。
332行目「Me.ComboBox1.AddItem ""」で、まず空の行を追加します。その空行の1列目(インデックスはゼロ)に333行目「Me.ComboBox1.List(i - 1, 0) = buf(i, 1)」で、取引先番号のデータを書込み、334行目「Me.ComboBox1.List(i - 1, 1) = buf(i, 2)」で、2列目(インデックスは1)に取引先名のデータを書き込みます。

図7-3の310行目から呼び出されるのが「商品コンボボックスを作成」する図7-5です。
  1. '========== ⇩(15) 商品コンボボックスの作成 ============
  2. Sub makeCombo2()
  3.  Dim sql As String    '←SQL文
  4.  Dim i As Long      '←カウンタ変数(行数)
  5.  Dim buf As Variant   '←商品データの配列
  6.  Me.ComboBox2.Clear
  7.  sql = "SELECT PNo,Pname,Uprice FROM " & TBL2 & " where Del = false order by PNo"
  8.  buf = SQL_exec5(sql)
  9.  For i = 1 To UBound(buf, 1)
  10.   Me.ComboBox2.AddItem ""
  11.   Me.ComboBox2.List(i - 1, 0) = buf(i, 1)
  12.   Me.ComboBox2.List(i - 1, 1) = buf(i, 2)
  13.   Me.ComboBox2.List(i - 1, 2) = buf(i, 3)
  14.  Next i
  15. End Sub
図7-5

346行目「Me.ComboBox2.Clear」で、コンボボックスのリストをクリアしています。
348行目「sql = "SELECT PNo,Pname,Uprice FROM " & TBL2 & " where Del = false order by PNo"」は、商品データを取得するSQL文です。データのあるTBL2(=Pdctテーブル)から3つの列(PNo列、Pname列、Uprice列)のデータを取り出します。但し、削除相当のデータ(Del列がTrue)は除き、表示する順序はPNo(商品番号)の順番にしています。
作成したSQL文を引数にして、349行目「buf = SQL_exec5(sql)」で図6-16を呼出し、データが格納された戻り値を受け取り、変数bufに代入します。

351~356行目では、コンボボックスのリスト上に商品データを書き込みます。
351行目「For i = 1 To UBound(buf, 1)」で商品データの行数分だけカウンタ変数iを回します。
352行目「Me.ComboBox2.AddItem ""」で、まず空の行を追加します。その空行の1列目(インデックスはゼロ)に353行目「Me.ComboBox2.List(i - 1, 0) = buf(i, 1)」で、商品番号のデータを書込み、354行目「Me.ComboBox2.List(i - 1, 1) = buf(i, 2)」で2列目(インデックスは1)に商品名、355行目「Me.ComboBox2.List(i - 1, 2) = buf(i, 3)」で3列目(インデックスは2)に単価のデータを書き込みます。

7-2-3.リストボックス作成

図7-3の311行目、図7-16の510行目、図7-17の538行目、及び図7-18の547行目から呼び出されるのが「リストボックスに売り上げデータを表示」させる図7-6です。
  1. '========== ⇩(16) リストボックス作成 ============
  2. Private Sub makeListBox()
  3.  Dim i As Long      '←カウンタ変数(行数)
  4.  Dim buf As Variant   '←売上データの配列
  5.  Me.ListBox1.Clear
  6.  buf = SQL_exec5(SQLword("Desc"))   '←降順(最新データが一番上)でデータ取得するSQL文
  7.  For i = 1 To UBound(buf, 1)
  8.   Me.ListBox1.AddItem ""
  9.   Me.ListBox1.List(i - 1, 0) = buf(i, 1)
  10.   Me.ListBox1.List(i - 1, 1) = buf(i, 2)
  11.   Me.ListBox1.List(i - 1, 2) = buf(i, 3)
  12.   Me.ListBox1.List(i - 1, 3) = buf(i, 4)
  13.   Me.ListBox1.List(i - 1, 4) = buf(i, 5)
  14.   Me.ListBox1.List(i - 1, 5) = buf(i, 6)
  15.   Me.ListBox1.List(i - 1, 6) = buf(i, 7)
  16.   Me.ListBox1.List(i - 1, 7) = Format(buf(i, 8), "#,##0")
  17.   Me.ListBox1.List(i - 1, 8) = buf(i, 9)
  18.   Me.ListBox1.List(i - 1, 9) = Format(buf(i, 10), "#,##0")
  19.  Next i
  20. End Sub
図7-6

365行目「Me.ListBox1.Clear」は、リストボックスのデータを一旦クリアしています。取引先コンボボックスと商品コンボボックスの際には「Clearは念の為」だったのですが、ここでは「フォーム起動時」の他に、起動後の売上データの「登録/更新」、データの「削除」、表示年を切り替えた後の「リスト再表示」ボタンをクリックした時にも呼び出されます。ですので元の表示データをクリアしておかないと、ダブって表示されてしまいます。

367行目「buf = SQL_exec5(SQLword("Desc"))」ではSQL文を図7-7から呼出し、それを引数にしてSQL文を実行し、戻り値である売上データ配列を変数bufに代入します。
SQL文を呼出し式にしたのは、今回「取得する列、絞り込み条件は一緒」なのに「並べ方のみが異なる」SQL文が必要になったため、コードの効率化+修正容易化を目的に独立させました。
なおSQLwordに渡す引数は「Asc(昇順)」「Desc(降順)」のどちらかで、「""(長さゼロの文字列)」を渡すと、既定の昇順になります。

369~381行目では、売上データ配列をリスト化しています。「データ列数=配列の列数」としており、配列の列順序通りにListBox1へ並べれば良いように、図7-7のSQL文を作っています
369行目「For i = 1 To UBound(buf, 1)」で、カウンタ変数iを配列の行数分だけ回しています。
370行目「Me.ListBox1.AddItem ""」では、リストボックスに空の行を作成します。
371行目「Me.ListBox1.List(i - 1, 0) = buf(i, 1)」では、作った空の行の1列目(インデックスはゼロ)に配列の1列目のデータ(売上番号)を書き込みます。372行目以降も2列目・3列目・・・と書き込みます。
なお8列目(インデックス=7)は「単価」、10列目(インデックス=9)は「合計」で両方とも金額のため、「Format(buf(i, 8), "#,##0")」と「3桁ごとにカンマ」を入れた表示に変換しています。

図7-6の367行目、図7-19の560行目から呼び出される「売上データ」を抽出するSQL文を作るのが図7-7です。引数として「並び順を指定するString」を受け取ります。
  1. '========== ⇩(17) 売上データ抽出のSQL文 ============
  2. Private Function SQLword(ORD As String) As String
  3.  SQLword = "SELECT S.SNo,S.Slip,S.SaleD,S.CustNo,C.Cname,S.PdctNo,P.Pname," & _
  4.       "S.UnitPrice,S.Quant,S.UnitPrice*S.Quant " & _
  5.       " FROM " & TBL0 & " S ," & TBL1 & " C ," & TBL2 & " P " & _
  6.       " where S.CustNo = C.CNo and S.PdctNo = P.PNo " & _
  7.       " and year(S.SaleD) =" & Me.TextBox4.Value & _
  8.       " order by S.SaleD " & ORD & " , S.SNo " & ORD
  9. End Function
図7-7

今回のSQL文は、3つのテーブルからデータを取り出しています。393~394行目「SQLword = "SELECT S.SNo,S.Slip,S.SaleD,S.CustNo,C.Cname,S.PdctNo,P.Pname," & "S.UnitPrice,S.Quant,S.UnitPrice*S.Quant " & _ 」では、取り出す10列分の列名を列記しています。しかし、どの列名がどのテーブルのものかを明記する必要があります。
ですので395行目「" FROM " & TBL0 & " S ," & TBL1 & " C ," & TBL2 & " P " & _」では、「テーブル名」の後ろにスペースを空けて別名(エイリアス:S、C、P )を設定し、各列名の前に付けています。例えば「S.SNo」は、TBL0(=Saleテーブル)のSNo列という意味になります。

396行目「" where S.CustNo = C.CNo and S.PdctNo = P.PNo " & _」は、3つのテーブルの結合の仕方を定めています。Saleテーブル(TBL0)のCustNoとCustテーブル(TBL1)のCNoを結びつけ、またSaleテーブル(TBL0)のPdctNoとPdctテーブル(TBL2)のPNoを結びつけて「見掛け上1つのテーブル」にしています。
また397行目「" and year(S.SaleD) =" & Me.TextBox4.Value & _」では、売上日付をTextBox4(表示年)のものに絞り込みをしています。

398行目「" order by S.SaleD " & ORD & " , S.SNo " & ORD」では、売上日付・売上番号を「引数で得た順序(Ascまたは""の時には昇順、Descの時には降順)」で並べ替えをしています。

7-2-4.入力部の処理

伝票番号を入力するTextBox1に、値を入力した時に発生するChangeイベントが図7-8です。
407行目「Me.TextBox1.Value = UCase(Me.TextBox1.Value)」で、入力した文字を「大文字」に変換しています。
  1. '========== ⇩(18) 伝票番号を入力 ============
  2. Private Sub TextBox1_Change()
  3.  Me.TextBox1.Value = UCase(Me.TextBox1.Value)
  4. End Sub
図7-8

売上日付を入力するTextBox2に、値を入力した時に発生するKeyPressイベントが図7-9です。
日付は「2022/12/24」や「2022-12-24」のように、数字及び「-」「/」しか使いません。ですので412行目「If Not Chr(KeyAscii) Like "[0-9/-]" Then KeyAscii = 0」で、数字及び「-」「/」以外は無視をしています。
Likeに続くカギカッコの中に、通過させる文字列を記載し、Notで反転することでそれ以外を無視させています。
  1. '========== ⇩(19) 売上日付を入力 ============
  2. Private Sub TextBox2_KeyPress(ByVal KeyAscii As MSForms.ReturnInteger)
  3.  If Not Chr(KeyAscii) Like "[0-9/-]" Then KeyAscii = 0
  4. End Sub
図7-9

売上個数を入力するTextBox3に、値を入力した時に発生するKeyPressイベントが図7-10です。
個数は数値のみなので、417行目「If Not Chr(KeyAscii) Like "[0-9]" Then KeyAscii = 0」で数値以外を無視します。
なお417行目の代わりに「If KeyAscii < Asc("0") Or KeyAscii > Asc("9") Then KeyAscii = 0」としてもOKです。
  1. '========== ⇩(20) 売上個数を入力 ============
  2. Private Sub TextBox3_KeyPress(ByVal KeyAscii As MSForms.ReturnInteger)
  3.  If Not Chr(KeyAscii) Like "[0-9]" Then KeyAscii = 0
  4. End Sub
図7-10

商品のコンボボックスを選択した時に発生するのが図7-11です。この商品リストの3列目の商品単価の処理を行ってます。
422行目「Me.Label2.Caption = Me.ComboBox2.List(Me.ComboBox2.ListIndex, 2)」で選択した項目の単価の値をLabel2の単価欄へ書き込みます。
次に、423行目「Me.Label3.Caption = Val(Me.Label2.Caption) * Val(Me.TextBox3.Value)」で、そのLable2の単価とTextBox3の個数を掛け合わせて、合計金額をLabel3の合計欄に書き込みます。もちろん個数が未入力(=ゼロ個)の場合は合計値もゼロとなります。
424行目「Me.Label3.Caption = Format(Me.Label3.Caption, "#,##0")」は、Label3の金額を3桁毎にカンマを付けています。423と424行目は、もちろん1つの行にすることが出来ます。
  1. '========== ⇩(21) 商品コンボボックスを選択 ============
  2. Private Sub ComboBox2_Click()
  3.  Me.Label2.Caption = Me.ComboBox2.List(Me.ComboBox2.ListIndex, 2)
  4.  Me.Label3.Caption = Val(Me.Label2.Caption) * Val(Me.TextBox3.Value)
  5.  Me.Label3.Caption = Format(Me.Label3.Caption, "#,##0")
  6. End Sub
図7-11

個数を入力した時に発生するのが図7-12です。商品単価とこの個数の値を使って合計値処理をします。
432行目「Me.Label3.Caption = Val(Me.Label2.Caption) * Val(Me.TextBox3.Value)」で、Lable2の単価と個数を掛け合わせて、合計金額をLabel3の合計欄に書き込みます。
433行目「Me.Label3.Caption = Format(Me.Label3.Caption, "#,##0")」は、Label3の金額を3桁毎にカンマを付けています。
Label2の単価とTextBox3の個数の掛け算がLabel3の合計金額になる訳ですが、単価と個数のどちらが先に値が入るのかは分かりませんので、図7-11と図7-12の両方に掛け算の式を入れています。
  1. '========== ⇩(22) 個数を入力 ============
  2. Private Sub TextBox3_Change()
  3.  Me.Label3.Caption = Val(Me.Label2.Caption) * Val(Me.TextBox3.Value)
  4.  Me.Label3.Caption = Format(Me.Label3.Caption, "#,##0")
  5. End Sub
図7-12

表示年を入力するTextBox4の文字種制限をするのが図7-13です。
442行目「If Not Chr(KeyAscii) Like "[0-9]" Then KeyAscii = 0」では、数値(0~9)以外は無視をしています。
代わりに「If KeyAscii < Asc("0") Or KeyAscii > Asc("9") Then KeyAscii = 0」としてもOKです。
  1. '========== ⇩(23) 表示年を入力 ============
  2. Private Sub TextBox4_KeyPress(ByVal KeyAscii As MSForms.ReturnInteger)
  3.  If Not Chr(KeyAscii) Like "[0-9]" Then KeyAscii = 0
  4. End Sub
図7-13

7-2-5.リストボックスの選択

売上データのリストボックスを選択した時、選択したデータを入力部に転記します。その処理を行うのが図7-14です。
  1. '========== ⇩(24) リストボックスの選択 ============
  2. Private Sub ListBox1_Click()
  3.  On Error Resume Next
  4.   With Me.ListBox1
  5.    Me.Label1.Caption = .List(.ListIndex, 0)
  6.    Me.TextBox1.Value = .List(.ListIndex, 1)
  7.    Me.TextBox2.Value = .List(.ListIndex, 2)
  8.    Me.ComboBox1.ListIndex = -1
  9.    Me.ComboBox1.Value = .List(.ListIndex, 3)
  10.    Me.ComboBox2.ListIndex = -1
  11.    Me.ComboBox2.Value = .List(.ListIndex, 5)
  12.    Me.Label2.Caption = .List(.ListIndex, 7)
  13.    Me.TextBox3.Value = .List(.ListIndex, 8)
  14.    Me.Label3.Caption = .List(.ListIndex, 9)
  15.   End With
  16.  On Error GoTo 0
  17. End Sub
図7-14

454行目「With Me.ListBox1」以降でListBox1の値を各コントロールへ転記処理を行います。転記するのは、図7-15のように全部で8項目です。
送る側受け取る側
項目列Index
売上番号0Label1
伝票番号1TextBox1
売上日付2TextBox2
取引先番号3ComboBox1
商品番号5ComboBox2
単価7Label2
個数8TextBox3
合計金額9Label3
図7-15

455~457行目では、リストの先頭側からまず「売上番号」「伝票番号」「売上日付」を「Label1」「TextBox1」「TextBox2」に値転記します。

次に、取引先関係は「取引先番号」と「取引先名」がありますが、受け取るComboBox1側には「コンボボックスの先頭列の取引先番号」を渡します。これはInitializeイベント等で「BoundColumn」の設定をしていない為、既定値の1(=先頭列がコンボボックスの値)となっている為です。
例えばBoundColumn値を2と設定した場合は、取引先名を転記することになります。しかし、今回「取引先名には何も制約を設定していない(図6-10)」ために、名前が重複していた場合に先にある方を選択してしまうことになります。
ですので「BoundColumn値にはPrimary Keyに設定した列を指定」する必要があります(今回は先頭列だった為省略した)。

また、コンボボックスの転記する前の458行目で「Me.ComboBox1.ListIndex = -1」と、一旦「非選択状態」にしています。これは「削除相当(Del列=True)の取引先データを選択」した場合、459行目「Me.ComboBox1.Value = .List(.ListIndex, 3)」が「(Del列=Trueのレコードは、リスト非表示のため)コンボボックスのリスト内には、相当する取引先番号が無い」ためにエラーとなります。エラー自体は453行目「On Error Resume Next」でスルーされるのですが、459行目が実行されない為に「前回選択した取引先データが表示されたまま」となってしまいます。
この矛盾を解消するために、一旦458行目で「非選択」にしています。ですので、もし削除相当の項目を選択した時には、コンボボックスは「何も選択されていない状態」となります。

460~461行目の商品番号をComboBox2に転記するのも、ComboBox1と同様です。

462~464行目は、「単価」「個数」「合計金額」を「Label2」「TextBox3」「Label3」に値転記します。

7-2-6.データ処理

7-2-6-1.登録/更新ボタン
「登録/更新」ボタン(CommandButton1)をクリックした時に実行されるのが図7-16です。
  1. '========== ⇩(25) 登録/更新ボタン ============
  2. Private Sub CommandButton1_Click()
  3.  Dim sql As String        '←SQL文
  4.  Dim Ans As VbMsgBoxResult   '←MsgBoxでの返答
  5.  If isMissingInput Then Exit Sub     '←入力部の入力有無を確認
  6.  If Me.Label1.Caption = "" Then     '←SNoが無い場合
  7.   sql = "Insert into " & TBL0 & " (Slip,SaleD,CustNo,PdctNo,UnitPrice,Quant)" & _
  8.      " values('" & Me.TextBox1.Value & "','" & _
  9.      Me.TextBox2.Value & "','" & _
  10.      Me.ComboBox1.List(Me.ComboBox1.ListIndex, 0) & "','" & _
  11.      Me.ComboBox2.List(Me.ComboBox2.ListIndex, 0) & "'," & _
  12.      Val(Me.Label2.Caption) & "," & _
  13.      Val(Me.TextBox3.Value) & ")"
  14.  Else   '←SNoが有る場合
  15.   Ans = MsgBox("データを上書き(Yes)しますか? 追加(No)しますか?", vbYesNo)
  16.   If Ans = vbYes Then     '←「上書き」をユーザーが指定した場合
  17.    sql = "Update " & TBL0 & _
  18.       " set Slip= '" & Me.TextBox1.Value & "'," & _
  19.       " SaleD='" & Me.TextBox2.Value & "'," & _
  20.       " CustNo='" & Me.ComboBox1.List(Me.ComboBox1.ListIndex, 0) & "'," & _
  21.       " PdctNo='" & Me.ComboBox2.List(Me.ComboBox2.ListIndex, 0) & "'," & _
  22.       " UnitPrice=" & Val(Me.Label2.Caption) & "," & _
  23.       " Quant=" & Val(Me.TextBox3.Value) & _
  24.       " where SNo=" & Val(Me.Label1.Caption)
  25.   Else     '←「追加」をユーザーが指定した場合
  26.    Me.Label1.Caption = ""     '←SNoを消して
  27.    Call CommandButton1_Click     '←データ新規追加として再帰呼び出し
  28.    Exit Sub
  29.   End If
  30.  End If
  31.  Call SQL_exec5(sql)
  32.  Call makeListBox
  33. End Sub
図7-16

475行目「If isMissingInput Then Exit Sub」では、図7-24を呼び出し「入力部全てに、正しい値が入力されているか」をチェックし、正しくなければTrueを返してきます。正しくない場合は「Exit Sub」で処理を中止します。

正しく入力されている場合、SNo(売上番号:Label1)が入力されているか否かで処理を分岐させます。SNoが無ければ「ダイアログを起動した後、ユーザーが全ての項目を手入力した」ことになります。また、SNoが有れば「リストを選択し、項目の内容を変更してから登録/更新ボタンをクリック」した、と考えられます。
ですので、まず477行目「If Me.Label1.Caption = "" Then」が成立する場合は、479~485行目のSQL文を作成します。
作成するSQL文はInsert文で、479行目「sql = "Insert into " & TBL0 & " (Slip,SaleD,CustNo,PdctNo,UnitPrice,Quant)" & _」で、TBL0(=Saleテーブル)に6列分(Slip,SaleD,CustNo,PdctNo,UnitPrice,Quant)のデータを挿入します。
データとしては、
Slip列には480行目「" values('" & Me.TextBox1.Value & "','" & _」でTextBox1の伝票番号を、
SaleD列には481行目「Me.TextBox2.Value & "','" & _」でTextBox2の売上日付を、
CustNo列には482行目「Me.ComboBox1.List(Me.ComboBox1.ListIndex, 0) & "','" & _」でComboBox1の取引先番号を、
PdctNo列には483行目「e.ComboBox2.List(Me.ComboBox2.ListIndex, 0) & "'," & _」でComboBox2の商品番号を、
UnitPrice列には484行目「Val(Me.Label2.Caption) & "," & _」でLabel2の単価を、
Quant列には485行目「Val(Me.TextBox3.Value) & ")"」でTextBox3の個数を挿入します。

なおSaleテーブルは全7列です。479~485行目のInsert文では6列しかデータを入れていませんが、残り1列「SNo」はデータ型をCOUNTERとしていますので「自動的に採番」を振ってくれますのでInsert文では対象外としています。

SNoが有る場合(487行目「Else」以降)は、488行目「Ans = MsgBox("データを上書き(Yes)しますか? 追加(No)しますか?", vbYesNo)」でメッセージボックスを表示させ、ユーザーに「上書き」か「新規としてデータ追加」するかを選択させます。
MsgBoxの回答が、490行目「If Ans = vbYes Then」で上書きの場合は、491~498行目でUpdate文を作成します。
491行目「sql = "Update " & TBL0 & _」ではTBL0(Saleテーブル)に対して更新をします。
492行目「" set Slip= '" & Me.TextBox1.Value & "'," & _」で、Slip列をTextBox1(伝票番号)の値にします。
493行目「" SaleD='" & Me.TextBox2.Value & "'," & _」で、SaleD列をTextBox2(売上日付)の値にします。
494行目「" CustNo='" & Me.ComboBox1.List(Me.ComboBox1.ListIndex, 0) & "'," & _」で、CustNo列をComboBox1で選択中のゼロ列目(取引先番号)の値にします。
495行目「" PdctNo='" & Me.ComboBox2.List(Me.ComboBox2.ListIndex, 0) & "'," & _」で、PdctNo列をComboBox2で選択中のゼロ列目(商品番号)の値にします。
496行目「" UnitPrice=" & Val(Me.Label2.Caption) & "," & _」で、UnitPrice列をLabel2(単価)の文字列を数値に変換した値にします。
497行目「" Quant=" & Val(Me.TextBox3.Value) & _」で、Quant列をTextBox3(個数)の文字列を数値に変換した値にします。
498行目「" where SNo=" & Val(Me.Label1.Caption)」では、SNoがLabel1(売上番号)のものだけを変更します。SNoはSaleテーブルではPrimary Keyですので、テーブル内で1行のみが書き換わることになります。

なお、合計値はSaleテーブルの中では保持せず「単価×個数」で、都度計算をしています。

488行目のメッセージボックスで、ユーザーが「いいえ(=新規としてデータ追加)」を選んだ(500行目「Else」)場合は、501行目「Me.Label1.Caption = ""」でSNo(売上番号)を削除してから、502行目「Call CommandButton1_Click」で「このプロシージャを再帰呼び出し」します。呼び出された側では、「SNoが無い」ことからそのまま「新規追加」として479~485行目のInsert文が実行されます。
503行目「Exit Sub」では、再帰呼び出しでInsert文を実行した後このプロシージャを抜け出し、処理を終了します。

SQL文が作成されたのち、508行目「Call SQL_exec5(sql)」で図6-16を呼出し、SQL文を実行します。
なお図6-16はFunctionプロシージャですが、今回実行するInsert文とUpdate文では「Select文のように受け取るレコードが無い」ために、「buf = SQL_exec5(sql)」とする必要はありません。

テーブルへの追加・変更が完了した後、510行目「Call makeListBox」で図7-6を呼出し、リストボックスを更新します。

7-2-6-2.削除ボタン
削除ボタンをクリックした時に呼び出されるのが図7-17です。
  1. '========== ⇩(26) 削除ボタン ============
  2. Private Sub CommandButton2_Click()
  3.  Dim sql As String       '←SQL文
  4.  Dim Ans As VbMsgBoxResult   '←MsgBoxの回答
  5.  If Me.ListBox1.ListIndex = -1 Then
  6.   MsgBox "リストから削除する項目を選択して下さい。"
  7.   Exit Sub
  8.  End If
  9.  Ans = MsgBox("選択項目を削除して良いですか?", vbYesNo)
  10.  If Ans = vbNo Then Exit Sub
  11.  sql = "Delete from " & TBL0 & _
  12.     " where SNo= " & Me.ListBox1.List(Me.ListBox1.ListIndex, 0)
  13.  Call SQL_exec5(sql)
  14.  Call makeListBox
  15. End Sub
図7-17

項目を削除するには、その削除対象が明確である必要があります。今回は「リスト上で選択した項目」を削除対象としています。525~528行目では、その削除対象が選択されているかを確認しています。
525行目「If Me.ListBox1.ListIndex = -1 Then」でリストボックスが選択されていない(ListIndex = -1 )場合は、526行目「MsgBox "リストから削除する項目を選択して下さい。"」でメッセージを出し、527行目「Exit Sub」で処理を中止します。
リストボックスが選択されている場合は、530行目「Ans = MsgBox("選択項目を削除して良いですか?", vbYesNo)」でユーザーへ再確認を行い、「いいえ(=削除しない)」を選んだ場合は531行目「If Ans = vbNo Then Exit Sub」で処理を中止します。

削除OKの場合は533~534行目でDelete文を作成します。
533行目「sql = "Delete from " & TBL0 & _」では、TBL0(Saleテーブル)を対象にレコードを削除します。
534行目「" where SNo= " & Me.ListBox1.List(Me.ListBox1.ListIndex, 0)」では削除するレコードを、SNo列が「リストボックスで選択している項目の先頭列の値(売上番号)のもの」に絞り込んでいます。SNoはSaleテーブルではPrimary Keyになっていますので、実際は1レコードに絞られます。
なお、リストボックスで選択した項目のSNoは、図7-14の455行目でLabel1に転記されていますので、534行目を「" where SNo=" & Val(Me.Label1.Caption)」としてもOKです。

536行目「Call SQL_exec5(sql)」は、作成したSQL文を図6-16に送付し、SQL文を実行(=レコード削除)しています。
削除が完了したら、リストボックスに反映するため、538行目「Call makeListBox」で図7-6を呼出し、リストを更新します。

7-2-6-3.リストボックス再表示
「リスト再表示」ボタンをクリックした時に呼び出されるのが図7-18です。主に表示年(TextBox4)を変更した後にリスト更新するのに使われると考えています。
547行目「Call makeListBox」で図7-6を呼出して、リストボックスを更新します。
  1. '========== ⇩(27) リストボックス再表示 ============
  2. Private Sub CommandButton3_Click()
  3.  Call makeListBox
  4. End Sub
図7-18

7-2-6-4.シートへの出力
「リスト出力」ボタンをクリックした時に呼び出されるのが図7-19です。
  1. '========== ⇩(28) シートへの出力 ============
  2. Private Sub CommandButton4_Click()
  3.  Dim buf As Variant   '←売上データ配列
  4.  Dim r As Range     '←シート上に既に出力されているデータ
  5.  Set r = Sheet1.Range("B2").CurrentRegion
  6.  Sheet1.Range(Range("B2:K2"), Cells(r.Row + r.Rows.Count - 1, 2)).Offset(1, 0).ClearContents
  7.  Sheet1.Range("C1") = Me.TextBox4.Value
  8.  buf = SQL_exec5(SQLword("Asc"))   '←昇順のデータ
  9.  Set r = Sheet1.Range("B3").Resize(UBound(buf, 1) + 1, UBound(buf, 2))
  10.  r = buf
  11.  r.Rows(1).Delete
  12. End Sub
図7-19

555行目「Set r = Sheet1.Range("B2").CurrentRegion」は、タイトルを含め「シート上に出力されているデータ範囲」を調べています。図7-20のように、セル範囲「B2」は、売上番号である「No」が元々記入されている場所です。それに繋がっている範囲(CurrentRegion)が変数rに設定されます。

一番下の行を見つける
図7-20

556行目「Sheet1.Range(Range("B2:K2"), Cells(r.Row + r.Rows.Count - 1, 2)).Offset(1, 0).ClearContents」は、タイトルを除いた「データの範囲のみをクリア」しています。
まず「Sheet1.Range(Range("B2:K2"), Cells(r.Row + r.Rows.Count - 1, 2))」は、行位置を表している「Range("B2:K2")」がタイトル行で、列位置を表している「Cells(r.Row + r.Rows.Count - 1, 2)」が一番したのセル位置ですので、それを外枠とする範囲(図7-21)となります。
タイトルを含む範囲の特定
図7-21

その範囲を「.Offset(1, 0)」で、図7-22のように1行下に移動させ、その範囲をクリア(.ClearContents)します。
クリアする範囲の一番下は「もともと空行」ですが、数式が長くなるので許容させています。
クリアする範囲を選択
図7-22

558行目「Sheet1.Range("C1") = Me.TextBox4.Value」は、C1セルにTextBox4(表示年)の値を書き込んでいます。
560行目「buf = SQL_exec5(SQLword("Asc"))」は、図7-7のSQLwordを呼出し、SQL文を取得します。引数に「Asc」を指定していますので、「昇順のデータ配列」が戻され、変数bufに代入します。
561行目「Set r = Sheet1.Range("B3").Resize(UBound(buf, 1) + 1, UBound(buf, 2))」では、タイトル行のすぐ下(B3セル)を基準にして、配列bufのサイズに合うようにデータ貼り付け範囲を設定します。行方向を「+1」しているのは、データ配列bufのゼロ行目には、タイトルが含まれている為です。
562行目「r = buf」で、データ配列を561行目で定めたセル範囲r に貼り付けます。
563行目「r.Rows(1).Delete」で、データ貼付け範囲の1行目(タイトル行)を削除します。「r.Rows(1)」ですので、行削除では無く、範囲内の行削除で「データが上に持ち上がる」ことになります。

7-2-6-5.終了ボタン
「終了」ボタンをクリックした時に呼び出されるのが図7-23です。572行目「Unload Me」で、フォームを閉じます。
  1. '========== ⇩(29) 終了ボタン ============
  2. Private Sub CommandButton5_Click()
  3.  Unload Me
  4. End Sub
図7-23

7-2-6-6.入力部のチェック
図7-16の475行目から呼び出される「入力が正しく行われているか」をチェックするのが図7-24です。
  1. '========== ⇩(30) 入力部のチェック ============
  2. Private Function isMissingInput() As Boolean
  3.  If Me.ComboBox1.ListIndex = -1 Or Me.ComboBox2.ListIndex = -1 Or _
  4.     Trim(Me.TextBox1.Value) = "" Or Trim(Me.TextBox3.Value) = "" Or _
  5.     Not IsDate(Me.TextBox2.Value) Then
  6.   isMissingInput = True
  7.   MsgBox "伝票、日付、取引先、商品名、個数のどれかが無効です"
  8.  End If
  9. End Function
図7-24

582~584行目のIF文で、2ヶ所のコンボボックス+3ヶ所のテキストボックスの内容をチェックしています。
582行目「If Me.ComboBox1.ListIndex = -1 Or Me.ComboBox2.ListIndex = -1 Or _」では、取引先用コンボボックス(ComboBox1)、商品用コンボボックス(ComboBox2)が選択されているか(選択されていなければListIndex = -1)をチェックします。
583行目「Trim(Me.TextBox1.Value) = "" Or Trim(Me.TextBox3.Value) = "" Or _」では、伝票番号(TextBox1)、個数(TextBox3)が空で無いかをチェックしています。スペースだけ入れられても困るので、Trim関数でスペース削除しています。
584行目「Not IsDate(Me.TextBox2.Value) Then」では、売上日付(TextBox2)の値が日付になっているかをチェックします。

If文の5つの入力部の「どれか1ヶ所でもNG」だった場合は、585行目「isMissingInput = True」でこの関数の戻り値をTrue(=エラー有り)にし、586行目「MsgBox "伝票、日付、取引先、商品名、個数のどれかが無効です"」でコメントを出しています。
5ヶ所の内のどこが違っているかをコメントする事も可能ですが、今回は一括チェックとしました。
なお、エラーが無い場合の戻り値は、Boolean型の初期値のFalseとなります。

8.取引先ダイアログ(UserForm2)

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

取引先データを操作するダイアログがUserForm2になり、そのフォーム上のコントロール類の配置は図8-1の様にしました。
取引先ダイアログ
図8-1

操作を実行するCommandButtonをフォーム下部に3つ並べます。また中央部にデータ一覧を表示するListBox1を配置します。
ダイアログ上部に入力の為のTextBoxを2つと、CheckBoxを配置します。

8-2.フォームモジュール(UserForm2)

フォームモジュール先頭で、モジュール内で使用するフラグ変数を宣言します。
  1. '========== ⇩(31) 宣言部 ============
  2. Dim EventOff As Boolean
図8-2

このフォームのリストボックスを選択した時には、その値を入力部であるテキストボックス等にリストの値を転記します。しかし、そのテキストボックスには「変更があった時にはリストボックスを操作する」コードが記されているため、おかしな動きをすることになってしまいます。
そこで591行目「Dim EventOff As Boolean」で宣言するフラグ変数「EventOff」を使って、一時的にテキストボックスのコードが実行されないようにしています。

8-2-1.起動時準備

フォーム起動時には、図8-3のInitializeイベントが発生します。ここではコントロール類の静的な設定をします。
  1. '========== ⇩(32) フォーム起動時 ============
  2. Private Sub UserForm_Initialize()
  3.  Me.TextBox1.IMEMode = fmIMEModeOff
  4.  Me.ListBox1.ColumnCount = 3
  5.  Me.ListBox1.ColumnWidths = "50;120;15"
  6.  Me.ListBox1.TextAlign = fmTextAlignLeft
  7. End Sub
図8-3

602行目「Me.TextBox1.IMEMode = fmIMEModeOff」は、取引先番号欄(TextBox1)を編集する際には、一旦IMEをOffにしています。全角を番号に使う事はほとんど無いと考えたためです。

604行目「Me.ListBox1.ColumnCount = 3」は、取引先一覧を表示するリストボックスを3列(取引先番号、取引先名、非表示有無)の指定にしています。
605行目「Me.ListBox1.ColumnWidths = "50;120;15"」では、各列の列幅を設定しています。
606行目「Me.ListBox1.TextAlign = fmTextAlignLeft」では、リストの各列を「左寄せ」で表示させます。
寄り道
なお数値等を自然に見せるため、リストを「右寄せ」にする場合もあると思います。その際には606行目を「Me.ListBox1.TextAlign = fmTextAlignRight」と設定するのですが、注意する点があります。

右寄せをすると、全ての列の値が右寄せになり、且つ最も右側の列の値はリストボックスの右端に貼り付くように表示されます。表示行数が少ない場合は問題ないのですが、行数が増えて縦移動用のスクロールバーが表示されると、図8-4の中央のように「縦スクロールバーで際右列の値が隠れてしまう」現象が起きます。今回の様に1文字(レ点)表示の列だった場合は、全く表示している意味がありません。
リスト右寄せ時の注意
図8-4

そこでリストを右寄せ表示する場合は、ダミー列(今回ならば4列目)を作り「縦スクロールバーが現れても、隠れるのはダミー列」という状態にすると、情報が欠落せずに済みます。

ダイアログが表示された後に実行されるのが図8-5のActivateイベントです。
612行目「Call makeListBox」で図8-6を呼出し、取引先一覧をリストボックスに表示させます。
  1. '========== ⇩(33) フォーム表示時 ============
  2. Private Sub UserForm_Activate()
  3.  Call makeListBox
  4. End Sub
図8-5

8-2-2.リストボックス作成

図8-5の612行目、及び図8-10の711行目、図8-11の750行目から呼び出される「リストボックス上に取引先一覧を作成」するのが図8-6です。
  1. '========== ⇩(34) リストボックス作成 ============
  2. Private Sub makeListBox()
  3.  Dim sql As String   '←SQL文
  4.  Dim i As Long     '←カウンタ変数(行数)
  5.  Dim buf As Variant   '←取引先データ配列
  6.  Me.ListBox1.Clear
  7.  sql = "SELECT CNo,Cname,IIF(Del=True,'レ','') FROM " & TBL1 & _
  8.     " order by CNo"
  9.  buf = SQL_exec5(sql)
  10.  For i = 1 To UBound(buf, 1)
  11.   Me.ListBox1.AddItem ""
  12.   Me.ListBox1.List(i - 1, 0) = buf(i, 1)
  13.   Me.ListBox1.List(i - 1, 1) = buf(i, 2)
  14.   Me.ListBox1.List(i - 1, 2) = buf(i, 3)
  15.  Next i
  16. End Sub
図8-6

626行目「Me.ListBox1.Clear」では、一旦リストボックスのデータをクリアします。
628~629行目「sql = "SELECT CNo,Cname,IIF(Del=True,'レ','') FROM " & TBL1 & " order by CNo"」では、取引先データをTBL1(Custテーブル)から取得するSQL文を作成します。取り出す列は全3列の「CNo」「Cname」「Del」ですが、Del列はBoolean型なので「True(非表示)の時はレ点、Falseの時は空文字」に変換しています。
なお、VBAでは文字列を囲む時は「"(ダブルクォーテーション)」を使いますが、SQL文では「'(シングルクォーテーション)」を使います。
また並び順は「order by CNo」と正逆を指定していない為「既定のAsc」を指定した事となり、取引先番号の昇順で取り出します。

631行目「buf = SQL_exec5(sql)」で、図6-16を呼出してSQL文を実行し、戻り値として取引先データの配列をbufで受け取ります。
633行目「For i = 1 To UBound(buf, 1)」では、受け取った配列の行数分だけカウンタ変数iを回します。
634行目「Me.ListBox1.AddItem ""」でリストボックスに新たな行を作成し、635行目「Me.ListBox1.List(i - 1, 0) = buf(i, 1)」でリストボックスの1列目(インデックス=0)に配列の1列目データを書き込みます。以下同様に2列目・3列目のデータをリストボックスの2列目・3列目に書き込みます。

8-2-3.入力部の処理

取引先番号欄に入力を行った時には図8-7が呼び出されます。
  1. '========== ⇩(35) 取引先番号 ============
  2. Private Sub TextBox1_Change()
  3.  If EventOff = True Then Exit Sub
  4.  TextBox1.Value = Trim(UCase(TextBox1.Value))
  5.  Me.ListBox1.ListIndex = -1
  6. End Sub
図8-7

この取引先番号欄に値が入るのは、図8-8のように「ユーザーが手入力」した場合、「リストボックスの項目を選択した結果、既存の取引先番号が転記」された場合、「その転記された取引先番号をユーザーが手直し」した場合の3つが考えられます。
入力の方法入力値の意味処理
1ユーザーが手入力新たな番号アルファベット等を大文字揃え
2リストボックスを選択既存の番号リストは選択状態
3転記された番号を修正新番号に修正新番号の為、リスト選択を解除
図8-8

652行目「If EventOff = True Then Exit Sub」では、フラグ変数EventOffがTrueの時には以下を実行せずに抜け出します。これは、図8-8のNo.2に相当します。
654行目「TextBox1.Value = Trim(UCase(TextBox1.Value))」では、Ucase関数で「大文字揃え」にしています。これは、図8-8のNo.1、及びNo.3に相当します。加えてTrim関数で両端のスペースを削除しています。
スペース削除処理は、登録直前の図8-14(isMissingInputプロシージャ)で行っても良いのですが「真ん中にスペースが入る取引先番号はあり得ない」だろうとの判断から、入力の都度チェックが入るTextBoxのChangeイベントで処理を行うことにしました。他方、TextBox2の取引先名の方は「スペースを真ん中に含んだ名前もあり得る」と思い、図8-14側でTrim処理をしています。
655行目「Me.ListBox1.ListIndex = -1」では、リストボックスを非選択状態にしています。これは、図8-8のNo.3に相当します。

8-2-4.リストボックスの選択

リストボックスの項目をユーザーが選択した場合には、図8-9が呼び出されます。
  1. '========== ⇩(36) リストボックスの選択 ============
  2. Private Sub ListBox1_Click()
  3.  EventOff = True
  4.   With Me.ListBox1
  5.    Me.TextBox1.Value = .List(.ListIndex, 0)
  6.    Me.TextBox2.Value = .List(.ListIndex, 1)
  7.    Me.CheckBox1.Value = IIf(.List(.ListIndex, 2) = "レ", True, False)
  8.   End With
  9.  EventOff = False
  10. End Sub
図8-9

663行目「With Me.ListBox1」では、以下のコードをListBox1ベースで実行していきます。
664行目「Me.TextBox1.Value = .List(.ListIndex, 0)」で、リストボックスの選択している行の1列目(インデックス=0)の値をTextBox1(取引先番号)へ書き込みます。
665行目「Me.TextBox2.Value = .List(.ListIndex, 1)」で、2列目(インデックス=1)の値をTextBox2(取引先名)へ書き込みます。
666行目「Me.CheckBox1.Value = IIf(.List(.ListIndex, 2) = "レ", True, False)」で、3列目(インデックス=2)の値を確認し、もしレ点が付いていればCheckBox1のValueプロパティをTrueにします。逆にレ点が付いていない(=値が長さゼロの文字列)時はFalseにします。
このCheckBoxの値は、CustテーブルのDel列の値と同じになります。
(Del列がTrue = 非表示 = リスト上でレ点有り = CheckBoxでレ点有り = CheckBoxのValue値がTrue)

なお664行目を実行すると、TextBox1のChangeイベント(図8-7)が呼び出され、その中の655行目「Me.ListBox1.ListIndex = -1」でリストボックスが非選択状態になってしまいます。ですので662行目「EventOff = True」でフラグ変数EventOffをTrueにしています。

8-2-5.データ処理

8-2-5-1.登録/更新ボタン
「登録/更新」ボタンをクリックした時に呼び出されるのが、図8-10です。
  1. '========== ⇩(37) 登録/更新ボタン ============
  2. Private Sub CommandButton1_Click()
  3.  Dim sql As String    '←SQL文
  4.  Dim buf As Variant    '←Select文の戻り値の配列
  5.  Dim Ans As VbMsgBoxResult    '←MsgBoxのユーザーの回答
  6.  If isMissingInput Then Exit Sub    '←入力部の入力有無確認
  7.  sql = "SELECT count(*) FROM " & TBL1 & _
  8.     " where CNo = '" & Me.TextBox1.Value & "'"
  9.  buf = SQL_exec5(sql)
  10.  If buf(1, 1) = 0 Then    '←Custテーブルには同じ取引先番号が無い
  11.   sql = "Insert into " & TBL1 & " (Cno,Cname,del)" & _
  12.      " values('" & Me.TextBox1.Value & "','" & _
  13.      Me.TextBox2.Value & "'," & _
  14.      Me.CheckBox1.Value & ")"
  15.  Else    '←Custテーブル内に同じ取引先番号が有る
  16.   If vbNo = MsgBox("取引先場号が既に存在します。上書きしますか?", vbYesNo) _
  17.     Then Exit Sub    '←ユーザーが上書きを拒否
  18.   sql = "Update " & TBL1 & _
  19.      " set Cname='" & Me.TextBox2.Value & "'," & _
  20.      "del = " & Me.CheckBox1.Value & _
  21.      " where CNo= '" & Me.TextBox1.Value & "'"
  22.  End If
  23.  Call SQL_exec5(sql)
  24.  Call makeListBox
  25. End Sub
図8-10

686行目「If isMissingInput Then Exit Sub」は図8-14を呼出し、「入力部(TextBox1とTextBox2)に正しく入力されているか」を確認します。戻り値として「入力OK=False、NG=True」が返ってきますので、NGだった場合は「Exit Sub」で処理を中止します。

688~689行目「sql = "SELECT count(*) FROM " & TBL1 & " where CNo = '" & Me.TextBox1.Value & "'"」は、TextBox1に入力された取引先番号が、TBL1(Custテーブル)内にいくつあるかを求めるSQL文です。
CustテーブルのCNo列はPrimary Keyですので、その取引先番号があったら「1」、無ければ「ゼロ」が得られるはずです。
690行目「buf = SQL_exec5(sql)」で、上記のSQL文を実行し、戻り値の配列を変数bufに代入します。

bufは配列です。今回のSQL文の取得する列は「count(*)」で、値は1レコード(1 または 0)ですので、求める値はbuf(1, 1)に入っている事になります。
692行目「If buf(1, 1) = 0 Then」でその値を調べ、ゼロだった場合(同じ取引先番号が無い)には693~696行目を実行、1だった場合(同じ取引先番号が有る)には699~705行目を実行します。

693~696行目は、同じ取引先番号が無い場合にレコードを追加するInsert文です。
693行目「sql = "Insert into " & TBL1 & " (CNo,Cname,del)" & _」では、TBL1(Custテーブル)のCNo列、Cname列、Del列の順番に値を追加します。値は、以下のvalues()のカッコ内に、指定した列の順番通りに指定していきます。
694行目「" values('" & Me.TextBox1.Value & "','" & _」は、CNo列にTextBox1(取引先番号)の値を入れます。
695行目「Me.TextBox2.Value & "'," & _」は、Cname列にTextBox2(取引先名)の値を入れます。
696行目「Me.CheckBox1.Value & ")"」は、Del列にCheckBox(非表示)の値を入れます。
なおDel列のデータ型はBIT型(VBAだとBoolean型)なので、既定値はFalseとなります。もしレ点が無い(=False)ことが分かっていれば、あえて値をInsertしなくても値は「False」となります。今回はCheckBoxでユーザー指定できるようにしたため、Del列にも値を指定しています。

同じ取引先番号が有る場合は、699~700行目「If vbNo = MsgBox("取引先場号が既に存在します。上書きしますか?", vbYesNo) Then Exit Sub」で、上書きするか否かのメッセージボックスを表示し、その回答が「いいえ(上書きしない)」だった場合に、処理を中止しています。
「はい(上書きする)」を選択した場合は、702~705行目で「TextBox1の取引先番号のレコード」を更新するUpdate文を作成します。
702行目「sql = "Update " & TBL1 & _」で、TBL1(Custテーブル)に対してデータの更新をします。
703行目「" set Cname='" & Me.TextBox2.Value & "'," & _」は、Cname列の値をTextBox2の値に書き換えます。
704行目「"del = " & Me.CheckBox1.Value & _」は、Del列の値をCheckBox1のValue値(True 又は False)に書き換えます。
705行目「" where CNo= '" & Me.TextBox1.Value & "'"」は更新の対象を、CNo列がTextBox1の値のレコードに絞っています。CNo列はPrimary Keyですので、書き換えられるのは1レコードのみになります。

SQL文が完成しましたので、709行目「Call SQL_exec5(sql)」で図6-16を呼出し、SQL文を実行します。
テーブルを書き換えた後は、711行目「Call makeListBox」で図8-6を呼び出し、リストボックスを更新します。

8-2-5-2.削除ボタン
「削除」ボタンをクリックした時に呼び出されるのが図8-11です。
  1. '========== ⇩(38) 削除ボタン ============
  2. Private Sub CommandButton2_Click()
  3.  Dim sql As String     '←SQL文
  4.  Dim buf As Variant    '←Select文の戻り値の配列
  5.  Dim Ans As VbMsgBoxResult    '←MsgBoxのユーザーの回答
  6.  If Me.ListBox1.ListIndex = -1 Then
  7.   MsgBox "リストから削除する項目を選択して下さい。"
  8.   Exit Sub
  9.  End If
  10.  Ans = MsgBox("選択項目を削除して良いですか?", vbYesNo)
  11.  If Ans = vbNo Then Exit Sub    '←ユーザーが削除を拒否
  12.  sql = "SELECT count(*) FROM " & TBL0 & _
  13.     " where CustNo = '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"
  14.  buf = SQL_exec5(sql)
  15.  If buf(1, 1) = 0 Then    '←売上台帳に使用されていない場合
  16.   sql = "Delete from " & TBL1 & _
  17.      " where CNo= '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"
  18.  Else    '←売上台帳に使用されている場合
  19.   sql = "Update " & TBL1 & _
  20.      " set Del= true " & _
  21.      " where CNo= '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"
  22.   MsgBox "データが他のテーブルで使用されている為、非表示扱いにします。"
  23.  End If
  24.  Call SQL_exec5(sql)
  25.  Call makeListBox
  26. End Sub
図8-11

項目を削除するには、その削除対象が明確である必要であり、今回は「リスト上で選択した項目」を削除対象としています。
726~729行目では、その削除対象が選択されているかを確認しています。
726行目「If Me.ListBox1.ListIndex = -1 Then」でリストボックスが選択されていない(ListIndex = -1 )場合は、727行目「MsgBox "リストから削除する項目を選択して下さい。"」でメッセージを出し、728行目「Exit Sub」で処理を中止します。

リストボックスが選択されている場合は、731行目「Ans = MsgBox("選択項目を削除して良いですか?", vbYesNo)」でユーザーへ再確認を行い、「いいえ(=削除しない)」を選んだ場合は732行目「If Ans = vbNo Then Exit Sub」で処理を中止します。

削除OKの場合、まず「削除しようとしている項目が、他のテーブル(Saleテーブル:TBL0)で使われていないか」を調べる必要があります。無理に削除しようとすると図8-12のようなエラーが発生します。
参照制約でのエラー
図8-12
寄り道
参照制約で引っ掛かった時のエラー番号は図8-12の通り「-2147467259」です。このエラー番号を使い「構わずにDeleteを実行し、参照制約違反のエラー番号が出たら改めてUpdateする」という手法を当初考えていました。しかし調べてみると、このエラー番号は参照制約違反「専用」のものでは無さそうでした。
ですので、図6-16(SQL_exec5関数プロシージャ)の中ではエラー番号を取得し呼出し元に戻すことはしませんでした。

データベースに接続出来なかったり、制約を違反したりした際のエラー番号を上手く使えれば、もう少しカッコ良い方法でSQLが実行できそうなのですが・・・ エラー種類については、もう少し調べる必要がありそうです。

734~735行目「sql = "SELECT count(*) FROM " & TBL0 & " where CustNo = '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"」は、削除しようとしている取引先番号が、TBL0(Saleテーブル)のCustNo列の中に何レコード使われているかを取得するSQL文です。
SQL文は736行目「buf = SQL_exec5(sql)」で実行され、そのレコード数は変数bufに代入されます。

738行目「If buf(1, 1) = 0 Then」は、「使用しているレコード数」を調べた結果ゼロ(=使用されていない)の場合は「レコードを削除するSQL文(Delete文)」を作成します。それ以外(=使用されている)の場合は、削除する訳にはいかないので「選択リストには表示しない」状態にするため「Del列をTrueに変更するSQL文(Update文)」を作成します。

まずDelete文は、739行目「sql = "Delete from " & TBL1 & _」で TBL1(Custテーブル)に対して削除を行い、740行目「" where CNo= '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"」で、その削除対象のCNoを「リスト選択している先頭列の取引先番号」に絞り込みます。CustテーブルではCNoはPrimary Keyですので、削除される対象は1レコードとなります。

次にUpdate文は、742行目「sql = "Update " & TBL1 & _」で TBL1(Custテーブル)に対して変更を行い、743行目「" set Del= true " & _」でDel列をTrueに設定します。その変更の対象は744行目「" where CNo= '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"」でCNo列がリストボックスで選択している取引先番号となります。
なお、ユーザーにしてみれば「削除を指定したのに、システム側が勝手にデータ変更」するのですから、ユーザーに対して745行目「MsgBox "データが他のテーブルで使用されている為、非表示扱いにします。"」で、理由と対応内容について伝えます。

そして748行目「Call SQL_exec5(sql)」で図6-16を呼び出し、上記で作成したSQL文を実行します。
テーブル内容が変更されたので、750行目「Call makeListBox」で図8-6を呼出し、リストボックスを更新します。

8-2-5-3.終了ボタン
「終了」ボタンをクリックした時には図8-13が呼び出され、762行目「Unload Me」でフォームが閉じられます。
  1. '========== ⇩(39) 終了ボタン ============
  2. Private Sub CommandButton3_Click()
  3.  Unload Me
  4. End Sub
図8-13

8-2-6.入力部のチェック

図8-10の686行目から呼び出される「入力部のチェック」をするのが図8-14です。入力ミスがある場合はTrueを、OKな場合はFalseを戻します。
  1. '========== ⇩(40) 入力部のチェック ============
  2. Private Function isMissingInput()
  3.  Me.TextBox2.Value = Trim(Me.TextBox2.Value)
  4.  If Me.TextBox1.Value = "" Or Me.TextBox2.Value = "" Then
  5.   isMissingInput = True
  6.   MsgBox "取引先No 又は 取引先名が空です"
  7.  End If
  8. End Function
図8-14

今回の場合、入力部は「取引先番号」「取引先名」「非表示有無」の3ヶ所で、必要な処理・確認は図8-15の様になります。
チェック項目大文字化両端スペース削除値の有無
取引先番号図8-7図8-7(774行目)
取引先名文字種自由(772行目)(774行目)
非表示有無(True or False)
図8-15

「取引先番号(TextBox1)」は、値が入力された時に図8-7のTextBox1_Changeが呼び出されるため、その中で「両端スペースの削除」「大文字化」が行われています。残りの「値が入力されているか否か」をここで確認する必要があります。
また「取引先名」は、ここまで何のチェックもしていないので、「両端スペースの削除」「値が入力されているか否か」を確認します。
また「非表示有無」はTrueかFalse(既定値)のどちらかしかありませんので、チェック対象外となります。

772行目「Me.TextBox2.Value = Trim(Me.TextBox2.Value)」では、入力した取引先名の両端のスペースを削除し、TextBox2に再記入します。
774行目「If Me.TextBox1.Value = "" Or Me.TextBox2.Value = "" Then」では、取引先番号(TextBox1)と取引先名(TextBox2)が空か否かを確認しています。

不足(実質的な文字列が入力されていない)があった場合には、775行目「isMissingInput = True」で、この関数プロシージャの戻り値をTrueにし、776行目「MsgBox "取引先No 又は 取引先名が空です"」で、どの部分でエラーが出ているかをコメントします。

9.商品ダイアログ(UserForm3)

9-1.コントロールの配置

商品ダイアログは、取引先ダイアログとほぼ同じです。異なる点は対象テーブルが異なることと入力項目数が多いこと位です。
商品データを操作するダイアログがUserForm3になり、そのフォーム上のコントロール類の配置は図9-1の様にしました。
商品ダイアログ
図9-1

操作を実行するCommandButtonをフォーム下部に3つ並べます。また中央部にデータ一覧を表示するListBox1を配置します。
ダイアログ上部に入力の為のTextBoxを3つと、CheckBoxを配置します。

9-2.フォームモジュール(UserForm3)

今回システムでは、リストボックス選択時に入力用テキストボックス等に値を転記しますが、その時にテキストボックス側のChangeイベントをスルーさせるためのフラグ変数を宣言します(図9-2の781行目)。
  1. '========== ⇩(41) 宣言部 ============
  2. Dim EventOff As Boolean
図9-2

9-2-1.起動時準備

フォーム起動時には、図9-3のInitializeイベントが発生します。ここではコントロールの静的な設定をします。
  1. '========== ⇩(42) フォーム起動時 ============
  2. Private Sub UserForm_Initialize()
  3.  Me.TextBox1.IMEMode = fmIMEModeOff
  4.  Me.TextBox3.IMEMode = fmIMEModeDisable
  5.  Me.ListBox1.ColumnCount = 4
  6.  Me.ListBox1.ColumnWidths = "50;80;30;15"
  7.  Me.ListBox1.TextAlign = fmTextAlignLeft
  8. End Sub
図9-3

792行目「Me.TextBox1.IMEMode = fmIMEModeOff」は、商品番号欄を編集する際に一旦IMEをOffにしています。全角を番号に使う事はほとんど無いと考えたためです。
793行目「Me.TextBox3.IMEMode = fmIMEModeDisable」は、単価欄は数値ですので半角のみになるようにIMEを切っています。

795行目「Me.ListBox1.ColumnCount = 4」は、商品一覧を表示するリストボックスを4列(商品番号、商品名、単価、非表示有無)の指定にしています。
796行目「Me.ListBox1.ColumnWidths = "50;80;30;15"」では、各列の列幅を設定しています。
797行目「Me.ListBox1.TextAlign = fmTextAlignLeft」では、リストの各列を「左寄せ」で表示させます。

ダイアログが表示された後に呼び出されるのが図9-4のActivateイベントです。
802行目「Call makeListBox」で図9-5を呼出し、商品一覧をリストボックスに表示させます。
  1. '========== ⇩(43) フォーム表示時 ============
  2. Private Sub UserForm_Activate()
  3.  Call makeListBox
  4. End Sub
図9-4

9-2-2.リストボックス作成

図9-4の802行目、及び図9-9の911行目、図9-10の950行目から呼び出される「リストボックス上に商品一覧を作成」するのが図9-5です。
  1. '========== ⇩(44) リストボックス作成 ============
  2. Private Sub makeListBox()
  3.  Dim sql As String   '←SQL文
  4.  Dim i As Long     '←カウンタ変数(行数)
  5.  Dim buf As Variant   '←商品データ配列
  6.  Me.ListBox1.Clear
  7.  sql = "SELECT PNo,Pname,Uprice,IIF(Del=True,'レ','') FROM " & TBL2 & _
  8.     " order by PNo"
  9.  buf = SQL_exec5(sql)
  10.  For i = 1 To UBound(buf, 1)
  11.   Me.ListBox1.AddItem ""
  12.   Me.ListBox1.List(i - 1, 0) = buf(i, 1)
  13.   Me.ListBox1.List(i - 1, 1) = buf(i, 2)
  14.   Me.ListBox1.List(i - 1, 2) = buf(i, 3)
  15.   Me.ListBox1.List(i - 1, 3) = buf(i, 4)
  16.  Next i
  17. End Sub
図9-5

816行目「Me.ListBox1.Clear」では、一旦リストボックスのデータをクリアします。
818~819行目「sql = "SELECT PNo,Pname,Uprice,IIF(Del=True,'レ','') FROM " & TBL2 & " order by PNo"」では、商品データをTBL2(Pdctテーブル)から取得するSQL文を作成します。取り出す列は全4列の「PNo」「Pname」「Uprice」「Del」ですが、Del列はBoolean型なので「True(非表示)の時はレ点、Falseの時は空文字」に変換しています。
また並び順は「order by PNo」と正逆を指定していない為「既定のAsc」を指定した事となり、商品番号の昇順で取り出します。

820行目「buf = SQL_exec5(sql)」で、図6-16を呼出してSQL文を実行し、戻り値として商品データの配列をbufで受け取ります。
822行目「For i = 1 To UBound(buf, 1)」では、受け取った配列の行数分だけカウンタ変数iを回します。
823行目「Me.ListBox1.AddItem ""」でリストボックスに新たな行を作成し、824行目「Me.ListBox1.List(i - 1, 0) = buf(i, 1)」でリストボックスの1列目(インデックス=0)に配列の1列目データを書き込みます。以下同様に2列目・3列目・4列目のデータをリストボックスの2列目・3列目・4列目に書き込みます。

9-2-3.入力部の処理

商品番号欄に入力を行った時には図9-6が呼び出されます。
  1. '========== ⇩(45) 商品番号の入力時 ============
  2. Private Sub TextBox1_Change()
  3.  If EventOff = True Then Exit Sub
  4.  TextBox1.Value = Trim(UCase(TextBox1.Value))
  5.  Me.ListBox1.ListIndex = -1
  6. End Sub
図9-6

842行目「If EventOff = True Then Exit Sub」では、フラグ変数EventOffがTrueの時には以下を実行せずに抜け出します。フラグ変数EventOffがTrueになるのは、ユーザーがリストボックスを選択した結果として、商品番号がTextBoxに転記される場合ですので、その時には何も処理をしないことになります。
844行目「TextBox1.Value = Trim(UCase(TextBox1.Value))」では、「大文字揃え(Ucase関数)」+「両端のスペースを削除(Trim関数)」の処理をしています。これは、ユーザーが手入力した時に、商品番号として「文字間にスペースを入れさせない」ためです。
845行目「Me.ListBox1.ListIndex = -1」では、リストボックスを非選択状態にします。最初から手入力をした時はもちろん、リストを選択した後で商品番号を変更するという行為は「新たな商品番号を入力しようとしている」ので、リストの選択状態を外しています。

単価の入力欄であるTextBox3内でキー入力した時に呼び出されるのが図9-7です。
  1. '========== ⇩(46) 単価の入力時 ============
  2. Private Sub TextBox3_KeyPress(ByVal KeyAscii As MSForms.ReturnInteger)
  3.  If Not Chr(KeyAscii) Like "[0-9]" Then KeyAscii = 0
  4. End Sub
図9-7

単価は「数値のみ」なので、852行目「If Not Chr(KeyAscii) Like "[0-9]" Then KeyAscii = 0」で数字以外を無視します。

なお、このTextBox3に「コピペでアルファベットや全角文字を貼り付ける」と、KeyPressイベントを通過しないために入力が出来てしまいます。そしてそのまま「登録/更新」をクリックされると、Uprice列はLong型なので「実行時エラーが発生」しマクロが停止してしまいます。
これを避けるには、TextBox3_Changeイベントで入力された値を解析し、数値のみにするなどの処理が必要になります。今回は性善説に立ってKeyPressイベントのみの設定としましたので御了承下さい。

9-2-4.リストボックスの選択

リストボックスの項目をユーザーが選択した場合には、図9-8が呼び出されます。
  1. '========== ⇩(47) リストボックスの選択 ============
  2. Private Sub ListBox1_Click()
  3.  EventOff = True
  4.   With Me.ListBox1
  5.    Me.TextBox1.Value = .List(.ListIndex, 0)
  6.    Me.TextBox2.Value = .List(.ListIndex, 1)
  7.    Me.TextBox3.Value = .List(.ListIndex, 2)
  8.    Me.CheckBox1.Value = IIf(.List(.ListIndex, 3) = "レ", True, False)
  9.   End With
  10.  EventOff = False
  11. End Sub
図9-8

863行目「With Me.ListBox1」では、以下のコードをListBox1ベースで実行していきます。
864行目「Me.TextBox1.Value = .List(.ListIndex, 0)」で、リストボックスの選択している行の1列目(インデックス=0)の値をTextBox1(商品番号)へ書き込みます。
865行目「Me.TextBox2.Value = .List(.ListIndex, 1)」で、2列目の値をTextBox2(商品名)へ書込み、
866行目「Me.TextBox3.Value = .List(.ListIndex, 2)」で、3列目の値をTextBox3(単価)へ書き込みます。
867行目「Me.CheckBox1.Value = IIf(.List(.ListIndex, 3) = "レ", True, False)」で、4列目の値を確認し、もしレ点が付いていればCheckBox1のValue値をTrueにします。逆にレ点が付いていない(=値が長さゼロの文字列)時はFalseにします。
このCheckBoxの値は、PdctテーブルのDel列の値と同じになります。

なお864行目を実行すると、TextBox1のChangeイベント(図9-6)が呼び出され、その中の845行目「Me.ListBox1.ListIndex = -1」でリストボックスが非選択状態になってしまいます。ですので862行目「EventOff = True」でフラグ変数EventOffをTrueにしています。

9-2-5.データ処理

9-2-5-1.登録/更新ボタン
「登録/更新」ボタンをクリックした時に呼び出されるのが、図9-9です。
  1. '========== ⇩(48) 登録/更新ボタン ============
  2. Private Sub CommandButton1_Click()
  3.  Dim sql As String    '←SQL文
  4.  Dim buf As Variant    '←Select文の戻り値の配列
  5.  Dim Ans As VbMsgBoxResult    '←MsgBoxのユーザーの回答
  6.  If isMissingInput Then Exit Sub
  7.  sql = "SELECT count(*) FROM " & TBL2 & _
  8.     " where PNo = '" & Me.TextBox1.Value & "'"
  9.  buf = SQL_exec5(sql)
  10.  If buf(1, 1) = 0 Then
  11.   sql = "Insert into " & TBL2 & " (Pno,Pname,Uprice,del)" & _
  12.      " values('" & Me.TextBox1.Value & "','" & _
  13.      Me.TextBox2.Value & "'," & _
  14.      Me.TextBox3.Value & "," & _
  15.      Me.CheckBox1.Value & ")"
  16.  Else
  17.   If vbNo = MsgBox("商品番号が既に存在します。上書きしますか?", vbYesNo) _
  18.        Then Exit Sub
  19.   sql = "Update " & TBL2 & _
  20.      " set Pname='" & Me.TextBox2.Value & "'," & _
  21.      " Uprice = " & Me.TextBox3.Value & "," & _
  22.      " del = " & Me.CheckBox1.Value & _
  23.      " where PNo= '" & Me.TextBox1.Value & "'"
  24.  End If
  25.  Call SQL_exec5(sql)
  26.  Call makeListBox
  27. End Sub
図9-9

886行目「If isMissingInput Then Exit Sub」は図9-12を呼出し、「入力部(TextBox1~3)に正しく入力されているか」を確認します。戻り値として「入力OK=False、NG=True」が返ってきますので、NGだった場合は「Exit Sub」で処理を中止します。

888~889行目「sql = "SELECT count(*) FROM " & TBL2 & " where PNo = '" & Me.TextBox1.Value & "'"」は、TextBox1に入力された商品番号が、TBL2(Pdctテーブル)内にいくつあるかを求めるSQL文です。
PdctテーブルのPNo列はPrimary Keyですので、その商品番号があったら「1」、無ければ「ゼロ」が得られるはずです。
890行目「buf = SQL_exec5(sql)」で、上記のSQL文を実行し、戻り値の配列を変数bufに代入します。

bufは配列で、今回のSQL文の取得する列は「count(*)」で、値は1レコード(1 または 0)ですので、求める値はbuf(1, 1)に入っています。
892行目「If buf(1, 1) = 0 Then」でその値を調べ、ゼロだった場合(同じ商品番号が無い)には893~897行目を実行し、1だった場合(同じ商品番号が有る)には899~906行目を実行します。

893~897行目は、同じ商品番号が無い場合にレコードを追加するInsert文です。
893行目「sql = "Insert into " & TBL2 & " (PNo,Pname,Uprice,del)" & _」では、TBL2(Pdctテーブル)のPNo列、Pname列、Uprice列、Del列の順番に値を追加します。値は、以下のvalues()のカッコ内に、指定した列の順番通りに指定していきます。
894行目「" values('" & Me.TextBox1.Value & "','" & _」は、PNo列にTextBox1(商品番号)の値を入れます。
895行目「Me.TextBox2.Value & "'," & _」は、Pname列にTextBox2(商品名)の値を入れます。
896行目「Me.TextBox3.Value & "'," & _」は、Uprice列にTextBox3(単価)の値を入れます。
897行目「Me.CheckBox1.Value & ")"」は、Del列にCheckBox(非表示)の値を入れます。

同じ商品番号が有る場合は、899~900行目「If vbNo = MsgBox("商品場号が既に存在します。上書きしますか?", vbYesNo) Then Exit Sub」で、上書きするか否かのメッセージボックスを表示し、その回答が「いいえ(上書きしない)」だった場合に、処理を中止しています。
「はい(上書きする)」を選択した場合は、902~906行目で「TextBox1の商品番号のレコード」を更新するUpdate文を作成します。
902行目「sql = "Update " & TBL2 & _」で、TBL2(Pdctテーブル)に対してデータの更新をします。
903行目「" set Pname='" & Me.TextBox2.Value & "'," & _」は、Pname列の値をTextBox2(商品名)の値に書換えます。
904行目「" Uprice = " & Me.TextBox3.Value & "," & _」は、Uprice列の値をTextBox3の値(単価)に書換えます。
905行目「"del = " & Me.CheckBox1.Value & _」は、Del列の値をCheckBox1(非表示)のValue値(True 又は False)に書換えます。
906行目「" where PNo= '" & Me.TextBox1.Value & "'"」は更新の対象が、PNo列がTextBox1(商品番号)の値のレコードに絞っています。PNo列はPrimary Keyですので、書き換えられるのは1レコードのみになります。

SQL文が完成しましたので、909行目「Call SQL_exec5(sql)」で図6-16を呼出し、SQL文を実行します。
テーブルを書き換えた後は、911行目「Call makeListBox」で図9-5を呼び出し、リストボックスを更新します。

9-2-5-2.削除ボタン
「削除」ボタンをクリックした時に呼び出されるのが図9-10です。
  1. '========== ⇩(49) 削除ボタン ============
  2. Private Sub CommandButton2_Click()
  3.  Dim sql As String     '←SQL文
  4.  Dim buf As Variant    '←Select文の戻り値の配列
  5.  Dim Ans As VbMsgBoxResult    '←MsgBoxのユーザーの回答
  6.  If Me.ListBox1.ListIndex = -1 Then
  7.   MsgBox "リストから削除する項目を選択して下さい。"
  8.   Exit Sub
  9.  End If
  10.  Ans = MsgBox("選択項目を削除して良いですか?", vbYesNo)
  11.  If Ans = vbNo Then Exit Sub
  12.  sql = "SELECT count(*) FROM " & TBL0 & _
  13.     " where PdctNo = '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"
  14.  buf = SQL_exec5(sql)
  15.  If buf(1, 1) = 0 Then    '←売上台帳に使用されていない場合
  16.   sql = "Delete from " & TBL2 & _
  17.      " where PNo= '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"
  18.  Else    '←売上台帳に使用されている場合
  19.   sql = "Update " & TBL2 & _
  20.      " set Del= true " & _
  21.      " where PNo= '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"
  22.   MsgBox "データが他のテーブルで使用されている為、非表示扱いにします。"
  23.  End If
  24.  Call SQL_exec5(sql)
  25.  Call makeListBox
  26. End Sub
図9-10

項目を削除するには、その削除対象が明確である必要であり、今回は「リスト上で選択した項目」を削除対象としています。
926~929行目では、その削除対象が選択されているかを確認しています。
926行目「If Me.ListBox1.ListIndex = -1 Then」でリストボックスが選択されていない(ListIndex = -1 )場合は、927行目「MsgBox "リストから削除する項目を選択して下さい。"」でメッセージを出し、928行目「Exit Sub」で処理を中止します。

リストボックスが選択されている場合は、931行目「Ans = MsgBox("選択項目を削除して良いですか?", vbYesNo)」でユーザーへ再確認を行い、「いいえ(=削除しない)」を選んだ場合は932行目「If Ans = vbNo Then Exit Sub」で処理を中止します。

削除OKの場合、まず「削除しようとしている項目が、他のテーブル(Saleテーブル:TBL0)で使われていないか」を調べる必要があります。無理に削除しようとすると実行時エラーが発生します。
934~935行目「sql = "SELECT count(*) FROM " & TBL0 & " where PdctNo = '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"」は、削除しようとしている商品番号が、TBL0(Saleテーブル)のPdctNo列の中に何レコード使われているかを取得するSQL文です。
SQL文は936行目「buf = SQL_exec5(sql)」で実行され、そのレコード数は変数bufに代入されます。

938行目「If buf(1, 1) = 0 Then」では「使用しているレコード数」を調べ、ゼロ(=使用されていない)の場合は「レコードを削除するSQL文(Delete文)」を作成します。それ以外(=使用されている)の場合は、削除する訳にはいかないので「選択リストには表示しない」状態にするため「Del列をTrueに変更するSQL文(Update文)」を作成します。

まずDelete文は、939行目「sql = "Delete from " & TBL2 & _」で TBL2(Pdctテーブル)に対して削除を行い、940行目「" where PNo= '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"」で、その対象をPNoがリスト選択している先頭列の商品番号に絞り込みます。PdctテーブルではPNoはPrimary Keyですので、削除される対象は1レコードとなります。

次にUpdate文は、942行目「sql = "Update " & TBL2 & _」で TBL2(Pdctテーブル)に対して変更を行い、943行目「" set Del= true " & _」でDel列をTrueに設定します。その変更の対象は944行目「" where PNo= '" & Me.ListBox1.List(Me.ListBox1.ListIndex, 0) & "'"」でPNo列がリストボックスで選択している商品番号となります。
なおユーザーにしてみれば「削除を指定したのに、システム側が勝手にデータ変更」するのですから、ユーザーに対して945行目「MsgBox "データが他のテーブルで使用されている為、非表示扱いにします。"」で、理由と対応内容について伝えます。

そして948行目「Call SQL_exec5(sql)」で、図6-16を呼び出し、上記で作成したSQL文を実行します。
テーブル内容が変更されたので、950行目「Call makeListBox」で図9-5を呼出し、リストボックスを更新します。

9-2-5-3.終了ボタン
「終了」ボタンをクリックした時には図9-11が呼び出され、962行目「Unload Me」でフォームが閉じられます。
  1. '========== ⇩(50) 終了ボタン ============
  2. Private Sub CommandButton3_Click()
  3.  Unload Me
  4. End Sub
図9-11

9-2-6.入力部のチェック

図9-9の886行目から呼び出される「入力部のチェック」をするのが図9-12です。入力ミスがある場合はTrueを、OKな場合はFalseを戻します。
  1. '========== ⇩(51) 入力部のチェック ============
  2. Private Function isMissingInput()
  3.  Me.TextBox2.Value = Trim(Me.TextBox2.Value)
  4.  If Me.TextBox1.Value = "" Or Me.TextBox2.Value = "" Or Me.TextBox3.Value = "" Then
  5.   isMissingInput = True
  6.   MsgBox "商品No、商品名、または単価が空です"
  7.  End If
  8. End Function
図9-12

972行目「Me.TextBox2.Value = Trim(Me.TextBox2.Value)」では、入力した商品名の両端のスペースを削除し、TextBox2に再記入します。
974行目「If Me.TextBox1.Value = "" Or Me.TextBox2.Value = "" Or Me.TextBox3.Value = "" Then」では、商品番号(TextBox1)、商品名(TextBox2)、単価(TextBox3)が空か否かを確認しています

不足(実質的な文字列が入力されていない)があった場合には、975行目「isMissingInput = True」で、この関数プロシージャの戻り値をTrueにし、976行目「MsgBox "商品先No、商品名、または単価が空です"」で、どの部分でエラーが出ているかをコメントします。

10.最後に

Deleteが使用できれば相当楽になりそうだ、というのがプログラムを作る前の予想だったのですが、テーブル同士は参照テーブル等で結びつけられているため「簡単にはDelete出来ない」というのを改めて感じました。メインのテーブル(今回ならばSaleテーブル)ならば素直にDelete出来ますが、「よりみち」でも書きましたが、やはりエラー種類によって処理を分岐することを考えないと複雑なデータベースに対応するのは難しいのでは、という印象です。
データベースについて、まだまだ知識・経験が不足しているのを痛感しています。


Accessデータベースを使用した売上台帳(it-082.xlsm)
Accessサンプルデータ(it-082.accdb)

このit-082.xlsmを開くと、ファイルを保存した場所にit-082.accdbというデータベースファイルが自動的に作成されます。
また添付のit-082.accdbを使用すると、説明文の中に出てくるサンプルデータが使用できます。

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