添加项目文件。
This commit is contained in:
3
OneClick.slnx
Normal file
3
OneClick.slnx
Normal file
@@ -0,0 +1,3 @@
|
||||
<Solution>
|
||||
<Project Path="OneClick/OneClick.csproj" />
|
||||
</Solution>
|
||||
8
OneClick/App.xaml
Normal file
8
OneClick/App.xaml
Normal file
@@ -0,0 +1,8 @@
|
||||
<Application
|
||||
x:Class="OneClick.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:OneClick"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources />
|
||||
</Application>
|
||||
16
OneClick/App.xaml.cs
Normal file
16
OneClick/App.xaml.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using Prism.Ioc;
|
||||
using System.ComponentModel;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace OneClick
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
10
OneClick/AssemblyInfo.cs
Normal file
10
OneClick/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
344
OneClick/AxisService.cs
Normal file
344
OneClick/AxisService.cs
Normal file
@@ -0,0 +1,344 @@
|
||||
using GTN;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace OneClick
|
||||
{
|
||||
public class AxisService
|
||||
{
|
||||
|
||||
public void InitMotionCard()
|
||||
{
|
||||
// 初始化运动控制卡
|
||||
int sRtn = -1;
|
||||
short eCATStatus = -1;
|
||||
short core = 1; // 使用第一个EtherCAT主站
|
||||
try
|
||||
{
|
||||
sRtn = GTN.mc.GTN_Open(5, 2);
|
||||
//MessageBox.Show($"运动控制卡初始化成功,返回代码{sRtn}", "信息", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
sRtn = GTN.mc.GTN_InitEcatComm(core);
|
||||
int waitcount = 0;
|
||||
do
|
||||
{
|
||||
sRtn = GTN.mc.GTN_IsEcatReady(core, out eCATStatus);
|
||||
//waitcount++;
|
||||
if (waitcount > 50000)
|
||||
{
|
||||
waitcount = 0;
|
||||
MessageBox.Show("运动控制卡以太网通信初始化失败,请检查连接!", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
} while (eCATStatus != 1 || sRtn != 0);
|
||||
sRtn = GTN.mc.GTN_StartEcatComm(core);
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"运动控制卡初始化失败:{ex.Message},错误代码{sRtn}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 上使能
|
||||
/// </summary>
|
||||
/// <param name="selectedindex">轴号</param>
|
||||
/// <param name="core">核号</param>
|
||||
/// returns>状态码</returns>
|
||||
public int AxisOn(short selectedindex, short core)
|
||||
{
|
||||
int sRtn = -1;
|
||||
sRtn = mc.GTN_ClrSts(core, (short)(selectedindex), 1);
|
||||
sRtn = GTN.mc.GTN_AxisOn(core, selectedindex);
|
||||
return sRtn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 下使能
|
||||
/// </summary>
|
||||
/// <param name="selectedindex">轴号</param>
|
||||
/// <param name="core">核号</param>
|
||||
/// <returns>状态码</returns>
|
||||
public int AxisOff(short selectedindex, short core)
|
||||
{
|
||||
int sRtn = -1;
|
||||
sRtn = GTN.mc.GTN_AxisOff(core, selectedindex);
|
||||
return sRtn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置报警
|
||||
/// </summary>
|
||||
/// <param name="selectedindex">轴号</param>
|
||||
/// <param name="core">核号</param>
|
||||
/// <returns></returns>
|
||||
public int ResetAlarm(short selectedindex, short core)
|
||||
{
|
||||
int sRtn = -1;
|
||||
sRtn = mc.GTN_ClrSts(core, (short)(selectedindex), 1);
|
||||
return sRtn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置位置
|
||||
/// </summary>
|
||||
/// <param name="selectedindex">轴号</param>
|
||||
/// <param name="core">核号</param>
|
||||
/// <returns></returns>
|
||||
public int ResetPosition(short selectedindex, short core)
|
||||
{
|
||||
int sRtn = -1;
|
||||
sRtn = mc.GTN_ZeroPos(core, selectedindex, 1);
|
||||
return sRtn;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 读取轴的状态
|
||||
/// </summary>
|
||||
/// <param name="core">核号</param>
|
||||
/// <param name="axis">轴号</param>
|
||||
/// <param name="AxisSts">轴状态</param>
|
||||
/// <returns></returns>
|
||||
public int ReadAxisStatus(short core, short axis, out string AxisSts)
|
||||
{
|
||||
int sRtn = -1;
|
||||
Int32 psts;
|
||||
UInt32 clock;
|
||||
AxisSts = "";
|
||||
// 读取轴状态
|
||||
sRtn = mc.GTN_GetSts(core, axis, out psts, 1, out clock);
|
||||
|
||||
if ((psts & 0x2) != 0)
|
||||
{
|
||||
AxisSts += "报警\r\n";
|
||||
}
|
||||
if ((psts & 0x200) != 0)
|
||||
{
|
||||
AxisSts += "使能\r\n";
|
||||
}
|
||||
if ((psts & 0x40) != 0)
|
||||
{
|
||||
AxisSts += "负限位触发\r\n";
|
||||
}
|
||||
if ((psts & 0x20) != 0)
|
||||
{
|
||||
AxisSts += "正限位触发\r\n";
|
||||
}
|
||||
if ((psts & 0x400) != 0)
|
||||
{
|
||||
AxisSts += "正在移动\r\n";
|
||||
}
|
||||
if ((psts & 0x800) != 0)
|
||||
{
|
||||
AxisSts += "到位\r\n";
|
||||
}
|
||||
if ((psts & 0x10) != 0)
|
||||
{
|
||||
AxisSts += "跟随误差超限\r\n";
|
||||
}
|
||||
if ((psts & 0x80) != 0)
|
||||
{
|
||||
AxisSts += "平滑停止IO触发\r\n";
|
||||
}
|
||||
if ((psts & 0x100) != 0)
|
||||
{
|
||||
AxisSts += "急停触发\r\n";
|
||||
}
|
||||
|
||||
|
||||
|
||||
return sRtn;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Jog开始
|
||||
/// </summary>
|
||||
/// <param name="core">核号</param>
|
||||
/// <param name="axis">轴号</param>
|
||||
/// <returns></returns>
|
||||
public int JogMove(short core, short axis,int rpm)
|
||||
{
|
||||
int sRtn = -1;
|
||||
double speed;
|
||||
Int32 mask;
|
||||
// 设置轴为点动模式
|
||||
sRtn = mc.GTN_PrfJog(core, axis);
|
||||
if (sRtn != mc.CMD_SUCCESS) return sRtn;
|
||||
|
||||
// 将参数写死
|
||||
mc.TJogPrm jog = new mc.TJogPrm
|
||||
{
|
||||
acc = 10, // 加速度
|
||||
dec = 10, // 减速度
|
||||
smooth = 0.0 // 平滑时间
|
||||
};
|
||||
speed = rpm*0.0667;
|
||||
// 传入结构体引用
|
||||
sRtn = mc.GTN_SetJogPrm(core, axis, ref jog);
|
||||
if (sRtn != mc.CMD_SUCCESS) return sRtn;
|
||||
// 设置点动速度
|
||||
sRtn = mc.GTN_SetVel(1, axis, speed);
|
||||
// 开始点动
|
||||
mask= (int)AxisAndCoreToMask(core, axis);
|
||||
sRtn =mc.GTN_Update(core,mask);
|
||||
return sRtn;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 停止运动
|
||||
/// </summary>
|
||||
/// <param name="core">核号</param>
|
||||
/// <param name="axis">轴号</param>
|
||||
/// <returns></returns>
|
||||
public int AxisStop(short core, short axis)
|
||||
{
|
||||
int sRtn = -1;
|
||||
Int32 mask;
|
||||
Int32 option=0;
|
||||
// 停止点动
|
||||
mask = (int)AxisAndCoreToMask(core, axis);
|
||||
sRtn = mc.GTN_Stop(core, mask,option);
|
||||
return sRtn;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 定时旋转
|
||||
/// </summary>
|
||||
/// <param name="core">核号</param>
|
||||
/// <param name="axis">轴号</param>
|
||||
/// <param name="rpm">速度</param>
|
||||
/// <param name="time">时间</param>
|
||||
/// <returns></returns>
|
||||
|
||||
public int TimelyRotate(short core,short axis,int rpm,int time)
|
||||
{
|
||||
int sRtn = -1;
|
||||
sRtn = mc.GTN_PrfTrap(core, axis);
|
||||
|
||||
|
||||
mc.TTrapPrm trap = new mc.TTrapPrm
|
||||
{
|
||||
acc = 10, // 加速度
|
||||
dec = 10, // 减速度
|
||||
velStart = 0.0, // 平滑时间
|
||||
smoothTime = 10
|
||||
};
|
||||
sRtn = mc.GTN_SetTrapPrm(core, axis, ref trap);
|
||||
sRtn=mc.GTN_SetPos(core, axis, (int)((double)rpm * 0.0667*time*1000));
|
||||
sRtn=mc.GTN_SetVel(core,axis, (double)rpm*0.0667);
|
||||
sRtn=mc.GTN_Update(core, (int)AxisAndCoreToMask(core, axis));
|
||||
|
||||
|
||||
return sRtn;
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 关闭控制卡
|
||||
/// </summary>
|
||||
/// <param name="core">核号</param>
|
||||
public void CloseMotionCard(short core)
|
||||
{
|
||||
// 关闭运动控制卡
|
||||
int sRtn = -1;
|
||||
try
|
||||
{
|
||||
sRtn = mc.GTN_TerminateEcatComm(core);
|
||||
sRtn = GTN.mc.GTN_Close();
|
||||
//MessageBox.Show($"运动控制卡关闭成功,返回代码{sRtn}", "信息", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"运动控制卡关闭失败:{ex.Message},错误代码{sRtn}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将给定的 core(核号) 与 axis(轴号) 转换为 32 位掩码 (mask)。
|
||||
/// 规则:
|
||||
/// - core=1 映射轴 1..32 到 mask 的位 0..31(轴1 -> bit0,轴32 -> bit31)
|
||||
/// - core=2 映射轴 33..64 到 mask 的位 0..31(轴33 -> bit0,轴64 -> bit31)
|
||||
/// 返回值为 UInt32 掩码;当输入不合法时抛出 <see cref="ArgumentOutOfRangeException"/>。
|
||||
/// </summary>
|
||||
/// <param name="core">核号(目前支持 1 或 2)</param>
|
||||
/// <param name="axis">轴号(1..64)</param>
|
||||
/// <returns>对应的 32 位掩码</returns>
|
||||
public static uint AxisAndCoreToMask(short core, short axis)
|
||||
{
|
||||
if (core < 1 || core > 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(core), "目前只支持 core=1 或 core=2。");
|
||||
}
|
||||
|
||||
int baseAxis = (core - 1) * 32; // core=1 -> 0, core=2 -> 32
|
||||
if (axis <= baseAxis || axis > baseAxis + 32)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(axis), $"轴号不在 core {core} 的有效范围 ({baseAxis + 1}..{baseAxis + 32})。");
|
||||
}
|
||||
|
||||
int bitIndex = axis - baseAxis - 1; // 0..31
|
||||
return 1u << bitIndex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 32 位掩码转换为该 core 下的轴列表。
|
||||
/// 规则同 <see cref="AxisAndCoreToMask"/>。
|
||||
/// 返回的轴号为全局轴号(1..64)。
|
||||
/// </summary>
|
||||
/// <param name="core">核号(1 或 2)</param>
|
||||
/// <param name="mask">32 位掩码,bit0 对应该 core 的最低号轴</param>
|
||||
/// <returns>被选中轴的列表(按升序),掩码为0时返回空列表。</returns>
|
||||
public static List<short> MaskToAxisList(short core, uint mask)
|
||||
{
|
||||
var axes = new List<short>();
|
||||
|
||||
if (core < 1 || core > 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(core), "目前只支持 core=1 或 core=2。");
|
||||
}
|
||||
|
||||
int baseAxis = (core - 1) * 32; // core=1 -> 0, core=2 -> 32
|
||||
|
||||
for (int bit = 0; bit < 32; bit++)
|
||||
{
|
||||
if ((mask & (1u << bit)) != 0)
|
||||
{
|
||||
short axis = (short)(baseAxis + bit + 1);
|
||||
axes.Add(axis);
|
||||
}
|
||||
}
|
||||
|
||||
return axes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将 axis 和 core 转换为 mask,转换成功返回 true 并输出 mask;失败返回 false 并输出 0。
|
||||
/// 该方法便于在不抛出异常的情况下做验证。
|
||||
/// </summary>
|
||||
/// <param name="core">核号</param>
|
||||
/// <param name="axis">轴号</param>
|
||||
/// <param name="mask">输出掩码</param>
|
||||
/// <returns>是否成功</returns>
|
||||
public static bool TryAxisAndCoreToMask(short core, short axis, out uint mask)
|
||||
{
|
||||
mask = 0;
|
||||
if (core < 1 || core > 2) return false;
|
||||
int baseAxis = (core - 1) * 32;
|
||||
if (axis <= baseAxis || axis > baseAxis + 32) return false;
|
||||
int bitIndex = axis - baseAxis - 1;
|
||||
mask = 1u << bitIndex;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
171
OneClick/CameraService.cs
Normal file
171
OneClick/CameraService.cs
Normal file
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Basler.Pylon;
|
||||
|
||||
namespace OneClick
|
||||
{
|
||||
public interface ICameraService
|
||||
{
|
||||
IList<string> EnumerateDevices();
|
||||
void Open(string deviceId);
|
||||
void Close();
|
||||
bool StartGrabbing(bool continuous = true);
|
||||
void StopGrabbing();
|
||||
IGrabResult? GrabOnePic(int timeoutMs = 2000);
|
||||
string? CurrentDeviceId { get; }
|
||||
event EventHandler? GrabStarted;
|
||||
event EventHandler<ImageGrabbedEventArgs>? ImageGrabbed;
|
||||
event EventHandler<GrabStopEventArgs>? GrabStopped;
|
||||
event EventHandler? ConnectionLost;
|
||||
}
|
||||
|
||||
public class BaslerCameraService : ICameraService
|
||||
{
|
||||
private Camera? _camera;
|
||||
private readonly PixelDataConverter _converter = new PixelDataConverter();
|
||||
|
||||
public string? CurrentDeviceId { get; private set; }
|
||||
|
||||
public event EventHandler? GrabStarted;
|
||||
public event EventHandler<ImageGrabbedEventArgs>? ImageGrabbed;
|
||||
public event EventHandler<GrabStopEventArgs>? GrabStopped;
|
||||
public event EventHandler? ConnectionLost;
|
||||
|
||||
public IList<string> EnumerateDevices()
|
||||
{
|
||||
var devices = CameraFinder.Enumerate();
|
||||
var ids = new List<string>();
|
||||
foreach (var info in devices)
|
||||
{
|
||||
if (info.ContainsKey(CameraInfoKey.SerialNumber))
|
||||
{
|
||||
ids.Add(info[CameraInfoKey.SerialNumber]);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
public void Open(string deviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId))
|
||||
throw new ArgumentException("<22>豸IDΪ<44>ա<EFBFBD>", nameof(deviceId));
|
||||
|
||||
Close();
|
||||
|
||||
_camera = new Camera(deviceId);
|
||||
|
||||
// ע<><D7A2><EFBFBD>¼<EFBFBD>
|
||||
_camera.ConnectionLost += Camera_ConnectionLost;
|
||||
_camera.StreamGrabber.GrabStarted += StreamGrabber_GrabStarted;
|
||||
_camera.StreamGrabber.ImageGrabbed += StreamGrabber_ImageGrabbed;
|
||||
_camera.StreamGrabber.GrabStopped += StreamGrabber_GrabStopped;
|
||||
|
||||
_camera.Open();
|
||||
CurrentDeviceId = deviceId;
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_camera != null)
|
||||
{
|
||||
if (_camera.StreamGrabber.IsGrabbing)
|
||||
_camera.StreamGrabber.Stop();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ֹͣʧ<D6B9>ܲ<EFBFBD>Ӱ<EFBFBD><D3B0><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ر<EFBFBD>
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_camera != null)
|
||||
{
|
||||
// ע<><D7A2><EFBFBD>¼<EFBFBD>
|
||||
_camera.ConnectionLost -= Camera_ConnectionLost;
|
||||
_camera.StreamGrabber.GrabStarted -= StreamGrabber_GrabStarted;
|
||||
_camera.StreamGrabber.ImageGrabbed -= StreamGrabber_ImageGrabbed;
|
||||
_camera.StreamGrabber.GrabStopped -= StreamGrabber_GrabStopped;
|
||||
|
||||
_camera.Close();
|
||||
_camera.Dispose();
|
||||
_camera = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CurrentDeviceId = null;
|
||||
}
|
||||
}
|
||||
|
||||
public bool StartGrabbing(bool continuous = true)
|
||||
{
|
||||
if (_camera == null)
|
||||
throw new InvalidOperationException("<22><><EFBFBD><EFBFBD>δ<EFBFBD><EFBFBD>");
|
||||
|
||||
try
|
||||
{
|
||||
if (continuous)
|
||||
{
|
||||
Configuration.AcquireContinuous(_camera, null);
|
||||
_camera.StreamGrabber.Start(GrabStrategy.OneByOne, GrabLoop.ProvidedByStreamGrabber);
|
||||
}
|
||||
else
|
||||
{
|
||||
Configuration.AcquireSingleFrame(_camera, null);
|
||||
_camera.StreamGrabber.Start(1, GrabStrategy.OneByOne, GrabLoop.ProvidedByStreamGrabber);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void StopGrabbing()
|
||||
{
|
||||
if (_camera == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_camera.StreamGrabber.Stop();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// <20><><EFBFBD><EFBFBD>ֹͣ<CDA3>쳣
|
||||
}
|
||||
}
|
||||
|
||||
public IGrabResult? GrabOnePic(int timeoutMs = 2000)
|
||||
{
|
||||
if (_camera == null)
|
||||
return null;
|
||||
|
||||
return _camera.StreamGrabber.GrabOne(timeoutMs);
|
||||
}
|
||||
|
||||
private void Camera_ConnectionLost(object? sender, EventArgs e)
|
||||
{
|
||||
ConnectionLost?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void StreamGrabber_GrabStarted(object? sender, EventArgs e)
|
||||
{
|
||||
GrabStarted?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
private void StreamGrabber_ImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
||||
{
|
||||
// ֱ<><D6B1>ת<EFBFBD><D7AA><EFBFBD><EFBFBD><EFBFBD><EFBFBD> UI <20><><EFBFBD><EFBFBD>ʾ<EFBFBD><CABE><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
ImageGrabbed?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void StreamGrabber_GrabStopped(object? sender, GrabStopEventArgs e)
|
||||
{
|
||||
GrabStopped?.Invoke(this, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
420
OneClick/ChangeSetting.xaml
Normal file
420
OneClick/ChangeSetting.xaml
Normal file
@@ -0,0 +1,420 @@
|
||||
<Window
|
||||
x:Class="OneClick.ChangeSetting"
|
||||
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:local="clr-namespace:OneClick"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="更改设置"
|
||||
Width="1000"
|
||||
Height="600"
|
||||
MinWidth="600"
|
||||
MinHeight="400"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
<Grid Margin="12">
|
||||
<Grid.RowDefinitions>
|
||||
<!-- 顶部:配方选择 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<GroupBox
|
||||
Grid.Row="0"
|
||||
Margin="0,0,0,12"
|
||||
Header="配方选择">
|
||||
<Grid Margin="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Label
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="选择配方:" />
|
||||
<ComboBox
|
||||
x:Name="RecipeComboBox"
|
||||
Grid.Column="2"
|
||||
MinWidth="220"
|
||||
IsEditable="False" />
|
||||
<Button
|
||||
x:Name="AddRecipeButton"
|
||||
Grid.Column="4"
|
||||
Padding="12,4"
|
||||
Content="添加配方" />
|
||||
<Button
|
||||
x:Name="DeleteRecipeButton"
|
||||
Grid.Column="6"
|
||||
Padding="12,4"
|
||||
Content="删除配方" />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 左侧:相机/拍照设置 + 光源控制(光源移动到左侧) -->
|
||||
<StackPanel
|
||||
Grid.Column="0"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<GroupBox Margin="0,0,0,8" Header="相机与拍照设置">
|
||||
<Grid Margin="10">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="8" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="8" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 拍照间隔(毫秒) -->
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="拍照间隔:" />
|
||||
<TextBox
|
||||
x:Name="CaptureIntervalTextBox"
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
MinWidth="120"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="输入拍照间隔(毫秒)" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Grid.Column="4"
|
||||
VerticalAlignment="Center"
|
||||
Content="毫秒" />
|
||||
|
||||
<!-- 照片保存位置 + 选择文件夹 -->
|
||||
<Label
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="保存位置:" />
|
||||
<TextBox
|
||||
x:Name="SaveFolderTextBox"
|
||||
Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
MinWidth="100"
|
||||
Margin="0,0,0,0"
|
||||
VerticalAlignment="Center" />
|
||||
<Button
|
||||
x:Name="BrowseFolderButton"
|
||||
Grid.Row="4"
|
||||
Grid.Column="4"
|
||||
Padding="12,4"
|
||||
Content="选择..." />
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 光源控制:已移到左侧 -->
|
||||
<GroupBox Margin="0,0,0,8" Header="光源控制">
|
||||
<Grid Margin="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="8" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="8" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="8" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<CheckBox
|
||||
x:Name="LightEnableCheckBox"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
VerticalAlignment="Center"
|
||||
Content="启用光源(暂时无效)" />
|
||||
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="亮度:" />
|
||||
<Slider
|
||||
x:Name="LightIntensitySlider"
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
IsSnapToTickEnabled="False"
|
||||
Maximum="100"
|
||||
Minimum="0"
|
||||
TickFrequency="10"
|
||||
ToolTip="调整光源亮度(百分比)"
|
||||
Value="50" />
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal">
|
||||
<Label VerticalAlignment="Center" Content="模式:" />
|
||||
<ComboBox x:Name="LightModeComboBox" MinWidth="140">
|
||||
<ComboBoxItem Content="持续" />
|
||||
<ComboBoxItem Content="脉冲" />
|
||||
<ComboBoxItem Content="同步触发" />
|
||||
</ComboBox>
|
||||
|
||||
<!-- 测试光源链接按钮 -->
|
||||
<Button
|
||||
x:Name="TestLightButton"
|
||||
Width="110"
|
||||
Margin="8,0,0,0"
|
||||
Padding="8,4"
|
||||
VerticalAlignment="Center"
|
||||
Click="TestLightButton_Click"
|
||||
Content="测试光源链接" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 光源指令输入行 -->
|
||||
<StackPanel
|
||||
Grid.Row="6"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="3"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal">
|
||||
<Label VerticalAlignment="Center" Content="指令:" />
|
||||
<TextBox
|
||||
x:Name="LightCommandTextBox"
|
||||
MinWidth="150"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="在此输入要发送给光源控制器的原始指令(例如 SA0100#)" />
|
||||
<Button
|
||||
x:Name="SendLightCommandButton"
|
||||
Width="90"
|
||||
Margin="8,0,0,0"
|
||||
Padding="8,4"
|
||||
VerticalAlignment="Center"
|
||||
Click="SendLightCommandButton_Click"
|
||||
Content="发送指令" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
|
||||
<!-- 保持左侧与右侧高度一致的占位(可留空) -->
|
||||
<Border Height="8" Background="Transparent" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 右侧:仅电机参数和控制(移除位置/应用/回原点,新增使能、清警、Jog按钮) -->
|
||||
<GroupBox
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
Header="电机参数和控制">
|
||||
<Grid Margin="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="8" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 转速行 (0) -->
|
||||
<RowDefinition Height="8" /> <!-- 1 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 转动时间行 (2) -->
|
||||
<RowDefinition Height="8" /> <!-- 3 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 轴选择行 (4) -->
|
||||
<RowDefinition Height="8" /> <!-- 5 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 滑块行 (6) -->
|
||||
<RowDefinition Height="12" /> <!-- 7 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 使能/去使能/清警 (8) -->
|
||||
<RowDefinition Height="8" /> <!-- 9 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- Jog 按钮 (10) -->
|
||||
<RowDefinition Height="8" /> <!-- 11 -->
|
||||
<RowDefinition Height="Auto" />
|
||||
<!-- 开始停止旋转(保留) (12) -->
|
||||
<RowDefinition Height="*" /> <!-- 13 -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Label
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="转速:" />
|
||||
<TextBox
|
||||
x:Name="MotorSpeedTextBox"
|
||||
Grid.Row="0"
|
||||
Grid.Column="2"
|
||||
MinWidth="120"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="输入电机转速(RPM)" />
|
||||
<Label
|
||||
Grid.Row="0"
|
||||
Grid.Column="4"
|
||||
VerticalAlignment="Center"
|
||||
Content="RPM" />
|
||||
|
||||
<!-- 新增:转动时间输入(毫秒) -->
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="转动时间:" />
|
||||
<TextBox
|
||||
x:Name="RotationTimeTextBox"
|
||||
|
||||
Grid.Row="2"
|
||||
Grid.Column="2"
|
||||
MinWidth="120"
|
||||
VerticalAlignment="Center"
|
||||
ToolTip="输入转动时间(秒)" />
|
||||
<Label
|
||||
Grid.Row="2"
|
||||
Grid.Column="4"
|
||||
VerticalAlignment="Center"
|
||||
Content="秒" />
|
||||
|
||||
<Label
|
||||
Grid.Row="4"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Content="选择轴:" />
|
||||
<ComboBox
|
||||
x:Name="AxisComboBox"
|
||||
Grid.Row="4"
|
||||
Grid.Column="2"
|
||||
MinWidth="120"
|
||||
IsEditable="False"
|
||||
ToolTip="在此输入或选择轴编号(例如 A1 / 轴1 / 1),后台自行处理" />
|
||||
|
||||
<Slider
|
||||
x:Name="MotorSpeedSlider"
|
||||
Grid.Row="6"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5"
|
||||
IsSnapToTickEnabled="False"
|
||||
Maximum="7000"
|
||||
Minimum="0"
|
||||
TickFrequency="500"
|
||||
ToolTip="滑动以快速调整转速(RPM)" />
|
||||
|
||||
<!-- 新增:选定轴上使能 / 去使能 / 清除警报和位置 -->
|
||||
<StackPanel
|
||||
Grid.Row="8"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5"
|
||||
Margin="0,6,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="EnableAxisButton"
|
||||
Width="130"
|
||||
Height="28"
|
||||
Margin="5"
|
||||
Content="选定轴上使能" Click="EnableAxisButton_Click" />
|
||||
<Button
|
||||
x:Name="DisableAxisButton"
|
||||
Width="130"
|
||||
Height="28"
|
||||
Margin="5"
|
||||
Content="选定轴去使能" Click="DisableAxisButton_Click" />
|
||||
<Button
|
||||
x:Name="ClearAlarmPositionButton"
|
||||
Width="150"
|
||||
Height="28"
|
||||
Margin="5"
|
||||
Content="清除警报和位置" Click="ClearAlarmPositionButton_Click" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 新增:Jog 正/反转(保持按住触发逻辑在后端实现) -->
|
||||
<StackPanel
|
||||
Grid.Row="10"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="JogForwardButton"
|
||||
Width="140"
|
||||
Height="36"
|
||||
Margin="10"
|
||||
Content="Jog运动正转" MouseDown="JogForwardButton_MouseDown" MouseUp="JogForwardButton_MouseUp" MouseLeftButtonDown="JogForwardButton_MouseLeftButtonDown"/>
|
||||
<Button
|
||||
x:Name="JogReverseButton"
|
||||
Width="140"
|
||||
Height="36"
|
||||
Margin="10"
|
||||
Content="Jog运动反转" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 保留开始/停止旋转按钮 -->
|
||||
<StackPanel
|
||||
Grid.Row="12"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="5"
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="StartRotationButton"
|
||||
Width="110"
|
||||
Height="28"
|
||||
Margin="0,0,10,0"
|
||||
Click="StartRotationButton_Click"
|
||||
Content="开始旋转" />
|
||||
<Button
|
||||
x:Name="StopRotationButton"
|
||||
Width="110"
|
||||
Height="28"
|
||||
Click="StopRotationButton_Click"
|
||||
Content="停止旋转" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</GroupBox>
|
||||
</Grid>
|
||||
|
||||
<!-- 底部操作按钮 -->
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0,12,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
x:Name="SaveButton"
|
||||
Margin="0,0,8,0"
|
||||
Padding="16,6"
|
||||
Content="保存"
|
||||
IsDefault="True" />
|
||||
<Button
|
||||
x:Name="CancelButton"
|
||||
Padding="16,6"
|
||||
Content="取消"
|
||||
IsCancel="True" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
636
OneClick/ChangeSetting.xaml.cs
Normal file
636
OneClick/ChangeSetting.xaml.cs
Normal file
@@ -0,0 +1,636 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace OneClick
|
||||
{
|
||||
/// <summary>
|
||||
/// ChangeSetting.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class ChangeSetting : Window
|
||||
{
|
||||
private readonly RecipeManager _recipeManager;
|
||||
private Recipe? _workingRecipe;
|
||||
|
||||
// 新的构造函数:注入外部 RecipeManager 实例
|
||||
public ChangeSetting(RecipeManager recipeManager)
|
||||
{
|
||||
_recipeManager = recipeManager ?? throw new ArgumentNullException(nameof(recipeManager));
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
// 事件绑定
|
||||
AddRecipeButton.Click += AddRecipeButton_Click;
|
||||
DeleteRecipeButton.Click += DeleteRecipeButton_Click;
|
||||
BrowseFolderButton.Click += BrowseFolderButton_Click;
|
||||
SaveButton.Click += SaveButton_Click;
|
||||
|
||||
RecipeComboBox.SelectionChanged += RecipeComboBox_SelectionChanged;
|
||||
MotorSpeedSlider.ValueChanged += MotorSpeedSlider_ValueChanged;
|
||||
LightIntensitySlider.ValueChanged += LightIntensitySlider_ValueChanged;
|
||||
AxisComboBox.SelectionChanged += AxisComboBox_SelectionChanged;
|
||||
|
||||
LoadRecipeNamesToCombo();
|
||||
InitializeAxisComboItems();
|
||||
InitializeWorkingRecipe();
|
||||
LoadWorkingRecipeToUI();
|
||||
}
|
||||
|
||||
private void LoadRecipeNamesToCombo()
|
||||
{
|
||||
RecipeComboBox.ItemsSource = _recipeManager.GetNames().OrderBy(x => x).ToList();
|
||||
if (_recipeManager.CurrentRecipe != null)
|
||||
{
|
||||
RecipeComboBox.SelectedItem = _recipeManager.CurrentRecipe.Name;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeAxisComboItems()
|
||||
{
|
||||
// 依据实际项目轴数填充,这里示例填充 1-8
|
||||
AxisComboBox.ItemsSource = Enumerable.Range(1, 8).ToList();
|
||||
}
|
||||
|
||||
private void InitializeWorkingRecipe()
|
||||
{
|
||||
if (_recipeManager.CurrentRecipe != null)
|
||||
{
|
||||
_workingRecipe = CloneRecipe(_recipeManager.CurrentRecipe);
|
||||
return;
|
||||
}
|
||||
|
||||
// 若无当前配方,创建一个默认配方
|
||||
_workingRecipe = new Recipe
|
||||
{
|
||||
Name = "Default",
|
||||
CaptureIntervalMs = 1000,
|
||||
SaveFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "OneClick"),
|
||||
MotorSpeed = 1000,
|
||||
SelectedAxis = 1,
|
||||
LightIntensity = 50,
|
||||
MotorRunTimeSec = 10
|
||||
};
|
||||
|
||||
if (!_recipeManager.GetNames().Contains("Default"))
|
||||
{
|
||||
_recipeManager.Add(CloneRecipe(_workingRecipe));
|
||||
_recipeManager.SetCurrent("Default");
|
||||
LoadRecipeNamesToCombo();
|
||||
RecipeComboBox.SelectedItem = "Default";
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadWorkingRecipeToUI()
|
||||
{
|
||||
CaptureIntervalTextBox.Text = _workingRecipe?.CaptureIntervalMs?.ToString() ?? string.Empty;
|
||||
SaveFolderTextBox.Text = _workingRecipe?.SaveFolder ?? string.Empty;
|
||||
MotorSpeedTextBox.Text = _workingRecipe?.MotorSpeed?.ToString() ?? string.Empty;
|
||||
MotorSpeedSlider.Value = _workingRecipe?.MotorSpeed.HasValue == true ? _workingRecipe!.MotorSpeed!.Value : 0;
|
||||
|
||||
// 光源亮度
|
||||
LightIntensitySlider.Value = _workingRecipe?.LightIntensity.HasValue == true ? _workingRecipe!.LightIntensity!.Value : 0;
|
||||
|
||||
// 选择轴
|
||||
if (_workingRecipe?.SelectedAxis.HasValue == true)
|
||||
{
|
||||
AxisComboBox.SelectedItem = _workingRecipe!.SelectedAxis!.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
AxisComboBox.SelectedItem = null;
|
||||
}
|
||||
|
||||
// 转动时间(毫秒)
|
||||
RotationTimeTextBox.Text = _workingRecipe?.MotorRunTimeSec?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
private static Recipe CloneRecipe(Recipe r)
|
||||
{
|
||||
return new Recipe
|
||||
{
|
||||
Name = r.Name,
|
||||
CaptureIntervalMs = r.CaptureIntervalMs,
|
||||
SaveFolder = r.SaveFolder,
|
||||
MotorSpeed = r.MotorSpeed,
|
||||
// 包含新参数
|
||||
SelectedAxis = r.SelectedAxis,
|
||||
LightIntensity = r.LightIntensity,
|
||||
MotorRunTimeSec = r.MotorRunTimeSec
|
||||
};
|
||||
}
|
||||
|
||||
private void RecipeComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
var name = RecipeComboBox.SelectedItem as string;
|
||||
if (string.IsNullOrWhiteSpace(name)) return;
|
||||
|
||||
var recipe = _recipeManager.Get(name);
|
||||
if (recipe == null) return;
|
||||
|
||||
_workingRecipe = CloneRecipe(recipe);
|
||||
LoadWorkingRecipeToUI();
|
||||
}
|
||||
|
||||
private void AddRecipeButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// 生成一个建议名称(保持与之前逻辑一致)
|
||||
var baseName = "NewRecipe";
|
||||
var idx = 1;
|
||||
string suggested;
|
||||
do
|
||||
{
|
||||
suggested = $"{baseName}{idx}";
|
||||
idx++;
|
||||
} while (_recipeManager.GetNames().Contains(suggested));
|
||||
|
||||
// 循环弹出输入框,直到用户取消或输入合法且不重复的名称
|
||||
while (true)
|
||||
{
|
||||
var input = PromptForRecipeName(suggested);
|
||||
if (input == null)
|
||||
return; // 用户取消
|
||||
|
||||
input = input.Trim();
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
MessageBox.Show(this, "配方名称不能为空,请重新输入。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
suggested = input == string.Empty ? suggested : input;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ContainsInvalidFileNameChars(input))
|
||||
{
|
||||
MessageBox.Show(this, "配方名称包含非法字符(例如 \\ / : * ? \" < > | 等),请使用其他名称。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
suggested = input;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_recipeManager.GetNames().Contains(input))
|
||||
{
|
||||
MessageBox.Show(this, "该配方名称已存在,请使用不同的名称。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
suggested = input;
|
||||
continue;
|
||||
}
|
||||
|
||||
var newRecipe = new Recipe
|
||||
{
|
||||
Name = input,
|
||||
CaptureIntervalMs = 1000,
|
||||
SaveFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "OneClick"),
|
||||
MotorSpeed = 1000,
|
||||
SelectedAxis = 1,
|
||||
LightIntensity = 50,
|
||||
MotorRunTimeSec = 1000
|
||||
};
|
||||
|
||||
if (_recipeManager.Add(newRecipe))
|
||||
{
|
||||
_recipeManager.SetCurrent(input);
|
||||
_workingRecipe = CloneRecipe(newRecipe);
|
||||
LoadRecipeNamesToCombo();
|
||||
RecipeComboBox.SelectedItem = input;
|
||||
LoadWorkingRecipeToUI();
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 在内存中弹出一个简单的输入对话框,返回 null 表示取消
|
||||
private string? PromptForRecipeName(string defaultName)
|
||||
{
|
||||
var dlg = new Window
|
||||
{
|
||||
Title = "输入配方名称",
|
||||
Owner = this,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
||||
Width = 420,
|
||||
Height = 150,
|
||||
ResizeMode = ResizeMode.NoResize,
|
||||
WindowStyle = WindowStyle.SingleBorderWindow,
|
||||
ShowInTaskbar = false
|
||||
};
|
||||
|
||||
var panel = new Grid
|
||||
{
|
||||
Margin = new Thickness(10)
|
||||
};
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
panel.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = "请输入配方名称:",
|
||||
Margin = new Thickness(0, 0, 0, 6)
|
||||
};
|
||||
Grid.SetRow(label, 0);
|
||||
panel.Children.Add(label);
|
||||
|
||||
var textBox = new TextBox
|
||||
{
|
||||
Text = defaultName ?? string.Empty,
|
||||
MinWidth = 360
|
||||
};
|
||||
Grid.SetRow(textBox, 1);
|
||||
panel.Children.Add(textBox);
|
||||
|
||||
var btnPanel = new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
HorizontalAlignment = HorizontalAlignment.Right,
|
||||
Margin = new Thickness(0, 8, 0, 0)
|
||||
};
|
||||
|
||||
var okBtn = new Button
|
||||
{
|
||||
Content = "确定",
|
||||
Width = 80,
|
||||
IsDefault = true,
|
||||
Margin = new Thickness(0, 0, 8, 0)
|
||||
};
|
||||
var cancelBtn = new Button
|
||||
{
|
||||
Content = "取消",
|
||||
Width = 80,
|
||||
IsCancel = true
|
||||
};
|
||||
|
||||
btnPanel.Children.Add(okBtn);
|
||||
btnPanel.Children.Add(cancelBtn);
|
||||
Grid.SetRow(btnPanel, 2);
|
||||
panel.Children.Add(btnPanel);
|
||||
|
||||
dlg.Content = panel;
|
||||
|
||||
okBtn.Click += (_, _) =>
|
||||
{
|
||||
dlg.DialogResult = true;
|
||||
dlg.Close();
|
||||
};
|
||||
|
||||
cancelBtn.Click += (_, _) =>
|
||||
{
|
||||
dlg.DialogResult = false;
|
||||
dlg.Close();
|
||||
};
|
||||
|
||||
// 聚焦并选择全部文本,便于用户直接输入
|
||||
dlg.Loaded += (_, _) =>
|
||||
{
|
||||
textBox.Focus();
|
||||
textBox.SelectAll();
|
||||
};
|
||||
|
||||
var result = dlg.ShowDialog();
|
||||
if (result == true)
|
||||
return textBox.Text;
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool ContainsInvalidFileNameChars(string name)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
return name.IndexOfAny(invalid) >= 0;
|
||||
}
|
||||
|
||||
private void DeleteRecipeButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var name = RecipeComboBox.SelectedItem as string;
|
||||
if (string.IsNullOrWhiteSpace(name)) return;
|
||||
|
||||
if (_recipeManager.Remove(name))
|
||||
{
|
||||
LoadRecipeNamesToCombo();
|
||||
// 切到任意一个剩余配方
|
||||
var next = _recipeManager.GetNames().OrderBy(x => x).FirstOrDefault();
|
||||
if (next != null)
|
||||
{
|
||||
_recipeManager.SetCurrent(next);
|
||||
_workingRecipe = CloneRecipe(_recipeManager.Get(next)!);
|
||||
RecipeComboBox.SelectedItem = next;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 没有配方,创建默认
|
||||
InitializeWorkingRecipe();
|
||||
LoadRecipeNamesToCombo();
|
||||
}
|
||||
LoadWorkingRecipeToUI();
|
||||
}
|
||||
}
|
||||
|
||||
private void BrowseFolderButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new OpenFolderDialog
|
||||
{
|
||||
Title = "选择图片保存位置"
|
||||
};
|
||||
|
||||
// 根据当前文本框内容设定初始目录
|
||||
var current = SaveFolderTextBox.Text;
|
||||
if (!string.IsNullOrWhiteSpace(current) && Directory.Exists(current))
|
||||
{
|
||||
dlg.InitialDirectory = current;
|
||||
}
|
||||
|
||||
var result = dlg.ShowDialog(this);
|
||||
if (result == true)
|
||||
{
|
||||
SaveFolderTextBox.Text = dlg.FolderName;
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveButton_Click(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
// 将 UI 的值写回工作配方
|
||||
if (string.IsNullOrWhiteSpace(_workingRecipe?.Name))
|
||||
{
|
||||
MessageBox.Show("配方名称不能为空。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
if (int.TryParse(CaptureIntervalTextBox.Text, out var interval))
|
||||
_workingRecipe!.CaptureIntervalMs = interval;
|
||||
else
|
||||
_workingRecipe!.CaptureIntervalMs = null;
|
||||
|
||||
var folder = SaveFolderTextBox.Text;
|
||||
_workingRecipe!.SaveFolder = string.IsNullOrWhiteSpace(folder) ? null : folder;
|
||||
|
||||
if (double.TryParse(MotorSpeedTextBox.Text, out var rpm))
|
||||
_workingRecipe!.MotorSpeed = rpm;
|
||||
else
|
||||
_workingRecipe!.MotorSpeed = null;
|
||||
|
||||
// 新增:保存光源亮度和选择轴号
|
||||
_workingRecipe!.LightIntensity = (int)Math.Round(LightIntensitySlider.Value);
|
||||
var selectedAxisObj = AxisComboBox.SelectedItem;
|
||||
if (selectedAxisObj is int axisNum)
|
||||
{
|
||||
_workingRecipe!.SelectedAxis = axisNum;
|
||||
}
|
||||
else
|
||||
{
|
||||
_workingRecipe!.SelectedAxis = null;
|
||||
}
|
||||
|
||||
// 新增:保存转动时间(秒)
|
||||
if (int.TryParse(RotationTimeTextBox.Text, out var rotMs))
|
||||
_workingRecipe!.MotorRunTimeSec = rotMs;
|
||||
else
|
||||
_workingRecipe!.MotorRunTimeSec = null;
|
||||
|
||||
// 写入 RecipeManager 并设为当前
|
||||
var exists = _recipeManager.Get(_workingRecipe!.Name!) != null;
|
||||
if (exists)
|
||||
_recipeManager.Update(CloneRecipe(_workingRecipe!));
|
||||
else
|
||||
_recipeManager.Add(CloneRecipe(_workingRecipe!));
|
||||
|
||||
_recipeManager.SetCurrent(_workingRecipe!.Name!);
|
||||
_recipeManager.Save();
|
||||
|
||||
// 关闭并返回主界面
|
||||
DialogResult = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void MotorSpeedSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
MotorSpeedTextBox.Text = ((int)e.NewValue).ToString();
|
||||
}
|
||||
|
||||
private void LightIntensitySlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
// 若需要在界面显示当前亮度值,可在此将值显示到 ToolTip 或状态文本
|
||||
// 保留为轻量处理:无需文本框,值直接在保存时取用
|
||||
}
|
||||
|
||||
private void AxisComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
// 轻量:选择变更时暂不立即写回,统一在保存时处理
|
||||
}
|
||||
|
||||
private void TestLightButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var lightService = new LightSourceService();
|
||||
lightService.TestSourceService();
|
||||
}
|
||||
|
||||
private void SendLightCommandButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var cmd = LightCommandTextBox.Text?.Trim();
|
||||
if (string.IsNullOrEmpty(cmd))
|
||||
{
|
||||
MessageBox.Show(this, "请输入要发送的光源指令。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
LightCommandTextBox.Focus();
|
||||
return;
|
||||
}
|
||||
|
||||
var lightService = new LightSourceService();
|
||||
try
|
||||
{
|
||||
var response = lightService.SendCommand(cmd);
|
||||
if (!string.IsNullOrEmpty(response))
|
||||
{
|
||||
MessageBox.Show(this, "收到响应: " + response, "光源响应", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
else
|
||||
{
|
||||
MessageBox.Show(this, "未收到响应或响应为空。", "光源响应", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, "发送命令时发生错误: " + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// 新增的占位事件处理器:开始旋转(后端由你实现)
|
||||
private void StartRotationButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var axisService = new AxisService();
|
||||
int axisNum = 1;
|
||||
int speed = (int)MotorSpeedSlider.Value;
|
||||
int time = 0;
|
||||
var sel = AxisComboBox.SelectedItem;
|
||||
if (sel is int i)
|
||||
{
|
||||
axisNum = i;
|
||||
}
|
||||
else if (sel is string s && int.TryParse(s, out var parsed))
|
||||
{
|
||||
axisNum = parsed;
|
||||
}
|
||||
// 解析 RotationTimeTextBox.Text 为 int
|
||||
if (!int.TryParse(RotationTimeTextBox.Text, out time))
|
||||
{
|
||||
MessageBox.Show(this, "转动时间输入无效,已默认为 0。", "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
time = 0;
|
||||
}
|
||||
axisService.TimelyRotate(1, (short)axisNum, speed, time);
|
||||
//清除位置
|
||||
//axisService.ResetPosition((short)axisNum, 1);
|
||||
}
|
||||
|
||||
// 停止旋转
|
||||
private void StopRotationButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
|
||||
var sel = AxisComboBox.SelectedItem;
|
||||
var axisService = new AxisService();
|
||||
if (sel is int i)
|
||||
{
|
||||
axisService.AxisStop(1, (short)i);
|
||||
//axisService.ResetPosition((short)i, 1);
|
||||
}
|
||||
else if (sel is short s)
|
||||
{
|
||||
axisService.AxisStop(1, s);
|
||||
//axisService.ResetPosition(s, 1);
|
||||
}
|
||||
else if (sel is string str && short.TryParse(str, out var p))
|
||||
{
|
||||
axisService.AxisStop(1, p);
|
||||
//axisService.ResetPosition(p, 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 处理未选择或不可解析的情况
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private void EnableAxisButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var axisService = new AxisService();
|
||||
int axisToEnable;
|
||||
var sel = AxisComboBox.SelectedItem;
|
||||
try
|
||||
{
|
||||
if (sel is int i)
|
||||
{
|
||||
axisToEnable = i;
|
||||
}
|
||||
else if (sel is string s && int.TryParse(s, out var parsed))
|
||||
{
|
||||
axisToEnable = parsed;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
axisToEnable = 1;
|
||||
MessageBox.Show(this, "未选择轴号,默认启用轴 1。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
axisService.AxisOn((short)axisToEnable, 1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, "启用轴时发生错误: " + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void DisableAxisButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var axisService = new AxisService();
|
||||
int axisToDisable;
|
||||
var sel = AxisComboBox.SelectedItem;
|
||||
try
|
||||
{
|
||||
if (sel is int i)
|
||||
{
|
||||
axisToDisable = i;
|
||||
}
|
||||
else if (sel is string s && int.TryParse(s, out var parsed))
|
||||
{
|
||||
axisToDisable = parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
axisToDisable = 1;
|
||||
MessageBox.Show(this, "未选择轴号,默认禁用轴 1。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
axisService.AxisOff((short)axisToDisable, 1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, "禁用轴时发生错误: " + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void ClearAlarmPositionButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var axisService = new AxisService();
|
||||
int axisToClear;
|
||||
var sel = AxisComboBox.SelectedItem;
|
||||
try
|
||||
{
|
||||
if (sel is int i)
|
||||
{
|
||||
axisToClear = i;
|
||||
}
|
||||
else if (sel is string s && int.TryParse(s, out var parsed))
|
||||
{
|
||||
axisToClear = parsed;
|
||||
}
|
||||
else
|
||||
{
|
||||
axisToClear = 1;
|
||||
MessageBox.Show(this, "未选择轴号,默认清除轴 1 的报警位置。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
axisService.ResetAlarm((short)axisToClear, 1);
|
||||
axisService.ResetPosition((short)axisToClear, 1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, "清除报警位置时发生错误: " + ex.Message, "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void JogForwardButton_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
|
||||
var sel = AxisComboBox.SelectedItem;
|
||||
var speed = (int)MotorSpeedSlider.Value;
|
||||
var axisService = new AxisService();
|
||||
axisService.JogMove(1, (short)sel,speed);
|
||||
|
||||
}
|
||||
|
||||
private void JogForwardButton_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
var sel = AxisComboBox.SelectedItem;
|
||||
var axisService = new AxisService();
|
||||
if (sel is int i)
|
||||
{
|
||||
axisService.AxisStop(1, (short)i);
|
||||
}
|
||||
else if (sel is short s)
|
||||
{
|
||||
axisService.AxisStop(1, s);
|
||||
}
|
||||
else if (sel is string str && short.TryParse(str, out var p))
|
||||
{
|
||||
axisService.AxisStop(1, p);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 处理未选择或不可解析的情况
|
||||
}
|
||||
}
|
||||
|
||||
private void JogForwardButton_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
101
OneClick/LightSourceService.cs
Normal file
101
OneClick/LightSourceService.cs
Normal file
@@ -0,0 +1,101 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
|
||||
namespace OneClick
|
||||
{
|
||||
class LightSourceService
|
||||
{
|
||||
private static TcpClient? OpenSocket()
|
||||
{
|
||||
try
|
||||
{
|
||||
TcpClient tcpclient = new TcpClient();
|
||||
tcpclient.Connect("192.168.0.7", 1234);
|
||||
return tcpclient;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("光源连接失败: " + ex.Message, "连接错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int TestSourceService()
|
||||
{
|
||||
// 使用通用发送方法测试固定命令
|
||||
var response = SendCommand("SA0000#");
|
||||
if (!string.IsNullOrEmpty(response) && response.StartsWith("A", StringComparison.Ordinal))
|
||||
{
|
||||
//MessageBox.Show("光源通信成功!收到响应: " + response, "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return 0;
|
||||
}
|
||||
else if (response != null)
|
||||
{
|
||||
MessageBox.Show("光源响应异常,收到: " + response, "提示", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送指定的原始指令到光源控制器,并尝试读取响应。
|
||||
/// 调用者根据返回值判断是否通信成功;方法内部会在连接失败时弹窗提示错误。
|
||||
/// </summary>
|
||||
/// <param name="command">要发送的命令(请包含必要的结束符,例如 #)</param>
|
||||
/// <param name="timeoutMs">发送/接收超时(毫秒)</param>
|
||||
/// <returns>收到的响应字符串(可能为空),连接失败或异常时返回 null</returns>
|
||||
public string? SendCommand(string command, int timeoutMs = 2000)
|
||||
{
|
||||
if (string.IsNullOrEmpty(command))
|
||||
throw new ArgumentException("command 不能为空", nameof(command));
|
||||
|
||||
var tcpclient = OpenSocket();
|
||||
if (tcpclient == null)
|
||||
return null;
|
||||
|
||||
NetworkStream? networkstream = null;
|
||||
try
|
||||
{
|
||||
tcpclient.ReceiveTimeout = timeoutMs;
|
||||
tcpclient.SendTimeout = timeoutMs;
|
||||
|
||||
networkstream = tcpclient.GetStream();
|
||||
|
||||
byte[] sendbytes = Encoding.ASCII.GetBytes(command);
|
||||
networkstream.Write(sendbytes, 0, sendbytes.Length);
|
||||
networkstream.Flush();
|
||||
|
||||
// 读取响应(阻塞,受 ReceiveTimeout 控制)
|
||||
byte[] buffer = new byte[1024];
|
||||
int bytesRead = networkstream.Read(buffer, 0, buffer.Length); // 受 tcpclient.ReceiveTimeout 控制
|
||||
|
||||
if (bytesRead > 0)
|
||||
{
|
||||
string response = Encoding.ASCII.GetString(buffer, 0, bytesRead).Trim();
|
||||
return response;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
catch (SocketException se)
|
||||
{
|
||||
MessageBox.Show("光源通信超时或套接字错误: " + se.Message, "通信错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("光源通信错误: " + ex.Message, "通信错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { networkstream?.Close(); } catch { }
|
||||
try { tcpclient.Close(); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
OneClick/LoadingDialog.xaml
Normal file
37
OneClick/LoadingDialog.xaml
Normal file
@@ -0,0 +1,37 @@
|
||||
<Window
|
||||
x:Class="OneClick.LoadingDialog"
|
||||
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:local="clr-namespace:OneClick"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="LoadingDialog"
|
||||
Width="400"
|
||||
Height="200"
|
||||
AllowsTransparency="False"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
WindowStyle="None"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Border
|
||||
Padding="20"
|
||||
Background="White"
|
||||
BorderBrush="Gray"
|
||||
BorderThickness="1"
|
||||
CornerRadius="10">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<TextBlock
|
||||
x:Name="LoadStatusMessage"
|
||||
Margin="0,0,0,20"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="24"
|
||||
FontWeight="Bold"
|
||||
Text="系统启动中,请等待" />
|
||||
<ProgressBar
|
||||
Width="300"
|
||||
Height="30"
|
||||
IsIndeterminate="True" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
27
OneClick/LoadingDialog.xaml.cs
Normal file
27
OneClick/LoadingDialog.xaml.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace OneClick
|
||||
{
|
||||
/// <summary>
|
||||
/// LoadingDialog.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class LoadingDialog : Window
|
||||
{
|
||||
public LoadingDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
87
OneClick/MainWindow.xaml
Normal file
87
OneClick/MainWindow.xaml
Normal file
@@ -0,0 +1,87 @@
|
||||
<Window
|
||||
x:Class="OneClick.MainWindow"
|
||||
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:ha="http://schemas.mvtec.com/halcondotnet"
|
||||
xmlns:local="clr-namespace:OneClick"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
Title="MainWindow"
|
||||
Width="800"
|
||||
Height="450"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid Grid.Column="0">
|
||||
<ha:HSmartWindowControlWPF x:Name="ImageWindow" />
|
||||
</Grid>
|
||||
<StackPanel Grid.Column="1" Margin="10">
|
||||
<TextBlock
|
||||
Margin="0,20,0,20"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Text="更改设置" />
|
||||
<StackPanel
|
||||
Margin="0,0,0,10"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="选择配方:" />
|
||||
<ComboBox x:Name="SettingsListBox" Width="120" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- 新增:选择相机与重新扫描按钮 -->
|
||||
<StackPanel
|
||||
Margin="0,0,0,10"
|
||||
HorizontalAlignment="Center"
|
||||
Orientation="Horizontal">
|
||||
<TextBlock
|
||||
Margin="0,0,10,0"
|
||||
VerticalAlignment="Center"
|
||||
Text="选择相机:" />
|
||||
<ComboBox x:Name="CameraComboBox" Width="160" />
|
||||
<Button
|
||||
x:Name="RescanButton"
|
||||
Width="80"
|
||||
Height="25"
|
||||
Margin="10,0,0,0"
|
||||
Content="重新扫描" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
|
||||
<Button
|
||||
Width="100"
|
||||
Height="25"
|
||||
Margin="0,10,0,0"
|
||||
Click="ChangeSettingButton_Click"
|
||||
Content="更改设置" />
|
||||
<Button
|
||||
Width="100"
|
||||
Height="25"
|
||||
Margin="10,10,0,0"
|
||||
Click="ResetSettingButton_Click"
|
||||
Content="重置设置" />
|
||||
<Button
|
||||
Width="100"
|
||||
Height="25"
|
||||
Margin="10,10,0,0"
|
||||
Click="StartButton_Click"
|
||||
Content="开始" />
|
||||
</StackPanel>
|
||||
<Button
|
||||
x:Name="CameraTest"
|
||||
Height="25"
|
||||
Margin="30"
|
||||
Click="CameraTest_Click"
|
||||
Content="停止" />
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
593
OneClick/MainWindow.xaml.cs
Normal file
593
OneClick/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,593 @@
|
||||
using Basler.Pylon;
|
||||
using HalconDotNet;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
using GTN; // 示例运动控制卡命名空间
|
||||
|
||||
namespace OneClick
|
||||
{
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly RecipeManager _recipeManager;
|
||||
private readonly ICameraService _cameraService;
|
||||
private HImage? image;
|
||||
|
||||
// 启动或刷新列表时抑制相机选择事件
|
||||
private bool _suppressCameraSelectionChanged;
|
||||
private readonly Stopwatch _displayStopwatch = new Stopwatch();
|
||||
private readonly Dispatcher _uiDispatcher;
|
||||
|
||||
// 间隔保存相关
|
||||
private DispatcherTimer? _captureTimer;
|
||||
private int _captureIndex;
|
||||
private string? _currentSaveFolder;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
var dataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "OneClick");
|
||||
var storePath = Path.Combine(dataDir, "recipes.json");
|
||||
|
||||
_recipeManager = new RecipeManager(storePath);
|
||||
_cameraService = new BaslerCameraService();
|
||||
|
||||
Loaded += MainWindow_Loaded;
|
||||
RescanButton.Click += RescanButton_Click;
|
||||
CameraComboBox.SelectionChanged += CameraComboBox_SelectionChanged;
|
||||
Closing += MainWindow_Closing;
|
||||
|
||||
_uiDispatcher = Dispatcher.CurrentDispatcher;
|
||||
|
||||
// 订阅相机服务事件
|
||||
_cameraService.GrabStarted += CameraService_GrabStarted;
|
||||
_cameraService.ImageGrabbed += CameraService_ImageGrabbed;
|
||||
_cameraService.GrabStopped += CameraService_GrabStopped;
|
||||
_cameraService.ConnectionLost += CameraService_ConnectionLost;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 载入配方列表到主界面下拉框
|
||||
SettingsListBox.ItemsSource = _recipeManager.GetNames().OrderBy(x => x).ToList();
|
||||
if (_recipeManager.CurrentRecipe != null)
|
||||
{
|
||||
SettingsListBox.SelectedItem = _recipeManager.CurrentRecipe.Name;
|
||||
}
|
||||
|
||||
// 显示启动对话框
|
||||
var loadingDialog = new LoadingDialog
|
||||
{
|
||||
Owner = this,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner
|
||||
};
|
||||
loadingDialog.Show();
|
||||
|
||||
// 异步枚举相机并在 UI 线程更新列表(避免 UI 卡顿)
|
||||
try
|
||||
{
|
||||
loadingDialog.LoadStatusMessage.Text = "正在初始化运动控制卡...";
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var axisService = new AxisService();
|
||||
axisService.InitMotionCard();
|
||||
Thread.Sleep(500); // 模拟初始化延迟
|
||||
});
|
||||
|
||||
loadingDialog.LoadStatusMessage.Text = "正在启动光源...";
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var lightservice = new LightSourceService();
|
||||
lightservice.TestSourceService();
|
||||
Thread.Sleep(500); // 模拟初始化延迟
|
||||
});
|
||||
|
||||
|
||||
loadingDialog.LoadStatusMessage.Text = "正在启动相机...";
|
||||
var devices = await Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _cameraService.EnumerateDevices();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
});
|
||||
|
||||
_uiDispatcher.Invoke(() =>
|
||||
{
|
||||
_suppressCameraSelectionChanged = true;
|
||||
try
|
||||
{
|
||||
var list = devices.ToList();
|
||||
CameraComboBox.ItemsSource = list;
|
||||
CameraComboBox.SelectedIndex = list.Count > 0 ? 0 : -1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressCameraSelectionChanged = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"枚举相机失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 隐藏启动对话框
|
||||
_uiDispatcher.BeginInvoke(new Action(() => loadingDialog.Close()));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var firstId = _cameraService.EnumerateDevices().FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(firstId))
|
||||
{
|
||||
_cameraService.Open(firstId);
|
||||
_cameraService.Close();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"相机初始化失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshCameraList()
|
||||
{
|
||||
// 枚举相机设备
|
||||
var devices = _cameraService.EnumerateDevices();
|
||||
|
||||
// 在更新列表和默认选择期间抑制 SelectionChanged 事件
|
||||
_suppressCameraSelectionChanged = true;
|
||||
try
|
||||
{
|
||||
CameraComboBox.ItemsSource = devices;
|
||||
CameraComboBox.SelectedIndex = devices.Count > 0 ? 0 : -1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressCameraSelectionChanged = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void RescanButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
RefreshCameraList();
|
||||
}
|
||||
|
||||
private void CameraComboBox_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppressCameraSelectionChanged) return;
|
||||
|
||||
var id = CameraComboBox.SelectedItem as string;
|
||||
if (string.IsNullOrWhiteSpace(id)) return;
|
||||
|
||||
try
|
||||
{
|
||||
_cameraService.StopGrabbing();
|
||||
_cameraService.Close();
|
||||
_cameraService.Open(id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"打开相机失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ChangeSettingButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 通过构造函数注入同一个 RecipeManager 实例
|
||||
var changeSettingWindow = new ChangeSetting(_recipeManager)
|
||||
{
|
||||
Owner = this,
|
||||
WindowStartupLocation = WindowStartupLocation.CenterOwner
|
||||
};
|
||||
|
||||
bool? dialogResult = changeSettingWindow.ShowDialog();
|
||||
if (dialogResult == true)
|
||||
{
|
||||
// 直接从相同的 _recipeManager 刷新 UI(内存状态已被更新)
|
||||
SettingsListBox.ItemsSource = _recipeManager.GetNames().OrderBy(x => x).ToList();
|
||||
if (_recipeManager.CurrentRecipe != null)
|
||||
{
|
||||
SettingsListBox.SelectedItem = _recipeManager.CurrentRecipe.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetSettingButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// 简单重置当前配方(示例)
|
||||
var currentName = SettingsListBox.SelectedItem as string;
|
||||
if (string.IsNullOrWhiteSpace(currentName))
|
||||
{
|
||||
MessageBox.Show("请先选择一个配方。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var recipe = _recipeManager.Get(currentName);
|
||||
if (recipe == null) return;
|
||||
|
||||
recipe.CaptureIntervalMs = 1000;
|
||||
recipe.SaveFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "OneClick");
|
||||
recipe.MotorSpeed = 1000;
|
||||
|
||||
_recipeManager.Update(recipe);
|
||||
_recipeManager.Save();
|
||||
|
||||
MessageBox.Show("已重置当前配方为默认参数。", "完成", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
|
||||
private async void StartButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var currentName = SettingsListBox.SelectedItem as string;
|
||||
if (string.IsNullOrWhiteSpace(currentName))
|
||||
{
|
||||
MessageBox.Show("请先选择一个配方。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var recipe = _recipeManager.Get(currentName);
|
||||
if (recipe == null)
|
||||
{
|
||||
MessageBox.Show("未找到所选配方。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(recipe.SaveFolder))
|
||||
{
|
||||
MessageBox.Show("保存位置未配置。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(recipe.SaveFolder);
|
||||
|
||||
var id = CameraComboBox.SelectedItem as string;
|
||||
if (string.IsNullOrWhiteSpace(id))
|
||||
{
|
||||
MessageBox.Show("未选择相机。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_cameraService.StopGrabbing();
|
||||
_cameraService.Close();
|
||||
_cameraService.Open(id);
|
||||
|
||||
// 启动持续抓取(参考示例)
|
||||
if (!_cameraService.StartGrabbing(continuous: true))
|
||||
{
|
||||
MessageBox.Show("启动抓取失败。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 启动按配方的间隔保存
|
||||
StartIntervalCapture(recipe);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"启动抓取失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void CameraService_GrabStarted(object? sender, EventArgs e)
|
||||
{
|
||||
_uiDispatcher.Invoke(() =>
|
||||
{
|
||||
_displayStopwatch.Reset();
|
||||
});
|
||||
}
|
||||
|
||||
private void CameraService_ImageGrabbed(object? sender, ImageGrabbedEventArgs e)
|
||||
{
|
||||
// 与示例一致:只显示最新帧,节流到约30FPS
|
||||
if (!_displayStopwatch.IsRunning || _displayStopwatch.ElapsedMilliseconds > 33)
|
||||
{
|
||||
_displayStopwatch.Restart();
|
||||
|
||||
// 由于该事件可能在非 UI 线程,切回 UI 线程
|
||||
_uiDispatcher.Invoke(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var grabResult = e.GrabResult;
|
||||
if (grabResult == null || !grabResult.IsValid || !grabResult.GrabSucceeded) return;
|
||||
|
||||
var hImage = ConvertGrabResultToHalconImage(grabResult);
|
||||
if (hImage == null) return;
|
||||
|
||||
image?.Dispose();
|
||||
image = hImage;
|
||||
|
||||
// 显示到 HSmartWindowControl
|
||||
HOperatorSet.DispImage(image, ImageWindow.HalconWindow);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"显示图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 释放克隆的抓取结果(仿照 WinForms 示例中 e.DisposeGrabResultIfClone)
|
||||
e.DisposeGrabResultIfClone();
|
||||
}
|
||||
|
||||
private void CameraService_GrabStopped(object? sender, GrabStopEventArgs e)
|
||||
{
|
||||
_uiDispatcher.Invoke(() =>
|
||||
{
|
||||
_displayStopwatch.Reset();
|
||||
StopIntervalCapture();
|
||||
|
||||
if (e.Reason != GrabStopReason.UserRequest)
|
||||
{
|
||||
MessageBox.Show($"抓取错误:{e.ErrorMessage}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void CameraService_ConnectionLost(object? sender, EventArgs e)
|
||||
{
|
||||
_uiDispatcher.Invoke(() =>
|
||||
{
|
||||
_cameraService.StopGrabbing();
|
||||
_cameraService.Close();
|
||||
StopIntervalCapture();
|
||||
RefreshCameraList();
|
||||
MessageBox.Show("相机连接丢失。", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
});
|
||||
}
|
||||
|
||||
public HImage? ReadBuffer()
|
||||
{
|
||||
IGrabResult? grabResult = null;
|
||||
try
|
||||
{
|
||||
grabResult = _cameraService.GrabOnePic();
|
||||
if (grabResult == null || !grabResult.GrabSucceeded)
|
||||
return null;
|
||||
|
||||
return ConvertGrabResultToHalconImage(grabResult);
|
||||
}
|
||||
finally
|
||||
{
|
||||
grabResult?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private HImage? ConvertGrabResultToHalconImage(IGrabResult grabResult)
|
||||
{
|
||||
var hImg = new HImage();
|
||||
|
||||
if (IsMonoData(grabResult))
|
||||
{
|
||||
// 对高位深单通道,统一转换到 Mono16 并以 16 位生成 Halcon 图像,避免 8 位截断
|
||||
switch (grabResult.PixelTypeValue)
|
||||
{
|
||||
case PixelType.Mono10:
|
||||
case PixelType.Mono10p:
|
||||
case PixelType.Mono10packed:
|
||||
case PixelType.Mono12:
|
||||
case PixelType.Mono12p:
|
||||
case PixelType.Mono12packed:
|
||||
case PixelType.Mono16:
|
||||
{
|
||||
var converter = new PixelDataConverter { OutputPixelFormat = PixelType.Mono16 };
|
||||
var bytes = new byte[grabResult.Width * grabResult.Height * 2]; // 16位
|
||||
converter.Convert(bytes, grabResult);
|
||||
|
||||
var handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
var ptr = handle.AddrOfPinnedObject();
|
||||
// Halcon 中 16 位无符号类型为 "uint2"
|
||||
hImg.GenImage1("uint2", grabResult.Width, grabResult.Height, ptr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// 纯 8 位单通道,保持 8 位
|
||||
var buffer = grabResult.PixelData as byte[];
|
||||
if (buffer == null || buffer.Length < grabResult.Width * grabResult.Height)
|
||||
return null;
|
||||
|
||||
var handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
var ptr = handle.AddrOfPinnedObject();
|
||||
hImg.GenImage1("byte", grabResult.Width, grabResult.Height, ptr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 彩色:仍使用 RGB8packed(无损容器保存),若需原始 Bayer 可改为保存原始缓冲区而非去马赛克
|
||||
var rgb = new byte[grabResult.Width * grabResult.Height * 3];
|
||||
var converter = new PixelDataConverter { OutputPixelFormat = PixelType.RGB8packed };
|
||||
converter.Convert(rgb, grabResult);
|
||||
|
||||
var handle = GCHandle.Alloc(rgb, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
var ptr = handle.AddrOfPinnedObject();
|
||||
hImg.GenImageInterleaved(ptr, "rgb", grabResult.Width, grabResult.Height, 0, "byte",
|
||||
grabResult.Width, grabResult.Height, 0, 0, -1, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
return hImg;
|
||||
}
|
||||
|
||||
// 窗口关闭处理:确保停止抓取并释放相机资源
|
||||
private void MainWindow_Closing(object? sender, CancelEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 停止抓取(若正在抓取)
|
||||
_cameraService.StopGrabbing();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略停止时的异常,仍尝试关闭资源
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 关闭并释放相机资源
|
||||
_cameraService.Close();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略关闭时的异常
|
||||
}
|
||||
|
||||
StopIntervalCapture();
|
||||
|
||||
// 如果需要,终结 Pylon 库资源(只有在使用 Pylon.Initialize 时才需要)
|
||||
|
||||
}
|
||||
|
||||
private bool IsMonoData(IGrabResult iGrabResult)
|
||||
{
|
||||
switch (iGrabResult.PixelTypeValue)
|
||||
{
|
||||
case PixelType.Mono1packed:
|
||||
case PixelType.Mono2packed:
|
||||
case PixelType.Mono4packed:
|
||||
case PixelType.Mono8:
|
||||
case PixelType.Mono8signed:
|
||||
case PixelType.Mono10:
|
||||
case PixelType.Mono10p:
|
||||
case PixelType.Mono10packed:
|
||||
case PixelType.Mono12:
|
||||
case PixelType.Mono12p:
|
||||
case PixelType.Mono12packed:
|
||||
case PixelType.Mono16:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void CameraTest_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cameraService.StopGrabbing();
|
||||
StopIntervalCapture();
|
||||
MessageBox.Show("已停止抓取。", "提示", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show($"停止抓取失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 新增:间隔保存实现 =====
|
||||
private void StartIntervalCapture(Recipe recipe)
|
||||
{
|
||||
StopIntervalCapture();
|
||||
|
||||
_currentSaveFolder = recipe.SaveFolder;
|
||||
if (string.IsNullOrWhiteSpace(_currentSaveFolder))
|
||||
return;
|
||||
|
||||
Directory.CreateDirectory(_currentSaveFolder);
|
||||
|
||||
_captureIndex = 0;
|
||||
_captureTimer = new DispatcherTimer(DispatcherPriority.Background, _uiDispatcher)
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(Math.Max(50, recipe.CaptureIntervalMs ?? 1000))
|
||||
};
|
||||
_captureTimer.Tick += CaptureTimer_Tick;
|
||||
_captureTimer.Start();
|
||||
}
|
||||
|
||||
private void StopIntervalCapture()
|
||||
{
|
||||
if (_captureTimer != null)
|
||||
{
|
||||
_captureTimer.Tick -= CaptureTimer_Tick;
|
||||
_captureTimer.Stop();
|
||||
_captureTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureTimer_Tick(object? sender, EventArgs e)
|
||||
{
|
||||
if (image == null || string.IsNullOrWhiteSpace(_currentSaveFolder))
|
||||
return;
|
||||
|
||||
HImage? copy = null;
|
||||
try
|
||||
{
|
||||
copy = image.Clone();
|
||||
}
|
||||
catch
|
||||
{
|
||||
copy?.Dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
// 改为保存 tiff,无损且支持 16 位灰度
|
||||
var fileName = $"{DateTime.Now:yyyyMMdd_HHmmss_fff}_{_captureIndex:D5}.tiff";
|
||||
var fullPath = Path.Combine(_currentSaveFolder!, fileName);
|
||||
_captureIndex++;
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (copy != null)
|
||||
{
|
||||
// 第三个参数对 tiff 无效,仅用于 jpeg;此处传 0
|
||||
HOperatorSet.WriteImage(copy, "tiff", 0, fullPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_uiDispatcher.BeginInvoke(() =>
|
||||
{
|
||||
MessageBox.Show($"保存图像失败:{ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
copy?.Dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
36
OneClick/OneClick.csproj
Normal file
36
OneClick/OneClick.csproj
Normal file
@@ -0,0 +1,36 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Prism.Core" Version="9.0.537" />
|
||||
<PackageReference Include="Prism.Unity" Version="9.0.537" />
|
||||
<PackageReference Include="Prism.Wpf" Version="9.0.537" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="View\" />
|
||||
<Folder Include="ViewModel\" />
|
||||
<Folder Include="Service\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="Basler.Pylon">
|
||||
<HintPath>D:\Pylon\Development\Assemblies\Basler.Pylon\net8.0\x64\Basler.Pylon.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="halcondotnetxl">
|
||||
<HintPath>D:\MVTec\Halcon\HALCON-25.11-Progress\bin\dotnet35\halcondotnetxl.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="hdevenginedotnetxl">
|
||||
<HintPath>D:\MVTec\Halcon\HALCON-25.11-Progress\bin\dotnet35\hdevenginedotnetxl.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
137
OneClick/RecipeManager.cs
Normal file
137
OneClick/RecipeManager.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace OneClick
|
||||
{
|
||||
public class RecipeManager
|
||||
{
|
||||
private readonly Dictionary<string, Recipe> _recipes = new();
|
||||
|
||||
// 当前配方(可选,但非常实用)
|
||||
public Recipe? CurrentRecipe { get; private set; }
|
||||
|
||||
private readonly string _storePath;
|
||||
|
||||
public RecipeManager(string storePath)
|
||||
{
|
||||
_storePath = storePath;
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_storePath)!);
|
||||
Load();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增配方
|
||||
/// </summary>
|
||||
public bool Add(Recipe recipe)
|
||||
{
|
||||
if (recipe == null || string.IsNullOrWhiteSpace(recipe.Name))
|
||||
return false;
|
||||
|
||||
if (_recipes.ContainsKey(recipe.Name))
|
||||
return false;
|
||||
|
||||
_recipes.Add(recipe.Name, recipe);
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除配方
|
||||
/// </summary>
|
||||
public bool Remove(string name)
|
||||
{
|
||||
if (!_recipes.Remove(name))
|
||||
return false;
|
||||
|
||||
if (CurrentRecipe?.Name == name)
|
||||
CurrentRecipe = null;
|
||||
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询配方
|
||||
/// </summary>
|
||||
public Recipe? Get(string name)
|
||||
{
|
||||
_recipes.TryGetValue(name, out var recipe);
|
||||
return recipe;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 切换当前配方
|
||||
/// </summary>
|
||||
public bool SetCurrent(string name)
|
||||
{
|
||||
var recipe = Get(name);
|
||||
if (recipe == null)
|
||||
return false;
|
||||
|
||||
CurrentRecipe = recipe;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 修改配方(按名字整体替换)
|
||||
/// </summary>
|
||||
public bool Update(Recipe recipe)
|
||||
{
|
||||
if (recipe == null || string.IsNullOrWhiteSpace(recipe.Name))
|
||||
return false;
|
||||
|
||||
if (!_recipes.ContainsKey(recipe.Name))
|
||||
return false;
|
||||
|
||||
_recipes[recipe.Name] = recipe;
|
||||
|
||||
if (CurrentRecipe?.Name == recipe.Name)
|
||||
CurrentRecipe = recipe;
|
||||
|
||||
Save();
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取所有配方名(给 UI / 下拉框用)
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<string> GetNames()
|
||||
{
|
||||
return _recipes.Keys;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var list = new List<Recipe>(_recipes.Values);
|
||||
var json = JsonSerializer.Serialize(list, new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true
|
||||
});
|
||||
File.WriteAllText(_storePath, json);
|
||||
}
|
||||
|
||||
public void Load()
|
||||
{
|
||||
if (!File.Exists(_storePath))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_storePath);
|
||||
var list = JsonSerializer.Deserialize<List<Recipe>>(json) ?? new List<Recipe>();
|
||||
_recipes.Clear();
|
||||
foreach (var r in list)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(r.Name))
|
||||
_recipes[r.Name] = r;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 读失败保持空,不抛异常以避免影响启动
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
OneClick/RecipePara.cs
Normal file
26
OneClick/RecipePara.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace OneClick
|
||||
{
|
||||
public class Recipe
|
||||
{
|
||||
// 配方名称
|
||||
public string? Name { get; set; }
|
||||
// 相机拍照间隔时间,单位毫秒
|
||||
public int? CaptureIntervalMs { get; set; }
|
||||
//保存文件夹路径
|
||||
public string? SaveFolder { get; set; }
|
||||
// 电机速度,单位RPM
|
||||
public double? MotorSpeed { get; set; }
|
||||
//电机转动时间,单位秒
|
||||
public double? MotorRunTimeSec { get; set; }
|
||||
// 光源强度,范围0-255
|
||||
public int? LightIntensity { get; set; }
|
||||
// 选择的轴号
|
||||
public int? SelectedAxis { get; set; }
|
||||
//
|
||||
|
||||
|
||||
// 可根据需要扩展更多参数
|
||||
}
|
||||
}
|
||||
2891
OneClick/gts.cs
Normal file
2891
OneClick/gts.cs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user