cad.net 自执行初始化接口IExtensionApplication
原因
继承IExtensionApplication
初始化接口的派生类会加载后自动执行.
但是此接口是不能在同一个dll实现一次以上的,继承了多次也不会执行多次,会报错.
那么这给编写代码带来了一种不好的情况是,每次都要去修改这个派生类,
如果是一个小的测试功能,你又要去动前面的核心,
老板都说这个类OK了,结果你git日志上面还能看到对它的修改,这样就感觉很蛋疼.
违反了编程原则
开闭原则: 对拓展进行开放,对修改进行关闭.
所以我是这么想的,只实现一次IExtensionApplication
初始化接口,
通过它反射仿初始化接口
的派生类,然后实例化它们,
运行它们的 仿Initialize方法
和 仿Terminate方法
,从而绕开只能唯一继承的缺点.
当然,除了反射仿初始化接口
还可以反射初始化特性
.
原始接口功能
派生类执行顺序
1,成员变量会被JIT以线程安全的方式初始化.
2,运行构造函数.
3,运行 Initialize 方法.
4,关闭CAD前会运行 Terminate 方法.
此方法遇到强行关闭进程是不会运行的,所以是弱执行.
此方法和 Initialize 方法位于同一个实例化类,为了验证说法,请看:
// A:如果输出都是1,就是不同实例化.
// B:如果输出是1和2,就是同一个实例化.
// 测试证明是B,说明Acad是会缓存的.
public class TestClass : IExtensionApplication {
int a = 1;
public void Initialize() {
Debug.WriteLine(a);
a++;
}
public void Terminate() {
Debug.WriteLine(a);
}
}
但是我们实例化时仍然需要构建缓存,
因为我们存在同一个类下的多个方法写初始化特性,
为了避免实例化这个类多次,下面代码会说这个事情.
加载dll时机
CAD加载dll有两种方式,它们的触发接口时机不同,
需要共同保证(我下面代码就是帮大家完成这个工作)
01,通过注册表启动
CAD启动时候会从进程后台到界面完成,
接口派生类在执行时候是后台,
此时界面和文档并没有完成,执行上下文和UI同步上下文还没有成为同一个.
a: 文档函数是无法使用,会为null,也就不存在doc.Editor了.
b: 新建图纸会致命错误.
c: Autodesk.Windows.ComponentManager.Ribbon==null.
d: 设置CAD系统变量会出错.
因此必须用dm文档事件,等待文档出现,然后执行任务.
dm全局事件可以不取消订阅,doc和db事件则需要注意取消订阅.
02,通过netload加载启动
此时界面已经完成,可以尝试直接执行任务.
03,运行时/结束前
dm是永远不为null的
var dm = Acap.DocumentManager;
Debug.Assert(dm is not null, "不可思议的为空Acap.DocumentManager");
当关闭全部文档后: dm.Count == 0;
但是我们不需要这样判断,直接获取当前文档是一样的:
var doc = dm.MdiActiveDocument;
此时doc is null,而发送命令和提示信息需要doc.
例如发送开启文档ctrl+n就要改用win32API.
搜索qnew: https://www.cnblogs.com/JJBox/p/14187031.html
更多实现
南胜还反射特性提取每个命令,把它们放在cuix的菜单工具位置
南胜博客,自动生成cuix
封装
接口和特性
namespace JoinBox;
[Flags]
public enum Sequence : int {
StartFirst = 1, // 进程开启,后台最先
StartLast = 1 << 1, // 进程开启,后台最后
StartOnce = 1 << 2, // 文档首次开启,单例(特性默认/打印信息/发送命令/必然拥有前台文档)
StartDocs = 1 << 3, // 文档每次开启
EndDocs = 1 << 4, // 文档每次关闭
EndOnce = 1 << 5, // 文档最后关闭,不是单例
EndFirst = 1 << 6, // 进程关闭,最先,此时已经没有前台
EndLast= 1 << 7 // 进程关闭,最后
}
// 初始化接口,仿IExtensionApplication
public interface IAutoGo {
// 控制加载顺序
Sequence SequenceId();
// 被cad加载时候会自动执行
void Initialize();
// 关闭cad的时候会自动执行
void Terminate();
}
// 初始化特性,放在命令函数上用来初始化
[AttributeUsage(AttributeTargets.Class/*没有用*/
| AttributeTargets.Method
| AttributeTargets.Property/*没有用*/,
AllowMultiple = true)]
public class JAutoGo : Attribute {
public Sequence SequenceId { get; }
public JAutoGo(Sequence sq = Sequence.StartOnce) {
SequenceId = sq;
}
}
自动执行
#define parallel
namespace JoinBox;
// 执行器
public class Actuator : IEquatable<Actuator>, IComparable<Actuator> {
public Sequence SequenceId { get; }
MethodInfo _methodInfo;
public Actuator(MethodInfo method, Sequence sequence) {
SequenceId = sequence;
_methodInfo = method;
}
public bool Equals(Actuator other) {
if (other is null) return false;
return (int)SequenceId == (int)other.SequenceId
&& _methodInfo.Equals(other._methodInfo);
}
public override bool Equals(object obj) {
if (obj is not Actuator other) return false;
return Equals(other);
}
public override int GetHashCode() {
return (int)SequenceId;
}
// SortedSet/SortedList/SortedDictionary都是不能重复,
// 而且要实现IComparable<T>接口,而且不是鸭子类型
public int CompareTo(Actuator other) {
if((int)SequenceId > (int)other.SequenceId)
return 1;
if((int)SequenceId < (int)other.SequenceId)
return -1;
return 0;
}
/// <summary>
/// 运行方法
/// </summary>
public Actuator Run(object[]? args = null) {
try {
TypeCache.Invoke(_methodInfo, args);
/* 此处由用户自己保证
// Acad年份获取 https://www.cnblogs.com/JJBox/p/18511807
// 立即处理队列
if (VersionTool.CurrentYear >= 2014){
System.Windows.Forms.Application.DoEvents();
}
// 底层一样ed.UpdateScreen();
Acap.UpdateScreen();
*/
return this;
} catch (System.Exception e) {
Debugger.Break();
Debug.WriteLine("Actuator.Run出错" + e.Message
+ $"出错位置:{_methodInfo.ReflectedType?.FullName}.{_methodInfo.Name}");
return this;
}
}
}
public static class TypeCache {
// 如果同一个类中多个方法有初始化特性,
// 那么这个类会被实例化n次,所以需要构造缓存.
// 如果key是方法信息,另一个方法信息进来找不到key,但是它们是同一个类.
// 所以key是类名才对,
// 但是key是方法信息,可以快速命中同一个方法,
// 因此双缓存.
static ConcurrentDictionary<MethodInfo, object> _cache1 = new();
static ConcurrentDictionary<string, object> _cache2 = new();
public static void Clear() {
_cache1.Clear();
_cache2.Clear();
}
public static object? Invoke(MethodInfo methodInfo, object[]? args = null) {
if (methodInfo.IsStatic) {
// 静态调用,参数数量要匹配,为null.
var pas = methodInfo.GetParameters();
if (args is null) args = new object[pas.Length];
return methodInfo.Invoke(null, args);
}
// 非静态调用,实例化类
object instance;
string? fullName = null;
Type? reftype = null;
bool flag = _cache1.TryGetValue(methodInfo, out instance);
if (!flag) {
// 类位置,扩展方法会为空
reftype = methodInfo.ReflectedType;
// 命名空间+类
fullName = reftype?.FullName;
if (fullName is null) return null;
flag = _cache2.TryGetValue(fullName, out instance);
}
if (!flag) {
if (fullName is null) return null;
var type = reftype?.Assembly.GetType(fullName);
if (type is null) return null;
instance = Activator.CreateInstance(type);
_cache1.TryAdd(methodInfo, instance);
_cache2.TryAdd(fullName, instance);
}
return methodInfo.Invoke(instance, args);
}
}//class
// 剔除重复
public static class ListExtensions {
public static void ExceptWith<T>(this List<T> sourceList, T[] other) {
for (int i = other.Length - 1; i >= 0; i--) {
if (other.Contains(sourceList[i]))
sourceList.RemoveAt(i);
}
}
}
public class AutoClass : IExtensionApplication {
// 储存全部初始化和释放执行函数
// C#的有序结构都是不能重复的
// SortedSet/SortedList/SortedDictionary
// 我们还是用List吧
List<Actuator> _set = new();
// 只反射本dll的程序集
bool _constraint = true;
// 被cad加载时候自动执行
public void Initialize() {
try {
var dm = Acap.DocumentManager;
Debug.Assert(dm is not null, "不可思议的为空Acap.DocumentManager");
// 获取特性下面全部方法
GetAttributeFunc();
// 获取接口下面全部方法
GetInterfaceFunc();
// 执行任务,此时即使调用doc.Editor输出也是无效的
var array = _set.Where(ac => ac.SequenceId == Sequence.StartFirst).ToArray();
_set.ExceptWith(array);
array.ForEach(ac => ac.Run());
array = _set.Where(ac => ac.SequenceId == Sequence.StartLast).ToArray();
_set.ExceptWith(array);
array.ForEach(ac => ac.Run());
// 文档每次开启,专用数组
_startDocs = _set.Where(ac => ac.SequenceId == Sequence.StartDocs).ToArray();
_set.ExceptWith(_startDocs);
// 文档每次关闭,专用数组
_endDocs = _set.Where(ac => ac.SequenceId == Sequence.EndDocs).ToArray();
_set.ExceptWith(_endDocs);
// 为了能够无论何种加载都能doc.Editor输出:
// x01,通过注册表加载StartFirst/Last,有doc没有doc.Editor,所以不会输出.
// 用订阅文档事件等待,其后会立即触发一次,文档事件内就可以发送打印了.
// x02,通过netload命令加载,虽然有订阅文档事件,
// 它会在下次创建文档(ctrl+n)触发,此时必然有doc.Editor需要直接触发.
dm.DocumentCreated += DmCreated;
dm.DocumentToBeDestroyed += DmDestroyed;
if (IsNetload()) {
var doc = dm.MdiActiveDocument;
MyTask(doc);
}
// 开启线程等待editor就绪,会打印四次,不知道为什么.
// ThreadHelper.NewlyThread(ed => MyTask(ed));
} catch (System.Exception e) {
Debugger.Break();
Debug.WriteLine("AutoClass.Initialize出错::" + e.Message);
}
}
// 用命令输入中判断,那么动态加载dll就判断失效?
bool IsNetload() {
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var ed = doc?.Editor;
return doc is not null && doc.CommandInProgress.ToUpper() == "NETLOAD"
&& ed is not null
&& !(ed.IsDragging && ed.IsQuiescent
&& ed.IsQuiescentForTransparentCommand
&& ed.MouseHasMoved
&& ed.UseCommandLineInterface);
}
// 执行我们的任务,
// 只需要运行一次,就弄个标记做单例,为1就不执行
int _isOnceExecuted = 0;
void MyTask(Document doc) {
Debug.Assert(doc is not null, "不可思议的为空doc");
if (Interlocked.CompareExchange(ref _isOnceExecuted, 1, 0) == 0) {
var array = _set.Where(ac => ac.SequenceId == Sequence.StartOnce).ToArray();
_set.ExceptWith(array);
var docArgs = new object[]{doc};
array.ForEach(ac => ac.Run(docArgs));
}
}
// 文档开启
Actuator[] _startDocs = [];
void DmCreated(object sender, DocumentCollectionEventArgs e) {
try {
var doc = e.Document;
var docArgs = new object[]{doc};
// 首次执行比每次先
MyTask(doc);
// 每次执行
_startDocs.ForEach(ac => ac.Run(docArgs));
} catch (System.Exception ex) {
Debugger.Break();
Debug.WriteLine("AutoClass.DmCreated出错::" + ex.Message);
}
}
// 文档关闭
Actuator[] _endDocs = [];
void DmDestroyed(object sender, DocumentCollectionEventArgs e) {
try {
var doc = e.Document;
var docArgs = new object[]{doc};
// 每次执行
_endDocs.ForEach(ac => ac.Run(docArgs));
// 最后关闭的文档,它不是单例,因为重复多次.
var dm = Acap.DocumentManager;
if (dm.Count == 1) {
_set.Where(ac => ac.SequenceId == Sequence.EndOnce)
.Select(ac => ac.Run(docArgs))
.ToArray();
}
} catch (System.Exception ex) {
Debugger.Break();
Debug.WriteLine("AutoClass.DmDestroyed出错::" + ex.Message);
}
}
// 关闭cad的时候会自动执行
public void Terminate() {
try {
var dm = Acap.DocumentManager;
Debug.Assert(dm is not null, "不可思议的为空Acap.DocumentManager");
dm.DocumentCreated -= DmCreated;
dm.DocumentToBeDestroyed -= DmDestroyed;
// 执行任务
_set.Where(ac => ac.SequenceId == Sequence.EndFirst)
.Select(ac => ac.Run())
.ToList();
_set.Where(ac => ac.SequenceId == Sequence.EndLast)
.Select(ac => ac.Run())
.ToList();
// 释放缓存,类析构会在此之后.
TypeCache.Clear();
} catch (System.Exception e) {
Debugger.Break();
Debug.WriteLine("AutoClass.Terminate出错::" + e.Message);
}
}
/// <summary>
/// 遍历程序域下所有类型
/// </summary>
/// <param name="dllNameWithoutExtension">约束此文件反射,不含扩展名</param>
public static IEnumerable<Type> AppDomainGetTypes(string? dllNameWithoutExtension = null) {
try {
// 01,过滤ass.IsDynamic,因为Acad2021报错:
// System.NotSupportedException:动态程序集中不支持已调用的成员
// 02,过滤AcInfoCenterConn.dll,通讯库,因为反射它会依赖其他造成报错:
// ReflectionTypeLoadException
// 03,WPF的报错 https://dev59.com/rWIi5IYBdhLWcg3w_QmH
// Microsoft.Expression.Interactions程序集
// 跳过全部微软的包就好了?
const string str1 = "AcInfoCenterConn";
const string str2 = "Microsoft";
#if parallel
System.Diagnostics.Trace.WriteLine("这是一条Trace消息,此时是并行");
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
.AsParallel()
.WithDegreeOfParallelism(Environment.ProcessorCount)
.Where(ass => !ass.IsDynamic)
.Where(ass => !ass.GetName().Name.Contains(str2))
.Where(ass => Path.GetFileNameWithoutExtension(ass.Location) != str1);
// 约束在此dll中反射.
if (dllNameWithoutExtension is not null) {
assemblies = assemblies.Where(ass =>
Path.GetFileNameWithoutExtension(ass.Location)
== dllNameWithoutExtension);
}
// 只反射公开
var types = assemblies.SelectMany(ass => ass.GetExportedTypes());
#else
/*
// 找名称
var names = AppDomain.CurrentDomain.GetAssemblies()
.Where(ass => !ass.IsDynamic
&& Path.GetFileNameWithoutExtension(ass.Location) == str1)
.Select(ass => ass); // ass.GetName().Name 为什么是空?
MessageBox.Show(string.Join("\n\r", names.ToString()));
*/
// 串行测试
var assemblies = AppDomain.CurrentDomain.GetAssemblies()
.Where(ass => !ass.IsDynamic)
.Where(ass => !ass.GetName().Name.Contains(str2))
.Where(ass => Path.GetFileNameWithoutExtension(ass.Location) != str1);
// 约束在此dll中反射.
if (dllNameWithoutExtension is not null) {
assemblies = assemblies.Where(ass =>
Path.GetFileNameWithoutExtension(ass.Location)
== dllNameWithoutExtension);
}
int error = 0;
var types = assemblies.SelectMany(ass => {
try {
++error;
// return ass.GetTypes();
return ass.GetExportedTypes(); // 只反射公开
} catch(ReflectionTypeLoadException e) {
Debug.WriteLine($"出错:App;计数{error};错误信息:{e}");
foreach (Type type in e.Types) {
if (type is null) continue;
Debug.WriteLine($"可用类型:{type.FullName}");
}
Debugger.Break();
return null;
} catch(Exception e) {
Debug.WriteLine($"出错:App2;计数{error};程序集:{ass}");
Debug.WriteLine($"出错:App2;计数{error};错误信息:{e}");
Debugger.Break();
return null;
}
});
#endif
return types;
} catch (System.Exception e) {
Debugger.Break();
Debug.WriteLine($"出错:AppDomainGetTypes;错误信息:{e.Message}");
return null;
}
}
/// <summary>
/// 收集接口下的函数
/// </summary>
/// <returns></returns>
void GetInterfaceFunc() {
// 约束只反射本dll的类
string? dll = null;
if (_constraint) {
var ass = Assembly.GetExecutingAssembly();
dll = Path.GetFileNameWithoutExtension(ass.Location);
}
var ts = AppDomainGetTypes(dll);
#if parallel
System.Diagnostics.Trace.WriteLine("这是一条Trace消息,此时是并行");
// 我要实例化派生类(跳过委托/抽象和静态)
var acs = ts.Where(type => type.IsClass)
.Where(type => !type.IsAbstract)
.Where(type => type.GetInterfaces().FirstOrDefault(
iface => iface.Name == nameof(IAutoGo)) is not null)
// .SelectMany(type => type.GetMethods()); // 方法展开会错序.
.Select(type => CreateActuator2(type))
.ToArray();
// 串行加入容器
foreach(var ac in acs) {
if (ac is null) continue;
_set.Add(ac.Value.Init);
_set.Add(ac.Value.Term);
}
#else
var types = ts.ToArray();
for (int i = 0; i < types.Length; i++) {
var type = types[i];
if (type is null) continue;
// 我要实例化派生类(跳过委托/抽象和静态)
if (!type.IsClass || type.IsAbstract) continue;
var hasIface = type.GetInterfaces().FirstOrDefault(
iface => iface.Name == nameof(IAutoGo));
if (hasIface is null) continue;
var ac = CreateActuator2(type);
if(ac is null) continue;
_set.Add(ac.Value.Init);
_set.Add(ac.Value.Term);
}
#endif
}
const string _sequenceId = nameof(Sequence) + "Id";
const string _in = "Initialize";
const string _te = "Terminate";
(Actuator Init, Actuator Term)? CreateActuator2(Type type) {
// 获取接口实现的成员函数,虽然它们只会出现一次,
// 但万一别人写了重载呢,要参数数量是0才行.
var mets = type.GetMethods();
var im = mets.FirstOrDefault(m => m.Name == _in
&& m.GetParameters().Length == 0
&& !m.IsAbstract);
if (im is null) return null;
var tm = mets.FirstOrDefault(m => m.Name == _te
&& m.GetParameters().Length == 0
&& !m.IsAbstract);
if (tm is null) return null;
var sq = Sequence.StartOnce;
var sm = mets.FirstOrDefault(m => m.Name == _sequenceId
&& m.GetParameters().Length == 0
&& !m.IsAbstract);
if (sm is not null) {
// 这里是多线程环境,缓存需要线程安全容器
var obj = TypeCache.Invoke(sm);
if(obj is not null)
sq = (Sequence)obj;
}
// 如果派生类写的是End需要映射回Start.
if (sq == Sequence.EndFirst)
sq = Sequence.StartFirst;
else if (sq == Sequence.EndLast)
sq = Sequence.StartLast;
else if (sq == Sequence.EndDocs)
sq = Sequence.StartDocs;
else if (sq == Sequence.EndOnce)
sq = Sequence.StartOnce;
Sequence sq2;
if (sq == Sequence.StartFirst)
sq2 = Sequence.EndFirst;
else if (sq == Sequence.StartLast)
sq2 = Sequence.EndLast;
else if (sq == Sequence.StartDocs)
sq2 = Sequence.EndDocs;
else if (sq == Sequence.StartOnce)
sq2 = Sequence.EndOnce;
else throw ArgumentException("出错类型");
return (new Actuator(im, sq), new Actuator(tm, sq2));
}
// 根据特性创建类型,允许同一个方法多个特性
Actuator[] CreateActuator(MethodInfo methodInfo) {
return methodInfo.GetCustomAttributes(true)
.OfType<JAutoGo>();
.Select(att => new Actuator(methodInfo, att.SequenceId))
.ToArray();
}
/// <summary>
/// 收集特性下的函数
/// </summary>
void GetAttributeFunc() {
// 特性会出现在同一个类中的多个方法,
// 特性下的方法要public,否则就被编译器优化掉了.
// 约束只反射本dll的类
string? dll = null;
if (_constraint) {
var ass = Assembly.GetExecutingAssembly();
dll = Path.GetFileNameWithoutExtension(ass.Location);
}
var ts = AppDomainGetTypes(dll);
#if parallel
System.Diagnostics.Trace.WriteLine("这是一条Trace消息,此时是并行");
var acs = ts.SelectMany(type => type.GetMethods())
.SelectMany(m => CreateActuator(m))
.ToArray();
// 串行加入
acs.ForEach(ac => _set.Add(ac));
#else
var types = ts.ToArray();
for (int i = 0; i < types.Length; i++) {
var mets = types[i].GetMethods();
for (int j = 0; j < mets.Length; j++) {
CreateActuator(mets[j]).ForEach(ac => _set.Add(ac));
}
}
#endif
}
}
线程等待
本文并没有使用,只是测试过程尝试插入任务到主线程.
1,同步上下文System.Threading.SynchronizationContext
因为UI的线程亲和性的原因,必须要创建线程才能修改,
它Post会安全插入UI线程中任务队列,不需要阻塞UI线程.
但是我希望等到某个时机才执行任务.
a,当前没有执行命令中的时机:
那么为什么不发送命令呢?
因为发送命令前需要检查doc是不是空.
那么为什么不用文档事件呢?
因为才发现文档事件可以.
b,初始化后静止的时机:
此时可以嵌入文档栏,但是不能等到界面完成,
因为完成之后插入的其他控件计算是失败的.
但是这个任务似乎也可以在文档事件内进行,
因为文档事件虽然初始化了很多东西,但是没有完成界面计算.
(所以说基本上不需要这个Post任务了????)
2,同步上下文System.Threading.SynchronizationContext
只是为了访问到变量,之后依然有并发问题,
就好像控制台开启两个线程,本身就能访问变量,但依然有并发问题.
a:需要资源的锁,
但是Acad符号表记录我锁什么?没法锁.
b:如果主线程仍然对于资源做出修改呢,
岂不是仍然需要阻塞主线程?
所以说插入任务时机的重要性,要在读取前插入任务.
正确流程:
插入任务T1(读写资源A)--主线程读资源A--主线程写资源A--插入任务T2...
错误流程:
主线程读资源A--插入任务T1(读写资源A)--主线程写资源A--插入任务T2...
那么如何保证呢?
在消息循环内进行判断是否是事务中!!
3,Send和Post有区别,
Send无法使用editor.Getpoint等交互函数,
交互函数也是另外的线程切入,而Send则屏蔽了其他插入.
4,消息循环
a:空闲事件System.Windows.Forms.Application.Idle
b:空闲事件高版本自带Acap.Idle
c:WPF的定时器DispatcherTimer
d:子类化NativeWindow
5,低版本处理
之前测试: 直接运行Acad08 4a会失效,而debug有效,
怀疑1,没有文档首次出现才订阅Idle事件,
此时System.Windows.Forms.Application没有初始化,
自然没有Idle事件,不过也不报错...
怀疑2,之前没有用volatile读取,导致一直是空的上下文.
怀疑3,之前整个流程出错了.
如果几个怀疑都不行,改为子类化NativeWindow
拦截是肯定行.
public static class JAutoTask {
/// <summary>
/// 线程上下文的缓存,一旦不为空就一直不是空的<br/>
/// 使用时出现对话框(例如新建图纸)上下文会是null<br/>
/// 若为null就直接发送任务,会卡死,所以使用前循环判断<br/>
/// </summary>
static System.Threading.SynchronizationContext? _mainContext = null;
// 线程队列(这个容器无序啊!)
static ConcurrentQueue<Action> _postQueue = new();
static ConcurrentQueue<Action> _sendQueue = new();
/// <summary>
/// 把任务发送到主线程,异步
/// </summary>
public static void Post(Action action) {
if(action is null) throw new ArgumentNullException("Post委托参数为空");
_postQueue.Enqueue(action);
}
/// <summary>
/// 把任务发送到主线程,同步
/// </summary>
public static void Send(Action action) {
if(action is null) throw new ArgumentNullException("Send委托参数为空");
_sendQueue.Enqueue(action);
}
// 执行队列任务,并移除已执行的任务
static void Run() {
var context = Volatile.Read(ref _mainContext);
if (context is null)
throw new ArgumentNullException("上下文为空");
Action postAction;
while (_postQueue.Count > 0 && _postQueue.TryDequeue(out postAction)) {
context.Post(state => {
try { postAction.Invoke(); } catch { }
}, null);
}
Action sendAction;
while (_sendQueue.Count > 0 && _sendQueue.TryDequeue(out sendAction)) {
context.Send(state => {
try { sendAction.Invoke(); } catch { }
}, null);
}
}
#if NET45_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
static System.Threading.SynchronizationContext? TrySetContext() {
var context = System.Threading.SynchronizationContext.Current;
Interlocked.CompareExchange(ref _mainContext, context, null);
return Volatile.Read(ref _mainContext);
}
// 放在文档首次出现,form显示后才是前台线程上下文.
public static void Init() {
var context = Volatile.Read(ref _mainContext);
context ??= TrySetContext();
if(context is null) {
// 等待线程上下文首次出现,没有就刷新主界面
Refresh(Acap.MainWindow.Handle);
System.Diagnostics.Trace.WriteLine("1.初始化设置上下文失败");
}
// 消息循环中插入任务
System.Windows.Forms.Application.Idle += (sender, e) => {
var context = Volatile.Read(ref _mainContext);
context ??= TrySetContext();
if(context is null) {
// 等待线程上下文首次出现,没有就刷新主界面
Refresh(Acap.MainWindow.Handle);
System.Diagnostics.Trace.WriteLine("2.消息循环设置上下文失败");
return;
}
// 写入数据库的话,需要阻止:
// 1,文档锁跳过(必跳过)
// 2,一个事务中或者嵌套事务运行中不要插入,事务数/事务栈顶不是空(必跳过).
// 3,一个命令中有多个事务,
// 不要插在两个事务缝隙之中,要跳过命令输入中.
// 透明命令呢?就是能够套入另一个命令中,画直线的时候能够缩放.
// 根据特性,发送到透明命令队列.
// 4,C++/.NET/VB/Lisp执行中要跳过,
// Lisp可以靠事件获取执行时机,发送到透明Lisp队列.
// 其他呢?
// 别人也开线程Post然后直接id.Open的话这种怎么办呢?
// 插进去咯,谁叫它不用事务/文档锁,不然无法约定互斥.
// 文档锁flag执行任务必然跳过锁定,是不是执行任务我也加自动锁呢?
// 5,多个命令的连续编组
// 是否跳过活动编组,仿发送命令这个不需要,
// 是否约定一个每个任务都需要编组?由用户控制.
// 那么不是写入数据库呢?需要设计不同的任务队列.
// 1,其他form(例如登陆对话框)比Acad主界面先弹出?
// 如果是,那么UI上下文将捕获这个form的,那就必须跳过焦点不是主界面的.
// 2,嵌入文档栏需要在Acad主界面完成前,
// 如果跳过doc is null,岂不是不能嵌入了?通过无界面队列完成.
// 3,用户交互中,你总不能断开别人GetPoint/GetString/输入命令中吧..
var dm = Acap.DocumentManager;
// 订阅事件为了获取是否Lisp/cmd中
// 订阅事件之后跳过,下次循环再执行任务
if(Interlocked.CompareExchange(ref _isOnce, 1, 0) == 0) {
dm.DocumentCreated += DmCreated;
dm.DocumentToBeDestroyed += DmDestroyed;
return;
}
var doc = dm.MdiActiveDocument;
if(doc is null) return;
var lockmode = doc.LockMode();
if(!lockmode.HasFlag(DocumentLockMode.None)) return;
if(!lockmode.HasFlag(DocumentLockMode.NotLocked)) return;
if(doc.TransactionManager.TopTransaction is not null) return;
// 活动的编组
// if((int)Acap.GetSystemVariable("Undoctl") == 8) return;
// 关键字输入/ESC可能是""吗?还要靠事件上面的判断?
if(doc.CommandInProgress != "") return;
_docFlagsMap.TryGetValue(doc, out RunFlag ffff);
if (ffff.HasFlag(RunFlag.LispRuning) ||
ffff.HasFlag(RunFlag.CmdRuning)) return;
Run();
};
// 低版本是这个报错吗?
if(!System.Windows.Forms.Application.MessageLoop) {
throw new("3.消息循环还没初始化");
}
}
enum RunFlag : int {
None = 0,
CmdEnded = 1,
CmdRuning = 1 << 1,
CmdCancelled = 1 << 2,
LispEnded = 1 << 3,
LispRuning = 1 << 4,
LispCancelled = 1 << 5,
}
// 首次订阅事件
static int _isOnce = 0;
// 每个文档的是否正在运行Lisp/cmd的标记
static Dictionary<Document, RunFlag> _docFlagsMap = new();
static void DmCreated(object sender, DocumentCollectionEventArgs e) {
var doc = e.Document;
if(_docFlagsMap.ContainsKey(doc)) return;
_docFlagsMap.Add(doc, RunFlag.None);
SubscribeToEvents(doc, false); // 订阅
}
static void DmDestroyed(object sender, DocumentCollectionEventArgs e) {
var doc = e.Document;
if(_docFlagsMap.ContainsKey(doc)) {
SubscribeToEvents(doc, true); // 取消订阅
_docFlagsMap.Remove(doc);
}
}
// 订阅文档事件判断是否执行任务中,例如Lisp
static void SubscribeToEvents(Document doc, bool isUn){
if(isUn){
doc.LispWillStart -= Doc_LispWillStart;
doc.LispEnded -= Doc_LispEnded;
doc.LispCancelled -= Doc_LispCancelled;
doc.CommandWillStart -= Doc_CommandWillStart;
doc.CommandEnded -= Doc_CommandEnded;
doc.CommandCancelled -= Doc_CommandCancelled;
return;
}
doc.LispWillStart += Doc_LispWillStart;
doc.LispEnded += Doc_LispEnded;
doc.LispCancelled += Doc_LispCancelled;
doc.CommandWillStart += Doc_CommandWillStart;
doc.CommandEnded += Doc_CommandEnded;
doc.CommandCancelled += Doc_CommandCancelled;
}
static void Doc_CommandWillStart(object sender, CommandEventArgs e) {
var doc = (Document)sender;
_docFlagsMap[doc] &= ~RunFlag.CmdEnded;
_docFlagsMap[doc] &= ~RunFlag.CmdCancelled;
if(!_docFlagsMap[doc].HasFlag(RunFlag.CmdRuning))
_docFlagsMap[doc] |= RunFlag.CmdRuning;
}
static void Doc_CommandEnded(object sender, CommandEventArgs e) {
var doc = (Document)sender;
_docFlagsMap[doc] &= ~RunFlag.CmdRuning;
_docFlagsMap[doc] &= ~RunFlag.CmdCancelled;
if(!_docFlagsMap[doc].HasFlag(RunFlag.CmdEnded))
_docFlagsMap[doc] |= RunFlag.CmdEnded;
}
static void Doc_CommandCancelled(object sender, CommandEventArgs e) {
var doc = (Document)sender;
_docFlagsMap[doc] &= ~RunFlag.CmdRuning;
// 结束和取消
if(!_docFlagsMap[doc].HasFlag(RunFlag.CmdEnded))
_docFlagsMap[doc] |= RunFlag.CmdEnded;
if(!_docFlagsMap[doc].HasFlag(RunFlag.CmdCancelled))
_docFlagsMap[doc] |= RunFlag.CmdCancelled;
}
static void Doc_LispWillStart(object sender, LispWillStartEventArgs e) {
var doc = (Document)sender;
_docFlagsMap[doc] &= ~RunFlag.LispEnded;
_docFlagsMap[doc] &= ~RunFlag.LispCancelled;
if(!_docFlagsMap[doc].HasFlag(RunFlag.LispRuning))
_docFlagsMap[doc] |= RunFlag.LispRuning;
}
static void Doc_LispEnded(object sender, EventArgs e) {
var doc = (Document)sender;
_docFlagsMap[doc] &= ~RunFlag.LispRuning;
_docFlagsMap[doc] &= ~RunFlag.LispCancelled;
if(!_docFlagsMap[doc].HasFlag(RunFlag.LispEnded))
_docFlagsMap[doc] |= RunFlag.LispEnded;
}
static void Doc_LispCancelled(object sender, EventArgs e) {
var doc = (Document)sender;
_docFlagsMap[doc] &= ~RunFlag.LispRuning;
// 结束和取消
if(!_docFlagsMap[doc].HasFlag(RunFlag.LispEnded))
_docFlagsMap[doc] |= RunFlag.LispEnded;
if(!_docFlagsMap[doc].HasFlag(RunFlag.LispCancelled))
_docFlagsMap[doc] |= RunFlag.LispCancelled;
}
// 刷新窗口
#if NET45_OR_GREATER
[MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
static bool Refresh(IntPtr handle) {
[DllImport("user32.dll", SetLastError = true)]
static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
const int SWP_NOMOVE = 0x0002;
const int SWP_NOSIZE = 0x0001;
return SetWindowPos(handle, IntPtr.Zero, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
}
}
调用
public class TestNewlyThread {
[JAutoGo]
public void CmdStartOnce(Document doc) {
JAutoTask.Init();
NewlyThread(ed => ed.WriteMessage("文档首次打开位置初始化"));
}
// 进程最开始初始化呢?...上下文有没有错误呢?如果有就需要等了.
[JAutoGo(Sequence.StartFirst)]
public void CmdStartFirst() {
JAutoTask.Init();
NewlyThread(ed => ed.WriteMessage("进程最开始初始化"));
}
static Editor? GetEditorValid() {
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
var ed = doc?.Editor;
if (ed is not null
// && Acap.IsQuiescent
// && Utils.IsEditorReady
// && ed.CurrentViewportObjectId != ObjectId.Null
&& ed.IsQuiescent) {
return ed;
}
return null;
}
// 新建一个线程,忙等待
public static void NewlyThread(Action<Editor> action){
// 直接执行
var ed = GetEditorValid();
if(ed is not null) action.Invoke(ed);
// 异步执行
int loopState = 0;
new Thread(() => {
while (Interlocked.CompareExchange(ref loopState, 0, 0) == 0) {
JAutoTask.Post(() => {
var ed = GetEditorValid();
if(ed is null) return;
// 设置为1结束循环,排除其他Post任务执行
if (Interlocked.CompareExchange(ref loopState, 1, 0) == 0)
action.Invoke(ed);
});
if (Interlocked.CompareExchange(ref loopState, 0, 0) == 0)
Thread.Sleep(100);
}
}).Start();
}
}
调用
接口例子
派生类继承 IAutoGo
接口,然后实现下面三个方法.
namespace JoinBox;
public class Test : IAutoGo {
public Sequence SequenceId() {
// 最先,最好只使用一次,写注册表+设置信任路径
// 其余使用 Last 或者 Once
return Sequence.StartFirst;
}
// 构造函数
public Test() {
}
public void Initialize() {
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
doc?.Editor.WriteMessage("接口初始化是StartFirst 注册表加载不会打印输出\n\r");
}
// 关闭时候执行
// 此处用于释放安全退出,例如卸载注册表/跨进程通讯
// 用户直接关闭CAD进程,此处是不会执行的,所以是弱执行
public void Terminate() {
// CAD主窗口已经回收(打印无效)
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
doc?.Editor.WriteMessage("执行释放");
MessageBox.Show("执行释放");
// VS输出窗口打印(打印成功)
Debug.WriteLine("执行释放");
}
}
特性例子
如果你觉得接口很麻烦,那么就用特性吧,
存在启动的两种模式
问题已经被分解成不同的参数上面处理.
namespace JoinBox;
public class TestClass {
[JAutoGo(Sequence.StartFirst)]
[JAutoGo(Sequence.StartLast)]
public void ProcessInitialize() {
Debug.WriteLine("用于进程初始化时候执行");
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
doc?.Editor.WriteMessage("特性初始化是StartFirst/Last 注册表加载不会打印输出\n\r");
}
// 无论何种加载方式,此处doc肯定不为空,可以打印输出
[JAutoGo]
public void DocumentFirstOpen(Document doc) {
Debug.WriteLine("JAutoGo文档首次开启" + doc.Name);
doc.Editor.WriteMessage("无论何种加载方式都能输出: 惊惊博客https://www.cnblogs.com/JJBox \n\r");
}
[JAutoGo(Sequence.StartDocs)]
public void DocumentEveryOpen(Document doc) {
Debug.WriteLine("文档每次开启" + doc.Name);
}
[JAutoGo(Sequence.EndDocs)]
public void DocumentEveryClosed(Document doc) {
Debug.WriteLine("文档每次关闭" + doc.Name);
}
[JAutoGo(Sequence.EndOnce)]
public void DocumentLastClosed(Document doc) {
Debug.WriteLine("文档最后关闭" + doc.Name);
}
[JAutoGo(Sequence.EndLast)]
public void ProcessTerminate() {
// CAD主窗口已经回收(打印无效)
var dm = Acap.DocumentManager;
var doc = dm.MdiActiveDocument;
doc?.Editor.WriteMessage("进程关闭时");
MessageBox.Show("进程关闭时");
// VS输出窗口打印(打印成功)
Debug.WriteLine("进程关闭时");
}
}
定义命令和检测重复
因为命令定义有几种方式:
1,命令特性定义
如果你在同一个dll中写了两个名称相同的命令,
Acad只会Editor上面输出异常信息"无法加载程序集...",
而没有提示冲突的命令名称.
我们利用初始化运行找它出来,
不过我不建议大家用这个了,因为还可以用其他定义方式.
2,自定义命令特性
然后通过Commands.Add(),不过Acad08没有这个函数.
自定义命令特性AOP切面
3,动态编译
本质上也是利用命令特性,只是Acad08的补充方案.
4,空闲事件
通过键盘钩子拦截命令栏(输入法切换工程),
检测空格和回车键认为发送命令(注意用户输入字符串中的空格要跳过),
再调用JAutoTask.Post发送到空闲事件上面.
namespace JoinBox;
public class CmdCheckHelper {
// 命令和类名
struct CommandInfo {
public string GlobalName;
public string FullName;
}
[JAutoGo]
public void CmdCheck(Document doc) {
// 获取所有相关命令信息,反射程序集全部dll
var commandInfos = AutoClass.AppDomainGetTypes()
.SelectMany(type =>
type.GetMethods()
.SelectMany(method =>
method.GetCustomAttributes(true)
.OfType<CommandMethodAttribute>()
.Select(att => new CommandInfo {
GlobalName = att.GlobalName,
FullName = type.FullName + "." + method.Name
}))).ToList();
// 命令分组并统计重复,记录类名
var map = commandInfos.GroupBy(info => info.GlobalName)
.Where(group => group.Count() > 1) // 排除不重复的
.ToDictionary(group => group.Key,
group => (group.First().FullName, group.Count()));
if (map.Count() == 0) return;
doc.Editor.WriteMessage("多个DLL中存在重复命令:");
foreach (var kvp in map) {
doc.Editor.WriteMessage($"命令:{kvp.Key},重复次数:{kvp.Value.Item2},位于:{kvp.Value.Item1}\n");
}
}
}
注册表加载
namespace JoinBox;
public class JAutoRegisterHelper {
// 获取AutoCAD的Applications键
// Acad2012版 RegistryProductRootKey属性
// Acad2014版 MachineRegistryProductRootKey属性
// string _key = HostApplicationServices.Current.MachineRegistryProductRootKey;
#if NET35
string _key = HostApplicationServices.Current.RegistryProductRootKey; //这里浩辰读出来是""
#else
string _key = HostApplicationServices.Current.UserRegistryProductRootKey;
#endif
const string _applications = "Applications";
const string _appName = nameof(JAutoRegister);
[JAutoGo(Sequence.StartFirst)]
[CommandMethod(nameof(JAutoRegister))]
public void JAutoRegister() {
// 信任加载
// Acad年份获取 https://www.cnblogs.com/JJBox/p/18511807
if (VersionTool.CurrentYear >= 2014){
Acap.SetSystemVariable("secureload", 0); // Acad2014加载Lisp不警告
}
Acap.SetSystemVariable("acadlspasdoc", 1); // 将acad.lsp文件加载到每一个文档
using RegistryKey regAcadProdKey = Registry.CurrentUser.OpenSubKey(_key);
if(regAcadProdKey is null) return;
using RegistryKey regAcadAppKey = regAcadProdKey.OpenSubKey(_applications, true);
if(regAcadAppKey is null) return;
// 检查键是否存在,如果存在则更新路径,那就不检查得了
// string[] subKeys = regAcadAppKey.GetSubKeyNames();
// var k = subKeys.FirstOrDefault(a=> a == _appName);
// if (k is not null) return;
// 获取本模块的位置
string dllFile = Assembly.GetExecutingAssembly().Location;
// 注册插件
using RegistryKey reg = regAcadAppKey.CreateSubKey(_appName);
// 描述,可选
reg.SetValue("DESCRIPTION", _appName, RegistryValueKind.String);
// 控制加载时机
// 01,检测到代理对象时加载插件
// 02,启动时加载插件
// 04,启动一个命令时加载插件
// 08,用户或其它插件请求时加载插件
// 16,不加载插件
// 32,透明加载插件
reg.SetValue("LOADCTRLS", 2, RegistryValueKind.DWord);
// 加载完整路径和文件名
reg.SetValue("LOADER", dllFile, RegistryValueKind.String);
// 键值1是.NET程序集文件
reg.SetValue("MANAGED", 1, RegistryValueKind.DWord);
}
[CommandMethod(nameof(JAutoUnRegister))]
public void JAutoUnRegister() {
using RegistryKey regAcadProdKey = Registry.CurrentUser.OpenSubKey(_key);
if(regAcadProdKey is null) return;
using RegistryKey regAcadAppKey = regAcadProdKey.OpenSubKey(_applications, true);
if(regAcadAppKey is null) return;
var names = regAcadAppKey.GetSubKeyNames();
if (names.Contains(_appName)) {
regAcadAppKey.DeleteSubKeyTree(_appName);
}
}
}