DLL中封装Qt窗口给MFC或Qt应用程序调用,并将DLL中的窗口嵌入主程序中,实现与主程序交互和自身事件循环的解决方案

近期接到的任务场景是需要在windows动态链接库(dll)中封装Qt的QWebEngine控件,调用该dll的主程序可能是win32或MFC程序,也可能是Qt程序本身。要求是在dll内部封装的QWebEngine显示在调用该dll的MFC或Qt程序界面中,类似于将dll中的Qt窗口嵌入到主程序的窗口中,并接收用户操作和响应。

查阅了网上的各种资料(网上关于这种应用场景的资料实在少的可怜),终于找到一个关于该方面的开源代码qt-solutions-master,并找到相关帖子写出的使用方法,地址为:

https://blog.csdn.net/libin88211/article/details/38183791

然而,当我按照网上的方法使用qtwinmigrate中的qtdll示例,将Qt的界面封装进DLL中,再用MFC的应用程序调用,的确可以在MFC界面上嵌入了DLL中封装的Qt界面,但是拖动MFC界面时,DLL中的Qt界面并没有跟随MFC而动!MFC和DLL中的Qt界面是两个不同的界面。而且也无法做到MFC主程序中的数据和DLL中的Qt界面数据进行交互。

在尝试多次之后依然获取不到预期的结果,把使用qtwinmigrate这个方案彻底否决了。通过不停试验和写了无数个demo程序研究过后,终于找到了一个符合预期的解决方案。

目录

1、实现效果

2、MFC作为主程序调用dll

3、Qt作为主程序调用dll

4、代码实现

 5、dll接口和注意事项

6、界面嵌入


 

系统:win7 64位

环境:Qt 5.8.0 + VisualStudio 2015

Qt版本: qt-opensource-windows-x86-msvc2015_64-5.8.0.exe

MFC程序:V140版本64位

1、实现效果

如下图所示是在dll封装的一个Qt界面,这是一个QFrame,增加了一个QPushButton,下面增加了一个滑块QSlider,QSlinder下面是一个QLineEdit用来显示滑块数值。这三个控件仅仅用来测试看效果,QFrame上可以任意增加Qt控件和布局。

当使用MFC对话框调用该dll并嵌入到对话框中时:

点击测试按钮并移动滑块:

可以看到能够正确作出Qt事件响应。


当使用Qt调用该dll并嵌入到Qt主窗口时:

在具体的实现中,可以看到MFC主程序调用该dll,Qt主程序调用该dll是作出区分的,下面会说明原因。

 

2、MFC作为主程序调用dll

我们知道,一般性的Qt应用程序,是有自己的事件循环,在main()函数中,一般情况下会这样写代码:

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QWidget w;
    w.show();
    return a.exec();
}

当调用a.exec()后,主程序就会阻塞在这里,并且开启Qt自身的消息循环和处理。那么如果想在dll中嵌入Qt界面并实现Qt正常的工作,就必然少不了在dll中实现Qt事件循环,包括QApplication::exec()的调用。

所以在MFC调用dll之前,dll不仅仅是需要创建内部的Qt界面或者控件,而且也需要开启Qt的事件循环,最简单的方法就是开启单独的一个线程,用来作为Qt的事件循环。具体的实现就是初始化dll的接口中开启一个线程,在该线程中实现如下代码: 

int argc = 1;
TCHAR targv[MAX_PATH] = { 0 };
 GetModuleFileName(NULL, targv, MAX_PATH);
USES_CONVERSION;
std::string strPath = T2A(targv);
char argv[MAX_PATH];
memcpy(argv, strPath.c_str(), strPath.length());
char* pargv = argv;
QApplication* a= new QApplication(argc, &pargv);
// 这里创建dll内部的Qt界面例如QWidget
QWidget w;
w.show();
a.exec();

当调用a.exec()之后,该线程就被阻塞在这里,该线程就是实现QWidget的事件循环的线程动力。当需要结束这个事件循环,或者关闭这个dll中界面时,我们需要调用a->quit();来结束QApplication。

以上是MFC作为主程序来调用dll的实现。

 

3、Qt作为主程序调用dll

那么为什么MFC作为主程序调用和Qt作为主程序调用要区分开呢?原因是当Qt作为主程序的时候,Qt本身自带了自己的Qt事件循环,这个时候我们再像MFC调用dll那样弄个单独的线程创建一个QApplication就会失败,提示进程空间中已经存在QApplication了,并且一个主线程Qt事件循环,一个我们自己的线程Qt事件循环,两者就会有冲突。解决这个方法如下:

在我的dll内部实现中,我继承一个QObject的类,在初始化中通过调用:

QCoreApplication* app = QCoreApplication::instance();

将QApplication或者称为QCoreApplication实例给取出来,然后调用:

this->moveToThread(app->thread());

将当前类(当前类继承自QObject)附着到Qt的主线程中,这个Qt的主线程就是Qt主程序,即调用该dll的Qt程序,这个时候我们就可以使用Qt的事件循环了,事件循环在一个线程中,并且在调用该dll的Qt主线程中。

我用QCoreApplication::postEvent()来发送创建自定义的消息,并通过继承实现Qt的

virtual void customEvent(QEvent *event);

接口来处理自定义的消息,在这个函数中实现内部Qt界面的创建、初始化和销毁。

 

4、代码实现

​​​我首先在dll中写出一个接口类:

// dialog接口类,根据主程序是Qt还是win32程序实例化不同的实现类
class libDialogBase
{
public:
    libDialogBase() : qDialogPtr_(nullptr), hDialogHandle_(0) {}
    virtual ~libDialogBase() {}

public:
    virtual bool Initialize(const libGlobalParam* globalParam) = 0; // 初始化
    virtual void UnInitialize() = 0;                                // 结束
    virtual bool GetHandle(HWINDOW& handle) = 0;                    // 获取dll内部Qt窗口句柄
    virtual void ShowDialog() = 0;                                  // 显示dll内部的Qt窗口
    virtual void HideDialog() = 0;                                  // 隐藏dll内部的Qt窗口
    virtual void Resize(int width, int height) = 0;                 // 改变dll内部的Qt窗口大小

protected:
    std::shared_ptr<DialogQt>               qDialogPtr_;
    HWINDOW                                 hDialogHandle_;
};

其中DialogQt是真正在dll内部封装的Qt界面,继承QFrame,在DialogQt中布局了QPushbutton、QSlider和QLineEdit这些自己需要的控件,DialogQt声明如下:

#ifndef __DIALOG_QT_H__
#define __DIALOG_QT_H__

#include <QFrame>
#include <QPushButton>
#include <QSlider>
#include <QLineEdit>

// 在本dll中封装的Qt界面,真正的Qt窗口。
class DialogQt : public QFrame
{
    Q_OBJECT
public:
    DialogQt(QWidget* parent = nullptr);
    ~DialogQt();

private:
    void init();
    void loadStyleSheet();
    void connectSlots();

public:
    void showDialog();
    void hideDialog();
    void resizeDialog(int width, int height);

signals:
    void sglShowDialog();
    void sglHideDialog();
    void sglResizeDialog(int width, int height);

    private slots:
    void slotShowDialog();
    void slotHideDialog();
    void slotResizeDialog(int width, int height);
    void slotBtnClick();
    void slotSliderValue(int value);

private:
    int                 width_;
    int                 height_;
    QPushButton*        btnTest_;
    QSlider*            sliderTest_;
    QLineEdit*          editTest_;
};

#endif // !__DIALOG_QT_H__

其次我继承libDialogBase并实现了两个类:libDialogWin和libDialogQt。

libDialogWin是当MFC或Win32这样的主程序调用dll时实例化,来开启内部Qt的事件循环。而libDialogQt是当Qt主程序调用dll的时候实例化的。所以在dll接口传入参数时,还需要调用者传入当前主程序是否为Qt应用程序,这个区分需要调用者保证。

libDialogWin的声明和实现

libDialogWin.h:

#ifndef __LIB_DIALOG_WIN_H__
#define __LIB_DIALOG_WIN_H__

#include "libDialogBase.h"
#include "libTool.h"
#include <atomic>
#include <thread>
#include <QApplication>

// libDialogBase的实例类,当调用该库的程序为win32程序时实例化该类
class libDialogWin : public libDialogBase, public std::enable_shared_from_this<libDialogWin>
{
public:
    libDialogWin();
    ~libDialogWin();

protected:
    virtual bool Initialize(const libGlobalParam* globalParam);
    virtual void UnInitialize();
    virtual bool GetHandle(HWINDOW& handle);
    virtual void ShowDialog();
    virtual void HideDialog();
    virtual void Resize(int width, int height);

private:
    void thread_proc();
    void initWin();
    void finishWin();

private:
    std::atomic<bool>               inited_;
    libtool::Event                  initEvent_;
    libtool::Event                  finishEvent_;
    std::thread                     thread_;
    libGlobalParam                  globalParam_;
    QApplication*                   qtapp_;
};

#endif // !__LIB_DIALOG_WIN_H__

libDialogWin.cpp:

#include "libDialogWin.h"
#include <process.h>
#include <atlconv.h>
#include <windows.h>

libDialogWin::libDialogWin()
    : libDialogBase()
    , inited_(false)
    , initEvent_()
    , finishEvent_()
    , thread_()
    , globalParam_()
    , qtapp_(nullptr)
{
}
libDialogWin::~libDialogWin()
{
}

bool libDialogWin::Initialize(const libGlobalParam* globalParam)
{
    bool expected = false;
    if (!inited_.compare_exchange_strong(expected, true))
        return true;

    if (!globalParam || !globalParam->hWindow)
        return false;

    globalParam_ = *globalParam;

    thread_ = std::thread(std::bind(&libDialogWin::thread_proc, shared_from_this()));
    initEvent_.Wait();

    return true;
}
void libDialogWin::UnInitialize()
{
    bool expected = true;
    if (!inited_.compare_exchange_strong(expected, false))
        return;

    if (qtapp_)
        qtapp_->quit();

    finishEvent_.Wait();
    // finishWin();

    inited_ = false;
}
bool libDialogWin::GetHandle(HWINDOW& handle)
{
    if (!inited_)
    {
        handle = 0;
        return false;
    }

    handle = hDialogHandle_;

    return true;
}
void libDialogWin::ShowDialog()
{
    if (inited_ && qDialogPtr_)
        qDialogPtr_->showDialog();
}
void libDialogWin::HideDialog()
{
    if (inited_ && qDialogPtr_)
        qDialogPtr_->hideDialog();
}
void libDialogWin::Resize(int width, int height)
{
    if (inited_ && qDialogPtr_)
        qDialogPtr_->resizeDialog(width, height);
}

void libDialogWin::thread_proc()
{
    initWin();
    qtapp_->exec();
    finishWin();
}
void libDialogWin::initWin()
{
    if (qtapp_ == nullptr && qDialogPtr_ == nullptr)
    {
        int argc = 1;
        TCHAR targv[MAX_PATH] = { 0 };

        GetModuleFileName(NULL, targv, MAX_PATH);
        USES_CONVERSION;
        std::string strPath = T2A(targv);
        char argv[MAX_PATH];
        memcpy(argv, strPath.c_str(), strPath.length());
        char* pargv = argv;
        qtapp_ = new QApplication(argc, &pargv);

        QWidget* parentWidget = QWidget::find(WId(globalParam_.hWindow));
        if (parentWidget != nullptr)
        {
            int w = parentWidget->width();
            int h = parentWidget->height();
        }
        qDialogPtr_ = std::make_shared<DialogQt>();
        hDialogHandle_ = (HWINDOW)qDialogPtr_->winId();

        initEvent_.Set();
    }
}
void libDialogWin::finishWin()
{
    if (qDialogPtr_)
    {
        qDialogPtr_->close();
        qDialogPtr_.reset();
    }
    SAFE_DELETE(qtapp_);
    finishEvent_.Set();
}

libDialogQt的声明和实现

libDialogQt.h:

#ifndef __LIB_DIALOG_QT_H__
#define __LIB_DIALOG_QT_H__

#include "libDialogBase.h"
#include "libTool.h"
#include <windows.h>
#include <atomic>
#include <thread>
#include <QObject>

// libDialogBase的实例类,当调用该库的程序为Qt程序时实例化该类
class libDialogQt : public QObject, public libDialogBase
{
    Q_OBJECT
public:
    libDialogQt(QObject* parent = nullptr);
    ~libDialogQt();

protected:
    virtual bool Initialize(const libGlobalParam* globalParam);
    virtual void UnInitialize();
    virtual bool GetHandle(HWINDOW& handle);
    virtual void ShowDialog();
    virtual void HideDialog();
    virtual void Resize(int width, int height);

protected:
    virtual void customEvent(QEvent *event);

private:
    std::atomic<bool>               inited_;
    libtool::Event                  initEvent_;
    libtool::Event                  finishEvent_;
    std::thread                     thread_;
    libGlobalParam                  globalParam_;
};


#include <QEvent>
class libDialogEvent : public QEvent
{
public:
    enum EventCode
    {
        CREATE,
        CLOSE,
    };

public:
    libDialogEvent(libDialogEvent::EventCode evtCode);
    ~libDialogEvent();
    static QEvent::Type evtType_;
    EventCode evtCode_;
};


#endif // !__LIB_DIALOG_QT_H__

libDialogQt.cpp:

 

#include "stdafx.h"
#include "libDialogQt.h"
#include <QCoreApplication>
#include <QWidget>


libDialogQt::libDialogQt(QObject* parent)
    : libDialogBase()
    , inited_(false)
    , initEvent_()
    , finishEvent_()
    , thread_()
    , globalParam_()
{
}
libDialogQt::~libDialogQt()
{
}

bool libDialogQt::Initialize(const libGlobalParam* globalParam)
{
    bool expected = false;
    if (!inited_.compare_exchange_strong(expected, true))
        return true;

    if (!globalParam || !globalParam->hWindow)
        return false;
    QCoreApplication* app = QCoreApplication::instance();
    if (globalParam->appType == APPTYPE::MAT_QT && app == nullptr)
        return false;

    globalParam_ = *globalParam;

    this->moveToThread(app->thread());
    QCoreApplication::postEvent(this, new libDialogEvent(libDialogEvent::EventCode::CREATE));
    initEvent_.Wait();
    inited_ = true;

    return true;
}
void libDialogQt::UnInitialize()
{
    bool expected = true;
    if (!inited_.compare_exchange_strong(expected, false))
        return;

    QCoreApplication::postEvent(this, new libDialogEvent(libDialogEvent::EventCode::CLOSE));
    inited_ = false;
}
bool libDialogQt::GetHandle(HWINDOW& handle)
{
    if (!inited_)
    {
        handle = 0;
        return false;
    }

    handle = hDialogHandle_;

    return true;
}
void libDialogQt::ShowDialog()
{
    if (inited_ && qDialogPtr_)
        qDialogPtr_->showDialog();
}
void libDialogQt::HideDialog()
{
    if (inited_ && qDialogPtr_)
        qDialogPtr_->hideDialog();
}
void libDialogQt::Resize(int width, int height)
{
    if (inited_ && qDialogPtr_)
        qDialogPtr_->resizeDialog(width, height);
}

void libDialogQt::customEvent(QEvent *event)
{
    if (event == nullptr) 
        return;
    if (event->type() == libDialogEvent::evtType_)
    {
        libDialogEvent* libEvent = dynamic_cast<libDialogEvent*>(event);
        if (libEvent->evtCode_ == libDialogEvent::CREATE)
        {
            QWidget* parent = QWidget::find(WId(globalParam_.hWindow));
            qDialogPtr_ = std::make_shared<DialogQt>(parent);
            initEvent_.Set();
        }
        else if (libEvent->evtCode_ == libDialogEvent::CLOSE)
        {
            if (qDialogPtr_)
            {
                qDialogPtr_->close();
                qDialogPtr_.reset();
            }
            finishEvent_.Set();
        }
    }
}

QEvent::Type libDialogEvent::evtType_ = (QEvent::Type)QEvent::registerEventType(QEvent::User + 100);
libDialogEvent::libDialogEvent(libDialogEvent::EventCode evtCode)
    : QEvent(evtType_)
    , evtCode_(evtCode)
{
}
libDialogEvent::~libDialogEvent()
{
}

可以看到增加了自定义的事件,并在事件处理中创建DialogQt的。

 

 5、dll接口和注意事项

dll的接口声明和注释如下:

extern "C"
{
#ifndef __LIB_QT_IN_H__
#define __LIB_QT_IN_H__

#include "libCommon.h"
    
#ifdef LIBQTIN_EXPORTS
#define LIBQTIN_API  __declspec(dllexport)
#else
#define LIBQTIN_API  __declspec(dllimport)
#endif

    /*
    * 初始化接口。通过全局参数传入一些必要的信息,比如主窗口句柄,主程序类型
    * 返回true表示接口调用成功,否则失败。通过回调告诉主程序初始化是否成功。
    */
    LIBQTIN_API bool libQtIn_Initliaze(const libGlobalParam* globalParam);

    /*
    * 加载接口。初始化并加载dll中封装的Qt界面并显示到主程序窗口中。
    * 返回true:接口调用成功,否则失败。
    * 通过回调函数告诉主程序加载是否成功。
    */
    LIBQTIN_API bool libQtIn_Load();

    /*
    * 获取库中封装的Qt主界面句柄
    */
    LIBQTIN_API bool libQtIn_GetHandle(HWINDOW& handle);

    /*
    * 设置dll中界面大小。
    * 主窗口在改变大小的时候可调用此接口让dll中的窗口随主窗口改变而改变
    * true表示成功,否则失败。
    */
    LIBQTIN_API bool libQtIn_Resize(int width, int height);

    /*
    * 卸载接口。卸载dll中的Qt窗口。
    */
    LIBQTIN_API void libQtIn_UnLoad();

    /*
    * 结束dll的调用,释放一些资源。
    */
    LIBQTIN_API void libQtIn_Finish();


#endif // !__LIB_QT_IN_H__
}

 

在dll中创建了Qt窗口(即类中的DialogQt界面),我们知道Qt界面程序需要使用Qt自带的moc来处理源文件。moc 全称是 Meta-Object Compiler,也就是“元对象编译器”。Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 源文件。如果它发现在一个头文件中包含了宏 Q_OBJECT,则会生成另外一个 C++ 源文件。这个源文件中包含了 Q_OBJECT 宏的实现代码。这个新的文件名字将会是原文件名前面加上 moc_ 构成。这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此我们可以知道,这个新的文件不是“替换”掉旧的文件,而是与原文件一起参与编译。另外,我们还可以看出一点,moc 的执行是在预处理器之前。因为预处理器执行之后,Q_OBJECT 宏就不存在了。

基于上述的原因,我们dll中封装的Qt界面同样需要moc来处理,否则无法使用。我的方法是在dll工程中增加一个moc.bat的批处理脚本,脚本内容为:

C:\Qt\Qt5.8.0\5.8\msvc2015_64\bin\moc "libDialogQt.h" -o "moc_libDialogQt.cpp"
C:\Qt\Qt5.8.0\5.8\msvc2015_64\bin\moc "DialogQt.h" -o "moc_DialogQt.cpp"

其中的路径为自己的Qt安装路径。

在第一次使用的时候,我们手动执行moc.bat,让其生成 moc_libDialogQt.cpp 和 moc_DialogQt.cpp两个源文件并加入工程中。然后在dll工程预处理事件中增加moc.bat命令,如下图所示:

 

这样每次在编译dll工程时,如果libDialogQt.h和DialogQt.h中有改变,执行moc命令后将改变写入到相应的源文件中编译了。

6、界面嵌入

将dll界面嵌入主程序中是通过窗口句柄来完成。

我们定义dll初始化参数为:

typedef struct _lib_Global_Param_ libGlobalParam;
struct _lib_Global_Param_
{
    HWINDOW             hWindow;            // 调用该dll窗口的父窗口句柄
    APPTYPE             appType;            // 调用该dll的主程序类型
    void*               userData;           // 用户数据,由调用者提供,可为nullptr
    void*               qtApplication;      // 调用该dll的程序若为Qt,则该成员为调用者的主程序指针,
                                            // 若为win32程序,则可为空
    void(*OnEvent)(LIBEVENT eventType, bool bSuccess, libGlobalParam* globalParam); // 调用接口的回调函数
                                            // 其中bSuccess为true,则表示该接口成功,否则为失败

    libGlobalParam()
        : hWindow(0)
        , appType(APPTYPE::MAT_QT)
        , userData(nullptr)
        , qtApplication(nullptr)
        , OnEvent(nullptr)
    {}
};

HWINDOW是void*类型,作为窗口句柄的类型。

MFC在拿到dll中窗口句柄并嵌入到自己界面的方法如下:

 

void CWinTestDlg::OnSize(UINT nType, int cx, int cy)
{
    CDialogEx::OnSize(nType, cx, cy);

    // TODO: 在此处添加消息处理程序代码
    HWINDOW libWinHandle = 0;
    if (libQtIn_GetHandle(libWinHandle) && libWinHandle != 0)
    {
        CRect rect;
        GetDlgItem(IDC_STATIC_QTDIALOG)->GetClientRect(&rect);
        ::SetParent((HWND)libWinHandle, GetDlgItem(IDC_STATIC_QTDIALOG)->GetSafeHwnd());
        ::MoveWindow((HWND)libWinHandle, rect.left, rect.top, rect.Width(), 
            rect.Height(), FALSE);
        ::ShowWindow((HWND)libWinHandle, SW_SHOW);
    }   
}

Qt主程序则将要嵌入的主界面句柄作为libGlobalParam的hWindow成员传进去即可。

该dll库程序是一个demo示例,仅仅用来作为DLL中封装Qt界面的一种解决方案。可以在dll的DialogQt上任意嵌入窗口、控件和布局,也可以让其作为父窗口创建子窗口。

 dll中窗口与主程序直接的消息传送则靠dll导出的接口。

文中的代码及测试示例下载地址为:https://download.csdn.net/download/explorer114/12326450

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 10
    点赞
  • 66
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 在Qt主界面运行DLLQt窗口,可以按照以下步骤进行: 1. 在Qt主界面,使用QLibrary类加载DLL文件。可以使用QLibrary的load函数来加载DLL文件,可以通过指定DLL文件的路径或者名称来加载。 2. 在加载DLL成功后,使用QLibrary的resolve函数来获取DLL的函数地址。可以使用QLibrary的resolve函数来获取DLL定义的Qt窗口类的构造函数。 3. 在获取到DLL的函数地址后,可以通过函数指针来调用DLL的函数。使用函数指针来调用DLL的构造函数,创建Qt窗口的实例。 4. 创建Qt窗口的实例后,可以将其作为子窗口添加到Qt主界面,在合适的位置显示。 需要注意的是,主界面和DLLQt窗口的代码需要使用相同的Qt版本,并且需要保证DLLQt窗口类与主界面Qt窗口类是一致的。此外,还需要确保DLL文件与运行的平台相匹配。 另外,为了避免内存泄漏,需要在合适的时候释放使用QLibrary加载的DLL文件。 以上是简单的步骤说明,具体实现过程可能根据实际情况有所区别,需要根据具体需求进行相应的调整。 ### 回答2: 在Qt主界面运行DLLQt窗口,需要以下几个步骤: 首先,确保你有一个DLL文件。DLL文件是包含了动态链接库的文件,它可以在运行时被加载并使用其的函数和类。 然后,在Qt主界面项目,通过Qt的插件机制将DLL文件加载到项目。可以使用QLibrary类来加载DLL文件,并使用resolve动态地获取DLL的函数和类。 接下来,通过获取DLL的类和函数来创建和显示Qt窗口。通过QLibrary的resolve函数可以获取DLL的类的指针,然后使用该指针创建窗口实例。通过调用窗口实例的show函数可以将窗口显示在Qt主界面。 最后,在适当的时机,需要释放DLL文件资源。可以在Qt主界面的析构函数,使用QLibrary的unload函数来释放DLL文件资源。 总结起来,要在Qt主界面运行DLLQt窗口,需要加载DLL文件,并使用resolve函数获取DLL的类指针,然后创建窗口实例并显示。在适当的时机需要释放DLL文件资源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值