PropertyGridにコピペ機能を追加する(1/2)

# はじめに
PropertyGridにいろいろ出力して値が見れて便利！→でも上手くコピペできないじゃん。
値のところを範囲選択して何とかコピペしてみるけど、やっぱりプロパティ名＋値でツリーっぽくなってほしい。

ここでは、複数回に分けて、PropertyGridのコピペ機能拡張に挑んでみます。
本記事では、比較的容易で効果の高い方法で拡張を行っていきます。
大まかに分けて以下の機能を追加します。
1. プロパティ名＋値のコピー
2. ツリー構造でコピー
3. LINQPad連携 (HTML表示)
![Figure1.png](https://qiita-image-store.s3.amazonaws.com/0/98713/832653bb-fcdb-ded0-0834-9cc0cd432c6e.png)

[第2回はこちら http://qiita.com/longlongago_k/items/a58e7b5e57e02d90e544](http://qiita.com/longlongago_k/items/a58e7b5e57e02d90e544)

#プロパティ名＋値のコピー 機能の実装
## プロジェクトの作成
1. クラスライブラリプロジェクトを作成します。デフォルトで作成されるClass1.csはいらないので削除してください。
2. プロジェクトを右クリック→追加→新しい項目→カスタムコントロール でカスタムコントロールを追加してください。ここではPropertyGridPlusという名前で作成しました。
3. 親クラスはPropertyGridへ変更してください。
この時点でのコードは以下です。

```csharp
//PropertyGridPlus.cs
namespace PropertyGridPlus
{
  public partial class PropertyGridPlus : PropertyGrid //ControlからPropertyGridに変更する
  {
    public PropertyGridPlus()
    {
      InitializeComponent();
    }

    protected override void OnPaint(PaintEventArgs pe)
    {
      base.OnPaint(pe);
    }
  }
}
```
##コピー機能の実装
まずはUIから作成していきます。右クリメニューを作成します。
1. デザイナを開き、ツールボックスからContexMenuStripをD&Dして追加します。
2. カスタムコントロールの時はなぜかグラフィカルに編集できないので、Itemsプロパティを直接編集して"選択した項目をコピー"と言うようなメニューを追加します。
3. デザイナの何もないところをクリックするかプロパティビューの上の欄から選ぶかしてPropertyGridPlus本体を選択します。ContexMenuStripに2.で作成したContexMenuStripを設定します。この時点で適当なフォームにPropertyGridPlusを張り付けると以下のようになっています。細かいところは適当にチューニングしてください。
![Figure2.png](https://qiita-image-store.s3.amazonaws.com/0/98713/9f7b2fae-a795-f520-d440-e02fed6ead4a.png)
4. 3.で作成したメニューのClickイベントとコピー用テキスト生成関数を作成します。Clickイベントはデザイナから作れなかったのでコードで入れています。PropertyGridで現在選択している項目はGridItem型のSelectedGridItemで取得できます。SelectedGridItemを入力として、コピー用テキストを生成するCreateTextを作成します。今後色々機能を追加するので引数は少し賑やかになっています。詳しくはコメントを見てください。

```csharp
  public partial class PropertyGridPlus : PropertyGrid
  {
    public PropertyGridPlus()
    {
      InitializeComponent();
      toolStripMenuItemCopySelected.Click += ToolStripMenuItemCopySelected_Click;
    }

    private void ToolStripMenuItemCopySelected_Click(object sender, EventArgs e)
    {
      string text = CreateText(SelectedGridItem, false, 1);
      Clipboard.SetText(text);
    }
    /// <summary>
    /// 選択した項目の内容をテキストに出力します。
    /// </summary>
    /// <param name="item">選択した項目</param>
    /// <param name="useIndent">インデント処理するか</param>
    /// <param name="limitDepth">子アイテムを読み込む深さ</param>
    /// <param name="depth">再帰処理用のカウンタ(内部用)</param>
    /// <returns></returns>
    public string CreateText(GridItem item, bool useIndent, int limitDepth = int.MaxValue, int depth = 0)
    {
      string ret = "";
      ret = $"{item.Label}\t{item.Value}";
      return ret;
    }
  }
```
この時点で選択した項目がいい感じにコピーできるようになっています。
##ツリー構造でコピーする
ツリー構造でコピーする際は、以下について考慮する必要があります。
1. どの深さまでツリーを展開するかを設定できるようにする必要がある
2. **”①前方のスペース”＋”②プロパティ名”＋"③後方のスペース"＋"値"**の構成でテキストを生成する必要がある。①についてはツリーの深さ分タブを入れればよいです。③については、一番深いアイテムの深さを1.も考慮した上でカウントする必要があります。

###一番深いアイテムの深さを取得する
- PropertyGridから渡されるGridItem
- 読み込む最大の深さ

を入力として一番深いアイテムの深さを取得します。再帰処理で一段ツリーを展開するごとに深さ+1をするような処理になっています。

```csharp
 /// <summary>
 /// 指定した深さ以内で最も深い深さを取得します。
 /// </summary>
 /// <param name="item">PropertyGridのアイテム</param>
 /// <param name="limitDepth">最大深さ</param>
 /// <param name="currentDepth">再帰処理用の内部カウンタ。呼び出す際は指定しない。</param>
 /// <returns></returns>
 private int getDepthFromGridItem(GridItem item, int limitDepth, int currentDepth = 0)
 {
   //指定した深さ以上になっていればツリーの展開を中断し、現在の深さをそのまま返す。
   if (currentDepth >= limitDepth)
   {
     return currentDepth;
   }
   //サブツリーの展開
   int retDepth = currentDepth;
   foreach (var child in item.GridItems)
   {
     int childDepth = getDepthFromGridItem(child as GridItem, limitDepth, currentDepth + 1);
     //サブツリーの中で最も深い深さを取得する。
     if (retDepth < childDepth)
       retDepth = childDepth;
   }
   return retDepth;
 }
```
###インデント付きコピーを実装する


1. 現在の深さ分タブを入れる。(インデント)
2. プロパティ名を出力する。もし、子要素を持っていれば先頭に"+"や"▼"等の記号を入れる。
3. (最大深さ - 現在の深さ + 1 )個だけタブを入れて値を出力する。
![Figure3.png](https://qiita-image-store.s3.amazonaws.com/0/98713/0da8f5b1-dc20-d1ca-57d7-fd7b4652e2d3.png)

コードは以下のようになります。インデント有り版と無し版で関数を分けました。

```csharp
    /// <summary>
    /// 選択した項目の内容をテキストに出力します。
    /// </summary>
    /// <param name="item">選択した項目</param>
    /// <param name="useIndent">インデント処理するか</param>
    /// <param name="limitDepth">子アイテムを読み込む深さ</param>
    /// <param name="depth">再起処理用のカウンタ(内部用)</param>
    /// <returns></returns>
    public string CreateText(GridItem item, bool useIndent, int limitDepth = int.MaxValue, int depth = 0)
    {
      string ret = "";
      if (useIndent)
      {
        int depth_max;
        depth_max = getDepthFromGridItem(item, limitDepth);
        ret = createTextWithIndent(item, limitDepth, depth_max);
      }
      else
      {
        ret = createTextWithoutIndent(item, limitDepth);
      }

      return ret;
    }

    /// <summary>
    /// GridItemからツリーのコピーテキストを取得します。
    /// </summary>
    /// <param name="item">PropertyGridのアイテム</param>
    /// <param name="limitDepth">最大の深さ</param>
    /// <param name="maxDepth">最も深いノードの深さ(getDepthFromGridItemで取得する)</param>
    /// <param name="currentDepth">再帰処理用の内部カウンタ。呼び出す際は指定しない。</param>
    /// <returns></returns>
    private string createTextWithIndent(GridItem item, int limitDepth, int maxDepth, int currentDepth = 0)
    {
      StringBuilder ret = new StringBuilder();
      //インデントのため、タブを現在の深さ分挿入する。
      ret.Append(new string('\t', currentDepth));
      //子アイテムがあるときの記号。自由に設定してください。
      string expandSign = "";
      if (item.GridItems.Count > 0)
        expandSign = "▼";
      //プロパティ名の追加
      ret.Append($"{expandSign}{item.Label.TrimStart('\t')}");
      //プロパティ名と値の間のスペースを追加
      ret.Append($"{new string('\t', maxDepth - currentDepth + 1)}");
      //値の出力
      ret.AppendLine($"{item.Value}");
      //サブツリーの展開
      if (currentDepth < limitDepth)
      {
        foreach (var child in item.GridItems)
        {
          ret.Append(createTextWithIndent(child as GridItem, limitDepth, maxDepth, currentDepth + 1));
        }
      }
      return ret.ToString();
    }

    /// <summary>
    /// GridItemからツリーのコピーテキストを取得します。(インデント無し版)
    /// </summary>
    /// <param name="item">PropertyGridのアイテム</param>
    /// <param name="limitDepth">最大の深さ</param>
    /// <param name="maxDepth">最も深いノードの深さ(getDepthFromGridItemで取得する)</param>
    /// <param name="currentDepth">再帰処理用の内部カウンタ。呼び出す際は指定しない。</param>
    /// <returns></returns>
    private string createTextWithoutIndent(GridItem item, int limitDepth, int currentDepth = 0)
    {
      StringBuilder ret = new StringBuilder();
      ret.AppendLine($"{item.Label}\t{item.Value}");
      if (currentDepth < limitDepth)
      {
        foreach (var child in item.GridItems)
        {
          ret.Append(createTextWithoutIndent(child as GridItem, limitDepth, currentDepth + 1));
        }
      }
      return ret.ToString();
    }
```

ここでのポイントは、stringのコンストラクタでn個の文字を生成するすることができる事です。
> new string('\t', 3); //タブを3個生成

###クリップボードへテキストをコピーする
これまで組んだコードで生成したテキストをクリップボードへコピーします。これは簡単で、Clipboard.SetText()するだけです。

```csharp
    //コンテキストメニューのClickイベントハンドラ
    private void ToolStripMenuItemCopyAllWithIndent_Click(object sender, EventArgs e)
    {
      string text = CreateText(SelectedGridItem, true);
      Clipboard.SetText(text);
    }
```
##完成
コンテクストメニューに対して適当にコピー設定のバリエーションを作るか、コピー設定ダイアログ等を作ってCreateText()を呼び出してください。
以下は実装例です。
![Figure4.png](https://qiita-image-store.s3.amazonaws.com/0/98713/1eaf5bf0-d0a0-92f0-3ac6-162933b7dcf4.png)

サンプルプロジェクトファイルは下記になります。zipがアップロードできなかったので、拡張子をjpgに変えています。
右クリックから保存してjpg→zipにリネームしてください。
![PropertyGridPlus.zip.jpg](https://qiita-image-store.s3.amazonaws.com/0/98713/63199fc4-8936-0f5c-c674-77f337186f7a.jpeg)