Files
Ayay/SHH.CameraSdk/Core/Services/DisplayWindowManager.cs

400 lines
16 KiB
C#
Raw Normal View History

using OpenCvSharp;
using System;
using System.Collections.Concurrent;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace SHH.CameraSdk
{
/// <summary>
/// [终极版] 动态窗口管理器
/// 核心修复:
/// 1. 防止手动关闭窗口后,下一帧自动创建不可缩放的僵尸窗口
/// 2. 确保关闭时流控策略 (FPS=0) 正确下发,释放帧调度资源
/// 3. 新增鼠标点击暂停/恢复功能,支持单窗口帧流控制
/// 4. 【关键修复】在 UILoop 中增加独立轮询机制,**暂停状态下也能检测窗口关闭**
/// 核心特性:
/// - 单线程UI渲染规避 OpenCV GUI 多线程操作风险
/// - 策略强联动:窗口生命周期与帧控制器订阅状态深度绑定
/// - 资源闭环:自动清理订阅、销毁窗口、释放 Mat 内存,无内存泄漏
/// </summary>
public class DisplayWindowManager : IDisposable
{
#region --- ---
/// <summary>
/// 单个窗口的上下文信息载体
/// 存储设备关联、运行状态、回调函数等核心数据
/// </summary>
private class WindowContext
{
/// <summary> 关联的设备ID </summary>
public int DeviceId { get; set; }
/// <summary> 窗口暂停状态volatile 保证多线程可见性) </summary>
public volatile bool IsPaused;
2025-12-27 14:16:50 +08:00
/// <summary> 鼠标事件回调函数 </summary>
public MouseCallback MouseHandler;
/// <summary> 窗口是否已实际创建(防止重复初始化) </summary>
public bool IsWindowCreated;
}
#endregion
#region --- ---
/// <summary> 活跃窗口注册表(线程安全) </summary>
/// <remarks> Key = 窗口标识 AppId, Value = 窗口上下文 </remarks>
private readonly ConcurrentDictionary<string, WindowContext> _activeWindows = new();
/// <summary> UI操作指令队列 </summary>
/// <remarks> 容量限制100防止渲染阻塞导致内存溢出 </remarks>
private readonly BlockingCollection<Action> _uiActionQueue = new BlockingCollection<Action>(100);
/// <summary> UI渲染专属线程长驻后台 </summary>
private readonly Task _uiThread;
/// <summary> 全局取消令牌源:用于优雅终止渲染循环 </summary>
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
/// <summary> 相机管理器实例:用于联动帧控制器的流控策略 </summary>
private readonly CameraManager _cameraManager;
#endregion
#region --- ---
/// <summary>
/// 初始化动态窗口管理器
/// </summary>
/// <param name="cameraManager">相机管理器实例,用于帧策略联动</param>
public DisplayWindowManager(CameraManager cameraManager)
{
_cameraManager = cameraManager;
// 启动长驻UI线程设置 LongRunning 提升调度优先级
_uiThread = Task.Factory.StartNew(UILoop, TaskCreationOptions.LongRunning);
Console.WriteLine("[DisplayManager] 渲染引擎就绪 (防僵尸窗口终极版)");
}
/// <summary>
/// 释放所有资源,优雅终止窗口管理器
/// </summary>
public void Dispose()
{
// 1. 发送取消信号,终止 UI 循环
_cts.Cancel();
// 2. 等待 UI 线程退出最多等待1秒防止卡死
try { _uiThread.Wait(1000); }
catch (Exception ex) { Console.WriteLine($"[DisplayManager] 线程退出异常: {ex.Message}"); }
// 3. 强制清理所有活跃窗口
foreach (var appId in _activeWindows.Keys)
{
StopDisplay(appId);
}
// 4. 清空 UI 指令队列,释放残留资源
while (_uiActionQueue.TryTake(out var _)) { }
// 5. 销毁所有 OpenCV 残留窗口
Cv2.DestroyAllWindows();
// 6. 释放核心组件资源
_uiActionQueue.Dispose();
_cts.Dispose();
Console.WriteLine("[DisplayManager] 渲染引擎已安全销毁");
}
#endregion
#region --- ---
/// <summary>
/// 启动指定设备的本地显示窗口
/// </summary>
/// <param name="appId">窗口唯一标识</param>
/// <param name="deviceId">关联的设备ID</param>
public void StartDisplay(string appId, int deviceId)
{
// 防重入:已存在该窗口则直接返回
if (_activeWindows.ContainsKey(appId)) return;
Console.WriteLine($"[DisplayManager] 正在启动窗口: {appId} -> Device {deviceId}");
// 初始化窗口上下文
var context = new WindowContext
{
DeviceId = deviceId,
IsPaused = false,
IsWindowCreated = false
};
// 注册鼠标左键回调:切换暂停/恢复状态
context.MouseHandler = (mouseEvent, x, y, flags, userData) =>
{
if (mouseEvent == MouseEventTypes.LButtonDown)
{
context.IsPaused = !context.IsPaused;
Console.WriteLine($"[DisplayManager] 窗口 {appId} 状态切换: {(context.IsPaused ? "" : "")}");
}
};
// 线程安全地添加窗口上下文
if (_activeWindows.TryAdd(appId, context))
{
// 步骤1投递窗口初始化指令到 UI 线程
_uiActionQueue.TryAdd(() =>
{
try
{
// 创建可缩放窗口,并设置初始尺寸 640x360
Cv2.NamedWindow(appId, WindowFlags.Normal | WindowFlags.GuiExpanded);
Cv2.ResizeWindow(appId, 640, 360);
// 绑定鼠标事件回调
Cv2.SetMouseCallback(appId, context.MouseHandler);
// 标记窗口已实际创建
context.IsWindowCreated = true;
}
catch (Exception ex)
{
Console.WriteLine($"[DisplayManager] 窗口 {appId} 初始化失败: {ex.Message}");
}
});
// 步骤2订阅设备帧数据流
GlobalStreamDispatcher.Subscribe(appId, deviceId, frame =>
{
// 快速判断:取消/暂停/窗口已移除,直接跳过帧处理
if (_cts.IsCancellationRequested || context.IsPaused || !_activeWindows.ContainsKey(appId))
{
return;
}
Mat frameClone = null;
try
{
// 深拷贝帧数据:原帧属于解码线程,必须克隆后移交 UI 线程
2025-12-27 07:05:07 +08:00
if (frame.TargetMat != null)
frameClone = frame.TargetMat.Clone();
else
frameClone = frame.InternalMat.Clone();
}
catch (Exception ex)
{
Console.WriteLine($"[DisplayManager] 帧克隆失败: {ex.Message}");
return;
}
// 投递帧显示指令到 UI 线程
bool enqueued = _uiActionQueue.TryAdd(() =>
{
try
{
// 双重检查:窗口上下文是否存在 + 窗口是否已创建
if (_activeWindows.TryGetValue(appId, out var ctx) && ctx.IsWindowCreated)
{
// 最后一道防线:检测窗口是否被手动关闭
if (Cv2.GetWindowProperty(appId, WindowPropertyFlags.Visible) < 1.0)
{
Task.Run(() => StopDisplay(appId));
return;
}
// 执行帧显示
Cv2.ImShow(appId, frameClone);
}
}
finally
{
// 强制释放克隆的 Mat 内存,杜绝内存泄漏
frameClone?.Dispose();
}
});
// 队列满时的兜底处理:手动释放克隆帧
if (!enqueued) frameClone?.Dispose();
});
}
}
/// <summary>
/// 停止并销毁指定的显示窗口
/// </summary>
/// <param name="appId">窗口唯一标识</param>
public void StopDisplay(string appId)
{
// 从注册表中移除窗口上下文
if (_activeWindows.TryRemove(appId, out var context))
{
Console.WriteLine($"[DisplayManager] 正在清理窗口资源: {appId}");
// 步骤1立即取消帧数据流订阅
GlobalStreamDispatcher.Unsubscribe(appId);
// 步骤2投递窗口销毁指令到 UI 线程
_uiActionQueue.TryAdd(() =>
{
try
{
Cv2.DestroyWindow(appId);
}
catch (Exception ex)
{
Console.WriteLine($"[DisplayManager] 窗口 {appId} 销毁失败: {ex.Message}");
}
});
// 步骤3关键操作 - 下发流控策略FPS=0释放帧控制器资源
UpdateDevicePolicy(context.DeviceId, appId, 0);
}
}
#endregion
#region --- ---
/// <summary>
/// 暂停指定窗口的帧显示
/// </summary>
/// <param name="appId">窗口唯一标识</param>
public void PauseDisplay(string appId)
{
if (_activeWindows.TryGetValue(appId, out var ctx))
{
ctx.IsPaused = true;
Console.WriteLine($"[DisplayManager] 窗口 {appId} 已暂停");
}
}
/// <summary>
/// 恢复指定窗口的帧显示
/// </summary>
/// <param name="appId">窗口唯一标识</param>
public void ResumeDisplay(string appId)
{
if (_activeWindows.TryGetValue(appId, out var ctx))
{
ctx.IsPaused = false;
Console.WriteLine($"[DisplayManager] 窗口 {appId} 已恢复");
}
}
#endregion
#region --- ---
/// <summary>
/// 更新设备的帧订阅策略,与帧控制器强联动
/// </summary>
/// <param name="deviceId">设备ID</param>
/// <param name="appId">订阅标识窗口ID</param>
/// <param name="fps">目标帧率0表示取消订阅</param>
private void UpdateDevicePolicy(int deviceId, string appId, int fps)
{
try
{
// 获取设备实例
var device = _cameraManager.GetDevice(deviceId);
if (device == null)
{
Console.WriteLine($"[策略联动] 设备 {deviceId} 不存在");
return;
}
// 获取设备内置的帧控制器
var frameController = device.Controller;
if (frameController == null)
{
Console.WriteLine($"[策略联动] 设备 {deviceId} 未配置帧控制器");
return;
}
// 根据帧率更新订阅状态
if (fps > 0)
{
frameController.Register(appId, fps);
Console.WriteLine($"[策略联动] ✅ 已注册流控: {appId} -> {fps} FPS");
}
else
{
frameController.Unregister(appId);
Console.WriteLine($"[策略联动] 🗑️ 已注销流控: {appId}");
}
// 记录审计日志,用于前端排查问题
device.AddAuditLog($"UI播放窗口状态变更: {appId} -> {(fps > 0 ? $"{fps}fps" : "")}");
}
catch (Exception ex)
{
Console.WriteLine($"[策略联动] ❌ 联动失败: {ex.Message}");
}
}
#endregion
#region --- ---
/// <summary>
/// 单线程UI渲染循环所有 OpenCV GUI 操作的唯一入口
/// </summary>
private void UILoop()
{
// 轮询计数器:控制窗口状态检查频率
int checkCounter = 0;
while (!_cts.IsCancellationRequested)
{
try
{
// 阶段1处理 UI 指令队列
int processedCount = 0;
// 单次循环最多处理10条指令避免长时间占用线程
while (processedCount < 10 && _uiActionQueue.TryTake(out var action, 5))
{
action.Invoke();
processedCount++;
}
// 阶段2执行 OpenCV 消息泵,防止窗口无响应
Cv2.WaitKey(1);
// 阶段3【核心修复】独立轮询检测窗口状态
// 每20次循环约200ms检查一次不受帧渲染暂停影响
checkCounter++;
if (checkCounter >= 20)
{
checkCounter = 0;
CheckClosedWindows();
}
}
catch (Exception ex)
{
Console.WriteLine($"[UI] 渲染循环异常: {ex.Message}");
}
}
// 循环退出时,销毁所有残留窗口
Cv2.DestroyAllWindows();
}
/// <summary>
/// 检查所有活跃窗口的状态,自动清理被手动关闭的窗口
/// 【必须在 UI 线程调用】
/// </summary>
private void CheckClosedWindows()
{
// 获取当前活跃窗口ID的快照避免遍历过程中集合被修改
var activeIds = _activeWindows.Keys.ToArray();
foreach (var appId in activeIds)
{
try
{
// GetWindowProperty 必须在 UI 线程执行
// Visible < 1.0 表示窗口已被用户手动关闭
if (Cv2.GetWindowProperty(appId, WindowPropertyFlags.Visible) < 1.0)
{
Console.WriteLine($"[UI] 检测到窗口 {appId} 已被手动关闭,触发清理...");
// 异步清理:避免 StopDisplay 内部的队列操作阻塞 UI 线程
Task.Run(() => StopDisplay(appId));
}
}
catch
{
// 捕获异常:通常是窗口已不存在,直接触发清理
Task.Run(() => StopDisplay(appId));
}
}
}
#endregion
}
}