Visual C++ ActiveX 开发指南:第一章 什么是ActiveX

11月 2nd, 2005 by 天地狂虫

第一章  什么是ActiveX

 

一种Internet策略  ActiveX是以一种Internet策略出现的。它包含了OLECOMInternet开发的各个方面。

 

ActiveX开发涉及范围广泛  ActiveX开发的包含了许多方面,比如自动化服务器,ActiveX控件,COM对象等等。

 

术语ActiveX在过去的几年中成为了许多开发人员及团队的战斗口号,另一方面市场也对其大肆宣扬,然而,没有几个人能解释清楚其确切的含义。本书主要目的就是说明什么是ActiveX以及它对开发人员意味着什么。我希望读者能够学到和我为写本书而学到的知识尽可能一样多。

 

应用程序开发的一种Internet策略

 

Microsoft第一次介绍ActiveX是在1996年的Intenet专业开发人员大会(Internet PDC)上。ActiveX源自于大会的口号“Activate the Internet”(可理解为:让因特网活跃起来),与其说ActiveX是一种开发应用程序的技术或是架构,不如说它是一种召唤。

 

在开发大会期间,Microsoft正在与控制了Internet浏览器市场的Netscape进行激烈的竞争。但是,大会表明了Microsoft感兴趣的不仅仅是浏览器市场。Microsoft演示的工具从电子存储前端产品、新的OLE控件到虚拟现实聊天软件等等,应有尽有。

 

ActiveXMicrsoft共同的新口号,类似于90年代初的提出的OLE(Object Linking and Embedding,对象链接与嵌入),而且在很短的时间内,远远超越了“Activate the Internet”

 

ActiveX成为了包含一切的术语:从Web页面到OLE控件。ActiveX开始变得重要起来:一方面,小型、快速、可重用的组件能够让你紧紧抓住来自于Micrsoft,Internete及工业的最新技术;另一方面,ActiveX代表了Internet与应用程序集成的策略。目前,在产品或公司中没有使用InternetActiveX技术被认为是过时的。事实上,描述ActiveX就像描述色彩一样,它既不是技术也不是架构,而是一个概念,一个指导。

 

ActiveX, OLEInternet

 

    ActiveXOLE开始成为同义词,人们曾经谈到的OLE控件现在成为了ActiveX控件,OLE文档对象现在成为了ActiveX文档对象。有时,整个关于如何实现OLE技术的文档被更新为ActiveX技术,唯一的变化就是术语OLE,现在命名为ActiveX

 

    尽管OLEActiveX取得了巨大的进步,表面上每天还有与其相关的新技术出现,但Internet是否已经或直接卷入到许多相关的领域还是令人置疑的。对小型、快速、可重用组件(COM组件)的需求已经些年头了,分布式组件(DCOM组件)在几年前的OLE 2.0 专业人员开发大会上作了第一次演示。Visual Basic(VB)开发组在使ActiveX技术成为可能的早期扮演了得要角色。包含在ActiveX SDK中的BaseCtl框架就是由VB开发组开发的,它解决了VB为减少载入时间而对小型,轻量级组件的需求。Internet唯一的贡献就是它需要一种方式来实现和发布Web页面。实际上,每一个ActiveX的新功能都能追溯到最基本的,全球泛围的对小型、快速,可重用组件的需求,而这,就是从OLECOM开始的。

 

    ActiveX并不意味着要代替OLE,仅仅把它扩大到包括Internet,企业内部网商务应用程序及家庭应用程序的开发,以及开发这些应用的工具。

 

    Microsoft发布了大量关于ActiveX开发的文档。OC 96 规范定义了如何开发启动更快速,绘制能力更强的控件,它也定义了哪些接口是必需的,而哪些接口是可选的。”OLE Control and Control Container Guidelines”提供了关于控件与控件容器交互的重要信息。MicrosoftWeb站点成为了信息丰富的及创建、使用、分发ActiveX组件的中心。

 

    除了创建ActiveX组件的技术细节外,Microsoft建立起了一套使用和集成ActiveX组件的标准。从VBMicrsoft WordJava的每一个产品都继承了使用ActiveX组件的能力。在ActiveX技术出现前,一大半的应用程序无法像如今这样如此紧密相关地无缝集成。

 

    接下来的部分将谈到我们可以创建的ActiveX组件的类型,以及我们何时,为什么才需要使用它这可能更有帮助。

 

ActiveX组件的分类

 

    本书的主题是ActiveX组件的开发。这些组件可以分为以下几类:

 

    Automation Servers:自动化服务器

Automation Controllers:自动化控制器

Controls:控件

COM ObjectsCOM对象

Documents:文档

Containers:容器

 

本书只是详细谈到了自动化服务器,控件及COM对象。自动化控制器、文档及容器涉及到太多的接口,太多的技术,超出本书所能承受的范围。

 

自动化服务器

 

自动化服务器是可以被其它应用程序编程驱动的组件。一个自动化服务器包含到少一个或多个基于IDispatch的接口,其目的是为了让其它的应用程序创建和连接它。根据服务器本身的特性,一个自动化服务器可以包含也可以不包含用户界面。

 

    自动化服务器可以是进程内的(运行在控制器的进程空间),本地的(运行在自己独立的进程空间),远程的(运行在其它机器的进程空间)。有些情况下,特定的服务器实现将会指定服务器在哪里运行,但是,这一点是不能保证的。DLL能够在进程内,本地或远程运行,而EXE则只能在本地或远程远行。

 

注意:对于控制器来说,执行最快的就是进程内自动化服务器。但要记住,使用进程内服务器并不能保证其性能。如果一个进程内自动化服务器在一个进程空间内被创建,而被另一进程内的控制器所控制,它就降级为进程外服务器,其性能与进程外服务器相同。关于进程空间与服务器冲突的更多信息请参见本书的第二部分。

 

自动化控制器

 

    自动化控制器是那些能使用和操作自动化服务器的应用程序,一个很好的例子就是VB。使用VB你可以创建,使用并销毁自动化服务器,就好像它们是VB语言的完整一部分一样。

 

    自动化控制器可以是任意类型的应用程序,DLl或是EXE,能够以进程内,本地,远程的方式访问自动化服务器。一般地,注册表入口与服务器的实现指明了与控制器相关的自动化服务器应该在哪一个进程空间运行。

 

控件

 

ActiveX等同与OLE控件或是OCX。一个典型的ActiveX控件由一个在设计时及运行时都存在的用户界面,一个定义了控件的所有方法及属性的IDispatch接口,一个定义了控件可以触发的事件的IConnectionPoint接口所组成。此外,它还可能支持运行生命期内的持久化,以及各种用户界面功能,例如,剪切粘贴,拖放操作。从结构上来讲,一个控件必须支持大量的COM接口以发挥这些功能的优势。

 

随着新的OLE控件及ActiveX开发指南的发行,一个控件不仅限于上述那些功能。但开发人员可以仅仅实现上述的那些功能,因为它们用处最大,而且对于使用应用程序的用户来说,他们也最感兴趣。由Microsoft出版的控件与容器指南列出了所有的接口以及它们的特殊要求。你可以在Microsoft的网站http://www.microsoft.com找到这些信息。

 

    ActiveX控件对于容器来说,总是进程内运行的。一个控件的扩展名通常是OCX,但从执行模式上来讲,它就是一个DLL而已。

 

COM对象

 

    Com对象在结构上类似于自动化服务器和控制器。它们包含一个或多个COM接口,有一部分或完全没有用户界面。然而,这些对象不能被控制器以使用自动化对象的方式所使用。控制器必须理解COM接口才能与这些接口通讯,这些都与自动化接口无关。Windows 95NT操作系统定义了成百上千的COM接口和自定义接口作为操作系统扩展,控制了从桌面外观到屏幕三维图像渲染的一切。COM对象是一种组织相关功能及数据的很好的方式,并且仍然保留了DLL对高性能的要求。

 

注意:自动化服务器也能受益于COM接口,这些服务器就是双接口服务器。自动化服务器接口有一个伴随的COM接口,它描述了对象的方法及属性。象VB这样的自动他控制器在使用服务器的时候能够利用双接口的优势提供更高的性能。双接口服务器有一个缺点就是在定义属性和方法时,其数据类型被限制为被OLE自动化所支持的类型。

 

文档

 

ActiveX文档或者说是最初被称为的文档对象,表示那些不仅仅只是一个控件或服务器的对象。一个文档可以是任意的,可以是电子表格或者是一个财务应用程序中的复杂的发票。文档,就像控件,有用户界面,并且有容器应用程序作为宿主。Microsoft WordMicrosoft Excel就是复杂的文档服务器,Microsoft Office BinderMicrosoft Internet Explorer就是ActiveX文档容器。

 

ActiveX文档构架是对OLE链接与嵌入模型的扩展,它允许文档透过其宿主容器得到更多的控制,最明显的改变就是菜单如何呈现。标准OLE文档的菜单将会与容器的菜单合并,提供组合的功能集;而ActiveX文档则会占用整个菜单系统,因此只表示文档的功能而不是同时表示文档和容器的功能。事实上,文档所暴露的功能集是ActiveX文档与OLE文档之间差别的前提。容器只是一种宿主机制,而文档则拥有所有的控制。

 

其它的不同就是打印和存储。OLE文档有意地成为其宿主容器文档的一部分,因此,作为容器文档的一部分来打印与存储。而ActiveX文档则期望支持其本身的打印与存储功能,并没有与宿主文档集成。

 

ActiveX文档采用统一的表示结构,而不是OLE文档的嵌入式的文档架构。Microsoft Internet Explorer就是一个很好的例子,它只不过给用户显示Web页面,但是将页面作为一个实体进行浏览、打印及存储。Microsoft WordMicrosoft Excel则是OLE文档架构,如果Excel的电子表格嵌入到Word文档,那么电子表格将与Word文档一起存储,成为完整Word文档的一部分。

 

ActiveX文档还有一些额外的功能用来支持InternetInternatWeb页面发布。想像一下一个内部的订单跟踪系统运行在与连接Internet所使用的相同的Web浏览器中。

 

容器

 

    ActiveX容器是能够作为ActiveX服务器、控件或文档的宿主的应用程序。VBActiveX Control Pad都是能够作为ActiveX服务器和控件的容器。Microsoft Office BinderMicrosoft Internet Explorer则是能够作为ActiveX服务器、控件及文档宿主的容器。

 

    随着ActiveX控件和文档规范中对必要要求的减少,容器必须足够健全以处理控件或文档缺少某些接口的情况。容器应用程序可能与控件或文档只有很少或根本没有交互,也可能在表现和操作组件时提供了很重要的交互能力。这种能力完全依赖于组件的宿主容器,在任何的容器开发中都不是必要的。

DirectX9编程-入门

10月 28th, 2005 by 天地狂虫

什么是DirectX?

        DirectX是由Microsoft开发的基于Windows平台的游戏编程接口。

Winsock编程-入门

10月 27th, 2005 by 天地狂虫

什么是Winsock

        Winsock是Windows下的网络编程接口,它是由Unix下的BSD Socket发展而来,是一个与网络协议无关的编程接口。

构建编程环境

        Winsock在常见的Windows平台上有两个主要的版本,即Winsock1和Winsock2。编写与Winsock1兼容的程序你需要引用头文件WINSOCK.H,如果编写使用Winsock2的程序,则需要引用WINSOCK2.H。此外还有一个MSWSOCK.H头文件,它是专门用来支持在Windows平台上高性能网络程序扩展功能的。使用WINSOCK.H头文件时,同时需要库文件WSOCK32.LIB,使用WINSOCK2.H时,则需要WS2_32.LIB,如果使用MSWSOCK.H中的扩展API,则需要MSWSOCK.LIB。正确引用了头文件,并链接了对应的库文件,你就构建起编写WINSOCK网络程序的环境了。

初始化Winsock

        每个Winsock程序必须使用WSAStartup载入合适的Winsock动态链接库,如果载入失败,WSAStartup将返回SOCKET_ERROR,这个错误就是WSANOTINITIALISED,WSAStartup的定义如下:

int WSAStartup(
    WORD wVersionRequested,
    LPWSADATA lpWSAData
);

wVersionRequested指定了你想载入的Winsock版本,其高字节指定了次版本号,而低字节指定了主版本号。你可以使用宏MAKEWORD(x, y)来指定版本号,这里x代表高字节,而y代表低字节。lpWSAData是一个指向WSAData结构的指针,WSAStartup会向该结构中填充其载入的Winsock动态链
库的信息。

lpWSAData是一个指向WSAData结构的指针,WSAStartup会向该结构中填充其载入的Winsock动态链
库的信息。


lpWSAData是一个指向WSAData结构的指针,WSAStartup会向该结构中填充其载入的Winsock动态链
库的信息。

typedef struct WSAData
{
    WORD           wVersion;
    WORD           wHighVersion;
    char           szDescription[WSADESCRIPTION_LEN + 1];
    char           szSystemStatus[WSASYS_STATUS_LEN + 1];
    unsigned short iMaxSockets;
    unsigned short iMaxUdpDg;
    char FAR *     lpVendorInfo;
} WSADATA, * LPWSADATA; 
    wVersion为你将使用的Winsock版本号,wHighVersion为载入的Winsock动态库支持的最高版本,注意,它们的高字节代表次版本,低字节代表主版本。
    szDescription与szSystemStatus由特定版本的Winsock设置,实际上没有太大用处。
    iMaxSockets表示最大数量的并发Sockets,其值依赖于可使用的硬件资源。
    iMaxUdpDg表示数据报的最大长度;然而,获取数据报的最大长度,你需要使用WSAEnumProtocols对
协议进行查询。

利用Delphi编写Windows外壳扩展

01月 5th, 2005 by 天地狂虫

利用Delphi编写Windows外壳扩展
    对于操作系统原理比较了解的朋友都会知道,一个完备的操作系统都会提供了一个外壳(Shell),以方便普通的用户
使用操作系统提供的各种功能。Windows(在这里指的是Windows 95Windows NT4.0以上版本的操作系统)的外壳不但提供
了方便美观的GUI图形界面,而且还提供了强大的外壳扩展功能,大家可能在很多软件中看到这些外壳扩展了。例如在你的
系统中安装了Winzip的话,当你在Windows Explore中鼠标右键点击文件夹或者文件后,在弹出菜单中就会出现Winzip的压
缩菜单。又或者Bullet FTP中在Windows资源管理器中出现的FTP站点文件夹。
    Windows支持七种类型的外壳扩展(称为Handler),它们相应的作用简述如下:

  (1)Context menu handlers:向特定类型的文件对象增添上下文相关菜单;

  (2)Drag-and-drop handlers用来支持当用户对某种类型的文件对象进行拖放操作时的OLE数据传输;

  (3)Icon handlers用来向某个文件对象提供一个特有的图标,也可以给某一类文件对象指定图标;

  (4)Property sheet handlers给文件对象增添属性页(就是右键点击文件对象或文件夹对象后,在弹出菜单中选属性
    项后出现的对话框),属性页可以为同一类文件对象所共有,也可以给一个文件对象指定特有的属性页;

  (5)Copy-hook handlers在文件夹对象或者打印机对象被拷贝、移动、删除和重命名时,就会被系统调用,通过为Windows
    增加Copy-hook handlers,可以允许或者禁止其中的某些操作;

  (6)Drop target handlers在一个对象被拖放到另一个对象上时,就会被系统被调用;

  (7)Data object handlers在文件被拖放、拷贝或者粘贴时,就会被系统被调用。

  Windows的所有外壳扩展都是基于COM(Component Object Model) 组件模型的,外壳是通过接口(Interface)来访问对象的。
外壳扩展被设计成32位的进程中服务器程序,并且都是以动态链接库的形式为操作系统提供服务的。因此,如果要对Windows
的用户界面进行扩充的话,则具备写COM对象的一些知识是十分必要的。 由于篇幅所限,在这里就不介绍COM,读者可以参考
微软的MSDN库或者相关的帮助文档,一个接口可以看做是一个特殊的类,它包含一组函数合过程可以用来操作一个对象。
    写好外壳扩展程序后,必须将它们注册才能生效。所有的外壳扩展都必须在Windows注册表的HKEY_CLASSES_ROOTCLSID键
之下进行注册。在该键下面可以找到许多名字像{0000002F-0000-0000-C000-000000000046}的键,这类键就是全局唯一类标识
符(Guid)。每一个外壳扩展都必须有一个全局唯一类标识符,Windows正是通过此唯一类标识符来找到外壳扩展处理程序的。
在类标识符之下的InProcServer32子键下记录着外壳扩展动态链接库在系统中的位置。与某种文件类型关联的外壳扩展注册在
相应类型的shellex主键下。如果所处的Windows操作系统为Windows NT,则外壳扩展还必须在注册表中的
HKEY_LOCAL_MACHINESoftwareMicrosoftWindowsCurrentVersionShellExtensionsApproved主键下登记。
    编译完外壳扩展的DLL程序后就可以用Windows本身提供的regsvr32.exe来注册该DLL服务器程序了。如果使用Delphi,也可
以在Run菜单中选择Register ActiveX Server来注册。

    下面首先介绍一个比较常用的外壳扩展应用:上下文相关菜单,在Windows中,用鼠标右键单击文件或者文件夹时弹出的那
个菜单便称为上下文相关菜单。要动态地在上下文相关菜单中增添菜单项,可以通过写Context Menu Handler来实现。比如大家
所熟悉的WinZip和UltraEdit等软件都是通过编写Context Menu Handler来动态地向菜单中增添菜单项的。如果系统中安装了
WinZip,那么当用右键单击一个名为Windows的文件(夹)时,其上下文相关菜单就会有一个名为Add to Windows.zip的菜单项。
本文要实现的Context Menu Handler与WinZip提供的上下文菜单相似。它将在任意类型的文件对象的上下文相关菜单中添加一个
文件操作菜单项,当点击该项后,接口程序就会弹出一个文件操作窗口,执行文件拷贝、移动等操作。
     编写Context Menu Handler必须实现IShellExtInit、IContextMenu和TComObjectFactory三个接口。IShellExtInit实现
接口的初始化,IContextMenu接口对象实现上下文相关菜单,IComObjectFactory接口实现对象的创建。
    下面来介绍具体的程序实现。首先在Delphi中点击菜单的 File|New 项,在New Item窗口中选择DLL建立一个DLL工程文件。
然后点击菜单的 File|New 项,在New Item窗口中选择Unit建立一个Unit文件,点击点击菜单的 File|New 项,在New Item窗口
中选择Form建立一个新的窗口。将将工程文件保存为Contextmenu.dpr ,将Unit1保存为Contextmenuhandle.pas,将Form保存为
OpWindow.pas。
Contextmenu.dpr的程序清单如下:
library contextmenu;
    uses
  ComServ,
  contextmenuhandle in ‘contextmenuhandle.pas’,
  opwindow in ‘opwindow.pas’ {Form2};

exports
   DllGetClassObject,
   DllCanUnloadNow,
   DllRegisterServer,
   DllUnregisterServer;

{$R *.TLB}

{$R *.RES}

begin

end.

    Contextmenuhandle的程序清单如下:
unit ContextMenuHandle;

interface
   uses Windows,ActiveX,ComObj,ShlObj,Classes;

type
   TContextMenu = class(TComObject,IShellExtInit,IContextMenu)
   private
      FFileName: array[0..MAX_PATH] of Char;
   protected
      function IShellExtInit.Initialize = SEIInitialize; // Avoid compiler warning
      function SEIInitialize(pidlFolder: PItemIDList; lpdobj: IDataObject;
               hKeyProgID: HKEY): HResult; stdcall;
      function QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst, idCmdLast,
               uFlags: UINT): HResult; stdcall;
      function InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult; stdcall;
      function GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;
               pszName: LPSTR; cchMax: UINT): HResult; stdcall;
end;

const

   Class_ContextMenu: TGUID = ‘{19741013-C829-11D1-8233-0020AF3E97A0}’;

{全局唯一标识符(GUID)是一个16字节(128为)的值,它唯一地标识一个接口(interface)}
var
   FileList:TStringList;


implementation

uses ComServ, SysUtils, ShellApi, Registry,UnitForm;

function TContextMenu.SEIInitialize(pidlFolder: PItemIDList; lpdobj: IDataObject;
   hKeyProgID: HKEY): HResult;
var
   StgMedium: TStgMedium;
   FormatEtc: TFormatEtc;
   FileNumber,i:Integer;
begin
   file://如果lpdobj等于Nil,则本调用失败
   if (lpdobj = nil) then begin
      Result := E_INVALIDARG;
      Exit;
   end;

   file://首先初始化并清空FileList以添加文件
   FileList:=TStringList.Create;
   FileList.Clear;
   file://初始化剪贴版格式文件
   with FormatEtc do begin
      cfFormat := CF_HDROP;
      ptd := nil;
      dwAspect := DVASPECT_CONTENT;
      lindex := -1;
      tymed := TYMED_HGLOBAL;
   end;
   Result := lpdobj.GetData(FormatEtc, StgMedium);

   if Failed(Result) then Exit;

   file://首先查询用户选中的文件的个数
   FileNumber := DragQueryFile(StgMedium.hGlobal,$FFFFFFFF,nil,0);
   file://循环读取,将所有用户选中的文件保存到FileList中
   for i:=0 to FileNumber-1 do begin
      DragQueryFile(StgMedium.hGlobal, i, FFileName, SizeOf(FFileName));
      FileList.Add(FFileName);
      Result := NOERROR;
   end;

   ReleaseStgMedium(StgMedium);
end;

function TContextMenu.QueryContextMenu(Menu: HMENU; indexMenu, idCmdFirst,
   idCmdLast, uFlags: UINT): HResult;
begin
  Result := 0;
  if ((uFlags and $0000000F) = CMF_NORMAL) or
     ((uFlags and CMF_EXPLORE) <> 0) then begin
    // 往Context Menu中加入一个菜单项 ,菜单项的标题为察看位图文件
    InsertMenu(Menu, indexMenu, MF_STRING or MF_BYPOSITION, idCmdFirst,
        PChar(‘文件操作’));
    // 返回增加菜单项的个数
    Result := 1;
  end;
end;

function TContextMenu.InvokeCommand(var lpici: TCMInvokeCommandInfo): HResult;
var
  frmOP:TForm1;
begin
  // 首先确定该过程是被系统而不是被一个程序所调用
  if (HiWord(Integer(lpici.lpVerb)) <> 0) then
  begin
     Result := E_FAIL;
     Exit;
  end;
  // 确定传递的参数的有效性
  if (LoWord(lpici.lpVerb) <> 0) then begin
     Result := E_INVALIDARG;
     Exit;
  end;

   file://建立文件操作窗口
  frmOP:=TForm1.Create(nil);
  file://将所有的文件列表添加到文件操作窗口的列表中
  frmOP.ListBox1.Items := FileList;
  Result := NOERROR;
end;


function TContextMenu.GetCommandString(idCmd, uType: UINT; pwReserved: PUINT;
         pszName: LPSTR; cchMax: UINT): HRESULT;
begin
   if (idCmd = 0) then begin
   if (uType = GCS_HELPTEXT) then
      {返回该菜单项的帮助信息,此帮助信息将在用户把鼠标
      移动到该菜单项时出现在状态条上。}
      StrCopy(pszName, PChar(‘点击该菜单项将执行文件操作’));
      Result := NOERROR;
   end
   else
      Result := E_INVALIDARG;
end;

type
   TContextMenuFactory = class(TComObjectFactory)
   public
   procedure UpdateRegistry(Register: Boolean); override;
end;

procedure TContextMenuFactory.UpdateRegistry(Register: Boolean);
var
   ClassID: string;
begin
   if Register then begin
      inherited UpdateRegistry(Register);
      ClassID := GUIDToString(Class_ContextMenu);
      file://当注册扩展库文件时,添加库到注册表中
      CreateRegKey(‘*shellex’, ‘, ‘);
      CreateRegKey(‘*shellexContextMenuHandlers’, ‘, ‘);
      CreateRegKey(‘*shellexContextMenuHandlersFileOpreation’, ‘, ClassID);

    file://如果操作系统为Windows NT的话
      if (Win32Platform = VER_PLATFORM_WIN32_NT) then
      with TRegistry.Create do
      try
         RootKey := HKEY_LOCAL_MACHINE;
         OpenKey(‘SOFTWAREMicrosoftWindowsCurrentVersionShell Extensions’, True);
         OpenKey(‘Approved’, True);
         WriteString(ClassID, ‘Context Menu Shell Extension’);
      finally
         Free;
      end;
   end
   else begin
      DeleteRegKey(‘*shellexContextMenuHandlersFileOpreation’);
      inherited UpdateRegistry(Register);
   end;
end;

 

initialization
 TContextMenuFactory.Create(ComServer, TContextMenu, Class_ContextMenu,
   ‘, ‘Context Menu Shell Extension’, ciMultiInstance,tmApartment);

end.


    在OpWindow窗口中加入一个TListBox控件和两个TButton控件,OpWindows.pas的程序清单如下:
unit opwindow;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  ExtCtrls, StdCtrls,shlobj,shellapi,ActiveX;

type
  TForm1 = class(TForm)
    ListBox1: TListBox;
    Button1: TButton;
    Button2: TButton;
    procedure FormCreate(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
  private
    { Private declarations }
  public
    FileList:TStringList;
    { Public declarations }
  end;

var
   Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.FormCreate(Sender: TObject);
begin
  FileList:=TStringList.Create;
  Button1.Caption :=’复制文件’;
  Button2.Caption :=’移动文件’;
  Self.Show;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  FileList.Free;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  sPath:string;
  fsTemp:SHFILEOPSTRUCT;
  i:integer;
begin
  sPath:=InputBox(‘文件操作’,'输入复制路径’,'c:windows’);
  if sPath<>’then begin
    fsTemp.Wnd := Self.Handle;
    file://设置文件操作类型
    fsTemp.wFunc :=FO_COPY;
    file://允许执行撤消操作
    fsTemp.fFlags :=FOF_ALLOWUNDO;
    for i:=0 to ListBox1.Items.Count-1 do begin
      file://源文件全路径名
      fsTemp.pFrom := PChar(ListBox1.Items.Strings[i]);
      file://要复制到的路径
      fsTemp.pTo := PChar(sPath);
      fsTemp.lpszProgressTitle:=’拷贝文件’;
      if SHFileOperation(fsTemp)<>0 then
        ShowMessage(‘文件复制失败’);
    end;
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  sPath:string;
  fsTemp:SHFILEOPSTRUCT;
  i:integer;
begin
  sPath:=InputBox(‘文件操作’,'输入移动路径’,'c:windows’);
  if sPath<>’then begin
    fsTemp.Wnd := Self.Handle;
    fsTemp.wFunc :=FO_MOVE;
    fsTemp.fFlags :=FOF_ALLOWUNDO;
    for i:=0 to ListBox1.Items.Count-1 do begin
      fsTemp.pFrom := PChar(ListBox1.Items.Strings[i]);
      fsTemp.pTo := PChar(sPath);
      fsTemp.lpszProgressTitle:=’移动文件’;
      if SHFileOperation(fsTemp)<>0 then
        ShowMessage(‘文件复制失败’);
    end;
  end;
end;

end.

    点击菜单的 Project | Build ContextMenu 项,Delphi就会建立Contextmenu.dll文件,这个就是上下文相关菜单程序了。
使用,Regsvr32.exe 注册程序,然后在Windows的Explore 中在任意的一个或者几个文件中点击鼠标右键,在上下文菜单中就会
多一个文件操作的菜单项,点击该项,在弹出窗口的列表中会列出你所选择的所有文件的文件名,你可以选择拷贝文件按钮或者
移动文件按钮执行文件操作。

利用Windows外壳扩展保护文件夹(转载)

01月 5th, 2005 by 天地狂虫

利用Windows外壳扩展保护文件夹
在Win32操作系统(包括Win9X、Windows NT、Windows 2000)不但有方便的图形用户(GUI)界面,微软还为windows用户界面保留了强大的可扩充性。其中对于Windows界面的操作环境(这里称为外壳Shell),微软提供了一种称为外壳扩展(Shell Extensions)的功能来实现文件系统操作的可编程性。如果你的机器中安装了Word 7.0以上的版本,当你鼠标右键单击一个DOC文件,在弹出菜单中选"属性"项,在属性页中不仅显示显示文件的大小、建立日期等信息,同时还增加了Doc文档的摘要、统计等信息;又例如安装了winZip 6.0以上版本后,当选中一个或多个文件或文件夹后在单击鼠标右键,在弹出的右键菜单中就增加了"Add To Zip"等一个zip文件压缩选项。上面的这些功能都是通过Windows外壳扩展来实现的。
Windows外壳扩展是这样实现的。首先要编写外壳扩展程序,一个外壳扩展程序是基于COM(Component Object Model)组件模型的。外壳是通过接口(Interface)来访问对象的。外壳扩展被设计成32位的进程中服务器程序,并且都是以动态链接库的形式为操作系统提供服务的。
写好外壳扩展程序后,必须将它们注册才能生效。所有的外壳扩展都必须在Windows注册表的HKEY_CLASSES_ROOT\CLSID键之下进行注册。在该键下面可以找到许多名字像{ACDE002F-0000-0000-C000-000000000046}的键,这类键就是全局唯一类标识符。每一个外壳扩展都必须有一个全局唯一类标识符,Windows正是通过此唯一类标识符来找到外壳扩展处理程序的。在类标识符之下的InProcServer32子键下记录着外壳扩展动态链接库在系统中的位置。

Windows系统支持以下7类的外壳扩展功能:
(1)Context menu handlers向特定类型的文件对象增添上下文相关菜单;
(2)Drag-and-drop handlers用来支持当用户对某种类型的文件对象进行拖放操作时的OLE数据传输;
(3)Icon handlers用来向某个文件对象提供一个特有的图标,也可以给某一类文件对象指定图标;
(4)Property sheet handlers给文件对象增添属性页,属性页可以为同一类文件对象所共有,也可以给一个文件对象指定特有的属性页;
(5)Copy-hook handlers在文件夹对象或者打印机对象被拷贝、移动、删除和重命名时,就会被系统调用,通过为Windows增加Copy-hook handlers,可以允许或者禁止其中的某些操作;
(6)Drop target handlers在一个对象被拖放到另一个对象上时,就会被系统被调用;
(7)Data object handlers在文件被拖放、拷贝或者粘贴时,就会被系统被调用。
本文介绍的文件夹保护功能就是通过上面的第5类,既Copy-hook handlers来实现的。一个支持Copy-hook handlers的程序除了上面提到的要在注册表的HKEY_CLASSES_ROOT\CLSID下注册之外,还需要在HKEY_CLASSES_ROOT\Directory\shellex\CopyHookHandlers\下注册服务器程序的类。
由于Windows外壳服务器程序是基于COM组件模型的,所以编写外壳程序就是构造一个COM对象的过程,由于Delphi4.0以上的版本支持Windows外壳扩展和COM组件模型,所以可以利用Delphi来编写外壳扩展程序。
利用Delphi编写Copy-hook handle需要实现ICopyHook接口。ICopyHook是一个十分简单的接口,要实现的只有CopyCallBack方法。ICopyHook的CopyCallBack方法的定义如下:
UINT CopyCallback(
HWND hwnd, file://Handle/ of the parent window for displaying UI objects
UINT wFunc, file://Operation/ to perform.
UINT wFlags, file://Flags/ that control the operation
LPCSTR pszSrcFile, file://Pointer/ to the source file
DWORD dwSrcAttribs, file://Source/ file attributes
LPCSTR pszDestFile, file://Pointer/ to the destination file
DWORD dwDestAttribs file://Destination/ file attributes
);
其中的参数hwnd是一个窗口句柄,Copy-hook handle以此为父窗口。参数wFunc指定要被执行的操作,其取值为下表中所列之一:
常量 取值 含义
FO_COPY $2 复制由pszSrcFile指定的文件到由pszDestFile指定的位置。
FO_DELETE $3 删除由pszSrcFile指定的文件。
FO_MOVE $1 移动由pszSrcFile指定的文件到由pszDestFile指定的位置。
FO_RENAME $4 重命名由pszSrcFile指定的文件到由pszDestFile指定的文件名。
PO_DELETE $13 删除pszSrcFile指定的打印机。
PO_PORTCHANGE $20 改变打印机端口。PszSrcFile和pszDestFile为两个以Null结尾的字符串,分别指定当前和新的打印机端口名。
PO_RENAME $14 重命名由pszSrcFile指定的打印机端口。
PO_REN_PORT $34 PO_RENAME和PO_PORTCHANGE的组合。

参数wFlags指定操作的标志;参数pszSrcFile和pszDestFile指定源文件夹和目标文件夹。参数dwSrcAttribs和dwDesAttribs指定源文件夹和目标文件夹的属性。函数返回值可以为IDYES、IDNO和IDCANCEL。分别指示Windows外壳允许操作、阻止操作,但是其他操作继续、阻止当前操作,取消为执行的操作。
下面是具体的程序实现:
首先在Delphi的菜单中选 File|New选项,选择其中的DLL图标,按Ok键建立一个DLL工程文件,在其中添加以下代码:
library CopyHook;

uses
ComServ,
CopyMain in ‘CopyMain.pas’;

exports
DllGetClassObject,
DllCanUnloadNow,
DllRegisterServer,
DllUnregisterServer;

{$R *.TLB}

{$R *.RES}

begin
end.
将文件保存为 CopyHook.dpr。再在Delphi菜单中选File|New选项,选择其中的Unit图标,按Ok键建立一个Pas文件,在其中加入以下代码:
unit CopyMain;

interface

uses Windows, ComObj, ShlObj;

type
TCopyHook = class(TComObject, ICopyHook)
protected
function CopyCallback(Wnd: HWND; wFunc, wFlags: UINT; pszSrcFile: PAnsiChar;
dwSrcAttribs: DWORD; pszDestFile: PAnsiChar; dwDestAttribs: DWORD): UINT; stdcall;
end;

TCopyHookFactory = class(TComObjectFactory)
protected
function GetProgID: string; override;
procedure ApproveShellExtension(Register: Boolean; const ClsID: string);
virtual;
public
procedure UpdateRegistry(Register: Boolean); override;
end;

implementation

uses ComServ, SysUtils, Registry;

{ TCopyHook }

file://当/Windows外壳程序执行文件夹或者打印机端口操作时,CopyCallBack
file://方/法就会被调用。
function TCopyHook.CopyCallback(Wnd: HWND; wFunc, wFlags: UINT;
pszSrcFile: PAnsiChar; dwSrcAttribs: DWORD; pszDestFile: PAnsiChar;
dwDestAttribs: DWORD): UINT;
const
FO_COPY = 2;
FO_DELETE = 3;
FO_MOVE = 1;
FO_RENAME = 4;
var
sOp:string;
begin
Case wFunc of
FO_COPY: sOp:=format(‘你确定要将 %s 拷贝到 %s 吗?’,[pszSrcFile,pszDestFile]);
FO_DELETE: sOp:=format(‘你确定要将 %s 删除吗?’,[pszSrcFile]);
FO_MOVE: sOp:=format(‘你确定要将 %s 转移到 %s 吗?’,[pszSrcFile,pszDestFile]);
FO_RENAME: sOp:=format(‘你确定要将 %s 重命名为 %s 吗?’,[pszSrcFile,pszDestFile]);
else
sOp:=format(‘无法识别的操作代码 %d’,[wFlags]);
end;
// 提示,让用户决定是否执行操作
Result := MessageBox(Wnd, PChar(sOp),
‘文件挂钩演示’, MB_YESNOCANCEL);
end;

{ TCopyHookFactory }

function TCopyHookFactory.GetProgID: string;
begin
Result := ”;
end;

procedure TCopyHookFactory.UpdateRegistry(Register: Boolean);
var
ClsID: string;
begin
ClsID := GUIDToString(ClassID);
inherited UpdateRegistry(Register);
ApproveShellExtension(Register, ClsID);
if Register then
file://将/clsid 加入到注册表的CopyHookHandlers中
CreateRegKey(‘directory\shellex\CopyHookHandlers\’ + ClassName, ”,
ClsID)
else
DeleteRegKey(‘directory\shellex\CopyHookHandlers\’ + ClassName);
end;

procedure TCopyHookFactory.ApproveShellExtension(Register: Boolean;
const ClsID: string);
const
SApproveKey = ‘SOFTWARE\Microsoft\Windows\CurrentVersion\Shell Extensions\Approved’;
begin
with TRegistry.Create do
try
RootKey := HKEY_LOCAL_MACHINE;
if not OpenKey(SApproveKey, True) then Exit;
if Register then WriteString(ClsID, Description)
else DeleteValue(ClsID);
finally
Free;
end;
end;

const
CLSID_CopyHook: TGUID = ‘{66CD5F60-A044-11D0-A9BF-00A024E3867F}’;
LIBID_CopyHook: TGUID = ‘{D2F531A0-0861-11D2-AE5C-74640BC10000}’;

initialization
TCopyHookFactory.Create(ComServer, TCopyHook, CLSID_CopyHook,
‘CR_CopyHook’, ‘文件操作挂钩演示’,ciMultiInstance, tmApartment);
end.
将文件保存为CopyMain.Pas文件,然后编译程序为CopyHook.Dll文件,然后注册CopyHook.Dll文件,你可以使用Windows提供的RegSvr32.exe来注册,注册的方法是在Dos窗口中进入Windows的System子目录,然后在其中输入Regsvr32 x:\xxx\xxx\copyhook.dll ,其中x:\xxx\xxx\是编译的CopyHook.dll所在的全路径名。也可以在Run菜单中选择Register ActiveX Server来注册。
当文件注册成功之后,在Windows的Explore中任意改变一个文件夹的名字或者移动一个目录,就会有一个提示框弹出,提示用户是否确定执行操作。如图所示:

按"是"将执行文件夹操作,按"否"或者"取消"将取消相应的文件夹操作。
上面介绍的只是Delphi实现Windows外壳扩展的一种,参照上面的程序和Delphi关于Windows的COM组件模型的编程,就可以编写出十分专业化的Windows外壳扩展程序。

铁腕总理–朱镕基

12月 26th, 2004 by 天地狂虫
    
铁腕总理–朱镕基















世界十大不可思议景观

12月 26th, 2004 by 天地狂虫

 

1.北欧附近——天蝎座

2极光——这幅图要仔细看才看得出来,里面有张贞子的脸!

 加拿大与美国阿拉斯加交接处——没太看明白是什么东西,感觉象个乌龟。

某海湾——死亡的武士

南美火地岛——有个骷髅头,感觉象地狱……

南美秘鲁——富兰克林?

南斯拉夫境内——这老猫睡得还挺香的呢!

斯堪的纳维亚半岛的峡湾风光——怎么有一个狮身人面象,神了!

新西兰南岛——太极,不可思议!

 星星和月亮——怎么不是镰刀和锄头

让用户通过宏和插件向您的 .NET 应用程序添加功能(转自MSDN)

12月 25th, 2004 by 天地狂虫
发布日期: 12/17/2004 | 更新日期: 12/17/2004

Jason Clark

本文假设您熟悉 .NET 与 C#

下载本文的代码: Plug-Ins.exe (135KB)

想像一下完美的文本编辑器是什么样子的。 它启动时间不超过两秒,支持针对流行的编程语言的上下文着色和自动缩进,支持多文档界面 (MDI) 以及很酷并且大受欢迎的选项卡式文档排列方式。 构想这种完美的文本编辑器的问题在于完美只存在旁观者的眼中。 这些功能只是我对完美的文本编辑器的定义,其他人肯定会有不同的标准。 也许完美的文本编辑器所能拥有的最重要的功能就是支持丰富的可扩展性,这样开发人员就可以使用他们需要的功能来扩展该应用程序。

可扩展的文本编辑器可能支持创建自定义工具栏、菜单、宏,甚至是自定义文档类型。 它应该允许我编写能挂接到编辑进程的插件,以便添加自动完成、拼写检查及其他诸如此类的美妙功能。 最后,完美的文本编辑器应该能让我用任何语言编写自己的插件(我个人的首选是 C#)。

诚然,我希望所用的每个应用程序都能按这种方式来扩展。 如果在某些地方编写少量代码就可以自定义自己喜欢的应用程序,那就再好不过。 即使我做不到,我也知道其他人能够做到,我再通过下载来从 Internet 利用他们的扩展。 这就是我开展此项活动以让所有开发人员都来编写可扩展应用程序的初衷。

理想的可扩展应用程序

许多应用程序都可以使用可插入代码来修改。 实际上,整个 Microsoft Office 应用程序 套件都可以如此广泛地进行自定义,以致人们能够使用 Office 作为平台来编写完整的自定义应用程序。 然而,即便有这么完备的可自定义能力,我还是为 Microsoft Word(一个我几乎天天使用的应用程序)编写了我的第一个插件。

原因很简单。 Microsoft Office 的所有功能并不能完全符合我的标准,包括:

简单性。 我想以已经很熟悉的非常简单的软件工具来操作我的可插入应用程序。

权限。 我想让我的插件有权访问应用程序中内置的某些对象和功能子集。 这种权限应该是自然而然的,如同我选择的编程语言的一部分。

编程语言。 有时我想使用特别选择的编程语言。

能力。 除了访问应用程序的文档对象模型 (DOM) 外,我还要一个丰富的 API。

安全性。 我需要能够下载其他人编写并且可以通过 Internet 下载的插件。 我希望执行有潜在威胁或错误百出的组件而不必考虑系统的安全。

所列的简短但近乎苛求。 实际上,在 Microsoft .NET Framework 发行之前,这些标准对普通应用程序而言太过严格,是无法做到的。 但现在,我可以向您展示如何使用 .NET Framework 来将所有这些可扩展性功能添加到您的托管和非托管应用程序中。

.NET Framework 可扩展性功能

可扩展性构建在晚期绑定之上,它是指在运行时而非编译时(更典型的情况)发现和执行代码的能力。 在这几年中有许多技术创新对晚期绑定做出了重大贡献,其中包括 DLL 和 COM API。 .NET Framework 将晚期绑定的简单性提高到一个全新的层次。 为加深理解,我们来看一个非常简单的代码示例。

图 1 显示了使用反射在托管对象中执行晚期绑定是如何的简单。 如果您在 LateBinder.exe 内构建 图 1 中的代码并运行它,则可以将任何程序集(比如从图 2 中的代码构建的程序集)的名称作为命令行参数传递给给它。 LateBinder.exe 会反射程序集并在该程序集中创建从 Form 派生的类的实例,并使它们成为它自己的 MDI 子类。 .NET Framework 中的反射使晚期绑定大大简化。

反射是 .NET Framework 的基本工具之一,它促进了可扩展性应用程序的开发。 它是我这里提到的可使应用程序可扩展的四种功能之一。

公共类型系统 使用 .NET Framework 一段时间之后,您可能就会开始认为公共类型系统 (CTS) 理所当然了。 不过,它的确是使该平台中可扩展性变得如此简单的原因之一。 CTS 定义了所有托管语言都必须遵循的部分面向对象特征,例如对派生的规则、命名空间、对象类型、接口和基元类型。 CTS 的这些基本规定是针对公共语言运行库 (CLR) 运行的代码设置的。

反射 反射是在运行时发现信息(例如,程序集实现的类型或类型定义的方法等信息)的能力。 之所以反射成为可能,是因为所有托管代码都是通过嵌入到程序集中的数据结构(称为元数据)自描述的。

Fusion .NET Framework 使用 Fusion 来将程序集加载到托管进程 (AppDomain) 中。 Fusion 有助于实现一些高级功能,如强命名和简化 DLL 的搜索规则。

代码访问安全性 代码访问安全性 (CAS) 是 .NET Framework 的一个功能,可以简化部分受信任的代码的执行。 简而言之,您可以使用 Microsoft .NET Framework 的功能来限制晚期绑定代码可以访问的内容,这样就不用担心插件破坏用户的系统。

这就是.NET Framework 使可扩展性变成现实的四个功能。 然而,由于这些功能是如此的酷,所以一篇文章介绍一个功能不可能将可扩展性讲得非常透彻。 因此,最好的做法是从一个任务出发引入这个话题。

可扩展性入门

不管您的应用程序有什么用途,只要它是可扩展的,就必须执行三个基本任务: 发现、加载和激活扩展。 发现是查找您的应用程序在运行时绑定的插件和其他代码的过程。 加载是将代码(打包为程序集)放入进程或 AppDomain 以便激活并使用由程序集定义的类型的过程。 激活是创建晚期绑定对象的实例并调用它们的方法的过程。

这三个阶段的每一个都包含着许多 .NET 技术和应用程序设计注意事项。 虽然技术上能够做到,但 .NET Framework 并没有定义一种特殊的方式来实现可扩展性,所以您可以有许多选择。

加载: 在运行时绑定到代码

从逻辑上讲,可扩展性应用程序在加载代码之前要先发现它。 但反射必须加载代码才能发现与它有关的内容,所以实际上发现过程可能在加载代码之后。 我们来看一下这是什么意思。

反射可以用来实现晚期绑定。 大多数反射类都可以在 System.Reflection 命名空间中找到。 三种最重要的类是 System.AppDomain、System.Type 和 System.Reflection.Assembly。后面我将会介绍 System.Type。 为了理解 AppDomain 和 Assembly 类,我们简单看一下托管进程。

CLR 在 Win32 进程中运行托管代码,粒度比在非托管应用程序中找到的更细。 例如,一个托管进程可以包含多个 AppDomain,您可以认为后者是一种子进程或轻量级的应用程序容器。

程序集是 DLL 和 EXE 的托管版本,它们包含可重用的对象类型(如类库类型)以及应用程序代码。 另外,应用程序的任何扩展或插件也应该存在于程序集中(与 DLL 非常类似)。 程序集也要加载到托管进程内的一个或多个 AppDomain 中。

每个托管进程至少有一个默认 AppDomain,同时包含某些共享资源,例如托管堆 (heep)、托管线程池和执行引擎本身。 除了这些逻辑组件之外,托管进程还可以创建任意数量的 AppDomain。请参见 图 3,它显示了一个包含两个 AppDomain 的托管线程。在后面有关插件发现的话题中,AppDomain 显得极为重要。

fig03

图 3 托管进程

现在我们回到 AppDomain 和 Assembly 类型。 您可以使用 Assembly.Load 静态方法将程序集加载到当前的 AppDomain 中。 Assembly.Load 方法将引用返回给 Assembly 对象。 这个方法在运行时将代码绑定到您的应用程序,方法是加载驻留代码的程序集。

Assembly.Load 通过名称(不带扩展名)从 AppBaseDir 目录加载程序集(AppDomain 就是从这个目录秘密加载已部署程序集的)。 默认情况下,Assembly.Load 从加载 EXE 的目录中寻找常规可执行程序。 当加载早期绑定的程序集时,Assembly.Load 遵循的程序集绑定规则与 CLR 使用的一样。

可以通过获取 AppDomain 对象的一个引用并调用 AppDomain 的 AppendPrivatePath 和 ClearPrivatePath 实例方法来调整 AppDomain 的 AppBaseDir。 如果您使用 Assembly.Load 加载插件程序集,则可能需要操作 AppDomain 的 AppBaseDir。 这是因为维护主应用程序目录的子目录下的插件非常有用。 具体原因我很快就会解释。

Assembly 类也实现了一个称为 LoadFrom 的方法,它与 Load 的不同之处在于,它带有程序集文件的完整名称(包括路径和扩展名)。 LoadFrom 只是简单地加载您指向的文件,而不是按照绑定和发现规则寻找程序集。 这在加载用作插件的程序集时十分有用。 不过要注意,与 Load 相比,LoadFrom 对性能产生了不利的影响,而且它缺少版本绑定智能,这使得除插件方案外的几乎所有方案都行不通。

一旦获取了 Assembly 对象的引用,您就可以发现包含在程序集中的类型,并创建这些类型的实例和调用方法。 接下来的两节中我将介绍这些操作。

发现: 在运行时发现代码

当编译应用程序时,您必须显式或隐式地告诉编译器应用程序在运行时绑定和使用的代码。 这些代码出现的形式是可广泛重用的类库类型(例如 Object、FileStream 和 ArrayList)以及包装在 helper 程序集中特定于应用程序的类型。 编译器存储在程序集清单中实现这些类型的程序集的引用,而 CLR 在运行时引用该清单以加载必要的程序集。 这就是典型的托管代码绑定过程。

正如前面所提到的,晚期绑定也使用程序集形式的代码;然而,编译器并不直接涉及绑定过程。 相反,代码必须在运行时发现它需要加载哪些程序集。 发现过程可以通过各种方法实现。 事实上,.NET Framework 并没有指定一个发现方法,不过它确实为您提供了实现自己的技术的重要工具。

两种发现机制都值得一提。 第一种也是最简单的一种(从软件角度看)就是维护一个 XML 文件,它记录应用程序在运行时应该绑定的代码。 第二种是比较高级的基于反射的方法,它可以使应用程序的可用性更高。 我们首先来看 XML 方法。

XML 方法只需代码解析 XML 文件并使用其中的信息即可加载程序集。 以下示例展示了一种可能的 XML 格式:

<PluginAssembly name="MyPlugin.dll">
   <Type name="SomeType"/>
   <Type name="AnotherType"/>
</PluginAssembly>
<PluginAssembly name="MorePlugins.dll">
</PluginAssembly>

这个 XML 包含使用反射实现加载过程所需要的最少信息;也就是想要激活的程序集的名称和程序集中一种或多种类型的名称。 代码只需找出 XML 中程序集的名称,然后使用如 图 1 所示的代码将程序集加载到 AppDomain 中。

XML 方法易于实现,但产生的应用程序可用性不高。 例如,我不是特别喜欢这样的文本编辑器:必须编辑某种 .config 文件才能扩展应用程序。 然而,对于服务器端应用程序,这种方法可能比使用反射更合适。

基于反射的发现方法可以使用相对于应用程序目录的已知位置来自动操作。 例如,我为本文编写的示例应用程序在一个名为“Plugins”的子目录中搜索可能的可扩展性程序集(要下载完整的源代码,请转到本文顶部的链接)。 这种特殊的方法比较有用,因为用户只需要将程序集文件复制到文件系统,应用程序在启动时就会绑定新的代码。

有两个原因造成这种方法的实现比 XML 方法困难。 首先,必须制订加载程序集应该遵循的标准。 其次,必须反射程序集以发现它是否符合标准,以及是否应该加载到 AppDomain 中。

有三个有用的标准可用于在作为插件的程序集中为代码确定一种类型。 当将反射方法用于发现时,应该采用这些标准中的一个。

接口标准 代码可以使用反射搜索整个程序集以发现实现已知接口类型的所有类型。

基类标准 代码再次使用反射搜索整个程序集以发现从一个已知基类派生的所有类型。

自定义属性标准 最后,可以使用自定义属性来将一种类型标记为应用程序的插件。

我马上就会向您展示如何使用反射来发现晚期绑定程序集中的某种类型是否实现一个接口、是否从一个基类派生,或者是否属性化。 但首先我们来看一下使用这些绑定标准的设计折衷方案。

接口和基类标准比自定义属性方法(或基于反射的晚期绑定的其他任何方法)更加有用,这有两个原因。 首先,相对于自定义属性而言,插件开发人员很可能对接口或基类比较熟悉。 更重要的是,可以使用接口和基类以早期绑定方式调用晚期绑定代码。 例如,当您将接口作为绑定到一个对象类型的标准进行使用时,则可以通过该接口类型的引用来使用对象的实例。 这样就可以避免需要使用反射来调用方法和访问实例的其他成员,从而提高可扩展性应用程序的性能和代码能力。

要想在运行时发现一个类型的相关信息,可以使用反射类型 System.Type。从 Type 派生的类型的每个实例都引用系统中的一个类型。 Type 实例可以用于在运行时发现关于该类型的一些事实,比如它的基类或它实现的接口。

要想获取由一个程序集实现的一组类型,只需调用 Assembly 对象的 GetTypes 实例方法即可。 GetTypes 方法返回一个 Type 派生的对象的引用数组,程序集中定义的每个类型对应一个引用。

要想确定一个类型是实现一个已知接口还是从某个基类派生,可以使用 Type 对象的 IsAssignableFrom 方法。 以下代码片段展示了如何测试 someType 变量所表示的类型是否实现了 IPlugin 接口:

Boolean ImplementsPluginInterface(Type someType){
   return typeof(IPlugin).IsAssignableFrom(someType);
}

这个原则也适用于测试基类型。

对于发现要在应用程序逻辑中插入哪些类型,加载程序集以反射该程序集中的类型是很有效的。 但如果没有一种类型与您的标准相匹配,会怎么样呢? 简单地说,就是无法卸载在 AppDomain 中已经存在的程序集。 这就是基于反射的发现方法比 XML 方法稍难实现的第二个原因。

对于某些客户端应用程序,将代码反射的所有程序集(甚至不符合插件绑定标准的程序集)都加载到进程中也许还可以接受。 但对于可扩展的服务器应用程序,没有足够的空间来将随后不能卸载的任何程序集都加载进来。 这个问题的解决办法分为两个阶段。 首先,只要卸载加载程序集的整个 AppDomain,就可以卸载该程序集。 其次,可以通过编程方式创建一个临时的 AppDomain,其唯一目的是加载程序集来进行反射,以便发现是否需要将程序集中的类型作为插件使用。 一旦发现阶段结束,就可以卸载这个临时 AppDomain 及其所有程序集。

这种解决办法听起来好像很复杂,但其实要实现的只是一段非常简单的代码。 本文使用的 PluginManager 类采用了这种方法,它要实现的代码不超过 100 行。 如果您想在自己的可扩展性应用程序中采用这种方法,您会发现 PluginManager 的源代码非常有用(请参见下载文件)。

激活: 创建实例和调用方法

一旦确定了要在应用程序中插入哪些类型,接下来就需要创建对象的实例。 这完全可以通过反射来实现;不过您会发现反射速度慢、不实用,而且难以维护。 另外,您还应该选择使用反射来创建插件对象的实例,将对象强制转换为一种已知接口或基类,然后在其整个剩余生命周期内根据已知类型使用这些对象。 请记住,接口和基类标准很适合采用这种方法发现插件类型。

由于有了 CTS,托管代码才是面向对象且类型安全的。 晚期绑定代码面临的一个挑战是在编译时不一定知道对象的类型。 但由于有了 CTS,所以您可以将任何对象看作是 Object 基类,然后将其强制转换为应用程序和可插入代码已知的某个基类或接口。 如果对象无法进行强制转换,则会引发 InvalidCastException 异常,应用程序可以捕获这个异常并进行相应的处理。

然而,在进行任何这样的操作之前,必须创建要绑定到的对象的实例。 与早期绑定对象不同,您不能简单地使用 new 关键字来创建实例,因为编译器需要与 new 一起使用的类型名称,而对于晚期绑定类型,这显然是不知道的。 解决办法是采用静态方法 Activator.CreateInstance。这个方法将创建对象的实例(假定引用对象的 Type 派生的实例)和一个可选的 Object 引用数组(用作构造函数参数)。 以下代码使用 CreateInstance 创建一个对象并返回一个已知接口:

IPlugIn CreatePlugInObject(Type type){
   return (IPlugIn) Activator.CreateInstance(type);
}

一旦拥有一个对象并将其强制转换为一种已知类型,就可以通过引用变量调用该对象的方法,就像调用其他任何对象的方法一样。 此时,晚期绑定代码中的对象就与应用程序的其他部分无缝集成在一起了。

保护可扩展性

如果您的应用程序将广泛分发,而且任何人都可以编写插件,则必须考虑安全性。 幸运的是,.NET Framework 使保护代码变得非常容易。 让我们看一下这是如何做到的。

再次想像一下可扩展文本编辑器。 在理想的情况下,用户应该能够从 Internet 下载插件并安全地将其插入到应用程序中,即使该插件是由非受信任的第三方设计的。 而且如果它不是受信任的,则应该在部分受信任的状态下执行,在这种状态下,它无权访问像文件系统或注册表这样的系统资源。

.NET Framework 可以通过 CAS 执行部分受信任的代码。 因为托管代码是实时编译的,所以 CLR 有能力断言部分受信任的托管代码不执行它缺少权限的操作。 如果部分受信任的代码试图执行一个不被允许的操作,CLR 就会引发 SecurityException 异常,应用程序可以捕获该异常。

在某些情况下,代码默认为部分受信任,比如分布在 Internet 中或嵌入 HTML 文档内的控件。 然而,您也可以利用 CAS,这样用户就可以安全地使用来自第三方的扩展程序集。 在所有情况下,CLR 将一个程序集视为一个安全单元,这意味着应用程序可以包含多个程序集,授予其中每个程序集的安全权限都可能有所不同。 这对插件来说非常适合。

另外,.NET Framework 提供了许多功能,并且有许多方法,您可以使用这些方法来创建部分受信任的插件。 以下步骤采用最简单也是最安全的方法。 首先,为应用程序的插件创建两个子目录。 一个存放完全受信任的插件,另一个存放部分受信任的插件。 然后通过编程方式调整本地安全策略,将代码组与部分受信任插件对应的子目录相关联。 最后,授予代码组中的代码 Internet 权限,也就是说使它拥有一个权限子集,将即使可能有恶意的代码也视为是安全的。

一旦生成了这种代码组,CLR 就会自动将降低的权限与从部分受信任子目录加载的程序集相关联。 除了要调整本地安全策略(我马上就会向您介绍如何来做)外,插件的安全性会自动工作。

调整安全策略

.NET Framework 安全策略引擎非常灵活,可以调整。 在实际情况中,可以使用随 Framework 安装的 .NET Framework Configuration 控制面板 applet 手动调整安全策略(参见 图 4)。 此外,也可以通过编程方式调整策略。 要编写修改安全策略的代码,必须从 SecurityManager 类型开始。 这个有用的类型可以帮助您访问 .NET Framework 安装的三种策略级别: Enterprise、Machine 和 User。 我建议您将插件的自定义代码组添加到 Machine 策略级别中。 要发现 machine PolicyLevel 对象,可以使用如 图 5 所示的代码。

fig04

图 4 安全配置

代码组是以逻辑层次结构排列的。 一旦获取了 PolicyLevel 对象,就可以从 PolicyLevel.RootCodeGroup 属性返回的代码组开始遍历代码组的层次结构。 默认情况下根代码组的名称为 ALL_CODE,它代表所有托管代码。 您应该创建自定义的代码组,作为 ALL_CODE 代码组的子组。

图 6 中的代码为通过特定的 URL 加载的任何代码创建一个自定义代码组。 该代码组具有 Internet 权限,并设置了 PolicyStatementAttribute.Exclusive 和 PolicyStatementAttribute.LevelFinal 位来指示与这个代码组匹配的代码只具有这些权限。 URL 可以是 HTTP、HTTPS 或 FILE URL。 要将这个新代码组与文件系统中的目录相关联,可以使用具有如下结构的文件 URL: file://d:/programs/extensible-app/partially-trusted。

.NET Framework 的 CAS 功能非常灵活,但可能需要一段时间才能习惯使用。 通过阅读本文以及我在下载文件中提供的示例应用程序代码,您应该能够获取创建插件体系结构所需的信息。 不过,我强烈建议您使用 .NET Framework Configuration 控制面板 applet 来尝试对系统策略进行更改,只有这样才能熟悉概念。 如果更改太多,可以随时恢复策略默认值。

可扩展应用程序设计

本文附带的 ExtensibleApp.exe 示例应用程序是一个支持自定义插件的例子。 其实,该应用程序只不过是一个 shell,它仅仅显示一个 MDI 窗口并允许用户安装插件。 如果您正在使用该示例中的代码作为编写自己的可扩展应用程序的学习工具,则应该特别注意 PluginManager.cs 中的代码。该模块包含 PluginManager 可重用类,它为示例应用程序处理所有非特定于应用程序的插件逻辑。

fig07

图 7 插件安装前

如果您构建并运行 ExtensibleApp.exe 示例,则会发现它允许您选择 DLL 作为插件安装到应用程序中。 该示例包含两个插件对象:PluginProject1.dll 和 PluginProject2.dll。 它们利用应用程序本身公开的 API 来创建工具栏和菜单,并在应用程序中添加一个文档类型。 图 7 显示插入自定义代码之前的应用程序,图 8 显示插入之后的应用程序。

fig08

图 8 运行插件

该应用程序用到了本文前面讨论的技术和技巧。 另外,它还展示了一些设计方法,这些方法在使应用程序可扩展时应该加以考虑。 让我们看一下其中一些注意事项。

版本控制

版本控制是应用程序生命周期的一个重要方面。 它也对可扩展应用程序有重要的影响。 可扩展应用程序需要定义用来发现和使用插件的接口、基类型或属性类型。 如果您将这些类型包括在常规应用程序 EXE 或 DLL 程序集中,则它们的版本会由应用程序的其他部分控制,对于不按相同的时间表进行版本控制的插件程序集来说,这可能会产生绑定冲突。 但有一个办法可以解决这个问题。

解决办法就是,应该将用来发现或绑定到插件的任何类型都定义在它自己的程序集中,在该程序集中只有其他由插件产生的类型存在。 还应该避免将任何代码(至少不应该太多)都放在该程序集中,因为需要尽可能少地对该程序集进行版本控制。 同时,也可以根据需要对应用程序的其他部分进行更改和版本控制。 应用程序和插件将共享很少进行版本控制的粘连程序集。

这就带来了一个与插件所使用的基类型有关的问题。 与接口不同,基类一般包含相同的代码。 这是一个棘手的问题,它也是我们更喜欢选择通过接口调用晚期绑定对象的主要原因之一。

然而,您应该知道,如果需要对基类和接口程序集中的代码进行版本控制,.NET Framework 提供的灵活性可以满足您将绑定从旧版本程序集重定向到新版本的需要。 但这需要对绑定策略作出更改,所作的更改必须输入到应用程序的 app.config 文件或整个系统的全局程序集缓存 (GAC) 中。 最好避免这种可能性,因此要管理好插件接口和基类以便尽可能少地对它们进行版本控制(如果曾经这样做过)。

健壮性

请记住,在编写可扩展应用程序时,虽然您的代码能够得到严格的质量控制,但插件代码却很可能没有。 因此,通过接口或基类引用调用晚期绑定对象的任何代码都应该会遇到这些无法预料的问题。 即使开发人员本意不想造成破坏,但部分受信任插件还是可能引发安全异常。 同样,部分受信任插件和完全受信任插件都可能有错误存在,因为插件作者对您的应用程序内部的了解和您是不一样的。

如果您设计的可扩展应用程序只支持由您自己或您的团队编写的插件,则可以不考虑这个问题。 但是许多可扩展应用程序都希望当插件对象出现异常情况时能够尽可能地正常恢复。

托管代码的一个强大之处在于,当一个对象确实运行失败时,它会以一种定义良好的方式失败 — 也就是说该对象会引发一个异常。 所以如果您一贯坚持注册未处理的异常处理程序,并处理调用插件代码应该会产生的异常,则即使插件失败也可以给用户提供一致的体验。

如果您使用接口调用插件对象,则可以使用一种高级技术,也就是将所有晚期绑定对象包装在一个实现了相同接口的代理对象中。 这个常规目的的代理对象会将所有调用传递到基础插件,但也会将与日志记录失败一致的异常处理程序与调用一起包装,同时警告用户插件出现异常及其他这样的情况。 对最终的插件健壮性而言,这是一个很好的主意,但对于许多应用程序来说,可能并不需要如此程度的稳定性。

安全注意事项

只要您制订策略来限制所加载的程序集的权限,.NET Framework 就可以维护部分受信任的插件的安全性。 然而,这虽然对保护系统起了很大作用,但是不能自动保护应用程序的状态。 部分受信任的对象与应用程序中的对象共享一个托管堆,并且需要考虑如何限制它们访问您的内部应用程序对象。 以下是一些要点。

如果没有必要,就不要将应用程序中的对象类型指定为公共对象。 如果类型为内部类型(默认情况),则可以限制应用程序中的部分受信任代码对其进行访问。 然而,很容易在不经意间就将公共类型引入应用程序。 例如,当您将从 Form 派生的类型添加到项目中时,Visual Studio 向导会生成公共类。 这对大多数应用程序来说都是不必要的,所以应该将这些类型中的 public 关键字删除,当您觉得有必要时再添加上去。

同样,如果没有必要,不应该使公共类型的成员为公共或受保护成员。 即使对于不可扩展的应用程序,在您觉得有必要提高它们的可访问性之前,成员也应该是私有的。 因而,如果内部可访问性能够满足要求,就不要提升。 只有当您打算在程序集外公开成员时才使用受保护成员和公共成员。

您应该关注类库类型是否有不好的或不完整的安全策略。 例如,System.Windows.Forms.MainMenu 类公开一个称为 GetForm 的方法,它返回菜单所在的窗体。 这通常是应用程序的主窗体。 即使您不打算将对应用程序主窗体的引用传递给部分受信任的插件,您也可能无意中让插件直接访问应用程序中从 Menu 派生的对象,从而允许插件访问应用程序主窗体。 CLR 类库开发人员考虑了类似的安全问题。 例如,Form.Parent 在返回对父窗体的引用之前要求它的调用者具有一个安全权限。 例如,以 Internet 权限运行的部分受信任代码在默认情况下无法访问该属性。

正如您可以看到的,部分受信任的插件可能无法访问一般文件系统或注册表,但您仍然要提防有恶意的插件执行某些事情,比如关闭您的应用程序。 对于客户端应用程序,此类问题通常无关紧要。 但对于服务器应用程序却是很重大的问题。

在最后一节中,我将简要讨论可扩展应用程序的一些高级可能性。 这些主题范围太广,很难详细介绍,大多数应用程序也可能不需要用到,但了解这些可能性是有帮助的。

可卸载的插件程序集

在本文前面介绍插件的章节中,您可能还记得有关卸载不想要的程序集的问题。 解决这个问题的办法是在临时的 AppDomain 中测试程序集是否有用,以便在需要时将它们卸载。 然后,如果发现有程序集您确实需要用到,可以将它们加载到您的主 AppDomain 中。 但是,如果不想让所有插件无限期地存在,该怎么办呢?

对于客户端应用程序,以下做法是可以接受的:加载插件程序集,使用它们的类型直到它们不再需要使用,然后在应用程序剩余的生命周期内不用再去管它。 然而,在服务器端的要求要严格得多。 服务器端应用程序必须无限期运行,而且不能耗尽诸如进程地址空间等重要资源,所以需要您加载和卸载包含插件类型的程序集。

为做到这一点,您需要在一个临时应用程序域中寻找程序集,然后专门到另一个 AppDomain 中使用该插件类型。 这给应用程序的设计增加了一定的复杂性,但也很好地将插件与核心应用程序逻辑分开。

It is possible to instantiate and use objects entirely from within a separate AppDomain, and the feature of the .NET Framework that implements this is called Remoting.完全在一个独立的 AppDomain 中实例化和使用对象是可以做到的,而在 .NET Framework 中实现它的功能称为远程处理 (Remoting)。 远程处理用于跨进程和跨网络访问对象,但也可以用来访问不同 AppDomain 中的对象(即使它们位于相同的 Windows 进程)。 我无法在这里完整地讲述远程处理,但您可以在 以前出版的 MSDN Magazine 中找到详细的介绍,也可以在我的示例 PluginManager 类型中找到一些简单的远程处理代码。

非托管应用程序中的托管插件

有了这么多强大的可扩展功能可供您使用,但如果您的应用程序采用非托管语言(如 Visual Basic 6.0 或 C++)编写的旧式应用程序, 可能会觉得非常失望。 而现在可以不必失望了。 CLR 本身是一个 COM 对象,它可以宿主在由可以编写 COM 客户端的语言编写的任何 Win32 进程中。

这意味着您可以编写插件托管代码(用 C# 或 Visual Basic .NET),然后通过非托管代码加载运行库和粘连代码,再由该代码以您喜欢的方式加载可与您的非托管应用程序进行交互的插件。 同时,COM Interop 允许您在托管和非托管代码之间来回无缝地传递 COM 接口。

实际上,有三种方式可以宿主非托管程序集中的托管代码和与其进行交互。 可以使用 COM Interop。 CLR 允许您采用 C#、Visual Basic .NET 和其他托管语言创建 COM 服务器。 您可以在任何非托管应用程序中绑定和使用这些托管对象。 也可以使用带托管扩展的 C++ (MC++),它能够将托管和非托管代码自然地混合在单个进程中。 如果您的应用程序是用 C++ 编写的,您会发现托管 C++ 包含丰富的可扩展性功能。 另外,还可以直接宿主 CLR。 CLR 本身是一个可以宿主的 COM 对象。 .NET Framework SDK 带有一个名为 MSCorEE.h 的 C++ 头文件,它包含将运行库作为 COM 对象使用所需的定义。

再次强调,一旦托管代码绑定到非托管应用程序中,托管代码就可以使用本文所介绍的技术来实现应用程序的可扩展性。

小结

.NET Framework 为代码反射、后期绑定和代码安全性提供了一些非常灵活的功能。. 这些功能可以以各种方式混合和搭配使用来实现可扩展应用程序。 如果一个可靠的应用程序也是可扩展的,则可能有更长的寿命,也可能获得进一步的发展,这可以促使更多插件的创建并且更广泛地为人们所接受。 所以请认真研究这些可扩展性功能。 我想结果您会喜欢的。

相关文章请参阅:
.NET Framework: Building, Packaging, Deploying, and Administering Applications and Types
.NET Framework: Building, Packaging, Deploying, and Administering Applications and Types—Part 2
背景资料请参阅:
.NET Framework Security by Brian A. LaMacchia, Sebastian Lange, Matthew Lyons, Rudi Martin, and Kevin T. Price (Addison-Wesley, 2002)

Jason Clark provides training and consulting for Microsoft and Wintellect (http://www.wintellect.com) and is a former developer on the Windows NT and Windows 2000 Server team.Jason Clark 为 Microsoft 和 Wintellect (http://www.wintellect.com) 提供培训和咨询,他以前是 Windows NT 和 Windows 2000 Server 团队的开发人员。 他与人合著了 Programming Server-side Applications for Microsoft Windows 2000 (Microsoft Press, 2000)。 您可以通过 JClark@Wintellect.com与 Jason 联系。

使用 C# 2.0 命令行编译器

12月 25th, 2004 by 天地狂虫
发布日期: 12/22/2004 | 更新日期: 12/22/2004

Andrew W. Troelsen,Microsoft MVP

Intertech Training

适用于:

Microsoft Visual C# 2.0

本文假定您熟悉 C# 编程语言和 .NET Framework 的结构。体验一下使用命令行工具的感觉还将证明很有帮助。

下载 CSCSample.msi 文件。

*
本页内容
scsc.exe 带来的乐趣 scsc.exe 带来的乐趣
C# 编译器选项概览 C# 编译器选项概览
配置环境变量 配置环境变量
命令行基础知识 命令行基础知识
用于指定输入和控制输出的选项 用于指定输入和控制输出的选项
编译 .NET 代码库 编译 .NET 代码库
使用 C# 响应文件 使用 C# 响应文件
使用 /reference 引用外部程序集 使用 /reference 引用外部程序集
理解 C# 2.0 引用别名 理解 C# 2.0 引用别名
使用 /addmodule 生成多文件程序集 使用 /addmodule 生成多文件程序集
创建 Windows 窗体应用程序 创建 Windows 窗体应用程序
通过 csc.exe 使用资源 通过 csc.exe 使用资源
使用 /define 定义预处理器符号 使用 /define 定义预处理器符号
csc.exe 的以调试为中心的选项 csc.exe 的以调试为中心的选项
杂项 杂项
小结 小结


scsc.exe 带来的乐趣

几乎没有人会否认集成开发环境 (IDE)(例如,Visual Studio 2005 和 Visual C# Express 2005)所提供的能使编程工作变得相当简单的诸多功能。但是,实际上 IDE 自己通常不能提供对基础编译器的所有方面的访问。例如,Visual Studio 2005 不支持生成多文件程序集。

此外,了解在命令行编译代码的过程,对于具有以下特征的用户可能有用:

偏爱最简单的生成 .NET Framework 应用程序的方法。

希望揭开 IDE 处理源代码文件的方法的秘密。

希望利用 .NET 生成实用工具,例如,nantmsbuild

没有集成开发环境,例如,Visual Studio(但实际上具有免费提供的 .NET Framework SDK)。

正在基于 Unix的系统(在该系统中,命令行是必须使用的工具)上使用 .NET Framework,并且希望更好地了解 Mono 和/或 Portable .NET ECMA 兼容 C# 编译器。

正在研究当前未集成到 Visual Studio 中的备选 .NET 编程语言

只是希望扩展他们的 C# 编程语言知识。

如果您属于上面所述的这些用户,那么就忠实于自己的选择并继续读下去吧。


C# 编译器选项概览

C# 编译器 csc.exe 提供了大量用于对创建 .NET 程序集的方式进行控制的选项。站在一个较高层次来看,命令行选项属于下列八个类别之一(表 1)。

1. csc.exe 提供的标记的类别

C# 编译器类别 定义

输出文件

用于控制所生成的程序集的格式、可选的 XML 文档文件和强名称信息的选项。

输入文件

使用户可以指定输入文件和引用的程序集的选项。

资源

用于将任何必需的资源(例如,图标和字符串表)嵌入到程序集中的选项。

代码生成

这些选项控制调试符号的生成。

错误和警告

控制编译器处理源代码错误/警告的方式。

语言

启用/禁用 C# 语言功能(例如,不安全代码)以及条件编译符号的定义。

杂项

该类别的最有趣的选项使您可以指定 csc.exe 响应文件。

高级

该类别指定一些更加深奥并且通常不太重要的编译器选项。

1.0 和 1.1 版本的 C# 编译器中存在的 /incremental 标志现在已过时。

在阅读本文的过程中,您将了解每个编译器类别中存在的核心 标志(最重要的词是核心)。对于大多数开发方案,可以安全地忽略 C# 编译器的很多高级选项。如果您需要有关本文未予讨论的 csc.exe 功能的详细信息,请尽管放心,您可以参阅 Microsoft Visual Studio 2005 文档帮助系统(只须从“Search”选项卡中搜索“csc.exe”并深入查阅)。

MSDN 文档也会对您也很所帮助,因为它描述了如何在 Visual Studio(如果可用)内部设置 csc.exe 的特定选项。


配置环境变量

在使用任何 .NET SDK 命令行工具(包括 C# 编译器)之前,需要配置开发计算机以识别它们的存在。最简单的方法是使用 Start | All Programs | Visual Studio 2005 | Visual Studio Tools 菜单选项,启动预配置的 Visual Studio 命令提示。这一特定的控制台能够自动初始化必要的环境变量,而无须您执行任何操作。(Visual Studio .NET 2003 用户需要启动他们各自的命令提示)。

如果您没有 Visual Studio,但是已经安装了 .NET Framework SDK,则可以从 Start | All Programs | Microsoft .NET Framework SDK 2.0 菜单选项启动预配置的命令提示。

如果您希望从任意的 命令提示使用 .NET 命令行工具,则需要手动更新计算机的 Path 变量。做法是,请右键单击桌面上的 My Computer 图标并选择 Properties 菜单选项。从出现的对话框中,单击位于 Advanced 选项卡下面的 Environment Variables 按钮。从出现的对话框中,在 System 变量列表框中的当前 Path 变量的结尾添加以下目录清单(请注意,必须用分号分隔各个条目):

C:\Windows\Microsoft.NET\Framework\v2.0.40607
C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin

上面的列表指向我的当前 .NET 2.0 测试版的路径。您的路径可能因 Visual Studio 和/或 .NET SDK 的安装和版本的不同而略有不同,因此请确保执行完整性检查。

在更新 Path 变量之后,请立即关闭所有对话框和当前打开的任何 Console 窗口,以便提交设置。您现在应当能够从任何命令提示执行 csc.exe 和其他 .NET 工具了。要进行测试,请输入以下命令:

csc -?
ildasm -?

如果您看到有大量信息显示出来,那么您就可以继续了。


命令行基础知识

已经能够熟练地在命令行工作的用户在使用 csc.exe 时不会有任何问题,因而可以跳到下一节。但是,如果您使用命令行的次数很有限,那么请让我说明一些基本的详细信息,以便进行必要的准备。

首先,可以使用反斜杠或单个短划线指定 csc.exe 的选项。其次,在 / 或 – 以及随后的标志之间具有额外的空格是非法 的。因此,“-help”是完全正确,而“- help”就行不通了。为了加以说明,让我们使用 help 标志检查完整的命令行选项集:

csc –help
csc /help

如果一切正常,则您应当看到所有可能的标志,如图 1 所示。


图 1. 帮助标志

很多选项都提供了简写表示法,以便为您节省一些键入时间。假设 help 标志的简写表示法是 ?,则您可以如下所示列出 csc.exe 的选项:

csc –?
csc /?

很多选项都需要附加的修饰,例如,目录路径和文件名。这种性质的标志使用冒号将标志与它的修饰分隔开来。例如,/reference 选项要求将 .NET 程序集的名称包括在程序集清单中:

csc /reference:MyLibrary.dll ...

其他标志在性质上是二元 的,因为它们或者被启用 (+),或者被禁用 (-)。二元标志总是默认为它们的禁用状态,因此您通常只需要使用加号标记。例如,要将所有编译警告都视为错误,可以启用 warnaserror 标志:

csc /warnaserror+ ...

标志的顺序无关紧要,但是在指定输入文件集合之前,必须列出所有标志的集合。另外值得说明的是,在修饰和它的关联数据之间具有额外的空格是非法 的。例如,请使用 /reference:MyLibrary.dll,而不要使用 /reference:MyLibrary.dll。


用于指定输入和控制输出的选项

我们将要分析的第一组命令行选项用于指定编译器输入(表 2)和控制得到的输出(表 3)。请注意,下面的表还标记了特定于 C# 2.0 的选项和任何可用的简写表示法。

2. csc.exe 的以输入为中心的选项

输入标志 定义 是否特定于 C# 2.0?

/recurse

通知 csc.exe 编译位于项目的子目录结构中的 C# 文件。该标志支持通配符语法。

/reference (/r)

用于指定要在当前编译中引用的外部程序集。

否,但是 2.0 编译器提供了别名变体。

/addmodule

用于指定要包括在多文件程序集中的模块。

3. csc.exe 的以输出为中心的选项

输出标志 定义 是否特定于 C# 2.0?

/out

指定要生成的程序集的名称。如果省略该标志,则输出文件的名称基于初始输入文件的名称(对于 *.dll 程序集而言)或定义 Main() 方法的类的名称(对于 *.exe 程序集而言)。

/target (/t)

指定要创建的程序集的文件格式。

/doc

用于生成 XML 文档文件。

/delaysign

使您可以使用强名称的延迟签名生成程序集。

/keyfile

指定用于对程序集进行强命名的 *.snk 文件的路径。

/keycontainer

指定包含 *.snk 文件的容器的名称。

/platform

指定必须存在以便承载程序集的 CPU(x86、Itanium、x64 或 anycpu)。默认为 anycpu。

也许用途最多的输入/输出选项是 /target。该标志通过使用附加的修饰(表 4)告诉编译器您对生成哪个类型的 .NET 程序集感兴趣。

4. /target 标志的变体

目标修饰 定义

/target:exe

创建基于控制台的程序集。如果未指定 /target 选项,则这是默认选项。

/target:winexe

创建基于 Windows 窗体的可执行程序集。尽管您可以使用 /target:exe 创建 Windows 窗体应用程序,但控制台窗口将在主窗体的后台隐现。

/target:library

用于生成 .NET 代码库 (*.dll)。

/target:module

创建将成为多文件程序集的一部分的模块。


编译 .NET 代码库

为了说明使用 csc.exe 的输入/输出选项的过程,我们将创建一个强命名的单文件程序集 (MyCodeLibrary.dll),以定义一个名为 SimpleType 的类类型。为了展示 /doc 选项的作用,我们还将生成一个 XML 文档文件。

首先,请在驱动器 C 上创建一个名为 MyCSharpCode 的新文件夹。在该文件夹中,创建一个名为 MyCodeLibrary 的子目录。使用您选择的文本编辑器(notepad.exe 就完全合乎需要)输入以下代码,并将该文件保存为刚刚创建的 C:\MyCSharpCode\MyCodeLibrary 目录中的 simpleType.cs。

// simpleType.cs
using System;

namespace MyCodeLibrary
{
  /// <summary>
  /// Simple utility type.
  /// </summary>
  public class SimpleType
  {
    /// <summary>
    /// Print out select environment information
    /// </summary>
    public static void DisplayEnvironment()
    {
      Console.WriteLine("Location of this program: {0}",
        Environment.CurrentDirectory);
      Console.WriteLine("Name of machine: {0}",
        Environment.MachineName);
      Console.WriteLine("OS of machine: {0}",
        Environment.OSVersion);
      Console.WriteLine("Version of .NET: {0}",
        Environment.Version);
    }
  }
}

现在,打开命令提示,并且使用 cd(更改目录)命令导航到 simpleType.cs 文件的位置 (C:\MyCSharpCode\MyCodeLibrary):

cd MyCSharpCode\MyCodeLibrary

cd C:\MyCSharpCode\MyCodeLibrary

要将该源代码文件编译为名为 MyCodeLibrary.dll 的单文件程序集,请指定以下命令集:

csc /t:library /out:MyCodeLibrary.dll simpleType.cs

此时,您应当在应用程序目录中具有一个全新的 .NET 代码库,如图 2 所示。


图 2. 新的 .NET 代码库

当在命令行编译多个 C# 文件时,可以分别列出每个文件 — 如果您希望编译包含在单个目录中的 C# 文件的子集,则这可能有所帮助。假设我们已经创建了另外一个名为 asmInfo.cs 的 C# 代码文件(保存在同一目录中),它定义了下列程序集级别属性以描述我们的代码库:

// asmInfo.cs
using System;
using System.Reflection;

// A few assembly level attributes.
[assembly:AssemblyVersion("1.0.0.0")]
[assembly:AssemblyDescription("Just an example library")]
[assembly:AssemblyCompany("Intertech Training")]

要只编译 simpleType.cs 和 asmInfo.cs 文件,请键入:

csc /t:library /out:MyCodeLibrary.dll simpleType.cs asmInfo.cs

正如您可能希望的那样,csc.exe 支持通配符表示法。因而,要编译单个目录中的所有文件,请仅将 *.cs 指定为输入选项:

csc /t:library /out:MyCodeLibrary.dll *.cs

使用 /recurse 指定子目录

在创建应用程序时,您肯定喜欢为您的项目创建逻辑目录结构。您可以通过将代码文件放到特定的子目录(\Core、\AsmInfo、\MenuSystem 等等)中对它们进行组织,而不是将多达 25 个文件转储到名为 myApp 的单个目录中。尽管我们的当前示例只包含几个文件,但假设您将 AsmInfo.cs 文件放到一个名为 \AsmInfo 的新的子目录(如图 3 所示)中。


图 3. 新的 \AsmInfo 子目录

要告诉 C# 编译器编译位于根目录以及 AsmInfo 子目录中的所有 C# 文件,请使用 /recurse 选项:

csc /t:library /out:MyCodeLibrary.dll /recurse:AsmInfo /doc:myDoc.xml *.cs

这里,/recurse 已经用特定子目录的名称限定。要指定多个子目录,我们可以再次使用通配符语法。如果我们要将 simpleType.cs 文件移到一个新的名为 Core 的子目录中,则我们可以用以下命令集编译所有子目录中的所有 C# 文件:

csc /t:library /out:MyCodeLibrary.dll /recurse:*.cs

在任何一种情况下,输出都是相同的。

使用 /doc 生成 XML 文档文件

SimpleType 类已经配备了各种 XML 元素。就像您很可能知道的那样,C# 编译器将使用这些带有三条斜杠的代码注释生成 XML 文档文件。要告诉 csc.exe 创建这样的文件,必须提供 /doc 选项,并且用要生成的文件的名称修饰它:

csc /t:library /out:MyCodeLibrary.dll /recurse:*.cs /doc:myDoc.xml

在应用程序目录中,您现在应当看到一个名为 myDoc.xml 的新文件。如果您打开该文件,则会发现您的类型以 XML 的形式进行了说明,如图 5 所示。


图 5. XML 形式的类型文档

如果您希望了解 C# XML 代码注释的详细信息,则请参阅文章 XML Comments Let You Build Documentation Directly From Your Visual Studio .NET Source Files

使 /keyfile 建立强名称

当前示例的最后一项任务是为我们的程序集分配一个强名称。在 .NET 1.1 下,创建强命名程序集需要使用 [AssemblyKeyFile] 属性。尽管这样做就很好了,但 C# 2.0 编译器现在提供了 /keyfile 标志,以指定强名称密钥文件 (*.snk) 的位置。

在驱动器 C 上创建一个名为 MyKeyPair 的新文件夹,然后使用命令提示更改至该目录。接下来,使用 sn.exe 实用工具的 –k 选项创建一个名为 myKeyPair.snk 的新密钥对。

sn -k myKeyPair.snk

要使用 csc.exe 对 MyCodeLibrary.dll 进行强命名,请发出以下命令集:

csc /t:library /out:MyCodeLibrary.dll /recurse:*.cs /doc:myDoc.xml /keyfile:C:\MyKeyPair\myKeypair.snk

要验证该程序集的确具有强名称,请使用安全实用工具 (secutil.exe) 和 –s 选项显示强名称信息:

secutil /sMyCodeLibrary.dll

您应当发现,程序集清单中记录的公钥值被显示为如图 6 所示的输出。


图 6. 公钥值的输出

C# 2.0 编译器确实还有其他一些以强名称为中心的标志(/delaysign 和 /keycontainer),您可能希望在空闲时加以研究。特别地,如果您希望启用延迟签名,则请使用 /delaysign 选项。


使用 C# 响应文件

尽管通过命令行工作时可以体验到其与生俱来的优势,但没有人能够否认键入数十个编译器选项可能导致手指抽筋和录入错误。为了有助于减轻这两个问题,C# 编译器支持使用响应文件。

所有命令提示都允许您使用 Up 和 Down 箭头键遍历以前的命令。

响应文件(它们按照约定采用 *.rsp 文件扩展名)包含您希望供给到 csc.exe 中的所有选项。在创建了该文件以后,您就可以将它的名称指定为 C# 编译器的唯一选项。为了便于说明,下面提供了一个将用于生成 MyCodeLibrary.dll 的响应文件(请注意,您可以使用 # 符号指定注释)。

# MyCodeLibraryArgs.rsp
#
# These are the options used
# to compile MyCodeLibrary.dll

# Output target and name.
/t:library
/out:MyCodeLibrary.dll 

# Location of C# files.
/recurse:*.cs 

# Give me an XML doc.
/doc:myDoc.xml 

# Give me a strong name as well.
/keyfile:C:\MyKeyPair\myKeypair.snk

给定该文件以后,您现在就可以使用 @ 选项指定 MyCodeLibraryArgs.rsp 了:

csc @MyCodeLibraryArgs.rsp

如果您愿意,则可以指定多个响应文件:

csc @MyCodeLibraryArgs.rsp @MoreArgs.rsp @EvenMoreArgs.rsp

请记住,按照遇到的顺序对响应文件进行处理。因此,以前的文件中的设置可能被以后的文件中的设置重写。

默认的响应文件和 /noconfig 选项

最后,请记住有一个默认的响应文件 — csc.rsp,它由 csc.exe 在每次编译期间自动处理。如果您分析该文件(它与 csc.exe 本身位于相同的文件夹中)的内容,则您将只是发现一组经常引用的程序集(System.Windows.Forms.dll、System.Data.dll 等等)。

在您希望禁止包括 csc.rsp 的极少数的场合中,您可以指定 /noconfig 标志:

csc /noconfig @MyCodeLibraryArgs.rsp

如果您引用程序集,而实际上并不使用它,则它将不会在程序集清单中列出。因此,请不要担心代码膨胀问题,因为它们根本不存在。


使用 /reference 引用外部程序集

此时,我们已经使用命令行编译器创建了具有强名称(并且进行了说明)的单文件代码库。现在,我们需要一个客户端应用程序以便使用它。请在 C:\MyCSharpCode 中创建一个名为 MyClient 的新文件夹。在该文件夹中,创建一个新的 C# 代码文件 (simpleTypeClient.cs),该文件从程序的入口点调用静态的 SimpleType.DisplayEnvironment() 方法:

// simpleTypeClient.cs
using System;

// Namespace in MyCodeLibrary.dll
using MyCodeLibrary;  

namespace MyClientApp
{
  public class MyApp
  {
    public static void Main()
    {
      SimpleType.DisplayEnvironment();
      Console.ReadLine();
    }
  }
}

因为我们的客户端应用程序要使用 MyCodeLibrary.dll,所以我们需要使用 /reference(或只是使用 /r)选项。该标志很灵活,因为您可以指定所讨论的 *dll 的完整路径,如下所示:

csc /t:exe /r:C:\MyCSharpCode\MyCodeLibrary\MyCodeLibrary.dll *.cs

或者,如果私有程序集的副本与输入文件位于相同的文件夹中,则可以只指定程序集名称:

csc /t:exe /r:MyCodeLibrary.dll *.cs

请注意,我没有指定 /out 选项。给定该条件,csc.exe 基于我们的初始输入文件 (simpleTypeClient.cs) 创建了一个名称。此外,已知 /target 的默认行为是生成基于控制台的应用程序,所以 /t:exe 参数是可选的。

在任何情况下,因为 MyCodeLibrary.dll 是私有程序集,所以您需要将该库的一个副本放到 MyClient 目录中。在您完成该工作以后,您就能够执行 simpleTypeClient.exe 应用程序。图 7 显示了可能的测试运行。


图 7. 可能的测试运行输出

请回忆一下这个问题,不必将具有强名称的程序集部署到全局程序集缓存 (GAC) 中。实际上,因为强名称具有天然的安全性方面的好处,所以向每个程序集(无论共享与否)提供强名称是一种 .NET 最佳策略。

引用多个外部程序集

如果您希望在命令行引用大量程序集,则可以指定多个 /reference 选项。为了说明这一点,假设我们的客户端应用程序需要使用包含在名为 NewLib.dll 的库中的类型:

csc /t:exe /r:MyCodeLibrary.dll /r:NewLib.dll *.cs

作为一种稍微简单一些的替代方法,您可以使用单个 /reference 选项,并且使用分号分隔的列表指定每个程序集:

csc /t:exe /r:MyCodeLibrary.dl;NewLib.dll *.cs

当然,在创作 C# 响应文件时使用相同的语法。

关于 /lib 的简短说明

在查看 C# 2.0 程序集别名的作用之前,请允许我对 /lib 标志加以简短说明。该选项可用于将包含由 /reference 选项指定的程序集的目录告诉给 csc.exe。为了进行说明,假设您具有三个位于驱动器 C 的根目录中的 *.dll 程序集。要指示 csc.exe 在 C:\ 下查找 asm1.dll、asm2.dll 和 asm3.dll,需要发出以下命令集:

csc /lib:c:\ /reference:asm1.dll;asm2.dll;asm3.dll *.cs

如果您未使用 /lib,则需要将这三个 .NET 代码库手动复制到包含输入文件的目录中。还请注意,如果在给定的命令集中多次发出 /lib 标志,则结果将累积起来。


理解 C# 2.0 引用别名

关于 /reference 选项需要进行的最后一点说明是,在 C# 2.0 中,现在可以为引用的程序集创建别名。通过该功能,可以解决在唯一命名的程序集中包含的名称完全相同的类型之间存在的名称冲突问题。

为了说明该功能的实用性,请在 C:\MyCSharpCode 目录中创建一个名为 MyCodeLibrary2 的新文件夹。将现有的 simpleType.cs 和 AsmInfo.cs 文件的副本放到新目录中。现在,向 SimpleType 中添加一个能够显示客户端提供的字符串的新方法:

/// <summary>
/// Display a user supplied message.
/// </summary>
public static void PrintMessage(string msg)
{
 Console.WriteLine("You said: {0}", msg);
}

编译这些文件,以创建一个名为 MyCodeLibrary2.dll 的新程序集,如下所示:

csc /t:library /out:MyCodeLibrary2.dll *.cs

最后,将这一新代码库的副本放到 MyClient 文件夹(图 8)中。


图 8. MyClient 文件夹中的新代码

现在,如果我们的当前客户端程序希望引用 MyCodeLibrary.dll 以及 MyCodeLibrary2.dll,则我们执行以下操作:

csc /t:exe /r:MyCodeLibrary.dll;MyCodeLibrary2.dll *.cs

编译器告诉我们,我们已经引入了名称冲突,因为这两个程序集都定义了一个名为 SimpleType 的类:

simpleTypeClient.cs(13,7): error CS0433: The type 'MyCodeLibrary.SimpleType'
  exists in both 'c:\MyCSharpCode\MyClient\MyCodeLibrary.dll' and
  'c:\MyCSharpCode\MyClient\MyCodeLibrary2.dll'

乍看起来,您可能认为可以通过在客户端代码中使用完全限定名称来修复该问题。但是,这样做无法纠正该问题,因为这两个程序集定义了相同的完全限定名称 (MyCodeLibrary。SimpleType)。

使用 /reference 标志的新别名选项,我们可以为引用的每个代码库生成唯一的名称。在我们这样做之后,我们就可以更新客户端代码,以便将某个类型与特定的程序集相关联。

第一步是修改 simpleTypeClient.cs 文件,以使用我们将通过新的 extern alias 语法在命令行指定的别名:

// Extern alias statements must be
// listed before all other code!
extern alias ST1;
extern alias ST2;

using System;

namespace MyClientApp
{
  public class MyApp
  {
    public static void Main()
    {
      // Bind assembly to type using the '::' operator.
      ST1::MyCodeLibrary.SimpleType.DisplayEnvironment();
      ST2::MyCodeLibrary.SimpleType.PrintMessage("Hello!");

      Console.ReadLine();
    }
  }
}

请注意,我们已经使用 C# 2.0 extern alias 语句捕获了在命令行定义的别名。这里,ST1(简单类型 1)是为 MyCodeLibrary.dll 定义的别名,而 ST2 是 MyCodeLibrary2.dll 的别名:

csc /r:ST1=MyCodeLibrary.dll /r:ST2=MyCodeLibrary2.dll *.cs

给定这些别名,请注意 Main() 方法如何使用 C# 2.0 范围解析运算符 (::) 将程序集别名绑定到类型本身:

// This says "I want the MyCodeLibrary.SimpleType class
// that is defined in MyCodeLibrary.dll".
ST1::MyCodeLibrary.SimpleType.DisplayEnvironment();

进而,/reference 选项的这一变体可以提供一种避免名称冲突(当两个具有唯一名称的程序集包含名称完全相同的类型时发生)的方式。


使用 /addmodule 生成多文件程序集

就像您可能已经知道的那样,多文件程序集提供了一种将单个 .NET 二进制文件分解为多个较小的小文件的方式,这在远程下载 .NET 模块时证明很有帮助。多文件程序集的最终效果是让一组文件像一个单独引用和进行版本控制的单元那样工作。

多文件程序集包含一个主 *.dll,它包含程序集清单。该多文件程序集的其他模块(按照约定,它们采用 *.netmodule 文件扩展名)被记录在主模块的清单中,并且由 CLR 按需加载。迄今为止,生成多文件程序集的唯一方式是使用命令行编译器。

为了说明该过程,请在 C:\MyCSharpCode 目录中创建一个名为 MultiFileAsm 的新子目录。我们的目标是创建一个名为 AirVehicles 的多文件程序集。主模块 (Airvehicles.dll) 将包含单个名为 Helicopter 的类类型(稍后定义)。程序集清单编录了一个附加模块 (ufos.netmodule),该模块定义了一个名为 UFO 的类类型:

// ufo.cs
using System;
using System.Windows.Forms;

namespace AirVehicles
{
     public class UFO
     {
          public void AbductHuman()
          {
               MessageBox.Show("Resistance is futile");
          }
     }
}

要将 ufo.cs 编译为 .NET 模块,请指定 /t:module 作为目标类型,这会自动遵循 *.netmodule 命名约定(请回想一下,默认的响应文件自动引用 System.Windows.Forms.dll,因此我们不需要显式引用该库):

csc /t:module ufo.cs

如果您要将 ufo.netmodule 加载到 ildasm.exe 中,您会发现一个记载了该模块的名称和外部引用程序集的模块级别清单(请注意,*.netmodules 没有指定版本号,因为那是主模块的工作):

.assembly extern mscorlib{...}
.assembly extern System.Windows.Forms{...}
.module ufo.netmodule

现在,请创建一个名为 helicopter.cs 的新文件,该文件将用于编译主模块 Airvehicles.dll:

// helicopter.cs
using System;
using System.Windows.Forms;

namespace AirVehicles
{
     public class Helicopter
     {
          public void TakeOff()
          {
               MessageBox.Show("Helicopter taking off!");
          }
     }
}

假设 Airvehicles.dll 是该多文件程序集的主模块,则您将需要指定 /t:library 标志。但是,因为您还希望将 ufo.netmodule 二进制文件编码到程序集清单中,所以您还必须指定 /addmodule 选项:

csc /t:library /addmodule:ufo.netmodule /out:airvehicles.dll helicopter.cs

如果您要分析 airvehicles.dll 二进制文件内部包含的程序集级别清单(使用 ildasm.exe),则会发现 ufo.netmodule 确实是使用 .file CIL 指令记录的:

.assembly airvehicles{...}
.file ufo.netmodule

多文件程序集的使用者可以较少关心他们要引用的程序集由大量二进制文件组成这一事实。实际上,在句法上将看起来与使用单文件程序集的行为完全相同。为了使本文变得更有趣一些,让我们创建一个基于 Windows 窗体的客户端应用程序。


创建 Windows 窗体应用程序

我们的下一个示例项目将是一个使用 airvehicles.dll多文件程序集的 Windows 窗体应用程序。在 C:\MyCSharpCode 目录中创建一个名为 WinFormClient 的新的子目录。创建一个派生自 Form 的类,该类定义了单个 Button 类型,当它被单击时,将创建 HelicopterUFO 类型,并且调用它们的成员:

using System;
using System.Windows.Forms;
using AirVehicles;

public class MyForm : Form
{
  private Button btnUseVehicles = new Button();

  public MyForm()
  {
    this.Text = "My Multifile Asm Client";
    btnUseVehicles.Text = "Click Me";
    btnUseVehicles.Width = 100;
    btnUseVehicles.Height = 100;
    btnUseVehicles.Top = 10;
    btnUseVehicles.Left = 10;
    btnUseVehicles.Click += new EventHandler(btnUseVehicles_Click);
    this.Controls.Add(btnUseVehicles);
}

  private void btnUseVehicles_Click(object o, EventArgs e)
  {
    Helicopter h = new Helicopter();
    h.TakeOff();

    UFO u = new UFO();
    u.AbductHuman();
  }

  private static void Main()
  {
    Application.Run(new MyForm());
  }
}

在继续操作之前,请确保将 airvehicals.dll 和 ufo.netmodule 二进制文件复制到 WinFormClient 目录中。

要将该应用程序编译为 Windows 窗体可执行文件,请确保指定 winexe 作为 /target 标志的修饰。请注意,在引用多文件程序集时,只须指定主模块的名称:

csc /t:winexe /r:airvehicles.dll *.cs

在运行您的程序并单击按钮之后,您就应当看到按照预期显示的每个消息框。图 9 显示了我创建的一个消息框。


图 9. 示例消息框


通过 csc.exe 使用资源

下一个议程是分析如何使用 csc.exe 将资源(例如,字符串表或图像文件)嵌入到 .NET 程序集中。首先,可以使用 /win32Icon 选项指定 Win32 *.ico 文件的路径。假设您已经将一个名为 HappyDude.ico 的图标文件放到当前 Windows 窗体应用程序的应用程序目录中。要将 HappyDude.ico 设置为可执行文件的图标,请发出以下命令集:

csc /t:winexe /win32icon:HappyDude.ico /r:airvehicles.dll *.cs

此时,应当更新可执行程序集,如图 10 所示。


图 10. 经过更新的可执行程序集

除了使用 /win32icon 分配应用程序图标以外,csc.exe 还提供了三个附加的以资源为中心的选项(表 5)。

5. csc.exe 的以资源为中心的选项

csc.exe 的以资源为中心的选项 定义

/resource

将 *.resources 文件内部包含的资源嵌入到当前程序集中。请注意,通过该选项可以“以公共方式”嵌入资源以供所有使用者使用,或者“以私有方式”嵌入资源以便仅供包含程序集使用。

/linkresource

在程序集清单中记录指向外部资源文件的链接,但实际上并不嵌入资源自身。

/win32res

使您可以嵌入存在于旧式 *.res 文件中的资源。

在我们了解如何将资源嵌入到我们的程序集中以前,请让我对 .NET 平台中的资源的性质进行简短的介绍。正如您可能已经知道的那样,.NET 资源开始时通常呈现为一组被记录为 XML(*.resx 文件)或简单文本 (*.txt) 的名称/值对。该 XML/文本文件随后被转换为等效的二进制文件,它采用 *.resources 文件扩展名。然后,这些二进制文件被嵌入到程序集中并且被记录在清单中。当需要以编程方式从该程序集中读取资源的时候,System.Resources 命名空间会提供很多类型来完成该工作,其中最值得注意的是 ResourceManager 类。

尽管您肯定可以手动创建 *.resx 文件,但您最好使用 resgen.exe 命令行工具(或者,您当然可以使用 Visual Studio .NET 本身)。虽然本文不打算描述 resgen.exe 的全部详细信息,但是让我们演练一个简单的示例。

使用 /resource 嵌入资源

MyCSharpCode 下面创建一个名为 MyResourceApp 的新目录。在该目录中,使用 notepad.exe 创建一个名为 myStrings.txt 的新文件,并且使其包含您选择的一些有趣的名称/值对。例如:

# A list of personal data
#
company=Intertech Training
lastClassTaught=.NET Security
lastPresentation=SD East Best Practices
favoriteGameConsole=XBox
favoriteComic=Cerebus

现在,使用命令提示,通过以下命令将 *.txt 文件转换为一个基于 XML 的 *.resx 文件:

resgen myStrings.txt myStrings.resx

如果您使用 notepad.exe 打开该文件,则会找到许多描述名称/值对的 XML 元素,例如:

<data name="company">
  <value xml:space="preserve">Intertech Training</value>
</data>
<data name="lastClassTaught">
  <value xml:space="preserve">.NET Security</value>
</data>

要将该 *.resx 文件转换为二进制的 *.resources 文件,只须将文件扩展名作为 resgen.exe 的参数进行更新:

resgen myStrings.resx myStrings.resources

此时,我们具有了一个名为 myStrings.resources 的二进制资源文件。通过 /resource 标志可以达到使用 csc.exe 将该数据嵌入到 .NET 程序集中的目的。假设我们已经创作了位于 MyResourceApp 目录中的以下 C# 文件 (resApp.cs):

// This simple app reads embedded
// resources and displays them to the
// console.
using System;
using System.Resources;
using System.Reflection;

public class ResApp
{
  private static void Main()
  {
    ResourceManager rm = new ResourceManager("myStrings",
      Assembly.GetExecutingAssembly());
    Console.WriteLine("Last taught a {0} class.",
      rm.GetString("lastClassTaught"));
  }
}

要相对于您的 myStrings.resources 文件编译该程序集,请输入以下命令:

csc /resource:myStrings.resources *.cs

因为我尚未指定 /out 标志,所以在该示例中,我们的可执行文件的名称将基于定义了 Main() 的文件 resApp.exe。如果一切正常,则在执行以后,您应当发现类似于图 11 的输出。


图 11. 输出

我希望您能够轻松地使用 csc.exe 和所选的文本编辑器创建单文件和多文件 .NET 程序集(带有资源!)。您已经学习了 csc.exe 的最常见的命令行选项,而本文的其余部分将分析一些不太常用但仍然有帮助的选项。

如果您愿意继续学习,请在 MyCSharpCode 文件夹中创建一个名为 FinalExample 的新目录。


使用 /define 定义预处理器符号

尽管 C# 编译器没有真正预处理代码,但该语言的确允许我们使用类似于 C 的预处理器符号来定义该编译器以及与其进行交互。使用 C# 的 #define 语法,可以创建能够控制应用程序内部的执行路径的标记。

必须在使用任何语句或其他 C# 类型定义之前列出所定义的符号。

为了利用常见的示例,假设您希望定义一个名为 DEBUG 的符号。为此,请创建一个名为 finalEx.cs 的新文件,并将其保存在 MyCSharpCode\FinalExample 目录中:

// Define a 'preprocessor' symbol named DEBUG.
#define DEBUG

using System;

public class FinalExample
{
  public static void Main()
  {
    #if DEBUG
      Console.WriteLine("DEBUG symbol defined");
    #else
      Console.WriteLine("DEBUG not defined");
    #endif
  }
}

请注意,在我们使用 #define 定义了符号以后,我们就能够使用 #if、#else 和 #endif 关键字来有条件地检查和响应该符号。如果您现在编译该程序,则应当看到在 finalEx.exe 执行时,消息“DEBUG symbol defined”显示到控制台上:

csc *.cs

但是,如果您注释掉符号定义:

// #define DEBUG

则输出将会像您预料的那样(“DEBUG not defined”)。

在 .NET 程序集的开发和测试期间,在命令行定义符号可能有所帮助。这样做可以快速地即时指定符号,而不必更新代码基。为了进行说明,假设您希望在命令行定义 DEBUG 符号,则请使用 /define 选项:

csc /define:DEBUG *.cs

当您再次运行该应用程序时,您应当看到显示“DEBUG symbol defined”— 即使 #define 语句已经被注释掉。


csc.exe 的以调试为中心的选项

即使是最好的程序员,有时也会发现有对他们的代码基进行调试的需要。尽管我假设大多数读者更喜欢使用 Visual Studio .NET 进行调试活动,但对 csc.exe 的一些以调试为中心的选项(表 6)进行说明是值得的。

6. csc.exe 的以调试为中心的选项

csc.exe 的以调试为中心的选项 定义

/debug

指示 csc.exe 发出一个 *.pdb 文件,以供调试工具(例如,cordbg.exe、dbgclr.exe 或 Visual Studio)使用。

/warnaserror

将所有警告视为严重错误。

/warn

使您可以指定当前编译的警告级别(0、1、2、3 或 4)。

/nowarn

使您可以禁用特定的 C# 编译器警告。

/bugreport

如果应用程序在运行时出现故障,则该选项可生成错误日志。该选项将提示您输入纠正信息以发送到您希望的任何地方(例如,QA 小组)。

要说明 /debug 选项的用法,我们首先需要在我们的 finalEx.cs 代码文件中插入一些编码错误。请将以下代码添加到当前的 Main() 方法中:

// Create an array.
string[] myStrings = {"Csc.exe is cool"};

// Go out of bounds.
Console.WriteLine(myStrings[1]);

正如您可以看到的那样,我们试图使用越界索引访问我们的数组的内容。如果您重新编译和运行该程序,则会得到 IndexOutOfRangeException。尽管我们可以明显地指出该错误,但假如是一个不那么明显的更为复杂的错误,又该怎么办呢?

要调试使用 csc.exe 创建的程序集,第一步是生成包含各种 .NET 调试实用工具所需信息的 *.pdb 文件。为此,请输入下列命令(它们在功能上是等效的)之一:

csc /debug *.cs
csc /debug+ *.cs

此时,您应当在应用程序目录中看到一个名为 finalEx.pdb 的新文件,如图 12 所示。


图 12. 应用程序目录中的新 finalEx.pdb

可以根据情况用 full 或 pdbonly 标记限定 /debug 标志。当您指定 /debug:full(它是默认标记)时,将以适当的方式对程序集进行修改,以使其可以附加到当前正在执行的调试器。既然该选项能够 影响所产生的 .NET 程序集的大小和速度,那么请确保只在调试过程中指定该选项。因为 full 是 /debug 标志的默认行为,所以上述所有选项在功能上是等效的:

csc /debug *.cs
csc /debug+ *.cs
csc /debug:full *.cs

另一方面,指定 /debug:pdbonly 可以生成一个 *.pdb 文件,以及一个只能在程序由调试工具直接启动时进行调试的程序集:

csc /debug:pdbonly *.cs

在任何情况下,既然您具有必需的 *.pdb 文件,那么您就可以使用许多调试工具(cordbg.exe、dbgclr.exe 或 Visual Studio)调试应用程序。为了不偏离本文的重点介绍命令行这一特征,我们将使用 cordbg.exe 实用工具调试该程序:

cordbg finalEx.exe

在调试会话开始以后,您就可以使用 so(单步执行)命令单步执行每个代码行了。当您单击出错的代码行时,您可以找到如图 13 所示的代码转储。


图 13. 单步执行命令代码转储

要终止 cordbg.exe 实用工具,请键入 exit 并按 Return 键。

本文的重点不是解释 .NET 调试工具的用法。如果您希望了解有关在命令行进行调试的过程的更多信息,请在 Visual Studio 帮助系统内查找“cordbg.exe”。


杂项

至此,您已经了解了 C# 命令行编译器的核心选项背后的详细信息。为了使本文的内容更加完整,表 7 简要描述了我尚未谈论到的其余标志。

7. csc.exe 的其余选项

csc.exe 的其余选项 定义

/baseaddress

该选项使您可以指定加载 *.dll 的预期基址。默认情况下,该基址由 CLR 选择。

/checked

指定溢出数据类型界限的整数运算是否会在运行时导致异常。

/codepage

指定要用于编译中的所有源代码文件的代码页。

/filealign

该选项控制输出程序集内部的节大小调整(512、1024、2048、4096 或 8192 字节)。如果目标设备是手持型设备(例如,Pocket PC),则可以使用 /filealign 指定可能存在的最小节。

/langversion

该选项指示编译器只使用 ISO-1 C# 语言功能,它基本上可以归结为 C# 1.0 语言功能。

/main

如果当前项目定义了多个 Main() 方法(这在单元测试期间可能有所帮助),则可以使用该标志指定在程序集加载时执行哪个 Main() 方法。

/nostdlib

默认情况下,程序集清单自动引用 mscorlib.dll。指定该选项可以禁止这一行为。

/optimize

当被启用 (/optimize+) 时,可指示编译器尽可能生成最小且最快的程序集。该选项会发出还指示 CLR 在运行时优化代码的元数据。

/platform

该标志告诉编译器针对 32 位或 64 位处理器优化程序集。一般来说,该选项只在 C# 代码基使用 P/Invoke 和/或不安全的代码结构时有用。默认值是“anycpu”。

/unsafe

当被启用时,该选项使 C# 文件可以声明不安全的作用范围,这通常用于操纵 C++ 样式指针。

/utf8output

该选项告诉编译器使用 UTF-8 编码输出数据。

用好你的快速启动栏(转载)

12月 22nd, 2004 by 天地狂虫

快速启动栏中的每个图标都代表计算机上的一个程序,通过单击相应的图标可以快速启动应用程序。Windows系统默认的有Windows Media Player播放器图标、IE浏览器图标、OE图标和显示桌面图标等。当我们安装一些软件之后,也会添加快速启动栏图标。

1、问:一次朋友用过我的电脑之后,不知道怎么回事快速启动栏没有了。弄了很长时间都没有找到,有什么办法呢?

答:不用着急,你的这位朋友一定是不小心把它隐藏掉了。你只要用鼠标在任务栏的空白处右击,勾选和不勾选 “工具栏→快速启动栏”就能显示和隐藏快速启动栏。

2、问:除了系统自带的快速启动栏项目,可以自己手动添加和删除快速启动栏中的项目吗?

答:完全可以。比如你要把记事本程序的快捷方式放到快速启动栏中,那么这样做:首先找到记事本程序所在位置(C:\Windows\Notepad.exe),然后用右键把Notepad.exe拖曳到快速启动栏中,直到出现一竖条时松手,在弹出的快捷菜单中选择“在当前位置创建快捷方式”,这样就添加成功了。

如果你要添加的对象本身就是快捷方式,那就可以用左键直接拖曳即可。还是以添加记事本为例,依次打开“开始菜单/程序/附件”,将记事本图标直接用鼠标左键拖曳到快速启动栏即可。

如果要删除某个项目,只要鼠标指到该图标,然后单击右键在弹出的快捷菜单中选择“删除”。

3、问:一次不小心操作,把快速启动栏移到任务栏的右边,有没有办法让它回到左边?

答:如果你想把它移到左侧,只要把鼠标移到左边任务栏的移动条上,此时鼠标指针会出现双箭头,然后按住左键不放,把任务栏向右方移动,这时候快速启动栏就会回到左边了。

4、问:为了操作的方便,我想调换快速启动栏各图标的位置,该怎样操作?

答:直接用鼠标左键或右键拖动图标,在出现的竖条位于合适位置时松手即可。

5、问:快速启动栏中显示桌面的功能非常有用,可是一次误操作把它删除了,那该如何添加呢?

答:打开快速启动栏所在的文件夹C:\WINDOWS\Application Data\Microsoft\Internet Explorer\Quick Launch。在其中新建一个文本文件,添加内容如下:

[Shell]
Command=2
IconFile=explorer.exe,3
[Taskbar]
Command=ToggleDesktop

保存后,将文件重命名成“显示桌面.scf”即可。(提示:其实没有了也没关系,可以用“Windows键+D”代替,不信你试试!)

6、问:我用了“Windows优化大师”的“使用Windows XP的仿真界面”功能,可是快速启动栏中的图标显示混乱,该怎么办?

答:快速启动栏中的图标有时会变成其他图标,这就意味着系统中的ShellIconCache文件被破坏了。最好启动到安全模式(ShellIconCache文件是隐藏的,所以你要切换到显示所有文件的方式),在Windows目录下找到它,放心地删除这个文件,然后重启动到正常模式,你的图标应该能恢复正常了。或者是使用“Windows优化大师”去除Windows XP的仿真界面功能。

7、问:URL也可以放到Windows的快速启动栏中吗?

答:如果你要经常访问某个网站,又不想把它设置成首页,如Web界面的E-mail收信页面。你倒是可以把它放到这里,方法是:鼠标左键按住网页标题栏前面的“e”字图标不放,一直向下拖到快速启动栏上再放开,即可添加成功。

8、问:可以利用快速启动栏实现鼠标的“一按关机”吗?

答:首先在桌面上创建一个快捷方式,在命令行窗口输入:“rundll.exe user.exe,exitwindows”,单击下一步,命名为“关机”,单击“完成”。然后将此图标用左键直接拖曳到快速启动栏,以后要关机时只要鼠标单击快速启动栏中的该图标,立刻关机,真是方便不小(提示:本技巧适用于Windows 98