You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用ctypes在Python中调用DLL创建wxFrame的多窗口问题

wxWidgets DLL与Python ctypes交互:多窗口显示问题

问题背景

我有一个名为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 19:44:55