我们是否总能制定一个伟大的、并且能持续坚持的目标?如果现在有这样一个简单的系统,它能够时刻关注着我们的进步,并且能在偏离目标时提醒我们,那怎么样?

我知道这听起来很棒,但似乎又不太现实。如果你对自己有着足够的责任心,其实这是可行的。

我的经验

我使用这个系统已经差不多一年了,而且已经证实了它的价值。我曾经总是设定远大的计划,但当我心烦意乱或者为其他事情忙得不可开交的时候,我就会让这些计划坐冷板凳,而这个过程确实让我很疲倦。

但是现在我做到了,即使有重要的或者新事物在吸引我的注意力,我仍能够完成目标。

我虽仍然不能完全地左右时间,也得承认时间管理对我来说也是一项要不断进行的工作。尽管如此,在没有额外增加工作时间的前提下,我的“生产力”提高了,我的空闲时间也就多了。

小心帕金森定律

当你在计划日程时,一定要记得帕金森定律:工作会自动地膨胀,占满一个人所有可用的时间。这意味着你要考虑以下几件事:

就每一件具体的事情,给自己定一个截止日期。如果不这样做,你很有可能会花比你实际需要多几倍的时间去做。

计划自己的时间,要把额外的任务、计划阅读、运动、或者家人时间都考虑进去。当你没有空闲时间时,如果有必要的话就随身携带一个未完成任务的列表。我所说的空闲时间,指的是你的确是无事可做,或者有些无聊了。所以如果你有空闲时间,不妨做些有计划的休闲活动,或者做些思考。如果你无计划的放松时间,尤其是已经给接下来的工作确定了截止日期,那你就很有可能打破截止日期,浪费时间了。

请对日常事务无情

一份日常计划表其实是在很多的试验和错误下建立的。通过第一次实验性的草图你才会意识的你忽略的某些事情。有时你也会意识到由于某些不可避免的例外,你的计划表变得不切实际。因此就要不断的去试验,去修改,直到它变得可行。

我们都知道自己身体的运转是基于生物钟,或者叫做24小时循环。当我们在每天同样的时间里睡觉,吃饭,运动,我们的身体就会去学着将我们体内系统调整到最佳状态。举几个例子:

当你确定每天都在相同的时间进餐时,你的身体就会提前释放一定的消化酶,这会极大的促进你的消化。

如果你受过训练,每天在一个特定的时间学习,你的头脑就会像激光一样聚焦于一点。

每当到你要参加体育运动的时间时,你的身体就会增加能量水平。

一旦你把每天的睡眠和起床时间定为规律,你就会能快的入睡,同样也能醒得更早。

使用情绪触发器

那个能判断你是否在朝着你的目标前进的人就是你自己。当你没能达到你的潜能或者目标时,你自己就会觉得被轻蔑,羞愧或者感到内疚。这些想法其实是很有价值的——它们可以让你产生动力。尤其是内疚,它比其他任何感情都更能提醒你需要负责任。

为了能给自己打基础,并对自己的进步做个评估。我之前写了一篇《改变生活的七个问题》,文章也飞快的变成了本站内最受欢迎的文章。原因就在于它们很重要,也很有效。我们可以每周末问问自己这些问题,也可以通过每天那些基本的事情而将问题长远化。

问问自己下面的问题:

我今天有尽可能的多完成一些任务吗?

有没有什么其他的事情需要我去做呢?

我会为今天所做的某些事情感到羞愧吗?

我今天是否已经有效地利用了时间?

我能利用今天去促进明天吗?

有什么事我要绝对避免吗?

我明天会做什么令我感到更骄傲的事情吗?

利用这些问题的答案来修正自己的日程表,将浪费的时间去掉,降低那些无效的事情所占用的时间。你会注意到只要你日复一日的进步,你就会感觉自己越来越棒。你的感觉不会说谎,因为那些自内反省的问题就是来帮你寻找真相的。当你以最佳状态工作时,你就会为自己的工作能力感到伟大。

紧记在心,为了真的使你的对自己的效率感到开心,你的日计划和周计划需要包含每一件事情,比如任务,短期目标,长期目标,家人时间,或许甚至包括用于宗教和精神的时间。当你重新修正了你的日程表,通过你对每件事花多少时间,你就会清楚的看到它对你意味有多重要。

作者: James Hobart

翻译: spark.bbs@bbs.nankai.edu.cn

日期: 2001-3-23

转自:http://nku.nankai.edu.cn/cim/students/doctor/spark/articles/PrinciplesOfGUIDesign.htm

译序:我在网上查找中文的 GUI 设计规范,居然没有详细一点的,一篇泛泛而谈的文章却被转载了几十次。只好退而求其次,找来这篇英文的,顺带翻译成中文,以方便国内编程人员。

+++++++++++++++++++++++++++++++++++++++++++++++++

 

图形用户界面( GUI )已经成为用户界面的首选,但不论 GUI 如何流行,令人诧异的是没几个程序有好的界面设计。另外,想找一些介绍如何编制出色用户界面的材料也相当困难。本文给出了出色界面应该如何和不该如何的一些最重要的基本规则。

 

无论如何,开始谈论什么是好的界面设计之前,我需要解释一下导致差的界面设计的因素。这样,如果你试图偏离那些已经被证明是好的界面设计的原则时,你就会知道是什么导致你如此,我希望,你能回到好的界面设计上来。

 

忽略了用户

开发者常常只设计他们自己知道的,而非用户知道的东西。这个古老的问题在软件开发的多个领域发生,例如测试、文档编写等等。设计界面时这样会更有害,因为用户在使用产品的时候会立刻感到一点不熟、无所适从。这个错误是最应努力避免的。

由用户控制

GUI 设计者倾向于控制程序是显而易见的,在程序中通过使菜单项和控件变灰或变黑,不断的试图控制用户的走向。控制用户同事件驱动的程序设计风格是极端矛盾的,事件驱动要求是用户而非软件来决定什么事件应该发生。作为开发者,如果你花费了大量的时间在动态的控制控件的变灰和变黑中,就需要反省一下自己的设计方法和实现。可能你正在试图控制用户,而他不希望被控制。在业务变化越来越快的今天,用户界面的弹性将成为适应改变的关键方法。允许用户用各种方式甚至是你自己都想不到的方式使用程序,有点令人心里不安,但这会让你作为开发者很有成就感,同时赋予用户更大的权利。

顶层有太多的功能特性

看一下 1985 年产的录像机,然后再看一下 1995 年产的。你一定会为这两款录像机界面上的差异感到震惊。 1985 年的那款在前面板上会有各种各样易用的按钮,很多按钮会因为你几年前丢了说明书而永远不知道它们是干什么用的。 1995 年的那款可能只有大家常用的几个按钮:播放、快进、倒带、停止和弹出。这款可能比十年前那款有更多的功能,但这些功能将被隐藏在弹出式面板或滑门之后,你需要的时候才去用它们,而不是放在表面上。

同样,你应该只选择常用和易用的功能,避免把所有的东西都放到第一屏或者在工具条上放不常用的按钮。多做一点分析,看看那些功能可以放到隐藏的面板而非前面板。

成功的用户界面(GUI)

现在,让我们谈谈一些成功的 GUI 设计。成功的 GUI 设计具有很多共同的特征。最重要的,出色的图形用户界面( GUI )应该是非常带有直觉特征的。实现这些的一个方式是尽可能的采用现实世界中的抽象(暗示、隐喻)。例如,我最近看到一个用 Visa 卡和 Master (万事达)卡图标做为按钮图标的程序,这个按钮用来指示用户如何付款,这个图形立刻使用户产生一种直觉并帮助他们更快的学会使用程序。

出色的用户图形界面的另一个重要特征是速度,更专业一点说,是响应速度。很多速度问题的处理是通过 GUI 而非硬件。根据应用程序的类型,速度可能是决定程序是否被用户群接受的成败关键。例如,如果你的程序是面向在线事务处理( OLTP )的,操作太慢很快就会导致用户产生放弃系统的念头。

你可以用几种方法使用户界面上显得很快的样子。除非绝对必要,不要重绘屏幕。另一个方法是使这个屏幕的所有区域同时可用,而非一个区域一个区域的来。另外,根据用户的熟练程度,应该在用户界面中加入一些功能,这些功能可以让熟练用户在不同的区域快速的输入数据。这些功能包括重复功能、快捷键、带有有意义的图标的按钮等等,所有这些可以使速度快的用户可以控制界面并加快数据的输入。

应该怎样和不该怎样

每个好的开发者都应该把目标定在尽可能的设计最好的图形用户界面。 但如何把这个目标变成现实呢?下文中,在各个章节给出了图形用户界面设计的规范(标准)。

同任何出色的专业人士一样,你需要一些可重复的成功设计法则。我们就是用这里提供的法则为我们的客户服务并教授了超过 20000 名的国内国际 GUI 设计专业的学生。这些规范也会对你有帮助的。

对人的理解

程序必须反映用户的视角和行为。要充分理解用户开发者首先要理解人,因为我们都具有共同的特征。人类通过辨别比通过记忆学习起来更容易。要经常试着提供一个数据列表给用户,而非让用户凭记忆自己输入数据。普通人能记住 2000 3000 单词,但却可以认出 50000 单词。

留意不同的视角

很多设计者在设计图标或程序整个行为的时候会不自觉的陷入视角陷阱。最近我看到一个图标,它用于在一个会计系统中指明汇总。为了标示这个功能,设计者花了很多心思在画一个把桂圆组合到一起的图标。不幸的是,这个系统的用户对这个图标的喻意更本就没有一点概念,虽然它从设计者的视角来看是非常直观的。保留图标列表中给出了标准图标,如图一所示,可以帮助你消除这些问题。(原 Html 文件中就没图,估计老外也时兴转载)

 

Reserved Icons
Figure 1

Picture

Meaning and Behaviour

Use to Identify an Application

Used to Identify a Function

Reserved Word Text Label

 

Information Message

No

Yes
(identifies Information message box)

None

 

Warning Message

No

Yes
(identifies Warning message box)

None

 

Question Message

No

Yes
(identifies question message box)

None

 

Error Message

No

Yes
(identifies error message box)

None

清楚一致的设计

很多 GUI 程序对最终用户常常不够清楚。一个增强程序清楚表述能力的有效方法是使用列表中的保留字进行开发。用户中最常见的抱怨是某个术语表述的不清楚或不一致。我常常看见开发者们激烈的争论按钮或菜单项上用那个术语更合适,而同时就在一墙之隔的另一群开发者也在争论同样的问题,在程序发布之后,一个屏幕上可能写着“项目”,而下一屏却写着“产品”,而第三屏又变成了“货物”,可是其实这三个术语是指的同一个东西。这种一致性的缺乏导致用户非常迷惑并产生操作失误。

图二给出了保留字列表的一个例子。一个开发小组应该用更多的保留字来完善和扩充这个表。

 

保留字列表
图二

文本

含义和行为

是否出现在按钮上

是否出现在菜单上

Mnemonic
Keystrokes

热键?

Shortcut
Keystrokes

快捷键?

OK

接受输入的数据或显示的响应信息,关掉窗口

Yes

No

None

<Return> or <Enter>

Cancel

不接受输入的信息,关掉窗口

Yes

No

None

Esc

Close

结束当前的任务,让程序继续进行;关掉数据窗口

Yes

Yes

Alt+C

None

Exit

推出程序

No

Yes

Alt+X

Alt+F4

Help

调出程序的帮助信息

Yes

Yes

Alt+H

Fl

Save

保存数据,停留在当前窗口

Yes

Yes

Alt+S

Shift+Fl2

Save As

用新名字保存数据

No

Yes

Alt+A

F12

Undo

撤销前一个动作

No

Yes

Alt+U

Ctrl+Z

Cut

剪切高亮字符

No

Yes

Alt+T

Ctrl+X

Copy

拷贝高亮的文本

No

Yes

Alt+C

Ctrl+C

Paste

在插入点粘贴被拷贝或剪切的文本

No

Yes

Alt+P

Ctrl+V

同常见软件保持一致性的设计

出色的用户界面在程序中将实现同用户以前用过的其它成功软件一致的动作。写商用程序软件的时候应该尽可能的给用户提供这种一致性。例如, EmbassySuit CourtyardMarriot 连锁旅店增长的非常快,因为商务旅行者知道这些连锁的旅店能为他们提供相似的客房和其它大体差不多的服务。最次也使得商务旅行者不必每到一个新的城市都为找新旅店发愁。你的软件的商务用户有同样的需求。你程序中提供的每个新的特色都可能让用户感到焦躁,迫使他们反复试验或着给你的维护小组打昂贵的长途电话。

提供可视反馈

如果你曾有过傻傻的瞪着自己电脑上显示的沙漏等着一个操作结束的时候,就会明白没有可视化的反馈信息有多糟糕。你的用户非常希望知道一个操作会花费多长的时间以便准备好足够的耐心。作为最一般的规则,当一个操作超过 7 10 秒的时候,大多数用户希望看到一个带有进度条的消息对话框。时间的长短要根据用户类型和应用程序的特点来调整。

提供声音反馈

上周,我有幸乘坐了一次电梯,这部电梯用悦耳的声音通知乘客他们到那一层了。大楼非常新,而首先,雇员们认为声音非常可爱。一层层的走来走去的六个月后,雇员忽略了声音,开始觉得它厌烦而不认为是一个帮助。同样的事情在你的用户界面上也会发生,除了一个是厌烦的声音限制在电梯之内,一个是到了工作间的每个人的耳朵里。把音效放到几百台电脑上,在开放式的工作间中就会产生刺耳的杂音。但无论如何,声音反馈是有用的,尤其是在你需要警告用户一个严重问题产生的地方,例如进一步的操作将导致数据的丢失或程序出错。允许用户取消声音反馈,除非错误不得不通知。

保持文字内容清楚

开发者常常通过增加大量词汇来尽力使文字反馈内容清楚。但事与愿违,他们最后使消息更不清楚了。简化文本标签、用户错误信息和一行的帮助信息上的词汇是一项挑战。文字反馈的任务可以交给科普作家,通常他们可以高效的处理。

提供操作路径跟踪

如果你的用户曾经说过象这样的话:“我也不知道怎么就到这个窗口了,可是我在这里了,我也不知道怎么才能退回去。”那么就是你没有提供一个可跟踪的路径,或者说,在这种情况下,是一个可重复的操作路径。提供一个可重复的操作路径说着容易做来难。应该从设计简洁的启动你指定的功能的菜单结构开始着手。

你也要指明你菜单结构的展开位置,避免超过两级的级联菜单。为每个对话框提供描述性的标题可以非常有用的提醒用户是哪个菜单项或按钮被按下后把他们带到当前窗口的。

提供键盘支持

键盘是用户桌面上常见的固定设备,为用户输入文本和数据提供了一个有效手段。在介绍图形用户界面程序时,我们常常假定用户把鼠标作为主要的交互设备。而用鼠标操作程序对于录入员或常用用户来讲是非常费时和低效的。

加速建可以给用户提供一种非常有效的操作方式来访问窗口中的指定菜单项和控件。加速建应该易于使用并限制在一到两个键(如 F3 或者 Ctrl-P )。键盘在 GUI 的世界中有一定的限制,例如在实现象拖拽、点击、变大变小窗口等直接操作任务的时候。相对来说,你总会发现有一小批人坚持用鼠标从不碰键盘。这导致你需要对所有菜单和窗口操作提供完整等价的键盘和鼠标支持。

注意表达模式

把所有界面的各个方面连起来的一个重点是界面的外观和风格。外观和风格必须一致。用户使用一个窗体或对话框过后,在此基础上,他们希望在使用下一个窗体或控件时有同样的感受。

研究好的界面设计模式和连续性是最重要的。决定模式一定要用心,例如程序是有单文档界面还是多文档界面。模式也包括用户如何在程序中完成他们的任务。

指定合适的程序表达方式对开发后续窗口提供了很大的便利,因为它们有很多通用的内在框架。另一方面,如果你不尽早在你的界面设计中定义好表达方式,拖后对程序外观和风格的修改将浪费大量的时间和金钱,因为改动几乎会影响到所有的窗口。

有模式和无模式对话框

当我们需要用户的输入时,我们常常就需要用有模式对话框。使用有模式对话框一直被很多开发者尽量避免,因为它对用户限制太多。但不管怎样,有模式对话框在复杂程序中还是很有用的,因为很多人同一时间只在一个窗口内工作。当有特定任务时可以试着用有模式对话框。对不确定完成时限的任务无模式对话框是一种更好的选择,但有个提示:在同一时刻,要使用户的无模式对话框保持在 3 个以内。如果超过这个数的话,你维护部门的电话就会响个不停了,这时用户就很难把注意力集中到他们的任务上,却要花费很多的时间管理各种各样打开的窗口。利用图三中的表来决定合理的使用对话框和窗口。

 

何时使用对话框、窗口
图三

类型

描述

使用

例子

有模式

对话框

给出一个确定的任务

打开文件对话框
另存为对话框

无模式

对话框

给出一个持久的任务

查找框
历史记录框
任务列表框

应用程序窗口

含有子文档窗口的窗口框架

给出一个对象的多个实例
比较两个或多个窗口中的数据

文字处理
电子表格

文档窗口

无模式对话框或者被应用程序窗口管理和包含的文档窗口

给出一个程序的多个部分

数据的多个视图 ( 表单 )

从属窗口

附属应用程序的主窗口

给出被父程序调用的另一个程序

调出一个程序的帮助

控件约定

控件是用户同程序交互的可见单元。用户界面设计人员面对着的控件集合取之不尽。每个新的控件带有自己特定的行为和特征。为每个用户选择合适的控件会产生更高的产出、更低的错误率和更高的用户满意度。可以在你的屏幕上按图四的表中列出的控件用法使用控件。

 

控件使用说明
图四

控件

范围内应用的数量

控件类型

Menu Bar

最多十个子项

Static action

Pull-Down Menu

最多十二个子项

Static action

Cascading Menu

最多五个子项 , 一层级联

Static action

Pop-up Menu

最多十个子项

Static action

Push-button

每个对话框中最多六个

Static action

Check Box

每组最多 10 12

Static set/select value

Radio Button

每组最多六个

Static set/select value

List Box

表中最多 50 , 显示 8 10

Dynamic set/select value

Drop-down List Box

控件中一次显示一个选项,下拉框中不超过 20

Dynamic set/select single value

Combination List Box

控件中按标准格式一次显示一个选项,下拉框中不超过 20

Dynamic set/select single value; add value to list

Spin Button

最多十个子项

Static set/select value

Slider

依赖于显示的数据

Static set/select value in range

最后,尽量在整个应用程序中保持这些控件基本行为和摆放的一致性。一旦你改变这些基本控件的行为,用户就会迷糊。要仔细想过才改,并且这些改变用的时候要一致。

使用设计规范

要理解出色的用户界面设计( GUI )背后的规范并把它们应用到自己的程序中是一个挑战。让我们检查一个程序,看看如何应用这些规范来改善界面。

看一看要重新设计的用户界面设计

图五中的界面被一家救护车分派公司用来维护客户数据,提供财务信息和分派救护车。应用程序是从字符系统移植过来的,包含很多设计错误,这些错误将影响到重要任务应用系统程序的用户使用。要记住,在重要应用系统中,界面的清晰简单尤其重要。例如这里,对请求的快速处理攸关生死。这个窗体中有如下错误:

图五

  • 顶层有太多的功能。 用户要求新系统方便的提供所有信息,这使得窗体同时用于客户管理和救护车派送。如果你输入完整的客户资料并按更新按钮,记录就更新了。但是,如果你只输入最少量的客户信息,例如社会安全号,诊断,从哪里到哪里,然后按分派按钮,救护车就被派出。更新功能和派送功能需要在不同的对话框中处理。
  • 太多按钮。 右侧的按钮应该在父窗口中,也许就在工具栏中,但不应该在子窗口中。
  • 差的导向帮助。 GUI 控件应该按使用的频率摆放。最重要的字段应该放在左上;次要的字段应该放在右下。当分派救护车时很难想象公司名和发票号是最重要的字段。
  • 控件的不合理使用。 设计者采用了文本标签而不是组别框来区分屏幕上的数据应该归哪一组。这许许多多的文本标签弄得屏幕非常乱同时使数据和标签很难区分。可编辑的字段应该用一个框子框起来,以便可以非常直观的看出那些字段可以更改。
  • 缺乏对称性。 简单的调整一些字段、组框和按钮就可以使界面更容易使用。我们的大脑喜欢有序,而非无序。

改进的用户界面

图六和图七展示了同一应用程序大大改进后的界面。

图六

图七

  • 不再乱七八糟。 这个应用程序应该有几个子窗口以便用户做不同的任务。这些任务可以简单的通过任务菜单或按竖着的工具条上的按钮来操作。派车按钮调出一个有模式的对话框而非无模式的子窗口。这样,你就可以要求用户确认完成了派车任务。如果用无模式的窗口的话,有可能被用户覆盖掉,甚至根本就没派车。
  • 重排了输入字段。 混乱的字段顺序已经按重要性和使用频率更有逻辑性的调整了结构。
  • 改进的控件。 修正后的界面显示了数据输入字段使用的一致性。所有用户可以输入数据的字段都被框了起来。组别框被用于分组相关字段或表明一个范围。

利用我们以前讨论过的规范,这些修正使我们得到一个清楚干净的并非常直观的一个界面。

实施有效的标准

当你把一些出色的设计经验应用到你的程序当中时,你怎么保证你的团队中其他人也这么做呢?最有价值效用的使得你的 GUI 程序保持一致性的方法是采用易用、清晰、简明的 GUI 标准。我们都有过这种经验,被积极发放给协作成员的标准手册,立刻就被放到了开发者的书架上,同其它从未读过的标准手册摆在一起。要使你的标准避免这种命运,可以给他们提供在线的超文本格式的标准手册。把你的标准分成一条一条规则和建议,要求开发人员必须遵守,如果违反,要求开发人员给出理由。开发人员希望知道那些是强制执行的那些他们可以由他们自己调整。

总结

无论 GUI 用于哪个平台,九十年代以来对程序开发人员来讲,设计出色的图形用户界面( GUI )是一项重要的技能。出色的 GUI 设计不是自发的。它需要开发者学习和应用一些基本的规则,包括设计用户每天都乐于使用的一些东西。它也需要开发人员坚持不懈用这些规则获得尽可能多的经验,以及向出色的 GUI 设计学习。

记住,如果你应用了规范并设计出来出色的用户界面,你的用户利用你为他们设计的界面将更容易和熟练的完成他们的工作。

定时机制是指在程序运行当中间隔特定的时间引发指定的事件。在DOS下编程时,主要依靠时钟中断Int 8及其调用中断 Int 1cH来实现,应用程序通过修改这些系统中断来达到实现定时触发。而在Windows下,若想象在DOS下肆无忌惮的修改系统是不现实的,那么应当如何实现定时机制呢?下面在下就在学习当中的几点体会谈谈这个问题,提出几种方案供大家参考。

第一种方案是大家熟悉的截获定时消息的途径。在Windows提供给我们使用的系统资源当中,有一种称为“定时器(Timer)”的特殊资源,在申请了这类资源的程序当中每间隔一段时间会接收到值为WM_TIMER的消息。需要定时执行的代码可以放在该消息的处理部分。如果在VC中,我们可以具体按照以下步骤实现这一目的:

  1. 利用MFC AppWizard创建一个标准的工程,接受所有缺省选项。名为s1
  2. 在Classview中选中“CMainFrame”类,然后按Ctrl+W激活ClassWizard,在“Message Map”选项卡中Class Name选“CMainFrame”,接着在“Message”中选“WM_TIMER”,最后按下“Add Funcation”。以上步骤加入了对WM_TIMER消息的映射处理。
  3. 回到Classview中,双击“OnCreate”成员函数,在函数的末尾添加申请Timer的语句:
    SetTimer(100,1000,NULL);//申请一个标识值为100的Timer,定时间隔为1000毫秒(1秒)。
  4. 在“Classview”中双击OnTimer函数,输入要定时实现的代码。本例子中为:
    MessageBeep(1000);;//每隔一秒发出通告声
  5. 编译并执行之,我们可以每隔一秒就听到声音。这正是我们在OnTimer函数内要求执行的。

实际当中,我们可以将“MessageBeep(1000);”换成任何我们想完成的任务,譬如定时存盘等。

第二种方案也利用Timer资源,但却是采用已经编写好的代码&#0;&#0;我们可以加入一个具有定时功能的组件至当前工程当中。这种方法特别适用于基于对话框的工程。具体步骤如下:

  1. 利用MFC AppWizard创建一个基于对话框的工程,其余接受所有缺省选项。名为s2。
  2. 在ResourceView中,双击IDD_S2_DIALOG,显示对话框,将其中的“To do:”改为“定时触发演示的例子”,表明工程的作用。
  3. 右击对话框编辑区,在弹出的右键菜单中选择“Insert ActiveX Control”,从弹出的列表框中选择“Timer Object”,确定后会在对话框内出现一个Timer对象。
  4. 我们右击Timer对象,从弹出的菜单中选择“Properties”,接着选“All”选项卡,将其中的Interval值设为5000,即每隔5秒发生一次Timer事件。
  5. 回到对话框编辑界面,双击Timer,产生一个CS2Dlg::OnTimerTimer1成员函数,接受缺省值,并在函数实现部分输入:
    MessageBox("定时触发消息框","定时演示" ,MB_OK);
  6. 编译并运行此工程,将会在产生的对话框运行期间,每隔5秒弹出一个消息框。

同样,我们可以以任何自己的代码来替换5中的消息框语句。详细见附例s2。

第三种方法是采用线程技术。众所周知,Windows 9X是一个基于多线程的多任务操作系统,在内核中以线程作为调度的基本单位,由系统分时间片进行调度。利用这一点,我们可以在程序当中创建一个“司职”计时的线程,通过线程间的同步来定时触发我们要完成的任务的代码。不象前两种方法需要至少有一个窗口作为接受消息的主窗口,采用线程技术实现定时触发将免去创建窗口的麻烦以及带来的系统各种资源的消耗。下面我们来举一个例子来说明这个问题:我们在CmyApp类的Initstance成员中不建立主窗口而是创建一个工作线程,该线程休眠一定的时间后,自动调用主线程的SomeThing函数。为了支持线程的运行,我们需要给CmyApp类增加相应的线程函数。下面,我们还是一步一步的实现:

  1. 利用MFC AppWizard创建一个标准工程,其中为不产生多余的代码,不选文档/视图支持,并选择单文档。工程名为S3。
  2. 在CS3App:: InitInstance()中用“/* … */”注释掉“return TRUE;”之前的所有代码。这是为了不建立窗口。并添加以下代码:
    ExitFlag=TRUE;//是否结束主线程的循环的标志变量。因为子线程严重依赖主线程,所以在本例子中为了避免没有主窗口而提前结束应用程序,从而使子线程无法存在,所以给主线程一个循环,知道全局变量ExitFlag在子线程退出前被设置成FALSE为止.
    StartThread();//启动线程
    do{}while(ExitFlag);//直到结束子线程
    ::MessageBox(NULL,"主线程结束!","定时触发演示",MB_OK);
    return TRUE;
  3. 在Globals中增加一标志变量“ExitFlag”,类型为BOOL。它被主线程用来判断是否结束自身运行。
  4. 通过ClassView在CS3App的Public部分声明以下函数:
    void StartThread(void); //启动线程
    static UINT ThreadFunction(void); //主要执行代码的函数
    static UINT StaticThreadFunc(LPVOID lpparam);//设置线程时用到的函数
    需要特别指出的是,用AfxBeginThread进行线程设置时,第一参数必须象本例所指出的那样声明为Static ,不然参数转换的错误会扰得你不得安宁。
  5. 在StartThread中输入如下代码:
    AfxBeginThread(StaticThreadFunc,this);//建立并启动线程
  6. 在StaticThreadFunc中输入如下代码:
    return ThreadFunc();//调用完成主要线程代码的函数,注意一定要是Static.
  7. 实现ThreadFunction:
    int i;
    i=5;//触发5次
    while(i--)
    {
    Sleep(5000);//间隔5秒
    ::MessageBox (NULL,"我被定时触发了!","定时触发演示",MB_OK);
    }
    ExitFlag=FALSE;//ExitFlag是一全局变量,通知主线程结束运行。
    return 0;
    }
  8. 编译并运行工程,将看不到应用程序窗口,但可以看到每隔5秒,桌面上出现一个消息框,5次后弹出主线程结束的消息框。

以上即本人在学习当中解决 Windows下实现定时触发而采取的一些办法,各自方法的特点也在介绍当中指出。希望所述能给大家一点帮助,更希望能得到大家的指正。如果您有什么意见和设想,欢迎发E-Mail给我(yangshanhe@21cn.com)。

==

很早之前2000年的拙作,集在一起,免得自己都不清楚干过什么。

将STLPort解压出来。为STLPort注册环境变量。

在VC安装目录里搜索“vcvars32.bat”文件出来。找到INCLUDE这个键,将$(STLPort)\stlport注册进去。然后在机器中的环境变量中注册INCLUDE这个变量,将此目录也注册好。

开始编译,进入$(STLPort)\src目录。
copy vc71.mak makefile
这一步就是将一个vc71版本的mak做为makefile文件。
然后使用nmake开始编译。
也可以使用这个来做
nmake -f vc71.mak

这里我做了一个例子来来说明使用这个STLPort..

#include <stl/_config.h>
#include <stl/_vector.h>
#include <iostream>

using namespace _STLP_STD;
using namespace std;
void main()
{
vector<int> arrInt;
for (int i=0;i<100;i++)
arrInt.push_back(i);
for (i=0;i<100;i++)
  cout<<arrInt[i]<<endl;
return ;
}

前言: 
错误处理和socket释放, 是IOCP编程中的一大难点. 本文试图就IOCP设计中经常遇到的这个难题展开论述并寻找其解决方案, 事实上, 文中所述的解决方式不仅仅适用于IOCP, 它同样适用于EPOLL等多种服务器编程的网络模型中, 前提是: 领会这种处理方式的实质.
正文:
在使用IOCP开发时, 大家经常遇到的一个难题是与socket相关的缓冲区释放不当带来的错误, 这种错误通常是由于多次对同一个指针执行了delete操作引起的. 比如, 当在执行wsasend或wsarecv返回了非pending的错误信息时, 我们就要对此错误进行处理, 通常情况下, 我们会想到执行这两步操作:
a. 释放此次操作使用的缓冲区数据(如果不释放可能造成内存泄漏);
b. 关闭当前操作所使用的socket.
而另一方面, 我们可能也会在get函数(GetQueuedCompletionStatus)的处理中, 当get函数返回值为FALSE时也作这两步相同的操作.  此时, 就会造成对同一缓冲区的重复释放, 问题由此产生.
解决的方法, 可以有这几种:
1. 对数据缓冲区使用引用计数机制;
2. 在clientsock的对象设计机制上使释放操作线性化.
关于这两种方法, 任何一种如果要详细说清, 可能篇幅都会比较长, 笔者并无耐心和精力将每一个细节都一一道来, 在此仅选第2种方案的关键步骤和核心思想来与大家分享.
由前面对问题的描述可以看出, 造成多次释放的原因可能是在执行收发操作和GET函数返回值为FALSE时, 我们重复执行了释放操作. 很自然地, 我们会想到,  能不能把这两次释放合并成一次释放,  这样不就没问题了吗?  yes,  这个思路是没问题的.  但要想让这个思路能变成现实,  需要在设计机制上对这个思路进行一定的支持.
首先,  我们假设, 是在get函数返回时统一进行相应的释放和关闭操作.
如果在执行wsasend操作时, 发生了非pending错误(io操作正在进行中), 而此时我们如果不释放资源, 那至少得让IOCP在GET返回时得知这个错误和发生错误时的缓冲区指针. 通知IOCP的方式, 是使用post函数(PostQueuedCompletionStatus)向IOCP抛一个特殊标志的消息, 这个特殊标志可以通过get函数的第二个参数, 即: 传送字节数来表示, 可以选择任何一个不可能出现的值, 比如任何一个跟它的初始值不相等的负数.  当然, 如果你通过单句柄数据或单IO数据来传递也是可以的. 而发生错误的这个缓冲区指针, 我们是必须要通过单句柄数据或单IO数据来传递的. 但是, 从整个缓冲区的管理机制上来说, 我不推荐这样的离散缓冲区机制, 我的建议是: 把收发缓冲区或数据队列与相应的clientsocket对象相绑定, 释放操作写在该对象的析构函数里, 这样当释放clientsocket对象时就释放了这些缓冲区.
ok, 这样一来, 在get函数里, 有三种情况需要执行释放逻辑:
1. get的返回值为FALSE;
2. 传送字节数为0;
3. 接收到刚才我们post的那个错误类型消息.
把释放操作全放在get函数里以后, 对释放操作的处理, 就比较统一了. 当然, 为了实现真正的线性化和元子化, 在释放操作的最终执行逻辑上, 还需要对释放代码加锁以实现线程互斥(当然, 这是在你开了多个工作者线程的情况下).

在WinSock上使用IOCP
本文章假设你已经理解WindowsNT的I/O模型以及I/O完成端口(IOCP),并且比较熟悉将要用到的API,如果你打算学习IOCP,请参考Jeffery Richter的Advanced Windows(第三版),第15章I/O设备,里面有极好的关于完成端口的讨论以及对即将使用API的说明。
IOCP提供了一个用于开发高效率和易扩展程序的模型。Winsock2提供了对IOCP的支持,并在WindowsNT平台得到了完整的实现。然而IOCP是所有WindowsNT I/O模型中最难理解和实现的,为了帮助你使用IOCP设计一个更好的Socket服务,本文提供了一些诀窍。
Tip 1:使用Winsock2 IOCP函数例如WSASend和WSARecv,如同Win32文件I/O函数,例如WriteFile和ReadFile。
微软提供的Socket句柄是一个可安装文件系统(IFS)句柄,因此你可以使用Win32的文件I/O函数调用这个句柄,然而,将Socket句柄和文件系统联系起来,你不得不陷入很多的Kernal/User模式转换的问题中,例如线程的上下文转换,花费的代价还包括参数的重新排列导致的性能降低。
因此你应该使用只被Winsock2中IOCP允许的函数来使用IOCP。在ReadFile和WriteFile中会发生的额外的参数重整以及模式转换只会发生在一种情况下,那就是如果句柄的提供者并没有将自己的WSAPROTOCOL_INFO结构中的DwServiceFlags1设置为XP1_IFS_HANDLES。
注解:即使使用WSASend和WSARecv,这些提供者仍然具有不可避免的额外的模式转换,当然ReadFile和WriteFile需要更多的转换。
TIP 2: 确定并发工作线程数量和产生的工作线程总量。
并发工作线程的数量和工作线程的数量并不是同一概念。你可以决定IOCP使用最多2个的并发线程以及包括10个工作线程的线程池。工作线程池拥有的线程多于或者等于并发线程的数量时,工作线程处理队列中一个封包的时候可以调用win32的Wait函数,这样可以无延迟的处理队列中另外的封包。
如果队列中有正在等待被处理的封包,系统将会唤醒一个工作线程处理他,最后,第一个线程确认正在休眠并且可以被再次调用,此时,可调用线程数量会多于IOCP允许的并发线程数量(例如,NumberOFConcurrentThreads)。然而,当下一个线程调用GetQueueCompletionStatus并且进入等待状态,系统不会唤醒他。一般来说,系统会试图保持你设定的并发工作线程数量。
一般来讲,每拥有一个CPU,在IOCP中你可以使用一个并发工作线程,要做到这点,当你第一次初始化IOCP的时候,可以在调用CreateIOCompletionPort的时候将NumberOfConcurrentThreads设置为0。
TIP 3:将一个提交的I/O操作和完成封包的出列联系起来。
当对一个封包进行出列,可以调用GetQueuedCompletionStatus返回一个完成Key和一个复合的结构体给I/O。你可以分别的使用这两个结构体来返回一个句柄和一个I/O操作信息,当你将IOCP提供的句柄信息注册给Socket,那么你可以将注册的Socket句柄当做一个完成Key来使用。为每一个I/O的"extend"操作提供一个包含你的应用程序IO状态信息的复合结构体。当然,必须确定你为每个的I/O提供的是唯一的复合结构体。当I/O完成的时候,会返回一个指向结构体的指针。
TIP 4:I/O完成封包队列的行为
IOCP中完成封包队列的等待次序并不决定于Winsock2 I/O调用产生的顺序。如果一个Winsock2的I/O调用返回了SUCCESS或者IO_PENDING,那么他保证当I/O操作完成后,完成封包会进入IOCP的等待队列,而不管Socket句柄是否已经关闭。如果你关闭了socket句柄,那么将来调用WSASend,WSASendTo,WSARecv和WSARecvFrom会失败并返回一个不同于SUCCES或者IO_PENDING的代码,这时将不会产生一个完成封包。而在这种情况下,前一次使用GetQueuedCompletionStatus提交的I/O操作所得到的完成封包,会显示一个失败的信息。
如果你删除了IOCP本身,那么不会有任何I/O请求发送给IOCP,因为IOCP的句柄已经不可用,尽管系统底层的IOCP核心结构并不会在所有已提交I/O请求完成之前被移除。
TIP5:IOCP的清除
很重要的一件事是使用复合I/O时候的IOCP清除:如果一个I/O操作尚未完成,那么千万不要释放该操作创建的复合结构体。HasOverlappedIoCompleted函数可以帮助你检查一个I/O操作是否已经完成。
关闭服务一般有两种情况,第一种你并不关心尚未结束的I/O操作的完成状态,你只希望尽可能快的关闭他。第二种,你打算关闭服务,但是你需要获知未结束I/O操作的完成状态。
第一种情况你可以调用PostQueueCompletionStatus(N次,N等于你的工作线程数量)来提交一个特殊的完成封包,他通知所有的工作线程立即退出,关闭所有socket句柄和他们关联的复合结构体,然后关闭完成端口(IOCP)。在关闭复合结构体之前使用HasOverlappedIOCompleted检查他的完成状态。如果一个socket关闭了,所有基于他的未结束的I/O操作会很快的完成。
在第二种情况,你可以延迟工作线程的退出来保证所有的完成封包可以被适当的出列。你可以首先关闭所有的socket句柄和IOCP。可是,你需要维护一个未完成I/O的数字,以便你的线程可以知道可以安全退出的时间。尽管当队列中有很多完成封包在等待的时候,活动的工作线程不能立即退出,但是在IOCP服务中使用全局I/O计数器并且使用临界区保护他的代价并不会象你想象的那样昂贵。
INFO: Design Issues When Using IOCP in a Winsock Server
适用于
This article was previously published under Q192800
SUMMARY
This article assumes you already understand the I/O model of the Windows NT I/O Completion Port (IOCP) and are familiar with the related APIs. If you want to learn IOCP, please see Advanced Windows (3rd edition) by Jeffery Richter, chapter 15 Device I/O for an excellent discussion on IOCP implementation and the APIs you need to use it.
An IOCP provides a model for developing very high performance and very scalable server programs. Direct IOCP support was added to Winsock2 and is fully implemented on the Windows NT platform. However, IOCP is the hardest to understand and implement among all Windows NT I/O models. To help you design a better socket server using IOCP, a number of tips are provided in this article.
MORE INFORMATION
TIP 1: Use Winsock2 IOCP-capable functions, such as WSASend and WSARecv, over Win32 file I/O functions, such as WriteFile and ReadFile.
Socket handles from Microsoft-based protocol providers are IFS handles so you can use Win32 file I/O calls with the handle. However, the interactions between the provider and file system involve many kernel/user mode transition, thread context switches, and parameter marshals that result in a significant performance penalty. You should use only Winsock2 IOCP- capable functions with IOCP.
The additional parameter marshals and mode transitions in ReadFile and WriteFile only occur if the provider does not have XP1_IFS_HANDLES bit set in dwServiceFlags1 of its WSAPROTOCOL_INFO structure.
NOTE: These providers have an unavoidable additional mode transition, even in the case of WSASend and WSARecv, although ReadFile and WriteFile will have more of them.
TIP 2: Choose the number of the concurrent worker threads allowed and the total number of the worker threads to spawn.
The number of worker threads and the number of concurrent threads that the IOCP uses are not the same thing. You can decide to have a maximum of 2 concurrent threads used by the IOCP and a pool of 10 worker threads. You have a pool of worker threads greater than or equal to the number of concurrent threads used by the IOCP so that a worker thread handling a dequeued completion packet can call one of the Win32 "wait" functions without delaying the handling of other queued I/O packets.
If there are completion packets waiting to be dequeued, the system will wake up another worker thread. Eventually, the first thread satisfies it's Wait and it can be run again. When this happens, the number of the threads that can be run is higher than the concurrency allowed on the IOCP (for example, NumberOfConcurrentThreads). However, when next worker thread calls GetQueueCompletionStatus and enters wait status, the system does not wake it up. In other words, the system tries to keep your requested number of concurrent worker threads.
Typically, you only need one concurrent worker thread per CPU for IOCP. To do this, enter 0 for NumberOfConcurrentThreads in the CreateIoCompletionPort call when you first create the IOCP.
TIP 3: Associate a posted I/O operation with a dequeued completion packet.
GetQueuedCompletionStatus returns a completion key and an overlapped structure for the I/O when dequeuing a completion packet. You should use these two structures to return per handle and per I/O operation information, respectively. You can use your socket handle as the completion key when you register the socket with the IOCP to provide per handle information. To provide per I/O operation "extend" the overlapped structure to contain your application-specific I/O-state information. Also, make sure you provide a unique overlapped structure for each overlapped I/O. When an I/O completes, the same pointer to the overlapped I/O structure is returned.
TIP 4: I/O completion packet queuing behavior.
The order in which I/O completion packets are queued in the IOCP is not necessarily the same order the Winsock2 I/O calls were made. Additionally, if a Winsock2 I/O call returns SUCCESS or IO_PENDING, it is guaranteed that a completion packet will be queued to the IOCP when the I/O completes, regardless of whether the socket handle is closed. After you close a socket handle, future calls to WSASend, WSASendTo, WSARecv, or WSARecvFrom will fail with a return code other than SUCCESS or IO_PENDING, which will not generate a completion packet. The status of the completion packet retrieved by GetQueuedCompletionStatus for I/O previously posted could indicate a failure in this case.
If you delete the IOCP itself, no more I/O can be posted to the IOCP because the IOCP handle itself is invalid. However, the system's underlying IOCP kernel structures do not go away until all successfully posted I/Os are completed.
TIP 5: IOCP cleanup.
The most important thing to remember when performing ICOP cleanup is the same when using overlapped I/O: do not free an overlapped structure if the I/O for it has not yet completed. The HasOverlappedIoCompleted macro allows you to detect if an I/O has completed from its overlapped structure.
There are typically two scenarios for shutting down a server. In the first scenario, you do not care about the completion status of outstanding I/Os and you just want to shut down as fast as you can. In the second scenario, you want to shut down the server, but you do need to know the completion status of each outstanding I/O.
In the first scenario, you can call PostQueueCompletionStatus (N times, where N is the number of worker threads) to post a special completion packet that informs the worker thread to exit immediately, close all socket handles and their associated overlapped structures, and then close the completion port. Again, make sure you use HasOverlappedIoCompleted to check the completion status of an overlapped structure before you free it. If a socket is closed, all outstanding I/O on the socket eventually complete quickly.
In the second scenario, you can delay exiting worker threads so that all completion packets can be properly dequeued. You can start by closing all socket handles and the IOCP. However, you need to maintain a count of the number of outstanding I/Os so that your worker thread can know when it is safe to exit the thread. The performance penalty of having a global I/O counter protected with a critical section for an IOCP server is not as bad as might be expected because the active worker thread does not switch out if there are more completion packets waiting in the queue.

当然TCP方式的模型还有事件选择模型。
就是把所有的网络事件和我们的一个程序里定义的事件梆定。
这个有它的好处,可能可以让我们更好的写一个线程来管理
接收与发送。
现在来讲一下一个完成端口模型。

  完成端口
 
一个完成端口其实就是一个通知队列,由操作系统把已经完成的重叠I/O请求的通知
放入其中。当某项I/O操作一旦完成,某个可以对该操作结果进行处理的工作者线程
就会收到一则通知。而套接字在被创建后,可以在任何时候与某个完成端口进行关
联。
 
步骤:
1、创建一个空的完成端口;
2、得到本地机器的CPU个数;
3、开启CPU*2个工作线程(又名线程池),全部都在等待完成端口的完成包;
4、创建TCP的监听socket,使用事件邦定,创建监听线程;
5、当有人连接进入的时候,将Client socket保存到一个我们自己定义的关键键,
    并把它与我们创建的完成端口关联;
6、使用WSARecv和WSASend函数投递一些请求,这是使用重叠I/O的方式;
7、重复5~6;

注:1、重叠I/O的方式中,接收与发送数据包的时候,一定要进行投递请求这是
   它们这个体系结构的特点
   当然,在完成端口方式中,不是直接使用的WSARecv和WSASend函数进行请求
   的投递的。而是使用的ReadFile,Write的方式
  2、完成端口使用了系统内部的一些模型,所以我们只要按照一定的顺序调用就
   可以完成了。
  3、完成端口是使用在这样的情况下,有成千上万的用户连接的时候,它能够
   保证性能不会降低。


 

#include < winsock2.h >
#include
< windows.h >
#include
< stdio.h >

#define PORT 5150
#define DATA_BUFSIZE 8192

// 关键项
typedef struct
{
   OVERLAPPED Overlapped;
   WSABUF DataBuf;
   CHAR Buffer[DATA_BUFSIZE];
   DWORD BytesSEND;
   DWORD BytesRECV;
}
PER_IO_OPERATION_DATA, * LPPER_IO_OPERATION_DATA;


typedef
struct  
{
   SOCKET Socket;
}
PER_HANDLE_DATA, * LPPER_HANDLE_DATA;

DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID);

void main( void )
{
   SOCKADDR_IN InternetAddr;
   SOCKET Listen;
   SOCKET Accept;
   HANDLE CompletionPort;
   SYSTEM_INFO SystemInfo;
   LPPER_HANDLE_DATA PerHandleData;
   LPPER_IO_OPERATION_DATA PerIoData;
  
int i;
   DWORD RecvBytes;
   DWORD Flags;
   DWORD ThreadID;
   WSADATA wsaData;
   DWORD Ret;

  
if ((Ret = WSAStartup( 0x0202 , & wsaData)) !=   0 )
  
{
      printf(
" WSAStartup failed with error %d\n " , Ret);
     
return ;
   }


  
// 打开一个空的完成端口

  
if ((CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0 , 0 )) == NULL)
  
{
      printf(
" CreateIoCompletionPort failed with error: %d\n " , GetLastError());
     
return ;
   }


  
// Determine how many processors are on the system.

   GetSystemInfo(
& SystemInfo);

  
// 开启cpu个数的2倍个的线程

  
for (i =   0 ; i < SystemInfo.dwNumberOfProcessors *   2 ; i ++ )
  
{
      HANDLE ThreadHandle;

     
// Create a server worker thread and pass the completion port to the thread.

     
if ((ThreadHandle = CreateThread(NULL, 0 , ServerWorkerThread, CompletionPort,
        
0 , & ThreadID)) == NULL)
     
{
         printf(
" CreateThread() failed with error %d\n " , GetLastError());
        
return ;
      }


     
// Close the thread handle
      CloseHandle(ThreadHandle);
   }


  
// 打开一个服务器socket

  
if ((Listen = WSASocket(AF_INET, SOCK_STREAM, 0 , NULL, 0 ,
      WSA_FLAG_OVERLAPPED))
== INVALID_SOCKET)
  
{
      printf(
" WSASocket() failed with error %d\n " , WSAGetLastError());
     
return ;
   }
 

   InternetAddr.sin_family
= AF_INET;
   InternetAddr.sin_addr.s_addr
= htonl(INADDR_ANY);
   InternetAddr.sin_port
= htons(PORT);

  
if (bind(Listen, (PSOCKADDR) & InternetAddr, sizeof (InternetAddr)) == SOCKET_ERROR)
  
{
      printf(
" bind() failed with error %d\n " , WSAGetLastError());
     
return ;
   }



  
if (listen(Listen, 5 ) == SOCKET_ERROR)
  
{
      printf(
" listen() failed with error %d\n " , WSAGetLastError());
     
return ;
   }


  
// 开始接收从客户端来的连接

  
while (TRUE)
  
{
     
if ((Accept = WSAAccept(Listen, NULL, NULL, NULL, 0 )) == SOCKET_ERROR)
     
{
         printf(
" WSAAccept() failed with error %d\n " , WSAGetLastError());
        
return ;
      }


     
// 创建一个关键项用于保存这个客户端的信息,用户接收发送的重叠结构,
     
// 还有使用到的缓冲区
      if ((PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR,
        
sizeof (PER_HANDLE_DATA))) == NULL)
     
{
         printf(
" GlobalAlloc() failed with error %d\n " , GetLastError());
        
return ;
      }


     
// Associate the accepted socket with the original completion port.

      printf(
" Socket number %d connected\n " , Accept);
      PerHandleData
-> Socket = Accept;

     
// 与我们的创建的那个完成端口关联起来,将关键项也与指定的一个完成端口关联
      if (CreateIoCompletionPort((HANDLE) Accept, CompletionPort, (DWORD) PerHandleData,
        
0 ) == NULL)
     
{
         printf(
" CreateIoCompletionPort failed with error %d\n " , GetLastError());
        
return ;
      }


     
// 投递一次接收,由于接收都需要使用这个函数来投递一个接收的准备

     
if ((PerIoData = (LPPER_IO_OPERATION_DATA) GlobalAlloc(GPTR,          sizeof (PER_IO_OPERATION_DATA))) == NULL)
     
{
         printf(
" GlobalAlloc() failed with error %d\n " , GetLastError());
        
return ;
      }


      ZeroMemory(
& (PerIoData -> Overlapped), sizeof (OVERLAPPED));
      PerIoData
-> BytesSEND =   0 ;
      PerIoData
-> BytesRECV =   0 ;
      PerIoData
-> DataBuf.len = DATA_BUFSIZE;
      PerIoData
-> DataBuf.buf = PerIoData -> Buffer;

      Flags
=   0 ;
     
if (WSARecv(Accept, & (PerIoData -> DataBuf), 1 , & RecvBytes, & Flags,
        
& (PerIoData -> Overlapped), NULL) == SOCKET_ERROR)
     
{
        
if (WSAGetLastError() != ERROR_IO_PENDING)
        
{
            printf(
" WSARecv() failed with error %d\n " , WSAGetLastError());
           
return ;
         }

      }

   }

}

// 工作线程
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
{
   HANDLE CompletionPort
= (HANDLE) CompletionPortID;
   DWORD BytesTransferred;
   LPOVERLAPPED Overlapped;
   LPPER_HANDLE_DATA PerHandleData;
   LPPER_IO_OPERATION_DATA PerIoData;
   DWORD SendBytes, RecvBytes;
   DWORD Flags;
  
  
while (TRUE)
  
{
     
// 完成端口有消息来了
      if (GetQueuedCompletionStatus(CompletionPort, & BytesTransferred,
         (LPDWORD)
& PerHandleData, (LPOVERLAPPED * ) & PerIoData, INFINITE) ==   0 )
     
{
         printf(
" GetQueuedCompletionStatus failed with error %d\n " , GetLastError());
        
return   0 ;
      }



     
// 是不是有人退出了

     
if (BytesTransferred ==   0 )
     
{
         printf(
" Closing socket %d\n " , PerHandleData -> Socket);

        
if (closesocket(PerHandleData -> Socket) == SOCKET_ERROR)
        
{
            printf(
" closesocket() failed with error %d\n " , WSAGetLastError());
           
return   0 ;
         }


         GlobalFree(PerHandleData);
         GlobalFree(PerIoData);
        
continue ;
      }


     
//

     
if (PerIoData -> BytesRECV ==   0 )
     
{
         PerIoData
-> BytesRECV = BytesTransferred;
         PerIoData
-> BytesSEND =   0 ;
      }

     
else
     
{
         PerIoData
-> BytesSEND += BytesTransferred;
      }


     
if (PerIoData -> BytesRECV > PerIoData -> BytesSEND)
     
{

        
// Post another WSASend() request.
        
// Since WSASend() is not gauranteed to send all of the bytes requested,
        
// continue posting WSASend() calls until all received bytes are sent.

         ZeroMemory(
& (PerIoData -> Overlapped), sizeof (OVERLAPPED));

         PerIoData
-> DataBuf.buf = PerIoData -> Buffer + PerIoData -> BytesSEND;
         PerIoData
-> DataBuf.len = PerIoData -> BytesRECV - PerIoData -> BytesSEND;

        
if (WSASend(PerHandleData -> Socket, & (PerIoData -> DataBuf), 1 , & SendBytes, 0 ,
           
& (PerIoData -> Overlapped), NULL) == SOCKET_ERROR)
        
{
           
if (WSAGetLastError() != ERROR_IO_PENDING)
           
{
               printf(
" WSASend() failed with error %d\n " , WSAGetLastError());
              
return   0 ;
            }

         }

      }

     
else
     
{
         PerIoData
-> BytesRECV =   0 ;

        
// Now that there are no more bytes to send post another WSARecv() request.

         Flags
=   0 ;
         ZeroMemory(
& (PerIoData -> Overlapped), sizeof (OVERLAPPED));

         PerIoData
-> DataBuf.len = DATA_BUFSIZE;
         PerIoData
-> DataBuf.buf = PerIoData -> Buffer;

        
if (WSARecv(PerHandleData -> Socket, & (PerIoData -> DataBuf), 1 , & RecvBytes, & Flags,
           
& (PerIoData -> Overlapped), NULL) == SOCKET_ERROR)
        
{
           
if (WSAGetLastError() != ERROR_IO_PENDING)
           
{
               printf(
" WSARecv() failed with error %d\n " , WSAGetLastError());
              
return   0 ;
            }

         }

      }

   }

}

TinyXML 指南

 

这是什么?

这份指南有一些关于如何有效地使用TinyXML的技巧和建议。

我也会尝试讲一些诸如怎样使字符串与整型数相互转化的C++技巧。这与TinyXML本身没什么关系,但它也许会对你的项目有所帮助,所以我还是把它加进来了。

如果你不知道基本的C++概念,那么这份指南就没什么用了。同样的,如果你不知道什么是DOM,那先从其它地方找来看看吧。

在我们开始之前

一些将会被用到的XML数据集/文件。

example1.xml:

 

<?xml version="1.0" ?>

<Hello>World</Hello>

example2.xml:

 

<?xml version="1.0" ?>

<poetry>

   <verse>

      Alas

         Great World

            Alas (again)

   </verse>

</poetry>

example3.xml:

 

<?xml version="1.0" ?>

<shapes>

   <circle name=”int-based” x=”20” y=”30” r=”50” />

   <point name=”float-based” x=”3.5” y=”52.1” />

</shapes>

example4.xml:

 

<?xml version="1.0" ?>

<MyApp>

   <!– Settings for MyApp –>

   <Messages>

      <Welcome>Welcome to MyApp</Welcome>

      <Farewell>Thank you for using MyApp</Farewell>

   </Messages>

   <Windows>

      <Window name=”MainFrame” x=”5” y=”15” w=”400” h=”250” />

   </Windows>

   <Connection ip=”192.168.0.1” timeout=”123.456000” />

</MyApp>

开始

把文件加载成XML

把一个文件加载成TinyXML DOM的最简单方法是:

 

TiXmlDocument doc( "demo.xml" );

doc.LoadFile();

一个更接近于现实应用的例子如下。它加载文件并把内容显示到标准输出STDOUT上:

 

// 加载指定的文件并把它的结构输出到STDOUT上

void dump_to_stdout(const char* pFilename)

{

    TiXmlDocument doc(pFilename);

    bool loadOkay = doc.LoadFile();

    if (loadOkay)

    {

        printf(“\n%s:\n”, pFilename);

        dump_to_stdout( &doc ); // 稍后在指南中定义

    }

    else

    {

        printf(“Failed to load file "%s&#8221;\n”, pFilename);

    }

}

在main中使用此函数的一个简单应用示范如下:

 

int main(void)

{

    dump_to_stdout(“example1.xml”);

    return 0;

}

回想example1的XML:

 

<?xml version="1.0" ?>

<Hello>World</Hello>

用这个XML运行程序就会在控制台/DOS窗口中显示:

 

DOCUMENT

  • DECLARATION

  • ELEMENT Hello

  + TEXT[World]

”dump_to_stdout“函数稍后会在这份指南中定义,如果你想要理解怎样递归遍历一个DOM它会很有用。

用程序建立文档对象

这是用程序建立example1的方法:

 

void build_simple_doc( )

{

    // 生成xml: <?xml ..><Hello>World</Hello>

    TiXmlDocument doc;

    TiXmlDeclaration * decl = new TiXmlDeclaration( “1.0”“”“” );

    TiXmlElement * element = new TiXmlElement( “Hello” );

    TiXmlText * text = new TiXmlText( “World” );

    element->LinkEndChild( text );

    doc.LinkEndChild( decl );

    doc.LinkEndChild( element );

    doc.SaveFile( “madeByHand.xml” );

}

然后可以用以下方法加载并显示在控制台上:

 

dump_to_stdout("madeByHand.xml"); // 此函数稍后会中指南中定义

你会看到跟example1一模一样:

 

madeByHand.xml:

Document

  • Declaration

  • Element [Hello]

  + Text: [World]

这段代码会产生相同的XML DOM,但它以不同的顺序来创建和链接结点:

 

void write_simple_doc2( )

{

    // 实现与 write_simple_doc1一样的功能,(译注:我想它指是build_simple_doc)

    // 但尽可能早地把结点添加到树中。

    TiXmlDocument doc;

    TiXmlDeclaration * decl = new TiXmlDeclaration( “1.0”“”“” );

    doc.LinkEndChild( decl );

    

    TiXmlElement * element = new TiXmlElement( “Hello” );

    doc.LinkEndChild( element );

    

    TiXmlText * text = new TiXmlText( “World” );

    element->LinkEndChild( text );

    

    doc.SaveFile( “madeByHand2.xml” );

}

两个都产生同样的XML,即:

 

<?xml version="1.0" ?>

<Hello>World</Hello>

结构构成都是:

 

DOCUMENT

  • DECLARATION

  • ELEMENT Hello

  + TEXT[World]

属性

给定一个存在的结点,设置它的属性是很容易的:

 

window = new TiXmlElement( "Demo" ); 

window->SetAttribute(“name”“Circle”);

window->SetAttribute(“x”, 5);

window->SetAttribute(“y”, 15);

window->SetDoubleAttribute(“radius”, 3.14159);

你也可以用TiXmlAttribute对象达到同样的目的。

下面的代码向我们展示了一种(并不只有一种)获取某一元素属性并打印出它们的名字和字符串值的方法,如果值能够被转化为整型数或者浮点数,也把值打印出来:

 

// 打印pElement的所有属性。

// 返回已打印的属性数量。

int dump_attribs_to_stdout(TiXmlElement* pElement, unsigned int indent)

{

    if ( !pElement ) return 0;

    

    TiXmlAttribute* pAttrib=pElement->FirstAttribute();

    int i=0;

    int ival;

    double dval;

    const char* pIndent=getIndent(indent);

    printf(“\n”);

    while (pAttrib)

    {

        printf( “%s%s: value=[%s]”, pIndent, pAttrib->Name(), pAttrib->Value());

        

        if (pAttrib->QueryIntValue(&ival)==TIXML_SUCCESS) printf( “ int=%d”, ival);

        if (pAttrib->QueryDoubleValue(&dval)==TIXML_SUCCESS) printf( “ d=%1.1f”, dval);

        printf( “\n” );

        i++;

        pAttrib=pAttrib->Next();

    }

    return i;

}

把文档对象写到文件中

把一个已经建立好的DOM写到文件中是非常简单的:

 

doc.SaveFile( saveFilename );

回想一下,比如example4:

 

<?xml version="1.0" ?>

<MyApp>

   <!– Settings for MyApp –>

   <Messages>

      <Welcome>Welcome to MyApp</Welcome>

      <Farewell>Thank you for using MyApp</Farewell>

   </Messages>

   <Windows>

      <Window name=”MainFrame” x=”5” y=”15” w=”400” h=”250” />

   </Windows>

   <Connection ip=”192.168.0.1” timeout=”123.456000” />

</MyApp>

以下函数建立这个DOM并把它写到“appsettings.xml”文件中:

 

void write_app_settings_doc( ) 


    TiXmlDocument doc; 

    TiXmlElement* msg;

    TiXmlDeclaration* decl = new TiXmlDeclaration( “1.0”“”“” ); 

    doc.LinkEndChild( decl ); 

    

    TiXmlElement * root = new TiXmlElement( “MyApp” ); 

    doc.LinkEndChild( root ); 

    

    TiXmlComment * comment = new TiXmlComment();

    comment->SetValue(“ Settings for MyApp ” ); 

    root->LinkEndChild( comment ); 

    

    TiXmlElement * msgs = new TiXmlElement( “Messages” ); 

    root->LinkEndChild( msgs ); 

    

    msg = new TiXmlElement( “Welcome” ); 

    msg->LinkEndChild( new TiXmlText( “Welcome to MyApp” )); 

    msgs->LinkEndChild( msg ); 

    

    msg = new TiXmlElement( “Farewell” ); 

    msg->LinkEndChild( new TiXmlText( “Thank you for using MyApp” )); 

    msgs->LinkEndChild( msg ); 

    

    TiXmlElement * windows = new TiXmlElement( “Windows” ); 

    root->LinkEndChild( windows ); 

    

    TiXmlElement * window;

    window = new TiXmlElement( “Window” ); 

    windows->LinkEndChild( window ); 

    window->SetAttribute(“name”“MainFrame”);

    window->SetAttribute(“x”, 5);

    window->SetAttribute(“y”, 15);

    window->SetAttribute(“w”, 400);

    window->SetAttribute(“h”, 250);

    

    TiXmlElement * cxn = new TiXmlElement( “Connection” ); 

    root->LinkEndChild( cxn ); 

    cxn->SetAttribute(“ip”“192.168.0.1”);

    cxn->SetDoubleAttribute(“timeout”, 123.456); // 浮点数属性

    

    dump_to_stdout( &doc );

    doc.SaveFile( “appsettings.xml” ); 

}

dump_to_stdout函数将显示如下结构:

 

Document

  • Declaration

  • Element [MyApp]

  (No attributes)

  + Comment: [ Settings for MyApp ]

  + Element [Messages]

  (No attributes)

    + Element [Welcome]

  (No attributes)

      + Text: [Welcome to MyApp]

    + Element [Farewell]

  (No attributes)

      + Text: [Thank you for using MyApp]

  + Element [Windows]

  (No attributes)

    + Element [Window]

      + name: value=[MainFrame]

      + x: value=[5] int=5 d=5.0

      + y: value=[15] int=15 d=15.0

      + w: value=[400] int=400 d=400.0

      + h: value=[250] int=250 d=250.0

      5 attributes

  + Element [Connection]

    + ip: value=[192.168.0.1] int=192 d=192.2

    + timeout: value=[123.456000] int=123 d=123.5

    2 attributes

TinyXML默认以其它APIs称作“pretty”格式的方式来输出XML,对此我感到惊讶。这种格式修改了元素的文本结点中的空格,以使输出来的结点树包含一个嵌套层标记。

我还没有仔细看当写到一个文件中时是否有办法关闭这种缩进——这肯定很容易做到。(译注:这两句话大概是Ellers说的

[Lee:在STL模式下这很容易做到,只需要cout << myDoc就行了。在非STL模式下就总是用“pretty”格式了,加多一个开关是一个很好的特性,这已经被要求过了。]

XML与C++对象的相互转化

介绍

这个例子假设你在用一个XML文件来加载和保存你的应用程序配置,举例来说,有点像example4.xml。

有许多方法可以做到这点。例如,看看TinyBind项目:http://sourceforge.net/projects/tinybind

这一节展示了一种普通老式的方法来使用XML加载和保存一个基本的对象结构。

建立你的对象类

从一些像这样的基本类开始:

 

#include <string>

#include <map>

using namespace std;

 

typedef std::map<std::string,std::string> MessageMap;

 

// 基本的窗口抽象 - 仅仅是个示例

class WindowSettings

{

    public:

    int x,y,w,h;

    string name;

    

    WindowSettings()

        : x(0), y(0), w(100), h(100), name(“Untitled”)

    {

    }

    

    WindowSettings(int x, int y, int w, int h, const string& name)

    {

        this->x=x;

        this->y=y;

        this->w=w;

        this->h=h;

        this->name=name;

    }

};

&nbsp;

class ConnectionSettings

{

    public:

    string ip;

    double timeout;

};

 

class AppSettings

{

    public:

    string m_name;

    MessageMap m_messages;

    list<WindowSettings> m_windows;

    ConnectionSettings m_connection;

    

    AppSettings() {}

    

    void save(const char* pFilename);

    void load(const char* pFilename);

    

    // 仅用于显示它是如何工作的

    void setDemoValues()

    {

        m_name=“MyApp”;

        m_messages.clear();

        m_messages[“Welcome”]=“Welcome to ”+m_name;

        m_messages[“Farewell”]=“Thank you for using ”+m_name;

        m_windows.clear();

        m_windows.push_back(WindowSettings(15,15,400,250,“Main”));

        m_connection.ip=“Unknown”;

        m_connection.timeout=123.456;

    }

};

这是一个基本的mian(),它向我们展示了怎样创建一个默认的settings对象树,怎样保存并再次加载:

 

int main(void)

{

    AppSettings settings;

    

    settings.save(“appsettings2.xml”);

    settings.load(“appsettings2.xml”);

    return 0;

}

接下来的main()展示了如何创建,修改,保存和加载一个settings结构:

 

int main(void)

{

    // 区块:定制并保存settings

    {

        AppSettings settings;

        settings.m_name=“HitchHikerApp”;

        settings.m_messages[“Welcome”]=“Don’t Panic”;

        settings.m_messages[“Farewell”]=“Thanks for all the fish”;

        settings.m_windows.push_back(WindowSettings(15,25,300,250,“BookFrame”));

        settings.m_connection.ip=“192.168.0.77”;

        settings.m_connection.timeout=42.0;

        

        settings.save(“appsettings2.xml”);

    }

    

    // 区块:加载settings

    {

        AppSettings settings;

        settings.load(“appsettings2.xml”);

        printf(“%s: %s\n”, settings.m_name.c_str(), 

        settings.m_messages[“Welcome”].c_str());

        WindowSettings & w=settings.m_windows.front();

        printf(“%s: Show window ’%s’ at %d,%d (%d x %d)\n”

        settings.m_name.c_str(), w.name.c_str(), w.x, w.y, w.w, w.h);

        printf(“%s: %s\n”, settings.m_name.c_str(),


                           settings.m_messages[“Farewell”].c_str());

    }

    return 0;

}

当save()和load()完成后(请看下面),运行这个main()就会在控制台看到:

 

HitchHikerApp: Don’t Panic

HitchHikerApp: Show window ‘BookFrame’ at 15,25 (300 x 100)

HitchHikerApp: Thanks for all the fish

把C++状态编码成XML

有很多方法能够做到把文档对象保存到文件中,这就是其中一个:

 

void AppSettings::save(const char* pFilename)

{

    TiXmlDocument doc; 

    TiXmlElement* msg;

    TiXmlComment * comment;

    string s;

    TiXmlDeclaration* decl = new TiXmlDeclaration( “1.0”“”“” ); 

    doc.LinkEndChild( decl ); 

    

    TiXmlElement * root = new TiXmlElement(m_name.c_str()); 

    doc.LinkEndChild( root ); 

    

    comment = new TiXmlComment();

    s=“ Settings for ”+m_name+“ ”;

    comment->SetValue(s.c_str()); 

    root->LinkEndChild( comment ); 

    

    // 区块:messages

    {

        MessageMap::iterator iter;

        

        TiXmlElement * msgs = new TiXmlElement( “Messages” ); 

        root->LinkEndChild( msgs ); 

        

        for (iter=m_messages.begin(); iter != m_messages.end(); iter++)

        {

            const string & key=(*iter).first;

            const string & value=(*iter).second;

            msg = new TiXmlElement(key.c_str()); 

            msg->LinkEndChild( new TiXmlText(value.c_str())); 

            msgs->LinkEndChild( msg ); 

        }

    }

    

    // 区块:windows

    {

        TiXmlElement * windowsNode = new TiXmlElement( “Windows” ); 

        root->LinkEndChild( windowsNode ); 

        

        list<WindowSettings>::iterator iter;

        

        for (iter=m_windows.begin(); iter != m_windows.end(); iter++)

        {

            const WindowSettings& w=*iter;

            

            TiXmlElement * window;

            window = new TiXmlElement( “Window” ); 

            windowsNode->LinkEndChild( window ); 

            window->SetAttribute(“name”, w.name.c_str());

            window->SetAttribute(“x”, w.x);

            window->SetAttribute(“y”, w.y);

            window->SetAttribute(“w”, w.w);

            window->SetAttribute(“h”, w.h);

        }

    }

    

    // 区块:connection

    {

        TiXmlElement * cxn = new TiXmlElement( “Connection” ); 

        root->LinkEndChild( cxn ); 

        cxn->SetAttribute(“ip”, m_connection.ip.c_str());

        cxn->SetDoubleAttribute(“timeout”, m_connection.timeout); 

    }

    

    doc.SaveFile(pFilename); 

}

用修改过的main运行会生成这个文件:

 

<?xml version="1.0" ?>

<HitchHikerApp>

   <!– Settings for HitchHikerApp –>

   <Messages>

      <Farewell>Thanks for all the fish</Farewell>

      <Welcome>Don&apos;t Panic</Welcome>

   </Messages>

   <Windows>

      <Window name=”BookFrame” x=”15” y=”25” w=”300” h=”250” />

   </Windows>

   <Connection ip=”192.168.0.77” timeout=”42.000000” />

</HitchHikerApp>

从XML中解码出状态

就像编码一样,也有许多方法可以让你从自己的C++对象结构中解码出XML。下面的方法使用了TiXmlHandles。

 

void AppSettings::load(const char* pFilename)

{

    TiXmlDocument doc(pFilename);

    if (!doc.LoadFile()) return;

    

    TiXmlHandle hDoc(&doc);

    TiXmlElement* pElem;

    TiXmlHandle hRoot(0);

    

    // 区块:name

    {

        pElem=hDoc.FirstChildElement().Element();

        // 必须有一个合法的根结点,如果没有则温文地处理(译注:直接返回

        if (!pElem) return;

        m_name=pElem->Value();

        

        // 保存起来以备后面之用

        hRoot=TiXmlHandle(pElem);

    }

    

    // 区块:string table

    {

        m_messages.clear(); // 清空已有的table

        

        pElem=hRoot.FirstChild( “Messages” ).FirstChild().Element();

        for( pElem; pElem; pElem=pElem->NextSiblingElement())

        {

            const char *pKey=pElem->Value();

            const char *pText=pElem->GetText();

            if (pKey && pText) 

            {

                m_messages[pKey]=pText;

            }

        }

    }

    

    // 区块:windows

    {

        m_windows.clear(); // 清空链表

        

        TiXmlElement* pWindowNode=hRoot.FirstChild( “Windows” )


                                       .FirstChild().Element();

        for( pWindowNode; pWindowNode;


             pWindowNode=pWindowNode->NextSiblingElement())

        {

            WindowSettings w;

            const char *pName=pWindowNode->Attribute(“name”);

            if (pName) w.name=pName;

            

            pWindowNode->QueryIntAttribute(“x”, &w.x); // 如果失败,原值保持现状

            pWindowNode->QueryIntAttribute(“y”, &w.y);

            pWindowNode->QueryIntAttribute(“w”, &w.w);

            pWindowNode->QueryIntAttribute(“hh”, &w.h);

            

            m_windows.push_back(w);

        }

    }

    

    // 区块:connection

    {

        pElem=hRoot.FirstChild(“Connection”).Element();

        if (pElem)

        {

            m_connection.ip=pElem->Attribute(“ip”);

            pElem->QueryDoubleAttribute(“timeout”,&m_connection.timeout);

        }

    }

}

dump_to_stdout的完整列表

下面是一个可直接运行的示例程序,使用上面提到过的递归遍历方式,可用来加载任意的XML文件并把结构输出到STDOUT上。

 

// 指南示例程序

#include “stdafx.h”

#include “tinyxml.h”

 

// ———————————————————————-

// STDOUT输出和缩进实用函数

// ———————————————————————-

const unsigned int NUM_INDENTS_PER_SPACE=2;

 

const char * getIndent( unsigned int numIndents )

{

    static const char * pINDENT=“ + ”;

    static const unsigned int LENGTH=strlen( pINDENT );

    unsigned int n=numIndents*NUM_INDENTS_PER_SPACE;

    if ( n > LENGTH ) n = LENGTH;

    

    return &pINDENT[ LENGTH-n ];

}

 

// 与getIndent相同,但最后没有“+”

const char * getIndentAlt( unsigned int numIndents )

{

    static const char * pINDENT=“ ”;

    static const unsigned int LENGTH=strlen( pINDENT );

    unsigned int n=numIndents*NUM_INDENTS_PER_SPACE;

    if ( n > LENGTH ) n = LENGTH;

    

    return &pINDENT[ LENGTH-n ];

}

 

int dump_attribs_to_stdout(TiXmlElement* pElement, unsigned int indent)

{

    if ( !pElement ) return 0;

    

    TiXmlAttribute* pAttrib=pElement->FirstAttribute();

    int i=0;

    int ival;

    double dval;

    const char* pIndent=getIndent(indent);

    printf(“\n”);

    while (pAttrib)

    {

        printf( “%s%s: value=[%s]”, pIndent, pAttrib->Name(), pAttrib->Value());

        

        if (pAttrib->QueryIntValue(&ival)==TIXML_SUCCESS) printf( “ int=%d”, ival);

        if (pAttrib->QueryDoubleValue(&dval)==TIXML_SUCCESS) printf( “ d=%1.1f”, dval);

        printf( “\n” );

        i++;

        pAttrib=pAttrib->Next();

    }

    return i; 

}

 

void dump_to_stdout( TiXmlNode* pParent, unsigned int indent = 0 )

{

    if ( !pParent ) return;

    

    TiXmlNode* pChild;

    TiXmlText* pText;

    int t = pParent->Type();

    printf( “%s”, getIndent(indent));

    int num;

    

    switch ( t )

    {

        case TiXmlNode::DOCUMENT:

            printf( “Document” );

            break;

        

        case TiXmlNode::ELEMENT:

            printf( “Element [%s]”, pParent->Value() );

            num=dump_attribs_to_stdout(pParent->ToElement(), indent+1);

            switch(num)

            {

                case 0: printf( “ (No attributes)”); break;

                case 1: printf( “%s1 attribute”, getIndentAlt(indent)); break;

                default: printf( “%s%d attributes”, getIndentAlt(indent), num); break;

            }

            break;

        

        case TiXmlNode::COMMENT:

            printf( “Comment: [%s]”, pParent->Value());

            break;

        

        case TiXmlNode::UNKNOWN:

            printf( “Unknown” );

            break;

        

        case TiXmlNode::TEXT:

            pText = pParent->ToText();

            printf( “Text: [%s]”, pText->Value() );

            break;

        

        case TiXmlNode::DECLARATION:

            printf( “Declaration” );

            break;

            default:

            break;

    }

    printf( “\n” );

    for ( pChild = pParent->FirstChild(); pChild != 0; pChild = pChild->NextSibling()) 

    {

        dump_to_stdout( pChild, indent+1 );

    }

}

 

// 加载指定的文件并把它的结构输出到STDOUT上

void dump_to_stdout(const char* pFilename)

{

    TiXmlDocument doc(pFilename);

    bool loadOkay = doc.LoadFile();

    if (loadOkay)

    {

        printf(“\n%s:\n”, pFilename);

        dump_to_stdout( &doc ); 

    }

    else

    {

        printf(“Failed to load file "%s&#8221;\n”, pFilename);

    }

}

 

// ———————————————————————-

// main(),打印出从命令行指定的文件

// ———————————————————————-

int main(int argc, char* argv[])

{

    for (int i=1; i<argc; i++)

    {

        dump_to_stdout(argv[i]);

    }

    return 0;

}

从命令行或者DOS窗口运行它,例如:

 

C:\dev\tinyxml> Debug\tinyxml_1.exe example1.xml

example1.xml:

Document

  • Declaration

  • Element [Hello]

  (No attributes)

  + Text: [World]

作者与修改

    <li><em>Ellers写于2005年4,5,6月</em>
    
    <li><em>Lee Thomason于2005年9月略加编辑后集成到文档系统中</em>
    
    <li><em>Ellers于2005年10月做了更新</em> </li>
    

译注:本文是TinyXML 2.5.2版本Document的中文文档,经原作者Lee Thomason同意由hansen翻译,如有误译或者错漏,欢迎指正。
版权:版权归原作者所有,翻译文档版权归本人hansen所有,转载请注明出处。
原文:http://www.grinninglizard.com/tinyxmldocs/index.html


 

TinyXml 文档

2.5.2

TinyXML

TinyXML是一个简单小巧,可以很容易集成到其它程序中的C++ XML解析器。

它能做些什么

简单地说,TinyXML解析一个XML文档并由此生成一个可读可修改可保存的文档对象模型(DOM)。

XML的意思是“可扩展标记语言“(eXtensible Markup Language)。它允许你创建你自己的文档标记。在为浏览器标记文档方面HTML做得很好,然而XML允许你定义任何文档标记,比如可以为一个组织者应用程序定义一个描述“to do”列表的文档。 XML拥有一个结构化并且方便的格式,所有为存储应用程序数据而创建的随机文件格式都可以用XML代替,而这一切只需要一个解析器。

最全面正确的说明可以在http://www.w3.org/TR/2004/REC-xml-20040204/找到,但坦白地说,它很晦涩难懂。事实上我喜欢http://skew.org/xml/tutorial上关于XML的介绍。

有不同的方法可以访问和与XML数据进行交互。TinyXML使用文档对象模型(DOM),这意味着XML数据被解析成一个可被浏览和操作的C++对象,然后它可以被写到磁盘或者另一个输出流中。你也可以把C++对象构造成一个XML文档然后把它写到磁盘或者另一个输出流中。

TinyXML被设计得容易快速上手。它只有两个头文件和四个cpp文件。只需要把它们简单地加到你的项目中就行了。有一个例子文件——xmltest.cpp来引导你该怎么做。

TinyXML以Zlib许可来发布,所以你可以在开源或者商业软件中使用它。许可证更具体的描述在每个源代码文件的顶部可以找到。

TinyXML在保证正确和恰当的XML输出的基础上尝试成为一个灵活的解析器。TinyXML可以在任何合理的C++适用系统上编译。它不依赖于异常或者运行时类型信息,有没有STL支持都可以编译。TinyXML完全支持UTF-8编码和前64k个字符实体(<i>译注:如果你不明白这句译文,可能你需要了解一下Unicode编码</i>)。

它无法做些什么

TinyXML不解析不使用DTDs(文档类型定义)或者XSLs(可扩展样式表语言)。有其它解析器(到www.sourceforge.org搜索一下XML)具有更加全面的特性,但它们也就更大,需要花更长的时间来建立你的项目,有更陡的学习曲线,而且经常有一个更严格的许可协议。如果你是用于浏览器或者有更复杂的XML需要,那么TinyXML不适合你。

下面的DTD语法在TinyXML里是不做解析的:

 

<!DOCTYPE Archiv [
<!ELEMENT Comment (#PCDATA)>
]>

因为TinyXML把它看成是一个带着非法嵌入!ELEMENT结点的!DOCTYPE结点。或许这在将来会得到支持。

指南

有耐性些,这是一份能很好地指导你怎么开始的指南,它(非常短小精悍)值得你花时间完整地读上一遍。

代码状况

TinyXML是成熟且经过测试的代码,非常健壮。如果你发现了漏洞,请提交漏洞报告到sourcefore网站上 (www.sourceforge.net/projects/tinyxml)。 我们会尽快修正。

有些地方可以让你得到提高,如果你对TinyXML的工作感兴趣的话可以上sourceforge查找一下。

相关项目

你也许会觉得TinyXML很有用!(简介由项目提供)

特性

使用STL

TinyXML可以被编译成使用或不使用STL。如果使用STL,TinyXML会使用std::string类,而且完全支持std::istream,std::ostream,operator<<和operator>>。许多API方法都有 ‘const char*’和’const std::string&’两个版本。

如果被编译成不使用STL,则任何STL都不会被包含。所有string类都由TinyXML它自己实现。所有API方法都只提供’const char*’传入参数。

使用运行时定义:

TIXML_USE_STL

来编译成不同的版本。这可以作为参数传给编译器或者在“tinyxml.h”文件的第一行进行设置。

注意:如果在Linux上编译测试代码,设置环境变量TINYXML_USE_STL=YES/NO可以控制STL的编译。而在Windows上,项目文件提供了STL和非STL两种目标文件。在你的项目中,在tinyxml.h的第一行添加"#define TIXML_USE_STL"应该是最简单的。

UTF-8

TinyXML支持UTF-8,所以可以处理任何语言的XML文件,而且TinyXML也支持“legacy模式”——一种在支持UTF-8之前使用的编码方式,可能最好的解释是“扩展的ascii”。

正常情况下,TinyXML会检测出正确的编码并使用它,然而,通过设置头文件中的TIXML_DEFAULT_ENCODING值,TinyXML可以被强制成总是使用某一种编码。

除非以下情况发生,否则TinyXML会默认使用Legacy模式:

  1. 如果文件或者数据流以非标准但普遍的"UTF-8引导字节" (0xef 0xbb 0xbf)开始,TinyXML会以UTF-8的方式来读取它。
  2. 如果包含有encoding="UTF-8"的声明被读取,那么TinyXML会以UTF-8的方式来读取它。
  3. 如果读取到没有指定编码方式的声明,那么TinyXML会以UTF-8的方式来读取它。
  4. 如果包含有encoding=“其它编码”的声明被读取,那么TinyXML会以Legacy模式来读取它。在Legacy模式下,TinyXML会像以前那样工作,虽然已经不是很清楚这种模式是如何工作的了,但旧的内容还得保持能够运行。
  5. 除了上面提到的情况,TinyXML会默认运行在Legacy模式下。

如果编码设置错误或者检测到错误会发生什么事呢?TinyXML会尝试跳过这些看似不正确的编码,你可能会得到一些奇怪的结果或者乱码,你可以强制TinyXML使用正确的编码模式。

通过使用LoadFile( TIXML_ENCODING_LEGACY )或者LoadFile( filename, TIXML_ENCODING_LEGACY ), 你可以强制TinyXML使用Legacy模式。你也可以通过设置TIXML_DEFAULT_ENCODING = TIXML_ENCODING_LEGACY来强制一直使用Legacy模式。同样的,你也可以通过相同的方法来强制设置成TIXML_ENCODING_UTF8。

对于使用英文XML的英语用户来说,UTF-8跟low-ASCII是一样的。你不需要知道UTF-8或者一点也不需要修改你的代码。你可以把UTF-8当作是ASCII的超集。

UTF-8并不是一种双字节格式,但它是一种标准的Unicode编码!TinyXML当前不使用或者直接支持wchar,TCHAR,或者微软的_UNICODE。"Unicode"这个术语被普遍地认为指的是UTF-16(一种unicode的宽字节编码)是不适当的,这是混淆的来源。

对于“high-ascii”语言来说——几乎所有非英语语言,只要XML被编码成UTF-8, TinyXML就能够处理。说起来可能有点微妙,比较旧的程序和操作系统趋向于使用“默认”或者“传统”的编码方式。许多应用程序(和几乎所有现在的应用程序)都能够输出UTF-8,但是那些比较旧或者难处理的(或者干脆不能使用的)系统还是只能以默认编码来输出文本。

比如说,日本的系统传统上使用SHIFT-JIS编码,这种情况下TinyXML就无法读取了。但是一个好的文本编辑器可以导入SHIFT-JIS的文本然后保存成UTF-8编码格式的。

Skew.org link上关于转换编码的话题做得很好。

测试文件“utf8test.xml”包含了英文、西班牙文、俄文和简体中文(希望它们都能够被正确地转化)。“utf8test.gif”文件是从IE上截取的XML文件快照。请注意如果你的系统上没有正确的字体(简体中文或者俄文),那么即使你正确地解析了也看不到与GIF文件上一样的输出。同时要注意在一个西方编码的控制台上(至少我的Windows机器是这样),Print()或者printf()也无法正确地显示这个文件,这不关TinyXML的事——这只是操作系统的问题。TinyXML没有丢掉或者损坏数据,只是控制台无法显示UTF-8而已。

实体

TinyXML认得预定义的特殊“字符实体”,即:

 

&amp; &
&lt; <
&gt; >
&quot; "
&apos; ‘

这些在XML文档读取时都会被辨认出来,并会被转化成等价的UTF-8字符。比如下面的XML文本:

 

Far &amp; Away

从TiXmlText 对象查询出来时会变成"Far & Away"这样的值,而写回XML流/文件时会以“&amp;”的方式写回。老版本的TinyXML“保留”了字符实体,而在新版本中它们会被转化成字符串。

另外,所有字符都可以用它的Unicode编码数字来指定, " "和" "都表示不可分的空格字符。

打印

TinyXML有几种不同的方式来打印输出,当然它们各有各的优缺点。

  • Print( FILE* ):输出到一个标准C流中,包括所有的C文件和标准输出。
    • "相当漂亮的打印", 但你没法控制打印选项。
    • 输出数据直接写到FILE对象中,所以TinyXML代码没有内存负担。
    • 被Print()和SaveFile()调用。

     

  • operator<<:输出到一个c++流中。
    • 与C++ iostreams集成在一起。
    • 在"network printing"模式下输出没有换行符,这对于网络传输和C++对象之间的XML交换有好处,但人很难阅读。
  • TiXmlPrinter:输出到一个std::string或者内存缓冲区中。
    • API还不是很简练。
    • 将来会增加打印选项。
    • 在将来的版本中可能有些细微的变化,因为它会被改进和扩展。

设置了TIXML_USE_STL,TinyXML就能支持C++流(operator <<,>>)和C(FILE*)流。但它们之间有些差异你需要知道:

C风格输出:

  • 基于FILE*
  • 用Print()和SaveFile()方法

生成具有很多空格的格式化过的输出,这是为了尽可能让人看得明白。它们非常快,而且能够容忍XML文档中的格式错误。例如一个XML文档包含两个根元素和两个声明仍然能被打印出来。

C风格输入:

  • 基于FILE*
  • 用Parse()和LoadFile()方法

速度快,容错性好。当你不需要C++流时就可以使用它。

C++风格输出:

  • 基于std::ostream
  • operator<<

生成压缩过的输出,目的是为了便于网络传输而不是为了可读性。它可能有些慢(可能不会),这主要跟你系统上ostream类的实现有关。无法容忍格式错误的XML:此文档只能包含一个根元素。另外根级别的元素无法以流形式输出。

C++风格输入:

  • 基于std::istream
  • operator>>

从流中读取XML使其可用于网络传输。通过些小技巧,它知道当XML文档读取完毕时,流后面的就一定是其它数据了。TinyXML总假定当它读取到根结点后XML数据就结束了。换句话说,那些具有不止一个根元素的文档是无法被正确读取的。另外还要注意由于STL的实现和TinyXML的限制,operator>>会比Parse慢一些。

空格

对是保留还是压缩空格这一问题人们还没达成共识。举个例子,假设‘_’代表一个空格,对于"Hello____world",HTML和某些XML解析器会解释成"Hello_world",它们压缩掉了一些空格。而有些XML解析器却不会这样,它们会保留空格,于是就是“Hello____world”(记住_表示一个空格)。其它的还建议__Hello___world__应该变成Hello___world 。

这是一个解决得不能让我满意的问题。TinyXML一开始就两种方式都支持。调用TiXmlBase::SetCondenseWhiteSpace( bool )来设置你想要的结果,默认是压缩掉多余的空格。

如果想要改变默认行为,你应该在解析任何XML数据之前调用TiXmlBase::SetCondenseWhiteSpace( bool ) ,而且我不建议设置之后再去改动它。

句柄

想要健壮地读取一个XML文档,检查方法调用后的返回值是否为null是很重要的。一种安全的检错实现可能会产生像这样的代码:

 

TiXmlElement* root = document.FirstChildElement( "Document" );
if ( root )
{
    TiXmlElement* element = root->FirstChildElement( "Element" );
    if ( element )
    {
        TiXmlElement* child = element->FirstChildElement( "Child" );
        if ( child )
        {
            TiXmlElement* child2 = child->NextSiblingElement( "Child" );
            if ( child2 )
            {
                // Finally do something useful.

用句柄的话就不会这么冗长了,使用TiXmlHandle类,前面的代码就会变成这样:

 

TiXmlHandle docHandle( &document );
TiXmlElement* child2 = docHandle.FirstChild( "Document" ).FirstChild( "Element" ).Child( "Child", 1 ).ToElement();
if ( child2 )
{
    // do something useful

这处理起来容易多了。 查阅TiXmlHandle可以得到更多的信息。

行列追踪

对于某些应用程序来说,能够追踪节点和属性在它们源文件中的原始位置是很重要的。另外,知道解析错误在源文件中的发生位置可以节省大量时间。

TinyXML能够追踪所有结点和属性在文本文件中的行列原始位置。TiXmlBase::Row() 和 TiXmlBase::Column() 方法返回结点在源文件中的原始位置。正确的制表符号可以经由TiXmlDocument::SetTabSize() 来配置。

使用与安装

编译与运行xmltest:

提供了一个Linux Makefile和一个Windows Visual C++ .dsw 文件。只需要简单地编译和运行,它就会在你的磁盘上生成demotest.xml文件并在屏幕上输出。它还尝试用不同的方法遍历DOM并打印出结点数。

那个Linux makefile很通用,可以运行在很多系统上——它目前已经在mingw和MacOSX上测试过。你不需要运行 ‘make depend’,因为那些依赖关系已经硬编码在文件里了。

用于VC6的Windows项目文件

  • tinyxml: tinyxml 库,非STL
  • tinyxmlSTL: tinyxml 库,STL
  • tinyXmlTest: 用于测试的应用程序,非STL
  • tinyXmlTestSTL: 用于测试的应用程序,STL

Makefile

在makefile的顶部你可以设置:

PROFILE,DEBUG,和TINYXML_USE_STL。makefile里有具体描述。

在tinyxml目录输入“make clean”然后“make”,就可以生成可执行的“xmltest”文件。

在某一应用程序中使用:

把tinyxml.cpp,tinyxml.h, tinyxmlerror.cpp, tinyxmlparser.cpp, tinystr.cpp, 和 tinystr.h 添加到你的项目和makefile中。就这么简单,它可以在任何合理的C++适用系统上编译。不需要为TinyXML打开异常或者运行时类型信息支持。

TinyXML怎么工作

举个例子可能是最好的办法,理解一下:

 

<?xml version="1.0" standalone=no>
<!– Our to do list data –>
<ToDo>
<Item priority="1"> Go to the <bold>Toy store!</bold></Item>
<Item priority="2"> Do bills</Item>
</ToDo>

它称不上是一个To Do列表,但它已经足够了。像下面这样读取并解析这个文件(叫“demo.xml”)你就能创建一个文档:

TiXmlDocument doc( "demo.xml" );
doc.LoadFile();

现在它准备好了,让我们看看其中的某些行和它们怎么与DOM联系起来。

 

<?xml version="1.0" standalone=no>

第一行是一个声明,它会转化成TiXmlDeclaration 类,同时也是文档结点的第一个子结点。

这是TinyXML唯一能够解析的指令/特殊标签。一般来说指令标签会保存在TiXmlUnknown 以保证在它保存回磁盘时不会丢失这些命令。

 

<!– Our to do list data –>

这是一个注释,会成为一个TiXmlComment对象。

 

<ToDo>

"ToDo"标签定义了一个TiXmlElement 对象。它没有任何属性,但包含另外的两个元素。

 

<Item priority="1">

生成另一个TiXmlElement对象,它是“ToDo”元素的子结点。此元素有一个名为“priority”和值为“1”的属性。

 

Go to the

TiXmlText ,这是一个叶子结点,它不能再包含其它结点,是"Item" TiXmlElement的子结点。

 

<bold>

另一个TiXmlElement, 这也是“Item”元素的子结点。

等等

最后,看看整个对象树:

 

TiXmlDocument "demo.xml"
TiXmlDeclaration "version=’1.0′" "standalone=no"
TiXmlComment " Our to do list data"
TiXmlElement "ToDo"
TiXmlElement "Item" Attribtutes: priority = 1
TiXmlText "Go to the "
TiXmlElement "bold"
TiXmlText "Toy store!"
TiXmlElement "Item" Attributes: priority=2
TiXmlText "Do bills"

 

文档

本文档由Doxygen使用‘dox’配置文件生成。

许可证

TinyXML基于zlib许可证来发布:

本软件按“现状”提供(即现在你看到的样子),不做任何明确或隐晦的保证。由使用此软件所引起的任何损失都决不可能由作者承担。

只要遵循下面的限制,就允许任何人把这软件用于任何目的,包括商业软件,也允许修改它并自由地重新发布:

1. 决不能虚报软件的来源;你决不能声称是你是软件的第一作者。如果你在某个产品中使用了这个软件,那么在产品文档中加入一个致谢辞我们会很感激,但这并非必要。

2. 修改了源版本就应该清楚地标记出来,决不能虚报说这是原始软件。

3. 本通告不能从源发布版本中移除或做修改。

参考书目

万维网联盟是定制XML的权威标准机构,它的网页上有大量的信息。

权威指南:http://www.w3.org/TR/2004/REC-xml-20040204/

我还要推荐由OReilly出版由Robert Eckstein撰写的"XML Pocket Reference"……这本书囊括了入门所需要的一切。

捐助者,联系人,还有简史

非常感谢给我们建议,漏洞报告,意见和鼓励的所有人。它们很有用,并且使得这个项目变得有趣。特别感谢那些捐助者,是他们让这个网站页面生机勃勃。

有很多人发来漏洞报告和意见,与其在这里一一列出来不如我们试着把它们写到“changes.txt”文件中加以赞扬。

TinyXML的原作者是Lee Thomason(文档中还经常出现“我”这个词) 。在Yves Berquin,Andrew Ellerton,和tinyXml社区的帮助下,Lee查阅修改和发布新版本。

我们会很感激你的建议,还有我们想知道你是否在使用TinyXML。希望你喜欢它并觉得它很有用。请邮寄问题,评论,漏洞报告给我们,或者你也可登录网站与我们取得联系:

www.sourceforge.net/projects/tinyxml

Lee Thomason, Yves Berquin, Andrew Ellerton

开发软件时经常需要把一些东西做成可配置的,于是就需要用到配置文件,以前多是用ini文件,然后自己写个类来解析。现在有了XML,许多应用软件就喜欢把配置文件做成XML格式。但是如果我们的程序本身很小,为了读取个配置文件却去用Xerces XML之类的库,恐怕会得不偿失。那么用TinyXML吧,它很小,只有六个文件,加到项目中就可以开始我们的配置文件之旅了。

前些时候我恰好就用TinyXML写了一个比较通用的配置文件类,基本可以适应大部分的场合,不过配置文件只支持两层结构,如果需要支持多层嵌套结构,那还需要稍加扩展一下。

从下面的源代码中,你也可以看到怎么去使用TinyXML,也算是它的一个应用例子了。

 

/*

** FileName:    config.h

** Author:        hansen

** Date:        May 11, 2007

** Comment:        配置文件类,主要用来读取xml配置文件中的一些配置信息

*/

 

#ifndef _CONFIG

#define _CONFIG

 

#include <string>

#include “tinyxml.h”

 

using namespace std;

 

class CConfig

{

public:

    explicit CConfig(const char* xmlFileName)

        :mXmlConfigFile(xmlFileName),mRootElem(0)

    {

        //加载配置文件

        mXmlConfigFile.LoadFile();    

        

        //得到配置文件的根结点

        mRootElem=mXmlConfigFile.RootElement();

    }

 

public:

    //得到nodeName结点的值

    string GetValue(const string& nodeName);

 

private:

    //禁止默认构造函数被调用

    CMmsConfig();

 

private:

    TiXmlDocument    mXmlConfigFile;

    TiXmlElement*    mRootElem;

 

};

 

#endif

 

 

/*

** FileName:    config.cpp

** Author:        hansen

** Date:        May 11, 2007

** Comment:        

*/

 

#include “config.h”

#include <iostream>

 

string CConfig::GetValue(const string& nodeName)

{

    if(!mRootElem)

    {

        cout<<“读取根结点出错”<<endl;

        return “”;

    }

 

    TiXmlElement* pElem=mRootElem->FirstChildElement(nodeName.c_str());

    if(!pElem)

    {

        cout<<“读取”<<nodeName<<“结点出错”<<endl;

        return “”;

    }

 

    return pElem->GetText();

 

}

 

 

int main()

{

    CConfig xmlConfig(“XmlConfig.xml”);

 

    //获取Author的值

    string author = xmlConfig.GetValue(“Author”);

    cout<<“Author:”<<author<<endl;

 

    //获取Site的值

    string site = xmlConfig.GetValue(“Site”);

    cout<<“Site:”<<site<<endl;

 

    //获取Desc的值

    string desc = xmlConfig.GetValue(“Desc”);

    cout<<“Desc:”<<desc<<endl;

    

    return 0;

}

 

假设配置文件是这样的:

<!– XmlConfig.xml –> 

<?xml version=“1.0” encoding=“GB2312” ?>

<Config>

    <Author>hansen</Author>

    <Site>www.hansencode.cn</Site>

    <Desc>这是个测试程序</Desc>

</Config>

 

怎么使用上面的配置类来读取XmlConfig.xml文件中的配置呢?很简单:

int main()

{

    CConfig xmlConfig(“XmlConfig.xml”);

 

    //获取Author的值

    string author = xmlConfig.GetValue(“Author”);

    cout<<“Author:”<<author<<endl;

 

    //获取Site的值

    string site = xmlConfig.GetValue(“Site”);

    cout<<“Site:”<<site<<endl;

 

    //获取Desc的值

    string desc = xmlConfig.GetValue(“Desc”);

    cout<<“Desc:”<<desc<<endl;

    

    return 0;

}

 

运行结果如下:

D:\config\Debug>config.exe

Author:hansen

Site:www.hansencode.cn

Desc:这是个测试程序

 

点这里下载本文的配套代码

引子

2006年,中国互联网上的斗争硝烟弥漫。这时的战场上,先前颇为流行的窗口挂钩、API挂钩、进程注入等技术已然成为昨日黄花,大有逐渐淡出之势;取而代之的,则是更狠毒、更为赤裸裸的词汇:驱动、隐藏进程、Rootkit……

前不久,我不经意翻出自己2005年9月写下的一篇文章《DLL的远程注入技术》,在下面看到了一位名叫L4bm0s的网友说这种技术已经过时了。虽然我也曾想过拟出若干辩解之词聊作应对,不过最终还是作罢了——毕竟,拿出些新的、有技术含量的东西才是王道。于是这一次,李马首度从ring3(应用层)的围城跨出,一跃而投身于ring0(内核层)这一更广阔的天地,便有了这篇《城里城外看SSDT》。——顾名思义,城里和城外的这一墙之隔,就是ring3与ring0的分界。

在这篇文章里,我会用到太多杂七杂八的东西,比如汇编,比如内核调试器,比如DDK。这诚然是一件令我瞻前顾后畏首畏尾的事情——一方面在ring0我不得不依靠这些东西,另一方面我实在担心它们会导致我这篇文章的阅读门槛过高。所以,我决定尽可能少地涉及驱动、内核与DDK,也不会对诸如如何使用内核调试器等问题作任何讲解——你只需要知道我大概在做些什么,这就足够了。

什么是SSDT?

什么是SSDT?自然,这个是我必须回答的问题。不过在此之前,请你打开命令行(cmd.exe)窗口,并输入“dir”并回车——好了,列出了当前目录下的所有文件和子目录。

那么,以程序员的视角来看,整个过程应该是这样的:

  1. 由用户输入dir命令。
  2. cmd.exe获取用户输入的dir命令,在内部调用对应的Win32 API函数FindFirstFile、FindNextFile和FindClose,获取当前目录下的文件和子目录。
  3. cmd.exe将文件名和子目录输出至控制台窗口,也就是返回给用户。

到此为止我们可以看到,cmd.exe扮演了一个非常至关重要的角色,也就是用户与Win32 API的交互。——你大概已经可以猜到,我下面要说到的SSDT亦必将扮演这个角色,这实在是一点新意都没有。

没错,你猜对了。SSDT的全称是System Services Descriptor Table,系统服务描述符表。这个表就是一个把ring3的Win32 API和ring0的内核API联系起来的角色,下面我将以API函数OpenProcess为例说明这个联系的过程。

你可以用任何反汇编工具来打开你的kernel32.dll,然后你会发现在OpenProcess中有类似这样的汇编代码:

call ds:NtOpenProcess

这就是说,OpenProcess调用了ntdll.dll的NtOpenProcess函数。那么继续反汇编之,你会发现ntdll.dll中的这个函数很短:

mov eax, 7Ah
mov edx, 7FFE0300h
call dword ptr [edx]
retn 10h

另外,call的一句实质是调用了KiFastSystemCall:

mov edx, esp
sysenter

上面是我的XP Professional sp2中ntdll.dll的反汇编结果,如果你用的是2000系统,那么可能是这个样子:

mov eax, 6Ah
lea edx, [esp+4]
int 2Eh
retn 10h

虽然它们存在着些许不同,但都可以这么来概括:

  1. 把一个数放入eax(XP是0x7A,2000是0x6A),这个数值称作系统的服务号。
  2. 把参数堆栈指针(esp+4)放入edx。
  3. sysenter或int 2Eh。

好了,你在ring3能看到的东西就到此为止了。事实上,在ntdll.dll中的这些函数可以称作真正的NT系统服务的存根(Stub)函数。分隔ring3与ring0城里城外的这一道叹息之墙,也正是由它们打通的。接下来SSDT就要出场了,come some music。

站在城墙看城外

插一句先,貌似到现在为止我仍然没有讲出来SSDT是个什么东西,真正可以算是“犹抱琵琶半遮面”了。——书接上文,在你调用sysenter或int 2Eh之后,Windows系统将会捕获你的这个调用,然后进入ring0层,并调用内核服务函数NtOpenProcess,这个过程如下图所示。

SSDT在这个过程中所扮演的角色是至关重要的。让我们先看一看它的结构,如下图。

当程序的处理流程进入ring0之后,系统会根据服务号(eax)在SSDT这个系统服务描述符表中查找对应的表项,这个找到的表项就是系统服务函数NtOpenProcess的真正地址。之后,系统会根据这个地址调用相应的系统服务函数,并把结果返回给ntdll.dll中的NtOpenProcess。图中的“SSDT”所示即为系统服务描述符表的各个表项;右侧的“ntoskrnl.exe”则为Windows系统内核服务进程(ntoskrnl即为NT OS KerneL的缩写),它提供了相对应的各个系统服务函数。ntoskrnl.exe这个文件位于Windows的system32目录下,有兴趣的朋友可以反汇编一下。

附带说两点。根据你处理器的不同,系统内核服务进程可能也是不一样的。真正运行于系统上的内核服务进程可能还有ntkrnlmp.exe、ntkrnlpa.exe这样的情况——不过为了统一起见,下文仍统称这个进程为ntoskrnl.exe。另外,SSDT中的各个表项也未必会全部指向ntoskrnl.exe中的服务函数,因为你机器上的杀毒监控或其它驱动程序可能会改写SSDT中的某些表项——这也就是所谓的“挂钩SSDT”——以达到它们的“主动防御”式杀毒方式或其它的特定目的。

KeServiceDescriptorTable

事实上,SSDT并不仅仅只包含一个庞大的地址索引表,它还包含着一些其它有用的信息,诸如地址索引的基地址、服务函数个数等等。ntoskrnl.exe中的一个导出项KeServiceDescriptorTable即是SSDT的真身,亦即它在内核中的数据实体。SSDT的数据结构定义如下:

typedef struct _tagSSDT {
    PVOID pvSSDTBase;
    PVOID pvServiceCounterTable;
    ULONG ulNumberOfServices;
    PVOID pvParamTableBase;
} SSDT, *PSSDT;

其中,pvSSDTBase就是上面所说的“系统服务描述符表”的基地址。pvServiceCounterTable则指向另一个索引表,该表包含了每个服务表项被调用的次数;不过这个值只在Checkd Build的内核中有效,在Free Build的内核中,这个值总为NULL(注:Check/Free是DDK的Build模式,如果你只使用SDK,可以简单地把它们理解为Debug/Release)。ulNumberOfServices表示当前系统所支持的服务个数。pvParamTableBase指向SSPT(System Service Parameter Table,即系统服务参数表),该表格包含了每个服务所需的参数字节数。

下面,让我们开看看这个结构里边到底有什么。打开内核调试器(以kd为例),输入命令显示KeServiceDescriptorTable,如下。

lkd> dd KeServiceDescriptorTable l4
8055ab80 804e3d20 00000000 0000011c 804d9f48

接下来,亦可根据基地址与服务总数来查看整个服务表的各项:

lkd> dd 804e3d20 l11c
804e3d20 80587691 f84317aa f84317b4 f84317be
804e3d30 f84317c8 f84317d2 f84317dc f84317e6
804e3d40 8057741c f84317fa f8431804 f843180e
804e3d50 f8431818 f8431822 f843182c f8431836
...

你获得的结果可能和我会有不同——我指的是那堆以十六进制f开头的地址项,因为我的SSDT被System Safety Monitor接管了,没留下几个原生的ntoskrnl.exe表项。

现在是写些代码的时候了。KeServiceDescriptorTable及SSDT各个表项的读取只能在ring0层完成,于是这里我使用了内核驱动并借助DeviceIoControl来完成。其中DeviceIoControl的分发代码实现如下面的代码所示,没有什么技术含量,所以不再解释。

switch ( IoControlCode )
{
case IOCTL_GETSSDT:
    {
__try
        {
            ProbeForWrite( OutputBuffer, sizeof( SSDT ), sizeof( ULONG ) );
            RtlCopyMemory( OutputBuffer, KeServiceDescriptorTable, sizeof( SSDT ) );
        }
__except ( EXCEPTION_EXECUTE_HANDLER )
        {
            IoStatus->Status = GetExceptionCode();
        }
    }
break;
case IOCTL_GETPROC:
    {
        ULONG uIndex = 0;
        PULONG pBase = NULL;
__try
        {
            ProbeForRead( InputBuffer, sizeof( ULONG ), sizeof( ULONG ) );
            ProbeForWrite( OutputBuffer, sizeof( ULONG ), sizeof( ULONG ) );
        }
__except( EXCEPTION_EXECUTE_HANDLER )
        {
            IoStatus->Status = GetExceptionCode();
break;
        }
        uIndex = *(PULONG)InputBuffer;
if ( KeServiceDescriptorTable->ulNumberOfServices <= uIndex )
        {
            IoStatus->Status = STATUS_INVALID_PARAMETER;
break;
        }
        pBase = KeServiceDescriptorTable->pvSSDTBase;
        *((PULONG)OutputBuffer) = *( pBase + uIndex );
    }
break;
// ...
}

补充一下,再。DDK的头文件中有一件很遗憾的事情,那就是其中并未声明KeServiceDescriptorTable,不过我们可以自己手动添加之:

extern PSSDT KeServiceDescriptorTable;

——当然,如果你对DDK开发实在不感兴趣的话,亦可以直接使用配套代码压缩包中的SSDTDump.sys,并使用DeviceIoControl发送IOCTL_GETSSDT和IOCTL_GETPROC控制码即可;或者,直接调用我为你准备好的两个函数:

BOOL GetSSDT( IN HANDLE hDriver, OUT PSSDT buf );
BOOL GetProc( IN HANDLE hDriver, IN ULONG ulIndex, OUT PULONG buf );

获取详细模块信息

虽然我们现在可以获取任意一个服务号所对应的函数地址了已经,但是你可能仍然不满意,认为只有获得了这个服务函数所在的模块才是王道。换句话说,对于一个干净的SSDT表来说,它里边的表项应该都是指向ntoskrnl.exe的;如果SSDT之中有若干个表项被改写(挂钩),那么我们应该知道是哪一个或哪一些模块替换了这些服务。

首先我们需要获得当前在ring0层加载了那些模块。如我在本文开头所说,为了尽可能地少涉及ring0层的东西,于是在这里我使用了ntdll.dll的NtQuerySystemInformation函数。关键代码如下:

typedef struct _SYSTEM_MODULE_INFORMATION {
    ULONG Reserved[2];
    PVOID Base;
    ULONG Size;
    ULONG Flags;
    USHORT Index;
    USHORT Unknown;
    USHORT LoadCount;
    USHORT ModuleNameOffset;
    CHAR ImageName[256];
} SYSTEM_MODULE_INFORMATION, *PSYSTEM_MODULE_INFORMATION;
typedef struct _tagSysModuleList {
    ULONG ulCount;
    SYSTEM_MODULE_INFORMATION smi[1];
} SYSMODULELIST, *PSYSMODULELIST;
s = NtQuerySystemInformation( SystemModuleInformation, pRet,
sizeof( SYSMODULELIST ), &nRetSize );
if ( STATUS_INFO_LENGTH_MISMATCH == s )
{
// 缓冲区太小,重新分配
delete pRet;
    pRet = (PSYSMODULELIST)new BYTE[nRetSize];
    s = NtQuerySystemInformation( SystemModuleInformation, pRet,
        nRetSize, &nRetSize );
}

需要说明的是,这个函数是利用内核的PsLoadedModuleList链表来枚举系统模块的,因此如果你遇到了能够隐藏驱动的Rootkit,那么这种方法是无法找到被隐藏的模块的。在这种情况下,枚举系统的“\Driver”目录对象可能可以更好解决这个问题,在此不再赘述了就。

接下来,是根据SSDT中的地址表项查找模块。有了SYSTEM_MODULE_INFORMATION结构中的模块基地址与模块大小,这个工作完成起来也很容易:

BOOL FindModuleByAddr( IN ULONG ulAddr, IN PSYSMODULELIST pList,
                      OUT LPSTR buf, IN DWORD dwSize )
{
for ( ULONG i = 0; i < pList->ulCount; ++i )
    {
        ULONG ulBase = (ULONG)pList->smi[i].Base;
        ULONG ulMax  = ulBase + pList->smi[i].Size;
if ( ulBase <= ulAddr && ulAddr < ulMax )
        {
// 对于路径信息,截取之
            PCSTR pszModule = strrchr( pList->smi[i].ImageName, '\\' );
if ( NULL != pszModule )
            {
                lstrcpynA( buf, pszModule + 1, dwSize );
            }
else
            {
                lstrcpynA( buf, pList->smi[i].ImageName, dwSize );
            }
return TRUE;
        }
    }
return FALSE;
}

详细枚举系统服务项

到现在为止,还遗留有一个问题,就是获得服务号对应的服务函数名。比如XP下0x7A对应着NtOpenProcess,但是到2000下,NtOpenProcess就改为0x6A了。

——有一个好消息一个坏消息,你先听哪个?

——什么坏消息?

——Windows并没有给我们开放这样现成的函数,所有的工作都需要我们自己来做。

——那好消息呢?

——牛粪有的是。

坏了,串词儿了。好消息是我们可以通过枚举ntdll.dll的导出函数来间接枚举SSDT所有表项所对应的函数,因为所有的内核服务函数对应于ntdll.dll的同名函数都是这样开头的:

mov eax, <ServiceIndex>

对应的机器码为:

B8 <ServiceIndex>

再说一遍:非常幸运,仅就我手头上的2000 sp4、XP、XP sp1、XP sp2、2003的ntdll.dll而言,无一例外。不过Mark Russinovich的《深入解析Windows操作系统》一书中指出,IA64的调用方式与此不同——由于手头上没有相应的文件,所以在这里不进行讨论了就。

接着说。我们可以把mov的一句用如下的一个结构来表示:

#pragma pack( push, 1 )
typedef struct _tagSSDTEntry {
    BYTE  byMov;   // 0xb8
    DWORD dwIndex;
} SSDTENTRY;
#pragma pack( pop )

那么,我们可以对ntdll.dll的所有导出函数进行枚举,并筛选出“Nt”开头者,以SSDTENTRY的结构取出其开头5个字节进行比对——这就是整个的枚举过程。相关的PE文件格式解析我不再解释,可参考注释。整个代码如下:

#define MOV        0xb8
void EnumSSDT( IN HANDLE hDriver, IN HMODULE hNtDll )
{
    DWORD dwOffset                  = (DWORD)hNtDll;
    PIMAGE_EXPORT_DIRECTORY pExpDir = NULL;
int nNameCnt                    = 0;
    LPDWORD pNameArray              = NULL;
int i                           = 0;
// 到PE头部
    dwOffset += ((PIMAGE_DOS_HEADER)hNtDll)->e_lfanew + sizeof( DWORD );
// 到第一个数据目录
    dwOffset += sizeof( IMAGE_FILE_HEADER ) + sizeof( IMAGE_OPTIONAL_HEADER )
        - IMAGE_NUMBEROF_DIRECTORY_ENTRIES * sizeof( IMAGE_DATA_DIRECTORY );
// 到导出表位置
    dwOffset = (DWORD)hNtDll
        + ((PIMAGE_DATA_DIRECTORY)dwOffset)->VirtualAddress;
    pExpDir = (PIMAGE_EXPORT_DIRECTORY)dwOffset;
    nNameCnt = pExpDir->NumberOfNames;
// 到函数名RVA数组
    pNameArray = (LPDWORD)( (DWORD)hNtDll + pExpDir->AddressOfNames );
// 初始化系统模块链表
    PSYSMODULELIST pList = CreateModuleList( hNtDll );
// 循环查找函数名
for ( i = 0; i < nNameCnt; ++i )
    {
        PCSTR pszName = (PCSTR)( pNameArray[i] + (DWORD)hNtDll );
if ( 'N' == pszName[0] && 't' == pszName[1] )
        {
// 找到了函数,则定位至查找表
            LPWORD pOrdNameArray = (LPWORD)( (DWORD)hNtDll + pExpDir->AddressOfNameOrdinals );
// 定位至总表
            LPDWORD pFuncArray   = (LPDWORD)( (DWORD)hNtDll + pExpDir->AddressOfFunctions );
            LPCVOID pFunc        = (LPCVOID)( (DWORD)hNtDll + pFuncArray[pOrdNameArray[i]] );
// 解析函数,获取服务名
            SSDTENTRY entry;
            CopyMemory( &entry, pFunc, sizeof( SSDTENTRY ) );
if ( MOV == entry.byMov )
            {
                ULONG ulAddr = 0;
                GetProc( hDriver, entry.dwIndex, &ulAddr );
                CHAR strModule[MAX_PATH] = "[Unknown Module]";
                FindModuleByAddr( ulAddr, pList, strModule, MAX_PATH );
                printf( "0x%04X\t%s\t0x%08X\t%s\r\n", entry.dwIndex,
                    strModule, ulAddr, pszName );
            }
        }
    }
    DestroyModuleList( pList );
}

下图是示例程序SSDTDump在XP sp2上的部分运行截图,显示了SSDT的基地址、服务个数,以及各个表项所对应的服务号、所在模块、地址和服务名。

结语

ring3与ring0,城里与城外之间为一道叹息之墙所间隔,SSDT则是越过此墙的一道必经之门。因此,很多杀毒软件也势必会围绕着它大做文章。无论是System Safety Monitor的系统监控,还是卡巴斯基的主动防御,都是挂钩了SSDT。这样,病毒尚在ring3内发作之时,便被扼杀于摇篮之内。

内核最高权限,本就是兵家必争之地,魔高一尺道高一丈的争夺于此亦已变成颇为稀松平常之事。可以说和这些争夺比起来,SSDT的相关技术简直不值一提。但最初发作的病毒体总是从ring3开始的——换句话说,任你未来会成长为何等的武林高手,我都可以在你学走路的时候杀掉你——知晓了SSDT的这点优势,所有的病毒咂吧咂吧也就都没味儿了。所以说么,杀毒莫如防毒。

——就此打住罢,貌似扯远大发了。

终于,喧闹一时的“熊猫烧香”案尘埃落定,李俊及其同伙伏法。等待他们的虽是牢狱,但更有重新改过的机会。对于熊猫,这是个结尾;但对于很多人,这只是个开始。一个简单的蠕虫,竟然折射出了万万千千的嘴脸。我并无意将这个话题扯大,但是我不得不说。关于道德,关于盲目的畸形崇拜。

作为一个有五年Windows编程经验的职业程序员,我认为我有资格写下这些文字。如果来访的你以名气为尺度来衡量李马和李俊,那么你可以暂缓写下你的评论,先看看其他来访者的评论再说不迟。另外,我不想分析“熊猫烧香”这个病毒——直说就是不屑于分析。如果你抱着“你牛逼你也写一个啊”的态度来看李马,就先搜索一下Japussy这个开源的病毒源码,我没必要在这个问题上和你多费唇舌。

谈正题,道德与盲目的畸形崇拜。写病毒的人沦丧了自己的道德,盲目崇拜的人则助长了更多的人去沦丧自己的道德。随便写一个蠕虫,竟被媒体奉为“天才”,甚至于蒙蔽了民众的双眼,“吸纳李俊入安全部门”的呼声频起。我并不想借此讨论中国人治和法治的矛盾——我是赞成法外施恩的,但是就李俊来说,他还够不上陈盈豪的那个资格。我记得我说过,现在写Windows病毒的门槛很低,随随便便一个蹩脚的程序员就可以搞定。但这个事实对于绝大多数的网民来说仍然是个盲点,所以他们亦仍然沉浸在对所谓“天才”的盲目的、畸形的崇拜中。于是,“天才”这个头衔就像潘多拉魔盒一样,频频吸引着一些不知天高地厚的蹩脚程序员们。他们打开这个魔盒,释放了自己内心的恶灵,为害人间。

今年8月份的时候,我顺着50bang的访问来源来到了一个14岁少年的百度空间,这是一个用脑残体书写他名字的孩子,但他更是红极一时的“小浩”病毒作者。是的,接下来我看到了他转载的《城里城外看SSDT》,说明他对技术的热爱,当然也说明,他也许也会看到我这篇blog。——历史总是惊人的相似,他终于也被“天才”这个邪恶的光环俯身了。然而他不知道,不懂事写下并开源的病毒被某些别有用心的人利用了——这些人并不是编译病毒变种搞破坏的人,而是一些借刀杀人的商业实体的决策者。虽然14岁的你已经可以用MFC写病毒了,但你并不能预料成年人所使用的阴毒手段。由此看来,我们的这个互联网真的需要少一些浮躁,把“天才”这个词归还给和它真正相配的Bill Gates们。

就说这么多了。技术这东西是为产业服务的,换句话说,是有利益驱动的。但是,君子爱财取之有道,别拿亏心钱才是王道。

不知道诸位看官是否有过这样的经历:在不经意之间发现一个DLL文件,它里边有不少有趣的导出函数——但是由于你不知道如何调用这些函数,所以只能大发感慨而又无能为力焉。固然有些知名的DLL可以直接通过搜索引擎来找到它的使用方式(比如本文中的例子ipsearcher.dll),不过我们诚然不能希望自己总能交到这样的好运。所以在本文中,李马希望通过自己文理不甚通达的讲解能够给大家以授人以渔的效果。

先决条件

阅读本文,你需要具备以下先决条件:

  • 初步了解汇编语言,虽然你并不一定需要去读懂DLL中导出函数的汇编代码,但是你至少应该了解诸如push、mov这些常用的汇编指令。
  • 一个能够查看DLL中导出函数的工具,Visual Studio中自带的Dependency Walker就足够胜任了,当然你也可以选择eXeScope。
  • 一个调试器。理论上讲VC也可以完成调试的工作,但它毕竟是更加针对于源代码一级调试的工具,所以你最好选择一个专用的汇编调试器。在本文中我用的是OllyDbg——我不会介绍有关这个调试工具的任何东西,而只是简要介绍我的调试过程。

准备好了吗?那么我们做一个热身运动吧先。

热身——函数调用约定

这里要详细介绍的是有关函数调用约定的内容,如果你已经了解了这方面的内容,可以跳过本节。

你可能在学习Windows程序设计的时候早已接触过“函数调用约定”这个词汇了,那个时候你所了解的内容可能是一个笼统的概念,内容大抵是说函数调用约定就是指的函数参数进栈顺序以及堆栈修正方式。譬如cdecl调用约定是函数参数自右而左进栈,由调用者修复堆栈;stdcall调用约定亦是函数参数自右而左进栈,但是由被调用者修复堆栈……噢不,这太晦涩了——在源代码上我们是无法看到这些东西的!

那么我们别无选择,只有深入到汇编一层了。考虑以下C++代码:

#include <stdio.h>
int __cdecl max1( int a, int b )
{
return a > b ? a : b;
}
int __stdcall max2( int a, int b )
{
return a > b ? a : b;
}
int main()
{
    printf( "max( 1, 2 ) of cdecl version: %d\n", max1( 1, 2 ) );
    printf( "max( 1, 2 ) of stdcall version: %d\n", max2( 1, 2 ) );
return 0;
}

对应的汇编代码为:

; int __cdecl max1( int a, int b )
00401000 MOV EAX,DWORD PTR SS:[ESP+4]
00401004 MOV ECX,DWORD PTR SS:[ESP+8]
00401008 CMP EAX,ECX
0040100A JG SHORT CppTest.0040100E
0040100C MOV EAX,ECX
0040100E RETN
; int __stdcall max2( int a, int b )
00401010 MOV EAX,DWORD PTR SS:[ESP+4]
00401014 MOV ECX,DWORD PTR SS:[ESP+8]
00401018 CMP EAX,ECX
0040101A JG SHORT CppTest.0040101E
0040101C MOV EAX,ECX
0040101E RETN 8 ; 被调用者的堆栈修正
; max1( 1, 2 )
00401030 PUSH 2
00401032 PUSH 1
00401034 CALL CppTest.00401000
00401039 ADD ESP,8 ; 调用者的堆栈修正
; max2( 1, 2 )
0040104A PUSH 2
0040104C PUSH 1
0040104E CALL CppTest.00401010

好了,我来简要介绍一下。函数参数传入函数体是借由堆栈段完成的,也就是将各个参数依某种次序推入SS中——在cdecl与stdcall约定中,这个次序都是自右而左的。另外,由于将参数推入了堆栈致使堆栈指针ESP发生了变化,所以要在函数结束的时候重新修正ESP。从上边的汇编代码中你也可以很清楚地看到,cdecl约定是在调用max1之后修正的ESP,而stdcall约定则是在max2返回时借由RETN 8完成了这个修正工作。

另外,从上边的汇编代码中还可以看到,函数的返回值是由EAX带回的。

庖丁解牛

在了解了以上的知识后,我们就可以使用调试器来调试那个未知的DLL了。可以说,这整个的调试过程充满了惊险和刺激,而且我们还需要一定的技巧——如果你像我一样不喜欢阅读汇编代码的话。

在本文中,我所选择的调试示例是FTerm中附带的ipsearcher.dll,它提供了对纯真IP数据库的查询接口。下图是用Dependency Walker对其分析的结果:

你可以看到,这里边有两个导出函数:LookupAddress和_GetAddress,那么我们可以按照返回值、调用约定、函数名、参数列表的顺序将它们声明如下:

? ? LookupAddress( ? );
? ? _GetAddress( ? );

是的,有太多的未知,下面李马将要逐一地破解这些问号。

调试器不可能孤立地对DLL进行调试,我们所需要的应该是一个合适的EXE,这样有助于我们的探究工作。在这里我选择的EXE是我编写的ipsearcher.exe,当然这可能会让你认为我这篇文章的组织顺序有问题——毕竟是我已经知道了这两个导出函数之后(编写了ipsearcher.exe)还要假装成不知道的样子来对ipsearcher.dll来进行探究,所以我决定在下文中不对ipsearcher.exe的代码进行任何关注,而是直接进入到ipsearcher.dll的领空。

打开调试器,载入ipsearcher.exe。当ipsearcher.dll被装载后,会引发一个访问异常,可以忽略这个异常继续调试。根据Dependency Walker的分析结果,在ipsearcher.dll的0x00001BB0和0x00001C40处各下一个断点。现在在“IP地址”中输入一个IP地址(这里以寒泉BBS的IP为例),点击“查询”,会发现指令跳入0x00001C40中(也就是_GetAddress),它的代码如下:

10001C40 MOV EAX,DWORD PTR SS:[ESP+4] ; 一个参数
10001C44 PUSH ipsear_1.10009BE8
10001C49 PUSH EAX
10001C4A CALL ipsear_1.LookupAddress ; 两个参数
10001C4F ADD ESP,8 ; LookupAddress是cdecl调用约定
10001C52 MOV EAX,ipsear_1.10009BE8
10001C57 RETN ; _GetAddress这厮也是cdecl调用约定

很短的几行代码,不过它已经可以提供这些信息了:

  • 从SS的使用来看,_GetAddress只带有一个参数。
  • _GetAddress中调用了LookupAddress,后者带有两个参数。
  • 调用LookupAddress之后进行了堆栈修正,所以LookupAddress是cdecl调用约定。
  • _GetAddress返回时并未进行堆栈修正,所以_GetAddress也是cdecl调用约定。

于是,我们可以替换一下刚才的问号了:

? CDECL LookupAddress( ?, ? );
? CDECL _GetAddress( ? );

下面可以进行单步调试了,当代码步至10001C44时,你会发现寄存器窗口发生了如下的变化:

“202.207.177.9”终于出现了,这样一来我们可以继续对问号进行替换了:

? CDECL LookupAddress( PCSTR, ? );
? CDECL _GetAddress( PCSTR );

现在继续对代码进行跟踪,是进入LookupAddress的时候了。我们可以从先前_GetAddress的代码中可以发现,这两个导出函数一直在围绕10009BE8这个地址做文章,那么我们就要在单步调试LookupAddress的同时关注这个地址的数据改变。几步跟踪之后,你会发现10009BE8开头的8字节(两个DWORD)数据发生了改变,变成了10009AB4和10009B1C。那么我们再转向这两个地址,会发现:

这样一来就很清楚了,10009BE8是一个字符串指针的数组,它有两个元素。也就是说,我们的函数声明可以换成这样:

? CDECL LookupAddress( PCSTR, PSTR* );
PSTR* CDECL _GetAddress( PCSTR );

接下来需要确定的就是LookupAddress的返回值了。纵观LookupAddress的返回代码,你会发现这样的片断:

; 片断1
10001C0B XOR EAX,EAX
10001C0D POP ESI
10001C0E RETN
; 片断2
10001C2B MOV EAX,1
10001C30 POP ESI
10001C31 RETN

也就是说,这个函数有两个返回值:0或1。那么最后的真相终于大白于天下——

BOOL CDECL LookupAddress( PCSTR, PSTR* );
PSTR* CDECL _GetAddress( PCSTR );

GetProcAddress?

到此为止,这两个函数的声明终于让我们找出来了。也许你会觉得这就够了——接下来就是用typedef定义函数指针,然后使用LoadLibrary、GetProcAddress调用这些函数的事情了。

如果你真的这么认为的话,那我认为我有必要向你介绍这另外的一种方式。

首先请你建立一个名为ipsearcher.def的文件,然后在其中写入如下内容:

LIBRARY "ipsearcher"
EXPORTS
LookupAddress @1
_GetAddress   @2

将文件保存后,进入到命令行模式下,输入以下命令(前提是你拥有Visual Studio的附带工具lib.exe并有正确的路径指向。以Visual Studio 6.0为例,这个工具通常位于Microsoft Visual Studio\VC98\Bin下):

lib /def:ipsearcher.def

执行的结果有一个警告,不必理会。这时候我们会发现,lib为我们生成了一个ipsearcher.lib。

然后,我们继续编写ipsearcher.h文件,如下:

#ifndef IPSEARCHER_H
#define IPSEARCHER_H
#include <windows.h>
#pragma comment( lib, "ipsearcher.lib" )
extern "C"
{
BOOL CDECL LookupAddress( PCSTR, PSTR* );
PSTR* CDECL _GetAddress( PCSTR );
};
#endif // IPSEARCHER_H

大功告成!这样我们就为这个光秃秃的ipsearcher.dll做了一份SDK开发包,而不必再使用动态加载的方法了。

总结一下再

其实,探究一个DLL并非像我这里所讲述的这么简单。这项工作很可能需要阅读大量的汇编代码,了解DLL函数体的流程才能使真相大白于天下。另外,还不能排除有的DLL被加密、加壳、反跟踪……也就是说对于ipsearcher.dll,那简直就是我捡了个便宜来借花献佛了。

点这里下载ipsearcher SDK

DLL的远程注入技术是目前Win32病毒广泛使用的一种技术。使用这种技术的病毒体通常位于一个DLL中,在系统启动的时候,一个EXE程序会将这个DLL加载至某些系统进程(如Explorer.exe)中运行。这样一来,普通的进程管理器就很难发现这种病毒了,而且即使发现了也很难清除,因为只要病毒寄生的进程不终止运行,那么这个DLL就不会在内存中卸载,用户也就无法在资源管理器中删除这个DLL文件,真可谓一箭双雕哉。

记得2003年QQ尾巴病毒肆虐的时候,就已经有些尾巴病毒的变种在使用这种技术了。到了2004年初,我曾经尝试着仿真了一个QQ尾巴病毒,但独是跳过了DLL的远程加载技术。直到最近在学校论坛上看到了几位朋友在探讨这一技术,便忍不住将这一尘封已久的技术从我的记忆中拣了出来,以满足广大的技术爱好者们。

必备知识

在阅读本文之前,你需要了解以下几个API函数:

OpenProcess - 用于打开要寄生的目标进程。
VirtualAllocEx/VirtualFreeEx - 用于在目标进程中分配/释放内存空间。
WriteProcessMemory - 用于在目标进程中写入要加载的DLL名称。
CreateRemoteThread - 远程加载DLL的核心内容,用于控制目标进程调用API函数。
LoadLibrary - 目标进程通过调用此函数来加载病毒DLL。

在此我只给出了简要的函数说明,关于函数的详细功能和介绍请参阅MSDN。

示例程序

我将在以下的篇幅中用一个简单的示例Virus.exe来实现这一技术。这个示例的界面如下图:

首先运行Target.exe,这个文件是一个用Win32 Application向导生成的“Hello, World”程序,用来作为寄生的目标进程。

然后在界面的编辑控件中输入进程的名称“Target.exe”,单击“注入DLL”按钮,这时候Virus.exe就会将当前目录下的DLL.dll注入至Target.exe进程中。

在注入DLL.dll之后,你也可以单击“卸载DLL”来将已经注入的DLL卸载。

点这里下载示例程序

模拟的病毒体DLL.dll

这是一个简单的Win32 DLL程序,它仅由一个入口函数DllMain组成:

BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved )
{
switch ( fdwReason )
    {
case DLL_PROCESS_ATTACH:
        {
            MessageBox( NULL, _T("DLL已进入目标进程。"), _T("信息"), MB_ICONINFORMATION );
        }
break;
case DLL_PROCESS_DETACH:
        {
            MessageBox( NULL, _T("DLL已从目标进程卸载。"), _T("信息"), MB_ICONINFORMATION );
        }
break;
    }
return TRUE;
}

如你所见,这里我在DLL被加载和卸载的时候调用了MessageBox,这是用来显示我的远程注入/卸载工作是否成功完成。而对于一个真正的病毒体来说,它往往就是处理DLL_PROCESS_ATTACH事件,在其中加入了启动病毒代码的部分:

case DLL_PROCESS_ATTACH:
    {
        StartVirus();
    }
break;

注入!

现在要开始我们的注入工作了。首先,我们需要找到目标进程:

DWORD FindTarget( LPCTSTR lpszProcess )
{
    DWORD dwRet = 0;
    HANDLE hSnapshot = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS, 0 );
    PROCESSENTRY32 pe32;
    pe32.dwSize = sizeof( PROCESSENTRY32 );
    Process32First( hSnapshot, &pe32 );
do
    {
if ( lstrcmpi( pe32.szExeFile, lpszProcess ) == 0 )
        {
            dwRet = pe32.th32ProcessID;
break;
        }
    } while ( Process32Next( hSnapshot, &pe32 ) );
    CloseHandle( hSnapshot );
return dwRet;
}

这里我使用了Tool Help函数库,当然如果你是NT系统的话,也可以选择PSAPI函数库。这段代码的目的就是通过给定的进程名称来在当前系统中查找相应的进程,并返回该进程的ID。得到进程ID后,就可以调用OpenProcess来打开目标进程了:

// 打开目标进程
HANDLE hProcess = OpenProcess( PROCESS_CREATE_THREAD | PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, dwProcessID );

现在有必要说一下OpenProcess第一个参数所指定的三种权限。在Win32系统下,每个进程都拥有自己的4G虚拟地址空间,各个进程之间都相互独立。如果一个进程需要完成跨进程的工作的话,那么它必须拥有目标进程的相应操作权限。在这里,PROCESS_CREATE_THREAD表示我可以通过返回的进程句柄在该进程中创建新的线程,也就是调用CreateRemoteThread的权限;同理,PROCESS_VM_OPERATION则表示在该进程中分配/释放内存的权限,也就是调用VirtualAllocEx/VirtualFreeEx的权限;PROCESS_VM_WRITE表示可以向该进程的地址空间写入数据,也就是调用WriteProcessMemory的权限。

至此目标进程已经打开,那么我们该如何来将DLL注入其中呢?在这之前,我请你看一行代码,是如何在本进程内显式加载DLL的:

HMODULE hDll = LoadLibrary( "DLL.dll" );

那么,如果能控制目标进程调用LoadLibrary,不就可以完成DLL的远程注入了么?的确是这样,我们可以通过CreateRemoteThread将LoadLibrary作为目标进程的一个线程来启动,这样就可以完成“控制目标进程调用LoadLibrary”的工作了。到这里,也许你会想当然地写下类似这样的代码:

DWORD dwID;
LPVOID pFunc = LoadLibraryA;
HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, (LPVOID)"DLL.dll", 0, &dwID );

不过结果肯定会让你大失所望——注入DLL失败!

嗯嗯,那么现在让我们来分析一下失败的原因吧。我是前说过,在Win32系统下,每个进程都拥有自己的4G虚拟地址空间,各个进程之间都是相互独立的。在这里,我们当作参数传入的字符串"DLL.dll"其实是一个数值,它表示这个字符串位于Virus.exe地址空间之中的地址,而这个地址在传给Target.exe之后,它指向的东西就失去了有效性。举个例子来说,譬如A、B两栋大楼,我住在A楼的401;那么B楼的401住的是谁我当然不能确定——也就是401这个门牌号在B楼失去了有效性,而且如果我想要入住B楼的话,我就必须请B楼的楼长为我在B楼中安排新的住处(当然这个新的住处是否401也就不一定了)。

由此看来,我就需要做这么一系列略显繁杂的手续——首先在Target.exe目标进程中分配一段内存空间,然后向这段空间写入我要加载的DLL名称,最后再调用CreateRemoteThread。这段代码就成了这样:

// 向目标进程地址空间写入DLL名称
DWORD dwSize, dwWritten;
dwSize = lstrlenA( lpszDll ) + 1;
LPVOID lpBuf = VirtualAllocEx( hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE );
if ( NULL == lpBuf )
{
    CloseHandle( hProcess );
// 失败处理
}
if ( WriteProcessMemory( hProcess, lpBuf, (LPVOID)lpszDll, dwSize, &dwWritten ) )
{
// 要写入字节数与实际写入字节数不相等,仍属失败
if ( dwWritten != dwSize )
    {
        VirtualFreeEx( hProcess, lpBuf, dwSize, MEM_DECOMMIT );
        CloseHandle( hProcess );
// 失败处理
    }
}
else
{
    CloseHandle( hProcess );
// 失败处理
}
// 使目标进程调用LoadLibrary,加载DLL
DWORD dwID;
LPVOID pFunc = LoadLibraryA;
HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, lpBuf, 0, &dwID );

需要说的有两点,一是由于我要在目标进程中为ANSI字符串来分配内存空间,所以这里凡是和目标进程相关的部分,都明确使用了后缀为“A”的API函数——当然,如果要使用Unicode字符串的话,可以换作后缀是“W”的API;第二,在这里LoadLibrary的指针我是取的本进程的LoadLibraryA的地址,这是因为LoadLibraryA/LoadLibraryW位于kernel32.dll之中,而Win32下每个应用程序都会把kernel32.dll加载到进程地址空间中一个固定的地址,所以这里的函数地址在Target.exe中也是有效的。

在调用LoadLibrary完毕之后,我们就可以做收尾工作了:

// 等待LoadLibrary加载完毕
WaitForSingleObject( hThread, INFINITE );
// 释放目标进程中申请的空间
VirtualFreeEx( hProcess, lpBuf, dwSize, MEM_DECOMMIT );
CloseHandle( hThread );
CloseHandle( hProcess );

在此解释一下WaitForSingleObject一句。由于我们是通过CreateRemoteThread在目标进程中另外开辟了一个LoadLibrary的线程,所以我们必须等待这个线程运行完毕才能够释放那段先前申请的内存。

好了,现在你可以尝试着整理这些代码并编译运行。运行Target.exe,然后开启一个有模块查看功能的进程查看工具(在这里我使用我的July)来查看Target.exe的模块,你会发现在注入DLL之前,Target.exe中并没有DLL.dll的存在:

在调用了注入代码之后,DLL.dll就位于Target.exe的模块列表之中了:

矛盾相生

记得2004年初我将QQ尾巴病毒成功仿真后,有很多网友询问我如何才能杀毒,不过我都没有回答——因为当时我研究的重点并非病毒的寄生特性。这一寄生特性直到今天可以说我才仿真完毕,那么,我就将解毒的方法也一并公开吧。

和DLL的注入过程类似,只不过在这里使用了两个API:GetModuleHandle和FreeLibrary。出于篇幅考虑,我略去了与注入部分相似或相同的代码:

// 使目标进程调用GetModuleHandle,获得DLL在目标进程中的句柄
DWORD dwHandle, dwID;
LPVOID pFunc = GetModuleHandleA;
HANDLE hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, lpBuf, 0, &dwID );
// 等待GetModuleHandle运行完毕
WaitForSingleObject( hThread, INFINITE );
// 获得GetModuleHandle的返回值
GetExitCodeThread( hThread, &dwHandle );
// 释放目标进程中申请的空间
VirtualFreeEx( hProcess, lpBuf, dwSize, MEM_DECOMMIT );
CloseHandle( hThread );
// 使目标进程调用FreeLibrary,卸载DLL
pFunc = FreeLibrary;
hThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pFunc, (LPVOID)dwHandle, 0, &dwID );
// 等待FreeLibrary卸载完毕
WaitForSingleObject( hThread, INFINITE );
CloseHandle( hThread );
CloseHandle( hProcess );

用这个方法可以卸载一个进程中的DLL模块,当然包括那些非病毒体的DLL。所以,这段代码还是谨慎使用为好。

在完成卸载之后,如果没有别的程序加载这个DLL,你就可以将它删除了。

到此为止,整个的技术细节我就讲完了。

Windows SDK开发包中并未提供所有的API函数,在本文中我将讨论如何调用这种未公开的API函数。

事实上所有未公开的API函数都和其它的API函数一样包含在系统的动态链接库中,调用这些函数的方法是取得它们的指针,然后通过指针来进行操作。而取得函数地址,是通过GetProcAddress这个API函数实现的:

FARPROC WINAPI GetProcAddress(
    HMODULE hModule, // DLL模块句柄
    LPCSTR lpProcName // 函数名称
);

当然,在取得地址之前,需要用LoadLibrary获得模块的句柄。还有,为了书写方便,最好用typedef将函数指针定义为一种类型。

下面我将通过两个例子来演示如何调用这些未公开的API函数。

一、有名称的函数

这种函数在DLL中拥有自己的函数名称,但是在SDK包中并没有提供声明,其中最有代表性的是RegisterServiceProcess函数:

DWORD WINAPI RegisterServiceProcess(
    DWORD dwProcessId, // 进程ID
    DWORD dwType // 注册种类,1表示注册
);

这个函数的功能是在Win98下将进程注册为系统服务进程,很多木马程序的隐藏就是用这个函数实现的。调用它的示例代码如下:

typedef DWORD (WINAPI * REGISTER)( DWORD, DWORD );
HMODULE hModule;
REGISTER RegisterServiceProcess;
hModule = LoadLibrary( "kernel32.dll" );
if ( hModule != NULL )
{
    RegisterServiceProcess = (REGISTER)GetProcAddress( hModule, "RegisterServiceProcess" );
    RegisterServiceProcess( GetCurrentProcessId(), 1 );
    FreeLibrary( hModule );
}

二、无名称的函数

有的函数在DLL中并没有函数名称,这又如何调用呢?事实上所有的API函数无论有无名称,都会有一个ID,来在DLL中标识自己。比如函数RunFileDlg,它的ID是61,功能是显示系统“运行”对话框。下图所列的是我开发的进程管理软件July中所调用的“运行”对话框:

事实上调用这种函数的方法和前一种非常相似,唯一不同的只是把GetProcAddress的lpProcName参数使用MAKEINTRESOURCE宏将函数的ID转换一下即可。示例代码如下:

typedef void (WINAPI* RUN)( HWND, HICON, LPCSTR, LPCSTR, LPCSTR, UINT );
HMODULE hShell32;
RUN RunFileDlg;
hShell32 = LoadLibrary( "shell32.dll" );
RunFileDlg = (RUN)GetProcAddress( hShell32, MAKEINTRESOURCE( 61 ) );
RunFileDlg( hParent, hIcon, NULL, NULL, NULL, 0 );
FreeLibrary( hShell32 );

未公开的API函数的调用方法就介绍到这里了。事实上还有很多这样的函数,关于这些函数的介绍及使用方法,请下载我的“未公开的Windows API函数”文档

首先请大家看这么一个简单的小程序:

#include <stdio.h>
void main()
{
int i, b[10];
for ( i = 0; i <= 10; i++ )
    {
        b[i] = 0;
    }
}

请问这个程序是否有错?A.正常 B.越界 C.死循环

正确答案是C,相信选A或选B的朋友一定会很纳闷。事实上我也是如此,单单从程序的表面上看,按定义这应该是个越界,因为当循环进行到i == 10的时候,程序将试图将b[10]赋值为0,而C语言中,b[10]的声明就是指定b[0]~b[9]可用。

然而程序的结果你看到了,这是个死循环无疑。

也好,那么让汇编来告诉你——以及我——这一切的真相吧,在这之前请你把i和b[10]的定义改成:

int i = 0, b[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

然后,将这个程序反汇编,可以得到:

让我来解释一下这段汇编代码的含义吧。在系统的实现中,i和数组b[10]是分配在栈上的变量,在内存中的分布如下图:

现在你看到了,i所占据的正是b[10]的位置,而b[10] = 0;这一句会被这样运行:

*(&b[0] + 10) = 0;

所以这一句的结果,就是把0赋值给i。这样一来在第11次循环的时候,i将会被重新置为0,那么循环结束的条件也就永远不会满足了,循环也就是个死循环了。

事实上单单讨论C语言的内部实现并没有什么意义,而且这样书写的循环在程序设计中也绝对不能够出现。所以我所想要讨论的,就是如何让汇编帮助我们解决表面上无法看清楚的东西,仅此而已。

题目:

设有如下C++类

class A
{
int value;
public:
    A( int n = 0 ) : value( n ) {}
int GetValue()
    {
return value;
    }
};

请使用某种方式来在类的外部改变私有成员A::value的值。

程序员的可能做法:

class A
{
int value;
public:
    A( int n = 0 ) : value( n ) {}
int GetValue()
    {
return value;
    }
void SetValue( int n )
    {
        value = n;
    }
};
void f()
{
    A a;
    a.SetValue( 5 );
}

黑客的可能做法:

void f()
{
    A a;
    *( (int *)&a ) = 5;
}

结论:

程序员习惯于遵循既有的限制来增加既有的东西。

黑客习惯于利用既有的东西来打破既有的限制。

第一阶段

此阶段主要是能熟练地使用某种语言。这就相当于练武中的套路和架式这些表面的东西。

第二阶段

此阶段能精通基于某种平台的接口(例如我们现在常用的Win 32的API函数)以及所对应语言的自身的库函数。到达这个阶段后,也就相当于可以进行真

实散打对练了,可以真正地在实践中做些应用。

第三阶段

此阶段能深入地了解某个平台系统的底层,已经具有了初级的内功的能力,也就是”手中有剑,心中无剑”。

第四阶级

此阶段能直接在平台上进行比较深层次的开发。基本上,能达到这个层次就可以说是进入了高层次。这时进入了高级内功的修炼。比如能进行DDK或操作系统的

内核的修改。

这时已经不再有语言的束缚,语言只是一种工具,即使要用自己不会的语言进行开发,也只是简单地熟悉一下,就手到擒来,完全不像是第一阶段的时候学习语言

的那种情况。一般来说,从第三阶段过渡到第四阶段是比较困难的。为什么会难呢?这就是因为很多人的思想变不过来。

第五阶级

此阶段就已经不再局限于简单的技术上的问题了,而是能从全局上把握和设计一个比较大的系统体系结构,从内核到外层界面。可以说是”手中无剑,心中有

剑”。到了这个阶段以后,能对市面上的任何软件进行剖析,并能按自己的要求进行设计,就算是MS Word这样的大型软件,只要有充足的时间,也一定会

设计出来。

第六阶级

此阶段也是最高的境界,达到”无招胜有招”。这时候,任何问题就纯粹变成了一个思路的问题,不是用什么代码就能表示的。也就是”手中无剑,心中也无

剑”。

此时,对于练功的人来说,他已不用再去学什么少林拳,只是在旁看一下少林拳的对战,就能把此拳拿来就用。这就是真正的大师级的人物。这时,Win 32

或Linux在你眼里是没有什么差别的。

每一个阶段再向上发展时都要按一定的方法。第一、第二个阶段通过自学就可以完成,只要多用心去研究,耐心地去学习。

要想从第二个阶段过渡到第三个阶段,就要有一个好的学习环境。例如有一个高手带领或公司里有一个好的练手环境。经过二、三年的积累就能达到第三个阶段。

但是,有些人到达第三个阶段后,常常就很难有境界上的突破了。他们这时会产生一种观念,认为软件无非如此,认为自己已无所不能。其实,这时如果遇到大的

或难些的软件,他们往往还是无从下手。

现在我们国家大部分程序员都是在第二、三级之间。他们大多都是通过自学成才的,不过这样的程序员一般在软件公司也能独当一面,完成一些软件的模块。

但是,也还有一大堆处在第一阶段的程序员,他们一般就能玩玩VB,做程序时,去找一堆控件集成一个软件。

朋友帖了如下一段代码:

  #pragma pack(4)

  class TestB

  {

  public:

    int aa;

    char a;

    short b;

    char c;

  };

  int nSize = sizeof(TestB);

  这里nSize结果为12,在预料之中。

  现在去掉第一个成员变量为如下代码:

  #pragma pack(4)

  class TestC

  {

  public:

    char a;

    short b;

    char c;

  };

  int nSize = sizeof(TestC);

  按照正常的填充方式nSize的结果应该是8,为什么结果显示nSize为6呢?

事实上,很多人对#pragma pack的理解是错误的。

#pragma pack规定的对齐长度,实际使用的规则是:

结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。

也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。

而结构整体的对齐,则按照结构体中最大的数据成员 和 #pragma pack指定值 之间,较小的那个进行。


具体解释

#pragma pack(4)

  class TestB

  {

  public:

    int aa; //第一个成员,放在[0,3]偏移的位置,

    char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。

    short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。

    char c; //第四个,自身长为1,放在[8]的位置。

  };

这个类实际占据的内存空间是9字节

类之间的对齐,是按照类内部最大的成员的长度,和#pragma pack规定的值之中较小的一个对齐的。

所以这个例子中,类之间对齐的长度是min(sizeof(int),4),也就是4。

9按照4字节圆整的结果是12,所以sizeof(TestB)是12。


如果

#pragma pack(2)

    class TestB

  {

  public:

    int aa; //第一个成员,放在[0,3]偏移的位置,

    char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。

    short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。

    char c; //第四个,自身长为1,放在[8]的位置。

  };

//可以看出,上面的位置完全没有变化,只是类之间改为按2字节对齐,9按2圆整的结果是10。

//所以 sizeof(TestB)是10。

最后看原贴:

现在去掉第一个成员变量为如下代码:

  #pragma pack(4)

  class TestC

  {

  public:

    char a;//第一个成员,放在[0]偏移的位置,

    short b;//第二个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。

    char c;//第三个,自身长为1,放在[4]的位置。

  };

//整个类的大小是5字节,按照min(sizeof(short),4)字节对齐,也就是2字节对齐,结果是6

//所以sizeof(TestC)是6。


感谢 Michael 提出疑问,在此补充:


当数据定义中出现__declspec( align() )时,指定类型的对齐长度还要用自身长度和这里指定的数值比较,然后取其中较大的。最终类/结构的对齐长度也需要和这个数值比较,然后取其中较大的。


可以这样理解, __declspec( align() ) 和 #pragma pack是一对兄弟,前者规定了对齐的最小值,后者规定了对齐的最大值,两者同时出现时,前者拥有更高的优先级。

__declspec( align() )的一个特点是,它仅仅规定了数据对齐的位置,而没有规定数据实际占用的内存长度,当指定的数据被放置在确定的位置之后,其后的数据填充仍然是按照#pragma pack规定的方式填充的,这时候类/结构的实际大小和内存格局的规则是这样的:

在__declspec( align() )之前,数据按照#pragma pack规定的方式填充,如前所述。当遇到__declspec( align() )的时候,首先寻找距离当前偏移向后最近的对齐点(满足对齐长度为max(数据自身长度,指定值) ),然后把被指定的数据类型从这个点开始填充,其后的数据类型从它的后面开始,仍然按照#pragma pack填充,直到遇到下一个__declspec( align() )。

当所有数据填充完毕,把结构的整体对齐数值和__declspec( align() )规定的值做比较,取其中较大的作为整个结构的对齐长度。

特别的,当__declspec( align() )指定的数值比对应类型长度小的时候,这个指定不起作用。

经常在blog上写些东西,可是有些心底的东西还是无法表达,因为面对的都是认识的人,害怕有些东西被看穿,毕竟,人还是要有点秘密的

      近来心情非常的不好,茫然、迷茫、,对于生活缺乏所有的信心,不管做什么事,我都觉得自己是一个局外人,什么都不会,我经常会怀疑自己的能力,这么多年,是怎么考上大学的,在大学里都学了些什么,为什么接触工作之后,我什么都不会,为什么面对同样的问题,我总是最后一个才理解,有些到最后也不理解,我经常脑袋里一片空空,不知道干什么,静下心来看书的时间一般不会超过十分钟,当碰到问题的时候又不知道该如何去解决。我真的快要崩溃了。

       尝试着碰到问题的时候去问别人,经常会得到模棱两可的答案,也有时候是别人说过答案我却不记得,或者我从来就没有理解过,我真的有些恐慌了,感觉自己完全不能适应这个社会,我不知道我的明天会是什么样子,经常有过一天算一天的感觉,知道别人就是别人,我就是我之后,我尝试着去学习,自己刻苦钻研,可是我又发现,自己真的是笨阿,一个问题就能把握难上半天,然后又是一阵迷茫,生活啊,该怎样继续下去。

       又发现了自己的一个非常严重的毛病,喜欢怨天尤人,不知道从什么时候开始,有很多的抱怨,有很多的无奈和叹息,曾经是多少人学习的榜样,可是现在我却发现我是最底层,自己搞不懂为什么?我真的想迅速成长起来,可是迈一步都是困难重重,接踵而至的就是漫无边际的苦恼和彷徨,而且这种痛苦我不知道怎样排解,没有一个人理解我,他们认为,不会就赶快学,谁都帮不了你,是的,这是正确的,我试过无数次,可是总看不到效果。

       这种压力,这种郁闷是我生活二十多年来从来没碰到过的,我又犯错误了,很多人说,不要再说这样的话,以后会有更多你碰不到的,会有更苦的日子等着你,毕竟我们现在要学会承担责任了。。。

       罗嗦了一大顿,终究还是最后一句话给我力量了,对,我要学会承担责任,我才刚刚开始自己生活,以后碰到的困难会很多,而且父母老了,以后还会有孩子,我要成为别人依靠的对象了,想想这些,还有什么苦不能吃呢,好好的努力,慢慢的煎熬,总有一天,我会慢慢的理解很多。

       只要努力,猪也会成才的!! 


        Road of the rise.

呵呵,学习之余,做歌一首:

    加加难
                作者: 小小C

C++之难,难于上青天!
自学路上多险阻,只身孤闯山林间.
又无大师指明路,摔跤绊倒是平闲.
整型浮点分不清,数据转换失心眼.
最怕指针空中悬,莫名其妙内存泄.
又恐数组越了位,运行期间报危险.
指针引用似兄弟,使用不当却翻脸.
const功能虽广泛,理解还需费时间.
自增自减应注意,前缀后缀意思变.
运算符号分不清,胡乱重载图方便.
判断循环结构难,拍桌撞墙快疯癫.
类与对象好难懂,一个概念想半天.
请位高手来讲解,越说越乱直转圈.
到了最后说不懂,鼻孔顿出斤鼻血.
无可奈何自己上,锥股悬头到半夜.
咣当一声五窍通,拔开乌云见青天.
急忙登堂又入室,不料难题阻眼前.
继承还分父与子,多态又出虚方法.
公有私有才分清,隐性显性头晕眩.
构造析构又重载,抽象封装还友元.
异常处理要细心,模板容器需常练.
冥思苦想类架构,脑袋抓破头冒烟.
狠下决心写游戏,万行代码出指间.
提心吊胆去编译,千条错误直瞪眼.
心理承受能力低,差点进了疯人院!
大哭一场砸机器,硬着头皮又重写.
积累经验熟生巧,程序出炉功效见.
被人高手一声叫,面红心跳喜笑颜.
拿来新手问题看,抓耳挠腮没法解.
浪得高手名儿虚,空得欢喜只一叹:
——C++之难,难于上青天!

Game Developer Magazine 1994 - 2000年,共7年的游戏开发者杂志电子版(含源码)

Graphics Programming Black Book (by Michael Abrash),图形编程黑书,Id software的Michael Abrash编著

Game Programming Gems I.rar ,游戏编程精粹I、II(含源码)

Game Programming Gems II.rar

Graphics Gems I.rar ,图形学精粹I、II、III、IV、V

Graphics Gems II.rar

Graphics Gems III IBM.rar

Graphics Gems IV IBM.rar

Graphics Gems V.rar

Computer Graphics, C Version (2nd Ed.).rar ,计算机图形学C版(第二版)

half_life2_engine.rar ,半条命2游戏引擎源代码

D3DTutorial10_Half-Life2_Shading.pdf ,半条命2游戏引擎光照分析

Course.PTR.3D.Game.Engine.Programming.eBook-LinG.rar ,3D游戏引擎编程

3D Game Engine Design.rar ,3D游戏引擎设计

3D Game Engine Design source code.rar ,3D游戏引擎设计源代码

3D.Game.Programming.All.In.One.pdf ,3D游戏编程大全

Game Design - Theory and Practice.rar ,游戏设计 - 理论与实践

game.programming.all.in.one.pdf ,游戏编程大全

The Cg Tutorial - The Definitive Guide to Programmable Real-Time Graphics.rar ,CG指导 - 可编程实时图形权威指南

Tricks Of The 3D Game Programming Gurus - Advanced 3D Graphics And Rasterization.rar ,3D游戏编程大师技巧 - 高级3D图形和光栅化

Ultimate Game Design Building Game Worlds.rar ,终极游戏设计 - 创建游戏世界

Core.Techniques.And.Algorithms.In.Game.Programming.rar ,核心技术和算法在游戏编程

Simulating Humans.rar ,仿真人类


3D Lighting - History, Concepts, and Techniques.rar ,3D光照 - 历史,概念和技术

Lighting.Techniques.For.Real-Time.3D.Rendering.rar ,光照技术For实时3D渲染

Vector.Game.Math.Processors.pdf ,向量游戏数学处理器

AI for Computer Games and Animation - A Cognitive Modeling Approach.rar ,AI(人工智能)for计算机游戏和动画 - 一个认知建模方案

AI.Game.Development.Synthetic.Creatures.With.Learning.And.Reactive.Behaviors.rar ,AI游戏开发 - 合成生物With学习和反应举止

AI.Techniques.for.Game.Programming.rar ,AI技术for游戏编程(含源码)

Ai.Game.Programming.Wisdom.rar ,AI游戏编程(代码)

Real_Time_Rendering.rar ,实时渲染

special.effects.game.programming.rar ,特效(特殊效果)游戏编程(含源码)

Shaders.for.Game.Programmers.and.Artists.pdf ,着色器for游戏开发者和艺术家

Real-Time Shader Programming.rar ,实时着色器编程

Wordware.Publishing.Advanced.Lighting.and.Materials.With.Shaders.rar ,高级光照和材质with着色器

OReilly - Physics for Game Developers.rar ,物理for游戏开发者


GPU Programming Guide.rar ,GPU编程指南

Collision.Detection.pdf.rar ,碰撞检测

Collision.Detection.-.Algorithms.and.Applications.rar ,碰撞检测 - 算法与应用

focus.on.3D.terrain.pdf ,游戏3D地形编程

Focus.On.3D.Models.pdf ,游戏3D模型编程

Focus.On.2D.in.Direct3D.-.fly.pdf ,集中于Direct3D中的2D

Beginning.Game.Audio.Programming.rar ,开始游戏音频编程(含源码)

Internetworked.3D.Computer.Graphics.rar ,互连网间的3D计算机图形学


Beginning.Math.and.Physics.For.Game.Programmers.pdf ,开始数学和物理for游戏编程者(PDF版)

Beginning Math and Physics for Game Programmers[CHM].rar ,开始数学和物理for游戏编程者(CHM版)

3D.Math.Primer.for.graphics.and.game.development.pdf ,3D数学初步for图形和游戏开发

The art of computer game design.rar ,计算机游戏设计艺术

Sams.Beginning.3D.Game.Programming.eBook-LiB.rar ,3D游戏编程入门

MIT.Press.Rules.of.Play.Game.Design.Fundamentals.rar ,游戏设计基础

design a pc game engine.rar ,设计一个PC游戏引擎


Advanced Graphics Programming Techniques Using Opengl.rar ,高级图形编程技术用OpenGL

Real-time.Rendering.Tricks.and.Techniques.in.DirectX.pdf ,DirectX实时渲染技巧与技术

Real-Time Rendering Tricks and Techniques in DirectX(src).ZIP ,DirectX实时渲染技巧与技术(源代码)

Real Time 3D Terrain Engines Using C++ And Dx9.rar ,实时3D地形引擎用C++和Dx9

MS.Press.-.Microsoft.DirectX.9.Programmable.Graphics.Pipeline.rar ,Microsoft DirectX9可编程图形管线

Wordware.Publishing.OpenGL.Game.Development.eBook-YYePG.rar ,OpenGL游戏开发

Beginning.OpenGL.Game.Programming.ebook.pdf ,OpenGL游戏编程入门

OpenGL.Programming.Guide.rar ,OpenGL编程指南

Addison.Wesley.-.OpenGL.Programming.Guide.2nd.Edition.rar ,OpenGL编程指南(第二版)

Addison-Wesley,.OpenGL.Shading.Language.(2004).DDU.ShareConnector.rar ,OpenGL着色语言(2004)

Learn Vertex and Pixel Shader Programming With Directx 9.rar ,学习顶点和像素着色器编程用DirectX9

Shaderx2 - Shader Programming Tips & Tricks With Directx 9.rar ,Shaderx2 - 着色器编程提示与技巧With DirectX9

ShaderX2 Introductions and Tutorials with DirectX9.rar ,ShaderX2介绍和指导With DirectX9

Direct3D.ShaderX.-.Vertex.and.Pixel.Shader.Tips.and.Tricks.rar ,Direct3D.ShaderX - 顶点和像素着色器提示和技巧

Advanced 3D Game Programming with DirectX 9.rar ,高级3D游戏编程用DirectX 9.0(含源码,CHM版)

Advanced 3D Game Programming with DirectX 9[PDF].rar ,高级3D游戏编程用DirectX 9.0(PDF版)

DirectX 3D Graphics Programming Bible.rar ,DirectX 3D图形编程宝典

Introduction to 3D Game Programming with DirectX 9.0.rar ,介绍对3D游戏编程用DirectX9.0(含部分源代码)

Beginning.Direct3D.Game.Programming.rar ,Direct3D游戏编程入门

Beginning.DirectX9.pdf ,DirectX9入门

Cutting Edge Direct 3D Programming.rar ,Cutting Edge(刀刃)Direct 3D编程

Game.Scripting.Mastery.pdf ,游戏描述语言掌握

Data.Structures.for.Game.Programmers.rar ,数据结构for游戏编程者(含源码)

2_OpenGL.Extensions.-.Nvidia.rar ,OpenGL扩展(Nvidia)

Managed.DX.9.Kick.Start.Graphics.And.Game.Programming.rar ,DirectX9图形和游戏编程

OpenGL.Reference.Manual.rar ,OpenGL参考手册

OpenGL.Super.Bible.rar ,OpenGL超级宝典


Tricks of the Windows Game Programming Gurus.rar ,Windows游戏编程大师技巧

Tricks of Win Game Programming Gurus 2ed.rar ,Windows游戏编程大师技巧(第二版)

Game.Programming.Beginners.Guide.rar ,游戏编程初学者指南

Chris_Crawford_on_Game_Design.rar ,Chris Crawford写的游戏设计书


Advanced.Animation.with.DirectX.rar ,高级动画with DirectX(含源码)

Inside Direct3D.rar ,深入Direct3D

Direct3D_9_Basics.rar ,Direct3D 9基础

Sams Teach Yourself DirectX 7 in 24 Hours.rar ,教你自己DirectX7在24小时

Programming.Role.Playing.Games.with.DirectX.rar ,用DirectX编程RPG游戏(含源码)

Programming Multiplayer Games.rar ,编程多玩家游戏

Net Game Programming With Directx 9.0.rar ,网络游戏编程with Directx 9.0

Programming Linux Games.rar ,编程Linux游戏

Developing Online Games - An Insiders Guide.rar ,开发在线游戏 - 一个权威人士的指导

Game Coding Complete.rar ,游戏编码完全

Strategy Game Programming with DirectX 9.0.rar ,策略游戏编程用DirectX9.0

Strategy Game Programming with DirectX 9.0 Source Code.zip ,策略游戏编程用DirectX9.0(源代码)

Addison-Wesley - Software Engineering and Computer Games.rar ,软件工程和计算机游戏

Artificial Intelligence and Software Engineering.rar ,AI和软件工程

Game-Programming-OpenGL-C++.rar ,有关OpenGl和C++的一些资料


GBA Programming Game Boy Advance The Unofficial Guide.rar ,GBA编程非官方指南

Palm.OS.Game.Programming.pdf ,Palm掌上操作系统游戏编程

Mac.Game.Programming.pdf ,苹果机游戏编程

Premier.Press.J2ME.Game.Programming.rar ,J2ME游戏编程

J2ME Game Development with MIDP2.rar ,J2ME游戏开发with MIDP2

PHP.Game.Programming.pdf ,PHP语言游戏编程

Game.Programming.with.Python.Lua.And.Ruby.pdf ,游戏编程用Python,Lua和Ruby语言

Apress.dot.NET.Game.Programming.with.DirectX.9.0.eBook-KB.rar ,点NET游戏编程用DirectX9

Wordware.Wireless.Game.Development.In.C.Cpp.With.BREW.chm ,无线游戏开发用C、C++ With BREW


DirectX9 User Interfaces Design and Implementation.rar ,DirectX9用户接口设计和实现

Game.Interface.Design.rar ,游戏接口设计




SAMS Teach Yourself Game Programming in 24 Hours.rar ,教你自己游戏编程在24小时

C.Game.Programming.For.Dummies.2.rar ,C游戏编程傻瓜书2

Beginners Guide to DarkBASIC Game Programming.rar ,初学者指南对DarkBASIC游戏编程

Windows Graphics Programming Win32 GDI and DirectDraw.rar ,Windows图形编程 - Win32 GDI 和 DirectDraw

Game Programming Genesis.rar ,游戏编程起步


2D.Artwork.and.3D.Modeling.for.Game.Artists.pdf ,2D艺术品和3D建模for游戏艺术家

Game.Art.for.Teens.pdf ,游戏艺术for青年人


Game Development and Production.rar ,游戏开发和产品

Game.Developers.Market.Guide.rar ,游戏开发者市场指南



MIT.Press.A.History.Of.Modern.Computing.eBook-LiB.rar ,现代计算历史

The C++ Programming Language NO.3 Edition.rar ,C++编程语言(第三版)

Computer Systems A Programmers Perspective.rar ,计算机系统 - 一个编程者的透视

Intel Architecture Software Developer Manual.zip ,Intel架构软件开发者手册

Intel Itanium Assembly Language Reference.rar ,Intel Itanium架构汇编语言参考

Agile Software Development.rar ,灵活的软件开发

Code Reading The Open Source Perspective.rar ,代码阅读与开放源透视


ps2DevEnvironment.exe ,PS2开发环境

doxygen-1.3.9.1-setup.exe ,一种文档自动生成软件

wolf_source.exe ,wolf游戏(id software)源代码

doom_src.zip ,doom游戏(id software)源代码

q2src320.exe ,Quake2游戏(id software)源代码

Q3A_TA_GameSource_127.exe ,QuakeIII(id software)游戏源代码

Q3A_ToolSource.exe ,QuakeIII(id software)工具源代码

如何为我的游戏实现一个UI系统,这个问题我想了很久,不过我现在可不像开始的时候那样一点思路也没有。如果你也被这个问题所困扰,我十分乐意与你分享这几天来的学习成果。嘿嘿,我是不是有点得意忘形了?

在开始之前,我要提醒你,学而不思则惘。在看这篇文章的时候,请时刻保持头脑清醒,如果有什么不太明白的话,请停下一两分钟,好好想想,这篇文章可不是囫囵吞枣就能看懂的哦!此外这篇文章是建立在部分实例和猜测的基础上的,可能存在着大量的不科学的想法和严重的错误,如果你在实践的过程中出现了问题欢迎提问,如果你发现了其中的错误请你指出来,如果将来你发现被误导了(当然我会尽力减小这种可能),请不要埋怨,因为是否继续往下看是你自己的决定。

1、窗口
UI系统的表现形式是什么?在开始前我们有必要弄清这个问题。
我们需要对话框、按钮、单选按钮、复选按钮、滚动条、下拉列表……好了好了,想不到你一口气竟能说出这么多种窗口。是的,这些都是不同形式的窗口,UI系统正是靠着形形色色的窗口展示自己,请记住这一点。如果你还是不明白,就看一看MSDN中的Hierarchy Chart。

2、理解windows的UI系统
windows这样一套经典的图形操作系统,我如果不拿它做例子,实在是有点儿对不住比尔大叔啊。
窗口都是矩形的,不要跟我说不规则窗口,其实那也是一个矩形的,只不过有些地方没有画而已。既然是矩形,只要知道它的长和宽(Width,Height,有点儿不一样是吧?windows里叫宽和高),它就确定了。然后,你把它放在某个位置上,所以它又有了坐标(xPos,yPos)。你的UI系统至少也要有这些数据,不然就没法画了。
然后是各式各样的事件,当鼠标经过的时候,按钮变亮了;当你按下Alt+F的时候,弹出了一个菜单。不管在windows里是哪个设备驱动把这些信息告诉了UI系统,我们的确需要它,不是吗?
一个菜单被按下然后一个对话框弹了出来,是谁的结果?是鼠标吗?怎么可能!当然是UI系统干的。UI系统不仅要接受各种输入设备的信息改变相应的外观,它还要根据不同的操作产生消息。菜单按下时,windows会主动地往消息队列里发送一个WM_COMMAND消息。至于对话框是否会弹出来,就要看你是否对这个消息进行处理了。

还有别的什么吗?
当然有啦,只是我也不很清楚,毕竟才研究了三四天嘛。不过我可以告诉你是什么:那么多不同类型的窗口,windows是怎么区分的?这个问题很关键啦,窗体如何绘制,事件如何激发全都需要判断的。比如说一个普通按钮和一个单选按钮,一个按下之后要弹起来,另一个要有一个圆点,明显的不同。既然都是窗口,windows是怎么做到的呢?我在winuser.h里面看到了大量关于STYLE的宏,我猜是用了CASE判断,可是为每一个窗口都作判断也太麻烦了吧,还在研究中。。。

3、窗口类的封装
封装是为了更好的使用,但不是必需的。就像使用MFC我们可以方便开发项目。(不要跟我讨论你对MFC的感情,那是你自己的事)然而如果没有他,只用SDK我们一样可以完成上司的任务。但是为了方便我建议你这样做,封装你的窗口类。我没有对此作太多的研究,我想把更多的时间放到研究UI系统本身上。如果你想了解得更多,请看这里http://blog.csdn.net/mythma

4、消息映射及命令绕行
和封装窗口类一样,这也只是一个辅助性的工作。这也是MFC的做法,如果你的游戏不需要如此复杂的方法,或是你对此方法有成见的话就不要做了。也许,我真的有点偏离我的初衷,不像是在给我的游戏实现一个UI系统了。如果你想为类似DOS的系统添加一个GUI的话,嘿嘿,X-Window那样子的。按本文的UI系统做也许会有点小用。

对于消息映射和命令绕行,我说不好,看了两天《深入浅出MFC》,才稍微有点明白,建议你自己去看。我只说一点不同之处,游戏中MFC的方法不是完全适用的。MFC是一个应用程序框架,MFC命令绕行的过程控制的是程序的全部,而游戏的UI系统只是一个辅助部分,不是全部,甚至可以去掉(我是说做到游戏里去,我猜95仙剑的菜单就是和游戏一体的)。UI系统要根据鼠标键盘做出不同的反应,绘制不同的界面,它又不是程序的全部,所以你需要把每一个可用(有用)的消息都传给UI系统。(不一定是全部的消息,比如说我的UI只使用鼠标操作,那就只传递WM_LBUTTONDOWN、WM_LBUTTONUP、WM_MOUSEMOVE、WM_LBUTTONDBLCLK等等)
while(1)
{
    if(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)
    {
        if(!GetMessage(&msg,NULL,0,0))
            break;
        if(!TranslateAccelerator(msg.hwnd,hAccelTable,&msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
        // 在这里添加你向UI系统传递消息的代码
            UI_DecipherMessage(&msg);
    }
    else
    {
        // 在这里添加你游戏的主要逻辑代码
        GameMain();
    }
}
然后,做一个仅使用在UI系统内部的消息映射和命令绕行,它处理的是UI系统自己产生的消息,或是改变一些窗口的显示与关闭,或是向游戏主要逻辑做出如使用物品、退出游戏之类的信号。是的,UI系统需要自己产生很多消息,不要指望windows会主动因为你的控件被按下而向消息队列里传送WM_COMMAND消息,你得自己做这些,使用PostMessage函数就可以了。有时候工作会更多,因为你面对的是windows的消息队列和UI系统自身的消息队列。

5、一个例子
HOHOv5中的UI系统作为这篇文章的例子再合适不过了,可以说我的思路可能是被它干扰了。

在下一篇中,我可能会讲讲本文未能说清楚的问题和一个不用消息映射及命令绕行的实例。

参考:
(1)HOHOv5的UI系统头文件(没办法,我没有任何一个版本的源文件,也没厚着脸皮去找人家要。嘿嘿,关键是没法要。写这篇文章的时候我的网还没有办下来,怎么到我这里就预留接口不足,气死了)
(2)《深入浅出MFC》第一章的讯息映射的雏形、第三章的讯息映射和命令绕行、第九章的全部
(3)MSDN和winuser.h
(4)窗口之父CXWnd的封装

7.1 什么是用户界面库(UI LIB)?

程序员总是喜欢捷径,没有人希望做重新发明车轮的事。在开发程序的时候,我们总是想法设法的包含各式各样的库,通过那些事先写好的函数来完成我们的工作。例如,文件读写函数或者printf和scanf例程允许我们完成不同的任务而不需要学习硬件的细节。因此,使用库可以节省我们的开发时间并且使我们的软件兼容性更好。我们在第二部分开发的UI LIB同样会为界面开发人员提供这样的好处。最终,它将包含一组类和函数来帮助我们在短时间内开发出一流的界面。对于那些使用我们库的开发者来说,他们只需要简单的在工程中添加#include <UILIB.h>和适当的lib文件就可以获得全部的功能。下面让我仔细看看UI LIB由什么组成。

7.2 像类一样的控件

在第一章中我们曾解释过如何用一组控件制作界面,像按钮、列表框、文本框和复选按钮等,以及用户和程序是如何通过控件通信的。因此,UI LIB也将会是一组控件的集合。

7.3 控件——类的层次和基础控件

开发UI LIB从哪里开始最好呢?我们应该从开发一个按钮、文本框或者一个下拉列表开始吗?或者有什么基本的开发结构是我们必须遵守的?实际上,最好的开始是问问自己什么是控件。只有这么做,我们才能知道什么是所有控件的共同属性。实际上,只要它包含这些属性它就可以被认为是一个控件了。随着章节的进程我们将逐个检测这些属性。根据类的特点,阶段性的开发这些控件意义重大,我们将从一个基础类或者基础控件开始。它仅包含了所有控件的基本属性,什么也不多。其他的控件,诸如按钮和标签,都将从它派生出来。这样我们就不必为每一个控件单独编写相同的功能了。我们把这个基础类命名为CXControl,我们将用两章讲解它。图7.1展示了UI LIB的层次结构。

图7.1

注意
我用了两章讲解CXControl,因为它是一个那样庞大并且重要的概念。我为这个类添加了大量的功能,以便定制那些派生类的工作简单并且迅速。

7.4 CXControl——旅行的开始

在UI LIB中CXControl作为一个基类出现,其他的类皆从CXControl派生而来。作为其他类的祖先,CXControl为它们提供了一组共有的特征。本章致力于CXControl的开发,后面的部分研究了什么才是所有控件的共有属性以及如何在CXControl中实现它们。开发将从一个空白的类的声明开始,随着研究的深入,我们将会慢慢地为它填充内容。

7.5 定义CXControl——控件和画布

图7.2

图7.2 一张空白的画布

不同的控件之间有着明显的区别,列表框是一个外观,按钮则是另外一个样子的。但是所有的控件都表现为其父控件边界内的一块矩形区域,控件在这个区域内绘制自己。在术语中,这块绘制图像的矩形区域被称为画布(canvas)。实际上,它就是像表面(surface)或纹理(texture)那样的一组像素。它的大小用宽(width)和高(height)表示,显示状态分为可见和不可见。下面的代码展示了canvas是如何实现的。

class CXControl
...{
protected:
    DWORD m_Width;        //画布的宽
    DWORD m_Height;       //画布的高
bool m_Visible;       //画布是否可视
    CXTexture * m_Canvas; //指向画布的指针
    CXPen * m_Pen;        //一些要画在画布上的东西
public:
    CXTexture * GetCanvas(void) ...{return m_Canvas;}
void SetCanvas(CXTexture * Texture) ...{m_Canvas = Texture;}
bool GetVisible(void) ...{return m_Visible;}
void SetVisible(bool Visible) ...{m_Visible = Visible;}
    CXPen * GetPen(void) ...{return m_Pen;}
void SetPen(CXPen * Pen) ...{m_Pen = Pen;}
    DWORD GetWidth(void) ...{return m_Width;}
    DWORD GetHeight(void) ...{return m_Height;}
void SetWidth(DWORD Width) ...{m_Width = Width;}
void SetHeight(DWORD Height) ...{m_Height = Height;}
};

注:绘制的细节将在下一章研究消息和事件响应的时候介绍。另外,像media player使用的那种非矩形控件不在本书的讨论范围内。

7.6 父控件、兄弟控件、子控件

第一章曾简要地提到过界面中各种控件的层次关系。因此,控件之间是密切相关的。例如,除了桌面之外这些界面中顶层的控件是没有父控件的。通常,这类控件用作应用程序的主窗口,它包含着按钮、复选按钮之类的其他控件。这些控件是一个窗口的孩子,是彼此的兄弟,而这个窗口就是它们的父亲。实际上,这种层次关系对控件来说是最重要的影响之一,在应用程序创建和销毁它们的时候就决定了。看图7.3来想象一下这种层次关系。

图7.3

图7.3

在先前的章节中,我们研究过如何使用链表来有效地管理鼠标指针列表。现在我们将使用一个改进了的方法来处理控件之间的关系。还记得吗,链表就是一个线性的项的列表,其中每一项都有一个指针指向它的下一项,但最后一项是个例外,它的指向为空(NULL)。这是存储像控件的孩子那样的项的理想方式,但缺点是你只能在链表上沿着一个方向移动。虽然不是什么大问题,但这是多么的不方便和不切实际啊。解决这个问题的办法就是使用双向链表。这样,每个控件都维持了指向前后兄弟的指针,换句话说就是用两个指针分别指向链表中此控件的前一控件和后一控件。对于开发者来说,这样的安排有几个好处:一、你可以在此列表上双向移动,从任意一点开始到任意一点结束;二、你可以删除任意的项,然后将缺口修补好;三、你可以完成所有形式的排序以及重新整理项的操作。看图7.4理解双向链表的概念。

图7.4

图7.4

因此,使用双向链表来实现控件之间的关系是不错的选择。要为CXControl添加这样一个列表来操纵它的孩子只需要简单地添加几个不同的指针:一个指向父控件,一个指向前后兄弟控件(译者:其实就是两个),一个指向第一个子控件。为了管理这些指针,我们还得添加几个函数,这包括添加子控件的函数、通过兄弟列表操纵的函数和删除子控件的函数。这些在后面的小节中都有讲解。先看一下修改过的CXControl类的声明。

class CXControl
...{
protected:
    DWORD m_Width;
    DWORD m_Height;
bool m_Visible;
    CXTexture * m_Canvas;
    CXPen * m_Pen;
    CXControl * m_ChildControls;
    CXControl * m_NextSibling;
    CXControl * m_PreviousSibling;
    CXControl * m_Parent;
public:
// Accessors
    CXTexture * GetCanvas(void) ...{return m_Canvas;}
void SetCanvas(CXTexture * Texture) ...{m_Canvas = Texture;}
bool GetVisible(void) ...{return m_Visible;}
void SetVisible(bool Visible) ...{m_Visible = Visible;}
    CXPen * GetPen(void) ...{return m_Pen;}
void SetPen(CXPen * Pen) ...{m_Pen = Pen;}
    DWORD GetWidth(void) ...{return m_Width;}
    DWORD GetHeight(void) ...{return m_Height;}
void SetWidth(DWORD Width) ...{m_Width = Width;}
void SetHeight(DWORD Height) ...{m_Height = Height;}
    CXControl * GetParentControl(void) ...{return m_Parent;}
void SetParentControl(CXControl * Control) ...{m_Parent = Control;}
    CXControl * GetNextSibling(void) ...{return m_NextSibling;}
void SetNextSibling(CXControl * Control) ...{m_NextSibling = Control;}
    CXControl * GetPreviousSibling(void) ...{return m_PreviousSibling;}
void SetPreviousSibling(CXControl * Control) ...{m_PreviousSibling = Control;}
    CXControl * GetFirstChild(void) ...{return m_ChildControls;}
void SetFirstChild(CXControl * Control) ...{m_ChildControls = Control;}

    CXControl * AddChildControl(CXControl * Control);
    CXControl * RemoveChildControl(CXControl * Control);
void RemoveAllChildren();
int GetChildCount();
};

7.6.1 添加子控件

控件通过m_ChildControls指针存储其子控件的信息。如果要把一个已存在的控件变成另外一个控件的孩子,你需要调用CXControl的AddChildControl方法。看看这个函数的定义,是不是有点眼熟?

注意,这个函数与前一章把光标添加到链表的函数稍有不同。这里我们创建的是一个双向链表,因此,除了后一个兄弟控件之外,前一个兄弟控件也需要设置。这样我们才能双向的操纵这个列表。

CXControl * CSControl::AddChildControl(CXControl * Control)
...{
    Control->SetParentControl(this);
    CXPen * Pen = Control->GetPen();
    SAFE_DELETE(Pen);
    Control->SetPen(this->GetPen());

if(!m_ChildControls)
        m_ChildControls = Control;
else
...{
        CXControl * Temp = this->GetFirstChild();

while(Temp->GetNextSibling())
            Temp = Temp->GetNextSibling();

        Temp->SetNextSibling(Control);
        Control->SetPreviousSibling(Temp);
    }

return Control;
}

7.6.2 清除子控件

清除子控件就是将它们全部删除的过程。要达到这个目的,调用CXControl的RemoveAllChildren方法就可以了。在前一章,我们展示过一个类似的过程,请看下面的函数定义。

void CXControl::RemoveAllChildren()
...{
    CXControl * Temp = this->GetFirstChild();

while(Temp)
...{
        CXControl * Next = Temp->GetNextSibling();
        SAFE_DELETE(Temp);
        Temp = Next;
    }
}

7.6.3 删除指定的子控件

图7.5

图7.5

在前一章我们没有见到过如何删除列表中任意位置的项,而双向链表使这个过程变得简单了。例如,我们想要删除项目I,只要完成一下步骤:用N指向I的后一个兄弟控件,P指向I的前一个兄弟控件,然后删除I,最后将P的后一个兄弟指向N。看图和下面的定义你可以很快理解它。

CXControl * CXControl::RemoveChildControl(CXControl * Control)
...{
    CXControl * Next = Control->GetNextSibling();
    CXControl * Previous = Control->GetPreviousSibling();
    SAFE_DELETE(Control);
    Next->SetPreviousSibling(Previous);
    Control = Next;
return Control;
}

7.6.4 统计子控件的数量

有时候如果能知道指定的控件有多少子控件会很有帮助。计算它们很简单。只要遍历它的子控件并增加计数器就可以了。你可以调用CXControl的GetChildCount来完成此功能,函数定义如下:

int CXControl::GetChildCount()
...{
int Count = 0;
    CXControl * Temp = this->GetFirstChild();

while(Temp)
...{
        CXControl * Next = Temp->GetNextSibling();
        Count++;
        Temp = Next;
    }
return Count;
}

7.7 绝对坐标和相对坐标

图7.6

图7.6
注意,这个按钮的绝对坐标和相对坐标是不一样的。一个表示的是它在屏幕上的位置,而另一个表示的是它在它的父控件中的位置。

第7.5小节解释过什么是画布以及任何可视的东西本质上都是控件。它示范了如何用宽和高描述一个控件的大小,如何用可见和不可见表示控件的显示状态。但是我们忽略了另外一个属性——坐标。很显然,每一个控件都有X和Y两个坐标。坐标又分绝对坐标和相对坐标两种。绝对坐标是人们想起坐标时立即跳进人们脑子的想法,它是从屏幕的左上角开始计算的。相对坐标是相对于它的父控件来说的,换句话说,它是从其父控件的左上角开始计算的。有些人可能想问这个区别是否真的重要。我可以十分肯定的回答你,是的。为什么?看看图7.6你就明白了。在UI LIB中,所有的控件都使用的是相对坐标,因为它比绝对坐标更简单更直观。虽然有时候我们也不得不计算它的绝对坐标,你会在下一小节看到如何实现它。

class CXControl 
...{
public:
    CXControl();
virtual ~CXControl();

protected:
    D3DXVECTOR2 m_Position;
    DWORD m_Width;
    DWORD m_Height;
bool m_Visible;
    CXTexture * m_Canvas;
    CXPen * m_Pen;
    CXControl * m_ChildControls;
    CXControl * m_NextSibling;
    CXControl * m_PreviousSibling;
    CXControl * m_Parent;
public:
// Accessors
    CXTexture * GetCanvas(void) ...{return m_Canvas;}
void SetCanvas(CXTexture * Texture) ...{m_Canvas = Texture;}
bool GetVisible(void) ...{return m_Visible;}
void SetVisible(bool Visible) ...{m_Visible = Visible;}
    CXPen * GetPen(void) ...{return m_Pen;}
void SetPen(CXPen * Pen) ...{m_Pen = Pen;}
    DWORD GetWidth(void) ...{return m_Width;}
    DWORD GetHeight(void) ...{return m_Height;}
void SetWidth(DWORD Width) ...{m_Width = Width;}
void SetHeight(DWORD Height) ...{m_Height = Height;}
    CXControl * GetParentControl(void) ...{return m_Parent;}
void SetParentControl(CXControl * Control) ...{m_Parent = Control;}
    CXControl * GetNextSibling(void) ...{return m_NextSibling;}
void SetNextSibling(CXControl * Control) ...{m_NextSibling = Control;}
    CXControl * GetPreviousSibling(void) ...{return m_PreviousSibling;}
void SetPreviousSibling(CXControl * Control) ...{m_PreviousSibling = Control;}
    CXControl * GetFirstChild(void) ...{return m_ChildControls;}
void SetFirstChild(CXControl * Control) ...{m_ChildControls = Control;}
    D3DXVECTOR2 * GetPosition(void) ...{return &m_Position;}
    FLOAT GetXPos(void) ...{return m_Position.x;}
    FLOAT GetYPos(void) ...{return m_Position.y;}
void SetXPos(FLOAT X) ...{m_Position.x = X;}
void SetYPos(FLOAT Y) ...{m_Position.y = Y;}
void SetXYPos(FLOAT X, FLOAT Y);

    CXControl * AddChildControl(CXControl * Control);
    CXControl * RemoveChildControl(CXControl * Control);
void RemoveAllChildren();
int GetChildCount();
void GetAbsolutePosition(D3DXVECTOR2 * Position);
};

注意
在层次结构中,像应用程序主窗口这样的顶层控件的绝对坐标和相对坐标是一样的。这是因为除了桌面以外,顶层控件没有父控件,而桌面覆盖了整个屏幕。(译者:我一直都把桌面作为顶层控件的父控件,而桌面的ParentControl为NULL)

7.7.1 计算坐标

图7.7

图7.7

通过一个控件的相对坐标可以轻松地计算出它的绝对坐标。这使得拖动窗口在屏幕上移动时控件正确的重绘变得简单,因为控件与控件之间的相对坐标是不变的。

CXControl的GetAbsolutePosition方法可以返回一个控件的绝对坐标,也就是控件在整个屏幕上的坐标。为了正确的绘制控件,我们会经常用到绝对坐标。计算控件的绝对坐标是一个简单的过程:你可以简单地用控件的相对坐标加上其父控件的绝对坐标。实际上,这是从一个控件到它的顶层父控件的相对坐标的累积。函数定义如下。

void CXControl::GetAbsolutePosition(D3DXVECTOR2 * Position)
...{
    Position->x+=this->GetXPos();
    Position->y+=this->GetYPos();

if(this->m_Parent)
this->m_Parent->GetAbsolutePosition(Position);
}

7.8 类CXControl目前的声明

代码同7.7,略

7.9 总结

本章以CXControl的形式初步介绍了UI LIB。这个类包含了所有控件都必须拥有的一般属性。下一章我们将继续深入这个主题。但在继续之前,我们复习一下所学内容。

■库是能完成某一任务的函数、结构和类的集合。库主要是通过提供完成任务的工具来减轻程序开发者的负担。像DirectX就是一个例子。

■UI LIB是User Interface Library的缩写。它由像按钮、列表框、复选框之类的一组控件组成。开发者可以用它为自己的软件创建用户界面。

■即使控件千差万别,但它们都继承了一个共有的属性集。这就是为什么开发CXControl。虽然它本身不能独立实例化,但它作为一个基类出现,为其他控件提供基础特征集。

■在几何学上,控件是一个典型的被称为画布的矩形区域。它可以用宽和高,可见和不可见表示。控件在画布区域内绘制自己。因此,按钮有一个外观,而列表是另一个。

■界面中的每一个控件都存在在一个层次结构中。顶层的控件被认为是最终的祖先或根控件,通常用作程序的主窗口。其他的控件则是它的子孙,它们同样也可以有兄弟和孩子。

■控件有两个坐标,一个绝对坐标,一个相对坐标。前者表示的是控件从屏幕左上角开始计算的真实坐标,后者表示的是从其父控件左上角开始计算的坐标。无论何时在屏幕上移动一个窗口,这个程序对重新调整控件都很有用处。

声明:本书的英文版权归原作者所有,我翻译的这些版权自然归我。你可以下载到本地保存留念,但在未取得本人书面许可时,谢绝任何形式的转载。你如果将其用于商业目的,请先与原英文版版权所有者联系,以免引起不必要的麻烦。