TMemoryStream の使い方を調べるのを目的に、ここでは複数のファイルをまとめて一つのファイルにしたり、それを元にもどしたりすることを試してみよう。アプリケーションが必要とする沢山のファイルを一つにしてしまうと、配布に便利であったり、ユーザが勝手に置き換えたりすることを防止できる。また、リソースにするときも、簡単にすることができるだろう。
複数のファイルを一つにするための方法は、分割してもとに戻すときに便利なように、いろいろなフォーマットがおもいつくだろう。ここでは、格納した各ファイルの情報をファイルの先頭に書き込むことにしよう。そのためのレコード型を定義しよう。
type TFileInfoData = record Size:Cardinal; // ファイルのサイズ(バイト数) Offset:Cardinal; // 書き込んだ位置(先頭からのオフセットバイト) Name:array[0..30] of Char; // 元ファイルの名前 end; TCombineFilesHeader = record NumberOfFiles:integer; // ファイルの数 FileInfo:array[0..19] of TFileInfoData; // 各ファイルの情報 end;
このようなレコード型を先頭に書き込んでおくと、取り出すときは Offset や Size を参照して簡単になるだろう。また、もとのファイル名も知ることができる。ここでは、20個までのファイルを対象としよう。それではさっそくやってみよう。
List 1
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) btnCombine: TButton; Memo1: TMemo; btnGetHeader: TButton; procedure btnCombineClick(Sender: TObject); procedure btnGetHeaderClick(Sender: TObject); private { Private 宣言 } public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.DFM} type TFileInfoData = record Size:Cardinal; Offset:Cardinal; Name:array[0..30] of Char; end; TCombineFilesHeader = record NumberOfFiles:integer; FileInfo:array[0..19] of TFileInfoData; end; function CombineFiles(MS:TMemoryStream;FileList:TStrings):Boolean; var CFH:TCombineFilesHeader; EachFile:TMemoryStream; i,Num:integer; begin result := false; if FileList.Count = 0 then exit; if FileList.Count > 20 then Num := 20 else Num := FileList.Count; FillChar(CFH,SizeOf(TCombineFilesHeader),#0); MS.Clear; EachFile := TMemoryStream.Create; try CFH.NumberOfFiles := Num; MS.Position := SizeOf(CFH); for i := 0 to Num-1 do begin EachFile.LoadFromFile(FileList[i]); with CFH.FileInfo[i] do begin Size := EachFile.Size; Offset := MS.Position; StrPLCopy(Name,ExtractFileName(FileList[i]),30); end; MS.Write(EachFile.Memory^,EachFile.Size); end; MS.Position := 0; MS.Write(CFH,SizeOf(CFH)); result := true; finally EachFile.Free; end; end; function GetFileHeader(CombFile:string;var CFH:TCombineFilesHeader):Boolean; var FS:TFileStream; begin FS := TFileStream.Create(CombFile,fmOpenRead or fmShareDenyWrite); try result := false; FS.Position := 0; FS.Read(CFH,SizeOf(CFH)); result := true; finally FS.Free; end; end; const CombFileName = 'C:\ABEHome\Test\CombFile.cmb'; procedure TForm1.btnCombineClick(Sender: TObject); var SL:TStringList; MS:TMemoryStream; begin SL := TStringList.Create; MS := TMemoryStream.Create; try SL.Add('C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\chemical.ico'); SL.Add('C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\chip.ico'); SL.Add('C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\earth.ico'); if CombineFiles(MS,SL) then MS.SaveToFile(CombFileName); finally SL.Free; MS.Free; end; end; procedure TForm1.btnGetHeaderClick(Sender: TObject); var CFH:TCombineFilesHeader; i:integer; begin if GetFileHeader(CombFileName,CFH) then begin Memo1.Clear; for i := 0 to CFH.NumberOfFiles-1 do begin with CFH.FileInfo[i] do Memo1.Lines.Add(Format('%d : %s Size: %d Offset: %d', [i,Name,Size,Offset])); end; end; end; end.
実行して、btnCombine を押し、続いて btnGetHeader を押すと右図のようになる。どうやらうまくいってそうである。ここではまだ分割して取り出していない。
コードを見てみよう。ファイルの連結は以下の関数で行っている。
function CombineFiles(MS:TMemoryStream;FileList:TStrings):Boolean; var CFH:TCombineFilesHeader; EachFile:TMemoryStream; i,Num:integer; begin result := false; if FileList.Count = 0 then exit; if FileList.Count > 20 then Num := 20 else Num := FileList.Count; FillChar(CFH,SizeOf(TCombineFilesHeader),#0); // ヘッダレコードの初期化 MS.Clear; EachFile := TMemoryStream.Create; try CFH.NumberOfFiles := Num; // 結合するファイルの数 MS.Position := SizeOf(CFH); // ヘッダ部分の後に書き込む for i := 0 to Num-1 do begin // ファイルの数だけ繰り返す EachFile.LoadFromFile(FileList[i]); // 個々のファイルを読み込む with CFH.FileInfo[i] do begin Size := EachFile.Size; // サイズをヘッダに設定する Offset := MS.Position; // オフセットバイトを設定する StrPLCopy(Name,ExtractFileName(FileList[i]),30); // フィル名の設定 end; MS.Write(EachFile.Memory^,EachFile.Size); // 書き込む end; MS.Position := 0; // 先頭に戻す MS.Write(CFH,SizeOf(CFH)); // ヘッダレコードを書き込む result := true; finally EachFile.Free; end; end;
この関数は、連結したファイルイメージを格納するための TMemoryStream オブジェクトと複数のファイル名のフルパスを設定した TStrings オブジェクトを受け取り、実際に TMemoryStream オブジェクトの中に連結する。ここでは、
const CombFileName = 'C:\ABEHome\Test\CombFile.cmb'; procedure TForm1.btnCombineClick(Sender: TObject); var SL:TStringList; MS:TMemoryStream; begin SL := TStringList.Create; MS := TMemoryStream.Create; try SL.Add('C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\chemical.ico'); SL.Add('C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\chip.ico'); SL.Add('C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\earth.ico'); if CombineFiles(MS,SL) then MS.SaveToFile(CombFileName); finally SL.Free; MS.Free; end; end;
のようにして、三つのアイコンファイル名を設定した TStringList オブジェクトを渡して連結を実行し、CombFileName として保存している。ヘッダ部分を取り出す関数を見てみよう。
function GetFileHeader(CombFile:string;var CFH:TCombineFilesHeader):Boolean; var FS:TFileStream; begin FS := TFileStream.Create(CombFile,fmOpenRead or fmShareDenyWrite); try result := false; FS.Position := 0; // ヘッダ読み込み位置(先頭) FS.Read(CFH,SizeOf(CFH)); // 読み込み result := true; finally FS.Free; end; end;
ここでは TFileStream のインスタンスで連結したファイルを読み込み、TCombineFilesHeader 型のレコード変数にヘッダ部分を読み込んでいる。
それでは、上で作った連結ファイルを元にもどして取り出してみよう。その前に、コードを管理しやすいように、ユニットファイルに分離しよう。IDE のメニュー「ファイル」「新規作成」から「ユニット」を選んで、以下のようにする。
List 2
unit CombineFilesUnit; interface uses Windows, Messages, SysUtils, Classes; type TFileInfoData = record Size:Cardinal; Offset:Cardinal; Name:array[0..30] of Char; end; TCombineFilesHeader = record NumberOfFiles:integer; FileInfo:array[0..19] of TFileInfoData; end; TMemoryStreamArray = array[0..19] of TMemoryStream; function CombineFiles(MS:TMemoryStream;FileList:TStrings):Boolean; function GetFileHeader(CombFile:string;var CFH:TCombineFilesHeader):Boolean; procedure SplitCombFile(CombFile:string; var MSA:TMemoryStreamArray;var CFH:TCombineFilesHeader); implementation function CombineFiles(MS:TMemoryStream;FileList:TStrings):Boolean; var CFH:TCombineFilesHeader; EachFile:TMemoryStream; i,Num:integer; begin result := false; if FileList.Count = 0 then exit; if FileList.Count > 20 then Num := 20 else Num := FileList.Count; FillChar(CFH,SizeOf(TCombineFilesHeader),#0); MS.Clear; EachFile := TMemoryStream.Create; try CFH.NumberOfFiles := Num; MS.Position := SizeOf(CFH); for i := 0 to Num-1 do begin EachFile.LoadFromFile(FileList[i]); with CFH.FileInfo[i] do begin Size := EachFile.Size; Offset := MS.Position; StrPLCopy(Name,ExtractFileName(FileList[i]),30); end; MS.Write(EachFile.Memory^,EachFile.Size); end; MS.Position := 0; MS.Write(CFH,SizeOf(CFH)); result := true; finally EachFile.Free; end; end; function GetFileHeader(CombFile:string;var CFH:TCombineFilesHeader):Boolean; var FS:TFileStream; begin FS := TFileStream.Create(CombFile,fmOpenRead or fmShareDenyWrite); try result := false; FS.Position := 0; FS.Read(CFH,SizeOf(CFH)); result := true; finally FS.Free; end; end; procedure SplitCombFile(CombFile:string; var MSA:TMemoryStreamArray;var CFH:TCombineFilesHeader); var MS:TMemoryStream; i:integer; begin if not GetFileHeader(CombFile,CFH) then exit; MS := TMemoryStream.Create; for i := 0 to CFH.NumberOfFiles-1 do MSA[i] := TMemoryStream.Create; try MS.LoadFromFile(CombFile); for i := 0 to CFH.NumberOfFiles-1 do begin MSA[i].SetSize(CFH.FileInfo[i].Size); MS.Position := CFH.FileInfo[i].Offset; MS.Read(MSA[i].Memory^,CFH.FileInfo[i].Size); end; finally MS.Free; end; end; end.
ここでは、
type TMemoryStreamArray = array[0..19] of TMemoryStream;
と、SplitCombFile 手続きだけを追加した。他は、以前と同じである。これを、CombineFilesUnit.pas として保存する。SplitCombFile 手続きをテストするプログラムは、前回のに btnSlpit ボタンを追加して以下のようにする。全リストを示す。
List 3
unit Main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) btnCombine: TButton; Memo1: TMemo; btnGetHeader: TButton; btnSplit: TButton; procedure btnCombineClick(Sender: TObject); procedure btnGetHeaderClick(Sender: TObject); procedure btnSplitClick(Sender: TObject); private { Private 宣言 } public { Public 宣言 } end; var Form1: TForm1; implementation {$R *.DFM} uses CombineFilesUnit; const CombFileName = 'C:\ABEHome\Test\CombFile.cmb'; procedure TForm1.btnCombineClick(Sender: TObject); var SL:TStringList; MS:TMemoryStream; begin SL := TStringList.Create; MS := TMemoryStream.Create; try SL.Add('C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\chemical.ico'); SL.Add('C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\chip.ico'); SL.Add('C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\earth.ico'); if CombineFiles(MS,SL) then MS.SaveToFile(CombFileName); finally SL.Free; MS.Free; end; end; procedure TForm1.btnGetHeaderClick(Sender: TObject); var CFH:TCombineFilesHeader; i:integer; begin if GetFileHeader(CombFileName,CFH) then begin Memo1.Clear; for i := 0 to CFH.NumberOfFiles-1 do begin with CFH.FileInfo[i] do Memo1.Lines.Add(Format('%d : %s Size: %d Offset: %d', [i,Name,Size,Offset])); end; end; end; procedure TForm1.btnSplitClick(Sender: TObject); var MSA:TMemoryStreamArray; CFH:TCombineFilesHeader; i:integer; IC:TIcon; begin Refresh; Memo1.Clear; SplitCombFile(CombFileName,MSA,CFH); IC := TIcon.Create; try for i := 0 to CFH.NumberOfFiles-1 do begin IC.LoadFromStream(MSA[i]); Canvas.Draw(10+50*i,10,IC); Memo1.Lines.Add(CFH.FileInfo[i].Name); end; finally for i := 0 to CFH.NumberOfFiles-1 do MSA[i].Free; IC.Free; end; end; end.
実行して、btnSplit ボタンを押すと右図のようになる。このようにして、元のファイルを取り出すことができた。
SplitCombFile 手続きでは、連結ファイル名と、TMemoryStreamArray 型と TCombineFilesHeader 型の変数を参照渡しで受け取って、TMemoryStreamArray 型変数の各要素である TMemoryStream のインスタンスをファイルの数の分だけつくり、それらにファイルイメージを格納している。また、TCombineFilesHeader 型の変数にはヘッダの内容が格納される。
procedure SplitCombFile(CombFile:string; var MSA:TMemoryStreamArray;var CFH:TCombineFilesHeader); var MS:TMemoryStream; i:integer; begin if not GetFileHeader(CombFile,CFH) then exit; MS := TMemoryStream.Create; for i := 0 to CFH.NumberOfFiles-1 do MSA[i] := TMemoryStream.Create; // ファイルの数と同じ数のインスタンスを作る try MS.LoadFromFile(CombFile); // 連結ファイルの読み込み for i := 0 to CFH.NumberOfFiles-1 do begin MSA[i].SetSize(CFH.FileInfo[i].Size); // ファイルのサイズを設定 MS.Position := CFH.FileInfo[i].Offset; // 読み込み位置を設定 MS.Read(MSA[i].Memory^,CFH.FileInfo[i].Size); // 読み込み end; finally MS.Free; end; end;
このようにして、複数の元ファイルを同数の TMemoryStream のインスタンスに取り出している。
procedure TForm1.btnSplitClick(Sender: TObject); var MSA:TMemoryStreamArray; CFH:TCombineFilesHeader; i:integer; IC:TIcon; begin Refresh; Memo1.Clear; SplitCombFile(CombFileName,MSA,CFH); IC := TIcon.Create; try for i := 0 to CFH.NumberOfFiles-1 do begin IC.LoadFromStream(MSA[i]); Canvas.Draw(10+50*i,10,IC); Memo1.Lines.Add(CFH.FileInfo[i].Name); end; finally for i := 0 to CFH.NumberOfFiles-1 do MSA[i].Free; // ストリームオブジェクトの破棄(重要!) IC.Free; end; end;
ここでは、それぞれの元ファイルのストリームを TIcon の LoadFromStream() で読み込み、Canvas に描画している。関数の中で複数のストリームオブジェクトを作り、呼び出し側で破棄する仕様になっている。これはかなり駄目な仕様ではある。(笑)
次に、連結ファイルの中から任意の一つのファイルを取りだしてみよう。CombineFilesUnit.pas に以下の関数を追加する。
List 4
function FindFileIndex(CombFile, FileName:string):integer; var CFH:TCombineFilesHeader; i:integer; begin result := -1; if GetFileHeader(CombFile,CFH) then for i := 0 to CFH.NumberOfFiles-1 do if UpperCase(CFH.FileInfo[i].Name) = UpperCase(FileName) then begin result := i; exit; end; end; function ExtractFileFromCombined(CombFile:string; indx:integer; MS:TMemoryStream):Boolean; var CFH:TCombineFilesHeader; Source:TMemoryStream; begin result := false; if not GetFileHeader(CombFile,CFH) then exit; if (indx < 0) or (indx > CFH.NumberOfFiles-1) then exit; Source := TMemoryStream.Create; try Source.LoadFromFile(CombFile); MS.Clear; Source.Position := CFH.FileInfo[indx].Offset; MS.SetSize(CFH.FileInfo[indx].Size); Source.Read(MS.Memory^,CFH.FileInfo[indx].Size); result := true; finally Source.Free; end; end;
FindFileIndex() は、連結ファイルのヘッダを読みとって、ファイル名を検索して同名のファイルが見つかったら、そのインデックスを返す。ExtractFileFromCombined() は、インデックスを指定して、連結ファイルの中から一つだけ MS に取り出す。FindFileIndex() を使えば、ファイル名を指定して取り出すこともできる。これをテストするために、前のプログラムに btnExtarct ボタンを追加して、OnClick イベントハンドラを以下のようにする。
List 5
procedure TForm1.btnExtractClick(Sender: TObject); var MS:TMemoryStream; IC:TIcon; begin Refresh; MS := TMemoryStream.Create; IC := TIcon.Create; try if ExtractFileFromCombined(CombFileName,1,MS) then begin IC.LoadFromStream(MS); Canvas.Draw(10,10,IC); end; finally MS.Free; IC.Free; end; end;
実行して、btnExtract ボタンを押すと右図のようになる。ここではインデックス1のファイルを取り出している。
つぎに、連結ファイルの末尾に任意のファイルを一つ追加する関数と、インデックスを指定して一つのファイルを削除する関数をつくろう。CombineFilesUnit.pas に以下の関数を追加する。
List 6
function AddCombineFile(SourceFile,AddFile:string):Boolean; var CFH:TCombineFilesHeader; MS,EachFile:TMemoryStream; Num:integer; begin MS := TMemoryStream.Create; EachFile := TMemoryStream.Create; try MS.LoadFromFile(SourceFile); MS.Position := 0; MS.Read(CFH,SizeOf(CFH)); Num := CFH.NumberOfFiles; if Num < 20 then begin EachFile.LoadFromFile(AddFile); CFH.NumberOfFiles := Num+1; MS.Position := MS.Size; with CFH.FileInfo[Num] do begin Size := EachFile.Size; Offset := MS.Position; StrPLCopy(Name,ExtractFileName(AddFile),30); end; MS.Write(EachFile.Memory^,EachFile.Size); MS.Position := 0; MS.Write(CFH,SizeOf(CFH)); MS.SaveToFile(SourceFile); result := true; end else result := false; finally MS.Free; EachFile.Free; end; end; function DeleteFromCombFile(CombFile:string; DeleteIndex:integer):Boolean; var CFH,NEWCFH:TCombineFilesHeader; Source,NewComb,Each:TMemoryStream; i,indx:integer; begin result := false; if GetFileHeader(CombFile,CFH) then begin if (DeleteIndex > CFH.NumberOfFiles-1) or (DeleteIndex < 0) then Exit; end else Exit; FillChar(NEWCFH,SizeOf(TCombineFilesHeader),#0); Source := TMemoryStream.Create; NewComb := TMemoryStream.Create; Each := TMemoryStream.Create; try Source.LoadFromFile(CombFile); indx := 0; NEWCFH.NumberOfFiles := CFH.NumberOfFiles-1; NewComb.Position := SizeOf(NEWCFH); for i := 0 to CFH.NumberOfFiles-1 do begin if i = DeleteIndex then continue; NEWCFH.FileInfo[indx] := CFH.FileInfo[i]; NEWCFH.FileInfo[indx].Offset := NewComb.Position; Source.Position := CFH.FileInfo[i].Offset; Each.SetSize(CFH.FileInfo[i].Size); Source.Read(Each.Memory^,CFH.FileInfo[i].Size); NewComb.Write(Each.Memory^,CFH.FileInfo[i].Size); Each.Clear; Inc(indx) end; NewComb.Position := 0; NewComb.Write(NEWCFH,SizeOf(CFH)); NewComb.SaveToFile(CombFile); result := true; finally Source.Free; NewComb.Free; Each.Free; end; end;
これをテストするために、前のプログラムに btnAdd ボタンと btnDelete ボタンを追加して、その OnClick イベントハンドラを以下のようにする。
List 7
procedure TForm1.btnAddClick(Sender: TObject); begin if AddCombineFile(CombFileName, 'C:\ABEHome\Appl\Delphi5\Borland Shared\Images\Icons\factory.ico') then btnSplitClick(Sender) else ShowMessage('AddCombineFile Failed!'); end; procedure TForm1.btnDeleteClick(Sender: TObject); begin DeleteFromCombFile(CombFileName,1); btnSplitClick(Sender); end;
実行して、btnAdd を押すと右図のようになる。四番目のアイコンが追加されているのが分かる。
次に、btnDelete を押すと右図のようになる。インデックス1のアイコンが削除されている。
以上のように、ここでは複数のアイコンファイルの連結、分割、追加、削除、などを試したが、実際は、種類が違う複数のファイルについても同様にして連結することができる。この場合、分割して取り出すときは、ヘッダのファイル名を参照して、またその拡張子などを適切に判断して、とりだしたファイルの処理を分岐する必要がある。最後に、ここで作った CombineFilesUnit.pas をリンクしておきます。