用户界面与 Ribbon
Revit 插件通过 Ribbon 面板和内置对话框与用户交互。本章涵盖 Ribbon 按钮、选择交互、TaskDialog 和 WPF 集成。
Ribbon 面板注册
在 IExternalApplication.OnStartup 中注册:
public Result OnStartup(UIControlledApplication uiApp)
{
string tabName = "我的工具";
// 创建选项卡(如果已存在则复用)
try
{
uiApp.CreateRibbonTab(tabName);
}
catch (Autodesk.Revit.Exceptions.ArgumentException)
{
// 选项卡已存在
}
// 创建面板
RibbonPanel panel = uiApp.CreateRibbonPanel(tabName, "基本操作");
// 创建按钮
PushButtonData btnData1 = new PushButtonData(
"CmdHello", // 内部名称(唯一标识)
"Hello", // 按钮显示文本
dllPath, // 当前程序集路径
"MyAddin.HelloCommand" // 完整类名
);
btnData1.ToolTip = "向 Revit 问好";
btnData1.LargeImage = GetImage("hello_32x32.png");
PushButton button1 = panel.AddItem(btnData1) as PushButton;
return Result.Succeeded;
}按钮图片
// 从嵌入资源加载图片
private System.Windows.Media.ImageSource GetImage(string resourceName)
{
Assembly assembly = Assembly.GetExecutingAssembly();
using (Stream stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream == null) return null;
// Revit 要求 ImageSource,需要转换
System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(stream);
// ... 转换为 BitmapImage
}
}
// 推荐做法:使用 P/Invoke 或 ImageConverter
// 或使用 Revit 内置的图标(如 PushButtonData.Image = null 显示默认图标)Ribbon 控件类型
PushButton(普通按钮)
PushButtonData pbd = new PushButtonData("id", "显示文本", dllPath, className);
pbd.ToolTip = "工具提示";
pbd.LongDescription = "详细说明(鼠标悬停多行)";
pbd.LargeImage = image;
pbd.Image = smallImage;
pbd.AvailabilityClassName = "MyAddin.Availability"; // 可用性控制器
PushButton btn = panel.AddItem(pbd) as PushButton;
btn.Enabled = true;PullDownButton(下拉按钮)
PullDownButtonData pdbData = new PullDownButtonData("pulldown1", "工具集");
PullDownButton pullDown = panel.AddItem(pdbData) as PullDownButton;
// 添加子按钮
pullDown.AddPushButton(new PushButtonData("sub1", "子命令1", dllPath, "MyAddin.Cmd1"));
pullDown.AddPushButton(new PushButtonData("sub2", "子命令2", dllPath, "MyAddin.Cmd2"));
pullDown.AddSeparator();
pullDown.AddPushButton(new PushButtonData("sub3", "子命令3", dllPath, "MyAddin.Cmd3"));SplitButton(分离按钮)
SplitButtonData sbData = new SplitButtonData("split1", "分离按钮");
SplitButton splitBtn = panel.AddItem(sbData) as SplitButton;
splitBtn.AddPushButton(new PushButtonData("default", "默认操作", dllPath, "MyAddin.Default"));
splitBtn.AddPushButton(new PushButtonData("opt1", "选项1", dllPath, "MyAddin.Opt1"));RadioButtonGroup(单选按钮组)
RadioButtonGroupData rbgData = new RadioButtonGroupData("mode");
RadioButtonGroup rbg = panel.AddItem(rbgData) as RadioButtonGroup;
ToggleButtonData mode1 = new ToggleButtonData("mode_draft", "草图", dllPath, "MyAddin.DraftCmd");
ToggleButtonData mode2 = new ToggleButtonData("mode_detail", "详细", dllPath, "MyAddin.DetailCmd");
rbg.AddItem(mode1);
rbg.AddItem(mode2);TextBox(文本框输入)
TextBoxData tbData = new TextBoxData("prefix");
tbData.ToolTip = "输入前缀";
tbData.PromptText = "请输入...";
TextBox textBox = panel.AddItem(tbData) as TextBox;
textBox.Width = 120;
textBox.Value = "默认值";
// 在命令中获取文本框的值
string prefix = textBox.Value;按钮可用性控制
// 实现 IsCommandAvailable 来控制按钮何时可点击
public class MyAvailability : IExternalCommandAvailability
{
public bool IsCommandAvailable(UIApplication uiApp, CategorySet selectedCategories)
{
// 只在有活动文档时可用
if (uiApp.ActiveUIDocument == null)
return false;
Document doc = uiApp.ActiveUIDocument.Document;
return !doc.IsFamilyDocument; // 族文档中不可用
}
}
// 绑定到按钮
PushButtonData pbd = new PushButtonData(...);
pbd.AvailabilityClassName = "MyAddin.MyAvailability";选择与拾取
拾取对象
UIDocument uiDoc = commandData.Application.ActiveUIDocument;
// 拾取一个元素
Reference pickedRef = uiDoc.Selection.PickObject(
ObjectType.Element,
new MySelectionFilter(), // 自定义过滤器
"请选择一面墙" // 状态栏提示
);
Element element = uiDoc.Document.GetElement(pickedRef);
// 拾取一个点
XYZ point = uiDoc.Selection.PickPoint("请选择一个点");
// 拾取多个元素
IList<Reference> refs = uiDoc.Selection.PickObjects(
ObjectType.Element,
new MySelectionFilter(),
"请选择多个门"
);
// 用矩形框选择
IList<Reference> boxRefs = uiDoc.Selection.PickObjects(
ObjectType.Element,
new MySelectionFilter(),
"请框选元素",
new List<string>() // 之前选择的对象
);自定义选择过滤器
public class WallSelectionFilter : ISelectionFilter
{
public bool AllowElement(Element elem)
{
return elem is Wall;
}
public bool AllowReference(Reference reference, XYZ position)
{
return false; // 不允许选择引用(如面、边)
}
}
// 更复杂的过滤:只允许选中特定类型的门
public class DoorFilter : ISelectionFilter
{
public bool AllowElement(Element elem)
{
if (elem is FamilyInstance fi)
{
return fi.Category?.Id.IntegerValue == (int)BuiltInCategory.OST_Doors;
}
return false;
}
public bool AllowReference(Reference reference, XYZ position)
{
return false;
}
}获取当前选择集
// 获取用户在 Revit 界面中已选中的元素
ICollection<ElementId> selectedIds = uiDoc.Selection.GetElementIds();
if (selectedIds.Count == 0)
{
TaskDialog.Show("提示", "请先选择元素。");
return Result.Succeeded;
}
foreach (ElementId id in selectedIds)
{
Element elem = doc.GetElement(id);
// 处理选中元素
}
// 设置选择集(编程选择元素)
uiDoc.Selection.SetElementIds(new List<ElementId> { wall1.Id, wall2.Id });TaskDialog —— Revit 内置对话框
// 简单消息框
TaskDialog.Show("标题", "内容");
// 带按钮的对话框
TaskDialog dialog = new TaskDialog("确认")
{
MainInstruction = "是否删除选中的墙?", // 主文字(加粗大字)
MainContent = $"将删除 {count} 面墙,此操作不可撤销。", // 副文字
CommonButtons = TaskDialogCommonButtons.Yes | TaskDialogCommonButtons.No,
DefaultButton = TaskDialogResult.No,
TitleAutoPrefix = true, // 自动加插件名前缀
};
// 添加自定义按钮
dialog.AddCommandLink(
TaskDialogCommandLinkId.CommandLink1,
"删除并导出报告",
"删除后自动导出变更报告到 Excel 文件"
);
// 添加验证文本框
dialog.AddTextWithLabel("请输入'DELETE'确认:", "");
TaskDialogResult result = dialog.Show();
if (result == TaskDialogResult.Yes)
{
// 执行删除
}
// 展开详情区域
dialog.ExpandedContent = $"将删除以下墙体:\n{string.Join("\n", wallNames)}";WPF 集成
Revit 2019+ 支持在 Ribbon 面板中嵌入 WPF 控件,但更常见的是弹出自定义 WPF 窗口:
// 创建自定义 WPF 窗口
public partial class MyDialog : Window
{
public List<Wall> SelectedWalls { get; set; }
public MyDialog(List<Wall> walls)
{
InitializeComponent();
SelectedWalls = walls;
WallsListBox.ItemsSource = walls;
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
Close();
}
}
// 在 Revit 命令中显示
public Result Execute(ExternalCommandData commandData, ...)
{
List<Wall> walls = GetAllWalls(doc);
MyDialog dialog = new MyDialog(walls);
// 设置 Owner 为 Revit 主窗口(使对话框居中且模态)
dialog.WindowStartupLocation = WindowStartupLocation.CenterScreen;
bool? result = dialog.ShowDialog();
if (result == true)
{
// 用户点击了确定
ProcessWalls(dialog.SelectedWalls);
}
return Result.Succeeded;
}插件级设置与持久化
// 使用 Revit 内置的 ExtensibleStorage 存储插件数据
// 或使用简单的 Application-level 存储
public class AddinSettings
{
private static readonly string SettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"MyRevitAddin",
"settings.json"
);
public string LastExportPath { get; set; } = "";
public int DefaultFontSize { get; set; } = 12;
public static AddinSettings Load()
{
if (!File.Exists(SettingsPath)) return new AddinSettings();
string json = File.ReadAllText(SettingsPath);
return JsonSerializer.Deserialize<AddinSettings>(json) ?? new AddinSettings();
}
public void Save()
{
Directory.CreateDirectory(Path.GetDirectoryName(SettingsPath)!);
File.WriteAllText(SettingsPath,
JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }));
}
}状态栏与进度条
// 状态栏文字
uiApp.StatusBarText = "正在处理...";
// 进度条(Windows 内置控件)
using (ProgressDialog pd = new ProgressDialog("正在处理", "请稍候"))
{
pd.Show();
for (int i = 0; i < elements.Count; i++)
{
pd.SetFraction(i / (double)elements.Count);
if (pd.Cancel)
break;
ProcessElement(elements[i]);
}
}