c#软件开发学习笔记--WPF(Canvas、数据绑定、MVVM模式、值转换器)

WPF 学习总结


一、Canvas 绘图

1.1 概述

Canvas 是 WPF 中的一个布局容器,允许通过绝对坐标(Canvas.LeftCanvas.Top)精确定位子元素,适合绘制自定义图形和仪表盘等场景。

1.2 常用绘图元素

Ellipse(椭圆/圆)
xml 复制代码
<!-- 外圆 -->
<Ellipse Canvas.Left="0" Canvas.Top="0" Width="300" Height="300"
         Fill="Black" Stroke="Black" StrokeThickness="20" />

<!-- 内圆 -->
<Ellipse Canvas.Left="20" Canvas.Top="20" Width="260" Height="260"
         Fill="Black" Stroke="Yellow" StrokeThickness="2" />
Line(线条)
xml 复制代码
<!-- 绘制刻度线 -->
<Line Stroke="White" StrokeThickness="3"
      X1="150" X2="150" Y1="30" Y2="50" />
Path + ArcSegment(弧形)
xml 复制代码
<!-- 使用 Path 绘制弧形 -->
<Path Fill="Transparent" Stroke="Red" StrokeThickness="30">
    <Path.Data>
        <PathGeometry>
            <PathFigure StartPoint="150,30">
                <ArcSegment IsLargeArc="False" Point="270,150"
                            Size="135,135" SweepDirection="Clockwise" />
            </PathFigure>
        </PathGeometry>
    </Path.Data>
</Path>
  • Path:绘制任意线条和图形的控件
  • PathGeometry:描述几何形状数据,由多段路径组合而成
  • PathFigure:实现一段或多段路径
  • ArcSegment:绘制弧形的段
  • 关键属性:StartPoint(起始点)、Point(弧形结束位置)、Size(半径)、SweepDirection(方向:Clockwise 顺时针 / Counterclockwise 逆时针)、IsLargeArc(大弧/小弧)

二、静态资源与动态资源

2.1 概念对比

类型 关键字 加载时机 运行时更新 适用场景
静态资源 StaticResource 窗口加载时 不更新 固定不变的资源
动态资源 DynamicResource 实际使用时 运行时更新 需要动态切换的资源

三、数据绑定

3.1 绑定基础

绑定到元素
xml 复制代码
<Slider x:Name="slider2" />
<TextBox Text="{Binding ElementName=slider2, Path=Value, Mode=OneWay}" />
绑定到数据源(非元素)

方式一:创建数据上下文(XAML 中,推荐,有智能提示)

xml 复制代码
<!-- 引用命名空间 -->
xmlns:local="clr-namespace:_3数据绑定"

<!-- 创建上下文对象 -->
<Window.DataContext>
    <local:Student />
</Window.DataContext>

方式二:代码中设置 DataContext

csharp 复制代码
// MainWindow.xaml.cs
Personcs p1 = new Personcs();
this.DataContext = p1;
xml 复制代码
<!-- 配合设计时数据上下文获取智能提示 -->
d:DataContext="{d:DesignInstance Type=local:Personcs}"

3.2 五种绑定模式

模式 描述 使用场景
Default 默认绑定,根据目标属性决定 常规绑定
OneWay 单向:数据源 → UI 数据变化需更新界面
TwoWay 双向:数据源 ↔ UI 表单输入等双向同步
OneTime 一次性:数据源 → UI(仅初始化时) 只需初始值的静态展示
OneWayToSource 单向:UI → 数据源 UI 变化需更新数据源

3.3 INotifyPropertyChanged 接口

实现该接口后,当属性值变化时通知 UI 更新。

方式一:手动传递属性名

csharp 复制代码
public class Personcs : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string name = "张三";
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            OnPropertyChanged("Name"); // 手动传属性名
        }
    }
}

方式二:使用 [CallerMemberName](推荐,更简洁)

csharp 复制代码
public class Personcs1 : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    // CallerMemberName 让编译器自动传入属性名
    public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string name = "张三";
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            OnPropertyChanged(); // 无需传参
        }
    }
}

3.4 绑定实体类属性

xml 复制代码
<StackPanel>
    <WrapPanel>
        <Label Content="用户" />
        <TextBox Width="100" Height="30" Text="{Binding Path=Name}" />
    </WrapPanel>
    <WrapPanel>
        <Label Content="年龄" />
        <TextBox Width="100" Height="30" Text="{Binding Age}" />
    </WrapPanel>
    <WrapPanel>
        <Label Content="性别" />
        <ComboBox Width="100" ItemsSource="{Binding Sex}" />
    </WrapPanel>
    <WrapPanel>
        <Label Content="出生日期" />
        <DatePicker Width="150" Text="{Binding BirthDay}" />
    </WrapPanel>
</StackPanel>

四、MVVM 模式

4.1 MVVM 理论

MVVM 分为三层,各司其职,完全解耦:

复制代码
┌──────────┐    数据绑定    ┌────────────┐    操作/通知    ┌──────────┐
│   View   │ ◄──────────► │ ViewModel  │ ◄────────────► │   Model  │
│  (界面)   │              │  (视图模型)  │               │  (数据模型) │
└──────────┘              └────────────┘               └──────────┘
  • View(视图):XAML 界面,负责展示和交互,不写业务逻辑
  • ViewModel(视图模型):中间桥梁,封装数据、命令、业务逻辑,不持有 View 引用
  • Model(模型):数据实体、数据访问、业务模型

数据流向 :Model → ViewModel → View(数据展示)

用户操作:View → ViewModel → Model(数据修改)

完整流程

  1. 用户操作界面 View → Binding 将数据传给 ViewModel
  2. ViewModel 做业务处理
  3. ViewModel 执行 Update,将新数据写入 Model
  4. Model 数据改变,Notify 通知 ViewModel
  5. ViewModel 触发 PropertyChanged 事件,通知绑定,刷新 View 界面

4.2 ViewModelBase(基类)

csharp 复制代码
public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

4.3 RelayCommand(ICommand 实现)

csharp 复制代码
public class RelayCommand : ICommand
{
    private Predicate<object> _canExecute;  // 控制按钮是否可执行
    private Action<object> _excute;         // 按钮点击回调逻辑

    // 构造函数:传入控制条件和执行逻辑
    public RelayCommand(Predicate<object> canExecute, Action<object> excute)
    {
        this._excute = excute;
        this._canExecute = canExecute;
    }

    // 构造函数:始终可执行
    public RelayCommand(Action<object> excute)
    {
        this._canExecute = (o) => true;
        this._excute = excute;
    }

    // 使用 CommandManager.RequerySuggested 自动刷新按钮状态
    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public bool CanExecute(object parameter) => _canExecute(parameter);
    public void Execute(object parameter) => _excute(parameter);
}

4.4 Model(数据模型)

csharp 复制代码
public class Student : ViewModelBase
{
    public int Id { get; set; }

    private string name;
    public string Name
    {
        get { return name; }
        set { name = value; OnPropertyChanged(); }
    }

    public int Age { get; set; }
}

4.5 ViewModel(核心逻辑)

csharp 复制代码
public class MainViewModel : ViewModelBase
{
    private ObservableCollection<Student> _allStudents;
    private ObservableCollection<Student> students;

    public ObservableCollection<Student> Students
    {
        get { return students; }
        set { students = value; OnPropertyChanged(); }
    }

    // 构造时初始化数据
    public MainViewModel()
    {
        students = new ObservableCollection<Student>(MockDataHelper.initData);
        _allStudents = new ObservableCollection<Student>(MockDataHelper.initData);
    }

    // 添加命令
    public RelayCommand AddCommand => new RelayCommand((obj) =>
    {
        Student stu = new Student() { Id = _nextId++, Name = "Foo", Age = 20 };
        _allStudents.Add(stu);
        DoFitter();
    });

    // 带条件的查询命令
    public RelayCommand SearchCommand => new RelayCommand(
        (obj) => !string.IsNullOrEmpty(searchName), // 仅在搜索框不为空时可执行
        (obj) => { DoFitter(); }
    );

    // 筛选方法
    public void DoFitter()
    {
        IEnumerable<Student> list = _allStudents;
        if (!string.IsNullOrEmpty(SearchName))
            list = _allStudents.Where(s => s.Name.Contains(SearchName));
        Students = new ObservableCollection<Student>(list);
    }
}

4.6 View 绑定(XAML)

xml 复制代码
<Window.DataContext>
    <vm:MainViewModel />
</Window.DataContext>

<!-- 绑定命令 -->
<Button Content="查询" Command="{Binding SearchCommand}" />
<Button Content="添加" Command="{Binding AddCommand}" />

<!-- 绑定输入框,实时更新 -->
<TextBox Text="{Binding SearchName, UpdateSourceTrigger=PropertyChanged}" />

<!-- DataGrid 绑定集合 -->
<DataGrid ItemsSource="{Binding Students}" AutoGenerateColumns="False">
    <DataGridTemplateColumn Header="编号">
        <DataGridTemplateColumn.CellTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Id}" HorizontalAlignment="Center" />
            </DataTemplate>
        </DataGridTemplateColumn.CellTemplate>
    </DataGridTemplateColumn>
</DataGrid>

4.7 DataGrid 单元格内命令绑定(RelativeSource)

xml 复制代码
<!-- 在 DataGrid 单元格模板中绑定 ViewModel 的命令 -->
<Button Content="修改"
        Command="{Binding DataContext.CellEditCommand,
                  RelativeSource={RelativeSource AncestorType=DataGrid}}"
        CommandParameter="{Binding}" />
  • RelativeSource AncestorType=DataGrid:向上查找父级 DataGrid,获取其 DataContext(即 MainViewModel)
  • CommandParameter="{Binding}":传递当前行绑定的 Student 对象作为参数

4.8 ObservableCollection(可观察集合)

  • ObservableCollection<T> 是 WPF 中可被监听的集合
  • 当添加或删除元素时,UI 会自动更新
  • 普通 List<T> 的 Add/Remove 操作不会被 UI 监听

五、值转换器

5.1 单值转换器(IValueConverter)

实现 IValueConverter 接口,将 bool 转为 string 显示文本。

csharp 复制代码
public class BoolToTextConvert : IValueConverter
{
    // 数据源 → UI(bool → string)
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool b)
            return b ? "正常" : "禁用";
        return "未知";
    }

    // UI → 数据源(string → bool)
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        string text = value as string;
        if (text == "正常") return true;
        if (text == "禁用") return false;
        return Binding.DoNothing;
    }
}

XAML 中使用

xml 复制代码
<!-- 引入转换器资源 -->
<Window.Resources>
    <local:BoolToTextConvert x:Key="BoolToText" />
</Window.Resources>

<!-- 使用转换器 -->
<TextBox Text="{Binding IsNormal, Converter={StaticResource BoolToText}}" />

5.2 多值转换器(IMultiValueConverter)

实现 IMultiValueConverter 接口,将多个值合并为一个。

csharp 复制代码
public class NameAgeMul : IMultiValueConverter
{
    // 多个值 → 合并为一个字符串
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        string name = values[0].ToString();
        int age = (int)values[1];
        return $"姓名:{name},年龄:{age}";
    }

    // 字符串 → 拆分为多个值
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        string str = value.ToString();
        string[] arr = str.Split(',');
        string name = arr[0].Split(':')[1];
        string age = arr[1].Split(':')[1];
        return new object[] { name, int.Parse(age) };
    }
}

XAML 中使用 MultiBinding

xml 复制代码
<Window.Resources>
    <local:NameAgeMul x:Key="NameAge" />
</Window.Resources>

<TextBlock>
    <TextBlock.Text>
        <MultiBinding Converter="{StaticResource NameAge}">
            <Binding Path="Name" />
            <Binding Path="Age" />
        </MultiBinding>
    </TextBlock.Text>
</TextBlock>

5.3 转换器使用步骤总结

  1. 定义转换器 :实现 IValueConverter(单值)或 IMultiValueConverter(多值)接口
  2. 引入 XAML:先导入命名空间,再定义为资源
  3. 应用转换器 :在标签中使用 Converter="{StaticResource 资源Key}"

相关推荐
茯苓gao13 分钟前
从零开发 EtherCAT 主站(六):SOEM 初始化流程详解,主站是如何发现所有从站的?
笔记·嵌入式硬件·学习·信息与通信
老当益壮梁奶奶25 分钟前
Linux软件编程学习笔记(九):消息队列、共享内存与信号灯详解
linux·c语言·笔记·学习
MartinYeung527 分钟前
[论文学习]激活差异揭示后门:SAE架构对比研究
人工智能·学习·架构
小雪崩30 分钟前
嵌入式学习 day35:进程间通信-消息队列、共享空间及有名信号量
linux·c语言·学习
从零开始的嵌入式之旅43 分钟前
day33
linux·c语言·经验分享·笔记·嵌入式硬件
淡海水2 小时前
05-03-栈队列-PriorityQueue-TElement-TPriority-NET6优先队列语义与四叉堆实现
服务器·前端·c#·priorityqueue·clr·telement
一条破秋裤2 小时前
STM32 学习笔记:STM32 简介与学习路线
笔记·stm32·学习
念何架构之路2 小时前
Gin辅助模块与学习路径
学习·gin
具身AGI2 小时前
基座模型架构之争,物理AI 人类学习路线 的双脑答案
人工智能·学习·架构