Compare commits

...

6 Commits

Author SHA1 Message Date
xyy
264cee4e1a 2026-04-01 21:30:13 +08:00
xyy
c08d568570 2026-04-01 20:34:51 +08:00
xyy
4a6393ba8d 2026-04-01 20:33:23 +08:00
xyy
05c38a7dab 2026-04-01 20:29:45 +08:00
xyy
dd0bd405ee 2026-03-25 21:34:41 +08:00
xyy
01a07f8811 2026-03-24 21:05:32 +08:00
43 changed files with 1451 additions and 2446 deletions

View File

@@ -1,7 +1,13 @@
<Application x:Class="MembranePoreTester.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Views/MainWindow.xaml">
StartupUri="Views/TestView.xaml">
<Application.Resources>
<!-- 全局样式定义 -->
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,54 +0,0 @@
using OfficeOpenXml;
using System.IO;
public static class ExportHelper
{
public static void ExportBubblePoint(BubblePointEntity entity, string filePath)
{
using var package = new ExcelPackage();
var sheet = package.Workbook.Worksheets.Add("泡点法测试");
sheet.Cells["A1"].Value = "工位"; sheet.Cells["B1"].Value = entity.StationId;
sheet.Cells["A2"].Value = "测试日期"; sheet.Cells["B2"].Value = entity.TestDate.ToString("yyyy-MM-dd HH:mm:ss");
sheet.Cells["A3"].Value = "测试者"; sheet.Cells["B3"].Value = entity.Tester;
sheet.Cells["A4"].Value = "样品类型"; sheet.Cells["B4"].Value = entity.SampleType;
sheet.Cells["A5"].Value = "规格"; sheet.Cells["B5"].Value = entity.SampleSpec;
sheet.Cells["A6"].Value = "室温(°C)"; sheet.Cells["B6"].Value = entity.RoomTemperature;
sheet.Cells["A7"].Value = "浸润时间(h)"; sheet.Cells["B7"].Value = entity.SoakingTime;
sheet.Cells["A8"].Value = "测试液体"; sheet.Cells["B8"].Value = entity.LiquidName;
sheet.Cells["A9"].Value = "液体生产厂家"; sheet.Cells["B9"].Value = entity.LiquidManufacturer;
sheet.Cells["A10"].Value = "泡点压力"; sheet.Cells["B10"].Value = $"{entity.BubblePointPressure} {entity.PressureUnit}";
sheet.Cells["A11"].Value = "最大孔径(μm)"; sheet.Cells["B11"].Value = entity.MaxPoreSize;
package.SaveAs(new FileInfo(filePath));
}
public static void ExportPoreDistribution(PoreDistributionEntity entity, string filePath)
{
using var package = new ExcelPackage();
var sheet = package.Workbook.Worksheets.Add("孔分布测试");
sheet.Cells["A1"].Value = "工位"; sheet.Cells["B1"].Value = entity.StationId;
sheet.Cells["A2"].Value = "测试日期"; sheet.Cells["B2"].Value = entity.TestDate.ToString("yyyy-MM-dd HH:mm:ss");
sheet.Cells["A3"].Value = "测试者"; sheet.Cells["B3"].Value = entity.Tester;
sheet.Cells["A4"].Value = "样品类型"; sheet.Cells["B4"].Value = entity.SampleType;
sheet.Cells["A5"].Value = "规格"; sheet.Cells["B5"].Value = entity.SampleSpec;
sheet.Cells["A6"].Value = "室温(°C)"; sheet.Cells["B6"].Value = entity.RoomTemperature;
sheet.Cells["A7"].Value = "浸润时间(h)"; sheet.Cells["B7"].Value = entity.SoakingTime;
sheet.Cells["A8"].Value = "测试液体"; sheet.Cells["B8"].Value = entity.LiquidName;
sheet.Cells["A9"].Value = "液体生产厂家"; sheet.Cells["B9"].Value = entity.LiquidManufacturer;
sheet.Cells["A10"].Value = "泡点压力"; sheet.Cells["B10"].Value = $"{entity.BubblePointPressure} {entity.PressureUnit}";
sheet.Cells["A11"].Value = "平均孔径(μm)"; sheet.Cells["B11"].Value = entity.AveragePoreSize;
// 数据点表格
sheet.Cells["A13"].Value = "压力"; sheet.Cells["B13"].Value = "湿膜流量(L/min)"; sheet.Cells["C13"].Value = "干膜流量(L/min)";
int row = 14;
foreach (var dp in entity.DataPoints)
{
sheet.Cells[$"A{row}"].Value = dp.Pressure;
sheet.Cells[$"B{row}"].Value = dp.WetFlow;
sheet.Cells[$"C{row}"].Value = dp.DryFlow;
row++;
}
package.SaveAs(new FileInfo(filePath));
}
}

View File

@@ -17,39 +17,50 @@
public ushort PressureRegisterStation1 { get; set; }
public ushort PressureRegisterStation2 { get; set; }
public ushort PressureRegisterStation3 { get; set; }
public ushort PressureModeRegister { get; set; }
public ushort PressCoil { get; set; }
public ushort StartCoil { get; set; }
public ushort EnableCoil { get; set; }
public ushort StopCoil { get; set; }
public ushort PressureUpperLimit { get; set; } = 300;
public ushort PressureRate { get; set; } = 280;
public ushort PressureCoeff { get; set; } = 282;
public ushort HPCoeff1 { get; set; } = 3120;
public ushort LPCoeff1 { get; set; } = 3122;
public ushort HPCoeff2 { get; set; } = 3124;
public ushort LPCoeff2 { get; set; } = 3126;
public ushort HPCoeff3 { get; set; } = 3128;
public ushort LPCoeff3 { get; set; } = 3130;
public ushort LargeFlowCoeff1 { get; set; } = 3048;
public ushort SmallFlowCoeff1 { get; set; } = 380;
public ushort LargeFlowCoeff2 { get; set; } = 1218;
public ushort SmallFlowCoeff2 { get; set; } = 1318;
public ushort LargeFlowCoeff3 { get; set; } = 1418;
public ushort SmallFlowCoeff3 { get; set; } = 1468;
public ushort FlowModeRegister1 { get; set; } = 5; // 工位1流量模式 (0=大,1=小)
public ushort FlowModeRegister2 { get; set; } = 6;
public ushort FlowModeRegister3 { get; set; } = 7;
// 校准系数地址需与PLC约定
public ushort PressureCalibZero { get; set; } = 3200;
public ushort PressureCalibSpan { get; set; } = 3202;
public ushort FlowCalibZero { get; set; } = 3204;
public ushort FlowCalibSpan { get; set; } = 3206;
// 设备参数地址示例请根据实际PLC地址修改
public ushort UpperLampData1 { get; set; } = 350;
public ushort UpperLampData2 { get; set; } = 351;
public ushort UpperLampData3 { get; set; } = 352;
public ushort UpperLampData4 { get; set; } = 353;
public ushort UpperLampData5 { get; set; } = 354;
public ushort UpperLampData6 { get; set; } = 355;
public ushort LowerLampData1 { get; set; } = 356;
public ushort LowerLampData2 { get; set; } = 357;
public ushort LowerLampData3 { get; set; } = 358;
public ushort LowerLampData4 { get; set; } = 359;
public ushort LowerLampData5 { get; set; } = 360;
public ushort LowerLampData6 { get; set; } = 361;
public ushort LeftEyeAreaCoeff { get; set; } = 212;
public ushort RightEyeAreaCoeff { get; set; } = 214;
public ushort SaveRateCorrectionCoeff { get; set; } = 428;
public ushort MiddleLamp1 { get; set; } = 362;
public ushort MiddleLamp2 { get; set; } = 363;
public ushort MiddleLamp3 { get; set; } = 364;
public ushort MiddleLamp4 { get; set; } = 365;
public ushort MiddleLamp5 { get; set; } = 366;
public ushort MiddleLamp6 { get; set; } = 367;
public ushort MiddleLamp7 { get; set; } = 368;
public ushort MotorLimit { get; set; } = 330;
public ushort ResetCompensation { get; set; } = 340; // 新增复位补偿
@@ -58,7 +69,11 @@
}
public enum ParameterDataType
{
Int16, // 16位整数
Float32 // 32位浮点数
}
public interface IPlcService
@@ -73,7 +88,12 @@
Task<ushort[]> ReadHoldingRegistersAsync(ushort startAddress, ushort count);
Task WriteSingleRegisterAsync(ushort registerAddress, ushort value);
float UshortToFloat(ushort P1, ushort P2);
Task WriteMultipleRegistersAsync(ushort registerAddress, float value);
}
}

View File

@@ -1,31 +0,0 @@
using MembranePoreTester.Models;
namespace MembranePoreTester.Helpers
{
public static class PoreCalculator
{
public static double PressureToPore(double pressure, string unit, TestLiquid liquid)
{
if (liquid == null) return 0;
return unit switch
{
"Pa" => liquid.C_Pa / pressure,
"cmHg" => liquid.C_cmHg / pressure,
"psi" => liquid.C_psi / pressure,
_ => 0
};
}
public static double PoreToPressure(double pore, string unit, TestLiquid liquid)
{
if (liquid == null) return 0;
return unit switch
{
"Pa" => liquid.C_Pa / pore,
"cmHg" => liquid.C_cmHg / pore,
"psi" => liquid.C_psi / pore,
_ => 0
};
}
}
}

View File

@@ -1,69 +0,0 @@
using MembranePoreTester.Models;
using System.Collections.Generic;
using System.Linq;
namespace MembranePoreTester.Helpers
{
public static class PoreDistributionAnalysis
{
public static double CalculateAveragePore(
IEnumerable<DataPoint> points,
string unit,
TestLiquid liquid)
{
var sorted = points.OrderBy(p => p.Pressure).ToList();
if (sorted.Count < 2) return 0;
double[] pressures = sorted.Select(p => p.Pressure).ToArray();
double[] dryFlows = sorted.Select(p => p.DryFlow).ToArray();
double[] wetFlows = sorted.Select(p => p.WetFlow).ToArray();
double halfDryFlow = dryFlows.Max() / 2.0;
// 找到湿膜流量等于 halfDryFlow 的压力(交点)
for (int i = 0; i < wetFlows.Length - 1; i++)
{
if ((wetFlows[i] <= halfDryFlow && wetFlows[i + 1] >= halfDryFlow) ||
(wetFlows[i] >= halfDryFlow && wetFlows[i + 1] <= halfDryFlow))
{
double p = Interpolation.Linear(
new[] { wetFlows[i], wetFlows[i + 1] },
new[] { pressures[i], pressures[i + 1] },
halfDryFlow);
return PoreCalculator.PressureToPore(p, unit, liquid);
}
}
return 0;
}
public static double CalculatePoreRangePercentage(
IEnumerable<DataPoint> points,
string unit,
TestLiquid liquid,
double lowerPore,
double upperPore)
{
var sorted = points.OrderBy(p => p.Pressure).ToList();
if (sorted.Count < 2) return 0;
double[] pressures = sorted.Select(p => p.Pressure).ToArray();
double[] wetFlows = sorted.Select(p => p.WetFlow).ToArray();
double[] dryFlows = sorted.Select(p => p.DryFlow).ToArray();
// 大孔径对应低压,小孔径对应高压
double pLower = PoreCalculator.PoreToPressure(upperPore, unit, liquid);
double pUpper = PoreCalculator.PoreToPressure(lowerPore, unit, liquid);
// 插值
double qWetLower = Interpolation.Linear(pressures, wetFlows, pLower);
double qDryLower = Interpolation.Linear(pressures, dryFlows, pLower);
double qWetUpper = Interpolation.Linear(pressures, wetFlows, pUpper);
double qDryUpper = Interpolation.Linear(pressures, dryFlows, pUpper);
double ratioLow = qWetLower / qDryLower;
double ratioHigh = qWetUpper / qDryUpper;
return (ratioHigh - ratioLow) * 100;
}
}
}

View File

@@ -1,227 +0,0 @@
using MembranePoreTester.Models;
using System;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Xps.Packaging;
namespace MembranePoreTester.Helpers
{
public static class ReportGenerator
{
public static void GenerateBubblePointReport(BubblePointRecord record)
{
FlowDocument doc = CreateBubblePointDocument(record);
ShowPrintPreview(doc, "泡点法测试报告");
}
public static void GeneratePoreDistributionReport(PoreDistributionRecord record)
{
FlowDocument doc = CreatePoreDistributionDocument(record);
ShowPrintPreview(doc, "孔分布测试报告");
}
private static FlowDocument CreateBubblePointDocument(BubblePointRecord record)
{
FlowDocument doc = new FlowDocument();
doc.PageWidth = 800; // 适应打印
// 标题
doc.Blocks.Add(new Paragraph(new Run("泡点法测试报告"))
{
FontSize = 24,
FontWeight = FontWeights.Bold,
TextAlignment = TextAlignment.Center
});
// 日期/测试者
doc.Blocks.Add(new Paragraph(new Run($"测试日期: {record.TestDate:yyyy-MM-dd} 测试者: {record.Tester}"))
{
TextAlignment = TextAlignment.Right
});
// 样品信息表格
Table table = new Table();
table.Columns.Add(new TableColumn { Width = new GridLength(150) });
table.Columns.Add(new TableColumn { Width = new GridLength(200) });
TableRowGroup group = new TableRowGroup();
AddRow(group, "膜类型", record.SampleType);
AddRow(group, "规格", record.SampleSpec);
AddRow(group, "室温(°C)", record.RoomTemperature.ToString("F1"));
AddRow(group, "浸润时间(h)", record.SoakingTime.ToString("F1"));
AddRow(group, "测试液体", record.Liquid?.Name);
AddRow(group, "液体生产厂家", record.LiquidManufacturer);
AddRow(group, "泡点压力", $"{record.BubblePointPressure} {record.PressureUnit}");
AddRow(group, "最大孔径(μm)", record.MaxPoreSize.ToString("F3"));
table.RowGroups.Add(group);
doc.Blocks.Add(table);
doc.Blocks.Add(new Paragraph(new Run("本报告依据 GB/T 32361-2015 生成。"))
{
FontStyle = FontStyles.Italic,
TextAlignment = TextAlignment.Center,
Margin = new Thickness(0, 20, 0, 0)
});
return doc;
}
private static FlowDocument CreatePoreDistributionDocument(PoreDistributionRecord record)
{
FlowDocument doc = new FlowDocument();
doc.PageWidth = 800;
// 标题
doc.Blocks.Add(new Paragraph(new Run("孔分布测试报告"))
{
FontSize = 24,
FontWeight = FontWeights.Bold,
TextAlignment = TextAlignment.Center
});
doc.Blocks.Add(new Paragraph(new Run($"测试日期: {record.TestDate:yyyy-MM-dd} 测试者: {record.Tester}"))
{
TextAlignment = TextAlignment.Right
});
// 样品信息表格
Table infoTable = new Table();
infoTable.Columns.Add(new TableColumn { Width = new GridLength(150) });
infoTable.Columns.Add(new TableColumn { Width = new GridLength(200) });
TableRowGroup group = new TableRowGroup();
AddRow(group, "膜类型", record.SampleType);
AddRow(group, "规格", record.SampleSpec);
AddRow(group, "室温(°C)", record.RoomTemperature.ToString("F1"));
AddRow(group, "浸润时间(h)", record.SoakingTime.ToString("F1"));
AddRow(group, "测试液体", record.Liquid?.Name);
AddRow(group, "液体生产厂家", record.LiquidManufacturer);
infoTable.RowGroups.Add(group);
doc.Blocks.Add(infoTable);
// 计算结果
doc.Blocks.Add(new Paragraph(new Run("测试结果"))
{
FontSize = 18,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 10, 0, 5)
});
Table resultTable = new Table();
resultTable.Columns.Add(new TableColumn { Width = new GridLength(150) });
resultTable.Columns.Add(new TableColumn { Width = new GridLength(200) });
TableRowGroup resultGroup = new TableRowGroup();
AddRow(resultGroup, "泡点压力", $"{record.BubblePointPressure} {record.PressureUnit}");
AddRow(resultGroup, "平均孔径(μm)", record.AveragePoreSize.ToString("F3"));
resultTable.RowGroups.Add(resultGroup);
doc.Blocks.Add(resultTable);
// 数据点列表
doc.Blocks.Add(new Paragraph(new Run("压力-流量数据"))
{
FontSize = 14,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 10, 0, 5)
});
Table dataTable = new Table();
dataTable.Columns.Add(new TableColumn { Width = new GridLength(80) });
dataTable.Columns.Add(new TableColumn { Width = new GridLength(80) });
dataTable.Columns.Add(new TableColumn { Width = new GridLength(80) });
dataTable.CellSpacing = 2;
TableRowGroup dataGroup = new TableRowGroup();
// 表头
TableRow headerRow = new TableRow();
headerRow.Cells.Add(new TableCell(new Paragraph(new Run("压力"))));
headerRow.Cells.Add(new TableCell(new Paragraph(new Run("湿膜流量(L/min)"))));
headerRow.Cells.Add(new TableCell(new Paragraph(new Run("干膜流量(L/min)"))));
headerRow.FontWeight = FontWeights.Bold;
dataGroup.Rows.Add(headerRow);
foreach (var dp in record.DataPoints)
{
TableRow row = new TableRow();
row.Cells.Add(new TableCell(new Paragraph(new Run(dp.Pressure.ToString("F2")))));
row.Cells.Add(new TableCell(new Paragraph(new Run(dp.WetFlow.ToString("F3")))));
row.Cells.Add(new TableCell(new Paragraph(new Run(dp.DryFlow.ToString("F3")))));
dataGroup.Rows.Add(row);
}
dataTable.RowGroups.Add(dataGroup);
doc.Blocks.Add(dataTable);
// 孔分布结果
if (record.PoreDistributions != null && record.PoreDistributions.Any())
{
doc.Blocks.Add(new Paragraph(new Run("孔分布"))
{
FontSize = 14,
FontWeight = FontWeights.Bold,
Margin = new Thickness(0, 10, 0, 5)
});
Table distTable = new Table();
distTable.Columns.Add(new TableColumn { Width = new GridLength(80) });
distTable.Columns.Add(new TableColumn { Width = new GridLength(80) });
distTable.Columns.Add(new TableColumn { Width = new GridLength(80) });
TableRowGroup distGroup = new TableRowGroup();
TableRow distHeader = new TableRow();
distHeader.Cells.Add(new TableCell(new Paragraph(new Run("孔径下限(μm)"))));
distHeader.Cells.Add(new TableCell(new Paragraph(new Run("孔径上限(μm)"))));
distHeader.Cells.Add(new TableCell(new Paragraph(new Run("百分比(%)"))));
distHeader.FontWeight = FontWeights.Bold;
distGroup.Rows.Add(distHeader);
foreach (var pd in record.PoreDistributions)
{
TableRow row = new TableRow();
row.Cells.Add(new TableCell(new Paragraph(new Run(pd.LowerPore.ToString("F3")))));
row.Cells.Add(new TableCell(new Paragraph(new Run(pd.UpperPore.ToString("F3")))));
row.Cells.Add(new TableCell(new Paragraph(new Run(pd.Percentage.ToString("F1")))));
distGroup.Rows.Add(row);
}
distTable.RowGroups.Add(distGroup);
doc.Blocks.Add(distTable);
}
doc.Blocks.Add(new Paragraph(new Run("本报告依据 GB/T 32361-2015 生成。"))
{
FontStyle = FontStyles.Italic,
TextAlignment = TextAlignment.Center,
Margin = new Thickness(0, 20, 0, 0)
});
return doc;
}
private static void AddRow(TableRowGroup group, string label, string value)
{
TableRow row = new TableRow();
row.Cells.Add(new TableCell(new Paragraph(new Run(label))));
row.Cells.Add(new TableCell(new Paragraph(new Run(value ?? ""))));
group.Rows.Add(row);
}
private static void ShowPrintPreview(FlowDocument document, string title)
{
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
// 调整文档页面大小以匹配打印机
document.PageHeight = printDialog.PrintableAreaHeight;
document.PageWidth = printDialog.PrintableAreaWidth;
document.PagePadding = new Thickness(50);
IDocumentPaginatorSource dps = document;
printDialog.PrintDocument(dps.DocumentPaginator, title);
}
}
}
}

View File

@@ -1,30 +0,0 @@
namespace MembranePoreTester.Models
{
public class BubblePointRecord
{
public string SampleType { get; set; } // 平板膜/中空纤维膜
public string SampleSpec { get; set; } // 规格
public double RoomTemperature { get; set; } // 室温(°C)
public double SoakingTime { get; set; } // 浸润时间(小时)
public TestLiquid Liquid { get; set; } // 测试液体
public string LiquidManufacturer { get; set; } // 生产厂家
public double BubblePointPressure { get; set; } // 泡点压力(数值)
public string PressureUnit { get; set; } // Pa/cmHg/psi
public DateTime TestDate { get; set; } = DateTime.Now;
public string Tester { get; set; }
public double MaxPoreSize => CalculateMaxPore();
private double CalculateMaxPore()
{
if (Liquid == null) return 0;
return PressureUnit switch
{
"Pa" => Liquid.C_Pa / BubblePointPressure,
"cmHg" => Liquid.C_cmHg / BubblePointPressure,
"psi" => Liquid.C_psi / BubblePointPressure,
_ => 0
};
}
}
}

View File

@@ -1,10 +0,0 @@
namespace MembranePoreTester.Models
{
public class DataPoint
{
public double Pressure { get; set; } // 压力
public double WetFlow { get; set; } // 湿膜流量(L/min)
public double DryFlow { get; set; } // 干膜流量(L/min)
}
}

38
Models/DeviceDataModel.cs Normal file
View File

@@ -0,0 +1,38 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace MembranePoreTester.Models
{
public partial class DeviceDataModel : ObservableObject
{
// 灯条数据
[ObservableProperty] private double _upperLampData1;
[ObservableProperty] private double _lowerLampData1;
[ObservableProperty] private double _upperLampData2;
[ObservableProperty] private double _lowerLampData2;
[ObservableProperty] private double _upperLampData3;
[ObservableProperty] private double _lowerLampData3;
[ObservableProperty] private double _upperLampData4;
[ObservableProperty] private double _lowerLampData4;
[ObservableProperty] private double _upperLampData5;
[ObservableProperty] private double _lowerLampData5;
[ObservableProperty] private double _upperLampData6;
[ObservableProperty] private double _lowerLampData6;
// 面积系数
[ObservableProperty] private double _leftEyeAreaCoeff;
[ObservableProperty] private double _rightEyeAreaCoeff;
[ObservableProperty] private double _saveRateCorrectionCoeff;
// 中灯
[ObservableProperty] private double _middleLamp1;
[ObservableProperty] private double _middleLamp2;
[ObservableProperty] private double _middleLamp3;
[ObservableProperty] private double _middleLamp4;
[ObservableProperty] private double _middleLamp5;
[ObservableProperty] private double _middleLamp6;
[ObservableProperty] private double _middleLamp7;
// 电机限位
[ObservableProperty] private double _motorLimit;
}
}

View File

@@ -1,81 +0,0 @@
using System.Collections.ObjectModel;
namespace MembranePoreTester.Models
{
/// <summary>
/// 孔分布测试的记录模型,存储一次孔分布测试的所有输入数据和计算结果。
/// 对应 GB/T 32361-2015 标准中的平均流量法测试。
/// </summary>
public class PoreDistributionRecord
{
/// <summary>
/// 膜类型:平板膜 或 中空纤维膜。
/// </summary>
public string SampleType { get; set; }
/// <summary>
/// 样品规格,如直径、厚度或型号等。
/// </summary>
public string SampleSpec { get; set; }
/// <summary>
/// 测试时的室温单位摄氏度°C
/// </summary>
public double RoomTemperature { get; set; }
/// <summary>
/// 样品在测试液体中的浸润时间单位小时h
/// </summary>
public double SoakingTime { get; set; }
/// <summary>
/// 测试使用的液体对象,包含液体名称、表面张力等信息。
/// </summary>
public TestLiquid Liquid { get; set; }
/// <summary>
/// 测试液体的生产厂家,用于溯源。
/// </summary>
public string LiquidManufacturer { get; set; }
/// <summary>
/// 压力单位Pa、cmHg 或 psi。所有压力相关输入均以此单位为准。
/// </summary>
public string PressureUnit { get; set; }
/// <summary>
/// 压力-流量数据点集合,每个点包含压力、湿膜流量、干膜流量。
/// 使用 ObservableCollection 以便在界面添加/删除时自动更新。
/// </summary>
public ObservableCollection<DataPoint> DataPoints { get; set; } = new();
/// <summary>
/// 测试日期,默认为当前日期时间。
/// </summary>
public DateTime TestDate { get; set; } = DateTime.Now;
/// <summary>
/// 测试人员姓名。
/// </summary>
public string Tester { get; set; }
// ---------- 计算结果(由计算逻辑填充) ----------
/// <summary>
/// 泡点压力(由用户单独记录或从数据点推断),用于最大孔径计算。
/// </summary>
public double BubblePointPressure { get; set; }
/// <summary>
/// 计算出的平均孔径单位微米μm
/// 对应标准中平均流量法的平均孔径结果。
/// </summary>
public double AveragePoreSize { get; set; }
/// <summary>
/// 孔分布计算结果列表,每个元素表示一个孔径区间的流量百分比。
/// 对应标准中孔分布的多个区间结果。
/// </summary>
public List<PoreDistributionResult> PoreDistributions { get; set; } = new();
}
}

View File

@@ -1,9 +0,0 @@
namespace MembranePoreTester.Models
{
public class PoreDistributionResult
{
public double LowerPore { get; set; } // 孔径下限(μm)
public double UpperPore { get; set; } // 孔径上限(μm)
public double Percentage { get; set; } // 流量百分比
}
}

View File

@@ -1,22 +0,0 @@
namespace MembranePoreTester.Models
{
public class TestLiquid
{
public string Name { get; set; }
public double SurfaceTension { get; set; } // mN/m, 25°C
// Cγ
public double C_Pa => 2860 * SurfaceTension;
public double C_cmHg => 2.15 * SurfaceTension;
public double C_psi => 0.415 * SurfaceTension;
public static List<TestLiquid> Predefined => new()
{
new TestLiquid { Name = "水", SurfaceTension = 72.0 },
new TestLiquid { Name = "石油馏分", SurfaceTension = 30.0 },
new TestLiquid { Name = "乙醇", SurfaceTension = 22.3 },
new TestLiquid { Name = "液态石蜡", SurfaceTension = 34.7 },
new TestLiquid { Name = "BSD16", SurfaceTension = 16.0 } // 新增
};
}
}

View File

@@ -1,59 +0,0 @@
using MembranePoreTester.Models;
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
namespace MembranePoreTester.Report
{
public class Report
{
public static void GenerateBubblePointReport(BubblePointRecord record)
{
FlowDocument doc = new FlowDocument();
doc.PageWidth = 800; // 适应打印
Section header = new Section();
header.Blocks.Add(new Paragraph(new Run("泡点法测试报告"))
{ FontSize = 24, FontWeight = FontWeights.Bold, TextAlignment = TextAlignment.Center });
header.Blocks.Add(new Paragraph(new Run($"测试日期: {record.TestDate:yyyy-MM-dd} 测试者: {record.Tester}"))
{ TextAlignment = TextAlignment.Right });
// 样品信息表格
Table table = new Table();
table.Columns.Add(new TableColumn { Width = new GridLength(150) });
table.Columns.Add(new TableColumn { Width = new GridLength(200) });
table.RowGroups.Add(new TableRowGroup());
AddRow(table, "膜类型", record.SampleType);
AddRow(table, "规格", record.SampleSpec);
AddRow(table, "室温(°C)", record.RoomTemperature.ToString("F1"));
AddRow(table, "浸润时间(h)", record.SoakingTime.ToString("F1"));
AddRow(table, "测试液体", record.Liquid?.Name);
AddRow(table, "液体生产厂家", record.LiquidManufacturer);
AddRow(table, "泡点压力", $"{record.BubblePointPressure} {record.PressureUnit}");
AddRow(table, "最大孔径(μm)", record.MaxPoreSize.ToString("F3"));
doc.Blocks.Add(table);
doc.Blocks.Add(new Paragraph(new Run("本报告依据GB/T 32361-2015生成。")) { FontStyle = FontStyles.Italic });
// 打印预览
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == true)
{
IDocumentPaginatorSource dps = doc;
printDialog.PrintDocument(dps.DocumentPaginator, "泡点法测试报告");
}
}
private static void AddRow(Table table, string label, string value)
{
TableRow row = new TableRow();
row.Cells.Add(new TableCell(new Paragraph(new Run(label))));
row.Cells.Add(new TableCell(new Paragraph(new Run(value ?? ""))));
table.RowGroups[0].Rows.Add(row);
}
}
}

View File

@@ -0,0 +1,50 @@
using System;
using MembranePoreTester.Models;
namespace MembranePoreTester.Services
{
public class HardwareService : IHardwareService
{
public DeviceDataModel LoadDeviceData()
{
// TODO: 实现硬件读取逻辑
throw new NotImplementedException("请在此处实现硬件读取逻辑");
}
public void SaveDeviceData(DeviceDataModel data)
{
// TODO: 实现硬件保存逻辑
throw new NotImplementedException("请在此处实现硬件保存逻辑");
}
public void Reset()
{
// TODO: 实现复位指令
throw new NotImplementedException("请在此处实现复位指令");
}
public void OpenLeftEye()
{
// TODO: 实现左眼开指令
throw new NotImplementedException("请在此处实现左眼开指令");
}
public void OpenRightEye()
{
// TODO: 实现右眼开指令
throw new NotImplementedException("请在此处实现右眼开指令");
}
public void ReverseMotor()
{
// TODO: 实现电机反转
throw new NotImplementedException("请在此处实现电机反转");
}
public void ForwardMotor()
{
// TODO: 实现电机正转
throw new NotImplementedException("请在此处实现电机正转");
}
}
}

View File

@@ -0,0 +1,22 @@
using MembranePoreTester.Models;
namespace MembranePoreTester.Services
{
public interface IHardwareService
{
// 读取设备全部参数
DeviceDataModel LoadDeviceData();
// 保存设备全部参数
void SaveDeviceData(DeviceDataModel data);
// 控制命令
void Reset();
void OpenLeftEye();
void OpenRightEye();
void ReverseMotor();
void ForwardMotor();
// 可扩展其他方法...
}
}

View File

@@ -104,6 +104,51 @@ namespace MembranePoreTester.Communication
return await ReadFloatAsync(startAddress);
}
/// <summary>
/// ushort转为float类型
/// </summary>
/// <param name="P1"></param>
/// <param name="P2"></param>
/// <returns>float型数据</returns>
public float UshortToFloat(ushort P1, ushort P2)
{
int intSign, intSignRest, intExponent, intExponentRest;
float faResult, faDigit;
intSign = P1 / 32768;
intSignRest = P1 % 32768;
intExponent = intSignRest / 128;
intExponentRest = intSignRest % 128;
faDigit = (float)(intExponentRest * 65536 + P2) / 8388608;
faResult = (float)Math.Pow(-1, intSign) * (float)Math.Pow(2, intExponent - 127) * (faDigit + 1);
return faResult;
}
public async Task WriteMultipleRegistersAsync(ushort registerAddress, float value)
{
await EnsureConnectedAsync();
await Task.Delay(100);
await _master.WriteMultipleRegistersAsync(_config.SlaveId, registerAddress, SplitFloatToUShortArray((float)value));
}
/// <summary>
/// Float转为Ushort数组发送
/// </summary>
/// <param name="value"></param>
/// <returns>返回ushort数组</returns>
public ushort[] SplitFloatToUShortArray(float value)
{
byte[] floatBytes = BitConverter.GetBytes(value);
ushort[] ushortArray = new ushort[floatBytes.Length / 2];
for (int i = 0, j = 0; i < floatBytes.Length; i += 2, j++)
{
ushortArray[j] = BitConverter.ToUInt16(floatBytes, i);
}
return ushortArray;
}
public void Dispose()
{

112
Styles.xaml Normal file
View File

@@ -0,0 +1,112 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- 主窗口背景 -->
<SolidColorBrush x:Key="WindowBackground" Color="#1E1E2F" />
<!-- 卡片背景 -->
<SolidColorBrush x:Key="CardBackground" Color="#2D2D3A" />
<!-- 按钮样式 -->
<Style TargetType="Button">
<Setter Property="Background" Value="#3A6EA5" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="12,6" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#2C5A8C" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#1F3F60" />
</Trigger>
</Style.Triggers>
</Style>
<!-- 文本框样式 -->
<Style TargetType="TextBox">
<Setter Property="Background" Value="#3A3A4A" />
<Setter Property="Foreground" Value="White" />
<Setter Property="BorderBrush" Value="#555" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="6,4" />
<Setter Property="FontSize" Value="13" />
<Setter Property="CaretBrush" Value="White" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="BorderBrush" Value="#3A6EA5" />
</Trigger>
</Style.Triggers>
</Style>
<!-- 标签样式 -->
<Style TargetType="Label">
<Setter Property="Foreground" Value="#DDD" />
<Setter Property="FontSize" Value="13" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<!-- GroupBox 样式 -->
<Style TargetType="GroupBox">
<Setter Property="BorderBrush" Value="#3A6EA5" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Background" Value="{StaticResource CardBackground}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Margin" Value="8" />
<Setter Property="Padding" Value="8" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupBox">
<Border BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
CornerRadius="8">
<Grid Margin="{TemplateBinding Padding}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Grid.Row="0"
Background="{TemplateBinding BorderBrush}"
CornerRadius="4,4,0,0"
Margin="-8,-8,-8,0"
Padding="8,4">
<ContentPresenter ContentSource="Header"
RecognizesAccessKey="True"
TextElement.Foreground="White"
TextElement.FontWeight="Bold"/>
</Border>
<ContentPresenter Grid.Row="1" Margin="0,8,0,0" />
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -1,232 +0,0 @@
using MembranePoreTester.Communication;
using MembranePoreTester.Helpers;
using MembranePoreTester.Models;
using Microsoft.Win32;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Input;
namespace MembranePoreTester.ViewModels
{
public class BubblePointViewModel : ViewModelBase
{
private BubblePointRecord _record = new();
private TestLiquid _selectedLiquid;
private bool _isCustomLiquid;
private double _customSurfaceTension = 30.0;
// 添加字段
private readonly IPlcService _plcService;
private readonly PlcConfiguration _plcConfig;
public ICommand ReadPlcCommand { get; }
public BubblePointRecord Record
{
get => _record;
set => SetProperty(ref _record, value);
}
public List<TestLiquid> Liquids => TestLiquid.Predefined;
public List<string> PressureUnits => new() { "Pa", "cmHg", "psi" };
public List<string> MembraneTypes => new() { "平板膜", "中空纤维膜" };
public TestLiquid SelectedLiquid
{
get => _selectedLiquid;
set
{
if (SetProperty(ref _selectedLiquid, value))
{
Record.Liquid = value;
OnPropertyChanged(nameof(Record.MaxPoreSize));
}
}
}
public bool IsCustomLiquid
{
get => _isCustomLiquid;
set
{
if (SetProperty(ref _isCustomLiquid, value))
{
UpdateCustomLiquid();
}
}
}
public double CustomSurfaceTension
{
get => _customSurfaceTension;
set
{
if (SetProperty(ref _customSurfaceTension, value))
{
UpdateCustomLiquid();
}
}
}
private void UpdateCustomLiquid()
{
if (IsCustomLiquid)
{
Record.Liquid = new TestLiquid
{
Name = "自定义",
SurfaceTension = CustomSurfaceTension
};
OnPropertyChanged(nameof(Record.MaxPoreSize));
}
}
public ICommand CalculateCommand { get; }
public ICommand GenerateReportCommand { get; }
public ICommand OpenPressureCalibCommand { get; }
public BubblePointViewModel()
{
_plcService = App.PlcService;
_plcConfig = App.PlcConfig;
CalculateCommand = new RelayCommand(Calculate);
GenerateReportCommand = new RelayCommand(GenerateReport);
SelectedLiquid = Liquids[0];
ReadPlcCommand = new RelayCommand(async () => await ReadPlcAsync());
SaveCommand = new RelayCommand(SaveToDatabase);
ExportCommand = new RelayCommand(ExportToExcel);
OpenPressureCalibCommand = new RelayCommand(OpenPressureCalibration);
}
private async Task ReadPlcAsync()
{
try
{
float rawPressure = await _plcService.ReadPressureAsync(StationId);
Record.BubblePointPressure = rawPressure * _plcConfig.PressureFactor;
OnPropertyChanged(nameof(Record.BubblePointPressure));
}
catch (Exception ex)
{
MessageBox.Show($"读取PLC失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
public double MaxPoreSize => Record.MaxPoreSize;
private void Calculate()
{
// 触发最大孔径重新计算(通过绑定已自动刷新)
OnPropertyChanged(nameof(Record.MaxPoreSize));
}
private void GenerateReport()
{
ReportGenerator.GenerateBubblePointReport(Record);
}
public void UpdateBubblePointPressure(double pressure)
{
Record.BubblePointPressure = pressure;
OnPropertyChanged(nameof(Record.BubblePointPressure));
OnPropertyChanged(nameof(MaxPoreSize)); // 最大孔径依赖于压力
}
private int _stationId;
public int StationId
{
get => _stationId;
set => SetProperty(ref _stationId, value);
}
// 保存当前记录到数据库
public void SaveToDatabase()
{
var entity = new BubblePointEntity
{
StationId = StationId,
TestDate = Record.TestDate,
Tester = Record.Tester,
SampleType = Record.SampleType,
SampleSpec = Record.SampleSpec,
RoomTemperature = Record.RoomTemperature,
SoakingTime = Record.SoakingTime,
LiquidName = Record.Liquid?.Name,
LiquidSurfaceTension = Record.Liquid?.SurfaceTension ?? 0,
LiquidManufacturer = Record.LiquidManufacturer,
BubblePointPressure = Record.BubblePointPressure,
PressureUnit = Record.PressureUnit,
MaxPoreSize = Record.MaxPoreSize
};
using var db = new AppDbContext();
db.BubblePointRecords.Add(entity);
db.SaveChanges();
}
public void LoadFromDatabase(int recordId)
{
using var db = new AppDbContext();
var entity = db.BubblePointRecords.Find(recordId);
if (entity == null) return;
Record.SampleType = entity.SampleType;
Record.SampleSpec = entity.SampleSpec;
Record.RoomTemperature = entity.RoomTemperature;
Record.SoakingTime = entity.SoakingTime;
Record.Liquid = TestLiquid.Predefined.FirstOrDefault(l => l.Name == entity.LiquidName)
?? new TestLiquid { Name = entity.LiquidName, SurfaceTension = entity.LiquidSurfaceTension };
Record.LiquidManufacturer = entity.LiquidManufacturer;
Record.BubblePointPressure = entity.BubblePointPressure;
Record.PressureUnit = entity.PressureUnit;
Record.TestDate = entity.TestDate;
Record.Tester = entity.Tester;
OnPropertyChanged(nameof(Record));
OnPropertyChanged(nameof(MaxPoreSize)); // 触发界面更新
}
public ICommand SaveCommand { get; }
public ICommand ExportCommand { get; }
private void OpenPressureCalibration()
{
// 使用简单的输入框获取新零点系数和量程系数
string zeroStr = Microsoft.VisualBasic.Interaction.InputBox("请输入压力零点系数", "压力校准", "0");
string spanStr = Microsoft.VisualBasic.Interaction.InputBox("请输入压力量程系数", "压力校准", "1");
if (float.TryParse(zeroStr, out float zero) && float.TryParse(spanStr, out float span))
{
Task.Run(async () =>
{
await WriteFloatAsync(_plcConfig.PressureCalibZero, zero);
await WriteFloatAsync(_plcConfig.PressureCalibSpan, span);
MessageBox.Show("压力校准系数已写入", "完成");
});
}
}
private void ExportToExcel()
{
var saveFileDialog = new SaveFileDialog
{
Filter = "Excel文件|*.xlsx",
FileName = $"泡点法_工位{StationId}_{DateTime.Now:yyyyMMddHHmmss}.xlsx"
};
if (saveFileDialog.ShowDialog() == true)
{
// 转换为Entity后导出
var entity = new BubblePointEntity { /* 从Record复制 */ };
ExportHelper.ExportBubblePoint(entity, saveFileDialog.FileName);
MessageBox.Show("导出成功");
}
}
}
}

View File

@@ -1,160 +0,0 @@
using MembranePoreTester.Communication;
using System.Collections.ObjectModel;
using System.ComponentModel; // 用于 PropertyChanged
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace MembranePoreTester.ViewModels
{
public class MainViewModel : ViewModelBase
{
public ObservableCollection<StationItem> Stations { get; } = new();
public class StationItem : ViewModelBase
{
private readonly IPlcService _plcService;
private readonly PlcConfiguration _plcConfig;
private bool _isPressing;
private string _pressButtonText = "加压";
private string _highLowPressure = "低压";
private bool _enableStatus; // M21 状态
public string Name { get; set; }
public BubblePointViewModel BubblePointVM { get; set; }
public PoreDistributionViewModel PoreDistributionVM { get; set; }
public int StationId { get; set; }
public string HighLowPressure
{
get => _highLowPressure;
set
{
if (SetProperty(ref _highLowPressure, value))
{
// 当选择变化时,写入 PLC 压力模式寄存器
Task.Run(async () => await WritePressureModeAsync(value));
}
}
}
public string PressButtonText
{
get => _pressButtonText;
set => SetProperty(ref _pressButtonText, value);
}
// 使能状态(只读)
public bool EnableStatus
{
get => _enableStatus;
private set => SetProperty(ref _enableStatus, value);
}
// 使能状态显示文本
public string EnableStatusText => EnableStatus ? "运行中" : "未启动";
// 使能状态显示颜色(绿色表示运行中,灰色表示未启动)
public string EnableStatusColor => EnableStatus ? "Green" : "Gray";
// 定时器,用于轮询 M21 状态
private System.Windows.Threading.DispatcherTimer _timer;
public ICommand PressCommand { get; }
//public ICommand BurstCommand { get; }
public ICommand StartCommand { get; }
public ICommand StopCommand { get; }
public ICommand EnableCommand { get; } // 备用,但可以使用复选框直接绑定
public StationItem()
{
_plcService = App.PlcService;
_plcConfig = App.PlcConfig;
PressCommand = new RelayCommand(async () => await TogglePressAsync());
//BurstCommand = new RelayCommand(async () => await ReadBurstPressureAsync());
StartCommand = new RelayCommand(async () => await WriteCoilAsync(_plcConfig.StartCoil, true));
StopCommand = new RelayCommand(async () => await WriteCoilAsync(_plcConfig.StopCoil, true));
// 启动定时器,每秒读取一次 M21 状态
_timer = new System.Windows.Threading.DispatcherTimer();
_timer.Interval = TimeSpan.FromSeconds(1);
_timer.Tick += async (s, e) => await ReadEnableStatusAsync();
_timer.Start();
}
private async Task TogglePressAsync()
{
_isPressing = !_isPressing;
await WriteCoilAsync(_plcConfig.PressCoil, _isPressing);
PressButtonText = _isPressing ? "停止加压" : "加压";
}
private async Task ReadEnableStatusAsync()
{
try
{
bool status = await _plcService.ReadCoilAsync(_plcConfig.EnableCoil); // 读取 M21
EnableStatus = status;
}
catch (Exception ex)
{
// 读取出错时保持原状态或显示错误
System.Diagnostics.Debug.WriteLine($"读取使能状态失败: {ex.Message}");
}
}
private async Task WriteCoilAsync(ushort coil, bool value)
{
try
{
await _plcService.WriteCoilAsync(coil, value);
}
catch (Exception ex)
{
MessageBox.Show($"写线圈失败: {ex.Message}");
}
}
private async Task WritePressureModeAsync(string mode)
{
ushort val = mode == "高压" ? (ushort)1 : (ushort)0;
try
{
await _plcService.WriteRegisterAsync(_plcConfig.PressureModeRegister, val);
}
catch (Exception ex)
{
MessageBox.Show($"写压力模式失败: {ex.Message}");
}
}
}
public MainViewModel()
{
for (int i = 1; i <= 3; i++)
{
var station = new StationItem
{
Name = $"工位 {i}",
BubblePointVM = new BubblePointViewModel { StationId = i },
PoreDistributionVM = new PoreDistributionViewModel { StationId = i },
StationId = i
};
Stations.Add(station);
}
}
}
}

View File

@@ -0,0 +1,308 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MembranePoreTester.Communication;
using Modbus.Device;
using System;
using System.Net;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace MembranePoreTester.ViewModels
{
public partial class ParameterViewModel : ObservableObject
{
private readonly IPlcService _plcService;
private readonly PlcConfiguration _config;
private DispatcherTimer _autoRefreshTimer;
private bool _isUpdatingFromPlc = false; // 防止定时器更新触发写入
// 构造函数(依赖注入)
public ParameterViewModel()
{
_plcService = App.PlcService;
_config = App.PlcConfig;
// 启动定时器(每秒自动刷新)
_autoRefreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_autoRefreshTimer.Tick += AutoRefreshTimer_Tick;
_autoRefreshTimer.Start();
}
// ========== 设备数据属性(全部使用源生成器) ==========
[ObservableProperty]
private double _upperLampData1;
[ObservableProperty]
private double _upperLampData2;
[ObservableProperty]
private double _upperLampData3;
[ObservableProperty]
private double _upperLampData4;
[ObservableProperty]
private double _upperLampData5;
[ObservableProperty]
private double _upperLampData6;
[ObservableProperty]
private double _lowerLampData1;
[ObservableProperty]
private double _lowerLampData2;
[ObservableProperty]
private double _lowerLampData3;
[ObservableProperty]
private double _lowerLampData4;
[ObservableProperty]
private double _lowerLampData5;
[ObservableProperty]
private double _lowerLampData6;
[ObservableProperty]
private double _middleLamp1;
[ObservableProperty]
private double _middleLamp2;
[ObservableProperty]
private double _middleLamp3;
[ObservableProperty]
private double _middleLamp4;
[ObservableProperty]
private double _middleLamp5;
[ObservableProperty]
private double _middleLamp6;
[ObservableProperty]
private double _middleLamp7;
[ObservableProperty]
private double _leftEyeAreaCoeff;
[ObservableProperty]
private double _rightEyeAreaCoeff;
[ObservableProperty]
private double _saveRateCorrectionCoeff;
[ObservableProperty]
private double _motorLimit;
[ObservableProperty]
private double _resetCompensation;
// 通信状态(用于界面显示)
[ObservableProperty]
private string _communicationStatus = "未连接";
[ObservableProperty]
private System.Windows.Media.SolidColorBrush _statusColor = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
// ========== 属性变化时的自动写入用户修改时立即写入PLC ==========
// 这里我们重写属性 setter在值变化时写入PLC排除定时器更新
//partial void OnUpperLampData1Changing(double value) => WriteParameterAsync(_config.UpperLampData1, value, false);
//partial void OnUpperLampData2Changing(double value) => WriteParameterAsync(_config.UpperLampData2, value, false);
// ... 其他灯条属性类似(为了简洁,仅展示几个,您需全部添加)
// 您可以使用代码生成工具批量添加,或手动添加所有属性对应的 Changing 方法。
// 注意:只有需要自动写入的才添加,只读属性不添加。
// 为节省篇幅,下面仅示例部分,实际使用时请按此模式为所有需要读写的属性添加
partial void OnLowerLampData1Changing(double value) => WriteParameterAsync(_config.LowerLampData1, value, false);
partial void OnMiddleLamp1Changing(double value) => WriteParameterAsync(_config.MiddleLamp1, value, false);
partial void OnLeftEyeAreaCoeffChanging(double value) => WriteParameterAsync(_config.LeftEyeAreaCoeff, value, true);
partial void OnRightEyeAreaCoeffChanging(double value) => WriteParameterAsync(_config.RightEyeAreaCoeff, value, true);
partial void OnSaveRateCorrectionCoeffChanging(double value) => WriteParameterAsync(_config.SaveRateCorrectionCoeff, value, true);
partial void OnMotorLimitChanging(double value) => WriteParameterAsync(_config.MotorLimit, value, true);
partial void OnResetCompensationChanging(double value) => WriteParameterAsync(_config.ResetCompensation, value, true);
// 写入PLC的通用方法
private async Task WriteParameterAsync(ushort address, double value, bool isFloat)
{
if (_isUpdatingFromPlc) return; // 定时器读取时跳过写入
try
{
if (isFloat)
await WriteFloatAsync(address, (float)value);
else
await WriteInt16Async(address, (ushort)value);
System.Diagnostics.Debug.WriteLine($"写入成功: 地址={address}, 值={value}");
}
catch (Exception ex)
{
System.Windows.MessageBox.Show($"写入参数失败: {ex.Message}", "错误");
}
}
//// ========== 手动保存/读取命令 ==========
//[RelayCommand]
//private async Task SaveAllParameters()
//{
// // 将所有当前值写入PLC
// // 这里可以遍历所有属性,但为了简单,直接调用各个写入方法
// await WriteParameterAsync(_config.UpperLampData1, UpperLampData1, false);
// // ... 所有其他属性
// CommunicationStatus = "参数已保存";
// StatusColor = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Green);
//}
//[RelayCommand]
//private async Task LoadAllParameters()
//{
// // 从PLC读取所有参数并刷新界面
// await RefreshAllFromPlc();
// CommunicationStatus = "参数已读取";
// StatusColor = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Green);
//}
// 定时刷新从PLC读取但不触发写入
private async void AutoRefreshTimer_Tick(object sender, EventArgs e)
{
await RefreshAllFromPlc();
}
private async Task RefreshAllFromPlc()
{
_isUpdatingFromPlc = true;
try
{
// 读取并更新每个属性16位整数和浮点数
UpperLampData1 = await ReadInt16Async(_config.UpperLampData1);
UpperLampData2 = await ReadInt16Async(_config.UpperLampData2);
UpperLampData3 = await ReadInt16Async(_config.UpperLampData3);
UpperLampData4 = await ReadInt16Async(_config.UpperLampData4);
UpperLampData5 = await ReadInt16Async(_config.UpperLampData5);
UpperLampData6 = await ReadInt16Async(_config.UpperLampData6);
LowerLampData1 = await ReadInt16Async(_config.LowerLampData1);
LowerLampData2 = await ReadInt16Async(_config.LowerLampData2);
LowerLampData3 = await ReadInt16Async(_config.LowerLampData3);
LowerLampData4 = await ReadInt16Async(_config.LowerLampData4);
LowerLampData5 = await ReadInt16Async(_config.LowerLampData5);
LowerLampData6 = await ReadInt16Async(_config.LowerLampData6);
MiddleLamp1 = await ReadInt16Async(_config.MiddleLamp1);
MiddleLamp2 = await ReadInt16Async(_config.MiddleLamp2);
MiddleLamp3 = await ReadInt16Async(_config.MiddleLamp3);
MiddleLamp4 = await ReadInt16Async(_config.MiddleLamp4);
MiddleLamp5 = await ReadInt16Async(_config.MiddleLamp5);
MiddleLamp6 = await ReadInt16Async(_config.MiddleLamp6);
MiddleLamp7 = await ReadInt16Async(_config.MiddleLamp7);
LeftEyeAreaCoeff = await ReadFloatAsync(_config.LeftEyeAreaCoeff);
RightEyeAreaCoeff = await ReadFloatAsync(_config.RightEyeAreaCoeff);
SaveRateCorrectionCoeff = await ReadFloatAsync(_config.SaveRateCorrectionCoeff);
MotorLimit = await ReadFloatAsync(_config.MotorLimit);
ResetCompensation = await ReadFloatAsync(_config.ResetCompensation);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"刷新参数失败: {ex.Message}");
CommunicationStatus = "通信异常";
StatusColor = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Colors.Red);
}
finally
{
_isUpdatingFromPlc = false;
}
}
// 设备控制命令(供右侧按钮使用)
[RelayCommand]
private async Task Reset()
{
// 实现硬件复位逻辑
CommunicationStatus = "复位指令已发送";
await _plcService.WriteCoilAsync(91, true);
Thread.Sleep(100);
await _plcService.WriteCoilAsync(91, false);
Thread.Sleep(100);
}
[RelayCommand]
private async Task OpenLeftEye()
{
CommunicationStatus = "左眼开启指令已发送";
await _plcService.WriteCoilAsync(0, true);
Thread.Sleep(100);
}
[RelayCommand]
private async Task OpenRightEye()
{
CommunicationStatus = "右眼开启指令已发送";
await _plcService.WriteCoilAsync(1, true);
Thread.Sleep(100);
}
[RelayCommand]
private async Task Reverse()
{
CommunicationStatus = "反转指令已发送";
await _plcService.WriteCoilAsync(10, true);
Thread.Sleep(100);
await _plcService.WriteCoilAsync(10, false);
Thread.Sleep(100);
}
[RelayCommand]
private async Task Forward()
{
CommunicationStatus = "正转指令已发送";
await _plcService.WriteCoilAsync(11, true);
Thread.Sleep(100);
await _plcService.WriteCoilAsync(11, false);
Thread.Sleep(100);
}
// 导航命令(可根据实际需求实现)
[RelayCommand]
private void NavigateHome()
{
// 切换页面逻辑
}
[RelayCommand]
private void NavigateTest()
{
// 切换页面逻辑
}
[RelayCommand]
private void NavigateDataRecord()
{
// 切换页面逻辑
}
[RelayCommand]
private void NavigateRecordScreen()
{
// 切换页面逻辑
}
// ========== PLC 读写基础方法 ==========
private async Task<ushort> ReadInt16Async(ushort address)
{
var registers = await ((ModbusTcpPlcService)_plcService).ReadHoldingRegistersAsync(address, 1);
return registers[0];
}
private async Task WriteInt16Async(ushort address, ushort value)
{
await ((ModbusTcpPlcService)_plcService).WriteSingleRegisterAsync(address, value);
}
private async Task<float> ReadFloatAsync(ushort address)
{
var registers = await ((ModbusTcpPlcService)_plcService).ReadHoldingRegistersAsync(address, 2);
return ((ModbusTcpPlcService)_plcService).UshortToFloat(registers[1], registers[0]);
}
private async Task WriteFloatAsync(ushort address, float value)
{
await ((ModbusTcpPlcService)_plcService).WriteMultipleRegistersAsync(address, value);
}
// 释放定时器(在窗口关闭时调用)
public void Dispose()
{
_autoRefreshTimer?.Stop();
}
}
}

View File

@@ -1,409 +0,0 @@
using MembranePoreTester.Communication;
using MembranePoreTester.Helpers;
using MembranePoreTester.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Win32;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Input;
namespace MembranePoreTester.ViewModels
{
public class PoreDistributionViewModel : ViewModelBase, IStationViewModel
{
private PoreDistributionRecord _record = new();
private TestLiquid _selectedLiquid;
private bool _isCustomLiquid;
private double _customSurfaceTension = 30.0;
private double _lowerPore = 0.2;
private double _upperPore = 0.8;
private double _rangePercentage;
// 添加字段
private readonly IPlcService _plcService;
private readonly PlcConfiguration _plcConfig;
private DataPoint _selectedDataPoint;
// 添加属性
public DataPoint SelectedDataPoint
{
get => _selectedDataPoint;
set => SetProperty(ref _selectedDataPoint, value);
}
public ICommand ReadPlcCommand { get; }
public PoreDistributionRecord Record
{
get => _record;
set => SetProperty(ref _record, value);
}
public List<TestLiquid> Liquids => TestLiquid.Predefined;
public List<string> PressureUnits => new() { "Pa", "cmHg", "psi" };
public List<string> MembraneTypes => new() { "平板膜", "中空纤维膜" };
public TestLiquid SelectedLiquid
{
get => _selectedLiquid;
set
{
if (SetProperty(ref _selectedLiquid, value))
{
Record.Liquid = value;
}
}
}
public bool IsCustomLiquid
{
get => _isCustomLiquid;
set
{
if (SetProperty(ref _isCustomLiquid, value))
{
UpdateCustomLiquid();
}
}
}
public double CustomSurfaceTension
{
get => _customSurfaceTension;
set
{
if (SetProperty(ref _customSurfaceTension, value))
{
UpdateCustomLiquid();
}
}
}
public double LowerPore
{
get => _lowerPore;
set => SetProperty(ref _lowerPore, value);
}
public double UpperPore
{
get => _upperPore;
set => SetProperty(ref _upperPore, value);
}
public double RangePercentage
{
get => _rangePercentage;
set => SetProperty(ref _rangePercentage, value);
}
public ICommand AddDataPointCommand { get; }
public ICommand RemoveDataPointCommand { get; }
public ICommand CalculateCommand { get; }
public ICommand GenerateReportCommand { get; }
public PoreDistributionViewModel()
{
_plcService = App.PlcService;
_plcConfig = App.PlcConfig;
AddDataPointCommand = new RelayCommand(AddDataPoint);
RemoveDataPointCommand = new RelayCommand(RemoveDataPoint, () => Record.DataPoints.Any());
CalculateCommand = new RelayCommand(Calculate);
GenerateReportCommand = new RelayCommand(GenerateReport);
SelectedLiquid = Liquids[0];
Record.PressureUnit = PressureUnits[0]; // 默认 "Pa"
ReadPlcCommand = new RelayCommand(async () => await ReadPlcAsync());
SaveCommand = new RelayCommand(SaveToDatabase);
ExportCommand = new RelayCommand(ExportToExcel);
OpenFlowCalibCommand = new RelayCommand(OpenFlowCalibration);
Record.DataPoints.CollectionChanged += (s, e) => UpdatePlot();
}
private async Task ReadPlcAsync()
{
try
{
// 始终读取压力
float rawPressure = await _plcService.ReadPressureAsync(StationId);
double pressure = rawPressure * _plcConfig.PressureFactor;
if (SelectedDataPoint != null)
{
// 更新选中行
SelectedDataPoint.Pressure = pressure;
if (TestMode == "湿膜")
{
float rawWet = await _plcService.ReadWetFlowAsync();
SelectedDataPoint.WetFlow = rawWet * _plcConfig.WetFlowFactor;
}
else
{
float rawDry = await _plcService.ReadDryFlowAsync();
SelectedDataPoint.DryFlow = rawDry * _plcConfig.DryFlowFactor;
}
}
else
{
// 新增一行
var newPoint = new DataPoint { Pressure = pressure };
if (TestMode == "湿膜")
{
float rawWet = await _plcService.ReadWetFlowAsync();
newPoint.WetFlow = rawWet * _plcConfig.WetFlowFactor;
}
else
{
float rawDry = await _plcService.ReadDryFlowAsync();
newPoint.DryFlow = rawDry * _plcConfig.DryFlowFactor;
}
Record.DataPoints.Add(newPoint);
}
}
catch (Exception ex)
{
MessageBox.Show($"读取PLC失败: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void AddDataPoint()
{
Record.DataPoints.Add(new DataPoint());
}
private void RemoveDataPoint()
{
if (Record.DataPoints.Any())
Record.DataPoints.RemoveAt(Record.DataPoints.Count - 1);
}
private void UpdateCustomLiquid()
{
if (IsCustomLiquid)
{
Record.Liquid = new TestLiquid
{
Name = "自定义",
SurfaceTension = CustomSurfaceTension
};
}
}
// 添加私有字段和公共属性
private double _averagePoreSize;
public double AveragePoreSize
{
get => _averagePoreSize;
set => SetProperty(ref _averagePoreSize, value);
}
// 修改 Calculate 方法:
private void Calculate()
{
if (Record.DataPoints.Count < 2) return;
AveragePoreSize = PoreDistributionAnalysis.CalculateAveragePore(
Record.DataPoints, Record.PressureUnit, Record.Liquid);
RangePercentage = PoreDistributionAnalysis.CalculatePoreRangePercentage(
Record.DataPoints, Record.PressureUnit, Record.Liquid, LowerPore, UpperPore);
}
private void GenerateReport()
{
ReportGenerator.GeneratePoreDistributionReport(Record);
}
private void OpenFlowCalibration()
{
string zeroStr = Microsoft.VisualBasic.Interaction.InputBox("请输入流量零点系数", "流量校准", "0");
string spanStr = Microsoft.VisualBasic.Interaction.InputBox("请输入流量量程系数", "流量校准", "1");
if (float.TryParse(zeroStr, out float zero) && float.TryParse(spanStr, out float span))
{
Task.Run(async () =>
{
await WriteFloatAsync(_plcConfig.FlowCalibZero, zero);
await WriteFloatAsync(_plcConfig.FlowCalibSpan, span);
MessageBox.Show("流量校准系数已写入", "完成");
});
}
}
private int _stationId;
public int StationId
{
get => _stationId;
set => SetProperty(ref _stationId, value);
}
public void SaveToDatabase()
{
var entity = new PoreDistributionEntity
{
StationId = this.StationId,
TestDate = Record.TestDate,
Tester = Record.Tester,
SampleType = Record.SampleType,
SampleSpec = Record.SampleSpec,
RoomTemperature = Record.RoomTemperature,
SoakingTime = Record.SoakingTime,
LiquidName = Record.Liquid?.Name,
LiquidSurfaceTension = Record.Liquid?.SurfaceTension ?? 0,
LiquidManufacturer = Record.LiquidManufacturer,
PressureUnit = Record.PressureUnit,
BubblePointPressure = Record.BubblePointPressure,
AveragePoreSize = AveragePoreSize, // 使用计算后的属性
DataPoints = Record.DataPoints.Select(dp => new DataPointEntity
{
Pressure = dp.Pressure,
WetFlow = dp.WetFlow,
DryFlow = dp.DryFlow
}).ToList()
};
using var db = new AppDbContext();
db.PoreDistributionRecords.Add(entity);
db.SaveChanges();
MessageBox.Show("保存成功!", "提示");
}
public void LoadFromDatabase(int recordId)
{
using var db = new AppDbContext();
var entity = db.PoreDistributionRecords
.Include(p => p.DataPoints)
.FirstOrDefault(p => p.Id == recordId);
if (entity == null) return;
Record.SampleType = entity.SampleType;
Record.SampleSpec = entity.SampleSpec;
Record.RoomTemperature = entity.RoomTemperature;
Record.SoakingTime = entity.SoakingTime;
Record.Liquid = TestLiquid.Predefined.FirstOrDefault(l => l.Name == entity.LiquidName)
?? new TestLiquid { Name = entity.LiquidName, SurfaceTension = entity.LiquidSurfaceTension };
Record.LiquidManufacturer = entity.LiquidManufacturer;
Record.PressureUnit = entity.PressureUnit;
Record.BubblePointPressure = entity.BubblePointPressure;
Record.TestDate = entity.TestDate;
Record.Tester = entity.Tester;
Record.DataPoints.Clear();
foreach (var dp in entity.DataPoints)
{
Record.DataPoints.Add(new DataPoint
{
Pressure = dp.Pressure,
WetFlow = dp.WetFlow,
DryFlow = dp.DryFlow
});
}
// 重新计算平均孔径和分布(触发计算命令)
Calculate();
}
public ICommand SaveCommand { get; }
public ICommand OpenFlowCalibCommand { get; }
public ICommand ExportCommand { get; }
private void ExportToExcel()
{
var saveFileDialog = new SaveFileDialog
{
Filter = "Excel文件|*.xlsx",
FileName = $"泡点法_工位{StationId}_{DateTime.Now:yyyyMMddHHmmss}.xlsx"
};
if (saveFileDialog.ShowDialog() == true)
{
// 转换为Entity后导出
var entity = new BubblePointEntity { /* 从Record复制 */ };
ExportHelper.ExportBubblePoint(entity, saveFileDialog.FileName);
MessageBox.Show("导出成功");
}
}
private string _testMode = "湿膜";
public string TestMode
{
get => _testMode;
set => SetProperty(ref _testMode, value);
}
private int _selectedFlowModeIndex; // 0=大流量1=小流量
public int SelectedFlowModeIndex
{
get => _selectedFlowModeIndex;
set
{
if (SetProperty(ref _selectedFlowModeIndex, value))
{
ushort reg = StationId switch
{
1 => _plcConfig.FlowModeRegister1,
2 => _plcConfig.FlowModeRegister2,
3 => _plcConfig.FlowModeRegister3,
_ => 0
};
Task.Run(async () => await _plcService.WriteSingleRegisterAsync(reg, (ushort)value));
}
}
}
private OxyPlot.PlotModel _plotModel;
public OxyPlot.PlotModel PlotModel
{
get => _plotModel;
set => SetProperty(ref _plotModel, value);
}
private void UpdatePlot()
{
var sorted = Record.DataPoints.OrderBy(p => p.Pressure).ToList();
if (sorted.Count == 0)
{
PlotModel = null;
return;
}
var model = new OxyPlot.PlotModel { Title = "流量-压力曲线" };
model.Axes.Add(new OxyPlot.Axes.LinearAxis { Position = OxyPlot.Axes.AxisPosition.Bottom, Title = "压力" });
model.Axes.Add(new OxyPlot.Axes.LinearAxis { Position = OxyPlot.Axes.AxisPosition.Left, Title = "流量 (L/min)" });
var wetSeries = new OxyPlot.Series.LineSeries { Title = "湿膜流量", Color = OxyPlot.OxyColors.Blue, MarkerType = OxyPlot.MarkerType.Circle };
var drySeries = new OxyPlot.Series.LineSeries { Title = "干膜流量", Color = OxyPlot.OxyColors.Red, MarkerType = OxyPlot.MarkerType.Circle };
foreach (var dp in sorted)
{
wetSeries.Points.Add(new OxyPlot.DataPoint(dp.Pressure, dp.WetFlow));
drySeries.Points.Add(new OxyPlot.DataPoint(dp.Pressure, dp.DryFlow));
}
model.Series.Add(wetSeries);
model.Series.Add(drySeries);
PlotModel = model;
}
}
}

View File

@@ -1,97 +0,0 @@
using System.Windows.Input;
using MembranePoreTester.Communication;
using System.Threading.Tasks;
using System.Windows;
namespace MembranePoreTester.ViewModels
{
public class StationViewModel : ViewModelBase
{
private readonly IPlcService _plcService;
private readonly PlcConfiguration _plcConfig;
private bool _isPressing;
private string _pressButtonText = "加压";
private string _highLowPressure = "低压";
private bool _enableChecked;
public int StationId { get; set; }
public BubblePointViewModel BubblePointVM { get; } = new();
public PoreDistributionViewModel PoreDistributionVM { get; } = new();
public string HighLowPressure
{
get => _highLowPressure;
set => SetProperty(ref _highLowPressure, value);
}
public string PressButtonText
{
get => _pressButtonText;
set => SetProperty(ref _pressButtonText, value);
}
public bool EnableChecked
{
get => _enableChecked;
set => SetProperty(ref _enableChecked, value);
}
public ICommand PressCommand { get; }
public ICommand BurstCommand { get; }
public ICommand StartCommand { get; }
public ICommand StopCommand { get; }
public ICommand EnableCommand { get; } // 用于使能复选框
public StationViewModel()
{
_plcService = App.PlcService;
_plcConfig = App.PlcConfig;
BubblePointVM.StationId = StationId;
PoreDistributionVM.StationId = StationId;
// 初始化按钮文字
_pressButtonText = "加压"; // 直接设置字段,避免触发属性通知循环
PressButtonText = "加压"; // 或者通过属性设置
System.Diagnostics.Debug.WriteLine($"工位{StationId} PressButtonText初始值: {PressButtonText}");
PressCommand = new RelayCommand(async () => await TogglePressAsync());
BurstCommand = new RelayCommand(async () => await ReadBurstPressureAsync());
StartCommand = new RelayCommand(async () => await WriteCoilAsync(_plcConfig.StartCoil, true));
StopCommand = new RelayCommand(async () => await WriteCoilAsync(_plcConfig.StopCoil, true));
// 使能复选框点击时写入对应线圈
EnableCommand = new RelayCommand(async () => await WriteCoilAsync(_plcConfig.EnableCoil, EnableChecked));
}
private async Task TogglePressAsync()
{
_isPressing = !_isPressing;
await WriteCoilAsync(_plcConfig.PressCoil, _isPressing);
PressButtonText = _isPressing ? "停止加压" : "加压";
}
private async Task ReadBurstPressureAsync()
{
try
{
float pressure = await ((ModbusTcpPlcService)_plcService).ReadPressureAsync(StationId);
BubblePointVM.UpdateBubblePointPressure(pressure * _plcConfig.PressureFactor);
MessageBox.Show($"涨破压力: {pressure} {BubblePointVM.Record.PressureUnit}", "提示");
}
catch (Exception ex)
{
MessageBox.Show($"读取压力失败: {ex.Message}");
}
}
private async Task WriteCoilAsync(ushort coil, bool value)
{
try
{
await ((ModbusTcpPlcService)_plcService).WriteCoilAsync(coil, value);
}
catch (Exception ex)
{
MessageBox.Show($"写线圈失败: {ex.Message}");
}
}
}
}

263
ViewModels/TestViewModel.cs Normal file
View File

@@ -0,0 +1,263 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Windows.Forms;
using System.Windows.Threading;
namespace MembranePoreTester
{
public partial class TestViewModel : ObservableObject
{
private readonly DispatcherTimer _angleUpdateTimer;
private bool _isTestRunning;
public TestViewModel()
{
_angleUpdateTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(100)
};
_angleUpdateTimer.Tick += AngleUpdateTimer_Tick;
// 默认值
ResolutionAngle = 0.1;
RotationSpeed = 30;
LeftEyeCoeff = 1.0;
RightEyeCoeff = 1.0;
RetentionCorrectionCoeff = 1.0;
IsSampleMode = true;
}
// 当前模式显示文本
[ObservableProperty]
private string _currentMode = "试样测试";
// 模式切换
private bool _isBlankMode;
public bool IsBlankMode
{
get => _isBlankMode;
set
{
if (SetProperty(ref _isBlankMode, value) && value)
{
CurrentMode = "空白模式";
IsSampleMode = false;
OnModeChanged();
}
}
}
private bool _isSampleMode;
public bool IsSampleMode
{
get => _isSampleMode;
set
{
if (SetProperty(ref _isSampleMode, value) && value)
{
CurrentMode = "试样测试";
IsBlankMode = false;
OnModeChanged();
}
}
}
// 参数设置
[ObservableProperty]
private double _resolutionAngle;
[ObservableProperty]
private double _rotationSpeed;
[ObservableProperty]
private double _currentAngle;
// 视野面积数据
[ObservableProperty]
private double _leftEyeArea;
[ObservableProperty]
private double _rightEyeArea;
[ObservableProperty]
private double _binocularArea;
[ObservableProperty]
private double _lowerField;
[ObservableProperty]
private double _blankArea;
[ObservableProperty]
private double _fieldRetentionRate;
// 灯条数据
[ObservableProperty]
private double _upperLampData;
[ObservableProperty]
private double _middleLampData;
[ObservableProperty]
private double _lowerLampData;
// 面积系数
[ObservableProperty]
private double _leftEyeCoeff;
[ObservableProperty]
private double _rightEyeCoeff;
[ObservableProperty]
private double _retentionCorrectionCoeff;
// 命令
[RelayCommand]
private void Reset()
{
CurrentAngle = 0;
// 用户在此添加硬件复位代码
}
[RelayCommand]
private void OpenLeftEye()
{
// 用户实现左眼开逻辑
UpdateLeftEyeArea();
}
[RelayCommand]
private void OpenRightEye()
{
// 用户实现右眼开逻辑
UpdateRightEyeArea();
}
[RelayCommand]
private void Reverse()
{
// 反转方向
RotationSpeed = -Math.Abs(RotationSpeed);
}
[RelayCommand]
private void Forward()
{
// 正转方向
RotationSpeed = Math.Abs(RotationSpeed);
}
[RelayCommand]
private void StartTest()
{
if (_isTestRunning) return;
_isTestRunning = true;
_angleUpdateTimer.Start();
}
[RelayCommand]
private void StopTest()
{
_isTestRunning = false;
_angleUpdateTimer.Stop();
}
// 导航命令(供用户跳转页面)
[RelayCommand]
private void NavigateHome()
{
// 用户实现:关闭当前窗口或切换主窗口内容
}
[RelayCommand]
private void NavigateTest()
{
// 已在测试界面,可忽略
}
[RelayCommand]
private void NavigateData()
{
// 跳转到数据记录界面
}
[RelayCommand]
private void NavigateRecord()
{
// 跳转到记录画面
}
// 内部辅助方法
private void OnModeChanged()
{
if (IsBlankMode)
{
BlankArea = 0;
FieldRetentionRate = 0;
}
else
{
CalculateFieldAreas();
}
}
private void AngleUpdateTimer_Tick(object sender, EventArgs e)
{
if (!_isTestRunning) return;
// 模拟角度变化
CurrentAngle += RotationSpeed * 0.1;
if (CurrentAngle >= 360) CurrentAngle -= 360;
if (CurrentAngle < 0) CurrentAngle += 360;
CalculateFieldAreas();
UpdateLampData();
}
private void CalculateFieldAreas()
{
double angle = CurrentAngle;
LeftEyeArea = Math.Round(50 + 30 * Math.Sin(angle * Math.PI / 180) * LeftEyeCoeff, 3);
RightEyeArea = Math.Round(50 + 30 * Math.Cos(angle * Math.PI / 180) * RightEyeCoeff, 3);
BinocularArea = Math.Round(LeftEyeArea + RightEyeArea - 20, 3);
LowerField = Math.Round(30 + 20 * Math.Sin(angle * Math.PI / 180), 1);
if (IsBlankMode)
{
BlankArea = Math.Round(10 + 5 * Math.Sin(angle * Math.PI / 180), 3);
FieldRetentionRate = BinocularArea > 0
? Math.Round((BinocularArea - BlankArea) / BinocularArea * 100, 2)
: 0;
}
else
{
FieldRetentionRate = Math.Round(BinocularArea / 100 * 100 * RetentionCorrectionCoeff, 2);
if (FieldRetentionRate > 100) FieldRetentionRate = 100;
if (FieldRetentionRate < 0) FieldRetentionRate = 0;
}
}
private void UpdateLeftEyeArea()
{
LeftEyeArea = Math.Round(LeftEyeArea + 5, 3);
if (LeftEyeArea > 100) LeftEyeArea = 100;
CalculateFieldAreas();
}
private void UpdateRightEyeArea()
{
RightEyeArea = Math.Round(RightEyeArea + 5, 3);
if (RightEyeArea > 100) RightEyeArea = 100;
CalculateFieldAreas();
}
private void UpdateLampData()
{
Random rand = new Random();
UpperLampData = Math.Round(rand.NextDouble() * 100, 2);
MiddleLampData = Math.Round(rand.NextDouble() * 100, 2);
LowerLampData = Math.Round(rand.NextDouble() * 100, 2);
}
}
}

View File

@@ -1,10 +1,11 @@
using MembranePoreTester.Communication;
using CommunityToolkit.Mvvm.ComponentModel;
using MembranePoreTester.Communication;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MembranePoreTester.ViewModels
{
public class ViewModelBase : INotifyPropertyChanged
public class ViewModelBase : ObservableObject
{
public event PropertyChangedEventHandler PropertyChanged;

View File

@@ -1,89 +0,0 @@
<UserControl x:Class="MembranePoreTester.Views.BubblePointView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MembranePoreTester.ViewModels"
xmlns:conv="clr-namespace:MembranePoreTester.Converters">
<UserControl.Resources>
<conv:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
</UserControl.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 输入区域 -->
<StackPanel Grid.Row="0">
<GroupBox Header="样品信息" Margin="0,0,0,10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="膜类型:"/>
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding MembraneTypes}"
SelectedItem="{Binding Record.SampleType}" Margin="5"/>
<Label Grid.Row="0" Grid.Column="2" Content="规格:"/>
<TextBox Grid.Row="0" Grid.Column="3" Text="{Binding Record.SampleSpec}" Margin="5"/>
<Label Grid.Row="1" Grid.Column="0" Content="室温(°C):"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Record.RoomTemperature}" Margin="5"/>
<Label Grid.Row="1" Grid.Column="2" Content="浸润时间(h):"/>
<TextBox Grid.Row="1" Grid.Column="3" Text="{Binding Record.SoakingTime}" Margin="5"/>
</Grid>
</GroupBox>
<GroupBox Header="测试液体" Margin="0,0,0,10">
<StackPanel>
<RadioButton Content="预定义" IsChecked="{Binding IsCustomLiquid, Converter={StaticResource InverseBooleanConverter}}" Margin="5"/>
<ComboBox ItemsSource="{Binding Liquids}" SelectedItem="{Binding SelectedLiquid}"
DisplayMemberPath="Name" IsEnabled="{Binding IsCustomLiquid, Converter={StaticResource InverseBooleanConverter}}"
Margin="5,0,5,5"/>
<RadioButton Content="自定义" IsChecked="{Binding IsCustomLiquid}" Margin="5"/>
<StackPanel Orientation="Horizontal" Margin="20,0,0,0" IsEnabled="{Binding IsCustomLiquid}">
<Label Content="表面张力(mN/m):"/>
<TextBox Text="{Binding CustomSurfaceTension}" Width="100" Margin="5"/>
</StackPanel>
<!--<Label Content="生产厂家:" Margin="5"/>
<TextBox Text="{Binding Record.LiquidManufacturer}" Margin="5"/>-->
</StackPanel>
</GroupBox>
<GroupBox Header="泡点压力" Margin="0,0,0,10">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding Record.BubblePointPressure}" Width="150" Margin="5"/>
<Button Content="涨破" Command="{Binding ReadPlcCommand}" Padding="10,5" Margin="5"/>
<ComboBox ItemsSource="{Binding PressureUnits}" SelectedItem="{Binding Record.PressureUnit}" Width="80" Margin="5"/>
<Button Content="计算最大孔径" Command="{Binding CalculateCommand}" Padding="10,5" Margin="5"/>
</StackPanel>
</GroupBox>
</StackPanel>
<!-- 结果显示 -->
<GroupBox Grid.Row="1" Header="测试结果" VerticalAlignment="Center">
<TextBlock FontSize="36" TextAlignment="Center"
Text="{Binding MaxPoreSize, StringFormat={}{0:F3} μm}"/>
</GroupBox>
<!-- 底部按钮 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="压力校准" Command="{Binding OpenPressureCalibCommand}" Padding="15,8" Margin="5"/>
<Button Content="保存到历史" Command="{Binding SaveCommand}" Padding="15,8" Margin="5"/>
<Button Content="生成报告" Command="{Binding GenerateReportCommand}" Padding="15,8" Margin="5"/>
<Button Content="导出Excel" Command="{Binding ExportCommand}" Padding="15,8" Margin="5"/>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,13 +0,0 @@
using System.Windows.Controls;
namespace MembranePoreTester.Views
{
public partial class BubblePointView : UserControl
{
public BubblePointView()
{
InitializeComponent();
//DataContext = new ViewModels.BubblePointViewModel();
}
}
}

View File

@@ -1,57 +0,0 @@
<Window x:Class="MembranePoreTester.Views.HistoryWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="历史记录" Height="600" Width="900"
WindowStartupLocation="CenterOwner">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 过滤条件 -->
<StackPanel Orientation="Horizontal" Grid.Row="0" Margin="0,0,0,10">
<Label Content="工位:"/>
<ComboBox x:Name="cmbStation" SelectedIndex="0" Width="80" Margin="5,0">
<ComboBoxItem Content="全部"/>
<ComboBoxItem Content="1"/>
<ComboBoxItem Content="2"/>
<ComboBoxItem Content="3"/>
</ComboBox>
<Label Content="测试类型:"/>
<ComboBox x:Name="cmbType" SelectedIndex="0" Width="120" Margin="5,0">
<ComboBoxItem Content="全部"/>
<ComboBoxItem Content="泡点法"/>
<ComboBoxItem Content="孔分布"/>
</ComboBox>
<Label Content="日期范围:"/>
<DatePicker x:Name="dpStart" Width="120"/>
<Label Content="-" Margin="2"/>
<DatePicker x:Name="dpEnd" Width="120"/>
<Button Content="查询" Click="Query_Click" Padding="15,5" Margin="10,0"/>
<Button Content="导出选中" Click="ExportSelected_Click" Padding="15,5"/>
</StackPanel>
<!-- 历史记录列表 -->
<DataGrid x:Name="dgHistory" Grid.Row="1" AutoGenerateColumns="False"
IsReadOnly="True" SelectionMode="Single" SelectionUnit="FullRow">
<DataGrid.Columns>
<DataGridTextColumn Header="ID" Binding="{Binding Id}" Width="50"/>
<DataGridTextColumn Header="工位" Binding="{Binding StationId}" Width="50"/>
<DataGridTextColumn Header="类型" Binding="{Binding Type}" Width="80"/>
<DataGridTextColumn Header="测试日期" Binding="{Binding TestDate}" Width="150"/>
<DataGridTextColumn Header="样品类型" Binding="{Binding SampleType}" Width="100"/>
<DataGridTextColumn Header="规格" Binding="{Binding SampleSpec}" Width="100"/>
<DataGridTextColumn Header="测试者" Binding="{Binding Tester}" Width="100"/>
<DataGridTextColumn Header="结果" Binding="{Binding Result}" Width="150"/>
</DataGrid.Columns>
</DataGrid>
<!-- 底部按钮 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
<Button Content="加载到当前工位" Click="LoadToCurrentStation_Click" Padding="15,5" Margin="5"/>
<Button Content="关闭" Click="Close_Click" Padding="15,5"/>
</StackPanel>
</Grid>
</Window>

View File

@@ -1,156 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace MembranePoreTester.Views
{
public partial class HistoryWindow : Window
{
public int? SelectedStation { get; set; } // 传入当前工位ID
public HistoryWindow()
{
InitializeComponent();
}
private void Query_Click(object sender, RoutedEventArgs e)
{
using var db = new AppDbContext();
var query = new List<object>();
// 获取泡点法记录
var bubblePoints = db.BubblePointRecords
.Select(b => new
{
b.Id,
b.StationId,
Type = "泡点法",
b.TestDate,
b.SampleType,
b.SampleSpec,
b.Tester,
Result = $"最大孔径: {b.MaxPoreSize:F3} μm"
});
query.AddRange(bubblePoints);
// 获取孔分布记录
var poreDistributions = db.PoreDistributionRecords
.Select(p => new
{
p.Id,
p.StationId,
Type = "孔分布",
p.TestDate,
p.SampleType,
p.SampleSpec,
p.Tester,
Result = $"平均孔径: {p.AveragePoreSize:F3} μm"
});
query.AddRange(poreDistributions);
// 过滤工位
if (cmbStation.SelectedIndex > 0)
{
int station = cmbStation.SelectedIndex;
query = query.Where(x => ((dynamic)x).StationId == station).ToList();
}
// 过滤类型
if (cmbType.SelectedIndex == 1)
query = query.Where(x => ((dynamic)x).Type == "泡点法").ToList();
else if (cmbType.SelectedIndex == 2)
query = query.Where(x => ((dynamic)x).Type == "孔分布").ToList();
// 过滤日期范围
if (dpStart.SelectedDate != null)
query = query.Where(x => ((dynamic)x).TestDate >= dpStart.SelectedDate).ToList();
if (dpEnd.SelectedDate != null)
query = query.Where(x => ((dynamic)x).TestDate <= dpEnd.SelectedDate.Value.AddDays(1)).ToList();
dgHistory.ItemsSource = query.OrderByDescending(x => ((dynamic)x).TestDate).ToList();
}
private void ExportSelected_Click(object sender, RoutedEventArgs e)
{
if (dgHistory.SelectedItem == null)
{
MessageBox.Show("请先选中一条记录");
return;
}
dynamic selected = dgHistory.SelectedItem;
int id = selected.Id;
string type = selected.Type;
var saveFileDialog = new SaveFileDialog
{
Filter = "Excel文件|*.xlsx",
FileName = $"{type}_{id}.xlsx"
};
if (saveFileDialog.ShowDialog() == true)
{
if (type == "泡点法")
{
using var db = new AppDbContext();
var entity = db.BubblePointRecords.Find(id);
if (entity != null)
{
ExportHelper.ExportBubblePoint(entity, saveFileDialog.FileName);
}
}
else
{
using var db = new AppDbContext();
var entity = db.PoreDistributionRecords
.Include(p => p.DataPoints)
.FirstOrDefault(p => p.Id == id);
if (entity != null)
{
ExportHelper.ExportPoreDistribution(entity, saveFileDialog.FileName);
}
}
}
}
private void LoadToCurrentStation_Click(object sender, RoutedEventArgs e)
{
if (dgHistory.SelectedItem == null) return;
if (SelectedStation == null)
{
MessageBox.Show("未指定当前工位");
return;
}
dynamic selected = dgHistory.SelectedItem;
int id = selected.Id;
string type = selected.Type;
// 通过事件或回调将记录加载到当前工位
// 这里需要与主窗口交互,简单起见,我们使用静态事件
LoadRecordEvent?.Invoke(this, new LoadRecordEventArgs
{
RecordId = id,
Type = type,
TargetStation = SelectedStation.Value
});
DialogResult = true;
Close();
}
private void Close_Click(object sender, RoutedEventArgs e) => Close();
public static event EventHandler<LoadRecordEventArgs> LoadRecordEvent;
}
public class LoadRecordEventArgs : EventArgs
{
public int RecordId { get; set; }
public string Type { get; set; }
public int TargetStation { get; set; }
}
}

View File

@@ -1,66 +0,0 @@
<Window x:Class="MembranePoreTester.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MembranePoreTester.Views"
xmlns:viewModels="clr-namespace:MembranePoreTester.ViewModels"
Title="膜孔径测试系统 (GB/T 32361-2015)"
Width="1024" Height="768"
WindowStartupLocation="CenterScreen" KeyDown="Window_KeyDown">
<Window.DataContext>
<viewModels:MainViewModel />
</Window.DataContext>
<DockPanel>
<!-- 全局历史记录按钮 -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="5">
<Button Content="历史记录" Click="OpenHistory_Click" Padding="10,5" Margin="5"/>
</StackPanel>
<!-- 工位选项卡 -->
<TabControl x:Name="stationTabControl" ItemsSource="{Binding Stations}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<DockPanel>
<!-- 工位控制栏 -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="5">
<ComboBox SelectedItem="{Binding HighLowPressure}" Width="80" Margin="5">
<ComboBoxItem IsSelected="True">低压</ComboBoxItem>
<ComboBoxItem>高压</ComboBoxItem>
</ComboBox>
<Button Content="启动" Command="{Binding StartCommand}" Width="80" Margin="5"/>
<Button Content="停止" Command="{Binding StopCommand}" Width="80" Margin="5"/>
<Button Content="{Binding PressButtonText}" Command="{Binding PressCommand}" Width="80" Margin="5"/>
<!--<Button Content="涨破" Command="{Binding BurstCommand}" Width="80" Margin="5"/>-->
<!--<CheckBox Content="使能" IsChecked="{Binding EnableChecked}" Margin="5"/>-->
<!-- 原来的复选框 -->
<!-- <CheckBox Content="使能" IsChecked="{Binding EnableChecked}" Margin="5"/> -->
<!-- 替换为状态指示灯 -->
<StackPanel Orientation="Horizontal" Margin="5">
<Ellipse Width="12" Height="12" Fill="{Binding EnableStatusColor}" Margin="2"/>
<TextBlock Text="{Binding EnableStatusText}" VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
<!-- 测试类型选项卡 -->
<TabControl>
<TabItem Header="泡点法测试最大孔径">
<local:BubblePointView DataContext="{Binding BubblePointVM}"/>
</TabItem>
<TabItem Header="孔分布测试">
<local:PoreDistributionView DataContext="{Binding PoreDistributionVM}"/>
</TabItem>
</TabControl>
</DockPanel>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</Window>

View File

@@ -1,57 +0,0 @@
using MembranePoreTester.ViewModels;
using System.Windows;
using System.Windows.Input;
namespace MembranePoreTester.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
HistoryWindow.LoadRecordEvent += OnLoadRecord;
}
private void OnLoadRecord(object sender, LoadRecordEventArgs e)
{
var mainVM = DataContext as MainViewModel;
if (mainVM == null) return;
// 根据工位和类型找到对应的 ViewModel
if (e.Type == "泡点法")
{
var targetVM = mainVM.Stations[e.TargetStation - 1].BubblePointVM;
targetVM.LoadFromDatabase(e.RecordId);
}
else
{
var targetVM = mainVM.Stations[e.TargetStation - 1].PoreDistributionVM;
targetVM.LoadFromDatabase(e.RecordId);
}
}
private void OpenHistory_Click(object sender, RoutedEventArgs e)
{
var mainVM = DataContext as MainViewModel;
if (mainVM == null) return;
// 获取当前选中的工位索引(假设选项卡控件是 TabControl名称为 stationTabControl
// 需要在 XAML 中为 TabControl 设置 x:Name="stationTabControl"
int currentStation = stationTabControl.SelectedIndex + 1; // 假设索引从0开始
var historyWin = new HistoryWindow { SelectedStation = currentStation };
historyWin.ShowDialog();
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.P)
{
var win = new ParameterWindow();
win.Owner = this;
win.ShowDialog();
}
}
}
}

View File

@@ -1,120 +1,280 @@
<Window x:Class="MembranePoreTester.Views.ParameterWindow"
<Window x:Class="MembranePoreTester.ParameterWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="运维参数设置" Height="600" Width="800"
WindowStartupLocation="CenterOwner">
<ScrollViewer>
<StackPanel Margin="10">
<GroupBox Header="加压控制">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0">加压上限(D300):</Label>
<TextBox x:Name="txtPressureUpper" Grid.Row="0" Grid.Column="1"/>
<Label Grid.Row="1" Grid.Column="0">加压速率(D280):</Label>
<TextBox x:Name="txtPressureRate" Grid.Row="1" Grid.Column="1"/>
<Label Grid.Row="2" Grid.Column="0">加压系数(D282):</Label>
<TextBox x:Name="txtPressureCoeff" Grid.Row="2" Grid.Column="1"/>
</Grid>
</GroupBox>
xmlns:local="clr-namespace:MembranePoreTester"
Title="智能设备控制平台"
Width="1024" MinHeight="768" WindowState="Maximized"
WindowStartupLocation="CenterScreen"
ResizeMode="CanResize"
Background="#F5F7FA" FontFamily="Segoe UI">
<GroupBox Header="高压/低压系数">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
<!-- 预留空白 -->
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Window.Resources>
<!-- 统一按钮样式 -->
<Style TargetType="Button">
<Setter Property="Background" Value="#2196F3"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="4" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#1976D2"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#0D47A1"/>
</Trigger>
</Style.Triggers>
</Style>
<Label Grid.Row="0" Grid.Column="0" FontWeight="Bold">工位</Label>
<Label Grid.Row="0" Grid.Column="1" FontWeight="Bold">高压系数</Label>
<Label Grid.Row="0" Grid.Column="2" FontWeight="Bold">低压系数</Label>
<!-- 统一文本框样式 -->
<Style TargetType="TextBox">
<Setter Property="Height" Value="28"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#D0D3D9"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="5,2"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Label Grid.Row="1" Grid.Column="0">工位1</Label>
<TextBox x:Name="txtHPCoeff1" Grid.Row="1" Grid.Column="1" Width="80" HorizontalAlignment="Left"/>
<TextBox x:Name="txtLPCoeff1" Grid.Row="1" Grid.Column="2" Width="80" HorizontalAlignment="Left"/>
<!-- 统一标签样式 -->
<Style TargetType="Label">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#2C3E50"/>
<Setter Property="Margin" Value="5,5,5,5"/>
</Style>
<Label Grid.Row="2" Grid.Column="0">工位2</Label>
<TextBox x:Name="txtHPCoeff2" Grid.Row="2" Grid.Column="1" Width="80" HorizontalAlignment="Left"/>
<TextBox x:Name="txtLPCoeff2" Grid.Row="2" Grid.Column="2" Width="80" HorizontalAlignment="Left"/>
<!-- 统一GroupBox样式 -->
<Style TargetType="GroupBox">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#E9ECF0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="0,0,0,12"/>
<Setter Property="Padding" Value="12"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupBox">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="6">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{TemplateBinding Header}" FontWeight="SemiBold" FontSize="14" Margin="0,0,0,12"/>
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<Label Grid.Row="3" Grid.Column="0">工位3</Label>
<TextBox x:Name="txtHPCoeff3" Grid.Row="3" Grid.Column="1" Width="80" HorizontalAlignment="Left"/>
<TextBox x:Name="txtLPCoeff3" Grid.Row="3" Grid.Column="2" Width="80" HorizontalAlignment="Left"/>
</Grid>
</GroupBox>
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<GroupBox Header="流量系数">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="100"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" FontWeight="Bold">工位</Label>
<Label Grid.Row="0" Grid.Column="1" FontWeight="Bold">大流量系数</Label>
<Label Grid.Row="0" Grid.Column="2" FontWeight="Bold">小流量系数</Label>
<Label Grid.Row="1" Grid.Column="0">工位1</Label>
<TextBox x:Name="txtLargeFlow1" Grid.Row="1" Grid.Column="1"/>
<TextBox x:Name="txtSmallFlow1" Grid.Row="1" Grid.Column="2"/>
<Label Grid.Row="2" Grid.Column="0">工位2</Label>
<TextBox x:Name="txtLargeFlow2" Grid.Row="2" Grid.Column="1"/>
<TextBox x:Name="txtSmallFlow2" Grid.Row="2" Grid.Column="2"/>
<Label Grid.Row="3" Grid.Column="0">工位3</Label>
<TextBox x:Name="txtLargeFlow3" Grid.Row="3" Grid.Column="1"/>
<TextBox x:Name="txtSmallFlow3" Grid.Row="3" Grid.Column="2"/>
</Grid>
</GroupBox>
<GroupBox Header="校准参数">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="150"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0">压力零点校准:</Label>
<TextBox x:Name="txtPressureZero" Grid.Row="0" Grid.Column="1"/>
<Label Grid.Row="1" Grid.Column="0">压力量程校准:</Label>
<TextBox x:Name="txtPressureSpan" Grid.Row="1" Grid.Column="1"/>
<Label Grid.Row="2" Grid.Column="0">流量零点校准:</Label>
<TextBox x:Name="txtFlowZero" Grid.Row="2" Grid.Column="1"/>
<Label Grid.Row="3" Grid.Column="0">流量量程校准:</Label>
<TextBox x:Name="txtFlowSpan" Grid.Row="3" Grid.Column="1"/>
</Grid>
</GroupBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="0,20,0,0">
<!--<Button Content="读取参数" Click="ReadParameters_Click" Width="100" Margin="5"/>-->
<Button Content="写入参数" Click="WriteParameters_Click" Width="100" Margin="5"/>
<Button Content="关闭" Click="Close_Click" Width="100" Margin="5"/>
<!-- 标题栏 -->
<Border Grid.Row="0" Background="#2196F3" CornerRadius="6" Padding="12" Margin="0,0,0,12">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="智能设备控制平台" FontSize="24" FontWeight="Bold" Foreground="White"/>
<TextBlock Text="v1.0" FontSize="14" Foreground="#E3F2FD" VerticalAlignment="Bottom" Margin="10,0,0,5"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Border>
<!-- 主要内容区 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2.5*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧参数区 -->
<ScrollViewer Grid.Column="0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="0,0,12,0">
<!-- 灯条数据组 -->
<GroupBox Header="📊 灯条数据">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 第1行 -->
<Label Grid.Row="0" Grid.Column="0" Content="上灯条数据1"/>
<TextBox IsReadOnly="True" Name="txtUpperLampData1" Grid.Row="0" Grid.Column="1" Text="{Binding UpperLampData1, UpdateSourceTrigger=LostFocus}" Width="80"/>
<Label Grid.Row="0" Grid.Column="2" Content="上灯条数据4"/>
<TextBox IsReadOnly="True" Name="txtUpperLampData4" Grid.Row="0" Grid.Column="3" Text="{Binding UpperLampData4, UpdateSourceTrigger=LostFocus}" Width="80"/>
<!-- 第2行 -->
<Label Grid.Row="1" Grid.Column="0" Content="下灯条数据1"/>
<TextBox IsReadOnly="True" Grid.Row="1" Name="txtLowerLampData1" Grid.Column="1" Text="{Binding LowerLampData1, UpdateSourceTrigger=LostFocus}" Width="80"/>
<Label Grid.Row="1" Grid.Column="2" Content="下灯条数据4"/>
<TextBox IsReadOnly="True" Grid.Row="1" Name="txtLowerLampData4" Grid.Column="3" Text="{Binding LowerLampData4, UpdateSourceTrigger=LostFocus}" Width="80"/>
<!-- 第3行 -->
<Label Grid.Row="2" Grid.Column="0" Content="上灯条数据2"/>
<TextBox IsReadOnly="True" Name="txtUpperLampData2" Grid.Row="2" Grid.Column="1" Text="{Binding UpperLampData2, UpdateSourceTrigger=LostFocus}" Width="80"/>
<Label Grid.Row="2" Grid.Column="2" Content="上灯条数据5"/>
<TextBox IsReadOnly="True" Name="txtUpperLampData5" Grid.Row="2" Grid.Column="3" Text="{Binding UpperLampData5, UpdateSourceTrigger=LostFocus}" Width="80"/>
<!-- 第4行 -->
<Label Grid.Row="3" Grid.Column="0" Content="下灯条数据2"/>
<TextBox IsReadOnly="True" Grid.Row="3" Name="txtLowerLampData2" Grid.Column="1" Text="{Binding LowerLampData2, UpdateSourceTrigger=LostFocus}" Width="80"/>
<Label Grid.Row="3" Grid.Column="2" Content="下灯条数据5"/>
<TextBox IsReadOnly="True" Name="txtLowerLampData5" Grid.Row="3" Grid.Column="3" Text="{Binding LowerLampData5, UpdateSourceTrigger=LostFocus}" Width="80"/>
<!-- 第5行 -->
<Label Grid.Row="4" Grid.Column="0" Content="上灯条数据3"/>
<TextBox IsReadOnly="True" Name="txtUpperLampData3" Grid.Row="4" Grid.Column="1" Text="{Binding UpperLampData3, UpdateSourceTrigger=LostFocus}" Width="80"/>
<Label Grid.Row="4" Grid.Column="2" Content="上灯条数据6"/>
<TextBox IsReadOnly="True" Name="txtUpperLampData6" Grid.Row="4" Grid.Column="3" Text="{Binding UpperLampData6, UpdateSourceTrigger=LostFocus}" Width="80"/>
<!-- 第6行 -->
<Label Grid.Row="5" Grid.Column="0" Content="下灯条数据3"/>
<TextBox IsReadOnly="True" Grid.Row="5" Name="txtLowerLampData3" Grid.Column="1" Text="{Binding LowerLampData3, UpdateSourceTrigger=LostFocus}" Width="80"/>
<Label Grid.Row="5" Grid.Column="2" Content="下灯条数据6"/>
<TextBox IsReadOnly="True" Name="txtLowerLampData6" Grid.Row="5" Grid.Column="3" Text="{Binding LowerLampData6, UpdateSourceTrigger=LostFocus}" Width="80"/>
</Grid>
</GroupBox>
<!-- 面积系数与保存率矫正系数 -->
<GroupBox Header="📐 面积系数">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="左眼面积系数"/>
<TextBox x:Name="txtLeftEyeAreaCoeff" Grid.Row="0" Grid.Column="1" Text="{Binding LeftEyeAreaCoeff, UpdateSourceTrigger=LostFocus}" Width="120"/>
<Label Grid.Row="1" Grid.Column="0" Content="右眼面积系数"/>
<TextBox x:Name="txtRightEyeAreaCoeff" Grid.Row="1" Grid.Column="1" Text="{Binding RightEyeAreaCoeff, UpdateSourceTrigger=LostFocus}" Width="120"/>
<Label Grid.Row="2" Grid.Column="0" Content="保存率矫正系数"/>
<TextBox x:Name="txtSaveRateCorrectionCoeff" Grid.Row="2" Grid.Column="1" Text="{Binding SaveRateCorrectionCoeff, UpdateSourceTrigger=LostFocus}" Width="120"/>
</Grid>
</GroupBox>
<!-- 中灯数据组 -->
<GroupBox Header="💡 中灯数据">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 第1行 -->
<Label Grid.Row="0" Grid.Column="0" Content="中灯1"/>
<TextBox IsReadOnly="True" Name="txtMiddleLamp1" Grid.Row="0" Grid.Column="1" Text="{Binding MiddleLamp1, UpdateSourceTrigger=LostFocus}" Width="80"/>
<Label Grid.Row="0" Grid.Column="2" Content="中灯5"/>
<TextBox IsReadOnly="True" Name="txtMiddleLamp5" Grid.Row="0" Grid.Column="3" Text="{Binding MiddleLamp5, UpdateSourceTrigger=LostFocus}" Width="80"/>
<!-- 第2行 -->
<Label Grid.Row="1" Grid.Column="0" Content="中灯2"/>
<TextBox IsReadOnly="True" Name="txtMiddleLamp2" Grid.Row="1" Grid.Column="1" Text="{Binding MiddleLamp2, UpdateSourceTrigger=LostFocus}" Width="80"/>
<Label Grid.Row="1" Grid.Column="2" Content="中灯6"/>
<TextBox IsReadOnly="True" Name="txtMiddleLamp6" Grid.Row="1" Grid.Column="3" Text="{Binding MiddleLamp6, UpdateSourceTrigger=LostFocus}" Width="80"/>
<!-- 第3行 -->
<Label Grid.Row="2" Grid.Column="0" Content="中灯3"/>
<TextBox IsReadOnly="True" Name="txtMiddleLamp3" Grid.Row="2" Grid.Column="1" Text="{Binding MiddleLamp3, UpdateSourceTrigger=LostFocus}" Width="80"/>
<Label Grid.Row="2" Grid.Column="2" Content="中灯7"/>
<TextBox IsReadOnly="True" Name="txtMiddleLamp7" Grid.Row="2" Grid.Column="3" Text="{Binding MiddleLamp7, UpdateSourceTrigger=LostFocus}" Width="80"/>
<!-- 第4行 -->
<Label Grid.Row="3" Grid.Column="0" Content="中灯4"/>
<TextBox IsReadOnly="True" Name="txtMiddleLamp4" Grid.Row="3" Grid.Column="1" Text="{Binding MiddleLamp4, UpdateSourceTrigger=LostFocus}" Width="80"/>
</Grid>
</GroupBox>
</StackPanel>
</ScrollViewer>
<!-- 右侧控制区 -->
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<!-- 电机状态 -->
<GroupBox Header="⚙️ 电机状态">
<StackPanel Margin="5">
<Label Content="电机限位" FontWeight="SemiBold"/>
<TextBox x:Name="txtMotorLimit" Text="{Binding MotorLimit, UpdateSourceTrigger=LostFocus}" Height="28" Margin="0,0,0,12"/>
<Label Content="复位补偿" FontWeight="SemiBold"/>
<TextBox x:Name="txtResetCompensation" Text="{Binding ResetCompensation, UpdateSourceTrigger=LostFocus}" Height="28" Margin="0,0,0,12"/>
<!--<Button Content="💾 保存参数" Command="{Binding SaveParametersCommand}" Margin="0,5" Background="#4CAF50"/>
<Button Content="🔄 读取参数" Command="{Binding LoadParametersCommand}" Margin="0,5" Background="#FF9800"/>-->
</StackPanel>
</GroupBox>
<!-- 控制按钮组 -->
<GroupBox Header="🎮 设备控制">
<WrapPanel Margin="5">
<Button Content="🔄 复位" Command="{Binding ResetCommand}" Width="80" Background="#607D8B"/>
<Button Content="👁️ 左眼开" Command="{Binding OpenLeftEyeCommand}" Width="80" Background="#4CAF50"/>
<Button Content="↩️ 反转" Command="{Binding ReverseCommand}" Width="80" Background="#FF9800"/>
<Button Content="👁️ 右眼开" Command="{Binding OpenRightEyeCommand}" Width="80" Background="#4CAF50"/>
<Button Content="↪️ 正转" Command="{Binding ForwardCommand}" Width="80" Background="#FF9800"/>
</WrapPanel>
</GroupBox>
<!-- 通信状态 -->
<!--<GroupBox Header="📡 通信状态">
<Border Background="#F8F9FA" CornerRadius="4" Padding="10" Margin="5">
<StackPanel>
<Ellipse Width="12" Height="12" Fill="{Binding StatusColor}" Margin="0,0,0,5" HorizontalAlignment="Left"/>
<TextBlock Text="{Binding CommunicationStatus}" FontWeight="Bold" Foreground="{Binding StatusColor}" TextWrapping="Wrap"/>
</StackPanel>
</Border>
</GroupBox>-->
</StackPanel>
</Grid>
<!-- 底部导航栏 -->
<Border Grid.Row="2" Background="White" CornerRadius="6" Padding="10" Margin="0,12,0,0" BorderBrush="#E9ECF0" BorderThickness="1">
<WrapPanel HorizontalAlignment="Center">
<Button Content="🏠 主页" Command="{Binding NavigateHomeCommand}" Width="100" Background="#607D8B"/>
<Button Content="🧪 测试界面" Command="{Binding NavigateTestCommand}" Width="100" Background="#2196F3"/>
<Button Content="📊 数据记录" Command="{Binding NavigateDataRecordCommand}" Width="100" Background="#4CAF50"/>
<Button Content="📸 记录画面" Command="{Binding NavigateRecordScreenCommand}" Width="100" Background="#FF9800"/>
</WrapPanel>
</Border>
</Grid>
</Window>

View File

@@ -1,186 +1,18 @@
using MembranePoreTester.Communication;
using MembranePoreTester.ViewModels;
using System;
using MembranePoreTester.ViewModels;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace MembranePoreTester.Views
namespace MembranePoreTester
{
public partial class ParameterWindow : Window
{
private readonly IPlcService _plcService;
private readonly PlcConfiguration _config;
private DispatcherTimer _autoRefreshTimer;
private bool _isEditing = false; // 用户是否正在编辑任何文本框
private ParameterViewModel _viewModel;
public ParameterWindow()
{
InitializeComponent();
_plcService = App.PlcService;
_config = App.PlcConfig;
// 为所有文本框注册焦点事件在XAML中设置事件或在此处统一查找
RegisterTextBoxEvents();
Loaded += async (s, e) =>
{
// 启动自动刷新定时器每秒1次
_autoRefreshTimer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(1)
};
_autoRefreshTimer.Tick += AutoRefreshTimer_Tick;
_autoRefreshTimer.Start();
// 首次加载时读取一次
await ReadParametersAsync();
};
Closed += (s, e) => _autoRefreshTimer?.Stop();
}
/// <summary>
/// 为所有文本框注册焦点事件,用于检测编辑状态
/// </summary>
private void RegisterTextBoxEvents()
{
// 查找当前窗口中所有文本框(可根据实际布局调整)
var textBoxes = FindVisualChildren<System.Windows.Controls.TextBox>(this);
foreach (var tb in textBoxes)
{
tb.GotFocus += (s, e) => _isEditing = true;
tb.LostFocus += (s, e) => _isEditing = false;
}
}
/// <summary>
/// 定时器事件:如果用户未在编辑,则刷新参数
/// </summary>
private async void AutoRefreshTimer_Tick(object sender, EventArgs e)
{
if (!_isEditing)
{
await ReadParametersAsync();
}
}
private async Task ReadParametersAsync()
{
try
{
// 加压控制
txtPressureUpper.Text = (await ReadFloatAsync(_config.PressureUpperLimit)).ToString("F3");
txtPressureRate.Text = (await ReadFloatAsync(_config.PressureRate)).ToString("F3");
txtPressureCoeff.Text = (await ReadFloatAsync(_config.PressureCoeff)).ToString("F3");
// 高压/低压系数
txtHPCoeff1.Text = (await ReadFloatAsync(_config.HPCoeff1)).ToString("F3");
txtLPCoeff1.Text = (await ReadFloatAsync(_config.LPCoeff1)).ToString("F3");
txtHPCoeff2.Text = (await ReadFloatAsync(_config.HPCoeff2)).ToString("F3");
txtLPCoeff2.Text = (await ReadFloatAsync(_config.LPCoeff2)).ToString("F3");
txtHPCoeff3.Text = (await ReadFloatAsync(_config.HPCoeff3)).ToString("F3");
txtLPCoeff3.Text = (await ReadFloatAsync(_config.LPCoeff3)).ToString("F3");
// 流量系数
txtLargeFlow1.Text = (await ReadFloatAsync(_config.LargeFlowCoeff1)).ToString("F3");
txtSmallFlow1.Text = (await ReadFloatAsync(_config.SmallFlowCoeff1)).ToString("F3");
txtLargeFlow2.Text = (await ReadFloatAsync(_config.LargeFlowCoeff2)).ToString("F3");
txtSmallFlow2.Text = (await ReadFloatAsync(_config.SmallFlowCoeff2)).ToString("F3");
txtLargeFlow3.Text = (await ReadFloatAsync(_config.LargeFlowCoeff3)).ToString("F3");
txtSmallFlow3.Text = (await ReadFloatAsync(_config.SmallFlowCoeff3)).ToString("F3");
// 校准参数
txtPressureZero.Text = (await ReadFloatAsync(_config.PressureCalibZero)).ToString("F3");
txtPressureSpan.Text = (await ReadFloatAsync(_config.PressureCalibSpan)).ToString("F3");
txtFlowZero.Text = (await ReadFloatAsync(_config.FlowCalibZero)).ToString("F3");
txtFlowSpan.Text = (await ReadFloatAsync(_config.FlowCalibSpan)).ToString("F3");
}
catch (Exception ex)
{
// 静默处理,避免频繁弹窗干扰(可记录日志)
System.Diagnostics.Debug.WriteLine($"自动读取参数失败: {ex.Message}");
}
}
private async void WriteParameters_Click(object sender, RoutedEventArgs e)
{
try
{
// 加压控制
await WriteFloatAsync(_config.PressureUpperLimit, ParseFloat(txtPressureUpper.Text));
await WriteFloatAsync(_config.PressureRate, ParseFloat(txtPressureRate.Text));
await WriteFloatAsync(_config.PressureCoeff, ParseFloat(txtPressureCoeff.Text));
// 高压/低压系数
await WriteFloatAsync(_config.HPCoeff1, ParseFloat(txtHPCoeff1.Text));
await WriteFloatAsync(_config.LPCoeff1, ParseFloat(txtLPCoeff1.Text));
await WriteFloatAsync(_config.HPCoeff2, ParseFloat(txtHPCoeff2.Text));
await WriteFloatAsync(_config.LPCoeff2, ParseFloat(txtLPCoeff2.Text));
await WriteFloatAsync(_config.HPCoeff3, ParseFloat(txtHPCoeff3.Text));
await WriteFloatAsync(_config.LPCoeff3, ParseFloat(txtLPCoeff3.Text));
// 流量系数
await WriteFloatAsync(_config.LargeFlowCoeff1, ParseFloat(txtLargeFlow1.Text));
await WriteFloatAsync(_config.SmallFlowCoeff1, ParseFloat(txtSmallFlow1.Text));
await WriteFloatAsync(_config.LargeFlowCoeff2, ParseFloat(txtLargeFlow2.Text));
await WriteFloatAsync(_config.SmallFlowCoeff2, ParseFloat(txtSmallFlow2.Text));
await WriteFloatAsync(_config.LargeFlowCoeff3, ParseFloat(txtLargeFlow3.Text));
await WriteFloatAsync(_config.SmallFlowCoeff3, ParseFloat(txtSmallFlow3.Text));
// 校准参数
await WriteFloatAsync(_config.PressureCalibZero, ParseFloat(txtPressureZero.Text));
await WriteFloatAsync(_config.PressureCalibSpan, ParseFloat(txtPressureSpan.Text));
await WriteFloatAsync(_config.FlowCalibZero, ParseFloat(txtFlowZero.Text));
await WriteFloatAsync(_config.FlowCalibSpan, ParseFloat(txtFlowSpan.Text));
MessageBox.Show("参数写入成功");
}
catch (Exception ex)
{
MessageBox.Show($"写入参数失败: {ex.Message}");
}
}
private float ParseFloat(string text) => float.TryParse(text, out var val) ? val : 0;
private async Task<float> ReadFloatAsync(ushort address)
{
var registers = await ((ModbusTcpPlcService)_plcService).ReadHoldingRegistersAsync(address, 2);
byte[] bytes = new byte[4];
bytes[0] = (byte)(registers[0] >> 8);
bytes[1] = (byte)(registers[0] & 0xFF);
bytes[2] = (byte)(registers[1] >> 8);
bytes[3] = (byte)(registers[1] & 0xFF);
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
return BitConverter.ToSingle(bytes, 0);
}
private async Task WriteFloatAsync(ushort address, float value)
{
byte[] bytes = BitConverter.GetBytes(value);
if (BitConverter.IsLittleEndian) Array.Reverse(bytes);
ushort high = (ushort)((bytes[0] << 8) | bytes[1]);
ushort low = (ushort)((bytes[2] << 8) | bytes[3]);
await ((ModbusTcpPlcService)_plcService).WriteRegisterAsync(address, high);
await ((ModbusTcpPlcService)_plcService).WriteRegisterAsync((ushort)(address + 1), low);
}
private void Close_Click(object sender, RoutedEventArgs e) => Close();
/// <summary>
/// 辅助方法:查找视觉树中的所有指定类型子元素
/// </summary>
private static IEnumerable<T> FindVisualChildren<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj == null) yield break;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
if (child is T t) yield return t;
foreach (var childOfChild in FindVisualChildren<T>(child)) yield return childOfChild;
}
_viewModel = new ParameterViewModel();
DataContext = _viewModel;
Closed += (s, e) => _viewModel.Dispose();
}
}
}

View File

@@ -1,140 +0,0 @@
<UserControl x:Class="MembranePoreTester.Views.PoreDistributionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:oxy="http://oxyplot.org/wpf"
xmlns:conv="clr-namespace:MembranePoreTester.Converters">
<UserControl.Resources>
<conv:InverseBooleanConverter x:Key="InverseBooleanConverter"/>
</UserControl.Resources>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<GroupBox Grid.Row="0" Header="样品信息" Margin="0,0,0,10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<!-- 新增第4行 -->
</Grid.RowDefinitions>
<!-- 第0行 -->
<Label Grid.Row="0" Grid.Column="0" Content="膜类型:"/>
<ComboBox Grid.Row="0" Grid.Column="1" ItemsSource="{Binding MembraneTypes}"
SelectedItem="{Binding Record.SampleType}" Margin="5"/>
<Label Grid.Row="0" Grid.Column="2" Content="规格:"/>
<TextBox Grid.Row="0" Grid.Column="3" Text="{Binding Record.SampleSpec}" Margin="5"/>
<!-- 第1行 -->
<Label Grid.Row="1" Grid.Column="0" Content="室温(°C):"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding Record.RoomTemperature}" Margin="5"/>
<Label Grid.Row="1" Grid.Column="2" Content="浸润时间(h):"/>
<TextBox Grid.Row="1" Grid.Column="3" Text="{Binding Record.SoakingTime}" Margin="5"/>
<!-- 第2行 -->
<Label Grid.Row="2" Grid.Column="0" Content="测试液体:"/>
<ComboBox Grid.Row="2" Grid.Column="1" ItemsSource="{Binding Liquids}"
SelectedItem="{Binding SelectedLiquid}" DisplayMemberPath="Name" Margin="5"/>
<Label Grid.Row="2" Grid.Column="2" Content="生产厂家:"/>
<TextBox Grid.Row="2" Grid.Column="3" Text="{Binding Record.LiquidManufacturer}" Margin="5"/>
<!-- 第3行压力单位 -->
<Label Grid.Row="3" Grid.Column="0" Content="压力单位:"/>
<ComboBox Grid.Row="3" Grid.Column="1" ItemsSource="{Binding PressureUnits}"
SelectedItem="{Binding Record.PressureUnit}" Margin="5"/>
</Grid>
</GroupBox>
<!-- 数据表格和简单图表此处用ListView代替图表 -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 左侧数据输入表格 -->
<GroupBox Grid.Column="0" Header="压力-流量数据" Margin="0,0,5,0">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="0,0,0,5">
<Button Content="添加行" Command="{Binding AddDataPointCommand}" Padding="10,5" Margin="5"/>
<Button Content="删除行" Command="{Binding RemoveDataPointCommand}" Padding="10,5" Margin="5"/>
<!-- 新增:模式选择 -->
<ComboBox SelectedItem="{Binding TestMode}" Width="80" Margin="5">
<ComboBoxItem Content="湿膜" IsSelected="True"/>
<ComboBoxItem Content="干膜"/>
</ComboBox>
<ComboBox SelectedIndex="{Binding SelectedFlowModeIndex}" Width="100" Margin="5">
<ComboBoxItem IsSelected="True" Content="大流量"/>
<ComboBoxItem Content="小流量"/>
</ComboBox>
<Button Content="读取PLC" Command="{Binding ReadPlcCommand}" Padding="10,5" Margin="5"/>
</StackPanel>
<DataGrid ItemsSource="{Binding Record.DataPoints}" AutoGenerateColumns="False" CanUserAddRows="False">
<DataGrid.Columns>
<DataGridTextColumn Header="压力" Binding="{Binding Pressure, UpdateSourceTrigger=PropertyChanged}" Width="*"/>
<DataGridTextColumn Header="湿膜流量(L/min)" Binding="{Binding WetFlow, UpdateSourceTrigger=PropertyChanged}" Width="*"/>
<DataGridTextColumn Header="干膜流量(L/min)" Binding="{Binding DryFlow, UpdateSourceTrigger=PropertyChanged}" Width="*"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</GroupBox>
<GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Center"/>
<!-- 右侧简易数据展示可用图表库此处仅用ListBox预览 -->
<GroupBox Grid.Column="2" Header="数据预览" Margin="5,0,0,0">
<!--<ListBox ItemsSource="{Binding Record.DataPoints}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Pressure, StringFormat=P:{0:F2}}"/>
<TextBlock Text=" "/>
<TextBlock Text="{Binding WetFlow, StringFormat=W:{0:F3}}"/>
<TextBlock Text=" "/>
<TextBlock Text="{Binding DryFlow, StringFormat=D:{0:F3}}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>-->
<GroupBox Grid.Column="2" Header="流量-压力曲线" Margin="5,0,0,0">
<oxy:PlotView Model="{Binding PlotModel}" Height="300" Margin="5"/>
</GroupBox>
</GroupBox>
</Grid>
<!-- 计算结果区域 -->
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Stretch" Margin="0,10,0,0">
<GroupBox Header="平均孔径" Margin="5">
<TextBlock Text="{Binding AveragePoreSize, StringFormat={}{0:F3} μm}" FontSize="16"/>
</GroupBox>
<GroupBox Header="孔分布区间计算" Margin="5">
<StackPanel Orientation="Horizontal">
<TextBox Text="{Binding LowerPore}" Width="60" Margin="5"/>
<TextBlock Text="~" VerticalAlignment="Center"/>
<TextBox Text="{Binding UpperPore}" Width="60" Margin="5"/>
<TextBlock Text="μm" VerticalAlignment="Center"/>
<Button Content="计算" Command="{Binding CalculateCommand}" Margin="10,0" Padding="10,5"/>
<TextBlock Text="{Binding RangePercentage, StringFormat={}{0:F1}%}" FontSize="16" VerticalAlignment="Center" Margin="10,0"/>
</StackPanel>
</GroupBox>
<Button Content="流量校准" Command="{Binding OpenFlowCalibCommand}" Padding="15,8" Margin="5"/>
<Button Content="生成报告" Command="{Binding GenerateReportCommand}" Padding="15,8" Margin="5"/>
<Button Content="保存到历史" Command="{Binding SaveCommand}" Padding="15,8" Margin="5"/>
<Button Content="导出Excel" Command="{Binding ExportCommand}" Padding="15,8" Margin="5"/>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,13 +0,0 @@
using System.Windows.Controls;
namespace MembranePoreTester.Views
{
public partial class PoreDistributionView : UserControl
{
public PoreDistributionView()
{
InitializeComponent();
//DataContext = new ViewModels.PoreDistributionViewModel();
}
}
}

245
Views/TestView.xaml Normal file
View File

@@ -0,0 +1,245 @@
<Window x:Class="MembranePoreTester.TestView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="测试界面"
Width="1024" MinHeight="768" WindowState="Maximized"
WindowStartupLocation="CenterOwner"
Background="#F5F7FA" FontFamily="Segoe UI" KeyDown="Window_KeyDown">
<Window.Resources>
<!-- 统一按钮样式(与 MainWindow 一致) -->
<Style TargetType="Button">
<Setter Property="Background" Value="#2196F3"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="4" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#1976D2"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#0D47A1"/>
</Trigger>
</Style.Triggers>
</Style>
<!-- 统一文本框样式 -->
<Style TargetType="TextBox">
<Setter Property="Height" Value="28"/>
<Setter Property="Margin" Value="5"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#D0D3D9"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="5,2"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<!-- 只读文本框样式 -->
<Style x:Key="ReadOnlyTextBox" TargetType="TextBox" BasedOn="{StaticResource {x:Type TextBox}}">
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="Background" Value="#F5F5F5"/>
<Setter Property="Foreground" Value="#2196F3"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<!-- 统一标签样式 -->
<Style TargetType="Label">
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="#2C3E50"/>
<Setter Property="Margin" Value="5"/>
</Style>
<!-- 统一GroupBox样式 -->
<Style TargetType="GroupBox">
<Setter Property="Background" Value="White"/>
<Setter Property="BorderBrush" Value="#E9ECF0"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Margin" Value="0,0,0,12"/>
<Setter Property="Padding" Value="12"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupBox">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="6">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{TemplateBinding Header}" FontWeight="SemiBold" FontSize="14" Margin="0,0,0,12"/>
<ContentPresenter Grid.Row="1" Content="{TemplateBinding Content}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 统一 RadioButton 样式 -->
<Style TargetType="RadioButton">
<Setter Property="FontSize" Value="12"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Margin" Value="5"/>
</Style>
</Window.Resources>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<Grid Margin="12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 标题栏(蓝底卡片) -->
<Border Grid.Row="0" Background="#2196F3" CornerRadius="6" Padding="12" Margin="0,0,0,12">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="测试界面" FontSize="20" FontWeight="Bold" Foreground="White" VerticalAlignment="Center"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock Text="当前模式:" Foreground="White" FontSize="14" VerticalAlignment="Center" Margin="0,0,10,0"/>
<Border Background="White" CornerRadius="20" Padding="12,4">
<TextBlock Text="{Binding CurrentMode}" Foreground="#2196F3" FontSize="14" FontWeight="Bold"/>
</Border>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" >
<RadioButton Content="空白模式" IsChecked="{Binding IsBlankMode}" GroupName="TestMode" Foreground="White" FontSize="13"/>
<RadioButton Content="试样测试" IsChecked="{Binding IsSampleMode}" GroupName="TestMode" Foreground="White" FontSize="13" Margin="10,5,0,0"/>
</StackPanel>
</Grid>
</Border>
<!-- 参数设置区域 -->
<GroupBox Grid.Row="1" Header="⚙️ 参数设置" Margin="0,0,0,12">
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Horizontal" HorizontalAlignment="Center">
<Label Content="分辨角度:" Margin="0,0,5,0"/>
<TextBox Text="{Binding ResolutionAngle, UpdateSourceTrigger=PropertyChanged}" Width="80"/>
<Label Content="°" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center">
<Label Content="转动速度:" Margin="0,0,5,0"/>
<TextBox Text="{Binding RotationSpeed, UpdateSourceTrigger=PropertyChanged}" Width="80"/>
<Label Content="°/s" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" HorizontalAlignment="Center">
<Label Content="当前角度:" Margin="0,0,5,0"/>
<TextBox Text="{Binding CurrentAngle, UpdateSourceTrigger=PropertyChanged}" Width="80" Background="#F5F5F5" Foreground="#2196F3" FontWeight="Bold"/>
<Label Content="°" Margin="5,0,0,0"/>
</StackPanel>
</Grid>
</GroupBox>
<!-- 视野面积数据区域 -->
<GroupBox Grid.Row="2" Header="👁️ 视野面积数据" Margin="0,0,0,12">
<Grid Margin="5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" Margin="5">
<Label Content="左目视野面积:" Width="110"/>
<TextBox Text="{Binding LeftEyeArea}" Width="100" IsReadOnly="True" Background="#F5F5F5" Foreground="#2196F3" FontWeight="Bold"/>
<Label Content="cm²" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal" Margin="5">
<Label Content="右目视野面积:" Width="110"/>
<TextBox Text="{Binding RightEyeArea}" Width="100" IsReadOnly="True" Background="#F5F5F5" Foreground="#2196F3" FontWeight="Bold"/>
<Label Content="cm²" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" Margin="5">
<Label Content="双目视野面积:" Width="110"/>
<TextBox Text="{Binding BinocularArea}" Width="100" IsReadOnly="True" Background="#F5F5F5" Foreground="#2196F3" FontWeight="Bold"/>
<Label Content="cm²" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" Margin="5">
<Label Content="下方视野:" Width="110"/>
<TextBox Text="{Binding LowerField}" Width="100" IsReadOnly="True" Background="#F5F5F5" Foreground="#2196F3" FontWeight="Bold"/>
<Label Content="°" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="0" Orientation="Horizontal" Margin="5">
<Label Content="空白视野面积:" Width="110"/>
<TextBox Text="{Binding BlankArea}" Width="100" IsReadOnly="True" Background="#F5F5F5" Foreground="#2196F3" FontWeight="Bold"/>
<Label Content="cm²" Margin="5,0,0,0"/>
</StackPanel>
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" Margin="5">
<Label Content="视野保存率:" Width="110"/>
<TextBox Text="{Binding FieldRetentionRate}" Width="100" IsReadOnly="True" Background="#F5F5F5" Foreground="#2196F3" FontWeight="Bold"/>
<Label Content="%" Margin="5,0,0,0"/>
</StackPanel>
</Grid>
</GroupBox>
<!-- 控制按钮区域 -->
<GroupBox Grid.Row="3" Header="🎮 设备控制" Margin="0,0,0,12">
<WrapPanel HorizontalAlignment="Center" Margin="5">
<Button Content="🔄 复位" Command="{Binding ResetCommand}" Background="#607D8B"/>
<Button Content="👁️ 左眼开" Command="{Binding OpenLeftEyeCommand}" Background="#4CAF50"/>
<Button Content="↩️ 反转" Command="{Binding ReverseCommand}" Background="#FF9800"/>
<Button Content="👁️ 右眼开" Command="{Binding OpenRightEyeCommand}" Background="#4CAF50"/>
<Button Content="↪️ 正转" Command="{Binding ForwardCommand}" Background="#FF9800"/>
<Button Content="▶️ 测试" Command="{Binding StartTestCommand}" Background="#2196F3"/>
<Button Content="⏹️ 停止" Command="{Binding StopTestCommand}" Background="#F44336"/>
</WrapPanel>
</GroupBox>
<!-- 底部导航栏 -->
<Border Grid.Row="4" Background="White" CornerRadius="6" Padding="10" Margin="0,12,0,0" BorderBrush="#E9ECF0" BorderThickness="1">
<WrapPanel HorizontalAlignment="Center">
<Button Content="🏠 主页" Command="{Binding NavigateHomeCommand}" Width="100" Background="#607D8B"/>
<Button Content="🧪 测试界面" Command="{Binding NavigateTestCommand}" Width="100" Background="#2196F3"/>
<Button Content="📊 数据记录" Command="{Binding NavigateDataRecordCommand}" Width="100" Background="#4CAF50"/>
<Button Content="📸 记录画面" Command="{Binding NavigateRecordScreenCommand}" Width="100" Background="#FF9800"/>
</WrapPanel>
</Border>
</Grid>
</ScrollViewer>
</Window>

29
Views/TestView.xaml.cs Normal file
View File

@@ -0,0 +1,29 @@
using MembranePoreTester.ViewModels;
using System.Windows;
using System.Windows.Input;
namespace MembranePoreTester
{
/// <summary>
/// TestView.xaml 的交互逻辑
/// </summary>
public partial class TestView : Window
{
public TestView()
{
InitializeComponent();
// 如果需要在打开时自动开始模拟,可以在此设置 DataContext
DataContext = new TestViewModel();
}
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (Keyboard.Modifiers == ModifierKeys.Control && e.Key == Key.P)
{
var win = new ParameterWindow();
win.Owner = this;
win.ShowDialog();
}
}
}
}

View File

@@ -3,55 +3,6 @@
"IpAddress": "192.168.1.10",
"Port": 502,
"SlaveId": 1,
"PressureRegisterStation1": 6,
"PressureRegisterStation2": 8,
"PressureRegisterStation3": 10,
"PressureModeRegister": 200, // 高压/低压选择寄存器0=低压1=高压)
"PressCoil": 100, // 加压线圈M100
"StartCoil": 20, // 启动线圈M20
"EnableCoil": 21, // 使能线圈M21
"StopCoil": 7, // 停止线圈M7
"PressureFactor": 1.0,
"WetFlowRegister": 2,
"DryFlowRegister": 4,
"WetFlowFactor": 1.0,
"DryFlowFactor": 1.0,
"PressureUpperLimit": 300,
"PressureRate": 280,
"PressureCoeff": 282,
"HPCoeff1": 3120,
"LPCoeff1": 3122,
"HPCoeff2": 3124,
"LPCoeff2": 3126,
"HPCoeff3": 3128,
"LPCoeff3": 3130,
"LargeFlowCoeff1": 3048,
"SmallFlowCoeff1": 380,
"LargeFlowCoeff2": 1218,
"SmallFlowCoeff2": 1318,
"LargeFlowCoeff3": 1418,
"SmallFlowCoeff3": 1468,
"FlowModeRegister1": 5,
"FlowModeRegister2": 6,
"FlowModeRegister3": 7,
"PressureCalibZero": 3200,
"PressureCalibSpan": 3202,
"FlowCalibZero": 3204,
"FlowCalibSpan": 3206
}

View File

@@ -27,4 +27,8 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Report\" />
</ItemGroup>
</Project>