第十四章 设计Shell集成应用
有一些工具可以使应用程序更紧密地与Shell和底层系统进行集成。也就是说,用户可以象处理系统文档和程序那样处理你的文档和程序。例如,右击文件来显示可用功能列表等。Windows为每一个文件提供默认的功能集,如‘打开…’,‘属性’,‘拷贝’等。是否能为特定的文档增加特殊功能。为此,我们必须客户化这个文档类的关联菜单。
另一个应该与Shell集成的例子是:假设你的程序有建立空文档的能力,用户使用系统的‘新建’菜单项在任一文件夹上飞快地建立新文档,要想如此,就必须在系统注册表中记入一些信息。
当然,这是特殊情形,作为开发人员和应用设计人员还应该重视许多其它有用的特征。在这一章中我们将讨论Shell集成的各方面技术,以帮助开发者将应用与系统Shell无缝集成,使你的产品更专业。这些技术包括:
怎样客户化关联菜单
怎样注册新文件类型
怎样设计和编程处理命令行
怎样编程定制‘打开’对话框
我们将设计一个基于文档的全属性应用,这个应用显示和打印所有Windows支持的元文件,从传统的(*.wmf)到增强的(*.emf)。我们将应用所有我们前面讨论过的理论技术,以及Shell对元文件的丰富支持。
Shell集成应用
头一个问题显然是Shell集成应用的组成结构是怎样的。就Win32的面向文档程序概念,有几个关系到系统Shell的特征需要给出。简单地说,有三组定义Shell集成应用的特征:
为程序处理的任何文档注册图标和类型名
为程序处理的文档客户化关联菜单
在系统的‘新建’菜单中可能有一个或多个客户化条目
单一程序实例
对每一个打开过的文档在‘最近文档’文件夹中有一个新条目
完全支持长文件名,尤其是对于用户文档。
除了这些基本特征外,我们还需增加一些不常使用的特征:
在系统的‘发送到’菜单中客户化一个或多个条目
在‘开始’和‘程序’菜单中客户化一个或多个条目
在‘Favorites’文件夹中客户化一个或多个项
在桌面上生成一个或多个新快捷方式
有一个应用桌面工具条来集中程序的所有主要功能
客户化某些系统公共对话框
在用户下一次登录时注册应用自动启动
第三组特征主要受限于特定安装器,如InstallShield 和WISE,它们是:
拷贝共享文件到系统公共路径
在‘程序文件’文件夹下安装应用
提供卸载程序
探测Shell的应用路径名来定义查找文件的路径
这些要求来自于Windows标记规范,然而对于Windows环境下更高层次的抽象概念而言它们是最基础的:为了打开和使用文档,用户不必知道实际装入和显示文档的是什么程序,只需找到和双击文档的图标和这个文档的名字即可(实际在桌面设置中也可以设置单机打开方式)。
文档和Shell
在Windows95发布以后,文档成为系统Shell更中心的角色,文档已经成为了主角,而实际处理它的程序则缩减成配角。甚至它们在硬盘驱动器上的位置都标明其状态的低下:程序被分组在‘程序文件’文件夹下,每一个都有自己的子文件夹,以及一个存储DLL和其它帮助文件的子目录。这样的许多文件夹都是隐藏的——这进一步确认程序相对于文档是次要的。
查看上面的截图,我们可以看到文档有它们自己的图标和描述,更进一步,每一个文档都有专门的关联菜单,从菜单中可以执行一些Shell功能。有些功能可以应用于各种文档,因而出现在所有文档的关联菜单中,有些则是个别文档类型所具有的。
基本的文档功能
Windows Shell提供一些菜单动词,可以自由使用,它们是:
拷贝、剪切、粘贴
删除
重命名
建立快捷方式
属性
此外,还有两个菜单命令,‘打开’或‘打开方式…’,但是这两个是相互排斥的——后者仅仅在没有注册程序打开指定文档时出现,并且导出下面的对话框:
相反‘打开’命令则依赖于存储在注册表中的条目内容,这就是我们将要讨论的。
‘发送到’命令
另一个我们总能看到的命令是‘发送到’,它有一个子菜单。这个子菜单列出了选中文档可能的目的地。‘目的地’是一个在命令行上接收给定文件名的程序。下图说明‘发送到’菜单是怎样把文件设置成新邮件的附件:
通过我们在这一节中列出的命令,Shell保证了对PC上各种文档的最低层支持。对于有经验的用户和软件工程师,这就等于提供了使用更多的文档特定特征扩展这些基本行为的机会。
注册文档类型
关于Shell构造的所有信息都存储在系统注册表中,所以修改Shell表现或行为的任何方法都必须通过注册表。
为了使Shell识别和适当处理一定种类的文档,它就必须是一个注册类型。一个文档的类型由它的文件名的扩展所标识,而且所有注册的文档类型都存储在HKEY_CLASSES_ROOT注册表节点下:
文件扩展(.ext)的条目指向同一个节点下的另一个键,其名字存储在.ext的默认值中,在上图中,对EML文件(微软邮件消息,Outlook Express格式文件),其值为:
Microsoft Internet Mail Message
如果需要获取这个文档类型的注册信息,你就必须探索:
HKEY_CLASSES_ROOT
/Microsoft Internet Mail Message
在这个键下,存储了应用于三个方面的信息:
用户接口
关联菜单
Shell扩展
文档的Shell用户接口
从这一节的标题可以看出,我们主要是想说明设置文档的图像属性——即图标和类型名。‘DefaultIcon’键使你可以指派图标到特定类型的所有文件。这个键的Default值包含一个如下格式的字符串:
C:/PROGRAMS/THEPROG.EXE,0
再次注意,这个信息不是存储在.ext键下的,而是在.ext指向的键下。
这个串标识默认图标由全路径名、逗号和索引号构成。要显示的图标是给定文件中指定索引号的图标——记住图标索引总是从0开始的。如果索引是负数,则表示是一个资源ID,比如对于EML,DefaultIcon串为:
C:/PROGRAM FILES/OUTLOOK EXPRESS/MSIMN.EXE,-4
正象上面显示的,主键(上面示例中是Microsoft Internet Mail Message)的默认值包含了用于这个文档类型名的串。要修改这个设置并不需要专门的Windows程序员进行,任何老练的Windows用户都可以改变EML文件的描述,或表示的图标。然而要编程地插入键来注册文档完全不同于手动修改注册表操作。我们需要集中讨论软件自动集成其文档与Shell的操作关系。
关联菜单的特定文档命令
命名为Shell的键可以包含一定数量的子键,每一个子键都对应一个特殊的命令,这是将要显示在文档关联菜单上的。在Shell键下的这些键称为动词(此时的动词是‘打开’),这些键的默认值包含了将要显示在关联菜单上的串。如果没有设置,则菜单显示键本身的名字。因此动词打开可以在菜单中显示一个相当不同的名字。在我们讨论的邮件消息这个例子中,‘Read’命令在菜单中替代了‘打开’。如果你改变了默认值的内容并且调出关联菜单,你将看到这个结果:
关联菜单显示的是‘读’,而实际行为一点也没改变,因为这是在命令子键的默认值中确立的。每一个动词必须有一个命令子键包含可执行文件路径和命令行,以及任何其它必要的设置。指定命令行以及适当的开关,用%1表示要操作的文件也是十分重要的:
C:/PROGRAM FILES/OUTLOOK EXPRESS/MSIMN.EXE" /eml:%1
上面这行显示EML文件的命令行——在你的机器上是否有相同的设置依赖于Outlook Express的安装设置。
Shell对文档的扩展
通过修改注册表你可以添加静态动词到文档的关联菜单。你所定义的任何静态动词总是被显示,并且总是执行同样的命令行。
更灵活的动态的行为可以通过Shell扩展获得,我们将在下一章中进行讨论。现在,我们可以说Shell扩展是运行在探测器地址空间中的一段代码,每次探测器需要做某个客户化行为时都调用这段代码,如绘制图标、显示关联菜单等。你的代码段给出一个动态确定菜单项添加的机会和响应用户的点击的操作。
对于给定文档类的所有Shell扩展,都列出在shellex键下,它与Shell键同层。
怎样影响程序
我们现在接触到了一定数量的影响文档的属性,并且在这一章的开始我们就说明了文档是Windows Shell的重点。然而,我们并没有脱离文档仍然通过程序显示这样一个事实——问题是,通过我们致力的Shell集成怎样和什么程度上影响到程序。
有两个重点,首先是,用户可以点击重复打开不同的文档,甚或是同一个文档的多个拷贝。当这种情况发生时,程序被重复调用,所以,为了避免窗口增殖,你可能希望只允许运行一个程序实例。其次是,程序命令行的重要性,因为静态动词通常是通过命令行的开关实现的。你应该以非常标准的方法输出最重要的功能。
在第11章中我们讨论了RunDLL32模块,它给出了通过命令行调用具有固定原型的动态库函数的好方法。在这两种情形下程序的功能都必须明显地隔离开,而且可容易地从外部模块调用。
当某人在Windows Shell下点击文档时,程序被调用,程序每次启动,都检查是否有运行中的副本,如果有,则传递控制和命令行,而当前的实例则退出,我们将在后面进一步讨论这个问题。
MDI 与 SDI
MDI和SDI是Windows基于文件应用的两个典型设计。MDI表示多文档界面,说明程序可以同时打开几个文档,每一个都有自己的窗口。相反,SDI是单文档界面的首字母缩写——SDI程序每次仅能打开一个文档。传统上主要的Windows应用都是MDI形式的——Office套件是一个典型的MDI例子,而记事本和图画程序则是SDI的例子。
从Shell的观点上看,选择MDI或SDI并不是实际的结果。然而在进一步的探索后,你可能会认识到对MDI和SDI之间差异的讨论实际是要在一个更宽泛的对比上打开窗口:应用为中心对比文档为中心的环境。
MDI方案由应用来支配,是应用打开和管理各个子文档。反之SDI界面是更加文档为中心的:你查看由可用工具环绕的单一文档,可以使用这些工具来修改它。
自从Windows95发布以后微软就开始推荐尽可能使用SDI开发应用,但是有许多人都对这个建议不感冒。
建立新文档
在任何时候你在探测器显示文件夹内容的窗口上右击,都会有下图样式的菜单出现:
‘New’命令列出了所有文档类型,这些是可以经由Shell建立的文档类型。当你选择了一个列出的文档类型后,Shell调用注册的应用,并请求它来建立一个新的文档,其名字是一个来自文档类型名(与它在菜单中的相同),前缀有一个‘New’字。例如,要建立一个新的bitmap图像文档,文件名默认为:
New Bitmap Image.bmp
New菜单
出现在‘New’菜单中的每一项(除了文件夹和快捷方式)都有相关的文件类对应的ShellNew键存在,它在下面的注册表路径上:
HKEY_CLASSES_ROOT
/.ext
/ShellNew
ShellNew键的内容确定了New菜单上显示的内容,以及当点击时所需要做的动作。实际上有四种通过Shell建立新文档的方法,你可以建立:
空,零长度文档
从默认文档拷贝的文档
来自注册表存储的二进制数据的文档
由特殊外部程序建立的文档,例如建立大师软件
很自然这些选择要求不同的注册表设置:
值 |
内容 |
NullFile |
空字符串 |
FileName |
作为模板使用的文件名。这是假设这种文件是驻留在Windows/ShellNew目录下的。 |
Data |
一块从注册表读出的二进制数据 |
Command |
建立文档所需要的命令行 |
下面图像显示对机器上的BMP文件的设置:
一般使用NULLFile设置,令应用处理空或零长度文件。FileName设置与Word和Excel密切相关,如果使用复杂的、混合文件作为文件所需要的最小结构,即使是空文件,这个设置是有用的。此时,你可以准备一个标准文件(空的或不空的),把它保存到FileNew值中,每次建立这种类型的新文件时,就建立这个文件的一个拷贝。Data是可以包含二进制数据的值,它们可以填充到新建立的文件中。这与FileNew的情况稍有不同,在FileNew中,模板是单独文件,而Data,是一块存储在注册表中的数据。
在第11章讨论置换快建立捷方式标准处理器时我们遇到过Command值。如果给出这个值,将限制Shell运行指定的命令行,并断定它将建立指定类型的新文档。这个选项由逐步建立文档的大师程序所特殊设定。
下面我们将考察一个例子,其作用是添加一个命令到Shell的‘New’菜单,建立一个新的具有最小内容的HTML文件。
建立HTML新文件
我们假设在PC上注册了一个处理HTML文件的程序。当需要从腹稿建立新的HTML文档时,通常是:
运行可视HTML编辑器(例如微软的FrontPage)
运行记事本或其它普通的文字编辑器
像绝大多数其它Windows文档一样,在你需要编写HTML页面时,需要借助‘记事本’。然而,HTML文件不是ASCII文件,它需要有标记来描述使它成为有效的浏览器可处理的文档。最小HTML文件可以有如下形式:
<html>
<body>
</body>
</html>
保存这段代码到一个命名为html4.htm的文件,并把它放置在Windows/ShellNew(或Winnt/ShellNew)目录中。然后打开注册表编辑器,添加ShellNew键到:
HKEY_CLASSES_ROOT
/.htm
下。这个新建立的键也必须给定FileName串值:
保存了这些设置后,你就能右击桌面,激活菜单:
这个图说明在PC上改变了htmlfile注册表键的初始描述为‘Web Page’之后任何从这个菜单项建立的新文件都将调用‘New Web Page.htm’。
注意:只有在文件类型被正确地注册之后,你才可以添加项目到‘New’菜单项。
其它特征
要设计和编写良好的Shell集成应用,还有两个特征需要注意。它们是:
存储目录列表到辅助模块如DLL可以找到的位置
安排在下一次登录时自动运行
第一个特征可能与设置程序有更大的关联,但是并不是所有安装器都确实完成你所要求的任务,因此需要你自己编写功能扩展,以及深入专研注册表路径。第二个特征典型是探测器和其他几个应用的特征。在关闭系统时,如果应用仍然在运行,Shell将在下一次登录时自动重新启动它。现在就让我们看一下怎样编码这个行为。
应用路径
几乎所有的Windows应用都由一个或多个文件组成。典型地是有一个EXE文件和多个DLL(不是提及的系统Dlls,如kernel32.dll和user32.dll)。辅助的DLL必须由安装器拷贝到某个地方。它们可以在‘程序’文件夹或其它地方,但是,微软反对将DLL拷贝到系统主文件夹下,如Windows或Windows/System等。如果你确定不将DLL放到与EXE文件相同的目录下,很快你就会获得系统通知的错误信息,说明系统不能定位指定的DLL。
决定不把DLL放置到与EXE相同的文件夹下是为什么。因为你的应用可以是一套共享辅助DLL的很多程序的一部分,如果每一部分都备份这些DLL,显然是一种浪费,所以,你可以建立一个公共文件夹,放置可共享的每一件东西到这个文件夹。现在的问题是怎样使Shell知道它的存在——当你导出需要确定的库的应用时,你必须保证这个库的路径是全程可视的。
MS-DOS基础程序(Windows程序也一样)依赖于PATH环境变量。类似地,对于Shell把这个置换成应用路径。要使用这个概念,在安装了应用之后,需要添加下面的注册表键(比如对于Program.exe):
HKEY_LOCAL_MACHINE
/SOFTWARE
/Microsoft
/Windows
/CurrentVersion
/App Paths
/Program.exe
这个键的默认值包含可执行文件的全路径名,如果给出,则路径值列出所有可以查找其它文件的路径:
应用的自动启动
当一个特殊用户登录时,Windows将努力读出下面的键:
HKEY_CURRENT_USER
/Software
/Microsoft
/Windows
/CurrentVersion
/RunOnce
如果这个键存在,名字被存储在键值中的任何程序都将被执行。在检查过之后所有条目都将被删除。所以,它们仅被执行一次。据此,我们可以编码使应用能够在下一次特殊用户登录时执行。注意,这仅仅是在下一次登陆时,不是每一次持续登陆时。要每一次特殊用户登录都运行应用有一个注册表条目在上面位置的Run键下。自动执行不完全是系统特征,程序必须协同操作。特别是,程序添加其本身(或任何其它应用)到RunOne键下,正确地作这个操作就是响应WM_ENDSESSION消息。当然,在任何时候都可以做,然而因为我们的目标是获得持续的交互会话,所以,只有在关闭当前会话后我们的程序仍然在运行时才建立这个条目。这也WM_ENDSESSION消息到达时。关于应用运行的信息在注册表中有如下格式:
ID = program name
你需要建立一个值,其内容是可执行程序的路径名。ID必须是唯一的,但是与你指定什么名字并不重要。下图显示一个例子:
这些条目以它们键入的顺序依次取出——这个顺序不一定符合注册表编辑器的输出顺序,在编辑器中条目是按字母顺序显示的。程序是一个接一个异步导出的,如果你有上面图中显示的注册表条目设置,你就会发现,当你登录时记事本程序在桌面上打开。
另一个RunOnce键
还有另一个比RunOnce更强的RunOnce键在下面的路径下:
HKEY_LOCAL_MACHINE
/SOFTWARE
/Microsoft
/Windows
/CurrentVersion
/RunOnce
使用这个键的语法实际与前面介绍的相同,但是,其工作方法有三方面不同,它们是:
任何用户登录,这个键的内容都被检查
各个注册程序同步执行——仅在前一程序完成之后,下一条目开始运行。
在这个键下注册的程序运行在HKEY_CURRENT_USER节点相同子键注册的程序之前。
如果你在HKEY_LOCAL_MACHINE/.../RunOnce键下注册了记事本程序,下一次登录或重启动时记事本将在任务条和桌面图标之前出现在桌面上。更要紧的是你不能看到桌面和图标,直道关闭这个窗口终止过程之后才可以。
运行键
Run键,我们在上面讨论中临时提及的键,也是在HKEY_LOCAL_MACHINE和HKEY_CURRENT_USER
下都存在的键。Run和RunOnce除了在最后删除所读出的注册条目外,它们有一致的逻辑。在Run下的项目每次用户登录时都执行。
RunServices键
在Windows95和Windows98下有两个键允许你模仿NT服务——即,在用户登录之前运行模块。它们是:
HKEY_LOCAL_MACHINE
/SOFTWARE
/Microsoft
/Windows
/CurrentVersion
/RunServices
/RunServicesOnce
它们的语法与我们前面讨论过的其它键的语法相同。RunService在每次登录之前运行应用,而RunServiceOnce则与下一次登陆作相同的操作。执行程序是异步的,在用户实际登录之后可以终止。在任何场合,比照RunOnce和Run键,所有服务都必须在系统开始之前执行,
下面的表给出启动期间,Windows操作注册表键的实际顺序:
步 |
键 |
1 |
HKLM/.../RunServicesOnce ( NT下不支持) |
2 |
HKLM/.../RunServices (NT下不支持) |
3 |
用户登录。用户可以在所有服务开始之前登录 |
4 |
所有服务开始和用户登录 |
5 |
HKLM/.../RunOnce |
6 |
所有注册程序完成 |
7 |
HKLM/.../Run |
8 |
HKCU/.../Run |
9 |
当前用户包含在Startup文件夹下的程序 |
10 |
HKCU/.../RunOnce |
Winlogon键
如果在任何用户登录之前,仅需要简单地显示信息,你可以探索下面键的注册条目:
HKEY_LOCAL_MACHINE
/SOFTWARE
/Microsoft
/Windows
/CurrentVersion
/Winlogon
LegalNoticeCaption和LegalNoticeText值可以定义系统消息框的标题和文字,这个消息框将在任何用户登录之前出现。
Windows 9x的服务
在WindowsNT下你可以编写服务来实现要求系统特权的特定任务。NT服务是具有特殊结构和行为的Win32应用。除了特殊的实现细节外,服务的主要特征可以概括为以下几点:
服务在任何用户登录之前运行
服务持续运行,即使用户注销也不终止
服务没有用户界面,不是交互的
服务从操作系统取得特殊待遇——如,它可以在包括系统帐号的任何用户帐号下自动启动和运行
服务运行在单独的虚拟桌面上,它不同于应用使用的桌面
服务可以停止或暂停
WindowsNT有一个特殊的控件称为服务控制管理器(SCM),它管理运行中的服务。由于这个接口非常强,因此在NT下不需要RunServices和RunServicesOnce注册表键帮忙。
我们的目的是通过探索RunServices键,你可以仿真NT服务,以获得大致相同的行为。Windows9x的服务正常是Win32应用(无论是否为GUI或控制台应用),仅是简单地注册在RunServices下在登陆之前运行的程序。
通过调用RegisterServiceProcess() API函数,你可以注册当前进程(或任何其它运行中的进程)为服务。以使它持续工作即使用户注销也不停止。这个函数并不在任何内部库中输出,因此,需要动态经由GetProcAddress()加载,它包含在kernel32.dll中。
下表列出WindowsNT与Windows9x服务的区别:
WindowsNT服务 |
Windows9x服务 |
输出ServiceMain()函数的Win32应用 |
传统的Win32应用 |
在登陆之前运行 |
如果注册在RunServices之下,在登陆之前运行 |
在注销后继续运行 |
如果使用RegisterServiceProcess()注册为服务,在注销后继续运行 |
一个没有用户界面的GUI或控制台应用 |
一个没有用户界面的GUI或控制台应用 |
可在系统帐号下运行 |
不支持 |
运行在单独桌面上 |
不支持 |
可以停止或暂停 |
仅可以通过调用TerminateProcess()停止 |
服务必须没有用户界面并不是系统要求的,但确实合理并强烈推荐。
设计Shell集成的应用
到目前为止我们已经调查了应用程序与Shell集成的主要技术。现在我们给出一个具体的例子来说明实际程序的设计原理和规则。
对于面向Shell的应用程序,第一个要求是程序应该是基于文件的。这就是说,应用必须是围绕一定种类文档操作而工作的。其菜单应该诚实地呈现出对处理文档的操作活动,所以在设计Shell集成应用中你应该清楚地知道哪些功能是要通过Shell展示的。
其次,这些功能必须尽可能模块化编码,并且通过命令行、RunDLL32或Shell扩展的方法可以访问它们,下面我们就研究怎样来吸纳这些建议。
元文件观察器
我们将要开发的这个应用是元文件观察器。选择这个例子有两个原因:
它是一个有意义的基于文件的应用
在Windows中没有系统实用程序来查看WMF和WMF文件
要说明第二点,当前唯一观察元文件的方法是打开文件夹的‘观察 | 作为Web页面’选项,并且有赖于一个嵌入的小控件(当然,不难获得这个共享件使用程序,但是这仍然可以说明这样的实用程序提供了足够的Shell层支持)。
我们的例子是一个简单的基于对话框的应用,它允许你选择,显示,打印和转换任何Windows元文件。下面的截图显示这个样例程序的外观,使用AppWizard建立一个称之为WMFView的基于对话框的应用:
我们首先调查怎样使这个程序能实际显示元文件,而后怎样增强它的代码求得关联菜单客户化的帮助。
Windows元文件和增强元文件
元文件是一些称作记录的图形指令集,它们以产生图像的顺序一个接一个地执行。在Win32以前有两种元文件:
Windows元文件
可定位元文件
例如,Office的剪裁文件就全部是可定位元文件,这两种类型的元文件通常都有.wmf扩展名。
关于元文件的详细说明超出了本书的范围,你可以参考MSDN库的帮助。
随着Win32平台的出现,Windows元文件的格式也发生了变化。Win32提出了更新的.emf格式(增强元文件),但是仍然提供对老的WMF型元文件的支持。
显然,API函数更多地关注增强元文件,值得注意的是,在Win32公共控件中是‘图像’控件才有能力显示增强元文件,因而它打开和显示EMF也是直观的,然而不幸的是,对于老的WMF文件就不是那么容易的了。很幸运,我们发现了一个工具在微软的Web站点上:
http://support.microsoft.com/download/support/mslfiles/enmeta.exe
因此我们能够使用这个例子作为参考建立我们自己的源文件观察器。
显示元文件
wmfview.exe程序识别三种类型的元文件:
Windows元文件
可定位元文件
增强元文件
头两个有.wmf扩展名,最后一个有.emf扩展名。无论当前打开文件的初始格式是什么,程序总是内部使用增强元文件格式。下面代码显示怎样打开和显示元文件,无论其初始格式如何:
//////////////////////////////////////////////////////////////////
// 需要处理16位可定位元文件
#pragma pack(push)
#pragma pack(2)
typedef struct{
DWORD dwKey;
WORD hmf;
SMALL_RECT bbox;
WORD wInch;
DWORD dwReserved;
WORD wCheckSum;
} APMHEADER, *LPAPMHEADER;
#pragma pack(pop)
//////////////////////////////////////////////////////////////////
// 获取Handle和显示指定的元文件
void DisplayMetaFile(HWND hwndMeta, LPTSTR szFile)
{
// 取得元文件的Handle
HENHMETAFILE hemf = GetMetaFileHandle(szFile);
if(hemf == NULL)
{
MessageBox(NULL, __TEXT("不能处理这个文件."),
szFile, MB_OK | MB_ICONSTOP);
return;
}
// 释放老文件并显示新文件
HENHMETAFILE hemfOld = reinterpret_cast<HENHMETAFILE>(
SendMessage(hwndMeta, STM_GETIMAGE, IMAGE_ENHMETAFILE, 0));
if(hemfOld)
DeleteEnhMetaFile(hemfOld);
// hwndMeta 是图像控件
SendMessage(hwndMeta, STM_SETIMAGE, IMAGE_ENHMETAFILE,
Reinterpret_cast<LPARAM>(hemf));
lstrcpy(g_szCurFile, szFile);
}
DisplayMetaFile()函数调用GetMetaFileHandle()辅助函数来获取传递来的元文件Handle,删除任何当前显示,然后发送消息到控件使它显示新的元文件。
// 对指定文件恢复它的 HENHMETAFILE Handle
HENHMETAFILE GetMetaFileHandle(LPTSTR szFile)
{
DWORD dwSize = 0;
LPBYTE pb = NULL;
// 试着作为EMF读取文件
HENHMETAFILE hEMF = GetEnhMetaFile(szFile);
if(hEMF)
return hEMF;
// 试着作为WMF读取文件
HMETAFILE hWMF = GetMetaFile(szFile);
if(hWMF)
{
dwSize = GetMetaFileBitsEx(hWMF, 0, NULL);
if(dwSize == 0)
{
DeleteMetaFile(hWMF);
return NULL;
}
// 分配足够的内存
pb = new BYTE[dwSize];
if(pb == NULL)
{
DeleteMetaFile(hWMF);
return NULL;
}
// 取得元文件的位
dwSize = GetMetaFileBitsEx(hWMF, dwSize, pb);
if(dwSize == 0)
{
delete [] pb;
DeleteMetaFile(hWMF);
return NULL;
}
// 转换成 EMF
hEMF = SetWinMetaFileBits(dwSize, pb, NULL, NULL);
// 清理
DeleteMetaFile(hWMF);
delete [] pb;
return hEMF;
}
// 试着处理输入为可定位元文件
HANDLE hFile = CreateFile(szFile, GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE)
return NULL;
// 读文件到缓冲
dwSize = GetFileSize(hFile, NULL);
pb = new BYTE[dwSize];
ReadFile(hFile, pb, dwSize, &dwSize, NULL);
CloseHandle(hFile);
// 检查看是否为可定位元文件
if((reinterpret_cast<LPAPMHEADER>(pb))->dwKey != 0x9ac6cdd7l)
{
// 这个文件不可知怎样处理
delete [] pb;
return NULL;
}
// 从位信息建立增强元文件
hEMF = SetWinMetaFileBits(dwSize, &(pb[sizeof(APMHEADER)]), NULL, NULL);
delete [] pb;
return hEMF;
}
GetMetaFileHandle()的操作隐藏了它所涉及到的一般元文件和增强元文件的不同,而且上述大多数代码都是关于格式转换的。最后总是返回增强型元文件的Handle到调用它的函数。
打印和转换元文件
这个程序还可以打印元文件,或转换元文件从WMF到EMF,或从EMF到WFM。打印就是简单地取得关联的打印设备,然后在打印机上显示元文件。
void PrintMetaFile(LPTSTR szFile)
{
// 取得 EMF handle
HENHMETAFILE hEMF = GetMetaFileHandle(szFile);
if(hEMF == NULL)
return;
// 取得打印机的 DC
PRINTDLG pdlg;
ZeroMemory(&pdlg, sizeof(PRINTDLG));
pdlg.lStructSize = sizeof(PRINTDLG);
pdlg.Flags = PD_RETURNDC;
HDC hDC = NULL;
if(PrintDlg(&pdlg))
hDC = pdlg.hDC;
else
return;
// 准备打印文档
DOCINFO di;
ZeroMemory(&di, sizeof(DOCINFO));
di.cbSize = sizeof(DOCINFO);
di.lpszDocName = "Printing EMF";
// 启动打印
StartDoc(hDC, &di);
StartPage(hDC);
// 标定符合整个打印页
RECT rc;
SetRect(&rc, 0, 0, GetDeviceCaps(hDC, HORZRES), GetDeviceCaps(hDC, VERTRES));
PlayEnhMetaFile(hDC, hEMF, &rc);
// 清理
EndPage(hDC);
EndDoc(hDC);
DeleteDC(hDC);
DeleteEnhMetaFile(hEMF);
}
转换元文件也并不复杂,就像下面程序清单中说明的。三个函数里的头一个SaveMetaFile(),处理EMF存储成可定位的WMF文件(或相反),具有相同的名字,和不同的扩展名。实际上,每一个元文件都首先转换成EMF(由GetMetaFileHandle()函数),然后再作为EMF或WMF存储到磁盘。
void SaveMetaFile(LPTSTR szFile)
{
TCHAR szOutputFile[MAX_PATH] = {0};
HENHMETAFILE hEMF = GetMetaFileHandle(szFile);
if(hEMF == NULL)
return;
// 确定输出格式
lstrcpy(szOutputFile, szFile);
strlwr(szFile);
if(strstr(szFile, ".emf"))
{
PathRenameExtension(szOutputFile, ".wmf");
SaveToWMF(hEMF, szOutputFile);
}
else if(strstr(szFile, ".wmf"))
{
PathRenameExtension(szOutputFile, ".emf");
SaveToEMF(hEMF, szOutputFile);
}
DeleteEnhMetaFile(hEMF);
}
两个辅助函数SaveToEMF()和SaveToWMF()是非常简单的:
void SaveToEMF(HENHMETAFILE hEMF, LPTSTR szFile)
{
// 取得存储EMF位的内存
DWORD dwSize = GetEnhMetaFileBits(hEMF, 0, NULL);
LPBYTE pb = new BYTE[dwSize];
// 取得EMF位信息
GetEnhMetaFileBits(hEMF, dwSize, pb);
// 存储到文件
HANDLE hFile = CreateFile(szFile, GENERIC_WRITE,
0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE)
{
UINT rc = MessageBox(GetFocus(), "File exists. Overwrite?",
szFile, MB_ICONQUESTION | MB_YESNO);
if(rc == IDYES)
hFile = CreateFile(szFile, GENERIC_WRITE,
0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
else
{
delete [] pb;
return;
}
}
DWORD dwBytes;
WriteFile(hFile, pb, dwSize, &dwBytes, NULL);
CloseHandle(hFile);
delete [] pb;
}
void SaveToWMF(HENHMETAFILE hEMF, LPTSTR szFile)
{
// 取得存储WMF位信息的内存
HDC hDC = GetDC(NULL);
DWORD dwSize = GetWinMetaFileBits(hEMF, 0, NULL, MM_ANISOTROPIC, hDC);
LPBYTE pb = new BYTE[dwSize];
// 从EMFHandle中取出WMF位信息
GetWinMetaFileBits(hEMF, dwSize, pb, MM_ANISOTROPIC, hDC);
ReleaseDC(NULL, hDC);
// 存储到文件
HANDLE hFile = CreateFile(szFile, GENERIC_WRITE,
0, NULL, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE)
{
UINT rc = MessageBox(GetFocus(), "File exists. Overwrite?",
szFile, MB_ICONQUESTION|MB_YESNO);
if(rc == IDYES)
hFile = CreateFile(szFile, GENERIC_WRITE,
0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
else
{
delete [] pb;
return;
}
}
DWORD dwBytes;
WriteFile(hFile, pb, dwSize, &dwBytes, NULL);
CloseHandle(hFile);
delete [] pb;
}
它们看来对于我们的讨论似乎是多余的,然而转换函数不仅是用于显示的。尽管Win32开始就支持增强元文件,但你总能接触到WMF文件——大部分是可定位元文件。因此对于这类应用一种容易的格式转换方法是无价的。
组建观察器
要把前面给出的离散函数组装到一起组建成一个应用,我们需要在某个地方调用它们。在开发的这个一阶段我们选择对话框的相关菜单来完成这个工作(你可以使用VC++的资源编辑器的‘属性’关联菜单生成)。添加本节开始的截图上显示的菜单项,然后修改APP_DlgProc()中的WM_COMMAND处理器,如下:
case WM_COMMAND:
switch(wParam)
{
case ID_FILE_OPEN:
OnOpen(hDlg);
return FALSE;
case ID_FILE_PRINT:
OnPrint(hDlg);
return FALSE;
case ID_FILE_SAVEAS:
OnSave(hDlg);
return FALSE;
case ID_FILE_EXIT:
case IDCANCEL:
EndDialog(hDlg, FALSE);
return FALSE;
}
break;
最后,问题归结到实现三个相对容易的函数或例程上,我们在下面给出它们的定义:
void OnOpen(HWND hDlg)
{
TCHAR szFile[MAX_PATH] = {0};
OPENFILENAME ofn;
ZeroMemory(&ofn, sizeof(OPENFILENAME));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.lpstrFilter =
"Metafiles/0*.?mf/0WMF/0*.wmf/0Enhanced/0*.emf/0All Files/0*.*/0";
ofn.nMaxFile = MAX_PATH;
ofn.lpstrFile = szFile;
if(!GetOpenFileName(&ofn))
return;
else
{
HWND hwndMeta = GetDlgItem(hDlg, IDC_METAFILE);
DisplayMetaFile(hwndMeta, ofn.lpstrFile);
RefreshUI(hDlg, ofn.lpstrFile);
}
}
void OnPrint(HWND hDlg)
{
if(lstrlen(g_szCurFile))
PrintMetaFile(g_szCurFile);
else
Msg("当前没有打开的元文件.");
}
void OnSave(HWND hDlg)
{
TCHAR s[1024] = {0};
TCHAR szOutputFile[MAX_PATH] = {0};
if(!lstrlen(g_szCurFile))
{
Msg("当前没有打开的元文件.");
return;
}
// 请求用户确认
lstrcpy(szOutputFile, g_szCurFile);
if(strstr(g_szCurFile, ".emf"))
PathRenameExtension(szOutputFile, ".wmf");
else if(strstr(g_szCurFile, ".wmf"))
PathRenameExtension(szOutputFile, ".emf");
wsprintf(s, "You're about to convert %s to %s./nAre you really sure?",
g_szCurFile, szOutputFile);
UINT rc = MessageBox(hDlg, s, APPTITLE, MB_ICONQUESTION | MB_YESNO);
// 处理...
if(rc == IDYES)
SaveMetaFile(g_szCurFile);
}
上面代码显示这些函数完成了应用的操作,除了下述的三件事。第一,APPTITLE是一个全局串常量,它是对话框的标题,因而应该等于“元文件观察器”。第二,g_szCurfile是一个全局字符数组,用于存储打开的元文件名,并且在WinMain()中应该设置成空串。第三,RefreshUI()是一个辅助函数,它用于把打开的源文件名字附加到对话框标题上:
void RefreshUI(HWND hWnd, LPTSTR szFile)
{
TCHAR szCaption[MAX_PATH] = {0};
// 刷新标题条
wsprintf(szCaption, "%s - %s", APPTITLE, szFile);
SetWindowText(hWnd, szCaption);
}
使用这最后一个函数和通用对话框及Shell轻量级API的头文件和库文件,你现在就可以自豪的拥有这个有用的应用程序来显示,打印和转换元文件了。
改编这个应用
通过文档的关联菜单打印和转换元文件是一种更好的方法。在上面的函数中,我们已经用模块的方法实现了三个函数:
打开
打印
转换到
也就是说,有三个静态动词可以加到WMF和EMF文档上,然而,在我们能够断言可以成功地客户化关联菜单之前,在应用中有几个问题需要解决。首先,我们需要加入对命令行的支持,其次,我们需要注册EMF和WMF系统文件类——默认情况下它们是不可知文件类。此后,第三个问题是每次点击元文件都导出新的wmfview实例运行。这比只有一个实例运行要好,因为可以打开,打印或转换任何新文档。下面就让我们来着手解决这些问题。
命令行的重要性
这个应用应该支持下面的命令行:
wmfview.exe filename
wmfview.exe /p filename
wmfview.exe /s filename
头一行打开指定的文件,其他两行是打印和转换文件。有命令行的支持可以使我们容易地添加新动词到EMF和WMF文档,看一下这段代码:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevious,LPTSTR lpsz, int iCmd)
{
// 这段代码没有改变,因而省略
// 运行主对话框
BOOL b = DialogBoxParam(hInstance, "DLG_MAIN", NULL, APP_DlgProc,
reinterpret_cast<LPARAM>(lpsz));
// 退出
DestroyIcon(g_hIconLarge);
DestroyIcon(g_hIconSmall);
return b;
}
WinMain()函数传递对话框过程通过调用DialogBoxParam() API函数而接收的命令行串。任何命令行变量则通过WM_INITDIALOG消息的响应来处理,如下代码显示:
void OnInitDialog(HWND hDlg, LPARAM lParam)
{
// 设置图标(T/F 作为大/小图标)
SendMessage(hDlg, WM_SETICON, FALSE, reinterpret_cast<LPARAM>(g_hIconSmall));
SendMessage(hDlg, WM_SETICON, TRUE, reinterpret_cast<LPARAM>(g_hIconLarge));
if(lstrlen(reinterpret_cast<LPTSTR>(lParam)))
ParseCommandLine(hDlg, reinterpret_cast<LPTSTR>(lParam));
}
void ParseCommandLine(HWND hwnd, LPTSTR pszCmdLine)
{
if(!lstrlen(pszCmdLine))
return;
// 取得命令行的头两个字符(+ 1 它是开关)
TCHAR pszSwitch[2] = {0};
lstrcpyn(pszSwitch, pszCmdLine, 3);
LPTSTR psz = pszCmdLine + lstrlen(pszSwitch) + 1;
// 解析条件并发送客户消息
if(!lstrcmpi(pszSwitch, "/p"))
SendMessage(hwnd, WM_EX_PRINTMETA, 0, reinterpret_cast<LPARAM>(psz));
else if(!lstrcmpi(pszSwitch, "/s"))
SendMessage(hwnd, WM_EX_SAVEMETA, 0, reinterpret_cast<LPARAM>(psz));
else
SendMessage(hwnd, WM_EX_DISPLAYMETA,
0,reinterpret_cast<LPARAM>(pszCmdLine));
}
如上所示,ParseCommandLine()函数检查命令行,决定要做什么,然后发送客户消息到应用窗口过程。客户消息如下定义:
const int WM_EX_DISPLAYMETA = WM_APP + 1;
const int WM_EX_PRINTMETA = WM_APP + 2;
const int WM_EX_SAVEMETA = WM_APP + 3;
APP_DlgProc()得到几个调用我们已经定义过的函数处理器,如下:
BOOL CALLBACK APP_DlgProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
switch(uiMsg)
{
case WM_INITDIALOG:
OnInitDialog(hDlg, lParam);
break;
case WM_EX_DISPLAYMETA:
DisplayMetaFile(GetDlgItem(hDlg, IDC_METAFILE),
reinterpret_cast<LPTSTR>(lParam));
RefreshUI(hDlg, reinterpret_cast<LPTSTR>(lParam));
break;
case WM_EX_PRINTMETA:
PrintMetaFile(reinterpret_cast<LPTSTR>(lParam));
break;
case WM_EX_SAVEMETA:
SaveMetaFile(reinterpret_cast<LPTSTR>(lParam));
break;
case WM_COMMAND:
现在你可以看出,用命令行运行应用和显示、打印以及保存元文件就象使用对话框的菜单一样。
为什么应用要单实例化
上面代码显示每次都运行一个新实例。一旦我们把它添加到Shell使之支持元文件,就可以在任何WMF或EMF文件上点击来导出wmfview,显示指定的元文件。问题是新的程序副本被导出不仅是在打开文件时,在打印或转换文件时也产生。为了避免太多的wmfview窗口,我们需要使之成为单实例应用。
回顾Windows3.x中WinMain()的hPrevious变量,它用于表示应用的前一个实例是否存在。在Win32平台下,这个变量仅仅是为了维护兼容性而存在。并且总是NULL。因此很难知道是否有同一个进程的副本当前正在运行,但是仍然有几种可用的技术:
技术 |
描述 |
FindWindow() |
这个API函数返回属于指定类并具有给定标题的头一个窗口的Handle。因而它可以通过类名或标题鉴别窗口。 |
EnumWindows() |
这个API函数枚举所有存在的非子窗口的窗口。这对于调查是否有相同类或标题的多重窗口是有用的。 |
进程名 |
这项技术要求枚举所有活动进程并检查程序名。 |
互斥量和信号灯 |
如果你想限制实例的数量,也可以使用同步结构,如互斥量和信号灯。互斥量对于单实例应用更好一些,而信号灯则允许有固定数量的副本。 |
|
|
基于对话框的单实例的应用
在wmfview情况下,要求我们采用EnumWindows()方法。首先,互斥量和进程名的方法对于我们不是太好,因为我们需要恢复前面窗口的Handle以便再重新使用它。简单地知道它存在是没有帮助的。
再有,尽管FindWindow()更简单,但是我们的程序是基于对话框的,所以没有一个可以容易辨别的类名。相反,我们程序的主窗口类名是#32770,这实际与任何对话框或基于对话框应用同名。而且FindWindow()在头一个匹配后就停止了。我们可以使用标题来减少匹配的机会,但是我们在标题条上附加了打开的文件名,因此标题是经常变化的。
剩余的只有基于EnumWindows()这一种方法。我们将枚举窗口,并每次检查类名和标题。对于对话框窗口,我们验证标题具有我们期望的前缀——即,“源文件观察器”。为了绝对保证我们获得正确的窗口,我们还检查建立它的可执行文件。
取得可执行文件名比想象的要困难得多,因为在Windows9x和WindowsNT中没有共同的方法。我们需要在Windows9x下使用ToolHelp API函数,而在NT下使用PSAPI函数。关于这两种API的资料在MSDN库中可以找到。
只要获得了前一个实例的HWND Handle,我们就可以把它推到前台并调用ParseCommandLine(),传递我们接收的命令行。下面是对WinMain()代码的修改:
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevious,LPTSTR lpsz, int iCmd)
{
// 是否有运行中的实例
HWND hwnd = AnotherInstanceRunning();
if(IsWindow(hwnd))
{
// 推送前一个窗口到顶部
if(IsIconic(hwnd))
ShowWindow(hwnd, SW_RESTORE);
SetForegroundWindow(hwnd);
// 解析‘这个’命令行而不是发送消息到前一个窗口
ParseCommandLine(hwnd, lpsz);//这里使用的是获得的 hWnd,在分析中发送消息。
// 现在可以退出了
return 1;
}
// 其余函数不变
}
这里的AnotherInstanceRunning()就是实际用于测试的函数,它调用EnumWindows()函数,而回调函数仅在找到同类窗口时停止。
HWND AnotherInstanceRunning()
{
HWND hwndFound = NULL;
EnumWindows(CheckRunningApps, reinterpret_cast<LPARAM>(&hwndFound));
// hwndFound 将取匹配窗口的Handle
return hwndFound;
}
BOOL CALLBACK CheckRunningApps(HWND hwnd, LPARAM lParam)
{
TCHAR szClass[MAX_PATH] = {0};
GetClassName(hwnd, szClass, MAX_PATH);
if(!lstrcmpi(szClass, "#32770"))
{
TCHAR s[MAX_PATH] = {0};
TCHAR szTitle[MAX_PATH] = {0};
GetWindowText(hwnd, szTitle, MAX_PATH);
lstrcpyn(s, szTitle, 1 + lstrlen(APPTITLE));
if(!lstrcmpi(s, APPTITLE))
{
// 使用缓冲指针lparam返回HWND
HWND* lphwnd = reinterpret_cast<HWND*>(lParam);
*lphwnd = hwnd;
return FALSE;
}
}
return TRUE;
}
现在我们最终有了一个单实例应用,可用它来观察,打印和转换元文件,因此可以考虑怎样添加某些Shell的支持了。
添加Shell支持
典型地,Shell支持意思是:
注册应用处理的每一种文件类型
注册文件类型的默认图标
添加关联菜单动词
添加打开的文档到最近文档列表
我们已经看到了怎样注册新的文件类型及其图标,并且在Wmfview.exe的资源中我们也安排了包含元文件和增强元文件的图标:
添加到系统注册表中的条目包含在下面的脚本代码中:
REGEDIT4
; //////////////////////////////////////////////////
; // 注册 WMF 和 EMF 文件类型
[HKEY_CLASSES_ROOT/.wmf]
@= "WinMetafile"
[HKEY_CLASSES_ROOT/WinMetafile]
@= "Windows Metafile"
[HKEY_CLASSES_ROOT/.emf]
@= "EnhMetafile"
[HKEY_CLASSES_ROOT/EnhMetafile]
@= "Enhanced Metafile"
; //////////////////////////////////////////////////
; // 注册WMF 和 EMF的图标
[HKEY_CLASSES_ROOT/WinMetafile/DefaultIcon]
@= "C://WmfView//WmfView.exe,2"
[HKEY_CLASSES_ROOT/EnhMetafile/DefaultIcon]
@= "C://WmfView//WmfView.exe,1"
; //////////////////////////////////////////////////
; // 添加 WMF 打开,打印和保存动词
; 打开
[HKEY_CLASSES_ROOT/WinMetafile/Shell/Open/Command]
@= "C://WmfView//WmfView.exe %1"
; 打印
[HKEY_CLASSES_ROOT/WinMetafile/Shell/Print/Command]
@= "C://WmfView//WmfView.exe /p %1"
; 保存到 EMF
[HKEY_CLASSES_ROOT/WinMetafile/Shell/Save]
@= "&Convert to EMF"
[HKEY_CLASSES_ROOT/WinMetafile/Shell/Save/Command]
@= "C://WmfView//WmfView.exe /s %1"
; //////////////////////////////////////////////////
; // 添加打开,打印和保存动词到 EMF
; 打开
[HKEY_CLASSES_ROOT/EnhMetafile/Shell/Open/Command]
@= "C://WmfView//WmfView.exe %1"
; 打印
[HKEY_CLASSES_ROOT/EnhMetafile/Shell/Print/Command]
@= "C://WmfView//WmfView.exe /p %1"
; 保存到 WMF
[HKEY_CLASSES_ROOT/EnhMetafile/Shell/Save]
@= "&Convert to WMF"
[HKEY_CLASSES_ROOT/EnhMetafile/Shell/Save/Command]
@= "C://WmfView//WmfView.exe /s %1"
除了假定已经安装了WmfView.exe到c:/wmfview文件夹下,这个清单还展示了在编写自己的注册脚本时所需要了解的一些事情。特别是:
在REG脚本中 @符号表示默认值
非常重要的是在路径名中使用两个反斜杠,而在注册条目中使用一个反斜杠
在编写REG脚本时,记住不要割裂包含在括号中的注册表路径到两个行或多个行上
默认情况下动词名(此时为Shell键下的任何子键)就是实际出现在关联菜单上的项。这对于‘打开’和‘打印’是适合的,但是并不适合‘保存’表示的从EMF转换到WMF或相反的命令。这就是为什么我们设置‘保存’动词的默认值为客户想要出现在关联菜单项上的串的理由。你可以通过使用注册表编辑器的‘注册表 | 输入注册文件’菜单项,通过双击这个REG文件,或编程通过ShellExecute()函数执行这个REG文件运行这个脚本。此后,重新启动探测器,其效果如下图中显示的一样:
改变默认菜单项
如图表示的,‘打开’动词总是设置为默认的关联菜单项。默认项是以粗体显示的,并且双击左键自动选择(或单击,依赖于桌面设置)。默认项总是显示在关联菜单中的第一项。然而,通过设置下面键的默认值,我们可以重排增强元文件关联菜单的项,例如:
HKEY_CLASSES_ROOT
/EnhMetafile
/Shell
正常情况下这个值包含空字符串,但是如果你设置它为用逗号分隔的串,其标记是各个动词的名,则他们将以你指定的顺序显示,而第一项是默认项,这并不要求你指定所有列表中的动词,所以,你可以限制有多少要重排的项。
记住这个技术仅对定义在Shell键下的静态动词有效
添加所有文件的关联菜单项
在上面的截图中你可能已经注意到有两个关系到WinZip的项。这两个项并不是特别针对元文件的,而是针对所有文件的(但不对文件夹)。因此,它们没有在WinMetaFile或EnhMetaFile的Shell键下列出。要为所有文件类添加关联菜单项,简单地在下面键中添加动词即可:
HKEY_CLASSES_ROOT
/*
/Shell
有时添加新项将从‘打开方式’项中删除默认风格。还要注意,所有在这里添加的项总是在特殊文件类型的Shell项之前显示。然而这显然不是WinZip的场合。那么WinZip是怎么做的呢。如果你想要添加新命令在HKEY_CLASSES_ROOT/*/Shell键下而又不删除当前默认项,一个办法就是定义Shell扩展。就象上图
中的WinZip和‘公文包’那样。Shell扩展在shellex键下,并且,如果应用于关联菜单,还需要有进一步的ContextMenuHandlers子键。我们将在下一章中讨论这个问题。
如果想要添加客户菜单项到任何文件夹或驱动器,则注册键应该分别在:
HKEY_CLASSES_ROOT/Folder
和
HKEY_CLASSES_ROOT/Drive
下。
给文件夹指定客户图标
假设你编写了一套应用,并安装在公共路径下。如果这个文件夹有一个客户化图标是否会更好一点。看一下下面的图:
这个Wrox Applications应用的文件夹比正常的要好一些,而且它的图标也是不同的。要获得这个效果,只需完成下面几步:
在想要客户化的文件夹中建立一个名为desktop.ini的ASCII文件。可以使其隐藏,但并不是必要的。过一会我们将讨论这个文件的内容。
使这个目录为只读。这可以编程实现或通过属性对话框实现 。
desktop.ini文件是一个特殊意义的探测器——它表示在Shell与一个客户化文件夹外观和行为的代码段之间的连接点。在第16章中我们将揭示怎样通过命名空间扩展改变文件夹的行为,而不要求包含多余信息的desktop.ini文件。为了改变文件夹的图标,我们需要把下面行添加到这个文件中:
[.ShellClassInfo]
IconIndex=0
IconFile=C:/WMFVIEW.EXE
这些条目的作用是相当清楚的:IconIndex表示图标在IconFile文件中的索引。因此这两个项组合定义了文件夹显示的图标。
注意,当你改变文件夹的设置时——特别是在Web观察下——这个文件的内容被自动更新。
自由添加最近文档
Win32资料说如果想要添加文档到系统文件夹作为最近使用的文档,你需要产生对SHAddToRecentDocs()函数的调用。要完成这项工作,我们发现有时并不需要走这么远。当处理元文件已经被注册了之后,我们发现wmfview自动保存打开的文档到这个文件夹,甚至我们并没有显式地调用SHAddToRecentDocs()。这显然是Shell施加于注册了文档的应用的一个特征。对于非注册文件类型,你仍然需要这个API函数。
事实上,你没必要必须打开文档,当你打印或确切地执行任何动词的时候,都可以。然而,当你使用‘文档’菜单时,总是执行默认动词。
支持拖拽
基于文件的应用有对拖拽的支持是一个极强的功能,并且如果你仔细地设计软件并很好地与Shell集成,添加这个能力就象给你的主窗口赋一个WS_EX_ACCEPTFILES风格的值一样容易,这就使它能够感觉到后面的WM_DROPFILES消息。
重要地是需要有一个可以作为模块化过程运行的函数来处理这个事件。如果使用命令行来解释,则添加Shell拖拽要求附加下面的代码,它将被唤醒响应WM_DROPFILES消息:
case WM_DROPFILES:
HandleFileDrop(hDlg, reinterpret_cast<HDROP>(wParam));
break;
void HandleFileDrop(HWND hDlg, HDROP hDrop)
{
TCHAR szFileName[MAX_PATH] = {0};
// 由于是SDI应用,它不能感觉接收超过一个文件,因此仅是第一个文件被接收
DragQueryFile(hDrop, 0, szFileName, MAX_PATH);
SendMessage(hDlg, WM_EX_DISPLAYMETA, 0, reinterpret_cast<LPARAM>(szFileName));
DragFinish(hDrop);
}
客户化打开对话框
面向Shell应用可以使用的另一个值得注意的属性是定制系统公共对话框。Win32 API描述了我们需要客户化公共对话框的技术,比如颜色,字体或打印,你应该参考微软的资料来获得更多细节信息。
就我们的目的而言,最值得关注的对话框是‘打开’/‘保存’对话框。让我们从一个客户化打开对话框的例子开始,在对话框上添加几个新按钮作为特殊路径的标签。下图显示了这个客户化的对话框。要特别注意‘Open As’的标号和组合框。
下面我们将说明怎样建立类似于Office2000产生的打开对话框。在此之前我们要向你解释一下对话框的客户化是怎样发生的。
定义新的模版
这个打开对话框允许你定义非标准模板,但是你没有象Windows3.1中那样可用的自由度。如果你使用类探测器的外观,则不允许隐藏不需要的控件。这是由设计规定的,我们现在还没有看到可靠的工作方法,除非编写一个新的初级对话框。
要调用打开对话框,你需要GetOpenFileName()函数,显示如下:
OPENFILENAME ofn;
TCHAR szFile[MAX_PATH] = {0};
ZeroMemory(&ofn, sizeof(OPENFILENAME));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.lpstrFile = szFile;
ofn.nMaxFile = sizeof(szFile);
ofn.lpstrFilter = __TEXT("All files/0*.*/0");
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
GetOpenFileName(&ofn);
为了允许客户化,需要在调用之前添加下面的代码行:
ofn.lpTemplateName = MAKEINTRESOURCE(IDD_DIALOG);
ofn.lpfnHook = OpenDlgExProc;
ofn.Flags |= OFN_EXPLORER | OFN_ENABLEHOOK | OFN_ENABLETEMPLATE;
ofn.lpTemplateName指向一个对话框模板,ofn.lpfnHook指向一个窗口过程,这个过程将处理所有客户控件的消息。
形成初始对话框的控件必须作为一个整体控件加以考虑,所以,你所设计的任何模板都必须包含整个初始对话框作为构件块。实际上,这就是说要使用你所附加控件的相对位置,看下面截图,其中对话框模版有一个‘Child’风格,但是没有边缘:
高亮部分是静态部件,其ID为stc32,这是定义在dlg.h(VC++的一个头文件)中的特殊常量。它表示为一个标准的‘打开’对话框。然后你可以在这个部件周围放置你的控件,并且不必考虑stc32控件的尺寸,最后的窗口是适当可调整的。就如同图中显示的。下面截图显示这个打开对话框在运行时使用上面的模版形成的显示界面,这是一个由AppWizard生成的应用。
标准对话框被扩展成有三个竖排按钮在左侧的客户化对话框,看上去不过如此,它的行为又如何呢?
新对话框属性
除了新的外表,这个‘打开’对话框应该有什么样的客户化行为呢?
快捷方式的常用路径是一个好想法,如果可以由用户定义就更好了
新控件上的工具标签是另一个特征,这改进了对话框的外观和感觉
这个对话框应该能够防止删除和重命名文件夹中的项
常用路径的标签
返回前面的讨论,我们选择添加了几个按钮来存储指向‘Favorites’,‘最近文档’和‘Windows’目录的标签。代码的最有技巧部分是编程获得特定的路径,方案与手动所作的一样。换言之,只需设置‘文件名’编辑框到路径名,并点击‘打开’即可。
所有在公共对话框中使用的控件,其ID都定义在dlgs.h头文件中,但不幸的是这个文件并没有给这些ID分配助记符。通过使用Spy++和dlg.h,可以对应给出它们的标记。文件名编辑框的ID是0x0480,其相关助记常量是edt1。从窗口过程调用函数处理按钮点击,我们需要作下面的操作:
#include <dlgs.h>
#include <shlobj.h>
void Goto(HWND hDlg, WORD wID)
{
TCHAR szDir[MAX_PATH] = {0};
LPITEMIDLIST pidl;
// 恢复转跳路径
if(wID == IDC_WINDOWS)
GetWindowsDirectory(szDir, MAX_PATH);
else
{
if(wID == IDC_FAVORITES)
SHGetSpecialFolderLocation(hDlg, CSIDL_FAVORITES, &pidl);
else
if(wID == IDC_RECENT)
SHGetSpecialFolderLocation(hDlg, CSIDL_RECENT, &pidl);
SHGetPathFromIDList(pidl, szDir);
}
// 在文件名编辑框中设置新路径
HWND hdlgParent = GetParent(hDlg);
SetDlgItemText(hdlgParent, edt1, szDir);
SendMessage(hdlgParent, WM_COMMAND, IDOK, 0);
SetDlgItemText(hdlgParent, edt1, "");
}
注意这段代码,钩子过程所接收的窗口Handle不是实际对话框窗口的Handle。这个操作仅仅适用于‘打开’对话框,要获得实际窗口,你必须取得传输窗口的父窗口Handle:
HWND hdlgParent = GetParent(hDlg);
获得父窗口后,你才能安全地获得‘打开’对话框的每一个子控件。
按钮的图标和提示
每一个新按钮都有一个图标和提示。下面是怎样设置按钮的图标和提示:
void InitNewButtons(HWND hDlg)
{
SHFILEINFO sfi;
LPITEMIDLIST pidl = NULL;
// 指派图标到‘Favorites’按钮
SHGetSpecialFolderLocation(hDlg, CSIDL_FAVORITES, &pidl);
SHGetFileInfo(reinterpret_cast<LPTSTR>(pidl),
0, &sfi, sizeof(SHFILEINFO), SHGFI_PIDL | SHGFI_ICON);
SendDlgItemMessage(hDlg, IDC_FAVORITES, BM_SETIMAGE, IMAGE_ICON,
reinterpret_cast<LPARAM>(sfi.hIcon));
// 指派图标到‘最近文档’按钮
SHGetSpecialFolderLocation(hDlg, CSIDL_PERSONAL, &pidl);
SHGetFileInfo(reinterpret_cast<LPTSTR>(pidl),
0, &sfi, sizeof(SHFILEINFO), SHGFI_PIDL | SHGFI_ICON);
SendDlgItemMessage(hDlg, IDC_RECENT, BM_SETIMAGE, IMAGE_ICON,
reinterpret_cast<LPARAM>(sfi.hIcon));
// 指派图标到‘Windows’按钮
SendDlgItemMessage(hDlg, IDC_WINDOWS, BM_SETIMAGE, IMAGE_ICON,
reinterpret_cast<LPARAM>(LoadIcon(NULL, IDI_WINLOGO)));
// 设置每个按钮的提示
SetTooltips(hDlg);
}
图标根据特定的图标需求从不同的地方恢复,对于特殊文件夹,如‘Favorites’或‘最近文档’,使用SHGetFileInfo(),而LoadIcon()则帮助获得Windows的标记。每一个按钮也都有自己的提示:
void SetTooltips(HWND hDlg)
{
// 建立提示控件
HWND hwndTT = CreateWindow(TOOLTIPS_CLASS, NULL, TTS_ALWAYSTIP,CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, hDlg,
NULL, GetModuleHandle(NULL), NULL);
// 为每一个按钮定义要求的工具
// 查看‘Favorites’
TOOLINFO ti;
ZeroMemory(&ti, sizeof(TOOLINFO));
ti.cbSize = sizeof(TOOLINFO);
ti.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
ti.hwnd = hDlg;
ti.uId = reinterpret_cast<UINT>(GetDlgItem(hDlg, IDC_FAVORITES));
ti.lpszText = __TEXT("查看Favorites");
SendMessage(hwndTT, TTM_ADDTOOL, 0, reinterpret_cast<LPARAM>(&ti));
// 查看最近文档
ZeroMemory(&ti, sizeof(TOOLINFO));
ti.cbSize = sizeof(TOOLINFO);
ti.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
ti.hwnd = hDlg;
ti.uId = reinterpret_cast<UINT>(GetDlgItem(hDlg, IDC_RECENT));
ti.lpszText = __TEXT("查看最近文档");
SendMessage(hwndTT, TTM_ADDTOOL, 0, reinterpret_cast<LPARAM>(&ti));
// 查看Windows
ZeroMemory(&ti, sizeof(TOOLINFO));
ti.cbSize = sizeof(TOOLINFO);
ti.uFlags = TTF_IDISHWND | TTF_SUBCLASS;
ti.hwnd = hDlg;
ti.uId = reinterpret_cast<UINT>(GetDlgItem(hDlg, IDC_WINDOWS));
ti.lpszText = __TEXT("Look in Windows");
SendMessage(hwndTT, TTM_ADDTOOL, 0, reinterpret_cast<LPARAM>(&ti));
}
我们建立一个新的工具窗口,并定义了五个工具,每一个都有自己的显示文字。在每一个定义新工具的块中,uFlags字段指定uId字段表示窗口Handle(TTF_IDISHWND)。意思是这个窗口,即按钮之一是一个显示分配给lpszText文字的区域。
对于要显示的标签,本质上是一个窗口区域,它具有发送适当的鼠标通知的到提示窗口的能力。TTF_SUBCLASS标志使提示窗口自动子类为按钮以便发送消息。
组装代码
为了尽可能地重用代码,下面的函数是围绕标准GetOpenFileName()API函数构件的一个封装,它接收指向OPENFILENAME结构的指针作为输入。如果这个结构中包含了非空的模版字段,则调用标准函数,换句话说,如果用户请求了定制的文件夹,则这个函数只是调用原始例程,否则,它用我们开发的对话框模版置换标准的:
#include <commdlg.h>
BOOL GetOpenFileNameEx(LPOPENFILENAME lpofn)
{
// 如果模板是定制的,回复到标准对话框
if(lpofn->lpTemplateName)
return GetOpenFileName(lpofn);
// 调整 OPENFILENAME 结构
lpofn->hInstance = GetModuleHandle(NULL);
lpofn->lpTemplateName = MAKEINTRESOURCE(IDD_DIALOG);
lpofn->lpfnHook = OpenDlgExProc;
lpofn->Flags |= OFN_EXPLORER | OFN_ENABLEHOOK | OFN_ENABLETEMPLATE;
BOOL b = GetOpenFileName(lpofn);
return b;
}
由上函数使用的回调需要处理两个消息。由于对话框构造而接收到WM_NOTIFY消息时,必须调用InitNewButtons()函数来设置按钮的图标和提示。如果接收到WM_COMMAND消息,我们就检查按下的是哪一个按钮,并作出适当的响应:
UINT CALLBACK OpenDlgExProc(HWND hDlg, UINT uiMsg, WPARAM wParam, LPARAM lParam)
{
LPOFNOTIFY pN = NULL;
switch(uiMsg)
{
case WM_NOTIFY:
pN = reinterpret_cast<LPOFNOTIFY>(lParam);
if(pN->hdr.code == CDN_INITDONE)
InitNewButtons(hDlg);
break;
case WM_COMMAND:
switch(LOWORD(wParam))
{
case IDC_FAVORITES:
case IDC_RECENT:
case IDC_WINDOWS:
Goto(hDlg, LOWORD(wParam));
break;
}
break;
}
return 0;
}
现在所有剩下的是使用来自主应用对话框OnOK()函数的适当变量调用GetOpenFileNameEx()函数。下面是它的代码:
void OnOK(HWND hDlg)
{
// 局部数据
OPENFILENAME ofn;
TCHAR szFile[MAX_PATH] = {0};
ZeroMemory(&ofn, sizeof(OPENFILENAME));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = hDlg;
ofn.lpstrFile = szFile;
ofn.nMaxFile = sizeof(szFile);
ofn.lpstrFilter = "All files/0*.*/0";
ofn.nFilterIndex = 1;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_HIDEREADONLY;
if(GetOpenFileNameEx(&ofn))
MessageBox(hDlg, ofn.lpstrFile, "Open", MB_OK | MB_ICONINFORMATION);
}
防止重命名项
默认情况下‘打开’对话框允许删除和重命名它显示的文件。这个特征是由系统提供的,而且没有标志来抑制它。然而,在很多情况下,并不需要这个功能,有时它会变得非常讨厌。现在让我们来看一下怎样关闭这个功能。
允许列出文件的控件是列表观察控件,它具有LBS_EDITLABELS风格。为了得到我们想要的行为,只需要把这个位关闭掉即可。最困难的是怎样取得这个列表观察控件的Handle。
void ModifyStyle(HWND hDlg)
{
// 取得文件的列表观察Handle
HWND hwndDefView = GetDlgItem(GetParent(hDlg), lst2);
HWND hwndListView = GetDlgItem(hwndDefView, 1);
// 关闭这个位
DWORD dwStyle = GetWindowLong(hwndListView, GWL_STYLE);
dwStyle &= ~LVS_EDITLABELS;
SetWindowLong(hwndListView, GWL_STYLE, dwStyle);
}
列表观察是窗口容器的子控件,其ID为lst2——这是另一个来自dlg.h头文件的值。这个列表观察的ID是1,这主要是由Spy++获得。必须在每次用户改变目录后调用上面函数,因为每次目录发生改变时都销毁和重建这个观察。一个好方法是响应CDN_FOLDERCHANGE通知:
case WM_NOTIFY:
pN = reinterpret_cast<LPOFNOTIFY>(lParam);
if(pN->hdr.code == CDN_INITDONE)
InitNewButtons(hDlg);
if(pN->hdr.code == CDN_FOLDERCHANGE)
ModifyStyle(hDlg);
break;
文件删除提示
即使你子类化了列表观察,你也不能捕捉到对应的‘删除’键。显然探测器通过处理消息的键盘钩子陷落了文件删除的请求,然后吃掉了它。
如此对于我们最好的方法也是安装键盘钩子,用以陷落‘删除’键,陷落消息,然后中断钩子链,就象探测器那样。此时探测器不能得到消息,因此将不能删除文件。钩子应该在调用GetOpenFileName()之前安装,而后立即删除。代码有一点小小的改动:
BOOL GetOpenFileNameEx(LPOPENFILENAME lpofn)
{
// 如果模版是定制的,回复到标准对话框
if(lpofn->lpTemplateName)
return GetOpenFileName(lpofn);
// 调整 OPENFILENAME 结构
lpofn->hInstance = GetModuleHandle(NULL);
lpofn->lpTemplateName = MAKEINTRESOURCE(IDD_DIALOG);
lpofn->lpfnHook = OpenDlgExProc;
lpofn->Flags |= OFN_EXPLORER | OFN_ENABLEHOOK | OFN_ENABLETEMPLATE;
// 设置键盘钩子到当前线程
g_hHook = SetWindowsHookEx(WH_KEYBOARD,HookProc,NULL,GetCurrentThreadId());
BOOL b = GetOpenFileName(lpofn);
// 删除钩子
UnhookWindowsHookEx(g_hHook);
return b;
}
每当在打开对话框中按键时钩子过程都被调用,这个钩子过程是非常简单的,下面是它的代码:
LRESULT CALLBACK HookProc(int iCode, WPARAM wParam, LPARAM lParam)
{
// 吃掉 DELETE 键
if(wParam == VK_DELETE)
return 1;
// 否则自由处理
return CallNextHookEx(g_hHook, iCode, wParam, lParam);
}
什么是Shell集成的应用
我们已经花费了一整章的内容来讨论把应用与Shell集成的各个方面。我们还发现,有一定数量的特征是可以编进可执行程序的。包括解析命令行参数,确保应用单一运行实例等。还有一些其它的特征,它们是不必贴附到只应用上的,如文件类型的注册和关联菜单的改进等。基本上有两个层次的Shell集成。第一个是美化和瞄准用户界面的:完好的文件类型名,完好的图标和新的关联菜单项。其次是编写代码,有一定的全程应用设计规则,还有其它许多小技巧,比如处理WM_ENDSESSION消息导致应用自动在下一次登录时运行,以及添加快捷方式到‘Favorites’和‘最近文档’文件夹等。
小结
在这一章中,我们论证了Win32应用与系统Shell集成的各方面技术。我们调查了Shell集成的方法,要考虑的各个方面,以及怎样设计和工程化存在的代码以获得Shell对应用处理文档的支持。我们还讨论了客户化和扩展‘打开’对话框的能力。
这一章所设计的主要科目是:
文件类和存储在注册表中的信息
关联菜单的客户化
经由Shell建立新文档
应用的命令行
怎样编写基于对话框的单实例应用
客户化打开文件公共对话框
使应用为Shell感知的原理
然而我们说Shell扩展是扩展WindowsShell能力和行为的最灵活有力的方法。下一章我们将探讨这个课题。