等待Acad界面完成
问题
Acad的 IExtensionApplication 接口实现时,
程序界面未完成(基本上正规程序都会是后台线程转为前台线程),若此时执行前台操作会出现:
- 新建图纸会致命错误
- 在获取Autodesk.Windows.ComponentManager.Ribbon==null
解决方案
- 新建一个线程等 Utils.IsEditorReady(命令栏就绪) ,但是这个方法Acad08也不能用.
- 新建一个线程等 Acap.IsQuiescent 和获取到 Acad主线程上下文
- 利用Acad窗口的空闲事件,System.Windows.Forms.Application.Idle 但是这个事件直接运行Acad08会失效(后面有补救方法).
解决方案可直接参考工程: CadLabelBar
然后我们现在有了一个贯彻始终的问题:线程通讯
新建的线程等待cad界面完成,然后等cad完成,完成了之后你需要从子线程发送点东西给主线程吧,就衍生了此问题.
Invoke? cad可没有给你这个东西.
没有?那就造一个.
多线程同步线程_利用WinForm的Invoke(不好)
在 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()后发生切换时间片会触发.(刚开始我这么认为,结果有点背离)
WinForm的空闲状态
WinForm下面有一个空闲事件: System.Windows.Forms.Application.Idle
既然Acad2008是个窗体程序,那么可以利用这个监控.
就是又利用了WinForm,兜兜转转又回来了...这样起码不用new Form()作为载体,貌似也能接受.
于是乎,我终于知道了一点窍门了,写出了一个demo给大家参考.
阻塞Acad主线程_空闲事件(非Debug运行无效)
using Acap = Autodesk.AutoCAD.ApplicationServices.Application;
public class AutoDocs : IExtensionApplication//cad自动运行接口 IAutoGo
{
//主线程的上下文
SynchronizationContext _mainContext = null;
// 线程的开关
ManualResetEvent _OnOff = new(true);
/// <summary>
/// 上下文处理
/// </summary>
/// <param name="ac"></param>
public void InvokeContext(Action ac)
{
_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)
{
_mainContext = SynchronizationContext.Current;
_OnOff.WaitOne();//阻塞主线程的闸门_需要开关控制
}
public void Terminate() { }
}
仔细分析一下,这个事件是来自于win窗体的消息循环机制:
创建进程-创建主线程-进入窗体的消息循环-发现空闲事件委托链有东西(被+=了)就跑进去运行一下.
此时"空闲"的定义不是时间片切换,而是主线程循环执行过程中执行空闲事件.
我就说不太可能每次切换时间片都给所有线程发送一个我好闲的信号🤣
然而...Acad08测试时候,空闲事件在vs开debug的时候运行很正常,而直接运行cad就失效了.
再归纳一次,
我目的是将子线程提供的内容插入到Acad主线程上,
所以"阻塞闸门"必须放到Acad主线程的"空闲"上,再利用"空闲"频繁执行.
那"空闲"能自己去制造吗?
如果是控制台程序(需要仿WinForm/WPF),那就需要自己仿一个空闲事件.
我尝试手写相关的线程,发现new Thread线程没有上下文,
因为只有UI线程才有,也就是new Form() 再获取上下文SynchronizationContext.Current才有,
那WinForm是怎么做的呢?
它是new SynchronizationContext()...就那么简单...
现在仿是仿了,但是这样上下文是捏造的Post没有获取Editor...(debug才发现我这个想法不对🌚)
最后灵机一动,发现可以把"阻塞闸门"放到Acad主线程的子类化WndProc上面(空闲的伪装),是成功获取到当前上下文的!
阻塞Acad主线程_子类化(最佳)
子类化文章参考另见cad.net 重置cad之后创建文档出错,拦截cad致命错误
也可以直接看工程: CadLabelBar的WndProc
在里面的: 拦截消息:CAD主窗口-WndProc-最后写上下文切换就好了
工程中的语句如下,去搜索:
AutoDocs.SyncManager?.Loaded();
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) =>
{
_mainContext = System.Threading.SynchronizationContext.Current;
_onOff.WaitOne();//阻塞线程
});
}
(完)