Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Zooming into a window based on the mouse position

Tags:

c++

winapi

Hi im writing a win32 application in C++ at the moment and I have really a problem to get zooming into the content of my window. Here is the pseudo code I started with to get zooming done:

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

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

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

My coordinate system is set up to have its origin in the center of the window. I would like to zoom when the mouse wheel is used. The problem with the solution above is that zooming always happens from the center of the window. This kinda looks ugly when your mouse isnt at the center of the window. I would like to zoom into the region where the mouse is but i can not find a suitable algorithm to caclulate the offsets in x and y direction. For example if the mouse has the coordinates (-200, 0) point one should have the coordinates (-200, 0) and point two the coordinates (600, 0) with a scaling factor of two. I already tried alot of things but didnt get it to work especially when the mouse moves to other locations between zooming everything gets messed up. Anyone knows how to solve this problem?

Here is some example code of my appliaciton. The first snippet is my callback function for handling the WM_MOUSEWHEEL message.

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 is simply a class that wraps the GDI functions. It has a scaling factor member. The snippet below is the DrawCircle member function of my Draw object using the scale factor to present the circle correctly on the screen.

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);
 }

As you can see my DrawCircle function does not take the mouse position into account when applying the current scale factor.

EDIT

Ok i got closer to the solution here is the updated version of my mouse callback function.

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
    {
        // ...
    }
}

And here is the function that draws the circle.

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);
 }

This does work as long as i keep the mouse at the same position but when i move to another position it doesnt zoom as expected once after that it zooms correctly again. Maybe this helps a bit.

Thanks in advance!

Solved

Ok, I solved my Problem now. I just moved the origin of my coordinate system based on the mouse position multiplied with the scaling factor. Thank for your answers.

like image 257
roohan Avatar asked Nov 09 '12 21:11

roohan


People also ask

How do you zoom in with a mouse pointer?

If your mouse does not have a wheel, hold the Windows key and + (plus) or - (minus) to increase or decrease magnification.

Why is my cursor a magnifying glass?

Simply go into your control panel>hardware>devices>mouse and turn off the multitouch gestures. Uncheck all the boxes and click apply. If you still want to use the multitouch gestures, just re-check all the boxes and click apply again. Hope this solves your problem.

How do I zoom out on Windows mouse?

If you are on Microsoft Edge, you can click the three dots in the top corner and then click the Xoom button five down. If you have a mouse, you can hold CTRL and scroll forwards or backwards to zoom in as well. Or, press CTRL + and CTRL - to zoom in and out. Thanks!

How do I stop my mouse from zooming in Windows 10?

Click Additional mouse options under Related settings. Go to the Device Settings tab, then click the Settings button. Click Pinch Zoom from the left pane, then uncheck the Enable Pinch Zoom box. Click the Apply button, then OK.


2 Answers

The most 'general-ish' solution uses matrix transformations, but here is a simplified explanation. The following pseudo-code might help you:

/*
    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!

For your problem, you need to separate the view (your window) from the scene (objects that are drawed). You should have a function drawing a part of (or all) the scene:

void drawScene(Rect viewArea);

and a function zooming an area (using the algorithm presented before):

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

Now, your callback is a lot more simpler:

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);
}
like image 191
Synxis Avatar answered Sep 27 '22 22:09

Synxis


You are trying to implement a subset of affine transformations in the plane. In your case, you only need to combine a translation and a scaling (zooming) of your drawing plane. The full set of possibilities of affine transformations in the plane involves using matrices of 3 dimensions, but for now I'll just give the minimum necessary for your problem. Feel free to look up the full topic on the net, there's plenty of literature on this.

First of all, we'll declare a 2D vector, and some operators:

  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) { /* ... */ }
  };

I'll let you fill out the blanks, or use your own class if you have one. Note that this interface might not be optimal, but I want to concentrate on the algorithms, rather that performance.

Now, let's cover the display transformations:

We'll call zf the zooming factor, trans the translation part of the transformation, and origin the origin the view in the window. You mentioned that your coord system is centred in the window, thus the origin would be that centre of the window screen. The transformation from the view system to the window coordinate can be decomposed in two separate stages: one which will be the zooming and translating of the displayed objects, which we'll call modelview, and one which will be the translation from the view coords to the window coords, which we'll call projection. If you are familiar with 3D rendering, this can be seen as an analogous mechanism to the one used in OpenGL.

The projection can be described as a simple translation from top left of the window to the origin of the view.

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

The modelview combines translations and zooming (at this point, the UI code will only deal with zooming at arbitrary points).

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

I'll let you organize these functions and relevant data (zf, centre, trans) the most convenient way for you.

Next, let's see how the different data should be modified by the UI.

Basically, you need to change your point coordinates from a coordinate system placed at the centre of your view to the system centred at the zooming point, then have their new coordinates scaled, then go back to the view centre. Each object that you wish to draw will have to undergo this transformation.

the formula is then:

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

where zp is the zooming point, s is the scaling factor, v is the coordinate of the point in the system which is to be transformed, and thus v' is the resulting zoomed point.

If you want to chain zooming in different places, you need to take in account the precedent zooming factor and centre:

if c is the new zoom centre, t the current translation, z the current zoom factor, and z2 is the new zoom factor, then we can compute the new global transformation with:

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

These are derived from moving the coordinate system to the zooming centre, applying zooming to the transformation, and moving back to the origin.

Regarding the zooming centre, you must be careful about the fact that the mouse input will be in the window coordinate system, and thus must be converted back to your view system (centred on origin). The following unproject function does exactly that:

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

Finally, let's have a simple implementation of the function transforming the modelview transformation according to new input:

 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
 }

As you can see, I provided more or less generic code, which you'll have to adapt to the specificities of the Win32 API.

like image 37
didierc Avatar answered Sep 27 '22 21:09

didierc