日曜日にBCB!



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


第7話「動くだけでは使えない」

前回までで寿分割は動くようになりました。でもとても使える代物ではありません。何故なら、 と実用には程遠いものです。これからは寿分割を強く使いやすく改良していきましょう。
まずは強くしていかないと心配で使えませんので、エラー処理を中心に処理を追加してきます。プログラムが複雑になる要因の大部分はエラー処理です。私やあなたを含めてプログラムのユーザーは必ず誤った操作をします。それに対して適切な処理をしないと大変なことになります。エラー処理を侮ってはいけません。ユーザーインターフェースとはこのエラー処理も含まれています。面倒な作業が続きますが、根気良くプログラミングしていきましょう。


分割先は大丈夫?

分割先メディア ファイルを分割すると言うことは、分割先に分割ファイルを受け取る容量があることを前提にしています。分割先もハードディスクはもちろん、フロッピーディスクやMO、ネットワークドライブが考えられます。分割を実行する前に空き容量をチェックする必要があります。
ここで3つの問題があります。

一つはフロッピードライブやMO等のリムーバブルメディアの場合は、分割ファイル毎にメディアを交換できるが、ハードディスクやネットワークドライブはメディアを交換できません。ネットワークドライブがフロッピードライブやMOと言うのも考えられます。フロッピーディスクやMOならばメディアを交換する必要があります。

もう一つはセクタ単位です。たとえば1バイトのデータをメディアに書き込むとしても、メディアにはセクタ単位を考慮したの書き込み容量が必要です。セクタサイズはメディアによってまちまちです。空き容量はセクタサイズも考慮して取得する必要があります。

最後は書き込みエラーです。記憶メディアにはライトプロテクトがかけられます。また記憶メディアは必ず書き込みエラーが発生します。ハードディスクでもフロッピーディスクでもMOでも不良セクタが発生します。不良セクタが発生したからと言って文句を言ってはいけません。メディアは消耗品ですから。書き込みエラー処理も必要です。

以上3つを考えただけでもかなり面倒になりそうです。面倒だから省略してしまえ、では自分だけが使うにしても心配です。しっかり処理しましょう。

メディアが固定か交換可能か?

分割先メディアが固定か交換可能かは、またまたWindowsのAPIのGetDriveType関数を使います。この関数には調べるドライブのルートのパスを与えます。分割先パスはステータスバーが保持しています。ちょうど具合良く、分割先の文字列の先頭から3文字はドライブ名+':\'となっていますので、これを引数に用います。
AnsiString DrivePath = StatusBAR->Panels->Items[1]->Text.SubString(1, 3);
unsigned int DriveType = GetDriveType(DrievePath.c_str());
DriveTypeはメディアタイプによって値が決まります。
意味
0 ドライブタイプが不明(エラー)
1 ルートディレクトリがない(エラー)
DRIVE_REMOVABLE ドライブは交換可能
DRIVE_FIXED ドライブは交換不可能
DRIVE_REMOTE ドライブはリモート(ネットワークドライブ)
DRIVE_CDROM ドライブはCD-ROM(書き込み不可能)
DRIVE_RAMDISK ドライブはRAMドライブ(交換不可能)
空き容量が分割サイズよりも少ない場合、分割先がメディア交換可能な場合はメディア交換を促します。分割先が固定の場合はファイル分割を中断します。ではネットワークドライブの場合はどうしましょう? 本当はネットワーク先のコンピュータに問い合わせたいのですが、Windowsにはそのようなサービスはありません(私が知らないだけかもしれません)。RPC(リモートプロシジャーコール)と言う手法もあるのですが、かなり話がややこしくなってきます。大体ネットワーク先のコンピュータの交換可能メディアにファイル分割すること自身が危険ですので、今回はネットワーク先は固定メディアとして取り扱いたいと思います。もし簡単にネットワーク先のメディアタイプがわかる方法をご存知でしたら教えてください。

空き容量は大丈夫か?

空き容量チェックはVCLのDiskFree関数でわかります。しかし、この関数には問題があります。2ギガ以上のディスク空き容量を正しく取得できません。実は分割アクセサリを自作した動機はこの問題です。Windowsのディスク管理はディスク容量の大規模化に伴い変化してきています。VCLのDiskFree関数はWindowsのAPI関数GetDiskFree関数を呼び出しています。これはWindows95時代に作られた関数で、NTの登場以降はGetDiskFreeSpaceEx関数を使います。この関数はWindows95 OSR2、Windows98、Windows2000で利用できますが、初代Windows95では利用できません。
話がとてもややこしくなってしまうのですが、寿分割はどのプラットホームでも正常に動作させたいので、API関数が入っているKernel.dllから動的ロードを利用して二つのAPI関数を使い分けます。

GetDiskFreeSpaceEx関数で得られるディスク空き容量は64ビットの符号なし整数値となります。一方符号なし整数型unsigned longが取り扱えるのは32ビットです。つまり、ディスク空き容量を整数値で取得することは不可能となります。そこでこれを実数値doubleで取り扱います。ドライブ番号(A=1,B=2,C=3...)を引数とし、初代Win95からWindows2000までで正しくディスク空き容量を取得するGetDiskFree関数を作成し、ディスク空き容量を実数値で取得します。GetDiskFreeSpaceEx関数はディスクの他の情報も同時に返すのですが、ディスク空き容量のみを取得する関数を作った方が便利です。再利用性を考えてTMainWNDのメンバ関数とせずに独立させましょう。

GetDiskFree関数

#include <winbase.h>

        :

//---------------------------------------------------------------------------
double GetDiskFree(int DriveNum)
// DriveNum A=1, B=2, C=3...
{
    static BOOL (WINAPI *APIGetDiskFreeSpaceEx)(LPCSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER);
                                    // 関数のエントリポインタを保持する変数
    double FreeSize;                // 空き容量(実数表現)
    char *DriveName = "$:\\"       // ドライブ名作成バッファの確保
    ULARGE_INTEGER FreeAvBytes;     // 使用可能空き容量(64ビット整数表現)
    ULARGE_INTEGER TotalBytes;      // 総容量(64ビット整数表現)
    ULARGE_INTEGER FreeBytes;       // 空き容量(64ビット整数表現)
    DriveName[0] = (char)('A' + DriveNum - 1);    // ドライブ名作成

    HANDLE DLLInstance = LoadLibrary("KERNEL32.DLL");      // DLL動的ロード
    APIGetDiskFreeSpaceEx = (BOOL(WINAPI *)(LPCSTR, PULARGE_INTEGER, PULARGE_INTEGER, PULARGE_INTEGER))(GetProcAddress(DLLInstance, "GetDiskFreeSpaceExA"));
                       // GetDiskFreeSpaceExA関数エントリ取得
    if (APIGetDiskFreeSpaceEx==NULL){           // GetDiskFreeSpaceExA関数がない(初代Win95)
        FreeSize = (double)DiskFree((BYTE)DriveNum);    // 旧API関数で空き容量取得
    }
    else{                               // GetDiskFreeSpaceExA関数がある(それ以降のWindows)
        int Result = APIGetDiskFreeSpaceEx(DriveName, &FreeAvBytes, &TotalBytes, &FreeBytes);    // 新API関数で空き容量取得
        if (Result) FreeSize = (double)FreeAvBytes.u.HighPart*4294967296.0 + (double)FreeAvBytes.u.LowPart;    // 64ビット整数→実数表現変換
        else        FreeSize = -1.0;    // 取得エラー
    }

    FreeLibrary(DLLInstance);    // DLL開放

    return(FreeSize);    // 空き容量(実数表現)を返す
}
//---------------------------------------------------------------------------
一方、書き込みに必要なサイズは、分割ファイルサイズをセクタサイズで丸めればわかります。セクタサイズはWindowsのAPIのGetDiskFreeSpace関数でわかります(この関数はバイト数が2Gを超えられないが、セクタ数は正しく返す)。GetDiskFreeSpace関数はディスクの他の情報も同時に返すので、セクタサイズのみを取得する関数を作った方が便利です。GetDiskFree関数同様、再利用性を考えてTMainWNDのメンバ関数とせずに独立させましょう。

GetSectorSize関数

#include <winbase.h>

        :

//---------------------------------------------------------------------------
int GetSectorSize(int DriveNum)
{
    DWORD SecPerClu, BytePerSec, NumFreeClu, TotalClu;    // GetFreeDiskSpace()関数引数のクラスタ毎のセクタ数、
                                                          // セクタバイト数、空きクラスタ数、総クラスタ数変数の宣言
    char *DriveName = "$:\\";    // ドライブ名作成バッファの確保
    DriveName[0] = (char)('A' + DriveNum - 1);    // ドライブ名作成
    int SecSize;                 // 戻り値用セクタサイズ変数宣言(APIではDWORD、戻り値はintと型宣言が異なるため

    int Result = GetDiskFreeSpace(DriveName, &SecPerClu, &BytePerSec, &NumFreeClu, &TotalClu);    // API関数で空き情報取得
    if (Result) SecSize = (int)BytePerSec;    // API関数成功の場合はセクタサイズ設定
    else        SecSize = -1;                 // エラーの場合は-1を設定

    return(SecSize);    // セクタサイズを返す
}
//---------------------------------------------------------------------------

書き込みエラーは?

書き込みエラーはGoDivideMNUClick関数で実際に書き込みを行っているfwrite()関数の戻り値を見ればわかります。この関数は実際に書き込まれたデータの個数を返しますので、書き込んだ個数(1個)に対して書き込まれた個数が0個の場合はエラーとします。
               :
if (fwrite(buffer, サイズ, 1, DstFP)!=1){
    エラー処理;
}
               :


分割元は大丈夫?

分割先ばかりが気になりましたが、ここでちょっと第5話の「異常な操作をしてみる」を思い出してください。「寿分割では分割元ファイルに存在しないファイルを指定したり、データサイズが非常に小さい、もしくはサイズ0のファイルを分割させてみましょう。」と書きました。当然異常な動作をするはずです。これに対する処理を加えましょう。また分割先と同様に読み込みエラーも処理しましょう。

存在しないファイルを指定

第3話のファイルオープンダイアログ設計で、 と設計しました。これにより、存在しないファイルを指定することは既にできません。しかし、ファイルを指定した後、そのファイルが削除されていたらどうなるでしょうか? 「そんなこと考える必要ないよ」と思われるかもしれませんが、ユーザーの勘違いやネットワーク上のファイル、プログラムが勝手に削除してしまう作業ファイル等で、分割実行時にファイルが存在しなくなっているかもしれません。この場合は、ファイルを開いた時にエラーチェックを入れます。
    :
FILE *fp = fopen(分割元ファイル, "rb");
if (fp==NULL){
    エラー処理;
}
    :
定石と言えば定石ですね。

データサイズが小さい

分割元ファイルサイズが分割サイズに満たない場合、当然分割を実行できなくすれば済みます。この処理としては、 の2通りが考えられます。さてどちらが良いのでしょう? ここは私も迷うところです。私の好みとしてはあまりメッセージを表示したくありません。分割ファイル一覧に分割元ファイルを表示し、結合バッチファイルを作らないようにしましょう。その上で次項のUpdateMenu関数を使って分割できないようにしたいと思います。
まずListDivideFiles関数を改造して、分割ファイルが1つの場合は結合バッチファイルを作成しないようにします。

改造 ListDivideFiles関数

//---------------------------------------------------------------------------
bool __fastcall TMainWND::ListDivideFiles(AnsiString FilePath)
{
// 分割先ファイル一覧をクリア
    :

// 分割元ファイルサイズの取得(ファイルサイズは分割残りサイズで更新される)
    :

// 分割サイズの取得
    :

// 分割数の計算(分割ファイル数、結合バッチファイルは除く)
    int NumCut   = FileSize/CutSize+1;    // 第3話で作成済み

// 分割先ファイル一覧作成(分割ファイル名作成と分割サイズの設定)
    :

// 分割先ファイルが1つの場合は結合バッチコマンドを作成しない
    if (NumCut==1) return(true);    // 今回改造

// 結合バッチフコマンド作成
    :

// 結合バッチファイルサイズ設定
    :

// 結合バッチファイル情報設定
    :

// 成功を返す
    return(true);    // 第3話で作成済み
}
//---------------------------------------------------------------------------

分割をできなくするのはUpdateMenu関数に任せましょう。

読み込みエラーは?

読み込みエラーは書き込みエラー同様、GoDivideMNUClick関数で実際に読み込みを行っているfread関数の戻り値を見ればわかります。この関数はfwrite関数同様に実際に読み込まれたデータの個数を返しますので、読み込んだ個数(1個)に対して読み込まれた個数が0個の場合はエラーとします。
               :
if (fread(buffer, サイズ, 1, SrcFP)!=1){
    エラー処理;
}
               :

以上を寿分割に実装します。分割前に固定ディスクに対する書き込みチェックCheckDivide2Fix関数の作成と、分割実行中に交換可能メディアに対する処理をしましょう。

改造GoDivideMNUClick関数


//---------------------------------------------------------------------------
void __fastcall TMainWND::GoDivideMNUClick(TObject *Sender)
{
// 分割実行チェック
    AnsiString DrivePath = StatusBAR->SimpleText.SubString(1, 3);    // ドライブパス取得
    bool Removable = (GetDriveType(DrivePath.c_str())==DRIVE_REMOVABLE);
    if (!Removable){                                        // 固定ディスク
        if (!CheckDivide2Fix(DrivePath)) return;            // 分割中断
    }
        :

// 分割バッファ準備
        :

// 分割元ファイル読込みオープン
    FILE *SrcFP = fopen(OpenDLG->FileName.c_str(), "rb");
    if (SrcFP==NULL){    // オープンエラー
        Application->MessageBox("分割元ファイルを開けません", "分割実行", MB_ICONEXCLAMATION);
        delete buffer;
        return;
    }

// 分割数繰り返し(結合バッチファイルは除く)
    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();                      // 分割サイズ取得

        int DriveNum = (int)DrivePath[1] - 'A' + 1;     // ドライブ番号取得
        while(GetDiskFree(DriveNum)<(double)CutSize){       // 空き容量確保ループ
            if (Removable){                             // 交換可能メディア
                int okcancel = Application->MessageBox("ディスクを交換してください", "分割実行", MB_OKCANCEL);
                if (okcancel==IDCANCEL){        // 交換中断
                    delete buffer;              // 分割バッファ開放
                    fclose(SrcFP);              // 分割先ファイルクローズ
                    return;
                 }
            }
            else{                           // 固定メディア
                Application->MessageBox("空き容量が不足しています", "分割実行", MB_ICONEXCLAMATION);
                delete buffer;              // 分割バッファ開放
                fclose(SrcFP);              // 分割先ファイルクローズ
                return;
            }
        }

    // 分割元ファイル読込み
        if (fread(buffer, CutSize, 1, SrcFP)!=1){           // 分割サイズ分読込み
            Application->MessageBox("分割元ファイルを読み込めません", "分割実行", MB_ICONEXCLAMATION);
            delete buffer;              // 分割バッファ開放
            fclose(SrcFP);              // 分割先ファイルクローズ
            return;
        }

    // 分割先ファイル作成
        AnsiString DstFilePath = StatusBAR->SimpleText;    // 分割先ファイルパス取得
        if (DstFilePath[DstFilePath.Length()]!='\\'){      // パス区切り文字追加
            DstFilePath = DstFilePath + "\\";
        }
        DstFilePath = DstFilePath + Item->Caption;         // 分割先ファイル名作成
        FILE *DstFP = fopen(DstFilePath.c_str(), "wb");    // 分割先ファイル作成オープン
        if (DstFP==NULL){
            Application->MessageBox("分割先ファイルを開けません", "分割実行", MB_ICONEXCLAMATION);
            delete buffer;              // 分割バッファ開放
            fclose(SrcFP);              // 分割先ファイルクローズ
            return;
        }
        if (fwrite(buffer, CutSize, 1, DstFP)!=1){         // 分割先へ分割サイズ分書込み
            Application->MessageBox("分割先ファイルに書き込めません", "分割実行", MB_ICONEXCLAMATION);
            fclose(DstFP);              // 分割先ファイルクローズ
            delete buffer;              // 分割バッファ開放
            fclose(SrcFP);              // 分割先ファイルクローズ
            return;
        }
        fclose(DstFP);                                     // 分割先ファイルクローズ
    }

// 分割元ファイルクローズ
        :

// 分割バッファ開放(開放タイミングを移動)
    delete buffer;

// 結合バッチコマンド出力
    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");        // 結合バッチファイル作成オープン
    if (DstFP==NULL){
        Application->MessageBox("結合バッチファイルを作成できません", "分割実行", MB_ICONEXCLAMATION);
        return;
    }
    if (fwrite(ConnectBatch.c_str(), ConnectBatch.Length(), 1, DstFP)!=1){   // 結合バッチコマンド書込み
        Application->MessageBox("結合バッチファイルに書き込めません", "分割実行", MB_ICONEXCLAMATION);
        fclose(DstFP);                                     // 結合バッチファイルクローズ
        return;
    }

    fclose(DstFP);                                         // 結合バッチファイルクローズ
}
//---------------------------------------------------------------------------

CheckDivide2Fix関数

//---------------------------------------------------------------------------
bool __fastcall TMainWND::CheckDivide2Fix(AnsiString DrivePath)
{
// セクタサイズ取得
    int DriveNum = (int)DrivePath[1] - 'A' + 1;     // ドライブ番号取得
    int SectorSize = GetSectorSize(DriveNum);       //  セクタサイズ取得

// セクタサイズを考慮した空き容量チェック
    AnsiString ConnectBatch = MakeConnectBatch(OpenDLG->FileName.c_str(), ListVEW->Items->Count-1);
    int RequireSize = (int)((ConnectBatch.Length()-1)/SectorSize+1)*SectorSize;
                                                                // 結合バッチテキストサイズ
    for (int i=0;i<ListVEW->Items->Count;i++){                  // 分割ファイルサイズ加算ループ
        int ACutSize = (int)ListVEW->Items->Item[i]->Data;      // 各分割ファイルサイズ
        ACutSize = (int)((ACutSize-1)/SectorSize+1)*SectorSize;
        RequireSize += ACutSize;                                // 分割ファイルサイズ加算
    }
    if (GetDiskFree(DriveNum)<(double)RequireSize){             // 空き容量チェック
        Application->MessageBox("ドライブの空き容量が足りません",
                                "分割実行", MB_ICONEXCLAMATION | MB_OK);    // 容量不足
        return(false);
    }

// 分割実行
    return(true);
}
//---------------------------------------------------------------------------


寿の定石「UpdateMenu」登場

メニューやツールバー、ボタン等では、実行できないコントロールはグレー表示にすることが一般的です。メニュー等で実行したのに「それは現在できません」ではあまりにも不親切です。メニュー等の有効・無効はアプリケーションの現在の状況で設定します。私はいつもUpdateMenu関数を作って、ここで一元管理しています。
寿分割では、分割先パス、分割実行メニューは、
  1. 分割ファイルが読み込まれている。
  2. 分割先ファイル数が2つ以上ある。
の条件を満たす場合のみ有効です。1の条件は2の条件が満たされていれば満足するので、「分割先ファイル数が2つ以上ある」場合のみこれらのメニューを有効にします。

UpdateMenu関数

//---------------------------------------------------------------------------
void __fastcall TMainWND::UpdateMenu()
{
    PathMNU->Enabled = (ListVEW->Items->Count>2);     // 分割先メニューは分割先ファイル数が2つ以上で有効
    GoDivideMNU->Enabled = (ListVEW->Items->Count>2); // 分割実行メニューは分割先ファイル数が2つ以上で有効
}
//---------------------------------------------------------------------------
この処理を必要な場所にばら撒きます。メニュー更新が必要なのは、
  1. 寿分割を起動した時。
  2. 分割ファイルを読み込んだ時。
  3. 分割ファイルを新規作成した時。
  4. 分割サイズを変更した時。
です。特に分割サイズを変更した時、720キロバイトなら分割ファイルが2つでも、1.44メガバイトなら分割ファイルが1つになってしまう可能性があります。この時に分割を実行できないように設定することを忘れては行けません。寿分割が起動した時は、メインフォームMainWNDのOnCreateコールバック関数を追加し、ここにUpdateMenu関数を追加します。オブジェクトインスペクタでMainWNDを選び、イベントのOnCreateの関数名でダブルクリックします。FormCreate関数が追加され、エディタに切り替わりますので、この関数からUpdateMenu関数を呼び出します。

寿分割を起動した時

//---------------------------------------------------------------------------
void __fastcall TMainWND::FormCreate(TObject *Sender)
{
    UpdateMenu();    // メニュー更新
}
//---------------------------------------------------------------------------

その他は、既に作成したコールバック関数LoadMNUClick関数、NewMNUClick関数、SizeMNUClick関数の最後でUpdateMenu関数を呼び出します。

以上、寿分割は若干強くなったと思います。でもまだまだ便利とは言い難いですね。分割実行の進捗度を表示できるようにして、さらに強くて便利なアクセサリに仕上げていきましょう。

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


次回は実際のマルチスレッドファイル分割を実現します。寿分割の心臓部を大改造します。マルチスレッドは少々面倒ですが、実装できれば使いやすさは飛躍的に向上します。お楽しみに。

今回のプログラミング行数: 58行
今回までのプログラミング行数: 252行


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