Windows Shell 编程 第十一章

本文探讨了Windows Shell的命令行特性及其核心组件探测器的工作原理。介绍了rundll32.exe程序如何帮助开发者调用DLL函数及访问无文档的系统对话框。同时,概述了特殊Shell对象如打印机和我的公文包的功能。
摘要由CSDN通过智能技术生成

第十一章 探索Shell

         我们现在将注意力从API转向Windows Shell本身。从这一章往后,我们的目标主要集中在清晰和全面的揭示探测器的工作原理和Shell 命名空间的构成对象上,最后给出客户化探测器和扩展其特征和行为的示例程序。

         Windows Shell,也称之为探测器,是一群特殊模块的集合,这些模块一起形成了Shell的命名空间,并且给出了执行大量特殊任务的能力,例如,探测文件夹,显示特定的目录子树,装入外部模块并与它通讯等。尽管最终结果可能会有差异,只要这些对象被调用和显示,探测器总是进入后台操作。

         在许多情况下,探测器提供的服务都是通过命令行与给定程序通讯来进行的,因而,如果你希望探测器有效地管理你的程序,就需要了解应用应该向探测器提供什么,这是十分重要的。(这个题目将在14章中更详细地讨论)

         在这一章中我们将揭示下面科目:

                   探测器的命令行

                   运行系统对话框的RunDll32程序

        Shell对象‘我的公文包’,‘控制面板’,‘打印机’和‘任务调度’

        零碎对象

这一章给出的源代码的示例提供了一个有用的工具,其中涵盖了许多我们以前解释过的技术,如快捷方式和注册表处理等。这个工具是NewLink,它提供了在桌面或其它任何地方建立快捷方式的许多灵活方法。如果愿意,可以安装它作为快捷方式处理器来取代标准的Windows大师。

 

探测器的命令行

         探测器有一个可以取四种选项开关的命令行,下面是几种可能的组合结果:

 

explorer.exe [/n [, <folder>]]

[/e [, <folder>]]

[, /root, <object>]

[[, /select], <sub object>]

 

注意,命令行结构中逗号的使用。这肯定是不常看到的。下面是这些开关的意义:

 

开关

描述

/n

在新的单框观察窗口中打开指定的文件夹。单框观察窗口是一个基于列表观察的窗口。

/e

在新的双框观察窗口中打开指定文件夹。这是典型的探测器观察。左边是命名空间的树观察,细节在右边框中。

/root

使指定的文件夹作为观察树的根。需要一种/e类型的观察。

/select

在左框中选择指定项(树观察)

 

最简单的开关是 /select 选项,它用于在打开的文件夹中选择特殊的子项。下面是一个例子:

explorer /e, /select, c:/windows

这个标志需要与 /e 联合使用,因为 /select 标志要求树观察。在右框或单框文件夹中没有办法选择项。在使用 /n /e 开关时,可以在逗号后指定文件夹名:

explorer /e, c:/

explorer /n, c:/

explorer c:/

上面的第二和第三行产生相同的结果(单框观察)—这就是说 /n 是默认选项。

 

/root 开关

         我们经常看到类似探测器观察的情况,但是它们都有一个特殊的文件夹作为根。你可以通过组合使用 /e /root 打开这样的探测器观察,例如:

explorer /e, /root, c:/windows

注意,当打开一个观察使用的根不是桌面时,用户就不能沿着树向上行进。如果探测器使用c:/windows作为根,则用户不能访问这个树中的 c:/ 或任何与 /windows 同层的目录。

 

使用特殊的文件夹作为根

    特殊文件夹,如‘回收站’,与文件系统文件夹没有一对一对应。就象我们前面讨论过的,这些命名空间扩展是进程内服务器,是由CLSID所标识的。/root 开关使你可以指定一个CLSID作为一个文件夹使用,但是需要满足两个限制条件:

    这个COM服务器必须实现所有命名空间扩展所要求的接口

你必须在探测器命令行上使用::前缀在CLSID之前来引用它。其语法为::(CLSID)。这使得它被处理成普通的目录名。

例如:

explorer ::{645FF040-5081-101B -9F 08-00AA 002F 954E}

这将打开一个回收站新窗口,你也可以使用自己的客户文件夹这么做。下面的表中给出了几个可以在桌面上找到的文件夹对象CLSIDs,但是需要注意,并不是桌面上所有对象都是文件夹—例如‘Inbox’和‘我的公文包’就不是文件夹而是应用。

CLSID

对象

645FF040-5081-101B -9F 08-00AA 002F 954E

回收站

20D04FE0-3AEA-1069-A2D8-08002B30309D

我的计算机

208D 2C 60-3AEA-1069-A2D7-08002B30309D

网上邻居

871C 5380 -42A 0-1069-A2EA-08002B30309D

IE 浏览器

21EC2020-3AEA-1069-A2DD-08002B30309D

控制面板

992CFFA0-F557 -101A -88EC-00DD010CCC48

拨号连接

2227A 280-3AEA-1069-A2DE-08002B30309D

打印机

 

注意,在这里你可以把CLSIDs看作传统的文件夹,并且可以使用斜线连接它们。例如要访问打印机文件夹,它是‘我的计算机’的子文件夹,你可以使用下面的文法:

explorer ::{20D04FE0-3AEA-1069-A2D8-08002B30309D}/

::{ 2227A 280-3AEA-1069-A2DE-08002B30309D}

 

关于rundll32.exe

         我们曾说过在探测器组织中命令行扮演着重要的角色。许多在文件对象上执行的功能都是通过命令行实现的。为了进一步增强,Windows9xNT4.0以后的版本推出了rundll32.exe辅助程序。它允许你直接从命令行调用DLL输出的函数。这个实用程序简单地封装了几个需要动态执行的API调用。

         下面的伪代码说明了rundll32的操作:

void DoRunDll32(LPCTSTR szDllName, LPCTSTR szFuncName, LPCTSTR szCmdLine)

{

// 装入这个库

HANDLE hLib = LoadLibrary(szDllName);

// 取得请求执行的函数地址

FARPROC pFunc = GetProcAddress(hLib, szFuncName);

// 使用指针执行函数

pFunc(GetFocus(), hLib, szCmdLine, SW_SHOW);

// 释放库

FreeLibrary(hLib);

}

在实际使用中,rundll32接收它所解析的单个字符串,从中抽取动态库和函数名,以及选择的函数变量。命令行有下面形式:

rundll32 dllname,funcname [arguments]

DLL名和函数名之间用逗号分割,并且不能有空格。如果调用rundll32没有指定DLL的全路径,它搜索所有标准路经包括应用的目录,Windows目录和当前目录。

         这个程序的缺点是在错误情况下通常不能返回足够的信息。如果你试图调用的DLL函数缺失(比如,键入的名字错),则它可以使你相当快地知道发生了什么,然而,如果这个函数可以被调用,但是在执行时失败,此时你就必须自己猜测究竟发生了什么。

         rundll32的接口通常用于调用某些不熟知的系统对话框,别的用户也可以用它来调用你的DLL,要这样做的唯一要求是需要可调用函数的预定义原型。

 

rundll32.exe可调用的函数

         在上面的伪代码中,看一下实际调用DLL函数那一行。Rundll32仅仅可以调用具有下面原型的函数—有四个参数,而仅仅有一个用户可以设置的:

void CALLBACK FuncName(HWND hwnd, // Window handle

HINSTANCE hinst, // Instance handle

LPTSTR lpszCmdLine, // Command line

int nCmdShow); // ShowWindow() parameter

hwnd参数指定函数建立任何窗口的父窗口,也就是说,这个对话框是相对于父窗口的模式对话框。事实上,这个参数总是等价于桌面窗口(即,hwnd NULL),所以,相对于任何Shell中打开的窗口,新窗口总是非模式的。第二个参数是HINSTANCE类型的Handle,是一个由LoadLibrary()返回的库Handle。第四个参数nCmdShow确定窗口的显示方式,而且在通过rundll32调用函数时应该总是SW_SHOW

    唯一一个可控制参数是lpszCmdLine,通过这个参数,函数接收数据变量,例如,假设有一个函数接收两个数字,定义如下:

void MyFunc(int iFirst, long lSecond)

{

...

}

要使这个函数可通过rundll32接口调用,它应该变形为:

void MyFunc1(HWND hwnd, HINSTANCE hinst, LPTSTR lpszCmdLine, int nShow)

{

int iFirst;

long lSecond;

// 解析命令行和截断出参数

ParseCommandLine(lpszCmdLine, &iFirst, &lSecond);

// 用参数调用已经抽取的函数

MyFunc(iFirst, lSecond);

}

NT下,要调用的函数首先搜索Unicode名,然后是ANSI名,最后才是实际所写的名。也就是说,调用MyFunc()被依次转换成调用MyFuncW(), MyFuncA() MyFunc(),当然,此时的参数串也被传递成宽字符串,封装为LPWSTR,而不是LPSTR

 

rundll32.exe能做些什么

         rundll32(16位祖先是rundll)主要用于显示系统对话框和在仅能使用命令行的情况下调用DLL函数。初始它作为微软内部使用的工具而设计,因此,故意限制其实际设置是显然的。

         在测试代码时,这个程序可以使加载和卸载DLL明显地节省时间。有时它也能帮助你检查DLL输出的对话框。反过来,它也强制函数调用采用固定的格式,因而也就没有数据返回调用者的机理。然而,它有一种特殊的功能是你想使用的。

         你可以使用rundll32来获得对某些系统对话框的访问,包括那些由于资料缺乏不容易访问的对话框。在许多情况下访问某些东西的唯一推荐的(和资料说明的)方法是使用rundll32。例如‘添加新打印机’操作:

             

这个操作是系统通过调用sysdm.cpl库中的一个函数实现的。扩展名说明这个库是‘控制面板’DLL。资料中建议这样调用:

rundll32.exe sysdm.cpl,InstallDevice_Rundll printer

而不是直接调用InstallDevice_Rundll()函数。这个函数的四个参数我们已经讨论过了,使用‘printer’作为变量来确定要做什么。事实上尽管没有资料说明,我们也能够使用相同的语法来安装新的调制解调器或监视器,只须简单地替换‘printer’为‘modem’或‘monitor’即可。

 

RunDll() 函数

         许多系统对话框和应用大师仅支持使用rundll32的调用。这可能是因为可以适应将来的变化,其中使用rundll32可以屏蔽新的细节。还有可能是因为这些函数和对话框不是完全公共的,再有就是使用rundll32接口可以隐藏它们的许多方面。下面的清单显示了用C++ 函数模仿rundll32的可能的实现。回顾一下我们早先给出的伪代码例子,这里加入了错误检查和指定父窗口的方法:

void RunDll(HWND hwnd, LPCTSTR szDllName, LPCTSTR szFunc, LPCTSTR szCmdLine)

{

HANDLE hLib = NULL;

hLib = LoadLibrary(szDllName);

if(hLib == NULL)

return;

FARPROC pFunc = NULL;

pFunc = GetProcAddress(hLib, szFunc);

if(!pFunc == NULL)

pFunc(hwnd, hLib, szCmdLine, SW_SHOW);

FreeLibrary(hLib);

}

以这种方式封装我们自己的函数,就可以把我们想要的任何窗口作为父窗口传递给子对话框,这就允许我们显示模式对话框。

 

Rundll32.exe保护错

         你当然可以用rundll32调用不是为了与rundll32一起工作而设计的函数。但是这样做需要冒险—可能出问题。例如,如果试图运行一个对话框,在关闭对话框时可能会有保护错发生。试着在‘运行’对话框中运行下面代码:

rundll32 appwiz.cpl,ConfigStartMenu

这个命令显示一个可以修改程序菜单内容的窗口:

                      

当你关闭这个对话框时,rundll32产生这个错误:

                 

然而我们发现,如果使用你自己的代码在程序中输出相同的命令,使用上面的RunDll()函数,它正好能正常地工作。

         这只是一个例子。如果你搜索MSDN知识库文档,使用‘rundll’或‘rundll 32 文字搜索,你将会找到很多描述由rundll32引起的退出对话框的各种错误。

 

通常使用的命令

         在某些情况下,这些冲突来自于函数的不正确使用。如果你请求rundll32执行一个函数具有非推荐原型,将可能获得错误。rundll32实际也不可能知道此处的函数是否为安全可调用函数。但是我们可以保证,如果函数有一个自解释的RunDll前缀或后缀,它将正常工作。在深入研究了知识库文章和新闻组等信息后,我们给出下表总结了允许访问无资料系统对话框的调用:

 

对话框

命令行

Internet 属性

Rundll32 Inetcpl.cpl,LaunchInternetControlPanel

删除快捷方式/文件夹

Rundll32 appwiz.cpl,ConfigStartMenu

打开文件

Rundll32 shell32.dll,OpenAs_RunDLL file

连接到‘我的连接’

Rundll32 rnaui.dll,RnaDial My Connection

建立新连接

Rundll32 RnaUI.dll,RnaWizard

添加打印机

Rundll32 sysdm.cpl,InstallDevice_Rundll printer

安装新的调制解调器

Rundll32 sysdm.cpl,InstallDevice_Rundll modem

安装新监视器

Rundll32 sysdm.cpl,InstallDevice_Rundll monitor

添加新硬件

Control.exe sysdm.cpl,Add New Hardware

 

internet 属性’是已知的标签对话框,当你在控制面板上双击Internet下的小程序时显示这个对话框,或在IE的‘观察 | Internet选项’菜单下,或从联系IE图标的关联菜单中选择‘属性’时也显示这个对话框。下一个是‘删除快捷方式 / 文件夹’对话框,而‘打开文件’对话框则是打开文件时弹出的对话框。‘连接到’对话框可以通过指定的拨号连接到Internet网,注意不需要把表示连接的串用引号括起来,即使名字中包含空格也不用。‘建立新连接’运行系统大师,并添加新连接到‘拨号网络’文件夹。正像我们提到过的,还有一个‘添加 / 建立’对话框允许你添加新的modem ,打印机或监视器到系统硬件配置中。

         表中最后一项‘添加新硬件’是默认的PC搜索即插即用硬件的大师,然而,它并没有绑定到rundll32,而是由control.exe取代,这是一个在控制面板后台的可执行程序。我们之所以在这里引出它是因为它引出一个新课题:探测器委托对象。

    如果运行非英语班本的Windows,‘添加新硬件’的命令行不能工作,我们可以告诉你问题所在—是由于串‘添加新硬件’应该使用本地版本。

 

探测器对象

         下图中显示了Shell命名空间的一个观察。所有你所看到的文件夹在桌面下都是形成Shell的对象‘我的计算机’和‘网上邻居’包含了这台PC的详细信息和它所连接的网络。IE节点是一个表现Internet的虚拟文 件夹。如果给出一个连接,你就可以展开它来查看任何web页面,就像任何普通文件一样显示。‘回收站’和‘我的公文包’则完全是桌面对象的列表。

              

         在第10章中我们已经谈到过‘回收站’。它是局部驱动器上多个物理文件夹的状态集合,这些文件夹中的每一个都包含了对标志为删除文件的引用。

         ‘我的公文包’是有趣的,但是我们很少了解它的特征,它帮助你在多台PC上保持文件和目录的同步。后面将详细介绍。

         在‘我的计算机’节点下有一些更特殊的文件夹。从程序员的观点看,最有趣的是:

                   打印机

                   控制面板

                   拨号网络

                   任务调度

另一些特殊文件夹在Windows目录下,它们包含了子描述,下载的程序文件,以及站点访问历史。

 

控制面板

         ‘控制面板’是描述和配置硬软件部件的对话框库。系统自动提供一定数量的对话框,也可以添加你自己的。‘控制面板’文件夹是由读取所有在‘system’目录中找到的 .cpl 信息充填的命名空间扩展。还有一个可执行程序control.exe,功能是简单地请求Shell打开这个文件夹,和管理用户的活动,据此通知这些小程序(applets)

         当然,你所写的应用程序作为控制面板项,需要有一个不同于.cpl的扩展名。首先也是最重要的是这个程序必须是一个DLL,并且还要满足附加的条件:

         必须输出CPlApplet()函数

    必须适当地响应一定的消息

    必须提供图标和对话框

控制面板小程序全都是围绕配置对话框构建的——每个小程序,一个对话框。

         无论现在applets意义如何,它最常用于描述小的java模块。在微软资料中很长时间以来都表示为控制面板部件。

 

开发控制面板小程序

    控制面板小程序DLL必须输出CPlApplet()函数,这个函数的原型如下:

LONG CPlApplet(HWND hwndCPl, // 对话框父窗口的Handle

UINT uMsg, // 接收的消息

LONG lParam1, // 由消息指定的头一个变量

LONG lParam2); // 有消息指定的第二个变量

这个函数基本上是小程序的窗口过程——通过消息控制应用与小程序通讯。特别,可以请求DLL实现的多个小程序,以及请求图标信息,名字和给定小程序的描述信息。很多.cpl文件仅实现单个小程序,然而并没有限制你在其中实现多个小程序。下表列出了所有可能的消息:

 

消息

描述

CPL_DBLCLK

小程序的图标已经被双击,所以应该显示相关的对话框。lParam变量基于0开始的数,表示小程序在DLL的序号,这个消息在CPL_INQUIRECPL_NEWINQUIRE之后发送。lParam2变量包含CPLINFO结构的lData成员定义的用户数据(见下面)

CPL_EXIT

这个消息在CPL_STOP之后,并且在小程序卸载之后立即发生,它没有参数。

CPL_GETCOUNT

询问DLL返回它所实现的小程序数。它在CPL_INIT之后发送,没有参数。

CPL_INIT

在小程序被加载时立即发送。没有参数。

CPL_INQUIRE

用于获取小程序的信息。它仅被调用一次,并且返回的信息由系统缓存。lParam1变量是DLL中小程序的基于0的序号。lParam2是一个指向CPLINFO结构的指针,它必须被填充。

CPL_NEWINQUIRE

CPL_INQUIRE目的相同,它使用不同结构,会话期间可以多次发送。lParam1变量是DLL中基于0的对话框序号,lParam2是指向NEWCPLINFO结构的指针,它必须被填充。

CPL_SELECT

在所有Win32平台上作废的和不支持的消息。

CPL_STOP

只发送一次,表示对话框打算关闭。lParamDLL中基于0的对话框序号,lParam2变量表示在CPLINFO结构中lData成员定义的用户数据。

 

如上所述,除了没有参数的消息,lParam1程序总是表示DLL中对话框的索引。相反,lParam2变量有两个意义:表示一个客户的32位缓冲,或指向数据结构的指针,这个结构是用于收集对话框信息的。

         有两个消息是控制应用用来获取给定小程序显示的对话框信息的:CPL_INQUIRE CPL_NEWINQUIRE。在此情况下lParam2指向两个不同的结构——CPLINFONEWCPLINFO——其定义如下:

typedef struct tagCPLINFO

{

int idIcon; // 小程序的图标资源ID

int idName; // 对话框短名字串的资源ID

int idInfo; // 对话框描述串的资源ID

LONG lData; // 应用定义的数据

} CPLINFO;

 

typedef struct tagNEWCPLINFO

{

DWORD dwSize; // 结构尺寸

DWORD dwFlags; // 省略

DWORD dwHelpContext; // 省略

LONG lData; // 应用定义的数据

HICON hIcon; // 小程序图标的 Handle

TCHAR szName[32]; // 对话框的短名字

TCHAR szInfo[64]; // 对话框的描述

TCHAR szHelpFile[128]; // 省略

} NEWCPLINFO;

如上所见,尽管声明不同,但是结构包含了相同的信息。控制面板小程序应该回答至少一个相关的消息,在绝大多数情况下,因为缓存信息,所以CPL_INQUIRE消息提供较好的性能。如果有任何返回的信息在会话期间改变了,则你需要支持CPL_NEWINQUIRE消息,事实上后一个消息在控制器每次使用小程序信息时都被发送。另一个敏感的差异是CPLINFO信息要求存储在小程序资源中的信息。而NEWCPLINFO则可以使用缓冲返回它们。

         可能有一种情况,你希望关联小程序的全程状态信息——随没有必要,但有时会有帮助,这就是lData起作用的地方。下面的清单显示了一个示例CPlApplet()函数:

LONG CPlApplet(HWND hwndCPl, UINT uMsg, LONG lParam1, LONG lParam2)

{

// 保存操作对话框点索引

int iDlgIndex = lParam1;

switch(uMsg)

{

// 作需要的初始化操作

case CPL_INIT:

return 1;

// 返回DLL中小程序的数量

case CPL_GETCOUNT:

return g_iNumOfApplets;

// 充填CPLINFO 结构的字段

case CPL_INQUIRE:

LPCPLINFO pCPL = reinterpret_cast<LPCPLINFO>(lParam2);

pCPL->idIcon = g_iIconIndex;

pCPL->idName = g_pszAppName;

pCPL->idInfo = g_pszDesc;

break;

// 在接收双击后显示对话框

case CPL_DBLCLK:

DialogBox(GetModuleHandle(NULL),

MAKEINTRESOURCE(g_iDlgID), hwndCPl, pfnDlgProc);

break;

}

return 1;

}

 

运行控制面板小程序

         我们前面说过control.exe不是控制面板文件夹后面的程序,而简单地是一个存根,是调用探测器来显示可以找到的所有.cpl文件内容的导引。‘控制面板’不是物理文件夹——其表现和所有管理工作都由实现这个文件夹的命名空间扩展完成。如果你想要从一个程序中运行控制面板小程序,最好的方法是使用rundll32来执行Control_RunDLL()函数,这是一个由shell32.dll库输出的函数:

rundll32.exe shell32.dll,Control_RunDLL applet.cpl

上面代码通过发送CPL_DBLCLKDLL输出的CPlApplet()函数执行名为applet.cpl的小程序。你也可以使用我们前面给出的RunDll()函数调用Control_RunDLL()

RunDll(hDlg, "shell32.dll", "Control_RunDLL", "desk.cpl,,3");

这个命令的输出显示如图所示:

                     

这个命令调用显示小程序,并告诉它显示第四个页面(命令行中的3,基于0的记数,引用第四个元素)。因为通过Control_RunDLL()调用控制面板小程序时,指定的三个参数有两个是可选的,头一个是.cpl文件名,第二个是DLL中基于0的小程序号,默认为0,必须有前缀@。第三个变量是基于0的页面索引,这个页面是想要初始选择的。当然,这个小程序仅是对页面对话框,并且有0为默认页面。记住,上面的串‘desk.cpl,,3’必须读作“显示desk.cpl中的头一个小程序的第四个页面”。在后面的例子中我们考虑sysdm.cpl模块,其中包含两个小程序:‘系统’和‘添加新硬件’。要显示头一个小程序的第二个页面,它列出了系统中所安装的设备,你可以使用下面的命令行:

RunDll(hDlg, "shell32.dll", "Control_RunDLL", "sysdm.cpl,,1");

这等价于:

RunDll(hDlg, "shell32.dll", "Control_RunDLL", "sysdm.cpl,@0,1");

反之为了启动‘添加新硬件大师’,应该使用:

RunDll(hDlg, "shell32.dll", "Control_RunDLL", "sysdm.cpl,@1");

这个调用反映出,我们正在调用DLL的第二个小程序,它是一个没有分页的对话框。

 

Control_RunDLL() . Control.exe

         前面我们给出过另一种导出‘添加新硬件大师’的方法,使用control.exe程序:

control.exe sysdm.cpl,Add New Hardware

而要使它工作,有一个重要的缺点,就是命令行将根据本地位置而变化,因此它不如Control_RunDLL()方案。如果你运行的是意大利版的系统,串就变为‘Nuovo Hardware’,而不是‘Add New Hardware’。也就是说,命令行必须变为:

control.exe sysdm.cpl,Nuovo Hardware

sysdm.cpl库的串表中,本地化串有ID 202索引,这也是通过CPlApplet()接口返回的。

 

RunDll32.exeRunDll()的交换

         rundll32.exe和我们的函数RunDll()提供了相同的功能。在二者各有自己的的特点。rundll32.exe程序是操作系统的标准部件,它可能随版本的升级而更新,也就是说即使微软改变了rundll32.exe的编程接口——正确调用函数所需要的原型——程序仍能正常工作。

    相反,如果你在NT上开发16位代码,不可能有等价16位的rundll32.exe(Windows9x上有一个rundll.exe)。更重要的是,rundll32是一个你不能直接控制的程序,并且它总是启动新的进程。相反RunDll()则是运行在调用者空间上的。

 

打印机文件夹

         ‘打印机’文件夹也不映射到真实的文件系统文件夹——它是虚拟文件夹,提供对系统中可用的打印设备进行访问的能力,比如打印机和传真机。在往PC上安装新打印机时,在隐藏的子目录中建立一个新文件,这个隐藏的目录是Windows下的一个目录。这个子目录与‘回收站’的‘Recycled’文件夹(10章中已经说明了)有同样的作用。这个目录称为‘PrintHood’,可以使用SHGetSpecialFolderPath() API 函数恢复这个路径。

         我们在第5章中检视过怎样浏览‘打印机’文件夹的内容,在下一章中我们返回到这个科目,我们将查看一个新的脚本Shell对象,它提供了许多我们先前使用自动控制构建的功能。

         关于打印机,我们需要查看SHInvokePrinterCommand()函数。它允许我们向打印机对象发送命令。这个函数仅在4.71以上版操作系统上支持。它的原型为:

BOOL SHInvokePrinterCommand(HWND hwnd,

UINT uAction,

LPCTSTR lpBuf1,

LPCTSTR lpBuf2,

BOOL fModal);

 

参数

描述

hwnd

由函数显示的任何对话框或窗口的父窗口

uAction

打印机执行活动的标识代码

lpBuf1

包含相关活动附加信息的缓冲,总是打印机名。

lpBuf2

包含相关活动附加信息的缓冲

fModal

如果设置为TRUE,函数返回之前必须等待活动完成。

 

唤醒打印机命令

         uAction参数可以是下面显示的值:

 

命令

描述

PRINTACTION_OPEN

打开显示打印机状态的窗口

PRINTACTION_PROPERTIES

显示打印机的属性

PRINTACTION_TESTPAGE

打印测试页

PRINTACTION_OPENNETPRN

PRINTACTION_OPEN相同,但是是网络打印机

PRINTACTION_NETINSTALL

安装指定的网络打印机

PRINTACTION_NETINSTALLLINK

建立指定网络打印机的快捷方式,lpBuf2指向这个快捷方式的路径。

 

在所有情况下lpBuf1都指向打印机名,可以是本地的,网络的或共享的。如果是网络打印机,名字必须遵循UNC格式(//server/printer)。相对的lpBuf2参数仅与PRINTACTION_NETINSTALLLINK标志一起使用。在NT下还有另外两个标志支持网络打印机:

PRINTACTION_SERVERPROPERTIES, 显示服务器属性

PRINTACTION_DOCUMENTDEFAULTS, 显示打印机上默认文档属性

通常,可以使用AppWizard快速建立测试SHInvokePrinterCommand()函数的应用。

 

函数的返回

         资料说明函数成功返回非零值,但是经过测试,我们不能使它返回 0 值,这个函数即使在指定了不存在的打印机的情况下也顽强地返回TRUE。与执行命令相关的是可能获得错误消息,告诉你发生了什么。

 

拨号网络

         ‘拨号网络’是一个虚拟文件夹,它聚集了所有可用的Internet和网络连接。相关的函数由rnaui.dll输出,是RnaDial()(拨号连接)RnaWizard()(建立新连接)。这两个函数都支持rundll32接口,因而可以由前面描述过的方法运行。如果想要连接到Internet而不使用特定的连接,你可以求助于WinInetAPI。尤其是InternetAutoDial()函数可以通过定义在‘拨号网络’文件夹中的默认连接连接上网。

 

离线浏览

         IE4.0引进了离线浏览——一种浏览模式,从本地专有缓存中浏览页面。你可以通过WinInet API中的InternetQueryOption()函数感知这种状态,而InternetGoOnline()函数则导出选择对话框,询问是否连接或保持离线状态。所有这些科目都在Internet客户SDK资料中有详细说明,更多的关于WinInet API资料请参见在线帮助。

 

任务调度

         任务调度(调度代理)是一个与Windows95NT4.0下活动桌面一起引进的模块,在后来的Windows98中也包含了这个模块。它的主要用途就是提供在特殊时间或预定义的状态出现时运行指定任务的能力。从程序员的角度上看,任务调度器是一个COM服务器,它给出定义任务和触发任务执行的功能。基本上,任务调度是一个简单的监视器应用程序,它花费所有时间监视一定的日期、时间、周的组合,以及标志为‘感兴趣’的时间,然后执行要求的活动。

 

Windows NT支持的调度

         这个与Shell4.71版一起引进的调度代理是一个功能上类似于NT提供的AT命令的应用程序。它与AT的差别是采用了COM接口,而AT则是NetSchedule API

 

调度代理

         Windows9x的调度代理是mstask.exe程序,而在NT下它是Schedule服务。这个代理默认情况下不启动,除非运行的是Windows98,代理也包含一个特殊文件夹,用来保持被调度任务的轨迹。Internet客户SDK提供了几段代码说明怎样启动和驱动这个代理。

    代理管理的对象是任务,任务基本上是可执行文件,在每一个任务上可以有一个或多个触发条件,用以确定什么时候运行它。

    调度代理是一个服务器应用程序,它管理所有确定的任务,在正确的时间导出任务,以及返回最近执行时间和全部可执行任务数信息等。调度代理的功能完全由ITaskScheduler接口描述。

 

任务和触发器

         任务IScheduledWorkItemITask接口描述,而且接口输出了与在快捷方式中看到的完全不同的方法。你可以使用诸如应用名、工作目录、参数、优先级、最大允许执行时间,以及——最重要的——触发器等信息设置任务。ITaskIScheduledWorkItem中导出,是这个接口的一个更特殊的版本。在将来可以定义很多不同类型的工作项,但是现在它仅仅支持任务项。任务可以是32位或16位的应用,OS2MS-DOS应用,批处理文件(*.bat),命令文件(*.cmd),或注册的任何文件类型的处理器应用。

    触发器是用于辨识正确时段以运行工作项的事件。在许多情况下,触发器是一个唯一的时间,如"12:00:00 1998/12/3 "。另一些情况下,它是可重复的,如"6:00:00 在每个月的第三个星期一"。用ITaskTrigger接口和TASK_TRIGGER结构操作触发器,它定义了任务的开始时间、重复频率和参数。

    任务调度所涉及的所有相关接口和结构在Internet客户SDK中都有完整的资料说明。你可以参考这个帮助文档获得更详细的信息和示例。

 

我的公文包

         ‘我的公文包’是Windows95上的一个实用程序,设计用于帮助用户在不同计算机上维护相同文档的多份拷贝。一旦你把文档放入‘我的公文包’文件夹,这个软件将注意保持公文包中的备份与初始文档同步。当你使用桌面PC和笔记本电脑协同工作时,这个同步是有用的。通常把桌面机器中的文档作为初始文档,并可以用来自笔记本电脑修改过的版本替换这个文档,只需在‘我的公文包’中点击这个文件名,它就可以检查和同步这个备份:

                  

如果在上一次同步操作后有一个文件已经改变,Windows自动用修改过的备份替换未修改的文件。如果两个文件都有改变,就发生合并操作。合并操作涉及到要实现某些接口来专门处理文档的合并:

               

实现这些接口的对象称为协调者(reconcilers),用来确定相同文件的两个版本文档是否要并列。如果两个文档都有改变,这个模块可以提供合并内容产生新的备份。也可以根据需要进行交互操作,和放弃残余的文件。精确的合并操作依赖于协调者的特殊实现。

         关于协调者的详细信息可以在Internet客户SDK资料中找到。

 

零碎对象

你是否从未从微软的Word文档中选择一段文字,然后移动或拷贝它到Windows的桌面上。这样做时,鼠标的光标变成鼓励操作的矢量而不是禁止符号。也就是说,你可以直接拖动Word文档的文字段到桌面(或其它Windows文件夹)。此时,你建立了一个零碎对象

                                  

基本上你所看到的是对象的一个连接,零碎文档是一个具有.shs扩展名的文件,它在IDataObject实现的对象被拖动到文件夹上或桌面时自动建立。要读出零碎文档的内容有一个由shscrap.dll输出的rundll32兼容的函数。这个库在‘System’目录下,是没有正规资料说明的,这是因为零碎文档的支持应该自动由建立它的完全OLE应用所提供。然而,如果你有兴趣测试,shscrap.dll输出的函数OpenScrap_RunDLL()可以用于打开任何.shs文件。

 

新快捷方式处理器

         到目前为止在这本书中我们讨论了一定数量的课题,也讨论了在应用中集成特殊特征的方法。然而在这一章中,我们开始探索Shell的组成部分,主要集中在文件夹和命令行方面。现在,我们打算介绍一个有意义的例子,它使用了我们解释过的许多基本课题(快捷方式,图标,和文件夹),以及未来将要涉及的Shell客户化技术。我们打算组成一个类似于Windows标准部件的工具,然后探索怎样用我们自己的工具替换Windows标准部件。

         如果试图通过右击在桌面或任何探测器文件夹上建立一个快捷方式,或通过选择‘文件 | 新文件 | 快捷方式’菜单建立快捷方式,则出现一个建立大师,它允许你指定目标对象和要建立的文件名。.lnk文件的目标文件夹总是设定在进程启动的文件夹上。不能指定描述文字,热键,甚至图标,也不能决定在哪里建立这个快捷方式。

               

为了绕过这些限制,我们打算构建我们自己的快捷方式建立器,更进一步,我们还打算用它来取代Windows标准的快捷方式大师工具。用这种方法,每次右击建立新的快捷方式时,无论在Shell的任何地方,我们的应用都替代标准对话框弹出。

 

用户界面

         这个建立快捷方式的新大师将提供全部目标文件对象的输入,如,描述,热键和图标。此外还允许用户选择路经和最终快捷方式的名。路经可以用绝对驱动器和目录方式表示,也可以用指定文件夹的ID方式表示,例如,‘桌面’,‘发送到’,‘程序’,‘开始菜单’等。

         

上面截图显示了这个应用的用户界面。头一个编辑框包含有目标文件名,这可以浏览选择。第二个编辑框是快捷方式描述文字,而第三个指定用于唤醒快捷方式的组合键。

         下一个区域可以选择快捷方式相关的图标,既可以选择源文件(使用‘选择图标路经’按钮)也可以选择文件内图标的索引,我们使用SHBrowseForIcon()函数(在第9章中定义的)实现这个功能。在‘Save as’区域,可以键入路径名或从下拉框选取预定义项,预定义项是特殊文件夹,如‘桌面’,‘发送到’等。右手边的编辑框包含了快捷方式文件名,没有.lnk扩展名。最后,要建立快捷方式还需要按‘建立’按钮来唤醒SHCreateShortcutEx()函数,这是在第6章中定义的函数。

    到此,我们的应用是基于AppWizard生成的对话框应用——我们称之为NewLink。当从Shell调用这个程序时(我们将简短地说明怎样做),它接收一个临时文件名,这是Shell在唤醒这个大师时自动建立的。这个名字被传递给WinMain()函数的lpsz变量,由于我们对它不感兴趣,因此,第一个活动就是删除这个文件。而后我们却使用这个名字作为输出的.lnk文件。

 

旧函数

         完成对话框界面布局之后,下面我们开始编码WinMain()函数,删除临时文件,但是保留名字为以后使用:

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevious,LPTSTR lpsz, int iCmd)

{

// 删除任何由Shell建立的临时文件

if(lstrlen(lpsz))

DeleteFile(lpsz);

// 保留全程数据

g_hIconLarge = static_cast<HICON>(LoadImage(hInstance, "APP_ICON", IMAGE_ICON,

GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CXICON), 0));

g_hIconSmall = static_cast<HICON>(LoadImage(hInstance, "APP_ICON", IMAGE_ICON,

GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CXSMICON), 0));

lstrcpy(g_szNewLinkName, lpsz);

// 允许通用控件

INITCOMMONCONTROLSEX iccex;

iccex.dwSize = sizeof(INITCOMMONCONTROLSEX);

iccex.dwICC = ICC_WIN95_CLASSES;

InitCommonControlsEx(&iccex);

// 初始化COM 以使用SHCreateShortcutEx()函数

CoInitialize(NULL);

// 运行主对话框

BOOL b = DialogBox(hInstance, "DLG_MAIN", NULL, APP_DlgProc);

CoUninitialize()

// 退出

DestroyIcon(g_hIconLarge);

DestroyIcon(g_hIconSmall);

return b;

}

对话框过程调用所有新按钮的处理器:

case WM_COMMAND:

switch(wParam)

{

case IDC_CREATE:

DoCreateShortcut(hDlg);

return FALSE;

case IDC_BROWSEPATH:

OnBrowse(hDlg, IDC_PATH);

return FALSE;

case IDC_CHOOSEICON:

OnChooseIcon(hDlg);

return FALSE;

case IDC_BROWSETARGET:

OnBrowse(hDlg, IDC_TARGET);

return FALSE;

case IDC_BROWSEICON:

OnBrowse(hDlg, IDC_ICONPATH);

return FALSE;

case IDCANCEL:

EndDialog(hDlg, FALSE);

return FALSE;

}

break;

后三个是预定义的函数,OnInitDialog()初始化所有控件:

void OnInitDialog(HWND hDlg)

{

// 设置图标(T/F /小图标)

SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));

SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));

// 指定可用的文件夹

HWND hwndCbo = GetDlgItem(hDlg, IDC_PATH);

int i = ComboBox_AddString(hwndCbo, "Desktop");

ComboBox_SetItemData(hwndCbo, i, CSIDL_DESKTOP);

i = ComboBox_AddString(hwndCbo, "Favorites");

ComboBox_SetItemData(hwndCbo, i, CSIDL_FAVORITES);

i = ComboBox_AddString(hwndCbo, "Programs");

ComboBox_SetItemData(hwndCbo, i, CSIDL_PROGRAMS);

i = ComboBox_AddString(hwndCbo, "My Documents");

ComboBox_SetItemData(hwndCbo, i, CSIDL_PERSONAL);

i = ComboBox_AddString(hwndCbo, "SendTo");

ComboBox_SetItemData(hwndCbo, i, CSIDL_SENDTO);

i = ComboBox_AddString(hwndCbo, "Start Menu");

ComboBox_SetItemData(hwndCbo, i, CSIDL_STARTMENU);

ComboBox_SetCurSel(hwndCbo, 0);

// 初始化热键控件,加前缀 Ctrl-Alt

SendDlgItemMessage(hDlg, IDC_HOTKEY, HKM_SETRULES,

HKCOMB_NONE | HKCOMB_S | HKCOMB_A | HKCOMB_C,

HOTKEYF_CONTROL | HOTKEYF_ALT);

SetDlgItemText(hDlg, IDC_TARGET, "C://");

SetDlgItemText(hDlg, IDC_ICONINDEX, "0");

// 处理通过命令行接收的文件名

if(lstrlen(g_szNewLinkName))

{

LPTSTR pszBuf = g_szNewLinkName;

LPTSTR psz = strrchr(g_szNewLinkName, '//');

SetDlgItemText(hDlg, IDC_LNKFILE, ++psz);

pszBuf[psz - pszBuf] = 0;

SetDlgItemText(hDlg, IDC_PATH, pszBuf);

}

else

SetDlgItemText(hDlg, IDC_LNKFILE, "NewLink");

}

开始的活动是使用特殊文件夹名充填‘Save As’中的下拉框内容,这些特殊文件夹可以接收我们建立的快捷方式文件。为了使进一步的处理容易些,我们为每一个串关联了一些项数据。接下来的工作是初始化热键控件。加一个前缀Ctrl-Alt,对于快捷方式这是必须的。然后设置目标串和图标索引的默认值,使用命令行上传递来的文件名作为连接名的基础。

 

新函数

         第二个处理器函数,也是我们加到应用中的第一个新函数,在对话框含有的三个浏览按钮中的任何一个被按下时被调用,第二个变量用于区分这三个按钮:

void OnBrowse(HWND hDlg, WPARAM wItemType)

{

// 仅浏览目录...

if(wItemType == IDC_PATH)

{

LPMALLOC pMalloc = NULL;

TCHAR szDir[MAX_PATH] = {0};

LPITEMIDLIST pidl = NULL;

BROWSEINFO bi;

ZeroMemory(&bi, sizeof(BROWSEINFO));

bi.hwndOwner = hDlg;

bi.lpszTitle = "Choose a folder:";

pidl = SHBrowseForFolder(&bi);

SHGetPathFromIDList(pidl, szDir);

SetDlgItemText(hDlg, IDC_PATH, szDir);

SHGetMalloc(&pMalloc);

pMalloc->Free(pidl);

pMalloc->Release();

return;

}

// 浏览文件...

TCHAR szFile[MAX_PATH] = {0};

OPENFILENAME ofn;

ZeroMemory(&ofn, sizeof(OPENFILENAME));

ofn.lStructSize = sizeof(OPENFILENAME);

switch(wItemType)

{

case IDC_TARGET:

ofn.lpstrFilter = "All files/0*.*/0";

break;

case IDC_ICONPATH:

ofn.lpstrFilter = "Icons/0*.exe;*.dll;*.ico/0";

break;

}

TCHAR szWinDir[MAX_PATH] = {0};

ofn.nMaxFile = MAX_PATH;

GetWindowsDirectory(szWinDir, MAX_PATH);

ofn.lpstrInitialDir = szWinDir;

ofn.lpstrFile = szFile;

if(!GetOpenFileName(&ofn))

return;

SetDlgItemText(hDlg, wItemType, ofn.lpstrFile);

// 默认显示头一个图标

HICON hIcon = ExtractIcon(GetModuleHandle(NULL), ofn.lpstrFile, 0);

SendDlgItemMessage(hDlg, IDI_ICON, STM_SETICON,

reinterpret_cast<WPARAM>(hIcon), 0);

}

如果正在浏览路经,我们可以使用SHBrowseForFolder() API函数使用户找到路经并将其显示在适当的编辑控件上,然后返回。如果在浏览文件,我们填写适合于要查找文件类型的滤波串,然后调用GetOpenFileName(),把文件名显示在适当的编辑控件上,并且把文件的第一个图标作为默认图标显示。

         ‘选择图标路经’按钮的处理器简单地使用SHBrowseForIcon()函数,它是第9章从文件中选择图标的函数:

void OnChooseIcon(HWND hDlg)

{

TCHAR szFileName[MAX_PATH] = {0};

GetDlgItemText(hDlg, IDC_ICONPATH, szFileName, MAX_PATH);

HICON hIcon;

int iIconIndex = SHBrowseForIcon(szFileName, &hIcon);

if(iIconIndex >= 0)

{

SetDlgItemText(hDlg, IDC_ICONPATH, szFileName);

SetDlgItemInt(hDlg, IDC_ICONINDEX, iIconIndex, TRUE);

SendDlgItemMessage(hDlg, IDI_ICON, STM_SETICON,

reinterpret_cast<WPARAM>(hIcon), 0);

}

}

在获得了所有数据之后,这个应用的主要工作就是‘建立’按钮的处理器:

void DoCreateShortcut(HWND hDlg)

{

TCHAR szTarget[MAX_PATH] = {0};

TCHAR szDesc[MAX_PATH] = {0};

// 取得热键

SHORTCUTSTRUCT ss;

ss.wHotKey = static_cast<WORD>(SendDlgItemMessage(

hDlg, IDC_HOTKEY, HKM_GETHOTKEY, 0, 0));

// 取得目标和描述

GetDlgItemText(hDlg, IDC_TARGET, szTarget, MAX_PATH);

GetDlgItemText( hDlg, IDC_DESCRIPTION, szDesc, MAX_PATH);

ss.pszTarget = szTarget;

ss.pszDesc = szDesc;

// 取得图标

TCHAR szIcon[MAX_PATH] = {0};

GetDlgItemText(hDlg, IDC_ICONPATH, szIcon, MAX_PATH);

ss.pszIconPath = szIcon;

ss.wIconIndex = 0;

// 确定快捷方式文件名

// 取得目标文件夹和最后的反斜杠

HWND hwndCbo = GetDlgItem(hDlg, IDC_PATH);

int i = ComboBox_GetCurSel(hwndCbo);

DWORD nFolder = ComboBox_GetItemData(hwndCbo, i);

TCHAR szPath[MAX_PATH]= {0};

if(nFolder)

SHGetSpecialFolderPath(hDlg, szPath, nFolder, FALSE);

else

GetDlgItemText(hDlg, IDC_PATH, szPath, MAX_PATH);

if(szPath[lstrlen(szPath) - 1] != '//')

lstrcat(szPath, "//");

TCHAR szLnkFile[MAX_PATH] = {0};

GetDlgItemText(hDlg, IDC_LNKFILE, szLnkFile, MAX_PATH);

lstrcat(szPath, szLnkFile);

lstrcat(szPath, ".lnk");

// 建立...

SHCreateShortcutEx(szPath, &ss);

}

这里做的所有事情就是收集屏幕上各个控件的信息,封装信息到SHORTCUTSTRUCT结构,然后调用SHCreateShortCutEx()做实际建立的操作。(这里所涉及的函数和结构在第6章中已经定义了)。要编译这段代码,你需要添加通常的头文件和连接库,需要#includes resource.h, shlobj.h commdlg.h, 以及连接ole32.lib comdlg32.lib.库。

 

怎样替换Windows的大师

         如果能用我们的应用替换掉Windows标准的建立快捷方式大师程序就好了。事实上这并不困难,就像我们期望的,关键在于注册表。快捷方式是.lnk类型的文件,所以头一个要查看的地方应该是:

HKEY_CLASSES_ROOT

/.lnk

在这个键下,我们发现了ShellNew键。在从Shell建立一个给定类型的新文件时——即,通过‘新文件’菜单——探测器总是搜索文件类子树的ShellNew键。进入这个键,‘命令’的值显示了命令行,你应该看到,它设置为:

runDLL32 AppWiz.Cpl,NewLinkHere %2

  

 

注意,使用rundll32.exe来运行DLL函数作为命令行指令。替换标准的大师所有做的全部工作就是改变‘命令’的值,使之执行我们的程序,如下:

c:/Utility/NewLink/NewLink.exe %2

注意最后的%2是使命令行正常工作的基础——删除或置换都将使对话框消失。当你从桌面选择建立新快捷方式时,将会看到我们的对话框出现。

         NewLink.exe程序感知和使用由Shell作为变量传递的文件名——在唤醒快捷方式建立器之前,Shell总是建立一个空文件并把名字传递给程序,然而对于我们,这个文件名的处理不是问题。

 

编辑注册表

         替换默认的快捷方式大师需要编辑注册表,这可以通过注册表编辑器手动完成,或使用脚本文件编程实现。例如,你可以恢复初始状态,简单的把初始值赋给‘命令’实体就可以了:

; restore.reg

REGEDIT4

[HKEY_CLASSES_ROOT/.lnk/ShellNew]

"Command" = "RunDLL32 appwiz.cpl,NewLinkHere %2"

用相同的方法,你可以用脚本来安装处理器到同一个地方:

; replace.reg

REGEDIT4

[HKEY_CLASSES_ROOT/.lnk/ShellNew]

"Command" = "c://utility//newlink//newlink.exe %2"

一定要保证在.reg脚本中输入路径时总是使用双斜线,还要记住例子中置换的路径应该是newlink.exe文件所在的路径。

 

小结

         我们第一次Windows Shell的旅行到这里就结束了。我们已经视察了探测器的命令行,并且发现了一个有趣的实用程序rundll32.exe,这个程序允许我们以命令行的形式使用DLL函数。在讨论rundll32.exe特征期间,我们还探索了怎样编程访问系统对话框,其编程接口是没有说明资料的,例如‘添加打印机大师’,‘添加新硬件’,‘建立新连接’和‘打开文件’。

这一章的第二部分讨论了某些特殊的虚拟文件夹,它们实现为Shell对象,例如‘打印机’,‘拨号网络’‘任务调度’和‘我的公文包’。我们给出了这些项目的概览,和进一步资料的来源。最后,我们给出了结合许多曾经讨论过的科目的一个例子。这个快捷方式的处理器还初步说明了我们将在第14章讨论的科目。概括地讲,这一章包含了:

         探测器命令行

         RunDLL32编程接口

         访问无资料函数显示系统对话框

         回顾某些虚拟文件夹如‘打印机’和‘我的公文包’

         零碎对象

         怎样编写和安装新的,客户建立快捷方式的模块。

在下两章中我们继续探索和发掘两个方面的有用技术,一是脚本Shell对象,这使你能编程访问任何Shell特征,从对话框到文件夹,从窗口到快捷方式。此后我们聚焦于非常有前途的子系统——Windows脚本环境(WSH),这使DOS批处理文件的概念被带到了Windows中。

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

愚鬼

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

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

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

打赏作者

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

抵扣说明:

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

余额充值