Internet Explorer 完全解析 [2]
第二章 元素之源
2.1 CElement
介绍了mshtml的一个重量级明星CBase之后,这一步,我们需要看看一看CElement,同样,可以猜测它是很多元素的基类。事实如何?我们深入看一下这个类。
本节中,由于CElement会引入许多之前未介绍的类,因此,在这节中,当引用到一个新类的时候,我都会在之后另开一章介绍,而且会修改本文指向那一个类。
2.2 巨人CElement的结构
CElement是CBase的派生类。不同的元素通过派生和重载此类来区别。CElement作为元素的基类,结构十分庞大,从自身来说,它实现了IDispatchEx、IProvideMultipleClassInfo、IServiceProvider、IRecalcProperty、IMsoCommandTarget等接口。这些我们稍后都会看到。
CElement的成员变量中,__pvChain是其最重要的关联成员,这是一个void*格式的变量,在CElement构造时,这个变量将会被赋值。如果一个element有layout
(见参考,简单的说,就是有layout的元素自己会处理自己的大小之类的东西,参见layout _2)的话,__pvChain会指向这个layout,如果它没有layout,而它在一个树中,那么它会指向树中下一个ped
,其他情况下,它指向当前document。
__pNodeFirstBranch指向第一个分支的node
。还有两个成员变量DWORD _fMark1、DWORD _fMark2,它们用位来标记作用,分别如下:
_fMark1:
0~7 element tag; 8~14 如果旁视列表有指针的话为TRUE; 15 元素有name或者id属性; 16~31 元素锁定标记
_fMark2:
0 元素有一个Markup指针时为TRUE
1 元素有一个layout指针时为TRUE
2 有挂起状态的filter任务时为TRUE
3 有挂起状态的recalc任务时为TRUE
4 元素是一个site或者从未有过layout时为TRUE
5 元素控制着所有其下的函数运转时为TRUE
6 元素用来继承site和一些特殊格式时为TRUE
7 元素占1行,即使没有任何文字
8-11 未使用
12 元素没有边框
13 元素与tabindex关联(_aryTabIndexInfo)
14 元素有至少一个图像上下文
15 图片变化时需要强制重置大小
16 元素有关闭tag (例如<p></p>
)
17 用户创建时为FALSE,代码生成时TRUE
18 未使用
19 元素的行为像按钮
20 为init中的TestClassFlag而设置
21 元素默认就有layout
22 默认的确认按钮
23 默认的取消按钮
24 元素提交的事件被挂起
25 其他元素或者对象需要一个连接点或者设置一个事件
26 _pAA中加入了FilterCollectionPtr
27 元素有一个挂起的Exittree消息
28 GetFirstCommonAncestor会用到这个位,标记是否为第一个祖先
29-30 随机位
31 元素有style expression
为了做到普适性,这个CElement不得不称作一个巨人,下面我们介绍它的成员函数。
2.3 CElement的成员函数
- CElement::CElement。构造函数,将当前CElement所属pDoc赋给__pvChain,调用pDoc->SubAddRef(),增加一个Reference。并保存传入的ELEMENT_TAG类型数据到自身的_etag中,标记当前CElement的真正Tag。
- CElement::~CElement。 析构函数,Passivate()中做了清理,这儿几乎没动作。
- CElement::CLock::CLock。 设置某个CElement元素的ELEMENTLOCK_FLAG。
- CElement::CLock::~CLock。 将设置的Lock取消。 CElement::Passivate。
清理函数,当调用这个函数时,CElement
(1)不会在一棵树中(如果在树中就不应该被清理);
(2)延迟队列中不能有当前CElement,否则会重复释放。
当满足上述2个要求时,程序检查是否有pending task,如果有的话,则从pDoc中获取View,然后调用删除任务的函数,将pending task清除。
如果当前CElement有任何sub Markup,那么将它们删除,然后清除master Makrup,再Release释放计数。如果当前CElement是一个peer holder(节点持有者),那么在当前CElement上也只应该有1个peer,否则不应该调用这个清理函数,有的话,随后将这1个peer的Reference清除。
接着,清除peer Manager(如果有),data bindings(如果有),element capture,methods,images,layouts(如果有),accessible objs(如果有),pending filter tasks(如果有),recalculate task(如果有),filter collections pointer(如果有,AA的),cached style pointer(如果有,调用super::Passivate来清除关联的属性数组),all lookasides(也即上述这些)。
最终,减少CElement的计数和obj count。 - CElement::PrivateQueryInterface。 IUnknown的QueryInterface实现。
- CElement::PrivateAddRef。 IUnknown的AddRef实现。
- CElement::PrivateRelease。IUnknown的Release实现。
- CElement::PrivateEnterTree。 进入树处理时调整引用计数。会把在Markup中的当前CElement父类引用加一,MarkupPtr的引用也随之加一。
- CElement::PrivateExitTree。 退出树处理时调整引用计数。与上述操作相反,引用数大于1时,调用父类的Relase将引用计数减一,同时将传入的Markup引用减一(如果引用大于1时)。
- CElement::contains。 IHTMLElement的函数。传入一个IHTMLDocument*类型的参数,如果CElement里包含这个IHTMLDocument*类型的对象,将返回一个VARIANT_BOOL。
判断方式:
(1)获取一个CTreeNode*(从传入的PIHTMLDocument中QueryInterface:CLSID_CTreeNode),如果失败,则获取第一个分支CElement * (QueryInterface:CLSID_CElement),调用获取到的CElement*的GetFirstBranch()函数。
(2)从(1)步获得的这个Node开始向上遍历,直至遍历到根Node,或者当前Node为止;
(3)如果(2)步的结果是遍历到了根Node,那么返回VB_FALSE,如果是遍历到了当前Node,返回VB_TRUE,这个逻辑很好理解:
- CElement::ClearRunCaches。获得当前CElement所在的Markup,调用Markup的ClearRunCaches函数。
- CElement::IsFormatCacheValid。遍历当前所在树中第一个branch开始的所有branch(First->>Next->>Next...),进行有效性检查。
- CElement::EnsureFormatCacheChange。当当前函数不在一个树中或者没有设置需要清理cache时,返回S_OK;如果设置了标记位,那么调用ClearRunCaches。
- CElement::OnPropertyChange。 onpropertychange事件。
notenote: onproprty事件是IE专属
(1)当前CElement的onpropertychange事件刚刚被设置时应该执行事件;
例如:
<div id="txt"> www.nul.pw</div> <script> function d(){alert(5);} document.getElementById('txt').onpropertychange=d(); </script>
将会触发alert(5)。 (IE11可能不一定会正确触发,请在IE10-,或者兼容性视图中测试)
(2)当前CElement不在任何tree中是不可以触发的,只有当前CElement在放置到tree中之后才能触发事件;
<div id="txt"> www.nul.pw</div> <script> function d(){alert(5);} p = document.createElement("div"); p.onpropertychange=d(); p.innerText="123"; document.getElementById('txt').appendChild(p); </script>
例如上述代码不能触发onpropertychange事件。
<div id="txt"> www.nul.pw</div> <script> function d(){alert(5);} p = document.createElement("div"); p.onpropertychange=d(); document.getElementById('txt').appendChild(p); p.innerText="123"; </script>
则可以触发事件。
以及一些额外的操作,例如cache调整,根据传入的dispid来设置对应应该触发事件的对象,或者调整branch。因为onpropertychange是在一个元素的任意
属性改变时触发的,所以这儿的判断流程十分长,例如background-image(背景图片)、list-style-image(列表图片)、z-index(顺序)、left,right,bottom,top(位置)、disabled(有效性)、visiablilty(可见性)、marginleft,right,bottom,top(边缘对齐)、clipleft,right,bottom,top(剪裁绝对元素)等等等等,几乎是所有样式操作导致属性变化都会有对应的方式来处理,这一部分可以参考w3school的对应部分。例如;clip:rect
、list-style-image
等。
当propertychange发生时,如果当前CElement有layout的话,将获取离得最近的layout的视图,然后向它发送一个property changed 的notify。
如果pDoc中的current元素指向自身,但是获取不到layout,可标记里还是提示有layout;或者获取到了layout,但是标记里面提示没有layout,那么将最近的一个layout的视图的owner变成当前元素的子元素。这个逻辑是:标记有layout但是获取不到layout的时候,表明当前元素有子元素,但是获取不到layout,证明子元素没有设置owner标记; 获取到了layout,证明子元素可达,而且有layout标记,但是标记里没有layout,这时应当也设置一次owner标记,让其也标记上layout。
这步操作之后,会设置上RemeasureInParent标记,告知Parent应该重新测量自己的子元素属性。同时去除SIZECHANGED属性。
RemeasureInParent之后应该设置DisplayChange通知,将标记元素祖先为脏的数据,因此只有ZParent
全部计算完成之后,ZParentChange才会被调用,之前都一直会在队列中排着。
(注:DisplayChange
请参见CNotification
节中的内容,将在后续介绍。 )
除非是在一个non-site
的元素上进行的z-index改变,否则z-index的改变将CSite的z-order处理函数FixZOrder来处理。
当一个property改变而且会引起继承的值都会出现改变时(通过dwFlags确认),会调用Invalidate()通知parent,然后发送一个notification以通知后代。
当触发onpropertychange之后,之前获取到的layout就不再可靠了,因为脚本可能会改变页面内容,所以此时,还应当测试性的调用一次onchage事件(当然,不一定触发,这里面还是会检查是否应当触发事件的),以及通知当前document数据变化了,最后会做一些其他小的触发行为。根据ie版本不同而不同,ie11以及兼容性视图行为差异很大。
- CElement::OpenView。打开与document关联的view(这个view是与最近的一个layout相关联的)。
- CElement::*Element。 发送消息用的辅助函数。InvalidateElement、MinMaxElement、ResizeElement(没有layout、当前标记为clean、它的父容器没有设置过它的大小时,这个消息才会送达)、RemeasureElement、RemeasureInParentContext、RepositionElement、ZChangeElement。
- CElement::SendNotification。重载函数,传入一个CNotification*,向关联的element发送一个notification,做法是:先获取Markup,然后调用Markup的Notification函数发送。
- CElement::SendNotification。重载函数,带NTYPE和NFLAGS的,当场组合一个CNotification,然后发送。
- CElement::DirtyLayout。 标记当前元素关联的layout为脏。
- CElement::HitTestPoint。 和常见的WM_NCHITTEST的目的差不多,它的作用是确定某个点是否在某个CElement上。判断方式:获取最近的layout,获取其View(
CView
),然后转而调用View的HitTest函数。 - CElement::GetRange。 返回这个CElement的文字范围,包括结束节点。计算方式:获取起始节点和最终节点的范围,相减后加一。
- CElement::GetFocusShape。返回当一个控件被选中时应该被重绘的区域范围,区域是一个从CShape派生来的对象。默认的TextRange是用这个的(GetClientRect),其他的例如多选框之类的,都是从这儿重载的,否则无法提供正确的区域范围。
- CElement::GetImageUrlCookie。返回向指定Doc的图像url缓存里面添加一个url的结果,称为新cookie。
- CElement::AddImgCtx。向AA中添加在ImgCtxInfo中一个指定的信息。如果AA中现在已经有值了,先会把它释放掉。具体步骤:先找到AAIndex,这时候会得到一个cookie,根据它来检索,如果有ImgCtx(image context),释放旧的,然后添加新的cookie。
- CElement::ReleaseImageCtxts。 释放所有与此CElement关联的图像上下文,例如背景图像。
- CElement::DeleteImageCtx。同上,只不过删掉的是所有dispid指定的与CElement关联的图像上下文。
- CElement::GetSourceIndex。 获得当前元素的SourceIndex。由于ROOT node的存在,获取的SourceIndex会减一修正。
- CElement::CompareZOrder。 与另一个CElement比较ZOrder。返回值:>0,这个元素z-index比较大;<0,这个元素的z-index比较小;0,元素的z-index相等。
- CElement::GetTreeExtent。遍历上下文chain,然后获取第一个和最后一个node的位置。
- CElement::GetLastBranch。 获得最后一个branch。从first branch一直获取nextbranch即可。
- CElement::ReplacePtr。 替换两个Ptr,同时修正引用。
- CElement::ReplacePtrSub。 类似,只不过修正的是subref。
- CElement::SetPtr。 类似,只不过修正一个元素的ref。
- CElement::StealPtrSet。 SetPtr的封装,修正引用。
- CElement::StealPtrReplace。 ReplacePtr的封装,修正引用。
- CElement::ClearPtr。 置NULL,Release ref。
- CElement::ReleasePtr。 Relase ref。
- CElement::IsBlockElement。 返回这个CElement是否是
Block Element
。 - CElement::IsOwnLineElement。 返回这个CElement是否是内联元素。
- CElement::IsBlockTag。 返回是否是Block Tag,例如css里面设置的display:block的元素。
- CElement::BreaksLine。 返回这个节点是否起新行,例如hr,div。
- CElement::HasFlag。 返回这个节点是否有指定的flag。
- CElement::SetTagNameAndScope。 设置tag,然后scope一个元素的属性。
- CElement::TagName。 返回tag名称。
- CElement::Namespace。 返回GetAAscopeName。
- CElement::NamespaceHtml。 返回namespace,如果没有namespace,返回“HTML”。
- SameScope。 重载函数,参数为CTreeNode,CElement 或者 CElement, CTreeNode 或者两个CTreeNode ; 检查CTreeNode所包含的元素是否就是后面的CElement。
- CElement::NameOrIDOfParentFor。如果有的话,返回一个对象的父form的id或者name,具体方式是获取第一个branch,然后从branch到root搜索任意一个tag为ETAG_FORM的对象,这个即可算作父form。
- CElement::SaveAttribute。将一个属性存入流(
CStreamWriteBuff
类)中。也就是加入一个属性="值"
的对,同时请注意到IE中的一个特性是属性中的
值
部分中的回车都不会忽略
。 - CElement::SaveUnknown。把所有的expando(AAType为CAttrValue::AA_Expando)的值输出到流中。
- CElement::SaveUnknown。重载函数,输出到IPropertyBag中。没有值的属性不会被保存。例如
<script defer>
的defer,但是建议写成defer="defer"
。 - StoreLineAndOffsetInfo。将对象的AA属性的行号等存储起来。
- CElement::GetLineAndOffsetInfo。 从某个AA中返回某个属性的行号和偏移值。
- CElement::ConnectEventHandler。从AA中获取指定的dispid对应的字符串,然后从这个字符串里构建一段代码,然后把对应的dispid和这段代码放置到AA中。当倒数第二个参数为TRUE时,表明这是普通的事件,例如onclick,AA中的AA_Attribute以dispid来区分;为FALSE时,代表dispid指代的是AA的AA_Expando节。
- CElement::InitAttrBag。从
CHtmTag
里面取值放入bag中。bag就是AttrBag,存储属性-值对的一个“包”。 - CElement::MergeAttrBag。将CHtmTag中任何不在attrbag的属性放到bag中。
- CElement::Init2。 元素级别的初始化。
- CElement::Save。将元素保存到流中。
- CElement::SaveAttributes。 将属性保存到流/IPropertyBag中。
- CElement::HandleMessage。 处理特定元素的消息。将对应消息转交给CLayOut去处理。
- CElement::DisallowSelection。 当选区必须不能被选中时返回TRUE。对话框中,只有能编辑的部分可以被选中。
- CElement::CloseErrorInfo。 调用Doc的CloseErrorInfo,以便返回它的clsid而不是控件的clsid。
- CElement::SetCursorStyle。响应类似这样的
onmouseover="this.style.cursor='pointer';this.style.cursor='hand'"
的"element.style.cursor"属性,在发生WM_CURSOR消息时处理消息。 - IsIDMSuperscript/IsIDMSubscript/IsIDMBold/IsIDMCharAttr。 判断tag是否是上标(sup)/下标(sub)/加粗(b/strong)/文字(这样的话当用户选择移除文字格式时,可以判断这个元素是否可以删除)
- IsIDMItalic/IsIDMUnderlined。判断tag是否为斜体(i)/下划线(u)
- CElement::WriteTag。向流中写入一个完整的tag,也就是包括
<
xxx>
www.nul.pw</
xxx>
的tag对。 - CElement::scrollIntoView。 对ScrollIntoView的封装。
- CElement::DeferScrollIntoView。 两个重载,延迟ScrollIntoView,其实是把消息post进队列了,也是ScrollIntoView的封装。
- CElement::GetAtomTable。获取AtomTable,如果一个CElement没有atom table,那么可以证明它没有和CDoc关联。
- CElement::GetPlainTextInScope。 返回这个CElement的所有文字部分,像是innerText的东西,这个函数的参数传入NULL的话返回的是文字长度。
- CElement::MatchAccessKey。 接受键盘消息并判断是否为快捷键。
- CElement::ShowMessage。 CElement::ShowMessageV的封装。
- CElement::ShowMessageV。 CDoc的ShowMessageV的封装。
- CElement::ShowLastErrorInfo。 CDoc的ShowLastErrorInfo的封装。
- CElement::ShowHelp。 CDoc的ShowHelp的封装。
- CElement::GetOmWindow。 返回CDoc的OmWindow成员。
- CElement::UndoManager。CDoc的UndoManager的封装,获取撤销管理器的接口(IOleUndoManager)。
- CElement::QueryCreateUndo。 CDoc的QueryCreateUndo的封装。判断是否应该建立一个撤销对象,会影响到CDoc的内容。
- CElement::ShowTooltip。 显示站点的tooltip提示,如果有标题的话,那标题就是tooltip,可以把鼠标移动到tab上面看一看,一会儿出来的黄色方框就是tooltip。
- CElementFactory::PrivateQueryInterface。 QueryInterface的实现。
- CElementFactory::InvokeEx。 InvokeEx的实现。
- CElement::MergeAttributes。 将指定元素的属性复制到另一个元素里面,只有相同元素名的元素才可以做此操作,这个操作发生在编辑元素时将一个元素以另一个替代时。merge的方式是将一个inline元素的aa给merge到另一个里面,对CAttrArray的Merge的封装。
- CElement::GetNameSpaceParent。获取命名空间层次上的parent。通常发生在FORM里面,具体做法:从node的parent向上遍历所有branch到第一个form。当当前元素不在任何markup(即标识)中或者在主markup中,将返回omdoc作为命名空间,即返回Markup的IDispatchEx的query结果。非主markup时,返回Markup的DefaultDispatch的IDispatchEx。
- CElement::CanShow。元素此时是否可以展示。加载完成时可以展示。
- CElement::OnCssChangeStable。 当前元素在markup中时,向markup发送OnCssChange事件。
- CElement::Invalidate。 Invalidate当前元素的区域。
- CElement::GetElementRc。 返回元素位置。
- CElement::ComputeHorzBorderAndPadding。 计算水平边框宽度和padding,位置计算辅助函数。
- CElement::Clone。 字面意思,复制一个一模一样的元素。
- CElement::StealAttributes。 从另一个元素里面“偷”来属性。计算完所有属性,创建完属性包之后如果需要替换元素的话,就会调用这个函数。
- CElement::AddExtension。创建一个css扩展对象(css filter),
alpha/blur/dropshadow这些滤镜,ie中它们是和一个clsid对应的,使用时都要CoCreateInstance创建对应实例才可以
,其中,被实例化的对象必须标记为safe for init
、safe for script
,IPersistPropertyBag2或者IPersistPropertyBag中可以查询到SAFETY_INIT、SAFETY_SCRIPT的结果。 - CElement::get_filters。返回filter collection。
- CElement::toString。 BSTR转为CString。
- CElement::setCapture。 设置鼠标捕获。
- CElement::releaseCapture。 释放鼠标捕获。
- CElement::WantTextChangeNotifications。返回该element是否应该有文本变换时的通知。
- CElement::Notify。 通知处理函数。
- CElement::EnterTree。 进入树,连接上事件处理器并保存。
- CElement::ExitTree。 退出树,并发送notify,以及做维护工作。
- CElement::IsEqualObject。 IObjectIdentity的实现,判断对象是否相等。
- CElement::QueryService。 IServiceProvider的实现。
- CElement::ContextThunk_Invoke。 IDispatch对应的Invoke。
- CElement::ContextThunk_InvokeEx。 读取context(放到eax),然后传给ContextInvokeEx。
- CElement::GetDispID。 获得DispID。
- CElement::GetExpandoDispID。 获取Expando的Dispid。
- CElement::GetNextDispID。 获取下一个DispID。
- CElement::GetMemberName。 从dispid获取成员名。
- CElement::GetMultiTypeInfoCount。 IProvideMultipleClassInfo的实现。
- CElement::CreateTearOffThunk。 对::CreateTearOffThunk的封装。
- CElement::get_dir/CElement::put_dir。 获取/设置dir属性。
- CElement::GetLookasidePtr/CElement::SetLookasidePtr/CElement::DelLookasidePtr。当页面有LookasidePtr时,获取/设置/删除Lookasideptr。
- CElement::put_onresize/CElement::set_onresize。 设置/获取onresize事件属性。
- CElement::Fire_onfocus。 触发onfocus事件,FireEvent的封装,事件名是“focus”。
- CElement::Fire_onblur。 触发onblur事件。
- CElement::CreateAccObj。 根据不同的CElement tag(例如body、frameset、a,etc)创建CAccElement可访问对象。
- CElement::GetParentAncestorSafe。 获取某个etag的祖先,有有效性判断。
- CElement::GetIDHelper。 CElement的collection辅助函数。获取dispid。
- CElement::SetIdentifierHelper。 辅助函数。 非script的标签,将所有关联的脚本全部unhook,设置name或者id,如果没有的话,从aa中获取指定的字符,命名设置标识符,然后hook上脚本。
其中,第二个参数为DISPID_CElement_id、第三个参数为STDPROPID_XOBJ_NAME时设置ID,第2个参数为STDPROPID_XOBJ_NAME时设置NAME。第四个参数为STDPROPID_XOBJ_NAME时设置unique name(一个name只允许出现1次)。 - CElement::SetIDHelper。 对SetIdentifierHelper的封装。设置ID。
- CElement::GetnameHelper。 获取name。
- CElement::SetnameHelper。 对SetIdentifierHelper的封装。设置name。
参考
layout
http://msdn.microsoft.com/en-us/library/ie/ms530764(v=vs.85).aspx
layout #2
http://www.sitepoint.com/web-foundations/internet-explorer-haslayout-property/
ped
结构中的一个个体
node
节点
site
显示出来的一个实节点
style expression
style中包含有expression表达式
clip:rect
http://www.w3school.com.cn/cssref/pr_pos_clip.asp
list-style-image
http://www.w3school.com.cn/cssref/pr_list-style-image.asp
ZParent
, 按z-index排列的parent
non-site
是个元素,但是并没有展现出来(根本没有加入到文档里,而不是不可见之类的操作)
Block Element
块元素,块级元素生成一个元素框,(默认地)它会填充其父级元素的内容,旁边不能有其他元素。换句话说,他在元素框之前和之后生成了“分隔”符。我们最熟悉的HTML元素是p和div。
safe for init
、safe for script
http://msdn.microsoft.com/en-us/library/aa751977%28VS.85%29.aspx