von Dr. Frank Gunzer
Am 5. März ist die C++ Mobile Edition in der Version 1.1 für den C++Builder 6 erschienen. Dementsprechend häufig wurde am Borland-Stand auf der CEBIT 2003 gefragt, was sie leistet und wie man mit ihr Software für mobile Geräte erstellt. Dieser Artikel stellt die Mobile Edition vor und zeigt mit Hilfe eines Beispiels die Prinzipien der Mobile-Programmierung mit dem C++Builder.
Schon seit längerer Zeit ist bekannt, dass es vom C++Builder 6 eine Mobile Edition geben wird, mit der man im Zusammenhang mit dem Series60-Framework von Nokia Handys und andere mobile Geräte programmieren kann. Anfang März wurde dann endlich die Mobile Edition auf der Homepage von Borland zum Download angeboten (www.borland.com). Die Datei ist über 200 Megabyte groß, da sie nicht nur das Plug-In für den C++Builder enthält, sondern auch die Updates #4 für die Personal-, Professional- und Enterprise-Version sowie das Series60-Framework samt Emulator, der zudem noch Perl benötigt, was ebenfalls enthalten ist. Damit alles funktioniert, muss das Update #4 installiert werden. Es ist in diesem Paket nur für die englischen C++Builder-Versionen enthalten, aber man kann es problemlos auch auf die deutsche Version installieren.
Wenn man die Installation abgeschlossen hat, merkt man beim Start des C++Builders zunächst keinen Unterschied. Unter Datei / Neu / Weitere ... hat man jetzt aber ein Tab mit der Überschrift
Mobile, unter dem man mit Hilfe von
Import Mobile Application entsprechende Anwendungen laden kann oder mit Hilfe von
New Mobile Application eine komplett neue Anwendung erstellen kann. Das Importieren ist dann nötig, wenn man eine Mobile-Anwendung weiterentwickeln möchte, die nicht mit dem C++Builder erstellt wurde und zu der es daher keine
BPR- beziehungsweise keine
BPG-Datei gibt. Wenn die entsprechenden Dateien vorhanden sind kann man auch ganz normal mit
Projekt öffnen die Anwendungen laden. Unter dem Menüpunkt
Projekt befinden sich auch einige neue Punkte, die benötigt werden, wenn man die fertige Anwendung auf einem Handy installieren möchte. Unter
Tools kann man unter
Mobile Options Pfade und Konfigurationen einstellen sowie unter
Mobile Build Tools einige Tools des Nokia-SDKs direkt aufrufen (zum Beispiel den Emulator). Unter
Hilfe befinden sich auch zwei neue Punkte, nämlich
Series60 SDK Help und
Borland C++ Mobile Edition Help, mit denen man die entsprechende Dokumentation anschauen kann.
Bevor wir zu der eigentlichen Erstellung einer Mobile-Anwendung übergehen, soll zunächst einmal gesagt werden, was die Mobile Edition darstellt. Sie ist im wesentlichen ein Compiler bzw. Debugger, kein visueller Designer. Wenn man zum Beispiel das Mobile Set für den JBuilder kennt, dann ist das zunächst einmal eine riesige Enttäuschung. Bei diesem kann man wie in herkömmlichen Anwendungen seine GUI einfach durch das Ziehen entsprechender Komponenten auf die Form erstellen. Hier beim C++Builder bzw. der Mobile Edition 1.1 geht das nicht, dementsprechend stehen auch keine Formen zur Verfügung. Man muss den gesamten Code per Hand eingeben und erhält dabei keine Unterstützung. Code-Completion oder Code-Insight funktionieren bei der Erstellung von Mobile-Anwendungen nicht.
Die Unterstützung durch die Mobile Edition besteht darin, dass man mit einem Klick die Anwendungen kompilieren, linken und im Emulator starten lassen kann, dass man alle Series60-Tools per Klick auf einen Menüpunkt starten kann und dass man den Code debuggen kann. Der Debugger erlaubt wie üblich Breakpoints zu setzen, Variablen zu inspizieren usw. Was man anfangs am meisten vermisst, ist eben die Möglichkeit, die GUI wie bei herkömmlichen Anwendungen zu gestalten. Das soll sich (laut CTIA_Borland_Tools.pdf, erhältlich auf der Nokia-Page www.forum.nokia.com) mit der Version 2.0 ändern, wobei noch nicht bekannt ist, wann diese erhältlich sein wird. Ebenfalls unklar ist, wie es sich dann mit Code-Insight und Code-Completion verhalten wird.

Abb.1: Der C++Builder 6 mit installierter C++ Mobile Edition
Series60-Anwendungen
Wie wir jetzt wissen, müssen also alle Klassen, Units usw. vom Programmierer selbst geschrieben werden. Daher soll jetzt einmal kurz dargestellt werden, aus welchen Teilen eine Series60-Anwendung im wesentlichen besteht. In der Dokumentation, die der Mobile Edition beigefügt ist, ist das zwar prinzipiell enthalten, aber es kostet doch ein wenig Mühe, um alles in den richtigen Zusammenhang zu bringen. Hilfreich sind auf jeden Fall folgende Dokumente (auch auf der Homepage von Nokia vorhanden):
- Designing_C_Applications_for_Series_60.pdf
- Developer_Guide_to_Series_60_UI_Category.pdf
- Introduction_to_Series_60_Appls_for_CPP_Developers.pdf
- Series_60_App_Framework_Handbook.pdf
- Series_60_basics.pdf
- Coding Idioms for Symbian OS.pdf
- Getting_Started_C.pdf
- sowie: An Introduction to the SymbianOS platform for Palm OS developers (von der metrowerks-Homepage)
Die Anwendungen, die man mit dem Series60-Framework schreibt, laufen auf dem SymbianOS-Betriebssystem. Dafür gibt es auch schon viele Klassen, daher ist es sinnvoll, über selbige Bescheid zu wissen. Dieses Framework nennt sich
UIKON beziehungsweise
EIKON. Das Series60-Framework leitet sich von
UIKON/EIKON ab und nennt sich
AVKON. Man erkennt dieses an den Klassennamen: Die oberste Hierarchie hat
Apa im Namen, zum Beispiel
CApaApplication. Die abgeleiteten
EIKON-Klassen haben
Eik im Namen (die von
CApaApplication abgeleitete Klasse heißt
CEikApplication) und die davon abgeleiteten
AVKON-Klassen haben
Akn im Namen (dementsprechend
CAknApplication, abgeleitet von
CEikApplication). Die Unterschiede sind manchmal sehr gering, sodass es von der Funktionalität her oftmals keinen Unterschied macht, welche Klassen man benutzt. In der Regel werden es die
AVKON-Klassen sein, häufig aber auch die
EIKON-Klassen, je nachdem, ob man eine Series60- oder SymbianOS-Anwendung vor sich hat, wobei eine Series60-Anwendung automatisch eine SymbianOs-Anwendung ist, eine SymbianOS-Anwendung aber aufgrund der eventuellen Funktionalitätsunterschiede nicht unbedingt eine Series60-Anwendung.
Jede Series60/SymbianOS-Anwendung hat zunächst einmal eine
bld.inf-Datei. Sie ist auch die, die angegeben werden muss, wenn man eine Mobile-Anwendung importiert. Sie enthält
Build-Informationen, was hier meistens bedeutet, dass sie auf ein
mmp-File zeigt. Dieses
mmp-File ist die wichtigste Datei, da sie alle notwendigen Informationen zum Projekt enthält, wie zum Beispiel die Namen der Source-Dateien, der einzubindenden Bibliotheken oder den Namen der zu erstellenden Anwendung (mit ihrer Hilfe erstellt der C++Builder auch die BPR/BPG-Datei, wenn man eine Mobile-Anwendung importiert). Diese beiden Dateien sind auch in der Series60-Hilfe der Mobile Edition beschrieben. Daneben besteht die Anwendung natürlich aus den verschiedenen
cpp/h-Dateien, die den Code enthalten. Zusätzlich enthält sie auch ein
rss-File, eine sogenannte
Resource-Datei. In ihr sind gewisse Grundinformationen zur Anwendung angegeben, aber auch und das ist viel wichtiger, die graphischen Komponenten (Menüs etc.) beschrieben. Diese können wiederum gewisse Konstanten etc. verwenden, die, wenn sie auch an anderer Stelle verwendet werden sollen, in einer
hrh-Datei abgelegt werden. Die Dateiendungen sind Konvention, man kann auch andere verwenden. Die Anwendung selbst besteht prinzipiell aus folgenden Segmenten:
- Einer Einsprungfunktion, die die Anwendung erzeugt,
- der Anwendung, die eine Klasse vom Typ CApaApplication darstellt und ein Objekt vom Typ CEikDocument erzeugt,
- dem Dokument vom Typ CEikDocument, das ein Objekt vom Typ CEikAppUi erzeugt,
- dem User Interface(UI), welches vom Typ CEikAppUi ist und eine View vom Typ CCoeControl erzeugt,
- der View, die vom Typ CCoeControl ist.
Diese Segmente haben folgende Bedeutung: Die Anwendung (
CApaApplication) kann die Eigenschaften zum Programmstart festlegen. Auf jeden Fall aber muss sie das Dokument erzeugen. Daher muss die Funktion
CreateDocumentL überschrieben werden. Zudem muss die Funktion
AppDllUid überschrieben werden, die eine eindeutige Identifikationsnummer zurückliefert. Das Dokument kann die persistenten Daten der Anwendung enthalten. Jede Anwendung muss ein Dokument enthalten, selbst wenn es nur dazu benutzt wird, um das UI zu erzeugen (was es mindestens tun muss). Dazu muss die Funktion
CreateAppUiL überschrieben werden. Das UI enthält die eigentliche Funktionalität, indem es auf Eingaben reagiert usw. Dazu wird die Funktion
HandleCommandL überschrieben. Die View schließlich stellt die Informationen dar. Bei ihr muss die Funktion
Draw überschrieben werden. Eine Anwendung kann viele Views enthalten, typischerweise stellt eine View quasi ein Formular einer Mobile-Anwendung dar.
Wenn wir eine Series60-Anwendung schreiben, müssen wir die eben genannten Klassen schreiben und (mindestens) die angegeben Funktionen überschreiben, wobei man natürlich auch beliebige eigene Funktionen hinzufügen kann. Das ganze wird deutlicher werden, wenn wir den Code für die Anwendung schreiben. Die Trennung in ein UI und eine oder mehrere View-Klassen liegt daran, dass an das Dokument eigentlich ein Model-View-Controller-Pattern gehängt wird. In unserem Beispiel werden wir das Model weglassen und nur die zuvor genannten Klassen schreiben.

Abb. 2: Der Handy-Emulator
Die Beispielanwendung
Nach all der Theorie jetzt erst mal ein wenig Praxis. Im folgenden werden wir also fünf Units schreiben (eine für die Einsprungfunktion und vier für die Klassen), das
mmp-File anpassen (wo wir diese fünf Units angeben werden), das
rss-File schreiben (mit den Menüs etc.) und dazu noch eine
hrh-Datei mit Konstanten für die
rss-Datei. Die Beispielanwendung wird ein Hauptmenü darstellen mit einem Menüpunkt, unter dem zwei Untermenüpunkte zu finden sind, die man anwählen kann und die dann einen Text auf dem Bildschirm darstellen.
Angefangen wird, indem unter Datei / Neu / Weitere das Tab
Mobile angewählt wird und dort auf
New Mobile Application geklickt wird. Unter
Project Type kann man ein nacktes neues Projekt erzeugen (
Blank Application Template) oder eine Hello World-Anwendung. Wir bleiben bei dem
Blank Application Template. Unter
Project Name und
Directory Name werden sinnvolle Einträge für den Projektnamen und das Verzeichnis eingetragen. Die UID2 und UID3 werden automatisch vom C++Builder gewählt und nicht verändert (UID3 muss eindeutig jede Anwendung identifizieren). Ein Klick auf OK erzeugt die
mmp-Datei, die Datei
bdl.inf und eine
mpo-Datei, die der C++Builder zur Organisation benutzt. Ansonsten ist kein Code erzeugt worden (außer der
xyz_Sources.cpp, in der nur ein
#define steht).
Wir erzeugen eine neue Unit (Datei / Neu / Unit), die unter dem Namen
main.cpp abgespeichert wird. Sie soll die Einsprungfunktion enthalten. Sie heißt bei Series60-Anwendungen nicht
main wie bei normalem C++, sondern
NewApplication mit Rückgabetyp
CApaApplication*. Sie erzeugt per Operator
new ein Objekt vom Typ
CApaApplication und liefert dieses zurück. Dadurch wird die eigentliche, auszuführende Anwendung erzeugt. Unsere Anwendungsklasse wird
Application heißen und in einer weiteren Unit mit dem gleichen Namen stehen. Des weiteren braucht jede Series60-Anwendung noch eine Funktion E32Dll als Einsprungfunktion für DLLs, die hier aber nur eine Konstante zurückliefert, die besagt, dass kein Fehler aufgetreten ist. Der Code der
main.cpp lautet daher:
#include "Application.h"
GLDEF_C TInt E32Dll(TDllReason)
{
return KErrNone;
}
EXPORT_C CApaApplication* NewApplication()
{
return (new Application);
}
In der
Application.h wird auch die Unit eingebunden, in der
KErrNone und alle anderen Typen definiert werden. Die Header-Datei zur
main.cpp wird nicht benutzt. Der hier dargestellte Code ist Standard für die Einsprungfunktionen und ist deshalb auch genau in dieser Form bei den ganzen Demo-Anwendungen des Series60-Frameworks zu finden.
Jetzt wird eine neue Unit erzeugt,
Application.cpp, in der die Klasse Application geschrieben wird. Sie muss sich von
CApaApplication ableiten und das Dokument erzeugen. Die Funktionen, die diese Klasse mindestens überschreiben muss, sind
CreateDocumentL und
AppDllUid. In die Headerdatei
Application.h kommt daher folgender Code:
#ifndef ApplicationH
#define ApplicationH
#include <aknapp.h>
class Application : public CAknApplication
{
public:
TUid AppDllUid() const;
protected:
CApaDocument* CreateDocumentL();
};
#endif
Unsere Application-Klasse leitet sich von
CAknApplication ab und damit von
CApaApplication und überschreibt die angegeben Funktionen. Es ist wichtig, hier genau die Signaturen der Funktionen einzuhalten (insbesondere das
const vergisst man leicht). Dieser Code ist der Minimalcode, den jede Anwendung enthalten muss. Für Details sei wieder auf die SDK-Hilfe verwiesen. In die
Application.cpp kommt folgender Code:
#include "Document.h"
#include "Application.h"
static const TUid KUidMyApp = {0x0100006C};
CApaDocument* Application::CreateDocumentL()
{
CApaDocument* document = Document::NewL(*this);
return document;
}
TUid Application::AppDllUid() const
{
return KUidMyApp;
}
Das Dokument wird in einer Unit
Document.cpp definiert werden. Die Konstante
KUidMyApp wird von der Methode
AppDllUid zurückgeliefert. Ihr Wert muss an den in der
mmp-Datei angegebenen UID3-Wert angepasst werden. In der
CreateDocumentL-Methode wird das Dokument erzeugt und zurückgeliefert. Wie man sieht, wird dazu eine statische Methode
NewL aufgerufen und nicht der Konstruktor direkt. Diese Methode wird im Zusammenhang mit der Klasse
Document beschrieben. Die eben geschriebenen Methoden sowie alle weiteren, die ebenfalls überschrieben werden müssen, werden übrigens vom Framework automatisch aufgerufen. Daher wird kein entsprechender Aufruf in dem von uns eingegebenen Code auftauchen.
Jetzt muss die Dokument-Klasse geschrieben werden. Dazu wird eine weitere Unit erzeugt. Diese bekommt den Namen
Document.cpp. In die Header-Datei kommt wie üblich die Klassendeklaration. Die Klasse enthält eine statische Methode, über die sie erzeugt wird. Der Grund liegt darin, dass hier eine sogenannte Two-Phase-Construction durchgeführt werden könnte. Damit bezeichnet man den Mechanismus, wie man bei Series60/SymbianOs-Anwendungen Objekte aus anderen Klassen heraus erzeugt. Würde man diese im Konstruktor erzeugen, so könnte man bei einem Speicherfehler den bereits belegten Speicher nicht wieder freigeben. Daher darf in den Konstruktor nur Code, der keinen Speicher anlegt beziehungsweise anfordert, also im wesentlichen nur Initialisierungen von primitiven Datentypen. Alles andere lagert man aus in eine Funktion
ConstructL. Der Aufruf des Konstruktors erfolgt üblicherweise in einer statischen Funktion
NewL, die man an dessen Stelle aufruft. Man benutzt dann auch nicht
new XY, sondern
new (ELeave) XY, eine überladene Variante des
new-Operators, die sicherstellt, dass Speicherallokationsfehler korrekt behoben werden (für Details siehe wie immer Series60-SDK, in diesem Zusammenhang besonders die
Coding Idioms). Wenn Funktionen Speicher anfordern und dann andere Funktionen aufrufen, die das ebenfalls tun, dann legt man die bereits erzeugten Objekte vor dem Aufruf der Funktion üblicherweise auf einem sogenannten
Clean-Up-Stack ab. Das dient dazu, dass das vor dem Funktionsaufruf erzeugte Objekt bei einem Speicherallokationsfehler in der aufgerufenen Funktion auch wieder freigegeben werden kann. In der Klasse
AppView wird das später noch demonstriert werden.
Hier im Dokument brauchen wir das alles noch nicht, aber normalerweise taucht an dieser Stelle das eben skizzierte Problem zum ersten Mal auf. Daher wird hier die bereits erwähnte statische Methode
NewL deklariert sowie der Konstruktor und eine Methode
CreateAppUiL, die das UI erzeugen wird. Der Code der Header-Datei sieht folgendermaßen aus:
#ifndef DocumentH
#define DocumentH
#include <eikdoc.h>
class AppUi;
class CEikApplication;
class Document : public CEikDocument
{
public:
static Document* NewL(CEikApplication& aApp);
private:
Document(CEikApplication& aApp);
CEikAppUi* CreateAppUiL();
};
#endif
Die Methode
NewL bekommt als Parameter das Application-Objekt der aktuellen Anwendung, das an den Konstruktor weitergereicht wird, der dieses Objekt standardmäßig an den Basisklassenkonstruktor von
CEikDocument weiterreicht (der Aufruf erfolgt in der Methode
CreateDocumentL der Klasse
Application). Die Methode
CreateAppUiL erzeugt einfach das UI (sie wird vom Framework automatisch aufgerufen; das ist möglich, da sie in der Basisklasse public ist). Der Code in der
Document.cpp ist daher der folgende:
#include "AppUi.h"
#include "Document.h"
Document* Document::NewL(CEikApplication& aApp)
{
Document* self = new (ELeave) Document(aApp);
return self;
}
Document::Document(CEikApplication& aApp) : CEikDocument(aApp)
{
}
CEikAppUi* Document::CreateAppUiL()
{
CEikAppUi* appUi = new (ELeave) AppUi;
return appUi;
}
Die Basisklasse besitzt noch Methoden, mit denen man Daten abspeichern und laden kann, aber wir benutzen hier das Dokument nur zur Erzeugung des UI. Dessen Code kommt in eine Unit namens
AppUi.cpp, daher wird eine neue Unit erzeugt und unter diesem Namen abgespeichert. Es wird hier eine Klasse
AppUi definiert, die sich, wie vom Framework vorgeschrieben, von
CAknAppUi ableitet. Sie enthält einen Konstruktor, einen Destruktor, eine Methode
ConstructL (den richtigen Konstruktor), sowie eine Methode
HandleCommandL:
class AppView;
class AppUi : public CAknAppUi
{
public:
void ConstructL();
AppUi();
~AppUi();
void HandleCommandL(TInt aCommand);
private:
AppView* iAppView;
};
Die Methode
ConstructL muss von der Basisklasse die Methode
BaseConstructL aufrufen. Dadurch wird mit Hilfe der Ressource-Datei (beziehungsweise dem vom Ressource-Compiler aus dieser Datei generierten Code) die erste Maske erstellt und auf dem Handy dargestellt. Des weiteren muss sie die View erstellen und die View auf dem Control Stack ablegen. Alle Elemente auf dem Control Stack werden benachrichtigt, wenn gewisse Ereignisse (meistens Tastendrücke) eingetreten sind. Sie können dann darauf reagieren, in dem bestimmte Methoden (bei Tastendrücken
OfferKeyEventL) überschrieben werden.
OfferKeyEventL funktioniert ähnlich wie die
OnKey...-Ereignisse bei VCL-Anwendungen. Will man aber auf allgemeine Ereignisse reagieren, dann kann man das schon in der
AppUi-Klasse tun, indem man die Methode
HandleCommandL überschreibt. Eigentlich gilt das eben gesagte nur für sogenannte Controls, also Klassen, die sich von
CCoeControl ableiten. Views vom Typ
CAknView zum Beispiel werden mit
AddViewL bei einem UI vom Typ
CAknViewAppUi registriert oder wie hier, bei einem UI vom Typ
CAknAppUi und damit
CEikAppUi, mit Hilfe von
RegisterViewL, wobei die View in diesem Falle vom Typ
MCoeView sein muss. Views müssen dann eine Funktion überschreiben, die eine eindeutige ID zurückliefert (z.B.
ViewId bei
CAknView), und mit einer weiteren Methode wird dann unter Zuhilfenahme der IDs zwischen den Views umgeschaltet (bei
CAknViewAppUi macht das
ActivateLocalViewL). Die Views selber haben auch wieder Methoden, die bei bestimmten Ereignissen aufgerufen werden. Dieses Vorgehen kommt zur Anwendung, wenn man bei einer Applikation zwischen verschiedenen Screens hin- und herschalten möchte. Das wollen wir hier nicht, deswegen arbeiten wir mit Controls und überschreiben in dem UI die Methode
HandleCommandL. Sie bekommt eine ID übergeben, die das eingetretene Ereignis repräsentiert. Hier in unserem Beispiel sind das Werte, die im Zusammenhang mit einem angeklickten Menüpunkt stehen. Welcher Wert bei welchem Menüpunkt übermittelt wird, wird später in der Ressource-Datei festgelegt. Diese Werte werden daher ausgelagert in eine
hrh-Datei, die wir in die vorliegende Unit einbinden müssen (
Projektname.hrh wird sie typischerweise genannt). Der Code in der Unit hat insgesamt folgende Form (Listing 1).
Listing 1 #include <avkon.hrh>
#include "AppUi.h"
#include "AppView.h"
#include "Projektname.hrh"
void AppUi::ConstructL()
{
BaseConstructL();
iAppView = AppView::NewL(ClientRect());
AddToStackL(iAppView);
}
AppUi::AppUi()
{
}
AppUi::~AppUi()
{
if (iAppView)
{
RemoveFromStack(iAppView);
delete iAppView;
iAppView = NULL;
}
}
void AppUi::HandleCommandL(TInt aCommand)
{
switch(aCommand)
{
case EAknSoftkeyExit:
Exit();
break;
case FirstID:
{
iEikonEnv->InfoWinL(_L("first pressed"), _L("everything works!"));
break;
}
case SecondID:
{
iEikonEnv->InfoMsg(_L("second pressed"));
break;
}
default:
break;
}
}
Die Werte nach den case-Anweisungen werden bei der Ressource-Datei erläutert. Das globale Objekt
iEikonEnv ist mit dem globalen Application-Objekt in VCL-Anwendungen vergleichbar. Es bietet unter anderem zwei Methoden an, mit denen man Informationen auf dem Bildschirm einblenden kann:
InfoWinL zeigt eine kleine Botschaft in einem extra Fenster,
InfoMsg zeigt einen Text für wenige Sekunden am oberen Bildschirmrand an. Das Makro
_L dient dazu, Strings direkt im Programmcode anzugeben (wird hier bei Mobile-Anwendungen normalerweise über Konstanten und
Resource-Files gelöst). Der Code in dieser Methode sorgt also dafür, dass das Programm entweder beendet wird oder, je nach übergebenen Wert, ein Text erscheint. Sie stellt damit die Methode dar, die die eigentliche Funktionalität enthält. Es können von hier aus natürlich noch andere Funktionen aufgerufen werden.
Die letzte Klasse, die noch fehlt, ist die View-Klasse. Hier muss zumindest die Draw-Methode überschrieben werden. Ansonsten erzeugt sie in diesem Beispiel ein Fenster, in dem die Anwendung ausgeführt wird. Über die Funktion
NewL wird eine Instanz der Klasse erzeugt; hier passiert das in zwei Schritten über die Funktionen
NewL bzw.
NewLC und
ConstructL. Die Headerdatei hat folgende Form (Name der neuen Unit:
AppView.cpp):
#ifndef AppViewH
#define AppViewH
#include <coecntrl.h>
class AppView : public CCoeControl
{
public:
static AppView* NewL(const TRect& aRect);
static AppView* NewLC(const TRect& aRect);
~AppView();
void Draw(const TRect& aRect) const;
private:
void ConstructL(const TRect& aRect);
AppView();
};
#endif
In die zugehörige
cpp-Datei sehen Sie in Listing 2.
Listing 2 #include "AppView.h"
AppView* AppView::NewL(const TRect& aRect)
{
AppView* self = AppView::NewLC(aRect);
CleanupStack::Pop();
return self;
}
AppView* AppView::NewLC(const TRect& aRect)
{
AppView* self = new (ELeave) AppView;
CleanupStack::PushL(self);
self->ConstructL(aRect);
return self;
}
void AppView::ConstructL(const TRect& aRect)
{
CreateWindowL();
SetRect(aRect);
ActivateL();
}
AppView::AppView()
{
}
AppView::~AppView()
{
}
void AppView::Draw(const TRect& /*aRect*/) const
{
CWindowGc& gc = SystemGc();
gc.Clear(Rect());
}
Die Methode
NewL ruft zunächst die Methode
NewLC auf. Wenn man Objekte auf dem Clean-Up-Stack ablegt, dann verteilt man dieses manchmal auf zwei Funktionen, wobei man die, die ein Objekt auf dem Stack ablegt, mit einem Buchstaben
C versieht.
NewLC erzeugt also die View über den überladenen
new-Operator und legt sie auf dem Clean-Up-Stack ab. Dann wird die Funktion
ConstructL aufgerufen, die die eigentliche Initialisierung darstellt. Hier wird ein Fenster erzeugt, dessen Größe durch die Variable
aRect bestimmt wird. Über
ActivateL wird das Fenster gemalt. Dadurch kann man den Zeitpunkt der Erzeugung der View von dem ihrer Darstellung trennen.
NewL löscht dann die View vom Clean-Up-Stack und liefert einen Pointer auf das Objekt zurück. Die Methode Draw löscht nur den gesamten Bildschirm. Sie wird immer dann aufgerufen, wenn es erforderlich ist, das Fenster der Anwendung neu zu malen. Der hier dargestellte Code stellt erneut das absolut Notwendige dar, was die View-Klasse enthalten muss.

Abb. 3: Die Beispiel-Anwendung nach dem Start
Von der Programmierung sind wir an dieser Stelle eigentlich fertig. Es stellt sich die Frage, woher das Betriebssystem jetzt weiß, was eigentlich dargestellt werden soll, wenn die Anwendung gestartet wird. Dieses wird jetzt in der Ressource-Datei (mit Namen
Projektname.rss) festgelegt. Des weiteren muss dem externen Compiler noch mitgeteilt werden, welche Dateien kompiliert werden sollen. Dazu werden wir anschließend die
mmp-Datei anpassen.
Die GUI
Wie bereits bemerkt wurde, ist es derzeit nicht möglich, über einen visuellen Designer die Masken der Anwendungen festzulegen. Statt dessen muss dazu ein
Resource-File geschrieben werden. In dieses werden Informationen derart eingetragen, dass man auf (bereits vordefinierte) Structs verweist und angibt, welchen Wert die Felder des entsprechenden Structs annehmen soll. Dabei wird man wiederum vordefinierte und selbst definierte Konstanten benutzen. Um das zu erläutern, soll zunächst erst mal die Ressource-Datei für dieses Projekt gezeigt werden. Dazu wird eine neue Unit oder Textfile angelegt mit (Dateiname
Projektname.rss) und folgende Zeilen eingefügt (Listing 3).
Listing 3 NAME FGVC
#include <eikon.rh>
#include <eikon.rsg>
#include <avkon.rh>
#include <avkon.rsg>
#include "Projektname.hrh"
RESOURCE RSS_SIGNATURE { }
RESOURCE TBUF { buf=""; }
RESOURCE EIK_APP_INFO
{
menubar = my_menubar;
cba = R_AVKON_SOFTKEYS_OPTIONS_EXIT;
}
RESOURCE MENU_BAR my_menubar
{
titles =
{
MENU_TITLE {menu_pane = my_menu; }
};
}
RESOURCE MENU_PANE my_menu
{
items =
{
MENU_ITEM {command = FirstID; txt = "push me";},
MENU_ITEM {command = SecondID; txt = "or push me";},
MENU_ITEM {command = EAknSoftkeyExit; txt = "Exit";}
};
}
Am Anfang wird über
NAME vier Zeichen übergeben, die für jede Anwendung eindeutig sein müssen. Der Grund hierfür liegt darin, dass aus dieser Datei eine Menge Konstanten erzeugt werden, und damit die eindeutig sind, muss man diese vier Zeichen übergeben, die mit bei der Werterzeugung der Konstanten benutzt werden. Dann folgen in der Regel die notwendigen
#include-Anweisungen, durch die die Structs (
rh-Dateien) oder weitere Konstanten (
rsg-Dateien) eingebunden werden. Darauf folgen eine Menge
RESOURCE-Anweisungen. Nach
RESOURCE kommt der Struct, für dessen Felder man Werte festlegen möchte und dann ein selbstgewählter Name für diese Definition. Nach
RSS_STRUCTURE kommt nichts,
TBUF legt den Standard-Dokumentnamen fest.
EIK_APP_INFO legt fest, was dargestellt wird, wenn das UI erzeugt wird. Hier sieht man, wie mit den Ressourcen gearbeitet wird. In der Regel zeigt ein
Resource-Statement auf ein weiteres, welches eventuell wiederum auf ein weiteres zeigt. Was man an dieser Stelle festlegen kann, erfährt man aus der Datei
Uikon.rh. Wir legen hier fest, welches Menü und welche Buttons am Anfang dargestellt werden. Buttons heißt hier, welche Texte unten links und unten rechts am Bildschirmrand stehen, die man mit der Handy-Tastatur anwählen kann. Die Konstante
R_AVKON_SOFTKEYS_OPTIONS_EXIT legt fest, dass unten links
Options und unten rechts
Exit steht. Sie wird übrigens in der Datei
avkon.rsg definiert. Dazu muss man anmerken, dass die Namen der Konstanten in der Regel eigentlich recht aussagekräftig gewählt wurden, aber leider nirgendwo in der Dokumentation erläutert werden. Aus diesem Grund muss man am Anfang sehr viel ausprobieren, bis man wenigstens einen kleinen Überblick erhält. Der Eintrag
menubar zeigt auf das nächste
Resource-Statement, in dem die Menübar festgelegt wird, nämlich
my_menubar. Hier gibt man unter
TITLES die Menüeinträge an. Wenn man mehrere Titel benutzt, werden diese untereinander dargestellt. Zur Abtrennung kann man auch Separators benutzen. Wir verweisen hier auf
my_menu, wo wir die Menüpunkte eintragen. Nach
command kommt die Konstante, die bei Anwählen dieses Menüpunktes an
HandleCommandL oder entsprechende Funktionen übermittelt wird. Diese definieren wir in der Datei
Projektname.hrh. Anschließend definiert
txt, welcher Text bei diesem Menüeintrag stehen soll. Auch für die Menü-Items gibt es eine Menge mehr Einstellungsmöglichkeiten (siehe zum Beispiel in der API-Referenz unter
UIKON Resources), wir benutzen hier nur die einfachsten. Wenn man mehrere Views definieren wollte, so würde man Ressourcen vom Typ
AVKON_VIEW definieren, die wie
EIK_APP_INFO aussehen, und so seine Maske definieren. Auch wesentlich komplexere Aufbauten sind hier möglich. Es gibt Radiobuttons, Checkboxes, Buttons usw. und man kann sie mit Hilfe des Ressourcen-Typs
FORM gruppieren. Im Prinzip geht man aber immer wie in diesem Beispiel vor.
Die letzte Datei, die noch fehlt, ist die
Projektname.hrh. Hier definieren wir mit Hilfe des
enum-Statements die Konstanten
FirstID und
SecondID für die Menüpunkte (
EAknSoftkeyExit wird in
avkon.hrh definiert). Über
enums werden eigentlich immer die Konstanten definiert, die durch Menüklicks etc. an die entsprechenden Funktionen gesendet werden. Der Inhalt der
hrh-Datei ist daher eine einzelne Anweisung:
enum TMyIds
{
FirstID = 1,
SecondID=2
};
Die Konstanten dürfen nicht den Wert
null haben.
Hiermit sind alle Units etc. geschrieben. Würde man jetzt das ganze kompilieren wollen (durch Druck auf den grünen Start-Button bzw. F9), so würde es einen Haufen Fehlermeldungen geben, vor allem, weil der Compiler nichts zum Kompilieren findet. Damit er das tut, muss die
mmp-Datei so angepasst werden, dass der externe Compiler weiß, welche Dateien zu kompilieren sind. Das wiederum geschieht, indem man hinter dem Schlüsselwort
SOURCE die
cpp-Dateien angibt. Des weiteren wird mit dem Schlüsselwort
RESOURCE auf das zur Anwendung gehörende
rss-File verwiesen. Zum Abschluss werden alle benötigten Bibliotheken angegeben, sodass das
mmp-File in unserem Falle folgende Form annimmt (Listing 4).
Listing 4 TARGET Projektname.app
TARGETTYPE app
UID 0x100039CE 0x0100006C
TARGETPATH \system\apps\Projektdirectory
SOURCEPATH .
SOURCE main.cpp
SOURCE Application.cpp
SOURCE AppView.cpp
SOURCE AppUi.cpp
SOURCE Document.cpp
USERINCLUDE .
SYSTEMINCLUDE \epoc32\include
RESOURCE Projektname.rss
LIBRARY euser.lib apparc.lib cone.lib eikcore.lib
LIBRARY avkon.lib
Der zweite Wert hinter UID wird natürlich bei jedem Projekt ein anderer sein, da er automatisch vom C++Builder generiert wird. Bis auf die
SOURCE-,
RESOURCE- und die zweite
LIBRARY-Anweisung sollten alle Anweisungen bereits vorhanden sein. Wenn man jetzt kompiliert, dann bekommt man im Meldungsfenster einige Fortschrittsmeldungen und am Ende hoffentlich die Mitteilung
Project build successful with warnings. Sollte das nicht der Fall sein, so wird unter dem Punkt
Missing des Compiler-Fortschrittfensters keine
null stehen, und hinter
Errors und
Fatal eventuell auch nicht. Wenn man Glück hat, kann man im Meldungsfenster mit der Maus auf entsprechende Hinweise klicken und bekommt so Auskunft über irgendwelche Tippfehler. Wenn nicht, muss man die
cmd /c perl -S abldb.pk make winsb udeb-Anweisung direkt in einem Konsolenfenster eingeben (vorher in das Projektverzeichnis wechseln). Dann erhält man (ganz am Schluss) die Compiler-Fehlermeldungen, wobei die erste in der Regel ignoriert werden kann, da sie verschwindet, wenn die anderen Fehler behoben wurden. Sehr selten tritt auch der Fall auf, dass man hier keine Hinweise bekommt, das Programm aber trotzdem nicht erzeugt wird. Wenn jedoch alles geklappt hat, und man im Compiler-Fortschrittfenster OK anklickt, dann wird der Emulator automatisch gestartet. Die eigene Anwendung ist im Ordner
Other unter dem Projektnamen zu finden. Startet man die Anwendung, so sieht man die Texte
Options und
Exit, wobei beim Anklicken von
Options ein Menü erscheint mit den zwei Einträgen, die beim Anklicken jeweils einen Text auf dem Bildschirm darstellen. Ein Klick auf
Exit beendet die Anwendung.

Abb. 4: Bildschirmausgabe, nachdem der erste Menüpunkt angeklickt wurde
Zusammenfassung
An manchen Stellen mag einem das Beispielprogramm noch etwas unklar erscheinen, aber es gibt hier nicht viel zu bemerken, da der größte Teil vom Framework automatisch erledigt wird. Die Aufgabe des Entwicklers besteht am Anfang eigentlich immer darin, die hier dargestellten Funktionen zu überschreiben. Vieles ist im wesentlichen genauso vom Framework vorgeschrieben, wie es in der vorliegenden Beispielanwendung umgesetzt wurde. Das wird verständlicher, wenn man sich einige der Demo-Anwendungen im Series60Ex-Verzeichnis anschaut (insbesondere das
Dialer- und das
Listbox-Beispiel).
Viele Fragen klären sich beim Durcharbeiten der Help-Files des SDKs. Etwas mehr Dokumentation von Nokia könnte allerdings an dieser Stelle nicht schaden. Das Framework ist sehr mächtig und bietet für eine Vielzahl von Problemen bereits Lösungen an, aber ohne Dokumentation ist es sehr schwer, diese zu benutzen. Zudem vermisst man, wie bereits angesprochen, den visuellen Designer. Es ist prinzipiell möglich, sehr ausgefallene GUIs zu konstruieren, aber über die Ressource-Dateien ist das eine sehr mühsame Arbeit. Die Programmentwicklung wird noch dadurch erschwert, dass man Compiler-Fehler meistens nur über die Kommandozeile in Erfahrung bringen kann.
Die Mobile Edition 1.1 lässt den Programmierer einen Eindruck gewinnen, was man mit Series60/SymbianOS alles machen kann und weckt sehr schnell den Hunger nach mehr. Sollte in der Version 2 wirklich ein visueller Designer zur Verfügung stehen und die eben genannten Mängel behoben worden sein, dann wird das ganze eine sehr gute Sache werden. Hoffentlich lassen sich Nokia und Borland nicht all zu lange Zeit mit der Version, denn die derzeitige Version 1.1 verlangt dem Entwickler noch eine Menge Mühe ab.