共计 2659 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
在 C# 中集成 Halcon 进行图像处理时,绘图工具(如画 ROI 区域)的交互经常遇到两个典型问题:

-
UI 线程阻塞:当直接调用 Halcon 的绘图方法(如
draw_rectangle1)时,整个操作会同步阻塞 UI 线程,导致界面冻结直到绘制完成。这对需要长时间绘制或用户可能需要中途取消的场景很不友好。 -
内存泄漏风险 :直接使用
hDevWindowStack.Push()和Pop()管理绘图窗口时,如果绘制过程中抛出异常或提前退出,可能导致 Halcon 句柄未正确释放,最终积累内存泄漏。
技术方案对比
1. 暴力终止(Thread.Abort)
- 不推荐:强制终止线程可能导致 Halcon 内部状态不一致,甚至引发死锁。
2. ManualResetEvent 信号量
- 中等推荐:通过信号量通知绘图线程停止,但需要手动处理资源清理,代码较复杂。
3. CancellationTokenSource(推荐)
- 优点:与.NET 异步任务模型天然契合,可无缝集成
async/await。 - 关键点 :结合 Halcon 的
set_check("~give_error")模式,允许绘图操作被外部取消而不抛出异常。
代码实现
封装绘图上下文
public class HalconDrawingContext : IDisposable
{
private HTuple mWindowHandle;
private CancellationTokenSource mCts;
public HalconDrawingContext(HTuple windowHandle)
{
mWindowHandle = windowHandle;
mCts = new CancellationTokenSource();
HOperatorSet.SetCheck("~give_error"); // HDevelop: set_check('~give_error')
}
public async Task<HObject> DrawRectangle1Async(IProgress<string> progress = null)
{return await Task.Run(() =>
{
HObject roi = null;
try
{progress?.Report("开始绘制矩形...");
HOperatorSet.DrawRectangle1(mWindowHandle, out HTuple row1, out HTuple column1,
out HTuple row2, out HTuple column2); // HDevelop: draw_rectangle1
if (mCts.IsCancellationRequested)
return null;
HOperatorSet.GenRectangle1(out roi, row1, column1, row2, column2);
progress?.Report("绘制完成");
return roi;
}
finally
{if (mCts.IsCancellationRequested)
roi?.Dispose();}
}, mCts.Token);
}
public void Cancel() => mCts?.Cancel();
public void Dispose()
{mCts?.Dispose();
HOperatorSet.SetCheck("give_error"); // 恢复默认错误处理
}
}
使用示例
// 在 WinForms/WPF 按钮事件中
private async void btnDraw_Click(object sender, EventArgs e)
{using (var ctx = new HalconDrawingContext(hWindowControl.HalconWindow))
{var progress = new Progress<string>(msg => labelStatus.Text = msg);
var cancelBtn = new Button {Text = "取消"};
cancelBtn.Click += (s, _) => ctx.Cancel();
try
{var roi = await ctx.DrawRectangle1Async(progress);
if (roi != null)
DisplayROI(roi); // 显示绘制结果
}
catch (OperationCanceledException)
{MessageBox.Show("用户取消了绘制");
}
}
}
避坑指南
1. 句柄泄漏检测
// 检查未释放的 Halcon 对象数量
HOperatorSet.CountObj(HObject.EMPTY, out HTuple count); // HDevelop: count_obj(EMPTY, Count)
Debug.WriteLine($"未释放对象数: {count}");
2. 异常处理
⚠️ Halcon 的错误代码需要转换为.NET 异常:
try
{// Halcon 操作}
catch (HOperatorException ex)
{var errCode = ex.GetErrorCode();
if (errCode == 1400) // 用户取消
HandleCancellation();
else
throw;
}
3. 高 DPI 坐标映射
在窗体构造函数中添加:
if (Environment.OSVersion.Version.Major >= 6)
SetProcessDPIAware();
[DllImport("user32.dll")]
private static extern bool SetProcessDPIAware();
验证指标
1. 取消延迟测试
var sw = Stopwatch.StartNew();
ctx.Cancel();
Debug.WriteLine($"取消延迟: {sw.ElapsedMilliseconds}ms");
// 应 <100ms
2. 内存泄漏测试
var before = GC.GetTotalMemory(true);
// 执行绘制并取消
var after = GC.GetTotalMemory(false);
Debug.Assert(Math.Abs(after - before) < 1024); // 内存增长应 <1KB
思考题
如何扩展本方案支持多绘图窗口竞争取消?可以考虑:
- 使用
CancellationTokenSource.CreateLinkedTokenSource合并多个取消令牌 - 为每个绘图窗口维护独立的绘图上下文
- 引入全局取消注册表,通过窗口句柄映射到对应的取消操作
正文完
