BAT批处理、VBScript批量安装字体脚本分享
根据新系统要求,经常要部署一些原来系统里没有的字体,原先我为了图省事经常会要求用户手动安装字体文件,虽然Windows的易用性做得相当不错,但是仍然要照顾一些不会安装字体的人,其实把这些字体打包进安装包更为方便,不过我觉得总不能每有新字体都要搞个安装包那么麻烦吧。更重要的是仍然有人会问我字体怎么安装,以前清一色的WindowsXP系统,我倒也方便,直接告知打开控制面板找到字体文件夹,把要安装的字体拖进去即可;现在有Windows7还是Windows8等各种版本Windows系统,对于安装字体这个小小操作我也开始分情况讨论了。
使用特殊文件夹或者DESKTOP.INI方法
使用特殊文件夹方法
Windows保留了一种特殊文件夹引用,比如在WindowsXP的情况下,新建一个文件夹,然后在文件夹重命名后缀.{645FF040-5081-101B-9F08-00AA002F954E}(注意以点号分隔),然后这个文件夹就变成了回收站的一个引用,当我们点击进去的时候实际上进去的是回收站。
好了我在想对于字体是不是也可以搞个文件夹引用,这样直接叫用户把要安装的字体拖进去即可,大家注意到这个成功的关键在于后面那段长长的ID号,那个学名叫做GUID,通常可以通过注册表查询,主要路径在于:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer
比如回收站就位于下面的注册表路径:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace
对于字体我也在如下路径找到了:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\ControlPanel\NameSpace
字体的GUID是{D20EA4E1-3957-11d2-A40B-0C5020524152},但是当我新建一个文件夹并且名称以.{D20EA4E1-3957-11d2-A40B-0C5020524152}(注意点号)结尾,当我点进去时却不能进入字体文件夹,于是这个想法被验证为失败。
使用Desktop.ini方法
其实建立特殊文件夹还有一个方法就是采用文件夹的Desktop.ini,抱着试试的心态,我在文件夹内部建立了Desktop.ini,内容如下:
[.ShellClassInfo] IconFile=%SystemRoot%\system32\SHELL32.dll IconIndex=38 CLSID={D20EA4E1-3957-11d2-A40B-0C5020524152}
很遗憾,依然不能直达字体目录,所以这一种办法也是行不通的。
本着方便群众的想法,我决定做个小小的程序,当然我首先求助了万能的Google。原本想搞个桌面程序来着,也找到老外现成的代码FontReg–WindowsFontRegistration&InstallationUtility。后来随着研究的深入,突然发现这玩意儿用批处理或者脚本实现更为简单。
CMD或BAT批处理安装字体
通常情况下字体文件夹位于C:\Windows\Fonts,转换为带环境变量的通用版本为%SystemRoot%\Fonts\,我们也许想当然的认为将字体复制到这个路径下就完成了安装,其实不然,系统安装字体不单单是将字体文件复制到这个路径下,其还进行了其他操作,比如更新注册表字体列表。通常情况下这个列表位于路径如下:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Fonts
于是对于批处理来说,网上安装字体流程大概分为两派,首先第一步复制到Fonts文件夹,这个是公认的,第二步则有不同:一派认为应该更新注册表;另一派则倾向于使用AddFontResource这个函数。
使用AddFontResource更新系统字体列表
什么是AddFontResource函数?这是个Win32API函数,位于gdi32.dll动态链接库上,MSDN参考见这里。我们可以编译调用这个函数,什么?“编译”?貌似和这里讲的批处理差远了吧,别急,好在这个函数签名不复杂,其有个AddFontResourceA的ANSI版本,这样给我们直接外部通过rundll32调用提供了可能,例如下面的代码片段:
rundll32.exegdi32.dll,AddFontResourceA%SystemRoot%\Fonts\字体.ttf
具体的代码如下(来源不详,将该批处理和TTF字体位于同一路径下,然后双击即可):
for/f%%ain('dir/x/b*.ttf')do( dir%windir%fonts%%a>nul2>nul||(copy%%a%windir%fonts>nul2>nul&rundll32.exegdi32.dll,AddFontResourceA%windir%fonts%%a) )
实际操作来看,这段代码在我的电脑上没有产生任何效果。
使用注册表更新系统字体列表
参考《Windows7:Installingfontsviacommandline/script》这个帖子,找到下面的代码:
@ECHOOFF TITLEAddingFonts.. REMFilename:ADD_Fonts.cmd REMScripttoADDTrueTypeandOpenTypeFontsforWindows REMByIslamAdel REM2012-01-16 REMHowtouse: REMPlacethebatchfileinsidethefolderofthefontfilesOR: REMOptionalAddsourcefolderasparameterwithendingbackslashanddontusequotes,spacesareallowed REMexample"ADD_fonts.cmd"C:\Folder1\Folder2\ IFNOT"%*"==""SETSRC=%* ECHO. ECHOAddingFonts.. ECHO. FOR/F%%iin('dir/b"%SRC%*.*tf"')DOCALL:FONT%%i REMOPTIONALREBOOT REMshutdown-r-f-t10-c"RebootrequiredforFontsinstallation" ECHO. ECHODone! PAUSE EXIT :FONT ECHO. REMECHOFILE=%~f1 SETFFILE=%~n1%~x1 SETFNAME=%~n1 SETFNAME=%FNAME:-=% IF"%~x1"==".otf"SETFTYPE=(OpenType) IF"%~x1"==".ttf"SETFTYPE=(TrueType) ECHOFILE=%FFILE% ECHONAME=%FNAME% ECHOTYPE=%FTYPE% COPY/Y"%SRC%%~n1%~x1""%SystemRoot%\Fonts\" regadd"HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\Fonts"/v"%FNAME%%FTYPE%"/tREG_SZ/d"%FFILE%"/f GOTO:EOF
仔细阅读代码后发现,这段批处理在复制字体并更新注册表后居然要重启电脑(汗~),这种做法显然对最终用户不太友好,综合以上我决定放弃批处理的方式安装字体。
使用VBSCRIPT安装字体
最后我还是干回老本行,使用VBScript脚本来实现这个功能。脚本的重点是采用Shell.ApplicationActiveX/COM对象实现复制到系统特殊文件夹下,实际上这个操作和用户手动复制到字体文件夹下一样,系统会自动为我们安装字体而不需要我们顾及注册表更新的问题,对于Vista及更高版本的系统来说,我参考了《ThetrueultimatefontinstallforWindows7andXPvbs》的做法,使用.InvokeVerb("Install")直接调用字体文件对象的安装命令。
详细的代码如下(请复制的朋友手下留情,保留版权信息,谢谢):
' 'FileDescription:VBScriptWindowsFontsInstaller ' 'Copyright(c)2012-2013WangYe.Allrightsreserved. ' 'Author:WangYe 'ThiscodeisdistributedundertheBSDlicense ' 'Usage: ' DragFontfilesorfoldertothisscript ' orDoubleclickthisscriptfile,Itwillinstallfontsonthecurrentdirectory ' orselectfontdirectorytoinstall '***请不要移除此版权信息*** ' OptionExplicit ConstFONTS=&H14& ConstHKEY_LOCAL_MACHINE=&H80000002 ConststrComputer="." ConstSHELL_MY_COMPUTER=&H11 ConstSHELL_WINDOW_HANDLE=0 ConstSHELL_OPTIONS=0 FunctionGetOpenDirectory(title) DimShlApp,ShlFdr,ShlFdrItem SetShlApp=WSH.CreateObject("Shell.Application") SetShlFdr=ShlApp.Namespace(SHELL_MY_COMPUTER) SetShlFdrItem=ShlFdr.Self GetOpenDirectory=ShlFdrItem.Path SetShlFdrItem=Nothing SetShlFdr=Nothing SetShlFdr=ShlApp.BrowseForFolder_ (SHELL_WINDOW_HANDLE,_ title,_ SHELL_OPTIONS,_ GetOpenDirectory) IfShlFdrIsNothingThen GetOpenDirectory="" Else SetShlFdrItem=ShlFdr.Self GetOpenDirectory=ShlFdrItem.Path SetShlFdrItem=Nothing EndIf SetShlApp=Nothing EndFunction FunctionIsVista() IsVista=False DimobjWMIService,colOperationSystems,objOperationSystem SetobjWMIService=GetObject("winmgmts:{impersonationLevel=impersonate}!\\"&strComputer&"\root\cimv2") SetcolOperationSystems=objWMIService.ExecQuery("Select*fromWin32_OperatingSystem") ForEachobjOperationSystemIncolOperationSystems IfCInt(Left(objOperationSystem.Version,1))>5Then IsVista=True ExitFunction EndIf Next SetcolOperationSystems=Nothing SetobjWMIService=Nothing EndFunction ClassFontInstaller PrivateobjShell PrivateobjFolder PrivateobjRegistry PrivatestrKeyPath PrivateobjRegExp PrivateobjFileSystemObject PrivateobjDictFontFiles PrivateobjDictFontNames PrivatepfnCallBack PrivateblnIsVista PublicPropertyGetFileSystemObject SetFileSystemObject=objFileSystemObject EndProperty PublicPropertyLetCallBack(value) pfnCallBack=value EndProperty PrivateSubClass_Initialize() strKeyPath="Software\Microsoft\WindowsNT\CurrentVersion\Fonts" SetobjShell=CreateObject("Shell.Application") SetobjFileSystemObject=CreateObject("Scripting.FileSystemObject") SetobjFolder=objShell.Namespace(FONTS) SetobjDictFontFiles=CreateObject("Scripting.Dictionary") SetobjDictFontNames=CreateObject("Scripting.Dictionary") SetobjRegistry=GetObject("winmgmts:{impersonationLevel=impersonate}!\\"&_ strComputer&"\root\default:StdRegProv") SetobjRegExp=NewRegExp objRegExp.Global=False objRegExp.Pattern="^([^\(]+)\(.+$" blnIsVista=IsVista() makeFontNameList makeFontFileList EndSub PrivateSubClass_Terminate() SetobjRegExp=Nothing SetobjRegistry=Nothing SetobjFolder=Nothing objDictFontFiles.RemoveAll SetobjDictFontFiles=Nothing objDictFontNames.RemoveAll SetobjDictFontNames=Nothing SetobjFileSystemObject=Nothing SetobjShell=Nothing EndSub PrivateFunctionGetFilenameWithoutExtension(ByValFileName) 'http://social.technet.microsoft.com/Forums/en-US/ebe19301-541a-412b-8e89-08c4263cc60b/get-filename-without-extension DimResult,i Result=FileName i=InStrRev(FileName,".") If(i>0)Then Result=Mid(FileName,1,i-1) EndIf GetFilenameWithoutExtension=Result EndFunction PrivateSubmakeFontNameList() OnErrorResumeNext DimstrValue,arrEntryNames objRegistry.EnumValuesHKEY_LOCAL_MACHINE,strKeyPath,arrEntryNames ForEachstrValueinarrEntryNames objDictFontNames.AddobjRegExp.Replace(strValue,"$1"),strValue Next IfErr.Number<>0ThenErr.Clear EndSub PrivateSubmakeFontFileList() OnErrorResumeNext DimobjFolderItem,colItems,objItem SetobjFolderItem=objFolder.Self 'Wscript.EchoobjFolderItem.Path SetcolItems=objFolder.Items ForEachobjItemincolItems objDictFontFiles.AddGetFilenameWithoutExtension(objItem.Name),objItem.Name Next SetcolItems=Nothing SetobjFolderItem=Nothing IfErr.Number<>0ThenErr.Clear EndSub FunctiongetBaseName(ByValstrFileName) getBaseName=objFileSystemObject.GetBaseName(strFileName) EndFunction PublicFunctionPathAddBackslash(strFileName) PathAddBackslash=strFileName IfobjFileSystemObject.FolderExists(strFileName)Then Dimlast '文件夹存在 '截取最后一个字符 last=Right(strFileName,1) Iflast<>"\"Andlast<>"/"Then PathAddBackslash=strFileName&"\" EndIf EndIf EndFunction PublicFunctionisFontInstalled(ByValstrName) isFontInstalled=objDictFontNames.Exists(strName)OrobjDictFontFiles.Exists(strName) EndFunction PublicFunctionisFontFileInstalled(ByValstrFileName) isFontFileInstalled=isFontInstalled(objFileSystemObject.GetBaseName(strFileName)) EndFunction PublicSubinstallFromFile(ByValstrFileName) DimstrExtension,strBaseFileName,objCallBack,nResult strBaseFileName=objFileSystemObject.GetBaseName(strFileName) strExtension=UCase(objFileSystemObject.GetExtensionName(strFileName)) IfLen(pfnCallBack)>0Then SetobjCallBack=GetRef(pfnCallBack) Else SetobjCallBack=Nothing EndIf IfstrExtension="TTF"OrstrExtension="TTC"OrstrExtension="OTF"Then IfNotisFontInstalled(strBaseFileName)Then IfblnIsVistaThen DimobjFont,objFontNameSpace SetobjFontNameSpace=objShell.Namespace(objFileSystemObject.GetParentFolderName(strFileName)) SetobjFont=objFontNameSpace.ParseName(objFileSystemObject.GetFileName(strFileName)) 'WSH.EchoobjFileSystemObject.GetParentFolderName(strFileName) objFont.InvokeVerb("Install") SetobjFont=Nothing SetobjFontNameSpace=Nothing Else 'WSH.EchostrFileName objFolder.CopyHerestrFileName EndIf nResult=0 Else nResult=1 EndIf Else nResult=-1 EndIf IfIsObject(objCallBack)Then objCallBackMe,strFileName,nResult SetobjCallBack=Nothing EndIf EndSub PublicSubinstallFromDirectory(ByValstrDirName) DimobjFolder,colFiles,objFile SetobjFolder=objFileSystemObject.GetFolder(strDirName) SetcolFiles=objFolder.Files ForEachobjFileincolFiles IfobjFile.Size>0Then installFromFilePathAddBackslash(strDirName)&objFile.Name EndIf Next SetcolFiles=Nothing SetobjFolder=Nothing EndSub PublicSubsetDragDrop(objArgs) 'http://msdn.microsoft.com/en-us/library/c488f3e0(v=vs.84).aspx Dimi Fori=0toobjArgs.Count-1 IfobjFileSystemObject.FileExists(objArgs(i))Then installFromFileobjArgs(i) ElseIfobjFileSystemObject.FolderExists(objArgs(i))Then installFromDirectoryobjArgs(i) EndIf Next EndSub EndClass SubForceCScriptExecution() 'https://stackoverflow.com/questions/4692542/force-a-vbs-to-run-using-cscript-instead-of-wscript 'http://www.winhelponline.com/articles/185/1/VBScripts-and-UAC-elevation.html DimArg,Str IfNotLCase(Right(WScript.FullName,12))="\cscript.exe"Then ForEachArgInWScript.Arguments IfInStr(Arg,"")ThenArg=""""&Arg&"""" Str=Str&""&Arg Next IfIsVista()Then CreateObject("Shell.Application").ShellExecute_ "cscript.exe","//nologo"""&_ WScript.ScriptFullName&_ """"&Str,"","runas",1 Else CreateObject("WScript.Shell").Run_ "cscript//nologo"""&_ WScript.ScriptFullName&_ """"&Str EndIf WScript.Quit EndIf EndSub SubDisplayMessage(ByRefobjInstaller,ByValstrFileName,ByValnResult) WScript.StdOut.Write"Install"&objInstaller.getBaseName(strFileName)&"->>>" SelectCasenResult Case0 WScript.StdOut.Write"SUCCEEDED" Case1 WScript.StdOut.Write"ALREADYINSTALLED" Case-1 WScript.StdOut.Write"FAILED(Reason:NotaFontFile)" EndSelect WScript.StdOut.WritevbCrLf EndSub SubPause(strPause) WScript.Echo(strPause) WScript.StdIn.Read(1) EndSub FunctionVBMain(colArguments) VBMain=0 ForceCScriptExecution() WSH.Echo"EasyFontInstaller1.0"&vbCrLf&_ "WrittenByWangYe"&vbCrLf&vbCrLf DimobjInstaller,objFso,objDictFontFiles SetobjInstaller=NewFontInstaller objInstaller.CallBack="DisplayMessage" IfcolArguments.Count>0Then objInstaller.setDragDropcolArguments Else SetobjFso=objInstaller.FileSystemObject SetobjDictFontFiles=CreateObject("Scripting.Dictionary") DimobjFolder,colFiles,objFile,strDirName,strExtension strDirName=objFso.GetParentFolderName(WScript.ScriptFullName) SetobjFolder=objFso.GetFolder(strDirName) SetcolFiles=objFolder.Files ForEachobjFileincolFiles IfobjFile.Size>0Then strExtension=UCase(objFso.GetExtensionName(objFile.Name)) IfstrExtension="TTF"OrstrExtension="TTC"OrstrExtension="OTF"Then objDictFontFiles.AddobjFile.Name,objInstaller.PathAddBackslash(strDirName)&objFile.Name EndIf EndIf Next SetcolFiles=Nothing SetobjFolder=Nothing SetobjFso=Nothing IfobjDictFontFiles.Count>0Then IfMsgBox("CurrentDirectoryhas"&objDictFontFiles.Count&"FontFiles."&vbCrLf&_ vbCrLf&"ClickOKtocontinueinstallorCanceltoSelectDirectory",1)=1Then Dimi,objItems Fori=0To objDictFontFiles.Count-1 objItems=objDictFontFiles.Items objInstaller.installFromFileobjItems(i) Next Else strDirName=GetOpenDirectory("SelectFontsDirectory:") IfstrDirName<>""Then objInstaller.installFromDirectorystrDirName Else WScript.Echo"-----DragFontFileToThisScript-----" EndIf EndIf EndIf objDictFontFiles.RemoveAll SetobjDictFontFiles=Nothing EndIf SetobjInstaller=Nothing PausevbCrLf&vbCrLf&"PressEntertocontinue" EndFunction WScript.Quit(VBMain(WScript.Arguments))
这个脚本的使用方法很简单,将上述代码保存为VBS文件,然后将要安装的字体或者包含字体的文件夹拖放到这个脚本文件即可,还有个方法就是直接双击脚本,然后按照提示会自动安装与脚本同路径的字体文件或者提示选择字体所在路径以便于安装。
还有一处值得注意的是:我对已经安装的字体是采取建立字体列表,然后判断当前安装的字体是否存在于字体列表,字体列表的来源是已经安装的字体在系统的注册名(存在于注册表中)和已经安装的字体文件名。唯一遗憾的是我是通过比判断安装字体的文件名是否在字体列表中来判断字体是否安装,这里的问题主要是待安装的字体文件名不一定与字体真实的名字一致,字体真实的名字是需要读取二进制字体文件从中来获取的,这样脚本又复杂了,所以放弃了这种方式。