C#ATIA

↑タイトル詐欺 主にFusion360API 偶にCATIA V5 VBA(絶賛ネタ切れ中)

ダイアログなスクリプト入門1

モチベーションも奮い立たせる為、Fusion360API入門者向けコンテンツを
再開する事にしました。

ダイアログを利用したスクリプトを作成する際、ドキュメントにあまり良い
サンプルが無く、加えてイベント処理に関しては、それらしい説明が無さそうな為
それらをテーマにしたもので進めていこうかと思ってます。
(当方C++の知識が無い為Pythonです)

今回のゴール

いきなりですが、ゴールです。
取りあえず、こんな状態のダイアログが表示されるスクリプトを用意しました。
f:id:kandennti:20220107172837p:plain
念の為、OKボタンを押しても何もしません。
全体的なコードはこちらです。

# Fusion360API Python script

import traceback
import adsk.fusion
import adsk.core

# 頻繁に使うため、2個をグローバル変数としています。
_app: adsk.core.Application = None
_ui: adsk.core.UserInterface = None

# スクリプトが終了するまで、各イベントハンドラを保持用のリスト。
# こちらにハンドラを代入する事でGCに回収されることを防いでいる。
# 逆に書けば、突っ込んでおしまい。
_handlers = []


# スクリプトのエントリーポイント。
def run(context):
    try:
        # グローバルな変数
        global _app, _ui
        _app = adsk.core.Application.get()
        _ui = _app.userInterface

        # 既に同じIDのコマンドが定義されていないか調べる為、
        # IDを利用してCommandDefinitionを取得
        cmdDef: adsk.core.CommandDefinition = _ui.commandDefinitions.itemById(
            'test_cmd_id'
        )

        # 同じIDのコマンドが定義されていない場合、CommandDefinitionを新作する。
        if not cmdDef:
            cmdDef = _ui.commandDefinitions.addButtonDefinition(
                'test_cmd_id',
                'ダイアログです',
                'ツールチップです'
            )

        # コマンドを作成する為、イベントハンドラーを作成し
        # commandCreatedイベントに登録する。
        global _handlers
        onCommandCreated = MyCommandCreatedHandler()
        cmdDef.commandCreated.add(onCommandCreated)
        _handlers.append(onCommandCreated)

        # CommandDefinition実行する。
        # 基本的に、これによりcommandCreatedイベントが発火する。
        # と思っていたのですが、run関数終了後に発火します。
        cmdDef.execute()

        # コマンドが自動的に終了しないようにする。
        # ここをTrueとすると、commandCreatedイベントが発火せず
        # スクリプト自体が即終了する。
        adsk.autoTerminate(False)

    except:
        if _ui:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


# コマンドを作る為、細かな定義を行う。
# このハンドラのnotifyメソッドが終了するまでは、コマンドの作成が完了していません。
# その為、他のイベントで利用出来る処理が出来ない反面、他のイベントでは出来ない
# 処理を行う事が可能です。その一つはダイアログの設定です。
class MyCommandCreatedHandler(adsk.core.CommandCreatedEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.CommandCreatedEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)
        try:
            global _handlers

            # コマンドの取得
            cmd: adsk.core.Command = adsk.core.Command.cast(args.command)

            # --- イベント ---
            # コマンド終了時のイベントの登録
            onDestroy = MyCommandDestroyHandler()
            cmd.destroy.add(onDestroy)
            _handlers.append(onDestroy)

            # ダイアログのOKボタンを押した際のイベントの登録
            onExecute = MyExecuteHandler()
            cmd.execute.add(onExecute)
            _handlers.append(onExecute)

            # --- ダイアログのデザイン(CommandInputs) ---
            # コマンドのダイアログの土台の様なもの
            inputs: adsk.core.CommandInputs = cmd.commandInputs

            # テキストボックスインプット
            txtIpt: adsk.core.TextBoxCommandInput = inputs.addTextBoxCommandInput(
                'txtIpt',
                'テキストボックス',
                'Hello World',
                1,
                True
            )

        except:
            _ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))


# ダイアログのOKボタンを押した際のイベントハンドラ
class MyExecuteHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.CommandEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)
        # 本来はここで目的の処理を行います。
        # 今回は特に何も行いません。


# コマンド終了時のイベントハンドラ
class MyCommandDestroyHandler(adsk.core.CommandEventHandler):
    def __init__(self):
        super().__init__()

    def notify(self, args: adsk.core.CommandEventArgs):
        adsk.core.Application.get().log(args.firingEvent.name)

        # スクリプトを終了させる
        # こちらを行わないと、スクリプトが実行中の状態となります。
        # 但し、それによる弊害は良く分かっていません。
        adsk.terminate()

最低限+αぐらいのボリュームです。単に処理するだけのスクリプトとは
異なり、ソコソコ長くなります。
コメントをグダグダ付けていますが、これを元にご説明しましょう。

エントリーポイント(run関数)

単純なスクリプトと同様で、エントリーポイントはrun関数となります。
但し、大きく異なる点が一つあります。
CommandDefinitionのインスタンスを作成します。これは独自のコマンドを作成する事を
意味します。

cmdDef = _ui.commandDefinitions.addButtonDefinition(
    'test_cmd_id',
    'ダイアログです',
    'ツールチップです'
)

CommandDefinitionのインスタンスの作成は、一般的なpythonのオブジェクトの
作成方法とは異なり、該当するコレクションに用意されたメソッドを使用する事が
ほとんどです。
今回の場合であればaddButtonDefinitionメソッドが該当します。
Fusion 360 Help
今回は "resourceFolder" を省略しています。これはアイコンを利用したい場合に
指定する必要があります。アドインの場合は必要性を感じますが、スクリプトの場合は
必要無いと思います。


続いて、commandCreatedイベントにイベントハンドラーを登録する必要があります。
CommandDefinitionのインスタンスを作成だけでは、ダイアログを作成する事が
出来ません。

その為、CommandDefinitionオブジェクトの唯一のイベント "commandCreated" に
イベントハンドラーを登録します。

global _handlers
onCommandCreated = MyCommandCreatedHandler()
cmdDef.commandCreated.add(onCommandCreated)
_handlers.append(onCommandCreated)

イベントハンドラーとなる "MyCommandCreatedHandler" オブジェクトは一般的な
インスタンスの作り方になります。
単にcommandCreatedに追加するだけではNGで、グローバルな変数にインスタンス
代入しておく必要が有ります。
これはスコープが外れてしまう事によりガベージコレクション(GC)に回収されることを
防ぐためです。(恐らく・・・)
この辺りは、ドキュメントの例文で記述されている通りで問題無いはずです。
Fusion 360 Help


残りは、executeメソッドを呼び出し、autoTerminate関数で即終了しないようにします。

cmdDef.execute()
adsk.autoTerminate(False)

よほど複雑な事を行う場合を除いて、run関数に関しては、ほぼ雛形だと思って頂いて
構わないと思います。唯一の注意点はaddButtonDefinitionメソッドのパラメータで
他のコマンドと重複しないコマンドIDを指定することぐらいです。

CommandCreatedイベントハンドラー(MyCommandCreatedHandler)

ここではコマンドのイベントやダイアログの設定等、コマンドの詳細を設定します。

コマンドのイベントですが、今回はdestroyとexecuteだけを利用しています。

onDestroy = MyCommandDestroyHandler()
cmd.destroy.add(onDestroy)
_handlers.append(onDestroy)

onExecute = MyExecuteHandler()
cmd.execute.add(onExecute)
_handlers.append(onExecute)

executeイベントはOKボタンが押された場合に呼び出されるハンドラーを代入します。
Fusion 360 Help

destroyイベントはコマンドの終了直前に呼び出されるハンドラーを代入します。
Fusion 360 Help
こちらはOK・キャンセルどちらのボタンが押されても呼び出されます。

イベントに関しては、commandCreatedイベント同様でドキュメントの例文で
記述されている通りで問題無いはずです。
※各イベントは、リストとして保持されるため、複数のハンドラを代入する事が可能です。


続いてダイアログのUI設定です。APIとして提供されているCommand Inputsを
配置する事になります。
予めお伝えしておくと、Command Inputsが一個も無い(OK・キャンセルボタンのみ)
ダイアログは作成する事が出来ません。

今回は無難に読み込み専用のTextBoxCommandInputのみを配置しています。

txtIpt: adsk.core.TextBoxCommandInput = inputs.addTextBoxCommandInput(
    'txtIpt',
    'テキストボックス',
    'Hello World',
    1,
    True
)

Fusion 360 Help

他にも複数のCommand Inputsが用意されております。
https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-8B9041D5-75CC-4515-B4BB-4CF2CD5BC359
まぁこれでも足りないのですが、大体は事足ります。

各Command InputsもインスタンスはCommandInputsオブジェクトのadd~メソッドを
利用します。
通常1個のCommand Inputsだけでは済まないはずですね。コンテナっぽいもの
(TabCommandInput等)を除いて、基本的にadd~メソッドを行った順にダイアログの
上から配置されます。・・・レイアウトの自由度は少ないです。


CommandCreatedイベントは特別です。CommandCreatedイベントが終了するまでは
コマンドが作成されていません。逆にCommandCreatedイベントでしか出来ない処理が
あります。
その一つはCommand Inputsのインスタンスの作成は他のイベントでは出来ません。
仮に何かのCommand Inputsを途中で表示させたい場合(BoolValueCommandInputを
ONにするとSelectionCommandInputが表示される等)は、予めCommandCreatedイベント時に
作成しておき非表示にしておく必要が有ります。 兎に角特別なイベントです。

executeイベントハンドラー(MyExecuteHandler)

こちらはOKボタンを押した場合に呼び出されます。
今回は何か処理を行うわけでは無いため特に記述していませんが、本来はこの位置に
実行したい処理を書いたり、呼び出したりをする部分です。

destroyイベントハンドラー(MyCommandDestroyHandler)

こちらはOK/キャンセルボタンのどちらを押しても、スクリプト終了直前に呼び出されます。
本来であれば、何らかを破棄したり(アドインであればボタン等)するのですが、
こちらも今回は特に処理する必要が無いため、一行のみ記載しています。

adsk.terminate()

これを実行しなければ、スクリプトが完全には終了しないようです。

イベントの呼び出し

今回は全てのイベントハンドラには

adsk.core.Application.get().log(args.firingEvent.name)

を記載しました。
これにより、実際に操作を行いながらイベントの呼び出しが確認できます。
出力先はテキストコマンドのパレット部分です。
f:id:kandennti:20220107173724p:plain
もし、テキストコマンドのパレットが表示されていない場合は、こちらから表示
させてください。
f:id:kandennti:20220107173742p:plain

OKボタンを押した場合とキャンセルボタンを押した場合で、出力される文字が
異なりますし、呼び出されるイベントの順番も確認出来ると思います。

結び

と、まぁ興味が湧くような内容が無い、薄い内容となりました。
個人的に "使いにくいな" と感じる部分が有ったり、有志の方が便利なフレーム
ワーク(apperやその他2個ぐらいあります)を公開されていますが、
Fusion360API的に標準的な記述方法は上記のようなスタイルです。
特にフレームワークを使ってしまうと、仕組みの様な部分が隠されてしまい
あまり知識が身に付かない印象を受けました。

取りあえずこれをベースに今後はお話を進めましょう。