等待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();//阻塞線程
});
}
(完)