正文:
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哥给大家带来如此好的开源作品。
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哥给大家带来如此好的开源作品。
大部分转载 小部分自写