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 } }