複数のファイルをまとめて一つにする


redball.gif目次へ戻る

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;

このようなレコード型を先頭に書き込んでおくと、取り出すときは OffsetSize を参照して簡単になるだろう。また、もとのファイル名も知ることができる。ここでは、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.
n02001.gif

実行して、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.
n02002.gif

実行して、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;

ここでは、それぞれの元ファイルのストリームを TIconLoadFromStream() で読み込み、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;
n02003.gif

実行して、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;
n02004.gif

実行して、btnAdd を押すと右図のようになる。四番目のアイコンが追加されているのが分かる。



n02005.gif

次に、btnDelete を押すと右図のようになる。インデックス1のアイコンが削除されている。




以上のように、ここでは複数のアイコンファイルの連結、分割、追加、削除、などを試したが、実際は、種類が違う複数のファイルについても同様にして連結することができる。この場合、分割して取り出すときは、ヘッダのファイル名を参照して、またその拡張子などを適切に判断して、とりだしたファイルの処理を分岐する必要がある。最後に、ここで作った CombineFilesUnit.pas をリンクしておきます。

CombineFilesUnit.txt

redball.gif目次へ戻る