1. 从零开始接触PDM二次开发的真实起点
两天前我还在纠结要不要碰PDM这块,因为网上搜一圈,资料确实少得可怜。SolidWorks本身的API文档还算全,但PDM的二次开发资料基本要靠翻官方示例和去论坛里扒老帖,很多帖子还是十年前发的。Day1我主要在装环境、跑通最简单的登录逻辑,Day2才算真正开始碰PDM的数据结构和核心对象模型。
说实话,C#做PDM二次开发这件事,最劝退人的不是C#语法,也不是SolidWorks API,而是PDM那套自成体系的对象模型。它跟SolidWorks主体API完全是两套逻辑,理解了之后会发现设计得挺巧妙,但入门阶段确实容易一头雾水。
这篇内容适合谁看?如果你已经在用SolidWorks做设计或管理,想给公司的PDM库加点自定义功能;或者你是个C#开发者,刚接到PDM二次开发的需求,不知道从哪下手;再或者你就是单纯好奇PDM二次开发到底在搞什么——这篇应该都能给你一点参考。我会把Day2实际踩过的坑、验证过的思路、以及排查问题的完整过程都写出来,不光是"怎么做",更会讲"为什么这么做"。
先交代一下我的开发环境,后面所有代码和操作都基于这套环境:
- SolidWorks PDM Professional 2023(客户端装的是Professional版本)
- Visual Studio 2022,.NET Framework 4.8(PDM API对.NET Core的支持很有限,建议直接用Framework)
- C#,Windows Forms项目(PDM插件最常用的宿主就是WinForms,因为需要跟PDM客户端界面交互)
- 测试库用的是PDM自带的教学库,新建了一个专门用于测试的文件夹结构
这里有个很重要的前置知识:PDM二次开发分两种模式,一种是独立应用程序(Standalone),直接运行一个exe去连PDM库;另一种是插件模式(Add-in),在PDM客户端里注册DLL,跟着界面事件走。Day2我做的主要是第一种,独立程序去读取PDM数据,先把数据模型搞明白,后面再做插件会轻松很多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先把PDM整明白:它跟普通文件系统差在哪
想做好PDM二次开发,第一件事不是写代码,而是理解PDM的数据组织方式。很多SolidWorks用户把PDM当成一个"能检入检出文件的网盘",这个理解不能说错,但会严重限制你对API的理解。
PDM本质上是一个管理文件状态和关系的数据库系统,文件只是它管理的一个对象。它跟普通文件系统的核心区别在于以下几个方面。
2.1 PDM保存的不只是文件,而是一整套"状态"
一个文件在PDM里会经历工作流状态的变化,比如"正在编辑"到"已发布"。这些状态不是文件名前缀那种花活,而是数据库里真实记录的属性。当你用API去读取一个文件时,你能拿到的信息远不止文件名和路径,还包括版本、状态、变量(自定义属性)、关联关系、历史记录等等。
类比一下:普通文件系统是一个抽屉,里面放着文件夹和文件;PDM更像一个图书馆管理系统,每本书有编号、有借阅记录、有存放位置、有内容简介。你要写程序操作PDM,操作对象是"书"及其"元数据",而不是简单地在抽屉里挪动纸张。
2.2 版本与变量:PDM二次开发的两大核心概念
版本(Version):每次检入(Check In)都会生成一个新版本,上一个版本不会消失,而是被完整保存在数据库里。API里操作版本相关的方法,是PDM开发最频繁碰到的。
变量(Variable):这是PDM的灵魂。我们在SolidWorks里给零件填的那些自定义属性(比如材料、供应商、零件号),检入到PDM后会被映射成PDM变量。PDM二次开发很大一部分工作,就是读写这些变量。
Day2我发现的最关键的信息是:PDM变量在API里的操作方式,和直接读SolidWorks文件属性完全不一样。操作PDM变量需要走IEdmVariable和IEdmVariableMgr这套接口,而不是直接访问SolidWorks的CustomPropertyManager。这俩不能混用,一个是文件系统层面的,一个是数据库层面的,理解这个对后续开发方向很关键。
2.3 连接机制:不是直接"打开"文件,而是"获取"对象
第一次接触PDM API的人最容易犯的错,就是想用传统的"打开文件"方式去操作PDM里的文档。实际上PDM API的思路是:你登录PDM库之后,通过搜索或浏览拿到某个文件的ID,然后基于这个ID去获取文件对象、版本对象、变量对象。整个过程都是去数据库里"查"记录,而不像本地文件一样直接操作IO流。
csharp复制// 伪代码示意:PDM里面拿一个文件的路径不是直接给路径,而是先找对象
IEdmVault5 vault = new EdmVault5();
// 登录...
IEdmFolder5 folder = vault.GetFolderFromPath("路径");
// 再通过folder去枚举下面的文件对象
IEdmPos5 pos = folder.GetFirstChildFile();
这个思维模式的转变,是PDM二次开发入门的第一道坎。跨过去之后,后面理解API就顺很多了。
3. 环境搭建与第一个能跑的登录程序
Day1我把环境和项目结构搭好了,Day2开头又踩了个大坑,正好把这个过程完整记录下来。如果你还没搭环境,照着走就行。
3.1 添加PDM API引用的正确姿势
PDM的API不是一个独立的DLL,而是随PDM客户端安装附带的一系列COM组件。主要涉及的程序集在PDM安装目录下,一般是C:\Program Files\SOLIDWORKS PDM\这个路径。
在Visual Studio里添加引用时,有几种方式:
- 在"引用管理器"里选择"COM"选项卡,找
SolidWorks PDM Professional相关的组件 - 直接浏览到安装目录,引用
EdmInterface.dll等文件 - 把PDM的Interop程序集拷贝到项目目录,用相对路径引用(便于移植)
我推荐第三种做法,因为直接引用COM组件的话,版本一旦变化项目的引用路径可能失效,而拷贝到本地可以固定版本。
具体操作:先浏览到C:\Program Files\SOLIDWORKS PDM\目录,把EdmInterface.dll拷贝到项目的lib文件夹,然后在引用管理器里添加这个DLL。如果版本对不上,还要手动去PDM安装目录找对应文件。
Day2我踩的坑就在这:WinForms项目默认是"首选32位",而PDM客户端是64位进程,程序跑起来会报"类未注册"或者根本连不上PDM库。 解决办法是在项目属性的"生成"选项卡里,把"平台目标"改为x64,这个细节花了我将近半小时排查,后来发现是PDM API的COM组件是64位注册的,32位进程看不到它们。
3.2 最小可运行的登录流程
PDM的所有操作都始于登录。登录PDM库的逻辑比较固定,大致是这四步:
- 创建
EdmVault5对象 - 判断PDM客户端是否已关联到库
- 通过
Login方法登录 - 验证登录状态
csharp复制using EdmLib;
public class PdmService
{
private IEdmVault5 _vault;
public bool Login(string vaultName)
{
_vault = new EdmVault5();
// 判断库是否存在且已注册到当前客户端
if (!_vault.IsVaultName(vaultName))
{
Console.WriteLine($"找不到名为 {vaultName} 的PDM库");
return false;
}
// 判断是否已连接
if (_vault.IsLoggedIn)
{
Console.WriteLine($"当前已登录库: {_vault.Name}");
return true;
}
// 执行登录,最后一个参数传0表示不弹出登录窗口,使用Windows集成认证
bool result = _vault.Login(vaultName, "");
if (result)
{
Console.WriteLine($"登录成功,库路径: {_vault.RootFolderPath}");
}
else
{
Console.WriteLine("登录失败,请检查网络或账号权限");
}
return result;
}
}
这里有个细节值得留意:Login方法的参数不是"用户名/密码",而是库名和登录类型。PDM默认使用Windows集成认证,也就是你用当前Windows账号直接登录PDM库。如果你在PDM客户端里能做到无障碍操作,那么在独立程序里Login(vaultName, "")大概率也能直接通。
真正需要传密码的情况是使用PDM的特定账号认证,这在API文档里有说明,但绝大多数企业内部场景用Windows集成认证就够了。
3.3 登录成功后先看看能拿到什么
登录之后别急着写业务逻辑,先把基础信息打印出来验证一下连接是否正常。我用WinForms界面拉了三个文本框,分别显示库名、库路径、当前用户,代码长这样:
csharp复制private void btnLogin_Click(object sender, EventArgs e)
{
var service = new PdmService();
bool ok = service.Login("测试库");
if (ok)
{
var vault = service.GetVault();
txtVaultName.Text = vault.Name;
txtRootPath.Text = vault.RootFolderPath;
IEdmUserMgr5 userMgr = (IEdmUserMgr5)vault.GetObject(EdmObjectType.EdmObject_UserMgr);
IEdmUser5 currentUser = userMgr.GetCurrentUser();
txtCurrentUser.Text = currentUser.Name;
}
}
注意看GetObject(EdmObjectType.EdmObject_UserMgr)这段,这就是PDM API的典型风格——想拿任何东西,都要先找到对应的管理器对象,再去获取具体实例。UserMgr管理用户,VariableMgr管理变量,Search负责搜索,整个API体系就是一堆Manager带着一堆对象在转。
我的亲身感受是:PDM API学起来最费劲的地方就是这个"管理器模式",但它用熟了之后效率非常高,因为所有操作入口都非常明确,不用像操作Excel COM那样到处瞎试。
4. 一把梭遍历文件夹:树状结构的打开方式
登录验证通过后,我做的第一件正经事是遍历PDM库里的文件夹结构。这个功能看着简单,但它是后面所有复杂功能的基础——你要做文件批量改名、变量批量编辑、工作流批量操作,第一步基本都是"定位到某个文件夹,遍历里面的文件"。
4.1 从根目录开始往下走
PDM的文件夹树本质上是一个数据结构里的多叉树。根文件夹是IRootFolder,下面有若干子文件夹,每个子文件夹下面又可能有文件和子文件夹。API层次和我们直觉一致:
IEdmVault5.RootFolderPath可以拿到根目录的本地缓存路径IEdmFolder5表示一个PDM文件夹IEdmFolder5.GetFirstSubFolder()和GetNextSubFolder()用来遍历子文件夹IEdmFolder5.GetFirstChildFile()和GetNextChildFile()用来遍历文件
这里是最容易出bug的地方,原因是PDM API的枚举器**不是foreach,而是"先拿第一个,然后循环拿下一个"**的传统COM风格。直接foreach是编译不过的,必须自己控制循环。
csharp复制public void WalkFolder(IEdmFolder5 folder, int depth)
{
// 缩进显示层级关系
string indent = new string(' ', depth * 2);
Console.WriteLine($"{indent}[文件夹] {folder.Name}");
// 先遍历子文件夹(递归)
IEdmFolder5 subFolder = folder.GetFirstSubFolder();
while (subFolder != null)
{
WalkFolder(subFolder, depth + 1);
subFolder = folder.GetNextSubFolder();
}
// 再遍历当前文件夹下的文件
IEdmPos5 filePos = folder.GetFirstChildFile();
while (filePos != null)
{
IEdmFile5 file = new EdmFile5();
filePos.GetFile(file);
Console.WriteLine($"{indent} └─ [文件] {file.Name}");
// 拿到文件的变量信息
PrintFileVariables(file, depth + 1);
filePos = folder.GetNextChildFile(filePos);
}
}
有几个细节需要特别说明。
第一个细节:IEdmPos5是一个位置的句柄,代表你在文件列表里的当前位置。MDN文档里会把它比作"游标",这个类比很准确。你不能直接用foreach枚举文件,必须用GetFirstChildFile拿到第一个,然后不断调用GetNextChildFile(当前位置)向后移动,直到返回null表示遍历结束。
第二个细节是文件夹遍历和文件遍历的循环条件写法不一样——一个是.GetNextSubFolder()直接返回下一个对象,一个是.GetNextChildFile(当前Pos)需要传入当前位置。这个不对称性容易把人搞蒙,写代码的时候要留意。
第三个细节,在遍历文件时,我用了new EdmFile5()这个操作。PDM API里面EdmFile5是一个类,你可以提前new一个空对象,然后让GetFile方法把数据填充进去。如果你在一个循环里反复new对象,记得在循环外面声明,避免产生大量垃圾对象。
4.2 不是所有的文件都能"打开"
遍历过程中我发现一个现象:有些文件在PDM里是正常的SolidWorks零件,有些文件则处于"检入"状态,本地缓存里只有只读副本。这直接影响后续能不能对文件做写入操作。
PDM文件在本地其实有一份缓存(Cache),API操作的是这份缓存。什么时候缓存里是可写的?两种情况:一是文件被检出(Check Out)到当前用户;二是文件处于未检出的"本地新增"状态。其他情况下,缓存文件是只读的,想通过程序修改文件内容,必须先调用API执行检出。
这个坑我Day2就掉进去了——想读一个零件的自定义属性,程序报"没有权限",折腾半天发现是文件状态不是"检出"。后来我在遍历逻辑里加了状态判断:
csharp复制IEdmFile5 file = new EdmFile5();
filePos.GetFile(file);
// 判断文件当前检出状态
IEdmFile5 file5 = (IEdmFile5)file;
EdmFileState state = file5.GetState(file5.CurrentVersion);
Console.WriteLine($"文件: {file.Name}, 当前状态: {state.ToString()}");
PDM的文件状态有这么几种常见值:EdmFileState_CheckedIn(已检入)、EdmFileState_CheckedOut(已检出)、EdmFileState_Local(本地新建未检入)、EdmFileState_Locked(被锁定)。搞清楚这些状态,才能判断后续哪些操作可以做。
4.3 文件夹遍历的完整调用方法
前面写了一个递归的WalkFolder方法,实际调用时可以这样:
csharp复制private void btnBrowse_Click(object sender, EventArgs e)
{
var vault = service.GetVault();
// 拿到根文件夹
IEdmFolder5 rootFolder = vault.GetFolderFromPath(vault.RootFolderPath);
if (rootFolder == null)
{
MessageBox.Show("无法访问根目录,请先登录PDM库");
return;
}
// 清空输出
txtOutput.Clear();
// 遍历整个文件夹树
WalkFolder(rootFolder, 0);
MessageBox.Show("遍历完成,详见输出窗口");
}
这里有一个在实际开发中很实用的技巧:如果只想遍历某个子树,不一定要从根开始。vault.GetFolderFromPath("具体路径")可以直接定位到任意文件夹,然后把那个文件夹作为递归起点。这样你只处理项目相关的子目录,效率高很多,尤其是PDM库里面有几千个文件夹的时候。
5. 变量系统是PDM的灵魂,读它别踩这些坑
遍历文件的过程中,我已经简单调用了PrintFileVariables方法。现在专门展开讲变量系统,因为它是PDM二次开发最核心、也最容易搞混的部分。
5.1 变量到底是什么?它和SolidWorks属性什么关系?
前面提到过,变量(Variable)是PDM管理文件信息的方式。在SolidWorks里,用户给零件填的属性叫"自定义属性"(Custom Property),它们是存在文件内部的。PDM变量则是存在数据库里的,一个PDM变量可以通过"变量映射"和SolidWorks文件属性绑定。
举个例子:工程师在SolidWorks里给零件添加了材料这个自定义属性,值为304不锈钢。检入PDM后,PDM里也有一个叫材料的变量,通过映射规则,PDM自动从SolidWorks文件里抓取这个值存到数据库。这样即使不开SolidWorks,PDM查询界面也能直接看到每件材料的分类,甚至能做材料相关的批量搜索。
所以,在PDM二次开发里,读取变量不是打开SolidWorks文件去读属性,而是从PDM数据库的变量系统里查。这个思路贯穿整个开发。
5.2 用API读变量的完整代码
PDM支不支持变量,取决于你用的API版本。不同版本的PDM API在变量处理上有差异,我以PDM 2023为例,关键代码段如下:
csharp复制private void PrintFileVariables(IEdmFile5 file, int depth)
{
// 获取当前版本
IEdmVersion5 version = file.CurrentVersion;
// 获取变量管理器
IEdmVariableMgr5 variableMgr = (IEdmVariableMgr5)file.GetObject(EdmObjectType.EdmObject_VariableMgr);
// 列出所有变量
IEdmEnumeratorVariable5 enumerator = (IEdmEnumeratorVariable5)file.GetEnumerator(version, EdmVarEnumType.EdmVarEnumType_All);
string indent = new string(' ', depth * 2);
Console.WriteLine($"{indent} 变量列表:");
// 变量名和值成对读取
int count = enumerator.GetCount();
for (int i = 0; i < count; i++)
{
string varName;
EdmVariableType varType;
object varValue;
enumerator.GetNext(out varName, out varType, out varValue);
Console.WriteLine($"{indent} - {varName} = {varValue} (类型: {varType})");
}
}
关于这段代码,有几个关键点要说清楚。
变量的枚举方式是"名、类型、值"三件套。GetNext方法会一次返回三个信息,不仅仅是值。这个设计有历史原因——PDM的变量类型很多,包括字符串、数字、日期、布尔、列表、外部引用等等,为了统一,API干脆在操作时一并告诉你类型。
获取变量管理器:file.GetObject(EdmObjectType.EdmObject_VariableMgr)这个操作背后是创建一个变量管理器对象。在PDM API里,管理器对象是全局共享的,你不用每次读取都重新拿。IEdmEnumeratorVariable5这个临时对象更像一个"游标集合",用完要释放。
还有一点要提醒:如果程序运行时报"变量不存在",大概率是你在遍历的文件不是PDM管理的"文件对象",而是一个本地缓存文件路径。确认你拿到的是IEdmFile5而不是字符串路径。我用一个单独方法封装了变量读取逻辑,遇到变量为空时不要直接抛异常,而是打印Unknown,方便后续排查。
5.3 变量类型太多,统一处理要小心
PDM变量类型有十几种,在代码里统一处理时最容易踩坑的是日期类型。如果变量值是DateTime,直接ToString()会带出一堆时间格式,而你实际想要的可能是短日期。再比如数字变量,PDM可能以double或int返回,直接拼字符串容易丢失精度。
我的建议是封装一个变量值格式化方法来统一处理:
csharp复制private string FormatVariableValue(object value, EdmVariableType type)
{
if (value == null) return "";
switch (type)
{
case EdmVariableType.EdmVariableType_Date:
DateTime dt = (DateTime)value;
return dt.ToString("yyyy-MM-dd");
case EdmVariableType.EdmVariableType_Number:
if (value is double d) return d.ToString("0.####");
return value.ToString();
case EdmVariableType.EdmVariableType_Bool:
return ((bool)value) ? "是" : "否";
default:
return value.ToString();
}
}
这个封装看起来简单,但能省掉很多显示层的小麻烦。PDM数据库里的日期、数字、布尔,跟你在界面上看到的表现形式往往不一样,格式化函数就是"翻译层"。
6. 踩坑实录:COM对象释放那些事——不处理会崩
Day2后半段我一直在鼓捣一个批量读取的功能:把PDM库里所有零件和装配体的重要变量批量导出一个Excel清单。功能本身不难,但程序跑着跑着就内存暴涨,最后直接卡死。排查了很久,最后发现是COM对象没有释放导致的。
6.1 为什么PDM API会产生COM泄漏
PDM API基于COM(组件对象模型),C#里调用COM对象时,.NET会生成一个运行时可调用包装器(RCW)。COM对象的引用计数由RCW维护,但这个计数并不是自动归零的——只有当C#侧的RCW被垃圾回收(GC)时,才会释放底层COM对象。
问题在于:如果你在一个循环里创建了大量IEdmFolder5、IEdmPos5、IEdmFile5对象,并且这些对象被局部变量持有,它们可能长时间不被GC回收,导致底层COM对象一直占用内存。这种泄漏不会立刻报错,但会在跑批量的过程中逐渐累积,最后内存爆炸。
以下场景特别容易触发这个问题:
- 遍历大量文件夹和文件时,循环体内创建了很多临时COM对象
- 在递归方法里创建到底层的对象,方法退出后没有显式释放
- 使用了
foreach遍历COM集合(虽然PDM API有些集合支持),foreach结束不会自动释放RCW
6.2 一个简单的释放策略:宁可多释放,不能不放
我查了很多资料,最终的策略是在遍历方法里显式释放不用的COM对象。技术上有两种做法:
第一种:显式调用Marshal.ReleaseComObject
csharp复制using System.Runtime.InteropServices;
// 在使用完IEdmPos5之后
if (filePos != null)
{
Marshal.ReleaseComObject(filePos);
filePos = null;
}
第二种:用try-finally保证释放,或者用GC.Collect兜底
csharp复制IEdmPos5 filePos = folder.GetFirstChildFile();
try
{
while (filePos != null)
{
IEdmFile5 file = new EdmFile5();
filePos.GetFile(file);
// 处理文件...
filePos = folder.GetNextChildFile(filePos);
}
}
finally
{
if (filePos != null) Marshal.ReleaseComObject(filePos);
}
在实际开发中,我会在遍历文件夹树的递归方法入口处记录对象数量,在出口处统计没有释放的对象,然后统一释放。这种"记账"方式对我的排查帮助特别大——我能清楚地看到是哪一层循环没有释放对象。
不建议一上来就整个GC.Collect,因为强制回收会带来明显的性能抖动,在批量任务里可能让程序卡出奇怪的bug。先把循环里的临时变量都处理好,实在不行再考虑兜底。
6.3 排查过程的完整复盘
我的程序原本长这样,批量处理整个PDM库大概5000多个文件,跑到2000左右就卡死:
csharp复制// 有问题的版本
private void ProcessAllFiles(IEdmFolder5 folder)
{
IEdmPos5 filePos = folder.GetFirstChildFile();
while (filePos != null)
{
IEdmFile5 file = new EdmFile5();
filePos.GetFile(file);
// 重点项目:读取日期变量时可能返回DBNull
object value = GetVariableValue(file, "创建日期");
listBox1.Items.Add($"{file.Name} - {value}");
filePos = folder.GetNextChildFile(filePos);
}
// 递归子文件夹
IEdmFolder5 sub = folder.GetFirstSubFolder();
while (sub != null)
{
ProcessAllFiles(sub);
sub = folder.GetNextSubFolder();
}
}
这个版本跑久了内存占用一直往上走。我在循环末尾加了Marshal.ReleaseComObject之后,内存曲线很快就平稳了。具体改造:
csharp复制// 修正后的版本
private void ProcessAllFiles(IEdmFolder5 folder)
{
IEdmPos5 filePos = folder.GetFirstChildFile();
while (filePos != null)
{
IEdmFile5 file = new EdmFile5();
try
{
filePos.GetFile(file);
object value = GetVariableValue(file, "创建日期");
listBox1.Items.Add($"{file.Name} - {value}");
}
finally
{
if (file != null) Marshal.ReleaseComObject(file);
}
filePos = folder.GetNextChildFile(filePos);
}
IEdmFolder5 sub = folder.GetFirstSubFolder();
while (sub != null)
{
ProcessAllFiles(sub);
sub = folder.GetNextSubFolder();
}
}
顺带说一句,filePos = folder.GetNextChildFile(filePos)这一行里,参数filePos传的是之前的游标对象。如果我在循环里把它Marshal.ReleaseComObject了,就会导致下一次取下一个文件时报"变量已释放"错误。所以我在修正版本里没有在循环体释放filePos,而是在循环结尾用完后由GC统一处理。这也是我掉过的一个小坑。
其实还有更省心的方式——用PDM API自带的搜索接口直接一次拉取所有符合条件的文件,而不是一棵树一棵树地遍历。但搜索有它自己的复杂性(搜索配置、搜索范围、安全限制),Day2还没深入,我打算Day3再专门研究。
7. 一个真实业务场景:批量导出文件清单到Excel
把变量系统讲完之后,我趁热打铁做了一个比较实用的功能:把某个文件夹下所有SolidWorks零件的关键变量导出到Excel。这个功能在公司里特别常用——每次出图前,工程部都要整理一份物料清单,如果靠人手去PDM里一个个看,效率低还容易漏。程序自动导出,几分钟搞定。
7.1 先想清楚要导出哪些字段
动手前需要明确,导出什么字段取决于你们企业的PDM变量配置。我的演示环境里,变量字段的名字可能跟你公司的不同,但导出逻辑是一样的。我设计了这几个通用字段:
| 字段名 | 说明 | PDM变量类型 |
|---|---|---|
| 文件名 | PDM文件对象名 | 字符串 |
| 文件状态 | 检入/检出等 | 枚举 |
| 零件号 | 企业自定义变量 | 字符串 |
| 材料 | 企业自定义变量 | 字符串 |
| 重量 | 企业自定义变量 | 数字 |
| 创建日期 | 企业自定义变量 | 日期 |
拿不到某些变量的时候就留空,不要中断整个导出过程。
7.2 用最小依赖实现Excel导出
关于导出到Excel,网上有五花八门的方案。有人用NPOI,有人用EPPlus,有人直接用COM操作Excel。但Day2我选择了最简单的方案——生成CSV文件,然后用Excel打开。原因很简单:PDM API部分已经够复杂了,不要在导出环节再引入额外库,能出结果最重要。CSV是纯文本格式,用StreamWriter就能写,不需要任何第三方依赖,所有人都能用Excel打开。
csharp复制private void BtnExportCsv_Click(object sender, EventArgs e)
{
// 选择导出位置
SaveFileDialog dlg = new SaveFileDialog();
dlg.Filter = "CSV文件|*.csv";
if (dlg.ShowDialog() != DialogResult.OK) return;
var vault = service.GetVault();
if (vault == null) return;
// 定位到指定文件夹
IEdmFolder5 folder = vault.GetFolderFromPath(@"测试库\产品A");
if (folder == null)
{
MessageBox.Show("文件夹不存在,请检查路径");
return;
}
StringBuilder sb = new StringBuilder();
sb.AppendLine("文件名,零件号,材料,重量,创建日期,状态");
int count = 0;
ExportFolderToCsv(folder, sb, ref count);
File.WriteAllText(dlg.FileName, sb.ToString(), Encoding.UTF8);
MessageBox.Show($"导出完成,共 {count} 个文件");
}
可能你会问:StringBuilder把所有内容都存内存里,文件数量多不还是会卡?确实会。但当文件量在几千到一万以内时,StringBuilder的性能完全够用,而且CSV文件本身也就几百KB。等以后文件量大了,再改成边写边输出文件流的方式。
CSV文件还有一个细节是编码问题。如果直接用默认的File.WriteAllText(path, content),中文字符在Excel里会乱码。需要指定Encoding.UTF8,但最好在前面加BOM头(Encoding.UTF8默认就带BOM),这样Excel打开能正确识别UTF-8中文。如果你用new UTF8Encoding(false)(不带BOM),反而可能乱码,具体看你用的Excel版本。我实测下来带BOM是最稳妥的。
7.3 导出的完整实现
下面是ExportFolderToCsv方法,递归处理文件夹下的所有子文件夹和文件:
csharp复制private void ExportFolderToCsv(IEdmFolder5 folder, StringBuilder sb, ref int count)
{
// 先处理当前文件夹的文件
IEdmPos5 filePos = folder.GetFirstChildFile();
while (filePos != null)
{
IEdmFile5 file = new EdmFile5();
filePos.GetFile(file);
// 只处理SolidWorks零件和装配体,不处理工程图和pdf
string ext = Path.GetExtension(file.Name).ToLower();
if (ext == ".sldprt" || ext == ".sldasm")
{
// 读取变量值
string partNo = GetVariableString(file, "零件号");
string material = GetVariableString(file, "材料");
string weight = GetVariableString(file, "重量");
string created = GetVariableString(file, "创建日期");
// 获取状态
IEdmFile5 f5 = file;
EdmFileState state = f5.GetState(file.CurrentVersion);
string stateStr = state == EdmFileState.EdmFileState_CheckedIn ? "检入" :
state == EdmFileState.EdmFileState_CheckedOut ? "检出" :
"其他";
// 拼接CSV行,注意CSV的转义
sb.AppendLine($"\"{file.Name}\",\"{partNo}\",\"{material}\",\"{weight}\",\"{created}\",\"{stateStr}\"");
count++;
}
if (file != null) Marshal.ReleaseComObject(file);
filePos = folder.GetNextChildFile(filePos);
}
// 递归子文件夹
IEdmFolder5 subFolder = folder.GetFirstSubFolder();
while (subFolder != null)
{
ExportFolderToCsv(subFolder, sb, ref count);
subFolder = folder.GetNextSubFolder();
}
}
GetVariableString又是封装了一层:PDM API读变量返回的是object,我们统一转成string,读不到就返回空字符串,不能抛异常中断整体流程。
csharp复制private string GetVariableString(IEdmFile5 file, string variableName)
{
try
{
IEdmEnumeratorVariable5 enumerator =
(IEdmEnumeratorVariable5)file.GetEnumerator(file.CurrentVersion, EdmVarEnumType.EdmVarEnumType_All);
int count = enumerator.GetCount();
for (int i = 0; i < count; i++)
{
string name;
EdmVariableType type;
object value;
enumerator.GetNext(out name, out type, out value);
if (name == variableName)
{
return FormatVariableValue(value, type);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"读取变量 {variableName} 时出错: {ex.Message}");
}
return "";
}
这个功能做出来之后我立刻在测试库上跑了一把,效果挺直观:产品A文件夹下二十多个零件,文件名、零件号、材料、重量、状态全部正确导出。中途只发现一个问题——有个零件只有SolidWorks文件属性,没有映射到PDM变量,导致读取为空。这个不是代码bug,而是PDM端的变量映射配置问题,需要管理员在PDM管理控制台里配置好,开发程序时需要在文档或配置文件里标注清楚哪些变量是必须的。
8. 给Day3留的清单:下一步做插件还是做工具
Day2的内容大概就是从登录、遍历、读变量、导出CSV这条线走了一遍,算是把PDM API的骨架摸清了。如果你跟我一样刚接触PDM二次开发,Day2做到导出CSV的程度,已经具备了独立开发工具程序的基本能力。接下来可以考虑往这些方向深入。
8.1 插件模式:从离线程序到PDM内嵌
Day2做的都是独立程序,也就是"从外部连进PDM库"。但实际企业里,用得更多的是PDM插件,也就是把写好的DLL注册到PDM客户端里,用户直接在PDM界面右键点一下,就能弹出你的自定义菜单,执行批量操作。
插件模式和独立程序最大的区别是上下文关系不一样。独立程序每次运行都要重新登录和定位;插件模式是在PDM客户端进程里运行的,能拿到用户当前选中的文件或文件夹,并且能直接操作这个上下文对象。所以插件模式的切入点通常是:
- 右键菜单注册:可选文件/文件夹,注册自定义命令
- 事件响应:可以监听文件检入/检出、状态变更、变量修改等事件,按要求自动触发逻辑
- 自定义界面:在PDM客户端里嵌入你自己的管理面板
要把独立程序改成插件模式,整体思路不变,只是把一个"由你自己调用Login"的程序,改成"PTM客户端已经登录好了,你只管干活"的程序。登录、上下文获取的方式不同,剩余逻辑几乎能复用。这个我准备Day3用专门一篇来写,因为涉及IEdmAddin5接口和插件注册步骤,篇幅会比较长。
8.2 变量写入:从"读"到"写"的注意事项
Day2我们只做了读变量操作,没有涉及写变量。写变量比读变量复杂得多,因为牵扯到权限和版本状态:
- 写变量前必须确保文件被检出
- 写入后要提交(
IEdmFile5.SetVariables)并检入,或者在检入时一并写入 - 写变量涉及变量类型校验,类型不对会报错
想试写变量的同学,我建议在测试库上操作,不要在公司正式库上练手,不然写坏了没法恢复。
8.3 搜索接口:替代全库遍历的高效方案
Day2的遍历是"把所有文件先列出来再过滤",数据量大了效率很低。PDM API提供了搜索能力,可以按文件名、变量值、状态等条件直接查询,返回符合条件的文件ID列表,不需要遍历整个树。这个是我Day3很重要的一个探索方向。
计划清单大致是这样:
- 研究
IEdmSearch5接口的用法 - 尝试按"变量值"搜索,比如找出所有材料为304的不锈钢零件
- 做一个搜索界面的Demo,支持组合条件
- 如果可以,把搜索结果直接做成报表模板
9. 最后说几句实在话
说实话,Day2比Day1的收获大多了。Day1基本是在"安装环境、跑通登录"这种最外围的地方打转,Day2才真正碰到了PDM的核心机制。两天下来我最大的感受是:PDM API的入门门槛并不在编程本身,而在思维方式转换。
从普通C#编程切换到PDM编程,你需要接受几个不太符合直觉的设定:不能像操作本地文件夹那样直接访问路径;不能用一个统一的对象直接读写所有数据;操作前要搞清楚对象当前的状态和权限。这些设定初看觉得繁琐,但顺着PDM的设计目的想就很合理——它本来就不是给你当网盘用的,而是为了安全、规范地管理企业数据资产。API再怎么变,核心逻辑始终是"先找到对象,再操作对象,最后检查状态"。
如果你正在学PDM二次开发,我的建议是别急着写复杂功能,先把这三件事做扎实:登录、遍历、读变量。把这三件事做到不看文档就能写出来,后面的搜索、写入、插件、工作流都是同样的套路延伸。
另外分享一个我自己的小习惯:每做一个功能,就在代码里写清楚用到的API方法名和版本,同时在注释里贴一段MSDN链接或官方示例的关键行。PDM相关的代码片段实在太容易忘了,隔两周回来看可能完全不知道当时怎么调通的。这个习惯帮我省了很多回头翻文档的时间。
下一篇Day3,我会继续沿着这条线,先试着把PDM的搜索接口用起来,再研究怎么把独立程序改造成PDM客户端插件。如果你也在学PDM二次开发,欢迎一起交流踩坑经验。
