根据鼠标位置放大窗口

6

您好,目前我正在使用C++编写Win32应用程序,我在尝试让窗口内容实现缩放时遇到了问题。以下是我开始编写的伪代码:

// point One
int XPointOne = -200;
int YPointTwo = 0;

// point Two
int XPointTwo = 200;
int YPointTwo = 0;

// Draw point function.
DrawPoint(XCoordinate * ScalingFactor, YCoordinate * ScalingFactor) {
    ....
}

我的坐标系是以窗口中心为原点。我希望在使用鼠标滚轮时进行缩放。上面提供的解决方案的问题在于,缩放总是发生在窗口中心。当您的鼠标不在窗口中心时,这看起来有些丑陋。我想将缩放定位到鼠标所在的区域,但我找不到一个合适的算法来计算x和y方向的偏移量。例如,如果鼠标的坐标为(-200, 0),则point one应具有坐标(-200, 0),point two应具有坐标(600, 0),缩放因子为2。我已经尝试了很多方法,但在鼠标在缩放之间移动时,一切都会混乱。有人知道如何解决这个问题吗?
以下是我的应用程序示例代码。第一个片段是处理WM_MOUSEWHEEL消息的回调函数。
VOID OnMouseWheel(WPARAM const& WParam, LPARAM const& LParam) {
    if(GET_WHEEL_DELTA_WPARAM(WParam) > 0)
    {
        // Zoom in
        Draw.ScaleFactor += 0.1;
    }
    else
    {
         // Zoom out
    }
}

Draw是一个简单的类,它封装了GDI函数。它有一个缩放因子成员。下面的代码片段是我Draw对象的DrawCircle成员函数,使用缩放因子正确地显示圆形在屏幕上。

VOID DrawCircle(DOUBLE const& XCoordinate, DOUBLE const& YCoordinate, DOUBLE const& Radius, COLORREF const& Color) {
    HBRUSH Brush = CreateSolidBrush(Color);
    HBRUSH OldBrush = (HBRUSH)SelectObject(this->MemoryDC, Brush);

    Ellipse(this->MemoryDC, (INT) ((XCoordinate - Radius) * this->ScaleFactor), 
        -(INT)((YCoordinate + Radius) * this->ScaleFactor), 
         (INT)((XCoordinate + Radius) * this->ScaleFactor), 
        -(INT)((YCoordinate - Radius) * this->ScaleFactor)); 
    SelectObject(this->MemoryDC, OldBrush);
    DeleteObject(Brush);
 }

您可以看到,我的DrawCircle函数在应用当前比例因子时没有考虑鼠标位置。

编辑

好的,我接近解决方案,这里是我的鼠标回调函数的更新版本。

VOID OnMouseWheel(WPARAM const& WParam, LPARAM const& LParam) {
    // Get Mouse position in real coordinates and not window coordinates.
    INT XOffset = (Window.GetClientWidth() / -2) + XMousePos;
    INT YOffset = (Window.GetClientHeight() / 2) - YMousePos;


    if(GET_WHEEL_DELTA_WPARAM(WParam) > 0)
    {
        Draw.ScaleFactor += 0.1;
        Draw.XOffsetScale = -XOffset * (Draw.ScaleFactor - 1.0);
        Draw.YOffsetScale = YOffset * (Draw.ScaleFactor - 1.0);
    }
    else
    {
        // ...
    }
}

下面是绘制圆形的函数。

VOID DrawCircle(DOUBLE const& XCoordinate, DOUBLE const& YCoordinate, DOUBLE const& Radius, COLORREF const& Color) {
        HBRUSH Brush = CreateSolidBrush(Color);
        HBRUSH OldBrush = (HBRUSH)SelectObject(this->MemoryDC, Brush);

        Ellipse(this->MemoryDC, (INT) ((XCoordinate - Radius) * this->ScaleFactor + XOffsetScale) , 
            -(INT)((YCoordinate + Radius) * this->ScaleFactor - YOffsetScale), 
            (INT)((XCoordinate + Radius) * this->ScaleFactor + XOffsetScale), 
            -(INT)((YCoordinate - Radius) * this->ScaleFactor - YOffsetScale)); 
        SelectObject(this->MemoryDC, OldBrush);
        DeleteObject(Brush);
 }

只要我保持鼠标在同一位置,它就能正常工作,但是当我移动到其他位置时,它不会按预期缩放,之后再次缩放就会正确。也许这可以帮助一点。

提前感谢!

已解决

好的,我现在解决了我的问题。我只是根据鼠标位置乘以缩放因子移动了我的坐标系原点。谢谢你们的回答。


这与WinAPI有什么关系?你没有提供任何有意义的代码来理解你的问题! - didierc
添加了一些我的代码片段。很抱歉,但我认为这些代码并不能帮助解决一般性的问题。 - roohan
你想链式缩放吗,还是只能缩放一次,然后返回完整视图? - didierc
链接应该是可能的。缩放一次我已经可以做到了。问题是我缩放一次->有效,然后我在不同的鼠标位置第二次缩放,它就无效了。当我保持鼠标位置时,我也可以多次缩放。 - roohan
3个回答

9
最“通用”的解决方案使用矩阵变换,但这里提供一个简化的解释。以下伪代码可能会对你有所帮助:
/*
    VARIABLES (all in space coordinates, not pixel coordinates):

      input:
        viewRect = rectangle of the viewed area
        zoomFactor = factor of zoom relative to viewRect, ex 1.1
        mousePos = position of the mouse

      output:
        zoomedRect = viexRect after zoom
*/

/*
    A little schema:

      viewRect
    *-----------------------------------------------------------------------*
    |                       ^                                               |
    |                       | d_up                                          |
    |        zoomedRect     v                                               |
    |      *-----------------------------------------*                      |
    |d_left|                                         |       d_right        |
    |<---->|                mousePos                 |<-------------------->|
    |      |                    +                    |                      |
    |      |                                         |                      |
    |      |                                         |                      |
    |      *-----------------------------------------*                      |
    |                       ^                                               |
    |                       |                                               |
    |                       |                                               |
    |                       | d_down                                        |
    |                       |                                               |
    |                       v                                               |
    *-----------------------------------------------------------------------*

    dX = d_left + d_right
    dY = d_up + d_down
    The origin of rects is the upper left corner.
*/

/*
    First, find differences of size between zoomed rect and original rect
    Here, 1 / zoomFactor is used, because computations are made relative to the
    original view area, not the final rect):
*/
dX = viewRect.width * (1 - 1 / zoomFactor)
dY = viewRect.height * (1 - 1 / zoomFactor)

/*
    Second, find d_* using the position of the mouse.
    pX = position of the mouse along X axis, relative to viewRect (percentage)
    pY = position of the mouse along Y axis, relative to viewRect (percentage)
    The value of d_right and d_down is not computed because is not directly needed
    in the final result.
*/
pX = (mousePos.X - viewRect.X) / viewRect.width
pY = (mousePos.Y - viewRect.Y) / viewRect.height

d_left = pX * dX
d_up = pY * dY

/*
    Third and last, compute the output rect
*/
zoomedRect = viewRect
zoomedRect.X += d_left
zoomedRect.Y += d_up
zoomedRect.width -= dX
zoomedRect.height -= dY

// That's it!

针对您的问题,您需要将视图(窗口)与场景(绘制的对象)分离开来。您应该有一个函数来绘制场景的一部分(或全部):

void drawScene(Rect viewArea);

同时,还需要一个函数来放大区域(使用之前提到的算法):

Rect zoomArea(Rect rectToZoom, Point zoomCenter, double factor);

现在,您的回调函数变得更加简单:
VOID OnMouseWheel(WPARAM const& WParam, LPARAM const& LParam)
{
    // Get the position of the mouse relative to the window (in percent)
    double XMouseRel = XMousePos / double(Window.GetClientWidth());
    double YMouseRel = YMousePos / double(Window.GetClientHeight());

    // Get Mouse position in scene coordinates and not window coordinates.
    // viewArea is in scene coordinates
    // window = your window or your draw information on the scene
    // The following assumes that you're using a scene with X left-to-right and
    // Y top-to-bottom.
    double XMouse = window.viewArea.width * XMouseRel + window.viewArea.upperleft.X;
    double YMouse = window.viewArea.height * YMouseRel + window.viewArea.upperleft.Y;

    // Zoom parameters
    double zFactor = 0.1 * GET_WHEEL_DELTA_WPARAM(WParam);
    Rect viewArea = getViewArea(); // or something like this
    Point zCenter(XMouse,YMouse);

    // Zoom
    Rect zoomedRect = zoomArea(viewArea,zCenter,zFactor);
    drawScene(zoomedRect);
}

对于这个绘图和好的解释,我给你点赞。问题是我需要转换我的对象的坐标而不是用户所看到的矩形。我仍在评估您的解决方案,我不确定那是否是我正在寻找的。但还是谢谢你。 - roohan
请查看我的编辑。有了矩形,您就知道渲染场景的部分。缩放只是应用于视图矩形的变换。 - Synxis
嗨,感谢您的答案。问题是,如果我使用您的方法,我只会拉伸原始图像并显示拉伸后图像的某个区域。因此,我的所有圆形等对象都不会被缩放。但这正是我想要实现的。如果我表达不清楚,对不起。我想缩放我的对象,因为这样它们在缩放时也具有更多的细节。 - roohan
应该是“显示场景的某个特定区域”,并且保持比例,这样显示场景的一部分就可以让你拥有缩放的圆形。 - Synxis

7
您正在尝试在平面上实现仿射变换的一个子集。在您的情况下,您只需要将绘图平面进行平移和缩放(缩放)。平面中仿射变换的完整可能性涉及使用三维矩阵,但现在我只会给出您问题所需的最小限度。随时在网络上查找完整主题,这方面有大量的文献资料。
首先,我们将声明一个二维向量和一些运算符:
  class vector2D {
  protected:
      /* here your class implementation details */
  public:
      vector2D(const vector2D &v);
      vector2D(float x, float y) { /* ... */ }
      vector2D operator +(const vector2D &v) const { /* ... */ }
      vector2D operator -(const vector2D &v) const { /* ... */ }
      vector2D operator *(float v) const { /* ... */ }
      bool operator ==(const vector2D &v) const { /* ... */ }
      const vector2D &operator = (const vector2D &v) { /* ... */ }
  };

我会让你填写空白或使用自己的类(如果有)。请注意,这个接口可能不是最优的,但我想集中精力研究算法,而不是性能。

现在,让我们来谈谈显示变换:

我们将称zf为缩放因子,trans为变换的平移部分,origin为窗口中视图的原点。你提到你的坐标系统以窗口为中心,因此原点将是窗口屏幕的中心。从视图系统到窗口坐标的转换可以分解为两个单独的阶段:一个是显示对象的缩放和平移,我们将其称为模型视图,另一个是从视图坐标到窗口坐标的转换,我们将其称为投影。如果你对3D渲染熟悉,可以将其视为类似于OpenGL中使用的机制。

投影可以描述为从窗口左上角到视图的origin的简单平移。

  vector2D project(const vector2D &v){
      return v + origin;
  }

modelview将平移和缩放结合在一起(目前,UI代码只处理任意点的缩放)。

  vector2D modelview(const vector2D &v){
      return trans + (v * zf);
  }

我会让您以最方便的方式组织这些功能和相关数据(zfcentretrans)。

接下来,让我们看看UI应如何修改不同的数据。

基本上,您需要将点坐标从放置在视图中心的坐标系更改为以缩放点为中心的系统,然后缩放它们的新坐标,然后返回到视图中心。您希望绘制的每个对象都必须经过此转换。

公式如下:

v' = (v + zp) * s - zp

其中,zp是缩放点,s是缩放因子,v是要进行变换的系统中的点的坐标,因此v'是结果的缩放点。

如果您想在不同的地方链接缩放,则需要考虑先前的缩放因子和中心:

如果c是新的缩放中心,t是当前平移,z是当前缩放因子,z2是新的缩放因子,则可以使用以下公式计算新的全局变换:

t' = t + c * (1 - z2) z' = z * z2

这些公式来自于将坐标系移动到缩放中心,对变换应用缩放,然后移回原点。

关于缩放中心,您必须注意鼠标输入将在窗口坐标系中,因此必须转换回您的视图系统(以origin为中心)。 以下的unproject函数正是这样做的:

 vector2D unproject(const vector2D &v){
     return v - origin;
 }

最后,让我们来实现一个简单的函数,根据新的输入来转换模型视图转换:
 void onMouseWheel(float mouseX, float mouseY, bool zoom_in){
     float z2 = zoom_in? 1.1 : 1/1.1;
     vector2D m(mouseX,mouseY);
     if (! (m == origin)) { // this is very likely
         trans = trans + unproject(m) * (1.0 - z2);
     }
     zf *= z2;
     // here perhaps have a redraw event fired
 }

正如您所见,我提供了更通用的代码,您需要根据Win32 API的具体要求进行调整。


这看起来非常像我问题的解决方案!我现在要尝试实施并查看它是否有效。非常感谢! - roohan
1
在我开始编程之前,我进行了一些简单的计算,我不确定我是否正确理解了您的公式。如果我的点具有坐标x=200和y=0,则我的坐标系原点位于x=0和y=0处,初始缩放因子为1。现在我的鼠标位于位置x=200和y=0,并且我的新缩放因子应该为2。如果我将这些值放入您的公式中,我会得到一个意外的结果。x'=(200+1*200+0)12=800。但是,如果鼠标在与点相同的位置,我希望点保持在其初始坐标上。我在您的公式中理解错了什么吗? - roohan
我重新做了答案,但还是有一个错误。如果Synxis的解决方案适用于您,请不要使用我的。 - didierc
嘿,再次感谢您的答案,也感谢您提供非常好的描述。正如我在主贴中已经提到的,我已经通过根据鼠标位置和缩放因子转换我的坐标系来解决问题。现在这个方法运行得相当不错。无论如何,您的解决方案是更通用的解决方案,感谢您的发布。 - roohan

0
你需要做的是将坐标转换,使得鼠标点位于(0,0),然后缩放坐标,最后将(0,0)偏移回鼠标坐标。
Ellipse(this->MemoryDC, (INT) (((XCoordinate - XMouse) - Radius) * this->ScaleFactor) + XMouse, 
    -(INT)(((YCoordinate - YMouse) + Radius) * this->ScaleFactor) + YMouse, 
     (INT)(((XCoordinate - XMouse) + Radius) * this->ScaleFactor) + XMouse, 
    -(INT)(((YCoordinate - YMouse) - Radius) * this->ScaleFactor) + YMouse); 

嗯,某种方式这不起作用。我正在进一步调查以找出错误所在。 - roohan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接