Excel シートの特定範囲内で移動する方法

Excel シートにデータを入力し、【Enter】キーを押下すると、ふつうは右側のセルに移動します。

※ [ツール]-[オプション]-[編集] で、入力後に移動するセルの方向をカスタマイズしていない場合の話です。

しかし、ある特定範囲内だけで連続してデータを入力したい場合、その範囲内の右端を入力し終わったら、【Enter】で右側のセルに移動するのではなく、下の行の左端に移動してくれると便利ですね。

Excel にそのような設定は無いようですので、VBA で実現してみましょうか。

そのような設定が有ることが判明しました。

VBE から対象シートの ScrollArea というプロパティに「$A:$C」のように対象範囲を設定すると、その範囲内でしか移動できなくなります。

この方法にはブックを閉じると設定が保持されないという難点が有るものの、対象範囲外に移動する必要が無い場合は、これが一番簡単です。

対象範囲外に移動する必要が有る場合は、下記の手法を適用できます。

YU-TANG 2003/12/29

対象シートのクラスモジュールに、以下のコードをコピーします。

Dim rngPrev As Range    ' モジュールレベル変数
Private Sub Worksheet_Activate() ' 変数に選択セルを退避します。     Set rngPrev = ActiveCell End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range) ' 折り返し処理を行います。     Call ReturnNext(Application.Names("名前").RefersToRange, Target) ' 変数に選択セルを退避します。     Set rngPrev = Target End Sub
Private Sub ReturnNext(ByVal ReturnArea As Range, ByVal Target As Range) Dim rng As Range ' 直前に選択されていたセルが無い場合は関数を終了します。     If (rngPrev Is Nothing) Then Exit Sub ' 現在の選択セルが指定領域内の場合は関数を終了します。     Set rng = Intersect(ReturnArea, Target) If (rng Is Nothing) = False Then Exit Sub ' 直前の選択セルが指定領域外の場合は関数を終了します。     Set rng = Intersect(ReturnArea, rngPrev) If (rng Is Nothing) Then Exit Sub ' 直前の選択セルと現在の選択セルが連続していない場合は関数を終了します。     If (rngPrev.Next.Address <> Target.Address) Then Exit Sub ' 現在の選択セルの下の行と指定領域が交差していない場合は関数を終了します。     Set rng = Intersect(ReturnArea, Target.Offset(1).EntireRow) If (rng Is Nothing) Then Exit Sub ' セルを選択します。     rng(1, 1).Select End Sub

このコードの中で書き変える必要があるのは、名前の一箇所だけです。

ここには対象セル範囲に付けた [名前] を指定する必要があります。

[名前] は、対象セル範囲を選択して、数式バーの左側に表示されている [名前] ボックスに入力するか、あるいは [挿入]-[名前]-[定義] から作成することができます。

ReturnNext 関数の引数には Range オブジェクトさえ渡せればよいので、名前を定義せずに次のように指定することも可能です。

    Call ReturnNext(Range("$A:$C"), Target)

なお、当然ですが、これはマクロが有効になっていないと機能しません。

したがって、[ツール]-[マクロ]-[セキュリティ] の設定が「高」になっていると使えません。

「中」にしておいて、ブックを開く際に <マクロを有効にする> を選択するようにしましょう。

COM Object の落とし穴

最近とある投稿で、VBA から別のファイルのコンテキストメニューを実行するためのコードを書く機会がありました。

コンテキストメニューというのは、ファイルを右クリックすると出てくるメニューのことです。

もちろん以下の COM を利用することになりました。

Microsoft Shell Controls And Automation (SHELL32.dll)

で、テスト用に書いたコードが以下です。

' ファイルのコンテキストメニューを実行します。
Sub InvokeVerbTest()
    Const DIR_PATH = "C:\TEMP"
    Const DOC_NAME = "TEST.doc"
    Const VERV_NAME = "開く(&O)"
    Dim oApp As Object
    Set oApp = CreateObject("Shell.Application")
    oApp.NameSpace(DIR_PATH) _
        .ParseName(DOC_NAME).InvokeVerb VERV_NAME
    Set oApp = Nothing
End Sub

実行すると、問題なく TEST.doc が開きます。

テスト結果が OK だったので、カプセル化して、実用向きに書き直すことにしました。

書き直したものが下記です。

Sub TestOpen()
    Const DIR_PATH = "C:\TEMP"
    Const DOC_NAME = "TEST.doc"
    Const VERV_NAME = "開く(&O)"
    InvokeFileVerb DIR_PATH, DOC_NAME, VERV_NAME
End Sub
' ファイルのコンテキストメニューを実行します。 Sub InvokeFileVerb(folder As String, _ file As String, _ verb As String) Dim oApp As Object Set oApp = CreateObject("Shell.Application") oApp.NameSpace(folder) _ .ParseName(file).InvokeVerb verb Set oApp = Nothing End Sub

パッと見でお分かりの通り、コンテキストメニューの実行部を関数として独立させ、必要な引数を外部から渡して再利用できるように組み直しています。

それ以外、ロジックは何一つ変わっていません

にもかかわらず、このコードを実行した私は、まったく予期しなかった次のようなエラーに遭遇して引っくり返りました。

Run-time Error 91.

デバッグモードに切り替えると、Shell.Application の実行部で止まっていますが、引数を確認しても、間違いなく渡されているようです。

Debug Run-time Error 91.

仕方が無いので、オブジェクト変数に段階的にセットしていきながら、様子を見る事にしてみました。

すると、まったく同じ実行時エラー 91 が発生。

デバッグモードに切り替えると、FolderItem オブジェクトの Set 部で止まっています。

そこで Folder オブジェクトの状態を確認した私は、再度引っくり返りました。

Debug Run-time Error 91.

あろうことか、Folder オブジェクトが Nothing になっています。

オブジェクトが無いのでは、メソッドが失敗するのも無理はありません。

しかし一体なぜオブジェクトが生成されなかったのでしょうか。

フォルダーのパスを渡し損ねたのでしょうか。

しかし、引数を確認した私は、大方の予想通り、三度引っくり返ることになりました。

Debug Run-time Error 91.

何の問題もなく、フォルダーのパスが渡されています。

まったくワケが分かりません。

ここでちょっと推理です。

うまくいった最初のコードと、失敗した 2 番目のコードとの違いは何でしょうか。

どう考えても、ローカル定数/変数として宣言された DIR_PATH と、関数の仮引数として渡された folder との差しか思い当たりません。

VBA 上で考えると、動作上では両者には特に何の違いもありません。

しかし今回の Folder オブジェクトを巡る一件を見る限り、実際にはローカル定数/変数と関数の仮引数との間には何らかの違いが存在し、VBA はその違いを言語側で吸収してくれるので問題にならないが、一部の COM オブジェクトはその違いの影響を受けている可能性があるようです。

ちょっとテストしてみましょう。

まず仮引数 folder を CStr 関数で文字列変換してみます。

VBA 的に考えると、String 型の仮引数を String 型に変換しても何の意味もありませんが、COM 的には意味があるかもしれません。

' ファイルのコンテキストメニューを実行します。
Sub InvokeFileVerb(folder As String, _
                   file As String, _
                   verb As String)
    Dim oApp As Object
    Set oApp = CreateObject("Shell.Application")
    oApp.NameSpace(CStr(folder)) _
        .ParseName(file).InvokeVerb verb
    Set oApp = Nothing
End Sub

走らせてみると...ビンゴ!

無事 Word 文書が起動しました。

では推理をもう一歩進めてみましょうか。

今回は引数を参照渡ししています。これはつまり、内部的には変数ではなく変数のポインタを渡していることになるはずです。

そして COM がポインタを処理できなければ、そこで失敗する可能性は十分に考えられます。

もしこの仮定が正しければ、引数を値渡しにすれば、エラーにならないはずです。

コードをさらに改変して、引数を値渡しにするため、ByVal ステートメントを追加します。

代わりに CStr 関数は外します。

' ファイルのコンテキストメニューを実行します。
Sub InvokeFileVerb(ByVal folder As String, _
                   file As String, _
                   verb As String)
    Dim oApp As Object
    Set oApp = CreateObject("Shell.Application")
    oApp.NameSpace(folder) _
        .ParseName(file).InvokeVerb verb
    Set oApp = Nothing
End Sub

実行してみると...うーん、残念。

やはり実行時エラー 91 が起きてしまいました。

どうも値渡しか参照渡しかは関係ないようです。

しかし、CStr 関数は何が違うんでしょうか。

CStr 関数でなければいけないんでしょうか。

ひょっとして...。

' ファイルのコンテキストメニューを実行します。
Sub InvokeFileVerb(folder As String, _
                   file As String, _
                   verb As String)
    Dim oApp As Object
    Set oApp = CreateObject("Shell.Application")
    oApp.NameSpace((folder)) _
        .ParseName(file).InvokeVerb verb
    Set oApp = Nothing
End Sub

なんと、これでも OK でした。

仮引数 folder を小括弧 ( ) で括り、ほとんど無意味に式として評価させただけで、通るようになるのです。

このような現象が起きる理由は依然として分かりませんが、これは VBA から COM Object を利用する際の落とし穴として肝に銘じておく価値は有りそうです。

〜解決編〜

HIDE さんから原因と回避策を教えていただきました。(多謝!)

非常に分かりやすいメールだったので、私が下手に解説するより、そのまま引用してみます(手抜きか? ^^;)。

原因は関数引数の型宣言が邪魔をしているだけです。

呼び出し側ではバリアント型のデータですから As String を削除すればいいと
思います。

以上の修正で当方の環境では問題なく動作します。

ただ、プログラム中でフォルダオブジェクトを引き回したりする場合は参照設定に
Microsoft Shell Controls and Automation を追加した方がいいと思います。

なんたってオブジェクトブラウザで使えるメソッドや定数が確認できますからね。
ただし名前の衝突は起こりやすくなるので変数やメソッド名はできるだけ素っ気
ないほうが安全なようです。以下サンプル

Sub inv(fo, fi, ve)
    Dim sh As Shell32.Shell
    Set sh = New Shell32.Shell
    sh.NameSpace(fo).ParseName(fi).InvokeVerb (ve)
    Set oApp = Nothing
End Sub
Sub test() Call inv("c:\temp", "test.doc", "開く(&O)") End Sub

これで完っ璧に動作します。

上記を掲載してから苦節 3 ヶ月(いや載せただけで、別に苦労は何もしてないだろ)、ついに堂々の大団円を迎えることが出来ました!

私は Shell オブジェクトが引数を Variant で受けることは知っていたのですが、どうせ内部処理形式は String なんだろうし、型が厳しい分には問題ないだろうと高を括っていたのですが、大間違いでした。

引数の受け渡しは、受け側と渡し側のデータ型を可能な限り揃える、という大原則を、自分の都合の良いようにねじまげた結果の墓穴でした。

プログラマーたるもの、基本に忠実に、毎晩素振り 200 回を欠かさぬよう肝に銘じておきたいものです。

最後に改めて、HIDE さん、ありがとうございました。

2003/10/04 YU-TANG 記

ShowOLEInfo 関数

フォーム上に連結オブジェクトフレームを配置し、OLE オブジェクト型フィールドの内容を表示しているときに、VBA 上で OLE オブジェクトに関する情報を取り出すデモです。

元々は準備中のトピック用ですが、関数だけ先に公開します。

' 連結オブジェクトフレーム(への参照)を引数に取り、
' OLE オブジェクトの情報をメッセージボックスで表示
' します。
' 使用例:Call ShowOLEInfo(Me.連結オブジェクトフレーム名)
Sub ShowOLEInfo(bof As Access.BoundObjectFrame)
    ' 定数/変数宣言部
    Const OFFSET_OLE_CLASS_HEADER = 20
    Const OFFSET_OLE_SERVER_HEADER = 12
    Dim s            As String
    Dim v            As Variant
    Dim bytClsHdrLen As Byte
    Dim bytOLEClsLen As Byte
    ' データが無い場合は関数を終了します。
    If IsNull(bof.Value) Then Exit Sub
    ' ヘッダーのサイズを取得します。
    bytClsHdrLen = bof.Value(2)
    ' ヘッダー情報を取得します。
    s = MidB(bof.Value, _
            OFFSET_OLE_CLASS_HEADER + 1, _
            bytClsHdrLen - OFFSET_OLE_CLASS_HEADER)
    ' ユニコードに変換します。
    s = StrConv(s, vbUnicode)
    ' オブジェクトタイプとクラスに分割します。
    v = Split(s, vbNullChar, -1, vbBinaryCompare)
    ' OLE クラス名のサイズを取得します。
    bytOLEClsLen = bof.Value(bytClsHdrLen _
                            + OFFSET_OLE_SERVER_HEADER + 1)
    ' ヘッダー情報を取得します。
    s = MidB(bof.Value, _
            bytClsHdrLen + OFFSET_OLE_SERVER_HEADER + 1, _
            bytOLEClsLen - 1)
    ' ユニコードに変換します。
    s = StrConv(s, vbUnicode)
    ' 情報をユーザーに表示します。
    MsgBox "OLE オブジェクトタイプ:" & v(0) _
            & vbCrLf & "クラス:" & v(1) _
            & vbCrLf & "OLE クラス:" & s, _
            vbInformation, _
            "OLE Object Infomation!"
End Sub

Slice 関数

たとえば Excel からインポートしてきた住所録データが有るとします。

見ると、こんな感じでデータが入力されています。

住所録テーブル
氏名
宛先:山田太郎様
宛先:長谷川泰治様
宛先:井上弘様

なんと余計な文字が前後に付いています。

欲しいのは名前だけなので、先頭の「宛先:」と末尾の「様」は要りません。

しかし文字数がデータによって違うので、Mid 関数で一律何文字取り出すというわけにはいかなそうです。

こんなとき、どうしますか。

おそらく Mid 関数と Len 関数を組み合わせることになるでしょう。

式1: Mid$([氏名], 4, Len([氏名])-4)

もちろん、これでもいいのですが、しかしこの式、分かりにくくは無いですか?

もっとこう単純に、左から 3 文字、右から 1 文字要らないということがはっきり分かるシンプルな式にならないものでしょうか。

Python みたいにスライスできれば話は簡単なのですが、VBA ではそうはいきません。

ここはユーザー定義関数 Slice を作ることにしましょう。

' 両側から指定文字数を取り除いた文字列を返します。
' 引数の説明:
' source    必須。対象の文字列を指定します。
' lenL   省略可。先頭から削除する文字数を指定します。
' lenR   省略可。末尾から削除する文字数を指定します。
' 補足:
' 引数 lenL と lenR に負の数値が渡された場合は 0 と
' みなします。また削除する文字数が対象文字列総数を
' 超えた場合は空文字列を返します。エラーにはなりません。
' 使用例:
'   ?Slice("宛名:山田太郎様",3,1)
'   実行結果 -> 山田太郎
Public Function Slice( _
    source As String, _
    Optional ByVal lenL As Integer, _
    Optional ByVal lenR As Integer) As String
    ' 定数/変数宣言部
    Dim lngLenLR As Long
    Dim lngLenSource As Long
    ' 引数を補正します。
    If (lenL < 0) Then lenL = 0
    If (lenR < 0) Then lenR = 0
    ' 変数に値をセットします。
    lngLenLR = (lenL + lenR)
    lngLenSource = Len(source)
    ' 文字列が全て削除される場合は関数を終了します。
    If (lngLenSource <= lngLenLR) Then Exit Function
    ' 戻り値をセットします。
    Slice = Mid$(source, lenL + 1, lngLenSource - lngLenLR)
End Function

"モジュールがありません" エラー

ある日 MDB をいじっていた私は、突如エラーに遭遇しました。

Error: No Module.

MDB にはフォーム 1 個しか存在せず、クラスモジュールを見ても構文的に問題はなく、エラーの原因となるようなものは何も見当たりません。

参照設定も OK で、念のため MS-Access を再起動したりモジュールをコンパイルし直したりしてみましたが、現象は変わりませんでした。

コマンドボタンは、処理に関わらず全滅です。

ただし、直前にちょっとクリティカルな MS-Access の隠しコマンドを検証していたので、状況からプロジェクト破損と判断しました。

プロジェクトレベルの破損なら、デコンパイルで直るかもしれません。

というわけで、MS-Access をいったん終了させ、[スタート]-[ファイル名を指定して実行] から以下のコマンドを流しました。

MSACCESS /decompile "MDB のパス"

これでデコンパイル後の MDB が開くので、コンパイルし直してテストすると...OK!

直りました。(^o^)

私はこの手のエラーに(最近は)余り遭遇しないので、ちょっと珍しい体験として記録しておきます。

心当たりの無い "モジュールがありません" エラーにお悩みの方は、一度デコンパイルをお試しあれ。

拡張子 MDB の意味

なんかトリビアっぽいネタですが、皆さんは拡張子 MDB が何の略か疑問に思ったことは無いでしょうか。

拡張子 MDB の意味については、MS からの公式文書で言及されているのを見たことはありません。

したがって、最初にお断りしておきますが、以下は全て私の私見です。

なんとなく「Microsoft Access DataBase」の略のような気がするかもしれませんが、以前 私が調べた際には、少なくとも複数の民間サイトに「Microsoft Access Multikey DataBase format」の略という説明が載っていたのを確認しています。

Web 魚拓を取り忘れたため、今となっては情報元がひとつも残っていない状態ですが、個人的にはこの情報の信憑性は高いと思っています。

理由は 2 つ有ります。

1 つは、思い込みで与太を飛ばすなら「Microsoft Access DataBase」の略と言うほうが自然なわけで、なんとなくでっちあげるときに「Multikey」という単語を織り込む方が不自然に感じるからです。

むしろそれなりの根拠が有ったと考えるべきでしょう。

おそらく初期の MS-Access の開発チームが拡散していく過程で、内部名称が漏れたのではないでしょうか。

もう 1 つは、弱い援護要因ですが、「Microsoft Access DataBase」が有り得ないと思うからです。

拡張子というのは、データフォーマットの正体を示すものであって、通常ここに製品名の頭文字を使うことは有っても企業名の頭文字を入れることはまず有り得ません。

おそらく M$ の唯我独尊的イメージから やりかねないと思われたのかもしれませんが、いかに M$ と言えどもそこまで常識外れのネーミングをするというのはちょっと考えられないのです。

実際 他の MS-Office 製品のどれを見ても、データフォーマットの拡張子に製品名ではなく企業名の頭文字を入れた例は有りません。

ましてや Office ファミリーとしては後発の MS-Access が、これらの一般ルールに則るならまだしも、単独で破るという推測は、どう考えても苦しい。苦しすぎます。

したがって私は「MDB = Microsoft Access Multikey DataBase format」説が今のところ有力ではないかと思っています。

では MDE はどうか、ということになると思いますが、おそらく「Microsoft Access Multikey DataBase Extension」の略でしょう。

MDE について言及された公式文書は知りませんが、ADE については「Microsoft Access プロジェクト エクステンション」である、と MSKB に載っています。

824266 - [ACC2003] マルチユーザー環境で Access プロジェクトまたはエクステンション ファイルを開くと "プロジェクト名は読み取り専用で開かれます" エラー メッセージが表示される

ADE の E がエクステンションなら、MDE の E もエクステンションと考えるのが自然に思えます。

...と書いてはきましたが、MS が公表するまで結局のところ、真偽のほどは不明です。

皆さんはどう思います?

# まあ、どうでもいいっちゃいいんですケド。^^;)

DoCmd.OpenReport で OpenArgs を代替する

DoCmd.OpenForm には OpenArgs という引数が有り、この引数に任意の値を渡すと開かれたフォーム側のイベント プロシージャで OpenArgs を利用して分岐処理を行うことが出来ます。

ときおり、これを DoCmd.OpenReport でも使えればなぁという状況が有るのですが、残念ながら DoCmd.OpenReport には OpenArgs が有りません(Access 2002 以降は知りません)。

そういう場合は仕方が無いので Global 変数を経由したりフォーム上の(往々にして非可視の)コントロールを参照させたりして分岐処理を行うことになります。

正攻法としてはこれでも問題は有りません。

以下で紹介するのは はっきり言って外道業で、たぶん公には誰も推奨しない方法です。 私のように、Global 変数は気持ち悪いし、かと言ってコントロール参照も特定フォーム依存でやだ、という天邪鬼だけが使う方法です。

DoCmd.OpenReport "レポート名", acViewPreview, , "'任意の文字列'"

あるいは

DoCmd.OpenReport "レポート名", acViewPreview, , 数値

任意の値を渡している引数は、本来は WhereCondition(フィルター条件式)用です。

ただ評価した結果が False にさえならなければ結果的にレコードセットに影響が無いことを利用(悪用?^^;)して、任意の値を渡しています。

したがって、後者の形式(数値)では 0 は使えません。0 を渡すと False と判定されてフィルターがかかってしまい、レコードが 0 件になってしまいます。

受け側(レポートのイベントプロシージャ)では、次のように値を利用します(文字列を渡して [開く時] イベントで処理する場合の例です)。

Private Sub Report_Open(Cancel As Integer)
    If Me.Filter = "('任意の文字列')" Then
        ' 条件式が True の場合の処理
    Else
        ' 条件式が False の場合の処理
    End If
End Sub

Where 句を本来の目的でも利用したい場合は、少しひねる必要が有りますが、Split 関数で取り出してから評価させるようにすれば両立は可能です。

MultiFormat 関数

ある文字列の中の複数箇所に、任意の文字列を埋め込むための関数です。

検索文字はメタ文字 % の直後に 1〜n までのインデックスを記述して指定します。

先に使用例を見ていただいた方が早いかもしれません。

Sub TestMultiFormat()
    Dim strSource As String
    strSource = "%1さん、%1さん、%2テーブルで%3様がお待ちです。"
    Debug.Print MultiFormat(strSource, "乙姫", "4 番", "彦星")
End Sub

実行結果(イミディエイトウィンドウ)は次の通り。

乙姫さん、乙姫さん、4 番テーブルで彦星様がお待ちです。

ご覧のように、%n の箇所を引数によって置き換えた文字列を返します。

仕様は以下の通りです。

では、コードです。

' ***************************************
' 関数名: MultiFormat
' 目 的: 任意個数のマーカーを置換した文字列を返します。
' 作成者: YU-TANG@http://www.f3.dion.ne.jp/~element/msaccess/
' 作成日: 2003/11/03
' 更新日: 2005/01/02
' 更新内容: 検索マーカーの上限を 9 個から無制限に拡張。
' 戻り値: String 型
' 引数の説明:
' args 引数を任意個数指定します。
'       先頭の引数は操作対象文字列になります。
'       第 2 引数以降は置換文字列となります。
'       検索マーカーは先頭から %1,%2...とカウントアップしていきます。
' 使用上の注意:
'    Replace 関数を使用するため、Access 2000 以降限定です。
'    "%" 記号自体を表現する場合は、"%%" のように、"%" 記号を 2 回
'    続けてください。
' 使用例:
'  s = MultiFormat("〒%1-%2","104","0123")
' ***************************************
Public Function MultiFormat(ParamArray args()) As String

    ' 変数宣言部
    Dim i As Integer
    Dim n As Integer
    Dim s As String

    ' 変数を初期化
    n = UBound(args)

    ' エラーチェック
    If n = -1 Then ' 引数が無い場合
        Exit Function
    Else           ' 上記以外
        If (LenB(Nz(args(0), vbNullString)) = 0) Then Exit Function
    End If

    ' 本処理
    s = args(0)
    ' "%%" を Null 文字でエスケープします。
    s = Replace(s, "%%", vbNullChar, 1, -1, vbBinaryCompare)
    ' マーカーを順次置換します。
    For i = n To 1 Step -1
        s = Replace(s, "%" & i, args(i), 1, -1, vbBinaryCompare)
    Next
    ' Null 文字を "%" に戻します。
    s = Replace(s, vbNullChar, "%", 1, -1, vbBinaryCompare)

    ' 戻り値をセットします。
    MultiFormat = s

End Function

冒頭の例よりも、もう少し実際的な例を挙げてみましょう。

次のコードは、フォーム上のコントロールの値を利用して、レコードを 1 件追加する SQL 文を生成しています。

' 新しい ID を取得します。
lNewId = Nz(DMax("BillId", "tblBill"), 0) + 1
' コメントを取得します。
If IsNull(Me.txtComment) Then
    sComment = "Null"
Else
    ' 引用符をエスケープします。
    sComment = Replace(Me.txtComment, "'", "''", 1, -1, vbBinaryCompare)
    sComment = "'" & sComment & "'"
End If
' SQL 文を生成します。
sSQL = "INSERT INTO tblBill " & _
       "(BillId, BillDate, AgencyId, ChargePercent, TaxPercent, " & _
       "SubTotal, Charge, Tax, GrandTotal, Comment) VALUES " & _
       "(" & lNewId & ", #" & Format$(Me.txtBillDate, "yyyy-mm-dd") & _
       "#, " & Me.lstAgency.Value & ", " & Me.ChargePercent & ", " & _
       Me.txtTaxPercent & ", " & Me.txtSubTotal & ", " & Me.txtCharge & _
       ", " & Me.txtTax & ", " & Me.txtGrandTotal & ", " & sComment & ");"

これはこれで意図どおりの SQL 文は生成されるのですが、後半、& 演算子が乱舞する辺りの見通しが非常に悪くなっています。

これを MultiFormat 関数を使って書きなおしてみましょう。

' 新しい ID を取得します。
lNewId = Nz(DMax("BillId", "tblBill"), 0) + 1
' コメントを取得します。
If IsNull(Me.txtComment) Then
    sComment = "Null"
Else
    ' 引用符をエスケープします。
    sComment = Replace(Me.txtComment, "'", "''", 1, -1, vbBinaryCompare)
    sComment = "'" & sComment & "'"
End If
' SQL 文を生成します。
sSQL = "INSERT INTO tblBill " & _
       "(%1, %3, %5, %7, %9, %11, %13, %15, %17, %19) " & _
       "VALUES " & _
       "(%2, #%4#, %6, %8, %10, %12, %14, %16, %18, %20);"
sSQL = MultiFormat(sSQL, _
                   "BillId", lNewId, _
                   "BillDate", Format$(Me.txtBillDate, "yyyy-mm-dd"), _
                   "AgencyId", Me.lstAgency.Value, _
                   "ChargePercent", Me.ChargePercent, _
                   "TaxPercent", Me.txtTaxPercent, _
                   "SubTotal", Me.txtSubTotal, _
                   "Charge", Me.txtCharge, _
                   "Tax", Me.txtTax, _
                   "GrandTotal", Me.txtGrandTotal, _
                   "Comment", sComment)

コードの行数は長くなりましたが、見通しが格段に向上しているのが分かります。

Value プロパティは Properties コレクションに存在しない

これは Office 2000 日英コマンド一覧の MDB を作成していたときに体験した話です。

私は検索キーワード用テキストボックスにフォーカスが有れば Text プロパティの値を、フォーカスが無ければ Value プロパティの値を取得しようとしていました。

そこでテスト用に書いたのが次のコードです。

sProp = IIf(fActive, "Text", "Value")
MsgBox Me.テキスト1.Properties(sProp)

ところがこのコード、Text は取れるのですが、Value の場合は実行時エラーになってしまうのです。

Run-time Error 2455.

参照の仕方を端折りすぎたかな?と思って、次のように書き直してみました。

MsgBox Me.テキスト1.Properties.Item(sProp).Value

ところが、やはり Value の場合に限ってエラーになってしまいます。

この時点で、下記のようなコードにすれば問題ないのは分かっていました。

If fActive Then
    MsgBox Me.テキスト1.Text
Else
    MsgBox Me.テキスト1.Value
End If

ですが、やっぱり短いコードの方がいいじゃないですか。

たしかにちょっと考えると Value プロパティの Value を参照するというのは妙な気もしますが、一方で Value も立派なプロパティの一つであって、当然メンバとして Name や Value を持っている気もします。

しかし、とにかく Properties コレクションから取れていないのもまた事実です。

事ここにいたって、私は念のためテキストボックスの Properties コレクションの要素をループで列挙して確認してみることにしました。

その結果なんと!(と言うか、やはりと言うか ^^;)Properties コレクションに Value は有りませんでした

Value がコントロールのプロパティであることに違いは無いのですが、Properties コレクションにオブジェクトとしては実装されていなかったのです。

従って「テキスト1.Value」では参照できても、「テキスト1.Properties("Value")」では参照できなくて当然ということになります。

私は Access VBA を使い初めてかれこれ 6、7 年になると思いますが、初めて気付きました。

単に不勉強と言われればそれまでですが、Properties コレクションに Value が無いというのは私にとってちょっとした驚きでした。

結局、この件の解法は次の 3 種類ということになりそうです。

If fActive Then
    MsgBox Me.テキスト1.Text
Else
    MsgBox Me.テキスト1.Value
End If
sProp = IIf(fActive, "Text", "Value")
MsgBox Eval("Forms." & Me.Name & ".テキスト1." & sProp)
On Error Resume Next
    MsgBox Me.テキスト1.Text
    If (Err.Number <> 0) Then MsgBox Me.テキスト1.Value
On Error GoTo 0

# なんかスマートじゃないけど。(ーー;)

HTA ファイルの文字化け

私はテキストエディタに EmEditor Free を使っています。

EmEditor Free はシェアウェア版と違ってユニコードが使えないのですが、別に不自由は感じないので、そのまま使い続けています。

で、私はそれで HTML ファイルや HTA ファイルのソースも書いています(本サイトは全部そうです)。

ところがそれで作成した HTA ファイルを、Windows NT 4.0 や Windows 98 などの古い OS 上で起動すると、2 バイト文字の箇所が文字化けを起こすのです。

しばらく原因が分からなかったのですが、ある日ふと思いついて、ファイルをメモ帳で開いて Shift-JIS ではなくユニコードで保存すると、問題なく表示されることが判明しました。

どうやら mshta.exe のバージョンが古いと、ユニコードにしか対応していないようです。

私は別に Web 開発者ではないので(Web 開発者はテキストエディタでソース直書きしたりしないよな)、ひょっとしたらその筋ではこれ、常識なのかもしれません。

更新履歴