1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Autodesk Inventor API Hacking (NET環境での別スレッドUI)

Last updated at Posted at 2024-11-07

0. はじめに

Freeradicalの中の人、yamarahです。
自前ダイアログで条件を設定し、Inventor自体に重い処理をさせる場合に、キャンセルボタンが欲しいですよね。
完全にシングルスレッドにしてしまうと、重い処理を実行中にUIが操作できなくなります。何らかの方法でマルチスレッドにするしかないのですが、どのようにすれば・・・というお話です。

1. 前置き

1.1. 正攻法

通常のWPFアプリケーションでは、メインスレッドたるUIスレッドは瞬時リターンが原則で、時間のかかる処理はasync, awaitを使って別スレッドに委託します。
今回の時間がかかる処理は、Inventorが処理します。すると、Inventorが考え込む = UIスレッドが止まる、ためにキャンセルボタンが押せなくなります。

1.2. 方針

Inventorは構造解析などの例外を除きシングルスレッドなので、Inventorに重い処理をさせている間はUIスレッドが止まります。これは逃げられないです。
ですので、awaitTask.Run()InventorのAPI呼び出しと繋いでも無意味で、UIがロックします。
仕方が無いので正攻法とは逆の発想で、標準とは別のUIスレッドを作って、そこでダイアログを表示させます。

1.3. 本記事のテストコードについて

InventorのAddIn上で実行するのが本来の目的のものの、別スレッドでUIを動かくことが本記事の主題なので、以下の方針でテストコードを書きます。

  • InventorのAddInにせず、単独アプリケーションとする
  • Button1, Button2の2つのButtonを持ったMainWindowをxamlで用意しているものとする
  • Button1を押すと、新たなMainWindowを別スレッドで開く
  • Button2を押すと、Button1で開いたMainWindowを閉じる
  • lockSemaphoreSlimを使いスレッド間で同期を取るべき場面も、今回は省略しています。ですので、このままのコードでは危険です。

2. 別スレッドでUI起動

この記事を参考にして、まずは起動側のコードを書きます。
(参考記事は「複数UIスレッドは止めとけ」って内容なのですけど)

using System.Reactive.Concurrency;
using System.Windows;
using System.Windows.Threading;

namespace WpfApp1;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private MainWindow? ChildWindow;

    private void Button1_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is not null) return;
        NewThreadScheduler scheduler = NewThreadScheduler.Default;
        var context = SynchronizationContext.Current;
        var AnotherUIThread = new Thread(_ =>
        {
            ChildWindow = new MainWindow();
            ChildWindow.ShowDialog();
            Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
            Dispatcher.Run();
            ChildWindow = null;
        });
        AnotherUIThread.SetApartmentState(ApartmentState.STA);
        AnotherUIThread.IsBackground = true;
        AnotherUIThread.Start();
    }

    private void Button2_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is null) return;
        // ChildWindowを閉じる処理
    }
}

元記事ではWindow.ClosedでしていたDispatcherのshutdownを、ShowDialog()の後に変更しています。
また、IsBackgroundを設定しました。
起動はこれで良しとします。

3. 別スレッドのWindowを閉じる

ここからが、いばらの道の始まりです。

3.1. 無策で挑む

まず、想像に難しくありませんが単純な以下のコードでは例外が発生します。

    private void Button2_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is null) return;
        ChildWindow.Close(); // 例外発生
    }

ChildWindowは別スレッドで走っているので、そのスレッドでClose()しないといけません。

3.2. ObserveOnの登場

「はいはい、ObservableからのObserveOnね。」と思ったあなた、お目が高い。その方針で進めます。
ReactivePropertyでコマンドを作り、

private ReactiveCommandSlim CloseCommand = new();

Button2_Click()で実行します。

private void Button2_Click(object sender, RoutedEventArgs e)
{
    if (ChildWindow is null) return;
    CloseCommand.Execute();
}

3.3. DispatcherScheduler.Current

あとは、別スレッドでwindow.ShowDialog()する前に、

CloseCommand.ObserveOn(DispatcherScheduler.Current).Subscribe(_ => window.Close());

とやるだけ・・・のはずが、NET8にはDispatcherSchedulerが無いのです。NetFramework時代はこの手法を使っていたのに、さて困りました。

3.4. Scheduler.CurrentThread

この辺りのAPIはNET移行時に整理されたようです。似たような名前のScheduler.CurrentThreadを使ってみます。

CloseCommand.ObserveOn(Scheduler.CurrentThread).Subscribe(_ => window.Close());

はい、これも失敗。ドキュメントを読んでみます。

解説
CurrentThread スケジューラは、元の呼び出しを行うスレッドで実行されるアクションをスケジュールします。 アクションはすぐには実行されませんが、キューに配置され、現在のアクションが完了した後にのみ実行されます。

つまり、ここでいうCurrentThreadとは絶対的ではなく相対的であり、呼び出しスレッドで実行するという意味です。罠だよ、こんなの。

3.5. SynchronizationContext.Current

ObserveOnISchedulerもしくはSynchronizationContextを引数にします。ここまでは前者で検討しましたが、ここでは後者で攻めてみます。
これまでと同様に、ShowDialog()の手前に1行加えます。

CloseCommand.ObserveOn(SynchronizationContext.Current).Subscribe(_ => window.Close());

ここでコンパイラから、「引数がnullかもしれない」と警告が。念のため、

var context = SynchronizationContext.Current ?? throw new Exception();
CloseCommand.ObserveOn(context).Subscribe(_ => window.Close());

としたところ、なんとcontextnullでした。万策尽きた!!

3.6. SynchronizationContext.Current 解決編

上記事をよると、UIスレッド以外ではnullが返ってくるとのことです。
今回はUIスレッドで実行しているつもりですが、window.ShowDialog()の手前なので、まだUIスレッドと認識されていないのではないか? と想定しました。
window.ShowDialog()の後では、閉じた後でないと実行されないので無意味です。そこで、Loadedに記述しました。

ChildWindow.Loaded += (_, _) =>
{
    var context = SynchronizationContext.Current ?? throw new Exception();
    CloseCommand.ObserveOn(context).Subscribe(_ => ChildWindow.Close());
};

これが当たりで、無事閉じられるようになりました。

4. 結論

using Reactive.Bindings;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Windows;
using System.Windows.Threading;

namespace WpfApp1;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private MainWindow? ChildWindow;
    private ReactiveCommandSlim CloseCommand = new();

    private void Button1_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is not null) return;
        NewThreadScheduler scheduler = NewThreadScheduler.Default;
        var context = SynchronizationContext.Current;
        var AnotherUIThread = new Thread(_ =>
        {
            ChildWindow = new MainWindow();
            ChildWindow.Loaded += (_, _) =>
            {
                var context = SynchronizationContext.Current ?? throw new Exception();
                CloseCommand.ObserveOn(context).Subscribe(_ => ChildWindow.Close());
            };
            ChildWindow.ShowDialog();
            Dispatcher.CurrentDispatcher.BeginInvokeShutdown(DispatcherPriority.SystemIdle);
            Dispatcher.Run();
            ChildWindow = null;
        });
        AnotherUIThread.SetApartmentState(ApartmentState.STA);
        AnotherUIThread.IsBackground = true;
        AnotherUIThread.Start();
    }

    private void Button2_Click(object sender, RoutedEventArgs e)
    {
        if (ChildWindow is null) return;
        CloseCommand.Execute();
    }
}

99. 親の記事に戻る

Autodesk Inventor API Hacking (概略)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?