GTK+ 中有一些容器小部件,通過使用該工具箱的 API,可以創建用戶定義的容器。在 PyGTK 中也公開了這個 API。在本文中,學習如何在 PyGTK 中創建一個“權重表(weighted-table)” 容器。本文的實現介紹了 GTK+ 幾何結構管理的基本模型,並讓您大致了解實現容器部件時應該考慮哪些事項。
GTK+ 工具箱和它的 PyGTK 綁定包有一些便利的容器。如果只需將一組部件放在一起,水平的 gtk.HBox 或垂直的 gtk.VBox 通常就足夠了。如果要同時在這兩個方向上對齊,通常可以使用 gtk.Table。但是在某些情況下,您可能想通過設置一些更精細的需求來獲得更好的外觀。
通常,放置和調整部件大小並非計算密集型任務,但是可能涉及不同的因素和細節,需要多方考慮。因此,使用解釋語言創建一個管理器,便可以快速嘗試不同的方法和設置。本文描述的容器也許將鼓勵 PyGTK 開發人員編寫或改進其他的容器。另外,它也可能鼓勵在其他 GUI 工具箱中實現類似的容器策略。
|
關於表的權重
讓我們來看一個表容器的實現,在這個表容器中,行和列的像素大小可以通過指定的權重進行擴展。但是,在詳細討論之前,我們來看一些例子。
以下三個例子中的每個例子都有一組不同的按鈕子集,並且有一個附件策略。對於每個例子,首先顯示一個較小的像素空間,之後顯示更大的像素空間。
介面
WTable 部件類似於標準的 gtk.Table。它是從 gtk.Container 派生而來的。其主要的不同點在於:
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 方法有以下介面:
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 有以下類定義:
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 方法組成:
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。
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 類由下面的類構造函數中顯示的值組成。
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 值扮演兩個 角色:
為方便起見,我們使用以下的 “total wGrow” 方法:
def total_grow_weight(self): return self.wGrow + self.springs[0].wGrow + self.springs[1].wGrow |
然後,返回來看看 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+ 幾何結構管理模型由兩個階段組成:
要注意,這兩個階段都是遞歸的,有些 child 本身也是容器。
gtk.Widget(所有 PyGTK 部件的基類)的一些 foo 方法在內部調用虛方法 do_foo。目前,這一點沒有明確整理成文檔,但是本文和 參考資料 小節中的例子應該清楚地展示了這一點。同樣,我們的實現覆蓋了其中一些 do_foo 虛方法。
請求
請求階段通過以下方法實現:
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.width 和 oreq.height 欄位返回值。 reqMgrs 對象收集 child 所需的大小,然後調用類 req.Manager 的 solve() 方法。它確定並返回每個列和行所需的大小。現在,我們感興趣的是通過 oreq 返回的總數。注意,即使我們得到每個列和行的大小的解決方法,需求可能是以行和列的區間 的形式給出的。區間可能包含不止一個列或一個行。
分配
在此階段,您得到一部分珍貴的屏幕空間。這裡所得到的分配是之前詢問的空間請求和祖先(ancestor)容器策略(其中之一就是桌面窗口管理器,它可以以用戶交互的方式調整頂級窗口的大小)的結果。
如果分配的空間剛好符合建議的 WTable 需求,那麼只需按照前一步中的 req.Manager 類提供的方法在 WTable 的 child 之間分配空間。 否則,會得到額外的空間,或者出現像素不足,后一種情況較少見。在這兩種情況下,都需要對差額進行分配。
glue 和 widget 的 wGrow 值現在被用作偽(pseudo)需求;可以以類似的方法收集和解決這些需求。這一次,單位不是像素,而是相對權重,它們的相對份額被用於擴展或縮小列和行(參見 其他注意)。
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() 方法:
最後,調用 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 類的幫助下推算出所需的權重。
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 權重的情況,這種情況被視作全等。
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 提供的分配空間與它的需求之間的差額。
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 值本身。
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) 的一個二元組。
它使用前面提到的 total_grow_weight() 方法和下面的 round_div 方法(參見 參考資料 中的 PEP 238):
def round_div(n, d): return (n + d/2) / d |
需求解決
列區間 [b,e) 中的列 的每個大小需求 — 即符合 b <= c < e 的所有 c — 將得到其總和至少為某個值的一組大小。需求 r 用以下公式表示:
我想找到滿足需求不等式的 “最小” 大小(Si)。這實際上是一個線性編程問題。這種情況比一般的問題更特殊,因為:
由於這些特殊情況,最小解可能不是 惟一的,所以我還要直觀地衡量這個解。
我將簡要描述可以提供合理解決方法的 req.Manager 類功能。由於這不是本文的重點,而且它的解決方法是次優的,這裡不作詳述。
這個類在一個單獨的 req.py 模塊中,該模塊獨立於 GTK+。該模塊有一個內部測試,該測試使用 Python 的 if __name__ == '__main__': 結構。
下面是解決方法測試的一些例子。每個例子有一個給定的三元組 (Bi, Ei, Si),這意味著要尋找一組大小,並應滿足 [Bi, Ei) 的每個子區間加起來至少為 Si。
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 類的構造函數,其中包含添加需求的方法。
class Manager: def __init__(self): self.rangeReqs = [] self.reqs = None def addRangeReq(self, rangeReq): self.rangeReqs += [rangeReq] |
在這裡,rangeReq 是以下類的一個對象:
class RangeSize: def __init__(self, be=(0,1), sz=1): self.begin = be[0] self.end = be[1] self.size = sz |
通過啟髮式方法解決
最佳解決方案需要線性編程技術,而且所需的代碼更少。
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() 方法,並採用一種啟髮式方法,該方法由以下步驟組成:
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 代碼作比較,後者具有:
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,該程序有以下主要功能:
除了用戶編輯的 WTable 外,測試程序本身的控制窗口使用有默認 glue 的兩個 WTable 實例,因此要確認 WTable 實現是否是全局狀態。
(責任編輯:A6)
[火星人 ] 在 PyGTK 中管理部件幾何結構已經有690次圍觀