☰
AutoCAD二次开发:VBA与VB.NET从环境搭建到批量处理实战
2026/10/8 20:42:53 网站建设 项目流程

简介:面向 AutoCAD 二次开发初学者与进阶开发者,这份示例源码包系统汇集了 VBA 与 VB.NET 的基础编程知识和实例工程。内容覆盖图层管理、实体操作、图形数据库交互、自定义 Ribbon 界面与事件驱动工具等典型场景,包含从基础对象操作到交互工具开发的多个层次,并配有 ARX 插件项目与可运行的 DLL/EXE 示例,帮助读者搭建环境、理解对象模型,再到通过 .NET API 自动化和扩展设计绘图流程。资源共 1777 个文件,压缩包约 35MB,以 C# 源码(.cs)、Visual Studio 解决方案(.sln/.csproj)和编译库(.dll/.pdb)为主,另含 DWG 测试图形、LSP 脚本、CHM/PDF 帮助文档及多种资源与配置文件,目录和工程结构清晰,便于按章节检索与对照调试。已有 995 人学习下载,适合课程实训、项目参考或自学入门。

1. 拿到这套 AutoCAD VBA / VB.NET 开发源码包,先别急着解压

很多工程师第一次接触 AutoCAD 二次开发,都是从网上下载一个标注着「基础与实例教程示例全部源码」的压缩包开始的。解压后里面通常是几十个.dvb、.bas、.cs、.vbproj文件,随手双击一个.dvb,AutoCAD 提示「未安装 VBA 支持库」;换成 Visual Studio 打开工程,又缺一堆 Autodesk 命名空间的引用。这个标题覆盖的其实是两条完整的技术路线:老牌的 VBA 宏,和现代 .NET 环境下的 VB.NET 插件。它解决的是两类问题——一是设计院、制造企业里大量重复的图纸处理工作,比如批量改图框、导明细表;二是把 AutoCAD 能力嵌入到企业自己的业务系统里。适合刚转岗 CAD 二次开发的人,也适合被图纸批量操作折磨了几年、想系统化写工具却不知从哪里下手的工程师。看完这篇,你能搞清楚这两条路分别怎么搭环境、怎么写第一行能跑的代码、以及遇到「无法运行文档中的宏」「unhandled access violation writing」这类报错时去查哪里。

2. 开发环境搭建:把 VBA 和 VB.NET 的最小可运行代码跑起来

2.1 为什么 VBA 在 64 位 AutoCAD 里越来越难用

AutoCAD VBA 的历史比很多人想象的更长。从 R14 时代开始,VBA 就内置在安装包里,用户直接打开 VBA 管理器就能写宏。但现在的问题在于:Autodesk 从 2010 版开始逐步把 VBA 从默认安装中移除,64 位版本尤其明显。打开 AutoCAD 后命令行输入VBAIDE,如果弹出「未安装 VBA 支持库」,说明你的机器上没有装 VBA Enabler。这不是给 AutoCAD 打补丁,而是 Autodesk 单独发布的一个运行库安装包,需要去官网按对应版本下载。

另一个现实约束是,VBA 本质上是 COM 自动化,它通过AutoCAD.Application这个 ProgID 与主进程通信。在 64 位 Windows 上,AutoCAD 主程序是 64 位进程,而 VBA 7.1 运行库本身是 32 位组件,两者通过 COM 互操作层进行桥接。这种架构在单机脚本场景下问题不大,但一旦涉及大量图形实体遍历,性能会明显劣于 .NET API 直调。

所以我的建议是:如果你是今天才决定入坑,直接学 VB.NET,把 VBA 当作「读旧代码、改小脚本」的辅助技能。如果手上就有一批老.dvb文件要维护,可以两条腿走路。

2.2 VBA 环境检查与最小宏:三行代码验证 COM 链路

先确认环境。打开 AutoCAD,命令行输入:

VBAIDE

能打开 VBA 编辑器,说明运行库正常。如果提示未安装,需要先装对应版本的 VBA Enabler。装好后,在 VBA 编辑器的 Immediate 窗口(按 Ctrl+G 调出)里输入以下代码并回车:

ThisDrawing.Utility.Prompt "VBA-OK" & vbCrLf

能输出VBA-OK,说明 VBA 与当前图纸的 Application 对象通信正常。这段代码不涉及任何文件操作,只用来验证最基本的 COM 链路是否通畅。ThisDrawing是 VBA 对当前激活文档的入口,Utility.Prompt是向命令行输出文本的标准方法,vbCrLf是换行符,避免输出粘在下一行命令前。

如果是第一次接触 VBA,可以用宏录制先建立感性认识。工具菜单里的「宏」→「录制」,画一条线再停止录制,VBA 会自动生成类似AddLine的代码。录制的代码虽然啰嗦,但它能告诉你对象模型的调用顺序——从ThisDrawing.ModelSpace拿到空间,再调用AddLine方法传入两个坐标。这个顺序就是后面所有代码的基础。

2.3 VB.NET 侧的环境准备:Visual Studio + AutoCAD .NET API

VB.NET 走的是另一条路。AutoCAD 从 2006 版开始提供基于 .NET 的托管 API,命名空间是Autodesk.AutoCAD.*,底层是对 ObjectARX(C++ API)的托管封装。和 VBA 的 COM 桥接不同,.NET API 直接在进程内运行,性能好很多,也支持事件、事务、自定义实体等高级功能。

推荐开发环境如下表:

组件推荐版本说明
AutoCAD2020 或更高版本越高,.NET 目标框架要求越高
Visual Studio2019 或 2022Community 版足够
.NET Framework4.7.2 或 4.8AutoCAD 2020 对应 4.7+,2023 以上要求 4.8
托管 API 引用Autodesk.AutoCAD.Interop需在添加引用时手动指向安装目录

创建一个 VB.NET 类库项目后,右键「添加」→「引用」,从 AutoCAD 安装目录(通常是C:\Program Files\Autodesk\AutoCAD 2020)找到如下 DLL:

AcDbMgd.dll AcMgd.dll AccoreMgd.dll

这三个引用缺一不可。AcMgd.dll包含 Application 和 Document 相关的核心类型,AcDbMgd.dll是数据库对象模型,也就是操作实体、图层、块表的基础,AccoreMgd.dll是命令行和编辑器相关功能。很多新手的错误是只引了AcMgd.dll,一写Transaction就报类型不明确。

2.4 第一个 VB.NET 插件:从 Netload 到命令行输出

下面这个例子是插件的最简骨架,功能是在命令行输出一段文字,类似 VBA 里Prompt的效果:

Imports Autodesk.AutoCAD.ApplicationServices Imports Autodesk.AutoCAD.EditorInput Imports Autodesk.AutoCAD.Runtime Public Class FirstPlugin <CommandMethod("HELLO")> Public Sub HelloWorld() Dim doc As Document = Application.DocumentManager.MdiActiveDocument Dim ed As Editor = doc.Editor ed.WriteMessage(vbLf & "HELLO from VB.NET" & vbLf) End Sub End Class

代码说明:<CommandMethod("HELLO")>特性声明了这是一个 AutoCAD 命令,命令名是HELLO,在命令行输入它就会触发下面的方法。Application.DocumentManager.MdiActiveDocument拿到当前激活的文档对象,Editor是面向用户的命令行输入输出接口,WriteMessage把字符串写到命令行。整个类不需要实例化,AutoCAD 通过反射找到带CommandMethod特性的方法。

编译项目得到ClassLibrary1.dll,然后回到 AutoCAD,命令行输入:

NETLOAD

在弹出的文件选择框中选中这个 DLL。命令行输入HELLO,能看到输出就说明加载成功。这里有个冷知识:NETLOAD命令会常驻内存,多次加载同一个 DLL 时不会覆盖旧版本,调试代码时需要先UNLOAD或者重启 AutoCAD。可以在命令行执行NETLOAD后按 F2,查看加载日志确认是否有程序集冲突。

3. 理解 AutoCAD 对象模型:从 ThisDrawing 到 Transaction 的演进

3.1 对象层级和入口选择

VBA 的代码入口非常直观,ThisDrawing就是一个全局变量,背后是 COM 的AcadDocument对象。顺着ThisDrawing可以拿到ModelSpace、Layers、SelectionSets。它的代码风格简洁,适合小工具,但问题也很明显:COM 对象的属性和方法名是固定的,想要扩展必须靠 COM 的ExecuteMso或者反射,不方便做复杂业务封装。

VB.NET 的入口则要显式得多。每次操作数据库前,都要建立一个Transaction对象,然后通过Transaction.GetObject获取块表、注册表或实体。一个典型的操作流程如下:

Using tr As Transaction = doc.Database.TransactionManager.StartTransaction() Dim bt As BlockTable = tr.GetObject(doc.Database.BlockTableId, OpenMode.ForRead) Dim btr As BlockTableRecord = tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite) ' 在这里创建或修改实体 tr.Commit() End Using

代码说明:StartTransaction()是事务的起点,BlockTable是块表,存储了图纸里所有块定义。bt(BlockTableRecord.ModelSpace)取出模型空间对应的记录。OpenMode.ForRead表示只读打开,如果后续要添加新实体,需要对模型空间OpenMode.ForWrite。所有修改操作完成后必须Commit(),否则事务回滚,图纸不会生效。这个模式对读操作也适用,因为 AutoCAD 所有对象访问都被纳入事务模型,跳过事务直接操作对象会导致运行时异常。

为什么这么设计?因为 AUTOCAD 的图形数据库支持多文档环境,用户可以在一个进程里打开多个图纸,每个文档对应一个独立的Database实例。事务机制保证了对数据库的并发访问是安全的——读取和写入不会互相污染。VBA 时代的 COM 对象其实底层也走事务,只是被封装掉了,你感觉不到。但一旦你的 VBA 宏触发了Undo,就会发现部分操作回退不了,原因就是没有显式事务边界。

3.2 SelectionSet:过滤和选择是高频操作

几乎每个生产工具都要让用户选择实体,然后批量处理。VBA 里用SelectionSet.SelectOnScreen接受用户框选,VB.NET 用Editor.SelectAll或SelectWindow。下面这段是 VB.NET 里选择所有圆并把它们改成红色圆环的完整代码:

Imports Autodesk.AutoCAD.DatabaseServices Imports Autodesk.AutoCAD.EditorInput Imports Autodesk.AutoCAD.Runtime Imports Autodesk.AutoCAD.ApplicationServices Public Class CircleTool <CommandMethod("REDCIRCLE")> Public Sub RedCircle() Dim doc As Document = Application.DocumentManager.MdiActiveDocument Dim db As Database = doc.Database Dim ed As Editor = doc.Editor Dim filter As New SelectionFilter(New TypedValue() { New TypedValue(0, "CIRCLE") }) Dim res As PromptSelectionResult = ed.SelectAll(filter) If res.Status <> PromptStatus.OK Then Return Using tr As Transaction = db.TransactionManager.StartTransaction() For Each id As ObjectId In res.Value.GetObjectIds() Dim ent As Entity = tr.GetObject(id, OpenMode.ForWrite) Dim circle As Circle = TryCast(ent, Circle) If circle IsNot Nothing Then circle.ColorIndex = 1 End If Next tr.Commit() End Using ed.WriteMessage(vbLf & "已处理 " & res.Value.Count & " 个圆" & vbLf) End Sub End Class

代码说明:TypedValue是过滤器的最小单元,(0, "CIRCLE")表示按实体类型过滤,DXF 组码 0 对应实体类型。如果还想过滤图层,可以再加一维,比如New TypedValue(8, "标注层")表示只选该图层上的实体。SelectAll不带参数会全图选择,配合过滤器就是「全图中满足条件的对象」。GetObject(id, OpenMode.ForWrite)是事务内获取对象的唯一方式,ColorIndex = 1对应红色,这是 AutoCAD 标准调色板的索引值。

这里有一个很多人容易踩的坑:For Each循环里对同一个对象调用GetObject两次,第二次必须用ForWrite。如果第一个GetObject用了ForRead,后面又用ForWrite再次打开同一个实体,AutoCAD 会抛出eWasOpenForWrite异常。处理方式是把第一次打开的实体先Dispose,或者在循环外用TransactionManager.GetObject一次性拿取。

3.3 字典与扩展数据:VBA 字典和 AutoCAD 自带的 Dictionaries

热词里经常出现的「vba 字典」和 AutoCAD 没有直接关系——VBA 里Scripting.Dictionary是 Windows 脚本宿主提供的键值对集合,常用来做去重、缓存和计数,和普通Collection的区别在于它提供Exists方法,查询接近 O(1)。我在写批量导出工具时,常用它做「图层名 → 数量」的统计。下面是一个典型用法,统计当前图纸里每种图层的实体数量:

Sub CountByLayer() Dim dict As Object Set dict = CreateObject("Scripting.Dictionary") Dim ent As AcadEntity For Each ent In ThisDrawing.ModelSpace If dict.Exists(ent.Layer) Then dict(ent.Layer) = dict(ent.Layer) + 1 Else dict.Add ent.Layer, 1 End If Next Dim key As Variant For Each key In dict.Keys Debug.Print key & ": " & dict(key) Next End Sub

代码说明:CreateObject("Scripting.Dictionary")是创建字典对象的标准方式,它的键可以是字符串也可以是数字。For Each ent In ThisDrawing.ModelSpace遍历模型空间全部实体,ent.Layer取出该实体所在图层名。dict(ent.Layer)的写法在字典已包含该键时直接更新值,比Item属性更简洁。需要注意字典的键区分大小写,如果你要统计的图层名存在「标注」和「标注 」这种带尾随空格的情况,键会被当成不同项,建议对ent.Layer先做Trim。

AutoCAD 自带的Dictionaries则是图形数据库的一部分,藏在Database.NamedObjectsDictionary里。它和 VBA 字典完全是两回事——前者是把自定义数据持久化在 DWG 文件内,后者是内存中的临时集合。如果你要在图面上挂业务属性,比如给每根管道附上「材质、压力等级」,正确做法是XRecord写到 NamedObjectsDictionary,而不是指望某个宏结束后内存里的字典还活着。

4. 批量处理实战:两个拿来就能改的实例源码

4.1 批量替换文字前缀:从选择集到逐实体修改

生产现场最常见的需求是把一批文字对象的编号前缀统一改掉,比如把「OLD-」改成「NEW-」。VBA 实现如下:

Sub ReplacePrefix() Dim sset As AcadSelectionSet Set sset = ThisDrawing.SelectionSets.Add("TMP_SSET") Dim fType(0) As Integer Dim fData(0) As Variant fType(0) = 0 fData(0) = "TEXT" sset.SelectOnScreen fType, fData Dim ent As AcadEntity Dim txt As AcadText Dim count As Integer For Each ent In sset If TypeOf ent Is AcadText Then Set txt = ent If Left(txt.TextString, 4) = "OLD-" Then txt.TextString = "NEW-" & Mid(txt.TextString, 5) count = count + 1 End If End If Next ThisDrawing.SelectionSets.Item("TMP_SSET").Delete MsgBox "已替换 " & count & " 个文字" End Sub

代码说明:SelectionSets.Add创建新的选择集,名称要唯一,所以用了带时间戳的想法也可以用Add("TMP_" & Timer)。SelectOnScreen让用户在屏幕上框选,过滤条件只有一个——实体类型是TEXT。TypeOf ent Is AcadText判断实体是否为文字对象,避免把多行文字MTEXT也算进去。Left和Mid是 VBA 字符串函数,分别取左侧指定长度和从第 5 位开始取子串。最后删除选择集是个好习惯,否则下次运行时SelectionSets.Add会报「集已存在」。

.NET 版本思路一致,但要用Transaction包住修改过程。注意编辑文字内容这个动作在 VBA 里是直接改TextString属性,在 .NET 里要先UpgradeOpen()或初始就以ForWrite打开,否则读打开状态下写入属性会抛异常。

4.2 导出块属性到 CSV:VBA 字典辅助去重

导出明细表是设计院高频需求。块的属性数据存在GetAttributes集合中,每个属性有TagString和TextString两个关键字段。下面的 VBA 代码把所有块参照的「名称、属性、数量」导出为 CSV 文件,中间用字典做名称去重和数量累加:

Sub ExportBlockAtts() Dim dict As Object Set dict = CreateObject("Scripting.Dictionary") Dim blk As AcadBlockReference Dim att As AcadAttributeReference Dim key As String For Each blk In ThisDrawing.ModelSpace If TypeOf blk Is AcadBlockReference Then key = blk.EffectiveName If dict.Exists(key) Then dict(key) = dict(key) + 1 Else dict.Add key, 1 End If End If Next Dim fso As Object Dim ts As Object Set fso = CreateObject("Scripting.FileSystemObject") Set ts = fso.CreateTextFile "D:\block_export.csv", True ts.WriteLine "块名,数量" Dim k As Variant For Each k In dict.Keys ts.WriteLine k & "," & dict(k) Next ts.Close MsgBox "导出完成,共 " & dict.Count & " 种块" End Sub

代码说明:blk.EffectiveName取得块参照的真实名称,如果是动态块,Name属性可能是匿名块名,EffectiveName才是用户定义的名称,这一行就避开了动态块统计混乱的坑。CreateObject("Scripting.FileSystemObject")创建文件系统对象,CreateTextFile的第二个参数True表示覆盖同名文件。CSV 文件的换行符在 VBA 里用vbCrLf,但 Excel 打开时建议用vbLf更兼容,不过大部分情况WriteLine已经追加了换行,不需要手动加。

.NET 版本的导出通常用File.AppendAllText或StreamWriter,并且注意System.IO.File和 AutoCAD 的文件锁会冲突——如果你用 AutoCAD 的SaveAs方法保存了同路径文件,NET 的FileStream可能会抛 IO 异常,正确做法是先File.Close释放句柄。

4.3 崩溃点:unhandled access violation writing 的常见来源

热词「autocad unhandled access violation writing」对应的是一类系统级错误,本质是 AutoCAD 进程内存访问违例。做过 .NET 开发的都知道,AccessViolationException最常见的触发方式是托管代码持有非托管对象,并在对象被垃圾回收后继续访问它。在 AutoCAD 二次开发里,典型场景包括:Transaction已Dispose但你保存了其中的ObjectId稍后用,或者对Document对象在另一个线程执行了修改。VBA 里也有类似的坑——AcadBlockReference对象没有显式释放,循环里反复创建和丢弃,COM 引用计数错乱会触发内存问题。

避免方法:一是所有ObjectId只做短生命周期使用,跨事务必须重新GetObject;二是 .NET 代码里绝不要自己GC.Collect(),让运行时自己管理;三是 VBA 宏结束后主动Set obj = Nothing释放 COM 对象。调试时打开 Windows 事件查看器,如果错误记录里显示故障模块是acdb22.dll,基本可以判定是数据库对象访问时序问题,优先检查事务边界。

5. 源码包的正确打开方式:阅读顺序、引用缺失和排错清单

5.1 这类教程包的典型目录结构

网上流传的「基础与实例教程示例全部源码」压缩包,解压后通常按阶段分目录,命名一般是01_Basic、02_Commands、03_Entities、04_DataExchange这类。里面既有.dvb也有.sln,意味着教程同时覆盖 VBA 和 VB.NET。不要从第一个文件按顺序看——很多初学者的错误是打开01_HelloWorld.dvb运行一下觉得简单,直接跳到最后一个复杂工程,结果被事务、事件、命令覆盖搞得一头雾水。

推荐的阅读顺序是:

  1. 先把所有.sln后缀的 VB.NET 工程单独拎出来,按工程名排序,找到名字最短的那一两个。它们通常对应第 2 章里那个「第一个插件」:类文件只有一个,包含一个CommandMethod。打开后检查引用路径,如果显示黄色感叹号,需要手动重新指定 AutoCAD 安装目录下的AcMgd.dll。
  2. 把所有.dvb文件用压缩工具解压到独立文件夹。.dvb本质上是复合文档,里面嵌入了 VBA 工程,可以用 7-Zip 直接打开查看内部结构。这个技巧比启动 AutoCAD 逐个加载要快得多。具体做法:右键.dvb文件,用 7-Zip 的「打开压缩包」,能看到_VBA_PROJECT目录,里面就是 VBA 的源码模块。
  3. 找到名字里带Util、Common、Helper的模块,这部分是跨示例复用的基础函数,比如坐标转换、字符串处理。先读懂这些,后面的实例里大量调用它们,不先看会卡在「这个函数哪来的」这个问题上。

5.2 加载时最常见的三个报错及处理对策

报错信息出现原因处理对策
未安装 VBA 支持库AutoCAD 组件缺失下载对应版本 VBA Enabler 并安装,注意区分 32/64 位
无法运行文档中的宏宏被安全级别拦截命令行输入OPTIONS→「系统」→「安全选项」,把宏安全级别调为「启用所有宏」,或对文件右键「解除锁定」
加载 DLL 时找不到指定模块引用的托管 DLL 路径错误在 Visual Studio 中引用属性里把「复制本地」设为 False,并把 AutoCAD 的托管 DLL 放到系统能找到的路径

第三类报错在 64 位系统上偶尔还会伴随「未能加载文件或程序集 Autodesk.AutoCAD.Interop」。原因是引用了 Interop 程序集但没有启用 Embed Interop Types(嵌入互操作类型)。处理方式:选中引用,按 F4 打开属性,把「嵌入互操作类型」改为 False。这个问题在 .NET Framework 4.7 以上特别常见,因为默认是 True。

如果遇到NETLOAD之后命令不响应,检查你的类有没有写<CommandMethod>特性,以及类本身是否声明为Public。VB.NET 中类默认是Public,但如果你复制代码时把它改成了Friend,AutoCAD 反射可能看不到方法。从排错效率看,先看输出面板有没有「加载成功」字样,有则说明程序集加载没问题,问题在命令注册环节。

5.3 WPS 环境下的 VBA 兼容性问题

很多教程源码里附带了 Excel 导出相关的.bas模块,源码直接用Excel.Application。如果你本机装的是 WPS,且没有安装「WPS VBA 宏插件」,运行到CreateObject("Excel.Application")时会报「自动化错误」。处理方式有两种:其一,安装 WPS 官方提供的 VBA 宏插件下载包,但要注意它与 Office VBA 的运行库存在细微差异,比如ActiveSheet的属性加载顺序不一样;其二,把导出目标从 Excel 换成 CSV 文件,用Scripting.FileSystemObject直接写文本,这样完全不依赖 Excel 组件。从集成角度看,后者比前者更稳,因为两台电脑的 Office/WPS 环境几乎不可能完全一致。

6. 进阶技巧:日期版本化、性能定位和功能边界确认

做批处理工具时,给输出文件加时间戳是基本操作。VBA 里Format(Now, "yyyymmdd_hhmm")是主流写法,但注意Now返回的是本地时间,如果你的脚本分发给不同时区的同事使用,导出的文件名可能排序错乱。这时用两个 API 组合获得 UTC 时间戳更可靠:

Dim utc As Date utc = DateAdd("h", -TimeZone.CurrentTimeZone.GetUtcOffset(Now).Hours, Now) Dim stamp As String stamp = Format(utc, "yyyymmdd_hhmmss")

DateAdd的参数说明:第一个参数"h"表示按小时偏移,第二个参数是时区差的小时数,第三个参数是本地时间。VBA 没有内置的 UTC 获取函数,这是最标准的模拟方式。「vba 日期比较大小」这类搜索意图也和此相关——直接用<、>比较两个Date类型即可,但要注意 VBA 的日期类型是浮点数,整数部分是天数,小数部分是时间,直接相减得到的是天数差,乘以 24 才是小时差。

性能定位方面,写好的工具命令如果感觉慢,别急着优化对象模型。先在代码前后加计时:

Dim sw As New System.Diagnostics.Stopwatch() sw.Start() ' 你的处理逻辑 sw.Stop() ed.WriteMessage(vbLf & "耗时: " & sw.ElapsedMilliseconds & " ms" & vbLf)

Stopwatch是 .NET 的计时器,第一次调用会校验频率,所以在循环体外创建,不要在每次循环里重置。如果耗时集中在遍历实体的For Each,考虑改用手工过滤,比如用Editor.SelectAll加过滤条件,而不是全图遍历再逐个判断类型。一个判断依据是:10 万条实体的图纸,全图遍历在 2 秒内完成是正常水平,超过 5 秒说明你每访问一个实体都触发了 I/O 或字符串拼接等重操作,需要把这类操作移到事务外。

最后确认你的代码到底运行在哪个对象之上。命令行输入SYSVARMONITOR,如果能看到 ACM 相关的变量,说明当前进程既有 COM 也有 .NET 的宿主环境。调试时注意不要把Application类型混用——Application在 VBA 里是全局单例,在 VB.NET 里是ApplicationServices.Application,它的DocumentManager属性只在 .NET 侧存在。把 VBA 的ThisDrawing.Application.ActiveDocument直接翻译成 VB.NET 会报错,正确对照是Application.DocumentManager.MdiActiveDocument。这是从示例源码迁移时最容易出错的一行代码。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询