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

从零搭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路径一致。建议用绝对路径,编译完直接复制到那个目录。
★
踩坑提示2:
ClientId用 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 == null) returnnull;
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
五、几个最常见的新手坑
|
|
|
|
|---|---|---|
|
|
|
%APPDATA%\Autodesk\Revit\Addins\2020\ 下 |
|
|
|
using (Transaction tx = ...) |
|
|
|
Revit.exe (64 bit),不是 Revit.exe |
|
|
|
.cs 文件,确认 namespace.ClassName 和addin里完全一致 |
|
|
|
RevitSDK,或在csproj里写绝对路径 |
六、进阶方向
这套框架能跑之后,可以往这些方向延伸:
1. 命令分组:面板里加 SplitButton,把相似功能的命令归到一组
2. 级联菜单:PulldownButton 下放多个命令,节省面板空间
3. 事件订阅:在 App.cs 的 OnStartup 里订阅 DocumentChanged 等事件,做自动检测
4. 面板持久化:把用户偏好的设置存到 Revit 文档参数里,重启后保留
写在最后
Revit插件开发本身不难,门槛在于不知道框架长什么样。一旦把Ribbon面板、事务管理、命令注册这几件事搭清楚了,剩下的就是写业务逻辑——和写普通C#代码没什么区别。
这套模板的核心价值就一句话:让工具为你服务,而不是让你为工具写一堆重复代码。
优易科技 专注BIM软件研发与数字化建造解决方案
优易BIM助手,懂技术,更懂落地。
欢迎转发
夜雨聆风