SolidWorks二次开发语法技巧及基础

语法:

//变量
'HRESULT 
接口返回值 用于异常调用时判断 
本质 typedef LONG HRESULT; 32位
S_OK
S_FALSE

'OLECHAR
特定平台上表示文本数据
win32内 定义为 wchar_t 16 或32 位 char 8位
一般定义为 OLECHAR * 形式的参数

'BSTR
包含长度的OLECHAR字符串,提高速度
typedef OLECHAR BSTR;
属性
    是一个指针类型数据,指向数组第一个字符,长度前缀以整数形式恰好存储在数组第一个字符之前
    BSTR是一个指向包含长度前缀的OLECHAR字符数组的指针
    字符数组以NULL字符作为结尾
    长度前缀以字节为单位而不是字符,并且不包括NULL终止符

'CComBSTR ''ATL工具类
是一个ATL工具类,封装了BSTR,文件atlbase.h中包含了CComBSTR的定义,经常使用<向导中已经加入>
这个类维护的唯一状态是一个BSTR类型的公有成员变量m_str
class CComBSTR
{
    public:
    BSTR m_str;
}
CComBSTR的初始化--有8个构造函数
直接初始化: CComBSTR name("Hello SolidWorks");
调用CComBSTR的LoadString函数,二次开发中使用较多
CComBSTR title;
title.LoadString(IDS_TOOLBAR_TITLE);//IDS_TOOLBAR_TITLE是在资源文件中定义的一个字符串

'VARIANT
通用数据类型,常用于接口方法参数,接口方法返回值
既包含数据又包含数据的类型可以实现各种不同的自动化传输
定义 太长了... 不写了----在第16页,内有类型代号
C结构,包含一个类型成员vt,用于表示该VARIANT是什么类型,后面的union保存变量值 先判断再取值
VARIANT va;
::Variantlnit(&va);        //初始化,该函数为COM库函数
int a=2002;
va.vt=VT_I4;            //指明long数据类型
va.1Val=a;                //赋值

'SafeArray
安全数组,用于OLE自动化中的数组型参数传递,在SolidWorks API中也有广泛的应用,用于传递数组型参数
如:零件文档对象中的设置零件材质的函数
  PartDoc::SetMaterialPropertyValues(MaterialPropertyValues)
  MaterialPropertyValues即是SafeArray,传递描述材质的9个值(r,g,b,Ambient,Diffuse,Specular,Shininess,Transparency,Emission)
SafeArray实质上:将通常的数组添加描述符,说明维度,长度,边界,以及元素类型---一般不单用,包装于VARIANT类型中 VARIANT的vt值为VT_ARRAY|*则封装了SafeArray,SafeArray中可以放在VARIANT的任何类型包括VARIANT

'CComPtr '智能指针
//CComPtr被称为智能指针,是ATL提供的一个模版类,能够从语法上自动完成AddRef和Release。(源代码在atlbase.h中)
//CComPtr的用法很简单,以IHello*为例,将程序中所有接口指针类型(除了参数),都使用CComPtr<IHello> 代替即可。即程序中除了参数之外,再也不要使用IHello*,全部以CComPtr<IHello>代替。
//CComPtr的用法和普通COM指针几乎一样,另外使用中有以下几点需要注意。
//1. CComPtr已经保证了AddRef和Release的正确调用,所以不需要,也不能够再调用AddRef和Release。
//2. 如果要释放一个智能指针,直接给它赋NULL值即可。
//3. CComPtr本身析构的时候会释放COM指针。
//4. 当对CComPtr使用&运算符(取指针地址)的时候,要确保CComPtr为NUL。(因为通过CComPtr的地址对CComPtr赋值时,不会自动调用AddRef,若不为NULL,则前面的指针不能释放,CComPtr会使用assert报警)
//以刚才的程序为例:
//void SomeApp( IHello * pHello )
//{
//CComPtr<IHello> pCopy = pHello;
//OtherApp();
//pCopy->Hello();
//}
//由于pCopy是一个局部的对象,所以即使OtherApp()抛出异常,pCopy也会被析构,指针能够被释放。
//如果不想在程序临近发布前,还因为COM指针的引用计数造成崩溃的话,就牢记这一点吧:程序中除了参数之外,不要直接使用COM指针类型,一定要全部以CComPtr<IXXX>代替。

例子:
CComPtr<IModelDoc2>pDoc;            //ModelDoc2所有文档类的父类,IModelDoc2 ModelDoc2的接口指针?
CComPtr<ISelectMgr>pSleMgr;            //选择管理器的接口?
CComPtr<IDispatch>pDisp;            //接口指针
psldWorks->get_IActiveDoc2(&pDoc);    //得到当前活动窗口 引用形参...
pDoc->get_ISelectionManager(&pSleMgr);//得到选择管理器
pSleMgr->GetSelectedObject5(1,&pDisp);//得到用户选择的对象
IFeaturePtr pFeat(pDisp);            //默认为构造一个特征对象
pDisp=NULL;
pFeat->GetSpecificFeature(&pDisp);
ISketchPtr pSketch(pDisp);            //得到该特征的草图
pDisp=NULL;
VARIANT vSplines;
pSketch->GetSplinesInterpolate(&vSplines);//得到样条曲线的控制点
SafeArray arrSplines(vSplines);              //定义一个安全数组
for(int idx=0;idx<arrSplines.getSize();idx++)
{
    ::AfxTrace("arrSplines(%d)=%f\n",idx,arrSplines[idx]); //输出数据
}


@接口指针
往往采用接口指针的方法,获得接口指针后调用接口中的方法进行开发

直接定义:IDispatch * pDisp;//定义一个指向IDispatch接口的指针
ptr后缀:IDispatchPtr pDisp;//定义一个指向IDispatch接口的指针
LP开头 :LPModelDoc2 pModel=NULL;//定义一个指向IModelDoc2接口的指针
pSldWorks->get_IActiveDoc2(&pModel);    //通过ISldWorks获取接口指针
//调用接口实现功能
...
pModel->Release();//释放接口

以上三种方法必须手动释放否则会一直占用资源,因而引入了智能指针CComPtr CComQIPtr
CComPtr是ATL提供的只能COM接口指针类
使用COM组件过程中,需要严格使用AddRef与Release,否则会导致COM组件不被释放而占用系统资源

CComPtr功能:
    1构造函数多
    2自动释放
    3异常时也能释放
    4赋值期间,在改写被封装的的接口指针之前释放他
    5赋值期间,对接收到的接口指针调用AddRef
    6可以在许多(不是全部)通过使用原始接口指针的场合使用

    CComPtr 可以为指定的接口指针类型创建实例
例子:
    pSldWorks->get_IActiveDoc2(&pModel);//获取接口指针
    ...//调用接口的方法实现功能
    CComQIPtr
    更加智能的指针类完成前者所有功能以及更多功能
    当把一个与智能指针不同类型的接口指针赋值给CComQIPtr实例时,这个类会对输入的接口调用QueryInterface
例子:
    CComPtr<IUnknown>punk /*初始化某一接口*/
    CComQIPtr<INamedObject>pno=puk;//此处会调用punk->QueryInterface(IID_INameObject...)

获取接口的方法
方式:
1 QueryInterface方法 '限于需要访问的借口是在同一个COM对象内,对于不在同一个COM对象内的接口,需要其他方法
    是COM组件IUnknown接口中的三个基本函数之一,为COM组件提供接口相互访问的功能,通过它可以访问组件内的任意接口
    {
    //通过SolidWorks获取当前活动文档的ModelDoc
    CComPtr<IModelDoc2>pModel;//定义IModelDoc2类型指针
    retval=m_ISldWorks->get_IActiveDoc2(&pModel);//m_ISldWorks在入口函数处初始化
    ASSERT(pModel);//测试pModel是否为NULL 为NULL则是假 则跳出 
    if(retval!=S_OK)
        return;
    CComPtr<IPartDoc>pPartDoc;//定义IPartDoc类型的智能指针
    retval=m_ModelDoc->QueryInterface(IID_IPartDoc,(LPVOID*)&pPartDoc);//LPVOID是一个没有类型的指针,也就是说你可以将任意类型的指针赋值给LPVOID类型的变量(一般作为参数传递),然后在使用的时候再转换回来。 
    ASSERT(pPartDoc);
    if(retval!=S_OK)
        return;
    }
    retval 
    HRESULT/*接口返回值类型*/  f([in] long n1, [out, retval]   long   *pn);
    [in]表示参数方向是输入;[out]表示参数方向是输出;[out,retval]表示参数方向是输出,同时可以作为函数运算结果的返回值。
    一个函数中,可以有多个[in]、[out],但[retval]只能有一个,并且要和[out]组合后在最后一个位置。
    retval 表示  *pn 是返回值   
    HRESULT f([out,retval]   long   *pn) 的含义可以理解为该接口有如下形式的方法: long* f()   
    如果去掉它,则类似的表示:     void   f(   long*   pn)   
    out是输出参数,加上retval表示同时作为函数的返回值返回。这是为了支持VB、ASP等语言。因为在C++中可以以参数引用的方式取得返回指,而VB、asp中没有引用,没有指针的概念。

    assert( <expression> ); DEBUG运行时 测试函数 如果表达式的值为假,整个程序将退出,并输出一条错误信息,测试表达式
    SUCCEEDED(hr);判断返回的HRESULT类型的hr是否为S_OK
    assert(pvTo   !=   NULL   &&   pvFrom   !=   NULL);//当为假时就会报错 
    当expression结果为“假”时,会在stderr中输出这条语句所在的文件名和行号,以及这条表达式。这只在调试版本中起作用,在Release版本中不会产生任何代码。 
    #ifdef   DEBUG
    ...//测试代码
    #endif
    assert并不是一个仓促拼凑起来的宏,为了不在程序的交付版本和调试版本之间引起重要的差别,需要对其进行仔细的定义。宏assert不应该弄乱内存,不应该对未初始化的数据进行初始化,
    即它不应该产主其他的副作用。正是因为要求程序的调试版本和交付版本行为完全相同,所以才不把assert作为函数,而把它作为宏。如果把assert作为函数的话,其调用就会引起不期望的内存或代码的兑换。
    要记住,使用assert的程序员是把它看成一个在任何系统状态下都可以安全使用的无害检测手段 

    接口查询QueryInterface
    原始定义:QueryInterface(const IID& iid, void ** ppv) =0;  
    interface IUnknown
    {
        virtual HRESULT __stdcall 
        QueryInterface(const IID& iid, void    ** ppv) =0; //原始定义           
        virtual     ULONG __stdcall     AddRef() =0 ;
        virtual     ULONG __stdcall     Release() =0 ;
    };
    1.作用 
    看命名就可以猜这个方法是用来查询组件是否支持某个特定接口。若支持泽返回指向此接口的指针。
    2.参数 
    QueryInterface方法有两个参数,
    第一个为接口标识符,简称IID,全名(Interface Identifier),现在可以只理解为一个常量 常引用形参
    第二个参数即为返回的接口指针 将查的指针赋给第二个参数
    返回值为HRESULT 返回S_OK S_FAULT 也可用SUCCEEDED()测试 若hr的值为S_OK则为真
    3.使用方法
    我们先来看示例代码

    定义一个继承自IUnknown接口的IX

    interface IX : IUnknown
    {
        virtual void __stdcall     Fx() = 0 ;
    } ;
    
    若CA实现了该IX接口,则使用如下

    static const IID IID_IX = {0x32bb8320, 0xb41b, 0x11cf,{0xa6, 0xbb, 0x0, 0x80, 0xc7, 0xb2, 0xd6, 0x82}} ;
    void foo(IUnknown* pI)
    {
        IX* pIX=NULL;
        HRESULT hr=pI->QueryInterface(IID_IX,(void    **)&pIX);//HRESULT存储是否调用成功的变量 
        if    (SUCCEEDED(hr))
        {
           pIX->Fx();
        }
    }

    HRESULT 
    HRESULT是一个查询结果状态集合,并非true或false可以表示的,所以用SUCCEEDED方法来判断查询结果是否成功,失败的话,接口指针为NULL.

    4.QueryInterface的实现 
    CA继承了IX,由于IX继承了IUnknown接口,所以CA也必须实现IUnknown接口成员,我们重新来看一下QueryInterface方法的参数
    virtual HRESULT __stdcall QueryInterface(const IID& iid, void ** ppv) =0; 
    该方法会根据传输不同iid,而返回相对应的接口指针,为了演示,我们再定义一个接口IY,CA实现IX和IY,
    如下实现
    HRESULT __stdcall CA::QueryInterface(const IID& iid, void    ** ppv)
    {   
        if (iid == IID_IUnknown)
        {
            *ppv = static_cast    <IX*>(this    ) ;
        }
        else if (iid == IID_IX)
        {
            *ppv = static_cast    <IX*>(this    ) ;
        }
        else if     (iid == IID_IY)
        {
            *ppv = static_cast    <IY*>(this    ) ;
        }
        else    
        {         
            *ppv = NULL ;
            return E_NOINTERFACE;
        }
        return    S_OK ;
    }
    
    是不是很简单?,获取到以后用static_cast 来转换类型。IID_IUnknown,S_OK,E_NOINTERFACE等都是内置的状态码

2层级访问
SolidWorks API对象访问也是一个从上到下依次访问的过程,为了访问该接口对象先获得高一级的接口对象
要获得当前活动文档中的选择管理器指针--API结构图--SldWorks->ModelDoc->SelectionMgr
实现代码
{
//首先必须获得 SolidWorks应用程序对象--m_iSldworks--在用户加载插件时完成初始化的
//通过SldWorks获取当前活动文档的ModelDoc
CComPtr<IModelDoc2>pModel;
retval=m_iSldWorks->get_IActiveDoc2(&pModel);//m_iSldWorks对象的get_IActiveDoc2函数获得当前活动文档接口后
ASSERT(pModel);                                 //m_iSldWorks在入口函数处初始化 retval是HRESULT类型的数据
if(retval!=S_OK)
return;
//通过IModelDoc2获取ISelectionMgr
CComPtr<ISelectionMgr>pSelMgr;
retval=pModel->get_ISelectionManager(&pSleMgr);//通过IModelDoc2接口中的get_ISelectionManager函数获得SelectionMgr对象指针
ASSERT(pSelMgr);
if(retval!=S_OK)
return;
}
获取接口是一个由上到下依次访问的过程

间接访问
如:对于用户选择的元素对象,可以先获取SelectionMgr对象,然后通过该对象中的GetSelectedObject获取被选择的元素,
在判断元素类型的基础上,通过引用获得目标对象,如获取一个用户选择的面
实现:
{
    //通过SldWorks获取当前活动文档的ModelDoc
    CComPtr<IModelDoc2>pModel;
    Retval=m_iSldWorks->get_IActiveDoc2(&pModel);//m_iSliWorks在入口函数处初始化
    ASSERT(pModel);
    if(retval!=S_OK)
        return;
    //通过IModelDoc2获取ISelectionMgr
    CComPtr<ISelectionMgr>pSelMgr;
    retval=pModel->get_ISelectionManager(&pSelMgr);
    ASSERT(pSelMgr);
    if(retval!=S_OK)
    return;
}

很多时候上述三种方法结合使用,而且SolidWorks API中一个对象往往可以通过多种方法获得
若在一个COM对象内可以用QueryInterface查询
获得面积 Face2::GetArea函数需先获得Face对象
PartDoc-Body2-Body2::GetFirstFace Body2::GetNextFace

假设用户选择一个面,可以通过SelectionMgr::GetSelectedObject函数获得选择对象,通过引用得到选择的面对象

知道Face对象名称基础上,通过PartDoc中的GetEntityByName函数获得面对象

使用接口
SolidWorks API接口的基本形式为 return-value object::Function(Parameters) //parameters参量

返回类型

@一般类型为bool long等

如:ModelDocExtension::SelectByID含有8个参数,返回一个bool类型的值
  Boolean ModelDocExtension::SelectByID(BSTR Name,BSTR Type,double x,double y,double z,VARIANT_BOOL Append,long Mark,LPCALLOUT Callout)

  COM中调用如下
  VARIANT_BOOL Result=FALSE;
HRESULT hres=ModelDocExtensionObj->SelectByID(_T("Pointl"),_T("SKETCHPOINT"),.2,.3,0,0,0,0,&result);

@返回值为接口

OLE模式-返回一个IDispatch类型类型的接口指针,然后通过IDispatch接口指针获取对应的SolidWorks接口指针
COM模式-直接返回对应的SolidWorks接口指针,因此,在SolidWorks API 中某些功能会有两个函数

目前基于COM方式,OLE就不写了...真懒 - -
//获取第一个面接口指针
HRESULT Body2::IGetFirstFace(LPFACE * retval)
//使用
LPFACE2 FaceObj=NULL;
HRESULT hres=BodyObj->IGetFirstFace(&FaceObj);

@返回值为数组

此时OLE与COM大大不同

COM模式下,使用指针作为参数或者返回值
如:ModelDocExtension::GetMassProperties函数
  HRESULT ModelDocExtension::IGetMassProperties(longAccuracy,longstatus,double * mPropsData)

  BOOL
FALSE = 0

TRUE  = 1

VARIANT_BOOL
VARIANT_FALSE =  0

VARIANT_TRUE  = -1

com技术,运用最多的API,dll插件形式

VBA VB C++ 

除非效率瓶颈在于软件本身,或者设计出现失误,一般而言,dll要比基于OLE的exe快速

VB 定义对象,然后让basic OLE做其他事
C++ 利用类向导生成头文件中的类,通过导入SolidWorks类型库生成应用程序
C 从类型库中确定程序的ID后可设置所调用的应用程序

SolidWorks API 使用面向对象的方法 API中所有函数都适用于对象的方法或属性

另外所有SolidWorks的接口都是基于MFC的

SolidWorks软件通过标准的COM对象显示其API功能,对于OLE使用Idispatch来显示API

COM实现的程序的特点:
是应用程序直接访问基本对象组功能,提高了性能
对每个API调用返回一个HERSULT值
对所有插件DLL推荐COM接口,对于单独的可执行程序(*.exe)也可以使用COM接口除非方法或属性传递一个数组...此时可使用一个VARIANT来压缩数组

访问SolidWorks对象及属性
先获得对象,再调用方法
获得一个面face的area Face2::GetArea

获得Face的方式 
1 PartDoc::Body 获得Body2对象,然后用Body2::GetFirstFace Face2::GetNextFace方法遍历面
2 使用SelectionMgr::GetSelectedObject4从当先所选项目中获得Face2对象   ----或许可以用于后期修改
3 使用PartDoc::GetEntityByName,通过Face2的名称获得Face2对象.获得了Face2对象,便可以访问Face2类中的属性和方法
例如通过Face2::GetEdgeCount获得面的边的数目,或者通过Face2::Normal属性获得法矢量

注意:一些方法或属性要求有所选的项目如:/*组装*/AssemblyDoc::OpenCompFile,要调用它,必须有AssemblyDoc对象和所选的一个零部件

  用户交互选择
  DodelDocExtension::SelectByID 程序化选择
  访问从ACIS SAT文件导入的属性 导入后即变为SolidWorks的属性了,可用标准的属性访问访问这些属性和方法
  属性名定义如下:
    XLTR_ATTRIBUTE_DEFINITION_DOUBLE_TYPE for ATTRIB_GEN_REAL
  ...
  GEN_VECTOR
  每个属性定义有两个参数
  值:该参数是swParamTypeString 类型
    SwParamTypeDouble swParamTypeDouble swParamTypeString swParamTypeInteger 或 swParamTypeDVector是哪种类型取决于以前定义的属性
布尔值
BOOL
FALSE=0
TRUE=1
VARIANT_BOOL
VARIANT_FALSE=0
VARIANT_TRUE=-1

FALSE总是零

SolidWorks使用API函数的预定义文件
用于C++的
安装目录\samples\appcomm\swconst.h
安装目录\samples\appcomm\swoptions.h
用于vb的,不用...

事件
事件或通知使用 IConnectionPoint机制  安装目录下\samples子目录下有示范代码,摘自控制代码的适当部分 ,这些机制用于DLLs和EXEs
为了接收事件,dll应用程序必须通过对象类型注册通知,每个特殊的对象实例必须注册
如:为接受PartDoc事件,运行程序需要注册现有的每一个PartDoc对象(当前打开的零件文件)也需要注册任何将来生成的PartDoc对象(随后打开或生成的零件文件)
--具体可参阅comuserdll示范工程,提供了事件管理的范例 
除非另有说明,消息函数应该返回S_OK
支持信息的对象有:SldWorks PartDoc AssemblyDoc DrawingDoc ModelView 和 FeatMgrView

帮助函数
为了使用方便SolidWorks提供了一些帮助函数,他们不受SolidWorks API约束,可用于一般的数学,向量,矩阵和数组的操作
位置: 安装目录\samples\appcomm
可以任意使用 它们提供了SLDMG.DLL的类信息
为了使用它们,可用SLDMG.LIB连接工程,对Unicode而言,名称分别是SLDMUGU.DLL和SLDMGU.DLL
也可以在安装目录下找到这些文件,包含MGDEG.H文件到你的代码中和你希望参考的其他需要*.h文件的地方

其他提示:
    获得头文件--为获得任何工程的头文件,在SolidWorks中生成宏也许是有帮助的
    API限制,对SolidWorks内核的要求和限制同时适于API
    检查返回值是否为空或NULL
    执行程序指导--一般OLE运用程序的调用者有责任分配和释放内存,这包括通过SolidWorks函数返回类型

从C++双精度数组中提取整数
一些API返回整数,改整数被打包成双精度数组
建议使用联合...//  联合表示几个变量公用一个内存位置, 在不同的时间保存不同的数据类型和不同长度的变量。
union PackedData_
{
    double doubleValue;
    int intValues[2];
}PackedData;
int iValue1, iValue2;
为了访问赋给doubleValue的数组元素数据,使用intValues来访问整数...
PackedData.doubleValue=dArray[0];
iValue1=PackedData.intValues[0];
iValue2=PackedData.intValues[1];

接口指针
每个自动返回接口指针的SolidWorks API方法使参考接口指针数自动增加一
COM实现程序:
可以调用返回接口指针的SolidWorks API,然后可以随心所欲的使用该指针,但是也有义务释放它
C++ 获得接口指针示范:
{
    LPMODELDOC m_ModelDoc = NULL;
    HRESULT res = UserApp()->getSWApp()->get_IActiveDoc( &m_ModelDoc);//获得IModelDoc指针
    if( m_ModelDoc == NULL)
    res=m_ModelDoc->QueryInterface(IID_IPartDoc,(LPVOID *) &m_PartDoc);//获得IPartDoc指针
    ASSERT(res==S_OK);
    ...//在代码中使用接口
    m_ModelDoc->Release();//释放IModelDoc指针
    m_PartDoc->Release();//释放IPartDoc指针

}

调用的实现(Dispatch implementations)
Dispatch//调用
implementation//实现

接口指针的释放隐藏在调用对象的销毁里(例如 ImodelDoc Iface 等等)
这意味着附加接口指针到一个以上的调用对象将导致每个对象执行释放,因为超出了了范围当接口指针返回时,参考数仅增加了一次,
如果附加指针到一个意向的对象时,为了避免这个问题必须手工增加参考数 (pdisp->AddRef();)
示范如何处理接口指针的参考数

接口实现范例
{
    LPDISPATCH modDisp;
    modDisp=UserApp->getSWApp()->GetActiveDoc();//获得家伙文件接口的指针
    //modDisp的参考数自动增加一
        if(modDisp==NULL)
            return;
    IModelDoc m_ModelDoc( modDisp ); //连接到IModelDoc对象
    IPartDoc m_PartDoc(modDisp);//连接到IPartDoc
    modDisp->AddRef();
    //手动增加modDisp的参考数,因为这是第二次使用modDisp
    ...//在代码中适用对象
}//变量超出范围 调用IModelDoc和IPartDoc销毁,这将减少modDisp的参考数为2
 

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

qq_38220914

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值