编辑
2025-06-23
AutoCAD
00

目录

一、WPF技术简介
二、模态对话框实现
1. 基础模态对话框
2. 带数据绑定的模态对话框
三、基于MVVM模式的面板实现

一、WPF技术简介

所有上述示例均采用WinForms技术实现。而WPF(Windows Presentation Foundation)作为创建图形界面的"新技术",借助XAML语言定义界面,使开发者能够像编写HTML一样构建丰富的图形用户界面。

核心特性

  • 通过XAML语言实现界面布局与逻辑的分离
  • 支持强大的数据绑定机制、依赖属性和路由事件
  • 采用双文件结构:
    • .xaml 文件:定义界面外观、事件订阅及控件数据绑定
    • .xaml.cs 文件:实现用户界面与应用程序之间的交互逻辑(代码隐藏)

Visual Studio集成说明: 默认情况下,若项目类型非WPF应用程序,Visual Studio 2015不会提供"Window (WPF)"作为可添加元素。此时可通过以下步骤解决:

  1. 添加"User Control (WPF)"元素
  2. .xaml.xaml.cs 文件中将 UserControl 标签替换为 Window
  3. 可进一步添加如 Title 等属性至 Window 标签
  4. 通过"文件"菜单中的"导出模板"功能将其保存为项目模板,以便后续复用

二、模态对话框实现

1. 基础模态对话框

作为示例,我们先实现一个与前文WinForm模态对话框功能等价的WPF版本。

主要区别在于使用XAML语言描述用户界面,其架构与HTML有几分相似(但仅止于此)。界面中的控件均被命名以便在代码隐藏文件中访问,控件事件则订阅到代码隐藏文件中的事件处理程序。

在AutoCAD中显示WPF模态对话框,需使用 Application.ShowModalWindow() 方法,并传入一个通过重载构造函数创建的 System.Windows.Window 实例,类似于WinForms中的用法。

Commands.cs

C#
using System.Collections.Generic; using System.Linq; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Geometry; using Autodesk.AutoCAD.Runtime; using AcAp = Autodesk.AutoCAD.ApplicationServices.Application; [assembly: CommandClass(typeof(AcadUISample.ModalWpf.Commands))] namespace AcadUISample.ModalWpf { public class Commands { // 实例字段 Document doc; Database db; Editor ed; double radius; // 半径默认值 string layer; // 图层默认值 /// <summary> /// 创建Commands类的新实例 /// 此构造函数在首次调用'CommandMethod'方法时针对每个文档运行一次 /// </summary> public Commands() { // 私有字段初始化(初始默认值) doc = AcAp.DocumentManager.MdiActiveDocument; db = doc.Database; ed = doc.Editor; // 初始默认值 layer = (string)AcAp.GetSystemVariable("clayer"); radius = 10.0; } /// <summary> /// 显示对话框的命令 /// </summary> [CommandMethod("CMD_MODAL_WPF")] public void ModalWpfDialogCmd() { var layers = GetLayerNames(); if (!layers.Contains(layer)) { layer = (string)AcAp.GetSystemVariable("clayer"); } // 显示对话框 var dialog = new ModalWpfDialog(layers, layer, radius); var result = AcAp.ShowModalWindow(dialog); if (result.Value) { // 更新字段 layer = dialog.Layer; radius = dialog.Radius; // 绘制圆 var ppr = ed.GetPoint("\n指定圆心: "); if (ppr.Status == PromptStatus.OK) { // 在当前空间绘制圆 using (var tr = db.TransactionManager.StartTransaction()) { var curSpace = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite); using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius)) { circle.TransformBy(ed.CurrentUserCoordinateSystem); circle.Layer = layer; curSpace.AppendEntity(circle); tr.AddNewlyCreatedDBObject(circle, true); } tr.Commit(); } } } } /// <summary> /// 获取图层列表 /// </summary> /// <param name="db">此方法应用的数据库实例</param> /// <returns>图层名称列表</returns> private List<string> GetLayerNames() { using (var tr = db.TransactionManager.StartOpenCloseTransaction()) { return ((LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead)) .Cast<ObjectId>() .Select(id => ((LayerTableRecord)tr.GetObject(id, OpenMode.ForRead)).Name) .ToList(); } } } }

ModalWpfDialog.xaml

xml
<Window x:Class="AcadUISample.ModalWpf.ModalWpfDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:AcadUISample.ModalWpf" mc:Ignorable="d" Title="ModalWpfDialog" Height="160" Width="300" MinHeight="160" MinWidth="250" WindowStyle="ToolWindow" WindowStartupLocation="CenterOwner" > <Grid Background="WhiteSmoke"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!--第一行--> <Grid Grid.Row="0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Label Margin="5,15,5,5">图层:</Label> <ComboBox x:Name="cbxLayer" Grid.Column ="1" Margin="5,15,10,5" HorizontalAlignment="Stretch" /> </Grid> <!--第二行--> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Label Margin="5">半径:</Label> <TextBox x:Name="txtRadius" Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" TextChanged="txtRadius_TextChanged" /> <Button Grid.Column="2" Margin="5,5,10,5" Content=" > " Click="btnRadius_Click" /> </Grid> <!--第三行--> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button x:Name="btnOK" Margin="10" HorizontalAlignment="Right" Content="确定" Height="24" Width="80" Click="btnOK_Click"/> <Button Grid.Column="1" Margin="10" HorizontalAlignment="Left" Content="取消" Height="24" Width="80" IsCancel="True" /> </Grid> </Grid> </Window>

ModalWpfDialog.xaml.cs

C#
using Autodesk.AutoCAD.EditorInput; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using AcAp = Autodesk.AutoCAD.ApplicationServices.Application; namespace AcadUISample.ModalWpf { /// <summary> /// ModalWpfDialog.xaml的交互逻辑 /// </summary> public partial class ModalWpfDialog : Window { // 私有字段 double radius; /// <summary> /// 获取所选图层名称 /// </summary> public string Layer => (string)cbxLayer.SelectedItem; /// <summary> /// 获取半径值 /// </summary> public double Radius => radius; /// <summary> /// 创建ModalWpfDialog的新实例 /// </summary> /// <param name="layers">图层名称集合</param> /// <param name="layer">默认图层名称</param> /// <param name="radius">默认半径</param> public ModalWpfDialog(List<string> layers, string layer, double radius) { InitializeComponent(); this.radius = radius; cbxLayer.ItemsSource = layers; cbxLayer.SelectedItem = layer; txtRadius.Text = radius.ToString(); } /// <summary> /// 处理"Radius"按钮的"Click"事件 /// </summary> /// <param name="sender">事件源</param> /// <param name="e">事件数据</param> private void btnRadius_Click(object sender, RoutedEventArgs e) { // 提示用户指定距离 var ed = AcAp.DocumentManager.MdiActiveDocument.Editor; var opts = new PromptDistanceOptions("\n指定半径: "); opts.AllowNegative = false; opts.AllowZero = false; var pdr = ed.GetDistance(opts); if (pdr.Status == PromptStatus.OK) { txtRadius.Text = pdr.Value.ToString(); } } /// <summary> /// 处理"OK"按钮的"Click"事件 /// </summary> /// <param name="sender">事件源</param> /// <param name="e">事件数据</param> private void btnOK_Click(object sender, RoutedEventArgs e) { DialogResult = true; } /// <summary> /// 处理"Radius"文本框的"TextChanged"事件 /// </summary> /// <param name="sender">事件源</param> /// <param name="e">事件数据</param> private void txtRadius_TextChanged(object sender, TextChangedEventArgs e) { btnOK.IsEnabled = double.TryParse(txtRadius.Text, out radius); } } }

2. 带数据绑定的模态对话框

除了丰富的界面内容,WPF还提供了强大的数据绑定机制,部分基于依赖属性(能够通知其值发生变化的属性)。

要使用此机制,必须将数据上下文(DataContext)设置为一个类(此处为包含代码隐藏的类),并且该类必须实现INotifyPropertyChanged接口。

为了说明这一点,我们继续使用之前的模态对话框示例。

在XAML中,一些控件的依赖属性与代码隐藏中定义的属性相绑定。因此,包含代码隐藏的类必须实现INotifyPropertyChanged接口。同样,"Radius"和"OK"按钮绑定到代码隐藏中定义的RoutedCommand实例,这些实例管理它们的行为。使用WPF绑定可以避免为不同控件命名。

为了进一步优化,我们在下拉列表项中添加一个与图层颜色相同的正方形。

在XAML中,在ComboBox标签内定义一个ItemTemplate,以便在图层名称旁边显示彩色正方形。因此,与ComboBox控件相关的集合元素必须具有一个对应图层名称的属性和另一个SolidColorBrush类型的属性(WPF中用于绘制纯色的类型)。这里我们使用一个字典,其中包含图层名称(键)和每个图层颜色对应的画刷(值)。

Commands.cs

C#
using System.Collections.Generic; using System.Linq; using System.Windows.Media; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Geometry; using Autodesk.AutoCAD.Runtime; using AcAp = Autodesk.AutoCAD.ApplicationServices.Application; [assembly: CommandClass(typeof(AcadUISample.ModalWpfBinding.Commands))] namespace AcadUISample.ModalWpfBinding { public class Commands { // 实例字段 Document doc; Database db; Editor ed; double radius; // 半径默认值 string layerName; // 图层默认值 /// <summary> /// 创建Commands类的新实例 /// 此构造函数在首次调用'CommandMethod'方法时针对每个文档运行一次 /// </summary> public Commands() { // 私有字段初始化(初始默认值) doc = AcAp.DocumentManager.MdiActiveDocument; db = doc.Database; ed = doc.Editor; // 初始默认值 layerName = (string)AcAp.GetSystemVariable("clayer"); radius = 10.0; } /// <summary> /// 显示对话框的命令 /// </summary> [CommandMethod("CMD_MODAL_WPF_BINDING")] public void ModalWpfDialogCmd() { var layers = GetLayerBrushes(); if (!layers.ContainsKey(layerName)) { layerName = (string)AcAp.GetSystemVariable("clayer"); } var layer = layers.First(l => l.Key == layerName); // 显示对话框 var dialog = new ModalWpfDialog(layers, layer, radius); var result = AcAp.ShowModalWindow(dialog); if (result.Value) { // 更新字段 layerName = dialog.Layer.Key; radius = dialog.Radius; // 绘制圆 var ppr = ed.GetPoint("\n指定圆心: "); if (ppr.Status == PromptStatus.OK) { // 在当前空间绘制圆 using (var tr = db.TransactionManager.StartTransaction()) { var curSpace = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite); using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, radius)) { circle.TransformBy(ed.CurrentUserCoordinateSystem); circle.Layer = layerName; curSpace.AppendEntity(circle); tr.AddNewlyCreatedDBObject(circle, true); } tr.Commit(); } } } } /// <summary> /// 收集图层及其对应颜色的画刷 /// </summary> /// <returns>图层和画刷的字典</returns> private Dictionary<string, SolidColorBrush> GetLayerBrushes() { var layerBrushes = new Dictionary<string, SolidColorBrush>(); using (var tr = db.TransactionManager.StartOpenCloseTransaction()) { var layerTable = (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead); foreach (ObjectId id in layerTable) { var layer = (LayerTableRecord)tr.GetObject(id, OpenMode.ForRead); var drawingColor = layer.Color.ColorValue; var mediaColor = Color.FromRgb(drawingColor.R, drawingColor.G, drawingColor.B); layerBrushes.Add(layer.Name, new SolidColorBrush(mediaColor)); } } return layerBrushes; } } }

ModalWpfDialog.xaml

xml
<Window x:Class="AcadUISample.ModalWpfBinding.ModalWpfDialog" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:AcadUISample.ModalWpfBinding" mc:Ignorable="d" Title="ModalWpfDialog" Height="160" Width="300" MinHeight="160" MinWidth="250" WindowStyle="ToolWindow" WindowStartupLocation="CenterOwner" > <Window.CommandBindings> <CommandBinding Command="{x:Static local:ModalWpfDialog.OkCmd}" Executed="OkCmdExecuted" CanExecute="OkCmdCanExecute" /> <CommandBinding Command="{x:Static local:ModalWpfDialog.RadiusCmd}" Executed="RadiusCmdExecuted" CanExecute="RadiusCanExecute" /> </Window.CommandBindings> <Grid Background="WhiteSmoke"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!--第一行--> <Grid Grid.Row="0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Label Margin="5,15,5,5">图层:</Label> <ComboBox Grid.Column ="1" Margin="5,15,10,5" HorizontalAlignment="Stretch" ItemsSource="{Binding Layers}" SelectedItem="{Binding Layer}"> <!--定义下拉列表的模板--> <ComboBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--带有图层颜色的正方形--> <Rectangle Grid.Column="0" Margin="3" VerticalAlignment="Stretch" Width="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}" Stroke="Black" StrokeThickness="0.5" Fill="{Binding Value}"/> <!--图层名称--> <TextBlock Grid.Column="1" VerticalAlignment="Center" Text="{Binding Key}" /> </Grid> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox> </Grid> <!--第二行--> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Label Margin="5">半径:</Label> <TextBox Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" Text="{Binding TextRadius, UpdateSourceTrigger=PropertyChanged}" /> <Button Grid.Column="2" Margin="5,5,10,5" Content=" > " Command="{x:Static local:ModalWpfDialog.RadiusCmd}" /> </Grid> <!--第三行--> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Button Margin="10" HorizontalAlignment="Right" Content="确定" Height="24" Width="80" Command="{x:Static local:ModalWpfDialog.OkCmd}"/> <Button Grid.Column="1" Margin="10" HorizontalAlignment="Left" Content="取消" Height="24" Width="80" IsCancel="True" /> </Grid> </Grid> </Window>

ModalWpfDialog.xaml.cs

C#
using System.Collections.Generic; using System.ComponentModel; using System.Windows; using System.Windows.Input; using System.Windows.Media; using Autodesk.AutoCAD.EditorInput; using AcAp = Autodesk.AutoCAD.ApplicationServices.Application; namespace AcadUISample.ModalWpfBinding { /// <summary> /// ModalWpfDialog.xaml的交互逻辑 /// </summary> public partial class ModalWpfDialog : Window, INotifyPropertyChanged { #region INotifyPropertyChanged实现 /// <summary> /// 当属性变更时引发的事件 /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// 在需要通知变更的属性的setter中调用的方法 /// </summary> protected void OnPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion // 私有字段 KeyValuePair<string, SolidColorBrush> layer; double radius; string txtRad; bool validNumber; /// <summary> /// 获取绑定到"确定"按钮的命令 /// </summary> public static RoutedCommand OkCmd = new RoutedCommand(); /// <summary> /// 获取绑定到"半径"按钮的命令 /// </summary> public static RoutedCommand RadiusCmd = new RoutedCommand(); /// <summary> /// 获取或设置绑定到ComboBox控件中所选图层的KeyValuePair实例 /// </summary> public KeyValuePair<string, SolidColorBrush> Layer { get { return layer; } set { layer = value; OnPropertyChanged(nameof(Layer)); } } /// <summary> /// 获取绑定到ComboBox控件项的图层数据集合 /// </summary> public Dictionary<string, SolidColorBrush> Layers { get; } /// <summary> /// 获取半径值 /// </summary> public double Radius => radius; /// <summary> /// 获取或设置"半径"编辑框中的文本 /// </summary> public string TextRadius { get { return txtRad; } set { txtRad = value; ValidNumber = double.TryParse(value, out radius) && radius > 0.0; OnPropertyChanged(nameof(TextRadius)); } } /// <summary> /// 获取或设置一个值,该值指示"半径"编辑框中的文本是否表示一个有效数字 /// </summary> public bool ValidNumber { get { return validNumber; } set { validNumber = value; OnPropertyChanged(nameof(ValidNumber)); } } /// <summary> /// 创建ModalWpfDialog的新实例 /// </summary> /// <param name="layers">要绑定到ComboBox控件的图层数据集合</param> /// <param name="layer">默认图层数据</param> /// <param name="radius">默认半径</param> public ModalWpfDialog(Dictionary<string, SolidColorBrush> layers, KeyValuePair<string, SolidColorBrush> layer, double radius) { InitializeComponent(); // 定义数据上下文 DataContext = this; // 初始化绑定 Layers = layers; Layer = layer; TextRadius = radius.ToString(); } /// <summary> /// 定义绑定到"确定"按钮的操作 /// </summary> /// <param name="sender">事件源</param> /// <param name="e">事件数据</param> private void OkCmdExecuted(object sender, ExecutedRoutedEventArgs e) => DialogResult = true; /// <summary> /// 确定绑定的"确定"按钮操作是否可以执行 /// </summary> /// <param name="sender">事件源</param> /// <param name="e">事件数据</param> private void OkCmdCanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = ValidNumber; /// <summary> /// 定义绑定到"半径"按钮的操作 /// </summary> /// <param name="sender">事件源</param> /// <param name="e">事件数据</param> private void RadiusCmdExecuted(object sender, ExecutedRoutedEventArgs e) { // 提示用户指定距离 var ed = AcAp.DocumentManager.MdiActiveDocument.Editor; var opts = new PromptDistanceOptions("\n指定半径: "); opts.AllowNegative = false; opts.AllowZero = false; var pdr = ed.GetDistance(opts); if (pdr.Status == PromptStatus.OK) TextRadius = pdr.Value.ToString(); } /// <summary> /// 确定绑定的"半径"按钮操作是否可以执行 /// </summary> /// <param name="sender">事件源</param> /// <param name="e">事件数据</param> private void RadiusCanExecute(object sender, CanExecuteRoutedEventArgs e) => e.CanExecute = true; } }

三、基于MVVM模式的面板实现

MVVM(模型-视图-视图模型)设计模式特别适合WPF应用。它允许将"业务"部分(通常是数据,即Model)与其展示(View)以及两者之间的交互逻辑(ViewModel)分离开来。

严格应用这种架构可能看起来有些繁琐(特别是在像这样简单的示例中),但它通过优化绑定的使用来鼓励更好的代码结构。

在此示例中,Model部分除了圆绘制命令外不包含任何特定代码。可以认为它由AutoCAD API组成。例如,图层下拉列表将使用UIBindings API与AutoCAD链接(无模式模式需要"动态"集合)。

Commands.cs

C#
using Autodesk.AutoCAD.ApplicationServices.Core; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Geometry; using Autodesk.AutoCAD.Runtime; [assembly: CommandClass(typeof(AcadUISample.PaletteWpf.Commands))] namespace AcadUISample.PaletteWpf { public class Commands { // 静态字段 static CustomPaletteSet palette; // 实例字段(默认值) double radius = 10.0; string layer; /// <summary> /// 显示面板的命令 /// </summary> [CommandMethod("CMD_PALETTE_WPF")] public void ShowPaletteSetWpf() { if (palette == null) palette = new CustomPaletteSet(); palette.Visible = true; } /// <summary> /// 绘制圆的命令 /// </summary> [CommandMethod("CMD_CIRCLE_WPF")] public void DrawCircleCmd() { var doc = Application.DocumentManager.MdiActiveDocument; var db = doc.Database; var ed = doc.Editor; // 选择图层 if (string.IsNullOrEmpty(layer)) layer = (string)Application.GetSystemVariable("clayer"); var strOptions = new PromptStringOptions("\n图层名称: "); strOptions.DefaultValue = layer; strOptions.UseDefaultValue = true; var strResult = ed.GetString(strOptions); if (strResult.Status != PromptStatus.OK) return; layer = strResult.StringResult; // 指定半径 var distOptions = new PromptDistanceOptions("\n指定半径: "); distOptions.DefaultValue = radius; distOptions.UseDefaultValue = true; var distResult = ed.GetDistance(distOptions); if (distResult.Status != PromptStatus.OK) return; radius = distResult.Value; // 指定圆心 var ppr = ed.GetPoint("\n指定圆心: "); if (ppr.Status == PromptStatus.OK) { // 在当前空间绘制圆 using (var tr = db.TransactionManager.StartTransaction()) { var curSpace = (BlockTableRecord)tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite); using (var circle = new Circle(ppr.Value, Vector3d.ZAxis, distResult.Value)) { circle.TransformBy(ed.CurrentUserCoordinateSystem); circle.Layer = strResult.StringResult; curSpace.AppendEntity(circle); tr.AddNewlyCreatedDBObject(circle, true); } tr.Commit(); } } } } }

视图部分的用户界面分布在以下文件中:

  • CustomPaletteSet.cs文件:描述面板集
  • PaletteTabView.xaml文件:描述用户控件并定义与ViewModel属性的绑定
  • PaletteTabView.xaml.cs文件:通常包含代码隐藏,在MVVM架构中一般几乎为空(交互逻辑属于ViewModel部分)

CustomPaletteSet.cs

C#
using System; using Autodesk.AutoCAD.ApplicationServices.Core; using Autodesk.AutoCAD.Windows; namespace AcadUISample.PaletteWpf { internal class CustomPaletteSet : PaletteSet { // 静态字段 static bool wasVisible; /// <summary> /// 创建CustomPaletteSet的新实例 /// </summary> public CustomPaletteSet() :base("Palette WPF", "CMD_PALETTE_WPF", new Guid("{42425FEE-B3FD-4776-8090-DB857E9F7A0E}")) { Style = PaletteSetStyles.ShowAutoHideButton | PaletteSetStyles.ShowCloseButton | PaletteSetStyles.ShowPropertiesMenu; MinimumSize = new System.Drawing.Size(250, 150); AddVisual("Circle", new PaletteTabView()); // 当没有文档处于活动状态时自动隐藏面板(无文档状态) var docs = Application.DocumentManager; docs.DocumentBecameCurrent += (s, e) => Visible = e.Document == null ? false : wasVisible; docs.DocumentCreated += (s, e) => Visible = wasVisible; docs.DocumentToBeDeactivated += (s, e) => wasVisible = Visible; docs.DocumentToBeDestroyed += (s, e) => { wasVisible = Visible; if (docs.Count == 1) Visible = false; }; } } }

PaletteTabView.xaml

xml
<UserControl x:Class="AcadUISample.PaletteWpf.PaletteTabView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:AcadUISample.PaletteWpf" mc:Ignorable="d" d:DesignHeight="140" d:DesignWidth="250"> <!--定义资源(颜色转换器)--> <UserControl.Resources> <local:LayerColorConverter x:Key="colorConverter"/> </UserControl.Resources> <Grid Background="WhiteSmoke"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <!--第一行--> <Grid Grid.Row="0"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Label Margin="5,15,5,5">图层:</Label> <ComboBox x:Name="cbxLayer" Grid.Column ="1" Margin="5,15,10,5" HorizontalAlignment="Stretch" ItemsSource="{Binding Layers}" SelectedItem="{Binding Layer}"> <!--定义下拉列表的模板--> <ComboBox.ItemTemplate> <DataTemplate> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <!--带有图层颜色的正方形--> <Rectangle Grid.Column="0" Margin="3" VerticalAlignment="Stretch" Width="{Binding Path=ActualHeight, RelativeSource={RelativeSource Self}}" Stroke="Black" StrokeThickness="0.5" Fill="{Binding Color, Converter={StaticResource colorConverter}}"/> <!--图层名称--> <TextBlock Grid.Column="1" VerticalAlignment="Center" Text="{Binding Name}" /> </Grid> </DataTemplate> </ComboBox.ItemTemplate> </ComboBox> </Grid> <!--第二行--> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> </Grid.ColumnDefinitions> <Label Margin="5">半径:</Label> <TextBox Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" Text="{Binding TextRadius, UpdateSourceTrigger=PropertyChanged}" /> <Button Grid.Column="2" Margin="5,5,10,5" Content=" > " Command="{Binding GetRadiusCommand}"/> </Grid> <!--第三行--> <Button Grid.Row="2" Margin="5,15,5,5" Content="确定" Height="24" Width="80" Command="{Binding DrawCircleCommand}"/> </Grid> </UserControl>

PaletteTabView.xaml.cs

C#
using System.Windows.Controls; namespace AcadUISample.PaletteWpf { /// <summary> /// PaletteTabView.xaml的交互逻辑 /// </summary> public partial class PaletteTabView : UserControl { public PaletteTabView() { InitializeComponent(); // 定义与ViewModel部分的数据绑定 DataContext = new PaletteTabViewModel(); } } }

ViewModel部分支持交互逻辑(在前面的示例中由代码隐藏完成)。PaletteTabViewModel类通过继承关系实现INotifyPropertyChanged接口,以处理属性变更,就像前面的示例一样。

对于按钮,我们使用Command属性而不是代码隐藏中的'Click'事件及其处理程序,这使得可以将展示(View)与其逻辑(ViewModel)分离。此属性必须是实现ICommand接口的类型。

因此,在MVVM架构中,我们在此部分找到了两个不可避免的小类:

  • ObservableObject:实现INotifyPropertyChanged接口
  • RelayCommand:实现ICommand接口

在ViewModel部分,还有一些展示所需的资源,包括一个LayerColorConverter类,用于将表示图层颜色的Autodesk.AutoCAD.Colors.Color类型元素(下拉列表的元素)转换为相应颜色的SolidColorBrush实例;正方形的填充将与此转换器关联。

PaletteTabViewModel.cs

C#
using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Windows.Data; using System.ComponentModel; using AcAp = Autodesk.AutoCAD.ApplicationServices.Application; namespace AcadUISample.PaletteWpf { class PaletteTabViewModel : ObservableObject { // 私有字段 ICustomTypeDescriptor layer; double radius; string txtRad; bool validRad; /// <summary> /// 获取绑定到确定按钮的Command对象 /// 如果CanExecute谓词返回false,按钮将自动禁用 /// </summary> public RelayCommand DrawCircleCommand => new RelayCommand((_) => DrawCircle(), (_) => validRad); /// <summary> /// 获取绑定到半径按钮(>)的Command对象 /// </summary> public RelayCommand GetRadiusCommand => new RelayCommand((_) => GetRadius(), (_) => true); /// <summary> /// 获取或设置所选图层 /// </summary> public ICustomTypeDescriptor Layer { get { return layer; } set { layer = value; OnPropertyChanged(nameof(Layer)); } } /// <summary> /// 获取图层集合 /// </summary> public DataItemCollection Layers => AcAp.UIBindings.Collections.Layers; /// <summary> /// 获取或设置文本框中显示的半径值 /// </summary> public string TextRadius { get { return txtRad; } set { txtRad = value; validRad = double.TryParse(value, out radius) && radius > 0.0; OnPropertyChanged(nameof(TextRadius)); } } /// <summary> /// 创建PaletteTabViewModel的新实例 /// </summary> public PaletteTabViewModel() { TextRadius = "10"; Layer = Layers.CurrentItem; Layers.CollectionChanged += (s, e) => Layer = Layers.CurrentItem; } /// <summary> /// 由DrawCircleCommand调用的方法 /// 使用当前选项启动CMD_CIRCLE_WPF命令 /// </summary> private async void DrawCircle() { var docs = AcAp.DocumentManager; var ed = docs.MdiActiveDocument.Editor; await docs.ExecuteInCommandContextAsync( (_) => { ed.Command("CMD_CIRCLE_WPF", ((INamedValue)Layer).Name, radius); return Task.CompletedTask; }, null); } // 对于AutoCAD 2016之前的版本,使用Document.SendStringToExecute //private void DrawCircle() => // AcAp.DocumentManager.MdiActiveDocument?.SendStringToExecute( // $"CMD_CIRCLE_WPF \"{((INamedValue)Layer).Name}\" {TextRadius} ", false, false, false); /// <summary> /// 由GetRadiusCommand调用的方法 /// </summary> private void GetRadius() { // 提示用户指定距离 var ed = AcAp.DocumentManager.MdiActiveDocument.Editor; var opts = new PromptDistanceOptions("\n指定半径: "); opts.AllowNegative = false; opts.AllowZero = false; var pdr = ed.GetDistance(opts); if (pdr.Status == PromptStatus.OK) TextRadius = pdr.Value.ToString(); } } }

ObservableObject.cs

C#
using System.ComponentModel; namespace AcadUISample.PaletteWpf { /// <summary> /// 提供实现INotifyPropertyChanged的类型 /// </summary> class ObservableObject : INotifyPropertyChanged { /// <summary> /// 当属性变更时引发的事件 /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> /// 在需要通知变更的属性的'setter'中调用的方法 /// </summary> /// <param name="propertyName">属性名称</param> protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }

RelayCommand.cs

C#
using System; using System.Windows.Input; namespace AcadUISample.PaletteWpf { /// <summary> /// 提供实现ICommand的类型 /// </summary> class RelayCommand : ICommand { readonly Action<object> execute; readonly Predicate<object> canExecute; /// <summary> /// 创建RelayCommand的新实例 /// </summary> /// <param name="execute">要执行的操作</param> /// <param name="canExecute">指示操作是否可以执行的谓词</param> public RelayCommand(Action<object> execute, Predicate<object> canExecute) { this.execute = execute; this.canExecute = canExecute; } /// <summary> /// 执行传递给构造函数的操作 /// </summary> /// <param name="parameter">操作参数(可能为null)</param> public void Execute(object parameter) => execute(parameter); /// <summary> /// 执行传递给构造函数的谓词 /// </summary> /// <param name="parameter">谓词参数(可能为null)</param> /// <returns>谓词执行的结果</returns> public bool CanExecute(object parameter) => canExecute(parameter); /// <summary> /// 指示谓词返回值已更改的事件 /// </summary> public event EventHandler CanExecuteChanged { add { CommandManager.RequerySuggested += value; } remove { CommandManager.RequerySuggested -= value; } } } }

LayerColorConverter.cs

C#
using System; using System.ComponentModel; using System.Globalization; using System.Windows.Data; using System.Windows.Media; namespace AcadUISample.PaletteWpf { /// <summary> /// 提供获取图层颜色的转换方法 /// </summary> [ValueConversion(typeof(ICustomTypeDescriptor), typeof(SolidColorBrush))] class LayerColorConverter : IValueConverter { /// <summary> /// 将表示图层的ICustomTypeDescriptor对象转换为其颜色 /// </summary> /// <param name="value">要转换的AutoCAD颜色</param> /// <param name="targetType">SolidColorBrush类型</param> /// <param name="parameter">未使用</param> /// <param name="culture">未使用</param> /// <returns>表示图层颜色的SolidColorBrush实例</returns> public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value != null && value is Autodesk.AutoCAD.Colors.Color) { var acadColor = (Autodesk.AutoCAD.Colors.Color)value; var drawingColor = acadColor.ColorValue; var mediaColor = Color.FromRgb(drawingColor.R, drawingColor.G, drawingColor.B); return new SolidColorBrush(mediaColor); } return null; } /// <summary> /// 反向转换方法(未使用) /// </summary> /// <returns>始终返回null</returns> public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return null; } } }