Compare commits

...

2 Commits

Author SHA1 Message Date
GukSang.Jin
1cc402450b gx 2026-06-09 16:19:06 +08:00
GukSang.Jin
e8e81b1e46 更新121 2026-06-09 15:17:53 +08:00
7 changed files with 728 additions and 57 deletions

View File

@@ -102,9 +102,9 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
return;
}
if (!TryParseInt32(LoadSpeedSettingInput, out int loadSpeedSetting))
if (!TryParseFloat(LoadSpeedSettingInput, out float loadSpeedSetting))
{
StatusText = "负载转速设置必须是有效的 32 位整数。";
StatusText = "负载转速设置必须是有效数。";
return;
}
@@ -112,9 +112,16 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
StatusText = "正在保存负载转速设置...";
try
{
await _plcRegisterService.WriteInt32Async(_plcConfig, LoadSpeedSettingRegister, loadSpeedSetting);
int confirmedValue = await _plcRegisterService.ReadInt32Async(_plcConfig, LoadSpeedSettingRegister);
LoadSpeedSettingInput = confirmedValue.ToString(CultureInfo.InvariantCulture);
await _plcRegisterService.WriteFloatAsync(_plcConfig, LoadSpeedSettingRegister, loadSpeedSetting);
float confirmedValue = await _plcRegisterService.ReadFloatAsync(_plcConfig, LoadSpeedSettingRegister);
if (float.IsNaN(confirmedValue)
|| float.IsInfinity(confirmedValue)
|| !AreEquivalentFloatRegisterValues(loadSpeedSetting, confirmedValue))
{
throw new InvalidOperationException("PLC 写入后回读不一致。");
}
LoadSpeedSettingInput = FormatNumber(confirmedValue);
StatusText = "负载转速设置已保存并回读确认。";
}
catch (Exception ex)
@@ -146,12 +153,14 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
{
await _plcRegisterService.WriteFloatAsync(_plcConfig, row.Address, value);
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}已保存并回读确认。";
}
catch (Exception ex)
@@ -166,11 +175,16 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
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();
IReadOnlyDictionary<ushort, float> values = await _plcRegisterService.ReadFloatValuesAsync(_plcConfig, addresses);
LoadSpeedSettingInput = loadSpeedSetting.ToString(CultureInfo.InvariantCulture);
LoadSpeedSettingInput = FormatNumber(loadSpeedSetting);
foreach (SpeedCoefficientSettingRow row in SpeedCoefficientSettings)
{
if (!values.TryGetValue(row.Address, out float value)
@@ -180,14 +194,13 @@ public sealed class HiddenSpeedSettingsViewModel : ObservableObject
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)
|| int.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
return value.ToString("0.########", CultureInfo.InvariantCulture);
}
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);
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"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:DentistryHandpieces"
Title="转速隐藏参数设置"
Width="920"
Height="720"
@@ -19,6 +20,11 @@
<Setter Property="Background" Value="#F8FAFC" />
<Setter Property="BorderBrush" Value="#B8C5D1" />
</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">
<Setter Property="MinHeight" Value="44" />
<Setter Property="Padding" Value="18,8" />
@@ -54,8 +60,10 @@
VerticalAlignment="Center"
Foreground="#22313F" />
<TextBox Grid.Column="1"
Text="{Binding LoadSpeedSettingInput, UpdateSourceTrigger=PropertyChanged}"
Margin="12,0" />
Text="{Binding LoadSpeedSettingInput, UpdateSourceTrigger=PropertyChanged}"
Style="{StaticResource HiddenDecimalInputTextBox}"
local:NumericKeypad.Title="负载转速设置"
Margin="12,0" />
<Button Grid.Column="2"
Content="保存"
Command="{Binding SaveLoadSpeedSettingCommand}"
@@ -88,7 +96,9 @@
<ColumnDefinition Width="92" />
</Grid.ColumnDefinitions>
<TextBox Text="{Binding ValueText, UpdateSourceTrigger=PropertyChanged}"
Margin="0,0,8,0" />
Style="{StaticResource HiddenDecimalInputTextBox}"
local:NumericKeypad.Title="{Binding RangeText}"
Margin="0,0,8,0" />
<Button Grid.Column="1"
Content="保存"
Command="{Binding DataContext.SaveSpeedCoefficientCommand, RelativeSource={RelativeSource AncestorType=Window}}"

View File

@@ -27,6 +27,23 @@
<Setter Property="BorderThickness" Value="1" />
</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">
<Setter Property="MinHeight" Value="46" />
<Setter Property="Padding" Value="18,8" />
@@ -489,7 +506,9 @@
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<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"
Text="N"
Style="{StaticResource MutedText}"
@@ -505,7 +524,9 @@
<ColumnDefinition Width="50" />
</Grid.ColumnDefinitions>
<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"
Text="s"
Style="{StaticResource MutedText}"
@@ -769,7 +790,9 @@
Margin="0,0,12,0" />
<TextBox Grid.Column="1"
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"
Text="mN.m"
Style="{StaticResource MutedText}"
@@ -788,7 +811,9 @@
Margin="0,0,12,0" />
<TextBox Grid.Column="1"
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"
Text="s"
Style="{StaticResource MutedText}"
@@ -871,7 +896,7 @@
LostTouchCapture="ManualMotionButton_LostTouchCapture"
Margin="8,0,8,0" />
<Button Grid.Column="2"
Content="通气阀"
Content="{Binding VentValveButtonText}"
Command="{Binding VentValveCommand}"
Margin="8,0,8,0" />
<Button Grid.Column="3"
@@ -971,6 +996,8 @@
<TextBox Grid.Row="1"
Grid.Column="1"
Text="{Binding NoLoadSpeedSettingInput, UpdateSourceTrigger=PropertyChanged, Delay=600}"
Style="{StaticResource DecimalInputTextBox}"
local:NumericKeypad.Title="目标空载转速"
Margin="0,16,12,0" />
<TextBlock Grid.Row="1"
Grid.Column="2"
@@ -1039,31 +1066,31 @@
<TextBlock Grid.ColumnSpan="3" Text="轴向移动量参数2号轴" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" />
<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="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="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="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="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="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="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" />
</Grid>
</Border>
@@ -1090,35 +1117,35 @@
<TextBlock Grid.ColumnSpan="3" Text="转速/扭矩参数1号轴" Style="{StaticResource MetricTitle}" HorizontalAlignment="Left" />
<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="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="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="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="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="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="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="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" />
</Grid>
</Border>
@@ -1141,12 +1168,12 @@
<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" />
<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" />
<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" />
<Grid Grid.Row="2" Grid.Column="4" Grid.ColumnSpan="2" Margin="0,14,0,0">
<Grid.ColumnDefinitions>
@@ -1155,8 +1182,8 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="54" />
</Grid.ColumnDefinitions>
<TextBox x:Name="PlcPulseMillisecondsInput" Text="{Binding PlcPulseMillisecondsInput, UpdateSourceTrigger=PropertyChanged}" />
<TextBox Grid.Column="2" x:Name="PlcTimeoutMillisecondsInput" Text="{Binding PlcTimeoutMillisecondsInput, 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}" Style="{StaticResource IntegerInputTextBox}" local:NumericKeypad.Title="PLC 超时时间(毫秒)" />
<TextBlock Grid.Column="3" Text="ms" Style="{StaticResource MutedText}" Margin="10,0,0,0" VerticalAlignment="Center" />
</Grid>
</Grid>

View File

@@ -1,6 +1,7 @@
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text.Json;
using System.Text.Json.Serialization;
using ClosedXML.Excel;
@@ -41,6 +42,7 @@ public sealed class MainWindowViewModel : ObservableObject
private const ushort VentValveCoil = 6;
private const ushort AxialForceModeCoil = 30;
private const ushort AxialStartCoil = 70;
private const ushort AxialDoneCoil = 72;
private const ushort AxialStopCoil = 73;
private const ushort SpeedTorqueStartCoil = 80;
private const ushort SpeedTorqueDoneCoil = 82;
@@ -136,6 +138,8 @@ public sealed class MainWindowViewModel : ObservableObject
private static readonly ushort[] RealtimeCoilAddresses =
[
VentValveCoil,
AxialDoneCoil,
SpeedTorqueDoneCoil,
SpeedTorqueResetEnabledCoil,
SpeedTorqueResetDoneCoil,
@@ -203,6 +207,7 @@ public sealed class MainWindowViewModel : ObservableObject
private bool _isApplyingParameterConfigToInputs;
private bool _isDisplacementResetting;
private bool _isSpeedTorqueResetting;
private bool _isVentValveOpen;
private bool _hasShownSpeedTorqueEndWarnings;
private DateTime _lastParameterReadFailureLogAt = DateTime.MinValue;
private string _relativeDisplacementText = "0.000 mm";
@@ -238,6 +243,7 @@ public sealed class MainWindowViewModel : ObservableObject
private string _axialForceSetpointModeButtonText = "轴向跳动力设置";
private string _displacementResetButtonText = "复位";
private string _speedTorqueResetButtonText = "复位";
private string _ventValveButtonText = "开启通气阀";
public MainWindowViewModel(IPlcCoilService plcCoilService, IPlcRegisterService plcRegisterService, IFileDialogService fileDialogService)
{
@@ -261,7 +267,7 @@ public sealed class MainWindowViewModel : ObservableObject
SelectAxialJumpForceSetpointModeCommand = new AsyncRelayCommand(SelectAxialJumpForceSetpointModeAsync);
ForwardSpeedTorqueCommand = new AsyncRelayCommand(ForwardSpeedTorqueAsync);
BackwardSpeedTorqueCommand = new AsyncRelayCommand(BackwardSpeedTorqueAsync);
VentValveCommand = new AsyncRelayCommand(TriggerVentValveAsync);
VentValveCommand = new AsyncRelayCommand(ToggleVentValveAsync);
StartSpeedTorqueCommand = new AsyncRelayCommand(StartSpeedTorqueAsync);
StopSpeedTorqueCommand = new AsyncRelayCommand(StopSpeedTorqueAsync);
ResetSpeedTorqueCommand = new AsyncRelayCommand(ResetSpeedTorqueAsync);
@@ -662,6 +668,12 @@ public sealed class MainWindowViewModel : ObservableObject
private set => SetProperty(ref _speedTorqueResetButtonText, value);
}
public string VentValveButtonText
{
get => _ventValveButtonText;
private set => SetProperty(ref _ventValveButtonText, value);
}
private async void RealtimeTimer_Tick(object? sender, EventArgs e)
{
if (_isReadingRealtime)
@@ -697,6 +709,7 @@ public sealed class MainWindowViewModel : ObservableObject
_realtimeSpeed = realtimeSpeed;
AppendTorqueSample(GetScaledTorque(), DateTime.Now);
ApplyResetCoilValues(coilValues);
UpdateVentValveState(ReadCoilValue(coilValues, VentValveCoil));
CaptureRealtimeSample(dialIndicator, coilValues);
FinalizeNoLoadSpeedRunIfDue();
QueueSnapshotIfDue();
@@ -706,6 +719,11 @@ public sealed class MainWindowViewModel : ObservableObject
_maxDisplacement = Math.Max(_maxDisplacement, Math.Abs(_relativeDisplacement));
}
if (_isDisplacementRunning && ReadCoilValue(coilValues, AxialDoneCoil))
{
StopDisplacementTest("状态:已完成");
}
if (_isSpeedTorqueRunning && ReadCoilValue(coilValues, SpeedTorqueDoneCoil))
{
await StopSpeedTorqueTestAsync("状态:已完成");
@@ -1892,8 +1910,16 @@ public sealed class MainWindowViewModel : ObservableObject
}
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);
Log.Information("PLC参数写入成功参数 {FieldName}D{RegisterAddress}={Value}", fieldName, registerAddress, newValue);
Log.Information("PLC参数写入并回读确认成功,参数 {FieldName}D{RegisterAddress}={Value}", fieldName, registerAddress, confirmedValue);
}
}
@@ -1926,9 +1952,17 @@ public sealed class MainWindowViewModel : ObservableObject
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;
}
@@ -1962,7 +1996,7 @@ public sealed class MainWindowViewModel : ObservableObject
SpeedStopThreshold = speedStopThreshold,
PressureCoefficient = pressureCoefficient,
NoLoadSpeedSetting = _parameterConfig.NoLoadSpeedSetting,
PlcIpAddress = PlcIpAddressInput.Trim(),
PlcIpAddress = plcIpAddress,
PlcPort = plcPort,
PlcUnitId = plcUnitId,
PlcPulseMilliseconds = plcPulseMilliseconds,
@@ -2040,11 +2074,28 @@ public sealed class MainWindowViewModel : ObservableObject
await MoveSpeedTorqueDisplacementAsync();
}
private async Task TriggerVentValveAsync()
private async Task ToggleVentValveAsync()
{
if (await PulsePlcAsync(VentValveCoil, "通气阀"))
try
{
StatusText = "通气阀已触发。";
IReadOnlyDictionary<ushort, bool> values = await _plcCoilService.ReadCoilValuesAsync(
_parameterConfig.ToPlcConnectionConfig(),
[VentValveCoil]);
bool targetState = !ReadCoilValue(values, VentValveCoil);
await _plcCoilService.WriteCoilAsync(
_parameterConfig.ToPlcConnectionConfig(),
VentValveCoil,
targetState);
UpdateVentValveState(targetState);
StatusText = targetState ? "通气阀已开启。" : "通气阀已关闭。";
Log.Information("PLC常开触点开关成功通气阀M{CoilAddress}={Value}", VentValveCoil, targetState ? 1 : 0);
}
catch (Exception ex)
{
StatusText = $"PLC 通气阀开关失败:{OperatorMessageFormatter.FromException(ex)}";
Log.Error(ex, "PLC常开触点开关失败通气阀M{CoilAddress}", VentValveCoil);
}
}
@@ -2089,9 +2140,11 @@ public sealed class MainWindowViewModel : ObservableObject
PlcConnectionConfig config = _parameterConfig.ToPlcConnectionConfig();
await _plcRegisterService.WriteFloatAsync(config, registerAddress, (float)value);
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);
@@ -2116,6 +2169,11 @@ public sealed class MainWindowViewModel : ObservableObject
PlcConnectionConfig config = _parameterConfig.ToPlcConnectionConfig();
await _plcRegisterService.WriteUInt16Async(config, registerAddress, rawValue);
ushort confirmedRawValue = await _plcRegisterService.ReadUInt16Async(config, registerAddress);
if (confirmedRawValue != rawValue)
{
throw new InvalidOperationException($"{fieldName} D{registerAddress} 写入后回读不一致。");
}
double confirmedValue = ScaleTenthsFromPlc(confirmedRawValue);
await ApplyConfirmedTestPageInputAsync(parameter, fieldName, registerAddress, value, confirmedValue);
}
@@ -2838,6 +2896,12 @@ public sealed class MainWindowViewModel : ObservableObject
SpeedTorqueResetButtonText = _isSpeedTorqueResetting || enabled ? "复位中" : "复位";
}
private void UpdateVentValveState(bool isOpen)
{
_isVentValveOpen = isOpen;
VentValveButtonText = _isVentValveOpen ? "关闭通气阀" : "开启通气阀";
}
private static bool ReadCoilValue(IReadOnlyDictionary<ushort, bool> coilValues, ushort address)
{
return coilValues.TryGetValue(address, out bool value) && value;
@@ -3479,7 +3543,7 @@ public sealed class MainWindowViewModel : ObservableObject
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)
@@ -3515,7 +3579,14 @@ public sealed class MainWindowViewModel : ObservableObject
private static bool CanScaleTenthsToPlc(double value, string fieldName, out string error)
{
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)
{
error = $"{fieldName}不能大于 {FormatConfigNumber(ushort.MaxValue / TenthsRegisterScale)}。";
@@ -3560,13 +3631,16 @@ public sealed class MainWindowViewModel : ObservableObject
private static bool TryReadNumber(string input, string fieldName, out double value, out string error)
{
error = string.Empty;
if (double.TryParse(input, NumberStyles.Float, CultureInfo.CurrentCulture, out value)
|| double.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out value))
if ((double.TryParse(input, NumberStyles.Float, CultureInfo.CurrentCulture, out value)
|| double.TryParse(input, NumberStyles.Float, CultureInfo.InvariantCulture, out value))
&& !double.IsNaN(value)
&& !double.IsInfinity(value)
&& Math.Abs(value) <= float.MaxValue)
{
return true;
}
error = $"{fieldName}不是有效数。";
error = $"{fieldName}不是有效的 PLC 浮点数。";
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(',', '.');
}
}