使用C#执行MySQL的SQL文件

最近的一个项目,需要在初始化时建库及建表,因为建表的SQL语句是从MySQL中导出的,所以在建库之后,需要执行导出的Sql语句实现建表及插入数据。方法如下:

[csharp] view plain copy

  1. /// <summary> 
  2. /// 执行Sql文件 
  3. /// </summary> 
  4. /// <param name=”varFileName”>sql文件</param> 
  5. /// <param name=”Conn”>连接字符串</param> 
  6. /// <returns></returns> 
  7. private bool ExecuteSqlFile(string varFileName, String Conn)
  8. {
  9.     using (StreamReader reader = new StreamReader(varFileName, System.Text.Encoding.GetEncoding(“utf-8”)))
  10.     {
  11.         MySqlCommand command;
  12.         MySqlConnection Connection = new MySqlConnection(Conn);
  13.         Connection.Open();
  14.         try
  15.         {
  16.             string line = “”;
  17.             string l;
  18.             while (true)
  19.             {
  20.                 // 如果line被使用,则设为空
  21.                 if (line.EndsWith(“;”))
  22.                     line = “”;
  23.                 l = reader.ReadLine();
  24.                 // 如果到了最后一行,则退出循环
  25.                 if (l == nullbreak;
  26.                 // 去除空格
  27.                 l = l.TrimEnd();
  28.                 // 如果是空行,则跳出循环
  29.                 if (l == “”continue;
  30.                 // 如果是注释,则跳出循环
  31.                 if (l.StartsWith(“–“)) continue;
  32.                 // 行数加1 
  33.                 line += l;
  34.                 // 如果不是完整的一条语句,则继续读取
  35.                 if (!line.EndsWith(“;”)) continue;
  36.                 if (line.StartsWith(“/*!”))
  37.                 {
  38.                     continue;
  39.                 }
  40.                 //执行当前行
  41.                 command = new MySqlCommand(line, Connection);
  42.                 command.ExecuteNonQuery();
  43.             }
  44.         }
  45.         finally
  46.         {
  47.             Connection.Close();
  48.         }
  49.     }
  50.     return true;
  51. }
查看评论
5楼 chengjun163 2012-11-22 09:42发表 [回复]
weikai20007你那边可以执行成功吗?
Re: weikai20007 2012-12-06 19:31发表 [回复]
回复chengjun163:已经解决,链接:

http://www.cnblogs.com/easy5weikai/archive/2012/12/06/2805558.html

Re: 骏爷在此 2015-11-17 14:56发表 [回复]
回复weikai20007:这个方法遇到异常就停止执行了
Re: weikai20007 2012-12-06 13:10发表 [回复]
回复chengjun163:可以执行,我只是在楼主的基础上处理了DELIMITER,没处理其它特殊字符。
// 如果是空行,则跳出循环
if (temp == “” || temp == cmdEndFlag) continue;
// 如果是注释,则跳出循环
if (temp.StartsWith(“–“)) continue;

// 行数加1
lineToExecute += temp;
// 如果不是完整的一条语句,则继续读取
if (!lineToExecute.EndsWith(cmdEndFlag)) continue;//(!lineToExecute.EndsWith(“;”)) continue;
if (lineToExecute.StartsWith(“/*!”))
{
continue;
}

//执行当前行
command = new MySqlCommand(lineToExecute, Connection);
command.ExecuteNonQuery();
}
}
finally
{
Connection.Close();
}
}

return true;
}
}

// 如果line被使用,则设为空
if (lineToExecute.EndsWith(cmdEndFlag))//(lineToExecute.EndsWith(“;”))
lineToExecute = “”;

temp = reader.ReadLine();

if (temp != null && temp.Contains(“DELIMITER”))
{
temp = temp.Replace(“DELIMITER”,” “);
temp = temp.Trim();

cmdEndFlag = temp;
}

// 如果到了最后一行,则退出循环
if (temp == null) break;
// 去除空格
temp = temp.TrimEnd();

/// <summary>
/// 执行Sql文件
/// </summary>
/// <param name=”varFileName”>sql文件</param>
/// <param name=”Conn”>连接字符串</param>
/// <returns></returns>
public bool ExecuteSqlFile(string varFileName, String Conn)
{
//没处理lineToExecute.Contains(“DELIMITER ;;”)
string cmdEndFlag = “;”;

using (StreamReader reader = new StreamReader(varFileName, System.Text.Encoding.GetEncoding(“utf-8”)))
{
MySqlCommand command;
MySqlConnection Connection = new MySqlConnection(Conn);
Connection.Open();
try
{

楼主,你没有考虑脚本文件中存在 DELIMITER 的特殊情况,在此斗胆班门弄斧下,在你的基础上修改了下,借花献佛。

WPF 设置TextBox为空时,背景为文字提示。

<TextBox FontSize="17" Height="26" Margin="230,150,189,0" Name="txt_Account" VerticalAlignment="Top" Foreground="Indigo" TabIndex="0" BorderThickness="1">
            <TextBox.Resources>
                <VisualBrush x:Key="HelpBrush" TileMode="None" Opacity="0.3" Stretch="None" AlignmentX="Left">
                    <VisualBrush.Visual>
                        <TextBlock FontStyle="Italic" Text="请输入用户名"/>
                    </VisualBrush.Visual>
                </VisualBrush>
            </TextBox.Resources>
            <TextBox.Style>
                <Style TargetType="TextBox">
                    <Style.Triggers>
                        <Trigger Property="Text" Value="{x:Null}">
                            <Setter Property="Background" Value="{StaticResource HelpBrush}"/>
                        </Trigger>
                        <Trigger Property="Text" Value="">
                            <Setter Property="Background" Value="{StaticResource HelpBrush}"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </TextBox.Style>
        </TextBox>

关于Installshield里一些常见问题的解答—艾泽拉斯之海洋女神出品

Q: 如何替换setup.exe的图标?

A: 这不是一个推荐的操作,因为可能会引起不可预见的错误,而且IS没有开放这个接口。如果你坚持要这么做,可以使用第三方软件比如ExeScope来进行图标替换。

Q: 如何去掉安装界面左上方的Installshield Wizard字样?

A: Installation Designer -> Installation Information -> General Information -> String Tables ->你使用的语言,右键-> Export String Tables导出为文本文件,然后把里面相应的“Installshield Wizard”字段替换成空白字符串即可。建议做好备份后再修改。

Q: 如何自定义每个安装界面的标题或者说明文字?

A: Installation Designer -> Behavior and Logic -> InstallScript ->Setup.rul,没有这个文件的话在file底下new一个,默认就是这个名字的,我是在Installscript msi类型下测试的,其他的可能略有不同。

在Setup.rul打开OnFirstUIBefore函数,这个函数里面显示出安装过程中的所有界面,在需要修改的界面中把标题或者说明性文字赋值为自己想要的值即可。

举例:

Dlg_SdWelcome:

szTitle = “”;

szMsg = “”;

nResult = SdWelcome(szTitle, szMsg);

if (nResult = BACK) goto Dlg_SdWelcome;

szTitle = “”;

svName = “”;

svCompany = “”;

szTitle就是安装时候欢迎界面所显示的标题,比如我改成szTitle=”欢迎使用XX软件”这样的形式即可。

Q: 如何在卸载时不要弹出”Modify, Repair, Remove”界面,而是点击卸载后直接卸载掉?

A: 在Installation Designer->Behavior and Logic->InstallScrip中Setup.rul里打开OnMaintUIBefore这个函数,找到Dlg_Start:,把这个框体包含的代码全部注释掉,也就是从Dlg_Start:到Dlg_SdFeatureTree:上面一行的内容全部去掉,并且添加一句nType = REMOVEALL;在Dlg_SdFeatureTree:前面,这样就可以实现不出现那个“modify,repair, remove”的界面,而是直接进行删除动作了。

Q: 我怎样在目标机上安装.NET,如果目标机上没有安装的话?

A: 在Release Wizard的倒数第三步将”Include or setup .NET framework”的选项勾上。

Q: 我如何调用第三方软件?

A: 在Installation Designer -> Behavior and Logic -> InstallScript ->Setup.rul里使用LaunchAppAndWait或者LaunchApp函数,至于写在哪儿要看具体应用,比如可以写在OnBegin, OnFirstUIBefore之类的函数体里。例子请参阅我的另外一个文档《一个完整的Installshield安装程序—艾泽拉斯之海洋女神出品》,csdn有下载。或者你也可以加入installshield中文论坛官方QQ群来获取这份放在群共享里的文件。

在笔者写这篇文档的时候,有网友告诉我Installshield2009已经无需如此繁琐地写脚本了,而是有界面允许用户自己指定一个注册表键值,并指定需要安装的软件所在的路径,当键值不存在的时候IS自动调用指定的软件了,有兴趣的朋友可以去研究下。

Q: 我如何调用bat文件?

A: 在Installation Designer -> Behavior and Logic -> InstallScript ->Setup.rul里使用LaunchAppAndWait或者LaunchApp函数。

Q: 我如何为自己的程序创建一个在开始菜单里的卸载快捷方式?

A: 某些工程类型比如Installscript MSI自带有这个选项,但是在08版本前都不推荐使用,因为容易导致系统崩溃;写脚本是一个不错的解决方法。代码如下,但是具体的详细解释请参考我的另外一个文档《一个完整的Installshield安装程序—艾泽拉斯之海洋女神出品》,csdn有下载。或者你也可以加入installshield中文论坛官方QQ群来获取这份放在群共享里的文件。

szfilename = UNINSTALL_STRING +” /UNINSTALL”;

nresult = StrFind(szfilename,”.exe”);

if nresult >=0 then

StrSub(szmsg1,szfilename,0,nresult + 4);

StrSub(szmsg2,szfilename,nresult + 4,200);

LongPathToQuote(szmsg1, FALSE );

LongPathToQuote(szmsg2, FALSE );

szfilename = “/”” + szmsg1 + “/”” +szmsg2;

endif;

AddFolderIcon(FOLDER_PROGRAMS^”TEST”,”Uninstall”,szfilename,WINDIR,””,0,””,REPLACE);

Q: 我如何修改“添加或删除程序”里我的软件的卸载图标?

A: Project Assisant -> Application Information,Select the icon to display your application in Add or Remove Program这项,点击Browse选择你想要的图标即可。

一个完整的Installshield安装程序实例—艾泽拉斯之海洋女神出品(五) –补遗 (已补充第三部分完整版)

第三部分:其他
1. 修改显示界面的风格
Installshield 原始安装界面我始终觉得很丑,幸好Installscript 是可以不用写代码就可以改界面风格的。
在 Installation Designer 的左边导航树上找到User Interface | Dialogs

image
在中间的导航树上找到 Dialogs | Skins

image
Skins 选项下面显示了不同的界面风格,默认是None,选择一个喜好的风格即可,笔者一般使用Blue 或BlueTC,适用于一般商业软件的稳
重风格;Midnight 比较酷,要是做电脑游戏的安装程序,我一定会选这个风格。
2. 编译打包
一切都准备就绪之后,就差一个编译打包成实际的安装程序的过程了。
编译
编译可以使用工具栏上的WOTX[}}25C{DY637]~}(KXN ,检查一下有无定义错误,编译错误等。
打包
打包可以使用工具栏上的image 。
1. 第一步,指定一个打包的配置版本,如果使用同一个安装程序源来打包成不同的配置,就可以选择新的配置版本。这里的配置指的
是安装程序本身的配置,包括是否压缩文件、打包成网络安装程序或光盘安装程序、安装程序的客户信息、是否加密等等。

image

2. 同一配置下允许多个版本的存在;一般笔者习惯于当安装源文件升级的时候,就打包一个新版本出来以示区别,同时也可以保留老
版本的安装程序备用不时之需。

image

3. 过滤设置,笔者从来没用过。貌似是对feature 的一些设置参数,大概是针对某些版本,如果某些feature 不需要的话,就直接过滤掉
了,安装时候无法选择安装了。不过参数具体怎么设置,包括下面的语言过滤,笔者尚未使用过。但是回想起以前安装的一些大型
软件比如Oracle 之类的,会有一些选项是灰色无法选择安装的,大概就是类似这种功能。

image

4. 安装程序的语言和被安装的应用程序的语言没有必然联系,因此这里还需要设置一下安装时所用的语言。选择列表里显示的语言取
决于在做安装程序时选择的语言,刚才我们只选择了English,因此这里只显示English 了。如果选择了多项语言,那么在安装时出
现的第一个界面会是让你选择安装时所用语言的界面,非常智能。这种功能针对需要发布到多个语种国家的软件是非常有用的。不
过这样的话,在写脚本的时候,只要是显示在界面上的语言,除了系统可以默认显示的,都要多加一个语言判断,并且显示不同的
内容了。

image

5. 选择介质类型,一般笔者都会把安装程序刻录到光盘上,因此选择 CD-ROM

image

6. 光盘选项,第一个选项 Automatic 会自动为你检测所需光盘的规格数量,以及制作出光盘之间的断点。我一般都是选第一项,免去
不少啰嗦事,第二个选项应该是高人才会选的吧。

image

7. 这个选项指定了打包时的形态:
Compress all files:所有的文件都压缩打包(这里不包括脚本里所写的安装时候从外部拷贝的文件,只包括在Project Assistant 里指定
到各个feature 下的文件)
Leave files uncompressed and separate from the installation package:所有的文件都不打包,以原始形态存在
Custom:定制,允许你指定一部分压缩打包,一部分散放。
笔者喜欢把所有的文件都打包,看上干净并且专业。

image

8. 对操作系统的要求,一般都默认即可

image

9. 这个是 Windows 安装引擎,对此没有研究过,凭着上面的解释选了第二个,会帮你打包进安装程序,这样就高枕无忧了。

image

10. 签名,目前用不到,有兴趣的话可以导入数字签名文件

image
11. 密码和版权声明。密码还是算了吧,一个商业软件是不该这样设的,客户会骂死的。

image

12. 是否要包括.NET Framework,Installshield 看来和微软很和谐啊。

image

13. 编译出来的文件放置的位置,以及一些相关设置,建议长文件名这个选项要选上,以免路径太深造成不必要的问题。

image

14. 最后一步,显示之前所配置的设置,点击“完成”即可编译出一个安装程序来。

image

15. 编译好的安装程序文件应该在 工程路径/My Project Name/Product Configuration X/Release X/DiskImages/DISK1 下

image

点击 setup.exe,就可以安装了。
这里顺便说一下,这个安装程序虽然制作好了,但是如果这样光秃秃地拿给客户,客户是要对产品的印象打折扣的。可以使用 Flash 或者其
他的专业光盘制作软件来制作一个漂亮的应用界面来提供给客户,在插入光盘的时候自动弹出一个漂亮的使用界面,不但印象深刻,而且客
户使用起来也方便。

 

第四部分:补遗

这篇补遗是《一个完整的安装程序实例—艾泽拉斯之海洋女神出品》的追加叙述,是在这个安装程序安装后发现应用程序运行时的一些小问题,通过安装程序中传递参数和设置环境变量来解决掉问题的,与安装程序本身无关。

这个程序做完之后,工程进入最后的调试阶段;里面发现了两个问题,其实和安装程序本身没有关系,但是都可以通过安装程序来解决,记录在此,以备以后碰到此类问题时可以查阅。

1. JAVA_HOME的问题

之前在第二部分的第九小节里提到了安装完毕后,为JDK设置一个环境变量,事实上这个文档写到这里的时候有一点搞错了先后顺序,因为需要这个环境变量的是第二部分第八小节里安装完毕后需要启动的那个程序,所以后来调试时候发现了,就把第八节的程序内容和第九节的程序内容调换了一下顺序。

但是,很快发现了新问题,在注册表里添加环境变量和在桌面上“我的电脑”里直接添加环境变量是不一样的;注册表里的操作,都需要通过重启动计算机来使之生效,所以矛盾出现了:当这个程序启动的时候,环境变量还没有生效;而如果设置了让计算机重启动,就必须让客户手动启动这个程序,这是非常不友好的操作。

所以这里修改了一下方法,首先把需要启动的程序,也就是一个批处理文件,里面的

set JAVA_HOME=%JAVA_HOME%

这句话改成了

set JAVA_HOME=%1

在批处理里面,需要从外部接收参数的时候,可以把参数写成%1,%2…%n。

然后,在第八小节的程序基础上修改,把

if (LaunchApp (serviceTarget, “”) < 0) then

这句话修改成

if (LaunchApp (serviceTarget, javahome) < 0) then

即可。

2. 代码解释

这里javahome就是第九小节里if(RegDBSetKeyValueEx(szKey, “JAVA_HOME”, REGDB_STRING, svValue, -1)<0) then这句话里的svValue,即JDK的安装路径。

LaunchApp (serviceTarget, javahome)

不能不佩服IS函数设计者,在这篇文档里,三个地方用到了LaunchApp这个函数,而且每个用法都不同。

参数一:这里写我们要打开的文件,带相对路径的

参数二:cmd_line,这里,我们写入了JDK的路径,这个值将作为参数传递给我们要打开的批处理文件,批处理接收到JDK路径后,便可以正确启动了。

3. Path的问题

这个问题其实和JavaSerive以及操作系统相关;因为在笔者的计算机上一直没有发现这个问题。

在第二部分的第八小节中提到,我们会向Windows安装一个服务,但是笔者在工程用的计算机上始终不能启动这个服务,这次这个调试任务推给了经理,他经过多次试验,发现是环境变量中Path 的问题,只要在Path里添加上JRE的Bin文件路径,这个服务就可以正确启动了。因为没有深入了解JavaSerive的运行机制和Windows服务的运行机制,也没有深究为什么了,猜想可能是这个服务需要找这个路径,而有的操作系统只要指定了JAVA_HOME就可以寻找JRE了,而有的却不行。

闲话休叙,我们需要写一段程序来实现这个功能。

要注意的地方有以下几点:

l Path往往已存在,并且里面有内容,因此不可以像设置JAVA_HOME一样,而要考虑往已有内容中添加JRE路径,并且要考虑内容之间的分号问题

l 要考虑到卸载状态时,不能把Path卸载。说到这个问题,要提一下前面的第九小节,发现引文作者关于卸载时是否卸载自己添加的注册表键值的理解还是不正确的,Help里关于RegDBSetKeyValueEx有这样一句话However, the newly created key is not logged for uninstallation unless it is a subkey of a key already logged for uninstallation.也就是新创建的键值不会被日志记录了要反卸载掉,除非它有子键值被日志记录了要卸载

程序内容仍然添加在OnEnd()里,写在最后,如下:

szKey = “SOFTWARE//JavaSoft//Java Runtime Environment//1.6.0_04″;//jre的键

RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE);//设置一下根键

if (RegDBKeyExist(szKey)=1) then//如果这个键存在

if(RegDBGetKeyValueEx(szKey,”JavaHome”,nvType,svValue,nvSize)=0) then //查找这个键的值

javaPath= svValue;

endif;

endif;

**************************************以上为第一部分,以下为第二部分

//wirte the environment variable PATH

szKey = “SYSTEM//CurrentControlSet//Control//Session Manager//Environment”; //环境变量在注册表中所在位置

javaPath=javaPath+”//bin”;//jre/bin的路径

if(RegDBGetKeyValueEx(szKey,”Path”,nvType,svValue,nvSize)=0)then//如果Path存在

if(svValue!=””) then

if(StrFind ( svValue, javaPath )<0) then//如果path还没有jre/bin的路径信息

svValue=svValue+”;”+javaPath; //添加路径信息,此时要带上分号

endif;

else

svValue= javaPath;//如果键值为空,则直接添加即可,事实上键值为空的情况不会出现,这句话是无用的判断

endif;

else

svValue= javaPath; //如果没有这个键值,把值也直接添加进去,事实上这个情况也不会出现,因为path在操作系统安装完毕后就存在了,//除非你手动删除了,但是那样操作系统也会有问题了

endif;

if(!MAINTENANCE)then

Disable(LOGGING);

if(RegDBSetKeyValueEx(szKey, “Path”, REGDB_STRING_EXPAND, svValue, -1)<0) then//添加或者重设键值

MessageBox (“Path create failed, please set it manually!”, SEVERE);

endif;

Enable(LOGGING);

endif;

4. 代码解释

第一部分的目的在于找出JRE的安装路径,所有的函数之前都有解释,不再赘述

第二部分:

szKey = “SYSTEM//CurrentControlSet//Control//Session Manager//Environment”;

环境变量,位于注册表的这个位置

*************************************************************************

javaPath=javaPath+”//bin”;

需要寻找的是JRE下的bin文件

*************************************************************************

if(RegDBGetKeyValueEx(szKey,”Path”,nvType,svValue,nvSize)=0)then

如果Path存在并返回了值。

这里其实如果加上一个判断此键是否存在,代码会更加完善,不过操作系统装完之后这个键是一定存在的,这里笔者偷懒了

*************************************************************************

if(StrFind ( svValue, javaPath )<0) then

判断返回的键值里是否包含jre的bin文件夹的路径

StrFind(szString, strFineMe)

在源字串里查找是否包含指定的字串

参数一: szString,被查找的源字串

参数二:strFineMe,要查找的字串

如果包含要查找的字符串,则返回要查找的字符串在源字符串里的位置;如果查找不到则返回小于0的随机数字

*************************************************************************

if(!MAINTENANCE)then

判断一下安装状态,使之只有在非维护(修改,重新安装,卸载)状态使起作用

*************************************************************************

Disable(LOGGING);

Enable(LOGGING);

这两句话作用分别是停止日志记录和使日志记录重新生效,这是从网上的一篇心得里抄录的,当日志停止记录时候,安装程序就无视了停止日志后的所有操作,这样保证键值不会在反安装时候被操作;其实本来上面一句if(!MAINTENANCE)then(非维护状态时操作)在原文是没有加上去,作者说只要不让日志记录到操作注册表,这个操作就不会被反安装掉,不过好像自己试了一下不行,加上一句,比较保险些,至少试验出来是没问题的。

*************************************************************************

if(RegDBSetKeyValueEx(szKey, “Path”, REGDB_STRING_ EXPAND, svValue, -1)<0)

写注册表的键值。

Help里RegDBSetKeyValueEx的对应帮助里有这样一句话If the value data already exists, RegDBSetKeyValueExoverwrites it,也就是,如果键值存在,那么覆盖它。不过对注册表操作后,要把计算机重新启动才能生效;这一点和我们直接在“我的电脑”里操作环境变量是不一样的。

这里,第三个参数改成了REGDB_STRING_ EXPAND,因为在Windows 2003server下,path含有一个%SystemRoot%的相对路径,当时一开始使用了REGDB_STRING,结果无意中发现所有的dos命令都用不了了,在dos下输入path一看,该替换成绝对路径的地方都没有替换掉,当时也是一头雾水,上CSDN一问,有人提醒说我应该看一下IS程序里键值设置时候的类型问题,跑回来一看果然设置有问题,REGDB_STRING是不认识相对路径的,换成REGDB_STRING_ EXPAND就可以了。

5. 文件的只读性问题

用光盘装程序的时候发现一个问题,当从光盘上拷贝出文件的时候,文件会默认为只读格式,导致配置文件不能正常存储数据库信息,因此,在安装程序代码里拷贝完文件后,指定一下文件的属性

SetFileInfo ( szPathFile, nType, nAttribute, szValue );

此处用作SetFileInfo ( szPathFile, FILE_ATTRIBUTE, FILE_ATTR_NORMAL, “” );

一个完整的Installshield安装程序实例—艾泽拉斯之海洋女神出品(四) –高级设置二

4. 根据用户选择的组件,从外部文件夹拷贝相应的文件到安装目标路径的文件夹中

这个用途常见于配置文件和授权文件的应用,同一程序,授权给不同的用户,只需要不同的配置和授权文件。如果将配置和授权文件每次都打包在安装程序里,那么变更一个用户就需要重新打包一次,这是一个浪费时间和精力的行为。如果将授权和配置文件(当然内容是加密过的)放在外部文件夹中,每次安装的时候从这个文件夹中读取拷贝,那么会是一个比较通用型的安装程序。

另外,本程序的好几个feature用到了相同的库,如果直接在feature下加库文件也可以,但是每一个feature都加一次这个库文件夹,整个安装程序就会变得很庞大,因此比较理想的情况是选到了这个feature的时候从外部拷贝这些库文件。

这里我们先不包括文档这个feature的说明,文档feature另有详细说明。

1. 这个功能需要在OnFirstUIAfter()函数体中实现,选择After Move Data | OnFirstUIAfter选项,即在选择了移动哪些数据后这个操作生效。

clip_image002

2. 之前我们已经接触过了如何判断是否选择了某个Feature,这里也需要判断是否选择了某个Feature,并且根据这个Feature来拷贝对应的外部文件

首先定义一些需要的变量并且进行赋值,蓝色字体即为所定义变量和赋值语句

function OnFirstUIAfter()

//feature name

STRING szFeatureName1;

STRING szFeatureName2;

STRING szFeatureName3;

STRING szFeatureName4;

STRING szFeatureName5;

STRING szSrcFile1;

STRING szSrcFile2;

STRING szTarFolder1;

STRING szTarFolder2;

NUMBER nResult;

STRING szTitle, szMsg1, szMsg2, szOption1, szOption2;

NUMBER bOpt1, bOpt2;

begin

//feature 定义

szFeatureName1 =”Server”;

szFeatureName2 =”Client”;

szFeatureName3 =”Watch_Portion”;

szFeatureName4 =”Log_Portion”;

szFeatureName5 =”Report_Portion”;

//需要拷贝的源文件

szSrcFile1 = “Test//lib//*.*”;

szSrcFile2 = “Test//databaselib//*.*”;

//拷贝的目的地,目标文件夹

szTarFolder1 = “lib//*.*”;

szTarFolder2 = “databaselib//*.*”;

3. 对每一个feature进行判断,进行相应的文件拷贝

在OnFirstUIAfter()的begin和end之间添加如下代码:

//copy the lib to the target ,copy the necessary file to the target

if (FeatureIsItemSelected(MEDIA, szFeatureName1)=1) then

CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);

CopyFile(SRCDISK^”Test//configure//title.gif”, TARGETDIR^”Server// title.gif”);

CopyFile(SRCDISK^”Test//configure//background.gif”, TARGETDIR^” Server // background.gif”);

CopyFile(SRCDISK^”Test//configure//configure.dat”, TARGETDIR^” Server //configure.dat “);

endif;

if (FeatureIsItemSelected(MEDIA, szFeatureName2)=1) then

CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);

CopyFile(SRCDISK^”Test//configure//configure.dat”, TARGETDIR^”Client//configure.dat “);

CopyFile(SRCDISK^”Test//configure//license.dat”, TARGETDIR^” Client //license.dat”);

endif;

if (FeatureIsItemSelected(MEDIA, szFeatureName3)=1) then

CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);

CopyFile(SRCDISK^”Test//configure//configure”, TARGETDIR^” Watch Portion //configure”);

CopyFile(SRCDISK^”Test//configure//license.dat”, TARGETDIR^” Watch Portion //license.dat”);

endif;

if (FeatureIsItemSelected(MEDIA, szFeatureName4)=1) then

CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);

endif;

if (FeatureIsItemSelected(MEDIA, szFeatureName5)=1) then

CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);

endif;

4. 代码解释

if (FeatureIsItemSelected(MEDIA, szFeatureName1)=1) then

CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);

CopyFile(SRCDISK^”Test//configure//title.gif”, TARGETDIR^”Server// title.gif”);

CopyFile(SRCDISK^”Test//configure//background.gif”, TARGETDIR^” Server // background.gif”);

CopyFile(SRCDISK^”Test//configure//configure.dat”, TARGETDIR^” Server //configure.dat “);

endif;

**************************************************************************************

FeatureIsItemSelected(MEDIA, szFeatureName1) 这个函数用于判断用户是否选择了某feature。Help里对这个函数是这样描述的:FeatureIsItemSelected ( szFeatureSource, szFeature );

参数一:szFeatureSource,大意好像是feature的来源,具体不是很明白到底指什么,反正help自带的例子里写的MEDIA照抄没有错。

参数二:szFeatureName1,就是 feature的名字了

如果返回值为1,则说明用户选择了这个feature

**************************************************************************************

CopyFile(SRCDISK^szSrcFile1, TARGETDIR^szTarFolder1);

拷贝文件的函数。Help里是这样描述的:CopyFile ( szSrcFile, szTargetFile );

参数一:szSrcFile,源文件,可带路径,要带有扩展名的文件名。当这个文件带路径时,则从这个指定路径下拷贝指定的文件;如果是不带路径的,则直接从安装文件所在盘的盘符下寻找指定的文件来进行拷贝。如果要拷贝某个文件夹下的一系列文件,可以使用通配符。

参数二:目标文件,可带路径,要带有扩展名的文件名。当这个文件带路径时,则将文件拷贝到这个指定路径下;如果是不带路径的,则将文件拷贝到安装路径下。支持通配符。

小结:上面这段代码的意思是:如果用户选择了某个feature,则从安装程序所在的盘下面的一些文件夹下拷贝文件到目标路径下的一些对应文件夹下。这里记住拷贝文件一定要带上文件的全名,包括扩展名。

5. 如果用户选择了文档feature,则把文档文件夹拷贝进来,并且对该文件夹进行盲读,为每一个文档创建一个在开始菜单下的快捷方式

1. 这个功能仍然在After Move Data | OnFirstUIAfter()的函数里实现

先定义一些变量并赋值,蓝色字体

function OnFirstUIAfter()

//feature name

STRING szFeatureName6;//feature名

STRING szSrcFile3; //需要拷贝的源文件

STRING szTarFolder3; //拷贝的目的地,带文件名

STRING szTarFolder4; //拷贝的目标文件夹,后面有一个函数要用到不带文件名的目标路径

STRING szDocFile, szDocFileName;// szDocFile,查找函数返回的查询得到文件名;szDocFileName,要查找的文件名

NUMBER nResult; //数字型变量,存放函数的返回结果

begin

//feature 定义

szFeatureName6 =”Document”;

//需要拷贝的源文件

szSrcFile3 = “Docs//*.*”;

//拷贝的目的地,目标文件夹

szTarFolder3 = TARGETDIR^”Docs//*.*”;

szTarFolder4 = TARGETDIR^”Docs”;//文档的存放路径,不带文件名

2. 仍然在begin和end之间的函数体内把下面的代码拷贝进去即可

if (FeatureIsItemSelected(MEDIA, szFeatureName6)=1) then //如果选择了此feature

if(CopyFile(SRCDISK^szSrcFile3, szTarFolder3)=0) then //那么把要拷贝的文件拷贝过去

nResult = FindAllFiles(TARGETDIR^”Docs”, “*.pdf”, szDocFile, RESET); //对拷贝过去的文件进行查找,该函数会在第一个符合条件//的文件处停止

while (nResult = 0)

LongPathToQuote(szDocFile, TRUE );

ParsePath (szDocFileName, szDocFile, FILENAME_ONLY);//对查找到的文件获取文件名

AddFolderIcon(FOLDER_PROGRAMS^”Test//Docs”,szDocFileName, szDocFile, “”, TARGETDIR^”Docs//icons//help.ico” , 0 ,”” , REPLACE ); //为该文件创建快捷方式,快捷方式的显示名就是刚才获取的文件名

nResult = FindAllFiles(TARGETDIR^”Docs”, “*.pdf”, szDocFile, CONTINUE);//从上一个查找的位置继续向下查找,进行循环

endwhile;

endif;

endif;

3. 代码解释

***************************************************************************************

if (FeatureIsItemSelected(MEDIA, szFeatureName6)=1) then

endif;

如果用户选择了文档feature,则进行一些相应操作

***************************************************************************************

if(CopyFile(SRCDISK^szSrcFile3, szTarFolder3)=0) then

endif;

这里执行了两步操作:

第一步,从源盘的Docs文件夹下把所有文件都拷贝安装路径的Docs文件夹下,注意在定义变量的时候使用了通配符

第二步,如果拷贝成功,则返回值为0,那么进行下一步相应操作

**************************************************************************************

nResult = FindAllFiles(TARGETDIR^”Docs”, “*.pdf”, szDocFile, RESET);

查找目标文件夹下所有后缀名为pdf的文件,从文件夹的开始位置进行查找,查找成功则返回0。

这个函数在这里有一个巧妙的应用,因为这个函数会在查找到第一个符合条件的文件时就会停止继续向下查找,因此利用静态变量的传值不同,来实现对文件夹的全部查找。

Help里的解释如下:

FindAllFiles ( szDir, szFileName, svResult, nOp );

参数一:szDir,被查找的文件夹

参数二:szFileName,需要查找的文件的名字,支持通配符,例如*.*,*.pdf,*.doc

参数三:svResult,函数会在查找到第一个符合条件的文件时停止,返回这个符合条件的文件的文件名,带全路径和含扩展名的文件名

参数四:nOp, 静态变量。CONTINUE,从上一次查找的位置开始查找,这个特性我们呆会儿会用到;RESET,从文件夹的开始位置进行查找;CANCEL,释放被上一次的FindAllFiles查找的函数。在Windows NT系统下,需要在安装过程中使用带CANCEL的FindAllFiles来释放之前的查找,确保安装的正确性(因此我怀疑查找有bug,这个函数用来弥补这个bug…)。

**************************************************************************************

LongPathToQuote(szDocFile, TRUE );

szDocFile为上一个函数查找到的第一个符合条件的文件名,带完整路径,这个LongPathToQuote函数加上这个文件名上的括号;否则下面一个函数无法解析不带括号的长文件名。

Help里的解释如下:

LongPathToQuote ( svPath, nParameter );

参数一:svPath,长文件名

参数二:nParameter,静态变量。 TRUE,为长文件名加上括号;FALSE,为长文件名脱去括号。

**************************************************************************************

ParsePath (szDocFileName, szDocFile, FILENAME_ONLY);

解析带路径的长文件名,返回文件本身的文件名

Help里的解释如下:

ParsePath ( svReturnString, szPath, nOperation );。

参数一:svReturnString为返回的解析过的文件名,

参数二:szPath,即被解析的长文件名

参数三:nOperation,静态变量,指定用何种方式来解析。这里使用FILENAME_ONLY,也就说返回值为不带路径、不包含扩展名的文件名。这个文件名被下面一步用作显示的快捷方式的名称。

**************************************************************************************

AddFolderIcon(FOLDER_PROGRAMS^”Test//Docs”,szDocFileName, szDocFile, “”, TARGETDIR^”Docs//icons//help.ico” , 0 ,”” , REPLACE );

创建一个快捷方式,使用指定的图标。

Help里的解释如下:

AddFolderIcon ( szProgramFolder, szItemName, szCommandLine, szWorkingDir, szIconPath, nIcon, szShortCutKey, nFlag );

参数一:szProgramFolder, 要创建的快捷方式所在的文件夹。这里FOLDER_PROGRAMS指开始 | 所有程序,因此我们的快捷方式将会出现在开始 | 所有程序 | Test的Docs下;如果要添加到桌面上,可以设置为FOLDER_DESKTOP;FOLDER_STARTUP 指添加为启动项;FOLDER_STARTMENU添加到开始菜单下。

参数二:szItemName,help里解释很晦涩,解释为要添加到文件夹下的图标的名称,即出现的图标旁边的那个字符串。其实就是我们常说的快捷方式的名称。这里填写被解析出来的那个不带路径也不带扩展名的文件名。

参数三:szCommandLine,全限定路径的文件名或文件夹名,可包含命令行参数。这里传入刚才查找到的文件名,包含路径、文件名和扩展名。读者可能注意到这个参数被做了一些预处理,这个处理也是折腾了几次才搞出来的,不同的操作系统默认路径也是有是否带引号的差别的,这里需要显式地指定一下,以免在不同操作系统上运行时引起不同的结果。

参数四:szWorkingDir,工作目录。Help里的解释如下:设置这个目录为你的应用程序文件所在的地方;要设置包含了应用程序的目录为工作目录,则可传一个空字符串给这个参数。这个参数一开始我并未理解其含义,不过传空字符串也没有出错;在后来经理提出新要求:允许用户自行选择是否在桌面上创建快捷方式时无意中明白这个参数的含义;请读者随便寻找一个自己计算机上的任意位置的快捷方式,右键点击选择“属性”,这个szWorkingDir就是属性面板上的“起始位置”,值为这个快捷方式所指的应用程序所在的文件夹的路径。至少在我试验的程序里,创建开始菜单的快捷方式和桌面快捷方式,这个参数要求的值还是略有不同的,开始菜单里创建,可以直接传空字符串;而桌面快捷方式,传控字符串总是会出错,查看属性面板里的“起始位置”值为空,因此手动地传了快捷方式所指向的应用程序的所在文件夹的路径,后面在“安装结束时允许用户选择创建桌面快捷方式”话题里有详细说明。

参数五:szIconPath,带全限定名的图标的路径,即包含路径、文件名和扩展名

参数六:nIcon。如果不是使用Windows图标的话,统统指定为0;Windows图标我没有研究过,Help里说可以指定为0,1,2,3…n我猜测是不是图标文件本身包含了多个图标,而我可以指定使用哪个图标?

参数七:szShortCutKey,热键,一般用不到。如果有需要可以设置为比如”Ctrl + Alt + 1″这种形式。

参数八:nFlag,静态变量,多个用途。这个程序里我们使用了REPLACE,即永远使用当前这个快捷方式的属性;RUN_MAXIMIZED ,当从这个快捷方式登录程序时,程序界面最大化;RUN_MINIMIZED,当从这个快捷方式登录程序时,程序界面最小化; NULL,无任何操作(不知道这个无任何操作适用于何种情况?)。

小结:这段代码的重点在于

1) 实现对文件夹下的文件的盲读。因为之前笔者的文档都打包在程序里,苦于文档的名称和数量常常变更,每做一次都要耗费人力物力,而且在光盘里仍然需要单独放置一个文档文件夹供用户在没有安装程序前的随时查看,重复打包安装使得安装内容容量巨大,以至于从刻录小光盘改成刻录大光盘,从VCD盘改成DVD盘。这段代码在用户选择了安装文档的条件下,对外部文件夹进行了拷贝,并且读取文件夹下所有的pdf文件(依次类推,只要设置了正确的过滤条件,可以读取文件夹下想要的文件)。难点就在于将文件夹下的文件一个个读取出来并且获取该文件的信息。

2) 对读取的文件创建快捷方式,这个难点在于8个参数的理解。我在互联网上搜索了一阵子,并且啃了一阵子help,但是可能自己外语水平不是很过关,以至于第四个参数没有完全理解到底是什么意思,所见的例子也很单调并且偷懒,能赋””的地方都给赋了””,无语~~~~

整个安装程序做下来这一段代码是最难的,FindAllFiles在Help里解释是当碰到第一个符合条件的文件就会停下来,因此如何读取全部文件,并且获取文件信息,代码的撰写也是费了很大的功夫,并且参考了别人的程序修改出来的。

6. 在安装结束时,显示readme.txt文件

这是个很有用的设置,但是在InstallScript工程里不是默认自带的,因此也需要脚本编程实现。

这段代码的位置是在After Move Data | OnFirstUIAfter()的函数里实现的

1. 首先,在安装的时候把readme.txt文件从源盘拷贝到安装目录下。把这段代码拷贝到After Move Data | OnFirstUIAfter()的begin和end;之间即可。README.TXT文件放置在源盘的根目录下,并且在安装时拷贝到安装目录下。

CopyFile(SRCDISK^”README.TXT”, TARGETDIR^”README.TXT”);

这段代码意味着当安装执行的时候,这个文件总会被拷贝过去。

2. 创建一个Finish界面,并在界面上设置询问是否显示readme.txt文件的选项。

之前我们看到当我们第一次选取了After Move Data | OnFirstUIAfter()选项时,系统会为我们创建如下代码(当然不创建也不要紧,自己敲就是了)

这个就是结束界面。Installscript工程默认安装完毕后,界面直接消失,而不会出现一个带有Finish按钮的界面让用户点击了以后才结束整个安装过程。

这段代码就是创建了一个Finish界面了,我们要对这段代码进行改造,使之出现一个是否显示readme的选项。

clip_image004

把上图中从Disable(STATUSEX);起到SdFinishEx这行的代码,全部替换成如下代码:

Disable(STATUSEX);

ShowObjWizardPages(NEXT);

bOpt1 = TRUE;

bOpt2 = TRUE;

szMsg1 = SdLoadString(IFX_SDFINISH_MSG1);

szTitle=””;

szMsg1=””;

szMsg2=””;

szOption1=”Show Readme”;

szOption2=””;

SdFinishEx(szTitle, szMsg1, szMsg2, szOption1, szOption2, bOpt1, bOpt2);

if (bOpt1=TRUE) then

if(FindFile(TARGETDIR, “README.TXT”, szDocFile)=0) then

LaunchApp ( WINDIR^”Notepad.exe” , TARGETDIR^”README.TXT” );

endif;

endif;

3. 代码解释

*******************************************************************************************

Disable(STATUSEX);

使默认的安装设置对话框无效。

*******************************************************************************************

ShowObjWizardPages(NEXT);

顺序执行这个OnFirstUIAfter()的代码,如果参数为BACK,则逆序执行

*******************************************************************************************

SdLoadString(IFX_SDFINISH_MSG1);

返回参数所关联的字符串值,这个参数应当是一个资源ID。

*******************************************************************************************

SdFinishEx(szTitle, szMsg1, szMsg2, szOption1, szOption2, bOpt1, bOpt2);

参数一:szTitle,即显示在界面上的左上角的标题,如果传空值,则显示默认值

参数二:szMsg1,安装结束的界面上允许最多有两个可选项,这个参数可以显示第一个选项的一些相关说明,如果赋空则不显示任何说明

参数三:szMsg2,解释同上

参数四:szOption1,选项名。这个是一个Checkbox,如果设置为空则不显示,如果赋值则显示一个Checkbox并且在这个Checkbox旁边显示这个所赋的简短值。

参数五:szOption2,解释同上。

参数六:第一个选项的状态,如果设置为TRUE,则第一个选项Checkbox默认为选中状态,FALSE则为未选中状态。

参数七:第二个选项的状态,解释同上。

*******************************************************************************************

if (bOpt1=TRUE) then

判断是否选择了checkbox。如果用户选择了这个选项,则进行下一步操作

*******************************************************************************************

if(FindFile(TARGETDIR, “README.TXT”, szDocFile)=0) then

为了保险起见,需要进一步判断一下这个readme.txt是否被拷贝进来了

Help里解释如下:

FindFile ( szPath, szFileName, svResult );

参数一:szPath,文件所在的路径,不包含文件名

参数二:szFileName,文件名,包含扩展名

参数三:szDocFile,返回的文件名

如果查找成功,则返回值为1

*******************************************************************************************

LaunchApp ( WINDIR^”Notepad.exe” , TARGETDIR^”README.TXT” );

打开readme文件

Help里没有对这个函数的专门的解释,但是有个例子,以至于我看了好几遍才看懂要表达的意思

参数一:应用程序,也就是你用什么工具来打开第二个参数指定的文件。我们这里用记事本打开,因此要引用一下Windows下自带的程序Notepad.exe,路径为WINDIR^”Notepad.exe” 。如果是一些不是Windows自带的程序,比如PDF,DOC,还需要从注册表里得到所安装的目标位置,从这个目标位置得到要用的工具。有兴趣的朋友可以试验一下。

参数二:要打开的文件,带路径,包含扩展名

小结:这个界面我曾经试图写在OnFirstUIBefore()里的结尾部分,用Dlg_SdFinish来实现,但是总是发现虽然结束界面能出来,但是上一个界面不能消失掉的情况。因为这个资料也不好找,仓促之间试验出上述所说的办法,估计是等安装界面结束后补上一个界面来达到这个效果的;其实我本人是比较讨厌结束的时候有这么一个要看readme的选项的,一般自己装到这种软件,都是去掉钩选框,不看readme的;但是如果直接结束掉,不出这个结束界面又觉得提示不足,有时候不能确定安装程序有没有结束,所以私下里还是比较想去掉readme选项,而直接显示一个只有一个finish按钮的界面的。

7. 在安装结束时,允许用户选择是否显示桌面快捷方式

有时候我们会看到别的安装程序在安装过程中允许用户选择是否要在桌面上显示快捷方式,一开始因为我们公司的分布式系统的组件太多了,不想显示在桌面上,而且觉得和在开始菜单中显示快捷方式的原理是一样的,因此也就轻轻带过;后来经理抱怨说没有桌面快捷方式,总是要去开始菜单找,觉得麻烦,而且客户是使用专用计算机运行我们的程序,也就是桌面上会很干净,希望我能够做这个功能出来。我试了一下,发现和在开始菜单中显示快捷方式还是有一点不同的,也是值得写出来的,至少可以让读者少走一些弯路。

1. 首先要显示一个允许用户选择是否显示桌面快捷方式的界面,这个界面上要有一个checkbox(钩选框),当钩选了以后,安装程序就要在安装时为用户显示桌面快捷方式。

这段代码的位置是在After Move Data | OnFirstUIAfter()的函数里实现的,也就是和“显示readme文件”的功能放在一起。

把从Disable(STATUSEX);起到SdFinishEx这行的代码,全部替换成如下代码:

Disable(STATUSEX);

ShowObjWizardPages(NEXT);

bOpt1 = TRUE;

bOpt2 = TRUE;

szMsg1 = SdLoadString(IFX_SDFINISH_MSG1);

szTitle=””;

szMsg1=””;

szMsg2=””;

szOption1=”Show Readme”;

szOption2=”Create Shortcut on Desktop?”;

SdFinishEx(szTitle, szMsg1, szMsg2, szOption1, szOption2, bOpt1, bOpt2);

2. 代码解释

与上面的“显示readme文件”中的代码相比,只动了一个地方,即szOption2=”Create Shortcut on Desktop?”;

这个是一个Checkbox,如果值设置为空则不显示,如果赋值则显示一个Checkbox并且在这个Checkbox旁边显示这个所赋的简短值。

这里我们需要它显示出来,这样在界面上用户就会看到一个钩选框询问是否要显示桌面快捷方式。

3. 接下来我们要对用户所做的选择做一些判断,并且显示桌面快捷方式,在这段代码后面加上

if(bOpt2=TRUE) then

if (FeatureIsItemSelected(MEDIA, szFeatureName1)=1) then

szDocFile = TARGETDIR^”Server//server.bat”;

LongPathToQuote(szDocFile, TRUE );

AddFolderIcon(FOLDER_DESKTOP, “Server” , szDocFile, TARGETDIR^”Server” , TARGETDIR^”Server//icons//appClient.ico” , 0 ,”” , REPLACE );

endif;

4. 代码解释

因为上面对这些函数的每个参数都有详细解释了,所以这里就不做一一解释了,只对要注意的地方做说明。

这里,一开始,笔者对第四个参数仍然传的是空字符串,但是创建的快捷方式总是不能运行,对比属性面板才发现,桌面快捷方式的“起始位置”的值居然是空的,看来Help解释的“当传空值的时候,默认为快捷方式所指的应用程序所在的目录”并未生效,只好老老实实地把运行目录的值手动地传进去。

读者可能注意到在AddFolderIcon函数里的第三个参数被做了一些预处理,这个处理也是折腾了几次才搞出来的,不同的操作系统默认路径也是有是否带引号的差别的,这里需要显式地指定一下,以免在不同操作系统上运行时引起不同的结果。

8. 在安装结束后,启动指定的程序

在全部安装完毕后,启动指定的程序,向Windows安装一个服务。或者也可使用于安装结束后的程序的自启动。

1. 这部分很明显是要在安装全部结束后进行的,因此放在After Move Data | OnEnd里

clip_image006

2. 把OnEnd()的代码替换如下

function OnEnd()

STRING szFeatureName;

STRING serviceTarget;

STRING szDocFile;

begin

/*

//这个服务所需的文件只有在钩选了某feature时候才会被拷贝,并且也只有在用户钩选安装了此feature时候才会在安装结束时安装此服务,因此首要判断是否选择了此feature,然后寻找到该执行文件,并且进行安装

*/

szFeatureName=”Watch_Portion”;

serviceTarget=TARGETDIR^”watch.exe”;

if (FeatureIsItemSelected(MEDIA, szFeatureName)=1) then

if(FindFile(TARGETDIR, ” watch.exe “, szDocFile)=0) then

if (LaunchApp (serviceTarget, “”) < 0) then

MessageBox (“Unable to launch “+serviceTarget+”.”, SEVERE);

endif;

endif;

endif;

end;

3. 代码解释

***************************************************************************************

if (FeatureIsItemSelected(MEDIA, szFeatureName)=1) then

endif;

首先判断这个feature是否被用户选择安装。因为在这个应用程序里这个服务只与此feature相关,因此要做一下判断,如果用户没有安装这个feature,就不需要启动这个服务了。

当用户选择了这个feature时,返回值为0

***************************************************************************************

if(FindFile(TARGETDIR, ” watch.exe “, szDocFile)=0) then

endif;

这个是判断一下文件是否被正确地拷贝过去了,这个文件应该位于安装目录下,名为watch.exe。当该文件存在时,返回值为0

***************************************************************************************

if (LaunchApp (serviceTarget, “”) < 0) then

endif;

启动该服务;如果启动失败,则返回小于0的值。

这里LaunchApp的用法和上面第6段的用法略有不同。这个函数的本意是启动第一个参数指定的运行程序来打开第二个参数指定的文件。这里第二个参数指定为空,因为没有要打开的文件;第一个参数指向我们需要启动的可执行程序即可。

***************************************************************************************

MessageBox (“Unable to launch “+serviceTarget+”.”, SEVERE);

如果上一步中判断到程序未能正确启动,则弹出一个错误提示框体现用户。

小结:这段代码的用法非常简单,但是如果用在适当的安装程序里会非常重要;笔者的安装程序,在一开始的时候需要用户安装完毕后手动地去安装目录里找到这个服务并且启动,使人感觉非常不友好;现在在安装完毕后做到了静默启动,用户无需做任何事情。而且这个服务需要JDK的支持,配合上述第2段中判断是否安装了JDK这个应用,就不会出现安装了此服务但是无法运行的局面。

9. 安装结束后,为JDK设置一个环境变量

之前提到了,要在安装本系统时判断是否安装了JDK,在最初笔者所做的安装盘中,还要让用户手动地去为JDK设置环境变量JAVA_HOME,设置环境变量对于外行来说简直就是天方夜谭,在Java论坛新手区最常见就是求助设置环境变量的问题了,因此,这个功能最好还是由安装程序代劳为妙。

1. 这段代码在Before Move Data | OnFirstUIAfter()里

//write the environment variable

szKey = “SOFTWARE//JavaSoft//Java Development Kit//1.6.0_04″;

RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE);

if (RegDBKeyExist(szKey)=1) then//如果该注册表值存在

if(RegDBGetKeyValueEx(szKey,”JavaHome”,nvType,svValue,nvSize)=0) then//获取注册表值成功

szKey = “SYSTEM//CurrentControlSet//Control//Session Manager//Environment”;

if(RegDBSetKeyValueEx(szKey, “JAVA_HOME”, REGDB_STRING, svValue, -1)<0) then

MessageBox (“Javahome create failed, please set it manually!”, SEVERE);

endif;

endif;

endif;

2. 代码解释

****************************************************************************

RegDBKeyExist(szKey)

判断JDK1.6.0_04的注册表值是否存在;要判断JDK1.6.0_04是否被安装,只有通过注册表来判断啦,同理可得,要是自己开发的一套系统中有多个安装程序,而且相互关联,就得朝注册表里写入值了。

如果返回值为1,则说明存在该键值;

如果返回值小于0,则说明该键值不存在。

****************************************************************************

RegDBGetKeyValueEx(szKey,”JavaHome”,nvType,svValue,nvSize)

因为设置JAVA_HOME环境变量需要JDK的安装位置,所以要根据注册表来寻找到这个安装位置,而幸运的是,该键值下的JavaHome键名所对应的值就是JDK的安装位置。

Help里对该函数的解释如下:

RegDBGetKeyValueEx ( szKey, szName, nvType, svValue, nvSize );

参数一:szKey, 要查找的注册表的键,这里我们查找SOFTWARE//JavaSoft//java Development Kit//1.6.0_04

参数二:szName,一些注册表键下面会有一些键名,如果你去看一下我们查找的键,会发现该键下存在多个键名,这里我们只要查找JavaHome键名对应的值,因此,指定szName为JavaHome

参数三:nvType,返回该键名对应的值的类型,比如字符型,数字型;当时笔者还犯了一个错误,以为这个参数是需要笔者指定类型的,因此写了一个REGDB_STRING,结果编译出错,搞了半天发现这个参数是个返回值,汗一个。

参数四:svValue,返回该键名对应的值

参数五:nvSize,返回该键名对应的值的字节数

****************************************************************************

szKey = “SYSTEM//CurrentControlSet//Control//Session Manager//Environment”;

RegDBSetKeyValueEx(szKey, “JAVA_HOME”, REGDB_STRING, svValue, -1)

如果搜索注册表发现JDK已经安装了,就去读一下注册表的键值,并且设置我们所需要的环境变量,这两句话就是用来设置环境变量的。

环境变量也是利用注册表键值设置函数RegDBSetKeyValueEx来实现的,这个键是一个特殊的位置,一定是”SYSTEM//CurrentControlSet//Control//Session Manager//Environment”,我们对该函数进行进行详细说明。

RegDBSetKeyValueEx ( szKey, szName, nType, szValue, nSize );

函数作用:设置注册表键值

参数一:szKey注册表里的键,这里,我们需要设置环境变量的值,因此这里固定传值为”SYSTEM//CurrentControlSet//Control//Session Manager//Environment”

参数二:szName,键名,这里我们需要设置的是名为JAVA_HOME的环境变量

参数三:nType,被设置的键的类型,这里是字符串型,并且不带%PATH%之类的符号,也不转行

参数四:szValue,就是键值了,这里我们已经从上面得到了JDK的安装路径,就把安装路径传进去

参数五:nSize,help里说明如果键类型为REGDB_STRING, REGDB_STRING_EXPAND, 或者 REGDB_NUMBER时,都可以设置该值为-1,installshield会自动为我们计算正确的长度,而当键类型为REGDB_BINARY 和REGDB_STRING_MULTI时,就必须传该键值的实际大小进去。

小结:Installshield默认键值位置是在HKEY_CLASSES_ROOT下的,因此在这里,我们需要在进行搜索键值和设置键值的操作之前使用RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE);这句话来设置一下默认的根键值为HKEY_LOCAL_MACHINE;另,在网上看了一个帖子,当时匆匆看了一下,说是设置的键值会在反安装时候卸载掉,我倒是没有在自己的安装程序里发现这个问题,不过可以研究一下;作者说当时为了解决这个问题,是在代码头加上DISABLE(LOGGING);代码尾加上ENABLE(LOGGING)来实现的,虽然我没有碰到这个问题,但是还是很感谢这位作者,因为当时他也说了,根本找不到资料,自己啃了天书般的HELP来解决,而自己一旦解决了问题,就分享出来,以便于大家少走弯路。

10. 完美卸载

在第一部分的第9点我们提到过InstallScript工程里自带的Uninstall快捷方式的缺陷,这里我们将会创建一个可以实现全部卸载的卸载方式,这个卸载方式会以快捷方式出现在开始菜单下,利用安装程序本身的反安装功能来实现

3. 这段代码在Before Move Data | OnFirstUIAfter()里,和其他创建快捷方式的代码放一起

function OnFirstUIAfter()

STRING szfilename,szFolder ,szmsg1,szmsg2;

NUMBER nresult;

begin

//创建删除快捷方式

szfilename = UNINSTALL_STRING +” /UNINSTALL”;

nresult = StrFind(szfilename,”.exe”);

if nresult >=0 then

StrSub(szmsg1,szfilename,0,nresult + 4);

StrSub(szmsg2,szfilename,nresult + 4,200);

LongPathToQuote(szmsg1, FALSE );

LongPathToQuote(szmsg2, FALSE );

szfilename = “/”” + szmsg1 + “/”” +szmsg2;

endif;

AddFolderIcon(FOLDER_PROGRAMS^”Test”,”Uninstall”,szfilename,WINDIR,””,0,””,REPLACE);

End;

4. 代码解释

****************************************************************************

szfilename = UNINSTALL_STRING +” /UNINSTALL”;

参数一:UNINSTALL_STRING这个静态变量指向的就是我们的安装程序,也就是setup.exe,不过指向的位置不是我们的源盘里的setup.exe,而是C:/Program Files/InstallShield Installation Information/{0D9DF66A-44E5-4754-A522-2AD6C9D5CDBE}/setup.exe;Installshield创建的安装文件在安装时总会在这个文件夹里创建对应信息,一长串数字型序列码就是安装程序的Product ID。利用这个setup.exe就可以进行反安装

参数二:/UNINSTALL,告诉程序启动这个setup.exe时为非安装状态,即修复、重新安装和卸载状态。

因此,这个字符串的值应该是这种形式:

“C:/Program Files/InstallShield Installation Information/{0D9DF66A-44E5-4754-A522-2AD6C9D5CDBE}/setup.exe” -runfromtemp -l0x0409 /UNINSTALL

****************************************************************************

nresult = StrFind(szfilename,”.exe”);

寻找到“.exe”这个字符串在szfilename这个字符串中的位置。

Help里对这个函数的描述如下:

StrFind (szString, szFindMe);

参数一:szString,被查找的源字符串

参数二:szFindMe,要查找的字符串

返回值为要查找的字符串在源字符串中的位置,如果返回值小于0,则说明源字符串中找不到要查找的字符串

****************************************************************************

StrSub(szmsg1,szfilename,0,nresult + 4);

StrSub(szmsg2,szfilename,nresult + 4,200);

如果要查找的字符串存在,那么源字符串就是正确的;这两句语句就对源字符串进行截断,得到想要的子串。

szmsg1应该为C:/Program Files/InstallShield Installation Information/{0D9DF66A-44E5-4754-A522-2AD6C9D5CDBE}/setup.exe

而szmsg2应该为 -runfromtemp -l0x0409 /UNINSTALL

Helpl里的解释如下:
StrSub ( svSubStr, szString, nStart, nLength );
参数一:svSubStr返回的结果字符串
参数二:szfilename源字符串
参数三:开始截断的位置。如果指定的位置大于整个被解析的字符串长度,则返回一个空字串。
参数四:结束截断的位置。如果指定的位置大于整个被解析的字符串长度,则默认为结束截断的位置是字符串的结尾处。

****************************************************************************

LongPathToQuote(szmsg1, FALSE );

LongPathToQuote(szmsg2, FALSE );

这两句的作用是对上面解析出的两个子串脱去括号。原本笔者参考的例子里没有这两句,在自己计算机上运行正常,但是换了一台计算机后,创建出的卸载快捷方式无效,查看快捷方式的指向发现和原来计算机的指向略有差别,查阅了一些资料得知Windows下的长文件名就有这个缺陷,每个操作系统解析出来的可能会有所不同,主要是引号的麻烦。在笔者自己的计算机上获取的长文件名是不带引号的,因此,解析正确;而测试的那台计算机上获取的文件名却是带引号的,这就造成了解析后拼凑的字符串的差别。这里就要显式地为解析出来的子串脱一下引号。

****************************************************************************

szfilename = “/”” + szmsg1 + “/”” +szmsg2;

拼凑出正确的可执行文件的长文件名,带路径,包含扩展名

****************************************************************************

AddFolderIcon(FOLDER_PROGRAMS^”Test”,”Uninstall”,szfilename,WINDIR,””,0,””,REPLACE);

添加一个快捷方式到开始 | 所有程序 | Test下;照抄即可。

小结:可能读者会比较奇怪这一段代码的写法,因为中间那段if endif;代码看上去简直就是多此一举。在Installshield7之前,一直是这样写的:

szfilename = UNINSTALL_STRING +” /UNINSTALL”;

AddFolderIcon(FOLDER_PROGRAMS^”Test”,”Uninstall”,szfilename,WINDIR,””,0,””,REPLACE);

从Installshield8开始,长文件名一直有引号封闭不正确的问题,因此if endif;代码完全是为了解决这个问题而存在的,而上面提到的两个脱去引号的语句,是笔者在前人基础上修改加上的,因为发现解析出来的字串要是不脱一下括号还是有问题。

这个快捷方式运行的时候,出现界面和在安装完毕后再次运行安装程序出现的界面相同。选择Remove即可进行卸载。

clip_image008

这个卸载不会把程序运行时产生的文件卸载掉,比如日志文件、配置信息文件等;会把安装目录中所有从安装程序中安装的文件都卸载掉,包括安装时从外部拷贝的文件。利用Project Assistant创建的卸载快捷方式则无法卸载掉安装时从外部拷贝的文件。

11. 完美卸载之卸载时触发命令(卸载Windows服务)

在做完这个安装程序后,以为可以结束了,没想到经理又提出了一个新的要求,因为之前的安装里(参阅第二部分的第8小节),在安装完毕后,启动了一个指定程序,这个指定程序干的事情就是向Windows写了一个服务进去(有兴趣的同学可以去看看Java Service相关资料,是一个把Java程序注册为Windows服务的一个工具或者说是组件更合适些);所以,这里希望能够在卸载的时候能够把这个服务给卸载掉。

首先我们介绍一下两条Windows cmd命令:

1) SC stop XXX

这条命令用于停止某个名叫XXX的正在运行的Windows服务

2) SC delete XXX

这条命令用于删除某个名叫XXX的Windows服务

一开始我的思路是这样的,获取安装程序的卸载状态,然后调用这两条命令来删除服务;没想到这个“获取安装程序的卸载状态”让我浪费了整整一个下午的时间,只知道MAINTENANCE是程序的反安装状态,而这个反安装状态是有可能包括“重装”、“修复”和“卸载状态”的,当然我可以让反安装界面只能处于卸载状态,只要把前面创建卸载快捷方式中的szfilename = UNINSTALL_STRING +” /UNINSTALL”; 这句话改成szfilename = UNINSTALL_STRING +” /REMOVEONLY”; 就可以了;但是试验出来是不等我确认删除,这个服务就卸载掉了,原因是这个界面一出来就是MAINTENANCE状态,而程序捕获了这个状态后,是不管我是否按下了确认按钮就会去做这个操作了。

后来想在Onbegin里添加一个SdWelcomeMaint函数的判断,结果是判断倒是成功的,但是多了另一个重复界面。

看来这个思路可能是有问题的,然后满地google之,还是吞硬币的小猪的一篇文章给了启发,原文地址找不到了,只找到了这篇http://school.ogdev.net/ArticleShow.asp?id=1699&categoryid=7,这里面其实是谈反安装时候不执行OnMaintUIBefore函数的问题,我想既然这个函数是反安装时候“应该执行的”,那么就看看这个函数吧。

于是 打开Before Move Data | OnMainUIBefore

clip_image010

打开一看,大喜过望,这个函数里明明白白地显示了反安装时候的所有界面。

于是顺着向下看,找到Dlg_SdFeatureTree。

clip_image012

这里红色圈出来的一行代码明确地告诉我们:如果为反安装状态,那么卸载所有组件!OK,代码只要添在这里就可以了。

clip_image014

这里就运用了一个函数LaunchAppAndWait来达到目的。其实一开始我还在想是不是要写批处理文件来执行呢,结果是不需要,直接写在这个函数里就可以了。

LaunchAppAndWait ( szProgram, szCmdLine, nOptions );

参数一:szProgram,要运行的程序。在Help里有这样一句解释:想在命令行里指定要运行的程序,那么可以对这个参数传空值

参数二: szCmdLine,命令行参数;很奇妙的参数,这里我们就可以写入我们想要的批处理语句了。

参数三:静态变量,操作类型,这里LAAW_OPTION_HIDDEN可以使批处理窗口隐藏掉,如果使用了LAAW_OPTION_WAIT,就会看到一个命令行窗口一闪而过,让人十分不爽。

于是,折腾了一下午的问题,就靠这短短的两分钟就解决了…

一个完整的Installshield安装程序实例—艾泽拉斯之海洋女神出品(三) –高级设置一

第二部分:脚本编程

在开始进行编程前,我们先明确一下我们要用编程来弥补前面设置的哪些功能的不足

1. 显示软件许可协议

2. 判断是否安装了本软件所需要的先决软件JKD1.6.0_04,如无,则启动外部安装程序进行安装(同样原理可以用来判断是否安装了其他软件,只要该软件在注册表中有键值)

3. 用户的输入信息、所选安装路径、所选安装组件将显示在安装界面上(Installshield虽然自带了此界面,但是默认是显示为空的,需要写脚本来显示信息)

4. 根据用户选择的组件,从外部文件夹拷贝相应的文件到安装目标路径的文件夹中

5. 根据从外部拷贝进来的文件,创建快捷方式(这里主要是拷贝文档,并在开始菜单中创建快捷方式)

6. 在安装结束时,显示readme.txt文件

7. 在安装结束后,启动指定的程序

8. 完美卸载

脚本编程这部分都将在Installer Designer这个界面进行。后面不再赘述。

Installshield大小写敏感,因此请严格按照示例上所写的大小写规则来书写。例:字符串变量STRING和string都支持,但是String不支持。

1. 显示软件许可协议

1. 添加许可协议文本

在左边导航树上找到Behavior and Logic | Support Files/Billboards选项。这个选项允许用户添加一些在安装过程中需要用到的文件。

clip_image002

中间的导航栏会显示对应的选项

clip_image004

在Support Files分支下,会显示一个Language Independent和所有你所选择的语言类型。 Language Independent意为,如果你在这里分支下做了设置,那么无论选择用何种语言安装,这个设置都会生效;而各个语言类型意为,如果你在某语言下做了设置,那么这个设置只有在选择了用这种语言安装的时候才会生效。

点击Language Independent,这次我们将在这个分支下进行试验。

clip_image006

在右边的Files栏中右键点击,在弹出菜单上选择Insert Files选项。

clip_image008

选择事先撰写好的许可协议的文本文件,插入到Files栏中。

许可协议允许两种文本格式:txt和rtf格式,此处我们采用 txt格式。

2. 然后切换到Behavior and Logic | InstallScript选项,

clip_image010

3. 中间的导航栏Files下有一个默认的Rul文件Setup.Rul,我们这个工程的全部installscript代码都将写在这个默认文件里

clip_image012

4. 点击选中Setup.Rul节点,右边会显示该文件的可编程面板。

5. 许可协议应该在一开始运行安装程序的时候就显示,也就是在拷贝数据前。请在第一个下拉框中选择Before Move Data选项,然后在第二个下拉框中选择OnBegin选项(不要因为默认显示的是这两个选项,而不做这个打开下拉列表进行选择的动作,否则软件检测不到你选择了选项,无法自动添加代码),则编程界面上会自动添加一些代码如下图所示。当然,如果你手动敲代码上去也是可以的。

clip_image014

6. 我们将在function OnBegin()的函数体里面写代码来显示刚才添加的许可协议文本的内容,直接把下面的代码拷贝到OnBegin()函数的begin和end;之间就可以了

Disable (BACKBUTTON);

if(!MAINTENANCE)then

SdLicense2 (“License “, “”, “”, SUPPORTDIR ^ “2.txt”, FALSE);

endif;.

7. 代码解释

************************************************************************

Disable (BACKBUTTON);

将“上一步”按键设置为不可用。安装程序在一开始的时候会有一个默认的开始界面,第二步才显示许可协议,一般来说没必要回退回去看这个什么都没有的开始界面,因此将回退按键设置为不可用

************************************************************************

if(!MAINTENANCE)then

endif;

这一个条件用来判断安装程序处于何种状态,安装、修复、重新安装或卸载状态,后三者都属于MAINTENANCE状态,因此判断只有在正常安装的状态才显示许可协议

************************************************************************

SdLicense2 (“License “, “”, “”, SUPPORTDIR ^ “2.txt”, FALSE);

这个函数用于在界面上显示所用的许可协议。Help里对该函数的构造函数如下

SdLicense2 ( szTitle, szOpt1, szOpt2, szLicenseFile, bLicenseAccepted );

参数一:szTitle,显示在界面左上角的标题,如果填写空字符串””,则显示为默认值”License Agreement”。

参数二:szOpt1,我们常见许可协议界面上会有两个选项,一个是“同意”,一个是“不同意”,szOpt1和szOpt2就是这两个选项,如果填写空字符串,则会显示为默认值”I accept the terms of the license agreement”和”I do not accept the terms of the license agreement”。

参数三:szOpt2,见参数二的说明

参数四:szLicenseFile,指定需要显示的文档,包含路径和带扩展名的文档名。我们刚才把许可协议文本放在supportfile选项下了,这个路径在Installshield里有专门的静态变量来指明,即SUPPORTDIR,然后再添加上带扩展名的文档名,这里是2.txt。静态变量路径和引号引起来的路径之间用^符号来连接。

参数四:bLicenseAccepted,布尔型变量,TRUE状态,则在许可协议界面上默认选中的是那个“同意”的选项;不过好像一般更常见的是默认选中为“不同意”的选项,因此这里可以填入FALSE。

clip_image016

这是许可协议的界面。当用户选择了I accept the terms of the license agreement这个选项后,Next按键可用,安装程序可以继续。(请忽略这里显示的许可协议内容…网上有很多软件许可协议的范本供下载…)

小结:至此,许可协议就添加完毕,在安装执行的时候,用户就可以看到许可协议显示在界面上,并且只有选择了“同意”选项后,安装程序才会往下执行。

显示许可协议的函数一共有三个SdLicense,SdLicenseRtf和SdLicense2,参数略有不同,显示的界面也略有不同,用户可以根据喜好来选择。目前我常用的就是SdLicense2这个函数,显示的界面符合大多数目前流行的安装界面的习惯。

2. 判断是否安装了本软件所需要的先决软件或运行环境

1. 代码还是在OnBegin()函数体内实现,直接把下面的代码拷贝到OnBegin()函数的begin和end;之间就可以了

RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE);

if (RegDBKeyExist (“SOFTWARE//JavaSoft//Java Development Kit//1.6.0_04″) < 0) then

LaunchAppAndWait (SRCDISK^”jdk//jdk-6u4-windows-i586-p.exe”,””, LAAW_OPTION_WAIT);

endif;

2. 代码解释

************************************************************************

RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE);

设置一下默认的注册表键值根节点为HKEY_LOCAL_MACHINE。

打开注册表可以看到“我的电脑”下的根节点有HKEY_CLASSES_ROOT, HKEY_CURRENT_USER,HKEY_LOCAL_MACHINE等。我们这次要寻找的JDK软件的注册表键值在HKEY_LOCAL_MACHINE下,因此要把根键设置为HKEY_LOCAL_MACHINE。

表告诉我你不知道怎么看注册表,开始-〉运行-〉输入命令regedit

***********************************************************************

RegDBKeyExist (“SOFTWARE//JavaSoft//java Development Kit//1.6.0_04″) < 0)

判断是否存在键值SOFTWARE//JavaSoft//Java Development Kit//1.6.0_04,这个是JDK1.6.0_04安装时向注册表写入的值;

RegDBKeyExist( szSubKey );如果存在键值则返回1,否则返回小于0的随机数字。

***********************************************************************

LaunchAppAndWait (SRCDISK^”jdk//jdk-6u4-windows-i586-p.exe”,””, LAAW_OPTION_WAIT);

当上面判断了没有安装JDK1.6.0_04这个软件时,则启动光盘里jdk文件夹下的jdk-6u4-windows-i586-p.exe安装程序来安装。

这个函数在help里是这样叙述的:

LaunchAppAndWait ( szProgram, szCmdLine, nOptions );

参数一:szProgram,即要启动的程序。这里我们写入的参数是SRCDISK^”jdk//jdk-6u4-windows-i586-p.exe”, SRCDISK指源盘,安装程序所在的盘,光盘和硬盘都可以。”jdk//jdk-6u4-windows-i586-p.exe”源盘下jdk文件夹下的jdk-6u4-windows-i586-p.exe安装程序。

参数二:szCmdLine,如果要启动的程序需要从命令行读入参数来启动,那么在这里写入对应的参数值;我们这里不需要,因此输入空字符串””。

参数三:nOptions,静态变量,不同的静态变量会得到不同的执行结果,比如无等待安装,静默安装,鼠标外形改变等等。详情请参阅Installshield自带的Help。这里我们用LAAW_OPTION_WAIT,即当JDK安装结束后(无论是正常安装了,还是用户点击取消了安装),安装程序才往下继续。

clip_image018

这里可以看到,当点击了同意许可协议的时候,安装程序会自动检测是否安装了JDK,如果没有安装,则弹出安装界面。

这里在函数体里面,没有对找不到JDK安装程序,以及安装出错等情况做判断。如果用户有需要,可以添加一个消息框,提示在找不到安装程序或者安装出错的情况下,用户可以手动地安装需要的软件。代码可以改写为

RegDBSetDefaultRoot(HKEY_LOCAL_MACHINE);

if (RegDBKeyExist (“SOFTWARE//JavaSoft//Java Development Kit//1.6.0_04″) < 0) then

if(LaunchAppAndWait (SRCDISK^”jdk//jdk-6u4-windows-i586-p.exe”,””, LAAW_OPTION_WAIT)<0)then

MessageBox (“You haven’t installed JDK 1.6.0_04 yet! “, INFORMATION);

endif;

endif;

小结:至此,判断运行所需软件的功能结束,用户可以自己试验一下判断多个软件。用法就是重复上述代码功能,仍在OnBegin()函数体内执行。

3. 用户的输入信息、所选安装路径、所选安装组件显示在安装界面上

Installshield是自带这个界面的,在安装过程中用户可以看到这个界面,但是这个界面上的信息是空的,这一点让人很是疑惑,怀疑是Installshield的bug。因此我们不得不手动地实现这个功能。

1. 这个功能需要在OnFirstUIBefore()函数体中实现,选择Before Move Data | OnFirstUIBefore选项

clip_image020

2. 选择了这个选项后,软件会自动在编程界面生成大量代码,如图所示,这里的每一个Dlg_SdXXXX都对应着一个界面,例如Dlg_SdWelcome就是对应着最初开始的欢迎界面。如果开发人员对这些很熟悉,可以在这里对每一个界面编程设置。

clip_image022

3. 找到Dlg_SdStartCopy这个界面选项,我们将在这里对已有的代码进行改动,使之显示用户输入的用户信息、所选安装路径和组件等信息

clip_image024

4. 首先定义所需变量。

在begin前定义6个feature的名字和两个NUMBER类型的变量,即蓝色字串。之前在第一部分我们定义了6个可用的feature,这里就要对这6个feature进行一些判断。

在begin字样后对这6个feature赋值,所赋的值就是我们在第一部分定义的feature的名字(Name, not Display Name)。

//—————————————————————————

function OnFirstUIBefore()

NUMBER nResult, nSetupType, nvSize, nUser;

STRING szTitle, szMsg, szQuestion, svName, svCompany, szFile;

STRING szLicenseFile;

LIST list, listStartCopy;

BOOL bCustom;

STRING szFeatureName1;

STRING szFeatureName2;

STRING szFeatureName3;

STRING szFeatureName4;

STRING szFeatureName5;

STRING szFeatureName6;

NUMBER bvOpt1,bvOpt2;

begin

// TO DO: if you want to enable background, window title, and caption bar title

// SetTitle( @PRODUCT_NAME, 24, WHITE );

// SetTitle( @PRODUCT_NAME, 0, BACKGROUNDCAPTION );

// Enable( FULLWINDOWMODE );

// Enable( BACKGROUND );

// SetColor(BACKGROUND,RGB (0, 128, 128));

szFeatureName1 =”Server”;

szFeatureName2 =”Client”;

szFeatureName3 =”Watch_Portion”;

szFeatureName4 =”Log_Portion”;

szFeatureName5 =”Report_Portion”;

szFeatureName6 =”Document”;

5. 在Dlg_SdStartCopy的listStartCopy = ListCreate( STRINGLIST ); 和ListDestroy(listStartCopy);之间加入如下代码。

ListAddString(listStartCopy,”Customer Information:”,AFTER);

ListAddString(listStartCopy,”User Name: ” + svName,AFTER);

ListAddString(listStartCopy,”Company Name: ” + svCompany,AFTER);

ListAddString(listStartCopy,”Destination Location: ” + INSTALLDIR,AFTER);

switch (nSetupType)

case TYPICAL : ListAddString(listStartCopy,”Setup Type: Typical”,AFTER);

case COMPACT: ListAddString(listStartCopy,”Setup Type: Compact”,AFTER);

case CUSTOM: ListAddString(listStartCopy,”Setup Type: Custom”,AFTER);

endswitch;

ListAddString(listStartCopy,” “,AFTER);

ListAddString(listStartCopy,”The Selected Feature:”,AFTER);

if (FeatureIsItemSelected(MEDIA, szFeatureName1)=1) then

ListAddString(listStartCopy,” “+szFeatureName1,AFTER);

endif;

if (FeatureIsItemSelected(MEDIA, szFeatureName2)=1) then

ListAddString(listStartCopy,” “+szFeatureName2,AFTER);

endif;

if (FeatureIsItemSelected(MEDIA, szFeatureName3)=1) then

ListAddString(listStartCopy,” “+szFeatureName3,AFTER);

endif;

if (FeatureIsItemSelected(MEDIA, szFeatureName4)=1) then

ListAddString(listStartCopy,” “+szFeatureName4,AFTER);

endif;

if (FeatureIsItemSelected(MEDIA, szFeatureName5)=1) then

ListAddString(listStartCopy,” “+szFeatureName5,AFTER);

endif;

if (FeatureIsItemSelected(MEDIA, szFeatureName6)=1) then

ListAddString(listStartCopy,” “+szFeatureName6,AFTER);

endif;

6. 代码解释

*************************************************************

ListAddString(listStartCopy,”XXXXXX”,AFTER);

把要显示的信息添加到list里去,这个list的内容稍后会添加到界面上进行显示。

Help里对这个函数是这样描述的:ListAddString ( listID, szString, nPlacementFlag );

参数一:listID,需要用户事先创建一个list,这里我们看到listStartCopy = ListCreate( STRINGLIST );这句话,即创建了一个叫listStartCopy的list

参数二:szString,要添加的字符串

参数三:nPlacementFlag,如果设置为AFTER,则顺序添加;如果为BEFORE,则逆序添加,即新添加的内容会放在前面显示。

*************************************************************

switch (nSetupType)

case TYPICAL : ListAddString(listStartCopy,”Setup Type: Typical”,AFTER);

case COMPACT: ListAddString(listStartCopy,”Setup Type: Compact”,AFTER);

case CUSTOM: ListAddString(listStartCopy,”Setup Type: Custom”,AFTER);

endswitch;

这是根据用户选择的安装类型来显示安装类型信息。安装类型分三种:TYPICAL,COMPACT和CUSTOM。

*************************************************************

if (FeatureIsItemSelected(MEDIA, szFeatureName1)=1) then

ListAddString(listStartCopy,” “+szFeatureName1,AFTER);

endif;

这里的FeatureIsItemSelected(MEDIA, szFeatureName1)=1是一个很重要的函数,将会在本安装程序内反复出现多次。这个函数用于判断用户是否选择了某feature。Help里对这个函数是这样描述的:FeatureIsItemSelected ( szFeatureSource, szFeature );

参数一:szFeatureSource,大意好像是feature的来源,具体不是很明白到底指什么,反正help自带的例子里写的MEDIA照抄没有错。

参数二:szFeatureName1,就是 feature的名字了

如果用户选择了这个feature,返回值就为1,往list里添加一个关于该feature的相关信息即可。

如此反复,判断所有的feature是否被选择,如被选择则添加一个相关信息即可。

clip_image026

这个就是显示了用户信息,安装路径和安装组件的信息。如果没有添加上述代码,这个界面默认是显示的,但是信息栏里是空白的。

顺便说一句,以前在制作这个安装程序的时候,因为这块显示是空白的,那时候对编程也是一窍不通的,情急之下,笔者把这个显示设置的框框设置了不可见。设置方法如下:

找到User Interface | Dialogs

clip_image028

在中间的导航树上找到SdStartCopy这个选项

clip_image030

这里我们使用的是英文界面,因此点击选中English选项

clip_image032

选中这个界面上的将会显示用户信息的框,把右边的Visible选项设置为False即可

小结:在Dlg_SdStartCopy界面里,用户还可以设置左上角显示的标题和消息,szTitle = “”; szMsg = “”;这两行代码如果赋值为空,则显示如图所示的默认信息,用户可以赋值成自己想要显示的信息。

一个完整的Installshield安装程序实例—艾泽拉斯之海洋女神出品(二) –基本设置二

7. 点击引导条上的Application Files

我们将在这里对安装路径进行微调,并且为每个Feature指定需要安装的文件

clip_image002

这里我不想使用Program Files | Company Name | Product Name这个路径,我想使用Program Files | Product Name,我直接点击选中My Product Name[INSTALLDIR]拖动到ProgramFileFolder下,还可以直接将My Product Name 改成自己想要的文件夹名字

clip_image004

接下来,为每个Feature指定要安装的文件。

clip_image006

打开这个下拉列表,所有的Feature都在这里,按顺序来给每一个Feature建立文件夹,并且导入所需的文件。

选择第一个Feature, 即Server,点击My Product Name[INSTALLDIR]节点,右键点击,在菜单上选择New Folder来创建一个文件夹。

clip_image008

创建一个Server文件夹,这个文件夹将用来存放该组件需要的一些文件。

再在Server文件夹下创建一个icon文件夹,存放该组件所用的图标。

clip_image010

然后为该Feature添加安装时该Feature要安装的文件。

这里我们建立的icon文件夹是用来存放这个feature在后面要建立快捷方式时使用的图标的。为这个icon文件夹添加相应的图标文件,并且记住图标文件的来源文件夹,后面设置快捷方式的时候要用。

clip_image012

点击选中要添加文件的文件夹,然后点击右下角的Add Files,然后添加文件

clip_image014

接下来我们为Feature添加文件夹,如果这个文件夹中的全部文件都为这个Feature所需。添加文件夹的好处在于只要文件夹位置和名称不变,那么文件夹里面的文件都是动态加载的,有多少加载多少,不用考虑文件名的改动带来的影响。

点击选中要添加文件夹的文件夹,然后点击右下角的Add Folders,然后添加文件夹。

clip_image016

选中文件夹,点击确定。

clip_image018

会询问你是否要使用动态文件链接,我都选择确定,好处就在于我刚才上面所述。

clip_image020

显示了源文件夹,如果这个文件夹下有子文件夹,并且也需要一并添加进来的话,务必钩选Include subfolders选项。

这里还允许做一些简单设置来包含或者排除一些特定文件,支持通配符。

点击OK确定加入文件夹。

clip_image022

如法炮制为每个Feature建立文件夹,并且添加文件,最后效果如图所示

clip_image024

Document这个Feature,除了文件所用的图标外,什么都不要添加,后面我们将用安装时实时拷贝的方式来拷贝文档进来。

clip_image026

8. 接下来我们为可执行文件创建快捷方式。

点击引导条上的Application Shortcuts

clip_image028

点击New新建一个快捷方式

clip_image030

选择一个要建立快捷方式的Feature。

如果要建立快捷方式的程序为非.exe形式,请把Files Of选择选为All Files(*.*)格式。

我们的程序安装目标路径设置在Program Files下,因此双击[ProgramFilesFolder]打开,层层点击进入。

clip_image032

我们这里要为client.bat建立一个快捷方式,因为这个是启动用的批处理文件。

Installshield可以自动监测到.exe文件的存在,自动生成快捷方式,用户只需要做一些适当修改即可。

clip_image034

新建的快捷方式将出现在这里,名字不好听,样子也不好看,我们将为它改一个名字,并且换一个图标。

选中快捷方式,点击Rename,并且为这个快捷方式改一个适当的名字。

clip_image036

注意右边的几个选项。

Create shortcut in Start Menu,将在开始菜单里创建一个快捷方式。

Create shortcut on Desktop,将在桌面上创建一个快捷方式。

Use alternate shortcut Icon,替换快捷方式的图标

Associate a file extension with the shortcut’s target,没用过,不知道什么意思。

我们在这里将只创建开始菜单的快捷方式,因此钩选第一项。

钩选第三项,并且点击Browse来浏览图标。

clip_image038

请回想刚才在为Feature添加文件的时候,每个feature都添加了对应的icon。这里,请把浏览的文件夹设定为刚才添加icons所用的文件夹,通俗的说,就是你刚才从哪儿添加一个图标进feature的,现在还是从哪儿添加的这个图标。

其实这一点我是一直很费解的,当初不知道要这么选择图标,随便从外面一个任意文件夹里添加了一个图标,以至于打包后死活找不到图标,后来经过试验才知道这个被选中的图标文件要拷贝进来,打包进安装文件才可以。这一点上不能不提一下visual studio,这个工具做安装程序虽然功能一般,但是思想还是不错的,当它的组件指定拷贝了图标文件后,在建立快捷方式时,快捷方式使用的图标是指向虚拟的安装目标路径下的图标文件的,而不是指定到这个实实在在的源文件夹。这一点差别就体现出了思想上的差异。

如法炮制为每个Feature指定快捷方式,Document除外,因为我们在这个feature里除了图标文件外什么都没有添加。

clip_image040

至此我们为每个可执行程序添加了开始菜单下的快捷方式。

我们再切换去Installation Designer,找到System Configuration | Shortcuts。

看到快捷方式在开始菜单中是以 公司名 | 软件名 | 快捷方式   这种形式存在的。事实上我是不喜欢这种形式了,想想点开一层还有一层,不如直接了当来得干脆,因此做一些修改。

clip_image042

这里我改成了如下设置

clip_image044

不要告诉我你不会改,直接拖动Test文件夹往Program Menu(即开始菜单下的那个“所有程序”)下一塞即可,然后删除掉多余的Company Name文件夹。

9. 可能刚才在Project Assistant界面有人已经注意到了左边栏上More Options下Create an uninstallation shortcut这个诱人的字样了。

clip_image046

可是我要告诉你,如果你选择了这种方式建立卸载快捷方式的话,你会很沮丧地发现:

a) 似乎只有在安装某个feature的时候这个卸载快捷方式才会出现(当然,就是那个default feature,这种要命的feature形式决定了每个文件或者快捷方式都必须明确地归属到某个feature下),因此,当你的客户只选择了其他feature安装时,这个卸载方式不会出现,而他必须去“添加或卸载程序”里面去卸载

b) 如果你写脚本使得安装时会拷贝一些外部文件进来,那么这些文件在这种卸载方式下是删除不掉的。(如果你确实想保存这些文件,你可以在脚本里设置它们属性为permanent,这个属性可以保证什么卸载方式都不能删除你的这些文件)。

所以这里我们忽视这个卸载快捷方式的存在,而将在后面采用脚本形式实现完美卸载。

10. 点击引导条上的Application Registry

向注册表写键和键值,由于本工程不需要,忽略之。有需要的朋友可以查阅相关资料,不难。

11. 点击引导条上的Installation Location

这个是用来设置安装包的语言的,选择了多个语言后,用户可以在安装界面开始的时候选择安装时所用的语言;不过作为一个公司产品来说,这么偷懒,客户的印象是要打折扣的,所以还是选个单语言吧,该什么语言的安装包就什么语言的安装包,各归各。

不过你又会沮丧地发现,如果要选择一种其他语言作为Default Language,好像又报错了。

这个问题当时折腾了我一个星期(当然那时候才接触Installshield,还一窍不通),最后问了技术支持才得以解决。

切换去Installation Designer,找到Installation Information | General Information,看到String Tables下面是什么?对,所有你选的语言都列出来了,选中你要的语言,右键,选择Make Default,OK,再切换回Project Assistant去把所有不要的语言统统去掉钩选即可。

clip_image048

看到此处,已经变成了English为默认语言了。

clip_image050

12. 点击引导条上的Build Installation。

打包安装盘的设置,本人从来不用这个选项,都用工具条上的Release Wizard。

至此,第一部分基本完成。如果是一些没有特别要求的安装包,这部分讲解的内容足够可以做一个基本的安装包了

一个完整的Installshield安装程序实例—艾泽拉斯之海洋女神出品(一)—基本设置一

Installshield可以说是最好的做安装程序的商业软件之一,不过因为功能的太过于强大,以至于上手和精通都不是容易的事情,之前都是用Installshield的Project Assistant对付过去的,这次做这个安装程序,为了实现一些功能,必须写代码,国内外现成的资料很少,而且很多都语焉不详,自己反复啃了多次,对比Installshiel自带的help,才明白资料所表达的意思。这个安装程序虽然比较简陋,在行家眼里可能是小菜一碟,但是也花了笔者一个星期的时间,阅读了很多资料,啃了好几天英文help,集成了很多先驱者的经验,也费了自己不少心血做成的,对每一段代码的用处、每一个用到的函数都进行了详细的说明,因此转载时请务必保留转载出处和由艾泽拉斯之海洋女神出品的字样;如需刊登,请与作者联系。

在此要感谢吞硬币的小猪,天下晓明,余满青,海洋C++乐园(此海洋不是彼海洋)等大虾在互联网上的无私奉献,他们的贴子和博客给了我很大启示。

因为本人是做Java出身的,因此对这种类C++语言还是第一次接触,有理解不当之处,请朋友们指正。欢迎Email至little_fairycat@126.com

 

正文

需求:公司做了一个软件产品,

1.         该软件运行需要JDK环境(不是JRE,因为该软件要向windows注册一个服务,用到了JavaService,JDK才支持这个功能;不过这里侧重于判断是否安装了某软件是否安装,而不是纠缠于该装JDK还是该装JRE);

2.         由于是Server-Client形式的,需要允许用户选择安装组件,比如A机只装Server端,B机只装Client端;

3.         文档不打包在安装程序里,直接存放在光盘文件夹下方便用户查看,同时允许用户指定是否安装文档到计算机上(为什么这样做,后面说明详细原因);

4.         该软件会以授权形式发放给用户,不同的用户,软件本身可能相同,而不同的只是授权文件和一些配置,因此希望授权文件和配置文件不打包在安装程序里,而直接存放在光盘里,以减少可能的重复打包安装程序的劳动;

5.         在安装完毕后,希望能自启动程序(因为该软件需要在安装完毕后启动一个程序,该程序实现向Windows注册服务的功能,该程序最好由安装程序启动,而不是由客户手动启动)。

6.         希望有反安装程序

本文提到的“外部”指不打包在安装程序里的,与安装程序一起存放在光盘里的一些文件夹,这些文件夹包含了安装中所需要的文件,同时也可能有其他用途,因此不适合直接压缩打包在安装程序里。

该实例实现了如下功能:

1.         显示软件许可协议

2.         判断是否安装了本软件所需要的先决软件JKD1.6.0_04,如无,则启动外部安装程序进行安装(同样原理可以用来判断是否安装了其他软件,只要该软件在注册表中有键值)

3.         安装允许用户选择需要安装的组件

4.         用户的输入信息、所选安装路径、所选安装组件将显示在安装界面上(Installshield虽然自带了此界面,但是默认是显示为空的,需要写脚本来显示信息)

5.         根据用户选择的组件,在开始菜单显示程序的快捷方式(同样适用于桌面快捷方式,后面做详细说明)

6.         根据用户选择的组件,从外部文件夹拷贝相应的文件到安装目标路径的文件夹中

7.         根据从外部拷贝进来的文件,创建快捷方式(这里主要是拷贝文档,并在开始菜单中创建快捷方式)

8.         在安装结束时,显示readme.txt文件

9.         在安装结束后,启动指定的程序

10.     完美卸载

笔者所用的环境为Installshield 12 Premier Edition,Windows XP with SP2, 该环境下建立的工程可以直接使用在Installshield 2008 Premier Edition下,Installshield 2008在打开Installshield 12所建的工程时会提示你是否需要进行Upgrade,确认即可,软件会自动为你进行升级,很方便。

下面我们一步一步来建立一个基本的工程,并且使用脚本来完善和丰富所需功能

 

第一部分:基本设置

 

1.         打开Installshield 12 Premier Edition,新建一个Installscript MSI Project,这种被称之为半脚本程序,因为兼具Basic Project基本类型和Installscript Project全脚本类型两者的优点,我比较喜欢用。像我这样需求的,既要用到Wizard的便利,又想写一点脚本来实现一点自定义操作的,就比较适合用这种类型啦。

选择类型为Windows Installer | InstallScript MSI Projcet,输入工程名,指定工程所在的文件夹。

 

2.         界面会切换到Project Assistant,我们先从这里开始把工程的基本组件和基本文件建立好。

 

 

4.         点击引导条上的Application Information

这里输入:

公司名,公司名将会出现在Setup.exe的注解中

软件名,将会出现在安装过程的左上角标题栏上

版本号,没看到在哪,不过自己比较方便地知道自己在编译哪个版本的软件

公司网址,没看到在哪,而且如果该公司没有网址呢?而且这里有点bug,好像默认的值总是会报一个String_ID1为空的错误,自己输入一个网址就不会报错。

是否在你创建了更新时自动通知最终用户,没用过,我都选了No。

选择一个图标,这个图标会出现在“添加或删除程序”里,我一般用默认的,当然你可以替换成自己想要的图标。

 

5.         点击引导条上的Installation Requirement

这里选择对操作系统和一些软件的需求。根据自己需要来选择是否要求操作系统的版本,已经是否要求安装了某些软件。

 

6.         点击引导条上的Installation Architecture

这是个十分有用的设置,对于本文所用的分布式软件来说非常合适,分布式软件的每个组件可以设置为一个Feature,用户可以自由选择安装某些功能。

将选项Do you want to customize your Installation选择为Yes。

点击选中根节点Installation Architecture,点击New创建新的Feature,可以为每个Feature指定新名称。

还可以在Feature下创建子Feature,比如如果文档Feature下包括软件本身文档,和软件所需的运行环境的文档,那么可以创建两个子Feature,分别包含两种文档,用户在安装时就可以选择安装部分或者全部文档了。这里我们没有用到子Feature,用途和普通Feature一样。

 

这里,建立好所有Feature后,我们将切换到Installation Designer做一个设置

 

 

找到Installation Designer页面上左边导航树Organization | Features分支,你会看到这里Features都显示为原始的名称,而非我们改过的名字,因为Feature有Name和Display Name两种名称,我们刚才改的不过是Display Name,为了便于查看和使用,我们在这里把Name也改一下

 

 

注意Name不可以有空格,可以使用下划线

 

 

继续切换回Project Assistant

3.      在Project Assistant界面的底部,会有一个引导动作条,在建立该工程的基本结构和文件时,我们都将在此界面进行操作,下文都将以“引导条”来指代这个引导动作条。

InstallShield 获得用户选择的安装语言

关于如何在安装包内获得用户选择的安装语言,方法如下:

InstallScript和InstallScript MSI工程类型

直接可以用系统变量SELECTED_LANGUAGE获得。

Basic MSI工程类型: 

通过获取ProductLanguage的Property值得到用户选择的安装语言。

export prototype SelectedLanguage(HWND);

function SelectedLanguage(hMSI)
   STRING szLanguageID; 
   NUMBER nLength; 
begin
   MsiGetProperty(hMSI, “ProductLanguage”, szLanguageID, nLength); 
   end;

无觅关联推荐,快速提升流量

WPF中解决内存泄露的几点提示与解决方法

一直以来用WPF做一个项目,但是开发中途发现内存开销太大,用ANTS Memory Profiler分析时,发现在来回点几次载入页面的操作中,使得非托管内存部分开销从起始的43.59M一直到150M,而托管部分的开销也一直持高不下,即每次申请的内存在结束后不能完全释放。在网上找了不少资料,甚受益,现在修改后,再也不会出现这种现象了(或者说,即使有也不吓人),写下几个小心得:

1. 慎用WPF样式模板合并

我发现不采用合并时,非托管内存占用率较小,只是代码的理解能力较差了,不过我们还有文档大纲可以维护。

2. WPF样式模板请共享

共享的方式最简单不过的就是建立一个类库项目,把样式、图片、笔刷什么的,都扔进去,样式引用最好使用StaticResource,开销最小,但这样就导致了一些写作时的麻烦,即未定义样式,就不能引用样式,哪怕定义在后,引用在前都不行。

3. 慎用隐式类型var的弱引用

这个本来应该感觉没什么问题的,可是不明的是,在实践中,发现大量采用var与老老实实的使用类型声明的弱引用对比,总是产生一些不能正确回收的WeakRefrense(这点有待探讨,因为开销不是很大,可能存在一些手工编程的问题)

4. 写一个接口约束一下

谁申请谁释放,基本上这点能保证的话,内存基本上就能释放干净了。我是这么做的:

复制代码
    interface IUIElement : IDisposable
    {
        /// <summary>
        /// 注册事件
        /// </summary>
        void EventsRegistion();

        /// <summary>
        /// 解除事件注册
        /// </summary>
        void EventDeregistration();
    }
复制代码

在实现上可以这样:

复制代码
 1 #region IUIElement 成员
 2 public void EventsRegistion()
 3 {
 4     this.traineeReport.SelectionChanged += new SelectionChangedEventHandler(traineeReport_SelectionChanged);
 5 }
 6 
 7 public void EventDeregistration()
 8 {
 9     this.traineeReport.SelectionChanged -= new SelectionChangedEventHandler(traineeReport_SelectionChanged);
10 }
11 
12 private bool disposed;
13 
14 ~TraineePaymentMgr()
15 {
16     ConsoleEx.Log("{0}被销毁", this);
17     Dispose(false);
18 }
19 
20 public void Dispose()
21 {
22     ConsoleEx.Log("{0}被手动销毁", this);
23     Dispose(true);
24     GC.SuppressFinalize(this);
25 }
26 
27 protected void Dispose(bool disposing)
28 {
29     ConsoleEx.Log("{0}被自动销毁", this);
30     if(!disposed)
31     {
32         if(disposing)
33         {
34             //托管资源释放
35             ((IDisposable)traineeReport).Dispose();
36             ((IDisposable)traineePayment).Dispose();
37         }
38         //非托管资源释放
39     }
40     disposed = true;
41 }
42 #endregion
复制代码

比如写一个UserControl或是一个Page时,可以参考以上代码,实现这样接口,有利于资源释放。

5. 定时回收垃圾

复制代码
DispatcherTimer GCTimer = new DispatcherTimer();
public MainWindow()
{
    InitializeComponent();
    this.GCTimer.Interval = TimeSpan.FromMinutes(10); //垃圾释放定时器 我定为每十分钟释放一次,大家可根据需要修改
  this.GCTimer.start();

    this.EventsRegistion();    // 注册事件
}

public void EventsRegistion()
{
    this.GCTimer.Tick += new EventHandler(OnGarbageCollection);
}

public void EventDeregistration()
{
    this.GCTimer.Tick -= new EventHandler(OnGarbageCollection);
}

void OnGarbageCollection(object sender, EventArgs e)
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
}
复制代码

6. 较简单或可循环平铺的图片用GeometryDrawing实现

一个图片跟几行代码相比,哪个开销更少肯定不用多说了,而且这几行代码还可以BaseOn进行重用。

复制代码
<DrawingGroup x:Key="Diagonal_50px">
    <DrawingGroup.Children>
        <GeometryDrawing Brush="#FF2A2A2A" Geometry="F1 M 0,0L 50,0L 50,50L 0,50 Z"/>
        <GeometryDrawing Brush="#FF262626" Geometry="F1 M 50,0L 0,50L 0,25L 25,0L 50,0 Z"/>
        <GeometryDrawing Brush="#FF262626" Geometry="F1 M 50,25L 50,50L 25,50L 50,25 Z"/>
    </DrawingGroup.Children>
</DrawingGroup>
复制代码

这边是重用

<DrawingBrush x:Key="FrameListMenuArea_Brush" Stretch="Fill" TileMode="Tile" Viewport="0,0,50,50" ViewportUnits="Absolute"
Drawing="{StaticResource Diagonal_50px}"/>

上面几行代码相当于这个:

7. 使用Blend做样式的时候,一定要检查完成的代码

众所周知,Blend定义样式时,产生的垃圾代码还是比较多的,如果使用Blend,一定要检查生成的代码。

 

8. 静态方法返回诸如List<>等变量的,请使用out

比如

public static List<String> myMothod()

{...}

请改成

public static myMothod(out List<String> result)

{...}

 

9. 打针对此问题的微软补丁

3.5的应该都有了吧,这里附上NET4的内存泄露补丁地址,下载点这里 (QFE:  Hotfix request to implement hotfix KB981107 in .NET 4.0 )

这是官方给的说明,看来在样式和数据绑定部分下了点工夫啊:

  1. 运行一个包含样式或模板,请参阅通过使用 StaticResource 标记扩展或 DynamicResource 标记扩展应用程序资源的 WPF 应用程序。 创建使用这些样式或模板的多个控件。 但是,这些控件不使用引用的资源。 在这种情况的一些内存WeakReference对象和空间泄漏的控股数组后,垃圾回收释放该控件。
  2. 运行一个包含的控件的属性是数据绑定到的 WPF 应用程序DependencyObject对象。 该对象的生存期是超过控件的生存期。 许多控件时创建,一些内存WeakReference对象和容纳数组空格被泄漏后垃圾回收释放该控件。
  3. 运行使用树视图控件或控件派生于的 WPF 应用程序,选择器类。 将控件注册为控制中的键盘焦点的内部通知在KeyboardNavigation类。 该应用程序创建这些控件的很多。 例如对于您添加并删除这些控件。 在本例中为某些内存WeakReference对象和容纳数组空格被泄漏后垃圾回收释放该控件。

继续更新有关的三个8月补丁,详细的请百度:KB2487367  KB2539634  KB2539636,都是NET4的补丁,在发布程序的时候,把这些补丁全给客户安装了会好的多。

10.  对string怎么使用的建议

这个要解释话就长了,下面仅给个例子说明一下,具体的大家去找找MSDN

复制代码
        string ConcatString(params string[] items)
        {
            string result = "";
            foreach (string item in items)
            {
                result += item;
            }
            return result;
        }

        string ConcatString2(params string[] items)
        {
            StringBuilder result = new StringBuilder();
            for(int i=0, count = items.Count(); i<count; i++)
            {
                result.Append(items[i]);
            }
            return result.ToString();
        }
复制代码

建议在需要对string进行多次更改时(循环赋值、连接之类的),使用StringBuilder。我已经把工程里这种频繁且大量改动string的操作全部换成了StringBuilder了,用ANTS Memory Profiler分析效果显著,不仅提升了性能,而且垃圾也少了。

 

11. 其它用上的技术暂时还没想到,再补充…

 

如果严格按以上操作进行的话,可以得到一个满意的结果:

运行了三十分钟,不断的切换功能,然后休息5分钟,回头一看,结果才17M左右内存开销,效果显著吧。

然后对于调试信息的输出,我的做法是在窗体应用程序中附带一个控制台窗口,输出调试信息,给一个类,方便大家:

复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace Trainee.UI.UIHelper
{
    public struct COORD
    {
        public ushort X;
        public ushort Y;
    };

    public struct CONSOLE_FONT
    {
        public uint index;
        public COORD dim;
    };

    public static class ConsoleEx
    {
        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern bool AllocConsole();

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern bool SetConsoleFont(IntPtr consoleFont, uint index);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern bool GetConsoleFontInfo(IntPtr hOutput, byte bMaximize, uint count, [In, Out] CONSOLE_FONT[] consoleFont);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern uint GetNumberOfConsoleFonts();

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32", CharSet = CharSet.Auto)]
        internal static extern COORD GetConsoleFontSize(IntPtr HANDLE, uint DWORD);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32.dll ")]
        internal static extern IntPtr GetStdHandle(int nStdHandle);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        internal static extern int GetConsoleTitle(String sb, int capacity);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("user32.dll", EntryPoint = "UpdateWindow")]
        internal static extern int UpdateWindow(IntPtr hwnd);

        [System.Security.SuppressUnmanagedCodeSecurity]
        [DllImport("user32.dll")]
        internal static extern IntPtr FindWindow(String sClassName, String sAppName);

        public static void OpenConsole()
        {
            var consoleTitle = "> Debug Console";
            AllocConsole();


            Console.BackgroundColor = ConsoleColor.Black;
            Console.ForegroundColor = ConsoleColor.Cyan;
            Console.WindowWidth = 80;
            Console.CursorVisible = false;
            Console.Title = consoleTitle;
            Console.WriteLine("DEBUG CONSOLE WAIT OUTPUTING...{0} {1}\n", DateTime.Now.ToLongTimeString());

            try
            {
                //这里是改控制台字体大小的,可能会导致异常,在我这个项目中我懒得弄了,如果需要的的话把注释去掉就行了
                //IntPtr hwnd = FindWindow(null, consoleTitle);
                //IntPtr hOut = GetStdHandle(-11);

                //const uint MAX_FONTS = 40;
                //uint num_fonts = GetNumberOfConsoleFonts();
                //if (num_fonts > MAX_FONTS) num_fonts = MAX_FONTS;
                //CONSOLE_FONT[] fonts = new CONSOLE_FONT[MAX_FONTS];
                //GetConsoleFontInfo(hOut, 0, num_fonts, fonts);
                //for (var n = 7; n < num_fonts; ++n)
                //{
                //    //fonts[n].dim = GetConsoleFontSize(hOut, fonts[n].index);
                //    //if (fonts[n].dim.X == 106 && fonts[n].dim.Y == 33)
                //    //{
                //        SetConsoleFont(hOut, fonts[n].index);
                //        UpdateWindow(hwnd);
                //        return;
                //    //}
                //}
            }
            catch
            {

            }
        }

        public static void Log(String format, params object[] args)
        {
            Console.WriteLine("[" + DateTime.Now.ToLongTimeString() + "] " + format, args);
        }
        public static void Log(Object arg)
        {
            Console.WriteLine(arg);
        }
    }
}
复制代码

在程序启动时,可以用ConsoleEx.OpenConsole()打开控制台,用ConsoleEx.Log(…..)或者干脆用Console.WriteLine进行输出就可以了。

WPF的TextBox产生内存泄露的情况

前段时间参与了一个WPF编写的项目,在该项目中有这样一个场景:在程序运行过程中需要动态地产生大量文本信息,并追加WPF界面上的一个TextBox的Text中进行显示。编写完之后,运行该项目的程序,发现在产生大量信息之后,发现系统变慢了,打开任务管理器才发现,该项目的程序占用了将近1.5G的内存(天啊!!!这不是一般的耗内存啊!!!)。后来通过查资料和探索才发现了WPF的TextBox在追加Text显示文本时会造成内存泄露。下面通过一个小Demo程序来展示一下这个内存泄露。

我的Demo程序很简单,就是在界面上显示一个TextBox和一个Button,点击Button后就从0到9999进行for循环并将这些数字追加的TextBox的Text中进行显示。代码如下,

<window x:Class="TextBoxMemoryLeak.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="测试TextBox内存泄露" Height="350" Width="525"
        WindowStartupLocation="CenterScreen">
    <grid Margin="5">
        </grid><grid .RowDefinitions>
            <rowdefinition Height="*"></rowdefinition>
            <rowdefinition Height="35"></rowdefinition>
        </grid>
        <dockpanel Grid.Row="0">
            <textbox Name="tbOutput" IsReadOnly="True" VerticalScrollBarVisibility="Auto"></textbox>
        </dockpanel>
        <stackpanel Grid.Row="1"
                    FlowDirection="RightToLeft"
                    Orientation="Horizontal">
            <button Name="btnStart" Content="开 始" Margin="5,4,5,4" Width="65" Click="btnStart_Click"></button>
        </stackpanel>
    
</window>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace TextBoxMemoryLeak
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            this.btnStart.IsEnabled = false;
            this.tbOutput.Text = "";

            for (int i = 0; i < 10000; i++)
            {
                //使用此语句进行Textbox的追加会造成内存泄露
                //this.tbOutput.Text += string.Format("{0}\n", i);

                //使用此语句进行Textbox的追加可避免内存泄露
                this.tbOutput.AppendText(string.Format("{0}\n", i));
            }

            this.btnStart.IsEnabled = true;
        }
    }
}

界面如下所示:

内存泄露情况

最初我们采用的是TextBox的Text追加方式如下

this.tbOutput.Text += string.Format("{0}\n", i);

构建,启动调试后,我们查看任务管理器,此时所占内存只有16M,

点击【开始】按钮之后,等到从0输出到9999之后,我们再查看任务管理器,发现此时所占的内存飙到了600+M,

若此时再点击【开始】按钮,等循环结束,发现所占内存飙到了900+M,

再点击【开始】按钮的话,就要发生OutOfMemory异常的。当我们将循环改为从0到19999时,第一次点击【开始】按钮,我的机器就发生OutOfMemory异常了。

避免内存泄露的情况

将TextBox的Text追加方式改为下面语句

this.tbOutput.AppendText(string.Format("{0}\n", i));

构建,启动调试,然后点击界面的【开始】按钮,等循环结束,我们查看任务管理器,测试Demo程序只占了29M内存(此时是从0到19999的循环)。

 

 

 

TextBox存在内存泄露的可能

背景

-WPF桌面程序中,增加了一个TextBox控件,用于显示输出的日志信息,日志信息量很大(具体数值未统计)

XAML 代码
 <TextBox Text="{Binding Message}" TextWrapping="Wrap" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" TextChanged="TextBox_TextChanged" />

CS 代码
private void TextBox_TextChanged(object sender, TextChangedEventArgs e)
    {
        if (!(sender is TextBox)) return;
        var tb = (TextBox)sender;
        if (tb.Text.Length > 30000) //textbox maxlength = 32767
      {
        tb.Text = "Auto Clear Histroy Information\n\r";
      }
       tb.Select(tb.Text.Length, 0);
    }

问题

运行一段时间后操作系统弹出信息,称“XXX程序出问题了”(截图后补吧)。在增加日志显示功能前,此异常未出现。

解决

参考:Google搜索到的信息
怀疑是日志信息太多,造成程序内存泄露。TextBox控件支持回退功能(Undo),但是回退功能需要占用更多内存。

设置 
     UndoLimit="0"  或者 IsUndoEnabled="False"
 可关闭Undo功能

如何在.net应用中发现和避免内存和资源泄露

如何在.net应用中发现和避免内存和资源泄露

By Fabrice Marguerie

尽管很多人相信在.net应用中谈及内存及资源泄露是件很轻松的事情。但GC(垃圾回收器)并不是魔法师,并不能把你完全从小心翼翼处理内存与资源损耗中解放出来。

本文中我将解释缘何内存泄露依然存在以及如何避免其出现。别担心,本文不涉及GC内部工作机制及其它.net的资源及内存管理等高级特性中。

理解泄露本身及如何避免其出现很重要,尤其因为它无法轻松地自动检测到。单元测试在此方面无能为力。一旦产品中你的程序崩溃了,你需要马上找出解决方案。所以在一切都还不是太晚前,花些时间来学习一下本文吧。

Table of Content

·         介绍

·         泄露?资源?指什么?

·         如何检测泄露并找到泄露的资源

·         常见内存泄露原因

·         常见内存泄露原因演示

·         如何避免泄露

·         相关工具

·         结论

·         资源

介绍

近期,我参与了一个大的.net项目(暂叫它项目X吧),我在项目中负责追踪内存与资源泄露。大部分时间我都花在与GUI关联的泄露上,更准确地说是一个基于Composite UI Application Block (CAB).的windows窗体应用。接下来我要说的直接应用到winform上的内容,多数见解同样可以适用到其它.net应用中(像WPF,Silverlight,ASP.NET,Windows service,console application 等等)。

我不是个处理泄露方面的专家,所以我不得不深入钻研了一下应用程序,做一些清理工作。本文的目标是与你们分享在我解决问题过程中的所得所悟。希望能够帮助那些需要检测与解决内存、资源泄露问题的朋友。下面的概述部分首先会介绍什么是泄露,之后会看看如何检测到泄露和被泄露资源,以及如何解决与避免类似泄露,最后我会列出一个对此过程有帮助的工具列表及相关资源。

泄露?资源?指什么?

内存泄露

在进一步深入前,让我们先来定义下我所谓的“内存泄露”。简单引用在Wikipedia上找到的定义吧。该定义与我打算通过本文所帮助解决的问题完美的一致:

在计算机科学领域中,内存泄露是指一种特定的内存损耗,该损耗是由一个计算机程序未成功释放不需要的内存引起的。通常是程序中的BUG阻碍了不需要内存的释放。

仍然来自Wikipedia:”以下语言提供了自动的内存管理,但并不能避免内存泄露。像 Java,C#,VB.net或是LISP等。”

GC只回收那些不再使用的内存。而使用中的内存无法释放。在.net中,只要有一个引用指向的对象均不会被GC所释放。

句柄与资源

内存可不是唯一被视为资源的。当你的.net应用程序在Windows上运行时,消耗着一个完整的系统资源集。微软定义了系统三类对象:用户(user),图形设备接口(GUI),以及系统内核(kernel)。我不会在此给出完整的分类对象列表,只是指出一些重要的:

·         系统通过使用用户对象(User objects) 来支持windows管理。相关对象包括:提速缓冲表(Accelerator tables),Carets(补字号?),指针(Cursors),钩子(Hooks),图标(Icons),菜单(Menus)和窗体(Windows)。

·         GDI对象 支持图形绘制:位图(bitmaps),笔刷(Brushes),设备上下文(DC),字体(Fonts),内存设置上下文(Memory DCs),元文件(Metafiles),画笔(Pens),区域(Regions)等。

·         内核对象 支持内存管理,进程执行和进程间通讯(IPC):文件,进程,线程,信号(Semaphores),定时器(Timer),访问记号(Access tokens),套接字(Sockets)等。

所有系统对象的详细情况都可以在MSDN中找到。

系统对象之外,你还会碰到句柄(handles).据MSDN的陈述,应用程序不能直接访问对象数据或是对象所代表的系统资源。取而代之,应用程序一定都会获得一个对象句柄(Handle),可以使用它检查或是修改系统资源。在.net中无论如何,多数情况下系统资源的使用都是透明的,因为系统对象与句柄都由.net类直接或间接代表了。

非托管资源

像系统对象(System objects)这样的资源自身都不是个问题,但本文仍涵盖了它们,因为像Windows这样的操作系统对可同时打开的 套接字、文件等的数量都有限制。所以关注应用程序所使用系统对象的数量非常重要。

在特定时间段内一个进程所能使用的User与GDI对象数目也是有配额的。缺省值是10000个GDI对象和10000个User对象。如果想知道本机的相关设置值,可以使用如下的注册表键:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows: GDIProcessHandleQuota 和 USERProcessHandleQuota.

猜到了什么?确实没有这么简单,还有一些你会很快达到的其它限制。比如参照:我的一篇有关桌面堆的博客 所述。

假设这些值是可以自定义的,你也许认为一个解决方案就是打破默认值的限制—调高这些配额。但我认为这可不是个好主意,有如下原因:

1. 配额存在的原因:系统中不是只有你独自一个应用程序,所有运行在计算机中的其它进程与你的应用应该分享系统资源。

2.  如果你修改配额,使它不同于其它系统了。你不得不确认所有你的应用程序需要运行的机器都完成了这样的修改,而且这样的修改从系统管理员的角度来说是否会有问题也需要确认。

3.  大部分都采用了默认配额值。如果你发现配置值对你应用程序来说不够,那你可能确实有些清理工作要做了。

如何检测泄露及找到泄露的资源

泄露带来的实际问题在MSDN上的一篇文章中有着很好的描述:

哪怕在小的泄露只要它反复出现也会拖垮系统。

这与水的泄露异曲同工。一滴水的落下不是什么大问题。但是一滴一滴如此反复的泄露也会变为一个大问题。

像我稍后解释的,一个无意义的对象可以在内存中维持一整图的重量级对象。

仍然是同一篇文章,你会了解到:

通常三步根除泄露:

1.发现泄露

2.找到被泄露的资源

3.决定在源码中何时何处释放该资源

最直接“发现”泄露的方式是遭受泄露引发的问题

你或许没有见过内存不足。“内存不足”提示信息极少出现。因为操作系统运行中实际内存(RAM)不足时,它会使用硬盘空间来扩展内存。(称为虚拟内存)。

在你的图形应用程序中可能更多出现的是“句柄不足”的异常。准确的异常不是System.ComponentModel.Win32Exception 就是 System.OutOfMemoryException 均包含如下信息:”创建窗体句柄错误”。这两个异常多发于两个资源被同时使用的情况下,通常都因为该释放的对象没有被释放所致。

另外一种你会经常碰到的情况是你的应用程序或是整个系统变更得越来越慢。这种情况的发生是因为你的系统资源即将耗尽。

我来做个生硬的推断:大多数应用程序的泄露在多数时间里都不是个问题,因为由泄露导致出现的问题只在你的应用程序集中使用很长时间的情况下才会出现。

如果你怀疑有些对象在应该被释放后仍逗留在内存中,那需要做的第一件事就是找出这些对象都是什么。

这看起来很明显,但是找起来却不是这样。

建议通过内存工具找到非预期逗留在内存中的高级别对象或是根容器。在项目x中,这些对象可能是类似LayoutView实例一样的对象们(我们使用了MVP(Model View Presentation )模式)。在你的实际项目中,它可能依赖于你的根对象是什么。

下一步就是找出它们该消失却还在的原因。这才是调试器与工具能真正帮忙的。它们可以显示出这些对象是如何链接在一起的。通过查看那些指向“僵尸对象”(the zombie object)的引用你就可以找到引起问题的根本原因了。

你可以选择 ninja方式(译者:间谍方式?)(参照 工具介绍章节中有关 SOS.dll 和 WinDbg 的部分)。

我在项目X中用了JetBrains的dotTrace,本文中我将继续使用它来介绍。在后面的工具相关章节中我会向你更多的介绍该工具。

你的目标是找到最终引起问题的那个引用。不要停留在你找到的第一个目标上,但是也要问问自己为什么这个家伙还在内存中。

常见内存泄露的原因

上面提到的泄露情况在.net中较常见。好消息是造成这些泄露的原因并不多。这意味着当你尝试解决一个泄露问题时,不需要在大量可能的原因间搜寻。

我们来回顾一下这些常见的罪魁祸首,我把它们区别开来:

·         静态引用

·         未注销的事件绑定

·         未注销的静态事件绑定

·         未调用Dispose方法

·         Dispose方法未正常完成

除了上列典型的原因外,还有些其它情况也可能引发泄露:

·         Windows Forms:绑定源滥用

·         CAB:未移除对工作项的调用

我只列出了可能在你应用程序中出现的一些原因,但应该清楚你的应用程序依赖的其它.net代码、库实际使用中也可能引发泄露。

我们来举个例子。在项目x中,使用了一套第三方控件来构造界面。其中一个用来显示所有工具栏的控件,它管理着一个工具栏列表。这种方式没什么,但有一点,即使被管理的工具栏自身实现了IDisposable接口,管理类却永远也不会去调用它的Dispose方法。这是一个bug.幸运的是这发生在一个很容易发现的工作区:只能我们自身来调用所有工具样的Dispose方法了。不幸的是这还不够,工具栏类自身问题也不少:它并没有释放自身承载的控件(按钮,标签等等)。所以在解决方案中还要添加对每个工具栏中控件的释放,但是这次可就没那么简单了,因为工具栏中的每个子控件都不同。不管怎么样这只是一个特殊的例子,我要表达的观点是你应用程序中使用的任何第三方库、组件都可能引发泄漏。

最后,还有一种由.net framework造成的泄露,由一些不好的使用习惯引起。即使.net framework自身可能引发泄露,但这是你极少会遭遇到的情况。把责任推到.net身上很容易,但在我们把问题推到别人头上前,还是应该先从自身写的代码出发,看看里面有没有问题。

常见泄露演示

我已经列举出了泄露主要的来源,但我还不想仅限于此。如果每个泄露我都能举个鲜活的例子的话,我想本文会更实用些。好,我们先启动Vs 和 dotTrace , 然后看些示例代码。我会同时演示如何解决或是避免每个泄露情况。

项目X中使用了CAB和MVP模式,这意味着界面由工作空间、视图和呈现者组成。简单起见,我决定使用包含一组窗口的Winform应用。其中使用了与Jossef Goldberg的一篇关于“Wpf应用程序内存泄露”文章中相同的方法。甚至我会直接把相同的例子和事件处理函数应用到我的Winform App中。

当一个窗体被关闭及处置后,我们期待的结果是它同时也在内存中被释放了。对吧?但我下面要展示的是何种情况下该窗体未被释放。

下面的就是我创建的示例程序中的主窗口:

这个主窗口可以打开不同的子窗口;每个打开的子窗口都会分别引发不同的内存泄露。

本文相关示例代码在后面的“资源”一节中可以找到。

静态引用

我们先把显而易见的放在一边。如果一个对象被一个静态字段引用,那它永远也不会被释放。

像singletons模式中就是如此。每个Singletone对象通常都是一个静态对象,即使不是静态对象,那它至少也是会长期存在的对象。

这种情况很显而易见,但是记住不只是直接引用才危险。真正的危险往往来自间接引用。事实上,你一定要注意引用链。整个引用链中有多少个根。如果有静态对象作为根,那所有它的子节点将会始终存在着。

上图上如果Object1是静态的话,多数情况下会长时间存在,那所有它下面的引用将会一直在内存中保留着。危险就在于链条太长了以至于忽略了根节点是静态的。如果你只关注在一个深度级别上,考虑Object3和Object4一旦Object2离开内存那它们也就被释放了。这个没错,确定是,但你需要考虑到它们可能因为Object1一直存在而未被释放。

小心各种静态类型。如果可能尽量不用。非要使用的话,花些时间关注它们。

一个来自特定类型的风险—静态事件,我会在讲解常规事件相关内容时介绍它。

事件,或 “失效监听器”问题

一个子窗口订阅了主窗口中的一个事件,以便主窗口透明度变化时得到通知(包含在EventForm.cs文件中):

C#

mainForm.OpacityChanged += mainForm_OpacityChanged;

问题就是这个针对OpacityChanged事件的订阅创建了一个从主窗口到子窗口的引用。

下图显示了完成事件订阅后两个对象间是如何通讯的:

看看我这篇更多学习事件与引用的博文。下图就是该文章中体现事件观察与被观察者背后引用关系的:

下图所示就是你使用dotTrace搜索EventForm然后点击“最短路径”的结果:

应能看到主窗体(MainForm)保留着对事件窗体(EventForm)的引用。这种情况会出现了第一个你在应用中打开的事件窗口(EventForm)。这就意味着所有在应用中打开的事件窗口(EventForm)只要程序还未销毁,哪怕你已经不在使用它们了(包括关闭了它们)。

这些子窗口不光只是赖在内存中,它们还可能会引发异常,比如你改变了主窗口(MainForm)的透明度(opacity),那些已经被关闭的事件窗口(EventForm)就会引发异常,因为主窗口依然通过事件通知他们,但它们已经是“已处置(disposed)窗口”了。

最简单的解决方案就是通过在这些事件窗口(EventForm)被处置(dispose)时取消事件订阅,从而移除主窗口对事件窗口的引用:

C#

Disposed += delegate { mainForm.OpacityChanged -= mainForm_OpacityChanged; };

注意:我们这里有一个问题,MainForm对象在整个应用被关闭后依然在内存中存在。较短的生命周期内的对象相互引用可能不会引起内存问题。任何孤立的对象链都会被GC自动从内存中卸载掉。孤立的对象链由两个单向引用的对象或是一组没有外部引用的连接对象组成。

另一个解决方案是使用基于弱引用的弱委托。在我的那篇《事件与引用》的博文中有涉猎。网上也有几篇文章讲解了如何付诸实现。比如这篇:“弱引用事件”。找到的多数解决方案都是基于弱引用类。更多弱引用方面的学习可以参见MSDN

注意一下,一个以“弱事件模式”形成的解决方案已经在WPF中存在了。

现有的一些框架中如:CAB (Composite UI Application Block) 或 Prism (Composite Application Library) 均有一些其它的解决方案,像EventBroker 和 EventAggregator 。只要你想也可以使用自己实现的其它事件模式:broker/aggregator/mediator

订阅了静态对象或是生命周期较长的对象上的事件却没有适时取消订阅也会造成问题。另外一种问题来源于静态事件。

静态事件

来直接看个例子(StaticEventForm.cs中):

C#

SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;

这次的例子与前面的很相似,不同的是这次我们订阅的是一个静态事件。因为是一个静态事件,所以对应的监听对象永远也不会被释放掉。

解决方案就是当我们的监听者完成所要做的事情后取消该静态事件订阅。

C#

SystemEvents.UserPreferenceChanged -= SystemEvents_UserPreferenceChanged;

Dispose方法没有被调用的情况

你是否已经开始注意事件和静态类型了呢?很好,但这还不够。你仍能发现一些游弋的未释放对象,即使你写了正确的清理代码。这种情况的发生有时仅仅只是因为这些清理的代码未被调用。。。

在Dispose方法或是Disposed事件中取消事件订阅和释放资源是很好的习惯,但如果未调用Dispose那也没有用。

再来看个有趣的例子。下面示例代码给一个窗体创建了一个上下文菜单(来自ContextMenuStripNotOKForm.cs):

C#

ContextMenuStrip menu = new ContextMenuStrip(); menu.Items.Add(“Item 1”); menu.Items.Add(“Item 2”); this.ContextMenuStrip = menu;

在窗口已经关闭且Dispose的情况下,下图是你可以通过dotTrace 看到的结果:

ContextMenuStrip仍在内存中!注意:想复现该问题,先通过右键鼠标显示上下文菜单,然后关闭窗口。

这就是一个由静态事件引发的泄露。通常解决方法就是下面的在Disposed事件处理程序中调用ContextMenuStrip的Dispose方法。

C#

Disposed += delegate { ContextMenuStrip.Dispose(); };

我猜你已经开始明白如果不多加小心,在.net中使用事件是很危险的。此处我想强调的是只此一行代码就轻易的引起了泄露。而当你创建一个Context-menu时是否考虑过潜在的内存泄露风险呢?

可能比你想象的还要糟。不只ContextMenuStrip未被释放,它使得整个窗体都还在内存中保留着!在下面截图中你可以看到ContextMenuStrip引用着窗体:

导致的结果就是只要ContextMenuStrip还在内存中,整个窗体也会被释放。哦,当然你也不要忘了,因为窗体还在,与其相关的一组对象都会持续保留在内存中 – 像窗体上的控件包含的组件,如下图所示:

这是我严重警告一定要对这种情况足够注视的原因。可能因为一个小对象而使内存中一个大的对象链不能被正常释放。我总能在项目X中看到这种情况的出现。水滴石穿,不要小看一滴水能带来的破坏。

因为单个控件不是指定它的父控件就是指向父控件的事件,所以这使得如果某个控件没有被调用dispose而致整个对象链都无法释放的潜在可能。这当然也包括上级容器包含的其它控件。所以示例中导致整个窗体还问题存在于内存中的情况也就可能出现(至少在整个应用程序完全终止前是这样的)。

在这个具体例子中,你可能正在考虑是否有ContextMenuStrip的地方总是出现这个问题呢。当然不会问题出现,使用设计器直接在窗体上直接创建,至少在此情形下,Vs自动生成的代码可以确保ContextMenuStrip相关组件可能被正确处置。

如果你对于设计器是如何处理的好奇,可以看看ContextMenuStripOKForm类及它的字段在ContextMenuStripOKForm.Designer.cs中是如何处理的。

我想指出另外一个在项目x 中看到的解决方法。由于某些原因,有些控件在源文件中没有与之相关的.Designer.cs文件。设计器代码生成的代码就在.cs文件中。别问我为什么。除了与众不同的代码结构(不推荐)外,问题在于这些代码是被完整拷贝过去的:但要吗Dispose方法就没有,要吗就是没有调用组件的Dispose方法。我想你能明白这种情况下出问题就不奇怪了。

不完整的Dispose方法

我想现在你已经领会了调用那些实现Dispose模式类的Dispose方法的重要性了,强调Dispose这是我要说的一件事情。在你的类中实现IDispose接口以及包含其它Dispose的调用这非常棒,对你的程序也非常有益,只要Dispose方法实现正确。

这段评论看起来好像有点儿傻,但是我这么做是因为我看到太多这种不完整实现Dispose的例子了。

你清楚它是怎么发生的。你创建好了自己的类;实现了IDisposable接口;你在Dispose方法中取消事件订阅和释放资源;并且你在所有需要调用Dispose的地方都调用了它。这很好,直到后来你的类中有一个订阅了新的事件或是消耗了新的资源。编码很容易,你热情洋溢的完成编码、测试。它运行起来很好,所以你很高兴。你提交代码,不错!但是。。。唔,你忘记了更新Dispose方法去释放新的事件或资源了。这种事情总在发生。

我就不举例了。这应该相当常见。

Windows窗体:绑定源误用

我们来解决一个Windows窗体上的问题。如果你用BindingSource组件,请确认你是按照它设计时给定的方式使用的。

通过静态引用,我已经看到了出现的BindingSource,而BindingSource的运转方式导致了内存泄露。使用BindingSource作为数据源的控件都保留着一个BindingSource的引用 ,即使这些控件被处置后。(Disposed)

下图所示的情况,就是在一个数据源是静态或长生命周期的BindingSource(如:BindingSourceForm.cs)的ComboBox被处置后 用doTrace查看的结果。

一个解决方案就是用一个BindlingList替代BindingSource.如你可以这样,拖放一个BindingSource到你的窗体上(指设计时),将BindingList分配给BindingSource作为它的数据源,同时把BindingSource分配给ComboBox作为它的数据源。这种方式下,你仍将使用一个BindingSource。

看BindingListForm.cs(例子源码中)中的这个处理。

这种方式并没有妨碍你使用BindingSource,但是应该在视图界面中创建它 (设计时窗体中,从而生成自动化代码。).总之这样做是有道理的:BindingSource是一个定义在System.WindowsForms命名空间中的表现层组件。BindingList比较而言,它只是一个集合,不隶属于可视化的组件。

注意:如果你并不是非用BindingSource不可,可以完全只用BindingList。

CAB:缺少从工作项(WorkItem)上移除

下面是一条对针对CAB应用程序的建议,但是你也可以应用到其它类型的应用中。

工作项(WorkItems)是构建CAB应用程序的中心。一个工作项(WorkItems)就是一个在上下文中保留相关对象轨迹的容器,并且执行依赖注入。通常一个视图(View)创建后就会增加一个工作项(WorkItems)与之对应。当视图关闭且被回收后,它应该从对应的工作项(WorkItems)上移除,否则工作项(WorkItems)就会使得它(视图)始终存于内存中,因为工作项(WorkItems)中维护着一个指向视图的引用。

如果你忘记了从对应工作项(WorkItems)上移除视图,那泄露由此产生。

在项目x中,我们使用了MVP设计模式(Model-View-Presenter)。下图显示了一个视图显示后不同元素间是如何连接的:

(译者:此处的插图有误,所以未添加)

注意WorkItem通过依赖注入得到presenter。而WorkItem多数时候又会顺便把presenter注入到视图(View)中。为了确保项目X中的所有内容都能适当的释放掉,我们使用了下图所示的一个职责链:

当一个视图(View)被处置(disposed)了(很可能就是因为它被关闭了),那它的Dispose方法就会被调用。该方法会依次调用presenter的dispose方法。Presenter认识WorkItem,在它的Dispose方法中会把自己和源头的视图从Worktem中移除掉。通过这种方式,所有内容都被适当的处置和释放了。

我们的应用程序框架中包括了实现了上面职责链条的基类,所以视图开发人员不需要重要实现这些类也不用每次都为此担心。即使不是CAB应用程序,我也鼓励在你的应用中实现这类模式。在你的对象中正确实现自动释放模式将助你避免那些由于疏忽引起的泄露。但要确保所有实现的方式一致,不要因为他们不知道适当的处理方式而出现每个开发人员实现都不同的情况,这也会导致泄露。

如何避免泄露

现在你对泄露本身以及它是如何产生的有了进一步的了解,此时我想强调几个重点并给出几个技巧。

我们先来探讨一条一般的规则。通常一个负责创建另外一个对象的对象也负责处置(disposing)它。当然不包括工厂类。

反过来:一个对象对于从别的对象上得到的对象没有处置(disposing)的职责。

事实上,这确实要依靠具体情形而定。无论如何,重要的是谨记对象属于谁(who created it)。

第二条原则:每一个+=(事件订阅)都是一个潜在的敌人!

根据我的个人经验,事件是.net中的主要泄露来源。它值得你反复确认甚至确认再三。每次你在代码中增加事件订阅,都应该考虑一下结果问问自己是否需要再增加一个-=来取消事件订阅。如果答案是需要,在你没忘记前马上加上。经常是加到Dispose方法中。

为了确保对象被成功回收,推荐的做法是有事件订阅的对象就需要对应的事件取消订阅。无论如何,当你绝对清楚一个事件源将不再发布事件通知了,而且你希望所有订阅它事件的其它对象能被释放,那么你可以强制移除所有该事件的订阅。我一篇博文中包含了如何做的代码事例。

马上给出一个技巧。通常当对象引用在一定数量的多个对象间共享时,问题就会出现了。因为这种情况下明了哪个对象引用了哪些引用就很困难了。有时在内存中克隆所需的对象要胜于引用现有对象,这样可以避免对象间反复缠绕。

最后,即使这已是.net中众所周知了,我还是想要再次强调调用Dispose的重要性。每次你分配了一个资源,一定要确保调用了Dispose或是将资源使用的代码写在一个using代码块中。如果你不是始终都这么做的话,你很快会被资源泄露搞死,通常情况下都是非托管资源引起的。

Tools

相关工具

几款工具可能助你追踪对象实例、也包括系统对象和句柄。我来列举几个。

Bear

Bear是一个可显示出所有Windows下运行进程信息的免费程序:

·         支持所有GDI对象的用法(hDC, hRegion, hBitmap, hPalette, hFont, hBrush)

·         支持所有用户对象的使用(hWnd,hMenu,hCursor,SetWindowsHookEx,SetTimer 和其它形式的对象)

·         句柄统计

GDIUsage

另外一个实用的工具是GDIUsage.这款工具也是免费的而且开源。

GDIUsage聚集在GDI对象上。通过它,你可以对当前GDI消耗情况拍摄快照,执行一个可能诱发泄露的行为,之后比较泄露前后资源的使用情况。这样可以大大的帮助我们,它能让我们看到操作期间增加了(或是释放了)哪些GDI对象。

此外,GDIUsage不光只是给出一个数字,还可以提供GDI对象的图形化显示。肉眼观察位图(bitmap)泄露的内容可以轻松的找出泄露的原因。

dotTrace

JetBrains出品的dotTrace是一个.net程序的内存与性能分析工具。

下图就是dotTrace的截屏。这也是项目X中我用的最多的工具。其它.net分析工具我也不太了解,但是dotTrace为我解决项目X中检测到的泄露提供了所需的信息。多达20个以上。。。我没说过这就是一个bug项目?

dotTrace允许你及时标识出特定时间下内存中的对象,它们如何存在(被哪些对象引用),以及它们是谁(谁被引用)。你还可以使用它提供的高级调试功能:追踪栈分配情况,查看销毁对象列表等。

下图展示的是两种内存状态间的差别

dotTrace也是一个性能分析工具:

使用的方法就是先启动dotTrace然后指定exe文件的路径,之后就可以请求它对你选择的应用程序进行分析了。

如果你想检查应用程序的内存使用情况,可以在程序运行时用dotTrace拍摄快照,然后让它显示出相应信息。你要做的第一件事大概就是让它显示出指定类在内存中有多少个实例,以及这些实例是如何存在的。

除了搜索托管实例外,你也可以搜索非托管资源。dotTrace没有对非托管资源追踪提供直接支持,但你可能搜索对应的.net包装对象。例如:你搜索位图、字体或是笔刷类的实例。如果你发现一个实例没有被释放,那么它在应用程序上分配的资源也仍然还在。

下一个我要介绍的工具就内置了对非托管资源的追踪。也就是说通过它你就能够直接探索HBITMAP, HFONT 或是 HBRUSH 句柄。

.net内存分析器

.net内存分析器是另一个有趣的工具。它提供了一些dotTrace不包括的实用特性:

·         查看哪些已经调用了处置方法,却还存在的对象

·         查看哪些已经被释放却没有调用处置方法的对象

·         非托管资源的追踪

·         附加到一个运行中的进程上

·         附加到一个进程的同时作为VS的调试器

·         自动内存分析(关于常见内存使用问题的提示和警告)

另外一些可用的内存分析器

上述几个工具只是一些工具所能帮助你的示例。dotTrace和.NET Memory Profiler是众多.net内存及性能分析工具中的两个。其它一些有名的包括:ANTS ProfilerYourKit ProfilerPurifyPlusAQtime 和 CLR Profiler.这些工具中的多数都提供了和dotTrace相同类型的功能。在SharpToolbox.com上你可以找到完整的专注于.net的分析工具集合

SOS.dll and WinDbg

另一个你可用的工具是SOS.dll. SOS.dll 是一个扩展调试的工具,帮助你在WinDbg.exe调试器和VS中调试托管程序,提供CLR资源有关的内部信息。可以用它来获取与GC相关的信息,与内存中对象、线程与锁、调用栈等相关的信息。

WinDbg 是你通常需要附加到产品中某个进程时用的较多的工具。关于SOS.dll 和 WinDBg 如果想更多了解可以看看 Rico Marian 的一篇博文,和Mike Taulty的两篇博文(SOS.dll 与 WinDbg 和 SOS.dll 与 Visual Studio) ,还可以参照Wikipedia

SOS.dll 和 WinDbg 作为Windows调试工具包中的一部分由微软免费提供。二者比之上述的其它工具的一大优势就是在保持强大功能的同时又有着较低的资源损耗。

下图是使用sos.dll和gcroot命令的示例输出:

WinDbg screenshot:

自定义工具

除去市面上的可用工具外,别忘了你也可以创建自己的工具。可以在多个应用程序中重用的独立工具。当然开发这种工具可能有点儿难度。

我们为项目X开发了一套完整工具,帮助我们保持实时的资源使用情况和潜在泄露情况进行跟踪。

这些工具之一在右侧显示一组存在与消亡的对象列表。它由一个CAB服务和一个CAB视图组成,可以用来检查我们期望被释放的对象是否真的被释放了。

下图就是该工具的截图:

如果保留应用中所有对象的痕迹,对于大的应用程序来说代价太高并且也违背产品设定的对象数量。事实上,我们无需关注所有的对象,只要关注那些应用中的高级别对象和根容器。这些对象我在解释如何检测泄露时已经提议过进行追踪。

创建该 工具使用的技术很简单。它使用了弱引用。弱引用类允许你引用一个同时可被GC回收的对象。此外,它还通过提供IsAlive属性提供你测试该引用是否已消亡的功能。

项目X中,我们还有一个提供GDI和用户对象使用概况的小部件。

当资源接近枯竭之时,这个小部件会有一个小的警告图标:

此外,该工具还会让用户关闭一些当前打开的窗口/选项卡/文档,并且阻止用户打开新的窗口等,直到资源使用情况重新低于临界级别。

为了读取到当前UI资源使用情况,我们使用了User32.dll中的GetGuiResources API。下面代码展示了如何在C#中引入该API:

C#

// uiFlags: 0 – Count of GDI objects

// uiFlags: 1 – Count of USER objects

// GDI objects: pens, brushes, fonts, palettes, regions, device contexts, bitmaps, etc.

// USER objects: accelerator tables, cursors, icons, menus, windows, etc.

[DllImport(“User32”)]

extern public static int GetGuiResources(IntPtr hProcess, int uiFlags);

public static int GetGuiResourcesGDICount(Process process)

{

  return GetGuiResources(process.Handle, 0);

}

public static int GetGuiResourcesUserCount(Process process)

{

  return GetGuiResources(process.Handle, 1);

}

通过Process.GetCurrentProcess()的WorkingSet64属性,获取内存使用情况。

结论

我希望本文为你改善应用程序和解决泄露方面提供了一个良好的基础。追踪泄露可以很有趣。。。如果你确实没什么其它更好的事情做的话:-)有时,你别无选择,因为对于你的应用程序来说解决泄露至关重要。

一旦解决了泄露,仍有工作要做。我强烈建议你在尽量降低资源消耗方面改善应用程序。不要损失功能。最后,我邀请你阅读我的另外一篇包含相关建议的博文

相关资源

演示程序源代码下载地址

假如你有意进一步钻研,下面是一些有趣的补充资源

·         Jossef Goldberg: Finding memory leaks in WPF applications

·         Tess Ferrandez has a series of posts about memory issues (ASP.NET, WinDbg, and more)

·         MSDN article by Christophe Nasarre: Resource Leaks: Detecting, Locating, and Repairing Your Leaky GDI Code

·         Article in French by Sami Jaber: Audit et analyse de fuites mémoire

·         My blog post about the Desktop Heap

·         My blog post about lapsed listeners

·         My blog post that shows how to force unsubscription from an eventc

WPF – 简单异步模式

以WeatherForecast为例. 需求: 用户在窗体上点击一个按钮, 程序去网络上查询天气情况, 并把结果显示在窗体上. 网络查询是一个耗时任务, 在等待结果的同时, 用户将看到一个旋转的时钟动画表示程序正在查询.

 

模式为:

  1. 窗口类MainWindow中有耗时函数: string FetchWeatherFromInternet().
  2. 窗口类MainWindow中包含一个函数 UpdateUIWhenWeatherFetched(string result), 用于把任务结果显示在界面上.
  3. 当用户点击按钮时, 在 btnFetchWeather_Click() 中,

    如果是同步调用, 代码很简单, 如下:
    string result = this.FetchWeatherFromInternet();
    this.UpdateUserInterface( result );

    现在需要异步执行, 稍微麻烦点, 需要用到3个delegate, 其中一个代表要执行的任务, 另一个代表任务结束后的callback, 还有一个代表交给UI执行的任务, 上述代码被替换成如下:

    // 代表要执行的异步任务
    Func<string> asyncAction = this.FetchWeatherFromInternet();

    // 处理异步任务的结果
    Action<IAsyncResult> resultHandler = delegate( IAsyncResult asyncResult )
    {
    //获得异步任务的返回值, 这段代码必须在UI线程中执行
    string weather = asyncAction.EndInvoke( asyncResult );
    this.UpdateUIWhenWeatherFetched( weather );
    };

    //代表异步任务完成后的callback
    AsyncCallback asyncActionCallback = delegate( IAsyncResult asyncResult )
    {
    this.Dispatcher.BeginInvoke( DispatcherPriority.Background, resultHandler, asyncResult );
    };

    //这是才开始执行异步任务
    asyncAction.BeginInvoke( asyncActionCallback, null );

=======================================================================

private void ForecastButtonHandler(object sender, RoutedEventArgs e)
{
    this.UpdateUIWhenStartFetchingWeather();

    //异步任务封装在一个delegate中, 此delegate将运行在后台线程 
    Func<string> asyncAction = this.FetchWeatherFromInternet;

//在UI线程中得到异步任务的返回值,并更新UI
//必须在UI线程中执行

    Action<IAsyncResult> resultHandler =

delegate(IAsyncResult asyncResult)
{
string weather = asyncAction.EndInvoke(asyncResult);
this.UpdateUIWhenWeatherFetched(weather);
};


    //异步任务执行完毕后的callback, 此callback运行在后台线程上. 
    //此callback会异步调用resultHandler来处理异步任务的返回值.
    AsyncCallback asyncActionCallback = delegate(IAsyncResult asyncResult)
    {
        this.Dispatcher.BeginInvoke(DispatcherPriority.Background, resultHandler, asyncResult);
    };

    //在UI线程中开始异步任务, 
    //asyncAction(后台线程), asyncActionCallback(后台线程)和resultHandler(UI线程)
    //将被依次执行
    asyncAction.BeginInvoke(asyncActionCallback, null);
}

private string FetchWeatherFromInternet()
{
    // Simulate the delay from network access.
    Thread.Sleep(4000);
    String weather = "rainy";
    return weather;
}

private void UpdateUIWhenStartFetchingWeather()
{
    // Change the status
    this.fetchButton.IsEnabled = false;
    this.weatherText.Text = "";
}

private void UpdateUIWhenWeatherFetched(string weather)
{
    //Update UI text
    this.fetchButton.IsEnabled = true;
    this.weatherText.Text = weather;
}

在WPF的DataGrid中对行添加单击事件

在做的一个c#的项目中发现Datagrid没办法直接对鼠标单击进行响应,调用MouseDown事件也需要点击某一行第二次才能响应。所以借助EventSetter来简单的实现了一个。

界面部分的代码

        <DataGrid x:Name="dataGrid" HorizontalAlignment="Left" Margin="10,38,0,0"
                  VerticalAlignment="Top" Height="257" FontSize="13.333"
                  AutoGenerateColumns="False" AlternationCount="2" CanUserAddRows="False"
                  MinWidth="504" Width="513" SelectionUnit="FullRow">


            <DataGrid.RowStyle>
                <Style  TargetType="DataGridRow">
                    <EventSetter Event="GotFocus" Handler="Item_GotFocus"/>
                </Style>
            </DataGrid.RowStyle>
            <DataGrid.Columns>
                <DataGridCheckBoxColumn />
                <DataGridTextColumn Header="歌曲名" Width="200" Binding="{Binding Path=Title}" IsReadOnly="True"/>
                <DataGridTextColumn Header="艺术家" Width="127" Binding="{Binding Path=Artist}" IsReadOnly="True"/>
                <DataGridTextColumn Header="专辑" Width="140" Binding="{Binding Path=Album}" IsReadOnly="True"/>
            </DataGrid.Columns>
        </DataGrid>
  • 对应的c#的代码
        private void Item_GotFocus(object sender, RoutedEventArgs e)
        {
            var item = (DataGridRow)sender;
            FrameworkElement objElement = dataGrid.Columns[0].GetCellContent(item);
            if (objElement != null)
            {
                CheckBox objChk = (CheckBox)objElement;
                objChk.IsChecked = !objChk.IsChecked;
            }
        }

附上用mousedow事件的代码

        private void DataGrid_MouseDown(object sender, MouseButtonEventArgs e)
        {
            if (e.LeftButton == MouseButtonState.Pressed)
            {
                var se = dataGrid.SelectedItem;
                FrameworkElement objElement = dataGrid.Columns[0].GetCellContent(se);
                if (objElement != null)
                {
                    CheckBox objChk = (CheckBox)objElement;
                    objChk.IsChecked = !objChk.IsChecked;
                }
            }
        }
  • 需要在这个界面的构造函数中添加
 dataGrid.MouseDown += DataGrid_MouseDown;
  • 再附上一个效果图

这里写图片描述

1