using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using TeamAAS.Camera;
using TeamAAS.Core;
using TeamAAS.Camera.Enums;
using TeamAAS.Camera.Images;
using TeamAAS.Camera.Interfaces;
using TeamAAS.FlowEditor.Plugins;
using Plugins.Standard.Models;
namespace Plugins.Standard.Views
{
///
/// 图像采集插件的自定义编辑视图(UserControl)- 承载在 PluginViewDialog 中。
/// DataContext 由宿主设为 ImageAcqModel(宿主执行回调会把 DataContext 当作模型回写,故不可替换)。
/// 支持三种图像来源:相机采集 / 本地图像 / 本地目录,并复用 ImageDisplayControl 做缩放平移预览。
/// - IPluginViewSave:确定时把界面值回写模型;
/// - IPluginRunFeedback:窗体「执行」前后同步预览(执行完成后显示采集到的图像);
/// - IPluginViewTip:把提示写到承载窗体底部提示栏,并给带 Tag 的按钮挂悬停提示。
///
public partial class ImageAcqView : UserControl, IPluginViewSave, IPluginRunFeedback, IPluginViewTip
{
private static readonly string[] ImageExtensions =
{ ".bmp", ".jpg", ".jpeg", ".png", ".tif", ".tiff" };
private volatile bool _busy;
/// 正在程序化刷新界面(抑制 SelectionChanged 回写模型)
private bool _syncingUi;
///
/// 缓存的模型引用。DataContext 是 WPF 依赖属性(DispatcherObject),只能在 UI 线程读取;
/// 而 IPluginRunFeedback.EndRun 由宿主在后台线程回调,若在其中直接读 DataContext 会触发
/// VerifyAccess 抛出“调用线程无法访问此对象”。故在 UI 线程(DataContextChanged)缓存模型,
/// 后台线程只读该普通字段,规避跨线程访问依赖对象。
///
private ImageAcqModel _model;
/// 当前编辑的模型(宿主注入的 DataContext,已在 UI 线程缓存)
private ImageAcqModel Model => _model;
public ImageAcqView()
{
InitializeComponent();
DataContextChanged += (s, e) => _model = e.NewValue as ImageAcqModel;
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
LoadCameraList();
SyncUiFromModel();
// 若模型已有上次采集/加载的图像(例如再次打开节点),直接回显
if (Model?.PreviewImage != null)
ShowGenImage(Model.PreviewImage, DescribeImage(Model.PreviewImage));
HookHoverHints(this);
}
#region 相机列表 & UI 同步
private void LoadCameraList()
{
CmbCamera.Items.Clear();
try
{
foreach (var d in CoreManager.Camera.Devices)
{
if (!string.IsNullOrEmpty(d.Name) && !CmbCamera.Items.Contains(d.Name))
CmbCamera.Items.Add(d.Name);
}
}
catch { }
}
/// 把模型当前值同步到界面(加载时、以及参数设置窗体确认后调用)
private void SyncUiFromModel()
{
if (Model == null) return;
_syncingUi = true;
try
{
if (!string.IsNullOrEmpty(Model.CameraName) && CmbCamera.Items.Contains(Model.CameraName))
CmbCamera.SelectedItem = Model.CameraName;
else if (CmbCamera.Items.Count > 0 && CmbCamera.SelectedItem == null)
CmbCamera.SelectedIndex = 0;
TxtImagePath.Text = Model.LocalImagePath ?? "";
TxtDirPath.Text = Model.LocalImageDir ?? "";
}
finally
{
_syncingUi = false;
}
// 模型里的相机已不存在时,回落到列表首项并同步回模型
if (CmbCamera.SelectedItem is string sel && Model.CameraName != sel)
Model.CameraName = sel;
UpdateSourcePanels();
RefreshTriggerModes();
if (Model.Source == AcqImageSource.本地目录 &&
!string.IsNullOrEmpty(Model.LocalImageDir) && LstDirFiles.Items.Count == 0)
PopulateDirList(Model.LocalImageDir, autoPreview: false);
}
/// 根据图像来源切换各配置面板的可见性与单选状态
private void UpdateSourcePanels()
{
var src = Model?.Source ?? AcqImageSource.相机采集;
PanelCamera.Visibility = src == AcqImageSource.相机采集 ? Visibility.Visible : Visibility.Collapsed;
PanelFile.Visibility = src == AcqImageSource.本地图像 ? Visibility.Visible : Visibility.Collapsed;
PanelDir.Visibility = src == AcqImageSource.本地目录 ? Visibility.Visible : Visibility.Collapsed;
PanelLocalOpts.Visibility =
(src == AcqImageSource.本地图像 || src == AcqImageSource.本地目录)
? Visibility.Visible : Visibility.Collapsed;
RbCamera.IsChecked = src == AcqImageSource.相机采集;
RbFile.IsChecked = src == AcqImageSource.本地图像;
RbDir.IsChecked = src == AcqImageSource.本地目录;
TxtSourceTag.Text = src == AcqImageSource.相机采集 ? "· 相机"
: src == AcqImageSource.本地图像 ? "· 本地文件" : "· 本地目录";
}
#endregion
#region 事件:来源切换 / 相机选择
private void Source_Changed(object sender, RoutedEventArgs e)
{
if (Model == null) return;
if (ReferenceEquals(sender, RbCamera)) Model.Source = AcqImageSource.相机采集;
else if (ReferenceEquals(sender, RbFile)) Model.Source = AcqImageSource.本地图像;
else if (ReferenceEquals(sender, RbDir)) Model.Source = AcqImageSource.本地目录;
UpdateSourcePanels();
if (Model.Source == AcqImageSource.本地目录 &&
!string.IsNullOrEmpty(Model.LocalImageDir) && LstDirFiles.Items.Count == 0)
PopulateDirList(Model.LocalImageDir, autoPreview: false);
}
private void CmbCamera_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_syncingUi) return;
if (Model != null && CmbCamera.SelectedItem is string name)
{
Model.CameraName = name;
// 换相机后触发能力可能不同(如 UVC 只有连续采集),刷新可选项
RefreshTriggerModes();
}
}
#endregion
#region 触发模式(选项随相机能力动态变化)
/// 按名称取当前选中的相机实例(未找到返回 null)
private ICamera CurrentCamera
{
get
{
var name = _model?.CameraName;
if (string.IsNullOrEmpty(name)) return null;
try { return CoreManager.Camera.Devices.FirstOrDefault(c => c.Name == name); }
catch { return null; }
}
}
///
/// 按当前选中相机的能力刷新触发模式下拉——各品牌 SDK 支持的模式不同,
/// 所以选项列表是动态的(UVC/Cognex 只有“跟随相机/连续采集”,工业相机有全部 5 种)。
///
private void RefreshTriggerModes()
{
_syncingUi = true;
try
{
CmbTrigger.Items.Clear();
var cam = CurrentCamera;
IReadOnlyList modes = cam?.SupportedTriggerModes;
if (modes == null || modes.Count == 0)
modes = new[] { CameraTriggerMode.跟随相机 };
foreach (var m in modes)
CmbTrigger.Items.Add(m);
// 模型当前值若不被该相机支持(例如从 Basler 换到 UVC),回落到“跟随相机”
var want = _model?.TriggerMode ?? CameraTriggerMode.跟随相机;
CmbTrigger.SelectedItem = CmbTrigger.Items.Contains(want) ? want : CameraTriggerMode.跟随相机;
TxtTriggerHint.Text = cam == null
? "未选择相机,仅可设「跟随相机」"
: $"该相机支持 {modes.Count} 种:{string.Join("、", modes)}";
}
finally
{
_syncingUi = false;
}
}
private void CmbTrigger_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_syncingUi) return;
if (_model != null && CmbTrigger.SelectedItem is CameraTriggerMode mode)
_model.TriggerMode = mode;
}
#endregion
#region 事件:采集预览 / 浏览文件 / 浏览目录
private async void BtnGrabPreview_Click(object sender, RoutedEventArgs e)
{
if (_busy) return;
var model = Model;
if (model == null) return;
if (string.IsNullOrEmpty(model.CameraName))
{
TxtInfo.Text = "请先选择相机设备";
return;
}
_busy = true;
BtnGrab.IsEnabled = false;
TxtInfo.Text = "采集中...";
try
{
var img = await Task.Run(() => GrabFromCamera(model));
if (img != null)
{
model.PreviewImage = img;
ShowGenImage(img, $"相机: {model.CameraName} {DescribeImage(img)}");
}
else
{
TxtInfo.Text = "采集失败,请检查相机连接或参数";
}
}
catch (Exception ex)
{
TxtInfo.Text = $"采集异常: {ex.Message}";
}
finally
{
_busy = false;
BtnGrab.IsEnabled = true;
}
}
/// 直接从相机采集一帧(后台线程),仅用于视图内即时预览
private static GenImage GrabFromCamera(ImageAcqModel model)
{
var camera = CoreManager.Camera.Devices.FirstOrDefault(c => c.Name == model.CameraName);
if (camera == null) return null;
if (model.AutoOpen && !camera.IsConnected)
{
if (!camera.OpenDevice()) return null;
}
// 与正式执行保持一致:显式指定且相机支持时才下发触发模式
if (model.TriggerMode != CameraTriggerMode.跟随相机 &&
camera.SupportedTriggerModes != null &&
camera.SupportedTriggerModes.Contains(model.TriggerMode))
camera.SetTriggerMode(model.TriggerMode);
float exposure = model.ExposureTime?.Value ?? 0f;
if (exposure > 0) camera.SetExposureTime(exposure);
float gain = model.Gain?.Value ?? 0f;
if (gain >= 0) camera.SetGain(gain);
float gamma = model.Gamma?.Value ?? 1f;
if (gamma > 0) camera.SetGamma(gamma);
return camera.Grab();
}
private void BtnBrowseFile_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Filter = "图像文件|*.bmp;*.jpg;*.jpeg;*.png;*.tif;*.tiff|所有文件|*.*"
};
if (!string.IsNullOrEmpty(Model?.LocalImagePath) && File.Exists(Model.LocalImagePath))
{
dlg.InitialDirectory = Path.GetDirectoryName(Model.LocalImagePath);
dlg.FileName = Path.GetFileName(Model.LocalImagePath);
}
if (dlg.ShowDialog() == true)
{
if (Model != null) Model.LocalImagePath = dlg.FileName;
TxtImagePath.Text = dlg.FileName;
PreviewFile(dlg.FileName);
}
}
private void BtnBrowseDir_Click(object sender, RoutedEventArgs e)
{
using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
{
if (!string.IsNullOrEmpty(Model?.LocalImageDir) && Directory.Exists(Model.LocalImageDir))
dlg.SelectedPath = Model.LocalImageDir;
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
{
if (Model != null) Model.LocalImageDir = dlg.SelectedPath;
TxtDirPath.Text = dlg.SelectedPath;
PopulateDirList(dlg.SelectedPath, autoPreview: true);
}
}
}
private void LstDirFiles_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (LstDirFiles.SelectedItem is string path)
PreviewFile(path);
}
/// 枚举目录内的图像文件填充列表,可选自动预览第一张
private void PopulateDirList(string dir, bool autoPreview)
{
LstDirFiles.Items.Clear();
if (string.IsNullOrEmpty(dir) || !Directory.Exists(dir))
{
TxtSourceTag.Text = "· 本地目录(空)";
return;
}
var files = Directory.EnumerateFiles(dir, "*.*", SearchOption.TopDirectoryOnly)
.Where(f => ImageExtensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)
.ToList();
foreach (var f in files)
LstDirFiles.Items.Add(f);
TxtSourceTag.Text = $"· 本地目录({files.Count} 张)";
if (autoPreview && files.Count > 0)
PreviewFile(files[0]);
}
/// 加载并预览单个本地图像文件(按选项应用 Gamma)
private void PreviewFile(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return;
try
{
var img = ImageAcqPlugin.LoadImageFile(path);
if (img == null)
{
TxtInfo.Text = $"无法加载图像: {Path.GetFileName(path)}";
return;
}
if (Model != null)
{
if (Model.ApplyGamma)
{
float g = Model.Gamma?.Value ?? 1f;
if (g > 0) img.ApplyGamma(g);
}
Model.PreviewImage = img;
}
ShowGenImage(img, $"{Path.GetFileName(path)} {DescribeImage(img)}");
}
catch (Exception ex)
{
TxtInfo.Text = $"预览失败: {ex.Message}";
}
}
#endregion
#region 事件:参数设置
private void BtnProperties_Click(object sender, RoutedEventArgs e)
{
if (Model == null) return;
// 先把界面上的即时编辑写回模型,避免属性窗体覆盖未保存的输入
SaveChanges();
if (TeamAAS.DialogHelper.EditProperties(Model, $"图像采集参数 - {Model.NodeName}"))
SyncUiFromModel();
}
#endregion
#region 图像显示
private static string DescribeImage(GenImage img)
=> img == null ? "" : $"分辨率: {img.Width} x {img.Height} 格式: {img.Format}";
/// 把 GenImage 显示到预览控件(线程安全,内部切回 UI 线程)
private void ShowGenImage(GenImage img, string info)
{
if (img == null)
{
Dispatcher.Invoke(() =>
{
Display.Clear();
TxtInfo.Text = info ?? "无图像";
});
return;
}
Dispatcher.Invoke(() =>
{
try
{
// SetImage(Bitmap) 内部完成 Bitmap→BitmapSource 转换并释放该 Bitmap
Display.SetImage(img.ToBitmap());
TxtInfo.Text = info ?? DescribeImage(img);
}
catch (Exception ex)
{
TxtInfo.Text = $"显示失败: {ex.Message}";
}
});
}
#endregion
#region IPluginViewSave
/// 窗体「确定」时把界面值回写模型(来源、相机、路径)
public void SaveChanges()
{
if (Model == null) return;
if (CmbCamera.SelectedItem is string cam)
Model.CameraName = cam;
if (CmbTrigger.SelectedItem is CameraTriggerMode tm)
Model.TriggerMode = tm;
Model.LocalImagePath = (TxtImagePath.Text ?? "").Trim();
Model.LocalImageDir = (TxtDirPath.Text ?? "").Trim();
if (RbFile.IsChecked == true) Model.Source = AcqImageSource.本地图像;
else if (RbDir.IsChecked == true) Model.Source = AcqImageSource.本地目录;
else Model.Source = AcqImageSource.相机采集;
}
#endregion
#region IPluginRunFeedback
public void BeginRun()
{
Dispatcher.Invoke(() =>
{
BtnGrab.IsEnabled = false;
TxtInfo.Text = "执行中...";
});
}
public void EndRun()
{
// 全程在 UI 线程执行:读取模型预览图、刷新按钮与画面,避免跨线程访问 WPF 依赖对象
Dispatcher.Invoke(() =>
{
BtnGrab.IsEnabled = true;
var img = _model?.PreviewImage;
if (img != null)
ShowGenImage(img, DescribeImage(img));
else
TxtInfo.Text = "执行结束,无图像输出";
});
}
#endregion
#region IPluginViewTip 底部提示栏
private readonly HashSet _hoverHooked = new HashSet();
/// 承载本视图的基窗体(PluginViewDialog),未承载时为 null
private TeamAAS.Dialogs.Dialogs.PluginViewDialog HostDialog =>
Window.GetWindow(this) as TeamAAS.Dialogs.Dialogs.PluginViewDialog;
public void ShowTip(string message) => HostDialog?.ShowTip(message);
public void ShowHoverHint(string message) => HostDialog?.ShowHoverHint(message);
private void HoverEl_MouseEnter(object sender, MouseEventArgs e)
{
if ((sender as FrameworkElement)?.Tag is string tip && !string.IsNullOrWhiteSpace(tip))
ShowHoverHint(tip);
}
private void HoverEl_MouseLeave(object sender, MouseEventArgs e) => ShowHoverHint(null);
/// 给所有 Tag 为非空字符串的按钮挂悬停提示(显示到承载窗体底部提示栏)
private void HookHoverHints(DependencyObject root)
{
if (root == null) return;
int count = System.Windows.Media.VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < count; i++)
{
var child = System.Windows.Media.VisualTreeHelper.GetChild(root, i);
if (child is System.Windows.Controls.Primitives.ButtonBase btn
&& !_hoverHooked.Contains(btn)
&& btn.Tag is string tip && !string.IsNullOrWhiteSpace(tip))
{
btn.MouseEnter += HoverEl_MouseEnter;
btn.MouseLeave += HoverEl_MouseLeave;
_hoverHooked.Add(btn);
}
HookHoverHints(child);
}
}
#endregion
}
}