到目前为止,布局是一个线性过程,可以独立处理打开标签和关闭标签。但网页是树木,看起来像它们:边界和背景在视觉上彼此嵌套。为了支持,这一章将切换到基于树的布局,其中元素树被转换为页面的可视元素的布局对象树。在此过程中,我们将使我们的网页更加丰富多彩。
现在,我们的浏览器分别铺设了元素的打开和关闭标记。两个标签都修改了全局状态,如Cursor_x和cursor_y变量,但它们没有其他连接,并且与整个元素的信息相似,与其宽度和高度一样,从未计算过。这使得在文本后面绘制背景颜色很难。所以Web浏览器结构不同的布局。
在浏览器中,布局是关于生成布局树,其节点是布局对象,每个都与HTML元素相关联,例如< script>不要生成布局对象,并且某些元素生成多个(< li>元素为子弹点有一个布局对象!),但主要是每个布局对象。每个都有一个尺寸和位置。浏览器遍历HTML树生成布局树,然后计算每个布局对象的大小和位置,最后将每个布局对象绘制到屏幕。
让我们开始一个名为blocklayout的新类,这将表示布局树中的节点。与我们的元素类一样,布局对象形成树,因此他们有一个孩子和父母。我们还将为HTML元素提供一个节点字段,Layout对象对应于。
class boltllayout:def __init __(self,node,父,上一个):self.node = node self.parent = parent self.previous =上一个self.children = []
我还为布局对象的上一个兄弟添加了一个字段。我们需要它来计算大小和位置。
每个布局对象还需要大小和位置,我们将存储在宽度,高度,x和y字段中。但让我们留下来。 BlockLayout的第一个作业正在创建布局树本身。
我们将以新的布局方法执行此操作,循环在每个子节点上并为其创建新的子布局对象。
class blocklayout:def布局(self):上一页= none for self.node.children:next = blocklayout(child,self,preak)self.children.append(下一个)上一页=下一个
这个代码很棘手,因为它涉及两棵树。节点和子是HTML树的一部分;但是,自我,以前和接下来是布局树的一部分。两棵树有类似的结构,所以很容易被混淆。但请记住,此代码从HTML树构造布局树。所以它从node.children(在HTML树中)读取并写入self.children(在布局树中)。
因此,这为有问题的节点的直接子项创建了布局对象。现在,这些儿童自己的布局方法可以调用递归地构建整个树:
我们将在一瞬间讨论递归的基本情况,但首先让我们询问它是如何开始的。不方便地,BlockLayout构造函数需要一个父节点,因此我们需要root另外一种布局对象。您无法为父权使用None,因为根布局对象还会以不同方式计算其大小和位置,因为我们将在稍后介绍本章。我认为那个根作为文档本身,所以让我们称之为DomectLayout:
class documentlayout:def __init __(self,node):self.node = node self.parent = none self.children = [] def布局(self):child = blocklayout(self.node,self,self,none)self.children.append (孩子)child.layout()
因此,我们通过HTML节点的一个布局对象构建了一个布局树,通过递归调用布局以及root处的额外布局对象。它看起来像这样:
在此示例中,有四个BlockLayout对象,绿色,每个元素一个。 root还有一个文档列表。
浏览器现在必须继续到每个布局对象的计算大小和位置。但在我们编写该代码之前,我们必须面对一个重要的事实:不同的HTML元素被不同地布置。他们需要不同种类的布局物体!
Web开发人员无法访问布局树,因此它尚未标准化,其结构在浏览器之间不同。即使是名字也不匹配!镀铬物叫它布局树,safari渲染树,和firefox框架树。
元素如<身体>和<标题>包含垂直堆叠的块。但是< p>和< h1>包含文本并将其水平的文本放在行中。在欧洲语言,至少!摘要有点,有两个布局模式,两种方式可以相对于其孩子奠定了一个元素:块布局和内联布局。
我们已经有BlockLayout块布局。实际上,我们也有直行布局:它只是我们已经实现的文本布局了,因为它自第2章。所以让我们将现有的布局类重命名为inlinelayout和Refactor以匹配BlockLayout的方法。
将布局重命名为inlinelayout并将其构造函数重命名为布局。添加类似于BlockLayout的新构造函数:
class inlinelayout:def __init __(self,node,父,上一个):self.node = node self.parent = parent self.previous =上一个self.children = []
让我们还初始化来自x和y的Cursor_x和Cursor_y,而不是HSTEP和VSTEP,都在布局和刷新中:
类inlinelayout:def布局(self):#... self.cursor_x = self.x self.cursor_y = self.y#... def flush(self):#... self.cursor_x = self.x#。 ..
内联布局对象现在不会有任何孩子,因此我们不需要任何代码在布局中。所以新的Inlinelayout现在匹配BlockLayout的方法。就像块布局一样,让我们将实际计算x和y和宽度和高度留在稍后。
我们的树创建代码现在需要为每个元素使用正确的布局对象。通常情况下,这很简单:有文本中的东西得到了inlinelayout,与块元素的东西如< div>里面得到blocklayout。但是如果元素包含两者,会发生什么?在某种意义上,这是Web开发人员的一部分错误。就像在第4章中的隐式标签一样,浏览器使用维修机制来理解情况。在真实的浏览器中,使用“匿名块框”,但在我们的玩具浏览器中,我们将实现一些更简单的东西。
block_elements = [" html&#34 ;,"身体&#34 ;,"文章&#34 ;,#34;部分"" nav&#34 ;," 34;旁边&#34 ;,#34; H1&#34 ;," h2&#34 ;," h3&#34 ;," h4&#34 ;," h5" h5" ;," H6&#34 ;," hgroup&#34 ;,"标题&#34 ;,"页脚&#34 ;,"地址&#34 ;,&#34 ; P&#34 ;," HR&#34 ;," ol&#34 ;," ul&#34 ;,"菜单&#34 ;," li" ," dl&#34 ;," dt&#34 ;," dd&#34 ;,"图&#34 ;,"图34; figeqution&#34 ;,#34;主要"" div""表&#34 ;,#34;形式&#34 ;," fieldset&#34 ;,"传奇", "详情""摘要"]
我们将使用该列表中的子节点的BlockLayout for Elements,否则inlinelayout。将该逻辑放在新的layout_mode函数中:
def layout_mode(节点):如果是isinstance(节点,文本):返回"内联" elif node.children:对于node.children的孩子:如果isinstance(子,文本):如果child.tag in block_elements继续:返回"块"返回"内联"否则:返回"块"
此功能另外,确保文本节点获取内联布局,而空元素获取块布局。现在我们可以调用layout_mode来确定为每个元素使用哪个布局模式:
class blocklayout:def布局(self):上一页= for self.node.children:如果layout_mode(child)=="内联&#34 ;: next = inlinelayout(儿童,自我,以前)别的= blocklayout(child,self,preak)self.children.append(next)上一页=下一个#...
我们的布局树现在有一个OffortLayout在root,内部节点处的blocklayouts,叶子中的inlinelayouts:或者,叶节点可能是空元素的blocklayouts。
使用布局树构建,我们最终可以继续执行树中布局对象的大小和位置。
在CSS中,布局模式由Display属性设置。最旧的CSS布局模式,如内联和块,都设置在子项上而不是父级,这导致匿名块框等打嗝。较新的属性,如内联块,Flex和网格上都设置在父级上。本章使用较新的,更令人困惑的约定,即使它实际上正在实施内联和块布局。
默认情况下,布局对象是贪婪的,并占用他们可以的所有水平空间。在下一章中,我们将为用户样式添加支持,这些支持这些规则并允许设置自定义宽度,边框或填充。所以他们的宽度是他们的父母的宽度:
布局对象的垂直位置取决于其先前兄弟姐妹的位置和高度。如果没有以前的兄弟姐妹,他们从父母的顶部边缘开始:
这三个计算必须在对每个孩子的布局方法递归调用之前进行。毕竟,布局对象的宽度取决于父宽度;因此,必须在铺设子项之前计算宽度。该位置是相同的:它取决于父级和先前的兄弟姐妹,因此父母必须在接收前进行计算,并且当传递时,它必须按顺序铺设孩子。
高度是相反的。 BlockLayout应该足够高,以包含所有的孩子,因此其高度应该是其儿童高度的总和:
由于Blocklayout的高度取决于其子女的高度,因此必须在抬起后计算其高度以计算其子项的高度。获取此依赖性秩序右键是至关重要的:弄错,有些布局对象将尝试读取尚未计算的值,浏览器将有一个错误。
inlinelayout计算宽度,x和y相同的方式,但高度有点不同:inlinelayout必须包含它内部的所有文本,这意味着它的高度必须从其y-cursor计算它。
同样,在布置文本之前必须计算宽度,x和y,但必须在后面计算高度。这完全是关于该依赖令。
最后,甚至DomecodLayout也需要一些布局代码,但由于文档始终从同一位置开始,它非常简单:
Class DocumentLayout:Def布局(self):#... self.width = width - 2 * hstep self.x = hstep self.y = vstep child.layout()self.height = child.height + 2 * vstep
请注意,左侧和右侧的内容周围有一些填充,而vstep上方和下面。这就是这样,文本不会进入窗口的非常边缘并被切断。
对于所有三种类型的布局对象,布局方法中的步骤的顺序应该是相同的:
调用布局时,它首先为每个子元素创建一个子布局对象。
然后,布局计算宽度,x和y字段,从父读数和先前的布局对象读取。
坚持下列顺序是为了满足尺寸和位置字段之间的依赖性;第10章将更详细地探讨此主题。
正式地,可以通过属性语法来描述这样的树上的计算。属性语法引擎分析不同属性之间的依赖关系以确定遍历树的正确顺序并计算每个属性。
现在我们的布局对象具有大小和位置信息,我们的浏览器应使用该信息来呈现页面本身。首先,我们需要在浏览器的加载方法中运行布局:
类浏览器:def load(self,URL):标题,body =请求(URL)节点= htmlParser(body).parse()self.document = documentlayout(节点)self.document.layout()
回想一下,我们的浏览器通过首先收集显示列表,然后调用渲染来绘制列表中的内容。具有基于树的布局,我们通过在布局树下进行显示列表。
我认为通过向每个布局对象添加绘制函数来做到这一点是最方便的。这里的一个整洁的技巧是将列表本身作为参数传递,并将递归函数附加到该列表。对于OfficeLayout,只有一个孩子,递归看起来像这样:
最后,Inlinelayout已经将事物存储在其display_list变量中,因此我们可以复制它们:
查看:您的浏览器现在正在使用基于花哨的树的布局!我建议暂停测试和调试。基于树的布局强大但很复杂,我们即将添加更多功能。稳定的基础适合舒适的房屋。
布局树在GUI框架中很常见,但还有其他方法可以构建布局,例如基于约束的布局。 Tex的盒子和胶水和iOS自动布局是此替代范式的两个例子。
浏览器使用布局树很多,例如,在第7章中,我们将使用每个链接的大小和位置来弄清楚用户点击了哪一个!并且一个简单且视觉上引人注目的用例是绘制背景。
Backgrounds是矩形,因此我们的第一个任务将矩形放在显示列表中。概念上,显示列表包含命令,我们希望两种类型的命令:
class drawtext:def __init __(self,x1,y1,text,字体):self.top = y1 self.left = x1 self.text = text self.font = font class drawrect:def __init __(self,x1,y1,x2 ,y2,颜色):self.top = y1 self.left = x1 self.bottom = y2 self.right = x2 self.color =颜色
现在Inlinelayout必须将DrawText对象添加到显示列表中:为什么不将Display_List更改为直接包含DrawText命令?你可以,但稍后重构会有点难得。
类inlinelayout:def draw(self,display_list):对于x,y,word,self.display_list:display_list.append(drawttext(x,y,word,字体))
与此同时,BlockLayout可以为背景添加绘制命令。让我们将灰色背景添加到Preg标签(用于代码示例):
class blocklayout:def draw(self,display_list):如果self.node.tag ==" pre&#34 ;: x2,y2 = self.x + self.width,self.y + self.height rect = drawrect (self.x,self.y,x2,y2,"灰色")display_list.append(Rect)#...
确保此代码在递归绘制呼叫上对儿童布局对象之前:必须在下面绘制背景,因此在源块内的文本之前。
使用填写的显示列表,我们需要渲染方法来运行每个图形命令。让我们为此添加一个执行方法。在drawtext上它调用create_text:
Class DrawText:def执行(self,scroll,canvas):canvas.create_text(self.lefte_text(self.left,self.top - 滚动,tement = self.text,font = self.font,anchor =' nw', )
请注意,执行将滚动量作为参数;这样,每个图形命令都是相关的坐标转换本身。绘制与create_rectangle相同:
Class TraxRect:def执行(self,scroll,canvas):canvas.create_rectangle(self.lefte_rectangle(self.leftee_rectangangle(self.leftee_rectangangle(self.telop - 滚动,self.rite,self.bottom - 滚动,宽度= 0,fill = self.color,)
默认情况下,create_rectangle绘制一个像素的黑色边框,对于我们不想要的背景,因此确保通过宽度= 0:
我们仍然想跳过屏幕上的图形命令,因此让我们将底部字段添加到drawText中,因此我们知道何时跳过那些:
浏览器的渲染方法现在只使用顶部和底部来确定要执行哪个命令:
def渲染(self):self.canvas.delete("所有")for self.display_list:如果cmd.top> self.scroll + height:cmd.bottom< self.scroll:继续cmd.execute(self.scroll,self.canvas)
尝试在页面上的浏览器 - 也许这是它的代码片段。您应该看到使用灰色背景设置的每个代码片段。
在某些系统上,测量和度量标准命令非常慢。添加另一个呼叫使事情变慢。
幸运的是,这个度量标准呼叫重复刷新呼叫。如果您要小心,可以将该调用的结果传递给DrawTtext作为参数。
这是基于树的布局的一个更可爱的好处。由于基于树的布局,我们现在记录整页的高度。浏览器可以使用它来避免滚动到页面的底部:
def scroldown(self,e):max_y = self.document.height - height self.scroll = min(self.scroll + scroll_step,max_y)self.render()
所以这是基于树的布局的基础!事实上,正如我们在接下来的两章中看到的那样,这只是布局树在浏览器中角色的一部分。但在我们到达之前,我们需要在甚至更加引人注目的方面制作网页。
CSS绘制API草案允许页面以JavaScript实现的新类型命令扩展显示列表。这使得可以使用CSS与库提供的视觉复杂样式使用CSS进行样式。
基于树的布局可以大大扩展我们的浏览器的样式功能。我们将在下一章中工作。
链接栏:本书各一章的顶部和底部是一个灰色的栏,命名章节并提供背部和转发链接。它被封装在一个< nav类="链接">标签。让你的浏览器给出这个链接吧浅灰色背景真正的浏览器。
隐藏的头:您的浏览器仍然有很好的机会仍然显示您访问的每个页面顶部的脚本,样式和页面标题。使它使其成为<头部>元素及其内容永远不会显示。这些元素仍应在HTML树中,但不在布局树中。
子弹:添加bullet键列出项目,其中在HTML中是< li>标签。您可以使它们位于列表项本身的左侧的小方块。也缩进< li>元素所以元素内的文本位于子弹点的右侧。
滚动条:在屏幕的右边缘,绘制一个蓝色的矩形滚动条。其高度与屏幕高度的比率应与屏幕高度与文档高度的比率相同,其位置应反映文档内屏幕的位置。如果整个文档适合屏幕,请隐藏滚动条。
目录:本书在每个章节的顶部有一个目录,括在< nav id =" toc">标记,包含链接列表。添加文本“目录”,其中包含灰色背景,上方列表。不要修改Lexer或Parser。
匿名块框:有时,元素有一个文本和容器状儿童的混合。 例如,在此HTML中, < div> 元素有三个儿童:<< b>,和< p> 元素。 前两个是文字; 最后一个是容器。 这应该看起来像两个段落,一个用于< i> 和< b> 并且第二是< p&gt ;. 让你的浏览器做到这一点。 具体地,修改inlinelayout,因此可以传递一系列兄弟节点,而不是单个节点。 然后,修改构造布局树的算法,以便任何文本状元素序列都变为单个inlinelayout。 run-ins:“run-in标题”是标题,作为下一段文本的一部分绘制。 本节中的锻炼名称可以被视为运行标题。 但由于浏览器支持显示:run-in属性很差,本书实际使用它; 标题AR. ......