← → 翻页 · B 静态 · ESC 索引
软件 二次开发 · 本科生工作坊
SS · 26.07 · 01 / 19
SECONDARY DEVELOPMENT · CUSTOM CARD

从零做出你的
第一张自定义卡片

用 .NET Framework 写一个能输出 “Hello, world!” 的 软件 数据处理卡片 —— 参数化结构设计的二次开发入门。
面向本科生 · 约 90 分钟
→ 方向键 / 滚轮翻页
Course Map · 课程地图
02 / 19
90 分钟 · 五节 + 今日产出

我们今天要走的路

01
背景与认知
参数化结构设计、卡片与数据流是什么
02
原理与架构
自定义卡片 = .NET 类库 + 四个抽象成员
03
实战编码
一步步写出 Hello, World 卡片
04
部署与排错
.crd 复制、重启加载、常见错误
05
拓展与练习
带输入端口的卡片 + 课后任务
今日产出
一张能在 软件 里输出文字的卡片
SECTION
01
01
BACKGROUND
第一节 · 15 分钟

背景与认知

先建立整体图景:卡片和数据流怎么运作、为什么值得二次开发。

Core Concepts · 参数化画布
04 / 19
画布上一切皆卡片

三个必须先懂的概念

CONCEPT 01
卡片 Card
画布上看到的每个方块都是一张卡片,是功能的最小单元。
Building Block
CONCEPT 02
端口 Input/Output
卡片左侧是输入端口、右侧是输出端口,用来接收和送出数据。
Input · Output
CONCEPT 03
数据流 Dataflow
卡片之间连线,数据从左流向右,串起一条参数化工作流。
Left → Right
Why Extend · 为什么要二次开发
05 / 19
内置卡片够用吗?

从“用现成的”到“造自己的”

现状 只用内置卡片
受限于框架

遇到特殊的计算逻辑、专属的数据处理,找不到对应卡片就卡住。

  • 功能边界固定
  • 重复手工操作
  • 无法沉淀团队方法
目标 编写自定义卡片
按需扩展

把自己的算法封装成卡片,像积木一样复用到任意工作流中。

  • 功能自由生长
  • 逻辑一次编写、处处复用
  • 沉淀为团队资产
Today's Goal · 今日目标
06 / 19
THE HOOK · 一个小而完整的目标

做一张卡片,
让它输出 Hello, world!

麻雀虽小五脏俱全 —— 走通这一张卡片,你就掌握了所有自定义卡片的骨架。

SECTION
02
02
ARCHITECTURE
第二节 · 20 分钟

原理与架构

看清“自定义卡片到底是什么”:一个类库、两个依赖、四个抽象成员、一套加载机制。

The Essence · 卡片的本质
08 / 19
本质是什么

一个 .NET Framework
4.7.2 类库

⚠ 必须是 .NET Framework,不是 .NET,也不是 .NET Core;目标版本 ≤ 4.7.2。
运行必须引用的两个 DLL
ProjectPenguin.dll
软件 的主框架类库
NSGeo3dm.dll
图形数据描述依赖类库
Inheritance · 继承 ProcessorCardBase
09 / 19
继承 ProcessorCardBase 后必须补齐

一个构造 + 四个抽象成员

CTOR
无参构造函数
给卡片起名字、定分类(base 五参数)
01
CardGuid
卡片的“身份证”,全局唯一标识
02
Build
卡片运行的主逻辑(核心)
03
AddInputSideDataCards
声明左侧输入端口
04
AddOutputSideDataCards
声明右侧输出端口
全部实现
删光 NotImplementedException 即可编译
Load Mechanism · 加载机制
10 / 19
编译产物如何被 软件 认出

类库 → .crd → 启动加载

STEP 01编译类库生成 .dll 文件
STEP 02改名 .crd复制并改后缀
STEP 03放入卡片文件夹Penguin\Cards
STEP 04启动时加载实例化卡片类
SECTION
03
03
HANDS-ON
第三节 · 30 分钟

实战:Hello, World 卡片

跟着一步步敲代码:新建项目 → 加引用 → 继承实现 → 写核心逻辑。

Step 1 & 2 · 建项目 + 加引用
12 / 19
动手前的四个关键动作

项目搭建的四个要点

01
新建类库
选“类库 (.NET Framework)”,不要选纯“类库”
02
目标框架
选 .NET Framework 4.7.2 或更低版本
03
添加引用
浏览到 软件 安装目录,添加两个 DLL
04
复制本地 = 否
选中两个引用,属性里将 Copy Local 设为 false
Step 3 · 继承骨架 + 命名
13 / 19
using ProjectPenguin.Cards · 实现抽象类

继承 + 无参构造函数

using ProjectPenguin.Cards;

public class Class1 : ProcessorCardBase
{
    public Class1() : base(
        "基本名称",   // 画布中显示
        "缩写",       // 太长时用
        "描述信息",   // 悬停提示
        "大类",       // 分类
        "子类")
    {
    }
}
Class1 继承 ProcessorCardBase,用 VS 的“实现抽象类”一键生成骨架。
卡片的名字与分类由 无参构造函数 的 base(...) 决定。
Step 3 · 实现四个抽象成员
14 / 19
身份 · 端口 · 逻辑

CardGuid / 端口 / Build

01
CardGuid
public override Guid
CardGuid =>
 Guid.Parse(
 "{95FAD3CB-…}");
工具→创建 GUID,切勿手写
02
输出端口
helper.
 AddWellKnownType
 OfDataCard(
 typeof(string),
 "出口","出口",
 "描述…",
 DataProcessingMode
  .OneByOne);
声明一个 string 出口
03
Build
public override
void Build(
 IDataDelivery idd)
{
 idd.SetDataItem(
  0,
  "Hello, world!");
}
把出口 0 设为字符串
Step 4 · 自动部署
15 / 19
生成后事件命令行 · Post-build

编译后自动复制到卡片文件夹

Copy "$(TargetPath)"
  "C:\Users\<用户名>\AppData\Roaming
   \nonstructure\Penguin\Cards\<你的工程名>.crd"
在项目属性 → 生成事件 里填入命令,每次编译自动复制并改名为 .crd
引号用英文半角;.crd 文件名 = 你的 dll 名,只把后缀 .dll 改成 .crd。
Verify · 重启 软件
16 / 19
关闭并重启 软件 · 拖入卡片

Hello, world!

看到输出的这一刻,你已经完整走通了自定义卡片的全流程。

Pitfalls · 常见错误
17 / 19
卡不住的四个坑

加载失败?先查这四条

GUID 手写
冲突导致加载失败,务必用工具生成
复制本地忘设否
两个 DLL 的 Copy Local 必须为 false
残留未实现异常
NotImplementedException 必须删干净
全角引号 / 缺构造
命令行引号要半角;无参构造不能少
18 / 19
CLOSING
NEXT STEP

再进一步

把今天的骨架,长成你自己的卡片。
软件 二次开发工作坊
TAKEAWAYS
03
01

课后练习

做一张“两数相加”卡片,练习输入端口与在 Build 中读取数据。

下载 processorcard-builder Skill (.zip)
02

思考题

DataProcessingMode.OneByOne 是什么含义?多端口如何编号?

03

继续探索

关注官方公众号与《二次开发指南》,加入交流群一起玩。

→ 见下页 · ANSWER
Answer · gama-processorcard-builder
19 / 19
ANSWER · 已由 Qoder 编译验证 ✓

答案:两数相加卡片工程

目录结构 · SDK 风格工程
F:\repos\GamaCardDemo\
├─ GamaCardDemo.csproj        SDK · net472
├─ TwoNumberAdditionCard.cs   卡片类
└─ bin\Debug\
   └─ GamaCardDemo.dll        编译产物

%AppData%\nonstructure\Penguin\Cards\
└─ GamaCardDemo.crd           生成后自动复制
BUILD
0 Error · 0 Warning
CardGuid
344AD618-…383C
→ 完 · END OF DECK
TwoNumberAdditionCard.cs
using System;
using ProjectPenguin.Cards;
using ProjectPenguin.Cards.Enums;
using ProjectPenguin.Cards.ProcessorCardHelpers;

namespace GamaCardDemo;

public class TwoNumberAdditionCard : ProcessorCardBase
{
  public TwoNumberAdditionCard()
    : base("两数相加","相加","计算 A + B","数学","数学运算符") { }

  public override string EnglishAlias => "Add Two Numbers";
  private static readonly Guid sc_guid =
    Guid.Parse("{344AD618-F33D-48C4-8E77-68B8BA49383C}");
  public override Guid CardGuid => sc_guid;

  protected override void AddInputSideDataCards(IDataCardEmbedmentHelper h) {
    h.AddWellKnownTypeOfDataCard(typeof(double),"第一个数","A","加数A",DataProcessingMode.OneByOne);
    h.AddWellKnownTypeOfDataCard(typeof(double),"第二个数","B","加数B",DataProcessingMode.OneByOne);
  }
  protected override void AddOutputSideDataCards(IDataCardEmbedmentHelper h) {
    h.AddWellKnownTypeOfDataCard(typeof(double),"结果","结果","A + B",DataProcessingMode.OneByOne);
  }
  public override void Build(IDataDelivery idd) {
    double a = 0, b = 0;
    if (!idd.GetDataItem(0, ref a)) return;
    if (!idd.GetDataItem(1, ref b)) return;
    idd.SetDataItem(0, a + b);
  }
}