So just working with embedding matplotlib through the embedded example provided on the GitHub, I believe I have a good grasp as to how I can control modify the plot from C++ through the Python C API.
Below is said code that adds a simple matplotlib to the bottom panel of the splitter. As an example you can generate random peak plots via the File menu event “Refresh plot”.
wxWidgetApp.cpp
#include <Python.h>
// For compilers that support precompilation, includes "wx/wx.h".
#include <wx/wxprec.h>
// if precompilation fails or is not supported.
#ifndef WX_PRECOMP
#include <wx/wx.h>
#endif
// not included in the aformentioned precompiled header...
#include <wx/splitter.h>
// Import Python and wxPython headers
#include <wxPython/sip.h>
#include <wxPython/wxpy_api.h>
#include <numeric>
#include <random>
#include <vector>
//----------------------------------------------------------------------
// Class definitions; should try to be their onw header files?
class MyApp : public wxApp
{
public:
virtual bool OnInit();
virtual int OnExit();
bool Init_wxPython();
PyThreadState* m_mainTState;
private:
//PyThreadState* m_mainTState; // orignially was private
};
class MyFrame : public wxFrame
{
public:
MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
wxWindow* DoPythonStuff(wxWindow* parent);
//void OnExit(wxCommandEvent& event);
void OnClose(wxCloseEvent& event);
void OnPyPlot(wxCommandEvent& event);
private:
PyObject* m_panel;
DECLARE_EVENT_TABLE()
};
namespace helper
{
void stop_python_code();
}
//----------------------------------------------------------------------
// MyApp methods
bool MyApp::OnInit()
{
if (!Init_wxPython())
// don't start the app if we can't initialize wxPython.
return false;
MyFrame* frame = new MyFrame(_T("Embedded matplotlib Test"),
wxDefaultPosition, wxSize(700, 600));
frame->Show(true);
return true;
}
bool MyApp::Init_wxPython()
{
// Initialize Python
Py_InitializeEx(0);
//PyEval_InitThreads(); // C4996 'PyEval_InitThreads': deprecated in 3.9
// Load the wxPython core API. Imports the wx._core_ module and sets a
// local pointer to a function table located there. The pointer is used
// internally by the rest of the API functions.
if (wxPyGetAPIPtr() == NULL) {
wxLogError(wxT("***** Error importing the wxPython API! *****"));
PyErr_Print();
Py_Finalize();
return false;
}
// Save the current Python thread state and release the
// Global Interpreter Lock.
m_mainTState = wxPyBeginAllowThreads();
return true;
}
int MyApp::OnExit()
{
// Restore the thread state and tell Python to cleanup after itself.
// wxPython will do its own cleanup as part of that process. This is done
// in OnExit instead of ~MyApp because OnExit is only called if OnInit is
// successful.
wxPyEndAllowThreads(m_mainTState);
Py_Finalize();
//return 0;
return wxApp::OnExit(); // should be the same...
}
IMPLEMENT_APP(MyApp)
//----------------------------------------------------------------------
// MyFrame methods
enum
{
ID_EXIT = 1001,
ID_PYPLOT
};
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
//EVT_MENU(ID_EXIT, MyFrame::OnExit)
EVT_MENU(ID_PYPLOT, MyFrame::OnPyPlot)
END_EVENT_TABLE()
MyFrame::MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
: wxFrame(NULL, -1, title, pos, size,
wxDEFAULT_FRAME_STYLE | wxNO_FULL_REPAINT_ON_RESIZE)
{
wxMenuBar* mbar = new wxMenuBar;
wxMenu* menu = new wxMenu;
menu->Append(ID_PYPLOT, _T("Refresh Plot"));
//menu->AppendSeparator();
//menu->Append(ID_EXIT, _T("&Close Frame\tAlt-X"));
mbar->Append(menu, _T("&File"));
SetMenuBar(mbar);
CreateStatusBar();
// Make some child windows from C++
wxSplitterWindow* sp = new wxSplitterWindow(this, -1);
wxPanel* p1 = new wxPanel(sp, -1, wxDefaultPosition, wxDefaultSize, wxSUNKEN_BORDER);
new wxStaticText(p1, -1,
_T("The frame, menu, splitter, this panel and this text were created in C++..."),
wxPoint(10, 10));
// And get a panel from Python
wxWindow* p2 = DoPythonStuff(sp);
if (p2) sp->SplitHorizontally(p1, p2, GetClientSize().y / 4);
Bind(wxEVT_CLOSE_WINDOW, &MyFrame::OnClose, this);
}
//void MyFrame::OnExit(wxCommandEvent& event)
//{
// //Py_DECREF(m_panel);
// Close();
//}
void MyFrame::OnClose(wxCloseEvent& event)
{
//Py_DECREF(m_panel); // not the issue it seems
//helper::stop_python_code(); // not sure logically if this really achieve anything
//wxPyEndAllowThreads(wxGetApp().m_mainTState); // manulay moved from MyApp::OnExit()
//Py_Finalize();
//wxGetApp().OnExit(); // bad practice
//Destroy(); // same as Close?
//Close(); // same as Destroy?
event.Skip();
}
//----------------------------------------------------------------------
// wxPython
namespace helper
{
void stop_python_code()
{
wxPyBlock_t blocked = wxPyBeginBlockThreads();
PyErr_SetString(PyExc_Exception, "Stopping Python code");
wxPyEndBlockThreads(blocked);
}
}
const char* python_code = "\
import sys\n\
sys.path.append('.')\n\
import plot\n\
\n\
def makeWindow(parent):\n\
win = plot.MyPanel(parent)\n\
return win\n\
";
wxWindow* MyFrame::DoPythonStuff(wxWindow* parent)
{
// More complex embedded situations will require passing C++ objects to
// Python and/or returning objects from Python to be used in C++. This
// sample shows one way to do it. NOTE: The above code could just have
// easily come from a file, or the whole thing could be in the Python
// module that is imported and manipulated directly in this C++ code. See
// the Python API for more details.
wxWindow* window = NULL;
PyObject* result;
// As always, first grab the GIL
wxPyBlock_t blocked = wxPyBeginBlockThreads();
// Now make a dictionary to serve as the global namespace when the code is
// executed. Put a reference to the builtins module in it. (Yes, the
// names are supposed to be different, I don't know why...)
PyObject* globals = PyDict_New();
#if PY_MAJOR_VERSION > 2
PyObject* builtins = PyImport_ImportModule("builtins");
#else
PyObject* builtins = PyImport_ImportModule("__builtin__");
#endif
PyDict_SetItemString(globals, "__builtins__", builtins);
Py_DECREF(builtins);
// Execute the code to make the makeWindow function
result = PyRun_String(python_code, Py_file_input, globals, globals);
// Was there an exception?
if (!result) {
PyErr_Print();
wxPyEndBlockThreads(blocked);
return NULL;
}
Py_DECREF(result);
// Now there should be an object named 'makeWindow' in the dictionary that
// we can grab a pointer to:
PyObject* func = PyDict_GetItemString(globals, "makeWindow");
wxASSERT(PyCallable_Check(func));
// Now build an argument tuple and call the Python function. Notice the
// use of another wxPython API to take a wxWindows object and build a
// wxPython object that wraps it.
PyObject* arg = wxPyConstructObject((void*)parent, "wxWindow", false);
wxASSERT(arg != NULL);
//PyObject* tuple = PyTuple_New(1);
//PyTuple_SET_ITEM(tuple, 0, arg);
PyObject* tuple = PyTuple_Pack(1, arg);
result = PyObject_CallObject(func, tuple);
// Was there an exception?
if (!result)
PyErr_Print();
else {
// Otherwise, get the returned window out of Python-land and
// into C++-ville...
bool success = wxPyConvertWrappedPtr(result, (void**)&window, _T("wxWindow"));
wxASSERT_MSG(success, _T("Returned object was not a wxWindow!"));
// Save a reference to the returned MyPanel instance
m_panel = result;
//Py_INCREF(m_panel); // is this needed?
Py_DECREF(result);
}
// Release the python objects we still have
Py_DECREF(globals);
Py_DECREF(tuple);
// Finally, after all Python stuff is done, release the GIL
wxPyEndBlockThreads(blocked);
return window;
}
void MyFrame::OnPyPlot(wxCommandEvent& event)
{
// As always, first grab the GIL
wxPyBlock_t blocked = wxPyBeginBlockThreads();
std::vector<double> ydata(2048, 0.00);
// Create a random number generator
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_real_distribution<> dis(0.2, 1.0);
// Randomly select 6 indices to mutate
int size = ydata.size();
std::vector<int> indices(ydata.size());
std::iota(indices.begin(), indices.end(), 0);
std::shuffle(indices.begin(), indices.end(), gen);
indices.resize(6);
// Mutate the selected elements
for (int i : indices) {
ydata[i] = dis(gen);
}
// Convert the ydata vector to a Python list
PyObject* ydata_list = PyList_New(ydata.size());
for (int i = 0; i < ydata.size(); i++) {
PyList_SetItem(ydata_list, i, PyFloat_FromDouble(ydata[i]));
}
// Call the add_data method of the MyPanel instance
PyObject* result = PyObject_CallMethod(m_panel, "add_data", "O", ydata_list);
// Check for errors
if (!result) {
PyErr_Print();
}
else {
Py_DECREF(result);
}
// Clean up
Py_DECREF(ydata_list);
// Finally, after all Python stuff is done, release the GIL
wxPyEndBlockThreads(blocked);
}
//----------------------------------------------------------------------
plot.py
import wx
import matplotlib.pyplot as plt
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wx import NavigationToolbar2Wx
import numpy as np
from scipy.signal import find_peaks
class MyPanel(wx.Panel):
""" wxPython code loading matplotlib """
def __init__(self, parent):
super(MyPanel, self).__init__(parent, -1, style = wx.BORDER_NONE | wx.MAXIMIZE)
# Create a figure and a canvas to draw on
self.figure = plt.figure()
self.canvas = FigureCanvas(self, -1, self.figure)
# Create a navigation toolbar
self.toolbar = NavigationToolbar2Wx(self.canvas)
self.toolbar.Realize()
# Draw a simple plot
self.axes = self.figure.add_subplot(111)
self.axes.plot([1, 2, 3], [2, 1, 4])
# Create a sizer to manage the layout
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND)
sizer.Add(self.canvas, 1, wx.EXPAND)
self.SetSizer(sizer)
def add_data(self, ydata):
"""Overwrite the plot data with new data"""
self.axes.clear()
xdata = np.arange(1, 2049)
self.axes.plot(xdata, ydata, color='green')
# Find the peaks in the ydata
peaks, _ = find_peaks(ydata, height=0.1)
# Add labels for the peaks
for peak in peaks:
# Display the x value at each peak
label = str(xdata[peak])
self.axes.annotate(label, xy=(xdata[peak], ydata[peak]), xytext=(0, 5), textcoords='offset points', ha='center', va='bottom', rotation=90, color='green')
# Adjust the y-axis limits to prevent the labels from being cut off
ymin, ymax = self.axes.get_ylim()
ymax += 0.1 * (ymax - ymin) # Increase the upper y-axis limit by 10%
self.axes.set_ylim(ymin, ymax)
# Set the x-ticks to be at multiples of 256
self.axes.set_xticks(np.arange(0, 2049, 256))
# Fix the x-axis limits to be exactly 1 and 2048
self.axes.set_xlim(1, 2048)
# Set the view limits to new "Home" position
plt.xlim(1, 2048)
plt.ylim(ymin, ymax)
# Update the view history to set the new "Home" position
self.toolbar.push_current()
self.canvas.draw()
Now that I have a better understanding of passing objects between the languages, I want to address an issue I noticed when I first started where when I close the wxFrame, the VS debugger does not automatically terminate like it did for the original embedded python example.
Using breakpoints,I found out that when I close the frame, it never invokes MyApp::OnExit(). I have since tried a MyFrame::OnClose() binded to the wxCloseEvent to try and see if I can I resolve this issue to no luck, they are currently commented out.
Besides switching the example from a PyScript to a matplotlib, the sample code should be very similar to the original. I do have to save the result = PyObject_CallObject(func, tuple); to a private class variable PyObject* m_panel [MyFrame::DoPythonStuff()] such that my code can later modify the plot via PyObject* result = PyObject_CallMethod(m_panel, “add_data”, “O”, ydata_list); [MyFrame::OnPyPlot]. I originally thought that a reference to result or m_panel was the cause, but it seems that they have been properly decrement to 0 by the time the frame is closed… even if I simplify the code further to no events and just an empty plot, closing the last and only frame does not invoke MyApp::OnExit().
I tried to step through or step into breakpoints but I will run into the VS No symbols Loaded error that wxmsw32u_core_vc140_x64.pdb is not loaded as it does not exit of course. And if I were to provide the wxWidgets pdb file, then it does not load due to mismatch of course.
I was wondering if anyone else knows what I am likely missing?