Donnerstag, 4. Dezember 2008

entwickler.com Magazine Konferenzen Akademie Entwickler-Forum Jobbörse Bücher
Software & Support Verlag




April 2006
aus Der Entwickler Ausgabe: 04.2003
Handy-Programmierung mit dem C++Builder 6
Eine Übersicht über die Borland C++ Mobile Edition 1.1
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.


    Hat Ihnen dieser Artikel gefallen? Dann abonnieren Sie das Entwickler Magazin direkt über unser Online-Formular.



zur vorherigen Seite
zurück
an den Anfang der Seite
nach oben
Diesen Artikel drucken
drucken
Diesen Artikel weiterempfehlen
empfehlen
Software & Support Verlag GmbH