很多接触SolidWorks二次开发的C#新手,最容易有的一个疑问就是:我装了SolidWorks,也装了Visual Studio,代码也照着网上抄了,为什么一运行就报错,或者压根连SolidWorks都连不上?我当年踩这个坑的时候,翻遍了各种论坛,最后才把环境、引用、对象模型这几件事彻底搞清楚。这篇文章就是把我积累下来的这些经验一次性讲透,专门给准备入坑SolidWorks API开发的新手看。读完你能明白API开发到底能干什么、环境怎么搭、第一段能跑的代码怎么写、以及最核心的对象模型是怎么回事——这些都是后续所有二次开发功能的地基。
1. 先说清楚:SolidWorks API到底能干什么,值不值得学
1.1 二次开发最常见的三大应用场景
SolidWorks本身已经是一款很成熟的CAD软件,但再成熟的软件也没法覆盖所有人的需求。API二次开发解决的核心问题,总结下来其实就三类:批量重复操作、参数化自动建模、数据打通。
批量重复操作最常见。比如你有500个零件需要导成STEP格式,或者100张工程图需要批量转PDF,手工操作半天就过去了,人还容易出错。用API写一个循环,挂机跑几分钟全搞定。再比如BOM表导出,SolidWorks自带的BOM功能导出的格式往往不满足ERP系统的要求,用API就可以直接把BOM数据按你们公司的格式输出。
参数化自动建模是另外一个高频场景。很多企业做非标自动化设备,零件结构相似但尺寸随项目变化。这类需求可以在SolidWorks里建好参数化模板,再用C#通过API修改关键尺寸、更新模型、自动出图。设计人员只需要填一个订单参数表格,模型和图纸自动生成,效率提升不是一星半点。
数据打通则是把SolidWorks和企业其他系统联系起来,比如从PDM/PLM系统读取物料信息、把模型属性写入数据库、读取零件自定义属性生成采购清单。在这个方向上,SolidWorks API扮演的就是连接器角色。
1.2 宏录制、VBA宏、C#外接程序,新手该从哪个开始
SolidWorks提供了多种二次开发方式,新手容易一上来就懵:宏录制、VBA宏、C#插件、VB.NET插件,到底选哪个?
先说宏录制。SolidWorks自带的宏录制功能可以把你的手动操作录制为VBA代码。很多老工程师教新人的套路是:先手动操作一遍,录制成宏,然后在宏的基础上修改。这个思路用来学习API的对象模型和函数调用确实有效,我到现在查API用法时偶尔还会录一段宏看看它调了哪个接口。但宏录制的代码非常冗长,有很多视图刷新的噪声代码,不适合直接作为正式项目的基础。
VBA宏的优点是运行简单,工具-->宏-->运行即可,不需要编译环境。但它受限于SolidWorks进程内运行,单线程,且无法做复杂的界面交互。适合自己用的小工具、临时脚本,不适合做正式交付的应用程序。
C#外接程序(Add-In)是正规的二次开发主流方式。它可以做成独立的DLL注册到SolidWorks中,启动SolidWorks时自动加载,拥有完整的菜单、工具栏、任务窗格界面。也可以做成独立EXE,通过API连接SolidWorks进程进行操作。C#相对VBA的优势在于:完整的面向对象能力、更好的异常处理机制、方便与其他系统(数据库、WebService)集成、内存和性能管理更可控。
我建议新手的路线是:用宏录制辅助学习API调用方法,然后用C#写一个最简单的连接程序跑通环境,再逐步往插件方向深入。
2. 开发环境搭建:这一步卡住了80%的新手
2.1 SolidWorks版本与Visual Studio版本该怎么匹配
关于环境搭建,网上说啥的都有,其实核心就一句话:只要你的SolidWorks是2016以后的版本,用Visual Studio 2019或2022都完全没有问题,不用刻意追求版本一一对应。
SolidWorks从2015版本开始就全面转向.NET Framework 4.0以上,API类型库基本稳定。Visual Studio 2019/2022都可以正常创建.NET Framework 4.8项目来引用SolidWorks类型库。要注意的是,你创建的C#项目目标框架必须选择**.NET Framework**,而不是.NET Core/.NET 5+。别问我为什么知道,我见过太多人创建了.NET 6项目,然后发现引不了SolidWorks.Interop的COM类型库。
如果条件允许,建议用Visual Studio 2022 + SolidWorks 2020及以上版本。这套组合我实测稳定,调试体验也最好。SolidWorks版本太老(比如2014年以前),API接口和现代VS的支持都有一些别扭的地方,新手没必要给自己加难度。
2.2 引用的DLL到底是哪几个
新手常常困惑:我要引用哪个dll才算数?SolidWorks安装目录下有一堆dll,随便选行不行?
答案是:不用去安装目录翻。在Visual Studio的引用管理器里,切换到COM选项卡,搜索SolidWorks,你会看到下面几个主要项:
- SolidWorks Interop sldworks:这是核心API库,对应
SolidWorks.Interop.sldworks.dll,包含SldWorks、ModelDoc2、PartDoc、AssemblyDoc等核心接口。 - SolidWorks Interop swconst:常量库,对应
SolidWorks.Interop.swconst.dll,包含所有枚举常量,比如文件保存类型、特征类型、单位制等。
在实际项目中,这两个都要引用。swconst里的常量如果记不住,开发时直接在代码里输入swConst_,智能提示会列出一大串候选。
除了这两个,还有一些按需引用的库:SolidWorks.Interop.swpublished(用来开发Add-In插件注册用)、SolidWorks.Interop.swcommands(插件命令ID定义)。做第一阶段的连接程序不需要它们。
注意:通过COM选项卡添加的引用,Visual Studio会自动给这些dll启用"嵌入互操作类型"(Embed Interop Types)。对SolidWorks API开发来说,这个选项最好手动改成False,否则后面容易出现类型转换异常。怎么改:选中引用,在属性面板里把"嵌入互操作类型"改为"False"。
2.3 AnythingCPU问题是64位时代最大的坑
引用搞定了,项目平台没选对的话,程序一运行照样崩。现在的SolidWorks基本都是64位版本,对应的C#项目必须设置成x64平台。
Visual Studio新建的C#项目默认是AnyCPU,在64位操作系统上运行时默认采用64位进程,理论上没问题,但新手很容易在NuGet装了某些32位依赖包后,或者在调试时被VS强制切回x86,然后程序一跑就报错“试图加载格式不正确的程序”。这个报错十有八九是平台目标不一致。
项目设置的位置:右键项目 -> 属性 -> 生成 -> 平台目标 -> 选择x64。
另外一个容易忽略的点:如果是64位SolidWorks,在调试时把VS里的调试平台也切到x64。在工具栏的"解决方案配置"旁边一般有个平台下拉框,确保是x64而不是x86或Any CPU。
2.4 验证环境是否可用的最快方法
环境搭建完,最快速的验证方法是创建一个控制台项目,引用上述两个Interop DLL,设置x64平台,然后写一段三行代码尝试连接SolidWorks。这一步跑通了,环境问题就彻底翻篇了。
验证代码我这里先不展开,下一节详细讲。这里要叮嘱的只有一件事:验证环境的项目一定用控制台应用程序,而不是Windows窗体或类库。控制台项目最简单、启动最快、没有UI线程干扰,最适合用来做边界测试。
3. 跑通第一个C#程序:连接SolidWorks并操作文档
3.1 连接方式:GetObject还是CreateObject,这是个问题
在C#里连接SolidWorks,网上最常见的两种写法:
// 写法一:获取正在运行的SolidWorks实例 SolidWorks.Interop.sldworks.SldWorks swApp = Marshal.GetActiveObject("SldWorks.Application") as SldWorks.Interop.sldworks.SldWorks; // 写法二:创建新的SolidWorks实例 SldWorks swApp = new SldWorks();这两个写法各有应用场景,但新手往往不知道该用哪个。
写法一(GetActiveObject):连接已经打开的SolidWorks窗口。好处是快,不占用额外内存;坏处是如果用户没打开SolidWorks,程序会直接报错(获取不到活动对象)。适合独立EXE程序在外部连接SolidWorks的场景。
写法二(new SldWorks):创建一个新的SolidWorks进程。好处是不管SolidWorks有没有打开,都能保证有一个可用实例;坏处是如果SolidWorks已经开了,再new一个就等于多开了一个SolidWorks进程,内存消耗大,而且两次创建的实例ID不同,后续通过API操作起来逻辑会乱。
我推荐的稳妥写法:先尝试获取正在运行的实例,如果获取不到再创建新实例。代码里用try-catch包一层判断即可。
3.2 完整可运行的连接程序代码
下面是一个可以直接跑通的完整案例。这个程序做了三件事:连接SolidWorks、新建一个零件文档、向文档写入一行注释文字。用它来验证环境非常合适。
using System; using System.Runtime.InteropServices; using SolidWorks.Interop.sldworks; using SolidWorks.Interop.swconst; namespace SolidWorksApiFirstTry { class Program { static void Main(string[] args) { // 1. 连接SolidWorks SldWorks swApp = ConnectToSolidWorks(); if (swApp == null) { Console.WriteLine("连接SolidWorks失败,请确认软件已安装并能正常运行。"); return; } // 2. 显示SolidWorks窗口(如果被隐藏的话) swApp.Visible = true; // 3. 新建一个零件文档 int error = 0; int warning = 0; ModelDoc2 swDoc = swApp.NewDocument( "D:\\Program Files\\SOLIDWORKS Corp\\SOLIDWORKS\\data\\templates\\零件.prtdot", 0, 0.01, 0.01, ref error, ref warning) as ModelDoc2; if (swDoc == null) { Console.WriteLine($"新建文档失败,错误码:{error}"); return; } // 4. 向模型写入一行业务信息 bool ret = swDoc.SetTitle3("第一次连接的测试零件"); if (ret) { Console.WriteLine("文档标题设置成功"); } // 5. 保存(这里演示保存到D盘根目录,可以自行修改路径) int saveError = 0; int saveWarning = 0; bool saveRet = swDoc.Extension.SaveAs( "D:\\first_part.SLDPRT", 0, (int)swSaveAsOptions_e.swSaveAsOptions_Silent, null, ref saveError, ref saveWarning); if (saveRet) { Console.WriteLine("零件已保存:D:\\first_part.SLDPRT"); } else { Console.WriteLine($"保存失败,错误码:{saveError}"); } Console.WriteLine("按任意键结束程序..."); Console.ReadKey(); } static SldWorks ConnectToSolidWorks() { try { // 尝试获取正在运行的SolidWorks实例 object obj = Marshal.GetActiveObject("SldWorks.Application"); if (obj != null) { return obj as SldWorks; } } catch { // 没有正在运行的实例,进入catch逻辑 } // 创建新的SolidWorks实例 try { SldWorks swApp = new SldWorks(); return swApp; } catch (Exception ex) { Console.WriteLine("创建SolidWorks实例失败:" + ex.Message); return null; } } } }提示:NewDocument的模板路径绝不能照抄我代码里的。每个机器SolidWorks安装位置不同,模板路径也不同。正确的获取方式见下方第3.3节。
3.3 新手的第一个拦路虎:模板路径变硬编码了
运行上面的代码,你大概率会遇到的问题是:找不到模板文件(错误码非0或直接抛异常)。这是因为每个SolidWorks版本的模板路径都不一样,你电脑上的模板不一定在D盘那个路径。
正确的做法是从已经打开的SolidWorks实例获取默认模板路径,而不是自己猜。改法如下:
// 获取默认模板路径 string defaultTemplate = (string)swApp.GetUserPreferenceStringValue( (int)swUserPreferenceStringValue_e.swDefaultTemplatePart); // 用默认模板新建零件 ModelDoc2 swDoc = swApp.NewDocument( defaultTemplate, 0, 0.01, 0.01, ref error, ref warning) as ModelDoc2;GetUserPreferenceStringValue这个方法在后续开发中也会频繁用到,它用来读取SolidWorks的各种用户设置。这里我们读的是默认零件模板路径,拿到之后直接传给NewDocument,就再也不会出现"模板文件找不到"的问题了。
3.4 调试时如何验证程序确实连上了SolidWorks
程序跑起来后,如果你看到SolidWorks窗口自动弹出来,并且新建了一个零件,那就说明连接成功、基本对象调用成功。这时环境算是彻底通过了。
如果你想进一步确认,可以在代码里加一个判断语句:输出当前SolidWorks的版本号和当前文档名。
Console.WriteLine($"SolidWorks版本:{swApp.RevisionNumber()}"); Console.WriteLine($"当前文档标题:{swDoc.GetTitle()}");这里顺便提一个很多新手都会遇到的问题:程序一退出,SolidWorks也自动退出了。
为什么?因为你的进程通过new SldWorks()创建了SolidWorks实例,当你的程序进程结束时,这个实例失去了创建者,部分系统配置下会自动退出。如果不想这样,可以在程序退出前把SolidWorks主窗口显示出来,或者在代码里设置swApp.Visible = true之后不要再隐藏它。如果是用GetActiveObject连接已经打开的实例,就不存在这个问题。
4. 理解SolidWorks API的对象模型层级:这是整个二次开发的核心地图
4.1 从SldWorks到Feature,整个API就是一棵树
一个SolidWorks文档在API层面是怎么组织数据的?搞懂了这点,后面所有功能开发就等于拿到了地图。
SolidWorks API整体是树状结构,从上往下大致是这样的:
| 层级 | 类/接口 | 作用 | 类比 |
|---|---|---|---|
| 应用层 | SldWorks | 管理SolidWorks进程、文件操作、系统设置 | 整个软件的操作系统 |
| 文档层 | ModelDoc2 / ModelDocExtension | 管理文档文件,对应零件、装配体、工程图 | 一个打开的Word文件 |
| 子文档层 | PartDoc / AssemblyDoc / DrawingDoc | 访问特征树、配合关系、视图等 | Word里的段落和图片 |
| 特征层 | Feature | 所有建模步骤,拉伸、旋转、圆角都在这 | Word里的一个对象(文本框/图表) |
| 几何层 | Body / Face / Edge / Vertex | 访问实体几何拓扑信息 | 对象内部的文字和坐标数据 |
在代码里最核心的就是这个对应关系:SldWorks(应用)-> ModelDoc2(文档)-> Feature(特征)-> Body/Face等(几何)。
日常开发中,几乎一半以上的功能都是围绕这三层之间的跳转和遍历展开的。
4.2 从文档到特征:遍历一个零件里的所有特征
看一段最典型的遍历代码。这段代码能把当前零件文档里所有特征的名称打印出来:
ModelDoc2 swDoc = swApp.ActiveDoc as ModelDoc2; if (swDoc == null) return; Feature swFeat = swDoc.FirstFeature() as Feature; while (swFeat != null) { Console.WriteLine($"特征名称:{swFeat.Name}"); // 如果特征有子特征,比如阵列产生的一组特征,也需要遍历 Feature subFeat = swFeat.GetFirstSubFeature() as Feature; while (subFeat != null) { Console.WriteLine($" ├─ 子特征:{subFeat.Name}"); subFeat = subFeat.GetNextSubFeature() as Feature; } swFeat = swFeat.GetNextFeature() as Feature; }这套遍历套路就是经典的链表遍历模式:先取第一个,然后不断调用GetNext直到返回null。SolidWorks API很多集合都钟爱这种模式,包括特征、面、边、视图等等。记住这个模式,后面能省很多时间。
4.3 按名称查找对象,是新手最容易懵的地方
有时候你不需要遍历所有特征,只需要找到某一个叫"拉伸1"的特征改它的尺寸。用名称查找比遍历高效得多。
关键方法是ModelDocExtension接口下的FindFeatureByName或者广义的FindObjectByName:
ModelDocExtension swExt = swDoc.Extension; Feature feat = swExt.FindFeatureByName("拉伸1") as Feature; if (feat != null) { Console.WriteLine($"找到了特征:{feat.Name}"); }这里有个隐藏的小陷阱要提醒:特征名称不是唯一的。同一个SolidWorks文档里,不同层级的特征(比如零件里的特征和阵列里的子特征)名称可能相同。FindFeatureByName默认找到的是第一个匹配项。如果文档结构复杂,精确查找还是得走遍历,同时比对特征类型和上游下游关系。
4.4 SelectionManager:和SolidWorks界面交互的桥梁
做二次开发经常遇到一个需求:用户在SolidWorks界面里手动点选了一个面,你的程序要读出这个面的面积。这就要用到SelectionManager。
// 获取用户当前选中的对象 SelectionMgr selMgr = swApp.ISelectionManager as SelectionMgr; int count = selMgr.GetSelectedObjectCount2(-1); for (int i = 1; i <= count; i++) { object selObj = selMgr.GetSelectedObject6(i, -1) as object; if (selObj is Face2) { Face2 face = selObj as Face2; double area = face.GetArea(); Console.WriteLine($"选中了面,面积是:{area} 平方米"); } }GetSelectedObjectCount2和GetSelectedObject6里的第二个参数是标记值,传-1表示忽略标记,取所有选中对象。如果你是编程向的选择(代码里通过Select4选中),可以设置不同的标记值来区分用途——比如你先选中一个面当"目标面",再选中一条边当"方向参考",就可以分别标记为1和2,然后用标记值过滤取出对应的对象。
选中的对象返回类型是object,使用前一定要用is/as做类型判断。你永远不知道用户选的是面、边、还是草图点。
4.5 属性(CustomProperty)操作:和PDM/ERP对接的基础
很多二次开发项目最终都要读取或写入文件的自定义属性,比如零件号、材料、设计师、版本号。
// 写入自定义属性 string configName = ""; bool ret = swDoc.SetCustomProperty(configName, "零件号", "PART-001"); ret = swDoc.SetCustomProperty(configName, "材料", "Q235"); // 读取自定义属性 string partNo = swDoc.GetCustomProperty(configName, "零件号") as string; Console.WriteLine($"零件号:{partNo}");需要注意configName参数。如果你传入空字符串,读写的是"默认配置"的自定义属性。如果你的模型里存在多个配置(Configuration),必须指定配置名,否则可能出现属性读不到或者读到默认配置的属性。这一点在从装配体里循环读取每个零件属性时尤其重要。
5. 新手阶段最容易踩的坑:崩溃、类型不匹配、API文档看不懂
5.1 为什么我的程序一运行SolidWorks就崩溃
这是新手反馈最多的问题。SolidWorks异常崩溃的原因很多,但二次开发导致的崩溃,最常见的是这几类:
COM对象生命周期管理不当。SolidWorks API本质是COM接口,C#通过运行时互操作调用。如果一个COM对象被垃圾回收机制在错误的时间点释放了,SolidWorks进程可能就崩溃了。这个问题在循环遍历大量特征/面/凸台时尤其容易出现。
规避方式:避免在循环内频繁调用Marshal.ReleaseComObject,让运行时自己管理大部分对象即可。只有在处理大量几何对象(几万条边)时,才考虑配合Marshal.ReleaseComObject手动释放。
在错误的时间操作模型。比如在模型重建(Rebuild)过程中去访问特征树,或者在文档尚未加载完成时就去读属性。新手常犯的错是:新建文档后立即访问它的特征,其实文档虽然返回了,但内部还没初始化好。稳妥做法是新建/打开文档后,先swDoc.ForceRebuild3(false)重建一次,或者用循环等待swDoc.GetType()!= 0确认文档状态正常。
5.2 “调用目标发生了异常”和“Object reference not set”到底是谁的锅
这两行错误信息几乎每个二次开发新手都见过。我的经验是:
调用目标发生了异常(TargetInvocationException)大多是COM异常被封装成了.NET异常。表面上看是抛了异常,背后往往是调用的API本身返回了失败,或者参数类型不对。解决思路是:捕获异常后看InnerException,大多数情况下真实原因在InnerException里。如果一个API调用在界面上手动操作能成功,但程序调用失败,优先怀疑参数类型和null值。
Object reference not set to an instance of an object(NullReferenceException)更是家常便饭。SolidWorks API很多方法如果执行失败,不会抛异常,而是直接返回null。新手容易直接拿返回结果继续操作,于是空引用异常就出现了。
养成习惯:每个API调用返回的对象,先判空再使用。别看这习惯简单,能帮你省掉一晚上排查时间。
5.3 类型库版本冲突:为什么同一份代码换个电脑就报错
在一个项目里,如果你同时引用了通过COM添加的Interop DLL和从SolidWorks安装目录直接添加的DLL,版本不同会出现"强名称签名冲突"或者"类型定义无法解析"这一类问题。
另外,SolidWorks的Interop类型库是强签名的,如果一个类型被两个不同的程序集各自定义了一份,CLR就分不清了。避免这个问题的办法简单:只通过COM选项卡添加Interop引用,并且把嵌入互操作类型设为False。不要混着引用不同路径下的同一系列DLL。
5.4 怎么查API文档效率最高
最后说说文档查询。SolidWorks安装目录下自带API帮助文档,一般在SolidWorks安装目录\api\apihelp.html。但说实话,必须连接到SolidWorks官网的在线API帮助才能查看具体方法和属性,本地端只有索引。
我的查阅习惯是这样的:
- 优先用VS的智能提示。输入对象名+小数点,弹出的方法列表里基本能看到所有可用API,配合XML注释足够解决80%的问题。
- 不确定参数含义时,打开API帮助查具体方法。在API帮助的索引里输入方法名,比如
SelectByID2,能看到参数说明、返回值含义和示例代码。 - 录一段宏,看SolidWorks自己怎么调。这一步真的非常管用。比如我想知道"保存PDF"用哪个API,手动操作一遍同时录制宏,录出来的VBA代码里直接就有
ExportToPdf之类的调用,翻译成C#只是语法层面的转换。 - 遇到属性(Property)和接口(Interface)的多版本问题,比如
ModelDoc2和ModelDocExtension,很多新功能只在Extension接口里有。习惯上优先使用ModelDocExtension,因为它是后来扩展功能的主入口。
6. 从控制台程序到真正的插件:新手下一步该往哪走
6.1 控制台程序能做什么,做不了什么
学会了上面这些内容,你已经可以用控制台程序完成很多实用工具了。比如批量转格式、批量读取属性、自动生成BOM等,这类独立运行的程序本质上不依赖SolidWorks界面,适合做后台批处理工具。
但控制台程序有两个先天局限:一是它需要独立启动,无法被集成到SolidWorks的菜单和工具栏里;二是它运行起来之后,界面交互能力弱——无法添加窗体和控件,无法让用户在SolidWorks里"点选-执行-反馈"这种交互式操作。
如果你的工具需要设计人员经常用、反复用,做成插件(Add-In)才是正道。
6.2 Add-In开发的核心要点提前预告
在SolidWorks里注册一个Add-In插件,核心要解决三件事:
第一,实现ISwAddin接口。这是SolidWorks加载插件的契约。插件类继承这个接口后,需要实现ConnectToSW和DisconnectFromSW两个方法。前者在插件加载时被调用,用于初始化菜单和命令;后者在插件卸载时被调用,用于清理资源。
public class MyAddin : ISwAddin { public bool ConnectToSW(object ThisSW, int Cookie) { // 保存SolidWorks应用对象、Cookie; // 调用CommandManager注册命令、菜单、工具条 return true; } public bool DisconnectFromSW() { // 注销命令、释放资源 return true; } }第二,注册表注册与swAddin文件。要在SolidWorks中识别你的插件,需要修改注册表,在HKEY_CURRENT_USER\Software\SolidWorks\AddIns下新建项,写入插件ID(Guid)和DLL路径。同时SolidWorks要求插件DLL旁边放一个.swAddin文件,里面记录了插件的ID和加载方式。
第三步,命令ID分配。SolidWorks插件的每个菜单、按钮都绑定一个命令ID,是从swCommands_e常量范围里分配出来的。注册菜单、绑定命令ID、处理点击事件回掉,这个环节对新手来说很容易绕晕,后面我会单独用一篇详细讲——这也是我把这个系列定位为"新手引导"的一部分。
6.3 中间过渡方案:WinForm独立程序先顶一阵子
如果你暂时不打算踩Add-In的坑,但又想做带界面的工具,一个折中方案是:用C#写一个WinForm独立程序,界面放在自己程序的窗体里,通过API连接SolidWorks,用户点击窗体上的按钮,程序操作SolidWorks完成对应功能。
这个是很多公司"轻量化二次开发"的典型做法。优点是开发难度比Add-In低一个量级,不需要注册表,不需要理解命令ID机制,调试也简单。缺点是每次都要先启动程序再连接SolidWorks,且交互体验远不如原生插件。
我个人的建议是:如果你的工具预期只服务你自己或者同一个办公室里的几个人,独立WinForm程序完全够用。如果工具要大范围铺开、甚至要打包安装包交付给客户,那还是老老实实做Add-In插件。
这个系列下一步的文章会分别讲Add-In从零到注册成功的完整过程、以及如何用TaskPane做自定义UI界面。先把这篇文章里的环境搭建和第一个程序跑通,你在SolidWorks API开发这条路上就算是正式入了门。有事没事打开Visual Studio敲一敲,把遍历特征、读写自定义属性这些基础操作练到闭眼能写,后面学什么都快。