Up to: Mika Raento's Symbian Programming pages

Symbian Programming - A floating, wordwrapped textbox

Note that this obviously only works for left-to-right top-to-bottom languages.

I want to display hints in Merkitys-Meaning, as Opera does. Try to teach the user how to navigate and interact with the application. The first step is to build a class that can show the hint text in a nice way. This is that class:

#define MAX_LINES 20

class CTextBox : public CCoeControl {
    /* This class will draw a floating window with some
       text, nicely word-wrapped. You can anchor to window
       to any of the for corners. It'll calculate the minimum
       size needed based on the maximum width given, and use
       that */
public:
    enum TPosition {
        ETopLeft,
        ETopRight,
        EBottomLeft,
        EBottomRight
    };
public:
    TRgb iBgColor, iFgColor;

    void ZeroRect() {
        SetRect( TRect(TPoint(0, 0), TSize(0, 0)) );
    }
    void ConstructL(TRgb aBackgroundColor, TRgb aForegroundColor,
            TRgb aBorderColor, TInt aBorder, TInt aMargin) {
        /* if we just CreateWindowL() we get a 'floating' window,
           since each new window is created on top of the 
           previous ones. No key events will be routed if
           we don't add it to the control stack.*/
        CreateWindowL();
        /* default window covers the whole screen, we
           don't want that in case somebody Activate()s
           without calling SetText() */
        ZeroRect();

        iBgColor=aBackgroundColor;
        iFgColor=aForegroundColor;
        iFont=CEikonEnv::Static()->DenseFont();
        /* 1.1 times font height is a standard estimate
           for line height when we don't know any better */
        iLineHeight=iFont->HeightInPixels()*1.1;
        iMargin=aMargin;
        iBorder=aBorder;
        iBorderColor=aBorderColor;
    }
    void Draw(const    TRect& aRect) const {
        /* we don't care if aRect is actually smaller than
           this window:
            a) it won't normally happen, since this will
            float on top of all other windows in the app,
            and menus and other transient windows use a
            copy of the window below to redraw
            b) if we did, we'd have to worry about how to
            draw the border correctly, instead of just
            using the pen on the rect
        */
        CWindowGc& gc =    SystemGc();
        gc.SetPenStyle(CGraphicsContext::ESolidPen);
        gc.SetPenSize(TSize(iBorder, iBorder));
        gc.SetPenColor(iBorderColor);
        gc.SetBrushColor(iBgColor);
        gc.SetBrushStyle(CGraphicsContext::ESolidBrush);
        gc.DrawRect(Rect());

        if (! iText || iText->Length()==0) return;

        gc.UseFont(iFont);
        TInt baseline=iFont->HeightInPixels()-iFont->DescentInPixels();
        TPoint p(iBorder+iMargin, baseline+iBorder+iMargin);
        TInt pos=0;
        gc.SetPenStyle(CGraphicsContext::ESolidPen);
        gc.SetPenColor(iFgColor);
        for (int i=0; i<iCurrentLineCount; i++) {
            /* pos will be one over length of string at end of
               loop */
            TPtrC text=(*iText).Mid(pos, iLines[i]-pos);
            gc.DrawText(text, p);
            pos=iLines[i];

            /* we have to check whether the line was broken
               at whitespace, and advance if so */
            if ( pos<(*iText).Length() && 
                TChar((*iText)[pos]).IsSpace() ) pos++;
            p.iY+=iLineHeight;
        }
        gc.DiscardFont();
    }
    const CFont* iFont;
    TInt iLines[MAX_LINES];
    TInt iCurrentWidth, iCurrentLineCount, iLineHeight;
    TInt iMargin, iBorder; TRgb iBorderColor;
    HBufC* iText;
    /* doesn't take ownership of the text, you need to make
       sure its lifetime is long enough*/
    void SetText(TInt aMaxWidth, HBufC* aText, TPosition aPosFrom,
            TPoint aPos) {
        iText=aText;
        if (! iText || iText->Length()==0) {
            ZeroRect();
            return;
        }

        /* Calculate the line breaks. We go one over
         * the string length in the loop so that the 
         * last line width is calculated.
         */
        TInt i, w, l, previous_space=0, max_w=0, previous_w=0;
        for (i=0, w=0, l=0; i<=aText->Length() && l<MAX_LINES; i++) {
            if ( i==aText->Length() || TChar((*aText)[i]).IsSpace()) {
                previous_w=w;
                previous_space=i;
            }
            TInt this_w=0;
            if (i<aText->Length()) 
                this_w=iFont->CharWidthInPixels( (*aText)[i] );
            if (i==aText->Length() || w+this_w > aMaxWidth) {
                if (previous_space==0) {
                    /* if there is no break opportunity,
                       we just cut the word */
                    previous_w=w;
                    previous_space=i-1;
                }
                if (previous_w > max_w) max_w=previous_w;
                iLines[l]=previous_space;
                l++;
                i=previous_space;
                previous_space=0;
                w=0;
            } else {
                w+=this_w;
            }
        }
        iCurrentLineCount=l;
        iCurrentWidth=max_w+2*iMargin+2*iBorder;
        TPoint tl; 
        TSize s(iCurrentWidth, iCurrentLineCount*iLineHeight+2*iMargin+2*iBorder);
        switch(aPosFrom) {
            case ETopLeft:
                tl=aPos;
                break;
            case ETopRight:
                tl.iX=aPos.iX-s.iWidth;
                tl.iY=aPos.iY;
                break;
            case EBottomLeft:
                tl.iX=aPos.iX;
                tl.iY=aPos.iY-s.iHeight;
                break;
            case EBottomRight:
                tl.iX=aPos.iX-s.iWidth;
                tl.iY=aPos.iY-s.iHeight;
                break;
        }
        SetRect(TRect(tl, s));
    }
};

Usage is simple, something like:

    /* CTextBox* iFloating; HBufC* iText; */
    iFloating=new (ELeave) CTextBox;
    iFloating->ConstructL(KRgbBlue, KRgbWhite, KRgbBlack, 1, 2);
    _LIT(KText, "testing and testing and testing andthisistoolongtobreaknicelyisntit");
    iText=KText().AllocL();
    iFloating->SetText(70, iText, CTextBox::EBottomLeft, TPoint(10, 180));
    iFloating->ActivateL();

Mika Raento, mikie(at)iki.fi