In this article, we will explore the creation of a custom widget for an automotive application using the Zinzala SDK. Introduced in April 2003, Zinzala is a BeOS flavored Software Development Kit for QNX. It has improved since then, taking on small features inspired by the Symbian OS. Because Zinzala brings in the benefits of object oriented programming, it can leverage the quality of your QNX RTOS based embedded products.
Intended audience
This article is intended for C++ developers who are familiar with Graphical User Interface development. No specific knowledge of QNX is required. However, some knowledge of the Zinzala SDK will be useful, so check out the updated version of Introducing Zinzala.
Note to the reader
The latest version of the Zinzala SDK can be downloaded from the hexaZen web site. In the demo folder of this release, there is a ready to build version of the application described in this article. Feel free to play with the demo after reading this article.
1. Overview
Over the past few years, QNX has been making strong in-roads into the automotive market. Cars manufactured by Audi, DaimlerChrysler and Saab are currently being shipped with infotainment systems built on top of the RTOS made by QNX Software Systems (QSS). The recent purchase of QSS by Harman International Industries can be seen as proof of the pioneering position taken by QNX over the recent years. This endorsement by Harman is a sign of the quality of this embedded Operating System.
Like many other embedded systems, an infotainment system relies on a custom Graphical User Interface (GUI); most often built on top of an existing GUI framework. For example, in the smartphone world, the Symbian OS provides Uikon, which is a basic framework that each licensee can use to build their own set of widgets or customize the existing ones. In this very competitive market, differentiating your products from the concurrent ones is an absolute priority. Through different UI styles and attractive application offerings, each Symbian licensee will try to inforce its own branding. This also applies to the automotive industry as well. However, the look of the infotainment may not be at the top of the buyer’s list of considerations during a car purchase.
In this article, we are going to explore the creation of a custom widget for QNX using the basic components provided by the Zinzala SDK. Here is a screenshot of what we will build:
This widget is only one element of a multimedia console, much like the one we would expect to find in any kind of multimedia player. However, its implementation will offer us an interesting ground to explore in this article.
The big picture
Let’s suppose that we are building the application framework for a QNX based in-car device, similar to the ones made by G-Net. Our console widget is going to be one of the many custom widgets that will be available to the application developers. For instance, it could be used by the music and video player. Each application will use class derivations in order to program the console widget to perform specific actions when the user interacts with the GUI. For instance, causing the current audio track to start playing with the user presses the Play button.
The application framework (let’s call it Carrozza) that we will be using to build our applications is not limited to the GUI side of the embedded system. It could also provide libraries for audio playback, data persistence or networking. This framework is simply an extra layer, intended to unify the development of every application by offering architects and developers all the components they will need. Of course, this doesn’t forbid the use of some of the classes available from the lower layers. Providing a set of components designed to fit the common needs of all applications, help reduce the risk of duplication of code. This also leads to a modular solution where components can be selected, combined and reused. This provides a powerful, consistent and efficient platform.
Here’s a representation of the layering:
Although Photon is part of the QNX platform, I have placed it on its own layer to show Zinzala’s dependencies. We will expect application developers to only go as low as the Zinzala layer, which is composed of Cincinella and Farfalla. Low level components, such as a CD player control class, will probably require access to the QNX API. However, they will be made available to the application developers from the top layer.
Zooming in …
Now that we have pitched the overall view, let’s go back to the console widget. We will walk through the construction of the widget, and then show how to integrate it to a simple test application. Here’s what we are going to look at:
- How to create a shared object from the raw skin graphics
- How the widget will use the skin
- How to render the console and each button’s states
- How to render an animation effect for the active button
- How to handle user inputs
- How to support the change of skin on the fly
Graphics
All the graphics we will be using have been created by hexaZen for this article. In a real system, images such as these would be provided to the development teams by the UI designers to ensure consistency across the entire product line.
Here is a green skin for example:
Button states (normal, dimmed and pressed) |
|
64×64 pixels |
---|---|---|
Active button animation |
|
64×64 pixels |
Button labels (normal) |
|
25×25 pixels |
Button labels (dimmed) |
|
25×25 pixels |
Console background | 321×98 pixels |
The following figure indicates the position of each of the 4 buttons:
Like many other custom UI, the size and position of our UI elements will be fixed, even when we change skin. This means that if we were to reuse the same widget on a different product, perhaps one with a bigger screen, we may have to adapt it instead of just reusing it. A better solution would be to use a scalable UI framework, which would allow each element of the UI to adapt its position and size according to the form factor and UI layout of the product. Such frameworks are not widely in use, although they do increase the reusability of the applications (develop once for any screen size and layout) from a product to another one. This is about to change for some embedded systems such as the smartphones based on Symbian Series 60.
A few considerations
Before we get down to business, let’s talk a little about the coding conventions and practices that we are going to use. The coding style might be new to you, unless you have already been exposed to some Symbian programming. We will also be using exceptions for error handling associated with two very interesting things called two-phase construction and cleanup stack.
The need for both mechanisms arise from the fact that an embedded system must be stable and must run for a long period of time. This puts pressure on the applications to avoid memory leak, and to be robust to situations where resources are low. Error conditions must also be handled in a safe and appropriate fashion.
The cleanup stack provides a way of cleaning objects allocated on the heap, whose pointers will be lost when an exception occurs. Because a class destructor will not be called on a partially created object, a C++ constructor should not throw an exception in order to avoid memory leak. It is recommended that you use a two-phase construction. This also gives the opportunity to push objects on the cleanup stack. If you have some Symbian experience, it will look very familiar…
Since we are only focusing on building a custom widget, we are not going to discuss the purpose and usefulness of the two-phase construction and cleanup stack in detail. Instead, you may want to read the first Zinzala newsletter or a document on Symbian Application Development to learn more about them.
The code sequences that we will be addressing in this article have been annotated in order to explain particular points. Don’t get confused if it doesn’t look like it could compile ๐ A link to the complete version will be given before each annotated sequence.
2. Implementing skinability
In order to support skinability in the widget, we need to answer these two questions:
- How do I transform a graphic into something useful for the widget?
- How will the widget access it ?
Turning a GIF into a resource
Instead of letting the widget load each graphic file, we are going to process and store them into one shared object (a .so). This solution saves the widget the need of transforming each image from a GIF file to a cBitmap object each time we construct it. It also presents a more elegant solution, since all the graphics will be in one file. Now, we are going to transform all our graphics into a set of resources. For that purpose we are going to create a simple tool, which outputs given GIF files into a C++ file to be compiled. We will be calling it img2res.
Loading an image into a cBitmap
First, we will see how we can load a given graphic file into a cBitmap object which defines a basic bitmap. The current version of the Zinzala SDK does not provide any mechanism to do so in the way BeOS was using translators, so we are going to use a little bit of pure Photon API.
Here’s the implementation of the function LoadBitmap():
cBitmap *LoadBitmap(const tChar *aFilename) { PhImage_t* lImage = NULL; PiLoadInfo_t lInfo; PiIoHandler_t* lHandler; const tChar* lExt; tUint16 lPos; // extract extension of the image if(sStrTools::FindLast(aFilename,'.',&lPos)) { |
In order to load an image, we need to find out what the image format is from its file extension. By using the static class sStrTools, which provides many methods to manipulate strings, we can search for the position of the last dot in the string. The file extension will be everything after the last dot. If that character cannot be found, sStrTools::FindLast() will return false.
// get the handler for the image format lExt = &aFilename[lPos+1]; lHandler = PiIoGetHandlerByExt(lExt); if(lHandler) { |
Once we have got the file extension in the variable lExt, we can call the Photon function PiIoGetHandlerByExt() to give us the handler that will be used to load the image. If the file format is not supported lHandler will be set to NULL. Photon currently provide only handlers for GIF, JPEG, BMP and PNG.
memset(&lInfo,NULL,sizeof(PiLoadInfo_t)); lInfo.flags = Pi_IO_SHMEM; lInfo.shmem_threshold = 100; lInfo.handler = lHandler; lInfo.filename = aFilename; // load the image lImage = PiLoadImage(&lInfo); |
PiLoadImage() loads the image from the file into a Photon image structure PhImage_t. A structure of type PiLoadInfo_t is used as an argument to pass all the required information to the function, such as the filename and the handler to be used. The flag Pi_IO_SHMEM indicates that the image to be created should use shared memory, if its size is above a given threshold. Using shared memory for the image data, which are rows of pixels, is a nice idea when the bitmap is to be displayed. The Photon server will access the image’s data in the shared memory when it is draw, instead of having the application send it. This is not really required here, since we will not be displaying the image.
if(lImage) { cBitmap* lBitmap; // create a cBitmap and return it lBitmap = new cBitmap(lImage); if(!cBitmap::VerifyD(lBitmap)) return lBitmap; else return NULL; } else return NULL; |
If the image was successfully loaded into lImage, we can use it to create a cBitmap object. The static method VerifyD() checks the validity of the object. If the object has been allocated and is invalid, it will be deleted by that method. Otherwise, the method will return kErrNone. Since the cBitmap assume ownership of the Photon image, we don’t need to delete it.
} else return NULL; } else return NULL; } |
From cBitmap to resource
Earlier, we decided that all the graphics are going to be stored as resources in a shared object. For that reason, we need to store all the required information in a C++ file. The following type will provide everything we need to instantiate a cBitmap object:
typedef struct tBitmapResource { tUint32 iWidth; // width of the bitmap tUint32 iHeight; // height of the bitmap tUint32 iIndex; // index of the transparent color in the palette tUint16 iColors; // number of color in the palette tUint32 iLength; // length in bytes of the bitmap's data tUint8* iBits; // bitmap's data tUint32* iPalette; // palette of colors } tBitmapResource; |
Here’s the function DumpBitmapToFile() that will output a bitmap into a C++ compilable form:
void DumpBitmapToFile(cFile &aHeader,cFile &aCode,cBitmap *aBitmap,tColor *aPalette, tUint16 aColors,const char *aName) { tUint16 lWidth,lHeight; tUint8 lIndex; tUint32 lLength; tUint8* lBits; PgColor_t lColor; aBitmap->GetTransparent(lIndex); aBitmap->GetSize(lWidth,lHeight); lLength = aBitmap->BitsLength(); lBits = aBitmap->GetBits(); |
First, we retrieve the size, transparency and pixel data from aBitmap. The local variable lLength will contain the length of the data in bytes.
aHeader.Write("extern const tBitmapResource kRscBitmap%s;\n\n",aName); |
aHeader is a reference to the cFile object pointing to the C++ header file. Here, we define a constant of type tBitmapResource for the bitmap we are currently outputting.
aCode.Write("tUint8 kBitmap%sBits [] = {\n\n\t",aName); for(tUint16 k=0;k<lLength;k++) { lIndex = lBits[k]; if(k + 1 == lLength) aCode.Write("0x%02x",lIndex); else aCode.Write("0x%02x,",lIndex); if(!((1+k) % 16)) aCode.Write("\n\t"); } aCode.Write("};\n\n"); |
Writing each pixel index color is fairly easy. Just as a reminder, we are using palette based graphics. We simply loop over all the pixels and write their hexadecimal value. For better legibility, we output these values into lines 16 pixels wide.
aCode.Write("tUint32 kBitmap%sPalette[%d] = {\n\n\t",aName,aColors); for(tUint16 i=0;i<aColors;i++) { lColor = PgARGB(aPalette[i].iAlpha,aPalette[i].iRed,aPalette[i].iGreen, aPalette[i].iBlue); aCode.Write("0x%06x,",(tUint32)lColor); if(!((1+i) % 8)) aCode.Write("\n\t"); } |
Outputting the palette of color used by the bitmap is not very complicated. However, there is a little trick to know. If we write the RGB color in the form of an unsigned 32 bit integer, we can use them directly when instantiating the bitmap. By using the Photon macro PgARGB(), we insure that the color will be coded in a format that can be directly understood by the underlying Photon. To improve the legibility of the C++ file, we will also output only 8 colors per lines.
aCode.Write("\n};\n\n"); aCode.Write("const tBitmapResource kRscBitmap%s\t= {\n",aName); aCode.Write("\t%d,%d,0x%02x,%d,%d,kBitmap%sBits,kBitmap%sPalette\n};\n",lWidth, lHeight,lIndex,aColors,lLength,aName,aName); } |
Last but not least, in the C++ file we declare the same constant that we have previously defined in the header file. This time, we fill in all the data specifics to the bitmap.
Now, how will it look like in the C++ file ?
tUint8 kBitmapStopDBits [] = { 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01, 0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, ... ... ... 0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03, 0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x03,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00 }; tUint32 kBitmapStopDPalette[4] = { 0x88c188,0x82bd82,0x77b577,0x6bad6b, }; const tBitmapResource kRscBitmapStopD = { 25,25,0x02,4,800,kBitmapStopDBits,kBitmapStopDPalette }; |
The final touch to img2res
What’s left to do in img2res is to use the 2 functions we implemented above in the main:
int main(int argc,char **argv) { tColor lPalette[256]; tChar lFilename[256]; cBitmap* lBitmap; cFile lHeader; cFile lCode; tChar* lName; tErr lErr; if(argc<3) { printf("Usage : <filename> (<image> <label>)+\n"); return 1; } lName = argv[1]; // create header file sprintf(lFilename,"%s.h",lName); lErr = lHeader.Open(lFilename,kFileWrite | kFileTrunc); if(lErr) return 1; // create code file sprintf(lFilename,"%s.cpp",lName); lErr = lCode.Open(lFilename,kFileWrite | kFileTrunc); if(lErr) { lHeader.Close(); return 1; } |
To create and fill both C++ and header files, we use the class cFile. This class provides writing and/or reading in a file.
lHeader.Write("#ifndef _RES_%s\n",lName); lHeader.Write("#define _RES_%s\n\n",lName); lHeader.Write("#include <Cincinella/Types.h>\n\n"); lHeader.Write("using namespace zSDK::Cincinella;\n\n"); lHeader.Write("typedef struct tBitmapResource {\n"); lHeader.Write("\ttUint32 iWidth;\n"); lHeader.Write("\ttUint32 iHeight;\n"); lHeader.Write("\ttUint32 iIndex;\n"); lHeader.Write("\ttUint16 iColors;\n"); lHeader.Write("\ttUint32 iLength;\n"); lHeader.Write("\ttUint8* iBits;\n"); lHeader.Write("\ttUint32* iPalette;\n"); lHeader.Write("} tBitmapResource;\n\n"); |
In the header file we output the definition of the type tBitmapResource.
lCode.Write("#include |
For each graphic file given in the command line, we will try to load it. If this is successful and the bitmap is palette based, then we will process it further:
if(lBitmap) { if(lBitmap->GetColorSpace() == eSpaceColor8Bit) { tUint16 lWidth,lHeight; tUint16 lColors = lBitmap->CountColors(); lBitmap->GetSize(lWidth,lHeight); printf("bitmap is %dx%d pixels and have %d colors\n",lWidth,lHeight,lColors); if(!lBitmap->GetPalette(lPalette)) { DumpBitmapToFile(lHeader,lCode,lBitmap,lPalette,lColors,argv[i+1]); printf("dumped\n"); } else printf("failed to get the colors palette\n"); } else printf("sorry this isn't a palette based image!\n"); } else printf("failed\n"); } // fill files with closing stuff lHeader.Write("\n"); lHeader.Write("#endif\n"); // and close them lHeader.Close(); lCode.Close(); return 0; } |
To simplify things, I created a simple shell script to rebuild a C++ file from the graphics whenever we needed to. We would need to do this when the graphic team modifies some of them, or when we create a new skin:
#/bin/sh img2res Bitmaps \ Images/background.gif Background \ Images/dimmed.gif Dimmed \ Images/normal.gif Normal \ Images/pressed.gif Pressed \ Images/animate2.gif Animate1 \ Images/animate3.gif Animate2 \ Images/animate4.gif Animate3 \ Images/forward.gif ForwardN \ Images/forward_dim.gif ForwardD \ Images/pause.gif PauseN \ Images/pause_dim.gif PauseD \ Images/play.gif PlayN \ Images/play_dim.gif PlayD \ Images/rewind.gif BackwardN \ Images/rewind_dim.gif BackwardD \ Images/stop.gif StopN \ Images/stop_dim.gif StopD |
When creating a new skin, even if the name of each graphic file needs to be changed, the resource name should stay the same in order to keep the necessary adaptations from a skin to another as small as possible.
You can have a look at the complete source code of img2res, as well as an example of the header and source file generated by this tool.
Skin as shared object
Now that we have all our graphics neatly packed away in a C++ file, it is time to see how we are going to integrate it into to a shared
object. To help us load a shared object later on, we will be using the class cAddOn, provided by the Zinzala SDK. This class is a simple wrapper around the famous function dlopen().
Shared object contents
Each skin shared object must provide a function through which a given resource can be retrieved. A simple tUint16 is enough to identify each resource defined in the C++ file we have created from the graphics:
const tBitmapResource *gResources[] = { &kRscBitmapBackground, &kRscBitmapDimmed, &kRscBitmapNormal, &kRscBitmapPressed, &kRscBitmapAnimate1, &kRscBitmapAnimate2, &kRscBitmapAnimate3, &kRscBitmapForwardN, &kRscBitmapForwardD, &kRscBitmapPauseN, &kRscBitmapPauseD, &kRscBitmapPlayN, &kRscBitmapPlayD, &kRscBitmapBackwardN, &kRscBitmapBackwardD, &kRscBitmapStopN, &kRscBitmapStopD }; tUint16 kResourcesCount = 17; extern "C" const tBitmapResource *GetResource(tUint16 aResourceID) { if(aResourceID < kResourcesCount) return gResources[aResourceID]; else return NULL; } |
The array gResources contains a pointer to each graphic resource that we created earlier. The function GetResource() accesses the array and returns a resource from a position in the array.
You may have a look at the skin C++ file. The functions _AddOn_* are mandatory since they are required by the class cAddOn.
Loading the shared object
To load and access the skin shared object, we are now going to derive the class cAddOn and add to it a method that will
retrieve the resource when given a resource ID. It will simply use the GetResource() function defined in every skin shared object.
Here’s the declaration of the class:
class cDSkin : public cAddOn { public: cDSkin(const tChar *aPath); const tBitmapResource &GetResourceL(tSkinResource aResource); private: const tBitmapResource *(*iFunction)(tUint16 aResourceID); }; |
The member data iFunction will be set in the constructor to point to the shared object function we implemented earlier. The method GetResourceL() relies on it to get the tBitmapResource associated with the requested resource. Note that if the resource doesn’t exist in the skin, this method will throw an exception. This is implied by the L, as in Leave, that is added to the end of its name. This is a common practice in Symbian. This naming convention allows developers to easily recognize which methods can throw an exception.
The type tSkinResource defines all the resources that should be accessible from any skin:
enum tSkinResource { eResBackground = 0, eResDimmed, eResNormal, eResPressed, eResAnimate1, eResAnimate2, eResAnimate3, eResForwardN, eResForwardD, eResPauseN, eResPauseD, eResPlayN, eResPlayD, eResBackwardN, eResBackwardD, eResStopN, eResStopD }; |
Here’s the code of the class constructor:
cDSkin::cDSkin(const tChar *aPath) : cAddOn(aPath) { if(!iFault) { iFunction = (const tBitmapResource *(*)(tUint16 aResourceID))Find("GetResource"); if(!iFunction) iFault = kErrNotFound; } } |
If the skin shared object is correctly loaded, the data member iFault will be kErrNone (whose value is 0). We can then use the method cAddOn::Find() to find the function GetResource() implemented in the shared object. If we cannot find it, iFault will indicate the error. It is the responsability of the calling code to check the validity of the cDSkin object. Because we have used extern "C" when defining the function GetResource(), we don’t have to deal with mangled name.
The method GetResourceL() shows how the resource will be retrieved from the shared object:
const tBitmapResource &cDSkin::GetResourceL(tSkinResource aResource) { if(!iFault) { const tBitmapResource *lRes; lRes = (*iFunction)((tUint16)aResource); if(lRes) return *lRes; else throw uException(kErrNotFound,"cDSkin::GetResourceL"); } else throw uException(iFault,"cDSkin::GetResourceL"); } |
If the skin is not valid or if we cannot find the specified skin, an exception will be raised. The class uException is the basic exception class in the Zinzala SDK. It takes an error code and a string indicating where the exception was raised as its arguments.
You may have a look at the complete header and C++ files if you like.
3. The cDConsoleView class
To build the multimedia console widget, we are going to derive the widget cDrawView. This class defines and implements a very basic type of widget, making it the perfect starting point for building a custom widget. It provides various drawing methods that we will be using to render the console contents.
Here’s the declaration of cDConsoleView:
class cDConsoleView : public cDrawView { public: static cDConsoleView *NewL(cDSkin &aSkin); virtual ~cDConsoleView(); void SetState(tConsoleState aState); tConsoleState GetState() const {return iState;}; protected: virtual void PlayPressed(tBool aActive) {}; virtual void PausePressed(tBool aActive) {}; virtual void StopPressed(tBool aActive) {}; virtual void FwdPressed(tBool aActive) {}; virtual void BwdPressed(tBool aActive) {}; protected: void OnScreen(); void Beat(); void MouseUp(uPoint aPoint); void MouseDown(uPoint aPoint); void OutBound(uPoint aPoint); protected: cDConsoleView(); virtual void ConstructL(cDSkin &aSkin); private: tUint8 FindButton(uPoint aPoint) const; void Render(tUint8 aButton,tBool aBackground = false); void Pressed(tUint8 aButton); void Invoked(); private: typedef struct tButton; private: tConsoleState iState; // Console state cBitmap* iBackground; // Background bitmap cBitmap** iBStates; // Button's states bitmaps cBitmap** iBLabelsN; // Button's labels bitmaps (non dimmed) cBitmap** iBLabelsD; // Button's labels bitmaps (dimmed) tButton* iButtons; // Button's data tUint8 iPressedButton; // ID of the button currently pressed tUint8 iActiveButton; // ID of the button currently active tUint8 iPrevState; // Previous state of the current pressed button tInt8 iActiveDir; // Direction of the Active button animation }; |
The console defines five callback methods to be implemented by derived classes. PlayPressed() will respond to a user action on the Play button, PausePressed() on the Pause button, and so on. The console implements how to handle user actions in the methods MouseDown(), MouseUp() and OutBound().
The state of the console can be changed at anytime with the method SetState(). The console itself will not change its state, however; the state will be used when rendering the console. Here’s the definition of tConsoleState:
enum tConsoleState { ePlaying, eStopped, ePaused, eForward, eBackward }; |
Construction & Destruction
Implementing a two-phase construction is not difficult. However, it does require a bit more coding than just a standard C++ constructor. The following methods are involved in the construction of an object: cDConsoleView(), ConstructL() and NewL(). NewL() is a factory function, the frontend of the creation of an object. Instead of using the new C++ operator, we will call this method to instantiate a new object. To inforce that NewL() is the only possibilty for object instantiation, we have set the constructor as a protected method.
Let’s see the code of the C++ constructor cDConsoleView():
cDConsoleView::cDConsoleView() : cDrawView(uRect(kConsoleWidth,kConsoleHeight),"console",kFollowNone, kFlgBeatNeeded | kFlgInteractive | kFlgOffscreen) { iBackground = NULL; iBStates = NULL; iBLabelsN = NULL; iBLabelsD = NULL; iButtons = NULL; iPressedButton = kButtonsCount; iActiveButton = kButtonsCount; iActiveDir = 1; iState = eStopped; if(!iFault) { iButtons = new tButton[kButtonsCount]; if(!iButtons) iFault = kErrOutOfMemory; else { iBStates = new (cBitmap *)[kStatesCount]; if(!iBStates) iFault = kErrOutOfMemory; else { memset(iBStates,0,sizeof(cBitmap *) * kStatesCount); iBLabelsN = new (cBitmap *)[kLabelsCount]; if(!iBLabelsN) iFault = kErrOutOfMemory; else { memset(iBLabelsN,0,sizeof(cBitmap *) * kLabelsCount); iBLabelsD = new (cBitmap *)[kLabelsCount]; if(!iBLabelsD) iFault = kErrOutOfMemory; else memset(iBLabelsD,0,sizeof(cBitmap *) * kLabelsCount); } } } } } |
Using the two-phase construction idiom doesn’t mean that no memory allocation should ever be done in the C++ constructor. But if we choose to do so, we must make sure that it can still be deleted if the construction fails and that the partially constructed object is destroyed.
After some initialization, we try to allocate the 4 arrays that will contain all the buttons data (iButtons) and the various corresponding bitmaps (iBStates, iBLabelsN, iBLabelsD). If any of the memory allocation fails, we will mark the object as faulty by setting the data member iFault to kErrOutOfMemory. This member variable is inherited from deep inside the Zinzala SDK (class pBase from the Cincinella Kit). Because the constructor of the base class cDrawView could lead to an invalid object, it is recommended that you test whether or not iFault already indicates a fault. There is no point in allocating more memory if we going to delete the object right away because it is faulty.
The flags passed to the base class cDrawView sets a couple of behaviors for the widget. kFlgBeatNeeded indicates that the widget will make use of the window beat. At a given interval, the method Beat() of the widget will be called. We need this flag to render the animation of the active button. kFlgInteractive specifies that the widget will accept user inputs. The flag kFlgOffscreen is specific to the widget cDrawView, and indicates that we will be using an offscreen bitmap to render the widget in order to avoid any flickering. For instance, when drawing the animation.
Here’s the definition of the various constants we have used in the constructor:
const tUint16 kConsoleWidth = 321; const tUint16 kConsoleHeight = 98; const tUint8 kButtonsCount = 4; const tUint8 kStatesCount = 7; const tUint8 kLabelsCount = 5; |
We also need to define tButton whose implementation is hidden from the class users:
typedef struct cDConsoleView::tButton { uRect iBFrame; // frame of the button uRect iLFrame; // frame of the label tUint8 iState; // button state tUint8 iLabel; // label displayed on the button } tButton; |
This type holds all the button specific information, such as its position within the console or its state.
The method ConstructL() contains the rest of the widget’s construction where exceptions can be raised. Here’s its
implementation:
void cDConsoleView::ConstructL(cDSkin &aSkin) { uPoint lPoint; // If the view is already faulty, we will leave right away sEnv::LeaveIfError(iFault); |
Because this method will be called right after the class constructor is called, the widget might already be faulty. If this is the case, there is no need to proceed, we will leave right away.
// Instantiate the background bitmap iBackground = CreateBitmapFromResourceL(aSkin.GetResourceL(eResBackground),false); // Instantiate the button bitmaps iBStates[kStateNormal] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResNormal)); iBStates[kStateDimmed] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResDimmed)); iBStates[kStatePressed] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPressed)); iBStates[kStateActive] = iBStates[kStateNormal]; // use same bitmap as the normal state iBStates[kStateActive1] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResAnimate1)); iBStates[kStateActive2] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResAnimate2)); iBStates[kStateActive3] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResAnimate3)); // Instantiate the label bitmaps iBLabelsN[kLabelBwd] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResBackwardN)); iBLabelsN[kLabelFwd] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResForwardN)); iBLabelsN[kLabelStop] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResStopN)); iBLabelsN[kLabelPlay] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPlayN)); iBLabelsN[kLabelPause] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPauseN)); iBLabelsD[kLabelBwd] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResBackwardD)); iBLabelsD[kLabelFwd] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResForwardD)); iBLabelsD[kLabelStop] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResStopD)); iBLabelsD[kLabelPlay] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPlayD)); iBLabelsD[kLabelPause] = CreateBitmapFromResourceL(aSkin.GetResourceL(eResPauseD)); |
The function CreateBitmapFromResourceL() that we will see later, instantiates a cBitmap from the resource contained in the skin. If something goes wrong, it will leave.
tUint16 lX = kPositionX; // initialise the buttons data (all are dimmed) for(tUint8 i=0;i<kButtonsCount;i++) { iButtons[i].iState = kStateDimmed; iButtons[i].iLabel = i; iButtons[i].iBFrame.Set(lX,kPositionY,lX + kButtonWidth,kPositionY + kButtonHeight); lPoint.Set(lX + kButtonWidth / 2,kPositionY + kButtonHeight / 2); lPoint.iX -= kLabelWidth / 2; lPoint.iY -= kLabelHeight / 2; iButtons[i].iLFrame.Set(lPoint.iX,lPoint.iY,lPoint.iX+kLabelWidth,lPoint.iY+kLabelHeight); lX += kButtonWidth + kSpacing; } |
Beside initializing the button’s data, we compute its position within the console and its label’s position, which is always centered within the button.
// The play button is not dimmed by default iButtons[kButtonPlay].iState = kStateNormal; } |
Here’s the definition of the various constants we have used in ConstructL():
const tUint16 kButtonWidth = 64; const tUint16 kButtonHeight = 64; const tUint16 kLabelWidth = 25; const tUint16 kLabelHeight = 25; const tUint16 kPositionX = 18; const tUint16 kPositionY = 17; const tUint16 kSpacing = 10; const tUint8 kButtonBwd = 0; const tUint8 kButtonPlay = 1; const tUint8 kButtonPause = 2; const tUint8 kButtonFwd = 3; const tUint8 kStateDimmed = 0; const tUint8 kStatePressed = 1; const tUint8 kStateNormal = 2; const tUint8 kStateActive = 3; const tUint8 kStateActive1 = 4; const tUint8 kStateActive2 = 5; const tUint8 kStateActive3 = 6; const tUint8 kLabelBwd = 0; const tUint8 kLabelPlay = 1; const tUint8 kLabelPause = 2; const tUint8 kLabelFwd = 3; const tUint8 kLabelStop = 4; |
In order to instantiate the class cDConsoleView, we now need to implement the method NewL(). It is simple and short:
cDConsoleView *cDConsoleView::NewL(cDSkin &aSkin) { cDConsoleView *lView = new cDConsoleView(); cDConsoleView::VerifyLC(lView); lView->ConstructL(aSkin); sCleanupStack::PopL(lView); return lView; } |
As you can see, we first construct a new object using the standard C++ operator new. Then we use the method VerifyLC(), inherited from the base class pBase, to verify that the object is valid. The LC at the end of the method’s name indicates that it can leave. If it doesn’t leave, the object in lView will be left on the cleanup stack. We need the object to be on the stack before we call the ConstructL() method on the object. Since this method can leave, we need to make sure that it will be deleted automatically when the exception is handled. If there is no problem in the construction of the object, we will pop the object from the stack then return it.
We already saw the method cSkin::GetResourceL() earlier, so here’s CreateBitmapFromResourceL():
cBitmap *CreateBitmapFromResourceL(const tBitmapResource &aRes,tBool aTransparent = true) { tErr lErr; cBitmap *lBitmap = NULL; lBitmap = new cBitmap(aRes.iWidth,aRes.iHeight,eSpaceColor8Bit,true,aRes.iColors); lErr = cBitmap::VerifyD(lBitmap); if(!lErr) { lBitmap->SetPalette(aRes.iPalette); lBitmap->SetBits(aRes.iBits,aRes.iLength,0); if(aTransparent) lBitmap->MakeTransparent(aRes.iIndex); return lBitmap; } else throw uException(lErr,"CreateBitmapFromResourceL()"); } |
Its implementation is quite straightforward. A bitmap is created with the size and number of colors required. Then we use VerifyD() to test the validity of the bitmap. If the bitmap is valid, we will set the palette of colors and copy its pixel data. If invalid, the object will be deleted by VerifyD(). Now, if the bitmap shall be transparent, we will apply the transparency. If the bitmap cannot be created succesfully, due to a lack of memory or some other reason, the method will throw an exception.
In the class destructor, we need to delete all the memory that was allocated (if any). Here is the implementation:
cDConsoleView::~cDConsoleView() { if(iButtons) delete [] iButtons; if(iBackground) delete iBackground; if(iBStates) { for(tUint8 i=0;i<kStatesCount;i++) if(iBStates[i] && i!=kStateActive) // Active and Normal states share the same bitmap delete iBStates[i]; delete [] iBStates; } if(iBLabelsN) { for(tUint8 i=0;i<kLabelsCount;i++) if(iBLabelsN[i]) delete iBLabelsN[i]; delete [] iBLabelsN; } if(iBLabelsD) { for(tUint8 i=0;i<kLabelsCount;i++) if(iBLabelsD[i]) delete iBLabelsD[i]; delete [] iBLabelsD; } } |
Drawing
The method Render() is responsible for drawing the console on the widget. Because the widget is using an offscreen bitmap, we do not have to worry about redrawing any part of the widget that is damaged. This is handled by the cDrawView.
The methods StartDrawing() and EndDrawing() from the class cDrawView are required before and after every block that perform some drawing:
void cDConsoleView::Render(tUint8 aButton,tBool aBackground) { StartDrawing(); if(aButton < kButtonsCount) { |
We will be using this method to render either the whole console or just a given button. When aButton equal kButtonsCount the whole console will be redrawn.
// Render only one button DrawBitmap(iBStates[iButtons[aButton].iState],iButtons[aButton].iBFrame.GetLeftTop()); if(iButtons[aButton].iState == kStateDimmed) DrawBitmap(iBLabelsD[iButtons[aButton].iLabel],iButtons[aButton].iLFrame.GetLeftTop()); else DrawBitmap(iBLabelsN[iButtons[aButton].iLabel],iButtons[aButton].iLFrame.GetLeftTop()); |
The method cDrawView::DrawBitmap() draws a bitmap at a given location. We use it here to render the button using the bitmap associated with the button’s state, followed by the label of the button.
} else { // Render the background if(aBackground) DrawBitmap(iBackground); // Render all the button for(tUint8 i=0;i<kButtonsCount;i++) { DrawBitmap(iBStates[iButtons[i].iState],iButtons[i].iBFrame.GetLeftTop()); if(iButtons[i].iState == kStateDimmed) DrawBitmap(iBLabelsD[iButtons[i].iLabel],iButtons[i].iLFrame.GetLeftTop()); else DrawBitmap(iBLabelsN[iButtons[i].iLabel],iButtons[i].iLFrame.GetLeftTop()); } |
When rendering the whole console, we first draw the background bitmap if required, then the 4 buttons with their labels.
} EndDrawing(); } |
In order to render the console for the first time, we implement the method OnScreen(). This method is executed when the widget is put onscreen, following the creation of the parent window:
void cDConsoleView::OnScreen() { cDrawView::OnScreen(); // Render the whole console (for the first time) Render(kButtonsCount,true); } |
User’s inputs
Although the user may think that our console contains 4 independent button widgets, it is in fact only one widget. Handling the user actions is going to require the need to know which one of the buttons was acted upon.
When given a position within the widget, the method FindButton() will give us the related button:
tUint8 cDConsoleView::FindButton(uPoint aPoint) const { tUint8 lButton; for(lButton=0;lButton<kButtonsCount;lButton++) { if(iButtons[lButton].iBFrame.Contains(aPoint)) break; } return lButton; } |
Its algorithm is simple. We loop over all the buttons and check if aPoint is contained in the button frame. If none of the buttons contain the position being tested, the method will return kButtonsCount. Although simple, there is one little issue … What if the buttons are circular?
Because we are using the rectangular frame of the button, if we were to tap in a corner of the frame, which is dearly outside of the button, it will still act as if we clicked in the center of the button. Can we live with it ? If our UI is to be used on a touch-screen in a moving vehicle, it wouldn’t matter much since the user’s input will not be too precise. However, if the input is triggered by a pen, then we should do something to improve it.
One solution, on top of what we’ve already done, is to use the button’s bitmap to find out if the position where the user has clicked, is transparent or not. If it is transparent, then we are outside of the button.
Here’s the improved method:
tUint8 cDConsoleView::FindButton(uPoint aPoint) const { tUint8 lButton; for(lButton=0;lButton<kButtonsCount;lButton++) { if(iButtons[lButton].iBFrame.Contains(aPoint)) { tUint8 lIndex; tUint8 lTrans; uPoint lPoint; // calculate position within the button frame lPoint.iX = aPoint.iX - iButtons[lButton].iBFrame.iLeft; lPoint.iY = aPoint.iY - iButtons[lButton].iBFrame.iTop; // get transparent color index iBStates[kStateNormal]->GetTransparent(lTrans); // get pixel color index iBStates[kStateNormal]->GetPixelColor(lPoint.iX,lPoint.iY,&lIndex); // if the color is transparent, then we are outside of the button if(lIndex != lTrans) break; } } return lButton; } |
Now, there are two main events that we are going to handle: the user presses down on a button, and the user releases the button. The first one will be handled in the method MouseDown(), the second in MouseUp():
void cDConsoleView::MouseDown(uPoint aPoint) { Pressed(FindButton(aPoint)); } void cDConsoleView::MouseUp(uPoint aPoint) { if(FindButton(aPoint) == iPressedButton) Invoked(); else { if(iButtons[iPressedButton].iState == kStatePressed) { // revert the state of the button iButtons[iPressedButton].iState = iPrevState; // redraw the button Render(iPressedButton); } iPressedButton = kButtonsCount; } } |
When the user clicks, taps or presses somewhere on the console, the method MouseDown() will be called. This method will execute Pressed() with the identifier of the button pressed. If a button was indeed pressed, we will mark the button as pressed. It is only when the user releases the button, that we will invoke it from MouseUp(). If the button is released outside of its boundary, we will not invoke it.
Here is the method Pressed():
void cDConsoleView::Pressed(tUint8 aButton) { // if the mouse event was on a button if(aButton < kButtonsCount) { iPressedButton = aButton; // if the button is not dimmed if(iButtons[aButton].iState == kStateNormal || iButtons[aButton].iState >= kStateActive) { // set the button as pressed iPrevState = iButtons[aButton].iState; iButtons[aButton].iState = kStatePressed; // redraw the button Render(aButton); } } } |
Whether the button is dimmed or not, we mark it as pressed. However, we only really set it as pressed and redraw it, if it is not dimmed.
The purpose of the method Invoked() is to accept and acknowledge the user action on a given button. In order to improve the illusion of a physical button, we are going to redraw the button with a bit of a delay. That way it will look pressed a little bit longer. Here it goes:
void cDConsoleView::Invoked() { tBool lGood = false; if(iPressedButton < kButtonsCount) { if(iButtons[iPressedButton].iState == kStatePressed) { // delay a bit to have a better effect sSnooze::SomeMS(kDelayPressed); // restore the previous state of the button iButtons[iPressedButton].iState = iPrevState; // redraw the button Render(iPressedButton); lGood = true; } // call the hook method linked to the pressed button switch(iPressedButton) { case kButtonPlay: { if(iButtons[kButtonPlay].iLabel == kLabelStop) StopPressed(lGood); else PlayPressed(lGood); break; } case kButtonPause: { PausePressed(lGood); break; } case kButtonBwd: { BwdPressed(lGood); break; } case kButtonFwd: { FwdPressed(lGood); break; } } iPressedButton = kButtonsCount; } } |
It is only if the button can be pressed, not dimmed, that we will redraw it and restore its state. The button callback will be called in both cases, however; lGood will be false when the button is dimmed.
The last part of handling the user input goes into specific case where the user clicks or taps down on a button, but never releases the mouse button or its finger (when there is no mouse or pen). How could that be? Imagine you are driving a four wheel suv on a bumpy road (…. okay, trail ๐ … Now, you reach for the console in order to stop the playback, but just as you touch the on-screen button, the car rocks and your finger moves off the button. The problem is that the console widget may register the pressed down action on the button, but because you released your finger outside of the widget, the pressed up event will not be processed by the widget.
A simple solution is to use the method OutBound() which is called every time the mouse or finger leaves the widget while still pressed down. When this occurs, we will revert its state and redraw it:
void cDConsoleView::OutBound(uPoint aPoint) { if(iPressedButton < kButtonsCount) { if(iButtons[iPressedButton].iState == kStatePressed) { // revert the state of the button iButtons[iPressedButton].iState = iPrevState; // redraw the button Render(iPressedButton); } iPressedButton = kButtonsCount; } } |
Animation
When a button is active, we want to display a pulsating effect on the active button. To render this, we need to redraw the button at a given interval in time. We have used the flag kFlgBeatNeeded in the cDConsoleView base class initialization. Now let’s have a look at the implementation of the method Beat(), which will be called at the rate set by the parent window:
void cDConsoleView::Beat() { // if there is an active button if(iActiveButton < kButtonsCount) { if(iButtons[iActiveButton].iState >= kStateActive) { |
Because this method will still be called even when there is no button active, we will only draw the animation effect if there is an active button.
// set the state of the button iButtons[iActiveButton].iState += iActiveDir; |
The pulsating effect is rendered by incrementing the state of the button from kStateActive to kStateActive2 and then reversing back to kStateActive.
if(iButtons[iActiveButton].iState > kStateActive2) { iButtons[iActiveButton].iState = kStateActive2; iActiveDir = -1; } else if(iButtons[iActiveButton].iState < kStateActive) { iButtons[iActiveButton].iState = kStateActive; iActiveDir = 1; } |
We use the data member iActiveDir to store the direction we are going. The only thing left to do, is to redraw the button. We simply call the method Render() which use the button state to render it:
// redraw the button Render(iActiveButton); } } } |
Later, we will see how to setup the window to beat.
Changing state
All that is left to be done, is SetState(). This method will be used from outside the class in order to change the state of the console. For example, when the user presses the Play button and playback successfully starts, the state should be changed to ePlaying.
Here’s the method implementation:
void cDConsoleView::SetState(tConsoleState aState) { if(!iFault) { TheLooperMustBeLocked(); |
Since we do not know who will be calling this method and when, we need to insure that the looper (the window) is locked. The macro TheLooperMustBeLocked() will check this for us. If it is not locked, an exception will be raised and the application will likely be terminated. This helps us to easily detect situations where we are executing code that should only be executed when the window is locked. Don’t forget that the Zinzala SDK, is multithreaded and that only one thread should be changing the button’s state or widget internals at any given time.
switch(aState) { case ePlaying: { // enable the buttons (all of them) for(tUint8 i=0;i<kButtonsCount;i++) iButtons[i].iState = kStateNormal; // change the label of the play button iButtons[kButtonPlay].iLabel = kLabelStop; // and set it as active iButtons[kButtonPlay].iState = kStateActive; iActiveButton = kButtonPlay; break; } case eStopped: { // disable the buttons (all of them) for(tUint8 i=0;i<kButtonsCount;i++) iButtons[i].iState = kStateDimmed; // change the label of the play button iButtons[kButtonPlay].iLabel = kLabelPlay; // and enable it iButtons[kButtonPlay].iState = kStateNormal; // there is no active button iActiveButton = kButtonsCount; break; } case ePaused: { // disable some of the buttons iButtons[kButtonBwd].iState = kStateDimmed; iButtons[kButtonFwd].iState = kStateDimmed; // and set it as active iButtons[kButtonPause].iState = kStateActive; iActiveButton = kButtonPause; break; } case eForward: { // disable some of the buttons iButtons[kButtonBwd].iState = kStateDimmed; iButtons[kButtonPause].iState = kStateDimmed; // and set it as active iButtons[kButtonFwd].iState = kStateActive; iActiveButton = kButtonFwd; break; } case eBackward: { // disable some of the buttons iButtons[kButtonFwd].iState = kStateDimmed; iButtons[kButtonPause].iState = kStateDimmed; // and set it as active iButtons[kButtonBwd].iState = kStateActive; iActiveButton = kButtonBwd; break; } } iState = aState; |
Because several button states may have changed, we simply redraw them all:
// render all the buttons Render(kButtonsCount); } } |
As you have noticed, this method does not check the logic of state changes. The derived class or whoever else called this method, is responsible for this check.
If you would like to see the complete source code of the cDConsoleView class, here’s the header and the C++ file.
4. Using cMyConsoleView
In order to test the console class we just created, we are going to derive it and implement the button pressed callbacks. Then, we will assemble a simple test application.
Derivating
Here’s the declaration of the class cMyConsoleView:
class cMyConsoleView : public cDConsoleView { public: static cMyConsoleView *NewL(cDSkin &aSkin); void Reset(); protected: void PlayPressed(tBool aActive); void PausePressed(tBool aActive); void StopPressed(tBool aActive); void FwdPressed(tBool aActive); void BwdPressed(tBool aActive); protected: cMyConsoleView(); }; |
No surprise here. The class will implement the callback for each button of the console.
The method NewL() needs to be re-implemented, even though it is already in the base class:
cMyConsoleView::cMyConsoleView() : cDConsoleView() { } cMyConsoleView *cMyConsoleView::NewL(cDSkin &aSkin) { cMyConsoleView *lView = new cMyConsoleView(); cMyConsoleView::VerifyLC(lView); lView->ConstructL(aSkin); sCleanupStack::PopL(lView); return lView; } |
The implementation of NewL() is very close to the one we have in the base class. In fact, such methods are always very similar, since their purpose is to create a new object and construct it. The method NewL() from the base class is not really necessary because it is very unlikely that someone will want to use that class without derivating it, but you never know … ๐
I have added the method Reset() to the class. Its purpose is to reset the console to its stopped state:
void cMyConsoleView::Reset() { SetState(eStopped); } |
Our implementation of the button callbacks that will be presented, is as simple as it can get. Real world implementations will likely be a bit more complex, although they will perform the same basic actions as here, such as changing the console state:
void cMyConsoleView::PlayPressed(tBool aActive) { if(aActive) SetState(ePlaying); } void cMyConsoleView::PausePressed(tBool aActive) { if(aActive) { if(GetState() != ePaused) SetState(ePaused); else SetState(ePlaying); } } void cMyConsoleView::StopPressed(tBool aActive) { if(aActive) SetState(eStopped); } void cMyConsoleView::FwdPressed(tBool aActive) { if(aActive) { if(GetState() != eForward) SetState(eForward); else SetState(ePlaying); } } void cMyConsoleView::BwdPressed(tBool aActive) { if(aActive) { if(GetState() != eBackward) SetState(eBackward); else SetState(ePlaying); } } |
If you would like to see the complete source code of the cMyConsoleView class, here’s the header and the C++ file.
Window and Application
Now that we have implemented the console class and specialized it, we can move on to the window and application. We will also have a look at the main of the test program.
cDWindow
Here’s the window declaration from DWindow.h:
class cDWindow : public cWindow { public: static cDWindow *NewL(cSettings *aSettings,cDSkin &aSkin); virtual ~cDWindow(); protected: void Ready(); bool CloseRequested(); protected: cDWindow(cSettings *aSettings); void ConstructL(cDSkin &aSkin); private: cSettings* iSettings; cMyConsoleView* iConsole; }; |
The cSettings class provides access to the application specific settings, which will be saved and restored each time the application runs. We will use it to save the position of the window on screen. This is not a requirement for any window or application with the Zinzala SDK, but rather a personal preference. I prefer that my window always appears at the last place I left it.
As you might guess by now, our window class will use the two-phase construction idiom. Note, although the SDK provides everything that is needed to use this idiom, it does not force the developer to use it, nor does it enforce the use of exceptions as the means of error handling.
Here’s the C++ constructor of the class:
cDWindow::cDWindow(cSettings *aSettings) : cWindow(uRect(0,0),"window",eWinNormal,kFlgResizeToFit | kFlgNotResizable, kCurrentWorkspace) { iSettings = aSettings; iConsole = NULL; if(!iFault) { uPoint *lPos; SetTitle("Zinzala : Custom Widget Demo ..."); SetPadding(5,5); SetBGColor(kColorWhite); |
If the base class construction turns out to be a success, we set the title of the window, then specify a 5×5 padding for the widget area. This padding doesn’t mean that each widget placed on the window will be surrounded by 5 pixels of empty space. It simply means that there will be an inset of 5 pixels to the window bounds. We also set the background color to white.
// Get position of the window is possible if(iSettings->HasItem("pos")) { iSettings->GetData("pos",eDataBuffer,(char **)&lPos,sizeof(uPoint)); MoveTo(*lPos); } |
If the demo application was already used once, the settings should contain the position of the window on screen. If this is the case, we read that position from the settings and move the window accordingly.
} } |
The instantiation of our console will be placed in the ConstructL() method of the window, as it can throw an exception:
void cDWindow::ConstructL(cDSkin &aSkin) { sEnv::LeaveIfError(iFault); iConsole = cMyConsoleView::NewL(aSkin); AddChild(iConsole); } |
If the cWindow class does not delete all its children views when it is destroyed, we would have had to add the destruction of iConsole in it. However since it does, the class destructor is empty.
Here is the method NewL() of the window class:
cDWindow *cDWindow::NewL(cSettings *aSettings,cDSkin &aSkin) { cDWindow *lWin = new cDWindow(aSettings); cDWindow::VerifyLC(lWin); lWin->ConstructL(aSkin); sCleanupStack::PopL(lWin); return lWin; } |
The method Ready() and CloseRequested() are implemented in our class and defined in cWindow. The first one is
called after the window is displayed on screen. We will be using it to set the beat rate of the window, which our console widget needs for the active button animation:
void cDWindow::Ready() { SetBeatRate(kBeatRate); cWindow::Ready(); } |
The constant kBeatRate is defined as const tUint32 kBeatRate = 150 and is expressed in milliseconds.
When the user asks to close the window, the method CloseRequested() will be executed. If it returns true, the window will be closed, else it will remain open. This is the perfect time for us to save the current position of the window on screen, for future usage:
tBool cDWindow::CloseRequested() { // Store current position of the window in the settings uPoint pos = GetPosition(); iSettings->SetData("pos",eDataBuffer,(char *)&pos,sizeof(uPoint)); return true; } |
Here’s the header and the C++ file of cDWindow.
cDApplication
We are now going to derive the application class to create and display the window. We will also be retreiving the application settings. Here’s the application declaration from DApplication.h:
class cDApplication : public cApplication { public: cDApplication(); ~cDApplication(); protected: void Ready(); private: cDWindow* iWindow; cSettings* iSettings; }; |
The method Ready() is called once the application starts to run. We will use this method to construct and display the window. First, let’s see the class constructor:
cDApplication::cDApplication() : cApplication("hexaZen/Custom",0,0) { iWindow = NULL; if(!iFault) { // Create the object to read/write application settings iSettings = new cSettings("Custom"); if(!iSettings) iFault = kErrOutOfMemory; else iFault = iSettings->GetFault(); } else iSettings = NULL; } |
The first argument of the base class constructor is the name of the application. This name will be used to identify the application if we wanted to send some scripting messages to it. We will come back to that later. The class cSettings from which we instantiate iSettings provides access to the application settings. If something is wrong with it, or if the object cannot be created, the data member iFault of the application will be set to something other than kErrNone.
If you recall the destructor of cDWindow, you may remember that we didn’t delete the console widget in it, since the window took care of that. The application class, does not take care of deleting the window for us, so we will need to delete iWindow in it:
cDApplication::~cDApplication() { if(iSettings) { // save settings iSettings->Save(); delete iSettings; } // delete the window if(iWindow) delete iWindow; } |
Calling the method Save() on the cSettings object will save the last position of the window in the application settings file. Storing data in it, like we did in the method CloseRequested(), does not automatically save the changes to a file.
Before we look into the method Ready(), which is quiet interesting, we first need to talk a bit about how the skin shared object will be loaded and then passed down to the window. When the application is launched, the skin to be used will be passed in the command line argument. We will be using the switch -s for that. The static class sArgs provided by the SDK allows for an easy parsing of the command line. It will be used in the main of the application. Since it is static, we can use it in the Ready() method to get the path of the skin.
Now, we can start looking at Ready():
void cDApplication::Ready() { // load settings if(iSettings) iSettings->Load(); |
Although we have had iSettings since the constructor, we have not loaded the settings file until now. If it is the very first time the application is started, the settings file will not exist. The method Load() will fail, but it doesn’t really matter since we will create the file when we save the settings in the application destructor. Now,
we are going to use the method sArgs::Presents() to test if the switch was present on the command line:
if(sArgs::Presents("s")) { // load the skin cDSkin lSkin(sArgs::Value("s")); |
Using the class cDSkin presented earlier, we have loaded the shared object containing the skin from the path name retrieved from the value associated with the switch -s. However, it is only after testing the validity of lSkin, will we know if the skin object was loaded correctly:
if(lSkin.IsValid()) { |
If the skin was loaded properly, we can go ahead and create the window. Since the window creation can throw an exception, we will be putting it in a try…catch block:
// create & show window try { sCleanupStack::WindL(); |
Calling the method WindL() is mandatory in the beginning of the block. Its purpose is to prepare the cleanup stack. Note that this method can leave if something is not right. However, since we are already in the try…catch block, it’s okay. The creation of the window is no surprise; we use the method NewL(). If there is no exception, the window is ready to go and we simply show it on screen:
iWindow = cDWindow::NewL(iSettings,lSkin); iWindow->Show(); |
The last statement inside the try block, must be a call to the method UnWind(). This will un-prepare the cleanup stack and all the objects still present on the stack will be destroyed:
sCleanupStack::UnWind(); } catch (uException &lException) { |
If an exception is raised in the try block, we will be displaying the exception information and request the application to terminate:
sEnv::PrintToStream("The exception "%s" was raised in %s\n", sEnv::ErrorToString(lException.iWhy),lException.iWhere); Quit(); |
Like in the try block, we must end the catch block by a call to UnWind(). This will get all the objects on the cleanup stack to be destroyed, which is afterall the whole point of the idiom:
sCleanupStack::UnWind(); } } else { sEnv::PrintToStream("Invalid skin\n"); Quit(); } } else { sEnv::PrintToStream("No skin given\n"); Quit(); } |
Last but not least, we will call the base class Ready() method as required by the framework:
cApplication::Ready(); } |
Here’s the header and the C++ file of cDApplication.
Main
Finally, we are going to look at the contents of the main. We need to parse the command line arguments, then create and start the application:
int main(tInt32 aArgc,const tChar **aArgv) { tErr lErr; cDApplication lApp; if(!(lErr = lApp.GetFault())) { // if there is args on the command line, // extract them and give them to the app if(aArgc>1) { cCan lCan(0); if(!cCan::Verify(&lCan)) { sArgs::SetCan(&lCan); sArgs::Extract(aArgc,aArgv); // Start the app if(!(lErr = lApp.Run())) // and wait for it to end lApp.WaitEnd(); else sEnv::PrintToStream("The application failed to run : %s\n", sEnv::ErrorToString(lErr)); } else sEnv::PrintToStream("The application failed to run : %s\n", sEnv::ErrorToString(kErrOutOfMemory)); } else sEnv::PrintToStream("No skin specified.\n"); } else sEnv::PrintToStream("The application construction failed : %s\n", sEnv::ErrorToString(lErr)); } |
Once the application object is created, we parse the command line arguments then ask the application to run. Once this is done, we just wait for it to end using the WaitEnd() method. The class sArgs requires a cCan object in order to parse the command line. We create one on the stack.
Throughout this article, we have been using the static class sEnv a couple of times. This is an handy static class which provides access to various useful methods, such as PrintToStream(), LeaveIfError() or ErrorToString(). The latter method transforms an error code into a human readable string.
Here’s the C++ file for the main.
Now that our test application is complete, we can try it out with an alternate skin:
#> ./custom -s ./Skins/Orange/Orange.so |
Here’s an animated GIF assembled from screenshots:
5. Adding scriptability
A key feature of the Zinzala SDK is the ease with which we can add support for application scripting. We will now see how we can use this feature to add support for an on the fly skin change. This will be done while the application is running.
For testing, we will be using the tool yo which is part of the Zinzala library distribution. However, any application can send scripting messages to another application. For example, in the case of the Carrozza layer we discussed earlier, if requested a configuration panel could be sending a message to all the UI applications running in order to change the overall look of the UI. Because this will be done on the fly, there will be no interruption of services. For example, music or movie playback will remain active.
Without any modification to the application, we can already send some messages to it. For example:
#> yo hexaZen/Custom ping got answer in 0.000000 ms cCan::0x8047850 items=0 What = 'zAPo' Context = 0x0 #> yo hexaZen/Custom info got answer in 0.999928 ms cCan::0x8047850 items=6 What = 'zARy' Context = 0x0 Item 'zSDK' have 1 value(s) 0 string (4) '0.8' Item 'Username' have 1 value(s) 0 string (4) 'jlv' Item 'MaxInstances' have 1 value(s) 0 tUint8 (1) 125 Item 'Scripting' have 1 value(s) 0 tUint8 (1) 0 Item 'MaxMSGLength' have 1 value(s) 0 tUint16 (2) 100 Item 'GUIEngine' have 1 value(s) 0 string (7) 'Photon' #> yo hexaZen/Custom list window |
However, if we ask the application to use a different skin, it will obviously not work:
#> yo hexaZen/Custom tell window do reskin Orange.so Not allowed |
There are 2 raisons for this. By default, scripting is not enabled, and the keyword reskin is unknown to the cWindow class. This can be easily changed. Let’s have a look at what needs to be done in order to support the change of a skin on the fly.
cDApplication
Before adding support in the window class, we need to allow scripting at the application level. This is done by modifying the method Ready() as follow:
void cDApplication::Ready() { // Allow scripting AllowScripting(eLocal); // Increase the buffer size for messaging SetBufferLength(250); ... ... ... } |
Because the path to the skin to be loaded is going to be included in the scripting message, we need to set the size of the receiving buffer to 250 bytes. The default size is 100 bytes. If we didn’t do that, the path could have been truncated. 250 bytes should be enough for most cases. However, a better solution would have been to use the constant PATH_MAX defined in limits.h, and then add more bytes to it for the overhead generated by the transmission.
cDWindow
Next, we are going to add handling for the reskin command. The window should handle this scripting message, then ask each widget to use the new skin. This is what we are going to do.
First, we modify the definition of the cDWindow class:
class cDWindow : public cWindow { public: static cDWindow *NewL(cSettings *aSettings,cDSkin &aSkin); virtual ~cDWindow(); protected: void Ready(); bool CloseRequested(); void ScriptingReceived(cMessage *aMessage,cMessage *aReply); protected: cDWindow(cSettings *aSettings); void ConstructL(cDSkin &aSkin); private: void ReSkinL(const tChar *aSkin); private: cSettings* iSettings; cMyConsoleView* iConsole; }; |
We have added the methods ScriptingReceived() and ReSkinL(). The first method is inherited from the framework and will be called when the window receives a scripting message. We will be using the second method to perform the change of skin.
Just like in the application class, we need to allow scripting for the window. We modify the method Ready() as follows:
void cDWindow::Ready() { AllowScripting(eLocal); SetBeatRate(kBeatRate); cWindow::Ready(); } |
In ScriptingReceived(), we need to detect if the scripting command is do reskin. If it is not, we can pass the scripting message to the base class method and let it deal with it. Here’s how the method looks like:
void cDWindow::ScriptingReceived(cMessage *aMessage,cMessage *aReply) { tUint32 lCmd; aReply->SetBool(kScriptItemDone,true); if(!aMessage->GetUint32(kScriptItemCommand,&lCmd)) { |
The string do is converted by the tool yo into a tUint32 command, which we retrieve in the scripting message by using the label kScriptItemCommand. We will expect any other application sending scripting messages to send them in the format outlined in the protocol defined in the framework. If we can’t find the command ID, the reply message will indicate that the script message was malformed.
switch(lCmd) { case kScriptCmdDo: { const tChar *lFunction; |
Now that we know that this scripting message is asking the window to do something, we will retrieve the first argument which is the label of the action that must be performed:
if(!aMessage->GetString(kScriptItemArgs,&lFunction,0)) { if(!strcmp(lFunction,"reskin")) { const tChar *lSkin; |
If the message asks the window to change the skin it is using, we will try to retrieve the next argument, which should be the path to the new skin:
if(!aMessage->GetString(kScriptItemArgs,&lSkin,1)) { tErr lErr; TRAP(lErr,ReSkinL(lSkin)); |
Because the method can leave (ea. if the skin cannot be loaded), we use the macro TRAP() to catch any possible exceptions. The variable lErr will be different from kErrNone if we catch an exception. In that case, in the reply message we will indicates that we failed to perform the command:
if(lErr) { aReply->SetBool(kScriptItemDone,false); aReply->SetUint8(kScriptItemReason,kScriptErrFailed); } } else { aReply->SetBool(kScriptItemDone,false); aReply->SetUint8(kScriptItemReason,kScriptErrMissingArg); } } else { aReply->SetBool(kScriptItemDone,false); aReply->SetUint8(kScriptItemReason,kScriptErrIncorrect); } } else { aReply->SetBool(kScriptItemDone,false); aReply->SetUint8(kScriptItemReason,kScriptErrMalformed); } break; } |
For any other scripting command, we will pass the message to the base class method for handling:
default: cWindow::ScriptingReceived(aMessage,aReply); } } else { aReply->SetBool(kScriptItemDone,false); aReply->SetUint8(kScriptItemReason,kScriptErrMalformed); } } |
The application sending the scripting message expects to receive notice of whether or not it was successful. And if not, what the reason was for the failure. As we have seen in the above code, the reply’s item kScriptItemDone will have true or false for its value. It will indicate if the command was performed or not. If an error occurs, the reply message will also contain the item kScriptItemReason which will contain an error code.
As we are going to see, the method ReSkinL() is far from complicate:
void cDWindow::ReSkinL(const tChar *aSkin) { cDSkin lSkin(aSkin); sEnv::LeaveIfError(lSkin.GetFault()); iConsole->ReSkinL(lSkin); } |
It first creates a cDSkin object from the path of the skin we would like to use. If the object is invalid, we will leave right away. If valid, we will be calling the method ReskinL() for all the widgets of the window that needs their skin to change. For this test application,we only have the console widget to update.
cDConsoleView
In the method ReSkinL() of our console class, we perform something similar to what we have done in ConstructL(). We are going to instantiate all the bitmaps we need from the skin, then replace the old ones by the new ones. However, because the method can leave anytime, we only need to replace the old bitmaps once all the new ones have been created. This implies that we must use the cleanup stack to insure that all the bitmaps loaded before will be destroyed if we leave when we are instantiating the last bitmap of the skin. It also requires us to keep the newly created bitmap in some temporary array until they replace the old ones.
Here’s the method implementation:
void cDConsoleView::ReSkinL(cDSkin &aSkin) { cBitmap *lBackground; cBitmap *lBStates[kStatesCount]; cBitmap *lBLabelsN[kLabelsCount]; cBitmap *lBLabelsD[kLabelsCount]; // Leave if the widget is faulty sEnv::LeaveIfError(iFault); // Instantiate the background bitmap lBackground = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResBackground),false); // Instantiate the button bitmaps lBStates[kStateNormal] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResNormal)); lBStates[kStateDimmed] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResDimmed)); lBStates[kStatePressed] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPressed)); lBStates[kStateActive] = lBStates[kStateNormal]; // use same bitmap as the normal state lBStates[kStateActive1] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResAnimate1)); lBStates[kStateActive2] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResAnimate2)); lBStates[kStateActive3] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResAnimate3)); // Instantiate the label bitmaps lBLabelsN[kLabelBwd] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResBackwardN)); lBLabelsN[kLabelFwd] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResForwardN)); lBLabelsN[kLabelStop] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResStopN)); lBLabelsN[kLabelPlay] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPlayN)); lBLabelsN[kLabelPause] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPauseN)); lBLabelsD[kLabelBwd] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResBackwardD)); lBLabelsD[kLabelFwd] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResForwardD)); lBLabelsD[kLabelStop] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResStopD)); lBLabelsD[kLabelPlay] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPlayD)); lBLabelsD[kLabelPause] = CreateBitmapFromResourceLC(aSkin.GetResourceL(eResPauseD)); // delete all the bitmap we currently use // and assign the newly loaded bitmaps delete iBackground; iBackground = lBackground; for(tUint8 i=0;i<kStatesCount;i++) { if(iBStates[i] && i!=kStateActive) // Active and Normal states share the same bitmap delete iBStates[i]; iBStates[i] = lBStates[i]; } for(tUint8 i=0;i<kLabelsCount;i++) if(iBLabelsN[i]) { delete iBLabelsN[i]; iBLabelsN[i] = lBLabelsN[i]; } for(tUint8 i=0;i<kLabelsCount;i++) if(iBLabelsD[i]) { delete iBLabelsD[i]; iBLabelsD[i] = lBLabelsD[i]; } // remove the bitmaps from the cleanupstack sCleanupStack::PopL(17,lBackground); Render(kButtonsCount,true); } |
Instead of using the method CreateBitmapFromResourceL that we implemented earlier, we are using one that keeps the created bitmap on the cleanup stack even if there is no exception. Once we have replaced all the old bitmaps by the new ones, at the end of ReSkinL(), we pop all the 17 objects we have placed on the cleanup stack. The method PopL() verifies that the last object to remove is the background bitmap. If this is not the case, something is wrongx and the method will throw an exception. If everything goes according to plan, we can then redraw the whole console.
The method CreateBitmapFromResourceLC isn’t much different from the one we did earlier. It simply pushes the bitmap on the cleanup stack:
cBitmap *CreateBitmapFromResourceLC(const tBitmapResource &aRes,tBool aTransparent = true) { tErr lErr; cBitmap *lBitmap = NULL; lBitmap = new cBitmap(aRes.iWidth,aRes.iHeight,eSpaceColor8Bit,true,aRes.iColors); lErr = cBitmap::VerifyD(lBitmap); if(!lErr) { sCleanupStack::PushL(lBitmap); lBitmap->SetPalette(aRes.iPalette); lBitmap->SetBits(aRes.iBits,aRes.iLength,0); if(aTransparent) lBitmap->MakeTransparent(aRes.iIndex); return lBitmap; } else throw uException(lErr,"CreateBitmapFromResourceLC()"); } |
Now, let’s add some extra scripting abilities to the console widget itself by supporting invocations of any of the console’s buttons. By doing so, we allow remote control and automated testing capabilities, since the console can be invoked from anywhere:
#> yo hexaZen/Custom tell window.console invoke play |
Because by default the widgets do not allow scripting, we are going to allow it in the window ConstructL() method:
void cDWindow::ConstructL(cDSkin &aSkin) { sEnv::LeaveIfError(iFault); iConsole = cMyConsoleView::NewL(aSkin); iConsole->AllowScripting(eLocal); AddChild(iConsole); } |
Then, we will be declaring the method ScriptingMessageReceived() in the console class and implementing it. Here it is:
void cDConsoleView::ScriptingReceived(cMessage *aMessage,cMessage *aReply) { tUint32 lCmd; aReply->SetBool(kScriptItemDone,true); if(!aMessage->GetUint32(kScriptItemCommand,&lCmd)) { switch(lCmd) { case kScriptCmdInvoke: { const tChar *lArg; // if a button is already pressed, we will ignore this command if(iPressedButton == kButtonsCount) { if(!aMessage->GetString(kScriptItemArgs,&lArg)) { if(!strcmp(lArg,kScrButtonPlay) || !strcmp(lArg,kScrButtonStop)) { Pressed(kButtonPlay); Invoked(); } else if(!strcmp(lArg,kScrButtonPause)) { Pressed(kButtonPause); Invoked(); } else if(!strcmp(lArg,kScrButtonBwd)) { Pressed(kButtonBwd); Invoked(); } else if(!strcmp(lArg,kScrButtonFwd)) { Pressed(kButtonFwd); Invoked(); } else { aReply->SetBool(kScriptItemDone,false); aReply->SetUint8(kScriptItemReason,kScriptErrIncorrect); } } else { aReply->SetBool(kScriptItemDone,false); aReply->SetUint8(kScriptItemReason,kScriptErrMissingArg); } } break; } default: cView::ScriptingReceived(aMessage,aReply); } } else { aReply->SetBool(kScriptItemDone,false); aReply->SetUint8(kScriptItemReason,kScriptErrMalformed); } } |
If the command in the scripting message is kScriptCmdInvoke, we retrieve its argument and compare it to the label of the console’s buttons. Here’s the declaration of the constants we are using to distinguish the different buttons invoked:
const tChar *kScrButtonPlay = "play"; const tChar *kScrButtonStop = "stop"; const tChar *kScrButtonBwd = "bwd"; const tChar *kScrButtonFwd = "fwd"; const tChar *kScrButtonPause = "pause"; |
If we were to add scriptability to all the widgets of the UI, we could then support automated testing of the applications. This will allow for a more consistent approach to testing since it is fully documented, repeatable and very flexible. Saving developers from painful manual testing also helps to keep their focus on more rewarding activities. Because it is repeatable, regression testing can be performed on a regular basis. In short, scriptability can help your product achieve a better quality in a relatively painless way. It can also ease the integration of an application in a network of collaborative applications.
6. Conclusion
By using object-oriented programming, we were able to quickly build a custom widget. Inheritance permits the use of the widget in many applications, saving time and effort. The Zinzala SDK can leverage the quality of your embedded systems, while making developers lives easier. If you are developing set-top boxes, using object-oriented techniques will help you lower the time to market of your product. A goal worth the cost of the little overhead that C++ will bring.
7. Acknowledgment
The author would like to thank Chris McKillop and Eugenia Loli-Queru for reviewing this article and giving valuable feedback. Thanks also goes out to Susan for providing the various graphics and for proofreading this article.
If you would like to see your thoughts or experiences with technology published, please consider writing an article for OSNews.
also the guy who makes this makes a lot of usefull apps for QNX. I cant wait for ZenCam ( http://www.hexazen.com/Products/Software/ZenCam/ )
and I’m waiting for PhCam for 6.3
So similiar that it is scary! I guess they say that immitation is the sincerest form of flattery…
btw, does he need to check his iWindow before deleting it??
// delete the window
if(iWindow)
delete iWindow;
Hi,
You are right, checking that iWindow is not NULL is not
necessary .. it’s just an old habit I have
Zinzala is not so similar to Symbian hopefully! It just implement a couple of features that I beleive are valuable.
jean-louis
I’ve installed QNX Momentics on my laptop.
I believe it will expire after Jan 10th.
No where in the site it says about pricing.
I mean I was just curious about the OS given that it is based on the micro-kernel approach rather than monolithic.
It’s meant to be faster and more responsive.
But the Window-Manager is very similar to ice-wm
no anti-alias on the fonts.
avant web-browser crashes.
again i just installed out of curiosity – interesting experience – but will have to uninstall it very soon.
just saw the website ..
Susan is such a babe!
* my heart pounds *
and married.
damn
my wheel mouse doesn’t work
and its voyager that crashes (not avante)
and everything here is so very not anti-aliased
pah!
oh the urgency to uninstall this OS
i feel a silent pressure here
similar to asking for resignation before being fired.
QNX Photon does Anti-Aliasing, you just have to turn it ON
in the Settings ๐
jl
Most of QNX will work after the expiry date. It is only the QSS Momentics development tools that will stop working (PhAB, qcc, QCC, etc.) All of the basic OS will keep working, like Photon, gcc, ls, mv, etc. This is what is called the non-commercial version of QNX. There is quite an active community involved with the NC version of QNX – see http://www.openqnx.com.
Speaking of QNX related websites, there is also QNXZone :
http://www.qnxzone.com
It will be a shame to see it going.
I was quite liking it.
And thanks for the anti-aliased setting
mmm .. have to locate the settings now
any answer why the scroll mouse doesn’t work?
the best feature i liked so far is the package manager
that was extremely cool.
thanks for the links.
looks like I won’t be uninstalling the OS then.