日曜日にBCB!



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


第6話「寿分割誕生」

前回でやっと張りぼてのプログラムにデータが流れ出しました。今回は残りのプログラムを追加して実際にファイル分割を行えるようにします。ただ、動いたと言ってもとても実用のレベルまでは達しません。あくまでも「最後までデータが流れた」ことを確認できる程度です。


「分割サイズ」データフロー

ステップ3 - 分割サイズ選択

分割サイズの選択部分をプログラミングしましょう。
このステップでは「分割」「1.44M」と「720K」メニューに対するコールバック関数を追加します。コールバック関数では、2つの排他チェックメニューにチェックマークをつけるのと、分割リストの更新を行います。分割リスト作成は既にListDivideFile関数として作成しましたので、これを呼び出します。
2つのメニューに対するコールバック処理はメニューの違いだけです。そこでちょっと工夫して「コールバック関数の共有化」を行います。どちらのメニューが選択されたのかはコールバック関数のSenderオブジェクトでわかります。両方ともTMenuItemクラスオブジェクトですから(SenderオブジェクのクラスTObjectクラスはTMenuItemクラスの継承元なのでこのような処理ができます)、このオブジェクトに対する振る舞いをプログラムします。なお、コールバック関数名はデフォルトのSize1440MNUClickではなくSizeMNUClickとしておきましょう。
//---------------------------------------------------------------------------
void __fastcall TMainWND::SizeMNUClick(TObject *Sender)
{
    TMenuItem *MNU = (TMenuItem*)Sender;    // メニューオブジェクト取得
    MNU->Checked = true;                   // メニュー選択
    ListDivideFile(OpenDLG->FileName);    // 分割ファイルリスト更新
}
//---------------------------------------------------------------------------
この処理をSize0720MNUのコールバック関数としても登録します。


ステップ4 - 分割先の変更

分割先の変更はステータスバーに登録したパスを変更します。パスの変更方法はいくつかあります。
分割パスをキーボード入力するのは考えものです。何故なら存在しないパスを入力される可能性があるからです。入力されたパスが存在するかのチェックが必要になりますし、なによりユーザーが一生懸命入力したパスに対して「そのパスは存在しません。再度入力してください」などとエラーメッセージを表示するのも不親切です。そもそもキーボード入力するのもユーザーインターフェースから見ればダサいです。

分割パスを「ファイルを開く」画面を使って選択する方法は一見無難に見えます。しかし。この画面はあくまで「ファイル」を選択するものであって「パス」を選択するものではありません。この方法は手軽には行えますがユーザーインターフェースからみればイマイチです。

フォルダの選択(ちょっと小さいですが) 最後の「フォルダ選択」画面を使うのが一番スマートです。エクスプローラの検索画面で参照ボタンを押すと出てくるお馴染みのフォルダ選択画面です。しかし残念ながらBCB3にはフォルダ選択画面を表示するコントロールはありません。INPRISEのホームページを覗くと「フォルダの選択ダイアログを表示するには?」というTipsが載っていました。このTipsを参照しましょう。フォルダ選択画面を利用するにはWindowsのAPI関数を使います。API関数を使うからと言って文句をいっては行けません。より良いユーザーインターフェースを実現するには努力を惜しんではいけません。

このTipsを参考してまずはフォルダ選択関数を作成しましょう。フォルダの選択はSHBrowseForFolderとSHGetPathFromIDListというAPI関数を使います。BROWSEINFO構造体に必要なデータを設定したり、デフォルトフォルダを指定するために専用のコールバックプロシジャーBrowseCallbackProc関数を使ったりと少々厄介ですが、一般関数として一度まとめてしまえばこっちのものです。SHBrowseForFolder、SHGetPathFromIDList、BrowseCallbackProcはWin32プログラマーズリファレンス(英語版)のシェルネームスペース関数グループに説明があります。このリファレンスはBCB3に付随しています(5では日本語の説明があるかもしれません)。
この関数にはちょっと問題があります。デフォルトのフォルダを指定するために専用のコールバックプロシジャーを必要としますが、この関数にデフォルトのフォルダを引き渡すにはグローバル変数を必要とします。グローバル変数はデータの流れを不明瞭にする悪玉ですが、関数の仕様上仕方ありません。デフォルトのフォルダを選択最初のフォルダに設定するためAnsiString型のグローバル変数glFirstFolderを宣言しておきます。フォルダ選択SelectFolder関数を作成し、以下の処理を行います。
  1. グローバル変数glFirstFolderにデフォルトの分割パスを設定する。
  2. BROWSEINFO型の変数に必要なデータを設定する。
  3. SHBrowseForFolder関数を呼び出す。このAPI関数はBrowseCallbackProc関数を自動的に呼び出す。
  4. BrowseCallbackProc関数でデフォルトのパスをglFirstFolderから設定する。
  5. パスが選択されるとSHBrowseForFolder関数はLPITEMIDLIST型のアイテムIDリストを作成し、そのポインタを返します。選択が中断されるとNULLを返します。
  6. 選択されたら、アイテムIDリストからSHGetPathFromIDList関数を呼び出し、選択されたパスを取り出す。
  7. 最後に作成されたアイテムIDリストを開放する。
話がかなりややこしいですが、WindowsのAPI関数はこの手の手順を踏むのが多いので「郷に入れば郷に従え」で慣れましょう。
フォルダ選択SelectFolder関数は選択フォルダとフォルダ選択画面のタイトルを引数とし、選択されたらtrueを、中断ならfalseを返す仕様とします。引数にはもう一つフォルダ選択画面のオーナーハンドルも必要になります。選択フォルダの引数は参照型として戻り値にも使いましょう(参照型の使用には賛否両論ありますが、ここでは割愛します)。
#include <windowsx.h>
#include <shlobj.h>

        :

//---------------------------------------------------------------------------
AnsiString glFirstFolder;
//---------------------------------------------------------------------------
bool SelectFolder(HWND Handle, AnsiString &FolderName, AnsiString Title)
{
    BROWSEINFO BrowsingInfo;    // ユーザー選択フォルダ情報構造体(API定義)
    char DirPath[MAX_PATH];     // 選択フォルダパス
    char TitleStr[MAX_PATH];    // フォルダ選択ウィンドタイトル

    glFirstFolder = FolderName; // コールバック関数引渡し用変数設定

    memset(&BrowsingInfo, 0, sizeof(BROWSEINFO));  // 構造体クリア
    strcpy(DirPath, FolderName.c_str());           // デフォルトフォルダAnsiString->C言語文字列変換
    strcpy(TitleStr, Title.c_str());               // ウィンドタイトルAnsiString->C言語文字列変換
    BrowsingInfo.hwndOwner      = Handle;          // オーナーウィンドハンドル設定
    BrowsingInfo.pszDisplayName = DirPath;         // 選択フォルダ(戻り値)登録バッファ設定
    BrowsingInfo.lpszTitle      = TitleStr;        // ウィンドタイトル設定
    BrowsingInfo.ulFlags        = BIF_RETURNONLYFSDIRS;  // フォルダのみを選択するように設定
    BrowsingInfo.lpfn           = BrowseCallbackProc;    // コールバック関数設定
    LPITEMIDLIST ItemID = SHBrowseForFolder(&BrowsingInfo);  // フォルダ選択
    if (ItemID==NULL) return(false);               // 選択中断の場合はfalseを返す

    memset(DirPath, 0, MAX_PATH);                  // 選択フォルダクリア
    SHGetPathFromIDList(ItemID, DirPath);          // アイテムIDリストから選択フォルダ取得
    GlobalFreePtr(ItemID);                         // アイテムIDリスト領域開放
    FolderName = (AnsiString)DirPath;              // フォルダ名C言語文字列->AnsiString変換
    return(true);                                  // 選択済みtrueを返す
}
//---------------------------------------------------------------------------
int WINAPI BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
    if (uMsg==BFFM_INITIALIZED)    // フォルダ選択画面が初期化されたら
        SendMessage(hwnd, BFFM_SETSELECTION, TRUE, (long)glFirstFolder.c_str());  // デフォルトパス登録
    return(0);
}
//---------------------------------------------------------------------------
このフォルダ選択関数を「分割」「分割先...」メニューのコールバック関数から利用します。
//---------------------------------------------------------------------------
void __fastcall TMainWND::PathMNUClick(TObject *Sender)
{
    AnsiString Path = StatusBAR->SimpleText;    // 分割先パスを作業文字列に複写
    if (SelectFolder(Handle, Path, "分割先を選択してください")){    // フォルダ選択実行
        StatusBAR->SimpleText = Path;    // 選択されたら分割パスを更新
    }
}
//---------------------------------------------------------------------------


ステップ5 - ステータスバー表示・非表示

ステータスバーを持つウィンドは表示・非表示を切り換えられるのが一般的です。このプログラミングは至って簡単です。ステータスバーコントロールTStatusBarの表示プロパティVisibleを「表示」「ステータスバー」メニューのチェックCheckedと連動させるだけです。
//---------------------------------------------------------------------------
void __fastcall TMainWND::StatusBarMNUClick(TObject *Sender)
{
    TMenuItem *MNU = (TMenuItem*)Sender;    // メニューオブジェクト取得
    MNU->Checked = (!MNU->Checked);         // メニューチェックの反転
    StatusBAR->Visible = MNU->Checked;      // ステータスバー表示切替え
}
//---------------------------------------------------------------------------


ステップ6 - 分割実行

「分割実行」データフロー やっと分割実行のプログラミングまでたどり着きました。この処理でプログラミングも一段落です。分割実行は第2話のアルゴリズムで述べたことをプログラミングするだけです。プログラミングに際するポイントは、 「分割」「分割実行」のコールバック関数は以下となります。
//---------------------------------------------------------------------------
void __fastcall TMainWND::GoDivideMNUClick(TObject *Sender)
{
// 分割バッファ準備
    char *buffer = new char [GetCutSize()];

// 分割元ファイル読込みオープン
    FILE *SrcFP = fopen(OpenDLG->FileName.c_str(), "rb");

// 分割数繰り返し(結合バッチファイルは除く)
    for (int i=0;i<ListVEW->Items->Count-1;i++){
    // 分割先ファイル情報取得
        TListItem *Item = ListVEW->Items-Item[i];         // 分割先ファイル情報取得
        cDivideFile *File = (cDivideFile*)Item->Data;      // 分割ファイルオブジェクト取得
        int CutSize = File->GetSize();                     // 分割サイズ取得

    // 分割元ファイル読込み
        fread(buffer, CutSize, 1, SrcFP);                  // 分割サイズ分読込み

    // 分割先ファイル作成
        AnsiString DstFilePath = StatusBAR->SimpleText;    // 分割先ファイルパス取得
        if (DstFilePath[DstFilePath.Length()]!='\\'){      // パス区切り文字追加
            DstFilePath = DstFilePath + "\\";
        }
        DstFilePath = DstFilePath + Item->Caption;         // 分割先ファイル名作成
        FILE *DstFP = fopen(DstFilePath.c_str(), "wb");    // 分割先ファイル作成オープン
        fwrite(buffer, CutSize, 1, DstFP);                 // 分割先へ分割サイズ分書込み
        fclose(DstFP);                                     // 分割先ファイルクローズ
    }

// 分割元ファイルクローズ
    fclose(SrcFP);

// 結合バッチコマンド出力
    TListItem *Item = ListVEW->Items->Item[ListVEW->Items->Count-1];  // 分割先ファイル情報取得
    AnsiString ConnectBatch = MakeConnectBatch(OpenDLG->FileName.c_str(), ListVEW->Items->Count-1);
                                                           // 結合バッチコマンド作成
    AnsiString DstFilePath = StatusBAR->SimpleText;        // 結合バッチファイルパス取得
    if (DstFilePath[DstFilePath.Length()]!='\\'){          // パス区切り文字追加
        DstFilePath = DstFilePath + "\\";
    }
    DstFilePath = DstFilePath + Item->Caption;             // 結合バッチファイル名作成
    FILE *DstFP = fopen(DstFilePath.c_str(), "wb");        // 結合バッチファイル作成オープン
    fwrite(ConnectBatch.c_str(), ConnectBatch.Length(), 1, DstFP);       // 結合バッチコマンド書込み
    fclose(DstFP);                                         // 結合バッチファイルクローズ

    delete buffer;                                    // 分割バッファ開放
}
//---------------------------------------------------------------------------
これで寿分割は無事生まれました。第5話同様動作確認をしてみましょう。ただし、初めから大切なデータを絶対に寿分割にかけないでください。異常な動作をすると分割元ファイルさえ破壊してしまう危険性があります。必ずファイルをコピーするなりして壊れても良いデータで動作確認をしてください。当然ですがワタクシ斉藤 寿成は寿分割を使ってどのような不利益を被っても一切関知しませんから、ご承知の程を。
数メガのデータを準備して分割を行います。分割ができたようであれば、分割ファイル一覧と比較して、分割ファイルの有無、サイズ、結合バッチファイルの有無、中身の結合コマンドを確認してください。異常がないようであれば、結合バッチを実行して結合を行ってみてください。元通りに戻っていれば異常なしです。元どおりに戻っているかどうかの確認はそれぞれです。私はLZH圧縮ファイルを分割→結合し、書庫テストをかけて異常がないこと、解凍して元通りのファイルができているかを確認しました。


さて、寿分割は生まれましたが使ってみていかがですか? お世辞にも使いやすいプログラムとは言えませんよね。なぜでしょう。

これらを解決するためにはエラー処理とユーザーインターフェースの向上が必要です。このままでは作った本人さえ安心して使える代物ではないですね。これからはプログラムを強く使いやすく改良していきましょう。

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


次回はエラー処理を沢山追加したいと思います。ここからが執念の必要な作業になりますが、これがプログラムを使いやすくするための必須項目です。是非お付き合いください。

今回のプログラミング行数: 68行
今回までのプログラミング行数: 194行


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