cad.net WPF嵌入技术3_等待界面+线程同步+子类化拦截消息循环

跨线程通讯

问题: 如何等待Acad界面完成
实现 IExtensionApplication 初始化接口,
程序界面未完成(进程启动之后是后台线程转前台线程),
若此时执行前台操作会出现:
a. 新建图纸会致命错误
b. 在获取Autodesk.Windows.ComponentManager.Ribbon==null

解决方案
1, 自执行接口文档事件
2, 自执行接口线程等待
开启线程等Acap.IsQuiescent 或者 Utils.IsEditorReady(测试发现它不对)
空闲事件/子类化NativeWindow获取Acad主线程上下文(Acad08可用,最佳实现).

问题2: 跨线程怎么发送信息到cad
1,阻塞Acad主线程-切换线程上下文-通过Editor发送信息.
2,发送命令也只是最后一步改变一下.

现在产生了一个贯彻始终的问题:跨线程通讯.
新建的线程等待cad界面完成,从子线程发送任务给主线程吧
Invoke?cad可没有给你这个东西.没有?那就造一个.
就衍生了此本文.

流程如下:

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

跨线程通讯

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

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

第一个想法利用WinForm.Invoke来和Acad通讯,
这也是福萝卜(赫思工具箱作者)教会我的方法,可谓是懒人最好的方法,
其他线程(WPF)调用这个form.Invoke()就可以发送任务了,
这个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>
        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 {
        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)  {
            if (disposing && (components != null))  {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code
        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
    }

想法

随着我将工程结构分开,之后还有更多个插件,那岂不是会产生n个这个同步面板?
总不能整合所有的插件用同一个隐藏面板吧...

阻塞线程

我问了B站的UP主糖君:
子线程如何在某个时刻阻塞一下主线程,然后执行我的任务.
他说: BV18t411V7CL
他实现的是阻塞一个子线程,换过来阻塞主线程就行.

上下文

接着发现阻塞和放行主线程成功了,
但是获取不到Acap内的Editor变量,永远都是错误的,为什么呢?
因为子线程没有主线程线程上下文,需要保存旧的,并切换一下.

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

比较有用的信息是:
xx类添加一个锁,然后主线程空闲的时候就等待这个锁.
那我怎么知道它什么时候空闲呢?
不好意思,文章就是没有,甚至没有个demo.

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

错误想法2(可跳过)
然后我想到了:
新建一个线程进行忙等待,判断主线程是否空闲,然后插入上下文.
那Thread有这个指示空闲的函数吗?
找到了这个,判断线程状态
一番测试之后,发现竟然没有空闲状态...
突然一想,这不废话吗!因为线程是一直运行的.
那么就要定义什么叫空闲
刚开始我以为是"线程运行"期间执行了Thread.Sleep()
后发生切换时间片会触发,结果有点背离.

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();

空闲事件WinForm

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

见:自执行接口线程等待

空闲事件WPF

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

它跟下面效果一样,只是二次开发时候,别人不提供给你呢?

Application.Current.Dispatcher.Invoke(() =>{
    textBox1.Text = "新的文本内容";
 });
using System;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

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

    private void Loaded(object sender, RoutedEventArgs e) {
        // loaded事件才有UI线程上下文
        _mainContext = SynchronizationContext.Current;
        var idle = (s, e) => {
            // 避免编译器优化了此变量,需要volatile保证
            var context = Volatile.Read(ref _mainContext);
            if(context is null) {
                var tx = System.Threading.SynchronizationContext.Current;
                Interlocked.CompareExchange(ref _mainContext, tx, null);
            }
            执行什么任务写入这里...
        };

        // 创建定时器,每10毫秒检查是否空闲
        var timer = new DispatcherTimer(
            TimeSpan.FromMilliseconds(10),
            DispatcherPriority.ApplicationIdle,// 或 DispatcherPriority.SystemIdle
            idle,
            Dispatcher.CurrentDispatcher
        );
        timer.Start();
    }
}

子类化NativeWindow

Acad08测试Winform空闲事件,
在vs开debug正常出现,但是直接运行cad就失效.
那空闲事件能自己去制造吗?

我们明白了:
1,空闲不是时间片切换,而是主线程循环执行过程消息循环.
空闲事件是来自于win窗体的消息循环机制.

2,那么我们可以利用子类化拦截消息循环.
Acad是已经有界面的程序,放任务到主窗口WndProc上面,
就成功获取到当前上下文的!

相关调用:
cad.net 拦截cad致命错误

为了懒鬼,我还是直接把拦截代码剪贴过来:
子类化核心是接口NativeWindow

namespace JoinBox.Forms;

using System;
using System.Windows.Forms;

/// <summary>
/// 窗口控件子类化
/// </summary>
public class NativeCallProc : NativeWindow, IDisposable {
    Func<Message, bool>? WndProcEvent;

    /// <summary>
    /// 窗口控件子类化
    /// </summary>
    /// <param name="intPtr">窗体句柄</param>
    public NativeCallProc(IntPtr intPtr) {
        this.AssignHandle(intPtr);
    }

    /// <summary>
    /// 消息循环:传委托进去不断替换
    /// </summary>
    /// <param name="WndProc">消息,true不拦截回调</param>
    public void WndProc(Func<Message, bool> WndProc) {
        WndProcEvent = WndProc;
    }

#line hidden
    // 此处会不断的执行消息循环
    protected override void WndProc(ref Message msg)
    {
        if (WndProcEvent is null)
            return;
        if (WndProcEvent.Invoke(msg))
            base.WndProc(ref msg);
    }
#line default

    #region IDisposable接口相关函数
    public bool IsDisposed { get; private set; } = false;

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~NativeCallProc() {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing) {
        // 不重复释放,并设置已经释放
        if (IsDisposed) return;
        IsDisposed = true;
        // 释放占用窗体的句柄
        ReleaseHandle();
    }
    #endregion
}

调用时候传入Acap.Windown.Handle就好了

namespace JoinBox.Forms;

using JoinBox.Basal;
using System;
using System.Diagnostics;

/// <summary>
/// 线程上下文/子类化拦截消息
/// </summary>
public class SyncEx : SyncContext, IDisposable
{
    /// <summary>
    /// 句柄
    /// </summary>
    public IntPtr Handle => _nativeCallProc.Handle;

    /// <summary>
    /// 子类化窗体对象
    /// </summary>
    readonly NativeCallProc _nativeCallProc;

    /// <summary>
    /// 子类化窗体上下文
    /// </summary>
    /// <param name="windowHandle">嵌入到此句柄的窗体</param>
    /// <exception cref="ArgumentException"></exception>
    [DebuggerStepThrough]
    public SyncEx(IntPtr windowHandle)
    {
        if (windowHandle == IntPtr.Zero)
            throw new ArgumentException("子类化失败,句柄为空");

        _nativeCallProc = new(windowHandle);

        /// 如果从来不实现,
        /// 会导致 <see cref="SyncContext.ExistContextCache"/> 永远不存在
        /// 因此在此处进行必然的一次实现
        WndProc(msg => { return true; });
    }

    /// <summary>
    /// 消息循环
    /// </summary>
    /// <param name="wndProc">消息,true不拦截回调</param>
    [DebuggerStepThrough]
    public void WndProc(Func<System.Windows.Forms.Message, bool> wndProc)
    {
        _nativeCallProc.WndProc(msg => {
            WaitOne();
            return wndProc.Invoke(msg);
        });
    }

    /// <summary>
    /// 触发刷新,避免卡死<br/>
    /// 需要在其他线程中实现,若先利用 Send/Post (切到cad主线程) 会导致卡死;<br/>
    /// 0x01 注册表自动load的时候,鼠标在绘图区中间或者界面外,<br/>
    /// 0x02 netload命令的时候,<see cref="Handle"/>的界面已经完成,<br/>
    /// 这两种情况,将永远等不到当前上下文切换,导致上下文缓存会一直为null<br/>
    /// 此函数将触发当前上下文切换<br/>
    /// 0x03 当上下文缓存不为null,那么当前上下文是null,也需要触发<see cref="WM.WM_NCCALCSIZE"/>,否则获取cad当前文档是null<br/>
    /// 因为需要窗口的Rect信息<br/>
    /// 所以不能自己进行:<br/>
    /// <![CDATA[
    ///    SendMessage(AutoGo.AcapMidSyncEx.Handle,
    ///                WM.WM_NCCALCSIZE,
    ///                (IntPtr)0x01,
    ///                rect/*NCCALCSIZE_PARAMS*/)
    /// ]]><br/>
    /// </summary>
    public void Refresh()
    {
        WindowsAPI.SetWindowPos(Handle);
    }

    #region IDisposable接口相关函数
    public bool IsDisposed { get; private set; } = false;

    /// <summary>
    /// 手动调用释放
    /// </summary>
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    /// <summary>
    /// 析构函数调用释放
    /// </summary>
    ~SyncEx()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool disposing)
    {
        // 不重复释放
        if (IsDisposed) return;
        IsDisposed = true;

        _nativeCallProc.Dispose();
    }
    #endregion
}

public class SyncEx2Form : Basal.SyncContext
{
    public System.Windows.Forms.Form? Window;

    /// <summary>
    /// 处理Form上下文
    /// </summary>
    /// <param name="window"></param>
    public SyncEx2Form(System.Windows.Forms.Form window)
    {
        Window = window ?? throw new ArgumentNullException(nameof(window));
    }

    /// <summary>
    /// 加载事件,因为显示之后才确定分配了线程
    /// </summary>
    public void Loaded()// object? sender = null/*, RoutedEventArgs? e = null*/
    {
        // 空闲时候执行的事件-cad08直接非Debug用不了
        System.Windows.Forms.Application.Idle += (sender, e) => {
            WaitOne();
        };
    }
}

ARX

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

(完)

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