维拓标准接口开发项目总结

引言

维拓标准接口的开发也基本完成了,这也是我首次用 C# 完成定制项目。之前 Unity 学的很多东西都忘掉了,再捡起来用还是有些吃力,所以写了这篇博客总结一下。

C# 编程范式

数组越界

老生常谈的问题了,这里构造 Paper 的时候 item[] 如果没有这些索引又会抛出异常,但自己写的时候经常注意不到:

+if (item.Length < 2)
+ continue;

+try
+{
Paper paper = new Paper(item[0], Double.Parse(item[1]), Double.Parse(item[2]));
result.Add(paper);
+}

文件路径合并

之前对于需要合并的路径我都是这么通过对 string 直接进行加减来操作的:

string FilePath1 = @"C:\Users\MFGYF-WXY\source\repos";
string FilePath2 = @"\PLMInterface\bin\cadext.exe"
string MergeFilePath = FilePath1 + FilePath2;

但这种方式很业余,这种情况正确的处理是用 Path.Combine() 方法将两个路径进行合并:

string FilePath1 = @"C:\Users\MFGYF-WXY\source\repos";
string FilePath2 = @"\PLMInterface\bin\cadext.exe"
string MergeFilePath = Path.Combine(FilePath1, FilePath2);

程序配置与 <bindingRedirect/>

在完成部分接口把程序打包成exe时重新校对了一下 NewtonJson 的版本,之前在 Nuget 上下载的是 13 版本,但是 GStarCAD 目录下的版本是 12。将版本调整之后 exe 无法正常使用:

未经处理的异常:SystemI0.Fi1eLoadException:未能加载文件或程序集“Newtonsoft.Json,Version=13.0.0.0 Culture=neutral Pub1icKeyToken=30ad4fe6b2a6aeed”或它的某一个依赖项。找到的程序集清单定义与程序集引用不匹配。(异常来自 HRESULT:0x80131040)--->SystemIO.Fi1eLoadException:未能加载文件或程序集“Newtonsoft.Ison,Version=12.0.0.0,Culture=neutral, PublicKe yToken=30ad4fe6b2a6aeed”或它的某一个依赖项。找到的程序集清单定义与程序集引用不匹配。(异常来自 HRESULT:0x80131040)

再次检查 Nuget 版本已经调整过来了,但是编译之后的 exe 还是会报错 NewtonJson 版本错误。其实只需要调整一下 app.config 文件:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

问题在于 <bindingRedirect/> 这个标签,微软官方API文档 这里有比较详细的解释。

将一个程序集版本重定向到另一个版本。

<bindingRedirect
oldVersion="existing assembly version"
newVersion="new assembly version"/>
  • oldVersion:指定最初请求的程序集的版本。 程序集版本号的格式为 major.minor.build.revision。 该版本号的每个部分的有效值介于 0 和 65535 之间。
  • newVersion:指定要用来取代最初请求的版本的程序集版本(格式为:n.n.n.n) ,此值可以指定 oldVersion 之前的版本。

官方还给出了一个示例演示如何将一个程序集版本重定向到另一个版本:

<configuration>  
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="myAssembly"
publicKeyToken="32ab4ba45e0a69a1"
culture="neutral" />
<bindingRedirect oldVersion="1.0.0.0"
newVersion="2.0.0.0"/>
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

所以把这个标签删掉,程序就可以正常运行了,这是一个知识盲点记录下来。

读取 .dat 文件数据

所需要读取的 PaperSet.dat 长相如下,包含了国标图幅的各个数据尺寸:

Name   B      L     a     c    e     k    BD     LD
A0 841 1189 25 10 20 1 16 12
A1 594 841 25 10 20 1 12 8
A2 420 594 25 10 10 1 8 6
A3 297 420 25 5 10 1 2 2
A4 210 297 25 5 10 1 2 2
A0X2 1189 1682 25 10 20 3 16 24
A0X3 1189 2523 25 10 20 3 16 36
A1X3 841 1783 25 10 20 3 12 24
A1X4 841 2378 25 10 20 3 12 32
A2X3 594 1261 25 10 20 3 8 18
A2X4 594 1682 25 10 20 3 8 24
A2X5 594 2102 25 10 20 3 8 24
A3X3 420 891 25 10 10 2 6 12
A3X4 420 1189 25 10 10 2 6 16
A3X5 420 1486 25 10 10 2 6 10
A3X6 420 1783 25 10 10 3 6 12
A3X7 420 2080 25 10 10 3 6 14
A4X3 297 630 25 5 10 2 2 8
A4X4 297 841 25 5 10 2 2 12
A4X5 297 1051 25 5 10 2 2 12
A4X6 297 1261 25 5 10 3 2 12
A4X7 297 1471 25 5 10 3 2 14
A4X8 297 1682 25 5 10 3 2 16
A4X9 297 1892 25 5 10 3 2 18

观察数据结构,每行数据均以若干空格分隔图幅名称、长度、宽度等等数据,所以我们需要逐行去读取,先简单声明一个图幅信息类包含图幅名称和长宽:

/// <summary>
/// 国标图幅信息类
/// </summary>
public class Paper
{
public Paper(string Name, double Width, double Length)
{
this.Name = Name;
this.Length = Length;
this.Width = Width;
}

/// <summary>
/// 图幅名称
/// eg. A4
/// </summary>
public string Name { get; }

/// <summary>
/// 图幅长度
/// </summary>
public double Length { get; }

/// <summary>
/// 图幅宽度
/// </summary>
public double Width { get; }
}

之后通过 StreamReader 逐行读取(首行不读取)即可:

List<Paper> result = new List<Paper>();
string path = Path.Combine(installPath, @"MCADSetting\PaperSet.dat");

// 逐行读取 PaperSet.dat 数据
using (StreamReader reader = new StreamReader(path))
{
int index = 0;
string line;
while ((line = reader.ReadLine()) != null)
{
// 首行不作为数据读取,直接跳过
if (index++ == 0) continue;

// 去除字符串中的多余空格
string[] item = line.Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries);

if (item.Length < 2) continue;

Paper paper = new Paper(item[0], Double.Parse(item[1]), Double.Parse(item[2]));
result.Add(paper);
}
}

String.Split 方法可以参考微软的这篇 如何在 C# 中使用 String.Split 分隔字符串StringSplitOptions.RemoveEmptyEntries 参数来排除返回数组中的任何空字符串。要对返回的集合进行更复杂的处理,可使用 LINQ 来处理结果序列。

CADNET & PLM 接口使用

GetCADApplication

获取 CAD 应用程序,首先明确我们的思路:

  1. 若 CAD 程序已经存在,则直接跳转至已打开的 CAD 程序
  2. 否则新建 CAD 应用程序
try
{
// 先尝试是否可以直接捕获打开的CAD应用程序
IGcadApplication app = Marshal.GetActiveObject("GStarCAD.Application") as IGcadApplication;

// 避免出现后台打开的情况
app.Visible = true;
}
catch(Exception exception)
{
// 捕获失败则新建CAD应用程序
app = new GcadApplicationClass();
app.Visible = true;
......
}

这里捕获用到了 System.Runtime.InteropServices 中的 Marshal.GetActiveObject() 方法。

注意 app.Visible 可以让后台的应用程序前置显示,在忘记关闭 Quit() 时会出现多个后台运行程序,在项目的前期测试造成了不少麻烦。

CloseFile

该命令会传入一个图纸路径 filePath,关闭该指定路径的图纸。

关闭图纸的业务场景相对简单,只需要判断两点:

  1. 当前 CAD 是否打开过任何图纸
  2. 在已打开过的图纸中是否有和 filePath 相对应的图纸

最后对找到的图纸进行关闭操作即可。

// 图纸数量小于等于 0 则未打开任何图纸
if (app.Documents.Count <= 0)
return;

// 遍历当前已打开图纸
foreach (GcadDocument doc in app.Documents)
{
if (string.Equals(doc.FullName, filePath, StringComparison.OrdinalIgnoreCase))
{
doc.Close();
return; // 关闭操作成功
}
}

return; // 未找到关闭图纸

需要注意的是这里需要用 doc.FullName 而非 doc.Name,以规避出现诸如 C:\test.dwgD:\test.dwgName 中均为 test.dwg 的末端图纸路径重名的情况。

OpenFile

// 若当前已有打开图纸
if (app.Documents.Count > 0)
{
// 遍历当前已打开图纸
foreach (GcadDocument doc in app.Documents)
{
if (string.Equals(doc.FullName, path, StringComparison.OrdinalIgnoreCase))
return; // 图纸已经打开
}
}

// 没找到指定图纸则尝试打开该图纸
app.Documents.Open(path);

SaveFile

// 图纸数量小于等于 0 则未打开任何图纸
if (appController.CadApp.Documents.Count <= 0)
return Message.SendMessage(false, "没有图纸被打开,保存失败");

// 遍历当前已打开图纸
foreach (GcadDocument doc in appController.CadApp.Documents)
{
if (string.Equals(doc.FullName, path, StringComparison.OrdinalIgnoreCase))
{
// 只读模式下的图纸不保存
if (doc.ReadOnly)
return Message.SendMessage(false, "指定图纸为只读模式,保存失败"); ;

doc.Save();
}
}

return; // 找不到要保存的指定图纸,保存失败;

GetOpeningFile

if (appController.CadApp.Documents.Count <= 0)
return; // 没有图纸被打开

GcadDocument doc = appController.CadApp.ActiveDocument;
int titled = 0;
string dwgTitled = doc.GetVariable("DWGTITLED").ToString();

if (!int.TryParse(dwgTitled, out titled))
return; // DWGTITLED类型转换失败

return titled == 0 ? null : doc.FullName;

NewtonSoftJson自定义Convertor序列化Json

这个项目一个比较关键的过程就是去解析图纸数据并转换为 Json 格式,前面 PLM 接口负责解析数据,那么该如何将这些数据转换为 Json 呢?

查阅官网的 Custom JsonConverter,给出了一个比较完整的示例:

public class KeysJsonConverter : JsonConverter
{
private readonly Type[] _types;

public KeysJsonConverter(params Type[] types)
{
_types = types;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
JToken t = JToken.FromObject(value);

if (t.Type != JTokenType.Object)
{
t.WriteTo(writer);
}
else
{
JObject o = (JObject)t;
IList<string> propertyNames = o.Properties().Select(p => p.Name).ToList();

o.AddFirst(new JProperty("Keys", new JArray(propertyNames)));

o.WriteTo(writer);
}
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
throw new NotImplementedException("Unnecessary because CanRead is false. The type will skip the converter.");
}

public override bool CanRead
{
get { return false; }
}

public override bool CanConvert(Type objectType)
{
return _types.Any(t => t == objectType);
}
}

public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public IList<string> Roles { get; set; }
}
Employee employee = new Employee
{
FirstName = "James",
LastName = "Newton-King",
Roles = new List<string>
{
"Admin"
}
};

string json = JsonConvert.SerializeObject(employee, Formatting.Indented, new KeysJsonConverter(typeof(Employee)));

Console.WriteLine(json);
// {
// "Keys": [
// "FirstName",
// "LastName",
// "Roles"
// ],
// "FirstName": "James",
// "LastName": "Newton-King",
// "Roles": [
// "Admin"
// ]
// }

Employee newEmployee = JsonConvert.DeserializeObject<Employee>(json, new KeysJsonConverter(typeof(Employee)));

Console.WriteLine(newEmployee.FirstName);
// James

UML类图绘制