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 をリンクしておきます。