|
||
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
customisations. 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.
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).
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.
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.