# WPF 学习记录( 第二天)

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
相关推荐
梦071 小时前
Trae Friends福州线下活动收获一二-vibeCoding现状
经验分享·学习
星恒随风1 小时前
C++ 模板初阶:从泛型编程、函数模板到类模板,一篇打通基础概念
开发语言·c++·笔记·学习
踏着七彩祥云的小丑1 小时前
嵌入式测试学习第35 天:蓝牙、WiFi嵌入式设备测试基础概念
单片机·嵌入式硬件·学习
承渊政道2 小时前
【MySQL数据库学习】(MySQL内置函数)
数据库·学习·mysql·ubuntu·bash·数据库开发·数据库系统
力学与人工智能2 小时前
论文分享 | 优化离散损失求解反问题:无需神经网络的快速精确学习
人工智能·神经网络·学习·优化·离散损失·反问题求解·快速准确学习
V搜xhliang024610 小时前
AI智能体的数据安全与合规实践
人工智能·学习·数据分析·自动化·ai编程
无敌的牛11 小时前
redis学习过程
数据库·redis·学习
旅僧13 小时前
Π环境部署(运行 且 无理论讲解)
学习
jushi899913 小时前
Lucas Chess R国际象棋、中国象棋、日本将棋、五子棋训练学习工具游戏软件
学习