← 全部文章 机器视觉

Halcon:HWindow 以鼠标为中心缩放和拖动

2023 年 2 月 26 日 · 2 分钟读完

目录
  1. 原理:Part 就是「看图像的哪一块」
  2. 以鼠标为中心缩放
  3. 左键拖动
  4. 双击还原
  5. 拖动时闪烁
  6. 延伸

原理: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) 取灰度,加上坐标一起显示在状态栏,调试很方便。注意先判断坐标在图像范围内。

# Halcon # C#

原文首发于 blog.csdn.net