PowerBuilder 9开发技术讲座-PowerBuilder Native Interface(PBNI)

PowerBuilder 9现在对于其他开发语言的支援,有了全新的突破,在以往使用PowerBuilder开发程式时,要和C++或是Java程式互通有无是有一些折衷的办法,但是总是没有办法做到简易而且全面性的支援。现在只要透过PowerBuilder 9的PBNI技术,就可以让PowerBuilder的程式呼叫Java,或是在一个C++的程式中引用PowerBuilder NVO物件函数。

 

以往的PowerBuilder程式只能够透过外在函数呼叫的方式来存取C/C++的函数,但在PowerBuilder 9.0之中扩增了一项强而有力的介面-「PowerBuilder Native Interface」,简称PBNI。透过PBNI的开发方式,PowerBuilder开发人员不仅可以使用物件导向的方式来存取C/C++函数,而且还可反向地让C/C++程式呼叫PowerBuilder之中的物件,达到应用程式的整合。更甚者,在藉由JNI与PBNI两者的结合,Java应用程式也可双向地与PowerBuilder程式沟通。

 

在谈什么是PBNI之前,我们先来谈谈下面三个问题: 
1.开发人员有办法用PowerBuilder程式呼叫C或是C++的程式吗? 
2.开发人员有办法用PowerBuilder程式呼叫一些外部元件像是Java EJB元件、 Web Service元件、Java Class程式等诸如此类的元件吗? 
3.开发人员有办法用反过来,用C或是C++呼叫已经使用PowerBuilder开发好的程式吗?

 
上述三个问题,在过去的PowerBuilder其实都可以做到某种程度的地步,只是都有些问题。传统上使用PowerBuilder开发上述的程式时,如果要呼叫C或是C++ 的程式,是可以使用宣告外部函数的方式来使用一个已经撰写好的DLL函数, 

 

例如: 
FUNCTION ulong GetSysColor (int index) LIBRARY "USER32.DLL” 
FUNCTION boolean sndPlaySoundA (string SoundName, uint Flags) LIBRARY "WINMM.DLL"


可是如果是下面的程式呢: 
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM, lParam); 
这个可是个大问题了,因为这个Windows DLL Function中会用到所谓的「Callback」 函数的技术,所谓的Callback Function指的是今天有A和B两个物件,在程式中A物件呼叫B物件的Function,而在该B物件的Function又会回头呼叫A物件的其他Function,这就叫「Callback」。在PowerBuilder呼叫C的Function后,在这个C的Function中要再回头呼叫PowerBuilder的函数是不可能用引用外部函数的方式来达到这个目地的。除了Callback Function使用困难之外,使用外部函数也有资料型态的限制,以及没有办法使用物件导向的方式开发等种种的困难及问题。 


再来谈谈PowerBuilder呼叫外部的元件的方法,在以前能够让PowerBuilder呼叫EJB元件,就只能透过一些协力厂商开发的「COM Bridge」,让PowerBuilder程式透过COM元件来呼叫Java程式。至于要让Java或是C++来呼叫PowerBuilder 程式的话,过去最常见的方法就是把这个PowerBuilder的程式包装成为「OLE automation server」。这些方法都不是一个真正解决的好方法,说穿了,这些方法
跟本就没有办法直接和PowerBuilder的核心「PowerBuilder Visual Machine」做沟通,所以在过去的版本的PowerBuilder,是一直有这种和其他语言程式不能沟通的困扰,这也是大家一直认为,PowerBuilder是一个封闭不开放的开发工具。 PowerBuilder 9这个版本有几个突破性的技术,而PBNI就是其中一个。所谓的PBNI (PowerBuilder Native Interface),指的是PowerBuilder提供一个「原生介面
(Native Interface)」,透过这个介面可以使得PowerBuilder提高了对其他程式语言的扩展能力,比方说透过该介面可以存取任何类型的外部应用应用程式,或是让外界其他的程式语言存取或是呼叫PowerBuilder开发的程式,下面是一个简单的PBNI的示意图: 

此主题相关图片如下:


在上面这张图中,PBNI提供了两道让外界可以和PowerBuilder核心(PBVM)的介面窗口,第一个对外的窗口是指在图的右半边,我们可以开发「PB Extension」, PB Extension其实最后会变成DLL,透过该技术,C或是C++的DLL程式可以包装成为一个「PBD」的档案,而该PBD的档案就可以在开发程式时,加到Library Search Path中,让PowerBuilder直接存取PBD里的物件函数,你可以把它当作是一个很像PowerBuilder NVO的东西来对待它。第二个对外的窗口是指在图的左半边,你可以把PowerBuilder Virtual Machine 「内嵌」到一个C++的应用程式中,在C++程式中就可以直接呼叫PowerScript Function。


PBNI的元素


PBNI提供了一些基本的元素,透过这些元素,程式开发人员可以快速的引用外部程式语言,下面是常见到的PBNI元素: 
PBNI提供的介面(Interface): 
IPB_VM:这个介面的作用,在于当你要用C++或是其他的程式语言来呼叫PowerBuilder开发的程式,或是你希望要和PowerBuilder的核心「PBVM」进行互动,或是沟通协调,你可以使用这个介面。 
IPB_Session:这是一个抽象的介面,这个介面可以用来定义诸如存取PowerScript里面的资料、建立PowerBuilder物件和呼叫PowerScript函数操作的方法
IPB_Value:这个介面你可以把它想像成是它就是代表PowerBuilder的值。这些值可以是PowerBuilder的标准资料型态,例如String、Long、Integer、Char等等。所以这个介面提供了关于每个变数的资讯,包括变数的类型、标记、存取权限(Public、Private和Protected)、变数值或参数存取方式(例如Call by Value或是Reference)。 
IPB_Arguments:这个介面可以让使用者在PowerBuilder VM和「PB Extension」间传递参数。 
IPBX_NonVisualObject和IPBX_VisualObject:这两介面很意思,因为它们可以在C++程式中实作出来,而且是放在PB Extension里面,你在PowerBuilder中就可以用PBD的方式看到你实作出来的物件,而要写这些可见或是不可见的物件,靠的就是IPBX_NonVisualObject和IPBX_VisualObject介面。 
IPBX_Marshaler:这个介面是当你要出一个「PB marshaler extension 」时, 一定要实作出IPBX_Marshaler这个`介面。这个介面尤其是你要由PowerBuilder呼叫Java程式时,一定要用到的一个介面。 

PBNI提供的Structures: 
PBCallInfo:这个Structure可以在开发PBNI程式时,让PBNI和PowerBuilder之间呼叫的函数保持参数和回传值的资讯。如果要存取在
PBCallInfo中的资讯,可以使用IPB_Arguments介面来获得PBCallInfo。 
PBArrayInfo:PBArrayInfo是一个C++的structure,这个Structure可以在阵列中保持一些资讯。 
PBNI提供的Globle Function: 如果你要写一个PowerBuilder extension的程式(说穿了就是用C++写一个DLL档啦),这个物件必须要汇出两个Global Functions,让这个程式可以「内嵌」 PowerBuilder VM并且建立实体出来。下面是PBNI提供的Globle 
Function: 
PBX_GetDescription() 
PBX_CreateNonVisualObject() 
PBX_CreateVisualObject() 
PBX_InvokeGlobalFunction() 
PBNI提供的Helper classes: 
Helper Classes指的是一些辅助的类别物件,PBNI提供像是PBObjectCreator、 PBArrayAccessor和PBEventTrigger等辅助类别,透过这些辅助类别物件可使PBNI在开发上更简单。


PBNI的开发方式


在了解PBNI有那些元素后,读着应该也了解到何谓PBNI,并且知道PBNI能帮我们做什么。在针对不同的目地,PBNI也有不同的开发方式,常见的PBNI开发目地为下列四个,在后面的部份会祥细的说明PBNI的开发方式为何: 
建立PB extensions 
建立PB marshaler extensions 
建立PB visual extensions 
内嵌PBVM到C++的应用程式中

 

之前有跟各位读者提过,PBNI提供了两个对外的方法,其中一种就是将C或是C++写好的DLL档案,透过PBNI提供的介面来包装成一个PowerBuilder认得的PBD档案,这种方式称之为建立「PB Extensions」。在开发一个PB Extensions的程式时,我们必须先设想好,最后我们要产生的PBD中,会有那些物件。比方说,我现在手头上正在写一个C++的程式,我希望这个C++的程式最后透过PBNI 
的帮助,产生一个PBD档案,而且在这个PBD里面有一个Funtion物件,而这个Function物件会对照于我在C++里面写好的Function,让我只要呼叫该Function 物件,就等于是执行C++里的程式。刚才的设想中,开发的步骤如下: 
1.使用C++的开发工具建立一个C++专案。 
2.在C++的程式中,汇入PBNI SDK提供给C++的相关表头档(h档案)。 
3.在C++的程式中,透过PBX_GetDescription()这个PBNI提供的函数,告知到时后会汇出一个Globle Function。 
4.因为要做的是一个Globle Function物件,所以在C++的程式中,透过PBX_InvokeGlobalFunction()这个PBNI提供的函数,实作该Function的程式出来。 
5.将开发好的C++程式编辑成DLL档。 
6.透过PBNI提供的「pbx2pbd90.exe」小工具,将这个DLL档案转换为PBD档。 
7.打开PowerBuilder后,将这个PBD档加到Library Search Path中。 
8.开发相关的PowerBuilder程式,并且呼叫这个PBD档的Globle Function。


定义要汇出的物件类别


在上述的步骤中,PBX_GetDescription()这个PBNI提供的函数是一定要有的,因为这个函数是用来产生相关的类别定义,而这个类别定义最后会在将DLL档案转换成PBD档时,跟据你在PBX_GetDescription()函数中的定义,产生相对应的PowerBuilder物件。下面在C++的程式,最后会产生一个PB的Globle Function物件,这个Globle Function物件名称是GetUserName(),而它的回传值是String资料型态: 
PBXEXPORT LPCTSTR PBXCALL PBX_GetDescription() 

static const TCHAR desc[] = { 
"globalfunctions /n" 
"function string GetUserName()/n" 
"end globalfunctions /n" 
}; 

return desc ; 

再举一个例子,下面写在C++里的PBX_GetDescription()函数程式,最后会产生一个PowerBuilder的可视物件「flagext」,在这个可视物件中,有两个事件为分别为「onclick」Event和「ondoubleclick」 Evnet;在可视物件中,还有两个物件Function 「settext(string txt)」和「setflag(int flag)」: 
PBXEXPORT LPCTSTR PBXCALL PBX_GetDescription() 

static const TCHAR desc[] = { 
"class flagext from usero b j e c t/n" 
"event int onclick()/n" 
"event int ondoubleclick()/n" 
"subroutine settext(string txt)/n" 
"subroutine setflag(int flag)/n" 
"end class/n" 
}; 
return desc; 
}


实做类别物件程式码


之前提到的步骤中,除了PBX_GetDescription()之外,我们还会步骤四中看到一个PBNI提供的Function,叫PBX_InvokeGlobalFunction(),这是因为我们要实作出Globle Function的程式,所以就必须要使用PBX_InvokeGlobalFunction()函数;相同的道理,如果在PBX_GetDescription()中我们准备建立的是一个NVO物件,那就要用PBX_CreateNonVisualObject()函数实作出NVO物件的程式;如果在PBX_GetDescription()中我们准备建立的是一个可视的PowerBuilder物件,那就要用PBX_CreateVisualObject()函数实作出这一个可视物件的程式。下面是一个在C++中使用PBX_InvokeGlobalFunction()来实作出一个Globle Function程式的例子: 
PBXEXPORT PBXRESULT PBXCALL PBX_InvokeGlobalFunction 

IPB_Session* pbsession, 
LPCTSTR functionName, 
PBCallInfo* ci 


if ( strcmp( functionName, "getusername" ) == 0 ) 

CWinAPI *WinAPI = new CWinAPI( pbsession ) ; 
WinAPI->PBNIGetUserName ( ci ) ; 
if ( WinAPI != NULL ) delete WinAPI ; 
return PBX_OK ; 
} ; 
return PBX_E_NO_SUCH_CLASS ; 

在上面的程式码中,读者可以发现,程式PB Extension和PB的核心PBVM的沟通是透过IPB_Session这个指标变数来保持C++和PowerBuilder程式的连结,而在PBNI和PowerBuilder之间呼叫的函数保持参数和回传值的资讯就是透过之前介绍的PBCallInfo这个指标结构来保存,下面是PBVM和PB Extension之间的关系示意图: 

此主题相关图片如下:


下面是完整的程式码,这个程式码中,在pbniwinapi.cpp程式实作出类别「CWinAPI」,在这个Class中,有一个PBNIGetUserName()函数会透过Windows 作业系统的API取得电脑的使用者名称,而main.cpp中会汇出一个PB Globle Function叫「GetUserName()」: 
pbniwinapi.cpp 
#include <WINDOWS.H> 
#include <stdio.h> 
#include "PBNIWINAPI.h" 
CWinAPI::CWinAPI( IPB_Session * pSession ) 
: m_pSession( pSession ) 


CWinAPI::~CWinAPI(void) 


void CWinAPI::PBNIGetUserName ( 
PBCallInfo *ci ) 

LPTSTR lpszSystemInfo; 
DWORD cchBuff = 256; 
TCHAR tchBuffer[1024]; 
lpszSystemInfo = tchBuffer; 
GetUserName ( lpszSystemInfo, &cchBuff) ; 
ci->returnValue->SetString ( lpszSystemInfo ) ; 

void CWinAPI::Destroy() 

delete this ; 

main.cpp 
#include <windows.h> 
#include <pbext.h> 
#include "pbniwinapi.h" 
BOOL APIENTRY DllMain( 
HANDLE hModule, 
DWORD reasonForCall, 
LPVOID lpReserved 


switch( reasonForCall ) 

case DLL_PROCESS_ATTACH: 
case DLL_THREAD_ATTACH: 
case DLL_THREAD_DETACH: 
case DLL_PROCESS_DETACH: 
break; 

return TRUE; 

PBXEXPORT LPCTSTR PBXCALL PBX_GetDescription() 

static const TCHAR desc[] = { 
"globalfunctions /n" 
"function string GetUserName()/n" 
"end globalfunctions /n" 
}; 
return desc ; 

PBXEXPORT PBXRESULT PBXCALL PBX_InvokeGlobalFunction 

IPB_Session* pbsession, 
LPCTSTR functionName, 
PBCallInfo* ci 


if ( strcmp( functionName, "getusername" ) == 0 ) 

CWinAPI *WinAPI = new CWinAPI( pbsession ) ; 
WinAPI->PBNIGetUserName ( ci ) ; 
if ( WinAPI != NULL ) delete WinAPI ; 
return PBX_OK ; 
} ; 
return PBX_E_NO_SUCH_CLASS ; 
}


产生PowerBuilder延伸物件


在上面的C++程式完成后,便可以编辑一个DLL档案,可是这个DLL档案并不是要让PowerBuilder直接拿来用,因为这样子一来,就又回到使用外部函数的做法,比较好的方式是要产生PB Extension,也就是说把这个DLL「再包一层」,用一个PBD帮这个DLL档案做一个PowerBuilder看的懂的外皮,然后让PowerBuilder透过PBD来「认识」你写好的DLL程式。 在PowerBuilder9安装目录下%Sybase9%/PowerBuilder 9.0/SDK/PBNI,你会发现有一个叫「pbx2pbd90.exe」的档案,如果要帮DLL再包一层PBD档案,必须透过pbx2pbd90.exe这一个档案,它的语法如下: 
pbx2pbd90 your.pbd your.dll 
比方说有一个DLL档案叫「pbniwinapi.dll」,要把这个档案转成PBD档,就可以这样子下: 

pbx2pbd90 pbniwinapi.pbd pbniwinapi.dll 
如此一来,它会产生「pbniwinapi.pbd」档,并且根据你原先写在C++程式中的PBX_GetDescription()函数内容,在这个PBD档案产生出相对应的PowerBuilder 可以认得的物件,让开发人员取得该物件后,用PowerBuilder原生语法PowerScript就可以呼叫该物件的函数来做事情。 


下图是一个产生PBD档案的示意图:


此主题相关图片如下:

PowerBuilder使用PB Extension开发程式


产生好PB Extension后,读者一定很好奇两件事,第一件事就是我可不可以只要用产生好的PBD物件,而把原来的DLL档案删除?答案是不行,因为PBD物件只是一个帮DLL档案产生出来的一个「空壳」,借由这个PBD空壳,PowerBuilder 才行使用DLL程式;第二个问是就是使用PB Extension的步骤为何?其实使用PB Extension很简单,只要把它当作是一般的PowerBuilder程式物件来用就可以
了,下面是它的使用步骤: 
1.将产生好的.PBD档案加到你的PowerScript Target,也就是把这个PBD档案加到Library Search Path中。 
2.将PB extension DLL档案拷贝到开发程式目录下面

3.使用PowreScript语法呼叫物件的函数假设现在有一个PB Extension档案加到Library Search Path中,在这个PBD里面有一个NVO物件叫「SimpleExt」,在该物件有一个「hello()」函数,在PowerBuilder的程式中,就会这样子写: 
SimpleExt ext 
ext = create SimpleExt 
String str 
Str = ext.hello( “Hello, what’s your name? ”) 
Messagebox( “hello ”, str); 
下面是这次呼叫PB Extension程式的流程示意图:


此主题相关图片如下:

在一开始的时后提到,PBNI提供了两扇对外的门户,一个是可以将C或C++的程式转成PB Extensions,也就是转成PBD的方式;另一个对外的门户是可以在C++程式中「内嵌PBVM」。关于第一种方法在上面介绍过了,而接下来就是要介绍第二种对外的门户,内嵌PBVM。 

相信读者在开发其他的语言程式时,一定时常会有一个希望:「啊,如果我的Java 程式可以使用DataWindow物件就好了。 」 ;或是「上次的专案我已经用PowerBuilder开发好一些模组,在这次的C++专案中,真不想再写一次。 
如果读者有这种需求,这时后最好的方式,就是使用「内嵌PBVM」的作法,透过这个PBNI的技术,C++的程式也可以顺利的呼叫PowerBuilder开发好的物件,具体的开发方式为: 
1.在C++程式中载入PBVM。 
2.在C++程式中利用IPB_VM介面来取得C++和PB的连结。 
3.建立该PBL或是PBD的Library Session(其实也是透过IPB_Session介面)。 
4.在C++里面建立这个NVO物件的实体。 
5.呼叫这个NVO物件的Function。 


举例来说,我有一个「trypbni.pbl」,在这个PBL档案中,有一个NVO物件叫作「n_ben」物件,在该物件中有一个foo()函数,现在在开发一个C++的程式时, 就可以把PBVM给内嵌到C++程式中,并且在C++程式呼叫这个n_ben.foo()函数,相关的的部份程式码如下: 
trypbni.cpp 
1. int main(int argc, char* argv[]) 
2. { 
3. HINSTANCE hinst = LoadLibrary("pbvm90.dll"); 
4. P_PB_GetVM getvm = (P_PB_GetVM)GetProcAddress(hinst, "PB_GetVM"); 
5. IPB_VM* vm = NULL; 
6. getvm(&vm); 
7. static const char *liblist[] = { "trypbni.pbl" }; 
8. IPB_Session* session = NULL; 
9. vm->CreateSession("trypbni", liblist, 1, &session); 
10. pbgroup group = session->FindGroup("n_ben", pbgroup_usero b j e c t); 
11. pbclass clz = session->FindClass(group, "n_ben"); 
12. pbmethodID mid = session->GetMethodID(clz, "foo", PBRT_FUNCTION, "IS"); 
13. pbo b j e c t obj = session->NewObject(clz); 
14. PBCallInfo ci; 
15. session->InitCallInfo(clz, mid, &ci); 
16. ci.pArgs->GetAt(0)->SetString("Calling PowerScript from C++"); 
17. session->InvokeObjectFunction(obj, mid, &ci); 
18. session->FreeCallInfo(&ci); 

19. session->Release(); 
20. FreeLibrary(hinst); 
21. return 0; 
22. } 
读者可以在第三行发现在C++要「内嵌」PBVM,会使用LoadLibrary()这个函数把pbvm90.dll加到C++程式中,在第七行指定要使用trypbni.pbl;在第十一行找到n_ben物件;在第十二行呼叫n_ben.foo()函数。在完成上面的程式后,就可以把这支C++程式编辑,变成一支可以呼叫PowerBuilder物件的程式。 如果是要在Java中呼叫PowerBuilder的程式呢?这比较麻烦一点,简单的来说,
还是要用到「内嵌」PBVM的技术,可是现在有一个问题,就是理论上,Java 是没有办法内嵌PowerBuilder的程式,如此一来,想要让Java直接呼叫或使用PowerBuilder的程式是有点难度的。关于这一点,其实可以用Java呼叫C++程式的技术「Java JNI」方式达到我们的目地,也就是说,首先可以先用C++写一个呼叫PowerBuilder的DLL程式,当然,这支程式一定是用PBNI的「内嵌」 PBVM技术做出该程式,接着再用「JNI」的方式,让Java呼叫C++的DLL程式。


结论


PowerBuilder Native Interface,这个PowerBuilder9功能强大的新程式设计介面, 大大的改变了世人对PowerBuilder的认知,透过PBNI的支援,可将原来PowerBuilder应用程式的功能,延伸到C++及Java应用程式中,为这些程式开启新的世界和市场。尤其是在这个资讯以倍速成长的竞争环境下,企业如何能一方面保有原来的投资,另一方面又可让系统更具扩充性和延展性,而且兼具高效能和高生产力,相信市场领导开发工具—PowerBuilder已经为这样子的需求做了最好的解释。

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
PB9以上的PB版本适用。 使用只有三个函数,很简单。 这是用PBNI编写的控件,VC6的IDE,VC2003的编译器编译,UNICODE版本。 三个函数的使用说明: of_addtip(unsignedlong winhandle, string tip) 添加一个tip。这是必须首先调用的函数,即:在使用其他两个函数前,必须先调用此函数。 winhandle:窗口句柄,操作tip的窗口的句柄,tip的图标即用此窗口的图标。 tip: tip的字符内容。 of_modifytip(string title, string info) 修改tip的内容,并且动态将修改的内容立即显示。参数的含义明显。 of_deletetip() 删除tip. 对tip图标上的鼠标操作的处理: 原理是:图标上的所有windows消息,都会转发到参数winhandle所代表的窗口,作为该窗口的WM_USER消息,即触发该窗口的pbm_custom01事件。pbm_custom01事件的lparm参数指定tip图标的windows消息号。 具体作法:在参数winhandle所代表的窗口的pbm_custom01事件中写代码,检测lparm的值,以判断鼠标进行了哪种操作,如:lparm为517(WM_RBUTTONUP),表示鼠标弹起。 pbm_custom01中的示例代码: //start CONSTANT long WM_MOUSEMOVE = 512 CONSTANT long WM_LBUTTONDOWN = 513 CONSTANT long WM_LBUTTONUP = 514 CONSTANT long WM_LBUTTONDBLCLK = 515 CONSTANT long WM_RBUTTONDOWN = 516 CONSTANT long WM_RBUTTONUP = 517 CONSTANT long WM_RBUTTONDBLCLK = 518 CONSTANT long WM_USER = 1024 CONSTANT long NIN_BALLOONSHOW =(WM_USER + 2) CONSTANT long NIN_BALLOONHIDE =(WM_USER + 3) CONSTANT long NIN_BALLOONTIMEOUT =(WM_USER + 4) CONSTANT long NIN_BALLOONUSERCLICK =(WM_USER + 5) m_base lm_menu CHOOSE CASE lparam CASE WM_MOUSEMOVE // CASE WM_LBUTTONDOWN //鼠标左键按下 CASE WM_LBUTTONUP //鼠标左键弹起 CASE WM_LBUTTONDBLCLK //鼠标左键双击 CASE WM_RBUTTONDOWN // SetFocus ( this ) lm_menu = CREATE m_base lm_menu.PopMenu(PointerX(),PointerY()) DESTROY lm_menu CASE WM_RBUTTONUP // CASE WM_RBUTTONDBLCLK // CASE NIN_BALLOONSHOW // CASE NIN_BALLOONHIDE // CASE NIN_BALLOONTIMEOUT // CASE NIN_BALLOONUSERCLICK // CASE ELSE Beep(0) END CHOOSE //end 注意事项: 经测试,本PBNI如果与其他非UNICODE版的PBNI同用时,会显示乱码。无解。 如有BUG,敬请及时告知。 本控件的讨论贴: http://topic.csdn.net/u/20090510/20/14a57e09-02ca-44df-8f9b-e3811678360d.html

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值