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可没有给你这个东西.没有?那就造一个.
就衍生了此本文.
流程如下:
跨线程通讯
利用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
(完)