第13章 动态数据交换和对象链接与嵌入

剪贴板

  在Windows操作系统中大量使用剪贴板使用户能够在同一应用程序之中或不同应用程序之间传输数据。
13.1.1 剪贴板数据格式
  当用户选中某些数据并对它进行复制操作时,Windows把这些数据从选中区中读出并传送到剪贴板上:当用户使用粘贴命令时,该数据从剪贴板上读出并粘贴到插入位置。剪贴板倾向于对用户保持透明,用户甚至不会意识到它的存在。
注意:剪贴板只是一个简单的数据暂存场所,这些数据需要用户初始化并完成传输任务。
Windows系统定义了几种标准的剪贴板数据格式,这些格式如表所示。

格 式
说 明
CF_BITMAP与设备有关的位图格式
CF_DIB与设备无关的位图内存块
CF_DIF包含数据交换格式的全局内存块
CF_DSPBITMAP和一种私有格式相关联的位图显示格式
CF_DSPENHAMETAFILE和一种私有格式相关联的增强型图元显示格式
CF_DSPTEXT和一种私有格式相关联的文本显示格式
CF_METAFILEPICT图元文件格式
CF_OEMTEXTOEM字符集的文本格式
CF_OWNERDISPLAY自画格式
CF_PALETTE调色板句柄,定义位图所使用的调色板
CF_SYLK包含符号链接格式的全局内存块
CF_TEXTNULL,结尾的ASCII字符集的文本格式
CF_WAVE波形音频
CF_TIFF包含符号图像格式数据的全局内存块

在任何时候,只有一个程序可以打开剪贴板。函数OpenClipboard用于打开一个剪贴板,其函数原型定义如下:
BOOL OpenClipboard(
  HWND hWndNewOwner //窗口句柄
);
当其他程序正在使用剪贴板时,函数OpenClipboard将返回一个FALSE值。关闭剪贴板可以通过调用函数CloseClipboard来实现,这个函数的原型定义如下所示:
BOOL CloseClipboard(VOID);
  如果要把文本复制到剪贴板上,首先必须使用GlobalAlloc函数分配所需的全局内存块,这个全局内存块是可移动的,剪贴板中的文本数据就记录在其中。GlobalAlloc函数成功调用后会返回一个标识该内存块的句柄,GlobalAlloc函数原型定义如下:
HGLOBAL GlobalAlloc(
  UFNT uFlags,//内存分配属性
  DWORD dwBytes //内存块分配的大小
);
分配好内存块后,就可以调用函数GlobalLock锁定这个内存块并得到指向这个内存块的指针。函数GlobalLock的原型定义如下:
LPVOID GlobalLock(
  HGLOBAL hMem //内存块旬柄
);
有了指向内存块的指针就可以通过多种手段向该内存块中复制数据了。函数SetClipboardData可以实现将数据复制到剪贴板上。函数SetClipboardData的原型定义如下:
HANDLE SetClipboardData(
  UINT uFormat, //剪贴板格式
  HANDLE hMem //存放数据的内存块句柄
);
  当打开剪贴板并把数据传送给它时,必须先调用 EmptyClipboard函数通知 Windows系统释放或删除剪贴板上的内容,还不能在现有的剪贴内容中附加其他内容。 
  因此,剪贴板每次只能保持一个数据项。不过可以在打开和关闭剪贴板之间,多次调用SetClipboardData函数来设置剪贴板中的数据,并且每次可以指定不同的数据格式。
  获取剪贴板上的文本是把文本复制到剪贴板的逆过程,但它的操作过程相对复杂一些。首先,必须判断剪贴板上的数据是否为文本内容,这可以通过调用函数IsClipboardFormatAvailable来实观。如果剪贴板上的数据为CF_TEXT,则该函数的返回值为TRUE,否则为FALSE,这个函数的原型定义如下:
BOOL IsClipboardFormatAvailable(
  UINT format //剪贴板格式
);
如果剪贴板上的数据格式是文本格式,则继续调用函数OpenClipboard打什剪贴权,然后调用函数GetClipboardData获得剪贴板上的数据。函数GetClipboardData的原型定义如下:
HANDLE GetClipboardData(
  UINT uFormat //剪贴板数据格式
);
13.1.2 剪贴板应用实例
下面是一个在程序中操纵剪贴板的例子。
在例的消息处理函数中,首先通过调用函数setClipboardViewer把程序的主窗口加入到剪贴板例览器链表中。函数SetClipboardViewer的原型定义如下:
HWND SetClipboardViewer(
  HWND hWndNewViewer //窗口句柄
);
如果希望从剪贴板测览器链表中删除某一个窗口,可以调用函数 ChangeClipboardChain来实现,这个函数的原型定义如下:
BOOL ChangeClipboardChain(
  HWND hWndRemove,//清除的窗口句柄
  HWND hwndNewNext//链表中的下一个窗口句柄
);
  剪贴板链表中的元素发生变化时将触发WM-CHANGECBCHAIN消息,在WM_CHANGECBCHAIN消息的wParam参数中记录了当前正被清除的窗口句柄,而lParam参数中记录了链表中的下一个窗口的句柄。
  当应用程序的主窗口进行重绘时,调用函数 OpenClipboard打开当前正在使用的剪贴板,然后通过函数GetClipboardData获取剪贴板中的数据。获取的数据保存在由函数GlobalLock锁定的内存块中,然后再通过函数DrawText就可以把剪贴板中的数据显示在应用程序的主窗口中。
#include <windows.h> 
static char szAppName[] = "剪贴板"; 
HWND hwnd; 
//函数声明 
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; 
BOOL MyRegisterClass(HINSTANCE hInstance); 
BOOL InitInstance(HINSTANCE hInstance,int iCmdShow); 
//函数:WinMain 
//作用:程序入口
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) 
{
  MSG msg; 
  if(!MyRegisterClass(hInstance)) 
  {
    return FALSE;
  } 

  if(!InitInstance(hInstance,iCmdShow)) 
  {
    return FALSE;
  }
  //消息循环
  while (GetMessage (&msg, NULL, 0, 0))
  {
    TranslateMessage (&msg) ;
    DispatchMessage (&msg) ;
  }
  return msg.wParam;
}
//函数:WndProc 
//作用:消息处理 
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam) 

  static HWND hwndNextViewer; 
  HGLOBAL hGMem;//全局内存句柄 
  HDC hdc;//设备描述表句柄 
  PSTR pGMem;//全局内存指针 
  PAINTSTRUCT ps; 
  RECT rect; 
  switch (iMsg) 
  { 
    case WM_CREATE://窗口创建 
    hwndNextViewer = SetClipboardViewer (hwnd); 
    return 0;

    case WM_CHANGECBCHAIN://剪贴板链表发生改变 
    if ((HWND)wParam == hwndNextViewer) 
      hwndNextViewer = (HWND) lParam; 
    else 
      if (hwndNextViewer) 
        SendMessage (hwndNextViewer, iMsg, wParam, lParam); 
    return 0; 

    case WM_DRAWCLIPBOARD://更新对剪贴板数据的显示 
    if(hwndNextViewer) 
      SendMessage (hwndNextViewer, iMsg, wParam, lParam); 
    InvalidateRect (hwnd, NULL, TRUE); 
    return 0;

    case WM_PAINT://重绘窗口 
    hdc = BeginPaint (hwnd, &ps); 
    GetClientRect (hwnd, &rect);//获取客户区 
    OpenClipboard (hwnd);//打开剪贴板 
    hGMem = GetClipboardData (CF_TEXT);//获取剪贴板数据 
    if (hGMem != NULL) 
    { 
      pGMem = (PSTR) GlobalLock (hGMem);//锁定内存块 
      DrawText (hdc, pGMem, -1, &rect, DT_EXPANDTABS); 
      GlobalUnlock (hGMem);//解锁内存块 
    } 
    CloseClipboard ();//关闭剪贴板 
    EndPaint (hwnd, &ps); 
    return 0;

    case WM_PAINT://重绘窗口 
    hdc = BeginPaint (hwnd, &ps); 
    GetClientRect (hwnd, &rect);//获取客户区 
    OpenClipboard (hwnd);//打开剪贴板 
    hGMem = GetClipboardData (CF_TEXT);//获取剪贴板数据 
    if (hGMem != NULL) 
    { 
      pGMem = (PSTR) GlobalLock (hGMem);//锁定内存块 
      DrawText (hdc, pGMem, -1, &rect, DT_EXPANDTABS); 
      GlobalUnlock (hGMem);//解锁内存块 
    } 
    CloseClipboard ();//关闭剪贴板 
    EndPaint (hwnd, &ps); 
    return 0;

    case WM_DESTROY://销毁窗口 
    ChangeClipboardChain (hwnd, hwndNextViewer); 
    PostQuitMessage (0); 
    return 0; 
  } 
  return DefWindowProc (hwnd, iMsg, wParam, lParam); 

//函数:MyRegisterClass 
//作用:注册窗口类 
BOOL MyRegisterClass(HINSTANCE hInstance) 

  WNDCLASSEX wndclass; 
  wndclass.cbSize = sizeof (wndclass); 
  wndclass.style = CS_HREDRAW | CS_VREDRAW; 
  wndclass.lpfnWndProc = WndProc;
  wndclass.cbClsExtra = 0; 
  wndclass.cbWndExtra = 0; 
  wndclass.hInstance = hInstance; 
  wndclass.hIcon = NULL; 
  wndclass.hCursor = LoadCursor (NULL, IDC_ARROW); 
  wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH); 
  wndclass.lpszMenuName = NULL; 
  wndclass.lpszClassName = szAppName; 
  wndclass.hIconSm = LoadIcon (NULL, IDI_APPLICATION); 
  RegisterClassEx (&wndclass); 
  return TRUE; 
}
//函数:InitInstance 
//作用:创建窗口 
BOOL InitInstance(HINSTANCE hInstance,int iCmdShow) 

  hwnd = CreateWindow (szAppName, "剪贴板监控器", 
             WS_OVERLAPPEDWINDOW, 
             CW_USEDEFAULT, CW_USEDEFAULT, 
             CW_USEDEFAULT, CW_USEDEFAULT, 
             NULL, NULL, hInstance, NULL); 
  ShowWindow (hwnd, iCmdShow); 
  UpdateWindow (hwnd); 
  return TRUE; 
}
执行结果如图所示。

动态数据交换

  在应用程序之间经常要进行数据交换。动态数据交换(Dynamic Data Exchange简称DDE)使用普通的Windows通信联络系统进行内部进程之间的通信联系,对象链接与嵌入 ( Object Link and Embed简称 OLE)则是DDE的高级实现。DDE是基于消息的,而OLE则是基于文本的。
  DDE协议能够使一个应用程序和其他不同的应用程序之间交换数据,而且还可以利用Windows消息这一手段传递远程命令。Windows是基于消息传递系统建立起来的,利用消息传送数据。不过,在Windows操作系统中,消息只利用IParam和wParam两个参数来传送数据,如果有多于这两个参数的消息需要在不同的应用程序之间传送,那么这两个参数必须间接指向其他的数据块。 DDE协议指明了参数 IPat8111和 WPat8Ill两个参数如何通过全局原子和全局共享内存等手段来实现数据传递。
  全局原子实际上是一个字符串,在DDE协议中,这些字符串用于指明进行该数据交换的应用程序、要交换的数据特征和交换的数据内容。全局共享内存实际上是指一块由函数GlobalAlloc。分配的内存块。在DDE协议中,全局共享内存对象是用于不同应用程序之间传递大的数据结构。DDE中有明确的规则用于指定客户和服务器的不同责任,这些责任包括对全局原子以及共享内存对象的分配、重新分配以及删除等。
13.2.1动态数据交换的用途
  在大多数场合DDE被用来传递那些不需要用户不断干涉的数据流。用户只要建立原始的链路,然后让有关的应用程序接管过去,而不需要用户进一步的介入。
一般说来,DDE可以应用在以下场合:
  1、与实时数据相连:
  2、创建复合文本:
  3、访问不同类型的文本。 
13.2.2动态数据交换的基本概念
1.客户和服务器对话

  DDE需要两个应用程序参与一个对话以完成一个数据交换。使对话开始的应用程序被定义为DDE的客户,响应对话的应用程序被定义为DDE的服务器。客户可以从服务器中查询有关信息,也可以向服务器发送有关信息。客户与服务器的区别在于谁首先初始化DDE对话。
  原则上一个DDE会话发生在两个窗口之间。一个窗口对应于客户,另一个窗口对应于服务器,任何一个窗口都可以参与一个DDE会话,它可以是一个应用程序的主窗口,一个文本窗口,也可以是一个隐含窗口。
  一个DDE会话由参与会话的窗口的窗口句柄的有序对来进行标识。所以,任何一个窗口都不应该参与和其他窗口之间的多于一个的DDE会话过程。如果一个客户和一个服务器之间有多于一个的对话过程存在,它们就必须为每一个新的会话过程在一对一的基础之上提供一个附加的窗口。
2. DDE消息
  DDE消息是由应用程序名称、主题名称以及项名称组成的一个三级层次进行标识。
  一个特定的DDE会话唯一地被它的应用程序名称以及主题名称加以定义。初始化一个DDE会话时,DDE客户访问特定的DDE服务器的应用程序的名称和主题名称。
  DDE主题是一个包含多个数据项的数据类型。有效的主题以及项的选择由DDE服务器任意设置。对于一个字处理程序,主题可以是一个文本或者文件名,项可能是文件中的某些段落。
  因为客户与服务器窗口一起来识别一个DDE会话,所以在对话框中不能改变应用程序或主题。不过,在需要的情况下,可以任意地改变主题中的项。
  一个DDE的数据项是真正要发送的信息内容,数据项的值能够从客户传向服务器或者从服务器发送到客户。
  因为DDE是基于Windows中消息传送协议的基础之上的,因此它不需要任何特别的功能或者库。所有的对话都是由在客户或者服务器窗口之间传送某些定义的DDE消息来完成的。其中,比较重要的 DDE消息如表所示。

消 息
定义描述
WM_DDE_ACK对已经收到的消息的响应信号
WM_DDE_ADVISE客户向服务器发送一个请求,要求服务器无论何时,只要一个数据项改变就立即提供该数据或者向用户发送一个数据已经改变的通知
WM_DDE_DATA服务器向客户方发送数据项
WM_DDE_EXECUTE客户向服务器发送一个命令序列,服务器应当将此视为一系列命令而加以处理
WM_DDE_INITIAL客户向服务器方发送一个初始化消息
WM_DDE_POKE客户向服务器方发送一个数据项
WM_DDE_REQUEST客户请求服务器提供有关一个数据项
WM_DDE_TERMINATE客户终止对话过程
WM_DDE_UNADVISE客户终止数据链路的使用

一个DDE会话实际上可以分成下面几个子过程。
1、客户应用程序开始启动DDE会话,同时服务器应用程序响应这一过程。
2、会话过程以下述方法之一进行数据交换:
  客户请求有关消息,同时服务器响应:
  客户向服务器传送未请求的消息:
  任何时刻客户请求服务器,只要数据一改变就立刻把改变后的数据传送给客户:
  任何时刻客户请求服务器,只要数据一改变就立刻通知客户:
客户向服务器传送一系列命令码,使其在服务器上执行。
3、DDE会话结束。
3.DDE管理库
  DDE管理库 DDEML(Dynamic Data Exchange Management Library)通过把消息、原子管理和内存管理封装在同一个函数调用接口中,从而大大简化了DDE程序设计。在DDEML.H头文件中定义了30个函数和处理16种DDE事务的回调函数。尽管所有的DDEML事务仍然基于应用程序、主题和数据项,但是在DDEML中应用程序名被称为“服务器”名。使用DDLML的任何程序均须使用函数DdeInitialize在DDEML中注册自己。该函数得到一个应用程序实例识别符,它被用于所有其他DDEML函数。当程序结束时,还必须调用函数DdeUnInitialize来解除DDE所占的资源。
  在应用程序间传递数据时,程序不直接分配共享的全局内存,而是通过调用函数DdeCreateDataHandle从包含数据的缓冲区创建数据句柄。接收数据的程序通过该句柄,利用DdeAccessData或DdeGetData函数获得数据,调用DdeFreeDataHandle函数释放数据句柄。
13.2.3 动态数据交换的实现
下面是一个动态数据交换的实际例子。
  动态数据交换的实现在例中,首先调用函数 DdeInitialize初始化一个 DDE会话,这个函数的原型定义如下:
UINT DdeInitialize(
  LPDWORD pidInst, //应用程序句柄
  PFNCALLBACK pfnCallback, //DDE回调函数
  DWORD afCmd, //命令过滤条件
  DWORD ulRes //系统保留
);
  如果应用程序不能正确地创建立窗口,则调用DdeUnInitialize结束DDE,并且释放DDE占有的所有资源。
函数DdeUnInitialize的原型定义如下:
BOOL DdeUnlnitialize(
  DWORD idInst //应用程序句柄
);
  函数DdeCreateStringHandle可以返回一个用于标识指定的字符串的句柄,这个句柄可以在其他的DDEML函数中引用。
函数DdeCreateStringHandle的原型定义如下:
HSZ DdeCreateStringHandle(
  DWORD idlnst, //应用程序实例句柄
  LPTSTR psz, //字符串指针
  int iCodePage //代码页标识符
);
  自定义的剪贴板数据格式必须通过函数RegisterCliPboardFormat向系统注册后才能使用,因此在例子的 WinMain函数中,紧接函数DdeCreateStringHandle后又调用了函数RegisterClipboardFormat来注册自定义的剪贴板数据格式。
函数RegisterClipboardFormat的原型定义如下:
UINT RegisterClipboardFormat(
  LPCTSTR lpszFormat //自定义剪贴板数据格式名指针
);
函数DdeNameService用于命名DDE服务名,其函数原型定义如下:
HDDEDATA DdeNameService(
  DWORD idlnst, //应用程序句柄
  HSZ hszl, //记录服务名的字符串的句柄
  HSZ hsz2, //系统保留
  UINT afCmd //服务名标志
);
  接下来调用的函数 DdeConnectList 用来建立一个和其他支持同一服务类型的服务器之间的连接列表。
函数 DdeConnectList 的原型定义如下:
HCONVLIST DdeConnectList(
  DWORD idInst, //应用程序句柄
  HSZ hszService, //月艮务名句柄
  HSZ hszTopic, //主题名句柄
  HCONVLIST hConvList, //连接列表句柄
  PCONVCONTEXT pCC //记录上下文数据的指针
);
函数 BroadcastTransaction是一个自定义的函数,在例13-2中的具体定义如下:
VOID BroadeastTransaction(PBYTE pSrc,DWORD cbData,UINT fint,UINT xtyp)
{
  HCONV hConv;
  DWORD dwResult;
  int cConvsOrg;
  cConvsOrg=cConvs;
  cConvs=0;
  if(hConvList)
  {
    hConv=DdeQueryNextServer(hCoavList,0);//查询下一个服务器
    while(hConv)
    {
      cConvs++; //计数器
      if(DdeClientTransaction(pSrc,cbData,hConv,hszAppName,fmt,xtyp,TIMEOUT_ASYNC,&dwResult))
      {
        DdeAbandonTransaction(idlnst,hConv,dwResult);
      }
      hConv=DdeQueryNextServer(hConvList,hConv);
    }
  }
  if(cConvs!=cConvsOrg)
  {
    InvalidateRect(hWndMain,NULL,TRUE);
  }
}
  在上述程序段中,函数DdeQueryNextServer用于获得连接列表中的下一个连接句柄。
其函数原型定义如下:
HCONV DdeQueryNextServer(
  HCONVLIST hConvList, //连接列表
  HCONV hConvPrev //前一个连接句柄
);
  如果函数 DdeQueryNextServer 返回值不为空,则程序将进入一个 while 循环,在 while 循环体中定义了一个计数器,用于记录服务器的个 数。同时循环体通过函数 DdeClientTransaction 来开始一个客户事务处理, DdeClientTransaction 函数的原型定义如下:
HDDEDATA DdeClientTransaction(
  LPBYTE pData, //传向服务器的数据指针
  DWORD cbData, //数据长度
  HCONV hConv, //连接句柄
  HSZ hszItem, //标识主题某一项的名称的句柄
  UINT wFmt, //剪贴板数据格式
  UINT wType, //事务类型
  DWORD dwTimeout, //延时
  LPDWORD pdwResult //事务处理结果指针
);
  如果服务器不能对客户方的事务进行处理,程序中通过调用函数 DdeAbandonTransaction放弃事务处理并回收所有的资源。
函数DdeAbandonTransaction的原型定义如下:
BOOL DdeAbandonTransaction(
  DWORD idlnst, //应用程序实例句柄
  HCONV hConv, //转换句柄
  DWORD idTransaction //事务标识符
);
  在主窗口的消息处理函数MainwindProc中,处理WM_TIMER消息时还用到了一个DdePostAdvise函数,这个函数将使系统发送一条XTYP_ADVREQ消息给DDE回调函数,DdePostAdvise函数的原型定义如下:
BOOL DdePostAdvise(
  DWORD idlnst, //应用程序句柄
  HSZ hszTopic, //标识主题名的句柄
  HSZ hszltem //标识主题中的某一项的句柄
);
例的核心是DDE回调函数,本例中定义的回调函数如下:
HDDEDATA CALLBACK DdeCallback(WORD wType,WORD wFmt,HCONV hConv,HSZ hszTopic,HSZ hszItem,
                HDDEDATA hData,DWORD IData1,DWORD IData2)
{
  LPTSTR pszExec;
  switch(wType)
  {
    case XTYP_CONNECT: //连接请求
    return((HDDEDATA)TRUE);
    case XTYP_ADVREQ:
    case XTYP_REQUEST:
    //创建数据句柄
    return(DdeCreateDataHandle(idInst,(PBYTE)&count,sizeof(count),0,
    hszAppName,OurFormat,0));
    case XTYP_ADVSTART:
    return(HDDEDATA)((UINT)wFmt=OurFormat&&hszItem==hszAppName);
    case XTYP_ADVDATA:
    //获取数据
    if(DdeGetData(hData,(PBYTE)&InCount,sizeof(InCount),0))
    {
      DdeSetUserHandle(hConv,QID_SYNC,InCount);
    }
    InvalidateRect(hWndMain,NULL,TRUE);
    return((HDDEDATA)DDE_FACK);
    case XTYP_EXECUTE:
    pszExec=(LPTSTR)DdeAccessData(hData,NULL);
    if(pszExec)
    {
      if(fActive && !STRICMP(szPause,pszExec))
      {
        KillTimer(hWndMain,1); //销毁定时器
        fActive=FALSE;
        InvalidateRect(hWndMain,NULL,TRUE);
        UpdateWindow(hWndMain);
      }
      else if(!fActive && !STRICMP(szResume,pszExec))
      {
        //设置定时器
        SetTimer(hWndMain,1,BASE_TIMEOUT+(rand()& 0xff),NULL);
        fActive=TRUE;
        InvalidateRect(hWndMain,NULL,TRUE);
        UpdateWindow(hWndMain);
      }
      MessageBeep(0);
    }
    break;
    case XTYP DISCONNECT: //断开连接
    InvalidateRect(hWndMain,NULL,TRUE);
    break;
    case XTYP REGISTER: //注册服务器
    BroadeastTransaction(NULL,0,OurFormat,XTYP_ADVSTOP);
    //创建连接列表
    hConvList=DdeConnectList(idlnst,hszItcm,hszAppName,hConvList,NULL);
    BroadcastTransaction(NULL,0,OurFormat,XTYP_ADVSTART);
    SetWindowPos(hWndMain,0,0,0,cxText,(cyText*(eConvs+1))+cyTitle,SWP_NOMOVE|SWP_NOZORDER);
    UpdateWindow(hWndMain);
    return((HDDEDATA)TRUE);
  }
  return(0);
}
  回调函数参数表中的wType参数用于指定回调类型;hszTopic参数用于指定主题名:参数hszItem比m用于指定主题中的某一项的名称。不难看出,上面的回调函数和消息处理函数有几分相似。
  当服务器处理完XTYP_CONNECT事务并返回TRUE时,DDE会话也就开始了。服务器和客户之间的通信实际上都是借助上述的事务类型来实现的。当客户向服务器发送一个请求时,DDE管理库就会用相应的事务类型调用服务器的回调函数。
  回调函数接收的事务确定于应用程序在DdeInitialize中指定的回调过滤标志,以及应用程序是否是客户或服务器,还是二者兼是。DDE事务类型如表所示。

事务类型
事务原因
XTYP_ADVDATA客户通过返回数据句柄应答XTYP_ADVREQ事务
XTYP_ADVREQ服务器调用DdePostAdvise函数,指示协商链路中一个数据项目的值已经发生变化
XTYP_ADVSTART客户在调用DdeClientTransaction函数中指定XTYP_ADVSTART事务类型
XTYP_ADVSTOP客户在调用DdeClientTransaction函数中指定XTYP_ADVSTOP事务类型
XTYP_CONNECT客户调用DdeConnect函数并指定由服务器支持的服务名和话题名
XTYP_CONNECT_CONFIRM服务器在应答XTYP_CONNECT和XTYP_WILDCONNECT事务
XTYP_DISCONNECT会话中一个成员调用DisConnect函数,使两个成员都接收此事务
XTYP_ERROR发生严重错误
XTYP_EXECUTE客户在调用DdeClientTransaction中指定XTYP_EXECUTE事务类型

例13-2的源程序
#include <windows.h>
#include <ddeml.h>
#include <stdlib.h>
#include <string.h>
#ifdef UNICODE
#define STRICMP wcsicmp
#define ITOA(c, sz, b) (itoa(sizeof(szA), szA, b),
mbstowcs(sz, szA, b), sz)
#else
#define STRICMP stricmp
#define ITOA itoa
#endif 
//函数声明
HDDEDATA CALLBACK DdeCallback(WORD wType, WORD wFmt, HCONV hConv, HSZ hszTopic,HSZ hszItem,
               HDDEDATA hData, DWORD lData1, DWORD lData2);
VOID PaintDemo(HWND hwnd);
LONG APIENTRY MainWndProc(HWND hwnd, UINT message, WPARAM wParam, LONG lParam);
VOID BroadcastTransaction(PBYTE pSrc,DWORD cbData,UINT fmt,UINT xtyp);
#define BASE_TIMEOUT 100
BOOL fActive; //标识数据是否正在被改变
DWORD idInst = 0; // DDEML对象
HANDLE hInst; //应用程序句柄
HCONVLIST hConvList = 0; //连接列表
HSZ hszAppName = 0; //应用程序名
HWND hwndMain; //主窗口句柄
TCHAR szT[20]; //用于重绘的静态缓冲区句柄
#ifdef UNICODE
CHAR szA[20]; //用于UNICODE 转化缓冲区
TCHAR szTitle[] = TEXT("DDEmo (U)");
#else
TCHAR szTitle[] = TEXT("DDEmo");
#endif
TCHAR szApp[] = TEXT("DDEmo"); // DDE服务名
TCHAR szPause[] = TEXT("PAUSE"); // DDE可执行命令
TCHAR szResume[] = TEXT("RESUME");// DDE可执行命令
UINT OurFormat; //定制的格式
int InCount = 0; //输入数据缓冲区
int cConvs = 0; // 激活的conversations数
int count = 0; //输出数据
int cyText, cxText, cyTitle; //重绘区域
//函数:WinMain
//作用:程序入口
int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,INT nCmdShow)
{
  MSG msg;
  WNDCLASS wc;
  TEXTMETRIC metrics;
  HDC hdc;
  //窗口初始化
  wc.style = 0;
  wc.lpfnWndProc = MainWndProc;
  wc.cbClsExtra = 0;
  wc.cbWndExtra = 0;
  wc.hInstance = hInstance;
  wc.hIcon = NULL;
  wc.hCursor = LoadCursor(NULL, IDC_ARROW);
  wc.hbrBackground = NULL;
  wc.lpszMenuName = NULL;
  wc.lpszClassName = szTitle;
  if(!RegisterClass(&wc))
  return(FALSE);
  //DDE初始化
  if(DdeInitialize(&idInst,(PFNCALLBACK)MakeProcInstance((FARPROC)DdeCallback, hInstance),
    APPCMD_FILTERINITS|CBF_SKIP_CONNECT_CONFIRMS|CBF_FAIL_SELFCONNECTIONS|CBF_FAIL_POKES,0))
    return(FALSE);
    hInst = hInstance;
  //创建窗口
  hwndMain = CreateWindow(szTitle,szTitle,WS_CAPTION | WS_BORDER | WS_SYSMENU, CW_USEDEFAULT,
              CW_USEDEFAULT,0, 0, NULL, NULL,hInstance, NULL);
  if (!hwndMain) 
  {
    DdeUninitialize(idInst);//结束DDE
    return(FALSE);
  }
  hdc = GetDC(hwndMain);
  GetTextMetrics(hdc, &metrics);//获取当前所用字体
  cyText = metrics.tmHeight + metrics.tmExternalLeading;
  cxText = metrics.tmMaxCharWidth * 8;
  cyTitle = GetSystemMetrics(SM_CYCAPTION);
  ReleaseDC(hwndMain, hdc);
  //创建字符串句柄
  hszAppName = DdeCreateStringHandle(idInst, szApp, 0);
  //注册自定义数据格式
  OurFormat = RegisterClipboardFormat(szApp);
  //注册服务器名
  DdeNameService(idInst, hszAppName, 0, DNS_REGISTER);
  //建立DDE会话列表
  hConvList = DdeConnectList(idInst, hszAppName, hszAppName, hConvList, NULL);
  //向所有的服务器广播事务
  BroadcastTransaction(NULL, 0, OurFormat, XTYP_ADVSTART);
  SetWindowPos(hwndMain, 0, 0, 0, cxText,(cyText * (cConvs + 1)) + cyTitle, 
         SWP_NOMOVE | SWP_NOZORDER);
  ShowWindow(hwndMain, nCmdShow);
  UpdateWindow(hwndMain);
  //消息循环
  while(GetMessage(&msg, 0, 0, 0)) 
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
  DestroyWindow(hwndMain);
  UnregisterClass(szTitle, hInstance);
  return(FALSE);
}
//函数:BroadcastTransaction
//作用:向所有服务器广播客户的事务请求
VOID BroadcastTransaction(PBYTE pSrc,DWORD cbData,UINT fmt,UINT xtyp)
{
  HCONV hConv;
  DWORD dwResult;
  int cConvsOrg;
  cConvsOrg = cConvs;
  cConvs = 0; //初始化连接数为0
  if (hConvList)
  { //下一个会话句柄
    hConv = DdeQueryNextServer(hConvList, 0);
    while(hConv) 
    {
      cConvs++;
      //处理客户事务
      if (DdeClientTransaction(pSrc, cbData, hConv, hszAppName, fmt, xtyp,
                   TIMEOUT_ASYNC, &dwResult)) 
      {
        //放弃客户事务
        DdeAbandonTransaction(idInst, hConv, dwResult);
      }
      hConv = DdeQueryNextServer(hConvList, hConv);
    }
  }
  if(cConvs != cConvsOrg)
  {//重绘窗口
    InvalidateRect(hwndMain, NULL, TRUE);
  }
}
//函数:MyProcessKey
//作用:处理键盘消息
VOID MyProcessKey(TCHAR tchCode,LONG lKeyData)
{
  switch (tchCode) 
  {
    case TEXT('B'):
    case TEXT('b'):
    *((PBYTE)(-1)) = 0; 
    break;
  }
}
//函数:MainWndProc
//作用:主窗口消息循环
LONG APIENTRY MainWndProc(HWND hwnd,UINT message,WPARAM wParam,LONG lParam)
{
  RECT rc;
  switch (message) 
  {
    case WM_CREATE://创建窗口
    fActive = FALSE;
    break;
    case WM_RBUTTONDOWN: //按下鼠标右键
    if(GetKeyState(VK_CONTROL) & 0x8000) //判断控制键的状态
    {
      //广播事务处理
      BroadcastTransaction((PBYTE)szPause, sizeof(szPause), 0, XTYP_EXECUTE);
      MessageBeep(0);
    }
    KillTimer(hwndMain, 1);//销毁定时器
    fActive = FALSE;
    InvalidateRect(hwnd, NULL, TRUE);//重绘窗口
    UpdateWindow(hwnd);
    break;

    case WM_LBUTTONDOWN: //按下鼠标左键
    if(GetKeyState(VK_CONTROL) & 0x8000) //判断控制键状态
    {
      //广播事务处理 
      BroadcastTransaction((PBYTE)szResume, sizeof(szResume), 0, XTYP_EXECUTE);
      MessageBeep(0); //发出蜂鸣声
    }
    //设置定时器
    SetTimer(hwndMain, 1, BASE_TIMEOUT + (rand() & 0xff), NULL);
    fActive = TRUE;
    InvalidateRect(hwnd, NULL, TRUE);//重绘窗口
    UpdateWindow(hwnd);
    break;

    case WM_CHAR://键盘消息
    MyProcessKey((TCHAR)wParam, lParam);
    break;

    case WM_TIMER://定时器消息
    count++;
    //发送XTYP_ADVREQ事务给服务器的回调函数
    DdePostAdvise(idInst, hszAppName, hszAppName);
    SetRect(&rc, 0, 0, cxText, cyText);
    InvalidateRect(hwndMain, &rc, TRUE);//重绘窗口
    UpdateWindow(hwndMain);
    break;

    case WM_PAINT://绘制窗口
    PaintDemo(hwnd);
    break;

    case WM_CLOSE://结束应用程序
    KillTimer(hwnd, 1);
    DdeDisconnectList(hConvList);//断开DDE会话
    //撤消已注册的服务器名
    DdeNameService(idInst, 0, 0, DNS_UNREGISTER);
    //销毁字符串句柄
    DdeFreeStringHandle(idInst, hszAppName);
    DdeUninitialize(idInst);//结束DDE会话
    PostQuitMessage(0);
    break;

    default://其他消息
    return (DefWindowProc(hwnd, message, wParam, lParam));
  }
  return(0);
}
//函数:PaintDemo
//作用:绘制窗口
VOID PaintDemo(HWND hwnd)
{
  PAINTSTRUCT ps;
  RECT rc;
  HCONV hConv;
  CONVINFO ci;
  int cConvsOrg = cConvs;
  BeginPaint(hwnd, &ps);//开始绘制
  SetRect(&rc, 0, 0, cxText, cyText);//设定矩形区域
  SetBkMode(ps.hdc, TRANSPARENT);//设置背景模式
  SetTextColor(ps.hdc, 0x00FFFFFF); //文本颜色为白色
  //填充矩形区域
  FillRect(ps.hdc, &rc, (HBRUSH)GetStockObject(fActive ? BLACK_BRUSH : GRAY_BRUSH));
  //输出文本
  DrawText(ps.hdc, ITOA(count, szT, 10), -1, &rc, DT_CENTER | DT_VCENTER);
  if (hConvList) 
  {
    OffsetRect(&rc, 0, cyText);
    SetTextColor(ps.hdc, 0); //设定文本颜色为黑色
    cConvs = 0;
    //查询下一个服务器
    hConv = DdeQueryNextServer(hConvList, 0);
    while (hConv) 
    {
      cConvs++;
      ci.cb = sizeof(CONVINFO);
      //获取DDE会话信息
      DdeQueryConvInfo(hConv, QID_SYNC, &ci);
      FillRect(ps.hdc,&rc, (HBRUSH)GetStockObject(WHITE_BRUSH));// 例中的源程序色背景
      //输出文本
      DrawText(ps.hdc, ITOA(ci.hUser, szT, 10), -1, &rc,DT_CENTER | DT_VCENTER);
      OffsetRect(&rc, 0, cyText);
      hConv = DdeQueryNextServer(hConvList, hConv);
    }
  }
  EndPaint(hwnd, &ps);//结束绘制
  if (cConvsOrg != cConvs) 
  {
    SetWindowPos(hwndMain, 0, 0, 0, cxText, (cyText * (cConvs + 1)) + cyTitle,
           SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
  }
}
//函数:DdeCallback
//作用:DDE回调函数
HDDEDATA CALLBACK DdeCallback(WORD wType,WORD wFmt,HCONV hConv,HSZ hszTopic,HSZ hszItem,
               HDDEDATA hData,DWORD lData1,DWORD lData2)
{
  LPTSTR pszExec;
  switch (wType) 
  {
    case XTYP_CONNECT://处理XTYP_CONNECT事务
    return((HDDEDATA)TRUE);
    case XTYP_ADVREQ:
    case XTYP_REQUEST:
    //创建数据句柄
    return(DdeCreateDataHandle(idInst,(PBYTE)&count, sizeof(count), 0, 
        hszAppName, OurFormat, 0));
    case XTYP_ADVSTART:
    return(HDDEDATA) ((UINT)wFmt == OurFormat && hszItem == hszAppName);
    case XTYP_ADVDATA:
    if (DdeGetData(hData, (PBYTE)&InCount, sizeof(InCount), 0))
    {
      //设置用户句柄
      DdeSetUserHandle(hConv, QID_SYNC, InCount);
    }
    InvalidateRect(hwndMain, NULL, TRUE);
    return((HDDEDATA)DDE_FACK);
    case XTYP_EXECUTE:
    //访问DDE数据
    pszExec = (LPTSTR)DdeAccessData(hData, NULL);
    if (pszExec) 
    {
      if (fActive && !STRICMP(szPause, pszExec)) 
      {
        //消息定时器
        KillTimer(hwndMain, 1);
        fActive = FALSE;
        InvalidateRect(hwndMain, NULL, TRUE);
        UpdateWindow(hwndMain);
      }
      else if (!fActive && !STRICMP(szResume, pszExec)) 
      {
        //设定定时器
        SetTimer(hwndMain, 1, BASE_TIMEOUT + (rand() & 0xff), NULL);
        fActive = TRUE;
        InvalidateRect(hwndMain, NULL, TRUE);
        UpdateWindow(hwndMain);
      }
      MessageBeep(0);
    }
    break;
    case XTYP_DISCONNECT:
    InvalidateRect(hwndMain, NULL, TRUE);
    break;
    case XTYP_REGISTER:
    //向服务器广播事务
    BroadcastTransaction( NULL, 0, OurFormat, XTYP_ADVSTOP );
    hConvList = DdeConnectList(idInst, hszItem, hszAppName, hConvList, NULL);
    BroadcastTransaction(NULL, 0, OurFormat, XTYP_ADVSTART);
    SetWindowPos(hwndMain, 0, 0, 0, cxText,(cyText * (cConvs + 1)) + cyTitle, 
           SWP_NOMOVE | SWP_NOZORDER);
    UpdateWindow(hwndMain);
    return((HDDEDATA)TRUE);
  }
  return(0);
}
13.3对象链接与嵌入基础知识
  对象链接与嵌入实际上包括链接和嵌入对象、观察对象和编辑对象等涉及的所有技术。
11.3.1对象链接与嵌入
  在这里一个对象被定义为任何一个文本中显示的并且可以被最终用户修改的数据。对象范围可从小到一个电子表格单元大到一个完整的文本,当一个对象归属于一个文本时,它与创建它的应用程序保持着一种联系。当一个对象被嵌入到其他对象之中时,不仅相关的数据被嵌入,而且相应的数据规则也被嵌入,这样用户不必返回该源应用程序就可以对一个对象进行编辑和修改。
  当一个OLE文本包含了以各种不同格式表示的数据时,它就被称为一个复合文本。如果没有实现OLE技术,则每一种不同的数据类型都能够被最终用户修改。最终用户不得不在不同的应用程序中取得不同的编辑工具;如果应用程序实现了OLE技术,在同一环境下就可以实现对不同格式的数据进行编辑和修改。OLE技术给予了最终用户一个以文本为中心的参照框架系统,而不是一个以应用程序为中心的参照框架系统。
  复合文本使用应用程序的许多特性来修改包含在其中的数据类型,任何一种数据均可以包含在一个复合文本之中。最终用户可以不必知道哪些数据格式与其他的数据格式相兼容,最终用户也不必知道怎样寻找创建该数据的其他应用程序。当用户在一个文本的一个部分里工作时,负责那个部分的应用程序合自动地开始工作。
OLE的好处在于:
1、文件的链接对象可以动态地加以改变;
2、当前的应用程序可以对未来的数据格式进行扩展,因为一个对
  象的内容与包含它的文本无关;
3、文本文件可以更加紧凑,这是因为对一个对象的链接使得该文
  本使用了该对象, 而不必存储该对象的数据;
4、一个应用程序可以很好地、专业化地做某项工作;
5、最终用户面向的是~个以文本为中心的用户系统;
6、实现了OLE协议就可以利用多种多样不同类型的对象优点。
13.3.2对象插入与嵌入操作
  一般地,命令Copy将把一个对象的拷贝放到剪贴板中。然后,命令Paste将该对象从剪贴板中嵌入到目标文件中。这个规则存在一个例外:如果该对象由一个备用的格式表示,而该备用格式完全代表原数据而且它能够被目的应用程序编辑,那么应用程序就可以选择这个备用格式,而不嵌入该对象。当然,大多数目的应用程序并不能识别备用格式,只能依赖源对象提供必须的编辑内容。
  在一般意义上,命令Paste比在剪贴板包含有另一个应用程序中的一个对象时将嵌入已剪贴的对象。在那些并不支持OLE的应用程序中,则插入一个静态的拷贝。如果对象在某些时候不得不被编辑的话,该静态拷贝不能很容易地被源编辑访问。
  1、Paste Link:
  在对象中插入链与插入对象一样简单。首先使用Copy命令从一个源中复制选中的内容,然后选择Paste Link 命令。这样,一个链被加入到当前光标的位置。 尽管粘贴内容显示在目标文件中,但是,实际上它并不存在那里,而是存储在源文件中。这时,如果图形在源文件中被修改了,那么它在目标文件中也能得到修改。
  2、Paste Special和Insert Special:
  更先进的应用程序可以处理各种各样的文件格式,其中包括格式化的正文、位图以及其他的文件类型。在这些格式中,选择Paste Special命令可以实现对象的链接与嵌入。Paste Special在从一个源应用程序中复制信息时能够给予用户更多的格式控制。
  从剪贴板中复制一个对象时,总有一个默认的格式用于将对象移动到目的应用程序中去。然而,如果有适用的备用格式,那么用户可以利用命令Paste Special把默认的格式替换成各用格式。
13.4 ActiveX简介
  ActiveX最早是作为Internet策略提出的,现在涉及OLE/COM/Internet应用程序开发的各个方面,包括自动化服务器、组件和COM对象等。
  Microsoft最早是在 1996年 3月的 Internet专业开发人员研讨会(Internet PDC)上提出ActiveX一词的。当时ActiveX指的是大会口号“Active the Internet”,这仅仅是一种号召而非具体的应用程序开发技术或体系结构。
  在Internet PDC期间, Microsoft与Netscape针对Internet Web浏览器市场的控制权展开了面对面的交锋。PDC表现出Microsoft不仅对浏览器市场感兴趣,而且还对其他业务也存在浓厚的兴趣。会上还展示了从电子存储前端到新的OLE组件、到虚拟现实交谈软件等一系列工具。
  随后, ActiveX成了 Microsoft的新企业口号,它的含义很快就超过了“Active the Internet”。 ActiveX成了定义从Web页面到 OLE(Object Link and Embedding,对象链接和嵌入)组件的所有内容的核心术语。一方面,它表示将你联系到Microsoft、Internet和业界新技术的小型快速可重用组件;另一方面,ActiveX代表了Internet与应用程序的一种集成策略。如今,如果哪个产品或公司在它的词汇中不出现ActiveX,那无论是从内部还是从外部来看它都落伍了。事实上,ActiveX不是一种技术或—种体系,而是一种概念和潮流。
ActiveX和OLE有很深的渊源,以前所说的OLE组件现在已被称作了ActiveX组件,OLE DocObjects现在称为ActiveX文档。在一些情行下,有关如何实现OLE技术的文档已经全部更新为ActiveX技术文档。
虽然OLE和ActiveX已经有了很大的发展,而且看上去每天都有新的技术出现,但值得怀疑的是,是否这些都是 Internet引发的。对于小型快速可重用的组件(COM Objects)的要求已经提出了很多年,分布式组件(DCOM Objects)是在很多年前的OLE 2.0 PDC上最早演示的。事实上,ActiveX的每个特征都可以追溯到对小型快速可重用组件的需求,而这些都是以OLE和COM为基础的。
  ActiveX并不是为了替换OLE,而是将它扩展为适应Internet、Intranet幻、商业应用程序和家庭应用程序的开发,以及开发它们所使用的工具。除了生成AciveX组件的特定技术外, Microsoft还建立了一套使用和集成 ActiveX组件的标准,从 Visual Basic到Microsoft Word……的所有产品都具有使用 ActiveX组件的能力。
一般来说,ActiveX组件类型包含以下几个方面的内容:
  1、自动化服务器;
  2、自动化控制器;
  3、组件;
  4、COM对象;
  5、文档:
  6、容器。
  自动化服务器是可以由其他应用程序编程驱动的组件。自动化服务器至少包含一个,也可能是多个供其他应用程序生成或连接的基于IDispatch的接口。自动化服务器可以包含也可以不包含用户界面,这取决于服务器的特性和功能。
  自动化服务器可以在控制器的进程空间运行、在它自己的编程空间内运行,或在另一台机器的进程空间运行。服务器的特定实现方式决定了它将如何以及在何处运行,但也并非完全如此。一个DLL既可以在进程内,也可以是本地和远程的,而EXE只能在本地或者远程运行。
  自动化控制器是那些使用和操纵自动化服务器的应用程序。一个自动化控制器可以是各种类型的应用程序和动态链接库。所以可以通过进程内、本地或者远程的各种方式访问服务器。通常,注册项和自动化服务器的实现表明服务器将在相对于控制器的那个处理空间内运行。
  ActiveX组件等价于以前的OLE组件或者OCX组件。一个典型的组件包括设计时和运行时的用户界面,唯一的IDispatch接口定义组件的方法和属性,以及唯一的IConnectPoint接口用于组件可以引发的事件。除此之外,一个组件还可以包含对其整个生命周期的一致性的支持,以及对剪贴、拖放等用户界面特性的支持。从结构上看,一个组件有大量必须支持的COM接口,以利用这些特性。
  从针对组件开发的 OLE和 ActiveX的新开发指南上看,一个组件的特性已不限于以上描述的内容,而且开发者也可以仅实现用户感兴趣且有用的应用程序特性。ActiveX组件永远都是在其所放置的容器中进程内运行的,组件的典型扩展名是.ocx,若从运行模块的角度来看,它不过是一个标准的 Windows DLL而已。
  COM对象在结构上与自动化服务器和控制器类似,它们有一个或多个COM接口,没有或有很少的用户界面。然而,这些对象不能像自动化服务器那样被典型的控制器应用程序所使用。或者为了使用接口,控制器必须具有和它进行对话的COM接口,Windows95和 NT操作系统有上百个 COM对象和 Customer接口,用于对操作系统进行扩展,控制包括桌面的外观和显示器的三维图形渲染等各个方面。COM对象是一种组织相关功能和数据的好方式,同时还保留了DLL的高速性能。
  所谓 ActiveX文档实际上是指 DocObject,一个文档可以是从电子表格到财务报表等应用程序中的任何元素。与组件一样,文档也有用户界面并且包含于容器应用程序之中。
  ActiveX文档在结构上是对OLE链接和嵌入模型的扩展,并对其所在的容器具有更多的控制权,一种最显著的变化是菜单的显示方式。一个典型的OLE文档的菜单会与容器菜单合并成一个新的菜单,而ActiveX文档将替换整个菜单系统,只表现出文档的特性而不是文档与容器共同的特性。容器只是一种宿主机制,而由文档本身进行所有控制。文档特性的表达方式的不同是ActiveX文档和OLE文档所有差别的根源。
  其他区别是打印和存储。一个OLE文档被认为是其容器文档的一部分,因此是作为宿主容器文档的一部分进行打印和存储的。ActiveX文档自身具有打印和存储功能,而不是集中在容器文档中。
  ActiveX文档在一个统一的表示结构中使用,而不是位于嵌入式文档结构中,后者是OLE文档的基础, Microsoft Internet Explorer就是这方面的一个很好的例子。 IE只是将Web页面展示给用户,但它是作为一个单一的实体进行显示、打印和存储的。Microsoft Word和Microsoft Excel则是OLE文档结构的例子,如果一个Excel电子表格嵌入在一个Word文档中,电子表格实际上是存储在W。rd文档中,并成了它的一个集成部分。
  ActiveX容器是一个可以作为自动化服务器、组件和文档宿主的应用程序。Microsoft Internet Explorer可以作为自动化服务器、组件和文档的宿主。容器必须足够健壮,以便处理组件和文档中缺少的一些接口特性。容器应用程序与它所包含的文档和组件只进行很少或者根本就不进行交互,或者用某种特殊的方式来维护和表示所包含的宿主组件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值