WPF 学习记录 --- 第二天
一、数据模型扩展
从 2 个属性扩展到 8 个属性,每行同时显示位置/速度/加速度/减速度:
csharp
public class AxisItem
{
public string PosLabel { get; set; }
public string PosValue { get; set; }
public string VelLabel { get; set; }
public string VelValue { get; set; }
public string AccelLabel { get; set; }
public string AccelValue { get; set; }
public string DecelLabel { get; set; }
public string DecelValue { get; set; }
}
ItemsSource 绑定:
csharp
AxisItems.ItemsSource = new List<AxisItem>()
{
new AxisItem
{
PosLabel = "Axis1Pos:", VelLabel = "AxisVel:",
AccelLabel = "Axis1Accel:", DecelLabel = "Axis1Decel:"
},
// ... 共 4 条
};
二、XAML 12 列布局
一行共 12 列(每 3 列一组:Label + TextBox + Button):
列 0~2 → 位置 (Label | TextBox | Button)
列 3~5 → 速度 (Label | TextBox | Button)
列 6~8 → 加速度 (Label | TextBox | Button)
列 9~11 → 减速度 (Label | TextBox | Button)
xml
<Grid.ColumnDefinitions>
<!-- 位置 -->
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<!-- 速度 -->
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<!-- 加速度 -->
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<!-- 减速度 -->
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
三、外层 Grid 分层(底部按钮)
* 和 Auto 的协作:
xml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/> <!-- 第0行:ItemsControl 占满剩余空间 -->
<RowDefinition Height="Auto"/> <!-- 第1行:底部按钮,高度由内容决定 -->
</Grid.RowDefinitions>
<ItemsControl Grid.Row="0" Name="AxisItems">
<!-- ... -->
</ItemsControl>
<StackPanel Grid.Row="1" Orientation="Horizontal"
HorizontalAlignment="Center" Margin="0,10">
<Button Content="启动" Width="80" Height="35" Margin="5,0" />
<Button Content="停止" Width="80" Height="35" Margin="5,0" />
<Button Content="复位" Width="80" Height="35" Margin="5,0" />
<Button Content="急停" Width="80" Height="35" Margin="5,0" />
<Button Content="读取" Width="80" Height="35" Margin="5,0" />
<Button Content="写入" Width="80" Height="35" Margin="5,0" />
</StackPanel>
</Grid>
计算顺序:
窗口高度 800px
第1行 Auto → 按钮栏 45px(按钮 35px + Margin 10px) ← 先算
第0行 * → 800 - 45 = 755px ← 剩下的全给 *
为什么按钮在底部? * 行占满中间空间,Auto 行自然被推到窗口底部。
StackPanel 说明:
| 属性 | 作用 |
|---|---|
Orientation="Horizontal" |
按钮水平从左到右排列 |
HorizontalAlignment="Center" |
StackPanel 在父容器中水平居中 |
四、Button_Click 动态数据展示
核心思路: 用 Dictionary 只收集有值的字段,用 LINQ Select + string.Join 一次性展示。
csharp
private void Button_Click(object sender, RoutedEventArgs e)
{
var btn = sender as Button;
var item = btn.DataContext as AxisItem;
var dict = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(item.PosValue)) dict[item.PosLabel] = item.PosValue;
if (!string.IsNullOrEmpty(item.VelValue)) dict[item.VelLabel] = item.VelValue;
if (!string.IsNullOrEmpty(item.AccelValue)) dict[item.AccelLabel] = item.AccelValue;
if (!string.IsNullOrEmpty(item.DecelValue)) dict[item.DecelLabel] = item.DecelValue;
if (dict.Count > 0)
MessageBox.Show(string.Join("\n",
dict.Select(kvp => $"{kvp.Key} = {kvp.Value}")));
else
MessageBox.Show("请输入内容,请勿提交空数据");
}
效果示例:
| 填写情况 | 弹窗显示 |
|---|---|
| 只填位置 | Axis1Pos: = 100 |
| 填了位置+速度 | Axis1Pos: = 100 AxisVel: = 200 |
| 全部为空 | 请输入内容,请勿提交空数据 |
string.Join 规则:
string.Join("\n", ["A=1", "B=2", "C=3"])
结果: "A=1\nB=2\nC=3"
分隔符数量 = 元素数量 - 1。集合为空返回空字符串。
五、TextBox 输入验证
5.1 XAML 配置
xml
<TextBox Text="{Binding PosValue}"
InputMethod.IsInputMethodEnabled="False" <!-- 禁用中文输入法 -->
PreviewTextInput="TextBox_PreviewTextInput" <!-- 键盘输入验证 -->
DataObject.Pasting="TextBox_Pasting" <!-- 粘贴验证 -->
MaxLength="8"
TextAlignment="Center"
VerticalContentAlignment="Center"
Width="80" Height="30" />
5.2 验证方法设计
为什么需要两个事件?
| 事件 | 拦截场景 | 取消方式 |
|---|---|---|
PreviewTextInput |
按键输入 | e.Handled = true |
DataObject.Pasting |
Ctrl+V / 右键粘贴 | e.CancelCommand() |
InputMethod.IsInputMethodEnabled="False" 作用: 禁掉中文输入法,因为 PreviewTextInput 对 IME 拦截不可靠。
5.3 抽取验证方法(两个事件共用)
csharp
private bool verifyText(string currentTx, string inputTx,
int selectionStart, int selectionLength)
{
// 拼接出输入后的最终结果
string resultTx = currentTx.Substring(0, selectionStart)
+ inputTx
+ currentTx.Substring(selectionStart + selectionLength);
// 规则1:只能输入数字和小数点
foreach (char ch in inputTx)
{
if (!char.IsDigit(ch) && ch != '.')
return false;
}
// 规则2:小数点不能在第一位
if (resultTx.StartsWith("."))
return false;
// 规则3:最多只能有一个小数点
int dotCount = 0;
foreach (char ch in resultTx)
{
if (ch == '.') dotCount++;
}
if (dotCount > 1)
return false;
return true;
}
5.4 键盘输入事件
csharp
private void TextBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
TextBox tx = sender as TextBox;
if (!verifyText(tx.Text, e.Text, tx.SelectionStart, tx.SelectionLength))
{
e.Handled = true;
}
}
5.5 粘贴事件
csharp
private void TextBox_Pasting(object sender, DataObjectPastingEventArgs e)
{
string pasteText = e.DataObject.GetData(typeof(string)) as string;
if (string.IsNullOrEmpty(pasteText))
{
e.CancelCommand();
return;
}
TextBox tx = sender as TextBox;
if (!verifyText(tx.Text, pasteText, tx.SelectionStart, tx.SelectionLength))
{
e.CancelCommand();
}
}
六、验证逻辑核心原理
6.1 为什么需要拼接 resultTx?
验证必须在字符真正进入 TextBox 之前判断:
| 变量 | 内容 | 时机 |
|---|---|---|
currentTx |
"12.5" |
输入前 |
inputTx / e.Text |
"6" |
正要输入的字符 |
resultTx |
"12.56" |
输入后的结果 |
6.2 Substring 拼接
csharp
string resultTx = currentTx.Substring(0, selectionStart) // 光标前
+ inputTx // 新输入
+ currentTx.Substring(selectionStart + selectionLength); // 光标后到末尾
Substring(n) 只有一个参数: 从索引 n 到字符串末尾。
"12.5".Substring(2) → ".5" ← 从索引2到末尾
"12.5".Substring(0, 2) → "12" ← 从索引0取2个字符
6.3 为什么 for 循环检查 e.Text
单次按键 e.Text 只有 1 个字符,但粘贴时 e.Text 是一串(如 "123.45"),用 for 循环逐个检查每个字符是否合法。
6.4 DataObject.GetData 原理
csharp
string pasteText = e.DataObject.GetData(typeof(string)) as string;
e.DataObject--- 剪贴板数据包(含多种格式)typeof(string)--- 获取System.String类型对象GetData()--- 返回object类型as string--- 安全转为string
剪贴板不是文本时(如图片),GetData 返回 null,自动取消粘贴。
七、验证一览表
| 场景 | 键盘输入 | 粘贴 |
|---|---|---|
123 |
✅ 允许 | ✅ 允许 |
123.45 |
✅ 允许 | ✅ 允许 |
abc |
❌ 阻止 | ❌ 阻止 |
.5(点开头) |
❌ 阻止 | ❌ 阻止 |
12.34.56(两个点) |
❌ 阻止 | ❌ 阻止 |
| 粘贴图片 | --- | ❌ 阻止 |
| 输入中文 | ❌ IME 禁用 | ❌ |
八、今日知识速查
| 知识点 | 一句话总结 |
|---|---|
* 和 Auto |
* 占完 Auto 剩下的空间,按钮在底部 |
StackPanel |
水平/垂直自动排列的容器 |
ItemsControl.DataTemplate |
定义每条数据长什么样 |
DataContext |
按钮的 DataContext 就是这一行的数据对象 |
Dictionary<string, string> |
只存有值的字段,避免空数据 |
string.Join |
分隔符放元素之间,空集合返回空字符串 |
Select(kvp => ...) |
把每个键值对转成字符串 |
PreviewTextInput |
键盘输入时拦截验证 |
DataObject.Pasting |
粘贴时拦截验证 |
CancelCommand() |
粘贴事件的取消方式(不是 Handled=true) |
InputMethod.IsInputMethodEnabled |
禁用中文输入法 |
.avif |
WPF 不支持,转 .jpg/.png |