Delphi Starter Edition でメモ帳クローンを作る - 2nd Edition - [編集(E)]（1）

# // これは...

これは [Delphi Advent Calendar 2017](https://qiita.com/advent-calendar/2017/delphi) 15 日目の記事です。
長くなってしまったので機能単位で分割しています。

## [編集(E)]

まずは ActionList にアクションを追加しましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/93d1ce1a-fd75-6ba8-0cb4-e4d94179b962.png)

カテゴリが [Edit] になっている事に注意してください。

|Action (Name)|Caption|Category|ShortCut|
|:-----------------|:------------------|:------------------|:----------------------|
|acUndo|元に戻す(&U)|Edit|Ctrl+Z|
|acCut|切り取り(&T)|Edit|Ctrl+X|
|acCopy|コピー(&C)|Edit|Ctrl+C|
|acPaste|貼り付け(&P)|Edit|Ctrl+V|
|acDelete|削除(&L)|Edit|Del|
|acFind|検索(&F)...|Edit|Ctrl+F|
|acFindNext|次を検索(&N)|Edit|F3|
|acReplace|置換(&R)...|Edit|Ctrl+H|
|acGoTo|行へ移動(&G)...|Edit|Ctrl+G|
|acSelectAll|すべて選択(&A)|Edit|Ctrl+A|
|acTimeDate|日付と時刻(&D)|Edit|F5|

すべて登録するとこうなります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/40a66e30-f9cb-8af8-e6c0-728c540e24bb.png)

次に MainMenu1 のコンポーネントエディタを開き、[ファイル(F)] の右横を選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/f24c6c51-2544-b222-e44c-c6b8480e0a11.png)

オブジェクトインスペクタで Caption プロパティに **編集(&E)** と入力します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/8ce15324-cf9c-5498-c7b6-d04fd30eca38.png)

[編集(E)] メニューができました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/037c9358-56aa-3e29-55fa-a7a9c8c34412.png)

今度は MainMenu1 のコンポーネントエディタで [編集(E)] メニューの下をクリックして、Edit のアクションを割り当てていきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/420535b8-732c-8dcc-1ae9-56c96c4877c2.png)

セパレーターも入れておきましょう。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/c1bbd503-97e3-db54-ea68-dcf388b72a6c.png)

以下、実装となります。

### [元に戻す(U)]
いわゆる**アンドゥ**です。以前の操作を取り消します。

この機能は TMemo に最初から備わっており、例えば acUndo の ShortCut プロパティが空だったとしても、AAA と入力した後に〔Ctrl〕+〔Z〕を押すと入力が取り消されます。逆に ShortCut プロパティに Ctrl+Z が割り当てられていると、コードを記述しなくてはなりません。

```pascal:frmuMain.pas
procedure TForm1.acUndoExecute(Sender: TObject);
// Action: 元に戻す(U)
begin
  Memo1.Undo;
end;
```

TMemo が持ってる機能なので簡単ですね。これでアンドゥの実装が終わりかと思ったらそうではありません。この [元に戻す(U)] メニューは**アンドゥができない状態の時はグレーアウトする**のです。例えばメモ帳を起動した直後に [編集(E)] メニューをドロップダウンしてみてください。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/e543d3d2-ea9d-757b-a04c-f9d476dddcc2.png)

これを実現するにはいくつか方法の方法がありますが、今回は ActionList の OnUpdate イベントを使ってみましょう。[このイベントの説明](http://docwiki.embarcadero.com/Libraries/ja/System.Actions.TContainedActionList.OnUpdate)にはこうあります。

> アプリケーションがアイドル状態に発生し、
> アクション リストが、リスト内の特定のアクションを更新できるようにします。

つまりは

 - アプリケーションが暇なときに発生する。
 - ここでアクションの状態を変更しましょう。

という事です。


オブジェクトインスペクタで ActionList1 の OnUpdate イベントの値の部分をダブルクリックし、ActionList1Update() イベントハンドラを作ります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/f3df41df-0a32-1bef-c08a-ba13c771820a.png)

acUndo の有効/無効は [Enabled プロパティ](http://docwiki.embarcadero.com/Libraries/ja/Vcl.ActnList.TAction.Enabled)で切り替えられます。アクションがメニューアイテムに割り当てられている時、[Enabled プロパティ](http://docwiki.embarcadero.com/Libraries/ja/Vcl.ActnList.TAction.Enabled)が False の場合にはメニューアイテムはグレーアウトします。

そして、TMemo がアンドゥ可能かどうかは [CanUndo プロパティ](http://docwiki.embarcadero.com/Libraries/ja/Vcl.StdCtrls.TCustomEdit.CanUndo)で調べられます。つまり、このようなコードになります。

```pascal:frmuMain.pas
// リスト更新

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  // [元に戻す(U)] の有効/無効
  acUndo.Enabled := Memo1.CanUndo;
end;
```
〔Alt〕,〔E〕,〔U〕や〔Ctrl〕+〔Z〕で [元に戻す(U)] が実行される事を確認してください。

### [切り取り(T)]
このメニューは、

 - 選択した文字を切り取ってクリップボードにコピー
 - 文字が選択されていなければグレーアウト

という処理を行う必要があります。

前者は TMemo の [CutToClipboard メソッド](http://docwiki.embarcadero.com/Libraries/ja/Vcl.StdCtrls.TCustomEdit.CutToClipboard)を使います。

```pascal:frmuMain.pas
procedure TForm1.acCutExecute(Sender: TObject);
// Action: 切り取り(T)
begin
  Memo1.CutToClipboard;
end;
```
後者は ActionList1Update() に追記すればいいです。TMemo には選択された文字数を返すメソッド [SelLength()](http://docwiki.embarcadero.com/Libraries/ja/Vcl.StdCtrls.TCustomEdit.SelLength) を使い、文字数が 0 かどうかで有効無効を切り替えればいいでしょう。 

```pascal:frmuMain.pas
// リスト更新

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  (中略)

  // [切り取り(T)] の有効/無効
  acCut.Enabled := Memo1.SelLength <> 0;
end;
```

〔Alt〕,〔E〕,〔T〕や〔Ctrl〕+〔X〕で [切り取り(T)] が実行される事を確認してください。


### [コピー(C)]
このメニューは、

 - 選択した文字をコピークリップボードにコピー
 - 文字が選択されていなければグレーアウト

という処理を行う必要があります。

前者は TMemo の [CopyToClipboardメソッド](http://docwiki.embarcadero.com/Libraries/ja/Vcl.StdCtrls.TCustomEdit.CopyToClipboard)を使います。

```pascal:frmuMain.pas
procedure TForm1.acCopyExecute(Sender: TObject);
// Action: コピー(C)
begin
  Memo1.CopyToClipboard;
end;
```
後者は [切り取り(T)] と同様、[SelLength()](http://docwiki.embarcadero.com/Libraries/ja/Vcl.StdCtrls.TCustomEdit.SelLength) を使ったロジックとなります。

```pascal:frmuMain.pas
// リスト更新

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  (中略)

  // [コピー(C)] の有効/無効
  acCopy.Enabled := Memo1.SelLength <> 0;
end;
```

〔Alt〕,〔E〕,〔C〕や〔Ctrl〕+〔C〕で [コピー(C)] が実行される事を確認してください。


### [貼り付け(P)]
このメニューは、

 - クリップボードからテキストを貼り付け
 - クリップボードにテキスト形式で格納されていなければグレーアウト

という処理を行う必要があります。

前者は TMemo の [PasteFromClipboardメソッド](http://docwiki.embarcadero.com/Libraries/ja/Vcl.StdCtrls.TCustomEdit.PasteFromClipboard)を使います。

```pascal:frmuMain.pas
procedure TForm1.acPasteExecute(Sender: TObject);
// Action: 貼り付け(P)
begin
  Memo1.PasteFromClipboard;
end;
```

後者は...ちょっと難しいですよ。クリップボードが特定の形式...今回ですとテキスト形式を格納しているかどうかを調べるには、まず uses 句に [Vcl.Clipbrd](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Clipbrd) を追加します。

```pascal:frmuMain.pas
unit frmuMain;

interface

uses
  ..., Vcl.Clipbrd;

  ...
```
このユニットを使うと、TClipboard のインスタンスを返す [Clipboard() 関数](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Clipbrd.Clipboard)が使えます。TClipboard にはクリップボードに特定のフォーマットのデータが含まれるかを調べる [HasFormat() メソッド](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Clipbrd.TClipboard.HasFormat)がありますので、このメソッドでテキスト形式 (CF_TEXT) が格納されているかどうかを調べればよさそうです。

```pascal:frmuMain.pas
// リスト更新

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  (中略)

  // [貼り付け(P)] の有効/無効
  acPaste.Enabled := ClipBoard.HasFormat(CF_TEXT);
end;
```
〔Alt〕,〔E〕,〔P〕や〔Ctrl〕+〔V〕で [貼り付け(P)] が実行される事を確認してください。


### [削除(L)]
このメニューは、

 - 選択文字を削除。
 - 文字が選択されていなければグレーアウト

という処理を行う必要があります。

前者は TMemo の [ClearSelectionメソッド](http://docwiki.embarcadero.com/Libraries/ja/Vcl.StdCtrls.TCustomEdit.ClearSelection)を使います。

```pascal:frmuMain.pas
procedure TForm1.acDeleteExecute(Sender: TObject);
// Action: 削除(L)
begin
  Memo1.ClearSelection;
end;
```

後者は [切り取り(T)] や [コピー(C)] と同様、[SelLength()](http://docwiki.embarcadero.com/Libraries/ja/Vcl.StdCtrls.TCustomEdit.SelLength) を使ったロジックとなります。

```pascal:frmuMain.pas
// リスト更新

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  (中略)

  // [削除(L)] の有効/無効
  acDelete.Enabled := Memo1.SelLength <> 0;
end;
```

あれ？選択された文字は消せますが、選択されていない文字を〔Del〕キーで消せませんね。これは acDelete のショートカットに Del が指定されているからです。ちょっと姑息な方法でこれを回避しましょう。

 - 見えてない時は acDelete のショートカットを空にする。
 - メニューが見えてる時はショートカットとして〔Del〕キーを割り当てる。

この [編集(E)] を選択し、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/62fd5a8d-b311-b0f9-c52a-d95bcb8cff9e.png)

オブジェクトインスペクタで確認します。このメニューアイテム名は E1 のようですね。これの OnClick イベントをダブルクリックしてイベントハンドラを生成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/dad2d225-9bfd-83b4-ba6c-5ea5725275d8.png)

[編集(E)] がクリックされた時の処理ですね。acDelete の [ShortCut プロパティ](http://docwiki.embarcadero.com/Libraries/ja/System.Classes.TShortCut)に〔Del〕キーを割り当てます。

```pascal:frmuMain.pas
procedure TForm1.E1Click(Sender: TObject);
begin
  acDelete.ShortCut := TextToShortCut('Del');
end;
```

そして、暇なときには [ShortCut プロパティ](http://docwiki.embarcadero.com/Libraries/ja/System.Classes.TShortCut)を空にします。

```pascal:frmuMain.pas
// リスト更新

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  (中略)

  // [削除(L)] の有効/無効
  acDelete.Enabled := Memo1.SelLength <> 0;
  acDelete.ShortCut := 0; // <----- 追加
end;
```

〔Alt〕,〔E〕,〔L〕や〔Del〕で [削除(L)] が実行される事を確認してください。

 - [キーおよびショートカットの表現 (DocWiki)](http://docwiki.embarcadero.com/RADStudio/ja/%E3%82%AD%E3%83%BC%E3%81%8A%E3%82%88%E3%81%B3%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%88%E3%82%AB%E3%83%83%E3%83%88%E3%81%AE%E8%A1%A8%E7%8F%BE)


### [検索(F)]
このメニューは、

 - 検索ダイアログを出す。
 - [次を検索(F)] ボタンが押されたら、検索条件を基に検索。
 - 検索条件に一致しなかったら 「"検索文字列" が見つかりません。」というメッセージを出す。
 - 一致したら文字列を選択状態にする。
 - メモに何も入力されていなかったら [検索(F)] メニューをグレーアウト

という処理を行う必要があります。

検索ダイアログは [TFindDialog](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.TFindDialog) がツールパレットの [Dialogs] にありますので、これを追加しておきます (**FindDialog1**)。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/9764c2a4-751a-6098-4abc-12f4b822d5b4.png)

プレビューしてみると **単語単位で探す(W)** がありますが、これは利用しないので、[Options プロパティ](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.TFindDialog.Options)の frHideWholeWord フラグを True にします。プロパティとフラグの関係はこのようになっています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/826e61e8-61c1-0556-cab3-d3c8cf3282bb.png)

これを踏まえて acFind アクションのイベントハンドラのコードを書くとこうなります。

```pascal:frmuMain.pas
procedure TForm1.acFindExecute(Sender: TObject);
// Action: 検索(F)
begin
  FindDialog1.Execute;
end;
```
もちろんこれだけでは検索されません。検索の処理は TFindDialog の onFind イベントのイベントハンドラも記述する必要があります。検索処理を文字列処理だけで記述すると割と大変なので、**正規表現ライブラリ**を使います。

```pascal:frmuMain.pas
unit frmuMain;

interface

uses
  ..., System.RegularExpressions, System.UITypes; // <- 追加
```

uses に [System.RegularExpressions](http://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions) を追加すると正規表現ライブラリが使えるようになります。メッセージダイアログも使うので、[System.UITypes](http://docwiki.embarcadero.com/Libraries/ja/System.UITypes) もついでに追加しておきます。

次に、イベントハンドラ FindDialog1Find() を記述します。オブジェクトインスペクタで OnFind イベントの値部分をダブルクリックして FindDialog1Find() イベントハンドラを生成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/cb8e4639-5a09-72a5-4a2a-5173fc018c1e.png)

ロジックは以下のようになります。

```pascal:frmuMain.pas
procedure TForm1.FindDialog1Find(Sender: TObject);
var
  RegEx: TRegEx;
  Exp, Msg: String;
  Options: TRegExOptions;
  Match, dMatch: TMatch;
  StartPos, EndPos: Integer;
begin
  // 検索文字列を正規表現文字列へエスケープ
  Exp := TRegEx.Escape(FindDialog1.FindText);

  // "大文字と小文字を区別する" にチェックが入っていなければ
  // オプションに roIgnoreCase (大文字小文字を区別しない) を追加
  if not (frMatchCase in FindDialog1.Options) then
    Include(Options, roIgnoreCase);

  // 正規表現オブジェクトの生成
  RegEx := TRegEx.Create(Exp, Options);

  if (frDown in FindDialog1.Options) then
    begin
      // 順方向検索 (下へ)
      StartPos  := Memo1.SelStart + Memo1.SelLength + 1;
      Match := RegEx.Match(Memo1.Lines.Text, StartPos);
    end
  else
    begin
      // 逆方向検索 (上へ)
      StartPos := 0;
      EndPos   := Memo1.SelStart + 1;
      Match := RegEx.Match(Memo1.Lines.Text, StartPos, EndPos);
      while Match.Success do
        begin
          // 最後にマッチした箇所を探す
          dMatch := Match.NextMatch;
          if dMatch.Success then
            Match := dMatch
          else
            break;
        end;
    end;

  if Match.Success then
    begin
      // 一致する文字列があった
      SetFocus;
      Memo1.SelStart := Match.Index - 1;
      Memo1.SelLength := Match.Length;
    end
  else
    begin
      // 一致する文字列がなかった
      Msg := Format('"%s" が見つかりません。', [FindDialog1.FindText]);
      MessageDlg(Msg, TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], -1);
    end;
end;
```
[TRegEx](http://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions.TRegEx) が正規表現用のレコードです。つまり、**正規表現を平文検索に使っています**。TRegEx.Escape() を使うのがミソです。

なぜ正規表現ライブラリを使うのかというと、以下のような利点があるからです。

 - "単語単位で探す(W)" のような機能拡張が簡単
 - バグがない
 - 高速に動作する

[Pos()](http://docwiki.embarcadero.com/Libraries/ja/System.Pos) や [PosEx()](http://docwiki.embarcadero.com/Libraries/ja/System.StrUtils.PosEx) を使えば単純な検索ロジックを構築するのは難しくはないのですが、”単語単位で検索” のような事をやろうとすると途端に難易度が上がります。そうして苦労して作ったロジックよりも正規表現ライブラリを使った方が高速に動作する、なんて事はよくある話です。

ロジックのすべてを説明すると長くなるので、各オブジェクトの説明へのリンクを張っておきます。

 - [Vcl.StdCtrls.TMemo](http://docwiki.embarcadero.com/Libraries/ja/Vcl.StdCtrls.TMemo)
 - [System.RegularExpressions.TRegEx](http://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions.TRegEx)
 - [System.RegularExpressions.TMatch](http://docwiki.embarcadero.com/Libraries/ja/System.RegularExpressions.TMatch)
 - [System.SysUtils.Format](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.Format)
 - [Vcl.Dialogs.MessageDlg](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.MessageDlg)
 - [正規表現の活用 (主に Delphi 2009 以降) (VCL によるテクニック＆アルゴリズム)](http://ht-deko.com/tech064.html)

おっと、グレーアウトの処理も追加しなくてはなりませんね。メモに文字が何も入力されていない (中身が空) とグレーアウトするようです。

```pascal:frmuMain.pas
// リスト更新

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  (中略)

  // [検索(F)] の有効/無効
  acFind.Enabled := Memo1.Lines.Text.Length > 0;
end;
```

〔Alt〕,〔E〕,〔F〕や〔Ctrl〕+〔F〕で [検索(F)] が実行される事を確認してください。

### [次を検索(N)]
このメニューは、

 - 一度も検索されていなければ検索ダイアログを出す。
 - 検索ダイアログの [次を検索(F)] と同じ挙動。
 - メモに何も入力されていなかったら[次を検索(N)] メニューをグレーアウト

という処理を行う必要があります。

acFindNext アクションのイベントハンドラのコードはこうなります。

```pascal:frmuMain.pas
procedure TForm1.acFindNextExecute(Sender: TObject);
// Action: 次を検索(N)
begin
  if FindDialog1.FindText = '' then
    acFind.Execute
  else
    FindDialog1Find(nil);
end;
```
FindDialog1 の検索文字列が空だったら acFind (検索) のイベントを実行し、そうでなかったら FindDialog1Find() イベントハンドラを直接呼びます。イベントハンドラもメソッドですから、引数さえ合えば呼び出す事ができます。OnFind イベントのイベントハンドラは引数に TObject 型の変数を渡す事になっていますが、イベントハンドラ内で Sender は使われていないので nill を渡します。

イベントハンドラを直接呼ぶのは行儀がいいとは言えません。本来ならイベントハンドラ内のロジックをメソッドとしておき、そのメソッドを FindDialog1Find() と acFindNextExecute() から呼び出すようにすべきです。

グレーアウトの処理は [検索(F)] と同じでいいようです。

```pascal:frmuMain.pas
// リスト更新

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  (中略)

  // [次を検索(N)] の有効/無効
  acFindNext.Enabled := Memo1.Lines.Text.Length > 0;
end;
```

〔Alt〕,〔E〕,〔N〕や〔F3〕で [次を検索(N)] が実行される事を確認してください。


### [置換(R)]

このメニューは、

 - 検索ダイアログを出す。
 - [次を検索(F)] ボタンが押されたら、検索条件を基に検索。
 - 検索条件に一致しなかったら 「"検索文字列" が見つかりません。」というメッセージを出す。
 - 一致したら文字列を選択状態にする。
 - [置換して次に(R)] ボタンが押されたら、検索文字列に一致した文字列を置換後の文字列で置換
 - [すべて置換(A)] ボタンが押されたら、検索文字列に一致した文字列を置換後の文字列で一括置換

という処理を行う必要があります...かなり面倒ですね。

置換ダイアログは [TReplaceDialog](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.TReplaceDialog) がツールパレットの [Dialogs] にありますので、これを追加しておきます (**ReplaceDialog1**)。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/0d624cf9-566c-08a2-598f-84122134689f.png)

プレビューしてみると **単語単位で探す(W)** がありますが、これは利用しないので、[Options プロパティ](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.TFindDialog.Options)の frHideWholeWord フラグを True にします。プロパティとフラグの関係はこのようになっています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/b9c3dc53-e7d4-1db5-358c-88ab362302d5.png)

[次を検索(F)] ボタンが押されたら OnFind イベントが発生し、[置換して次に(R)] または [すべて置換(A)] が押されたら OnReplace イベントが発生します。

...という事は、うまくいけば検索と置換で検索部分は共通化できそうです。

オブジェクトインスペクタで ReplaceDialog1 の [イベント] タブを開き、OnFind イベントをドロップダウンして、FindDialog1Find() イベントハンドラを割り当てます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/733b945a-d0af-756c-0f70-d9cd5e19fd6e.png)

TFindDialog と TReplaceDialog の ***OnFind イベントのは同じ TNotifyEvent 型***なので、イベントハンドラを共有する事ができます。

 - [Vcl.Dialogs.TFindDialog.OnFind](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.TFindDialog.OnFind)
 - [System.Classes.TNotifyEvent](http://docwiki.embarcadero.com/Libraries/ja/System.Classes.TNotifyEvent)

しかしながら、FindDialog1Find() という名前は紛らわしいので、ドロップダウンリストでイベントハンドラを選択した後に、名前を **Dialog_Find** に変更します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/a022a8f8-29ab-d819-d01a-7d421e91ea57.png)

コードエディタで確認すると、こちらでも名前が変わっていますね。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/7471f07c-6064-64c8-de1a-b5cb6d3ab4c1.png)

acFindNextExecute()　の中は流石に自動で変わらないので、手動で変更します。

```pascal:frmuMain.pas
procedure TForm1.acFindNextExecute(Sender: TObject);
// Action: 次を検索(N)
begin
  if FindDialog1.FindText = '' then
    acFind.Execute
  else
    Dialog_Find(nil); // <--- 変更
end;
```

さて、このままですと共有した事にはなりません。FindDialog1 オブジェクトを直接参照している箇所があります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/990f5c93-9a9f-f83a-8953-baccb4595c9d.png)

このイベントハンドラは誰が処理しているのか判断する方法があります。それがイベントハンドラの Sender です。Sender でこのイベントを発生させたオブジェクトを知ることができます。

```pascal:frmuMain.pas
procedure TForm1.Dialog_Find(Sender: TObject);
  ...
begin
  if (Sender is TReplaceDialog) then
    begin
      // イベントを発生させたのは TReplaceDialog 型の誰か
    end
  else
    begin
      // イベントを発生させたのは TFindDialog 型の誰か
    end;
  ...
```

または

```pascal:frmuMain.pas
procedure TForm1.Dialog_Find(Sender: TObject);
  ...
begin
  if (Sender = ReplaceDialog1) then
    begin
      // イベントを発生させたのは ReplaceDialog1
    end
  else
    begin
      // イベントを発生させたのは ReplaceDialog1 以外
    end;
  ...
```

前者の方法だと、オブジェクトを直接指定していないので、ロジックが汎用的になりますが、

```pascal:frmuMain.pas
procedure TForm1.Dialog_Find(Sender: TObject);
  ...
begin
  if (Sender is TFindDialog) then
    begin
      // イベントを発生させたのは TFindDialog (型の誰か)
    end
  else
    begin
      // イベントを発生させたのは TReplaceDialog (型の誰か)
    end;
  ...
```
この判定ではダメです。何故なら [TReplaceDialog](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.TReplaceDialog) クラスは [TFindDialog](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.TFindDialog) から派生しているため、「あなたは TFindDialog なの？」と尋ねてもどちらも「はい」と答えるからです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/a305059e-a3f6-4601-a1d8-964f5f950315.png)

共通項の部分では [TReplaceDialog](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.TReplaceDialog) は [TFindDialog](http://docwiki.embarcadero.com/Libraries/ja/Vcl.Dialogs.TFindDialog) として振舞うことができます。この検索ロジックでは共通項の部分しか使わないので、以下のようなコードにする事ができます。

```pascal:frmuMain.pas
procedure TForm1.Dialog_Find(Sender: TObject);
// 検索イベントハンドラ
var
  RegEx: TRegEx;
  Exp, Msg: String;
  Options: TRegExOptions;
  Match, dMatch: TMatch;
  StartPos, EndPos: Integer;
begin
  // 検索文字列を共有
  FindDialog1.FindText := (Sender as TFindDialog).FindText;
  ReplaceDialog1.FindText := (Sender as TFindDialog).FindText;

  // 検索文字列を正規表現文字列へエスケープ
  Exp := TRegEx.Escape((Sender as TFindDialog).FindText);

  // "大文字と小文字を区別する" にチェックが入っていなければ
  // オプションに roIgnoreCase (大文字小文字を区別しない) を追加
  if not (frMatchCase in (Sender as TFindDialog).Options) then
    Include(Options, roIgnoreCase);

  // 正規表現オブジェクトの生成
  RegEx := TRegEx.Create(Exp, Options);
  if (frDown in (Sender as TFindDialog).Options) then
    begin
      // 順方向検索 (下へ)
      StartPos  := Memo1.SelStart + Memo1.SelLength + 1;
      Match := RegEx.Match(Memo1.Lines.Text, StartPos);
    end
  else
    begin
      // 逆方向検索 (上へ)
      StartPos := 0;
      EndPos   := Memo1.SelStart + 1;
      Match := RegEx.Match(Memo1.Lines.Text, StartPos, EndPos);
      while Match.Success do
        begin
          // 最後にマッチした箇所を探す
          dMatch := Match.NextMatch;
          if dMatch.Success then
            Match := dMatch
          else
            break;
        end;
    end;

  if Match.Success then
    begin
      // 一致する文字列があった
      SetFocus;
      Memo1.SelStart := Match.Index - 1;
      Memo1.SelLength := Match.Length;
    end
  else
    begin
      // 一致する文字列がなかった
      Msg := Format('"%s" が見つかりません。', [(Sender as TFindDialog).FindText]);
      MessageDlg(Msg, TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], -1);
    end;
end;
```

FindDialog1 とオブジェクトを直接指定していた箇所を (Sender as TFindDialog) で置き換えてあります。ん？Sender を利用した...という事は？

```pascal:frmuMain.pas
procedure TForm1.acFindNextExecute(Sender: TObject);
// Action: 次を検索(N)
begin
  if FindDialog1.FindText = '' then
    acFind.Execute
  else
    Dialog_Find(FindDialog1); // <-- FindDialog1 を渡す
end;
```
こうしないと [次を検索(N)] で nil が渡ってきてしまいますね。後は acReplace アクションのイベントハンドラを書けば検索部分は OK です。

```pascal:frmuMain.pas
procedure TForm1.acReplaceExecute(Sender: TObject);
// Action: 置換(H)
begin
  // 置換ダイアログを開く
  ReplaceDialog1.Execute;
end;
```

とりあえずこの時点で〔F9〕を押して実行し、検索 / 次を検索 / 置換 (の検索) を試してみましょう。あー、オリジナルのメモ帳では検索と置換の検索文字列は共有しているのですね。では、こうなりますね。

```pascal:frmuMain.pas
procedure TForm1.Dialog_Find(Sender: TObject);
var
  Exp, Msg: String;
  Options: TRegExOptions;
  dMatch: TMatch;
  StartPos, EndPos: Integer;
begin
  // 検索文字列を共有
  FindDialog1.FindText := (Sender as TFindDialog).FindText;    // <--- 追加
  ReplaceDialog1.FindText := (Sender as TFindDialog).FindText; // <--- 追加
  ...
```

さて次は置換ロジックです。オブジェクトインスペクタで ReplaceDialog1 の OnReplace
 イベントの値部分をダブルクリックして ReplaceDialog1Replace() イベントハンドラを生成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/21785/765f32d3-fb9d-4f3e-bd6c-cbca0e8142f5.png)

[置換して次へ] と [すべて置換] を分けるロジックはこのようになると思います。

```pascal:frmuMain.pas
procedure TForm1.ReplaceDialog1Replace(Sender: TObject);
// 置換イベントハンドラ
begin
  if (frReplaceAll in ReplaceDialog1.Options) then
    begin
      // [すべて置換]

      // (処理)
    end
  else
    begin
      // [置換して次へ]

      // (処理)
    end;
end;
```
文字列の一括置換には StringReplace() 関数を使うのが簡単です。

 - [System.SysUtils.StringReplace](http://docwiki.embarcadero.com/Libraries/ja/System.SysUtils.StringReplace)

```pascal:frmuMain.pas
procedure TForm1.ReplaceDialog1Replace(Sender: TObject);
// 置換イベントハンドラ
var
  srFlgs: TReplaceFlags;
begin
  if (frReplaceAll in ReplaceDialog1.Options) then
    begin
      // [すべて置換]
      srFlgs := [rfReplaceAll]; // "すべて置換" のフラグ
      if  not (frMatchCase in ReplaceDialog1.Options) then
        Include(srFlgs, rfIgnoreCase); // "大文字小文字を無視する" フラグを追加
      Memo1.Lines.Text := StringReplace(Memo1.Lines.Text, ReplaceDialog1.FindText, ReplaceDialog1.ReplaceText, srFlgs);
    end
  else
    begin
      // [置換して次へ]

      // (処理)
    end;
end;
```
簡単でしたね。

なお、TReplaceFlags は[集合](http://docwiki.embarcadero.com/RADStudio/ja/%25E6%25A7%258B%25E9%2580%25A0%25E5%258C%2596%25E5%259E%258B%25EF%25BC%2588Delphi%25EF%25BC%2589%23.E9.9B.86.E5.90.88)でフラグビットの集まりです。集合にフラグビットを設定するにはこのようにします。

```
集合型 := [フラグ, フラグ...];
```

集合型にフラグを追加したい場合には、いずれかの方法でできます。

```
集合型 := 集合型 + [フラグ, フラグ...];

Include(集合型, フラグ);
```

集合型からフラグを削除したい場合には、いずれかの方法でできます。

```pascal: 
集合型 := 集合型 - [フラグ, フラグ...];

Exclude(集合型, フラグ);
```

ちなみに、こういった**置換関数は出来合いのものがあればそれを使う**のがベストです。検索に一致した文字列を置換して検索位置を保存して...とやってると、バグを生むことがあるからです。

例えば **A を BAKA に置換**する場合、置換文字列に検索文字列が含まれるため、無限に置換するバグを生みます。また、検索文字列が長くて置換文字列が短い場合、置換されない文字列が出てくるバグを生みます。いずれも文字列を処理した位置を適切に管理しないと発生するバグです。

 [置換して次へ] の処理ですが、これは

 1. 検索されていなければ検索
 2. 検索されていなければ置換して検索
 3. 検索条件に一致しなかったら 「"検索文字列" が見つかりません。」というメッセージを出す。

このようになっています。"検索されていなければ検索" の部分ですが、"検索されている" の判定は、**選択されているテキストが検索文字列と一致しているか？** というロジックのようです。ならば、この置換のロジックは、

 1. 選択されているテキストが検索文字列と一致しているか調べる
 2. 一致していれば選択文字列を置換文字列で置き換える
 3. 検索を行う

こうなります。TMemo.SelText が選択されているテキストの**プロパティ**であり、このプロパティは**代入可能**というのがミソです。これを知っていると置換のロジックはシンプルになります。

```pascal:frmuMain.pas
procedure TForm1.ReplaceDialog1Replace(Sender: TObject);
// 置換イベントハンドラ
var
  srFlgs: TReplaceFlags;
  FindText: string;
  SelectFlg: Boolean;
begin
  // 検索文字列を共有
  FindDialog1.FindText := (Sender as TFindDialog).FindText;
  ReplaceDialog1.FindText := (Sender as TFindDialog).FindText;

  // フラグ
  if (frReplaceAll in ReplaceDialog1.Options) then
    begin
      // [すべて置換]
      srFlgs := [rfReplaceAll]; // "すべて置換" のフラグ
      if  not (frMatchCase in ReplaceDialog1.Options) then
        Include(srFlgs, rfIgnoreCase); // "大文字小文字を無視する" フラグを追加
      Memo1.Lines.Text := StringReplace(Memo1.Lines.Text, ReplaceDialog1.FindText, ReplaceDialog1.ReplaceText, srFlgs);
    end
  else
    begin
      // [置換して次へ]
      // 検索済みかどうかの判定
      FindText := (Sender as TFindDialog).FindText;
      if (frMatchCase in ReplaceDialog1.Options) then
        SelectFlg := SameStr(Memo1.SelText, FindText)   // 大文字小文字を区別する
      else
        SelectFlg := SameText(Memo1.SelText, FindText); // 大文字小文字を区別しない
      // 検索済みか？
      if SelectFlg then
        Memo1.SelText := ReplaceDialog1.ReplaceText;
      // 検索
      Dialog_Find(ReplaceDialog1);
    end;
end;
```

[置換(R)...] メニューは何故かグレーアウトしないようなので、置換処理はこれで完了となります。〔Alt〕,〔E〕,〔R〕や〔Ctrl〕+〔H〕で [置換(R)...] が実行される事を確認してください。

# // 戻る

今回はここまでです。ここまでの全ソースを掲載しておきます。

```pascal:frmuMain.pas
unit frmuMain;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, System.Actions,
  Vcl.ActnList, Vcl.Menus, Vcl.ExtDlgs, System.IOUtils, Vcl.Clipbrd,
  System.RegularExpressions, System.UITypes;

type
  TForm1 = class(TForm)
    Memo1: TMemo;
    MainMenu1: TMainMenu;
    ActionList1: TActionList;
    acNew: TAction;
    acOpen: TAction;
    acSave: TAction;
    acSaveAs: TAction;
    acPageSetup: TAction;
    acPrint: TAction;
    acExit: TAction;
    F1: TMenuItem;
    N1: TMenuItem;
    O1: TMenuItem;
    S1: TMenuItem;
    A1: TMenuItem;
    U1: TMenuItem;
    P1: TMenuItem;
    X1: TMenuItem;
    N2: TMenuItem;
    N3: TMenuItem;
    OpenTextFileDialog1: TOpenTextFileDialog;
    SaveTextFileDialog1: TSaveTextFileDialog;
    acUndo: TAction;
    acCut: TAction;
    acCopy: TAction;
    acPaste: TAction;
    acDelete: TAction;
    acFind: TAction;
    acFindNext: TAction;
    acReplace: TAction;
    acGoTo: TAction;
    acSelectAll: TAction;
    acTimeDate: TAction;
    E1: TMenuItem;
    U2: TMenuItem;
    N4: TMenuItem;
    C1: TMenuItem;
    T1: TMenuItem;
    P2: TMenuItem;
    L1: TMenuItem;
    N5: TMenuItem;
    F2: TMenuItem;
    N6: TMenuItem;
    R1: TMenuItem;
    G1: TMenuItem;
    N7: TMenuItem;
    A2: TMenuItem;
    D1: TMenuItem;
    FindDialog1: TFindDialog;
    ReplaceDialog1: TReplaceDialog;
    procedure FormCreate(Sender: TObject);
    procedure acExitExecute(Sender: TObject);
    procedure acOpenExecute(Sender: TObject);
    procedure acSaveExecute(Sender: TObject);
    procedure acSaveAsExecute(Sender: TObject);
    procedure acNewExecute(Sender: TObject);
    procedure acUndoExecute(Sender: TObject);
    procedure ActionList1Update(Action: TBasicAction; var Handled: Boolean);
    procedure acCutExecute(Sender: TObject);
    procedure acCopyExecute(Sender: TObject);
    procedure acPasteExecute(Sender: TObject);
    procedure E1Click(Sender: TObject);
    procedure acFindExecute(Sender: TObject);
    procedure Dialog_Find(Sender: TObject);
    procedure acFindNextExecute(Sender: TObject);
    procedure acDeleteExecute(Sender: TObject);
    procedure acReplaceExecute(Sender: TObject);
    procedure ReplaceDialog1Replace(Sender: TObject);
  private
    { Private 宣言 }
    FFileName: String;
    FEncodingIndex: Integer;
    procedure Init;
    procedure SaveFile;
    procedure UpdateCaption;
  public
    { Public 宣言 }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
// フォーム作成時
begin
  Init;
end;

// [FILE] Actions

procedure TForm1.acNewExecute(Sender: TObject);
// Action: 新規(N)
begin
  Init;
end;

procedure TForm1.acOpenExecute(Sender: TObject);
// Action: 開く(O)...
var
  Enc: TEncoding;
begin
  if OpenTextFileDialog1.Execute then
    begin
      // 値を保存
      FFileName := OpenTextFileDialog1.FileName;
      FEncodingIndex := OpenTextFileDialog1.EncodingIndex;
      // キャプションを変更
      UpdateCaption;
      // EncodingIndex によりエンコーディングを指定
      case FEncodingIndex of
        1: Enc := TEncoding.Unicode;
        2: Enc := TEncoding.BigEndianUnicode;
        3: Enc := TEncoding.UTF8;
      else
        Enc := TEncoding.Default;
      end;
      // エンコーディングを指定して読み込み
      Memo1.Lines.LoadFromFile(FFileName, Enc);
    end;
end;

procedure TForm1.acSaveExecute(Sender: TObject);
// Action: 上書き保存(S)
begin
  if (FFileName = '') then
    begin
      // 名前を付けて保存
      acSaveAs.Execute;
    end
  else
    begin
      // 上書き保存
      SaveFile;
    end;
end;

procedure TForm1.acSaveAsExecute(Sender: TObject);
// Action: 名前を付けて保存(A)...
begin
  if SaveTextFileDialog1.Execute then
    begin
      // 値を保存
      FFileName := SaveTextFileDialog1.FileName;
      FEncodingIndex := SaveTextFileDialog1.EncodingIndex;
      // キャプションを変更
      UpdateCaption;
      // ファイルへ保存
      SaveFile;
    end;
end;

procedure TForm1.acExitExecute(Sender: TObject);
// Action: メモ帳の終了(X)
begin
  Self.Close;
end;

// [EDIT] Actions

procedure TForm1.E1Click(Sender: TObject);
begin
  acDelete.ShortCut := TextToShortCut('Del');
end;

procedure TForm1.acUndoExecute(Sender: TObject);
// Action: 元に戻す(U)
begin
  Memo1.Undo;
end;

procedure TForm1.acCutExecute(Sender: TObject);
// Action: 切り取り(T)
begin
  Memo1.CutToClipboard;
end;

procedure TForm1.acCopyExecute(Sender: TObject);
// Action: コピー(C)
begin
  Memo1.CopyToClipboard;
end;

procedure TForm1.acPasteExecute(Sender: TObject);
// Action: 貼り付け(P)
begin
  Memo1.PasteFromClipboard;
end;

procedure TForm1.acDeleteExecute(Sender: TObject);
// Action: 削除(L)
begin
  Memo1.ClearSelection;
end;

procedure TForm1.acFindExecute(Sender: TObject);
// Action: 検索(F)
begin
  // 検索ダイアログを開く
  FindDialog1.Execute;
end;

procedure TForm1.acFindNextExecute(Sender: TObject);
// Action: 次を検索(N)
begin
  if FindDialog1.FindText = '' then
    acFind.Execute
  else
    Dialog_Find(FindDialog1);
end;

procedure TForm1.acReplaceExecute(Sender: TObject);
// Action: 置換(H)
begin
  // 置換ダイアログを開く
  ReplaceDialog1.Execute;
end;

procedure TForm1.Dialog_Find(Sender: TObject);
// 検索イベントハンドラ
var
  RegEx: TRegEx;
  Exp, Msg: String;
  Options: TRegExOptions;
  Match, dMatch: TMatch;
  StartPos, EndPos: Integer;
begin
  // 検索文字列を共有
  FindDialog1.FindText := (Sender as TFindDialog).FindText;
  ReplaceDialog1.FindText := (Sender as TFindDialog).FindText;

  // 検索文字列を正規表現文字列へエスケープ
  Exp := TRegEx.Escape((Sender as TFindDialog).FindText);

  // "大文字と小文字を区別する" にチェックが入っていなければ
  // オプションに roIgnoreCase (大文字小文字を区別しない) を追加
  if not (frMatchCase in (Sender as TFindDialog).Options) then
    Include(Options, roIgnoreCase);

  // 正規表現オブジェクトの生成
  RegEx := TRegEx.Create(Exp, Options);
  if (frDown in (Sender as TFindDialog).Options) then
    begin
      // 順方向検索 (下へ)
      StartPos  := Memo1.SelStart + Memo1.SelLength + 1;
      Match := RegEx.Match(Memo1.Lines.Text, StartPos);
    end
  else
    begin
      // 逆方向検索 (上へ)
      StartPos := 0;
      EndPos   := Memo1.SelStart + 1;
      Match := RegEx.Match(Memo1.Lines.Text, StartPos, EndPos);
      while Match.Success do
        begin
          // 最後にマッチした箇所を探す
          dMatch := Match.NextMatch;
          if dMatch.Success then
            Match := dMatch
          else
            break;
        end;
    end;

  if Match.Success then
    begin
      // 一致する文字列があった
      SetFocus;
      Memo1.SelStart := Match.Index - 1;
      Memo1.SelLength := Match.Length;
    end
  else
    begin
      // 一致する文字列がなかった
      Msg := Format('"%s" が見つかりません。', [(Sender as TFindDialog).FindText]);
      MessageDlg(Msg, TMsgDlgType.mtInformation, [TMsgDlgBtn.mbOK], -1);
    end;
end;

procedure TForm1.ReplaceDialog1Replace(Sender: TObject);
// 置換イベントハンドラ
var
  srFlgs: TReplaceFlags;
  FindText: string;
  SelectFlg: Boolean;
begin
  // 検索文字列を共有
  FindDialog1.FindText := (Sender as TFindDialog).FindText;
  ReplaceDialog1.FindText := (Sender as TFindDialog).FindText;

  // フラグ
  if (frReplaceAll in ReplaceDialog1.Options) then
    begin
      // [すべて置換]
      srFlgs := [rfReplaceAll]; // すべて置換
      if  not (frMatchCase in ReplaceDialog1.Options) then
        Include(srFlgs, rfIgnoreCase);
      Memo1.Lines.Text := StringReplace(Memo1.Lines.Text, ReplaceDialog1.FindText, ReplaceDialog1.ReplaceText, srFlgs);
    end
  else
    begin
      // [置換して次へ]
      srFlgs := []; // 最初の一回だけ置換
      if  not (frMatchCase in ReplaceDialog1.Options) then
        Include(srFlgs, rfIgnoreCase);
      // 検索済みかどうかの判定
      FindText := (Sender as TFindDialog).FindText;
      if (frMatchCase in ReplaceDialog1.Options) then
        SelectFlg := SameStr(Memo1.SelText, FindText)   // 大文字小文字を区別する
      else
        SelectFlg := SameText(Memo1.SelText, FindText); // 大文字小文字を区別しない
      // 検索済みか？
      if SelectFlg then
        Memo1.SelText := ReplaceDialog1.ReplaceText;
      // 検索
      Dialog_Find(ReplaceDialog1);
    end;
end;

// リスト更新

procedure TForm1.ActionList1Update(Action: TBasicAction; var Handled: Boolean);
begin
  // [元に戻す(U)] の有効/無効
  acUndo.Enabled := Memo1.CanUndo;
  // [切り取り(T)] の有効/無効
  acCut.Enabled := Memo1.SelLength <> 0;
  // [コピー(C)] の有効/無効
  acCopy.Enabled := Memo1.SelLength <> 0;
  // [貼り付け(P)] の有効/無効
  acPaste.Enabled := ClipBoard.HasFormat(CF_TEXT);
  // [削除(L)] の有効/無効
  acDelete.Enabled := Memo1.SelLength <> 0;
  acDelete.ShortCut := 0;
  // [検索(F)] の有効/無効
  acFind.Enabled := Memo1.Lines.Text.Length > 0;
  // [次を検索(N)] の有効/無効
  acFindNext.Enabled := Memo1.Lines.Text.Length > 0;
end;

// メソッド

procedure TForm1.Init;
// エディタの初期化
begin
 // 値を初期化
  FFileName := '';
  FEncodingIndex := 0;
  Memo1.Lines.Clear; // Memo1 の中身を消去する。
  // キャプションを変更
  UpdateCaption;
end;

procedure TForm1.SaveFile;
var
  Enc: TEncoding;
begin
  // EncodingIndex によりエンコーディングを指定
  case FEncodingIndex of
    1: Enc := TEncoding.Unicode;
    2: Enc := TEncoding.BigEndianUnicode;
    3: Enc := TEncoding.UTF8;
  else
    Enc := TEncoding.Default;
  end;
  // エンコーディングを指定して保存
  Memo1.Lines.SaveToFile(FFileName, Enc);
end;

procedure TForm1.UpdateCaption;
// キャプション (ファイル名) の更新
var
  Dmy: String;
begin
  if FFileName = '' then
    Dmy := '無題'
  else
    Dmy := TPath.GetFileName(FFileName);
  Self.Caption := Dmy + ' - メモ帳クローン';
end;

end.
```

[前の記事へ] [[親記事へ]](https://qiita.com/ht_deko/items/525cd67aaba16681c3ba) [[次の記事へ]](https://qiita.com/ht_deko/private/d40247d93533c42f4ef0)