symbian入门系列 -- Windows C++ 程序员如何过度到Symbian C++ ?(转)
时间:2010-10-09 来源:冰岛
Symbian OS C++ for Windows C++ programmers
Andy Weinstein, Degel Software Ltd
Version 1.0, Oct 2002
1.简介。
本文讨论了当一个典型的Windows C++程序员初次接触Symbian操作系统时可能遇到的问题。我们开发过三个成功版本Symbian操作系统的经验使我们十分清楚在这个不算丰富稳定的环境中工作什么才是困难的。Symbian成功的一个原因是许多手机生产商非常不希望被绑在微软这条贼船上,另一个原因是Symbian集成了轻量级、一流的系统,同时又可以提供如此广泛的功能。这里的一些提示也许会对开发成功的Symbian操作系统应用程序有所帮助。
2.文档和资源。
对于一个普通的Windows程序员来说,他对Symbian操作系统首先注意到的方面就是相比起微软的高级精良装备来说,Symbian开发文档实在是太少了。虽然这种情况正在得到改善,某些API类仍然没有相关文档。比如CEikRichTextEditor这个类在文档中就没有独立条目。CRichText这个类包括了大多数相关功能,被收入了文档,但你可能并不知道何时第一次使用这种控制(参见我们下面关于对象结构的说明)。
相对花费大量的人力物力去完善文档,更简便的方法基于这样一种事实:Symbian提供了大量的源代码(虽然还不是整个操作系统)和例程。程序员无需猜测许多API的行为——他们只要看看执行情况。要是这样还不够,Symbian对一些组件,例如Word,一样提供了源代码。API和例程相结合足以满足大多数程序员的需要。
从6.0版开始,Symbian操作系统95%的源代码是对其白金合作伙伴项目(Platinum
Partner Program,http://www.symbian.com/partners/part-platnm.html)成员开放的。这个需要额外付费注册。这个项目的影响之一就是某些老版本开发包的完整源代码不再提供,取而代之的是Symbian或者Nokia公司一边继续完善文档,一边提供不断扩充的例程。比如随Series 60提供的文档就有一个名为“如何操作rich text”的独立条目(虽然CEikRichTextEditor仍然没有条目)。
文档中还有许多其他有用的,写得很好的“How to”文章。在Series 60中,文档和例程的结合使用是显而易见的——文档中对每一个Avkon UI类都直接引用有例程。
Windows程序员还有一个要习以为常的事情是缺乏有用的外部资源,虽然这种情况正在得到改善。《专业Symbian编程》(‘Professional Symbian Programming’ (PSP))包含了许多有用的信息,但是作为快速参考就不太合适,而且它成书于Symbian操作系统第5版的时候——新版将于2003年早些时候面世。PSP还不算过时,但情况有所改变,尤其是在UI层次上。有本书叫《Symbian设备无线Java》(‘Wireless Java for Symbian Devices’)也许更新一些,可对C++开发者没什么用处。还有《Symbian操作系统通讯编程》(‘Symbian OS Communications Programming’)和《Series 60及Symbian操作系统编程》(‘Programming for Series 60 and Symbian OS’)也已上架。更多细节可以在http://www.symbian.com/books/index.html找到。
在线文档和Q-A资源确实有助于弥补空白。Symbian自己设立了“Symbian开发者网络”,这是一个集论坛、FAQ、样例和其它有用信息于一身的网络资源。它的地址是http://www.symbian.com/developer。Nokia也有一个类似的项目叫做“Nokia论坛”,网址位于http://www.forum.nokia.com。到底该去哪里可并不一定,而且你贴出了问题也不一定就会有答案——这依赖于你其它的开发伙伴的与人为善。Symbian和Nokia的内部人员有时的确会出现在这里,但是如果你希望从知道答案的那个人口中得到确切答复,你需要付费注册。Symbian的付费注册地址在http://www.symbian.com/partners/part-servs.html的“Partner Programs”中,Nokia的则可以从Nokia论坛的“Developer Support, Technical Case Solving”中找到。不过还是先看看免费资源吧:这里有大量有用的最新信息和资源,包括定时的开发包升级。
3.对象结构。
Symbian操作系统有一个需要花费时间来熟悉的方面在于他非常强大的对象结构。例如,一个列表框(list box)不是一个对象——而是四个:列表框对象(list box object)、模型(model)、视图(view)和绘图器(drawer)。它们之间的功能划分是可以预期并且很符合逻辑的。
编辑控制就复杂的多。乍一看上去,它好像只有两个主要对象:UI控制和一个包含处理文本格式化的文本对象。但是对格式化的操作引入了对字符格式化和对段落格式化的更进一步的对象。这些,按照次序,使用了独立的mask对象来指示显示了你希望对任何给定调用定位的格式化的API调用。所以操作文本显示为黑体还是非黑体,同时还影响到行间距的代码就需要用到CEikRichTextEditor、CRichText、TCharFormat、TCharFormatMask、CParaFormat和CparaFormatMask这几个类。
这里有一段这样的代码:
TCharFormat defaultFormat;
TCharFormatMask formatMask;
formatMask.SetAttrib(EAttFontStrokeWeight);
CRichText* text = iDisplay->RichText();
text->Reset();
for (int i = 0; i < 10; ++i)
{
TPtrC boldText = getBoldPiece(i);
TPtrC plainText = getRomanPiece(i);
...
TInt insertPos = text->DocumentLength();
text->SetInsertCharFormatL(*iCharFormat, formatMask, insertPos);
text->InsertL(insertPos, boldText);
text->CancelInsertCharFormat();
insertPos = text->DocumentLength();
text->SetInsertCharFormatL(defaultFormat, formatMask, insertPos);
text->InsertL(insertPos, plainText);
text->CancelInsertCharFormat();
}
CParaFormat paraFormat;
TParaFormatMask paraFormatMask;
iDisplay->RichText()->GetParaFormatL(¶Format, paraFormatMask,
0, iDisplay->TextLength());
paraFormatMask.ClearAll();
paraFormatMask.SetAttrib(EAttLineSpacing);
paraFormatMask.SetAttrib(EAttLineSpacingControl);
paraFormat.iLineSpacingControl = CParaFormat::ELineSpacingExactlyInTwips;
CGraphicsDevice* screenDevice = iEikonEnv->ScreenDevice();
TInt paraDelta;
...
TInt lineHeight = screenDevice->VerticalPixelsToTwips(
iRegularFont->HeightInPixels() + paraDelta);
paraFormat.iLineSpacingInTwips = lineHeight;
iDisplay->RichText()->ApplyParaFormatL(
¶Format, paraFormatMask, 0, iDisplay->TextLength());
但是这还不是全部——关于文本视图对象还另有天地,特别是CTextView和CTextLayout,连同他们的帮助对象一起。当我们想在编辑控制中定位滚动点,以使文本的最后一行可以处于编辑控制底部之上一行的位置,我们就不得不了解到CTextView的存在,以及如何获得它,还有关于CTextView::SetViewL、TViewYPosQualifier::SetHotSpot及TViewYPosQualifier::SetMakeLineFullyVisible。他们看起来是这个样子的:
TInt yPos = iDisplay->TextView()->ViewRect().iBr.iY;
TViewYPosQualifier yPosQualifier;
yPosQualifier.SetHotSpot(TViewYPosQualifier::EFViewBottomOfLine);
yPosQualifier.SetMakeLineFullyVisible(
TViewYPosQualifier::EFViewForceLineFullyVisible);
iDisplay->TextView()->SetViewL(
iDisplay->TextLength(),
yPos,
yPosQualifier,
CTextView::EFViewDiscardAllFormat);
谁会想得到呢?我们的一位同事就是不相信这是就此可行的惟一途径,但是他通过利用许多(各种各样的)存在于各个类之中的各种API,试用了各种不同的方法来证明了这一点。
我们正在描述的这种复杂性其实是因为Symbian操作系统提供的功能实在太丰富了,理解这一点很重要。一旦你了解了这个领域,你就会对Symbian操作系统的对象结构是如此明智感激不尽,然后很快你就会发现你能够预知你需要的函数藏在什么地方。如此重复,你会发现我们刚开始的困惑现在已经变成了一种审美享受。
4.串。
串,啊,Symbian中的串!毫无疑问Symbian中对串的实现是经过深思熟虑的,强壮的和经济的。同样无疑的是这代表着一种富贵病。串是通过Symbian称之为“描述符”的机制来实现的,跟以下几个类有关:TDesC、TBufCBase、TDes、TPtrC、TBufC、HBufC、TBuf和TPtr。这还不包括由TLitC操纵的直接量,它严格来说还算不上是个描述符。我至今也还没有提到Unicode,虽然它显然很受注意。这些类可以使你精确的使用最少量的内存来存储不同类型的串,同时还允许它们“干净”的互相操作。但是每次都要考虑到底使用哪一个实在不是一件有趣的事情,这种情况是无法避免的,因为不同的API有不同的参数或返回值。
这里有个小例子把一个名字转化为样本消息:
_LIT(KBoilerplate, "Hello there, %S");
TPtrC name = GetPointerIntoNameDescriptorWithoutAllocatingAnyMemory();
TPtr finishedProduct = HBufC::NewLC(KBoilerplate().Length() + name.Length())->Des();
finishedProduct.Format(KBoilerplate(), &name);
// 这段代码还缺少了一行,在下文会添加并讨论。
根本没有哪个类提供类似于MFC串或是Java串——完全动态的串。Java中对于String和StringBuffer的划分对我们在此讨论的问题几乎没有任何价值。Symbian所做的值得赞扬的事就是他们把这些的文档做得很好。但是你真正需要的文档却不会频繁去读它。Symbian在开发时考虑的是那些资源有限的机器,不可否认对于某些应用方面和平台来说这是必不可少的。但是就目前来说,一个Symbian操作系统的C++程序员会感觉像那些早期的IBM PC程序员,他们在对付的是Intel的分段结构,然而68000芯片又大又单一的寻址空间使得苹果看起来是个更酷的选择。
5.清洁栈及错误处理。
Symbian操作系统的错误处理框架,主要是清洁栈,是个Windows程序员不太熟悉的元素。就像描述符一样,它很优雅,Symbian宣布它比C++语言的异常处理机制有效得多。在一篇叫做《Symbian操作系统编程术语》的文章里对此进行了很好的讨论,可以从http://www.symbian.com得到。但是,在系统层次上几乎是一定的“正确的事”却减慢了应用开发。Symbian为需要在有限资源条件下长时间运行的应用程序设计了这样一个框架,这需要在开发时间为强壮度付出代价。使用TRY/CATCH这种类型的框架当然是简单明了受到欢迎的,但是要频繁的在恰当的时候把已某种分配的内存压入和弹出清洁栈意味着你就会偶尔犯错误。当发生这种情况时,你就得花费大量的时间去追踪在你的代码中造成的不对称现象。
作为一个对描述符和清洁栈不熟悉的程序员是如何陷入困境的例子,让我们简单的看看一个关于串的示例:
_LIT(KBoilerplate, "Hello there, %S");
TPtrC name = GetPointerIntoNameDescriptorWithoutAllocatingAnyMemory();
TPtr finishedProduct = HBufC::NewLC(KBoilerplate().Length() + name.Length())->Des();
finishedProduct.Format(KBoilerplate(), &name);
// 下面的就是缺少的一行——见之前的注释
// CleanupStack::Pop();这段代码确实把内存推入了清洁栈,但仅仅是对变量的数据类型一瞥而过你就发现不了这一点。这是因为我们使用了一种便利的方法定义了一个TPtr来持有finishedProduct并把HBufC::NewLC(...)->Des()的值赋给它。HBufC是在堆上已分配内存的标识,但是如果我们仅仅看到了左边,可能就会忽略掉刚一分配我们就使用了Des()的快捷方式这一事实。NewLC也直接把分配的内存推入清洁栈。一旦我们完成了finishedProduct,最好调用CleanupStack::Pop(),否则我们的清洁栈就会不平衡。问题在于,我们可能起先发现不了这种不平衡,直到它引发错误为止。还有比追踪这种不对称更有趣的事情呢。
6.应用框架。
Symbian操作系统的确提供了一套用以建立应用程序的类,然而同时,使之能够建立基于非对话框的应用程序界面的框架代码却非常少。要处理像焦点转移这种事件只有靠手工。这个例子当然很容易理解怎样来做,但实际上每个屏幕画面被设计的时候都要仔细考虑它们可是一件费时费力又容易招致错误的事情。从Symbian操作系统6.1版开始,Symbian操作系统增加了一个很简单的机制使用惟一的ID号来实现不同屏幕之间的切换,但是要做的事情还很多。
这里有一个适用于大多数初级情况的例子:这里没有使用API来给屏幕增加一个控制,而是使用了视图对象中叫做CountComponentControls和ComponentControl的两个对象,后者必须为从0到前者返回值之间的每个整数给UI控制返回一个指针。要添加一个控制,除了构造,调整大小,在恰当地点激活,你必须在代码里对CountComponentControls的返回值进行累加,还要在ComponentControl函数中给switch语句添加一个case。
Symbian操作系统确实提供了根据资源文件建立对话框的完美合理的途径。当你在资源文件里声明对话框时,你不必直接对这些东西寻址。但是由于对话框只允许在一个竖列中布置控制,对于一些高级的应用程序中复杂的屏幕设置来说可能并不适合。
7.窗口问题。
Symbian操作系统不鼓励使用“自有窗口”控制。这就是说,一个控制并不总是持有自已的窗口(从受按Z轴顺序裁剪的窗口系统控制的图形对象意义上来说)。因为这些对象使用了系统资源,应用程序应该简单的执行一个合适的绘画功能来规定它所在矩形的边界。这对于习惯每个控制有一个真正窗口的开发者来说可能感觉不同,但是掌握这一点并不困难。
虽然Symbian操作系统的标准UI控制集十分丰富而且强壮,但是偶尔也会有误操作的情况发生。比如我们已经有了一个带滚动条的编辑框,现在要能够暂时用另外一个控制取代它。仅仅使编辑框不可见并不能解决问题。编辑框的某些内容,包括滚动条,仍然以多种有趣但不可接受的方式存在着。我们认为这可能应该归咎于前面提到的编辑框的复杂的对象结构;看起来在这种分布式结构中要想进行隐藏可不是无关紧要的问题。我们最后是通过在画面以外重新定位它解决的。
用这种方法,我们探究了一些其它的可能性,还研究了像控制栈以及Symbian称之为弹出窗口的东西,以后这可以使你按下按钮激活弹出选择列表时顺手拈来。这里进行了示范:
iPopout = new (ELeave) CEikColumnListBox;
iPopout->ConstructL(NULL,CEikListBox::EPopout);
iPopout->CreateScrollBarFrameL();
iPopout->ScrollBarFrame()->SetScrollBarVisibilityL(
CEikScrollBarFrame::EOff,CEikScrollBarFrame::EAuto);
iPopout->SetObserver(this);
TSize popoutSize = TSize(width, height);
TPoint popoutOrigin = TPoint(topLeft, bottomRight);
iPopout->Model()->SetItemTextArray(iArray);
iPopout->Model()->SetOwnershipType(ELbmDoesNotOwnItemArray);
iPopout->ItemDrawer()->ColumnData()->
SetColumnWidthPixelL(0, firstColWidth);
iPopout->ItemDrawer()->ColumnData()->
SetColumnWidthPixelL(1, secondColWidth);
iPopout->SetExtent(popoutOrigin, popoutSize);
iPopout->SetCurrentItemIndex(0);
iEikonEnv->AddDialogLikeControlToStackL(iPopout);
iPopout->ActivateL();
iPopout->DrawNow();一种很早也很有趣的学习经验是必须手工书写代码来刷新和显示列表框的内容。刷屏代码很复杂,而且要跟踪所有的插入和删除并不是一件轻而易举的事。无论什么样的刷新率,我们都希望代码可以在刷新过程的最后得到正确显示的结果而不管刷新了什么,还不能丢掉滚动条的位置和选区,也不能产生不必要的闪屏和闪烁感。Symbian操作系统在基础列表框类中提供了叫做HandleItemRemovalL和HandleItemAdditionL的功能。另外还有设置当前项的SetCurrentItemIndex。但是在Symbian操作系统第5版中没有关于这些函数如何工作的文档。我们只能通过研究源代码才知道HandleItemAdditionL隐式使用了DrawNow,但是HandleItemRemovalL不负责重绘。被HandleItemRemovalL所调用的Reset也不得不通过源代码来理解,当前项索引在调用HandleItemRemovalL后需要刷新。这种处理重绘的不对称性看起来很奇怪又带有很多潜在问题。但是有个好消息就是在目前的Symbian操作系统文档中,所有这些都清楚明白的被收录在你能找到的地方。
作为前面被我们一带而过的Symbian的其它方面,在图形领域,为了使系统的总开销尽可能低,有些应用程序的代码开销也被考虑在内。Symbian确确实实提供的特性是优雅和绝大多数情况下的一致性,而且现在在文档方面已经做得很好了,他使创建平滑的图形动作变得相对简单。
8.消息,异步服务和活动对象。
MFC在对无论是初级还是了解驱动他们应用程序的消息循环和调度机制的高级程序员隐藏Windows消息范例方面做得很好。在Symbian操作系统中,驱动应用程序的是一套完全不同又十分强大的机制,被称为活动对象。它与Symbian操作系统的客户端-服务器端结构紧密结合,提供了各种系统服务,同时也可以为程序员所使用来给他们可能需要为应用程序创建的任何异步服务建立干净、标准的接口。
简而言之,CActiveScheduler执行了Windows消息循环,在一个给定线程内提供了共用的多任务,CActive(一个活动对象)充当了消息处理者。在Symbian的UI框架中,这要比MFC把消息隐藏得更好:接收到的消息总是由框架调度给预先定义的可以被应用程序重载的方法——比如OfferKeyEventL(处理键盘输入)。
定时器是使用了活动对象框架的简单系统服务的一个实例。不同于Windows中调用StartTimer来触发WM_TIMER消息的做法,Symbian操作系统中是由一个CTimer对象与系统时钟服务进行交互,RTimer与其RunL函数根据由API规定的时间间隔被调用。程序员从CTimer继承到一个对象,并重载RunL方法。
其它的异步系统服务,如Nokia 7650上的照相机功能也是通过类似方法提供的。CCameraManager定义了一个活动对象提出拍照操作的请求并随即获得通知。实际的异步服务是由RCameraServ提供的,但是典型的程序员根本用不着处理任何客户端-服务器端或者显式的进程间通信问题。
使用活动对象时,头脑中时刻牢记活动对象何时按预定计划执行这个很简单的准则非常重要。这就是说,到底什么时候CActiveScheduler会调用一个CActive对象的RunL方法呢?
·CActive对象必需是“活动”的。这是通过调用Cactive::SetActive实现的,通常在活动对象自己对应用程序某个请求做出的响应中完成。
·CActive对象的状态,由成员变量iStatus所示,一定不能为KRequestPending。这个值表示该对象正在等待服务完成。通常在提出一个服务请求时变量iStatus才会被服务提供者设为KRequestPending。iStatus随后在服务结束时被服务者提供者更新。
·CActiveScheduler需要接收到一个信号——该信号由异步服务在结束时发出。
当异步进程对应用程序发出结束信号,然而没有准备好的CActive对象时,就会产生一个“迷失”信号。这在以下几种情况下可能发生:
·你忘记了使用CActive::SetActive使活动对象处于“活动”状态。
·服务提供者在发出操作完成信号时忘记将iStatus置为非KRequestPending的值。这只会在你写了错误的服务提供者时出现——典型的应用程序会使用系统定义的服务。
·服务提供者给同一个操作发出了不只一个终止信号。
第三种情况在服务提供者设计不够仔细的情况下可能发生。例如,有一个服务已经结束并且对客户端通知了这一事实,但是客户端恰恰在收到这个通知之前发出了取消服务的请求。服务提供者必需忽略这一当前不合理的请求。要是服务提供者没有忽略这一请求,而是对它做出响应并给出一个附加信号说已经结束了,会是怎么样呢?在这种情况中,附加的信号最终就会成为“迷失”信号,并会引起程序出错。这对于纠错来说很困难,因为“迷失”信号是被CActiveScheduler检测到的,没有显示是什么CActive对象的责任。
CActive对象必需确保在各种情况下与服务相关联的终止信号都恰当。在解除程序中应该典型的具有一个CActive::Cancel调用。如果Cancel没有被调用,而对象在有请求仍被挂起时就被删除,错误就会发生。而且,每个CActive对象必需以确保任何由该对象请求的服务都被取消的方式执行CActive::DoCancel,否则CActive::Cancel就会一直等待服务提供者的终止信号。这两种错误都很难被检测到,因为它们只在存在明显的请求时才会被发现。
结束语
我们希望本文对Windows程序员开始Symbian操作系统下的工作能提供值得的期待和有用帮助。我们期望看到随着这片新市场的成长未来的Symbian操作系统及相关的编程范例是如何发展的。