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:
Implementing the Object Provider (MOP) interface
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 }
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()
.
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.
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.
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.
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.
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; ...
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() ; }
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
.
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.