業務に効くAPI

既存のコントロールでは実現できない機能を盛り込む



初音 玲 HATSUNE, Akira



Windows API(Application Programming Interface)は、Windowsの機能をVBプログラムから使うための方法だ。 Windowsの機能を使うことができるということは、OS添付のツールと同等のものを自由に作れるということになる。 WindowsはC言語により作られているので、Windows APIもC言語から呼び出されることを前提としているため、Visual Basicからは使い難かったり、直接使えなかったりするが、大部分のWindows APIは、Declare宣言さえすれば(多少の注意は必要ではあるが)Visual Basicから普通の関数やサブプロシージャのように利用できる。 しかし、使い方の確かな情報をもたずに、Visual Basicで普通にプログラムを進めるようにRAD的手法でメイク&トライしてゆけば完成できるほど、Windows APIは甘くはない。
本誌2000年3月号の特集で、「業務に効くAPI入門」を扱ったが、今回はその第2弾として、知っていれば業務アプリケーション作成時に、ほどよいスパイスとなるような、少し複雑なAPIの使い方を紹介してゆきたいと思う。

APIって何だ?

 APIとは“プログラムを呼び出すときの形式”だが、Windowsの世界では、DLL(ダイナミックリンクライブラリ)形式のプログラムを呼び出すときの形式を指す。Windows自体もさまざまなDLL形式のプログラムの集合であり、そこに含まれるものをWindows APIと呼ぶ(表1)。
 DLL形式のプログラムは、拡張子が.DLLで提供されることが多いためCOMサーバー(ActiveX DLL)と混同されることあるが、両者はまったく別物だ。また、Visual BasicではCOMサーバーは作成できるが、DLL形式のプログラムを作成することはできない。もし、DLL形式のプログラムを作成するときは、C言語など他の言語製品を選択する必要がある。

表1:Windowsの代表的なダイナミックリンクライブラリ
DLL内容
Advapi32.dllセキュリティおよびレジストリの呼び出しを含む高度な(Advanved)APIライブラリ
Comdlg32.dllコモンダイアログAPIライブラリ
Gdi32.dllGDI(Graphics Device Interface)APIライブラリ
Kernel32.dll32ビットベースのWindows APIの中心的ライブラリ
Lz32.dll32ビットベースの圧縮ルーチンAPIライブラリ
Mpr.dll複数プロバイダルーター(Multiple Provider Router)APIライブラリ
Netapi32.dll32ビットベースのネットワークAPIライブラリ
Shell32.dll32ビットベースのシェルAPIライブラリ
User32.dllユーザーインターフェイスルーチン用APIライブラリ
Version.dllバージョンAPIライブラリ
Winmm.dllWindowsマルチメディアAPIライブラリ
Winspool.drvプリンタスプーラのAPI呼び出しを格納するプリンタスプーラインターフェイス

APIをVisual Basicから使うには

 Visual BasicからDLL形式のプログラムを使うには、DLLに含まれているAPIをDLLプロシージャとして宣言する。
 APIのパラメータ仕様などは、MSDNライブラリ(Microsoft Developers Network:Visual Basic 5.0以上に添付されているオンラインマニュアル。マイクロソフトのWebサイトでも同様の内容が公開されている)にすべて掲載されている。Visual BasicのAPIビューアやいろいろなAPI解説記事や解説本も、元をたどればMSDNライブラリに行き着く。MSDNライブラリがC言語を前提に記述されているから、と敬遠せずに、最新の仕様を確認し、より深くAPIを理解するように心がけるのがAPIマイスターへの道への第1歩だ。
 さて、DLLプロシージャが値を返却するときには、

Declare Function publicname _
  Lib "libname" _
  (パラメータ,……) As 関数型
のように関数として宣言し、値を返さないときには、

Declare Sub publicname _
  Lib "libname" _
  (パラメータ,……)
のようにサブプロシージャとして宣言する。
 この宣言は、プログラム内で共通に使用するのであれば標準モジュールの宣言セクションに記述し、特定のフォームモジュール内でのみ使用するときはPrivateキーワードを先頭に付加してフォームモジュールの宣言セクションに記述する。なお、API名は大文字と小文字を区別する点に注意して宣言を記述することが必要だ(コラム参照)。

コラム APIを使うときの各種注意事項
 以下の基本的な注意点を踏まえて、MSDNライブラリの内容から個々のAPIをDLLプロシージャ宣言に読み替えてゆけば、Visual BasicでAPIを使う準備が完了する。

文字列関連API宣言の注意点

 文字列関連のWindows APIは、ANSI形式とUnicode形式の2種類がある。Windows 95などではANSI形式しかサポートされていないので、さまざまなプラットフォームに対応するには、ANSI形式を選択しなければならない。たとえば、INIファイルから設定値を取得する“GetPrivateProfileString API”は、ANSI形式の“GetPrivateProfileStringA”と、Unicode形式の“GetPrivateProfileStringW”としてKernel32.DLLに実装されている(図A)。よって、GetPrivateProfileString APIを使うときは、“GetPrivateProfileStringA”と宣言してもよいが、MSDNライブラリ上の記述と合わせたほうがいろいろと便利なので、DLLプロシージャ宣言にAlias節を追加してリストAのように宣言し、“GetPrivateString”の名前で利用するのがよいだろう。もちろん、Alias節で指定する名前も大文字と小文字の区別を意識する必要はある。

図A:Dependency WalkerでKernel32.dllを見る
図A

リストA:GetPrivateProfileString APIを使うとき
Declare Function GetPrivateProfileString _
         Lib "Kernel32" Alias "GetPrivateProfileStringA" _
         (ByVal lpApplicationName As String, _
          ByVal lpKeyName As Any, _
          ByVal lpDefault As String, _
          ByVal lpReturnedString As String, _
          ByVal lSize As Long, _
          ByVal lpFileName As String) As Long

パラメータ宣言時の基本的注意点

 APIを使う上で一番注意しなければならない点は、APIのパラメータの宣言だろう。Visual Basicではパラメータの基本は参照渡しだが、APIでは値渡しだからだ。よって、DLLプロシージャ宣言のパラメータにはByValキーワードを忘れずに記述する。

文字列を扱う時の注意点

 文字列をパラメータとしているときは、さらに注意が必要だ。MSDNライブラリに文字列パラメータの形式としてLPSTRが指定されていたときには値渡しでよいが、まれにBSTRが指定されているときは参照渡しとして宣言する。
 また、APIから文字列を受け取るような時は、Visual Basic側で文字列の領域を確保しておく必要がある。つまり、

Dim strFixBuf As String * 256
のように固定長文字列として宣言しておくか、

strBuf = String$(256,vbNullChar)
のようにNULL文字を埋めて可変長文字列の有効長をAPIからの返却値より大きくしておく必要がある。

構造体を扱う時の注意点

 APIの中には、パラメータに構造体を使っているものもある。C言語の構造体は、Visual Basicのユーザー定義型に読み替えればよい。問題は、Visual Basicはユーザー定義型変数を値渡しできない点だが、ユーザー定義型の場合、“参照渡しをする”=“構造体の先頭アドレスを渡す”という意味になるので、ユーザー定義型変数を参照渡しすれば、DLLプロシージャが期待している“構造体の先頭アドレス”をパラメータに指定したことになる。


Visual BasicからAPIを使うべきか?

 話は前後するが、Visual BasicでAPIを使う理由のひとつに、「部品(コンポーネント)として提供されていないOSの機能を使用する」ということがあげられる。  他のアプリケーションには実装されているが、Visual Basicに最初から組み込まれているコントロールでは実現できない機能があれば、それはOSが実装している機能を組み合わせて使用している場合があるのだ。  もちろんVisual Basicに含まれていなくても、市販品を含めたCOMコンポーネント(ActiveXコントロールなど)として存在しているかもしれないので、まずは、市販COMコンポーネントを調査する必要がある。その上で、適切なコンポーネントがないとなったら、いよいよAPIを使うことを決断するのがよいだろう。
API使用の実例

 本誌2000年3月号の特集では「ファイルコピーとCrystal Report」に注目した。今回は「ネットワーク」に注目してサンプルを作ってゆきたいと思う。
 なお、今回のサンプルは、Windows 95/98、およびWindows NT4.0に実装されているAPIで、Visual Basic 4.0/5.0/6.0から使ったときの動作を確認している。付録CD-ROMに収録しているサンプルプログラムは、表2の環境で確認した。なお、Visual Basic 6.0については、Office 2000が導入されているPCでコンパイルしたので、Office 2000を導入していない場合は、EXEファイルの再コンパイルを行なうか、IDE上で動作確認を行なっていただきたい。

表2:動作確認環境
Visual Basic OS Office IE
Visual Basic 4.0(UPDATE2) Windows 95 OSR2.1 Office 95 IE3.02
Visual Basic 5.0(SP3) Windows NT 4.0(SP3) Office 97 IE4.0
Visual Basic 6.0(SP3) Windows 98 SE Office 2000 IE5.01

APIでのみ使える機能を堪能する
-PINGユーティリティを作成する-

 Visual Basic 5.0以降では、WinSock32コントロールが付属しているので、TCP/IPやUDP/IPのプロトコルを扱うのが比較的簡単になった(図1)。もちろん、このコントロールは“おまけコントロール”だから、市販コントロールに比べて品質や機能が劣るので、使うべきではない。Visual Basicに付属しているコントロールがマイクロソフト社製のものか、おまけコントロールであるかは、コントロールのバージョン情報を参照すればよい。図2のようにマイクロソフト社以外のCopyrightが記述されていたら、他社からOEM供給されたコントロール(すなわち、おまけコントロール)だ。TCP/IPやUDP/IPを使うならば、市販のWinSock32コントロールを購入するのが、よいだろう。
 しかし、現在市販されているWinSock32コントロールには、ひとつ問題がある。それはWinSock32バージョン2の機能が使えない、ということだ。
 WinsSock32コントロールは、WinSock32 APIを使いやすい形にしたコントロールであるが、現在市販されているWinSock32コントロールがもっている機能は、WinSock32バージョン1の機能のみなのである。したがってWindows NT4.0やWindows 95のオプションパック、Windows 98やWindows 2000で導入されたWinSock32バージョン2の機能を使えるものは、まだ出回っていない。通常使う分には困らないが、WinSock32バージョン2から取り入れられた機能を使いたいときなどは、市販のWinSock32コントロールでは役不足になることが多い。
 たとえば、図1にあるICMP(Internet Control Message Protocol)関連は、WinSock32バージョン2には含まれているがWinSock32バージョン1には含まれておらず、ICMP.DLL(ICMP API)という別DLLとして提供されている。そのため、現時点で流通しているWinSock32コントロールを使ってICMPプロトコルを使用するPINGユーティリティなどを気軽に作成することができない。よって、PINGを使ったプログラムを作成するときには、迷わずAPIを使う道を選択することになるだろう。

図1:プロトコルの階層
図1

図2:“おまけコントロール”のバージョン情報
図2

PINGについて考察する

 APIを使う使わないに関わらず、モノ作りの第1歩は、作ろうとしているモノの理解だ。ソフトウェアの世界では「要件分析」ともいう。PINGというのは、自分のマシンと相手マシン間のネットワークが繋がっているかを確認するユーティリティで、“サーバーに繋がらない”というようなときに有効だ。Windows 95/98/NT/2000にも標準添付されている。DOSプロンプト上で、

PING www.hogehoge.co.jp
のように相手先を指定してコマンドを実行すると、内部的には、ICMPエコー要求というICMPデータが相手先に向けて送信されることになる。ICMPエコー要求を受信した相手先は、ICMPエコー応答というICMPデータを返信してくるので、

Reply from xxx.yyy.zzz.ccc …………
    :
のように画面に表示され、ネットワークが正しく接続されていることがわかる。
 ICMPはIP(Internet Protocol)と同じくネットワーク層に位置するプロトコルで、このIPを使ってICMPデータを送受信している。つまり、ICMPが送受信できたということは、IPがやり取りできるということであり、IPと表裏一体のようなものであるTCP(Transmission Control Protocol)やUDP(User Datagram Protocol)ともやり取りできるということになる。つまり、PINGの応答が返ってくるということは、指定先との間でTCP/IPレベルでの接続が可能であるということだ。ルータなどを導入したり、インターネットと接続するように設定したり、社内ネットワークの構成を変更したときには、このPINGを活用してLANケーブルの接続ミス、IPアドレス設定ミス、ルータの設定ミスなどさまざまなチェックを行なうことが可能だ。

ICMP APIの使い方を調査する

 ICMP APIの情報源は、もちろんMSDNライブラリだ(図3)。なお、ICMP APIは、Visual Basicに添付されているAPIビューアには掲載されていない。これは将来的にWinSock32がバージョン2に統一されたときに、ICMP APIを使わずにWinSock32 APIを使うことが予定されていることと関連していると思われる。

図3:ICMP APIの定義(IcmpSendEcho API)
図3

 APIビューアに掲載されていないということは、MSDNライブラリに記述されているC言語形式の情報を参照しながら、DLLプロシージャを宣言しなければならない。宣言時の注意点(コラム参照)を踏まえながらVisual Basic形式に読み替えたものがリスト1だ。この中でもっとも重要なDLLプロシージャはIcmpSendEcho APIだ。

リスト1:ICMP APIのDLLプロシージャ宣言など
Private Type typPIP_OPT_INFO
  TTL As Byte               ' 生存時間
  TOS As Byte               ' ICMPタイプ
  Flags As Byte             ' フラグ
  Optsize As Byte           ' オプション長
  Options As String         ' オプション
End Type
Private Type typICMPEchoReply
  Address(1 To 4) As Byte   ' IPアドレス
  Status As Long            ' コード
  TripTime As Long          ' 応答時間(ms)
  DataSize As Integer       ' バッファサイズ
  Reserved As Integer       ' 予備
  ReplyData As String       ' レスポンス領域
  IPOptions As typPIP_OPT_INFO
End Type
Private Declare Function IcmpCreateFile _
                         Lib "ICMP.DLL" () As Long
Private Declare Function IcmpCloseHandle _
                         Lib "ICMP.DLL" _
                        (ByVal ICMPHandle As Long) As Integer
Private Declare Function IcmpSendEcho _
                         Lib "ICMP.DLL" _
                        (ByVal ICMPHandle As Long, _         ----《1》
                         ByVal DestinationAddress As Long, _ ----《2》
                         RequestData As String, _            ----《3》
                         ByVal RequestSize As Integer, _     ----《4》
                         RequestOption As typPIP_OPT_INFO, _ ----《5》
                         ReplyBuffer As Byte, _              ----《6》
                         ByVal ReplySize As Long, _          ----《7》
                         ByVal Timeout As Long) As Long      ----《8》
Private Declare Function IcmpGetLastError _
                         Lib "wsock32.dll" Alias "WSAGetLastError" _
                        () As Long
Private Const ICMP_FLAG_NO_FRAGMENT = 2 ' フラグメントなし
Private Const ICMP_ECHO_REQUEST = 8     ' エコー要求

 IcmpSendEcho APIは、ICMPデータを送信して、その応答を待ち合わせてくれる同期型APIなので、このAPIを呼び出すだけで、ICMPデータの送受信が完了する。そのため、パラメータの数も多く設定も少々煩雑だ。
     
  1. 第1パラメータ《1》は、ICMPハンドルと呼ばれるもので、ICMPを管理するための識別子のようなものだ。IcmpCreateFile APIにより生成できるので、IcmpCreateFile APIの戻り値を設定すればよい。
  2.  
  3. 第2パラメータ《2》は、送信先のアドレスを指定する。もちろん、10.23.1.1のようなIPアドレス(Internetプロトコルドットアドレス)形式ではなく、32ビットのInternetアドレスとして指定する。通常は画面上で、送信先のホスト名やIPアドレスを指定するので、それをバイナリ形式(Long型)に変換しなければならない。
  4.  
  5. 第3パラメータ《3》は、ICMPデータを送信するときのデータ部の値を指定する。
  6.  
  7. 第4パラメータ《4》は、データ部の長さを指定する。
  8.  
  9. 第5パラメータ《5》は、ICMPデータ送信時のオプションに、値の入った構造体を指定する。構造体には、TTLや送信データのICMPタイプなどICMPヘッダ部の情報を指定できる(図4)。
  10.  
  11. 第6パラメータ《6》は、受信したICMP応答データの値が返却されてくるパラメータだ。パラメータに指定する文字列変数は、返却されてくるサイズよりも長い領域をあらかじめ確保しておかないと、受信したデータの一部分しか取得できなくなってしまう。
  12.  
  13. 第7パラメータ《7》は、第6パラメータに指定した返却領域の長さを指定する。
  14.  
  15. 第8パラメータ《8》はICMPの応答を待ち合わせる時間をミリ秒の単位で指定する。
図4:ICMPパケットの構造
図4

関連APIを調査する

 IcmpSendEcho APIを使ってICMPのデータ送受信を実現するときに問題になってくるのは、送信先のIPアドレスをどうやってLong型のInternetアドレスに変換するかという点だ。この問題を解決するには、WinSock32 APIを使うのがよいだろう。WinSock32 APIには、アドレス変換関数やホスト名からアドレスを取得するデータベース関数などが充実している(表3・4)。MSDNライブラリに掲載されているWinSock32 APIの定義をVisual Basicに読み替えて、DLLプロシージャ宣言を行なったものがリスト2だ。

表3:WinSock32 API(バークレイソケット互換)
関数名 処理
バイトオーダー変換
htol4バイト整数をWindows形式からネットワークバイトオーダー形式に変換する
htos4バイト整数をWindows形式からネットワークバイトオーダー形式に変換する
htohl4バイト整数をネットワークバイト形式からWindows形式に変換する
htohs2バイト整数をネットワークバイト形式からWindows形式に変換する
アドレス変換
inet_addrInternetプロトコルドットアドレスから32ビットのInternetアドレスに変換する
inet_ntoa32ビットのInternetアドレスからInternetプロトコルドットアドレスに変換する
その他
gethostbyaddr32ビットのInternetアドレスからホスト情報を取得する
gethostbynameホスト名からホスト情報を取得する
gethostname自分のマシンのホスト名を取得する
getpeernameソケット接続しているリモートアドレスとポート番号を取得する
getservbynameサービス名からサービス情報を取得する
getservbynameサービスのポート番号からサービス情報を取得する
getprotobynameプロトコル名からプロトコル情報を取得する
getprotobynumberプロトコル番号からプロトコル情報を取得する
getsocknameソケットから自分のマシンのアドレスとポート番号を取得する
ioctlsocketソケットの動作パラメータの取得と設定をする

表4:拡張WinSock32 API(初期化関数)
関数名 処理
WSAStartupWinSock32 APIを初期化する
WSACleanupすべての未処理のデータを送信し、ソケットを閉じる
WSAGetLastError最後に発生したエラーを取得する

リスト2:WinSock API(WinSock.basより抜粋)
Private Const WSAEINTR = 10004
Private Const WSAEACCES = 10013
Private Const WSAEFAULT = 10014
Private Const WSAEINVAL = 10022
Private Const WSAEMFILE = 10024
Private Const WSAEWOULDBLOCK = 10035
Private Const WSAEINPROGRESS = 10036
Private Const WSAEALREADY = 10037
Private Const WSAENOTSOCK = 10038
Private Const WSAEDESTADDRREQ = 10039
Private Const WSAEMSGSIZE = 10040
Private Const WSAEPROTOTYPE = 10041
Private Const WSAENOPROTOOPT = 10042
Private Const WSAEPROTONOSUPPORT = 10043
Private Const WSAESOCKTNOSUPPORT = 10044
Private Const WSAEOPNOTSUPP = 10045
Private Const WSAEPFNOSUPPORT = 10046
Private Const WSAEAFNOSUPPORT = 10047
Private Const WSAEADDRINUSE = 10048
Private Const WSAEADDRNOTAVAIL = 10049
Private Const WSAENETDOWN = 10050
Private Const WSAENETUNREACH = 10051
Private Const WSAENETRESET = 10052
Private Const WSAECONNABORTED = 10053
Private Const WSAECONNRESET = 10054
Private Const WSAENOBUFS = 10055
Private Const WSAEISCONN = 10056
Private Const WSAENOTCONN = 10057
Private Const WSAESHUTDOWN = 10058
Private Const WSAETOOMANYREFS = 10059
Private Const WSAETIMEDOUT = 10060
Private Const WSAECONNREFUSED = 10061
Private Const WSAEHOSTDOWN = 10064
Private Const WSAEHOSTUNREACH = 10065
Private Const WSAEPROCLIM = 10067

' Extended Windows Sockets error constant definitions
Private Const WSASYSNOTREADY = 10091
Private Const WSAVERNOTSUPPORTED = 10092
Private Const WSANOTINITIALISED = 10093
Private Const WSAHOST_NOT_FOUND = 11001
Private Const WSATRY_AGAIN = 11002
Private Const WSANO_RECOVERY = 11003
Private Const WSANO_DATA = 11004

Type typHostEnt
    h_name          As Long
    h_aliases       As Long
    h_addrtype      As Integer
    h_length        As Integer
    h_addr_list     As Long
End Type

Public Const INADDR_NONE = &HFFFF
Public Const INADDR_ANY = &H0

Public Const INVALID_SOCKET = -1
Public Const SOCKET_ERROR = -1
Public Const SOCK_STREAM = 1
Public Const SOCK_DGRAM = 2
Public Const AF_INET = 2
Public Const PF_INET = 2

Public Const WSA_DESCRIPTIONLEN = 256
Public Const WSA_DescriptionSize = WSA_DESCRIPTIONLEN + 1
Public Const WSA_SYS_STATUS_LEN = 128
Public Const WSA_SysStatusSize = WSA_SYS_STATUS_LEN + 1
Type typWSADataType
    wVersion       As Integer
    wHighVersion   As Integer
    szDescription  As String * WSA_DescriptionSize
    szSystemStatus As String * WSA_SysStatusSize
    iMaxSockets    As Integer
    iMaxUdpDg      As Integer
    lpVendorInfo   As Long
End Type
' アドレス変換
Public Declare Function inet_addr _
                        Lib "wsock32.dll" _
                       (ByVal cp As String) As Long
Public Declare Function inet_ntoa _
                        Lib "wsock32.dll" _
                       (ByVal lngIn As Long) As Long
' データベース関数
Public Declare Function gethostbyaddr _
                        Lib "wsock32.dll" _
                       (addr As Long, _
                        ByVal lngLen As Long, _
                        ByVal lngType As Long) As Long
Public Declare Function gethostbyname _
                        Lib "wsock32.dll" _
                       (ByVal strName As String) As Long
Public Declare Function gethostname _
                        Lib "wsock32.dll" _
                       (ByVal strName As String, _
                        ByVal namelen As Long) As Long
Public Declare Function getprotobyname _
                        Lib "wsock32.dll" _
                       (ByVal strName As String) As Long
Public Declare Function getprotobynumber _
                        Lib "wsock32.dll" _
                       (ByVal lngNumber As Long) As Long
Public Declare Function getservbyname _
                        Lib "wsock32.dll" _
                       (ByVal strName As String, _
                        ByVal proto As String) As Long
Public Declare Function getservbyport _
                        Lib "wsock32.dll" _
                       (ByVal Port As Long, _
                        ByVal proto As String) As Long
Public Declare Function getsockopt _
                        Lib "wsock32.dll" _
                       (ByVal s As Long, _
                        ByVal level As Long, _
                        ByVal optname As Long, _
                        optval As Any, _
                        optlen As Long) As Long
' 拡張機能
Public Declare Function WSAStartup _
                        Lib "wsock32.dll" _
                       (ByVal wVersionRequested As Long, _
                        lpWSAData As typWSADataType) As Long
Public Declare Function WSACleanup _
                        Lib "wsock32.dll" () As Long
Public Declare Function WSAGetLastError _
                        Lib "wsock32.dll" () As Long

Public Declare Sub MemCopy _
                   Lib "Kernel32" Alias "RtlMoveMemory" _
                  (hpvDest As Any, _
                   hpvSource As Any, _
                   ByVal cbCopy As Long)
Public Declare Function lstrlen _
                        Lib "Kernel32" Alias "lstrlenA" _
                       (lpString As Any) As Long

 PINGユーティリティでは、ホスト名を指定して、32ビットのInternetアドレスを取得したいので、inet_addr APIとgethostbyname APIを使う。まず、inet_addr APIを使って、IPアドレスが指定されていたときの変換をサポートし、inet_addr APIで変換できなかったときは、ホスト名が指定されたと見なしてgethostbyname APIを使う。実際にVisual Basicでプログラミングするとリスト3のようになる。gethostbyname APIの戻り値は、ホスト情報の構造体へのポインタになっているので、RtlMoveMemory API(DLLプロシージャ名はMemCopy)を使って、ホスト情報をユーザー定義型変数の領域にコピーしている。さらに、ホスト情報の構造体には、Internetアドレス自体ではなくInternetアドレス配列へのポインタが含まれているので、ポインタの追跡して配列の先頭のInternetアドレスを変数lngIPにコピーしている。

リスト3:Internetアドレスへの変換関数
Public Function plngGetHostByNameAlias( _
       ByVal strHostName As String) As Long
  Dim lngRet      As Long
  Dim heDestHost  As typHostEnt
  Dim lngAddr     As Long
  Dim lngIP       As Long

  lngIP = inet_addr(strHostName)
  If lngIP = INADDR_NONE Then
    lngRet = gethostbyname(strHostName)
    If lngRet <> 0 Then
      MemCopy heDestHost, ByVal lngRet, Len(heDestHost)
      MemCopy lngAddr, ByVal heDestHost.h_addr_list, 4
      MemCopy lngIP, ByVal lngAddr, heDestHost.h_length
    Else
      lngIP = INADDR_NONE
    End If
  End If
  plngGetHostByNameAlias = lngIP
End Function

バイトオーダーの違い

 複数バイトで整数を表現するとき、インテル系のCPUを使っているマシンで採用しているリトルエンディアン(little endian)方式以外に、モトローラ系のCPUを使っているマシンで採用しているビッグエンディアン(big endian)方式がある(図5)。バイナリ形式の整数データをネットワーク経由で送受信するときは、ビッグエンディアン方式にする約束になっていて、これをネットワークバイトオーダーと言う。
図5:バイトオーダーの違い
図5

PINGをプログラミングする

 今までの調査を踏まえてPINGをプログラミングするとリスト4のようになる。
 プログラムが完成したら、自分のマシンのホスト名などを入力して動作確認するとよいだろう(図6)。

図6:PING実行画面
図6

リスト4:PING(frmPINGからの抜粋)
Private Sub cmdPing_Click()
  Dim lngAddr            As Long             ' Internetアドレス
  Dim usrICMP            As typPIP_OPT_INFO  ' ICMP情報領域
  Dim usrICMPER          As typICMPEchoReply ' ICMP返却領域
  Dim bytRes(1 To 4096)  As Byte             ' ICMPレスポンス領域
  Dim strName            As String           ' ターゲット名
  Dim lngRet             As Long             ' 戻り値
  Dim iintLoop           As Integer          ' PINGカウンタ
  Dim strErrText         As String           ' エラーテキスト

  On Error GoTo errClick:

  Me.MousePointer = vbHourglass
  Me.Refresh

  strName = txtIP.Text
  lngAddr = plngGetHostByNameAlias(strName)       ----《1》

  lstResult.Clear

  For iintLoop = 1 To 4
    usrICMP.TTL = CByte("128")                    ----《2》-1
    usrICMP.TOS = ICMP_ECHO_REQUEST               ----《2》-2
    usrICMP.Options = ""                          ----《2》-3
    usrICMP.Optsize = Len(usrICMP.Options)        ----《2》-4
    usrICMP.Flags = ICMP_FLAG_NO_FRAGMENT         ----《2》-5

    lngRet = IcmpSendEcho(mlngICMP, _             ----《3》
                          lngAddr, _
                          Space$(32), _
                          32, _
                          usrICMP, _
                          bytRes(1), _
                          UBound(bytRes), _
                          10&)
      If lngRet = 0 Then
        lstResult.AddItem "ICMP_Error" & ":" & CStr(WSAGetLastError())
      Else
        MemCopy usrICMPER.Address(1), bytRes(1), LenB(usrICMPER)  …《4》
        lstResult.AddItem "Reply From " & _                         …《5》
                           CStr(usrICMPER.Address(1)) & "." & _
                           CStr(usrICMPER.Address(2)) & "." & _
                           CStr(usrICMPER.Address(3)) & "." & _
                           CStr(usrICMPER.Address(4)) & _
                          ": bytes=32 " & _
                          "time=" & CStr(usrICMPER.TripTime) & "ms " & _
                          "TTL=" & usrICMP.TTL
      End If
  Next

exitClick:
  On Error Resume Next
  Me.MousePointer = vbDefault
  Me.Refresh
  Exit Sub

errClick:
  strErrText = pstrWSAErrorGet(WSAGetLastError())
  If strErrText <> "" Then
    strErrText = Error$
  End If
  MsgBox strErrText, vbOKOnly + vbExclamation, App.Title
  Resume exitClick:
End Sub

《1》:IPアドレスやホスト名をInternetアドレスに変換
《2》:オプションを指定
《2》-1:TTLとして128を指定(設定画面を作って任意の値を指定できるようにしてもよいだろう)
《2》-2:ICMPエコー要求の送信を指定
《2》-3:オプションなしを指定
《2》-4:オプションのサイズを指定
《2》-5:フラグメントなしを指定
《3》:IcmpSendEcho APIを呼び出す
《4》:ICMPエコー応答からInternetアドレスを取得
《5》:InternetアドレスをIPアドレスに変換しながら結果を表示

APIでのみ使える機能を堪能する
-TRACEユーティリティを作成する-

 Windows NTやWindows 98以降では、TRACERT.EXEというDOSプロンプトで使うプログラムが存在する。このプログラムは、自分のマシンから指定されたマシンまでの経路を表示してくれるので、PINGがエラーで返ってきたときなどに使用すれば、経路のどこでエラーになっているかがわかる。インターネット上のサーバーに対してだけではなく、社内LANであってもルータ経由でサーバーに接続しているときなどは重宝するプログラムだ。このプログラムの機能をプログラミングできれば、ネットワーク管理の力強い味方を手に入れることができる。

TRACERTについて考察する

 TRACERTおよびその元祖であるUNIXマシンのTRACEROUTEコマンドでは、UDP/IPパケットを利用してこの機能を実現しているが、ICMP系のAPIの使用を考えるとICMPエコー要求を利用して実現するほうが簡単だろう。PINGユーティリティとの違いは、PINGがTTL値にある程度大きな値を設定してICMPエコー要求を送信しているが、TRACEユーティリティではTTL値を1から段々大きくして使用する。これは、ルータを通過するときに“必ずTTLの値をひとつ減らす”というインターネットの決まり事と、“TTL値をひとつ減らして0になったら、そのルータからICMPがリプライされてくる”という決まり事を利用している(図7)。

図7:TTLの考え方
図7

TRACEをプログラミングする

 TRACEをプログラミングするときに必要なAPIは、PINGの場合とまったく同一である。使い方が違うだけだ。PINGでは、TTLは比較的大きな一定値としていたが、TRACEでは、送信先にたどり着くまでTTLの値を1から順番に加算して、ICMPエコー要求を送信してゆく(リスト5)。あとは、表示するメッセージをそれらしく加工してあげるだけでよい。
 プログラムが完成したら、自分が使っているメールサーバーなどのホスト名などを入力して動作確認するとよいだろう(図8)。

図8:TRACE実行画面
図8

リスト5:TRACERT(frmTraceからの抜粋)
If lngAddr <> INADDR_NONE Then
  For iintLoop = 1 To 255
    ' TTLを1づつ増加させてルートを特定する
    usrICMP.TTL = CByte(iintLoop)
    usrICMP.TOS = ICMP_ECHO_REQUEST
    usrICMP.Options = ""
    usrICMP.Optsize = Len(usrICMP.Options)
    usrICMP.Flags = ICMP_FLAG_NO_FRAGMENT

                 :
         (リスト4と同等なので省略)
                 :

    If iintCnt > 4 Then
      lstResult.AddItem txtIP.Text & " Time out"
      Exit For
    Else
      If lngAddr = plngGetHostByNameAlias(strName) Then
        ' ターゲットに到達
        lstResult.AddItem txtIP.Text & _
               " [" & strName & "] " & _
               "Trace Complete."
        Exit For
      End If
    End If
  Next
Else
        lstResult.AddItem "Unable to resolve target system name " & txtIP.Text
    End If

APIでのみ使える機能を堪能する
-現在のユーザーを取得する-

 ネットワークを使っていると、データベースなどをはじめとする共有資源やメールなどの個人資源にアクセス制限をかけるということがよくある。ドメイン認証を活用すれば、現在のログインユーザーの権限が自動的に適用されるが、アプリケーションで扱っている資源がすべてWindowsセキュリティ配下にあるとは限らない。そうした場合、利用者の特定はユーザーIDによりアクセス制限を行ない、本人であるかどうかの認証はパスワードで行なうことが多いだろう。このようなときWindowsにログインしているユーザーIDをデフォルト表示してあげると“業務を行なうときにパスワードを入れてください”だけで済む。
 Visual Basicのプロパティには、Windowsログインユーザーを取得するものは存在しないので、この要求を満たすためには、GetUserName APIもしくはWNetGetUser APIを使うことになる。

GetUserName APIの使い方を調査する

 GetUserName APIは、WindowsにログインしたユーザーのユーザーIDを取得するAPIだ。
     
  1. 第1パラメータは、ユーザーIDの返却領域を指定する。事前に十分な領域を確保してからDLLプロシージャを呼び出すことになる。
  2.  
  3. 第2パラメータは、ユーザーIDの返却領域の長さを指定する。
 パラメータを指定してAPIを呼び出すと第1パラメータにユーザーIDが取得されてくる。このとき、文字列の最後はNULL文字になっているので、INSTR関数でvbNullCharを検索して有効な文字列部分を切り出すことになる(リスト6)。

リスト6:ログインしているユーザーを取得(frmNetUserより抜粋)
Private Declare Function GetUserName _
        Lib "advapi32" Alias "GetUserNameA" _
       (ByVal lpBuffer As String, _
        nSize As Long) As Long

                :

  Dim strUsrId  As String * 1024  ' ユーザーIDの取得バッファ
  Dim lngRet    As Long           ' APIの戻り値

                :

  lngRet = GetUserName(strUsrId, Len(strUsrId))
  txtGetUserName.Text = Left$(strUsrId, _
                        InStr(strUsrId, vbNullChar) - 1)

WNetGetUser APIの使い方を調査する

 WNetGetUser APIは、WindowsネットワークへログインしているユーザーのユーザーIDを取得するAPIだ。通常、GetUserName APIとWNetGetUser APIは同じユーザーIDを返却してくる。しかし、WNetGetUser APIでは、ネットワークドライブを指定したときには事情が変わってくる。共有フォルダをドライブに割り当てる時に、たとえば参照権限のあるユーザーIDとパスワードで接続していたとすれば、WNetGetUser APIはログインユーザーではなく、そのユーザーIDを返却するのだ。
     
  1. WNetGetUser APIの第1パラメータは、ネットワークドライブを指定する。ここで指定したドライブのユーザーIDが返却されてくる。もちろん、vbNullCharを指定することでWindowsネットワークにログインしたユーザーIDも取得可能だ。
  2.  
  3. 第2パラメータは、ユーザーIDの返却領域を指定する。事前に十分な領域を確保してからDLLプロシージャを呼び出すことになる。
  4.  
  5. 第3パラメータは、ユーザーIDの返却領域の長さを指定する。
 パラメータを指定してAPIを呼び出すと第2パラメータにユーザーIDが取得されてくる。このとき、文字列の最後はNULL文字になっているので、INSTR関数でvbNullCharを検索して有効な文字列部分を切り出すことになる(リスト7)。

リスト7:ネットワークを使っているユーザーを取得する(frmNetUserより抜粋)
Private Declare Function WNetGetUser _
        Lib "mpr" Alias "WNetGetUserA" _
       (ByVal lpName As String, _
        ByVal lpUserName As String, _
        lpnLength As Long) As Long
               :
            (中略)
               :
  Dim strUsrId   As String * 1024  ' ユーザーIDの取得バッファ
  Dim lngRet     As Long           ' APIの戻り値
  Dim strNetWork As String
               :
            (中略)
               :
  strNetWork = vbNullChar
  lngRet = WNetGetUser(strNetWork, _
                       strUsrId, Len(strUsrId))
  txtWNetGetUser.Text = Left$(strUsrId, _
                        InStr(strUsrId, vbNullChar) - 1)

APIでのみ使える機能を堪能する
-ネットワークドライブを割り当てる1-

WNetConnectionDialog APIの使い方

 エクスプローラの[ツール]-[ネットワークドライブの割り当て]と同様の動作をするプログラム(図9)を作成するには、WNetConnectionDialog APIを使うのがよいだろう。このAPIは、エクスプローラからも呼び出しているAPIなので、Windows 95/98とWindows NT 4.0では、それぞれのOSにあったダイアログボックスが自動表示される(図10)点も便利だ。しかもこのAPIの使い方は非常に簡単で、リスト8に示したコードだけで、共有フォルダの選択からドライブへの割り当てまで実現できてしまう。

図9:ネットワークドライブの割り当て
図9

図10:Windows NTでサンプルを実行
図10

     
  1. まず、第1パラメータはWindowsハンドルと呼ばれるポインタで、呼び出し元のフォームを識別するのに使用される。他のAPIもそうだが、Windowsハンドルを指定するときは、Me.hWndと記述して現在のフォームオブジェクトのWindowsハンドルを指定する。
  2.  
  3. 第2パラメータは、選択リストに表示する共有資源の種類を指定する。今回のようにRESURCETYPE_DISKを指定すれば共有フォルダが選択対象になる。
 この2つのパラメータを指定してAPIを呼び出せば、ダイアログでの操作について、プログラムは一切関知しなくてよい。

リスト8:ネットワークドライブの割り当て(frmNetDrvより抜粋)
Private Declare Function WNetConnectionDialog _
        Lib "mpr" _
       (ByVal hWnd As Long, _
        ByVal dwType As Long) As Long
Private Const RESURCETYPE_DISK = &H1
               :
            (中略)
               :
Private Sub cmdNetDrv_Click()
  Dim lngRet  As Long

  lngRet = WNetConnectionDialog(Me.hWnd, _
                                RESURCETYPE_DISK)
  If lngRet = 0 Then
  ' 正常終了
    drvList.Refresh
  End If
End Sub

WNetDisconnectDialog APIの使い方

 割り当てたネットワークドライブを切断するには、WNetDisconnectDialog APIを使う(リスト9)。このAPIの使い方も非常に簡単で、パラメータなどはWNetConnectDialog APIとまったくいっしょだ。もちろん、OSに合わせた「ネットワークドライブの切断」ダイアログが自動表示(図11)され、切断操作が完了するまで呼び出し元のプログラムに制御は戻ってこない。

図11:ネットワークドライブの切断
図11

リスト9:ネットワークドライブの切断(frmNetDrvより抜粋)
Private Declare Function WNetDisconnectDialog _
        Lib "mpr" _
       (ByVal hWnd As Long, _
        ByVal dwType As Long) As Long
Private Const RESURCETYPE_DISK = &H1
              :
           (中略)
              :
Private Sub cmdDisc_Click()
  Dim lngRet  As Long

  lngRet = WNetDisconnectDialog(Me.hWnd, _
                                RESURCETYPE_DISK)
  If lngRet = 0 Then
  ' 正常終了
    drvList.Refresh
  End If
End Sub

APIでのみ使える機能を堪能する
-ネットワークドライブを割り当てる2-

「ネットワークドライブの割り当て」ダイアログを表示する方法は、APIの使い方も含めて非常に簡単だった。しかし、ダイアログを表示しないでいつも決まったネットワークドライブの割り当てを行ないたいときもあるだろう。また、Visual BasicではUNC(Universal Naming Code)名を使って共有フォルダのファイルを直接操作できるので、ネットワークドライブの割り当ても必要ないとも言える。しかし、共有フォルダに権限が設定されているときは、マシンを立ち上げてから最初に共有フォルダを使うときにパスワードが要求されることがある。このようなときにプログラムの中から自動的にパスワードを設定できると便利だ。そういった要件を満たすには、WNetConnectinoDialog APIやUNC指定ではなく、WNetAddConnection APIを使うのがよいだろう。

WNetAddConnection APIの使い方

 WNetAddConection APIは、パスワードを指定してネットワークドライブの割り当てを行なう(リスト10)。

リスト10:ネットワークドライブを自動割り当てする(frmNetAddより抜粋)
Private Declare Function WNetAddConnection _
        Lib "mpr" Alias "WNetAddConnectionA" _
       (ByVal lpRemoteName As String, _
        ByVal lpPassword As String, _
        ByVal lpLocalName As String) As Long
                :
             (中略)
                :
Private Sub cmdAdd_Click()
  Dim strShare        As String   ' 共有フォルダ名
  Dim strPass         As String   ' パスワード
  Dim strDrive        As String   ' ドライブ名
  Dim lngRet          As Long

  On Error GoTo errClick:

  strShare = Trim$(txtUNC.Text) & vbNullChar
  strPass = "" & vbNullChar
  strDrive = UCase(Trim$(txtDrive.Text)) & ":" & vbNullChar
  lngRet = WNetAddConnection(strShare, strPass, strDrive)
  If lngRet = 0 Then
  ' 正常終了
    drvList.Refresh
  Else
    MsgBox pstrLastDllErrorText(Err.LastDllError), _
     vbOKOnly + vbExclamation, App.Title
  End If

exitClick:
  On Error Resume Next
  Exit Sub

errClick:
  MsgBox Error$, vbOKOnly + vbExclamation, App.Title
  Resume exitClick:
End Sub
  1. WNetAddConnection APIの第1パラメータにはUNC名を指定する。UNC名は、

    \\Servername\share-holdername
    
    のような形式になる。
  2.  
  3. 第2パラメータは共有フォルダのパスワードを指定する。もしNULL(vbNullChar)を指定したときは、パスワードなしでアクセスを試みる。
  4.  
  5. 第3パラメータは、共有フォルダを割り当てるドライブを指定する。ドライブの指定は[E:]のように1文字の英字と:(コロン)により行なう。

WNetCancelConnection APIの使い方

 ダイアログを表示せずに割り当てを行なえるならば、ダイアログを表示せずにネットワークドライブを切断する方法もある。その方法が、WNetCancelConnection APIだ。
 WNetCancelConnection APIの使い方はシンプルで、第1パラメータはネットワークドライブを指定し、第2パラメータにはネットワークドライブを使用中でも切断するかどうか指定する。第2パラメータは、通常、0を指定して使用完了を待ち合わせるのが安全だ(リスト11)。

リスト11:ネットワークドライブを自動切断する(frmNetAddより抜粋)
Private Declare Function WNetCancelConnection _
        Lib "mpr" Alias "WNetCancelConnectionA" _
       (ByVal lpName As String, _
        ByVal fForce As Long) As Long
               :
            (中略)
               :
Private Sub cmdCancel_Click()
  Dim strDrive   As String   ' ドライブ名
  Dim lngRet     As Long

  On Error GoTo errClick:

  strDrive = UCase(Trim$(txtDrive.Text)) & ":" & vbNullChar
  lngRet = WNetCancelConnection(strDrive, 0)
  If lngRet = 0 Then
  ' 正常終了
    drvList.Refresh
  End If

exitClick:
  On Error Resume Next
  Exit Sub

errClick:
  MsgBox Error$, vbOKOnly + vbExclamation, App.Title
  Resume exitClick:
End Sub

APIでのみ使える機能を堪能する
-ネットワークドライブを割り当てる3-

 Windows NT 4.0の「ネットワークドライブの割り当て」ダイアログでは、ユーザー名も指定できる。残念ながらWNetAddConnection APIではパスワードは指定できるがユーザー名は指定できない。そのような要望に対応できるのは、WNetAddConnection3 APIだ。
 ただし、WNetAddConnection3 APIは、単純にユーザー名がパラメータに追加されただけではなく、第2パラメータに構造体を指定することで、さまざまなオプションもサポートしている。しかし、ネットワークドライブの割り当てに使い方を限定するならば、
の4つの設定に注目すればよい(リスト12)。この第2パラメータの他に、第3パラメータにパスワード、第4パラメータにユーザーID、第5パラメータに0を指定してAPIをコールすれば、Windows 95/98でもWindows NT 4.0のような「ネットワークドライブの割り当て」ダイアログが作成可能になる(図12)。なお、切断についてはWNetCancelConnection APIを使用すればよい。

図12:ユーザーIDとパスワード指定の割り当て
図12

リスト12:ユーザー名を指定してネットワークドライブを割り当てる(frmNetAdd2より抜粋)
Private Type typNETRESOURCE
  dwScope         As Long
  dwType          As Long     ' 共有資源のタイプ
  dwDisplayName   As Long
  dwUsage         As Long
  lpLocalName     As String   ' ドライブ名
  lpRemoteName    As String   ' UNC名
  lpComment       As String
  lpProvider      As String
End Type
Private Declare Function WNetAddConnection3 _
        Lib "mpr" Alias "WNetAddConnection3A" _
       (ByVal hWnd As Long, _
        lpNetResource As typNETRESOURCE, _
        ByVal lpPassword As String, _
        ByVal lpUserName As String, _
        ByVal dwFlags As Long) As Long
' dwTypeの設定値
Private Const RESURCETYPE_ANY = &H0
Private Const RESURCETYPE_DISK = &H1
Private Const RESURCETYPE_PRINT = &H2
             :
          (中略)
             :
Private Sub cmdAdd_Click()
  Dim lngRet          As Long
  Dim strUser         As String
  Dim strPass         As String
  Dim usrNetResource  As typNETRESOURCE

  On Error GoTo errClick:
  usrNetResource.dwType = RESURCETYPE_DISK
  usrNetResource.lpLocalName = UCase(Trim$(txtDrive.Text)) & _
                              ":" & vbNullChar
  usrNetResource.lpRemoteName = Trim$(txtUNC.Text) & vbNullChar
  usrNetResource.lpProvider = vbNullString
  strUser = Trim$(txtUser.Text) & vbNullChar
  strPass = Trim$(txtPass.Text) & vbNullChar
  lngRet = WNetAddConnection3(Me.hWnd, _
                              usrNetResource, _
                              strPass, _
                              strUser, _
                              0)
  If lngRet = 0 Then
  ' 正常終了
    drvList.Refresh
  End If

exitClick:
  On Error Resume Next
  Exit Sub

errClick:
  MsgBox Error$, vbOKOnly + vbExclamation, App.Title
  Resume exitClick:
End Sub

Windowsの機能を活用する
-フォルダを指定する-

 何種類かのファイルの出力先としてフォルダのみを指定したいときもあるだろう(図13)。コモンダイアログコントロールでは、出力先のファイル名を指定することは簡単に実現できるが、フォルダの指定だけに留めることはできない。そして、なぜかフォルダ指定用のコントロールやオプションは、Visual Basicには存在しない。しかし、フォルダを指定するAPIは存在する。それが、SHBrowseForFolder APIだ(リスト13)。

図13:「フォルダの参照」ダイアログ
図13

リスト13:フォルダ指定のAPI(frmFolderより抜粋)
Private Declare Function SHBrowseForFolder _
        Lib "shell32.dll" Alias "SHBrowseForFolderA" _
       (lpbi As typBROWSEINFO) As Long
Private Declare Function SHGetPathFromIDList _
        Lib "shell32.dll" Alias "SHGetPathFromIDListA" _
       (ByVal pidl As Long, _
        ByVal pszPath As String) As Long
Private Declare Sub CoTaskMemFree Lib "ole32" (ByVal pv As Long)

' フォルダ参照のダイアログ用構造体
Private Type typBROWSEINFO
  hWndOwner As Long         ' Handle
  pidlRoot As Long          ' Address
  pszDisplayName As String  ' Address
  lpszTitle As String       ' null-terminated string
  ulFlag As Long
  lpfn As Long              ' デフォルトフォルダ指定用CallBack関数のAddress
  lParam As Long            ' オプション
  iImage As Long
End Type

' BROWSEINFO.pidlRoot定数
' Locating the Standard Folders Where Data Belongs (MSDN)

' スタートアップ(All Users)
Private Const CSIDL_COMMON_ALTSTARTUP = &H1E
               :
            (中略)
               :
Private Const CSIDL_TEMPLATES = &H15  ' テンプレート

' BROWSEINFO.ulFlag定数
' ネットワークコンピュータの一覧選択
Private Const BIF_BROWSEFORCOMPUTER = &H1000  
' ネットワークプリンタの一覧選択
Private Const BIF_BROWSEFORPRINTER = &H2000   
' ネットワーク共有リソースの一覧選択
Private Const BIF_BROWSEINCLUDEFILES = &H4000 
' ネットワーク関連を除いて表示
Private Const BIF_DONTGOBELOWDOMAIN = &H2     
' 共有フォルダ表示とネットワークコンピュータの選択
Private Const BIF_RETURNFSANCESTORS = &H8     
' 共有フォルダ一覧選択
Private Const BIF_RETURNONLYFSDIRS = &H1      
Private Sub cmdFolder_Click()

  Dim usrBROWSEINFO As typBROWSEINFO
  Dim lngFolder As Long
  Dim strPath As String

  With usrBROWSEINFO
  ' 親ウィンドウを指定
    .hWndOwner = Me.hWnd
  ' ルートを指定
    strPath = cboRoot.Text
    strPath = Left$(strPath, InStr(strPath, ":") - 1)
    .pidlRoot = CLng(strPath)
  ' タイトル設定
    .lpszTitle = "フォルダを指定してください。"
  ' オプションを表示する
    .ulFlag = 0
    If chkOPt(0).Value = "1" Then
      .ulFlag = .ulFlag Or BIF_BROWSEFORCOMPUTER
    End If
    If chkOPt(1).Value = "1" Then
      .ulFlag = .ulFlag Or BIF_BROWSEFORPRINTER
    End If
    If chkOPt(2).Value = "1" Then
      .ulFlag = .ulFlag Or BIF_BROWSEINCLUDEFILES
    End If
    If chkOPt(3).Value = "1" Then
      .ulFlag = .ulFlag Or BIF_DONTGOBELOWDOMAIN
    End If
    If chkOPt(4).Value = "1" Then
      .ulFlag = .ulFlag Or BIF_RETURNFSANCESTORS
    End If
    If chkOPt(5).Value = "1" Then
      .ulFlag = .ulFlag Or BIF_RETURNONLYFSDIRS
    End If

      strPath = String$(256, vbNullChar)
      .pszDisplayName = strPath
  End With

' 「フォルダの参照」ダイアログを呼び出す
  lngFolder = SHBrowseForFolder(usrBROWSEINFO)

' ダイアログで得られた値からフォルダのパスを取得
  Call SHGetPathFromIDList(lngFolder, strPath)

' 割り当てられたメモリを解放
  CoTaskMemFree lngFolder

' 選択した情報を取得する
  If Left$(strPath, 1) = vbNullChar Then
  ' 表示名
    strPath = Left$(usrBROWSEINFO.pszDisplayName, _
              InStr(usrBROWSEINFO.pszDisplayName, _
                    vbNullChar) - 1)
  End If
  txtFolder.Text = strPath
End Sub
Private Sub Form_Load()
  cboRoot.AddItem "&H00:デスクトップ"
               :
            (中略)
               :
  cboRoot.AddItem "&H15:テンプレート"

  cboRoot.ListIndex = 0
End Sub

SHBrowseForFolder APIの使い方

 APIのパラメータはひとつだけだ。しかし、そのパラメータは構造体になっているので、実際には数個のパラメータがあるような感じがする。
 まず、構造体の「hWndOwner」には、このAPIはダイアログを表示するものなので、呼び出し元のウィンドウハンドルを指定する。
「pidlRoot」には、フォルダを表示する範囲を指定する。SHBrowseForFolder APIの説明では“ルートフォルダ”と呼ばれている項目だ。この項目に、たとえば「Network Computer」を指定すると、ネットワークコンピュータに含まれるフォルダ、つまりネットワークコンピュータやそこで公開されている共有資源が表示される。
「lpszTitle」には、ダイアログに表示するメッセージを指定する。
「ulFlag」には、表示するフォルダの種類を指定する。
「pszDisplayName」は、フォルダの表示名が返却されてくるので、あらかじめString(256, vbNullChar)などを代入して領域を確保しておく。表示名とは、たとえば「デスクトップ」や「マイドキュメント」など「フォルダの参照」ダイアログに表示される説明文だ。

SHGetPathFromIDList APIの使い方

 構造体の項目に値を設定してSHBrowseForFolder APIを呼び出すと、その復帰値として、IDListと呼ばれる情報を得ることができる。構造体の「pszDisplayName」には表示名しか設定されないので、このIDListからパスを得るために、SHGetPathFromIDList APIを使う。
 SHGetPathFromIDList APIの第1パラメータは、SHBrowseForFolder APIの戻り値、第2パラメータにはパスが返却されてくる領域を指定する。

CoTaskMemFree APIの使い方

 SHBrowseForFolder APIは、IDListの領域をタスク単位で確保するため、必要がなくなった段階で明示的に領域を解放するプログラムを記述しなければならない。領域の解放は、CoTaskMemFree APIにSHBrowseForFolder APIの戻り値を指定して行なう。

Windowsの機能を活用する
-フォルダを指定する2-

 フォルダを指定するときに、前回指定したフォルダを覚えておけば、「フォルダの参照」ダイアログを開いたときのデフォルトフォルダにできるので、利用者の手間を軽減できる。
 SHBrowseForFolder APIでデフォルトフォルダを指定するには、構造体の「lpfn」と「lparm」の2つを使用する。しかし、単純にデフォルトフォルダの値を指定すれば使えるという仕組みにはなっていない。
 では、SHBrowseForFolder APIはどのようにデフォルトフォルダを認識するのだろうか。

SHBrowseForFolder APIにコールバック関数を付与

 SHBrowserForFolder APIでデフォルトフォルダを扱う基本は、当たり前のことだが、「フォルダの参照」ダイアログ自体にデフォルトフォルダを通知することだ。しかし、当たり前ではないのが、通知するためのパラメータがない点だ。
 ではどうするのか。
 この「フォルダの参照」ダイアログを表示する直前にフォルダの情報を通知する問題を解決する仕組みとしてSHBrowseForFolder APIには、コールバック関数を指定する機能がある。この関数を指定することで、Windowsがダイアログを表示するときのロジックの中に、独自ロジックを割り込ませることが可能になる。この仕組みは、Windowsが画面の描画などをWindowsメッセージと呼ばれるプロセス間メッセージにより実現しているからこそ成り立っているものだ(図14)。

図14:コールバック関数の仕組み
図14

 このコールバック関数のアドレスをダイアログ用構造体の第6パラメータ「lpfn」に指定する。アドレス指定にはVisual Basic 5.0から追加されたAddressOf関数を使用するが、単純に代入式の右辺に記述できないので関数プロシージャを介して右辺に記述する。また、コールバック関数自体にデフォルトフォルダの情報を伝えるために第7パラメータ「lParam」にシフトJISで指定したデフォルトフォルダ名の領域(アドレス)を指定する(リスト14)。なお、シフトJISで文字列を渡すためには、StrConv関数によりUnicodeからシフトJISに変換したものをバイト配列に設定すればよい。

リスト14:コールバック関数「plngCallback」を指定する
  .lpfn = lngAddr(AddressOf plngCallback)
  abytPath() = StrConv(strInitPath & vbNullChar, _
                       vbFromUnicode)
  .lParam = VarPtr(abytPath(0))
                :
             (中略)
                :
Private Function lngAddr(ByRef rlngAddr) As Long
  lngAddr = rlngAddr
End Function

「フォルダの参照」ダイアログにデフォルトフォルダを通知

 コールバック関数は、Windowsメッセージを横取りするためのものなので、そのパラメータはあらかじめ決定している。
 コールバック関数の第1パラメータは、Windowsメッセージを受け取るウィンドウの識別子が設定される。第2パラメータには、Windowsメッセージの種類だ。そして、第3パラメータと第4パラメータはWindowsメッセージごとに決定される特定の指定値だ。
 SHBrowseForFolder APIのコールバック関数では、「フォルダの参照」ダイアログ向けのWindowsメッセージをダイアログに届く前に解析して、初期化メッセージが到着したときに、デフォルトフォルダを通知するメッセージを生成するような処理を記述する(リスト15)。
 このような仕組みを介して「フォルダの参照」ダイアログを表示すれば、前回選択したフォルダの初期表示が可能になる(図15)。

図15:デフォルトフォルダ指定のダイアログ
図15

リスト15 :「フォルダ指定」ダイアログにデフォルトフォルダを通知する
Private Declare Function SendMessage _
        Lib "user32" Alias "SendMessageA" _
       (ByVal hWnd As Long, _
        ByVal wMsg As Long, _
        ByVal wParam As Long, _
        lParam As Any) As Long
Private Const WM_USER = &H400
Private Const BFFM_INITIALIZED = 1
Private Const BFFM_SETSELECTION = (WM_USER + 102)
Public Function plngCallback(ByVal hWnd As Long, _
                             ByVal uMsg As Long, _
                             ByVal wParam As Long, _
                             ByVal lParam As Long) As Long
  Dim lngRet      As Long
    
  Debug.Print Hex(uMsg), Hex(wParam), Hex(lParam)
  If uMsg = BFFM_INITIALIZED Then
    lngRet = SendMessage(hWnd, _
                         BFFM_SETSELECTION, _
                         -1&, _
                         ByVal lParam)
  End If
End Function

Visual Basicの進歩がAPIの使用を不要にした例もある

 ここまで、Visual Basic 6.0を選択したとしても、APIを使わなければ実現できなかった機能を紹介してきた。
 しかし、以前はAPIしか選択肢がなかったものが、Visual Basicの標準機能で実現できるようになったものがある。たとえば、チェックボックス付きのリストボックスなどがその代表的な例だろう(図16)。

図16:チェックボックス付きリストボックス
図16

 また、Visual Basic 4.0では、フォームのメニューに多くの項目を設定したときに画面からメニューがはみ出してしまう問題が発生するので、ModifyMenu APIを使ってメニューの折り返す必要がある(リスト16・図17)。しかし、この方法では、図17の項目27など折り返した列の先頭項目を選択しても、メニューのクリックイベントが発生しないなど一筋縄ではいかない。別フォームにメニュー一覧をリスト表示した方が楽なくらいだ(図18)。このように苦労してきた問題だが、Windows 98+Visual Basic 6.0などでは、画面からメニュー項目がはみ出してスクロークさせることができるようになっている(図19)。
 このように、Visual Basicが新版になったり、サービスパックを適用したりすることで、GUI(Graphical User Interface)周りが変更されるときもある。昔のソースコードを流用するときに、APIで実現されている部分が、Visual Basicの標準機能としてすでに実現されていないかを確認するくらいはしたほうがよいだろう。

図17:メニューの折り返し
図17

図18:擬似的なメニュー
図18

図19:現在のバージョンでのメニュー表示
図19

リスト16:フォームメニューの折り返し
Private Declare Function ModifyMenu _
        Lib "user32" Alias "ModifyMenuA" _
       (ByVal hMenu As Long, _
        ByVal nPosition As Long, _
        ByVal wFlags As Long, _
        wlDNewItem As Long, _
        ByVal lpString As String) As Long
Private Declare Function GetMenu _
        Lib "user32" _
       (ByVal hWnd As Long) As Long
Private Declare Function GetSubMenu _
        Lib "user32" _
       (ByVal hMenu As Long, _
        ByVal nPos As Long) As Long
Private Const MF_MENUBARBREAK = &H20&
Private Const MF_BYPOSITION = &H400&
                :
             (中略)
                :
' 折り返しのAPI
  For ilngLoop = 27 To 100 Step 28
    lngRet = ModifyMenu(GetSubMenu(GetMenu(Me.hWnd), 1), _
                        ilngLoop, _
                        MF_MENUBARBREAK Or MF_BYPOSITION, _
                        ilngLoop, _
                        mnuAPILst(ilngLoop).Caption)
  Next

Visual Basicの標準機能も充実している
-テンポラリフォルダを取得する-

 APIは力強い相棒になりえるが、APIに盲目的に頼るのは問題がある。たとえば、一時的なワークファイル(テンポラリファイル)を格納する場所として、Windowsのテンポラリフォルダを利用したいとすれば、その情報を取得しなければならない。MSDNライブラリを参照してAPIの情報を得ることからはじめると、GetTempPath APIを発見する。しかし、Windowsはテンポラリフォルダの情報を「TEMP」環境変数に設定しており、Visual Basicには環境変数を取得するEnviron関数が存在する。Windowsに関する知識とVisual Basicの知識があれば、APIを使う必要はないのだ。この手のフォルダについては、他にも環境変数とEnviron関数の組み合わせで解決できてしまうものがある(表5)。APIに慣れはじめた時ほど、まずは“APIを使わなくても実現できるのではないか?”と自分に問いかけることも必要だろう。

表5:APIと環境変数の関係
API 環境変数
GetTempPathTEMP
GetWindwosDirectoryWINDIR

APIで発生したエラーを処理する

 APIで発生したエラーは、On Error Goto文などを使ってエラートラップできない。APIからの戻り値を判定し、正常終了していなければ、

Err.LastDllError
としてErrオブジェクトのプロパティにより、エラー番号を取得する。GetLastError APIを使ってもよいようにも思えるが、Visual Basicの標準機能でできることをAPIで実現する必要はない点と、GetLastError APIとVisual Basicの内部で使われているAPIとでは、エラーが発生していたときに取得できる値が変わってしまう点から推奨できない。
 Err.LastDllErrorにより取得できたエラー番号から独自のエラーメッセージを表示してもよいが、Windows APIの場合は、FormatMessage APIを使えば、エラー番号に対応したエラーメッセージを取得することが可能だ。

FormatMessage APIの使い方

 FormatMessage APIも汎用性のあるAPIなので、パラメータの設定をMSDNライブラリで調べてもなかなか理解できないかもしれない。しかし、Windows API関連のエラーメッセージを取得することにのみ注目すれば、使い方は比較的簡単になる(リスト17)。

リスト17:エラー処理(basLastErrより抜粋)
Private Declare Function FormatMessage _
        Lib "kernel32" Alias "FormatMessageA" _
       (ByVal dwFlags As Long, _
        lpSource As Any, _
        ByVal dwMessageId As Long, _
        ByVal dwLanguageId As Long, _
        ByVal lpBuffer As String, _
        ByVal nSize As Long, _
        ByVal Argument As Long) As Long

' Argumentを使わないことを指定
Private Const FORMAT_MESSAGE_IGNORE_INSERTS = &H200
' GetLastErrorに対応するメッセージを取得
Private Const FORMAT_MESSAGE_FROM_SYSTEM = &H1000
' デフォルト言語を指定
Private Const PROCESS_DEFAULT_LANGUAGE = &H400

Public Function pstrLastDllErrorText( _
       ByVal vlngDllError As Long) As String
  Dim strBuffer  As String * 1024
  Dim lngRet     As Long
  Dim lngFlg     As Long

  lngFlg = FORMAT_MESSAGE_FROM_SYSTEM Or _
           FORMAT_MESSAGE_IGNORE_INSERTS
  lngRet = FormatMessage(lngFlg, _
                         ByVal vbNullString, _
                         vlngDllError, _
                         PROCESS_DEFAULT_LANGUAGE, _
                         strBuffer, _
                         Len(strBuffer), _
                         0)
  pstrLastDllErrorText = Left$(strBuffer, _
                         InStr(strBuffer, _
                         vbNullChar) - 1)
End Function

     
  1. まず、第1パラメータには、Windows APIのエラーであることとArgumentパラメータを使わないことを指定する。
  2.  
  3. 第2パラメータも使用しないので、NULLを指定する。
  4.  
  5. 第3パラメータには、Err.LastDllErrorにより取得したエラー番号を指定する。
  6.  
  7. 第4パラメータには、規定の言語を使うことを指定する。
  8.  
  9. 第5パラメータには、エラーメッセージが返却される領域を指定する。返却項目なので、十分な長さの固定長文字列として定義するか、あらかじめ十分な長さのNULL文字を代入しておいて、領域を確保しておくことを忘れてはならない。
  10.  
  11. 第6パラメータには、エラーメッセージの返却領域の長さを指定する。
  12.  
  13. 第7パラメータは、先ほど使用しないとしたArgumentパラメータなので、0を指定する。
 この7つのパラメータを指定してFormatMessage APIを呼び出せば、エラーメッセージを得ることが可能だ。

最後に

 万人が使いこなせるとは言い難い面があるAPIだが、APIを使えるようになれば、他のプログラムとちょっと差をつけることも可能だ。だからこそ、難しさを乗り越えて、使いこなせるようになったときの楽しさも生まれてくるだろう。しかしながら、そういった楽しさを業務アプリケーションの作成の中に取り込むときには注意が必要だ。業務アプリケーションは、あくまでも開発会社やお客様のものであり、開発者個人が管理するものではない。そのメインテナンスは、開発者個人としてではなく、会社として(もしくはお客様自身が)行なってゆくことを考慮しなければいけないだろう。また将来的に、Windowsが.NET(ドットネット)化したときに、アーキテクチャの違いから現在のAPIが生き残ってゆくかどうかはまったく未知数であり、APIを直接使っているものは動作しないなどということだってありえる。そういったマイナス面を心の片隅に留めつつ、APIライフを堪能してほしいと思う。

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

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