RETROF-16統合開発環境の製作（１）DockingWindowの実現とRichTextBoxの継承　

##はじめに
####本記事の内容
本記事は以下の外観と機能を持つアプリケーションの作り方を解説します。一部の自動生成されるソースコードを除く**全ソースコードを公開**します。
なお、このアプリ自体はキーボードから何を入力しても「Unknown command」を返すだけであり、具体的な事は何もできません。しかしこのアプリのソースコードに順次手を加えて行く事により、様々なアプリケーションに変身する事が可能です。すなわち、ここで紹介するアプリは**同様の画面構成をを持つ全てのアプリの土台**として利用することを想定しています。


![WS2019_000697.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/6051e3e5-bc71-a7ec-c9ab-ccc7c7e18aea.jpeg)
各領域は自由に配置できます。
![WS2019_000704.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/5764a4c3-a2a8-4787-d4e7-a25e816fb330.jpeg)



プログラミング上の主なテーマは以下の通り。

- 複数の**ドッキングウィンドウ**を持つアプリの作成方法
- 既存の**コントロールの継承**方法と、その具体的な利用方法
- 継承コントロールからの**独自イベントの発生**方法と、その受取方法

尚、ここで紹介するアプリは筆者が別途作成した「RETROF-16統合開発環境」の土台となっています。このため本記事のタイトルにRETROF-16統合開発環境という文言が含まれていますが、本記事内では「RETROF-16統合開発環境」に関する説明は一切ありません。本記事はあくまでも本記事内で完結する「Unknown command」を返すだけのアプリの作り方を紹介するものです。

####開発環境等
- OS Windows10　バージョン 1909 ビルド 18363.1198
- Microsoft Visual Studio Community 2019 Version 16.8.2
- Microsoft .NET FrameWork Version 4.8.03752


##下準備
####本アプリの開発用のフォルダ作成
C:直下にR16BLACKの名でフォルダを作ります。C:直下に作るのは好きではありませんが、こちらの方が何かと説明が楽なのでこうしました。実際にはどこでも構いません。
R16BLACKは、ここではその意味の詳細説明は割愛します。「筆者にとって特別な意味あるワード」とお考え下さい。これも実際には任意の名で構いません。

####必要なDLL
R16BLACKフォルダに「WeifenLuo.WinFormsUI.Docking.dll」を置きます。
このDLLの入手方法やその役割は「WeifenLuo.WinFormsUI.Docking.dll download」等で検索するとダウンロード可能なサイトがいくつか見つかると思います。
https://www.dllme.com/dll/
上記はその一例ですが、サイトによっては**悪意のある改変dll**を提供するサイトも存在します。ダウンロードに関する危険性は全て**自己責任**で行って下さい。

####プロジェクトの作成
次に、普通にプロジェクトを作成します。プロジェクト名は任意ですが、ここでは「RETROF」としたものとして話を薦めます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/c5d62817-d8cd-e499-e83f-d989d87f7795.jpeg" width=75%>

ここでビルド（及び実行）を行うと、いつもの以下の画面が出ます。通常のツール開発では、ここにフォーム等を配置して行きますが、ここではその前に一工夫します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/59bbb7a0-deb9-0111-44d7-3056da7114a3.jpeg" width=75%>


##ドックパネルの採用

様々な機能を有するウィンドウズアプリを開発する場合、ウィンドウ（フォーム）をいくつかの区画に分割し、その区画単位に各々の機能の入出力領域を割り振るのが一般的な作り方です。
勿論、アプリによってはこの作り方で十分な場合をありますが、ここでは更に一歩進んだ「ドックパネル」を全面的に採用します。

####ドックパネルとは

ドックパネルとはパネルの一種で、そのパネル（以下、親パネル）に含まれる子パネル（以下、**ドッキングウィンドウ**）の配置をユーザー自身が自由に決めることができる機能です。子パネルを親パネルの外に出すと子パネルは独立したフォームの様に振舞います。

VS（Visual Studio）自身がこの機能を使ったアプリケーションですので、VSをお使いの方は必ずこの機能を使っているはずです。しかし不思議な事に、このドックパネルを用いたアプリ開発はVS自身が持つ標準機能では実現できません。このため、前述の「WeifenLuo.WinFormsUI.Docking.dll」の取り込みが必要になります。

####（補足）他のDLLに関して

ドックパネルを実現するDLLは複数あります。本ツールでは「WeifenLuo.WinFormsUI.Docking.dll」を利用しましたが他のDLLとの性能比較はしてません。従ってこれが最適なDLLなのかは分かりません。単に最もポピュラーなDLLに思えたので、これを採用しました。

##ドックパネルを使える様にする手順

####既に「Docking.dll」が取り込まれているVSの場合

「ツール」⇒「ツールボックスアイテムの選択」を開き、右クリックで「アイテムの選択」を選択。
下記のダイアログボックスが現れるので、「WeifenLuo.WinFormsUI.Docking.dll」を選択し「OK」。
（初めてドックパネルを使う場合は、「WeifenLuo.WinFormsUI.Docking.dll」は現れませんので、キャンセルをして次項の “初めて「Docking.dll」を使うVSの場合” に進んで下さい）

![WS2019_000674.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/1a766563-6896-e0d1-f5be-904a7bb53fa7.jpeg)
「OK」ボタンを押しダイアログを閉じると、ツールボックスに「DockPanel」が出現します。

####初めて「Docking.dll」を使うVSの場合

「ツールボックス」を開き、右クリックで「アイテムの選択」を選択。
上記のダイアログボックスが現れます。但し「WeifenLuo.WinFormsUI.Docking.dll」はそこにはありません。
参照ボタンを押し、「C:\R16BLACK\Docking.dll」を指定すると、ダイアログに「Dock Panel」が現れますので、それを指定して「OK」。

####(参考)ドックパネルが使えるツールボックスと使えないツールボックス
右がドックパネルが使えるようになったツールボックスです。ドックパネルが使えるようになった後でアルファベット順にソートしていない場合は、追加された「Dock Panel」はリストの末尾に現れますので注意して下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/dcfc065e-56e8-c11f-e429-4fa374641335.jpeg" width=40%>　　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/2a86f741-81d4-50eb-b384-6ece2f249f64.jpeg" width=40%>

##親パネルを含むフォームの生成

ツールボックスから必要なアイテムを選びフォームに貼り付けます。以下の表は貼り付けるアイテムの一覧です。メニューバーやツールバーも中身は空のままで構わないのでここで貼り付けておきます。
各コントロールの名前は任意で構いませんが、VSが暗黙で決める名前（種別＋数字）は区別しずらいのでお勧めできません。

|名前|種別|生成位置|補足説明|
|---|---|---|---|
|MainForm|Form|（全体）|最初から存在するので貼り付け不要
|MenuBar|MenuStrip|Formの最上部|Windowsアプリの標準的な要素
|ToolBar|ToolStrip|MenuStripの下|Windowsアプリの標準的な要素
|StatusBar|StatusStrip|Formの最下部|Windowsアプリの標準的な要素
|MainDock|DockPanel|Formの残り領域全体|配置には外部DLLが必要

####ここまでの実行結果
この状態でビルド（及び実行）を行うと、以下に示す形状のアプリが起動します。
見かけ上の形状は、C#によるウィンドウズアプリ構築の入門書等ででよく見かけますが、中央の灰色の広い領域が「ドッキングパネル」という特殊な領域になっている点が一般の入門書と異なります。
![WS2019_000673.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/2e281e7c-0ae5-04f0-4829-a6d36c71a93a.jpeg)


##ドッキングウィンドウ（子パネル）の配置

今回用意するドッキングウィンドウ（子パネル）は下記の表の通りです。現時点では全く必要がなく、その存在理由も曖昧なものも含まれますが、ここでまとめて作ります。
生成位置と占有サイズは仮の値です。後で使い勝手を見ながら微調整します。
ここでは詳細説明は割愛しますが、ドッキングウィンドウを追加したり削除する事も後で簡単に行う事ができます。

|名前|生成位置と<br>占有サイズ|背景色|生成目的（ここでは目的は<br>まだ意味を持ちません）|
|---|---|:-:|---|
|MapWin|Maindockの<br>右側40%|薄桃|メモリマップを表示予定
|SrcWin|Maindockの<br>上部30%|薄黄|ソースプログラムのエディタを<br>構築予定
|CslWin|上記2つの<br>残り領域|灰色|コンソール風の会話領域を<br>構築予定
|ObJWin|SrcWinの<br>右側30%|薄緑|RETROF-16のメモリダンプを<br>表示予定
|EmuWin|ObJWinの<br>上部40％|濃紺|RETROF-16のエミュレータを<br>構築予定

####子パネルの生成方法
EmuWinを例として手順を紹介します。

この手順内では、まだ各々の子パネルの生成位置と占有サイズはまだ未定となります。また子パネルそのものもビルド（及び実行）をしてもまだ現れません。

######手順1
「プロジェクト」⇒「新しい項目の追加」、現れたダイアログの左ペインで「Windows Forms」を選択、右ペインで「継承フォーム（Windows フォーム）」を選択し、名前をEmuWinとして「追加」。
![WS2019_000693.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/c5979600-c2ef-e3cc-64d6-f3486aad3a24.jpeg)

######手順2
継承ピッカーが現れますので、そのまま何もせずに「参照」をクリックし、現れたエクスプローラで、本記事冒頭でダウンロードした「WeifenLuo.WinFormsUI.dll」を指定。

######手順3
継承ピッカーの項目が増えます。「DockContent」を選択し「OK」。
![WS2019_000695.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/deae56c5-3e02-fb86-dd04-7a60d3e9d0e3.jpeg)

######手順4
作成した子パネルに対する、普通のフォームと同様のデザイナーが表示されます。
ここでは背景色のみ変更し、他は何もせずデザインを終了します。

![WS2019_000696.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/d7923c8d-d993-726e-cd03-6a6205a208e7.jpeg)


######手順5
他の子パネルも同様に生成します。

####ソースコードの変更
この時点でビルドしても何も変化はありません。生成した子パネルを表示させるには、直接ソースファイルに手を加える必要があります。手を加えるソースファイルはMainForm.csです。生成（new）と、表示位置、サイズを指定しているだけですので、特に解説は不要かと思います。

<details><summary><font color="Red">**MainForm.csの全リスト**</font>(クリックで展開）</summary><div>

```MainForm.cs
using System.Windows.Forms;
using WeifenLuo.WinFormsUI.Docking;
namespace RETROF {
    public partial class MainForm : Form {
        public MainForm() {
            InitializeComponent();
            //-------------------------------------------
            //フォームをMDIコンテナにする
            IsMdiContainer = true;
            //ドッキングウィンドウ以外の領域（ドキュメント領域）をSDIドキュメントとする。
            MainDock.DocumentStyle = DocumentStyle.DockingSdi; 

            //子パネルの生成
            CslWin CslWin = new CslWin();
            EmuWin EmuWin = new EmuWin();
            ObjWin ObjWin = new ObjWin();
            SrcWin SrcWin = new SrcWin();
            MapWin MapWin = new MapWin();

            //子パネルの表示
            MainDock.DockRightPortion = 0.4;
            MapWin.Show(MainDock, DockState.DockRight);
            MainDock.DockTopPortion = 0.3;
            SrcWin.Show(MainDock, DockState.DockTop);
            CslWin.Show(MainDock, DockState.Document);
            ObjWin.Show(SrcWin.Pane, DockAlignment.Right, 0.3);
            EmuWin.Show(ObjWin.Pane, DockAlignment.Top, 0.4);
            //-------------------------------------------
        }
    }
}
```
　
</div></details>

MainForm.csを変更後、ビルド（及び実行）を行うと、以下の外観を持つアプリが現れます。



![aaa.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/7a676c3d-404e-64b1-2480-385a7906909b.jpeg)


<!--
この時に、灰色の領域の左上に、以下の赤丸で示す小さな三角形が現れた場合は、MainDockのDocumentStyle属性を「DockingSdi」に変更して下さい。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/b852071a-5cb8-b9a4-cbd0-3980558dcf66.jpeg" width=40%> 　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/f9b8923f-7e22-5988-34d3-9a52cb3d1b41.jpeg" width=55%>
-->

##RichTextBoxの継承
既存コントロールを単に継承するだけならば、その方法の詳細は「C# 既存コントロール 継承」等で検索すると多くの記事が見つかると思います。
ここでは標準コントロールであるRichTextBoxを単純に継承する方法ではなく、**具体的な意味を持つ独自コントロール**を作る方法を解説します。
具体的には標準コントロールであるRichTextBoxを継承し、独自コントロールShellTextBoxを作る方法となります。

####まずはRichTextBoxを単純継承しShellTextBoxを作る

これは機械的な作業ですので手順の概略のみ以下に示します。

1.　[プロジェクト(P)]⇒[新しい項目の追加(W…)]
2.　現れたダイアログで『ユーザーコントロール（Windowsフォーム）』を選択
3.　『名前』を「ShellTextBox.cs」として『追加』
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/990a94c3-9f19-0839-2e11-058cf7ee3fdf.jpeg" width=30%> 　
4. 上記の絵が出るので、この絵の上で右ボタンメニュー[コードの表示]
5. 継承元がUserControlになっているので、これをRichTextBoxに変更
6. ビルドするとShellTextBox.Designer.の27行目でエラーが出るのでこの行をコメントアウト(MS公認バグ？)
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/db8f08a8-709a-8595-38a1-06015da4e1f4.jpeg" width=75%> 
7. 再度ビルドし、ツールボックスにShellTextBoxが現れることを確認
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/17d6a8b3-3e68-c14f-ce89-e57edb1d42e6.jpeg" width=40%> 

####ShellTextBoxを単純継承した状態で使ってみる
ツールボックスに現れたShellTextBoxをデザイナーを用いてCslWinの全面（DockプロパティをFillに設定）に名前を**Shell**として貼り付け、ビルド（及び実行）します。
######Shellと言う名前について
プログラム的には、この名前自体は任意で構いませんが、ここでは以降の解説の都合上**Shell**として下さい。デザイナーは名前を**shellTextBox**を暗黙で設定しますから、これを**Shell**に変更が必要です。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/5d919aa4-58a5-5a98-3015-984c502ae56e.jpeg" width=75%>

######この時点での実行結果

![WS2019_000694.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/c49e139e-e544-9d63-2e7b-c9e157c5851a.jpeg)

継承したとは言え、機能はRichTextBoxのままですから当然、RichTextBoxを張り付けた場合と何ら変わりません。

####ShellTextBox独自の処理を実装する

ShellTextBox.csに独自コードを書き込みます。継承元のRichTextBoxに加わる主な機能は以下の通りです。

- 任意の文字列からなるプロンプトの表示
- プロンプト領域を侵すカーソル移動（BSキーや左矢印キー）を制限
- プロンプトに対する「文字列＋Enterキー」入力に対する独自イベントの発生
- プロンプト、入力文字列、出力文字列の色分け機能の実装

具体的なコードは以下となります。コードの説明はコード中にコメントとして記載してますので、それを参照して下さい。

<details><summary><font color="Red">**ShellTextBox.csの全リスト**</font>(クリックで展開）</summary><div>

```ShellTextBox.cs
using System.Windows.Forms;
using System.Drawing;

namespace RETROF {
    ///補助クラス、EventArgsクラスを継承するためだけに存在
    public class ShellEventArgs : System.EventArgs {
        public string Command;
        public string Result;
    }

    ///独自クラス
    public partial class ShellTextBox : RichTextBox {
        ///プロパティ
        private string Prompt ="RETROF>";
        private const string LFCR ="\r\n";
        private const char CR ='\n';
        private const char COUTIONCHER ='!';
        private Color ResultColor;
        private Color PromptColor;
        private Color CommandColor;
        private Color CoutionColor;

        ///デリゲートの宣言　（独自に継承したShellEventArgs型の引数を伴う）
        public delegate void ShellEventHandler(object sender, ShellEventArgs e);

        ///上記デリゲートのインスタンスを（イベント型として）定義
        public event ShellEventHandler KeyEnter;

        ///本クラスのコンストラクタ
        public ShellTextBox() {
            //RichTextBoxのアジア圏のフォント化け（公認バグ？）の抑制
            LanguageOption = RichTextBoxLanguageOptions.UIFonts;
            Prompt = ""; //仮設定、正式な設定はInitialize()で行う
        }

        ///初期化
        public void Initialize(string prompt, Color prompt_color = default(Color),
            Color command_color = default(Color), Color coution_color = default(Color)) {
            Prompt = prompt;
            PromptColor = (prompt_color != default(Color)) ? prompt_color : SelectionColor;
            CommandColor = (command_color != default(Color)) ? command_color : SelectionColor;
            CoutionColor = (coution_color != default(Color)) ? coution_color : Color.Red;
            ResultColor = SelectionColor;
            ShowPrompt();
        }

        ///MyEnterが空（==null）ではない事を確認し、「Enterキーが押されたイベント」を発行する
        protected virtual void OnConsole(ShellEventArgs e) {
            //KeyEnter(this, e);　//確認しないならこちらで十分
            KeyEnter?.Invoke(this, e);
        }

        ///（オーバーライド）マウスクリックによるカーソル移動を強制的に末尾にする
        protected override void OnMouseUp(MouseEventArgs e) {
            base.OnMouseUp(e);
            SelectionStart = int.MaxValue; //領域末尾を超す値の指定は領域末尾に移動する
        }

        ///（オーバーライド）矢印キー、BSキー、Enterキーの挙動変更
        protected override void OnKeyDown(KeyEventArgs e) {
            //カーソル上下移動の無効化
            if (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down) { e.Handled = true; return; }
            //プロンプト文字列領域を侵すBackSpace(もしくはカーソル左移動)の無効化
            if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Back) {
                if (GetColumn() < Prompt.Length + 1) { e.Handled = true; return; }
            }
            if (e.KeyCode != Keys.Enter) return; //Enterキー以外なら終わり
            //以下はEnterキーが押下された場合
            ShellEventArgs ee = new ShellEventArgs();
            ee.Command = GetCommand();　　　//コマンド（入力文字列）をShellEventArgsに設定
            OnConsole(ee);　　　            //実行　　　
            SelectionColor = ResultColor;   //実行結果の表示色を設定
            //表示文字列の先頭が「！」なら、その一文字を削除し、表示色をCoutionColorに変更
            if (ee.Result != null && ee.Result.Length != 0 && ee.Result[0] == COUTIONCHER) {
                ee.Result = ee.Result.Remove(0, 1);
                SelectionColor = CoutionColor;
            }
            //実行結果の表示
            AppendText(LFCR + ee.Result + LFCR);
            ShowPrompt();
            e.Handled = true;
        }

        ///（視認性向上目的のprivateな補助関数）プロンプトを除く現在行の取り出し
        private string GetCommand() {
            return (Text.Substring(Text.Length - GetColumn()).Substring(Prompt.Length));
        }

        ///（視認性向上目的のprivateな補助関数）現在のカーソルのカラム位置を返す
        private int GetColumn() {
            return (SelectionStart - Text.LastIndexOf(CR) - 1);
        }

        ///（視認性向上目的のprivateな補助関数）プロンプトを指定色で表示する
        private void ShowPrompt() {
            SelectionColor = PromptColor;
            AppendText(Prompt);
            SelectionColor = CommandColor;
        }
    }
}
```
　
</div></details>

この時点でビルド（及び実行）を行うと、RichTextBoxとはかなり異なる動きをする事を確認できると思います。但しこの時点ではまだプロンプトも表示されませんし、何を入力しても何も表示されません。

####ShellTextBoxが発する独自イベントを受け取る

ShellTextBoxがEnterキーを押下された時に発生するイベントは誰が（どのクラスもインスタンスが）受け取っても構いません。一般的にはShellTextBoxを生成したCslWin.csが受け取る事が多いですが、ここではMainForm.csにコードを追加し、MainForm.csが受け取る例を紹介します。

######手順１.

ShellTextBoxを利用には、MainForm.csに下記の一行の追加が必要です。最初のプロンプトはこの関数が呼ばれた時に表示されます。

```MainForm.cs
 CslWin.Shell.Initialize("PROMPT>", Color.DarkViolet, Color.Green, Color.Red);
```

引数の意味は以下の通りです。

|  |型  |意味|省略の可否|
|---|---|---|---|
|第1引数|String|プロンプト文字列|省略不可 |
|第2引数|Color|プロンプトの色|省略時はTextCororプロパティの色|
|第3引数|Color|入力文字列の色|省略時はTextCororプロパティの色|
|第4引数|Color|警告色|省略時はColor.Red（赤色）<br>警告色の意味は**手順2**を参照|

上記1行を追記すると、"CslWin.Shellはアクセスできない保護レベルになっています"のエラーが発生しますので、ClsWin.Designer.csの末尾に自動生成されたShellの定義をprivateからpublicに変更します。これでプロンプトが出現し、それに対してコマンドを入力できる事を確認でます。

```ClsWin.Designer.cs
 // private ShellTextBox Shell;
    public ShellTextBox Shell;
```

######手順２.

ShellTextBoxが発する独自イベントを受け取る宣言と、そのイベントが発生した時の処理をMainForm.csの末尾に追加します。

```MainForm.cs
///Shellに入力があれば、本クラス内の OnShell() をコールする様に設定
CslWin.Shell.KeyEnter += new ShellTextBox.ShellEventHandler(OnShell);

///Enterキーダウン時に発生する独自イベントの処理
///現時点では何を入力しても、入力文字列＋" : Unknown command"と表示する
void OnShell(object sender, ShellEventArgs e) {
    e.Result = e.Command + " : Unknown command";
}
```

OnShell()はShellTextBox内でEnterキーを押下した時に呼ばれます。この時にe.CommandにプロンプトからEnterキーまでに入力したコマンド文字列（プロンプトとEnterキーは含まない、空文の場合もある）が格納されています。またOnShell()内で任意の文字列をe.Resultに設定するとそれがOnShell()終了後にShellTextBoxに表示されます。

上記のOnShell()は単に入力文字列に、" : Unknown command"を付加したものを出力するだけの処理を行っています。出力文字の色はTextColorプロパティと同色になりますが、出力文字列の先頭文字が"!"の場合は、Initialize()の第4引数で指定した警告色で"!"を除く文字列が表示されます。

######ShellEventArgsのメンバ

|メンバ名 |型  |意味|
|---|---|---|
|command|String|入力された文字列がセットされる|
|result|String|出力したい文字列をセットする<br>先頭文字が"!"の場合は文字色が変わる<br>（先頭文字の"!"は表示されない）|
|その他||継承元であるSystem.EventArgsと同じ|

##終わりに
以上で、本記事冒頭で紹介した「何を入力してもUnKnownは返るアプリ」の完成です。

![WS2019_000697.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/535901/73f1969c-2467-856a-687b-4f8c8674fc29.jpeg)