周立功的CAN /FD实现
This commit is contained in:
@@ -198,6 +198,7 @@ namespace CapMachine.Wpf
|
||||
containerRegistry.RegisterDialog<DialogExpInfoView, DialogExpInfoViewModel>();
|
||||
containerRegistry.RegisterDialog<DialogUserView, DialogUserViewModel>();
|
||||
containerRegistry.RegisterDialog<DialogCanRwConfigView, DialogCanRwConfigViewModel>();
|
||||
containerRegistry.RegisterDialog<DialogZlgCanLinRwConfigView, DialogZlgCanLinRwConfigViewModel>();
|
||||
containerRegistry.RegisterDialog<DialogCanLinConfigCreateView, DialogCanLinConfigCreateViewModel>();
|
||||
containerRegistry.RegisterDialog<DialogPIDConfigView, DialogPIDConfigViewModel>();
|
||||
containerRegistry.RegisterDialog<DialogLimitConfigView, DialogLimitConfigViewModel>();
|
||||
|
||||
@@ -88,10 +88,10 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
|
||||
#endregion
|
||||
|
||||
#region 函数
|
||||
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
|
||||
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
|
||||
public static extern IntPtr ZCAN_OpenDevice(uint device_type, uint device_index, uint reserved);
|
||||
|
||||
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
|
||||
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true)]
|
||||
public static extern IntPtr ZCAN_OpenDeviceByName(uint device_type, string name);
|
||||
|
||||
[DllImport("zlgcan.dll", CallingConvention = CallingConvention.StdCall)]
|
||||
|
||||
@@ -2,8 +2,10 @@ using CapMachine.Wpf.CanDrive;
|
||||
using CapMachine.Wpf.Services;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -21,6 +23,9 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
|
||||
private readonly object _sync = new object();
|
||||
private readonly ILogService _log;
|
||||
|
||||
private static IntPtr _preloadedZlgcanHandle = IntPtr.Zero;
|
||||
private static string? _preloadedZlgcanError;
|
||||
|
||||
private IntPtr _deviceHandle = IntPtr.Zero;
|
||||
private readonly IntPtr[] _canChannelHandles = new IntPtr[2];
|
||||
private IntPtr _linChannelHandle = IntPtr.Zero;
|
||||
@@ -189,16 +194,189 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
|
||||
|
||||
EnsureNativeDllExists("zlgcan.dll");
|
||||
|
||||
_deviceHandle = ZLGCAN.ZCAN_OpenDevice(ZLGCAN.ZCAN_USBCANFD_200U, deviceIndex, 0);
|
||||
var deviceType = ZLGCAN.ZCAN_USBCANFD_200U;
|
||||
|
||||
try
|
||||
{
|
||||
_deviceHandle = ZLGCAN.ZCAN_OpenDevice(deviceType, deviceIndex, 0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
var dllFullPath = Path.Combine(baseDir, "zlgcan.dll");
|
||||
var msg = $"ZCAN_OpenDevice 调用异常。deviceType={deviceType}, deviceIndex={deviceIndex}, " +
|
||||
$"Is64BitProcess={Environment.Is64BitProcess}, ProcessArch={RuntimeInformation.ProcessArchitecture}, " +
|
||||
$"DllPath={dllFullPath}, BaseDir={baseDir}。异常:{ex.Message}";
|
||||
_log.Error(msg);
|
||||
throw new InvalidOperationException(msg, ex);
|
||||
}
|
||||
|
||||
if (_deviceHandle == IntPtr.Zero)
|
||||
{
|
||||
throw new InvalidOperationException("ZCAN_OpenDevice 失败,请确认驱动/设备连接/程序位数与 DLL 匹配。");
|
||||
var lastError = Marshal.GetLastWin32Error();
|
||||
var lastErrorMsg = string.Empty;
|
||||
try
|
||||
{
|
||||
lastErrorMsg = new Win32Exception(lastError).Message;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var baseDir = AppContext.BaseDirectory;
|
||||
var dllFullPath = Path.Combine(baseDir, "zlgcan.dll");
|
||||
var loadedModulePath = GetLoadedModulePath("zlgcan.dll") ?? string.Empty;
|
||||
var depDiag = string.Join(" | ", new[]
|
||||
{
|
||||
BuildDllDiag(baseDir, "zlgcan.dll"),
|
||||
BuildDllDiag(baseDir, "USB2XXX.dll"),
|
||||
BuildDllDiag(baseDir, "libusb-1.0.dll"),
|
||||
BuildDllDiag(baseDir, "zdbc.dll"),
|
||||
BuildDllDiag(baseDir, "kerneldlls\\USBCANFD.dll"),
|
||||
BuildDllDiag(baseDir, "kerneldlls\\CANDevCore.dll"),
|
||||
BuildDllDiag(baseDir, "kerneldlls\\CANDevice.dll"),
|
||||
BuildDllDiag(baseDir, "kerneldlls\\CANFDCOM.dll"),
|
||||
BuildDllDiag(baseDir, "kerneldlls\\CANFDNET.dll"),
|
||||
BuildDllDiag(baseDir, "kerneldlls\\ZPSCANFD.dll"),
|
||||
BuildDllDiag(baseDir, "kerneldlls\\USBCANFD800U.dll"),
|
||||
BuildDllDiag(baseDir, "kerneldlls\\USBCAN.dll"),
|
||||
BuildDllDiag(baseDir, "kerneldlls\\usbcan.dll"),
|
||||
}.Where(s => !string.IsNullOrWhiteSpace(s)));
|
||||
string dllVer = string.Empty;
|
||||
try
|
||||
{
|
||||
if (File.Exists(dllFullPath))
|
||||
{
|
||||
var vi = FileVersionInfo.GetVersionInfo(dllFullPath);
|
||||
dllVer = vi?.FileVersion ?? string.Empty;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var msg = $"ZCAN_OpenDevice 失败。deviceType={deviceType}, deviceIndex={deviceIndex}。" +
|
||||
$"Win32LastError={lastError}(0x{lastError:X}){(string.IsNullOrWhiteSpace(lastErrorMsg) ? string.Empty : $"({lastErrorMsg})")}。" +
|
||||
$"Is64BitProcess={Environment.Is64BitProcess}, ProcessArch={RuntimeInformation.ProcessArchitecture}。" +
|
||||
$"DllPath={dllFullPath}, DllVersion={dllVer}, LoadedModulePath={loadedModulePath}, BaseDir={baseDir}。" +
|
||||
$"PreloadError={_preloadedZlgcanError ?? string.Empty}。" +
|
||||
$"DirDllDiag={depDiag}。" +
|
||||
"请确认:1) 已安装ZLG驱动;2) 设备已连接且未被其它程序占用;3) 程序位数与 zlgcan.dll 匹配;4) 设备类型/索引正确。";
|
||||
|
||||
_log.Error(msg);
|
||||
throw new InvalidOperationException(msg);
|
||||
}
|
||||
|
||||
OpenState = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 预加载指定路径的原生 DLL,尽量避免被 PATH 中的同名 DLL 干扰。
|
||||
/// </summary>
|
||||
/// <param name="dllFullPath">DLL 完整路径。</param>
|
||||
private static void TryPreloadNativeDll(string dllFullPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_preloadedZlgcanHandle != IntPtr.Zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(_preloadedZlgcanError))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_preloadedZlgcanHandle = NativeLibrary.Load(dllFullPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_preloadedZlgcanError = $"{ex.GetType().Name}: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构造输出目录下某个 DLL 的诊断信息(架构/大小/版本)。
|
||||
/// </summary>
|
||||
/// <param name="baseDir">输出目录。</param>
|
||||
/// <param name="fileName">DLL 文件名。</param>
|
||||
/// <returns>诊断信息字符串;不存在返回 "xxx:not_found";异常返回 null。</returns>
|
||||
private static string? BuildDllDiag(string baseDir, string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(baseDir) || string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var full = Path.Combine(baseDir, fileName);
|
||||
if (!File.Exists(full))
|
||||
{
|
||||
return $"{fileName}:not_found";
|
||||
}
|
||||
|
||||
var arch = TryGetPeArchitecture(full) ?? string.Empty;
|
||||
long size = 0;
|
||||
try
|
||||
{
|
||||
size = new FileInfo(full).Length;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var ver = string.Empty;
|
||||
try
|
||||
{
|
||||
var vi = FileVersionInfo.GetVersionInfo(full);
|
||||
ver = vi?.FileVersion ?? string.Empty;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return $"{fileName}:arch={arch},size={size},ver={ver}";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前进程中已加载模块的完整路径。
|
||||
/// </summary>
|
||||
/// <param name="moduleFileName">模块文件名(如 zlgcan.dll)。</param>
|
||||
/// <returns>完整路径;获取失败返回 null。</returns>
|
||||
private static string? GetLoadedModulePath(string moduleFileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(moduleFileName))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
using var p = Process.GetCurrentProcess();
|
||||
foreach (ProcessModule m in p.Modules)
|
||||
{
|
||||
if (string.Equals(m.ModuleName, moduleFileName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return m.FileName;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打开设备并初始化 CANFD 通道。
|
||||
/// </summary>
|
||||
@@ -1286,6 +1464,78 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
|
||||
{
|
||||
throw new FileNotFoundException($"未找到 {dllName},请将其复制到程序输出目录:{baseDir}", full);
|
||||
}
|
||||
|
||||
TryPreloadNativeDll(full);
|
||||
if (_preloadedZlgcanHandle == IntPtr.Zero && !string.IsNullOrWhiteSpace(_preloadedZlgcanError))
|
||||
{
|
||||
throw new InvalidOperationException($"预加载 {dllName} 失败:{_preloadedZlgcanError}。DLL 路径:{full}");
|
||||
}
|
||||
|
||||
var dllArch = TryGetPeArchitecture(full);
|
||||
if (!string.IsNullOrWhiteSpace(dllArch))
|
||||
{
|
||||
var procArch = Environment.Is64BitProcess ? "x64" : "x86";
|
||||
if (Environment.Is64BitProcess && !string.Equals(dllArch, "x64", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x64 版本 DLL,或将程序改为 x86 运行。DLL 路径:{full}");
|
||||
}
|
||||
if (!Environment.Is64BitProcess && !string.Equals(dllArch, "x86", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new InvalidOperationException($"{dllName} 位数不匹配:当前进程为 {procArch},但 DLL 架构为 {dllArch}。请替换为 x86 版本 DLL,或将程序改为 x64 运行。DLL 路径:{full}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试读取 PE 头判断 DLL 架构。
|
||||
/// </summary>
|
||||
/// <param name="dllFullPath">DLL 完整路径。</param>
|
||||
/// <returns>返回 "x86"/"x64"/"arm64"/"unknown(...)";读取失败返回 null。</returns>
|
||||
private static string? TryGetPeArchitecture(string dllFullPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var fs = new FileStream(dllFullPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var br = new BinaryReader(fs);
|
||||
|
||||
if (fs.Length < 0x40)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var mz = br.ReadUInt16();
|
||||
if (mz != 0x5A4D)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
fs.Position = 0x3C;
|
||||
var peOffset = br.ReadInt32();
|
||||
if (peOffset <= 0 || peOffset > fs.Length - 6)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
fs.Position = peOffset;
|
||||
var peSig = br.ReadUInt32();
|
||||
if (peSig != 0x00004550)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var machine = br.ReadUInt16();
|
||||
return machine switch
|
||||
{
|
||||
0x014c => "x86",
|
||||
0x8664 => "x64",
|
||||
0xAA64 => "arm64",
|
||||
_ => $"unknown(0x{machine:X})"
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, CanDbcModel> BuildDbcModelIndex(IEnumerable<CanDbcModel> models)
|
||||
|
||||
@@ -79,6 +79,33 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
|
||||
throw new FileNotFoundException("DBC 文件不存在。", dbcPath);
|
||||
}
|
||||
|
||||
var fileSize = 0L;
|
||||
try
|
||||
{
|
||||
fileSize = new FileInfo(dbcPath).Length;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var fullPath = dbcPath;
|
||||
try
|
||||
{
|
||||
fullPath = Path.GetFullPath(dbcPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var shortPath = TryGetShortPath(fullPath);
|
||||
var candidatePaths = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(fullPath)) candidatePaths.Add(fullPath);
|
||||
if (!string.IsNullOrWhiteSpace(shortPath)
|
||||
&& !string.Equals(shortPath, fullPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
candidatePaths.Add(shortPath);
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (_dbcHandle == 0 || _dbcHandle == ZDBC.INVALID_DBC_HANDLE)
|
||||
@@ -91,26 +118,42 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
|
||||
}
|
||||
}
|
||||
|
||||
var fileInfo = new ZDBC.FileInfo
|
||||
var loadOk = false;
|
||||
foreach (var path in candidatePaths)
|
||||
{
|
||||
strFilePath = BuildFixedPathBytes(dbcPath, ZDBC._MAX_FILE_PATH_ + 1),
|
||||
type = protocolType,
|
||||
merge = (byte)(merge ? 1 : 0)
|
||||
};
|
||||
|
||||
IntPtr pFile = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.FileInfo)));
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(fileInfo, pFile, false);
|
||||
var ok = ZDBC.ZDBC_LoadFile(_dbcHandle, pFile);
|
||||
if (!ok)
|
||||
var fileInfo = new ZDBC.FileInfo
|
||||
{
|
||||
throw new InvalidOperationException("ZDBC_LoadFile 失败,请检查 DBC 文件格式。");
|
||||
strFilePath = BuildFixedPathBytes(path, ZDBC._MAX_FILE_PATH_ + 1),
|
||||
type = protocolType,
|
||||
merge = (byte)(merge ? 1 : 0)
|
||||
};
|
||||
|
||||
IntPtr pFile = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(ZDBC.FileInfo)));
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(fileInfo, pFile, false);
|
||||
loadOk = ZDBC.ZDBC_LoadFile(_dbcHandle, pFile);
|
||||
if (loadOk)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(pFile);
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
if (!loadOk)
|
||||
{
|
||||
Marshal.FreeHGlobal(pFile);
|
||||
// fallback:直接按内容加载,绕开路径编码问题;同时尝试多种编码以提升兼容性。
|
||||
loadOk = TryLoadContentFallback_NoLock(fullPath, merge);
|
||||
}
|
||||
|
||||
if (!loadOk)
|
||||
{
|
||||
var msg = $"ZDBC_LoadFile 失败。DbcPath={fullPath},ShortPath={shortPath ?? string.Empty},Size={fileSize}。请检查 DBC 文件格式,或将 DBC 放到纯英文路径后重试。";
|
||||
throw new InvalidOperationException(msg);
|
||||
}
|
||||
|
||||
RefreshMessageCache_NoLock();
|
||||
@@ -542,13 +585,128 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
|
||||
|
||||
private static byte[] BuildFixedPathBytes(string path, int fixedLen)
|
||||
{
|
||||
var bytes = Encoding.ASCII.GetBytes(path);
|
||||
var bytes = Encoding.Default.GetBytes(path);
|
||||
var dst = new byte[fixedLen];
|
||||
Array.Clear(dst, 0, dst.Length);
|
||||
Array.Copy(bytes, 0, dst, 0, Math.Min(bytes.Length, fixedLen - 1));
|
||||
return dst;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当 ZDBC_LoadFile 失败时的兜底加载策略:读取文件内容并调用 ZDBC_LoadContent。
|
||||
/// </summary>
|
||||
/// <param name="fullPath">DBC 完整路径。</param>
|
||||
/// <param name="merge">是否合并。</param>
|
||||
/// <returns>加载成功返回 true。</returns>
|
||||
private bool TryLoadContentFallback_NoLock(string fullPath, bool merge)
|
||||
{
|
||||
// zdbc.dll 入口为 ANSI 字符串,因此这里优先使用 ASCII(将非 ASCII 字符替换为 ?),
|
||||
// 避免中文注释/特殊字符导致原生库解析失败;同时需要兼容 UTF-16 BOM,否则用 ASCII 读会出现大量 \0 造成内容截断。
|
||||
|
||||
var encodings = new List<Encoding>();
|
||||
|
||||
try
|
||||
{
|
||||
var bytes = File.ReadAllBytes(fullPath);
|
||||
if (bytes.Length >= 2)
|
||||
{
|
||||
// UTF-16 LE BOM: FF FE
|
||||
if (bytes[0] == 0xFF && bytes[1] == 0xFE)
|
||||
{
|
||||
encodings.Add(Encoding.Unicode);
|
||||
}
|
||||
// UTF-16 BE BOM: FE FF
|
||||
else if (bytes[0] == 0xFE && bytes[1] == 0xFF)
|
||||
{
|
||||
encodings.Add(Encoding.BigEndianUnicode);
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes.Length >= 3)
|
||||
{
|
||||
// UTF-8 BOM: EF BB BF
|
||||
if (bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
|
||||
{
|
||||
encodings.Add(Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// 常规兜底编码序列
|
||||
encodings.Add(new UTF8Encoding(false, true));
|
||||
encodings.Add(Encoding.UTF8);
|
||||
encodings.Add(Encoding.Default);
|
||||
encodings.Add(Encoding.ASCII);
|
||||
|
||||
foreach (var enc in encodings.Distinct())
|
||||
{
|
||||
IntPtr pContent = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
var content = File.ReadAllText(fullPath, enc);
|
||||
pContent = Marshal.StringToHGlobalAnsi(content);
|
||||
var ok = ZDBC.ZDBC_LoadContent(_dbcHandle, pContent, merge ? 1u : 0u);
|
||||
if (ok)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pContent != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(pContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取 Windows 8.3 短路径。
|
||||
/// </summary>
|
||||
/// <param name="lpszLongPath">长路径。</param>
|
||||
/// <param name="lpszShortPath">短路径缓冲。</param>
|
||||
/// <param name="cchBuffer">缓冲区大小。</param>
|
||||
/// <returns>返回写入的字符数量;0 表示失败。</returns>
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern uint GetShortPathName(string lpszLongPath, StringBuilder lpszShortPath, uint cchBuffer);
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将长路径转换为短路径(8.3),用于兼容部分原生库对中文/特殊字符路径处理不完整的问题。
|
||||
/// </summary>
|
||||
/// <param name="fullPath">长路径。</param>
|
||||
/// <returns>短路径;失败返回 null。</returns>
|
||||
private static string? TryGetShortPath(string fullPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fullPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(1024);
|
||||
var ret = GetShortPathName(fullPath, sb, (uint)sb.Capacity);
|
||||
if (ret == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ByteArrayToAsciiString(byte[]? bytes)
|
||||
{
|
||||
if (bytes == null || bytes.Length == 0) return string.Empty;
|
||||
@@ -556,7 +714,7 @@ namespace CapMachine.Wpf.CanDrive.ZlgCan
|
||||
int len = Array.IndexOf(bytes, (byte)0);
|
||||
if (len < 0) len = bytes.Length;
|
||||
|
||||
return Encoding.ASCII.GetString(bytes, 0, len).Trim();
|
||||
return Encoding.Default.GetString(bytes, 0, len).Trim();
|
||||
}
|
||||
|
||||
private void ThrowIfDisposed()
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using CapMachine.Model.CANLIN;
|
||||
using CapMachine.Wpf.CanDrive;
|
||||
using CapMachine.Wpf.CanDrive.ZlgCan;
|
||||
using CapMachine.Wpf.Dtos;
|
||||
using ImTools;
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace CapMachine.Wpf.Services
|
||||
{
|
||||
@@ -109,6 +112,12 @@ namespace CapMachine.Wpf.Services
|
||||
/// </summary>
|
||||
public List<CanCmdData> CmdData { get; } = new List<CanCmdData>();
|
||||
|
||||
private readonly object _scheduleLock = new object();
|
||||
private List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> _scheduleItems = new List<(string, int, int, int)>();
|
||||
private CancellationTokenSource? _scheduleCts;
|
||||
private Task? _scheduleTask;
|
||||
private bool _scheduleUseConfigItems;
|
||||
|
||||
private CanCmdData? SpeedCanCmdData { get; set; }
|
||||
|
||||
private uint _deviceIndex = 0;
|
||||
@@ -224,6 +233,12 @@ namespace CapMachine.Wpf.Services
|
||||
{
|
||||
try
|
||||
{
|
||||
// Close 语义:关闭时必须停止循环发送与循环接收。
|
||||
// - 循环发送:停止软件调度,并关闭事件驱动发送标志。
|
||||
// - 循环接收:Driver.Close 内部会 StopReceiveLoop。
|
||||
StopSchedule();
|
||||
IsCycleSend = false;
|
||||
|
||||
Driver.Close();
|
||||
}
|
||||
finally
|
||||
@@ -233,6 +248,178 @@ namespace CapMachine.Wpf.Services
|
||||
}
|
||||
}
|
||||
|
||||
public void SetScheduleConfigs(IEnumerable<CANScheduleConfigDto> configs)
|
||||
{
|
||||
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List<CANScheduleConfigDto>();
|
||||
lock (_scheduleLock)
|
||||
{
|
||||
_scheduleItems = list
|
||||
.Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetScheduleConfigs(IEnumerable<CANFdScheduleConfigDto> configs)
|
||||
{
|
||||
var list = configs?.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).ToList() ?? new List<CANFdScheduleConfigDto>();
|
||||
lock (_scheduleLock)
|
||||
{
|
||||
_scheduleItems = list
|
||||
.Select(a => (a.MsgName!.Trim(), Math.Max(1, a.Cycle), a.OrderSend, a.SchTabIndex))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
public void StartSchedule()
|
||||
{
|
||||
if (!OpenState)
|
||||
{
|
||||
throw new InvalidOperationException("设备未连接,无法启动调度表。");
|
||||
}
|
||||
|
||||
List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items;
|
||||
lock (_scheduleLock)
|
||||
{
|
||||
items = _scheduleItems.ToList();
|
||||
}
|
||||
|
||||
if (items.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("调度表为空,无法启动调度表。");
|
||||
}
|
||||
|
||||
_scheduleUseConfigItems = true;
|
||||
StartSoftwareScheduler(items);
|
||||
}
|
||||
|
||||
public void StartPrecisionCycleSend(int cycleMs)
|
||||
{
|
||||
if (!OpenState)
|
||||
{
|
||||
throw new InvalidOperationException("设备未连接,无法启动循环发送。");
|
||||
}
|
||||
|
||||
var ms = Math.Max(1, cycleMs);
|
||||
var msgNames = CmdData.Where(a => !string.IsNullOrWhiteSpace(a.MsgName)).Select(a => a.MsgName!).Distinct(StringComparer.Ordinal).ToList();
|
||||
if (msgNames.Count == 0)
|
||||
{
|
||||
throw new InvalidOperationException("CmdData 为空,无法启动循环发送。");
|
||||
}
|
||||
|
||||
var items = msgNames.Select(n => (n, ms, 1, 0)).ToList();
|
||||
_scheduleUseConfigItems = false;
|
||||
StartSoftwareScheduler(items);
|
||||
}
|
||||
|
||||
public void StopSchedule()
|
||||
{
|
||||
CancellationTokenSource? cts;
|
||||
Task? task;
|
||||
lock (_scheduleLock)
|
||||
{
|
||||
cts = _scheduleCts;
|
||||
task = _scheduleTask;
|
||||
_scheduleCts = null;
|
||||
_scheduleTask = null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
cts?.Cancel();
|
||||
if (task != null)
|
||||
{
|
||||
task.Wait(TimeSpan.FromSeconds(2));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
finally
|
||||
{
|
||||
cts?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartSoftwareScheduler(List<(string MsgName, int CycleMs, int OrderSend, int SchTabIndex)> items)
|
||||
{
|
||||
StopSchedule();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
lock (_scheduleLock)
|
||||
{
|
||||
_scheduleCts = cts;
|
||||
}
|
||||
|
||||
// 统一:软件调度开启后,等同“循环发送开启”
|
||||
IsCycleSend = true;
|
||||
|
||||
_scheduleTask = Task.Run(async () =>
|
||||
{
|
||||
var token = cts.Token;
|
||||
|
||||
// next due time for each msg
|
||||
var now = DateTime.UtcNow;
|
||||
var due = new Dictionary<string, DateTime>(StringComparer.Ordinal);
|
||||
var cycle = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
var order = new Dictionary<string, int>(StringComparer.Ordinal);
|
||||
foreach (var it in items)
|
||||
{
|
||||
if (!due.ContainsKey(it.MsgName))
|
||||
{
|
||||
due[it.MsgName] = now;
|
||||
cycle[it.MsgName] = Math.Max(1, it.CycleMs);
|
||||
order[it.MsgName] = it.OrderSend;
|
||||
}
|
||||
}
|
||||
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
if (!OpenState)
|
||||
{
|
||||
await Task.Delay(50, token).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
var utcNow = DateTime.UtcNow;
|
||||
var minDue = due.Values.Min();
|
||||
var delay = minDue - utcNow;
|
||||
if (delay > TimeSpan.Zero)
|
||||
{
|
||||
var ms = (int)Math.Min(delay.TotalMilliseconds, 200);
|
||||
await Task.Delay(ms, token).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// due messages
|
||||
var ready = due.Where(kv => kv.Value <= utcNow).Select(kv => kv.Key).ToList();
|
||||
if (ready.Count == 0)
|
||||
{
|
||||
await Task.Delay(1, token).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 顺序/并行:这里只决定同一 tick 内的发送顺序(并行模式仍按字典序依次发)
|
||||
ready.Sort(StringComparer.Ordinal);
|
||||
foreach (var msg in ready)
|
||||
{
|
||||
if (token.IsCancellationRequested) break;
|
||||
try
|
||||
{
|
||||
Driver.SendOneMsgByCmdData(msg, 0, Mode == ZlgCanMode.Can ? (byte)ZDBC.FT_CAN : (byte)ZDBC.FT_CANFD);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warn($"调度表发送失败:{msg},{ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
due[msg] = DateTime.UtcNow.AddMilliseconds(cycle[msg]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, cts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载 DBC。
|
||||
/// </summary>
|
||||
|
||||
@@ -4,7 +4,10 @@ using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace CapMachine.Wpf.Services
|
||||
{
|
||||
@@ -150,8 +153,165 @@ namespace CapMachine.Wpf.Services
|
||||
/// <param name="path">LDF 路径。</param>
|
||||
public ObservableCollection<LinLdfModel> StartLdf(string path)
|
||||
{
|
||||
_log.Warn("ZLG LIN 当前版本未接入 LDF 解析(项目内仅存在 USB2XXX.dll 的 LDFParser)。");
|
||||
throw new NotSupportedException("ZLG LIN 暂未支持 LDF 解析,请后续提供/确认 ZLG 的 LDF DLL 接口(如 zldf.dll)后再接入。");
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("LDF 路径为空", nameof(path));
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
throw new FileNotFoundException($"LDF 文件不存在:{path}", path);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var text = File.ReadAllText(path, Encoding.UTF8);
|
||||
|
||||
// 去除单行注释,简化解析
|
||||
text = Regex.Replace(text, @"//.*?$", string.Empty, RegexOptions.Multiline);
|
||||
|
||||
var models = ParseLdfFramesAndSignals(text);
|
||||
|
||||
ListLinLdfModel.Clear();
|
||||
foreach (var item in models)
|
||||
{
|
||||
ListLinLdfModel.Add(item);
|
||||
}
|
||||
|
||||
LdfParserState = true;
|
||||
return ListLinLdfModel;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Error($"ZLG LIN 解析 LDF 失败:{ex.Message}");
|
||||
LdfParserState = false;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<LinLdfModel> ParseLdfFramesAndSignals(string ldfText)
|
||||
{
|
||||
// 说明:此解析器只用于生成“帧-信号全集池”,不做位宽/缩放等语义解析。
|
||||
// 目标:尽可能容错地从 Frames 区域提取 FrameName 与其包含的 SignalName 列表。
|
||||
|
||||
var framesBlock = TryExtractNamedBlock(ldfText, "Frames");
|
||||
if (string.IsNullOrWhiteSpace(framesBlock))
|
||||
{
|
||||
return new List<LinLdfModel>();
|
||||
}
|
||||
|
||||
var result = new List<LinLdfModel>();
|
||||
var exists = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
// Frame 定义一般形式:FrameName : ... { ... }
|
||||
// 这里以非贪婪匹配提取每个 Frame 的 body
|
||||
var frameRegex = new Regex(@"(?s)(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*:\s*.*?\{(?<body>.*?)\}\s*;?", RegexOptions.Compiled);
|
||||
var sigRegex = new Regex(@"(?m)^\s*(?<sig>[A-Za-z_][A-Za-z0-9_]*)\s*[,;]", RegexOptions.Compiled);
|
||||
|
||||
foreach (Match fm in frameRegex.Matches(framesBlock))
|
||||
{
|
||||
var frameName = fm.Groups["name"].Value;
|
||||
var body = fm.Groups["body"].Value;
|
||||
if (string.IsNullOrWhiteSpace(frameName) || string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Match sm in sigRegex.Matches(body))
|
||||
{
|
||||
var sigName = sm.Groups["sig"].Value;
|
||||
if (string.IsNullOrWhiteSpace(sigName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 排除明显的关键字(避免误采集)
|
||||
if (IsReservedKeyword(sigName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = $"{frameName}:{sigName}";
|
||||
if (!exists.Add(key))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new LinLdfModel
|
||||
{
|
||||
MsgName = frameName,
|
||||
SignalName = sigName,
|
||||
Name = null,
|
||||
SignalDesc = null,
|
||||
SignalUnit = null,
|
||||
IsSeletedInfo = 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string? TryExtractNamedBlock(string text, string blockName)
|
||||
{
|
||||
// 提取形如:blockName { ... } 的块内容(不包含外层大括号)。
|
||||
// 基于括号深度扫描,避免正则在嵌套结构上失效。
|
||||
var idx = CultureInvariantIndexOf(text, blockName);
|
||||
if (idx < 0) return null;
|
||||
|
||||
var braceIdx = text.IndexOf('{', idx);
|
||||
if (braceIdx < 0) return null;
|
||||
|
||||
int depth = 0;
|
||||
for (int i = braceIdx; i < text.Length; i++)
|
||||
{
|
||||
var ch = text[i];
|
||||
if (ch == '{') depth++;
|
||||
else if (ch == '}')
|
||||
{
|
||||
depth--;
|
||||
if (depth == 0)
|
||||
{
|
||||
return text.Substring(braceIdx + 1, i - braceIdx - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int CultureInvariantIndexOf(string text, string value)
|
||||
{
|
||||
return text.IndexOf(value, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool IsReservedKeyword(string token)
|
||||
{
|
||||
// LDF 常见关键字/区块名(用于降低误匹配概率)
|
||||
switch (token)
|
||||
{
|
||||
case "Frames":
|
||||
case "Signals":
|
||||
case "Signal":
|
||||
case "Nodes":
|
||||
case "Master":
|
||||
case "Slaves":
|
||||
case "Diagnostic":
|
||||
case "Diagnostics":
|
||||
case "Checksum":
|
||||
case "Event_triggered_frames":
|
||||
case "Sporadic_frames":
|
||||
case "Schedule_tables":
|
||||
case "Node_attributes":
|
||||
case "Node_composition":
|
||||
case "LIN_protocol":
|
||||
case "LIN_protocol_version":
|
||||
case "LIN_speed":
|
||||
case "Protocol_version":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
753
CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs
Normal file
753
CapMachine.Wpf/ViewModels/DialogZlgCanLinRwConfigViewModel.cs
Normal file
@@ -0,0 +1,753 @@
|
||||
using CapMachine.Core;
|
||||
using CapMachine.Model.CANLIN;
|
||||
using CapMachine.Wpf.Dtos;
|
||||
using CapMachine.Wpf.Services;
|
||||
using FreeSql;
|
||||
using Prism.Commands;
|
||||
using Prism.Services.Dialogs;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace CapMachine.Wpf.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// ZLG CAN/LIN 读写配置三栏管理弹窗 ViewModel。
|
||||
/// 左侧:写入/读取配置;右侧:信号全集候选池;统一保存落库。
|
||||
/// </summary>
|
||||
public class DialogZlgCanLinRwConfigViewModel : DialogViewModel
|
||||
{
|
||||
private readonly IFreeSql _freeSql;
|
||||
private readonly ILogService _logService;
|
||||
private readonly LogicRuleService _logicRuleService;
|
||||
|
||||
private long _canLinConfigProId;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数。
|
||||
/// </summary>
|
||||
/// <param name="freeSql">FreeSql。</param>
|
||||
/// <param name="logService">日志。</param>
|
||||
/// <param name="logicRuleService">逻辑规则服务。</param>
|
||||
public DialogZlgCanLinRwConfigViewModel(IFreeSql freeSql, ILogService logService, LogicRuleService logicRuleService)
|
||||
{
|
||||
_freeSql = freeSql;
|
||||
_logService = logService;
|
||||
_logicRuleService = logicRuleService;
|
||||
|
||||
Title = "读写设置";
|
||||
|
||||
WriteConfigs = new ObservableCollection<CanLinRWConfigDto>();
|
||||
ReadConfigs = new ObservableCollection<CanLinRWConfigDto>();
|
||||
SignalCandidates = new ObservableCollection<SignalCandidate>();
|
||||
SignalTree = new ObservableCollection<SignalFrameNode>();
|
||||
|
||||
SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates);
|
||||
SignalCandidatesView.Filter = FilterSignalCandidate;
|
||||
|
||||
WriteNameCbxItems = new ObservableCollection<CbxItems>()
|
||||
{
|
||||
new CbxItems(){ Key="转速",Text="转速"},
|
||||
new CbxItems(){ Key="功率限制",Text="功率限制"},
|
||||
new CbxItems(){ Key="使能",Text="使能"},
|
||||
new CbxItems(){ Key="Anti_Sleep",Text="Anti_Sleep"},
|
||||
new CbxItems(){ Key="PTC使能",Text="PTC使能"},
|
||||
new CbxItems(){ Key="PTC功率",Text="PTC功率"},
|
||||
new CbxItems(){ Key="PTC水流量",Text="PTC水流量"},
|
||||
new CbxItems(){ Key="PTC水温",Text="PTC水温"},
|
||||
};
|
||||
|
||||
ReadNameCbxItems = new ObservableCollection<CbxItems>()
|
||||
{
|
||||
new CbxItems(){ Key="通讯Cmp转速",Text="通讯Cmp转速"},
|
||||
new CbxItems(){ Key="通讯Cmp母线电压",Text="通讯Cmp母线电压"},
|
||||
new CbxItems(){ Key="通讯Cmp母线电流",Text="通讯Cmp母线电流"},
|
||||
new CbxItems(){ Key="通讯Cmp逆变器温度",Text="通讯Cmp逆变器温度"},
|
||||
new CbxItems(){ Key="通讯Cmp相电流",Text="通讯Cmp相电流"},
|
||||
new CbxItems(){ Key="通讯Cmp功率",Text="通讯Cmp功率"},
|
||||
new CbxItems(){ Key="通讯Cmp芯片温度",Text="通讯Cmp芯片温度"},
|
||||
new CbxItems(){ Key="通讯PTC入水温度",Text="通讯PTC入水温度"},
|
||||
new CbxItems(){ Key="通讯PTC出水温度",Text="通讯PTC出水温度"},
|
||||
new CbxItems(){ Key="通讯PTC峰值电流",Text="通讯PTC峰值电流"},
|
||||
new CbxItems(){ Key="通讯PTC母线电流",Text="通讯PTC母线电流"},
|
||||
new CbxItems(){ Key="通讯PTC膜温",Text="通讯PTC膜温"},
|
||||
new CbxItems(){ Key="通讯PTC模块温度",Text="通讯PTC模块温度"},
|
||||
};
|
||||
|
||||
IsEditable = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许编辑(由调用方根据 Active/打开状态决定)。
|
||||
/// </summary>
|
||||
public bool IsEditable { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 逻辑规则集合(下拉框 ItemsSource)。
|
||||
/// </summary>
|
||||
public IReadOnlyList<LogicRuleDto> LogicRuleDtos => _logicRuleService.LogicRuleDtos;
|
||||
|
||||
/// <summary>
|
||||
/// 写入配置“名称”下拉框集合(参考 CANConfigViewModel)。
|
||||
/// </summary>
|
||||
public ObservableCollection<CbxItems> WriteNameCbxItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 读取配置“名称”下拉框集合(参考 CANConfigViewModel)。
|
||||
/// </summary>
|
||||
public ObservableCollection<CbxItems> ReadNameCbxItems { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 写入配置集合。
|
||||
/// </summary>
|
||||
public ObservableCollection<CanLinRWConfigDto> WriteConfigs { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 读取配置集合。
|
||||
/// </summary>
|
||||
public ObservableCollection<CanLinRWConfigDto> ReadConfigs { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 信号候选集合(右侧池)。
|
||||
/// </summary>
|
||||
public ObservableCollection<SignalCandidate> SignalCandidates { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 信号树(按帧分组)。
|
||||
/// </summary>
|
||||
public ObservableCollection<SignalFrameNode> SignalTree { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 候选信号视图(含过滤)。
|
||||
/// </summary>
|
||||
public ICollectionView SignalCandidatesView { get; private set; }
|
||||
|
||||
private string? _signalFilterText;
|
||||
|
||||
/// <summary>
|
||||
/// 信号过滤文本(按 MsgName/SignalName/Name/Desc 匹配)。
|
||||
/// </summary>
|
||||
public string? SignalFilterText
|
||||
{
|
||||
get { return _signalFilterText; }
|
||||
set
|
||||
{
|
||||
_signalFilterText = value;
|
||||
RaisePropertyChanged();
|
||||
SignalCandidatesView.Refresh();
|
||||
RebuildSignalTree();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的候选信号。
|
||||
/// </summary>
|
||||
public SignalCandidate? SelectedSignalCandidate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的写入配置行。
|
||||
/// </summary>
|
||||
public CanLinRWConfigDto? SelectedWriteConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前选中的读取配置行。
|
||||
/// </summary>
|
||||
public CanLinRWConfigDto? SelectedReadConfig { get; set; }
|
||||
|
||||
private DelegateCommand<object>? _signalTreeSelectionChangedCmd;
|
||||
|
||||
/// <summary>
|
||||
/// 右侧信号树选中变化(仅当选中叶子节点时回写 SelectedSignalCandidate)。
|
||||
/// </summary>
|
||||
public DelegateCommand<object> SignalTreeSelectionChangedCmd =>
|
||||
_signalTreeSelectionChangedCmd ??= new DelegateCommand<object>(SignalTreeSelectionChangedCmdMethod);
|
||||
|
||||
private void SignalTreeSelectionChangedCmdMethod(object par)
|
||||
{
|
||||
if (par is SignalCandidate leaf)
|
||||
{
|
||||
SelectedSignalCandidate = leaf;
|
||||
RaisePropertyChanged(nameof(SelectedSignalCandidate));
|
||||
return;
|
||||
}
|
||||
|
||||
if (par is SignalFrameNode)
|
||||
{
|
||||
// 选中父节点时不变更 SelectedSignalCandidate
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private DelegateCommand? _addToWriteCmd;
|
||||
|
||||
/// <summary>
|
||||
/// 将右侧选中信号添加到写入配置。
|
||||
/// </summary>
|
||||
public DelegateCommand AddToWriteCmd => _addToWriteCmd ??= new DelegateCommand(AddToWriteCmdMethod);
|
||||
|
||||
private void AddToWriteCmdMethod()
|
||||
{
|
||||
if (!IsEditable)
|
||||
{
|
||||
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedSignalCandidate == null)
|
||||
{
|
||||
MessageBox.Show("请先在右侧信号集合中选中一条", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SelectedSignalCandidate.SignalName) || string.IsNullOrWhiteSpace(SelectedSignalCandidate.MsgName))
|
||||
{
|
||||
MessageBox.Show("选中的信号数据不完整(MsgName/SignalName 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (WriteConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Write))
|
||||
{
|
||||
MessageBox.Show("该信号已在写入配置中,无需重复添加", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ReadConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Read))
|
||||
{
|
||||
MessageBox.Show("该信号已在读取配置中,同一个信号不允许同时配置为写入与读取", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteConfigs.Add(new CanLinRWConfigDto
|
||||
{
|
||||
Id = 0,
|
||||
RWInfo = RW.Write,
|
||||
Name = string.IsNullOrWhiteSpace(SelectedSignalCandidate.Name) ? SelectedSignalCandidate.SignalName : SelectedSignalCandidate.Name,
|
||||
MsgFrameName = SelectedSignalCandidate.MsgName,
|
||||
SignalName = SelectedSignalCandidate.SignalName,
|
||||
DefautValue = "0",
|
||||
LogicRuleId = 0,
|
||||
});
|
||||
|
||||
RebuildSignalTree();
|
||||
}
|
||||
|
||||
private DelegateCommand? _addToReadCmd;
|
||||
|
||||
/// <summary>
|
||||
/// 将右侧选中信号添加到读取配置。
|
||||
/// </summary>
|
||||
public DelegateCommand AddToReadCmd => _addToReadCmd ??= new DelegateCommand(AddToReadCmdMethod);
|
||||
|
||||
private void AddToReadCmdMethod()
|
||||
{
|
||||
if (!IsEditable)
|
||||
{
|
||||
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedSignalCandidate == null)
|
||||
{
|
||||
MessageBox.Show("请先在右侧信号集合中选中一条", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SelectedSignalCandidate.SignalName) || string.IsNullOrWhiteSpace(SelectedSignalCandidate.MsgName))
|
||||
{
|
||||
MessageBox.Show("选中的信号数据不完整(MsgName/SignalName 为空)", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ReadConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Read))
|
||||
{
|
||||
MessageBox.Show("该信号已在读取配置中,无需重复添加", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (WriteConfigs.Any(a => string.Equals(a.SignalName, SelectedSignalCandidate.SignalName, StringComparison.Ordinal) && a.RWInfo == RW.Write))
|
||||
{
|
||||
MessageBox.Show("该信号已在写入配置中,同一个信号不允许同时配置为写入与读取", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
ReadConfigs.Add(new CanLinRWConfigDto
|
||||
{
|
||||
Id = 0,
|
||||
RWInfo = RW.Read,
|
||||
Name = string.IsNullOrWhiteSpace(SelectedSignalCandidate.Name) ? SelectedSignalCandidate.SignalName : SelectedSignalCandidate.Name,
|
||||
MsgFrameName = SelectedSignalCandidate.MsgName,
|
||||
SignalName = SelectedSignalCandidate.SignalName,
|
||||
DefautValue = "0",
|
||||
LogicRuleId = 0,
|
||||
});
|
||||
|
||||
RebuildSignalTree();
|
||||
}
|
||||
|
||||
private DelegateCommand? _removeWriteCmd;
|
||||
|
||||
/// <summary>
|
||||
/// 从写入配置移除当前选中行。
|
||||
/// </summary>
|
||||
public DelegateCommand RemoveWriteCmd => _removeWriteCmd ??= new DelegateCommand(RemoveWriteCmdMethod);
|
||||
|
||||
private void RemoveWriteCmdMethod()
|
||||
{
|
||||
if (!IsEditable)
|
||||
{
|
||||
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedWriteConfig == null)
|
||||
{
|
||||
MessageBox.Show("请先选中写入列表中的一行", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
WriteConfigs.Remove(SelectedWriteConfig);
|
||||
SelectedWriteConfig = null;
|
||||
RaisePropertyChanged(nameof(SelectedWriteConfig));
|
||||
|
||||
RebuildSignalTree();
|
||||
}
|
||||
|
||||
private DelegateCommand? _removeReadCmd;
|
||||
|
||||
/// <summary>
|
||||
/// 从读取配置移除当前选中行。
|
||||
/// </summary>
|
||||
public DelegateCommand RemoveReadCmd => _removeReadCmd ??= new DelegateCommand(RemoveReadCmdMethod);
|
||||
|
||||
private void RemoveReadCmdMethod()
|
||||
{
|
||||
if (!IsEditable)
|
||||
{
|
||||
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedReadConfig == null)
|
||||
{
|
||||
MessageBox.Show("请先选中读取列表中的一行", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
ReadConfigs.Remove(SelectedReadConfig);
|
||||
SelectedReadConfig = null;
|
||||
RaisePropertyChanged(nameof(SelectedReadConfig));
|
||||
|
||||
RebuildSignalTree();
|
||||
}
|
||||
|
||||
private DelegateCommand? _saveCmd;
|
||||
|
||||
/// <summary>
|
||||
/// 保存并落库。
|
||||
/// </summary>
|
||||
public DelegateCommand SaveCmd => _saveCmd ??= new DelegateCommand(SaveCmdMethod);
|
||||
|
||||
private void SaveCmdMethod()
|
||||
{
|
||||
if (!IsEditable)
|
||||
{
|
||||
MessageBox.Show("当前状态禁止修改", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_canLinConfigProId <= 0)
|
||||
{
|
||||
MessageBox.Show("配置程序ID无效,无法保存", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
PersistRwConfigs();
|
||||
|
||||
var pars = new DialogParameters
|
||||
{
|
||||
{ "Saved", true }
|
||||
};
|
||||
RaiseRequestClose(new DialogResult(ButtonResult.OK, pars));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logService.Error($"ZLG 读写设置保存失败:{ex}");
|
||||
MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private DelegateCommand? _cancelCmd;
|
||||
|
||||
/// <summary>
|
||||
/// 取消。
|
||||
/// </summary>
|
||||
public DelegateCommand CancelCmd => _cancelCmd ??= new DelegateCommand(CancelCmdMethod);
|
||||
|
||||
private void CancelCmdMethod()
|
||||
{
|
||||
RaiseRequestClose(new DialogResult(ButtonResult.Cancel));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 弹窗打开时接收参数。
|
||||
/// </summary>
|
||||
/// <param name="parameters">参数。</param>
|
||||
public override void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
_canLinConfigProId = parameters.GetValue<long>("CanLinConfigProId");
|
||||
|
||||
IsEditable = parameters.ContainsKey("IsEditable") ? parameters.GetValue<bool>("IsEditable") : true;
|
||||
RaisePropertyChanged(nameof(IsEditable));
|
||||
|
||||
if (parameters.ContainsKey("WriteConfigs"))
|
||||
{
|
||||
var list = parameters.GetValue<ObservableCollection<CanLinRWConfigDto>>("WriteConfigs") ?? new ObservableCollection<CanLinRWConfigDto>();
|
||||
WriteConfigs = list;
|
||||
RaisePropertyChanged(nameof(WriteConfigs));
|
||||
}
|
||||
|
||||
if (parameters.ContainsKey("ReadConfigs"))
|
||||
{
|
||||
var list = parameters.GetValue<ObservableCollection<CanLinRWConfigDto>>("ReadConfigs") ?? new ObservableCollection<CanLinRWConfigDto>();
|
||||
ReadConfigs = list;
|
||||
RaisePropertyChanged(nameof(ReadConfigs));
|
||||
}
|
||||
|
||||
if (parameters.ContainsKey("SignalCandidates"))
|
||||
{
|
||||
var list = parameters.GetValue<ObservableCollection<SignalCandidate>>("SignalCandidates") ?? new ObservableCollection<SignalCandidate>();
|
||||
SignalCandidates = list;
|
||||
RaisePropertyChanged(nameof(SignalCandidates));
|
||||
|
||||
SignalCandidatesView = CollectionViewSource.GetDefaultView(SignalCandidates);
|
||||
SignalCandidatesView.Filter = FilterSignalCandidate;
|
||||
RaisePropertyChanged(nameof(SignalCandidatesView));
|
||||
}
|
||||
|
||||
RebuildSignalTree();
|
||||
|
||||
if (parameters.ContainsKey("Title"))
|
||||
{
|
||||
Title = parameters.GetValue<string>("Title") ?? Title;
|
||||
}
|
||||
}
|
||||
|
||||
private bool FilterSignalCandidate(object obj)
|
||||
{
|
||||
if (obj is not SignalCandidate item)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(SignalFilterText))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var key = SignalFilterText.Trim();
|
||||
|
||||
return ContainsIgnoreCase(item.MsgName, key)
|
||||
|| ContainsIgnoreCase(item.SignalName, key)
|
||||
|| ContainsIgnoreCase(item.Name, key)
|
||||
|| ContainsIgnoreCase(item.Desc, key);
|
||||
}
|
||||
|
||||
private void RebuildSignalTree()
|
||||
{
|
||||
// 依据过滤条件 + 全量候选池生成树。
|
||||
// 树数据源独立于 ICollectionView,避免 TreeView 过滤复杂度。
|
||||
var filtered = SignalCandidates
|
||||
.Where(a => FilterSignalCandidate(a))
|
||||
.ToList();
|
||||
|
||||
var groups = filtered
|
||||
.GroupBy(a => string.IsNullOrWhiteSpace(a.MsgName) ? "(未命名帧)" : a.MsgName!.Trim(), StringComparer.Ordinal)
|
||||
.OrderBy(a => a.Key, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
SignalTree.Clear();
|
||||
foreach (var g in groups)
|
||||
{
|
||||
var signalNodes = g
|
||||
.OrderBy(a => a.SignalName ?? string.Empty, StringComparer.Ordinal)
|
||||
.ThenBy(a => a.Name ?? string.Empty, StringComparer.Ordinal)
|
||||
.Select(a => new SignalCandidate
|
||||
{
|
||||
MsgName = a.MsgName,
|
||||
SignalName = a.SignalName,
|
||||
Name = a.Name,
|
||||
Desc = a.Desc,
|
||||
AddedInfo = ComputeAddedInfo(a),
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var node = new SignalFrameNode
|
||||
{
|
||||
FrameName = g.Key,
|
||||
Signals = new ObservableCollection<SignalCandidate>(signalNodes)
|
||||
};
|
||||
SignalTree.Add(node);
|
||||
}
|
||||
|
||||
RaisePropertyChanged(nameof(SignalTree));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算候选信号是否已被添加到写入/读取。
|
||||
/// 0=未添加,1=已添加到写入,2=已添加到读取,3=同时存在于写入与读取。
|
||||
/// </summary>
|
||||
/// <param name="candidate">候选信号。</param>
|
||||
/// <returns>AddedInfo 标志值。</returns>
|
||||
private int ComputeAddedInfo(SignalCandidate candidate)
|
||||
{
|
||||
if (candidate == null || string.IsNullOrWhiteSpace(candidate.SignalName))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var signal = candidate.SignalName;
|
||||
var inWrite = WriteConfigs.Any(a => a.RWInfo == RW.Write && string.Equals(a.SignalName, signal, StringComparison.Ordinal));
|
||||
var inRead = ReadConfigs.Any(a => a.RWInfo == RW.Read && string.Equals(a.SignalName, signal, StringComparison.Ordinal));
|
||||
|
||||
if (inWrite && inRead) return 3;
|
||||
if (inWrite) return 1;
|
||||
if (inRead) return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool ContainsIgnoreCase(string? src, string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(src)) return false;
|
||||
return src.IndexOf(key, StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private void PersistRwConfigs()
|
||||
{
|
||||
// 互斥约束:同一 SignalName 不允许同时出现在写入与读取
|
||||
EnsureNoWriteReadConflict();
|
||||
|
||||
// 规范化 DTO(空值/默认值防御)
|
||||
NormalizeRwConfigs(WriteConfigs, RW.Write);
|
||||
NormalizeRwConfigs(ReadConfigs, RW.Read);
|
||||
|
||||
// 防重复:同一 SignalName 在同一 RW 列表中只允许一条
|
||||
EnsureNoDuplicateSignal(WriteConfigs, RW.Write);
|
||||
EnsureNoDuplicateSignal(ReadConfigs, RW.Read);
|
||||
|
||||
var existing = _freeSql.Select<CanLinRWConfig>()
|
||||
.Where(a => a.CanLinConfigProId == _canLinConfigProId)
|
||||
.Where(a => a.RWInfo == RW.Write || a.RWInfo == RW.Read)
|
||||
.ToList();
|
||||
|
||||
var desiredWrite = WriteConfigs
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a.SignalName))
|
||||
.Select(a => new DesiredItem(RW.Write, a.SignalName!, a))
|
||||
.ToList();
|
||||
|
||||
var desiredRead = ReadConfigs
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a.SignalName))
|
||||
.Select(a => new DesiredItem(RW.Read, a.SignalName!, a))
|
||||
.ToList();
|
||||
|
||||
var desiredAll = desiredWrite.Concat(desiredRead).ToList();
|
||||
|
||||
var desiredKeySet = new HashSet<string>(desiredAll.Select(a => BuildKey(a.Rw, a.SignalName)), StringComparer.Ordinal);
|
||||
var existingByKey = existing.ToDictionary(a => BuildKey(a.RWInfo, a.SignalName ?? string.Empty), a => a, StringComparer.Ordinal);
|
||||
|
||||
// 删除:DB 中存在,但目标集合里不存在
|
||||
foreach (var old in existing)
|
||||
{
|
||||
var key = BuildKey(old.RWInfo, old.SignalName ?? string.Empty);
|
||||
if (!desiredKeySet.Contains(key))
|
||||
{
|
||||
_freeSql.Delete<CanLinRWConfig>(old.Id).ExecuteAffrows();
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert:按 key(RWInfo + SignalName)更新或插入
|
||||
foreach (var item in desiredAll)
|
||||
{
|
||||
var key = BuildKey(item.Rw, item.SignalName);
|
||||
if (existingByKey.TryGetValue(key, out var old))
|
||||
{
|
||||
_freeSql.Update<CanLinRWConfig>(old.Id)
|
||||
.Set(a => a.Name, item.Dto.Name)
|
||||
.Set(a => a.MsgFrameName, item.Dto.MsgFrameName)
|
||||
.Set(a => a.SignalName, item.Dto.SignalName)
|
||||
.Set(a => a.DefautValue, item.Dto.DefautValue)
|
||||
.Set(a => a.LogicRuleId, item.Dto.LogicRuleId)
|
||||
.ExecuteAffrows();
|
||||
}
|
||||
else
|
||||
{
|
||||
_freeSql.Insert<CanLinRWConfig>(new CanLinRWConfig
|
||||
{
|
||||
CanLinConfigProId = _canLinConfigProId,
|
||||
RWInfo = item.Rw,
|
||||
Name = item.Dto.Name,
|
||||
MsgFrameName = item.Dto.MsgFrameName,
|
||||
SignalName = item.Dto.SignalName,
|
||||
DefautValue = item.Dto.DefautValue,
|
||||
LogicRuleId = item.Dto.LogicRuleId,
|
||||
}).ExecuteAffrows();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void NormalizeRwConfigs(IEnumerable<CanLinRWConfigDto> list, RW rw)
|
||||
{
|
||||
foreach (var item in list)
|
||||
{
|
||||
item.RWInfo = rw;
|
||||
if (string.IsNullOrWhiteSpace(item.SignalName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.Name))
|
||||
{
|
||||
item.Name = item.SignalName;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.MsgFrameName))
|
||||
{
|
||||
item.MsgFrameName = string.Empty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(item.DefautValue))
|
||||
{
|
||||
item.DefautValue = "0";
|
||||
}
|
||||
|
||||
if (item.LogicRuleId < 0)
|
||||
{
|
||||
item.LogicRuleId = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureNoDuplicateSignal(IEnumerable<CanLinRWConfigDto> list, RW rw)
|
||||
{
|
||||
var duplicates = list
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a.SignalName))
|
||||
.GroupBy(a => a.SignalName!, StringComparer.Ordinal)
|
||||
.Where(g => g.Count() > 1)
|
||||
.Select(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
if (duplicates.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException($"{rw} 配置中存在重复信号:{string.Join(",", duplicates)}");
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureNoWriteReadConflict()
|
||||
{
|
||||
var writeSet = new HashSet<string>(
|
||||
WriteConfigs
|
||||
.Where(a => a.RWInfo == RW.Write)
|
||||
.Select(a => a.SignalName)
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||
.Select(a => a!),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
var readSet = new HashSet<string>(
|
||||
ReadConfigs
|
||||
.Where(a => a.RWInfo == RW.Read)
|
||||
.Select(a => a.SignalName)
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a))
|
||||
.Select(a => a!),
|
||||
StringComparer.Ordinal);
|
||||
|
||||
writeSet.IntersectWith(readSet);
|
||||
if (writeSet.Count > 0)
|
||||
{
|
||||
throw new InvalidOperationException($"同一信号不允许同时配置为写入与读取,冲突信号:{string.Join(",", writeSet)}");
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildKey(RW rw, string signalName)
|
||||
{
|
||||
return $"{(int)rw}:{signalName}";
|
||||
}
|
||||
|
||||
private readonly struct DesiredItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 读写类型。
|
||||
/// </summary>
|
||||
public RW Rw { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 信号名称。
|
||||
/// </summary>
|
||||
public string SignalName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 原始 DTO。
|
||||
/// </summary>
|
||||
public CanLinRWConfigDto Dto { get; }
|
||||
|
||||
public DesiredItem(RW rw, string signalName, CanLinRWConfigDto dto)
|
||||
{
|
||||
Rw = rw;
|
||||
SignalName = signalName;
|
||||
Dto = dto;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 右侧信号候选项。
|
||||
/// </summary>
|
||||
public class SignalCandidate
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息名称/帧名称。
|
||||
/// </summary>
|
||||
public string? MsgName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 信号名称。
|
||||
/// </summary>
|
||||
public string? SignalName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 配置名称(若解析层已有中文名则传入)。
|
||||
/// </summary>
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 描述。
|
||||
/// </summary>
|
||||
public string? Desc { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已添加标记。
|
||||
/// 0=未添加,1=已添加到写入,2=已添加到读取,3=同时存在于写入与读取。
|
||||
/// </summary>
|
||||
public int AddedInfo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 帧节点。
|
||||
/// </summary>
|
||||
public class SignalFrameNode
|
||||
{
|
||||
/// <summary>
|
||||
/// 帧名。
|
||||
/// </summary>
|
||||
public string FrameName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 帧内信号集合。
|
||||
/// </summary>
|
||||
public ObservableCollection<SignalCandidate> Signals { get; set; } = new ObservableCollection<SignalCandidate>();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ using CapMachine.Model.CANLIN;
|
||||
using CapMachine.Wpf.Dtos;
|
||||
using CapMachine.Wpf.LinDrive;
|
||||
using CapMachine.Wpf.Services;
|
||||
using CapMachine.Wpf.Views;
|
||||
using ImTools;
|
||||
using Microsoft.Win32;
|
||||
using Prism.Commands;
|
||||
@@ -78,6 +79,25 @@ namespace CapMachine.Wpf.ViewModels
|
||||
/// </summary>
|
||||
public CanLinConfigPro? SelectCanLinConfigPro { get; set; }
|
||||
|
||||
private ObservableCollection<CanLinRWConfigDto> _listWriteCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>();
|
||||
public ObservableCollection<CanLinRWConfigDto> ListWriteCanLinRWConfigDto
|
||||
{
|
||||
get { return _listWriteCanLinRWConfigDto; }
|
||||
set { _listWriteCanLinRWConfigDto = value; RaisePropertyChanged(); }
|
||||
}
|
||||
|
||||
private ObservableCollection<CanLinRWConfigDto> _listReadCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>();
|
||||
public ObservableCollection<CanLinRWConfigDto> ListReadCanLinRWConfigDto
|
||||
{
|
||||
get { return _listReadCanLinRWConfigDto; }
|
||||
set { _listReadCanLinRWConfigDto = value; RaisePropertyChanged(); }
|
||||
}
|
||||
|
||||
public bool IsRwEditable
|
||||
{
|
||||
get { return !ZlgLinDriveService.OpenState; }
|
||||
}
|
||||
|
||||
private LINConfigExdDto? _SelectedLINConfigExdDto;
|
||||
/// <summary>
|
||||
/// 选中的 LIN 配置 DTO。
|
||||
@@ -146,6 +166,14 @@ namespace CapMachine.Wpf.ViewModels
|
||||
{
|
||||
if (SelectCanLinConfigPro == null) return;
|
||||
SelectedLINConfigExdDto = Mapper.Map<LINConfigExdDto>(SelectCanLinConfigPro.LINConfigExd);
|
||||
|
||||
var writeData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Write).ToList() ?? new List<CanLinRWConfig>();
|
||||
ListWriteCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>(Mapper.Map<List<CanLinRWConfigDto>>(writeData));
|
||||
|
||||
var readData = SelectCanLinConfigPro.CanLinConfigContents?.Where(a => a.RWInfo == RW.Read).ToList() ?? new List<CanLinRWConfig>();
|
||||
ListReadCanLinRWConfigDto = new ObservableCollection<CanLinRWConfigDto>(Mapper.Map<List<CanLinRWConfigDto>>(readData));
|
||||
|
||||
RaisePropertyChanged(nameof(IsRwEditable));
|
||||
}
|
||||
|
||||
private DelegateCommand<object>? _LinConfigProGridSelectionChangedCmd;
|
||||
@@ -270,11 +298,15 @@ namespace CapMachine.Wpf.ViewModels
|
||||
{
|
||||
ConfigService.CanLinRunStateModel.CurSysSelectedCanLin = CanLinEnum.Lin;
|
||||
}
|
||||
|
||||
RaisePropertyChanged(nameof(IsRwEditable));
|
||||
break;
|
||||
|
||||
case "Close":
|
||||
ZlgLinDriveService.CloseDevice();
|
||||
ConfigService.CanLinRunStateModel.CurSysSelectedCanLin = CanLinEnum.No;
|
||||
|
||||
RaisePropertyChanged(nameof(IsRwEditable));
|
||||
break;
|
||||
|
||||
case "Save":
|
||||
@@ -315,6 +347,113 @@ namespace CapMachine.Wpf.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private DelegateCommand? _openRwDialogCmd;
|
||||
public DelegateCommand OpenRwDialogCmd
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_openRwDialogCmd == null)
|
||||
{
|
||||
_openRwDialogCmd = new DelegateCommand(OpenRwDialogCmdMethod);
|
||||
}
|
||||
return _openRwDialogCmd;
|
||||
}
|
||||
}
|
||||
|
||||
private void OpenRwDialogCmdMethod()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (SelectCanLinConfigPro == null)
|
||||
{
|
||||
MessageBox.Show("选中LIN配置名称后再操作", "提示", MessageBoxButton.OK, MessageBoxImage.Hand);
|
||||
return;
|
||||
}
|
||||
|
||||
var writeClones = new ObservableCollection<CanLinRWConfigDto>(
|
||||
(ListWriteCanLinRWConfigDto ?? new ObservableCollection<CanLinRWConfigDto>())
|
||||
.Select(CloneRwDto));
|
||||
|
||||
foreach (var item in writeClones)
|
||||
{
|
||||
item.RWInfo = RW.Write;
|
||||
}
|
||||
|
||||
var readClones = new ObservableCollection<CanLinRWConfigDto>(
|
||||
(ListReadCanLinRWConfigDto ?? new ObservableCollection<CanLinRWConfigDto>())
|
||||
.Select(CloneRwDto));
|
||||
|
||||
foreach (var item in readClones)
|
||||
{
|
||||
item.RWInfo = RW.Read;
|
||||
}
|
||||
|
||||
var candidates = new ObservableCollection<DialogZlgCanLinRwConfigViewModel.SignalCandidate>();
|
||||
if (ZlgLinDriveService.ListLinLdfModel != null)
|
||||
{
|
||||
foreach (var sig in ZlgLinDriveService.ListLinLdfModel)
|
||||
{
|
||||
candidates.Add(new DialogZlgCanLinRwConfigViewModel.SignalCandidate
|
||||
{
|
||||
MsgName = sig.MsgName,
|
||||
SignalName = sig.SignalName,
|
||||
Name = sig.Name,
|
||||
Desc = sig.SignalDesc,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var pars = new DialogParameters
|
||||
{
|
||||
{ "Title", "读写设置" },
|
||||
{ "CanLinConfigProId", SelectCanLinConfigPro.Id },
|
||||
{ "IsEditable", IsRwEditable },
|
||||
{ "WriteConfigs", writeClones },
|
||||
{ "ReadConfigs", readClones },
|
||||
{ "SignalCandidates", candidates },
|
||||
};
|
||||
|
||||
DialogService.ShowDialog(nameof(DialogZlgCanLinRwConfigView), pars, r =>
|
||||
{
|
||||
if (r.Result == ButtonResult.OK)
|
||||
{
|
||||
ReloadCurrentConfigPro();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReloadCurrentConfigPro()
|
||||
{
|
||||
var id = SelectCanLinConfigPro?.Id;
|
||||
InitLoadLinConfigPro();
|
||||
|
||||
if (id != null)
|
||||
{
|
||||
SelectCanLinConfigPro = linConfigPros.Find(a => a.Id == id.Value);
|
||||
SyncSelectedConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private static CanLinRWConfigDto CloneRwDto(CanLinRWConfigDto src)
|
||||
{
|
||||
return new CanLinRWConfigDto
|
||||
{
|
||||
Id = src.Id,
|
||||
RWInfo = src.RWInfo,
|
||||
Name = src.Name,
|
||||
MsgFrameName = src.MsgFrameName,
|
||||
SignalName = src.SignalName,
|
||||
DefautValue = src.DefautValue,
|
||||
LogicRuleId = src.LogicRuleId,
|
||||
LogicRuleDto = src.LogicRuleDto,
|
||||
};
|
||||
}
|
||||
|
||||
private DelegateCommand<object>? _SchEnableCmd;
|
||||
/// <summary>
|
||||
/// 调度表使能写入驱动。
|
||||
|
||||
331
CapMachine.Wpf/Views/DialogZlgCanLinRwConfigView.xaml
Normal file
331
CapMachine.Wpf/Views/DialogZlgCanLinRwConfigView.xaml
Normal file
@@ -0,0 +1,331 @@
|
||||
<UserControl
|
||||
x:Class="CapMachine.Wpf.Views.DialogZlgCanLinRwConfigView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
|
||||
xmlns:local="clr-namespace:CapMachine.Wpf.Views"
|
||||
xmlns:localEx="clr-namespace:CapMachine.Wpf"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:prism="http://prismlibrary.com/"
|
||||
xmlns:vm="clr-namespace:CapMachine.Wpf.ViewModels"
|
||||
Width="1900"
|
||||
Height="900"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<localEx:BindingProxy x:Key="Proxy" Data="{Binding}" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition Height="auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="1.3*" />
|
||||
<ColumnDefinition Width="15" />
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid Grid.Column="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="15" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<GroupBox Grid.Row="0">
|
||||
<GroupBox.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="2,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="/Assets/Fonts/#iconfont"
|
||||
FontSize="18"
|
||||
Foreground="White"
|
||||
Text="" />
|
||||
<TextBlock FontSize="18" Foreground="White">写入配置</TextBlock>
|
||||
</StackPanel>
|
||||
</GroupBox.Header>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="5,0"
|
||||
Command="{Binding RemoveWriteCmd}"
|
||||
Content="从写入移除"
|
||||
Foreground="White"
|
||||
IsEnabled="{Binding IsEditable}" />
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid
|
||||
Grid.Row="1"
|
||||
Margin="5"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
HeadersVisibility="Column"
|
||||
ItemsSource="{Binding WriteConfigs}"
|
||||
SelectedItem="{Binding SelectedWriteConfig, Mode=TwoWay}"
|
||||
SelectionMode="Single"
|
||||
SelectionUnit="FullRow">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="180" Header="名称">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox
|
||||
DisplayMemberPath="Text"
|
||||
IsEditable="True"
|
||||
IsEnabled="{Binding Source={StaticResource Proxy}, Path=Data.IsEditable}"
|
||||
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.WriteNameCbxItems}"
|
||||
SelectedValue="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValuePath="Text" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
Width="200"
|
||||
Binding="{Binding MsgFrameName}"
|
||||
Header="消息名称"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn
|
||||
Width="200"
|
||||
Binding="{Binding SignalName}"
|
||||
Header="信号名称"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
Binding="{Binding DefautValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="默认值" />
|
||||
<DataGridTemplateColumn Width="200" Header="规则名称">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox
|
||||
DisplayMemberPath="Name"
|
||||
IsEnabled="{Binding Source={StaticResource Proxy}, Path=Data.IsEditable}"
|
||||
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.LogicRuleDtos}"
|
||||
SelectedValue="{Binding LogicRuleId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValuePath="Id" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<GroupBox Grid.Row="2">
|
||||
<GroupBox.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="2,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="/Assets/Fonts/#iconfont"
|
||||
FontSize="18"
|
||||
Foreground="White"
|
||||
Text="" />
|
||||
<TextBlock FontSize="18" Foreground="White">读取配置</TextBlock>
|
||||
</StackPanel>
|
||||
</GroupBox.Header>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="5,0"
|
||||
Command="{Binding RemoveReadCmd}"
|
||||
Content="从读取移除"
|
||||
Foreground="White"
|
||||
IsEnabled="{Binding IsEditable}" />
|
||||
</StackPanel>
|
||||
|
||||
<DataGrid
|
||||
Grid.Row="1"
|
||||
Margin="5"
|
||||
AutoGenerateColumns="False"
|
||||
CanUserAddRows="False"
|
||||
HeadersVisibility="Column"
|
||||
ItemsSource="{Binding ReadConfigs}"
|
||||
SelectedItem="{Binding SelectedReadConfig, Mode=TwoWay}"
|
||||
SelectionMode="Single"
|
||||
SelectionUnit="FullRow">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn Width="180" Header="名称">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox
|
||||
DisplayMemberPath="Text"
|
||||
IsEditable="True"
|
||||
IsEnabled="{Binding Source={StaticResource Proxy}, Path=Data.IsEditable}"
|
||||
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.ReadNameCbxItems}"
|
||||
SelectedValue="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValuePath="Text" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn
|
||||
Width="200"
|
||||
Binding="{Binding MsgFrameName}"
|
||||
Header="消息名称"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn
|
||||
Width="200"
|
||||
Binding="{Binding SignalName}"
|
||||
Header="信号名称"
|
||||
IsReadOnly="True" />
|
||||
<DataGridTextColumn
|
||||
Width="120"
|
||||
Binding="{Binding DefautValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
Header="默认值" />
|
||||
<DataGridTemplateColumn Width="200" Header="规则名称">
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<ComboBox
|
||||
DisplayMemberPath="Name"
|
||||
IsEnabled="{Binding Source={StaticResource Proxy}, Path=Data.IsEditable}"
|
||||
ItemsSource="{Binding Source={StaticResource Proxy}, Path=Data.LogicRuleDtos}"
|
||||
SelectedValue="{Binding LogicRuleId, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
SelectedValuePath="Id" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
|
||||
<GroupBox Grid.Column="2">
|
||||
<GroupBox.Header>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="2,0,5,0"
|
||||
VerticalAlignment="Center"
|
||||
FontFamily="/Assets/Fonts/#iconfont"
|
||||
FontSize="18"
|
||||
Foreground="White"
|
||||
Text="" />
|
||||
<TextBlock FontSize="18" Foreground="White">信号集合</TextBlock>
|
||||
</StackPanel>
|
||||
</GroupBox.Header>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition Height="auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBox
|
||||
Grid.Row="0"
|
||||
Margin="5"
|
||||
VerticalContentAlignment="Center"
|
||||
Text="{Binding SignalFilterText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="5"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="5,0"
|
||||
Command="{Binding AddToWriteCmd}"
|
||||
Content="添加到写入"
|
||||
Foreground="White"
|
||||
IsEnabled="{Binding IsEditable}" />
|
||||
<Button
|
||||
Margin="5,0"
|
||||
Command="{Binding AddToReadCmd}"
|
||||
Content="添加到读取"
|
||||
Foreground="White"
|
||||
IsEnabled="{Binding IsEditable}" />
|
||||
</StackPanel>
|
||||
|
||||
<TreeView
|
||||
x:Name="SignalTreeView"
|
||||
Grid.Row="2"
|
||||
Margin="5"
|
||||
ItemsSource="{Binding SignalTree}">
|
||||
<TreeView.Resources>
|
||||
<HierarchicalDataTemplate DataType="{x:Type vm:DialogZlgCanLinRwConfigViewModel+SignalFrameNode}" ItemsSource="{Binding Signals}">
|
||||
<TextBlock FontWeight="Bold" Text="{Binding FrameName}" />
|
||||
</HierarchicalDataTemplate>
|
||||
|
||||
<DataTemplate DataType="{x:Type vm:DialogZlgCanLinRwConfigViewModel+SignalCandidate}">
|
||||
<Grid>
|
||||
<Grid.Style>
|
||||
<Style TargetType="Grid">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding AddedInfo}" Value="1">
|
||||
<Setter Property="Background" Value="LightGreen" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding AddedInfo}" Value="2">
|
||||
<Setter Property="Background" Value="SkyBlue" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding AddedInfo}" Value="3">
|
||||
<Setter Property="Background" Value="Orange" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Grid.Style>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="180" />
|
||||
<ColumnDefinition Width="160" />
|
||||
<ColumnDefinition Width="120" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="{Binding MsgName}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding SignalName}" />
|
||||
<TextBlock Grid.Column="2" Text="{Binding Name}" />
|
||||
<TextBlock
|
||||
Grid.Column="3"
|
||||
Text="{Binding Desc}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</TreeView.Resources>
|
||||
|
||||
<i:Interaction.Triggers>
|
||||
<i:EventTrigger EventName="SelectedItemChanged">
|
||||
<prism:InvokeCommandAction Command="{Binding SignalTreeSelectionChangedCmd}" CommandParameter="{Binding ElementName=SignalTreeView, Path=SelectedItem}" />
|
||||
</i:EventTrigger>
|
||||
</i:Interaction.Triggers>
|
||||
</TreeView>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Margin="10,10"
|
||||
Command="{Binding SaveCmd}"
|
||||
Content="保存"
|
||||
Foreground="White"
|
||||
IsEnabled="{Binding IsEditable}" />
|
||||
<Button
|
||||
Margin="10,0"
|
||||
Command="{Binding CancelCmd}"
|
||||
Content="取消"
|
||||
Foreground="White" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
15
CapMachine.Wpf/Views/DialogZlgCanLinRwConfigView.xaml.cs
Normal file
15
CapMachine.Wpf/Views/DialogZlgCanLinRwConfigView.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace CapMachine.Wpf.Views
|
||||
{
|
||||
/// <summary>
|
||||
/// DialogZlgCanLinRwConfigView.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class DialogZlgCanLinRwConfigView : UserControl
|
||||
{
|
||||
public DialogZlgCanLinRwConfigView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -174,6 +174,14 @@
|
||||
VerticalContentAlignment="Center"
|
||||
Text="{Binding LinBaudRate, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<Button
|
||||
Margin="0,0,20,0"
|
||||
Command="{Binding OpenRwDialogCmd}"
|
||||
Foreground="White"
|
||||
IsEnabled="{Binding IsRwEditable}">
|
||||
<TextBlock FontSize="14" Text="读写设置" />
|
||||
</Button>
|
||||
|
||||
<CheckBox
|
||||
VerticalAlignment="Center"
|
||||
Content="调度使能"
|
||||
|
||||
Reference in New Issue
Block a user