插件系统与开发实战

第十五章 插件系统与开发实战

15.1 插件系统概述

15.1.1 插件架构

LibreCAD采用Qt插件机制,允许动态加载扩展功能:

┌─────────────────────────────────────────────┐
│              LibreCAD 主程序                 │
├─────────────────────────────────────────────┤
│             插件接口层                       │
│  ┌──────────────────────────────────────┐   │
│  │         QC_PluginInterface           │   │
│  │         Document_Interface           │   │
│  └──────────────────────────────────────┘   │
├─────────────────────────────────────────────┤
│             插件加载器                       │
└─────────────────────────────────────────────┘
              ↑         ↑         ↑
        ┌─────┴─┐  ┌────┴────┐  ┌─┴────┐
        │Plugin1│  │ Plugin2 │  │ ...  │
        └───────┘  └─────────┘  └──────┘

15.1.2 官方插件列表

LibreCAD自带的官方插件:

插件 功能
align 对象对齐
asciifile ASCII文件导入
divide 等分线段
gear 齿轮绘制
list 列表显示
picfile 图片文件处理
plotequation 方程曲线绘制
pointstocsv 点导出CSV
sameprop 相同属性选择
sample 示例插件

15.1.3 插件目录

插件文件位置:

  • Windows: %APPDATA%\LibreCAD\plugins\ 或安装目录
  • Linux: ~/.local/share/librecad/plugins//usr/share/librecad/plugins/
  • macOS: ~/Library/Application Support/LibreCAD/plugins/

15.2 插件接口详解

15.2.1 QC_PluginInterface

所有插件必须实现的接口:

// qc_plugininterface.h
#ifndef QC_PLUGININTERFACE_H
#define QC_PLUGININTERFACE_H

#include <QtPlugin>
#include <QString>
#include <QStringList>
#include "document_interface.h"

#define QC_PLUGININTERFACE_IID "org.librecad.PluginInterface"

class QC_PluginInterface {
public:
    virtual ~QC_PluginInterface() = default;
    
    // 插件名称
    virtual QString name() const = 0;
    
    // 插件功能列表
    virtual PluginCapabilities getCapabilities() const = 0;
    
    // 执行命令
    virtual void execComm(Document_Interface* doc,
                         QWidget* parent,
                         QString cmd) = 0;
};

Q_DECLARE_INTERFACE(QC_PluginInterface, QC_PLUGININTERFACE_IID)

#endif

15.2.2 PluginCapabilities

插件能力描述:

// 能力标志
enum PluginCapabilities {
    PluginNothing       = 0,      // 无特殊能力
    PluginDrawing       = 1 << 0, // 绘图功能
    PluginFileIO        = 1 << 1, // 文件I/O
    PluginModification  = 1 << 2, // 修改功能
    PluginInformation   = 1 << 3  // 信息功能
};

// 菜单入口
struct PluginMenuEntry {
    QString menuName;   // 菜单名称
    QString command;    // 命令标识
};

// 完整能力结构
struct PluginCapabilities {
    PluginCapabilities flags;
    QStringList commands;           // 命令列表
    QList<PluginMenuEntry> menuEntries; // 菜单项
};

15.2.3 Document_Interface

与图形文档交互的接口:

// document_interface.h (部分)
class Document_Interface {
public:
    virtual ~Document_Interface() = default;
    
    // ===== 实体创建 =====
    
    // 添加点
    virtual void addPoint(QPointF* start) = 0;
    
    // 添加直线
    virtual void addLine(QPointF* start, QPointF* end) = 0;
    
    // 添加圆
    virtual void addCircle(QPointF* center, qreal radius) = 0;
    
    // 添加圆弧
    virtual void addArc(QPointF* center, qreal radius,
                       qreal startAngle, qreal endAngle) = 0;
    
    // 添加椭圆
    virtual void addEllipse(QPointF* center, QPointF* majorP,
                           qreal ratio, qreal angle1, qreal angle2) = 0;
    
    // 添加文字
    virtual void addText(QString text, QString style, 
                        QPointF* start, qreal height,
                        qreal angle, DPI::HAlign ha, DPI::VAlign va) = 0;
    
    // 添加折线
    virtual void addPolyline(QList<Plug_VertexData>& data,
                            bool closed = false) = 0;
    
    // 添加样条曲线
    virtual void addSpline(QList<QPointF>& points, bool closed = false) = 0;
    
    // ===== 属性设置 =====
    
    // 设置当前图层
    virtual void setLayer(QString name) = 0;
    
    // 获取当前图层
    virtual QString getCurrentLayer() = 0;
    
    // 获取图层列表
    virtual QStringList getLayers() = 0;
    
    // 添加图层
    virtual void addLayer(QString name) = 0;
    
    // 设置颜色
    virtual void setColor(int r, int g, int b) = 0;
    
    // 设置线宽
    virtual void setWidth(DPI::LineWidth width) = 0;
    
    // 设置线型
    virtual void setLineType(DPI::LineType type) = 0;
    
    // ===== 文档操作 =====
    
    // 获取文件名
    virtual QString getFilename() = 0;
    
    // 获取所选实体
    virtual QList<Plug_Entity*> getSelectedEntities() = 0;
    
    // 获取所有实体
    virtual QList<Plug_Entity*> getAllEntities() = 0;
    
    // 更新视图
    virtual void updateView() = 0;
    
    // 获取点(用户输入)
    virtual QPointF getPoint(QString message) = 0;
    
    // 获取实数(用户输入)
    virtual qreal getReal(QString message, qreal default_value = 0.0) = 0;
    
    // 获取整数
    virtual int getInt(QString message, int default_value = 0) = 0;
    
    // 获取字符串
    virtual QString getString(QString message, QString default_value = "") = 0;
    
    // ===== 撤销支持 =====
    
    virtual void startUndoCycle() = 0;
    virtual void endUndoCycle() = 0;
};

15.2.4 Plug_Entity

实体操作接口:

class Plug_Entity {
public:
    virtual ~Plug_Entity() = default;
    
    // 获取实体类型
    virtual DPI::EntityType getType() const = 0;
    
    // 获取数据
    virtual QHash<int, QVariant> getData() const = 0;
    
    // 更新数据
    virtual void updateData(QHash<int, QVariant>& data) = 0;
    
    // 获取点列表
    virtual void getPoints(QList<QPointF>& points) const = 0;
    
    // 移动
    virtual void move(QPointF offset) = 0;
    
    // 旋转
    virtual void rotate(QPointF center, qreal angle) = 0;
    
    // 缩放
    virtual void scale(QPointF center, qreal factor) = 0;
};

15.3 创建插件项目

15.3.1 项目结构

myplugin/
├── CMakeLists.txt      # CMake配置
├── myplugin.pro        # qmake配置(可选)
├── myplugin.h          # 插件头文件
├── myplugin.cpp        # 插件实现
├── myplugin.json       # 插件元数据
└── ts/                 # 翻译文件
    ├── myplugin_zh_CN.ts
    └── myplugin_en.ts

15.3.2 CMakeLists.txt

cmake_minimum_required(VERSION 3.16)
project(myplugin)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)

# 查找Qt
find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets)

# 包含目录
include_directories(
    ${CMAKE_CURRENT_SOURCE_DIR}
    ${LIBRECAD_INCLUDE_DIR}/plugins
)

# 创建插件库
add_library(myplugin SHARED
    myplugin.cpp
    myplugin.h
)

# 链接库
target_link_libraries(myplugin
    Qt6::Core
    Qt6::Gui
    Qt6::Widgets
)

# 设置输出目录
set_target_properties(myplugin PROPERTIES
    LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins
)

15.3.3 qmake配置(可选)

# myplugin.pro
include(../../common.pri)

TEMPLATE = lib
CONFIG += plugin

TARGET = myplugin

HEADERS = myplugin.h
SOURCES = myplugin.cpp

INCLUDEPATH += ../../librecad/src/plugins

DESTDIR = ../../unix/resources/plugins

15.3.4 插件元数据

// myplugin.json
{
    "name": "My Custom Plugin",
    "version": "1.0.0",
    "author": "Your Name",
    "description": "A custom plugin for LibreCAD",
    "license": "GPL-2.0"
}

15.4 插件开发实战

15.4.1 简单插件示例

创建一个绘制正多边形的插件:

// polygon_plugin.h
#ifndef POLYGON_PLUGIN_H
#define POLYGON_PLUGIN_H

#include <QObject>
#include <QtPlugin>
#include "qc_plugininterface.h"

class PolygonPlugin : public QObject, public QC_PluginInterface {
    Q_OBJECT
    Q_PLUGIN_METADATA(IID QC_PLUGININTERFACE_IID FILE "polygon_plugin.json")
    Q_INTERFACES(QC_PluginInterface)

public:
    explicit PolygonPlugin(QObject* parent = nullptr);
    virtual ~PolygonPlugin() = default;
    
    // 实现接口
    QString name() const override;
    PluginCapabilities getCapabilities() const override;
    void execComm(Document_Interface* doc,
                 QWidget* parent,
                 QString cmd) override;

private:
    void drawPolygon(Document_Interface* doc, QWidget* parent);
};

#endif
// polygon_plugin.cpp
#include "polygon_plugin.h"
#include <QInputDialog>
#include <QMessageBox>
#include <cmath>

PolygonPlugin::PolygonPlugin(QObject* parent)
    : QObject(parent) {
}

QString PolygonPlugin::name() const {
    return "Polygon Plugin";
}

PluginCapabilities PolygonPlugin::getCapabilities() const {
    PluginCapabilities caps;
    caps.flags = PluginDrawing;
    caps.commands << "polygon" << "pl";
    
    PluginMenuEntry entry;
    entry.menuName = tr("绘制正多边形");
    entry.command = "polygon";
    caps.menuEntries << entry;
    
    return caps;
}

void PolygonPlugin::execComm(Document_Interface* doc,
                             QWidget* parent,
                             QString cmd) {
    Q_UNUSED(cmd)
    drawPolygon(doc, parent);
}

void PolygonPlugin::drawPolygon(Document_Interface* doc,
                                QWidget* parent) {
    // 获取边数
    bool ok;
    int sides = QInputDialog::getInt(
        parent,
        tr("正多边形"),
        tr("输入边数:"),
        6,      // 默认值
        3,      // 最小值
        100,    // 最大值
        1,      // 步长
        &ok
    );
    
    if (!ok) return;
    
    // 获取中心点
    QPointF center = doc->getPoint(tr("指定中心点:"));
    
    // 获取半径
    qreal radius = doc->getReal(tr("指定外接圆半径:"), 50.0);
    if (radius <= 0) {
        QMessageBox::warning(parent, tr("错误"), tr("半径必须大于0"));
        return;
    }
    
    // 开始撤销周期
    doc->startUndoCycle();
    
    // 计算顶点并创建折线
    QList<Plug_VertexData> vertices;
    
    for (int i = 0; i < sides; i++) {
        double angle = 2.0 * M_PI * i / sides - M_PI / 2;
        QPointF vertex(
            center.x() + radius * cos(angle),
            center.y() + radius * sin(angle)
        );
        
        Plug_VertexData vd;
        vd.point = vertex;
        vd.bulge = 0.0;
        vertices.append(vd);
    }
    
    // 添加闭合折线
    doc->addPolyline(vertices, true);
    
    // 结束撤销周期
    doc->endUndoCycle();
    
    // 更新视图
    doc->updateView();
}

15.4.2 齿轮插件分析

分析官方gear插件的实现:

// gear/gear.cpp (简化版)
#include "gear.h"
#include <QDialog>
#include <QFormLayout>
#include <QDoubleSpinBox>
#include <QPushButton>

void GearPlugin::execComm(Document_Interface* doc,
                          QWidget* parent,
                          QString cmd) {
    Q_UNUSED(cmd)
    
    // 创建参数对话框
    QDialog dialog(parent);
    dialog.setWindowTitle(tr("齿轮参数"));
    
    QFormLayout* layout = new QFormLayout(&dialog);
    
    QDoubleSpinBox* moduleBox = new QDoubleSpinBox;
    moduleBox->setRange(0.1, 100.0);
    moduleBox->setValue(2.0);
    layout->addRow(tr("模数:"), moduleBox);
    
    QSpinBox* teethBox = new QSpinBox;
    teethBox->setRange(6, 200);
    teethBox->setValue(20);
    layout->addRow(tr("齿数:"), teethBox);
    
    QDoubleSpinBox* pressureBox = new QDoubleSpinBox;
    pressureBox->setRange(14.5, 25.0);
    pressureBox->setValue(20.0);
    layout->addRow(tr("压力角:"), pressureBox);
    
    QPushButton* okButton = new QPushButton(tr("确定"));
    connect(okButton, &QPushButton::clicked, &dialog, &QDialog::accept);
    layout->addRow(okButton);
    
    if (dialog.exec() != QDialog::Accepted) return;
    
    // 获取参数
    double module = moduleBox->value();
    int teeth = teethBox->value();
    double pressure = pressureBox->value();
    
    // 获取中心点
    QPointF center = doc->getPoint(tr("指定齿轮中心:"));
    
    // 绘制齿轮
    drawGear(doc, center, module, teeth, pressure);
}

void GearPlugin::drawGear(Document_Interface* doc,
                          QPointF center,
                          double module,
                          int teeth,
                          double pressureAngle) {
    doc->startUndoCycle();
    
    // 计算齿轮参数
    double pitchRadius = module * teeth / 2.0;
    double addendum = module;
    double dedendum = 1.25 * module;
    double outerRadius = pitchRadius + addendum;
    double rootRadius = pitchRadius - dedendum;
    double baseRadius = pitchRadius * cos(pressureAngle * M_PI / 180.0);
    
    // 生成齿形
    QList<Plug_VertexData> profile;
    
    for (int i = 0; i < teeth; i++) {
        double toothAngle = 2.0 * M_PI * i / teeth;
        
        // 每个齿的轮廓点
        // ... 渐开线计算 ...
        
        // 添加到轮廓
    }
    
    // 添加折线
    doc->addPolyline(profile, true);
    
    // 添加节圆(参考)
    doc->setLineType(DPI::DashLine);
    doc->addCircle(&center, pitchRadius);
    
    doc->setLineType(DPI::SolidLine);
    doc->endUndoCycle();
    doc->updateView();
}

15.4.3 文件导入插件

创建从CSV导入点的插件:

// csv_import.cpp
#include "csv_import.h"
#include <QFileDialog>
#include <QFile>
#include <QTextStream>
#include <QMessageBox>

void CSVImportPlugin::execComm(Document_Interface* doc,
                               QWidget* parent,
                               QString cmd) {
    // 选择文件
    QString filename = QFileDialog::getOpenFileName(
        parent,
        tr("选择CSV文件"),
        QString(),
        tr("CSV文件 (*.csv);;所有文件 (*.*)")
    );
    
    if (filename.isEmpty()) return;
    
    QFile file(filename);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QMessageBox::critical(parent, tr("错误"), 
            tr("无法打开文件"));
        return;
    }
    
    doc->startUndoCycle();
    
    QTextStream in(&file);
    int lineNumber = 0;
    int pointCount = 0;
    
    while (!in.atEnd()) {
        QString line = in.readLine();
        lineNumber++;
        
        // 跳过空行和标题行
        if (line.isEmpty() || lineNumber == 1) continue;
        
        // 解析CSV行
        QStringList parts = line.split(',');
        if (parts.size() < 2) continue;
        
        bool okX, okY;
        double x = parts[0].trimmed().toDouble(&okX);
        double y = parts[1].trimmed().toDouble(&okY);
        
        if (okX && okY) {
            QPointF point(x, y);
            doc->addPoint(&point);
            pointCount++;
        }
    }
    
    file.close();
    doc->endUndoCycle();
    doc->updateView();
    
    QMessageBox::information(parent, tr("导入完成"),
        tr("成功导入 %1 个点").arg(pointCount));
}

15.5 插件UI开发

15.5.1 使用Qt Designer

创建可视化的参数对话框:

// 使用.ui文件
#include "ui_myplugindialog.h"

class MyPluginDialog : public QDialog {
    Q_OBJECT

public:
    explicit MyPluginDialog(QWidget* parent = nullptr)
        : QDialog(parent)
        , ui(new Ui::MyPluginDialog) {
        ui->setupUi(this);
        
        // 连接信号槽
        connect(ui->okButton, &QPushButton::clicked,
                this, &QDialog::accept);
        connect(ui->cancelButton, &QPushButton::clicked,
                this, &QDialog::reject);
    }
    
    ~MyPluginDialog() {
        delete ui;
    }
    
    // 获取参数
    double getRadius() const {
        return ui->radiusSpinBox->value();
    }
    
    int getSides() const {
        return ui->sidesSpinBox->value();
    }

private:
    Ui::MyPluginDialog* ui;
};

15.5.2 纯代码UI

QDialog* createParameterDialog(QWidget* parent) {
    QDialog* dialog = new QDialog(parent);
    dialog->setWindowTitle(tr("参数设置"));
    dialog->setMinimumWidth(300);
    
    QVBoxLayout* mainLayout = new QVBoxLayout(dialog);
    
    // 参数组
    QGroupBox* paramGroup = new QGroupBox(tr("参数"));
    QFormLayout* formLayout = new QFormLayout(paramGroup);
    
    QDoubleSpinBox* radiusBox = new QDoubleSpinBox;
    radiusBox->setRange(0.1, 10000.0);
    radiusBox->setValue(50.0);
    radiusBox->setDecimals(2);
    formLayout->addRow(tr("半径:"), radiusBox);
    
    QSpinBox* sidesBox = new QSpinBox;
    sidesBox->setRange(3, 100);
    sidesBox->setValue(6);
    formLayout->addRow(tr("边数:"), sidesBox);
    
    mainLayout->addWidget(paramGroup);
    
    // 按钮
    QHBoxLayout* buttonLayout = new QHBoxLayout;
    QPushButton* okButton = new QPushButton(tr("确定"));
    QPushButton* cancelButton = new QPushButton(tr("取消"));
    buttonLayout->addStretch();
    buttonLayout->addWidget(okButton);
    buttonLayout->addWidget(cancelButton);
    
    mainLayout->addLayout(buttonLayout);
    
    QObject::connect(okButton, &QPushButton::clicked,
                    dialog, &QDialog::accept);
    QObject::connect(cancelButton, &QPushButton::clicked,
                    dialog, &QDialog::reject);
    
    return dialog;
}

15.6 插件调试

15.6.1 调试输出

#include <QDebug>

void MyPlugin::execComm(Document_Interface* doc,
                        QWidget* parent,
                        QString cmd) {
    qDebug() << "Plugin executed with command:" << cmd;
    
    // 调试实体数据
    QList<Plug_Entity*> entities = doc->getSelectedEntities();
    qDebug() << "Selected entities:" << entities.size();
    
    for (Plug_Entity* entity : entities) {
        qDebug() << "Entity type:" << entity->getType();
        QHash<int, QVariant> data = entity->getData();
        qDebug() << "Entity data:" << data;
    }
}

15.6.2 在Qt Creator中调试

  1. 设置LibreCAD作为启动程序
  2. 设置插件输出目录
  3. 添加断点
  4. 启动调试

15.6.3 日志文件

void writeLog(const QString& message) {
    QFile file(QDir::homePath() + "/myplugin.log");
    if (file.open(QIODevice::Append | QIODevice::Text)) {
        QTextStream out(&file);
        out << QDateTime::currentDateTime().toString() 
            << " - " << message << "\n";
        file.close();
    }
}

15.7 插件国际化

15.7.1 使用tr()

QString PolygonPlugin::name() const {
    return tr("正多边形插件");
}

void PolygonPlugin::drawPolygon(Document_Interface* doc,
                                QWidget* parent) {
    QMessageBox::information(parent,
        tr("提示"),
        tr("请指定多边形的中心点"));
}

15.7.2 翻译文件

创建翻译文件:

# 提取需要翻译的字符串
lupdate myplugin.cpp -ts ts/myplugin_zh_CN.ts

# 使用Qt Linguist编辑翻译

# 编译翻译文件
lrelease ts/myplugin_zh_CN.ts -qm ts/myplugin_zh_CN.qm

15.7.3 加载翻译

class MyPlugin : public QObject, public QC_PluginInterface {
public:
    MyPlugin() {
        // 加载翻译文件
        QTranslator* translator = new QTranslator(this);
        QString locale = QLocale::system().name();
        
        if (translator->load("myplugin_" + locale, ":/translations")) {
            QCoreApplication::installTranslator(translator);
        }
    }
};

15.8 发布插件

15.8.1 打包清单

myplugin-1.0.0/
├── myplugin.dll (Windows) 或
│   libmyplugin.so (Linux) 或
│   libmyplugin.dylib (macOS)
├── translations/
│   ├── myplugin_zh_CN.qm
│   └── myplugin_en.qm
├── README.md
├── LICENSE
└── CHANGELOG.md

15.8.2 安装说明

# MyPlugin 安装说明

## Windows
1. 将 myplugin.dll 复制到 LibreCAD 安装目录下的 plugins 文件夹
2. 将翻译文件复制到 translations 文件夹
3. 重启 LibreCAD

## Linux
1. 将 libmyplugin.so 复制到 ~/.local/share/librecad/plugins/
2. 重启 LibreCAD

## macOS
1. 将 libmyplugin.dylib 复制到 ~/Library/Application Support/LibreCAD/plugins/
2. 重启 LibreCAD

15.8.3 版本兼容性

// 检查接口版本
#define PLUGIN_INTERFACE_VERSION 1

class MyPlugin : public QC_PluginInterface {
public:
    int getInterfaceVersion() const {
        return PLUGIN_INTERFACE_VERSION;
    }
};

15.9 本章小结

本章介绍了LibreCAD的插件系统:

  1. 插件架构:Qt插件机制、官方插件
  2. 插件接口:QC_PluginInterface、Document_Interface
  3. 创建项目:项目结构、CMake/qmake配置
  4. 开发实战:正多边形、齿轮、CSV导入
  5. UI开发:Qt Designer、纯代码UI
  6. 调试技巧:调试输出、Qt Creator调试
  7. 国际化:tr()、翻译文件
  8. 发布:打包、安装说明

插件开发是扩展LibreCAD功能的有效方式,可以根据需要添加自定义功能。


上一章:Action系统与命令开发 | 下一章:二次开发进阶与最佳实践


posted @ 2026-01-10 13:13  我才是银古  阅读(19)  评论(0)    收藏  举报