メニューより 「表示 > ツールバー > デバッグ」にチェックを入れる。
VBA のコードを実行する前にブレークポイントを設置。実行して停止したところで、ツールバーの「呼び出し履歴」のアイコンをクリック。
ショートカットキーは、Ctrl + L 。表示メニューにある。
書いて忘れて頭スッキリ
メニューより 「表示 > ツールバー > デバッグ」にチェックを入れる。
VBA のコードを実行する前にブレークポイントを設置。実行して停止したところで、ツールバーの「呼び出し履歴」のアイコンをクリック。
ショートカットキーは、Ctrl + L 。表示メニューにある。
例えば、家計簿のようなものを作ろうと思い、以下のようなリレーションシップを持ったテーブルを作成した。ここでは、使った金額の 「支払い」 において、誰がどこへ支払い、その明細を管理したいもとする。 (抜粋)
「支払」 テーブルの 「支払先ID」 フィールドは 「組織」 テーブルの ID を外部キーとして持ち、「支払人ID」 は 「支払人」 テーブルの ID を外部キーとして持ち、参照整合性を設定。
次に、「支払」 テーブルを元にフォームを作成し、動作の確認をする。
ここで、「支払先」 または 「支払人」 に値を入力をした後、フォームのデザインに戻りたいとする。もし、「支払先」 と 「支払人」 フィールドに参照整合性制約に抵触しない値を入力した場合、その値がデータベースに保存された後、フォームデザインの画面に切り替わる。しかし、参照整合性制約に抵触した値を入力した場合、フォームデザインに戻ろうと思っても、
バリアント型でない変数に Null 値を代入しようとしました。
とエラーダイアログが表示される。入力を取消したいと思い、入力した値を BackSpace キー消しても同じダイアログが表示。
メニュー下の「レコードの削除」ボタンを押しても、全てのフィールドに正当な値が入力されてないとダメ。仕方がないので、とりあえず正当な値を入力しておき、その後レコードの削除するということで対応できる。しかし、これでは面倒。 (+_+)
[ACC97]: Access データベース内の関連テーブル内のレコードを編集します。 によると、
注: 入力ミスを修正、BackSpace キーを押します。 現在のフィールドに加えた変更をキャンセルするには、Esc キーを押します。 レコード全体に加えた変更をキャンセル、Esc キーを押します
(太字は引用者による)
あ~、Esc キーだったのか。基本的な操作すらわかってないな。。 ^^;
「VBA で文字列の配列の初期化」のついでに配列の走査。変数 strAry が文字列の配列だとすると、
Dim idx As Integer For idx = LBound(strAry) To UBound(strAry) Debug.Print strAry(idx) Next
LBound, UBound 関数が記憶の彼方にあって全く思い出せなかった。 (o_ _)o~†
For Each を使う場合は、
For Each elem In strAry Debug.Print elem Next
もし、上記に先立ち、
Dim elem As String
と宣言すると、
For Each の場合、要素を指す変数はバリアント型でないといけなかったかぁ。。 (@_@;)
ヘルプ > Microsoft Visual Basic Documentation > Visual Basic プログラミングのヒント の「For Each...Next ステートメントの使い方」によると、
For Each...Next ステートメントは、コレクションの各オブジェクトまたは配列の各要素に対して、一連のステートメント ブロックを繰り返し実行します。
また、「For Each...Next ステートメント」 には、要素が代入される変数について、
コレクションや配列の各要素を繰り返す変数を指定します。コレクションの場合、引数 element にはバリアント型 (Variant) 変数、総称オブジェクト型変数、または任意の固有オブジェクト型のオブジェクトの変数を指定できます。また、配列の場合は、引数 element にはバリアント型のみ指定できます。
For Each … Next と言えば、オブジェクトのコレクションを走査するときによく使った。例えば、プロパティに「名前」を持つ Person クラスを定義し、
Dim mstrName As String Public Property Let Name(strName As String) mstrName = strName End Property Public Property Get Name() As String Name = mstrName End Property
このクラスのオブジェクトをコレクションに追加し、For Each … Next で走査するには、
Dim col As New Collection Dim p1 As New person p1.Name = "Tarou" Dim p2 As New person p2.Name = "Jirou" col.Add p1 col.Add p2 Dim person As person For Each person In col Debug.Print person.Name Next
ところで、バリアント型はあまり使ってなかったので、For Each で使えることを知らなかった。上記でも For Each で使っている要素を代入する変数 person の型宣言をせずにバリアント型としても使用できる。
ヘルプの「バリアント型 (Variant)」を見ると、
バリアント型は特殊なデータ型で、固定長の文字列型 (String) データとユーザー定義型を除く、あらゆる種類のデータを格納することができます。
(太字は引用者による)
バリアント型というと、文脈に応じて適用する演算子の意味が変化し、メモリを結構使うということしか頭に残っていない。 ^^;
バリアント型には、特殊な値 Empty 値、Null 値、およびエラー値や Nothing なども格納することができます。
(同上より)
あぁ~、なんかややこしいなぁ。。 (+_+)
Empty 値 とはヘルプによると、
その変数が初期化されていないことを示し
Null 値とは、
その変数に有効なデータが格納されていないことを示します。
確認しておく。
Dim hoge As Variant Debug.Print IsNull(hoge) ' False Debug.Print IsEmpty(hoge) ' True hoge = Null Debug.Print IsNull(hoge) ' True Debug.Print IsEmpty(hoge) ' False hoge = 0 Debug.Print IsEmpty(hoge) ' False Set hoge = Nothing Debug.Print IsNull(hoge) ' False Debug.Print IsEmpty(hoge) ' False
VBA で文字列の配列を初期化しようと思い、次のコードを実行した。
dim strAry as String = {"hoge", "piyo", "fuga"}
しかし、エラーが表示された。(@_@;) VB6 ってこういうのできなかったかな?
VB 配列の初期化 によると、
VB6では配列を初期化することはできません。要素ごとに値をセットする必要があります。ただし、固定長の配列は既定値で初期化されています。
ちなみに .NET では上記のように書けるようだ。
仕方がないので、
Dim strAry(2) As String strAry(0) = "hoge" strAry(1) = "piyo" strAry(2) = "fuga"
しかし、これは面倒だなぁ。。 (+_+)
VB 文字列操作 に split 関数を使う方法が書かれていた。これを参考にして、
Dim strAry() As String strAry = Split("hoge,piyo,fuga", ",")
こちらの方が楽。 ^^
Access が分かりにくい理由は、フォームがどういう仕組みの上で動いているかをちゃんと理解せず、場当たりに作ってしまうことにある。フォームに配置できる部品のプロパティを把握しておくことは重要。
フォームのデザインにおいて、
色々なイベント処理に対応していることがわかる。
しかし、その数があまりにも多く、どういうタイミングで何が呼出されているか理解していないと、簡単にイベント処理の森に迷いこんでしまう。 (@_@;)
イベントの意味、発生する順序について、以下の説明がわかりやすかった。
どういうタイミングでイベントが呼出されているのか知りたい。
イベント処理に対して、横からキャッチできないか調べてけれど、分からなかった… (+_+)
仕方がないので、フォームのイベント全部に Debug.Print 書いて、イミディエイト ウィンドウに出力させた。
Private Sub Form_Activate() Debug.Print "Activate" End Sub Private Sub Form_AfterDelConfirm(Status As Integer) Debug.Print "AfterDelConfirm" End Sub Private Sub Form_AfterFinalRender(ByVal drawObject As Object) Debug.Print "AfterFinalRender" End Sub Private Sub Form_AfterInsert() Debug.Print "AfterInsert" End Sub Private Sub Form_AfterLayout(ByVal drawObject As Object) Debug.Print "AfterLayout" End Sub Private Sub Form_AfterRender(ByVal drawObject As Object, ByVal chartObject As Object) Debug.Print "AfterRender" End Sub Private Sub Form_AfterUpdate() Debug.Print "AfterUpdate" End Sub Private Sub Form_ApplyFilter(Cancel As Integer, ApplyType As Integer) Debug.Print "ApplyFilter" End Sub Private Sub Form_BeforeDelConfirm(Cancel As Integer, Response As Integer) Debug.Print "BeforeDelConfirm" End Sub Private Sub Form_BeforeInsert(Cancel As Integer) Debug.Print "BeforeInsert" End Sub Private Sub Form_BeforeQuery() Debug.Print "BeforeQuery" End Sub Private Sub Form_BeforeRender(ByVal drawObject As Object, ByVal chartObject As Object, ByVal Cancel As Object) Debug.Print "BeforeRender" End Sub Private Sub Form_BeforeScreenTip(ByVal ScreenTipText As Object, ByVal SourceObject As Object) Debug.Print "BeforeScreenTip" End Sub Private Sub Form_BeforeUpdate(Cancel As Integer) Debug.Print "BeforeUpdate" End Sub Private Sub Form_Click() Debug.Print "Click" End Sub Private Sub Form_Close() Debug.Print "Close" End Sub Private Sub Form_CommandBeforeExecute(ByVal Command As Variant, ByVal Cancel As Object) Debug.Print "CommandBeforeExecute" End Sub Private Sub Form_CommandChecked(ByVal Command As Variant, ByVal Checked As Object) Debug.Print "CommandChecked" End Sub Private Sub Form_CommandEnabled(ByVal Command As Variant, ByVal Enabled As Object) Debug.Print "CommandEnabled" End Sub Private Sub Form_CommandExecute(ByVal Command As Variant) Debug.Print "CommandExecute" End Sub Private Sub Form_Current() Debug.Print "Current" End Sub Private Sub Form_DataChange(ByVal Reason As Long) Debug.Print "DataChange" End Sub Private Sub Form_DataSetChange() Debug.Print "DataSetChange" End Sub Private Sub Form_DblClick(Cancel As Integer) Debug.Print "DblClick" End Sub Private Sub Form_Deactivate() Debug.Print "Deactivate" End Sub Private Sub Form_Delete(Cancel As Integer) Debug.Print "Delete" End Sub Private Sub Form_Dirty(Cancel As Integer) Debug.Print "Dirty" End Sub Private Sub Form_Error(DataErr As Integer, Response As Integer) Debug.Print "Error" End Sub Private Sub Form_Filter(Cancel As Integer, FilterType As Integer) Debug.Print "Filter" End Sub Private Sub Form_GotFocus() Debug.Print "GotFocus" End Sub Private Sub Form_KeyDown(KeyCode As Integer, Shift As Integer) Debug.Print "KeyDown" End Sub Private Sub Form_KeyPress(KeyAscii As Integer) Debug.Print "KeyPress" End Sub Private Sub Form_KeyUp(KeyCode As Integer, Shift As Integer) Debug.Print "KeyUp" End Sub Private Sub Form_Load() Debug.Print "Load" End Sub Private Sub Form_LostFocus() Debug.Print "LostFocus" End Sub Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single) Debug.Print "MouseDown" End Sub Private Sub Form_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single) Debug.Print "MouseMove" End Sub Private Sub Form_MouseUp(Button As Integer, Shift As Integer, X As Single, Y As Single) Debug.Print "MouseUp" End Sub Private Sub Form_MouseWheel(ByVal Page As Boolean, ByVal Count As Long) Debug.Print "MouseWheel" End Sub Private Sub Form_OnConnect() Debug.Print "OnConnect" End Sub Private Sub Form_OnDisconnect() Debug.Print "OnDisconnect" End Sub Private Sub Form_Open(Cancel As Integer) Debug.Print "Open" End Sub Private Sub Form_PivotTableChange(ByVal Reason As Long) Debug.Print "PivotTableChange" End Sub Private Sub Form_Query() Debug.Print "Query" End Sub Private Sub Form_Resize() Debug.Print "Resize" End Sub Private Sub Form_SelectionChange() Debug.Print "SelectionChange" End Sub Private Sub Form_Timer() Debug.Print "Timer" End Sub Private Sub Form_Undo(Cancel As Integer) Debug.Print "Undo" End Sub Private Sub Form_Unload(Cancel As Integer) Debug.Print "Unload" End Sub Private Sub Form_ViewChange(ByVal Reason As Long) Debug.Print "ViewChange" End Sub
あぁ~面倒だった。パタッ(o_ _)o~†
以下の処理をしたときに、どのようなイベントが発生するか試してみた。
イミディエイト ウィンドウに、以下のように表示された。
Open Load Resize Activate Current Dirty BeforeUpdate AfterUpdate Unload Deactivate Close
close イベントの前に Before/AfterUpdate イベントが処理されている。Close イベントのときに、Me.Dirty が True でなかったのはこれが理由だったのか。
例えば、フォームにレコード を削除するためのボタンがあり、クリックされたときの処理を次のように記述したとする。
Private Sub btnDelete_Click() DoCmd.RunCommand acCmdDeleteRecord End Sub
エラー処理を記述してないので、削除ボタンを押したときに「いいえ」を選択すると、
実行時エラーのエラーのダイアログが表示される。
エラーをキャッチして、エラー番号とエラー内容を表示するように変更するなら、
Private Sub btnDelete_Click() On Error GoTo Err_Handler DoCmd.RunCommand acCmdDeleteRecord Exit Sub Err_Handler: MsgBox Err.Number & " : " & Err.Description End Sub
正常に処理されたとき、エラー処理に突入しないように Exit Sub の記述を忘れずに。
先ほどと同じ操作をすると、同じ内容がメッセージボックスに表示される。
ところで、ユーザに上記のメッセージを見せる必要はないので、エラーの内容によってエラー処理を変えたい。この場合、先ほどのエラー番号で処理を分ける。
Private Sub btnDelete_Click() On Error GoTo Err_Hander DoCmd.RunCommand acCmdDeleteRecord Exit Sub Err_Handler: If Err.Number = 2501 Then Exit Sub Else MsgBox Err.Number & "/" & Err.Description End If End Sub
Java ばかりいじっていたとき、エラー番号でトラップするのを見たときびびったけど ^^;
ちなみに Chapter 09 例外処理 - @IT によると、
VB.NETでは、On Error Gotoは「非構造化例外処理」という名前で呼ばれる。
もし、次のような要件がある場合は、
Private Sub btnDelete_Click() On Error GoTo Err_Handler DoCmd.RunCommand acCmdDeleteRecord Finally: ' 後処理 Exit Sub Err_Handler: If Err.Number = 2501 Then ' このエラー独自の処理 Else MsgBox Err.Number & "/" & Err.Description End If Resume Finally End Sub
(cf. Access の VBA でトランザクション処理 – CSV ファイルからデータの登録)
処理がシンプルなときは Resume Next でもいいけれど、バグを誘発しそうなので Resume ラベル名 を使うことにしようかな。
例えば、「いつも朝の 6:30 から 6:40 までラジオ体操をする」という事柄を表現したいとする。「いつも」とあるように、この表現は特定の日付と関連したものではない。過去のある期間においてそういう事実があったことを記録したいわけでもなく、未来の予定について述べているわけでもない。「日常」という言葉通り、典型的な「生活パターン」というような抽象的なレベルの事象をどのように表現して扱うかという問題。
さて、Access でデータを管理しようと思うと、上記の「時刻」をどのデータ型で扱えばいいのだろうか?最初に思い付くが「日付/時刻型」。(cf. Access で使用できるフィールドのデータ型 (MDB) - Access - Microsoft Office Online)
「日常生活」と命名したテーブルを以下のように定義し、
データを入力したとする。
これに対して、次のように「年月日」を表示させるクエリを実行すると、
SELECT 日常生活.id, 日常生活.内容, Format([開始時刻],"yyyy/mm/dd hh:nn") AS 式1, Format([終了時刻],"yyyy/mm/dd hh:nn") AS 式2 FROM 日常生活;
時刻の前に `1899/12/30’ が表示される。これでは表現したいデータを素直に表現しているとは言い難い。 (+_+)
ちなみにこの日付となる理由は、「フィールドのデータ型の設定を修正または変更する - Access - Microsoft Office Online」によると、
Access では、1899 年 12 月 30 日が日付 0 として使用されます。
プログラマのためのSQL 第2版 (P64) によると、
時間に関する重要な問題の 1 つに、時間には 3 種あるというのがあります。固定された出来事「彼は、13:00 に到着した」、期間「その旅は 3 時間かかる」、時間間隔「その列車は 10:00 に出発して 13:00 に到着する」は、それぞれ依存関係を持っています。 SQL-92 は、 INTERVAL データ型を導入していますが、ほとんどの現実の実装 (…) では、明白に持っていません。
(太文字は引用者による)
SQL – Wikipedia を見ると、SQL-92 で INTERVAL が追加されているのがわかる。
データ型の拡張 (可変長文字列、ビット、文字集合、日付・時刻・時間間隔 (DATE, TIME, TIMESTAMP, INTERVAL))
どうやらこの「時間間隔」というのが考える手がかりになりそうだ。
ちなみに Access と SQL のデータ型を比較した表を見ると、Access では INTERVAL 型を直接表現した型がないことがわかる。
ところで、Access ではユーザ定義型を作成することができない。(型とその型に適用できる演算子の定義がまともにできる RDB ってどれくらいあるのだろう?) よって、上記の要求を満たすドメインの値を素直に保持することはできない。そのため、結果として機能し得る型で代用することになる。
MySQL :: MySQL 5.1 Reference Manual :: 10.3.2 The TIME Type によると、
MySQL retrieves and displays
TIME
values in'HH:MM:SS'
format (or'HHH:MM:SS'
format for large hours values).TIME
values may range from'-838:59:59'
to'838:59:59'
. The hours part may be so large because theTIME
type can be used not only to represent a time of day (which must be less than 24 hours), but also elapsed time or a time interval between two events (which may be much greater than 24 hours, or even negative).
MySQL 5.1 では「時間間隔」と「一日の時刻」を同じ Time 型で表わすようだ。というより、「一日の時刻」を「時間間隔」で代用しているということかな。
Postgres は、「時間間隔」 (Interval) と「その日の時刻」(time) が分かれている。
ところで、以前に Python で指定した日数後の日付を得るのに timedelta オブジェクト を使ったことがある。timedelta 周りを見ると time オブジェクト があるのに気がついた。(@_@)
time クラスは、
理想化された時刻表現で、あらゆる特定の日における影響から独立しており、毎日厳密に 24*60*60 秒であると仮定します ("うるう秒: leap seconds" の概念はありません)。 属性: hour、 minute、second、 microsecond、 および tzinfo。
(5.1.1 利用可能なデータ型 より)
まさにこれ。ただし、
time オブジェクト間の四則演算はサポートされていないので注意してください。
(time オブジェクト より)
「年月日」がなく「時・分・秒」だけので、上記の例を表現するには概念的にちょうどいい。
VB にも、似たような見かけの TimeSerial 関数 があったが、
Debug.Print TimeSerial(6, 30, 0) Debug.Print Format(TimeSerial(6, 30, 0), "yyyy/mm/dd hh:nn")
実行すると、
6:30:00 1899/12/30 06:30
あ~、内部では日付型かぁ…
@IT:連載:プロフェッショナルVB.NETプログラミング 第5回 日付時刻の取得とフォーマット によると、
日付と時刻の扱いは、Microsoft系BASICの歴史に限ってもすでに激変が起きている。初期のBASICでは、日付と時刻はそれぞれ文字列として扱われており、日付と時刻は別個の情報として扱われていた。(…)
Timeの場合日付は1899年12月30日となっているが、これはVB 6の日付を扱う起点となる日付である。(…)
VB.NETでは日付時刻の起点は0001/01/01 12:00:00である。
うーむ… (@_@;)
では、Haskell ではどうかな?と思い調べてみると、Time of day に、
Time of day as represented in hour, minute and second (with picoseconds), typically used to express local time of day.
「時・分・秒」のみを値に持つ型がある。
先ほどの Python の Time オブジェクトと同様に、TimeOfDay 型は Num クラスのインスタンスではないので四則演算はできない。ただし、DiffTime 型という「時間間隔」を表わす型を利用すると計算を行うことができる。
最初に timeOfDayToTime 関数で TimeOfDay 型を DiffTime 型に変換。この関数は午前 0 時からの時間間隔を返してくれる。
timeOfDayToTime :: TimeOfDay -> DiffTime
DiffTime 型は Num クラスのインスタンスなので、この型の値を足したり引いたりできる。それを上記の関数とは逆の timeToTimeOfDay で元の型に戻す。例えば、
import Data.Time main = do let s = TimeOfDay 6 30 0 print s let ds = timeOfDayToTime s print ds print $ timeToTimeOfDay $ ds + (secondsToDiffTime (10*60))
結果は、
06:30:00 23400s 06:40:00
さて、話を元に戻して Access で時刻を扱うにはどうしよう。
一つは、年月日の情報は無視して、つまり ‘1899/12/30’ はないものとして時刻だけを「日付/時刻型」で扱う。とりあえず楽な方法。
自前でどうにかしたいなら、「時・分・秒」をそれぞれ別のフィールドで数値として扱い、それを一塊のものとして自分で思い込み、「時・分・秒」間での計算が必要なら関数を定義。ちょっと面倒だし、それに見合ういいことがあるのかわからない。
また別の方法として、NULL撲滅委員会 にあるように、
日付の列は文字型で宣言しておくべきです。(…) ほとんどの DBMS が日付型を用意していますが、私は個人的に使うメリットを感じません。暦日計算の場合など、必要なときだけキャストしています。
時刻を文字列として保持し、必要なときだけキャストする。ただし、年月日の情報は無視して日付型として計算を行う。この場合、最低でも文字列として時刻を保存するとき、時刻へとキャスト可能な形式であるか確認する必要となる。
トランザクションと言えば、やはりこの本。
未だ買ってすらいない。 パタッ(o_ _)o~†
昔からデータベースのこの辺がチンプンカンプン。入門レベルを読んでもトランザクション、排他制御になるとついていけない。いつかはちゃんと頭の中を整理したいのだけれど… (+_+)
トランザクションの大枠については以下の図を参照。
異常なければコミット、異常があればロールバック。これだけ聞くとシンプルなのだけれど…。とりあえず、クライアントアプリがトランザクションを利用するときのテンプレートを書いておくことに。参考にしたのは以下の二つの記事。
Access の ADO については、「ActiveX Data Object : ADO入門講座」を。
以下、ADO でテーブルにレコードを追加するときのテンプレート。Java のように try ~ catch がないのでエラーハンドラと Resume を利用。
ポイントは、トランザクションが開始された後に異常が置きたらロールバック。正常異常を問わず、最後にデータベースへのコネクションを閉じる。ただし、その際コネクションが存在していることを確認すること。
Sub tmplADOTran() On Error GoTo Err_Handler Dim cn As ADODB.Connection Dim rs As ADODB.Recordset Dim blnTran As Boolean blnTran = False Set cn = CurrentProject.Connection Set rs = New ADODB.Recordset ' トランザクションの開始 cn.BeginTrans ' blnTran = True rs.Open "テーブル名", cn, adOpenKeyset, adLockOptimistic ' データを列ごとに追加する rs.addnew rs!列名 = "値" rs.Update ' コミット cn.CommitTrans blnTran = False Finally: If Not rs Is Nothing Then rs.Close If Not rs Is Nothing Then cn.Close Exit Sub Err_Handler: ' ロールバック If blnTran Then cn.RollbackTrans MsgBox "エラーが発生しました: " & Err.Description, vbOKOnly Resume Finally End Sub
あぁ~、何かややこしいなぁ。 (+_+)
上記のテンプレートを元に、CSV ファイル (test.csv) のデータをテーブルに登録するコードを書いてみる。ただし、対象のテーブルは一つ。追加する先のテーブルの列は文字列型とする。
まず、データを追加するテーブルの名称と列の名前を保持する Table クラスを作成。(挿入 > クラスモジュール) 値を保持するだけなのでインスタンス変数は Public に。
Public name As String ' テーブルの名称 Public cols As New Collection ' 列の名前のコレクション
上記のテーブル情報を持ったオブジェクトと、データ元となるファイルへのパスを渡すとレコードを追加する関数は以下のようになる。
Sub insert(objTbl As table, strFilePath As String) On Error GoTo Err_Handler Dim cn As ADODB.Connection Dim rs As ADODB.Recordset Dim blnTran As Boolean Dim fileNo As Integer Dim idx As Integer blnTran = False Set cn = CurrentProject.Connection Set rs = New ADODB.Recordset ' トランザクションの開始 cn.BeginTrans ' blnTran = True rs.Open objTbl.name, cn, adOpenKeyset, adLockOptimistic ' CSV ファイルからデータを一行ずつ読み込む fileNo = FreeFile() Open strFilePath For Input As #fileNo Do Until EOF(fileNo) Line Input #fileNo, strLine strAry = Split(strLine, ",") ' データを列ごとに追加する rs.addnew idx = 0 For Each col In objTbl.cols rs(col) = strAry(idx) idx = idx + 1 Next rs.Update Loop Close #intFileNo ' コミット cn.CommitTrans blnTran = False Finally: If Not rs Is Nothing Then rs.Close If Not rs Is Nothing Then cn.Close Exit Sub Err_Handler: ' ロールバック If blnTran Then cn.RollbackTrans MsgBox "エラーが発生しました: " & Err.Description, vbOKOnly Resume Finally End Sub
上記を例えば次のように呼ぶ。
Sub test() Dim objTbl As New table Dim filePath As String ' CSV ファイルのパス filePath = Application.CurrentProject.Path & "\test.csv" objTbl.name = "テーブル名" ' データを追加するテーブルの名前 objTbl.cols.Add "列名" ' データを追加するテーブルの列の名前 insert objTbl, filePath End Sub
以下のような、「人」テーブルがあるとする。
各々の「人」に対して、次のことを知りたい。
生年月日から年齢を求めるには、
「今年の誕生日が来ているかどうか?」
がわかればよい。
そのため、
関数の定義は、
において、新規モジュールを作成する。
日付の関数については
を参考にした。
' ある誕生日の人が、指定した日付に何歳になっているか返す。 Function Age(birthday As Date, theDate As Date) As Integer If AfterThisYearsBirthday(birthday, theDate) Then Age = Year(theDate) - Year(birthday) Else Age = Year(theDate) - Year(birthday) - 1 End If End Function ' 指定された日付に今年の誕生日が来ているか? Function AfterThisYearsBirthday(birthday As Date, theDate As Date) As Boolean If Month(birthday) < Month(theDate) Or _ (Month(birthday) = Month(theDate) And Day(birthday) <= Day(theDate)) Then AfterThisYearsBirthday = True Else AfterThisYearsBirthday = False End If End Function
次に、年度末に何歳になっているか知りたい。
上記と同様に「指定された日付の年度末を返す」関数を書く。
' 指定された日付の年度末の日付を返す Function Nendomatu(theDate As Date) As Date Const DATE_331 = "/3/31" If 4 <= Month(theDate) And Month(theDate) <= 12 Then Nendomatu = CDate(Year(theDate) + 1 & DATE_331) Else Nendomatu = CDate(Year(theDate) & DATE_331) End If End Function
上記の関数を、クエリから呼出すようにする。
左下を見ると、「関数」フォルダに、先ほど作成したモジュール名が表示され、定義した関数が属しているのがわかる。
それぞれ以下のように定義する。
年齢: Age([生年月日],Now())
年度末の年齢: Age([生年月日],Nendomatu(Now()))
クエリの結果。今日の日付は 2009.4.13 。
追記(2009.4.25) : 学校の学年においては、 4/1 生まれは早生まれになるので、年度末の翌日における年齢を求める。
年度末の年齢: Age([生年月日],nendomatu(Now())+1)
(参考: 法制執務コラム集「4月1日生まれの子どもは早生まれ?」)
「生年月日から年齢を計算する方法」には、次のようなクエリが書かれていた
年齢: IIf(Right(Format([誕生日],"yyyy/mm/dd"),5)>Right(Format(Now(), "yyyy/mm/dd"),5),DateDiff("yyyy",[誕生日],Now())-1,DateDiff("yyyy", [誕生日],Now()))
(via 生年月日の入力によって年齢を出したい。 --Access Club 超初心者 FORUM—)
うへぇ~、一行だと読みにくい… (+_+)
そもそも、これは何だろう?
Right(Format([誕生日],"yyyy/mm/dd"),5)
関数をテストしてみる。
Debug.Print CDate("1/1") ‘ 2009/01/01 Debug.Print CDate("1") ‘ 1899/12/31 Debug.Print CDate("366") ‘ 1900/12/31 Debug.Print CDate("0") ‘ 0:00:00
CDate のヘルプを見ると、
CDate 日付型 (Date) 任意の有効な日付式
「日付式」とは、
日付として解釈可能な式。… 日付と解釈できる文字列、
結果を見ると、そこまで無理に解釈しなくても…と言いたくなるような仕様。 ^^;
日付の内、「年」を省略すると、「現在の年」と解釈されるようだ。
「生年月日から年齢を計算する簡単な計算式:ITpro」には、次のような計算式が書かれている。
(今日の日付-誕生日)/10000の小数点以下切捨て。
なるほど、「年月日」をわざと一連の数値として扱う方法のようだ。確かに、年月だけ見れば、年月を各々二桁で表現しているなら、誕生日が来ていれば、今日の日付よりも誕生日の数値が小さくなる。
先ほどの方法でパフォーマンスが悪ければ、こちらに変更しようかな。
ただし、次の注意点が述べられている。
「誕生日の前日が終了する瞬間(すなわち誕生日をむかえる午前0時00分の直前)に1歳を加えることになる」の部分の解釈には前日に1日加えると解釈される場合と当日に1日加えると解釈される場合の2種類があるようです。特に自治体においては前者で解釈される場合が多いとのことです。
(同上より)
ん?これは自治体の特殊ルールで、「年齢」と「年齢をどう解釈するか」とは別の話だよねぇ。
データベースを利用するユーザに、余計なメニューを見せたり、データベース ウィンドウを見せたくない。そのために、メニューより
において、チェックをいくつか外した。
その結果、データベースを起動すると、以下のようにすっきりとしたメニューになる。
上記の設定をした後、カスタマイズしたくなったとき、データベース ウィンドウをどうやって表示するのだろう ?
「起動時の設定を初期状態に戻すには」によると、
「起動時の設定」 を初期状態に戻したいファイルを、[Shift] キーを押しながら開きます
エクスプローラにおいて、初期状態に戻したいデータベースを開き、セキュリティ警告のダイアログが表示されたら、シフトキーを押しながら「開く」ボタンを押せばよい。
Access では、入力フォームとテーブルを、別々のデータベースに分割することができる。
Microsoft Access – Wikipedia によると、
1つの設計技術はAccessのアプリケーションをデータとプログラムに分割することである。1つのデータベースはテーブルとリレーションシップのみを含むべきであり、一方他のデータベースはすべてのプログラム、フォーム、レポート、及びクエリを含み、最初のデータベースのテーブルにリンクする。
データベースの分割を行うには、メニューより
これにより、データを含むバックエンドのデータベースを自動で作成してくれる。
これに対して、
には、手作業でデータベースを分割する方法が説明されている。
簡単に方法をまとめると、
データベースを分割するメリットは、次のように述べられている。
データベースを分割する最も一般的な理由は、ネットワーク上で複数のユーザーがデータベースを共有していることです。ネットワーク共有に単純にデータベースを保存している場合は、ユーザーがフォーム、クエリ、マクロ、モジュール、またはレポートを開くと、これらのオブジェクトがネットワークを経由して、データベースを使用する各ユーザーに送信されます。データベースを分割すると、各ユーザーに、フォーム、クエリ、マクロ、モジュール、およびレポートの自分用のコピーを与えられます。このため、ネットワークを経由して送信する必要があるデータは、テーブルのデータのみとなります。
(Access 2002 または Access 2003 で Microsoft Access データベースを手作業で分割する方法 より)
バックエンドデータベースに一度リンクした後、フォームを別のデータベースに接続したい場合は、メニューより
において、「リンク先を更新するためのプロンプトを毎回表示する」にチェックを入れ、更新が必要なリンクを選択して OK ボタンを押す。
「人」テーブルと「都道府県」テーブルがあるとする。
「人」テーブルは「都道府県」を参照。 id は主キーで、CD は候補キー。 id はシステムがタプルを一意に識別するためのもので、CD も一意に識別することができるが人間の都合で変更の可能性がある。
「都道府県」を参照しているフィールドは、「ルックアップ」で「コンボボックス」が表示されるようにする。
上記のテーブルから下記のクエリを作成。
これを元にフォームを作る。
このとき、フォームにおける「都道府県」の入力は CD (コード) でしようと考えていたので、「あれ? id を入力しなくていけないかな?どうしよう… (@_@;) 」と思ったけれど、上記の通り「人」テーブルの「都道府県id」フィールドには「ルックアップ」が設定されているので、id の入力をあたかも CD を入力してるかのようにフォームのコンボボックスで入力できる。
さて、ここで「都道府県」テーブルにない CD を入力しようとすると、
指定した項目はリストにありません。
とメッセージが表示される。
このメッセージではユーザにとってわかりにくいので変更したい。
コンボボックスのプロパティより「イベント > リスト外入力時」の VBA を記述する。
Private Sub 都道府県id_NotInList(NewData As String, Response As Integer) Response = acDataErrContinue MsgBox "存在しない都道府県コードです" End Sub
引数 Response の設定で先ほどのメッセージを抑制して、新たにメッセージを表示する。
試したい関数があるとき、
という方法は手間がかかる。
データベースウィンドウで右クリック > Visual Basic Editor を選択。
「表示 > イミディエイトウィンドゥ」 を選択しておく。
「標準モジュール」の中でプロシージャを作成し、
これにより、イミディエイトウィンドウに結果が表示される。
関数の場合は「表示 > ローカルウィンドウ」 を表示しておくと良い。
以下のように、前回と同じテーブルを使う。
「人」テーブルの「性別id」は、前回最後に作ったクエリを元にルックアップの設定をしてある。
データの値だけちょっと変えておく。性別コードを二桁にした。
ここで例えば、「人」テーブルを元に以下のようなコンボボックスを含むフォームを作成したとする。コンボボックスのコントロールソースは「性別id」。
繰り返すが「性別id」は、クエリを元にルックアップの設定がしてある。
test ボタンを押すと VB のイミディエイトウィンドウにコンボボックスの値が出力されるようにした。コードは次の通り。
Private Sub btnTest_Click() ' Value プロパティ Debug.Print "性別id.Value: " & 性別id.Value ' Text プロパティ 性別id.SetFocus Debug.Print "性別id.Text: " & 性別id.Text End Sub
結果、次のような出力が得られた。
性別id.Value: 4 性別id.Text: 09
Text プロパティを取得する前にフォーカスを与えているのは、
コントロールの Text プロパティを取得または設定するには、コントロールにフォーカスを与えておく必要があります。フォーカスを与えないと、エラーが発生します。
(ヘルプの「Text プロパティ」より)
上記の結果を見ると、Text プロパティはコンボボックスに表示された値そのものを取得している。それに対して、Value プロパティは「データ > コントロールソース」である「性別id」の値が表示された。このような違いが生じたのは、コンボボックスの「データ > 集合ソース」にルックアップの設定で使っているクエリが設定されているためということか。
「人」と「性別」テーブルがある。
「人」テーブルの「性別id」が、「性別」テーブルの「id」を参照する。
「性別」テーブルの内容は、以下の通り。
id はシステムが自動的に割り当てる数値。CD は性別コード。 id は不変とし、性別コードはユーザが変更可能な値とする。
ルックアップを利用しないで、「人」テーブルにデータを入力する場合、「人」テーブルの「性別id」に、「性別」テーブルの「id」に存在する値を、直接入力しなければならない。
ルックアップの機能を利用しないと、性別を入力しにくい。
「人」テーブルにおいて、「性別id」フィールドの「ルックアップ」を設定する。
よく忘れるのは「連結列」の意味。本来、RDB の列に順序はない。
ルックアップの設定をした場合、「性別コード」を入力するとき、「性別の名称」を確認しながら入力できる。ただし、「人」テーブルのデータを一覧するとき、性別id には性別コードではなくて id が表示されるのでわかりずらい。
そこで、「列幅」の先頭を 0 cm に設定し、全体の「リスト幅」を 4 cm に変更する。
これにより id が表示されなくなり、性別コードが先頭に見える。テーブルの内容を一覧したとき、性別コードで表示されるため、データの内容を把握しやすい。
上記の例では、ルックアップする先が「テーブル」だった。(「値集合タイプ」がテーブル)。
ルックアップ先は、「連結列」、つまり順序で指定している。このため、ルックアップ先のテーブルの列の順序を変更すると、影響を与える。
そのため、「性別」に対するクエリ (q_性別) を作成しておき、「値集合タイプ」でクエリを参照する方が良い。
「性別id」のルックアップの設定は、
これにより、元のテーブルを変更しても、ルックアップの設定に影響が及ばなくなる。
追記 (2010.1.16) : フォームのコンボボックスにおけるルックアップの設定も、テーブルのときと同様に考えればよい。
コンボボックスにしたいコントロールを
を選択。
表示されるコンボボックスの列を、以下のようにしたいとする。
ID, CD, 名称
ただし、主キーである ID はユーザに見えないようにしたい。
先ほどのコンボボックスのプロパティにおいて、書式タブを選択。
Access でテーブルを定義するとき、データ型はコンボボックスで選択する。
一々マウス使い、メニューから選択するのは面倒。
「はじめて作るAccessアプリケーション」(p57) によると、
コンボボックスからの選択操作にマウスと Alt + ↓ または F4 キーしか使えないならば、…
(太字は引用者による)
セレクトボックスを表示するために、
にあるサンプルを用いた。
でセレクトボックスが開いた。Access のコンボボックスのショートカットキーと同じ。
Google Chrome via kwout
F4 を押しても反応なし。カーソルキーで、セレクトボックスのメニューのフォーカスを切り替えることはできる。
F4 は、別の操作のショートカットキーが割り当てられている。
比較してみると、 Alt + ↓ が多くのアプリで使えるようなので、こちらで手を馴染ませておこうか。
しかし、Firefox は、F4 でセレクトボックスを開くことができるのは楽。
「リストから一つを選択する」
UI の名称は、「コンボボックス」や「セレクトボックス」と呼ばれる。
どちらが一般的な名称なのだろう?
VB では「コンボボックス」と呼ばれ、HTML のフォームだと「セレクトボックス」言う。
Gallery lumber-mill - コンボボックス?セレクトボックス? によると、
コンボボックスは(ユーザの自由な入力を受け付ける点で)機能が大きく異なるため、混同しないようにすべきでしょう。
ドロップダウンリストまたはリストボックスを1行のテキストボックスと組み合わせたもので、ユーザーは値を直接入力することもできるし、既存のオプションから選択することもできる。グラフィカルなウェブブラウザのアドレス入力バーは、一般にコンボボックスになっている。
ユーザが入力できるリストがコンボボックス。
A list box is a GUI widget that allows the user to select one or more items from a list contained within a static, multiple line text box. …
- Drop down list - Like a list box, but not permanently expanded to show the elements of the list.
- Combo box - Like a drop down list, but users also can make entries not on the list.