using System.Collections; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Data; using Autodesk.Revit.DB; using EPPlus.Core.Extensions; using EPPlus.Core.Extensions.Attributes; using FuzzySharp; using Nice3point.Revit.Toolkit.External.Handlers; using Nice3point.Revit.Toolkit.Helpers; using OfficeOpenXml; namespace Szmedi.RvKits.MEPTools { public partial class FacilityInfoProcessViewModel : ObservableObject { private readonly ActionEventHandler handler; [ObservableProperty] public partial string SearchText { get; set; } partial void OnSearchTextChanged(string value) { FacilitiesCollectionView.Refresh(); } [ObservableProperty] public partial bool ContainsAllLines { get; set; } partial void OnContainsAllLinesChanged(bool value) { FacilitiesCollectionView.Refresh(); } [ObservableProperty] public partial List Items { get; set; } [ObservableProperty] public partial string SearchInstanceText { get; set; } partial void OnSearchInstanceTextChanged(string value) { InstancesCollectionView.Refresh(); } private List Families { get; set; } = []; public ICollectionView FacilitiesCollectionView { get; set; } public ICollectionView InstancesCollectionView { get; set; } public FacilityInfoProcessViewModel() { GlobalVariables.UIApplication?.ViewActivated += UIApplication_ViewActivated; handler = new ActionEventHandler(); } [RelayCommand] private void ProcessExcel(string filePath) { FileInfo fi = new(filePath); using ExcelPackage package = new(fi); AppDomain.CurrentDomain.AssemblyResolve += ResolveHelper.ResolveAssembly; try { Items = package.ToList(1, configuration => configuration.SkipCastingErrors()); } catch (EPPlus.Core.Extensions.Exceptions.ExcelValidationException ex) { MessageBox.Show("列名不存在或不匹配,请检查表头是否存在换行或多余文字。", ex.Message); } catch (Exception ex) { MessageBox.Show(ex.Message); } finally { AppDomain.CurrentDomain.AssemblyResolve -= ResolveHelper.ResolveAssembly; } } private void UIApplication_ViewActivated(object sender, Autodesk.Revit.UI.Events.ViewActivatedEventArgs e) { GetFamilies(); FuzzyMatch(); } //private Assembly CurrentDomainOnAssemblyResolve(object sender, ResolveEventArgs args) //{ // if (args.Name.Contains("ComponentModel.Annotations")) // { // var assemblyFile = Path.Combine(GlobalVariables.DirAssembly, "System.ComponentModel.Annotations.dll"); // return File.Exists(assemblyFile) ? Assembly.LoadFrom(assemblyFile) : null; // } // if (args.Name.Contains("EPPlus")) // { // var epplusFile = Path.Combine(GlobalVariables.DirAssembly, "EPPlus.dll"); // return File.Exists(epplusFile) ? Assembly.LoadFrom(epplusFile) : null; // } // return null; //} public List GetUsedFamilies(Document doc) { // 获取所有实例元素(这里不需要由ToElements()转化全部对象,节省内存,仅遍历) var instances = doc.OfClass(); // 3. 收集所有被使用的“类型ID” (使用HashSet自动去重) HashSet usedTypeIds = new HashSet(); foreach (Element elem in instances) { ElementId typeId = elem.GetTypeId(); if (typeId != ElementId.InvalidElementId) { usedTypeIds.Add(typeId); } } // 4. 从类型反向获取族 (使用HashSet再次去重族ID,防止多个类型属于同一个族) HashSet usedFamilyIds = new HashSet(); List resultFamilies = new List(); foreach (ElementId typeId in usedTypeIds) { // 获取类型元素 (FamilySymbol) Element typeElem = doc.GetElement(typeId); if (typeElem is FamilySymbol symbol) { Family family = symbol.Family; // 确保族存在且未被添加过 if (family != null && usedFamilyIds.Add(family.Id)) { resultFamilies.Add(family); } } } return resultFamilies; } private void GetFamilies() { var doc = GlobalVariables.UIApplication.ActiveUIDocument.Document; if (doc.IsFamilyDocument) { return; } var families = GetUsedFamilies(doc) .Where( fam => fam.FamilyCategory.AllowsBoundParameters && fam.IsEditable && fam.FamilyCategory.CategoryType == CategoryType.Model && !Enum.GetName(typeof(BuiltInCategory), fam.FamilyCategoryId.IntegerValue)!.Contains( "Fitting") && fam.FamilyCategoryId.IntegerValue != -2008013);//风道末端 Families = [.. families]; } [ObservableProperty] public partial string ExcelPath { get; set; } [RelayCommand] private void BrowserExcel() { var ofd = new Microsoft.Win32.OpenFileDialog { Filter = "Excel 文件 (*.xlsx;*.xls)|*.xlsx;*.xls", Title = "选择 Excel 文件" }; bool? result = ofd.ShowDialog(); if (result == true) { GetFamilies(); ExcelPath = ofd.FileName; ProcessExcel(ofd.FileName); if (Items == null) { return; } FuzzyMatch(); FacilitiesCollectionView = CollectionViewSource.GetDefaultView(Items); FacilitiesCollectionView.Filter = o => { if (string.IsNullOrEmpty(SearchText)) return true; if (o is FacilityItem info) { //var title = GlobalVariables.UIApplication.ActiveUIDocument.Document.Title; //var match = Regex.Match(title, @"(?<=^\d+号线-)([^-]+?)(?=车辆段|站|区间|停车场|主变电|变电所)"); //string result = match.Success ? match.Value : string.Empty; return info.ToString().Contains(SearchText) || (ContainsAllLines && info.ToString().Contains("全线")); } return false; }; } } private void FuzzyMatch() { if (Items == null || Families == null) { return; } foreach (var item in Items) { double minRatio = 30; foreach (var family in Families) { var famName = family.Name; var tempRatio = Fuzz.TokenSetRatio(famName, item.Name); if (tempRatio >= minRatio) { minRatio = tempRatio; item.MappedFamily = family; } } } } public bool? IsAllItemsSelected { get { var selected = Items?.Select(item => item.IsSelected).Distinct().ToList(); if (selected == null) { return false; } return selected.Count == 1 ? selected.Single() : null; } set { if (value.HasValue) { SelectAll(value.Value, Items); OnPropertyChanged(); } } } private static void SelectAll(bool select, IEnumerable models) { foreach (var model in models) { model.IsSelected = select; } } [NotifyCanExecuteChangedFor(nameof(SelectInstanceCommand))] [ObservableProperty] private partial bool CanSelecting { get; set; } = true; /// /// 提取字符串中所有中文字符 /// private static string ExtractChinese(string text) { if (string.IsNullOrEmpty(text)) return string.Empty; // 先将非中文替换为空格,再将连续空格替换为单个空格,最后去除首尾空格 var result = Regex.Replace(text, @"[^\u4e00-\u9fa5]", " "); result = Regex.Replace(result, @"\s+", " ").Trim(); return result; } [RelayCommand(CanExecute = nameof(CanSelecting))] private void SelectInstance(FacilityItem item) { CanSelecting = false; try { var uidoc = GlobalVariables.UIApplication.ActiveUIDocument; var reference = uidoc.Selection .PickObject( Autodesk.Revit.UI.Selection.ObjectType.Element, new FuncFilter( e => e is FamilyInstance instance && instance.Category != null && !instance.Symbol.Family.IsCurtainPanelFamily && instance.Symbol.Family.IsUserCreated && instance.Symbol.Family.IsEditable && !Enum.GetName(typeof(BuiltInCategory), instance.Category.Id.IntegerValue)!.Contains("Fitting") && instance.Category.CategoryType == CategoryType.Model && instance.Category.Id.IntegerValue != -2008013), "请选择关联的族实例"); var instance = uidoc.Document.GetElement(reference) as FamilyInstance; item.MappedFamily = instance.Symbol.Family; } catch (Autodesk.Revit.Exceptions.OperationCanceledException) { } finally { CanSelecting = true; } } public static string ValidateDuplicatesWithDisplay( IEnumerable items, Func keySelector, // 用于判断重复的键(如 ID) Func displaySelector // 用于显示的文本(如 Name 或组合) ) { if (items == null) return "数据为空。"; var keyToDisplay = new Dictionary(new ObjectKeyComparer()); var duplicates = new List(); foreach (var item in items) { if (item == null) continue; var key = keySelector(item); var display = displaySelector(item) ?? "(空)"; // 如果键已存在,说明重复,记录显示文本 if (keyToDisplay.TryGetValue(key, out var existingDisplay)) { // 避免重复添加相同项的提示(可选) if (!duplicates.Contains(display) && !duplicates.Contains(existingDisplay)) { duplicates.Add($"[{existingDisplay} 与 {display}]"); } } else { keyToDisplay[key] = display; } } if (duplicates.Count > 0) return $"发现重复的项,可能包含:{string.Join(";", duplicates)}"; return string.Empty; } // 辅助类:解决 object 作为 key 时的 null 和类型一致性问题 class ObjectKeyComparer : IEqualityComparer { public new bool Equals(object x, object y) { if (x == null && y == null) return true; if (x == null || y == null) return false; return x.Equals(y); } public int GetHashCode(object obj) => obj?.GetHashCode() ?? 0; } [RelayCommand] private void ProcessModelInfo() { var processItems = Items.Where(i => i.IsSelected && i.MappedFamily != null && i.Instances.Count > 0).ToList(); if (processItems.Count == 0) { MessageBox.Show("未选择任何有效的设备项进行处理。", "提示", MessageBoxButton.OK, MessageBoxImage.Information); return; } handler.Raise( uiapp => { var doc = uiapp.ActiveUIDocument.Document; if (doc.IsFamilyDocument) { return; } doc.InvokeGroup( _ => { StringBuilder sb = new StringBuilder(); var allInstances = Items.Where(i => i.IsSelected).SelectMany(i => i.Instances).ToList(); string message = ValidateDuplicatesWithDisplay(allInstances, s => s.Id, s => $"{s.Symbol.FamilyName}:{s.Name}"); if (!string.IsNullOrEmpty(message)) { MessageBox.Show(message, "错误", MessageBoxButton.OK, MessageBoxImage.Error); return; } foreach (var item in processItems) { var family = item.MappedFamily; if (!family.IsValidObject) { sb.AppendLine($"<{item.Name}>因文档手动修改导致匹配族失效,请重新匹配,跳过该族。"); continue; } if (!family.IsEditable) { sb.AppendLine($"{family.Name} 族不可编辑,跳过该族。"); continue; } Document famdoc = null; try { famdoc = doc.EditFamily(family); famdoc.Invoke( _ => { foreach (var paramItem in item.Parameters) { try { var familyParameters = famdoc.FamilyManager.GetParameters() .Where(p => p.Definition.Name == paramItem.Name).ToList(); if (familyParameters.Count > 0) { var parameter = familyParameters.FirstOrDefault(); if (parameter.Definition is InternalDefinition def && def.BuiltInParameter == BuiltInParameter.INVALID) { famdoc.FamilyManager.RemoveParameter(parameter); famdoc.Regenerate(); } famdoc.FamilyManager.AddParameter(paramItem.Name, BuiltInParameterGroup.PG_TEXT, ParameterType.Text, true); } else { famdoc.FamilyManager .AddParameter( paramItem.Name, BuiltInParameterGroup.PG_TEXT, ParameterType.Text, true); } } catch (Exception ex) { sb.AppendLine($"{family.Name} 族参数 {paramItem.Name} 添加失败,{ex.Message}"); } } }, "添加缺失的族参数属性条目"); item.MappedFamily = famdoc.LoadFamily(doc, new FamilyLoadOption()); } catch (Exception ex) { sb.AppendLine($"{family.Name} 族加载到项目中失败,原因:{ex.Message}"); } finally { famdoc?.Close(false); } doc.Invoke( ts => { foreach (var instance in item.Instances) { foreach (var paramItem in item.Parameters) { Parameter param = instance.GetParameters(paramItem.Name)?.FirstOrDefault(); if (param is { IsReadOnly: false }) { param.Set(paramItem.Value); } else { sb.AppendLine($"{instance.Id} - {instance.Name} 未找到参数 {paramItem.Name} 或参数只读,未设置其值。"); } } } }); } MessageBox.Show(sb.Length > 0 ? sb.ToString() : "添加完成!"); }, "处理设备信息"); }); } [ObservableProperty] public partial FacilityItem SelectedFacility { get; set; } partial void OnSelectedFacilityChanged(FacilityItem value) { if (value != null) { InstancesCollectionView = CollectionViewSource.GetDefaultView(value.Instances); InstancesCollectionView.Filter = o => { if (string.IsNullOrEmpty(SearchInstanceText)) return true; if (o is FamilyInstance ins) { return ins.Name.Contains(SearchInstanceText); } return false; }; } } [RelayCommand] private void LocationInstance(FamilyInstance instance) { var uidoc = GlobalVariables.UIApplication.ActiveUIDocument; if (uidoc.Document.GetElement(instance.Id).IsValidObject) { uidoc.Selection.SetElementIds([instance.Id]); uidoc.ShowElements(instance); } } [RelayCommand] private void RemoveInstance(FamilyInstance instance) { SelectedFacility?.Instances.Remove(instance); } [RelayCommand] private void RemoveSelectedInstances(IList list) { if (SelectedFacility?.Instances == null || list == null) return; //先找出所有需要移除的项(不修改原集合) var toRemove = new List(); foreach (var item in list) { if (item is FamilyInstance instance && SelectedFacility.Instances.Contains(instance)) { toRemove.Add(instance); } } foreach (var instance in toRemove) { SelectedFacility.Instances.Remove(instance); } } [RelayCommand] private void SelectSelectedInstances(IList list) { if (list != null && list.Count > 0) { var candidates = new HashSet( list.Cast().Where(x => x is FamilyInstance).Cast()); var uidoc = GlobalVariables.UIApplication.ActiveUIDocument; uidoc.Selection.SetElementIds(new HashSet(candidates.Select(i => i.Id))); } } [RelayCommand] private void SelectAllInstances() { if (SelectedFacility?.Instances.Count > 0) { var uidoc = GlobalVariables.UIApplication.ActiveUIDocument; uidoc.Selection.SetElementIds(new HashSet(SelectedFacility.Instances .Select(i => i.Id))); } } [RelayCommand] private void RemoveAllInstances() { SelectedFacility?.Instances.Clear(); } [RelayCommand] private void RepickSamplingInstances(Family family) { if (family == null || !family.IsValidObject) { return; } var uidoc = GlobalVariables.UIApplication.ActiveUIDocument; try { while (true) { var excludedIds = new HashSet(SelectedFacility.Instances.Select(i => i.Id.IntegerValue)); var reference = uidoc.Selection .PickObject( Autodesk.Revit.UI.Selection.ObjectType.Element, new FuncFilter( e => e is FamilyInstance fi && fi.Symbol.Family.Id.IntegerValue == family.Id.IntegerValue && !excludedIds.Contains(e.Id.IntegerValue)), "请选择关联的族实例"); var instance = uidoc.Document.GetElement(reference) as FamilyInstance; SelectedFacility?.Instances.Add(instance); } } catch (Autodesk.Revit.Exceptions.OperationCanceledException) { } } [RelayCommand] private void RepickInstancesByRectangle(Family family) { if (family == null || !family.IsValidObject) { return; } var uidoc = GlobalVariables.UIApplication.ActiveUIDocument; try { var excludedIds = new HashSet(SelectedFacility.Instances.Select(i => i.Id.IntegerValue)); var instances = uidoc.Selection .PickElementsByRectangle( new FuncFilter( e => e is FamilyInstance fi && fi.Symbol.Family.Id.IntegerValue == family.Id.IntegerValue && !excludedIds.Contains(e.Id.IntegerValue)), "请选择关联的族实例").Cast(); foreach (var item in instances) { SelectedFacility?.Instances.Add(item); } } catch (Autodesk.Revit.Exceptions.OperationCanceledException) { } } ~FacilityInfoProcessViewModel() { if (GlobalVariables.UIApplication.IsValidObject) { GlobalVariables.UIApplication.ViewActivated -= UIApplication_ViewActivated; } } } public partial class ParameterItem : ObservableObject { public ParameterItem(string name, string value = "") { Name = name; Value = value; } public ParameterItem() { } [ObservableProperty] public partial string Name { get; set; } [ObservableProperty] public partial string Value { get; set; } } /// /// Excel表里的设备项 /// public partial class FacilityItem : ObservableObject { public FacilityItem() { if (!string.IsNullOrEmpty(ParameterString)) { Parameters = ProcessParameters(ParameterString); } } [ObservableProperty] public partial bool IsSelected { get; set; } [ExcelTableColumn("设备")] public string Name { get; set; } [ExcelTableColumn("部署范围")] public string Stations { get; set; } [ExcelTableColumn("设备参数")] [ObservableProperty] public partial string ParameterString { get; set; } partial void OnParameterStringChanged(string value) { if (!string.IsNullOrEmpty(value)) { Parameters = ProcessParameters(value); } } [ObservableProperty] public partial string SearchText { get; set; } public ICollectionView CollectionView { get; private set; } public ObservableCollection Instances { get; set; } = []; public string Brand { get; set; } public string Specification { get; set; } [ObservableProperty] [NotifyPropertyChangedFor(nameof(DisplayText))] public partial Family MappedFamily { get; set; } = null; partial void OnMappedFamilyChanged(Family value) { var doc = value.Document; Instances.Clear(); List instances = [..doc.ActiveView.QueryInstancesByType() .Cast() .Where(fi => fi.Symbol.Family.Id.IntegerValue == value.Id.IntegerValue)]; foreach (var instance in instances) { Instances.Add(instance); } CollectionView = CollectionViewSource.GetDefaultView(Instances); CollectionView.Filter = o => { if (string.IsNullOrEmpty(SearchText)) return true; if (o is FamilyInstance ins) { //var title = GlobalVariables.UIApplication.ActiveUIDocument.Document.Title; //var match = Regex.Match(title, @"(?<=^\d+号线-)([^-]+?)(?=车辆段|站|区间|停车场|主变电|变电所)"); //string result = match.Success ? match.Value : string.Empty; return ins.Name.Contains(SearchText); } return false; }; } public string DisplayText => MappedFamily switch { null => "未匹配", _ => $"{MappedFamily?.Name ?? "未命名族"}" }; [ObservableProperty] public partial string ErrorMessage { get; set; } [ObservableProperty] public partial List Parameters { get; set; } private List ProcessParameters(string input) { List parameters = []; //string pattern = @"^([^:]+):(.*)$"; string pattern = @"^([^::]+)[::](.*)$"; MatchCollection matches = Regex.Matches(input, pattern, RegexOptions.Multiline); foreach (Match match in matches) { // Group[1] 是冒号前的内容 (参数名) string paramName = match.Groups[1].Value.Trim(); if (string.IsNullOrEmpty(paramName)) { continue; } string paramValue = match.Groups[2].Value; if (string.IsNullOrEmpty(paramValue)) { continue; } if (paramName == "品牌") { Brand = paramValue; } if (paramName == "设备型号") { Specification = paramValue; } // Group[2] 是冒号后的所有内容 (参数值) var parameterItem = new ParameterItem { Name = paramName, Value = paramValue }; var isExist = parameters.Any(p => p.Name == paramName); if (isExist) { ErrorMessage += $"{paramName}参数名重复\n"; } else { parameters.Add(parameterItem); } } return parameters; } public override string ToString() { return $"{Name} {Brand} {Specification} {Stations}"; } } }