WarpPointer question

If you are using the main C++ distribution of wxWidgets, Feel free to ask any question related to wxWidgets development here. This means questions regarding to C++ and wxWidgets, not compile problems.
Post Reply
CktDesigner
Earned a small fee
Earned a small fee
Posts: 17
Joined: Mon Sep 21, 2015 5:16 am

WarpPointer question

Post by CktDesigner » Wed Jan 15, 2020 7:43 pm

I'm trying to add a zoom function to a wxWidgets application. I'm using 3.1.2 on both Linux and Windows.
I want the drawing to "zoom" when the mouse wheel is rotated, and for the position of the mouse when the rotation occurs to be zoomed and placed in the center of the window.
I have most of this working, but would also like the cursor to end up in the center of the window (where the zoomed diagram ends up).
I use GetPosition() to get the original position of the mouse cursor when the wheel is rotated, the perform the scaling, and offset the origin to move things to the center of the window. I use WarpPointer to set the cursor to the center coordinates. Refresh() is called. Subsequent calls to GetPosition() show the new mouse location to be the center of the window.
However, the actual cursor on the screen stays in its original position.
Is there some other call (or event creation) needed to update the screen cursor to the new the mouse position?
Many thanks...

PB
Part Of The Furniture
Part Of The Furniture
Posts: 2160
Joined: Sun Jan 03, 2010 5:45 pm

Re: WarpPointer question

Post by PB » Wed Jan 15, 2020 8:48 pm

Moving the mouse cursor on screen seems to work just fine for me with the master on MSW doing this, moving the cursor directly in the mouse wheel event handler

Code: Select all

#include <wx/wx.h>

class MyFrame : public wxFrame
{
public:
    MyFrame() : wxFrame(nullptr, wxID_ANY, "Mouse Wheel to Center Cursor")
    {
        Bind(wxEVT_MOUSEWHEEL, 
            [this](wxMouseEvent&)
            {
                const wxRect r = GetClientRect();
                
                WarpPointer(r.GetWidth() / 2, r.GetHeight() / 2);
            });
    }
};

class MyApp : public wxApp
{
public:
    bool OnInit() override
    {
        (new MyFrame())->Show();        
        return true;
    }
}; wxIMPLEMENT_APP(MyApp);
Assuming the code above works on your setup and also assuming you are calling WarpPointer on the correct window with the correct coordinates. You may be doing something in your code which hinders the mouse cursor from moving? I have no idea what that could be, as always in similar situations, maybe trying CallAfter() could prove yourself, even if I doubt that in this case.

User avatar
doublemax
Moderator
Moderator
Posts: 14518
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: WarpPointer question

Post by doublemax » Wed Jan 15, 2020 9:12 pm

Just for the record: IMHO moving the mouse pointer programmatically is horrible from a GUI design point of view.
Use the source, Luke!

CktDesigner
Earned a small fee
Earned a small fee
Posts: 17
Joined: Mon Sep 21, 2015 5:16 am

Re: WarpPointer question

Post by CktDesigner » Thu Jan 16, 2020 12:07 am

I agree that it is not good practice to move the mouse pointer programmatically...

However, I want the behavior of using the wheel to zoom (in/out) with the position of the cursor determining which part of the image/diagram should be shown in the center of the window. So if I want the see the upper left of the diagram in more detail, the cursor is placed over that area and the wheel is turned. The wheel event takes the mouse position as the new center, does the zoom, then translates (adjusts origin) to provide the zoomed area in the center of the window. A second wheel click should provide additional zoom, but if the cursor isn't moved, the "wrong" area of the screen will now be seen as the "area of interest". My conclusion was that moving the cursor to the center of the screen after performing the zoom and translate would solve this problem (as it then appears that the cursor is in the same relative position it was when the wheel was first clicked)...

Are there better ways to accomplish the behavior I'm looking for?

Thanks!

User avatar
doublemax
Moderator
Moderator
Posts: 14518
Joined: Fri Apr 21, 2006 8:03 pm
Location: $FCE2

Re: WarpPointer question

Post by doublemax » Thu Jan 16, 2020 12:43 am

Most applications ignore the mouse position when using the scroll wheel for zooming. If the user wants to zoom into a specific area, he can use a rubber-band to select the area.

Another option when using the scroll wheel is to zoom in a way that the point under the cursor always remains the same after zooming. Switching between these two modes is a matter of preference and should be selectable in program options.
Use the source, Luke!

ONEEYEMAN
Part Of The Furniture
Part Of The Furniture
Posts: 3799
Joined: Sat Apr 16, 2005 7:22 am
Location: USA, Ukraine

Re: WarpPointer question

Post by ONEEYEMAN » Thu Jan 16, 2020 3:32 pm

Hi,
Just look at how Google Maps doing zooming.
Open it on any desktop and try to zoom/pan the map.

Thank you.

CktDesigner
Earned a small fee
Earned a small fee
Posts: 17
Joined: Mon Sep 21, 2015 5:16 am

Re: WarpPointer question

Post by CktDesigner » Thu Jan 16, 2020 3:49 pm

Oneeyeman: Yes, this is exactly what I'm trying to accomplish... Calculating the offset for the new Origin (after scaling) has been somewhat trickey...

But also wanted another option where "zoomed item" is popped to the center of the window (much like the option in Microsoft Visio).

Manolo
Can't get richer than this
Can't get richer than this
Posts: 709
Joined: Mon Apr 30, 2012 11:07 pm

Re: WarpPointer question

Post by Manolo » Fri Jan 17, 2020 1:39 am

Zoom around the current mouse position is done like this:
a) Calculate the zoom factor from the mouse wheel event. Notice it can be greater or less than one. Multiply the factor for the current window/rectangle/block/whatever sizes. Now you have new horizontal/vertical sizes.
b) Get current mouse position. This is the centre of the new sized block.
c) The new corners of the block are center +- newsize/2. Be aware these corners may lay out of the window, so you need to cut/extend the corners to fit into the new scaled window.
d) Redraw new block.

The mouse position does NOT change.

If the mouse wheel only changes the zoom then I suggest to do this zoom over the current centre of the whole window, without moving the mouse.

If the user can select a rectangle which will be zoomed to fill the whole window, then I advise to draw this transparent rectangle capturing the mouse and using a wxClientDC instead of the common wxPaintDC. Then when the user ends holding a mouse button down then issue a "refresh" and redraw the new window in a wxPaintDC.

New Pagodi
Super wx Problem Solver
Super wx Problem Solver
Posts: 326
Joined: Tue Jun 20, 2006 6:47 pm
Contact:

Re: WarpPointer question

Post by New Pagodi » Mon Jan 20, 2020 12:09 am

CktDesigner wrote:
Thu Jan 16, 2020 12:07 am
I agree that it is not good practice to move the mouse pointer programmatically...

However, I want the behavior of using the wheel to zoom (in/out) with the position of the cursor determining which part of the image/diagram should be shown in the center of the window. So if I want the see the upper left of the diagram in more detail, the cursor is placed over that area and the wheel is turned. The wheel event takes the mouse position as the new center, does the zoom, then translates (adjusts origin) to provide the zoomed area in the center of the window. A second wheel click should provide additional zoom, but if the cursor isn't moved, the "wrong" area of the screen will now be seen as the "area of interest". My conclusion was that moving the cursor to the center of the screen after performing the zoom and translate would solve this problem (as it then appears that the cursor is in the same relative position it was when the wheel was first clicked)...

Are there better ways to accomplish the behavior I'm looking for?

Thanks!
Here's a minimal demo app that shows how to implement google maps style pan and zoom.

Code: Select all

// For compilers that support precompilation, includes "wx/wx.h".
#include "wx/wxprec.h"

#ifdef __BORLANDC__
    #pragma hdrstop
#endif

// for all others, include the necessary headers (this file is usually all you
// need because it includes almost all "standard" wxWidgets headers)
#ifndef WX_PRECOMP
    #include "wx/wx.h"
#endif

#include <wx/graphics.h>
#include <wx/dcbuffer.h>

class PanAndZoomCanvas:public wxWindow
{
    public:
        PanAndZoomCanvas(wxWindow *parent,
                         wxWindowID id = wxID_ANY,
                         const wxPoint &pos=wxDefaultPosition,
                         const wxSize &size=wxDefaultSize,
                         long style=0,
                         const wxString &name="PanAndZoomCanvas");
        wxRect2DDouble GetUntransformedRect() const;

    protected:
        void DoDrawCanvas(wxGraphicsContext*);

    private:
        void OnPaint(wxPaintEvent&);

        void OnMouseWheel(wxMouseEvent&);

        void OnLeftDown(wxMouseEvent&);
        void OnMotion(wxMouseEvent&);
        void OnLeftUp(wxMouseEvent&);
        void OnCaptureLost(wxMouseCaptureLostEvent&);

        void ProcessPan(const wxPoint&,bool);
        void FinishPan(bool);

        int m_zoomFactor;

        wxPoint2DDouble m_panVector;
        wxPoint2DDouble m_inProgressPanVector;
        wxPoint m_inProgressPanStartPoint;
        bool m_panInProgress;
};

PanAndZoomCanvas::PanAndZoomCanvas(wxWindow *parent, wxWindowID id,
                                   const wxPoint &pos, const wxSize &size,
                                   long style, const wxString &name)
                 :wxWindow(parent,  id, pos, size,  style, name)
{
    Bind(wxEVT_PAINT,&PanAndZoomCanvas::OnPaint,this);
    Bind(wxEVT_MOUSEWHEEL,&PanAndZoomCanvas::OnMouseWheel,this);
    Bind(wxEVT_LEFT_DOWN,&PanAndZoomCanvas::OnLeftDown,this);

    SetBackgroundStyle(wxBG_STYLE_PAINT);

    m_zoomFactor = 100;

    m_panVector = wxPoint2DDouble(0,0);
    m_inProgressPanStartPoint = wxPoint(0,0);
    m_inProgressPanVector = wxPoint2DDouble(0,0);
    m_panInProgress = false;
}

void PanAndZoomCanvas::DoDrawCanvas(wxGraphicsContext* gc)
{
    gc->SetPen(*wxBLACK_PEN);

    wxGraphicsPath path = gc->CreatePath();
    path.MoveToPoint(100,100);
    path.AddLineToPoint(300,100);
    path.AddLineToPoint(300,300);
    path.CloseSubpath();
    gc->StrokePath(path);
}

void PanAndZoomCanvas::OnPaint(wxPaintEvent& WXUNUSED(event))
{
    wxAutoBufferedPaintDC dc(this);
    dc.Clear();

    wxGraphicsContext* gc = wxGraphicsContext::Create(dc);

    if ( gc )
    {
        double a = m_zoomFactor / 100.0;
        wxPoint2DDouble totalPan = m_panVector + m_inProgressPanVector;

        gc->Translate(-totalPan.m_x, -totalPan.m_y);
        gc->Scale(a, a);

        DoDrawCanvas(gc);

        delete gc;
    }
}

void PanAndZoomCanvas::OnMouseWheel(wxMouseEvent& event)
{
    if ( m_panInProgress )
    {
        FinishPan(false);
    }

    int rot = event.GetWheelRotation();
    int delta = event.GetWheelDelta();

    int oldZoom = m_zoomFactor;
    m_zoomFactor += 10*(rot/delta);

    if ( m_zoomFactor<10 )
    {
        m_zoomFactor = 10;
    }

    if ( m_zoomFactor>800)
    {
        m_zoomFactor = 800;
    }

    double a = oldZoom / 100.0;
    double b = m_zoomFactor / 100.0;

    // Set the panVector so that the point below the cursor in the new
    // scaled/panned cooresponds to the same point that is currently below it.
    wxPoint2DDouble uvPoint = event.GetPosition();
    wxPoint2DDouble stPoint = uvPoint + m_panVector;
    wxPoint2DDouble xypoint  = stPoint/a;
    wxPoint2DDouble newSTPoint  = b * xypoint;
    m_panVector = newSTPoint - uvPoint;

    Refresh();
}

void PanAndZoomCanvas::ProcessPan(const wxPoint& pt, bool refresh)
{
    m_inProgressPanVector = m_inProgressPanStartPoint - pt;

    if ( refresh )
    {
        Refresh();
    }
}

void PanAndZoomCanvas::FinishPan(bool refresh)
{
    if ( m_panInProgress )
    {
        SetCursor(wxNullCursor);

        if ( HasCapture() )
        {
            ReleaseMouse();
        }

        Unbind(wxEVT_LEFT_UP, &PanAndZoomCanvas::OnLeftUp, this);
        Unbind(wxEVT_MOTION, &PanAndZoomCanvas::OnMotion, this);
        Unbind(wxEVT_MOUSE_CAPTURE_LOST, &PanAndZoomCanvas::OnCaptureLost, this);

        m_panVector += m_inProgressPanVector;
        m_inProgressPanVector = wxPoint2DDouble(0,0);
        m_panInProgress = false;

        if ( refresh )
        {
            Refresh();
        }
    }
}

wxRect2DDouble PanAndZoomCanvas::GetUntransformedRect() const
{
    double a = m_zoomFactor / 100.0;

    wxSize sz = GetSize();
    wxPoint2DDouble zero  = m_panVector/a;

    return wxRect2DDouble(zero.m_x, zero.m_y, sz.GetWidth()/a, sz.GetHeight()/a);
}

void PanAndZoomCanvas::OnLeftDown(wxMouseEvent& event)
{
    wxCursor cursor(wxCURSOR_HAND);
    SetCursor(cursor);

    m_inProgressPanStartPoint = event.GetPosition();
    m_inProgressPanVector = wxPoint2DDouble(0,0);
    m_panInProgress = true;

    Bind(wxEVT_LEFT_UP, &PanAndZoomCanvas::OnLeftUp, this);
    Bind(wxEVT_MOTION, &PanAndZoomCanvas::OnMotion, this);
    Bind(wxEVT_MOUSE_CAPTURE_LOST, &PanAndZoomCanvas::OnCaptureLost, this);

    CaptureMouse();
}

void PanAndZoomCanvas::OnMotion(wxMouseEvent& event)
{
    ProcessPan(event.GetPosition(), true);
}

void PanAndZoomCanvas::OnLeftUp(wxMouseEvent& event)
{
    ProcessPan(event.GetPosition(), false);
    FinishPan(true);
}

void PanAndZoomCanvas::OnCaptureLost(wxMouseCaptureLostEvent&)
{
    FinishPan(true);
}

class MyFrame : public wxFrame
{
    public:
        MyFrame(wxWindow* parent, int id = wxID_ANY, wxString title = "Demo",
                wxPoint pos = wxDefaultPosition, wxSize size = wxDefaultSize,
                int style = wxDEFAULT_FRAME_STYLE );
};

MyFrame::MyFrame( wxWindow* parent, int id, wxString title, wxPoint pos
                 , wxSize size, int style )
        :wxFrame( parent, id, title, pos, size, style )
{
    PanAndZoomCanvas* canvas = new PanAndZoomCanvas(this);
}

class myApp : public wxApp
{
    public:
        virtual bool OnInit()
        {
            MyFrame* frame = new MyFrame(NULL);
            frame->Show();
            return true;
        }
};

wxIMPLEMENT_APP(myApp);


panandzoom2.gif
(click to see animation.)

Post Reply