では、今回から新しいCSTRクラスのFind、ReplaceとReplaceAllという3つの関数を使ったアプリケーションであるBCC2ECCのプログラミングの解説を始めます。

 

まず、BCC2ECCとはなにか(BCC2ECCの設計仕様)、について話します。

 

(1)ANSI(Shift JIS)ベースのBCCSkeltonライブラリーを承継し、Windows NativeであるUTF-16(ワイド文字)ベースのECCSkeltonライブラリーを(一応)完成させたのですが、「テキストエディタ―で一から書く」というのは本来のBCCSkeltonの設計目標から外れるので、基本的にBCCSkeltonと同等の開発環境(BCCFormとSkeltonWizardが使える)を付与することにあります。

 

(2)この為、BCCFormの出力するrcファイルと、そのrcファイルからSkeltonWizardの出力するbdpファイル、hファイル、Proc.hファイルとcppファイル(以下「SkeltonWizardファイル」)を利用してECCSkeltonでの開発ができるようにする必要があります。

 

(3)しかしSkeltonWizardファイルは基本的にANSI(Shift JIS)ベースのファイルなので、「SkeltonWizardによる出力以降、(コードをいじる前の)なるべく早い段階でECCSkelton用に変換」しなければなりません。

 

(4)その為にはBCCSkeltonとECCSkeltonのコード上の差異を踏まえ、文字列を扱うrcファイルと3つのSkeltonWizardファイル(hファイル、Proc.hファイルとcppファイル)に必要な変更を与える、その「差異の変換」を行うツールBCC2ECCです。

 

(5)なるべく簡素にしたいので、モードレスダイアログベースとし、

①bdpファイルからファイルパスとプロジェクト名を取得し、

②rcファイル、cppファイル、hファイル、Proc.hファイルの順で差異変換を行ってゆき、

③変換後の結果を別個のファイル(拡張子前に"_ECC"を付ける-例:(プロジェクト)Proc.h-変換→(プロジェクト)Proc_ECC.h)として出力します。(注)

注:オリジナルのファイルは無変更です。しかし4つのファイルから"_ECC"を取ってコンパイルしたい場合には、BCCSkeltonのサンプルにあるRenamerを使うとよいでしょう。

 

(6)処理後はメッセージボックスによる「再変換、新しい変換、終了」の三択処理とします。

 

なお、「SkeltonWizardによる出力以降、(コードをいじる前の)なるべく早い段階でECCSkelton用に変換」が理想ですが、ANSIベースのプロジェクトをUTF-16に変更したい場合もあると考え、オプションで特定の関数等の変更も行えるようにしています。

 

本日テストを完了し、このBCC2ECC(モードレスダイアログベース)とこの間やったRTWEditor(SDIウィンドウベース)の二つのアプリケーションのリソースで出力されたコードをBCC2ECCで変換(注)してコンパイル実行できました。

注:RTWEditorがそうですが、SkeltonWizardは親ウィンドウをCMyWndクラスとしてコーディングしますので、親ウィンドウは自動で変換できますが、ユーザーが定義するその他のダイアログはそれぞれの任意のダイアログIDを基にクラス名が作られるので、これらはユーザーがマニュアルで対応する必要があります。

例:IDD_INPUT→INPUTDLGクラス、IDD_VERSION→VERSIONDLGクラス

但し、その場合も、

(1)テーブル開始行のクラス名の修正

   例:BEGIN_CMDTABLE(CMyWnd)→BEGIN_CMDTABLE(VERSIONDLG)

(2)テーブルのコントロールコマンド修正

   例:ON_COMMAND(versiondlg, IDC_VERTXT, OnVertxt())→ON(IDC_VERTXT, OnVertxt())

だけと、いたって簡便です。

 

2022年9月16日:大変お騒がせしましたが、本件は本日解決し、↓に詳しく述べております。なお、下にあるコードは修正箇所を青太字で明記しておきますので、ご確認ください。

 

【オリジナルブログ】

現在新しいCSTRのFind、Replace、ReplaceAll関数を使ったツール、BCC2ECCを開発しています。

そんな中で、

怪談「bcc32c(bcc102)でコンパイルすると意図通り正常動作するのに、bcc32(bcc55)でコンパイルすると突然落ちる」

という怪奇現象に遭遇しました。

 

何を大げさな。あんたのバグでしょ?」とおっしゃるあなた、以下をよく読んで何が原因か教えてください。

 

1.現象

(1)CSTRの新しいReplaceAll関数で「, "」という文字列(ToFind 文字列、", \"")を「, L"」という文字列(ToReplace 文字列、", L\"")に置き換える処理を行っている際、

(2)最初の「, "(MS 明朝")」、次の「, "(STATIC")」は意図通りの動作をするが、三番目の「, "(EDIT")」で、','の位置に0(NULL)を代入できない。

(3)その結果文字列のメモリー領域を超えて文字列が付加され落ちる。

 

2.該当箇所のコード(デバッグ用のメッセージボックス付)

//文字列の置換
char* CSTR::Replace(char* ToFind, char* ToReplace, char* sp = 0) {

    //CSTRのデータ長、検索文字長、置換文字長を計算する
    int OrgLen = lstrlen(m_str);
    int ToFindLen = lstrlen(ToFind);
    int ToReplaceLen = lstrlen(ToReplace);
    //spの指定が無ければCSTRデータの先頭から検索する
    if(!sp)
        sp = m_str;
    //検索文字列ポインターの取得
    char* Found_at = strstr(sp, ToFind);        //検索文字列が無ければNULLを返す
    //検索文字があれば
    if(Found_at) {
        DWORD dspl = Found_at - m_str;            //Found_atのm_str内の相対位置
        //置換後の文字列データバッファー(NULL終端付)の確保
        char* buff = new char[OrgLen - ToFindLen + ToReplaceLen + 1];

MessageBox(0, Found_at, "Test - Found_at", MB_OK);    //①

        *Found_at = NULL;                        //NULL終端
        lstrcpy(buff, m_str);                    //検索文字列(ToFind)までをbuffにコピー

MessageBox(0, m_str, "Test - m_str", MB_OK);    //②

        lstrcat(buff, ToReplace);                //検索文字列(ToFind)の代わりに置換文字列をコピー

MessageBox(0, buff, "Test - buff", MB_OK);    //③

        lstrcat(buff, Found_at + ToFindLen);    //検索文字列(ToFind)以降の文字列をコピー
        buff[OrgLen - ToFindLen + ToReplaceLen] = NULL;    //NULL終端を付ける
        delete [] m_str;                        //CSTRデータ(m_str)を削除
        m_str = new char[OrgLen - ToFindLen + ToReplaceLen + 1];    //新しいデータ領域を確保
        lstrcpy(m_str, buff);                    //置換後のデータをコピー
        Found_at = m_str + dspl;                //Found_atのm_str内の相対位置
        delete [] buff;                            //文字列データバッファーを破棄
    }
    return Found_at;    //置換できなかった場合NULL、できた場合置換文字列の先頭ポインター
}

3.正常動作(, "STATIC")の場合のメッセージ

ToFindである「, "」(STATIC"...)を探しました。

 

「,」をNULLにして、それまでのm_str文字列をbuffに入れます。

 

ToFindの前のm_strをもらったbuffは末尾にToReplaceを追加します。

 

 

4.異常動作(, "EDIT")の時のメッセージ

ToFindである「, "」(EDIT"...)を探しました。ここまでは同じです。

 

「,」をNULLにした筈なのに、m_strは末尾までになっています。

 

その結果、buffにはm_str全文が入り、末尾にToReplace「, L”」を付加せざるを得なくなっています。

 

4.結語

正直私には何故「*Found_at = NULL; 」の文が正常に働かないのか、また何故第3番目の文字列で生じるのか、また何故bcc32(bcc55)で生じ、bcc32c(bcc102)では生じないのか、全くわかりません。

 

「実に面白い」

 

というガリレオ先生、いませんか?

 

ps. 実はCSTRクラスに関してはまだ怪談があり、それはある非公開のソフトで「外部変数で宣言したCSTRインスタンスにconst char*文字列を代入すると落ちる」という現象です。それも(そしてそれは真逆で)「bcc32(bcc55)でコンパイルすると正常に動作するのですが、bcc32c(bcc102)でコンパイルすると落ちる」のです。これも原因は不明であり、現在bcc55版だけを使っています。bcc2eccについてもbcc102版だけリリースすることになりそうです。

 

今日のブログの二本目は、恥ずかしながら「バグ」報告です。

 

不良コードを書いたのではなく、書くべきコードを書き忘れた(従って実害はない)ので、「バグ」か否かはビミョーなんですが、BCCSkeltonとの互換性を失っているので潔く「バグ」とします。

 

当該箇所はCDLGの終了に関わるOnClose関数の処理についてです。

 

【BCCSkelton.h】

///////////////////////////////////
//ダイアログコールバック関数マクロ
///////////////////////////////////
//モーダルダイアログ書式=BEGIN_MODALDLGMSG(CDLG変数)
//又MODALDLGPROC(CDLG変数)がコールバック関数

#define    BEGIN_MODALDLGMSG(_PROC_, _P_)    BOOL CALLBACK _PROC_(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) {\

    if(Msg == WM_CLOSE) if((Done = _P_.OnClose(wParam, lParam))) _P_.EndModal(Done);\
 

//モードレスダイアログ書式=BEGIN_MODELESSDLGMSG(CDLG変数)
//又MODELESSDLGPROC(CDLG変数)がコールバック関数

#define    BEGIN_MODELESSDLGMSG(_PROC_, _P_)    BOOL CALLBACK _PROC_(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam) {\

    if(Msg == WM_CLOSE) if((Done = _P_.OnClose(wParam, lParam)))    _P_.Destroy();\

 

(解説)BCCSkeltonではウィンドウやダイアログのコールバック関数を外部関数にしてマクロで処理をしています。

モーダルダイアログとモードレスダイアログは共にWM_CLOSEメッセージでOnClose関数を呼び、その戻り値(Done)がTRUEであれば「モーダルダイアログはEndModal関数を、モードレスダイアログはDestory関数」を呼びだします。

 

【ECCSkeltonのCDLG.h】

//本来のコールバック関数(仮想関数)
BOOL CALLBACK CDLG::DlgProc(HWND hWnd, UINT Message, WPARAM wParam, LPARAM lParam) {

    bool Done = FALSE;
    switch(Message) {



        case WM_CLOSE:
            Done = OnClose(wParam, lParam);
            break;



    }
    return Done;
}

 

(解説)ECCSkeltonではコールバック関数をクラスのメンバー関数にし、ウィンドウメッセージの処理関数は仮想関数にしており、WM_CLOSEの場合はOnClose関数を呼び出すだけになっていました。

しかし、本来BCCSkeltonの様にDone==TRUEの場合、「モーダルダイアログはEndModal関数を、モードレスダイアログはDestory関数」を呼びださなければBCCSkeltonとの互換性が取れなくなるため、今回以下の通り「m_Modeというモーダルダイアログ、モードレスダイアログの区分フラグを追加して」修正しました。(m_ModeフラグはDoModal関数とCreate関数で決定します。)

 

        case WM_CLOSE:
            Done = OnClose(wParam, lParam);
            if(Done) {        //終了ならば
                if(m_Mode)    //TRUE-Modal ダイアログ
                    EndModal(Done);
                else        //FALSE-Modeless ダイアログ
                    Destroy();

            }
 

今回ダイアログベースでBCC2ECCプロジェクトを作っていてダイアログを閉じてもプログラムが終了しないことから今回の不具合に気が付きました。修正の後、BCC2ECCのみならず、ダイアログベースのECCSkeltonサンプルを再コンパイルして正常に動作していることを確認しています。(注)

注:サンプルにはモーダルダイアログベースのFileListと、モードレスダイアログベースのTextToSpeechがあり、「どうやって動作させていたのだろうか?」と確認をしたら、OnClose関数の中でFileListはEndModal関数を、TextToSpeechはDestory関数を別途呼んでいました。そういう意味で、分かっている(注2)なら問題は生じないのですが、矢張りBCCSkeltonと互換性が無いのは不味であり、告知の上、修正版をアップロードします。

注2:これら二つのダイアログベースのプログラムをECCSkeltonに移植する際に、適切なコードを書いたにせよ、OnClose関数でプログラムが終了しないことは分かっていて、BDDSkeltonとの互換性欠如に気が付かなかったことは、矢張りボケが進行していると考えるべきなのでしょうか?ナンマイダブ。

 

RTWEditorを完成させたので、一応ECCSkeltonライブラリーはVersion 1.0としては完成しました。では次は何をやろうか、というお話です。(プログラミング解説ではなく、ブログとしてお読みください。)

 

元々暇プロであった私(注)が、3年のブランクの後、ウィンドウズプログラミングに目覚めたのは(今はもう跡形もない)ActiveBasicという言語によってでしたが、すぐにフリーソフトのBCC5.5によるプログラミングに関心が移りました。

注:【昔話】私の8ビット時代【昔話】私の16ビット時代【昔話】私の32ビット時代

 

元々Cはカーニハンアンドリッチーで覚え、既に実用プログラム(といってもZ80アッセンブラーですが...汗)も書いていましたが、C++のクラス、継承、オーバーロード、抽象化とオーバーライド等、Cからの発展は大きく、一から出直しの学習でした。そんな中で、コード量の多いWin32SKDプログラミングに疑問を覚え、C++の学習をしながら効率的なライブラリーを使いたい、ということでBCCSkeltonを発展させてきたのですが、最初に書いたクラスは22年前にBCCFormを開発する際に書いたCSTRだったと記憶します。

 

BCCFormを作るうえで、C++の文字列処理の面倒くささから、「可変長の文字列を扱える変数(オブジェクト)で、文字列の結合、切り出しや比較演算等ができるもの」が必要であり、「ないなら作っちゃおう()」ということでCSTRクラスを書いた記憶があります。

:C++の標準ライブラリーにはstd::stringクラスというものがありますが、いつからライブラリーに入ったかは記録が無いのでわかりませんが、始まりはbasic_stringクラスといったようで、1990年代前半のユニコード標準化までは余り知られていなかったのかしら?少なくともBCCSkeltonを作っている時点では(私も初心者だったし)この存在を知りませんでした。今回std::stringの歴史を調べましたが、歴史的にその発展を記述しているサイトは見当たらず、結局BCCSkeltonが完成した2002年時点のstringクラスがどんなものであったかはわかりませんでした。当時CSTRを書かずにstringクラスを使っていたならばどうなっていただろう、と感慨深いです。なお、

 

注意:これから正しくC++を書こうと考えている方は、現在の標準ファイブラリーを基にstd::stringクラスを使ってください。(とはいえ、今からCSTRをstringに書き換えるだけでは何の意味もないので、私はCSTRを使い続けますが。汗;)

 

所でこのCSTRクラスは当時BCCFormを作るために必要な文字列処理をメンバー関数にした為、文字列処理ではあまり大したことをやっていませんが、stringクラスにはないファイル入出力が可能なために結構重宝しています。(また、デバッグでPrint()関数を使うことも多いです。)

 

所がCSTRにはstringクラスにある文字列の検索、置換処理の関数が基本的にありませんでした。(注)

注:公式発表ベースでは、という意味です。CSTR.hを見ると最後に、

    //文字列の検索
    char* CSTR::Find(char* ToFind) {

        return strstr(m_str, ToFind);
    }
という記述がありますが、この関数は何度実行してもCSTRのインスタンスの最初のToFindの位置を返すだけの「木偶の棒」関数です。(笑)
 

そんなこんなで、これではCSTRが可哀想、という想いで、(偶々CREDITクラスで文字列の検索・置換ダイアログを扱ったこともあり)このクラスに3つの関数を追加しました。(同時にECCSkeltonにも同様の関数を追加しています。)

 

//文字列の検索
char* CSTR::Find(char* ToFind, char* sp = 0) {

    if(!sp)
        sp = m_str;
    return strstr(sp, ToFind);
}


この関数は↑のいい加減な関数ではなく、CSTR::Find("探したい文字列");とすると最初の「探したい文字列の先頭アドレス」が返るので、次の検索にはCSTR::Find("探したい文字列", (第1回目の戻り値));とすることで検索を継続することができます。

//文字列の置換
char* CSTR::Replace(char* ToFind, char* ToReplace, char* sp = 0) {

    //CSTRのデータ長、検索文字長、置換文字長を計算する
    int OrgLen = lstrlen(m_str);
    int ToFindLen = lstrlen(ToFind);
    int ToReplaceLen = lstrlen(ToReplace);
    //spの指定が無ければCSTRデータの先頭から検索する
    if(!sp)
        sp = m_str;
    //検索文字列ポインターの取得
    char* Found_at = strstr(sp, ToFind);        //検索文字列が無ければNULLを返す
    //検索文字があれば
    if(Found_at) {
        //置換後の文字列データバッファーの確保
        char* buff = new char[OrgLen - ToFindLen + ToReplaceLen + 1];    //NULL終端を追加
        *Found_at = NULL;
        lstrcpy(buff, m_str);                    //検索文字列(ToFind)までをbuffにコピー
        lstrcat(buff, ToReplace);                //検索文字列(ToFind)の代わりに置換文字列をコピー
        lstrcat(buff, Found_at + ToFindLen);    //検索文字列(ToFind)以降の文字列をコピー
        buff[OrgLen - ToFindLen + ToReplaceLen] = NULL;    //NULL終端を付ける
        delete [] m_str;                        //CSTRデータ(m_str)を削除
        m_str = new char[OrgLen - ToFindLen + ToReplaceLen + 1];    //新しいデータ領域を確保
        lstrcpy(m_str, buff);                    //置換後のデータをコピー
        delete [] buff;                            //文字列データバッファーを破棄
    }
    return Found_at;    //置換できなかった場合NULL、できた場合置換文字列の先頭ポインター
}


このReplace関数はFind関数を使ってToFind文字列を探し、それをToReplace文字列に交換する処理を行います。引数の与え方はFind関数を同じ様にしているので、継続置換処理も可能です。

//文字列の全置換
void CSTR::ReplaceAll(char* ToFind, char* ToReplace, char* sp = 0) {

    char* Next = sp;
    while((Next = Replace(ToFind, ToReplace, Next)))
        Next += lstrlen(ToReplace);
}

 

最後はReplace関数を使った全置換関数で、Replace関数の繰り返し処理を行います。CSTR::ReplaceAll("検索文字列"、"置換文字列")でCSTRのはじめから全置換します。(途中で開始するには文字列ポインターspに値を与えることが必要です。)参考になるのではないでしょうか?

なお、ECCSkeltonではchar→WCHARの他、lstr***関数はlstr***W関数、strstr関数はStrStrW関数に置き換える必要があります。

 

所で、まだ本題(「次のブログネタ」)に入っていませんでした。

今回(ECCSkeltonの完成と共に)CSTRにこれらの関数を新設したので、これを使った久々のツールを考えています。名付けて"BCC2ECC"、SkeltonWizardで(プロジェクト名).rcから、(プロジェクト名).bdp、(プロジェクト名).h、(プロジェクト名)Proc.h、(プロジェクト名).cppファイルが生成された段階で適用する「BCCSkelttonコード」を「ECCSkelttonコード」へ変更するツールです。

お楽しみに!

 

RTWEditorのプログラミングについて書く前に、

【RTWEditor】ECCSkeltonのCREDITクラスのファイル入出力関数と一応の完成

で、次のように書きました。

 

【残された問題点】

1.上記したように、BOM付UTF-16形式だとBOM迄読み込むので、読み込んだデータの先頭1文字をマニュアルで削除する必要がある。

2.読み込みや、書き込みの際に必ずRTFでUTF-8かANSIの二択、TXTでUTF-16かUTF-8かANSIの三択のメッセージボックスが表示される仕様にしたので、少々ウザい。

3.何故かANSIでは問題がなあったが、ユニコード(UTF-16、UTF-8)のファイルを読むと、初期的にデータが表示されない。(データは読み込まれているのだけれども、文字が表示されない。)UpdateWindowをリッチエディットコントロールや親SDIウィンドウにかけて再描画を強制しても表示されない。(他のタブに移行して、戻ってくると表示される。)これはゆっくりと考えてみます。

 

現在、これらについて自分なりに一応の解決としていますので、報告します。(本日確定、今後サンプルでアップします。)

 

【解決内容】

1.については、不思議なんですが、3でコードをいじっている間にBOMに相当する文字(注)が表示されなくなりました。なんでだろう?

注:最初は「BOM」を別途読み込めば、ファイルポインターが移動するからBOM以降を読む、と思っていたのですが、実際に読み込むと「位置が右に来る空白文字」(だったと記憶しています)が表示されました。

 

2.については、慣れの問題で今は(終了時の三択を含め)余りウザくは感じません。

 

3.についてですが、「読み込み時にきれいに表示され、キャレット(カーソル)も先頭に表示」され、タブを変更した際にも同様に表示されることを条件に、SetFocus、ShowWindow、UpdateWindow等フォーカスと再描画関係の関数を色々試した結果、以下が一番イメージに近いものであったので、これを最終形としました。

 

//タブの表示サイズに親のコントロールサイズをあわせる
void CTAB::FitSiblings(WPARAM wParam) {

    if(wParam != (WPARAM)m_hMenu)    return;            //タブコントロールでなければ帰る
    int focus = TabCtrl_GetCurSel(m_hWnd);            //現在の選択タブを取得する
    if(focus == -1)                    return;            //選択されていなければ帰る
    //親のWM_SIZEメッセージでFitParent関数が実行される際に実行されるためm_x等の位置情報は最新
    m_rectTab.left = m_x;
    m_rectTab.right = m_x + m_Width;
    m_rectTab.top = m_y;
    m_rectTab.bottom = m_y + m_Height;
    TabCtrl_AdjustRect(m_hWnd, FALSE, &m_rectTab);    //タブウィンドウ矩形位置を求める
    //カレントタブ以外のタブのコントロールを隠し、サイズを調整する
    for(int i = 0, j = TabCount(); i < j; i++) {
        if(i != focus)        //カレントタブ以外のタブのコントロールを隠す
            ShowWindow(GetDlgItem(m_hParent, m_CtrlID[i]), SW_HIDE);
        MoveWindow(GetDlgItem(m_hParent, m_CtrlID[i]),
                            m_rectTab.left, m_rectTab.top,
                            m_rectTab.right - m_rectTab.left,
                            m_rectTab.bottom - m_rectTab.top, TRUE);
    }
    //最後にカレントタブコントロールを再表示してフォーカスを当てる
    ShowWindow(GetDlgItem(m_hParent, m_CtrlID[focus]), SW_SHOWNORMAL);
    SetFocus(GetDlgItem(m_hParent, m_CtrlID[focus]));
}

面白いのは、最初の形は「すべての親のコントロールを隠して、カレントタブのコントロールを再表示」させていた所、思ったようにカレントタブのコントロールが表示されず、「隠す」際にカレントタブのコントロールだけ隠さないようにしたところ思い通りの動作になったことです。理屈としては差はないように感じますがどうしてなんでしょうね?

 

いずれにしても、これでRTWEditorを完了します。

 

Album不具合解決報告の闖入がありましたが、今回がECCSkeltonによるRTWEditorの最終回です。残りはUser.hとRTWEditorProc.hの二つです。

 

【User.h】

このファイルは、外部変数の宣言とメニュー、ツールバーボタンの状態設定の外部関数があります。

 

//////////////////////////////////////////
// User.h
// For RTWEditor
// Copyright (c) 08/30/2022 by ECCSkelton
//////////////////////////////////////////
//////////////////////////////
//ポップアップメニューハンドル
//////////////////////////////
HMENU g_hPopup;
//(解説:タブやリッチテキストコントロールの右クリックでポップアップメニューを出す為の記録用です。)


///////////////////////////////
//ドラッグアンドドロップ用変数
///////////////////////////////
bool g_ByFile;
CARG g_Arg;
CSTR g_FileName;
//(解説:順にファイル起動フラグ、コマンドライン引数取得用及びファイル名記録用です。)
 

//--------------------------------
// メニュー、ツールバー状態の変更
//--------------------------------
void ChangeMenuStatus(bool EnableFlag) {

    HMENU hMenu = GetMenu(RTWEditor.GetHandle());    //メニューハンドルの取得

    //メニューバー
    EnableMenuItem(hMenu, 1, MF_BYPOSITION | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hMenu, 2, MF_BYPOSITION | (EnableFlag ? MF_ENABLED : MF_GRAYED));

    HMENU hFileMenu;
    hFileMenu = GetSubMenu(hMenu, 0);
    EnableMenuItem(hFileMenu, IDM_SAVE, MF_BYCOMMAND | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hFileMenu, IDM_SAVEAS, MF_BYCOMMAND | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hFileMenu, IDM_SETPRINT, MF_BYCOMMAND | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    EnableMenuItem(hFileMenu, IDM_PRINT, MF_BYCOMMAND | (EnableFlag ? MF_ENABLED : MF_GRAYED));
    DrawMenuBar(RTWEditor.GetHandle());    //Re-draw "&File" menu

    //ツールバー(MAKELONGマクロは16bit整数2つをunsigned 32bit整数にする)
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_SAVE, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_PRINT, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_UNDO, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_CUT, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_COPY, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_PASTE, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_FIND, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_REPLACE, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_DELETE, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_LEFT, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_CENTER, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_RIGHT, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_FONT, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_LINDENT, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_LOUTDENT, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_RINDENT, MAKELONG(EnableFlag, 0));
    SendMessageW(TBar.GetHandle(), TB_ENABLEBUTTON, IDM_ROUTDENT, MAKELONG(EnableFlag, 0));
}

//(解説:ここは純粋にWin32APIだけの記述です。引数を使ってメニューとツールバーボタンの状態を変えます。BCCSkelton版と異なるのは、タブを消去するメニュー(IDM_DELETE)を追加したことです。)

 

【RTWEditorProc.h】

基本的にRTWEditor.hで宣言したメンバー関数の定義部分です。単なるエディターなので余り難しいことはしていません。

 

//////////////////////////////////////////
// RTWEditorProc.h
// Copyright (c) 08/30/2022 by ECCSkelton
//////////////////////////////////////////

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//ウィンドウメッセージ関数
/////////////////////////////////
bool CMyWnd::OnCreate(WPARAM wParam, LPARAM lParam) {
    //コモンコントロールの初期化
    InitCommonControls();

//(解説:ここでの注意点は、先ずツールバーにコモンコントロールのビットマップを使って登録した後、カスタムビットマップを登録追加することです。)

    //ツールバー登録-Init(hWnd, hIinstance, ID, Style)-ツールチップを付ける
    TBar.Init(m_hWnd, m_hInstance, TOOLBAR, WS_CHILD | WS_VISIBLE | TBSTYLE_TOOLTIPS);
    //ツールバーボタン用ビットマップ追加
    TBar.AddBmp();
//(解説:bool引数のAddBmpはコモンコントロールのスタンダードビットマップを登録します。TRUE(規定値-省略可)の場合は"STD_XXX"のビットマップ、FALSEの場合”VIEW_XXX"のビットマップとなります。)

    //ツールバーボタン追加
    TBBUTTON tbb[26];
    ZeroMemory(tbb, sizeof(tbb));
    tbb[0].iBitmap = STD_FILENEW;
    tbb[0].fsState = TBSTATE_ENABLED;
    tbb[0].fsStyle = TBSTYLE_BUTTON;
    tbb[0].idCommand = IDM_NEW;
    tbb[1].iBitmap = STD_FILEOPEN;
    tbb[1].fsState = TBSTATE_ENABLED;
    tbb[1].fsStyle = TBSTYLE_BUTTON;
    tbb[1].idCommand = IDM_OPEN;
    tbb[2].iBitmap = STD_FILESAVE;
    tbb[2].fsStyle = TBSTYLE_BUTTON;
    tbb[2].idCommand = IDM_SAVE;
    tbb[3].iBitmap = STD_PROPERTIES;
    tbb[3].fsState = TBSTATE_ENABLED;
    tbb[3].fsStyle = TBSTYLE_BUTTON;
    tbb[3].idCommand = IDM_SETPRINT;
    tbb[4].iBitmap = STD_PRINT;
    tbb[4].fsStyle = TBSTYLE_BUTTON;
    tbb[4].idCommand = IDM_PRINT;
    tbb[5].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[6].iBitmap = STD_UNDO;
    tbb[6].fsStyle = TBSTYLE_BUTTON;
    tbb[6].idCommand = IDM_UNDO;
    tbb[7].iBitmap = STD_CUT;
    tbb[7].fsStyle = TBSTYLE_BUTTON;
    tbb[7].idCommand = IDM_CUT;
    tbb[8].iBitmap = STD_COPY;
    tbb[8].fsStyle = TBSTYLE_BUTTON;
    tbb[8].idCommand = IDM_COPY;
    tbb[9].iBitmap = STD_PASTE;
    tbb[9].fsStyle = TBSTYLE_BUTTON;
    tbb[9].idCommand = IDM_PASTE;
    tbb[10].iBitmap = STD_FIND;
    tbb[10].fsStyle = TBSTYLE_BUTTON;
    tbb[10].idCommand = IDM_FIND;
    tbb[11].iBitmap = STD_REPLACE;
    tbb[11].fsStyle = TBSTYLE_BUTTON;
    tbb[11].idCommand = IDM_REPLACE;
    tbb[12].fsStyle = TBSTYLE_SEP;     //セパレーター
    //カスタムビットマップの追加
    TBar.AddBmp(m_hInstance, MAKEINTRESOURCE(IDI_CUSTOM), 9);

//(解説:ここからはカスタムビットマップを登録して使用します。)

   tbb[13].fsStyle = TBSTYLE_SEP;     //セパレーター
    tbb[14].iBitmap = TBar.m_id;
    tbb[14].fsStyle = TBSTYLE_BUTTON;
    tbb[14].idCommand = IDM_DELETE;
    tbb[15].fsStyle = TBSTYLE_SEP;     //セパレーター
    tbb[16].iBitmap = TBar.m_id + 1;
    tbb[16].fsStyle = TBSTYLE_BUTTON;
    tbb[16].idCommand = IDM_LEFT;
    tbb[17].iBitmap = TBar.m_id + 2;
    tbb[17].fsStyle = TBSTYLE_BUTTON;
    tbb[17].idCommand = IDM_CENTER;
    tbb[18].iBitmap = TBar.m_id + 3;
    tbb[18].fsStyle = TBSTYLE_BUTTON;
    tbb[18].idCommand = IDM_RIGHT;
    tbb[19].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[20].iBitmap = TBar.m_id + 4;
    tbb[20].fsStyle = TBSTYLE_BUTTON;
    tbb[20].idCommand = IDM_FONT;
    tbb[21].fsStyle = TBSTYLE_SEP;    //セパレーター
    tbb[22].iBitmap = TBar.m_id + 5;
    tbb[22].fsStyle = TBSTYLE_BUTTON;
    tbb[22].idCommand = IDM_LINDENT;
    tbb[23].iBitmap = TBar.m_id + 6;
    tbb[23].fsStyle = TBSTYLE_BUTTON;
    tbb[23].idCommand = IDM_LOUTDENT;
    tbb[24].iBitmap = TBar.m_id + 7;
    tbb[24].fsStyle = TBSTYLE_BUTTON;
    tbb[24].idCommand = IDM_RINDENT;
    tbb[25].iBitmap = TBar.m_id + 8;
    tbb[25].fsStyle = TBSTYLE_BUTTON;
    tbb[25].idCommand = IDM_ROUTDENT;
    TBar.AddButtons(26, tbb);

    //ステータスバー登録-Init(hWnd, hIinstance, ID, (以下省略可)Style)
    SBar.Init(m_hWnd, m_hInstance, STATUSBAR);
    //ステータスバー区画設定
    int sec[4] = {120, 210, 330, -1};
    SBar.SetSection(4, sec);
    //ステータスバー文字列設定
    SBar.SetText(0, L"RTWEditor Version 1.0");

//(解説:1の区画にはファイルサイズ、2の区画にはキャロット位置、3の区画にはファイルパス名が入ります。)


    //タブコントロールの作成とタグのフォント設定
    m_Tab.Create(m_hWnd, m_hInstance, (HMENU)IDC_TAB);
    m_Tab.SetFont(9, L"MS P明朝");

//(解説:今回の主役、メインウィンドウのクライアントエリアに張るタブコントロールの生成とフォント設定です。)


    //ドラッグアンドドロップを許可する
    DragAcceptFiles(m_hWnd, TRUE);

    //メニューハンドルの取得
    g_hPopup = GetSubMenu(GetMenu(m_hWnd), 1);

//(解説:タブコントロール、リッチエディットコントロールの右クリックでポップアップさせるメニューとしてメインメニューの「編集」を選択しています。)


    //ファイル起動対応(g_ByFileが真の時、1回のみ)
    if(g_ByFile) {
        for(int i = 1; i < g_Arg.c(); i++) {
            if(g_ExtChk.CheckExt(g_Arg.v(i), FILEFILTER))    //対象ファイルか否かチェック
                AddTabEdit(g_Arg.v(i));                        //タブの追加
            else
                MessageBoxW(m_hWnd, g_Arg.v(i), L"対象外のファイル", MB_OK | MB_ICONERROR);
        }
        g_ByFile = FALSE;
    }

//(解説:ファイル起動フラグ(g_ByFile)により、対象ファイルの確認をEXTCHKクラスで選別し、共通の「タブを増やして、リッチテキストコントロールを貼る」ユーザー定義関数を呼びます。)


    return TRUE;
}

bool CMyWnd::OnNotify(WPARAM wParam, LPARAM lParam) {

    if(((LPNMHDR)lParam)->code == TTN_NEEDTEXT) {    //ツールバーツールチップの表示
        LPTOOLTIPTEXT lpTTText = (LPTOOLTIPTEXT)lParam;
        lpTTText->hinst = m_hInstance;
        switch(lpTTText->hdr.idFrom) {
        case IDM_NEW:
            lpTTText->lpszText = MAKEINTRESOURCE(String_00);
            break;
        case IDM_OPEN:
            lpTTText->lpszText = MAKEINTRESOURCE(String_01);
            break;
        case IDM_SAVE:
            lpTTText->lpszText = MAKEINTRESOURCE(String_02);
            break;
        case IDM_SETPRINT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_03);
            break;
        case IDM_PRINT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_04);
            break;
        case IDM_UNDO:
            lpTTText->lpszText = MAKEINTRESOURCE(String_05);
            break;
        case IDM_CUT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_06);
            break;
        case IDM_COPY:
            lpTTText->lpszText = MAKEINTRESOURCE(String_07);
            break;
        case IDM_PASTE:
            lpTTText->lpszText = MAKEINTRESOURCE(String_08);
            break;
        case IDM_FIND:
            lpTTText->lpszText = MAKEINTRESOURCE(String_09);
            break;
        case IDM_REPLACE:
            lpTTText->lpszText = MAKEINTRESOURCE(String_10);
            break;
        case IDM_DELETE:
            lpTTText->lpszText = MAKEINTRESOURCE(String_11);
            break;
        case IDM_LEFT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_12);
            break;
        case IDM_CENTER:
            lpTTText->lpszText = MAKEINTRESOURCE(String_13);
            break;
        case IDM_RIGHT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_14);
            break;
        case IDM_FONT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_15);
            break;
        case IDM_LINDENT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_16);
            break;
        case IDM_LOUTDENT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_17);
            break;
        case IDM_RINDENT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_18);
            break;
        case IDM_ROUTDENT:
            lpTTText->lpszText = MAKEINTRESOURCE(String_19);
            break;

//(解説:ここまでは定番のツールバービットマップボタンのツールチップです。今回はストリングテーブルを使っています。)

        default://hdr.idFromがタブコントロールのページ番号(0~)でツールチップ要求が入る為、
                //タブコントロールのページ番号で他のコントロールでIDを設けてはいけない
                if(lpTTText->hdr.idFrom < m_Tab.TabCount()) {        //タブのツールチップ
                    lpTTText->lpszText = m_Tab.GetTitle(lpTTText->hdr.idFrom);
                    break;
                }

//(解説:タブページは0からページ数-1のコントロールIDが付きますので、ツールバーと同じくここで処理します。コメントにあるように他のコントロールと重複しないように注意します。表示するのはタブタグに表示されるファイルパス名です。)

                else
                    return FALSE;
        }
        return TRUE;
    }
    //タブのページ選択変更
    else if(((LPNMHDR)lParam)->code == TCN_SELCHANGE) {
        //新たに選択されたタブのリッチエディットコントロールをm_Editでカプセル化する
        m_Edit.SetHandle(m_Tab.GetCtrl());
        m_Tab.FitSiblings((WPARAM)IDC_TAB);
        SBar.SetText(1, L"");                //バイト数をクリア
        SBar.SetText(2, L"");                //キャレット位置をクリア
        SBar.SetText(3, m_Tab.GetTitle());    //ファイル名を変更

//(解説:ここでは、別のタブをクリックして選択した時の処理を書きます。m_EditというCREDITクラスのインスタンスをカプセルにし、新規に選ばれたタブに張り付けられたリッチテキストコントロールのハンドルを渡し、選択されている限り処理はm_Editを通じて行います。その後は再描画とステータスバーの表記変更です。)

    }
    else if(((NMHDR*)lParam)->code == NM_RCLICK) {            //タブの右マウスボタン押し下げ通知
        //ポップアップメニューの表示
        PopupMenu();

//(解説:このポップアップメニュー処理はタブ部分(タブのタグ部分だけ)の右クリックに対応しています。)

    }
    //リッチエディットテキストの変更でバイト数とキャレット位置を表示
    else if(((NMHDR*)lParam)->code == EN_SELCHANGE) {
        WCHAR str[19];    //"(6桁)+行:(6桁)+桁" + NULL, //"xxxxxxxxxxxバイト" + NULL
        wsprintfW(str, L"%dバイト", m_Edit.GetSize());
        SBar.SetText(1, str);
        wsprintfW(str, L"%d行:%d桁", m_Edit.GetPos()->y + 1, m_Edit.GetPos()->x + 1);
        SBar.SetText(2, str); 

//(解説:これはリッチテキストコントロール(m_Edit)のキャレットに移動があった場合の処理で、ステータスバー表記を行います。)

    }
    //リッチエディットコントロールからの通知はMSGFILTER構造体で受け取る
    else if(((NMHDR*)lParam)->code == EN_MSGFILTER && ((MSGFILTER*)lParam)->msg == WM_RBUTTONUP) {
        //ポップアップメニューの表示
        PopupMenu();

//(解説:このポップアップメニュー処理はリッチテキストコントロール部分の右クリックに対応しています。)

    }
    else
        return FALSE;
    return TRUE;
}

bool CMyWnd::OnSize(WPARAM wParam, LPARAM lParam) {

    //ツールバー、ステータスバーの自動調整
    TBar.AutoSize();
    SBar.AutoSize();
    //現在のタブとそれのコントロールの自動調整
    m_Tab.FitParent(SBar.GetHandle(), TBar.GetHandle());

//(解説:親のサイズ変更があった場合、親の新しいサイズに合わせて自分もサイズを変えるCTABクラスの関数です。)

    return TRUE;
}

bool CMyWnd::OnClose(WPARAM wParam, LPARAM lParam) {

    int ret = MessageBoxW(m_hWnd, L"終了します。\nタブ毎に確認しますか?(「はい」)\n(終了せずに戻る-「キャンセル」)",
                    L"終了確認", MB_YESNOCANCEL | MB_ICONINFORMATION);
    if(ret == IDYES) {
        //リッチエディットに変更があるかすべてのタブでチェック
        for(int i = m_Tab.TabCount(); i > 0; i--) {
            m_Tab.SetPage(i - 1);            //最後のタブをカレントにする
            if(!DelTabEdit())                //そのタブとコントロールを破棄する
                return FALSE;

//(解説:FILOで保存確認の上、タブを閉め、リッチテキストコントロールを破壊するユーザー定義関数を呼んで終了します。)

        }
        //処理をするとDestroyWindow、PostQuitMessageが呼ばれる
        return TRUE;
    }
    else if(ret == IDNO)

//(解説:これは一挙に終了です。)

        return TRUE;
    else
        //そうでなければウィンドウではDefWindowProc関数をreturn、ダイアログではreturn FALSEとなる。

//(解説:これは終了しません。)

        return FALSE;
}

bool CMyWnd::OnDropFiles(WPARAM wParam, LPARAM lParam) {

    //ドラッグアンドドロップされたファイル名取得と処理
    WCHAR fn[MAX_PATH];
    //ドラッグアンドドロップされたファイルの数取得
    int num = DragQueryFileW((HDROP)wParam, 0xFFFFFFFF, NULL, 0);
    //ドラッグアンドドロップされたファイル名取得と処理
    for(int i = 0; i < num; i++) {
        DragQueryFileW((HDROP)wParam, i, fn, sizeof(fn));
        //OnOpen()関数を開くファイルの数だけ繰り返す
        if(g_ExtChk.CheckExt(fn, FILEFILTER)) {        //対象ファイルか否かチェック
            AddTabEdit(fn);                            //タブの追加
            ChangeMenuStatus(TRUE);
            SBar.SetText(3, fn);
        }
        else
            MessageBoxW(m_hWnd, fn, L"対象外のファイル", MB_OK | MB_ICONERROR);
    }
    DragFinish((HDROP)wParam);
    return TRUE;

//(解説:ワイド文字が使われているだけで、定番のドラッグアンドドロップ処理です。)

}

bool CMyWnd::OnGetMaxInfo(WPARAM wParam, LPARAM lParam) {

    //典型的なウィンドウのサイズ制限処理
    MINMAXINFO *pmmi;
    pmmi = (MINMAXINFO*)lParam;
    pmmi->ptMinTrackSize.x = MINW + 16;    //クライアントエリア + 16
    pmmi->ptMinTrackSize.y = MINH + 61;    //クライアントエリア + 61
    return FALSE;                        //処理はDefWndProcに任す

//(解説:メインウィンドウをMINWとMINHの最小サイズに制限する処理です。)

}

bool CMyWnd::OnOthers(UINT Msg, WPARAM wParam, LPARAM lParam) {

    //CREDITクラスは文字列検索・置換用にユーザーメッセージを作るので、
    //このような処理を行います。CREDITファイルを覗いてください。
    if(Msg == m_Edit.m_frMsg)
        m_Edit.FindReplace(lParam);    //検索・置換処理

//(解説:CREDITクラスの文字列の検索・置換処理であるFindReplace関数を、既に登録したメッセージ(m_frMsg)の際に処理します。なお、ここはboolではなく、voidですね。汗;)

}

/////////////////////////////////
//主ウィンドウCMyWndの関数の定義
//メニュー項目、コントロール関数
/////////////////////////////////
bool CMyWnd::OnNew() {

    //新しいタブにリッチエディットコントロールを張り付ける
    AddTabEdit(NULL);
    SetFocus(m_Edit.GetHandle());
    return TRUE;

//(解説:コメント通りです。作成したリッチテキストコントロールにフォーカスを当てて、キャレットを表示させます。)

}

bool CMyWnd::OnOpen() {

    if(!g_ByFile) {
        //ファイルを開くダイアログ
        WCHAR* fn = cmndlg.GetFileName(m_hWnd, FILEFILTER);
        if(!fn) {
            MessageBoxW(m_hWnd, L"操作はキャンセルされました", L"メッセージ", MB_OK | MB_ICONERROR);
            return FALSE;
        }
        g_FileName = fn;
    }

//(解説:ファイル起動の場合はg_FileNameにデータが入ってますが、そうでない場合はファイルを開くダイアログを使います。)

    //新しいタブにリッチエディットコントロールを張り付けg_FileNameを読み込む
    AddTabEdit(g_FileName.ToChar());    //SetFocusはこの中でやっている

//(解説:新規の場合AddTabEdit関数には"NULL"を渡しますが、ファイルを読み込ませる場合はファイル名を渡します。)

    return TRUE;
}

bool CMyWnd::OnSave() {

    WCHAR* fn = m_Tab.GetTitle();
    //フォーカスのあるタブのリッチエディットコントロールのタイトルを読む
    if(!lstrcmpW(fn, L"[無題]"))
        OnSaveas();

//(解説:未保存の新規ファイル([無題])の場合は「名前を付けて保存」に飛びます。)

    else
        //フォーカスのあるタブのリッチエディットコントロールのデータを書き出す
        m_Edit.SaveFile(fn);
    return TRUE;
}

bool CMyWnd::OnSaveas() {

    WCHAR* fn = cmndlg.GetFileName(m_hWnd, L"リッチテキストファイル(*.rtf)\0*.rtf\0総てのファイル(*.*)\0*.*\0\0", FALSE);

//(解説:保存するファイル形式のデフォルトはリッチテキストファイルです。)

    if(!fn)
        MessageBoxW(m_hWnd, L"操作はキャンセルされました", L"エラー", MB_OK | MB_ICONERROR);
    else {
        //フォーカスのあるタブのリッチエディットコントロールのデータを書き出す
        m_Edit.SaveFile(fn);
        SBar.SetText(3, fn);    //(解説:ファイル名を変更します。)

    }
    return TRUE;
}

bool CMyWnd::OnSetprint() {

    //フォーカスのあるタブのリッチエディットコントロール
    return m_Edit.SetPrinter();
}

bool CMyWnd::OnPrint() {

    //フォーカスのあるタブのリッチエディットコントロール
    return m_Edit.Print(m_Tab.GetTitle());
}

//(解説:印刷関係は、CREDITクラスの関数によりダイアログを表示させて処理します。)


bool CMyWnd::OnExit() {

    SendMessage(m_hWnd, WM_CLOSE, 0, 0);
    return TRUE;
}

bool CMyWnd::OnUndo() {

    m_Edit.SendMsg(EM_UNDO, 0, 0);
    return TRUE;
}

bool CMyWnd::OnCut() {

    m_Edit.SendMsg(WM_CUT, 0, 0);
    return TRUE;
}

bool CMyWnd::OnCopy() {

    m_Edit.SendMsg(WM_COPY, 0, 0);
    return TRUE;
}

bool CMyWnd::OnPaste() {

    m_Edit.SendMsg(WM_PASTE, 0, 0);
    return TRUE;
}

//(解説:もともとリッチテキストコントロールはコピペとやり直し処理を織り込んでいるのでこのように簡素です。)


bool CMyWnd::OnSelall() {

    CHARRANGE cr;
    cr.cpMin = 0;
    cr.cpMax = -1;
    m_Edit.SendMsg(EM_EXSETSEL, 0, (LPARAM)&cr);
    return TRUE;
}

//(解説:リッチテキストコントロール内の文字列の選択はCHARRANGE構造体を使って行います。0から-1(最後)までを選択する、という意味です。)


bool CMyWnd::OnFind() {

    m_hDlg = m_Edit.m_hFRDlg = FindTextW(m_Edit.SetFRDlg(L'F'));
    return TRUE;
}

bool CMyWnd::OnReplace() {

    m_hDlg = m_Edit.m_hFRDlg = ReplaceTextW(m_Edit.SetFRDlg(L'R'));
    return TRUE;
}

//(解説:既に解説を行った「文字列の検索・置換」処理です。)


bool CMyWnd::OnDelete() {

    return DelTabEdit();
}

bool CMyWnd::OnFont() {

    m_Edit.SelFont();
    return TRUE;
}

//(解説:CREDITクラスのフォント選択ダイアログでフォントを選択する関数を呼んでいます。)


bool CMyWnd::OnLeft() {

    m_Edit.Align(L'L');
    return TRUE;
}

bool CMyWnd::OnCenter() {

    m_Edit.Align(L'C');
    return TRUE;
}

bool CMyWnd::OnRight() {

    m_Edit.Align(L'R');
    return TRUE;
}

bool CMyWnd::OnLindent() {

    m_Edit.Indent(1);
    return TRUE;
}

bool CMyWnd::OnLoutdent() {
    m_Edit.Indent(2);
    return TRUE;
}

bool CMyWnd::OnRindent() {

    m_Edit.Indent(3);
    return TRUE;
}

bool CMyWnd::OnRoutdent() {

    m_Edit.Indent(4);
    return TRUE;
}

//(解説:これらも元々リッチテキストコントロールにある左右中央寄せ、インデントの処理をCREDITクラスのメンバー関数で行っています。)


bool CMyWnd::OnBrief() {

    MessageBoxW(m_hWnd,
    L"RTWEditorはテキストやリッチテキスト用のエディターです。\n"\
    "拡張子がリッチテキスト(*.rtf)の場合その形式で取り扱い、*.txt等"\
    "その他の拡張子のファイルではテキストファイルとして取り扱います。\n"\
    "起動はダブルクリックの他、ファイルをプログラムアイコンへドロップすることでも可能です。"\
    "また起動後もドラッグアンドドロップでファイルを開くことができます。",
    L"簡単な説明", MB_OK | MB_ICONINFORMATION);
    return TRUE;
}

//(解説:RTWEditorの概要説明です。)


bool CMyWnd::OnShortcut() {

    MessageBoxW(m_hWnd,
    L"Ctrl+N-新規作成\nCtrl+O-ファイルを開く\nCtrl+S-ファイルの保存\nCtrl+P-印刷\nCtrl+X-終了\n\n"\
    "Ctrl+U-元に戻す\nCtrl+T-切り取り\nCtrl+C-コピー\nCtrl+V-貼り付け\n\nCtrl+F-文字列の検索\n"\
    "Ctrl+R-文字列の置換\n\nF1-ショートカットキーの解説\nF2-バージョン情報",
    L"ショートカットキーの解説", MB_OK | MB_ICONINFORMATION);
    return TRUE;
}

//(解説:アクセラレーターの説明です。)


bool CMyWnd::OnVer() {

    versiondlg.DoModal(m_hWnd, L"IDD_VERSION", GetInstance());
    return TRUE;
}

//(解説:バージョン表示ダイアログです。)


///////////////////
//ユーザー定義関数
///////////////////
bool CMyWnd::AddTabEdit(WCHAR* fn) {

    //新しいタブに表示するリッチエディットコントロールの作成
    if(!m_Edit.Create(L"", WS_CHILD | WS_VSCROLL | ES_AUTOHSCROLL | ES_AUTOVSCROLL |
                WS_HSCROLL | ES_NOHIDESEL | ES_MULTILINE | ES_WANTRETURN,
                WS_EX_CLIENTEDGE, m_hWnd, m_EditID)) {
        MessageBoxW(m_hWnd, L"リッチエディットコントロールが作れませんでした",
                    L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    //リッチエディットコントロールからの特定通知(例:ENM_MOUSEEVENTS)を開始
    m_Edit.FilterMsg(ENM_MOUSEEVENTS | ENM_KEYEVENTS | ENM_SELCHANGE);
    //リッチエディットコントロールのフォント設定
    m_Edit.SetFont(11, L"MS 明朝");
    m_Edit.SetCol(RGB(0, 0, 0));
    //リッチエディットコントロールの文字色設定
    if(!fn)    //読み込むファイルが無い(指定ファイルパス、名ポインターがNULL)場合、「[無題]」にする
        fn = L"[無題]";
    else    //読み込むファイルが有れば読み込む
        m_Edit.LoadFile(fn);
    //新しいタブを作成し、新規リッチエディットコントロールを付ける
    if(!m_Tab.AddPage(m_EditID, IDI_DOC, fn)) {
        MessageBoxW(m_hWnd, L"既にコントロールが作られているか、これ以上タブを設定できません",
                    L"エラー", MB_OK | MB_ICONERROR);
        DestroyWindow(m_Edit.GetHandle());    //既に作成した新規リッチエディットコントロールを破棄
        return FALSE;
    }
    //メニューとツールバーの状態を変更する
    ChangeMenuStatus(TRUE);
    //ステータスバーにファイル名を表示する
    SBar.SetText(3, fn);
    //次のリッチエディットコントロールIDの設定(ユニークにする為に、コントロールが削除されても永久欠番にする)
    m_EditID++;
    return TRUE;
}

//(解説:タブを増やし、リッチテキストコントロールを張り付け、ファイルがあればそれを読み込む処理のユーザー定義関数です。)


bool CMyWnd::DelTabEdit() {

    int sel = m_Tab.GetPage();
    if(sel == -1) {
        MessageBoxW(m_hWnd, L"タブが選択されていません", L"エラー", MB_OK | MB_ICONSTOP);
        return FALSE;
    }
    if(MessageBoxW(m_hWnd, L"選択されているタブを削除しますか?", L"削除確認", MB_YESNO | MB_ICONQUESTION) == IDYES) {
        //ファイルが変更されていれば、保存するか否か確認する
        if(SendMessage(m_Edit.GetHandle(), EM_GETMODIFY, 0, 0)) {
            if(MessageBoxW(m_hWnd, L"ファイルが変更されています。保存しますか?", L"保存確認", MB_YESNO | MB_ICONQUESTION) == IDYES)
                m_Edit.SaveFile(m_Tab.GetTitle());
        }
        m_Tab.DeletePage();        //現在の表示タブページで表示されている親のコントロールを破壊してくれる
        //ステータスバーをクリアする
        SBar.SetText(1, L"");
        SBar.SetText(2, L"");
        SBar.SetText(3, L"");
        if(m_Tab.TabCount() == 0) {        //すべてのタブがなくなったら
            //メニューとツールバーの状態を変更する
            ChangeMenuStatus(FALSE);
            return TRUE;
        }
        else {
            if(!m_Tab.SetPage(sel))
                m_Tab.SetPage(sel - 1);
            m_Edit.SetHandle(m_Tab.GetCtrl());
            m_Tab.FitSiblings((WPARAM)IDC_TAB);
            SBar.SetText(3, m_Tab.GetTitle());
            return TRUE;
        }
    }
    return FALSE;
}

//(解説:逆にファイル保存確認を行い、タブとリッチテキストコントロールを削除する処理です。メインウィンドウではスタータスバーの初期化、フォーカスを与えるタブの設定、すべてのタブがなくなったらメニューとツールバーの状態変更を行います。)


bool CMyWnd::PopupMenu() {

        //ウインドウの位置情報を取得
        RECT rec;
        GetWindowRect(m_hWnd, &rec);
        //ポップアップメニューの表示場所を設定
        POINT pt;
        GetCursorPos(&pt);
        //ポップアップメニューの表示
        return TrackPopupMenu(g_hPopup, TPM_LEFTALIGN | TPM_TOPALIGN | TPM_LEFTBUTTON, pt.x, pt.y, 0, m_hWnd, &rec);
}

//(解説:タブとリッチテキストコントロールで共通のポップアップメニュー処理です。)


///////////////////////////////
//ユーザーダイアログの関数定義
//コントロール関数
///////////////////////////////
bool VERSIONDLG::OnIdok() {

    versiondlg.EndModal(TRUE);
    return TRUE;
}

//(解説:お馴染み、バージョン表示ダイアログです。)

 

いかがでしたでしょうか?元々MDIベースのリッチテキストエディタ―はちょっと時代遅れかな、と思い、タブベースのエディターを作りたかったのでECCSkeltonで組んでみました。(他の自作プログラム同様)現在私のちょちょっとしたメモ書き用のエディターで愛用していますが、RTEditorよりも使いやすいみたいです。

 

注意:翌9月4日に↓にあるCALBUMの修正部分を再修正し、すぐにアップしなおしました。DLされた方はお気を付けください。なお、その理由詳細は末尾参照。

 

既に旧い話(6月1日付ブログ)になりますが、Album(写真閲覧ソフト)が突如(A bolt out of the blue)として「落ちる」という現象に見舞われ、

【またまた、謎】Can you hit the bolt back?

というブログを書き、MicrosoftのGDIPLUS.DLLがおかしいんじゃないの?等と匂わせています。(スミマセン、Microsoftさん!)

 

実は(お恥ずかしい話ですが)このAlbumの不具合は覚えていて、「いつか fix せにゃあかんのぅ」とは思っていたんですが、原因のヒントが思いつかず、モチベーションが下がり、「先送り、先送り」で触らずにいました。その結果...原因調査をして↑のブログを書いたことも忘れてしまい、再度全く同じ様にトラップをかけて原因を探って行って、本日目出度く「ShowImg(int x, int y, int w, int h)関数で、m_Image->GetWidth()とGetHeight()関数がゼロを返すので、Division by zeroエラーが生じている」ということを「発見」しました。

 

【CALBUM抜粋】

//イメージの表示(x, yからw幅、h高さの矩形に原寸表示比率で表示)
bool CALBUM::ShowImg(int x, int y, int w, int h) {

    //エラー対応
    if(!m_Photo[m_Selected])
        return FALSE;
    //現在選択されている写真のファイル名をワイド文字にする
    mbstowcs(m_WFName, m_Photo[m_Selected]->m_FName, MAX_PATH);

    //現在選択されている写真のファイル名でイメージクラスインスタンスを生成
    m_Image = new (Image)(m_WFName);
    
//m_Imageインスタンスの生成に失敗した場合(解説:これが成否判定の積り)
    if(!m_Image)
        
return FALSE;
    //画像を回転させる-RotateFlipType:Rotate<A>Flip<B> <A:None, 90, 180, 270>, <B:X, Y, XY>
    m_Image->RotateFlip(m_RFType);
    
//イメージの原寸を取得
    int imgw = m_Image->GetWidth();    //imgwに0が代入される
    int imgh = m_Image->GetHeight();    //imghに0が代入される
    //w、h内で原寸の縦横対比のイメージを中央に配置
    double asp1, asp2;
    asp1 = (double)imgh / (double)
imgw;    //ここでDiv by zeroエラーが発生する
    asp2 = (double)h / (double)w;
    if(asp1 > asp2) {
        imgh = h;
        imgw = h / asp1;
    }
    else {
        imgw = w;
        imgh = w * asp1;
    }
    x += (w - imgw) / 2;
    y += (h - imgh) / 2;
    //m_Imageインスタンスを表示
    m_pGraphics->DrawImage(m_Image, x, y, imgw, imgh);
    //m_Imageインスタンスを解放
    delete m_Image;
    m_Image = 0;
    //再描画を要求
    InvalidateRect(m_hWnd, NULL, TRUE);
    return TRUE;
}

 

「そんなことってあるんかいっ!」ということで、原因をググって行って↑の自分の過去ブログを「再発見」した次第。

 

ほんにおそろしい忘却力、ボケ力(ヂカラ)!

 

まぁ、それはそれとして、今回解決まで進展したのは、前回と同じように色々と調べ、色々と試した際に2020年の英文の質疑応答サイトで回答者が「『全く同じファイルパス』-というのは間違いだね。ファイルアクセスの仕方が実質的に違うんだよ。("the very same file paths" - You aren't. You are using a substantially different way to access file system objects.)」という文章を見て、「FileListのファイルパスが間違っていて、イメージファイルが読み込めないという可能性」が頭をよぎりました。

 

当時のコードはCALBUMクラスのメンバー変数m_Image(注)というImageオブジェクトへのポインターにワイド文字のファイル名からオブジェクトを生成し、その成否判定も行って(いる積り↑で)いました。

注:m_Imageは画像を表示させるときだけポインターとして使い、使わない場合は常時0を代入している。

 

しかし、この視点で「コンストラクターって値を返さないよなぁ」と思いながら調べなおすと、矢張りImage::Image(WCHAR, bool = FALSE)というコンストラクターは、

Return value

    None

と書かれています。

と、いうことはよ?ファイルが読み込まれていなくても(ゼロ)メモリー領域が確保されて、それをm_Imageが指すので「Non 0」になり、その幅と高さを求めると(当然)「0」が返る.....そいこと?

 

もう言わずもがなですが、(その時、「あっ、そうだ。OneDriveの設定からイメージデータの入っていたフォールダー名「ピクチャ」を"Pictures"フォールダーに入れ替えたんだった」という記憶が...)「落ちる」FileListのファイルパスを見ると「...\ピクチャ\...」となっていたので、一括変換で"Pictures"にすると、

 

お目出度うございま―す。きちんと表示されましたぁ。

 

では、次にどのようにして「ファイルのNot foundエラーを検知させるのか?」ということで、ImageクラスのFromFileメソッドとImage::GetLastStatus()を使って戻り値がOk (0) でない場合にエラーとし、特によくありそうな FileNotFound (10) の時はメッセージボックスで表示させようか、とか考えたのですが、実験してみるとなかなかFileNotFoundを返さないんですね。

 

そんなこんなで、(イメージファイルが読み込めなかった場合、イメージサイズが幅高さともにゼロになることを逆手に取り)現実的な対応として「(幅か高さがゼロなら)『ファイルが読み込めなかった』というエラーメッセージを出して、『絵なし』表示にすることにしました。これで少なくとも「突然落ちる」ことはなくなりました。

 

【ShowImg関数の修正部】

    //現在選択されている写真のファイル名でイメージクラスインスタンスを生成
    m_Image = new (Image)(m_WFName, FALSE);
    //画像を回転させる-RotateFlipType:Rotate<A>Flip<B> <A:None, 90, 180, 270>, <B:X, Y, XY>
    m_Image->RotateFlip(m_RFType);
    //イメージの原寸を取得
    int imgw = m_Image->GetWidth();
    int imgh = m_Image->GetHeight();
  
 //m_Imageインスタンスの生成に失敗した場合imgw、imghはゼロになる
    if(!imgw || !imgh) {
        MessageBox(m_hWnd, "ファイルが読み込めませんでした。\nファイルパスを見直してください。",
                    "エラー", MB_OK | MB_ICONERROR);

        return FALSE;
    }

 

遅ればせながら、やっと夏休みの宿題が出せました。

 

【翌9月4日追記-CALBUM再修正】

翌朝も1時半くらいに目が覚めてしまい、ぼんやりスマホをいじってこのブログの確認をしていたら、途端に目が覚めました。

「ファイルが読み込めずにエラー処理しているのに、作ったイメージオブジェクト(サイズはゼロだが、メモリーリークはメモリーリーク)を消していないぞ?」

また、アホなことに、引数が違う別のShowImageのオーバーロード関数も同じ問題があるのに未処理でした。

 

ということで、次のようにして両ShowImg関数に入れて再修正を行いました。ゴメンナサイ。

    //m_Imageインスタンスの生成に失敗した場合imgw、imghはゼロになる
    if(!imgw || !imgh) {
        MessageBox(m_hWnd, "ファイルが読み込めませんでした。\nファイルパスを見直してください。",
                    "エラー", MB_OK | MB_ICONERROR);

        //m_Imageインスタンスを解放
        delete m_Image;
        m_Image = 0;

        return FALSE;
    }

さて、RTWEditorのUser.hとRTWEditorProc.hに行く前に、ワイド文字版REDIT.h等の今回アップデートした部分を解説します。

 

1.CTAB-指定ページのタイトルを取得するGetTitle(int)を追加
こちらは大した変更はなく、従来カレントページの未処理していた関数に引数を与えて非カレントページにも処理ができるようにしました。

 

//カレントページを設定する
bool CTAB::SetPage(int cur) {

    int ret = SendMessage(m_hWnd, TCM_SETCURSEL, cur, 0);
    if(ret == -1)
        return FALSE;
    else
        return TRUE;
}


//指定ページのタイトルを取得する
WCHAR* CTAB::GetTitle(int item) {

    if(item >= TabCount())
        return 0;
    static WCHAR Buff[MAX_PATH];    //ラベル文字列収容バッファ
    TCITEMW tcItem;                    //タブコントロール情報構造体
    tcItem.mask = TCIF_TEXT;
    tcItem.cchTextMax = MAX_PATH;
    tcItem.pszText = Buff;
    TabCtrl_GetItem(m_hWnd, item, &tcItem);
    return Buff;
}

 

2.CREDIT-文字列の検索、置換に関わるコードをRichEdit20に合わせて見直しを実施

それに比べるとCREDITクラスは「20年前のRICHED32.DLLから現在のRICHED20.DLLへ移行することによる見直し(注)」といえます。

注:RICHED32.DLLのVer 1.0とRICHED20.DLLのVer 2.0および3.0では、大きな仕様の相違があります。

 

(1)所謂ワイド文字対応

   変数がchar→WCHAR、定数が""→L""、関数が(関数名)(引数)→(関数名)W(引数)等の修正を行っています。

 

(2)コンストラクターのDLL読み込み

    当然以下のような変更を行っています。

    //リッチエディットコントロールDLLの読込み
    m_hRtLib = LoadLibraryW(L"RICHED20.DLL");
 

(3)ファイル入出力

    ワイド文字対応のリッチテキストコントロールは、

    ①リッチテキスト→ANSIとBOM無UTF-8対応

    ②テキスト→ANSI、BOM無UTF-8、BOM無UTF-16対応

になっています。

【RTWEditor】ECCSkeltonのCREDITクラスのファイル入出力関数と一応の完成

↑でも書きましたが、「BOM無UTF-16」は鬼っ子なのでBOMを付けることにして、次のようにしています。(なお、コールバック関数は変更ありません。)

 

//--------------------------------------------------------------------------
// リッチエディットコントロール用テキスト、リッチエディットファイル書込関数
// ファイル名 pszFileName のファイルが RichEdit Controlへ書きこまれる
//--------------------------------------------------------------------------
bool CREDIT::SaveFile(LPWSTR pszFileName) {

    EDITSTREAM eds;        //EDITSTREAM構造体
    bool nFileType;        //rtfかtxtかの判別フラグ
    if(StrStrW(pszFileName, L".rtf"))        nFileType = TRUE;    //rtfファイルとして
    else if(StrStrW(pszFileName, L".RTF"))    nFileType = TRUE;    //rtfファイルとして
    else                                    nFileType = FALSE;    //textファイルとして
    HANDLE hFile = CreateFile(pszFileName, GENERIC_WRITE, 0,
                        NULL, CREATE_ALWAYS,
                        FILE_ATTRIBUTE_NORMAL | FILE_FLAG_WRITE_THROUGH, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        MessageBoxW(m_hWnd, L"ファイルが開けませんでした", L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    eds.dwCookie = (DWORD)hFile;
    eds.dwError = 0;
    eds.pfnCallback = SaveRichEditProc;
    if(nFileType) {
        int ret = MessageBoxW(m_hWnd, L"UFT-8 RTFで保存しますか\n(「いいえ」はANSI RTF)", L"書き込み確認", MB_YESNO | MB_ICONQUESTION);
        if(ret == IDYES)
            SendMessageW(m_hWnd, EM_STREAMOUT, (WPARAM)(CP_UTF8 << 16) | SF_USECODEPAGE | SF_RTF, (LPARAM)&eds);
        else
            SendMessageW(m_hWnd, EM_STREAMOUT, (WPARAM)SF_RTF, (LPARAM)&eds);
    }
    else {
        int ret = MessageBoxW(m_hWnd, L"BOM付UFT-16で保存しますか\nBOM無UFT-8で保存しますか(「いいえ」)\n(「キャンセル」はANSI)", L"書き込み確認", MB_YESNOCANCEL | MB_ICONQUESTION);
        if(ret == IDYES) {
            //BOMの書き込み - WindowsはIntel系でUTF-16 FF FE(リトルエンディアン)
            BYTE BOM[2] = {0xFF, 0xFE};
            DWORD dwWritten;
            WriteFile(hFile, BOM, 2, &dwWritten, NULL);
            SendMessageW(m_hWnd, EM_STREAMOUT, (WPARAM)SF_TEXT | SF_UNICODE, (LPARAM)&eds);
        }
        else if(ret == IDNO)
            SendMessageW(m_hWnd, EM_STREAMOUT, (WPARAM)(CP_UTF8 << 16) | SF_USECODEPAGE | SF_TEXT, (LPARAM)&eds);
        else
            SendMessageW(m_hWnd, EM_STREAMOUT, (WPARAM)SF_TEXT, (LPARAM)&eds);
    }
    CloseHandle(hFile);
    SendMessageW(m_hWnd, EM_SETMODIFY, (WPARAM)FALSE, 0);
    SetFocus(m_hWnd);
    return TRUE;
}

//--------------------------------------------------------------------------
// リッチエディットコントロール用テキスト、リッチエディットファイル読込関数
// ファイル名 pszFileName のファイルが RichEdit Controlへ読みこまれる
//--------------------------------------------------------------------------
bool CREDIT::LoadFile(LPWSTR pszFileName) {

    EDITSTREAM eds;        //EDITSTREAM構造体
    bool nFileType;        //rtfかtxtかの判別フラグ
    if(StrStrW(pszFileName, L".rtf"))        nFileType = TRUE;    //rtfファイルとして
    else if(StrStrW(pszFileName, L".RTF"))    nFileType = TRUE;    //rtfファイルとして
    else                                    nFileType = FALSE;    //textファイルとして
    HANDLE hFile = CreateFile(pszFileName, GENERIC_READ, 0, NULL, OPEN_EXISTING,
                        FILE_ATTRIBUTE_NORMAL, NULL);
    if (hFile == INVALID_HANDLE_VALUE) {
        MessageBoxW(m_hWnd, L"ファイルが開けませんでした", L"エラー", MB_OK | MB_ICONERROR);
        return FALSE;
    }
    eds.dwCookie = (DWORD)hFile;
    eds.dwError = 0;
    eds.pfnCallback = ReadRichEditProc;
    if(nFileType) {
        int ret = MessageBoxW(m_hWnd, L"UFT-8 RTFで読み込みますか\n(「いいえ」はANSI RTF)", L"読み込み確認", MB_YESNO | MB_ICONQUESTION);
        if(ret == IDYES)
            SendMessageW(m_hWnd, EM_STREAMIN, (WPARAM)(CP_UTF8 << 16) | SF_USECODEPAGE | SF_RTF, (LPARAM)&eds);
        else
            SendMessageW(m_hWnd, EM_STREAMIN, (WPARAM)SF_RTF, (LPARAM)&eds);
    }
    else {
        int ret = MessageBoxW(m_hWnd, L"BOM付UFT-16で読み込みますか\nBOM無UFT-8にしますか(「いいえ」)\n(「キャンセル」はANSI)", L"読み込み確認", MB_YESNOCANCEL | MB_ICONQUESTION);
        if(ret == IDYES) {
            //BOMの読み込み - WindowsはIntel系でUTF-16 FF FE(リトルエンディアン)
            BYTE BOM[2];
            DWORD dwRead;
            ReadFile(hFile, BOM, 2, &dwRead, NULL);
            if(BOM[0] != 0xFF || BOM[1] != 0xFE) {
                MessageBoxW(m_hWnd, L"BOM付UFT-16ファィルではありません", L"エラー", MB_OK | MB_ICONERROR);
                CloseHandle(hFile);
                return FALSE;
            }
            SendMessageW(m_hWnd, EM_STREAMIN, (WPARAM)SF_TEXT | SF_UNICODE, (LPARAM)&eds);
        }
        else if(ret == IDNO)
            SendMessageW(m_hWnd, EM_STREAMIN, (WPARAM)(CP_UTF8 << 16) | SF_USECODEPAGE | SF_TEXT, (LPARAM)&eds);
        else
            SendMessageW(m_hWnd, EM_STREAMIN, (WPARAM)SF_TEXT, (LPARAM)&eds);
    }
    CloseHandle(hFile);
    SendMessageW(m_hWnd, EM_SETMODIFY, (WPARAM)FALSE, 0);
    SetFocus(m_hWnd);
    return TRUE;
}


3.文字列の検索・置換ダイアログ処理

コモンダイアログに文字列の検索ダイアログと文字列の置換ダイアログがあり、それぞれFINDREPLACE(W)構造体を使って、FindText関数とReplaceText関数で呼び出します。(注)

注:引数を設定する関数としてCREDITのSetFRDlg関数を使います。

//--------------------------------------
// 検索・置換用FINDREPLACE構造体の初期化
//--------------------------------------
FINDREPLACEW* CREDIT::SetFRDlg(WCHAR sign) {

    DWORD flag;
    switch(sign) {
    case L'F':
    case L'f':
        flag = FR_DOWN | FR_FINDNEXT;
        break;
    case L'R':
    case L'r':
        flag = FR_DOWN | FR_REPLACE;
        break;
    default :
        return 0;
    }

    static FINDREPLACEW fr;
    ZeroMemory(&fr, sizeof(FINDREPLACEW));
    fr.lStructSize = sizeof(FINDREPLACEW);
    fr.hwndOwner = m_hParent;
    fr.hInstance = NULL;
    fr.Flags = flag;
    fr.lpstrFindWhat = m_FindWhat;
    fr.lpstrReplaceWith = m_ReplaceWith;
    fr.wFindWhatLen = MAX_PATH;
    fr.wReplaceWithLen = MAX_PATH;
    fr.lCustData = NULL;
    fr.lpfnHook = NULL;
    fr.lpTemplateName = NULL;
    return &fr;
}

 

しかし、このダイアログはユーザーの選択した「次を検索(FR_FINDNEXT)」、「単語単位で探す(FR_WHOLEWORD)」、「大文字と小文字を区別する(FR_MATCHCASE)」、「上へ(FR_DOWN)」、「下へ(なし)」、「置換して次へ(FR_REPLACE)」、「全て置換(FR_REPLACEALL)」というメッセージをダイアログメッセージとして送るだけしかしません。

ということで、先ずはダイアログメッセージを送れるように、そのメッセージをOSに登録し、登録したメッセージをCREDITのメンバー変数m_frMsgに記録します。

 

    //検索・置換メーッセージの登録
    m_frMsg = RegisterWindowMessageW(FINDMSGSTRINGW);

 

次にダイアログメッセージをキャッチする為に、メインウィンドウのm_hDlg変数に検索ダイアログや文字列の置換ダイアログのモードレスダイアログのハンドルを設定します。(序にCREDITクラスのm_hFRDlgウィンドウハンドルにも設定しておきます。このモードレスダイアログハンドルは、メインウィンドウ(CMyWnd)のLoop関数内のIsDialogMessage関数で仕訳けられます。)

 

bool CMyWnd::OnFind() {

    m_hDlg = m_Edit.m_hFRDlg = FindTextW(m_Edit.SetFRDlg(L'F'));
    return TRUE;
}

bool CMyWnd::OnReplace() {

    m_hDlg = m_Edit.m_hFRDlg = ReplaceTextW(m_Edit.SetFRDlg(L'R'));
    return TRUE;
}

 

で、キャッチするにはOnOthers関数を使って、記録したm_frMsgを捕まえてCREDITクラスのFindReplace関数を呼び出します。

 

bool CMyWnd::OnOthers(UINT Msg, WPARAM wParam, LPARAM lParam) {

    //CREDITクラスは文字列検索・置換用にユーザーメッセージを作るので、
    //このような処理を行います。CREDITファイルを覗いてください。
    if(Msg == m_Edit.m_frMsg)
        m_Edit.FindReplace(lParam);    //検索・置換処理
}


FindReplace関数は、その下請けのWasFound関数を使って次のような処理をします。(注)

注:前に書いたように、BCCSkelton時代のリッチテキスト1.0ではFR_DOWNが何の意味も無かったのですが、リッチテキスト2.0からきちんと下方向検索、上方向検索を行えるようになりました。

 

//----------------
// 検索・置換関数
//----------------
bool CREDIT::WasFound(FINDTEXTW* ft, UINT flag) {

    return ((ft->chrg.cpMin = SendMessageW(m_hWnd, EM_FINDTEXTW,
                                flag, (LPARAM)ft)) != NOMORE);
}

WasFound
関数はFINDTEXT構造体ポインターの指すchrg.cpMinというキャラクターポインターに見つかった検索文字列の下位アドレスを代入して、「まだ検索できる(TRUE)」または「もうないよ(FALSE)」を返します。なお、「もうないよ」の場合、chrg.cpMinには-1(NOMORE)が代入されています。

//------------------------------
// 検索・置換ダイアログ処理関数
//------------------------------
void CREDIT::FindReplace(LPARAM lParam) {

    static int fcount = 0;    //検索回数のカウンター
    static int rcount = 0;    //置換回数のカウンター
    static int fc, rc;        //表示用のカウンター(上記が0に初期化されるので)
    static FINDTEXTW ft;    //FINDTEXTW構造体-メンバーにchrgとlpstrTextを持つ
    WCHAR FRMsg[MAX_PATH];    //出力メッセージ用文字配列
    LPFINDREPLACEW fr = (LPFINDREPLACEW)lParam;    //構造体FINDREPLACEWへのポインター
    //検索範囲の設定
    if(!fcount) {                                //初回だけ実行
        SendMessageW(m_hWnd, EM_EXGETSEL, 0,
                    (LPARAM)&ft.chrg);            //選択範囲を取得
        if(fr->Flags & FR_DOWN) {                //下方検索なら
            ft.chrg.cpMin = ft.chrg.cpMax;        //選択範囲の末尾(ft.chrg.cpMax)から
            ft.chrg.cpMax = -1;                    //文末までを検索
        }
        else                                    //上方検索なら選択範囲の先頭(ft.chrg.cpMin)から
            ft.chrg.cpMax = 0;                    //文頭迄(cpMax < Minでなければならない)を検索
    }
    //検索文字列の設定
    ft.lpstrText = m_FindWhat;
    //検索・置換処理
    if(fr->Flags & FR_DIALOGTERM) {        //検索・置換ダイアログが終了した場合
        fc = fcount;    rc = rcount;    //表示用に記録する
        m_hFRDlg = 0;                    //ハンドルを初期化
        fcount = 0;                        //検索カウンターを初期化
        rcount = 0;                        //置換カウンターを初期化
        SetFocus(m_hWnd);                //リッチエディットコントロールにフォーカスを当てる
    }
    else if(fr->Flags & FR_FINDNEXT) {    //「次を検索」の場合
        if(WasFound(&ft, (fr->Flags & FR_DOWN) | (fr->Flags & FR_MATCHCASE) | 
                    (fr->Flags & FR_WHOLEWORD))) {                //見つかった場合
            fcount++;                                            //回数を記録
            SendMessageW(m_hWnd, EM_SETSEL, ft.chrg.cpMin,
                        ft.chrg.cpMin + lstrlenW(m_FindWhat));    //検索文字列を選択
            SendMessageW(m_hWnd, EM_SCROLLCARET, 0, 0);            //キャレットの位置を表示する
            SetFocus(m_hWnd);            //リッチエディットコントロールにフォーカスを当てる
            if(fr->Flags & FR_DOWN)        //下方向検索の場合
                ft.chrg.cpMin += lstrlenW(m_FindWhat);            //次の検索開始位置に進める
        }
        else {                                                    //もう見つからない場合
            SendMessageW(m_hFRDlg, WM_CLOSE, 0, 0);                //検索・置換ダイアログを閉じる
            wsprintfW(FRMsg, L"検索文字列は %d回見つかりました", fc);
            MessageBoxW(m_hParent, FRMsg, L"メッセージ", MB_OK | MB_ICONINFORMATION);
        }
    }
    else if(fr->Flags & FR_REPLACE) {    //「置換して次に」の場合
        if(WasFound(&ft, (fr->Flags & FR_DOWN) | (fr->Flags & FR_MATCHCASE) | 
                    (fr->Flags & FR_WHOLEWORD))) {                //見つかった場合
            SendMessageW(m_hWnd, EM_SETSEL, ft.chrg.cpMin, ft.chrg.cpMin + lstrlenW(m_FindWhat));
            SendMessageW(m_hWnd, EM_SCROLLCARET, 0, 0);            //キャレット位置を表示する
            //メッセージボックスで確認して検索文字列を置換文字列に置換する
            if(MessageBoxW(m_hParent, L"置換しますか?", L"確  認",
                            MB_YESNO | MB_ICONQUESTION) == IDYES) {
                SendMessageW(m_hWnd, EM_REPLACESEL, TRUE, (LPARAM)m_ReplaceWith);
                rcount++;                                        //置換回数を記録
            }
            fcount++;                                            //回数を記録
            SendMessageW(m_hWnd, EM_SETSEL, ft.chrg.cpMin, ft.chrg.cpMin + lstrlenW(m_ReplaceWith));
            SendMessageW(m_hWnd, EM_SCROLLCARET, 0, 0);            //キャレット位置を表示する
            SetFocus(m_hWnd);            //リッチエディットコントロールにフォーカスを当てる
            if(fr->Flags & FR_DOWN)        //下方向検索の場合
                ft.chrg.cpMin += lstrlenW(m_ReplaceWith);        //次の検索開始位置に進める
        }
        else {                                                    //もう見つからない場合
            SendMessageW(m_hFRDlg, WM_CLOSE, 0, 0);                //検索・置換ダイアログを閉じる
            wsprintfW(FRMsg, L"検索文字列を %d検索し、%d回置換しました", fc, rc);
            MessageBoxW(m_hParent, FRMsg, L"メッセージ", MB_OK | MB_ICONINFORMATION);
        }
    }
    else if(fr->Flags & FR_REPLACEALL) {//「すべて置換」の場合
        ft.chrg.cpMin = 0;        //検索開始位置を文頭とし、
        ft.chrg.cpMax = -1;        //検索終了位置を文末とする
        while(WasFound(&ft, (fr->Flags & FR_DOWN) | (fr->Flags & FR_MATCHCASE) |
                    (fr->Flags & FR_WHOLEWORD))) {
            fcount++;                                            //回数を記録
            SendMessageW(m_hWnd, EM_SETSEL, ft.chrg.cpMin, ft.chrg.cpMin + lstrlen(m_FindWhat));
            SendMessageW(m_hWnd, EM_SCROLLCARET, 0, 0);            //キャレット位置を表示する
            SendMessageW(m_hWnd, EM_REPLACESEL, TRUE, (LPARAM)m_ReplaceWith);
            rcount++;                                            //置換回数を記録
            if(fr->Flags & FR_DOWN)        //下方向検索の場合
                ft.chrg.cpMin += lstrlenW(m_ReplaceWith);        //次の検索開始位置に進める
        }
        SendMessageW(m_hFRDlg, WM_CLOSE, 0, 0);                    //検索・置換ダイアログを閉じる
        wsprintfW(FRMsg, L"検索文字列を %d回置換しました", rc);
        MessageBoxW(m_hParent, FRMsg, L"メッセージ", MB_OK | MB_ICONINFORMATION);
    }
    SetFocus(m_hWnd);        //リッチエディットコントロールにフォーカスを当てる
}

 

FindReplace関数は可也コメントを書いているので何をやっているのかよく分かると思います。BCCSkeltonでは、リッチテキスト1.0がFR_DOWNがあっても無くても下方検索しかしないので、独自に「下方検索方法で上方検索を行う」ようにしていましたが(注)、きちんと動くようになったのでコードもスッキリしました。

注:その為に移植した直後、検索・置換動作がおかしくなりましたが...汗笑;

 

将に20年の埃を払って大掃除をした気分でした。

 

前回、ほとんど話題のなかったRTWEditor.rcとResRTWEditor.hでしたが、今回はコンパイルの対象となるRTWEditor.cppと基本的な定義ファイルであるRTWEditor.hをやります。なお、本プログラムはこの他メインウィンドウとダイアログのメンバー関数の実装を行うRTWEditorProc.hの他、User.hと今回移植のために大幅に修正したCREDIT.hの当該部分も紹介したいと考えています。

 

また、これらのファイルは基本的にBCCForm and BCCSkeltonのSkeltonWizardで生成したコードを基に修正したもので、RTWEditor作成の際のSkeltonWizardへの入力は次のようになります。

第1ページ:SDIウィンドウ、ツールバー、ステータスバー、コモンダイアログ、メニュー、アイコン、アクセラレーターを選択

第2ページ:コモンコントロールのスタンダードビットマップを使用

第3ページ:ウィンドウメッセージは、WM_CREATE、WM_NOTIFY、WM_SIZE、WM_CLOSEを選択(重要

注:ECCSkeltonではこれらSkeltonWizardで選択できるメッセージの他、OnDropFiles、OnGetMaxInfo等を仮想関数として有し、そのままメンバー関数に使用できます。また、それ以外のメッセージに対しては「bool OnOthers(UINT Msg, WPARAM wParam, LPARAM lParam);    //既定のメッセージ以外の処理」を利用し、"if(Msg == ~)"や"switch(Msg)"で処理するようにしています。今回のRTWEditorではCREDITのFindReplaceダイアログのメッセージ処理で実際に使っていますので参考にしてください。

 

【RTWEditor.cpp】

//////////////////////////////////////////
// RTWEditor.cpp
//Copyright (c) 08/30/2022 by ECCSkelton
//////////////////////////////////////////
#include    "RTWEditor.h"
#include    "User.h"    //(解説:User.hは.hとProc.hの間に挟むように入れてください。)
#include    "RTWEditorProc.h"

////////////////
// WinMain関数
////////////////
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    LPWSTR lpCmdLine, int nCmdShow) {
//(解説:ワイド文字対応部分です。bcc102cでコンパイルする際には "-tWU" スイッチを使ってください。)

    //ファイルドロップによる起動(g_ByFileフラグを立てる)
    if(g_Arg.c() > 1)
        g_ByFile = TRUE;
//(解説:データファイルのドロップによる起動用です。なお、SkeltonWizardが書く「2重起動防止用コード」は2重起動を容認するので、今回外しています。)

    //ウィンドウ登録 - Init(ClassName, hInstance, "IDM_MAIN",
    //                        (以下省略可)MAKEINTRESOURCE(IDI_ICON), IDC_ARROW, Brush)
    RTWEditor.Init(L"MainWnd", hInstance, L"MAIN_MENU", MAKEINTRESOURCE(IDI_ICON));
//(解説:BCCSkeltonではhInstanceの後に外部コールバック関数が引数になっていましたが、ECCSkeltonでは不要です。)

    //ウィンドウ作成と表示-Create(WindowTitle, (以下省略可)Style,
    //                        ExStyle, hParent, hMenu, x, y, w, h)
    if(!RTWEditor.Create(L"RTWEditor"))
        return 0L;

    //アクセラレーター登録-InclAccel("(Accelerator ID)")
    RTWEditor.InclAccel(L"IDA_ACCEL");
//(解説:今回はアクセラレーターを使うのでこのようになっています。)

    //メッセージループに入る
    return RTWEditor.Loop();
//解説:(注参照)
}

/*注:このループ処理の中にはアクセラレータ処理の他、m_hDlgメンバー関数をハンドルとするモードレスダイアログのメッセージ処理も含まれます。RTWEditorではCREDITクラスのFindReplace関数を呼ぶために使用しています。

 

//メインウィンドウの場合のメッセージループ
UINT CSDI::Loop() {

    while(GetMessageW(&m_Msg, NULL, 0, 0)) {
    //
アクセラレータモードレスダイアログのメッセージでなければ

        //モードレスダイアログがある場合
        
if(!IsDialogMessageW(m_hDlg, &m_Msg) &&
        //アクセラレーター処理
            
!TranslateAcceleratorW(m_hWnd, m_Accel, &m_Msg)) {
            TranslateMessage(&m_Msg);
            DispatchMessageW(&m_Msg);
        }
    }
    return m_Msg.wParam;
}

*/

 

【RTWEditor.h】

//////////////////////////////////////////
// RTWEditor.h
// Copyright (c) 08/30/2022 by ECCSkelton
//////////////////////////////////////////

//システム定数定義
#define    IDC_TAB        1000    //タブのコントロールID
#define    IDC_EDIT    2000    //最初のタブページのエディットコントロールID

//(解説:プログラムで生成するメインウィンドウに張り付けるタブとリッチテキストコントロール用です。)

//最小ウィンドウサイズの規定(ツールバーボタンが全て見える最小値)

#define        MINW    510
#define        MINH    255

//(解説:OnGetMaxInfoを使っています。サイズは「ツールバーボタンが全て見える最小値」としています。)

//対象ファイルのフィルター
#define        FILEFILTER    
L"テキストファイル(*.rtf;*.txt)\0*.rtf;*.txt\0C++ファイル(*.h;*.cpp;*.rc;*.mak;*.ini)\0*.h;*.cpp;*.rc;*.mak;*.ini\0全てのファイル(*.*)\0*.*\0\0"
//(解説:CCHKEXTクラスを使っています。ワイド文字対応は、""文字列の直前のスペース付き「 "」を「 L"」へ一括変換すると効率的です。)

//BCCSkeltonのヘッダー-これに必要なヘッダーが入っている
#include    "ECCSkelton.h"
//リソースIDのヘッダー
#include    "ResRTWEditor.h"

 

/////////////////////////////////////////////////////////////////////
//CMyWndクラスをCSDIクラスから派生させ、メッセージ用の関数を宣言する
/////////////////////////////////////////////////////////////////////
class CMyWnd : public CSDI

//(解説:CSDIクラスからCMyWndクラスを派生させます。)

{
private:
    CTAB    m_Tab;                    //メインウィンドウに貼り付けるタブコントロール
    CREDIT    m_Edit;                    //タブページのRichEditコントロールのカプセルインスタンス
    int     m_EditID = IDC_EDIT;    //新規タブページのリッチエディット用コントロールID(注)
                                    //注:ユニークである為にコントロールを削除しても永久欠番にする

//(解説:タブとリッチテキストコントロールのインスタンスをメンバーとして持ちます。各タブに一つのリッチテキストコントロールが付くので、タブを消去するとそのリッチエディットコントロールも消去されます。その際、「消去されるコントロールのIDは使いまわししない」とすることで不測の重複トラブルを避けています。)

public:    //以下はコールバック関数マクロと関連している
    //2重起動防止用のMutex用ID名称
    CMyWnd(WCHAR* UName) : CSDI(UName) {}

//(解説:二重起動防止用のコンストラクターですが、二重起動は容認しています。charになっていたものですが、"char"を"WCHAR"に一括変換して修正します。)

    //メニュー項目、ダイアログコントロール関連
    bool OnNew();
    bool OnOpen();
    bool OnSave();
    bool OnSaveas();
    bool OnSetprint();
    bool OnPrint();
    bool OnExit();
    bool OnUndo();
    bool OnCut();
    bool OnCopy();
    bool OnPaste();
    bool OnSelall();
    bool OnFind();
    bool OnReplace();
    bool OnDelete();
    bool OnFont();
    bool OnLeft();
    bool OnCenter();
    bool OnRight();
    bool OnLindent();
    bool OnLoutdent();
    bool OnRindent();
    bool OnRoutdent();
    bool OnBrief();
    bool OnShortcut();
    bool OnVer();

//(解説:これらメニューやコントロールからの割り込みメッセージはWM_COMMANDに対応する、OnCommand関数で処理します。)

   //ウィンドウメッセージ関連
    CMDTABLE    //CMyWndクラスのOnCommand()関数宣言

//(解説:このCMDTABLEという文字列が"bool OnCommand(WPARAM, LPARAM);"に展開されます。これは手書きで追加します。)

    bool OnCreate(WPARAM, LPARAM);
    bool OnNotify(WPARAM, LPARAM);
    bool OnSize(WPARAM, LPARAM);
    bool OnClose(WPARAM, LPARAM);
    bool OnDropFiles(WPARAM, LPARAM);
    bool OnGetMaxInfo(WPARAM, LPARAM);
    bool OnOthers(UINT, WPARAM, LPARAM);    //既定のメッセージ以外の処理

//(解説:これらウィザードが作った関数は既にCSDIクラスに仮想関数として定義済ですので、自由にオーバーライドできます。)

    //ユーザー定義関数
    bool AddTabEdit(WCHAR*);
    bool DelTabEdit();
    bool PopupMenu();

//(解説:これらは純粋なユーザー定義メンバー関数で、手書きで追加します。順にタブとリッチテキストコントロールの追加、削除とポップアップメニュ処理の関数です。)

};

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

//(解説:「 "」の「 L"」一括変換で修正します。)


//CMyWndクラスのOnCommand()関数定義

/*(解説:BCCSkeltonではのようになっていましたが、ECCDSkeltonではBIGIN_CMDTABLE(クラス名)~END_CMDTABLEに一本化されました。)

BEGIN_MDIMSG(RTEditor)    //ダイアログと違い、コールバック関数名を特定しない
    //メニュー項目、ダイアログコントロール関連
    ON_COMMAND(RTEditor, IDM_NEW, OnNew())
(略)

    //ウィンドウメッセージ関連
    //MDIFrame親ウィンドウは自動的にOnMDIFrameCreate()を呼ぶ
(略)

    ON_CLOSE(RTEditor)
    ON_(RTEditor, WM_DROPFILES, OnDropFiles(wParam))

END_MDIMSG(RTEditor)
*/

BEGIN_CMDTABLE(CMyWnd)
    //メニュー項目、ダイアログコントロール関連
    ON(IDM_NEW, OnNew())
    ON(IDM_OPEN, OnOpen())
    ON(IDM_SAVE, OnSave())
    ON(IDM_SAVEAS, OnSaveas())
    ON(IDM_SETPRINT, OnSetprint())
    ON(IDM_PRINT, OnPrint())
    ON(IDM_EXIT, OnExit())
    ON(IDM_UNDO, OnUndo())
    ON(IDM_CUT, OnCut())
    ON(IDM_COPY, OnCopy())
    ON(IDM_PASTE, OnPaste())
    ON(IDM_SELALL, OnSelall())
    ON(IDM_FIND, OnFind())
    ON(IDM_REPLACE, OnReplace())
    ON(IDM_DELETE, OnDelete())
    ON(IDM_FONT, OnFont())
    ON(IDM_LEFT, OnLeft())
    ON(IDM_CENTER, OnCenter())
    ON(IDM_RIGHT, OnRight())
    ON(IDM_LINDENT, OnLindent())
    ON(IDM_LOUTDENT, OnLoutdent())
    ON(IDM_RINDENT, OnRindent())
    ON(IDM_ROUTDENT, OnRoutdent())
    ON(IDM_BRIEF, OnBrief())
    ON(IDM_SHORTCUT, OnShortcut())
    ON(IDM_VER, OnVer())

/*(解説:SkeltonWizardでは↓の(1)の様になっているので、

    (1)ON_COMMAND(RTEditor, IDM_NEW, OnNew())

    (2)ON(IDM_NEW, OnNew())

    (2)にするために

    "_COMMAND(RTEditor, " を

    ↓

    "("

    に一括変換します。)

*/

END_CMDTABLE


///////////////////
//ツールバーの作成
///////////////////
CTBAR TBar;

///////////////////////
//ステータスバーの作成
///////////////////////
CSBAR SBar;

////////////////////////
//コモンダイアログの作成
////////////////////////
CMNDLG cmndlg;

//////////////////////
//CEXTCHKのインスタンス
//////////////////////
CEXTCHK g_ExtChk;

//(解説:ここら辺は全くBCCSkeltonのままです。)


///////////////////////////////////////////
// CDLGクラスからVERSIONDLGクラスを派生
// 複数の同一ダイアログ変数とダイアログも作
// れるが、一つのダイアログに一つの派生ダイ
// アログクラスを作成するのが基本
///////////////////////////////////////////
class VERSIONDLG : public CDLG {
public:
    CMDTABLE    //VERSIONDLGクラスのOnCommand()関数宣言
    bool OnIdok();
};

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

//VERSIONDLGクラスのOnCommand()関数定義
BEGIN_CMDTABLE(VERSIONDLG)
    //メニュー項目、ダイアログコントロール関連
    ON(IDOK, OnIdok())
END_CMDTABLE

//(解説:メインウィンドウのCMyWndクラスと全く同じ様にVERSIONDLGクラスを修正します。)

 

如何でしょうか?最初は慣れないので面倒くさそうですが(私もそうでした)、慣れると一括変換を利用して3分程度で修正完了できます。

 

では、次回は今回のRTWEditorでいじったCTABとCREDIT両クラスの変更点を解説してみましょう。(これらはBCCSkeltonでも修正しています。以下はBCCSkeltonのREADME.txtからです。)

 

CTAB-指定ページのタイトルを取得するGetTitle(int)を追加
CREDIT-文字列の検索、置換に関わるコードをRichEdit20に合わせて見直しを実施

 

今日またちょっと修正しましたが、基本型が完成したのでまたリソースからECCSkeltonによる、RTWEditorのプログラミングを解説します。

 

ワイド文字(UTF-16)を基本とするECCSkeltonのプログラミングは、基本的にShift-JISベースのBCCSkeltonのそれと変わりなく、同じツールを使用し、後にコードを修正してゆきます。

 

今回のRTWEditorは、既に公開しているBCCSkeltonのMDIベース(CMDI)のリッチテキストエディターを踏襲した、タブ(CTAB)ベースのリッチテキストエディターです。使用するECCSkeltonのクラスは以下の通りです。

(1)CSDI-メインウィンドウでメインメニュー、アイコンを備えます。

(2)CTBAR-そのツールバーです。

(3)CSBAR-そのステータスバーです。

(4)CTAB-メインウィンドウのクライアントエリアに張り付けてその管理を行います。

(5)CREDIT-リッチエディットコントロールを夫々のタブページに張り付けるのでその管理用に使います。

(6)CDLG-バージョンダイアログ用です。

(7)CMNDLG-「ファイルを開く」、「名前を付けて保存」のコモンダイアログダイアログを使います。

(8)CARG-ファイルドロップによる起動の際に利用します。

(9)CSTR-文字列処理で使います。

 

やはりリソースからプログラミングするので、BCCSkeltonのRTEditorのリソースを再利用していますが、任意のタブを閉じる処理があるので、

(1)MENUITEM L"ファイルを閉じる(&D)", IDM_DELETE
(2)ツールバービットマップに↑の為の「✖印」を追加しています。(注)

注:RTEditorでは標準ツールバービットマップとカスタムツールバービットマップを併用していますので、RTWEditorも同様に併用しています。

 

では、以下にRTWEditor.rcファイルを載せます。(ResRTWEditor.hは#defineでの値設定だけなので、省略します。)

【RTWEditor.rc】

//-----------------------------------------
//             BCCForm Ver 2.4
//    An Easy Resource Editor for BCC
//  Copyright (c) February 2002 by ysama
//-----------------------------------------
#include    "ResRTWEditor.h"    //(解説:IDの値を#defineで設定するヘッダーファイル(省略))

//----------------------------------
// ダイアログ (IDD_VERSION)
//----------------------------------
IDD_VERSION DIALOG DISCARDABLE 0, 0, 160, 40
EXSTYLE WS_EX_DLGMODALFRAME
STYLE WS_POPUP | WS_VISIBLE | WS_CAPTION | DS_MODALFRAME | DS_3DLOOK | DS_CENTER
CAPTION L"バージョン情報"
FONT 9, L"Times New Roman"
{
 CONTROL IDI_ICON, 0, L"STATIC", WS_CHILD | WS_VISIBLE | SS_ICON, 8, 8, 32, 32
 CONTROL L"RTWEditor Verson 1.0\nCopyright 2022 By Ysama", IDC_VERTXT, L"STATIC", SS_CENTER | SS_SUNKEN | WS_CHILD | WS_VISIBLE, 32, 8, 80, 24
 CONTROL L"OK", IDOK, L"BUTTON", BS_DEFPUSHBUTTON | BS_CENTER | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 128, 10, 20, 13
}
//(解説:これはRTEditorと変わりありません。)

//-------------------------
// メニュー(MAIN_MENU)
//-------------------------
MAIN_MENU MENU DISCARDABLE
{
    POPUP L"ファイル(&F)"
    {
        MENUITEM L"新規作成(&N)", IDM_NEW
        MENUITEM L"ファイルを開く(&O)", IDM_OPEN
        MENUITEM L"上書き保存(&S)", IDM_SAVE, GRAYED
        MENUITEM L"名前をつけて保存(&A)", IDM_SAVEAS, GRAYED
        MENUITEM SEPARATOR
        MENUITEM L"プリンター設定(&T)", IDM_SETPRINT
        MENUITEM L"印刷(&P)", IDM_PRINT, GRAYED
        MENUITEM SEPARATOR
        MENUITEM L"終了(&X)", IDM_EXIT
    }
    POPUP L"編集(&E)", GRAYED
    {
        MENUITEM L"元に戻す(&U)", IDM_UNDO
        MENUITEM SEPARATOR
        MENUITEM L"切り取り(&T)", IDM_CUT
        MENUITEM L"コピー(&C)", IDM_COPY
        MENUITEM L"貼り付け(&P)", IDM_PASTE
        MENUITEM SEPARATOR
        MENUITEM L"すべて選択(&A)", IDM_SELALL
        MENUITEM SEPARATOR
        MENUITEM L"文字列の検索(&F)", IDM_FIND
        MENUITEM L"文字列の置換(&R)", IDM_REPLACE
        MENUITEM SEPARATOR
        MENUITEM L"ファイルを閉じる(&D)", IDM_DELETE
//(解説:これだけ追加しました。)

    }
    POPUP L"書式(&F)", GRAYED
    {
        MENUITEM L"フォント設定(&F)", IDM_FONT
        POPUP L"文字寄せ(&A)"
        {
            MENUITEM L"左寄せ(&L)", IDM_LEFT
            MENUITEM L"センタリング(&C)", IDM_CENTER
            MENUITEM L"右寄せ(&R)", IDM_RIGHT
        }
        POPUP L"インデント(&I)"
        {
            MENUITEM L"左インデント(&I)", IDM_LINDENT
            MENUITEM L"左インデント解除(&O)", IDM_LOUTDENT
            MENUITEM L"右インデント(&I)", IDM_RINDENT
            MENUITEM L"右インデント解除(&O)", IDM_ROUTDENT
        }
    }
//(解説:逆にMDIベースのプログラムでは標準で付ける↓は削除しています。

/*

    POPUP "ウィンドウ(&W)", GRAYED
    {
        MENUITEM "重ねて並べる(&C)", IDM_CASCADE
        MENUITEM "縦に並べる(&V)", IDM_TILEHORZ
        MENUITEM "横に並べる(&H)", IDM_TILEVERT
        MENUITEM "アイコンの整列(&I)", IDM_ARRANGE
    }

*/

    POPUP L"ヘルプ(&H)"
    {
        MENUITEM L"簡単な説明(&B)", IDM_BRIEF
        MENUITEM L"ショートカットキー(&S)", IDM_SHORTCUT
        MENUITEM L"バージョン(&V)", IDM_VER
    }
}

//--------------------------
// イメージ(IDI_CUSTOM)
//--------------------------
IDI_CUSTOM    BITMAP    DISCARDABLE    "Custom.bmp"
//(解説:ツールバーのカスタムビットマップです。L""にしないので注意!)

 

//--------------------------
// イメージ(IDI_DOC)
//--------------------------
IDI_DOC    BITMAP    DISCARDABLE    "Doc.bmp"
//(解説:タブのタグに表示する「文書」を表すビットマップです。L""にしないので注意!)

//--------------------------
// イメージ(IDI_ICON)
//--------------------------
IDI_ICON    ICON    DISCARDABLE    "Icon.ico"
//(解説:システムアイコンです。L""にしないので注意!)

//--------------------------------
// アクセラレーター (IDA_ACCEL)
//--------------------------------
IDA_ACCEL ACCELERATORS DISCARDABLE
{
 VK_C, IDM_COPY, VIRTKEY, CONTROL, NOINVERT
 VK_F, IDM_FIND, VIRTKEY, CONTROL, NOINVERT
 VK_N, IDM_NEW, VIRTKEY, CONTROL, NOINVERT
 VK_O, IDM_OPEN, VIRTKEY, CONTROL, NOINVERT
 VK_P, IDM_PRINT, VIRTKEY, CONTROL, NOINVERT
 VK_R, IDM_REPLACE, VIRTKEY, CONTROL, NOINVERT
 VK_S, IDM_SAVE, VIRTKEY, CONTROL, NOINVERT
 VK_T, IDM_CUT, VIRTKEY, CONTROL, NOINVERT
 VK_V, IDM_PASTE, VIRTKEY, CONTROL, NOINVERT
 VK_X, IDM_EXIT, VIRTKEY, CONTROL, NOINVERT
 VK_U, IDM_UNDO, VIRTKEY, CONTROL, NOINVERT
 VK_F1, IDM_SHORTCUT, VIRTKEY, NOINVERT
 VK_F2, IDM_VER, VIRTKEY, NOINVERT
}
//(解説:珍しくこのプログラムではBCCFormでアクセラレーターを追加しています。)

//--------------------
// ストリングテーブル
//--------------------
STRINGTABLE DISCARDABLE
{
 String_00, L"新規作成"
 String_01, L"ファイルを開く"
 String_02, L"上書き保存"
 String_03, L"プリンター設定"
 String_04, L"印刷"
 String_05, L"元に戻す"
 String_06, L"切り取り"
 String_07, L"コピー"
 String_08, L"貼り付け"
 String_09, L"文字列の検索"
 String_10, L"文字列の置換"
 String_11, L"ファイルを閉じる"
 String_12, L"左寄せ"
 String_13, L"センタリング"
 String_14, L"右寄せ"
 String_15, L"フォント設定"
 String_16, L"左インデント"
 String_17, L"左インデント解除"
 String_18, L"右インデント"
 String_19, L"右インデント解除"
}

//(解説:また、ツールバーボタンのツールチップ用にBCCFormでつくったストリングテーブルを使っています。)

 

BCCSkeltonからECCSkeltonへの移植での注意としては、ANSI文字列(char)ワイド文字列(WCHAR)になるので、文字定数は"(文字列)"ではなく、L"(文字列)"と表記することが必要です。(しかしイメージリソースのファイル指定では、"(ファイルパス、名)"のままでないと正しくコンパイルされないので、注意が必要です。)

 

では、次回はRTWEditor.cppとRTWEditor.hファイルを解説します。