KEN's .NET |
[特集6] GetPrivateProfileStringにみるAPIの使用方法 |
1.はじめにVB6からVB.NETになってさまざまな仕様変更、機能追加が行われました。その1つに言語の枠を越えて.NET Framework共通のクラスライブラリを持つようになったことがあります。 VB6でWindows API(以降、API)を呼び出して実現していたことの多くは、このクラスライブラリに同等の機能があります。 例えば、フォームを常に手前に表示するにはTopMostプロパティを、丸いフォームやボタンを作るにはRegionプロパティを、透明化するにはOpacityプロパティを使う、といったふうに。 しかし、それでも本格的なアプリケーションを制作する場合、クラスライブラリにない機能を実現するためにAPIの呼び出しが必要になる場面があります。 iniファイルから初期設定を読み込むには、VB6ではAPIのGetPrivateProfileString関数を利用していたと思います。 .NET Frameworkにiniファイルを扱うクラスはないため、引き続きこの関数を使うことになります。 (初期設定ファイルをはじめ、さまざまなデータの扱いは今後XMLファイルの使用が中心になると予想されますが、本題からはずれるためここでは言及しません) 今回はこの関数を題材にVB6でのAPIの使用方法とVB.NETでのAPIの使用方法の違いを見ていきます。 2.引数、戻り値の型を適切な型に変更する
Public Declare Function GetPrivateProfileString Lib "kernel32" _
Alias "GetPrivateProfileStringA" ( _
ByVal lpApplicationName As String, _
ByVal lpKeyName As Any, _
ByVal lpDefault As String, _
ByVal lpReturnedString As String, _
ByVal nSize As Long, _
ByVal lpFileName As String) As Long
図1 VB6までのGetPrivateProfileString関数の宣言
図1はVB6のGetPrivateProfileString関数宣言です。この定義で実験コードを書いてコンパイルすると図2のエラーになります。
error BC30828: 'As Any' は、'Declare' ステートメントではサポートされていません。
ByVal lpKeyName As Any, _
~~~
図2 VB6の宣言を含む実験コードのコンパイル結果
.NET Framework SDKドキュメントの「'As Any' は、'Declare' ステートメントではサポートされていません。」を参照すると以下の説明があります。 任意のデータ型を格納できる引数を使用するために、以前のバージョンの Visual Basic の Declare ステートメントで Any データ型が使用されました。 ただし、Visual Basic .NET ではオーバーロードをサポートしているため、Any データ型は旧式になります。 この意味を理解するにはそもそも"As Any"とは何かを知る必要があります。 VB6のドキュメントのDLL プロシージャの宣言の「複数のデータ型を受け取るプロシージャの宣言」には下記のようにあります。 DLL プロシージャには、1 つの引数で複数のデータ型を受け取ることができるものがあります。 複数のデータ型で引数を渡す必要がある場合は、その引数を As Any として宣言して型の制限をなくします。 つまり、あるDLLのプロシージャのある引数が、たとえば文字列か整数のどちらかを要求する場合があり、このような場合にAnyが使われていました。 Anyが指定れた引数に渡す値は型チェックが行われないため、どんな値を渡してもコンパイル時にエラーになりません。 その結果、DLLのプロシージャを呼び出す側(VBアプリ)で正しく値を渡さないと実行時に容赦なく落ちます。 それに対して、VB.NETでは言語機能としてオーバーロードがあるため、文字列を要求する版(その引数をAs Stringで宣言)と整数を要求する版(その引数をAs Integerで宣言)の宣言をそれぞれ用意することで実現できます。 このようにAny型は必要なくなったため廃止されました。 というわけで、Anyは別の適切な型に置き換える必要があります。 どの型に置き換えるべきかを知るには、GetPrivateProfileString関数の本来の定義(C言語での定義)と正しい使い方を調べる必要があります。 そこでPlatform SDKのGetPrivateProfileStringの項を参照します。 DWORD GetPrivateProfileString( LPCTSTR lpAppName, // セクション名 LPCTSTR lpKeyName, // キー名 LPCTSTR lpDefault, // 既定の文字列 LPTSTR lpReturnedString, // 情報が格納されるバッファ DWORD nSize, // 情報バッファのサイズ LPCTSTR lpFileName // .ini ファイルの名前 ); 図2 C言語でのGetPrivateProfileString関数の定義
図2を見ると、戻り値とnSizeはDWORD型、それ以外のlpAppName、lpKeyName、lpDefault、lpReturnedString、lpFileNameはLPCTSTR型(LPTSTR型)とあります。 元の定義よりlpKeyNameの型はlpAppNameなどと同じ型のため"As Any"は"As String"と変更すればよさそうだとわかります。 C言語の定義の各型と.NETでの型との対応は、プラットフォーム呼び出しのデータ型に載っているので、こちらも見てみましょう。DWORD型、LPCTSTR、LPTSTR型の抜粋を下記に示します。
図3 API、C言語、.NET間での型の対応(抜粋)
図3を見ると、DWORD型が.NETでどの型になるかは一目瞭然ですが、そもそもDWORD型とはどんな型かを理解するためにC言語の整数型の概要を紹介します。 C言語の整数型にはchar、short(short intの省略)、int、long(long intの省略)という型があります。 さらに、これらの型の前にsigned、unsignedをつけて符号あり、符号なしを表します。 (16ビットの整数なら符号ありは-32,768 〜 32,767、符号なしは0 〜 65,535(= 2^16 - 1)という範囲の数値を扱えます) signedは省略可能なため、単にintと言う場合はsigned intを意味します。 C言語の標準規格上は、char <= short <= int <= long という型の大小関係とcharのサイズ、intのとりうる最低限の数値範囲くらいしか決まっていなかったと記憶しています。 Win32上では型のサイズは、charは1バイト、shortは16ビット、int、longは32ビットです。 WindowsのC言語のライブラリでDWORDはunsigned longの別名として定義されているためDWORDは32ビット符号なし整数で、.NET FrameworkではSystem.UInt32構造体がこれに該当します。 よって、この関数で旧VBでLong型を使用していたところは、System.UInt32を使用するのがより正確です。 旧VBには符号なし32ビット整数型がなかったため、符号あり32ビット整数型であるLong型で代用していたというわけです。 余談ですが、VB.NETのLong型(System.Long)は64ビット、32ビットはInteger型(System.Int32)、16ビットはShort型(System.Short)と変更になっている点に注意が必要です。 3.APIで要求される文字列LPCTSTR/LPTSTR型はWindowsのC言語のライブラリで定義されており、C言語における文字列表現の一つです。 単純にString型が対応しそうですがそう簡単にいきません。それぞれの型を細かく見ていきましょう。 LPTSTRは環境(OS)によってLPSTR、または、LPWSTRと読み替えられる型です。 LPCTSTRのCはconstのCで、関数側では「その値を変更しない」という宣言方法で、変更されることがないという点を除けばLPTSTRと同じです。 元々、C言語には文字列を扱う型がありません。そのため、文字列はchar型の配列として扱われます。 文字列のメモリ上のイメージを図4に示します。 ![]() 図4 C言語での文字列の扱い(LPSTR)
図4では、上部の16進数はメモリ空間上のアドレス、1つの四角が1バイトのメモリを意味します。
1バイトのメモリはchar型で表現できます。
コンピュータはある特定のメモリをアドレスによって識別します。
人間はある特定のメモリを変数名で識別します。
配列はメモリのグループを表現できるので、これにより文字列を表現できます。 図4を見ると"T"は1つの箱を使いますが、"日"は2つの箱を使うことがわかります。 文字コードがShift JISの場合、半角は1バイト、全角は2バイトだからです。 日本語には2バイト文字がありますが、英語にはこの2バイト表現は存在しないため、単純に1バイトが1文字を表します。 このように(Shift JISのようなものも含めて)1文字1バイトを基本としたものはANSI文字列と言いいます。 LPSTR型はこのchar*型の別名です。 これに対して半角、全角共に2バイトの文字列(wide char)で表す場合はUNICODEという文字コードで表すためUNICODE文字列と言います。 UNICODE文字列のメモリ上のイメージを図5に示します。 ![]() 図5 UNICODE文字列(LPWSTR)
UNICODE文字列表現は、wchar_t型の配列で表されます。 wchar_t型はWindowsのC言語ライブラリでunsigned short型の別名として定義されており、wchar_t型1つで2バイトを表現できます。 LPWSTRはこのwchar_t型のポインタ(wchar_t*型)の別名です。 変数sの値が「01」なら"THIS IS 日本語."を意味し、変数sの値が「11」なら"日本語."を意味します。 4.ANSI文字列版とUNICODE文字列版実はAPIにGetPrivateProfileStringという名前の関数はありません。 一般にGetPrivateProfileStringと呼ばれる関数は、GetPrivateProfileStringA関数、または、GetPrivateProfileStringW関数の総称です。 図1に示したVB6の宣言は、Alias句を使って、GetPrivateProfileStringA関数をGetPrivateProfileString関数として定義しています。 つまり、この宣言ではGetPrivateProfileStringA関数が呼ばれます。 GetPrivateProfileString関数に限らず、A付きとW付きの異なるAPIの違いは「OSによってこの関数を利用できるかどうか」と「文字列をANSI文字列、UNICODE文字列のどちらで扱うか」です。 GetPrivateProfileStringA関数はWin98、WinMe、WinNT4.0、Win2000、WinXPのどの環境でも利用できます。 GetPrivateProfileStringW関数はWinNT4.0、Win2000、WinXPといったNT系のOSで利用できます。 OSに関わらず同じAPIを利用したい場合、必然的にGetPrivateProfileStringA関数を利用することになりますが、 NT系OS(NT、2000、XP)ではGetPrivateProfileStringW関数を利用し、9x系OS(98、Me)ではGetPrivateProfileStringA関数を利用するというふうに使い分けることもできます。 GetPrivateProfileStringAとGetPrivateProfileStringWのどちらを使用するかは、文字列の扱いに影響を与えます。 前者ではANSI文字列を使い、後者はUNICODE文字列を使います。引数のlpAppName、lpKeyName・・・はLPCTSTR型なので、 GetPrivateProfileStringAではLPCSTRと読み替えられ、GetPrivateProfileStringWではLPCWSTRと読み替えられます。このことがlpAppName、lpKeyName・・・などの引数の型を単純に.NETのString型に置き換えるわけにはいかない理由です。 .NETのStringとLPWSTR型、LPSTR型では文字列の扱いが違うため、このままでは問題があります。 この型の違いを吸収するためにMarshalAs属性というものがあります。 5.MarshalAs属性とUnmanagedType 列挙体
<MarshalAs(UnmanagedType.LPStr)> ByVal lpApplicationName As String
図6 MarshalAs属性の適用
MarshalAs属性(実体はSystem.Runtime.InteropServices.MarshalAsAttributeクラス)は図6のように引数の前に付加します。 MarshalAs属性の引数としてUnmanagedType列挙体を指定します。 .NETのString型をLPSTR型にマーシャリング(プロセスを越えた型の調整)するにはUnmanagedType.LPStrを指定します。 LPWSTR型にマーシャリングするにはUnmanagedType.LPWStrを指定します。 また、OSに応じてLPSTR型、LPWSTR型にマーシャリングするにはUnmanagedType.LPTStrを指定します。 このようにMarshalAs属性を使用することでAPIが要求する種類の文字列型として.NETの文字列を変換して渡すことができます。 6.戻り値としての文字列さて、ここまでの解説でAPIの宣言は完成しそうですがもう一点だけ残っています。 図2に示したC言語のGetPrivateProfileString宣言で、引数lpReturnedStringだけがLPTSTR型です。他はLPCTSTR型でした。 GetPrivateProfileString関数は指定のiniファイルから、指定のセクション、指定のキーに該当する値を取得しますが、その取得した結果が引数lpReturnedStringに戻されます。 lpReturnedStringの文字列の値は関数が変更するので、LPCTSTR型ではなく"C"抜きのLPTSTR型になっています。 lpReturnedStringは変更できる型の必要がありますが、.NETのString型は値を変更することができないためここでは使用できません。 s = "あいう"とあった場合、s = "かきく"と変更できると思われるかもしれませんが、メモリ上で"あいう"が設定されたアドレスと同アドレスに"かきく"という値が設定されるわけではなく、新たにメモリが用意されて"かきく"を設定したのち、その先頭アドレスを変数sに設定しているに過ぎません。 そうではなく、"あいう"とあるメモリ上に上書きされる形で"かきく"のように設定できなければなりません。 図3の型の対応表を見ると文字列を表す型として、System.Stringクラス以外にString.Text.StringBuilderクラスがあることに気づきます。 こちらのStringBuilderクラスが値を変更できる文字列型として使用できます。 7.VB.NETでのGetPrivateProfileString関数の宣言' 名前空間のインポート Imports System Imports System.Text Imports System.Runtime.InteropServices Public Class IniApi ' ANSI版 GetPrivateProfileString Public Declare Function GetPrivateProfileString Lib "kernel32" _ Alias "GetPrivateProfileStringA" ( _ <MarshalAs(UnmanagedType.LPStr)> ByVal lpApplicationName As String, _ <MarshalAs(UnmanagedType.LPStr)> ByVal lpKeyName As String, _ <MarshalAs(UnmanagedType.LPStr)> ByVal lpDefault As String, _ <MarshalAs(UnmanagedType.LPStr)> ByVal lpReturnedString As StringBuilder, _ ByVal nSize As UInt32, _ <MarshalAs(UnmanagedType.LPStr)> ByVal lpFileName As String) As UInt32 End Class 図7 ANSI版 GetPrivateProfileString関数の宣言
これまでの説明から、ANSI版の宣言は図7のようになります。 また、NT系のOSではUNICDOE版を使い、9x系のOSではANSI版を使う場合は下記の図8のようになります。 ' 名前空間のインポート Imports System Imports System.Text Imports System.Runtime.InteropServices Public Class IniApi ' AUTO版 GetPrivateProfileString Public Declare Auto Function GetPrivateProfileString Lib "kernel32" _ Alias "GetPrivateProfileString" ( _ <MarshalAs(UnmanagedType.LPTStr)> ByVal lpApplicationName As String, _ <MarshalAs(UnmanagedType.LPTStr)> ByVal lpKeyName As String, _ <MarshalAs(UnmanagedType.LPTStr)> ByVal lpDefault As String, _ <MarshalAs(UnmanagedType.LPTStr)> ByVal lpReturnedString As StringBuilder, _ ByVal nSize As UInt32, _ <MarshalAs(UnmanagedType.LPTStr)> ByVal lpFileName As String) As UInt32 End Class 図8 AUTO版 GetPrivateProfileString関数の宣言
図8で図7と違うところは以下の通りです(図8の赤文字で示した箇所)。
最後に、関数の呼び出し側の例を示しておきます。 ' APIを呼び出す箇所 Dim sb As StringBuilder = New StringBuilder(256) Dim ret As UInt32= _ GetPrivateProfileString( _ "セクション", _ "キー", _ "デフォルト値", _ sb, _ Convert.ToUInt32(sb.Capacity), _ "INIファイルのパス") MessageBox.Show("""" & sb.ToString() & """") 図9 GetPrivateProfileString関数の使用例
8.まとめGetPrivateProfileString関数を通してAPIの使用方法を見てきました。
|