Delphi の IDE にメモを設置する (Tools API)

# はじめに
最近の Delphi はアンドックな IDE スタイルが利用できません。できなければできないで慣れはするのですが、一時的なメモを置いておく場所が欲しい事があります

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/5b8aedf6-4b69-3a4b-67a7-b1e6a6756fd4.png)

 - テキストファイルをコードエディタで開く事はできるのですが、タブを切り替える必要があります。
 - フォームデザイナとコードエディタは排他なので、オブジェクトインスペクタの内容をコピペしたい時はかなりガチャガチャします。
 - 外部のテキストエディタを使うとタスク切り替えになります。
 - [新規編集ウィンドウ] は IDE の裏に行かないので使い勝手が悪いです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/1f295bb6-e5c5-76fb-62cf-2e272c51a12d.png)

IDE から離れず、ウィンドウをガチャガチャしなくていいようにするためには IDE のどこかにメモがあれば便利なのですが...。

# ドッキング可能なメモフォーム

それなら **IDE にドック可能なメモフォームを追加すればいいんじゃね？** という結論に至り、重い腰を上げて IDE 拡張を作る事にしました。こういった拡張は既に誰かが作っている気がしますが、気にしない事にします。

## Tools API

Delphi の IDE は **Tools API** を使って拡張します。

**・Native Tools API (NTAPI)**
パッケージ内から使える Tools API です。IDE の機能を利用する事ができます。

**・Open Tools API (OTAPI)**
パッケージを作らなくても使える Tools API です。IDE の一部の機能を利用する事ができます。

IDE 拡張の具体的な方法については　DocWiki にドキュメントがあります。

 - [Tools API を使用した IDE の拡張 (DocWiki)](https://docwiki.embarcadero.com/RADStudio/ja/Tools_API_%E3%82%92%E4%BD%BF%E7%94%A8%E3%81%97%E3%81%9F_IDE_%E3%81%AE%E6%8B%A1%E5%BC%B5)

Delphi 8 辺りでドキュメントから Tools API の記述が消えていて、IDE 拡張を作成するハードルが高かったのですが、現在ではドキュメントが復活しており、IDE 拡張のハードルも幾分低くなっています。

## ドッキング可能なフォームのサンプル
ドッキング可能なフォームのサンプルが gexperts.org にありますので、これをベースにしたいと思います。

 - [How can I create a form that docks into the IDE like the Object Inspector? (gexperts.org)](https://www.gexperts.org/open-tools-api-faq/#dockform)
 - [DockingForm.zip (gexperts.org)](https://www.gexperts.org/examples/DockingForm.zip)

インストールすると、`[ヘルプ]` メニューに `[Show Dockable Form]` というメニューが追加され、これをクリックするとドッキング可能な空のフォームが表示されます。

これにメモコンポーネントを貼り付けるだけで目的は達成できそうですね。

## 改変
では、Delphi 2007 以降で使える事を念頭に改変していきたいと思います。

### ・リネーム
全体的にリネームして **DockMemo** という名称にします。

### ・メモ
ドッキングフォームにメモを最大化で貼ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/1028e019-ad5a-d3bc-a824-cb964212fe93.png)

| プロパティ | 値 |
|:---|:---|
| Align | alClient |
| Font.Name | Tahoma |
| Name | mmMemo  |
| ScrollBars | ssBoth |
| WantTabs | True |

### ・メニュー
[IOTAMenuWizard](https://docwiki.embarcadero.com/RADStudio/ja/%E3%82%A6%E3%82%A3%E3%82%B6%E3%83%BC%E3%83%89_%E3%82%AF%E3%83%A9%E3%82%B9%E3%81%AE%E4%BD%9C%E6%88%90) として登録されている (`[ヘルプ]` メニューに表示される) ので、これをやめます。

```pascal:DockFormExpert.pas
procedure Register;
begin
//RegisterPackageWizard(TDockMemoFormExpert.Create as IOTAMenuWizard);
  RegisterPackageWizard(TDockMemoFormExpert.Create);
  TDockableMemoForm.CreateDockMemoForm;
end;
```

クラスから IOTAMenuWizard インターフェイスも外して、GetMenuText() メソッドも削除しておきます。

これにより、メニューへは自前で登録しなければならなくなりました。メニューの位置は `[表示]` の `[デスクトップの配置]` とセパレーターの間がいいと思いました。 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/624cc346-ec1d-70f5-e895-0b8d7fc19743.png)

最近の Delphi はメニューがまとめられているので `[ツールウィンドウ]` の中がよさそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/10409497-be12-37f4-1c4c-f4ff36efa67c.png)

メニューアイテムを追加するコードは次のようになりました。

```pascal:DockFormExpert.pas
procedure RegisterMenu;
var
  NTA: INTAServices;
  i, j: Integer;
  Method: TMethod;
  NewMenu: Boolean;
begin
  NTA := BorlandIDEServices as INTAServices;
  for i := 0 to NTA.MainMenu.Items.Count-1 do
    begin
      if NTA.MainMenu.Items[i].Name <> 'ViewsMenu' then
        Continue;
      for j := 0 to NTA.MainMenu.Items[i].Count-1 do
        begin
          if NTA.MainMenu.Items[i][j].Name = 'ViewToolWindowsItem' then
            NewMenu := True
          else if NTA.MainMenu.Items[i][j].Name = 'ViewToggleSeparator' then
            NewMenu := False
          else
            Continue;
          MemoMenu := TMenuItem.Create(nil);
          MemoMenu.AutoHotkeys := maManual;
          {$IF CompilerVersion >= 21.0}
          if not SameText(PreferredUILanguages, 'JA') then
            MemoMenu.Caption := Title_EN
          else
          {$IFEND}
            MemoMenu.Caption := Title_JA;
          Method.Code := @MenuClick;
          Method.Data := MemoMenu;
          MemoMenu.OnClick := TNotifyEvent(Method);
          if NewMenu then
            NTA.MainMenu.Items[i][j].Add(MemoMenu)
          else
            NTA.MainMenu.Items[i].Insert(j, MemoMenu);
          Break;
        end;
      Break;
    end;
end; { RegisterMenu }
```

余談ですが、挿入するメニューアイテムの位置を決定するには、既存のメニューアイテムの名前を知る必要があります。メニューアイテムの名前を調べるには適当なパッケージを作成して次のようなユニットを含めます。

```pascal:EnumMenusExpert.pas
unit EnumMenusExpert;

interface

uses
  Sysutils, Classes, ToolsAPI, Menus, Dialogs;

type
  TEnumMenusExpert = class(TNotifierObject, IOTANotifier, IOTAWizard, IOTAMenuWizard)
  public
    procedure Execute;
    function GetIDString: string;
    function GetMenuText: string;
    function GetName: string;
    function GetState: TWizardState;
    procedure Destroyed;
  end;

  procedure Register;

implementation

procedure Register;
begin
  RegisterPackageWizard(TEnumMenusExpert.Create as IOTAMenuWizard);
end;

{ TEnumMenusExpert }

procedure TEnumMenusExpert.Destroyed;
begin
  { Do Nothing }
end;

procedure TEnumMenusExpert.Execute;
var
  NTA: INTAServices;
  i, j: Integer;
  SL: TStringList;
begin
  SL := TStringList.Create;
  try
    NTA := BorlandIDEServices as INTAServices;
    for i := 0 to NTA.MainMenu.Items.Count-1 do
      begin
        SL.Add(Format('[%s]: %s', [NTA.MainMenu.Items[i].Caption, NTA.MainMenu.Items[i].Name]));
        for j := 0 to NTA.MainMenu.Items[i].Count-1 do
          SL.Add(Format('  %s: %s', [NTA.MainMenu.Items[i][j].Caption, NTA.MainMenu.Items[i][j].Name]));
        SL.Add('');
      end;
    ShowMessage(SL.Text);
  finally
    SL.Free;
  end;
end;

function TEnumMenusExpert.GetIDString: string;
begin
  Result := 'EnumMenusExpert';
end;

function TEnumMenusExpert.GetMenuText: string;
begin
  Result := 'Enum Menus...';
end;

function TEnumMenusExpert.GetName: string;
begin
  Result := 'Enum Menus Expert';
end;

function TEnumMenusExpert.GetState: TWizardState;
begin
  Result := [wsEnabled];
end;

initialization

finalization

end.
```

**Requies** に `designide` を追加してインストールすれば、`[ヘルプ | Enum Menus...]` でメニューアイテムのキャプションと名前を調べる事ができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/db42550a-2930-051b-0b08-7ef1ec82ee15.png)

〔Ctrl〕+〔C〕でクリップボードにテキストが入ります


### ・ショートカット
IDE のショートカットが優先されるため、メモのクリップボード関連のショートカットが使えません。これではあまりに不便なのでメモコンポーネントに入った時に IDE のショートカットを無効にし、メモコンポーネントから出た時に有効に戻すようにします。

```pascal:DockMemoForm.pas
procedure EnableShortcuts(AValue: Boolean);
  procedure Apply(const Name, ShortCut: string);
  var
    action: TAction;
  begin
    action := TAction(Application.MainForm.FindComponent(Name));
    if action = nil then
      Exit;
    if AValue then
      action.ShortCut := TextToShortCut(ShortCut)
    else
      action.ShortCut := scNone;
  end;
begin
  Apply('EditUndoCommand'     , 'Ctrl+Z'      );
  Apply('EditRedoCommand'     , 'Shift+Ctrl+Z');
  Apply('EditCutCommand'      , 'Ctrl+X'      );
  Apply('EditCopyCommand'     , 'Ctrl+C'      );
  Apply('EditPasteCommand'    , 'Ctrl+V'      );
  Apply('EditSelectAllCommand', 'Ctrl+A'      );
end;
```

### ・自動保存 / 自動読込
メモはプロジェクト毎に関連付け、プロジェクトファイルのある場所に `*.memo` という名前で自動保存する事にします。

ノーティファイアを登録し、アクティブなプロジェクトが変更された時に保存/読込を行います。プロジェクトが閉じられた時にも保存を行います。

```pascal:DockMemoForm.pas
procedure TIDENotifier.FileNotification(NotifyCode: TOTAFileNotification;
  const FileName: string; var Cancel: Boolean);
begin
  if not Assigned(FormInstance) then
    Exit;
  case NotifyCode of
    ofnFileClosing:
      begin
        if GetActiveProject = nil then
          begin
            SaveFile;
            ChangeFile;
            SetCaption;
            TDockableMemoForm(FormInstance).mmMemo.Lines.Clear;
          end;
      end;
    ofnActiveProjectChanged:
      begin
        SaveFile;
        ChangeFile;
        SetCaption;
        LoadFile;
      end;
  end;
end;
```

Unicode 版 Delphi では UTF-8 で保存し、ANSI 版ではデフォルトのコードページ (日本語版 Windows なら Shift_JIS) で保存します。

```pascal:DockMemoForm.pas
procedure SaveFile;
begin
  with TDockableMemoForm(FormInstance) do
    if FileExists(ProjectFileName) then
      mmMemo.Lines.SaveToFile(MemoFileName{$IFDEF UNICODE}, TEncoding.UTF8{$ENDIF});
end; { SaveFile }
```

### ・リネーム対応
本来はプロジェクトがリネームされた時に、それに追従してメモファイルもリネームすべきですが、ノーティファイアがうまく働かなかった (ちゃんと動かせなかった) ので、リネーム対応にはなっていません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/b1725423-6be4-c021-9a6b-e5529af14ee5.png)

ここでリネームする事はあまりないので、そこまで不便ではないとは思います (今後の課題としておきます)。

### ・デバッグ
IDE 拡張はデバッグしにくいです。とりあえず、次のような手続きを作っておくと Print デバッグが可能となります。

```pascal
procedure WriteLog(TabName, s: string);
var
  IMessageServices: IOTAMessageServices;
  MessageGroup: IOTAMessageGroup;
begin
  IMessageServices := BorlandIDEServices as IOTAMessageServices;
  MessageGroup := IMessageServices.AddMessageGroup(TabName);
  IMessageServices.AddTitleMessage(s, MessageGroup);
end;
```

例えば

```pascal
WriteLog('DockMemo', 'FileNotification: 7');
```

のように記述すると、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/af73df96-2367-a847-9e84-bbe6fd50881f.png)

メッセージウィンドウに `DockMemo` タブが作られ、そこにログが表示されます。

### ・パッケージ
とりあえず 2007 / XE / 11 Alexandria 用のパッケージを作りました。

| Delphi | パッケージプロジェクト名 |
|:---|:---|
| Delphi 2007 | DockMemoPackageD2007.dproj  |
| Delphi XE | DockMemoPackageDXE.dproj |
| Delphi 11 Alexandria  | DockMemoPackageD11.dproj  |

Delphi 10 Seattle 以降、IDE は **Large Address Aware** に対応しているので、IDE 拡張も LAA 対応にする必要があったハズです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/9ceb513c-082f-019f-85bf-b4c3cd96dbcf.png)

Delphi 11 Alexandria 用のパッケージはプロジェクトオプションで LAA を有効にしておきました。

 - [【Delphi】Windows 32bit アプリケーションに 2GB を超える仮想メモリ空間を割り当てる (Qiita)](https://qiita.com/ht_deko/items/202ca4b4ab32986472bf)

## 完成 
出来上がった IDE 拡張のアーカイブを用意しました。

 - [DockMemo (ht-deko.com)](https://ht-deko.com/delphiforum/?vasthtmlaction=viewtopic&t=2343)

パッケージをインストールするとメモが使えるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/9e0708bd-8b6d-39b6-7c46-b0cdde7dd852.png)

お好きな所にドッキングさせて使って下さい。`デスクトップの保存` をするとドック状態が維持されます。

### アンインストール
アンインストールは `[コンポーネント | パッケージのインストール...]` から `IDE Docking Memo Form` を探して削除してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/21785/7bdf65c7-5994-0360-eb6a-03b9c4c897e6.png)


# おわりに
不満な所があったらご自由に改変してお使いください。

:::note
バグ修正コードやリネーム対応コードをお待ちしております (w
:::

**See also:**

 - [Extending the Delphi IDE (Embarcadero)](https://www.embarcadero.com/images/dm/technical-papers/extending-the-delphi-ide.pdf)
 - [第19回 エンバカデロ・デベロッパーキャンプ 「Delphi IDEでTwitter生活！Open Tools APIで開発環境を簡単カスタマイズ」(Embarcadero)](https://dtffvb2501i0o.cloudfront.net/images/jp/event/devcamp/session_docs/19/t3.pdf)

 - [Open Tools API (C++Builder好きの秘密基地)](http://d.hatena.ne.jp/A7M/)
   - [その0：概要](http://d.hatena.ne.jp/A7M/20101106/1289033279)
   - [その1："Hello,World!"を表示してみる](http://d.hatena.ne.jp/A7M/20101114/1289711588)
   - [その2：メニューの差し替えとノーティファイアインターフェース](http://d.hatena.ne.jp/A7M/20101231/1293763501)
   - [その3：ソースエディタへのアクセス](http://d.hatena.ne.jp/A7M/20110205/1296893189)
   - [その4：続・ソースエディタへのアクセス ソースエディタの「中身」をいじる](http://d.hatena.ne.jp/A7M/20110212/1297497495)

 - [Erik’s Open Tools API FAQ (GExparts)](https://www.gexperts.org/open-tools-api-faq/)
 - [Delphi Open Tools API (tempest-sw.com)](http://www.tempest-sw.com/opentools/)
 - [スワンマン氏による各種 IDE プラグイン (GitHub)](https://github.com/superswanman)
 - [970_Tools API を使用して IDE のメニューに項目を追加 (Mr.XRAY)](http://mrxray.on.coocan.jp/Delphi/plSamples/970_ToolsAPI_DlgTest.htm)
 - [Embarcadero / OTAPI-Docs (GitHub)](https://github.com/Embarcadero/OTAPI-Docs)
 - [Inside Delphi (Amazon)](https://www.amazon.co.jp/dp/4756119514/)
 - [Hidden Paths of Delphi 3: Experts, Wizards and the Open Tools API (Amazon)](https://www.amazon.co.jp/dp/0965736601/)