20200910のC#に関する記事は3件です。

C# ドッキングウィンドウとRichTextBoxの継承 (RETROF-16統合開発環境①)

はじめに

本記事の内容

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

WS2019_000697.jpg
各領域は自由に配置できます。
WS2019_000704.jpg

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

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

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

開発環境等

  • OS Windows10 バージョン 1909 ビルド 18363.1016
  • Microsoft Visual Studio Community 2019 Version 16.7.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」としたものとして話を薦めます。

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

ドックパネルの採用

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

ドックパネルとは

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

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」を選択」。
(初めてドックパネルを使う場合は、「WeifenLuo.WinFormsUI.Docking.dll」は現れませんので、キャンセルをして次項の “初めて「Docking.dll」を使うVSの場合” に進んで下さい。

WS2019_000674.jpg
「OK」ボタンを押しダイアログを閉じると、ツールボックスに「DockPanel」が出現します。

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

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

(参考)ドックパネルが使えるツールボックスと使えないツールボックス

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

  

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

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

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

ここまでの実行結果

この状態でビルド(及び実行)を行うと、以下に示す形状のアプリが起動します。
見かけ上の形状は、C#によるウィンドウズアプリ構築の入門書等ででよく見かけますが、中央の灰色の広い領域が「ドッキングパネル」という特殊な領域になっている点が一般の入門書と異なります。
WS2019_000673.jpg

ドッキングウィンドウ(子パネル)の配置

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

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

子パネルの生成方法

EmuWinを例として手順を紹介します。

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

手順1

「プロジェクト」⇒「新しい項目の追加」、現れたダイアログの左ペインで「Windows Forms」を選択、右ペインで「継承フォーム(Windows フォーム)」を選択し、名前をEmuWinとして「追加」。
WS2019_000693.jpg

手順2

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

手順3

継承ピッカーの項目が増えます。「DockContent」を選択し「OK」。
WS2019_000695.jpg

手順4

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

WS2019_000696.jpg

手順5

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

ソースコードの変更

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

MainForm.csの全リスト(クリックで展開)
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);
            //-------------------------------------------
        }
    }
}

 

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

aaa.jpg

RichTextBoxの継承

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

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

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

1. [プロジェクト(P)]⇒[新しい項目の追加(W…)]
2. 現れたダイアログで『ユーザーコントロール(Windowsフォーム)』を選択
3. 『名前』を「ShellTextBox.cs」として『追加』
 
4. 上記の絵が出るので、この絵の上で右ボタンメニュー[コードの表示]
5. 継承元がUserControlになっているので、これをRichTextBoxに変更
6. ビルドするとShellTextBox.Designer.の27行目でエラーが出るのでこの行をコメントアウト(MS公認バグ?)

7. 再度ビルドし、ツールボックスにShellTextBoxが現れることを確認

ShellTextBoxを単純継承した状態で使ってみる

ツールボックスに現れたShellTextBoxをデザイナーを用いてCslWinの全面(DockプロパティをFillに設定)に名前をShellとして貼り付け、ビルド(及び実行)します。

Shellと言う名前について

プログラム的には、この名前自体は任意で構いませんが、ここでは以降の解説の都合上Shellとして下さい。デザイナーは名前をshellTextBoxを暗黙で設定しますから、これをShellに変更が必要です。

この時点での実行結果

WS2019_000694.jpg

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

ShellTextBox独自の処理を実装する

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

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

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

ShellTextBox.csの全リスト(クリックで展開)
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;
        }
    }
}

 

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

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

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

手順1.

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(赤色)
警告色の意味は手順2を参照

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

ClsWin.Designer.cs
 // private ShellTextBox Shell;
    public ShellTextBox Shell;
手順2.

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 出力したい文字列をセットする
先頭文字が"!"の場合は文字色が変わる
(先頭文字の"!"は表示されない)
その他 継承元であるSystem.EventArgsと同じ

終わりに

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

WS2019_000697.jpg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# ドッキングウィンドウとRichTextBoxの継承 (RETROF-16統合開発環境 序章)

はじめに

本記事の内容

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

WS2019_000697.jpg

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

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

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

開発環境等

  • OS Windows10 バージョン 1909 ビルド 18363.1016
  • Microsoft Visual Studio Community 2019 Version 16.7.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」としたものとして話を薦めます。

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

ドックパネルの採用

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

ドックパネルとは

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

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」を選択」。
(初めてドックパネルを使う場合は、「WeifenLuo.WinFormsUI.Docking.dll」は現れませんので、キャンセルをして次項の “初めて「Docking.dll」を使うVSの場合” に進んで下さい。

WS2019_000674.jpg
「OK」ボタンを押しダイアログを閉じると、ツールボックスに「DockPanel」が出現します。

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

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

(参考)ドックパネルが使えるツールボックスと使えないツールボックス

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

  

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

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

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

ここまでの実行結果

この状態でビルド(及び実行)を行うと、以下に示す形状のアプリが起動します。
見かけ上の形状は、C#によるウィンドウズアプリ構築の入門書等ででよく見かけますが、中央の灰色の広い領域が「ドッキングパネル」という特殊な領域になっている点が一般の入門書と異なります。
WS2019_000673.jpg

ドッキングウィンドウ(子パネル)の配置

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

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

子パネルの生成方法

EmuWinを例として手順を紹介します。

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

手順1

「プロジェクト」⇒「新しい項目の追加」、現れたダイアログの左ペインで「Windows Forms」を選択、右ペインで「継承フォーム(Windows フォーム)」を選択し、名前をEmuWinとして「追加」。
WS2019_000693.jpg

手順2

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

手順3

継承ピッカーの項目が増えます。「DockContent」を選択し「OK」。
WS2019_000695.jpg

手順4

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

WS2019_000696.jpg

手順5

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

ソースコードの変更

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

MainForm.csの全リスト(クリックで展開)
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);
            //-------------------------------------------
        }
    }
}

 

MainForm.csを変更後、ビルド(及び実行)を行うと、以下の外観を持つアプリが現れます。
(フォームのタイトルは「ドッキングウィンドウの実例」に変更しています)

WS2019_000672.jpg

RichTextBoxの継承

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

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

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

1. [プロジェクト(P)]⇒[新しい項目の追加(W…)]
2. 現れたダイアログで『ユーザーコントロール(Windowsフォーム)』を選択
3. 『名前』を「ShellTextBox.cs」として『追加』
 
4. 上記の絵が出るので、この絵の上で右ボタンメニュー[コードの表示]
5. 継承元がUserControlになっているので、これをRichTextBoxに変更
6. ビルドするとShellTextBox.Designer.の27行目でエラーが出るのでこの行をコメントアウト(MS公認バグ?)

7. 再度ビルドし、ツールボックスにShellTextBoxが現れることを確認

ShellTextBoxを単純継承した状態で使ってみる

ツールボックスに現れたShellTextBoxをデザイナーを用いてCslWinの全面(DockプロパティをFillに設定)に名前をShellとして貼り付け、ビルド(及び実行)します。

Shellと言う名前について

プログラム的には、この名前自体は任意で構いませんが、ここでは以降の解説の都合上Shellとして下さい。デザイナーは名前をshellTextBoxを暗黙で設定しますから、これをShellに変更が必要です。

この時点での実行結果

WS2019_000694.jpg

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

ShellTextBox独自の処理を実装する

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

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

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

ShellTextBox.csの全リスト(クリックで展開)
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;
        }
    }
}

 

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

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

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

手順1.

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(赤色)
警告色の意味は手順2を参照

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

ClsWin.Designer.cs
 // private ShellTextBox Shell;
    public ShellTextBox Shell;
手順2.

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 出力したい文字列をセットする
先頭文字が"!"の場合は文字色が変わる
(先頭文字の"!"は表示されない)
その他 継承元であるSystem.EventArgsと同じ

終わりに

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

WS2019_000697.jpg

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】一定のテンポで処理をする【メトロノーム】

Unityで一定のテンポを刻みたくなりました。

【環境】

・Mac OSX El Capitan
・Unity versiton:2018.3.0

音だけをテンポよく鳴らしたい場合

音だけ大丈夫な場合はこちらの記事の仕組みがとてもいい感じです。
[Unity] 正確な時間間隔で効果音を出す
ググってたら上記の記事に書かれているコードを解説しているQ/Aに出会ったので貼っておきます。
【Unity】音ゲー制作で曲とテンポずれてしまう

音に合わせて他のことも同時に処理したい場合

AudioSource.PlayScheduled();は音を出してくれますが、音と同時に別の処理がしたい場合は他の方法が必要です。

Case1:ずっと同じテンポでOK!→InvokeRepeating()を使う

InvokeRepeatingを使う手があります。
メリットはコードがシンプルなので読みやすいこと!
第一引数にリピートしたいメソッド名、第二引数に始めるまでの待ち時間(秒)、第三引数にリピートする間隔(秒)を入れます。

public void InvokeRepeating (string methodName, float time, float repeatRate);
MonoBehaviour.InvokeRepeating

TempoMaker_InvokeRepeating.cs
//invokerepeatingのつかいかた
//https://docs.unity3d.com/ja/current/ScriptReference/MonoBehaviour.InvokeRepeating.html

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TempoMaker_InvokeRepeating : MonoBehaviour
{
    [SerializeField] AudioSource audioSource;
    public const float START_SECONDS = 0.0f;
    public const float INTERVAL_SECONDS = 1.0f;

    // Start is called before the first frame update
    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        InvokeRepeating("PlaySound", START_SECONDS, INTERVAL_SECONDS);
    }

    private void PlaySound()
    {
        audioSource.Play();
        print("Played");
    }
}

空のオブジェクトにAudioSourceとこのスクリプトをアタッチ。
AudioSourceには適当なAudioClipを入れておいてください。
ゲームを走らせると、一定間隔で音が再生され、コンソールに”Played”が増えていくと思います。

Case2:テンポは途中で変えたい!→コルーチンを使う

途中で好きなテンポに変えたいのであればコルーチンを使いましょう。
INTERVAL_SECONDSの値を変えることでテンポを変更できます。
内容はTempoMaker_InvokeRepeating.csとほぼ一緒ですが、マウスのボタンを押している間はテンポを止める機能を追加しています。

C#TempoMaker_Coroutine.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TempoMaker_Coroutine : MonoBehaviour
{
    [SerializeField] AudioSource audioSource;
    // Start is called before the first frame update
    private IEnumerator coroutine;
    public float INTERVAL_SECONDS = 1.0f;//インスペクターから変更可能(もちろんスクリプトからも)
    void Start()
    {
        audioSource = GetComponent<AudioSource>();
        coroutine = TempoMake();
        StartCoroutine(coroutine);

    }

    // Update is called once per frame
    void Update()
    {
        //マウスの左ボタンを押している間は止める、離すと再スタート
        if (Input.GetMouseButtonDown(0)) {
            StopCoroutine(coroutine);
        }else if (Input.GetMouseButtonUp(0)){
            StartCoroutine(coroutine);
        }
    }

    IEnumerator TempoMake()//一定間隔で音を鳴らして、”Played”をコンソールに表示
    {
        while (true) {
            yield return
             new WaitForSecondsRealtime(INTERVAL_SECONDS);
            audioSource.Play();
            print("Played");
        }
    }
}

InvokeRepeating vs コルーチン

InvokeRepeatingは可読性は高いですが、大量のオブジェクトに使いたい場合はパフォーマンス的に不向きのようでうです。
https://answers.unity.com/questions/477862/what-is-the-best-between-startcoroutine-or-invoker.html

InvokeRepatingとコルーチンのメリット・デメリットはこちらの記事にまとまっていました。(意訳しています)
InvokeRepeating vs Coroutines: Run a method at certain time intervals

Using an Invoke (or InvokeRepeating) is easier than using a coroutine. On the other hand, Coroutines are more flexible. You cannot pass a parameter to an invoked method but you can do this to a coroutine.

Invoke (または InvokeRepeating) はコルーチンより使いやすい一方、コルーチンは自由度が高い。
Invokeには変数を渡せないが、コルーチンなら渡すことできる。

Another thing which we have to mention is coroutines are more performance-friendly than the Invoke. For basic games, it does not matter much but if you have several objects which do the same thing, you should consider using Coroutine instead of Invoke.

さらに言えることはコルーチンの方がパフォーマンスに優れているということ。簡素なゲームであればあまり関係ないが、複数のオブジェクトで同じことを行う場合、Invokeの代わりにコルーチンの使用を考えるべき。

The last difference between Invoke and Coroutine which we will cover is the execution condition after the deactivation of the object. Invoke and InvokeRepeating do not stop after the game object is deactivated. On the other hand, this not true for coroutines. They stop after the game object is deactivated or destroyed. Therefore, you should use Invoke or InvokeRepeating, if you would like your method to continue running, even though the object is deactivated after the method is triggered.

最後の違いは、オブジェクトを非アクティブにした後の実行状況。InvokeとInvokeRepeatingはゲームオブジェクトを非アクティブにした後も止まらない。一方でコルーチンはゲームオブジェクトを非アクティブにしたり、デストロイすると止まる。なので、もしオブジェクトが非アクティブになってもメソッドを回し続けたいのであればInvokeかInvokeRepeatingを使うのが良い。

以上です。コルーチン便利だなー。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む