做WPF上位机项目时,我接过一个需求:在界面里加载一张大图,用户要用鼠标拖出一个矩形区域,拖动过程中能实时看到坐标,拖完还要能继续调整大小。第一反应是在WPF里画一个Rectangle控件,然后用MouseDown、MouseMove、MouseUp三个事件硬扣拖动逻辑,再把控件坐标换算成图像像素坐标。等真的动手才发现,光是坐标换算就够喝一壶——WPF单位、像素、DPI、图像缩放、窗口滚动条,每个环节都会让ROI偏移几个像素,特别是图像被缩放显示后,矩形框和实际框选区域对不上,排查半天也找不到原因。
后来换成Halcon 20.11自带的交互式绘图能力,整个ROI的创建、拖动、坐标获取直接从几十行自绘代码压缩到几行核心调用。Halcon的HDrawingObject对象内置了绘制、拖动、缩放的整套交互逻辑,还支持回调事件,在回调里实时取坐标、刷新UI,工程上稳定得多。这篇文章就结合我的实际项目,把Halcon 20.11在WPF中实现矩形ROI创建、拖动与坐标获取的完整思路和代码拆开讲,包括环境搭建、控件选型、回调机制、坐标验证和实测踩坑,适合正在做WPF上位机且需要集成视觉ROI交互的开发者参考。
1. 为什么我把ROI交互从自绘Canvas换成了Halcon内置方案
1.1 自绘矩形的坐标换算坑
先说说我在自绘方案里踩过的具体问题,这样你就能理解为什么后来坚持用Halcon的交互对象。
WPF的坐标系是逻辑单位(DIP),1个逻辑单位在96DPI下等于1物理像素,但在125%、150%缩放的系统里就被放大了。Halcon的HWindowControl显示图像时,内部又会根据控件尺寸和图像尺寸做一次缩放适配。如果你在控件上叠加一个WPF Rectangle,它使用的是WPF控件的本地坐标系;你通过鼠标事件拿到的e.GetPosition(rect)坐标,也是相对控件的逻辑坐标。要把这个坐标换算成Halcon图像的行列坐标,你需要先知道当前图像的显示缩放比:
double scaleX = imageWidth / (double)hSmartWindow.ActualWidth; double scaleY = imageHeight / (double)hSmartWindow.ActualHeight; // 再考虑图像居中的偏移量 double imageX = (mousePos.X - offsetX) * scaleX; double imageY = (mousePos.Y - offsetY) * scaleY;听起来简单,但一旦图像是等比缩放(黑边模式下),或者窗口可以自由拉伸,这个offset和scale会动态变化,鼠标拖动时稍有不稳,矩形框就“飘”了。更麻烦的是,自绘矩形还要自己处理命中测试——用户拖角点、拖边线、整体拖动是三种不同逻辑,再加上最小尺寸限制、越界纠正,代码量很快就失控了。
1.2 HDrawingObject的定位:它是“带回调的绘制对象”,不是一次性模态框
Halcon里做ROI交互,其实有两套思路。
一套是用draw_rectangle1、draw_rectangle2这类阻塞式交互算子,用户画一次就结束,程序拿到坐标继续往下走。这种方式做“画一次”的需求没问题,但如果你想“画完还能继续拖动微调”,它就不合适了——draw_rectangle1是一次性的,画完就释放交互控制器了。
另一套就是我推荐的create_drawing_object_rectangle1,创建出来的HDrawingObject是挂在窗口上的常驻绘制对象。它自带完整的交互能力:默认状态下鼠标可以拖动它整体移动;拖到角点会出现拉伸手柄,改变矩形尺寸;拖到边界会出现移动光标,调整矩形位置。所有这些交互都不需要你写任何鼠标事件代码,Halcon内部已经把命中测试、光标样式、手柄逻辑全部处理完了。你要做的,只是通过回调事件在适当的时候读取它的最新坐标。
这套机制还有一个明显的工程优势:HDrawingObject返回的坐标永远是图像坐标,不需要你做任何换算。无论图像被放大、缩小、平移,你拿到的row1、column1、row2、column2都是图像像素坐标系里的真实值,直接就可以喂给后续的reduce_domain、template matching、measure_pos等算子。
2. 环境与控件选型:Halcon 20.11接入WPF的两种姿势
2.1 引用halcondotnet.dll和运行时环境
在WPF里用Halcon,第一步是正确引用.NET程序集。Halcon 20.11安装完成后,在安装目录下会有halcondotnet.dll,它同时提供了WinForms控件和WPF控件。我项目里的路径类似:
C:\Program Files\MVTec\HALCON-20.11\bin\dotnet35\halcondotnet.dll C:\Program Files\MVTec\HALCON-20.11\bin\dotnet45\halcondotnet.dll你的WPF项目如果是.NET Framework 4.6.2或4.8,直接引用dotnet45目录下的这个dll即可。右键项目引用,添加引用,浏览到dll位置,确定。如果你想用VS工具箱拖控件,可以在“选择工具箱项”里勾选Halcon相关的控件名称,之后XAML设计器就能直接托入。
运行时还需要Halcon的原生DLL,比如halcon.dll、halconcpp.dll、hdevenginecpp.dll这些。如果安装时选择了加入系统PATH环境变量,通常没问题;如果没加,程序启动时会报“无法加载DLL 'halcon.dll'”这样的错。我的做法是安装后手动检查一下HALCONROOT环境变量是否指向安装目录,同时把bin\x64-win64加到PATH里。
注意:如果是.NET 5/6/8的WPF项目,Halcon 20.11虽然提供了.NET Standard 2.0版本的dll,但实际兼容性还是需要自己验证的。我在正式项目里用的是.NET Framework 4.8,踩坑最少。建议新项目在不确定的情况下先做一个小Demo验证,再决定整体框架。
2.2 WinForms版HWindowControl与WPF版HSmartWindowControlWPF
Halcon 20.11提供了两个窗口控件:
| 控件类 | 技术栈 | 使用方式 | 问题 |
|---|---|---|---|
| HSmartWindowControl | WinForms | 必须放在WindowsFormsHost里 | WinForms控件层级高,会盖住WPF元素 |
| HSmartWindowControlWPF | WPF原生 | 直接放XAML | 兼容性较好,推荐 |
很多老教程用的是HSmartWindowControl,因为早期版本对WPF支持不好。但20.11已经完全支持HSmartWindowControlWPF,没必要再套一层WindowsFormsHost。
不过要注意,HSmartWindowControlWPF在内部依然借助了WindowsFormsHost机制,所以它不能和WPF的手写控件做“透明叠加”,比如你设想在它上方放一个半透明的Border,是显示不出来的。但在我们的ROI需求里,不需要这种叠加,坐标通过回调刷新到界面旁边的TextBlock里就行。
2.3 一个XAML托管示例
我项目的XAML布局大概长这样。HSmartWindowControlWPF的命名空间引用方式如下:
<Window x:Class="HalconRoiDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:halcon="clr-namespace:HalconDotNet;assembly=halcondotnet" Title="Halcon 20.11 WPF交互式ROI Demo" Height="720" Width="1080"> <DockPanel> <StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Margin="8"> <Button Content="加载图像" Click="LoadImage_Click" Width="90"/> <Button Content="创建矩形ROI" Click="CreateRoi_Click" Width="110" Margin="8,0,0,0"/> <Button Content="清除ROI" Click="ClearRoi_Click" Width="90" Margin="8,0,0,0"/> <Button Content="应用ROI并统计" Click="ApplyRoi_Click" Width="120" Margin="20,0,0,0"/> </StackPanel> <StackPanel DockPanel.Dock="Bottom" Margin="8,4,8,8" Orientation="Horizontal"> <TextBlock Text="左上: " VerticalAlignment="Center"/> <TextBlock x:Name="TxtLeftTop" Text="--" Width="130" VerticalAlignment="Center"/> <TextBlock Text="右下: " VerticalAlignment="Center"/> <TextBlock x:Name="TxtRightBottom" Text="--" Width="130" VerticalAlignment="Center"/> <TextBlock Text="中心: " VerticalAlignment="Center"/> <TextBlock x:Name="TxtCenter" Text="--" Width="170" VerticalAlignment="Center"/> <TextBlock Text="宽高: " VerticalAlignment="Center"/> <TextBlock x:Name="TxtSize" Text="--" Width="120" VerticalAlignment="Center"/> </StackPanel> <Border Margin="8" BorderBrush="#999" BorderThickness="1"> <halcon:HSmartWindowControlWPF x:Name="hSmartWindow" /> </Border> </DockPanel> </Window>HSmartWindowControlWPF有一个HalconWindow属性,类型是HWindow,这就是你后续调用所有显示算子的窗口句柄。加载图像时,用SetPart设置图像显示范围,再用DispObj显示。
3. 核心实现:创建、拖动、回调一条龙的矩形ROI
3.1 图像显示与坐标系基准
在创建ROI之前,先要把图像显示到一个确定的状态。如果图像显示范围不固定,比如图像缩放比例一直在变,虽然HDrawingObject依然返回图像坐标,但用户在视觉上会感觉矩形对不上图像,体验很怪。
我的做法是在加载图像后固定显示整张图:
private HImage _image; private HDrawingObject _roiDrawingObject; private void LoadImage_Click(object sender, RoutedEventArgs e) { OpenFileDialog dlg = new OpenFileDialog(); dlg.Filter = "图像文件|*.png;*.bmp;*.jpg;*.jpeg;*.tif;*.tiff|所有文件|*.*"; if (dlg.ShowDialog() != true) return; _image?.Dispose(); _image = new HImage(dlg.FileName); hSmartWindow.HalconWindow.ClearWindow(); // 把显示范围设为整幅图像,坐标基准就是图像行列坐标系 hSmartWindow.HalconWindow.SetPart(0, 0, _image.Height - 1, _image.Width - 1); hSmartWindow.HalconWindow.DispObj(_image); }SetPart的前两个参数是显示区域左上角的行列坐标,后两个是右下角的行列坐标。这里设置成整图范围后,后续所有绘制对象和区域都在图像坐标系下工作,这是个非常重要的基准。
3.2 创建矩形ROI的两种API及差异
Halcon的矩形有两个典型类型:
| 参数 | rectangle1 | rectangle2 |
|---|---|---|
| 数学定义 | 轴对齐矩形 | 带旋转角度的矩形 |
| 参数 | row1, column1, row2, column2 | row, column, phi, length1, length2 |
| 适用场景 | 测量、模板匹配搜索区域 | 倾斜目标、带角度的ROI |
rectangle1是轴对齐矩形,参数是两个对角点;rectangle2是带旋转角度的矩形,中心点加旋转角加长半轴和短半轴。在做尺寸测量、缺陷定位时,rectangle2更常用,因为很多目标在图像里是倾斜的。但为了让交互最直接,我的Demo先用rectangle1,逻辑最简单,坐标也最容易理解。
在C#里创建HDrawingObject,我用的方法是:
private void CreateRoi_Click(object sender, RoutedEventArgs e) { if (_image == null) { MessageBox.Show("请先加载图像"); return; } // 重复创建前先清理旧对象 DisposeRoi(); int imgH = _image.Height; int imgW = _image.Width; // 在图像中心区域创建一个初始矩形 HTuple row1 = imgH / 4; HTuple col1 = imgW / 4; HTuple row2 = imgH * 3 / 4; HTuple col2 = imgW * 3 / 4; HDrawingObject roi = null; try { roi = new HDrawingObject(); roi.CreateDrawingObjectRectangle1(row1, col1, row2, col2); roi.SetDrawingObjectCallback(OnDrawingObjectCallback); roi.SetDrawingObjectParams("color", "green"); roi.SetDrawingObjectParams("line_width", 2); roi.AttachDrawingObjectToWindow(hSmartWindow.HalconWindow); _roiDrawingObject = roi; // 创建后立即刷新一次坐标 RefreshRoiCoordinate(roi); } catch (Exception ex) { MessageBox.Show("创建ROI失败:" + ex.Message); roi?.Dispose(); } }如果你在IDE里发现CreateDrawingObjectRectangle1方法名不对,可以改用构造函数带类型参数的形式:
HDrawingObject roi = new HDrawingObject(HDrawingObject.HDrawingObjectType.RECTANGLE1, row1, col1, row2, col2);两种方式本质上都是调用Halcon的create_drawing_object_rectangle1算子,看你项目里哪个写法编译更顺手。
3.3 完整代码:从创建到拖动回调
SetDrawingObjectCallback是整段逻辑的核心。Halcon在拖动、调整大小、属性变化等时机都会触发这个回调。我在回调里做的事只有三件:
- 从ROI对象上读取最新坐标;
- 把坐标刷新到WPF的TextBlock里;
- 在图像上重绘中心十字和坐标文本,给用户视觉反馈。
回调方法签名是固定的:
private void OnDrawingObjectCallback(IntPtr drawObject, IntPtr window, IntPtr objContext) { // 用户可能已经点击了“清除ROI”,此时回调可能还在队列中,必须先判空 if (_roiDrawingObject == null) return; // 回调线程不一定是UI线程,通过Dispatcher切到UI线程再更新控件 Dispatcher.BeginInvoke(new Action(() => { RefreshRoiCoordinate(_roiDrawingObject); DrawCenterCross(); })); } private void RefreshRoiCoordinate(HDrawingObject roi) { HTuple row1 = roi.GetDrawingObjectParams("row1"); HTuple col1 = roi.GetDrawingObjectParams("column1"); HTuple row2 = roi.GetDrawingObjectParams("row2"); HTuple col2 = roi.GetDrawingObjectParams("column2"); double r1 = row1.D; double c1 = col1.D; double r2 = row2.D; double c2 = col2.D; TxtLeftTop.Text = $"({r1:F1}, {c1:F1})"; TxtRightBottom.Text = $"({r2:F1}, {c2:F1})"; double centerR = (r1 + r2) / 2.0; double centerC = (c1 + c2) / 2.0; TxtCenter.Text = $"({centerR:F1}, {centerC:F1})"; double width = Math.Abs(c2 - c1) + 1; double height = Math.Abs(r2 - r1) + 1; TxtSize.Text = $"{width:F0} x {height:F0}"; } private void DrawCenterCross() { if (_image == null || _roiDrawingObject == null) return; HTuple row1 = _roiDrawingObject.GetDrawingObjectParams("row1"); HTuple col1 = _roiDrawingObject.GetDrawingObjectParams("column1"); HTuple row2 = _roiDrawingObject.GetDrawingObjectParams("row2"); HTuple col2 = _roiDrawingObject.GetDrawingObjectParams("column2"); double centerR = (row1.D + row2.D) / 2.0; double centerC = (col1.D + col2.D) / 2.0; hSmartWindow.HalconWindow.DispObj(_image); hSmartWindow.HalconWindow.SetColor("yellow"); hSmartWindow.HalconWindow.DispCross(centerR, centerC, 20, 0); hSmartWindow.HalconWindow.SetTpos(centerR - 30, centerC + 12); hSmartWindow.HalconWindow.WriteString($"({centerR:F1}, {centerC:F1})"); hSmartWindow.HalconWindow.SetColor("green"); }这段代码有两个细节要展开说。
第一个细节,为什么DrawCenterCross里要先DispObj(_image)重新显示图像?因为DispCross和WriteString画上去的内容,下一次再画的时候不会自动消失,如果不重绘图像,十字和文字会残留成一条条影子。先重绘图像,再画新十字,画面就干净了。代价是拖动时会有轻微闪烁,尤其在性能一般的机器上。如果你的场景对闪烁敏感,可以在回调里减少重绘次数,只保留坐标文本的刷新,不画十字,视觉负担会小很多。
第二个细节,为什么文字写的坐标是行列而不是X/Y?因为Halcon的图像坐标系以左上角为原点,行向下增加,列向右增加。后续所有图像处理算子返回的坐标都是这个行列体系,不是数学里常见的平面直角坐标系。上位机界面如果要显示给操作员看,建议在界面上明确标注“行/列”或“Row/Column”,否则很容易和数学坐标搞混。
3.4 清除ROI的完整姿势
清除ROI不只是从窗口移除绘制对象,还要把引用置空,防止回调继续访问已经释放的对象。
private void ClearRoi_Click(object sender, RoutedEventArgs e) { DisposeRoi(); TxtLeftTop.Text = "--"; TxtRightBottom.Text = "--"; TxtCenter.Text = "--"; TxtSize.Text = "--"; if (_image != null) { hSmartWindow.HalconWindow.DispObj(_image); } } private void DisposeRoi() { if (_roiDrawingObject != null) { _roiDrawingObject?.Dispose(); _roiDrawingObject = null; } }有一点要提醒:如果你调用了DisposeRoi之后,之前用Dispatcher.BeginInvoke排队的回调还没有执行,那回调里读取_roiDrawingObject时会得到null,所以回调方法开头的判空必须写。
4. 坐标获取与ROI落地验证
4.1 从矩形参数到中心点、宽高
ROI画出来只是第一步,工程里真正要的是坐标数据。对rectangle1来说,从四个参数算中心点、宽高非常简单。宽高为什么要加1?因为图像像素坐标是离散的,从行列100到200,实际的像素数量是101个。不同的测量算子对边界像素的处理方式不完全一样,有些算子内部会做类似width = col2 - col1 + 1的修正。在编写测量逻辑时,要和Halcon算子的实际行为对齐,否则可能差1个像素。做高精度测量时,这个“像素差”会直接影响结果。
如果你的后续流程用的是rectangle2,那获取的参数就变成了row、column、phi、length1、length2。中心点就是row和column,不需要再算;length1和length2是半轴长度,实际宽度是2倍的length2,实际高度是2倍的length1。
4.2 用十字与文字在图像上直接标注中心
坐标数据在界面里显示是一回事,但下一次视觉处理时,我想把中心点也叠加到图像上供肉眼确认。这就用到DispCross和WriteString。DispCross的第一个参数是窗口句柄,第二个是行坐标,第三个是列坐标,第四个是十字半径,第五个是旋转角度(0表示不旋转)。
我在前面代码里已经用了这个组合:先用黄色画十字和文字,再切回绿色。这样用户拖动ROI时,能直观看到中心点实时跟随,同时界面下方的坐标文本也在同步变化。
如果拖动过程觉得十字闪烁太明显,有一个折中方案:不要每次回调都重绘,而是在ROI拖动结束的那一帧重绘。但怎么知道拖动结束?Halcon的回调类型里可以区分on_drag、on_resize、on_attach等。你可以给SetDrawingObjectCallback传不同的回调函数,分别处理不同阶段。我在项目里是让on_drag只管刷新TextBlock,不做图像重绘;on_resize和on_attach才重绘十字。这样拖动过程中十字不闪,只在松手后稳定更新。代码结构也很简单:
roi.SetDrawingObjectCallback(OnDrawingObjectMoving, "on_drag"); roi.SetDrawingObjectCallback(OnDrawingObjectChanged, "on_resize");这里第二个参数指的是Halcon的callback type,不同版本的定义可能有差异,实际以你正在用的版本联机帮助为准。
4.3 ReduceDomain把ROI变成真正可用的子图像
坐标能不能用,最直接的验证方式就是拿ROI去做一次实际图像处理。Halcon里最标准的一套组合是:GetDrawingObjectRegion得到ROI的Region,再用ReduceDomain裁剪出ROI区域内的子图像,后续做灰度统计、模板匹配、字符识别都基于这个子图。
private void ApplyRoi_Click(object sender, RoutedEventArgs e) { if (_image == null || _roiDrawingObject == null) { MessageBox.Show("请先加载图像并创建ROI"); return; } HRegion roiRegion = null; HImage roiImage = null; try { // 从绘制对象上拿到区域,这个区域就是在图像坐标系下的 roiRegion = _roiDrawingObject.GetDrawingObjectRegion(); // 用区域裁剪图像 roiImage = _image.ReduceDomain(roiRegion); HTuple mean, deviation; HOperatorSet.Intensity(roiRegion, roiImage, out mean, out deviation); MessageBox.Show($"ROI区域灰度均值={mean.D:F2},标准差={deviation.D:F2}"); } finally { roiImage?.Dispose(); roiRegion?.Dispose(); } }这段代码跑通了,就证明坐标获取全链路是可靠的:用户拖动ROI的坐标已经准确作用到了图像上,而不是只停留在界面数字上。
实际项目里,这段逻辑的替换空间很大。比如你要做模板匹配,就把mean和deviation换成CreateShapeModel的输入区域;你要做尺寸测量,就用ROI区域配合measure_pos,只关注这个矩形内的边缘。本质上都是同一个思路:ROI确定了“看哪里”,后续算子决定“怎么看”。
5. 实战踩坑与工程化建议
5.1 回调线程与Dispatcher:别在回调里直接碰WPF控件
这是我最想提醒的一条。Halcon的drawing object回调并不保证运行在WPF的UI线程上,而WPF控件只允许STA线程操作。如果你在回调里直接写TxtLeftTop.Text = ...,轻则偶发跨线程异常,重则界面直接卡死或闪退。
解决方案就是前面代码里的Dispatcher.BeginInvoke。要注意的是,BeginInvoke是异步的,回调执行到一半的时候,UI线程可能还没来得及更新文本,于是你会看到坐标刷新有轻微延迟。这个延迟在拖动场景下通常无所谓,人眼感知不到。只有当ROI对象在UI线程被释放时,才可能出现问题,所以回调里一定要加null判断。
5.2 对象释放与内存泄漏:HObject、HDrawingObject都要Dispose
Halcon的.NET封装对象,比如HImage、HRegion、HDrawingObject,很多都直接包装了原生内存。如果只把引用置null而忘记Dispose,内存不会被完全回收。在连续图像处理的场景下,十几个画面后内存就会明显上涨,最终OOM。
我个人的习惯是:凡是new HImage()或new HDrawingObject()出来的对象,要么用using包裹,要么在finally里手动Dispose。ROI对象因为是窗体级的全局变量,在替换和关闭窗口时都要清理。窗口关闭事件里也别忘了:
protected override void OnClosed(EventArgs e) { DisposeRoi(); _image?.Dispose(); base.OnClosed(e); }5.3 多ROI场景如何区分drawObject
我的Demo只维护了一个ROI,但实际生产里经常要画多个区域:一个搜索区域、一个检测区域、一个字符识别区域,互相独立。这时候回调方法里的drawObject参数就派上用场了。
Halcon的C#回调签名是void Callback(IntPtr drawObject, IntPtr window, IntPtr objContext),其中drawObject可以理解为这个绘制对象的标识。如果你要区分多个ROI,可以在创建时给每个ROI一个业务ID,并存进字典:
private Dictionary<IntPtr, string> _roiIdMap = new Dictionary<IntPtr, string>();但问题在于,创建HDrawingObject时你拿不到它的IntPtr编号,只有在回调里才拿得到。所以一种可行的实践是:在第一次回调触发时,以drawObject为key注册,同时结合objContext参数做区分。如果不想过度设计,也可以简单点:给每个ROI设置不同的颜色或不同的回调方法:
roiA.SetDrawingObjectCallback(OnRoiACallback); roiB.SetDrawingObjectCallback(OnRoiBCallback);这种方法最直观,也好维护。
5.4 高DPI、窗口缩放与拖动偏移问题
在4K屏、150%缩放的工控机上,HSmartWindowControlWPF偶尔会出现鼠标拖动ROI时,矩形框和光标位置偏移的情况。这个问题多半不是Halcon本身的问题,而是宿主窗口的DPI感知设置不对。解决方法是:
- 在
MainWindow构造函数或App.xaml.cs启动最前面调用SetProcessDpiAwareness或声明DPI感知; - 或者使用.NET Framework 4.8自带的
app.config中添加<DpiAwareness>PerMonitorV2</DpiAwareness>声明。
遇到这类问题,先确认系统缩放比例,再检查进程是否被标记为DPI感知。很多时候界面看着发虚但ROI坐标是准的,那就是DPI感知没配好。
5.5 WindowsFormsHost带来的层级问题如果避不开怎么办
前面我推荐用HSmartWindowControlWPF,但如果你的项目历史原因已经在用HSmartWindowControl(WinForms版本)并且套了WindowsFormsHost,有一个坑要知道:WindowsFormsHost是一个窗口句柄隔离的宿主,它会浮在大部分WPF元素之上。不要在它的上层放需要显示的WPF控件,否则会被盖住。解决方法是把需要交互的WPF元素放在WindowsFormsHost之外,用布局隔离,而不是叠在同一块区域。
另外,WindowsFormsHost还有一个常见的焦点问题:点击它之后,WPF侧的键盘事件可能失效。处理方式是给WindowsFormsHost设置Focusable="true"或者在点击后手动调用Focus()转移焦点。
5.6 保存与恢复ROI参数
最后分享一个项目中的实用习惯:设备重启后,ROI位置通常是固定的,每次让操作员重新画一遍不现实。我建议把ROI参数序列化保存到配置文件里:
string roiConfig = $"{row1.D};{col1.D};{row2.D};{col2.D}"; File.WriteAllText("roi.txt", roiConfig);下次启动时读出来,直接调用CreateDrawingObjectRectangle1把ROI恢复到窗口上。这样既保留了交互能力,又免去了操作员重复劳动。坐标保存的标准统一用图像坐标,不要存界面坐标,因为下次加载的图像尺寸或显示缩放可能和上次不一样,界面坐标毫无可移植性。
在我实际做的几个测量项目里,这套方案的维护成本非常低。第一次做的时候,回调线程和对象释放是我花时间最多的地方;一旦把这两个点想清楚,后续新增圆形ROI、椭圆ROI、旋转矩形ROI,都只是换一个创建方法和几个参数的问题,交互逻辑完全复用。如果你在WPF集成Halcon时卡在了ROI交互上,希望这篇能把你的弯路省下来。