How to write controls

Cone itself does not provide any concrete controls. Uikon and the UI variant libraries provide a large number of 'stock' controls for application writers. Application writers often need to supplement the standard set of controls with application specific controls of their own. These may be completely new controls or, more often, compound controls which contain a number of standard controls.

This section describes how to create controls and how to integrate them in to the control framework. It is divided into the following sections:

Creating a control

Window owning or not?

Creating a compound control

Size, position and layout

Drawing and refreshing

Drawing backgrounds

Drawing text

Drawing graphics

Handling events

Implementing the Object Provider (MOP) interface

Creating a control

A control is a class which derives from CCoeControl. It should have a public constructor and, if any leaving function calls or memory allocations are required during construction, a ConstructL() function. The majority of re-useable and configurable controls have a ConstructFromResourceL() function which allows a specific instance of a control to be configured using an application's resource file. Obviously any memory allocated must be freed in the destructor. Before a control is drawn to the screen it must be activated. The ActivateL() function may be overriden to perform last-minute configuration (but must call the function in the base class).

Class CMyControl : public CCoeControl
    {
    public:
        CMyControl() ;
        void ConstructL(...) ;
        // from CCoeControl
        void ConsructFromResourceL( TResourceReader& aReader ) ; 
    private:
        ~CMyControl() ;

    // additional functions to handle events 
    // additional functions to draw the control
    // additional functions to determine the size, layout and position the control
    }

Window owning or not?

The decision over whether to make a control window owning or not is usually straightforward. Each view requires a window, so the top-level control must be window-owning and a child of the AppUi. Below this a window owning control is normally only necessary where a sense of layering is required: for instance a pop-up window or a scrolling window. Dialogs and menus are window owning controls but these are normally implemented in the Uikon and UI variant libraries and do not require custom derivation from CCoeControl. Unnecessary window-owning controls should be avoided as they require more infrastructure, place greater demand on the Window Server and reduce performance.

If a control must be window owning its window must either be created in the ConstructL() function or by the caller. The former is preferred. There are several overloads of the CreateWindowL() and CreateBackedUpWindowL() functions. Those which do not take a parent parameter create a top-level window which is a child of the root window.

If a control is not window owning its SetContainerWindowL() function must be called when it is instantiated.

If it can, the Framework will automatically set up the parent pointer when the window, or associated window relationship is established. If it cannot do this, because CreateWindowL() or SetContainerWindowL() did not provide a CCoeControl, the parent pointer (and MopParent) may be set expicitly using SetParent() and SetMopParent().

Creating a compound control

Most applications UIs are built from compound controls. Many custom controls are built up from stock controls and are therefore also compound controls. When a compound control is constructed it constructs its components in its ConstructL() function. When it receives commands itself, such as ActivateL() and DrawNow() it passes them on to each of its components. In most cases the Framework does much of the donkey work as long as the compound control has been constructed correctly.

There are now two methods of creating and managing lodger controls. The first method described is the one that should be used.

void MyControl::ConstructL( ... )
    {
    // initialise the component array. This must be called once (subsequent calls have no effect)
    InitComponentArrayL() ; 

    // construct each component control and add it to the component array.
    CComponent* myComponent = new (ELeave) CComponent ;
    Components().AppendLC( myComponent ) ; // or InsertLC or InsertAfterLC().  Places item on cleanup stack.
    myComponent->ConstructL() ;
    myComponent->SetThisAndThatL() ;
    CleanupStack::Pop( myComponent ) ;    
    }

The return value of the insert and append methods is a CCoeControlArray::TCursor object which works as an iterator. It will remain valid when other items are inserted or deleted, or even if the whole array is re-ordered.

The insert and append methods leave the component on the Cleanup Stack using a dedicated Cleanup Item that protects the parent's array as well as the component itself.

The insert and append methods allow each component to be given an ID. The ID must be unique only within the parent so typically a compound control will have an enum listing each of its children's IDs. CCoeControlArray , accessed using CCoeControl::Components(), has a ControlById() method to retrieve components using their IDs.

Components in the array are, by default, owned by the parent and will be deleted automatically when the parent is deleted. The default may be overridden using CCoeControlArray::SetControlsOwnedExternally(). The setting applies to all of the components.

Controls may be removed from the array using one of the Remove() methods. These do not delete.

class CCoeControlArray
        ...
    public:
        IMPORT_C TInt Remove(const CCoeControl* aControl);
        IMPORT_C CCoeControl* Remove(TCursor aRemoveAt);
        IMPORT_C CCoeControl* RemoveById(TInt aControlId);
        ...

Using the component array as described is now the approved method of constructing and managing compound controls. In older versions of Symbian OS a specific method of handling components was not provided and developers were obliged to design their own. Bypassing the component array is still possible. It is necessary to allocate and store the components (typically as member data) and to implement the CountComponentControls() and ComponentControl() functions to return the number of components and a specified component to the framework. The new method offers significant advantages when controls are added and removed dynamically or are dependant on run-time data. The new method is also integrated with new layout managers.

Size, position and layout

There are several factors which contribute to a control's size and position. The control itself will require a certain size in order to display itself (and its data) correctly. The control's container will be responsible for positioning the control but is also likely to be responsible for positioning other controls - each of which will have its own requirements. Additionally there are the requirements of the UI's look and feel that must be complied with.

Each control is responsible for implementing its own Size() function.

Until Symbian OS version 9.1 it was normal to write layout code for simple and compound controls in the SizeChanged() function. This is called by the framework, as one might expect, when a control's size (its 'extent') is changed. From 9.1, however, Symbian OS supports the use of the layout manager interface (MCoeLayoutManager) and the SizeChanged() function is now implemented in the base class. (Note that if a control's position is changed, with no size change, using CCoeControl::SetPosition() its PositionChanged() function is called and that default implementation of PositionChanged() is empty).

class MCoeLayoutManager
        ...
    protected:
        IMPORT_C MCoeLayoutManager();
    
    public:
        virtual TBool CanAttach() const = 0;
        virtual void AttachL(CCoeControl& aCompoundControl) = 0;
        virtual void Detach(CCoeControl& aCompoundControl) = 0;
        virtual TSize CalcMinimumSize(const CCoeControl& aCompoundControl) const = 0;
        virtual void PerformLayout() = 0;
        virtual TInt CalcTextBaselineOffset(const CCoeControl& aCompoundControl, const TSize& aSize) const = 0;
        virtual void SetTextBaselineSpacing(TInt aBaselineSpacing) = 0;
        virtual TInt TextBaselineSpacing() const = 0;
        virtual void HandleAddedControlL(const CCoeControl& aCompoundControl, const CCoeControl& aAddedControl) = 0;
        virtual void HandleRemovedControl(const CCoeControl& aCompoundControl, const CCoeControl& aRemovedControl) = 0;
        virtual TInt HandleControlReplaced(const CCoeControl& aOldControl, const CCoeControl& aNewControl) = 0;
        ...

A layout manager may be attached to a compound control.

class CCoeControl
        ...
    protected: 
        IMPORT_C MCoeLayoutManager* LayoutManager() const;
        IMPORT_C virtual void SetLayoutManagerL(MCoeLayoutManager* aLayoutManager);

    public:
        IMPORT_C virtual TBool RequestRelayout(const CCoeControl* aChildCtrl);
        ...

The default implementations of MinimumSize() and SizeChanged() now use the layout manager.

EXPORT_C TSize CCoeControl::MinimumSize()
    { 
    const MCoeLayoutManager* layoutManager = LayoutManager();
    if (layoutManager)
        return layoutManager->CalcMinimumSize(*this);
    else    
        return iSize;
    }

EXPORT_C void CCoeControl::SizeChanged()
    {
    MCoeLayoutManager* layout = LayoutManager();
    if (layout)
        layout->PerformLayout();

The layout manager is responsible for the size and position of the component controls. In practice it's likely that the UI variant libraries will provide concrete layout managers. Application developers should use these as the basis for control-specific layout managers.

Drawing and refreshing

A fundamental requirement of most controls is that they are able to render themselves onto the screen. For most controls the drawing process involves outputting text, painting backgrounds (either plain or from a bitmap), drawing shapes (graphics objects) and drawing component controls.

Screen drawing may be initiated by the application itself, following something within the application changing, or by the Window Server, due to something else in the system updating the screen while the application is visible. In both cases the control's Draw() function will be called automatically by the framework. For compound controls all of the components' Draw() functions will also be called - unless the component lies completely outside the area that requires redrawing.

As a control writer you will probably have to implement a Draw() function.

Here is the signature for Draw():

private:
    void Draw( const TRect& aRect ) const ;

Note that it is private, takes a const TRect& as a parameter, must not leave and is const.

It should only be called by the framework. Application initiated redraws should be through calls to DrawNow(), DrawDeferred() or custom functions for drawing smaller elements.

The aRect parameter is the part of the control that requires drawing (refreshing).

The function is const and non-leaving because it is intended to support the decoupling of drawing actions from application state.

Drawing backgrounds

A control's background is typically determined by the current colour scheme or skin. It may be a plain colour or a bitmap. It's also possible that a control is to appear non-rectangular or transparent in which case some of the background will be the control underneath. Prior to Symbian OS 9.1 controls were required to clear and update their whole area and creating these effects was rather complex. From 9.1 controls are drawn 'backmost first'.

Background drawing should be done by a dedicated background drawer - i.e. an object which implements the MCoeControlBackground interface. A background can be attached to a CCoeControl using SetBackground() and is used for that control and all of its children. When a control is drawn the framework looks for the nearest background up the run-time hierarchy and calls MCoeControlBackground::Draw().

UI variant libraries typically provide backgrounds. They are not owned by the controls to which they are attached.

Drawing text

Text must be drawn with the correct color, font, size and direction. As with backgrounds, these are determined at runtime according to UI customizations. This is achieved by means of a Text Drawer. Note the use of the XCoeTextDrawer class. This is a smart pointer (note the use of the -> operator to access CCoeTextDrawerBase functions) which ensures that only one text drawer is allocated on the heap at a time.

XCoeTextDrawer textDrawer( TextDrawer() );
textDrawer->SetAlignment(iAlignment); 
textDrawer->SetMargins(iMargin);
textDrawer->SetLineGapInPixels(iGapBetweenLines);
textDrawer.SetClipRect(aRect); // have to use . [dot] operator for SetClipRect() as not CCoeTextDrawerBase function.

textDrawer.DrawText(gc, *iTextToDraw, Rect(), *Font());

Text drawers are typically provided by the UI variant library or skin manager. Controls within the run-time hierarchy can set the text drawer for their children by overriding GetTextDrawer().

Note that the text drawer expects text to be passed as a TBidiText rather than a descriptor. Controls should store all display text in TBidiText objects. Application writers should consider the implications of right-to-left layouts for languages such as Hebrew and Arabic.

A control's GetTextDrawer() function might look something like this. It checks on the current state of the control (IsDimmed()) and passes the call on to a skin manager.

EXPORT_C void CMyButtonControl::GetTextDrawer(CCoeTextDrawerBase*& aTextDrawer, const CCoeControl* aDrawingControl, TInt /*aKey*/) const
    {
    const TInt textDrawerIndex = (IsDimmed() ? EButtonTextDimmed : EButtonText);

    SkinManager::GetTextDrawer(aTextDrawer, KSkinUidButton, textDrawerIndex, aDrawingControl);
    }

If the control is drawing text on its own graphics (and does not care about the text drawer of its parents) it can just create an XCoeTextDrawer object on the stack in its Draw() method and initiate it from the skin that it is currently using to draw its graphics, using the CSkinPatch::TextDrawer() method, like this:

const CSkinPatch& skin = SkinManager::SkinPatch(KSomeSkinUid, KSomeSkinIndex, this);

skin.DrawBitmap(gc, Rect(), aRect);
XCoeTextDrawer textDrawer( skin.TextDrawer(KSomeSkinUid, ESomeSkinTextDimmed, this) );

const CFont& font = ScreenFont(TCoeFont::NormalFont);
textDrawer.DrawText(gc, iText, rect, font);

The example above also illustrates how to retrieve the correct font. CFont objects must not be stored in control member data as they must change when the control's zoom state changes. Instead, a TCoeFont that represents a font's size (logical or absolute in pixels) and style (plain, bold, italic, subscript, or superscript) should be used.

class TCoeFont 
        ...
    public: 
        IMPORT_C TCoeFont(TLogicalSize aSize, TInt aStyle, TInt aFlags = ENoFlags); 
        IMPORT_C TCoeFont(TInt aHeightInPixels, TInt aStyle, TInt aFlags = ENoFlags); 
        IMPORT_C TCoeFont(const TCoeFont& aFont);
        IMPORT_C TCoeFont();
        ...

By creating a TCoeFont object describing the properties of the desired font, a CFont object reference (needed to actually draw the text) can be fetched from the CCoeFontProvider. A font provider can be attached to any CCoeControl and will keep information about the typeface used by that control and all controls below. A default font provider is attached to the CCoeEnv.

class CCoeControl
        ...
    public:
        IMPORT_C const CCoeFontProvider& FindFontProvider() const;
        IMPORT_C void SetFontProviderL(const CCoeFontProvider& aFontProvider);
        ...
        

To get hold of the CFont object a Draw() method can be implemented like this:

void CMyControl::Draw(const TRect& aRect)
    {
    const CCoeFontProvider& fontProvider = FindFontProvider();
    const CFont& font = fontProvider.Font(TCoeFont::LegendFont(), AccumulatedZoom());

    XCoeTextDrawer textDrawer( TextDrawer() );
    textDrawer->SetAlignment(EHCenterVCenter);
    textDrawer.DrawText(gc, iText, rect, font);
    }

For convenience there’s a CCoeControl::ScreenFont() method that locates the font provider and calls it with the control’s accumulated zoom:

class CCoeControl
        ...
    protected:
        IMPORT_C const CFont& ScreenFont(const TCoeFont& aFont) const;
        ...

Drawing graphics

Controls draw graphics objects - lines, rectangles, shapes and bitmaps to a graphics context. The graphics context is provided by the Window Server and represents a group of settings appropriate for the physical device that is ultimately being drawn to. In most cases the device is a screen and a graphics context should be obtained using CCoeControl::SystemGc(). CCoeControl::SystemGc() gets the current graphics context from the run-time hierarchy. Controls in the hierarchy may override graphics context settings which will then be passed on to their children. Controls should not get their graphics context directly from CCoeEnv as to do so would bypass the hierarchy.

void CMyControl::Draw( const TRect& aRect )
    {
    CWindowGc& gc = SystemGc() ; // get gc from run time hierarchy
    TRect rect = TRect( Size() ) ;
    if ( IsBlanked() )
        {
        // blank out the entire control
        gc.SetPenStyle( CGraphicsContext::ENullPen ) ;
        gc.SetBrushStyle( CGraphicsContext::ESolidBrush ) ;
        TRgb blankColor = BlankingColor() ;
        gc.SetBrushColor( blankColor ) ;
        gc.DrawRect( rect ) ;
        }
    else
        {
        // draw masked bitmap in the centre of the control 
        // The parent will draw the background 
        TInt id = BitMapId() ;

        TInt x = Size().iWidth - iBitmap[id]->SizeInPixels().iWidth ;
        TInt y = Size().iHeight - iBitmap[id]->SizeInPixels().iHeight ;

        TPoint pos = Rect().iTl ;
        pos.iX = pos.iX + ( x / 2 ) ;
        pos.iY = pos.iY + ( y / 2 ) ;

        gc.BitBltMasked( pos, iBitmap[id], rect, iMaskBitmap, ETrue ) ;
        }
    }

Before a graphics context can be used it must be activated. After use it must be deactivated. Activation and deactivation are done automatically by the framework in DrawNow(), DrawDeferred() and HandleRedrawEvent() but must be done explicitly for any other application initiated drawing by calling ActivateGc() and DeactivateGc().

Controls may implement partial drawing to speed up performance. The Draw() function may be split into sub functions: DrawThis(), DrawThat(), DrawTheOther(). Each of these requires a corresponding DrawThisNow() and/or DrawThisDeferred() function.

CMyControl::Draw()
    {
    DrawThis() ;
    DrawThat() ;
    DrawTheOther() ;
    }
CMyControl::DrawThisNow()
    {
    ActivateGc() ;
    DrawThis() ;
    DeactivateGc() ;
    }

Handling events

The Control Framework supports user interaction in two ways: key-press events and pointer events. Both types of event arrive through the Window Server though they each arrive in a slightly different way. Both are closely related to the concept of 'focus' and the location of the cursor.

Handling key events

Key events are delivered to the AppUi. The Window Server channels them through the root window of its current window group which maps to the AppUi foreground application. The AppUi offers each key event to each of the controls on its control stack in priority order until one of the controls 'consumes' it.

To receive key events a control must implement CCoeControl::OfferKeyEventL(). If it handles the event it must return EKeyWasConsumed: If it doesn't it must return EKeyWasNotConsumed so that the next control on the stack receives it.

TKeyResponse CMyControl::OfferKeyEventL( const TKeyEvent& aKeyEvent, TEventCode aType)
    {
    TKeyResponse returnValue = EKeyWasConsumed ;
    switch( aKeyEvent.iCode ) 
        {
        case EKeyEnter :
            // do stuff
            break ;
        case EKeyEscape :
            // do stuff :
            break ;

            ...
                
        default :
            // did not recognise key event
            returnValue = EKeyWasNotConsumed ;
            break ;
        }
    return returnValue ;
    }

The handling of key events will depend on the design and purpose of the control itself. Compound controls might need to keep track of an input focus, or cursor, and to pass key events amongst its lodgers. Input into one lodger might have an effect on another - pressing a navigation key might cause one control to lose the highlight and another to gain it, pressing a number key might cause a text editor to grow which might, in turn, require all of the components below it to shuffle downwards and a scroll bar to become visible (which might also require some controls to be laid out differently).

Handling pointer events

Pointer events are slightly different as the position of the pointer, rather than of the focus, is significant. The Window Server passes a pointer event to the top-most visible window at the point of contact. The Framework uses the functions ProcessPointerEventL() and HandlePointerEventL() to work down the hierarchy. The Framework also uses the MCoeControlObserver and focussing mechanisms to inform the observer of the controls that will be losing and gaining the focus.

Using the Control Observer Interface

The Control Framework facilitates this type of relationship between a container and its lodgers with the MCoeControlObserver interface. Typically the container implements the interface and becomes the observer for each lodger that can receive user input (focus). There is only one function in MCoeControlObserver:

virtual void HandleControlEventL( CCoeControl *aControl, TCoeEvent aEventType ) = 0 ;

and it is called when an observed control calls

void CCoeControl::ReportEvent( MCoeControlObserver::TCoeEvent aEvent ) ;

A control can have only one observer (or none) so ReportEvent() does not need to specify an observer. An observer may observe any number of controls so HandleControlEventL() takes the observed control as a parameter. The other piece of information passed to the observer is a TCoeEvent.

enum TCoeEvent
    {
    EEventRequestExit,
    EEventRequestCancel,
    EEventRequestFocus,
    EEventPrepareFocusTransition,
    EEventStateChanged,
    EEventInteractionRefused
	 };

CCoeControl also provides IsFocused(), SetFocused() and IsNonFocussing(). Note that Framework does not attempt to ensure exclusivity of focus, nor does it give any visible indication of focus. It is up to the application developer to ensure that only one control has the focus at a time, that the focus is correctly transferred between controls, that only appropriate controls receive the focus and that the focus is visible at all times.

void CContainer::HandleControlEventL(CCoeControl* aControl, TCoeEvent aEventType)
    {
	 switch (aEventType)
        {
		   case EEventRequestFocus:
			    {
			    if( !(aControl->IsFocussed()) )
				     {
				     aControl->SetFocus( ETrue ) ;
				     // remove focus from other controls
				     for ( Tint ii = 0 ; ii < CountComponentControls() ; ii++ ) 
                     {
					       CCoeControl* ctl = ComponentControl( ii ) ;
					       if( ( ctl != aControl ) && !( ctl->IsNonFocussing() ) )
						        {
						        aControl->SetFocus( EFalse ) ;
						        }
					       }
				      }
			     }
			     break;
	           ...
		     }
	    }

Control developers may implement HandlePointerEventL(), which is a virtual function, to perform pointer event functionality. The implementation must, however, call the base class function.

Controls may modify their pointer area, possibly if they appear non-rectangular or overlap. To do so requires the addition of a hit test which describes a hit-test region. A hit-test region may cover all or part of one or more controls. A hit for a control is registered in the area covered by both the control and its associated hit test.

The diagram below represents three controls, each of which is rectangular but which appears on the screen as a non-rectangular bitmap. Only a hit on a bitmap area should register. This could be achieved by defining a single hit-test region in the shape (and position) of the three blue areas and associating it with each of the controls. The class that implements the hit-test region must implement the MCoeControlHitTest interface.

Figure: Hit-test region example

class MCoeControlHitTest
        ...
    public:
        virtual TBool HitRegionContains( const TPoint& aPoint, const CCoeControl& aControl ) const = 0;

A hit test is associated with a control using CCoeControl::SetHitText(). The base class implementation of HandlePointerEventL() performs the following test:

    ...
    const MCoeControlHitTest* hitTest = ctrl->HitTest() ;
    if( hitTest )
        {
        if( hitTest->HitRegionContains( aPointerEvent.iPosition, *ctrl ) &&
                        ctrl->Rect().Contains( aPointerEvent.iPosition ) )

Note that this is performed by a container when deciding which lodger to pass the event onto. This snippet also illustrates how a control can find where (iPosition) the pointer event actually occurred.

Pointer support includes dragging & grabbing. See TPointerEvent.

Implementing the Object Provider (MOP) interface

The Object Provider mechanism exists to allow a control to call a function on another control in the hierarchy for which it does not have a reference. It simply calls MopGetObject() specifying the interface containing the function. It may also call MopGetObjectNoChaining() to inquire of a specific object whether it supports the requested interface.

Only controls which wish to supply an interface require customisation. In order to be identifiable an interface must have an associated UID. The following code samples show how CEikAlignedControl implements and supplies MEikAlignedControl:

class MEikAlignedControl
        ...
    public:
        DECLARE_TYPE_ID( 0x10A3D51B )  // Symbian allocated UID identifies this interface
        ...
class CEikAlignedControl : public CCoeControl, public MEikAlignedControl
    {
        ...
    private: //from CCoeControl
        IMPORT_C TTypeUid::Ptr MopSupplyObject( TTypeUid aId ) ;
        ...
EXPORT_C TTypeUid::Ptr CEikAlignedControl::MopSupplyObject( TTypeUid aId )
    {
    if( aId.iUid == MEikAlignedControl::ETypeId )
        return aId.MakePtr( static_cast<MEikAlignedControl*>( this ) ) ;

    return CCoeControl::MopSupplyObject( aId ) ; // must call base class!
    }

To get an interface from the object provider framework the caller must use a pointer to the interface.

    ...
    MEikAlignedControl* alignedControl = NULL ;
    MyControl->MopGetObject( alignedControl ) ;
    if ( alignedControl )
        {
        ... // etc.

To get an interface from a specific object the caller may use the no-chaining function call.

    ...
    MEikAlignedControl* alignedControl = NULL ;
    aControl->MopGetObjectNoChaining( alignedControl ) ;
    if ( alignedControl )
        {
        ... // etc.