Halcon:HWindow 以鼠标为中心缩放和拖动
2023 年 2 月 26 日 · 2 分钟读完
原理:Part 就是「看图像的哪一块」
HWindowControl 控件的大小是固定的,SetPart 决定把图像的哪个矩形区域铺满这个控件:
- 一张 800 × 600 的图,Part 设为左上 (0, 0)、右下 (600, 800)(行在前、列在后),整张图铺满控件;
- 设为 (0, 0) ~ (300, 400),用半张图铺满控件,看起来就放大了 2 倍;
- 设为 (100, 100) ~ (400, 500),大小不变、位置挪了,看起来就是图像向左上方移动了。
所以:想放大就把 Part 缩小,想缩小就把 Part 放大,想平移就整体挪动 Part。
以鼠标为中心缩放
只缩放 Part 的大小,图像会以左上角为中心缩放,用起来很别扭。我们希望鼠标指着的那个点在缩放前后不动。

黑框是当前的 Part,红点是鼠标位置。放大时 Part 应该变成红框,缩小时变成蓝框。
设当前 Part 左上角为 (r1, c1),鼠标所在的图像坐标为 (mr, mc),缩放系数为 k(新 Part 的尺寸是原来的 k 倍,k < 1 是放大)。鼠标点不动,意味着它到左上角的距离也按 k 缩放:
新的 r1 = mr - k × (mr - r1)
新的 c1 = mc - k × (mc - c1)
新的 r2 = 新的 r1 + k × 原高度
新的 c2 = 新的 c1 + k × 原宽度
HWindowControl 的鼠标事件(HMouseWheel、HMouseDown 等)给的 e.X、e.Y 已经是图像坐标,不是控件的像素坐标,所以可以直接代进公式。
// 绑定到 hWindowControl1.HMouseWheel
private void HWindow_Wheel(object sender, HMouseEventArgs e)
{
if (hImage == null) return;
HWindow w = hWindowControl1.HalconWindow;
w.GetPart(out int r1, out int c1, out int r2, out int c2);
double k = e.Delta > 0 ? 0.8 : 1.25; // 每格放大或缩小 25%
double h = (r2 - r1) * k, wd = (c2 - c1) * k;
// 限制范围:最多放大到看 20 个像素,最多缩小到看图像的 4 倍大
hImage.GetImageSize(out int imgW, out int imgH);
if (h < 20 || h > imgH * 4) return;
double nr1 = e.Y - k * (e.Y - r1);
double nc1 = e.X - k * (e.X - c1);
w.SetPart((int)nr1, (int)nc1, (int)(nr1 + h), (int)(nc1 + wd));
Redraw();
}
原文用的是 Zoom = 0.5 / 1.5,放大和缩小不对称:放大一次 × 0.5,再缩小一次 × 1.5,回不到原来的大小(0.5 × 1.5 = 0.75)。用互为倒数的 0.8 和 1.25 就能精确回去。原文的限制条件只防止缩得太小,没有防止放得太大,放大到一个像素以下时 Part 会变成 0 而报错,所以这里两头都加了限制。
左键拖动
记下左键按下时的位置,移动时算出偏移,让 Part 反方向移动同样的距离:
private double startX, startY;
private bool dragging;
private void HWindow_MouseDown(object sender, HMouseEventArgs e)
{
if (e.Button != MouseButtons.Left) return;
startX = e.X;
startY = e.Y;
dragging = true;
}
private void HWindow_MouseUp(object sender, HMouseEventArgs e) => dragging = false;
private void HWindow_MouseMove(object sender, HMouseEventArgs e)
{
if (!dragging || hImage == null) return;
HWindow w = hWindowControl1.HalconWindow;
w.GetPart(out int r1, out int c1, out int r2, out int c2);
int dr = (int)(startY - e.Y);
int dc = (int)(startX - e.X);
if (dr == 0 && dc == 0) return;
w.SetPart(r1 + dr, c1 + dc, r2 + dr, c2 + dc);
Redraw();
}
为什么起点不用更新? 因为 e.X、e.Y 是图像坐标。Part 移动之后,鼠标下面还是按下时抓住的那个图像点,所以它的图像坐标又变回了起点,下一次移动的偏移是从这里重新算的。效果就是图像「粘」在鼠标上。如果用的是控件的像素坐标,就必须每次更新起点。
原文是在 MouseDown 里 += 订阅 MouseMove、MouseUp 里 -=。万一鼠标在控件外松开,收不到 MouseUp,就会一直处于拖动状态,用一个标志位更稳。
双击还原
private void HWindow_DoubleClick(object sender, EventArgs e) => FitImage();
private void FitImage()
{
hImage.GetImageSize(out int w, out int h);
// 按控件的宽高比留白,图像不变形
double ratio = (double)hWindowControl1.Width / hWindowControl1.Height;
double partW = Math.Max(w, h * ratio), partH = partW / ratio;
int r1 = (int)((h - partH) / 2), c1 = (int)((w - partW) / 2);
hWindowControl1.HalconWindow.SetPart(r1, c1, r1 + (int)partH - 1, c1 + (int)partW - 1);
Redraw();
}
Part 的宽高比和控件不一致时,图像会被拉伸。所以居中留白,保证比例。
拖动时闪烁
快速拖动时刷新很频繁,ClearWindow 之后、DispObj 之前的空白瞬间会被看到,图像一闪一闪的。把这一帧的绘制包在 flush_graphic 里,画完再刷新到屏幕:
private void Redraw()
{
HWindow w = hWindowControl1.HalconWindow;
HOperatorSet.SetSystem("flush_graphic", "false");
w.ClearWindow();
HOperatorSet.SetSystem("flush_graphic", "true");
w.DispObj(hImage); // 打开刷新后的这次绘制会把整帧一起显示出来
}
延伸
- 区域和文字也要重画:每次
Redraw都要按「图像 → 区域 → 文字」的顺序重新显示,否则缩放后叠加的结果就没了,见 在 HWindow 上显示文字。 - HSmartWindowControl 自带缩放拖动:HALCON 12 以后的这个控件内置了滚轮缩放和拖动,WPF 里也能用。新项目可以直接用它,不用自己写这些代码;但想控制缩放范围、联动多个窗口时,还是要理解上面的 Part。
- 显示鼠标处的灰度值:在
HMouseMove里用hImage.GetGrayval((int)e.Y, (int)e.X)取灰度,加上坐标一起显示在状态栏,调试很方便。注意先判断坐标在图像范围内。
原文首发于 blog.csdn.net