Possible to embed a wxPython widget within a wxWidgets application?

I have a wxWidgets C++ application that includes plotting data from an instrument dynamically.

Currently to plot the data, I am using wxMathPlot but I am not quite satisfied with the library in terms of visual artifacts, plot control, and data analysis. And looking into the other C++ wx alternatives I think wxMathPlot is still the best amongst them…

Thus I was wondering whether an alternative approach could be to embed a matplotlib plot into a C++ app. In terms of embedding I am using pybind11 to call the python interpreter and call my python module to run with success.

The problem now was that I could not load the matplotlib into the GUI as wxWidgets does not have a widget to deal with it. I am aware workarounds are possible to load these plot as an image, but again I need to plot data dynamically and with user interactivity…

So now my approach was to create a wxPython wx.Panel class with the plot loaded within it in hopes to embed it to my wxWidget app. My current approach for the class is to take a wxFrame object as a parent, that is passed from the C++ app via pybind11. Of course, now the issue is that nothing can cast a wxFrame object to wx.Frame python object for the python module to work with.

Looking in the forums I see older posts that possibly hint that perhaps this was once possible:
how can I use wxPython from Python embedded in a wxWidgets C++ application?
But that is from 2011 and the links to said embedded samples are dead.

Anyways from looking into the Phoenix Github, I understand that the implementation since then must have evolved from the ‘Classic’ anyhow. I understand that in order for wxPython Project Phoenix to wrap wxWidgets, it utilizes python bindings generated with SIP.

That is where I am at now, attempting to create a .sip and .toml to wrap a wxWidget object to create python bindings, so that I can pass it to a python module so as to hopefully display it in my C++ application.

Is this possible? Or should I abandon this approach…

Below are my example files to investigate if this is doable.
I am stuck at building the specification file as when I try to make the class, SIP cannot recognize wxFrame* still in which I have read that I need to import %_core.sip in which I am attempting to follow:
Using SIP on extended wx
But I am still getting dependency errors…

pyproject.toml:

[build-system]
requires = ["sip >=6, <7"]
build-backend = "sipbuild.api"

[tool.sip.metadata]
name = "myframe"

myframe.sip:

%Module (name=myframe, language="C++")

%Import _core.sip

class MyFrame
{
%TypeHeaderCode
#include "myframe.h"
%End

public:
    MyFrame(wxFrame* frame);
    void SetTitle(const wxString& title);

    wxFrame* frame;
    
};

myframe.h

#ifndef MYFRAME_H
#define MYFRAME_H

#include <wx/wx.h>

class MyFrame
{

public:
	MyFrame(wxFrame* frame);

private:
	wxFrame* frame;

};

MyFrame::MyFrame(wxFrame* frame) : frame(frame) {}

#endif

project.cpp:

#include <wx/wx.h>
#include <pybind11/embed.h>
#include "myframe.h"

namespace py = pybind11;

class MyApp : public wxApp
{
public:
    virtual bool OnInit();
};

IMPLEMENT_APP(MyApp)

bool MyApp::OnInit()
{
    // Initialize the Python interpreter
    py::scoped_interpreter guard{};

    // Import the necessary Python modules
    py::module wx = py::module::import("wx");
    py::module mypanel = py::module::import("mypanel");
    py::module myframe = py::module::import("myframe");

    // Create a new frame
    wxFrame* frame = new wxFrame(NULL, wxID_ANY, "Hello World");

    // Create a MyFrame object
    MyFrame my_frame(frame);

    // Wrap the C++ my_frame object using the Python bindings; or do I do this within mypanel.py?
    py::object py_my_frame = myframe.attr("MyFrame")(my_frame);

    // Call the MyPanel function in Python with the wrapped my_frame object
    py::object result = mypanel.attr("MyPanel")(py_my_frame);

    // Show the frame, with the wx.Panel hopefully
    frame->Show();

    return true;
}

mypanel.py:

# mypanel.py
import wx
import myframe

def MyPanel(parent):
    # Create a simple button in a panel for now
    button = wx.Button(parent, label="Click me!")
    sizer = wx.BoxSizer(wx.VERTICAL)
    sizer.Add(button, 0, wx.ALL, 5)
    parent.SetSizer(sizer)

It is possible, here is the sample I got inspiration from

I don’t know how much help I can be, but I’m working on a project embedding python into AutoCAD(Windows)

AutoCAD is MFC, but I create a wxApp and create a frame from wxFrame,
https://github.com/CEXT-Dan/PyRx/blob/5ef07eba665add9fdb76e814d712a3ee4be36923/PyRx/PyRxApp.cpp
I actually have a test using matplotlib
https://github.com/CEXT-Dan/PyRx/blob/5ef07eba665add9fdb76e814d712a3ee4be36923/PySamples/wxWidgets/testWxPyMatPlotLib.py

proof of concept

I seemed to have missed the GitHub embedded sample.

I will explore this embed approach in more detail and will reconvene this thread when I ultimately miss something else :stuck_out_tongue:

Thanks for sharing wxdan!

Just trying to get the sample to run in VS 2022.

I keep returning false for bool wxApp::Init_wxPython() because I get NULL for wxPyGetAPIPtr()…

bool MyApp::Init_wxPython()
{
    // Initialize Python
    Py_Initialize();
    //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;
}

I am the same as the sample except I commented out PyEval_InitThreads(); as it was deprecated since Python 3.9

wxPython is 4.2.1
wxWidgets 3.2.2.1
Python 3.11.4

As far as I am aware, theses should all be compatible, but I am unsure if that extends to wxPyGetAPIPtr()…

Thanks for the info on PyEval_InitThreads, I have that in my code, apparently it’s a noop now.
I used Py_InitializeEx, I don’t remember why, geez I’m getting old… it could be that I ran into a similar issue.

Something I do remember, I had to add the path to python and wxPython to my $PATH env,
You must compile with WXUSINGDLL so wxPython’s wx DLLs get loaded

Since you’re making a .exe, you can probably add these your DLL paths

edit:
Oh yeah, when compiling wxWidgets, by default it added a ‘custom’ prefix to all the DLLs. There’s probably a setting to change this, but I just changed this in the solution

Hey wxdan,

So I tried Py_InitializeEx(0), which I read has to do with skipping the initialization of signal handlers when initializing the Python Interpreter… but that did not work.

Then I tried using WXUSINGDLL. I learned that I have only ever compiled wxWidgets with static libraries so I only had built .lib files. I have since built the dynamic libraries .dll files for both release and debug and added the dynamic library folder to the solution’s library/dll path instead.

I noticed the _custom appended to the .dll names that you mentioned, but it seems at least when built via VS, those names are expected as if I change the name, it says that I cannot load ‘example_custom.dll’ as I renamed it.

Now I have successfully built with the DLLs, but I still seem to get wxPyGetAPIPtr() == NULL being true.

I presumed that my system PATH for both python and wxPython are correct as I can run isolated wxPython code w/o issue.

Currently for python I have:
C:\Python311
C:\Python311\Scripts
C:\Python311\lib\

Then I realized that I did not have a path for wxPython; I have since added the following path:
C:\Python311\Lib\site-packages\wx (I pip installed it)
as for me it contains the .dll files… but I still get NULL pointer.

Was there any other configuration step you can recall that I am likely missing?

Try using a dependency tool (dependency walker) to see what wx DLLs you’re referencing
I had to manually change the _custom in the wxWigets solution before building. Just changing the DLL names won’t work (I tried that too lol)

Never knew of such a legacy tool, but I do not have Windows 8 or older…
So using dependencies v1.11 instead from Dependencies GitHub

And fixing wxWidget dynamic library issues that I thought I addressed yesterday… I finally have the following DLLs referenced:

and I still get the wxPyGetAPIPtr() == NULL

Is there anything amiss here? I do still have the _custom prefix as the files within my .dll directory still have them after I rebuilt them again.

Evidently I am noticing the lack of any reference to wxPython…

That’s the tool I use as well
Here’s my references, the missing ones are AutoCAD’s that get loaded at runtime

this is the wxWidgets solution,
I had to manually change the DLL names to match what was in wxPython’s folder

Thanks for the recent post! I finally put together what you have meant and have also manually changed all 16 .dll target names to match the ones in wxPython for Release Mode:

Now the sample code runs! Time to finally play with matplotlib and see how that all works lol.

Thanks for all the help so far wxdan!

=============================

For those in the future, here are my current VS solution settings and relevant PATH that I have added working from an empty C++ console project, in order for the embedded wxPython example code to run:

VS 2022 solution Properties:
Release x64

Configuration Properties>VC++ Directories>Include Directories: C:\Python311\include;
Configuration Properties>VC++ Directories>Library Directories: C:\Python311\libs;
so obviously change it to your python’s (that has wxPython pip installed) path

Configuration Properties>C/C++>General: $(WXWIN)\include\msvc;$(WXWIN)\include;C:\Python311\Lib\site-packages\wx\include;
where $(WXWIN) is a macro for the path of wxWidgets
again my path for wxPython that is pip installed

Configuration Properties>C/C++>Preprocessor: WXUSINGDLL;

Configuration Properties>Linker>General: $(WXWIN)\lib\vc_x64_dll;
the default folder for building Release(DLL) x64 via VS
I built with wx_vc17.sln as I am on VS 2022
You need to change all the target names like how wxdan describes above to match wxPython DLLs (removes “_custom” prefix)

Configuration Properties>Linker>System: Windows (/SUBSYSTEM:WINDOWS)
switched from console.

PATH:
python paths
C:\Python311\Scripts
C:\Python311
C:\Python311\libs
and wxPython DLL path for runtime
C:\Python311\Lib\site-packages\wx

Out of curiosity, I tried to see if this works for DEBUG mode but I still get wxPyGetAPIPtr() == NULL as expected.
I presume this easily translates to x86 assuming you have said architecture for python, wxWidgets, and wxPython… though I have not tried so myself.

Version:
wxPython is 4.2.1
wxWidgets 3.2.2.1
Python 3.11.4

the final dependencies:

1 Like

I realize this is slightly off-topic, but if you are already using wxPython and matplotlib to make interactive 2D plots, you might consider using wxmplot (https://github.com/newville/wxmplot/, I am the lead author). This is designed to give interactive and configurable plots to end-users. If you are already using matplotlib and embedding within C++ wx code that loads Python, this is a small, pure-python module and does not add much to the requirement of needing a wxPython event loop.

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?

In my plug in for AutoCAD,
::OnInit() gets or creates an instance of the main frame, then calls wxTheApp->SetTopWindow(pFrame);
I’m not sure this is a requirement, I think the goal is wxWidgets and wxPython share wxTheApp

All of the GUIs I create in python are child wx.Dialogs, setting the parent to null automatically sets the parent to wxTheApp->GetMainTopWindow().

I haven’t tried to add a panel to the main frame