WPF 入门实战笔记 --- 监控面板项目
一、项目背景
一个基于 WPF .NET Framework 4.7.2 的 PLC 监控面板雏形,通过 ItemsControl 批量生成轴控制行(Label + TextBox + Button),为后续写入 PLC 做准备。
二、项目文件结构
Monitor_Plc/
├── App.xaml ← 应用程序入口 (XAML)
├── App.xaml.cs ← 应用程序入口 (C# 代码后置)
├── MainWindow.xaml ← 主窗口界面 (XAML)
├── MainWindow.xaml.cs ← 主窗口逻辑 (C# 代码后置)
├── Monitor_Plc.csproj ← 项目配置 (.NET Framework 4.7.2)
└── Images/ ← 图片资源文件夹
注意 :WPF (.NET Framework) 项目没有 Program.cs ,入口点由编译系统自动生成,内部会创建
App实例并调用Run()。
三、App.xaml --- 应用程序入口(完整注释)
xml
<!--
Application 是 WPF 应用程序的根对象
x:Class 指向 C# 中的 App 类,两者通过 partial class 合并
StartupUri 指定启动时自动打开哪个窗口
-->
<Application x:Class="Monitor_Plc.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Monitor_Plc"
StartupUri="MainWindow.xaml">
<Application.Resources>
<!-- 全局资源定义处,当前为空 -->
</Application.Resources>
</Application>
四、App.xaml.cs --- 应用程序入口代码后置
csharp
using System;
using System.Windows;
namespace Monitor_Plc
{
/// <summary>
/// App.xaml 的交互逻辑
/// </summary>
public partial class App : Application
{
// WPF 项目没有 Program.cs,入口点由编译系统自动生成
// 编译时自动生成 Main() 方法:
// [STAThread]
// static void Main()
// {
// App app = new App();
// app.InitializeComponent();
// app.Run(); // 启动消息循环
// }
}
}
五、MainWindow.xaml --- 主窗口界面(完整注释)
xml
<!--
Window 是 WPF 窗口的根元素
x:Class 绑定到 C# 中的 MainWindow 类
xmlns 声明 XAML 命名空间:
- presentation : WPF 核心控件 (Button, Label, Grid 等)
- x : XAML 语言特性 (x:Class, x:Name, x:Type 等)
- local : 当前项目的 C# 命名空间 (Monitor_Plc)
-->
<Window x:Class="Monitor_Plc.MainWindow"
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:Monitor_Plc"
mc:Ignorable="d"
Title="Home" Height="800" Width="1000">
<!-- ===== 窗口背景:图片背景 ===== -->
<Window.Background>
<!--
ImageBrush: 用图片填充背景区域
ImageSource: 图片路径(相对路径,生成操作需设为 Resource)
Opacity: 透明度 (0~1), 1=完全不透明, 0=完全透明
Stretch: 图片拉伸方式
-->
<ImageBrush ImageSource="Images/杀戮天使.jpg" Opacity="0.9"
Stretch="UniformToFill" />
<!--
Stretch 取值说明:
Fill → 拉伸填满整个区域(可能变形)
Uniform → 等比例缩放,完整显示(可能有黑边)
UniformToFill → 等比例缩放,填满区域(可能裁剪边缘,最常用)
-->
</Window.Background>
<!-- ===== 根布局容器 Grid ===== -->
<Grid>
<!--
ItemsControl --- 数据项列表容器
核心机制:根据 ItemsSource 绑定的数据列表,自动为每条数据生成 UI
内部默认用 StackPanel 垂直排列每一条数据项
不需要手动写 RowDefinitions,每行是 ItemsControl 自动生成的
-->
<ItemsControl Name="AxisItems">
<!--
ItemTemplate --- 项模板
定义每条数据的显示样式,所有数据共用这一个模板
注意拼写:ItemsControl.ItemTemplate(有 s)
-->
<ItemsControl.ItemTemplate>
<!--
DataTemplate --- 数据模板
里面写的控件会为每条数据重复生成一份
-->
<DataTemplate>
<!--
Margin: 外边距,语法 "左,上,右,下" 或 "上下,左右"
这里 Margin="0,5" → 上下各留 5px,行与行之间不紧贴
-->
<Grid Margin="0,5">
<Grid.ColumnDefinitions>
<!--
ColumnDefinition 定义列:
Auto → 宽度由内容撑开
* → 占满剩余空间(可写 2* 表示两倍)
数字 → 固定像素,如 Width="200"
-->
<ColumnDefinition Width="Auto"/> <!-- 第0列:Label -->
<ColumnDefinition Width="Auto"/> <!-- 第1列:TextBox -->
<ColumnDefinition Width="Auto"/> <!-- 第2列:Button -->
</Grid.ColumnDefinitions>
<!--
Grid.Column="0" → 放在第 0 列
Grid.ColumnSpan="2" → 跨 2 列合并(从当前列向右数)
Grid.RowSpan="2" → 跨 2 行合并(从当前行向下数)
-->
<Label Grid.Column="0"
Content="{Binding LabelText}" <!-- {Binding} 绑定 AxisItem.LabelText 属性 -->
Foreground="Wheat" /> <!-- 文字颜色:Wheat 小麦色 -->
<TextBox Grid.Column="1"
Margin="0,0,5,0" <!-- 右边留 5px 间距 -->
TextAlignment="Center" <!-- 文字水平居中 -->
VerticalContentAlignment="Center" <!-- 文字垂直居中(内容在控件内居中) -->
Width="80" Height="30"
Text="{Binding Value}" /> <!-- 双向绑定 AxisItem.Value,修改后同步回数据 -->
<Button Grid.Column="2"
Content="Confirm"
Width="60" Height="30"
Click="Button_Click" /> <!-- 点击事件绑定到 C# 方法 -->
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
六、MainWindow.xaml.cs --- 主窗口逻辑(完整注释)
csharp
using System;
using System.Collections.Generic; // List<T>
using System.Windows; // Window, MessageBox, RoutedEventArgs
using System.Windows.Controls; // Button, TextBox, Label, ItemsControl
namespace Monitor_Plc
{
/// <summary>
/// 数据模型类
/// 表示 ItemsControl 中每一条轴的数据
/// 属性名必须与 XAML 中 {Binding 属性名} 完全一致
/// 位置:放在 MainWindow 类外面、命名空间里面
/// </summary>
public class AxisItem
{
// 自动属性 (Auto-Implemented Property)
// { get; set; } 编译器自动生成私有字段
public string LabelText { get; set; } // 标签文字,如 "Axis1Pos:"
public string Value { get; set; } // 输入框的值
}
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// partial 关键字:这个类的另一部分由 XAML 编译自动生成
/// </summary>
public partial class MainWindow : Window
{
/// <summary>
/// 构造函数 --- 窗口创建时调用
/// </summary>
public MainWindow()
{
// 必须调用!初始化 XAML 中定义的控件(加载编译后的 BAML 资源)
// 如果不调用,所有控件都不会被创建
InitializeComponent();
// 给 ItemsControl 绑定数据源
// ItemsSource 必须是一个实现了 System.Collections.IEnumerable 的集合
// List<T> 是最常用的集合类型
AxisItems.ItemsSource = new List<AxisItem>()
{
// 每一条 AxisItem → ItemsControl 生成一行 UI
// Value 留空,用户在 TextBox 中输入
new AxisItem{ LabelText = "Axis1Pos:", Value = "" },
new AxisItem{ LabelText = "Axis2Pos:", Value = "" },
new AxisItem{ LabelText = "Axis3Pos:", Value = "" },
new AxisItem{ LabelText = "Axis4Pos:", Value = "" }
};
}
/// <summary>
/// Confirm 按钮点击事件
/// DataTemplate 中所有 Button 点击都触发此方法
/// </summary>
/// <param name="sender">触发事件的控件(就是用户点的那一个 Button)</param>
/// <param name="e">路由事件参数</param>
private void Button_Click(object sender, RoutedEventArgs e)
{
// sender 是 object 类型,需要转换为 Button
// as 安全转换:失败返回 null,不会抛异常
var btn = sender as Button;
// 关键机制:获取当前行的数据
// 在 ItemsControl 中,每条数据的 DataContext 自动设为对应的 AxisItem 对象
// Button 没有设置 DataContext,就继承父级 Grid 的 DataContext
// 所以 btn.DataContext 就是这一行的 AxisItem
var item = btn.DataContext as AxisItem;
// 显示当前行的标签和值
// $ 字符串插值:{item.LabelText} 替换为属性值
MessageBox.Show($"{item.LabelText} = {item.Value}");
// 后续可替换为 PLC 写入逻辑:
// PLC.Write(item.LabelText, item.Value);
}
}
}
七、核心概念详解
7.1 Grid 布局系统
Grid 是 WPF 最强大的布局容器,通过行和列的二维表格来排列控件。
xml
<Grid>
<!-- 先定义行和列 -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- 第0行:高度由内容决定 -->
<RowDefinition Height="2*"/> <!-- 第1行:占剩余空间的 2/3 -->
<RowDefinition Height="*"/> <!-- 第2行:占剩余空间的 1/3 -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/> <!-- 第0列:宽度由内容决定 -->
<ColumnDefinition Width="*"/> <!-- 第1列:占满剩余空间 -->
<ColumnDefinition Width="100"/> <!-- 第2列:固定 100px -->
</Grid.ColumnDefinitions>
<!-- 通过附加属性指定位置 -->
<Label Grid.Row="0" Grid.Column="0" Content="左上" />
<TextBox Grid.Row="0" Grid.Column="1" />
<Button Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Content="跨两列" />
</Grid>
RowDefinition.Height / ColumnDefinition.Width 取值:
| 值 | 说明 | 场景 |
|---|---|---|
Auto |
由内容撑开 | 标题栏、标签、工具栏 |
* |
按比例占满剩余空间 | 主要内容区域 |
2* |
占两倍剩余空间(比 * 宽一倍) |
需要按比例分割时 |
100 |
固定 100 像素 | 侧边栏、固定宽度的列 |
Grid 附加属性:
| 属性 | 含义 | 示例 |
|---|---|---|
Grid.Row="0" |
放在第 0 行(索引从 0 开始) | |
Grid.Column="0" |
放在第 0 列 | |
Grid.RowSpan="2" |
跨 2 行合并 | 合并后的控件占两行高度 |
Grid.ColumnSpan="2" |
跨 2 列合并 | 合并后的控件占两列宽度 |
7.2 ItemsControl --- 数据驱动的 UI 生成器
ItemsControl 是 WPF 中根据数据列表自动生成 UI 的核心控件。这是 MVVM 模式的基础。
工作机制:
AxisItems.ItemsSource = List<AxisItem> (4 条数据)
└── ItemsPanelTemplate (默认是 StackPanel,垂直排列)
├── 数据[0]: AxisItem{LabelText="Axis1Pos:", Value=""}
│ └── DataTemplate → Grid(Label | TextBox | Button)
│ └── DataContext = 这条 AxisItem
│
├── 数据[1]: AxisItem{LabelText="Axis2Pos:", Value=""}
│ └── DataTemplate → Grid(Label | TextBox | Button)
│ └── DataContext = 这条 AxisItem
│
├── 数据[2]: AxisItem{LabelText="Axis3Pos:", Value=""}
│ └── DataTemplate → Grid(Label | TextBox | Button)
│ └── DataContext = 这条 AxisItem
│
└── 数据[3]: AxisItem{LabelText="Axis4Pos:", Value=""}
└── DataTemplate → Grid(Label | TextBox | Button)
└── DataContext = 这条 AxisItem
核心要点:
- ItemsSource 绑定数据源(
List<T>、ObservableCollection<T>等) - ItemTemplate 定义每条数据长什么样
- DataTemplate 里面的
{Binding}从当前数据的DataContext中取值 - 每一条数据 生成的控件组的
DataContext自动设为该数据对象本身 - 不需要写行定义,ItemsControl 内部自动处理排列
7.3 数据绑定 {Binding}
xml
<Label Content="{Binding LabelText}" />
含义: Label 的 Content 属性从当前 DataContext 的 LabelText 属性中取值。
双向绑定:
xml
<TextBox Text="{Binding Value}" />
用户在 TextBox 中输入时,Value 属性也会同步更新(需数据类实现 INotifyPropertyChanged,但简单场景下默认双向绑定对 TextBox.Text 已生效)。
7.4 事件处理 --- 获取当前行数据
csharp
private void Button_Click(object sender, RoutedEventArgs e)
{
// sender 是触发事件的 Button 控件
Button btn = sender as Button;
// 获取这一行绑定的数据对象
AxisItem item = btn.DataContext as AxisItem;
// 现在可以访问这一行的所有数据
string label = item.LabelText;
string value = item.Value;
}
为什么这样可以拿到当前行的数据?
- Button 在 DataTemplate 中,没有设置自己的 DataContext
- 它继承父级 Grid 的 DataContext
- 而 Grid 是由 ItemsControl 根据 AxisItem 生成的,DataContext 就是那个 AxisItem
- 所以
btn.DataContext就是点击那一行的数据
7.5 VerticalAlignment vs VerticalContentAlignment
很多初学者容易混淆这两个属性,这里做详细对比:
| 属性 | 作用对象 | 控制什么 | 可选值 |
|---|---|---|---|
VerticalAlignment |
控件自身 | 控件在父容器中的垂直位置 | Top, Center, Bottom, Stretch |
VerticalContentAlignment |
控件内部的内容 | 文本/内容在控件内部的垂直位置 | Top, Center, Bottom, Stretch |
HorizontalAlignment |
控件自身 | 控件在父容器中的水平位置 | Left, Center, Right, Stretch |
HorizontalContentAlignment |
控件内部的内容 | 内容在控件内部的水平位置 | Left, Center, Right, Stretch |
TextAlignment |
文本 | 文字在控件中的水平对齐 | Left, Center, Right, Justify |
示例对比:
xml
<!-- 错误:文字还在顶部 -->
<TextBox VerticalAlignment="Center" Text="Hello" />
<!-- 正确:文字在 TextBox 内部居中 -->
<TextBox VerticalContentAlignment="Center" TextAlignment="Center" Text="Hello" />
7.6 常用 WPF 控件一览
| 控件 | 命名空间 | 用途 | 主要属性 |
|---|---|---|---|
Label |
System.Windows.Controls |
只读标签,支持快捷键 | Content, Foreground, Background |
TextBlock |
System.Windows.Controls |
只读文本(轻量,无背景/边框) | Text, TextWrapping |
TextBox |
System.Windows.Controls |
可编辑输入框 | Text, MaxLength, IsReadOnly |
Button |
System.Windows.Controls |
按钮,可触发命令/事件 | Content, Click, Command |
Grid |
System.Windows.Controls |
表格布局容器 | RowDefinitions, ColumnDefinitions |
StackPanel |
System.Windows.Controls |
水平或垂直堆叠排列 | Orientation |
ItemsControl |
System.Windows.Controls |
数据驱动列表容器 | ItemsSource, ItemTemplate |
Image |
System.Windows.Controls |
显示图片 | Source, Stretch |
Rectangle |
System.Windows.Shapes |
矩形(用于背景/分割线) | Fill, Opacity |
7.7 背景设置
纯色背景:
xml
<Window Background="#FF2D2D2D"> <!-- 十六进制颜色:#AARRGGBB -->
<Window Background="AliceBlue"> <!-- 命名颜色 -->
<Window Background="{StaticResource MyBrush}"> <!-- 资源引用 -->
图片背景:
xml
<Window.Background>
<ImageBrush ImageSource="Images/background.jpg" Stretch="UniformToFill" Opacity="0.5" />
</Window.Background>
图片 + 半透明遮罩层(推荐,保留亮度降低饱和度):
xml
<Grid>
<!-- 图片层 -->
<Image Source="Images/background.jpg" Stretch="UniformToFill" />
<!-- 半透明白色覆盖层 -->
<Rectangle Fill="White" Opacity="0.3" />
<!-- Opacity 越大颜色越淡:0.2 → 稍微柔和, 0.4 → 明显变淡, 0.6 → 很淡 -->
<!-- 也可以使用黑色覆盖层来变暗 -->
<!-- <Rectangle Fill="Black" Opacity="0.3" /> -->
<!-- 控件层 -->
<Grid>
<ItemsControl Name="AxisItems"> ... </ItemsControl>
</Grid>
</Grid>
注意 :WPF 原生支持的图片格式:
.bmp.jpg.jpeg.png.gif.tiff.ico.wmf,不支持.avif。
7.8 XAML 注释语法
xml
<!-- 单行注释 -->
<!--
多行注释
多行注释
-->
注意:
Ctrl+/快捷键是 C# 的注释快捷键,XAML 不适用 ,需要手写<!-- -->。
7.9 TextBox 限制输入
xml
<!-- 限制字符数 -->
<TextBox MaxLength="5" />
<!-- 只允许输入数字(需代码后置配合) -->
<TextBox PreviewTextInput="TextBox_PreviewTextInput" />
csharp
private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
foreach (char c in e.Text)
{
if (!char.IsDigit(c))
{
e.Handled = true; // 阻止输入
return;
}
}
}
八、常见问题排查
8.1 XAML 编译/运行错误
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
| "类型 'ItemsControl' 不存在" | 拼写错误 | 检查 ItemsControl(有 s) |
| "未找到 ItemTemplate" | 标签写错了 | 写成 <ItemsControl.ItemTemplate> |
| 设计器不显示,但能运行 | XAML 语法错误 | 查看错误列表窗口 |
{Binding} 不显示内容 |
属性名拼写不一致 | 检查 LabelText 是否和 C# 类一致 |
8.2 布局问题
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 按钮跑到窗口最右边 | 中间列是 *,占满剩余空间 |
改为 Auto 或加 HorizontalAlignment="Left" |
| 控件重叠在一起 | 放在了同一个 Grid 格子 | 检查 Grid.Row/Grid.Column 索引 |
| TextBox 不显示 | 误用了 TextBlock |
改成 <TextBox> |
| 文字不居中 | 用了 VerticalAlignment 不是 VerticalContentAlignment |
改用 VerticalContentAlignment="Center" |
| 图片不显示 | 格式不支持(如 .avif)或路径错误 |
转成 .jpg/.png,生成操作设为 Resource |
| 控件位置不对 | 索引从 0 开始,忘记 +1 | 第 0 行/列是第一个 |
8.3 代码后置问题
| 现象 | 原因 | 解决方法 |
|---|---|---|
InitializeComponent() 报错 |
XAML 有语法错误 | 先修 XAML 错误 |
ItemsSource 赋值后不显示 |
忘记调 InitializeComponent() |
确保先调 InitializeComponent() |
sender as Button 返回 null |
sender 不是 Button | 检查事件是否绑定在 Button 上 |
DataContext as AxisItem 返回 null |
DataContext 未正确绑定 | 检查 ItemsSource 是否赋值 |
九、后续扩展方向
当前项目是一个基础的 WPF 监控面板模板,后续可以:
- PLC 通信 :引入
HslCommunication或S7.Net库,在Button_Click中写入 PLC - 数据刷新 :用
ObservableCollection<AxisItem>替代List<AxisItem>,实现 UI 自动更新 - MVVM 模式 :引入
INotifyPropertyChanged、ICommand,将业务逻辑与 UI 完全分离 - 样式统一 :在
App.xaml的<Application.Resources>中定义全局样式 - 多窗口:添加更多窗口,用导航框架切换
- 国际化:支持中英文切换(通过资源字典)