Visual Basic 200%活用術

おしゃべりシステムインフォメーションの作成
〜あまり知られていないVBの機能を活用しよう

アイデア+テクニック=200%

酒井 法雄 SAKAI,Norio

 Visual Basicはすでに何でもできる開発ツールである.しかし,それゆえに内容は多大であり,すべてを使いこなすことは難しい.特に,Visual Basic 4.0以降には知っているととても便利な機能ながら,あまり知られていないものも少なくない.
 ここでは,Visual Basicのあまり知られていない機能と,ActiveXコントロールやWindows APIを駆使し,さらにちょっとしたアイデアを加えた楽しいプログラムを作ってみよう.


●200%活用するための指針

  以前のVisual Basicはオモチャに近いものであり,あくまでもビギナー向けの開発ツールだった.Windowsネイティブならできることも,Visual Basicからでは実現が難しいものも少なくなかった.
 これを拡張するために,以前ならVBXコントロール,今ならActiveXコントロール(OCX)を使ったり,Windows APIを駆使したり,サードパーティのライブラリなどを追加したりした.この方向性は,今でも基本的には変わっていない.
 しかし,バージョン4.0からはC/S(クライアント/サーバー)型RDBMSへの対応がなされた他,ClassをベースとしたOLE(ActiveX)サーバーなどのコンポーネントをVisual Basicだけで作れるようになった.この流れは,バージョン5.0でActiveXコントロールが作成できるようになったり,COMまわりが強化され,さらに推し進められた.また,AddressOfなどの演算子がサポートされ,Windows APIの使い勝手も向上した.
 これらの内容を駆使すれば,Visual Basicがもっている機能を100%活用しただけで,かなりのことができるようになっている.しかし,あなたはこれらの機能を100%活用できているだろうか? 膨大になった言語仕様やライブラリを使いこなせているだろうか?
 もちろん,すべてを使いこなす必要はない.しかし,特に新しい概念の言語仕様やツールには,とても便利なものがある.そして残念ながらそれらは,派手なActiveXコントロールの作成などの影に隠れて,意外に知られていない.
 そこで,ここではこういったトピックから,知っておくと便利なものや,プログラミングの幅を広げることができるものを紹介することにしよう.
 今のあなたの活用状態を100%とすれば,これらの新しい機能を使いこなせれば,Visual Basicを150%くらいは活用できるようになるかもしれない.しかしそれでは200%にはならない.200%にするためには,機能だけではなく,アイデアが重要なのだ.たとえば,あまり役に立たないプログラムでも,「えっ,これVisual Basicで作ってあるの? 信じられない」というようにウケれば,作った人も満足できるし,これは200%活用できたと言ってもよいのではないだろうか.
 というわけで,今回はあまり知られていないコトをいくつか紹介する.最終的にはこれらを駆使し,あまり役に立たないしょうもないプログラムながら,ウケだけは狙えるものを作ってみることにしよう.


●チェックボックス付きのリストボックス
コントロールの新機能


 まずは,カンタンなところから始めよう.
 意外に知られていない機能に,リストボックスのチェックボックス表示がある.これは,Styleプロパティを「1-チェックボックス」にするだけで実現できる(図1).この形式にしておけば,複数項目を選択が用意になる.たとえば,従来はチェックボックスをたくさん配置しなくてはならなかったケースでも,これならたったひとつのコントロールで済む点がよい.
 プログラムからチェック状態を操作するには,Selectedプロパティを使う.たとえば,ListIndexが1番目のチェックボックスのチェック状態は,

List1.Selected(1)

がTrueかFalseかで知ることができる.また,コードからチェック状態を変更することも可能だ.
 ここからも分かるように,MultiSelectプロパティを「1-標準」または「2-拡張」にしたときと操作は同じだ.いくつ選択されているかを示すSelCountプロパティも使える.



●デザイン時にリストボックスの初期値を設定する
コントロールの新機能の応用

 Visual Basicで何が困るといえば,配列変数などの初期値を設定できないことだ.従来のBasicであれば,Data文に書いておき,Read文で配列に入れることができた.
 Visual Basic 5.0からは,Enum文で新しいデータ型に属する定数を書くことができるようになったが,これはあくまでも複数の定数値であり,インデックスでアクセスできるようなものではない.
 しかし,Visual Basic 5.0からは,リストボックスに初期値を入れておくことができるようになった.やり方はカンタンだ.プロパティウィンドウのListプロパティのドロップダウンリストボックスをクリックし,一行ずつ入力していく.ただし,改行は[Enter]ではなく,[Ctrl]+[Enter]キーを押すのがポイントだ.[Enter]だと,ドロップダウンリストが閉じてしまうのだ.カンタンにやるためには,メモ帳などにあらかじめ入力してあるデータをCopy&Pasteすればよいだろう(図2).

図1:リストボックスのチェックボックス表示

図2:リストボックスの初期値設定



 なお,Listプロパティだけではなく,同じようにしてItemDataプロパティも初期値を入力することができる.
 こうして初期値を入力したリストボックスを非表示にすれば,より高機能な配列のように扱うことができる.ただし,単純な配列と違ってそれなりにリソースを消費するので,注意が必要である.


● 配列の初期値をコーディングする
Variant型変数に配列を入れるツールを使ってお気楽コンバート

 リストボックスを使わないで,配列をうまく初期化する方法はないのだろうか.実は,とっておきのやり方がある.それは,Visual Basic 4.0から可能になっていたのだが,Variant変数に配列として使うことだ.
 使い方はこれもカンタンだ.ここでは,vArryというVariant型変数を宣言する.ここに,Array関数を使って,適当なデータの羅列を配列にしてしまい,代入するのである.データは文字列型でも数値型でもよい.
 以後,vArryは配列として扱うことができるのである.

Dim vArry As Variant
Dim i As Integer

vArry = Array _
("睦月", "如月", "弥生", "卯月",
"皐月")

For i = 0 To UBound(vArry)
  Debug.Print vArry(i)
Next i



 この方法はちょっとしたことをするのにはとても便利である.リストボックスにデザイン時に入力するのと違って,コードして残るのもよい.ただし,Variant型である以上は,これも内部的には余分なメモリが使われることになる.あまり大規模なものには使わない方がよいだろう.



● 連想配列のように使えるCollection
オブジェクト指向言語仕様の活用

 配列は便利なものである.何番目かが分かっていれば,それに対応するデータが得られる.しかし,逆はどうだろう? つまり目的のデータが何番目にあるかを求めたいときだ.
 このときには,必ずループを作ってひとつずつ中身を調べてゆかなくてはならない.大量のデータがあるときには,この処理は遅くなってしまうので,ソートやバイナリサーチなどを駆使しなくてはならない.
 だが,本当に求めたいのは「何番目である」ということではなく,何番目かに付随する別のデータであることがほとんどだ.たとえば,名前をキーにして電話番号を知りたいというようなことである.
 こんなときに便利なのが,連想配列である.残念ながらVisual Basicに連想配列はないが,似たような機能を提供してくれるのが,Collectionである.
 Collectionは,オブジェクトの集合である.Visual BasicにもFontsやForms,ControlsなどがCollectionとして実装されているので,ご存じだろう.個々のオブジェクトの集合がCollectionである.実は,Visual Basic 4.0からは,独自のCollectionを作ることができるようになっている.
 次に例を示そう.ここでは,あるタイミングでWaveファイルを鳴らすか鳴らさないかを設定することを考えよう.要素としては,Waveファイルの名前と,鳴らすか鳴らさないかのフラグである.まずは,オブジェクトの元になるクラスを作る.次のように,clsWavesにはWaveNameとSpeakをメンバー変数,すなわちプロパティとして作成した.

【ClsWaves】
Option Explicit

Public WaveName As String
Public Speak As Boolean

 Collectionオブジェクト自体は,次のように宣言する.
Private Waves As New Collection

 Collectionに個々のオブジェクトを追加するには,Classをインスタンシングし,プロパティを設定した上で,Addメソッドを使う.このとき,第2パラメータは検索時のキーとなるプロパティである.ここでは,WaveNameで後から検索して,Speakするかどうかを調べるものとする.

Dim oWv As New clsWaves

oWv.WaveName = "へろへろ"
oWv.Speak = True
Waves.Add oWv, oWv.WaveName


 たとえば,先ほどのチェックボックス付きリストボックスlstEventsで設定された内容をCollectionに追加するには,次のようにする.ループ中で使うためには,違うインスタンスを毎回作成しなくてはならない.このため,oWvの宣言にはNewはなく,ループ中でNewならびにNothingしていることに注目してほしい.


Dim oWv As clsWaves
Dim i As Integer

For i = 0 To lstEvents.ListCount - 1
Set oWv = New clsWaves
oWv.WaveName = lstEvents.List(i)
oWv.Speak = True
Waves.Add oWv, oWv.WaveName
Set oWv = Nothing
Next i


 これで,リストボックスの内容はコレクションに入った.ここから,特定のWaveNameのもののSpeakプロパティがどうなっているかを知るには,次のように書けばよい.


Waves.Item("へろへろ").Speak


 このように,ループを書かなくてもキーとなる内容を元にして一行で検索ができるようになる.
 Collectionオブジェクトには,次のメソッドがある.

Add オブジェクトの追加
Item オブジェクトの取得
Count オブジェクトの数
Remove オブジェクトの削除


 このように,Collectionは配列というよりは,データベース的な使い方ができるものだ.また,Collectionには,いろいろな型のオブジェクトを含めることができる.したがって,さまざまな構造のオブジェクトをひとつのコレクションにまとめることもできる.ただし,Itemで得た内容のプロパティ取得時などにエラーとならないような工夫が必要だ.


● ダイアログ表示と内部変数の受け渡しへの応用
ちょっとしたテクニック


  オプションなどを設定するダイアログボックスでは,[OK]が押されればそこで設定された内容が反映され,[キャンセル]が押されれば以前の設定のままになる.ということは,ダイアログにあるコントロールの内容を設定情報そのものとしてはマズい.他にグローバル変数などが必要になるわけだ.
 これは,フォームひとつだけのアプリケーションであっても,同じである.[キャンセル]時のために,別に変数を用意しておかなくてはならない.先ほどの例のように,チェックボックス付きリストボックスの内容を保存しようとするならば,配列変数が必要になる.これを,配列ではなくCollectionで実現してみよう.ダイアログでの受け渡し自体の手間は特に軽減されるわけではないが,実際に個々の設定を元に処理するかの判断をしてゆくには,配列よりCollectionが向いているからである.
 ここでは,図3のようなデザインとした.全リストをリスト1に示す.
 右側のリストボックスは,単にCollectionの内容を表示するためのものである.この表示をしているDispListでは,For Each文を使ってCollectionの内容をひとつずつ取り出している.クリックされたときには,Waves Collectionの内容を取得してメッセージボックスで表示する.[>>]ボタンが押されたときには,cmdOK_Clickイベントが発生し,リストボックスの内容をCollectionに持ってゆく.[<<]ボタンでは,cmdCancel_Clickイベントが発生し,逆の動作をする.

リスト1:Collectionによる内部変数の受け渡し
Option Explicit

Private Waves As New Collection
Private Sub cmdCancel_Click()
  Dim i As Integer

' Resore from Collection to ListBox
  For i = 0 To lstEvents.ListCount - 1
      lstEvents.Selected(i) = Waves.Item(lstEvents.List(i)).Speak
  Next i

  DispList
End Sub

Private Sub cmdOK_Click()
 Dim i As Integer

' Set from ListBox to Collection
  For i = 0 To lstEvents.ListCount - 1
      Waves.Item(lstEvents.List(i)).Speak = lstEvents.Selected(i)
  Next i

  DispList
End Sub

Private Sub Form_Load()
  Dim oWv As clsWaves
  Dim i As Integer

  For i = 0 To lstEvents.ListCount - 1
      Set oWv = New clsWaves
      oWv.WaveName = lstEvents.List(i)
      oWv.Speak = True
      Waves.Add oWv, oWv.WaveName
      lstEvents.Selected(i) = oWv.Speak
      Set oWv = Nothing
  Next i

  DispList
End Sub

Private Sub List1_Click()
  Dim sWaveName As String

  sWaveName = Left(List1.Text, InStr(List1.Text, vbTab) - 1)
  MsgBox Waves.Item(sWaveName).WaveName &_
  ":"& CStr(Waves.Item(sWaveName).Speak)
End Sub

Private Sub DispList()
  Dim oWv As New clsWaves

  List1.Clear
  For Each oWv In Waves
      List1.AddItem oWv.WaveName & vbTab & oWv.Speak
  Next
End Sub
図3:変数の受け渡しダイアログボックス例

●システム情報イベントを得るSysInfo.OCX
知られていないActiveXコントロール

 Windowsではイベントドリブンのプログラミングをする.Visual Basicは,Windowsのメッセージをイベントという形で表現している.しかし,すべてのシステムからのメッセージに対応するイベントがあるわけではない.
 そこで,Visual Basicでは知ることのできないメッセージを,AddressOfを使ったサブクラス化という手法を使って得ることになる.これについては,本誌1997年2月号に詳しく述べた.サブクラス化を使いこなすためには,それなりにWindowsネイティブな動作についての知識が必要であった.
 しかし,実はこんな面倒なことをしなくても,Visual Basicには便利なActiveXコントロールが附属している.それが,SysInfo.OCXである.このコントロールは,実はVisual Basic 4.0からUnSupportedとしてCD-ROMには含まれていたが,Visual Basic 5.0からは標準でインストールされる.
 せっかくあるのだから,使ってみることにしよう.
[Ctrl]+[T]キーを押してコンポーネントダイアログを開いたら,「Microsoft SysInfo Control 5.0」を選んで[OK]を押そう.これでSysInfoが使えるようになった.フォームにこのコントロールを配置し,ダブルクリックしてどんなイベントがあるのかを見てみよう.名前からだいたいどんなものなのかを予測することは容易だろう.実際には,イベントの他にもプロパティがいくつかある.次にこれらの一覧を示す(表1).

表1:SysInfo.OCXのイベントとプロパティ

■イベント
名称 解説
ConfigChangeCancelled ハードウェアプロファイルの変更が取り消されたというメッセージを,オペレーティングシステムがすべてのアプリケーションに送るときに発生
ConfigChanged システムのハードウェアプロファイルが変更されたときに発生
DeviceArrival 新しいデバイスがシステムに追加されたときに発生
DeviceOtherEvent 一般イベントに含まれない通知イベント
DeviceQueryRemove デバイスをシステムから削除する前に発生
DeviceQueryRemoveFailed DeviceQueryRemove イベントのコードがデバイスの削除を取り消した場合に発生
DeviceRemoveComplete デバイスの削除後に発生
DeviceRemovePending すべてのアプリケーションがデバイスの削除を承認し,削除の準備ができたときに発生
DevModeChanged ユーザーがデバイスのモード設定を変更したときに発生
DisplayChanged システム画面の解像度が変更されたときに発生
PowerQuerySuspend システムの電源が中断される直前に発生
PowerResume システムが中断モードから解放されて,アプリケーションが正常な動作を再開したときに発生
PowerStatusChanged システムの電源ステータスに変化が生じたときに発生
PowerSuspend システムが中断モードに入る直前に発生
QueryChangeConfig オペレーティングシステムのユーザーインターフェイスを介して,あるいはシステムにドックまたはアンドックする前に中断モードを要求して,現在のハードウェアプロファイルの変更を要求したときに発生
SettingChanged アプリケーションがシステム全体のパラメータを変更したときに発生
SysColorsChanged アプリケーションから,またはコントロールパネルを介してシステムのカラー設定が変更されたときに発生
TimeChanged アプリケーションから,またはコントロールパネルを介してシステム時間が変更されたときに発生

■プロパティ
名称 解説
ACStatus システムがAC電源を使用しているかどうかを示す値を返す
BatteryFullTime バッテリの完全充電寿命を示す値を返す
BatteryLifePercent 全バッテリ電力の残存量をパーセントで返す
BatteryLifeTime バッテリの残存寿命を示す値を返す
BatteryStatus バッテリ充電量のステータスを示す値を返す
OSBuild 現在アプリケーションを実行中のオペレーティングシステムについての情報を提供する値を返す
OSPlatform 現在アプリケーションを実行中のオペレーティングシステムを識別する値を返す
OSVersion 現在アプリケーションを実行中のオペレーティングシステムのバージョンを識別する値を返す
ScrollBarSize スクロールバーの幅のシステムメトリックをtwipで返す
WorkAreaLeft, WorkAreaTop, WorkAreaWidth. WorkAreaHeight タスクバー用に調整された表示デスクトップの属性を返す


 実は先ほどから例に出していたのが,このイベントなどなのであった.なお,詳しくは,ヘルプを参照していただきたい.一般的な使い方としては,次のパターンがある.

■いきなり使う

 OSの情報などは,いつでも取得できるので,必要なときに使う.

■イベントに応じた処理をする

 システムからの通知を元にして,イベントのパラメータや他のプロパティを参照してなんらかの処理をする.特に,Device系のイベントは引数が複雑である.

■イベントに応じてユーザーに確認する

 Queryという文字がつくイベントは,ユーザーに確認を求めて,処理の継続をキャンセルさせることができる.

■タイマーで監視する

 一定時間ごとに,プロパティを監視する.

 さて,ではSysInfoを使ってプログラムを作ってみよう.といっても,このOCXは「システム情報」という点でまとめられているだけであって,何か特定の処理をするための一連の機能が入っているというものではない.アプリケーションによっては必要になるかもしれないといったものなので,一通りの機能を使うようなサンプルプログラムは作りにくいのである.
 たとえば,DevModeChangedであれば,プリンタ設定が変更された可能性があるので,画面に表示していたページ幅を計算しなおすといった処理が必要になるかもしれない.時刻が変更されたなら,表示されているカレンダーや時計の表示をしなおさなくてはならないかもしれない.PowerSuspend時にはネットワークの先にあるどこかのシステムに自分がしばらく音信不通になることを通知するかもしれないし,PowerResume時やDeviceArrival時に処理を再開できることを通知するかもしれない.このように,千差万別なのだ.

 しかたがないので,ここではごく単純にふつうなら分からないイベントが起きたことをユーザーに通知しておしまいにしよう.とはいえ,いちいちメッセージが出てもうっとうしいし,コンソールウィンドウを出しておくのもナニだ.だいたい,そんなことではまったく実用性がない.いや,実用性はなくてもいいが,面白くないではないか.
 そこで,ここではあらかじめ用意しておいたWaveファイルが鳴って,何が起きたのかをユーザーに通知するようにしてみよう.プログラムの手間としては,メッセージボックスとたいして変わらないが,あらかじめデータを作っておくのが,結構面倒くさい.また,自分の声では気分が悪くなるのが普通だろう.
 ここはひとつ,女性の声だ.そういえば,TalkingHubというモノがあり,声優が「ネットワークの負荷が上昇中です」などのようにしゃべるHubすら売っているという.役に立つのか不明だが,なんとなく欲しいではないか(編注:アクトンテクノロジィ社製TalkingHub“宮村優子スペシャルバージョン”通称ミヤムルータ.編集子もちょっと欲しいです…).というわけで,この企画を考えていた折りに,たまたまPCDN事務局にいらした知人の吉野さんにお願いして,吹き込んでいただいた.

 このあたりのスケジュールの関係から,今回の付録CD-ROMには収まっていないが,来月号あるいはPCDNからダウンロードできるようにしておく予定だ.
 もうお分かりのように,このプログラム自体はまったくたいしたことがない.Waveファイルを鳴らす部分については,本誌1997年2月号で紹介したWin32 APIのsndPlaySoundを使えば一発である.ただし,続けて複数のWaveファイルを鳴らすときには,ASYNCではいけないので注意が必要である.
 ここでは,次のようなデザインのフォームにした(図4).

図4:デザイン時

 sndPlaySoundを使うための宣言は,リスト2-aのように記述した.
 Waveファイルは,イベントの名前と同じにした.したがって,一般的にはリスト2-bのようなコーディングとなる.ただし,Query系のキャンセル可能なものについては,リスト2-cのような書き方ができる.
 また,バッテリ残量については,Timerを使って監視し,キャプションに表示している.さらに10パーセントごとに変化をチェックしてしゃべるCheckBatteryLifePercentを呼び出している(リスト2-d).
 この関数では,以前の値から10%境界を過ぎるごとにWaveファイルを鳴らす.ただし,これは3つのWaveファイルを組み合わせてしゃべるようになっている. WAV_BATRESTは「バッテリ残量」,次に「10」から「100」まで10おきの数字,最後に「パーセント」という言葉が入っている.
 また,バッテリフル時のお言葉,バッテリ情報の詳細についてもしゃべるようになっている(リスト2-e).
 このように,一通りいろんなことをしゃべるようになっただけでも,なかなか楽しいプログラムになった.ただし,しゃべってくれるためのトリガは,もっぱらシステム設定が変更されたときになるから,ノートパソコンなどのようにレジュームやPCMCIAカードの抜き差しをしないと,あまり楽しくないのも事実である.
 このプログラムのよい点は,Waveファイルさえ変えれば,いろいろなおしゃべりをさせることができることだ.つまりサスペンドするときに「サスペンドします」のようなつまらない言葉ではなく,「See you Later!」と言わせるなど,アイデア次第なのである.




リスト2-a:sndPlaySoundの宣言
Option Explicit

Public Declare Function sndPlaySound _
 Lib "winmm.dll" Alias "sndPlaySoundA" _
 (ByVal lpszSoundName As String, _
  ByVal uFlags As Long) As Long
Public Const SND_SYNC = &H0
Public Const SND_ASYNC = &H1
Public Const SND_NODEFAULT = &H2
Public Const SND_MEMORY = &H4
Public Const SND_LOOP = &H8
Public Const SND_NOSTOP = &H10

リスト2-d:バッテリー残量の監視
Private Sub timStat_Timer()
    Me.Caption = sysiMessage.BatteryLifePercent & "%"
    CheckBatteryLifePercent sysiMessage.BatteryLifePercent
End Sub

リスト2-b:wavファイルの指定
Private Sub sysiMessage_TimeChanged()
    Call sndPlaySound("TimeChanged.wav", SND_ASYNC)
End Sub

リスト2-e:バッテリーフル/バッテリー情報の詳細報告
Private Sub CheckBatteryLifePercent(ByVal NewVal As Integer)
    Dim iNewVal As Integer

    iNewVal = CInt(NewVal / 10)
    If iOldVal <> iNewVal Then
        If sysiMessage.BatteryStatus < 128 Then
            Call sndPlaySound(WAV_BATREST, SND_SYNC)
            Call sndPlaySound(CStr(iNewVal * 10) & ".wav", SND_SYNC)
            Call sndPlaySound(WAV_PERCENT, SND_SYNC)
            If NewVal = 100 Then
                Call sndPlaySound("BatteryFullTime.wav", SND_SYNC)
            End If
        End If
        iOldVal = iNewVal
        Call sndPlaySound("BS" & CStr(sysiMessage.BatteryStatus) & _
         ".wav", SND_ASYNC)
    End If
End Sub

リスト2-c:キャンセル可能な場合
Private Sub sysiMessage_PowerQuerySuspend(Cancel As Boolean)
    Call sndPlaySound("PowerQuerySuspend.wav", SND_ASYNC)
    If MsgBox _
     ("Suspendしますか", vbOKCancel + vbExclamation, App.Title) _
     = vbCancel Then
        Cancel = True
    End If
End Sub


●VBアプリをタスクトレイに入れる
Win32 APIの活用アイデア最後に


 システム起動時から常駐して監視をするようなタイプのアプリケーションは,通常サイズのウィンドウになっていてはうっとうしい.かといって,アイコン化されていてもうっとうしい.最近では,こういったものはタスクトレイに入れてしまうのがスマートとされている.
 しかし,Visual Basicにはタスクトレイに入れる機能がない.そこで,Win32 APIの登場である.ここで使うのは,Shell_NotifyIcon関数である.この関数の引数はたった2つであり,細かい情報は第2引数となるPNOTIFYICONDATA型構造体に設定しておく.参考までに関数と構造体の概要を記事末に掲載しておく.
 Visual Basicでの宣言ならびに定義は,リスト3のようになる.

リスト3:Shell_NotifyIcon関数の宣言と定義
Option Explicit

Type NOTIFYICONDATA
    cbSize As Long
    hwnd As Long
    uID As Long
    uFlags As Long
    uCallbackMessage As Long
    hIcon As Long
    szTip As String * 64
End Type

Declare Function Shell_NotifyIcon _
 Lib "shell32.dll" Alias "Shell_NotifyIconA" _
 (ByVal dwMessage As Long, lpData As NOTIFYICONDATA) As Long

Public Const NIM_ADD = &H0
Public Const NIM_MODIFY = &H1
Public Const NIM_DELETE = &H2
Public Const NIF_ICON = &H2
Public Const NIF_MESSAGE = &H1
Public Const NIF_TIP = &H4

Public Const WM_MOUSEMOVE = &H200
Public Const WM_LBUTTONDOWN = &H201
Public Const WM_LBUTTONUP = &H202
Public Const WM_LBUTTONDBLCLK = &H203
Public Const WM_RBUTTONDOWN = &H204
Public Const WM_RBUTTONUP = &H205
Public Const WM_RBUTTONDBLCLK = &H206
Public Const WM_MBUTTONDOWN = &H207
Public Const WM_MBUTTONUP = &H208
Public Const WM_MBUTTONDBLCLK = &H209

■本当はタスクトレイへは常駐しない

 NOTIFYICONという単語からも分かるように,この関数はタスクトレイにアイコンとして登録されたアプリケーションに,タスクトレイから通知するためのものである.つまり,アプリケーション自体がタスクトレイに入るわけではないのだ.したがって,タスクトレイに常駐しているように見せるためには,ウィンドウ本体は通常はInVisible状態にしておき,タスクトレイのアイコンがダブルクリックされたり,右クリック時のメニューから呼び出されたりしたときに,必要なウィンドウを表示させてやればよい.

■常駐の開始

 では,実際にこの機能を使ってみることにしよう.モジュールレベルには,次のようにNOTIFYICONDATA型変数を宣言する.

Private nidSysInfo As NOTIFYICONDATA

 Form_Load時に,この構造体の初期値を設定する(リスト4).

リスト4:初期値の設定内容
nidSysInfo.cbSize = Len(nidSysInfo)     '構造体のサイズ
nidSysInfo.hwnd = Me.hwnd       '通知されるウィンドウハンドル
nidSysInfo.uID = 1      'ID
nidSysInfo.uFlags = NIF_ICON Or NIF_TIP Or NIF_MESSAGE  'フラグ
nidSysInfo.uCallbackMessage = WM_MBUTTONDOWN    'Calllback Message
nidSysInfo.hIcon = Me.Icon      'タスクトレイに表示されるアイコン
nidSysInfo.szTip = "SysInfo" & vbNullChar       'ToolTipに表示される文字列

 ここで,ちょっと面白いのが,メッセージの受け渡しである.タスクトレイ上のアイコンになんらかのアクションがあったとき,hwndに指定されたウィンドウに対してuCallbackMessageで指定されたメッセージが送信される.通常は,WM_USERなどを使うことを考えてしまうところだ.その場合には当然ながら,Visual Basic標準のイベントでこれを取得することはできないので,AddressOfを使ったサブクラス化が必要になる.
 しかし,ここでは通常の2ボタンマウスではまず使うことのない真ん中のボタンが押されたときのメッセージWM_MBUTTONDOWNを指定している.つまり,Form_MouseDownイベントが発生するのである.つまり,サブクラス化の必要がなくなるのである.
 これらの設定ができたら,NIM_ADDでタスクトレイにアイコンを常駐させることができる.

lRetVal = Shell_NotifyIcon _
(NIM_ADD, nidSysInfo)

 これで,タスクトレイに常習した.なお,このときのFormのVisibleプロパティはあらかじめFalseにしておく.

■タスクトレイからの通知

 タスクトレイのアイコンになんらかのアクションがあったときには,Form_MouseDownイベントが発生する.このとき,真ん中のボタンであることを判断し,本当のメッセージはなんであったかをX \ Screen.TwipsPerPixelXで得ることができる.後は,適切に対応した処理を書けばよい.たとえばポップアップウィンドウを開きたいならば,あらかじめ作成したメニューをPopupMenuメソッドで呼び出せばよい(リスト5).

リスト5:Form_MouseDownイベント
Private Sub Form_MouseDown _
 (Button As Integer, Shift As Integer, _
 X As Single, Y As Single)
    If (Button And vbMiddleButton) = vbMiddleButton Then
        Select Case X \ Screen.TwipsPerPixelX
        Case WM_LBUTTONDOWN
            Beep
            'mnuStatus_Click
        Case WM_RBUTTONDOWN
            Me.PopupMenu mnuPopUp, vbPopupMenuRightButton
        Case WM_LBUTTONDBLCLK
            Me.Visible = True
        End Select
    End If
End Sub


■ツールチップの更新

 最初に構造体の初期値を設定したとき,ToolTipへの文字列も設定した.しかし,時系列によって変化するような内容にしたいときには,この文字列を変更する必要がある. このときには,szTipを再設定し,NIM_MODIFYを指定してShell_NotifyIcon関数を呼び出してやればよい.
Private Sub Timer1_Timer() nidSysInfo.szTip = _ Time$ & vbNullChar lRetVal = Shell_NotifyIcon _ (NIM_MODIFY, nidSysInfo) End Sub

■タスクトレイからの削除

 終了時には,タスクトレイからアイコンを削除する必要がある.このときには,NIM_DELETEを指定してShell_NotifyIcon関数を呼び出してやればよい.


Private Sub Form_Unload _
 (Cancel As Integer)
  lRetVal = Shell_NotifyIcon _
   (NIM_DELETE, nidSysInfo)
End Sub


 このように,タスクトレイの扱いは意外にもカンタンなのである(リスト6,図5〜7参照).

リスト6:タスクトレイソースリスト
Private nidSysInfo As NOTIFYICONDATA
Private lRetVal As Long

Private Sub cmdHide_Click()
    Me.Visible = False
End Sub

Private Sub Form_Load()
    nidSysInfo.cbSize = Len(nidSysInfo)
    nidSysInfo.hwnd = Me.hwnd
    nidSysInfo.uID = 1
    nidSysInfo.uFlags = NIF_ICON Or NIF_TIP Or NIF_MESSAGE
    nidSysInfo.uCallbackMessage = WM_MBUTTONDOWN    'Calllback Message
    nidSysInfo.hIcon = Me.Icon
    nidSysInfo.szTip = "SysInfo" & vbNullChar
    lRetVal = Shell_NotifyIcon(NIM_ADD, nidSysInfo)
End Sub

Private Sub Form_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single)
    If (Button And vbMiddleButton) = vbMiddleButton Then
        Select Case X \ Screen.TwipsPerPixelX
        Case WM_LBUTTONDOWN
            Beep
            'mnuStatus_Click
        Case WM_RBUTTONDOWN
            Me.PopupMenu mnuPopUp, vbPopupMenuRightButton
        Case WM_LBUTTONDBLCLK
            Me.Visible = True
        End Select
    End If
End Sub

Private Sub Form_Unload(Cancel As Integer)
    lRetVal = Shell_NotifyIcon(NIM_DELETE, nidSysInfo)
End Sub

Private Sub mnuExit_Click()
    Unload Me
End Sub

Private Sub mnuStatus_Click()
    MsgBox Time
End Sub

Private Sub Timer1_Timer()
    nidSysInfo.szTip = Time$ & vbNullChar
    lRetVal = Shell_NotifyIcon(NIM_MODIFY, nidSysInfo)
End Sub


図5:タスクトレイへの常駐.ToolTipが出る
図6:右クリックすれば,ポップアップメニューが表示される
図7:ダブルクリックすれば,ウィンドウが表示される

●おしゃべりシステムインフォメーション
それらしいプログラム


 Visual Basic 5.0を活用するための隠れたテクニックを紹介してきたが,ここまで使ってきた機能を一通り使ったプログラムを作ってみた.
 もう,すでにお分かりであろう.SysInfoで得た情報のうち,チェックされたものだけについてしゃべるというものだ.そして,このアプリケーションはタスクトレイに常駐するのである.
 大きな改造はない.今まで作ってきたものをかき集めてひとつにすればよいのである.ただし,次のようにSysInfoの各イベントごとに,しゃべってよいかのチェックをしておく必要がある.


If Waves.Item("DeviceArrival").Speak _
Then
Call sndPlaySound _
("DeviceArrival.wav", SND_ASYNC)
End If

 フォームのデザイン,実行時のポップアップメニューとToolTip,設定ダイアログの画面(図8・9),全リストをそれぞれ示す(clsWaves, modSysSpeak, frmSysSpeak.リスト7参照).
 実際にうごかしてみると,レジューム時にはDeviceArrivalやDeviceRemoveCompleteなどは何度も発生したり,機種によって電池残量のステップが違ったり,不思議なタイミングで不思議なイベントが起きたり,起こらなかったり… といまいち動作がおかしいような気もする.おそらく,APMまわりのBIOSの問題なのだろう.実際にもっと役立つようなプログラムを作るときには,さらにイベントの引数について詳細にチェックしたり,例外処理を書いていかないといけないかもしれない.細かい詰めはもう少し必要そうだ.
 ともあれ,ActiveXコントロールでの拡張,Win32 APIでの拡張,言語仕様の有効利用といった3点を満足したプログラムができた.そして,何よりもしゃべるというアイデアもなかなか面白い.はた目で見ている人は必ず興味を持って寄ってくる.これぞ,まさに200%活用と言えるだろう.

リスト8:sysSpeak完成品全リスト
■clsWave
VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "clsWaves"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = True
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Public WaveName As String
Public Speak As Boolean

■frmSpeak

VERSION 5.00
Object = _
 "{6FBA474E-43AC-11CE-9A0E-00AA0062BB4C}#1.0#0" _
 ; "sysinfo.ocx"
Begin VB.Form frmSysSpeak
'(略)
End

Attribute VB_Name = "frmSysSpeak"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False

Option Explicit

Private Waves As New Collection
Private Const WAV_BATREST = "BATRest.wav"
Private Const WAV_PERCENT = "Percent.wav"
Private iOldVal As Integer
Private nidSysInfo As NOTIFYICONDATA
Private lRetVal As Long

Private Sub CheckBatteryLifePercent(ByVal NewVal As Integer)
    Dim iNewVal As Integer
    Dim i As Integer
    Dim vArry As Variant

    iNewVal = CInt(NewVal / 10)
    If iOldVal <>iNewVal Then
        If sysiMessage.BatteryStatus < 128 Then
            If Waves.Item("BatteryRestPercent").Speak Then
                Call sndPlaySound(WAV_BATREST, SND_SYNC)
                Call sndPlaySound(CStr(iNewVal * 10) & ".wav", SND_SYNC)
                Call sndPlaySound(WAV_PERCENT, SND_SYNC)
            End If
            If NewVal = 100 Then
                If Waves.Item("BatteryFullTime").Speak Then
                    Call sndPlaySound("BatteryFullTime.wav", SND_SYNC)
                End If
            End If
        End If
        iOldVal = iNewVal
        ' Battery Status
        If Waves.Item("BatteryStatus").Speak Then
            If sysiMessage.BatteryStatus = 255 Then
                Call sndPlaySound("BS" & CStr(255) & ".wav", SND_ASYNC)
            Else
                vArry = Array(1, 2, 4, 8, 128)
                For i = 0 To UBound(vArry)
                    If (sysiMessage.BatteryStatus And vArry(i)) = _
                     vArry(i) Then
                        Call sndPlaySound("BS" & CStr(vArry(i)) & _
                         ".wav", SND_ASYNC)
                    End If
                Next i
            End If
        End If
    End If
End Sub

Private Sub mnuBattStat_Click()
    iOldVal = -1
    CheckBatteryLifePercent Val(Me.Caption)
End Sub

Private Sub mnuExit_Click()
    Unload Me
End Sub

Private Sub mnuRestore_Click()
    Me.Visible = True
End Sub

Private Sub sysiMessage_ConfigChangeCancelled()
    If Waves.Item("ConfigChangeCancelled").Speak Then
        Call sndPlaySound("ConfigChangeCancelled.wav", SND_ASYNC)
    End If
End Sub

Private Sub sysiMessage_ConfigChanged _
 (ByVal OldConfigNum As Long, ByVal NewConfigNum As Long)
    If Waves.Item("ConfigChanged").Speak Then
        Call sndPlaySound("ConfigChanged.wav", SND_SYNC)
    End If
End Sub

Private Sub sysiMessage_DeviceArrival _
 (ByVal DeviceType As Long, ByVal DeviceID As Long, _
 ByVal DeviceName As String, ByVal DeviceData As Long)
    If Waves.Item("DeviceArrival").Speak Then
        Call sndPlaySound("DeviceArrival.wav", SND_ASYNC)
    End If
End Sub

Private Sub sysiMessage_DeviceRemoveComplete _
 (ByVal DeviceType As Long, ByVal DeviceID As Long, _
 ByVal DeviceName As String, ByVal DeviceData As Long)
    If Waves.Item("DeviceRemoveComplete").Speak Then
        Call sndPlaySound("DeviceRemoveComplete.wav", _
         SND_ASYNC + SND_NOSTOP)
    End If
End Sub

Private Sub sysiMessage_DevModeChanged()
    If Waves.Item("DevModeChanged").Speak Then
        Call sndPlaySound("DevModeChanged.wav", SND_SYNC)
    End If
End Sub

Private Sub sysiMessage_DisplayChanged()
    If Waves.Item("DisplayChanged").Speak Then
        Call sndPlaySound("DisplayChanged.wav", SND_ASYNC)
    End If
End Sub

Private Sub sysiMessage_PowerQuerySuspend(Cancel As Boolean)
    If Waves.Item("PowerQuerySuspend").Speak Then
        Call sndPlaySound("PowerQuerySuspend.wav", SND_ASYNC)
        If MsgBox("Suspend", vbOKCancel + vbExclamation, App.Title) = _
         vbCancel Then
            Cancel = True
        End If
    End If
End Sub

Private Sub sysiMessage_PowerResume()
    If Waves.Item("PowerResume").Speak Then
        Call sndPlaySound("PowerResume.wav", SND_SYNC)
    End If
End Sub
Private Sub sysiMessage_PowerStatusChanged()
    Static iAcStatus As Integer

    If Waves.Item("PowerStatusChanged").Speak Then
        If iAcStatus <> sysiMessage.ACStatus Then
            Select Case sysiMessage.ACStatus
            Case 0
                Call sndPlaySound("PowerStatusChanged2.wav", SND_SYNC)
                Call sndPlaySound("PowerStatusChanged1.wav", SND_ASYNC)
            Case 1
                Call sndPlaySound("PowerStatusChanged3.wav", SND_ASYNC)
            Case Else
            End Select
            iAcStatus = sysiMessage.ACStatus
        End If
    End If
End Sub

Private Sub sysiMessage_PowerSuspend()
    If Waves.Item("PowerSuspend").Speak Then
        Call sndPlaySound("PowerSuspend.wav", SND_SYNC)
    End If
End Sub

Private Sub sysiMessage_QueryChangeConfig(Cancel As Boolean)
    If Waves.Item("QueryChangeConfig").Speak Then
        Call sndPlaySound("QueryChangeConfig.wav", SND_SYNC)
        If MsgBox("Change Config", vbExclamation + vbOKCancel, App.Title) _
         = vbCancel Then
            Cancel = True
        End If
    End If
End Sub

Private Sub sysiMessage_SettingChanged(ByVal Item As Integer)
    If Waves.Item("SettingChanged").Speak Then
        Call sndPlaySound("SettingChanged.wav", SND_ASYNC)
    End If
End Sub

Private Sub sysiMessage_SysColorsChanged()
    If Waves.Item("SysColorsChanged").Speak Then
        Call sndPlaySound("SysColorsChanged.wav", SND_ASYNC)
    End If
End Sub

Private Sub sysiMessage_TimeChanged()
    If Waves.Item("TimeChanged").Speak Then
        Call sndPlaySound("TimeChanged.wav", SND_ASYNC)
    End If
End Sub

Private Sub timStat_Timer()
    Me.Caption = sysiMessage.BatteryLifePercent & "%"
    nidSysInfo.szTip = Me.Caption & vbNullChar
    lRetVal = Shell_NotifyIcon(NIM_MODIFY, nidSysInfo)
    CheckBatteryLifePercent sysiMessage.BatteryLifePercent
End Sub


Private Sub cmdCancel_Click()
    Dim i As Integer

    ' Resore from Collection to ListBox
    For i = 0 To lstEvents.ListCount - 1
        lstEvents.Selected(i) = Waves.Item(lstEvents.List(i)).Speak
    Next i
'    DispList
    Me.Visible = False
End Sub

Private Sub cmdOK_Click()
    Dim i As Integer

    ' Set from ListBox to Collection
    For i = 0 To lstEvents.ListCount - 1
        Waves.Item(lstEvents.List(i)).Speak = lstEvents.Selected(i)
    Next i

'    DispList
    Me.Visible = False
End Sub

Private Sub Form_Load()
    Dim oWv As clsWaves
    Dim i As Integer

    mnuPopUp.Visible = False
    nidSysInfo.cbSize = Len(nidSysInfo)
    nidSysInfo.hwnd = Me.hwnd
    nidSysInfo.uID = 1
    nidSysInfo.uFlags = NIF_ICON Or NIF_TIP Or NIF_MESSAGE
    nidSysInfo.uCallbackMessage = WM_MBUTTONDOWN    'Calllback Message
    nidSysInfo.hIcon = Me.Icon
    nidSysInfo.szTip = "SysInfo" & vbNullChar
    lRetVal = Shell_NotifyIcon(NIM_ADD, nidSysInfo)
    Me.Hide

    For i = 0 To lstEvents.ListCount - 1
        Set oWv = New clsWaves
        oWv.WaveName = lstEvents.List(i)
        oWv.Speak = True
        Waves.Add oWv, oWv.WaveName
        lstEvents.Selected(i) = oWv.Speak
        Set oWv = Nothing
    Next i

    timStat_Timer

'    DispList
End Sub

Private Sub Form_MouseDown _
 (Button As Integer, Shift As Integer, X As Single, Y As Single)
    If (Button And vbMiddleButton) = vbMiddleButton Then
        Select Case X \ Screen.TwipsPerPixelX
        Case WM_LBUTTONDOWN
            Me.Visible = True
        Case WM_RBUTTONDOWN
            Me.PopupMenu mnuPopUp, vbPopupMenuRightButton
        End Select
    End If
End Sub

Private Sub Form_Unload(Cancel As Integer)
    lRetVal = Shell_NotifyIcon(NIM_DELETE, nidSysInfo)
End Sub

Private Sub List1_Click()
    Dim sWaveName As String

    sWaveName = Left(List1.Text, InStr(List1.Text, vbTab) - 1)
    MsgBox Waves.Item(sWaveName).WaveName & ": " & _
     CStr(Waves.Item(sWaveName).Speak)
End Sub

Private Sub DispList()
    Dim oWv As New clsWaves

    List1.Clear
    For Each oWv In Waves
        List1.AddItem oWv.WaveName & vbTab & oWv.Speak
    Next
End Sub


■modSysSpeak

Attribute VB_Name = "modSysSpeak"
Option Explicit

Public Declare Function sndPlaySound _
 Lib "winmm.dll" Alias "sndPlaySoundA" _
 (ByVal lpszSoundName As String, ByVal uFlags As Long) As Long
Public Const SND_SYNC = &H0
Public Const SND_ASYNC = &H1
Public Const SND_NODEFAULT = &H2
Public Const SND_MEMORY = &H4
Public Const SND_LOOP = &H8
Public Const SND_NOSTOP = &H10

Type NOTIFYICONDATA
    cbSize As Long
    hwnd As Long
    uID As Long
    uFlags As Long
    uCallbackMessage As Long
    hIcon As Long
    szTip As String * 64
End Type

Declare Function Shell_NotifyIcon _
 Lib "shell32.dll" Alias "Shell_NotifyIconA" _
 (ByVal dwMessage As Long, lpData As NOTIFYICONDATA) As Long

Public Const NIM_ADD = &H0
Public Const NIM_MODIFY = &H1
Public Const NIM_DELETE = &H2
Public Const NIF_ICON = &H2
Public Const NIF_MESSAGE = &H1
Public Const NIF_TIP = &H4

Public Const WM_MOUSEMOVE = &H200
Public Const WM_LBUTTONDOWN = &H201
Public Const WM_LBUTTONUP = &H202
Public Const WM_LBUTTONDBLCLK = &H203
Public Const WM_RBUTTONDOWN = &H204
Public Const WM_RBUTTONUP = &H205
Public Const WM_RBUTTONDBLCLK = &H206
Public Const WM_MBUTTONDOWN = &H207
Public Const WM_MBUTTONUP = &H208
Public Const WM_MBUTTONDBLCLK = &H209



参考:Shell_NotifyIcon関数と構造体の概要

Shell_NotifyIcon

タスクバーステータスエリアにアイコンを追加修正、削除されるようにシステムにメッセージを送る

WINSHELLAPI BOOL WINAPI Shell_NotifyIcon(
DWORD dwMessage, // message identifier
PNOTIFYICONDATA pnid // pointer to structure
);

●Parameters(パラメータ)

どのようなメッセージを送るかを示す.次のパラメータのいずれか
NIM_ADD    アイコンを追加する
NIM_DELETE  アイコンを削除する
NIM_MODIFY  アイコンを修正する

●pnid

NOTIFYICONDATA構造体へのポインタ.dwMessageの値による.)

●Return Values(戻り値)

成功時は0以外、失敗時には0を返す.

●NOTIFYICONDATA

システムがタスクバーステータスエリアメッセージを処理するために必要な情報

typedef struct _NOTIFYICONDATA { // nid
DWORD cbSize;
HWND hWnd;
UINT uID;
UINT uFlags;
UINT uCallbackMessage;
HICON hIcon;
char szTip[64];
} NOTIFYICONDATA, *PNOTIFYICONDATA;

●Members(メンバ)

cbSize   NOTIFYICONDATA型データのサイズ
hWnd     タスクバーステータスエリアのアイコンに関連付けられたメッセージが送られるウィンドウのハンドル
uID     タスクバーアイコンのアプリケーションが定義した番号
uFlags   他のメンバに関連したフラグ.以下の組合わせになる

        NIF_ICON      hIconメンバが有効
        NIF_MESSAGE   uCallbackMessageメンバが有効
        NIF_TIP       szTipメンバが有効

●uCallbackMessage

アプリケーション定義.アイコンを含む矩形境界内でのマウスイベントを、hWndで指定されたウインドウに通知するためのメッセージ

hIcon   追加,修正,削除されるアイコンのハンドル

szTip   アイコンのツールチップテキスト



VB Magazine ライブラリ | Visual Basicコースホームページ
int21 ホームページ | PCDN ホームページ

PCDN LOGO
Copyright (c) 1998 int21 Corporation All Rights Reserved.
For questions or comments, please send mail to: pcdn@int21.co.jp