目录
插图清单
表格清单
范例清单
用NewEdit配合NewEdit专用的docbook snippets(代码片段)可以很方便的写Docbook文档.
如何安装呢? 下载docbook.zip,解压到一个目录下,选择 [工具]-[片段]-[片段分类管理...],增加一个分类,如docbook, 选增加,然后ok即可。再选择 [工具]->[片段]->[片段代码管理...]选中刚才增加的分类,然后选择import,选择解压后的docbook.xml文件,这时代码片段就被导入进去,打开代码片段窗口(点工具条上的侧边栏即可或[窗口]->[左侧窗口]) 在上面分类中选中增加的分类,在下面就可以看到对应的代码片段了。打开你要编辑的文档,选择需要插入代码的位置,在左边列表中双击即可插入代码。
按照某人的说法:跨平台的C++网络编程ICE才是王道。于是,我学习ICE。
ICE才出来两年,是“一种现代的面向对象中间件,可用于替代像CORBA或COM/DCOM/COM+这样的中间件。在易于学习的同时,它为各种有着苛刻的技术要求的应用提供了强大的网络基础设施。”Ice 3.0 已实现对C++, Java, Python, PHP, C# 及 Visual Basic 的支持。
这里我就不多说了,大家可以参考这篇文章:《反叛之冰:Internet Communications Engine 》。大家可以下载的ICE的官方参考手册,有中文版,不过是1.3.0版, 英文的是3.0版。
ICE是开源的,大家可以从源代码开始编译,不过较复杂,幸好有binary版本,比如我就是下载的VS2003.NET的安装包。安装完成之后按照安装目录下的Readme对IDE进行一下配置,比如VC7.1就是把ice的include加入VC7.1的引用文件目录,把ice的lib目录加入VC7.1的库文件目录。然后再把安装目录下的bin文件夹添加到系统的环境变量Path中,最后,把bin文件夹下的所有DLL文件都Copy到Windows安装目录下的System32文件夹下(win98下是System文件夹?)。
ICE自定义了一种SLICE语言,目的是定义接口,作用主要应该是保持对象调用或者数据传输时的语言无关性。
开发一个ICE应用程序可以分为三步:
写一个Slice定义, 并且编译它
写服务端, 并编译它
写客户端, 并编译它
OK,写一个小程序,实现客户发送要打印的文本给服务器,再由服务器把文本发给打印机(这里我们用屏幕显示替代),这里对代码解读请见下一章,这里不多说。
写一个Slice定义, 并且编译它:
文件Printer.ice.
module Demo {
interface Printer {
void printString(string s);
};
};
这个文件很简单, 但需要注意, 在区分大小写的系统上, 扩展名一定是小写.
编译也很简单,首先确认你已将你的bin目录加到系统的环境变量Path中.然后把上面这个片断保存成Printer.ice, 最后执行slice2cpp Printer.ice, 执行后的结果应该是自动生成了printer.h和printer.cpp.
写服务端, 并编译它
文件server.cpp.
#include <Ice/Ice.h>
#include "../print.h"
using namespace std;
using namespace Demo;
class PrinterI : public Printer {
public:
virtual void printString(const string& s,const Ice::Current&);
};
void PrinterI::printString(const string& s, const Ice::Current&)
{
cout << s << endl;
}
int main(int argc, char* argv[])
{
int status = 0;
Ice::CommunicatorPtr ic;
try {
ic = Ice::initialize(argc, argv);
Ice::ObjectAdapterPtr adapter
= ic->createObjectAdapterWithEndpoints(
"SimplePrinterAdapter", "default -p 10000");
Ice::ObjectPtr object = new PrinterI;
adapter->add(object,
Ice::stringToIdentity("SimplePrinter"));
adapter->activate();
ic->waitForShutdown();
} catch (const Ice::Exception& e) {
cerr << e << endl;
status = 1;
} catch (const char* msg) {
cerr << msg << endl;
status = 1;
}
if (ic) {
try {
ic->destroy();
} catch (const Ice::Exception& e) {
cerr << e << endl;
status = 1;
}
}
return status;
}
以VS2003的配置为例
把ice的include加入VC7.1的引用文件目录,把ice的lib目录加入VC7.1的库文件目录。然后再把安装目录下的bin文件夹添加到系统的环境变量Path中,最后,把bin文件夹下的所有DLL文件都Copy到Windows安装目录下的System32文件夹下(win98下是System文件夹?)(当然,DLL文件的问题也可以通过修改环境变量来解决,不过是那个变量呢?Who can tell me?)
新建一个C++的Win32的命令台控制程序,并且设置为空项目, 把server.cpp, printer.cpp和printer.h加入这个项目(printer.cpp和printer.h放在项目的目录的外一层目录)
项目-》属性-》C/C++ -》代码生成-》运行时库-》/MD(realse版)或/MDd(debug版)
项目-》配置属性-》C/C++-》语言-》启用运行时类型信息/GR 开启
设置:项目-》属性-》链接器-》输入-》加入iced.lib iceutild.lib,此处一定要把realse库和debug库分清, debug库后有个d
修改printer.cpp中的#include <printer.h>为#include "printer.h"
OK,编译
写客户端,并编译它
文件client.cpp.
#include <Ice/Ice.h>
#include "..\print.h"
using namespace std;
using namespace Demo;
int main(int argc, char* argv[])
{
int status = 0;
Ice::CommunicatorPtr ic;
try {
ic = Ice::initialize(argc, argv);
Ice::ObjectPrx base = ic->stringToProxy(
"SimplePrinter:default -p 10000");
PrinterPrx printer = PrinterPrx::checkedCast(base);
if (!printer)
throw "Invalid proxy";
printer->printString("Hello World!");
} catch (const Ice::Exception& ex) {
cerr << ex << endl;
status = 1;
} catch (const char* msg) {
cerr << msg << endl;
status = 1;
}
if (ic)
ic->destroy();
return status;
}
添加一个新项目到当前解决方案,按照上面的方法,对client再一次进行设置。
在解决方案管理器的解决方案上点击右键,选择批生成Debug版本,然到用资源管理器到两个解决方案的目录下的Debug文件夹中执行生产的可执行文件。先运行server.exe, 然后运行client.exe, 哈哈, 是不是在server.exe的窗口里出现了Hello World!(运行一次client.exe,出现一条)
这一节大部分内容整理自ICE中文手册,在这里我特别感谢马维达同志的翻译给我们的学习带来了方便。
读服务端代码
文件server.cpp.
#include <Ice/Ice.h>
#include "../print.h"
using namespace std;
using namespace Demo;
//惯例,用后缀I 表示这个类实现一个接口
class PrinterI : public Printer {
public:
virtual void printString(const string& s, const Ice::Current&);
};
/*
打开print.h,看看PrinterI父类的定义
namespace Demo {
class Printer : virtual public Ice::Object {
public:
//纯虚函数,不能实例化
virtual void printString(const std::string&,
//第二个参数有缺省值,实现中可以不使用
const Ice::Current&= Ice::Current()) = 0;
};
};
*/
void PrinterI::printString(const string& s, const Ice::Current&)
{
cout << s << endl;
}
int main(int argc, char* argv[])
{
//程序的退出时的状态,就是否成功执行
int status = 0;
//来包含Ice run time 的主句柄 (main handle)
Ice::CommunicatorPtr ic;
try {
//初始化Ice run time (argc和argv是run time命令参数;
//就这个例子而言,服务器不需要任何命令行参数)。
//initialize 返回一个指向Ice::Communicator对象的智能指针,
//这个指针是Ice run time 的主句柄。
ic = Ice::initialize(argc, argv);
//调用Communicator 实例上的createObjectAdapterWithEndpoints,
//创建一个对象适配器(比如:网卡就是一种适配器)。
//参数是"SimplePrinterAdapter" (适配器的名字)
//和"default -p 10000"(用缺省协议(TCP/IP),侦听端口10000 的请求。)
//显然,在应用中硬编码对象标识和端口号,是一种糟糕的做法,
//但它目前很有效;我们将在以后看到在架构上更加合理的做法。
Ice::ObjectAdapterPtr adapter
= ic->createObjectAdapterWithEndpoints(
"SimplePrinterAdapter", "default -p 10000");
//服务器端run time 已经初始化,实例化一个PrinterI 对象,
//为我们的Printer 接口创建一个servant(serv 服务+-ant人,背一下单词)。
Ice::ObjectPtr object = new PrinterI;
//我们调用适配器的add,告诉它有了一个新的servant ;
//传给add 的参数是刚才实例化的servant,再加上一个标识符。
//在这里,"SimplePrinter" 串是servant 的名字
//(如果我们有多个打印机,每个打印机都可以有不同的名字,
//更正确的说法是,都有不同的对象标识)。
adapter->add(object,
Ice::stringToIdentity("SimplePrinter"));
//调用适配器的activate 方法激活适配器
//(适配器一开始是在暂停(holding)状态创建的;
//这种做法在下面这样的情况下很有用:
//我们有多个servant,它们共享同一个适配器,
//而在所有servant实例化之前我们不想处理请求)。
//一旦适配器被激活,服务器就会开始处理来自客户的请求。
adapter->activate();
//最后,我们调用waitForShutdown。
//这个方法挂起发出调用的线程直到服务器实现终止
//——或者是通过发出一个调用关闭run time,
ic->waitForShutdown();
}
catch (const Ice::Exception& e) {
cerr << e << endl;
status = 1;
} catch (const char* msg) {
cerr << msg << endl;
status = 1;
}
if (ic) {
try {
//必须调用Communicator::destroy结束Ice run time。
//destroy 会等待任何还在运行的操作调用完成。
//此外, destroy 还会确保任何还未完成的线程都得以汇合(joined),
//并收回一些操作系统资源,比如文件描述符和内存。
//决不要让你的main 函数不调用destroy 就终止,
//否则,后果无法想象。
ic->destroy();
} catch (const Ice::Exception& e) {
cerr << e << endl;
status = 1;
}
}
return status;
}
注意,尽管以上的代码不算少,但它们对所有的服务器都是一样的。你可以把这些代码放在一个辅助类里,然后就无需再为它费心了(Ice 提供了这样的辅助类,叫作Ice::Application,参见 10.3.1 节) 。就实际的应用代码而言,服务器只有几行代码:六行代码定义PrinterI 类,再加上三2 行代码实例化一个PrinterI 对象,并向对象适配器注册它。
读客户端代码
文件client.cpp.
#include <Ice/Ice.h>
#include "..\print.h"
using namespace std;
using namespace Demo;
int main(int argc, char* argv[])
{
int status = 0;
Ice::CommunicatorPtr ic;
try {
ic = Ice::initialize(argc, argv);
//stringToProxy 返回的代理(Proxy)类型是Ice::ObjectPrx,
//这种类型位于接口和类的继承树的根部(接口的基类)。
Ice::ObjectPrx base
=ic->stringToProxy( "SimplePrinter:default -p 10000");
//但要实际要与我们的打印机交谈,
//我们需要的是Printer 接口、不是Object 接口的代理。
//为此,需要调用PrinterPrx::checkedCast 进行向下转换(向下转型)。
//这个方法会发送一条消息给服务器,
//询问“这是Printer 接口的代理吗?”
//如果回答“是”,就会返回Printer 的一个代理;
//如果代理代表的是其他类型的接口,返回一个空代理
PrinterPrx printer = PrinterPrx::checkedCast(base);
//测试向下转型是否成功,若不成功,就抛出出错消息并终止客户。
if (!printer) throw "Invalid proxy";
//现在,我们在我们的地址空间里有了一个激活的代理,
//可以调用printString 方法,
//把享誉已久的 "Hello World!" 串传给它。
//服务器会在它的终端上打印这个串。
printer->printString("Hello World!");
}
catch (const Ice::Exception& ex) {
cerr << ex << endl;
status = 1;
} catch (const char* msg) {
cerr << msg << endl;
status = 1;
}
if (ic)
ic->destroy();
return status;
}
如果出现任何错误,客户会打印一条出错消息。例如,如果我们没有先启动服务器就运行客户,我们会得到:
Network.cpp:471: Ice::ConnectFailedException: connect failed: Connection refused
(由于windows下的命令行窗口在出错后会一闪就消失,不过我们可以在client.cpp的main函数的return status;之前加上system("PAUSE");然后再在VS2003.net中把client设置为启动项目,重新编译,运行。OK,可以看到结果了。)
首先,请大家读ICE中文手册中的Slice语言一章。 这一部分除了model(模块),在 ICE 1.3中文手册中都有描述
含有Slice 定义的文件必须以.ice 扩展名结尾,例如, Clock.ice就是一个有效的文件名。编译器拒绝接受其他扩展名。
Slice 支持#ifndef、#define、#endif,以及#include 预处理指令。它们的使用方式有严格的限制: 你只能把#ifndef、#define,以及#endif 指令用于创建双包括(double-include)块。例如:
#ifndef _CLOCK_ICE #define _CLOCK_ICE // #include 文件 here... //定义 here... #endif _CLOCK_ICE
我们强烈建议你在所有的Slice 定义中使用双包括(double-include)块(所上),防止多次包括同一文件。
#include 指令只能出现在Slice 源文件的开头,也就是说,它们必须出现在其他所有Slice 定义的前面。此外,在使用#include 指令时,只允许使用<> 语法来指定文件名,不能使用""。例如:
#include <File1.ice> // OK #include "File2.ice" // 不支持!
你不能把这些预处理指令用于其他目的,也不能使用其他的C++ 预处理指令 (比如用\ 字符来连接行、token 粘贴,以及宏展开,等等)。
在Slice 定义里,既可以使用C 的、也可以使用C++ 的注释风格:
Slice 关键字必须小写。例如, class 和dictionary 都是关键字,必须按照所示方式拼写。这个规则有两个例外:Object 和LocalObject 也是关键字,必须按照所示方式让首字母大写。
标识符以一个字母起头,后面可以跟任意数目的字母或数字。Slice 标识符被限制在ASCII 字符范围内,不能包含非英语字母,与C++ 标识符不同, Slice 标识符不能有下划线。这种限制初看上去显得很苛刻,但却是必要的:保留下划线,各种语言映射就获得了一个名字空间,不会与合法的Slice 标识符发生冲突。于是,这个名字空间可用于存放从Slice 标识符派生的原生语言标识符,而不用担心其他合法的Slice 标识符会碰巧与之相同,从而发生冲突 。
标识符(变量名等等)是大小写不敏感的,但大小写的拼写方式必须保持一致(看了后面的话,再理解一下)。例如,在一个作用域内, TimeOfDay 和TIMEOFDAY 被认为是同一个标识符。但是,Slice 要求你保持大小写的一致性。在你引入了一个标识符之后,你必须始终一致地拼写它的大写和小写字母;否则,编译器就会将其视为非法而加以拒绝。这条规则之所以存在,是要让Slice 既能映射到忽略标识符大小写的语言,又能映射到把大小写不同的标识符当作不同标识符的语言。(可以这样理解,变量名区分大小写,并且不可以是相同的单词)
是关键字的标识符:你可以定义在一种或多种实现语言中是关键字的Slice 标识符。例如,switch是完全合法的Slice标识符,但也是C++和Java的关键字。语言映射定义了一些规则来处理这样的标识符。要解决这个问题,通常要用一个前缀来使映射后的标识符不再是关键字。例如, Slice 标识符switch 被映射到C++ 的_cpp_switch ,以及Java 的_switch。对关键字进行处理的规则可能会产生难以阅读的源码。像native、throw,或export 这样的标识符会与C++ 或Java(或两者)的关键字发生冲突。为了让你和别人生活得更轻松一点,你应该避免使用是实现语言的关键字的Slice 标识符。要记住,以后Ice 可能会增加除C++ 和Java 以外的语言映射。尽管期望你总结出所有流行的编程语言的所有关键字并不合理,你至少应该尽量避免使用常用的关键字。使用像self、import,以及while 这样的标识符肯定不是好主意。
转义的标识符:在关键字的前面加上一个反斜线,你可以把Slice 关键字用作标识符, 例如:
struct dictionary { // 错误!
// ...
};
struct \dictionary { // OK
// ...
};
反斜线会改变关键字通常的含义;在前面的例子中, \dictionary 被当作标识符dictionary。转义机制之所以存在,是要让我们在以后能够在Slice 中增加关键字,同时尽量减少对已有规范的影响:如果某个已经存在的规范碰巧使用了新引入的关键字,你只需在新关键字前加上反斜线,就能够修正该规范。注意,从风格上说,你应该避免用Slice 关键字做标识符(即使反斜线转义允许你这么做)。
保留的标识符:Slice 为Ice 实现保留了标识符Ice 及以Ice (任何大小写方式)起头的所有标识符。例如,如果你试图定义一个名为Icecream 的类型, Slice 编译器会发出错误警告3。 以下面任何一种后缀结尾的Slice 标识符也是保留的:Helper、Holder、Prx,以及Ptr。Java 和C++ 语言映射使用了这些后缀,保留它们是为了防止在生成的代码中发生冲突。
(注:ICE 1.3的中文手册上没有“模块”这一部分) 模块来组织一组相关的语句是为了解决名字冲突。模块可以包含所有合法的Slice语句和子模块。你可以用一些不常用的词来给最外层的模块命名,比如公司名、产品名等等。
module ZeroC {
module Client {
// Definitions here...
};
module Server {
// Definitions here...
};
};
Slice要求所有的定义都是模块的一部分,比如,下面的语句就是非法的。
interface I { // 错误:全局空间中只可以有模块
// ...
};
多个文件可以共享同一个模块,比如:
module ZeroC {
// Definitions here...
};
//另一个文件中 :
module ZeroC { // OK, reopened module
// More definitions here...
};
把一个大的模块放到几个文件中去可以方便编译(你只需重新编译被修改的文件,而没有必要编译整个模块)。
模块将映射的语言中的相应结构,比如 C++, C#, 和 Visual Basic, Slice的modules被映射为namespaces;java中被映射为package.
除了少数与特定的程序语言相关的调用之外,ice的绝大部分API(应用程序接口)都是用Slice来定义的 。这样做的好处是可以用一个ICE API定义文件来支持所有的程序语言。
![]() | 注意 |
|---|---|
| 为了保证代码的简洁,以后文章中提及的Slice定义没有写出包含的模块,你要假定该语句是在一个模块中。 |
枚举:enum Fruit { Apple, Pear, Orange };
这个定义引入了一种名为Fruit 的类型,这是一种拥有自己权利的新类型。关于怎样把顺序值(ordinal values)赋给枚举符的问题, Slice 没有作出定义。例如,你不能假定,在各种实现语言中,枚举符Orange 的值都是2。Slice 保证枚举符的顺序值会从左至右递增,所以在所有实现语言中,Apple 都比Pear 要小。与C++ 不同, Slice 不允许你控制枚举符的顺序值(因为许多实现语言不支持这种特性):
enum Fruit { Apple = 0, Pear = 7, Orange = 2 }; // 出错
在实践中,只要你不在地址空间之间传送枚举符的顺序值,你就不用管枚举符使用的值是多少。例如,发送值0 给服务器来表示Apple 可能会造成问题,因为服务器可能没有用0 表示Apple。相反,你应该就发送值Apple 本身。如果在接收方的地址空间中, Apple 是用另外的顺序值表示的, Ice run time 会适当地翻译这个值。
与在C++ 里一样, Slice 枚举符也会进入围绕它的名字空间,所以下面 的定义是非法的:
enum Fruit { Apple, Pear, Orange };
enum ComputerBrands { Apple, IBM, Sun, HP }; // Apple已经被定义!
Slice 不允许定义空的枚举。
结构
Slice 支持含有一个或多个有名称的成员的结构,这些成员可以具有任意类型,包括用户定义的复杂类型。例如:
struct TimeOfDay {
short hour; // 0 - 23
short minute; // 0 - 59
short second; // 0 - 59
};
与在 C++ 里一样,这个定义引入了一种叫作TimeOfDay 的新类型。结构定义会形成名字空间,所以结构成员的名字只需在围绕它们的结构里是唯一的。
在结构内部,只能出现数据成员定义,这些定义必须使用有名字的类型。例如,你不可能在结构内定义结构:
struct TwoPoints {
struct Point { //错误!
short x;
short y;
};
Point coord1;
Point coord2;
};
这个规则大体上适用于Slice:类型定义不能嵌套(除了模块支持嵌套)。其原因是,对于某些目标语言而言,嵌套的类型
定义可能会难以实现,而且,即使能够实现,也会极大地使作用域解析规则复杂化。对于像Slice 这样的规范语言而言,嵌套的类型定义并无必要——你总能以下面的方式编写上面的定义(这种方式在风格上也更加整洁):
struct Point {
short x;
short y;
};
struct TwoPoints { // Legal (and cleaner!)
Point coord1;
Point coord2;
}
序列
序列是变长的元素向量:
sequence<Fruit> FruitPlatter;
序列可以是空的——也就是说,它可以不包含元素;它也可以持有任意数量的元素,直到达到你的平台的内存限制。
序列包含的元素自身也可以是序列。这种设计使得你能够创建列表的列表:
sequence<FruitPlatter> FruitBanquet;
序列可用于构建许多种collection,比如向量、列表、队列、集合、包(bag),或是树(次序是否重要要由应用决定;如果无视次序,序列充当的就是集合和包)。
序列的一种特别的用法已经成了惯用手法,即用序列来表示可选的值。例如,我们可能拥有一个Part 结构,用于记录小汽车的零件的详细资料。这个结构可以记录这样的资料:零件名称、描述、重量、价格,以及其他详细资料。 备件通常都有序列号,我们用一个long 值表示。但有些零件,比如常用的螺丝钉,常常没有序列号,那么我们在螺丝钉的序列号字段里要放进什么内容?要处理这种情况,有这样一些选择:
用一个标记值,比如零,来指示“没有序列号”的情况。
这种方法是可行的,只要确实有标记值可用。尽管看起来不大可能有人把零用作零件的序列号,这并非是不可能的。而且,对于其他的值,比如温度值,在其类型的范围中的所有值都可能是合法的,因而没有标记值可用。
把序列号的类型从long 变成string。
串自己有内建的标记值,也就是空串,所以我们可以用空串来指示.“没有序列号”的情况。这也是可行的,但却会让大多数人感到不快:我们不应该为了得到一个标记值,而把某种事物自然的数据类型变成string
增加一个指示符来指示序列号的内容是否有效.
struct Part {
string name;
string description;
// ...
bool serialIsValid; // true if part has serial number
long serialNumber;
};
对于大多数人而言,这也让人讨厌,而且最终肯定会让你遇到麻烦:迟早会有程序员忘记在使用序列号之前检查它是否有效,从而带来灾难性的后果。
用序列来建立可选字段
这种技术使用了下面的惯用手法:
sequence<long> SerialOpt;
struct Part {
string name;
string description;
// ...
SerialOpt serialNumber; // optional: zero or one element
};
按照惯例, Opt 后缀表示这个序列是用来建立可选值的。如果序列是空的,值显然就不在那里;如果它含有一个元素,这个元素就是那个值。这种方案明显的缺点是,有人可能会把不止一个元素放入序列。为可选值增加一个专用的Slice 成分可以纠正这个问题。但可选值并非那么常用,不值得为它增加一种专门的语言特性(我们将看到,你还可以用类层次来建立可选字段)。
词典
词典是从键类型到值类型的映射。例如:
struct Employee {
long number;
string firstName;
string lastName;
};
dictionary<long, Employee> EmployeeMap;
这个定义创建一种叫作EmployeeMap 的词典,把雇员号映射到含有雇员详细资料的结构。你可以自行决定键类型(在这个例子中是long 类型的雇员号)是否是值类型(在这个例子中是Employee 结构)的一部分——就Slice 而言,你无需让键成为值的一部分。
词典可用于实现稀疏数组,或是具有非整数键类型的任何用于查找的数据结构。尽管含有键-值对的结构的序列可用于创建同样的事物,词典要更为适宜:
词典明确地表达了设计者的意图,也就是,提供从值的域(domain)到值的范围(range)的映射(含有键-值对的结构的序列没有如此明确地表达同样的意图)。
在编程语言一级,序列被实现成向量(也可能是列表),也就是说,序列不大适用于内容稀疏的域,而且要定位具有特定值的元素,需要进行线性查找。而词典被实现成支持高效查找的数据结构(通常是哈希表或红黑树),其平均查找时间是O(log n),或者更好。词典的键类型无需为整型。例如,我们可以用下面的定义来翻译一周每一天的名称:
dictionary<string, string> WeekdaysEnglishToGerman;
服务器实现可以用键-值对Monday–Montag、Tuesday–Dienstag,等等,对这个映射表进行初始化。
词典的值类型可以是用户定义的任何类型。但词典的键类型只能是以下类型之一:
整型(byte、short、int、long、bool,以及枚举类型)
string
元素类型为整型或string 的序列
数据成员的类型只有整型或string 的结构
复杂的嵌套类型,比如嵌套的结构或词典,以及浮点类型(float和double),不能用作键类型。之所以不允许使用复杂的嵌套类型,是因为这会使词典的语言映射复杂化;不允许使用浮点类型,是因为浮点值在跨越机器界线时,其表示会发生变化,有可能导致成问题的相等语义。
常量定义与直接量
Slice 允许你定义常量。常量定义的类型必须是以下类型中的一种:
整型(bool、byte、short、int、long,或枚举类型)
float 或double
string
下面有一些例子:
const bool AppendByDefault = true;
const byte LowerNibble = 0x0f;
const string Advice = "Don't Panic!";
const short TheAnswer = 42;
const double PI = 3.1416;
enum Fruit { Apple, Pear, Orange };
const Fruit FavoriteFruit = Pear;
直接量(literals)的语法与C++ 和Java 的一样(有一些小的例外):
布尔常量只能用关键字false和true初始化(你不能用0和1来表示false和true)。
和C++ 一样,你可以用十进制、八进制,或十六进制方式来指定整数直接量。例如:
const byte TheAnswer = 42; const byte TheAnswerInOctal = 052; const byte TheAnswerInHex = 0x2A; // or 0x2a
![]() | 注意 |
|---|---|
| 如果你把byte 解释成数字、而不是位模式,你在不同的语言里可能会得到不同的结果。例如,在C++ 里, byte 映射到char,取决于目标平台, char 可能是有符号的,也可能是无符号的。 |
![]() | 注意 |
|---|---|
用于指示长常量和无符号常量的后缀(C++ 使用的l、L、u、U)是非法的:
const long Wrong = 0u; // Syntax error const long WrongToo = 1000000L; // Syntax error |
整数直接量的值必须落在其常量类型的范围内,否则编译器就会发出诊断消息。
浮点直接量使用的是C++语法,除了你不能用l或L后缀来表示扩展的浮点常量;但是, f 和F 是合法的(但会被忽略)。下面是一些例子:
const float P1 = -3.14f; // Integer & fraction, with suffix const float P2 = +3.1e-3; // Integer, fraction, and exponent const float P3 = .1; // Fraction part only const float P4 = 1.; // Integer part only const float P5 = .9E5; // Fraction part and exponent const float P6 = 5e2; // Integer part and exponent
浮点直接量必须落在其常量类型(float 或double)的范围内;否则编译器会发出诊断警告。
串直接量支持与C++ 相同的转义序列。下面是一些例子:
const string AnOrdinaryString = "Hello World!"; const string DoubleQuote = "\""; const string TwoSingleQuotes = "'\'"; // ' and \' are OK const string Newline = "\n"; const string CarriageReturn = "\r"; const string HorizontalTab = "\t"; const string VerticalTab = "\v"; const string FormFeed = "\f"; const string Alert = "\a"; const string Backspace = "\b"; const string QuestionMark = "\?"; const string Backslash = "\\"; 70 Slice 语言 const string OctalEscape = "\007"; // Same as \a const string HexEscape = "\x07"; // Ditto const string UniversalCharName = "\u03A9"; // Greek Omega 和在 C++ 里一样,相邻的串直接量会连接起来: const string MSG1 = "Hello World!"; const string MSG2 = "Hello" " " "World!"; // Same message /* * Escape sequences are processed before concatenation, * so the string below contains two characters, * '\xa' and 'c'. */ const string S = "\xa" "c";
![]() | 注意 |
|---|---|
Slice 没有null 串的概念
const string nullString = 0; // Illegal!null 串在Slice 里根本不存在,因此,在Ice 平台的任何地方它都不能用作合法的串值。这一决定的原因是, null 串在许多编程语言里不存在 |
首先说明一下:本教程比较侧重于代码的实践,最好接合"Cross-Platform GUI Programming with wxWidgets"来读(网上有电子书,自己搜索一下),不过我写的还是自认为写的比较详细,单独阅读问题也应该不是很大.wxWidgets用户手册是必备的,伟大的孙波同志正在翻译中文版 ,希望他可以坚持到底!大家可以在邮件列表中找到不完全翻译版,名字叫"wxWidgets 2.6.1: 一个可移植的C++和Python图形用户接口工具包(GUI toolkit)".
另外请高手们多多指教,更期待有志同道合者可以一起来完成这个教程.ok,我们开始吧!
wxWidgets库的,是一个结构类似于MFC的开源的C++库,它的特点是跨平台:一次编程便可以在从XP到Linux,苹果机,WinCE等等操作系统上重新编译,运行.
安装和编译wxWidgets库我就不说了,大家可以参考Code Project上的"Introduction to wxWidgets"(作者:Priyank Bolia,期待哪位英文好的高手翻译一下)(我放在附件中了).
![]() | 注意:"Introduction to wxWidgets"中路径设置的问题 |
|---|---|
环境变量"WXWIN"应该设置为"c:\???"而不是"c:\???\",然后包含目录的时候格式为"$(WXWIN)\include\"而不是"$(WXWIN)include\".我也提供了一个VS2003修正版的"HelloWorld"的解决方案(本文的源代码只需直接复制到hello.cpp中便可编译运行). |
照着这篇文章安装,编译,设置环境变量之后,就可以开始我们的程序了.我的IDE是VS2003(Microsoft Visual Studio .NET 2003),wxWidgets库的版本是2.6.1.
先了解一下"HelloWorld"的源代码
过程 3.1. 程序的调用顺序
用宏绑定main函数
MyApp::OnInit()
MyFrame初始化:设置界面布局,绑定按钮到事件表
通知计算机显示MyFrame
被按下按钮-->回调事件表中的相应函数
#include "wx/wx.h"
#include "mainico.xpm"//包含图标文件,格式为XPM,我在源代码之后有该格式的说明
//新建一个窗体
class MyFrame: public wxFrame
{
public:
MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
//事件处理函数
void OnQuit(wxCommandEvent& event);
void OnAbout(wxCommandEvent& event);
//这个呢,写过MFC的朋友一定眼熟,就是消息映射了,wxWidgets叫事件表
DECLARE_EVENT_TABLE()
};
enum
{
ID_Quit,
ID_About,
};
//这里是事件的映射,当触发什么事件(如MENU_ABOUT)时候,调用什么函数(MyApp::OnAbout)
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU(ID_Quit, MyFrame::OnQuit)
EVT_MENU(ID_About, MyFrame::OnAbout)
END_EVENT_TABLE()
//初始化程序
class MyApp: public wxApp
{
virtual bool OnInit();
};
//把main函数放在这里
IMPLEMENT_APP(MyApp)
//程序初始化的代码
bool MyApp::OnInit()
{
/*
wxT("Hello World")中"wxT"是一个宏,也可以写作"_T()",用于在编译时改变文字的编码.
比如,一个程序可以分别编译为ASCLL码版(只支持英文)和UTF-8版(支持多国语言)
*/
MyFrame *frame = new MyFrame( wxT("Hello World"), wxPoint(299,250), wxSize(340,340) );
frame->Show(TRUE);
SetTopWindow(frame);
return TRUE;
}
MyFrame::MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
: wxFrame((wxFrame *)NULL, -1, title, pos, size)
//wxFrame中的初始化参赛以及其含义可以参见Gxl117翻译的《wxWindows 2用C++编写跨平台程序》(本文的附件中有)
{
SetIcon(wxIcon(mainico_xpm));//设置图标(mainico_xpm变量在mainico.xpm被声明)
wxMenu *menuFile = new wxMenu;//菜单条中一个条目
menuFile->Append( ID_About, wxT("&A菜单项--弹出窗口") );
menuFile->AppendSeparator();
menuFile->Append( ID_Quit, wxT("&Q退出") );
wxMenuBar *menuBar = new wxMenuBar;//菜单条
menuBar->Append( menuFile, wxT("&M菜单条中的条目" ));
SetMenuBar( menuBar );
CreateStatusBar();
SetStatusText(wxT( "欢迎使用wxWidgets!") );//状态栏
}
void MyFrame::OnQuit(wxCommandEvent& WXUNUSED(event))
{
Close(TRUE);
}
void MyFrame::OnAbout(wxCommandEvent& WXUNUSED(event))
{
wxMessageBox(wxT("消息框的文字:地球人,你好!"),
wxT("消息框的标题"),
wxOK | wxICON_MASK ,
this);
}
![]() | XPM图像格式说明 |
|---|---|
把其他图片格式转换为XPM格式可以用一个免费软件XnView(有汉化版),另外图片该程序的图标大小应该为16*16像素。 用文本编辑器打开XPM文件后可以看到内容如下:
/* XPM */
static char *mainico_xpm[] = {
/* width height num_colors chars_per_pixel */
" 16 16 256 2",
/* colors */
"`` c #000000",
"`. c #173b03",
"`# c #62ce19",
...................等等
|
所谓事件驱动,简单地说就是你点什么按钮(即产生什么事件),电脑执行什么操作(即调用什么函数).当然事件不仅限于用户的操作.
weWidgets的中所有可以处理事件的类都继承自wxEvtHandler,其中包含frames, buttons, menus, even documents,所有的窗体类(即从wxWindow继承的类)和程序类(application class).
这些类可以有一个事件表,用来绑定事件和被调用的函数(handler functions).
过程 3.2. 建立一个静态事件表(即编译时生成的事件表)的操作步骤
建立一个新类(直接或间接从wxEvtHandler继承)
为每个要处理的事件声明被调用的函数
在被处理的事件所在的类的声明中加入宏DECLARE_EVENT_TABLE
在宏BEGIN_EVENT_TABLE... END_EVENT_TABLE(就是事件表)中将函数与枚举的数字绑定(因为产生该类型的事件的按钮不唯一,要用枚举数来区分);有些事件不必与枚举数绑定,因为产生该类型的事件的对象可以确定(比如就是this).
在事件中指定被绑定的数字,wxWidgets会将其映射到对应的函数,并调用函数
所有在事件表中被绑定的函数有相似的形式:返回值都是void,不是virtual函数,参数为wxCommandEvent类型
一般说来,事件会从子类的事件表开始查询调用,如果没有找到对应的调用,再依次查询其各个父类的事件表.但以下的事件例外,不会调用父类事件表wxActivate Event, wxCloseEvent, wxEraseEvent, wxFocusEvent, wxKeyEvent, wxIdleEvent, wxInitDialogEvent, wxJoystickEvent, wxMenuEvent, wxMouseEvent, wxMoveEvent, wxPaintEvent, wxQueryLayoutInfoEvent, wxSizeEvent, wxScrollWinEvent, and wxSysColourChangedEvent(都是些啥玩意?以后慢慢学)
一个事件只能被处理一次,随即从消息队列(就是所有等待处理的事件)中被删除,也就是说,子类的事件表中处理过的事件,父类事件表中不会再次处理.如果想让该事件消息继续保留在消息队列中,可以使用wxCommandEven中的skip()函数.
skip实际用途举例:限制用户输入为数字,而具体的处理交付给父类去做.
例 3.3. helloWorld改版----wxCommandEven::skip()的示例
#include "wx/wx.h"
#include "mainico.xpm"
class MyFrame: public wxFrame
{
public:
MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
void OnQuit(wxCommandEvent& event);
void OnAbout(wxCommandEvent& event);
DECLARE_EVENT_TABLE()
};
//新增代码
class MySkip:public MyFrame
{
public:
MySkip(const wxString& title, const wxPoint& pos, const wxSize& size):MyFrame(title,pos,size),i(1)
{};
void OnAbout2(wxCommandEvent& event)
{
i*=-1;
if (i>0) {
wxMessageBox(wxT("i>0,not SKip(不跳过)"));
}
else
{
wxMessageBox(wxT("i<0,will SKip(将跳过)"));
event.Skip();
}
}
private:
int i;
DECLARE_EVENT_TABLE()
};
enum
{
ID_Quit,
ID_About,
};
//这里是MyFrame事件的映射
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU(ID_Quit, MyFrame::OnQuit)
EVT_MENU(ID_About, MyFrame::OnAbout)
END_EVENT_TABLE()
//这里是MySkip事件的映射
BEGIN_EVENT_TABLE(MySkip, MyFrame)
EVT_MENU(ID_About, MySkip::OnAbout2)
END_EVENT_TABLE()
class MyApp: public wxApp
{
virtual bool OnInit();
};
IMPLEMENT_APP(MyApp)
bool MyApp::OnInit()
{
MySkip *frame = new MySkip( wxT("Hello World"), wxPoint(299,250), wxSize(340,340) );
frame->Show(TRUE);
SetTopWindow(frame);
return TRUE;
}
MyFrame::MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
: wxFrame((wxFrame *)NULL, -1, title, pos, size)
{
SetIcon(wxIcon(mainico_xpm));
wxMenu *menuFile = new wxMenu;
menuFile->Append( ID_About, wxT("&wxCommandEven::skip()函数演示") );
menuFile->AppendSeparator();
menuFile->Append( ID_Quit, wxT("&Q退出") );
wxMenuBar *menuBar = new wxMenuBar;
menuBar->Append( menuFile, wxT("&M菜单条中的条目" ));
SetMenuBar( menuBar );
CreateStatusBar();
SetStatusText(wxT( "欢迎使用wxWidgets!") );
}
void MyFrame::OnQuit(wxCommandEvent& WXUNUSED(event))
{
Close(TRUE);
}
void MyFrame::OnAbout(wxCommandEvent& WXUNUSED(event))
{
wxMessageBox(wxT("消息框的文字:地球人,你好!"),
wxT("消息框的标题"),
wxOK | wxICON_MASK ,
this);
}
你除了可以用窗体类的子类来处理事件,也可以用继承wxEvtHandler的类来处理事件。给wxEvtHandler的子类定义一个事件表,并用wxWindow::PushEventHandler函数把这个对象加入该窗体的事件处理流程(栈堆)中去,该对象的事件表会在窗体的事件表之前被处理。当然PushEventHandler也可以用于添加窗体类的子类到该对象的事件表中去.
另外,用wxWindow::PopEventHandler会弹出(就是删除)事件表栈堆顶端的对象,如果执行成功则返回true。
而wxWindow::GetEventHandler可以返回事件表栈堆顶端的对象(如果你没有用PushEventHandler,那么GetEventHandler一般返回窗体自身)。
用独立的事件处理对象(不继承自窗体类)能够把不同的窗体中共同的事件处理动作分离出来,以便重复利用(复用).
有了以上知识我们可以临时或永久的改变一个窗体的事件处理方式.
PushEventHandler实际用途举例:一个学习《概率论》的程序,主窗体中在讲述了一个概念之后,弹出一个对话框让用户作练习,这时你可以用PushEventHandler临时把鼠标的事件处理权交给该对话框.完成之后用PopEventHandler返回.
例 3.4. helloWorld改版----wxWindow::PopEventHandler的示例
#include "wx/wx.h"
#include "mainico.xpm"
enum
{
ID_Quit,
ID_About,
ID_Add,
ID_Move
};
//新增代码
class MyEvt:public wxEvtHandler{
public:
MyEvt();
void OnClick(wxCommandEvent& e);
~MyEvt();
private:
int i;
static int number;
int myNumber;
DECLARE_EVENT_TABLE()
};
int MyEvt::number=0;
BEGIN_EVENT_TABLE(MyEvt,wxEvtHandler)
EVT_MENU(ID_About,MyEvt::OnClick)
END_EVENT_TABLE()
MyEvt::MyEvt(){
i=1;
number+=1;
myNumber=number;
}
MyEvt::~MyEvt()
{
number-=1;
}
void MyEvt::OnClick(wxCommandEvent& e)
{
wxString show=wxT("第");
show<<myNumber;
if (i>0)
{ show+=wxT("个事件表 MyEvt::OnClick被调用!");
wxMessageBox(show,
wxT("张沈鹏的演示程序"),
wxOK );
}
else
{
show+=wxT("个事件表 MyEvt::OnClick被调用, 然后wxCommandEvent::Skip!");
wxMessageBox(show,
wxT("张沈鹏的演示程序"),
wxOK );
e.Skip();
}
i*=-1;
}
class MyFrame:public wxFrame
{
public:
MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
void OnQuit(wxCommandEvent& event);
void OnAbout(wxCommandEvent& event);
void OnAdd(wxCommandEvent& event);
void OnMove(wxCommandEvent& event);
private:
wxEvtHandler *evtPro;
DECLARE_EVENT_TABLE()
};
BEGIN_EVENT_TABLE(MyFrame, wxFrame)
EVT_MENU(ID_Quit, MyFrame::OnQuit)
EVT_MENU(ID_About, MyFrame::OnAbout)
EVT_MENU(ID_Add,MyFrame::OnAdd)
EVT_MENU(ID_Move,MyFrame::OnMove)
END_EVENT_TABLE()
class MyApp: public wxApp
{
virtual bool OnInit();
};
IMPLEMENT_APP(MyApp)
bool MyApp::OnInit()
{
MyFrame *frame = new MyFrame( wxT("Hello World"), wxPoint(299,250), wxSize(340,340) );
frame->Show(TRUE);
SetTopWindow(frame);
return TRUE;
}
MyFrame::MyFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
: wxFrame((wxFrame *)NULL, -1, title, pos, size)
{
SetIcon(wxIcon(mainico_xpm));
wxMenu *menuFile = new wxMenu;
menuFile->Append( ID_About, wxT("&A当前的事件表--弹出窗口") );
menuFile->Append(ID_Add,_T("加载事件表(你可以尝试加载多次事件表然后看Skip的结果)"));
menuFile->Append(ID_Move,_T("弹出事件表(就是删除当前的事件表)"));
menuFile->AppendSeparator();
menuFile->Append( ID_Quit, wxT("&Q退出") );
wxMenuBar *menuBar = new wxMenuBar;
menuBar->Append( menuFile, wxT("&M菜单条中的条目" ));
SetMenuBar( menuBar );
CreateStatusBar();
SetStatusText(wxT( "欢迎使用wxWidgets!") );
}
void MyFrame::OnQuit(wxCommandEvent& WXUNUSED(event))
{
Close(TRUE);
}
void MyFrame::OnAbout(wxCommandEvent& WXUNUSED(event))
{
wxMessageBox(wxT("MyFrame::OnAbout:地球人,你好!"),
wxT("消息框的标题"),
wxOK | wxICON_MASK ,
this);
}
void MyFrame::OnAdd(wxCommandEvent& event)
{
evtPro=new MyEvt;
this->PushEventHandler(evtPro);
}
void MyFrame::OnMove(wxCommandEvent& event)
{
if (this->GetEventHandler()!=this)
{
evtPro=this->GetEventHandler();
this->PopEventHandler();
delete evtPro;
}
else
{
wxMessageBox(wxT("所有附加的事件表都已经被删除,当前的事件表为窗体本身自带!"),
wxT("消息框的标题"),
wxOK | wxICON_MASK ,
this);
}
}
一般说来,静态的事件表已经可以满足需要,但有时你也需要在运行时根据需要选择不同事件表.在某些不支持静态事件表的编程语言中(比如Python)也只有用动态事件表.
一个使用动态事件表的窗体类与使用静态的事件表类似,但不需要在类体中加入 DECLARE_EVENT_TABLE 宏.
例 3.5. 建立动态事件表
//MyFrame是一个窗体类 MyFrame *frame = new MyFrame(); frame->Connect( //窗体中的菜单项的ID ID_Exit, //注意事件标识宏不是EVT_MENU wxEVT_COMMAND_MENU_SELECTED, //wxCommandEventHandler宏可以调用类的非静态成员函数. wxCommandEventHandler(MyFrame::OnQuit) );
例 3.6. 断开动态事件表
//MyFrame是一个窗体类 MyFrame *frame = new MyFrame(); frame->Disconnect( ID_Exit, wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler(MyFrame::OnQuit) );
这一节我不给出源代码了(偷懒啦),那位写好源代码的可以发给我,添加到文章中来,谢谢!
事件表到此已经讲的差不多了,现在我们可以来探讨一下其中的细节问题.事件标示符,说白了就是前面源代码中的
enum
{
ID_Quit,
ID_About,
};
其用途是在事件表中链接事件与事件处理函数.
一个标识符可以被几个有着相同处理函数的事件所共用, 比如许多消息框都共用一个wxID_OK的标识符.这有一点类似于"www.google.com","www.google.com.cn","www.gooogle.com"这几个域名都指向了同一个网站.
wxWidgets中有几个默认的事件标识符,比如"wxID_ANY".wxWidgets会自动给wxID_ANY一个值,一般说来,你可以给不想去处理的事件绑定标识符wxID_ANY.当然,你也可以给wxID_ANY绑定一个函数,对其进行一些默认的处理.
框架类的基类wxFrame的第六个参数就是窗体风格,由于wxFrame是wxWindow的子类,所以wxWindow的窗体风格wxFrame也可以使用.
wxFrame((wxFrame *)NULL, -1, title, pos, size,风格) /* wxFrame的常用风格: wxSTAY_ON_TOP 始终在最上层 wxFRAME_FLOAT_ON_PARENT 始终在父框架的最上层 wxDEFAULT_FRAME_STYLE 默认的窗体风格等价于 wxMINIMIZE_BOX | wxMAXIMIZE_BOX | wxRESIZE_BORDER | wxSYSTEM_MENU | wxCAPTION | wxCLOSE_BOX | wxCLIP_CHILDREN wxWindow的常用风格: wxNO_BORDER 无边框窗体 wxSIMPLE_BORDER:有一条1象素的黑色边框 ....... 太多了用的时候可以查阅用户手册,我就不一一列举了:) */
几个风格之间一般可以用位或运算(就是"|")使用,比如:wxNO_BORDER|wxSTAY_ON_TOP ,就表示始终在最上层的无边框窗体.
这一节我也没有看懂,Who重新讲一下?
首先,我们明确一下什么是事件.前面程序中void OnQuit(wxCommandEvent& event);的参数就是一个事件类,而事件表中EVT_MENU(ID_About, MyFrame::OnAbout)的EVT_MENU是一个宏.这里,我们要学习如何自定义事件类和事件宏.
过程 3.3. 自定义事件类
继承一个适合的父类,声明运行时信息(dynamic type information)包含克隆函数(Clone function)(谁能告诉我这俩个是什么东西?).如果你想要让事件在窗体中传播,请直接或间接的以wxCommandEvent为基类.如果你想要一个事件可以被禁止(Veto),请以wxNotifyEvent为基类.
用typedef为事件的句柄函数(event handler function)重命名.
把你的事件类支持的每一个类型都写到如下的一个表中去,
BEGIN_DECLARE_EVENT_TYPES() DECLARE_EVENT_TABLE(name, integer) END_DECLARE_EVENT_TYPES()
定义一个事件表的宏
![]() | 禁止事件 |
|---|---|
一些事件是可以禁止的。当你禁止一个事件时,这个事件不会被进一步处理。下面一段代码演示了,当一个文本控件内的文本改变后如何禁止这个简单文本编辑器的关闭事件。这表示当在用户还没有保存改变后的文本内容时这个窗口不能被关闭。
void TextFrame::OnClose(wxCloseEvent& event)
{
bool destroy = true;
if ( event.CanVeto() )
{
if ( m_pTextCtrl->IsModified() )
{
wxMessageDialog *dlg =
new wxMessageDialog(this, "Text is changed!\nAre you sure you want to exit?",
"Text changed!!!", wxYES_NO | wxNO_DEFAULT);
int result = dlg->ShowModal();
if ( result == wxID_NO )
{
event.Veto();
destroy = false;
}
}
}
if ( destroy )
{
Destroy();
}
当CanVeto返回false时程序作什么呢?你将不能禁止这个事件你的程序将会退出。 |
客户区:可以放置子窗体区域,一般说来,就是除了菜单,工具条,状态栏的区域.
插入符(Caret):就是打字时那个一闪一闪的黑色竖线(我也不知道叫什么)
顶层窗体:比如wxFrame, wxDialog, wxPopup等等.只有顶层窗体可以在父窗体为NULL的情况下创建.
坐标:左上角为(0,0),单位为象素(px/pixel)
Window Variant:苹果机上的东西,不太清楚,好像是字体大小. 有以下的值 wxWINDOW_VARIANT_NORMAL (the default), wxWINDOW_ VARIANT_SMALL, wxWINDOW_VARIANT_MINI, or wxWINDOW_VARIANT_LARGE
窗体大小:当窗体大小改变时,有wxEVT_SIZE事件产生.如果窗体有子窗体,就需要重新摆放.
输入:一般来说,只有当前具有焦点的窗体可以响应键盘输入.当一个窗体获得焦点时将收到wxEVT_SET_FOCUS事件,失去焦点时,将收到wxEVT_KILL_FOCUS事件.
目录