跳到主要内容

2.4 控件与交互事件

学习目标​

本节用一个小型任务列表认识控件和事件。你将根据输入状态添加任务、根据选择状态启用删除按钮,并知道这些直接操作控件的代码在后续如何过渡到绑定与命令。

从控件职责出发​

TextBlock 负责展示文本,TextBox 接收可编辑文本;Button 表达一次操作,CheckBox 表达一个可持续变化的选项。列表通常用 ListBox,下拉选择用 ComboBox,表格编辑用 DataGrid。不要为了可点击而把所有元素都写成按钮,也不要把业务对象过早转换成字符串后丢失其身份。

WPF 中许多控件有 Content 属性,可以容纳文本,也可以容纳其他对象。例如按钮的内容可以是一块包含图标和文字的容器。ListBox 则是多项控件,有 Items 或 ItemsSource。本节先直接操作 Items,在通知与集合中再把列表连接到真正的数据对象。

事件用于响应 UI 已经发生的动作。Click 表示按钮被激活,不只来自鼠标,也可能来自键盘;SelectionChanged 表示选择发生变化,包括代码触发的变化。TextChanged 表示输入内容变化,可能在初始化期间发生。因此处理方法应检查当前状态,不能假定每一次事件都由用户点击引起。

完整的任务列表示例​

沿用 WpfBasics 项目,替换两个完整的 MainWindow 文件。这里没有数据绑定或第三方库,目的是观察控件与事件的连接。

MainWindow.xaml(完整文件)
<Window x:Class="WpfBasics.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="任务交互" Width="640" Height="420"
MinWidth="480" MinHeight="320">
<Grid Margin="20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>

<TextBox x:Name="TitleInput" Margin="0,0,12,0"
VerticalContentAlignment="Center" />
<Button Grid.Column="1" Content="添加" Padding="16,6"
IsDefault="True" Click="OnAddClick" />
<CheckBox x:Name="CompletedOption" Grid.Row="1"
Grid.ColumnSpan="2" Content="添加时标为已完成"
Margin="0,12" />
<ListBox x:Name="TasksList" Grid.Row="2" Grid.ColumnSpan="2"
SelectionChanged="OnSelectionChanged" />
<Button x:Name="RemoveButton" Grid.Row="3" Grid.Column="1"
Content="删除选中项" Margin="0,12,0,0" Padding="12,6"
IsEnabled="False" Click="OnRemoveClick" />
</Grid>
</Window>
MainWindow.xaml.cs(完整文件)
using System.Windows;
using System.Windows.Controls;

namespace WpfBasics;

public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
TitleInput.Focus();
}

private void OnAddClick(object sender, RoutedEventArgs e)
{
string title = TitleInput.Text.Trim();
if (title.Length == 0)
{
MessageBox.Show(this, "请输入任务标题。", "输入提示");
TitleInput.Focus();
return;
}

string prefix = CompletedOption.IsChecked == true ? "[完成] " : "[待办] ";
TasksList.Items.Add(prefix + title);
TitleInput.Clear();
TitleInput.Focus();
}

private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
// 初始化也可能触发选择事件,因此先检查命名控件是否已创建。
if (RemoveButton is not null)
RemoveButton.IsEnabled = TasksList.SelectedItem is not null;
}

private void OnRemoveClick(object sender, RoutedEventArgs e)
{
int index = TasksList.SelectedIndex;
if (index >= 0)
TasksList.Items.RemoveAt(index);
}
}

IsDefault="True" 让这个按钮承担窗口默认操作,常见效果是在单行输入框中按 Enter 添加任务。多行输入、其他控件的键盘处理会影响该行为,因此仍需要实测键盘流程。Focus() 请求输入焦点,使连续录入更顺手。

CheckBox.IsChecked 是可空布尔值,因为控件也支持三态。示例没有启用三态,仍用 == true 明确表达“确定被选中”,避免把空值误当成完成状态。SelectedIndex 在没有选择时为 -1,所以删除前要检查;按钮禁用是交互提示,处理方法里的检查仍保留,用于保证逻辑稳健。

事件参数和界面状态​

sender 表示调用当前处理方法的事件订阅对象。对路由事件而言,e.OriginalSource 还可能是控件内部更深的元素。不要在所有处理方法中无条件把 sender 强制转换为某个类型,再假设它一定来自预期的子元素。

这里“任务列表”和“删除按钮是否可用”都是界面状态。列表有选择时允许删除,没有选择时禁用按钮。代码后置直接同步这些状态容易理解,但当多个窗口都要修改同一批任务时,它会逐渐变得难维护。后续用数据对象存任务、绑定展示状态,再用命令表达可执行条件,便能减少视图之间的手工同步。

同时,点击事件运行在 UI 线程。短小的输入处理没有问题;若以后在事件中发 HTTP 请求,应使用异步 I/O,避免阻塞窗口。不要在本节为了“后台执行”把控件访问随意搬到线程池,异步与线程章节会解释边界。

常见错误​

  • 同一个点击执行两次:检查是否既在 XAML 中连接 Click,又在 C# 中使用事件订阅连接同一方法。
  • 使用了 ItemsSource 后还调用 Items.Add:绑定集合时应修改源集合,两种填充方式不要混用。
  • 删除后报索引错误:选择可能已被清空,先获取当前索引并判断是否有效。
  • 任务状态只能通过文字前缀识别:这个示例专为事件入门;真正业务状态应使用对象属性,而不是解析展示字符串。

动手练习与验收​

给任务列表左下方增加一个计数文本,每次添加或删除后更新为“共 N 项”。新增一个辅助方法统一更新计数,并在构造函数初始化后调用,避免在两个事件里复制同一段格式化逻辑。

验收标准:空标题和纯空格不会新增;鼠标和 Enter 均能添加;未选择时删除按钮禁用;删除选中项后计数正确;勾选完成选项时新增项显示完成前缀。你应能解释哪些代码属于数据状态,哪些属于焦点或选择等视图行为。

官方延伸资料​