Compare commits
2 Commits
90a5e93833
...
1cc402450b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1cc402450b | ||
|
|
e8e81b1e46 |
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}}"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
141
DentistryHandpieces/NumericKeypad.cs
Normal file
141
DentistryHandpieces/NumericKeypad.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
117
DentistryHandpieces/NumericKeypadWindow.xaml
Normal file
117
DentistryHandpieces/NumericKeypadWindow.xaml
Normal 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>
|
||||
284
DentistryHandpieces/NumericKeypadWindow.xaml.cs
Normal file
284
DentistryHandpieces/NumericKeypadWindow.xaml.cs
Normal 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(',', '.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user