内容


在 PyGTK 中管理部件几何结构

探索放置和调整容器部件的考虑事项

Comments

GTK+ 工具箱和它的 PyGTK 绑定包有一些便利的容器。如果只需将一组部件放在一起,水平的 gtk.HBox 或垂直的 gtk.VBox 通常就足够了。如果要同时在这两个方向上对齐,通常可以使用 gtk.Table。但是在某些情况下,您可能想通过设置一些更精细的需求来获得更好的外观。

通常,放置和调整部件大小并非计算密集型任务,但是可能涉及不同的因素和细节,需要多方考虑。因此,使用解释语言创建一个管理器,便可以快速尝试不同的方法和设置。本文描述的容器也许将鼓励 PyGTK 开发人员编写或改进其他的容器。另外,它也可能鼓励在其他 GUI 工具箱中实现类似的容器策略。

关于表的权重

让我们来看一个表容器的实现,在这个表容器中,行和列的像素大小可以通过指定的权重进行扩展。但是,在详细讨论之前,我们来看一些例子。

以下三个例子中的每个例子都有一组不同的按钮子集,并且有一个附件策略。对于每个例子,首先显示一个较小的像素空间,之后显示更大的像素空间。

  1. 这里有一个单独的按钮,其左边留出 20 个像素,并且平等共享按钮本身与其右空白之间的额外的水平像素空间。在垂直方向上,按照与(上边距,按钮本身,下边距)对应的(1,4,2)的比例分配额外空间。
    图 1. 较小空间内的单个按钮
    较小空间内的单个按钮
    图 2. 较大空间内的单个按钮
    较大空间内的单个按钮
    较大空间内的单个按钮
  2. 现在我们有两个按钮,两者之间的大小比例为 2:3,额外水平空间比例为 1:3。
    图 3. 较小空间内的两个按钮
    较小空间内的两个按钮
    图 4. 较大空间内的两个按钮
    较大空间内的两个按钮
    较大空间内的两个按钮
  3. 接下来是多列需求,我们希望 “middle7/8” 按钮获得 7/8 的额外 水平像素空间。
    图 5. 较小空间内的多列需求
    较小空间内的多列需求
    图 6. 较大空间内的多列需求
    较大空间内的多列需求
    较大空间内的多列需求

接口

WTable 部件类似于标准的 gtk.Table。它是从 gtk.Container 派生而来的。其主要的不同点在于:

  • 不需要预先确定行和列的数量。
  • gtk.Table 的 attach 方法提供了可选的 xoptions=GTK.EXPAND|GTK.FILL, yoptions=GTK.EXPAND|GTK.FILL, xpadding=0, ypadding=0 参数,而 WTable.attach 方法有一个可选的 glue 参数,后面会谈到这个参数。
清单 1. WTable 构造函数
class WTable(GTK.Container):

    def __init__(self, maxsize=None):
        GTK.Container.__init__(self)
        self.gChildren = []
        self.maxsize = maxsize or (gdk.screen_width(), gdk.screen_height())

默认情况下,清单 1 中的 self.maxsize 实际上对 WTable 的宽度和高度没有约束。

attach 方法有以下接口:

清单 2. WTable.attach 接口
def attach(self, widget, leftright=(0,1), topbottom=(0,1), glue=None):

leftrighttopbottom 是分别用于列和行的类 STL(标准模板库)的半开区间(half-open range)。区间 [a,b) 或 (a,b] 是半开的,其一端是 “打开的”(不包括它的极限点),另一端是 “关闭的”(包括它的极限点)。对于 glue 参数,要注意我如何使用 (0,1) 索引。

维和枚举

水平维与垂直维是对称的。这里不使用乘法代码(multiplying code),而是使用以下维枚举: (X, Y) = (0, 1)。我们在循环中使用它,索引大小为 2 的数组。对于左、右、上、下边距以及它们相关的 Spring 类,我们也使用类似的 (0,1) 枚举(后面有更详细的描述)。

注意,gtk.Requisition(该对象包含有关部件所需空间需求的信息)具有 widthheight 字段,其空间分配由 gtk.gdk.Rectangle(包含关于一个矩形的数据的对象)指定,gtk.gdk.Rectangle 具有 xywidthheight 字段,这些字段没有使用方便的索引。

用 Glue 实现附加

当 WTable 附加了一个部件时,会创建一个 child 对象,并将该对象添加到 WTable 的 gChildren 列表中。child 有以下类定义:

清单 3. Child
class Child:
    "Child of WTable. Adoption data"
    def __init__(self, w, leftright=(0,1), topbottom=(0,1), glue=None):
        self.widget = w
        # lrtb: 2x2 tuple:( Cols[begin, end), Rows[Begin, end) )
        self.lrtb = (leftright, topbottom)
        self.glue = glue

它存放传递给 WTable attach 方法的数据。

Glue 类同时处理两个维。它由两个单维的 Glue1D 方法组成:

清单 4. Glue
class Glue:
    "2-Dimensional Glue"
    def __init__(self, xglue=None, yglue=None):
        self.xy = (xglue or Glue1D(), yglue or Glue1D())

每个 Glue1D 由一个 grow weight 值和两个 spring 组成,取决于不同的维,这两个 spring 可以是 left 和 right 或 top 和 bottom。我们将这些类型的值称作 wGrow

清单 5. Glue1D
class Glue1D:
    "1-Dimensional Glue"
    def __init__(self, preSpring=None, postSpring=None, wGrow=1):
        self.springs = (preSpring or Spring(), postSpring or Spring())
        self.wGrow = wGrow # of owning widget

最后,Spring 类由下面的类构造函数中显示的值组成。

清单 6. Spring
class Spring:
    "One side attachment requirement"
    def __init__(self, pad=0, wGrow=0):
        self.pad = pad
        self.wGrow = wGrow

总之,一个 Glue 对象包含 4 个 Spring 对象,这 4 个 Spring 影响边距的大小。(2x(1+2)=6) wGrow 值扮演两个 角色:

  • 全局性确定行或列获得的相关的额外像素空间。
  • 对于每个附加的 child,局部确定 child 与它的(2x2=4)边距之间的额外空间的分配。

为方便起见,我们使用以下的 “total wGrow” 方法:

清单 7. Glue1D.total_grow_weight
def total_grow_weight(self):
    return self.wGrow + self.springs[0].wGrow + self.springs[1].wGrow

然后,返回来看看 attach 方法。(注意,您实际上是允许方便地指定单个的列和单个的行,而不是一个列和行区间。

清单 8. WTable.attach
def attach(self, widget, leftright=(0,1), topbottom=(0,1), glue=None):
    if glue == None:
        glue = Glue()
    # Conveniently change possible single n value to (n,n+1)
    lrtb = map(lambda x: (type(x) == types.IntType) and (x, x+1) or x,
               (leftright, topbottom))
    child = Child(widget, lrtb[0], lrtb[1], glue)
    self.gChildren.append(child)
    if self.flags() & GTK.REALIZED:
        widget.set_parent_window(self.window)
    widget.set_parent(self)
    self.queue_resize()
    return child

实现

GTK+ 几何结构管理模型由两个阶段组成:

  • 请求(requisition):在此阶段,容器遍历它的 child,并询问每个 child 所需的空间。
  • 分配(allocation):在此阶段,为容器指定子矩形像素空间,并将其划分给它的 child。

要注意,这两个阶段都是递归的,有些 child 本身也是容器。

gtk.Widget(所有 PyGTK 部件的基类)的一些 foo 方法在内部调用虚方法 do_foo。目前,这一点没有明确整理成文档,但是本文和 参考资料 小节中的例子应该清楚地展示了这一点。同样,我们的实现覆盖了其中一些 do_foo 虚方法。

请求

请求阶段通过以下方法实现:

清单 9. WTable.do_size_request
def do_size_request(self, oreq):
    reqMgrs = (req.Manager(), req.Manager()) # (X,Y)
    for child in self.gChildren:
        request = child.widget.size_request() # compute!
        for xyi in (X, Y):
            be = child.lrtb[xyi]
            glue1d = child.glue.xy[xyi]
            sz = request[xyi] + glue1d.base()
            rr = req.RangeSize(be, sz)
            reqMgrs[xyi].addRangeReq(rr)
    self.reqs = map(lambda m: m.solve(), reqMgrs) # (X,Y)
    bw2 = 2 * self.border_width
    oreq.width  = min(sum(self.reqs[X]) + bw2, self.maxsize[0])
    oreq.height = min(sum(self.reqs[Y]) + bw2, self.maxsize[1])

它通过 oreq.widthoreq.height 字段返回值。 reqMgrs 对象收集 child 所需的大小,然后调用类 req.Manager 的 solve() 方法。它确定并返回每个列和行所需的大小。现在,我们感兴趣的是通过 oreq 返回的总数。注意,即使我们得到每个列和行的大小的解决方法,需求可能是以行和列的区间 的形式给出的。区间可能包含不止一个列或一个行。

分配

在此阶段,您得到一部分珍贵的屏幕空间。这里所得到的分配是之前询问的空间请求和祖先(ancestor)容器策略(其中之一就是桌面窗口管理器,它可以以用户交互的方式调整顶级窗口的大小)的结果。

如果分配的空间刚好符合建议的 WTable 需求,那么只需按照前一步中的 req.Manager 类提供的方法在 WTable 的 child 之间分配空间。 否则,会得到额外的空间,或者出现像素不足,后一种情况较少见。在这两种情况下,都需要对差额进行分配。

glue 和 widget 的 wGrow 值现在被用作伪(pseudo)需求;可以以类似的方法收集和解决这些需求。这一次,单位不是像素,而是相对权重,它们的相对份额被用于扩展或缩小列和行(参见 其他注意)。

清单 10. WTable.do_size_allocate
def do_size_allocate(self, allocation):
    self.allocation = allocation
    allocs = (allocation.width, allocation.height)
    alloc_offsets = (self.border_width, self.border_width)
    self.crSizes = [None, None]  # 2-lists: columns size, rows sizes.
    self.offsets = [None, None]  # alloc_offsets + partial sums of crSizes
    for xyi in (X, Y):
        a = allocs[xyi]
        reqs = self.reqs[xyi]
        gWeights = self._getGrowWeights(xyi)
        self.crSizes[xyi] = given = self._divide(allocs[xyi], reqs, gWeights)
        offsets = len(given) * [None]
        offsets[0] = alloc_offsets[xyi]
        for oi in range(1, len(offsets)):
            offsets[oi] = offsets[oi - 1] + given[oi - 1]
        self.offsets[xyi] = offsets
    for child in self.gChildren:
        self._allocate_child(child)

    if self.flags() & GTK.REALIZED:
        self.window.move_resize(*allocation)

对于清单 10 中的步骤,do_size_allocate() 方法:

  • 确定每个维上的每个分段(列或行)的大小。将这些大小的部分和与 allocation.xallocation.y 相加,便得到偏移量。
  • 确定行和列的分配大小之后,就可以设置每个 child 的分配。同样要注意,一个 child 不一定就占用一个 “单元”,它可能占用由一个行和列区间 组成的一个矩形。

最后,调用 window.move_resize() 方法。注意,allocation 参数被加上 * 前缀,以利用 gtk.gdk.Rectangle 类提供的排序方法(参见 define-boxed Rectangle 中的 fields 元素,它用于自动生成定义 PyGdkRectangle_Type 的 C 代码。这将 allocation 转换成与类 class gtk.gdk.Window 的 move_resize() 方法匹配的元组)。

表的空间分配划分

在前面的请求阶段中,确定了每个列和行所需的像素大小。现在需要划分一个给定的总分配。如果刚好与需求相符,那么可以直接使用它们,否则需要进行调整,加上或减去(较少见)额外的像素。

应该根据权重来划分差额。同样,用户不会显式地将权重赋给列和行,而是通过一个 “Glue” 将 wGrow 值赋给每个 child。这样做可以在 req.Manager 类的帮助下推算出所需的权重。

清单 11. WTable._getGrowWeights
def _getGrowWeights(self, xyi):
    wMgr = req.Manager()
    for child in self.gChildren:
        be = child.lrtb[xyi]
        glue1d = child.glue.xy[xyi]
        rr = req.RangeSize(be, glue1d.total_grow_weight())
        wMgr.addRangeReq(rr)
    wMgr.solve()
    gws = wMgr.reqs
    if sum(gws) == 0:
        gws = len(gws) * [1]  # if zero weights then equalize
    return gws

对于 “cake” 分配空间(清单 12),根据需求和权重对其进行划分,以划分差额。注意,当像素不足时,要对权重进行转换,这样较大的权重损失较小。另外,还需要注意全 0 权重的情况,这种情况被视作全等。

清单 12. WTable._divide
def _divide(self, cake, requirements, growWeights):
    n = len(requirements)   # == len(growWeights)
    given = requirements[:] # start with exact satisfaction
    reqTotal = sum(requirements)
    delta = cake - reqTotal
    if delta < 0: # rarely, "invert" weights
        growWeights = map(lambda x: max(growWeights) - x, growWeights)
        if sum(growWeights) == 0:
            growWeights = n * [1]; # equalize
    i = 0
    gwTotal = sum(growWeights)
    while gwTotal > 0 and delta != 0:
        add = (delta * growWeights[i] + gwTotal/2) / gwTotal
        gwTotal -= growWeights[i]
        given[i] += add
        delta -= add
        i += 1
    return given

child 的空间分配

现在,列和行的分配已确定,并且用 crSizesoffsets 表示,接下来为每个 child 分配空间就很简单了。惟一要考虑的是为 child 提供的分配空间与它的需求之间的差额。

清单 13. WTable._allocate_child
def _allocate_child(self, child):
    offsetsxy = [None, None]
    req = list( child.widget.get_child_requisition() ) # pre-calculated
    for xyi in (X, Y):
        segRange = child.lrtb[xyi]
        g1d = child.glue.xy[xyi]
        supply = sum( self.crSizes[xyi][segRange[0]: segRange[1]] )
        (oadd, cadd) = g1d.place(req[xyi], supply)
        offsetsxy[xyi] = self.offsets[xyi][ segRange[0] ] + oadd
        req[xyi] += cadd
    allocation = gdk.Rectangle(x=offsetsxy[0], y=offsetsxy[1],
                               width=req[0], height=req[1])
    child.widget.size_allocate(allocation)

为了确定 child 与边距要增长(或缩小)多少,使用 Glue1D.place 方法。对于每个 child,该方法被调用两次:每个 (X,Y) 维调用一次。对于一个给定的维,有 3 个 wGrow 值要考虑:两个边(左和右,或者上和下)和 child 值本身。

清单 14. Glue1D.place
def place(self, cneed, supply):
    pads = self.base()
    need = cneed + pads
    delta = supply - need;
    if delta >= 0:
        gwTotal = self.total_grow_weight()
        oadd = self.springs[0].pad
        if gwTotal == 0:
            cadd = delta
        else:
            oadd += round_div(delta * self.springs[0].wGrow, gwTotal)
            cadd = round_div(delta * self.wGrow, gwTotal)
    else: # rare
        shrink = -delta
        if pads >= shrink:  # Cutting from the pads is sufficient
            oadd = round_div(self.springs[0].pad * delta, pads)
            cadd = 0
        else:
            oadd = 0
            cadd = delta    # reduce the child as well
    return (oadd, cadd)

Glue1D.place 方法返回整数 (oadd,cadd) 的一个二元组。

  • oadd 被加到偏移量中。同样,这意味着第一个边距(左边距或上边距)将增长多少。
  • cadd 被加到 child 的 widget 分配空间(宽度或高度)中。

它使用前面提到的 total_grow_weight() 方法和下面的 round_div 方法(参见 参考资料 中的 PEP 238):

清单 15. round_div
def round_div(n, d):
    return (n + d/2) / d

需求解决

列区间 [b,e) 中的 的每个大小需求 — 即符合 b <= c < e 的所有 c— 将得到其总和至少为某个值的一组大小。需求 r 用以下公式表示:

图 7. 列大小需求
列大小需求

我想找到满足需求不等式的 “最小” 大小(Si)。这实际上是一个线性编程问题。这种情况比一般的问题更特殊,因为:

  • 所有因子就是 0 或 1。
  • 所有为 1 的因子在一个连续的变量范围中。
  • 我们只对整数解感兴趣。

由于这些特殊情况,最小解可能不是 惟一的,所以我还要直观地衡量这个解。

我将简要描述可以提供合理解决方法的 req.Manager 类功能。由于这不是本文的重点,而且它的解决方法是次优的,这里不作详述。

这个类在一个单独的 req.py 模块中,该模块独立于 GTK+。该模块有一个内部测试,该测试使用 Python 的 if __name__ == '__main__': 结构。

下面是解决方法测试的一些例子。每个例子有一个给定的三元组 (Bi, Ei, Si),这意味着要寻找一组大小,并应满足 [Bi, Ei) 的每个子区间加起来至少为 Si。

清单 16. req.Manager 解决方法示例
python req.py    0 1 20    1 2 30
Solution: [20, 30]

python req.py    0 2 10    1 3 5
Solution: [5, 5, 1]

python req.py    0 2 100    1 3 50
Solution: [46, 54, 6]

python req.py    0 2 100    1 3 50    2 3 20
Solution: [49, 51, 21]

向 req.Manager 添加需求

下面是 req.Manager 类的构造函数,其中包含添加需求的方法。

清单 17. req.ManageraddRangeReq 方法
class Manager:
    def __init__(self):
        self.rangeReqs = []
        self.reqs = None

    def addRangeReq(self, rangeReq):
        self.rangeReqs += [rangeReq]

在这里,rangeReq 是以下类的一个对象:

清单 18. req.RangeSize
class RangeSize:
    def __init__(self, be=(0,1), sz=1):
        self.begin = be[0]
        self.end = be[1]
        self.size = sz

通过启发式方法解决

最佳解决方案需要线性编程技术,而且所需的代码更少。

清单 19. req.Manager.solve
def solve(self):
    n = self.nSegments()
    m = len(self.rangeReqs)
    self.reqs = n * [0]
    self.rangeReqs.sort()

    # Satisfy single segment requirements
    dumSingle = RangeSize((n, n+1), 1)
    endSingleIndex = bisect.bisect_right(self.rangeReqs, dumSingle)
    for rr in self.rangeReqs[ : endSingleIndex]:
        curr = self.reqs[rr.begin]
        needMore = rr.size - curr
        self.reqs[rr.begin] += needMore
    bigRangeReqs = self.rangeReqs[endSingleIndex:] # non-single ranges

    self.partialSatisfy(bigRangeReqs, 1, 2) # half
    self.partialSatisfy(bigRangeReqs, 1, 2) # half
    self.partialSatisfy(bigRangeReqs, 1, 1) # complete
    return self.reqs

上面的清单 19 包括一个 solve() 方法,并采用一种启发式方法,该方法由以下步骤组成:

  1. 对需求排序。
  2. 满足 [b,e) 区间内 b+1=e 条件的单个元素需求。
  3. 对于每个未被满足的需求 r,加上 r 的未满足大小的一半(在 r 区间元素中平均分布)。这是通过调用 req.Manager.partialSatisfy 方法来完成的。这一步要做两次
  4. 和前面的步骤一样,现在加上完整的 未满足的大小。
清单 20. req.Manager.partialSatisfy
def partialSatisfy(self, bigRangeReqs, rationN, ratioD):
    # Thru reqs, add to satisfy  rationN/ratioD of requirement
    for rr in bigRangeReqs:
        curr = sum(self.reqs[rr.begin: rr.end])
        needMore = rr.size - curr
        if needMore > 0:
            give = (rationN * (needMore + ratioD - 1)) / ratioD
            q, r = divmod(give, rr.end - rr.begin)
            for si in range(rr.begin, rr.end):
                self.reqs[si] += q
            for si in range(rr.begin, rr.begin + r):
                self.reqs[si] += 1

可以将它与 GTK+ 的 gtk_table_size_request C 代码作比较,后者具有:

清单 21. gtk_table_size_request 片段
GTK_table_size_request_pass1 (table);
GTK_table_size_request_pass2 (table);
GTK_table_size_request_pass3 (table);
GTK_table_size_request_pass2 (table);

和这些 gtk_table_size_request_pass {1,2,3} 函数的实现。

测试

本文中提供的包带有一个测试程序 wtbl-test.py,该程序有以下主要功能:

  • 通过 GUI 创建一个 gtk.ToggleButton 和 Glue,并使用它们调用 WTable.attach()
  • 修改和删除 WTable 的 child 和 glue。
  • 从文本文件装载 gtk.ToggleButtons 和 Glue 的定义;创建和附加它们。本文前面使用该功能一致地创建 “关于表的权重” 小节中的例子
  • 将当前的 child 和它们的 glue 转储到可装载的文本文件中。

除了用户编辑的 WTable 外,测试程序本身的控制窗口使用有默认 glue 的两个 WTable 实例,因此要确认 WTable 实现是否是全局状态。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=314570
ArticleTitle=在 PyGTK 中管理部件几何结构
publish-date=06172008