使用ctypes在Python中调用DLL创建wxFrame的多窗口问题
问题背景
我有一个名为scisuit_plot_d.dll的DLL文件,其中包含绘图函数c_plot,该DLL依赖其他若干DLL。c_plot的签名如下:
extern "C" DLLPLOT PyObject * c_plot(PyObject * y, PyObject * x);
简化实现:
auto frmPlot = new wxFrame(nullptr, wxID_ANY, "test frame"); frmPlot->Show(true);
Python调用代码(使用ctypes):
import ctypes as ct path = parent_path(__file__) + "scisuit_plotter_d" plt = ct.PyDLL(path) plt.c_plot.argtypes = [ct.py_object, ct.py_object] plt.c_plot.restype=ct.py_object hwnd = plt.c_plot(y, x) print(hwnd)
DLL主代码:
HANDLE ThreadId; class wxDLLApp : public wxApp { protected: bool OnInit() { return true; } }; IMPLEMENT_APP_NO_MAIN(wxDLLApp) DWORD WINAPI ThreadProc(LPVOID lpParameter) { wxApp::SetInstance(new wxDLLApp()); wxEntry(GetModuleHandle(NULL), NULL, NULL, SW_SHOW); return true; } BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: ThreadId = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: wxEntryCleanup(); break; } return TRUE; }
遇到的问题
- 在VS Code中运行Python程序时,出现
wxSocketBase::IsInitialized错误。 - 在
auto frmPlot前添加wxInitialize()后错误消失,但窗口快速显示后消失,多次运行后出现wxModule::DoCleanupModules错误。 - 在函数返回前添加
wxUninitialize()似乎能清除错误,但多次运行后仍会出现上述窗口消失的问题。
需求:从c_plot函数中启动多个wxFrame,使其保持显示直到用户关闭,使用Win11系统下的wxWidgets 3.2.2。
编辑1:部分生效的尝试
在c_plot函数中调用以下代码可部分生效:
wxApp* app = new wxApp(); auto Init = new wxInitializer(); auto frmPlot = std::make_unique<CFrmSinglePlot>(nullptr); frmPlot->Bind(wxEVT_CLOSE_WINDOW, [&](wxCloseEvent& event) { delete Init; event.Skip(); }); frmPlot->Show(true); if(!app->IsMainLoopRunning()) app->MainLoop();
但存在问题:如果要显示两个绘图窗口或执行后续Python代码,例如:
plt.c_plot(y1, x1) print("hello")
关闭窗口后,第二行代码print("hello")在VS Code中从未执行。
编辑2:线程调用的尝试
若在auto frmPlot前使用Py_BEGIN_ALLOW_THREADS,在app->MainLoop()后使用Py_END_ALLOW_THREADS,并通过Python线程调用:
class PlotThread (threading.Thread): def __init__(self, threadID): threading.Thread.__init__(self) self.threadID = threadID def run(self): plt.c_plot(y, x) thread1=PlotThread(1) thread1.start() print("Hello") # now it is executed
此时print("Hello")可正常执行,但启动第二个线程时:
thread2=PlotThread(2) thread2.start()
wxWidgets会抛出wxThread::IsMain错误。
解决方案
wxWidgets的核心限制是每个进程只能有一个wxApp实例,且必须在主线程中运行主事件循环,同时Python的GIL和线程模型需要和wx的事件循环做好隔离。以下是可行的实现方案:
1. 重构DLL的wxApp初始化逻辑
删除DllMain中创建线程的代码,改为在第一次调用c_plot时初始化wxApp,且确保只初始化一次:
#include <wx/wx.h> #include <Python.h> #include <atomic> std::atomic<bool> s_wx_app_initialized(false); wxApp* s_wx_app = nullptr; // 确保wxApp只初始化一次 void EnsureWxAppInitialized() { if (!s_wx_app_initialized.load()) { wxInitializer* init = new wxInitializer(); if (!init->IsOk()) { delete init; throw std::runtime_error("Failed to initialize wxWidgets"); } s_wx_app = wxApp::GetInstance(); s_wx_app_initialized.store(true); // 注册清理函数,进程退出时清理wx资源 atexit([]() { delete wxInitializer::Get(); wxEntryCleanup(); }); } }
2. 修改c_plot函数,使用非阻塞的事件循环方式
不要在c_plot中直接调用MainLoop(),而是让wx的事件循环在后台线程运行,但注意必须保证只有一个后台线程运行主循环:
#include <thread> #include <mutex> std::mutex s_loop_mutex; std::thread s_wx_loop_thread; void RunWxMainLoop() { wxTheApp->MainLoop(); } extern "C" DLLPLOT PyObject * c_plot(PyObject * y, PyObject * x) { Py_BEGIN_ALLOW_THREADS // 释放GIL,避免阻塞Python主线程 EnsureWxAppInitialized(); // 检查主循环是否已启动,未启动则启动后台线程 { std::lock_guard<std::mutex> lock(s_loop_mutex); if (!s_wx_loop_thread.joinable()) { s_wx_loop_thread = std::thread(RunWxMainLoop); // 设置线程分离,避免Python退出时需要join s_wx_loop_thread.detach(); } } // 创建并显示窗口,绑定关闭事件(仅销毁窗口,不退出主循环) wxFrame* frmPlot = new wxFrame(nullptr, wxID_ANY, "Plot Window"); frmPlot->Bind(wxEVT_CLOSE_WINDOW, [](wxCloseEvent& event) { event.GetEventObject()->Destroy(); // 销毁当前窗口 }); frmPlot->Show(true); Py_END_ALLOW_THREADS // 重新获取GIL // 返回窗口句柄(可选,可转换为Python整数) return PyLong_FromVoidPtr(frmPlot); }
3. Python端调用无需额外线程
修改后的DLL无需Python端创建线程,直接调用即可,且后续代码不会被阻塞:
import ctypes as ct from pathlib import Path path = str(Path(__file__).parent / "scisuit_plotter_d.dll") plt = ct.PyDLL(path) plt.c_plot.argtypes = [ct.py_object, ct.py_object] plt.c_plot.restype = ct.py_object # 直接调用,无需线程 hwnd1 = plt.c_plot(y1, x1) print("Hello after first plot") # 立即执行 hwnd2 = plt.c_plot(y2, x2) # 第二个窗口正常显示
关键注意事项
- 单wxApp实例:整个进程只能初始化一次wxApp,避免重复初始化导致的资源冲突。
- GIL释放:在DLL中调用wx相关代码时必须用
Py_BEGIN_ALLOW_THREADS释放GIL,防止阻塞Python的主线程和其他线程。 - 事件循环分离:将wx的主事件循环放在单独的后台线程,且只启动一次,所有窗口都共享这个事件循环,避免多线程创建wxApp的错误。
- 窗口关闭逻辑:关闭窗口时仅销毁窗口对象,不要退出主循环,这样其他窗口仍能正常运行,直到所有窗口关闭后,进程退出时自动清理wx资源。
内容的提问来源于stack exchange,提问作者macroland

