Visual Basicに関するメモ

このページは、不定期に更新されています。更新箇所の明示は行われませんのでご注意ください。また、内容はごく一時的なメモに類するものです。一貫性や真偽について、保証できませんので、ご了承ください。

また、こちらにいくつかのサンプルコードを掲載していますので、あわせてご利用ください。


TimerコントロールのTimerイベント発生のタイミング

Timerイベントの発生の間隔は、Intervalプロパティで調整することができると説明されているが、正確には、Timerイベントのプロシージャーから抜けたあと、次のTimerイベントが発生するまでの間隔である。

したがって、Intervalプロパティーを2000(2秒相当)に設定し、Timerプロシージャーの中に1秒を要する処理を記述した場合、(1)Timerイベント発生、(2)1秒間の処理、(3)Timerイベントを抜けて2秒待機、(4)次のTimerイベント発生、というように(1)と(4)のTimerイベント発生の間隔は3秒となる。


Formのアイコンの取得にLoadResPictureを使うと汚くなる(exeで実行時)

Formのアイコンを実行中に変更したい場合、LoadPictureやLoadResPictureなどを使うことができるが、リソースファイルからLoadResPictureで16×16のサイズのアイコンを取得すると表示が汚くなる。一度32×32に引き伸ばしたあとで再度16×16に戻しているように思われる。

きれいに表示させるためには、LoadPictureでファイルから直接取得するか、あるいは、ImageListコントロールから取得する方法がよい。


リッチテキストコントロールの右端折り返しを抑制する

リッチテキストコントロールは、デフォルトの状態では、長い行がコントロールの右端で折り返されて表示される。この折り返しを抑制し、長い行であっても一行で表示する方法については、ヘルプでも、Microsoft社のHPでも適切な説明がなされていない。一応、以下のプロパティーの設定で、この動作が実現できるようである。

MultiLine:
True
ScrollBars:
1 - rtfHorizontal 又は
3 - rtfBoth
RightMargin:
1E+09

要は、RightMarginを十分に大きな数値にすることである。なお、RightMarginは、オブジェクト・ブラウザによるとSingle型の値を受け付けるので、E+38のオーダーまでエラーにはならないが、大きすぎると、値が0の場合と同じように折り返し動作となってしまう。


コレクションのスキャン

コレクション内のアイテムを総当たりするスキャンは、

Dim I as Long
Dim colCollection as Collection

For I = 1 to colCollection.Count
    '(colCollection(I)を使って処理)
Next I

とするのが安全と思われる。サンプルとして次のような記述がよく見られるが、特にActiveX DLLのプロジェクト内でこの例を用いると、DLLの読み込みに失敗することがある。

Dim vntItem as Variant
Dim colCollection as Collection

For Each vntItem in colCollection
    '(vntItemを使って処理)
Next vntItem

日付の「定数値」(日付/時刻リテラル)

Visual Basicでは、日付のリテラル値は、#m/d/yyyy#のフォーマットが標準である。このフォーマットは、「アメリカ」式であるので、また、Visual Basicは、もともとアメリカの製品であるので、おそらく、どの国のバージョンであっても通用するものと思われる(未確認)。日本語バージョンの場合、様々なフォーマットでリテラル値を入力することはできるが、エディタにより、すべてこのフォーマットに自動変換される。例えば、

#97/06/07#    #97-June-07#    #07/June/1997#    #June/07/1997#

は、すべて、

#6/7/1997#

に変換される。なお、Visual Basicのスクリプト版(VB Script)の場合、この自動変換をするエディタはないので、必ず、#m/d/yyyy#のフォーマットで入力しなければならない。


ファイルとディレクトリの属性

属性の取得はGetAttr関数。属性の設定はSetAttrステートメント。SetAttrステートメントに関して、ヘルプにはファイルの属性設定のことしか書いていないが、ディレクトリの属性設定も可能である。ただし、ディレクトリに対してSetAttrで、vbDirectoryの属性を指定しようとするとエラーとなる。あるディレクトリの属性をGetAttrで取得して、そのままSetAttrで他のディレクトリにコピーする場合、要注意。したがって以下のような記述が必要となる。

lngAttribute = GetAttr(strOriginalDirectoryName)
SetAttr strNewDirectoryName, lngAttribute and (Not vbDirectory)

また、Windows NTにおいて、圧縮されたディレクトリ又はファイルに対してGetAttrを行うと、圧縮を示すフラッグ(2048)が立った結果が戻る。これに適当に演算を加えてSetAttrを行うと、エラーとなり正常に属性を設定することができない。以下のような措置が必要である。なお、この操作によって圧縮属性が変化することはない。

lngAttribute = GetAttr(strOriginalDirectoryName)
If lngAttribute >= 2048 Then lngAttribute = lngAttribute - 2048
....
SetAttr strNewDirectoryName, lngAttribute

インターネット・トランスファ・コントロールのOpenURLメソッド (VB5)

ヘルプ(オブジェクト・ブラウザからアクセス)を読むと、OpenURLメソッドにより、指定したURLのコンテンツを一括取得できそうに思われるが、そうならない場合もある。試行錯誤の結果、どうもTransfer-Encoding: chunkedである場合にうまくいかないようである。このときには、OpenURLメソッドに続けてGetChunkメソッドも実行する必要がある。

下記のコードでは、strURLで指定した場所のコンテンツがbytReceivedData()に格納される。コンテンツはhtml/textであっても、その他のバイナリ形式であってもよい。strReceivedDataとstrBufferというString型の変数を用いているが、バイト列とバイト列を連結するためのものである。

Dim strURL as string
Dim bytReceivedData() As Byte

Dim strReceivedData As String
Dim strBuffer As String

'**** First Call ****
strReceivedData = Inet1.OpenURL(strURL, icByteArray)

'**** Purge Chunk ****
Do
    strBuffer = Inet1.GetChunk(8192, icByteArray)
    If LenB(strBuffer) = 0 Then Exit Do
    strReceivedData = strReceivedData & strBuffer
Loop

bytReceivedData = strReceivedData

しかしながら、本当によい方法は、OpenURLメソッドを用いずに、Executeメソッドを用いることと思われる。その後、データの引き出しは、StateChangedイベントにおいて、State=icResponseCompletedとなったところで、GetChunkメソッドを用いて行う。


APIとの文字列のやりとり(ANSI vs UNICODE変換)

ByValにせよ、ByRefにせよ、文字列をString型で渡す場合には、Visual BasicによりUNICODEからANSIへの変換が自動的に行われる。おそらく、変換した結果を元の同じ場所に格納した上で(UNICODEからANSIへの変換では、バイト数は必ず等しいか小さくなる。)、APIに制御が移るようである。したがって、UNICODEのままで渡したい場合には、別途工夫が必要。

APIから文字列を受け取る方法は、

の4種類が考えられるが、いずれの場合にも、Nullで終了する(Null Terminated)ANSI文字列しか正常に受け取ることができず、しかも、UNICODEへの変換が行われる。したがって、API自身がUNICODEを返す場合には、別途工夫が必要。

特に、最後のOLE Automationによる書き換えについては、32bitアプリケーションのOLE Automationの場合、BSTRはUNICODE文字列が保持されていることが大前提(Visual BasicのString型と同じ)なので、取り扱いに注意を要する。


APIへの文字列の引き渡し

LPSTR、LPCSTR、LPTSTR、LPCTSTR、BSTR(いずれも、文字列先頭へのポインタ)で、ANSI文字列を引き渡す

Declare内の仮引数記述
ByVal LP As String
実引数記述
strVar

Visual Basicでは、DLL呼び出しで文字列を渡す場合、UNICODEをANSIに自動的に変換してくれるので、この方法を用いることができる。

LPTSTR、LPCTSTR、BSTRで、UNIOCDE文字列を引き渡す

Declare内の仮引数記述
ByVal LP As String
実引数記述
StrPtr(strVar)

UNICODEからANSIへの自動変換を回避するために、前もってアドレスを算出しておき、アドレスを直接渡す。BSTR自身は、文字列先頭へのポインターであるため、この方法を使うことができる。


文字列先頭へのアドレスから、文字列を引き出す

APIから文字列を得たい場合で、文字列先頭へのポインタしか受け取れないことがある。とくに、構造体でAPIとやり取りする場合、構造体が文字列先頭へのポインタ保持していると、文字列そのものを取り出すためには、工夫が必要となる。以下に、文字列先頭のアドレスが判明している場合に、文字列を取り出すコードを示す。

Null文字で終了しているANSI文字列の場合

Private Declare Function SysAllocStringByteLen _
                         Lib "oleaut32" _
                        (ByVal psz As Long, _
                         ByVal length As Long) _
                         As String

Private Declare Function lstrlenA _
                          Lib "kernel32" _
                         (ByVal lpString As Long) _
                          As Long

Public Function GetANSIStringAt(ByVal sz As Long) As String
    Dim lngLengthByte As Long

    If sz = 0 Then
        GetANSIStringAt = vbNullString
    Else
        lngLengthByte = lstrlenA(sz)
        If lngLengthByte = 0 Then
            GetANSIStringAt = ""
        Else
            GetANSIStringAt = SysAllocStringByteLen(sz, lngLengthByte)
        End If
    End If
End Function

SysAllocStringByteLenは、OLE AutomationのAPIであり、pszでポイントされている位置に存在するNull文字で終了するANSI文字列を、BSTRに、lengthバイトだけANSI文字列のままコピーし、さらに最後にNull文字を追加し、戻す。Visual Basicでは、APIからString型が戻ると自動的にANSIからUNICODEに変換してくれるので、Visual Basic側でANSIからUNICODEに変換する必要はない。

ただし、SysAllocStringByteLenでは、ANSI文字列のバイト数が分かっていなければならないので、lstrlenAを用いる。lstrlenAは、lpStringでポイントされている位置に存在するNull文字で終了するANSI文字列のバイト数を返す。このバイト数には、最後のNull文字は含まれない。

SDKによると、lstrlenの関数個別の説明では、ANSI文字列の場合バイト数を返すと説明されており、一方、文字列取扱関数の概説(String Manipulation in Windows)では、lstrlenは、ANSIでもUNICODEでも文字数を返すとなっており、矛盾が見られるが、動作を確認してみると、前者のlstrlenの関数個別の説明が正しい。

GetANSIStringAtは、szでポイントされている位置に存在するNull文字で終了するANSI文字列をString型(UNICODE)として取り出すVisual Basicの関数である。最初に、lstrlenAを呼び出しANSI文字列のバイト数を調べ、文字列の長さがゼロでなければ、SysAllocStringByteLenを呼び出す。SysAllocStringByteLenのAPI自身は、ANSIからUNICODEの変換を行わないが、結果がVisual Basicの渡される際に変換が行われる。

Null文字で終了しているUNICODE文字列の場合

Private Declare Function lstrlenW _
                         Lib "kernel32" _
                        (ByVal lpString As Long) _
                         As Long

Private Declare Sub RtlMoveMemory _
                         Lib "kernel32" _
                        (ByVal dest As Long, _
                         ByVal src As Long, _
                         ByVal count As Long)

Public Function GetUNICODEStringAt(ByVal sz As Long) As String
    Dim lngLengthChar As Long

    If sz = 0 Then
        GetUNICODEStringAt = vbNullString
    Else
        lngLengthChar = lstrlenW(sz)
        If lngLengthChar = 0 Then
            GetUNICODEStringAt = ""
        Else
            GetUNICODEStringAt = String$(lngLengthChar, vbNullChar)
            RtlMoveMemory StrPtr(GetUNICODEStringAt), sz, lngLengthChar * 2
        End If
    End If
End Function

lstrlenWは、lpStringでポイントされている位置に存在するNull文字で終了するUNICODE文字列の文字数を返す。この文字数には、最後のNull文字は含まれない。UNICODEの場合、1文字が必ず2バイトである。

RtlMoveMemoryは、srcでポイントされるアドレスから、destでポイントされるアドレスへ、メモリの内容をコピーする。コピーするデータの長さがcountバイトである。コピー元の領域(srcからsrc+count-1)とコピー先の領域(destからdest+count-1)に重複があっても、正しくコピー元の内容がコピー先に移る。

GetUNICODEStringAtは、szでポイントされている位置に存在するNull文字で終了するUNICODE文字列をString型(UNICODE)として取り出すVisual Basicの関数である。最初に、lstrlenWを呼び出しUNICODE文字列のバイト数を調る。文字列の長さがゼロでなければ、GetUNICODEStringAtの戻り値として文字数分の領域を確保した上で、RtlMoveMemoryにより、szから確保した領域へ内容のコピーを行う。

Null文字で終了するUNICODE文字列をBSTRに格納するだけであれば、OLE AutomationのSysAllocString系のAPIが使えるが、このBSTRがVisual BasicにString型として戻る際に、余計なANSI-UNICODEの変換が行われるので、内容が破壊されてしまう。このため、SysAllocString系のAPIが使えないので、ここで紹介したような方法を採用せざるを得ない。

ここでは、StrPtrという、Visual Basicの隠し関数を使用しているが、これを嫌う場合は、次のようなコードとなる。

Private Declare Function lstrlenW
                         Lib "kernel32" _
                        (ByVal lpString As Long) _
                         As Long

Private Declare Sub RtlMoveMemory _
                    Lib "kernel32" _
                   (dest As Any, _
                    ByVal src As Long, _
                    ByVal count As Long)

Public Function GetUNICODEStringAt(ByVal sz As Long) As String
    Dim lngLengthChar As Long
    Dim bytBuffer() As Byte

    If sz = 0 Then
        GetUNICODEStringAt = vbNullString
    Else
        lngLengthChar = lstrlenW(sz)
        If lngLengthChar = 0 Then
            GetUNICODEStringAt = ""
        Else
            Redim bytBuffer(1 to lngLengthChar * 2)
            RtlMoveMemory bytBuffer(1), sz, lngLengthChar * 2
            GetUNICODEStringAt = bytBuffer
        End If
    End If
End Function

リストビューコントロールで、アイテムへのダブルクリックを拾う (VB5)
ただし、リストビューコントロールのMultiSelectがFalseの場合

リストビューコントロールのClickに関するイベントプロシージャは、

  1. ItemClick:クリックされたアイテムが引数となる、
  2. Click:クリックされた位置は不明、
  3. DblClick:クリックされた位置は不明、

の三種類である。アイテム上でダブルクリックした場合には、1.でクリックされたアイテムがSelected=Trueとなり、2.、3.のイベントプロシージャ続けて実行される。アイテムと無関係な位置がダブルクリックされたときには、すべてのアイテムがSelected=Falseとなり、2.と3.のイベントプロシージャが実行される。(VB6の場合、すべてのアイテムがSelected=Falseとなることはない。アイテムと無関係な位置がダブルクリックされた場合でも、すでに選択されたアイテムが選択されつづける。)

したがって、DblClickにおいて、リストビューのListItemsの個々のItemをチェックし、Selected=Trueのものがあれば、このItemに対して処理を行う。Selected=Trueのものがなければ、処理を行わない。(VB6の場合、リストビューコントロールのSelectedItemプロパティで、選択されているListItemへの参照を得ることができる。)


frxファイル

frxファイルはfrmファイルに付随して生成されるが、frmファイルに関連するバイナリデータを格納している。例えば、フォームモジュールに貼り付けられたイメージリストコントロールの画像は、frxファイルに格納されている。

したがって、frmファイルを「部品」として取り扱う場合、同名のfrxファイルが生成されていれば、必ず一体として取り扱わなければならない。普通、frxファイルはfrmファイルと同じディレクトリに生成される。