Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2018年05月31日] [更新 2018年06月17日] Delphi 一般・その他
   
PChar 型の文字列配列の操作
動作確認等 Windows 7 U64(SP1) + Delphi XE5(UP2) Pro VCL-32
[整理番号 - 0770]







PChar 型の文字列配列の操作


Null 終端の文字列である PChar 型の文字列配列の操作に関するサンプルです .

本ページのサンプルでは下図のようなフォームを作成して,このフォームを使用するプロジェクトをコピーしてテストしています.
ジェネリックスの TArray を使用したサンプルでは,uses に Generics.Collections が必要です.



[備考]
Windows API の関数の中には,1 つの引数で複数の文字列を扱う際,Null 終端文字列を複数結合した文字列が使われることがあります.



図1
本ページのテストで使用しているフォーム




01_PPCharArray を使用した PChar 型の配列の操作


Delphi には PChar 型の配列用の,以下の型が定義されています.


リスト1
  TPCharArray = packed array[0..(MaxLongint div SizeOf(PChar))-1] of PChar;
  PPCharArray = ^TPCharArray;

本サンプルはこのポインタ型の PPCharArray を使用して PChar 型の配列を操作する例です.
まず,配列のデータの格納用のメモリ領域を AllocMem 関数で確保します.実際の施用する文字数分だけのメモリを確保する場合は,文字数が確定してからメモリを確保します.



図2
PChar 型の文字列配列の操作結果

リスト2
//=============================================================================
//  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 を使う慣習となっています.
同じ方法で静的配列を使用することもできます.



リスト3
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 型の文字列配列の操作結果

リスト4
//=============================================================================
//  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 でキャストして文字列を代入した結果

リスト5
//-----------------------------------------------------------------------------
//  テスト用の関数
//  呼び出し側に値をセットして返す
//  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 内の関数では,操作に必要なメモリを呼び出し側で確保したり,確保したメモリの解放も呼出し側で行わなければならないこともあります.
次項のサンプルは呼出し側で配列のメモリを確保する例になっています.



リスト6
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 型の文字列配列のデータを取得した結果

リスト7
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 型の文字列配列のデータを受け取る関数ということになります.



リスト8
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.


上の DLL 内の関数の使用例です.



図6
DLL 内の関数の引数に,PChar 型の文字列配列のデータを代入した結果
  • DLL 内で保存したテキストファイルを読み込んで表示している
図7
DLL 内の関数の引数から,PChar 型の文字列配列のデータを取得した結果

リスト9
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) の時の文字列のバイト数を取得例です.
このバイト数は前項のサンプルと同じです.


リスト10
    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 型の変数です.


リスト11
    LEncoding := TEncoding.GetEncoding(932);
    try
      ABytesSize := LEncoding.GetByteCount(LStrList.Text);
    finally
      FreeAndNil(LEncoding);
    end;


図9
文字のコードページが 932 (Shift-JIS) の時の文字列のバイト数