第十二章 专业编程技巧 |
随着程序变得越来越大,我们会发现很多问题。这些问题或许在编制几千行代码时不会出现,但是当编到上万行或更多时并且程序由不同的程序员来编写,在运行时问题就出现了。 这就要求程序有经深思熟虑的结构和详细的注释。在编制大程序时主要要考虑两点: 1 怎样使程序容易维护 2 怎样使程序可重用(reuse) 当然,我们想使程序可以被任何一个人都看得懂,容易维护并重用。特别是对Palm OS来说,由于它的很多应用窗体都十分相似,采取这样的策略将带来很大的效率。虽然PC的处理器速度和内存都在飞速的发展,但是就Palm OS现在的情况来看,必须要聪明的使用处理器和内存。但也不能忽视可维护性,只有具备好的可维护性,才能使代码更容易被优化。 这一章中,我们将接触到很多专业的编程策略。在学习的过程中,我们还将根据这些策略建立一些可重复使用的模块,希望你能在这些编程策略中得到益处。 通过这一章的学习,可为你创建Palm OS应用程序打下一个坚实的基础。 类型保护变量和可移植性 可移植性是代码可重复使用的关键因素之一。可移植性是指代码可以做很少的修改就能被不同的编译器所编译;并且只有将界面和代码分离(因为界面一般都是基于系统的,而代码要工作在不同的平台上)才能提高可移植性。 下几章中我们将以计算器的例子来证明下面讲到的内容。计算器的用户界面和程序代码被分离开来,这样就可以很容易将计算器程序应用到Windows 或Macintosh平台上。 为使程序工作在不同的编译器和系统下,使用类型保护变量是主要途径之一。或许你还不知道什么是类型保护变量,但是在本书中我们一直在使用类型保护变量——至少是使用Palm OS版本的类型保护变量。类型保护变量就是指:不是使用标准C中的数据类型如int或char,而是使用命令typedef来自定义的数据类型。 z 为什么类型保护是如此重要呢?这是因为在标准C中,如int在CodeWarrior C编译器中是16位,而在其它的一些常用编译器中包括GCC,它的长度都是32位。如果你在很多地方都使用了数据类型int而又想从CodeWarrior转换到GCC,那么由于类型int的长度问题就会引起很多的bug,数据结构要改变,数据库记录的大小也要改变。情况严重的话,程序将根本就不能运行。 不仅仅是类型int可以造成可移植性的问题。还有一些其它的类型存在此类问题,例如数据类型char,在日本,char是16位,而不是一般所指的8位。因此,如果要使所编制的程序国际化,那就需要重新做大量的工作。 有关类型保护变量和Palm OS,我的意见是如果你在写基于Palm OS的用户界面代码,就要使用Palm OS的类型保护变量。在Palm OS以后的发展中,其开发人员会小心的处理这些类型保护变量。这样你会发现只要使用了正确的类型保护变量,就可以很容易的将现有的版本升级到新版本。 如果你写的代码也将工作在其它的操作系统平台上,也应该使用自己定义的数据类型。当然,也不能再直接调用Palm OS的函数,利用自己的定义的函数名调用这些函数,或者在头文件中使用#define语句修改函数名来调用这些函数。 使用匈牙利符号(Hungarian Notation) 在看一个大块的代码时,很容易将在函数头部定义的变量的数据类型忘掉。使用匈牙利符号是解决这个问题的一个好方法,因为它可以使别人很容易看懂。在匈牙利符号中,它使用了一些字母在变量的开头,可以使你能记住变量的数据类型。例如,“c”代表char,“p”代表指针,所以cpBuffer代表指向一个缓冲区的char*变量。 匈牙利符号有很多样式。在表12-1中,是我的两个版本,我发现它们可以很好的应用在C、C++和Mac、Windows、Unix中,当然还包括Palm OS。在接下来的部分中,我将全部使用这些符号来定义变量。 字母 数据类型 描述和例子 a [] 数组符号:Char caBuffer[20] b Byte 8字节的数字变量:Byte bFlags c Char 字节:char c d Dword 32字节的数字变量:Dword dCounter e enum 枚举类型的变量:spEvent->eType f float 32字节浮点数:float fResult g global 全局性变量,即可以在所有的模块中使 用:app_t gsApp h handle 在一些系统中是void*,其它的为Int:VoidHand hRecord i int 整型变量:Int iCounter j k const 在C++中为常量定义或返回值定义 l long 长整型:long Ivalue m member 结构或类的成员变量:char mcaBuffer[20] n double 64位的浮点数:double nBigNum o Boolean 布尔值,真或假:Boolean oFirstPass p pointer 指针:void* vpPointer q r raw 二进制数据类(C++) s struct 结构或类:sEvent t text 文本型类(C++) u unsigned 无符号数:unsigned bong ulNumber v void 空数据类型:void* vpPointer w Word 字,一般为16字节:Word wNumber x y z 可重复使用的主模块 我们将重新从头开始,虽然有些烦但是你将有很大的收获。第一步我们先创建一个象在第二章中的Hello程序的例程。但是我们的新程序将更容易被重用并扩展至更大的程序。学习如何创建一个这样的主程序框架并不是什么困难的事。 首先打开CodeWarrior IDE创建一个叫计算器(Caculator)的程序。将源文件和资源文件夹中的文件全部删除,并清除Src文件夹中的所有文件。 从构造器中创建一个新的资源工程,将其保存为Calculator.rsrc。按下CTRL-K新建一个窗体。打开窗体并放置一个按钮到窗体的中间。使用这个按钮将可以测试到我们的程序是不是在运行。然后我们将为计算器创建一个真正的用户界面。程序的界面如图12-1所示: 我们也添加一个叫做LowROMVersionError的警告。当在我们使用的 Palm设备版本太老不能支持程序中使用的函数时,就弹出此警告框,如图-12所示: 现在向工程中添加一些源代码。 1. 打开CodeWarrior IDE并选中Project | Create New Group; 2. 将其命名为AppIncludes。这里面将放头文件。工程将缺省的创建一个叫Caculator_res.h的头文件放到文件夹中; 3. 将Caculator_res.h添加到这个新组中。 main.c模块 现在创建一个叫main.c的新文件,将其放在AppSource组中。这个模块包含着应用程序及事件循环(event loop)的入口以便能重复使用。详细内容如下: // // main.c // Main entry point and event loop. // Copyright (c) 1999, Robert Mykland. All rights reserved. // 注释程序十个很好的习惯,实际上这也是程序可维护性的关键。作为一个专业的程序员,如果写了大量的代码但是没有注释,那么这个程序员的水平就不会再提高并且会变得筋疲力尽。水平不能提高是因为没人能看懂他的程序而不能提出程序的缺陷;变得筋疲力尽是因为剩下的时间他不得不都用在这段烦人的程序上。记住,要注释程序,不要搞成这样。 有时我觉得很幸运,因为在我刚学编程时主要用的是汇编语言。如果不注释程序的话,就不得不花大量的时间来搞清在几周前到底写了些什么东西。使用C也是这样,只是程度有所不同。如果和其它12个人一起编制一个几十万行的代码,就十分有必要将当时是怎么想的以注释的形式写到程序里面。 过去我常常想不起在注释中写些什么内容,现在我基本有了一个大体的格式。例如:在每个模块的开头我都会写如上所示的注释内容,有些人喜欢将版本号也写到里面。把将版本号写到源程序控制系统(source control system)中是必要的,不然,我认为写入版本号没有什么用处。 注意: 什么是源程序控制系统(source control system)呢?这是一个用来存储和返回程序不同的版本号的工具。当发现一个大的bug时,就可以将能正常运行的程序和崩溃的程序相对照,看到底做了哪些改变。你还可以将代码分为不同的版本作不同的用途。如果你和其它的程序员共同编制一个大的程序,那么源程序控制系统将保证你和他们对程序的修改保持同步而防止了程序不统一。总起来说,对于专业程序员来说,源程序控制系统是个很重要的工具。 // // Includes // // #include "app.h" // The definitions for this application 你或许会问:Pilot.h跑到哪里去了?头文件app.h是我们将要自定义的文件,其中将包括我们程序中所用到的所有函数的定义,在这里面是以Pilot.h为头文件的。从某种意义来讲,这是一个自定义的Pilot.h,里面增添了我们自己需要的内容。我想甚至对很大的程序来讲,这也是一个很好的方法。如果你的头文件很大,由此造成了在编译时要花费大量的时间,那么就可以先让CodeWarrior 预编译这个文件,这样会使编译速度大大加快。 /// // Global Prototypes // /// DWord PilotMain( Word, Ptr, Word ); // Main entry point. Boolean processEvent( Long ); // Processes the next event. 这些是这个模块中的函数定义的原型。我们一直在使用函数原型,但是它到底有什么作用和好处呢?函数原型可以增强函数的可维护性。当定义了函数原型后,就可以保证不会将不匹配的参数传递到函数中。如果发生此类错误的话,将会造成一些很难理解的错误,并且也不容易找到这些错误。不过,在C中是允许不使用函数原型而调用函数的。为保证这些函数的正确使用,我们将在头文件app.h中定义另一个版本。本模块头部的函数原型定义就可以保证参数的匹配和定义。 / // Local Variables // / // The event handler list EVENT_HANDLER_LIST 在这里定义了局部变量。这个变量乍看起来像是个语法错误。它定义在app.h中。在后面我们将详细的讲解这个变量。它定义了程序中窗体的事件处理函数。实际上,这个#define语句定义了一个函数,我们还可以用很多其它的普通的方法来定义这个事件处理函数。我想在这里应该用大写字母或其它醒目的格式来代表它,这样可以让别人(还有自己在一段时间后看代码时)能更清楚的看出这个头文件中的定义,以便弄懂它到底有什么作用。 // // Global Functions // // //---------------------------------------------------------------------------- DWord PilotMain( //---------------------------------------------------------------------------- // The main entry point for this application. // Always returns zero. //---------------------------------------------------------------------------- Word wCmd, // The launch code Ptr, // The launch parameter block Word ) // The launch flags //---------------------------------------------------------------------------- { 这是我经常使用的注释形式之一。我觉得这样做很是不错,因为它很清晰,很容易在一个很长的函数模块中找到这个函数的头部。首先写出了函数的内容以及其返回值,后面注释了各个变量和参数的含义。有些人还喜欢将版本号加入到里面,但是我不想这样做,因为这样做并没有什么实际意义。 DWord dROMVersion; // Get the ROM version dROMVersion = 0; FtrGet( sysFtrCreator, sysFtrNumROMVersion, &dROMVersion ); // Alert and bail if the ROM version is too low if( dROMVersion < ROM_VERSION_MIN ) { FrmAlert( LowROMVersionErrorAlert ); // Palm OS 1.0 will continuously re-launch this app unless we switch // to another safe one if( dROMVersion < ROM_VERSION_2 ) { AppLaunchWithCommand( sysFileCDefaultApp, sysAppLaunchCmdNormalLaunch, NULL ); } return( 0 ); } 在以前也看到过这几行代码,它的作用就是可以防止用户的代码在不支持所用函数定义的Palm设备上运行。在我们的caculator例子中,由于使用的浮点库(floating-point libraries)只存在于Palm OS 2.0以及更高的版本,所以我们将常量ROM_VERSION_MIN定义为ROM_VERSION_2。这样只要我们使用不同的app.h就可以使main.c工作在不同的平台上,将各个模块连接在一起的“枢纽”就写在app.h里面。 // If this is not a normal launch, don't launch if( wCmd != sysAppLaunchCmdNormalLaunch ) return( 0 ); // Initialize all parts of the application appInit(); 上面的这些代码看起来也很熟悉吧。首先确定是正常运行后,调用函数appInit()初始化程序,它要把应用程序中所有待运行的数据初始化。例如:在Contacts程序中,我们使用appInit()将数据库打开,在下面的具体定义中还有详细的论述。 // Go to the starting form FrmGotoForm( StartForm ); 这里引发了事件来装载StartForm。但我们的并没有一个叫StartForm的窗体,它在app.h中被定义为CalcForm。这就是我们使main.c可重用的另一个技巧。当我们需要改变开始窗体时,我们只需改变app.h,而不需在main.c中做大量的改动。 // Wait indefinitely for events ErrTry { while( true ) processEvent( -1 ); } // Stop the application ErrCatch( lError ) { } ErrEndCatch // Clean up before exit appStop(); // We're done return( 0 ); } 这段代码和我们前面接触到的事件循环代码基本相同,只不过循环中的函数调用是在一个单独的函数processEvent()里面。这样我们就可以控制事件在程序的其它部分是怎样进行的,在后面的章节中,你会发现它是如何做到这样的。 在ErrCatch()后,我们调用了appStop()。象appInit()一样,它是定义于app.h连接不同程序模块的一个纽带,在退出程序前清除程序参数。正如你看到的,甚至正常的程序也在这个catch模块中停止,所以appStop()应该在模块的出口处调用。 下面是我们的事件处理函数: //---------------------------------------------------------------------------- Boolean processEvent( //---------------------------------------------------------------------------- // Waits for and processes the next event. // Returns false if the queue is empty. //---------------------------------------------------------------------------- Long lTimeout ) // Time to wait in 100ths of a second, -1 = forever //---------------------------------------------------------------------------- { 注意引入事件中的时间控制参数,这在控制动画制作和声音的程序中很有用。由于Palm OS是一个单任务操作系统,为使程序的效果更好,我们就需要控制程序的运行时间。你也许想问为什么PilotMain()没有返回值而processEvent()却返回了一个布尔量。在以后的程序中(如在动画制作中),我们或许会想知道事件是怎样运行的以便于使动画更加平滑。这时我们就会调用processEvent()来处理或刷新其它的函数。 EventType sEvent; // Our event Word wError; // The error word for the menu event handler // Get the next event EvtGetEvent( &sEvent, lTimeout ); 这里定义了一些熟悉的变量,后面调用了函数EvtGetEvent()。注意,这个函数将我们的时间参数传递给了系统。 // If it's a stop event, exit if( sEvent.eType == appStopEvent ) { // Exit ErrThrow( 0 ); } 停止事件处理和以前相比有了些变化。它不只是向main()返回一个值,而是向mian.c中的ErrCatch()模块回馈了函数ErrThrow()。为什么我们不能象从前那样做了呢?如果我们在程序的其它地方调用了函数processEvent()并收到一个appStopEvent,它就不是向main()返回值,而是向其它的地方返回了一个值。所以我们就需要一个机制,不论在哪里收到了appStopEvent,错误处理器都会象C中那样安全。 // If it's a nil event, return queue empty if( sEvent.eType == nilEvent ) return( false ); 这是循环中一个新的检查措施。当事件队列为空时,Palm OS就会发出一个nilEvent。由于以前的程序中,我们让Palm OS一直的等待,我们就从来没收到过此事件。现在收到nilEvent后,我们就知道了事件队列是空的。在做一些耗时的工作时,如果调用processEvent来周期性的刷新事件队列,它就会派上用场。因此让它返回一个具体值是很重要的。 // Handle system events if( SysHandleEvent( &sEvent ) ) return( true ); // Handle menu events if( MenuHandleEvent( NULL, &sEvent, &wError ) ) return( true ); 以上是一些常用函数,我们用它们处理系统事件和菜单事件。 // Load a form if( sEvent.eType == frmLoadEvent ) { Word wFormID; // The form ID FormPtr spForm; // Points to the form // Get the ID wFormID = sEvent.data.frmLoad.formID; // Initialize the form spForm = FrmInitForm( wFormID ); // Establish the event handler FrmSetEventHandler( spForm, getEventHandler( wFormID ) ); // Point events to our form FrmSetActiveForm( spForm ); 这里是普通的窗体加载代码。你看起来肯定很熟悉吧,首先从事件结构(event structure)中获得窗体的ID,然后初始化并绘制窗体。然而要注意函数FrmSetEventHandler()中的getEventHandler()。这个函数定义在app.h中,可以根据支持不同窗体的模块,以相应的事件句柄获得相匹配的窗体ID,这就允许我们以相同的方式处理main.c中所有的窗体。因此这段代码是我们不必知道程序中到底有什么样的窗体,就可以重用这些代码来进行处理。 // Handle form events FrmDispatchEvent( &sEvent ); // We're done return( true ); } 这是processEvent()的最后一点。我们将余下的事件分配到活动的窗体中。如果的确还有事件没有处理的话,函数将返回true。 Main.h模块 头文件main.h中有支持main.c的定义,并且有为了自定义main.c而在app.h中添加的函数定义。创建一个main.h文件放入源文件中,将之加入Calculator工程并保存在AppIncludes中。下面是它的详细代码: #ifndef MAIN_H #define MAIN_H // // main.h // Definitions for the main entry point. // Copyright (c) 1999, Robert Mykland. All rights reserved. // /// // Global Prototypes // /// DWord PilotMain( Word, Ptr, Word ); // Main entry point. Boolean processEvent( Long ); // Processes the next event. /// // Constants // /// // The different versions of PalmOS // Pilot 1000 and Pilot 5000 #define ROM_VERSION_1 0x01003001 // PalmPilot and PalmPilot Professional #define ROM_VERSION_2 0x02003000 // Palm III, IIIx, and V #define ROM_VERSION_3 0x03003000 #endif // MAIN_H 在程序的开始是两个预处理指令:#ifndef MAIN_H和#define MAIN_H,相应的在程序的结尾,也有一个预处理指令:#endif。如果MAIN_H没有定义的话,#ifndef和#endif这对预处理指令就会跳过中间所有的代码,MAIN_H将在它们的内部开始定义。预处理指令的作用就是把编译器已经编译过的内容忽略以防重复编译。 在一些比较大的程序中,为方便起见,在同一个文件中头文件不止被引用一次。这时候就可以将代码分开编译,而不必担心文件中函数到底有没有定义。这些预处理指令就可以避免让编译器花费一些不必要的时间来进行一些重复的编译工作。 在标准的头模块的内容下面是main.c中的函数原型,由于它们和源文件中的函数定义完全相同,所以如果两者间有一点点不同,编译器就会给出错误警告。 在函数的最后是对Palm OS的版本号的常量定义。为了使将来的程序少有版本不符的情况,将版本号在头文件中进行定义是有必要的(如果系统中没有定义的话)。这是因为代表版本号的十六进制数很难引用和理解,如果不定义的话,在将来会造成很多麻烦。 Fcalc.c模块 在fcalc.c中只有一个事件处理函数。创建一个叫fcalc.c的新文件并保存在工程的Src文件夹中。下面是fcalc.c的详细代码。在我们熟悉的头文件声明的代码后,是事件处理函数的函数原型。 // // fcalc.c // Code for the "calc" form. // Copyright (c) 1999, Robert Mykland. All rights reserved. // // // Includes // // #include "app.h" // The definitions for this application /// // Global Prototypes // /// Boolean calcFormEventHandler( EventPtr spEvent ); // // Global Functions // // //---------------------------------------------------------------------------- Boolean calcFormEventHandler( //---------------------------------------------------------------------------- // Handles events for this form. // Returns true if it fully handled the event. //---------------------------------------------------------------------------- EventPtr spEvent ) //---------------------------------------------------------------------------- { // Handle the event switch( spEvent->eType ) { // A control was selected case ctlSelectEvent: // Sound an alarm SndPlaySystemSound( sndAlarm ); return( false ); // A menu item was selected case menuEvent: // Handle the menu event calcFormMenuEventHandler( spEvent ); return( true ); } // We're done return( false ); } 当有按钮按下时,函数发出提示声音 ,当菜单选项被选中时,我们调用函数calcFormMenuEventHandler()来处理,它实际上是定义在app.h中的一个宏,允许我们在其它的程序中选择菜单时也可以调用这个函数。 注意到,当我们按下按钮时函数的返回值是false。这是因为Palm OS还需要进一步的处理按钮按下的图形。记住,一般情况下,当函数返回false时是希望系统忽略这个事件,但是在菜单事件中(memuEvent),如果在处理一个菜单事件后返回false,Palm OS就会发出一声警告声,好像出现了什么错误似的。 当编制这段程序后,我们就可以天衣无缝的将这个程序添加到其它的应用程序中。例如:在一个财政信息软件中,我们可以把它做成一个弹出窗口,再根据主窗口的需要进行计算。 Fcalc.h模块 Fcalc.h是fcalc.c的头文件。创建一个新文件,将其命名为fcalc.h,并保存在Caculator工程的Src文件夹中。 象main.h一样,fcalc.h也有预处理指令#ifdef、#define、#endif等以防止编译器作无用功。Fcalc.h中的代码定义了fcalc.c中函数原型,以保证在调用此函数时,这个函数已定义过并且参数传送正确。 #ifndef FCALC_H #define FCALC_H // // fcalc.h // Definitions for the "calc" form. // Copyright (c) 1999, Robert Mykland. All rights reserved. // /// // Global Prototypes // /// Boolean calcFormEventHandler( EventPtr spEvent ); #endif // FCALC_H 你或许会问,这样一个头文件中怎么就只定义了一个函数呢?这个问题关键在于我们现在只有一个函数。不过要十分注意的是,我们首先要有一个完整的开始框架,为将来的扩展着想,为以后的代码留出空间。如果永远也不扩展的话也没关系,这样作可以更容易的找到函数。 App.h模块 头文件app.h将前面所有模块的头文件汇总在了一起。创建一个新文件,将其命名为app.h并存放在Src文件夹中。将其添加在工程中,放在AppIncludes中。 下面让我们一步一步的来分析代码: #ifndef APP_H #define APP_H // // app.h // Definitions for the application. // Copyright (c) 1999, Robert Mykland. All rights reserved. // // // Includes // // #include <Pilot.h> // All the Palm includes #include "Calculator_res.h" // Resource definitions #include "fcalc.h" // Definitions for the "calc" form #include "main.h" // Definitions for the main entry point app.h看起来和我们以前编辑的头文件十分相似,首先是#ifndef和#define预处理指令,然后是一些头文件的定义。第一个是Pilot.h头文件的定义。由于我们将在所以的源文件中都使用app.h头文件,所以必须在此处定义Pilot.h。下一个是资源头文件。注意到因为Pilot.h是系统头文件,所以被包括在<>中,我们自定义的头文件被被包括在引号中。 下面是fcalc.c的头文件定义,app.h被分成了几个部分分别处理各个源文件,这样就使我们在将来可以很容易的从程序中摘取必要和充分的代码应用于其它的模块。 / // Definitions for fcalc.c // / // The menu event handler macro for the "calc" form #define calcFormMenuEventHandler(spEvent) 这里是菜单处理函数的定义。现在的定义没有什么实际意义,在代码中也没有内容,这是因为calc窗体还没有菜单。我特意在函数调用的括号中留出了一个空格,宏会在宏名称后的第一个空格后开始运行,所以你向宏添加参数时,为是编译正常进行,注意不要留出空格。 注意: 只有在必要的时候才使用宏,因为使用宏是十分危险的。一般的函数都是可以利用原型来检查函数的参数,而对于宏来说没有函数原型。如果你向宏中传递了错误的参数,你将很难检查出来。我们在这里使用宏是想创建一个快速简易的“函数”,除了在源文件中被引用,它们不会占有代码空间。我们不想在头文件中添加函数,因为那样会使它们在每次被在源文件调用时都会被执行。 再下面是main.c的定义,首先是函数getEventHandler()的原型,在这里有一个我们以前没有见过的定义:EVENT_HANDLER_LIST。 // Definitions for main.c // // The prototype for getEventHandler() static FormEventHandlerPtr getEventHandler( Word ); // This creates the function getEventHandler, // which returns the event handler for a given form // in this application. #define EVENT_HANDLER_LIST / static FormEventHandlerPtr getEventHandler( Word wFormID )/ {/ switch( wFormID )/ {/ case AboutForm:/ return( aboutFormEventHandler );/ case CalcForm:/ return( calcFormEventHandler );/ case PrefsForm:/ return( prefsFormEventHandler );/ }/ return( NULL );/ } EVENT_HANDLER_LIST实际上就是函数getEventHandler()的定义。在main.c中预处理器用这个定义代替了此函数。我们可以在任何地方使用#define语句,它实际上就象文本编译器中的查找替换命令。在每一行后面的反斜杠可以使预处理器忽略每一行后的行中断符,当遇到行中断符时,#define语句就会停止,所以我们在getEventHandler()的最后一行没有反斜杠。 现在,getEventHandler()只是关联了窗体CalcForm中的calcFormEventHandler()。如果想再添加窗体以及事件处理函数,我们可以在头文件中直接扩展。 // This defines the macro that initializes the app #define appInit() // This defines the macro that cleans up the app #define appStop() // This application works on PalmOS 2.0 and above #define ROM_VERSION_MIN ROM_VERSION_2 // Define the starting form #define StartForm CalcForm // Definitions for moptions.c // // Menu ID name conversions #define OptionsAbout OptionsAboutCalculator #endif // APP_H 这里是appInit()和appStop()的宏定义。但它们现在还没有实在意义。文件的最后定义了运行此程序的最低版本为version 2.0,其中变量使用了main.c中的定义。我们还定义了开始窗体是CalcForm。最后是#endif语句,千万不要忘记这一结束语句,否则在编译时会出现很奇怪的错误。 建立一个向app.h这样的文件的主要目的是节省运行时间,并做到尽量在头文件中修改代码,而不要在源文件中修改。这样做有很多好处因为只要你修改代码,就有可能引入bug,但是一般来说,修改程序中的常量可以减少引入bug的机会。如果将代码的修改集中在一些头文件中,就可以减少本来运行正常的程序经修改后崩溃的比率。 如果一个程序员一般不会向程序中引入bug,这样的程序结构也是有好处的,因为即使程序中有良好的注释,也需要花费一些时间来看懂程序并作修改。如果将改变集中在有着好的文件结构的头文件中,就会比重新学习各个模块的细节花费的时间少的多。 调试 所有模块均已完成,现在开始调试。如果不出意外的话,运行后的程序界面如下图所示: 程序从函数PilotMain()开始执行。初始化过程如下:检查ROM的版本并检查运行代码,然后调用了函数FrmGotoForm(),它将产生两个待处理的事件:frmLoadEvent和frmOpenEvent,接着就进入了无限的事件循环。 FrmLoadEvent引发了processEvent(),它将从事件结构中获得窗体的ID并初始化窗体,然后就等待其它事件的发生。 如果你按下了OK按钮,就会发出一个提示声音。 程序列表: 下面是app.h全部的源代码: #ifndef APP_H #define APP_H // // app.h // Definitions for the application. // Copyright (c) 1999, Robert Mykland. All rights reserved. // // // Includes // // #include <Pilot.h> // All the Palm includes #include "Calculator_res.h" // Resource definitions #include "fabout.h" // Definitions for the "about" form #include "fcalc.h" // Definitions for the "calc" form #include "fprefs.h" // Definitions for the "prefs" form #include "main.h" // Definitions for the main entry point #include "moptions.h" // Definitions for the "options" menu // // Definitions for fabout.c // // // The menu event handler macro for the "about" form #define aboutFormMenuEventHandler(spEvent) / // Definitions for fcalc.c // / // The menu event handler macro for the "calc" form #define calcFormMenuEventHandler(spEvent) / {/ optionsMenuEventHandler( spEvent );/ } // // Definitions for fprefs.c // // // The menu event handler macro for the "prefs" form #define prefsFormMenuEventHandler(spEvent) // Definitions for main.c // // The prototype for getEventHandler() static FormEventHandlerPtr getEventHandler( Word ); // This creates the function getEventHandler, // which returns the event handler for a given form // in this application. #define EVENT_HANDLER_LIST / static FormEventHandlerPtr getEventHandler( Word wFormID )/ {/ switch( wFormID )/ {/ case AboutForm:/ return( aboutFormEventHandler );/ case CalcForm:/ return( calcFormEventHandler );/ case PrefsForm:/ return( prefsFormEventHandler );/ }/ return( NULL );/ } // This defines the macro that initializes the app #define appInit() // This defines the macro that cleans up the app #define appStop() // This application works on PalmOS 2.0 and above #define ROM_VERSION_MIN ROM_VERSION_2 // Define the starting form #define StartForm CalcForm // Definitions for moptions.c // // Menu ID name conversions #define OptionsAbout OptionsAboutCalculator #endif // APP_H 下面是main.c全部的源代码: // // main.c // Main entry point and event loop. // Copyright (c) 1999, Robert Mykland. All rights reserved. // // // Includes // // #include "app.h" // The definitions for this application /// // Global Prototypes // /// DWord PilotMain( Word, Ptr, Word ); // Main entry point. Boolean processEvent( Long ); // Processes the next event. / // Local Variables // / // The event handler list EVENT_HANDLER_LIST // // Global Functions // // //---------------------------------------------------------------------------- DWord PilotMain( //---------------------------------------------------------------------------- // The main entry point for this application. // Always returns zero. //---------------------------------------------------------------------------- Word wCmd, // The launch code Ptr, // The launch parameter block Word ) // The launch flags //---------------------------------------------------------------------------- { DWord dROMVersion; // Get the ROM version dROMVersion = 0; FtrGet( sysFtrCreator, sysFtrNumROMVersion, &dROMVersion ); // Alert and bail if the ROM version is too low if( dROMVersion < ROM_VERSION_MIN ) { FrmAlert( LowROMVersionErrorAlert ); // Palm OS 1.0 will continuously re-launch this app unless we switch // to another safe one if( dROMVersion < ROM_VERSION_2 ) { AppLaunchWithCommand( sysFileCDefaultApp, sysAppLaunchCmdNormalLaunch, NULL ); } return( 0 ); } // If this is not a normal launch, don't launch if( wCmd != sysAppLaunchCmdNormalLaunch ) return( 0 ); // Initialize all parts of the application appInit(); // Go to the starting form FrmGotoForm( StartForm ); // Wait indefinitely for events ErrTry { while( true ) processEvent( -1 ); } // Stop the application ErrCatch( lError ) { } ErrEndCatch // Clean up before exit appStop(); // We're done return( 0 ); } //---------------------------------------------------------------------------- Boolean processEvent( //---------------------------------------------------------------------------- // Waits for and processes the next event. // Returns false if the queue is empty. //---------------------------------------------------------------------------- Long lTimeout ) // Time to wait in 100ths of a second, -1 = forever //---------------------------------------------------------------------------- { EventType sEvent; // Our event Word wError; // The error word for the menu event handler // Get the next event EvtGetEvent( &sEvent, lTimeout ); // If it's a nil event, return queue empty if( sEvent.eType == nilEvent ) return( false ); // Handle system events if( SysHandleEvent( &sEvent ) ) return( true ); // Handle menu events if( MenuHandleEvent( NULL, &sEvent, &wError ) ) return( true ); // Load a form if( sEvent.eType == frmLoadEvent ) { Word wFormID; // The form ID FormPtr spForm; // Points to the form // Get the ID wFormID = sEvent.data.frmLoad.formID; // Initialize the form spForm = FrmInitForm( wFormID ); // Establish the event handler FrmSetEventHandler( spForm, getEventHandler( wFormID ) ); // Point events to our form FrmSetActiveForm( spForm ); // Draw the form FrmDrawForm( spForm ); } // Handle form events FrmDispatchEvent( &sEvent ); // If it's a stop event, exit if( sEvent.eType == appStopEvent ) { // Exit ErrThrow( 0 ); } // We're done return( true ); } 可重用的About窗体 在这一部分中,我们将向工程中添加两个窗体和它们相应的代码:About窗体,本程序的关于对话框;prefs窗体,一个用来显示优先权的对话框。在这一章我们只是在这两个窗体上添加一个OK按钮以示它们的存在。 文件Calculator.rsrc的内容添加 我们将向窗体添加一个about窗体、一个prefs窗体和一个菜单项。 1. 运行构造器并打开文件Caculator.rsrc; 2. 创建about窗体的框架,我们将拷贝calc窗体然后作一些修改; 3. 选中calc窗体; 4. 选择Edit | Copy并按下CTRL-C拷贝窗体; 5. 选择Edit | Paste并按下CTRL-V将窗体粘贴到构造器上,此时会出现一个对话框显示输入该窗体的ID; 6. 选中Unique ID; 7. 将窗体的名字改为“about”; 8. 双击打开about窗体,进入窗体属性选项; 9. 选中Save Behind,设置title为Caculator; 10. 将按钮向下移动20个象素,避免和calc窗体的按钮重合,这样就可以容易的和calc窗体进行切换。当一切完成后,about窗体应如图12-3所示: 现在创建prefs窗体。拷贝粘贴About窗体并重命名为“prefs”。将title属性改为“Caculator Preference” 将按钮位置在下移20个象素,完成后的窗体应如图12-4所示: 然后添加一个菜单栏使我们可以在两个新建窗体间切换。首先从菜单栏资源类型和列表中创建一个菜单栏并按下CTRL-K,将其命名为“calc”。选择Resource type创建一个新的菜单并按下CTRL-K,并将其命名为“options”。双击打开菜单栏,将一个菜单拖动到菜单编辑框中。将菜单命名为Options,然后按下CTRL-K添加菜单项,并命名为Preferences...,快捷键为“R”。另外一个菜单项命名为About Calculator。设计完成后,菜单栏如下图所示: fabout.c模块 现在开始为我们新建的窗体和菜单添加代码。下面所示的是fabout.c的源代码。它包括了about窗体的事件处理函数以及这些函数的原型。因为在结构上它和fcalc.c十分相似,所以我建议将fcalc.c拷贝,在这样的基础上进行修改。最后别忘了将fabout.c保存在AppSource下面。 // // fabout.c // Code for the "about" form. // Copyright (c) 1999, Robert Mykland. All rights reserved. // // // Includes // // #include "app.h" // The definitions for this application /// // Global Prototypes // /// Boolean aboutFormEventHandler( EventPtr spEvent ); // // Global Functions // // //---------------------------------------------------------------------------- Boolean aboutFormEventHandler( //---------------------------------------------------------------------------- // Handles events for this form. // Returns true if it fully handled the event. //---------------------------------------------------------------------------- EventPtr spEvent ) //---------------------------------------------------------------------------- { // Handle the event switch( spEvent->eType ) { // A control was selected case ctlSelectEvent: // Return to the calling form FrmReturnToForm( 0 ); return( false ); // A menu item was selected case menuEvent: // Handle the menu event aboutFormMenuEventHandler( spEvent ); return( true ); } // We're done return( false ); } 在aboutFormEventHandler()中,我们处理了两种类型的事件。如果一有按钮按下,那么就调用FrmReturnToForm()将控制权交给所要引用的窗体。利用这个函数,我们就可以建立一个弹出窗体,它只能由函数FrmPopupForm()引发而不能被FrmGotoForm()所调用。由于about窗体通常都是弹出窗体,所以这样做并不会影响代码的可重用性。 对于菜单事件,我们调用了函数aboutFormEventHandler()。它是我们在app.h中的一个宏定义。在头文件中给出此宏定义,就可以让我们在不同的程序中的菜单上重用此代码。 Fabout.h模块 此头文件和calc的头文件本质上是一致的,所以我们拷贝fcalc.h并将其重命名为fabout.h。将其添加到工程中并放置在AppIncludes下面。修改代码和以下所示一致: #ifndef FABOUT_H #define FABOUT_H // // fabout.h // Definitions for the "about" form. // Copyright (c) 1999, Robert Mykland. All rights reserved. // /// // Global Prototypes // /// Boolean aboutFormEventHandler( EventPtr spEvent ); #endif // FABOUT_H fprefs.c模块 下面是fprefs.c的源文件。目前该文件除了将“about”改为“fpref”以外,其他的全部和fabout.c相同。将fabout.c拷贝并做相应修改,并添加到AppIncludes下的Calculator工程下面。 // // fprefs.c // Code for the "prefs" form. // Copyright (c) 1999, Robert Mykland. All rights reserved. // // // Includes // // #include "app.h" // The definitions for this application /// // Global Prototypes // /// Boolean prefsFormEventHandler( EventPtr spEvent ); // // Global Functions // // //---------------------------------------------------------------------------- Boolean prefsFormEventHandler( //---------------------------------------------------------------------------- // Handles events for this form. // Returns true if it fully handled the event. //---------------------------------------------------------------------------- EventPtr spEvent ) //---------------------------------------------------------------------------- { // Handle the event switch( spEvent->eType ) { // A control was selected case ctlSelectEvent: // Return to the calling form FrmReturnToForm( 0 ); return( false ); // A menu item was selected case menuEvent: // Handle the menu event prefsFormMenuEventHandler( spEvent ); return( true ); } // We're done return( false ); } fprefs.h模块 此头文件和calc和about的头文件基本上是一致的,拷贝fabout.h并将其重命名为fprefs.h。将其添加到工程中并放置在AppIncludes下面。下面是所要做的修改: #ifndef FPREFS_H #define FPREFS_H // // fprefs.h // Definitions for the "prefs" form. // Copyright (c) 1999, Robert Mykland. All rights reserved. // /// // Global Prototypes // /// Boolean prefsFormEventHandler( EventPtr spEvent ); #endif // FPREFS_H moptions.c模块 Options菜单的源代码也只有一个事件处理函数。从源文件中任意拷贝一个.c文件,并添加到AppSource下的工程中,根据下面代码列表做相应的修改: // // moptions.c // Code for the "options" menu. // Copyright (c) 1999, Robert Mykland. All rights reserved. // // // Includes // // #include "app.h" // The definitions for this application /// // Global Prototypes // /// Boolean optionsMenuEventHandler( EventPtr spEvent ); // // Global Functions // // //---------------------------------------------------------------------------- Boolean optionsMenuEventHandler( //---------------------------------------------------------------------------- // Handles events for this form. // Returns true if it fully handled the event. //---------------------------------------------------------------------------- EventPtr spEvent ) //---------------------------------------------------------------------------- { switch( spEvent->data.menu.itemID ) { // The about menu item was selected case OptionsAbout: FrmPopupForm( AboutForm ); return( true ); // The prefs menu item was selected case OptionsPreferences: FrmPopupForm( PrefsForm ); return( true ); } // We're done return( false ); } 当菜单项被选中后,我们调用FrmPopupForm()弹出相应的窗体。如果菜单项和窗体ID相符返回true,否则返回false。 Moptions.h模块 该头文件和以前的头文件也十分相似,只包含和一个事件处理函数的原型。将前面任意的一个头文件拷贝并重命名moptions.h,然后将其添加到AppIncludes下的工程中,相应的修改如下所示: #ifndef MOPTIONS_H #define MOPTIONS_H // // moptions.h // Definitions for the "options" menu. // Copyright (c) 1999, Robert Mykland. All rights reserved. // /// // Global Prototypes // /// Boolean optionsMenuEventHandler( EventPtr spEvent ); #endif // MOPTIONS_H app.h的内容添加 为了将新建的模块包含在app.h里面,我们需要添加一些代码,首先是新的头文件: // // Includes // // #include <Pilot.h> // All the Palm includes #include "Calculator_res.h" // Resource definitions #include "fabout.h" // Definitions for the "about" form #include "fcalc.h" // Definitions for the "calc" form #include "fprefs.h" // Definitions for the "prefs" form #include "main.h" // Definitions for the main entry point #include "moptions.h" // Definitions for the "options" menu 将新建的头文件逐个添加到app.h中,我一般是按照字母的顺序来排列这些文件名,这样就可以很容易查找所需头文件。 // // Definitions for fabout.c // // // The menu event handler macro for the "about" form #define aboutFormMenuEventHandler(spEvent) / // Definitions for fcalc.c // / // The menu event handler macro for the "calc" form #define calcFormMenuEventHandler(spEvent) / {/ optionsMenuEventHandler( spEvent );/ } fabout.c和fpref.c所需要的程序入口和fcalc.c相似。然后为各自的菜单添加相应的宏。另外,由于我们添加了菜单,所以菜单处理宏也应该干点什么了。在宏中我们调用了函数optionMenuEventHandler()来处理该菜单的各个选项。 下面向getEventHandler()函数添加代码,以使我们新建的窗体能够被引发。 // This creates the function getEventHandler, // which returns the event handler for a given form // in this application. #define EVENT_HANDLER_LIST / static FormEventHandlerPtr getEventHandler( Word wFormID )/ {/ switch( wFormID )/ {/ case AboutForm:/ return( aboutFormEventHandler );/ case CalcForm:/ return( calcFormEventHandler );/ case PrefsForm:/ return( prefsFormEventHandler );/ }/ return( NULL );/ } about窗体和prefs窗体的入口和calc窗体的模式相同,在main.c中不需要做其它的修改。 为Option菜单添加定义: // Definitions for moptions.c // // Menu ID name conversions #define OptionsAbout OptionsAboutCalculator 因为Calculator_res.h中的常量定义基于具体的菜单项,所以我们应该将其抽象为不含诸如“Calculator”等基于具体程序的文本的通用名称,使之能重复使用。 程序列表 下面是新的app.h的全部代码: #ifndef APP_H #define APP_H // // app.h // Definitions for the application. // Copyright (c) 1999, Robert Mykland. All rights reserved. // // // Includes // // #include <Pilot.h> // All the Palm includes #include "Calculator_res.h" // Resource definitions #include "fabout.h" // Definitions for the "about" form #include "fcalc.h" // Definitions for the "calc" form #include "fprefs.h" // Definitions for the "prefs" form #include "main.h" // Definitions for the main entry point #include "moptions.h" // Definitions for the "options" menu // // Definitions for fabout.c // // // The menu event handler macro for the "about" form #define aboutFormMenuEventHandler(spEvent) / // Definitions for fcalc.c // / // The menu event handler macro for the "calc" form #define calcFormMenuEventHandler(spEvent) / {/ optionsMenuEventHandler( spEvent );/ } // // Definitions for fprefs.c // // // The menu event handler macro for the "prefs" form #define prefsFormMenuEventHandler(spEvent) // Definitions for main.c // // The prototype for getEventHandler() static FormEventHandlerPtr getEventHandler( Word ); // This creates the function getEventHandler, // which returns the event handler for a given form // in this application. #define EVENT_HANDLER_LIST / static FormEventHandlerPtr getEventHandler( Word wFormID )/ {/ switch( wFormID )/ {/ case AboutForm:/ return( aboutFormEventHandler );/ case CalcForm:/ return( calcFormEventHandler );/ case PrefsForm:/ return( prefsFormEventHandler );/ }/ return( NULL );/ } // This defines the macro that initializes the app #define appInit() // This defines the macro that cleans up the app #define appStop() // This application works on PalmOS 2.0 and above #define ROM_VERSION_MIN ROM_VERSION_2 // Define the starting form #define StartForm CalcForm // Definitions for moptions.c // // Menu ID name conversions #define OptionsAbout OptionsAboutCalculator #endif // APP_H 调试 又到了调试的时候了,界面应该和以前相同。当你按下菜单栏上的prefs或about选项时,相应窗体就会跳出。当按下各自的OK按钮后,返回到calc窗体。 和以前一样,程序从PilotMain()开始,按照常规的路线向下运行,processEvent()产生的frmLoadEvent事件初始化了窗体的新的菜单栏。然后程序就进入无限循环来等待事件的触发。 如果你按下OK按钮,系统将发出警告声音。 面向对象的编程 面向对象编程的目的就是使代码更加容易被维护和重复使用。基于其它代码中的一段或变量,使用封装和抽象的方法,使bugs变得很明显。只要你弄懂各个代码部分之间的关系,就可以找到并修正这些bugs。代码的重用性提高是因为各个代码部分间的依赖性减少了,这样就可以从很容易的从面向对象的程序中摘出有用的代码应用到其它的工程中去。根据上个世纪中人们对面向对象编程的看法,或许你从认为它不过是骗局转变到认为它是唯一的编程方法吧。或许你是对的,也就是说,不管有没有价值,我也对面向对象的编程谈一下或许并不谦虚的看法。 首先,面向对象的编程应该是一种编程的设计原则,并不和你所使用的编程语言有多大的关系。的确,象C++和JAVA很容易实现面向对象的方法,但应用的便利应是面向对象这种原则所提供的。经过近十年的大量使用面向对象的语言诸如Smalltalk、C++和JAVA的编程,我知道了一个人如果精通JAVA的语法,并不代表他真正知道了如何有效的、有价值的利用对象。 真正重要的应该是理解面向对象的理念和设计模式,知道为什么使用这些理念和设计原则就更有效和更有价值。面向对象的编程也是一种工具,这和别的并没有区别,如果你胡乱用的话,它和其它工具的滥用一样会造成损害。没有人会因为锤子是最新的产品就在房间里乱砸东西。但是,我看到许多人由于使用面向对象的编程方法不对,结果弊大于利。 C++、JAVA和Palm OS 一般来讲,我也很喜欢使用C++和JAVA。但我不提倡在Palm OS中使用的原因是,这些语言的类库在Palm OS中的使用并没有带来任何益处。我们得到的只是程序更臃肿、运行速度更慢。 当有了一个小的、简洁为Palm OS或C++和JAVA开发的类库,我会第一个使用它。或许我将写一个。 幸运的是,在Palm OS应用程序的开发中,我们可以从面向对象的原则中获得一些有意义的启示。如我前面所言,关键就是掌握面向对象的原则。下面,我就向你介绍一下面向对象编程的一些最流行的概念,并给出一些如何在C程序使用这些原则的技巧。 数据封装 面向对象的基本原则就是将数据结构及其应用放在一个叫做“对象”的实体中。数据封装的思想就是把数据结构保护起来,以免遭到对象外代码的破坏。如果你遵从了这个原则,当数据结构遭到破坏时,就可以确定问题一定出在对象的里面,除非存在所谓的“wild pointer”。 “wild pointer”是指指针并没有指到一个具体的位置上。例如,下面的代码就会造成“wild pointer”的问题。 Char * cpBuffer=2000; 在有“wild pointer”的情况下,或许就有不走运的对象被它指定了,所以就会引起一些问题。因此,最好要节俭的、小心的使用指针。 数据封装在C中很容易实现的。将各成对象的代码分成单独的文件保存,并把变量声明成静态变量(static)。这就可以防止它们被其它文件中的函数所调用。 如果对象想访问其它对象中的变量,你可以把变量声明为全局变量来实现,但不要把全局变量声明的太多。不要把数据结构的指针传递给对象外的函数,而是让函数传递一个数据结构供你填入或者创建一个原始数据的复件来代表函数。 数据抽象 数据抽象的原则是指一个对象中的数据结构不依赖于任何其它对象中的数据结构。因此,一个对象可以重写或完全改变它的基本变量,而不会影响代码或数据在其它对象中的有效性。 数据抽象的用处就是可以重建对象,使程序更易重用和维护而不影响系统的其他部分。 显然,不要对其他对象的数据结构中使用“内部消息”。要把它们看成黑箱。不要认为你知道了其他对象的数据结构的大小,而应使用sizeof();不要认为你知道了变量在数据的什么地方。 继承 继承的含义是指在一般的对象上添加具体的内容生成其子对象。这就避免了你花费大量的时间来产生和已有的对象很多地方都相同的对象。 在C中实现继承的一种方法是使用#define语句。假设你有一个简单的列表框,现在你想再产生一个列表框。在这个列表框中,除了增加了浏览功能外,其它的和简单的列表框完全相同。下面是简单列表框的函数原型: VoidHand slCreateList(void); Void slDestroy(VoidHand); Void slAddToList(VoidHand,lidtitem_t*); Void slSaveList(VoidHand,FILE*); Void slLoadList(VoidHand,FILE*); 在可浏览列表的头文件中,你可以这样定义: #define ilCreatList slCreatList; #define ilDestroyList slDestroyList #define ilAddtoList slAddToList #define ilSaveList slSaveList; #define ilLoadList slLoadList void ilGetFirst(VoidHand,listitem_t*); void ilGetNext(voidHand,listitem_t*); 多态性 多态性指可以统一的处理一组对象。例如,我们可以删除一个列表框条目,而不必管列表是简单列表框还是可浏览的列表框。 在C中可以通过使用指针来实现多态性。注意,在删除函数中保存在列表建立时代表对象的指针,当调用函数 DeleteListItem()时,使用这个变量将所指记录删除。 下一步做什么 在下面的两章中,我们将使用本章讲到的新的框架生成一个calculator程序。 在下一章中,我们将应用第十章中讲到的用户界面知识来设计计算器,我们还将用到公共库,特别是MathLib的应用。 |