Open asmwarrior opened 1 month ago
For example, if you ever use python and matplotlib, you will see its toolbar is very nice, see below:
Interactive navigation — Matplotlib 3.2.2 documentation
When you click on the toolbar button, the "zoom" or "pan" feature can be enabled or not.
So, I'd like to see a similar feature in my own interactive application, because I plan to create some kinds of doodle application, which can draw lines by mouse, and later I can save the poly-lines with the mathplot's x,y coordinates, so later I can reload those lines again to the plot.
I maybe can add a callback just before the last event.Skip(); to run your code ?
Something like that :
if (m_OnUserMouseAction) m_OnUserMouseAction(this, event);
I maybe can add a callback just before the last event.Skip(); to run your code ? Something like that :
if (m_OnUserMouseAction) m_OnUserMouseAction(this, event);
Thanks for the help.
I think putting the user action(function call) before the event.Skip();
is not correct, because all the native mathplot's code about mouse handling is already done.
What I need is that the user action code could be put at the beginning of the event handler function.
One method: you can have an option to "disable" the build-int mouse handling(like zoom or pan). And if user want to add some custom action, they can use "Bind" function to add a dynamic event handler.
Yes but I don't want to break the present behaviour. I try something with callback. With this, your can do your stuff and ignore or no the official behaviour.
In your frame class, you just will have :
in Public section :
void OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel);
.... in m_plot create :
m_plot->SetOnUserMouseAction([this](void *Sender, wxMouseEvent &event, bool &cancel)
{ OnUserMouseAction(Sender, event, cancel);});
And your specific action :
void MyFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
wxMessageBox(wxString::Format("Type = %d", event.GetEventType()), ((mpWindow *)Sender)->GetLabel(),
wxOK | wxICON_INFORMATION, this);
// your code
cancel = false; // if you want continue the normal code
}
I put in Sandbox the code.
Hi, thanks for the work.
Normally, I think a lot of mouse event handler need to have a "user action".
EVT_MIDDLE_UP(mpWindow::OnShowPopupMenu) // this need user action
EVT_RIGHT_DOWN(mpWindow::OnMouseRightDown) // this need user action
EVT_RIGHT_UP (mpWindow::OnShowPopupMenu) // this need user action
EVT_MOUSEWHEEL(mpWindow::OnMouseWheel ) // this need user action
EVT_MOTION(mpWindow::OnMouseMove)// JLB // this need user action
EVT_LEAVE_WINDOW(mpWindow::OnMouseLeave)
EVT_LEFT_DOWN(mpWindow::OnMouseLeftDown) // this need user action
EVT_LEFT_UP(mpWindow::OnMouseLeftRelease) // this need user action
So, do I need to add all the user action functor to those build-in event handler?
It looks complex, and that's why I suggest a global bool variable to control the user mode and build-in mouse handling. (like the way Matplotlib's toolbar do, when the zoom or pan toolbar button is pressed on, the zoom/pan feature is enabled)
Maybe, you need to add the same code (see below) to every mouse event handler function's beginning?
void mpWindow::OnMouseLeftDown(wxMouseEvent &event)
{
if (m_OnUserMouseAction != NULL)
{
bool cancel = true;
m_OnUserMouseAction(this, event, cancel);
if (cancel)
{
event.Skip();
return;
}
}
// build in mouse event handling
So that in all mouse event handler, the same m_OnUserMouseAction function will be called.
And the user need to check which event type he is interested to handle.
See: https://docs.wxwidgets.org/3.2.5/classwx_mouse_event.html
Am I correct?
It looks like my guess is correct.
Here is the code:
wxString GetEventTypeString(int eventType)
{
if (eventType == wxEVT_LEFT_DOWN)
return "mouse left down";
else if (eventType == wxEVT_LEFT_UP)
return "mouse left up";
else if (eventType == wxEVT_RIGHT_DOWN)
return "mouse right down";
else if (eventType == wxEVT_RIGHT_UP)
return "mouse right up";
else if (eventType == wxEVT_MOTION)
return "mouse motion";
else if (eventType == wxEVT_KEY_DOWN)
return "key down";
else if (eventType == wxEVT_KEY_UP)
return "key up";
// Add more conditions for other event types as needed
else
return wxString::Format("unknown event (%d)", eventType);
}
void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
wxString eventTypeString = GetEventTypeString(event.GetEventType());
wxString label = ((mpWindow *)Sender)->GetLabel();
wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));
// your code
cancel = true; // set to false, if you want continue the normal code
}
I have enabled the console window by using this function call:
wxLog::SetActiveTarget(new wxLogStream(&std::cout));
mPlot->SetOnUserMouseAction([this](void *Sender, wxMouseEvent &event, bool &cancel)
{ OnUserMouseAction(Sender, event, cancel);});
Now it works, I mean I can print all the mouse event type strings in the console window.
like below:
...
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse motion, Label = Mathplot
17:21:33: Type = mouse left down, Label = Mathplot
17:21:34: Type = mouse left up, Label = Mathplot
17:21:35: Type = mouse left down, Label = Mathplot
17:21:35: Type = mouse left up, Label = Mathplot
17:21:35: Type = mouse right down, Label = Mathplot
17:21:36: Type = mouse right up, Label = Mathplot
17:21:36: Type = mouse right down, Label = Mathplot
17:21:36: Type = mouse right up, Label = Mathplot
17:21:37: Type = mouse motion, Label = Mathplot
17:21:37: Type = mouse motion, Label = Mathplot
17:21:37: Type = mouse motion, Label = Mathplot
17:21:37: Type = mouse motion, Label = Mathplot
...
Here is my codes to use the mouse to draw a polyline
wxString GetEventTypeString(int eventType)
{
if (eventType == wxEVT_LEFT_DOWN)
return "mouse left down";
else if (eventType == wxEVT_LEFT_UP)
return "mouse left up";
else if (eventType == wxEVT_RIGHT_DOWN)
return "mouse right down";
else if (eventType == wxEVT_RIGHT_UP)
return "mouse right up";
else if (eventType == wxEVT_MOTION)
return "mouse motion";
else if (eventType == wxEVT_KEY_DOWN)
return "key down";
else if (eventType == wxEVT_KEY_UP)
return "key up";
// Add more conditions for other event types as needed
else
return wxString::Format("unknown event (%d)", eventType);
}
void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
wxString eventTypeString = GetEventTypeString(event.GetEventType());
wxString label = ((mpWindow *)Sender)->GetLabel();
wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));
// Get the mouse position relative to the mpWindow
wxPoint mousePosition = event.GetPosition();
// Cast Sender to mpWindow and convert the coordinates
mpWindow *plotWindow = (mpWindow *)Sender;
double plotX, plotY;
plotX = plotWindow->p2x(mousePosition.x);
plotY = plotWindow->p2y(mousePosition.y);
wxLogMessage(wxString::Format("Mouse Position in Plot Coordinates: X = %f, Y = %f", plotX, plotY));
// Left mouse button down
if (event.LeftDown())
{
wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
// Start dragging, add the initial point
isDragging = true;
points.clear();
points.push_back(wxRealPoint(plotX, plotY));
}
// Mouse dragging with left button held down
else if (event.Dragging() && event.LeftIsDown())
{
if (isDragging)
{
wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
// Update the last point during dragging
points.push_back(wxRealPoint(plotX, plotY));
//plotWindow->Update();
}
}
// Left mouse button released
else if (event.LeftUp())
{
if (isDragging)
{
// Finalize the polyline by adding the last point
points.push_back(wxRealPoint(plotX, plotY));
isDragging = false;
AddNewPolyline();
//plotWindow->Update();
}
}
// Your code
cancel = true; // Set to false if you want to continue the normal code
}
// Override the drawing function to draw the polyline
void MathPlotDemoFrame::AddNewPolyline()
{
mpFXYVector* plotLayer = new mpFXYVector(); // Assuming mpFXYVector for drawing
std::vector<double> xData, yData;
for (const auto& point : points)
{
xData.push_back(point.x);
yData.push_back(point.y);
}
plotLayer->SetData(xData, yData);
// Customize the appearance of the polyline (optional)
plotLayer->SetPen(wxPen(*wxBLUE, 2)); // Set line color and thickness
plotLayer->SetContinuity(true);
mPlot->AddLayer(plotLayer, true); // Add the layer to the plot
// mPlot->Update();
// mPlot->Fit(); // Fit the plot to show all points
}
And I have some member variables in the main frame class:
void OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel);
std::vector<wxRealPoint> points; // Store the points of the polyline
bool isDragging = false; // Track if the user is dragging the mouse
void AddNewPolyline();
And here is the screen cast of user actions to draw the polyline.
https://github.com/user-attachments/assets/6e601404-a59a-4d5c-ba09-a8557475a4bc
I think the next step is:
I need to draw some lines(they are temporary lines when I drag the mouse) in the pixel coordinates when mouse motion, when I finally mouse button up, I need to remove the those lines and make the polyline in the plot(call the AddNewPolyline() function).
I'm not sure it is easy to do that, because this kinds of rubber band selection.
Yes, for the test, I just put the code in "mouse left down" event but the structure of the callback is intended to work for other events. I see that is what you do. So I can validate the test and push it on the main branch.
If necessary, I can add some other parameters to the callback. But with the Sender parameter, we can access to all the plot parameters :+1:
Hi, thanks.
I'm currently learning how to use the wxDCOverlay class, it looks like this is the way I need to use when I repaint the window in mouse motion event handler.
See discussion here:
wxWidgets: wxOverlay Class Reference
The code I'm learning is the "drawing" sample code from wxWidgets.
Currently, I can access all the functions inside the mpWindow class.
Note : You don't need a point buffer. You can draw directly your polyline :
void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
wxString eventTypeString = GetEventTypeString(event.GetEventType());
wxString label = ((mpWindow *)Sender)->GetLabel();
// wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));
// Get the mouse position relative to the mpWindow
wxPoint mousePosition = event.GetPosition();
// Cast Sender to mpWindow and convert the coordinates
mpWindow *plotWindow = (mpWindow *)Sender;
double plotX, plotY;
plotX = plotWindow->p2x(mousePosition.x);
plotY = plotWindow->p2y(mousePosition.y);
// wxLogMessage(wxString::Format("Mouse Position in Plot Coordinates: X = %f, Y = %f", plotX, plotY));
// Left mouse button down
if (event.LeftDown())
{
// wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
// Start dragging, add the initial point
isDragging = true;
points.clear();
points.push_back(wxRealPoint(plotX, plotY));
CurrentPolyline = new mpFXYVector("New polyline");
CurrentPolyline->AddData(plotX, plotY, false);
wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
CurrentPolyline->SetPen(wxPen(random_color, 2));
CurrentPolyline->SetContinuity(true);
plotWindow->AddLayer(CurrentPolyline);
}
// Mouse dragging with left button held down
else if (event.Dragging() && event.LeftIsDown())
{
if (isDragging)
{
// wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
// Update the last point during dragging
points.push_back(wxRealPoint(plotX, plotY));
//plotWindow->Update();
CurrentPolyline->AddData(plotX, plotY, true);
}
}
// Left mouse button released
else if (event.LeftUp())
{
if (isDragging)
{
// Finalize the polyline by adding the last point
points.push_back(wxRealPoint(plotX, plotY));
isDragging = false;
// AddNewPolyline();
//plotWindow->Update();
CurrentPolyline->AddData(plotX, plotY, true); // Last point
}
}
// Your code
cancel = true; // Set to false if you want to continue the normal code
}
And the member private variable :
mpFXYVector* CurrentPolyline = NULL;
CurrentPolyline->AddData(plotX, plotY, true);
Will this code cause too much repaint, and draw too many times when mouse motion.
Yes repaint for each point but with my (old) computer, I don't see flashing.
You can plot one point over x points with a modulo for example, one over 10 :
CurrentPolyline->AddData(plotX, plotY, (counter++ % 10));
and define a static counter.
Note that with this, we have a strange behaviour : line is slobbery !
Oups, I forgot the == 0 !
Anyway, you have two options :
CurrentPolyline->AddData(plotX, plotY, false); if ((counter++ % 5) == 0) plotWindow->Refresh();
Or
CurrentPolyline->AddData(plotX, plotY, (counter++ % 5) == 0);
With the second, you have a dotted line until the final point (you need a plotWindow->Refresh();
for the last point).
And I correct a potential bug, not sure but not bad.
Another remark in your code. Maybe it is better to initialize cancel to false at the beginning. And set cancel to true only when you have captured the event. With that, you keep the normal behaviour for other events (like wheel, ...)
I see the latest code that the comment is wrong for the new added function:
@@ -2770,6 +2789,13 @@ class WXDLLIMPEXP_MATHPLOT mpWindow: public wxWindow
m_OnDeleteLayer = event;
}
+ /** On delete layer event
+ @return reference to event */
+ void SetOnUserMouseAction(mpOnUserMouseAction event)
+ {
+ m_OnUserMouseAction = event;
+ }
+
This is a copy-paste error.
This is my current code:
void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
wxString eventTypeString = GetEventTypeString(event.GetEventType());
wxString label = ((mpWindow *)Sender)->GetLabel();
// wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));
// Get the mouse position relative to the mpWindow
wxPoint mousePosition = event.GetPosition();
static int counter = 0;
// Cast Sender to mpWindow and convert the coordinates
mpWindow *plotWindow = (mpWindow *)Sender;
double plotX, plotY;
plotX = plotWindow->p2x(mousePosition.x);
plotY = plotWindow->p2y(mousePosition.y);
// wxLogMessage(wxString::Format("Mouse Position in Plot Coordinates: X = %f, Y = %f", plotX, plotY));
// Left mouse button down
if (event.LeftDown())
{
// wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
// Start dragging, add the initial point
isDragging = true;
points.clear();
points.push_back(wxRealPoint(plotX, plotY));
counter = 0;
CurrentPolyline = new mpFXYVector("New polyline");
CurrentPolyline->AddData(plotX, plotY, false);
wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
CurrentPolyline->SetPen(wxPen(random_color, 2));
CurrentPolyline->SetContinuity(true);
plotWindow->AddLayer(CurrentPolyline);
}
// Mouse dragging with left button held down
else if (event.Dragging() && event.LeftIsDown())
{
if (isDragging)
{
// wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
// Update the last point during dragging
points.push_back(wxRealPoint(plotX, plotY));
//plotWindow->Update();
// CurrentPolyline->AddData(plotX, plotY, true);
CurrentPolyline->AddData(plotX, plotY, (counter++ % 5) == 0);
}
}
// Left mouse button released
else if (event.LeftUp())
{
if (isDragging)
{
// Finalize the polyline by adding the last point
points.push_back(wxRealPoint(plotX, plotY));
isDragging = false;
// AddNewPolyline();
// plotWindow->Update();
CurrentPolyline->AddData(plotX, plotY, true); // Last point
}
}
// Your code
cancel = true; // Set to false if you want to continue the normal code
}
When I finish dragging, It looks like the last function call CurrentPolyline->AddData(plotX, plotY, true); // Last point
does not refresh the whole plot?
See the screen cast below: I'm not sure why.
https://github.com/user-attachments/assets/f38b1404-2443-4e25-a1a9-a045ca685a6f
Yes repaint for each point but with my (old) computer, I don't see flashing.
My guess is that the plot is quite simple. But in my own project, I'd like to draw some bitmaps on the screen, maybe, there are some other extra contour lines I need to draw on top of the screen, so I guess the performance will be bad if I need to refresh the whole plot when mouse moves(mouse motion event). The only way I found is using a wxDCOverlay like class, this class will hold the background window's image, and let you draw something on top of it. The actual mpFXYVector object will be added after I release the mouse button(mouse motion finished).
void MathPlotDemoFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
static wxOverlay m_overlay;
wxString eventTypeString = GetEventTypeString(event.GetEventType());
wxString label = ((mpWindow *)Sender)->GetLabel();
// wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));
// Get the mouse position relative to the mpWindow
wxPoint mousePosition = event.GetPosition();
static int counter = 0;
// Cast Sender to mpWindow and convert the coordinates
mpWindow *plotWindow = (mpWindow *)Sender;
double plotX, plotY;
int x = mousePosition.x;
int y = mousePosition.y;
plotX = plotWindow->p2x(mousePosition.x);
plotY = plotWindow->p2y(mousePosition.y);
// wxLogMessage(wxString::Format("Mouse Position in Plot Coordinates: X = %f, Y = %f", plotX, plotY));
// Left mouse button down
if (event.LeftDown())
{
// wxLogMessage(wxString::Format("Start dragging, add the initial point: X = %f, Y = %f", plotX, plotY));
// Start dragging, add the initial point
isDragging = true;
points.clear();
points.push_back(wxRealPoint(x, y));
counter = 0;
wxLogMessage(wxString::Format("Start dragging, pixel point: X = %d, Y = %d", x, y));
}
// Mouse dragging with left button held down
else if (event.Dragging() && event.LeftIsDown())
{
if (isDragging)
{
wxClientDC dc(plotWindow);
PrepareDC(dc);
wxDCOverlay overlay(m_overlay, &dc);
// Only draw the last segment
if (points.size() > 0)
{
wxPoint lastPoint(points[points.size() - 1].x, points[points.size() - 1].y);
wxPoint currentPoint(x, y);
dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
dc.DrawLine(lastPoint, currentPoint);
}
// Add the current point to the points vector
points.push_back(wxRealPoint(x, y));
}
}
// Left mouse button released
else if (event.LeftUp())
{
if (isDragging)
{
// Finalize the polyline by adding the last point
points.push_back(wxRealPoint(x, y));
isDragging = false;
m_overlay.Reset();
// AddNewPolyline();
// plotWindow->Update();
mpFXYVector* CurrentPolylineNew = new mpFXYVector("New polyline");
wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
CurrentPolylineNew->SetPen(wxPen(random_color, 2));
CurrentPolylineNew->SetContinuity(true);
plotWindow->AddLayer(CurrentPolylineNew);
for (int i=0; i<points.size(); i++)
{
plotX = plotWindow->p2x(points[i].x);
plotY = plotWindow->p2y(points[i].y);
CurrentPolylineNew->AddData(plotX, plotY, false);
}
// std::vector<double> xData, yData;
// for (const auto& point : points)
// {
// xData.push_back(plotWindow->p2x(point.x));
// yData.push_back(plotWindow->p2y(point.y));
// }
//
// CurrentPolylineNew->SetData(xData, yData);
plotWindow->Update();
}
}
// Your code
cancel = true; // Set to false if you want to continue the normal code
}
The above code works correctly with the wxDCOverlay, and see the screen cast below, it is very fast when in mouse motion event handler, because the whole background image does not need to re-draw in the event handler.
https://github.com/user-attachments/assets/d11a0280-a7bc-4075-940b-124d514bbbd7
Now, the user can add any other graphics, like polylines, triangles, rectangles, I think we can even add some any other customized controls which can be converted to wxBitmap. (for example, JamesBremner/DXF_Viewer: A simple DXF File viewer project can be used to shown a CAD file in the wxWidgets windows, so the wxMathPlot may be used to show CAD files in the future).
So, the wxMathPlot control can be more interactive. That's great!
I found an potential issue:
I try to create a mpFXYVector like below:
mpFXYVector* CurrentPolylineNew = new mpFXYVector("New polyline");
for (int i=0; i<points.size(); i++)
{
plotX = plotWindow->p2x(points[i].x);
plotY = plotWindow->p2y(points[i].y);
CurrentPolylineNew->AddData(plotX, plotY, false);
}
wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
CurrentPolylineNew->SetPen(wxPen(random_color, 2));
CurrentPolylineNew->SetContinuity(true);
plotWindow->AddLayer(CurrentPolylineNew);
But I got crash in the above code, because I see that the CurrentPolylineNew->AddData(plotX, plotY, false);
will access the mpWindow* m_win;
member variables, but this variable is not set before the function call plotWindow->AddLayer(CurrentPolylineNew);
.
So, my guess is that the member variable inside the
class WXDLLIMPEXP_MATHPLOT mpLayer: public wxObject
Should set the nullptr
in its constructor?
Any ideas?
Another remark in your code. Maybe it is better to initialize cancel to false at the beginning. And set cancel to true only when you have captured the event. With that, you keep the normal behaviour for other events (like wheel, ...)
I think my idea is that I need a "toggle button" or "check box button", so that I can switch the user action and the normal mouse build-in zoom/pan behavior.
Ok so :
No need the point buffer array. I clean your code, work fine :
void MyFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
static wxOverlay m_overlay;
wxString eventTypeString = GetEventTypeString(event.GetEventType());
wxString label = ((mpWindow*)Sender)->GetLabel();
// wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));
// Get the mouse position relative to the mpWindow
wxPoint mousePosition = event.GetPosition();
// Cast Sender to mpWindow and convert the coordinates
mpWindow* plotWindow = (mpWindow*)Sender;
static wxPoint currentPoint;
static wxPoint lastPoint;
double plotX, plotY;
int x = mousePosition.x;
int y = mousePosition.y;
plotX = plotWindow->p2x(mousePosition.x);
plotY = plotWindow->p2y(mousePosition.y);
cancel = false;
// Left mouse button down
if (event.LeftDown())
{
// Start dragging, add the initial point
isDragging = true;
CurrentPolyline = new mpFXYVector("New polyline");
wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
CurrentPolyline->SetPen(wxPen(random_color, 2));
CurrentPolyline->SetContinuity(true);
// Add new Polyline but not plot it
plotWindow->AddLayer(CurrentPolyline, false);
// Add point to Polyline but not plot it
CurrentPolyline->AddData(plotX, plotY, false);
lastPoint = wxPoint(x, y);
cancel = true;
}
// Mouse dragging with left button held down
else
if (event.Dragging() && event.LeftIsDown())
{
if (isDragging)
{
wxClientDC dc(plotWindow);
PrepareDC(dc);
wxDCOverlay overlay(m_overlay, &dc);
// Only draw the last segment
currentPoint = wxPoint(x, y);
dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
dc.DrawLine(lastPoint, currentPoint);
lastPoint = currentPoint;
cancel = true;
// Add point to Polyline but not plot it
CurrentPolyline->AddData(plotX, plotY, false);
}
}
// Left mouse button released
else
if (event.LeftUp())
{
if (isDragging)
{
// Finalize the polyline by adding the last point
isDragging = false;
m_overlay.Reset();
CurrentPolyline->AddData(plotX, plotY, false); // Last point
plotWindow->Refresh(); // then refresh the plot
cancel = true;
}
}
}`
For your potential bug, yes you can not call AddData with parameter true or false before have added the new layer. That's logical because we need the window to calculate the bound, may be shall I add a test to prevent this. EDIT : add warning and initialise m_win to null
Ok so :
* corrected copy/paste comments * Super the overlay. In summary, you draw points in the Overlay when the mouse is moving and when the mouse button is released, you create the mpFXYVector layer. * No need the point buffer array. I clean your code, work fine :
void MyFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel) { static wxOverlay m_overlay; wxString eventTypeString = GetEventTypeString(event.GetEventType()); wxString label = ((mpWindow*)Sender)->GetLabel(); // wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label)); // Get the mouse position relative to the mpWindow wxPoint mousePosition = event.GetPosition(); // Cast Sender to mpWindow and convert the coordinates mpWindow* plotWindow = (mpWindow*)Sender; static wxPoint currentPoint; static wxPoint lastPoint; double plotX, plotY; int x = mousePosition.x; int y = mousePosition.y; plotX = plotWindow->p2x(mousePosition.x); plotY = plotWindow->p2y(mousePosition.y); cancel = false; // Left mouse button down if (event.LeftDown()) { // Start dragging, add the initial point isDragging = true; CurrentPolyline = new mpFXYVector("New polyline"); wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX); CurrentPolyline->SetPen(wxPen(random_color, 2)); CurrentPolyline->SetContinuity(true); // Add new Polyline but not plot it plotWindow->AddLayer(CurrentPolyline, false); // Add point to Polyline but not plot it CurrentPolyline->AddData(plotX, plotY, false); lastPoint = wxPoint(x, y); cancel = true; } // Mouse dragging with left button held down else if (event.Dragging() && event.LeftIsDown()) { if (isDragging) { wxClientDC dc(plotWindow); PrepareDC(dc); wxDCOverlay overlay(m_overlay, &dc); // Only draw the last segment currentPoint = wxPoint(x, y); dc.SetPen(wxPen(*wxLIGHT_GREY, 2)); dc.DrawLine(lastPoint, currentPoint); lastPoint = currentPoint; cancel = true; // Add point to Polyline but not plot it CurrentPolyline->AddData(plotX, plotY, false); } } // Left mouse button released else if (event.LeftUp()) { if (isDragging) { // Finalize the polyline by adding the last point isDragging = false; m_overlay.Reset(); CurrentPolyline->AddData(plotX, plotY, false); // Last point plotWindow->Refresh(); // then refresh the plot cancel = true; } } }`
Thanks, this code works fine.
BTW, I see there will be two context menu shown on the plot. Here is the steps to reproduce the bug:
1, right mouse button click, the context menu will be shown 2, middle mouse button click, another context menu will be shown again.
So, there are two context menus shown on the plot, so I guess this is a bug.
Yes and if you click several times on the middle mouse button, you will have several context menu.
Not fatal but not desired.
I can just suppress EVT_MIDDLE_UP(mpWindow::OnShowPopupMenu)
I add a GetSize() function to get the number of points in a series. With this function we can avoid single click and release at the same point in your code :
void MyFrame::OnUserMouseAction(void *Sender, wxMouseEvent &event, bool &cancel)
{
static wxOverlay m_overlay;
wxString eventTypeString = GetEventTypeString(event.GetEventType());
wxString label = ((mpWindow*)Sender)->GetLabel();
// wxLogMessage(wxString::Format("Type = %s, Label = %s", eventTypeString, label));
// Get the mouse position relative to the mpWindow
wxPoint mousePosition = event.GetPosition();
// Cast Sender to mpWindow and convert the coordinates
mpWindow* plotWindow = (mpWindow*)Sender;
static wxPoint currentPoint;
static wxPoint lastPoint;
double plotX, plotY;
int x = mousePosition.x;
int y = mousePosition.y;
plotX = plotWindow->p2x(mousePosition.x);
plotY = plotWindow->p2y(mousePosition.y);
cancel = false;
// Left mouse button down
if (event.LeftDown())
{
// Start dragging, add the initial point
isDragging = true;
CurrentPolyline = new mpFXYVector("New polyline");
wxColour random_color = wxIndexColour(rand() * 20 / RAND_MAX);
CurrentPolyline->SetPen(wxPen(random_color, 2));
CurrentPolyline->SetContinuity(true);
// Add new Polyline but not plot it
plotWindow->AddLayer(CurrentPolyline, false);
// Add point to Polyline but not plot it
CurrentPolyline->AddData(plotX, plotY, false);
lastPoint = wxPoint(x, y);
cancel = true;
}
// Mouse dragging with left button held down
else
if (event.Dragging() && event.LeftIsDown())
{
if (isDragging)
{
wxClientDC dc(plotWindow);
PrepareDC(dc);
wxDCOverlay overlay(m_overlay, &dc);
// Only draw the last segment
currentPoint = wxPoint(x, y);
dc.SetPen(wxPen(*wxLIGHT_GREY, 2));
dc.DrawLine(lastPoint, currentPoint);
lastPoint = currentPoint;
// Add point to Polyline but not plot it
CurrentPolyline->AddData(plotX, plotY, false);
cancel = true;
}
}
// Left mouse button released
else
if (event.LeftUp())
{
if (isDragging)
{
// Finalize the polyline by adding the last point
isDragging = false;
m_overlay.Reset();
// Prevent the simple click with no dragging
if (CurrentPolyline->GetSize() == 1)
{
plotWindow->DelLayer(CurrentPolyline, true, false);
}
else
{
CurrentPolyline->AddData(plotX, plotY, false); // Last point
plotWindow->Refresh(); // then refresh the plot
}
cancel = true;
}
}
}
Another point : I think I can pass several functions from public to protected. For example void DoPlot(wxDC &dc, mpWindow &w)
. Your opinion ?
Another point : I think I can pass several functions from public to protected. For example
void DoPlot(wxDC &dc, mpWindow &w)
. Your opinion ?
I agree, I think those kinds of function should not be called from the client code, they should only be called internally in the mpWindow class.
I add a GetSize() function to get the number of points in a series. With this function we can avoid single click and release at the same point in your code
Thanks, that's great.
Ok, I passed DoPlot from public to protected.
Hi Guys - A few thoughts about this, please bear with me for longish explanation... First, have a look at this demo, noting:
Demo: https://www.nadler.com/backups/2024-08-18_AnalysisProgramProgress.mp4
The normal way to extend functionality in C++ is with virtual member functions.
Thus to add the track highlight circle, I very simply extend mpFXY::DoPlot
:
virtual void DoPlot(wxDC &dc, mpWindow &w) override {
MathPlot::mpFXY::DoPlot(dc,w); // call base class to perform majority of drawing
// If enabled, draw circle to highlight a specific observation point
if(!highlightEnabled) return;
const GPSinfo_T &GPS = logInstance.sensorRecords[highlightIdx].gps;
wxCoord x = w.x2p(GPS.longitude);
wxCoord y = w.y2p(GPS.latitude);
dc.SetPen(wxPen( *wxBLACK, 3));
dc.DrawCircle(x,y,10);
}
To support my need for custom mpInfoCoords , I modified MathPlot (and Lionel has accepted pull request) by simply refactoring the text preparation into a virtual function. So a very tiny modifications to MathPlot. I can simply replace the virtual function like this:
virtual wxString GetInfoCoordsText(double xVal, double yVal, double y2Val, bool isY2Axis = false) {
int idx = logInstance.RangeCheckedIdx((int)xVal);
const SensorDerivedValues &sdv =logInstance.sensorDerivedValues[idx];
wxString result;
result.Printf("idx=%d\nAlt=%f.0\nTE=%f.2", idx, sdv.altitudeM, sdv.TEvario);
return result;
}
MathPlot was designed to use virtual functions throughout. But for some reason, the mouse functions were not virtual. I think the best way to make it possible to override the mouse behavior is simply make these functions virtual, so they can simply be overridden completely, or extended as I did above with DoPlot. Reasons:
I suggest we remove the added callback function API and members such as CheckUserMouseAction
, and simply make all the mouse functions virtual. This is simpler, more consistent with the library design, and makes it easy to override mouse behavior such as OnMouseLeave
(I need this for my application)...
@asmwarrior @GitHubLionel - what do you think? @asmwarrior you would need to make (small) changes to your code...
I suggest we remove the added callback function API and members such as CheckUserMouseAction, and simply make all the mouse functions virtual. This is simpler, more consistent with the library design, and makes it easy to override mouse behavior such as OnMouseLeave (I need this for my application)...
If you make the mouse event handler a "virtual function", can you still use the macro based event binding methods?
In wxWidgets, I think the macro based event binding way is a static method, we can't simply change the behavior easily.
BTW: I have view your posted video, looks nice!
Oh, about the virtual function method, you can first use the macro based event binding to connect a normal (non virtual) function in the mpWindow class, than in the normal function, you call a virtual function. So that the virtual function method should work.
If you make the mouse event handler a "virtual function", can you still use the macro based event binding methods?
I try, seem to work so I can just add virtual to all event functions. So we can keep the two methods.
Hi, currently, when the plot window is shown, the left mouse click event handler is hard coded, see below:
This is connected with the macros, see below:
What I want to do is that in some applications, I need some interactive feature. For example, when in some mode, when left mouse click happens, I need to add one point to a serial, or I need to use the mouse drag to draw a custom poly-line.
So, I'd like ask some suggestion from you that how to do that?
Maybe I need to edit the event handler function, and add an if condition at the begin of the function?
Something like:
Maybe you can have more suggestions? Thanks.