首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

GEF 入門系列(五):淺談佈局管理

     編輯:Java 相關知識

雖然許多 GEF 應用都會用到連接,但也有些應用不需要用連接表達關係,我們目前的專案即是一例。此類應用中,模型對象間關係主要通過圖形包含表達,多為一對多關係。

圖1 不使用連接的 GEF 應用

專案簡介與模型構造

專案需要一個圖形化模型編輯器,主要功能:在具有三行 N 列的表格中自由增刪節點,節點可在不同單元格間拖動,可合併相鄰節點,表格列可增減、拖動等。因 SWT/JFace 提供的表格難以實現這些功能,選擇使用 GEF 開發,效果不錯(見下圖)。這裡介紹實現過程中與圖形和佈局相關的問題。

動手前首先考慮模型構造。Draw2D 僅提供有限佈局(ToolbarLayout、FlowLayout、XYLayout),無 GridLayout,故不能把整個表格作為一個 EditPart,而應把每一列看作一個 EditPart(因對列操作多於行),才能實現列拖動。另外,節點包含在列中,但仔細研究發現,節點並非直接包含在列中,而是有單元格對象作為中間橋梁:每列包含固定三個單元格,每單元格可包含任意個節點。經分析,模型、EditPart 和 Figure 初步成形:

模型EditPartFigure
畫布DiagramDiagramPartFreeformLayer
列ColumnColumnPartColumnFigure
單元格CellCellPartCellFigure
節點NodeNodePartNodeFigure

表中從上到下為包含關係(一對多),下圖簡單顯示這些關係:

圖2 圖形包含關係圖

畫布佈局

畫布上列顯示為縱向矩形,每列有頭部顯示列名,所有列橫向排列。故畫布應使用 ToolbarLayout 或 FlowLayout。兩者相似,皆按指定方向排列圖形,差異在於:圖形過多容納不下時,ToolbarLayout 犧牲部分圖形保持一行(列),FlowLayout 則允許換行(列)顯示。對我們的畫布,顯然應用 ToolbarLayout,因子圖形 ColumnFigure 不應出現換行。定義畫布圖形代碼:

Figure f = new FreeformLayer();
ToolbarLayout layout = new ToolbarLayout();
layout.setVertical(false);
layout.setSpacing(5);
layout.setStretchMinorAxis(true);
f.setLayoutManager(layout);
f.setBorder(new MarginBorder(5));

setVertical(false) 指定橫向排列子圖形,setSpacing(5) 指定子圖形間保留 5 像素距離,setStretchMinorAxis(true) 指定每個子圖形高度保持一致。

ColumnFigure 佈局

ColumnFigure 較複雜:需有頭部區域,且三個子圖形(CellFigure)合起來要能充滿下部區域並適應高度變化。一開始用 Draw2D 提供的 Label 實現列頭,但無法設置高度,因 Label 覆寫了 getPreferredSize(),高度只與文本相關。解決方法是構造 HeaderFigure,維護一個 Label,設置列頭高度時實際設置 HeaderFigure 高度;或直接讓 HeaderFigure 繼承 Label 並重新覆寫 getPreferredSize()。專案採用前者。

第二個問題花了些時間解決:一開始在 CellPart 的 refreshVisuals() 手動設置 CellFigure 高度為 ColumnFigure 下部區域高度的三分之一,但這很勉強,還需額外考慮 spacing 影響。後來通過自定義佈局較圓滿解決:讓 ColumnFigure 使用自定義 ColumnLayout,繼承 ToolbarLayout 但覆寫 layout() 方法:

class ColumnLayout extends ToolbarLayout {
    public void layout(IFigure parent) {
        IFigure nameFigure = (IFigure) parent.getChildren().get(0);
        IFigure childrenFigure = (IFigure) parent.getChildren().get(1);
        Rectangle clientArea = parent.getClientArea();
        nameFigure.setBounds(new Rectangle(clientArea.x, clientArea.y, clientArea.width, 30));
        childrenFigure.setBounds(new Rectangle(clientArea.x, nameFigure.getBounds().height + clientArea.y,
                clientArea.width, clientArea.height - nameFigure.getBounds().height));
    }
}

即在 layout 中控制列頭和下部高度分別為 30 和剩餘高度。為讓單元格正確定位,還需指定列下部圖形(childrenFigure)的佈局管理器。Draw2D 無類似 SWT FillLayout 的管理器,故再自定義 FillLayout(與 SWT 同名),覆寫 layout 方法(因用了 transposer,horizontal 和 vertical 可統一處理,transposer 僅在 horizontal 時起作用):

public void layout(IFigure parent) {
    List children = parent.getChildren();
    int numChildren = children.size();
    Rectangle clientArea = transposer.t(parent.getClientArea());
    int x = clientArea.x;
    int y = clientArea.y;
    for (int i = 0; i < numChildren; i++) {
        IFigure child = (IFigure) children.get(i);
        Rectangle newBounds = new Rectangle(x, y, clientArea.width, -1);
        int divided = (clientArea.height - ((numChildren - 1) * spacing)) / numChildren;
        if (i == numChildren - 1)
            divided = clientArea.height - ((divided + spacing) * (numChildren - 1));
        newBounds.height = divided;
        child.setBounds(transposer.t(newBounds));
        y += newBounds.height + spacing;
    }
}

作用是將父圖形高(寬)度平均分配給每個子圖形,最後一位子圖形佔據所有剩餘空間(防止除不盡留下空白)。完成 FillLayout,讓 childrenFigure 使用它作為佈局管理器。下面是 ColumnFigure 大部分代碼,列頭圖形(HeaderFigure)和列下部圖形(ChildrenFigure)作為內部類:

private HeaderFigure name = new HeaderFigure();
private ChildrenFigure childrenFigure = new ChildrenFigure();
public ColumnFigure() {
    ToolbarLayout layout = new ColumnLayout();
    layout.setVertical(true);
    layout.setStretchMinorAxis(true);
    setLayoutManager(layout);
    setBorder(new LineBorder());
    setBackgroundColor(color);
    setOpaque(true);
    add(name);
    add(childrenFigure);
    setPreferredSize(100, -1);
}
class ChildrenFigure extends Figure {
    public ChildrenFigure() {
        ToolbarLayout layout = new FillLayout();
        layout.setMinorAlignment(ToolbarLayout.ALIGN_CENTER);
        layout.setStretchMinorAxis(true);
        layout.setVertical(true);
        layout.setSpacing(5);
        setLayoutManager(layout);
    }
}
class HeaderFigure extends Figure {
    private String text;
    private Label label;
    public HeaderFigure() {
        this.label = new Label();
        this.add(label);
        setOpaque(true);
    }
    public String getText() { return this.label.getText(); }
    public Rectangle getTextBounds() { return this.label.getTextBounds(); }
    public void setText(String text) {
        this.text = text;
        this.label.setText(text);
        this.repaint();
    }
    public void setBounds(Rectangle rect) {
        super.setBounds(rect);
        this.label.setBounds(rect);
    }
}

單元格與節點佈局

單元格佈局管理器同樣使用 FillLayout,因需求中:用戶向單元格添加第一個節點時,該節點充滿單元格;兩個節點時各佔二分之一高度;以此類推。下表總結各圖形使用的佈局管理器:

圖形佈局管理器直接子圖形
畫布ToolbarLayout列
列ColumnLayout列頭部、列下部
-列頭部無無
-列下部FillLayout單元格
單元格FillLayout節點
節點無無

只有包含子圖形的圖形才需佈局管理器,因佈局管理器關心和管理的是「子」圖形,請牢記此點。

常見問題與解決

特別提醒:圖形使用 ToolbarLayout 或子類作為佈局管理器時,對應 EditPart 若安裝了 FlowLayoutEditPolicy 或子類,可能得到 ClassCastException。例如 CellFigure 對應 CellPart,安裝了 CellLayoutEditPolicy(FlowLayoutEditPolicy 子類)。異常原因:FlowLayoutEditPolicy 的 isHorizontal() 會將圖形 layout 強制轉換為 FlowLayout,但我們用的是 ToolbarLayout。我認為這是 GEF 疏忽,因作者曾說 FlowLayout 可應用於 ToolbarLayout。解決方法:在你的 EditPolicy 中覆寫 isHorizontal(),先判斷 layout 是 ToolbarLayout 還是 FlowLayout,再返回合適布林值。

最後,畫布還有一問題未解:希望表格列增多到一定程度後,畫布可向右擴展尺寸。前文說畫布用 FreeformLayer,為達目的,還必須在 editor 設置 rootEditPart 為 ScalableRootEditPart,注意不是 ScalableFreeformRootEditPart,後者用於各方向都能擴展的畫布。各種 RootEditPart 用法後續將介紹。

以上結合具體實例講解了如何在 GEF 中使用 ToolbarLayout 及自定義簡單佈局管理器。構造圖形應遵守原則:盡量讓佈局管理器決定每個子圖形位置和尺寸,避免很多麻煩。當然有例外,如 XYLayout 這種只關心子圖形位置的管理器中,必須為每個子圖形指定尺寸,否則圖形因尺寸過小而不可見,這也是開發人員易疏忽的地方。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved