~$ 存档

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

duilib概述

参照duilib类图
这篇主要记录框架思路

PlaceHolder

PlaceHolder字面上看是布局容器,单独将布局相关的操作分离出来。从抽象来看,可以理解为就是一个矩形块。最重要的属性为:

  • m_rcItem 矩形坐标范围
  • m_cxyFixed 固定宽高

m_cxyFixed

虽然字面上表示固定宽高,但实际有三种情形,固定宽高,auto,stretch

  • 固定宽高,就是显式的指定 比如<Control width=”50” height=”20”..>
  • auto 宏定义 DUI_LENGTH_AUTO -2 比如<Control width=”auto” height=”20” ../>
  • stretch 宏定义 DUI_LENGTH_STRETCH -1 比如中<Label width=”stretch” .../>

这说明m_cxyFixed的可能取值有下面几种情形:{50,20};{-1,-1},{-2,-2},或相互之间组合的情形

除此之外,在PlaceHolder的构造中,可以得到一个信息
控件默认是DUI_LENGTH_STRETCH,也就是默认拉伸,这一点比较重要

  • 最大尺寸{9999999,9999999}
  • 最小尺寸{-1,-1}

SetPos()

从类图中可以看到,PlaceHolder的SetPos()是最原始操作,仅仅做了简单赋值。对于后面的派生类来说,显然是不够的。因此做为虚方法,派生类会重载

Control

Control是真正意义上的控件基类,占据了95%的属性和方法,也是内容最庞大的类。除了从PlaceHolder继承了一些布局属性方法,它还有背景色,边框,鼠标,键盘,事件等等各种属性和方法。

下面挑几个比较常用又比较重要的属性记录

SetAttribute:此方法为虚方法,用于给某类控件设置属性。由于派生类的控件可能具有自己独特的属性,因此,只要某个控件有专项属性,SetAttribute()方法就会重写。在类图中很清晰的表明这一点
SetPos():Control已经不同于最原始的PlaceHolder,进化的比较高级。因此SetPos()方法也要重写。在这个地方穿插一段duilib的思路。

————穿插的一段内容————

在duilib中,可以简单理解为只有两种控件

一种是容器,也就是Box及其以上派生类。
容器的特点:主要用于布局,可以包含容器和控件
容器也是控件

另一种是控件,这里是狭隘的说法,控件就是真正工作的控件,比如Control Label Button CheckBox等等,它们是叶子结点,不能再包含容器和控件

仅仅为了方便理解做了这样的区分,实际上容器也是控件
有了这个认识,再来看Control的SetPos()就很容易理解,Control由于是叶子结点,不再包含其它,因此它的SetPos()就是将自己移一移就可以,主要工作就是和祖先做交集运算而不致于超出祖先窗口之外,和子窗口的原理相似

Box

Box继承自Control,是容器的基类。由于它能包含子控件,因此多了一些Add,Remove的方法。Box承前启后,属于非常核心的类。

下面记录它的SetPos()

SetPos()

Box是容器,和Control进行比较,它的责任明显要重大很多。包含两层含义

  1. 首先Box也是控件,因此在进行SetPos()的时候,需要调用Control::SetPos()将自己固定好,也就是说先把自己安排好
  2. 之后,需要将自己的子控件也进行布局,这就是容器责任重大的原因,不仅要处理自身,还要附带将子控件一并进行布局。因此Box的SetPos()又复杂了一点。

下面看下Box的布局特点

Box的布局比较简单,就是无序叠放。如上图所示,Box包含三个子控件,每个控件默认都以左上角原点为起点进行堆放

1.如果一个Box是auto,它怎么计算自己需要的尺寸呢?从上面自然可以想到,它会遍历子结点,取最大的宽,最大的高做为自己的尺寸,也就是最大可能的容纳所有子控件。SetPos()的算法确实也是这么做的。

如果想在Box容器中有序布局,只有采用Float(浮动)的方式。Float和CSS中的浮动很类似,就是采用绝对定位,不再受限于父结点。 

SetPos()的具体调用为:SetPos() -> ArrangeChild() -> SetFloatPos() ->EstimateSize()

其中EstimateSize后面专门记录

有了思路再看上述的代码就比较简易

估算 EstimateSize

EstimateSize()是在PlaceHolder中定义的虚方法,字面上看是估算、预估等意思,这里采用估算的说法。在PlaceHolder中仅仅做了原始的赋值操作。
为什么需要估算呢?仅仅因为auto关键字。auto的意思是自适应,因为不能从字面上得到确切的尺寸信息,因此需要估算。

举个例子说,有一Box包含三个控件A B C,在进行SetPos()的时候必须知道A B C究竟多大,不然没法对它们进行排列布局

<Box width=”200” height=”auto”>

<Control...>

<Control..>

..</Box>

控件的估算

控件不能再包含控件和容器,它的估算方式进入源码可以得到下面信息:

  1. 如果有明确的高,或者拉伸,就返回值
  2. 如果宽高某项为auto就要估算。这也验证前面所说的,凡是需要Estimate的,都是和auto有关联

由于Control形式简单,它的估算算法是,先看有没有图片,像背景图和状态图等,如果有,就以背景图做为自己的尺寸,接着再看有没有文本之类,由于Control不具备文本的能力,因此在Label及其以上的派生类中会重写。

里面涉及到GDI+的操作,估算一幅图片或一片文字的尺寸,GDI+完全有能力做这事儿

总结:Control的估算,是以自己内部的图和文本为基准的相关计算,它的源码写的比较容易理解,可以参看源码Control::EstimateSize()

 

有了这个理解,就可以明白下面这句:

<Control width=”auto” height=”auto” bkimage=”file=’...png’” />

Control继承自PlaceHolder,它的默认m_cxyFixed没有改写,即默认拉伸。上面指明auto之后,所占用(估算)的面积以背景图片尺寸为准,可以理解为Control自适应图片

容器的估算

由于容器的形式有很多种,比如Box,VBox,HBox,每种的规则都不同,因此估算的方法也不同,所以EstimateSize()就设为虚方法

下面先不谈具体的算法,而是拿容器和控件进行对比。

容器可以包含容器,也可以包含控件。如果对容器进行估算,就是对子控件的遍历估算,如果它是容器,就进一步的遍历,直到穷尽到叶子结点(控件)

这样来看,最终还是控件的估算,理解这一点不至于在复杂的布局中绕迷糊

Box的估算

具体算法在源码中写的比较清楚,一句话概括:遍历子控件,取最大的宽度,和最大的高度做为自己的尺寸

下面举个简例:

<Box width=”200” height=”auto”>

<Control name=”A” bkcolor=”gray” />

<Control name=”B” height=”30” bkcolor=”red” />

<Control name=”C” width=”auto” height=”auto” bkimage=”file=’abc.png’ “ />

</Box>

分析:Box如果想知道自己尺寸多大,就必须先估算自己的子控件大小。

控件A,默认是DUI_LENGTH_STRETCH -1 拉伸,估算结果为:{-1,-1}
控件B,width默认为-1 拉伸,估算结果为:{-1,30}
控件C,由于设置了auto,估算结果为图片尺寸,假如为{140,140}

通过大小比较,得到height=140,最后的Box={200,140}

程序运行结果如下:

完整的xml:

<?xml version="1.0" encoding="UTF-8"?>

<Window size="600,500" caption="0,0,0,35" shadowattached="false">

<Box width="200" height="auto" name=”root”>

<Control name="A" bkcolor="gray" />

<Control name="B" height="30" bkcolor="red" />

<Control name="C" width="auto" height="auto" bkimage="file='abc.png'" />

</Box>

</Window>

布局起源

这节从0开始记录,为什么上面的XML会显示这样的结果?

为了简化问题,将窗口的默认阴影去掉

通过框架源码可知,Window的成员m_pRoot就是指向上面name=”root”的Box *

Window::Paint()中的估算

代码运行到Window::Paint()方法时

由于m_pRoot的height为auto,此处进行了估算操作,同时给出了预定义的最大尺寸为{99999,99999}

接下来,m_pRoot执行EstimateSize()进行估算,前面已经说明思路,流程比较简单,最终得到的估算结果为{200,140}

 

最后,调用::MoveWindow将窗口进行调整,而尺寸是估算的结果,这说明一个问题:

虽然<Window>设置了size=”600,500”,但是这个属性对最终窗口尺寸的影响不是绝对的,甚至不起作用。从这个例子可以看到,顶层Box设置了auto,在估算的情况下以估算结果做为最终结果

 

因此,可以将上面的XML改为

<Window>

<Box width=”200” height=”auto”..>

...

不再设置Window的size,而是将Box的height设为auto,这种情形就是窗口自适应。在自适应情况下,框架给了一块最大的可用面积{99999,99999},在这块面积上进行估算

此外,从上面的分析中还可以得到一些简单的结论

 

1、如果<Window>不设置size,那么最外层容器要么是给定固定宽高,比如<Box width=”200” height=”100”>,要么设置auto。
如果外层窗口什么也不设置,这个窗口就是不存在尺寸。因为默认情况下,容器的m_cxyFixed是stretch,结果什么都没做。

<Window> //这种写法是错误的

<Box>

..

...</Box>

</Window>

Window::Paint()中的SetPos()

接着代码运行到

 

这个地方是布局的开始,此处的rcClient有两种来源

  1. 如果m_pRoot什么都没设置,那么<Window>必须给出一个size,比如:
    <Window size=”600,500”>

<Box>

...

此时,m_pRoot自然也不需要估算,那么rcClient就是size的尺寸。为什么它们之间是相同的呢?
这个问题需要回到<Window>标签的解析,对于<Window>的size属性,在WindowBuilder::Create()中可以看到

此处调用了Window的SetInitSize()方法,在此方法中调用了API ::SetWindowPos(),此处解释了上面的问题

  1. 如果m_pRoot设置了auto,就通过估算得到窗口的尺寸

不管如何,rcClient都是通过::GetClientRect()得到..

另外,如果两者都设置了固定宽高,那么以window的size为准,多出的被裁剪掉,在这种情况下,size起了作用

BoxSetPos()

m_pRoot进行SetPos(),这个问题实际上可以换个说法,就是对容器进行SetPos()。因为最外层的控件必须是容器 — Box及其以上派生类。

但不管如何,此时rcClient必须有值,也就是说SetPos之前坐标事实上已经确定好了。

对于本例来说,m_pRoot是因为估算了自己确定了Window的尺寸。对于容器的SetPos()前面着重写过,需要两步,1. 先将自己固定好 2.再将子控件进行排列

对于第一步,就导致m_pRoot和窗口完全一样大小。

第二步的调用顺序:m_pRoot->SetPos()-> [m_pLayout->ArrangeChild()] -> [m_pLayout->SetFloatPos()] -> pControl->SetPos()

在这段流程的最后,每个控件的m_rcItem得到了值,确定了自己在容器中的位置,这个解释了每个控件的位置究竟从哪来的问题。

由于最外层必须是容器,因此也可以说所有控件都在容器中

VBoxHBox

两者非常相似,这里选VBox进行记录

Layout

对于容器,另外又分离出一个类Layout,专门用了布局相关的操作。从源码中可以看到,Layout主要处理布局相关的参数,比如padding,margin,ArrangeChild,AjustSizeBychild等

由于容器也有区别,因此又从Layout派生出VLayout,HLayout,分别对应于VBox,HBox

VBox的构造

VBox比较简易,构造方法:VBox::VBox() : Box(new VLayout())

仅仅产生一个VLayout,除此外再也没有其它

VLayout重写了ArrangeChild,AjustSizeByChild这两个最重要的布局方法,原因在于VBox有自己的一套规则

VBox是垂直容器,它的子控件默认是从上往下排列,特别着重y轴。如上图所示,假如VBox的高100,它的算法思路是:

高100称为可用高度

如果子控件的高度为固定高,就减

如果子控件的高度为stretch,就设置一个参数nAdjustables进行计数,这个词可以理解为自适应,也就是当前不知道它有多高,但通过最后的计算能得到调整。
如上图所示,假如A是固定高20,B C未设值,就是默认stretch,则nAdjustables为2,首先将明确具有固定高的20减去,然后将剩余的可用高/nAdjustables,得到B C的高度为40,40。
从这个算法中可以看到,如果height设置为stretch,就默认平分剩余的可用高。

除此外,还掺杂着其它的附加属性计算,比如padding,margin,ChildMargin等等,算法原理比较简单

VBoxEstimateSize/AjustSizeChild

EstimateSize在Box中被重载,核心是调用m_pLayout->AjustSizeByChild(items,szAvailable)
由于VBox没有重写任何方法,因此EstimateSize调用的是VLayout::AjustSizeChild()

在这个方法中,关键的就是一句:

totalSize.cy += itemSize.cy + pChildControl->GetMargin().top + pChildControl->GetMargin().bottom
totalSize将每个子控件的高度相加,也说明了VBox的特点,就是上下堆叠

 

总结

1、如果Window没有设置size,则顶层容器至少有auto

2、如果Window设置了size

1) 顶层容器设置了auto,则需要估算,以估算结果为准

2) 顶层容器也设置了固定宽高,则以size为准

<Window>

<Box width=”200” height=”auto”>

<Control height=”30”..>

<VBox height=”auto”..>

<Control height=”50”..

..

...

分析:Window没有设置size,则顶层容器必须有一个auto

1)首先进行Box的EstimateSize()。调用m_pLayout->ArrangeChild(),在排列子控件时,再对子控件进行EstimateSize(),即Control的估算,和VBox的估算。

Control的估算比较简单,牵涉到图片和文本

VBox估算则调用VLayout的AjustSizeByChild,主要原理是将控件高度相加

如果VBox有嵌套,则递归进行估算,以此类推

 

 

posted on 2021-02-10 00:19  LuoTian  阅读(1037)  评论(0编辑  收藏  举报