UiPathのカスタムアクティビティ開発で、見た目のカスタマイズを行う

ずっと調べているUiPath Studio のカスタムアクティビティ開発。いままでの記事はこんな感じ。

## いままでの記事目次
- [UiPath Studioで使用するカスタムアクティビティの作成方法](https://qiita.com/masatomix/items/1f63513e80313a99faeb)
- [UiPath Studioで使用するカスタムアクティビティの作成方法 つづき(GUI構築)](https://qiita.com/masatomix/items/e75598f73b0e019044ac) 
- [UiPathのカスタムアクティビティ開発で、外部ライブラリを使用する](https://qiita.com/masatomix/items/61024a6b9294a1707d23)
- [UiPathのカスタムアクティビティのnupkgの作り方(元記事の訂正)](https://qiita.com/masatomix/items/d62683a158da6d35f24d)
- [UiPathのカスタムアクティビティの配置場所について](https://qiita.com/masatomix/items/b8988fcc2bd7046825f2)

さてなんとなく開発のやり方が分かってきたところで、あとやりたいのは「Studio上での見た目のカスタマイズ」と「そのローカライズ」などがあったりしますが、まずは見た目のカスタイマイズについてです。


見た目のカスタマイズといってるのは、ざっくり言うと、

1. Activitiesペイン上のツリー構造
- Activitiesペイン上のアクティビティ名
- Activitiesペイン上のアクティビティにマウスを当てたときのツールチップ文言
- Propertiesペイン上の、プロパティの説明文言
- Propertiesペイン上の、カテゴリ名
- Propertiesペイン上の、プロパティ変数名
- カスタムアクティビティのアイコン

などのことです。何もしない状態だとこんな↓感じですが、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/34cafa23-fa9e-8c9e-2636-b707b2195e7c.png)


見た目のカスタマイズをすることで、こんな感じになります。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/3273db6c-7195-d4ac-05a4-eeb120282146.png)

愛着がわくというかなんというか、、自作してる！って感じがでてきますね。。


##やってみる

### ソースコード

まず対応前の初期状態は、こちら。
https://github.com/masatomix/UiPath_Path/releases/tag/gui_attribute_init
ココから開始します。

で、修正した結果の今回の成果物はこちら。
https://github.com/masatomix/UiPath_Path/releases/tag/gui_attribute_finish

さき書いてしまうと、差分はこんなかんじになっています。
https://github.com/masatomix/UiPath_Path/compare/gui_attribute_init...gui_attribute_finish


### 見た目をカスタマイズする、DesignerMetadata.cs の追加

プロジェクトに下記のコードを追加します。このクラスは IRegisterMetadata のサブクラスになっていて、UiPath Studioにこの nupkg が読み込まれたときに、自動的にロードされるようです。

```c#:DesignerMetadata.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.ComponentModel;
using System.Threading;
using System.Activities.Presentation.Metadata;
using Utils.PathUtils;

namespace Utils
{
    public class DesignerMetadata : IRegisterMetadata
    {
        public void Register()
        {
            AttributeTableBuilder builder = new AttributeTableBuilder();

            //string PathUtils_categories = "Utils.Path Utilities";
            string PathUtils_categories = "ツリー1.ツリー2.Utilities";

            // Activitiesペイン上のツリー構造を構築する。
            // ドットで階層を表現する
            builder.AddCustomAttributes(typeof(Combine), new CategoryAttribute(PathUtils_categories));

            // Activitiesペイン上のアクティビティをポイントしたときに表示されるツールチップ
            builder.AddCustomAttributes(typeof(Combine), new DescriptionAttribute("ツールチップに説明文を書こう"));

            // プロパティペイン内のプロパティの説明
            builder.AddCustomAttributes(typeof(Combine), nameof(Combine.PathArray), new DescriptionAttribute("このプロパティの説明を書く"));  //クラスのプロパティに直接 Desc書いたのと意味おなじ。

            // プロパティペインの、カテゴリ名
            builder.AddCustomAttributes(typeof(Combine), nameof(Combine.PathArray), new CategoryAttribute("インプット"));  //クラスのプロパティに直接 Categoryを書いたのと意味おなじ。

            // Activitiesペイン上のアクティビティ名
            builder.AddCustomAttributes(typeof(Combine), new DisplayNameAttribute("パス連結アクティビティ"));  //クラスに直接 DisplayNameを書いたのと意味おなじ。

            // プロパティペイン内のプロパティの変数名
            builder.AddCustomAttributes(typeof(Combine), nameof(Combine.PathArray), new DisplayNameAttribute("パス配列"));  //クラスのプロパティに直接 DisplayNameを書いたのと意味おなじ。

            MetadataStore.AddAttributeTable(builder.CreateTable());
        }
    }
}
```

このように、クラスや、そのクラスのプロパティ(メンバ変数)に対して、XXAttributeをAddすることで情報を記載していきます。

一つ一つ見ていくと、以下の通り。

### 1. Activitiesペイン上のツリー構造

```c#
string PathUtils_categories = "ツリー1.ツリー2.Utilities";
builder.AddCustomAttributes(typeof(Combine), new CategoryAttribute(PathUtils_categories));
```
とすると、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/1f800fb4-cb4c-5a03-9b91-61842dacb5da.png)
のようになります。ドットがデリミタになって、階層構造を構築します。

ちなみにデフォルト状態ではC#のnamespace名がセットされるようです。なのでnamespaceの階層がそのままツリー構造になる感じですね。

### Activitiesペイン上の、2.アクティビティ名、3.ツールチップ文言

```c#
builder.AddCustomAttributes(typeof(Combine), new DisplayNameAttribute("パス連結アクティビティ"));
builder.AddCustomAttributes(typeof(Combine), new DescriptionAttribute("ツールチップに説明文を書こう"));
```
とすると
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/f7d4e160-f518-4039-d7a6-fac3c29bc796.png)
のようになります。


ちなみに、DisplayNameAttribute については、直接クラスに対して、

```c#:Combine.cs
[Designer(typeof(CombineDesigner))]
[DisplayName("パス連結アクティビティ")]   ← コレ
public sealed class Combine : CodeActivity
{
...
```

とAttributeを書いたことと実質等価なようですが、ローカライズを考慮したときに前述のやりかたのほうが相性がよいようでなので、このやり方を採用します[^1]。

[^1]: クラス内で、プロパティに直接Attributeするやり方は `[Category("インプット")]` などの引数の文字列が定数である必要があり、その制約が、ローカライズしたときのキー値を渡すやり方と相性が悪いんです。その結果Attributeのサブクラスを作らなくてはいけなくなったり何かと面倒でしたorz

また DisplayNameAttribute はデフォルト状態ではクラス名のCamelCaseをスペースに変換してセットされるようです。Combine → Combine、CurrentDir → Current Dir ですが Base64Encode → Base 64 Encode などおしいっ、てヤツも。




### Propertiesペイン上の、4.プロパティの説明文言、5.カテゴリ名、6.プロパティ変数名
```c#
builder.AddCustomAttributes(typeof(Combine), nameof(Combine.PathArray), new DescriptionAttribute("このプロパティの説明を書く"));
builder.AddCustomAttributes(typeof(Combine), nameof(Combine.PathArray), new CategoryAttribute("インプット"));
builder.AddCustomAttributes(typeof(Combine), nameof(Combine.PathArray), new DisplayNameAttribute("パス配列"));
```

とすると
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/9be2ce69-ee01-54b4-3a2b-7e62edef74e0.png)
のようになります。

ちなみに、これらはそれぞれ、直接クラスのプロパティに対して、

```c#:Combine.cs
[Description("このプロパティの説明を書く")]
[Category("インプット")]
[DisplayName("パスの配列")]
public InArgument<String[]> PathArray { get; set; }
```

とAttributeを書いたことと実質等価なようですが、先と同様の理由により、このやり方を採用します。

ちなみにDisplayNameAttribute はデフォルト状態では、クラスの変数名がそのままセットされるようです。



### 7.アイコン
アイコンは、GUI上で下記の通り設定します。

```xml:CombineDesigner.xaml

<sap:ActivityDesigner x:Class="Utils.PathUtils.CombineDesigner"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:s="clr-namespace:System;assembly=mscorlib"
    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation"
    xmlns:sapc="clr-namespace:System.Activities.Presentation.Converters;assembly=System.Activities.Presentation" >

    <sap:ActivityDesigner.Resources>
        <ResourceDictionary>
            <sapc:ArgumentToExpressionConverter x:Key="ArgumentToExpressionConverter" />
        </ResourceDictionary>
    </sap:ActivityDesigner.Resources>
    <!-- ココから -->
    <sap:ActivityDesigner.Icon>
        <DrawingBrush>
            <DrawingBrush.Drawing>
                <DrawingGroup>
                    <DrawingGroup.Children>
                        <DrawingGroup>
                            <DrawingGroup.Transform>
                                <MatrixTransform Matrix="1,0,0,1,0,0"/>
                            </DrawingGroup.Transform>
                            <DrawingGroup.Children>
                                <ImageDrawing ImageSource="../images/folder_wrench.png" Rect="0,0,16,16"/>
                            </DrawingGroup.Children>
                        </DrawingGroup>
                    </DrawingGroup.Children>
                </DrawingGroup>
            </DrawingBrush.Drawing>
        </DrawingBrush>
    </sap:ActivityDesigner.Icon>
    <!-- ココまで -->
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="3*"/>
            <ColumnDefinition Width="7*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>

        <Label Content="pathArray" />
        <sapv:ExpressionTextBox 
        Expression="{Binding ModelItem.PathArray, 
            ConverterParameter=In, 
            Converter={StaticResource ArgumentToExpressionConverter}, 
            Mode=TwoWay}"
        ExpressionType="{x:Type s:String[]}"
        HintText="Enter a string array"
        OwnerActivity="{Binding ModelItem}"
        MaxLines="1" Grid.Column="1" />
    </Grid>
</sap:ActivityDesigner>
```

なんだか階層深すぎですが、
`<ImageDrawing ImageSource="../images/folder_wrench.png" Rect="0,0,16,16"/>`
このようにアイコンを設定しています。

さてプロジェクトへのアイコンファイルの追加ですが、xamlがある場所からの相対パスとなるので、そこから一つ上にのぼって imagesディレクトリを作り、そこにpngファイルを物理的に配置します。

で、Visual Studioは自動的にディレクトリやファイルを追加してくれないので、先ほど作ったimagesディレクトリをソリューションエクスプローラのUtilsの場所にドラッグ&ドロップします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/67ff3674-d8c6-2451-8bcb-06317313d881.png)

追加されたようです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/bc0ac754-07b8-5e34-2f6b-c836936d486b.png)


さてもうひとつ重要な点ですが、このように追加されたpngアイコンですが、プロパティの「ビルドアクション」がデフォルトでは「コンテンツ」になっているようです。コレを「Resource」に変更しないとアイコンとして利用出来ないようなので、忘れずに変更しておきましょう。

変更前:
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/8a3fa683-c05d-3111-357c-4e4d1fc3310f.png)


変更後:
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/5f659f78-43fc-005c-08e2-6ab25102193d.png)
コレでOKです。

ちなみにアイコンはむかっしからある、
http://www.famfamfam.com/lab/icons/silk/
を利用させていただいています。感謝。


さあ[プロジェクトをリビルドしてnupkgを作成](https://qiita.com/masatomix/items/1f63513e80313a99faeb#nupkg%E4%BD%9C%E6%88%90)して、Studioにインストールしましょう。カスタマイズ後のUIが表示されればＯＫです！

おつかれさまでした。


## 関連リンク
- [【UiPath】カスタムアクティビティのアイコンを変更する](https://qiita.com/UmegayaRollcake/items/4fe69c00e82402cc478b) アイコン変更について、参考にさせていただきました。ありがとうございます。
- [UiPath/Community.Activities](https://github.com/UiPath/Community.Activities)　今回のDesignerMetadata.csや、このあとやるローカライズなどはココを思いきり参考にしました。はじめ自前でちょーーー調べてめちゃ時間使ったんだけど、答えは以前みていたUiPathの中のヒトのリポジトリにありましたorz。感謝。
- [今回の完成版のソース](https://github.com/masatomix/UiPath_Path/releases/tag/gui_attribute_finish)
- [本アクティビティのマスターブランチ](https://github.com/masatomix/UiPath_Path)
- [nugetのダウンロードリンク kino.UiPath.Utils.Activities](https://www.nuget.org/packages/kino.UiPath.Utils.Activities/)