前回RichEditorのエディター本体部分を三連発(冒頭~メニュー1、ツールバー2、メソッド実装部分3)してしまい、「見たくもないっ!」と退かれてしまったかも?
今回はまず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)へ続きます。


















