界面設計應該是一項充滿創造性、富有樂趣的工作,但是卻往往被認為非常的枯燥和繁瑣。究其原因,是因為界面布局領域所采用的描述概念和具體的實現語言之間存在很大的語義隔閡。而一般的界面開發工具提供的所見即所得以及界面布局管理器等方案也無法很好地解決這個問題。
在本文中,我們會給出一種更好的解決方案,我們不是去試圖把界面設計者頭腦中的設計概念和樣式逐步降級、分解成所使用的實現語言能夠理解的低層概念,也不是提供一些已經完成的、確定的但難以擴充和更改的布局樣式庫供界面設計者使用。我們所提供的是一種專門用於描述高層界面設計樣式的語言。通過這種語言,界面設計者可以直接、明確地描述出他們頭腦中的布局設計樣式;通過這種語言,界面設計者可以自己方便地、靈活地制定自己需要的布局樣式。此外,本文中給出的設計思想對於其他領域的設計也有很好的借鑒作用。
創造性,還是乏味?
界面設計是一項非常有創造性,甚至富有藝術性的工作,一個簡潔、易用、漂亮的界面在帶給使用者方便的同時,也會給界面設計者帶來極大的成就感。但是,在現實中,情況似乎並非如此,很多人都認為做界面是一項非常繁瑣、機械、乏味的工作,並千方百計地去逃避界面相關的工作。這是為什麼呢?
原因很簡單,因為做界面其實涉及兩項工作,一項是界面的一些設計創意,包括界面的布局樣式以及和使用者的交互方式,這項工作充滿挑戰和樂趣。但是,這些設計創意最終是要落實到實現上的,這就是第二項工作。此時,你頭腦中那些清晰、完整的設計概念開始變得瑣碎,你不得不和那些低層次的坐標位置打交道。更糟糕的是,當你好不容易做好了一個界面,但是發現其中某些元素的布局需要一些調整時,這個你本應認為是一個很簡單的改變卻造成大量重復的低層次坐標位置更改時,你肯定會認為做界面是多麼的機械和乏味呀!
其實,造成這種認識的根源在於界面設計創意和實現這些創意概念的語言之間存在很大的斷層。這樣,在具體實現時,你就必須得把這些清晰、完整的布局樣式降級成一些瑣碎、沒有什麼意義的低層次的坐標值,使得實現語言能夠理解。這項工作不僅乏味,而且最終的實現也非常的脆弱 —— 一個在布局樣式層面非常簡單的更改,就會造成實現層面的巨大變動。比如:我們可以說把一組元素同時按比例縮小 10%,做過界面的朋友肯定知道這個更改意味著什麼。
為了應對這個斷層的問題,目前幾乎所有的涉及界面制作的開發工具都提供了相同的解決方法:可視化的界面設計工具以及布局管理器。但是這兩種方法都沒有從根本上解決這個問題。
可視化界面設計工具確實避免了不少繁瑣的界面元素擺放工作,但是對於專業的界面設計來說,通過拖放設計出來的界面在准確度和規范性上都有待提高,此外還有更為重要的一點,那就是存在於設計者頭腦中的布局樣式仍然沒有被明確地描述出來,而是被降級成一個個擺放在一起的零散的組件,雖然這些組件本身是可視的。這個語義斷層的存在同樣會使得通過可視化界面設計工具設計出來的界面非常脆弱。
布局管理器試圖通過提供一些常用的布局樣式來解決這個問題。但是,這種做法非常僵化,也就是說你只能使用現有的布局管理器,如果它們無法滿足你的要求,你也無法自己定制。此外,這些布局管理器僅僅適合於一些簡單的情況。對於一些復雜的布局樣式來說,它們的描述能力就顯得非常的不足。那些曾經和 GridBagLayOut 斗爭過的朋友對此肯定深有體會。
在本文中,我們會給出一種更好的解決方案,我們不是去試圖把界面設計者頭腦中的設計概念和樣式逐步降級、分解成所使用的實現語言能夠理解的低層概念,也不是提供一些已經完成的、確定的但難以擴充和更改的布局樣式庫供界面設計者使用。我們所提供的是一種專門用於描述高層界面設計樣式的語言。通過這種語言,界面設計者可以直接、明確地描述出他們頭腦中的布局設計樣式,通過這種語言,界面設計者可以自己方便地、靈活地制定自己需要的布局樣式。也就是說,本來僅存在於界面設計者頭腦中的抽象布局樣式,現在也變得可描述,可編程了。
界面布局語言介紹
在學習界面布局語言的設計之前,先來了解一下該語言的使用是非常有幫助的。我們的界面布局語言非常簡單,簡單到只有一種原子:Component。Component 是一種基本的布局元素,可以對 Component 進行平移和伸縮,使其和給定的一個布局空間 Rectangle 匹配。比如對於 Button 這個 Component 來講,它具有傳統按鈕的外觀,但是它在布局上所占的實際空間則是由為其指定的 Rectangle 決定的。此外,Component 要最終在界面上顯示出來,就必須有一個物理上的 Container。也就是說,只要給定了一個 Rectangle 和一個 Container,一個 Component 就可以在界面上指定的布局位置呈現出來。
例如,當我們使用布局語言在一個 JFrame 上坐標位置為 (0,0) 展示一個 width 為 200,height 為 60 的按鈕時,我們可以這樣來描述(為了簡潔起見,後面的代碼實例中均略去 Layout 名字空間前綴):
Button().title("button1").at(0,0,200,60).in(this.getContentPane());
其執行結果如下圖所示:
圖 1. Component 示例
僅僅提供這樣一種原子元素的語言顯然無法滿足我們前面提到的目標。在我們的界面布局語言中,還提供了兩種在布局中非常常用的兩種從已有組件構造新組件的組合手段:above 和 beside。其中 above 組合子接收 3 個參數:兩個現有 Component 以及一個比例,它會產生出一個新的復合 Component,其中按照給定的比例把第一個 Component 擺放在第二個 Component 之上。Beside 組合子接收同樣的 3 個參數,並且也產生出一個新的復合 Component,其中按照給定的比例把第一個 Component 擺放在第二個Component左邊。
例如,如果我們希望在一個給定的 Container C 上的 Rectangle(0,0,300,40) 中,平行擺放一個 TextField 和一個 Button,且希望 TextField 占據 80% 的比例時,可以這樣來描述:
beside(TextField(), Button().title("ok"), 0.8).at(0,0,300,40).in(C)
執行結果如下圖所示:
圖 2. beside 示例
同樣,我們可以使用 above 來進行如下描述:
above(TextField(), Button().title("ok"), 0.5).at(0,0,300,60).in(C)
其執行結果如下圖所示:
圖 3. above 示例
值得注意的是,在我們的界面布局語言中,Component 在 beside 和 above 操作下是封閉的,也就是說 beside 和 above 操作的結果同樣也是 Component,並完全可以作為基本的 Component 來再次進行 beside 和 above 組合。這樣我們就可以使用這兩個簡單的操作生成更加復雜的 Component 來,從而完成復雜的界面布局。比如,我們可以這樣來進行描述:
Component L = beside(TextField (), Button().title("…"), 0.8);
above(L, Button().title("ok"), 0.5). at(0,0,300,60).in(C)
其執行結果如下圖所示:
圖 4. 復雜的界面布局
為了保證界面布局語言的完備性,我們增加了一種特殊的原子元素:Empty。它的作用只是占據一定的布局空間。比如,如果我們希望在一個布局空間中右半邊放置一個 Button,左半邊空置,就可以這些描述:
beside(Empty(), Button(), 0.5).at(0,0,200,40).in(C)
其執行結果如下圖所示:
圖 5. Empty 原子元素示例
讀者在後面可以看到,正是這個 Empty 以及 beside 和 above 操作的閉包性質為我們描述任意復雜的布局樣式提供了可能。
在有了這些基礎的布局元素和組合手段後,我們就可以通過組合手段來把一些典型的布局樣式抽象出來。在下一小節中讀者將會看到,布局語言中的 beside 和 above 組合操作其實就是 Java 中的普通方法,因此我們的布局語言中不需要什麼特別的抽象手段。也就是說,我們可以直接使用 Java 中已有的抽象手段。
例如,如果我們希望抽象出這樣一種布局樣式,其中給定一個布局空間和一個布局組件,我們期望該組件能夠按照指定的縱、橫留白比例位於該布局空間的中心地帶。我們可以把該布局樣式抽象出來,並命名它為 center。並可以在更復雜的布局樣式中把 center 當作一個基本語素使用。center 的實現如下:
public Component center(Component cp, float hRatio, float vRatio) {
float s1 = (1-2.0* hRatio)/ (1.0 - hRatio);
float s2 = (1-2.0*vRatio)/ (1.0-vRatio);
Component u = above(Empty(), above(cp, Empty(), s2), vRatio);
return beside(Empty(), beside(u, Empty(), s1), hRatio);
}
當我們想把一個按鈕放置按照在橫向 0.2,縱向 0.1 的留白比例放在布局空間 (0,0,100,30) 中時,我們可以簡單的進行如下描述:
center(Button().title("I am at center."), 0.1,0.1).at(0,0,300,60).in(C)
其執行結果入下圖所示:
圖 6. center 示例
我們還可以構建出 h_seq 和 v_seq 這樣的布局樣式,它們分別為把一組給定的布局元素橫向順序排列和縱向順序排列,其實現如下:
public Component h_seq(Component[] cps) {
int len = cps.length;
if(len == 1) return cps[0];
return beside(cps[0], h_seq(slice(cps, 1, len)), 1.0/len);
}
public Component v_seq(Component[] cps) {
int len = cps.length;
if(len == 1) return cps[0];
return above(cps[0], v_seq(slice(cps, 1, len)), 1.0/len);
}
其中 slice 方法有 3 個參數,一個為布局元素數組,另外兩個為區間的起止位置,該方法把給定布局元素數組中指定起止位置的區間部分作為一個新的布局元素數組返回。這兩個方法的實現都比較簡單直接。下面是兩個應用例子:
Component[] cps = new Component[]
{ Button().title("1"), Button().title("2"), Button().title("3") };
h_seq(cps).at(0,0,300,60).in(C)
v_seq(cps).at(0,0,150,200).in(C)
它們的執行結果入下圖所示:
圖 7. 執行結果
圖 8. 執行結果
在 center、h_seq、v_seq 這些布局樣式的基礎上,我們可以定義出更加高階的樣式來,比如,給定一布局元素序列,我們希望它們在給定的布局空間中按照 N 行、M 列排列。我們稱之為 block,其實現如下:
public Component block (Component[] cps, int N, int M) {
Component[][] fcps = formalize(cps, N, M);
Component[] rows = new Component[fcps.length];
for(int i = 0; i < fcps.length; i++) {
rows[i] = h_seq(fcps[i]);
}
return v_seq(rows);
}
其中 formalize 是一個工具方法,它把一個給定的布局元素數組規范化為 N 行 M 列的形式,如果不足則用 Empty 組件補齊。
如果希望在 block 中,每個元素都可以指定一些橫向和縱向的留白,則可以定義一個 block_with_margin 布局樣式,其實現如下:
public Component block_with_margin(Component[] cps, int N, int M,
float hRatio, float vRatio) {
Component[] ncps = new Component[cps.length];
for(int i=0; i<cps.length; i++) {
ncps[i] = center(cps[i], hRatio, vRatio);
}
return block(ncps, N, M);
}
好了,現在我們來看一個稍微復雜一些的例子,我們將使用前面制作的一些布局樣式構建一個迷你計算器的外觀,如下圖所示:
圖 9. 迷你計算器
對應的描述代碼如下:
Component[] cs = new Component[]{
Button().title("0"),
Button().title("1"),
Button().title("2"),
Button().title("+"),
Button().title("3"),
Button().title("4"),
Button().title("5"),
Button().title("-"),
Button().title("6"),
Button().title("7"),
Button().title("8"),
Button().title("*"),
Button().title("9"),
Button().title("="),
Button().title("%"),
Button().title("/")
};
Component opLayout = block(cs,4,4);
above( above( TextField(),
beside( Button().title("Backspace"),
Button().title("C"),0.5),
0.5),
block(cs,4,4), 0.3).at(0,0,300,200).in(C);
如果我們現在希望將所有數字以及操作按鈕按照橫向和縱向各 2% 進行留白,我們所要做的僅僅是一行的改動,就是把:
Component opLayout = block(cs,4,4);
更改為:
Component opLayout = block_with_margin(cs, 4, 4, 0.02, 0.02);
執行結果如下圖所示:
圖 10. 數字以及操作按鈕按照橫向和縱向各 2% 進行留白
這意味著什麼呢?這意味著我們可以直接使用布局語言進行界面制作,我們可以直接針對布局進行編程,我們所寫出來的界面代碼就是我們的布局規格說明。
從上面的介紹中,讀者可以看出,我們的界面布局語言可以非常方便地定義出一些常見的布局樣式,還可以把這些樣式組合成更為復雜的一些高階布局樣式,並且這種組合是沒有任何限制的。此外,這些布局樣式的定義描述方式是和界面設計者頭腦中所使用的一些布局詞匯和規則貼近的。通過使用界面布局語言,界面設計者完全可以擺脫那些呆板、機械又難以定制和擴展的布局管理器,可以輕松地把頭腦中的布局創意直接描述出來,逐步形成自己的布局樣式庫,充分享受這種創造性的工作所帶來的樂趣。
界面布局語言設計與實現
在本小節中,我們會對上面介紹的界面布局語言的一些設計和實現細節進行介紹。我們這裡所講解的是基於 Java Swing 的實現。讀者可以根據自己的需要在其他的語言和界面開發工具包上去實現該界面布局語言。
界面布局語言的主要設計思路有兩點:
在接口中遵循《Domain Driven Desing》作者 Eric Evans 提出的 FluentInterface 的概念;
語言的層次化設計。
界面布局語言所提供的接口不是 Java 語言層面上的對象接口,也不是使用基於 Java 的語法來使用這些接口構建復雜的界面。相反,我們提供了一個面向界面設計規格描述的接口,接口的語義、規則以及命名完全和界面設計中的規則、概念相符,這樣就可以直接使用代碼來清晰、直接地表達出界面設計中的布局概念。
在界面布局語言的設計上,我們沒有采用定制的面向對象的設計,而是由一組處於不同層次的語言組成,每個層次都是通過對該層的基本原子進行組合構造而來,每個層次所構造出來的實體,則可以作為上一層語言的基本原子使用。這樣,我們就在通用的 Java 語言之上,逐步構建出了一種專用於表達界面布局的語言。比起傳統的對象設計,這種方法具有更高的抽象層次和通用性。
我們來看一下界面布局語言中基本原子的實現細節,先來看一下 Component 的定義:
public interface Component {
public Component at(int x, int y, int width, int height);
public Component in(Container);
……
}
Button 的實現如下:
public class Button implements Component{
public JButton btn = new JButton();
public Component title(String t){
btn.setText(t);
return this;
}
public Component at(int x, int y, int width, int height) {
Rectangle rect = new Rectangle(x,y,width,height);
btn.setBounds(rect);
return this;
}
public Component in(Container parent){
parent.add(btn);
return this;
}
……
}
從上面的代碼中,讀者會發現這種寫法和傳統的 API 寫法風格的不同。在這種風格中,為了能夠將調用形成一個句子,每個調用在結束時都返回了 this。另外,在給方法起名時也有不同的考慮,不只是關注於該方法的職責和功能,而是更關注於該方法名在整個句子這個上下文中是否通順、是否更富表達力。
隨著更多基本原子組件的編寫,會發現 in 和 at 方法在很多組件中都重復出現,此時可以把它們提取到一個抽象基類中。這裡這樣寫是為了清楚起見。
下面我們來看看 Empty 組件,beside 和 above 組合子的實現方法,它們都很簡單。
public class Empty implements Component {
public Component at(int x,int y,int width,int height) {
return this;
}
public Component in(Container {
return this;
}
}
Empty 只是起到了一個布局空間占位的作用。beside 和 above 的實現如下:
public class beside implements Component {
private Component left,right;
private float ratio;
public beside(Component left,Component right,float ratio){
this.left = left;
this.right = right;
this.ratio = ratio;
}
public Component at(int x,int y,int width,int height) {
left.at(x, y, width*ratio,height);
right.at(x+ width*ratio, y, width*(1-ratio),height);
return this;
}
public Component in(Container parent) {
left.in(parent);
right.in (parent);
return this;
}
……
}
public class above implements Component {
private Component up,low;
private float ratio;
public above(Component up, Component low, float ratio){
this.up = up;
this.low = low;
this.ratio = ratio;
}
public Component at(int x,int y,int width,int height) {
up.at(x, y, width,height*ratio);
low.at(x, y+height*ratio, width,height*(1-ratio));
return this;
}
public Component in(Container parent) {
up.in(parent);
low.in (parent);
return this;
}
……
}
為了保證組合操作的閉包性質,這兩個組合子都實現了 Component 接口,並且把組合的結果當作一個 Component 返回。這兩個組合子的主要功能就是把給定的布局空間按照指定的比例進行分隔,並把給定的組件放到分隔好的布局空間中去。其中的算法比較簡單,就不再贅述。
基於這些基本的原子元素和組合子,就可以構建出任意復雜程度的布局樣式。在前面語言介紹小節中,我們給出了一些如:center、h_seq、v_seq、block 以及 block_with_margin 等簡單布局樣式的實現。讀者可以根據自己的需要定義並積累自己的布局樣式庫。
前面提到過,我們的界面布局語言是分層的,大家可以看出,在最底層是我們的 Java Swing 界面開發語言,我們在其上構建出了界面布局位置描述語言,使用該布局位置描述語言中的組合子:beside 和 above,我們在其上又構建出了用來定義和表達各種布局樣式的布局樣式描述語言。這種層次關系如下圖所示:
圖 11. 層次關系
敏銳的讀者會發現,在前面講述的界面布局語言中僅僅涉及了界面布局元素的顯示樣式方面的內容,但是一個完整的界面是需要和後端的應用邏輯交互的,因此還需要一個粘合界面顯示和應用模型的層次。
確實是這樣的,我們在這裡之所以沒有提這項內容主要是為了避免陷入其實現的瑣碎細節中,從而可以集中介紹界面布局語言本身。為了能夠對界面布局元素進行編程控制,我們讓每個布局元素都有一個"擁有者"。和布局元素在物理上的包含關系不同,"擁有者"是編程語義上的。也就是說,對布局元素在編程意義上的所有控制操作都在其"擁有者"中完成,這種思路完全隔離了顯示和控制,其實就是 MVP 模式的一種實現。
比如,我們可以這樣描述一個 Button:
Button().title("button1").ownby(btn1Controller);
關於 Button 的所有事件處理和操控都在 btn1Controller 中完成。有機會的話,我們會在後續的文章中對此進行詳細的介紹,現在我們將其實現作為一個練習留給讀者來完成。
關於設計的幾點思考
在本文中,我們介紹了一種界面布局語言以及它的設計和實現。在此,我們有必要對其中的設計思路進行一個回顧。
在設計中,我們沒有采用對象技術中常用的一些設計手段,我們沒有對界面布局本身進行抽象,也不是設計出一些特定的界面布局管理器。相反,我們把對象技術當成一種低層的抽象工具,並基於它來構建更高層次的抽象,創建出更加接近我們所工作的問題領域的語言,從而獲得更高的生產力、表達力以及可重用性(還有什麼比語言更加易於重用),這就是目前探討的比較熱烈的面向語言編程(Language-Oriented Programming)。
前面已經介紹過,我們的界面布局語言是分層的,這種設計非常有助於構建健壯的程序。這裡健壯的含意是指:問題領域中的一個小的更改,所導致的程序更改也應當是相應地小的。比如,我們在構建迷你計算器時,希望所有數字以及運算符按鈕都在橫向和縱向留一些空白,這個問題領域中的一個小的更改,所對應的程序更改就是把 block 更改為 block_with_margine 而已。此外,由於分層的存在,我們可以自由地修改不同層次的表達細節而對其他層次不會造成任何影響。也就是說,每一層提供了用於表達系統特征的不同詞匯以及不同的更改方式和能力。
由於動態語言提供了更高的動態性和元編程能力,因此在動態語言中更容易實現這種設計思路,我們也用 Python 語言基於 wxPython 界面工具庫實現了本文中講解的界面布局語言,相比 Java,它的實現確實要容易和清晰地多。