Delphi Programming / Object Pascal
サイトのトップページへ リンクのページへ ロゴマーク
[掲載 2009年05月25日] [更新 2015年11月08日] Delphi サンプルプログラム集
   
779_ファイルやパス名の文字として使用可能かを検査
動作確認等 Windows 7 U64(SP1) + Delphi XE Pro
[02] [03] [05] は Delphi 2010 以降専用
779_CheckPathNameC.zip [3,255 KB] 2015年11月08日版 (EXE 同梱)



  • 2013年02月16日
  • 検査のアルゴリズムの間違いを修正
  • Windows 7 U64(SP1) + Delphi XE で動作確認
  • 2013年06月04日
  • TPath のメソッド類を使用したサンプルを追加
  • 2015年11月08日
  • MAX_PATH を超えるパス名を操作するサンプルを追加




01_ファイルやパス名の文字として使用可能かを検査


ファイル名やパス名には使用できない文字があります.それをチェックするテストです.
shlwapi.dll 内の PathGetCharType という関数を利用しています.
この関数を含め,パス名の取得や操作,検査関係の関数類が,MSDN の Shell Path Handling のページにあります.

ファイル名等には,この他,終端 (最後の文字) としては使用できないとか,システムで使用しているファイル名が使用できない等の制約もあります.現実的には,ファイル名を付けて保存や,名前の変更を実行した時のエラーで検出してしまった方がいいのかも知れません.
ユーザに,どの文字が使用できないかを知らせるのであれば利用価値があるかも知れませんが,実は,思った程簡単ではありません.以下を参考にしてください.


図1
設計時画面

図2
実行結果

  • 空白文字は,短いパス名では使用不可

リスト1
文字がファイルやパス名の文字として使用可能かを検査
unit CheckPathnameUnit;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    Memo1: TMemo;
    procedure Button1Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses ShLwApi;

{$R *.dfm}

//=============================================================================
//  簡単なテスト用文字列をセット
//=============================================================================
procedure TForm1.FormShow(Sender: TObject);
begin
  Memo1.ScrollBars := ssBoth;
  Memo1.Anchors    := [akLeft, akTop, akRight, akBottom];

  Edit1.Text := 'Delphi\ </東西';
end;

//=============================================================================
//  Edit1に入力した文字列がファイル名として使用可能かをチェックする
//  PathGetCharTypeの使用には,usesにShLwApiが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  StrText : String;
  iRet    : Cardinal;
  FmtStr  : String;
  i       : Integer;
begin
  StrText := Edit1.Text;

  Memo1.Clear;
  for i := 1 to Length(StrText) do begin
    iRet := PathGetCharType(StrText[i]);

    FmtStr := '';
    if IRet = GCT_INVALID then begin
      FmtStr := FmtStr + '使用不可';
    end else begin
      if (iRet and GCT_LFNCHAR) = GCT_LFNCHAR then begin
        FmtStr := FmtStr + '長いパス名で使用可 ';
      end;
      if (iRet and GCT_SHORTCHAR) = GCT_SHORTCHAR then begin
        FmtStr := FmtStr + '短いパス名で使用可 ';
      end;
      if (iRet and GCT_SEPARATOR) = GCT_SEPARATOR then begin
        FmtStr := FmtStr + '区切り文字で使用可 ';
      end;
      if (iRet and GCT_WILD) = GCT_WILD then begin
        FmtStr := FmtStr + 'ワイルドカード文字で使用可能 ';
      end;
    end;

    Memo1.Lines.Add(StrText[i] + '  ' + FmtStr);
  end;
end;

end.




02_ファイルやパス名の文字として使用可能かを検査  -  TPath 使用


Delphi 2010 で実装された TPaht は,パスに対する操作の実行に使用される静的メソッドで構成されたレコード型です.
この中の IsValidPathChar と IsValidFileNameChar を使用して,指定した文字がパス名の文字として,あるいは,ファイル名の文字して使用可能かを調べます.


図3
設計時画面

図4
パス名の文字として使用可能か

  • ここでは全て可能となっている
図5
ファイル名の文字として使用可能か

  • : や / は使用できない

リスト2
TPath のメソッドを利用して,ファイルやパス名の文字の使用可否を検査
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    Memo1: TMemo;
    Button2: TButton;
    procedure FormShow(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses IOUtils;

{$R *.dfm}

//=============================================================================
//  簡単なテスト用文字列をセット
//=============================================================================
procedure TForm1.FormShow(Sender: TObject);
begin
  Memo1.ScrollBars := ssVertical;
  Memo1.Anchors    := [akLeft, akTop, akRight, akBottom];

  Edit1.Text := 'G:\Delphi\ </東西';
end;

//=============================================================================
//  [バス名文字チェック]
//  Edit1に入力した文字列の各文字が,パス名の文字として使用可能かをチェック
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  StrText : String;
  i       : Integer;
begin
  StrText := Edit1.Text;

  Memo1.Clear;
  for i := 1 to Length(StrText) do begin
    if TPath.IsValidPathChar(Char(StrText[i])) then begin
      Memo1.Lines.Add(StrText[i] + '  使用可能');
    end else begin
      Memo1.Lines.Add(StrText[i] + '  使用不可');
    end;
  end;
end;

//=============================================================================
//  [ファイル名文字チェック]
//  Edit1に入力した文字列の各文字が,ファイル名として使用可能かをチェック
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  StrText : String;
  i       : Integer;
begin
  StrText := Edit1.Text;

  Memo1.Clear;
  for i := 1 to Length(StrText) do begin
    if TPath.IsValidFileNameChar(Char(StrText[i])) then begin
      Memo1.Lines.Add(StrText[i] + '  使用可能');
    end else begin
      Memo1.Lines.Add(StrText[i] + '  使用不可');
    end;
  end;
end;

end.




03_文字列がファイル名やパス名として使用可能な文字だけかを検査


Delphi 2010 で実装された TPaht は,パスに対する操作の実行に使用される静的メソッドで構成されたレコード型です.
この中の HasValidPathChars と HasValidFileNameChars を使用して,指定した文字例の中の文字がパス名として,あるいは,ファイル名として使用可能な文字列だけであるかを調べます.
このサンプルコードで使用可能の結果となっても,必ずしもその名前で,パス (ディレクトリ) やファイルが作成できるとき限りません.


図6
設計時画面

図7
実行結果例

  • パス名としては使用可能だが,ファイル名としては使用できない

リスト3
TPath のメソッドを利用して,文字列がファイル名やパス名として使用可能かを検査
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Edit1: TEdit;
    Memo1: TMemo;
    procedure FormShow(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses IOUtils;

{$R *.dfm}

//=============================================================================
//  簡単なテスト用文字列をセット
//=============================================================================
procedure TForm1.FormShow(Sender: TObject);
begin
  Edit1.Text := 'G:\Delphi\ </東西';
end;

//=============================================================================
//  文字列がパス名あるいはファイル名として使用可能かをチェック
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  StrText : String;
begin
  StrText := Edit1.Text;

  Memo1.Clear;
  //パス名として使用可能かをチェック
  if TPath.HasValidPathChars(StrText, True) then begin
    Memo1.Lines.Add('パス名      使用可能');
  end else begin
    Memo1.Lines.Add('パス名      使用不可');
  end;

  //ファイル名として使用可能かをチェック
  if TPath.HasValidFileNameChars(StrText, True) then begin
    Memo1.Lines.Add('ファイル名  使用可能');
  end else begin
    Memo1.Lines.Add('ファイル名  使用不可');
  end;
end;

end.


以下のように,実際にティレクトリやファイルを作成して確認してみるのがいいかも知れません.
ただし,ディレクトリとファイル名が同じ場合,これは,このプロジェクトの直下に作成する場合となりますが,同じ名前で,ディレクトリとファイルは作成できません.ご注意を.


  //ディレクトリの作成
  TDirectory.CreateDirectory(StrText);

  //ファイルの作成
  TFile.Create(StrText);




04_パス名の長さ制限


アプリケーションで認識可能なパス名の長さの最大値は,260 文字です.この値は,MAX_PATH という定数名で定義されています.この定数名で設定しておけば間違いないでしょう.
エクスプローラを直接操作すれば,この長さ制限を超えた深い階層のディレクトリを作成できますが,アプリケーションからは認識できなくなってしまいます.

MSDN オンラインの CreateFile 関数の日本語版に,≪MSDN ライブラリの「」(ファイル名の規則)を参照してください≫ と文章があります.この欠落部分を英語版をみると File Name Conventions であることが分かります.
これによると,ユニコード用の API 関数を使用し,パス名の文字列もユニコードを使用すれば,パスの前に文字列 \\?\ を付加すると 32,768 文字まで使用可能になるようです.UNC のプリフィックスは通常 \\ ですが,\\?\UNC\サーバ名\シェア名\ とすればいいようです.

パス名ではありませんが,データベースの項目名等は,たとえ長い名前が使用可能であっても,先頭の 32 文字程度までしか認識しない場合があります.





05_MAX_PATH の長さを超えるディレクトリの操作


前項で MAX_PATH を超えるパス名を操作する方法について記述しました.
それを実際に確認するテストです.

エクスプローラで,ディレクトリを階層的に作成していくと,ある程度の階層下で下図のようなダイアログが現れて,ディレクトリが作成できなくなります.これは,各々の階層のディレクトリ名の長さにもよるようです.



図8
MAX_PATH を超えるディレクトリ名は作成 できない

本サンプルでは長いパス名を作成するために,各々のディレクトリ名の長さを約 100 にしてみました.これだと作成できるようです.ディレクトリを作成したら,一番下の階層内にテキストファイルを作成して保存します.
この後のサンプルプログラムのテストのために,このディレクトリのルートフォルダを共有に設定しています.共有設定には net share コマンドを使用しています.
本サンプルのディレクトリの作成と削除のコードには,Delphi 2010 で実装された TDirectory を使用しています.



図9
設計時画面


実際に作成したディレクトリの構成は,下図のようになります.


  • 図10
  • 作成したディレクトリの構成

[備考 1]
ForceDirectories 関数を使用した場合,ディレクトリの作成に失敗しても,可能な限りまでディレクトリが作成されます.一方,TDirectory.CreateDirectory でディレクトリを作成する場合は,完全に作成できない場合は,全く作成されません.
ForceDirectories 関数でディレクトリの作成に失敗した場合,中途半端に作成されたディレクトリの扱いに注意が必要です.

[備考 2]
Delphi には拡張プレフィックスを付ける関数等はありませんが,拡張プレフィックスの有無と種類を返す関数はあります.ただし,Delph XE2 以降です.Delphi XE 以前でも使用可能なコードが Delphi Forum で提供されています.2 つのパス文字列を結合する Combine メソッドもあります.


リスト4
MAX_PATH を超える長さのディレクトリを作成して,テキストファイルを作成
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    procedure FormCreate(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
  private
    { Private 宣言 }
    FRootDir : String;
    FFullDir : String;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  IOUtils,
  StrUtils,
  ShellAPI;

{$R *.dfm}

//=============================================================================
//  フォーム生成時の処理
//
//  DupeStringの使用にはusesにStrUtilsが必要
//=============================================================================
procedure TForm1.FormCreate(Sender: TObject);
begin
  FRootDir := IncludeTrailingPathDelimiter('D:\100');
  //作成するディレクトリ名
  FFullDir := FRootDir
            + DupeString('LongPath0_', 10) + PathDelim
            + DupeString('LongPath1_', 10) + PathDelim
            + DupeString('LongPath2_', 10) + PathDelim
            + DupeString('LongPath3_', 10) + PathDelim
            + DupeString('LongPath4_', 10) + PathDelim;
end;

//=============================================================================
//  長いディレクトリ名のディレクトリを作成
//
//  ShellExecuteの使用にはusesにShellAPIが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LFolder  : String;
  LDir     : String;
  LCmdText : String;
begin
  //この後のサンプルの実行用に共有フォルダの設定
  LFolder  := ExcludeTrailingPathDelimiter(FFullDir);
  LCmdText := '/c net share 100=' + LFolder + ' /grant:everyone,full';
  ShellExecute(Handle, '', 'cmd.exe', PChar(LCmdText), nil, SW_HIDE);


  //拡張プレフィックスを追加
  LDir := '\\?\' + FFullDir;
  try
    TDirectory.CreateDirectory(LDir);
  except
    on E: Exception do begin
      MessageBox(Handle, PChar(E.Message), 'ディレクトリ作成', MB_ICONSTOP);
      exit;
    end;
  end;
end;

//=============================================================================
//  長いディレクトリ名の階層下にテキストファイルを作成
//=============================================================================
procedure TForm1.Button2Click(Sender: TObject);
var
  LDir        : String;
  LStringList : TStringList;
begin
  //拡張プレフィックスを追加
  LDir := '\\?\' + FFullDir;
  if not TDirectory.Exists(LDir) then begin
    MessageBox(Handle, 'ディレクトリが存在しません', '情報', MB_ICONSTOP);
    exit;
  end;

  LStringList := TStringList.Create;
  try
    LStringList.Add('非常に長いパス名の階層下にあるテキストファイルです.');
    LStringList.SaveToFile(IncludeTrailingPathDelimiter(LDir) + 'LongPathName.txt');
  finally
    FreeAndNil(LStringList);
  end;
end;

//=============================================================================
//  長いディレクトリ名のディレクトリを階層ごと削除
//=============================================================================
procedure TForm1.Button3Click(Sender: TObject);
var
  LDir : String;
begin
  //拡張プレフィックスを追加
  LDir := '\\?\' + FRootDir;

  //上の階層が削除されない場合があるのでループ
  //完全削除に時間がかかることがある
  while True do begin
    if not TDirectory.Exists(LDir) then Break;

    try
      TDirectory.Delete(LDir, True);
      Application.ProcessMessages;
      Sleep(1);
    except
      on E: Exception do begin
        MessageBox(Handle, PChar(E.Message), 'ディレクトリ削除', MB_ICONSTOP);
        exit;
      end;
    end;
  end;
end;

end.




06_MAX_PATH を超えるパス名のファイルを開く - ShellExecute 関数


前項のサンプルで作成したテキストファイルをメモ帳で表示します.
パス名に拡張プレフィックス \\?\ を追加してもテキストファイルは表示できませんでした.そこで,パス名を GetShortPathName 関数で短いパス名に変換してみました.下図がその実行結果です.ファイルが表示でき,編集した結果も保存できることを確認しました.


[備考]
短いファイル名ではなく,長いファイル名の途中を省略記号 ... で表示するには,MinimizeName 関数を使用します.


図11
MAX_PATH を超えるパス名のテキストファイルを表示


短いパス名にしない場合は,下図のようにファイルを見つけることができません.また,単に短いパス名にしただけでもファイルは開けませんでした.
今回は ShellExecute 関数を使用して,メモ帳でテキストファイルを表示していますが,表示に使用するアプリケーション,アプリケーションを起動する関数によっても動作が異なるようです.


図12
短いパス名にしなかった場合


リスト5
MAX_PATH を超えるパス名のテキストファイルを ShellExecute 関数で表示

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  ShellAPI,
  StrUtils;

{$R *.dfm}

//-----------------------------------------------------------------------------
//  長いパス名を短いパス名にする関数
//-----------------------------------------------------------------------------
function ToShortFilename(const Filename: String): String;
var
  LSize : Integer;
begin
  //必要なバッファサイズの取得
  LSize := GetShortPathName(PChar(Filename), nil, 0);
  if LSize = 0 then begin
    Result := Filename;
    exit;
  end;

  //短いファイル名に変換
  SetLength(Result, LSize);
  LSize := GetShortPathName(PChar(Filename), PChar(Result), LSize);
  SetLength(Result, LSize);
end;

//=============================================================================
//  長いフルパス名のテキストファイルをメモ帳で表示
//  ローカルディスク上のテキストファイルの場合
//
//  DupeStringの使用にはusesにStrUtilsが必要
//  ShellExecuteの使用にはusesにShellAPIが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LRootDir  : String;
  LFilePath : string;
begin
  LRootDir := IncludeTrailingPathDelimiter('D:\100');
  //開くテキストファイルのフルパス
  LFilePath := LRootDir
             + DupeString('LongPath0_', 10) + PathDelim
             + DupeString('LongPath1_', 10) + PathDelim
             + DupeString('LongPath2_', 10) + PathDelim
             + DupeString('LongPath3_', 10) + PathDelim
             + DupeString('LongPath4_', 10) + PathDelim
             + 'LongPathName.txt';

  //拡張プレフィックスを付け,更に短いファイル名にする
  LFilePath := ToShortFilename('\\?\' + LFilePath);

  //メモ帳でテキストファイルを表示
  ShellExecute(Handle, '', 'notepad.exe', PChar(LFilePath), nil, SW_SHOW);
end;

end.




07_MAX_PATH を超えるパス名のファイルを開く - ネットワーク


ネットワーク上の別のコンピュータの,MAX_PATH を超えるパス名にアクセスするには,以下のように拡張プレフィックスを付けます.アクセスする対象のファイルは,共有フォルダ (ディレクトリ) にあり,そのファイルが存在するコンピュータにログインしている必要があります.


    \\?\UNC\コンピュータ\共有名\
    \\?\UNC\サーバ\共有名\

今回は LAN 上のコンピュータで動作確認しています.
本サンプルも,ShellExecute 関数を使用して,メモ帳でテキストファイルを表示します.拡張プレフィックスにするだけでなく,GetShortPathName 関数で短いパス名に変換しています

本サンプルのコードは,筆者の環境のコンピュータ名を使用しています.したがって,添付の EXE を実行してもファイルは表示できません.



図13
LAN 上の PC で MAX_PATH を超えるテキストファイルを表示
  • テキストファイルがある PC にログインしていて,そのテキストファイルが共有フォルダに存在しなければならない

リスト6
MAX_PATH を超えるパス名のテキストファイルを ShellExecute 関数で表示
ネットワーク上 (LAN) のファイルの場合
unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls;

type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private 宣言 }
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

uses
  ShellAPI,
  StrUtils;

{$R *.dfm}

//-----------------------------------------------------------------------------
//  長いパス名を短いパス名にする関数
//-----------------------------------------------------------------------------
function ToShortFilename(const Filename: String): String;
var
  LSize : Integer;
begin
  //必要なバッファサイズの取得
  LSize := GetShortPathName(PChar(Filename), nil, 0);
  if LSize = 0 then begin
    Result := Filename;
    exit;
  end;

  //短いファイル名に変換
  SetLength(Result, LSize);
  LSize := GetShortPathName(PChar(Filename), PChar(Result), LSize);
  SetLength(Result, LSize);
end;

//=============================================================================
//  長いフルパス名のテキストファイルをメモ帳で表示
//  ネットワーク上のテキストファイルの場合
//  コンピュータ名がローカルコンピュータであって共有フォルダであれば開ける
//
//  DupeStringの使用にはusesにStrUtilsが必要
//  ShellExecuteの使用にはusesにShellAPIが必要
//=============================================================================
procedure TForm1.Button1Click(Sender: TObject);
var
  LFilePath : string;
begin
  //開くテキストファイルのフルパス
  LFilePath := DupeString('LongPath0_', 10) + PathDelim
             + DupeString('LongPath1_', 10) + PathDelim
             + DupeString('LongPath2_', 10) + PathDelim
             + DupeString('LongPath3_', 10) + PathDelim
             + DupeString('LongPath4_', 10) + PathDelim
             + 'LongPathName.txt';

  //拡張プレフィックスを付け,更に短いファイル名にする
  //100は共有フォルダ名(実フォルダ名とは限らない)
  LFilePath := ToShortFilename('\\?\UNC\MAKOTO-PC\100\' + LFilePath);

  //メモ帳でテキストファイルを表示
  ShellExecute(Handle, '', 'notepad.exe', PChar(LFilePath), nil, SW_SHOW);
end;

end.




08_パス操作関係関数とパスやドライブの区切り文字定数


Delphi には,下記のリンクの記事のように,SysUtils, IOUtils.TPath 等に,ファイル名,パス名等の操作関係の関数類があります.


また,SysUtils.pas 内で,パスやドライブ区切りの文字定数が次のように定義されています.PathSep は環境文字列のセパレータ等に使われている文字です.


const
  PathDelim  = {$IFDEF MSWINDOWS} '\'; {$ELSE} '/'; {$ENDIF}
  DriveDelim = {$IFDEF MSWINDOWS} ':'; {$ELSE} '';  {$ENDIF}
  PathSep    = {$IFDEF MSWINDOWS} ';'; {$ELSE} ':'; {$ENDIF}