counter

VB4なんでもガイド・その2   

Visual Basic Version 4 のあれこれについて気の向くままを書きます.
時には、VB4から離れてVBAなどにも浮気心を持つかも知れません.

このページは個人的な学習用として記述したものです.
したがって、このページをご覧頂いた結果について作者は一切の責任を負いません.

フォント・サイズは、"小" を推奨いたします.
印字は、ページ設定でA4縦、左マージン18mm、右マージン0mm、さらにプリンタドライバで86〜80%の
縮小をしてください.



======= 10)Timer の考察の章 ================================

VBで用意されているタイマーは、1/1000 秒単位でイベントを発生できるので一般的な用途には 十分に使用できるであろう.

使用方法は、

  1. ツールボックスから Timer を選択して Form に置く.
  2. 時間になったらどうするかの処理を書く.
  3. タイマーを起動するおまじないをかける.
  4. 処理が終わったらタイマーを止める.

と、こんなものだが多少の工夫が必要である.

まず、2の時間になったらの処理であるが、用意されているタイマーのイベント処理用の ステートメント Private Sub Timer1_Timer() には、ごちゃごちゃとした流れは書くべきではない.
ここには、"時間だよん" が分かる程度のフラグを設定して即、終了する. 時間になったからどうするかは 本筋の流れを持ったメイン側でこのフラグを見て分岐処理しよう.

つまり、3のタイマー起動の際にグローバル変数を用意し、TmOut = False などとしておく.
そして Timer1_Timer() には、TmOut = True とする.
メイン側では、TmOut を監視しておれば所定の時間が経過したかどうかはすぐに分かる.

' フラグの初期化
TmOut = False
' 時間の設定
Timer1.Interval = 3000
' タイマーの起動
Timer1.Enabled = True
' フラグを監視して時間になればループから抜ける
Do While Tmout = False
  ・
' 時間内に所定のデータが取得できれば
' ループを抜ける

 If msg = dat Then
  Exit Do
 End If
  ・
  ・
Loop
' タイマーを止める
Timer1.Enabled = False
If TmOut = True Then
' タイムアウトの処理
Else
' 正常終了時の処理
End If

Form1 のタイマーイベント処理

Private Sub Timer1_Timer()

' フラグの設定
 TmOut = True

End Sub

上の話しを元にサンプルプログラムを書けばこんなものになる.
しかし、このサンプルプログラムは動かない.

このままでは Timer1_Timer() が処理されないからである.
時間が経過するとタイマーイベントが発生するがシステムはキューに放り込んで置くだけで> 他のプログラムが実行中はその処理を保留している.
そこで、現在実行中のプログラムはシステムに対して "溜まっているキューを処理して下さい" と お願いする必要がある.

Do While Tmout = False
  ・
  ・
 Call DoEvents
Loop

こんな感じです. DoEvents の詳細は、HELP を見てください.
このイベントに関する考え方は、VBにおけるすべてのイベントに共通である.
また、画面に表示する事も、一つのイベントである.

TOPに戻る

======= 11)Timer の考察・その2の章 ========================

Timer1.Interval は、Integer 型のようである. つまり設定できる値は、32,767 mS までとなる.
つまり、30秒程である. モデムを相手とする場合は通常のATコマンドであればほぼ瞬時に "OK" の 応答が返ってきます. 少し時間の掛かるもの(ATZ や AT&W など)でも1秒から2秒もあれば大丈夫でしょう.
この範囲であれば特別な細工もなしに使用できます.
ただ、モデムの場合に特別な例としてダイヤルコマンドがあります. これは最大50秒間応答がありません.

そんな時のためのサンプルプログラムです. このプログラムでは設定時間を秒単位で与えます.

Dim TmOut As Boolean
Dim TmHi As Integer
Dim TmLo As Integer

 '
 ' 機能   タイマーの起動
 '       起動時は TmOut を False と設定している.
 '       tm で指定した時間経過後は TmOut が True となる
 '       約 3200 秒までの時間設定が可能である
 ' 入力条件  tm ... 設定時間(単位 秒)
 '

Sub RsTimerOn(tm As Integer)

 ' 30 秒のタイマーを繰り返した後
 TmHi = tm \ 30
 ' 残りの時間を設定する
 TmLo = tm Mod 30
 TmOut = False
 ' いったん、タイマーを止めて
 Form1.Timer1.Enabled = False
 Call RsTimerInterval

End Sub

 '
 ' 機能   タイマー割り込み時の処理
 '

Sub RsTimerInterval()

 Dim tms As Integer

 ' TmHi に残りがあれば、それを使用する
 ' TmHi は、30,000 mS です

 If TmHi <> 0 Then
  tms = 30000
  TmHi = TmHi - 1
 Else
 ' TmHi が 0 であれば TmLo を調べます
  If TmLo = 0 Then
 ' これも 0 であれば規定の時間が経過した事になります
 ' 規定の時間が経過している場合はタイマーを止め、

   Form1.Timer1.Enabled = False
 ' フラグを True にして上位プログラムへ規定の時間が経過したことを伝えます
   TmOut = True
   Exit Sub
  End If
 ' TmLo に値があればこの値を mS に直して設定します
  tms = TmLo * 1000
 ' TmLo の値は2度使用される事はありません
  TmLo = 0
 End If
 ' タイマーに新しい時間を設定します
 Form1.Timer1.Interval = tms
 Form1.Timer1.Enabled = True

End Sub

 '
 ' 機能   タイマーの停止
 '

Sub RsTimerOff()

 Form1.Timer1.Enabled = False

End Sub

Private Sub Timer1_Timer()

 Call RsTimerInterval

End Sub

上のプログラムを若干改造して TmOut、TmHi、TmLo および Form1.Timer を配列にすれば 簡単な変更で複数のタイマーを使えるようになる.

TOPに戻る

======= 12)Sleep の章 ======================================

DoEvents を HELP で調べていると "DoEvents に代わる Sleep API の使用" と題した項目に巡り会う.

そこには、こんな事が書かれている.

32 ビット版の Visual Basic では、コードの中で "イベント待ち状態" を実現するためには、DoEvents を 使用するよりも、Sleep API 関数を使用するほうがより適切です。この関数の宣言の方法は次のとおりです。

Declare Sub Sleep Lib "kernel32" Alias "Sleep" (ByVal dwMilliseconds As Long)

Sleep 関数を呼び出すには、次の形式のコードを使用します。

Call Sleep(1000)

信じる人は、"おお、成る程" と Timer の考察の章で見た、Call DoEvents を Call Sleep(100) と 書き換えるであろう.
そして、2〜3分後に "何いぃ〜〜!!" と、大きな声を上げているであろう.
信じるのは良いが、騙されてはいけない. この記述に騙された人は数限りない.
何が適切なんだ. 動かなくなるくせに. つまり、現時点では DoEvents の代替えに Sleep は使用できない.

しかし、指定した時間プログラムの動作が停止する方法を手に入れた訳だから許そう.

これには後日談がある.
キー入力待ちを行うのに下記のように DoEvents のみを利用すると CPU の使用率が 100% などと驚異的な値になってしまう.

' CPU の使用率が 100%
Do While KeyInFlg = True
 DoEvents
Loop
こんな場合は、Sleep() が有効に利用できる.

' 正常な CPU の使用率
Do While KeyInFlg = True
 Call Sleep(100)
 DoEvents
Loop
TOPに戻る

======= 13)基本的なものの章 ================================

入出力を行うプログラムの基本として、

  1. 使用を開始する.
  2. 一文字出力. これの拡張として文字列の出力.
  3. 一文字入力. 同じく文字列の入力.
  4. 使用を終わる.

の4つがあれば何とかなる. 上の4つをそれぞれの関数に置き換えれば察しは付くであろう.
使用を開始するは、Open であり、Close は、使用を終わる時に用いる.

VBからRS−232Cを使用するための基本的な関数を作ってみた.

サンプルプログラムには、

  1. RsOpen
  2. RsOut
  3. RsSend
  4. RsGet1c
  5. RsGetMsg
  6. RsClose

の各関数から構成されている.
あと、おまけとして10、11章でのタイマー関数も付属しいてる.

詳細に付いては、 RsUty.BAS の中のコメントを見て欲しい.
また、RS−232Cのポート2にモデムをつないでこのプログラムを実行すれば モデムに "AT" のコマンドを送り、その応答を取り込むことが出来る.

なお、このプログラムは下記の書籍を参考にしています.

    Win32 システムサービスプログラミング
    (株)プレンティスホール出版
    ISBN4-88735-030-9
    定価5,800円

第12章にCで書いた通信の参考例があります.
この内容をもとにRS−232CをVB4から使えるようにしております.
この本は、CからAPIを使用する事を目的に書いていますがVB4から APIを呼ぶような作りにすれば、かなりな事の参考書として使えます.

ダウンロード 準備中です. しばらくお待ちください.

TOPに戻る

======= 14)起動したプログラムの監視の章 ====================

どんな場合に必要となるのかはよく分からないが知っていればきっと何かの役に立つ時が来るかも知れない.
Aと言うプログラムからBなるプログラムを起動して、Aは、Bが存在するかどうかを監視するのが今回の サンプルである.
繰り返すが、私はどんな場合に必要となるのかは知らない. しかし、その方法を知っている所を見ると きっと、使わなければならないハメに陥った事があるのだろう.

方法は、

  1. VBの Shell() 関数で目的のプログラムBを起動する.
  2. API を使用して Shell() 関数で得られたプロセスIDをプロセスハンドルに変換する.
  3. 同じく API を使用してプログラムBのプロセスの状態を取得する.
  4. プロセスの状態が終了でなければ 3 から繰り返す.

プログラムBが消滅した理由を適当に判断した後、1 から繰り返せば永久に不滅である.

ダウンロード
サンプルプログラムです. PSS.Bas 2KB

TOPに戻る

======= 15)Timer の考察・その3の章 ========================

タイマーは、どの時点で起動されるのだろうか?
その解を得るために次の例題を考えてみよう.

一文字ずつ入力するルーチンがある.
最初の文字は10秒以内に入力しなければならない.
それ以降の各文字も同様にそれぞれ10秒以内に入力しなければならない.

     タイマー タイマー   タイマー
     起動   再起動    再起動      タイムアウト
START   |-------->|------------->|----------------->|
         "A"      "B"

プログラムの開始により10秒のタイマーが起動する.
適当な時間経過後 "A" の入力があったので、10秒のタイマーをもう一度起動する.
"B" の入力があったので同様にタイマーを起動する.

それでは、さっそくプログラムを.

Global TmOut As Boolean

TmOut = False
Form1.Timer1.Interval = 10000
Form1.Timer1.Enabled = True
Do While TmOut = False
  ・
  ・
 dat = 適当な入力ルーチン()
 If dat <> "" Then
 ' 何か文字が入力されたのでタイマーを延長する
  Form1.Timer1.Interval = 10000
  Form1.Timer1.Enabled = True
 End If
  ・
  ・
 Call DoEvents
Loop
If TmOut = False Then
 Call MsgBox("正常に終わった.")
Else
 Call MsgBox("なぜかタイムアウトで終わった.")
End if
End

Private Sub Timer1_Timer()
 TmOut = True
End Sub

10、11章で通い慣れた道なので、これぐらいは朝飯前である.

さて、デバッグ.

ン? 文字を入力しているにもかかわらずプログラム起動後10秒で なぜかタイムアウトで終わってしまう.
タイマーを延長するの部分を通っていないのか? いや、しっかり通っている.
DoEvents も入れた. はて?

Timer イベントの HELP を見ると下記のように解説されている.

Timer イベントを発生させるには、タイマー コントロールの Enabled プロパティを 真 (True) に設定し、さらに Interval プロパティを 0 を超える値に設定しておく 必要があります。

この説明を見る限り、上のプログラムは期待通りに動作していいはずである. が、
動作しなかった.

もう一度 HELP を見てみよう.
よく見ると、こう書いてある.

Timer イベントを発生させるには、タイマー コントロールの Enabled プロパティを 偽(False)から真 (True) に変化し、さらに Interval プロパティを 0 を超える値に 設定しておく必要があります。

そこでタイマーを延長するの部分をよく見れば、Enabled は True のままで変化して いないことになる. よって、

 If dat <> "" Then
 ' 何か文字が入力されたのでタイマーを延長する
  Form1.Timer1.Enabled = False
  Form1.Timer1.Interval = 10000  ' この行は無くても結果は同じです
  Form1.Timer1.Enabled = True
 End If

と書き換えてメデタシ、メデタシ.

TOPに戻る

======= 16)バイナリ・データの取り扱いの章 ==================

ひと月ほど悩んでいたことが解決したので、その事例を紹介しながらVBでのバイナリ・データの取り扱い 方法について極めてみる.

何を悩んでいたかと言えば、話せば長くなるので簡単に.
2年程前に一世を風靡したものにVOICEモデムなるものがある. 最近は姿を見かけなくなった.
最近のものはデータの隙間に音声データを紛れ込ませる方式に代わっている.
登場するのは、2年前のタイプのVOICEモデムである. このモデムに特定のコマンドを与えて置いて データをドドッと送ってやると、モデムはそのデータを音声に変換して相手先へ送ってくれる.
このデータは、バイナリ・データであり &H00 から &HFF までの値が含まれている.

モデムに付属してきたサンプルプログラムでは、きっちりとした音声で聞こえるのにVBで自作したプログラムからは ひどい音しか伝わってこない.

当初からモデムに渡したデータに何か違いがあるのでは、と見当を付けていたが、つい先日その具体的なしっぽを押さえる 事に成功した.

そのデータは xxx.VOX としてファイル上にある. その大きさは、1,328 バイトである.
モデムには、そのデータを頭から順に送り込めばいい. 最後の1バイト2バイトは誤差の内なのであまり気にしない.

問題のあった例

Declare Function WriteFile Lib "kernel32" Alias "WriteFile" _
  (ByVal hFile As Long, ByVal lpBuffer As String, ByVal nNumberOfBytesToWrite As Long, _
   lpNumberOfBytesWritten As Long, lpOverlapped As String) As Long

Dim bf As String

Open "xxx.VOX" For Binary As #1
bf = String(1328, " ")
Get #1,,bf
Close #1
  ・
  ・
Call WriteFile(comm, bf, Len(bf), wcnt, 0)   プログラム1

Open から Close までは、一連の手順である. Get #1,,bf により xxx.VOX から bf に 1,328 バイト(注1)読み込んで くれる.
しかし、意図した通りのデータを読んでくれない.
Get #1,,bf で読み込んだ直後の最初の何バイトかの値を下記に示す. アンダーラインを引いた所に注目してほしい.

7F 91 57 E9 48 49 64 D7 89 C8 25 81 45 B7 06 C7
68 A7 39 B5 4C 68 95 AB C9 65 81 45 27 9A 98 3D
82 8D 97 46 8A 86 7B A2 5D A6 56 E7 57 6A B6 28
81 45 BA 54 6D 83 7B 81 45 63 7B 83 C8 76 5B 13  データ1
^^^^^

成功例
Declare Function WriteFile Lib "kernel32" Alias "WriteFile" _
  (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToWrite As Long, _
   lpNumberOfBytesWritten As Long, lpOverlapped As String) As Long

Dim bf(1328) As Byte

Open "xxx.VOX" For Binary As #1
Get #1,,bf
Close #1
  ・
  ・
Call WriteFile(comm, bf(1), 1328, wcnt, 0)   プログラム2

これも、同様に Get #1,,bf で読み込んだ直後の値をダンプしてみる.
前のダンプとは異なる結果になっている. しかし、前の値は誤りでありこちらの値が正しい.

7F 91 57 E9 48 49 64 D7 89 C8 25 98 89 B7 06 C7
68 A7 39 B5 4C 68 95 AB C9 65 86 63 27 9A 98 3D
82 8D 97 46 8A 86 7B A2 5D A6 56 E7 57 6A B6 28
86 81 BA 54 6D 83 7B 87 B9 63 7B 83 C8 76 5B 13  データ2
^^^^^

教訓.
バイナリ・データ(特に &H80 以上の値を含む物)は、Byte 型を使うべきであり、間違っても String 型を 使ってはならない.

蛇足だが、Declare で、2番目の引数の宣言が異なっている事に注目して欲しい.
プログラム1は、String を渡しているので ByVal lpBuffer As String としている.
プログラム2では、配列のポインタを渡したいので lpBuffer As Any としている.

ここにたどり着くまでの裏話.

当初、モデムに与えるコマンドの誤りでは、との疑問から出発したがすぐにデータの内容が変ではと気になりだした.

モデムの入り口にオンラインスコープを接続してどんなデータがモデムに渡っているのかを確認すれば問題の 絞り込みは簡単である. しかし、手元にない.

次に WriteFile API にどんなデータが渡っているのかを知りたくなる. この解決はソフト的に可能である.
まず、自作のDLL作成しVBからこれを呼ぶ、その中でやってきたデータをファイルに吐き出しつつ WriteFile API を呼べばいい.

結果は、自作のDLLにデータが渡された時点ですでにデータが化けている事が判明する.
StrConv(bf, vbFromUnicode) などとやってみるが、いっこうに変わりない.

そこで、Get で取り込んだ時点で既に化けているのであろうとはたと気づく.

その結果が先のダンプである.

発想の逆転で、Get で取り込んだ値から調べていれば結果が出るのがもっと早かったであろう.
しかし、API と言う呪縛に縛られて遠回りをしたようだ.
モデムの入り口(RS−232Cの回線上)や API などの通常は見えない所に疑問が行き簡単に見える場所を 見過ごした結果といえる.

注1:
正確には、1,328 バイトではない. 1,328 文字である. 全角文字であっても 1 文字と数える.
つまり、取り込んだデータはこのあたりから既にあやしいのである.

ちなみに、プログラム1で WriteFile API に渡されたデータと StrConv(bf, vbFromUnicode) とした値とは 同じ物であった.(当然の事だが...)

TOPに戻る

======= 17)Option Base と動的配列の章 ======================

 こんな場合は Option Base の設定は無視されるのでしょうか?

 Option Base 1

 Dim x() As Byte

 x = StrConv("ABCD", vbFromUnicode)
 MsgBox x(0) ... 65 と表示

 Option Base 1 と宣言しているので x(0) からではなく
 x(1) から代入されても良さそうな気がするのですが...
 使用しているのはVB4です.

 そぼくな疑問です.

----------------------------------------------------------------------------

教訓.
Option Base 1 と設定しても添え字は必ず 1 から始まるとは限らない.
状況によって変化するので良く確認しよう.

影の声.
x = StrConv("ABCD", vbFromUnicode)
MsgBox x(0) ... 65 と表示

Option Base 1 と宣言しているので x(0) からではなく
x(1) から代入されても良さそうな気がするのですが...

これって、VBのバグと思うのは私だけ?

TOPに戻る

======= 18)VBの TextBox にDLLから表示するの章 =========

APIについて調べているとウィンドウのハンドルとかデバイスコンテキストとかよく理解できない 言葉が羅列されている.
まあ、これが取っつきにくくしている最大の原因であろうと考える.

愚痴はこれぐらいにして.

まず、必要なことは、VBでの TextBox のウィンドウ・ハンドルである.
これは、VB側でそのための準備をしてくれている.

 Form1.Text1.hWnd

とすれば、目的の TextBox のウィンドウ・ハンドルを簡単に取得できる.

後は、これをDLLに渡して表示すればよい.
DLLのどこかに下のような関数を一つ用意しておこう.

#include <windows.h>
#include <windowsx.h>
#include <stdio.h>

/*
* 機能   指定されたウィンドウが EditBox とみなし、文字列を表示する
* 入力条件 hwnd ... EditBox のウィンドウ・ハンドル
*      msg ... 表示する文字列
*/

void PutMsg( HWND hwnd, LPCTSTR msg )

{
  int chsz;

  // EditBox 内の文字列の長さを取得する
  chsz = Edit_GetTextLength( hwnd );
  // 表示開始位置を表示されている文字列の最終位置とする
  Edit_SetSel( hwnd, chsz + 1, chsz + 1 );
  // 新しい文字列を追加する
  Edit_ReplaceSel( hwnd, msg );
}

上で使用している Edit_***() は、windowsx.h で定義されたマクロです.
windowsx.h の内容をよく見ているとVBの SelText や Sel*** のプロパティに非常によく似ていることに 気が付きます. VBもこれを呼んでいるのだろうか?

改行する場合は、CR/LF を続けて送出します.

PutMsg( hwnd, "ABCDE\r\n" );
PutMsg( hwnd, "本日は\r\nよい天気です\r\n" );

TOPに戻る

======= 19)VBからプログラムの起動の章 ====================

第14章 起動したプログラムの監視 でVBからプログラムを起動し、終了するまでを監視する 例を説明したが、ここでは起動・終了を相手まかせではなく、起動したプログラムにもっと積極的に 手をだしてみることにします.

何か例題があった方が話しが早いのでVBから SCANDISK を起動し、各パラメータの設定、実行、 終了までをVB側から制御します.

その手始めとして実行ファイルを起動してそのプログラムの初期化が終るのを待つ事とします.

  1. SCANDISK の起動

    VBの中から実行ファイル(つまり EXE)を起動するには Shell 関数を使います.
    これを使う注意はただ一つ、ファイルを起動するとただちに Shell 関数は次の命令へ進むこと です.

    実行ファイルを起動してそのプログラムの初期化が終るのを待つ関数を用意しました.
    exeName には、"SCANDSKW.EXE E: F:" のように起動するファイル名と引数(ここではドライブ名を2つ)を 指定します.

     ' 既存のプロセスハンドルを取得する API の宣言
    Declare Function OpenProcess Lib "kernel32" _
             (ByVal dwDesiredAccess As Long, _
                 ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
    Public Const PROCESS_QUERY_INFORMATION = &H400&
     ' オープンされているオブジェクトハンドルをクローズする API の宣言
    Declare Function CloseHandle Lib "kernel32" _
             (ByVal hObject As Long) As Long
     ' 指定されたプロセスの初期化が終了するまで待機する API の宣言
    Declare Function WaitForInputIdle Lib "user32" _
             (ByVal hProcess As Long, ByVal dwMilliseconds As Long) As Long

     '
     ' 機能   プロセスを起動し、そのプロセス初期化が終了するまで待機する
     ' 入力条件 exeName .. 起動するファイル名とパラメータを指定する
     ' 出力条件 True : 正常終了
     '      False : 異常終了
     '

    Function MkProcess(exeName As String) As Boolean

     Dim ProcessId As Long
     Dim ProcessHd As Long
     Dim ExitCode As Long

     ' 実行ファイルを起動する
     On Error Resume Next
     ' ごく普通の状態で起動させる
     ProcessId = Shell(exeName, vbNormalFocus)
     On Error GoTo 0
     ' プロセスのハンドルを取得する
     ProcessHd = OpenProcess(PROCESS_QUERY_INFORMATION, False, ProcessId)
     ' プロセスを作成できたか確認する
     If ProcessHd = 0 Then
       Call MsgBox(exeName & " : 起動出来なかった." & vbCrLf, vbOKOnly + vbCritical)
       MkProcess = False
       Exit Function
     End If
     ' プロセスの初期化を最大2秒間待つ
     Call WaitForInputIdle(ProcessHd, 2000)
     ' システムに制御を渡す
     Call DoEvents
     ' プロセスオブジェクトのハンドルをクローズ
     Call CloseHandle(ProcessHd)
     MkProcess = True

    End Function

SCANDISK が無事起動しました.
次章へつづく.

TOPに戻る

======= 20)SendKeys によるパラメータの設定の章 =============

  1. SCANDISK のパラメータの設定を行う

    SCANDISK が無事起動したので各パラメータの設定を行うこととします. チェックするドライブは 起動時の引数として渡してありますので画面上で既に反転状態になっているはずです.

    設定内容は下記の通りです.

    詳細
    結果の表示  → しない
    ログファイル → 上書き
    クロスリンクファイル → コピーを作成
    破損ファイルの断片  → ファイルに変換
    ファイルのチェック内容 → 無効なファイル名 → チェックあり
                → 無効な日時データ → チェックあり
    ホストドライブを先にチェックする → チェックあり

    チェックする方法   → 標準
    エラーを自動的に修復 → チェックあり

    SCANDISK は、先の章で紹介した MkProcess 関数の中で使用している Shell 関数のパラメータ vbNormalFocus によりフォーカスを得た状態となっていますので設定を行う最も簡単な方法は Sendkeys によりキーデータを送ってやることです.

    Call SendKeys( "A" )
    これで詳細のコマンドボタンを押したのと同じ結果になります.

    また、結果の表示の選択では
     する(A)
     しない(E)
     エラー時のみ(O)
    と、なっているので
    Call SendKeys( "E" )
    と、すれば "しない" を選択したことになります.

    SendKeys() を使用する上での注意は、適時 DoEvents を実行してシステムに制御を渡してあげる必要が あります.

    択一式の OptionButton は、この方法で特に問題なく実行できます.
    CheckBox は、少々曲者です. なぜなら、トルグになっていてキーデータを送る度にチェックあり、なしが 入れ替わってしまうからです.

次章へつづく.

TOPに戻る

======= 21)setCheckBox によるパラメータの設定の章 ==========

そこで次のような関数を用意します. CheckBox や OptionButton を設定する関数です.

引数は、親ウィンドウ名とチェックを設定するタイトル名、それと設定情報です.

親ウィンドウ名とは、ウィンドウの左上の文字列(Caption)です. ここでは、"スキャンディスクの詳細オプション" とか "スキャンディスク - (E:)" などです. これは正確に記述する必要があります.

また、チェックを設定するタイトル名とは、CheckBox や OptionButton の右となりの文字列です. ここでは、 "無効なファイル名" とか "エラーを自動的に修復" などです.
これには、前方一致で判断をしていますので識別できるまでの文字列を設定します.

親ウィンドウ名とチェックを設定するタイトル名は、半角、全角を正確に調べて書く必要があります.

この関数は何をしているかと言うと、CheckBox や OptionButton のウィンドウ・ハンドルを使用して SendMessage API により CheckBox や OptionButton の設定を行います.

Call setCheckBox( "スキャンディスク - (E:)", "エラーを自動的", True )
こんな感じで使用します.

注意は、frame で囲われた OptionButton をマウスでクリックしたり Sendkeys でキーデータを送った場合には 択一式に他のチェックは消えます.
setCheckBox 関数では目的の OptionButton にチェックを入れることはできますが、frame で囲われた他の OptionButton を自動的に消すことは出来ません.
したがって、他の OptionButton には、False を指定してチェックを解除する必要があります.

' 指定されたウィンドウへメッセージを送信する API の宣言
Declare Function SendMessage Lib "user32" Alias "SendMessageA" _
         (ByVal hwnd As Long, ByVal wMsg As Long, _
             ByVal wParam As Long, lParam As Long) As Long
Public Const BM_SETCHECK = &HF1

 '
 ' 機能   Check Box を設定する
 ' 入力条件 winName .. 親ウィンドウ名
 '      checkName . チェックを設定るタイトル名
 '      dat   ... 設定情報
 '            True ... チェックを付ける
 '            False ... チェックを外す
 '

Sub setCheckBox(winName As String, checkName As String, dat As Boolean)

 Dim hnd As Long
 Dim chkdat As Long

 ' 目的のタイトル名を持つ子ウィンドウ・ハンドルを取得する
 hnd = getWinHnd(winName, checkName)
 If hnd <> 0 Then
 ' 情報の設定
   chkdat = 1 ' VBにはなぜ3項演算式がないのだ
   If dat = False Then
     chkdat = 0
   End If
   Call SendMessage(hnd, BM_SETCHECK, chkdat, 0)
   Call DoEvents
 End If

End Sub

getWinHnd() 関数とは何だ. との疑問を残して次章へつづく.

TOPに戻る

======= 22)親ウィンドウ、子ウィンドウ、孫ウィンドウの章 ===

下の関数は、ウィンドウ・ハンドルを取得します.

基本的な動作は、指定された親ウィンドウの中にある子ウィンドウを全数調べます.
親ウィンドウの指定は、ウィンドウ名を指定します. ウィンドウ名とは、ウィンドウの左上のバーにある 文字列です. VBの Form で Caption と呼ばれているところです.

ウィンドウ名が指定されていない場合は、デスクトップを親ウィンドウとみなします.
この場合は、デスクトップに浮かぶ各ウィンドウが子ウィンドウとなります.

ウィンドウ名を指定した場合は、デスクトップに浮かぶ各ウィンドウをウィンドウ名で一つ特定し、 これを親ウィンドウとしてその中にある細々としたウィンドウを子ウィンドウとみなします.
この段階での子ウィンドウは、デスクトップからみれば孫ウィンドウとなります.

さて、この細々としたウィンドウとは何の事かと言えば、VBの Form を思い出してください.
Form の部分がデスクトップから見たら子ウィンドウとなります. さらに、この Form に色々なコントロールを 貼り付けて Form の機能を作り上げていきます. この貼り付けたコントロールが一つのウィンドウとなります.
これは、Form から見れば子であり、デスクトップから見れば孫ウィンドウとなります.

そこで Form を識別するものは、Caption に書かれた文字列です. 細々としたコントロールのウィンドウを 識別するのもやはりそこに書かれた文字列です. Caption については説明するまでもないと思いますので Form に OptionButton や CommandButton を追加した場合を例に取りましょう.

SCANDISK を立ち上げると

 "チェック方法" と書いた Frame
 "標準"     と書いた OptionButton
 "完全"     と書いた OptionButton
 "オプション"  と書いた CommandButon

等々がみれます. これらがそれぞれすべて一つのウィンドウです. そして "チェック方法" とか "標準" などの文字列が 識別する文字列となります.

Windows は、全てのウィンドウにそれを区別するハンドルを与えて管理しています. 言い換えれば、> "標準" と書いてある OptionButton のハンドルを取得することができれば、このハンドルを使って OptionButton にチェックを入れたり外したりと自由に操作ができる訳です. もちろん "標準" とある文字列を 別物に置き換えることも.

この関数では、指定した親ウィンドウのすぐ下にある子ウィンドウのみを探します.
したがって、親ウィンドウとしてデスクトップを指定すれば、そこに浮かぶウィンドウの一つを検索できます.
親ウィンドウとして具体的なウィンドウ名を指定すれば、Form の中に貼り付けたコントロールの一つを検索できます.

親ウィンドウのウィンドウ・ハンドルを取得するには、ウィンドウ名(Caption)を引数として FindWindow API を 実行します.
ただし、ウィンドウ名が指定されていない場合は、GetDesktopWindow API によりデスクトップのハンドルを取得し、 これを親ウィンドウとみなします.

子ウィンドウの最初のものを探すには、親ウィンドウのハンドルと GW_CHILD を引数として GetWindow API を 実行します.
また、複数の子ウィンドウを検索するには、同格のウィンドウ・ハンドルと GW_HWNDNEXT を引数として同じく GetWindow API を実行します.

それぞれのウィンドウが持っている文字列(Caption とか CheckBox の右側の文字列など)は、ウィンドウ・ ハンドルを引数として GetWindowText API を実行します.

つまり、GetWindowText API で得られた文字列とチェックを設定するタイトル名(この関数での引数は keywd)が 一致したウィンドウのハンドルが求めるハンドルとなります.

'
' 機能   winName で指定された親ウィンドウ内にある子ウィンドを検索する
'      特定のキーワードを持つ子ウィンドウのハンドルを取得する
'      winName が "" の場合は、デスクトップを親ウィンドウとみなします
' 入力条件 winName .. 親ウィンドウ名
'            "" の場合は、デスクトップを親ウィンドウとする
'      keywd ... キーワード
' 出力条件 キーワードを持つウィンドウのハンドル
'      0 の場合は該当するウィンドウがなかった
'

Function getWinHnd(winName As String, keywd As String) As Long

 Dim hwnd As Long
 Dim lngHWnd As Long
 Dim msg As String

 ' 目的のウィンドウを検索する
 If winName = "" Then
 ' デスクトップにあるウィンドウを検索する
   hwnd = GetDesktopWindow()
 Else
 ' 指定されたウィンドウ内を検索する
   hwnd = FindWindow(vbNullString, winName)
 End If
 If hwnd <> 0 Then
 ' 最前面にある子ウィンドウを識別するハンドルを取得する
   lngHWnd = GetWindow(hwnd, GW_CHILD)
   Do While lngHWnd <> 0
     msg = String(100, Chr(0))
 ' ウィンドウのタイトルを取得する
     if GetWindowText(lngHWnd, msg, 100&) <> 0 Then
 ' 前方一致でキーワードとウィンドウのタイトルを比較する
       If Left(msg, Len(keywd)) = keywd Then
 ' 一致するものがあった
 ' キーワードを持つハンドルを設定します

         getWinHnd = lngHWnd
         Exit Function
       End If
     End If
 ' 次のウィンドウを取得する
     lngHWnd = GetWindow(lngHWnd, GW_HWNDNEXT)

   Loop

 End If

  ' 該当するウィンドウは見つからなかった、
 getWinHnd = 0

End Function

ウィンドウ・ハンドルがと言うより Windows が少しだけ身近になって次章へつづく.

TOPに戻る

======= 23)SCANDISK の開始と終了の章 ======================

  1. SCANDISK の開始

    Call SendKeys( "S" )
    Call DoEvents

    これでいいでしょう. "もういいでしょう." は水戸黄門です.

  2. SCANDISK の終了の確認と終了

    これも、先のテクニックが使用できます. SCANDISK の動きを注意深く見ていると "エラーを自動的に修復" の CheckBox の下に現在実行中の内容が表示されます.
    すべてのチェックが終了するとここに "完了" の2文字が表示されます.
    したがって、getWinHnd 関数を "完了" の文字列が出現するまで繰り返し呼べばいい事になります.
    運良く "完了" の文字列が見つかったら親ウィンドウを AppActivate 関数により念のために アクティブにしておいて SendKeys で C を送って SCANDISK を終了させます.
    もし、これで終わらなければ ALT + F4 で勝負してみましょう.

感想
他人の作ったアプリケーションがなぜか急に身近に感じませんか. 目的のプログラムにマウス一つ触れずに全ての操作をやってしまう事も可能でしょう.

SCANDISK をVBから起動、設定、実行、終了するサンプルプログラムを用意しています.
何かの折りには思い出してください.

ダウンロード 準備中です. しばらくお待ちください.

TOPに戻る

======= 24)複数のプログラムから共通したエリアの確保の章 ===

メモリ上に共有エリアを確保して複数のプログラムからそこに置かれたデータをアクセスしてみたいと思ったことはないだろうか?

その要望を少しだけかなえてみよう. ただし、今回はほんのさわりだけ.

今回の登場人物は、VBで作成したプログラムAとB、HDD上のテキストファイルが一つ、それとAPIを呼ぶためのDLLといったところです.

APIの役どころはこんなものです.

    CreateFile
    ファイルを開く
    CreateFileMapping
    CreateFile で開いたファイルをメモリ上にマップする ... 機能1
    既にマップされているものを使用できるようにする   ... 機能2
    MapViewOfFile
    データの読み込みと先頭アドレスを取得する
    UnmapViewOfFile
    マップの解放と更新をファイルに書き出す
    CloseHandle
    ファイルを閉じる

シナリオは、

  1. プログラムAでHDD上のファイル TEST.TXT をメモリ上に置く.

    __declspec( dllexport ) long __stdcall mapFl( char *fname )

    {
      fHnd = CreateFile( fname, GENERIC_WRITE | GENERIC_READ,
                      0, 0, OPEN_EXISTING, 0, 0 );
      mHnd = CreateFileMapping( fHnd, 0, PAGE_READWRITE, 0, 0, "MAPF" );
      mapV = ( char * )MapViewOfFile( mHnd, FILE_MAP_WRITE, 0, 0, 0 );
      return ( long )mapV;
    }

    Declare Function mapFl Lib "FM.DLL" (ByVal fname As String) As Long
    Dim ptr As Long
    ptr = mapFl( "TEST.TXT" )

    MapViewOfFile API の戻り値は、32ビットのポインタであるがVB側で保存するために
    Long 型に騙している.

  2. 同じくプログラムAでメモリ上のデータが読めるか試してみる.

    __declspec( dllexport ) void __stdcall mapRd( char *mapptr, char *msg )

    {
      strcpy( msg, mapptr );
    }

    Declare Sub mapRd Lib "FM.DLL" (ByVal ptr As Any, ByVal msg As String)
    Dim dat As String
    dat = String(512, Chr(0))
    Call mapRd(ptr, dat)
    MsgBox dat

  3. プログラムBから1でメモリ上に置いたデータにアクセスできるようにする.

    __declspec( dllexport ) long __stdcall Remap( void )

    {
      mHnd2 = CreateFileMapping( ( HANDLE )0xffffffff, 0, PAGE_READWRITE, 0, 0, "MAPF" );
      mapV2 = ( char * )MapViewOfFile( mHnd2, FILE_MAP_WRITE, 0, 0, 0 );
      return ( long )mapV2;
    }

    Declare Function Remap Lib "FM.DLL" () As Long
    Dim ptr2 As Long
    ptr2 = Remap()

  4. とりあえず、プログラムBでそのデータを読んでみる.

    Declare Sub mapRd Lib "FM.DLL" (ByVal ptr As Any, ByVal msg As String)
    Dim dat2 As String
    dat2 = String(512, Chr(0))
    Call mapRd(ptr2, dat2)
    MsgBox dat2

  5. プログラムAでメモリ上のデータを変更する.

    __declspec( dllexport ) void __stdcall mapWr( char *mapptr, char *msg )

    {
      strcpy( mapptr, msg );
    }

    Declare Sub mapWr Lib "FM.DLL" (ByVal ptr As Any, ByVal msg As String)
    Mid(dat, 5) = "マップファイルのテスト"
    Call mapWr(ptr, dat)

  6. プログラムBで変更内容を確認する.

    Call mapRd(ptr2, dat2)
    MsgBox dat2

  7. メモリを解放し変更をHDD上のファイルに反映する.

    __declspec( dllexport ) void __stdcall mapCl( void )

    {
      UnmapViewOfFile( mapV );
      UnmapViewOfFile( mapV2 );
      CloseHandle( mHnd );
      CloseHandle( fHnd );
      CloseHandle( mHnd2 );
    }

    Declare Sub mapCl Lib "FM.DLL" ()
    Call mapCl

ダウンロード 準備中です. しばらくお待ちください.

TOPに戻る

======= 25)究極のソートの章 ===============================

24章で紹介したファイルマッピングの技術をソートに応用してみます.
適当なデータの入ったテキスト・ファイルがHDDにあり、これをソートして結果を上書きします.

テキスト・ファイルをメモリ上に展開する事と、それをもう一度書き戻す事は先の章の通りなので詳細は省略します.

ポイントは、次の3つです.

  1. データを構造体としてとらえる.
    例えば、yymmdd hhnnss o pp qq が一行のデータとしてテキスト・ファイルに記述されているとすれば、メモリ上に読み込んだイメージで構造体を定義すれば下のようになる.

    typedef struct {
      char dat1[ 6 ];   // yymmdd
      char dmy1[ 1 ];   // space
      char dat2[ 6 ];   // hhnnss
      char dmy2[ 1 ];   // space
      char dat3[ 1 ];   // o
      char dmy3[ 1 ];   // space
      char dat4[ 2 ];   // pp
      char dmy4[ 1 ];   // space
      char dat5[ 2 ];   // qq
      char dmy5[ 2 ];   // 一行の終りの CR/LF
    } Sdata;

  2. MapViewOfFile の戻り値を qsort 関数の最初の引数として使用する.
  3. データの個数は、創意と工夫で何とかする.

さて、qsort 関数だが使ってみるとなかなか便利なものである.
qsort が使用する比較関数を自分で記述できる事から、複数のキーを記述し複雑なソートも簡単に行えるところが魅力である.
このあたりの細かいことはサンプルプログラムを参照してください.

ダウンロード 準備中です. しばらくお待ちください.

TOPに戻る

======= 26)VBとDLLと配列の章 =========================

構造体の配列をDLLに渡す場合にVBはどのような意味を持ったポインタを渡してくれるのかを調べてみます.

  1. すべて数値変数からなる構造体の配列の先頭の要素をDLLに渡します.

    Type Sdata
      d As Long
      e As Long
      f As Long
    End Type
    Dim dat(100) As Sdata

    Declare Sub chkArray Lib "ARRAY.DLL" (dat As Any)
    Call chkArray( dat( 0 ) )

  2. 同じく、すべて数値変数からなる構造体の配列をDLLに渡します.

    Type Sdata
      d As Long
      e As Long
      f As Long
    End Type
    Dim dat(100) As Sdata

    Declare Sub chkArray Lib "ARRAY.DLL" (dat() As Any)
    Call chkArray( dat() )

  3. 構造体のメンバーに固定長の文字列を含む場合で構造体の配列の先頭の要素をDLLに渡します

    Type Sdata
      d As Long
      e As String * 10
      f As Long
    End Type
    Dim dat(100) As Sdata

    Declare Sub chkArray Lib "ARRAY.DLL" (dat As Any)
    Call chkArray( dat( 0 ) )

  4. 構造体のメンバーに固定長の文字列を含む場合で構造体の配列をDLLに渡します

    Type Sdata
      d As Long
      e As String * 10
      f As Long
    End Type
    Dim dat(100) As Sdata

    Declare Sub chkArray Lib "ARRAY.DLL" (dat() As Any)
    Call chkArray( dat() )

確認用のDLLは下記のようなものを使用します. ブレークポイントを付けるためだけの一行のみです. 各パーターンともこのDLLを呼び出します.

__declspec( dllexport ) void __stdcall chkArray( LPSAFEARRAY FAR *arrayptr )

{
   LPSAFEARRAY FAR *p;
   p = arrayptr;
}

調べてみると下表のようになります. パターンとしては3つあるようです.
VB27の絵

大まかな雰囲気はつかめたでしょうか?
数値変数だけで構成される構造体の配列の先頭の要素を渡した場合は、実際にメモリ上に存在する配列を直接ポイントしているようです.
他の場合は、VBのワークエリアに情報をコピーしたり、または配列のヘッダーへのアドレスをVBのワークエリアに書き込んだ後、VBのワークエリアのアドレスを渡してくれるようです.

重要な事を一つ. VB4では文字列を UNICODE 型式で持っています. 下記のように単独の文字列をDLLへ渡す場合は、システムが自動的に UNICODE ←→ ANSI(ASCIZ) の変換をしてくれます.

Dim str As String
str = "ABCABC"
Call DLL( str )

これは、VBのワークエリアに UNICODE から ANSI に変換したものを作成して、そのアドレスをDLLに渡していると考えれば納得できます. この逆もしかりです.

ところが、今回のように構造体の中に文字列がある場合は、このような変換を行ってくれません.
DLL側で見えるものは UNICOE のままですから十分注意しましょう.

さて、その2やその4のように配列をDLLに渡した場合は下のAPIを使用することにより配列の各種情報を取得できます.

  1. 配列の次元
    unsigned int SafeArrayGetDim( SAFEARRAY FAR *psa );

    wk = SafeArrayGetDim( *arrayptr );
    注:chkArray の引数 arrayptr は、配列のヘッダーのアドレスを格納したVBのワークエリアを指しているので、SafeArrayGetDim API への引数としては *arrayptr となります. 以下同じ.

  2. 配列要素一つの大きさ
    unsigned int SafeArrayGetElemsize( SAFEARRAY FAR *psa );

    wk = SafeArrayGetElemsize( *arrayptr );

  3. 配列の添え字の値(下側、上側)
    HRESULT SafeArrayGetLBound( SAFEARRAY FAR *psa, unsigned int nDim, long FAR *plLbound);
    HRESULT SafeArrayGetUBound( SAFEARRAY FAR *psa, unsigned int nDim, long FAR *plLbound);

    SafeArrayGetLBound( *arrayptr, 1, &wk );

  4. 配列要素の内容を取得、設定
    HRESULT SafeArrayGetElement( SAFEARRAY FAR *psa, long FAR *rgIndices, void FAR *pvData);
    HRESULT SafeArrayPutElement( SAFEARRAY FAR *psa, long FAR *rgIndices, void FAR *pvData);

    static Sdata sd;
    i = 10;  // sd[ 10 ] の内容を取得する
    SafeArrayGetElement( *arrayptr, &i, &sd );

TOPに戻る

参考文献:
Visual Basic 4.0 データベースデザイン    株式会社オーム社
VisualBasic4.0 テクニカルガイド       株式会社翔泳社
VisualBasic4 パワフルテクニック大全集 Vol3 株式会社インプレス
Win32 API オフィシャルリファレンス     株式会社アスキー
VisualBasic4.0 テクニックライブラリ     株式会社アスキー
Win32 システムサービスプログラミング    株式会社プレンティスホール出版
VisualBasic4.0 入門 基礎編         ソフトバンク株式会社
VisualBasic4.0 入門 活用編         ソフトバンク株式会社
VisualBasic 初級プログラミング入門「上」  株式会社技術評論社
超図解 ACCESS 97 基礎編           株式会社エクスメディア
Access VBA プログラミング          株式会社オーム社


VB4なんでもガイド・その1

パソコンの小部屋メニュー
ケーブルの小部屋メニュー
資格の小部屋メニュー
総合メニュー

mailto