cad.net 事务栈
事务栈
设计概念
事务栈主要是作为一个代理类来保证流程的顺序正确,
不耦合官方的API,做一个代理类是很正常的,
例如中望API缺失,而你的下游代码又不想一个个预处理,就可以通过代理类提供虚假API先.
它才13个指针(9个符号表+4个本类)+1个表记录字典,不存在性能瓶颈,
而且是符号表是惰性求值的,优化也只是改为结构体放到非托管堆,而不是放弃事务栈结构.
它极大方便取值了呀.
函数多次跳转这种开销是可以通过内联标记进行优化的.
目的是多种场景的异常提示,避免陷入不知情的分析.
无论如何你都需要看一次事务栈内部做了什么,
因为它处理的问题非常多,它的复杂性是根据业务场景而来,
你裸写这些业务场景很大程度上面能够避免问题,
但是一旦遇到问题,你可能需要重构整个工程的全部分支,
例如DBTrans.Task
它究竟在规避什么问题.
和原生事务一样,事务栈也保证能够嵌套事务.
只是不推荐,为什么呢?见调用例子就知道了.
我们没有从静态字段直接取出时候创建,为什么不做呢?
因为new就是创建,Top就是栈顶取出,
免得Top出现new来,语义层面要合理.
使用的时候,命令上面new一次,其他子函数全部是Top.
public class TestCommands {
[CommandMethod(nameof(TestDBTrans))]
public void TestDBTrans() {
// 创建事务栈,using在函数最后默认提交和释放.
// 默认构造函数获取当前文档,因为执行命令必然有当前文档.
using DBTrans tr = new();
// 不需要传事务给子函数.
SubFunc();
}
private void SubFunc() {
// 从事务栈顶获取当前事务
var tr = DBTrans.Top;
// 验证ForEach函数,它得到id.IsOk的
tr.BlockTable.ForEach(record => {
tr.Editor?.WriteMessage(record.Id.ToString());
});
// 验证迭代器接口,它没有过滤任何id.
foreach(var id in tr.BlockTable) {
tr.Editor?.WriteMessage(id.ToString());
}
// 获取ids略...
foreach(var id in ids) {
// 因为Acad的图元读取出来是放在非托管堆和CPP交互的,
// 需要using约束图元生命周期,通过作用域释放对象,避免遗忘释放,例如面域就会打印信息.
// 通过强转避免可空类型空值传导,也就是明确不为空.
using var ent = (Entity)tr.GetObject(id);
// 如果需要判断类型用is,而不是as,is还是指令来着.
if (ent is BlockReference brf) {
}
}
}
}
任何静态字段本质上是跨文档,但是文档不一定存在(后台开图),
所以我们是用字典进行跨数据库,通过_dBTrans[db]来获取缓存的信息,
这样前台或者后台数据库都能索引了.
扩展功能
后台常开多个数据库,用于提取数据,怎么找到后台已经打开的数据库呢?
靠遍历文档集合是不行的,因为后台打开压根没有加入文档呀.
遇到此场景,调用者自行创建一个fdbMap<file, db>
,
再通过DBTrans.GetTop(db)
获取事务,两次O(1)寻址就能得到事务.
各种寻址:
文件找文档 fdocMap<file, doc>
文件找数据库 fdbMap<file, db> 后台db没有doc需要独立建立映射表
文件找事务
var db = fdbMap[file];
var tr = DBTrans.GetTop(db);
DBTrans.TryGetTop(db, out var tr);
文档找数据库 doc.Database
文档找文件 doc.Name
数据库找文档 Acaop.DocumentManager.GetDocument(db);
数据库找文件 db.Filename
数据库找事务
var tr = DBTrans.GetTop(db);
DBTrans.TryGetTop(db, out var tr);
工作数据库和当前文档数据库是不一样的,
场景: 后台开图或者创建数据库,后台文件切换布局.
要设置工作数据库才能访问布局管理器,它是CAD提供的唯一通道.
否则无法获取和设置布局数据.
当切换布局之后,保存事务前需要恢复为旧工作数据库,否则会致命错误.
原因是每次提交都表示你要关闭,
而关闭后工作数据库仍然显示了释放的数据库,这肯定不对了.
我加在了提交事务上面的报错.
原生事务
常规事务:
从db寻址,我记得doc也有,它还是父类.
var tr = db.TransactionManager.StartTransaction();
无撤事务: StartOpenCloseTransaction
它是常规事务派生,重写了很多,
官方资料说它是无撤回视图的事务.
var doc = Acaop.DocumentManager.MdiActiveDocument;
using var tr = doc.TransactionManager.StartOpenCloseTransaction();
不过也有人说它报错位置很多,
布局管理器不能用....没有测试过,怀疑
而且无论前台还得后台取值和修改必须要经过工作数据库
https://www.cnblogs.com/JJBox/p/18697838
网上一些资料不对,它没有Session相关的函数.
替换句柄需要用 StartOpenCloseTransaction()
https://www.cnblogs.com/JJBox/p/12489648.html
兜底策略
1,原生事务原理
对象开启是会放在非托管堆(和Arx交互),再通过事务容器进行释放对象.
这个容器在tr.GetAllObjects()
中可以看见,
少量对象,例如"面域"是绕过此机制的.
2,提权和降权
Acad偷懒,任何提权过的对象都不会降权,即使你已经使用了降权语句.
所以先读模式打开,再提权写入,避免事务崩溃.
https://www.cnblogs.com/JJBox/p/10798940.html
但是基于对称设计来说,我认为还是要保留using(obj.ForWrite())
只是事务栈和符号表模板内部采用最轻方式实现.
3,事务崩溃
事务容器存放超过一百万条多段线数据量会事务崩溃,
所以尽可能不要依赖兜底策略释放,
因此才有tr.GetObject()
使用using原则.
如果你要添加超过一百万根多段线,要分多个事务进行.
4,若我们的事务栈的tr.GetObject()
方法中
也加入一个HashSet<DBObject>
进行兜底,岂不是连同"面域"也能释放?
不过良好编程习惯还是建议我们不要这样做,虽然可以但不做.
这就是为什么有代理类,
我既可以加兜底释放,也可以不加,还可以有限度的加,下游代码修改极少.
你可以说是前期设计缺失,但是后期遇到BUG,
内部更改不至于推翻重建,正所谓人无完人,先加中间层,再考虑未来.
5,由于存在事务崩溃,那么如何避免呢?
方案a:控制对象数量: 1w个以内就提交多次事务.
方案b:自己实现事务
自己实现事务(不完美)
// 缓存弄成并发容器?
// obj.IsNewlyObject 表示新创建图元
// obj.IsWriteEnable 提权标记,
// 一旦提权会自动克隆对象创建副本,
// 然后修改的是副本,否则直接修改原生对象,
// 然后就无法用Abort()撤回事务,
// 提交事务就是通过替换句柄的方式进行,表示设置到数据库.
public class DBTrans {
// 构造函数要加编组开始标记
DBTrans() {
CadSystem.Undo(true);
}
// 1,读取数据库的对象,兜底释放容器<id,旧对象>
Dictionary<ObjectId, DBObject> _readObjectMap = new();
// 要不要区分呢?例如刷新肯定是图元
// Dictionary<ObjectId, Entity> _readEntityMap = new();
// 2,新建对象
HashSet<DBObject> _createSet = new();
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DBObject GetObject(ObjectId id, OpenMode openMode = OpenMode.ForRead,
bool openErased = false, bool openLockedLayer = false) {
// 此方式打开自己持有对象
if (!_readObjectMap.TryGetValue(id, out var obj)) {
obj = id.Open(openMode, openErased, openLockedLayer);
_readObjectMap.Add(id, obj);
}
// 要求提权
if (openMode == OpenMode.ForWrite && !obj.IsWriteEnable)
obj.UpgradeOpen();
return obj;
}
/// <summary>
/// 将新创建对象加入或移出事务
/// <para>不是新创建对象移出会异常:eNotNewlyCreated</para>
/// </summary>
/// <param name="obj">新对象(旧对象被加入是拒绝)</param>
/// <param name="add">true是加入事务,false是移出事务</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddNewlyCreatedDBObject(DBObject obj, bool add = true) {
if (!obj.IsNewlyObject)
throw new("eNotNewlyCreated");
if (add) _createSet.Add(obj);
else _createSet.Remove(obj);
}
/// <summary>
/// 提交事务
/// </summary>
public void Commit() {
// 替换句柄,也就是id指向克隆对象,
// 旧对象被替换了,是否应该释放呢?非托管内存不知道是不是缓冲区.
// 缓冲区就释放,不是缓冲区就表示数据库会原地固定.
// obj.Dispose()内部根据是否存在database来释放非托管资源.
// 直接id.Open的是不是不需要StartOpenCloseTransaction打开就能替换呢?
using var tr = _database.TransactionManager.StartOpenCloseTransaction();
foreach(var obj in _readObjectMap.Values) {
if (obj.IsWriteEnable) {
var cloneObj = obj.Clone();
obj.HandOverTo(cloneObj, true, true);
tr.AddNewlyCreatedDBObject(cloneObj, true);
}
obj.Dispose();
}
foreach(var obj in _createSet) {
tr.AddNewlyCreatedDBObject(obj, true);
}
tr.Commit();
_readObjectMap.Clear();
_createSet.Clear();
CadSystem.Undo(false);
}
// ...撤回视图信息,系统变量,回滚标记undo等等.
/// <summary>
/// 撤回事务
/// </summary>
public void Abort() {
foreach(var obj in _readObjectMap.Values)
obj.Dispose();
foreach(var obj in _createSet)
obj.Dispose();
_readObjectMap.Clear();
_createSet.Clear();
// 发送命令进行回滚,那么后台又如何回滚呢?
CadSystem.Undo(false);
Document?.SendStringToExecute("_u\n", false, false, false);
}
// 刷新是根据缓存进行的,可能是触动刷新消息循环之类的...
public void QueueForGraphicsFlush() {
var move0 = Matrix3d.Displacement(new Vector3d(0,0,0));
var allEntities = _readObjectMap.Values
.Concat(_createSet)
.OfType<Entity>();
foreach(var ent in allEntities) {
// 图元重绘,删除的绘制就是清理界面
ent.Draw();
if (ent.IsDisposed || ent.IsErased) continue;
// move0要提权才有效果,不然也不报错
if (!ent.IsWriteEnable)
ent.UpgradeOpen();
ent.TransformBy(move0);
// 图块刷新
ent.RecordGraphicsModified(true);
if (ent is BlockReference brf) {
// 更新动态块
var btr = (BlockTableRecord)tr.GetObject(brf.BlockTableRecord, OpenMode.ForWrite);
btr.UpdateAnonymousBlocks();
}
else if (ent is Dimension dim) dim.RecomputeDimensionBlock(true);
else if (ent is Viewport vp) vp.UpdateDisplay();
}
// 底层差不多ed.UpdateScreen();
Acap.UpdateScreen();
// acad2014及以上要加,立即处理队列上面的消息
System.Windows.Forms.Application.DoEvents();
}
}
这样提交的时候就知道谁改过了,不过由于实在复杂了,
本次事务栈还是利用Acad自己的事务机制,避免出错.
未解决:后台又如何回滚呢?
https://www.cnblogs.com/JJBox/p/10214582.html
和现有IFox0.9和1.0区别:
1,文档和命令行字段改为属性,不缓存了,二次寻址方式获取.
2,_fileName字段没有存在必要了,释放后台打开的可以用doc is null判断.
3,将提交和释放标记合并.
4,事务栈改为map,一个数据库一个事务,避免同栈切换数据库弹出出错.
5,提供两个静态函数用于检索数据库是否已经加入事务栈.
6,拆开后台和前台两个打开方式,具体见代码内部注释.
7,Task函数的参数改为回声提示.
8,说明工作数据库和当前文档数据库的区别.
9,加入1.0的获取句柄
10,提交事务前检查工作数据库报错,避免直接致命错误后不知道意义.
11,超多的积极内联标记.
12,Top直接指向GetTop()函数.
13,修正了抛错误加ex和不加,两种模式是不一样的.
14,构造函数添加对OCT的支持,增加嵌套OTC事务函数,打开文件增加OCT的支持.
15,强迫症式函数名.
16,和第6点关联,发现引发致命错误的逻辑
a:文件打开错误. b:Session标记缺失设置.
那么把这些作为静态函数打开文件,而不是构造函数,
那么Dispose释放时候就不再需要判断db和tr为空了,
IDE也不会绿色提示了.
17,想了一下还是制造一个字典缓存事务栈自己打开表记录,
使得取值之后能够本类回收,起码出现释放出错的时候会提示自己,
而不是由原生事务弹出不知道意义的提示.
代码
namespace IFoxCAD.Cad;
using System.Diagnostics;
using System.IO;
using Exception = System.Exception;
using Acaop = Application;
#if NET45_OR_GREATER
using System.Runtime.CompilerServices;
#else
// 以下就做一个假的内联标记,避免每个特性都要预处理.
// 让我们偷点编译器代码.
// https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/MethodImplAttribute.cs
[Flags]
public enum MethodImplOptions {
Unmanaged = 0x0004, // 指定方法使用非托管调用约定
NoInlining = 0x0008, // 阻止方法内联
ForwardRef = 0x0010, // 表示方法是一个正向引用
Synchronized = 0x0020, // 指示方法是同步的
NoOptimization = 0x0040, // 禁用方法的优化
PreserveSig = 0x0080, // 指示方法签名在跨语言调用时应保持不变
AggressiveInlining = 0x0100, // 强制方法尽可能内联
AggressiveOptimization = 0x0200, // 绕过分层编译的动态PGO优化
InternalCall = 0x1000 // 表示方法是一个内部调用
}
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Constructor, Inherited = false)]
public sealed class MethodImplAttribute : System.Attribute {
// public MethodCodeType MethodCodeType;
public MethodImplOptions Value { get; }
public MethodImplAttribute() { }
public MethodImplAttribute(MethodImplOptions methodImplOptions) {
Value = methodImplOptions;
}
public MethodImplAttribute(short value) {
Value = (MethodImplOptions)value;
}
}
#endif
/// <summary>
/// 事务栈
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
[DebuggerTypeProxy(typeof(DBTrans))]
public sealed class DBTrans : IDisposable {
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private string DebuggerDisplay => ToString();
#region 公共静态函数
/// <summary>
/// 获取原生事务栈顶
/// </summary>
/// <param name="database">数据库</param>
/// <returns>原生事务</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Transaction? GetTopTransaction(Database db)
{
var tr = db?.TransactionManager.TopTransaction;
if (tr is null) throw new ArgumentNullException(nameof(DBTrans), $"此数据库{db}没有原生事务");
return tr;
}
/// <summary>
/// 获取栈顶事务
/// </summary>
/// <param name="db">数据库,默认是工作数据库</param>
/// <returns>事务对象</returns>
/// <exception cref="ArgumentNullException"></exception>
public static DBTrans GetTop(Database? db = null)
{
// 工作数据库和当前文档数据库是不一样的.
// 例如布局管理器是经过工作数据库取值的,我们事务栈也是如此.
// 而默认构造函数则以当前文档创建.
// 工作文档是空的场景: 全部文档关闭,后台打开图纸,
// 要WorkingDatabase=后台db,并且提交事务前恢复原本.
db ??= HostApplicationServices.WorkingDatabase;
if (db is null)
throw new ArgumentNullException(nameof(DBTrans), $"工作数据库为空,后台调用需先设置或调用Task函数");
if (_dBTrans.Count == 0)
throw new ArgumentNullException(nameof(DBTrans), $"调用前必须创建事务栈");
if (!_dBTrans.TryGetValue(db, out var trStack))
throw new ArgumentNullException(nameof(DBTrans), $"此数据库{db}没有加入事务栈");
return trStack.Peek();
}
/// <summary>
/// 获取栈顶事务
/// </summary>
public static DBTrans Top => GetTop();
// 提供一个查询不报异常的,用于后台常开的dbMap检索用
/// <summary>
/// 尝试获取栈顶事务
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool TryGetTop(Database db, out DBTrans dBTrans)
{
if (_dBTrans.TryGetValue(db, out var trStack)) {
dBTrans = trStack.Peek();
return true;
}
dBTrans = null!;
return false;
}
/// <summary>
/// 事务栈是否已经包含此数据库
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool Has(Database db)
{
return _dBTrans.ContainsKey(db);
}
/// <summary>
/// 设置工作数据库
/// </summary>
public static DBTrans SetWorking(Database db)
{
if (db is null)
throw new ArgumentNullException(nameof(db));
HostApplicationServices.WorkingDatabase = db;
}
/// <summary>
/// 隐式转换为原生事务
/// </summary>
/// <param name="tr">事务栈</param>
/// <returns>原生事务</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Transaction(DBTrans tr) => tr._transaction;
#endregion
#region 静态资源
// 静态资源是不算到类大小的.
/// <summary>
/// 事务栈
/// </summary>
private static readonly Dictionary<Database, Stack<DBTrans>> _dBTrans = new();
#endregion
#region 本类字段
/// <summary>
/// 文档锁
/// </summary>
private readonly DocumentLock? _documentLock;
// 既然可以通过隐式转换,那么就私有它
// 例如: Transaction tr = DBTrans.Top;
/// <summary>
/// 原生事务
/// </summary>
private readonly Transaction _transaction;
/// <summary>
/// 数据库
/// </summary>
private readonly Database _database;
/// <summary>
/// 提交事务和释放标记
/// </summary>
private TransStatus _transStatus = new();
#endregion
#region 公开属性
/// <summary>
/// 文档
/// </summary>
public Document? Document => Acaop.DocumentManager.GetDocument(_database);
/// <summary>
/// 命令行
/// </summary>
public Editor? Editor => Document?.Editor;
/// <summary>
/// 数据库
/// </summary>
public Database Database => _database;
#endregion
#region 构造函数
/// <summary>
/// 事务栈
/// <para>默认构造函数,默认为打开当前文档,默认提交事务</para>
/// </summary>
/// <param name="doc">要打开的文档</param>
/// <param name="commit">事务是否提交</param>
/// <param name="docLock">是否锁文档</param>
/// <param name="openCloseTrans">无撤事务</param>
public DBTrans(Document? doc = null, bool commit = true,
bool docLock = false, bool openCloseTrans = false)
{
doc ??= Acaop.DocumentManager.MdiActiveDocument;
if (docLock && doc.LockMode(false) == DocumentLockMode.NotLocked)
_documentLock = doc.LockDocument();
_database = doc.Database;
CheckDatabaseError();
var tm = _database.TransactionManager;
if (openCloseTrans) _transaction = tm.StartOpenCloseTransaction();
else _transaction = tm.StartTransaction();
if (commit) _transStatus.Commit();
if (!_dBTrans.TryGetValue(_database, out var trStack)) {
trStack = new();
_dBTrans.Add(_database, trStack);
}
trStack.Push(this);
}
/// <summary>
/// 事务栈
/// <para>打开数据库,默认提交事务</para>
/// </summary>
/// <param name="db">要打开的数据库</param>
/// <param name="commit">事务是否提交</param>
/// <param name="openCloseTrans">无撤事务</param>
public DBTrans(Database db, bool commit = true, bool openCloseTrans = false)
{
_database = db;
CheckDatabaseError();
var tm = _database.TransactionManager;
if (openCloseTrans) _transaction = tm.StartOpenCloseTransaction();
else _transaction = tm.StartTransaction();
if (commit) _transStatus.Commit();
if (!_dBTrans.TryGetValue(_database, out var trStack)) {
trStack = new();
_dBTrans.Add(_database, trStack);
}
trStack.Push(this);
}
// 嵌套此事务是可以替换句柄的
// https://www.cnblogs.com/JJBox/p/12489648.html
/// <summary>
/// 无撤事务
/// <para>此处提交事务,外层可以进行回滚</para>
/// </summary>
/// <param name="action">无撤事务</param>
/// <param name="commit">是否提交</param>
public void StartTransaction(Action<OpenCloseTransaction> action, bool commit = true) {
if (action is null) throw new ArgumentNullException(nameof(action));
using var tr = _database.TransactionManager.StartOpenCloseTransaction();
action.Invoke(tr);
if (commit) tr.Commit();
}
/*
0x01,前台开图创建文档,记录doc的.
前台打开会被文档持有,是无法释放db的,并且需要发送命令保存和关闭.
后台开图或者后台创建数据库,不记录doc的,
所以doc is null也能保证是后台开的,用于释放后台打开的db,
所以我删掉file字段了,
并且移除了保存方式到外部,它就更加没有存在价值了.
0x02,原本参数有file的构造函数存在问题
21,有同名file已经加入事务栈,表示已经开过,
但是在此无论如何都会再次开图,因为构造函数必然是构造有效对象.
所以构造函数无法避免此类情况,
调用者想要避免此场景,要自己去构造 fdbMap<file, db>,
22,文件已经在前台打开(文档集合已经持有),但是通过file参数要求后台打开.
出现后台只读场景,功能上是可行的,但是从业务上来说是矛盾的,
你可以直接用前台文档进行呀,因此我删掉了此构造函数,
分解成 后台打开 和 前台打开 两个静态函数,
让调用者自行规避或明知而为.
0x03,当前进程前台已经打开,通过文档集合判断.
0x04,当前进程后台已经打开呢?
你无法通过遍历文档集合得到,它压根不加入文档集合,
因此和21规避同名file一样,自行构造后台 fdbMap<file, db>,
再通过DBTrans.GetTop(db)得到事务,两个O(1)检索就得到了.
41,通常后台处理完就关闭了,
所以改为静态读取文件后加入事务栈,完成就提交释放,
为了便利性,期间会一直持有事务直到提交后自动关闭数据库.
42,后台常开去拷贝数据呢?多次提交事务呢?是可能的.
所以提供静态创建数据库函数,不加入事务栈.
由调用者自行持有.
0x05,前台开图必须设置: CommandFlags.Session 标记
前台开图如果用命令,不设置标记的话就会卡死.
如何判断调用的函数位置是Session呢?
目前是让调用者自己肉眼保证,没有报错机制.
感觉不可行的方案:
拦截运行时的命令,获取输入中的命令,然后反射全部命令,
因为事件也是Session环境,也可以调用事务栈,但是此时没有命令.
并且还不一定用桌子的命令特性定义命令.
*/
/// <summary>
/// 后台打开文件并加入事务栈
/// <para>默认提交事务</para>
/// <para>后台打开后执行任务需要用Task函数保证工作数据库正确</para>
/// </summary>
/// <param name="file">要打开的文件</param>
/// <param name="commit">事务是否提交</param>
/// <param name="fileOpenMode">开图模式</param>
/// <param name="password">密码</param>
/// <param name="openCloseTrans">无撤事务</param>
/// <exception cref="FileNotFoundException"></exception>
public static DBTrans OpenPushToBackend(string file, bool commit = true,
FileOpenMode fileOpenMode = FileOpenMode.OpenForReadAndWriteNoShare,
string? password = null, bool openCloseTrans = false)
{
if (string.IsNullOrWhiteSpace(file))
throw new FileNotFoundException(nameof(OpenPushToBackend), "文件后缀不是dxf/dwg");
Database? db = null;
// 排除只读,因为磁盘文件可能已经释放可以可写打开.
// doc.Name: "D:\\JX.dwg" 比较时候需要一致
file = file.Replace("/", "\\");
var fdocMap = FileDocumentMap();
if (fdocMap.TryGetValue(file, out var doc)) {
db = doc.Database;
}
// 此处创建的数据库是肯定获取不到文档的.
db ??= OpenFileForBackend(file, fileOpenMode, password);
return new DBTrans(db, commit, openCloseTrans : openCloseTrans);
}
/// <summary>
/// 前台打开文件并加入事务栈
/// <para>默认提交事务,需要设置CommandFlags.Session</para>
/// </summary>
/// <param name="file">要打开的文件</param>
/// <param name="commit">事务是否提交</param>
/// <param name="fileOpenMode">开图模式</param>
/// <param name="password">密码</param>
/// <param name="openCloseTrans">无撤事务</param>
/// <exception cref="FileNotFoundException"></exception>
public static DBTrans OpenPushToFrontend(string file, bool commit = true,
FileOpenMode fileOpenMode = FileOpenMode.OpenForReadAndWriteNoShare,
string? password = null, bool openCloseTrans = false) {
if (string.IsNullOrWhiteSpace(file))
throw new FileNotFoundException(nameof(OpenPushToFrontend), "文件后缀不是dxf/dwg");
if (!File.Exists(file))
throw new FileNotFoundException(nameof(OpenPushToFrontend), "文件必须存在");
// 排除只读,因为磁盘文件可能已经释放可以可写打开.
// doc.Name: "D:\\JX.dwg" 比较时候需要一致
file = file.Replace("/", "\\");
var fdocMap = FileDocumentMap();
if (!fdocMap.TryGetValue(file, out var doc)) {
doc ??= OpenFileForFrontend(file, fileOpenMode, password);
}
// 前台进行同步激活文档和工作数据库
// 后台为什么不设置呢?因为后台需要还原,
// 因此不在打开时候修改工作数据库,而是制作Task函数进行.
// 而前台由文档持有,并通过它自己的机制保证关闭时候切换.
var dm = Acaop.DocumentManager;
if (!doc.IsActive) {
dm.MdiActiveDocument = doc;
HostApplicationServices.WorkingDatabase = doc.Database;
}
// file是当前文档就不用加锁,但是命令有Session就要加文档锁.
// 非当前文档要激活切换,此时必然是跨文档,也要加文档锁.
// 因此需要统一命令加上Session并且加文档锁.
// 否则 Editor?.Redraw() tm.QueueForGraphicsFlush() 将报错提示文档锁
return new DBTrans(doc, commit, true);
}
// 若命令没有设置 CommandFlags.Session
// Open会卡死进入中断状态不会执行打开,
// 直到切换文档ctrl+tab或者关闭文档,
// 并且doc.IsActive会异常
// 若是构造函数出错,导致Dispose函数出现需要判断本类资源为空场景.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Document OpenFileForFrontend(string file, FileOpenMode fileOpenMode, string password) {
try {
var dm = Acaop.DocumentManager;
return dm.Open(file,
fileOpenMode == FileOpenMode.OpenForReadAndReadShare,
password);
} catch(Exception ex)
throw;
}
}
// 文件不存在,创建数据库,允许之后保存.
// 文件存在,前台文档集合没找到,后台开图.
// 文件存在,前台文档集合找到,后台只读打开,诡异的业务场景,会致命错误吗?
// Acad08测试: new Database()第2个参数使用false时,
// 将导致关闭cad的时候出现致命错误:
// Unhandled Access Violation Reading Ox113697a0 Exception at 4b4154h
// 报错: ePermanentlyErased 就是new Database()参数写错了.
// 报错: eFileSharingViolation 他人在打开此图.
// 报错: eWrongObjectType 表示你没有用Task函数包裹.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Database OpenFileForBackend(string file, FileOpenMode fileOpenMode, string password)
{
if (!File.Exists(file))
return new Database(true, true);
var db = new Database(false, true);
var ext = Path.GetExtension(file);
if (string.Equals(ext, ".dwg", StringComparison.OrdinalIgnoreCase))
db.ReadDwgFile(file, fileOpenMode, true, password);
else if (string.Equals(ext, ".dxf", StringComparison.OrdinalIgnoreCase))
db.DxfIn(file, null);
else throw new FileNotFoundException(nameof(OpenFileForBackend), $"文件后缀不是dxf/dwg: {ext}");
// 读取文件就断开连接,释放磁盘占用,避免其他不能读取和删除.
db.CloseInput(true);
return db;
}
// 为了方便内联,这个函数还是写轻松点,大不了其他人重写一份
/// <summary>
/// 前台文件路径和文档映射表
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Dictionary<string, Document> FileDocumentMap() {
var dm = Acaop.DocumentManager;
var map = new Dictionary<string, Document>();
foreach (Document doc in dm) {
if (doc.IsDisposed || doc.IsReadOnly) continue;
if (!map.ContainsKey(doc.Name))
map.Add(doc.Name, doc);
}
return map;
}
// 后台可以之后加入
/// <summary>
/// 前台文件路径和数据库映射表
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Dictionary<string, Database> FileDatabaseMap() {
var dm = Acaop.DocumentManager;
var map = new Dictionary<string, Database>();
foreach (Document doc in dm) {
if (doc.IsDisposed || doc.IsReadOnly) continue;
if (!map.ContainsKey(doc.Name))
map.Add(doc.Name, doc.Database);
}
return map;
}
#endregion
#region 符号表
/// <summary>
/// 块表
/// </summary>
public SymbolTable<BlockTable, BlockTableRecord> BlockTable =>
_blockTable ??= new(this, _database.BlockTableId);
private SymbolTable<BlockTable, BlockTableRecord>? _blockTable;
/// <summary>
/// 层表
/// </summary>
public SymbolTable<LayerTable, LayerTableRecord> LayerTable =>
_layerTable ??= new(this, _database.LayerTableId);
private SymbolTable<LayerTable, LayerTableRecord>? _layerTable;
/// <summary>
/// 文字样式表
/// </summary>
public SymbolTable<TextStyleTable, TextStyleTableRecord> TextStyleTable =>
_textStyleTable ??= new(this, _database.TextStyleTableId);
private SymbolTable<TextStyleTable, TextStyleTableRecord>? _textStyleTable;
/// <summary>
/// 注册应用程序表
/// </summary>
public SymbolTable<RegAppTable, RegAppTableRecord> RegAppTable =>
_regAppTable ??= new(this, _database.RegAppTableId);
private SymbolTable<RegAppTable, RegAppTableRecord>? _regAppTable;
/// <summary>
/// 标注样式表
/// </summary>
public SymbolTable<DimStyleTable, DimStyleTableRecord> DimStyleTable =>
_dimStyleTable ??= new(this, _database.DimStyleTableId);
private SymbolTable<DimStyleTable, DimStyleTableRecord>? _dimStyleTable;
/// <summary>
/// 线型表
/// </summary>
public SymbolTable<LinetypeTable, LinetypeTableRecord> LinetypeTable =>
_linetypeTable ??= new(this, _database.LinetypeTableId);
private SymbolTable<LinetypeTable, LinetypeTableRecord>? _linetypeTable;
/// <summary>
/// 用户坐标系表
/// </summary>
public SymbolTable<UcsTable, UcsTableRecord> UcsTable =>
_ucsTable ??= new(this, _database.UcsTableId);
private SymbolTable<UcsTable, UcsTableRecord>? _ucsTable;
/// <summary>
/// 视图表
/// </summary>
public SymbolTable<ViewTable, ViewTableRecord> ViewTable =>
_viewTable ??= new(this, _database.ViewTableId);
private SymbolTable<ViewTable, ViewTableRecord>? _viewTable;
/// <summary>
/// 视口表
/// </summary>
public SymbolTable<ViewportTable, ViewportTableRecord> ViewportTable =>
_viewportTable ??= new(this, _database.ViewportTableId);
private SymbolTable<ViewportTable, ViewportTableRecord>? _viewportTable;
#endregion
#region 表记录
private readonly Dictionary<ObjectId, DBObject> _objectCache = new();
private T GetCache<T>(ObjectId objectId) where T : DBObject {
if (_objectCache.TryGetValue(objectId, out var obj)
&& obj is T result) {
return result;
}
result = (T)GetObject(objectId);
_objectCache.Add(objectId, result);
return result;
}
/// <summary>
/// 当前绘图空间(可能是不同布局的)
/// </summary>
public BlockTableRecord CurrentSpace =>
GetCache<BlockTableRecord>(_database.CurrentSpaceId);
/// <summary>
/// 模型空间
/// </summary>
public BlockTableRecord ModelSpace =>
GetCache<BlockTableRecord>(this.BlockTable[BlockTableRecord.ModelSpace]);
/// <summary>
/// 图纸空间
/// </summary>
public BlockTableRecord PaperSpace =>
GetCache<BlockTableRecord>(this.BlockTable[BlockTableRecord.PaperSpace]);
/// <summary>
/// 命名对象字典
/// </summary>
public DBDictionary NamedObjectsDict =>
GetCache<DBDictionary>(_database.NamedObjectsDictionaryId);
/// <summary>
/// 组字典
/// </summary>
public DBDictionary GroupDict =>
GetCache<DBDictionary>(_database.GroupDictionaryId);
/// <summary>
/// 多重引线样式字典
/// </summary>
public DBDictionary MLeaderStyleDict =>
GetCache<DBDictionary>(_database.MLeaderStyleDictionaryId);
/// <summary>
/// 多线样式字典
/// </summary>
// ReSharper disable once InconsistentNaming
public DBDictionary MLStyleDict =>
GetCache<DBDictionary>(_database.MLStyleDictionaryId);
/// <summary>
/// 材质字典
/// </summary>
public DBDictionary MaterialDict =>
GetCache<DBDictionary>(_database.MaterialDictionaryId);
/// <summary>
/// 表格样式字典
/// </summary>
public DBDictionary TableStyleDict =>
GetCache<DBDictionary>(_database.TableStyleDictionaryId);
/// <summary>
/// 视觉样式字典
/// </summary>
public DBDictionary VisualStyleDict =>
GetCache<DBDictionary>(_database.VisualStyleDictionaryId);
/// <summary>
/// 颜色字典
/// </summary>
public DBDictionary ColorDict =>
GetCache<DBDictionary>(_database.ColorDictionaryId);
/// <summary>
/// 打印设置字典
/// </summary>
public DBDictionary PlotSettingsDict =>
GetCache<DBDictionary>(_database.PlotSettingsDictionaryId);
/// <summary>
/// 打印样式表名字典
/// </summary>
public DBDictionary PlotStyleNameDict =>
GetCache<DBDictionary>(_database.PlotStyleNameDictionaryId);
/// <summary>
/// 布局字典
/// </summary>
public DBDictionary LayoutDict =>
GetCache<DBDictionary>(_database.LayoutDictionaryId);
#if !zcad
/// <summary>
/// 数据链接字典
/// </summary>
public DBDictionary DataLinkDict =>
GetCache<DBDictionary>(_database.DataLinkDictionaryId);
/// <summary>
/// 详细视图样式字典
/// </summary>
public DBDictionary DetailViewStyleDict =>
GetCache<DBDictionary>(_database.DetailViewStyleDictionaryId);
/// <summary>
/// 剖面视图样式字典
/// </summary>
public DBDictionary SectionViewStyleDict =>
GetCache<DBDictionary>(_database.SectionViewStyleDictionaryId);
#else
public DBDictionary DataLinkDict =>
throw new ArgumentNullException(nameof(DBTrans), "中望CAD遗忘设计");
public DBDictionary DetailViewStyleDict =>
throw new ArgumentNullException(nameof(DBTrans), "中望CAD遗忘设计");
public DBDictionary SectionViewStyleDict =>
throw new ArgumentNullException(nameof(DBTrans), "中望CAD遗忘设计");
#endif
#endregion
#region 通用方法
/// <summary>
/// 根据对象id获取对象
/// </summary>
/// <param name="id">对象id</param>
/// <param name="openMode">打开模式,默认为只读</param>
/// <param name="openErased">是否打开已删除对象,默认为不打开</param>
/// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
/// <returns>数据库DBObject对象</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public DBObject GetObject(ObjectId id, OpenMode openMode = OpenMode.ForRead,
bool openErased = false, bool openLockedLayer = false)
{
return _transaction.GetObject(id, openMode, openErased, openLockedLayer);
}
/*
返回值T还是T?
若返回值不可空,但是这函数又强转类,所以强转失败会为null.
若返回值可空,你就必须每次要调用之后判断空,
但是很多时候是明确类型的,会多了判断语句.
还记得那句话吗:可空类型标记不是让你写if,而是让你尽可能不写if.
是is还是as呢?这就是我们得放弃这个API.
*/
/// <summary>
/// 根据对象id获取图元对象
/// </summary>
/// <typeparam name="T">要获取的图元对象的类型</typeparam>
/// <param name="id">对象id</param>
/// <param name="openMode">打开模式,默认为只读</param>
/// <param name="openErased">是否打开已删除对象,默认为不打开</param>
/// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
/// <returns>图元对象</returns>
[Obsolete("可空类型标记出现后,建议使用非泛型标记的", false)]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public T? GetObject<T>(ObjectId id, OpenMode openMode = OpenMode.ForRead,
bool openErased = false, bool openLockedLayer = false) where T : DBObject
{
return _transaction.GetObject(id, openMode, openErased, openLockedLayer) as T;
}
/// <summary>
/// id有效,未被删除
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsOk(ObjectId id)
=> !id.IsNull && id.IsValid && !id.IsErased && !id.IsEffectivelyErased && id.IsResident;
/// <summary>
/// 根据句柄获取对象Id
/// </summary>
/// <param name="handleString">句柄字符串</param>
/// <returns>对象Id</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ObjectId GetObjectId(string handleString)
{
Handle handle;
if (IntPtr.Size == 4)
handle = new Handle(Convert.ToInt32(handleString, 16));
else
handle = new Handle(Convert.ToInt64(handleString, 16));
return _database.TryGetObjectId(handle, out ObjectId id) ? id : ObjectId.Null;
}
/// <summary>
/// 根据句柄获取对象Id
/// </summary>
/// <param name="handle">句柄</param>
/// <returns>对象Id</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public ObjectId GetObjectId(Handle handle)
{
return _database.TryGetObjectId(handle, out ObjectId id) ? id : ObjectId.Null;
}
/// <summary>
/// 将新创建对象加入或移出事务
/// <para>不是新创建对象移出会异常:eNotNewlyCreated</para>
/// </summary>
/// <param name="obj">对象</param>
/// <param name="add">true是加入事务,false是移出事务</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AddNewlyCreatedDBObject(DBObject obj, bool add = true) {
_transaction.AddNewlyCreatedDBObject(obj, add);
}
[Flags]
public enum Echo {
None = 0, // 不应用任何回声
ActiveDocument = 1 << 0, // 应用前台文档的回声
Errors = 1 << 1, // 应用报错的回声
FatalErrors = 1 << 2, // 应用致命错误的回声
All = ActiveDocument | Errors | FatalErrors // 启用所有回声功能
}
/// <summary>
/// 事务栈任务自动处理前台后台
/// </summary>
/// <param name="action">委托</param>
/// <param name="echo">回声,可以选择就是可以排除</param>
public DBTrans Task(Action action, Echo echo = Echo.All)
{
if (action is null) throw new ArgumentNullException(nameof(action));
if (CheckDatabaseError(echo.HasFlag(Echo.FatalErrors))) {
return this;
}
Database? dbBak = HostApplicationServices.WorkingDatabase;
Document? docBak = Acaop.DocumentManager.MdiActiveDocument;
var doc = Document;
// 直接执行: 当前文档前台开图 或 当前就是工作数据库
if ((docBak is not null && docBak == doc && docBak.Database == dbBak)
|| (dbBak is not null && dbBak == _database)) {
try {
action.Invoke();
return this;
}
catch (Exception ex) {
if (!echo.HasFlag(Echo.Errors))
throw; // 不,加ex表示由我的栈帧抛出.
System.Diagnostics.Trace.WriteLine(
$"捕获到异常:\n" +
$"错误时间: {DateTime.Now}\n" +
$"错误信息: {ex.Message}\n" +
$"堆栈跟踪: {ex.StackTrace}");
return this;
}
}
// GetTop()允许
// 1,当前文档就是工作数据库.
// 2,工作数据库不是当前文档,例如克隆后台数据到前台.
// GetTop()阻止
// 3,工作数据库为空表示:关闭全部文档并且后台开图.
// 设置 工作数据库=后台db,之后进入任务,完成任务后还原.
// 设置工作数据库还顺便处理了深度克隆导致单行文字偏移.
// 布局管理器取值和设置是经过工作数据库.
// 前台绑定参照用此方法会抛出异常:eWasErased
// 是不是之前测试有问题呢?
// 可能是原本只有一个栈的错误弹栈导致的.
// 可能是绑定要调用其他引擎,此时需要Session呢?
// 可能是用完工作数据库之后,提交事务前忘记切换.(现在才知道有这回事)
// 可能是已经位于前台文档集合中,但是没有激活为当前文档,
// 激活岂不是要设置Session?用事件处理?发送异步命令?
// 发送异步命令激活文档,但是委托怎么传入进去呢?
// 又或许这种切换是可以的,只是要文档锁?
// 切换会不会刷新工作数据库呢?影响了这两行位置
// 让用户自己去保证吧.
if (doc is not null && !doc.IsDisposed && docBak != doc) {
if (echo.HasFlag(Echo.ActiveDocument)) {
System.Diagnostics.Trace.WriteLine(
$"前台文档,非激活状态执行任务\n" +
$"规避错误需要设置CommandFlags.Session+切换文档+文档锁");
}
// 调用者需要外部设置和恢复
// Acaop.DocumentManager.MdiActiveDocument = doc;
}
HostApplicationServices.WorkingDatabase = _database;
try {
action.Invoke();
return this;
}
catch (Exception ex) {
if (!echo.HasFlag(Echo.Errors))
throw; // 不,加ex表示由我的栈帧抛出.
System.Diagnostics.Trace.WriteLine(
$"捕获到异常:\n" +
$"错误时间: {DateTime.Now}\n" +
$"错误信息: {ex.Message}\n" +
$"堆栈跟踪: {ex.StackTrace}");
return this;
}
finally {
// 工作数据库=后台db,使用完后还原应该允许设置null的.
HostApplicationServices.WorkingDatabase = dbBak;
// if (docBak is not null && !docBak.IsDisposed)
// Acaop.DocumentManager.MdiActiveDocument = docBak;
}
}
#endregion
#region IDisposable接口相关函数
/// <summary>
/// 取消事务
/// </summary>
public void Abort()
{
_transStatus.Abort();
Dispose();
}
/// <summary>
/// 提交事务
/// </summary>
public void Commit()
{
_transStatus.Commit();
Dispose();
}
/// <summary>
/// 释放标记
/// </summary>
public bool IsDisposed => _transStatus.IsDisposed;
/// <summary>
/// 手动调用释放
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// 析构函数调用释放
/// </summary>
~DBTrans() => Dispose(false);
/*
事务释放流程:
1,根据传入的_transStatus参数确定是否提交,
2,不管是否提交,既然进入dispose,就将当前事务弹出.
3,将文档锁释放
4,静态字段也就是全局变量,生命周期和进程一样,由GC释放.
5,通过事务栈打开读取的文件,需要释放数据库,否则遗忘释放了,
并且在此之后需要保证 工作数据库 是其他数据库,
工作数据库如果存放 释放了的数据库 ,这会产生错误.
为了简化这个流程,本类写了一个Task函数,使得后台用完工作数据库就切换.
*/
/// <summary>
/// 释放函数
/// </summary>
/// <param name="disposing"></param>
private void Dispose(bool disposing)
{
if (_transStatus.IsDisposed) return;
_transStatus.Dispose();
// 释放本类资源
if (disposing)
{
// 构造函数引发致命错误时,
// 本类资源会被清空,此处会连锁报错,
// 因此去掉打开文件的构造函数,避免构造出错,
// 构造都是有效对象,此处也就不需要判断为空了
if (!_transaction.IsDisposed) // 防止隐式转换提交绕过
{
if (_transStatus.IsCommit)
{
CheckWorkingDatabaseSync();
_transaction.Commit();
}
else
{
// 如果是前台文档集合的,但是不是当前文档呢?
// 和Task一样,由用户自己保证.
// 防止事务回滚造成的视图回滚
using var vtr = Editor?.GetCurrentView();
_transaction.Abort();
if (vtr is not null) Editor?.SetCurrentView(vtr);
}
_transaction.Dispose();
}
_documentLock?.Dispose();
// 表记录释放
foreach(var pair in _objectCache) {
pair.Value.Dispose();
}
_objectCache.Clear();
// 符号表释放
_blockTable?.Dispose();
_layerTable?.Dispose();
_textStyleTable?.Dispose();
_regAppTable?.Dispose();
_dimStyleTable?.Dispose();
_linetypeTable?.Dispose();
_ucsTable?.Dispose();
_viewTable?.Dispose();
_viewportTable?.Dispose();
}
// 释放全局资源,将当前事务栈弹栈
if (_dBTrans.TryGetValue(_database, out var trStack)) {
trStack.Pop();
if (trStack.Count == 0) {
_dBTrans.Remove(_database);
// 释放读取文件创建的数据库
if (Document is null)
_database.Dispose();
}
}
_blockTable = null!;
_layerTable = null!;
_textStyleTable = null!;
_regAppTable = null!;
_dimStyleTable = null!;
_linetypeTable = null!;
_ucsTable = null!;
_viewTable = null!;
_viewportTable = null!;
}
// 提交事务前如果工作数据库=后台图纸(还没有释放)
// 不报错也会直接致命错误,并且没有任何信息,不知其意义.
[Conditional("DEBUG")]
[MethodImpl(MethodImplOptions.NoInlining)]
private static void CheckWorkingDatabaseSync() {
Database? wdb = HostApplicationServices.WorkingDatabase;
if (wdb is null) return;
Document? doc = Acaop.DocumentManager.GetDocument(wdb);
if (doc is not null) return;
throw new Exception($"致命错误,工作数据库是后台数据库");
}
// _database没有可空标记,
// 被用户意外释放.
[MethodImpl(MethodImplOptions.NoInlining)]
private bool CheckDatabaseError(bool echo = true) {
if (_database.IsDisposed) {
if (!echo) return true;
throw new Exception("致命错误,数据库被错误释放");
}
return false;
}
#endregion
#region ToString
public override string ToString()
{
StringBuilder sb = new();
sb.AppendLine("事务栈信息:");
int i = 0;
foreach (var pair in _dBTrans) {
sb.AppendLine($"序号{++i}: 数据库路径: \"{pair.Key.Filename}\" - 事务栈数: {pair.Value.Count}");
}
sb.AppendLine("当前事务信息:");
sb.AppendLine($"_database = \"{_database.Filename}\"");
sb.AppendLine($"Document = {Document != null}");
sb.AppendLine($"Editor = {Editor != null}");
sb.AppendLine($"_transStatus = {_transStatus.ToString()}");
sb.AppendLine($"_documentLock = {_documentLock != null}");
sb.AppendLine($"_transaction = {_transaction.UnmanagedObject}");
return sb.ToString();
}
#endregion
}
保存文件
1,定点数判断dwg版本.
https://www.cnblogs.com/JJBox/p/18511807
2,判断是否已经被其他进程占用,并提示.
https://www.cnblogs.com/JJBox/p/18697838
namespace IFoxCAD.Cad;
public static class DatabaseHelper {
/// <summary>
/// 保存文件
/// </summary>
/// <param name="db">数据库</param>
/// <param name="saveAsFile">另存为文件,原位保存要填db.Filename</param>
/// <param name="version">取反:先提取原本DWG版本,失败时用你要求的</param>
public static void SaveFile(this Database db, string saveAsFile, int version = ~(int)DwgVersion.Current)
{
if (db is null) throw new ArgumentNullException(nameof(db));
if (saveAsFile is null) throw new ArgumentNullException(nameof(saveAsFile));
// 当前数据库的文件路径,新创建的数据库为"".
var file = db.Filename;
// 检查前台文档是否打开
var dm = Acaop.DocumentManager;
// var doc = dm.Cast<Document>()
// .FirstOrDefault(doc => !doc.IsDisposed && doc.Database == db);
Document? doc = dm.GetDocument(db);
if (doc is not null) {
bool qs = !doc.IsReadOnly && string.Equals(file, saveAsFile, StringComparison.OrdinalIgnoreCase);
doc.SendStringToExecute(qs ? "_qsave\n" : "_saveAs\n", false, true, true);
return;
}
// 后台另存为目标.
// 若是新创建数据库,则肯定于目标不同.
// 1,被自己占用就直接保存咯,数据库可能是只读.
// 2,被其他进程占用目标文件,报错.
if (!string.Equals(file, saveAsFile, StringComparison.OrdinalIgnoreCase)
&& IsFileOpen(saveAsFile))
throw new FileNotFoundException(nameof(saveAsFile), "另存目标文件被其他进程占用");
// DXF用任何版本号都会异常
var ext = Path.GetExtension(saveAsFile);
if (string.Equals(ext, ".dxf", StringComparison.OrdinalIgnoreCase)) {
db.DxfOut(saveAsFile, 7, true);
return;
}
// DWG保存需要版本号,
// 取反:先提取原本DWG版本,失败时用你要求的.
// 已经打开的数据库怎么会失败,无法识别参数呢?
// 1,没有维护VersionTool
// 2,兼容性加载,低加载高,
// DwgVersion.Current被编译固定,此时判断就会高于当前.
if (File.Exists(file) && version < 0) {
try {
int acNum = -1;
// 异常只会存在读取文件出错.
ReadWriteDWG(file, (index, fileStream) => {
acNum = VersionTool.DwgVers[index];
});
// 虽然编译时参数转为常量.
// 但是运行时候遍历是引用的dll版本,是在exe旁边的.
// 高于当前CAD是找不到的.
Dictionary<string, int> verMap = new();
foreach (DwgVersion dv in Enum.GetValues(typeof(DwgVersion)))
verMap.Add(dv.ToString(), (int)dv);
if (verMap.TryGetValue("AC" + acNum, out var v)) {
version = v;
}
} catch {}
}
if (version < 0) version = ~version;
// 此时是运行时
if (version > (int)DwgVersion.Current) {
version = (int)DwgVersion.Current;
}
db.SaveAs(saveAsFile, true, (DwgVersion)version, db.SecurityParameters);
}
}
状态结构体
只有1字节,嘿嘿,但是类和结构对齐就算一个指针长度了.
namespace IFoxCAD.Cad;
using System;
using System.Runtime.InteropServices;
public struct TransStatus
{
[System.Flags]
private enum TrStatus : byte {
IsCommit = 1 << 0, // 提交标记 (00000001)
IsDisposed = 1 << 1 // 释放标记 (00000010)
}
// 私有状态字段
private TrStatus _status;
// 公有属性
public bool IsCommit => _status.HasFlag(TrStatus.IsCommit);
public bool IsAbort => !_status.HasFlag(TrStatus.IsCommit); // 未提交即为取消
public bool IsDisposed => _status.HasFlag(TrStatus.IsDisposed);
// 提交事务
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Commit() => _status |= TrStatus.IsCommit;
// 取消事务
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Abort() => _status &= ~TrStatus.IsCommit;
// 释放事务
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose() => _status |= TrStatus.IsDisposed;
// 隐式类型转换
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator byte(TransStatus status)
=> (byte)status._status;
public override string ToString()
=> $"提交标记: {IsCommit}, 释放标记: {IsDisposed}";
}
测试
public class Program
{
public static void Main()
{
// 测试结构体大小
int size = Marshal.SizeOf(typeof(TransStatus));
Console.WriteLine($"TransStatus 结构体的大小为:{size} 字节");
// 测试事务操作
TransStatus status = new TransStatus();
byte statusAsByte = status;
Console.WriteLine($"状态值(byte): {statusAsByte}");
Console.WriteLine(status.ToString());
// 测试提交
status.Commit();
statusAsByte = status;
Console.WriteLine($"状态值(byte): {statusAsByte}");
Console.WriteLine(status.ToString());
// 测试取消
status.Abort();
statusAsByte = status;
Console.WriteLine($"状态值(byte): {statusAsByte}");
Console.WriteLine(status.ToString());
// 测试释放
status.Dispose();
statusAsByte = status;
Console.WriteLine($"状态值(byte): {statusAsByte}");
Console.WriteLine(status.ToString());
// 再次提交
status.Commit();
statusAsByte = status;
Console.WriteLine($"状态值(byte): {statusAsByte}");
Console.WriteLine(status.ToString());
}
}
符号表
// ReSharper disable RedundantNameQualifier
namespace IFoxCAD.Cad;
/// <summary>
/// 符号表模板类
/// </summary>
/// <typeparam name="TTable">符号表</typeparam>
/// <typeparam name="TRecord">表记录</typeparam>
public class SymbolTable<TTable, TRecord> : IEnumerable<ObjectId>, IDisposable
where TTable : SymbolTable
where TRecord : SymbolTableRecord, new()
{
#region 字段和属性
/// <summary>
/// 事务栈
/// </summary>
public DBTrans DBTrans => _dBTrans;
private DBTrans _dBTrans;
/// <summary>
/// 当前符号表,是九个符号表之一
/// </summary>
public TTable CurrentSymbolTable => _curSym;
private TTable _curSym;
#endregion
#region 构造函数
/// <summary>
/// 符号表模板类
/// </summary>
/// <param name="tr">事务栈</param>
/// <param name="tableId">符号表id</param>
/// <param name="defaultBehavior">默认行为:例如打开隐藏图层</param>
internal SymbolTable(DBTrans tr, ObjectId tableId, bool defaultBehavior = true)
{
_dBTrans = tr;
_curSym = (TTable)tr.GetObject(tableId);
if (!defaultBehavior) return;
if (_curSym is LayerTable layerTable) {
// 打开层表隐藏
layerTable = layerTable.IncludingHidden;
if (layerTable is TTable table)
_curSym = table;
}
}
#endregion
#region 增
/// <summary>
/// 添加表记录
/// </summary>
/// <param name="record">表记录</param>
/// <returns>对象id</returns>
public ObjectId Add(TRecord record)
{
ObjectId id;
if (!_curSym.IsWriteEnable)
_curSym.UpgradeOpen();
id = _curSym.Add(record);
_dBTrans.AddNewlyCreatedDBObject(record, true);
return id;
}
/// <summary>
/// 添加表记录
/// </summary>
/// <param name="recordName">表记录名</param>
/// <param name="action">表记录处理函数</param>
/// <returns>对象id</returns>
public ObjectId Add(string recordName, Action<TRecord>? action = null)
{
var id = this[recordName];
if (!id.IsNull) return id;
var record = new TRecord() { Name = recordName };
id = _curSym.Add(record);
_dBTrans.AddNewlyCreatedDBObject(record, true);
action?.Invoke(record);
return id;
}
/// <summary>
/// 有则修改无则添加表记录
/// </summary>
/// <param name="recordName">表记录名</param>
/// <param name="action">表记录处理函数</param>
/// <returns>对象id</returns>
public ObjectId AddOrUpdate(string recordName, Action<TRecord> action)
{
var id = this[recordName];
if (id.IsNull) id = Add(recordName, action);
else Change(recordName, action);
return id;
}
#endregion
#region 删
/// <summary>
/// 删除表记录
/// </summary>
/// <param name="record">表记录</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Remove(TRecord record)
{
if (!_curSym.IsWriteEnable)
record.UpgradeOpen();
record.Erase();
}
/// <summary>
/// 删除表记录
/// </summary>
/// <param name="recordName">表记录名</param>
public void Remove(string recordName)
{
// 热路径是非图层,在前面
if (_curSym is not LayerTable lt) {
var record = GetRecord(recordName);
if (record is not null)
Remove(record);
return;
}
if (SymbolUtilityServices.IsLayerZeroName(recordName) ||
SymbolUtilityServices.IsLayerDefpointsName(recordName))
return;
lt.GenerateUsageData();
var recordLayer = GetRecord(recordName);
if (recordLayer is not LayerTableRecord { IsUsed: false } ltr)
return;
if (!_curSym.IsWriteEnable)
ltr.UpgradeOpen();
ltr.Erase();
}
/// <summary>
/// 删除表记录
/// </summary>
/// <param name="recordId">表记录Id</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Remove(ObjectId recordId)
{
var record = GetRecord(recordId);
if (record is not null)
Remove(record);
}
#endregion
#region 改
/// <summary>
/// 修改表记录
/// </summary>
/// <param name="recordName">表记录名</param>
/// <param name="action">修改委托</param>
[DebuggerNonUserCode]
public void Change(string recordName, Action<TRecord> action)
{
var record = GetRecord(recordName);
if (record is null) return;
if (!_curSym.IsWriteEnable)
record.UpgradeOpen();
action(record);
}
/// <summary>
/// 修改表记录
/// </summary>
/// <param name="recordId">表记录Id</param>
/// <param name="action">修改的函数</param>
[DebuggerNonUserCode]
public void Change(ObjectId recordId, Action<TRecord> action)
{
var record = GetRecord(recordId);
if (record is null) return;
if (!_curSym.IsWriteEnable)
record.UpgradeOpen();
action(record);
}
#endregion
#region 查
/// <summary>
/// 获取表记录
/// </summary>
/// <param name="recordId">表记录Id</param>
/// <param name="openMode">打开模式</param>
/// <param name="openErased">是否打开已删除对象,默认为不打开</param>
/// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
/// <returns>表记录</returns>
[DebuggerNonUserCode]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TRecord? GetRecord(ObjectId recordId,
OpenMode openMode = OpenMode.ForRead,
bool openErased = false,
bool openLockedLayer = false)
{
if (!_map.TryGetValue(recordId, out var obj)) {
var tr = _dBTrans;
obj = (TRecord)tr.GetObject(recordId, openMode, openErased, openLockedLayer);
if (obj is not null)
_map.Add(recordId, obj);
}
return obj;
}
/// <summary>
/// 获取表记录
/// </summary>
/// <param name="recordName">表记录名</param>
/// <param name="openMode">打开模式</param>
/// <param name="openErased">是否打开已删除对象,默认为不打开</param>
/// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
/// <returns>表记录</returns>
[DebuggerNonUserCode]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TRecord? GetRecord(string recordName,
OpenMode openMode = OpenMode.ForRead,
bool openErased = false,
bool openLockedLayer = false)
{
var recordId = this[recordName];
if (!_map.TryGetValue(recordId, out var obj)) {
var tr = _dBTrans;
obj = (TRecord)tr.GetObject(recordId, openMode, openErased, openLockedLayer);
if (obj is not null)
_map.Add(recordId, obj);
}
return obj;
}
#region
// 这些函数挑战了using释放原则,要记录并释放.
Dictionary<ObjectId, TRecord> _map = new();
/// <summary>
/// 获取表记录
/// </summary>
/// <returns>表记录集合</returns>
public IEnumerable<TRecord> GetRecords()
{
foreach (var item in _curSym) {
var record = GetRecord(item);
if (record is null) continue;
yield return record;
}
}
/// <summary>
/// 获取表记录名字
/// </summary>
/// <returns>惰性返回名字集合</returns>
public IEnumerable<string> GetRecordNames() {
GetRecords().Select(record => record.Name);
}
/// <summary>
/// 获取表记录名字
/// </summary>
/// <param name="filter">过滤器</param>
/// <returns>惰性返回名字集合</returns>
public IEnumerable<string> GetRecordNames(Func<TRecord, bool> filter)
{
foreach (var item in _curSym) {
var record = GetRecord(item);
if (record is not null && filter.Invoke(record))
yield return record.Name;
}
}
#endregion
/// <summary>
/// 从源数据库拷贝符号表记录
/// </summary>
/// <param name="table">符号表</param>
/// <param name="recordName">表记录名</param>
/// <param name="over">是否覆盖,<see langword="true"/>为覆盖,<see langword="false"/>为不覆盖</param>
/// <returns>对象id</returns>
private ObjectId GetRecordFrom(SymbolTable<TTable, TRecord> table,
string recordName, bool over)
{
if (table is null)
throw new ArgumentNullException(nameof(table));
var rid = this[recordName];
var has = rid != ObjectId.Null;
if (has && (!has || !over)) return rid;
using IdMapping map = new();
using ObjectIdCollection ids = new();
var id = table[recordName];
ids.Add(id);
table._dBTrans.Database.WblockCloneObjects(ids,
_curSym.Id,
map,
DuplicateRecordCloning.Replace,
false);
return map[id].Value;
}
/// <summary>
/// 从文件拷贝符号表记录
/// </summary>
/// <param name="tableSelector">符号表过滤器</param>
/// <param name="file">文件</param>
/// <param name="recordName">表记录名</param>
/// <param name="over">是否覆盖,<see langword="true"/>为覆盖,<see langword="false"/>为不覆盖</param>
/// <returns>对象id</returns>
internal ObjectId GetRecordFrom(Func<DBTrans, SymbolTable<TTable, TRecord>> tableSelector,
string file, string recordName, bool over)
{
using var tr = DBTrans.OpenPushToBackend(file);
return GetRecordFrom(tableSelector(tr), recordName, over);
}
/// <summary>
/// 索引器
/// </summary>
/// <param name="key">对象名称</param>
/// <returns>对象的id</returns>
public ObjectId this[string key] =>
_curSym.Has(key) ? _curSym[key] : ObjectId.Null;
/// <summary>
/// 判断是否含有表记录
/// </summary>
/// <param name="key">记录名</param>
/// <returns>存在返回 <see langword="true"/>, 不存在返回 <see langword="false"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Has(string key) => _curSym.Has(key);
/// <summary>
/// 判断是否含有表记录
/// </summary>
/// <param name="objectId">记录id</param>
/// <returns>存在返回 <see langword="true"/>, 不存在返回 <see langword="false"/></returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool Has(ObjectId objectId) => _curSym.Has(objectId);
/// <summary>
/// 遍历符号表,执行委托
/// </summary>
/// <param name="task">要运行的委托</param>
/// <param name="openMode">打开模式,默认为只读</param>
/// <param name="checkIdOk">检查id是否删除,默认true</param>
/// <param name="openErased">是否打开已删除对象,默认为不打开</param>
/// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
[DebuggerNonUserCode]
public void ForEach(Action<TRecord> task,
OpenMode openMode = OpenMode.ForRead,
bool checkIdOk = true,
bool openErased = false,
bool openLockedLayer = false) {
// 此处有委托函数,用[DebuggerStepThrough]特性会进入,
// 预处理 #line hidden 肯定可以避免
// AI提供了一个 [DebuggerNonUserCode] 特性,或者不用Invoke方式.
ForEach((a, _, _) => {
task(a);
}, openMode, checkIdOk, openErased, openLockedLayer);
}
/// <summary>
/// 遍历符号表,执行委托(允许循环中断)
/// </summary>
/// <param name="task">要执行的委托</param>
/// <param name="openMode">打开模式,默认为只读</param>
/// <param name="checkIdOk">检查id是否删除,默认true</param>
/// <param name="openErased">是否打开已删除对象,默认为不打开</param>
/// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
[DebuggerNonUserCode]
public void ForEach(Action<TRecord, LoopState> task,
OpenMode openMode = OpenMode.ForRead,
bool checkIdOk = true,
bool openErased = false,
bool openLockedLayer = false)
{
ForEach((a, b, _) => { task(a, b); }, openMode, checkIdOk, openErased, openLockedLayer);
}
/// <summary>
/// 遍历符号表,执行委托(允许循环中断,输出索引值)
/// </summary>
/// <param name="task">要执行的委托</param>
/// <param name="openMode">打开模式,默认为只读</param>
/// <param name="checkIdOk">检查id是否删除,默认true</param>
/// <param name="openErased">是否打开已删除对象,默认为不打开</param>
/// <param name="openLockedLayer">是否打开锁定图层对象,默认为不打开</param>
[DebuggerNonUserCode]
public void ForEach(Action<TRecord, LoopState, int> task,
OpenMode openMode = OpenMode.ForRead,
bool checkIdOk = true,
bool openErased = false,
bool openLockedLayer = false)
{
if (task is null) throw new ArgumentNullException(nameof(task));
LoopState state = new(); /*这种方式比Action改Func更友好*/
var i = 0;
foreach (var id in _curSym) {
if (checkIdOk && !DBTrans.IsOk(id)) continue;
var record = GetRecord(id, openMode, openErased, openLockedLayer);
if (record is not null) task(record, state, i);
if (!state.IsRun) break;
i++;
}
}
/*
// 直接强转少了一层yield,这个不行,嘻嘻
[DebuggerNonUserCode]
public IEnumerator<ObjectId> GetEnumerator()
=> (IEnumerator<ObjectId>)_curSym.GetEnumerator();
*/
[DebuggerNonUserCode]
public IEnumerator<ObjectId> GetEnumerator() {
foreach(ObjectId id in _curSym)
yield return id;
}
[DebuggerNonUserCode]
IEnumerator IEnumerable.GetEnumerator() {
return GetEnumerator();
}
#endregion
#region 释放
private bool _disposed = false;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
_disposed = true;
if (disposing) {
_curSym.Dispose();
foreach(var record in _map.Values) {
record.Dispose();
}
_map.Clear();
}
}
~SymbolTable() => Dispose(false);
#endregion
}
LoopState
namespace IFoxCAD.Basal;
#line hidden
/// <summary>
/// 控制程序流程状态
/// </summary>
public class LoopState {
// 状态枚举
[System.Flags]
public enum StateFlags : int
{
None = 0, // 初始化状态
Run = 1 << 0, // 运行状态
Broken = 1 << 1, // 中断状态
Stopped = 1 << 2, // 停止状态
Canceled = 1 << 3, // 取消状态
Exceptional = 1 << 4 // 异常状态
}
private volatile StateFlags _state = StateFlags.None;
// 状态属性
public bool IsNone => _state == StateFlags.None;
public bool IsRun => _state.HasFlag(StateFlags.Run);
public bool IsBroken => _state.HasFlag(StateFlags.Broken);
public bool IsStopped => _state.HasFlag(StateFlags.Stopped);
public bool IsCanceled => _state.HasFlag(StateFlags.Canceled);
public bool IsExceptional => _state.HasFlag(StateFlags.Exceptional);
// 状态控制方法
public void Start() => _state = StateFlags.Run; // 设置为运行状态
public void Break() => _state = StateFlags.Broken; // 设置为中断状态
public void Stop() => _state = StateFlags.Stopped; // 设置为停止状态
public void Cancel() => _state = StateFlags.Canceled; // 设置为取消状态
public void Exceptional() => _state |= StateFlags.Exceptional; // 设置异常状态
public void Reset() => _state = StateFlags.None; // 重置为初始状态
}
#line default
测试类大小
using System;
using System.Runtime.CompilerServices;
namespace IFoxCAD.Cad;
public class MyClass {
public int IntField;
public double DoubleField;
public string StringField;
}
public class Program {
public static void Main() {
MyClass obj = new MyClass();
obj.IntField = 123;
obj.DoubleField = 456.789;
obj.StringField = "Hello";
long size = Unsafe.SizeOf<MyClass>();
Console.WriteLine("Size of MyClass: " + size + " bytes");
}
}
无事务图元过滤器
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System;
using System.Collections.Generic;
namespace IFoxCAD.Cad;
public class TestEntityFilterCommands {
[CommandMethod(nameof(TestEntityFilter), CommandFlags.UsePickSet)]
public void TestEntityFilter()
{
Document doc = Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
var ss = ed.GetSelection();
if (ss.Status != PromptStatus.OK) return;
// 获取选中的ObjectId集合
var ids = new HashSet<ObjectId>(ss.Value.GetObjectIds());
// 创建要过滤的类型集合
var filterTypes = new HashSet<Type> {
typeof(Line),
typeof(Circle)
};
// 过滤惰性返回id
var filteredIds = ids.Filter(filterTypes)
.ToList();
// 过滤惰性返回pair,转为字典
var filteredMap = ids.FilterToPair(filterTypes)
.ToDictionary(pair => pair.Key, pair => pair.Value);
ed.WriteMessage($"\n匹配的实体数量:{filteredIds.Count}");
// 高亮显示匹配的实体
using var tr = db.TransactionManager.StartTransaction();
foreach (ObjectId id in filteredIds) {
using var ent = (Entity)tr.GetObject(id, OpenMode.ForRead);
ent.Highlight();
}
tr.Commit();
}
}
public static class ObjectIdHelper {
// 过滤得到直接匹配类型和派生关系
// 自行保证两个参数不重复,当然,重复和无所谓
public static IEnumerable<ObjectId> Filter(this ObjectId[] ids, HashSet<Type> typeSet) {
if (ids is null)
throw new ArgumentNullException(nameof(ids));
if (types is null)
throw new ArgumentNullException(nameof(types));
// 获取顶层父类用于遍历,ClassVersion是0.0.0.0
var baseTypes = typeSet.Select(type =>
RXObject.GetClass(type))
.ToHashSet().ToList();
// 储存排除类型,查找过的类型就不再需要找了
var excludedTypes = new HashSet<Type>();
foreach (var id in ids) {
var myType = id.ObjectClass;
// 已经排除类型
if (excludedTypes.Contains(myType)) continue;
// 同类直接加入
if (typeSet.Contains(myType)) {
yield return id;
continue;
}
// 查询是否来自于派生,成功就加入同类容器,否则加入排除类型.
// 调用者其后打印typeSet内容,
// 就能直接加入参数,天正类型可能无法直接加入.
var matchType = baseTypes.Find(baseType =>
myType.IsDerivedFrom(baseType));
if (matchType is not null) {
typeSet.Add(matchType);
yield return id;
continue;
}
excludedTypes.Add(myType);
}
}
public static IEnumerable<KeyValuePair<Type, ObjectId>> FilterToPair(
this ObjectId[] ids, HashSet<Type> typeSet) {
if (ids is null)
throw new ArgumentNullException(nameof(ids));
if (typeSet is null)
throw new ArgumentNullException(nameof(typeSet));
// 获取顶层父类用于遍历
var baseTypes = typeSet.Select(type =>
RXObject.GetClass(type))
.ToHashSet().ToList();
// 储存排除类型,查找过的类型就不再需要找了
var excludedTypes = new HashSet<Type>();
foreach (var id in ids) {
var myType = id.ObjectClass;
// 已经排除类型
if (excludedTypes.Contains(myType)) continue;
// 同类直接加入
if (typeSet.Contains(myType)) {
yield return new KeyValuePair<Type, ObjectId>(myType, id);
continue;
}
// 查询是否来自于派生,成功就加入同类容器,否则加入排除类型.
var matchType = baseTypes.Find(baseType =>
myType.IsDerivedFrom(baseType));
if (matchType is not null) {
typeSet.Add(matchType);
yield return new KeyValuePair<Type, ObjectId>(matchType, id);
continue;
}
excludedTypes.Add(myType);
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?