UiPathのデモがしやすくなるカスタムアクティビティをVisual Studioで作ろう

UiPathのデモをおこなうことって、よくありますよね。
デモをしている時、今手動なのかロボットが実行しているか分からない。もっとロボットが実行していることをアピールしたいことってありませんか？

今回は、ロボット実行中であることを分かりやすくするカスタムアクティビティを作りました。

![Snapshot_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/8b226091-bd94-1141-d0f7-c512c328e80c.png)

今回は、カスタムアクティビティ作成を時短できるActivity Createrを使用してカスタムアクティビティを作成しています。

https://marketplace.uipath.com/listings/activity-set-creator

Visual Studioでカスタムアクティビティを作りたいと考えている方にも参考になるかと思います。
オリジナルの画像に差し替えたり、機能を追加したりチャレンジしてみてください。

公式ドキュメントはこちらをご覧ください

https://docs.uipath.com/developer/docs/using-activity-creator


## 実際に作ったもの(PresentationRobot)

Youtubeデモはこちら

<iframe width="100%" height="600" src="https://www.youtube.com/embed/fyoGTt4VihQ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

https://youtu.be/fyoGTt4VihQ

ソースコードはGithubにアップしています。
Marketplaceには申請中ですので、すぐに使いたい人はGithubからダウンロードしてください。

https://github.com/manabutech-kyushu/uipath-presentation-robot/tree/main/Output

### 機能

指定したメッセージをロボット実行中に表示し続けます

### 効果

-	ロボットが実行中であることが分かります
-	表示を更新させることで、ロボットが何の操作を行っているか分かります

### 注意

UiPath Marketplaceで提供されている字幕アクティビティを参考にしています。

https://marketplace.uipath.com/ja/listings/subtitle-activity

字幕アクティビティは、字幕表示中は処理を止めていますが、今回のアクティビティは止めていません。
よって、UiPathのGUI処理が今回作成するアクティビティの表示と重なってしまうとクリックや文字入力ができない可能性がありますのでご注意ください。シミュレートやウィンドウメッセージ送信等のバックグラウンドで動作するオプションを活用いただくか、表示位置を調整してください。

表示メッセージはプロセス終了まで消えずに残り続けますので、メッセージを更新する際はIsFullWidthをTrueにして使用することを推奨します。

## 作り方

それでは実際にVisual Studioでカスタムアクティビティを作っていきましょう。
当たり前ですが、UiPath Studioは必要ですのでインストールしておいてください。
カスタムアクティビティを作成するには以下のXステップ必要です

+ Visual Studio2019のインストール
+ UiPath Activity Creator 拡張機能の追加
+ UiPathプロジェクトの作成
+ アクティビティの追加
+ 機能の実装
+ パッケージのビルド
+ UiPath Studioで使用


### Visual Studio2019のインストール

Visual Studio 2019をインストールします。
.NETデスクトップ開発にチェックを入れておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/44334985-a3a8-fe4d-f6df-1dbe1896507c.png)

### UiPath Activity Creator 拡張機能の追加

拡張機能の管理よりUiPathと検索して、UiPath Activity Createrをダウンロードしましょう。1回閉じるとインストールが開始されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/ad261fac-d3d7-f2bc-c179-312ff87c2ad6.png)

### プロジェクトの作成と準備

Visual Studioのインストールが終わりましたので、実際にプロジェクトを作成しましょう。
UiPath Standard Activity Projectを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/14eed1da-d103-fdf1-69c3-f71b88ecdcb3.png)

プロジェクトの名前を付けます。
ここでは、PresentationRobotとします。
ManabuTech.PresentationRobotとすると、アクティビティパネルではフォルダで構造化されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/5f22a25c-6bcd-1bd2-b5c3-fc99e2ac4334.png)


### アクティビティの追加

拡張機能からUiPathの下にある「Add Activities」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/01e6c5bb-7c7c-8180-8ded-c46960e59a2a.png)

Createを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/c9958623-9861-eff9-1ce1-f2dc00e6a799.png)

＋ボタンからアクティビティを追加します
Name、Descriptionを設定します。
TypeはSimpleにしています。必須項目がGUI上で設定できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/ff0e7db1-9049-db1e-81e8-793df0f29a56.png)

Define Activities上にあるPropertiesのEditをクリックして引数を追加します。
ImageとPositionは後ほどコード上で独自のEnum型を指定するためString型のままにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/4b2e269f-bc3a-4725-62dc-56da8c1c09a7.png)



### 機能の実装

#### アセンブリ参照の追加

アセンブリ参照より、System.Windows.Formsを追加します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/39e4c8f2-a790-a175-1de2-bf5d660840c6.png)

#### リソースの追加

使用する画像をリソースに追加します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/b108044a-8d14-1cfb-444a-5626c63c77da.png)


#### コンボボックスに表示するパラメータの追加

コンボボックスで使用するパラメータを定義します
PresentationRobot\Enumsの下にPositionType.csとImageType.csを追加して次のように定義します

PositionType.cs
```C#
using System.ComponentModel;
namespace PresentationRobot.Enums
{
    public enum PositionType
    {
        [Description("Top Left")]
        TopLeft,
        [Description("Top Right")]
        TopRight,
        [Description("Down Left")]
        DownLeft,
        [Description("Down Right")]
        DownRight
    }
}
```

ImageType.cs
```C#
using System.ComponentModel;
namespace PresentationRobot.Enums
{
    public enum ImageType
    {
        [Description("Flying Robo")]
        Flying Robot,
        [Description("Processing Robot")]
        Processing Robot,
        [Description("Listening Robot")]
        Listening Robot,
        [Description("Recorder Robot")]
        Recorder Robot,
        [Description("Searching Robot")]
        Searching Robot,
        [Description("Receiving Robot")]
        Receiving Robot
    }
}
```

#### 参照とプロパティの修正

定義したEnumとFormを使用するため追加しています
EnumやBooleanで定義するとプロパティ上でチェックボックスやコンボボックスとして表示されます。


Presentation.cs 
```C#
using System;
using System.Activities;
using System.Threading;
using System.Threading.Tasks;
using System.Drawing;
using ManabuTech.Activities.Properties;
using UiPath.Shared.Activities;
using UiPath.Shared.Activities.Localization;

// Add
using PresentationRobot.Enums;
using System.ComponentModel;
using System.Windows.Forms; // Form アセンブリ参照の追加が必要

~~~
        #region Properties

        /// <summary>
        /// If set, continue executing the remaining activities even if the current activity has failed.
        /// </summary>
        [LocalizedCategory(nameof(Resources.Common_Category))]
        [LocalizedDisplayName(nameof(Resources.ContinueOnError_DisplayName))]
        [LocalizedDescription(nameof(Resources.ContinueOnError_Description))]
        public override InArgument<bool> ContinueOnError { get; set; }

        [LocalizedDisplayName(nameof(Resources.DisplayMessage_Message_DisplayName))]
        [LocalizedDescription(nameof(Resources.DisplayMessage_Message_Description))]
        [LocalizedCategory(nameof(Resources.Input_Category))]
        [DefaultValue("ロボット実行中")]
        public InArgument<string> Message { get; set; } = "ロボット実行中";

        [LocalizedDisplayName(nameof(Resources.DisplayMessage_FontSize_DisplayName))]
        [LocalizedDescription(nameof(Resources.DisplayMessage_FontSize_Description))]
        [LocalizedCategory(nameof(Resources.Input_Category))]
        [DefaultValue(50)]
        public InArgument<int> FontSize { get; set; } = 50;

        [LocalizedDisplayName(nameof(Resources.DisplayMessage_TextColor_DisplayName))]
        [LocalizedDescription(nameof(Resources.DisplayMessage_TextColor_Description))]
        [LocalizedCategory(nameof(Resources.Input_Category))]
        [DefaultValue(typeof(Color), "White")]
        public InArgument<Color> TextColor { get; set; } = Color.White;

        [LocalizedDisplayName(nameof(Resources.DisplayMessage_BackgroundColor_DisplayName))]
        [LocalizedDescription(nameof(Resources.DisplayMessage_BackgroundColor_Description))]
        [LocalizedCategory(nameof(Resources.Input_Category))]
        [DefaultValue(typeof(Color), "Black")]
        public InArgument<Color> BackgroundColor { get; set; } = Color.Black;

        [LocalizedDisplayName(nameof(Resources.DisplayMessage_BackgroundOpacity_DisplayName))]
        [LocalizedDescription(nameof(Resources.DisplayMessage_BackgroundOpacity_Description))]
        [LocalizedCategory(nameof(Resources.Input_Category))]
        [DefaultValue(0.9)]
        public InArgument<Double> BackgroundOpacity { get; set; } = 0.9;

        [LocalizedDisplayName(nameof(Resources.DisplayMessage_Image_DisplayName))]
        [LocalizedDescription(nameof(Resources.DisplayMessage_Image_Description))]
        [LocalizedCategory(nameof(Resources.Input_Category))]
        public ImageType Image { get; set; }

        [LocalizedDisplayName(nameof(Resources.DisplayMessage_Position_DisplayName))]
        [LocalizedDescription(nameof(Resources.DisplayMessage_Position_Description))]
        [LocalizedCategory(nameof(Resources.Input_Category))]
        public PositionType Position { get; set; }

        [LocalizedDisplayName(nameof(Resources.DisplayMessage_IsFullWidth_DisplayName))]
        [LocalizedDescription(nameof(Resources.DisplayMessage_IsFullWidth_Description))]
        [LocalizedCategory(nameof(Resources.Input_Category))]
        public Boolean IsFullWidth { get; set; }

        #endregion
```



#### 機能の実装

フォントサイズからフォームのサイズを指定しています。

```C#
        protected override async Task<Action<AsyncCodeActivityContext>> ExecuteAsync(AsyncCodeActivityContext context, CancellationToken cancellationToken)
        {
            // Inputs
            var message = Message.Get(context);
            var fontsize = FontSize.Get(context);
            var textcolor = TextColor.Get(context);
            var backgroundcolor = BackgroundColor.Get(context);
            var backgroundopacity = BackgroundOpacity.Get(context);
            //var image = Image.Get(context);
            //var position = Position.Get(context);
            //var isfullwidth = IsFullWidth.Get(context);
            var position = Position;
            var image = Image;
            var isfullwidth = IsFullWidth;

            ///////////////////////////
            // Add execution logic HERE
            ///////////////////////////

            // Add From --->

            var label = new Label
            {
                Font = new Font("Arial", fontsize, FontStyle.Bold),
                TextAlign = ContentAlignment.MiddleLeft,
                Text = message,
                AutoSize = true,
                ForeColor = textcolor
            };

            var pictureBox = new PictureBox
            {
                Width = label.PreferredHeight,
                Height = label.PreferredHeight
            };

            label.Left = pictureBox.Width + 10;
            label.Top = 10;
            pictureBox.Left = 10;

            //画像を表示する
            switch (image)
            {
                default:
                case ImageType.FlyingRobot:
                    pictureBox.Image = Properties.Resources.Flying_Robot;
                    break;
                case ImageType.ListeningRobot:
                    pictureBox.Image = Properties.Resources.Listening_Robot;
                    break;
                case ImageType.ProcessingRobot:
                    pictureBox.Image = Properties.Resources.Processing_Robot;
                    break;
                case ImageType.RecorderRobot:
                    pictureBox.Image = Properties.Resources.Recorder_Robot;
                    break;
                case ImageType.SearchingRobot:
                    pictureBox.Image = Properties.Resources.Searching_Robot;
                    break;
                case ImageType.ReceivingRobot:
                    pictureBox.Image = Properties.Resources.Receiving_Robot;
                    break;
            }

            //画像の大きさをPictureBoxに合わせる
            pictureBox.SizeMode = PictureBoxSizeMode.StretchImage;

            var form = new Form
            {
                Width = pictureBox.Left + pictureBox.Width + label.PreferredWidth + 20,
                Height = label.PreferredHeight + 10,
                BackColor = backgroundcolor,
                Opacity = backgroundopacity,
                FormBorderStyle = FormBorderStyle.None,
                TopMost = true
            };

            form.Show();

            switch (position)
            {
                case PositionType.DownLeft:
                    form.Left = 0;
                    form.Top = Screen.PrimaryScreen.WorkingArea.Height - form.Height;
                    break;
                case PositionType.DownRight:
                    form.Left = Screen.PrimaryScreen.WorkingArea.Width - form.Width;
                    form.Top = Screen.PrimaryScreen.WorkingArea.Height - form.Height;
                    break;
                case PositionType.TopLeft:
                    form.Left = 0;
                    form.Top = 0;
                    break;
                case PositionType.TopRight:
                    form.Left = Screen.PrimaryScreen.WorkingArea.Width - form.Width;
                    form.Top = 0;
                    break;
            }
            
            if (isfullwidth)
            {
                form.Left = 0;
                form.Width = Screen.PrimaryScreen.WorkingArea.Width;
            }

            form.Controls.Add(pictureBox);
            form.Controls.Add(label);
            form.Update();

            // デバッグ時、有効にする
            //System.Threading.Thread.Sleep(3000);
            //form.Close();

            // Add To <---

            // Outputs
            return (ctx) => {
            };
        }
```

#### Designerの修正

PresentationDesigner.xamlから不要な部分を削除します
今回はMessageのみをGUIに表示するようにしますので、Message以外を削除します。
```
            <Label Content="{x:Static activity:Resources.DisplayMessage_Message_DisplayName}" Grid.Row="0"
                   ToolTip="{x:Static activity:Resources.DisplayMessage_Message_Description}"/>
            <sapv:ExpressionTextBox Grid.Row="1" ExpressionType="{Binding ModelItem.Properties[Message].PropertyType.GenericTypeArguments[0]}" OwnerActivity="{Binding Path=ModelItem}" HintText="{x:Static activity:Resources.DisplayMessage_Message_Description}" UseLocationExpression="False">
                <sapv:ExpressionTextBox.Expression>
                    <Binding Path="ModelItem.Message" Converter="{StaticResource ArgumentToExpressionConverter}" Mode="TwoWay" ConverterParameter="In" UpdateSourceTrigger="PropertyChanged" />
                </sapv:ExpressionTextBox.Expression>
            </sapv:ExpressionTextBox>

        </Grid>
```

### パッケージのビルド

Activities.Designプロジェクトを選択してビルドから発行をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/b241cef8-e71c-065d-8e45-48f5f9f0f75b.png)


発行プロファイルの追加から、フォルダを選択して次へを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/5aa826cd-2ef3-3830-b2c1-5f24981bf1e8.png)

必須コンポーネントが必要な場合はインストールしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/448426fe-a509-ddee-9bf8-c7493be9f69d.png)

フォルダーの場所を指定して完了をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/be85ac92-7f26-146d-3b26-39762dea1d16.png)

Debugを選択して発行で指定フォルダにnupkgが作成されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/2058041c-c280-b457-9188-597e73fad43c.png)

### UiPath Studioで使用

パッケージ管理フォルダを指定
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/c1165a28-37a2-fe89-6908-01ddaa5ebebc.png)

作成したパッケージをインストールして保存
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/3a5646cf-7635-b81a-6061-80f7f0c104c6.png)


同意するを選択
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2577004/f6e5166e-e3c1-d037-1183-855144f690eb.png)


## 最後に

いかがでしたでしょうか？
UiPath Studioで作成したワークフローをデモする際には、ぜひこのアクティビティを使用してみてください。
余力があれば、画像を差し替えてカスタマイズしてみてはいかがでしょうか？