This commit is contained in:
GukSang.Jin
2026-06-09 16:19:06 +08:00
parent e8e81b1e46
commit 1cc402450b
7 changed files with 683 additions and 52 deletions

View File

@@ -102,9 +102,9 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
return; return;
} }
if (!TryParseInt32(LoadSpeedSettingInput, out int loadSpeedSetting)) if (!TryParseFloat(LoadSpeedSettingInput, out float loadSpeedSetting))
{ {
StatusText = "负载转速设置必须是有效的 32 位整数。"; StatusText = "负载转速设置必须是有效数。";
return; return;
} }
@@ -112,9 +112,16 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
StatusText = "正在保存负载转速设置..."; StatusText = "正在保存负载转速设置...";
try try
{ {
await _plcRegisterService.WriteInt32Async(_plcConfig, LoadSpeedSettingRegister, loadSpeedSetting); await _plcRegisterService.WriteFloatAsync(_plcConfig, LoadSpeedSettingRegister, loadSpeedSetting);
int confirmedValue = await _plcRegisterService.ReadInt32Async(_plcConfig, LoadSpeedSettingRegister); float confirmedValue = await _plcRegisterService.ReadFloatAsync(_plcConfig, LoadSpeedSettingRegister);
LoadSpeedSettingInput = confirmedValue.ToString(CultureInfo.InvariantCulture); if (float.IsNaN(confirmedValue)
|| float.IsInfinity(confirmedValue)
|| !AreEquivalentFloatRegisterValues(loadSpeedSetting, confirmedValue))
{
throw new InvalidOperationException("PLC 写入后回读不一致。");
}
LoadSpeedSettingInput = FormatNumber(confirmedValue);
StatusText = "负载转速设置已保存并回读确认。"; StatusText = "负载转速设置已保存并回读确认。";
} }
catch (Exception ex) catch (Exception ex)
@@ -146,12 +153,14 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
{ {
await _plcRegisterService.WriteFloatAsync(_plcConfig, row.Address, value); await _plcRegisterService.WriteFloatAsync(_plcConfig, row.Address, value);
float confirmedValue = await _plcRegisterService.ReadFloatAsync(_plcConfig, row.Address); float confirmedValue = await _plcRegisterService.ReadFloatAsync(_plcConfig, row.Address);
if (float.IsNaN(confirmedValue) || float.IsInfinity(confirmedValue)) if (float.IsNaN(confirmedValue)
|| float.IsInfinity(confirmedValue)
|| !AreEquivalentFloatRegisterValues(value, confirmedValue))
{ {
throw new InvalidOperationException("PLC 回读值无效。"); throw new InvalidOperationException("PLC 写入后回读不一致。");
} }
row.ValueText = confirmedValue.ToString("0.########", CultureInfo.InvariantCulture); row.ValueText = FormatNumber(confirmedValue);
StatusText = $"{row.RangeText}已保存并回读确认。"; StatusText = $"{row.RangeText}已保存并回读确认。";
} }
catch (Exception ex) catch (Exception ex)
@@ -166,11 +175,16 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
private async Task ReadValuesAsync() private async Task ReadValuesAsync()
{ {
int loadSpeedSetting = await _plcRegisterService.ReadInt32Async(_plcConfig, LoadSpeedSettingRegister); float loadSpeedSetting = await _plcRegisterService.ReadFloatAsync(_plcConfig, LoadSpeedSettingRegister);
if (float.IsNaN(loadSpeedSetting) || float.IsInfinity(loadSpeedSetting))
{
throw new InvalidOperationException("负载转速设置读取无效。");
}
ushort[] addresses = SpeedCoefficientSettings.Select(static row => row.Address).ToArray(); ushort[] addresses = SpeedCoefficientSettings.Select(static row => row.Address).ToArray();
IReadOnlyDictionary<ushort, float> values = await _plcRegisterService.ReadFloatValuesAsync(_plcConfig, addresses); IReadOnlyDictionary<ushort, float> values = await _plcRegisterService.ReadFloatValuesAsync(_plcConfig, addresses);
LoadSpeedSettingInput = loadSpeedSetting.ToString(CultureInfo.InvariantCulture); LoadSpeedSettingInput = FormatNumber(loadSpeedSetting);
foreach (SpeedCoefficientSettingRow row in SpeedCoefficientSettings) foreach (SpeedCoefficientSettingRow row in SpeedCoefficientSettings)
{ {
if (!values.TryGetValue(row.Address, out float value) if (!values.TryGetValue(row.Address, out float value)
@@ -180,14 +194,13 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
throw new InvalidOperationException($"{row.RangeText}读取无效。"); throw new InvalidOperationException($"{row.RangeText}读取无效。");
} }
row.ValueText = value.ToString("0.########", CultureInfo.InvariantCulture); row.ValueText = FormatNumber(value);
} }
} }
private static bool TryParseInt32(string input, out int value) private static string FormatNumber(float value)
{ {
return int.TryParse(input, NumberStyles.Integer, CultureInfo.CurrentCulture, out value) return value.ToString("0.########", CultureInfo.InvariantCulture);
|| int.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
} }
private static bool TryParseFloat(string input, out float value) private static bool TryParseFloat(string input, out float value)
@@ -196,4 +209,9 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
|| float.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out value); || float.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out value);
return parsed && !float.IsNaN(value) && !float.IsInfinity(value); return parsed && !float.IsNaN(value) && !float.IsInfinity(value);
} }
private static bool AreEquivalentFloatRegisterValues(float left, float right)
{
return BitConverter.SingleToInt32Bits(left) == BitConverter.SingleToInt32Bits(right);
}
} }

View File

@@ -1,6 +1,7 @@
<Window x:Class="DentistryHandpieces.HiddenSpeedSettingsWindow" <Window x:Class="DentistryHandpieces.HiddenSpeedSettingsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DentistryHandpieces"
Title="转速隐藏参数设置" Title="转速隐藏参数设置"
Width="920" Width="920"
Height="720" Height="720"
@@ -19,6 +20,11 @@
<Setter Property="Background" Value="#F8FAFC" /> <Setter Property="Background" Value="#F8FAFC" />
<Setter Property="BorderBrush" Value="#B8C5D1" /> <Setter Property="BorderBrush" Value="#B8C5D1" />
</Style> </Style>
<Style x:Key="HiddenDecimalInputTextBox" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="local:NumericKeypad.InputMode" Value="Decimal" />
<Setter Property="local:NumericKeypad.MaxDecimalPlaces" Value="8" />
<Setter Property="local:NumericKeypad.AllowNegative" Value="True" />
</Style>
<Style TargetType="Button"> <Style TargetType="Button">
<Setter Property="MinHeight" Value="44" /> <Setter Property="MinHeight" Value="44" />
<Setter Property="Padding" Value="18,8" /> <Setter Property="Padding" Value="18,8" />
@@ -55,6 +61,8 @@
Foreground="#22313F" /> Foreground="#22313F" />
<TextBox Grid.Column="1" <TextBox Grid.Column="1"
Text="{Binding LoadSpeedSettingInput, UpdateSourceTrigger=PropertyChanged}" Text="{Binding LoadSpeedSettingInput, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource HiddenDecimalInputTextBox}"
local:NumericKeypad.Title="负载转速设置"
Margin="12,0" /> Margin="12,0" />
<Button Grid.Column="2" <Button Grid.Column="2"
Content="保存" Content="保存"
@@ -88,6 +96,8 @@
<ColumnDefinition Width="92" /> <ColumnDefinition Width="92" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=PropertyChanged}" <TextBox Text="{Binding ValueText, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource HiddenDecimalInputTextBox}"
local:NumericKeypad.Title="{Binding RangeText}"
Margin="0,0,8,0" /> Margin="0,0,8,0" />
<Button Grid.Column="1" <Button Grid.Column="1"
Content="保存" Content="保存"

View File

@@ -27,6 +27,23 @@
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
</Style> </Style>
<Style x:Key="DecimalInputTextBox" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="local:NumericKeypad.InputMode" Value="Decimal" />
<Setter Property="local:NumericKeypad.MaxDecimalPlaces" Value="8" />
</Style>
<Style x:Key="TenthsInputTextBox" TargetType="TextBox" BasedOn="{StaticResource DecimalInputTextBox}">
<Setter Property="local:NumericKeypad.MaxDecimalPlaces" Value="1" />
</Style>
<Style x:Key="IntegerInputTextBox" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="local:NumericKeypad.InputMode" Value="Integer" />
</Style>
<Style x:Key="IpAddressInputTextBox" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="local:NumericKeypad.InputMode" Value="IpAddress" />
</Style>
<Style TargetType="Button"> <Style TargetType="Button">
<Setter Property="MinHeight" Value="46" /> <Setter Property="MinHeight" Value="46" />
<Setter Property="Padding" Value="18,8" /> <Setter Property="Padding" Value="18,8" />
@@ -489,7 +506,9 @@
<ColumnDefinition Width="50" /> <ColumnDefinition Width="50" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox x:Name="AxialForceSetpointInput" <TextBox x:Name="AxialForceSetpointInput"
Text="{Binding AxialForceSetpointInput, UpdateSourceTrigger=PropertyChanged, Delay=600}" /> Text="{Binding AxialForceSetpointInput, UpdateSourceTrigger=PropertyChanged, Delay=600}"
Style="{StaticResource DecimalInputTextBox}"
local:NumericKeypad.Title="轴向拉力/跳动力设置" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Text="N" Text="N"
Style="{StaticResource MutedText}" Style="{StaticResource MutedText}"
@@ -505,7 +524,9 @@
<ColumnDefinition Width="50" /> <ColumnDefinition Width="50" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox x:Name="AxialForceHoldTimeInput" <TextBox x:Name="AxialForceHoldTimeInput"
Text="{Binding AxialForceHoldTimeInput, UpdateSourceTrigger=PropertyChanged, Delay=600}" /> Text="{Binding AxialForceHoldTimeInput, UpdateSourceTrigger=PropertyChanged, Delay=600}"
Style="{StaticResource TenthsInputTextBox}"
local:NumericKeypad.Title="轴向力时间设置(最多 1 位小数)" />
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Text="s" Text="s"
Style="{StaticResource MutedText}" Style="{StaticResource MutedText}"
@@ -769,7 +790,9 @@
Margin="0,0,12,0" /> Margin="0,0,12,0" />
<TextBox Grid.Column="1" <TextBox Grid.Column="1"
x:Name="HoldTorqueInput" x:Name="HoldTorqueInput"
Text="{Binding HoldTorqueInput, UpdateSourceTrigger=PropertyChanged, Delay=600}" /> Text="{Binding HoldTorqueInput, UpdateSourceTrigger=PropertyChanged, Delay=600}"
Style="{StaticResource TenthsInputTextBox}"
local:NumericKeypad.Title="扭矩设置(最多 1 位小数)" />
<TextBlock Grid.Column="2" <TextBlock Grid.Column="2"
Text="mN.m" Text="mN.m"
Style="{StaticResource MutedText}" Style="{StaticResource MutedText}"
@@ -788,7 +811,9 @@
Margin="0,0,12,0" /> Margin="0,0,12,0" />
<TextBox Grid.Column="1" <TextBox Grid.Column="1"
x:Name="TorqueHoldTimeInput" x:Name="TorqueHoldTimeInput"
Text="{Binding TorqueHoldTimeInput, UpdateSourceTrigger=PropertyChanged, Delay=600}" /> Text="{Binding TorqueHoldTimeInput, UpdateSourceTrigger=PropertyChanged, Delay=600}"
Style="{StaticResource TenthsInputTextBox}"
local:NumericKeypad.Title="扭矩时间(最多 1 位小数)" />
<TextBlock Grid.Column="2" <TextBlock Grid.Column="2"
Text="s" Text="s"
Style="{StaticResource MutedText}" Style="{StaticResource MutedText}"
@@ -971,6 +996,8 @@
<TextBox Grid.Row="1" <TextBox Grid.Row="1"
Grid.Column="1" Grid.Column="1"
Text="{Binding NoLoadSpeedSettingInput, UpdateSourceTrigger=PropertyChanged, Delay=600}" Text="{Binding NoLoadSpeedSettingInput, UpdateSourceTrigger=PropertyChanged, Delay=600}"
Style="{StaticResource DecimalInputTextBox}"
local:NumericKeypad.Title="目标空载转速"
Margin="0,16,12,0" /> Margin="0,16,12,0" />
<TextBlock Grid.Row="1" <TextBlock Grid.Row="1"
Grid.Column="2" Grid.Column="2"
@@ -1039,31 +1066,31 @@
<TextBlock Grid.ColumnSpan="3" Text="轴向移动量参数2号轴" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" /> <TextBlock Grid.ColumnSpan="3" Text="轴向移动量参数2号轴" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Text="位移极限" Style="{StaticResource FormLabel}" Margin="0,16,0,0" /> <TextBlock Grid.Row="1" Text="位移极限" Style="{StaticResource FormLabel}" Margin="0,16,0,0" />
<TextBox Grid.Row="1" Grid.Column="1" x:Name="AxialDisplacementLimitInput" Text="{Binding AxialDisplacementLimitInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,16,10,0" /> <TextBox Grid.Row="1" Grid.Column="1" x:Name="AxialDisplacementLimitInput" Text="{Binding AxialDisplacementLimitInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="轴向位移极限" Margin="0,16,10,0" />
<TextBlock Grid.Row="1" Grid.Column="2" Text="mm" Style="{StaticResource MutedText}" Margin="0,16,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="1" Grid.Column="2" Text="mm" Style="{StaticResource MutedText}" Margin="0,16,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="2" Text="手/自动速度" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="2" Text="手/自动速度" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="2" Grid.Column="1" x:Name="AxialSpeedInput" Text="{Binding AxialSpeedInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="2" Grid.Column="1" x:Name="AxialSpeedInput" Text="{Binding AxialSpeedInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="轴向手/自动速度" Margin="0,14,10,0" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="mm/min" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="2" Grid.Column="2" Text="mm/min" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="3" Text="手动位移" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="3" Text="手动位移" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="3" Grid.Column="1" x:Name="AxialManualDisplacementInput" Text="{Binding AxialManualDisplacementInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="3" Grid.Column="1" x:Name="AxialManualDisplacementInput" Text="{Binding AxialManualDisplacementInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="轴向手动位移" Margin="0,14,10,0" />
<TextBlock Grid.Row="3" Grid.Column="2" Text="mm" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="3" Grid.Column="2" Text="mm" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="4" Text="轴向力下限" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="4" Text="轴向力下限" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="4" Grid.Column="1" x:Name="AxialForceLowerLimitInput" Text="{Binding AxialForceLowerLimitInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="4" Grid.Column="1" x:Name="AxialForceLowerLimitInput" Text="{Binding AxialForceLowerLimitInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="轴向力下限" Margin="0,14,10,0" />
<TextBlock Grid.Row="4" Grid.Column="2" Text="N" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="4" Grid.Column="2" Text="N" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="5" Text="轴向力上限" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="5" Text="轴向力上限" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="5" Grid.Column="1" x:Name="AxialForceUpperLimitInput" Text="{Binding AxialForceUpperLimitInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="5" Grid.Column="1" x:Name="AxialForceUpperLimitInput" Text="{Binding AxialForceUpperLimitInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="轴向力上限" Margin="0,14,10,0" />
<TextBlock Grid.Row="5" Grid.Column="2" Text="N" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="5" Grid.Column="2" Text="N" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="6" Text="轴向力系数" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="6" Text="轴向力系数" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="6" Grid.Column="1" x:Name="AxialForceCoefficientInput" Text="{Binding AxialForceCoefficientInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="6" Grid.Column="1" x:Name="AxialForceCoefficientInput" Text="{Binding AxialForceCoefficientInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="轴向力系数" Margin="0,14,10,0" />
<TextBlock Grid.Row="6" Grid.Column="2" Text="系数" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="6" Grid.Column="2" Text="系数" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="7" Text="轴向力保护" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="7" Text="轴向力保护" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="7" Grid.Column="1" x:Name="AxialForceProtectionInput" Text="{Binding AxialForceProtectionInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="7" Grid.Column="1" x:Name="AxialForceProtectionInput" Text="{Binding AxialForceProtectionInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="轴向力保护" Margin="0,14,10,0" />
<TextBlock Grid.Row="7" Grid.Column="2" Text="N" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="7" Grid.Column="2" Text="N" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
</Grid> </Grid>
</Border> </Border>
@@ -1090,35 +1117,35 @@
<TextBlock Grid.ColumnSpan="3" Text="转速/扭矩参数1号轴" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" /> <TextBlock Grid.ColumnSpan="3" Text="转速/扭矩参数1号轴" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" />
<TextBlock Grid.Row="1" Text="位移极限" Style="{StaticResource FormLabel}" Margin="0,16,0,0" /> <TextBlock Grid.Row="1" Text="位移极限" Style="{StaticResource FormLabel}" Margin="0,16,0,0" />
<TextBox Grid.Row="1" Grid.Column="1" x:Name="SpeedTorqueDisplacementLimitInput" Text="{Binding SpeedTorqueDisplacementLimitInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,16,10,0" /> <TextBox Grid.Row="1" Grid.Column="1" x:Name="SpeedTorqueDisplacementLimitInput" Text="{Binding SpeedTorqueDisplacementLimitInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="转速/扭矩位移极限" Margin="0,16,10,0" />
<TextBlock Grid.Row="1" Grid.Column="2" Text="mm" Style="{StaticResource MutedText}" Margin="0,16,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="1" Grid.Column="2" Text="mm" Style="{StaticResource MutedText}" Margin="0,16,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="2" Text="手/自动速度" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="2" Text="手/自动速度" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="2" Grid.Column="1" x:Name="SpeedTorqueSpeedInput" Text="{Binding SpeedTorqueSpeedInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="2" Grid.Column="1" x:Name="SpeedTorqueSpeedInput" Text="{Binding SpeedTorqueSpeedInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="转速/扭矩手/自动速度" Margin="0,14,10,0" />
<TextBlock Grid.Row="2" Grid.Column="2" Text="mm/min" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="2" Grid.Column="2" Text="mm/min" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="3" Text="手动位移" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="3" Text="手动位移" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="3" Grid.Column="1" x:Name="SpeedTorqueManualDisplacementInput" Text="{Binding SpeedTorqueManualDisplacementInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="3" Grid.Column="1" x:Name="SpeedTorqueManualDisplacementInput" Text="{Binding SpeedTorqueManualDisplacementInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="转速/扭矩手动位移" Margin="0,14,10,0" />
<TextBlock Grid.Row="3" Grid.Column="2" Text="mm" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="3" Grid.Column="2" Text="mm" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="4" Text="扭矩系数" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="4" Text="扭矩系数" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="4" Grid.Column="1" x:Name="TorqueCoefficientInput" Text="{Binding TorqueCoefficientInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="4" Grid.Column="1" x:Name="TorqueCoefficientInput" Text="{Binding TorqueCoefficientInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="扭矩系数" Margin="0,14,10,0" />
<TextBlock Grid.Row="4" Grid.Column="2" Text="系数" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="4" Grid.Column="2" Text="系数" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="5" Text="扭矩保护" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="5" Text="扭矩保护" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="5" Grid.Column="1" x:Name="TorqueProtectionInput" Text="{Binding TorqueProtectionInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="5" Grid.Column="1" x:Name="TorqueProtectionInput" Text="{Binding TorqueProtectionInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="扭矩保护" Margin="0,14,10,0" />
<TextBlock Grid.Row="5" Grid.Column="2" Text="mN.m" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="5" Grid.Column="2" Text="mN.m" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="6" Text="转速系数" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="6" Text="转速系数" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="6" Grid.Column="1" x:Name="SpeedCoefficientInput" Text="{Binding SpeedCoefficientInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="6" Grid.Column="1" x:Name="SpeedCoefficientInput" Text="{Binding SpeedCoefficientInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="转速系数" Margin="0,14,10,0" />
<TextBlock Grid.Row="6" Grid.Column="2" Text="系数" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="6" Grid.Column="2" Text="系数" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="7" Text="低速停止" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="7" Text="低速停止" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="7" Grid.Column="1" x:Name="SpeedStopThresholdInput" Text="{Binding SpeedStopThresholdInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="7" Grid.Column="1" x:Name="SpeedStopThresholdInput" Text="{Binding SpeedStopThresholdInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="低速停止" Margin="0,14,10,0" />
<TextBlock Grid.Row="7" Grid.Column="2" Text="r/min" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="7" Grid.Column="2" Text="r/min" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
<TextBlock Grid.Row="8" Text="压力系数" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="8" Text="压力系数" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="8" Grid.Column="1" x:Name="PressureCoefficientInput" Text="{Binding PressureCoefficientInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="8" Grid.Column="1" x:Name="PressureCoefficientInput" Text="{Binding PressureCoefficientInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource DecimalInputTextBox}" local:NumericKeypad.Title="压力系数" Margin="0,14,10,0" />
<TextBlock Grid.Row="8" Grid.Column="2" Text="系数" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Row="8" Grid.Column="2" Text="系数" Style="{StaticResource MutedText}" Margin="0,14,0,0" VerticalAlignment="Center" />
</Grid> </Grid>
</Border> </Border>
@@ -1141,12 +1168,12 @@
<TextBlock Grid.Row="1" Text="IP地址" Style="{StaticResource FormLabel}" Margin="0,16,0,0" /> <TextBlock Grid.Row="1" Text="IP地址" Style="{StaticResource FormLabel}" Margin="0,16,0,0" />
<TextBox Grid.Row="1" Grid.Column="1" x:Name="PlcIpAddressInput" Text="{Binding PlcIpAddressInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,16,10,0" /> <TextBox Grid.Row="1" Grid.Column="1" x:Name="PlcIpAddressInput" Text="{Binding PlcIpAddressInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource IpAddressInputTextBox}" local:NumericKeypad.Title="PLC IP 地址" Margin="0,16,10,0" />
<TextBlock Grid.Row="1" Grid.Column="3" Text="端口" Style="{StaticResource FormLabel}" Margin="0,16,0,0" /> <TextBlock Grid.Row="1" Grid.Column="3" Text="端口" Style="{StaticResource FormLabel}" Margin="0,16,0,0" />
<TextBox Grid.Row="1" Grid.Column="4" x:Name="PlcPortInput" Text="{Binding PlcPortInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,16,10,0" /> <TextBox Grid.Row="1" Grid.Column="4" x:Name="PlcPortInput" Text="{Binding PlcPortInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource IntegerInputTextBox}" local:NumericKeypad.Title="PLC 端口" Margin="0,16,10,0" />
<TextBlock Grid.Row="2" Text="站号" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="2" Text="站号" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<TextBox Grid.Row="2" Grid.Column="1" x:Name="PlcUnitIdInput" Text="{Binding PlcUnitIdInput, UpdateSourceTrigger=PropertyChanged}" Margin="0,14,10,0" /> <TextBox Grid.Row="2" Grid.Column="1" x:Name="PlcUnitIdInput" Text="{Binding PlcUnitIdInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource IntegerInputTextBox}" local:NumericKeypad.Title="PLC 站号" Margin="0,14,10,0" />
<TextBlock Grid.Row="2" Grid.Column="3" Text="脉冲/超时" Style="{StaticResource FormLabel}" Margin="0,14,0,0" /> <TextBlock Grid.Row="2" Grid.Column="3" Text="脉冲/超时" Style="{StaticResource FormLabel}" Margin="0,14,0,0" />
<Grid Grid.Row="2" Grid.Column="4" Grid.ColumnSpan="2" Margin="0,14,0,0"> <Grid Grid.Row="2" Grid.Column="4" Grid.ColumnSpan="2" Margin="0,14,0,0">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -1155,8 +1182,8 @@
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="54" /> <ColumnDefinition Width="54" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBox x:Name="PlcPulseMillisecondsInput" Text="{Binding PlcPulseMillisecondsInput, UpdateSourceTrigger=PropertyChanged}" /> <TextBox x:Name="PlcPulseMillisecondsInput" Text="{Binding PlcPulseMillisecondsInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource IntegerInputTextBox}" local:NumericKeypad.Title="PLC 脉冲时间(毫秒)" />
<TextBox Grid.Column="2" x:Name="PlcTimeoutMillisecondsInput" Text="{Binding PlcTimeoutMillisecondsInput, UpdateSourceTrigger=PropertyChanged}" /> <TextBox Grid.Column="2" x:Name="PlcTimeoutMillisecondsInput" Text="{Binding PlcTimeoutMillisecondsInput, UpdateSourceTrigger=PropertyChanged}" Style="{StaticResource IntegerInputTextBox}" local:NumericKeypad.Title="PLC 超时时间(毫秒)" />
<TextBlock Grid.Column="3" Text="ms" Style="{StaticResource MutedText}" Margin="10,0,0,0" VerticalAlignment="Center" /> <TextBlock Grid.Column="3" Text="ms" Style="{StaticResource MutedText}" Margin="10,0,0,0" VerticalAlignment="Center" />
</Grid> </Grid>
</Grid> </Grid>

View File

@@ -1,6 +1,7 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Net;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using ClosedXML.Excel; using ClosedXML.Excel;
@@ -1909,8 +1910,16 @@ public sealed class MainWindowViewModel : ObservableObject
} }
await _plcRegisterService.WriteFloatAsync(plcConfig, registerAddress, (float)newValue); await _plcRegisterService.WriteFloatAsync(plcConfig, registerAddress, (float)newValue);
float confirmedValue = await _plcRegisterService.ReadFloatAsync(plcConfig, registerAddress);
if (float.IsNaN(confirmedValue)
|| float.IsInfinity(confirmedValue)
|| !AreEquivalentFloatRegisterValues(newValue, confirmedValue))
{
throw new InvalidOperationException($"{fieldName} D{registerAddress} 写入后回读不一致。");
}
changedItems.Add(fieldName); changedItems.Add(fieldName);
Log.Information("PLC参数写入成功参数 {FieldName}D{RegisterAddress}={Value}", fieldName, registerAddress, newValue); Log.Information("PLC参数写入并回读确认成功,参数 {FieldName}D{RegisterAddress}={Value}", fieldName, registerAddress, confirmedValue);
} }
} }
@@ -1943,9 +1952,17 @@ public sealed class MainWindowViewModel : ObservableObject
return false; return false;
} }
if (string.IsNullOrWhiteSpace(PlcIpAddressInput)) string plcIpAddress = PlcIpAddressInput?.Trim() ?? string.Empty;
if (!IPAddress.TryParse(plcIpAddress, out IPAddress? ipAddress)
|| ipAddress.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork)
{ {
error = "PLC IP不能为空。"; error = "PLC IP必须是有效的 IPv4 地址。";
return false;
}
if (plcPort > ushort.MaxValue)
{
error = "PLC端口必须在 1-65535 范围内。";
return false; return false;
} }
@@ -1979,7 +1996,7 @@ public sealed class MainWindowViewModel : ObservableObject
SpeedStopThreshold = speedStopThreshold, SpeedStopThreshold = speedStopThreshold,
PressureCoefficient = pressureCoefficient, PressureCoefficient = pressureCoefficient,
NoLoadSpeedSetting = _parameterConfig.NoLoadSpeedSetting, NoLoadSpeedSetting = _parameterConfig.NoLoadSpeedSetting,
PlcIpAddress = PlcIpAddressInput.Trim(), PlcIpAddress = plcIpAddress,
PlcPort = plcPort, PlcPort = plcPort,
PlcUnitId = plcUnitId, PlcUnitId = plcUnitId,
PlcPulseMilliseconds = plcPulseMilliseconds, PlcPulseMilliseconds = plcPulseMilliseconds,
@@ -2123,9 +2140,11 @@ public sealed class MainWindowViewModel : ObservableObject
PlcConnectionConfig config = _parameterConfig.ToPlcConnectionConfig(); PlcConnectionConfig config = _parameterConfig.ToPlcConnectionConfig();
await _plcRegisterService.WriteFloatAsync(config, registerAddress, (float)value); await _plcRegisterService.WriteFloatAsync(config, registerAddress, (float)value);
float confirmedValue = await _plcRegisterService.ReadFloatAsync(config, registerAddress); float confirmedValue = await _plcRegisterService.ReadFloatAsync(config, registerAddress);
if (float.IsNaN(confirmedValue) || float.IsInfinity(confirmedValue)) if (float.IsNaN(confirmedValue)
|| float.IsInfinity(confirmedValue)
|| !AreEquivalentFloatRegisterValues(value, confirmedValue))
{ {
throw new InvalidOperationException($"{fieldName} D{registerAddress} 回读无效。"); throw new InvalidOperationException($"{fieldName} D{registerAddress} 写入后回读不一致。");
} }
await ApplyConfirmedTestPageInputAsync(parameter, fieldName, registerAddress, value, confirmedValue); await ApplyConfirmedTestPageInputAsync(parameter, fieldName, registerAddress, value, confirmedValue);
@@ -2150,6 +2169,11 @@ public sealed class MainWindowViewModel : ObservableObject
PlcConnectionConfig config = _parameterConfig.ToPlcConnectionConfig(); PlcConnectionConfig config = _parameterConfig.ToPlcConnectionConfig();
await _plcRegisterService.WriteUInt16Async(config, registerAddress, rawValue); await _plcRegisterService.WriteUInt16Async(config, registerAddress, rawValue);
ushort confirmedRawValue = await _plcRegisterService.ReadUInt16Async(config, registerAddress); ushort confirmedRawValue = await _plcRegisterService.ReadUInt16Async(config, registerAddress);
if (confirmedRawValue != rawValue)
{
throw new InvalidOperationException($"{fieldName} D{registerAddress} 写入后回读不一致。");
}
double confirmedValue = ScaleTenthsFromPlc(confirmedRawValue); double confirmedValue = ScaleTenthsFromPlc(confirmedRawValue);
await ApplyConfirmedTestPageInputAsync(parameter, fieldName, registerAddress, value, confirmedValue); await ApplyConfirmedTestPageInputAsync(parameter, fieldName, registerAddress, value, confirmedValue);
} }
@@ -3519,7 +3543,7 @@ public sealed class MainWindowViewModel : ObservableObject
private static string FormatConfigNumber(double value) private static string FormatConfigNumber(double value)
{ {
return value.ToString("0.###", CultureInfo.InvariantCulture); return value.ToString("0.########", CultureInfo.InvariantCulture);
} }
private static bool AreEquivalentFloatRegisterValues(double left, double right) private static bool AreEquivalentFloatRegisterValues(double left, double right)
@@ -3555,7 +3579,14 @@ public sealed class MainWindowViewModel : ObservableObject
private static bool CanScaleTenthsToPlc(double value, string fieldName, out string error) private static bool CanScaleTenthsToPlc(double value, string fieldName, out string error)
{ {
error = string.Empty; error = string.Empty;
double scaledValue = Math.Round(value * TenthsRegisterScale, MidpointRounding.AwayFromZero); double unroundedScaledValue = value * TenthsRegisterScale;
double scaledValue = Math.Round(unroundedScaledValue, MidpointRounding.AwayFromZero);
if (Math.Abs(unroundedScaledValue - scaledValue) > 0.0000001)
{
error = $"{fieldName}最多支持 1 位小数。";
return false;
}
if (scaledValue > ushort.MaxValue) if (scaledValue > ushort.MaxValue)
{ {
error = $"{fieldName}不能大于 {FormatConfigNumber(ushort.MaxValue / TenthsRegisterScale)}。"; error = $"{fieldName}不能大于 {FormatConfigNumber(ushort.MaxValue / TenthsRegisterScale)}。";
@@ -3600,13 +3631,16 @@ public sealed class MainWindowViewModel : ObservableObject
private static bool TryReadNumber(string input, string fieldName, out double value, out string error) private static bool TryReadNumber(string input, string fieldName, out double value, out string error)
{ {
error = string.Empty; error = string.Empty;
if (double.TryParse(input, NumberStyles.Float, CultureInfo.CurrentCulture, out value) if ((double.TryParse(input, NumberStyles.Float, CultureInfo.CurrentCulture, out value)
|| double.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out value)) || double.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out value))
&& !double.IsNaN(value)
&& !double.IsInfinity(value)
&& Math.Abs(value) <= float.MaxValue)
{ {
return true; return true;
} }
error = $"{fieldName}不是有效数。"; error = $"{fieldName}不是有效的 PLC 浮点数。";
return false; return false;
} }

View File

@@ -0,0 +1,141 @@
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
namespace DentistryHandpieces;
public enum NumericInputMode
{
None,
Decimal,
Integer,
IpAddress
}
public static class NumericKeypad
{
public static readonly DependencyProperty InputModeProperty = DependencyProperty.RegisterAttached(
"InputMode",
typeof(NumericInputMode),
typeof(NumericKeypad),
new PropertyMetadata(NumericInputMode.None, OnInputModeChanged));
public static readonly DependencyProperty TitleProperty = DependencyProperty.RegisterAttached(
"Title",
typeof(string),
typeof(NumericKeypad),
new PropertyMetadata("请输入数值"));
public static readonly DependencyProperty MaxDecimalPlacesProperty = DependencyProperty.RegisterAttached(
"MaxDecimalPlaces",
typeof(int),
typeof(NumericKeypad),
new PropertyMetadata(8));
public static readonly DependencyProperty AllowNegativeProperty = DependencyProperty.RegisterAttached(
"AllowNegative",
typeof(bool),
typeof(NumericKeypad),
new PropertyMetadata(false));
public static void SetInputMode(DependencyObject element, NumericInputMode value) => element.SetValue(InputModeProperty, value);
public static NumericInputMode GetInputMode(DependencyObject element) => (NumericInputMode)element.GetValue(InputModeProperty);
public static void SetTitle(DependencyObject element, string value) => element.SetValue(TitleProperty, value);
public static string GetTitle(DependencyObject element) => (string)element.GetValue(TitleProperty);
public static void SetMaxDecimalPlaces(DependencyObject element, int value) => element.SetValue(MaxDecimalPlacesProperty, value);
public static int GetMaxDecimalPlaces(DependencyObject element) => (int)element.GetValue(MaxDecimalPlacesProperty);
public static void SetAllowNegative(DependencyObject element, bool value) => element.SetValue(AllowNegativeProperty, value);
public static bool GetAllowNegative(DependencyObject element) => (bool)element.GetValue(AllowNegativeProperty);
private static void OnInputModeChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
{
if (dependencyObject is not TextBox textBox)
{
return;
}
textBox.PreviewMouseLeftButtonDown -= TextBox_PreviewMouseLeftButtonDown;
textBox.PreviewTouchDown -= TextBox_PreviewTouchDown;
textBox.PreviewKeyDown -= TextBox_PreviewKeyDown;
NumericInputMode mode = (NumericInputMode)e.NewValue;
if (mode == NumericInputMode.None)
{
textBox.IsReadOnly = false;
textBox.Cursor = Cursors.IBeam;
return;
}
textBox.IsReadOnly = true;
textBox.Cursor = Cursors.Hand;
textBox.PreviewMouseLeftButtonDown += TextBox_PreviewMouseLeftButtonDown;
textBox.PreviewTouchDown += TextBox_PreviewTouchDown;
textBox.PreviewKeyDown += TextBox_PreviewKeyDown;
}
private static void TextBox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
ShowKeypad((TextBox)sender);
}
private static void TextBox_PreviewTouchDown(object? sender, TouchEventArgs e)
{
if (sender is not TextBox textBox)
{
return;
}
e.Handled = true;
ShowKeypad(textBox);
}
private static void TextBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key is not (Key.Enter or Key.Space))
{
return;
}
e.Handled = true;
ShowKeypad((TextBox)sender);
}
private static void ShowKeypad(TextBox textBox)
{
NumericInputMode mode = GetInputMode(textBox);
if (mode == NumericInputMode.None || !textBox.IsEnabled)
{
return;
}
var keypad = new NumericKeypadWindow(
GetTitle(textBox),
textBox.Text,
mode,
Math.Max(0, GetMaxDecimalPlaces(textBox)),
GetAllowNegative(textBox))
{
Owner = Window.GetWindow(textBox)
};
if (keypad.ShowDialog() != true)
{
return;
}
textBox.Text = keypad.ResultText;
BindingExpression? binding = textBox.GetBindingExpression(TextBox.TextProperty);
binding?.UpdateSource();
textBox.CaretIndex = textBox.Text.Length;
}
}

View File

@@ -0,0 +1,117 @@
<Window x:Class="DentistryHandpieces.NumericKeypadWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="数字输入"
Width="560"
Height="650"
ResizeMode="NoResize"
WindowStartupLocation="CenterOwner"
FontFamily="Microsoft YaHei UI"
FontSize="18"
Background="#EAF0F4"
PreviewKeyDown="Window_PreviewKeyDown">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Margin" Value="5" />
<Setter Property="FontSize" Value="24" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Background" Value="#FFFFFF" />
<Setter Property="Foreground" Value="#17212B" />
<Setter Property="BorderBrush" Value="#B8C5D1" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
</Style>
<Style x:Key="FunctionButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="FontSize" Value="20" />
<Setter Property="Background" Value="#DDE6ED" />
</Style>
<Style x:Key="ConfirmButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="FontSize" Value="20" />
<Setter Property="Background" Value="#0F766E" />
<Setter Property="Foreground" Value="White" />
<Setter Property="BorderBrush" Value="#0A5D56" />
</Style>
</Window.Resources>
<Grid Margin="18">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock x:Name="PromptText"
FontSize="22"
FontWeight="Bold"
Foreground="#1F3344"
Margin="2,0,2,10" />
<Border Grid.Row="1"
Background="White"
BorderBrush="#9FB0BE"
BorderThickness="2"
CornerRadius="6"
Padding="14,10"
Margin="0,0,0,8">
<TextBlock x:Name="ValueText"
MinHeight="42"
FontFamily="Consolas"
FontSize="30"
FontWeight="SemiBold"
TextAlignment="Right"
VerticalAlignment="Center"
TextTrimming="CharacterEllipsis" />
</Border>
<Grid Grid.Row="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock x:Name="StatusText"
Foreground="#A33A2A"
FontSize="15"
Margin="4,0,4,4" />
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="0.85*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Content="7" Tag="7" Click="DigitButton_Click" />
<Button Grid.Column="1" Content="8" Tag="8" Click="DigitButton_Click" />
<Button Grid.Column="2" Content="9" Tag="9" Click="DigitButton_Click" />
<Button Grid.Column="3" Content="退格" Style="{StaticResource FunctionButton}" Click="BackspaceButton_Click" />
<Button Grid.Row="1" Content="4" Tag="4" Click="DigitButton_Click" />
<Button Grid.Row="1" Grid.Column="1" Content="5" Tag="5" Click="DigitButton_Click" />
<Button Grid.Row="1" Grid.Column="2" Content="6" Tag="6" Click="DigitButton_Click" />
<Button Grid.Row="1" Grid.Column="3" Content="清空" Style="{StaticResource FunctionButton}" Click="ClearButton_Click" />
<Button Grid.Row="2" Content="1" Tag="1" Click="DigitButton_Click" />
<Button Grid.Row="2" Grid.Column="1" Content="2" Tag="2" Click="DigitButton_Click" />
<Button Grid.Row="2" Grid.Column="2" Content="3" Tag="3" Click="DigitButton_Click" />
<Button x:Name="SignButton" Grid.Row="2" Grid.Column="3" Content="+/-" Style="{StaticResource FunctionButton}" Click="SignButton_Click" />
<Button Grid.Row="3" Grid.ColumnSpan="2" Content="0" Tag="0" Click="DigitButton_Click" />
<Button x:Name="DecimalButton" Grid.Row="3" Grid.Column="2" Content="." Tag="." Click="DecimalButton_Click" />
<Button Grid.Row="3" Grid.Column="3" Content="确定" Style="{StaticResource ConfirmButton}" Click="ConfirmButton_Click" />
<Button Grid.Row="4"
Grid.ColumnSpan="4"
Content="取消"
Style="{StaticResource FunctionButton}"
Click="CancelButton_Click" />
</Grid>
</Grid>
</Grid>
</Window>

View File

@@ -0,0 +1,284 @@
using System.Globalization;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace DentistryHandpieces;
public partial class NumericKeypadWindow : Window
{
private const int MaxInputLength = 32;
private readonly NumericInputMode _mode;
private readonly int _maxDecimalPlaces;
private readonly bool _allowNegative;
private string _input;
public NumericKeypadWindow(string prompt, string currentValue, NumericInputMode mode, int maxDecimalPlaces, bool allowNegative)
{
InitializeComponent();
_mode = mode;
_maxDecimalPlaces = maxDecimalPlaces;
_allowNegative = allowNegative;
_input = NormalizeInitialValue(currentValue);
PromptText.Text = string.IsNullOrWhiteSpace(prompt) ? "请输入数值" : prompt;
DecimalButton.IsEnabled = mode is NumericInputMode.Decimal or NumericInputMode.IpAddress;
SignButton.IsEnabled = allowNegative && mode != NumericInputMode.IpAddress;
UpdateDisplay();
}
public string ResultText { get; private set; } = string.Empty;
private void DigitButton_Click(object sender, RoutedEventArgs e)
{
if (sender is Button { Tag: string digit })
{
Append(digit);
}
}
private void DecimalButton_Click(object sender, RoutedEventArgs e)
{
if (_mode == NumericInputMode.Integer)
{
return;
}
if (_mode == NumericInputMode.Decimal)
{
if (_input.Contains('.', StringComparison.Ordinal))
{
SetStatus("只能输入一个小数点。");
return;
}
Append(_input is "" or "-" ? "0." : ".");
return;
}
int dotCount = _input.Count(static character => character == '.');
if (dotCount >= 3 || _input.EndsWith(".", StringComparison.Ordinal))
{
SetStatus("IP 地址需要 4 段数字,段与段之间只能有一个点。");
return;
}
Append(".");
}
private void BackspaceButton_Click(object sender, RoutedEventArgs e)
{
if (_input.Length > 0)
{
_input = _input[..^1];
UpdateDisplay();
}
}
private void ClearButton_Click(object sender, RoutedEventArgs e)
{
_input = string.Empty;
UpdateDisplay();
}
private void SignButton_Click(object sender, RoutedEventArgs e)
{
if (!_allowNegative || _mode == NumericInputMode.IpAddress)
{
return;
}
_input = _input.StartsWith("-", StringComparison.Ordinal) ? _input[1..] : $"-{_input}";
UpdateDisplay();
}
private void ConfirmButton_Click(object sender, RoutedEventArgs e)
{
Confirm();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key is >= Key.D0 and <= Key.D9)
{
Append(((int)e.Key - (int)Key.D0).ToString(CultureInfo.InvariantCulture));
e.Handled = true;
return;
}
if (e.Key is >= Key.NumPad0 and <= Key.NumPad9)
{
Append(((int)e.Key - (int)Key.NumPad0).ToString(CultureInfo.InvariantCulture));
e.Handled = true;
return;
}
switch (e.Key)
{
case Key.Decimal:
case Key.OemPeriod:
DecimalButton_Click(DecimalButton, e);
e.Handled = true;
break;
case Key.Back:
BackspaceButton_Click(this, e);
e.Handled = true;
break;
case Key.Delete:
ClearButton_Click(this, e);
e.Handled = true;
break;
case Key.Enter:
Confirm();
e.Handled = true;
break;
case Key.Escape:
DialogResult = false;
e.Handled = true;
break;
}
}
private void Append(string text)
{
if (_input.Length + text.Length > MaxInputLength)
{
SetStatus($"输入内容不能超过 {MaxInputLength} 个字符。");
return;
}
if (_mode == NumericInputMode.Decimal)
{
int dotIndex = _input.IndexOf('.');
if (dotIndex >= 0 && char.IsDigit(text[0]) && _input.Length - dotIndex - 1 >= _maxDecimalPlaces)
{
SetStatus($"该数值最多支持 {_maxDecimalPlaces} 位小数。");
return;
}
}
else if (_mode == NumericInputMode.IpAddress)
{
string activeSegment = _input.Split('.').LastOrDefault() ?? string.Empty;
if (char.IsDigit(text[0]) && activeSegment.Length >= 3)
{
SetStatus("IP 地址每段最多 3 位数字。");
return;
}
}
_input += text;
UpdateDisplay();
}
private void Confirm()
{
if (!TryValidateAndNormalize(out string normalized, out string error))
{
SetStatus(error);
return;
}
ResultText = normalized;
DialogResult = true;
}
private bool TryValidateAndNormalize(out string normalized, out string error)
{
normalized = string.Empty;
error = string.Empty;
if (string.IsNullOrWhiteSpace(_input))
{
error = "请输入数值。";
return false;
}
if (_mode == NumericInputMode.IpAddress)
{
string[] segments = _input.Split('.');
if (segments.Length != 4)
{
error = "IP 地址必须由 4 段数字组成。";
return false;
}
var normalizedSegments = new string[4];
for (int index = 0; index < segments.Length; index++)
{
if (!byte.TryParse(segments[index], NumberStyles.None, CultureInfo.InvariantCulture, out byte segmentValue))
{
error = "IP 地址每段必须是 0-255 的整数。";
return false;
}
normalizedSegments[index] = segmentValue.ToString(CultureInfo.InvariantCulture);
}
normalized = string.Join('.', normalizedSegments);
return true;
}
if (_mode == NumericInputMode.Integer)
{
if ((!_allowNegative && !ulong.TryParse(_input, NumberStyles.None, CultureInfo.InvariantCulture, out _))
|| (_allowNegative && !long.TryParse(_input, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out _)))
{
error = "请输入有效整数。";
return false;
}
normalized = _input;
return true;
}
int decimalPlaces = GetDecimalPlaces(_input);
if (decimalPlaces > _maxDecimalPlaces)
{
error = $"该数值最多支持 {_maxDecimalPlaces} 位小数。";
return false;
}
NumberStyles styles = NumberStyles.AllowDecimalPoint;
if (_allowNegative)
{
styles |= NumberStyles.AllowLeadingSign;
}
if (!double.TryParse(_input, styles, CultureInfo.InvariantCulture, out double value)
|| double.IsNaN(value)
|| double.IsInfinity(value))
{
error = "请输入有效数字。";
return false;
}
normalized = _input.EndsWith(".", StringComparison.Ordinal) ? _input[..^1] : _input;
return true;
}
private void UpdateDisplay()
{
ValueText.Text = string.IsNullOrEmpty(_input) ? " " : _input;
StatusText.Text = string.Empty;
}
private void SetStatus(string message)
{
StatusText.Text = message;
}
private static int GetDecimalPlaces(string input)
{
int dotIndex = input.IndexOf('.');
return dotIndex < 0 ? 0 : input.Length - dotIndex - 1;
}
private static string NormalizeInitialValue(string value)
{
return (value ?? string.Empty).Trim().Replace(',', '.');
}
}