前回RichEditorのエディター本体部分を三連発(冒頭~メニュー、ツールバー、メソッド実装部分)してしまい、「見たくもないっ!」と退かれてしまったかも?

 

今回はまずC#でのコントロールの作り方概論を述べ、今回のRichTextBoxExの場合について説明した後、コードを紹介することにします。

 

1.C#でのコントロールの作り方概論

Windows SDKのみ()でコントロールを作ろうと思ったことが有る方なら、ダミーの親の下でウィンドウアプリとして完動するプログラムを作り、それをDLL化(これがC++だとまた面倒です)するので「はぁ~」となっちゃいますね。

:基本的にC/C++で書かれた、Windows上のアプリケーション開発に必要なAPIヘッダー、その他関連ヘッダー、ライブラリ、サンプルコード、ツール等一式の「ソフトウェア開発キット(Software Development Kit)」のこと。

 

私も昔BCCSkeltonを作っていて、ウィンドウ描画用のCANVASクラスを使って自由に描画が出来る「描画コントロール」のようなものが欲しくて、BCCSkeltonのクラスとしてCPICBOX(Picture Boxのつもり)クラスを力技で作りました。(それがECCSkeltonを作る際に役立ちましたが...)

 

何れにしても大変!

 

ということです。ところが、

 

C#なら既存コントロールを派生させてカスタムコントロールを作り、それを「ライブラリーとしてコンパイル("/target:library" というスイッチを付けるだけ)」するだけで出来あがり、後は"using (DLLの名前);"と宣言するだけで簡単にDLLやカスタムコントロールが利用できます。

 

なーんて簡単で便利なんでしょう?

 

過去にも最も汎用的なFormから派生させてFileListクラス、PictureBoxからバイオリズムコントロール(BrmBoxクラス)、 PanelからMazeクラス等々、数々のコントロールを作ったりしていますので、

 

C#でプログラムを組んだら、後で再利用できるようにDLL化を考えた方が良い

 

というのが私の開発方針となっています。

 

2.RichTextBoxExの場合

RichTextBoxはコモンダイアログで文字列の検索も置換できない、印刷ダイアログ以前の問題で印刷自体が出来ない、という二重苦を解消するために努力するので、それは纏めて「リッチテキストボックス改(RichTextBoxExの意味です)」というコントロールにする、という方針は当たり前のものでした。方法は簡単で、

 

RichTextBoxからRichTextBoxExクラスを派生させ、文字列の検索メソッドと

 

文字列の置換メソッド、及び

 

印刷メソッド(ページ設定と印刷設定)

 

を追加するだけなんです。

 

3.コード紹介(richTextBoxEx.cs)

それでは、↑に書いたことを踏まえていつも通りの解説を行ってゆきます。

 

////////////////////////////////////////////
// RichTextBoxExクラス
// Copyright (c) 2025 by Ysama
// 概要:RichTextBoxクラスに文字列検索・置換
//         および印刷機能を追加
// 参考:https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.richtextbox?view=windowsdesktop-8.0
//         https://learn.microsoft.com/ja-jp/answers/questions/1515046/c-rtf
////////////////////////////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Printing;            //RichTextBoxEx用
using System.Runtime.InteropServices;    //RichTextBoxEx用

namespace richTextBoxEx
{
    public class RichTextBoxEx : RichTextBox    //解説:これがC#(やC++等)での「クラス承継」の書法です。
    {
        //Win32関連    //解説:C#だけでは書けないので、Win32 APIを利用します。
        [StructLayout(LayoutKind.Sequential)]    //解説:以下はWin32 SDKのC++で書かれた構造体定義をC#に導入しています。
        private struct STRUCT_RECT            //Win32 矩形構造体
        {
            public Int32 left;
            public Int32 top;
            public Int32 right;
            public Int32 bottom;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct STRUCT_CHARRANGE        //Win32 開始文字と終了文字による文字列範囲構造体
        {
            public Int32 cpMin;
            public Int32 cpMax;
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct STRUCT_FORMATRANGE    //Win32 リッチエディットコントロール用印刷範囲構造体
        {
            public IntPtr hdc;
            public IntPtr hdcTarget;
            public STRUCT_RECT rc;
            public STRUCT_RECT rcPage;
            public STRUCT_CHARRANGE chrg;
        }

        //解説:以下ではWin32 APIの関数をC#に導入しています。
        [DllImport("user32.dll")]
        private static extern Int32 SendMessage(IntPtr hWnd, Int32 msg, Int32 wParam, IntPtr lParam);
        private const Int32 EM_FORMATRANGE = 0x439;    //(WM_USER + 57)

        //メンバーフィールド
        private int FirstCharOnPage;    //印刷時の各ページの最初の文字のインデックス番号
        int pageNum;                    //印刷用ページ番号

        //印刷
        public void Print()    //解説:"(RichTextBoxExインスタンス).Print();"とするだけでダイアログが出て印刷できます。
        {
            //印刷用クラスインスタンス
            PrintDocument DocToPrint = new PrintDocument();        //印刷終了時に必ずDispose()が必要
            PrinterSettings PrinterToSet = DocToPrint.PrinterSettings;
            //デフォルト用紙設定(PrinterSettings、PageSettingsもデフォルトサイズはLetter)
            foreach (PaperSize ps in PrinterToSet.PaperSizes)    //A4サイズを探して設定する
            {
                if(ps.Kind == PaperKind.A4)    //解説:変更、削除可)
                {
                    PrinterToSet.DefaultPageSettings.PaperSize = ps;
                    break;
                }
            }

            //デフォルトプリンターに設定(解説:変更、削除可)
            DocToPrint.PrinterSettings.PrinterName = "Microsoft Print to PDF";
            //ページ設定ダイアログのインスタンス作成と設定
            PageSetupDialog psDlg = new PageSetupDialog();
            psDlg.Document = DocToPrint;
            psDlg.PageSettings = PrinterToSet.DefaultPageSettings;
            psDlg.AllowPrinter = true;
            psDlg.ShowNetwork = false;        //ネットワークプリンターは無視する(解説:変更、削除可)

            //ページ設定ダイアログウィ表示し、OKボタンで先に進む

            if(psDlg.ShowDialog() == DialogResult.OK)
            {
                //PrintDialogの作成
                PrintDialog prDlg = new PrintDialog();
                //印刷範囲の指定オプションの設定
                prDlg.AllowSelection = true;
                prDlg.AllowSomePages = true;
                prDlg.ShowHelp = false;        //「ヘルプ」ボタンは不使用
                prDlg.PrinterSettings = PrinterToSet;
                //PrintDIalogのDocumentプロパティにセットする
                prDlg.Document = DocToPrint;
                //印刷ダイアログを表示し、OKボタンで印刷を行う
                if(prDlg.ShowDialog() == DialogResult.OK)
                {
                    DocToPrint.PrinterSettings = prDlg.PrinterSettings;
                    PrintRichText(DocToPrint);
                }
                //オブジェクトを破棄する
                prDlg.Dispose();
            }
            else
                MessageBox.Show("設定がキャンセルされました。", "確認", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            //オブジェクトを破棄する
            psDlg.Dispose();
            DocToPrint.Dispose();
        }

        //RichTextBoxの印刷メソッド
        private void PrintRichText(PrintDocument printDoc)
        {
            //Print()メソッド時、最初のページが印刷される前に呼び出される
            printDoc.BeginPrint += OnBeginPrint;
            //印刷時、ページの印刷前に呼び出される
            printDoc.PrintPage += OnPrintPage;
            try
            {
                printDoc.Print();
            }
            finally
            {
                printDoc.BeginPrint -= OnBeginPrint;
                printDoc.PrintPage -= OnPrintPage;
            }
        }

        //Print()メソッド時、最初のページが印刷される前に呼び出される
        private void OnBeginPrint(object sender, PrintEventArgs e)
        {
            FirstCharOnPage = 0;    //印刷開始時に先頭文字をセットする
            pageNum = 1;            //ページ1から始める
        }

        private void OnPrintPage(object sender, PrintPageEventArgs e)
        {
            //FormatRangeメソッドにより次のページの先頭文字を求める
            FirstCharOnPage = this.FormatRange(false, e, FirstCharOnPage, this.TextLength);
            //次のページの先頭文字がテキストの長さより大きいか否かをe.HasMorePagesに設定
            e.HasMorePages = (FirstCharOnPage < this.TextLength);
            pageNum++;                //ページ番号をインクリメント
        }

        private int FormatRange(bool measureOnly, PrintPageEventArgs e, int charFrom, int charTo)
        {    //参考:https://learn.microsoft.com/ja-jp/windows/win32/api/richedit/ns-richedit-formatrange
            //分かり易い解説:http://www.kumei.ne.jp/c_lang/sdk2/sdk_136.htm
            //Twip(Twentieth of an Inch Point)とは1/20ポイントの長さの単位(1ポイントが1/72インチなので1/1440インチに相当)

            const double TWIPS_PER_HUNDREDTH_INCH = 14.4;    //0.01インチに相当
            //開始文字から終了文字までの選択範囲構造体(Win32)
            STRUCT_CHARRANGE cr;
            cr.cpMin = charFrom;
            cr.cpMax = charTo;
            //表示範囲の矩形構造体(Win32)
            STRUCT_RECT rc;
            rc.top = (int)(TWIPS_PER_HUNDREDTH_INCH * (double)e.MarginBounds.Top);
            rc.bottom = (int)(TWIPS_PER_HUNDREDTH_INCH * (double)e.MarginBounds.Bottom);
            rc.left = (int)(TWIPS_PER_HUNDREDTH_INCH * (double)e.MarginBounds.Left);
            rc.right = (int)(TWIPS_PER_HUNDREDTH_INCH * (double)e.MarginBounds.Right);
            //ページの矩形構造体(Win32)
            STRUCT_RECT rcPage;
            rcPage.top = (int)(TWIPS_PER_HUNDREDTH_INCH * (double)e.PageBounds.Top);
            rcPage.bottom = (int)(TWIPS_PER_HUNDREDTH_INCH * (double)e.PageBounds.Bottom);
            rcPage.left = (int)(TWIPS_PER_HUNDREDTH_INCH * (double)e.PageBounds.Left);
            rcPage.right = (int)(TWIPS_PER_HUNDREDTH_INCH * (double)e.PageBounds.Right);
            //FORMATRANGE設定
            IntPtr hdc = e.Graphics.GetHdc();    //デバイスコンテキストハンドル(HDC)取得
            STRUCT_FORMATRANGE fr;    //RICHEDITコントロール用のFORMATRANGE構造体(Win32)
            fr.chrg = cr;
            fr.hdc = hdc;
            fr.hdcTarget = hdc;
            fr.rc = rc;
            fr.rcPage = rcPage;
            //EM_FORMATRANGE処理
            Int32 wParam = (measureOnly ? 0 : 1);                        //0 - テキストは測定のみ、1 - テキストがレンダリングされる
            IntPtr lParam = Marshal.AllocCoTaskMem(Marshal.SizeOf(fr));    //COMタスクメモリアロケーターによるメモリー確保
            Marshal.StructureToPtr(fr, lParam, false);                    //構造体frのポインターをlParamにセット
            int res = SendMessage(this.Handle, EM_FORMATRANGE, wParam, lParam);    //Win32 SendMessage関数
            //終了処理
            Marshal.FreeCoTaskMem(lParam);                                //COMタスクメモリアロケーターによるメモリー開放
            e.Graphics.ReleaseHdc(hdc);                                    //HDC開放(アンマネージドリソースのメモリー開放)
            SendMessage(this.Handle, EM_FORMATRANGE, 0, IntPtr.Zero);    //キャッシュクリア(メモリー開放)
            //ページ番号付加
            string page = "- " + pageNum.ToString() + " -"; 
            SizeF size = e.Graphics.MeasureString(page, this.Font); 
            float x = e.MarginBounds.Left + (e.MarginBounds.Width - size.Width) / 2;
            float y = e.MarginBounds.Bottom + this.Font.GetHeight(e.Graphics) / 2;
            e.Graphics.DrawString(page, this.Font, Brushes.Black, x, y);
            return res;    //戻り値:次ページの開始文字(最終文字 + 1)
        }

        //文字列の検索(解説:FRDialogクラスは検索と置換両用)
        public void Find()
        {
            FRDialog frDlg = new FRDialog(this, false);    //解説:文字列検索ダイアログ(FRDialog+引数false)を作成
            frDlg.Show();
        }

        //文字列の置換(Ditto)
        public void Replace()
        {
            FRDialog frDlg = new FRDialog(this, true);    //解説:文字列置換ダイアログ(FRDialog+引数true)を作成
            frDlg.Show();
        }
    }

 

またまた文字列長オーバーなので最後にRichTextBoxExコントロールについて(2)へ続きます。

 

【RichEditor】先ずはエディターアプリから(2)」からの続き。


        //ドラッグされた時
        private void RichEdit_DragEnter(object sender, DragEventArgs e)
        {
            if(e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effect = DragDropEffects.Copy;
            }
        }

        //ドロップされたとき
        private void RichEdit_DragDrop(object sender, DragEventArgs e)
        {
            //ドロップされたファイルパスを取得
            string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
            if(files.Length > 0)
            {
                RichTextBoxStreamType rtst;
                if(Path.GetExtension(files[files.Length - 1]) == ".rtf")
                    rtst = RichTextBoxStreamType.RichText;    //rtfファイル
                else
                    rtst = RichTextBoxStreamType.PlainText;    //テキストファイル
                rtxtBox.LoadFile(files[files.Length - 1], rtst);
                //ファイル名を記録
                tssl[1].Text = files[files.Length - 1];
                tssl[1].ToolTipText = files[files.Length - 1];    //ToolTip設定
            }
        }

        //終了処理
        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 OnNew_Click(object sender, EventArgs e)
        {
            if(tssl[1].Text != "(ファイル名)")    //解説:ファイルパス名の保存は変数ではなく、tss[1]を使っています。
            {
                DialogResult dr = MessageBox.Show("データがありますが、初期化しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                if(dr == DialogResult.Yes)
                {
                    rtxtBox.Clear();
                    tssl[1].Text = "(ファイル名)";
                    tssl[1].ToolTipText = "(ファイル名)";        //ToolTip設定
                }
            }
        }

        //「ファイルを開く」処理
        private void OnOpen_Click(object sender, EventArgs e)
        {
            OpenFileDialog ofDlg = new OpenFileDialog();
            //ファイルフィルターの指定
            ofDlg.Filter = "リッチテキストファイル|*.rtf|テキストファイル|*.txt|すべてのファイル|*.*";
            ofDlg.FilterIndex = 1;
            ofDlg.RestoreDirectory = true;    //初期ディレクトリへ復帰
            ofDlg.CheckPathExists = true;    //ファイルパスの存在チェック
            ofDlg.InitialDirectory = ".";    // デフォルトのフォルダーの指定
            ofDlg.Title = "ファイルを開く";    //ダイアログのタイトルを指定する
            if(ofDlg.ShowDialog() == DialogResult.Cancel)    //ダイアログを表示する
            {
                MessageBox.Show("キャンセルされました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
            else
            {
                RichTextBoxStreamType rtst;
                if(Path.GetExtension(ofDlg.FileName) == ".rtf")
                    rtst = RichTextBoxStreamType.RichText;    //rtfファイル
                else
                    rtst = RichTextBoxStreamType.PlainText;    //テキストファイル
                rtxtBox.LoadFile(ofDlg.FileName, rtst);
                //ファイル名を記録
                tssl[1].Text = ofDlg.FileName;    //解説:ファイルパス名の保存は変数ではなく、tss[1]を使っています。
                tssl[1].ToolTipText = ofDlg.FileName;        //ToolTip設定
            }
            //オブジェクトを破棄する
            ofDlg.Dispose();
        }

        //「保存」処理
        private void OnSave_Click(object sender, EventArgs e)
        {
            if(tssl[1].Text == "(ファイル名)")
                OnSaveAs_Click(sender, e);    //解説:新規に作成したファイルは「名前を付けて保存」処理へ移行します
            else
            {
                RichTextBoxStreamType rtst;
                if(Path.GetExtension(tssl[1].Text) == ".rtf")
                    rtst = RichTextBoxStreamType.RichText;    //rtfファイル
                else
                    rtst = RichTextBoxStreamType.PlainText;    //テキストファイル
                rtxtBox.SaveFile(tssl[1].Text, rtst);    //解説:ファイルパス名の保存は変数ではなく、tss[1]を使っています。
            }
        }

        //「名前を付けて保存」処理
        private void OnSaveAs_Click(object sender, EventArgs e)
        {
            SaveFileDialog sfDlg = new SaveFileDialog();
            //ファイルフィルターの指定
            sfDlg.AddExtension = true;        //拡張子自動付加
            sfDlg.DefaultExt = ".rtf";
            sfDlg.Filter = "リッチテキストファイル|*.rtf|テキストファイル|*.txt|すべてのファイル|*.*";
            sfDlg.FilterIndex = 2;
            sfDlg.RestoreDirectory = true;    //初期ディレクトリへ復帰
            sfDlg.CheckPathExists = true;    //ファイルパスの存在チェック
            sfDlg.InitialDirectory = ".";    // デフォルトのフォルダーの指定
            sfDlg.Title = "ファイルを保存";        //ダイアログのタイトルを指定する
            sfDlg.OverwritePrompt = true;                    //上書き確認
            if(sfDlg.ShowDialog() == DialogResult.Cancel)    //ダイアログを表示する
            {
                MessageBox.Show("キャンセルされました。", "キャンセル", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
            }
            else
            {
                RichTextBoxStreamType rtst;
                if(Path.GetExtension(sfDlg.FileName) == ".rtf")
                    rtst = RichTextBoxStreamType.RichText;    //rtfファイル
                else
                    rtst = RichTextBoxStreamType.PlainText;    //テキストファイル
                rtxtBox.SaveFile(sfDlg.FileName, rtst);
                //ファイル名を記録(解説:ファイルパス名の保存は変数ではなく、tss[1]を使っています。)
                tssl[1].Text = sfDlg.FileName;
                tssl[1].ToolTipText = sfDlg.FileName;    //ToolTip設定
            }
            //オブジェクトを破棄する
            sfDlg.Dispose();
        }

        //「印刷」処理
        private void OnPrint_Click(object sender, EventArgs e)
        {
            rtxtBox.Print();    //解説:「ページ設定ダイアログ」「印刷ダイアログ」を含め、この一行で済ませました。
        }

        //「終了」処理
        private void OnExit_Click(object sender, EventArgs e)
        {
            Close();    //修了確認はOnClosingメソッドで行う
        }

        //「元に戻す」処理
        private void OnUndo_Click(object sender, EventArgs e)
        {
            rtxtBox.Undo();    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「やり直す」処理
        private void OnRedo_Click(object sender, EventArgs e)
        {
            rtxtBox.Redo();    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「切り取り」処理
        private void OnCut_Click(object sender, EventArgs e)
        {
            rtxtBox.Cut();    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「コピー」処理
        private void OnCopy_Click(object sender, EventArgs e)
        {
            rtxtBox.Copy();    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「貼り付け」処理
        private void OnPaste_Click(object sender, EventArgs e)
        {
            rtxtBox.Paste();    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「すべて選択」処理
        private void OnSelAll_Click(object sender, EventArgs e)
        {
            rtxtBox.SelectAll();    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「文字列の検索」処理
        private void OnFind_Click(object sender, EventArgs e)
        {
            rtxtBox.Find();    //解説:RichTextBoxの他の処理のように簡略になりました。
        }

        //「文字列の置換」処理
        private void OnReplace_Click(object sender, EventArgs e)
        {
            rtxtBox.Replace();    //解説:RichTextBoxの他の処理のように簡略になりました。
        }

        //「文字列の削除」処理
        private void OnDel_Click(object sender, EventArgs e)
        {
            rtxtBox.SelectedText = "";        //解説:選択部分を空文字列にします。


        }

        //「フォント設定」処理
        private void OnFont_Click(object sender, EventArgs e)
        {
            //フォント選択ダイアログで選択文字列のフォントと文字色を指定
            FontDialog fontDlg = new FontDialog();
            fontDlg.ShowColor = true;            //文字色を表示
            fontDlg.Font = rtxtBox.Font;        //初期値
            fontDlg.Color = rtxtBox.ForeColor;    //初期値
            if(fontDlg.ShowDialog() != DialogResult.Cancel)
            {
                rtxtBox.SelectionFont = fontDlg.Font;
                rtxtBox.SelectionColor = fontDlg.Color;
            }
            fontDlg.Dispose();
        }

        //「左寄せ」処理
        private void OnLeftAlign_Click(object sender, EventArgs e)
        {
            rtxtBox.SelectionAlignment = HorizontalAlignment.Left;    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「センタリング」処理
        private void OnCenter_Click(object sender, EventArgs e)
        {
            rtxtBox.SelectionAlignment = HorizontalAlignment.Center;    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「右寄せ」処理
        private void OnRightAlign_Click(object sender, EventArgs e)    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        {
            rtxtBox.SelectionAlignment = HorizontalAlignment.Right;    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「左インデント」処理
        private void OnLIndent_Click(object sender, EventArgs e)
        {
            rtxtBox.SelectionIndent += 64;    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「左インデント解除」処理
        private void OnLUnIndent_Click(object sender, EventArgs e)
        {
            if(rtxtBox.SelectionIndent > 63)
                rtxtBox.SelectionIndent -= 64;    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
            else
                rtxtBox.SelectionIndent = 0;
        }

        //「右インデント」処理
        private void OnRIndent_Click(object sender, EventArgs e)    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        {
            rtxtBox.SelectionRightIndent += 64;    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
        }

        //「右インデント解除」処理
        private void OnRUnIndent_Click(object sender, EventArgs e)
        {
            if(rtxtBox.SelectionRightIndent > 63)
                rtxtBox.SelectionRightIndent -= 64;    //解説:RichTextBoxの機能です。元々OOPの処理はこのように簡略なものです。
            else
                rtxtBox.SelectionRightIndent = 0;
        }

        //「ワードラップ」処理
        private void OnWordWrap_Click(object sender, EventArgs e)
        {
            

            //解説:メニューのチェックとWordWrapプロパティをトグル処理しています。

           miWWrap.Checked = !miWWrap.Checked;
            rtxtBox.WordWrap    = !rtxtBox.WordWrap;
        }

        //「使い方」処理
        private void OnHowtoUse_Click(object sender, EventArgs e)
        {
            //解説:手を抜いてMewssageBoxで済ませました。

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

        //「ショートカットキー」処理
        private void OnShortcutKey_Click(object sender, EventArgs e)
        {

            //解説:手を抜いてMewssageBoxで済ませました。

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

        //「バージョン」処理
        private void OnVersion_Click(object sender, EventArgs e)
        {
            VersionDlg verDlg = new VersionDlg(this.Icon);    //Versionダイアログ表示
            verDlg.ShowDialog();
            verDlg.Dispose();
        }
    }

    /////////////////////
    //Version ダイアログ
    /////////////////////

    class VersionDlg : Form
    {
        public VersionDlg(Icon ico)
        {
            //ダイアログの属性設定
            this.Text = "バーション情報";
            this.ClientSize = new Size(320, 100);
            this.MaximizeBox = false;        // 最大化ボタン
            this.MinimizeBox = false;        // 最小化ボタン
            this.ShowInTaskbar = false;        //タスクバー上表示
            this.FormBorderStyle = FormBorderStyle.FixedDialog;        // 境界のスタイル
            this.StartPosition = FormStartPosition.CenterParent;    // 親フォームの中央に配置
            //コントロールの属性設定
            Button btnOK = new Button();
            btnOK.Size = new Size(40, 28);
            btnOK.Location = new Point(ClientSize.Width - btnOK.Width - 10, (ClientSize.Height - btnOK.Height) / 2);
            btnOK.Text = "OK";
            btnOK.Click += new EventHandler(OnOK_Click);
            Label imglabel = new Label();
            imglabel.Size = new Size(40, 40);
            imglabel.Location = new Point(10, (ClientSize.Height - imglabel.Height) / 2);
            imglabel.BorderStyle = BorderStyle.Fixed3D;
            imglabel.Image = ico.ToBitmap();                        //親のシステムアイコン
            Label label = new Label();
            label.Size = new Size(ClientSize.Width - imglabel.Width - btnOK.Width - 40, ClientSize.Height- 20);
            label.Location = new Point(imglabel.Width + 20, (ClientSize.Height - label.Height) / 2);
            label.BorderStyle = BorderStyle.Fixed3D;
            label.Text = "RichEditor Version 1.0\r\nCopyright (c) 2025 by Ysama\r\n(written in Microsoft C#)";
            label.TextAlign = ContentAlignment.MiddleCenter;
            label.Font = new Font("Times New Roman", 10, FontStyle.Bold);
            this.Controls.Add(btnOK);
            this.Controls.Add(imglabel);
            this.Controls.Add(label);
        }

        private void OnOK_Click(object sender, EventArgs e)
        {
            Close();
        }
    }
}

 

以上です。単なるSDIウィンドウとしてはメニューとツールバーボタンが多いので(6,000字規制をオーバーして)3回に分けざるを得ませんでしたが、内容はとてもシンプルであったことがお分かりと存じます。さて、この後は肝心のRIchEditBoxExの作成になります。乞御期待!


 

【RichEditor】先ずはエディターアプリから(1)」からの続き。

 

//解説:以下でリソースマネージャーが読み込むリソース(RichEditor)の画像(rtToolBar)は次の通りです。


        //ツールバーとステータスバーの作成
        private void MakeBars()
        {
            //ToolStripクラスインスタンスの生成
            this.toolStrip = new ToolStrip();
            //ツールバーのレイアウトを一時停止
            this.toolStrip.SuspendLayout();
            //ToolStripButton配列を作成
            this.tsButton = new ToolStripButton[23];
            //本プログラムの埋め込みリソースのリソースマネージャーを作成
            ResourceManager rm = new ResourceManager("RichEditor", myOwn);
            //ツールバービットマップの読み込み
            ImageList imgList = new ImageList();
            imgList.ImageSize = new Size(16, 15);
            imgList.Images.AddStrip((Bitmap)rm.GetObject("rtToolBar"));
            imgList.TransparentColor = Color.FromArgb(0xC0, 0xC0, 0xC0);    //rtToolBarの背景色
            //ToolStripButton[0]を作成
            this.tsButton[0] = new ToolStripButton();
            this.tsButton[0].Text = "新規作成";                                //テキスト設定
            this.tsButton[0].Image = (Bitmap)imgList.Images[0];                //画像設定
            this.tsButton[0].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[0].Click += OnNew_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[0]);                        //ボタンを追加
            //ToolStripButton[1]を作成
            this.tsButton[1] = new ToolStripButton();
            this.tsButton[1].Text = "開く";                                    //テキスト設定
            this.tsButton[1].Image = (Bitmap)imgList.Images[1];                //画像設定
            this.tsButton[1].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[1].Click += OnOpen_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[1]);                        //ボタンを追加
            //ToolStripButton[2]を作成
            this.tsButton[2] = new ToolStripButton();
            this.tsButton[2].Text = "保存";                                    //テキスト設定
            this.tsButton[2].Image = (Bitmap)imgList.Images[2];                //画像設定
            this.tsButton[2].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[2].Click += OnSave_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[2]);                        //ボタンを追加
            //ToolStripButton[3]を作成
            this.tsButton[3] = new ToolStripButton();
            this.tsButton[3].Text = "印刷";                                    //テキスト設定
            this.tsButton[3].Image = (Bitmap)imgList.Images[3];                //画像設定
            this.tsButton[3].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[3].Click += OnPrint_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[3]);                        //ボタンを追加
            //セパレーターを挿入
            this.toolStrip.Items.Add(new ToolStripSeparator());
            //ToolStripButton[4]を作成
            this.tsButton[4] = new ToolStripButton();
            this.tsButton[4].Text = "終了";                                    //テキスト設定
            this.tsButton[4].Image = (Bitmap)imgList.Images[4];                //画像設定
            this.tsButton[4].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[4].Click += OnExit_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[4]);                        //ボタンを追加
            //セパレーターを挿入
            this.toolStrip.Items.Add(new ToolStripSeparator());
            //ToolStripButton[5]を作成
            this.tsButton[5] = new ToolStripButton();
            this.tsButton[5].Text = "元に戻す";                                //テキスト設定
            this.tsButton[5].Image = (Bitmap)imgList.Images[5];                //画像設定
            this.tsButton[5].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[5].Click += OnUndo_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[5]);                        //ボタンを追加
            //ToolStripButton[6]を作成
            this.tsButton[6] = new ToolStripButton();
            this.tsButton[6].Text = "やり直し";                                //テキスト設定
            this.tsButton[6].Image = (Bitmap)imgList.Images[6];                //画像設定
            this.tsButton[6].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[6].Click += OnRedo_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[6]);                        //ボタンを追加
            //ToolStripButton[7]を作成
            this.tsButton[7] = new ToolStripButton();
            this.tsButton[7].Text = "切り取り";                                //テキスト設定
            this.tsButton[7].Image = (Bitmap)imgList.Images[7];                //画像設定
            this.tsButton[7].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[7].Click += OnCut_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[7]);                        //ボタンを追加
            //ToolStripButton[8]を作成
            this.tsButton[8] = new ToolStripButton();
            this.tsButton[8].Text = "コピー";                                //テキスト設定
            this.tsButton[8].Image = (Bitmap)imgList.Images[8];                //画像設定
            this.tsButton[8].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[8].Click += OnCopy_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[8]);                        //ボタンを追加
            //ToolStripButton[9]を作成
            this.tsButton[9] = new ToolStripButton();
            this.tsButton[9].Text = "貼り付け";                                //テキスト設定
            this.tsButton[9].Image = (Bitmap)imgList.Images[9];            //画像設定
            this.tsButton[9].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[9].Click += OnPaste_Click;                        //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[9]);                    //ボタンを追加
            //ToolStripButton[10]を作成
            this.tsButton[10] = new ToolStripButton();
            this.tsButton[10].Text = "文字列の検索";                        //テキスト設定
            this.tsButton[10].Image = (Bitmap)imgList.Images[10];            //画像設定
            this.tsButton[10].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[10].Click += OnFind_Click;                        //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[10]);                    //ボタンを追加
            //ToolStripButton[11]を作成
            this.tsButton[11] = new ToolStripButton();
            this.tsButton[11].Text = "文字列の置換";                        //テキスト設定
            this.tsButton[11].Image = (Bitmap)imgList.Images[11];            //画像設定
            this.tsButton[11].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[11].Click += OnReplace_Click;                        //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[11]);                    //ボタンを追加
            //セパレーターを挿入
            this.toolStrip.Items.Add(new ToolStripSeparator());
            //ToolStripButton[12]を作成
            this.tsButton[12] = new ToolStripButton();
            this.tsButton[12].Text = "文字列の削除";                        //テキスト設定
            this.tsButton[12].Image = (Bitmap)imgList.Images[12];            //画像設定
            this.tsButton[12].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[12].Click += OnDel_Click;                            //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[12]);                    //ボタンを追加
            //セパレーターを挿入
            this.toolStrip.Items.Add(new ToolStripSeparator());
            //ToolStripButton[13]を作成
            this.tsButton[13] = new ToolStripButton();
            this.tsButton[13].Text = "左寄せ";                                //テキスト設定
            this.tsButton[13].Image = (Bitmap)imgList.Images[13];            //画像設定
            this.tsButton[13].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[13].Click += OnLeftAlign_Click;                    //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[13]);                    //ボタンを追加
            //ToolStripButton[14]を作成
            this.tsButton[14] = new ToolStripButton();
            this.tsButton[14].Text = "センタリング";                        //テキスト設定
            this.tsButton[14].Image = (Bitmap)imgList.Images[14];            //画像設定
            this.tsButton[14].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[14].Click += OnCenter_Click;                        //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[14]);                    //ボタンを追加
            //ToolStripButton[15]を作成
            this.tsButton[15] = new ToolStripButton();
            this.tsButton[15].Text = "右寄せ";                                //テキスト設定
            this.tsButton[15].Image = (Bitmap)imgList.Images[15];            //画像設定
            this.tsButton[15].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[15].Click += OnRightAlign_Click;                    //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[15]);                    //ボタンを追加
            //セパレーターを挿入
            this.toolStrip.Items.Add(new ToolStripSeparator());
            //ToolStripButton[16]を作成
            this.tsButton[16] = new ToolStripButton();
            this.tsButton[16].Text = "フォント設定";                        //テキスト設定
            this.tsButton[16].Image = (Bitmap)imgList.Images[16];            //画像設定
            this.tsButton[16].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[16].Click += OnFont_Click;                        //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[16]);                    //ボタンを追加
            //セパレーターを挿入
            this.toolStrip.Items.Add(new ToolStripSeparator());
            //ToolStripButton[17]を作成
            this.tsButton[17] = new ToolStripButton();
            this.tsButton[17].Text = "左インデント";                        //テキスト設定
            this.tsButton[17].Image = (Bitmap)imgList.Images[17];            //画像設定
            this.tsButton[17].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[17].Click += OnLIndent_Click;                        //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[17]);                    //ボタンを追加
            //ToolStripButton[18]を作成
            this.tsButton[18] = new ToolStripButton();
            this.tsButton[18].Text = "左インデント解除";                    //テキスト設定
            this.tsButton[18].Image = (Bitmap)imgList.Images[18];            //画像設定
            this.tsButton[18].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[18].Click += OnLUnIndent_Click;                    //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[18]);                    //ボタンを追加
            //ToolStripButton[19]を作成
            this.tsButton[19] = new ToolStripButton();
            this.tsButton[19].Text = "右インデント";                        //テキスト設定
            this.tsButton[19].Image = (Bitmap)imgList.Images[19];            //画像設定
            this.tsButton[19].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[19].Click += OnRIndent_Click;                        //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[19]);                    //ボタンを追加
            //ToolStripButton[20]を作成
            this.tsButton[20] = new ToolStripButton();
            this.tsButton[20].Text = "右インデント解除";                    //テキスト設定
            this.tsButton[20].Image = (Bitmap)imgList.Images[20];            //画像設定
            this.tsButton[20].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[20].Click += OnRUnIndent_Click;                    //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[20]);                    //ボタンを追加
            //セパレーターを挿入
            this.toolStrip.Items.Add(new ToolStripSeparator());
            //ToolStripButton[21]を作成
            this.tsButton[21] = new ToolStripButton();
            this.tsButton[21].Text = "使い方";                                //テキスト設定
            this.tsButton[21].Image = (Bitmap)imgList.Images[21];            //画像設定
            this.tsButton[21].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[21].Click += OnHowtoUse_Click;                        //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[21]);                    //ボタンを追加
            //ToolStripButton[22]を作成
            this.tsButton[22] = new ToolStripButton();
            this.tsButton[22].Text = "バージョン情報(&V)";                    //テキスト設定
            this.tsButton[22].Image = (Bitmap)imgList.Images[22];            //画像設定
            this.tsButton[22].DisplayStyle = ToolStripItemDisplayStyle.Image;    //画像表示のみ
            this.tsButton[22].Click += OnVersion_Click;                        //Clickイベントハンドラ追加
            this.toolStrip.Items.Add(this.tsButton[22]);                    //ボタンを追加

            //ツールバーの設定
            this.Controls.Add(this.toolStrip);
            //ツールバーのレイアウトを再開
            this.toolStrip.ResumeLayout(false);
            this.toolStrip.PerformLayout();

            //StatusStripクラスインスタンスの生成
            this.statusStrip = new StatusStrip();
            //ステータスバーにパネルとテキストを追加
            tssl = new ToolStripStatusLabel[2];
            tssl[0] = new ToolStripStatusLabel();
            tssl[0].BorderSides = ToolStripStatusLabelBorderSides.All;
            tssl[0].BorderStyle = Border3DStyle.SunkenInner;
            tssl[0].BackColor = SystemColors.Control;
            tssl[0].Text = "RichEditor Ver. 1.0";
            tssl[0].AutoSize = true;
            tssl[0].TextAlign = ContentAlignment.MiddleLeft;
            tssl[1] = new ToolStripStatusLabel();
            tssl[1].BorderSides = ToolStripStatusLabelBorderSides.All;
            tssl[1].BorderStyle = Border3DStyle.SunkenInner;
            tssl[1].BackColor = SystemColors.Control;
            tssl[1].TextAlign = ContentAlignment.MiddleLeft;
            tssl[1].Text = "(ファイル名)";
            tssl[1].ToolTipText = "(ファイル名)";        //ToolTip設定
            tssl[1].Spring = true;
            statusStrip.Items.AddRange(tssl);
            statusStrip.ShowItemToolTips = true;                    //ToolTip表示
            this.Controls.Add(this.statusStrip);                    //StatusStrip(ステータスバー)を追加
        }

 

【RichEditor】先ずはエディターアプリから(3)」へ続く

前々回前回で、C#の(クラスオブジェクトなのに文字列検索、置換と印刷の機能に弱点がある)RichTextBoxクラスを機能強化して、ダイアログによる文字列検索、置換と印刷機能を付加したRichTextBoxExの目途が見えました。今回はその解説の前に、RichTextBoxExを使うとこんなにアプリ作成が簡単になるという紹介をさせてください。なお、開発はRichTextBoxExクラスを含むスタンドアロンプログラムのRichEditor.csでしたが、今回はRichTextBoxExクラスをDLL化し、それ以外のエディターアプリ部分(C#のWinFormsのSDIウィンドウの雛形になりますよ)を先ず披露させていただきます。なお、いつも通りコメント解説:を書きますが、ピンクでDLL化したRichTextBoxExクラスの関連部分をハイライトします。

 

【RichTextEditor.csからRichTextBoxExクラス部分を除いた、Test_RichTextBoxEx.cs】

///////////////////////////////
// RichRichEditor.cs
// Copyright (c) 2025 by ysama
///////////////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Reflection;                //Assemblyを使う為
using System.Resources;                    //リソース関係クラス等の使用の為
using System.IO;                        //File.ReadAllText使用の為
using System.Text;                        //Encoding.GetEncoding使用の為
using richTextBoxEx;                    //RichTextBoxEx使用の為

namespace RichEditor
{
    public class RichEdit : Form
    {
        //クラスメンバー変数
        Assembly myOwn = Assembly.GetEntryAssembly();
        //クラスコントロール
        MenuStrip menuStrip;                        //メニュー
        ToolStripMenuItem miWWrap;                    //ワードラップメニューアイテム
        ToolStrip toolStrip;                        //ツールバー
        ToolStripButton[] tsButton;                    //ツールバーボタン
        StatusStrip statusStrip;                    //ステータスバー
        ToolStripStatusLabel[] tssl;                //ステータスバーラベル
        RichTextBoxEx rtxtBox;                        //リッチテキストボックスEx(カスタムコントロール)

        [STAThread]
        public static void Main()                    //エントリーポイント
        {
            Application.Run(new RichEdit());
        }

        public RichEdit()                            //コンストラクター
        {
            this.Icon = Icon.ExtractAssociatedIcon(myOwn.Location);    //プログラムアイコンをフォームにつける
            this.Size = new Size(640, 480);
            this.MinimumSize = new Size(620, 240);
            this.Text = "RichEditor";
            //Drag and Dropでの処理
            this.AllowDrop = true;
            this.DragEnter += RichEdit_DragEnter;
            this.DragDrop += RichEdit_DragDrop;
            //起動時処理
            this.Load += RichEdit_Load;
        }

        private void RichEdit_Load(object sender, EventArgs e)
        {
            //フォームのレイアウトを一時停止
            this.SuspendLayout();
            //ツールバーとステータスバーの設定
            MakeBars();
            //メニューの作成(MenuStripはコントロールであり、上からの並びはDock投入のFILO順となる)

            //解説:これはどういうことかというと、メニューとツールバーの順を入れ替えるとウィンドウ

            //上でも順番が変わるということです。(これの1,2参照)
            MakeMenu();
            //出力用リッチテキストボックスEx
            rtxtBox = new RichTextBoxEx();
            //位置
            rtxtBox.Location = new Point(0, this.menuStrip.Height + this.toolStrip.Height);
            //サイズ
            rtxtBox.Width = ClientSize.Width;
            rtxtBox.Height = ClientSize.Height - this.menuStrip.Height - this.toolStrip.Height - this.statusStrip.Height;
            //アンカー
            rtxtBox.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right);
            //その他プロパティ
            rtxtBox.AcceptsTab = true;
            rtxtBox.WordWrap    = false;
            rtxtBox.Font = this.Font;            
//フォントの指定
            rtxtBox.Multiline = true;            //複数行(既定値)
            rtxtBox.ScrollBars = RichTextBoxScrollBars.Both;    //既定値
            rtxtBox.WordWrap = false;            //ワードラップを行う(解説:メニューでtrueにできます)
            //Drag and Dropのイベントハンドラの追加
            rtxtBox.DragEnter += new DragEventHandler(RichEdit_DragEnter);
            rtxtBox.DragDrop += new DragEventHandler(RichEdit_DragDrop);
            rtxtBox.AllowDrop = true;

            //FormにRichTextBoxExを追加
            this.Controls.Add(rtxtBox);        //解説:以上のピンク行の設定はRichTextBoxと全く同じです。
            //フォームのレイアウトを再開
            this.ResumeLayout(false);
            this.PerformLayout();
            //起動時の引数をチェックし、引数にファイル名があればそれを読み込む
            string[] arg = System.Environment.GetCommandLineArgs();
            if(arg.Length > 1)        //解説:起動時にファイルを与えるとそれ(複数なら最後のもの)を開きます。
            {
                if(Path.GetExtension(arg[arg.Length - 1]) == ".rtf")
                {
                    rtxtBox.LoadFile(arg[arg.Length - 1], RichTextBoxStreamType.RichText);
                    //ファイル名を記録
                    tssl[1].Text = arg[arg.Length - 1];
                    tssl[1].ToolTipText = arg[arg.Length - 1];    //ToolTip設定
                }
                else
                {
                    rtxtBox.LoadFile(arg[arg.Length - 1], RichTextBoxStreamType.PlainText);
                    //ファイル名を記録
                    tssl[1].Text = arg[arg.Length - 1];
                    tssl[1].ToolTipText = arg[arg.Length - 1];    //ToolTip設定
                }
            }
            else        //解説:無ければ空テキストにします。(以上はRichTextBoxと同じ)
                rtxtBox.Text = "";
        }

        //メニューの作成
        private void MakeMenu()
        {
            //メインメニュー作成
            menuStrip = new MenuStrip();
            menuStrip.SuspendLayout();
            //メインメニューアイテム付加
            ToolStripMenuItem miFile = new ToolStripMenuItem();        //「ファイル」メニュー
            miFile.Text = "ファイル(&F)";
            menuStrip.Items.Add(miFile);
            ToolStripMenuItem miEdit = new ToolStripMenuItem();        //「編集」メニュー
            miEdit.Text = "編集(&E)";
            menuStrip.Items.Add(miEdit);
            ToolStripMenuItem miFormat = new ToolStripMenuItem();    //「書式」メニュー
            miFormat.Text = "書式(&F)";
            menuStrip.Items.Add(miFormat);
            ToolStripMenuItem miDisp = new ToolStripMenuItem();        //「表示」メニュー
            miDisp.Text = "表示(&D)";
            menuStrip.Items.Add(miDisp);
            ToolStripMenuItem miHelp = new ToolStripMenuItem();        //「ヘルプ」メニュー
            miHelp.Text = "ヘルプ(&H)";
            menuStrip.Items.Add(miHelp);
            //「ファイル」メニューアイテム付加
            ToolStripMenuItem miNew = new ToolStripMenuItem();        //「新規」メニューアイテム
            miNew.Text = "新規作成(&N)";
            miNew.Click += OnNew_Click;
            miNew.ShortcutKeys = Keys.Control | Keys.N;
            miFile.DropDownItems.Add(miNew);
            ToolStripMenuItem miOpen = new ToolStripMenuItem();        //「開く」メニューアイテム
            miOpen.Text = "開く(&O)";
            miOpen.Click += OnOpen_Click;
            miOpen.ShortcutKeys = Keys.Control | Keys.O;
            miFile.DropDownItems.Add(miOpen);
            ToolStripMenuItem miSave = new ToolStripMenuItem();        //「保存」メニューアイテム
            miSave.Text = "保存(&S)";
            miSave.Click += OnSave_Click;
            miSave.ShortcutKeys = Keys.Control | Keys.S;
            miFile.DropDownItems.Add(miSave);
            ToolStripMenuItem miSaveAs = new ToolStripMenuItem();    //「名前を付けて保存」メニューアイテム
            miSaveAs.Text = "名前を付けて保存(&A)";
            miSaveAs.Click += OnSaveAs_Click;
            miSaveAs.ShortcutKeys = Keys.Control | Keys.A;
            miFile.DropDownItems.Add(miSaveAs);
            miFile.DropDownItems.Add(new ToolStripSeparator());        //セパレーター
            ToolStripMenuItem miPrint = new ToolStripMenuItem();    //「印刷」メニューアイテム
            miPrint.Text = "印刷(&P)";
            miPrint.Click += OnPrint_Click;
            miPrint.ShortcutKeys = Keys.Control | Keys.P;
            miFile.DropDownItems.Add(miPrint);
            miFile.DropDownItems.Add(new ToolStripSeparator());        //セパレーター
            ToolStripMenuItem miExit = new ToolStripMenuItem();        //「終了」メニューアイテム
            miExit.Text = "終了(&X)";
            miExit.Click += OnExit_Click;
            miExit.ShortcutKeys = Keys.Control | Keys.X;
            miFile.DropDownItems.Add(miExit);
            //「編集」メニューアイテム付加
            ToolStripMenuItem miUndo = new ToolStripMenuItem();        //「元に戻す」メニューアイテム
            miUndo.Text = "元に戻す(&U)";
            miUndo.Click += OnUndo_Click;
            miUndo.ShortcutKeys = Keys.Control | Keys.U;
            miEdit.DropDownItems.Add(miUndo);
            ToolStripMenuItem miRedo = new ToolStripMenuItem();        //「やり直す」メニューアイテム
            miRedo.Text = "やり直す(&E)";
            miRedo.Click += OnRedo_Click;
            miEdit.DropDownItems.Add(miRedo);
            miEdit.DropDownItems.Add(new ToolStripSeparator());        //セパレーター
            ToolStripMenuItem miCut = new ToolStripMenuItem();        //「切り取り」メニューアイテム
            miCut.Text = "切り取り(&T)";
            miCut.Click += OnCut_Click;
            miCut.ShortcutKeys = Keys.Control | Keys.T;
            miEdit.DropDownItems.Add(miCut);
            ToolStripMenuItem miCopy = new ToolStripMenuItem();        //「コピー」メニューアイテム
            miCopy.Text = "コピー(&C)";
            miCopy.Click += OnCopy_Click;
            miCopy.ShortcutKeys = Keys.Control | Keys.C;
            miEdit.DropDownItems.Add(miCopy);
            ToolStripMenuItem miPaste = new ToolStripMenuItem();    //「貼り付け」メニューアイテム
            miPaste.Text = "貼り付け(&V)";
            miPaste.Click += OnPaste_Click;
            miPaste.ShortcutKeys = Keys.Control | Keys.V;
            miEdit.DropDownItems.Add(miPaste);
            miEdit.DropDownItems.Add(new ToolStripSeparator());        //セパレーター
            ToolStripMenuItem miSelAll = new ToolStripMenuItem();    //「すべて選択」メニューアイテム
            miSelAll.Text = "すべて選択(&A)";
            miSelAll.Click += OnSelAll_Click;
            miSelAll.ShortcutKeys = Keys.Control | Keys.A;
            miEdit.DropDownItems.Add(miSelAll);
            miEdit.DropDownItems.Add(new ToolStripSeparator());        //セパレーター
            ToolStripMenuItem miFind = new ToolStripMenuItem();        //「文字列の検索」メニューアイテム
            miFind.Text = "文字列の検索(&F)";
            miFind.Click += OnFind_Click;
            miFind.ShortcutKeys = Keys.Control | Keys.F;
            miEdit.DropDownItems.Add(miFind);
            ToolStripMenuItem miReplace = new ToolStripMenuItem();    //「文字列の置換」メニューアイテム
            miReplace.Text = "文字列の置換(&R)";
            miReplace.Click += OnReplace_Click;
            miReplace.ShortcutKeys = Keys.Control | Keys.R;
            miEdit.DropDownItems.Add(miReplace);
            miEdit.DropDownItems.Add(new ToolStripSeparator());        //セパレーター
            ToolStripMenuItem miDel = new ToolStripMenuItem();        //「文字列の削除」メニューアイテム
            miDel.Text = "文字列の削除(&D)";
            miDel.Click += OnDel_Click;
            miDel.ShortcutKeys = Keys.Control | Keys.D;
            miEdit.DropDownItems.Add(miDel);
            //「書式」メニューアイテム付加
            ToolStripMenuItem miFont = new ToolStripMenuItem();        //「フォント設定」メニューアイテム
            miFont.Text = "フォント設定(&F)";
            miFont.Click += OnFont_Click;
            miFormat.DropDownItems.Add(miFont);
            ToolStripMenuItem miAlign = new ToolStripMenuItem();    //「文字寄せ」メニューアイテム
            miAlign.Text = "文字寄せ(&A)";
            miFormat.DropDownItems.Add(miAlign);
            ToolStripMenuItem miIndent = new ToolStripMenuItem();    //「インデント」メニューアイテム
            miIndent.Text = "インデント(&I)";
            miFormat.DropDownItems.Add(miIndent);
            //「文字寄せ」メニューアイテム付加
            ToolStripMenuItem miLeft = new ToolStripMenuItem();        //「左寄せ」メニューアイテム
            miLeft.Text = "左寄せ(&L)";
            miLeft.Click += OnLeftAlign_Click;
            miAlign.DropDownItems.Add(miLeft);
            ToolStripMenuItem miCenter = new ToolStripMenuItem();    //「センタリング」メニューアイテム
            miCenter.Text = "センタリング(&C)";
            miCenter.Click += OnCenter_Click;
            miAlign.DropDownItems.Add(miCenter);
            ToolStripMenuItem miRight = new ToolStripMenuItem();    //「右寄せ」メニューアイテム
            miRight.Text = "右寄せ(&R)";
            miRight.Click += OnRightAlign_Click;
            miAlign.DropDownItems.Add(miRight);
            //「インデント」メニューアイテム付加
            ToolStripMenuItem miLInd = new ToolStripMenuItem();        //「左インデント」メニューアイテム
            miLInd.Text = "左インデント(&I)";
            miLInd.Click += OnLIndent_Click;
            miIndent.DropDownItems.Add(miLInd);
            ToolStripMenuItem miLUnInd = new ToolStripMenuItem();    //「左インデント解除」メニューアイテム
            miLUnInd.Text = "左インデント解除(&O)";
            miLUnInd.Click += OnLUnIndent_Click;
            miIndent.DropDownItems.Add(miLUnInd);
            ToolStripMenuItem miRInd = new ToolStripMenuItem();        //「右インデント」メニューアイテム
            miRInd.Text = "右インデント(&I)";
            miRInd.Click += OnRIndent_Click;
            miIndent.DropDownItems.Add(miRInd);
            ToolStripMenuItem miRUnInd = new ToolStripMenuItem();    //「右インデント解除」メニューアイテム
            miRUnInd.Text = "右インデント解除(&O)";
            miRUnInd.Click += OnRUnIndent_Click;
            miIndent.DropDownItems.Add(miRUnInd);
            //「表示」メニューアイテム付加
            miWWrap = new ToolStripMenuItem();    //「ワードラップ」メニューアイテム
            miWWrap.Text = "ワードラップ(&W)";
            miWWrap.Click += OnWordWrap_Click;
            miDisp.DropDownItems.Add(miWWrap);
            //「ヘルプ」メニューアイテム付加
            ToolStripMenuItem miHowtoUse = new ToolStripMenuItem();    //「使い方」メニューアイテム
            miHowtoUse.Text = "使い方(&H)";
            miHowtoUse.Click += OnHowtoUse_Click;
            miHowtoUse.ShortcutKeys = Keys.Control | Keys.H;
            miHelp.DropDownItems.Add(miHowtoUse);
            ToolStripMenuItem miShortcutKey = new ToolStripMenuItem();    //「ショートカットキー」メニューアイテム
            miShortcutKey.Text = "ショートカットキー(&S)";
            miShortcutKey.Click += OnShortcutKey_Click;
            miShortcutKey.ShortcutKeys = Keys.F1;
            miHelp.DropDownItems.Add(miShortcutKey);
            ToolStripMenuItem miVer = new ToolStripMenuItem();        //「バージョン」メニューアイテム
            miVer.Text = "バージョン(&V)";
            miVer.Click += OnVersion_Click;
            miVer.ShortcutKeys = Keys.F2;
            miHelp.DropDownItems.Add(miVer);
            //メインメニューに設定して表示
            this.MainMenuStrip = menuStrip;
            this.Controls.Add(menuStrip);
            menuStrip.ResumeLayout(false);
            menuStrip.PerformLayout();
        }
 

【RichEditor】先ずはエディターアプリから(2)」へ続く

前回【RichEditor】紆余曲折の結果...文字列の検索と置換はC#(WinForms)のRichTextBoxクラスで実装されていない「文字列の検索と置換」について、下方検索は良いのですが、上方検索になると検索完了の戻り値(-1)が返ってこない(Microsoftの「手抜き」)問題とその克服物語について触れました。

 

今回はもっと深刻で、「C#ライブラリーでは書けずに、Win32 APIを動員して印刷メソッドを作るしかない」()という問題について書きます。

:とはいえ、総てのWin32とC++のコードをC#へ移植する訳ではなく、一部は(あまり馴染みのない)C#のオブジェクトコンポーネントに組み合わせなければならないのですから、もっと面倒です。

 

C#を始めた頃、TextBoxでテキストエディターは組んだことが有りますが、プリンターを使わない私にとって「印刷」機能はまず使わないので(はっきり言ってRichTextBoxのみならず、あらゆる印刷関連機能について)全くの無知であり、(昔取った杵柄のWin32とC++の知見から)手始めに印刷関連のクラスやメンバーについて調べることから始めました。

 

Win32 API には「ファイルを開く」ダイアログのような汎用の「コモンダイアログ」というものがあり、それに「印刷ダイアログ」「ページダイアログ」があったので、C#におけるそれらから調査して行きました。

 

その結果PrintDialogPageSetupDialogというクラスがありましたが、それらに関連するPrintDocumentPrinterSettingsPageSettingsというデータクラスもあり、更にこれらには多くのプロパティがあるので、先ずこれらの概要を理解し、相互の関係を整理するとともに全体像を把握して、何はともあれ試行錯誤するサンプルコードを書いてみました。

 

しかし、役者多くして相関関係は分かり難くい

 

事から、またもや五里霧中となると頼れるのはChat-GPT様、今回もご相談させていただきました。これに関わる以下(↓)の「当初の私とChat-GPTのやり取り抜粋」から長い旅が始まります。

 

(私)C#でRichTexxtBoxを使って印刷を行おうと次のようなコードを組むと、コンパイル時に「error CS1061: 'System.Windows.Forms.RichTextBox' に 'Print' の定義が含まれておらず、型 'System.Windows.Forms.RichTextBox' の最初の引数を受け付ける拡張メソッドが見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足しています。」というエラーがでます。C#のVersionの問題でしょうか?

 

        //印刷ダイアログを表示する場合

        PrintDialog prtDlg = new PrintDialog();

        prtDlg.Document = new PrintDocument();

        //ダイアログを表示し、ユーザーがプリンターを選択できるようにする

        if(prtDlg.ShowDialog() == DialogResult.OK)

        {

            //ユーザーがOKを押したら印刷を実行

            rtxtBox.Print(printDialog.Document); // PrintDialog.Documentにはユーザーが選択したプリンターの情報が格納されている

       }

        prtDlg.Dispose();

 

(Chat-GPT)とても良い質問です。
結論から言うと、エラーの原因はC#や.NETのバージョンではなく、
RichTextBox クラスに Print() メソッドが存在しないためです。


🔍 詳細な説明

System.Windows.Forms.RichTextBox は WinForms のコントロールであり、印刷機能を 直接サポートしていません
そのため、rtxtBox.Print() のようなメソッドは存在せず、C# のバージョンに関係なくコンパイルエラーになります。

(後略)


✅ 解決方法(WinForms版 RichTextBox の印刷)

WinFormsの RichTextBox を印刷するには、Windows API(EM_FORMATRANGE メッセージ)を使って手動でレンダリングする必要があります。これを簡略化したクラスを自作して対応するのが一般的です。

 

目 == ・(目が点)何のためのオブジェクト志向プログラミングなんだ!

 

22年前にWin32APIでBCCSkeltonを作り、そのCREDITクラスで印刷機能を実装したので、C++でリッチエディットコントロールの「面倒な印刷処理」を卒業していましたが、なんとまぁ、再度これをC#でやれ、ということになるとは思いませんでした。

 

ということで、

 

先ずは20余年前に自作したWin32のRIch Edit Controlに関わるC++のCEDITクラスのコードをリビューし(だって忘れちゃったんだもん)、Win32 APIでは何をどうしているのかの大まかな流れを探ります。

 

BCCSkeltonのCREDITクラスのコードを見ると、

 

1.「プリンター設定」ダイアログ(PRINTDLG)で印刷用設定データを取得
(1)プリンターデバイスのデバイスコンテキストを取得
(2)引数であるPRINTDLG構造体でプリンター用紙の物理的幅、そのピクセル数を記録
(3)左右マージン(1 inch == 1440 twips)をtwip単位で設定

 

2.印刷
(1)印刷文書情報構造体(DOCINFO)に文書名、
(2)範囲成形構造体(FORMATRANGE)にデバイスコンテキスト、物理的サイズ、ページサイズ、文書長さ、開始文字インデックス、終了文字インデックスを入れて、
(3)StartDoc関数で印刷を開始
(4)印刷はページ単位でページごとの情報(EM_FORMATRANGE)をイメージとして作成(EM_DISPLAYBAND)し、

(5)文書の終わりまでStartPage関数、EndPage関数ループさせ、
(6)最後に"SendMessage(m_hWnd, EM_FORMATRANGE, TRUE, NULL);"によりコントロールによってキャッシュされた情報を解放


という流れです。


C#では、


(1)PrintDialogが構造体を使って関数として使うWin32とやや異なり、独立したクラスになっている。


しかし、これはWin32 APIのPrinter Setupダイアログとビミョーに異なる。

(左がC#で「印刷」、右がWin32で「プリンター設定」)

 

(2)C#の印刷ダイアログクラスではPrintDocumentクラスインスタンスを使って印刷するようである。
(3)印刷ダイアログクラスの他に、ページ設定ダイアログ(PageSetupDIalog)クラスもあるが、印刷処理におけるこれらの関係が不明瞭である。(印刷ダイアログクラスにもページプロパティが設定できる。)


(4)印刷ダイアログクラスには既定ページ設定(DefaultPageSettings)情報、プリンター設定(
PrinterSettings)クラス情報と印刷文書(PrintDocument)クラス情報をプロパティに設定して使うようである。
(5)印刷自体は印刷ダイアログクラスのメソッドではなく、印刷文書クラスの印刷(Print)メソッドを使うようである。

 

Microsoft Learnを見ると、PrintDocument.Print()メソッドにサンプルコードが出てますが、テキストファイルの印刷をストリームで行う例しか出ておらず、文書をグラフィック化して印刷するRIchTextBoxの場合には余り参考にはなりません。

ということで、更に色々と調べていったところ、最後に実用的に参考になるサンプルを見つけたので、これを応用しました。

但し、このサンプルはページ印刷後にキャッシュを開放していない点に注意する必要があります。使うことをご検討されているの方は、今後ご紹介する私のコードを参考にしてください。

 

これで一応印刷することが出来るようになりましたが、Win32の「プリンター設定」と「印刷」という2アクションのメニューやツールバーボタンを1アクションに纏め、「印刷」メニューまたはボタンを押すと、

 

ページ設定ダイアログが出て、OKボタンを押すと、

 

印刷ダイアログが出てOKボタンを押すと印刷される流れにしました。

 

まぁ、何とか印刷できるようになったので、いよいよ次回からはコードの解説をしてゆきましょうか?

 

何とか【PropertyGrid】その名は"PropertyAny"...?)で【PropertyGrid】シリーズを完了し、いよいよすったもんだしていたC#のRichTextBoxを使ったエディター作りに専念できます。

:実はファイルをドラッグアンドドロップしても選択できることを書き忘れました。

 

先般の【無駄話】愚痴の続き...から、決断へで書いたように、「無いのなら作って見せよう、不如帰」のノリで基本構想を「.NET FrameworkのTichTextBoxでは未実装の文字列検索、置換機能と印刷機能を実装した派生クラスを作る」としたのは良いのですが、結構難儀しています。その大きな理由、即ち、

 

(1)(HTML等のマークアップ言語による飾り文章が主流となったので)MicrosoftのRichText自体が時代遅れとなっている。

(2)Windowsのインフラについても、.NET FrameworkのWinFormsも然り(時代遅れ-現代では「それな」というのか?)で、Microsoftは時間や人やお金という資源をもう落とさない。

 

だから、

 

Microsoft Learn等公式文書のみならず、WEB上でも「.NET Framework 4.8のWinForms上のRichTextBox」に関わる情報がとても少ない!

 

という状況です。となると(最近依存しすぎで少し気になってきているのですが)またまたまたまたChat-GPT様に相談することになります。しかし、

 

生成系AIと言えども、情報量の少ないテーマでは間違える、または"e"加減なことを言う可能性が高い

 

ということを、これから実証します。以下は追加実装しようとしている「文字列検索・置換ダイアログによる検索メソッド」に関わるChat-GPTとのやり取り全てです。ご参考まで。

:実は、原稿は最初はコードを含む全やり取りをコピペしていたのですが。アメブロの「禁則タグ」があるということでアップできず、仕方なく書き直して以下は概要のみ。ご容赦を。

 

(私)またRichTextBox関連の相談です。

C#のRichTextBoxはFindメソッドはありますが、FindReplaceダイアログクラスが無いので、自作しています。 先ず検索を行う為自作FindReplaceダイアログの「次を検索」ボタンの処理を次のように(プログラムコードは除外しました)しています。(このダイアログには呼び出し元のRichTextBoxをコンストラクターの引数として保有しています。) 下方検索は思い通りに行くのですが、上方検索(RichTextBoxFinds.Reverse)の場合開始位置をキャレット、終了位置を0とすると検索せず、Win32のプログラミング経験から開始位置を0、終了位置をキャレットにすると動くには動くのですが、何度も検索を継続して終わりません。(Findメソッドの戻り値が-1にならない。)

 どうしてでしょう?又対処方法はあるでしょうか?

 

(Chat-GPT)

とても良い実装方針です。
確かに RichTextBox.Find()Reverse フラグ付き検索(上方向検索)を行うときに、開始位置と終了位置の解釈が非常に紛らわしく、ドキュメントの説明も実際の挙動と食い違う部分があります。
順方向と逆方向で引数の意味が入れ替わるのが最大の落とし穴です。


🔍 問題の核心

「上方検索の場合、開始位置をキャレット、終了位置を 0 にすると検索しない」
「開始位置を 0、終了位置をキャレットにすると動くが、繰り返し検索が止まらない」

その理由は以下の通りです:

RichTextBox.Find(string, int, int, RichTextBoxFinds) のドキュメント上では

検索は start から end までの範囲で行われる
とありますが、実際の挙動は以下のように異なります。

フラグ 実際の動作 注意点
なし(下方向) start → end の範囲で検索 正常に動作
Reverse(上方向) end → start の範囲で検索 引数が逆に扱われる

つまり、上方向検索では:

  • start < end」のままだと範囲が無効になり、何も見つからない。

  • さらに、「end → start`」の範囲なので、繰り返し検索で範囲を狭めないと無限ループする。


✅ 修正版ロジック

あなたのコードに最小限の修正を加えた安全な実装例を示します。

(解説:間違いなので省略)
💡ポイント解説
  1. 上方向では end < start にしないとダメ

    • 実際には「Find(str, end, start, Reverse)」という順序で呼び出す必要があります。

    • あなたのコードでは Find(str, start, end, Reverse) としていたため範囲が不正でした。

  2. 無限ループ防止

    • foundPos - 1 を次回検索の終了位置にすることで、前回の一致位置を再検出しなくなります。

  3. キャレット位置の扱い

    • 初回はキャレット位置、2回目以降は前回の位置の1つ手前。

 

(私)上のコードでは、実行時に「System.ArgumentException: 値 '0' は 'end' に対して有効な値ではありません。 'end' の値は 'start' 以上か -1 でなければなりません。」というエラーがでます。明らかにChat-GPTさんが言う「上方向では end < start にしないとダメ」は間違っています。このエラーの為に冒頭のコードにした経緯があります。

 

(CHat-GPT)ご指摘ありがとうございます — こちらの説明が間違っていて混乱させてしまい申し訳ありません。
あなたが遭遇した ArgumentExceptionend の値は start 以上か -1 でなければなりません) はまさにその通りで、.NETRichTextBox.Find(...)endstart より小さい値を許さない(-1 を除く)仕様です。公式ドキュメントにもその旨が書かれています。

では、正しい実装方針を短くまとめてから、動作する修正版コードを示します。

要点(簡潔)

実装(あなたの元コードを最小修正した例)

(共に省略)
改めて混乱を招いてしまい失礼しました。上の修正版で ArgumentException は出なくなり、かつ上方向継続検索が同じ一致を繰り返すことも防げるはずです。もし動作確認してまだ不具合が出るようなら、実際の rtBox.SelectionStartfoundPos の値(簡単なログ)を教えていただければ、さらに具体的に調べて対処案を出します。

 

(私)しかしこれって、 

 

(1)RicjhTextBoxFInds.Reverseを設定しているのに、Findメソッドの戻り値仕様(終了-1)にならないという仕様違反 

(2)公式文書でReverseの場合の「開始位置<終了位置」を保持するあまり、開始位置と(実際は終了位置)終了位置(実際は開始位置)に関わる説明を行っていないという説明不完全

 (3)分かっているならReverseでも文頭迄来たら-1を返すようにバグフィクスすればよいのにしていない 事から、Microsoftの怠慢(というかWinFormsを見捨てた?)

 

と感じられますが、如何?

 

(Chat-GPT)良い指摘です —— 要点をきっちり整理して短く答えます。結論から言うと「あなたの感じた不満(仕様の不親切さ・ドキュメント不足)はもっともで、実用上は設計ミスに近い体験を与える」が、裏には互換性・歴史的経緯・優先度の問題が絡んでいる、というのが現実的な説明です。以下、(1)〜(3) それぞれに対する解説と実務的な対処案を示します。

(1) 「Reverse を立てると end - 1 仕様違反になる」

(解説:因みにReverseを入れない下方検索ではきっちり-1を返しますが、つけると返しません。)

(2) 「公式文書が不親切」

(解説:ここ迄は大体同意していますね。)

(3) 「仕様ならば Reverse で文頭に達したら -1 を返すよう修正すれば良いのに放置されている」

  • 理由として考えられる点(推測):

    • 後方互換性の重視:既存のアプリ/ライブラリが期待する挙動を変えると互換性を壊す恐れがある。微妙な仕様変更は破壊的になるため慎重になります。

    • 歴史的経緯RichTextBox は Windows の Rich Edit コントロール(古い API)をラップしたものです。元のコントロールの振る舞いや .NET の過去実装に合わせた結果、いびつなインターフェースが残っていることがよくあります。

    • 優先度の低さ/フォーカスの移行:Microsoft が新しい UI 技術(WPF → WinUI → MAUI 等)に注力した結果、WinForms の細かい UX 修正は優先度が低い可能性があります。

  • つまり「怠慢」と断じるのは理解できる感情的反応ですが、現実的には技術的負債+互換性+優先度の問題が複合していることが多いです。とはいえ設計的に改善の余地が大きいのは確かです — 実務的には『.NET のこの API は信頼できない前提で自前実装に逃げる』のが堅実です。

(解説:利潤追求米国企業だから、という点で諦めちゃっていますよね。)

最後に(あなたの感想への返答)

あなたが「Microsoft の怠慢」と感じるのは自然な感情だと私も思います。設計上の不親切さ・ドキュメント不足は実際の開発コストを増やします。ただ現実的には「歴史的理由」と「互換性の壁」と「開発リソースの優先度」が背景にあり、単純には直せないケースが多い――というのが実務的な事情です。だからこそ現場では「当該 API を信頼しないで自前実装する」か「低レベル API を使って精密に制御する」かの選択が有効になります。

 

(私)「Microsoftの怠慢==金の亡者だから」というのは事実に近いジョークです。(まぁ、ビジネスの世界では仕方がないですがね。でも、米国の原告弁護士なら許さないでしょうが。)「RichTextBox.Find を使わない」方法は大掛かりなので遠慮します。 所で先のC#の推奨コードですが、stPosとedPosの関係で独自終了チェックを追加していますが、終了条件を単に

 

if(foundPos == -1 || edPos < stPos)

 

とするだけで十分じゃないでしょうか?(stPos、edPosはFindメソッドで影響を受けないし、Countも増えません。)

 

(Chat-GPT)はい、まさにその通りです。
あなたの指摘は完全に正確で、次のように整理できます。


✅ 結論:

if (foundPos == -1 || edPos < stPos) のようにまとめてチェックするだけで 十分に合理的かつ安全 です。

🔍 理由の整理:


  1. つまりあなたの書かれたとおり:

if(foundPos == -1 || edPos < stPos) に一本化するだけで十分」

 

──これは設計的にも実装的にも正しい判断です。
そして、その短い一文で「Microsoft の怠慢をカバーする」あたりが、実務家らしい最適解ですね。

 

いずれにしても、これで検索メソッドは完了しました。又、置換メソッドも「選択部分を置換したい文字列に置き換える」処理(RichTextBox.SelectedText = (置換したい文字列);)を追加するだけなので、簡単に完了してしまいました。

 

 

後は「印刷」メソッドなんですが、これも又悩み深い問題があります。又ご紹介しましょう。

 

最後に「【PropertyGrid】とはいえ、何の情報が見たいのか?」を書いた時からちょっと時間が経ちましたが、

 

PCの記憶装置(SSDやHD)に保存しているファイルの属性情報とか、画像ファイルの属性情報や、現在動いているウィンドウの属性情報を調べるツール

 

として表題の"PropertyAny"("Any"はいかにも大袈裟ですが)というプログラムを作りましたので、ソースプログラム(PropertyAny.cs ↓参照)をご紹介いたします。

 

実は最初「こんなプログラム書くのも面倒」とChat-GPTに書かせたら、画面レイアウトが「落第以下」の状態でプッシュボタンが画面の左 2 / 3 で、右にPropertyGridが 1 / 3という奇天烈ものだったので、 彼の基本レイアウトコントロール(SplitterとPanel)を生かして何とかみられるようにしたのが、

 

 

です。でもこれ、ダイアログを操作する際にメインのPropertyGrid画面を交差して(右利き用)マウスを動かすのって(後天的右利きで、原初的左利きである私にも)煩わしく感じます。

 

ということで、

 

結局やり直して

 

 

このようにしました。(起動すると最初に表示されるのはPropertyAnyプログラム自体のプロパティです。)

 

右に3つあるプッシュボタンが「ファイルを開く」ダイアログで選択したファイルの「ファイル情報」、

 

(2025年10月9日追記-ファイルの選択はドラッグアンドドロップでもできます。)

 

次が「ファイルを開く」ダイアログで選択したイメージファイルの「画像情報」で、

 

(2025年10月9日追記-ファイルの選択はドラッグアンドドロップでもできます。)

 

最後に現在デスクトップで動いている(ウィンドウタイトルを持つ)ウィンドウプログラムをListViewで列挙して表示し、その内の好きなものを"SelectWindow”ダイアログ(

:これはWindowInfoクラスを内蔵する"SelectWindows.cs"というスタンドアローンプログラムを作って、それを加工して導入しました。(「ウィンドウタイトルを持つ」プロセスに限定したのは、これで制限しないと「Program Manager状態」になるからです。)

 

 

で選択する「ウィンドウ情報」(以下↓はPropertyAny自体)

 

 

を見ることが出来ます。

 

そっ、唯それだけ。

 

ですが、この仕組みを応用すると他のアプリで面白いことが出来そうな可能性を感じますね。

 

【PropertyAny.cs】

/////////////////////
// PropertyAny.cs
// 参考サイト:https://dobon.net/vb/dotnet/control/propertygrid.html
/////////////////////

using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;
using System.ComponentModel;                //PropertyGridの[]機能を使う為
using System.IO;                                                 //FileInfoオブジェクト取得の為
using System.Diagnostics;                              //GetProcessを使う為
using System.Runtime.InteropServices;    //GetWindowRectを使用するために必要

namespace PropertyAny
{
    public class App : Form
    {
        private SplitContainer split;    //解説:クライアントエリアを分割するスプリッターコントロールです。
        private PropertyGrid propGrid;    //解説:主人公のPropertyGridコントロールです。
        private Panel panel;    //解説:ペインにコントロールを並べるためのコンテナコントロールです。
        private Button btnExit;    //解説:終了ボタン
        private Button btnFile;    //解説:ファイル情報用ボタン
        private Button btnImage;    //解説: 画像情報用ボタン
        private Button btnPickWindow;    //解説:デスクトップのウィンドウ用ボタン

        [STAThread]
        public static void Main()
        {
            Application.Run(new App());
        }

        public App()
        {
            this.Text = "PropertyAny";
            this.Width = 640;
            this.Height = 480;
            this.MinimumSize = new Size(640, 240);
            //Drag and Dropでの処理
            this.AllowDrop = true;
            this.DragEnter += PropGrid_DragEnter;
            this.DragDrop += PropGrid_DragDrop;
            //ウィンドウアイコンをプログラムアイコンにする
            this.Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
/*        //解説:
            Application.ExecutablePathは実行可能ファイル(.exe)の完全なパスを、
            myOwn.Locationはコードが実行されている場所のファイルパスを取得します。
            Application.ExecutablePathはデスクトップアプリケーションで用いられ、
            myOwn.Locationは主にASP.NETなどのウェブアプリケーション環境で、現在の
            リソースの物理パスを取得するために使用されます。但し、myOwnを使うには
            (1)using System.Reflection;    //Assemblyを使う為
            (2)Assembly myOwn = Assembly.GetEntryAssembly();
            とすることが必要です。
*/

            //スプリッター
            split = new SplitContainer();
            split.Dock = DockStyle.Fill;
            split.Orientation = Orientation.Vertical;
            split.SplitterDistance = 240;
            this.Controls.Add(split);
            //左PropertyGrid
            propGrid = new PropertyGrid();
            propGrid.Dock = DockStyle.Fill;
            propGrid.SelectedObject = this;
            split.Panel1.Controls.Add(propGrid);
            //右側側パネル
            panel = new Panel();
            panel.Dock = DockStyle.Fill;
            split.Panel2.Controls.Add(panel);
            //ボタン(解説:panelのコントロールにする)
            btnExit = new Button {Text = "終了", Dock = DockStyle.Bottom};
            btnExit.Height = 30;
            btnExit.Click += BtnExit_Click;
            panel.Controls.Add(btnExit);
            btnPickWindow = new Button {Text = "ウィンドウ情報", Dock = DockStyle.Top};
            btnPickWindow.Height = 30;
            btnPickWindow.Click += BtnPickWindow_Click;
            panel.Controls.Add(btnPickWindow);
            btnImage = new Button {Text = "画像情報", Dock = DockStyle.Top};
            btnImage.Height = 30;
            btnImage.Click += BtnImage_Click;
            panel.Controls.Add(btnImage);
            btnFile = new Button {Text = "ファイル情報", Dock = DockStyle.Top};
            btnFile.Height = 30;
            btnFile.Click += BtnFile_Click;
            panel.Controls.Add(btnFile);
        }

        //終了処理
        private void BtnExit_Click(object sender, EventArgs e)
        {
            DialogResult dr = MessageBox.Show("終了しますか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.Yes)
                Close();    //終了する
        }

        //ファイル情報
        private void BtnFile_Click(object sender, EventArgs e)
        {
            using(OpenFileDialog dlg = new OpenFileDialog())
            {
                dlg.Filter = "すべてのファイル|*.*";
                if(dlg.ShowDialog() == DialogResult.OK)
                {
                    propGrid.SelectedObject = new FileInfo(dlg.FileName);    //解説:FileInfoクラスは既存のクラスです。
                }
            }
        }

        //画像情報
        private void BtnImage_Click(object sender, EventArgs e)
        {
            using(OpenFileDialog dlg = new OpenFileDialog())
            {
                dlg.Filter = "画像ファイル|*.bmp;*.png;*.jpg;*.jpeg;*.gif;*.ico";    //解説:画像はGDI+のものです。
                if (dlg.ShowDialog() == DialogResult.OK)
                {
                    propGrid.SelectedObject = new ImageInfo(dlg.FileName);    //解説:ImageInfoクラスは本プログラムで定義します。
                }
            }
        }

        //ウィンドウ情報
        private void BtnPickWindow_Click(object sender, EventArgs e)
        {
            WindowInfo wi = new WindowInfo();    //解説:WindowInfoクラスは本プログラムで定義します。
            using(WinSelect wsDlg = new WinSelect(ref wi))
            {
                wsDlg.ShowDialog();
                propGrid.SelectedObject = wi;
            }    
        }

        private void PropGrid_DragEnter(object sender, DragEventArgs e)
        {
            if(e.Data.GetDataPresent(DataFormats.FileDrop))
            {
                e.Effect = DragDropEffects.Copy;
            }
        }

        private void PropGrid_DragDrop(object sender, DragEventArgs e)
        {
            string[] files = (string[])e.Data.GetData(DataFormats.FileDrop);
            if(files.Length > 0)
            {
                string ImgExtentions = ".bmp, .png, .jpg, .jpeg, .gif, .ico";
                if(ImgExtentions.Contains(Path.GetExtension(files[0]).ToLower()))    //解説:イメージファイルの場合
                {
                    DialogResult dr = MessageBox.Show("ファイル情報にしますか(はい)、またはイメージ情報にしますか(いいえ)?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
                    if(dr == DialogResult.Yes)    //解説:ファイル情報で表示するか、画像情報で表示するか選択する必要があります。
                        propGrid.SelectedObject = new FileInfo(files[0]);
                    else
                        propGrid.SelectedObject = new ImageInfo(files[0]);
                }
                else
                    propGrid.SelectedObject = new FileInfo(files[0]);
            }
        }
    }

    //イメージ情報取得クラス解説:表示用の情報をプロパティにしておきます。)
    [DefaultProperty("Path")]
    public class ImageInfo
    {
        private Image image;
        private string path;

        [Description("イメージファイルのパスです。")]
        public string Path {get{return path;}}
        [Description("イメージファイルのサイズです。")]
        public Size Size {get{return image.Size;}}
        [Description("イメージファイルのピクセルフォーマットです。")]
        public PixelFormat PixelFormat{get{return image.PixelFormat;}}
        [Description("イメージファイルの画像フォーマットです。")]
        public ImageFormat Format{get{return image.RawFormat;}}

        public ImageInfo(string filePath)
        {
            path = filePath;
            image = Image.FromFile(filePath);
        }
    }

    //ウィンドウ情報取得クラス解説:表示用の情報をプロパティにしておきます。)
    [DefaultProperty("wTitle")]
    public class WindowInfo
    {
        //メンバープロパティ
        [Description("ウィンドウタイトルです。")]
        public string wTitle {get; set;}
        [Description("プロセス名です。")]
        public string wProc {get; set;}
        [Description("ウィンドウハンドルの値です。")]
        public ulong wHnd {get; set;}
        [Description("ウィンドウの幅です。")]
        public int wWidth {get; set;}
        [Description("ウィンドウの高さです。")]
        public int wHeight {get; set;}
        [Description("ウィンドウプログラムのファイルパスです。")]
        public string wPath {get; set;}
    }

    //ウィンドウ選択ダイアログ(解説:現在動いているプロセスの内、ウィンドウタイトルがあるものをピックアップしてリストビューで表示させ、それから好みのものを選択するダイアログです。)
    public partial class WinSelect : Form
    {    //解説:ウィンドウサイズを取得する為に、Win32の構造体と関数を一部使用しています。
        //RECT構造体を定義
        [StructLayout(LayoutKind.Sequential)]
        public struct RECT
        {
            public int Left;
            public int Top;
            public int Right;
            public int Bottom;
        }
        //GetWindowRect関数を宣言
        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

        //メンバーコントロール
        ListView listView;
        Button btnOK, btnCancel;
        //メンバーフィールド
        WindowInfo wi;

        //コンストラクター
        public WinSelect(ref WindowInfo WinInfo)    //解説:参照引数WinInfoに書き込む形にしました。
        {
            wi = WinInfo;
            this.FormBorderStyle = FormBorderStyle.Sizable; 
            this.MinimizeBox = false;
            this.MaximizeBox = false;
            this.Width = 420;
            this.Height = 240;
            this.Text = "Select Windows";
            this.Load += WinSelect_Load;
        }
 
         //WM_CLOSE時処理
        protected override void OnFormClosing(FormClosingEventArgs e)
        {
            base.OnFormClosing(e);
            string msg;
            if(listView.SelectedItems.Count > 0)
                msg = "\"" + listView.SelectedItems[0].Text + "\"が選択されました。\r\n";
            else
                msg = "何も選択されていませんが、";
            DialogResult dr = MessageBox.Show(msg + "宜しいですか?", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
            if(dr == DialogResult.No)    //解説:ダイアログ終了時に選択内容を確認します。
            {
                listView.SelectedItems.Clear();
                e.Cancel = true;
            }
        }

        //WM_CREATE時処理
        private void WinSelect_Load(object sender, EventArgs e)
        {
            //OKボタン
            btnOK = new Button();
            btnOK.Location = new Point(ClientSize.Width - btnOK.Width - 10, ClientSize.Height - btnOK.Height - 10);
            btnOK.Text = "OK";
            btnOK.Anchor = (AnchorStyles.Bottom | AnchorStyles.Right);
            btnOK.Click += btnOK_Click;
            this.Controls.Add(btnOK);

            //Cancelボタン
            btnCancel = new Button();
            btnCancel.Location = new Point(10, ClientSize.Height - btnCancel.Height - 10);
            btnCancel.Text = "Cancel";
            btnCancel.Anchor = (AnchorStyles.Bottom | AnchorStyles.Left);
            btnCancel.Click += btnCancel_Click;
            this.Controls.Add(btnCancel);

            //リストビュー
            listView = new ListView();
            //位置
            listView.Location = new Point(10, 10);
            listView.Anchor = (AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right);
            //サイズ
            listView.Width = ClientSize.Width - 20;
            listView.Height = ClientSize.Height - btnOK.Height - 30;
            //複数選択
            listView.MultiSelect = false;
            //一行選択
            listView.FullRowSelect = true;
            //グリッドラインの表示
            listView.GridLines = true;
            //並び替え表示
            listView.Sorting = SortOrder.Ascending;
            //ポイントで選択できるようにする
            listView.HoverSelection = false;
            //シングルクリック選択
            listView.Activation = ItemActivation.OneClick;
            //チェックボックス有効化
            listView.CheckBoxes = false;
            //リストビューの表示方法
            listView.View = View.Details;
            //ヘッダー定義
            ColumnHeader columnTitle = new ColumnHeader{Text = "タイトル", Width = 140};
            ColumnHeader columnProc = new ColumnHeader{Text = "プロセス", Width = 140};
            ColumnHeader columnHdl = new ColumnHeader{Text = "ハンドル", Width = 100};
            ColumnHeader columnWid = new ColumnHeader{Text = "幅", Width = 50};
            ColumnHeader columnHgt = new ColumnHeader{Text = "高さ", Width = 50};
            ColumnHeader columnPath = new ColumnHeader{Text = "ファイルパス", Width = 240};
            ColumnHeader[] colHeaderRegValue = {                //ヘッダー設定
                columnTitle,
                columnProc,
                columnHdl,
                columnWid,
                columnHgt,
                columnPath
            };
            listView.Columns.AddRange(colHeaderRegValue);        //ヘッダー追加
            //データ設定
            foreach (Process proc in Process.GetProcesses())    //全てのプロセスを列挙する
            {
                //メインウィンドウのタイトルがある時だけ列挙する
                if(proc.MainWindowTitle.Length > 0)
                {
                    ListViewItem item = listView.Items.Add(proc.MainWindowTitle);
                    item.SubItems.Add(proc.ProcessName);
                    item.SubItems.Add(proc.MainWindowHandle.ToString());
                    RECT rec;
                    GetWindowRect(proc.MainWindowHandle, out rec);
                    int w = rec.Right - rec.Left;
                    int h = rec.Bottom - rec.Top;
                    item.SubItems.Add(w.ToString());
                    item.SubItems.Add(h.ToString());
                    item.SubItems.Add(proc.MainModule.FileName);
                }
            }
            //FormにListViewを追加
            this.Controls.Add(listView);
        }

        //解説:OKボタン(「はい」)を押した際のエラーチェック、参照引数への書き込み処理
        private void btnOK_Click(object sender, EventArgs e)
        {
            if(listView.SelectedItems.Count == 0)
            {
                MessageBox.Show("リスト項目が未選択です。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                return;
            }
            wi.wTitle = listView.SelectedItems[0].SubItems[0].Text;
            wi.wProc = listView.SelectedItems[0].SubItems[1].Text;
            ulong val;
            if(!ulong.TryParse(listView.SelectedItems[0].SubItems[2].Text, out val))
            {
                MessageBox.Show("ハンドルの値が不正です。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                wi.wHnd = 0;
            }
            else
                wi.wHnd = val;
            int len;
            if(!int.TryParse(listView.SelectedItems[0].SubItems[3].Text, out len))
            {
                MessageBox.Show("幅の値が不正です。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                wi.wWidth = 0;
            }
            wi.wWidth = len;
            if(!int.TryParse(listView.SelectedItems[0].SubItems[4].Text, out len))
            {
                MessageBox.Show("高さの値が不正です。", "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
                wi.wHeight = 0;
            }
            wi.wHeight = len;
            wi.wPath = listView.SelectedItems[0].SubItems[5].Text;
            Close();
        }

        //解説:Cancelボタン(「いいえ」)を押した際の初期化処理
        private void btnCancel_Click(object sender, EventArgs e)
        {
            listView.SelectedItems.Clear();
            wi.wTitle = "";
            wi.wProc = "";
            wi.wHnd = 0;
            wi.wWidth = 0;
            wi.wHeight = 0;
            wi.wPath = "";
            Close();
        }
    }
}

前回の【無駄話】昔のC++プログラマーが陥りやすい、C#<WinForms>プログラミングの盲点の末尾のpsで、

 

「印刷と『検索、置換()』で躓いています。

:Win32のコモンダイアログである「検索、置換ダイアログ(FindText関数、ReplaceText関数で呼び出す)」であれば、BCCSkeltonのCREDITクラスで簡単に、


bool CMyWnd::OnFind(HWND hWnd) {
    Edit.SetHandle(GetDlgItem(hWnd, IDC_CHILD_CONTROL));
    m_hDlg = Edit.m_hFRDlg = FindText(Edit.SetFRDlg('F'));
    return TRUE;
}

bool CMyWnd::OnReplace(HWND hWnd) {
    Edit.SetHandle(GetDlgItem(hWnd, IDC_CHILD_CONTROL));
    m_hDlg = Edit.m_hFRDlg = ReplaceText(Edit.SetFRDlg('R'));
    return TRUE;
}

行えたのですが、C#では「検索、置換ダイアログ」が無い、ということらしく、一から始めなければなりません。┐(´д`)┌ヤレヤレ」

 

と愚痴りました。その後、やっと見つけたRichTextBoxの印刷機能に関わる記事を読んでコードを書いたのですが、不明のエラーが出るので、またまたChat-GPT様に縋ってしまいました。

 

(私)「C#でRichTexxtBoxを使って印刷を行おうと次のようなコードを組むと、コンパイル時に「error CS1061: 'System.Windows.Forms.RichTextBox' に 'Print' の定義が含まれておらず、型 'System.Windows.Forms.RichTextBox' の最初の引数を受け付ける拡張メソッドが見つかりませんでした。using ディレクティブまたはアセンブリ参照が不足しています。」というエラーがでます。C#のVersionの問題でしょうか?

 

//印刷ダイアログを表示する場合

            PrintDialog prtDlg = new PrintDialog();
            prtDlg.Document = new PrintDocument();
            //ダイアログを表示し、ユーザーがプリンターを選択できるようにする
            if(prtDlg.ShowDialog() == DialogResult.OK)
            {
                //ユーザーがOKを押したら印刷を実行
                rtxtBox.Print(printDialog.Document); // PrintDialog.Documentにはユーザーが選択したプリンターの情報が格納されている
            }
            prtDlg.Dispose();

 

(Chat-GPT)「(前略)

  • rtxtBox.Print(...) のような簡単なAPIは 存在しない(WinFormsの制限)。

  • WPF(System.Windows.Controls.RichTextBox)では PrintDialog.PrintDocument() を使う方法もあるが、WinFormsとは別物。

(後略)

 

ということで、本日「C#5のWinFormsには文字列の検索、置換や印刷のメソッドは無い!」という驚くべき事実を知り、またまた落ち込んでいます。しかしこのまま引き下がるようでは「昭和の男」ではありません。

 

そーか、そ-か、わかった、だったら仕様があるまい!無いんだったらつくってやるよ!

 

ということで、【PropertyGrid】の後にこれをネタにします。

現在【PropertyGrid】シリーズが進行中ですが、どういう訳かWebを見ていて「あー、昔BCCSkeltonでANSI用、その後ECCSkeltonでUnicode用エディターを作ったRichEditコントロール、懐かしいなぁ。一つ、サンプルのテキストエディターを改造してC#で簡単なRichEditorを作ってみようかしら?」などと、私は発作的に考えてしまいました。

 

しっかし、これが全然簡単ではない!

 

ツールバーのボタンをやり直したり、透明化にてこずったりしてなかなか前に進みません。(【PropertyGrid】をほっぽらかして)旧いメニュー関連コードをMenuStripを使ったコードに書き直したりしていると、またまた思ったような動作をしません。

 

さんざんっぱら、試行錯誤を繰り返し、何とか少し前進したのですが、色々とトラップに嵌ったことが判り、それを【無駄話】のネタにしようと考えましたが、余りにつかれて面倒臭いので、Chat-GPTに書かせてしまいました。(あー、ドンドン馬鹿になってゆくなぁ。)

 

1. メニューは「ウィンドウ属性」ではなく「コントロール」

  • Win32 API: メニューはウィンドウに結びつく特別枠。GetClientRect で取得したクライアント領域は自動でメニュー高さを除外

  • WinForms: MenuStrip は単なる Control

    • ClientSize に高さは反映されない

    • 描画上は上に見えるが、プログラム的には自分で領域を計算する必要がある

2. Docked コントロールは追加順(FILO)で積み重なる

  • MenuStrip, ToolStrip, StatusStrip はすべて Dock = Top / Bottom のコントロール

  • Controls.Add() の順序 = Z-order

    • 後に追加したものほど上に表示される

    • 例: 「先にツールバー追加 → 後にメニュー追加」ならメニューが上に来る

    • 順序調整には Controls.SetChildIndex(menuStrip, 0) も利用可能

3. アクセスキーとショートカットキーの区別

  • Win32: &N をつけると Alt+キーで動く

  • WinForms:

    • Text = "新規(&N)" → Alt+N でアクセス

    • ShortcutKeys = Keys.Control | Keys.N → Ctrl+N でショートカット

    • アクセスキーとショートカットキーは別に設定する必要がある

4. クライアント領域計算は自分で行う

  • MenuStripToolStrip の高さを考慮して描画領域を求める必要あり

  • 例: int usableHeight = this.ClientSize.Height - menuStrip.Height - toolStrip.Height - statusStrip.Height;

5. イベント処理も Win32 とは異なる

  • Win32: WM_COMMAND メッセージで処理

  • WinForms: Click イベントや CheckedChanged などでハンドラを登録

  • メニュー/ツールバー/コントロールごとに個別にイベントを設定する

6. Designer とコードの順序に注意

  • デザイナを使う場合、コントロールの追加順や Dock 順序が自動で決まる

  • 手動でコードを書く場合は、追加順が Z-order に直結するので注意


💡 まとめ
WinForms は「すべてコントロール」として統一された設計

  • メニューもツールバーもステータスバーも同じ扱い

  • そのため、Win32 API 時代の「自動で領域を除外」「ウィンドウ属性」の感覚は通用しない

移行時は クライアント領域計算・Z-order・ショートカットキー設定・イベント登録 に注意するとスムーズにコーディングできる。(以下は参考図)

 

 

どうでしょうか?少しは参考になったかしら?

 

ps. 現在のRichTextBoxを使ったエディターの状況はこの(↓)通り。

ほぼ80%の機能はカバーできましたが、後印刷と「検索、置換(注)」で躓いています。

注:Win32のコモンダイアログである「検索、置換ダイアログ(FindText関数、ReplaceText関数で呼び出す)」であれば、BCCSkeltonのCREDITクラスで簡単に、


bool CMyWnd::OnFind(HWND hWnd) {
    Edit.SetHandle(GetDlgItem(hWnd, IDC_CHILD_CONTROL));
    m_hDlg = Edit.m_hFRDlg = FindText(Edit.SetFRDlg('F'));
    return TRUE;
}

bool CMyWnd::OnReplace(HWND hWnd) {
    Edit.SetHandle(GetDlgItem(hWnd, IDC_CHILD_CONTROL));
    m_hDlg = Edit.m_hFRDlg = ReplaceText(Edit.SetFRDlg('R'));
    return TRUE;
}


行えたのですが、C#では「検索、置換ダイアログ」が無い、ということらしく、一から始めなければなりません。┐(´д`)┌ヤレヤレ

 

ps. of ps 大分RichTextBoxのエディターに嵌ってしまいましたが、大丈夫、【PropertyGrid】は覚えていますので、ちゃんと終了させます。

前回はPropertyGridで見ることのできる対象であるWindowsの共通言語基盤(CLI)上で動く.NETクラス階層内Objectを概観し、共通言語仕様(CLS)に則ったOOP上のObjectの有するメンバープロパティはそのまま、Win32API等で動く旧いプログラム等もその情報を取得してObject化すればPropertyGridで表示ができることが判りました。しかし、...

 

じゃ、一体なにが見たいの?

 

が未だ分かりません。出来合いのMicrosoftのオブジェクトのプロパティを眺めても()、

 

前々回のサンプルプログラムで"propGrid.SelectedObject = this;"という行でウィンドウプログラム自体を対象にしていましたが、今回は"propGrid.SelectedObject = propGrid;"としてPropertyGridコントロールを設定してみました。

 

単に「フーン」という反応しかないでしょう。

 

恐らくは

 

何かゲームかツールのプレイ|使用上のパラメーターを変更する場合のUI(ユーザーインターフェース)に使う

 

というのが正解のようですが、現在そのような「手持ち駒」はありませんので、取り敢えず、

 

PCの記憶装置(SSDやHD)に保存しているファイルの属性情報(注)とか、画像ファイルの属性情報や、現在動いているウィンドウの属性情報を調べるツール

 

などを作ってみましょうかね?(ってか、それ以外に直ぐに手に入る対象が余り思い浮かびませんでした。)

注:これは出来合いのFileInfoクラス前々回やりましたね?

 

では、又次回!