WPF 开发之旅:从 UI 基础到高级架构 (一)

mindtian 发布于 2025-07-15 213 次阅读


AI 摘要

当XAML遇见CLR,命名空间成了连接UI与代码的暗门。你真的了解它背后的逻辑吗?

一、 WPF 相关知识概述

1. 什么是 WPF 框架?

WPF 的全称是 Windows Presentation Foundation,中文通常翻译为“Windows 呈现基础”。它是一个由微软开发的、用于构建桌面应用程序的用户界面(UI)框架,是 .NET Framework(现在也兼容 .NET Core / .NET)的一部分。

2. 命名空间

a. 基本 WPF 命名空间

WPF 核心控件(如 Window, Grid, Button, TextBox 等)所在的命名空间。

b. XAML 语言命名空间(x:)

XAML 本身的一些特殊指令和属性(如 x:Name, x:Key, x:Class, x:Static, x:Bind 等)所在的命名空间。

<Window x:Class="MyWPFApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="450" Width="800">
    <!-- 这里的 Grid 和 Button 默认就属于 http://schemas.microsoft.com/winfx/2006/xaml/presentation 命名空间 -->
    <Grid>
        <Button Content="点击我"/>
        <Button x:Name="myButton" Content="点击我"/> <!-- 这里的 x:Name 就属于 x: 命名空间 -->
    </Grid>
</Window>

c. 自定义(CLR)命名空间 / 映射的命名空间

想要在 XAML 中使用你自己的 C# 类(例如自定义控件、Converter、ViewModel 等),或者第三方库中的类时,就需要声明的命名空间。

3. 映射

“映射”在 WPF/XAML 中特指将一个XAML 前缀与一个CLR 命名空间和一个程序集(Assembly) 关联起来的过程。

a. 为什么需要映射

XAML 文件本身并不知道你的 MyCustomControl 类在哪里。它只知道 XML 元素名。为了让 XAML 处理器能够找到并实例化你的 C# 类,你必须告诉它:

  1. 这个 XAML 前缀(比如 local:)代表的是哪个 XAML 命名空间。
  2. 这个 XAML 命名空间实际上对应着 C# 代码中的哪个 namespace。
  3. 这个 C# namespace 又位于哪一个编译后的 DLL 文件(程序集)中。

映射的语法

xmlns:prefix="clr-namespace:ClrNamespaceName;assembly=AssemblyName"

b. 将 CLR 命名空间映射到程序集中的 xml 命名空间

[assembly: XmlnsDefinition(URI, CLRNamespace)]

AssemblyInfo.cs 中添加

二、 运行剖析

1. XAML 命名空间 x: 前缀

x: 前缀功能含义 (What it means)作用 (What it does)
x:Class指示 XAML 元素(通常是根元素)关联的 CLR 类。将 XAML 定义的 UI 结构与指定的 C#(或VB.NET)后台代码文件关联起来。 允许你在后台代码中访问 XAML 中定义的 UI 元素,并处理事件、实现业务逻辑。在编译时,XAML 会被处理成与此 CLR 类合并的部分类。
x:Name为 XAML 中声明的元素(如控件、面板)指定一个唯一的名称。在运行时,允许你在 C# 后台代码中通过这个名称直接访问和引用 XAML 中声明的 UI 元素。 这类似于 HTML 中的 id 属性,是 UI 元素与代码交互的桥梁,例如:myTextBox.Text = "新的文本";。
x:Key为资源字典中的资源项指定一个唯一的键。用于在 ResourceDictionary 中唯一标识一个资源。 其他 XAML 元素可以通过 StaticResource 或 DynamicResource 标记扩展,引用这个键来重用样式、模板、颜色刷、字符串等定义好的对象。例如:<Button Style="{StaticResource MyButtonStyle}"/>。

三、 常用控件(一)

1. 标签 Label

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
Contentobject设置标签显示的文本或其他任何 UI 元素内容。
TargetUIElement指定与此标签关联的 UI 元素,通常是输入控件(如 TextBox)。点击 Label 或使用其助记符时,焦点会转移到 Target 控件上。
VerticalAlignmentVerticalAlignment控制标签内容在标签内部可用空间的垂直对齐方式(Top, Center, Bottom, Stretch)。
HorizontalAlignmentHorizontalAlignment控制标签内容在标签内部可用空间的水平对齐方式(Left, Center, Right, Stretch)。

代码示例:

<Label Target="{Binding ElementName=emailTextBox}" Margin="0,10,0,0">
    <AccessText Text="_电子邮件地址:"/>
</Label>
<TextBox x:Name="emailTextBox" Width="200" HorizontalAlignment="Left"/>

2. 文本显示 TextBlock

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
Textstring获取或设置要显示的文本内容。
TextWrappingTextWrapping定义文本是否在控件边界处自动换行(NoWrap, Wrap, WrapWithOverflow)。
TextTrimmingTextTrimming定义文本如何被截断以适应可用区域,当不换行时使用(None, CharacterEllipsis, WordEllipsis)。
FontFamilyFontFamily设置文本的字体族。
FontSizedouble设置文本的字号,单位是设备无关像素。
FontWeightFontWeight设置文本的粗细(如 Normal, Bold, Light 等)。
ForegroundBrush设置文本的颜色。
BackgroundBrush设置 TextBlock 控件的背景色或背景图片。
<StackPanel Margin="10">
    <TextBlock Text="这是一个简单的文本显示。" FontSize="14" Foreground="DarkBlue"/>
    <TextBlock Margin="0,10,0,0" TextWrapping="Wrap" TextTrimming="CharacterEllipsis">
        这是一段很长的文本,当空间不足时,它会自动换行。如果 TextTrimming 设置为 CharacterEllipsis,则会在文本末尾显示省略号。
    </TextBlock>
    <TextBlock Margin="0,10,0,0">
        混合样式: <Bold>粗体</Bold>, <Italic>斜体</Italic>, <Run Foreground="Red">红色</Run>。
    </TextBlock>
</StackPanel>

3. 编辑框 TextBox

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
Textstring获取或设置文本框当前显示的文本内容。用户可以输入和编辑此属性的值。
IsReadOnlybool设置文本框是否只读(true)或可编辑(false)。只读时用户仍可选择和复制文本。
MaxLengthint限制用户在文本框中可以输入的字符的最大数量。
AcceptsReturnbool控制用户按下 Enter 键时,是否插入换行符(true)或执行默认行为(如提交)。与 TextWrapping="Wrap" 结合实现多行输入。
AcceptsTabbool控制用户按下 Tab 键时,是否插入制表符(true)或将焦点移到下一个控件。
TextWrappingTextWrapping控制文本是否在宽度不足时自动换行(Wrap)。
VerticalScrollBarVisibilityScrollBarVisibility控制当内容超出文本框可见区域时垂直滚动条的显示行为(AutoDisabledHiddenVisible)。
HorizontalScrollBarVisibilityScrollBarVisibility控制当内容超出文本框可见区域时水平滚动条的显示行为。
<StackPanel Margin="10">
    <TextBox x:Name="singleLineTextBox" 
             Text="请在这里输入一行短文本。" 
             MaxLength="50" Width="250" HorizontalAlignment="Left"/>
    
    <TextBox x:Name="multiLineTextBox" 
             Text="这是一个多行文本框。&#x0a;请随意输入多行内容。" 
             AcceptsReturn="True" TextWrapping="Wrap" 
             VerticalScrollBarVisibility="Auto" 
             Height="80" 
             Margin="0,10,0,0" Width="250" HorizontalAlignment="Left"/>
    
    <TextBox Text="这是一个只读文本框。" IsReadOnly="True" 
             Background="LightGray" 
             Margin="0,10,0,0" Width="250" HorizontalAlignment="Left"/>
</StackPanel>

4. 按钮 Button

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
Contentobject设置按钮上显示的文本、图像或其他 UI 元素内容。
IsEnabledbool控制按钮是否可用(true)或禁用(false)。禁用时按钮通常显示为灰色且不可交互。
CommandICommand绑定一个命令接口,当按钮被点击时,此命令将执行。常用于 MVVM 模式。
CommandParameterobject在按钮 Command 执行时,作为参数传递给命令的数据。
IsDefaultbool当设置为 true 时,此按钮在窗口中成为默认按钮。用户按下 Enter 键时若无其他活跃焦点,此按钮会被“点击”。
IsCancelbool当设置为 true 时,此按钮在窗口中成为取消按钮。用户按下 Esc 键时,此按钮会被“点击”。
<Button Content="普通按钮" Grid.Column="0" 
        HorizontalAlignment="Center" VerticalAlignment="Center" 
        Width="80" Height="30"/>

<Button Content="禁用按钮" IsEnabled="False" Grid.Column="1" 
        HorizontalAlignment="Center" VerticalAlignment="Center" 
        Width="80" Height="30"/>

<!-- 带有图标的按钮 -->
<Button Grid.Column="2" HorizontalAlignment="Center" VerticalAlignment="Center" Width="80" Height="30">
    <StackPanel Orientation="Horizontal">
        <Image Source="pack://application:,,,/YourAppName;component/Images/save.png" Width="16" Height="16" Margin="0,0,5,0"/>
        <TextBlock Text="保存"/>
    </StackPanel>
</Button>

5. 单选框 RadioButton

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
Contentobject设置单选按钮旁边显示的文本或其他内容。
IsCheckedbool?获取或设置一个值,指示单选按钮是否被选中(true)或未选中(false)。
GroupNamestring将 RadioButton 归入一个逻辑组。在同一组内,只能有一个 RadioButton 被选中。
<TextBlock Text="请选择您的偏好设置:" Margin="0,15,0,0"/>
<!-- 未设置 GroupName 的 RadioButton 会独立于其他组 -->
<RadioButton Content="启用功能A" Margin="0,5,0,0"/> 
<RadioButton Content="启用功能B" Margin="0,5,0,0"/>

6. CheckBox 复选框

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
Contentobject设置复选框旁边显示的文本或其他内容。
IsCheckedbool?获取或设置一个值,指示复选框是否被选中(true)、未选中(false)或处于不确定状态(null)。
IsThreeStatebool设置复选框是否允许处于不确定(null)状态。如果为 true,点击时会依序在选中、未选中、不确定状态间切换。
<StackPanel Margin="10">
    <CheckBox Content="我同意用户协议" IsChecked="True"/>
    <CheckBox Content="接收营销邮件" IsChecked="False" Margin="0,5,0,0"/>
    <CheckBox Content="开启高级功能 (不确定状态)" IsThreeState="True" IsChecked="{x:Null}" Margin="0,5,0,0"/>
</StackPanel>

四、常用控件(二)

1. 图片控件 Image

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
SourceImageSource指定要显示的图像文件的路径或 URI。支持多种图像格式(PNG, JPG, BMP, GIF等)。
StretchStretch定义图像如何调整大小以适应其可用空间:None (原大小), Fill (填充并可能变形), Uniform (保持比例填充), UniformToFill (保持比例填充并可能裁剪)。
StretchDirectionStretchDirection配合 Stretch 属性,控制图像是只放大、只缩小还是双向调整以适应内容区域。
<StackPanel Margin="10">
    <!-- 假设你项目根目录下有 "my_image.png" 或在 Images 文件夹有 "icon.png" -->
    <Image Source="my_image.png" Width="100" Height="100" Stretch="Uniform"/>
    <Image Source="Images/icon.png" Stretch="Fill" Width="150" Height="80" Margin="0,10,0,0"/>
    <Image Source="pack://application:,,,/YourAppName;component/Assets/logo.jpg" 
           Width="200" Height="50" Stretch="UniformToFill" Margin="0,10,0,0"/>
</StackPanel>

2. 滑块 Slider

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
Valuedouble获取或设置滑块的当前选定值。用户拖动滑块时,此值会更新。
Minimumdouble设置滑块可选择的最小值。
Maximumdouble设置滑块可选择的最大值。Value 始终在 Minimum 和 Maximum 之间。
TickFrequencydouble当刻度线可见时,指定刻度线之间的值间隔。
TickPlacementTickPlacement控制刻度线相对于滑块轨道的显示位置(如 TopLeftBottomRightBothNone)。
IsSnapToTickEnabledbool决定滑块的“拇指”是否只能吸附到 TickFrequency 定义的刻度位置上。
OrientationOrientation设置滑块的方向:Horizontal (水平) 或 Vertical (垂直)。
<TextBlock Text="亮度:" Margin="0,10,0,0"/>
<Slider Orientation="Horizontal" Minimum="0" Maximum="10" Value="5" 
        TickPlacement="BottomRight" TickFrequency="1" IsSnapToTickEnabled="True" 
        Margin="0,5,0,0"/>

<TextBlock Text="垂直滑块:" Margin="0,10,0,0"/>
<Slider Orientation="Vertical" Minimum="0" Maximum="100" Value="25" 
        Width="50" Height="150" VerticalAlignment="Top" HorizontalAlignment="Left" 
        TickPlacement="TopLeft" TickFrequency="25"/>

3. 进度条 ProgressBar

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
Valuedouble获取或设置进度条的当前进度值。
Minimumdouble设置进度条的最小值,通常为 0,表示操作的开始。
Maximumdouble设置进度条的最大值,通常为 100,表示操作的完成。
IsIndeterminatebool当设置为 true 时,进度条显示一个持续动画,表示操作正在进行但具体进度未知。此时 Value 属性被忽略。
OrientationOrientation设置进度条的方向:Horizontal (水平) 或 Vertical (垂直)。
<TextBlock Text="加载中 (不确定):" Margin="0,10,0,0"/>
<!-- 不确定模式,显示动画而非具体进度 -->
<ProgressBar IsIndeterminate="True" Width="200" HorizontalAlignment="Left" Margin="0,5,0,0"/>

<TextBlock Text="垂直进度条:" Margin="0,10,0,0"/>
<ProgressBar Orientation="Vertical" Value="30" Width="30" Height="100" HorizontalAlignment="Left" Margin="0,5,0,0"/>

4. 边框 Border

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
ChildUIElement设置 Border 内部包含的单个 UI 元素。如果需要包含多个,通常将一个布局面板(如 Grid 或 StackPanel)作为 Child
BorderBrushBrush设置边框的颜色或样式(例如 SolidColorBrushLinearGradientBrush)。
BorderThicknessThickness设置边框的粗细度。可以统一设置所有边(如 1),或分别设置(如 1,2,3,4 代表左、上、右、下)。
CornerRadiusCornerRadius设置边框的圆角半径,使边框呈现弧形。
BackgroundBrush设置 Border 内部区域(即 Child 元素所在的区域)的背景颜色或图片。
PaddingThickness设置 Border 内部内容(Child 元素)与其自身边框之间的间距。
<!-- 圆角边框 -->
<Border BorderBrush="Blue" BorderThickness="2" CornerRadius="10" Background="LightBlue" Padding="15"
        Margin="0,15,0,0" HorizontalAlignment="Left">
    <TextBlock Text="圆角边框示例" FontWeight="Bold" Foreground="White"/>
</Border>

<!-- 包含多个子元素的边框 (通过 StackPanel) -->
<Border BorderBrush="Green" BorderThickness="2,0,2,0" Background="LightGreen" Padding="5"
        Margin="0,15,0,0" HorizontalAlignment="Left">
    <StackPanel>
        <TextBlock Text="边框内的" FontWeight="Bold"/>
        <TextBlock Text="多个子元素"/>
        <Button Content="一个按钮"/>
    </StackPanel>
</Border>

5. 内容控件 ContentControl

常用属性 (Property Name)属性类型 (Property Type)作用 (Effect / Description)
Contentobject获取或设置控件显示的核心内容。因为是 object 类型,它可以是任何数据(字符串、自定义对象)或任何单个 UI 元素。
ContentTemplateDataTemplate当 Content 是一个数据对象(而非 UI 元素)时,此模板定义如何将该数据对象渲染成 UI 元素。是数据绑定和 MVVM 的核心概念之一。
ContentTemplateSelectorDataTemplateSelector提供自定义逻辑,在运行时根据 Content 对象的属性或类型,动态选择合适的 DataTemplate 来显示内容。

五、 资源

  • StaticResource 在编译时即确定值,而 DynamicResource 则表示资源在运行时动态计算值
  • WPF 资源访问时按照就近原则(顺序为元素级>页面级>应用级)
  • WPF 资源词典查找顺序由内向外,先查找自己的key-value值,若未找到再去MergedDictionaries中继续查找

六、 绑定

1. 数据绑定概述

数据绑定的本质上是绑定目标和绑定源之间的桥梁。通常,每个绑定都有四个组件

  • 绑定目标对象
  • 目标属性
  • 绑定源
  • 要使用的绑定源中值的路径
  • 如果控件要绑定的值就是源对象,可以使用 Path=. 或则 不指定 Path
  • 绑定中的 Path 可以省略,因为 Binding 元素含有一个带参构造函数

七、 数据绑定 DataContext

  • 一个 UI 元素只能有一个 DataContext

八、 数据绑定方向与 INotifyPropertyChange

1. 数据绑定方向

  • OneWay (单向:源 -> 目标)
    数据流: 从数据源 (ViewModel) 流向 UI 目标 (控件)。
    用途: 主要用于显示数据,如 TextBlockLabel
    例子: ViewModel 的 UserName 属性变了,界面上的 TextBlock 会自动更新。但用户无法直接修改 TextBlock
  • TwoWay (双向:源 <-> 目标)
    数据流: 数据可以在源和目标之间自由流动。
    用途: 主要用于用户输入和编辑,如 TextBoxCheckBoxSlider
    例子:
    • ViewModel 的 UserName 属性变了,TextBox 的内容会更新。
    • 用户在 TextBox 中输入了新名字,ViewModel 的 UserName 属性也会被自动更新。
  • OneWayToSource (单向到源:目标 -> 源)
    数据流: 从 UI 目标 (控件) 流向数据源 (ViewModel)。这是 TwoWay 的反向。
    用途: 比较少见。比如,一个只写的属性,或者当一个控件的状态改变时,我们只想更新后台数据,但不想让后台数据反过来影响这个控件。
  • OneTime (一次性)
    数据流: 在绑定建立时(通常是程序启动时)从源流向目标一次,之后便断开连接。
    用途: 用于显示不会再改变的静态数据,性能开销最小。

2. INotifyPropertyChanged 接口

INotifyPropertyChanged 是 System.ComponentModel 命名空间下的一个接口

public interface INotifyPropertyChanged
{
    event PropertyChangedEventHandler PropertyChanged;
}

a. 第一种用法

public class UserViewModel : INotifyPropertyChanged
{
    // 1. 实现接口要求的事件
    public event PropertyChangedEventHandler PropertyChanged;
    private string _name;
    public string Name
    {
        get { return _name; }
        set
        {
            // 只有当值真正改变时才更新和通知
            if (_name != value)
            {
                _name = value;
                // 3. 当属性值改变后,调用辅助方法来触发事件
                OnPropertyChanged(); 
            }
        }
    }
    private int _age;
    public int Age
    {
        get { return _age; }
        set
        {
            if (_age != value)
            {
                _age = value;
                OnPropertyChanged();
            }
        }
    }
    // 2. 创建一个辅助方法来触发事件
    //    [CallerMemberName] 是一个神奇的特性,它会自动获取调用此方法的属性的名称
    //    所以调用 OnPropertyChanged() 等同于 OnPropertyChanged("Name")
    protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

b. 第二种用法

public class Teacher : INotifyPropertyChanged

{
    private String name;

    public String Name
    {
        get { 
            return name; 
        }
        set { 
            name = value;
            // 内写参数
            OnPropertyChanged("Name");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
	// 令入参可为空
    protected virtual void OnPropertyChanged(String propertyName="") 
    {
        PropertyChanged?.Invoke( this, new PropertyChangedEventArgs( propertyName ) );
    }
}

九、 依赖属性

1. 什么是依赖属性

一个依赖属性由两部分组成:

  • 一个静态的、只读的 DependencyProperty 字段。 这是属性的“注册证”。它在 WPF 属性系统中注册了这个属性,并定义了它的元数据(如默认值、属性改变时的回调等)。
  • 一个传统的 C# 属性包装器 (CLR Wrapper)。 这是我们平时在代码中直接访问的 get 和 set

关键点: 这个包装器不存储属性值。它的 get 和 set 方法内部只是调用了 DependencyObject 基类的 GetValue() 和 SetValue() 方法,来从 WPF 属性系统中存取值。

using System.Windows;
using System.Windows.Controls;
public class MyCustomControl : Control
{
    // 1. 注册依赖属性 (The "Registration")
    // 这是一个公共的、静态的、只读的字段。
    // 命名约定: 属性名 + "Property"
    public static readonly DependencyProperty MyTextProperty =
        DependencyProperty.Register(
            "MyText",                                 // 属性名 (字符串形式)
            typeof(string),                           // 属性的数据类型
            typeof(MyCustomControl),                  // 拥有此属性的类的类型
            new PropertyMetadata("default value")     // 元数据: 包含默认值等信息
        );
    // 2. 提供 CLR 属性包装器 (The "Wrapper")
    // 这是我们在代码中实际使用的属性
    public string MyText
    {
        // get 和 set 内部只是调用基类的方法
        get { return (string)GetValue(MyTextProperty); }
        set { SetValue(MyTextProperty, value); }
    }
}

十、 布局与附加属性

1. 网格布局 Grid

1.1. 常用属性

属性名 (Property Name)说明 (Description)
定义网格结构 (在 <Grid> 标签上)
RowDefinitions行定义集合。用于在内部添加一个或多个 <RowDefinition> 来定义网格的行。
ColumnDefinitions列定义集合。用于在内部添加一个或多个 <ColumnDefinition> 来定义网格的列。
Height (在 RowDefinition 上)定义行高。可以是固定像素值 (如 100)、自动适应内容 (Auto) 或按比例占据剩余空间 (*2* 等)。
Width (在 ColumnDefinition 上)定义列宽。规则与 Height 相同,可以是像素、Auto 或 * 比例。
ShowGridLines显示网格线。一个布尔值 (True 或 False)。设置为 True 时会显示所有行和列的辅助线,非常适合在设计和调试时使用。
放置子控件 (在 Grid 的子控件上,作为附加属性)
Grid.Row指定控件所在的行索引。索引从 0 开始。如果不指定,默认为 0
Grid.Column指定控件所在的列索引。索引从 0 开始。如果不指定,默认为 0
Grid.RowSpan指定控件纵向跨越的行数。例如,Grid.RowSpan="2" 表示控件将占据两行的高度。
Grid.ColumnSpan指定控件横向跨越的列数。例如,Grid.ColumnSpan="3" 表示控件将占据三列的宽度。
子控件在单元格内的对齐 (在子控件上)
HorizontalAlignment水平对齐方式。决定控件在其所在单元格内的水平位置。可选值:LeftRightCenterStretch (默认, 水平撑满)。
VerticalAlignment垂直对齐方式。决定控件在其所在单元格内的垂直位置。可选值:TopBottomCenterStretch (默认, 垂直撑满)。
Margin外边距。设置控件与其所在单元格边界之间的距离,可以用来创建控件周围的空白区域。

1.2. 示例

<!-- 步骤 1: 定义列的结构 -->
<Grid.ColumnDefinitions>
    <!-- 第0列: 用于放置标签。Width="Auto" 使其宽度自动适应最长的标签文字。 -->
    <ColumnDefinition Width="Auto"/>

    <!-- 第1列: 用于放置输入框。Width="*" 使其占据所有剩余的水平空间。-->
    <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>

<!-- 步骤 2: 定义行的结构 -->
<Grid.RowDefinitions>
    <RowDefinition Height="Auto"/>      <!-- 第0行: 标题行,高度自适应内容。 -->
    <RowDefinition Height="Auto"/>      <!-- 第1行: 姓氏输入行,高度自适应。 -->
    <RowDefinition Height="Auto"/>      <!-- 第2行: 名字输入行,高度自适应。 -->
    <RowDefinition Height="Auto"/>      <!-- 第3行: 邮箱输入行,高度自适应。 -->
    <RowDefinition Height="*"/>         <!-- 第4行: 备注区,高度占据所有剩余的垂直空间,实现响应式。 -->
    <RowDefinition Height="50"/>        <!-- 第5行: 按钮行,固定高度50。 -->
</Grid.RowDefinitions>

<!-- 步骤 3: 放置控件到指定的单元格 -->

<!-- 标题: 位于第0行,从第0列开始,并横跨2列 (ColumnSpan="2") -->
<TextBlock Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" 
           Text="创建新用户" FontSize="20" FontWeight="Bold" 
           HorizontalAlignment="Center" Margin="0,0,0,20"/>

<!-- 姓氏 (Last Name) -->
<Label Grid.Row="1" Grid.Column="0" Content="姓氏(L):" VerticalAlignment="Center"/>
<TextBox Grid.Row="1" Grid.Column="1" Margin="5" VerticalAlignment="Center"/>

<!-- 名字 (First Name) -->
<Label Grid.Row="2" Grid.Column="0" Content="名字(F):" VerticalAlignment="Center"/>
<TextBox Grid.Row="2" Grid.Column="1" Margin="5" VerticalAlignment="Center"/>

<!-- 邮箱 (Email) -->
<Label Grid.Row="3" Grid.Column="0" Content="电子邮件:" VerticalAlignment="Center"/>
<TextBox Grid.Row="3" Grid.Column="1" Margin="5" VerticalAlignment="Center"/>

<!-- 备注 (Notes) -->
<Label Grid.Row="4" Grid.Column="0" Content="备注:" VerticalAlignment="Top" Margin="0,5,0,0"/>
<TextBox Grid.Row="4" Grid.Column="1" Margin="5"
         TextWrapping="Wrap" AcceptsReturn="True" VerticalScrollBarVisibility="Auto"/>

<!-- 按钮组: 放置在一个 StackPanel 中,然后将 StackPanel 放置在单元格中 -->
<StackPanel Grid.Row="5" Grid.Column="1" Orientation="Horizontal" 
            HorizontalAlignment="Right" VerticalAlignment="Center">
    <Button Content="提交" Width="80" Margin="5"/>
    <Button Content="取消" Width="80" Margin="5"/>
</StackPanel>

2. 统一网格 UniformGrid

2.1. 常用属性

属性名说明
Rows指定网格的行数。这是一个整数。<br>如果设置了 RowsColumns 会被自动计算。
Columns指定网格的列数。这是一个整数。<br>如果设置了 ColumnsRows 会被自动计算。
FirstColumn指定第一个子控件应该放置在哪一列(索引从 0 开始)。<br>默认是 0。这个属性可以用来创建“偏移”的布局,但使用较少。

2.2. 示例

<UniformGrid Rows="3" Columns="3" FirstColumn="1">
    <Button Content="Button1" />
    <Button Content="Button2" />
    <Button Content="Button3" />
    <Button Content="Button4" />
    <Button Content="Button5" />
    <Button Content="Button6" />
    <Button Content="Button7" />
    <Button Content="Button8" />
</UniformGrid>

3. 堆叠面板 StackPanel

3.1. 常用属性

属性名说明
Orientation获取或设置一个值,该值指示子元素的堆叠维度。
Horizontal 水平,Vertical 垂直

3.2. 示例

<StackPanel Orientation="Horizontal">
    <Button Content="Button"/>
    <Button Content="Button"/>
    <Button Content="Button"/>
    <Button Content="Button"/>
</StackPanel>

4. 包裹面板 WrapPanel

4.1. 常用属性

属性名说明
Orientation决定了子元素的排列和换行方向。这是最重要的属性。<br>• Horizontal (默认值): 元素从左到右排列。当碰到容器的右边界时,换到下一行,并从左边重新开始。<br>• Vertical: 元素从上到下排列。当碰到容器的下边界时,换到下一列,并从顶部重新开始。
ItemWidth为 WrapPanel 中的所有子元素设置一个统一的宽度。如果你设置了这个值,它会覆盖子元素自身的 Width 属性。
ItemHeight为 WrapPanel 中的所有子元素设置一个统一的高度。如果你设置了这个值,它会覆盖子元素自身的 Height 属性。

4.2. 示例

<WrapPanel Orientation="Horizontal" ItemHeight="30" ItemWidth="30">
    <Button Content="Button"/>
    <Button Content="Button"/>
    <Button Content="Button"/>
    <Button Content="Button"/>
</WrapPanel>

5. 停靠面板 DockPanel

5.1. 常用属性

属性名类型说明
DockPanel.Dock附加属性这是最重要的属性,写在 DockPanel 的子控件上。它告诉 DockPanel 应该把这个子控件停靠在哪个方向。<br>可选值:LeftRightTopBottom
LastChildFill自身属性 (布尔值)这个属性写在 <DockPanel> 标签上。决定了最后一个子元素是否应该自动填充剩余空间。<br>• True (默认值): 最后一个子元素自动填充。<br>• False: 最后一个子元素不会填充,它会根据自身的对齐属性来定位,剩余空间会留白。

5.2. 示例

<DockPanel LastChildFill="True">
    <Button Content="Button" DockPanel.Dock="Top"/>
    <Button Content="Button" DockPanel.Dock="Left"/>
    <Button Content="Button" DockPanel.Dock="Right"/>
    <Button Content="Button" DockPanel.Dock="Bottom"/>
</DockPanel>

6. 画布面板 Canvas

6.1. 常用属性

属性名说明
Canvas.Left指定子元素左上角相对于 Canvas 左边缘的距离 (X 坐标)。
Canvas.Top指定子元素左上角相对于 Canvas 上边缘的距离 (Y 坐标)。
Canvas.Right指定子元素右边缘相对于 Canvas 右边缘的距离。
Canvas.Bottom指定子元素下边缘相对于 Canvas 下边缘的距离。

6.2. 示例

<Canvas>
    <Button Content="1" Canvas.Left="100"/>
    <Button Content="2" Canvas.Left="200"/>
    <Button Content="3" Canvas.Top="100"/>
    <Button Content="4" Canvas.Bottom="200"/>
</Canvas>

十一、 数据模板DataTemplate与数据模板选择器DataTemplateSelector

<ContentControl Margin="220,70,345,303">
    <local:Teacher Name="张三老师" Desc="擅长高等数学"></local:Teacher>
</ContentControl>
<ContentControl Margin="220,151,345,217">
    <local:Student Name="学生" Age="20"/>
</ContentControl>
<ContentControl Margin="220,249,345,123">
    <local:Student Name="学生" Age="20" />
</ContentControl>

如果不提供 DataTemplate, 对于复杂数据,控件显示数据会默认调用对象的 Tostring() 方法转为字符串显示

1. 数据模板的三种用法

1.1. 在资源中描述 Teacher 类型数据显示的数据模板资源

    <Window.Resources>
        <!--在资源中描述 Teacher类型数据显示的数据模板资源-->
        <DataTemplate DataType="{x:Type local:Teacher}">
            <Border BorderBrush="red" BorderThickness="3">
                <StackPanel>
                    <TextBlock Text="{Binding Name}" />
                    <TextBlock Text="{Binding Desc}" />
                </StackPanel>
            </Border>
        </DataTemplate>
    </Window.Resources>

1.2. 在控件中定义数据模板

        <ContentControl Margin="220,151,345,217">
            <ContentControl.ContentTemplate>
                <DataTemplate>
                    <Border BorderBrush="Green" Background="Yellow" BorderThickness="3">
                        <StackPanel>
                            <Button Content="{Binding Name}" />
                            <Button Content="{Binding Age}" />
                        </StackPanel>
                    </Border>
                </DataTemplate>
            </ContentControl.ContentTemplate>
            <local:Student Name="学生" Age="20" />
        </ContentControl>

1.3. 在资源中的数据模板定义 Key

<Window.Resources>
    <!--在资源中的数据模板定义Key-->
    <DataTemplate x:Key="myDataTemplate">
        <Border BorderBrush="Gray" BorderThickness="3">
            <StackPanel >
                <TextBlock Text="{Binding Name}" />
                <ProgressBar Height="10" Value="{Binding Age}" />
            </StackPanel>
        </Border>
    </DataTemplate>
</Window.Resources>

<Grid>
    <ContentControl ContentTemplate="{StaticResource myDataTemplate}" Margin="220,249,345,123">
        <local:Student Name="王五" Age="28" />
    </ContentControl>
</Grid>

2. 数据模板选择器

c# 代码

using System.Windows;
using System.Windows.Controls;
public class Student
{
    public string Name { get; set; }
    public double Score { get; set; }
}
public class StudentTemplateSelector : DataTemplateSelector
{
    public override DataTemplate SelectTemplate(object item, DependencyObject container)
    {
        // 1. 获取当前数据项
        Student student = item as Student;
        if (student == null)
        {
            return base.SelectTemplate(item, container);
        }
        // 2. 获取承载模板的容器 (例如 ListBox)
        FrameworkElement element = container as FrameworkElement;
        if (element != null && student != null)
        {
            // 3. 根据 Student 对象的 Score 属性来选择模板
            if (student.Score >= 90)
            {
                // 从容器的资源中查找名为 "ExcellentStudentTemplate" 的模板
                return element.FindResource("ExcellentStudentTemplate") as DataTemplate;
            }
            else if (student.Score < 60)
            {
                return element.FindResource("WarningStudentTemplate") as DataTemplate;
            }
        }
        // 默认情况下,返回 null,WPF 会寻找默认模板
        return null; 
        // 或者也可以返回一个默认模板: return element.FindResource("NormalStudentTemplate") as DataTemplate;
    }
}

xaml 代码

<Window.Resources>
    <!-- 优秀学生的模板 (金色背景) -->
    <DataTemplate x:Key="ExcellentStudentTemplate">
        <Border Background="Gold" BorderBrush="Goldenrod" BorderThickness="1" CornerRadius="4" Padding="8" Margin="2">
            <StackPanel>
                <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                <TextBlock Text="{Binding Score, StringFormat='分数: {0}'}" />
            </StackPanel>
        </Border>
    </DataTemplate>
    <!-- 警告学生的模板 (黄色背景) -->
    <DataTemplate x:Key="WarningStudentTemplate">
        <Border Background="LightYellow" BorderBrush="Orange" BorderThickness="1" CornerRadius="4" Padding="8" Margin="2">
            <StackPanel>
                <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                <TextBlock Text="{Binding Score, StringFormat='分数: {0}'}" Foreground="Red" />
            </StackPanel>
        </Border>
    </DataTemplate>
    <!-- 为普通学生准备一个隐式模板,作为默认选项 -->
    <DataTemplate DataType="{x:Type local:Student}">
        <Border Background="WhiteSmoke" BorderBrush="Gray" BorderThickness="1" CornerRadius="4" Padding="8" Margin="2">
            <StackPanel>
                <TextBlock Text="{Binding Name}" FontWeight="Bold" />
                <TextBlock Text="{Binding Score, StringFormat='分数: {0}'}" />
            </StackPanel>
        </Border>
    </DataTemplate>
    <!-- 实例化我们的选择器类 -->
    <local:StudentTemplateSelector x:Key="MyStudentSelector" />
</Window.Resources>
<!-- 然后,在 ListBox 中使用它 -->
<ListBox ItemsSource="{Binding StudentList}" HorizontalContentAlignment="Stretch">
    <!-- 
      使用 ItemTemplateSelector 属性,而不是 ItemTemplate。
      它会为列表中的每一项调用 MyStudentSelector 的 SelectTemplate 方法。
    -->
    <ListBox.ItemTemplateSelector>
        <StaticResource ResourceKey="MyStudentSelector"/>
    </ListBox.ItemTemplateSelector>
</ListBox>

十二、 下拉框控件 ComboBox 与数据模板

1. 常用属性

1.1. 数据填充属性

属性描述
ItemsSource最重要的属性。用于绑定一个数据集合(如 List<T>ObservableCollection<T>)。这是实现数据驱动 UI 的核心。
Items一个 ItemCollection 集合。当你不使用 ItemsSource 时,可以通过它直接添加或移除项(如 <ComboBoxItem>)。不推荐在业务代码中频繁操作,主要用于静态的 XAML 定义。
HasItems(只读) 一个布尔值,表示 ComboBox 中是否有任何项。

1.2. 显示与模版属性

属性描述
DisplayMemberPath最快捷的显示方式。指定 ItemsSource 集合中对象的某个属性路径(如 "Name"),用该属性的文本值作为列表项的显示内容。
ItemTemplate最灵活的显示方式。接收一个 DataTemplate,用于自定义下拉列表中每一项的复杂外观(比如同时显示图片、姓名、分数等)。 DisplayMemberPath 和 ItemTemplate 通常只用其一。
ItemStringFormat一个格式化字符串,用于格式化由 DisplayMemberPath 指定的显示内容。例如,对于日期可以设置为 "{}{0:yyyy-MM-dd}"
ItemTemplateSelector数据模板选择器。当你想根据数据项的不同(如学生分数高低),为同一列表中的项应用不同的 DataTemplate 时使用。

1.3. 选择相关属性

属性描述
SelectedItemMVVM 模式下的首选。获取或设置被选中的整个数据对象。例如,如果 ItemsSource 绑定了 List<Student>,那么 SelectedItem 就是那个被选中的 Student 对象。
SelectedIndex获取或设置被选中项的索引int 类型,从 0 开始)。值为 -1 表示没有选中项。适合简单场景或需要按位置操作的场景。
SelectedValue获取或设置由 SelectedValuePath 指定的特定属性的值
SelectedValuePath与 SelectedValue 配套使用。指定当一项被选中时,SelectedValue 属性应该获取该项对象的哪个属性的值。例如,你可以在下拉框显示学生姓名 (DisplayMemberPath="Name"),但你实际需要获取的是他的学号 (SelectedValuePath="Id")。

1.4. 行为与外观属性

属性描述
IsEditable布尔值。如果为 TrueComboBox 的头部会变成一个可编辑的 TextBox,允许用户输入文本。默认为 False
Text当 IsEditable="True" 时,此属性获取或设置编辑框中的文本。你也可以通过绑定它来实现搜索和自动完成功能。
IsDropDownOpen布尔值。获取或设置下拉列表是否展开。你可以通过绑定或在代码中设置它来手动打开或关闭下拉框。
StaysOpenOnEdit布尔值。当 IsEditable="True" 时,如果此属性为 True,则在用户编辑文本时下拉列表保持打开状态。
MaxDropDownHeight设置下拉列表展开时的最大高度。超出部分会出现滚动条。
IsEnabledWidthHeightMarginFontSizeForeground 等标准的 FrameworkElement 属性,用于控制控件的基本尺寸、位置、可用状态和字体样式。

1.5. 常用事件

事件描述
SelectionChanged最重要的事件。当 SelectedItem 发生改变时触发。这是在后台代码 (code-behind) 中响应用户选择的主要方式。
DropDownOpened当下拉列表被打开时触发。
DropDownClosed当下拉列表被关闭时触发。

2. 填写内容的三种实现

    <Grid>
        <!--ComboBox.ItemsSource实现-->
        <ComboBox x:Name="comboBox1"  HorizontalAlignment="Left" Margin="285,89,0,0" VerticalAlignment="Top" Width="216" Height="27" FontSize="20">
            <ComboBox.ItemsSource>
                <Collections:ArrayList>
                    <system:String>高等数学</system:String>
                    <system:String>线性代数</system:String>
                    <system:String>数率统计</system:String>
                    <Rectangle Fill="red" Width="30" Height="20"/>
                </Collections:ArrayList>
            </ComboBox.ItemsSource>
        </ComboBox>

        <!--ComboBoxItem实现-->
        <ComboBox x:Name="comboBox2"  HorizontalAlignment="Left" Margin="285,160,0,0" VerticalAlignment="Top" Width="216" Height="27" FontSize="20">
            <ComboBoxItem>美术</ComboBoxItem>
            <ComboBoxItem>音乐</ComboBoxItem>
            <ComboBoxItem>体育</ComboBoxItem>
        </ComboBox>

        <!--C#代码实现-->
        <ComboBox x:Name="comboBox3"  HorizontalAlignment="Left" Margin="285,229,0,0" VerticalAlignment="Top" Width="216" Height="27" FontSize="20"/>
    </Grid>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        List<string> list = new List<string>();
        list.Add("C++");
        list.Add("Java");
        list.Add("Python");
        //数据源赋值
        comboBox3.ItemsSource = list;
    }
}