控制外部程序 WinExec ShellExecute

julian V.Moss


   想由您的Delphi程式控制应用程式的执行吗?这篇文章告诉您如何经由Delphi 1.0以及32 bit的Delphi 2.0执行另处的程式,开启或列印档案,并示 如何在Delphi 2.0中得知所执行的应用程式已经执行完毕,并取得它的结束码(exit code).)

引言部份  在Delphi程式开发者眼中, 有一个经常困扰着大家的问题,那就是如何由自己的Delphi程式控制外部程式的执行.由於Delphi本身并没有提供这方面的功能,因此我们就必须借助於Window API.总括来说,我们有几个选择:在16位元 Window下有二个API可供呼叫,在32位元Window下也有不同的二个API可以运用. 比较简单的方式:WinExec 如果你想要的只是去执行另一个个应用程式,那最简单的方法就是利用户WinExec函式. WinExec可以在WINPROCS单元中找到,它是以下列方式宣告: function WinExec(CmdLine:PChar;CmdShow:Word):Word;

   这个函式有二个参数,第一个参数CmdLine是指到包含要执行的命令而且以null字元结尾的字串指标,字串内容包含所要执行程式的名称以及所需的任何选择参数.由於Window本身会依照正常程序来执行这个程式,因此它并不需要一定要以 .exe,.com或者是.bat为副档名,而且如果这个档案在目前的目录,Window的根目录或是系统目录,Delphi程式所在的目录或者是在系统设定的PATH下找得到的话,我们也可以不给完整的路径资讯. 参数CmdShow则用来指定该应用程式开始执行时的状态,其中可能的值可以是WINTYPES单元中的Window常数中定义的任何一个值.你比较可能用到的有SW_SHOW ,SW_SHOWMAXIMIZED以及SW_SHOWMINIMIZED.其中SW_SHOW表示将程式用它预设值或初始值的设定状态显示出来,另外二个我就不多做解释,大家看字面就应该会明白了. 你也可以用SW_SHOWNOACTIVE来把应用程式叫出并停留在初始状态,然後依然把上前的执行权交回给你写的Delphi程式.指定SW_SHOWMINNOACTIVE时状况大致和上述的一样,只不过被执行的应用程式会保留在最小化的状态. WinExec会传回一个16位元instance,其实它是一个用来辨别所执行程式(正确一点应该是程式模组)的数字.合法 instance handle值必须大於或等於32,如果小於32时,Window会给你一个error code来告诉你WinExec执行失败的原因. 用ShellExecute玩些把戏 WinExec虽然简单好用,但是却只能用来执行程式而已.

   从Windows 3.1开始多了一个新的函式叫做 ShellExecute,在Delphi的SHELLAPI单元中有定义.就如它字面上意义一样,ShellExecute提供像Program Maneger之类Windows Shell程式的功能.这个函式除了提供执行应用程式的功能之外,你可以利用WIN.INI Registration Database或Windows 95 Registry中有关的资讯来开启或列印档案. ShellExecute也可以在我们没有加上.PIF副档名的情况下执行PIF,并且会寻找在WIN.INI或95 registry下(Program)项目中有关该程式的径设定. ShellExecute用起来有点比较复杂,它的宣告如下: functionShellExecute(Wnd:HWnd;Operation,FileName,Parameters,Directory:PChar;ShowCmd:Integer):THandle;
其中Wnd参数为用来设定系统任何可能发生的错误讯息的父视窗的handle,可以把它设为0.而四个PChar参数Operation.FileName.Parameters以及Dircetory 则为指到以null字元结尾字串的指标值,这些值都必须由Pascal型式的字串转换过来. Operation参数指定要执行的功能,在Windows 3.1下只有二个选择: open 以及 print .Windows 95则增加了如 printto 利用rgeistry机制的新功能,可以用来扩充到包含别的执行功能. open 操作的效果和double-click程式的小图式一样,如果在Filename参数中指定的是一个执行档的话,它就会被执行.如果是一个资料档,那麽就会执行它所连结的程式,如果它本身并没有连结到任何程式的话,你会得到一个Windows的错误讯息. print 操作则是用所连结的列印程式把档案内容列印出来,列印的方式则与所安装的列印该型式格式资料的程式有关,如果没有安装可以列印该格式资料的列印程式,那麽Windows就会显示一个错误讯息对话框. Parameters参数包含Filename中所指定程式的命令行参数,如果Filename中指定不是一个可执行的程式,那麽Parameters的内容应该设为NULL. Windows95相容的程式通常都能够支援它所能辨识档案型式的 操作.在这种情况下, 字串就必须包含如印表机,驱动程式和装置类别的名称,请参考以下的例子. ' Microsoft Fax WPSUNI.DRV FAV:' 'Olivetti JP 250 JP350.DRVLPT1:' 由於印表机名称之中包含空格,因此必须用双引号括起来.利用 printto 操作我们可以把文件送到非预设的印表机上,这在Windows 3.1下是无法做到的. Directory参数让你指定Shell操作的启始目录,如果不指定,就使用目前的目录. ShellExecute的传回值THandle和WinExec一样都是程式的instance handle.同样的,我们必须测试它的值是否大於等於32. 程式还在执行吗? 如果想要知道你由WinExec或者是ShellExecute所执行的程式是否还在执行中,我们应该要储存传回来的instance handle值,GetModuleUsage函式(在WINPROCS中宣告)传回由你所给的instance handle所指到的程式模组载入的次数,宣告如下: function GetModuleUsage(Module:THandle):integer;通常这个传回值在程式还在执行时为1,执行结束后为0.以下的程式可以用来测试Hanle值为ih的程式是否还在执行. isRunning:=GetModuleUsage(ih)>0;在通常情况下,我们会肥如上的指令放在一个回圈内,以方便别的程式使用CPU的资源,并一直等到GetModuleUsage的传回值变为0. 把WinExec及ShellExecute重新包装 程式列表一以及列表二分别包含了可以使用WinExec或ShellExecute执行程式的Delphi函式.你必须传给它一个命令行参数,一个用来指定视窗初始状态的SW_SHOW常数以及一个Boolean参数Wait,以告诉这个函式要在开始末执行外部应用程式后就直接跳出,或者是要等到所执行的应用程式执行完毕后才离开.这二个函数会在执行成功后传回True,失败则传回False.

程式列表一:利用WinExec执行程式

function Execute(const commandstr:String;show:Word;wait:Boolean):Boolean;

var ih:Word;

cmdbuf:Array[0..255]of Char;

begin ih:=WinExec(StrPCopy(cmdbuf,commandstr),show);

Result:=ih>=32;

if Result and wait then repeat Application.ProcessMessages until GetModuleUsage(ih)=0;

end;

程式列表二:利用ShellExecute执行程式

function ShellExec(const op,fn,par,dir:String show:Word;wait:Boolean):Boolean;

var ih:Word;

Operation,FileName,Parameters,Directory:PChar;

begin GetMem(Operation,Length(op)+1);

GetMem(Filename,Length(fn)+1);

GetMem(Parameters,Length(par)+1);

GetMem(Directory,Length(dir)+1);

try StrPCopy(Operation,op);

StrPCopy(Filename,fn);

StrPCopy(Parameters,par);

StrPCopy(Directory,dir);

ih:=ShellExecute(0,Opeation,Filename, Parameters,Directory,SW_SHOW);

Result and wait then repeat Application.ProcessMessages until GetModuleUsage(ih)=0;

finally FreeMem(Directory,Length(dir)+1);

FreeMem(Parameters,Length(par)+1);

FreeMem(Filename,Length(fn)+1);

FreeMem(Operation,Length(op)+1);

end;

end;

你可以利用本期所附磁片中的 例程式Exec16来试试这两个函式的功能.

如果你写的程式是打算在Windows95下运作的,那麽利用GetModuleUsage这个函式来测试一个程式是否已经执行结束就会发生一些问题.问题的根源在於在 Win32 底下,instance handle ih 并不是用来辨识某个应用程式的唯一handle.举例来说,如果你一次执行了两个Notepad,那GetModuleUsage会一直等到两个Notepad都结束执行以后才会传回0这个值.幸运的是,这种情况不常发生,不过却也没甚麽好的解决方法,除非使用Win32中新提供的函式来执行外部程式.当然,要这麽做的话,我们就必须要用到Delphi 2.0版了. Delphi2.0和Windows 95 在把使用WinExec或者ShellExec函式的Delphi1.0程式移植到Delphi2.0的环境中并不会造成任何问题,然而由於Win32 API并不支援GetModuleUsage这个函式,因此在像程式列表一或二中这类使用情况下,Delphi2.0的编译器就会给你一个错误讯息.加此如果我想要在Win32底下测试一个程式是否已经执行完毕,就必须利用 32 bit的API函式重写我们的程式. 在Win32中有二个函式可以用来控制应用程式的执行,分别为CreateProcessc以及ShellExecuteEx.其中CreateProcessc大致上具备WinExec的所有功能,不过参数多得吓人,共有10个.其中有4个是指到记录结构(record structure)的指标值,这些记录包含程式安全属性(process security attributes),执行绪安全属性(thread security attributes),启始属性(startup attributes)以及关於这个程序的传回资讯(return information).CreateProcess事实上是一个具有相当多功能的通用函式,执行程式只是其中的一个功能.如果你要的只是执行一个外部的应用和一式的话,使用ShellExecuteEx应该会简单一些.ShellExecuteEx的功用基本上各16位元版本的hellExecute函式差不多,其中Ex代表的是Extended的意思,这个字通常用来标示具有类似功能16位元函式的32位元版本. ShellExecuteEx 定义如下: function ShellExecuteEx(PShellExecuteinfo):Boolean;PShellExecuteinfo 是指到型态为TShellExecuteinfo的一笔记录的指标值,另外这个函式也会在执行成功后传回True,失败则传回False.传给ShellExecuteEx 函式的资讯是以参数方式进行,并且使用TshellExecuteInfo结构,这个结构同时也用来将资讯传回给原呼叫程序. TshellExecuteInfo结构的定义如下:

TShellExecuteInfo=Record cbSize:DWord;

fMask:ULong;

ipVerb:PChar;

ipFile:PChar;

ipParameters:PChar;

ipDirectory:PChar;

nShow:Word;

hlnstApp:ULong;

iplDList:Pointer;

ipClass:PChar;

hkeyClass:hKey;

dwHotKey:DWord;

hlcon:THandle;

hProcess:THandle;

end;

Delphi2.0编译器能接受如C++语言中DWord以及ULong等32位元的无正负号值,这些东西虽然看起来没有Pascal的味道,但是在参考以C++为准所撰写的文件时会变得比较容易. 在使用TShellEXecuteInfo结构之前,我们必须把它初始化为0,并且得在cbSize 栏位中填入结构的大小.fMask栏位含了一些告诉ShellExecuteEx如何来处理结构中其它资料的旗标值(flag),其中最基本的一个是SEE_MASK_NOCLOSEPROCESS,这会使得函式传回在hProcess栏位中所建立的Process的handle值.另外的指标值则用来指出允许的hotKey或者是改变该程式执行时的小图示(icon). 旗标SEE_MASK_CONNECTNETDRY指出lpFile字串是一个在网路上UNC(Universal Naming Convention)路径上的档案,因此系统须连到网路磁碟机.SEE_MASK_DOENVSUBST指出lpFile或lpDirectory字串内的环境变数会被展开.由於这些旗标值都是用位元做mask,因此在同时需要指定两个以上的功能时,我们可以直接将这些SEE_MASK_XXX常数加起来. lpVerb,lpFile,lpParameter和lpDirectory分别是指到代表要进行的操作,档案名称,命令行参数以及启始目录的null结尾字串.nShow的值则可以是SW_SHOW数值中的任一个,用来指定被执行的应用程式起始的状态.lpIDList,lpClass,hKeyClass,dwHotKey以及hIcon栏位通常都没有用到,除非你在fMask中设定一个到目前为止我们还没有提到的旗标值.如果各位读者想写一个 Windows Shell程式,那麽建议你参考Win32 API方面的文件以获得更详细的资讯,因为每个栏位都蛮有用处的,然而如果你只想要由Delphi执行一个程式或者列印档案,你也可以不管上述的这些参数. 函式中还有一个栏位用来收取所执行程式的相关资讯.hInstApp栏位的内容就相当於16元的WinExce或者是ShellExecute呼叫所传回的值.ShellExecuteEx的传回值则包含要被执行程式如果无法启动时的错误码,不过不管怎麽说,最重要的还是hProcess这个代表新执行程序的唯一handle值. 程式还在执行吗?32位元版

我们可以利用hProcess以及新的32位元函式WaitForSingleObject来测试程序是否已经执行完毕.

WaitForSingleObject的宣告如下:

function WaitForSingleObject(hObject:Thandle;dwTimeout:Dword):DWord;

hObject是我们要等待的物件的handle值,在这个例子中就是hProcess程序的handle.dwTimeOut则为此函式要等待的最长时间,以millisecond为单位.传回值DWord则有两种可能,第一是常数WAIT_TIMEOUT,告诉我们经过所设定的时间之后,这个程式还在执行,第二则是传回WAIT_OBJECT_0,用来告诉我们程式已经执行完毕.

用来等待程式执行结束的回圈大致如下:

while WaitForSingleObject(hProcess,100)=WAIT_TIMEOUT do Application.ProcessMessage;其中WaitForSingleObject将你的Delphi程式(或者更精确地,执行绪,不过通常都一样,除非你写的是一个多执行绪的程式)在等待timeout的期间完全进入睡眠状态. 当程式在睡眠状态时它并不能处理传进来的讯息,也就是说,如果程式在最小化状态或者因为有别的程式执行遮盖到它的视窗画面之后又结束时,它便无法再重绘自己的视窗,因此在选择dwTimeOut的值时必须不能太长,譬如说100ms,以便在程式的执行效率以及程式必须要能回应发生事件所需等待的时间两者之间取得最佳的协调. 取得结束码 32位元Windows API提供了程式师长久以来想要的东西,那就是提供被叫用程式(通常为DOS程式,虽然Windows程式也会传回结束码)的结束码.要取得结束码相当简单,只要在程序结束时使用GetExitCodeProcess呼叫即可,

函式宣告如下:

function GetExitCodeProcess(hProcess:THandle;var lpExitCode:DWord):Boolean;

这时,hProcess这个handle依旧是代表这个程式的唯一值,传回的值(或者例外情况下传回的例外值)会被放到变数lpExitCode中,如果这个程式还在运作,lpExitCode的值就是STILL_ACTIVE.如果有某些原因造成它执行错误,GetExitCodeProcess会传回false. 32位元版的ShellExec 程表式列表叁是一个32位元版的ShellExec函式,它用的参数与程式列表二中的16位元版本一样,不同的是,传回的是一个32位元的值.其中wait参数如果设为 True,函式将会等到所执行程式结束才回传回它的结束码,如果设为False的话,那麽就会因为无法得到结束码而只传回0.如果其中呼叫ShellExecEx或者是GetExitCodeProcess的过程有问题的话,传回值则为-1. 程式列表叁是,利用ShellExecuteEx执行程式

fumction ShellExec(op,fn,par,dir:PChar;show:Word;wait:Boolean):Longlnt;

var ih:Word;

OK:Boolean;

info:TShellExecutelnfo;

begin FillChar(info,SizeOf(info),Chr(0));

info.cbSize:=SizeOf(info);

info.fMask:=SEE_MASK_NOCLOSEPROCESS;

info.ipVerb:=op info.ipFile:=fn;

info.lpParameters:=par;

info.lpDirectory:=dir;

info.nShow:=show;

OK:Boolean(ShellExecuteEx(@info));

if OK then bgein if wait then bgein while WaitForSingleObject(info.hProcess,100)=WAIT_TINEOUT do Application.ProcessMessages;

OK:=GetExitCodeProcess(info.hProcess,DWord(Result));

end else Result:=0;

end;

if not OK then Result:=-1;

end;

磁片中的 例程式必须用Delphi 2.0来编译,它使得你可以在把ShellExec 函式并入你的应用程式之前先试一试函式的功能. 总结 由所撰写的Delphi应用程式来控制外部不管是DOS或者是Windows程式的执行可以应用在许多的场合.藉由本篇文章所介绍的函式可以轻易地把这个功能加到你的程式中,如果你使用了上述的ShellExec函式,那麽由於它在16位元以及32位元版本上的定义相当类似,因此在需要把程式由16位元转换到Delphi 2.0时,事情将会变得非常简单,只要做一些小修改就可完成.

你可能感兴趣的:(windows,单元测试,资讯,Delphi,pascal)