cad.net WPF嵌入技术3_等待界面完成+在主线程中插入子线程的上下文同步方法

Acad多线程问题

先看下面两个问题

问题1:要如何等待Acad界面完成

Acad的 IExtensionApplication 接口实现时,
程序界面未完成(基本上正规程序都会是后台线程转为前台线程),若此时执行前台操作会出现:

  1. 新建图纸会致命错误
  2. 在获取Autodesk.Windows.ComponentManager.Ribbon==null

解决方案:

方案一: 插件上新建一个线程等 Utils.IsEditorReady(命令栏就绪) ,但是这个方法Acad08也不能用.

方案二: 插件上利用Winform空闲事件System.Windows.Forms.Application.Idle ,但是这个事件直接运行Acad08会失效.

方案三: 新建一个线程等 Acap.IsQuiescent 和 获取Acad主线程上下文(Acad08可用,最佳实现),可直接参考工程: CadLabelBar

流程如下:

graph TD A(Acad主线程) K(同时True) S(等待主线程空闲) A-->执行其他 A-->启动新线程-->死循环忙等待-->S S-->什么是空闲-->事件 事件-->WinForm:System.Windows.Forms.Application.Idle 事件-->WPF:DispatcherTimer 什么是空闲-->子类化-->最佳实践 B(空闲时执行) S--> B-->AA(Acap.IsQuiescent或者Utils.IsEditorReady命令栏就绪) B-->BB(主线程上下文不为空,即为已是界面线程) AA-->K BB-->K K-->执行阻塞主线程-->获取上下文-->插入业务代码-->释放主线程阻塞

问题2:多线程怎么发送信息到cad

解决方案: 具体操作如上流程,简化描述就是:阻塞Acad主线程-获取Acad主线程上下文-Editor发送信息.

总结

然后我们现在又产生了一个贯彻始终的问题:线程通讯
新建的线程等待cad界面完成,然后完成之后,你需要从子线程发送点"业务"给主线程吧,就衍生了此问题.
Invoke? cad可没有给你这个东西.
没有?那就造一个.

多线程同步线程

利用WinForm(不好,可跳过本段)

cad.net WPF嵌入技术2_将WPF嵌入到Acad08 描述了一个问题,WPF的UI线程是新建的,新建的线程又无法和Acad08进行线程安全的操作.

第一个想法通过WinForm的Invoke来和Acad08进行多线程同步,这也是福萝卜教会我的方法,可谓是懒人最好的方法.

在其他线程(WPF)中,我们调用Acad主线程启动的WinForm的Invoke就可以阻塞Acad主线程并发送内容,从而实现交互,而这个WinForm是看不见的.

WinForm:
AcadIntermediary.cs

using System;
using System.Threading;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;

namespace JoinBox
{
    public partial class AcadIntermediary : Form
    {
        /// <summary> 
        /// Acad2008无法WPF多线程直接交互,为此建立桥梁.
        /// 实例化时必须在Acad主线程中
        /// </summary>
        public AcadIntermediary()
        {
            InitializeComponent();
            this.ShowInTaskbar = false;
            this.WindowState = FormWindowState.Minimized;
            Load += Start_Load;
        }

        private void Start_Load(object sender, EventArgs e)
        {
            this.Hide();//一定要这句,不然左下角有一条小小的标题栏           
        }
 
        public void Action(Action ac)
        {
            while (true)
            {
                //句柄创建后才可以用委托 && 没有活动命令
                if (this.IsHandleCreated && CadSystem.Getvar("cmdnames") == "")
                {                     
                    Invoke((Action)delegate ()//调度CAD程序的线程安全
                           {
                               ac?.Invoke();
                           });
                    break;
                }
                Thread.Sleep(100);
            }
        }

        //==================================================================================================

        /// <summary>
        /// 让程序不显示在alt+Tab视图窗体中
        /// </summary>
        /// https://www.cnblogs.com/xielong/p/6626105.html
        protected override CreateParams CreateParams
        {
            get
            {
                const int WS_EX_APPWINDOW = 0x40000;
                const int WS_EX_TOOLWINDOW = 0x80;
                CreateParams cp = base.CreateParams;
                cp.ExStyle &= ~WS_EX_APPWINDOW;    // 不显示在TaskBar
                cp.ExStyle |= WS_EX_TOOLWINDOW;    // 不显示在Alt+Tab
                return cp;
            }
        }
    }
}

AcadIntermediary.Designer.cs

namespace JoinBox
{
    partial class AcadIntermediary
    {
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.SuspendLayout();
            // 
            // Start
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.AutoSize = true;
            this.ClientSize = new System.Drawing.Size(10, 10);
            this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
            this.Name = "Start";
            this.Text = "Start";
            this.ResumeLayout(false);

        }

        #endregion
    }
}

想法

随着我将工程结构分开,将自己的插件JJBox和Labelbar分离成两个工程设计了,
如果之后还有更多个插件,那岂不是会产生n个这个同步面板?

总不能整合所有的插件用同一个隐藏起来的面板吧...
也不是不行,因为我现在就是这么做的...但是我还是觉得这样是不对的,毕竟要靠WinForm感觉就是不对...

参考_阻塞主线程

我问了B站的UP主糖君:
我想用子线程在某个时刻Action挡住一下主线程(空闲时候),并且在主线程中插入上下文,再释放,实现同步方法.我要在net3.5干这个.

糖君:
你可以去看我"怎样做线程暂停"那一期,里面讲了怎么在一个线程里调起另一个线程的等待:BV18t411V7CL

他实现了阻塞一个子线程,而认真思考一下就知道可以换过来阻塞主线程运行,
见下方的WinForm空闲的状态.

参考_上下文同步

接着发现阻塞和放行主线程成功了,但是获取不到Acap内的Editor,为什么呢?因为子线程没有主线程的上下文.

找到了这篇:自己实现一个线程同步上下文

比较有用的信息是: xx类添加一个锁,然后主线程空闲的时候就等待这个锁.

那我怎么知道它什么时候空闲呢?不好意思,文章就是没有,甚至没有个demo.

错误想法(可跳过)

然后我想到了:新建一个线程进行忙等待,判断主线程是否空闲,然后插入上下文.

那Thread有这个指示空闲的函数吗?找到了这个,判断线程状态

var mainThread = Thread.CurrentThread;
new Thread(() =>
{
    while (true)
    {
        if ((mainThread.ThreadState & ThreadState.Running) == ThreadState.Running)
            Debug.WriteLine("Running");
        if ((mainThread.ThreadState & ThreadState.StopRequested) == ThreadState.StopRequested)
            Debug.WriteLine("StopRequested");
        if ((mainThread.ThreadState & ThreadState.SuspendRequested) == ThreadState.SuspendRequested)
            Debug.WriteLine("SuspendRequested");
        if ((mainThread.ThreadState & ThreadState.Background) == ThreadState.Background)
            Debug.WriteLine("Background");
        if ((mainThread.ThreadState & ThreadState.Unstarted) == ThreadState.Unstarted)
            Debug.WriteLine("Unstarted");
        if ((mainThread.ThreadState & ThreadState.Stopped) == ThreadState.Stopped)
            Debug.WriteLine("Stopped");
        if ((mainThread.ThreadState & ThreadState.WaitSleepJoin) == ThreadState.WaitSleepJoin)
            Debug.WriteLine("WaitSleepJoin");
        if ((mainThread.ThreadState & ThreadState.Suspended) == ThreadState.Suspended)
            Debug.WriteLine("Suspended");
        if ((mainThread.ThreadState & ThreadState.AbortRequested) == ThreadState.AbortRequested)
            Debug.WriteLine("AbortRequested");
        if ((mainThread.ThreadState & ThreadState.Aborted) == ThreadState.Aborted)
            Debug.WriteLine("Aborted");
        Thread.Sleep(100);
    }
}).Start();

一番测试之后发现,竟然没有空闲状态...
突然一想:这不废话吗!因为线程是一直运行的.

那么就要定义什么叫"空闲"?
刚开始我以为是"线程运行"期间执行了Thread.Sleep()后发生切换时间片会触发,结果有点背离.

阻塞Acad主线程_空闲事件

WinForm

WinForm下面有一个空闲事件: System.Windows.Forms.Application.Idle
既然Acad是个窗体程序,那么可以利用这个监控.
就是又利用了WinForm,兜兜转转又回来了...这样起码不用new Form()作为载体,貌似也能接受.

using Acap = Autodesk.AutoCAD.ApplicationServices.Application;

public class AutoDocs : IExtensionApplication//cad自动运行接口
{     
    /// <summary>
    /// 当前上下文的缓存<br/>
    /// 它虽然是(<see cref="SynchronizationContext.Current"/>)赋值的,但是不完全相等<br/>
    /// 因为 <see cref="SynchronizationContext.Current"/> 的没有保留上次的能力<br/>
    /// 缓存的意义:<br/>
    /// 当使用cad的新建图纸对话框(其他对话框也是),此时 <see cref="SynchronizationContext.Current"/>==null,<br/>
    /// 若没有缓存的时候,会直接卡死<br/>
    /// </summary>
    SynchronizationContext? _mainContext = null;
    // 线程的开关
    ManualResetEvent _OnOff = new(true);

    /// <summary>
    /// 上下文处理
    /// </summary>
    /// <param name="ac"></param>
    public void InvokeContext(Action ac)
    {
        //....此时WaitOne在主线程"空闲处"(消息循环)不断运行着.

        _OnOff.Reset();//执行之后WaitOne会阻塞主线程.

        //主线程上文后接驳我的子线程下文,否则线程不安全,拿变量会出错.
        //Send和Post都可以用,但是不用Post的话,会无法使用getpoint等交互函数
        _mainContext?.Post(state =>
        {
           ac.Invoke();
        }, null);

        _OnOff.Set();//放行主线程
    }
    
    // 打开cad的时候会自动执行
    public void Initialize()
    {              
        System.Windows.Forms.Application.Idle += Application_Idle;
        //高版可以用这个
        //Acap.Idle += Application_Idle;
        
        new Thread(() =>
        {
            while (true)
            {
                InvokeContext(() =>
                {
                    var doc = Acap.DocumentManager.MdiActiveDocument;
                    var ed = doc.Editor;
                    ed.WriteMessage("\n插入子线程上下文,当前线程id:" + Thread.CurrentThread.ManagedThreadId);
                });
                Thread.Sleep(3000);//这里是子线程等待
            }
        }).Start();
    }
    
    //会频繁发生事件
    private void Application_Idle(object sender, System.EventArgs e)
    {
        //一旦不为null,一直不为null,否则无法获取前一个上下文
        if(SynchronizationContext.Current != null)
           _mainContext = SynchronizationContext.Current;
        _OnOff.WaitOne();
    }
    public void Terminate() { }
}

仔细分析一下,这个事件是来自于win窗体的消息循环机制:
创建进程-创建主线程-进入窗体的消息循环-发现空闲事件委托链有东西(被+=了)就跑进去运行一下.

此时"空闲"的定义不是时间片切换,而是主线程循环执行过程中执行空闲事件.
我就说不太可能每次切换时间片都给所有线程发送一个我好闲的信号

然而...Acad08测试时候,空闲事件在vs开debug的时候运行很正常,而直接运行cad就失效了.

再归纳一次,
我目的是将子线程提供的内容插入到Acad主线程上,
所以"阻塞闸门"必须放到Acad主线程的"空闲"上,再利用"空闲"频繁执行.

子类化

那"空闲"能自己去制造吗?
如果是控制台程序(需要仿WinForm/WPF),那就需要自己仿一个空闲事件.
但是cad是已经有界面的程序,
把"阻塞闸门"放到Acad主窗口的子类化WndProc上面,就成功获取到当前上下文的!

子类化文章参考另见cad.net 重置cad之后创建文档出错,拦截cad致命错误

也可以直接看工程: CadLabelBar的关键字WndProc或者SyncContext
在里面的: 拦截消息:CAD主窗口-WndProc-最后写上下文切换就好了

错误想法(可跳过)

我尝试手写相关的线程,发现new Thread线程没有上下文, 因为只有UI线程才有,也就是new Form() 再获取上下文SynchronizationContext.Current才有, 那WinForm是怎么做的呢? 它是new SynchronizationContext()...就那么简单... 现在仿是仿了,但是这样上下文是捏造的Post没有获取Editor...(debug才发现我这个想法不对🌚)

WPF

那如果切换到高版本cad的WPF呢?可以用子类化,也可以用计时器仿制一个WPF的Idle

#region 用计时器仿WinForm空闲事件,放在WPF的Loaded事件上

/// <summary>
/// 空闲事件
/// </summary>
public event EventHandler Idle;
/// <summary>
/// 用计时器仿WinForm空闲事件
/// </summary>
void DispatcherTimer(Action<object, EventArgs> idle)
{
    if (idle == null)
        throw new ArgumentNullException();

    Idle += (sender, e) =>
    {
        idle.Invoke(sender, e);
    };
    var timer = new DispatcherTimer
    (
        TimeSpan.FromMilliseconds(1),
        DispatcherPriority.ApplicationIdle,// 或 DispatcherPriority.SystemIdle
        Idle,
        Dispatcher.CurrentDispatcher
    );
    timer.Start();
}
#endregion

调用的时候需要这样写在WPF窗体的Loader事件上面.

private void Loaded(object sender, RoutedEventArgs e)
{
    //空闲时候执行的事件
    DispatcherTimer((sender1, e1) =>
    {
        //一旦不为null,一直不为null,否则无法获取前一个上下文
        if(SynchronizationContext.Current != null)
           _mainContext = SynchronizationContext.Current;
        _onOff.WaitOne();//阻塞线程
    });
}

ARX

在objectarx找到类似功能的方法AcApDocManager::beginExecuteInApplicationContext

(完)

posted @ 2020-12-23 17:39  惊惊  阅读(1148)  评论(0编辑  收藏  举报