Dynamo・PyRevitでGUIをつくる

# 目次
[__1. はじめに__](#はじめに)
[__2. rpw.ui.forms.__](#-rpwuiforms)
[__3. Revit.UI.__](#-revitui)
[__4. Matplotlib__](#-matplotlib)
[__5. WPF__](#-wpf)
[__6. WinForms__](#-WinForms)
[__7. Tkinter（使えない）__](#-tkinterpyrevitでは使えない)
[__8. PySimpleGUI（使えない）__](#-pysimpleguipyrevitでは使えない)
[__9. 参考リンク__](#参考リンク)



# はじめに
DynamoのPythonスクリプトやpyRevitでGUIをつくる方法を紹介します。本記事のコードはDynamo2.12(Revit2022)で動作確認しています。



# rpw.ui.forms.
PyRevitには __RPW（Revit Python Wrapper）__ というライブラリがデフォルトで搭載されており、これを使ってシンプルなGUIを簡単に作成できます。WPFを利用して実装されているため **ironPython** のみで動作します。DynamoのPythonスクリプトでも使用できます。

Dynamoの場合はパッケージをインストールしてください。

https://qiita.com/Z0E/items/2ad20959f0e44eecc401


### SelectFromList
辞書もしくはリストからドロップダウンリストを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/54af8881-c861-8505-e4e7-776dff051760.png)
```python
from rpw.ui.forms import SelectFromList
value = SelectFromList('タイトル', ['1','2','3'])
```

### TextInput
テキスト入力フォームを作成します。（改行などには対応していません。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/16cf7ba8-1240-e9ab-6129-bd68569ed276.png)
```python
from rpw.ui.forms import TextInput
value = TextInput('タイトル', default='デフォルトの値')
```

### TaskDialog
カスタマイズ可能なダイアログを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/9fbbf933-d64a-4321-0e44-a4e492f47f90.png)

```python
from rpw.ui.forms import CommandLink, TaskDialog
dialog = TaskDialog('メインテキスト',
            title='タイトル',
            title_prefix=False,
            content='サブテキスト',
            commands=[
                CommandLink('選択肢1', return_value='A'),
                CommandLink('選択肢2', return_value='B'),
                CommandLink('選択肢3', return_value='C'),
                CommandLink('選択肢4', return_value='D')],
            buttons=['None', 'Ok', 'Yes', 'No', 'Cancel', 'Retry', 'Close'],
            footer='フッターテキスト',
            verification_text='チェックボックス',
            expanded_content='詳細テキスト', 
            show_close=True)
dialog.show()
```

### Alert
メッセージダイアログを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/b711aebe-2a1c-fcc7-1f77-8afc9354c5eb.png)
```python
from rpw.ui.forms import Alert
Alert('サブテキスト', title='タイトル', header='メインテキスト', exit=True)
```

### Select Folder
フォルダ選択ダイアログを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/8d64f7fe-8b36-8a9c-467e-57cd888fc039.png)
```python
from rpw.ui.forms import select_folder
folderpath = select_folder()
```

### Select File
ファイル選択ダイアログを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/dccf03be-45f3-3b77-44a5-8f3003f5ae08.png)
```python
from rpw.ui.forms import select_file
filepath = select_file('プロジェクト ファイル (*.rvt, *.rte)|*.rvt, *.rte')
# select_file('<テキスト>|<ファイル形式>')
```

### Console
コンソールを表示します。
※Dynamoでは動作しないようです。

### FlexForm
ドロップダウンリスト・チェックボックス・ボタンなどを組み合わせてダイアログを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/ac71adf2-61d8-a23d-5f8f-88039e1a8d40.png)
```python
from rpw.ui.forms import *
dialog = FlexForm('タイトル',
        [Label('テキスト:'),
        ComboBox('combobox1', {'選択肢1': 10, '選択肢2': 20, '選択肢2': 30}),
        TextBox('textbox1', Text="テキストボックス"),
        CheckBox('checkbox1', 'チェックボックス'),
        Separator(),
        Button('ボタン')])
dialog.show()
```

# Revit.UI.
RevitAPIに標準搭載されているダイアログを使用する方法です。pyRevitやDynamoに標準搭載されています。

### TaskDialog
__rpw.ui.forms.TaskDialog__ と同様にカスタマイズ可能なダイアログを作成します。こちらはプログレスバーやアイコンも表示できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/416f4b42-ca65-8d88-d71f-c0b718b1ab2b.png)

```python
import clr
clr.AddReference("RevitAPIUI")
from  Autodesk.Revit.UI import *

dialog = TaskDialog("タイトル")
dialog.MainIcon = TaskDialogIcon.TaskDialogIconShield
dialog.MainInstruction = "メインテキスト"
dialog.MainContent = "サブテキスト"
dialog.AddCommandLink(TaskDialogCommandLinkId.CommandLink1, "選択肢1")
dialog.AddCommandLink(TaskDialogCommandLinkId.CommandLink2, "選択肢2")
dialog.AddCommandLink(TaskDialogCommandLinkId.CommandLink3, "選択肢3")
dialog.AddCommandLink(TaskDialogCommandLinkId.CommandLink4, "選択肢4")
dialog.CommonButtons = TaskDialogCommonButtons.Ok|TaskDialogCommonButtons.Yes|TaskDialogCommonButtons.No|TaskDialogCommonButtons.Cancel|TaskDialogCommonButtons.Retry|TaskDialogCommonButtons.Close
dialog.DefaultButton = TaskDialogResult.Ok
dialog.EnableMarqueeProgressBar = True
dialog.FooterText = "フッターテキスト"
dialog.ExtraCheckBoxText = "チェックボックス"
dialog.ExpandedContent = "詳細テキスト"

res = dialog.Show()
```
表示できるアイコンは以下の4種類です。
|||
|:--|:--|
|TaskDialogIconShield |![20190415130451.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/740c27fe-94f2-7a73-34a0-dc66cca9388b.png)
|TaskDialogIconInformation|![20190415130712.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/0b1464a5-f098-2853-fb0a-89020d141329.png)
|TaskDialogIconError|![20190415130723.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/7c994d89-ea6a-db77-d687-30da64c39ad5.png)
|TaskDialogIconWarning|![20190415130731.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/44b3462b-721e-bb40-26b9-b107e07c68cd.png)


### FileOpenDialog
__rpw.ui.forms.Select File__ と同様にファイル選択ダイアログを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/c65e879f-540d-1475-9173-ae37da21363c.png)
```python
import clr
clr.AddReference("RevitAPIUI")
from  Autodesk.Revit.UI import *

dialog = FileOpenDialog("プロジェクト ファイル (*.rvt, *.rte)|*.rvt, *.rte")
res = dialog.Show()
path  = ModelPathUtils.ConvertModelPathToUserVisiblePath(dialog.GetSelectedModelPath())
```

### FileSaveDialog
ファイル保存ダイアログを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/24c88962-4a6a-d8b8-e9d4-b8569763abdb.png)
```python
import clr
clr.AddReference("RevitAPIUI")
from  Autodesk.Revit.UI import *

dialog = FileSaveDialog("プロジェクト ファイル (*.rvt, *.rte)|*.rvt, *.rte")
dialog.Show()
```

### FilterDialog
フィルタダイアログを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/0a8c0028-a8bf-dd9a-20a7-342b3bddb48f.png)
```python
import clr
clr.AddReference("RevitAPIUI")
from  Autodesk.Revit.UI import *

clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager
doc = DocumentManager.Instance.CurrentDBDocument

dialog = FilterDialog(doc, "フィルタ");
dialog.Show();
param_filter = doc.GetElement(dialog.NewFilterId)
filter = param_filter.GetElementFilter()
```

### ColorSelectionDialog
色選択ダイアログを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/e63c06b5-0af1-8734-996e-fc2a5d90f995.png)
```python
import clr
clr.AddReference("RevitAPIUI")
from  Autodesk.Revit.UI import *

clr.AddReference("RevitServices")
import RevitServices
from RevitServices.Persistence import DocumentManager

dialog = ColorSelectionDialog()
dialog.Show()
selected = dialog.SelectedColor
```



# Matplotlib
__Matplotlib__ はPythonのグラフ描画のためのライブラリで、グラフの描画やデータの可視化が簡単に行えます。折れ線グラフ、ヒストグラムや散布図など表現可能です。

Matplotlib は標準ライブラリではないため自分でインストールする必要があります。下記記事の「ライブラリのインストール」を参考にしてください。

https://qiita.com/Z0E/items/7931d87c3e357d87e0d2#%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB

Matplotlib の使い方は下記記事を参考にしてください。

https://note.nkmk.me/matplotlib/

### グラフを表示する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/871e3e2a-ab32-abe6-0dcb-63f9e5755075.png)
```python
import sys
sys.path.append(r'C:\Users\<ユーザー名>\.conda\envs\Dynamo383\Lib\site-packages')
import numpy as np
import matplotlib.pyplot as plt

x = np.arange(-5, 5, 0.1)
y = np.sin(x)
plt.plot(x,y)
plt.show()
```


### Watch Imageノードで表示する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/2e1f399e-380b-2b25-cc82-7884f080119d.png)
```python
import sys
sys.path.append(r'C:\Users\<ユーザー名>\.conda\envs\Dynamo383\Lib\site-packages')
import io
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image as Img
import System.Drawing
from System.Drawing import *
from System.Drawing.Imaging import *
from System.IO import MemoryStream

def convertToBitmap(fig):
    rgba_buf = fig.canvas.buffer_rgba()
    (w,h) = fig.canvas.get_width_height()
    npImgArray = np.frombuffer(rgba_buf, dtype=np.uint8).reshape((h,w,4))   
    bitmap_ = None
    # alphaチャンネルを削除
    if npImgArray.ndim == 3 and npImgArray.shape[-1] == 4:
        npImgArray = npImgArray[:, :, :-1]
    img = Img.fromarray(npImgArray, "RGB")
    # 画像をバイナリデータに変換し、メモリ上に保存
    byteIO = io.BytesIO()
    img.save(byteIO, format='BMP')
    byteArr = byteIO.getvalue()
    # ByteArrayに変換して出力
    netBytes = System.Array[System.Byte](byteArr)
    with MemoryStream(netBytes) as ms:
        bitmap_ = Bitmap(ms)
    return bitmap_

fig = plt.figure(figsize=(10,10))

ax1 = fig.add_subplot(2,1,1)
x = np.arange(-5, 5, 0.1)
y = np.sin(x)
ax1.plot(x, y) 
ax1.grid(True)

ax2 = fig.add_subplot(2,1,2)
a = range(0, 7)
b = [55,21,61,98,85,52,99]
ax2.bar(a, b) 
ax2.grid(True)

fig.canvas.draw()
OUT = convertToBitmap(fig)
```



# WPF
__WPF__ は.NET Frameworkで利用できるGUIを開発するための機能で、**ironPython**のみで動きます。他の手法よりもリッチな（カッコイイ＆高機能な）GUIを作り込むことができます。また、後々にアドインをC#で再実装する場合にはWPFを使いまわすことができます。
C#.NETでの開発経験がある人にオススメです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/e1779740-465d-af45-90a5-48bddcd142a7.png)

WPFアプリケーションでは、ロジックと視覚的な部分を分離して、XAML（Extensible Application Markup Language）」と呼ばれるマークアップ言語で記述します。

```python
import clr
clr.AddReference("PresentationFramework")
clr.AddReference("PresentationCore")

from System.Windows import Application
from System.Windows.Markup import XamlReader
 
xaml = """<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xml:lang="ja-JP"
    Title="タイトル"
    Width="450"
    SizeToContent="Height">
    <Grid Margin="15" x:Name="grid1">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Label Grid.Row="0" Grid.Column="0" FontSize="24" Content="テキスト1:" />
        <Label Grid.Row="0" Grid.Column="1" FontSize="24" Content="バインディング1" />
        <Label Grid.Row="1" Grid.Column="0" FontSize="24" Content="テキスト2:" />
        <Label Grid.Row="1" Grid.Column="1" FontSize="24" Content="バインディング2" />
        <Button Grid.Row="2" FontSize="24" Content="ボタン"/>
    </Grid>
</Window>"""
 
w = XamlReader.Parse(xaml)
Application().Run(w)
```



# WinForms
__WinForms__ は.NET Frameworkで利用できるGUIを開発するための機能です。
C#.NETでの開発経験がある人にオススメです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2059946/1e711a2d-c33b-813c-c659-088815029d79.png)

```python
import clr
clr.AddReference('System.Windows.Forms')
clr.AddReference('System.Drawing')

import System.Drawing
import System.Windows.Forms
from System.Drawing import *
from System.Windows.Forms import *

class MainForm(System.Windows.Forms.Form):
    def __init__(self):
        self.InitializeComponent()

    def InitializeComponent(self):
        self._progressBar1 = System.Windows.Forms.ProgressBar()
        self._label1 = System.Windows.Forms.Label()
        self._button1 = System.Windows.Forms.Button()
        self._button2 = System.Windows.Forms.Button()
        self.SuspendLayout()

        # progressBar1
        self._progressBar1.Location = System.Drawing.Point(15, 50)
        self._progressBar1.Name = "progressBar1"
        self._progressBar1.Size = System.Drawing.Size(370, 20)
        self._progressBar1.Minimum = 0     #最小値を指定
        self._progressBar1.Maximum = 3   #最大値を指定

        # label1
        self._label1.Location = System.Drawing.Point(18, 20)
        self._label1.Name = "label1"
        self._label1.Size = System.Drawing.Size(300, 23)
        self._label1.TabIndex = 1
        self._label1.Text = "「実行」をクリックして処理を開始します。"
        
        # button1
        self._button1.Location = System.Drawing.Point(210, 80)
        self._button1.Name = "button1"
        self._button1.Size = System.Drawing.Size(80, 25)
        self._button1.TabIndex = 1
        self._button1.Text = "実行"
        self._button1.UseVisualStyleBackColor = True
        self._button1.Click += self.Button1Click
        
        # button2
        self._button2.Location = System.Drawing.Point(305, 80)
        self._button2.Name = "button2"
        self._button2.Size = System.Drawing.Size(80, 25)
        self._button2.TabIndex = 1
        self._button2.Text = "キャンセル"
        self._button2.UseVisualStyleBackColor = True
        self._button2.Click += self.Button2Click
        
        # MainForm
        self.ClientSize = System.Drawing.Size(400, 120)
        self.Controls.Add(self._label1)
        self.Controls.Add(self._button1)
        self.Controls.Add(self._button2)
        self.Controls.Add(self._progressBar1)
        self.Name = "MainForm"
        self.Text = "タイトル"
        self.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen
        self.ResumeLayout(False)
        self.PerformLayout()
    
    def update(self, text, progress_value):
        self._label1.Text = text
        self._label1.Update()
        if progress_value <= 3:
            self._progressBar1.Value = progress_value
            self._progressBar1.Update()
        
    def Button1Click(self, sender, e):
        
        self.update("処理1を実行中...", 1)
        <重い処理 1>

        self.update("処理2を実行中...", 2)
        <重い処理 2>

        self.update("処理3を実行中...", 2)
        <重い処理 3>
        
        self.update("処理が完了しました。", 7)
        self.Close()
        
    def Button2Click(self, sender, e):
        self.Close()
        
System.Windows.Forms.Application.EnableVisualStyles()
form = MainForm()
System.Windows.Forms.Application.Run(form)
```



# Tkinter（使えない）
Python標準のGUIライブラリであるTkinterは、DynamoやpyRevitで扱うことができません。Tkinterは**ironPython**と**CPython**のどちらにも含まれていません。

:::note warn
pyRevitに搭載されているPythonエンジンには、**ironPython**（C#で実装されたPython、Python2）と**CPython**（Cで実装されたPython、Python3）があります。この**CPython**は通常のものと異なる**組み込み Python**（Python Embeddable）というもので、Numpy など一部の外部ライブラリが使えますが、GUI関連のライブラリでは使えないモノもあります。
:::



# PySimpleGUI（使えない）
PySimpleGUIもDynamoやpyRevitで扱うことができません。PySimpleGUI内部でTkinterが用いられているためです。



# 参考リンク

https://revitpythonwrapper.readthedocs.io/en/latest/ui/forms.html

https://www.revitapidocs.com/2022/e86fd90a-8957-02a6-da7f-ced248966e3e.htm

https://e-words.jp/w/WPF.html

https://thebuildingcoder.typepad.com/blog/2020/01/revit-wpf-add-ins-and-template.html