Initial working version

This commit is contained in:
2026-05-02 10:18:00 +08:00
commit 5bc65d33ad
14 changed files with 1762 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
# Build results
bin/
obj/
# Visual Studio
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
# Rider
.idea/
# VS Code
.code/
# Rhino plugin output (should be .rhp, not tracked)
*.rhp
*.dll
# OS
Thumbs.db
.DS_Store

27
CHANGELOG.md Normal file
View File

@@ -0,0 +1,27 @@
# 更新日志
所有重要版本更新会记录在此文件。
## [0.1.0] - 2026-05-01
### 新增
- 项目骨架创建(插件主类、命令入口)
- XML 配置文件解析器
- 动态表单 UI根据 XML 生成 TextBox / ComboBox 控件
- 支持 string 类型字段TextBox
- 支持 list 类型字段ComboBox 下拉选择)
- 多 XML 模板支持:可同时加载多个 XML 文件
- 四种导入合并模式:追加、替代、合并更新、智能合并
- `XmlImportDialog`:导入时弹出模式选择对话框
- 模板持久化Rhino 重启后自动恢复已加载的模板
- `TemplateManager`:管理模板生命周期和 index 文件
- 默认 XML 模板内嵌在 DLL 中,首次启动自动提取
- 命令名:`UserstringProperty`
### 功能
- 选择 Block 后弹出属性编辑窗口
- 属性保存到 Rhino Block 的 UserString键值对存储
- 分隔标题自动识别(属性名含 `---` 时显示为分组标题)
### 文档
- README、LICENSE、.gitignore、CONTRIBUTING、CHANGELOG

17
CLAUDE.md Normal file
View File

@@ -0,0 +1,17 @@
> 本文件仅保留高优先级、易混淆、跨会话必须一致的约束。
> 本文件必须极精简,占用大模型注意力视为失效。
## 项目一句话
Rhino 8 C# 插件(.rhp管理 Block 的 UserString 属性,支持 XML 模板导入。
## 自动化约定
- 禁止自行 commit / push git用户下达指令再执行
- 完成任务后清理临时文件
## 文档入口
- PLAN.md待办事项
- CHANGELOG.md版本记录
- CLAUDE.md仅包含本文件不要混入项目说明

31
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,31 @@
# 贡献指南
欢迎提交 Issue 和 Pull Request
## 如何贡献
### 报告问题
发现 Bug 或有新功能想法?请先搜索 [Issues](https://gitea.com/user/project/issues) 确保不是重复提交,然后按以下格式描述:
- **问题描述**:清晰说明问题或建议
- **复现步骤**:如果是 Bug列出具体步骤
- **环境信息**Rhino 版本、操作系统、.NET SDK 版本
### 提交代码
1. Fork 本项目
2. 创建分支 (`git checkout -b feature/your-feature`)
3. 确保代码符合项目风格,编译通过
4. 提交时请写清楚改动内容和原因
5. Push 到你的 Fork发起 Pull Request
### 代码规范
- C# 代码遵循 [C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)
- 公共方法需有 XML 注释
- 新功能请附带测试或更新文档
## 问题解答
如有问题可以在 [Discussions](https://gitea.com/user/project/discussions) 中讨论。

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

123
README.md Normal file
View File

@@ -0,0 +1,123 @@
# UserStringProperty
Rhino 插件,根据用户自定义 XML 配置,给 Block 实例附加属性UserString
## 功能
- 根据 XML 配置文件动态生成属性编辑界面
- 支持 `string` 类型字段TextBox`list` 类型字段ComboBox
- 自动保存到 Rhino Block 的 UserString
- 支持多个 XML 模板同时加载、合并
- 模板自动持久化Rhino 重启后自动恢复
- 导入时检测属性集冲突,覆盖或新增
## 安装
### 编译
```bash
cd UserStringProperty
dotnet build
```
输出:`UserStringProperty/bin/UserStringProperty.dll`
### 加载到 Rhino
1.`bin/UserStringProperty.dll` 重命名为 `UserStringProperty.rhp`
2. Rhino → `Options``Plug-ins``Install` → 选择 `.rhp` 文件
### 使用
1. 在 Rhino 中输入命令:`UserstringProperty`
2. 选择要编辑的 Block 实例
3. 默认模板首次启动自动加载;点 **加载XML** 导入自定义模板
4. 在弹出的对话框中选择 cmig 和属性集,若同名已存在则需确认覆盖
5. 在下拉框中选择属性集,编辑各字段值
6. 点击 **写入属性**
## 模板导入
加载 XML 时,会弹出对话框让选择 cmig 和属性集,检测同名冲突:
- **同名属性集不存在** → 直接保存为新模板
- **同名属性集已存在** → 需确认是否覆盖
## XML 配置格式
插件根据 XML 动态生成 UI。示例 XML 格式如下:
```xml
<?xml version="1.0" encoding="utf-8"?>
<cmig name="构件属性">
<propertyset name="构件属性-基本" exclusive="false">
<!-- string 类型 → TextBox -->
<property name="0-00-02-年代" type="string" default="PRC"/>
<!-- list 类型 → ComboBox -->
<property name="0-00-06-构件类型" type="list" default="DS-JG">
<option>DS-JG</option>
<option>DS-WH</option>
<option>DS-SB</option>
</property>
<!-- 分隔标题default 值为一串 dash 且 type="string" -->
<property name="0-01-00-构件基本属性" type="string" default="--------------------" />
</propertyset>
</cmig>
```
### 字段类型
| type | 控件 | 说明 |
|------|------|------|
| `string` | TextBox | 自由文本输入 |
| `list` | ComboBox | 下拉选项(通过 `<option>` 定义) |
### 分隔标题
属性名的 `default` 值以 `---` 开头时(即 `default="--------------------"`),该属性显示为分组标题栏。
## 模板存储
已加载的 XML 模板会复制一份保存到插件目录,重启 Rhino 后自动恢复:
```
%APPDATA%\McNeel\Rhinoceros\8.0\plugins\UserStringProperty\
├── template_index.txt # 记录已加载的 XML 路径
└── *.xml # 模板副本
```
## 项目结构
```
UserStringProperty/
├── CLAUDE.md # agent 指示
├── PLAN.md # 待办事项与开发计划
├── UserStringProperty.csproj
├── UserStringProperty.cs # 插件主类
├── Core/
│ ├── XmlPropertyConfig.cs # XML 解析器
│ └── TemplateManager.cs # 模板管理与持久化
├── Commands/
│ └── CmdUserstringProperty.cs # 命令入口
├── UI/
│ ├── MainDialog.cs # 主窗口
│ └── XmlImportDialog.cs # 导入模式选择对话框
└── Resources/
└── EmbeddedTemplates.cs # 内嵌默认 XML 模板
```
## 编译环境
- **.NET SDK 8.0**
- **Rhino 8**(包含 RhinoCommon.dll SDK 引用)
- Windows 10/11
## 运行时要求
- Rhino 8
## License
MIT

View File

@@ -0,0 +1,43 @@
using Rhino;
using Rhino.Commands;
using Rhino.DocObjects;
using Rhino.Input;
namespace UserStringProperty.Commands
{
public class CmdUserstringProperty : Command
{
public CmdUserstringProperty()
{
Instance = this;
}
public static CmdUserstringProperty Instance { get; private set; }
public override string EnglishName => "UserstringProperty";
protected override Result RunCommand(RhinoDoc doc, RunMode mode)
{
var result = RhinoGet.GetOneObject(
"选择 Block 实例",
false,
ObjectType.InstanceReference,
out var objref);
if (result != Result.Success)
return result;
var instanceObject = objref.Object() as InstanceObject;
if (instanceObject == null)
{
RhinoApp.WriteLine("所选对象不是 Block 实例");
return Result.Failure;
}
var dialog = new UI.MainDialog(instanceObject);
dialog.ShowDialog();
return Result.Success;
}
}
}

View File

@@ -0,0 +1,330 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Linq;
using Rhino;
using UserStringProperty.Core;
using UserStringProperty.Resources;
using UserStringProperty.UI;
namespace UserStringProperty
{
public class TemplateManager
{
private static TemplateManager _instance;
// 当前加载的所有模板(合并后)
private List<XmlPropertyConfig> _configs = new List<XmlPropertyConfig>();
private List<string> _loadedPaths = new List<string>();
// 插件目录(存储模板副本)
private string _pluginFolder;
public static TemplateManager Instance
{
get
{
if (_instance == null)
_instance = new TemplateManager();
return _instance;
}
}
public IReadOnlyList<string> LoadedPaths => _loadedPaths;
public bool IsLoaded => _configs.Count > 0 && _configs.Any(c => c.PropertySets.Count > 0);
public TemplateManager()
{
_pluginFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"McNeel", "Rhinoceros", "8.0", "Plug-ins",
"UserStringProperty (C286E052-06F6-475F-A882-1C1AA31744CF)"
);
if (!Directory.Exists(_pluginFolder))
Directory.CreateDirectory(_pluginFolder);
}
// 获取插件目录下的模板副本文件列表
public List<string> GetLocalTemplateFiles()
{
return Directory.GetFiles(_pluginFolder, "*.xml")
.OrderBy(f => File.GetCreationTime(f))
.ToList();
}
// 用户选择 XML 文件后的预处理——返回导入对话框让用户选择模式
public void PrepareImport(string filePath, List<PropertySet> existingSets, out XmlPropertyConfig incomingConfig)
{
incomingConfig = XmlPropertyConfig.Load(filePath);
}
// 执行导入
public void Import(string filePath, string cmigName, string propertysetName)
{
RebuildIndexIfMissing();
var config = XmlPropertyConfig.Load(filePath);
// 检查文件是否已存在
var fileName = config.GetStorageFileName(config.PropertySets.FirstOrDefault(p => p.Name == propertysetName) ?? config.PropertySets.First());
var destPath = Path.Combine(_pluginFolder, fileName);
if (File.Exists(destPath))
{
// 文件已存在,询问是否替换(对话框已在外面处理,这里直接覆盖)
}
// 提取指定的 propertset 并保存
var ps = config.PropertySets.FirstOrDefault(p => p.Name == propertysetName);
if (ps == null) return;
// 保存文件
var xmlContent = ExtractPropertySetXml(filePath, propertysetName);
if (!string.IsNullOrEmpty(xmlContent))
{
File.WriteAllText(destPath, xmlContent);
_loadedPaths.Add(destPath);
// 将新导入的 config 加入内存列表
var newConfig = XmlPropertyConfig.Load(destPath);
if (newConfig.PropertySets.Count > 0)
{
_configs.Add(newConfig);
}
}
SaveIndex();
}
public void LoadFromFile(string filePath)
{
if (!File.Exists(filePath))
throw new FileNotFoundException($"XML 文件不存在: {filePath}");
if (_loadedPaths.Contains(filePath))
return;
var config = XmlPropertyConfig.Load(filePath);
_configs.Add(config);
_loadedPaths.Add(filePath);
SaveIndex();
}
public void LoadDefault()
{
_configs.Clear();
_loadedPaths.Clear();
// 确保插件目录存在
if (!Directory.Exists(_pluginFolder))
Directory.CreateDirectory(_pluginFolder);
// 如果 index 不存在,先提取默认模板
var indexPath = GetIndexFilePath();
if (!File.Exists(indexPath))
{
EmbeddedTemplates.ExtractToFolder(_pluginFolder);
}
// 加载 index 中记录的路径(这些路径指向插件目录副本)
if (File.Exists(indexPath))
{
var lines = File.ReadAllLines(indexPath);
foreach (var line in lines)
{
var trimmed = line.Trim();
if (!string.IsNullOrEmpty(trimmed) && File.Exists(trimmed))
{
try
{
var config = XmlPropertyConfig.Load(trimmed);
if (config.PropertySets.Count > 0)
{
_configs.Add(config);
_loadedPaths.Add(trimmed);
}
}
catch { }
}
}
}
// 扫描插件目录下所有 XML检查是否有未加载的文件防止有模板被复制进去但不在 index
var existingFiles = Directory.GetFiles(_pluginFolder, "*.xml");
foreach (var file in existingFiles)
{
if (_loadedPaths.Contains(file))
continue;
try
{
var config = XmlPropertyConfig.Load(file);
if (config.PropertySets.Count > 0)
{
_configs.Add(config);
_loadedPaths.Add(file);
}
}
catch { }
}
// 如果没有任何模板,重新提取并重试
if (_configs.Count == 0)
{
EmbeddedTemplates.ExtractToFolder(_pluginFolder);
foreach (var file in Directory.GetFiles(_pluginFolder, "*.xml"))
{
try
{
var config = XmlPropertyConfig.Load(file);
if (config.PropertySets.Count > 0)
{
_configs.Add(config);
_loadedPaths.Add(file);
}
}
catch { }
}
}
// 保存 index确保 index 与实际加载状态一致)
SaveIndex();
}
private string GetIndexFilePath()
{
return Path.Combine(_pluginFolder, "template_index.txt");
}
private void SaveIndex()
{
File.WriteAllLines(GetIndexFilePath(), _loadedPaths);
}
private void RebuildIndexIfMissing()
{
var indexPath = GetIndexFilePath();
if (!File.Exists(indexPath))
{
var files = GetLocalTemplateFiles();
_loadedPaths.Clear();
foreach (var file in files)
{
try
{
var config = XmlPropertyConfig.Load(file);
if (config.PropertySets.Count > 0)
{
_configs.Add(config);
_loadedPaths.Add(file);
}
}
catch { }
}
SaveIndex();
}
}
private void SaveTemplatesToPluginFolder()
{
// 按 [cmig name]-[propertyset name].xml 格式保存
foreach (var config in _configs)
{
foreach (var ps in config.PropertySets)
{
var fileName = config.GetStorageFileName(ps);
var destPath = Path.Combine(_pluginFolder, fileName);
// 如果 config 有原始内容,使用原始内容;否则从 config 重新生成 XML
if (!string.IsNullOrEmpty(config.SourcePath) && File.Exists(config.SourcePath))
{
// 从原始文件提取对应的 propertset 部分来保存
var xmlContent = ExtractPropertySetXml(config.SourcePath, ps.Name);
if (!string.IsNullOrEmpty(xmlContent))
File.WriteAllText(destPath, xmlContent);
}
}
}
}
// 从原始 XML 中提取指定 propertset 的完整 XML
private string ExtractPropertySetXml(string sourcePath, string propertysetName)
{
try
{
var doc = XDocument.Load(sourcePath);
var cmig = doc.Root;
if (cmig == null) return null;
var psElem = cmig.Elements("propertyset")
.FirstOrDefault(p => p.Attribute("name")?.Value == propertysetName);
if (psElem == null) return null;
// 重新构建只包含这个 propertset 的 XML
var newDoc = new XDocument(new XDeclaration("1.0", "utf-8", null));
var newCmig = new XElement(cmig.Name, cmig.Attributes());
newCmig.Add(new XElement(psElem));
newDoc.Add(newCmig);
return newDoc.ToString();
}
catch
{
return null;
}
}
public void Clear()
{
_configs.Clear();
_loadedPaths.Clear();
var indexPath = GetIndexFilePath();
if (File.Exists(indexPath))
File.Delete(indexPath);
}
public List<PropertySet> GetAllPropertySets()
{
var result = new List<PropertySet>();
foreach (var config in _configs)
{
foreach (var ps in config.PropertySets)
{
result.Add(ps);
}
}
return result;
}
public List<string> GetCmigNames()
{
return _configs.Select(c => c.CmigName).Distinct().ToList();
}
public List<string> GetPropertySetNames(string cmigName)
{
var config = _configs.FirstOrDefault(c => c.CmigName == cmigName);
if (config == null) return new List<string>();
return config.PropertySets.Select(ps => ps.Name).ToList();
}
public PropertySet GetPropertySet(string cmigName, string psName)
{
var config = _configs.FirstOrDefault(c => c.CmigName == cmigName);
if (config == null) return null;
return config.PropertySets.FirstOrDefault(ps => ps.Name == psName);
}
public List<string> GetPropertySetNames()
{
return GetAllPropertySets().Select(ps => ps.Name).ToList();
}
public string GetPluginFolderPath()
{
return _pluginFolder;
}
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace UserStringProperty.Core
{
public class PropertyDefinition
{
public string Name { get; set; }
public string Type { get; set; } // "string" or "list"
public string DefaultValue { get; set; }
public List<string> Options { get; set; } = new List<string>();
public bool IsSeparator => DefaultValue != null && DefaultValue.Length >= 10 && DefaultValue.All(c => c == '-') && Type == "string";
public PropertyDefinition Clone()
{
return new PropertyDefinition
{
Name = this.Name,
Type = this.Type,
DefaultValue = this.DefaultValue,
Options = new List<string>(this.Options)
};
}
}
public class PropertySet
{
public string Name { get; set; }
public string Picture { get; set; }
public bool Exclusive { get; set; }
public List<PropertyDefinition> Properties { get; set; } = new List<PropertyDefinition>();
public PropertySet Clone()
{
var clone = new PropertySet
{
Name = this.Name,
Picture = this.Picture,
Exclusive = this.Exclusive,
Properties = new List<PropertyDefinition>()
};
foreach (var p in this.Properties)
clone.Properties.Add(p.Clone());
return clone;
}
}
public class XmlPropertyConfig
{
public string CmigName { get; set; } // cmig 的 name即容器名称
public string Name { get; set; }
public string SourcePath { get; set; }
public List<PropertySet> PropertySets { get; set; } = new List<PropertySet>();
// 获取存储文件名:[cmig name]-[propertyset name].xml
public string GetStorageFileName(PropertySet ps)
{
return $"{CmigName}-{ps.Name}.xml";
}
public static XmlPropertyConfig Load(string filePath)
{
var config = new XmlPropertyConfig();
if (!System.IO.File.Exists(filePath))
return config;
var doc = XDocument.Load(filePath);
var cmig = doc.Root;
if (cmig == null) return config;
config.CmigName = cmig.Attribute("name")?.Value ?? "未命名";
config.Name = config.CmigName;
config.SourcePath = filePath;
foreach (var psElem in cmig.Elements("propertyset"))
{
var ps = new PropertySet
{
Name = psElem.Attribute("name")?.Value ?? "",
Picture = psElem.Attribute("picture")?.Value,
Exclusive = psElem.Attribute("exclusive")?.Value == "true"
};
foreach (var propElem in psElem.Elements("property"))
{
var prop = new PropertyDefinition
{
Name = propElem.Attribute("name")?.Value ?? "",
Type = propElem.Attribute("type")?.Value ?? "string",
DefaultValue = propElem.Attribute("default")?.Value
};
foreach (var opt in propElem.Elements("option"))
{
prop.Options.Add(opt.Value);
}
ps.Properties.Add(prop);
}
config.PropertySets.Add(ps);
}
return config;
}
public XmlPropertyConfig Clone()
{
var clone = new XmlPropertyConfig
{
CmigName = this.CmigName,
Name = this.Name,
SourcePath = this.SourcePath,
PropertySets = new List<PropertySet>()
};
foreach (var ps in this.PropertySets)
clone.PropertySets.Add(ps.Clone());
return clone;
}
}
}

View File

@@ -0,0 +1,224 @@
using System.IO;
namespace UserStringProperty.Resources
{
public static class EmbeddedTemplates
{
public const string BaseXml = @"<?xml version=""1.0"" encoding=""utf-8""?>
<cmig name=""构件属性"">
<propertyset name=""构件属性-基本"" exclusive=""false"" picture=""基础-任意实体.png"">
<property name=""0-00-00-构件编码属性"" type=""string"" default=""--------------------"" />
<property name=""0-00-01-星球"" type=""list"" default=""E"">
<option>E</option><option>M</option><option>V</option><option>J</option><option>S</option><option>U</option><option>N</option><option>L</option>
</property>
<property name=""0-00-02-年代"" type=""string"" default=""PRC""/>
<property name=""0-00-03-国家"" type=""string"" default=""156"" />
<property name=""0-00-04-地区"" type=""string"" default=""130723"" />
<property name=""0-00-05-项目楼栋"" type=""list"" default=""数据库自动生成后导入"" />
<property name=""0-00-06-构件类型"" type=""list"" default=""DS-JG"">
<option>DS-JG</option><option>DS-WH</option><option>DS-SB</option><option>DS-NZ</option><option>DS-HJ</option>
<option>DX-FH</option><option>DX-JG</option><option>DX-WH</option><option>DX-SB</option><option>DX-NZ</option>
</property>
<property name=""0-00-07-楼层号"" type=""string"" default=""1""/>
<property name=""0-00-08-标高信息"" type=""string"" default=""0.000""/>
<property name=""0-00-09-轴号"" type=""string"" default=""1/A""/>
<property name=""0-00-10-位置"" type=""string"" default=""H0V0""/>
<property name=""0-00-11-坐标"" type=""string"" default=""0,0,0""/>
<property name=""0-01-00-构件基本属性"" type=""string"" default=""--------------------"" />
<property name=""0-01-01-构件id"" type=""string"" default=""数据库自动生成后导入""/>
<property name=""0-01-02-构件名称"" type=""string"" default=""标准拱""/>
<property name=""0-01-03-所属项目名称"" type=""string"" default=""SFC""/>
<property name=""0-01-04-所属楼栋名称"" type=""string"" default=""SolarArk5.0""/>
<property name=""0-01-05-孪生属性"" type=""list"" default=""模拟"">
<option>模拟</option><option>实际</option>
</property>
<property name=""0-01-06-总体积"" type=""string"" default=""0.00""/>
<property name=""0-01-07-总体积单位"" type=""string"" default=""m³""/>
<property name=""0-01-08-材料(用,分隔)"" type=""string"" default=""钢,木""/>
<property name=""0-01-09-各材料体积"" type=""string"" default=""0,0""/>
<property name=""0-01-10-密度"" type=""string"" default=""0.00""/>
<property name=""0-01-11-密度单位"" type=""string"" default=""kg/m³""/>
<property name=""0-01-12-重量"" type=""string"" default=""0.00""/>
<property name=""0-01-13-重量单位"" type=""string"" default=""kg""/>
<property name=""0-01-14-表面积"" type=""string"" default=""0.00""/>
<property name=""0-01-15-结构层体积"" type=""string"" default=""0.00""/>
<property name=""0-02-00-性能属性"" type=""string"" default=""--------------------"" />
<property name=""0-02-01-热阻"" type=""string"" default=""0.00""/>
<property name=""0-02-02-热阻单位"" type=""string"" default=""m²·K/W""/>
<property name=""0-02-03-热容"" type=""string"" default=""0.00""/>
<property name=""0-02-04-热容单位"" type=""string"" default=""J/(kg·K)""/>
<property name=""1-00-00-材料制备阶段构件属性"" type=""string"" default=""--------------------"" />
<property name=""1-01-01-所属材料制备工序"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""1-01-02-材料制备序号"" type=""string"" default=""1"" />
<property name=""1-01-03-材料制备类型"" type=""string"" default=""钢材加工"" />
<property name=""1-01-04-材料制备用时(天)"" type=""string"" default=""15"" />
<property name=""1-01-05-材料制备开始日期"" type=""string"" default=""20260515"" />
<property name=""1-01-06-材料制备结束日期"" type=""string"" default=""20260530"" />
<property name=""2-00-00-生产制造阶段构件属性"" type=""string"" default=""--------------------"" />
<property name=""2-01-01-所属生产工序"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""2-01-02-生产序号"" type=""string"" default=""1"" />
<property name=""2-01-03-并行工序组"" type=""string"" default=""1"" />
<property name=""2-01-04-生产类型"" type=""string"" default=""焊接"" />
<property name=""2-01-05-生产用时(天)"" type=""string"" default=""30"" />
<property name=""2-01-06-生产开始日期"" type=""string"" default=""20260601"" />
<property name=""2-01-07-生产结束日期"" type=""string"" default=""20260701"" />
<property name=""3-00-00-物流运输阶段构件属性"" type=""string"" default=""--------------------""/>
<property name=""3-01-01-所属运输工序"" type=""string"" default=""数据库自动生成后导入""/>
<property name=""3-01-02-所属运输装备"" type=""string"" default=""苏A00000""/>
<property name=""3-01-03-运输方式"" type=""list"" default=""轻型汽油货车运输"">
<option>轻型汽油货车运输</option><option>中型汽油货车运输</option><option>重型汽油货车运输</option>
</property>
<property name=""3-01-04-运输用时(天)"" type=""string"" default=""3"" />
<property name=""3-01-05-运输开始日期"" type=""string"" default=""20260701"" />
<property name=""3-01-06-运输结束日期"" type=""string"" default=""20260705"" />
<property name=""4-00-00-装配施工阶段构件属性"" type=""string"" default=""--------------------""/>
<property name=""4-01-01-所属装配工序"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""4-01-02-装配序号"" type=""string"" default=""1"" />
<property name=""4-01-03-并行工序组"" type=""string"" default=""1"" />
<property name=""4-01-04-装配类型"" type=""string"" default=""吊装"" />
<property name=""4-01-05-装配用时"" type=""string"" default=""0:00"" />
<property name=""4-01-06-装配日期"" type=""string"" default=""20250710"" />
<property name=""5-00-00-运营维护阶段构件属性"" type=""string"" default=""--------------------"" />
<property name=""5-01-01-所属维护工序"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""6-00-00-改造再利用阶段构件属性"" type=""string"" default=""--------------------"" />
<property name=""6-01-01-所属改造工序"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""7-00-00-拆除再利用阶段构件属性"" type=""string"" default=""--------------------"" />
<property name=""7-01-01-所属拆除工序"" type=""string"" default=""数据库自动生成后导入"" />
</propertyset>
</cmig>";
public const string CarbonXml = @"<?xml version=""1.0"" encoding=""utf-8""?>
<cmig name=""构件碳排放属性"">
<propertyset name=""构件碳排放属性"" exclusive=""false"" picture=""碳排放.png"">
<property name=""1-02-00-材料制备碳排放属性"" type=""string"" default=""--------------------"" />
<property name=""1-02-01-构件材料制备碳排放因子"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""1-02-02-构件材料制备碳排放值"" type=""string"" default=""0.00"" />
<property name=""1-02-03-构件材料制备碳排放值单位"" type=""string"" default=""kgCO2e"" />
<property name=""2-02-00-生产制造碳排放属性"" type=""string"" default=""--------------------"" />
<property name=""2-02-01-构件生产制造碳排放因子"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""2-02-02-构件生产制造碳排放值"" type=""string"" default=""0.00"" />
<property name=""2-02-03-构件生产制造碳排放值单位"" type=""string"" default=""kgCO2e"" />
<property name=""2-02-04-生产制造产生废料比例"" type=""string"" default=""10%"" />
<property name=""2-02-05-生产制造废料碳排放因子"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""2-02-06-生产制造废料碳排放值"" type=""string"" default=""0.00"" />
<property name=""3-02-00-物流运输碳排放属性"" type=""string"" default=""--------------------"" />
<property name=""3-02-01-构件物流运输碳排放因子"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""3-02-02-构件物流运输碳排放值"" type=""string"" default=""0.00""/>
<property name=""3-02-03-构件物流运输碳排放值单位"" type=""string"" default=""kgCO2e"" />
<property name=""3-02-04-构件物流运输距离(km)"" type=""string"" default=""500""/>
<property name=""4-02-00-装配施工碳排放属性"" type=""string"" default=""--------------------"" />
<property name=""4-02-01-构件装配施工碳排放因子"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""4-02-02-构件装配施工碳排放值"" type=""string"" default=""0.00"" />
<property name=""4-02-03-构件装配施工碳排放值单位"" type=""string"" default=""kgCO2e"" />
<property name=""4-02-04-构件装配施工产生废料比例"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""4-02-05-构件装配施工废料碳排放因子"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""4-02-06-构件装配施工单位工人数量"" type=""string"" default=""1"" />
</propertyset>
</cmig>";
public const string ContractXml = @"<?xml version=""1.0"" encoding=""utf-8""?>
<!--
构件合同属性 XML
插件化设计 - 可与基础属性组合使用
前缀: 1-03-xx, 2-03-xx, 3-03-xx, 4-03-xx
-->
<cmig name=""构件合同属性"">
<propertyset name=""构件合同属性"" exclusive=""false"" picture=""合同.png"">
<!-- 材料制备阶段合同属性 -->
<property name=""1-03-00-材料制备合同属性"" type=""string"" default=""--------------------"" />
<property name=""1-03-01-构件材料制备合同编码"" type=""string"" default="""" />
<property name=""1-03-02-构件材料制备合作单位"" type=""string"" default=""筑晓龙商贸"" />
<property name=""1-03-03-构件材料制备合同合作形式"" type=""list"" default=""采购"" >
<option>赞助</option><option>采购</option><option>租赁</option><option>技术研发分包</option><option>劳务</option>
</property>
<property name=""1-03-04-构件材料制备质量"" type=""list"" default=""合格"">
<option>合格</option><option>不合格</option>
</property>
<property name=""1-03-05-构件材料制备造价因子"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""1-03-06-构件材料制备造价"" type=""string"" default=""0.00"" />
<property name=""1-03-07-构件材料制备造价单位"" type=""string"" default=""元"" />
<property name=""1-03-08-构件材料制备存在高空作业"" type=""string"" default=""否"" />
<property name=""1-03-09-构件材料制备高空作业率"" type=""string"" default=""0"" />
<property name=""1-03-10-构件材料制备状态"" type=""list"" default=""未制备"">
<option>未制备</option><option>制备中</option><option>制备完成</option><option>验收完成</option>
</property>
<property name=""1-03-11-构件材料制备进度%"" type=""string"" default=""0"" />
<!-- 生产制造阶段合同属性 -->
<property name=""2-03-00-生产制造合同属性"" type=""string"" default=""--------------------"" />
<property name=""2-03-01-构件生产制造合同编码"" type=""string"" default=""0"" />
<property name=""2-03-02-构件生产制造合作单位"" type=""string"" default=""筑新技术集团"" />
<property name=""2-03-03-构件生产制造合同合作形式"" type=""list"" default=""技术研发分包"" >
<option>赞助</option><option>采购</option><option>租赁</option><option>技术研发分包</option><option>劳务</option>
</property>
<property name=""2-03-04-构件生产制造质量"" type=""list"" default=""合格"">
<option>合格</option><option>不合格</option>
</property>
<property name=""2-03-05-构件生产制造造价因子"" type=""string"" default=""数据库自动生成后导入"" />
<property name=""2-03-06-构件生产制造造价"" type=""string"" default=""0.00"" />
<property name=""2-03-07-构件生产制造造价单位"" type=""string"" default=""元"" />
<property name=""2-03-08-构件生产制造存在高空作业"" type=""string"" default=""是"" />
<property name=""2-03-09-构件生产制造高空作业率"" type=""string"" default=""0"" />
<property name=""2-03-10-构件生产制造状态"" type=""list"" default=""未生产"">
<option>未生产</option><option>生产中</option><option>生产完成</option><option>验收完成</option>
</property>
<property name=""2-03-11-构件生产制造进度%"" type=""string"" default=""0"" />
<!-- 物流运输阶段合同属性 -->
<property name=""3-03-00-物流运输合同属性"" type=""string"" default=""--------------------""/>
<property name=""3-03-01-构件物流运输合同编码"" type=""string"" default=""0""/>
<property name=""3-03-02-构件物流运输合作单位"" type=""string"" default=""南京瑞新供应链""/>
<property name=""3-03-03-构件物流运输合同合作形式"" type=""list"" default=""采购"" >
<option>赞助</option><option>采购</option><option>租赁</option><option>技术研发分包</option><option>劳务</option>
</property>
<property name=""3-03-04-构件物流运输质量"" type=""list"" default=""合格"">
<option>合格</option><option>不合格</option>
</property>
<property name=""3-03-05-构件物流运输造价因子"" type=""string"" default=""数据库自动生成后导入""/>
<property name=""3-03-06-构件物流运输造价"" type=""string"" default=""0.00""/>
<property name=""3-03-07-构件物流运输造价单位"" type=""string"" default=""元""/>
<property name=""3-03-08-构件物流运输存在高空作业"" type=""string"" default=""否"" />
<property name=""3-03-09-构件物流运输高空作业率"" type=""string"" default=""0"" />
<property name=""3-03-10-构件物流运输状态"" type=""list"" default=""未运输"">
<option>未运输</option><option>装车中</option><option>运输中</option><option>运输完成</option><option>已堆场</option>
</property>
<property name=""3-03-11-构件物流运输进度%"" type=""string"" default=""0"" />
<!-- 装配施工阶段合同属性 -->
<property name=""4-03-00-装配施工合同属性"" type=""string"" default=""--------------------""/>
<property name=""4-03-01-构件装配施工合同编码"" type=""string"" default=""0""/>
<property name=""4-03-02-构件装配施工合作单位"" type=""string"" default=""内蒙古大元建筑""/>
<property name=""4-03-03-构件装配施工合同合作形式"" type=""list"" default=""技术研发分包"" >
<option>赞助</option><option>采购</option><option>租赁</option><option>技术研发分包</option><option>劳务</option>
</property>
<property name=""4-03-04-构件装配施工质量"" type=""list"" default=""合格"">
<option>合格</option><option>不合格</option>
</property>
<property name=""4-03-05-构件装配施工造价因子"" type=""string"" default=""数据库自动生成后导入""/>
<property name=""4-03-06-构件装配施工造价"" type=""string"" default=""0.00""/>
<property name=""4-03-07-构件装配施工造价单位"" type=""string"" default=""元""/>
<property name=""4-03-08-构件装配施工存在高空作业"" type=""string"" default=""是"" />
<property name=""4-03-09-构件装配施工高空作业率"" type=""string"" default=""0"" />
<property name=""4-03-10-构件装配施工状态"" type=""list"" default=""未装配施工"">
<option>未装配施工</option><option>装配施工中</option><option>装配施工完成</option><option>验收完成</option>
</property>
<property name=""4-03-11-构件装配施工进度%"" type=""string"" default=""0"" />
</propertyset>
</cmig>";
public static void ExtractToFolder(string folder)
{
if (!Directory.Exists(folder)) Directory.CreateDirectory(folder);
File.WriteAllText(Path.Combine(folder, "构件属性-构件属性-基本.xml"), BaseXml);
File.WriteAllText(Path.Combine(folder, "构件碳排放属性-构件碳排放属性.xml"), CarbonXml);
File.WriteAllText(Path.Combine(folder, "构件合同属性-构件合同属性.xml"), ContractXml);
}
}
}

View File

@@ -0,0 +1,504 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using Rhino.DocObjects;
using UserStringProperty.Core;
namespace UserStringProperty.UI
{
public enum ApplyMode
{
Replace, // 替代:清空现有属性后应用新模板
Append // 附加:合并,重复 key 时询问
}
public enum ConflictResolution
{
ReplaceDuplicates, // 替代重复的 key
SkipDuplicates // 跳过重复的 key
}
public partial class MainDialog : Form
{
private InstanceObject _instanceObject;
private ComboBox _cmbCmig;
private ComboBox _cmbPropertySet;
private Panel _panelFields;
private Dictionary<string, Control> _fieldControls = new Dictionary<string, Control>();
private Button _btnSave;
private Button _btnCancel;
private Button _btnLoadXml;
private Label _lblTemplateInfo;
public MainDialog(InstanceObject instanceObject)
{
_instanceObject = instanceObject;
InitializeComponent();
}
private void InitializeComponent()
{
this.Text = "属性编辑";
this.Size = new Size(550, 580);
this.StartPosition = FormStartPosition.CenterParent;
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
// Cmig 选择
var lblCmig = new Label
{
Text = "模板类型:",
Location = new Point(10, 10),
Size = new Size(80, 20)
};
_cmbCmig = new ComboBox
{
Location = new Point(95, 8),
Size = new Size(180, 20),
DropDownStyle = ComboBoxStyle.DropDownList
};
_cmbCmig.SelectedIndexChanged += CmbCmig_SelectedIndexChanged;
this.Controls.Add(lblCmig);
this.Controls.Add(_cmbCmig);
// PropertySet 选择
var lblPropertySet = new Label
{
Text = "属性集:",
Location = new Point(285, 10),
Size = new Size(80, 20)
};
_cmbPropertySet = new ComboBox
{
Location = new Point(370, 8),
Size = new Size(160, 20),
DropDownStyle = ComboBoxStyle.DropDownList
};
_cmbPropertySet.SelectedIndexChanged += CmbPropertySet_SelectedIndexChanged;
this.Controls.Add(lblPropertySet);
this.Controls.Add(_cmbPropertySet);
_btnLoadXml = new Button
{
Text = "加载XML",
Location = new Point(10, 38),
Size = new Size(90, 25)
};
_btnLoadXml.Click += BtnLoadXml_Click;
_lblTemplateInfo = new Label
{
Text = "",
Location = new Point(200, 41),
Size = new Size(330, 15),
ForeColor = Color.Gray,
Font = new System.Drawing.Font(Font.FontFamily, 8)
};
var lblFields = new Label
{
Text = "字段:",
Location = new Point(10, 75),
Size = new Size(60, 20)
};
_panelFields = new Panel
{
Location = new Point(10, 95),
Size = new Size(510, 370),
AutoScroll = true,
BorderStyle = BorderStyle.FixedSingle
};
_btnSave = new Button { Text = "写入属性", Location = new Point(370, 480), Size = new Size(90, 30) };
_btnSave.Click += BtnSave_Click;
_btnCancel = new Button { Text = "取消", Location = new Point(270, 480), Size = new Size(90, 30) };
_btnCancel.Click += (s, e) => this.Close();
this.Controls.AddRange(new Control[] {
_btnLoadXml, _lblTemplateInfo,
lblCmig, _cmbCmig, lblPropertySet, _cmbPropertySet,
lblFields, _panelFields, _btnSave, _btnCancel
});
RefreshCmigCombo();
UpdateTemplateInfo();
}
private void UpdateTemplateInfo()
{
var tm = TemplateManager.Instance;
if (!tm.IsLoaded)
{
_lblTemplateInfo.Text = "未加载模板";
}
else
{
var paths = tm.LoadedPaths;
if (paths.Count == 1)
_lblTemplateInfo.Text = $"已加载: {System.IO.Path.GetFileName(paths[0])}";
else
_lblTemplateInfo.Text = $"已加载 {paths.Count} 个模板文件";
}
}
private void RefreshCmigCombo()
{
_cmbCmig.Items.Clear();
_cmbPropertySet.Items.Clear();
var cmigNames = TemplateManager.Instance.GetCmigNames();
if (cmigNames.Count == 0)
{
_cmbCmig.Items.Add("请先加载XML模板");
_cmbCmig.SelectedIndex = 0;
_cmbCmig.Enabled = false;
_cmbPropertySet.Enabled = false;
}
else
{
_cmbCmig.Items.AddRange(cmigNames.ToArray());
_cmbCmig.Enabled = true;
if (_cmbCmig.Items.Count > 0)
_cmbCmig.SelectedIndex = 0;
}
}
private void RefreshPropertySetCombo(string cmigName)
{
_cmbPropertySet.Items.Clear();
var psNames = TemplateManager.Instance.GetPropertySetNames(cmigName);
if (psNames.Count == 0)
{
_cmbPropertySet.Items.Add("(无属性集)");
_cmbPropertySet.SelectedIndex = 0;
_cmbPropertySet.Enabled = false;
}
else
{
_cmbPropertySet.Items.AddRange(psNames.ToArray());
_cmbPropertySet.Enabled = true;
if (_cmbPropertySet.Items.Count > 0)
_cmbPropertySet.SelectedIndex = 0;
}
}
private void CmbCmig_SelectedIndexChanged(object sender, EventArgs e)
{
var cmigName = _cmbCmig.SelectedItem?.ToString();
if (!string.IsNullOrEmpty(cmigName))
{
RefreshPropertySetCombo(cmigName);
}
}
private void CmbPropertySet_SelectedIndexChanged(object sender, EventArgs e)
{
var cmigName = _cmbCmig.SelectedItem?.ToString();
var psName = _cmbPropertySet.SelectedItem?.ToString();
if (string.IsNullOrEmpty(cmigName) || string.IsNullOrEmpty(psName))
return;
// 直接加载字段显示,让用户先看到/编辑现有内容
// 替代/附加逻辑在保存时才处理
BuildFieldsForPropertySet(cmigName, psName);
}
private ApplyMode AskApplyMode()
{
var result = MessageBox.Show(
"Block 已有属性。\n\n选择「是」替代现有属性清空后应用新模板\n选择「否」附加到现有属性合并",
"应用模式",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Question);
if (result == DialogResult.Yes)
return ApplyMode.Replace;
else if (result == DialogResult.No)
return ApplyMode.Append;
else
return ApplyMode.Replace; // 默认替代
}
private ConflictResolution AskConflictResolution(List<string> duplicateKeys)
{
var suffix = duplicateKeys.Count > 5 ? "..." : "";
var result = MessageBox.Show(
$"发现 {duplicateKeys.Count} 个重复的属性 key\n{string.Join(", ", duplicateKeys.Take(5))}{suffix}\n\n选择「是」替代这些 key 的值\n选择「否」跳过这些 key",
"属性冲突",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Warning);
if (result == DialogResult.Yes)
return ConflictResolution.ReplaceDuplicates;
else
return ConflictResolution.SkipDuplicates;
}
private string GetExistingPropertyValue(string key)
{
var userStrings = _instanceObject.Attributes?.GetUserStrings();
if (userStrings != null && userStrings[key] != null)
{
return userStrings[key];
}
return null;
}
private void SetFieldValue(string key, string value)
{
if (_fieldControls.ContainsKey(key))
{
var ctrl = _fieldControls[key];
if (ctrl is ComboBox cmb)
{
if (cmb.Items.Contains(value))
cmb.SelectedItem = value;
}
else if (ctrl is TextBox txt)
{
txt.Text = value;
}
}
}
private List<string> GetExistingPropertyKeys()
{
var keys = new List<string>();
var userStrings = _instanceObject.Attributes?.GetUserStrings();
if (userStrings != null)
{
keys.AddRange(userStrings.AllKeys);
}
return keys;
}
private void BtnLoadXml_Click(object sender, EventArgs e)
{
using (var dlg = new OpenFileDialog())
{
dlg.Filter = "XML文件|*.xml";
dlg.Title = "选择属性配置文件";
if (dlg.ShowDialog() == DialogResult.OK)
{
var filePath = dlg.FileName;
XmlPropertyConfig incomingConfig;
try
{
incomingConfig = XmlPropertyConfig.Load(filePath);
}
catch (Exception ex)
{
MessageBox.Show($"XML 解析失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
return;
}
if (incomingConfig.PropertySets.Count == 0)
{
MessageBox.Show("XML 中没有找到属性集", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
var pluginFolder = TemplateManager.Instance.GetPluginFolderPath();
using (var importDlg = new XmlImportDialog(incomingConfig, pluginFolder))
{
if (importDlg.ShowDialog() == DialogResult.OK)
{
TemplateManager.Instance.Import(
filePath,
importDlg.SelectedCmigName,
importDlg.SelectedPropertySetName
);
RefreshCmigCombo();
UpdateTemplateInfo();
}
}
}
}
}
private void BuildFieldsForPropertySet(string cmigName, string psName)
{
_panelFields.Controls.Clear();
_fieldControls.Clear();
var ps = TemplateManager.Instance.GetPropertySet(cmigName, psName);
if (ps == null) return;
int y = 10;
foreach (var prop in ps.Properties)
{
if (prop.IsSeparator)
{
var sep = new Label
{
Text = prop.Name,
Location = new Point(10, y),
Size = new Size(480, 20),
Font = new System.Drawing.Font(Font.FontFamily, 9, FontStyle.Bold),
ForeColor = Color.DarkBlue,
BackColor = Color.AliceBlue
};
_panelFields.Controls.Add(sep);
y += 25;
continue;
}
// 只使用模板的默认值,不读取 block 现有属性
AddFieldControl(prop, y, prop.DefaultValue ?? "");
y += 28;
}
}
private void AddFieldControl(PropertyDefinition prop, int y, string value)
{
var lbl = new Label
{
Text = prop.Name,
Location = new Point(10, y),
Size = new Size(200, 20)
};
_panelFields.Controls.Add(lbl);
Control ctrl;
if (prop.Type == "list" && prop.Options.Count > 0)
{
var cmb = new ComboBox
{
Location = new Point(220, y),
Size = new Size(260, 20),
DropDownStyle = ComboBoxStyle.DropDownList
};
cmb.Items.AddRange(prop.Options.ToArray());
cmb.SelectedItem = prop.Options.Contains(value) ? value : prop.DefaultValue;
ctrl = cmb;
}
else
{
var txt = new TextBox
{
Location = new Point(220, y),
Size = new Size(260, 20),
Text = value
};
ctrl = txt;
}
_panelFields.Controls.Add(ctrl);
_fieldControls[prop.Name] = ctrl;
}
private void BtnSave_Click(object sender, EventArgs e)
{
var existingKeys = GetExistingPropertyKeys();
if (existingKeys.Count > 0)
{
// block 已有属性,询问 Replace 或 Append
var result = MessageBox.Show(
"Block 已有属性。\n\n选择「是」替代现有属性清空后应用新模板\n选择「否」附加到现有属性合并",
"应用模式",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Question);
if (result == DialogResult.Cancel)
return;
if (result == DialogResult.Yes)
{
// 替代模式:清空所有现有属性
ClearAllProperties();
}
else
{
// 附加模式:检查重复 key
var cmigName = _cmbCmig.SelectedItem?.ToString();
var psName = _cmbPropertySet.SelectedItem?.ToString();
if (!string.IsNullOrEmpty(cmigName) && !string.IsNullOrEmpty(psName))
{
var ps = TemplateManager.Instance.GetPropertySet(cmigName, psName);
if (ps != null)
{
var duplicateKeys = ps.Properties
.Where(p => existingKeys.Contains(p.Name) && !p.IsSeparator)
.Select(p => p.Name)
.ToList();
if (duplicateKeys.Count > 0)
{
var suffix = duplicateKeys.Count > 5 ? "..." : "";
var res = MessageBox.Show(
$"发现 {duplicateKeys.Count} 个重复的属性 key\n{string.Join(", ", duplicateKeys.Take(5))}{suffix}\n\n选择「是」替代这些 key 的值\n选择「否」跳过这些 key",
"属性冲突",
MessageBoxButtons.YesNoCancel,
MessageBoxIcon.Warning);
if (res == DialogResult.Cancel)
return;
if (res == DialogResult.Yes)
{
// 替代重复的 key - 不做特殊处理,直接保存会用模板值覆盖
}
else
{
// 跳过重复的 key - 需要恢复原值后再保存
foreach (var key in duplicateKeys)
{
var existingValue = GetExistingPropertyValue(key);
if (!string.IsNullOrEmpty(existingValue))
{
SetFieldValue(key, existingValue);
}
}
}
}
}
}
}
}
// 保存字段
var attrs = _instanceObject.Attributes;
foreach (var kvp in _fieldControls)
{
string value = "";
if (kvp.Value is ComboBox cmb)
value = cmb.SelectedItem?.ToString() ?? "";
else if (kvp.Value is TextBox txt)
value = txt.Text;
attrs.SetUserString(kvp.Key, value);
}
Rhino.RhinoApp.WriteLine($"已保存 {_fieldControls.Count} 个属性到 Block");
this.Close();
}
private void ClearAllProperties()
{
var attrs = _instanceObject.Attributes;
if (attrs == null) return;
var userStrings = attrs.GetUserStrings();
if (userStrings == null) return;
// 清除所有现有属性
foreach (var key in userStrings.AllKeys)
{
attrs.SetUserString(key, null);
}
}
}
}

View File

@@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Windows.Forms;
using UserStringProperty.Core;
namespace UserStringProperty.UI
{
public enum ImportMode
{
Append, // 添加新的(如果同名已存在则提示替换)
Replace // 完全替代现有同名属性集
}
public class XmlImportDialog : Form
{
private XmlPropertyConfig _incomingConfig;
private ComboBox _cmbCmig;
private ComboBox _cmbPropertySet;
private Label _lblIncomingInfo;
private Label _lblConflictWarning;
private Button _btnOK;
private Button _btnCancel;
private string _pluginFolder;
public ImportMode SelectedMode { get; private set; }
public string SelectedCmigName { get; private set; }
public string SelectedPropertySetName { get; private set; }
public bool ShouldReplace { get; private set; }
public XmlImportDialog(XmlPropertyConfig incoming, string pluginFolder)
{
_incomingConfig = incoming;
_pluginFolder = pluginFolder;
InitializeComponent();
UpdateConflictWarning();
}
private void InitializeComponent()
{
this.Text = "导入属性集";
this.Size = new Size(550, 250);
this.StartPosition = FormStartPosition.CenterParent;
this.FormBorderStyle = FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
int y = 15;
// 选择 Cmig
var lblCmig = new Label
{
Text = "模板类型 (cmig):",
Location = new Point(15, y),
Size = new Size(120, 20)
};
this.Controls.Add(lblCmig);
_cmbCmig = new ComboBox
{
Location = new Point(140, y),
Size = new Size(380, 20),
DropDownStyle = ComboBoxStyle.DropDownList
};
_cmbCmig.Items.Add(_incomingConfig.CmigName);
_cmbCmig.SelectedIndex = 0;
this.Controls.Add(_cmbCmig);
y += 30;
// 选择 PropertySet
var lblPropertySet = new Label
{
Text = "属性集 (propertyset):",
Location = new Point(15, y),
Size = new Size(120, 20)
};
this.Controls.Add(lblPropertySet);
_cmbPropertySet = new ComboBox
{
Location = new Point(140, y),
Size = new Size(380, 20),
DropDownStyle = ComboBoxStyle.DropDownList
};
foreach (var ps in _incomingConfig.PropertySets)
{
_cmbPropertySet.Items.Add(ps.Name);
}
if (_cmbPropertySet.Items.Count > 0)
_cmbPropertySet.SelectedIndex = 0;
_cmbPropertySet.SelectedIndexChanged += (s, e) => UpdateConflictWarning();
this.Controls.Add(_cmbPropertySet);
y += 35;
// 即将导入的信息
_lblIncomingInfo = new Label
{
Text = "",
Location = new Point(15, y),
Size = new Size(500, 20),
ForeColor = Color.DarkGreen,
Font = new Font(Font.FontFamily, 9, FontStyle.Bold)
};
this.Controls.Add(_lblIncomingInfo);
y += 25;
// 冲突警告
_lblConflictWarning = new Label
{
Text = "",
Location = new Point(15, y),
Size = new Size(500, 20)
};
this.Controls.Add(_lblConflictWarning);
y += 40;
// 按钮
_btnOK = new Button
{
Text = "确定导入",
Location = new Point(350, y),
Size = new Size(90, 30)
};
_btnOK.Click += (s, e) =>
{
var cmigName = _cmbCmig.SelectedItem?.ToString() ?? "";
var psName = _cmbPropertySet.SelectedItem?.ToString() ?? "";
var fileName = $"{cmigName}-{psName}.xml";
var existingPath = Path.Combine(_pluginFolder, fileName);
if (File.Exists(existingPath))
{
var result = MessageBox.Show(
$"文件 {fileName} 已存在,是否覆盖?",
"确认覆盖",
MessageBoxButtons.YesNo,
MessageBoxIcon.Warning);
if (result != DialogResult.Yes)
return;
}
SelectedCmigName = cmigName;
SelectedPropertySetName = psName;
this.DialogResult = DialogResult.OK;
this.Close();
};
this.Controls.Add(_btnOK);
_btnCancel = new Button
{
Text = "取消",
Location = new Point(245, y),
Size = new Size(90, 30)
};
_btnCancel.Click += (s, e) =>
{
this.DialogResult = DialogResult.Cancel;
this.Close();
};
this.Controls.Add(_btnCancel);
}
private void UpdateConflictWarning()
{
var cmigName = _cmbCmig.SelectedItem?.ToString() ?? "";
var psName = _cmbPropertySet.SelectedItem?.ToString() ?? "";
if (string.IsNullOrEmpty(cmigName) || string.IsNullOrEmpty(psName))
{
_lblIncomingInfo.Text = "";
_lblConflictWarning.Text = "";
return;
}
var fileName = $"{cmigName}-{psName}.xml";
var existingPath = Path.Combine(_pluginFolder, fileName);
var exists = File.Exists(existingPath);
_lblIncomingInfo.Text = $"待导入: {cmigName}-{psName}";
if (exists)
{
_lblConflictWarning.Text = $"⚠ 同名属性集已存在,将覆盖现有文件";
_lblConflictWarning.ForeColor = Color.DarkOrange;
}
else
{
_lblConflictWarning.Text = $"✓ 同名属性集不存在,将作为新文件保存";
_lblConflictWarning.ForeColor = Color.DarkGreen;
}
}
public bool CheckReplace()
{
var fileName = $"{SelectedCmigName}-{SelectedPropertySetName}.xml";
var existingPath = Path.Combine(_pluginFolder, fileName);
return File.Exists(existingPath);
}
}
}

View File

@@ -0,0 +1,36 @@
using System;
using System.Reflection;
using System.Runtime.InteropServices;
using Rhino;
using Rhino.PlugIns;
[assembly: Guid("C286E052-06F6-475F-A882-1C1AA31744CF")]
namespace UserStringProperty
{
public class UserStringPropertyPlugin : PlugIn
{
public UserStringPropertyPlugin()
{
Instance = this;
}
public static UserStringPropertyPlugin Instance { get; private set; }
protected override LoadReturnCode OnLoad(ref string errorMessage)
{
// 首次启动时,从内嵌资源提取默认 XML 到插件目录
Resources.EmbeddedTemplates.ExtractToFolder(
TemplateManager.Instance.GetPluginFolderPath()
);
// 启动时自动加载已保存的模板
TemplateManager.Instance.LoadDefault();
return LoadReturnCode.Success;
}
protected override void OnShutdown()
{
}
}
}

View File

@@ -0,0 +1,51 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<LangVersion>latest</LangVersion>
<OutputType>Library</OutputType>
<UseWindowsForms>true</UseWindowsForms>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<OutputPath>bin\</OutputPath>
<AssemblyName>UserStringProperty</AssemblyName>
<RootNamespace>UserStringProperty</RootNamespace>
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
<StartupObject />
<Nullable>disable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<!-- Rhino 插件配置 -->
<AssemblyName>UserStringProperty</AssemblyName>
<Version>1.0</Version>
<Company>YourCompany</Company>
<Description>User String Property Plugin</Description>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
</PropertyGroup>
<ItemGroup>
<Reference Include="RhinoCommon">
<HintPath>C:\Program Files\Rhino 8\System\RhinoCommon.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="Rhino.UI">
<HintPath>C:\Program Files\Rhino 8\System\Rhino.UI.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Drawing" />
<Reference Include="System.Windows.Forms" />
<Reference Include="System.Xml" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Compile Include="UserStringProperty.cs" />
<Compile Include="Commands\CmdUserstringProperty.cs" />
<Compile Include="UI\MainDialog.cs" />
<Compile Include="UI\XmlImportDialog.cs" />
<Compile Include="Core\XmlPropertyConfig.cs" />
<Compile Include="Core\TemplateManager.cs" />
<Compile Include="Resources\EmbeddedTemplates.cs" />
</ItemGroup>
</Project>