博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

htmlcontrol-for-symbian 源码解析

Posted on 2011-10-24 15:28  浪端之渡鸟  阅读(507)  评论(0编辑  收藏  举报
正文:
        ytom哥的HtmlControl是一个开源的轻量级的HTML/CSS解析和渲染的控件,可以轻松支持复杂的界面效果,也可以用来显示Web内容。大家可以从http://code.google.com/p/htmlcontrol-for-symbian/获得,首先感谢ytom哥给大家提供了这么好的一个选择。
        本文主要从源代码的角度来分析HtmlControl, 有需要从应用的角度多了解的朋友们可以到前面那个网址看ytom哥的例子。




HtmlControl使用方法:
        代码中最先与大家接触的就是CHtmlControl啦,它就是我们与此控件交互的接口,使用很简单,就像标准控件一样创建:
iControl = CHtmlControl::NewL(NULL);
iControl->SetMopParent(this);
iControl->SetRect(ClientRect());
iControl->SetEventObserver(this);
iControl->ActivateL();
AddToStackL(iControl);
        然后将包含html字符的描述符加入控件并刷新就行了:
                        _LIT(KHtml, "<body style='overflow:auto'><p align='center'>"
                        "<font size='large' color='#FF0000'>Hello World!</font><br>"
                        "<font size='20'><b><i>Hello World!</i></b></font><br>"
                        "<text res='"MAKESTR(R_COMMAND1_TEXT)"'><br>"
                        "<div style='line-height:+30'>Hello World!</div><br>"
                        "<a href='http://www.abc.com'>Hello World!</a><br>"
                        "<a href='#abc'><text res='"MAKESTR(R_COMMAND1_TEXT)"'></a><br>"
                        "</p>"
                        );
                        iControl->AppendContentL(KHtml);
                        iControl->RefreshAndDraw();
        这样就得到了本文第一张图片那样的界面。

CHtmlControl的介绍
        CHtmlControl最重要的两个函数就是上面的AppendContentL和RefreshAndDraw,其他的有InsertContentL,在指定位置插入html代码。而Element和ElementByTag则通过元素ID和元素标签两种方法得到元素实例的指针供下一步操作。FocusedElement直接得到处于焦点状态的元素。SetEventObserver通过Symbian Observer模式指定得到htmlcontrol里事件的实例,比如得到EOnClick事件后可以通过aEvent.iElement->GetProperty(KHStrHref,buf)得到用户点的超链接。

解析Html的流程
        CHtmlControl里面包含了CHtmlControlImpl实例的一个指针,实际上几乎所有操作都是通过此指针间接的调用CHtmlControlImpl来操作的,由于使用这种设计隐藏了实现的细节,因此使用此控件的时候会觉得很简单,用作者的话来讲就是“发布的头文件可以干净很多”。
        CHtmlControlImpl最重要的三个函数就是InsertContentL、 ParseL、Refresh,前两个与解析html有关,第三个是解析完成后显示阶段使用。按照程序流程,现在先介绍前两个,CHtmlControl里的AppendContentL、InsertContentL实际上是间接的调用这里的InsertContentL,然后在函数内部又调用了ParseL,在ParseL里又继续调用CHtmlParser里的ParseL,在CHtmlParser的ParseL又调用HcUtils里面EnumTag将Html文档解析为一个个的标签,形如:<a …></a>、<br />,然后在CHtmlParser里调用AppendElementL将完整的标签加入到一个链表里,这个链表完整的串联了所有的从CHtmlElementImpl继承而来的实例,表示各个html元素。现在可以支持的html标签有body、img、a、div、form、input、select、textarea等等,对应的从CHtmlElementImpl继承而来的类是CHtmlElementBody、CHtmlElementImg、CHtmlElementA、CHtmlElementDiv、CHtmlElementForm、CHtmlElementInput、CHtmlElementSelect、CHtmlElementTextArea,而实际上CHtmlElementImpl也是从CHtmlElement继承而来,CHtmlElement保存了一个html元素基本的信息,比如它的Id,标签名等等。
        下面有个简单的序列图可以参考:

        AppendElementL继续调用HtmlParser里的ParseTag,在ParseTag将循环调用HcUtils里的EnumAttribute,将一个完整的标签分解为一个个的属性,比如:
        "<a href='http://www.abc.com'>Hello World!</a>"的属性为href值为http://www.abc.com,创建一个CHtmlElementA并调用它的SetProperty将这些数据保存到CHtmlElementA里,对于不支持的属性将忽略,然后把CHtmlElementA加入到元素队列,用于后面遍历元素,计算各元素的位置并显示。
        每个元素都有SetProperty和GetProperty,负责把自己支持的属性保存起来,不支持的忽略,比如CHtmlElementA:
TBool CHtmlElementA::SetProperty(const TDesC& aName, const TDesC& aValue)
{//先调用基类的,用以处理所有元素的共有属性,比如id、tag、name等等。
        if(CHtmlElementImpl::SetProperty(aName, aValue))
                return ETrue;
        //以下处理不同的元素的不同属性
        if(aName.CompareF(KHStrHref)==0) 
        {//href
                delete iHref;
                iHref = aValue.AllocL();
        }
        else if(aName.CompareF(KHStrTarget)==0) 
        {//target
                delete iTarget;
                iTarget = aValue.AllocL();
        }
        else if(aName.CompareF(KHStrInnerText)==0)
        {
                ClearContent();
                
                if(aValue.Length()>0)
                {
                        CHtmlElementText* sub = new (ELeave)CHtmlElementText(iOwner);
                        sub->iParent = iParent;
                        CleanupStack::PushL(sub);
                        sub->PrepareL();
                        sub->SetTextL(aValue);
                        iOwner->Impl()->InsertContent(sub, sub, this, EAfterBegin);
                        CleanupStack::Pop();//sub
                }
                …………………………
        }
        else
                return EFalse;        //对于暂时不支持的属性则忽略
        return ETrue;
}

整个循环结束后,html字符解析就基本结束,下面就是要刷新显示了。

HtmlControl的刷新和显示
        使用的时候很简单,只要调用CHtmlControl的RefreshAndDraw就行了,
void CHtmlControl::RefreshAndDraw()
{
        Refresh();
        Window().Invalidate(Rect());
}
        其中,最重要的就是Refresh,它通过自己保存的CHtmlControlImpl实例指针间接的调用了CHtmlControlImpl的Refresh,这个实现可不简单,遍历先前创建的元素队列,对每个元素进行了3个操作,Measure、Layout、Refresh,Measure主要根据解析的结果对元素的风格、大小、位置进行了设置,Layout主要是对div类型的元素进行设置,其他的采用基类的通用的方法设置本元素的坐标位置,部分元素使用Refresh重新计算内部文字的位置等信息。
        前期准备工作做完后,就可以画出来了,最主要的就是CHtmlControlImpl的Draw,然后通过DrawOffscreen遍历元素列表,调用每个元素的Draw依次画在新创建的CFbsBitGc上,最后再一次性的把CFbsBitGc上的内容画到SystemGc上,核心代码如下:
void CHtmlControlImpl::DrawOffscreen()
{
        iOffScreenBitmap->Gc().CancelClippingRect();
        CHtmlElementImpl* current = iBody;
        do
        {
                if(!current->iState.IsSet(EElementStateHidden))
                        current->Draw(iOffScreenBitmap->Gc());
                current = current->iNext;
        }
        while(current && current!=iBody);
        iState.Clear(EHCSNeedRedraw);
}

按键事件处理
        同系统标准控件一样,按键控件的入口是CHtmlControl的OfferKeyEventL,间接的调用了CHtmlControlImpl的OfferKeyEventL,主要核心处理代码又放在了OfferKeyEventL2中,
TKeyResponse CHtmlControlImpl::OfferKeyEventL2 (CHtmlElementDiv* aContainer, const TKeyEvent &aKeyEvent, TEventCode aType) 
{        
        if(aContainer->iFocusedElement
                && aContainer->iFocusedElement->CanFocus() 
                && !aContainer->iFocusedElement->iState.IsSet(EElementStateHidden))
        {
                if(aContainer->iFocusedElement->TypeId()==EElementTypeDiv
                                && ((CHtmlElementDiv*)aContainer->iFocusedElement)->IsContainer())
                {//如果是容器,将此消息再次发给容器处理
                        if(OfferKeyEventL2((CHtmlElementDiv*)aContainer->iFocusedElement, aKeyEvent, aType)==EKeyWasConsumed)
                                return EKeyWasConsumed;
                }
                 
                if((VisibilityTest(aContainer->iFocusedElement, aContainer->iDisplayRect)
                        )如果控件可见,将按键消息发给控件处理
                        && aContainer->iFocusedElement->OfferKeyEventL(aKeyEvent, aType)==EKeyWasConsumed)
                        return EKeyWasConsumed;
        }
        if(aContainer->iList && !aContainer->iList->IsEmpty())
                return aContainer->iList->OfferKeyEventL(aKeyEvent, aType);

        if(aType!=EEventKey || aContainer->iNext==aContainer->iEnd)
                return EKeyWasNotConsumed;
//如果控件未处理该消息
        aContainer->iState.Clear(EElementStateFocusChanged);
        TInt keyCode = HcUtils::TranslateKey(aKeyEvent.iCode);
        switch(keyCode)
        {//如果是上下左右方向键,则进行元素焦点的转移和显示内容往上或者往下移动
                case EKeyLeftArrow:
                        iState.Set(EHCSNavKeyPrev);
                        HandleKeyLeft(aContainer);
                        break;
                        
                case EKeyRightArrow:
                        iState.Set(EHCSNavKeyNext);
                        HandleKeyRight(aContainer);
                        break;
                        
                case EKeyUpArrow:
                        iState.Set(EHCSNavKeyPrev);
                        HandleKeyUp(aContainer);
                        break;
                        
                case EKeyDownArrow:
                        iState.Set(EHCSNavKeyNext);
                        HandleKeyDown(aContainer); 
                        break;
        }
        ………………………………
}
列举个按了右方向键的处理:
void CHtmlControlImpl::HandleKeyRight(CHtmlElementDiv* aContainer, TBool aScrolled)
{
        CHtmlElementImpl* found = NULL;//找到右边或者下一行第一个可视又可获得焦点的元素
        UpdateVFEs(aContainer);//首先获得可视可获得焦点的元素集合
        if(iVFEs.Count()==0) //如果没有符合要求的元素
        {
                if(!aScrolled && aContainer->iScrollbar->AddStepPos()) //向下滚动视图
                {
                        UpdateVFEs(aContainer);//重新检查是否有可视可获得焦点的元素
                        if(iVFEs.Count()>0) //如果有那么第一个元素就是目标元素,否则不作处理
                                found = iVFEs[0];
                }
        }
        else
        {//如果有符合要求的元素
                TInt index = iVFEs.Find(aContainer->iFocusedElement);
                if(index==KErrNotFound) //如果当前没有焦点元素,那么设置第一个就是焦点元素
                        found = iVFEs[0];
                else if(index<iVFEs.Count()-1) //如果现有的焦点元素不是最后一个,那么它的下一个元素就成为即将设置焦点的元素
                        found = iVFEs[index+1];
                else if(!aScrolled && aContainer->iScrollbar->AddStepPos()) //如果现有元素是可视元素集合中的最后一个,那么下面滚动视图重新处理该消息
                        HandleKeyRight(aContainer, ETrue); 
        }
        if(found)
        {//如果找到了符合要求的元素
                TInt adjustY = aContainer->iScrollbar->RealPos() - aContainer->iScrollbar->Pos();
                TInt offset = found->iPosition.iY + adjustY + found->iSize.iHeight  - aContainer->iDisplayRect.iBr.iY + 2;
                if(offset>0 && offset<aContainer->iDisplayRect.Height())  //可能需要滚动视图
                        aContainer->iScrollbar->AddPos(offset);
                aContainer->FocusChangingTo(found);//设置新焦点元素
        }
}

至此,本文简单介绍了HtmlControl的大体流程,只需要知道怎么用的朋友只要看第一点就够了,如果现有HtmlControl有不符合使用要求或者需要支持的更多的html标签的情况,则大家可以全部看完,在了解了整个流程后,对自己需要修改的方面再详细阅读源代码,多了解细节方面则可以自己修改HtmlControl以适应自己的需求。

HtmlControl值的我们学习和借鉴的是:
        1)将WSD(可写静态数据)放在控件环境CHtmlCtlEnv中,这种方法很巧妙
        2)作者创建了图片池,利用CImageDecoder对各种类型的图片进行解析。
        3)经典的观察者模式的使用
        4)富有层次结构的类设计,如CHtmlControl、CHtmlControlImpl,CHtmlElement <= CHtmlElementImpl <= CHtmlElementA等等
        5)公用属性、代码与各自独有属性、代码的分层设计,比如SetProperty、GetProperty
        6)对不同版本SDK宏的大量运用,一套代码适应不同的SDK

最后再次感谢作者ytom哥给大家带来如此好的开源作品。