using Laservall.Solidworks.Extension; using Laservall.Solidworks.Model; using Laservall.Solidworks.Windows.ViewModel; using SolidWorks.Interop.sldworks; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; using System.IO; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; namespace Laservall.Solidworks.Windows { public class LevelToMarginConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is int level) { return new Thickness(level * 20, 0, 0, 0); } return new Thickness(0); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } public class ExpandIconConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is bool expanded) { return expanded ? "▼" : "▶"; } return "▶"; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } public partial class ItemsPropWindow : System.Windows.Window { private List allBomItems = new List(); private HashSet dirtyDocPaths = new HashSet(); private BomSettings currentSettings; private static readonly HashSet StaticColumnProps = new HashSet { "零件名称", "材质", "属性", "材料" }; private static readonly Dictionary PropNameToModelField = new Dictionary { { "PartName", "零件名称" }, { "MaterialProp", "材料" }, { "Material", "材质" }, { "Classification", "属性" } }; private static readonly Dictionary BuiltInColumnNameMap = new Dictionary { { "层级", "LevelColumn" }, { "图号", "DrawingNoColumn" }, { "零件名称", "ConfigNameColumn" }, { "属性", "ClassificationColumn" }, { "材料", "MaterialPropColumn" }, { "材质", "MaterialColumn" }, { "数量", "QuantityColumn" }, { "装配体", "IsAssemblyColumn" }, { "外购件", "IsOutSourcingColumn" } }; public ItemsPropWindow() { InitializeComponent(); if (DataContext == null) { DataContext = new ItemsViewModel(); } currentSettings = BomSettings.Load(); } private async void Window_Loaded(object sender, RoutedEventArgs e) { if (!(DataContext is ItemsViewModel viewModel)) { return; } viewModel.IsLoading = true; viewModel.LoadingProgress = 0; viewModel.LoadingProgressMaximum = 1; viewModel.LoadingText = "正在读取数据..."; try { await System.Threading.Tasks.Task.Yield(); ModelDoc2 model = SWUtils.SwApp.ActiveDoc as ModelDoc2; if (model == null) { viewModel.BomItems = new ObservableCollection(); return; } Configuration config = model.ConfigurationManager.ActiveConfiguration; var allParts = new SwAssDocTreeModel() { drawingNo = Path.GetFileNameWithoutExtension(model.GetPathName()), docPath = model.GetPathName(), configName = config?.Name ?? "", children = new List(), isAss = true, quantity = 1, props = SWUtils.GetConfig3(model.GetPathName()) }; Component2 rootComponent = config?.GetRootComponent3(true) as Component2; int totalCount = GetTotalComponentCount(model); viewModel.LoadingProgressMaximum = totalCount; viewModel.LoadingText = "正在解析装配结构..."; SWUtils.SetAllPartResolved(model.GetPathName()); if (rootComponent != null) { var progress = new Progress(p => { viewModel.LoadingProgressMaximum = p.Total; viewModel.LoadingProgress = p.Processed; viewModel.LoadingText = string.IsNullOrWhiteSpace(p.CurrentName) ? $"正在读取数据 ({p.Processed}/{p.Total})" : $"正在读取 {p.CurrentName} ({p.Processed}/{p.Total})"; }); var parts = await SWUtils.GetSwAssDocTreeChildrenAsync(rootComponent, true, true, totalCount, progress); allParts.children.AddRange(parts); } viewModel.LoadingText = "正在生成BOM..."; allBomItems = SWUtils.FlattenBomTree(allParts); var propKeys = SWUtils.CollectAllPropertyKeys(allBomItems); var dynamicKeys = propKeys .Where(k => !StaticColumnProps.Contains(k)) .OrderBy(k => k) .ToList(); currentSettings.MergeDynamicKeys(dynamicKeys); var userAddedKeys = currentSettings.Columns .Where(c => c.IsUserAdded && !dynamicKeys.Contains(c.Name)) .Select(c => c.Name) .ToList(); dynamicKeys.AddRange(userAddedKeys); EnsurePropsKeys(dynamicKeys); viewModel.AllPropertyKeys = dynamicKeys; AddDynamicColumns(dynamicKeys); ApplyColumnVisibility(); ApplyViewMode(viewModel); } catch (Exception ex) { System.Windows.MessageBox.Show(this, ex.Message, "读取数据失败", MessageBoxButton.OK, MessageBoxImage.Error); } finally { viewModel.IsLoading = false; } } private void AddDynamicColumns(List propKeys) { foreach (var key in propKeys) { var column = new DataGridTextColumn { Header = key, Width = new DataGridLength(100), Binding = new Binding($"Props[{key}]") { Mode = BindingMode.TwoWay, UpdateSourceTrigger = UpdateSourceTrigger.LostFocus } }; BomDataGrid.Columns.Add(column); } } private void EnsurePropsKeys(List keys) { foreach (var item in allBomItems) { if (item.Props == null) { item.Props = new Dictionary(); } foreach (var key in keys) { if (!item.Props.ContainsKey(key)) { item.Props[key] = ""; } } } } private void ApplyColumnVisibility() { if (currentSettings == null) { return; } foreach (var col in BomDataGrid.Columns) { string header = col.Header?.ToString(); if (string.IsNullOrEmpty(header)) { continue; } var config = currentSettings.GetColumn(header); if (config != null) { col.Visibility = config.IsVisible ? Visibility.Visible : Visibility.Collapsed; } } } private void ColumnConfig_Click(object sender, RoutedEventArgs e) { var configWindow = new ColumnConfigWindow(currentSettings); configWindow.Owner = this; if (configWindow.ShowDialog() == true && configWindow.ResultSettings != null) { currentSettings = configWindow.ResultSettings; currentSettings.Save(); if (DataContext is ItemsViewModel viewModel) { var existingDynamicHeaders = new HashSet(); var toRemove = new List(); foreach (var col in BomDataGrid.Columns) { string header = col.Header?.ToString(); if (!string.IsNullOrEmpty(header) && !BuiltInColumnNameMap.ContainsKey(header)) { existingDynamicHeaders.Add(header); toRemove.Add(col); } } foreach (var col in toRemove) { BomDataGrid.Columns.Remove(col); } var dynamicKeys = currentSettings.Columns .Where(c => !c.IsFixed) .Select(c => c.Name) .ToList(); EnsurePropsKeys(dynamicKeys); viewModel.AllPropertyKeys = dynamicKeys; AddDynamicColumns(dynamicKeys); } ApplyColumnVisibility(); } } private void ToggleExpand_Click(object sender, RoutedEventArgs e) { if (!(sender is System.Windows.Controls.Primitives.ToggleButton toggle)) { return; } if (!(toggle.DataContext is BomItemModel item)) { return; } UpdateChildrenVisibility(item); } private void UpdateChildrenVisibility(BomItemModel parentItem) { bool shouldShow = parentItem.IsExpanded; var descendants = GetDescendants(parentItem.NodeId); foreach (var child in descendants) { if (!shouldShow) { child.IsVisible = false; } else { child.IsVisible = IsAncestorChainExpanded(child); } } } private bool IsAncestorChainExpanded(BomItemModel item) { int parentId = item.ParentNodeId; while (parentId >= 0) { var parent = allBomItems.FirstOrDefault(b => b.NodeId == parentId); if (parent == null) { break; } if (!parent.IsExpanded) { return false; } parentId = parent.ParentNodeId; } return true; } private List GetDescendants(int parentNodeId) { var result = new List(); var directChildren = allBomItems.Where(b => b.ParentNodeId == parentNodeId).ToList(); foreach (var child in directChildren) { result.Add(child); result.AddRange(GetDescendants(child.NodeId)); } return result; } private void ExpandAll_Click(object sender, RoutedEventArgs e) { foreach (var item in allBomItems) { item.IsExpanded = true; item.IsVisible = true; } } private void CollapseAll_Click(object sender, RoutedEventArgs e) { foreach (var item in allBomItems) { if (item.HasChildren) { item.IsExpanded = false; } item.IsVisible = item.Level == 0; } } private void BomDataGrid_CellEditEnding(object sender, DataGridCellEditEndingEventArgs e) { if (e.EditAction == DataGridEditAction.Cancel) { return; } if (!(e.Row.Item is BomItemModel item)) { return; } if (!string.IsNullOrEmpty(item.DocPath)) { dirtyDocPaths.Add(item.DocPath); } if (e.EditingElement is TextBox textBox) { string newValue = textBox.Text; string header = e.Column.Header?.ToString() ?? ""; if (PropNameToModelField.ContainsValue(header)) { if (item.Props != null) { item.Props[header] = newValue; } } else if (item.Props != null && item.Props.ContainsKey(header)) { item.Props[header] = newValue; } } } private void SaveChanges_Click(object sender, RoutedEventArgs e) { if (dirtyDocPaths.Count == 0) { System.Windows.MessageBox.Show(this, "没有需要保存的修改", "提示", MessageBoxButton.OK, MessageBoxImage.Information); return; } try { int savedCount = 0; foreach (var docPath in dirtyDocPaths) { var item = allBomItems.FirstOrDefault(b => b.DocPath == docPath); if (item == null || item.Props == null) { continue; } ModelDoc2 doc = SWUtils.OpenDocSilently(docPath); if (doc == null) { continue; } Configuration activeConfig = doc.ConfigurationManager?.ActiveConfiguration; CustomPropertyManager configPropMgr = activeConfig?.CustomPropertyManager; CustomPropertyManager filePropMgr = null; try { filePropMgr = doc.Extension?.get_CustomPropertyManager(""); } catch { } foreach (var kvp in item.Props) { if (string.IsNullOrEmpty(kvp.Key)) continue; bool writtenToConfig = false; if (configPropMgr != null) { var configNames = configPropMgr.GetNames() as string[]; if (configNames != null && configNames.Contains(kvp.Key)) { configPropMgr.Set2(kvp.Key, kvp.Value ?? ""); writtenToConfig = true; } } if (!writtenToConfig && filePropMgr != null) { filePropMgr.Set2(kvp.Key, kvp.Value ?? ""); } else if (!writtenToConfig && configPropMgr != null) { configPropMgr.Set2(kvp.Key, kvp.Value ?? ""); } } doc.Save3(0, 0, 0); savedCount++; } dirtyDocPaths.Clear(); System.Windows.MessageBox.Show(this, $"已保存 {savedCount} 个文件的属性修改", "保存成功", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { System.Windows.MessageBox.Show(this, ex.Message, "保存失败", MessageBoxButton.OK, MessageBoxImage.Error); } } private void SearchBox_TextChanged(object sender, TextChangedEventArgs e) { if (!(DataContext is ItemsViewModel viewModel)) { return; } string keyword = SearchBox.Text?.Trim() ?? ""; var sourceList = GetCurrentSourceList(); if (string.IsNullOrEmpty(keyword)) { viewModel.BomItems = new ObservableCollection(sourceList); return; } var filtered = sourceList.Where(item => (item.DrawingNo != null && item.DrawingNo.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0) || (item.ConfigName != null && item.ConfigName.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0) || (item.Material != null && item.Material.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0) || (item.MaterialProp != null && item.MaterialProp.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0) || (item.Classification != null && item.Classification.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0) || (item.Props != null && item.Props.Values.Any(v => v != null && v.IndexOf(keyword, StringComparison.OrdinalIgnoreCase) >= 0)) ).ToList(); viewModel.BomItems = new ObservableCollection(filtered); } private void ExportExcel_Click(object sender, RoutedEventArgs e) { if (!(DataContext is ItemsViewModel viewModel) || viewModel.BomItems == null || viewModel.BomItems.Count == 0) { System.Windows.MessageBox.Show(this, "没有数据可导出", "提示", MessageBoxButton.OK, MessageBoxImage.Information); return; } var dialog = new Microsoft.Win32.SaveFileDialog { Filter = "CSV 文件|*.csv", DefaultExt = ".csv", FileName = "BOM_Export" }; if (dialog.ShowDialog() != true) { return; } try { var sb = new StringBuilder(); bool isFlatView = viewModel.IsFlatView; var builtInDefs = new List<(string Name, Func Getter, bool FlatOnly)> { ("层级", item => item.LevelDisplay ?? "", false), ("图号", item => item.DrawingNo ?? "", false), ("零件名称", item => item.ConfigName ?? "", false), ("属性", item => item.Classification ?? "", false), ("材料", item => item.MaterialProp ?? "", false), ("材质", item => item.Material ?? "", false), ("数量", item => item.Quantity.ToString(), false), ("装配体", item => item.IsAssembly ? "是" : "否", false), ("外购件", item => item.IsOutSourcing ? "是" : "否", false) }; var exportBuiltIns = builtInDefs .Where(d => currentSettings.IsColumnExport(d.Name)) .Where(d => !isFlatView || (d.Name != "层级" && d.Name != "装配体")) .ToList(); var exportDynamicKeys = (viewModel.AllPropertyKeys ?? new List()) .Where(k => currentSettings.IsColumnExport(k)) .ToList(); var headers = exportBuiltIns.Select(d => d.Name).Concat(exportDynamicKeys).ToList(); sb.AppendLine(string.Join(",", headers.Select(EscapeCsvField))); foreach (var item in viewModel.BomItems) { var fields = new List(); foreach (var def in exportBuiltIns) { fields.Add(def.Getter(item)); } foreach (var key in exportDynamicKeys) { fields.Add(item.GetProp(key)); } sb.AppendLine(string.Join(",", fields.Select(EscapeCsvField))); } File.WriteAllText(dialog.FileName, sb.ToString(), Encoding.UTF8); System.Windows.MessageBox.Show(this, $"已导出到:\n{dialog.FileName}", "导出成功", MessageBoxButton.OK, MessageBoxImage.Information); } catch (Exception ex) { System.Windows.MessageBox.Show(this, ex.Message, "导出失败", MessageBoxButton.OK, MessageBoxImage.Error); } } private static string EscapeCsvField(string field) { if (string.IsNullOrEmpty(field)) { return ""; } if (field.Contains(",") || field.Contains("\"") || field.Contains("\n")) { return "\"" + field.Replace("\"", "\"\"") + "\""; } return field; } private void FlatViewToggle_Changed(object sender, RoutedEventArgs e) { if (!(DataContext is ItemsViewModel viewModel)) { return; } ApplyViewMode(viewModel); } private void ApplyViewMode(ItemsViewModel viewModel) { bool isFlatView = viewModel.IsFlatView; LevelColumn.Visibility = isFlatView ? Visibility.Collapsed : Visibility.Visible; ExpandAllBtn.Visibility = isFlatView ? Visibility.Collapsed : Visibility.Visible; CollapseAllBtn.Visibility = isFlatView ? Visibility.Collapsed : Visibility.Visible; if (isFlatView) { var flat = allBomItems .Where(i => !i.IsAssembly) .GroupBy(i => i.DrawingNo ?? "") .Select(g => { var first = g.First(); return new BomItemModel { Level = 0, LevelDisplay = "", DrawingNo = first.DrawingNo, ConfigName = first.ConfigName, PartName = first.PartName, MaterialProp = first.MaterialProp, Material = first.Material, Classification = first.Classification, Quantity = g.Sum(x => x.Quantity), IsAssembly = false, IsOutSourcing = first.IsOutSourcing, DocPath = first.DocPath, Props = first.Props, HasChildren = false, IsExpanded = false, IsVisible = true }; }) .ToList(); viewModel.BomItems = new ObservableCollection(flat); } else { viewModel.BomItems = new ObservableCollection(allBomItems); } } private List GetCurrentSourceList() { if (!(DataContext is ItemsViewModel viewModel)) { return allBomItems; } if (viewModel.IsFlatView) { return allBomItems .Where(i => !i.IsAssembly) .GroupBy(i => i.DrawingNo ?? "") .Select(g => { var first = g.First(); return new BomItemModel { Level = 0, LevelDisplay = "", DrawingNo = first.DrawingNo, ConfigName = first.ConfigName, PartName = first.PartName, MaterialProp = first.MaterialProp, Material = first.Material, Classification = first.Classification, Quantity = g.Sum(x => x.Quantity), IsAssembly = false, IsOutSourcing = first.IsOutSourcing, DocPath = first.DocPath, Props = first.Props, HasChildren = false, IsExpanded = false, IsVisible = true }; }) .ToList(); } return allBomItems; } private int GetTotalComponentCount(ModelDoc2 model) { AssemblyDoc assembly = model as AssemblyDoc; if (assembly == null) { return 1; } object[] components = assembly.GetComponents(true) as object[]; return Math.Max((components?.Length ?? 0) + 1, 1); } } }