歡迎您光臨本站 註冊首頁

在 PyGTK 中管理部件幾何結構

←手機掃碼閱讀     火星人 @ 2014-03-12 , reply:0
  
GTK+ 中有一些容器小部件,通過使用該工具箱的 API,可以創建用戶定義的容器。在 PyGTK 中也公開了這個 API。在本文中,學習如何在 PyGTK 中創建一個“權重表(weighted-table)” 容器。本文的實現介紹了 GTK+ 幾何結構管理的基本模型,並讓您大致了解實現容器部件時應該考慮哪些事項。

GTK+ 工具箱和它的 PyGTK 綁定包有一些便利的容器。如果只需將一組部件放在一起,水平的 gtk.HBox 或垂直的 gtk.VBox 通常就足夠了。如果要同時在這兩個方向上對齊,通常可以使用 gtk.Table。但是在某些情況下,您可能想通過設置一些更精細的需求來獲得更好的外觀。

通常,放置和調整部件大小並非計算密集型任務,但是可能涉及不同的因素和細節,需要多方考慮。因此,使用解釋語言創建一個管理器,便可以快速嘗試不同的方法和設置。本文描述的容器也許將鼓勵 PyGTK 開發人員編寫或改進其他的容器。另外,它也可能鼓勵在其他 GUI 工具箱中實現類似的容器策略。

本文的主角
GTK+ 是一個用於創建圖形用戶界面的極易使用、功能豐富的工具箱。它的優勢在於跨平台兼容性和易於使用的 API。GTK+ 是用 C 編寫的,但是綁定了很多其他的流行編程語言,例如 C++、Python 和 C#。

PyGTK 是用於 Python 的 GTK+ — 它幫助您使用 Python 編程語言輕鬆創建具有圖形用戶界面的程序。

流行的對齊部件包括用於水平方向的 gtk.HBox 和用於垂直方向的 gtk.VBox。

至於水平和垂直兩個方向上的布局,可以考慮 gtk.Table。

關於表的權重

讓我們來看一個表容器的實現,在這個表容器中,行和列的像素大小可以通過指定的權重進行擴展。但是,在詳細討論之前,我們來看一些例子。

以下三個例子中的每個例子都有一組不同的按鈕子集,並且有一個附件策略。對於每個例子,首先顯示一個較小的像素空間,之後顯示更大的像素空間。

  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):        

leftright 和 topbottom 是分別用於列和行的類 STL(標準模板庫)的半開區間(half-open range)。區間 [a,b) 或 (a,b] 是半開的,其一端是 “打開的”(不包括它的極限點),另一端是 “關閉的”(包括它的極限點)。對於 glue 參數,要注意我如何使用 (0,1) 索引。

維和枚舉

水平維與垂直維是對稱的。這裡不使用乘法代碼(multiplying code),而是使用以下維枚舉: (X, Y) = (0, 1)。我們在循環中使用它,索引大小為 2 的數組。對於左、右、上、下邊距以及它們相關的 Spring 類,我們也使用類似的 (0,1) 枚舉(後面有更詳細的描述)。

注意,gtk.Requisition(該對象包含有關部件所需空間需求的信息)具有 width 和 height 欄位,其空間分配由 gtk.gdk.Rectangle(包含關於一個矩形的數據的對象)指定,gtk.gdk.Rectangle 具有 x、y、 width 和 height 欄位,這些欄位沒有使用方便的索引。

用 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])  

其他注意
作為演示,這裡忽略了很多可能的改進。例如,參數驗證和 Python 的標準的、可選的方法字元串文檔均被忽略。

類似地,我們並沒有優雅地處理用戶錯誤,例如檢查無效的區間。

Glue 對象可以安全地被共享,因為 WTable 既不會擴大它們,也不會更改它們的值。

wGrow 權重值主要用於擴展給定的額外空間。對於像素不足的特殊情況,實際上需要元素有更大的 wGrow,從而縮小得更少。 因此,可以以遞減函數的方式轉換這些數字。而更聰明的 Glue 類可以有顯式的縮小權重。

它通過 oreq.width 和 oreq.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.x 和 allocation.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 的空間分配

現在,列和行的分配已確定,並且用 crSizes 和 offsets 表示,接下來為每個 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 實現是否是全局狀態。

(責任編輯:A6)



[火星人 ] 在 PyGTK 中管理部件幾何結構已經有690次圍觀

http://coctec.com/docs/linux/show-post-69169.html