一个程序只运行一个实例(或限制实例数量)通常可以采用如下方法:
1)FindWindow 之<窗口标题>
通过查找窗口标题来确定上一实例是否正在运行,不适合窗口标题动态变化的程序。
2)FindWindow 之<任务栏按纽标题>
通过查找任务栏按纽标题来确定上一实例是否正在运行,不适合按纽标题动态变化的程序(如Winamp)。通常情况下,该方法还是优先考虑,因为按纽标题是一般是固定的。
3)Window Property
将某个数据(可以是字符串或句柄)通过SetProp加入到指定窗口的property list,程序运行时枚举窗口并检查该数据是否存在来确定上一实例是否正在运行。
4)全局Atom
将某个特定字符串通过GlobalAddAtom加入全局原子表(Global Atom Table),程序运行时检查该串是否存在来确定上一实例是否正在运行。该方法有个局限,就是程序终止前必须显式调用GlobalDeleteAtom来释放atom,否则该atom不会自动释放,如果程序运行时意外终结了,那么下一个实例就无法正常执行。早期版本的realplayer就存在这个现象,不知道是不是采用了该方法。
5)Mutex/Event/Semaphore
通过互斥对象/信号量/事件等线程同步对象来确定实例是否存在,在NT下要注意权限问题(SID)。
6)DLL全局共享区域
VC下的DLL工程可以通过下面代码来建立一个进程间共享数据段:
#pragma data_seg(“.share”)
//shared for all processes that attach to the dll
DWORD dllgs_dwRunCount = 1; //一定要在这里对变量进行初始化,否则工夫白做!
#pragma data_seg()
#pragma comment(linker,”/section:.share,rws”)
导出3个函数,分别为:
DWORD IncRunCount(void); //运行计数器加1,返回计数器结果
DWORD DecRunCount(void); //运行计数器减1,返回计数器结果
DWORD GetRunCount(void); //取当前运行计数器
由于DLL全局共享段在映射到各个进程地址空间时仅会被初始化一次,并且是在首次被windows加载时,所以利用该共享段数据就能对程序实例进行可靠计数。
7)内存映射文件(File Mapping)
通过把程序实例信息(如窗口句柄、计数器等等)放置到跨进程的内存映射文件,同样可以控制程序实例运行的数量,道理与DLL全局共享区域类似。
由于内存映射文件在映射到各个进程地址空间时会被初始化,所以利用该共享段数据就能对程序实例进行可靠计数。

字符编码的使用

字符编码主要分两种:MBCS以及 Unicode。 以 C/CC++ 为例,以 char 为单位的数组使用MBCS编码(如 ASCII,GB2312,BIG5),以wchar_t 为单位的数组使用Unicode作为编码。

比如你的程序中使用:
char szTitle[] = "窗体标题“;
此时,szTitle字符串使用的的是MBCS编码,如果用户的操作系统不是中文的Windows,你的程序将无法正常显示!

所以,要换成Unicode方式:
wchar_t szTitle[] = L"窗体标题";
此时,szTitle字符串使用的的是Unicode编码,加入你的程序以unicode方式编译,在任何语言的Windows上都能显示正常。


字符编码的转换

有时候你从外部文件读进来的字符串是MBCS编码(如GB2312),而你程序里面都是统一用Unicode处理字符串,这时候要进行字符编码转换。 Windows为我们提供了很好用的API函数 MultiByteToWideChar WideCharToMultiByte 帮我们轻松实现转换。     代码如下:

GB2312 转换成 Unicode:

CODE:
wchar_t* GB2312ToUnicode(const char* szGBString)
{
           UINT nCodePage = 936; //GB2312
           int nLength=MultiByteToWideChar(nCodePage,0,szGBString,-1,NULL,0);
           wchar_t* pBuffer = new wchar_t[nLength+1];
           MultiByteToWideChar(nCodePage,0,szGBString,-1,pBuffer,nLength);
           pBuffer[nLength]=0;
           return pBuffer;
}
BIG5 转换成 Unicode:

CODE:
wchar_t* BIG5ToUnicode(const char* szBIG5String)
{
           UINT nCodePage = 950; //BIG5
           int nLength=MultiByteToWideChar(nCodePage,0,szBIG5String,-1,NULL,0);
           wchar_t* pBuffer = new wchar_t[nLength+1];
           MultiByteToWideChar(nCodePage,0,szBIG5String,-1,pBuffer,nLength);
           pBuffer[nLength]=0;
           return pBuffer;
}
Unicode 转换成 GB2312:

CODE:
char* UnicodeToGB2312(const wchar_t* szUnicodeString)
{
           UINT nCodePage = 936; //GB2312
           int nLength=WideCharToMultiByte(nCodePage,0,szUnicodeString,-1,NULL,0,NULL,NULL);
           char* pBuffer=new char[nLength+1];
           WideCharToMultiByte(nCodePage,0,szUnicodeString,-1,pBuffer,nLength,NULL,NULL);
           pBuffer[nLength]=0;
           return pBuffer;
}
Unicode 转换成 BIG5:

CODE:
char* UnicodeToBIG5(const wchar_t* szUnicodeString)
{
           UINT nCodePage = 950; //BIG5
           int nLength=WideCharToMultiByte(nCodePage,0,szUnicodeString,-1,NULL,0,NULL,NULL);
           char* pBuffer=new char[nLength+1];
           WideCharToMultiByte(nCodePage,0,szUnicodeString,-1,pBuffer,nLength,NULL,NULL);
           pBuffer[nLength]=0;
           return pBuffer;
}
繁体和简体的相互转换

利用Unicode作为媒介,还可以做出很有意思的应用。在处理中文过程中,一个经常用到的功能就是繁体和简体的互相转换。 代码如下:

繁体中文BIG5 转换成 简体中文 GB2312

CODE:
char* BIG5ToGB2312(const char* szBIG5String)
{
           LCID lcid = MAKELCID(MAKELANGID(LANG_CHINESE,SUBLANG_CHINESE_SIMPLIFIED),SORT_CHINESE_PRC);

           wchar_t* szUnicodeBuff = BIG5ToUnicode(szBIG5String);
           char* szGB2312Buff = UnicodeToGB2312(szUnicodeBuff);

           int nLength = LCMapString(lcid,LCMAP_SIMPLIFIED_CHINESE, szGB2312Buff,-1,NULL,0);
           char* pBuffer = new char[nLength + 1];
           LCMapString(0x0804,LCMAP_SIMPLIFIED_CHINESE,szGB2312Buff,-1,pBuffer,nLength);
           pBuffer[nLength] = 0;
       
           delete[] szUnicodeBuff;
           delete[] szGB2312Buff;
           return pBuffer;
}
简体中文 GB2312 转换成 繁体中文BIG5

CODE:
char* GB2312ToBIG5(const char* szGBString)
{
           LCID lcid = MAKELCID(MAKELANGID(LANG_CHINESE,SUBLANG_CHINESE_SIMPLIFIED),SORT_CHINESE_PRC);

           int nLength = LCMapString(lcid,LCMAP_TRADITIONAL_CHINESE,szGBString,-1,NULL,0);
           char* pBuffer=new char[nLength+1];
           LCMapString(lcid,LCMAP_TRADITIONAL_CHINESE,szGBString,-1,pBuffer,nLength);
           pBuffer[nLength]=0;

           wchar_t* pUnicodeBuff = GB2312ToUnicode(pBuffer);
           char* pBIG5Buff = UnicodeToBIG5(pUnicodeBuff);

           delete[] pBuffer;
           delete[] pUnicodeBuff;
           return pBIG5Buff;
}

文本文件读写的编码问题

当你的程序读一个文本文件时,如何判断文件中的字符是MBCS格式还是Unicode格式? Windows定义了一个"字节顺序标记"(Byte-order Mark)的概念:当一个txt文件的前2个字节为FF FE时,这个文件里面的字符采用Unicode编码, 如果没有字节顺序标记,就是MBCS编码。更多关于字节顺序标记的说明,请看 MSDN的官方资料

团队,究竟是什么制度的?拓展训练据说是要问这个问题的。

在训练的一个游戏上,似乎感悟到了一点东西,不敢藏掖,遂贴于此。

10个人站在两根平行放置的木板上,两根木板很重,一个人抬不起来,每个木板上隔一段距离绑一根绳子,共绑有10根绳子。十个人站在这两根木板上,左右手各自抓握对应的一组绳子,然后向前迈进。

这个游戏的关键是:

1 当十个人同时迈左脚时,必须把重心全部放到右脚上而使左脚悬空,否则通过拉绳子是拉不起来左边的这块木板的。右边的木板也同理。

2 必须有人指挥,否则,即便9个人迈了左脚,只要有一个人迈了右脚,就走不动。

3 只有十个人同时迈左脚,或者迈右脚时,才能走动。

关键就是指挥。

前三个团队都是一个人指挥十个人,我们称这种制度为“民主集中制”。他们都在预定时间完成了任务,最快的一组:1分47秒。

我们团队的制度不同,我们是十个人同时呐喊口号,指挥者反倒晾在一边,这种制度大凡类似于“自由制”。结果是,我们团队速度最快,本可以在1分30秒左右冲线。由于十个人同时呐喊,在呐喊的同时每个人就知道自己该做什么,所以,速度非常快。十个人身形绝对统一,完美,近乎完美。

但,离终点只剩一点的地方,已经1分29秒了,我们遇到了一个小Bug。剩下的事情就……

十个人试图四次同时再次呐喊,但是每次,大家虽然都喊得很统一,但就是有人的身形无法跟上。第五次,不得不由一直被晾在一边的队长重新发令,我们破线的时候已经是2分了。

很可惜。

这似乎也折射出了团队制度的一些问题。

民主集中制,制度是一切,民主商议得到的结果,经由少数人形成决议,并将这些决议形成制度。而所有人,必须按照制度进行工作,不允许任何人有特殊化。民主集中制的结果,取决于决议形成的制度,也就是一开始的参议和决策过程。这个过程只要没有问题,那么最后最起码是能完成一个结果的,只不过可能不尽善尽美,但起码是可以达到的。这是大多数企业使用的管理模式——只不过很多企业是畸形的,参议权没有团队成员的份,少数人参议,少数人决议,少数人独裁——转化成了彻头彻尾的独裁制,在这个行业里混的兄弟姐妹们可不要告诉我你没看到过这种公司。好来不及鄙视他们,因为独裁不是我们的重点,而且这些企业现在都是某些势力的宠儿,别的不说什么,我们还得顾及自己的小命不是~。

民主集中制主要的效率问题是发生在执行期,执行的效率一般都比较低。因为制度和进度表一旦形成,人固有的惰性总会不由自主地向这个进度去靠,即便人们可以自觉自发地完成得更快更好,但是,制度相当于为人们的懒惰提供了法律依据。因此会有人说“项目总会超期,即便认识到这一点也是如此……”。但你如果是单枪匹马做一个小游戏,那么你反倒发现“项目总会早于预期的时间,即便认识到这一点也是如此……”了,这大约就是民主集中制的一些影响吧。^_^

另外,在民主集中制下,团队的凝聚力仅限于参议阶段,而非执行阶段,因此,也大打折扣。但与此同时,大团队的组织才有了可能。10个人的团队可以在执行期互通有无,拚命向前赶进度,但100个人呢?1000个人呢?

而自由制,它的好处是,对于小但素质高的团队、小目标而言,执行期的效率很高,每个人都会知道自己该做什么,也会努力去完成。但是,一旦发生方向性的或者重大的分歧,重新整合的难度会加大。

个人认为,民主集中制更适合正规而项目相对中型的团队,而自由制则适合于小型项目、小而稳定的团队,也就是小作坊。

制度没有好坏,没有最好的制度,没有最坏的制度,只有最适应的制度。一定的情况下,独裁也是好的抉择。用在团队上合适,用在社会上也合适。

制度就如同工具一般,只不过这种工具切换的时候总要有些麻烦,因此有时要刻意避免制度的切换。形而上学地去讨论制度的好坏,倒不如务实地分析自己所处的情况,选择对自己最有利的制度——哪怕是“德国国家社会主义”(考虑到某些神经脆弱的网友的请求,我以后绝对不会再用“纳粹”这个词)制度,在必要的历史时期也是可以选择的。

任何制度都会有弊端,也都会有相应地优点,因此,更重要的是分析自己,而不是盲目地跟随着上个世纪7、80年代的狂徒们一起去叫嚣“XX制度就是好”“XX制度就是不好”。已经21世纪了,我们毕竟应该像个人一样地活着,要有自己的头脑,用自己的分析,获得自己的结果。

嗯,好吧,偶然想到的也就写下来了。其实对于我大凡是没有区别的,作为团队的一员,发挥自己的参议权和主观能动性,在决议之后,按照已经生成的决议完成自己该做的工作,这大凡就是我们应该付出的责任吧。虽然我可能打心眼里喜欢那种狂放不羁的自由(谁不喜欢啊!),但是,或许更需要明白的是身为一个员工,身上沉甸甸的职责!

这是我收藏的一篇文章,每每在遇到挫折的时候就拿出来读一读,总是能给我动力!

这些日子我一直在写一个实时操作系统内核,已有小成了,等写完我会全部公开,希望能够为国内IT的发展尽自己一份微薄的力量。最近看到很多学生朋友和我当年一样没有方向 ,所以把我的经历写出来与大家共勉,希望能给刚如行的朋友们一点点帮助。 一转眼我在IT行业学习工作已经七年多了,这期间我做过网页,写过MIS、数据库,应用程序,做过通信软件、硬件驱动、协议栈,到现在做操作系统内核和IC相关开发,这中间走了很多弯路,也吃了不少苦。

我上的是一个三流的高校,就连同一个城市的人多数都不知道。因为学校不好也就没 有指望能靠学校名气找一个好工作。所有的希望都寄托在自己的努力上了,大一开学前的假期我就开始了学习,记得我买的第一本书是《计算机基础DOS3.0》,大家别吓着了,其实当时已经普及了DOS6.22了,只是我在书店里看到了DOS4.0,5.0,6.0的书,以为像英语那样是第四、五、六册,记得当时到处找DOS1.0,现在想想也幸好我没有找到:)开学前我学完了PASCAL,那时既没有计算机也没有人可以请教,我连程序是什么的概念都没有, 只好死记硬背代码,然后拿纸写,我一直到大三才有了一台486,在这之前用纸写了多少程序我也记不清楚了,只知道最长的一个我拿A4大小的草稿纸写了30多页,我的C语言、C++ 、VC都是在这样的条件下入门的。所以说条件是可以克服的,希望我的经历多少给条件艰苦的同学们一点信心。第一次上机是在我姐夫的机房,我的心情激动的无与伦比,但是一上机我立刻傻了眼,他们用的是英文版的Win3.1,我的那点DOS知识都见了鬼,上机提心吊胆的一阵瞎摸,一不小心把Word弄成了全屏,怎么都还不了原,当时真是心急如焚,我以为机器被我弄坏了。第一个C语言程序,就是那个经典的HelloWorld,我调了几个星期,上机机会非常少,也没有书告诉我开发环境(TC2.0)需要设置,而且开始我都不知道有编译器,我甚至自作聪明把写好的程序扩展名从.c改成.exe,结果可想

而知。大一学完了C、X86的汇编、数据结构、C++。由于精力都花在自学上了,大一下四门课挂了彩,三类学校就是这点好,挂上一二十门也照样毕业。不过扯远点说,我那么刻苦都及不了格,可见我们国家的计算机教育有多死板。

大二准备学VC和BC,当时难以取舍,后来选了VC,不为别的,只为书店里两本书,VC 那本便宜6块钱。我的努力在班上无人能及,学的日夜不分,大三有了计算机后更是如此, 很多次父亲半夜教训我说我不要命了,我一直觉得自己基础差,记忆又不行,条件也不好 ,所以觉得只有多花点时间才能赶上别人。居然后来有许多朋友说我有学计算机的天赋, 让我哭笑不得。我用的是486,16M内存,1G硬盘,当时同学们的配置都是P166MMX,我安装 一个Windows NT4.0需要一个通宵,编译一个BC5.0向导生成的程序需要近两个小时,我的显示器是个二手的,辐射非常大,开机屏幕冒火花,看起来很酷的:),有一次程序写的太久,觉得怎么白色的编辑器背景变成了紫色,以为显示器坏了,后来才发现眼睛不行了,不过说来也奇怪,到今天我的视力还能保持1.5,真是个奇迹。但是就是那台破机器陪伴了我两年,让我学会了VC、Delphi、SQLServer等。后来那台机器给我阿姨打字用,据她说一天她正打的开心,一股青烟夹着火苗从显示器钻出来,之后它才寿终正寝。

大三假期找了个机会在一个计算机研究所实习,与其说实习不如说是做义工,工作了两个月一分钱没有拿。但是这两个月对我的发展帮助很大,让我早一步了解了社会,刚去的时候我当然是一窍不通,在那里我熟悉了网络,学会了Delphi和Oracle。由于工作很认真, 得到了比较好的评价,在一位长者的引荐下,我开始和他们一起做项目,这使我在大三大四就有了自己的收入,大四又找了两家MIS公司兼职,虽然钱不多,但是在学生期间有1000多的收入我已经非常满足了,我终于用自己赚的钱把计算机换了。大四下开始找工作,这时我的工作经验已经比较多(当然现在想想非常幼稚),开始听父母的想去那个研究所, 实习过那个部门也希望我能去,但是不知道为什么最后不了了之,这种单位就是比较官僚 ,我一气之下就到了我兼职的一个公司做MIS的TeamLeader。在大三到毕业一年的时间,做过了各种MIS,从煤气、烟厂、公安、铁路、饮食到高校,什么有钱做什么,工作也很辛苦 ,经常加班和熬通宵,从跟客户谈需求到设计、编码、测试、交付都要上。那时觉得很有成就感,觉得自己还不错,现在想想真是很肤浅。

刚走上工作岗位的学生很容易被误导,各种开发工具让人眼花缭乱,同时也觉得很受 公司器重,但这样工作永远是一个低层次的开发者。不要跟我说什么系统分析有多么多么重要,多么多么难。你以为自己跟用户谈需求做设计就是系统分析和设计了吗,国内又有几个公司能够做的很到位很规范?我是ISO9000内审员,也在Rational公司受过多次培训,拿了4个证书,还有一个公司让我去做CMM。这些我听过很多,但是很多事情到国内就变了性质,一个公司不是通过了ISO9000或者CMM就能规范了,我现在在一家有几十年历史的外企工作,里面的管理不是一般国内企业能及的。作为一个毕业不久以前没有步入过社会的学生,几乎不可能在很短的时间掌握系统分析和设计,面向对象、UML只是一个工具,关键是人本身的思想,不是说你熟悉了C++、Rose就能够做出好的设计,相反如果你具备了很高的素质,你可以用C写出比别人用C++更加模块化的程序。

话说远一些,国内软件开发行业有一个怪圈,很多人觉得VC > Delphi > VB,真是很搞笑。这几个软件我都做过开发,说白了他们都是工具,应该根据应用的需要选择采用哪个,而不是觉得哪个上层次。如果你因为用某个开发工具很有面子而选择的话,只能说明你很浅薄。如果说层次,那么这些工具都不上层次,因为它们用来用去都是一些系统的API,微软的朋友不会因为你记住他们多少个API或者多少个类就会觉得你很了不起,你永远只是他们的客户,他们看重的是你口袋里的银子。我也做过系统内核,我也封装过很多API,同样我也不会看重那些使用这些API做二次开发的客户,除非他能够作出自己独到的设计。

至于有人认为C++ > C那更是让人笑掉大牙,不妨你去打听一下,现在有几个操作系统内核是用C++写的,又有几个实时系统用的是C++,当然我也不是说C++不好,但是目前的内核和实时系统中C++还无法与C匹敌,至于说C++适合做应用系统的开发那是另外一回事。所以我的观点是不在于你用什么工具和语言,而在于你干什么工作。你的设计体现了你的技术层次。

这样干了一年我觉得非常苦闷,做的大多数都是熟练工种的活,个人技术上没有太多 的提高也看不到方向。所以决定离开这个城市去上海,寻求更好的发展,并且打算放弃我以前的MIS转到通信行业。

写到这里不能不提到我女朋友,我们是在来上海前半年认识的,她大四在我公司实习,公司派她给我写文档,我们的感情发展的很快。她告诉我很多事情,她家原本是改革开放的第一批暴发户,她母亲爱打牌,输掉了几百万,还欠了很多债,她有男朋友,但是她对他没有感情,只因为他给了她母亲两万多块钱,后来还强迫她写了四万块的借条,她男朋友背叛过她并且不止一次打她,现在逼她结婚不然就要她还钱。这人居然还是一个高校的老师!她母亲把父亲给她的学费花了,因为拖欠学费她没有办法拿到毕业证。她母亲现在有病需要钱,我拿出了自己的一点积蓄并且跟朋友们接了一些,替她交了学费并给她母亲看 病(后来才知道看病的钱又不知所终,就连她母亲是不是有病我都不知道,但她也是没有办法)。这个时候我家知道了一些事情,坚决反对我和她在一起,她原来的男朋友也极力破坏。无奈之下我们决定早一定离开这个伤心的城市,并且瞒着我们家。由于时间仓促,我只准备了4000块钱,她仅有的几百块钱也被她母亲要去了,我买了三张票,一张是中午的,两张是晚上的,中午我的家人把我送上船,他们一离开我就下了船,我和她乘坐晚上的船离开了这个我和她生活了很多年的城市,带走的只是一身债务。没有来过上海的我们两个性倔强,都不愿意去麻烦同学和朋友。来到上海是傍晚6点半,我们都不知道该去哪里,我们找了一个20块钱的旅馆,这个房间连窗户都没有,7月份的天气酷热难耐,房间里非常闷热。第二天我们开始租房子,因为身上的钱不多,我们基本都是步行,花了一个星期时间,不知道在浦东转了多少圈后找到了一个400块的房子,但是我们都不了解上海是付三压一,还要付半个月的中介费,买了一些锅碗瓢盆后,我们身上只有800块钱了,工作都还没有着落,这800块钱要支持到我们拿到第一个月工资,为了省钱我们

自己做饭,每天买菜只花两块钱,她非常喜欢吃(也可能她在大学经常挨饿的愿意),看到她现在这样省吃俭用我真的很不忍心。她以前的男朋友也没有放过她,经常打电话来骚扰,并且来上海看她,还说了不少恐吓她的话,她过于善良,说他以前毕竟帮助过她,叫我不要与他一般见识。以后的每天在家就是苦等面试通知,原本我想迅速找一家MIS公司解决眼前的困难,但是她坚持让我不要放弃自己的理想,终于功夫不负有心人,我找到了一家通信公司,4000块的工资虽然赶不上MIS公司给我开出的价位,但也够在上海生存。她也找到了工作,第一天上班她哭了,这是她来上海第一次流泪,我心里很难受也很感动。

由于是全新的行业,我把自己降到了零点,我学的VC、Delphi、数据库派不上用场, 摆在我面前的是嵌入式、协议、信令一些我从未接触过的知识。我知道我没有退路,于是拼命的学习,我把自己当做一个应届毕业生一样,一分努力一分收获,半年过去我终于熟悉了工作,并且得到了公司的表彰,薪水也加了一级。后面的日子里我们省吃俭用,把欠朋友的1万多块钱还了,日子终于上了正轨。这时女朋友告诉我她想考研究生,我也很支持,于是她辞职在家备考。

另外,在这里我要感谢我的ProjectManager,他原来是一个大通信公司的产品经理, 对人非常和善,我从他那里学到了很多知识,而且他也给了我许许多多无私的帮助。在工作上他给我充分的空间和信任。记得公司安排我维护一个接入服务器软件,由于代码量不算太小(5万行),资料和文档都不齐全,我维护起来非常吃力,所以想重新把它做一遍, 公司领导不太支持,可能觉得工作量太大,但是他极力支持我,私下里他让我放手去做, 我的维护工作他挤时间做。在他的支持下,我花了半年时间完成了接入服务器的软件,并且实现了一个相对完整的TCP/IP协议栈。在这里我学会了嵌入式系统设计、驱动开发、TCP/IP和很多通信的知识,我花了一年时间终于使自己从MIS开发转到了通信行业,并且站稳了脚跟。我的开发大量是对硬件的直接操作,不再受微软的操作系统,VC、Delhpi这些开发工具的约束,我终于看到了另外一片天空。

我做事情喜欢追根问底,随着开发的深入,软件开发与硬件联系越来越紧密,硬件知 识的匮乏又对我的发展产生了障碍,而且芯片技术基本上掌握在国外公司的手里,这对做系统级设计是一个非常大的制约,一个新产品出来,第一道利润(也往往是最丰厚的利润)常常都被IC公司如Intel、Motorola赚去了,国内的厂商只能喝点汤。所以我决心解决自己的硬件技术障碍,并打算离开通信行业,进入IC设计相关领域。

当然我明白如果我对硬件了解的非常少,没有哪家IC公司会仁慈到招我这样一个一窍不通的人来培训。所以我必须努力打好基础,学一些相关知识为以后做准备。就像我开始从MIS转到通信一样,我看过大量通信方面的书,并且给一个ISP做过RADIUS计费分拣台,在这样的背景下这家通信公司才给了我这个机会。我在的通信公司是做系统设计的,有不少PCB Layout硬件人员,平常我就注意向他们学习,由于我做的是软件,在公司看硬件资料不好意思,所以开始只好在家看,刚来上海工作我连续一年都在加班,后来不加了,因为我要挤出时间学习,通常我12点左右睡,第二天5点半起,我上班比较早,地铁上如果人不多我也用来看书。学习当然不会是一帆风顺的,有些实在不懂的问题就积累起来问硬件人员,他们的帮助使我学习进度快了很多,因为在没有人点拨的情况下自学,我的一半时间是花在解决疑难问题上,但这种问题经常是别人的一句话就可以让我豁然开朗,我非常庆幸我有这样的学习环境。在后面的一年里,我学会了看硬件原理图,学会了简单的硬件设计(模拟电路方面还有不小的差距),事情就是这样的,当你安安份份做软件,别人永远认为你是软件开发人员,在你开始学习硬件时别人未必会认同,有位中兴通讯的朋友还对我说过,一个人不可能把所有东西都学完。我也明白这一点,但我希望自己做的更好。但当你熟悉硬件后大家又会觉得你好像原本就是软硬件都懂的,同事们也都习以为常了。这个时候我可以把硬件资料堂堂正正的拿到公司看,没有人再大惊小怪了。 让我比较自豪的是我通过自己的努力做了一个IAD(软交换的终端设备)系统方案,包含软硬件的选型、设计等内容,这个方案得到了公司和同事们的认同,让我感到非常欣慰。

技术是相辅相成的,当我的硬件有了一定的进步后,我的软件设计也有了很大的提高 ,我可以从更深层次理解问题,我做的接入服务器CPU是Motorola PowerPC860,熟悉的朋友都知道860 QMC与软件的批量数据传输通常采用BD表的方式,硬件人员做驱动的时候习惯采用固定BD表,每接收或发送数据都将数据从BD表拷贝到用户Buffer,或从用户Buffer拷贝到BD表,由于理解的比较深入,我自己重新实现了这个过程,采用动态BD表的方式,驱动从一个网口接收数据,提交给我的软件进行三层交换,直至从另外的接口发送出去,没有进行一次拷贝。这样的设计大大提高了性能,使系统的指标接近理论值。软硬件的结合使我的设计水平上了一个台阶。我现在写的这个操作系统,编译后我把程序反编译成汇编,找出其中不优化的代码,然后在C程序中进行调整。举个例子,很多CPU没有专门的乘法指令,这个大家应该都知道,在这种CPU上进行一个乘法操作常常会花费大量的指令周期, 有的朋友会说这个我知道,我会尽量避免采用×号,但是事情往往不是那么简单,你知道

C语言中数组的下标操作是怎么实现的吗?仔细看看反汇编的代码你就会明白,同样是通过下标的定位操作,C编译器会有时候会产生位移指令,但有时候会用乘法实现,两者效率往往是天壤之别,所以明白这些问题你才能将系统性能提升到极致。?

些问题就不多说了,有兴趣的话以后可以共同探讨。

话说远一点,我由衷的希望在软件上做的比较深入的朋友们有机会学学硬件以及其它 相关知识,尤其是做底层开发和嵌入式设计的。这对软件技术的提高有非常大的帮助,否则很多事情你只知道该这样但不会明白为什么该这样。我这个观点在我现在的IC公司Project Manager那里也得到了验证。他告诉我们公司现在的802.11芯片产品的软件经理原本是做该芯片硬件设计的,某某某原本是做软件的,现在在做IC,类似的例子还有很多,只是在国内这样的风气不是非常流行。

我有一些心得体会与大家分享,只有当我干好本职工作后,我才会学习与工作关系不 大的技术,这样公司的上司才不至于反感,在入门阶段的问题我通常不去问那些资深人士 ,而是问一些资历比较浅的朋友,比如刚毕业不久的学生,因为他们往往会跟你详细的讲解,而资深人士通常觉得你的问题太简单,所以回答的也很简单,我又不好意思多问。等技术上了一定的层次后我才会问他们,他们也能给你比较深入的回答。另外,有些朋友说我机会比较好,他们也希望能从事新的工作可惜没有机会,我听了只有苦笑,我的机会了解的人都应该知道,我没有出生在什么IT世家:)也没有谁一路提拔我,所有的路都是自己走出来的,我母亲去世比较早,我的后母(我叫她阿姨)看着我努力过来的,一次她看我大年30还在写程序,她说像我这样努力木头都能学出来。

我的最终目的是IC而不是PCB,所以我下一步的准备开始学习IC设计的知识。公司的同事没有懂IC设计的,后面的路又要靠自己了,我买了不少相关的书,在网上也查了很多的资料,我花了大量的时间去学习VHDL,并且用软件进行了一些简单的设计和仿真(没有设计ASIC,只是针对FPGA),随着学习的深入,我渐渐明白了IC设计的基本流程,同时也明白了这条路的艰辛。这个时候我已经做好了跳槽的准备,我向一家业界又一定知名度的IC设计公司投了简历,并通过了漫长的面试(4个多小时)。其他的一切我都比较满意,唯独薪资差强人意,我也明白原因,因为我是这个行业的新人,我没有经验,我再一次将自己清零了。公司老板问我6000多一个月能不能接受,我知道他也是照章办事。想想我通信行业的朋友们,基本上都是年薪10万以上,月薪过万的也比比皆是,朋友们也帮我介绍了不少待遇不错的公司,我该怎么选择,当时我很犹豫,我热爱我的事业,我向往我的追求, 但我也是一个普通的人,我也需要养家糊口,我也想早一点买房买车。生活给我出了一道难题。

爱因斯坦在63岁时说过“一个人没有在30岁以前达成科学上的最大成就,那他永远都不会有。”这句话给了我很大的压力和震动,我马上就26岁了,离30只有四年时间,我必须抓紧这几年宝贵的时间,努力达到我技术上的最高峰。为了这个理想,为了能离自己的梦更近一些,我选择了这家IC公司,我明白自己的薪资和公司刚进来的硕士研究生相差无几, 但为了今后的发展只能忍受,一切又得重新开始。换行业是一个非常痛苦的过程,尤其从一个春风得意的位置换到一个陌生的岗位,感觉象从温暖的被子里钻出来跳进冰水中,让人难以接受。在原来那家通信公司,我是唯一两年时间涨了五次工资的员工,公司和同事都给了我极大的认可,工作上也常常被委以重任。但现在这一切都成了过去,在新的公司我只是一个新人,没有人知道也没有人在意我过去的成绩。我决定重新开始,我把自己看作新毕业的学生,我要用自己的努力得到公司的认可。进入新的行业是非常痛苦的,我告诉自己必须忍受这一切,虽然外面有很多诱惑,但是既然作出了选择我就不允许自己轻易放弃。

我现在已经在这家新公司上了一个多月的班,开始非常艰难,现在慢慢适应了。第一 个月结束时,Team Leader找我谈话,说我是新进员工中最优秀的一个,我心里很欣慰,这也算对我努力的一个肯定吧。在这里还要感谢我的女朋友,她给了我很大的支持和鼓舞, 每次在我动摇的时候她都在鼓励我,让我坚持自己的理想,刚来上海是她让我不要勉强去做MIS,这次也是她让我顶住了月薪过万的诱惑,没有她我可能不会有今天的成绩。 现在的公司有自己的操作系统,自己的CPU、DSP和其它芯片,在这里我能学到世界上最先进的技术,我们的设计开发不再完全依赖别人的硬件和系统,这让我很开心。我打算等工作步入正轨后,全力学习新的知识,实现我的理想。

在后面的两年里我给自己定下了几个目标:

一.努力做好本职工作,在工作上得到公司和同事们的认同;

二.努力学习IC硬件设计知识,多向同事请教,并利用一切机会多实践;

三.实现我的实时操作系统的主要部分,完成TCP/IP协议栈模块,并免费发布源代码;

四.和我女朋友结婚并买一套小房子,这是最重要的,因为我明白事业是可以重来的,但是珍贵的感情很难失而复得。

在这里提一下我现在开发的操作系统,它是一个实时嵌入式系统,目前支持以下特性:

a.支持时间片轮转调度和基于优先级调度,最多64个优先级;

b.抢占式实时内核;

c.为了便于移植,主体用标准C实现;

d.汇编代码非常少,不到100行;

e.支持任务管理,各任务有独立的堆栈;

f.进程同步和通信目前完成了Semaphore,Message Queue正在调试;

g.实现了定时系统调用;

h.可以在windows上仿真调试

我还打算下一步实现优先级反转保护,Event Flag,Data Pipe,内存管理(以前实现过)、驱动接口等。 在这之后我还会努力完善它,比如加入文件系统,协议栈、调试接口等。希望朋友们提出自己的意见和建议,在此不胜感激!

后记:

就像有的朋友说的,我的经历或许会给一些朋友产生误导,在这里我必须说明一下。 我来上海以前学习过于拼命,常常晚上只睡3个多小时,我身高1米71,那时只有108斤(我现在130多),家人也说我这样拼命活不过60岁,但是当时的我太固执,我对他们说只要能实现理想活50岁我就够了。那时的拼命使我的身体受到了影响,有一次早上突然腰肌剧痛难忍,痛的我倒在床上站不起来。虽然我现在已经比较注意,但有时候还会隐隐作痛。后来在女朋友说服了我,来上海以后我不再如此。我经常引用父亲的一句话“身体是革命的本钱”。

而且我也发现拼命不是办法,我可以熬一两个通宵,最多的一次我连续工作了三天三夜, 但是我半个月都没有恢复过来,这样是不是得不偿失?学习工作应该是一个长期的过程, 像马拉松而不是百米冲刺。我现在非常注意调整学习和工作的强度,我要保证每天尽量有相对充沛的精力,一些年轻的朋友觉得自己也应该拼命努力,这让我多少有些担心,如果我的故事能让你在学习工作上多一点兴趣,我会感到很开心,但如果误导了某些朋友,让你做一些不值得的付出,我会感到很内疚。

技术没有贵贱只分,我以前换行业是因为自己的兴趣所致,而不是对哪个行业有什么 偏见。我希望我的经历不要给朋友一个错误的导向,觉得我始终向更高的技术发展。其实各行各业做到顶尖都是很困难的。话又说回来虽然技术没有贵贱,但是门槛是有高低的, 无论如何,做IC的门槛要比做网页的高,这一点无可否认。国家各种人才都是需要的,但是作为个人奋发向上的想法还是应该有的,努力在自己喜欢的行业上做的更好,而不应该停留在比较肤浅的层次上。

我是一个自己觉得比较有自知之明的人,或许我最大的优点就是知道自己有很多缺点 :)。我的故事中很多的曲折和错误都是由我的缺点造成的,希望大家用审慎的眼光看待我的经历,不要被我的“花言巧语”所迷惑。我学习有些随心所欲,这给我带来了无尽的麻烦,也大大阻碍的我的发展。记得我小时候成绩比较出色,但是后来学习严重偏科,导致我中学成绩一再滑坡,也没有考上什么好的学校,小时候的一个朋友,当时的成绩和我相仿,但是没有我这个缺点,她上了清华,后来在去了美国深造,在一个著名导师手下研究理论科学,这未尝不是一条更好的出路。另外我的学习方法也是在不断改善中的,过去 的学习过于讲究数量和时间,那样学习既苦而已效率不高,现在我非常注意学习的效率和技巧,这样才是学习的捷径(当然不是指投机取巧),比如说学一相对陌生的技术,如果有条件,不妨问一问有经验的人,不需要问很多,往往他不经意的几句话会给你非常大的帮助,甚至超过你看一个星期的书。带着这样的思想再去学习你会节省很多时间,这样何乐不为呢?这些年中我学了不少的东西,由于开始非常盲目,所以学的东西杂乱无章,现在回想起来让我啼笑皆非,我把大量的时间浪费在一些没有必要深入了解的知识上,毕竟一个人的精力是有限度的。很多朋友很我一样都背过五笔字形,的确它是个不错的输入法,但是对一个研发人员它绝对不值得你去背,你的时间应该花在有价值的地方。我这样的事情还做过很多,我背过CCED、WPS的命令和快捷键,在dBase基本退出历史舞台后我还花了很多时间去学习它的使用。所以我的学习在前期缺乏规划,没有明确的短期目的、中期目标,只有一个虚无飘渺的长期的理想。这就像做设计一样,好的设计是从需求抽象到代码有很多过程,而不能得到了需求就立刻开始开始编码。

当然这么些年的学习和工作多多少少有些收获,下面我说说我的一些学习的心得,这 些方法未必正确,我也在不断探索和改进中。我的学习和工作有相对明确的目标,我不会一时心动而去学习某一技术,在下决定之前我会考虑很多,包括长期的发展,个人路线的规划,需要付出的代价、可能遇到的困难及解决的办法等等,在决定后还会制定更加明确的计划,包括短期、中期和长期的,身边可以利用到的资源(包括好的书籍、资料、软硬件环境,也包括有经验的朋友或者师长),以及每一个阶段是怎么过渡到高一阶段的计划,往往在一个学习阶段一旦上路后会走的相对顺利,但是跨阶段通常比较麻烦,比如从学习基础知识转到实践。另外我买书也有自己的方法,现在世面上高质量的书远不如低质量书多,对于一个陌生的技术,往往在第一次买书会选择错误,即使买到一本好书但是它的方向也未必适合你,所以我通常会先在网上查找一些该技术的介绍,有了一点点概念后再去买一本比较薄、相对便宜并且内容相对泛泛而谈的书,这是国内作者最善于写的书:) ,再把它浏览一遍后我就会基本明白这门技术的要点,后面买书和制定计划就会明确的多。否则一开始就想找本好书往往比较困难,而且买回来后努力学习,有时候学了一半才发现是本低质量的书或者是相对过时技术,让人非常懊恼。另外让有经验的人帮你介绍,通常也是一个不错的选择。

有些朋友想学通信、嵌入式开发,但总觉得自己没有软硬件环境,我就按我的了解给 大家介绍一下怎么建立这样的环境,当然我了解的只是我学习和工作的方向。通信我做的是数据网方面的工作,包括TCP/IP、二三层交换、对接入网、H.323和软交换也有一点认识。这些软硬件环境都是可以在PC上构建的。你甚至可以在一个没有网卡的PC上建立一个包含多个路由器、接入服务器、VoIP网关、网守、主机等的仿真网络环境,而且与实际的网络相当接近,当然这需要你有清晰的网络概念和一定的网络知识,我一直在努力开发一套软件将这个过程简化,目前试验已经做完,我可能会将它融入我的操作系统外围扩展软件中。这样的方法我无法用简单的语句讲的很清楚,我可以说一下大概的思想,就是在PC上实现仿真网卡,(知道Windows怎么在没有网卡的机器实现虚拟网卡技术的朋友都应该会明白),然后每一个仿真网卡对应一个虚拟设备,如路由器或者主机。你也可以借助第三方工具完成部分工作,如VmWare等。我现在就是利用一个仿真网卡做自己的开发的。

至于嵌入式开发环境更加容易实现,PC就是一个非常大的硬件平台,现有的嵌入式操 作系统通常都支持X86,你可以在上面做开发,通过软盘Boot或者使用虚拟机装载,我用VxWorks做了试验,在一台PC上跑Windows和VxWorks两个系统。另外Windows上的兼容DOS的16位仿真X86环境也为很多操作系统提供了绝佳的试验环境,我的操作系统在Windows上就是这样实现的。Linux在嵌入式中应用也比较广泛,它在网上有大量的资料,而且也相对比较容易实践。同时很多完善的嵌入式开发环境支持软件仿真,如Tornado、WinCE等。

 

 

 

 

前段时间处理了很多事情,一直没有写下去,花光了所有的积蓄买了一套房子,同时把户口的事情也基本办完了,这几天稍微缓口气。昨天跟我的一个老上司见面聊了半天,心里感慨万千。他从外在条件看让不少外人羡慕,二十多岁做过到了863项目的负责人,博士毕业的爱人单位也非常好。现在三十出头的他在一个通信公司做产品经理,工资虽然不算高但也有一两万,而且还持有股份。但是我们了解的人才理解他的艰辛。“白领”这个词在一些人看来是仿佛是一个动人的光环,但是在我看来是一个无奈的名字,每天行走在大街上,来来往往的车流中有多少是“白领”的?又有几个“白领”住的起高档的住宅?在上海一套别墅300万不足为奇,按揭贷款下来总额接近600万,年薪二十万在上海算是一个中高级“白领”,高额的税金去掉了你百分之几十的收入后,这样算下来不吃不喝也要四十多年,加上生活的其他开支,注定了你与这样的住宅无缘。看着外面一套套别墅,一辆辆好车,我不知道它们是谁的,但我知道其中没有什么白领。我觉得自己很渺小,在这个喧闹的都市中我如同一只蚂蚁,但我有不甘于平凡,我不愿做一个单纯的“白领”。

  其实很多朋友并不了解我,我不是一个追逐时尚技术的人,我只是不愿意做一个所谓的“白领”,更加不愿意做一个单纯的“程序员”。我不甘愿平凡的生活一辈子。我在不断的努力,我的方向非常明确,我要做多数人不做和做不到的事情,很多朋友对我这样频繁的换方向不理解,觉得一个人只要熟悉一种技术就可以了,对于这样的看法我只能说你浅薄,现在的大的系统和产品往往都是软件、硬件和应用相结合的,我要做的不是哪个方面的专家,而是希望能够成为系统设计师。我不相信一个只精通发动机的专家能够设计一辆好车,同样我也不相信对硬件一窍不通的人能做出一个操作系统,或者一个对财会没有一点概念的人能设计出一个优秀的财务软件。在工作中我发现社会上非常缺乏边缘人才,尤其是在国内。在国外一个人软硬件兼修非常普遍。如果设计产品的人只了解他的专业那么是很难有出色的设计。所以我必须趁着自己年轻学的更加广泛一些,这样才能提高自己的综合素质,这也是为什么高校那么多非专业课程。学习工作了这些年,实际上都没有脱离IT这个行业,我现在的公司开发一个系统时,先是将最终功能列举清楚并分析可行性,然后划分哪些是用芯片实现,哪些是用硬件电路实现,哪些是用软件实现,这样的设计才能做出最好的系统。如果一个设计者单纯只懂一个方面是不可能做到这一点的。

  自负常常伴随着无知,记得我大学毕业时,论文答辩会上我和专家组组长争起来了,因为我对自己的设计非常得意,而他虽然是鸡蛋里挑骨头,但是由于知识非常有限,我无法回答他的问题,所以有些“恼羞成怒”。我原来一直喜欢用“所谓”最好的开发工具,记得做过一个愚蠢的设计,一个排课表的软件我用VC+Oracle开发。这些经历我牢记在心,时刻提醒自己学会谦虚。我的亲身经历加上我对一些身边朋友的观察发现这样一个现象。当一个人只会他认为最好的技术,而对其他的一无所知,这样的人经常是目空一切。

从第一个“Hello World”到今天的操作系统,前前后后写了很多代码,从这中间我也积累了很多心得。由于我是在没有人指导的情况下自学编码的,所以走了很多弯路,也犯了不少错误。最初我写程序全凭自己的感觉,写一个新程序对结构设计不很重视,以为学好语言,数据结构就可以写出好的程序,其实远不是这样的。没有设计的情况下,也可以写,但是程序无法写的很大、很复杂。我个人的经验是这样的系统超过8000行我就无法控制了,以前我用VC写过一个Windows下的应用程序,大概8000行左右我对它失去了控制,整个代码一团糟,这8000行倒是可以相对稳定的运行,但是我没有能力再增加什么新的代码,动辄前后冲突,要么就是新代码与旧设计格格不入,需要调整旧的程序。最开始我写程序喜欢追求代码的精巧,别人很多行写出来的代码自己只写很少就可以实现,感觉那样比较酷。其实这样也是非常错误的,我现在写程序非常注重结构设计,为了结构清晰我愿意牺牲一点效率。

  下面一段话是我写程序的座右铭,希望与大家共勉:

  Make it right before you make it faster.

  Keep it right when you make it faster.

  Make it clear before you make it faster.

  Do not sacrifice clarity for small gains in efficiency.

  Brian Kernighan

  另外补充一点:我和我的女朋友现在非常好,双方的家人都认可了,我们决定在近期结婚。

一、底层开发包和工具
1. ::URL::http://www.mesa3d.org
Mesa 是一个类OpenGL( ::URL::http://www.opengl.org  )的开源实现。
2. ::URL::http://openil.sourceforge.net
DevIL (即以前的OpenIL)是一个跨平台的图形处理包,支持
BMP、JPG、GIF 等多种图形文件格式。
二、2D 游戏开发包
1. ::URL::http://www.libsdl.org/
::URL::http://www-900.ibm.com/developerWorks/cn/linux/theme/special/
SDL(Simple DirectMedia Layer)是一个跨平台的多媒体和游戏开
发包,提供2D、音频、事件驱动、多线程和定时器等服务,并有大
量的扩充开发包,如TCP/IP 网络、游戏角色、混音等。SDL 是用C
开发的,但也有Perl、PHP、Delphi 等多种语言的版本。
2. ::URL::http://www.clanlib.org
ClanLib 是一个通用的C++游戏开发包,提供游戏资源操作、网络
对象处理、GUI 主题和游戏脚本等支持。支持Windows 和Linux。
PMT Files – Open Source Game Development
三、3D 引擎和游戏开发包
1. ::URL::http://crystal.sourceforge.net/
Crystal Space 是一个用C++开发的3D 游戏开发包。有丰富的
功能,支持Direct3D、OpenGL、Glide 等。
2. ::URL::http://ogre.sourceforge.net/
OGRE 是一个面向对象的3D 引擎,支持DirectX、OpenGL 和
Glide 等。
3. ::URL::http://hem.passagen.se/opengl/glfw/
GLFW 是一个OpenGL 的应用框架,支持Linux 和Windows。
4. ::URL::http://apocalyx.sourceforge.net/
Apocalyx 是一个基于OpenGL 的3D 引擎。
5. ::URL::http://www.faktiss.net/
Nive 是一个C++ 3D 引擎,基于OpenGL 和
DevIL,支持Windows 和Linux/Xwindow。
6. ::URL::http://plib.sourceforge.net/
Plib 是一个3D 游戏开发包。
7. ::URL::http://alleg.sourceforge.net/ Allegro 是一个跨平台的C/C++游戏开发包,提供2D、3D、声效、用
户输入、文件、压缩、GUI 等功能。
PMT Files – Open Source Game Development
四、游戏和游戏框架
1. ::URL::http://www.freecraft.org/
FreeCraft 是一个实时战略游戏(RTS)框架。
2. ::URL::http://www.worldforge.org/ Worldforge 是一个完整的大型网络RPG 游戏框架。
3. ::URL::http://arianne.info/
Arianne 是一个大型网络RPG 游戏,同时也是一个游戏框架。
五、其他
1. ::URL::http://openai.sourceforge.net/ OpenAI 是一个人工智能的工具包,包括神经网络、遗传算
法、有限状态机等。
一、英文网站
1. ::URL::http://www.flipcode.com/
Daily Game Development News & Resources
2. ::URL::http://www.gamedev.net/
All Your Game Development Needs
3. ::URL::http://www.gamedeveloper.net/
4. ::URL::http://www.gametutorials.com/
Game Programming with Personality, From Start to Finish
5. ::URL::http://www.cfxweb.net/
Demo & Game Development
6. ::URL::http://www.gdse.com
The Game Programming and Design Search Engine
7. ::URL::http://www.2dgame-tutorial.com
8. ::URL::http://www.gamasutra.com/
CMP - The Art & Science of Making Games
9. ::URL::http://www.gdmag.com/
CMP - Game Developer Magazine
二、中文网站
1. ::URL::http://www.gameres.com/
中文游戏开发技术资料和交流
2. ::URL::http://mays.soage.com/
中国游戏开发者
3. ::URL::http://www.gpgame.net
金点工作室
4. ::URL::http://www.codingnow.com
云风工作室
5. ::URL::http://lightwing.myrice.com/
琴心剑胆
6. ::URL::http://www.diamondgarden.net/
钻石花园
7. ::URL::http://www.joynb.com/
无名鸟游戏工作室
8. ::URL::http://www.npc6.com/
何苦做游戏,游戏制作的文化
PMT Files – Game Development Website
三、Linux 游戏及其开发网站
1. ::URL::http://www.happypenguin.org
The Linux Game Tome
2. ::URL::http://linuxgames.com/
Linux Games - For the people
3. ::URL::http://h.webring.com/webring?ring=linuxgp;list
Linux Game Programming Webring

  • 优化
  • /O1 最小化空间 minimize space
  • /Op[-] 改善浮点数一致性 improve floating-pt consistency
  • /O2 最大化速度 maximize speed
  • /Os 优选代码空间 favor code space
  • /Oa 假设没有别名 assume no aliasing
  • /Ot 优选代码速度 favor code speed
  • /Ob 内联展开(默认 n=0) inline expansion (default n=0)
  • /Ow 假设交叉函数别名 assume cross-function aliasing
  • /Od 禁用优化(默认值) disable optimizations (default)
  • /Ox 最大化选项。(/Ogityb2 /Gs) maximum opts. (/Ogityb1 /Gs)
  • /Og 启用全局优化 enable global optimization
  • /Oy[-] 启用框架指针省略 enable frame pointer omission
  • /Oi 启用内建函数 enable intrinsic functions
  • 代码生成
  • /G3 为 80386 进行优化 optimize for 80386
  • /G4 为 80486 进行优化 optimize for 80486
  • /GR[-] 启用 C++ RTTI enable C++ RTTI
  • /G5 为 Pentium 进行优化 optimize for Pentium
  • /G6 为 Pentium Pro 进行优化 optimize for Pentium Pro
  • /GX[-] 启用 C++ 异常处理(与 /EHsc 相同) enable C++ EH (same as /EHsc)
  • /EHs 启用同步 C++ 异常处理 enable synchronous C++ EH
  • /GD 为 Windows DLL 进行优化 optimize for Windows DLL
  • /GB 为混合模型进行优化(默认) optimize for blended model (default)
  • /EHa 启用异步 C++ 异常处理 enable asynchronous C++ EH
  • /Gd __cdecl 调用约定 __cdecl calling convention
  • /EHc extern "C"默认为 nothrow extern “C” defaults to nothrow
  • /Gr __fastcall 调用约定 __fastcall calling convention
  • /Gi[-] 启用增量编译 enable incremental compilation
  • /Gz __stdcall 调用约定 __stdcall calling convention
  • /Gm[-] 启用最小重新生成 enable minimal rebuild
  • /GA 为 Windows 应用程序进行优化 optimize for Windows Application
  • /Gf 启用字符串池 enable string pooling
  • /QIfdiv[-] 启用 Pentium FDIV 修复 enable Pentium FDIV fix
  • /GF 启用只读字符串池 enable read-only string pooling
  • /QI0f[-] 启用 Pentium 0x0f 修复 enable Pentium 0x0f fix
  • /Gy 分隔链接器函数 separate functions for linker
  • /GZ 启用运行时调试检查 enable runtime debug checks
  • /Gh 启用钩子函数调用 enable hook function call
  • /Ge 对所有函数强制堆栈检查 force stack checking for all funcs
  • /Gs[num] 禁用堆栈检查调用 disable stack checking calls
  • 输出文件
  • /Fa[file] 命名程序集列表文件 name assembly listing file
  • /Fo 命名对象文件 name object file
  • /FA[sc] 配置程序集列表 configure assembly listing
  • /Fp 命名预编译头文件 name precompiled header file
  • /Fd[file] 命名 .PDB 文件 name .PDB file
  • /Fr[file] 命名源浏览器文件 name source browser file
  • /Fe 命名可执行文件 name executable file
  • /FR[file] 命名扩展 .SBR 文件 name extended .SBR file
  • /Fm[file] 命名映射文件 name map file
  • 预处理器
  • /FI 命名强制包含文件 name forced include file
  • /C 不吸取注释 don’t strip comments
  • /U 移除预定义宏 remove predefined macro
  • /D{=|#} 定义宏 define macro
  • /u 移除所有预定义宏 remove all predefined macros
  • /E 将预处理定向到标准输出 preprocess to stdout
  • /I 添加到包含文件的搜索路径 add to include search path
  • /EP 将预处理定向到标准输出,不要带行号 preprocess to stdout, no #line
  • /X 忽略”标准位置” ignore “standard places”
  • /P 预处理到文件 preprocess to file
  • 语言
  • /Zi 启用调试信息 enable debugging information
  • /Zl 忽略 .OBJ 中的默认库名 omit default library name in .OBJ
  • /ZI 启用调试信息的”编辑并继续”功能 enable Edit and Continue debug info
  • /Zg 生成函数原型 generate function prototypes
  • /Z7 启用旧式调试信息 enable old-style debug info
  • /Zs 只进行语法检查 syntax check only
  • /Zd 仅要行号调试信息 line number debugging info only
  • /vd{0|1} 禁用/启用 vtordisp disable/enable vtordisp
  • /Zp[n] 在 n 字节边界上包装结构 pack structs on n-byte boundary
  • /vm 指向成员的指针类型 type of pointers to members
  • /Za 禁用扩展(暗指 /Op) disable extensions (implies /Op)
  • /noBool 禁用bool关键字 disable “bool” keyword
  • /Ze 启用扩展(默认) enable extensions (default)
  • 杂项
  • /?, /help 打印此帮助消息 print this help message
  • /c 只编译,不链接 compile only, no link
  • /W 设置警告等级(默认 n=1) set warning level (default n=1)
  • /H 最大化外部名称长度 max external name length
  • /J 默认 char 类型是 unsigned default char type is unsigned
  • /nologo 取消显示版权消息 suppress copyright message
  • /WX 将警告视为错误 treat warnings as errors
  • /Tc 将文件编译为 .c compile file as .c
  • /Yc[file] 创建 .PCH 文件 create .PCH file
  • /Tp 将文件编译为 .cpp compile file as .cpp
  • /Yd 将调试信息放在每个 .OBJ 中 put debug info in every .OBJ
  • /TC 将所有文件编译为 .c compile all files as .c
  • /TP 将所有文件编译为 .cpp compile all files as .cpp
  • /Yu[file] 使用 .PCH 文件 use .PCH file
  • /V 设置版本字符串 set version string
  • /YX[file] 自动的 .PCH 文件 automatic .PCH
  • /w 禁用所有警告 disable all warnings
  • /Zm 最大内存分配(默认为 %) max memory alloc (% of default)
  • 链接
  • /MD 与 MSVCRT.LIB 链接 link with MSVCRT.LIB
  • /MDd 与 MSVCRTD.LIB 调试库链接 link with MSVCRTD.LIB debug lib
  • /ML 与 LIBC.LIB 链接 link with LIBC.LIB
  • /MLd 与 LIBCD.LIB 调试库链接 link with LIBCD.LIB debug lib
  • /MT 与 LIBCMT.LIB 链接 link with LIBCMT.LIB
  • /MTd 与 LIBCMTD.LIB 调试库链接 link with LIBCMTD.LIB debug lib
  • /LD 创建 .DLL Create .DLL
  • /F 设置堆栈大小 set stack size
  • /LDd 创建 .DLL 调试库 Create .DLL debug libary
  • /link 链接器选项和库 linker options and libraries

《Advanced Animation with DirectX》

这本书主要讲的是怎样用DirectX9来完成游戏中的动画,主要是移动、骨骼动画等等,也涉及了不少的3d模型的知识。这本书起步比较的高,没有讲太多的DirectX9的基础,但是章节安排得十分合理,由简入难,是一本初学者和高手都比较适用的书。

《Focus On 3D Models》

说起游戏中的3d模型,这本书讲得十分透彻。也对各种知名游戏中的模型作了介绍。只是感觉这本书的高度不够,没有站在引擎的角度来看游戏中的模型的处理。技术比较专,偏于实用。

《3D Game Engine Design》

这本书应该比较早就有了,最近才开始看。觉得理论的方面讲得比较到位,数学和物理的内容相对讲得多一点。具体的代码没有过多的涵义,因为这本书的价值就在于它的理论。

《3D Game Engine Programming》

与上面的那本书比起来,这本书以具体的一个游戏引擎为背景,讨论了具体实现时需要注意的各种问题。比较全面和直观的讲解了游戏引擎的细节。但是在引擎最重要的环节-图形部分,没有太多的讨论,在效率方面也没有给读者带来什么惊喜。

《Game Scripting Mastery》 - 游戏脚本设计完全掌握

不可多得的好书。整本书围绕游戏脚本中的技术来谈。从lua到python,从汇编到高级语言的解析器,几乎涵盖了脚本的方方面面。该书又不拘泥于技术,而是教给读者能设计并实现自己的游戏脚本的能力。部分专题又站在引擎的高度,对引擎有兴趣的读者必定会受到不少启发。

《Shaders for Game Programmers and Artists》

初学者比较适用,没有过于深奥的理论,用了RenderMonkey作为开发shader的工具,从头讲解shader技术,以及hlsl。是shader比较好的入门书。

《Game Coding Complete》

开发者的指南。全面地介绍了游戏开发中的各种细节,文中的实例给人印象极深,各处的补注和注意事项也丰富了书的含量。不仅给门外汉上了第一课,也能让许多开发者意识到自己长期以来没有发现的错误。

《Data Structures For Game Programmers》

这绝不是一本枯燥无味的数据结构的书。书中提到的方方面面都以游戏为背景,代码和例程也用游戏Demo的形式出现在读者面前。

《Microsoft DirectX9 Programmable Graphics Pipeline》

微软出版,几乎就是工业标准的一本书。如果你想从头学习shader,或想了解shader更底层的技术,你就应该看这本书。同时,这也是一本工具书。

《3D Math Primer for graphics and game development》

不论是数学理论还是具体实现,都讲得十分详尽。对于迫切需要提高数学水平的开发人员来说是不错的教科书。另外还有这本书的缩略版,内容上面没有删减多少,只是少了具体化实现的代码。

《Programming Multiplayer Games》

比较深入的谈到了多人联机游戏中的技术专题,是网游开发爱好者的指南。

《Programming Role-playing Games With DirectX》

虽然说是DirectX8时代的书,但书中的知识丝毫没有落后于当前的技术。代码也十分优雅。还从游戏设计的方面讲解了rpg开发中需要注意的问题。

其实很简单,但如像我一样不怎么用UltraEdit的朋友们来说,可能不太清楚。首先去UltraEdit的官网下载支持Lua的Wordfiles文件(这里),是个文本文件(lua.txt)。打开UltraEdit安装目录下的wordfile.txt,把lua.txt文件中的内容拷贝粘贴到wordfile.txt的末尾,存盘,OK,于是UltraEdit语法高亮项多出Lua一项,可以选择使用了。其他语言的语法高亮支持与此类似。



/L14"Lua" Line Comment = -- Block Comment On = --[[ Block Comment Off = ]] Escape Char = \ String Chars = "' File Extensions = LUA BIN
/Delimiters = ~!@%^&*()-+=|\/{}[]:;"'<> , .?
/Function String = "%[a-zA-Z]*)"
/Function String 1 = "%[a-zA-Z_0-9]*::^([a-zA-Z_0-9^~]+^)[ ^t]++([^p*&, ^t^[^]/*=:&a-zA-Z_0-9.]++)[~;]"
/Function String 2 = "%[a-zA-Z_0-9]+[ ^t*]+^([a-zA-Z_0-9]+^)[ ^t]++([^p*&, ^t^[^]a-zA-Z_0-9.]++)[~;]"
/Function String 3 = "%[a-zA-Z_0-9"]* ^([a-zA-Z_0-9"]+^)[ ^t]++([^p*&, ^t^[^]a-zA-Z_0-9.]++)[~;]"
/Function String 4 = "%[a-zA-Z_0-9*&$]+[ ^t]+[a-zA-Z_0-9*&$]+[ ^t*]+^([a-zA-Z_0-9]+^)[ ^t]++([^p*&, ^t^[^]a-zA-Z_0-9.]++)[~;]"
/Function String 5 = "%^([a-zA-Z_0-9]+^)[ ^t]++([^p*&, ^t^[^]a-zA-Z_0-9.]++)[~;]"
/Function String 6 = "%^([a-zA-Z_0-9]+^)[ ^t]+([^p*&, ^t^[^]a-zA-Z_0-9.]++)[~;]"
/C1 "key words"
and
do
else elseif end
function
if
local
nil not
or
repeat return
then
until
while
/C2
abs acos appendto ascii asin assert atan atan2
call ceil clock collectgarbage copytagmethods cos
date deg dofile dostring
error execute exit
floor foreach foreachvar format frexp
getbinmethod getenv getglobal gettagmethod gsub
ldexp log log10
max min mod
newtag next nextvar
print
rad random randomseed rawgetglobal rawgettable rawsetglobal rawsettable read
readfrom remove rename
seterrormethod setglobal setlocale settag settagmethod sin sqrt strbyte
strchar strfind strlen strlower strrep strsub strupper
tag tan tmpname tonumber tostring type
write writeto
/C3
$debug
$else
$end
$endinput
$if
$ifnot
$nodebug
/C4
PI
_INPUT _OUTPUT _STDERR _STDIN _STDOUT
/C5
+
-
*
// /
^
<
>
=
~
%
.
:
/C6
;
,
(
)
{
}
[
]
.. ...
/C7
cgi cgilua cgilua_url char2hexa chdir
dbluaerrorfb dblua_escape decode default_script
encodecgi encodetable escape
filetype
getvalue
hexa hexa2char html_mask
includehtml insertfield
lua_mask
maketable map mkurl
nopipe
preprocess
redirect relativeurl relative_url
saveluavar savestate script_path script_pdir script_vdir stateerrormethod
statefile stdin strsplit
unescape
/C8
DBClose DBExec DBOpen DBRow

直接贴代码吧,保存成**.reg,导入就可以了,感谢cnnod32cn.cn提供服务器.

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Eset\Nod\CurrentVersion\Modules\Update\Settings]

“PROFILES_ENABLED”=dword:00000000

“DefaultServerCount”=dword:00000005

“DefaultServer0”=”http://u1.cnnod32.cn

“DefaultServer1”=”http://u2.cnnod32.cn

“DefaultServer2”=”http://u3.cnnod32.cn

“DefaultServer3”=”http://u4.cnnod32.cn

“DefaultServer4”=”http://u5.cnnod32.cn

“DefaultServer5”=”http://u6.cnnod32.cn

“DefaultServer6”=”http://u7.cnnod32.cn

“LastExpireCheck”=dword:45e2697d

“UserServerCount”=dword:00000000

“PROFILE_CURRENTPROFILE”=”我的设定档”

“LastUpdateAttempt”=dword:45e27831

“DefaultServerWeight0”=dword:00000014

“DefaultServerWeight1”=dword:00000014

“DefaultServerWeight2”=dword:00000014

“DefaultServerWeight3”=dword:00000014

“DefaultServerWeight4”=dword:00000014

“DefaultServerWeight5”=dword:00000014

“DefaultServerWeight6”=dword:00000014

[HKEY_LOCAL_MACHINE\SOFTWARE\Eset\Nod\CurrentVersion\Modules\Update\Settings\Config000]

“PROFILE_NAME”=”我的设定档”

[HKEY_LOCAL_MACHINE\SOFTWARE\Eset\Nod\CurrentVersion\Modules\Update\Settings\Config000\Settings]

“UpdateType”=dword:00000006

“ConnectionType”=dword:00000001

“SelectedServer”=”AUTOSELECT”

“Username”=””

“Password”=hex:

“UseProxy”=dword:00000000

“Proxy”=””

“ProxyPort”=dword:00000c38

“ProxyUsername”=””

“ProxyPassword”=hex:

“LanUserType”=dword:00000000

“LanUsername”=””

“LanPassword”=hex:

“DisconnectLan”=dword:00000000

“Restart”=dword:00000001

 

 

 

================================

其他的一些东西

主升级服务器 陕西    正常运行

http://u1.cnnod32.cn

第2升级服务器 江苏    正常运行

http://u2.cnnod32.cn

第3升级服务器 新疆    正常运行

http://u3.cnnod32.cn

第4升级服务器 安徽    正常运行

http://u4.cnnod32.cn

第5升级服务器 广东    正常运行

http://u5.cnnod32.cn

ESS 汉化测试版

NOD32 2.7.39简体标准版

NOD32 2.7.27简体标准版

VC编程规范-程序员应该这样写代码 

基本要求 

1.1 程序结构清析,简单易懂,单个函数的程序行数不得超过100行。 

1.2 打算干什么,要简单,直接了当,代码精简,避免垃圾程序。 

1.3 尽量使用标准库函数和公共函数。 

1.4 不要随意定义全局变量,尽量使用局部变量。 

1.5 使用括号以避免二义性。 


2.可读性要求 

2.1 可读性第一,效率第二。 

2.2 保持注释与代码完全一致。 

2.3 每个源程序文件,都有文件头说明,说明规格见规范。 

2.4 每个函数,都有函数头说明,说明规格见规范。 

2.5 主要变量(结构、联合、类或对象)定义或引用时,注释能反映其含义。 

2.7 常量定义(DEFINE)有相应说明。 

2.8 处理过程的每个阶段都有相关注释说明。 

2.9 在典型算法前都有注释。 

2.10 利用缩进来显示程序的逻辑结构,缩进量一致并以Tab键为单位,定义Tab为 6个字节。 

2.11 循环、分支层次不要超过五层。 

2.12 注释可以与语句在同一行,也可以在上行。 

2.13 空行和空白字符也是一种特殊注释。 

2.14 一目了然的语句不加注释。 

2.15 注释的作用范围可以为:定义、引用、条件分支以及一段代码。 

2.16 注释行数(不包括程序头和函数头说明部份)应占总行数的 1/5 到 1/3 。 


3. 结构化要求 

3.1 禁止出现两条等价的支路。 

3.2 禁止GOTO语句。 

3.3 用 IF 语句来强调只执行两组语句中的一组。禁止 ELSE GOTO 和 ELSE RETURN。 

3.4 用 CASE 实现多路分支。 

3.5 避免从循环引出多个出口。 

3.6 函数只有一个出口。 

3.7 不使用条件赋值语句。 

3.8 避免不必要的分支。 

3.9 不要轻易用条件分支去替换逻辑表达式。 


4. 正确性与容错性要求 

4.1 程序首先是正确,其次是优美 

4.2 无法证明你的程序没有错误,因此在编写完一段程序后,应先回头检查。 

4.3 改一个错误时可能产生新的错误,因此在修改前首先考虑对其它程序的影响。 

4.4 所有变量在调用前必须被初始化。 

4.5 对所有的用户输入,必须进行合法性检查。 

4.6 不要比较浮点数的相等, 

如: 10.0 * 0.1 == 1.0 , 不可靠 

4.7 程序与环境或状态发生关系时,必须主动去处理发生的意外事件,如文件能否逻辑锁定、打印机是否联机等。 

4.8 单元测试也是编程的一部份,提交联调测试的程序必须通过单元测试。 


5. 可重用性要求 

5.1 重复使用的完成相对独立功能的算法或代码应抽象为公共控件或类。 

5.2 公共控件或类应考虑OO思想,减少外界联系,考虑独立性或封装性。 

5.3 公共控件或类应建立使用模板。 


附:C++ 编程规范,delphi作相应的参考 

.1适用范围 

本标准适用于利用Visul C++ ,Borland C++进行软件程序开发的人员.。 

2变量命名 

命名必须具有一定的实际意义,形式为xAbcFgh,x由变量类型确定,Abc、Fgh表示连续意 

义字符串,如果连续意义字符串仅两个,可都大写.如OK. 

具体例程: 

BOOL类型 bEnable; 

ch * char chText 

c * 类对象 cMain(对象实例) 

h * Handle(句柄) hWnd 

i * int 

n * 无符号整型 

p * 指针 

sz,str * 字符串 

w WORD 

x,y 坐标 

Char或者TCHAR类型 与Windows API有直接联系的用szAppName[10]形式否则用 

FileName[10]形式,单个字符也可用小写字母表示; 

Int类型 nCmdShow; 

LONG类型 lParam; 

UINT类型 uNotify; 

DWORD类型 dwStart; 

PSTR类型 pszTip; 

LPSTR类型 lpCmdLine 

LPTSTR类型 lpszClassName; 

LPVOID类型 lpReserved 

WPARAM类型 wParam, 

LPARAM类型 lParam 

HWND类型 hDlg; 

HDC类型 hDC; 

HINSTANCE类型 hInstance 

HANDLE类型 hInstance, 

HICON类型 hIcon; 

int iTmp 

float fTmp 

DWORD dw* 

String , AnsiString str * 

m_ 类成员变量 m_nVal, m_bFlag 

g_ 全局变量 g_nMsg, g_bFlag 

局部变量中可采用如下几个通用变量:nTemp,nResult,I,J(一般用于循环变量)。其他资源句柄同上 

.3常量命名和宏定义 

常量和宏定义必须具有一定的实际意义; 

常量和宏定义在#include和函数定义之间; 

常量和宏定义必须全部以大写字母来撰写,中间可根据意义的连续性用下划线连接,每一条定义的右侧必须有一简单的注释,说明其作用; 

资源名字定义格式: 

菜单:IDM_XX或者CM_XX 

位图:IDB_XX 

对话框:IDD_XX 

字符串:IDS_XX 

DLGINIT:DIALOG_XX 

ICON:IDR_XX 

.4函数命名 

函数原型说明包括引用外来函数及内部函数,外部引用必须在右侧注明函数来源: 模块名及文件名, 如是内部函数,只要注释其定义文件名; 

第一个字母必须使用大写字母,要求用大小写字母组合规范函数命名,必要时可用下划线间隔,示例如下: 

void UpdateDB_Tfgd (TRACK_NAME); //Module Name :r01/sdw.c 

void PrintTrackData (TRACK_NAME); //Module Name :r04/tern.c 

void ImportantPoint (void); //Module Name :r01/sdw.c 

void ShowChar (int , int , chtype); //Local Module 

void ScrollUp_V (int , int); //Local Module 

.5结构体命名 

结构体类型命名必须全部用大写字母,原则上前面以下划线开始;结构体变量命名必须用大小写字母组合,第一个字母必须使用大写字母,必要时可用下划线间隔。对于私有数据区,必须注明其所属的进程。全局数据定义只需注意其用途。 


示例如下: 

typedef struct 


char szProductName[20]; 

char szAuthor[20]; 

char szReleaseDate[16]; 

char szVersion[10]; 

unsigned long MaxTables; 

unsigned long UsedTables; 

}DBS_DATABASE; 

DBS_DATABASE GdataBase; 

6 控件的命名: 

用小写前缀表示类别 

用小写前缀表示类别: 

fm 窗口 

cmd 按钮 

cob combo,下拉式列表框 

txt 文本输入框 

lab labal,标签 

img image,图象 

pic picture 

grd Grid,网格 

scr 滚动条 

lst 列表框 

frm fram 

一个人真正睡着觉最多只有两个钟头,其余都是浪费时间,躺在枕头上做梦,没有哪个人不做梦。至于醒来觉得自己没有做梦,那是因为他忘记了。通常一个人睡两个钟头就够了,为什么有人要睡七、八个钟头?那是你赖床躺在枕头上休息的习惯养成的,并非我们需要那么久的睡眠时间,尤其打坐做功夫的人晓得,正午只要闭眼真正睡着三分钟,等于睡两个钟头,不过要对好正午的时间。夜晚则要在正子时睡着,五分钟等于六个钟头。就这个时间的学问又大了,同宇宙法则、地球法则、易经阴阳的道理有关系,而且你会感觉到,心脏下面硬是有一股力量降下来,与丹田(肾上)的力量融合,所谓“水火既济”,豁然一下,那你睡眠够了,精神百倍。所以失眠或真要夜里熬夜的人,正子时的时刻,哪怕   二十分钟也一定要睡,睡不着也要训练自己睡着。过了正子时大约十二点半以后,你不会想睡了,这很糟糕。更严重的,到了天快亮,四、五点钟,五、六点卯时的时候,你又困得想睡,这时如果一睡,一天都会昏头。所以想从事熬夜工作的人,正子时,即使有天大的事也要摆下来,睡它半小时,到了卯时(7:00)想睡觉千万不要睡,那一天精神就够了。不过失眠的人都挨过十二点,在床上翻来覆去睡不着,结果快天亮睡着了,到第二天下午都昏头昏脑,因此你会感觉失眠、睡眠不足,实际上是你没有经验。
大家可以试试看看效果吧。

消息的接收

消息的接收主要有3个函数:GetMessagePeekMessageWaitMessage

  • GetMessage原型如下:BOOL GetMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax);
    该函数用来获取与hWnd参数所指定的窗口相关的且wMsgFilterMin和wMsgFilterMax参数所给出的消息值范围内的消息。需要注意的是,如果hWnd为NULL,则GetMessage获取属于调用该函数应用程序的任一窗口的消息,如果wMsgFilterMin和wMsgFilterMax都是0,则GetMessage就返回所有可得到的消息。函数获取之后将删除消息队列中的除WM_PAINT消息之外的其他消息,至于WM_PAINT则只有在其处理之后才被删除。

  • PeekMessage原型如下:BOOL PeekMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax,UINT wRemoveMsg);
    该函数用于查看应用程序的消息队列,如果其中有消息就将其放入lpMsg所指的结构中,不过,与GetMessage不同的是,PeekMessage函数不会等到有消息放入队列时才返回。同样,如果hWnd为NULL,则PeekMessage获取属于调用该函数应用程序的任一窗口的消息,如果hWnd=-1,那么函数只返回把hWnd参数为NULL的PostAppMessage函数送去的消息。如果wMsgFilterMin和wMsgFilterMax都是0,则PeekMessage就返回所有可得到的消息。函数获取之后将删除消息队列中的除WM_PAINT消息之外的其他消息,至于WM_PAINT则只有在其处理之后才被删除。

  • WaitMessage原型如下:BOOL VaitMessage();当一个应用程序无事可做时,该函数就将控制权交给另外的应用程序,同时将该应用程序挂起,直到一个新的消息被放入应用程序的队列之中才返回。 //多线程特点的体现

消息的处理

接下来我们谈一下消息的处理,首先我们来看一下VC中的消息泵:

1
2
3
4
5
6
7
8
while(GetMessage(&amp;msg, NULL, 0, 0)) 
{
if(!TranslateAccelerator(msg.hWnd, hAccelTable, &amp;msg))
{
TranslateMessage(&amp;msg);
DispatchMessage(&amp;msg);
}
}

首先,GetMessage从进程的主线程的消息队列中获取一个消息并将它复制到MSG结构,如果队列中没有消息,则GetMessage函数将等待一个消息的到来以后才返回。 如果你将一个窗口句柄作为第二个参数传入GetMessage,那么只有指定窗口的的消息可以从队列中获得。GetMessage也可以从消息队列中过滤消息只接受消息队列中落在范围内的消息。这时候就要利用GetMessage/PeekMessage指定一个消息过滤器。这个过滤器是一个消息标识符的范围或者是一个窗体句柄,或者两者同时指定。当应用程序要查找一个后入消息队列的消息是很有用。WM_KEYFIRST 和 WM_KEYLAST 常量用于接受所有的键盘消息。 WM_MOUSEFIRST 和 WM_MOUSELAST 常量用于接受所有的鼠标消息。

然后TranslateAccelerator判断该消息是不是一个按键消息并且是一个加速键消息,如果是,则该函数将把几个按键消息转换成一个加速键消息传递给窗口的回调函数。处理了加速键之后,函数TranslateMessage将把两个按键消息WM_KEYDOWN和WM_KEYUP转换成一个WM_CHAR,不过需要注意的是,消息WM_KEYDOWN,WM_KEYUP仍然将传递给窗口的回调函数。

处理完之后,DispatchMessage函数将把此消息发送给该消息指定的窗口中已设定的回调函数。如果消息是WM_QUIT,则GetMessage返回0,从而退出循环体。应用程序可以使用PostQuitMessage来结束自己的消息循环。通常在主窗口的WM_DESTROY消息中调用。

下面我们举一个常见的小例子来说明这个消息泵的运用:

1
2
3
4
if (::PeekMessage(&amp;msg, m_hWnd, WM_KEYFIRST,WM_KEYLAST, PM_REMOVE)) 
{
if (msg.message == WM_KEYDOWN &amp;&amp; msg.wParam == VK_ESCAPE)...
}

这里我们接受所有的键盘消息,所以就用WM_KEYFIRST 和 WM_KEYLAST作为参数。最后一个参数可以是PM_NOREMOVE 或者 PM_REMOVE,表示消息信息是否应该从消息队列中删除。

所以这段小代码就是判断是否按下了Esc键,如果是就进行处理。

窗口过程

窗口过程是一个用于处理所有发送到这个窗口的消息的函数。任何一个窗口类都有一个窗口过程。同一个类的窗口使用同样的窗口过程来响应消息。 系统发送消息给窗口过程将消息数据作为参数传递给他,消息到来之后,按照消息类型排序进行处理,其中的参数则用来区分不同的消息,窗口过程使用参数产生合适行为。

一个窗口过程不经常忽略消息,如果他不处理,它会将消息传回到执行默认的处理。窗口过程通过调用DefWindowProc来做这个处理。窗口过程必须return一个值作为它的消息处理结果。大多数窗口只处理小部分消息和将其他的通过DefWindowProc传递给系统做默认的处理。窗口过程被所有属于同一个类的窗口共享,能为不同的窗口处理消息。下面我们来看一下具体的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 
{
int wmId, wmEvent;
PAINTSTRUCT ps;
HDC hdc;
TCHAR szHello[MAX_LOADSTRING];
LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);

switch (message)
{
case WM_COMMAND:
wmId = LOWORD(wParam);
wmEvent = HIWORD(wParam);
// Parse the menu selections:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, (DLGPROC)About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &amp;ps);
// TODO: Add any drawing code here...
RECT rt;
GetClientRect(hWnd, &amp;rt);
DrawText(hdc, szHello, strlen(szHello), &amp;rt, DT_CENTER);
EndPaint(hWnd, &amp;ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

消息分流器

通常的窗口过程是通过一个switch语句来实现的,这个事情很烦,有没有更简便的方法呢?有,那就是消息分流器,利用消息分流器,我们可以把switch语句分成更小的函数,每一个消息都对应一个小函数,这样做的好处就是对消息更容易管理。

之所以被称为消息分流器,就是因为它可以对任何消息进行分流。下面我们做一个函数就很清楚了:

1
2
3
4
5
6
7
8
9
10
11
12
13
void MsgCracker(HWND hWnd,int id,HWND hWndCtl,UINT codeNotify) 
{
switch(id)
{
case ID_A:
if(codeNotify==EN_CHANGE)...
break;
case ID_B:
if(codeNotify==BN_CLICKED)...
break;
....
}
}

然后我们修改一下窗口过程:

1
2
3
4
5
6
7
8
9
10
11
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) 
{
switch(message)
{
HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker);
HANDLE_MSG(hWnd,WM_DESTROY,MsgCracker);
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

在WindowsX.h中定义了如下的HANDLE_MSG宏:

1
2
#define HANDLE_MSG(hwnd,msg,fn) \ 
switch(msg): return HANDLE_##msg((hwnd),(wParam),(lParam),(fn));

实际上,HANDLE_WM_XXXX都是宏,例如:HANDLE_MSG(hWnd,WM_COMMAND,MsgCracker);将被转换成如下定义:

1
2
#define HANDLE_WM_COMMAND(hwnd,wParam,lParam,fn)\ 
((fn)((hwnd),(int)(LOWORD(wParam)),(HWND)(lParam),(UINT)HIWORD(wParam)),0L);

好了,事情到了这一步,应该一切都明朗了。

不过,我们发现在windowsx.h里面还有一个宏:FORWARD_WM_XXXX,我们还是那WM_COMMAND为例,进行分析:

1
2
#define FORWARD_WM_COMMAND(hwnd, id, hwndCtl, codeNotify, fn) \ 
(void)(fn)((hwnd), WM_COMMAND, MAKEWPARAM((UINT)(id),(UINT)(codeNotify)), (LPARAM)(HWND)(hwndCtl))

所以实际上,FORWARD_WM_XXXX将消息参数进行了重新构造,生成了wParam && lParam,然后调用了我们定义的函数。

MFC消息的处理实现方式

初看MFC中的各种消息,以及在头脑中根深蒂固的C++的影响,我们可能很自然的就会想到利用C++的三大特性之一:虚拟机制来实现消息的传递,但是经过分析,我们看到事情并不是想我们想象的那样,在MFC中消息是通过一种所谓的消息映射机制来处理的。

为什么呢?在潘爱民老师翻译的《Visual C++技术内幕》(第4版)中给出了详细的原因说明,我再简要的说一遍。在CWnd类中大约有110个消息,还有其它的MFC的类呢,算起来消息太多了,在C++中对程序中用到的每一个派生类都要有一个vtable,每一个虚函数在vtable中都要占用一个4字节大小的入口地址,这样一来,对于每个特定类型的窗口或控件,应用程序都需要一个440KB大小的表来支持虚拟消息控件函数。

如果说上面的窗口或控件可以勉强实现的话,那么对于菜单命令消息及按钮命令消息呢?因为不同的应用程序有不同的菜单和按钮,我们怎么处理呢?在MFC库的这种消息映射系统就避免了使用大的vtable,并且能够在处理常规Windows消息的同时处理各种各样的应用程序的命令消息。

说白了,MFC中的消息机制其实质是一张巨大的消息及其处理函数的一一对应表,然后加上分析处理这张表的应用框架内部的一些程序代码.这样就可以避免在SDK编程中用到的繁琐的CASE语句。

MFC的消息映射的基类CCmdTarget

如果你想让你的控件能够进行消息映射,就必须从CCmdTarget类中派生。CCmdTarget类是MFC处理命令消息的基础、核心。MFC为该类设计了许多成员函数和一些成员数据,基本上是为了解决消息映射问题的,所有响应消息或事件的类都从它派生,例如:应用程序类、框架类、文档类、视图类和各种各样的控件类等等,还有很多。

不过这个类里面有2个函数对消息映射非常重要,一个是静态成员函数DispatchCmdMsg,另一个是虚函数OnCmdMsg。

DispatchCmdMsg专门供MFC内部使用,用来分发Windows消息。OnCmdMsg用来传递和发送消息、更新用户界面对象的状态。

CCmdTarget对OnCmdMsg的默认实现:在当前命令目标(this所指)的类和基类的消息映射数组里搜索指定命令消息的消息处理函数。

这里使用虚拟函数GetMessageMap得到命令目标类的消息映射入口数组_messageEntries,然后在数组里匹配命令消息ID相同、控制通知代码也相同的消息映射条目。其中GetMessageMap是虚拟函数,所以可以确认当前命令目标的确切类。

如果找到了一个匹配的消息映射条目,则使用DispachCmdMsg调用这个处理函数;

如果没有找到,则使用_GetBaseMessageMap得到基类的消息映射数组,查找,直到找到或搜寻了所有的基类(到CCmdTarget)为止;

如果最后没有找到,则返回FASLE。

每个从CCmdTarget派生的命令目标类都可以覆盖OnCmdMsg,利用它来确定是否可以处理某条命令,如果不能,就通过调用下一命令目标的OnCmdMsg,把该命令送给下一个命令目标处理。通常,派生类覆盖OnCmdMsg时 ,要调用基类的被覆盖的OnCmdMsg。

在MFC框架中,一些MFC命令目标类覆盖了OnCmdMsg,如框架窗口类覆盖了该函数,实现了MFC的标准命令消息发送路径。必要的话,应用程序也可以覆盖OnCmdMsg,改变一个或多个类中的发送规定,实现与标准框架发送规定不同的发送路径。例如,在以下情况可以作这样的处理:在要打断发送顺序的类中把命令传给一个非MFC默认对象;在新的非默认对象中或在可能要传出命令的命令目标中。

消息映射的内容

通过ClassWizard为我们生成的代码,我们可以看到,消息映射基本上分为2大部分:

在头文件(.h)中有一个宏DECLARE_MESSAGE_MAP(),他被放在了类的末尾,是一个public属性的;与之对应的是在实现部分(.cpp)增加了一章消息映射表,内容如下:

1
2
3
4
5
6
7
BEGIN_MESSAGE_MAP(当前类, 当前类的基类) 
//{{AFX_MSG_MAP(CMainFrame)

  消息的入口项

//}}AFX_MSG_MAP
END_MESSAGE_MAP()

但是仅是这两项还远不足以完成一条消息,要是一个消息工作,必须有以下3个部分去协作:

  1. 在类的定义中加入相应的函数声明;
  2. 在类的消息映射表中加入相应的消息映射入口项;
  3. 在类的实现中加入相应的函数体;

消息的添加

有了上面的这些只是作为基础,我们接下来就做我们最熟悉、最常用的工作:添加消息。MFC消息的添加主要有2种方法:自动/手动,我们就以这2种方法为例,说一下如何添加消息。

1、利用Class Wizard实现自动添加

在菜单中选择View–>Class Wizard,也可以用单击鼠标右键,选择Class Wizard,同样可以激活Class Wizard。选择Message Map标签,从Class name组合框中选取我们想要添加消息的类。在Object IDs列表框中,选取类的名称。此时, Messages列表框显示该类的大多数(若不是全部的话)可重载成员函数和窗口消息。类重载显示在列表的上部,以实际虚构成员函数的大小写字母来表示。其他为窗口消息,以大写字母出现,描述了实际窗口所能响应的消息ID。选中我们向添加的消息,单击Add Function按钮,Class Wizard自动将该消息添加进来。

有时候,我们想要添加的消息本应该出现在Message列表中,可是就是找不到,怎么办?不要着急,我们可以利用Class Wizard上Class Info标签以扩展消息列表。在该页中,找到Message Filter组合框,通过它可以改变首页中Messages列表框中的选项。这里,我们选择Window,从而显示所有的窗口消息,一把情况下,你想要添加的消息就可以在Message列表框中出现了,如果还没有,那就接着往下看:)

2、手动地添加消息处理函数

如果在Messages列表框中仍然看不到我们想要的消息,那么该消息可能是被系统忽略掉或者是你自己创建的,在这种情况下,就必须自己手工添加。根据我们前面所说的消息工作的3个部件,我们一一进行处理:

  1. 在类的. h文件中添加处理函数的声明,紧接在}AFX_MSG行之后加入声明,注意:一定要以afx_msg开头。
    通常,添加处理函数声明的最好的地方是源代码中Class Wizard维护的表下面,但是在它标记其领域的{}括弧外面。这些括弧中的任何东西都将会被Class Wizard销毁。

  2. 接着,在用户类的.cpp文件中找到}AFX_MSG_MAP行,紧接在它之后加入消息入口项。同样,也是放在{ {} }的外面

  3. 最后,在该文件中添加消息处理函数的实体

什么是消息?

   消息系统对于一个win32程序来说十分重要,它是一个程序运行的动力源泉。一个消息,是系统定义的一个32位的值,他唯一的定义了一个事件,向Windows发出一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使Windows发送一个消息给应用程序。

   消息本身是作为一个记录传递给应用程序的,这个记录中包含了消息的类型以及其他信息。例如,对于单击鼠标所产生的消息来说,这个记录中包含了单击鼠标时的坐标。这个记录类型叫做MSG,MSG含有来自windows应用程序消息队列的消息信息,它在Windows中声明如下:

typedef struct tagMsg
{
HWND hwnd; 接受该消息的窗口句柄
UINT message; 消息常量标识符,也就是我们通常所说的消息号
WPARAM wParam; 32位消息的特定附加信息,确切含义依赖于消息值
LPARAM lParam; 32位消息的特定附加信息,确切含义依赖于消息值
DWORD time; 消息创建时的时间
POINT pt; 消息创建时的鼠标/光标在屏幕坐标系中的位置
}MSG;

   消息可以由系统或者应用程序产生。系统在发生输入事件时产生消息。举个例子, 当用户敲键, 移动鼠标或者单击控件。系统也产生消息以响应由应用程序带来的变化, 比如应用程序改变系统字体改变窗体大小。应用程序可以产生消息使窗体执行任务,或者与其他应用程序中的窗口通讯。

   消息中有什么?

   我们给出了上面的注释,是不是会对消息结构有了一个比较清楚的认识?如果还没有,那么我们再试着给出下面的解释:

   hwnd 32位的窗口句柄。窗口可以是任何类型的屏幕对象,因为Win32能够维护大多数可视对象的句柄(窗口、对话框、按钮、编辑框等)。

   message用于区别其他消息的常量值,这些常量可以是Windows单元中预定义的常量,也可以是自定义的常量。消息标识符以常量命名的方式指出消息的含义。当窗口过程接收到消息之后,他就会使用消息标识符来决定如何处理消息。例如、WM_PAINT告诉窗口过程窗体客户区被改变了需要重绘。符号常量指定系统消息属于的类别,其前缀指明了处理解释消息的窗体的类型。

   wParam 通常是一个与消息有关的常量值,也可能是窗口或控件的句柄。

   lParam 通常是一个指向内存中数据的指针。由于WParam、lParam和Pointer都是32位的,因此,它们之间可以相互转换。

消息标识符的值

   系统保留消息标识符的值在0x0000在0x03ff(WM_USER-1)范围。这些值被系统定义消息使用。 应用程序不能使用这些值给自己的消息。应用程序消息从WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF范围的消息由应用程序自己使用;0XC000到0XFFFF范围的消息用来和其他应用程序通信,我们顺便说一下具有标志性的消息值:

WM_NULL---0x0000 空消息。
0x0001----0x0087 主要是窗口消息。
0x00A0----0x00A9 非客户区消息
0x0100----0x0108 键盘消息
0x0111----0x0126 菜单消息
0x0132----0x0138 颜色控制消息
0x0200----0x020A 鼠标消息
0x0211----0x0213 菜单循环消息
0x0220----0x0230 多文档消息
0x03E0----0x03E8 DDE消息
0x0400 WM_USER
0x8000 WM_APP
0x0400----0x7FFF 应用程序自定义私有消息

   消息有的分类?

   其实,windows中的消息虽然很多,但是种类并不繁杂,大体上有3种:窗口消息、命令消息和控件通知消息。

   窗口消息大概是系统中最为常见的消息,它是指由操作系统和控制其他窗口的窗口所使用的消息。例如CreateWindow、DestroyWindow和MoveWindow等都会激发窗口消息,还有我们在上面谈到的单击鼠标所产生的消息也是一种窗口消息。

   命令消息,这是一种特殊的窗口消息,他用来处理从一个窗口发送到另一个窗口的用户请求,例如按下一个按钮,他就会向主窗口发送一个命令消息。

   控件通知消息,是指这样一种消息,一个窗口内的子控件发生了一些事情,需要通知父窗口。通知消息只适用于标准的窗口控件如按钮、列表框、组合框、编辑框,以及Windows公共控件如树状视图、列表视图等。例如,单击或双击一个控件、在控件中选择部分文本、操作控件的滚动条都会产生通知消息。 她类似于命令消息,当用户与控件窗口交互时,那么控件通知消息就会从控件窗口发送到它的主窗口。但是这种消息的存在并不是为了处理用户命令,而是为了让主窗口能够改变控件,例如加载、显示数据。例如按下一个按钮,他向父窗口发送的消息也可以看作是一个控件通知消息;单击鼠标所产生的消息可以由主窗口直接处理,然后交给控件窗口处理。

   其中窗口消息及控件通知消息主要由窗口类即直接或间接由CWND类派生类处理。相对窗口消息及控件通知消息而言,命令消息的处理对象范围就广得多,它不仅可以由窗口类处理,还可以由文档类,文档模板类及应用类所处理。

   由于控件通知消息很重要的,人们用的也比较多,但是具体的含义往往令初学者晕头转向,所以我决定把常见的几个列出来供大家参考:

   按扭控件

BN_CLICKED 用户单击了按钮
BN_DISABLE 按钮被禁止
BN_DOUBLECLICKED 用户双击了按钮
BN_HILITE 用/户加亮了按钮
BN_PAINT 按钮应当重画
BN_UNHILITE 加亮应当去掉

   组合框控件

CBN_CLOSEUP 组合框的列表框被关闭
CBN_DBLCLK 用户双击了一个字符串
CBN_DROPDOWN 组合框的列表框被拉出
CBN_EDITCHANGE 用户修改了编辑框中的文本
CBN_EDITUPDATE 编辑框内的文本即将更新
CBN_ERRSPACE 组合框内存不足
CBN_KILLFOCUS 组合框失去输入焦点
CBN_SELCHANGE 在组合框中选择了一项
CBN_SELENDCANCEL 用户的选择应当被取消
CBN_SELENDOK 用户的选择是合法的
CBN_SETFOCUS 组合框获得输入焦点

   编辑框控件

EN_CHANGE 编辑框中的文本己更新
EN_ERRSPACE 编辑框内存不足
EN_HSCROLL 用户点击了水平滚动条
EN_KILLFOCUS 编辑框正在失去输入焦点
EN_MAXTEXT 插入的内容被截断
EN_SETFOCUS 编辑框获得输入焦点
EN_UPDATE 编辑框中的文本将要更新
EN_VSCROLL 用户点击了垂直滚动条消息含义

   列表框控件

LBN_DBLCLK 用户双击了一项
LBN_ERRSPACE 列表框内存不够
LBN_KILLFOCUS 列表框正在失去输入焦点
LBN_SELCANCEL 选择被取消
LBN_SELCHANGE 选择了另一项
LBN_SETFOCUS 列表框获得输入焦点

队列消息和非队列消息

   从消息的发送途径来看,消息可以分成2种:队列消息和非队列消息。消息队列由可以分成系统消息队列和线程消息队列。系统消息队列由Windows维护,线程消息队列则由每个GUI线程自己进行维护,为避免给non-GUI现成创建消息队列,所有线程产生时并没有消息队列,仅当线程第一次调用GDI函数数系统给线程创建一个消息队列。队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。

   对于队列消息,最常见的是鼠标和键盘触发的消息,例如WM_MOUSERMOVE,WM_CHAR等消息,还有一些其它的消息,例如:WM_PAINT、WM_TIMER和WM_QUIT。当鼠标、键盘事件被触发后,相应的鼠标或键盘驱动程序就会把这些事件转换成相应的消息,然后输送到系统消息队列,由Windows系统去进行处理。Windows系统则在适当的时机,从系统消息队列中取出一个消息,根据前面我们所说的MSG消息结构确定消息是要被送往那个窗口,然后把取出的消息送往创建窗口的线程的相应队列,下面的事情就该由线程消息队列操心了,Windows开始忙自己的事情去了。线程看到自己的消息队列中有消息,就从队列中取出来,通过操作系统发送到合适的窗口过程去处理。 //有关Windows中消息的走向的说明


   一般来讲,系统总是将消息Post在消息队列的末尾。这样保证窗口以先进先出的顺序接受消息。然而,WM_PAINT是一个例外,同一个窗口的多个 WM_PAINT被合并成一个 WM_PAINT 消息, 合并所有的无效区域到一个无效区域。合并WM_PAIN的目的是为了减少刷新窗口的次数

   非队列消息将会绕过系统队列和消息队列,直接将消息发送到窗口过程,。系统发送非队列消息通知窗口,系统发送消息通知窗口。 例如,当用户激活一个窗口系统发送WM_ACTIVATE, WM_SETFOCUS, and WM_SETCURSOR。这些消息通知窗口它被激活了。非队列消息也可以由当应用程序调用系统函数产生。例如,当程序调用SetWindowPos系统发送WM_WINDOWPOSCHANGED消息。一些函数也发送非队列消息,例如下面我们要谈到的函数。

   消息的发送

   了解了上面的这些基础理论之后,我们就可以进行一下简单的消息发送与接收。

   把一个消息发送到窗口有3种方式:发送、寄送和广播。

   发送消息的函数有SendMessage、SendMessageCallback、SendNotifyMessage、SendMessageTimeout;寄送消息的函数主要有PostMessage、PostThreadMessage、PostQuitMessage;广播消息的函数我知道的只有BroadcastSystemMessage、BroadcastSystemMessageEx。

   SendMessage的原型如下:LRESULT SendMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam),这个函数主要是向一个或多个窗口发送一条消息,一直等到消息被处理之后才会返回。不过需要注意的是,如果接收消息的窗口是同一个应用程序的一部分,那么这个窗口的窗口函数就被作为一个子程序马上被调用;如果接收消息的窗口是被另外的线程所创建的,那么窗口系统就切换到相应的线程并且调用相应的窗口函数,这条消息不会被放进目标应用程序队列中。函数的返回值是由接收消息的窗口的窗口函数返回,返回的值取决于被发送的消息。

   PostMessage的原型如下:BOOL PostMessage(HWND hWnd,UINT Msg,WPARAM wParam,LPARAM lParam),该函数把一条消息放置到创建hWnd窗口的线程的消息队列中,该函数不等消息被处理就马上将控制返回。需要注意的是,如果hWnd参数为HWND_BROADCAST,那么,消息将被寄送给系统中的所有的重叠窗口和弹出窗口,但是子窗口不会收到该消息;如果hWnd参数为NULL,则该函数类似于将dwThreadID参数设置成当前线程的标志来调用PostThreadMEssage函数。

   从上面的这2个具有代表性的函数,我们可以看出消息的发送方式和寄送方式的区别所在:被发送的消息是否会被立即处理,函数是否立即返回。被发送的消息会被立即处理,处理完毕后函数才会返回;被寄送的消息不会被立即处理,他被放到一个先进先出的队列中,一直等到应用程序空线的时候才会被处理,不过函数放置消息后立即返回。

   实际上,发送消息到一个窗口处理过程和直接调用窗口处理过程之间并没有太大的区别,他们直接的唯一区别就在于你可以要求操作系统截获所有被发送的消息,但是不能够截获对窗口处理过程的直接调用

   以寄送方式发送的消息通常是与用户输入事件相对应的,因为这些事件不是十分紧迫,可以进行缓慢的缓冲处理,例如鼠标、键盘消息会被寄送,而按钮等消息则会被发送。

   广播消息用得比较少,BroadcastSystemMessage函数原型如下:

long BroadcastSystemMessage(DWORD dwFlags,LPDWORD lpdwRecipients,UINT uiMessage,WPARAM wParam,LPARAM lParam);

   该函数可以向指定的接收者发送一条消息,这些接收者可以是应用程序、可安装的驱动程序、网络驱动程序、系统级别的设备驱动消息和他们的任意组合。需要注意的是,如果dwFlags参数是BSF_QUERY并且至少一个接收者返回了BROADCAST_QUERY_DENY,则返回值为0,如果没有指定BSF_QUERY,则函数将消息发送给所有接收者,并且忽略其返回值。

发现个小东东,在vista或2008里,可以把屏保显示到桌面上(没在xp和2003里测试过,应该也可以显示别的窗口)。


步骤如下:

1.打开spy++,找到“Program Manager”窗口。

2.用计算器把窗口句柄从16进制换算成10进制,比如000100E0换算成65760.

3.运行屏保 /p 刚才的10进制窗口句柄,比如:ssbranded.scr /p 65760,OK。

应该在桌面上显示了,不过桌面上的图标不见了。

要退出需要在进程管理器结束。

这两天办公室不时有人机器中毒,结果整个内部局域网经常被ARP Poison充斥,导致网络瞬断,于是不得不想办法来实现静态IP-Mac地址绑定。然而在我用的Windows Server 2008 beta3的机器上,却碰到了一个很奇怪的问题。

在本机键入如下命令,这个IP是我局域网网关的地址,MAC为其内网网卡的MAC:

C:\Windows\system32>arp -s 10.0.0.254  00-11-d8-64-6b-bc

结果失败,错误提示为:

The ARP entry addition failed: 5

放狗搜了一下这个错误提示,发现这个提示应该为权限不足。见鬼了,明明用的是administrators组的用户,却权限不够?于是把故障嫌疑定位到UAC上去,发现无论是怎样调整,甚至关掉UAC,都还是同样的结果。同时用Process Explorer检查发现我的CMD进程有足够权限执行此项配置,这样也断定不可能是用户权限问题,简直是见鬼了。

然后就是打开安全审核,拼了命查找各项安全日志,无奈发现不了任何蛛丝马迹。回头一想,貌似命令行下的一些个命令都不生成安全审计记录的,这次彻底郁闷了。

翻着翻着资料突然看到了netsh这个工具,这玩艺是MS建议以后再命令行配置Windows网络协议栈的一个类似IOS的新工具,死马当活马医,试试看这玩艺能不能操作ARP表,简单看了一下帮助,于是弄出来这么个命令:

C:\Windows\system32>netsh -c “interface ipv4″ add neighbors 10  “10.0.0.254″  ”00-11-d8-64-6b-bc”

其中数字10为我本地网卡连接的index,这个从show interface可以看到。或者直接替换成网卡名称也可以,就是”Local Area Connection”了。

结果这个命令竟然成功了,运行之后再运行arp -a可以看到static的ARP表项已经按照预想的一样创建成功。也就是说,前面使用arp -s添加失败根本不是权限不够的原因,而是其他原因了。具体什么原因呢?我个人猜想恐怕是内核网络部分一些数据结构或者API的变更,同时arp.exe这个工具没有跟随更新导致的broken问题吧。

更诡异的在后面,虽然我目的达到了,但不小甘心又使用arp -s修改了刚添加的静态表项,结果发现竟然修改成功-_-b,然后又继续测试其他情况,发现如下结论:

1,目前ARP表中不存在对应IP的记录,可以用arp -s添加新的静态IP-Mac绑定。

2,目前ARP表中已经存在对应IP的static表项的记录,可以用arp -s修改为新的静态IP-Mac绑定。

3,目前ARP表中已经存在对应IP的dynamic表项的记录,使用arp -s添加新的静态记录时会出现错误:The ARP entry addition failed: 5

总之这个版本的Windows附带的这个arp.exe一定有问题,改天找找看有没有联系方式能否反映给MS开发部门。

环境光的使用比较简单,Direct3D把它作为一个渲染状态,通过调用IDirect3DDevice9::SetRenderState进行设置,对应的状态常数为D3DRS_AMBIENT。

按光源划分,直射光可分为三种:

1)点光源

点光源(Point Light)从一个点向周围均匀地发射光线。点光源有颜色、位置、作用范围,光强随距离而衰减,没有方向。

clip_image001

屏幕剪辑的捕获时间: 2007-6-27, 18:29

2)平行光

平行光(Directional Light)由相互平行的光线组成。平行光只有颜色和方向,没有位置,也没有作用范围和衰减,因此不论实体位于场景的何处,所受到的光照都相同。

clip_image002

屏幕剪辑的捕获时间: 2007-6-27, 18:31

3)聚光灯(Spotlight)是三种直射光中最复杂的一种。它的光束是一个圆锥,分内、外核两部分:内核最亮,且亮度保持不变;外核较暗,沿径向有一个衰减。

clip_image003

屏幕剪辑的捕获时间: 2007-6-27, 18:34

如下图,其中夹角Theta和Phi定义了内、外核的大小。

clip_image004

屏幕剪辑的捕获时间: 2007-6-27, 18:35

聚光灯有颜色、位置、方向(即光束中心所指方向)、作用范围、衰减(沿光线方向)。

在Direct3D中,用结构D3DLIGHT9来描述直射光,它的定义如下:

typedef struct _D3DLIGHT9{

D3DLIGHTTYPE Type;

//类型:只能是点光源、平行光或聚光灯

D3DCOLORVALUE Diffuse;

//

1.世界变换

我们在建立三维实体的数学模型时,通常以实体的某一点为坐标原点,比如一个球体,很自然就用球心做原点,这样构成的坐标系称为本地坐标系(Local Coordinates)。实体总是位于某个场景(World Space)中,而场景采用世界坐标系(World Coordinates),如图所示,因此需要把实体的本地坐标变换成世界坐标,这个变换被称为世界变换(World Transformation)。
clip_image001_2
屏幕剪辑的捕获时间: 2007/6/22, 15:58

在Direct3D中,坐标变换通过一个4X4矩阵来实现,对于世界变换,只要给出实体在场景中的位置信息,就可以借助Direct3D函数得到变换矩阵,具体计算步骤如下:

  • 首先把实体旋转在世界坐标系的原点,使两个坐标系重合;
  • 在世界空间中,对实体进行平等移动,其对应的平移变换阵TT可由函数D3DXMatrixTranslation求得;
  • 把平移后的实体沿自身的Z轴旋转一个角度(角度大于0,表示从Z轴的正向朝原点看上去,旋转方向为顺时针;反之为逆时针,下同),对应的旋转变换阵TZ用D3DXMatrixRotationZ计算;
  • 把实体沿自身的Y轴旋转一个角度,用D3DXMatrixRotationY求出变换阵TY;
  • 把实体沿自身的Y轴旋转一个角度,用D3DXMatrixRotationX求出变换阵TX;
  • 最后对实体进行缩放,假设三个轴的缩放系数分别为SX、SY、SZ,该操作对应的变换阵TS可由函数D3DXMatrixScaling求得;
  • 最终的世界变换矩阵TW=TS·TX·TY·TZ·TT,在Direct3D中,矩阵乘法用函数D3DXMatrixMultiply实现,注意相乘顺序为操作的逆序。

从以上描述中,我们很容易得出:实体的运动可以通过不断的改变世界变换矩阵来实现。

2. 视角变换

实体确定后,接下来确定观察者在世界坐标系中的方位,换句话说,就是在世界坐标系中如何放置摄像机.观察者(摄像机)所看到的景象,就是Direct3D窗口显示的内容.
确定观察者需要三个量:

  • 观察者的点坐标;
  • 视线方向,为一个矢量,不过Direct3D用视线上的一个点来替代,此时视线方向就是从观察者指向该目标点,这样表示更直观一些;
  • 上方向,通俗地说,就是观察者的头顶方向,用一个矢量表示.

确定后,以观察者为原点,视线为Z轴,上方向或它的一个分量为Y轴(X轴可由左手法则得出,为右方向),构成了视角坐标系,如下图所示.我们需要把实体从世界空间转换到视角空间,这个坐标变换被称为视角变换(View Transformation).

clip_image002_2

屏幕剪辑的捕获时间: 2007/6/22, 20:56
与世界变换相比,视角变换矩阵的获取要容易得多,只需调用一个函数D3DXMatrixLookAtLH,其输入参数就是决定观察者的那三个量.

3. 投影变换

实体转换到视角空间后,还要经过投影变换(Projection Transformation)三维的实体才能显示在二维的计算机屏幕上.
Direct3D使用透视投影变换(Perspective Transformation),此时在视角空间中,可视区域是一个以视线为轴心的棱台(Viewing Frustum),如下图所示.想象一下你处在一个伸手不见五指的房间里,面前有一扇窗户,你可以透过窗户看到各种景物.窗户就是棱台的前裁剪平面,天空、远山等背景是后裁剪平面,期间的可视范围是景深。投影变换把位于可视棱台内的景物投影到前裁剪平面,其间的可视范围是景深。投影变换把位于可视棱台内的景物投影到前裁剪平面,由于采用透视投影,距离观察者远的对象会变小,从而更具有真实感。在Direct3D中,前裁剪平面被映射到程序窗口,最终形成了我们在屏幕上版的画面。

clip_image003_2

屏幕剪辑的捕获时间: 2007/6/22, 21:10
透视投影变换由四个量决定:

  • 前裁剪平面的宽度W;
  • 前裁剪平面的高度H;
  • 前裁剪平面到原点的距离Z1;
  • 后裁剪平面到原点的距离Z2。

由于W、H用起来不是很直观,因此实际应用中,常用fov和aspect代替W、H,其中fov是Y方向上的可视角度,通常取π/4;aspect是前裁剪平面的高度与宽度之比,通常取1(由三角函数定义,易知H=2·z1·tg(fov/2),W=H/aspect)。用这四个量来调用函数D3DXMatrixPerspectiveFovLH,即可获得投影变换矩阵。
得到三个变换矩阵后,还需要调用方法 IDirect3DDevice9::SetTransform把它们设置到渲染环境中。
最后,可以用三句话来概括这些变换的作用:世界变换决定实体的位置;视角变换决定观察者的位置;投影变换决定观察者的可视区域。

在Direct3D中,实体模型中的一个点可能被 多个三角形面所共用,如下图,虽然只有4个顶点,却由4个三角形面组成.

clip_image001

屏幕剪辑的捕获时间: 2007-6-27, 9:46

如果把顶点数据按对应图元的格式,直接放进顶点缓存区,该棱锥使用三角形列,4个锥面其需要4*3=12个顶点,也就是有8个顶点是重复的.如果实体比较复杂,重复的顶点会更多,造成资源浪费.

为些Direct3D引入了索引缓存的概念,把顶点的具体数据和代表图元格式的顶点顺序分开存储:顶点数据仍然放到顶点缓存区中,索引缓存区则按照图元格式,顺序存放顶点的索引.

以上图为例:头等在顶点缓存中保存A、B、C、D这4个顶点的FVF数据项,相应的索引为0、1、2、3;然后按照三角形列的组成顺序,把顶点索引值存入索引缓存区,4个三角形分别为△ACB、△ADC、△ADB、△BCD(注意顶点排列顺序和可视面的关系),则索引序列为0 2 1 0 3 2 0 1 3 1 2 3.这样原本要用12个顶点数据构建一个三棱锥,索引缓存后只需要4个。

CUSTOMVERTEX vertices[]={                // FVF顶点数据        // 四方体

{ 1.0f, 0.25f, 0.0f, D3DCOLOR_XRGB(0,255,255)},        // 蓝白

{ 1.0f, 0.75f, 0.0f, D3DCOLOR_XRGB(0,255,255)},

{0.25f, 0.75f, 0.0f, D3DCOLOR_XRGB(255,0,255)},        // 粉红

{0.25f, 0.25f, 0.0f, D3DCOLOR_XRGB(255,0,255)},

{ 1.0f, 0.25f, 1.0f, D3DCOLOR_XRGB(127,127,255)},        // 蓝

{ 1.0f, 0.75f, 1.0f, D3DCOLOR_XRGB(127,127,255)},

{0.25f, 0.75f, 1.0f, D3DCOLOR_XRGB(255,255,0)},        // 黄

{0.25f, 0.25f, 1.0f, D3DCOLOR_XRGB(255,255,0)}

};

//WORD indices[] = {0,1,2,2,1,3, 0,3,4,4,3,7, 1,2,5,5,2,6, 0,1,4,4,1,5, 3,2,7,7,2,6, 4,5,6,6,5,7};

WORD indices[] = {0,1,3,1,3,2, 0,3,4,3,4,7, 5,1,6,6,1,2, 1,0,5,5,0,4, 3,2,7,2,7,6, 4,5,7,5,7,6};

//创建顶点缓存区, 并获取接口IDirect3DVertexBuffer9的指针

m_pDevice->CreateVertexBuffer(

sizeof(vertices),                // 缓存区尺寸

0,D3DFVF_CUSTOMVERTEX,

D3DPOOL_DEFAULT, &m_pVB,NULL);

//把顶点数据填入顶点缓存区

void* pVertices;

m_pVB->Lock(0, sizeof(vertices), (void**)&pVertices, 0);

memcpy(pVertices, vertices, sizeof(vertices));

m_pVB->Unlock();

// 创建索引缓存区, 并获取接口 LPDIRECT3DINDEXBUFFR9 的指针

m_pDevice->CreateIndexBuffer(sizeof(indices),

0,D3DFMT_INDEX16,

D3DPOOL_DEFAULT, &m_pIB,NULL);

// 把索引值填入索引缓存区

void* pIndices;

m_pIB->Lock(0,sizeof(indices),(void**)&pIndices,0);

memcpy(pIndices,indices,sizeof(indices));

m_pIB->Unlock();

渲染:

// 设置自定义的FVF

m_pDevice->SetFVF(D3DFVF_CUSTOMVERTEX);

// 绑定顶点缓冲区至设备数据源

m_pDevice->SetStreamSource(0, m_pVB, 0, sizeof(CUSTOMVERTEX));

// 绑定索引缓存区

m_pDevice->SetIndices(m_pIB);

// 从索引缓存区绘制图元,参数1为图元格式,参数4为顶点数,参数6为三角形数

//m_pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 4, 0, 4);        // 三角形

m_pDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 8, 0, 12);

// 绘制图元,其中参数1为图元格式,参数3为三角形数目

//m_pDevice->DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

在Direct3D中,三角形是构成实体的基本单位,因为一个三角形正好是一个平面,以三角形面为单位进行渲染效率最高。

一个三角形由三个点构成,习惯上把这些点称为顶点(Vertex)。三角形平面有正反面之分,由顶点的排序决定:顶点按顺时针排列的表面是正面,如图。

clip_image001

屏幕剪辑的捕获时间: 2007/6/22, 14:59

其中与三角形平面垂直、且指向正面的矢量称为该平面的法线(Normal)。

在Direct3D中,为提高渲染效率,缺省条件下只有正面可见,不过可以通过IDirect3DDevice9::SetRenderState来改变设置,其对应的渲染状态常数为D3DRS_CULLMODE,具体用法请参阅SDK文档。

顶点法线(Vertex Normal)是过顶点的一个矢量,用于在高洛德着色(Gouraud Shading)中的计算光照和纹理效果。在生成曲面时,通常令顶点法线和相邻平面的法线保持等角,如图1,这样进行渲染时,会在平面接缝处产生一种平滑过渡的效果。如果是多边形,则令顶点法线等于该点所属平面(三角形)的法线,如图2,以便在接缝处产生突出的边缘。

clip_image002

屏幕剪辑的捕获时间: 2007/6/22, 15:17

clip_image003

屏幕剪辑的捕获时间: 2007/6/22, 15:16

按坐标轴之间的相互关系划分,三维坐标系可分为左手体系和右手体系,如下图所示。在左手体系中,坐标轴的定义符合法则:左手四个手指的旋转方向从X轴到Y轴,大拇指的指向就是Z轴。右手体系依次类推。Direct3D使用左手坐标系,其中X轴表示左右,Y轴表示上下,Z轴表示远近(深度)。

clip_image001

屏幕剪辑的捕获时间: 2007/6/22, 14:07

取定坐标系后,空间中的任意一点可以用一组坐标值(X,Y,Z)来表示。矢量是空间中的一条有向线段,Direct3D用它来标识空间方向。适量的表示方法与点坐标类似,也是用{X,Y,Z}不过它表示的是从原点指向点(X,Y,Z)的有向线段。适量与起点无关,只要两个矢量同向(平行)且等长,就认为它们相等。在Direct3D中,点和矢量通常使用同一个结构D3DXVECTOR3保存。

矢量的计算公式很简单:假设矢量的起点为M(X1,Y,Z1),终点为N(X2,Y2,Z2),则矢量→MN={X2-X1,Y2-Y1,Z2-Z1}。

使用D3DXVec3Normalize把它变换成单位矢量(长度为1)。

Lua的语法非常灵活, 使用他的metatable及metamethod可以模拟出很多语言的特性.

C#中我们这样使用事件:

xxx.Click += new System.EventHandler(xxx_Click);

private void xxx_Click(object sender, EventArgs e)

{

/**/

}

在Lua中要达到同样的效果, 并且支持事件多播机制, 其关键在于重写metamethod __call, 从而使得不光function才能被调用, table也能够被调用.

主要思想就是, 通过一个table来保存注册事件的若干响应函数, 然后拿table当function一样来调用, 重写__call后, 实现调用table时遍历执行table中的注册方法.

需要在lua5.0 或 lua.net上执行, lua 5.1略有改动.

1 --test.lua

   2 do

   3

   4 –事件原型对象, 所有事件由此原型生成

   5 Event = {}

   6

   7 function Event:New()

   8         local event = {}

   9         setmetatable(event, self)

  10         –覆盖__index逻辑

  11         self.__index = self

  12         –覆盖__call逻辑

  13         self.__call = self.Call

  14         return event

  15 end

  16

  17 –事件注册, 通过此方法将响应方法注册到事件上.

  18 --@source:响应方法的所属对象

  19 --@func:响应方法

  20 function Event:Add(source, func)

  21         table.insert(self, {source, func})     

  22 end

  23

  24 –内部方法, 重写了默认__call逻辑, 当event被触发调用时, 循环执行event中注册的响应方法

  25 --@table:对象产生调用时将本身传入

  26 --@…:调用参数

  27 function Event.Call(table, …)

  28         for _, item in ipairs(table) do

  29                 –item[1]就是source, item[2]就是func响应方法

  30                 –lua 5.1中无需使用unpack(arg), 直接使用…即可

  31                 item[2](item[1], unpack(arg))

  32         end

  33 end

  34

  35 ——————以下为测试用例———————–

  36

  37 –创建一个window对象, 注册按钮的点击事件

  38 Window = {

  39         Name = “Simonw’s Window”,      

  40 }

  41

  42 function Window:Init()

  43         –注册事件, self即Window, 对象来源.

  44         Button.ClickEvent:Add(self, self.Button_OnClick)       

  45 end

  46

  47 –响应事件方法, sender即是传来的Button对象

  48 function Window:Button_OnClick(sender) 

  49         print(sender.Name..“ Click On “..self.Name)

  50 end

  51

  52 –创建一个button对象, 拥有ClickEvent这样的事件

  53 Button = {

  54         Name = “A Button”,

  55         –创建事件

  56         ClickEvent = Event:New(),

  57 }

  58

  59 –执行点击按钮的动作

  60 function Button:Click()

  61         print(‘Click begin’)

  62         –触发事件, self即sender参数

  63         self.ClickEvent(self)

  64         print(‘Click end’)

  65 end

  66

  67 –从这里执行

  68 Window:Init()

  69 Button:Click()

  70 –[[

  71 执行结果:

  72 > dofile ‘test.lua’

  73 Click begin

  74 A Button Click On Simonw’s Window

  75 Click end

  76 ]]

  77

  78 end



      
    

写作目的:(此段可跳过)
    同步Internet时间,即通过Internet的校时网站传来的数据校准本机时间。但是现在网络上查到的相关编程资料并不多,且其中多是VB和Delphi的代码,VC的代码我还没找到过。是这个东西太难了?应该不是;是太简单了?那也总该有人写吧。
   我认为,自己懂和让别人懂压根不是一回事,我写这篇文章,目的当然是后者。当然,理工科出身的河蚌不大可能像文科出身的河蚌那样修出光彩夺目的珍珠来,所以,行文有不妥之处,欢迎指正。
校时原理:
        互联网上有很多时间服务器能够提供准确的时间,我们通过连接到这样的服务器来获取时间值。这里向大家介绍一下服务器传来的数据格式先。数据一共四个字节(4 Byte),我们可以在接收数据后对它进行“重新组装”,把组装所得的值放在一个32位的整数里,这个值的意义是:自1900年1月1日0时0分0秒 至 服务器发送这个时间数据时 所经历的秒数。显然,任何一个时刻到1900年所经历的秒数是唯一的,因此,由服务器传来的时间数据即可推出现在的时间,然后用API函数调整系统的时间即可。

流程图如下:

设计目标:

        好了,我们的目标是:(没有蛀牙~)
    -_-!!
    常言说一图千言,我们还是看图吧:

程序的实现:

从技术角度来看,解决三个问题即可:
1. 通过网络通信从服务器获取时间数据。
2. 处理基于1900年的时间数据,转化为我们常见的时间形式。
3. 解决网络造成的延时问题。
下面分条讲述:
1.
通过网络通信从服务器获取时间数据。

至于接收数据,没什么可说的,这里用CSocket就可以了。

代码片断:

    CSocket sockClient;
    sockClient.Create();            //创建socket

//for debug
    m_info += "Connect server: " + strServer + " ";
    UpdateData(FALSE);
//for debug

    sockClient.Connect((LPCTSTR)strServer, 37); // strServer:时间服务器网址; 37:端口号

    DWORD dwTime = 0;                //用来存放服务器传来的标准时间数据
    unsigned char nTime[8];            //临时接收数据
    memset(nTime, 0, sizeof(nTime));

    sockClient.Receive(nTime, sizeof(nTime));    //接收服务器发送来得4个字节的数据
    sockClient.Close();                //关闭socket

//for debug
    m_info += "Connect shut down. ";
    UpdateData(FALSE);
//for debug

    dwTime += nTime[0] << 24;        //整合数据   
    dwTime += nTime[1] << 16;
    dwTime += nTime[2] << 8;
    dwTime += nTime[3];       

if(0 == dwTime)    return FALSE;

到此为止,服务器传来的时间数据经过“重新组装”已经正确放置到DWORD类型的变量 dwTime 里面了。下面我们接着对其进行必要的处理。


2.
处理基于1900年的时间数据,转化为我们常见的时间形式。

在前面我们提到,时间数据已经正确放置到变量 dwTime 里面了。那么,怎样由它得到现在的时间呢?

微软已经给我们提供了一个很好用的时间类:CTime。不过,MFC的CTime类的时间起点是基于1970年的,而dwTime 里面的秒数是从1900年计时的。

用CTime?无法由 dwTime 中的数据直接构造CTime类的对象。

用C的函数库?我尝试了多次,N次碰壁。

说起最终敲定的实现方法,其实很简单- 改变计时基准。

时间转换的方法如下:

1.  用 COleDateTime 和 COleDateTimeSpan 算出1900年1月1日0时0分0秒 到 1970年1月1日0时0分0秒 所经历的秒数 dwSec00to70。

2. 从 dwTime 中减去 dwSec00to70。此后,dwTime 所代表的就是自1970年1月1日0时0分0秒以来逝去的秒数――显然,dwTime 已经被我们转变为基于1970年的时间值了,这回可以用CTime进行处理了。

怎么样?不复杂吧。(想起了近几天屡试屡败的经历和查阅的N多资料,自己吐血先)
代码片断:

//服务器传来的数据是自从1900年以来的秒数
//取得 1900~1970 的时间差(以秒数计算) ,放在dwSpan里面
    COleDateTime t00( 1900, 1, 1, 0, 0, 0 ); // 1900.1.1 00:00:00
    COleDateTime t70( 1970, 1, 1, 0, 0, 0 ); // 1970.1.1 00:00:00

    COleDateTimeSpan ts70to00 = t70 - t00;
    DWORD dwSpan = (DWORD)ts70to00.GetTotalSeconds();
    ASSERT( dwSpan == 2208988800L );

//把时间变为基于1970年的,便于用CTime处理
    dwTime -= dwSpan;       
//考虑网络延迟因素
    dwTime += dwDely;
//构造当前时间的CTime对象
    CTime timeNow = (CTime)dwTime;

//for debug
    m_info += timeNow.Format("%Y.%m.%d  %H:%M:%S  ");
    UpdateData(FALSE);
//for debug

3. 解决网络造成的延时问题。

    在从服务器获取时间数据时,由于网络本身的不稳定性,一般会有时间上的延迟(几秒以内),这样一来,从服务器接收到的数据总早于的真实时间。解决的办法是设定一个计时器,计算出本机从开始网络连接到接收完数据所耗费的时间dwDelay,然后加到 dwTime 上进行补偿。这样一来误差就可以控制在1秒以内(如果你不用你的爱机控制导弹飞行或者航天发射,应该够用了),详见源码。

大家都知道,一个3D 场景中,我们见到的任何光辉灿烂的物体,

都是由一个一个面片组成的。而装载面片位置信息的就是其各个定点的三维坐标。这是用来在模型中存储的,而要把物体显示在屏幕上,还需要将它们转换成显示器上的二维坐标。这就需要对每个点实施一套 3 to 2 的转换公式,在Direct3D中叫做“几何流水线”(Geometry Pipeline)。

每渲染一桢,我们都要用到这条流水线把所有定点的坐标转化成当前要显示的位置。不过放心,D3D不会改变你原有的顶点坐标,变换出的顶点数据会存放在新的地方用来渲染。想一想物体,也就是面片,也就是顶点要显示在屏幕上,其位置取决于什么呢?首先它一定取决于该点在场景中的位置,然后还在于你从什么角度看,更详细一点就是我的眼睛在哪儿,我注视着哪儿,以及我的视野宽窄等等。

对于每个独立被引入程序的mesh物体,它们的坐标系、坐标原点理论上都应该是不同的,其顶点也都是用局部坐标表示的。那么要做统一的变换,首先应将它们引入到同一个坐标系下,也就是我们称之为“世界坐标系”的坐标。这个变换也因此得名世界变换(World Transform)。对物体所需要做的移动、旋转等工作也是要在此时完成的(这些本质上不就是坐标的更改么)。

经过了以上一些操作后,每个顶点(也就是每个物体)在整个场景中的位置就如你所愿确定下来了。要把它们映射到屏幕上,还要确定观察者(你可以叫他玩家、摄影机都无所谓)的位置和视角。我们是要把所有的点变换到新建立的以观察者为基准的坐标系下。这个步骤就是“视图变换”(View Transform)。实际上和后面要说的射影变换相比,这两种变换并没有什么本质区别。有时候为了效率,可以把世界变换与视图变换合并为一个世界——视图变换。这不就是说你一开始就选择观察者的位置为世界坐标系的原点,并按照视角来确定坐标轴么?

后面一步是“射影变换”(Projection Transform),有必要重点说一下。很多教材(包括MSDN)上都是假装读者已经知道为什么要有射影变换而给读者讲它的。实际上,我们要做的所有坐标转换归根结蒂是要把三维的点投影到二维的屏幕上,如图所示

经过上述两次坐标转换后,我们已经让屏幕平行于坐标轴平面了,也就是说,经过一些比例范围的调整,理论上我们能从点的三维坐标中的某两个直接得到期待已久的屏幕坐标。但是别急,此时得到的坐标绘出的图就像我们小时候画的那些画一样——没有立体感。比如上图那个矩形,因为近大远小,在我们的视野中应该看起来像个梯形。但是如果我们不做任何处理就直接把它的顶点(已经过前两重变换)投影到显示器上(假设平行于图中的XY平面)这样还是一个方方正正的矩形。

想象一下,投影实际上就是把空间中的所有点都压扁,扁到某一个平面上。这样出来的图形自然不会有透视效果。(之所以有近大远小是因为人眼的凸透镜成像,其像高是物距的减函数。这里不多说了)你可能想到让每个点像这样斜着投影,但是仔细想想,如何斜着投影呢?等你想明白了再回答这样做真的方便么?于是另一种办法就是把整个空间范围变成一个棱台(里面的点随之进行放缩)。

相对来说把较远端缩小会造成数据的不准确,因此采用放大较近端。对每个点,我们进行最后一步变换就是根据其远近程度进行一下放缩。

D3D把剪切也纳入此流水线中,尽管它没对顶点作任何变换,只是剔出那些不用的点。

以上就是D3D中的几何流水线。幸运的是,我们并不需要自己去写代码来完成这些转换。实际上我们只需要设计好参数,调用相应的D3D函数设置上面提到的各种决定因素,它会在渲染画面的时候把每个顶点自动转化成所需的屏幕坐标的。正因为这一套流水线操作的通用性和规范性,各种3D渲染引擎都将它封装了,而当代很多先进的显卡都将其固化到硬件线路上,这样大大提高了渲染速度。

下面我们来看看一些具体的实施。在计算机图形学中,坐标的变换通常是通过与一个矩阵(Matrix)相乘来实现的。基本变换包括平移、缩放、旋转都用此方法完成,其他任何的变换,包括不同坐标系之间的互化,也都是通过这三种基本转换完成的。因此说,Matrix无处不在 , 在我们的周围,就在这间屋子里。你能在窗户往外看到它,在电视里看到它。当你上班,去教堂或者缴税你可以感觉到它。你眼前的世界让你看不到真实……(和我们说的Matrix不大一样,不过多少有点这个意思吧)。具体到三维坐标系中,定义某点的坐标为(X,Y,Z)则用(X,Y,Z,W)乘以一个相应的4X4矩阵就可以得到新的坐标(X',Y',Z',W'),这里的W自有用处,一般是1。还有一点很重要,一个矩阵就代表着一重变换,而几个矩阵的乘积就代表着多重变换的合变换。这点用处很大,读者会慢慢体会到。

那么在这条流水线中,按规范我们至少需要三个矩阵来实现以上三步变换,也就是世界矩阵(World Matrix)、视矩阵(View Matrix)以及射影矩阵(Projection Matirx)。

世界矩阵有时候需要我们自己填写,根据我们的各种变换需要来填写一个D3DXMATRIX结构体(其成员就是各行各列的数值),具体方法MSDN上有详细讲解,这里不多做赘述了。之后通过调用IDirect3DDevice9::SetTransform( D3DTRANSFORMSTATETYPE State,CONST D3DMATRIX *pMatrix )设置世界矩阵为你填好的那个。参数意义如下:

D3DTRANSFORMSTATETYPE State
代表你要设置的变换类型。D3DTS_WORLD,D3DTS_VIEW,D3DTS_PROJECTION分别表示要射知识界、视图、射影三种变换

CONST D3DMATRIX *pMatrix
指向一个矩阵结构的指针,就是你所要用到的矩阵。

后面的两个矩阵也要通过此函数设置。D3D中,三个变换矩阵是要存放在固定位置的,每次执行流水线,D3D就依次从这三个位置读取矩阵信息,并乘以所有的点,得到新的点的坐标,这个过程是不用我们操心的。我们调用SetTransform()就是要把填充好的矩阵放进这三个位置中的某一个,第一个参数表示了哪一个。

在设置视矩阵时,我们先要很清楚地(在脑子里或纸上)建立好“视坐标系”。这个坐标系以观察着为原点,沿着视线方向(观察着——注视点方向)为纵深方向(也就是Z轴方向)。仅有两个点还不足以确定一个三维坐标系,我们还需要一个参考点,能与另两个点构成某一个坐标平面。这样的坐标系构件起来后,就可以根据两个坐标系的变换填充视矩阵了。D3D提供了函数

D3DXMATRIX *D3DXMatrixLookAtLH(
D3DXMATRIX *pOut,
CONST D3DXVECTOR3 *pEye,
CONST D3DXVECTOR3 *pAt,
CONST D3DXVECTOR3 *pUp
);

或 D3DXMATRIX *D3DXMatrixLookAtLH( 参数同 ),区别仅在于前者用于左手系而后者用于右手系。该函数自动填充一个矩阵,参数依次是将要填充的矩阵以及上面说到的三个点,这里三个点构成视坐标系的YoZ平面。别忘了调用SetTransform()把这个矩阵交给D3D。经过上一步被统一了坐标的各个顶点将被这个矩阵转到视坐标中。

第三步要将点乘上一个射影矩阵,这个矩阵将越近的点放得越大。填充这个矩阵我们用函数

D3DXMATRIX *D3DXMatrixPerspectiveFovLH(
D3DXMATRIX *pOut,
FLOAT fovY,
FLOAT Aspect,
FLOAT zn,
FLOAT zf
);

或 D3DXMATRIX *D3DXMatrixPerspectiveFovLH( 参数同 ),区别同上面一样。第一个参数仍然是输出矩阵。第二个描述了在Y轴上的视角,弧度制表示,可以想象,视角越大,近端被抻拉的比例就越大。下一个参数是视图区的长宽比。后面两个参数就是最近视平面和最远视平面的位置,用它们的Z坐标(Z坐标的值在射影变换前后是不变的)表示。这两个平面的意义将在下一步说到。

最后说一下这条流水线的倒数第一步——剪切。剪切就是把理论上根本不该看到的点从渲染元中剔除掉(这里不包括因遮挡关系产生的图形的剪切以及隐面消除),用过DirectDraw的朋友很容易想到屏幕范围以外的就是这样的点。在3D世界里,还存在一个最近视平面和一个最远视平面,它们共同组成了一个视图截锥(Viewing Frustum)。对于这个东西,微软有个很好的说法:就好像你在一间黑屋子里向外看,窗户的四个边圈定了视图范围,并且窗户所在平面之前的物体是看不见的(黑屋子里的东西是看不见的),窗户所在的平面就是最近视平面;而且我们并不能看到无限远,总要有个最远视平面。这六个平面视可以根据需要设定的,它们组成了视截锥——下图中的蓝色范围。

可以想象,刚才进行的射影变换也可以说是把视图截锥这个棱台挤压成长方体的过程。读者还能发现,上述D3DXMatrixPerspectiveFovLH( )的参数实际上是描述视截锥的。你会觉得这个蓝色的东西很有用,它与射影变换以及剪切都有着异常紧密的联系。

以上,如图所示,就是一个顶点要被真正用于渲染所经历的四重门。笔者没有介绍多少算法,以及如何推导这几个矩阵。关于这些,网上有大量的文章可供参考,MSDN讲得更加详细,那些才是深入了解的工具,不过笔者相信读者朋友都有这个能力自己推导。本篇旨在阐述一些笔者认为比较重要的概念性问题,希望能给读者一个清晰的思路。欢迎大家来信与我讨论。

(四)一些问题的讨论

  前面几章的内容都是服务的一些通用的编写原理,但里面隐含着一些问题,编写简单的服务时看不出来,但遇到复杂的应用就会出现一些问题,所以本章就是用来分析、解决这些问题的,适用于高级应用的开发人员。我这一章的内容都是经过实验得到的,很有实际意义。

  我在第一章里面就说过,是由一个服务的主线程执行CtrlHandler函数,它将收到各种控制命令,但是真正处理命令,执行操作的是ServiceMain的线程。现在,当一个SERVICE_CONTROL_STOP到达之后,你作为一个开发者,要怎样停止这个服务?在我看过的一些源代码里,大部分只是简单的调用TerminateThread函数去强行杀掉服务进程。但应该稍稍有点线程编程的常识就应该知道TerminateThread函数是可用的调用中最为糟糕的一个,服务线程将得不到任何机会去做应该的清理工作,诸如清除内存、释放核心对象,Dlls也得不到任何线程已经被毁的通知。

  所以停止服务的适当方法是以某种方式激活服务线程,让它停止继续提供服务功能,然后执行完当前操作和清除工作后返回。这就表示你必须在CtrlHandler线程和ServiceMain线程之间执行适当的线程通信。现在已知的最好的内部线程通信机制是I/O Completion Port(I/O 完成端口),假如你编写的是一个大型的服务,需要同时处理为数众多的请求,并且运行在多处理器系统上面,这个模型就可以提供最佳的系统性能。但也正因为它的复杂性较高,在小规模的应用上面不值得花费很多的时间和精力,这时作为开发者可以适当的选取其它的通信方式,诸如异步过程调用队列、套接字和窗口消息,以适应实际情况。

  开发服务时的另外一个重要问题就是调用SetServiceStatus函数时的所有状态报告问题。很多的服务开发者为了在什么时候调用SetServiceStatus的问题而常常产生争论,一般推荐的方法就是:先调用SetServiceStatus函数,报告SERVICE_STOP_PENDING状态,然后将控制代码传给服务线程或者再建立一个新的线程,让它去继续执行操作,当该线程即将执行完操作之前,再由它将服务的状态设置成SERVICE_STOPPED,然后服务正好停止。

  上面的主意从两个方面来讲还是很不错的。首先服务可以立即确认收到了控制代码,并将在它认为适当的时候进行处理;然后就是因为前面说过的,执行CtrlHandler函数的是主线程,如果按照这种工作方法,CtrlHandler函数可以迅速的返回,不会影响到其它服务可能收到的控制请求,对含有多个服务的程序来说,响应各个服务的控制代码的速度会大大的提高。可是,随之而来的是问题—— race condition 即“竞争条件”的产生。

  摆在下面的就是一个竞争条件的例子,我花了一点时间来修改我的基本服务的代码,意图故意引发“竞争条件”的发生。我添加了一个线程,CtrlHandler函数的线程在收到请求后立刻作出反应,将当前的服务状态设置成“请求正在被处理”即..._PENDING,然后由我添加的线程在睡眠了5秒之后再将服务状态设置成“请求已完成”状态——以模拟服务正在处理一些不可中止的事件,只有处理完成后才会更改服务的状态。一切就绪之后,我尝试在短时间内连续发送两个“暂停”请求,如果“竞争条件”不存在的话应该只有先发送的那个请求能够到达SCM,而另一个则应该返回请求发送失败的信息,天下太平。

  事实上很不幸的,我成功了。当我在两个不同的“命令提示符”窗口分别同样的输入下面的命令:

net pause kservice

  之后在“事件查看器”里面,我找到了我的服务在“应用程序日志”里添加的事件记录,结果是我得到了这样的事件列表:

SERVICE_PAUSE_PENDING
SERVICE_PAUSE_PENDING
SERVICE_PAUSED
SERVICE_PAUSED

  看上去很奇怪是不是?因为服务处于正在暂停状态的时候,它不应该被再次暂停的。但事实摆在眼前,很多服务都曾明确的报告过上面的顺序状态。我曾经认为这时SCM应该说些什么或做些什么,以阻止“竞争状态”的出现,但实验结果告诉我SCM似乎对此无能为力,因为它不能控制状态代码在什么时候被发送。当用户使用“管理工具”里面的“服务”工具来管理服务的状态的时候,在一个“暂停”请求已经发出之后不能再次用这个工具向它发出“暂停”请求,如果正在暂停服务,会有一个对话框出现,阻止你按下它后面的“服务”工具的工具栏上的任何按钮,如果已经暂停,“暂停“按钮将变成灰色。但是这时用命令行工具 net.exe 就可以很顺利地将暂停请求再次送到服务。证据就是我添加的其他事件记录里面记下了SetServiceStatus的调用全都成功了,这更进一步的说明了我提交的两个暂停请求都经过SCM,然后到达了我的服务。

  接下来我又进行了其它的测试,例如先发送“暂停”请求,后发送“停止”请求,和先发送“停止”请求,再发送“暂停”或“停止”请求。前一种情况更加糟糕,先发送的“暂停”请求和后发送的“停止”请求都没有得到什么好下场,虽然SCM老老实实的先暂停了服务,后停止了服务,但 net.exe 的两个实例的调用均告失败。不过在测试先发送停止“请求”的时候,所有的现象都表示这两个请求只有先发送的“停止”到达了SCM,这还算是个好消息...

  为了解决这个问题,当服务得到一个“停止”“暂停”或“继续”请求的时候,应该首先检查服务是否已经在处理另外的一个请求,如果是,就依情况而定:是不调用SetServiceStatus直接返回还是暂时忍耐直到前一个请求动作完成再调用SetServiceStatus,这是你作为一个开发者要自己决定的。

  如果说前面的问题已经足够麻烦了,下面的问题会令你觉得更加怪异。它其实是一种可以解决上面的问题的方法:当CtrlHandler函数的线程收到SERVICE_PAUSE_PENDING请求之后,它调用SetServiceStatus报告服务正在暂停,然后由它自己调用SuspendThread来暂停服务的线程,然后再由它自己调用SetServiceStatus报告服务已经被暂停。这样做的确避免了“竞争条件”的出现,因为所有的工作都是由一个函数来做的。现在需要注意的不是“竞争条件”而是服务本身,挂起服务的线程会不会暂停服务呢?答案是会的。但是暂停服务意味着什么呢?

  假如我的服务是用来处理网络客户的请求,那么暂停对于我的服务来说应该是停止接受新的请求。如果我现在正处在处理请求的过程中,那么我应该怎么办?也许我应该结束它,使客户不至于无限期悬挂。但如果我只是简单的调用SuspendThread,那么不排除服务线程正处于孤立的中间状态的可能,或者正在调用malloc函数去尝试分配内存,如果运行在同一个进程中的另一个服务也调内存分配函数,那么它也会被挂起,这肯定不是我期望的结果。

  还有一个问题:用户认为自己可以被允许去停止一个已经被暂停了的服务吗?我认为是这样的,而且很明显的,微软也这么认为。因为当我们在“服务”管理工具里面选中一个已暂停的服务之后,“停止”按钮是可以被按下的。但我要怎样停止一个由于线程被挂起才处于暂停状态的服务呢?不,不要TerminateThread,请别跟我提起它。

  解决这所有的混乱的最好方法,就是有一个能够把所有事做好的线程,而且它应该是服务线程,而不是CtrlHandler线程。当CtrlHandler函数得到控制代码之后,它要迅速的将控制代码通过线程内部通讯手段送到服务线程中排队,然后CtrlHandler函数就应该返回,它决不应该调SetServiceStatus。这样,服务可以随心所欲的控制每件事情,因为没有什么比它更有发言权的了,没有“竞争条件”。服务决定暂停意味着什么,服务能够允许自己在已经暂停的情况下停止,服务决定什么内部通讯机制是最好的——并且CtrlHandler函数必须简单的与这种机制相一致。

  事情没有完美的,上面的方法也不例外,它仅有一个小缺陷:就是假定当服务收到控制代码后,在较短的时间内就能做出应有的响应。如果服务线程正在忙于处理一个客户的请求,控制代码可能进入等待队列,而且SetServiceStatus可能也无法迅速的被调用。如果真是这样的话,负责发送通知的SCP可能会认为你的服务已经失败,并向用户报告一个消息框。事实上服务并没有失败,而且也不会被终止。

  这种情况够糟糕了,没有用户会去责怪SCP——虽然SCP将他们引导到了错误的状态,他们只会责怪服务的作者——就是我或你...因此,在服务中怎么做才能防止这种问题发生呢?很简单,使服务快速有效的运行,并且总保持一个活动线程等待去处理控制代码。

  说起来好像很容易,但实际做起来就被那么简单了,这也不是我能够向各位解释的了,只有认真的调试自己的服务,才能找出最为适合处理方法。所以我的文章也真的到了该结束的时候了,感谢各位的浏览。如果我有什么地方说的不对,请不吝赐教,谢谢。

  下面是我写的一个服务的源代码,没什么功能,只能启动、停止和安装。

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <tchar.h>

#define SZAPPNAME "basicservice"
#define SZSERVICENAME "KService"
#define SZSERVICEDISPLAYNAME "KService"
#define SZDEPENDENCIES ""

void WINAPI KServiceMain(DWORD argc, LPTSTR * argv);
void InstallService(const char * szServiceName);
void LogEvent(LPCTSTR pFormat, ...);
void Start();
void Stop();

SERVICE_STATUS ssStatus;
SERVICE_STATUS_HANDLE sshStatusHandle;

int main(int argc, char * argv[])
{
  if ((argc==2) && (::strcmp(argv[1]+1, "install")==0))
  {
    InstallService("KService");
    return 0;
  }

SERVICE_TABLE_ENTRY   service_table_entry[] =
  {
    { "KService", KServiceMain },
    { NULL, NULL }
  };
  ::StartServiceCtrlDispatcher(service_table_entry);
  return 0;
}

void InstallService(const char * szServiceName)
{
  SC_HANDLE handle = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
  char szFilename[256];
  ::GetModuleFileName(NULL, szFilename, 255);
  SC_HANDLE hService = ::CreateService(handle, szServiceName,
  szServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
  SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, szFilename, NULL,
  NULL, NULL, NULL, NULL);
  ::CloseServiceHandle(hService);
  ::CloseServiceHandle(handle);
}

SERVICE_STATUS servicestatus;
SERVICE_STATUS_HANDLE servicestatushandle;

void WINAPI ServiceCtrlHandler(DWORD dwControl)
{
  switch (dwControl)
  {

//下面虽然添加了暂停、继续等请求的处理代码,但没有实际作用
  //这是为什么呢?到了下面的KServiceMain函数里面就明白了...

case SERVICE_CONTROL_PAUSE:
    servicestatus.dwCurrentState = SERVICE_PAUSE_PENDING;
    // TODO: add code to set dwCheckPoint & dwWaitHint
    // This value need to try a lot to confirm
    // ...
    ::SetServiceStatus(servicestatushandle, &servicestatus);
    // TODO: add code to pause the service
    // not called in this service
    // ...
    servicestatus.dwCurrentState = SERVICE_PAUSED;
    // TODO: add code to set dwCheckPoint & dwWaitHint to 0
    break;

  case SERVICE_CONTROL_CONTINUE:
    servicestatus.dwCurrentState = SERVICE_CONTINUE_PENDING;
    // TODO: add code to set dwCheckPoint & dwWaitHint
    ::SetServiceStatus(servicestatushandle, &servicestatus);
    // TODO: add code to unpause the service
    // not called in this service
    // ...
    servicestatus.dwCurrentState = SERVICE_RUNNING;
    // TODO: add code to set dwCheckPoint & dwWaitHint to 0
break;

  case SERVICE_CONTROL_STOP:
    servicestatus.dwCurrentState = SERVICE_STOP_PENDING;
    // TODO: add code to set dwCheckPoint & dwWaitHint
::SetServiceStatus(servicestatushandle, &servicestatus);
    // TODO: add code to stop the service
    Stop();
    servicestatus.dwCurrentState = SERVICE_STOPPED;
    // TODO: add code to set dwCheckPoint & dwWaitHint to 0
break;

  case SERVICE_CONTROL_SHUTDOWN:
    // TODO: add code for system shutdown
    // as quick as possible
    break;

  case SERVICE_CONTROL_INTERROGATE:
    // TODO: add code to set the service status
    // ...
    servicestatus.dwCurrentState = SERVICE_RUNNING;
    break;
  }
  ::SetServiceStatus(servicestatushandle, &servicestatus);
}

void WINAPI KServiceMain(DWORD argc, LPTSTR * argv)
{
  servicestatus.dwServiceType = SERVICE_WIN32;
  servicestatus.dwCurrentState = SERVICE_START_PENDING;
  servicestatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;//上面的问题的答案就在这里
  servicestatus.dwWin32ExitCode = 0;
  servicestatus.dwServiceSpecificExitCode = 0;
  servicestatus.dwCheckPoint = 0;
  servicestatus.dwWaitHint = 0;

  servicestatushandle =
  ::RegisterServiceCtrlHandler("KService", ServiceCtrlHandler);
  if (servicestatushandle == (SERVICE_STATUS_HANDLE)0)
  {
    return;
  }

  bool bInitialized = false;
  // Initialize the service
  // ...
  Start();

  bInitialized = true;

  servicestatus.dwCheckPoint = 0;
  servicestatus.dwWaitHint = 0;
  if (!bInitialized)
  {
    servicestatus.dwCurrentState = SERVICE_STOPPED;
    servicestatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
    servicestatus.dwServiceSpecificExitCode = 1;
  }
  else
  {
    servicestatus.dwCurrentState = SERVICE_RUNNING;
  }
  ::SetServiceStatus(servicestatushandle, &servicestatus);
  return;
}

void Start()
{
  LogEvent("Service Starting...");
}

void LogEvent(LPCTSTR pFormat, ...)
{
  TCHAR chMsg[256];
  HANDLE hEventSource;
  LPTSTR lpszStrings[1];
  va_list pArg;

  va_start(pArg, pFormat);
  _vstprintf(chMsg, pFormat, pArg);
  va_end(pArg);

  lpszStrings[0] = chMsg;

  if (1)
  {
    // Get a handle to use with ReportEvent().
    hEventSource = RegisterEventSource(NULL, "KService");
    if (hEventSource != NULL)
    {
    // Write to event log.
      ReportEvent(hEventSource, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
      DeregisterEventSource(hEventSource);
    }
  }
  else
  {
    // As we are not running as a service, just write the error to the console.
    _putts(chMsg);
  }
}

void Stop()
{
  LogEvent("Service Stoped.");
}

(三)对服务的深入讨论之下

  现在我们还剩下一个函数可以在细节上讨论,那就是服务的CtrlHandler函数。

  当调用RegisterServiceCtrlHandler函数时,SCM得到并保存这个回调函数的地址。一个SCP调一个告诉SCM如何去控制服务的Win32函数,现在已经有10个预定义的控制请求:

Control code

Meaning

SERVICE_CONTROL_STOP
Requests the service to stop. The hService handle must have SERVICE_STOP access.

SERVICE_CONTROL_PAUSE
Requests the service to pause. The hService handle must have SERVICE_PAUSE_CONTINUE access.

SERVICE_CONTROL_CONTINUE
Requests the paused service to resume. The hService handle must have SERVICE_PAUSE_CONTINUE access.

SERVICE_CONTROL_INTERROGATE
Requests the service to update immediately its current status information to the service control manager. The hService handle must have SERVICE_INTERROGATE access.

SERVICE_CONTROL_SHUTDOWN
Requests the service to perform cleanup tasks, because the system is shutting down.
For more information, see Remarks.

SERVICE_CONTROL_PARAMCHANGE
Windows 2000: Requests the service to reread its startup parameters. The hService handle must have SERVICE_PAUSE_CONTINUE access.

SERVICE_CONTROL_NETBINDCHANGE
Windows 2000: Requests the service to update its network binding. The hService handle must have SERVICE_PAUSE_CONTINUE access.

SERVICE_CONTROL_NETBINDREMOVE
Windows 2000: Notifies a network service that a component for binding has been removed. The service should reread its binding information and unbind from the removed component.

SERVICE_CONTROL_NETBINDENABLE
Windows 2000: Notifies a network service that a disabled binding has been enabled. The service should reread its binding information and add the new binding.

SERVICE_CONTROL_NETBINDDISABLE
Windows 2000: Notifies a network service that one of its bindings has been disabled. The service should reread its binding information and remove the binding.

  上表中标有Windows 2000字样的就是2000中新添加的控制代码。除了这些代码之外,服务也可以接受用户定义的,范围在128-255之间的代码。

  当CtrlHandler函数收到一个SERVICE_CONTROL_STOP、SERVICE_CONTROL_PAUSE、
SERVICE_CONTROL_CONTINUE控制代码的时候,SetServiceStatus必须被调用去确认这个代码,并指定你认为服务处理这个状态变化所需要的时间。

  例如:你的服务收到了停止请求,首先要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOP_PENDING,这样可以使SCM确定你已经收到了控制代码。当一个服务的暂停或停止操作正在执行的时候,必须指定你认为这种操作所需要的时间:这是因为一个服务也许不能立即改变它的状态,它可能必须等待一个网络请求被完成或者数据被刷新到一个驱动器上。指定时间的方法就像我上一章说的那样,用成员dwCheckPoint和dwWaitHint来指明它完成状态改变所需要的时间。如果需要,可以用增加dwCheckPoint成员的值和设置dwWaitHint成员的值去指明你期待的服务到达下一步的时间的方式周期性的报告进展情况。

  当整个启动的过程完成之后,要再一次调用SetServiceStatus。这时就要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOPPED,当报告状态代码的同时,一定要把成员dwCheckPoint和dwWaitHint设置为0,因为服务已经完成了它的状态变化。暂停或继续服务的时候方法也一样。

  当CtrlHandler函数收到一个SERVICE_CONTROL_INTERROGATE控制代码的时候,服务将简单的将dwCurrentState成员设置成服务当前的状态,同时,把成员dwCheckPoint和dwWaitHint设置为0,然后再调用SetServiceStatus就可以了。

  在操作系统关闭的时候,CtrlHandler函数收到一个SERVICE_CONTROL_SHUTDOWN控制代码。服务根本无须回应这个代码,因为系统即将关闭。它将执行保存数据所需要的最小行动集,这是为了确定机器能及时关闭。缺省时系统只给很少的时间去关闭所有的服务,MSDN里面说大概是20秒的时间,不过那可能是Windows NT 4的设置,在我的Windows 2000 Server里这个时间是10秒,你可以手动的修改这个数值,它被记录在HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control子键里面的WaitToKillServiceTimeout,单位是毫秒。

  当CtrlHandler函数收到任何用户定义的代码时,它应该执行期望的用户自定义行动。除非用户自定义的行动要强制服务去暂停、继续或停止,否则不调SetServiceStatus函数。如果用户定义的行动强迫服务的状态发生变化,SetServiceStatus将被调用去设置dwCurrentState、dwCheckPoint和dwWaitHint,具体控制代码和前面说的一样。

  如果你的CtrlHandler函数需要很长的时间执行操作的话,千万要注意:假如CtrlHandler函数在30秒内没有返回的话,SCM将返回一个错误,这不是我们所期望的。所以如果出现上述情况,最好的办法是再建立一个线程,让它去继续执行操作,以便使得CtrlHandler函数能够迅速的返回。例如,当收到一个SERVICE_CONTROL_STOP请求的时候,就像上面说的一样,服务可能正在等待一个网络请求被完成或者数据被刷新到一个驱动器上,而这些操作所需要的时间是你不能估计的,那么就要建立一个新的线程等待操作完成后执行停止命令,CtrlHandler函数在返回之前仍然要报告SERVICE_STOP_PENDING状态,当新的线程执行完操作之后,再由它将服务的状态设置成SERVICE_STOPPED。如果当前操作的时间可以估计的到就不要这样做,仍然使用前面交待的方法处理。

CtrlHandler函数我就先讲这些,下面说说服务怎么安装。一个服务程序可以使用CreateService函数将服务的信息添加到SCM的数据库。

SC_HANDLE CreateService(
SC_HANDLE hSCManager, // handle to SCM database
LPCTSTR lpServiceName, // name of service to start
LPCTSTR lpDisplayName, // display name
DWORD dwDesiredAccess, // type of access to service
DWORD dwServiceType, // type of service
DWORD dwStartType, // when to start service
DWORD dwErrorControl, // severity of service failure
LPCTSTR lpBinaryPathName, // name of binary file
LPCTSTR lpLoadOrderGroup, // name of load ordering group
LPDWORD lpdwTagId, // tag identifier
LPCTSTR lpDependencies, // array of dependency names
LPCTSTR lpServiceStartName, // account name
LPCTSTR lpPassword // account password
);

hSCManager是一个标示SCM数据库的句柄,可以简单的通过调用OpenSCManager得到。

SC_HANDLE OpenSCManager(
LPCTSTR lpMachineName, // computer name
LPCTSTR lpDatabaseName, // SCM database name
DWORD dwDesiredAccess // access type
);

lpMachineName是目标机器的名字,还记得我在第一章里说过可以在其它的机器上面安装服务吗?这就是实现的方法。对方机器名字必须以“\\”开始。如果传递NULL或者一个空的字符串的话就默认是本机。

lpDatabaseName是目标机器上面SCM数据库的名字,但MSDN里面说这个参数要默认的设置成SERVICES_ACTIVE_DATABASE,如果传递NULL,就默认的打开SERVICES_ACTIVE_DATABASE。所以我还没有真的搞明白这个参数的存在意义,总之使用的时候传递NULL就行了。

dwDesiredAccess是SCM数据库的访问权限,具体值见下表:

Object access

Description

SC_MANAGER_ALL_ACCESS
Includes STANDARD_RIGHTS_REQUIRED, in addition to all of the access types listed in this table.

SC_MANAGER_CONNECT
Enables connecting to the service control manager.

SC_MANAGER_CREATE_SERVICE
Enables calling of the CreateService function to create a service object and add it to the database.

SC_MANAGER_ENUMERATE_SERVICE
Enables calling of the EnumServicesStatus function to list the services that are in the database.

SC_MANAGER_LOCK
Enables calling of the LockServiceDatabase function to acquire a lock on the database.

SC_MANAGER_QUERY_LOCK_STATUS
Enables calling of the QueryServiceLockStatus function to retrieve the lock status information for the database.

  想要获得访问权限的话,似乎没那么复杂。MSDN里面说所有进程都被允许获得对所有SCM数据库的SC_MANAGER_CONNECT, SC_MANAGER_ENUMERATE_SERVICE, and SC_MANAGER_QUERY_LOCK_STATUS权限,这些权限使得你可以连接SCM数据库,枚举目标机器上安装的服务和查询目标数据库是否已被锁住。但如果要创建服务,首先你需要拥有目标机器的管理员权限,一般的传递SC_MANAGER_ALL_ACCESS就可以了。这个函数返回的句柄可以被CloseServiceHandle函数关闭。

lpServiceName是服务的名字,lpDisplayName是服务在“服务”管理工具里显示的名字。

dwDesiredAccess也是访问的权限,有一个比上面的还长的多的一个表,各位自己查MSDN吧。我们要安装服务,仍然简单的传递SC_MANAGER_ALL_ACCESS。

dwServiceType是指你的服务是否和其它的进程相关联,一般是SERVICE_WIN32_OWN_PROCESS,表示不和任何进程相关联。如果你确认你的服务需要和某些进程相关联,就设置成SERVICE_WIN32_SHARE_PROCESS。当你的服务要和桌面相关联的时候,需要设置成SERVICE_INTERACTIVE_PROCESS。

dwStartType是服务的启动方式。服务有三种启动方式,分别是“自动(SERVICE_AUTO_START)”“手动(SERVICE_DEMAND_START)”和“禁用(SERVICE_DISABLED)”。在MSDN里还有另外的两种方式,不过是专为驱动程序设置的。

dwErrorControl决定服务如果在系统启动的时候启动失败的话要怎么办。

意义

SERVICE_ERROR_IGNORE
启动程序记录错误发生,但继续启动。

SERVICE_ERROR_NORMAL
启动程序记录错误发生,并弹出一个消息框,但仍继续启动

SERVICE_ERROR_SEVERE
启动程序记录错误发生,如果是以last-known-good configuration启动的话,启动会继续。否则会以last-known-good configuration重新启动计算机。

SERVICE_ERROR_CRITICAL
启动程序记录错误发生,如果可能的话。如果是以last-known-good configuration启动的话,启动会失败。否则会以last-known-good configuration重新启动计算机。好严重的错误啊。

lpBinaryPathName是服务程序的路径。MSDN里面特别提到如果服务路径里面有空格的话一定要将路径用引号引起来。例如"d:\\my share\\myservice.exe"就一定要指定为"\"d:\\my share\\myservice.exe\""。

lpLoadOrderGroup的意义在于,如果有一组服务要按照一定的顺序启动的话,这个参数用于指定一个组名用于标志这个启动顺序组,不过我还没有用过这个参数。你的服务如果不属于任何启动顺序组,只要传递NULL或者一个空的字符串就行了。

lpdwTagId是应用了上面的参数之后要指定的值,专用于驱动程序,与本文内容无关。传递NULL。

lpDependencies标示一个字符串数组,用于指明一串服务的名字或者一个启动顺序组。当与一个启动顺序组建立关联的时候,这个参数的含义就是只有你指定的启动顺序组里有至少一个经过对整个组里所有的成员已经全部尝试过启动后,有至少一个成员成功启动,你的服务才能启动。不需要建立依存关系的话,仍是传递NULL或者一个空的字符串。但如果你要指定启动顺序组的话,必须为组名加上SC_GROUP_IDENTIFIER前缀,因为组名和服务名是共享一个命名空间的。

lpServiceStartName是服务的启动账号,如果你设置你的服务的关联类型是SERVICE_WIN32_OWN_PROCESS的话,你需要以DomainName\UserName的格式指定用户名,如果这个账户在你本机的话,用.\UserName就可以指定。如果传递NULL的话,会以本地的系统账户登陆。如果是Win NT 4.0或更早的版本的话,如果你指定了SERVICE_WIN32_SHARE_PROCESS,就必须传递.\System指定服务使用本地的系统账户。最后,如果你指定了SERVICE_INTERACTIVE_PROCESS,你必须使服务运行在本机系统账户。

  看名字就知道了,lpPassword是账户的密码。如果指定系统账户的话,传递NULL。如果账户没有密码的话,传递空字符串。

  总之服务的基本原理就是这样子了,到了这里这篇文章似乎可以告一段落了,但实际上还有很多内容必须要讨论,所以我还不能草草收笔,敬请关注下一章。