ファイルコピーからクリスタルレポートまで

業務に効くAPI入門
-VB4.0/5.0/6.0対応-



初音 玲 HATSUNE, Akira



Visual Basicが普及した要因のひとつに、C言語からAPIを駆使してWindowsアプリケーションを作るという非効率的な方法から解放されたことが上げられる。APIを使わずにIDE上でVisual Basicにより提供されている部品(オブジェクト)を組み合わせてゆけば画面を表示できるのである。まさに「Visual」という名前を冠している製品に相応しい機能だ。そのVisual Basicでどうして、再びAPIを使うことに注目しなければいけないのだろう。

Visual Basicで出来ることと出来ないこと

提供されていないOS機能を使う

 Visual Basicは、万能なツールではない。Visual Basicが前提としているOSのUI(ユーザーインターフェイス)の部品がすべて揃っているわけでもない。これは、同じマイクロソフトの製品といえども、OSの開発スケジュールとVisual Basicの開発スケジュールの差異によることが大きいだろう。
 Visual BasicでAPIを使う理由のひとつは、この「部品として提供されていないOSのUIを実現する」ということだ。たとえ、部品として提供されていなくても、他のアプリケーションで実現されているのならば、なんらかのOSの機能として提供されている機能を組み合わせて実現しているはずだ。もちろん、Visual Basicに標準で備わっていない機能であっても、市販品を含めたCOMコンポーネント(ActiveXコントロールなど)などの部品を追加すればよいときも多いし、また、Visual Basic、Windows、Internet Explorer(以下IE)、Officeの新バージョンでオブジェクトとして実装されることもある。だからこそ、

OSが発売されたあとのバージョンの
Visual Basicを開発言語として採用すべし

OSが発売されたあとの
SPの適用が必須である
という助言が生まれてくるのだ。この助言を聞き入れれば「APIの宣言がわからない」と悩まなくても目的の機能が実装できるときもある。よって、この後紹介するAPIについても「やりたい事」を明確にして、Visual BasicやOSのバージョンが上がったときには、その「やりたい事」が実装されているかを確認する必要があるだろう。

互換性の問題を乗り越える

 発売時期を考慮するもうひとつのポイントとしては、互換性の問題がある。OSに新しいSPを適用したときや、Windows 95で動いていたシステムを98に切り替えてから、作成したアプリケーションの動作がどうもおかしいという事例を時々見聞きする。ソースコードレベルではなにも問題がないのに「どうも調子が悪い」というのが特徴だ。これは、SPや製品間の互換性に問題があるということだ。注意したいのは、このことはWindowsの互換性がないことを意味しているのではないことだ。もちろん、Windows 95/98系とWindows NT系は別系譜のOSなのだから、その両者に高いレベルの互換性を求めること自体が間違いなのだが、同じ系列においては、Windowsは、APIレベルでは極めて高い互換性を維持していると思う。それにも関わらず先ほどの助言が生きてくるのはどうしてだろう。
 通常、Visual BasicがWindowsの機能を使うとき、図1のような流れのどれかになる。ここで注目すべきは、MFC(Microsoft Foundation Class)の存在だ。このMFCは、Visual Basicだけではなく、Visual C++などの開発ツール、OfficeやIE、OS付属のソフトなどでも使っている共通DLLだ。MFCは、よく使うWindowsのAPI(Windows API)の組み合わせをマクロのように登録してあり、MFCを呼び出すことで、直に使うには面倒で使用方法を間違えやすいWindows APIを手軽に使えるようにした仕組みだ。しかし、このMFCが曲者で、さまざまなバージョンが存在するし、(最近のバージョンではわからないが)そのバージョン間での互換性も低いという代物なのだ。そして最悪なのが、どの製品(開発ツール、Office製品、インターネット関連製品、その他、MFCのバージョンの違いを意識していないで販売している一般市販品)で、どのバージョンのMFCを使っているかの情報がインストール前に不明であり、また一度インストールしてしまったら、ハードディスクの初期化とOSの再インストールから始めて、元の環境を再構築しなければいけないことなのだ。

図1:Windowsの機能へのアクセス経路
図1:Windowsの機能へのアクセス経路

 こんなことで大丈夫かと心配になってしまうが、きちんとしたソフト会社の製品では、MFCを使わずに独自に自社専用共通DLLを作成する方法や、Visual C++などDLLもスタティックにリンクしてEXEの中に取り込んでしまう(図2)機能をもった開発言語を選択して、環境に影響を与えず、また環境に影響を受けにくい作り方をしている。もちろん、スタティックリンクを使うことでEXEのサイズが大きくなり、必要ディスク容量や必要メモリ容量が増大して、パフォーマンスに影響を与えるかもしれない。しかし、

上手く動作しないというのは、
「処理時間∞」ということに匹敵する
のだから、多少の遅延よりも確実に動作するものを提供するのが、技術者として当然選択すべき方向だろう。

図2:MFCの非互換を回避する
図2:MFCの非互換を回避する

 マイクロソフトも同様に考えていると思われるが、MFC=自社専用共通DLLのようなものであるので、MFCの互換性を向上させる方向に、また、mfc40.dllやmfc42.dllのようにdll名を分けたりして乗り越えようとしているようだ。もっともせっかく分けたdll名ではあるが、同一dll名の異バージョン間の互換性も怪しいところがあるのは困ったものである。よって、自衛手段として、

OS、開発ツール、Office製品の
バージョン間の整合性に注意すべし
ということになる(詳しくは、1999年12月号の特集記事「VBの2000年問題」参照)。
 この自衛手段は、特にVisual Basicを代表とするスタティックリンクができない開発言語を選択したときに、とくに重要である。そして、運悪くOSだけが新しくなり、Visual Basicのバージョンを上げられなかったり、新しいVisual Basicのバージョンでも互換性問題が発生したときには、迷わずWindows APIを駆使して回避するのがよいだろう。
 同時に、Visual Basicで作成したアプリケーションは、ソースファイルとして納入し、納入先が実行環境にあったEXEファイルを構築できるようにするか、インストールプログラムで、同名ファイルが存在するときは、インストールする方のファイル日付やバージョンが新しくても上書きしないようにして、最悪でも作成したアプリケーションを配布したことにより、既存の環境を壊すことがないように注意したい。
 なお、Windows 2000では、このDLLの問題の解決策をOSの機能として提供することで、一気に解決を図るようなので、もしかしたら、インストールすることで環境を壊してしまうということは、今後はそれほど気にしなくてもよいのかもしれない。この機能は、Windows 2000が発売されたら、是非確認してみたい機能のひとつだ。

API使用の実例

 業務アプリケーション作成に役に立つAPIの実例を具体的に説明するのが今回の目的なのだが、どのようなAPIが業務アプリケーションの作成に役に立つか非常に悩んでしまった。そこで、比較的簡単に確認ができて、それでいて応用範囲が広い“ファイルをコピーする”ということに注目してサンプルを作ってゆくことにする。また、需要が多いであろうINIファイルへの読み書きと、Crystal Report APIについても解説する。
 なお、今回のサンプルは、Windows 95/98/NT4.0に実装されているAPIで、Visual Basic 4.0/5.0/6.0から使用した場合の動作を確認している。付録CD-ROMに収録したサンプルプログラムは、Windows 95 OSR2.2上でVisual Basic 6.0(SP3)を使ってコンパイルした。もし、Visual Basic 4.0をお使いになっている場合は、frmファイルをテキストエディタなどで開き、該当ロジック部分をコピーしていただきたい。
 また、Office 2000が導入されているPCでコンパイルしたので、もしOffice 2000を導入していなかったり、Visual Basic 5.0のときは、EXEファイルの再コンパイルを行なうか、IDE上で動作確認を行なっていただきたい。

シェル関数を使ったファイルコピー
(SHFileOperation API)

 APIのサンプルプログラムの1本目は、ファイルの操作時にファイルエクスプローラと同等の状況表示UIを実現することを目的としている。つまり、「ファイルエクスプローラと同じ状況表示を行ないたい」というのが“やりたい事”だ。この“やりたい事”を実現するための方法として、今回はSHFileOperation APIを使用する。

どこでSHFileOperationを知ったのか

 ところで、このAPIを使うにあたって、最初に疑問に思うのは“やりたい事”の実現にふさわしいAPIが「SHFileOperation API」であるということを、どうやって突き止めたのかという点だろう。Visual Basic付属のオンラインヘルプやMSDNライブラリを使って“やりたい事”から検索できたとしたら、それは奇跡的ともいえる幸運の持ち主だ。つまり、開発ツールに付属している情報から検索してSHFileOperation APIにたどり着くことは皆無に等しい。もちろんSHFileOperation APIの情報は掲載されているから、SHFileOperationという名前さえわかっていれば検索は容易だし、きちんと使うためにはMSDNライブラリに掲載されている内容を熟読する必要があるだろう。しかし、こと「やる方法」を探す手段としては、MSDNライブラリは、使い勝手がよくない。
 では、どうやって「SHFileOperation」という名前にたどり着いたのかといえば、それほど深く考えずにとりあえず購入していた『PCDN CD-ROM Vol.1』(有限会社イント・ツーワン)という製品があったからだ。普段の情報収集が役に立つという典型かもしれない。今だったら、インターネットでの検索も有効な手段だろう。
「SHFileOperation」という名前がわかってさえしまえば、あとはMSDNライブラリをじっくり眺めればよい。ただし、Visual Basic付属のAPIビューアを参考にしてはいけない。APIビューアは、かなり内容に間違えがあり、SHFileOperation APIについても、実体名を“ SHFileOperationA”と半角空白を先頭に付けた形で宣言しており、これを参考にすると確実に動かないプログラムを作ることができる。再度記述するが、

APIビューアではなく、
MSDNライブラリを参考にすること
が鉄則だ。

MSDNライブラリで定義を調べる

 SHFileOperation APIをVisual Studio 6.0のMSDNライブラリで調べると図3のように非常に簡単な定義であることがわかる。この定義をVisual Basicで使える形に書き換えると、

Declare Function SHFileOperation Lib _
"shell32" Alias "SHFileOperationA"
 (lpFileOp As SHFILEOPSTRUCT) _
As Long

となる。これを標準モジュール(basファイル)に記述すればいいのだが、今回は、フォームモジュールの中でしか使わないので、frmFileCopyオブジェクトのDeclarationsセクションに、

Private Declare Function 〜

として定義する。
 SHFileOperation APIの定義を記述する上で難しいのは、引数であるlpFileOpがユーザー定義構造体[SHFILEOPSTRUCT]である点だ。再びMSDNライブラリのお世話になり、SHFILEOPSTRUCTの定義を調べるとかなりの量の英文が表示される(図4)。しかし、ユーザー定義体自体の定義はそれほど難しくはない、

Private Type SHFILEOPSTRUCT
  hWnd                  As Long
  wFunc                 As Long
  pFrom                 As String
  pTo                   As String
  fFlags                As Long
  fAnyOperationsAborted As Long
  hNameMappings         As Long
  lpszProgressTitle     As String
End Type

でよいのだ。問題は、それぞれの引数の実値は調べなければならないことだ。今回は、Visual C++に付属しているヘッダファイルで調べ、その結果をリスト1のように定義した。この定数値もフォームモジュールのみで使うので、Private属性にしている。
 なお、Windows APIは、C言語から呼び出されることを前提にしているので、Visual C++の開発環境は必須であるとも言える(と書いてはいるが、Cの開発環境としては、C++Builderを愛用している)のだが、残念ながらWindows用のC言語の開発環境を購入していないときにはどのように調べたらよいかはわからなかった。
 FileCopy.exe(サンプル1)は、複写元と複写先のファイル名を指定して、コマンドを選び実行することでエクスプローラ風の状況表示画面を表示する。もちろん、削除時には、図5のような確認画面が表示され、ゴミ箱に捨てることもできる(図6)。これは、fFlagsプロパティに、

usrFileOp.fFlags = _
FOF_RENAMEONCOLLISION Or _
FOF_ALLOWUNDO

としているからだ。
 状況表示UIが必要なく、複数ファイルのコピーなどの機能を実現したいときもSHFileOperation APIは活用できるが、もし、Visual Basic 6.0以降であれば、FileSystemオブジェクトを使うことをお勧めする(Windows 98やWindows Scripting Hostを導入しているときも同様だ)。FileSystemオブジェクトを使えば、SHFileOperation APIで実現できることだけではなく、その他にもさまざまな機能をプログラムから操作することができる。この2つは相反する技術ではなく、ポイントはUIを使って人が指定するのか、UIを排除してプログラムから自動化するかで使い分ける補完技術だ。

図3:SHFileOperationの定義
図3:SHFileOperationの定義

図4:SHFILEOPSTRUCTの定義
図4:SHFILEOPSTRUCTの定義

リスト1:API定数宣言
Private Const FO_MOVE = &H1&
Private Const FO_COPY = &H2&
Private Const FO_DELETE = &H3&
Private Const FO_RENAME = &H4&
Private Const FOF_MULTIDESTFILES = &H1&    ' 複数の複写先を指定する
Private Const FOF_SILENT = &H4&            ' 進行状況画面を表示しない
Private Const FOF_RENAMEONCOLLISION = &H8& ' 重複しないファイル名を自動的に作成する
Private Const FOF_NOCONFIRMATION = &H10&   ' 表示されるダイアログボックスに「すべてはい」を選択
Private Const FOF_ALLOWUNDO = &H40&        ' 削除時にゴミ箱に捨てる
Private Const FOF_FILESONLY = &H80         ' ワイルドカード指定時にディレクトリを含まない
Private Const FOF_SIMPLEPROGRESS = &H100   ' 進行状況画面にファイル名を表示しない
Private Const FOF_NOCONFIRMMKDIR = &H200   ' ディレクトリ作成時の確認をしない
Private Const FOF_NOERRORUI = &H400        ' エラー時ダイアログを表示しない

図5:ファイルコピーの状況表示
図5:ファイルコピーの状況表示

図6:削除確認のユーザーインターフェイス
図6:削除確認のユーザーインターフェイス

図7:削除後のごみ箱の中
図7:削除後のごみ箱の中

UNCとFTP

 SHFileOperation API、FileSystemオブジェクト、そしてFileCopyコマンドなどでは、使用中のWindowsネットワークで公開している共有資源に対して、ドライブに割り当てなくてもUNC(Universal Nameing Code)パスを指定して操作する事が可能だ。これはDOSプロンプトのdirコマンドでも指定可能な方法だが、案外、知らない方が多い。たとえば、balthsalという名前のマシンが、type-1という共有フォルダをWindowsネットワーク上に公開しているとしよう。そして、その共有フォルダ上に画像ファイルとして、vbm.jpgがあったとき、その画像ファイルをUNCパスで指定すると

\\balthsal\type-1\vbm.jpg

となる。そして、このようにUNCパスで指定したときは、そのファイルを使っている間だけ、2つのマシン間の接続が保持されることになる。これは、何台ものマシンが存在するようなネットワークでは、ネットワークトラフィックス(ネットワーク上の混み具合)を低下させるのに効果的だ。もし、先程の共有フォルダをドライブにマウントしてしまうと、そのドライブから共有フォルダを切断するまでの間、定期的に接続を維持するためのパケットが流れることになる。本当に多少の低下かもしれないが、ネットワークは、接続台数が業務の拡大に応じて増加するなどすることも多く、(ほんの少しの改善)×(多くの台数)=(大きな改善)となることだってある。こういったところの積み重ねが、使い勝手のよいシステムを構築するノウハウといえるかもしれない。

UNCパスで指定できない局面

 Windowsネットワークさえ維持できていれば、UNCパスを効果的に使うことで情報共有は比較的簡単にできる。しかし、Windowsネットワークを維持するのが困難なときには、非常に不安定な接続方法となる。
 Windowsネットワークは、LANなどの高速回線上でいろいろなパケットをやり取りして、あたかもローカルディスクを操作しているときと同じような情報を取得できるように工夫している。そのため、LAN間を従量課金制の回線(たとえば、ISDNも含めた一般の電話回線)では、マシンの電源を入れている間、常に通話状態(課金状態)になり、膨大なランニングコストが発生してしまう。また、しっかり設定したセキュリティが脆弱だとは思わないが、簡単に自マシンの資源を共有資源として公開できてしまうので、インターネットからの接続を可能にしておくことも避けるのが得策だろう。そうなってくると、専用線を張り巡らせるような大規模なシステムまたは個人個人がダイアルアップで接続するような小規模なシステム以外の、多分多くの企業システムが位置付けられるような中規模なシステムには向かないことになる。これは、データ通信でさえも従量課金制になっている日本と(マイクロソフトの資料によれば)ドイツ固有の問題であり、インフラが整っていないと見なされ、根本的な解決が図られそうもないのだ。ルータを設定してWindowsネットワークを維持するためだけの情報の従量課金制回線への流出を防ぐこと可能だが、そうした場合、そのような回線をまたがったWindowsネットワークを構築することができなくなる。では、どうしたらよいのか。
 答えは簡単だ。
 本来のインターネット技術を使えばいいのだ。インターネット上にWindowsネットワークを構築するのではなく、HTTPやFTPなどのインターネット生粋のプロトコルを選択すればよい。HTTPやFTPならば、ファイアウォールや利用権限などもインターネットにフィットした形で設定することもできる。

Webフォルダについて

 IE 4.0や5.0を導入することで、ファイルエクスプローラにWebフォルダという項目が現われる。これは、マイクロソフトが提唱するデスクトップとインターネットの統合の一例だ。このWebフォルダを利用すれば、Windowsネットワーク上の共有フォルダにあるファイルを操作するように、インターネット上のファイルを操作することができる。しかし、これはHTTPやFTP上に、さらに独自プロトコルを追加して利用している技術なので、普通のHTTPサーバーやFTPサーバーに対しては適用できない。WebDAV(WWW Distributed Authoring and Versioning)やMicrosoft FrontPageサーバー上でWebフォルダを設定しなければならない。

WinInet関数を使ったファイルコピー
〜FtpGetFile〜

 HTTPやFTPをVisual Baicから実装するとき、Visual Basic 5.0以降が手元にあれば、MSINET.ocxを利用する方法が思い浮かぶだろう。しかし、このocxは、細かな指定はすべて、IEの設定に依存する仕様になっていて、設定画面を別に用意しなくてもよいという反面、ほかのソフトの設定に依存せずにシステムを稼動させたいという一般的なニーズを実現することはできない。また、MSINET.ocx自体の動作が不安定であるという報告もある。さらに、MSINET.ocx独自の障害もある(表1)。そこで、APIを使うもうひとつの局面、

APIでVisual Basicのバグを回避する
に基づいて、APIを使ってHTTPやFTPを実装してみる。
 APIを使って実装する方法としては、Winsock.dllをAPI呼び出して、TCP/IPやUDP/IPを使って作る方法もあるが、今回は、WinInet.dllを使って、HTTPやFTPに相当するAPIを呼び出すことにする(図8)。このWinInet.dllは、MSINET.ocxからも使っているIE 3.0以上がインストールする共通ファイルだ。

表1:WinInet関連の障害情報
Article ID Title 対象 Reviewed
Q176419 Inet Control Method OpenURL Fails If No Proxy Selected MSINET.ocx (VB5.0/6.0) 1997/11/06
Q168766 大/小文字を区別するサーバーでのインターネットトランスファの動作 MSINET.ocx (VB5.0) 1997/08/27
Q167696 Debug Static MFC Library Links to UrlMon.dll/Wininet.dll MFC (VC++5.0) 1997/06/16
Q160060 Error Initializing the WinInet Cache WinInet.dll (IE3.01以前) 1998/01/22
Q176176 InternetOpenUrl Does Not Work When Called Asynchronusly WinInet.dll 1997/11/05
Q176439 InternetSetOption Does Not Set Credentials for NTLM Scheme WinInet.dll 1997/11/07
Q176420 InternetSetOption Does Not Set Timeout Values WinInet.dll 1997/11/06

出典:MSDNライブラリ VS6.0 Knowledge Base

図8:MSINET.ocx
図8:MSINET.ocx

MSDNライブラリで定義を調べる

 WinInet.dllのFTP用API定義をVisual Studio 6.0のMSDNライブラリで調べると図9および表2のような構成になっていることがわかる。MSDNライブラリやC言語のヘッダファイルを参照して、API宣言(リスト2)やAPI定数宣言(リスト3)をまとめておけば、実際のプログラムの手間は大して掛からない。
 たとえば、ファイルを取得する最短手順は、

InternetOpen
InternetConnect
FtpGetFile
InternetCloseHandle

となる。では、具体的にプログラムの流れを追ってみよう。

図9:WinInet.dll FTP API
図9:WinInet.dll FTP API

表2:WinInet.dllのFTP関連API一覧
API 概要
InternetOpen 接続のための情報を初期化する
InternetConnect FTPサーバーと接続する
FtpCreateDirectory FTPサーバー上に新規ディレクトリを作成する
FtpDeleteFile FTPサーバーからファイルを削除する
FtpGetCurrentDirectory FTPサーバー上のカレントディレクトリを取得する
FtpGetFile FTPサーバーからファイルを取得する
FtpPutFile FTPサーバーへファイルを転送する
FtpRemoveDirectory FTPサーバーからディレクトリを削除する
FtpRenameFile FTPサーバー上のファイル名を変更する
FtpSetCurrnetDirectory FTPサーバー上のカレントディレクトリを指定する
InternetCloseHandle InternetConnectで取得した情報領域を解放する
FtpOpenFile FTPサーバー上で読み書きするファイルを指定する
InternetQueryDataAvailable 指定したファイルから読み込み済みのバイト数を返却する
InternetReadFile 指定したファイルから読み込む
InternetWriteFile 指定したファイルに書き込む
InternetCloseHandle FtpOpenFileで取得した情報領域を解放する
FtpFindFirstFile 条件に一致するファイル名の集合を返却する
InternetFindNextFile ファイル名の集合からファイル名を取得する
InternetCloseHandle FtpFindFirstFileで取得した情報領域を解放する

リスト2:API宣言
Private Declare Function InternetOpen Lib "wininet.dll" _
  Alias "InternetOpenA" (ByVal lpszAgent       As String, _
                         ByVal dwAccessType    As Long, _
                         ByVal lpszProxyName   As String, _
                         ByVal lpszProxyBypass As String, _
                         ByVal dwFlags         As Long) As Long

Private Declare Function InternetConnect Lib "wininet.dll" _
  Alias "InternetConnectA" (ByVal hInternetSession As Long, _
                            ByVal lpszServerName   As String, _
                            ByVal nServerPort      As Long, _
                            ByVal lpszUserName     As String, _
                            ByVal lpszPassword     As String, _
                            ByVal dwService        As Long, _
                            ByVal dwFlags          As Long, _
                            ByVal dwContext        As Long) As Long

Private Declare Function FtpGetFile Lib "wininet.dll" _
  Alias "FtpGetFileA" (ByVal hFtpSession As Long, _
                       ByVal lpszRemoteFile As String, _
                       ByVal lpszNewFile As String, _
                       ByVal fFailIfExists As Long, _
                       ByVal dwLocalFlagsAndAttributes As Long, _
                       ByVal dwInternetFlags As Long, _
                       ByVal dwContext As Long) As Long

Private Declare Function InternetCloseHandle Lib "wininet.dll" _
    (ByVal hInternetSession As Long) As Long

Public Declare Function FtpPutFile Lib "wininet.dll" _
  Alias "FtpPutFileA" (ByVal hFtpSession As Long, _
                       ByVal lpszLocalFile As String, _
                       ByVal lpszNewRemoteFile As String, _
                       ByVal dwFlags As Long, _
                       ByVal dwContext As Long) As Long

リスト3:API定数宣言
Private Const INTERNET_OPEN_TYPE_PRECONFIG  As Long = 0&         ' IEの設定に従い接続
Private Const INTERNET_OPEN_TYPE_DIRECT     As Long = 1&         ' 直接接続
Private Const INTERNET_OPEN_TYPE_PROXY      As Long = 3&         ' Proxy経由接続
Private Const INTERNET_DEFAULT_FTP_PORT     As Long = 21&        ' RFCで既定されたftpポート
Private Const INTERNET_DEFAULT_HTTP_PORT    As Long = 80&        ' RFCで既定されたhttpポート
Private Const INTERNET_DEFAULT_HTTPS_PORT   As Long = 443&       ' RFCで既定されたhttpsポート
Private Const INTERNET_SERVICE_FTP          As Long = 1&         ' FTPを指定
Private Const INTERNET_SERVICE_HTTP         As Long = 3&         ' HTTP/HTTPSを指定
Private Const INTERNET_FLAG_PASSIVE         As Long = &H8000000  '
Private Const INTERNET_FLAG_RELOAD          As Long = &H80000000 ' キャシュを使わない
Private Const FTP_TRANSFER_TYPE_ASCII       As Long = &H1&       ' Asciiモードで転送
Private Const FTP_TRANSFER_TYPE_BINARY      As Long = &H2&       ' Binaryモードで転送


手順1 InternetOpen

 MSDNライブラリによるとInternetOpenのAPI定義は、

HINTERNET InternetOpen(
  IN LPCSTR lpszAgent,
  IN DWORD dwAccessType,
  IN LPCSTR lpszProxyName,
  IN LPCSTR lpszProxyBypass,
  IN DWORD dwFlags
);

となる。
 InternetOpen自体は、hInternetというハンドルを返却するVisual BasicのLong型関数に相当する。このハンドルは、WinInet.dllが接続しているアプリケーションを識別するための識別子だ。
 第1パラメーターのlpszAgentは、クライアント名を設定する文字列型変数だ。適当な名前を設定してもよいのだが、GetComputerName APIを使って、きちんとマシン名を設定する方が格好よい。GetComputerName APIはリスト4のように使う。
 第2パラメーターは、FTPサーバーとの接続方法を指定する。指定できる値とその意味を表3にまとめた。IEと独自アプリケーションで異なった接続方法を使うことはないはずなので、通常、IEの設定(図10)をそのまま使うのがよいだろう。もちろん、独自アプリケーションのみプロキシを経由させて、IEからは接続できないサーバーとやり取りするなどということも可能だ。
 第3パラメーターと第4パラメーターは、それぞれプロキシサーバー名とプロキシを使わないサイトを指定する。プロキシを使わないときは、vbNullStringを設定する。
 第5パラメーターは、WinInet.dll(でハンドルにより識別している動作単位)の動作モードを示していて、非同期動作、キャッシュからの読み出しなどの動作オプションを指定することができる。今回は、オプション動作を禁止して、確実に稼動し、その状況がわかりやすい基本動作を指定(0&)することにする。

リスト4:GetComputerName API使用例
Private Declare Function GetComputerName Lib "Kernel32" _
 Alias "GetComputerNameA" (ByVal lpBuffer  As String, _
                           ByRef nSize     As Long) As Long

Dim strBuf   As String * 34
Dim lngSize  As Long
Dim lngRet   As Long
Dim strTerm  As String

lngRet = GetComputerName(strBuf, lngSize)
If lngRet>0 Then
  strTerm = left$(strBuf, Instr(strBuf, vbNull) -1)
End If
MsgBox strBuf, vbOKOnly + vbExclamation

表3:dwAccessType
意味
INTERNET_OPEN_TYPE_PRECONFIG IEでの設定済みの内容を適用する
INTERNET_OPEN_TYPE_DIRECT 直接接続する
INTERNET_OPEN_TYPE_PROXY プロキシ経由で接続する

図10:IE5.0における設定画面
図10:IE5.0における設定画面

手順2 InternetConnect

 InternetOpenで戻ってきたハンドルを使って、InternetConnectでは、接続先および接続プロトコルを指定する。
 API定義は以下のようになる。

HINTERNET InternetConnect (
  IN HINTERNET hInternetSession,
  IN LPCSTR lpszServerName,
  IN INTERNET_PORT nServerPort,
  IN LPCSTR lpszUsername,
  IN LPCSTR lpszPassword,
  IN DWORD dwService,
  IN DWORD dwFlags,
  IN DWORD dwContext
);

 InternetConnectの戻り値は、Visual BasicのLong型に対応するセッション情報だ。このセッション情報を使って、データ転送系のAPIを呼び出すことになる。
 InternetConnectについてもパラメーターはすべて入力パラメーター。第1パラメーターには、InternetOpenの戻り値を指定する。
 第2パラメーターlpszServerNameには、FTPサーバー名を指定する。たとえば、

strSrv = "FTP.shoeisha.co.jp"

のように指定する。
 第3パラメーターnServerPortには、FTPのコントロール用のポート番号である“21”を指定する。
 第4パラメーターと第5パラメーターには、FTPの利用者IDとパスワードを指定する。なお、FTPサーバー側で特定のユーザーからのアクセスのみを許可してセキュリティを確保しているときには、サーバー管理者からIDとパスワードを教えてもらい、その値を指定する。また、公開FTPサーバー(anonymousサーバー)のときには、ユーザーIDには「anonymous」、パスワードには自分のメールアドレス(たとえば、「hatsune@junk.com」)を指定する。MSDNライブラリには、この両者にvbNullStringを指定すれば、勝手にanonymousと既定のパスワードを設定すると書かれている(表4)が、この既定のパスワードの値が極めてあやふやで、インストールしているIEのバージョンなどに大きく左右されてしまうようだ。それだったら、最初からきちんと指定しておけばよいことになる(サンプルプログラムでは、画面上で指定していないときに、anonymousと見なして文字列を設定してから、InternetConnectを呼び出している)。
 第6パラメーターdwServiceは、接続プロトコルの種類を指定するパラメーターで、今回は、INTERNET_SERVICE_FTPを指定して、FTPを指定している。その他にHTTPやgopherも指定できる。
 第7パラメーターdwFlagsは、いわゆるPASSIVEモードとしてデータ転送用のコネクションを張るかどうかを指定する。PASSIVEモードとは、データ転送用のコネクションを張るときに、サーバー側からではなく、クライアント側からコネクションを張ることだ。クライアント側からコネクションを張ることで、FireWallの中にあるクライアントからFireWall外の公開FTPサーバーを利用することができる(図11)。
 最後の第8パラメーターは、コールバックなどの特殊な使い方をするときに指定するものなので、今回は0&を指定する。

表4:InternetConnectのパラメーターの組合せ
パラメーター FTPに送られる名前
ユーザー名  パスワード  ユーザー名  パスワード 
なし なし anonymous PASS
指定あり なし 指定値 空文字
なし 指定あり エラーとなる
指定あり 指定あり 指定値 指定値

図11:PASSIVEモードの利用方法
図11:PASSIVEモードの利用方法

手順3 FtpGetFile

 さて、InternetConnectによりFTPのコントロール用コネクションが確立したら、そのコネクションを使って、データ転送用の命令を送受信する(FTPでは、実際にデータ転送が行なわれるのは、データ転送用のポートであることに注意)。
 たとえば、FTPサーバーからデータを取得するには、FtpGetFileを使う。FtpGetFileのAPI定義は、

BOOL FtpGetFile(
  IN HINTERNET hFtpSession,
  IN LPCSTR lpszRemoteFile,
  IN LPCSTR lpszNewFile,
  IN BOOL fFailIfExists,
  IN DWORD dwLocalFlagsAndAttributes,
  IN DWORD dwInternetFlags,
  IN DWORD dwContext
);

である。その戻り値は、TRUE(≠0&)かFALSE(=0&)で、API関数の実行が成功したときにはTRUEを返す。なお、FtpGetFileは、データ転送用のコネクションの接続切断を自動的に行なってくれるので、その存在を特に意識しなくてもファイルの取得プログラムを完成させることができる。
 パラメーターを具体的に見ていくと、第1パラメーターは、もちろんInternetConnectの戻り値であるコントロール用コネクションのハンドルだ。
 第2パラメーターは、FTPサーバー上のファイル名を指定する。FTPサーバー上のカレントディレクトリに存在しないときは、絶対パスや相対パスを使ってファイル名を指定する。
 第3パラメーターは、コピー先のファイル名を指定する。もちろん、ここでも絶対パスや相対パスを使うことができる。そして、FtpGetFileのときのコピー先とは、ローカルマシンであるWindowsマシンに他ならないので、UNCを指定することでWindowsネットワークの共有フォルダ上にコピーすることもできる。
 第4パラメーターのfFailIfExistsは、コピー先のファイル名が存在するときに、TRUE(≠0)になる。
 第5パラメーターは、所得したファイルを格納するときのファイルの属性を指定する。FTPサーバー上のファイル属性を反映したいときなどに必要だ。今回は属性に関しては、新規ファイル扱いにするので、特に指定はしない。
 第6パラメーターは、FTPの転送タイプを指定する。つまり、バイナリモードなのかアスキーモードなのかということだ。また、キャッシュを使うように指定することもできる(表5)。より安定度と安全性を求めるのならば、バイナリモードかアスキーモードかの指定のみ行ない、そのほかの値を指定せずに置くのがよいだろう。
 最後の第7パラメーターは、コールバックなどの特殊な使い方をするときに指定するものなので、今回はInternetConnectと同様に0&を指定する。

表5:FtpGetFileのパラメーター値
FTP_TARNSFER_TYPE_ASCII
FTP_TARNSFER_TYPE_BINARY
FTP_TRANSFER_TYPE_UNKONWN
INTERNET_FLAG_TRANSFER_ASCII
INTERNET_FLAG_TRANSFER_BINARY
INTERNET_FLAG_DONT_CACHE
INTERNET_FLAG_HYPERLINK
INTERNET_FLAG_MAKE_PERSISTENT
INTERNET_FLAG_MUST_CACHE_REQUEST
INTERNET_FLAG_NEED_FILE
INTERNET_FLAG_NO_CACHE_WRITE
INTERNET_FLAG_RELOAD
INTERNET_FLAG_RESYNCHRONIZE

手順4 InternetCloseHandel

BOOL InternetCloseHandle(
  IN HINTERNET hInet
);

 InternetCloseHandleは、ハンドルの解放を行なう関数だ。ハンドルの解放は、たとえば、IntenetConnectで取得したハンドルであれば、FTPセッションを切断することを意味する。この関数を呼び忘れたり、呼びすぎたりすると、メモリリークなどを起こし、使っていると不安定になるプログラムとなってしまう可能性があるので、十分注意して欲しい。

サンプルを動かす

 さて、上記の4つのAPIを使ったサンプルがFTPGet.exeだ(図12)。FTPサーバーおよびそこにあるファイルの名前、複写先のファイル名を指定して、[実行]をクリックすれば、FTPからのファイルの取得が完了する。FTPサーバー名を予め登録しておき、サーバーごとにユーザーIDとパスワードを管理できるようにするなどの工夫を行えば、使いやすさも向上する。実際に業務使用に耐えるだけの操作性の実装は、各自で工夫して欲しい。

図12:FTPGETサンプル
図12:FTPGETサンプル

INIファイルを扱うAPI COLUMN 1
 最近のVisual Basicでは、APIを使わなくてもSetSettingステートメントやGetSettingステートメントを使ってレジストリに情報を格納することが可能だ。しかし、相変わらずINIファイルを扱う機能は実装されていない。確かに、INIファイルではなくレジストリの方が誤ってファイルを削除されてしまう危険性も少ないし、ユーザーごとに保存値が管理できるなどいろいろな利点がある。また、マイクロソフトもレジストリによるアプリケーション情報の一括管理を推奨している。しかしながら、設定値をすぐに見ることができたり、ファイル転送だけで設定が変更できるなどの利点がINIファイルにはある。今回のFtpGetサンプルでは、最後に使ったFTPサーバー名、ユーザー名およびパスワードを保存するのにINIファイルを使っている。そして、INIファイルの入出力には、Visual Basicのファイル関連の機能を使うのではなく、APIを使ってスマートに行なってみた。(本当に時々だが)INIファイルに対してテキストファイルとしてOpenして、その内容を読み込みVisual Basicで内容を解析することでINIファイルとの入出力を行なっているサンプルプログラムや業務プログラムを見かけるが、テキストファイルの扱い方を説明することが主眼のサンプルでなければ、INIファイルの入出力はAPIを使って見せるのが意味あるサンプルといえるだろう。ましてや業務プログラムでINIファイル関連のAPIを使っていないとすれば、言わずもがなだ。
 今回使っているINIファイル関連のAPIは、

GetPrivateProfileString
WritePrivateProfileString

の2つだ。この2つのAPIを使えば、任意の名前のINIファイルに対して入出力が可能だ。API宣言部と使い方の例をリストにまとめてみたのでサンプル解読の参考にして欲しい。

WinInet関数を使ったファイルコピー
〜FtpPutFile〜

 FTPサーバーからファイルを取得できたならば、次にFTPサーバーにファイルを格納できるようにする。これによってファイル取得→更新→ファイル格納というような、Windows共有フォルダ上にファイルを編集する仕組みをFTPサーバーに対して実現できる。そのためには、FtpPutFile関数の使い方を見てゆく必要がある。
 FtpPutFile関数は、FtpGetFile関数と対をなす関数で、そのAPI定義は、

BOOL FtpPutFile(
  IN HINTERNET hFtpSession,
  IN LPCSTR lpszLocalFile,
  IN LPCSTR lpszNewRemoteFile,
  IN DWORD dwFlags,
  IN DWORD dwContext
);

となる。
 戻り値や第1パラメーターは、FtpGetFileと同様だ。つまり、戻り値はFtpPutFileの実行結果、第1パラメーターはFTPセッションのハンドルだ。
 第2パラメーターは、FTPサーバーに格納したいローカルファイルの名前を指定する。もちろん、UNCを指定して、Windowsネットワークの他の場所にあるファイルを指定することもできる。
 第3パラメーターは、FTPサーバー上の格納ファイル名を指定する。FTPサーバー上のカレントディレクトリ以外に格納するときは、絶対パスや相対パスを使ってファイル名を指定する。
 第4パラメーターは、FTPの転送タイプとキャッシュ利用の有無などを指定する(表6)。安定度と安全性を最重要課題とするならば、ここに設定するのは、転送モードのみの値になる。
 最後の第5パラメーターは、FtpGetFileの最後のパラメーターと同様にコールバックなどの特殊な使い方をするときに指定するものなので、今回は0&を指定する。

表6:FtpPutFileのパラメタ値
INTERNET_FLAG_TRANSFER_ASCII
INTERNET_FLAG_TRANSFER_BINARY
INTERNET_FLAG_DONT_CACHE
INTERNET_FLAG_HYPERLINK
INTERNET_FLAG_MAKE_PERSISTENT
INTERNET_FLAG_MUST_CACHE_REQUEST
INTERNET_FLAG_NEED_FILE
INTERNET_FLAG_NO_CACHE_WRITE
INTERNET_FLAG_RELOAD
INTERNET_FLAG_RESYNCHRONIZE


画像データベースに応用する

画像はどこに保存するのか

 SHFileOperation APIやWinInet APIを使って、ファイル転送を行なうような業務としては、画像ファイルをはじめとするファイル管理データベースの構築だろう。このようなシステムを構築する場合、管理するファイルをどのように保存しておくかがシステム構築の大きなポイントとなる。Microsoft Accessなどに親しんでいると、何の疑問もなくデータベースにファイル自体を格納するような設計をするだろう。ここでは、あえて「それは間違いだ」と断言する。特にAccessでは運用にかなりの工夫が必要になってくる。それは、Accessでは、mdbファイル自体をワークファイルとして使うため、テーブルのレコードを追加削除しただけではなく、クエリーの実行などを行なってもファイルサイズが増加してゆくので、定期的な「最適化」の実行が必要不可欠なためだ。そして、「最適化」の実行時間は、mdbファイルのサイズに依存するので、画像ファイルなどの大きな情報を格納することで、運用が難しくなる傾向にある。
 そこで、データベースには、ファイル管理情報(検索キー)とファイル格納場所(転送プロトコル、サーバー名およびファイル名)のみを格納し、ファイルはファイルとして存在するようにする方法を推奨したい。このときの利点と欠点を表7にまとめた。もちろん、管理対象のファイルとしては、htmlファイルでもよいので、urlと管理情報を格納して、社内情報だけではなく、発売元や製造元の情報すらも、同時に検索することで、多角的な情報の入手も可能になるということだ。このときは、今までの利点以外にも、データベースに直接格納するものと違い、検索したときの最新情報が得られるという利点も生まれてくる。シームレス時代にふさわしいシステムだと言えるだろう。

表7:ファイル格納方式とデータベース格納方式の違い

ファイル格納方式 データベース格納方式
ファイルの登録 ファイル自体の登録はファイルの移動でよい バイナリデータの扱いに注意しながら専用アプリケーションを開発する必要あり ×
ファイルの変更 単純なファイルならば、普通のファイル更新でよい バイナリデータの扱いに注意しながら専用アプリケーションを開発する必要あり ×
ファイルの参照 ファイル自体の参照は、temp領域へファイルをコピーして、それを専用アプリケーションや拡張子に対応したアプリケーションに読み込めばよい(ブラウザでの画像ファイルの扱いと同様) バイナリデータの扱いに注意しながら専用アプリケーションを開発する必要あり ×
テーブルのメンテナンス ファイルとの関連を維持してゆく必要あり 使用領域を効率的に使えないことが多い ×

セキュリティを考慮する

 もし、セキュリティを考慮して、ファイルの取得や更新を制限したいときは、FTPサーバーの設定や共有フォルダのプロパティで設定可能だ。また、ファイアウォールなどを活用して、インターネット、イントラネット、エクストラネットからのどこからアクセスしているかにより、取得や更新を制限することも可能だ。

Windows 2000ではどうなるのか

 Windows 2000のファイルシステムでは、ファイルに対して「概要」を設定可能で、その概要をキーにして検索することで、ファイル管理データベースもどきを実現することもできる。しかし、広くインターネットに散らばっているファイルを検索するとしたら、Windows 2000だとしても役不足だろう。今回の考え方は、まだまだ暫くの間は、十分第一線の仕様として通用するものだと思っている。

Oracle8iの先駆性 COLUMN 2
 ファイルはファイルのままで管理する方法を説明してきたが、この考えと似たような機能を実装したデータベース製品が存在する。それが、Oracle8iだ。Oracle8iは、単純にOracle8 R8.0.xの次版であるR8.1.xだというだけではない。Oracle8iのIにはインターネットという意味もあり、Oracle iFS(Internet File System)というオプション機能を使うことで、iFSが管理している保存領域に格納したファイルは、Oracle8iの管理下に置かれ、テーブルに対するセキュリティやトランザクション処理と同様の処理が行なえる。もちろん、iFSに対しては、普通のファイル操作方法(たとえば、Windowsネットワークの共有フォルダとしておくなど)以外にもOracleのネットワーク環境を使った操作なども可能だということだ。

CrystalReport API
(VB4.0/5.0のみに対応)

 クリスタルレポートは、ActiveXコントロール(.ocx)として提供され、しかも、Visual Basicに付属の“おまけ”コントロールでは、ヘルプファイルに記述されていないために、APIを使って操作可能なことを知らない方が多い。もしActiveXコントロールの機能が、APIでできることをすべてサポートしているのならば問題はないのだが、必ずしもそうではないことが問題となる。Visual Basic 4.0に対応したクリスタルレポート 4.5(このバージョンのサブセット版はVisual Basic 5.0にも添付されている)でも、ActiveXコントロールとAPIには、記事末の表8のような違いがある。APIを使えばプレビューウィンドウに対する操作や設置値の事前チェックなどが可能になる。Crystal Report APIの存在を知らないとActiveXコントロールで表示したプレビューウィンドウのハンドルを取得して、そのウィンドウに対してSendMessage APIなどのWindowsメッセージを送りつけるようなプログラムを作ってしまうかもしれない。確かにその方法でも同様な機能は実現可能かもしれないが、クリスタルレポートが本来もっている機能を使った方が安定しているし、プログラム作成も容易だ。
 Crystal Report APIの基本的な使い方の手順は

PEOpenEngine
PEOpenPrintJob
PEGetNthTableLoaction
PESetNthTableLocation
PEOutputToPrinter
PEStartPrintJob
PECanCloseEngine
PEClosePrintJob
PECloseEngine

となる(記事末リスト5)。

手順1 PEOpenEngine

Declare Function PEOpenEngine
  Lib "CRPE32.DLL" () As Integer

 PEOpenEngineは、Crystal Report APIの使用開始を宣言するAPIだ。この戻り値が1ならば正常に処理が終了したことを意味し、1以外ならば異常が発生したということになる。

手順2 PEOpenPrintJob

 実際に印刷するrptファイル名を指定する。この関数の戻り値もハンドルであり、このハンドルを使って以降の処理対象を指定することになる。

Declare Function PEOpenPrintJob
  Lib "CRPE32.DLL" (
    ByVal RptName As String
  ) As Integer

 もし、戻り値が0のときは、異常が発生していることになる。

手順3 PEGetNthTableLocation

 rptファイルに設定されているテーブル(データ)の位置などを取得する。この取得した情報に対して、PESetNthTableLocationを使って新たなテーブルの位置を設定する。
 このようにプログラムでテーブルの位置を指定することで、開発環境と異なるディレクトリ構成を持った実行環境でも、テキストファイルの内容をクリスタルレポートで正常に印刷することができる。

Declare Function
  PEGetNthTableLocation
  Lib "CRPE32.DLL" (
    ByVal PrintJob As Integer,
    ByVal TableN As Integer,
    Location As PETableLocation
  ) As Integer

 なお、戻り値が1以外のときは、異常が発生したことになるので、PEGetErrorTextなどで詳細を取得する。

手順4 PESetNthTableLocation

 テーブル位置は、PETableLocation構造体の.Locationメンバなので、ここにテキストファイルのファイル名を末尾にvbNull(=Chr$(0))を追加して設定する。

typLoc.Location = strFileName & vbNull

Declare Function
  PESetNthTableLocation
  Lib "CRPE32.DLL" (
    ByVal PrintJob As Integer,
    ByVal TableN As Integer,
    Location As PETableLocation
  ) As Integer

 なお、戻り値が1以外のときは、異常が発生したことになるので、PEGetErrorTextなどで詳細を取得する。

手順5 PEOutputToPrinter

 rptファイルとそれに設定する情報の入ったテキストファイルの指定が完了したら、いよいよ印刷先を指定する。もちろん、一度プレビュー画面を表示することも可能だが、今回は直接印刷するために、PEOutputToPrinterを使う。

Declare Function PEOutputToPrinter
  Lib "CRPE32.DLL" (
    ByVal PrintJob As Integer,
    ByVal nCopies As Integer,
  ) As Integer

 第2パラメーターは印刷部数であり、通常は1を指定する。
 なお、戻り値が1以外のときは、異常が発生したことになるので、PEGetErrorTextなどで詳細を取得する。

手順6 PEStartPrintJob

 出力先も決まったら、レポートの出力を開始する。そのためには、PEStartPrintJobを使う。

Declare Function PEStartPrintJob
  Lib "CRPE32.DLL" (
    ByVal PrintJob As Integer,
    ByVal WaitNoWait As Integer
  ) As Integer

 注意する点は、クリスタルレポートのバージョンが1.1以降のときは、第2パラメーターはTrueしか指定できなくなっている点だ。
 もちろん、戻り値が1以外のときは、異常が発生したことになるので、PEGetErrorTextなどで詳細を取得する。

手順7 PECanCloseEngine

 後処理であるPECloseEngineを呼び出すためには、事前にPECanCloseEngineで処理が終了しているかを確認する必要がある。

Declare Function PECanCloseEngine
  Lib "CRPE32.DLL" () As Integer

 このAPIの戻り値が1になったらプリントジョブの処理が終了したことになる。

手順8 PEClosePrintJob

 PEOpenPrintJobに対応する後処理用のAPIがPEClosePrintJobだ。

Declare Function PEClosePrintJob
  Lib "CRPE32.DLL" (
    ByVal PrintJob As Integer,
  ) As Integer

手順9 PECloseEngine

 PEOpenPrintJobとPEClosePrintJobが対になっているのと同じように、PEOpenEngineと対になるのがPECloseEngineだ。このAPIを読んで、はじめて一連の処理が終了したことになる。

Declare Sub PECloseEngine
  Lib "CRPE32.DLL" ()

最後に

 Visual BasicからAPIを使うのは、諸刃の剣だ。それは、Visual Basicの言語仕様やCOMコンポーネントなどが覆い隠しているWindowsの製品やバージョン、Service Packによる違いに直面する可能性があるからだ。しかし、同時にその違いを吸収するために使った製品や部品の障害を回避する手段として有効だとも言える。要は、
適材適所(Visual Basicでできない
Windows標準仕様を実現する)

という大原則を忘れてはいけないということだろう。

■サンプルプログラムの確認環境1
■サンプルプログラムの確認環境2

リスト5:サンプル(CRAPI.exe)

表8:OCXと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