NPAPI插件开发详细记录:插件开发入门

为什么这还是入门呢?因为前面的我们什么都没学会,只是学会了建立一个dll项目,并生成了一个Firefox可以识别dll,在测试页面中不会弹出需要下载插件的错误。而任何功能都没有实现。所以这里才是真正的入门。
本文随着作者本人的学习进度逐渐深入,难免有不周全之处,请看到本文的朋友帮忙指出有误的地方。

插件窗口在哪里

入门篇暂时只讲述一个问题,根据我的理解,plugin创建时,浏览器会创建一个子窗口,作为plugin的窗口(对于有窗口的plugin)。但是一直不知道这个子窗口怎么获取。仔细看上面的例子才发现,plugin类实现的不光是Plugin.cpp中的那几个函数,最重要的一个函数实现在Plugin.h中:NPBool init(NPWindow* pNPWindow) { m_bInitialized = TRUE; return TRUE;}
这个函数没有实现什么功能,但是注意这个函数的参数NPWindow*。啊哈!这个不就是我苦苦寻求的plugin窗口么?!!!
为了确认这一点,我就在这里下一个断点来看看,我觉得这个函数放在头文件中简直就是个错误,于是我将头文件中实现的三个函数都放到了.cpp文件中去实现了。
修改之后的头文件为:

[cpp]  view plain  copy
 print ?
  1. #pragma once  
  2. #include "pluginbase.h"  
  3. class Plugin :  
  4. public nsPluginInstanceBase  
  5. {  
  6. private:  
  7. NPP m_pNPInstance;  
  8. NPBool m_bInitialized;  
  9. public:  
  10. Plugin(NPP pNPInstance);  
  11. ~Plugin();  
  12.   
  13. NPBool init(NPWindow* pNPWindow);  
  14. void shut();  
  15. NPBool isInitialized();  
  16. };  

移动到Plugin.cpp文件中实现的三个函数为:
[cpp]  view plain  copy
 print ?
  1. NPBool Plugin::init(NPWindow* pNPWindow)  
  2. {  
  3. m_bInitialized = TRUE;  
  4. return TRUE;  
  5. }  
  6.   
  7. void Plugin::shut()  
  8. {  
  9. m_bInitialized = FALSE;  
  10. }  
  11.   
  12. NPBool Plugin::isInitialized()  
  13. {  
  14. return m_bInitialized;  
  15. }  


这样再次生成这个插件,下面以此为例展示一下如何对插件进行调试。

插件调试

首先在程序中下好断点:

然后打开网页,接着附加到进程:

注意plugin-container可能不止一个,选择的时候要根据其路径看准用来调试的那个。
附加到进程之后,在浏览器中刷新含有要调试的插件的页面。程序就在下断点的地方断下了:


可以看到pNPWindow传递过来的参数。这就是调试插件的具体操作了。

使用窗口句柄

从上面的调试过程可以发现,pNPWindow的window成员应该就是插件窗口的句柄了,根据相关资料进一步得知,对于windowed的plugin确实是窗口的句柄,而对于winless的plugin,window成员是一个HDC。因此,如果是winless的plugin,可以直接将pNPWindow->window强制转换为HDC然后在窗口中进行绘制、输出内容。
插件分为winless和windowed的,其主要区别是winless的插件窗口不是真正意义上的窗口只是网页的一部分,而windowed的插件窗口是浏览器的一个子窗口。由于winless的插件没有窗口所以其事件处理有一个专门的函数NPP_HandleEvent来对窗口的事件进行处理。对于winless的插件,事件响应函数就需要继承nsPluginInstanceBase类并实现函数HandleEvent;而对于windowed 的插件,插件窗口完全由插件控制,因此需要根据这个窗口句柄自行在插件中实现各种功能,包括事件消息响应等等。下面分别探讨在winless的插件和windowed的插件中如何使用窗口句柄。

winless插件

要创建winless的插件需要在新实例创建的时候指定为winless的,否则插件会默认创建为windowed的。修改NS_NewPluginInstance函数(该函数是我们自己实现的,在Plugin.cpp中,尽量不要修改np_entry.cpp、npn_gate.cpp、npp_gate.cpp这几个文件更不用说npapi.h了)。
[cpp]  view plain  copy
 print ?
  1. nsPluginInstanceBase * NS_NewPluginInstance(nsPluginCreateData * aCreateDataStruct)  
  2. {  
  3. if(!aCreateDataStruct)  
  4. return NULL;  
  5.   
  6. Plugin * plugin = new Plugin(aCreateDataStruct->instance);  
  7.   
  8. BOOL bWindowed = FALSE;  
  9. NPN_SetValue(aCreateDataStruct->instance, NPPVpluginWindowBool, (void *)bWindowed);//winless插件需要在这里指出,默认为windowed  
  10.   
  11. return plugin;  
  12. }  
用NPN_SetValue指定我们要创建的插件是winless的。上面的写法是中规中矩的写法,如果图省事可以写成NPN_SetValue(aCreateDataStruct->instance, NPPVpluginWindowBool, NULL);
本例参考sdk中sample目录下的winless例子来写的,有必要的话你可以参考那个例子。下面要实现一个功能:将插件窗口的背景设置为蓝色,另外按下鼠标左键会发出声音,鼠标移动时在插件窗口画线。
首先,为plugin类添加一个私有成员mWindow保存窗口指针,覆写基类的SetWindow函数,

[cpp]  view plain  copy
 print ?
  1. NPError Plugin::SetWindow(NPWindow* Window)  
  2. {  
  3. mWindow=Window;  
  4. return NPERR_NO_ERROR;  
  5. }  
其他功能都要在事件处理函数中实现,因此基类的虚函数HandleEvent需要加以实现。
[cpp]  view plain  copy
 print ?
  1. uint16_t Plugin::HandleEvent(void* aEvent)  
  2. {  
  3. NPEvent *event = (NPEvent *)aEvent;  
  4. switch(event->event)  
  5. {  
  6. case WM_PAINT:  
  7. {  
  8. if(!mWindow)  
  9. break;  
  10.   
  11. // get the dirty rectangle to update or repaint the whole window  
  12. RECT * drc = (RECT *)event->lParam;  
  13. if(drc)  
  14. FillRect((HDC)event->wParam, drc, (HBRUSH)(COLOR_WINDOW));  
  15. else {  
  16. RECT rc;  
  17. rc.bottom = mWindow->y + mWindow->height;  
  18. rc.left = mWindow->x;  
  19. rc.right = mWindow->x + mWindow->width;  
  20. rc.top = mWindow->y;  
  21. FillRect((HDC)event->wParam, &rc, (HBRUSH)(COLOR_WINDOW));  
  22.   
  23. }  
  24.   
  25. break;  
  26.   
  27. }  
下一个功能:按下鼠标左键时发声:
[cpp]  view plain  copy
 print ?
  1. case WM_LBUTTONDOWN:  
  2. Beep(1000,200);  
  3. break;  
最后实现画线的功能,这也是真正展示处理窗口的window成员的地方:
首先添加私有成员变量: int m_oldX,m_OldY,m_newX,m_newY;
在左键按下的事件中记录一个坐标,将左键按下的代码修改为:

[cpp]  view plain  copy
 print ?
  1. case WM_LBUTTONDOWN:  
  2. Beep(1000,200);  
  3. m_oldX=LOWORD(event->lParam);  
  4. m_OldY=HIWORD(event->lParam);  
  5. break;  
接着实现鼠标移动的事件响应代码:
[cpp]  view plain  copy
 print ?
  1. case WM_MOUSEMOVE:  
  2. {  
  3. m_newX=LOWORD(event->lParam);  
  4. m_newY=HIWORD(event->lParam);  
  5. HDC hDC = (HDC)mWindow->window;  
  6. MoveToEx(hDC,m_oldX,m_OldY,NULL);  
  7. LineTo(hDC,m_newX,m_newY);  
  8. m_oldX=m_newX;m_OldY=m_newY;  
  9. break;  
  10. }  
这里直接将mWindow->window强制转换为HDC,是因为对于winless的插件window成员在windows平台下就是该插件的HDC。注意由于这里有变量hDC的定义,因此这个case的代码必须用大括号括起来,否则会报错。下面是事件处理函数最后一点代码:
[cpp]  view plain  copy
 print ?
  1. default:  
  2. return 0;  
  3. }  
  4. return 1;  
  5. }  
实现完毕之后,生成项目,然后用前面的测试页面对这个新的dll进行测试。如下图:

上面实现的有一个问题,就是只有窗口重绘的时候才会显示出画的线,但我想已经说明了window成员在winless类型的插件中的使用,因此就不深入研究了,如果谁看到这个例子能够给出好的改进意见,我将不胜感激。

可能有的朋友一步步跟下来之后发现还是有问题,下面贴出前面这个项目的plugin.h文件及plugin.cpp文件的完整内容

[cpp]  view plain  copy
 print ?
  1. //Plugin.h文件  
  2. #pragma once  
  3. #include "pluginbase.h"  
  4. class Plugin :  
  5.     public nsPluginInstanceBase  
  6. {  
  7. private:  
  8.     NPP m_pNPInstance;  
  9.     NPBool m_bInitialized;  
  10.     NPWindow* mWindow;  
  11.     int m_oldX,m_OldY,m_newX,m_newY;  
  12.     bool bMdown;  
  13. public:  
  14.     Plugin(NPP pNPInstance);  
  15.     ~Plugin();  
  16.   
  17.     NPBool init(NPWindow* pNPWindow);  
  18.     void shut();  
  19.     NPBool isInitialized();  
  20.     NPError SetWindow(NPWindow* Window);  
  21.     uint16_t HandleEvent(void* event);//windowed 插件窗口自己接收消息,不需handleevent。  
  22.   
  23. };  

[cpp]  view plain  copy
 print ?
  1. //Plugin.cpp文件  
  2. #include "Plugin.h"  
  3.   
  4. // functions /  
  5. NPError NS_PluginInitialize()  
  6. {  
  7.     return NPERR_NO_ERROR;  
  8. }  
  9.   
  10. void NS_PluginShutdown()  
  11. {  
  12. }  
  13.   
  14. nsPluginInstanceBase * NS_NewPluginInstance(nsPluginCreateData * aCreateDataStruct)  
  15. {  
  16.     if(!aCreateDataStruct)  
  17.         return NULL;  
  18.   
  19.     Plugin * plugin = new Plugin(aCreateDataStruct->instance);  
  20.   
  21.     BOOL bWindowed = FALSE;  
  22.     NPN_SetValue(aCreateDataStruct->instance, NPPVpluginWindowBool, (void *)bWindowed);//winless插件需要在这里指出,默认为windowed  
  23.     return plugin;  
  24. }  
  25.   
  26. void NS_DestroyPluginInstance(nsPluginInstanceBase * aPlugin)  
  27. {  
  28.     if(aPlugin)  
  29.         delete (Plugin *)aPlugin;  
  30. }  
  31.   
  32. // Plugin /  
  33. Plugin::Plugin(NPP pNPInstance):nsPluginInstanceBase(),  
  34.     m_pNPInstance(pNPInstance),  
  35.     m_bInitialized(FALSE)  
  36. {  
  37.     bMdown=false;  
  38. }  
  39.   
  40. NPBool Plugin::init(NPWindow* pNPWindow)  
  41. {  
  42.     m_bInitialized = TRUE;  
  43.     return TRUE;  
  44. }  
  45.   
  46. void Plugin::shut()  
  47. {  
  48.     m_bInitialized = FALSE;   
  49. }  
  50.   
  51. NPBool Plugin::isInitialized()  
  52. {  
  53.     return m_bInitialized;  
  54. }  
  55.   
  56. Plugin::~Plugin(void)  
  57. {  
  58. }  
  59.   
  60. NPError Plugin::SetWindow(NPWindow* Window)  
  61. {  
  62.     mWindow=Window;  
  63.     return NPERR_NO_ERROR;  
  64. }  
  65.   
  66. uint16_t Plugin::HandleEvent(void* aEvent)  
  67. {  
  68.     NPEvent *event = (NPEvent *)aEvent;  
  69.     switch(event->event)  
  70.     {  
  71.     case WM_PAINT:    
  72.         {  
  73.             if(!mWindow)  
  74.                 break;  
  75.   
  76. // get the dirty rectangle to update or repaint the whole window  
  77.             RECT * drc = (RECT *)event->lParam;  
  78.             if(drc)  
  79.                 FillRect((HDC)event->wParam, drc, (HBRUSH)(COLOR_WINDOW));  
  80.             else {  
  81.                 RECT rc;  
  82.                 rc.bottom = mWindow->y + mWindow->height;  
  83.                 rc.left   = mWindow->x;  
  84.                 rc.right  = mWindow->x + mWindow->width;  
  85.                 rc.top    = mWindow->y;  
  86.                 FillRect((HDC)event->wParam, &rc, (HBRUSH)(COLOR_WINDOW));  
  87.   
  88.             }  
  89.   
  90.             break;  
  91.   
  92.         }  
  93.   
  94.     case WM_LBUTTONDOWN:  
  95.         Beep(1000,200);  
  96.         m_oldX=LOWORD(event->lParam);  
  97.         m_OldY=HIWORD(event->lParam);  
  98.         bMdown=true;  
  99.         break;  
  100.   
  101.     case WM_MOUSEMOVE:  
  102.         {  
  103.             m_newX=LOWORD(event->lParam);  
  104.             m_newY=HIWORD(event->lParam);  
  105.             if (bMdown)  
  106.             {  
  107.                 HDC hDC = (HDC)mWindow->window;  
  108.                 MoveToEx(hDC,m_oldX,m_OldY,NULL);  
  109.                 LineTo(hDC,m_newX,m_newY);  
  110.             }  
  111.             NPN_ForceRedraw(m_pNPInstance);           
  112.             m_oldX=m_newX;m_OldY=m_newY;  
  113.             break;  
  114.         }  
  115.     case WM_LBUTTONUP:  
  116.         bMdown=false;NPN_ForceRedraw(m_pNPInstance);break;  
  117.     default:  
  118.         return 0;  
  119.     }  
  120. //    
  121.     return 1;  
  122. }  


windowed插件

创建windowed的插件,可以这样:
[cpp]  view plain  copy
 print ?
  1. BOOL bWindowed = TRUE;  
  2. NPN_SetValue(aCreateDataStruct->instance, NPPVpluginWindowBool, (void *)bWindowed);//winless插件需要在这里指出,默认为windowed  
也可以什么都不写,因为默认就是创建windowed的插件。这个例子实现两个功能:设置背景并在创建窗口中输出一段文字。
首先,为Plugin类添加一个私有成员:HWND m_hWnd;
要对窗口进行处理需要使用到窗口子类化的函数,要包含头文件#include <windowsx.h>(Windows.h在其他头文件中已经包含了,这里可以不需要再写)
在plugin.cpp中声明一个回调函数和一个静态变量:

[cpp]  view plain  copy
 print ?
  1. static LRESULT CALLBACK PluginWinProc(HWNDUINTWPARAMLPARAM);  
  2. static WNDPROC lpOldProc = NULL;  
在类的构造函数中将m_hWnd初始化为NULL
修改Plugin::init为:

[cpp]  view plain  copy
 print ?
  1. NPBool Plugin::init(NPWindow* pNPWindow)  
  2. {  
  3. mWindow=pNPWindow;  
  4. m_hWnd =(HWND)pNPWindow->window;  
  5. if (m_hWnd==NULL) return FALSE;  
  6. // 对窗口进行子类化,这样就可以对消息进行处理并在窗口中进行绘制  
  7. lpOldProc = SubclassWindow(m_hWnd, (WNDPROC)PluginWinProc);  
  8.   
  9. // 将窗口与 Plugin 对象相关联,这样就可以在窗口处理中访问Plugin 对象  
  10. SetWindowLongPtr(m_hWnd, GWLP_USERDATA, (LONG_PTR)this);  
  11.   
  12. m_bInitialized = TRUE;  
  13. return TRUE;  
  14. }  
对于windowed的插件pNPWindow->window存储就是插件窗口的句柄,上面将其转换为HWND,并将窗口进行子类化,关键代码的作用请参看注释。将Plugin::shut修改为:
[cpp]  view plain  copy
 print ?
  1. void Plugin::shut()  
  2. {  
  3. // 反子类化subclass it back  
  4. SubclassWindow(m_hWnd, lpOldProc);  
  5. m_hWnd = NULL;  
  6. m_bInitialized = FALSE;  
  7. }  
到这一步已经完成了对窗口句柄的获取与释放,最后还需要的就是消息处理函数了,如下:
[cpp]  view plain  copy
 print ?
  1. static LRESULT CALLBACK PluginWinProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)  
  2. {  
  3. switch (msg) {  
  4. case WM_PAINT:  
  5. {  
  6. // draw a frame and display some string  
  7. PAINTSTRUCT ps;  
  8. HDC hdc = BeginPaint(hWnd, &ps);  
  9. RECT rc;  
  10. GetClientRect(hWnd, &rc);  
  11. FillRect( hdc, &rc, (HBRUSH) (COLOR_WINDOW));  
  12. FrameRect(hdc, &rc, GetStockBrush(BLACK_BRUSH));  
  13. Plugin * p = (Plugin*) GetWindowLongPtr(hWnd, GWLP_USERDATA);  
  14. if(p) {  
  15. char *s = "Hello , MY FIRST PLUGIN!";//p->GetGuiText();  
  16. DrawText(hdc, s, strlen(s), &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);  
  17. }  
  18.   
  19. EndPaint(hWnd, &ps);  
  20. }  
  21. break;  
  22. default:  
  23. break;  
  24. }  
  25.   
  26. return DefWindowProc(hWnd, msg, wParam, lParam);  
  27. }  
这里只是对WM_PAINT消息进行了处理,其他消息用DefWindowProc进行默认的处理。在WM_PAINT的消息处理代码中我们为窗口设置了背景,并输出一段文字"Hello , MY FIRST PLUGIN!",没错,这就是我们的插件的hello world!!
效果如下图:

最后这个项目的plugin.h文件和plugin.cpp文件完整内容

[cpp]  view plain  copy
 print ?
  1. //Plugin.h  
  2. #pragma once  
  3. #include "pluginbase.h"  
  4. class Plugin :  
  5.     public nsPluginInstanceBase  
  6. {  
  7. private:  
  8.     NPP m_pNPInstance;  
  9.     NPBool m_bInitialized;  
  10.     NPWindow* mWindow;  
  11.     HWND m_hWnd;  
  12.   
  13. public:  
  14.     Plugin(NPP pNPInstance);  
  15.     ~Plugin();  
  16.   
  17.     NPBool init(NPWindow* pNPWindow);//  {  m_bInitialized = TRUE;  return TRUE;}  
  18.     void shut();//  {  m_bInitialized = FALSE;  }  
  19.     NPBool isInitialized();// {  return m_bInitialized;  }  
  20. };  

[cpp]  view plain  copy
 print ?
  1. //Plugin.cpp  
  2. #include "Plugin.h"  
  3.   
  4. #include <windowsx.h>  
  5.   
  6. // functions /  
  7. NPError NS_PluginInitialize()  
  8. {  
  9.     return NPERR_NO_ERROR;  
  10. }  
  11.   
  12. void NS_PluginShutdown()  
  13. {  
  14. }  
  15.   
  16. nsPluginInstanceBase * NS_NewPluginInstance(nsPluginCreateData * aCreateDataStruct)  
  17. {  
  18.     if(!aCreateDataStruct)  
  19.         return NULL;  
  20.   
  21.     Plugin * plugin = new Plugin(aCreateDataStruct->instance);  
  22.   
  23.     return plugin;  
  24. }  
  25.   
  26. void NS_DestroyPluginInstance(nsPluginInstanceBase * aPlugin)  
  27. {  
  28.     if(aPlugin)  
  29.         delete (Plugin *)aPlugin;  
  30. }  
  31.   
  32. // Plugin /  
  33. Plugin::Plugin(NPP pNPInstance):nsPluginInstanceBase(),  
  34.     m_pNPInstance(pNPInstance),  
  35.     m_bInitialized(FALSE)  
  36. {  
  37.     m_hWnd = NULL;  
  38. }  
  39.   
  40. Plugin::~Plugin(void)  
  41. {  
  42. }  
  43.   
  44. static LRESULT CALLBACK PluginWinProc(HWNDUINTWPARAMLPARAM);  
  45. static WNDPROC lpOldProc = NULL;  
  46.   
  47. NPBool Plugin::init(NPWindow* pNPWindow)  
  48. {  
  49.     mWindow=pNPWindow;  
  50.     m_hWnd =(HWND)pNPWindow->window;  
  51.     if (m_hWnd==NULL) return FALSE;  
  52.     // 对窗口进行子类化,这样就可以对消息进行处理并在窗口中进行绘制  
  53.     lpOldProc = SubclassWindow(m_hWnd, (WNDPROC)PluginWinProc);  
  54.   
  55.     // 将窗口与 Plugin 对象相关联,这样就可以在窗口处理中访问Plugin 对象  
  56.     SetWindowLongPtr(m_hWnd, GWLP_USERDATA, (LONG_PTR)this);  
  57.   
  58.     m_bInitialized = TRUE;  
  59.     return TRUE;  
  60. }  
  61.   
  62. void Plugin::shut()  
  63. {  
  64.     // 将窗口子类化回去subclass it back  
  65.     SubclassWindow(m_hWnd, lpOldProc);  
  66.     m_hWnd = NULL;  
  67.     m_bInitialized = FALSE;   
  68. }  
  69.   
  70. NPBool Plugin::isInitialized()  
  71. {  
  72.     return m_bInitialized;  
  73. }  
  74.   
  75.   
  76. static LRESULT CALLBACK PluginWinProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)  
  77. {  
  78.   switch (msg) {  
  79.     case WM_PAINT:  
  80.       {  
  81.         // draw a frame and display some string  
  82.         PAINTSTRUCT ps;  
  83.         HDC hdc = BeginPaint(hWnd, &ps);  
  84.         RECT rc;  
  85.         GetClientRect(hWnd, &rc);  
  86.                 FillRect( hdc, &rc, (HBRUSH) (COLOR_WINDOW));  
  87.         FrameRect(hdc, &rc, GetStockBrush(BLACK_BRUSH));  
  88.         Plugin * p = (Plugin*) GetWindowLongPtr(hWnd, GWLP_USERDATA);  
  89.         if(p) {  
  90.             char *s = "Hello , MY FIRST PLUGIN!";//p->GetGuiText();  
  91.             DrawText(hdc, s, strlen(s), &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);  
  92.         }  
  93.   
  94.         EndPaint(hWnd, &ps);  
  95.       }  
  96.       break;  
  97.     default:  
  98.       break;  
  99.   }  
  100.   
  101.   return DefWindowProc(hWnd, msg, wParam, lParam);  
  102. }  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值