告别默认UI!用ArcGIS Pro SDK的ProWindow控件,打造你的专属工具箱窗口
用ArcGIS Pro SDK的ProWindow打造高效GIS工具箱从基础到实战在GIS数据处理和分析的日常工作中我们经常会遇到一些重复性高、步骤繁琐的任务。比如需要反复查询特定字段的值、统计不同类别的数量、或者对多个图层执行相同的操作流程。这些操作虽然简单但每次都需要打开多个对话框、点击不同的菜单项效率低下且容易出错。ArcGIS Pro SDK中的ProWindow控件为解决这类问题提供了完美的方案——它允许我们将这些常用功能集成到一个可停靠、可定制的面板中实现一键式工作流。1. ProWindow核心优势与设计理念ProWindow不仅仅是一个简单的WPF窗口控件它是ArcGIS Pro界面体系中的一等公民。与传统的WPF窗口相比ProWindow具有以下独特优势原生集成完美融入ArcGIS Pro界面风格支持停靠、浮动、标签化等特性功能丰富可以直接访问ArcGIS Pro的API和上下文环境性能优化针对GIS数据处理进行了特别优化避免UI卡顿在设计自定义工具箱时我们需要遵循几个关键原则功能聚合将相关操作集中在一个面板中减少界面跳转操作简化通过合理的默认值和智能预判减少用户输入状态保持记住用户上次的操作选择和参数设置反馈及时提供清晰的操作反馈和进度指示!-- 典型的ProWindow XAML结构示例 -- pro:DockPane x:ClassMyToolbox.ToolboxView xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml xmlns:proclr-namespace:ArcGIS.Desktop.Framework.Controls;assemblyArcGIS.Desktop.Framework Grid !-- 工具箱内容布局 -- /Grid /pro:DockPane2. 构建基础工具箱框架让我们从创建一个实用的字段操作工具箱开始。这个工具箱将包含字段浏览、值统计和快速导出等常用功能。2.1 项目设置与基础结构首先在Visual Studio中创建ArcGIS Pro模块项目然后添加ProWindow右键项目 → 添加 → 新建项选择ArcGIS Pro ProWindow命名为FieldToolboxView.xaml创建完成后我们会得到一个基本的XAML文件和一个对应的C#代码文件。先设置窗口的基本属性public partial class FieldToolboxView : ProWindow { public FieldToolboxView() { InitializeComponent(); this.Title 字段工具箱; this.Width 350; this.Height 500; } }2.2 界面布局设计使用WPF的Grid布局系统创建响应式界面Grid Grid.RowDefinitions RowDefinition HeightAuto/ RowDefinition HeightAuto/ RowDefinition HeightAuto/ RowDefinition Height*/ RowDefinition HeightAuto/ /Grid.RowDefinitions !-- 图层选择区域 -- GroupBox Grid.Row0 Header选择图层 StackPanel ComboBox x:NameLayerComboBox Margin5/ Button x:NameBrowseButton Content浏览... Margin5 ClickBrowseButton_Click/ /StackPanel /GroupBox !-- 字段操作区域 -- GroupBox Grid.Row1 Header字段操作 StackPanel ComboBox x:NameFieldComboBox Margin5/ Button x:NameShowValuesButton Content显示唯一值 Margin5 ClickShowValuesButton_Click/ /StackPanel /GroupBox !-- 值显示区域 -- GroupBox Grid.Row2 Header字段值 ListBox x:NameValuesListBox Height150 Margin5/ /GroupBox !-- 统计信息区域 -- GroupBox Grid.Row3 Header统计信息 DataGrid x:NameStatsDataGrid Margin5/ /GroupBox !-- 操作按钮区域 -- StackPanel Grid.Row4 OrientationHorizontal HorizontalAlignmentRight Button x:NameExportButton Content导出CSV Margin5 ClickExportButton_Click/ Button x:NameCloseButton Content关闭 Margin5 ClickCloseButton_Click/ /StackPanel /Grid3. 实现核心功能逻辑3.1 图层与字段加载我们需要在窗口加载时自动填充当前地图中的图层protected override async void OnInitialized(EventArgs e) { base.OnInitialized(e); await LoadLayersAsync(); } private async Task LoadLayersAsync() { await QueuedTask.Run(() { var map MapView.Active?.Map; if (map null) return; var layers map.GetLayersAsFlattenedList().OfTypeFeatureLayer(); Application.Current.Dispatcher.Invoke(() { LayerComboBox.ItemsSource layers; LayerComboBox.DisplayMemberPath Name; }); }); }当用户选择图层后自动加载其字段private async void LayerComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (LayerComboBox.SelectedItem is FeatureLayer selectedLayer) { await LoadFieldsAsync(selectedLayer); } } private async Task LoadFieldsAsync(FeatureLayer layer) { await QueuedTask.Run(() { var fields layer.GetFieldDescriptions().Select(f f.Name).ToList(); Application.Current.Dispatcher.Invoke(() { FieldComboBox.ItemsSource fields; }); }); }3.2 字段值统计与展示实现显示字段唯一值并计算基本统计信息private async void ShowValuesButton_Click(object sender, RoutedEventArgs e) { if (!(LayerComboBox.SelectedItem is FeatureLayer layer) || string.IsNullOrEmpty(FieldComboBox.SelectedItem as string)) return; var fieldName FieldComboBox.SelectedItem.ToString(); await ShowFieldValuesAsync(layer, fieldName); await CalculateFieldStatsAsync(layer, fieldName); } private async Task ShowFieldValuesAsync(FeatureLayer layer, string fieldName) { ValuesListBox.Items.Clear(); await QueuedTask.Run(() { var table layer.GetTable(); var uniqueValues new HashSetstring(); using (var cursor table.Search()) { while (cursor.MoveNext()) { var value cursor.Current[fieldName]?.ToString(); if (!string.IsNullOrEmpty(value) !uniqueValues.Contains(value)) { uniqueValues.Add(value); Application.Current.Dispatcher.Invoke(() { ValuesListBox.Items.Add(value); }); } } } }); } private async Task CalculateFieldStatsAsync(FeatureLayer layer, string fieldName) { await QueuedTask.Run(() { var table layer.GetTable(); var stats new Dictionarystring, int(); var totalCount 0; using (var cursor table.Search()) { while (cursor.MoveNext()) { var value cursor.Current[fieldName]?.ToString() ?? NULL; if (stats.ContainsKey(value)) stats[value]; else stats[value] 1; totalCount; } } var statsList stats.Select(kv new { Value kv.Key, Count kv.Value, Percentage Math.Round((double)kv.Value / totalCount * 100, 2) }).OrderByDescending(x x.Count).ToList(); Application.Current.Dispatcher.Invoke(() { StatsDataGrid.ItemsSource statsList; }); }); }4. 高级功能扩展4.1 数据导出功能添加将统计结果导出为CSV的功能private void ExportButton_Click(object sender, RoutedEventArgs e) { if (!(StatsDataGrid.ItemsSource is IList statsList) || statsList.Count 0) return; var saveDialog new SaveFileDialog { Filter CSV文件|*.csv, Title 保存统计结果 }; if (saveDialog.ShowDialog() true) { using (var writer new StreamWriter(saveDialog.FileName)) { // 写入标题行 var firstItem statsList[0]; var properties firstItem.GetType().GetProperties(); writer.WriteLine(string.Join(,, properties.Select(p p.Name))); // 写入数据行 foreach (var item in statsList) { var values properties.Select(p p.GetValue(item)?.ToString() ?? ).Select(v v.Contains(,) ? $\{v}\ : v); writer.WriteLine(string.Join(,, values)); } } } }4.2 界面状态持久化让工具箱记住用户上次的选择// 在类中添加字段 private string _lastUsedLayerName; private string _lastUsedFieldName; // 在窗口关闭时保存状态 protected override void OnClosed(EventArgs e) { if (LayerComboBox.SelectedItem is FeatureLayer layer) _lastUsedLayerName layer.Name; if (FieldComboBox.SelectedItem is string fieldName) _lastUsedFieldName fieldName; base.OnClosed(e); } // 在窗口加载时恢复状态 protected override async void OnInitialized(EventArgs e) { base.OnInitialized(e); await LoadLayersAsync(); if (!string.IsNullOrEmpty(_lastUsedLayerName)) { var layer LayerComboBox.Items.OfTypeFeatureLayer() .FirstOrDefault(l l.Name _lastUsedLayerName); if (layer ! null) { LayerComboBox.SelectedItem layer; if (!string.IsNullOrEmpty(_lastUsedFieldName) FieldComboBox.Items.Contains(_lastUsedFieldName)) { FieldComboBox.SelectedItem _lastUsedFieldName; } } } }4.3 多线程优化与进度反馈添加进度条和取消操作功能!-- 在XAML中添加进度条 -- ProgressBar x:NameProgressBar Grid.Row4 Height10 Margin5,0 VisibilityCollapsed IsIndeterminateTrue/// 修改统计方法以支持取消 private CancellationTokenSource _cts; private async Task CalculateFieldStatsAsync(FeatureLayer layer, string fieldName) { // 取消之前的操作 _cts?.Cancel(); _cts new CancellationTokenSource(); ProgressBar.Visibility Visibility.Visible; ExportButton.IsEnabled false; try { await QueuedTask.Run(() { var token _cts.Token; // ...原有统计代码... // 在循环中定期检查取消请求 while (cursor.MoveNext()) { token.ThrowIfCancellationRequested(); // ...处理当前行... } }, _cts.Token); } catch (OperationCanceledException) { // 用户取消了操作 } finally { ProgressBar.Visibility Visibility.Collapsed; ExportButton.IsEnabled true; } }5. 实战构建完整GIS工具箱将上述功能模块组合起来我们可以创建一个更加强大的GIS工具箱。以下是几个实用的功能模块建议5.1 批量字段计算器public class BatchFieldCalculator { public static async Task Calculate(FeatureLayer layer, string expression, IEnumerablestring targetFields) { await QueuedTask.Run(() { var table layer.GetTable(); var cursor table.Search(); while (cursor.MoveNext()) { var row cursor.Current; foreach (var field in targetFields) { // 执行计算并更新字段值 row[field] EvaluateExpression(expression, row); } row.Store(); } }); } private static object EvaluateExpression(string expr, Row row) { // 实现表达式解析逻辑 return null; } }5.2 空间查询工具public class SpatialQueryTool { public static async TaskListFeature QueryByLocation( FeatureLayer targetLayer, Geometry queryGeometry, SpatialRelationship relationship) { var results new ListFeature(); await QueuedTask.Run(() { var spatialFilter new SpatialQueryFilter { FilterGeometry queryGeometry, SpatialRelationship relationship }; using (var cursor targetLayer.Search(spatialFilter)) { while (cursor.MoveNext()) { results.Add(cursor.Current as Feature); } } }); return results; } }5.3 图层比较工具public class LayerComparer { public static async TaskComparisonResult CompareLayers( FeatureLayer layer1, FeatureLayer layer2, string[] compareFields) { var result new ComparisonResult(); await QueuedTask.Run(() { // 比较空间参考 result.SpatialReferenceMatch layer1.GetSpatialReference().IsEqual( layer2.GetSpatialReference()); // 比较字段结构 var fields1 layer1.GetFieldDescriptions(); var fields2 layer2.GetFieldDescriptions(); result.FieldStructureMatch fields1.Select(f f.Name).SequenceEqual( fields2.Select(f f.Name)); // 比较样本数据 result.SampleDataMatch CompareSampleData( layer1, layer2, compareFields); }); return result; } }在实际项目中我发现将常用工具模块化并集中在一个可停靠的面板中可以显著提升工作效率。特别是在处理大量重复性操作时这种自定义工具箱能够节省大量时间。