////////////////////////////////////////////
// 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
	{
		//Win32関連
		[StructLayout(LayoutKind.Sequential)]
		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;
		}

		[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()
		{
			//印刷用クラスインスタンス
			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;		//ネットワークプリンターは無視する
			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() + " -";				//C#5だから
			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）
		}

		//文字列の検索
		public void Find()
		{
			FRDialog frDlg = new FRDialog(this, false);
			frDlg.Show();
		}

		//文字列の置換
		public void Replace()
		{
			FRDialog frDlg = new FRDialog(this, true);
			frDlg.Show();
		}
	}

	/////////////////////////////////
	//文字列の検索・置換用ダイアログ
	/////////////////////////////////
	public class FRDialog : Form
	{
		//メンバープロパティ
		string ToBeRep {get; set;}		//「検索文字列」
		string ToRepWith {get; set;}	//「置換文字列」
		//メンバーフィールド
		RichTextBox rtBox;
		int stPos = 0;					//検索・置換始点
		int edPos = 0;					//検索・置換終点
		string ToFind;					//前回の検索文字列
		string ToReplace;				//前回の置換文字列
		int foundPos = 0;				//Findメソッドの結果
		int Count = 0;					//検索・置換回数
		//メンバーコントロール
		private const int UNIT = 4;
		private Label strToFind;
		private TextBox strToFindBox;
		private Label strReplaced;
		private TextBox strReplacedBox;
		private CheckBox ByWord;
		private CheckBox UNotL;
		private Button FindNext;
		private Button ReplaceNext;
		private Button ReplaceAll;
		private Button Cancel;
		private GroupBox gBox;
		private RadioButton Up;
		private RadioButton Down;

		public FRDialog(RichTextBox rte, bool replace = false)	//コンストラクター
		{
			//呼び出したRichTextBoxExを保存
			rtBox = rte;
			//ダイアログ本体
			this.FormBorderStyle = FormBorderStyle.FixedDialog;
			this.MaximizeBox = false;
			this.MinimizeBox = false;
			this.ShowIcon = false;
			this.ShowInTaskbar = false;
			this.StartPosition = FormStartPosition.CenterScreen;	//中央に配置
			this.ClientSize = new Size(UNIT * 120, UNIT * 36);
			this.AcceptButton = this.FindNext;
			this.CancelButton = this.Cancel;
			this.AutoScaleMode = AutoScaleMode.Font;
			this.TopMost = true;
			if(replace)
				this.Text = "置換";
			else
				this.Text = "検索";
			this.SuspendLayout();
			//strToFind
			this.strToFind = new Label();
			this.strToFind.AutoSize = true;
			this.strToFind.Location = new Point(UNIT * 2, UNIT * 3);
			this.strToFind.Size = new Size(UNIT * 28, UNIT * 3);
			this.strToFind.Text = "検索する文字列(&N):";
			this.Controls.Add(this.strToFind);
			//strToFindBox
			this.strToFindBox = new TextBox();
			this.strToFindBox.Location = new Point(UNIT * 40, UNIT * 3);
			this.strToFindBox.Size = new Size(UNIT * 48, UNIT * 5);
			this.strToFindBox.TabIndex = 0;
			this.strToFindBox.Text = ToFind = rte.SelectedText;
			this.strToFindBox.TextChanged += strToFindBox_TextChanged;
			this.Controls.Add(this.strToFindBox);
			//strReplaced
			this.strReplaced = new Label();
			this.strReplaced.Visible = replace ? true : false;
			this.strReplaced.AutoSize = true;
			this.strReplaced.Location = new Point(UNIT * 2, UNIT * 9);
			this.strReplaced.Size = new Size(UNIT * 28, UNIT * 3);
			this.strReplaced.Text = "置換する文字列(&P):";
			this.Controls.Add(this.strReplaced);
			//strReplacedBox
			this.strReplacedBox = new TextBox();
			this.strReplacedBox.Visible = replace ? true : false;
			this.strReplacedBox.Location = new Point(UNIT * 40, UNIT * 9);
			this.strReplacedBox.Size = new Size(UNIT * 48, UNIT * 5);
			this.strReplacedBox.TabIndex = 1;
			this.Controls.Add(this.strReplacedBox);
			//ByWord
			this.ByWord = new CheckBox();
			this.ByWord.AutoSize = true;
			this.ByWord.Location = new Point(UNIT * 3, UNIT * 18);
			this.ByWord.Size = new Size(UNIT * 40, UNIT * 4);
			this.ByWord.TabIndex = 2;
			this.ByWord.Text = "単語単位で探す(&W)";
			this.ByWord.UseVisualStyleBackColor = true;
			this.Controls.Add(this.ByWord);
			//UNotL
			this.UNotL = new CheckBox();
			this.UNotL.AutoSize = true;
			this.UNotL.Location = new Point(UNIT * 3, UNIT * 24);
			this.UNotL.Size = new Size(UNIT * 40, UNIT * 4);
			this.UNotL.TabIndex = 3;
			this.UNotL.Text = "大文字と小文字を区別する(&C)";
			this.UNotL.UseVisualStyleBackColor = true;
			this.Controls.Add(this.UNotL);
			//FindNext
			this.FindNext = new Button();
			this.FindNext.Enabled = false;
			this.FindNext.Location = new Point(UNIT * 92, UNIT * 2);
			this.FindNext.Size = new Size(UNIT * 26, UNIT * 6);
			this.FindNext.TabIndex = 4;
			this.FindNext.Text = "次を検索(&F)";
			this.FindNext.UseVisualStyleBackColor = true;
			this.FindNext.Click += FindNext_Click;
			this.Controls.Add(this.FindNext);
			//ReplaceNext
			this.ReplaceNext = new Button();
			this.ReplaceNext.Visible = replace ? true : false;
			this.ReplaceNext.Enabled = false;
			this.ReplaceNext.Location = new Point(UNIT * 92, UNIT * 10);
			this.ReplaceNext.Size = new Size(UNIT * 26, UNIT * 6);
			this.ReplaceNext.TabIndex = 5;
			this.ReplaceNext.Text = "置換して次に(&R)";
			this.ReplaceNext.UseVisualStyleBackColor = true;
			this.ReplaceNext.Click += ReplaceNext_Click;
			this.Controls.Add(this.ReplaceNext);
			//ReplaceAll
			this.ReplaceAll = new Button();
			this.ReplaceAll.Visible = replace ? true : false;
			this.ReplaceAll.Enabled = false;
			this.ReplaceAll.Location = new Point(UNIT * 92, UNIT * 19);
			this.ReplaceAll.Size = new Size(UNIT * 26, UNIT * 6);
			this.ReplaceAll.TabIndex = 6;
			this.ReplaceAll.Text = "すべて置換(&A)";
			this.ReplaceAll.UseVisualStyleBackColor = true;
			this.ReplaceAll.Click += ReplaceAll_Click;
			this.Controls.Add(this.ReplaceAll);
			//Cancel
			this.Cancel = new Button();
			this.Cancel.DialogResult = DialogResult.Cancel;
			this.Cancel.Location = new Point(UNIT * 92, UNIT * 27);
			this.Cancel.Size = new Size(UNIT * 26, UNIT * 6);
			this.Cancel.TabIndex = 7;
			this.Cancel.Text = "キャンセル";
			this.Cancel.UseVisualStyleBackColor = true;
			this.Cancel.Click += Cancel_Click;
			this.Controls.Add(this.Cancel);
			//gBox
			gBox = new GroupBox();
			this.gBox.Visible = replace ? false : true;
			this.gBox.Location = new Point(UNIT * 56, UNIT * 18);
			this.gBox.Size = new Size(UNIT * 28, UNIT * 15);
			this.gBox.Text = "検索する方向";
			this.Controls.Add(this.gBox);
			//Up
			this.Up = new RadioButton();
			this.Up.Visible = replace ? false : true;
			this.Up.Location = new Point(UNIT * 6, UNIT * 3);
			this.Up.Size = new Size(UNIT * 20, UNIT * 6);
			this.Up.TabIndex = 8;
			this.Up.Text = "上へ(&U)";
			this.Up.UseVisualStyleBackColor = true;
			this.gBox.Controls.Add(this.Up);
			//Down
			this.Down = new RadioButton();
			this.Down.Visible = replace ? false : true;
			this.Down.Location = new Point(UNIT * 6, UNIT * 8);
			this.Down.Size = new Size(UNIT * 20, UNIT * 6);
			this.Down.TabIndex = 9;
			this.Down.Text = "下へ(&D)";
			this.Down.UseVisualStyleBackColor = true;
			this.Down.Checked = true;
			this.gBox.Controls.Add(this.Down);
			//ボタン状態の設定
			FindNext.Enabled = ReplaceNext.Enabled = ReplaceAll.Enabled = (rte.SelectedText.Length > 0);
			//再開
			this.ResumeLayout(false);
			this.PerformLayout();
		}

		//終了処理
		protected override void OnClosed(EventArgs e)
		{
			base.OnClosed(e);
			this.Dispose();			//念の為
		}

		//「検索する文字列」が入力された場合
		private void strToFindBox_TextChanged(object sender, EventArgs e)
		{
			FindNext.Enabled = ReplaceNext.Enabled = ReplaceAll.Enabled =
			(strToFindBox.Text.Length > 0);	//true
		}

		private void FindNext_Click(object sender, EventArgs e)
		{
			//前回の検索文字列の設定
			if(!string.IsNullOrEmpty(ToFind))
				strToFindBox.Text = ToFind;
			//Findメソッドのフラグ
			RichTextBoxFinds flag = RichTextBoxFinds.None;
			//＜RichTextBoxFinds＞
			//名前			値	説明
			//MatchCase		4	大文字と小文字を区別して検索する。
			//NoHighlight	8	検索文字列を強調表示しない。
			//None			0	大小文字の区分、独立した単語か否かに拘わらず検索文字列に一致するものを検索する。
			//Reverse		16	テキストの末尾から先頭に向かって検索を開始する。
			//WholeWord		2	検索文字列に完全一致する単語だけを検索する。
			if(ByWord.Checked == true)			//単語検索
				flag |= RichTextBoxFinds.WholeWord;
			if(UNotL.Checked == true)			//大文字小文字区別
				flag |= RichTextBoxFinds.MatchCase;
			if(Up.Checked == true)				//上方検索
				flag |= RichTextBoxFinds.Reverse;
			if(Down.Checked == true)			//下方検索
			{
				if(Count == 0)	//初回はキャレットのある位置から
				{
					stPos = rtBox.SelectionStart;
					edPos = rtBox.Text.Length;	//文末迄（既定値は下方検索）
				}
				else			//継続中は見つかった文字列の次の位置から
					stPos = foundPos + rtBox.SelectionLength;
			}
			else								//上方検索
			{
				if(Count == 0)	//初回はキャレットのある位置から
				{
					stPos = 0;					//文頭迄
					edPos = rtBox.SelectionStart;
				}
				else			//継続中は見つかった文字列の前の位置から
					edPos = foundPos - 1;
			}
			foundPos = rtBox.Find(strToFindBox.Text, stPos, edPos, flag);
			if(foundPos == -1 || edPos < stPos)	//RichTextBoxのFindはReverseの際に-1を返さない為
			{
				MessageBox.Show("検索文字は " + Count.ToString() + "回見つかりました。", "検索結果", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
				stPos = edPos = foundPos = Count = 0;
				Close();
			}
			else
				Count++;
			rtBox.FindForm().Activate();		//(Form)をキャストしないとActivateメソッドが無いControlになる
			ToFind = strToFindBox.Text;				//今回の検索文字列の記録
		}

		private void ReplaceNext_Click(object sender, EventArgs e)
		{
			//前回の検索・置換文字列の設定
			if(!string.IsNullOrEmpty(ToFind))
				strToFindBox.Text = ToFind;
			if(!string.IsNullOrEmpty(ToReplace))
				strReplacedBox.Text = ToReplace;
			//「次を検索」等で既に検索文字列を選択していれば、先ず置換する
			if(rtBox.SelectedText == strToFindBox.Text)
				rtBox.SelectedText = strReplacedBox.Text;
			//次の置換処理
			RichTextBoxFinds flag = RichTextBoxFinds.None;	//Findメソッドのフラグ（下方検索のみ）
			if(ByWord.Checked == true)			//単語検索
				flag |= RichTextBoxFinds.WholeWord;
			if(UNotL.Checked == true)			//大文字小文字区別
				flag |= RichTextBoxFinds.MatchCase;
			if(Count == 0)						//初回は
			{
				stPos = 0;						//文頭から
				edPos = rtBox.Text.Length;		//文末迄（既定値は下方検索）
			}
			else			//継続中は見つかった文字列の次の位置から
				stPos = foundPos + rtBox.SelectionLength;
			foundPos = rtBox.Find(strToFindBox.Text, stPos, edPos, flag);
			if(foundPos == -1)
			{
				MessageBox.Show("検索文字を " + Count.ToString() + "回置換しました。", "検索結果", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
				stPos = edPos = foundPos = Count = 0;
				Close();
			}
			else
			{
				if(MessageBox.Show("置換しますか？", "確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
				{
					rtBox.SelectedText = strReplacedBox.Text;
					Count++;
				}
				else
					stPos = foundPos + rtBox.SelectionLength;
			}
			rtBox.FindForm().Activate();		//(Form)をキャストしないとActivateメソッドが無いControlになる
			//今回の検索・置換文字列の記録
			ToFind = strToFindBox.Text;
			ToReplace = strReplacedBox.Text;
		}

		private void ReplaceAll_Click(object sender, EventArgs e)
		{
			if(MessageBox.Show("本当にこの置換条件で実行しますか？", "再確認", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
			{
				stPos = 0;						//文頭から
				edPos = rtBox.Text.Length;		//文末迄（既定値は下方検索）
				//全置換処理
				RichTextBoxFinds flag = RichTextBoxFinds.None;	//Findメソッドのフラグ（下方検索のみ）
				if(ByWord.Checked == true)			//単語検索
					flag |= RichTextBoxFinds.WholeWord;
				if(UNotL.Checked == true)			//大文字小文字区別
					flag |= RichTextBoxFinds.MatchCase;
				while((foundPos = rtBox.Find(strToFindBox.Text, stPos, edPos, flag)) != -1)
				{
					rtBox.SelectedText = strReplacedBox.Text;
					Count++;
					stPos = foundPos + rtBox.SelectionLength;
				}
				MessageBox.Show("検索文字を " + Count.ToString() + "回置換しました。", "検索結果", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
				stPos = edPos = foundPos = Count = 0;
				Close();
				rtBox.FindForm().Activate();		//(Form)をキャストしないとActivateメソッドが無いControlになる
				//今回の検索・置換文字列の記録
				ToFind = strToFindBox.Text;
				ToReplace = strReplacedBox.Text;
			}
		}

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