前回書いた問題の原因は未だに分かりません。

先ずString.IsNullOrEmptyをすり抜けるには空行("")かnull(注1)以外の何かが残っている可能性を考えました。(注2)

 

注1:空行(Empty)とnullの違い参照

注2:Renamerでは最後にWindowsの改行コード(0x0D0A-\r\n)を行末に入れていますが、ASCIIだけのファイルだとファイルがShift-JISなのかUTF-8なのかわからず、例えば0X0Dが残る可能性を考えてみました。その場合は表見上何もないのですが、コードが一つ残っているのでIsNullOrEmptyメソッドをすり抜ける可能性があります。

 

この場合、想定するデータ行(2つの値がコンマで区切られるCSVフォーマット)を','でSplitしようとすると、Splitはエラーが出ないように設計されているようなので、最初の値にそのコードが入り、次の値にnullが入ることになりそうですが、その場合に「インデックスが配列の境界外」や「値をnullにはできません」エラーになるのかもしれない、と考えました。

 

となると、もうエラーが発生する可能性を払しょくすることができないので、「エラーが発生してもよいようにコーディング」するしかありません。

 

そこで...昨日書いたコードを以下のように書き直してみました。

 

            //ログファイル(各行がカンマ区切り2つの文字列のcsvファイル)による置換処理
            Encoding enc = Encoding.GetEncoding("shift_jis");    //Sift-JISを選択しているが、変更可能
            //ログファイル(File.ReadAllText(tssl[2].Textはファイル名)を読む
            StringReader sr = new StringReader(File.ReadAllText(tssl[2].Text, enc));
            string line = string.Empty;            //一行データ読み込み用変数
            while(true)                            //無限ループ
            {
                line = sr.ReadLine();
                if(String.IsNullOrEmpty(line))    //文末チェック
                    break;
                else                            //行データがある場合
                {
                    try                            //改行コード(0X0D0A)変換余りでデータ行以外がすり抜ける場合
                    {

                        //line.Contains(',')で「値がnul」エラーが生じるリスク
                        if(!line.Contains(','))    //データ行か否かのチェック(カンマが無ければ終了)
                            break;
                        //「Indexが配列境界外」エラーリスク対処
                        string[] words = line.Split(',');
                        RepFilesInLV((tbToRep.Text = words[0]), (tbRepWith.Text = words[1]));
                    }
                    catch 
   //エラー発生時はループを抜け出す
                    {
                        break;
                    }

                }
            }
            sr.Close();
 

これで昨日エラーが出たサンプルでテストすると今のところ何もエラーで止まることがないようですし、結果もちゃんと変換されているようです。

 

取り敢えず、これでここは我慢してRenamerの最終回へ向かおうか、と考えています。

 

最後のRenamerのProc.hファイルの解説が滞っており、申し訳なく存じます。

 

まぁ、取り敢えずは次のコードをご覧ください。

 

            //ログファイル(各行がカンマ区切り2つの文字列のcsvファイル)による置換処理
            Encoding enc = Encoding.GetEncoding("shift_jis");    //Sift-JISを選択しているが、変更可能
            //ログファイル(File.ReadAllText(tssl[2].Textはファイル名)を読む
            StringReader sr = new StringReader(File.ReadAllText(tssl[2].Text, enc));  //tssl[2].Textはファイル名

            string line = string.Empty;   //一行データ読み込み用変数
            while(true)    //無限ループ
            {
                line = sr.ReadLine();
                MessageBox.Show(line, "line");    //デバッグ用です
                if(String.IsNullOrEmpty(line))    //文末チェック

                //この条件式だと何故かtrueにならず、すり抜けて↓でエラーとなる

           //   if( !line.Contains(','))    //文末チェック
                //この条件式だとここで「値をnullにすることはできません」エラーとなる

                {
                    MessageBox.Show("lineが空です", "lineチェック");    //デバッグ用です
                    break;    //無限ループを抜ける
                }
                string[] words = line.Split(',');

                //↑ですり抜けられると空行のSplit処理になる為か、「インデックスが配列の境界外です」エラーとなる

                if(String.IsNullOrEmpty(words[0]) || String.IsNullOrEmpty(words[1]))    //行末チェック

                {
                    MessageBox.Show(words[0], "words[0]");    //デバッグ用です
                    MessageBox.Show(words[1], "words[1]");    //デバッグ用です

                    break;   //無限ループを抜ける
                }
                RepFilesInLV((tbToRep.Text = words[0]), (tbRepWith.Text = words[1]));
            }
            sr.Close();

 

Renamerの出すログファイルをもとに、テキストファイルの文字列変換を一括して行うReplacerをC#で書いており、「ほぼ完成」という段階になりましたが、最後の処理として「(検索して置換される文字列),(置換する文字列)\r\n」というCSV形式のlogファイルを読み込んで、自動的に複数の「(検索して置換される文字列)」と「(置換する文字列)」の置換処理を書いているのが↑のコードです。

 

要すれば、(最初はShift-JISにしてますが)logファイルを読み込み、Split(',')メソッドで「(検索して置換される文字列)」と「(置換する文字列)」を文字列配列にして置換処理(RepFilesInLV関数)を行う処理となっています。

 

logファイルは単純な"文字列1,文字列2\r\n"という形式であり、String.ReadLine()メソッドで一行読み込むと最後の"\r\n"がとられてstring[]配列で[0]が文字列1、[1]が文字列2になるはずでした。しかし、どういうわけか読み込む一行をString.IsNullOrEmpty()でチェックしても最終行がすり抜けてしまうので、苦肉の策で「一行の中に','が無ければ」という条件式にしようとString.Contains(',')メソッドでチェックしてもエラーが出ます。これは対象行がnullだけの文字列では

Contains()メソッドがエラーになるということのようです。

 

いずれにせよ、思いもよらぬ簡単な処理で手ごわいエラーにぶち当たり、心が折れそうになっていますので、現在Renamerの最終回を休筆しております。まだエラー原因の解析、対処方法の立案のめどが立たない現状であり、もう少しお時間がかかります。

 

ご容赦ください。m_(__)_m

前回リソースをやりましたので、今回はいつもほとんど手を付けない.cppファイルとクラス定義の.hファイル、加えて置換ダイア老処理に共通な関数を外部関数にしたのでUser.hを言追加していますので、それらをサクッとやりましょう。

なお、以下はBCC2ECCツールで処理した後のUnicode版です。

 

【Renamer.cpp】

//////////////////////////////////////////
// Renamer.cpp
//Copyright (c) 05/06/2020 by ECCSkelton
//////////////////////////////////////////
#include    "Renamer.h"
#include    "User.h"
#include    "RenamerProc.h"

////////////////
// WinMain関数
////////////////
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPWSTR lpCmdLine, int nCmdShow) {

    //2重起動防止
    if(!Renamer.IsOnlyOne()) {
        //ここに2重起動時の処理を書く(下記は1例)
        HWND hWnd = FindWindow(L"MainWnd", L"Renamer");
        if(IsIconic(hWnd))
            ShowWindow(hWnd, SW_RESTORE);
        SetForegroundWindow(hWnd);
        return 0L;
    }


    //モードレスダイアログを作成Create(hParent, DlgName, DlgProc);
    if(!Renamer.Create(NULL, hInstance, L"IDD_MAIN"))
        return 0L;

    //メッセージループに入る
    return Renamer.Loop();
}
 

基本SkeltonWizardが作る「ダイアログベースのスケルトン」通りです。自分で作るUser.hファイルはそこで書かれる定義や関数を参照する記述の前に書かないとならないので、通常(BCCSkeltonやECCSkeltonの定義を読む).hファイルの後でProc.hの前が一般的です。なお、一遍にいくつも使いたい方は「2重起動防止」の所を削除して下さい。

 

【Renamer.h】

//////////////////////////////////////////
// Renamer.h
// Copyright (c) 05/06/2020 by ECCSkelton
//////////////////////////////////////////
//ECCSkeltonのヘッダー-これに必要なヘッダーが入っている
#include    "ECCSkelton.h"
//リソースIDのヘッダー
#include    "ResRenamer.h"

/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCDLGクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////
class CMyWnd : public CDLG
{
public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(WCHAR* UName) : CDLG(UName) {}
    //メニュー項目、ダイアログコントロール関連
    bool OnSelect();
    bool OnListBox(WPARAM);
    bool OnReplace();
    bool OnSerial();
    bool OnHelp();
    bool OnExit();
    bool OnDel();
    bool OnAdd();
    //ウィンドウメッセージ関連
    CMDTABLE        //OnCommand()関数宣言
    bool OnInit(WPARAM, LPARAM);
    bool OnDropFiles(WPARAM, LPARAM);
    bool OnClose(WPARAM, LPARAM);
    bool OnDestroy(WPARAM, LPARAM);
    //ユーザー定義関数
    WCHAR** GetPathName(WCHAR*);
    void RenewListBox();
    bool MakeOrgFileList(WCHAR*);
    bool CheckList();

};
//(解説):スケルトン部分では特記すべき事項はありません。「ユーザー定義関数」についてはProc.hファイルの解説で説明します。


////////////////////////////////////////////////////////////////////////
//派生させたCMyWndクラスのインスタンスとコールバック関数(マクロ)の作成
//主ウィンドウはダイアログと違い、コールバック関数は一つしか作れない
////////////////////////////////////////////////////////////////////////
CMyWnd Renamer(L"Renamer");    //ウィンドウクラスインスタンスの生成

BEGIN_CMDTABLE(CMyWnd)    //クラス名がCMyWndではない場合、クラス名、テーブルを適宜マニュアルで修正してください
    //メニュー項目、ダイアログコントロール関連
    ON(IDC_SELECT, OnSelect())
    ON(IDC_LISTBOX, OnListBox(wParam))
    ON(IDC_REPLACE, OnReplace())
    ON(IDC_SERIAL, OnSerial())
    ON(IDC_HELP, OnHelp())
    ON(IDOK, OnExit())
    ON(IDM_DEL, OnDel())
    ON(IDM_ADD, OnAdd())
END_CMDTABLE

///////////////////////////////////////////
// CDLGクラスからSUBDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class SUBDLG : public CDLG {
public:
    //メニュー項目、ダイアログコントロール関連
  
 virtual bool OnPreview();
    bool OnOk();
    bool OnCancel();
    //ウィンドウメッセージ関連
    CMDTABLE        //OnCommand()関数宣言
    
virtual bool OnInit(WPARAM, LPARAM);
    //ユーザー定義関数
    WCHAR* InstrRep(WCHAR*, WCHAR*, WCHAR*, WCHAR*);
};

////////////////////////////////////////////////////////////////////////////
// SUBDLGクラスダイアログ変数の生成とそのコールバック関数(マクロ)を定義
// 複数同一クラスのダイアログを作成することを予期してコールバック関数を明記
////////////////////////////////////////////////////////////////////////////
SUBDLG subdlg;

BEGIN_CMDTABLE(SUBDLG)    //クラス名がCMyWndではない場合、クラス名、テーブルを適宜マニュアルで修正してください
    //メニュー項目、ダイアログコントロール関連
    ON(IDC_PREVIEW, OnPreview())
    ON(IDOK, OnOk())
    ON(IDCANCEL, OnCancel())
END_CMDTABLE
//(解説):これが「文字列の置換」によるファイル名変換を行うダイアログです。いつも通りCDLGクラスから派生させて作りました。いつもと違う所は、WM_INITDIALOGメッセージの際のOnInit関数と「事前確認」ボタンを押した際のOnPreview関数は、再定義(override)ができるように仮想(
virtual関数にしたことです。なお、「ユーザー定義関数」はProc.hファイルで説明します。

///////////////////////////////////////////
// CDLGクラスからSUB2DLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class SUB2DLG : public
SUBDLG {
public:
    //ウィンドウメッセージ関連
    bool
OnPreview();
    bool
OnInit(WPARAM, LPARAM);
    //ユーザー定義関数
};

////////////////////////////////////////////////////////////////////////////
// SUB2DLGクラスダイアログ変数の生成とそのコールバック関数(マクロ)を定義
// 複数同一クラスのダイアログを作成することを予期してコールバック関数を明記
////////////////////////////////////////////////////////////////////////////
SUB2DLG sub2dlg;

//(解説):これが「同名連番付け」ボタンを押した際に使う「二股ダイアログ」のラッパーであるSUB2DLGクラスです。まず派生元が「文字列の置換」ダイアログ(SUBDLG)であることで、virtualを付けたOnInitOnPreview関数以外はそのまま承継され、コマンドテーブルやIDOK、IDCANCELボタンを押した際の処理は全く同じなので記述がありません。逆にこれら二つの関数は再定義(override)を行う必要があり、宣言しなおしています。なお、実装内容はProc.hファイルで解説します。

 

【User.h】

//////////////////////////////////////////
// User.h
// Renamerで使用する外部変数と外部関数

// Copyright (c) 05/06/2020 by ECCSkelton
//////////////////////////////////////////
//(解説):↑の通りです。
 

///////////////////////
//コモンダイアログ関連
///////////////////////
CMNDLG g_CmnDlg;
CSTR g_Path;        //取得パス名
CTTIP g_Tip;
//(解説):順に、Renamerで使うコモンダイアログ用、パス名の保存用、パスのツールチップ用のインスタンスを宣言しています。

///////////////////////////
//ファイル名記録用CSTR変数
///////////////////////////
CSTR g_OldFiles, g_NewFiles, g_OrgFiles;
//(解説):この3つのCSTRファイルの文字列データは、順に「処理用に使うg_OrgFilesのコピー」、「ファイル名変換後」、「変換前のオリジナルファイル名」のファイルパス名がが引用符付き("")のコンマ区切り(Comma-Separated Value-CSVファイルですね)で並んでいます。フォーマットは以下の通りです。

"旧ファイルパス・名1","新ファイルパス・名1"(CRLF)
"旧ファイルパス・名2","新ファイルパス・名2"(CRLF)

"旧ファイルパス・名3","新ファイルパス・名3"(CRLF)

"旧ファイルパス・名N","新ファイルパス・名N"(CRLF)

/////////////////////////////
//ファイルパス記録用CSTR変数
/////////////////////////////
CSTR g_HelpFile;
CSTR g_LogPath;
//(解説):文字通り、ヘルプファイルのパス、ファイル名と自分自身のパスの保存用です。

///////////////////////////////////////
//外部関数 ListFiles
//CSTRのカンマ区切りファイル名をリスト
//ボックスに表示すると共に、ID(LISTBOX)
//のフォント当りのピクセルを調べて水平
//スクロールバーをセットする
///////////////////////////////////////
//(解説):↑の通りです。

void ListFiles(HWND hWnd, int ID, CSTR* files) {

    //変数リスト
    SIZE size;                                            //ListBoxの表示文字列サイズ
    int len = 0, max = 0;                                //文字列の長さ
    HWND hLB = GetDlgItem(hWnd, ID);                    //リストボックスのハンドル
    CSTR Name, List(*files), MaxStr;                    //ファイル名、リストデータ、最長名
    //リストボックス内掃除
    int i = SendMessage(hLB, LB_GETCOUNT, 0, 0);
    if(i > 0) {
        for(--i; i >= 0; i--)
            SendMessage(hLB, LB_DELETESTRING, (WPARAM)i, 0);
    }
    //変更予定ファイル名のリストボックス登録(解説:同時に最長文字列をチェックしています。)
    for(int i = 0; List.Next(Name); i++) {
        len  = lstrlenW(Name.ToChar());
        if(len > max) {
            max = len;
            MaxStr = Name;
        }
        SendMessage(hLB, LB_INSERTSTRING, i, (LPARAM)Name.ToChar());
    }
    //IDC_LISTBOXの水平スクロールバーを利用可能にする
    HDC hDC = GetDC(hLB);                                //リストボックスのDC取得
    GetTextExtentPointW(hDC, MaxStr.ToChar(), max, &size);
    ReleaseDC(hLB, hDC);                                //取得したDCの開放
    SendMessage(hLB, LB_SETHORIZONTALEXTENT, size.cx, (LPARAM)0);
}

///////////////////////////////////////
//外部関数 RenameFile
//g_OrgFilesのカンマ区切りファイル名を
//g_NewFilesのカンマ区切りファイル名に
//変更する
//戻り値:一つでも失敗があればFALSE)
//"RenameLog(MM-DD-YYYY-HH-MM).log"と
//いうログファイルをプログラムの下の
//RemaneLogフォールダーに残す。
///////////////////////////////////////
//(解説):↑の通りです。

bool RenameFiles() {

    bool result = TRUE;
    //本日のローカル日付をログファイル(log)のfilename文字列に入れる
    CSTR log = L"";
    SYSTEMTIME st;
    GetLocalTime(&st);
    WCHAR fn[36];
    wsprintfW(fn, L"RenameLog(%02d-%02d-%04d-%02d-%02d-%02d).log", st.wMonth, st.wDay, st.wYear, st.wHour, st.wMinute, st.wSecond);
    //ファイル名変更実行
    g_OldFiles = g_OrgFiles;        //g_OrgFilesのデータを変更しないようにコピーのg_OldFilesを使う
    CSTR filename, buff1, buff2;
    while(g_OldFiles.Next(filename)) {
        buff1 = g_Path + L"\\";
        buff1 = buff1 + filename;
        g_NewFiles.Next(filename);    //最終的にg_NewFilesのデータは空になる
        buff2 = g_Path + L"\\";
        buff2 = buff2 + filename;
        if(MoveFileW(buff1.ToChar(), buff2.ToChar())) {    //'\"'で括るとエラーになるので注意
            log = log + L"\"" + buff1 + L"\",\"" + buff2 + L"\"\r\n";
        }
        else {
            result = FALSE;
        }
    }
    //ログを保存する(後にバッチファイルとして使える)
    filename = g_LogPath + fn;
    log.ToFile(filename.ToChar());
    return result;
}

//(解説):特記事項としてはMoveFileW関数は引数のファイルパス、名に引用符付き(L"")を渡すとエラーになる、ということですね。

 

以上の2関数は「文字列の置換による変換」「同一ファイル名に連番をつける変換」の両方のダイアログから呼ばれるのでクラスのメンバー関数にせず、外部関数にしました。

 

ECCSkeltonのプログラミング作法は、前にGraphMakerでここから7回に亘り詳述しました。要点だけ言うと、

(1)必要なリソースを作成する。

(2)BCCFormで(Project名).rc、Res(Project名).hファイルを作成する。

(3)SkeltonWizardツールで、ANSI版の(Project名).h、(Project名)Proc.hおよび(Project名).cppファイルを作成する。

(4)BCC2ECCツールでANSI版をUnicode版に変換。

(5)(Project名).h、(Project名)Proc.hおよび(Project名).cppファイルを変更、修正してゆく。

という流れです。

 

今回のRenamerは、BCCSkeltonでサンプルがありましたが、ファイル名変更方法を大幅に変えるので旧Renamer資産を生かしながらも、リソースの再設計を行う必要が出てきました。

【旧版】

旧版はフォールダーを選択し、その中のファイルを列挙して、変換対象文字(検索文字列)と変換文字(置換文字列)を指定して事前確認して問題が無ければ変換を実行してプログラムが終わる仕様です。

 

新版は同様にフォールダーを選択し、その中のファイルを列挙しますが、変換方法が「文字列の置換」と「同名連番付け」の二種類となります。このため、変換方法に応じたダイアログを作りました。

 

現実に二つのダイアログを作りましたが、二つがあまりに似ているので、これで二つもダイアログを作ると沽券にかかわる、ということで一つに統一することにしました。

 

【Renamer.rc】(ResRenamer.hは省略-なお、以下のファイルはUnicode版の最終形です。(注))

//-----------------------------------------
//             BCCForm Ver 2.41
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------
#include    "ResRenamer.h"

//----------------------------------
// ダイアログ (IDD_MAIN)
//----------------------------------
IDD_MAIN DIALOG DISCARDABLE 0, 0, 266, 146
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_DLGFRAME | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_SETFONT | DS_CENTER
CAPTION L"Renamer"
FONT 9, L"MS 明朝"
{
 CONTROL L"選択", IDC_SELECT, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 230, 14, 32, 14
 CONTROL L"文字列の置換", IDC_REPLACE, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 4, 122, 80, 16
 CONTROL L"同名連番付け", IDC_SERIAL, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 92, 122, 80, 16
 CONTROL L"終了", IDOK, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 178, 122, 80, 16
 CONTROL L"Help", IDC_HELP, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 232, 0, 28, 12
 CONTROL L"", IDC_PATH, L"EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | ES_AUTOHSCROLL | ES_READONLY | ES_LEFT, 4, 14, 224, 14, WS_EX_CLIENTEDGE
 CONTROL L"", IDC_LISTBOX, L"LISTBOX", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | LBS_NOTIFY | LBS_SORT, 4, 32, 256, 88, WS_EX_CLIENTEDGE
 CONTROL L"フォールダー選択", 0, L"STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 6, 2, 80, 10
}

//----------------------------------
// ダイアログ (IDD_SUB)
//----------------------------------
IDD_SUB DIALOG DISCARDABLE 0, 0, 266, 142
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_DLGFRAME | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | DS_SETFONT | DS_CENTER
CAPTION L"文字列の置換"
FONT 9, L"MS 明朝"
{
 CONTROL L"", IDC_TOREPLACE, L"EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL | ES_LEFT, 158, 30, 104, 14, WS_EX_CLIENTEDGE
 CONTROL L"", IDC_REPLACEWITH, L"EDIT", WS_CHILD | WS_BORDER | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL | ES_LEFT, 158, 58, 104, 14, WS_EX_CLIENTEDGE
 CONTROL L"大文字小文字を区別", IDC_CHECKUORL, L"BUTTON", WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX | BS_LEFTTEXT, 160, 4, 98, 10
 CONTROL L"事前確認", IDC_PREVIEW, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 160, 92, 102, 16
 CONTROL L"キャンセル", IDCANCEL, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 160, 118, 50, 16
 CONTROL L"変更実行", IDOK, L"BUTTON", WS_CHILD | WS_VISIBLE | WS_TABSTOP | BS_DEFPUSHBUTTON, 212, 118, 50, 16
 CONTROL L"", IDC_LISTBOX, L"LISTBOX", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | LBS_NOTIFY | LBS_SORT, 4, 4, 148, 146, WS_EX_CLIENTEDGE
 CONTROL L"置換対象文字", IDC_LABEL1, L"STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 160, 16, 102, 10
 CONTROL L"置換文字", IDC_LABEL2, L"STATIC", WS_CHILD | WS_VISIBLE | SS_NOTIFY, 160, 46, 102, 10
}

//----------------------
// ポップアップメニュー
//----------------------
IDM_POPUP MENU DISCARDABLE
{
    POPUP L"ポップアップ"
    {
        MENUITEM L"リストからファイルを削除", IDM_DEL
        MENUITEM SEPARATOR
        MENUITEM L"リストへファイルを追加", IDM_ADD
    }
}

//--------------------------
// イメージ(IDI_ICON)
//--------------------------
IDI_ICON    ICON    DISCARDABLE    "Icon.ico"
 

注:実はこのブログを書いていて、このファイルを貼った段階でダイアログの部分だけユニコードの「L""」ではなく、ANSIの「""」になっていることを発見!!!(ポップアップメニューはL""になっていた。)(注)「マジかっ!!」ということでファイルを修正しましたが、不思議に思ったので実験したら、

注(2023年2月27日追記):なぜこんなことが起こったか、その原因を書いていませんでした。「L」抜けの原因は、BCC2ECCが動作しなかったわけではなく、ダイアログを一つにまとめる修正を行う際にBCC2ECCで修正したコードをコピーした後削除し、貼り付ける前に別の文字列をこぷーしてしまったので、私がオリジナルのANSI版BCCFormのものを誤って貼り付けてしまったためです。

 

・私のBCCFormは""でなくて、L""でも読み込みます。

・Embarcaderoのbrc32.exeは、ANSIの""とユニコードのL""が混在しても平気でコンパイルします。(また、コンパイル後の文字列の表示で文字化けもしません。→ANSIとUnicodeを使い分けてコンパイルしているのか、UNICODEフラグが立っているので皆ANSI表記をUnicodeコンパイルしているのか、よくわかりません。)

 

いずれにせよ、今は皆ユニコードに統一していますが...なにか?

 

前回書いた通り、本当は今回からECCSkeltonで書いたRenamerのコード解説を始めるつもりでしたが、最後に触れた

Replacer

について少し説明してから始めましょう。

 

実はReplacerは私の完全オリジナルではないのです。

 

(1)定年退職してC++を再学習し始め、Embarcadero C++ Builderで土をつけた(注)後、またBorland C++ Builder 5.0(BCB5)をいじくっていたら、

注:メモリー問題が生じたことと、フリー期間が切れたこと

 

(2)昔、Borland Delphi 6.0がおまけでついてくるのでフリーで手に入る、ということで買った「基礎からわかるDelphi」という本があり、その中のサンプルプログラム(Delphi、即ちPascalで書かれています)をBCB5に移植してみた。

 

(3)その結果できたのが、BCB5版(注)Replacerだった、というお話です。

注:BCB6相当のDelphi 6なので、BCB5にない機能もありましたが、自作関数で乗り越えました。

 

今回ECCSkelotnのRenamerのログを処理するのですから、(Renamerのリソースやコードが大分再利用できることもあり)本来ECCSkeltonで書くべきでしょうが、このReplacerのオマージュとして書くこともあり、(C#で書いたアプリもまだ数が少ないので)C#でやってみようということでスケルトンだけ作ってみました。

まだまだプロトタイプでバックアップ処理やUIも未定ですが、大分それっぽい感じになってきました。

 

まぁ、どれほど一般的に役に立つツールになるか不明ですが、C#でのファイル処理の学習にはちょうど良いと思います。

 

ps. ということで、次回からはRenamerに戻りましょうね。

前回、新Renamerを完成させ、実際に女房の未整理写真データを整理してみて動作試験すると書きました。

 

二日ほどかけて、

18年間、3,408ファイル、10.3GB」(ゼイゼイ)

の画像データを整理し、イベント毎連番や文字列置換による細分化による整理を行いましたが、随分きれいに整理されました。地味なソフトですが実用性は十分だと思います。

 

ということで、久々に新Renamerの解説を行おうかと思います。BCCSkeltonのサンプルに掲載している初期型との違いと今回のポイントとしては、

 

(1)ダイアログが3つに増えましたが、リソースとしては2枚しか使っていないこと

(2)ファイル変換用のダイアログは従来の「文字列置換型」のクラスを承継した「連番型」にしていること

(3)(結構苦しんだのですが)ユニコードを使ったファイルパス()は空白文字も許しているので引用符("")で括る場合と括らない場合を見極めること

 

でしょうか?

 

注:前にUnicodeは扱わないと書きながら(「ブランク20年間に生じた重荷」)、

 

 

既にWindowsがUTF-16ネイティブであったので、UTF-16をSift-JISに変換する際に生じる'?'で苦しみ(「バグは続くよ、何処までも」シリーズ)、

 

 

 

 

 

ユニコードを学習して、

 

 

BCCSkelton改めECCSkeltonを作り(2022年6月~9月迄の一連の【ECCSkelton】ネタ、および関連するBCC102、BatchGoodネタ等参照。一応RTWEditor等のサンプルやBCC2ECCという変換ツールまで作って)、すべてのバグを取り、完了しています。

 

 

 

 

いずれにしてもこの苦しみを経て、今のECCSkeltonとそのサンプルがあるのです。(この後、2022年終盤からMSCompassとC#の世界へ傾倒して行きますね。)

 

なお、最終仕様としてはやはりログを残すことにしました。出力ログは、

(1)ファイル名変更を実行した際に「"旧ファイルパス・名","新ファイルパス・名"(\r\n)」という行を作成し、それをまとめたテキストファイルを、

(2)"RenameLog(MM-DD-YYYY-HH-MM-SS).log"というファイル名で残す

だけですが、これは現在構想を温めている

Replacer

というツールで使うことを予定しています。

 

さーて、どんなことになるのでしょうか?

色々と無駄話で書いていたECCSkeltonによるUnicode対応の新Renamerですが、

 

 

本日基本的なコーディングが終わり、簡単な動作試験は良好。一応仕様通りの動作をしています。

 

後は使い方のHelpを仕上げて、カミさんのラップトップを使って実際に写真データを整理することで最終動作試験にしようと考えています。

 

ps. 写真データを扱うAlbumのツールに入れようか、とも考えたのですが、Albumの*.albファイルのファイルパス名のデータをツールのRenamerが書き換えると、今度はAlbumがalbファイルで読めなくなります。Renamerは任意のフォールダー内の任意のファイルの名前を変えるので様々なフォールダーにある写真(イメージ)データをまとめたalbファイルとは全く関連性がなく、Renamerのファイル名変更結果をログで残して、albファイルと照合して該当があればalbファイルを変更するような別のツールが必要になりますが、一応ログファイルをの子pすべきか否か、考え中です。(まぁ、人間の仕事と一緒で、手順としては写真データを整理して、それからアルバムに貼る手順を行えば、問題はないのですが、ユーザーは必ずしもそのように行動しませんし、それが予見できるのでプログラマーはその予見可能なユーザー行動に対処しなければならないのが現代の設計義務なんでしょうね。)

 

前回「悩み」を書きましたが、悩んでいても仕方がないし、このブログは本来BCCForm and BCCSkelton(Unicode版ECCSkeltonも含む)のサポートを行う場所なので、C#で作ってもよいのですが、B(E)CCSkeltonでのプログラムはやはり作らないと不味かろう、というのが結論です。

 

そういうわけで、BCCSkeltonのサンプルにある"Renamer"を、ウィンドウズのネイティブなUnicode(UTF-16)対応にして、また従来の「特定文字列の置換による一括変換」に加え、特定フォールダー内全ファイル、またはその一部のファイル(注)に対する「同一ファイル名+連番にする一括変換」ができるようにします。

注:これはドラッグ&ドロップ形式にする予定です。

 

本日リソースをまず決定しました。

【メインダイアログ】

【文字列置換変換ダイアログ】

【同一名連番ダイアログ】

後はコーディングですね。

>>>スミマセン、C#コードをちょっと修正します(翌日)<<<

実は今悩んでいます。

 

先日カミさんのラップトップ(C:ドライブが64GBしかない!)にWin 11を入れ、サブドライブとしてD:に1TBのMicroSDカードを入れたのですが、やっとD:に膨大な写真データを入れられるとなったものの、ファイル名がデジカメやスマホのシステム作成ファイル名なので「任意の複数ファイルに同一の名前を付けて末尾にシリアル番号を振る(例:"私の車(1).jpg"、"私の車(2).jpg"、...)」ソフトを(カミさんも「そう言うソフトがあれば使いたい」といったので)ネタとして思いつきました。

 

で、

 

普段ならすぐに仕様決めに移るのですが、既にSampleBCCSkeltonにANSIベースでRenamerというソフトがある()ので、こいつとの棲み分けをどうするか、なども考えなければならないと考えました。

注:これは2020年にBCCSkeltonを再度使い始めた際に、BCCSkeltonの5つ(Project.rc、ResProject.h、Project.h、ProjectProc.h、Project.cpp)の名を一挙に変える習作として作ったので、あまり出来栄えは良くないし、またANSIベースなので面倒なUnicodeファイルパスだと読み込めない可能性がありました。ということで、実はBCC2ECCを使ってUnicode対応版を既に作ってしまいました。

 

が、

 

ECCSkeltonにせよ、bcc32c.exeコンパイラーは32bitソフトしか作れないし、C++よりもすでに部品が山ほどそろったC#で作る方が簡単(しかし、簡単すぎて面白くもないのだけども...)なこともあり、C#で新たに作ろうかな?とも考え、ちっとも前に進みません。

 

ということで、

 

現在何をしているかというと、

 

(1)C#でファイルを扱ったことがない()ので、「どんな塩梅やろか?」ということで、調査、学習を兼ねて習作を作ってみました。

注:ResWriterとResReaderはファイルの読み書きをしますが、それは特定のファイルを特定の専用メソッドを使っただけで、ファイル一般の処理はしていない、という意味です。なお、今日作ったC#のサンプルのコードを末尾に載せておきます。

 

(2)ECCSkeltonのUnicode対応Renamerを機能アップさせるなら「こうするぜー」というリソースファイルを遊びがてらに作り始め、現在BCCFormで弄っているところです。

 

という「二股」で遊んでおり、まだ決めかねています。

 

どうなるんでしょうか?

 

【Test_FileOpe.cs】

//////////////////////
//C# Test_FileOpe.cs
//////////////////////


using System;
using System.Windows.Forms;
using System.Drawing;
using System.IO;
using System.Linq;                    // EnumerateFiles を使用するのに必要
using System.Text;                  // Encoding.GetEncoding を使用するのに必要

public partial class AppForm : Form
{
    TextBox txtBox;
    Button clrBtn, rdBtn, wrtBtn, extBtn;

    [STAThread]    //ファイルを開く(保存)ダイアログを使うのでこれは必須
    public static void Main()
    {
        AppForm ap = new AppForm();
        Application.Run(ap);
    }

    public AppForm()
    {
        this.Size = new Size(640, 480);
        this.MinimumSize = new Size(320, 190);    //翌日修正箇所
        this.Text = "TextBox Test";
        this.Load += AppForm_Load;
    }
 
    private void AppForm_Load(object sender, EventArgs e)
    {
        //Clearボタン
        clrBtn = new Button();
        clrBtn.Location = new Point(ClientSize.Width - clrBtn.Width - 10, 10);
        clrBtn.Text = "Clear";
        clrBtn.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
        clrBtn.Click += Button1_Click;
        this.Controls.Add(clrBtn);

        //Readボタン
        rdBtn = new Button();
        rdBtn.Location = new Point(ClientSize.Width - rdBtn.Width - 10, clrBtn.Height + 20);
        rdBtn.Text = "Read";
        rdBtn.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
        rdBtn.Click += Button2_Click;
        this.Controls.Add(rdBtn);

        //Writeボタン
        wrtBtn = new Button();
        wrtBtn.Location = new Point(ClientSize.Width - wrtBtn.Width - 10, clrBtn.Height + rdBtn.Height + 30);
        wrtBtn.Text = "Write";
        wrtBtn.Anchor = (AnchorStyles.Top | AnchorStyles.Right);
        wrtBtn.Click += Button3_Click;
        this.Controls.Add(wrtBtn);

        //Exitボタン
        extBtn = new Button();
        extBtn.Location = new Point(ClientSize.Width - extBtn.Width - 10, clrBtn.Height + rdBtn.Height + wrtBtn.Height + 40);

        extBtn.Location = new Point(ClientSize.Width - extBtn.Width - 10, ClientSize.Height - extBtn.Height - 20);    //翌日修正箇所
        extBtn.Text = "Exit";
        extBtn.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);
        extBtn.Click += Button4_Click;
        this.Controls.Add(extBtn);

        //テキストボックス
        txtBox = new TextBox();
        txtBox.Text = GetFiles();
        //位置
        txtBox.Location = new Point(10, 10);
        txtBox.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right);
        //サイズ
        txtBox.Width = ClientSize.Width - clrBtn.Width - 30;
        txtBox.Height = ClientSize.Height - 20;
        //その他
        txtBox.Multiline = true;
        txtBox.ScrollBars = ScrollBars.Both;
        txtBox.WordWrap  = false;
        //FormにTextBoxを追加
        this.Controls.Add(txtBox);
    }

    //終了時処理
    protected override void OnFormClosing(FormClosingEventArgs e)
    {
        base.OnFormClosing(e);
        DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
        if(dr == DialogResult.No)
        {
            e.Cancel = true;
        }
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        //txtBoxをクリアする
        txtBox.Clear();
    }

    private void Button2_Click(object sender, EventArgs e)
    {
        OpenFileDialog ofDlg = new OpenFileDialog();
        //ファイルフィルターの指定
        ofDlg.Filter = "テキストやC#ファイル(*.txt, *.cs, *.resx)|*.txt;*.cs;*.resx|すべてのファイル(*.*)|*.*";
        ofDlg.FilterIndex = 1;
        ofDlg.RestoreDirectory = true;    //初期ディレクトリへ復帰
        ofDlg.CheckFileExists = true;    //ファイルの存在チェック
        ofDlg.CheckPathExists = true;    //ファイルパスの存在チェック
        ofDlg.InitialDirectory = ".";    // デフォルトのフォルダーの指定
        ofDlg.Title = "ファイルを開く";    //ダイアログのタイトルを指定する
        if(ofDlg.ShowDialog() == DialogResult.OK)    //ダイアログを表示する
        {
            //txtBoxにファイルを読む
            txtBox.Text = FileRead(ofDlg.FileName);
        }
        else
        {
            MessageBox.Show("キャンセルされました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
        }
        // オブジェクトを破棄する
        ofDlg.Dispose();
    }

    private void Button3_Click(object sender, EventArgs e)
    {
        //ファイル保存ダイアログによるファイル名取得
        SaveFileDialog sfDlg = new SaveFileDialog();
        sfDlg.Filter = "テキストやC#ファイル(*.txt, *.cs, *.resx)|*.txt;*.cs;*.resx|すべてのファイル(*.*)|*.*";
        sfDlg.FilterIndex = 1;            //ファイルフィルターインデックス指定
        sfDlg.RestoreDirectory = true;    //処理後ディレクトリ復元
        sfDlg.CheckPathExists = true;    //存在しないパスの指定警告
        sfDlg.InitialDirectory = ".";    //デフォルトのフォルダ指定
        sfDlg.Title = "リソースファイルを保存";    //ダイアログタイトル指定
        //ファイル保存ダイアログの表示
        if(sfDlg.ShowDialog() == DialogResult.OK)
        {
            //txtBox内容をファイルに書く
            FileWrite(sfDlg.FileName, txtBox.Text);
        }
        else
        {
            MessageBox.Show("キャンセルされました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            return;
        }
        //sfDlgインスタンスの破棄
        sfDlg.Dispose();
        }

    private void Button4_Click(object sender, EventArgs e)
    {
        //終了する
        Close();
    }

    private string GetFiles()
    {
        string str = Directory.GetCurrentDirectory();
        // カレントフォルダ内のファイル名だけ表示(子以下を含めるならSearchOption.TopDirectoryOnlyの代わりにSearchOption.AllDirectoriesを使う)
         IEnumerable<string> files = Directory.EnumerateFiles(str, "*", SearchOption.TopDirectoryOnly);
        str = "現在のフォールダーは\"" + str + "\"です。\r\nここには\r\n";
        foreach (string file in files)
        {
            str += file + "\r\n";
        }
        str += "というファイルがあります。";
        return str;
    }

    private string FileRead(string fn)
    {
        string text = "";
    /*    出力UTF-8    :Encoding.UTF8;
        出力UTF-16LE:Encoding.Unicode;
        出力S-JIS    :GetEncoding("shift-jis")    */

        using (StreamReader file = new StreamReader(fn, Encoding.UTF8))
        {
            //ファイルを読む
            text = file.ReadToEnd();
            // ファイルを閉じる
            file.Close();
        }
        return text;
    }

    private void FileWrite(string path, string data)
    {
    /*    出力UTF-8    :Encoding.UTF8;
        出力UTF-16LE:Encoding.Unicode;
        出力S-JIS    :GetEncoding("shift-jis")    */

        using (StreamWriter file = new StreamWriter(path, false, Encoding.UTF8))
        {
            //ファイルを書く
            file.Write(data);
            // ファイルを閉じる
            file.Close();
        }
    }
}

/* ご参考-その他ファイル関係メソッド
//フォールダー内のファイルを調べる
Directory.EnumerateFiles(folderpath);
//新規ファイル作成
File.Create(string (filepathname);
//ファイルの削除
File.Delete(filepathname);
//ファイルの移動(ファイル名変更もこれを使う)
File.Move(fromfile, tofile);
//ファイルのコピー
File.Copy(fromfile, tofile);
//ファイルの存在確認
File.Exists(filepathname);
//フォールダー内のフォールダーを調べる
Directory.EnumerateDirectories(folderpath);
//新規フォールダー作成
Directory.CreateDirectory(folderpath);
//フォールダーの削除
Directory.Delete(folderpath);
//フォールダーの存在確認
Directory.Exists(folderpath);
//パス文字列の@-以下が等価となる(エスケープシーケンス\が文字\となる)
string path = "D:\\files\\sample.txt";
string path = @"D:\files\sample.txt";
*/

ps. しっかし、C#って簡単!あまりに簡単すぎて「調べて、試行錯誤する」プログラミングの醍醐味がなくなっちゃいそう。

実はもっと早くやらなくてはならなかったのですが、つい先送りにしていたBCCForm and BCCSkeltonパッケージの問題が二つあります。

 

1.BCCSkeltonのサンプルであるAlbum

ファイル起動(ファイルをドロップしてプログラムを起動する)とドラッグアンドドロップで、GDI+のイメージファイルだけしか受け付けず、肝心のアルバムファイル(*.alb)を開けないこと。

自分で使っていて「紙のアルバム感覚」が気に入っているのですが、自分で作ったアルバムファイル(*.alb)を開くときにいちいち「ファイルを開く」メニューを使わないとならないのが気に障っていました。

対処→拡張子チェックで先ず"alb"ファイルか否かを確認し、そうでなければイメージファイルチェックに移行するようにしました。

 

2.ECCSkeltonのサンプルであるRTWEditor

これも「ファイルを開く」では「全てのファイル(*.*)」を選択すればどのようなファイルでも開けるのですが、ファイル起動とドラッグアンドドロップでは「全てのファイル(*.*)」が開けず(注)、エラーメッセ―ジで「対象外ファイル」と切って捨てられること。

注:CEXTCHKクラスインスタンスによるファイル拡張子チェックは、MS-DOSのワイルドカード(*と?)に対応していないので厳密に".*"ファイルiか否かチェックしてしまいます。なお、ワイルドカード(*、?と[)が使える文字列比較関数については末尾参照

対処→拡張子チェックのエラーメッセ―ジで「対象外ファイル」を切り捨てず、MB_YESNOを使って「対象外ファイルですが、開きますか?」として開けるようにしました。

 

プログラミング的には大したことではないので共に(所謂)"Proc.h"のみを修正し、本日アップしておきました。(少なくともこれで大分私の「イラッ💢と来る(注)ストレス」は軽減されることになるでしょう。)

注:本日調べたらこの「イラッ、怒り」コード(U+1F4A2)は存在していたのでこれを使ってみました!

 

ps. 昔々の大昔に勉強したC言語のMS-DOSワイルドカード等に対応する文字列比較関数のサンプルを載せます。今風で言えばintを使っていますが、"bool"を返す関数になっていますね。参考になるかしら?

 

int strcmp2(char* str1, char* str2) {

    while(*str1) {           /* ptr is not NULL */
        switch(*str1) {
        case '?':                /* ? does match with any characters and nothing to do here */
                break;
        case '[': {                /* "[(-)]" matches with a group of characters */
                int found;        /* A flag for a case where they match */
                for(found = 0, str1++; *str1 && *str1 != ']'; str1++) {    /* Until EOS or ']' */
                    if(*str1 == *str2)
                        found = 1;
                    else if(str1[1] == '-' && *str1 <= *str2 && *str2 <= str1[2]) {    /* No space or tab allowed */
                        found = 1;
                        str1 += 2;    /* Skipping "-?" */
                    }
                }
                if(!found)
                    return 0;
                else    break;
        }
        case '*':    /* '*' matches with any characters */
                for(str1++; *str2; str2++)    /* Skipping '*' amd compare with the rest of str2 till EOS of str2*/
                    if(match2(str1, str2)) return 1;
                return match2(str1, str2);    /* Even if both are EOS, return 1 */ 
        case '\\':
                str1++;            /* Skipping '\' which protect reserved characters, e.g. "\*" */
        default:
                if(*str1 != *str2)    /* If they don't match or str is NULL, then end loop */
                    return 0;
        }
        str1++;
        str2++;
    }
//    return (*str1 == *str2);    /* Only both are NULL, return TRUE otherwise FALSE */
    return !(*str1 || *str2);    /* Ditto */
}