一个常见的Revit模型至少包含上千个元素,稍微复杂一些的都是上万甚至十万级别的元素。要在这样的模型中查找某一个元素会非常费时费力,Revit本身不提供基于类似名称、类型等过滤条件的搜索。本小节利用Revit API基于元素类型、名称等条件在全模型文档中查找并显示元素。
该样例的功能如下:
支持通过元素类别过滤查找
支持通过元素名称过滤查找
支持显示查找结果前50个元素
搜索结果显示元素ID及元素名称
支持显示单个元素(受API限制,不能保证每一个都成功显示)
如果希望更友好地显示过滤结果,例如分页等,因为不涉及到Revit功能,读者可以自行尝试。
由于我们需要支持选中元素后显示该元素,搜索对话框需要是非模态的,而对于非模态对话框,窗口加载完成后就已经不在Revit API有效上下文内了,这时如果调用RevitAPI,会导致异常使得程序崩溃。Revit API提供了UIApplication.Idling事件来处理这样的需求,为了响应Idling事件,我们需要实现IExternalApplication接口并在启动时注册该事件,在Revit关闭时注销该事件。需要注意的是,如果希望能在程序运行过程中动态根据情况注册/注销Idling事件,比较好的方法是定义一个静态的EventHandler,将该静态的对象注册,程序中其它地方的注册都注册到该静态EventHandler(参见Application类中的IdlingHandlers对象)。
实现接口的Application类代码如下:
public class Application : IExternalApplication
{
public static event EventHandler IdlingHandlers;
public Result OnStartup(UIControlledApplication application) {
// Create Ribbon Tab, panel and button ... ... application.Idling += OnIdling;
return Result.Succeeded; }
private void OnIdling(object sender, IdlingEventArgs e) {
if (IdlingHandlers != null) { IdlingHandlers(sender, e); } }
public Result OnShutdown(UIControlledApplication application) {
if (Command.searchDialog != null) { Command.searchDialog.Close(); } application.Idling -= OnIdling;
return Result.Succeeded; }}
其中searchDialog是Command类的静态对象,目的是为了避免重复创建搜索对话框。Command类代码如下:
public class Command : IExternalCommand
{
public static FormSearch searchDialog = null;
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) {
if (searchDialog == null) { searchDialog = new FormSearch(commandData.Application.ActiveUIDocument); searchDialog.Show(); }
return Result.Succeeded; }}
这里的搜索对话框如下图所示:
对于窗口的数据,微软提供的WPF可以很方便的进行数据绑定展示,由于制作Revit插件经常会遇到类似比较复杂的界面,学会如何利用WPF进行数据绑定展示十分重要。这里我们以本实例为基础大致说明一下数据绑定处理的方法,更多的内容请参考微软的开发文档。
要完成WPF界面数据绑定,需要根据窗口要展示的数据定义一个数据类,该数据类必须实现INotifyPropertyChanged接口,并在修改需要绑定的数据时,主动触发PropertyChanged事件。为了避免在不同的数据类中都重复同样的代码,建议用一个数据基类,如下所示,所有数据类可以直接从该数据基类继承。
public class NotifyObjectBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName) {
if (this.PropertyChanged != null) {
this.PropertyChanged.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }}
对于界面需要展示的数据,我们用下面的数据类来定义:
public class ElementInfo : NotifyObjectBase
{
#region Member variables private int _elementId;
private string _name;
#endregion public ElementInfo() { Id = -1; Name = ''; }
public ElementInfo(Element element) { Id = element.Id.IntegerValue; Name = element.Name; }
public int Id {
get {
return _elementId; }
set { _elementId = value; OnPropertyChanged('Id'); } }
public string Name {
get {
return _name; }
set { _name = value; OnPropertyChanged('Name'); } }}
public class SearchData : NotifyObjectBase
{
#region Member variables private ObservableCollection _elements;
private ObservableCollection _categories;
private ElementInfo _selectElement;
private ElementInfo _selectCategory;
#endregion public ObservableCollection Elements {
get {
return _elements; }
set { _elements = value; OnPropertyChanged('Elements'); } }
public ObservableCollection Categories {
get {
return _categories; }
set { _categories = value; OnPropertyChanged('Categories'); } }
public ElementInfo SelectElement {
get {
return _selectElement; }
set { _selectElement = value; OnPropertyChanged('SelectElement'); } }
public ElementInfo SelectCategory {
get {
return _selectCategory; }
set { _selectCategory = value; OnPropertyChanged('SelectCategory'); } }
public SearchData() { Elements = new ObservableCollection(); Categories = new ObservableCollection();
// add and select default one : all ElementInfo info = new ElementInfo(); info.Name = 'All'; Categories.Add(info); SelectCategory = info; }
public void Clear() { Elements.Clear(); }
public void AddElement(Element element) { Elements.Add(new ElementInfo(element)); }
public void AddCategory(Category category) { ElementInfo info = new ElementInfo(); info.Id = category.Id.IntegerValue; info.Name = category.Name; Categories.Add(info); }}
注意在所有属性的设置部分,都需要调用OnPropertyChanged,参数与该属性的名称一致。其次,对于数据集(比如该界面用到的结果,以及类别清单),必须使用ObservableCollection,否则添加/删除元素时界面不能正确更新。
界面代码分两部分,一部分是界面xaml定义,另一部分是响应代码。界面定义如下:
以类别下拉列表数据绑定为例,ItemsSource='{BindingPath=Categories}'表示将数据对象的Categories属性绑定到该下拉列表中,SelectedItem='{Binding Path=SelectCategory}'表示将数据对象的SelectCategory绑定到当前选中的对象,默认情况下绑定是双向的,如果需要指定单向,可以这样修改{Binding Path=SelectCategory,Mode=OneWay}。
界面响应代码如下:
public partial class FormSearch : Window
{
private UIDocument _uiDoc;
private SearchData _data;
private bool _searchClicked, _showClicked;
private const int MaxCount = 50;
public FormSearch(UIDocument uiDoc) { InitializeComponent(); _uiDoc = uiDoc; _data = new SearchData(); _searchClicked = _showClicked = false; }
private void Window_Loaded(object sender, RoutedEventArgs e) { Application.IdlingHandlers += OnIdling; DataContext = _data;
// Initial all categories foreach (Category cat in _uiDoc.Document.Settings.Categories) { _data.AddCategory(cat); } }
private void OnIdling(object sender, IdlingEventArgs e) {
if (_searchClicked) { _searchClicked = false; SearchElements(); }
else if (_showClicked) { _showClicked = false;
if (_data.SelectElement != null) { _uiDoc.ShowElements(new ElementId(_data.SelectElement.Id)); } } }
private void SearchElements() { _data.Clear();
string name = tbName.Text; Document doc = _uiDoc.Document; FilteredElementCollector collector = new FilteredElementCollector(doc).WhereElementIsNotElementType();
if (_data.SelectCategory != null && _data.SelectCategory.Id != -1) { collector = collector.OfCategoryId(new ElementId(_data.SelectCategory.Id)); }
var elements = collector.Where(x => x.Name.Contains(name)).Take(MaxCount);
foreach (var element in elements) { _data.AddElement(element); } }
private void Search_Click(object sender, RoutedEventArgs e) {
if (string.IsNullOrEmpty(tbName.Text)) { MessageBox.Show('Please input name filter text!'); tbName.Focus();
return; } _searchClicked = true; }
private void Show_Click(object sender, RoutedEventArgs e) {
if (_searchClicked || _data.SelectElement == null)
return; _showClicked = true; }
private void tbName_KeyUp(object sender, KeyEventArgs e) {
if (e.Key == Key.Enter) { Search_Click(sender, e); } }
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) { Application.IdlingHandlers -= OnIdling; Command.searchDialog = null; }}
注意一下这里OnIdling的注册/注销方法,所有与Revit相关的代码需要放到OnIdling函数中。其次在Window_Loaded函数中(该函数在界面加载时会被调用)需要绑定数据对象与整个界面,通过DataContext = _data;语句。
本小节主要包含了以下一些知识点:
Revit API调用需要在正确的上下文内,超出该范围的调用会引起程序崩溃
在非模态对话框中,可以通过响应Idling事件来获取Revit API上下文
Idling事件需要在IExternalApplication的OnStartup中注册,在OnShutdown中注销
界面数据绑定可以利用WPF的数据绑定功能
需要绑定的数据类都必须实现INotifyPropertyChanged接口
联系客服