Skip to content
C#
用户界面与 Ribbon

用户界面与 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]);
    }
}