日曜日にBCB!



Do It Yourselfでひとつプログラムでも作ってみませんか。


第5話「プログラミング開始」

アルゴリズム、データフローダイアグラム、イメージまで終わりました。これでプログラミングを開始できます。一般には個々の関数の仕様書を作成する(私も昔はそうしていた)のですが、ここでは仕事ではないので仕様書は作成しません。関数のほとんどはコントロールになんらかのイベントが発生たときに処理をするコールバック関数で、関数仕様は予め決められています。処理専用の関数も作成しますが、これはその時々に仕様を決めていきたいと思います。


「ファイルを開く」データフロー

ステップ1 - 分割ファイルを開く

まずは分割ファイルを開いて、分割先ファイル一覧、結合バッチファイル名と分割先パスを作成するステップをプログラミングします。

このステップでは「ファイル」「開く...」メニューに対するコールバック関数を追加します。コールバック関数では、
  1. オープンダイアログ(OpenDLG)を実行し、分割元ファイルパス+名前を取得。
  2. 分割先ファイル一覧および結合バッチファイル名を設定。
  3. 分割先パスをステータスバー(StatusBAR)に設定。
を行います。

メイン画面MainWNDから「ファイル」「開く...」メニューを選択します。プログラムエディタに切り替わりますので、この関数に処理を追加します。
//---------------------------------------------------------------------------
void __fastcall TMainWND::LoadMNUClick(TObject *Sender)
{
    if (OpenDLG->Execute()){    // オープンダイアログを実行し、分割元ファイルパス+名前を取得
        ListDivideFiles(OpenDLG->FileName);    // 分割先ファイル一覧および結合バッチファイル名を設定
        SetupDestPath(OpenDLG->FileName);      // 分割先パスをステータスバーに設定。
    }
}
//---------------------------------------------------------------------------
ここでListDivideFiles関数とSetupDestPath関数はこれから作ります。プログラムを作るときに問題になるのが「どのように処理をまとめるか」です。私は逆に「このような処理をする関数がある」と仮定してトップダウン設計していきます。トップダウン設計の方がプログラム全体を見通し良く作れるので良いと思います(たまには失敗することもありますが、あとは場数と経験でまかないましょう)。関数を作るときには「如何に再利用できるか」をポイントとしましょう。要は楽をするためです。
トップダウンで関数を仮定していきますし、仮定した関数が更に仮定した関数を呼び出すことも多々ありますので、関数名は必ず理解し易い名前としましょう。これを怠るとプログラムサイズがちょっとでも大きくなると理解できなくなります。なんたって「人間は忘れる葦である」とパスカルも言ったか言わないか。

ここでちょっと思い出してください。第4話のアルゴリズム「ファイルを分割した場合、最後のファイルサイズが他のファイルサイズよりも小さくなることがあります。そこで個々の分割サイズを各リストに設定します。同時に結合バッチファイル名とそのサイズもリストに設定します。」と述べました。実はこの「個々の分割サイズを各リストに設定」する作業が寿分割のキーポイントです。リストビューの個々のリストアイテムのクラスTListItemにはDataというプロパティがあります。これはTagプロパティと似ていて、各リストと個別の情報と関連付けることができる非常に便利な方法です。Dataというプロパティはvoid*型をしています。これは任意のオブジェクトへのポインタです。これに分割サイズをリンクさせます。折角C++言語を使っていますので、ひとつ「分割サイズクラス」でも作ってみましょう。

//---------------------------------------------------------------------------
class cDivideFile{
  private:
    long Size;    // 分割サイズ
  public:
    cDivideFile(int _Size){ Size = _Size; }  // コンストラクタ
    void SetSize(int _Size){ Size = _Size; } // 分割サイズ設定
    long GetSize(){ return(Size); }          // 分割サイズ取得
};
//---------------------------------------------------------------------------
クラスの中身は至って簡単です。オブジェクトインスタンス時に分割サイズを強制的に設定させます(コンストラクタ処理)。メンバー関数では分割サイズの設定と取得を行います。このような簡単な処理をクラス化する意味はないかもしれませんが、クラス作成の練習を兼ねていますのでご勘弁を。このクラス定義をメイン画面のヘッダファイルに定義しておきます。
このクラスは、
    ListVEW->Data = new cDivideFile(分割サイズ);
と利用します。

ListDivideFiles関数

割先ファイルパスを引数とし、
  1. 分割先ファイル一覧(ListVEW)をクリア。
  2. 分割元ファイルサイズの取得。
  3. 分割サイズの取得。
  4. 分割数の計算(分割ファイル数、結合バッチファイルは除く)。
  5. 分割先ファイル一覧作成(分割ファイル名作成と分割サイズの設定)。
  6. 結合バッチファイルの作成。
  7. 結合バッチファイルサイズの取得。
  8. 結合バッチファイル情報設定。
を行います。ここではリストビューオブジェクトのコントロールが重要です。TListViewクラスとTListItemクラスの関係、リストとアイコンとの接続、分割サイズオブジェクトの生成とリンク、サブアイテムへの文字列追加を行っています。これらはリストビューオブジェクトの基本的な操作です。
ファイルサイズの取り扱いについて考えます。分割ファイルサイズを個々のリストに設定する際、分割元ファイルサイズを予め取得し分割残りサイズとして初期化します。分割の繰り返しのループの中では、
  1. 分割残りサイズが指定分割サイズよりを超える場合は、指定分割サイズを設定
  2. 分割残りサイズが指定分割サイズ以下場合は、分割残りサイズを設定
  3. 分割残りサイズを更新
を行います。
また、分割サイズをリスト表示しますが、100万を超える値をバイト単位のまま表示しても不便です。そこで、分割ファイルサイズはキロバイト単位で表示します(バイト単位を1024で割る)。結合バッチファイルは数百バイト〜数キロバイトになるでしょうから、こちらはそのままバイト単位で表示します。
//---------------------------------------------------------------------------
bool __fastcall TMainWND::ListDivideFiles(AnsiString FilePath)
{
// 分割先ファイル一覧をクリア
    ListVEW->Items->Clear();
    if (FilePath=="") return(true);    // 分割元ファイルが未設定の場合、以下の処理をスキップ

// 分割元ファイルサイズの取得(ファイルサイズは分割残りサイズで更新される)
    int FileSize = GetFileSize(FilePath);

// 分割サイズの取得
    int CutSize  = GetCutSize();

// 分割数の計算(分割ファイル数、結合バッチファイルは除く)
    int NumCut   = FileSize/CutSize+1;

// 分割先ファイル一覧作成(分割ファイル名作成と分割サイズの設定)
    AnsiString FileName = ExtractFileName(FilePath);    // 分割ファイル名のみを取得(パスの削除)
    AnsiString SizeStr;         // 分割サイズ文字列バッファ
    for (int i=0;i<NumCut;i++){    // 分割数ループ
        TListItem *Item = ListVEW->Items->Add();                     // 分割先ファイルリストアイテム追加
        Item->Caption = ChangeFileExt(FileName, MakeOrderExt(i));    // 分割先ファイル名設定
        Item->ImageIndex = 0;                                        // 分割先ファイルアイコン設定
        cDivideFile *File;    // 分割ファイルオブジェクト
        if (FileSize>CutSize) File = new cDivideFile(CutSize);    // 分割サイズ設定
        else                  File = new cDivideFile(FileSize);
        Item->Data = File;    // リスト-分割ファイルオブジェクトリンク
        SizeStr = Format("%3.0nKB", OPENARRAY(TVarRec, ((double)(File->GetSize())/1024.0)));
            // 分割サイズ文字列作成(KB単位)
        Item->SubItems->Add(SizeStr);   // 分割サイズ文字列設定
        FileSize -= CutSize;            // ファイルサイズ更新
    }

// 結合バッチフコマンド作成
    AnsiString ConnectBatch = MakeConnectBatch(FileName, NumCut);       // 結合バッチコマンド作成
    TListItem *Item = ListVEW->Items->Add();    // 分割バッチファイルリストアイテム追加
    Item->Caption = ChangeFileExt(FileName, ".bat");    // 分割バッチファイル名設定
    Item->ImageIndex = 1;                               // 結合バッチファイルアイコン設定

// 結合バッチファイルサイズ設定
    cDivideFile *File = new cDivideFile(ConnectBatch.Length());    // 分割サイズ設定
    Item->Data = File;    // リスト-分割ファイルオブジェクトリンク

// 結合バッチファイル情報設定
    SizeStr = Format("%3.0n", OPENARRAY(TVarRec, ((double)DivideFile->GetSize())));
        // 結合バッチファイルサイズ文字列設定(B単位)
    Item->SubItems->Add(AnsiString(SizeStr + "B"));     // 結合バッチファイルサイズ設定

// 成功を返す
    return(true);
}
//---------------------------------------------------------------------------
ここで注意です。オブジェクトをnewでインスタンスしたら、不要になったら必ずdeleteで開放しましょう。さもないとメモリリークが発生します。TListViewクラスではリストが削除されるときにOnDeletionイベントが発生します。このイベントに対するコールバック関数で開放処理を入れます。

//---------------------------------------------------------------------------
void __fastcall TMainWND::ListVEWDeletion(TObject *Sender, TListItem *Item)
{
    cDivideFile *File = (cDivideFile*)Item->Data;    // 分割サイズオブジェクト取得
    delete File;    // 分割サイズオブジェクト開放
}
//---------------------------------------------------------------------------

GetFileSize関数

分割元ファイルのサイズを取得し、バイト単位で返すように作成します。ファイルサイズはBCBライブラリ関数FindFirstで取得すると便利です。このような処理は再利用性が高いのでメイン画面クラスTMainWNDのメンバ関数とせずに独立させましょう。
//---------------------------------------------------------------------------
int GetFileSize(AnsiString FileName)
{
    TSearchRec sr;    // FindFirst関数(BCBライブラリ関数)で使用する検索レコード型

    FindFirst(FileName, faAnyFile, sr);    // ファイル情報取得
    long size = sr.Size;                   // ファイル情報からファイルサイズ取得
    FindClose(sr);                         // ファイル情報領域開放

    return(size);    // ファイルサイズを返す
}
//---------------------------------------------------------------------------

GetCutSize関数

分割サイズメニューから分割サイズを取得します。第4話オブジェクトインスペクタで各メニューのTagに分割サイズを設定しました。分割サイズはこちらから取得します。
//---------------------------------------------------------------------------
int __fastcall TMainWND::GetCutSize()
{
    int CutSize;    // 分割サイズ(バイト単位)

    if (Size1440MNU->Checked)   CutSize = Size1440MNU->Tag;    // 1.44M分割サイズ取得
    if (Size0720MNU->Checked)   CutSize = Size0720MNU->Tag;    // 720K分割サイズ取得

    return(CutSize);    // 分割サイズを返す
}
//---------------------------------------------------------------------------

MakeOrderExt関数

分割ファイルの拡張子("001"、"002"、...)を分割番号(0、1、...)から作成します。BCBではFormat関数で数値を書式付きで文字列に変換できますが、'0'を空白に埋める処理が別途必要となります。このような処理も再利用性が高いのでメイン画面クラスTMainWNDのメンバ関数とせずに独立させましょう。
//---------------------------------------------------------------------------
AnsiString MakeOrderExt(int i)
{
    AnsiString Ext = "." + Format("%3d", OPENARRAY(TVarRec, (i+1)));    // "  1", "  2"... に変換
    for (int i=1;i<Ext.Length();i++){    // 空白を'0'で埋める
        if (Ext[i]==' ') Ext[i] = '0';
    }

    return(Ext);    // 分割ファイル拡張子を返す
}
//---------------------------------------------------------------------------

MakeConnectBatch関数

ファイルを結合するMS-DOSバッチコマンドを作成します。バッチコマンドは以下の書式のテキストです。
copy /b SrcFile1 + SrcFile2 DstFile
ここで、
・SrcFile1, SrcFile2 : 結合元ファイル名
・DstFile: 結合先ファイル名

寿分割では分割ファイルの結合バッチコマンドを並べたテキストをバッチファイルに出力します。
例えば"File.001"、"File.002"、"File.003"、"File.004"と分割し、File.outに結合する場合、
copy /b File.001 + File.002 File.out
copy /b File.out + File.003 File.out
copy /b File.out + File.004 File.out
とします。
//---------------------------------------------------------------------------
AnsiString __fastcall TMainWND::MakeConnectBatch(AnsiString FilePath, int NumCut)
{
    AnsiString FileName = ExtractFileName(FilePath);    // ファイルパスからファイル名の取得

    AnsiString Batch = "copy /b \""    // 001と002ファイルの結合コマンド作成
                     + ChangeFileExt(FileName, MakeOrderExt(0)) + "\"+\""
                     + ChangeFileExt(FileName, MakeOrderExt(1)) + "\" \""
                     + FileName + "\"\r\n";

    for (int i=2;i<NumCut;i++){    // 003以降の結合コマンド作成
        Batch = Batch + "copy /b \"" + FileName  + "\"+\""
                      + ChangeFileExt(FileName, MakeOrderExt(i)) + "\" \""
                      + FileName + "\"\r\n";
    }

    return(Batch);    // 結合コマンドテキストを返す
}
//---------------------------------------------------------------------------

SetupDestPath関数

分割先パスをステータスバーに設定します。フルパスを含むファイル名からパス名だけを取出すのはBCBのExtractFilePath関数を使うだけで処理できます。ただ、パス名の慣例的な表記方法には癖があって、
となります。
//---------------------------------------------------------------------------
void __fastcall TMainWND::SetupDestPath(AnsiString FilePath)
{
    AnsiString FolderPath = ExtractFilePath(FilePath);    // ファイルパスだけを取出す

    if (FolderPath.Length()>3){    // ルートパス以外の場合
        if (FolderPath[FolderPath.Length()]=='\\'){    // 最後が"\"の場合
            FolderPath = FolderPath.SubString(1, FolderPath.Length()-1);    // "\"を削除
        }
    }
    StatusBAR->SimpleText = FolderPath;    // ステータスバーにファイルパスを設定
}
//---------------------------------------------------------------------------


ステップ2 - 新規作成

分割ファイルを開く処理で、オープンダイアログの分割元ファイルパス+名前をクリアし、その他は同じ処理をします。

「ファイル」「新規作成」メニューに対するコールバック関数を追加します。
//---------------------------------------------------------------------------
void __fastcall TMainWND::NewMNUClick(TObject *Sender)
{
    OpenDLG->FileName = "";    // 分割元ファイルパス+名前をクリア
    ListDivideFiles(OpenDLG->FileName);    // 分割先ファイル一覧および結合バッチファイル名を設定
    SetupDestPath(OpenDLG->FileName);      // 分割先パスをステータスバーに設定。
}
//---------------------------------------------------------------------------


動作確認はこまめにしましょう

以上で分割元ファイル選択および新規作成から、分割ファイルとバッチファイル一覧表示までが動作します。さて、プログラムの動作確認はどのタイミングですればよいでしょうか。大型コンピュータや汎用機の時代は机上デバッグをとことんまでやりました。コンパイルするにも秒単位で課金されていたためです。家庭のパソコンでプログラムを開発するのであればパソコンは使ったほうが得です。たしかに机上デバッグは非常に優れたデバッグ方法ですし、私も仕事でプログラムを開発していたときには何日も部屋にこもって机上デバッグをしました。自分がCPUになったつもりでプログラムの1行、1文字を真剣にデバッグしました。しかし、趣味でこのような寡黙な作業をする人はまずいないでしょうし、私もDDTソフトウェアの開発にはこのような手段はとりません。
私流のデバッグの仕方は、
  1. まずコンパイルをかける。
    文法エラーや警告がワンサカ出てきます。これを一つ一つ修正します。コンパイラが出すエラーは初歩的なものが多く、人間で過ちを探すよりは機械に任せたほうが発見が早いです。
    ここで大切なことは警告(ワーニング)も決して見逃さないことです。ワーニングは型の不一致やあいまいな文法解釈で出てきます。「これぐらい平気だろう」と油断すると後々大きな過ちにはまってしまいます。プログラムは誤りがあっても動きます。いや、一見正しく動いているように見えるだけで、中に不具合を隠してしまいます。型の不一致やメモリ管理の甘さはある日突然プログラムを異常停止させます。丹念に修正してください。
  2. 基本的な操作をしてみる。
    まずは基本的な操作をしてみましょう。開発者の意図したとおりに動くか確認します。動いていなければ即修正作業となるのは当然ですが、動いていてもデバッガで一通りプログラムの全てのステップを通してみましょう。BCBはデバッガも強力ですから、変数インスペクトを使って主要な値の経過をチェックしましょう。意外と「これはおかしいな」という個所が出てくる可能性もあります。
  3. 異常な操作をしてみる。
    「こんな操作はしないな」と思われることをどんどんやりましょう。プログラムの強靭さは異常な操作にどのくらい対応できるかが決め手です。またデータを必要とするプログラムは異常なデータもわざと準備しましょう。寿分割では分割元ファイルに存在しないファイルを指定したり、データサイズが非常に小さい、もしくはサイズ0のファイルを分割させてみましょう。おそらく異常な動作をすること間違いないです。なんたってまだエラーリカバリー処理を全く入れてませんから。プログラムが大きくなる主な原因はこの「エラーリカバリー」です。主となる処理の何倍にもなることもあります。
以上が機能単位で行うコンパイル・動作確認です。今回ビルドした実行画面は以下のようになります。「ファイル」「新規作成」、「ファイル」「開く...」メニューが動作しますので、まずは正常な操作をしてみてください。

第5話ビルド実行結果


こちらから今回作成したBCBのプロジェクト一式をダウンロードできます。ダウンロード(8,268バイト) BCBのバージョン3、4、5でビルドできます。参考にしてください。


次回はファイル分割の中心部のプログラミングを行います。これで一応寿分割が動作するようになります。ご期待ください。

今回のプログラミング行数: 126行
今回までのプログラミング行数: 128行


目次に戻る目次に戻る トップに戻るトップに戻る