插件开发基础

第四章:插件开发基础

4.1 LightCAD插件体系

4.1.1 插件概述

LightCAD的插件系统允许开发者扩展平台功能,插件可以:

  • 添加新的元素类型
  • 注册新的命令
  • 扩展用户界面
  • 自定义绘图和编辑行为
  • 集成外部系统

4.1.2 插件加载机制

LightCAD启动
      │
      ▼
扫描插件目录
      │
      ▼
加载程序集(DLL)
      │
      ▼
反射查找ILcPlugin实现
      │
      ▼
创建插件实例
      │
      ▼
依次调用生命周期方法

4.1.3 插件接口定义

public interface ILcPlugin
{
    /// <summary>
    /// 插件加载时调用
    /// </summary>
    void Loaded();
    
    /// <summary>
    /// UI初始化时调用
    /// </summary>
    void InitUI();
    
    /// <summary>
    /// 完成所有初始化后调用
    /// </summary>
    void Completed();
    
    /// <summary>
    /// 文档运行时初始化时调用
    /// </summary>
    void OnInitializeDocRt(DocumentRuntime docRt);
    
    /// <summary>
    /// 文档运行时销毁时调用
    /// </summary>
    void OnDisposeDocRt(DocumentRuntime docRt);
}

4.2 创建基础插件

4.2.1 最小插件示例

using LightCAD.Core;
using LightCAD.Runtime;

namespace MyFirstPlugin
{
    public class MyFirstPlugin : ILcPlugin
    {
        public void Loaded()
        {
            Console.WriteLine("MyFirstPlugin: Loaded");
        }
        
        public void InitUI()
        {
            Console.WriteLine("MyFirstPlugin: InitUI");
        }
        
        public void Completed()
        {
            Console.WriteLine("MyFirstPlugin: Completed");
        }
        
        public void OnInitializeDocRt(DocumentRuntime docRt)
        {
            Console.WriteLine($"MyFirstPlugin: Document initialized - {docRt.Document.Name}");
        }
        
        public void OnDisposeDocRt(DocumentRuntime docRt)
        {
            Console.WriteLine($"MyFirstPlugin: Document disposed - {docRt.Document.Name}");
        }
    }
}

4.2.2 项目配置

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
  
  <ItemGroup>
    <Reference Include="LightCAD.Core">
      <HintPath>..\Libs\LightCAD.Core.dll</HintPath>
    </Reference>
    <Reference Include="LightCAD.Runtime">
      <HintPath>..\Libs\LightCAD.Runtime.dll</HintPath>
    </Reference>
  </ItemGroup>
</Project>

4.3 UI集成

4.3.1 TabItem结构

LightCAD使用Ribbon风格的选项卡界面,主要结构如下:

TabItem (选项卡)
├── Name           # 内部名称
├── Text           # 显示文本
├── ShortcutKey    # 快捷键
└── ButtonGroups   # 按钮组列表
    └── TabButtonGroup
        └── Buttons  # 按钮列表
            └── TabButton
                ├── Name        # 命令名称
                ├── Text        # 显示文本
                ├── Icon        # 图标
                ├── IsCommand   # 是否为命令
                ├── Width       # 宽度
                └── DropDowns   # 下拉菜单

4.3.2 创建选项卡

public static TabItem MyTabItem = new TabItem
{
    Name = "MyTab",
    Text = "我的工具",
    ShortcutKey = "ALT-M",
    ButtonGroups = new List<TabButtonGroup>
    {
        new TabButtonGroup
        {
            Buttons = new List<TabButton>
            {
                new TabButton
                {
                    Name = "MyCommand1",
                    Text = "功能一",
                    Icon = LoadIcon("icon1.png"),
                    IsCommand = true,
                },
                new TabButton
                {
                    Name = "MyCommand2",
                    Text = "功能二",
                    Icon = LoadIcon("icon2.png"),
                    IsCommand = true,
                    Width = 80,
                }
            }
        }
    }
};

// 在Completed()中注册
public void Completed()
{
    AppRuntime.UISystem.AddInitTabItems([MyTabItem]);
}

4.3.3 带下拉菜单的按钮

new TabButton
{
    Name = "DrawTools",
    Text = "绘图工具",
    Icon = Properties.Resources.DrawIcon,
    IsCommand = true,
    DropDowns = new List<TabButton>
    {
        new TabButton
        {
            Name = "DrawLine",
            Text = "画线",
            Icon = Properties.Resources.LineIcon,
            IsCommand = true,
        },
        new TabButton
        {
            Name = "DrawCircle",
            Text = "画圆",
            Icon = Properties.Resources.CircleIcon,
            IsCommand = true,
        },
        new TabButton
        {
            Name = "DrawRect",
            Text = "画矩形",
            Icon = Properties.Resources.RectIcon,
            IsCommand = true,
        },
    }
}

4.3.4 图标资源管理

使用Resources.resx管理图标:

  1. 在项目中创建Properties/Resources.resx
  2. 添加图像资源
  3. 在代码中引用:
// 自动生成的访问器
Icon = Properties.Resources.MyIcon

或者从文件加载:

private static System.Drawing.Image LoadIcon(string path)
{
    var assembly = Assembly.GetExecutingAssembly();
    using var stream = assembly.GetManifestResourceStream($"MyPlugin.Resources.{path}");
    return System.Drawing.Image.FromStream(stream);
}

4.4 命令开发

4.4.1 命令类定义

using LightCAD.Runtime;

namespace MyPlugin
{
    [CommandClass]
    public class MyCommands
    {
        // 命令方法将在这里定义
    }
}

4.4.2 基本命令

[CommandMethod(Name = "Hello", ShortCuts = "HI")]
public CommandResult HelloWorld(IDocumentEditor docEditor, string[] args)
{
    // 获取命令控制器
    var cmdCtrl = docEditor.GetCommandController();
    
    // 输出消息
    cmdCtrl.WriteInfo("Hello, LightCAD World!");
    
    return CommandResult.Succ();
}

4.4.3 带参数的命令

[CommandMethod(Name = "SetColor", ShortCuts = "SC")]
public CommandResult SetColor(IDocumentEditor docEditor, string[] args)
{
    if (args.Length < 1)
    {
        docEditor.GetCommandController().WriteError("请指定颜色参数");
        return CommandResult.Fail("缺少参数");
    }
    
    var colorName = args[0];
    // 处理颜色设置...
    
    return CommandResult.Succ();
}

4.4.4 异步命令

[CommandMethod(Name = "LongTask", ShortCuts = "LT")]
public CommandResult LongTask(IDocumentEditor docEditor, string[] args)
{
    Task.Run(async () =>
    {
        var cmdCtrl = docEditor.GetCommandController();
        cmdCtrl.WriteInfo("开始长时间任务...");
        
        await Task.Delay(3000); // 模拟耗时操作
        
        cmdCtrl.WriteInfo("任务完成!");
    });
    
    return CommandResult.Succ();
}

4.4.5 CommandResult详解

// 成功结果
CommandResult.Succ()

// 失败结果
CommandResult.Fail("错误信息")

// 带数据的结果
new CommandResult
{
    Status = CommandStatus.Success,
    Data = someData
}

4.5 文档操作

4.5.1 获取当前文档

[CommandMethod(Name = "DocInfo")]
public CommandResult GetDocInfo(IDocumentEditor docEditor, string[] args)
{
    var docRt = docEditor.GetDocumentRuntime();
    var doc = docRt.Document;
    
    var cmdCtrl = docEditor.GetCommandController();
    cmdCtrl.WriteInfo($"文档名称: {doc.Name}");
    cmdCtrl.WriteInfo($"元素数量: {doc.Elements.Count}");
    cmdCtrl.WriteInfo($"图层数量: {doc.Layers.Count}");
    
    return CommandResult.Succ();
}

4.5.2 遍历元素

[CommandMethod(Name = "ListElements")]
public CommandResult ListElements(IDocumentEditor docEditor, string[] args)
{
    var doc = docEditor.GetDocumentRuntime().Document;
    var cmdCtrl = docEditor.GetCommandController();
    
    foreach (var element in doc.Elements)
    {
        cmdCtrl.WriteInfo($"元素: {element.Type.Name} - {element.Id}");
    }
    
    return CommandResult.Succ();
}

4.5.3 图层操作

// 获取或创建图层
private LcLayer GetOrCreateLayer(LcDocument doc, string layerName, int color)
{
    var layer = doc.Layers.FirstOrDefault(l => l.Name == layerName);
    if (layer == null)
    {
        layer = doc.CreateObject<LcLayer>();
        layer.Name = layerName;
        layer.Color = color;
        layer.SetLineType(new LcLineType("ByLayer"));
        layer.Transparency = 0;
        doc.Layers.Add(layer);
    }
    return layer;
}

// 使用示例
var layer = GetOrCreateLayer(doc, "MyLayer", 0xFF0000); // 红色图层
element.Layer = layer.Name;

4.6 用户输入

4.6.1 PointInputer - 点输入

[CommandMethod(Name = "GetPoint")]
public async CommandResult GetPoint(IDocumentEditor docEditor, string[] args)
{
    var pointInputer = new PointInputer(docEditor);
    
    // 获取单个点
    var result = await pointInputer.Execute("请指定点:");
    
    if (result.Status == InputStatus.OK)
    {
        var point = result.Point;
        docEditor.GetCommandController().WriteInfo($"点坐标: ({point.X}, {point.Y})");
    }
    else if (result.Status == InputStatus.Cancel)
    {
        docEditor.GetCommandController().WriteInfo("操作已取消");
    }
    
    return CommandResult.Succ();
}

4.6.2 获取多个点

[CommandMethod(Name = "GetPoints")]
public async CommandResult GetPoints(IDocumentEditor docEditor, string[] args)
{
    var pointInputer = new PointInputer(docEditor);
    var points = new List<Vector2>();
    var cmdCtrl = docEditor.GetCommandController();
    
    cmdCtrl.WriteInfo("指定第一个点:");
    var result = await pointInputer.Execute();
    
    while (result.Status == InputStatus.OK)
    {
        points.Add(result.Point);
        cmdCtrl.WriteInfo($"已添加点 {points.Count}: ({result.Point.X}, {result.Point.Y})");
        
        result = await pointInputer.Execute("指定下一点或按ESC完成:");
    }
    
    cmdCtrl.WriteInfo($"共获取 {points.Count} 个点");
    return CommandResult.Succ();
}

4.6.3 CmdTextInputer - 文本输入

[CommandMethod(Name = "GetText")]
public async CommandResult GetText(IDocumentEditor docEditor, string[] args)
{
    var textInputer = new CmdTextInputer(docEditor);
    
    var result = await textInputer.Execute("请输入文本:");
    
    if (!string.IsNullOrEmpty(result))
    {
        docEditor.GetCommandController().WriteInfo($"输入的文本: {result}");
    }
    
    return CommandResult.Succ();
}

4.6.4 ElementSetInputer - 元素选择

[CommandMethod(Name = "SelectElements")]
public async CommandResult SelectElements(IDocumentEditor docEditor, string[] args)
{
    var elementInputer = new ElementSetInputer(docEditor);
    
    var result = await elementInputer.Execute("请选择元素:");
    
    if (result?.ValueX != null)
    {
        var elements = result.ValueX as List<LcElement>;
        var cmdCtrl = docEditor.GetCommandController();
        cmdCtrl.WriteInfo($"选择了 {elements.Count} 个元素");
        
        foreach (var ele in elements)
        {
            cmdCtrl.WriteInfo($"  - {ele.Type.DispalyName}");
        }
    }
    
    return CommandResult.Succ();
}

4.7 元素创建

4.7.1 创建基本元素

// 创建直线
var line = doc.CreateObject<LcLine>();
line.Start = new Vector2(0, 0);
line.End = new Vector2(100, 100);
line.Layer = "0";
vportRt.ActiveElementSet.InsertElement(line);

// 创建多段线
var polyline = doc.CreateObject<LcPolyLine>();
polyline.AddVertex(new Vector2(0, 0));
polyline.AddVertex(new Vector2(100, 0));
polyline.AddVertex(new Vector2(100, 100));
polyline.IsClosed = true;
vportRt.ActiveElementSet.InsertElement(polyline);

// 创建圆
var circle = doc.CreateObject<LcCircle>();
circle.Center = new Vector2(50, 50);
circle.Radius = 25;
vportRt.ActiveElementSet.InsertElement(circle);

4.7.2 创建自定义元素

[CommandMethod(Name = "CreateLawn")]
public CommandResult CreateLawn(IDocumentEditor docEditor, string[] args)
{
    var docRt = docEditor.GetDocumentRuntime();
    var doc = docRt.Document;
    var vportRt = docRt.ActiveViewportRuntime;
    
    // 获取组件定义
    var lawnDef = docRt.GetUseComDef("场布施工设计.绿色文明", "草坪", null) as QdLawnDef;
    
    // 创建草坪元素
    var lawn = new QdLawn(lawnDef);
    lawn.Initilize(doc);
    
    // 设置轮廓
    var poly = new Polyline2d();
    poly.AddCurve(new Line2d(new Vector2(0, 0), new Vector2(100, 0)));
    poly.AddCurve(new Line2d(new Vector2(100, 0), new Vector2(100, 100)));
    poly.AddCurve(new Line2d(new Vector2(100, 100), new Vector2(0, 100)));
    poly.AddCurve(new Line2d(new Vector2(0, 100), new Vector2(0, 0)));
    lawn.Outline = poly;
    
    // 设置其他属性
    lawn.ResetBoundingBox();
    lawn.Layer = "Layout_Lawn";
    lawn.Bottom = 0;
    
    // 插入到文档
    vportRt.ActiveElementSet.InsertElement(lawn);
    
    return CommandResult.Succ();
}

4.8 事件处理

4.8.1 文档事件

public void OnInitializeDocRt(DocumentRuntime docRt)
{
    var doc = docRt.Document;
    
    // 订阅元素添加事件
    doc.ElementAdded += (sender, e) =>
    {
        Console.WriteLine($"元素已添加: {e.Element.Type.Name}");
    };
    
    // 订阅元素删除事件
    doc.ElementRemoved += (sender, e) =>
    {
        Console.WriteLine($"元素已删除: {e.Element.Type.Name}");
    };
    
    // 订阅元素修改事件
    doc.ElementChanged += (sender, e) =>
    {
        Console.WriteLine($"元素已修改: {e.Element.Type.Name}");
    };
}

4.8.2 选择事件

public void OnInitializeDocRt(DocumentRuntime docRt)
{
    // 订阅选择变化事件
    docRt.SelectionChanged += (sender, e) =>
    {
        var selectedElements = docRt.GetSelectedElements();
        Console.WriteLine($"选择了 {selectedElements.Count} 个元素");
    };
}

4.9 配置持久化

4.9.1 使用Settings

// 创建Settings类
public class MyPluginSettings
{
    private static readonly string SettingsPath = 
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
                     "LightCAD", "MyPlugin", "settings.json");
    
    public string DefaultLayer { get; set; } = "0";
    public int DefaultColor { get; set; } = 0xFFFFFF;
    public double DefaultLineWidth { get; set; } = 1.0;
    
    public static MyPluginSettings Load()
    {
        if (File.Exists(SettingsPath))
        {
            var json = File.ReadAllText(SettingsPath);
            return JsonConvert.DeserializeObject<MyPluginSettings>(json);
        }
        return new MyPluginSettings();
    }
    
    public void Save()
    {
        var dir = Path.GetDirectoryName(SettingsPath);
        if (!Directory.Exists(dir))
            Directory.CreateDirectory(dir);
        
        var json = JsonConvert.SerializeObject(this, Formatting.Indented);
        File.WriteAllText(SettingsPath, json);
    }
}

4.9.2 使用示例

public class MyPlugin : ILcPlugin
{
    private static MyPluginSettings Settings;
    
    public void Loaded()
    {
        Settings = MyPluginSettings.Load();
    }
    
    public void OnDisposeDocRt(DocumentRuntime docRt)
    {
        Settings.Save();
    }
}

4.10 错误处理

4.10.1 命令异常处理

[CommandMethod(Name = "SafeCommand")]
public CommandResult SafeCommand(IDocumentEditor docEditor, string[] args)
{
    try
    {
        // 可能抛出异常的代码
        DoSomething();
        return CommandResult.Succ();
    }
    catch (ArgumentException ex)
    {
        docEditor.GetCommandController().WriteError($"参数错误: {ex.Message}");
        return CommandResult.Fail(ex.Message);
    }
    catch (Exception ex)
    {
        docEditor.GetCommandController().WriteError($"发生错误: {ex.Message}");
        // 记录详细日志
        LogError(ex);
        return CommandResult.Fail(ex.Message);
    }
}

4.10.2 输入验证

[CommandMethod(Name = "ValidatedInput")]
public async CommandResult ValidatedInput(IDocumentEditor docEditor, string[] args)
{
    var cmdCtrl = docEditor.GetCommandController();
    var pointInputer = new PointInputer(docEditor);
    
    // 获取第一个点
    var result1 = await pointInputer.Execute("指定第一个点:");
    if (result1.Status != InputStatus.OK)
    {
        cmdCtrl.WriteInfo("操作已取消");
        return CommandResult.Succ();
    }
    
    // 获取第二个点,并验证不能与第一个点相同
    Vector2 point2;
    while (true)
    {
        var result2 = await pointInputer.Execute("指定第二个点:");
        if (result2.Status != InputStatus.OK)
        {
            cmdCtrl.WriteInfo("操作已取消");
            return CommandResult.Succ();
        }
        
        point2 = result2.Point;
        if (point2.DistanceTo(result1.Point) > 0.001)
            break;
        
        cmdCtrl.WriteError("第二个点不能与第一个点重合,请重新输入");
    }
    
    // 继续处理...
    return CommandResult.Succ();
}

4.11 完整插件示例

using System;
using System.Collections.Generic;
using LightCAD.Core;
using LightCAD.Runtime;
using LightCAD.Drawing;
using LightCAD.MathLib;

namespace MyCompletePlugin
{
    /// <summary>
    /// 完整的插件示例
    /// </summary>
    public class MyCompletePlugin : ILcPlugin
    {
        public static TabItem MyTabItem = new TabItem
        {
            Name = "MyTools",
            Text = "我的工具",
            ShortcutKey = "ALT-Y",
            ButtonGroups = new List<TabButtonGroup>
            {
                new TabButtonGroup
                {
                    Buttons = new List<TabButton>
                    {
                        new TabButton
                        {
                            Name = "DrawRectangle",
                            Text = "绘制矩形",
                            IsCommand = true,
                        },
                        new TabButton
                        {
                            Name = "ShowInfo",
                            Text = "显示信息",
                            IsCommand = true,
                        }
                    }
                }
            }
        };
        
        public void Loaded()
        {
            Console.WriteLine("MyCompletePlugin loaded");
        }
        
        public void InitUI()
        {
            // UI初始化
        }
        
        public void Completed()
        {
            AppRuntime.UISystem.AddInitTabItems([MyTabItem]);
        }
        
        public void OnInitializeDocRt(DocumentRuntime docRt)
        {
            Console.WriteLine($"Document initialized: {docRt.Document.Name}");
        }
        
        public void OnDisposeDocRt(DocumentRuntime docRt)
        {
            Console.WriteLine($"Document disposed: {docRt.Document.Name}");
        }
    }
    
    [CommandClass]
    public class MyCommands
    {
        [CommandMethod(Name = "DrawRectangle", ShortCuts = "DR")]
        public async CommandResult DrawRectangle(IDocumentEditor docEditor, string[] args)
        {
            var cmdCtrl = docEditor.GetCommandController();
            var pointInputer = new PointInputer(docEditor);
            
            // 获取第一个角点
            cmdCtrl.WriteInfo("指定矩形第一个角点:");
            var result1 = await pointInputer.Execute();
            if (result1.Status != InputStatus.OK)
                return CommandResult.Succ();
            
            // 获取对角点
            cmdCtrl.WriteInfo("指定矩形对角点:");
            var result2 = await pointInputer.Execute();
            if (result2.Status != InputStatus.OK)
                return CommandResult.Succ();
            
            // 创建矩形
            var docRt = docEditor.GetDocumentRuntime();
            var doc = docRt.Document;
            var vportRt = docRt.ActiveViewportRuntime;
            
            var p1 = result1.Point;
            var p2 = result2.Point;
            
            var polyline = doc.CreateObject<LcPolyLine>();
            polyline.AddVertex(new Vector2(p1.X, p1.Y));
            polyline.AddVertex(new Vector2(p2.X, p1.Y));
            polyline.AddVertex(new Vector2(p2.X, p2.Y));
            polyline.AddVertex(new Vector2(p1.X, p2.Y));
            polyline.IsClosed = true;
            
            vportRt.ActiveElementSet.InsertElement(polyline);
            
            cmdCtrl.WriteInfo("矩形创建成功");
            return CommandResult.Succ();
        }
        
        [CommandMethod(Name = "ShowInfo", ShortCuts = "SI")]
        public CommandResult ShowInfo(IDocumentEditor docEditor, string[] args)
        {
            var docRt = docEditor.GetDocumentRuntime();
            var doc = docRt.Document;
            var cmdCtrl = docEditor.GetCommandController();
            
            cmdCtrl.WriteInfo("=== 文档信息 ===");
            cmdCtrl.WriteInfo($"文档名称: {doc.Name}");
            cmdCtrl.WriteInfo($"元素总数: {doc.Elements.Count}");
            cmdCtrl.WriteInfo($"图层数量: {doc.Layers.Count}");
            cmdCtrl.WriteInfo("================");
            
            return CommandResult.Succ();
        }
    }
}

4.12 本章小结

本章介绍了LightCAD插件开发的基础知识:

  1. 插件体系:ILcPlugin接口和生命周期
  2. UI集成:TabItem、TabButton的创建和配置
  3. 命令开发:CommandClass和CommandMethod的使用
  4. 文档操作:获取文档、遍历元素、图层管理
  5. 用户输入:点输入、文本输入、元素选择
  6. 元素创建:基本元素和自定义元素的创建
  7. 事件处理:文档事件和选择事件
  8. 配置持久化:设置的保存和加载
  9. 错误处理:异常处理和输入验证

掌握这些基础知识后,下一章我们将深入学习元素类型系统的详细实现。


posted @ 2026-01-31 16:02  我才是银古  阅读(2)  评论(0)    收藏  举报