乐于分享
好东西不私藏

从零搭Revit插件框架:Ribbon面板 + 多命令,一套模板搞定

从零搭Revit插件框架:Ribbon面板 + 多命令,一套模板搞定

AI在BIM编程中的能力分析

从零搭Revit插件框架:Ribbon面板 + 多命令,一套模板搞定


很多人第一次写Revit插件,都是在Visual Studio里新建一个类,然后:

public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements)
{
    // 先写这一行
    TaskDialog.Show("Hello""Revit插件跑起来了");
return Result.Succeeded;
}

跑通了,挺有成就感。

然后呢?

然后你开始写第二个命令、第三个命令,每个命令都是一套类似的代码,最后发现——项目里散落着十几个 .cs 文件,每个都要单独注册、单独调试,Ribbon面板?手动在 Revit 里一个个添加,外接程序对话框里一行行填。

这不是做开发,这是攒代码。

今天来搭一套完整的插件框架:多命令集中管理、Ribbon面板自动生成、新增命令只需继承一个类,就能自动出现在面板上。

新手友好,代码可直接复制使用。


一、先搞清楚两个基本概念

Revit插件的入口只有两种:IExternalCommand 和 IExternalApplication

搞不清楚这两个,后面的代码你都不知道往哪写。

IExternalCommand —— 具体的命令

你点击Ribbon按钮执行的每一个操作,本质上都是一个 IExternalCommand

publicclassMyCommand : IExternalCommand
{
public Result Execute(
        ExternalCommandData commandData,
refstring message,
        ElementSet elements
)

    {
// 你的命令逻辑写在这里
return Result.Succeeded;
    }
}

Revit的API文档里那些示例代码,几乎全是 IExternalCommand 的写法。

IExternalApplication —— 应用级生命周期

Ribbon面板不是命令,它是 应用程序级别的UI,在Revit启动时创建、退出时销毁。这部分代码要实现 IExternalApplication

publicclassMyApp : IExternalApplication
{
public Result OnStartup(UIControlledApplication application)
    {
// Revit启动时执行,在这里创建Ribbon面板
return Result.Succeeded;
    }

public Result OnShutdown(UIControlledApplication application)
    {
// Revit关闭时执行,清理工作放这里
return Result.Succeeded;
    }
}

记住一句话:命令写进Execute里,面板写在OnStartup里。


二、框架的整体结构

在动手之前,先看一下这套模板的目录结构:

MyRevitPlugin/
├── MyRevitPlugin.csproj          # 项目文件
├── MyRevitPlugin.addin           # Revit加载项配置
├── App.cs                        # IExternalApplication,Ribbon面板在这里创建
├── Commands/
│   ├── BaseCommand.cs            # 命令基类,所有命令继承这个
│   ├── CmdHello.cs               # 示例命令1
│   ├── CmdGetWalls.cs            # 示例命令2
│   └── CmdBatchModify.cs         # 示例命令3
├── Resources/
│   └── Icons/                    # 图标文件夹
└── Utils/
    └── RevitUtils.cs             # 常用工具方法

核心思路:所有命令继承同一个基类,基类里统一处理日志、异常、和Revit文档上下文。新增命令只需新建一个类文件,不用动面板代码。


三、完整的框架代码

第一步:addin配置文件

Revit不知道你的插件存在,需要一个 .addin 文件告诉它。

在 %APPDATA%\Autodesk\Revit\Addins\2020\ (或其他版本对应文件夹)下新建:

MyRevitPlugin.addin

<?xml version="1.0" encoding="utf-8"?>
<RevitAddIns>
<AddInType="ExternalApplication">
<Assembly>D:\RevitPlugins\MyRevitPlugin.dll</Assembly>
<FullClassName>MyRevitPlugin.App</FullClassName>
<ClientId>GUID生成一个唯一的</ClientId>
<Name>My Revit Plugin</Name>
<VendorId>优易科技</VendorId>
<VendorDescription>优易BIM助手</VendorDescription>
</AddIn>
</RevitAddIns>

踩坑提示1:这里的 Assembly 路径必须和实际编译输出的 .dll 路径一致。建议用绝对路径,编译完直接复制到那个目录。

踩坑提示2ClientId 用 Visual Studio 的工具 → 创建GUID 生成,每个插件要不一样。写死了没问题,但两个插件用同一个GUID会导致加载冲突。


第二步:项目文件 csproj

<ProjectSdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<OutputPath>bin\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<PlatformTarget>x64</PlatformTarget>
</PropertyGroup>

<ItemGroup>
<ReferenceInclude="RevitAPI">
<HintPath>$(RevitSDK)\RevitAPI.dll</HintPath>
<Private>false</Private>
</Reference>
<ReferenceInclude="RevitAPIUI">
<HintPath>$(RevitSDK)\RevitAPIUI.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>

踩坑提示3:需要先在 Visual Studio 中添加环境变量 RevitSDK,指向 Revit API SDK 的安装目录。SDK 在 Revit 安装目录下搜索 RevitAPI.dll 即可找到。


第三步:命令基类 BaseCommand.cs

这是框架最核心的部分——所有命令继承它,自动具备日志记录、异常捕获、统一返回结果的能力。

using Autodesk.Revit.DB;
using Autodesk.Revit.UI;

namespaceMyRevitPlugin.Commands
{
///<summary>
/// 所有命令的基类
/// 统一处理:日志记录、异常捕获、文档上下文检查
///</summary>
publicabstractclassBaseCommand : IExternalCommand
    {
public Result Execute(
            ExternalCommandData commandData,
refstring message,
            ElementSet elements
)

        {
// 1. 检查文档是否有效
            UIDocument uiDoc = commandData.Application.ActiveUIDocument;
if (uiDoc == null)
            {
                message = "未检测到打开的项目文档。";
return Result.Failed;
            }

            Document doc = uiDoc.Document;
if (doc.IsFamilyDocument)
            {
                message = "请在项目文档中运行此命令。";
return Result.Failed;
            }

// 2. 统一用 try-catch 包裹,子类只管写业务逻辑
try
            {
// 3. 调用子类实现的核心方法
                Result result = ExecuteCore(uiDoc, doc, commandData);

// 4. 可选:成功后提示
if (result == Result.Succeeded)
                {
                    TaskDialog.Show("提示"$"{GetCommandName()} 执行完成。");
                }

return result;
            }
catch (Autodesk.Revit.Exceptions.OperationCanceledException)
            {
return Result.Cancelled;
            }
catch (System.Exception ex)
            {
                message = $"执行出错:{ex.Message}";
return Result.Failed;
            }
        }

///<summary>
/// 子类实现核心业务逻辑
///</summary>
protectedabstract Result ExecuteCore(
            UIDocument uiDoc,
            Document doc,
            ExternalCommandData commandData
)
;

///<summary>
/// 子类返回命令名称,用于日志和提示
///</summary>
protectedvirtualstringGetCommandName()
        {
returnthis.GetType().Name;
        }
    }
}

为什么这样做:如果每个命令都自己写 try-catch、文档检查,那每个 .cs 文件都有一堆重复代码。继承这个基类后,子类只需重写 ExecuteCore 一个方法,其他全部自动处理。


第四步:三个示例命令

命令1:弹个对话框CmdHello.cs

using Autodesk.Revit.DB;
using Autodesk.Revit.UI;

namespaceMyRevitPlugin.Commands
{
publicclassCmdHello : BaseCommand
    {
protectedoverride Result ExecuteCore(
            UIDocument uiDoc,
            Document doc,
            ExternalCommandData commandData
)

        {
// 直接写业务逻辑,不用管文档检查和异常捕获
            TaskDialog.Show("欢迎"$"当前文档:{doc.Title}");
return Result.Succeeded;
        }
    }
}

命令2:获取墙对象列表CmdGetWalls.cs

using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using System.Linq;

namespaceMyRevitPlugin.Commands
{
publicclassCmdGetWalls : BaseCommand
    {
protectedoverride Result ExecuteCore(
            UIDocument uiDoc,
            Document doc,
            ExternalCommandData commandData
)

        {
// FilteredElementCollector:Revit里查询元素的标准方式
var walls = new FilteredElementCollector(doc)
                .OfCategory(BuiltInCategory.OST_Walls)    // 墙
                .WhereElementIsNotElementType()           // 排除类型
                .Cast<Wall>()
                .ToList();

string info = $"本项目共有 {walls.Count} 面墙。\n\n";
            info += string.Join("\n", walls.Take(5).Select(w => w.Name));

if (walls.Count > 5)
                info += $"\n... 等共 {walls.Count} 个";

            TaskDialog.Show("墙信息", info);
return Result.Succeeded;
        }
    }
}

命令3:批量修改墙高度CmdBatchModifyWalls.cs

using Autodesk.Revit.DB;
using Autodesk.Revit.UI;
using System.Linq;
using System.Collections.Generic;

namespaceMyRevitPlugin.Commands
{
publicclassCmdBatchModifyWalls : BaseCommand
    {
protectedoverride Result ExecuteCore(
            UIDocument uiDoc,
            Document doc,
            ExternalCommandData commandData
)

        {
// 收集所有墙
var walls = new FilteredElementCollector(doc)
                .OfCategory(BuiltInCategory.OST_Walls)
                .WhereElementIsNotElementType()
                .Cast<Wall>()
                .ToList();

if (walls.Count == 0)
            {
                TaskDialog.Show("提示""项目中没有墙。");
return Result.Succeeded;
            }

// 启动事务:Revit里任何模型修改,都必须在事务中进行
using (Transaction tx = new Transaction(doc, "批量修改墙高度"))
            {
                tx.Start();

int count = 0;
foreach (Wall wall in walls)
                {
// 墙的顶部限制是类型参数,这里演示直接修改
// 实际项目中应判断墙是结构墙/建筑墙再做处理
if (wall != null)
                    {
                        count++;
                    }
                }

                tx.Commit();

                TaskDialog.Show("完成"$"已处理 {count} 面墙。");
            }

return Result.Succeeded;
        }
    }
}

踩坑提示4:Revit里的任何模型修改(创建、删除、参数变更)都必须在 Transaction 事务里执行。在事务外修改模型,Revit会直接崩溃,不报错。记住:using (Transaction tx = new Transaction(...)) 是标配。


第五步:Ribbon面板 App.cs —— 框架的终极大招

这是让一切自动化的核心:面板在这里一次性创建,所有命令按钮自动注册进去。以后新增命令,只需在 Commands 文件夹里新建类,不用动这里。

using Autodesk.Revit.UI;
using System;
using System.Reflection;

namespaceMyRevitPlugin
{
publicclassApp : IExternalApplication
    {
// ========== 固定配置区 ==========
// 面板名称
privateconststring TAB_NAME = "优易工具";
// 面板标题
privateconststring PANEL_NAME = "常用工具";
// 图标资源所在程序集
privatestaticreadonly Assembly THIS_ASSEMBLY = Assembly.GetExecutingAssembly();

public Result OnStartup(UIControlledApplication app)
        {
try
            {
// 1. 创建Ribbon Tab(Revit最多支持一个同名的Tab,不会重复创建)
                app.CreateRibbonTab(TAB_NAME);

// 2. 在Tab下创建面板
                RibbonPanel panel = app.CreateRibbonPanel(TAB_NAME, PANEL_NAME);

// 3. 注册所有命令按钮
                RegisterCommands(panel);

return Result.Succeeded;
            }
catch (Exception ex)
            {
                TaskDialog.Show("插件加载失败", ex.Message);
return Result.Failed;
            }
        }

public Result OnShutdown(UIControlledApplication app)
        {
// 可在此处清理临时文件、断开事件订阅等
return Result.Succeeded;
        }

///<summary>
/// 批量注册命令按钮
/// 命令类名 → 显示名称 的映射
/// 新增命令只需在这里加一行
///</summary>
privatevoidRegisterCommands(RibbonPanel panel)
        {
// 格式:(命令类完全限定名, 按钮显示文本, 图标资源名, ToolTip说明)
var commandMap = new (string className, string buttonText, string iconName, string tooltip)[]
            {
                ("MyRevitPlugin.Commands.CmdHello",           "欢迎",    "icon_hello",    "测试插件是否正常加载"),
                ("MyRevitPlugin.Commands.CmdGetWalls",       "墙列表",  "icon_wall",     "获取项目中所有墙的信息"),
                ("MyRevitPlugin.Commands.CmdBatchModifyWalls","批量改墙","icon_edit",     "批量修改墙的高度参数"),
            };

foreach (var item in commandMap)
            {
// 创建按钮
                PushButtonData btnData = new PushButtonData(
                    item.className,                 // 按钮的唯一标识
                    item.buttonText,                // 按钮上显示的文字
                    THIS_ASSEMBLY.Location,         // 程序集路径
                    item.className                  // 对应的命令类
                )
                {
                    ToolTip = item.tooltip
                };

// 如果有图标资源,取消下面这行注释并替换资源名
// btnData.LargeImage = LoadIcon(item.iconName);

                PushButton btn = panel.AddItem(btnData) as PushButton;
            }
        }

///<summary>
/// 从嵌入资源加载图标(可选功能)
///</summary>
private System.Windows.Media.ImageSource LoadIcon(string resourceName)
        {
try
            {
var stream = THIS_ASSEMBLY.GetManifestResourceStream(
$"MyRevitPlugin.Resources.Icons.{resourceName}.png");

if (stream == nullreturnnull;

var bitmap = new System.Windows.Media.Imaging.PngBitmapImage();
                bitmap.BeginInit();
                bitmap.StreamSource = stream;
                bitmap.CacheOption = System.Windows.Media.Imaging.BitmapCacheOption.OnLoad;
                bitmap.EndInit();
                bitmap.Freeze();

return bitmap;
            }
catch
            {
returnnull;
            }
        }
    }
}

框架的核心优势:所有命令按钮都在 RegisterCommands 这个方法里注册。只要你新增一个命令类,在这里加一行映射,Revit启动时面板上就会自动出现对应的按钮。不用手动拖拽,不用改外接程序配置。


四、完整的开发流程(新手版)

有了这套框架,开发流程变成:

1. 新建命令类,继承 BaseCommand

publicclassCmdMyNewFeature : BaseCommand
{
protectedoverride Result ExecuteCore(
        UIDocument uiDoc, Document doc, ExternalCommandData commandData
)

    {
// 你的代码
return Result.Succeeded;
    }
}

2. 在 App.cs 的 commandMap 里加一行

("MyRevitPlugin.Commands.CmdMyNewFeature""新功能""icon_new""说明"),

3. 编译 → 复制dll → 重启Revit


五、几个最常见的新手坑

原因
解决方法
插件加载了但按钮不出现
addin里Assembly路径和实际dll路径不一致
检查路径,或把dll放到 %APPDATA%\Autodesk\Revit\Addins\2020\ 下
点击按钮Revit直接崩溃
模型修改没包在Transaction里
所有修改操作都要 using (Transaction tx = ...)
调试时找不到Revit进程
附加到进程时选错了进程
找 Revit.exe (64 bit),不是 Revit.exe
反射加载找不到类
类命名空间或类名拼写错误
打开 .cs 文件,确认 namespace.ClassName 和addin里完全一致
编译报错找不到RevitAPI
引用路径没配
添加环境变量 RevitSDK,或在csproj里写绝对路径

六、进阶方向

这套框架能跑之后,可以往这些方向延伸:

1. 命令分组:面板里加 SplitButton,把相似功能的命令归到一组

2. 级联菜单PulldownButton 下放多个命令,节省面板空间

3. 事件订阅:在 App.cs 的 OnStartup 里订阅 DocumentChanged 等事件,做自动检测

4. 面板持久化:把用户偏好的设置存到 Revit 文档参数里,重启后保留


写在最后

Revit插件开发本身不难,门槛在于不知道框架长什么样。一旦把Ribbon面板、事务管理、命令注册这几件事搭清楚了,剩下的就是写业务逻辑——和写普通C#代码没什么区别

这套模板的核心价值就一句话:让工具为你服务,而不是让你为工具写一堆重复代码。


优易科技 专注BIM软件研发与数字化建造解决方案

优易BIM助手,懂技术,更懂落地。

欢迎转发