Delphi Programming / Object Pascal
[掲載 2018年05月31日] [更新 2018年06月17日] Delphi 一般・その他
PChar 型の文字列配列の操作
動作確認等
Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
Null 終端の文字列である PChar 型の文字列配列の操作に関するサンプルです
.
本ページのサンプルでは下図のようなフォームを作成して,このフォームを使用するプロジェクトをコピーしてテストしています.
ジェネリックスの TArray を使用したサンプルでは,uses に Generics.Collections が必要です.
[備考]
Windows API の関数の中には,1 つの引数で複数の文字列を扱う際,Null 終端文字列を複数結合した文字列が使われることがあります.
図1
本ページのテストで使用しているフォーム
01_ PPCharArray を使用した PChar
型の配列の操作
Delphi には
PChar 型の配列用の,以下の型が定義されています.
copy code
TPCharArray = packed array [0 ..(MaxLongint div SizeOf(PChar))-1 ] of PChar;
PPCharArray = ^TPCharArray;
本サンプルはこのポインタ型の PPCharArray を使用して PChar 型の配列を操作する例です.
まず,配列のデータの格納用のメモリ領域を AllocMem 関数で確保します.実際の施用する文字数分だけのメモリを確保する場合は,文字数が確定してからメモリを確保します.
図2
PChar 型の文字列配列の操作結果
copy code
// =============================================================================
// Pchar型の配列
// System.pasで定義されているPChar型の配列の型を使用する場合
// =============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
LLen : Integer;
LPPCharArray : PPCharArray;
LPChar : PChar;
LIndex : Integer;
begin
// 配列のポインタのメモリを要素分確保
LLen := 4 ;
LPPCharArray := AllocMem(LLen * SizeOf(PChar));
try
// 配列に文字列をセット
LPPCharArray^[0 ] := '富山県' ;
LPPCharArray^[1 ] := '長野県' ;
LPPCharArray^[2 ] := '岐阜県' ;
LPPCharArray^[3 ] := '山梨県' ;
// 格納した配列の文字列を順番に表示
for LIndex := 0 to LLen - 1 do begin
LPChar := LPPCharArray^[LIndex];
Memo1.Lines.Add(LPChar);
end ;
finally
FreeMem(LPPCharArray);
end ;
end ;
以下は動的な配列を使用する例ですが,Delphi の VCL の定義と重複しないように型名を少し変更しています.型を定義する時は,この例のようにポインタ型も定義しておくと便利です.Delphi ではポインタ型のプリフィックス (先頭に付ける文字) として大文字の P を使う慣習となっています.
同じ方法で静的配列を使用することもできます.
copy code
type
TPCharDynArray = array of PChar;
PPCharDynArray = ^TPCharDynArray;
// =============================================================================
// PChar型の配列
// Delphiに実装されている定義ではなく,動的配列を定義して使用する例
// =============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
LLen : Integer;
LPCharArray : TPCharDynArray;
LIndex : Integer;
begin
// 配列の要素数を設定
LLen := 4 ;
SetLength(LPCharArray, LLen);
// 配列に文字列をセット
LPCharArray[0 ] := '富山県' ;
LPCharArray[1 ] := '長野県' ;
LPCharArray[2 ] := '岐阜県' ;
LPCharArray[3 ] := '山梨県' ;
// 格納した配列の文字列を順番に表示
for LIndex := 0 to Length(LPCharArray) - 1 do begin
Memo1.Lines.Add(LPCharArray[LIndex]);
end ;
end ;
02_ TArray <T> を使用した PChar 型の配列の操作
ジェネリックスの TArray 配列を使用して PChar 型の文字列を操作する例です.
TArray<PChar> の配列は動的な配列となります.
図3
ジェネリックスの TArray を使用した PChar 型の文字列配列の操作結果
copy code
// =============================================================================
// PChar型の配列
// ジェネリックスのTArrayを使用したPChar型の動的配列
//
// uses に Generics.Collections が必要
// =============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
LLen : Integer;
LArray : TArray<PChar>;
LIndex : Integer;
begin
// 配列の要素数を設定
LLen := 4 ;
SetLength(LArray, LLen);
// 配列に文字列をセット
LArray[0 ] := '富山県' ;
LArray[1 ] := '長野県' ;
LArray[2 ] := '岐阜県' ;
LArray[3 ] := '山梨県' ;
// 格納した配列の文字列を順番に表示
for LIndex := 0 to Length(LArray) - 1 do begin
Memo1.Lines.Add(LArray[LIndex]);
end ;
end ;
03_ String 型の文字列を PChar 型の配列要素に代入
ここまでのサンプルでは,配列の基本的な操作の説明用として,シングルクォート (シングルクォート) で括った文字列だけを PChar 型の配列の各要素に代入していました.本項ではより実用的なサンプルコードとして,String 型の文字列を PChar 型の配列の各要素に代入する例としています.
次のコードは
String 型の文字列を PChar でキャストして 配列の各要素に代入していますが,文字化けしていて元の文字列が判別できません.
図4
PChar 型の配列要素に PChar でキャストして文字列を代入した結果
copy code
// -----------------------------------------------------------------------------
// テスト用の関数
// 呼び出し側に値をセットして返す
// System.pasで定義されているPChar型の配列の型を使用する場合
// -----------------------------------------------------------------------------
function GetPCharArray(var ACount: Integer): PPChar;
var
LIndex : Integer;
LStrList : TStringList;
LText : string ;
begin
Result := AllocMem(MaxLongint div SizeOf(PChar));
ACount := 4 ;
LStrList := TStringList.Create;
try
LStrList.Add('富山県' );
LStrList.Add('長野県' );
LStrList.Add('岐阜県' );
LStrList.Add('山梨県' );
for LIndex := 0 to ACount - 1 do begin
LText := FormatFloat('000' , LIndex + 1 ) + ' : ' + LStrList[LIndex];
LStrList[LIndex] := LText;
end ;
for LIndex := 0 to ACount - 1 do begin
PPCharArray(Result)^[LIndex] := PChar(LStrList[LIndex]);
end ;
finally
FreeAndNil(LStrList);
end ;
end ;
// =============================================================================
// Pchar型の配列の操作例
// 関数の戻り値からPChar型の配列の値を取得する例
// System.pasで定義されているPChar型の配列の型を使用する場合
// =============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
LPPCharArray : PPCharArray;
LLen : Integer;
LPChar : PChar;
LIndex : Integer;
begin
Memo1.Lines.Clear;
PPChar(LPPCharArray) := GetPCharArray(LLen);
try
if LLen > 0 then begin
for LIndex := 0 to LLen - 1 do begin
LPChar := LPPCharArray^[LIndex];
Memo1.Lines.Add(LPChar);
end ;
end ;
finally
FreeMem(LPPCharArray);
end ;
end ;
04_ 戻り値が PChar 型の文字列配列である DLL 内関数の例
前項のサンプルのような文字化け現象を回避するには,String 型の文字列を代入する際,PChar 型の配列の各要素のメモリ領域を割り当ててから代入します.
Null 終端の文字列である PChar 型の文字列を使用する場面は,Windows API 等,DLL 内の関数を使用する場合です.そこで,本サンプルでは戻り値が PChar 型の文字列配列関数を実装した DLL を作成してテストしています.
PChar 型の文字列配列の各要素には StrNew 関数でその文字列の領域を確保して,StrDispose 関数で解放しています.
自分で関数を作成する場合は,仕様は自由に設計できます.Windows API 等,他から提供されている DLL 内の関数では,操作に必要なメモリを呼び出し側で確保したり,確保したメモリの解放も呼出し側で行わなければならないこともあります.
次項のサンプルは呼出し側で配列のメモリを確保する例になっています.
copy code
library DllTest_PChar;
uses
Winapi.Windows,
System.SysUtils,
System.Classes;
var
SaveDllProc : procedure (Reason: Integer);
FPPCharArr : PPCharArray;
FPPCharCount : Integer;
// -----------------------------------------------------------------------------
// 確保したPCharの各配列要素分のメモリの解放処理
// -----------------------------------------------------------------------------
procedure PCharArrDispose;
var
LIndex: Integer;
begin
if FPPCharCount > 0 then begin
for LIndex := 0 to FPPCharCount - 1 do begin
StrDispose(FPPCharArr^[LIndex]);
end ;
FreeMem(FPPCharArr);
FPPCharCount := 0 ;
end ;
end ;
// -----------------------------------------------------------------------------
// DllProcの処理
// 本サンプルでは,DllEntryという名前に置き換えている
// -----------------------------------------------------------------------------
procedure DLLEntry(Reason: Integer);
begin
if Reason = DLL_PROCESS_ATTACH then begin
// DLLのプロセス生成時の処理
FPPCharCount := 0 ;
end else
if Reason = DLL_PROCESS_DETACH then begin
// DLLプロセス終了時の処理
PCharArrDispose;
end ;
// 元のDllProcを実行
if Assigned(SaveDllProc) then begin
if Reason >= 0 then begin
SaveDllProc(Reason);
end ;
end ;
end ;
// -----------------------------------------------------------------------------
// DLL内の文字列を呼び出し側に渡す関数
// 戻り値にPChar型の配列を返す例
//
// AItemCountは配列の要素数
//
// 戻り値の型はPPCharでもPointerでもよい
// -----------------------------------------------------------------------------
function GetPCharArray(var AItemCount: DWORD): Pointer; stdcall ;
var
LStrList : TStringList;
LText : string ;
LIndex : Integer;
begin
// 既存データの解放処理
PCharArrDispose;
// DLL内で生成する文字列の例
LStrList := TStringList.Create;
try
LStrList.Add('北穂高岳' );
LStrList.Add('奥穂高岳' );
LStrList.Add('前穂高岳' );
LStrList.Add('西穂高岳' );
LStrList.Add('涸沢岳' );
// 文字列を少し加工
for LIndex := 0 to LStrList.Count - 1 do begin
LText := IntToStr((LIndex + 1 ) * 100 ) + ' : ' + LStrList[LIndex];
LStrList[LIndex] := LText;
end ;
// 作成した要素数
FPPCharCount := LStrList.Count;
AItemCount := FPPCharCount;
// PChar型の配列用のメモリを確保
// それをこの関数の戻り値とする
FPPCharArr := AllocMem(Length(LStrList.Text) * SizeOf(Char));
Result := FPPCharArr;
// StrNewで確保した値をDLL内部のPCharの配列に代入
// それを戻り値の各要素に代入
for LIndex := 0 to FPPCharCount - 1 do begin
LText := LStrList[LIndex];
FPPCharArr^[LIndex] := StrNew(PChar(LText));
PPCharArray(Result)^[LIndex] := FPPCharArr^[LIndex];
end ;
finally
FreeAndNil(LStrList);
end ;
end ;
// -----------------------------------------------------------------------------
// exports宣言
// -----------------------------------------------------------------------------
exports
GetPCharArray;
// -----------------------------------------------------------------------------
// DLL初期化時の処理
// DllProcはDLLのプロセス,スレッドの開始.終了時に自動実行される(SysInit.pas)
// -----------------------------------------------------------------------------
begin
// 元のDllProcを退避
SaveDllProc := @DllProc;
DllProc := @DLLEntry;
// DLL初期化時に設定
DLLEntry(DLL_PROCESS_ATTACH);
end .
上の DLL 内の関数を使用して PChar 型の文字列配列のデータを,DLL 内から取得する例です.
文字列が文字化けしないで取得できることが確認できます.
図5
関数の戻り値として,PChar 型の文字列配列のデータを取得した結果
copy code
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class (TForm)
Button1: TButton;
Memo1: TMemo;
procedure Button1Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end ;
var
Form1: TForm1;
implementation
{$R *.dfm}
function GetPCharArray(var AItemCount: DWORD): Pointer;
stdcall ; external 'DllTest_PChar.dll' ;
// =============================================================================
// PChar型の配列の操作
// 戻り値がPChar型の配列であるDLL内の関数を実行
// =============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
LPPCharArr : PPCharArray;
LIndex : Integer;
LText : string ;
LCount : DWORD;
begin
Memo1.Lines.Clear;
// DLL内の関数を実行してPChar型の配列のデータを取得
PPChar(LPPCharArr) := GetPCharArray(LCount);
// 取得したデータを表示
for LIndex := 0 to LCount - 1 do begin
LText := LPPCharArr^[LIndex];
Memo1.Lines.Add(LText);
end ;
Memo1.Lines.Add('' );
Memo1.Lines.Add('要素の数 : ' + IntToStr(LCount));
end ;
end .
05_ 引数の型が PChar 型の文字列配列である DLL 内関数の例
DLL から PChar 型の文字列配列のデータを,関数の引数から取得する例です.
テスト用に作成した DLL には,その引数に PChar 型の文字列配列のデータを渡す関数も実装してみました.この関数は,DLL 側からみると,外部から PChar 型の文字列配列のデータを受け取る関数ということになります.
copy code
library DllTest_PChar;
uses
Winapi.Windows,
System.SysUtils,
System.Classes;
var
SaveDllProc: procedure (Reason: Integer);
FPPCharArr: PPCharArray;
FPPCharCount: Integer;
// -----------------------------------------------------------------------------
// 確保したPCharの各配列要素分のメモリの解放
// -----------------------------------------------------------------------------
procedure PCharArrDispose;
var
LIndex: Integer;
begin
if FPPCharCount > 0 then begin
for LIndex := 0 to FPPCharCount - 1 do begin
StrDispose(FPPCharArr^[LIndex]);
end ;
FreeMem(FPPCharArr);
FPPCharCount := 0 ;
end ;
end ;
// -----------------------------------------------------------------------------
// DllProcの処理
// 本サンプルでは,DllEntryという名前に置き換えている
// -----------------------------------------------------------------------------
procedure DLLEntry(Reason: Integer);
begin
if Reason = DLL_PROCESS_ATTACH then begin
// DLLのプロセス生成時の処理
FPPCharCount := 0 ;
end else
if Reason = DLL_PROCESS_DETACH then begin
// DLLプロセス終了時の処理
PCharArrDispose;
end ;
// 元のDllProcを実行
if Assigned(SaveDllProc) then begin
if Reason >= 0 then begin
SaveDllProc(Reason);
end ;
end ;
end ;
// -----------------------------------------------------------------------------
// 呼び出し側でセットしたPChar型の文字列を引数から取得
//
// 引数ABuffの型はPPCharでもPointerでもよい
// -----------------------------------------------------------------------------
procedure SetPCharArray(ABuff: Pointer; ALen: Integer); stdcall ;
var
LStrList : TStringList;
LText : string ;
LIndex : Integer;
begin
// 既存データの解放処理
PCharArrDispose;
// 受け取ったデータの処理の例
// ファイルにリストとして保存
LStrList := TStringList.Create;
try
for LIndex := 0 to ALen - 1 do begin
LText := string (PPCharArray(ABuff)[LIndex]);
LStrList.Add(LText);
end ;
LStrList.SaveToFile('000.txt' );
finally
FreeAndNil(LStrList);
end ;
end ;
// -----------------------------------------------------------------------------
// DLL内の文字列を呼び出し側に渡す関数
// 引数をnilにすると必要なバッファの確保サイズの取得となる
//
// 引数のABytesSizeに実際に使用するバイト数を返す
// 戻り値は格納した要素数
//
// 引数ABuffの型はPPCharでもPointerでもよい
// -----------------------------------------------------------------------------
function GetPCharArray(ABuff: Pointer; var ABytesSize: DWORD): DWORD; stdcall ;
var
LStrList : TStringList;
LText : string ;
LIndex : Integer;
begin
// 既存データの解放処理
PCharArrDispose;
// DLL内で生成する文字列の例
LStrList := TStringList.Create;
try
LStrList.Add('北穂高岳' );
LStrList.Add('奥穂高岳' );
LStrList.Add('前穂高岳' );
LStrList.Add('西穂高岳' );
LStrList.Add('涸沢岳' );
// 文字列を少し加工
for LIndex := 0 to LStrList.Count - 1 do begin
LText := IntToStr((LIndex + 1 ) * 100 ) + ' : ' + LStrList[LIndex];
LStrList[LIndex] := LText;
end ;
FPPCharCount := LStrList.Count;
ABytesSize := Length(LStrList.Text) * SizeOf(Char);
Result := FPPCharCount;
if ABuff = nil then Exit;
// StrNewで確保した値をDLL内部のPChar型の配列に代入
// それを呼び出し側のPChar型の配列の各要素に代入
FPPCharArr := AllocMem(ABytesSize);
for LIndex := 0 to FPPCharCount - 1 do begin
LText := LStrList[LIndex];
FPPCharArr^[LIndex] := StrNew(PChar(LText));
PPCharArray(ABuff)^[LIndex] := FPPCharArr^[LIndex];
end ;
finally
FreeAndNil(LStrList);
end ;
end ;
// -----------------------------------------------------------------------------
// exports宣言
// -----------------------------------------------------------------------------
exports
SetPCharArray,
GetPCharArray;
// -----------------------------------------------------------------------------
// DLL初期化時の処理
// DllProcはDLLのプロセス,スレッドの開始.終了時に自動実行される(SysInit.pas)
// -----------------------------------------------------------------------------
begin
// 元のDllProcを退避
SaveDllProc := @DllProc;
DllProc := @DLLEntry;
// DLL初期化時に設定
DLLEntry(DLL_PROCESS_ATTACH);
end .
図6
DLL 内の関数の引数に,PChar 型の文字列配列のデータを代入した結果
DLL 内で保存したテキストファイルを読み込んで表示している
図7
DLL 内の関数の引数から,PChar 型の文字列配列のデータを取得した結果
copy code
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes,
Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;
type
TForm1 = class (TForm)
Button1: TButton;
Memo1: TMemo;
Button2: TButton;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private 宣言 }
public
{ Public 宣言 }
end ;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure SetPCharArray(ABuf: Pointer; ALen: Integer);
stdcall ; external 'DllTest_PChar.dll' ;
function GetPCharArray(ABuf: Pointer; var ABytesSize: DWORD): DWORD;
stdcall ; external 'DllTest_PChar.dll' ;
// =================================================================
// DLL内の関数にPChar型の配列データを渡す例
// =================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
LPPCharArr : PPCharArray;
LStrList : TStringList;
LText : string ;
LIndex : Integer;
begin
Memo1.Lines.Clear;
LStrList := TStringList.Create;
try
// テスト用の文字列を作成
LStrList.Add('富山県' );
LStrList.Add('長野県' );
LStrList.Add('岐阜県' );
LStrList.Add('山梨県' );
// テスト用の文字列を加工
for LIndex := 0 to LStrList.Count - 1 do begin
LStrList[LIndex] := FormatFloat('000' , LIndex + 1 ) + ' : ' + LStrList[LIndex];
end ;
// PChar型の文字列配列のメモリを確保
LPPCharArr := AllocMem(Length(LStrList.Text) * SizeOf(Char));
try
// PChar型の配列の各要素のメモリを確保(PChar型のデータとして認識させる)
for LIndex := 0 to LStrList.Count - 1 do begin
LText := LStrList[LIndex];
LPPCharArr^[LIndex] := PChar(LText);
end ;
// DLL内の関数を実行してPChar型の文字列配列を渡す
SetPCharArray(PPChar(LPPCharArr), LStrList.Count);
finally
FreeMem(LPPCharArr);
end ;
finally
FreeAndNil(LStrList);
end ;
// DLL内で保存したテキストファイルを読み込んで表示
// 関数の実行結果の確認用
Memo1.Lines.LoadFromFile('000.txt' );
end ;
// =================================================================
// DLL内の関数からPChar型の配列データを取得する例
// =================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
LPPCharArr : PPCharArray;
LCount : Integer;
LBytesCnt : DWORD;
LText : string ;
LIndex : Integer;
begin
// PChar型の文字列配列のメモリを確保
LPPCharArr := AllocMem(Length(LPPCharArr^));
try
// DLL内の関数を実行してPChar型の配列データを取得してメモリサイズを再設定
// メモリの再設定は必ずしも必要ないがReallocMemoryの動作確認用
LCount := GetPCharArray(PPChar(LPPCharArr), LBytesCnt);
LPPCharArr := ReallocMemory(LPPCharArr, LBytesCnt);
// 取得したデータを表示
for LIndex := 0 to LCount - 1 do begin
LText := LPPCharArr^[LIndex];
Memo1.Lines.Add(LText);
end ;
Memo1.Lines.Add('' );
Memo1.Lines.Add('要素の数 : ' + IntToStr(LCount));
Memo1.Lines.Add('バイト数 : ' + IntToStr(LBytesCnt));
finally
FreeMem(LPPCharArr);
end ;
end ;
end .
06_ 文字列のバイト数を TEncoding クラスを使用して取得
上のサンプルで,文字列のバイト単位でメモリ領域を確保しています.この時に必要な文字列のバイト数は TEncoding クラスの GetByteCount メソッドでも取得することができます.
以下のコードは文字のコードページが UTF-16 (Unidoce) の時の文字列のバイト数を取得例です.
このバイト数は前項のサンプルと同じです.
copy code
ABytesSize := TEncoding.Unicode.GetByteCount(LStrList.Text);
図8
文字のコードページが UTF-16 (Unicode) の時の文字列のバイト数
文字のコードページが 932,つまり Shift-JIS の時は次のように取得できます.文字が使用するバイト数はコードページによって異なります.
Windows Vista で Windows は全面的にユニコード化されましたが,その内部表現は Shift-JIS です.したがって,前項のサンプルでは,実際にバイトデータとして格納されたバイト数はこの値になります.Delphi は文字列を Unicode (UTF-16) として処理します.文字列を処理する時は UTF-16 としてのバイト数を確保しておく方が無難です.
LEncoding
は TEncoding
型の変数です.
copy code
LEncoding := TEncoding.GetEncoding(932 );
try
ABytesSize := LEncoding.GetByteCount(LStrList.Text);
finally
FreeAndNil(LEncoding);
end ;
図9
文字のコードページが 932 (Shift-JIS) の時の文字列のバイト数