2004-06-23的日记

今天第一天开通了Blog,也是我第一次开始去认识Blog,很感谢CSDN给了广大的网友这样的一次机会。

开通了Blog也不知道具体有什么意义,只是自己看着以前在文档中心贴的文章跑了过来,换了一副模样,觉得还有那么点意思,好歹Blog里有了点东西。

学期快结束了,要交几篇叫作业也好或都叫论文也好的东西。虽然学校改名成“信工大”了,宿舍里却还是只能拨号上网,于是只得去机房上网查一点资料。搜到了一点资料,这时发现了Blog的第一个作用:收藏文章。这就是我的Blog中“我的临时收藏夹”的由来。我觉得把一些觉得可能用点用的文章放进去是一个不错的选择。至于觉得值得长期收藏的文章,以后再在“收藏”中建相应的分类吧。

从来没有写日记习惯的我,突然发现如果每天能把发生的一些事情记录下来说不定还是挺有意思的,借开通Blog的契机,我打算把它作为我的日记本来用。这也就是这篇文章的由来。

今天发生的事:

开通了Blog,花了一点时间去熟悉它的功能和使用,又花了点时间写了这篇文章。

最近对开源软件突然有了很大的兴趣,学习Emacs的使用导致了我想为它翻译教程和手册,于是就加入了GNU的CTT(Chinese Translation Team)。作为CTT的第46名成员,很想为它多做点贡献,但是连今天在内折腾了整整两天也没能成功用的CVS连上团队的Repository。我是在Windows Server 2003+PuTTY+CVSNT中的命令行cvs来连的,总是返回出错提示end of file from server,在savannah的Support中发现也有不少朋友遇到这样的问题,据说用1.11.9版的cvs是好的,用Cygwin中的1.11.6是不行的,可我用的cvs已经是2.0.11的了,还是连不上,郁闷呀,不知谁能帮我解决这个问题

想写篇关于乐谱识别的小论文,搜了一下,出乎意料有很多人做过,这也挺好,有了很多的参考资料,学习一下,结合自己的实验就可以完成它了。

明天要去南京大学参加Intel的一个活动,但据说明天是大雨,那就要比较郁闷了,真得只能听天由命了。

[收藏] 智能客户端(SmartClient)

摘要
本 文主要讨论基于企业环境的客户端应用程序模型,由于本人曾经从事过传统的客户端/服务器两层结构应用程序和基于.net平台的多层结构应用程序的开发,因 此本文将着重描述.net平台上的智能客户端应用程序模型,并根据一般的企业应用系统的需求来一步一步构造出一个较为完整的客户端软件框架。

目录
简介
概述
定义
.net平台WinForms应用程序举例
一、系统需求
二、运行方式
三、创建程序
四、发布程序
五、更新程序
六、扩展应用组件
小结
参考资源链接

简介
智 能客户端的概念作为Microsoft.net平台的一个特性而被提出,其实它的很多功能在传统的应用程序中早已存在,只是.net平台从系统和语言级别 对它提供了支持,使开发智能客户端应用程序更为便捷。本文将从企业应用系统的角度来讨论智能客户端应具有的功能和特性以及如何在.net平台上实现。

概述
1、当前的客户端应用程序模型比较
当 前的客户端应用程序模型大致分为两种:C/S(客户端/服务器模型)和B/S(浏览器/服务器模型)。以企业环境为对象的软件开发人员,对于以浏览器为基 础的精简型客户端 (Thin Client) 应用程序模型,以及对应的丰富型客户端 (Rich Client) 之间,究竟该如何取舍,面临了困难的世代替换。
以浏览器为基础的应用程序的优缺点:
# 易于安装:可以用于许多桌上型计算机,并且和客户机算计的操作平台无关。大多数计算机已经默认安装有浏览器软件(有些应用系统需要基于IE浏览器,或者需要安装java虚拟机,在此暂且忽略不及)。
# 易于部署与维护:只需要在服务器端进行部署和维护工作。
# 必须在线工作:工作效率和网络是否延迟有关。
# 不能充分利用客户端计算机的资源:只能通过有限的HTML语言来呈现用户界面,没有利用客户端计算机的计算处理能力。只能利用浏览器的打印功能来打印资料,不适用于企业的报表打印。
# 网络传输量大:由于客户端不能保存状态数据,因此必须在客户端和服务器之间传输用户界面内容以及所需的数据。
# 安全性较低。对于服务器来说可以通过防火墙软件来过滤数据,因为所有传输内容都是基于HTTP端口。但很难对数据进行加密和签名以保证在传输过程中的完整性。(HTTPS似乎并不能解决问题)
# 适合电子商务或不要求严格控制客户端的应用程序。
丰富型客户端应用程序的优缺点:
# 可以离线工作:前提是本地必须有缓存数据的能力,这涉及到与服务器数据同步的问题。
# 充分利用客户端计算机的资源:可以为用户提供丰富的界面元素,可以存取本机磁盘与本机应用程序接口 (API),执行速度较快。
# 网络传输量较小:只需在客户端和服务器之间传输数据。
# 安全性较高。可以方便的在客户端和服务器执行加密和解密操作,同时也可以通过Web Service来消除传统的应用程序诸如防火墙和HTTP的障碍。
# 安装、部署和维护工作较为繁琐:对客户端计算机在操作平台和附加软件上有一定的限制和要求。
# 适合企业内部应用程序。
目 前,还有一种不是基于浏览器的瘦客户端应用程序。通过诸如Microsoft Terminal Server、Citrix System的Independent Computing Architecture和MetaFrame产品这样的技术和工具把它们提供给用户。这些类型的解决方案可以让用户运用胖客户端的经验,并通过集中的软 件安装和维护来进行管理,这对公司来说是很划算的。这种模式的问题是,当应用程序没有连接到应用服务器时,用户就不能用了。客户端不进行真正的处理,处理 完全依赖应用程序服务器,这种情况会导致服务器性能问题,并消耗桌面计算机的CPU周期。
总之,相对于丰富型客户端,以浏览器为基础的模型对于信息科技 (IT) 系统管理员而言是非常棒的模型,但对于开发人员与使用者而言却有很多不足的地方。
Microsoft .NET Framework 将满足这三方的需求。它的智能型客户端应用程序模型结合了丰富型客户端模型的强大功能与弹性,还有以浏览器为基础之模型的容易部署与稳定性。
2、客户端应用程序还应具备的其他功能
对于企业应用系统来说,客户端应用程序作为整个系统与用户的交互界面,还需要考虑以下功能:
# 安全性:这里的安全性主要是指应用层次的安全性,主要通过用户权限、角色分配来实现。对于客户端应用程序来说,通常需要提供一个登录窗体或登录页面来完成 用户身份的认证。对安全性要求较高的应用系统可能还需要通过公共密钥基础设施(PKI)为应用提供可靠的安全服务,客户端则附加了加密签名模块。
# 可扩展性:客户端应用程序应该具有包含各种不同类型的应用模块的能力,同时又能在使用过程中的不断的增加应用来完善整个系统。因此可以将客户端应用程序设计为一个容器,而各个应用模块作为组件由容器进行动态加载。
# 可配置性:由于用户权限和用户喜好的不同,客户端加载的应用组件以及呈现的表现形式都因人而异。需要由应用系统将用户权限的范围以及用户的个性化配置信息传递给客户端,由客户端应用程序来解释并呈现相应的用户界面。
定义
综上所述,我们对智能客户端应用模型下一个概括性的定义:一个可扩展的能集成不同应用的桌面应用程序,并具有以下特征:
# 无接触部署:安装时只要将一个主程序文件下载到本地,直接运行即可,无须改变注册表或共享的系统组件,其他应用组件将在第一次运行时自动下载。
# 自动更新:只需将新版本的程序发布在服务器上,由客户端自动发现最新版本的程序和应用组件,并自动下载和更新。
# 离线运用:允许脱离服务器时,利用本地的客户端程序和应用组件进行工作。
# 动态加载应用组件:应用软件开发商可根据企业应用系统的公共接口进行开发,然后将应用组件发布在企业的服务器上,客户端应用程序将自动发现并加载该应用组件。
# 个性化用户界面:用户可根据喜好自行设置客户端应用程序,配置信息将被保存到服务器上。
以下我们将以.net平台WinForms应用程序为例,对上述特征进行详细描述。

.net平台WinForms应用程序举例
一、系统需求
1、客户端软件要求
任何支持 .NET Framework 的操作系统
已安装 SP1 的 .NET Framework
Internet Explorer 5.0.1 或更高版本
2、服务器软件要求
自动发布、更新服务器:Microsoft .NET Framework、MDAC2.7、IIS
二、运行方式
客户端应用程序有两种运行方式,不同的运行方式将直接影响以后的程序集发布和更新,以下将详细解释:
1、网络运行
.NET Framework 安装提供了一个挂接 Internet Explorer 5.01 和更高版本以侦听所请求的 .NET 程序集的机制。在请求期间,可执行程序被下载到磁盘上称为程序集下载缓存的位置(Windows2000下为:C:/Documents and Settings/Administrator /Local Settings/Application Data/assembly下的某个子目录中),同时该程序集本身以及它引用的其他相关程序集也被下载到本地IE缓存中(Windows2000下 为:C:/Documents and Settings/Administrator/Local Settings/Temporary Internet Files)。然后,名为 IEExec 的进程在具有有限安全设置的环境中启动该应用程序。例如:您可以在IE的地址栏中输入一个已发布在web服务器上的.net可执行程序 (http://SmartClient/MyApplication.Exe),IE并不会像其他文件一样提示您另存为,而是直接执行该程序。
通 过这种方式运行的应用程序拥有非常有限的安全设置(Internet权限集),该权限集中的权限包括:安全性、文件对话框、正在打印、独立存储文件、用户 界面。独立存储文件允许您的应用程序保存一些数据(Windows2000下为:C:/Documents and Settings/Administrator.TOMATO/Local Settings/Application Data/IsolatedStorage下的某个子目录中,默认存储空间大小为10MB),您可以通过 System.IO.IsolatedStorage命名空间中的类来保存数据而不会抛出安全异常。
举例: //按用户、域、程序集获取独立存储区 IsolatedStorageFile isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Domain | IsolatedStorageScope.Assembly, null, null); //创建目录 isoStore.CreateDirectory("TestDir"); //创建文件 IsolatedStorageFileStream isoStream1 = new IsolatedStorageFileStream ("TestDir//test.txt", FileMode.Create, isoStore); //写入文件 StreamWriter writer = null; writer = new StreamWriter(isoStream1); writer.WriteLine("Hello Isolated Storage"); writer.Close(); isoStream1.Close();
为 了让你的智能应用程序运转,你需要改变一些客户端的安全设置,实质上就是通知客户端运行时间相信你的应用程序。一种方法就是将带有你的程序集的站点添加到 IE中可信任站点清单中,然后用安装在你的管理工具目录下的Microsoft .NET Framework Configuration工具来修改.NET Framework安全设置。打开Framework Configuration工具,选择运行库安全策略,然后选择调整安全区域。对于受信任站点中指定的所有站点,将信任级别调整到完全信任。作为选择,你 也可以用Framework Configuaration工具来修改安全策略,使它信任你的应用程序的个别程序集。右击运行库安全策略,选择提高程序集的信任级别。
另 一个可选择的方法就是用代码组,用Framework Configuration工具来帮助你提高应用系统的程序集的安全设置。你需要让所有运用你的应用程序的桌面用户做这种改变。为了帮助完成该任 务,Framework Configuration工具可以创建一个包含安全策略的Microsoft Installer (MSI)部署包。MSI安装了应用程序加载器来分布你的应用程序需要的安全策略和加载器装配。右击运行库安全策略,选择创建部署包。
在 网络运行中,自动更新是依靠IE的缓存机制来完成的。即当您需要下载并运行一个应用程序时,IE将向Web服务器发送一个HTTP请求,该请求将获取服务 器上该程序的最新更新日期,如果该日期大于本地缓存的程序的日期或者本地缓存中不存在该程序,则从服务器上下载,否则直接使用本地缓存的程序。因此对 于.net本身所具有的版本机制而言,不能作为版本更新的依据,只有在某个程序集文件引用另外一个程序集时,才会由.net运行时依据自身的版本机制判断 版本号。
注意事项:
# 这种运行方式通常需要在运行前先设置用户的安全策略。
# 加载应用组件时,需要一个完整的url地址。
# 如果应用程序集中需要调用Web Service,该Web Service所在的服务器地址只能是最初下载程序集的服务器,可以构造一个重定向来解决该问题。
# 某些文件可能不能通过自动更新机制来完成版本更新,如:.Config应用程序的配置文件。
# 如果某些应用程序集文件的版本之间存在着某些关联性,则在某些情况下(如:网络突然中断)可能会出现不能正确加载并导致客户端应用程序出错的问题。
# 如果用户清空IE的缓存,则客户端应用程序将不能离线工作。
2、本地运行
顾名思义,这种运行方式客户端应用程序和其他应用组件并不在IE缓存和.net下载缓存中运行,需要用户首先下载客户端程序集并保存到一个本地目录下,然后运行。这样客户端应用程序以及其他应用组件就拥有了所有的本地安全权限。
虽 然不涉及安全性问题,但应用组件以及程序的自动更新如何实现?这就需要一个单独的组件来完成这些任务。通过该链接地址可以下载一个非常完善且支持扩展的自 动更新组件(http://www.gotdotnet.com/team/windowsforms/DotNetUpdater.zip),该打包文 件中提供了源码、一些例子以及文档。该更新组件使用HTTP-DAV技术来完成文件在服务器和客户端之间的传输,因此对Web服务器有一定的限 制,IIS5.0和新版本的Apache支持该功能。具体使用方式请参见内含文档。
注意事项:
# 由于需要自己来实现更新和下载功能,所以会增加一定的工作量。(就是使用第三方的更新组件,也需要对其进行完善以满足自己的要求)
# 基本解决了网络运行的缺点,但需要每次更新时重新下载所有的文件(如果采用增量更新的话,某些情况下会出现某个版本的文件被遗漏的问题),会增加网络流量。
# 应该在后台线程中执行更新和下载,不影响用户的正常操作。
三、创建程序
客户端应用程序的执行步骤:
1、 登录系统。通过输入用户名和密码或其他任何方式进行系统登录,通常需要一个登录窗体。(该步操作需要客户端应用程序在线执行,因此可以考虑在窗体上提供一 个离线工作的按钮,然后直接使用上次在线登录时服务器传递的配置文件副本。这样做会引发一些安全性问题和客户端数据和服务器数据同步的问题。)
2、 由应用服务器验证通过以后,根据系统设定的用户权限获取用户可以加载的应用组件及其相应的配置信息,同时从数据库中获取用户的个性化配置信息,将这些配置信息组合成一个XML配置文件流传递给客户端应用程序。该步操作通常由一个Web服务调用来执行。
3、 客户端程序解析该配置文件流,根据个性化配置信息设置主窗体样式,根据应用组件配置信息加载相应的程序集文件。保存该配置文件以备离线工作时使用。
4、 (本地运行方式)启动后台更新线程,该线程定时搜索更新服务器上是否有最新版本的程序,如果有则自动下载,并提示用户更新程序。
5、 用户通过点击相应的应用组件的按钮或菜单,在客户端主窗体中将显示应用组件自身携带的用户界面。
客户端应用程序基本包含了登录窗体、主窗体和应用组件加载器。加载器内有一个应用组件集合类,同时还提供一些方法用于主程序和应用组件之间以及不同应用组件之间的通讯,某些应用组件是作为公共组件为其他应用提供数据,因此应该通过统一的方法来获取数据。
注意事项和建议:
1、对于网络运行方式,程序集在动态加载的过程从服务器被下载到本地缓存中,而本地运行方式的下载过程由更新组件执行,加载过程则客户端主窗体执行。
在.net中动态加载程序集的方法:
 using System.Reflection; //网络运行方式,加载的程序集路径为服务器上的url地址 assemblyFileName = "http://SmartClient/AppComponent1.dll"; //本地运行方式,加载的程序集路径为本地的目录路径 assemblyFileName = @"c:/SmartClient/AppComponent1.dll"; Assembly asm = Assembly.LoadFrom(assemblyFileName);
2、客户端应用程序的主窗体作为应用组件的容器,可以通过两种方法来显示应用组件的用户界面:
第一种方式是将客户端应用程序设计为多文档界面(MDI)应用程序,每个应用组件都包含有各自的启动主窗体,客户端应用程序执行每个应用组件时,将构造并显示该组件的启动窗体类型。MDI 应用程序中常有包含子菜单的”窗口”菜单项,用于在主窗口或应用之间进行切换。
另 一种方式是在客户端应用程序的主窗体界面中预留一块应用界面区域,而每个应用组件作为一个用户控件在执行时被添加到应用界面区域的控件数组中。应用界面区 域通常是一个面板(Pane)控件,通过更改用户控件的Z顺序在各个应用之间进行切换(Control. BringToFront())。
3、 在客户端应用程序根据配置信息加载相应的应用组件时,同时需要在客户端主窗体的工具栏或菜单上创建对应的按钮和菜单项,并添加事件以便于用户操作。客户端 应用程序和应用组件之间应尽量保持独立性,减少关联,以体现客户端应用程序结构清晰和简单。可以通过.net中的回调(事件委托)等方法实现这一点。
4、.net由于缓存的原因,在加载程序集时会出现以下问题:
 //第一次加载 Assembly asm = Assembly.LoadFrom (@"c:/SmartClient/AppComponent1.dll"); Activator.CreateInstanceFrom(asm.GetType("AppComponent1.MainForm")); //第二次加载一个不同目录下的文件名称相同但内容不同的程序集 Assembly asm = Assembly.LoadFrom (@"c:/test/AppComponent1.dll"); Activator.CreateInstanceFrom(asm.GetType("AppComponent1.MainForm"));
第二次加载后,没有正确的构造出不同目录下的程序集中的窗体类型,而是使用了缓存中的第一次加载时的构造类型。
为了解决上述问题,必须在不同的应用程序域(Domain)中构造这两个类型实例。
//第二次加载一个不同目录下的文件名称相同但内容不同的程序集
AppDomain newDomain = AppDomain.CreateDomain(“newDomain”);
newDomain.CreateInstanceFromAndUnwrap(@”c:/test/AppComponent1.dll”, “AppComponent1.MainForm”);
使 用此方法时需要注意,如果您构造的类型实例包含一个带参数的构造函数,则这些参数对象必须是可跨应用程序域访问的对象,即是可序列化的对象。可以通过继承 MarshalByRefObject对象或使用[Serializable]特性标记类,同时设置远程对象的生存期策略。
示例:
public class ParameterClass : MarshalByRefObject { public override Object InitializeLifetimeService() { //定义远程处理生存期服务所使用的生存期租约对象 ILease lease = (ILease)base.InitializeLifetimeService(); if (lease.CurrentState == LeaseState.Initial) { //设置租约的初始时间。该租约将永远不会到期,并且与之关联的对象将具有无限长的生存期。 lease.InitialLeaseTime =TimeSpan.Zero; } } }
四、发布程序
1、在发布和更新服务器上设置一个虚拟目录(如:SmartClient)将应用组件的所有文件放置在相应的目录下。
虚拟目录的设置属性中必须包括:读取和目录浏览权限,执行许可为纯脚本。
对 于本地运行方式,需要根据客户端应用程序的版本号设置多个目录,每个目录存放不同版本的所有文件。(也可以在新版本目录下只存放更新了的文件,但需要修改 更新组件。当用户更新版本时,必须下载大于当前版本的所有新版本目录下的文件,以防止某些情况下会出现某个版本的文件被遗漏的问题)。
在本地运行方式中,还需要在根目录下维护一个更新版本清单文件,因为它允许用户以老版本客户端应用程序登录。
2、发布一个Web Service。该Web服务主要完成以下任务:
# 执行客户端用户的身份验证
# 从整个应用系统的安全模块中获取该用户允许执行的应用组件的清单,包含应用组件的位置、文件名称和应用组件本身所需的配置信息。(在本地运行方式下,该清单还和版本有关)
# 从数据库中获取该用户的个性化配置信息,和上一步获取的信息合并为一个XML文件流。
# 将该配置文件流作为返回参数返回给客户端应用程序。
3、将客户端应用程序放置在虚拟根目录下作为应用组件程序集的加载器,同时在企业内部网站上增加一个指向客户端应用程序的链接。对于网络运行方式,还将提供一个安全策略部署包(MSI)文件的链接。
用户第一次运行时的执行步骤
网络运行方式:
# 下载并安装安全策略部署包
# 通过点击客户端应用程序的链接,下载并执行客户端应用程序
# 将该链接添加到收藏夹中或建立桌面快捷方式,以便于运行
本地运行方式:
# 不直接点击客户端应用程序的链接,将此链接另存到用户选择的目录下
# 在资源管理器中执行客户端应用程序
# 在桌面上建立可执行文件的快捷方式,以便于运行
五、更新程序
网络运行方式的更新步骤较为简单,只须将更新了的文件直接覆盖老文件即可。其缺点是某些程序集附带的引用文件不能自动下载新版本,只有清空客户端的IE缓存后才能实现更新。
本地运行方式的更新步骤如下:
# 在服务器虚拟根目录下新建一个以新版本号命名的目录,增量更新只需将更新了的文件复制到该目录下,否则需要将所有文件都复制到目录。
# 修改保存所有版本清单的文件,并指明当前活动的最新版本号,用于和客户端进行版本比较。
六、扩展应用组件
为了实现企业应用系统的可扩展性,通过接口定义,可以使应用开发商通过实现这些接口将自己的应用组件方便的添加到客户端应用程序中。以下介绍一些接口中主要包含的方法和属性。
1、 应用组件名称属性:用于标明该应用组件功能的注释性文本。
2、 应用组件在客户端应用程序上的各个界面元素属性:指客户端应用程序上的工具栏、菜单栏、下拉菜单项和列表框等用于标志该应用组件的控件上,需要应用组件提供的文本、图片、图标、菜单、事件处理方法等。
3、 初始化方法:在客户端应用程序加载应用组件时,首先执行的方法。在加载应用组件时,对于由服务器传来的配置文件中包含的各个应用组件自身的配置信息,客户端应用程序并不解析,而是将该配置信息在初始化应用组件时作为参数传递给应用组件,增加了灵活性。
4、 获取应用组件加载器方法:获取应用组件加载器后,可以通过其中包含的公共方法获取一些应用所需的共享数据。
5、 获取应用组件启动控件的方法:启动控件可以是窗体或用户控件。
6、 关闭应用组件的方法(可选):当客户端应用程序被关闭时,执行该方法。应用组件本身根据自身情况决定是否可以立即关闭,同时立即释放组件所占用的系统资源。对于MDI应用程序,可以在启动窗体的关闭事件中处理而不需要该方法。
客户端应用程序内的应用组件加载器可以通过枚举组件程序集内的类型来判断该组件是否符合借口定义。同时可以将多个应用组件打包在一个应用程序集中。此过程非常耗时。
示例: //根据程序集文件名称,加载程序集 Assembly asm = Assembly.LoadFrom(assemblyFile); Type[] types = asm.GetTypes(); //搜索程序集中的所有类型 foreach (Type t in types) { //指示类型是否由引用封送,支持远程处理 if (!t.IsMarshalByRef) continue; //判断该程序集的类型是否有符合应用组件接口的类型 if (Array.IndexOf(t.GetInterfaces(), pluginType) < 0) continue; }

小结
上述仅仅是对创建智能客户端应用程序作了简单的描述和示范,并对其中可能遇到的问题给与了必要的解释,希望能对您创建更为强大、灵活、便捷的应用程序提供帮助。

参考资源链接
1、Security and Versioning Models in the Windows Forms Engine Help You Create and Deploy Smart Clients
http://msdn.microsoft.com/msdnmag/issues/02/07/NetSmartClients/default.aspx
2、Smart Client Application Model and the .NET Framework
http://msdn.microsoft.com/netframework/techinfo/articles/smartclient/default.asp
3、State Sanity Using Smart Clients
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnforms/html/Winforms05202002.asp
4、Increasing Permissions for Web-Deployed Windows Forms Applications
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnforms/html/winforms11122002.asp
5、.NET Application Updater Component

[翻译] 让Windows 2000/XP中的任意窗口透明起来

Windows 2000/XP中的任意窗口透明起来

简介

  已经有很多的文章展示了如何通过使用新的系统函数在Windows 2000或Windows XP中建立透明窗口的应用程序,本文在此基础上为您展现了一种可以让任意应用程序窗口透明起来的方法,哪怕您根本没有那个应用程序的源程序。

  使用本文介绍的“WinTrans”程序,您只需把程序中的“魔棒”(程序左上角的那个图标)拖曳到另一个正在运行的程序的标题栏上就可以使它的窗口变得透明。您还可以通过拖到滑动杆来调节透明度。“WinTrans”的界面很像SPY程序的界面,它演示了如何使用Win32 API函数来捕获一个位于鼠标光标下的窗口并获取它的窗口类、窗口标题等信息。

  当您需要在一个最大化的窗口中工作而同时又需要查看另外一个在后台运行的程序的状态时,您会发现“WinTrans”程序是一个很实用的程序。

背景

  在Windows 2000和Windows XP中,User32.dll中添加了一个新的函数,名字是SetLayeredWindowAttributes。如果要在应用程序中使用这个函数,需要在创建窗口时为窗口的window style设置WS_EX_LAYERED(0x00080000)位,也可以在创建窗口后用SetWindowLong函数添加这个位。一旦这个标志位被设定了,我们就可以通过调用SetLayeredWindowsAttributes函数,并把窗口的句柄传给它来使得窗口或窗口上特定的颜色变得透明。这个函数的参数如下:

  • HWND hWnd: 窗口的名柄
  • COLORREF col: 希望变透明的颜色
  • BYTE bAlpha: 如果这个值设为0,窗口会变得完全透明。如果设为255,窗口会变得完全不透明。
  • DWORD dwFlags: 如果这个标志设为1,只有col指定的颜色会变得透明。如果这个标志设为2,则整个窗口会按bAlpha指定的程度变得透明。

代码解释

  首先,在WinTransDlg.h中的主对话框类中加下如下的成员变量。

bool m_bTracking;   //  当鼠标被捕捉时会置为true

HWND m_hCurrWnd;    //  鼠标最后一次指向的窗口的句柄

HCURSOR m_hCursor;  //  魔棒光标

  我们还定义了一个指向SetLayeredWindowAttributes函数的指针。这个函数位于User32.dll中。

//  全局定义

typedef BOOL (WINAPI *lpfn) (HWND hWnd, COLORREF cr,

              BYTE bAlpha, DWORD dwFlags);

lpfn g_pSetLayeredWindowAttributes;

  在OnInitDialog消息响应函数中,我们获取了SetLayeredWindowAttributes函数的地址并把它保存在g_pSetLayeredWindowAttributes变量中。同时,我们还加载了魔棒光标并把它的句柄放在m_hCursor中

BOOL CWinTransDlg::OnInitDialog()

{

    ….

    //  获取User32.dllSetLayeredWindowAttributes 函数的地址

    HMODULE hUser32 = GetModuleHandle(_T(“USER32.DLL”));

    g_pSetLayeredWindowAttributes = (lpfn)GetProcAddress(hUser32,

               “SetLayeredWindowAttributes”);

    if (g_pSetLayeredWindowAttributes == NULL)

        AfxMessageBox (

            “Layering is not supported in this version of Windows”,

             MB_ICONEXCLAMATION);

    //  加载魔棒光标

    HINSTANCE hInstResource = AfxFindResourceHandle(

         MAKEINTRESOURCE(IDC_WAND), RT_GROUP_CURSOR);

    m_hCursor = ::LoadCursor( hInstResource, MAKEINTRESOURCE(IDC_WAND) );

    …

}

  下面定义WM_LBUTTONDOWN, WM_LBUTTONUP和WM_MOUSEMOVE消息的响应函数。WM_LBUTTONDOWN的响应函数如下:

void CWinTransDlg::OnLButtonDown(UINT nFlags, CPoint point)

{

    …

    SetCapture();      // 使鼠标消息发回本窗口

    m_hCurrWnd = NULL; // 当前没有窗口被设成透明的

    m_bTracking = true;     // 设置鼠标被捕获的标志

    ::SetCursor(m_hCursor); // 把鼠标光标变成魔棒光标

    …

}

  对于处理鼠标移动消息的响应代码如下

void CWinTransDlg::OnMouseMove(UINT nFlags, CPoint point)

{

    …

    if (m_bTracking)

    {

        …

        //  把鼠标坐标转换为屏幕坐标

        ClientToScreen(&point);

        …

        //  获取鼠标位置的光标

        m_hCurrWnd = ::WindowFromPoint(point);

        …

        // 显示窗口的详细情况,包括窗口类、标题等

        …

    }

    …

}

  这样就使得只要在主窗口中按下鼠标按钮不松开就可以使鼠标光标变为魔棒光标,并且光标下的窗口的一些信息就会显示在WinTrans程序的对话框中。

  当鼠标按钮松开时,WM_LBUTTONUP消息的响应函数就会被调用。

void CWinTransDlg::OnLButtonUp(UINT nFlags, CPoint point)

{

    …

    //  停止捕获鼠标

    ReleaseCapture();

    m_bTracking = false;

    //  如果鼠标光标所指的窗口不是本应用程序本身

    //  就设置它的Layer样式位并按滑动杆的设置值来设置窗口的Alpha

    if (g_pSetLayeredWindowAttributes && m_hCurrWnd != m_hWnd)

    {

        ::SetWindowLong(m_hCurrWnd, GWL_EXSTYLE,

                        GetWindowLong(m_hCurrWnd,

                        GWL_EXSTYLE) ^ WS_EX_LAYERED);

        g_pSetLayeredWindowAttributes(m_hCurrWnd, 0,

                        (BYTE)m_slider.GetPos(), LWA_ALPHA);

        ::RedrawWindow(m_hCurrWnd, NULL, NULL,

                       RDW_ERASE | RDW_INVALIDATE |

                       RDW_FRAME | RDW_ALLCHILDREN);

    }

    …

}

其它值得关注的地方

  当前这个程序只有在魔棒指向窗口的标题栏或一个基于对话框的程序窗口体上才能有效的工作。举例来说:如果你把魔棒指向记事本的窗口体是没有用的。

  如果要去掉窗口透明的效果,您只需再次把魔棒拖曳到相应的窗口上即可。因为在OnLButtonUp中,我们是切换WS_EX_LAYERED标志位的设置状态的,透明效果也会进行相应的切换。

  TransWand程序不能在命令提示行窗口上正确的工作。

本文相关源程序下载:

原始下载地址 (12K)(要在该网站注册后方可下载)

国内镜象 (12K)

Delphi中两个BUG的分析与修复

Delphi中两个BUG的分析与修复

  在使用Delphi 7进行三层数据库开发时,遇到了两个小问题,通过反复试验,终于找出了Delphi 7中的两个小BUG并进行了修复(好像Delphi 6中也有相同的BUG),撰写此文与大家一起分享成功的喜悦。我也是初学Delphi,文中一定存在不少说的不对的地方,还请各位朋友多多指正。

  BUG1.传参时中文被截断的问题:

  BUG再现的方法:

  后台用SQL Server 2000,里面有一个XsHeTong表用于试验,您可以根据您的实际情况进行调整。

  先创建一个数据服务器:新建项目,创建一个远程数据模块,上面放置 ADOConnection、ADODataSet、DataSetProvider各一,并做好相应设置,其中ADODataSet的 ComamndText留空,并把它的Option中的poAllowCommandText设置为True。编译运行。

  再创建客户端程序:新建项目,在窗体上放置DCOMConnection, 连上前面上创建的数据服务器,再放置一个ClientDataSet,把它的连接设成这里的DCOMConnection,并设置它的 ProviderName为上面的服务器上的DataSetProvider的名字。最后放置DataSource和DBGrid各一并作相应设置用于查 看结果,再放置一Button用于测试。

  在Button的OnClick中写下类似于下面的代码(这里我用了XsHeTong的表和它的两个字段HTH(char 15)、GCMC(varchar 100),您可以根据你的实际测试情况进行调整):

  with ClientDataSet1 do
begin
Close;
CommandText := ‘Insert Into XsHeTong(HTH, GCMC) values(:HTH,:GCMC)’;
Params[0].AsString := ‘12345’;
Params[1].AsString := ‘会截断的中文字’;
Execute;
Close;
CommandText := ‘Select * from XsHeTong’;
Open;
end;

  运行程序,点击按钮,看到记录被插入了,可惜结果并不正确,“会截断的中文字”变成了“会截断”,但没有中文的“12345”倒是正确的插入了。

  BUG分析与修复:

  为了对照起见,我试着直接用一个ADOConnection和ADOCommand、ADOTable进行C/S构架测试,结果是正确的,中文字不会被切断。这说明了此BUG只在三层构架上出现。

   用SQL Server事件探查器探查提交到SQL Server上运行的语句,发现两层构架与三层构架的情况有以下不同:

  两层构架:
exec sp_executesql N’Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)’, N’@P1 varchar(15),@P2 varchar(100)’, ‘12345’, ‘会截断的中文字’

  三层构架:
  exec sp_executesql N’Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)’, N’@P1 varchar(5),@P2 varchar(7)’, ‘12345’, ‘会截断

  显然,两层构架时,参数的长度是按实际库结构传的,三层构架时,参数长度是按实际参数的字符串长度传的,而实际字符串长度又似乎是算错了,没有把一个中文当两个字符长度处理。

  没有办法只好进行跟踪调试,为了调试Delphi的VCL库,需要在工程选项的“Compiler Options”中选上“Use Debug DCUs”。

  先跟踪客户端程序,ClientDataSet1.Execute后,先后 经历了TCustomClientDataSet.Exectue、TCustomeClientDataSet.PackageParams、 TCustomClientDataSet.DoExecute等一系列函数,一直到 AppServer.AS_Execute(ProviderName, CommandText, Params, OwnerData); 把请求提交到服务器均没有什么异常情况,看来问题出在服务器端。

  对服务器进行跟踪,反复试验后,我把重点落在了 TCustomADODataSet.PSSetCommandText函数身上,经过反复细致的跟踪,目标越来越精 确:TCustomADODataSet.PSSetParams、TParameter.Assign、TParameter.SetValue、 VarDataSize。终于找到了BUG的源头:VarDataSize函数,下面是它的代码:

  function VarDataSize(const Value: OleVariant): Integer;
begin
if VarIsNull(Value) then
Result := -1
else if VarIsArray(Value) then
Result := VarArrayHighBound(Value, 1) + 1
else if TVarData(Value).VType = varOleStr then
begin
Result := Length(PWideString(@TVarData(Value).VOleStr)^); //出问题的行
if Result = 0 then
Result := -1;
end
else
Result := SizeOf(OleVariant);
end;

  就是在这个函数中计算实参的长度的,它把Value中的值取出地址,并把它作为一个WideString的指针去求字符串长度,结果就导致了“会截断的中文字”这个字符串的长度变成了7,而不是14。

  问题找到了,解决起来也就不困难了,只要简单的把
Result := Length(PWideString(@TVarData(Value).VOleStr)^); //出问题的行
改成
Result := Length(PAnsiString(@TVarData(Value).VOleStr)^); //没问题了
就可以了。

  但是这样就会导致求英文字符串的长度时长度被加倍了,所以也可以把这一行改成:
Result := Length(Value);

  这样,不管是中文还是英文还是中英混合的字符串就都可求得正确的长度了。这就我至今仍百思不解的问题,为什么Borland要绕个圈子通过指针去求参数值的长度呢?哪位朋友知道的话还请给我解释一下,非常感谢!

  有些朋友可能会有疑问,为什么在不通过三层构架来做的时候不产生这个字符串被截断的问题呢?答案并不复杂,在直接通过ADOCommand来向SQL Server发送命令时,它是按表结构来决定参数长度的。它会先向SQL Server发一条

  SET FMTONLY ON select HTH,GCMC from XsHeTong SET FMTONLY OFF

  来获取表结构。而在三层构架下,TCustomADODataSet内部虽然也是用TADOCommand对象来发命令,但它却在取得表结构的后,并不用这个值来作为传参长度,而是重新去按实际参数来计算长度,结果就导致了错误。

  BUG2.ClientDataSet的Lookup字段的问题:

  BUG再现的方法:

  新建工程,在上面放置两个ClientDataSet,分别为cds1和cds2,它的数据来源任意,其中cds1为主数据集,在里面增加一个新的Lookup字段,这个Lookup字段根据cds1中的一个字符型的字段值到cds2中找出对应值来。

  运行程序,一般来说是正常的,但是一旦cds1的被Lookup字段中的值出现了一个单引号”‘”(您可以修改或新增一条记录,输入单引号试试),立即会导致出错: Unterminated string constant(未结束的字符串常量)。

  BUG分析与修复:

  这个BUG的产生原因要比上一个明显得多,一定是没有正确处理单引号带来的副作用引起的。

  同样的,我们来跟踪VCL的源码:

  运行程序,出错时打开Call Stack窗口(在View->Debug Windows)菜单中,查看函数调用情况,前面的一些调用是显而易见的,没有问题,我们从跟Lookup有关的地方开始查原因,第一个与Lookup有 关的函数调用是TField.CalcLookupValue,我们在这个函数中设置断点,重新运行程序,中断下来后,进行单步调试。

  TCustomClientDataSet.Lookup->TCustomClientDataSet.LocateRecord

  经过上面的几次函数调用,很快的,我们就把目标定在了 LocateRecord过程中,在这个过程中,它根据Lookup字段的设置情况,生成相应的过滤条件,然后到目标数据集中把对应的值找到,错就错在过 滤条件的生成上了。比如,我们要按cds1中Cust字段(假设是001)的值到cds2中按CustID字段值找到对应的CustName字段值。那生 成的条件就应该是[CustID] = ‘001’,但如果Cust的值是aa’bb,按生成的条件就会变成[CustID] = ‘aa’bb’,显然导致了一个未结束的字符串常量。

  通常我们解决单引号中又出现单引号的情况,只需把引号中的引号写两就行了,这里也是一样,只要让生成的条件变成[CustID] = ‘aa”bb’就不会出错了。所以可以这样修改源代码:

  在LocateRecord过程中找到下面的代码:

  ftString, ftFixedChar, ftWideString, ftGUID
if (i = Fields.Count – 1) and (loPartialKey in Options) then
ValStr := Format(”’%s*”’,[VarToStr(Value)]) else
ValStr := Format(”’%s”’,[VarToStr(Value)]);

  改成:

  ftString, ftFixedChar, ftWideString, ftGUID:
if (i = Fields.Count – 1) and (loPartialKey in Options) then
ValStr := Format(”’%s*”’,[ StringReplace(VarToStr(Value),””,”””,[rfReplaceAll])])
else
ValStr := Format(”’%s”’,[ StringReplace(VarToStr(Value),””,”””,[rfReplaceAll])]);

  也就是在生成过滤条件字符串时把条件的过滤值中的单引号全部一个变两。

  为了确保这样修改的正确性,我查看了TCustomADODataSet中 的对应的LocateRecord过程(在用TADODataSet中的Lookup字段时不会因单引号出错,只在用 TCustomClientDataSet时有这样的情况),它的处理方法与TCustomClientDataSet稍有不同,它是通过 GetFilterStr函数来构造过滤条件的,但在GetFilterStr中,它正确处理了单引号的问题。所以这样来看,没有在 TCustomClientDataSet的LocateRecord中正确处理单引号的问题,确实是Borland一个不大不小的疏漏。

谁动了我的指针?

谁动了我的指针?

译者序:
本文介绍了一种在调试过程中寻找悬挂指针(野指针)的方法,这种方法是通过对newdelete运算符的重载来实现的。
这种方法不是完美的,它是以调试期的内存泄露为代价来实现的,因为文中出现的代码是绝不能出现在一个最终发布的软件产品中的,只能在调试时使用。
在VC中,在调试环境下,可以简单的通过把new替换成DEBUG_NEW来实现功能更强更方便的指针检测,详情可参考MSDN。DEBUG_NEW的实现思路与本文有相通的地方,因此文章中介绍的方法虽然不是最佳的,但还算实用,更重要的是,它提供给我们一种新的思路。

简介:
前几天发生了这样一件事,我正在调试一个程序,这个程序用了一大堆乱七八糟的指针来处理一个 链表,最终在一个指向链表结点的指针上出了问题。我们预计它应当指向的是一个虚基类的对象。我想到第一个问题是:指针所指的地方真的有一个对象吗?出问题 的指针值可以被4整除,并且不是NULL的,所以可以断定它曾经是一个有效的指针。通 过使用Visual Studio的内存查看窗口(View->Debug Windows->Memory)我们发现这个指针所指的数据是FE EE FE EE FE EE …这通常意味着内存是曾经是被分配了的,但现在却处于一种未分配的状态。不知是谁、在什么地方把我的指针所指的内存区域给释放掉了。我想要找出一种方 案来查出我的数据到底是怎么会被释放的。

背景:
我最终通过重载了newdelete运算符找到了我丢失的数据。当一个函数被调用时,参数会首先被压到栈上后,然后返回地址也会被压到栈上。我们可以在newdelete运算符的函数中把这些信息从栈上提取出来,帮助我们调试程序。

代码:
在经历了几次错误的猜测后,我决定求助于重载newdelete运算符来帮我找到我的指针所指向的数据。下面的new运算符的实现把返回地址从栈上提了出来。这个返回地址位于传递过来的参数和第一个局部变量的地址之间。编译器的设置、调用函数的方法、计算机的体系结构都会引响到这个返回地址的实际位置,所以您在使用下面代码的时候,要根据您的实际情况做一些调整。一旦new运算符获得了返回地址,它就在将要实际分配的内存前面分配额外的16个字节的空间来存放这个返回地址和实际的分配的内存大小,并且把实际要分配的内存块首地址返回。
对于delete运算符,你可以看到,它不再释放空间。它用与new同 样的方法把返回地址提取出来,写到实际分配空间大小的后面(译者注:就是上面分配的16个字节的第9到第12个字节),在最后四个字节中填上DE AD BE EF(译者注:四个十六进制数,当成单词来看正好是dead beef,用来表示内存已释放真是很形象!),并且把剩余的空间(译者注:就是原本实际应该分配而现在应该要释放掉的空间)都填上一个重复的值。
现在,如果程序由于一个错误的指针而出错,我只需打开内存查看窗口,找到出错的指针所指的地方,再往前找16个字节。这里的值就是调用new运算符的地址,接下来四个字节就是实际分配的内存大小,第三个四个字节是调用delete运算符的地址,最后四个字节应该是DE AD BE EF。接下的实际分配过的内存内容应该是77 77 77 77。
要通过这两个返回地址在源程序中分别找到对应的newdelete, 可以这样做:首先把表示地址的四个字节的内容倒序排一下,这样才能得到真正的地址,这里因为在Intel平台上字节序是低位在前的。下一步,在源代码上右 击点击,选“Go To Diassembly”。在反汇编的窗口上的左边一栏就是机器代码对应的内存地址。按Ctrl + G或选择Edit->Go To…并输入你找到的地址之一。反汇编的窗口就将滚动到对应的newdelete的函数调用位置。要回到源程序只需再次右键单击,选择“Go To Source”。您就可以看到相应的newdelete的调用了。
现在您就可以很方便的找出您的数据是何时丢失的了。至于要找出为什么delete会被调用,就要靠您自己了。
#include <MALLOC.H>

  void * ::operator new(size_t size)
{
int stackVar;
unsigned long stackVarAddr = (unsigned long)&stackVar;
unsigned long argAddr = (unsigned long)&size;

    void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2);

    void * retAddr = * retAddrAddr;

    unsigned char *retBuffer = (unsigned char*)malloc(size + 16);

    memset(retBuffer, 0, 16);

    memcpy(retBuffer, &retAddr, sizeof(retAddr));

    memcpy(retBuffer + 4, &size, sizeof(size));

    return retBuffer + 16;
}

  void ::operator delete(void *buf)
{
int stackVar;
if(!buf)
return;

    unsigned long stackVarAddr = (unsigned long)&stackVar;
unsigned long argAddr = (unsigned long)&buf;

    void ** retAddrAddr = (void **)(stackVarAddr/2 + argAddr/2 + 2);

    void * retAddr = * retAddrAddr;

    unsigned char* buf2 = (unsigned char*)buf;

    buf2 -= 8;

    memcpy(buf2, &retAddr, sizeof(retAddr));

    size_t size;

    buf2 -= 4;

    memcpy(&size, buf2, sizeof(buf2));

    buf2 += 8;

    buf2[0] = 0xde;
buf2[1] = 0xad;
buf2[2] = 0xbe;
buf2[3] = 0xef;


buf2 += 4;

    memset(buf2, 0x7777, size);

    // deallocating destroys saved addresses, so don’t
// buf -= 16;
// free(buf);
}

其它值得关注的地方:
这段代码同样可以用于内存泄露的检测。只需修改delete运算符使它真正的去释放内存,并且在程序退出前,用__heapwalk遍历所有已分配的内存块并把调用new的地址提取出来,这就将得到一份没有被delete匹配的new调用列表。
还要注意的是:这里列出的代码只能在调试的时候去使用,如果你把它段代码放到最终的产品中,会导致程序运行时内存被大量的消耗。