明輝手游網中心:是一個免費提供流行視頻軟件教程、在線學習分享的學習平臺!

簡單完成Java用戶界面編程

[摘要]Buoy 是一個構建在 Swing 之上的免費用戶界面(UI)工具包,它為 UI 開發(fā)人員提供了方便性和簡單性。在本文中作者用一個簡單的 fractal 用戶界面程序,介紹了 Buoy 可以做什么、為什么這么做。第一次嘗試用 Java 語言構建簡單的用戶界面時,我對 Swing 接口的復雜性感到有...

Buoy 是一個構建在 Swing 之上的免費用戶界面(UI)工具包,它為 UI 開發(fā)人員提供了方便性和簡單性。在本文中作者用一個簡單的 fractal 用戶界面程序,介紹了 Buoy 可以做什么、為什么這么做。

第一次嘗試用 Java 語言構建簡單的用戶界面時,我對 Swing 接口的復雜性感到有些驚訝。老實說,有點想打退堂鼓。最近,一個朋友向我提到,他使用的渲染程序 Art of Illusion(請參閱 參考資料)基于一個不同的工具包:Buoy。推薦它的原因之一是它的界面更友好。當他第一次提到它時,我以為他在談 "BUI",而它與 GUI 這個名字的相似是故意的。在這里 B 代表 better(更好),但是名字 Buoy 并不是縮寫。

Buoy 是免費的。實際上,它是公共的東西。它并沒有在某個開放程度合理的許可下發(fā)布,實際上它根本不受任何許可控制。這意味著在任何用 Java 語言編寫的能夠運行 Buoy 的項目中都可以使用 Buoy,而不用考慮許可問題。因為提供了完整的源代碼,所以這個工具包很容易修改和擴展。本文基于 Buoy 1.3 發(fā)行版,要求讀者對 Swing 有基本的了解,雖然不了解也能對付過去。

示例程序

我曾經嘗試用 Swing 構建的第一個應用程序最后以失敗告終。為了看出工具包之間的對比情況,我決定使用 Buoy 來構建這同一個程序。文章中的代碼示例全部來自該程序的 Buoy 版本。程序生成了一些分形,具體地說,是迭代的分形;舅枷牒芎唵危涸谄矫嫔隙x一系列的線條區(qū)段,從(0,0) 到(1,0),圍繞任意一個單位線條定位。繪制這些區(qū)段之后,繪制同一套變形線條,用這個區(qū)段作為單位向量。做起來比說的更容易,就像在圖 1 中看到的。

圖 1. 分形編輯器中的分形

這個程序的界面相當簡單。它有一些界面小部件,有一個畫布,在畫布上繪制漂亮的圖片,還支持用鼠標操縱圖片。實際上,必須要做的全部工作就是操縱構成原始曲線的點,原始曲線會迭代地繪制出來。界面還有一個最小化的菜單;它可以打開和關閉文件,關閉窗口,或者把當前圖像保存為 PNG 格式的文件。雖然簡單,但是這個界面簡要地提供了一個 Buoy 小部件的合理示例,還有相當數(shù)量對事件處理系統(tǒng)的體驗。

程序實際的核心代碼 —— 分形生成器 —— 已經寫好了,這把這個示例變成一個很好的測試程序。當然,在更新它的過程中,我也發(fā)現(xiàn)并且修補了一些 bug。


[page_break]    發(fā)行包中包含示例程序的源代碼,還有編譯好的類文件和 Buoy 的 JAR 文件(單擊本文頂部或底部的 Code 圖標,下載 factal.tar)。包中還包含一個叫做 frac 的目錄,里面包含一些示例分形。如果使用一臺 UNIX 風格的機器,在路徑中有 Java 編譯器,那么只要運行 make 就能運行它。否則,需要設置 classpath 包含當前路徑和 Buoy 的 JAR 文件所在的目錄,然后運行 FractalViewer 類。在 Windows 系統(tǒng)上,正確的命令行應當是 java -classpath .;Buoy.jar FractalViewer。

  sed -e s/J/B/g

  在第一次深入研究代碼時,也許會形成這樣的印象:把 Swing 代碼轉換成 Buoy 代碼簡單得就像把 UI 元素名稱中的字母 J 換成 B 一樣簡單。例如, FractalViewer 類不再擴展 JFrame;它現(xiàn)在擴展的是 BFrame。主要的小部件名稱也可以照此推測得到。Spinner 和 slider 像以前一樣有相同的名字,只是換了一個字母。 MenuBar(菜單條) 仍然由 Menus(菜單)構成,菜單則容納 MenuItems。

  有些命名轉換略有不同。在 Swing 引用 BorderLayout 的地方,Buoy 有 BorderContainer。一般來說,Buoy 的命名轉換相當統(tǒng)一,雖然不總是與 Swing 的命名一樣。一個明顯的區(qū)別是 Buoy 幾乎組合了容器和布局管理器的概念;每種容器類型都知道自己如何布局。這大大簡化了設計。例如,在分形生成器中使用的 LabelWidget 類是一個 BorderContainer;在 Swing 中,這可能是一個帶有 BorderLayout 布局管理器的 JPanel。

  但是,兩者還是有許多相似之處。這對適應新東西有很大幫助。更重要的是,Buoy 構建在 Swing 之上。這意味著,一般來說,如果需要做的事不能輕松地用 Bouy 完成時,可以把 Buoy 對象傳遞給它包裝的 Swing 對象。對于這種情況,如果想訪問一些沒有 Buoy 對應物的 Swing 對象,可以簡單地把它包裝在 AWTWidget 對象中,這個對象提供了非常薄的包裝器,通過它,不僅 Buoy 自己的小部件,而且所有的小部件都能訪問 Buoy 的小部件 API。例如,如果發(fā)現(xiàn)確實需要 GridBagLayout,可能就需要這樣做。

  例如,F(xiàn)ractalPanel 類是一個 AWTWidget。在早期設計中, 它是 JPanel 的子類, 但實際上我并不需要 JPanel 代碼。相反,我構建了包裝定制類的類 FractalCanvas, 它本身是普通的 Canvas 類的一個子類。把它變成一個 AWTWidget,就可以在它上面利用 Buoy 高效的事件處理機制。

  事件處理代碼非常簡單。在按下鼠標按鈕時,通過 addEventLink() 的魔力,Buoy 發(fā)送一個新的 MousePressedEvent 事件到 mousePressed() 函數(shù)。我忽略了按下哪個按鈕這個問題,只考慮按住 shift 單擊或普通單擊。普通單擊選擇最靠近的點,而按住 shift 單擊則重新把顯示居中。然后,如果鼠標移動,那么每次 Buoy 注意到移動時都會開始發(fā)送 MouseDraggedEvent 事件。在處理這些事件時,F(xiàn)ractalPanel 會生成自己的事件。

  近觀 PointChangedEvent

  為了讓一些討論更加具體,請來看 PointChangedEvent。這是一個試驗性的類,如果不喜歡它,那也只能怪老天了。這個類的想法是:讓一個類來表示狀態(tài)點中的變化。編輯器跟蹤“當前”點 —— 也就是編輯器小部件目前正在編輯的點?梢杂眠@些小部件或在分形面板中單擊選擇新的點,選擇的是最靠近的點。

  我得出這樣一個結論:在代碼中,大概有三類涉及到點的事件需要從一個類發(fā)送到另一個類。

  一個是改變某個點的特征: POINT 事件類型。如果由編輯器發(fā)送,就是告訴分形改變原型線條上的點,并要求重畫線條。如果由分形發(fā)送,則是告訴編輯器剛剛選中的點的特性。

  下一個是選擇某個點?梢园此饕蛭恢眠M行選擇。所以,如果只提供了索引或位置,那么構造函數(shù)會認為意圖是填充其他值。有一點特殊的地方,點索引 -1 用來表示沒有選中的點,所以必須用 -2表示編輯器正在尋找指定位置的點。這可能不漂亮,但是有效。

  有點意思的是 Fractal 類響應 SELECT 事件的方式。如果成功地選擇了一個點,就會發(fā)回一個新的 POINT 類型的 PointChangedEvent 事件,如清單 1 所示。

  清單 1. 用事件回答事件
case PointChangedEvent.SELECT:
if (e.getIndex() >= -1)
selectPoint(e.getIndex());
else
selectPoint(e.getPoint());
// just in case they don't know
event(new FractalChangedEvent(FractalChangedEvent.SIZE, size));
if (selectedPoint >= 0 && selectedPoint < size)
event(new PointChangedEvent(selectedPoint, points[selectedPoint]));
else
event(new PointChangedEvent(selectedPoint, null));
event(new FractalChangedEvent(FractalChangedEvent.REDRAW));
break;

  最后,移動點是一個特殊情況,如果不需要改變點的其他屬性(例如顏色),那么所要處理的就是位置。這就是 MOVE 事件類型。在效果上,它與 POINT 事件類型效果很像,但它不需要事件生成器(通常是 FractalPanel 類)去關心那些它根本不知道的屬性。
[page_break]  INSERT 和 DELETE 事件類型只有部分相關,可能應當屬于 FractalChangedEvent 事件。

  事件處理

  正如已經開始看到的,事件處理是 Buoy 與 Swing 最明顯的不同之處。事件處理提供了大量靈活性。Buoy 本身的事件集相當豐富,且允許您挑選自己感興趣的事件,從任何小部件向其他對象發(fā)送事件。例如,如果想在 Swing 中捕獲鼠標事件,捕獲事件的類需要實現(xiàn) MouseListener 接口。這個接口有 5 個函數(shù)需要實現(xiàn),即使它們就是擺設也必須實現(xiàn)。而且必須使用接口提供的函數(shù)名稱。更糟的是,函數(shù)必須是偵聽器接口的公共部分;要么把這作為公共接口的一部分公開,要么創(chuàng)建一個什么都不做、只是包裝事件偵聽器代碼的內部類。

  在 Buoy 中,每個小部件都是 EventSource 。這意味著可以從每個小部件偵聽事件。什么類型的事件呢?任何類型都可以。關鍵的函數(shù)是 addEventLink()。這允許您指定類、偵聽器以及可選的方法。每當 EventSource 分派這個類或它的子類的事件時,偵聽器都會接收到事件,要么是通過一個叫做 processEvent()的方法,要么是通過在開始調用 addEventLink() 時提供的方法名稱。提供的函數(shù)不能接受參數(shù),也不能接受與指定事件類型兼容的類的對象;父類和接口可以。

  這是一個方便的設置?梢园巡煌氖录酚傻讲煌暮瘮(shù)或相同的函數(shù)。例如,MousePressedEvent 和 MouseReleasedEvent 會被分別處理。在示例程序中,鼠標的按下、釋放和拖動分別有不同的線程,如清單 2 所示。注意,這遠遠超過 Swing 的 MouseListener 所能做的。如果用 Swing 編程的話,就需要實現(xiàn) MouseListener 和 MouseMotionListener 這兩個接口。

  清單2. 只挑感興趣的事件
this.addEventLink(MousePressedEvent.class, this, "mousePressed");
this.addEventLink(MouseReleasedEvent.class, this, "mouseReleased");
this.addEventLink(MouseDraggedEvent.class, this, "mouseDragged");
[...]
public void mouseReleased(WidgetMouseEvent ev) {
lastCenter = null;
dispatchEvent(new FractalChangedEvent(FractalChangedEvent.SLOW));
setAntiAliasing(true);
}

  mouseReleased() 函數(shù)只有最少的工作要做。它只是在 mousePressed() 函數(shù)之后進行清理,告訴 Fractal 對象到了開始全面重繪的時候了。

  Buoy 的事件處理還有另外一個有趣的特性。如果愿意的話,可以創(chuàng)建新的事件類型。一個事件類型就是一個類。確實如此。它甚至不需要繼承任何類或實現(xiàn)什么。它就是一個類。如果這個類的對象被發(fā)送到 dispatchEvent(),那么它或它的父類的偵聽器就會被調用。在 Swing 中也可以創(chuàng)建新的事件類型,但是完全要自己進行;必須設計 Listener 接口,還要編寫自己的代碼生成事件并偵聽事件。在示例程序中,設計了 Fractal 類,演示了可以相對容易地把事件處理功能加到任何原有的類中。只需要聲明一個 FractalViewer 類用來添加偵聽器的事件源 EventSource。FractalViewer 類就會把來自事件源(例如 FractalEditor)的事件鏈接設置到它們的偵聽器,如清單 3 所示。

  清單3. 綁定
private void tieEvents() {
// Set up event handling relations.
addEventLink(WindowResizedEvent.class, this, "layoutChildren");
addEventLink(WindowResizedEvent.class, panel, "repaint");
tieControlEvents();
tieFractalEvents();
tiePanelEvents();
}

  定制事件類一般是為了表示用戶行為。在 Buoy 中,一般只通過用戶行為,而不是系統(tǒng)接口生成事件 —— 除非自己想顯式地調用 dispatchEvent() 自行生成事件。當分形對象以某種會造成字段更新的方式變化的時候,所有部件的控制面板都會得到通知。這樣,我們發(fā)明一個新類 ParameterChangedEvent,用它表示參數(shù)已經變化。或者,如果變化的是選中的點的位置或是索引,就發(fā)送一個新的 PointChangedEvent。如果行為足夠明顯的話,那么事件處理器甚至不需要接受參數(shù)。作為事件處理的一個示例,請看清單 4,它演示了 FractalEditor 的 parameterChanged() 方法的開始部分。

  清單 4. 參數(shù)發(fā)生了變化
void parameterChanged(ParameterChangedEvent ev) {
FractalParameters p = ev.getParams();
int v = ev.getValue();

switch (ev.getType()) {
case ParameterChangedEvent.ALL:
maxSlider.setValue(p.getMaxIterations());
minSlider.setMaximum(p.getMaxIterations());
minSlider.setValue(p.getMinIterations());
maxSlider.setMinimum(p.getMinIterations());
zoomSlider.setValue(p.getZoom());
break;
[...]
  在這個例子中,用事件處理系統(tǒng)把各種信息前后傳遞。在以前的版本中,每個類都有對其他每個類的引用,而且亂七八糟的 get 方法是按天排序的。而在目前的版本中,Buoy 的事件處理系統(tǒng)被用來處理各種通知。例如,F(xiàn)ractalChangedEvent 類可以用來讓代碼的其他部分知道對分形的修改,可能是點的數(shù)量變化(編輯器用點的數(shù)量為點選擇器定義正確的 SpinnerNumberModel),或者是需要重繪的通知
[page_break] 清單 5. 顯然到了重繪的時候
public void fractalChanged(FractalChangedEvent e) {
switch (e.getType()) {
case FractalChangedEvent.REDRAW:
repaint();
break;
}
}
  Buoy 的文檔詳細討論了 Swing 事件模型與 Buoy 事件模型的差異,以及這些差異的原因。有很好的理由,而且 Buoy 的模型通常會導致更小、更清晰的代碼。當然,仍然可以做多余的或愚蠢的事情,就像在任何系統(tǒng)中都可以做的那樣,但是至少在做這些事情的時候有一個干凈漂亮的界面。

  學習曲線

  我曾經觀察到,學習使用一個 GUI 工具,一下午的時間還不夠長。對于 Buoy,我大概需要 6 個小時或者差不多一整個工作日。我確實從更有經驗的 Buoy 用戶那里得到了很棒的幫助。以前學習 Swing 的經驗也是有幫助的,但實際上,我并不認為 Swing 的經驗是必需的。Buoy 的文檔相當好,而它的簡單性確實有幫助。對于基本的 UI 事物,沒有太多要學的東西。

  Buoy 的文檔并不像 Swing 文檔那樣完整,但是覆蓋了許多細節(jié),而且非常好。另外,源代碼也在那兒,所以回答一些關于界面的簡單問題非常容易。具有更完整的文檔當然是好事。但是,既然這個項目放在 SourceForge 上,所以如果您愿意,您可以編寫更多的東西為它做貢獻。

  Buoy 的學習曲線比起 Swing 是一個很大的優(yōu)勢。用相當簡單的界面就能讓大多數(shù)界面小部件正確工作。要使用 Buoy 文檔中的一個示例:在 Swing 中,JList 要求要么使用靜態(tài)列表,要么構建一個實現(xiàn) ListModel 接口的新類。在 Buoy 中,只需向列表中添加項目;在大多數(shù)常見情況下,艱巨的工作已經由 Buoy 替您做了。

  Buoy 相當小。完整的發(fā)行包中包含源代碼、JAR文件和文檔,總共不到 1 MB。代碼的組織良好,可以容易地找到任何特定的代碼段,如果需要調整設計,也不困難。

  Bug

  盡管 Buoy 是一個穩(wěn)定、有用的系統(tǒng),但并不是一個絕對完美的東西。偶爾在明顯選擇很合理的地方它也會有奇怪的表現(xiàn),產生令人驚訝的行為。如果考慮用 Buoy 來完成一個實際的項目,就需要了解 bug:它們的普遍程度、嚴重性,以及克服它們的難度。

  在開發(fā)這個應用程序的過程中,我碰到一些事情,當時看起來像是 bug。但不全是。有一些可能是文檔中的 bug —— 在這些情況中,代碼的行為不是預期的,但是卻非常合理。實際上,我可以非常肯定,從實質上講并不是 Buoy 中的 bug,但它們確實呈現(xiàn)了在調試 Buoy 應用程序時可能會遇到一些事情。在調試了幾天代碼之后,我可以非?隙,我遇到的每個明顯的 bug,要么是我的錯誤,要么是我不太喜歡的底層 Swing 中的設計決策?梢钥隙ǖ卣f,在 Swing 中不可能避免這些問題。

  滾動條刻度

  早期我最常遇到的一個 bug 是處理標尺中的刻度標記的時候。最初,我無法得到在它們上面顯示的標簽。

  清單 6. 讓人郁悶的滾動條代碼
minSlider.setShowLabels(true);
minSlider.setMajorTickSpacing(2);
  清單 6 中的代碼不起作用?梢钥吹,標簽只是在設置了刻度間距后才顯示。如果在告訴滾動條顯示標簽之前沒有設置刻度間距,它不顯示任何刻度就結束了。更微妙的是,隨后也不能改變刻度間距;改變刻度間距的嘗試沒有效果。但是,這實際不是 Buoy 的 bug,而是 Swing 的工作方式。由于 BSlider 類只是把請求傳遞給 JSlider,所以責怪 Buoy 是不公平的。

[page_break]  一個更微妙、也與底層 JSlider 的毛病有關的 bug 發(fā)生在對齊刻度的行為上。 BSlider 的構造函數(shù)把次要刻度設為5,把主刻度設為 20 —— 相對于默認尺寸 100 來說這兩個值是合理的。但是,當用 1-10 的范圍創(chuàng)建滾動條時,卻看不到次要刻度,因此只能把主刻度間距值設為 1。結果產生一個刻度值為 1-10 的滾動條,而且只停留在 1 和 6 處;對齊刻度的行為妨礙了采用其他的值,因為對齊到了次要刻度而不是主刻度。

  雖然這個問題源自 JSlider 的實現(xiàn),但卻在 Buoy 的默認行為中發(fā)生了,在即將發(fā)布的 1.4 發(fā)行版中會修復它。

  注意,這對我是個問題的惟一原因是,示例程序要不斷地更新一些滾動條的范圍。例如,如果有一個線條區(qū)段,只允許進行最多 50 次迭代,那么要在滾動條上標上每個數(shù)字的工作量可能有點多。另一方面,如果只允許少數(shù)迭代,那么遺漏某些數(shù)字看起來就不好了。在一個滾動條范圍不常更新的界面,還是很方便的。

  菜單快捷鍵

  Buoy 用字符或鍵盤事件(KeyEvent)方便地為菜單快捷鍵提供了構造函數(shù)。在第一次測試它時,我沒法讓它工作?雌饋肀仨毷褂眯懽帜福坏谡{用構造函數(shù)時必須用 'W' 代替 'w',如清單 7 所示。

  清單 7. 添加 close 菜單項
mi = new BMenuItem("Close", new Shortcut('W'));
mi.setActionCommand("close");
mi.addEventLink(CommandEvent.class, this, "menuEvent");
fileMenu.add(mi);
  這樣可能必須要處理 Java 5.0 SDK 與 1.42 不小心模糊重復的地方。表面來看,如果把大寫字母傳遞給構造器,所做的事情正與期望的一樣。底層的問題 —— JVM 要用哪一套鍵或修飾符來表示 Ctrl-? —— 還需要一個小的自由的庫才能完全解決。

  文件選擇

  出于一些不明顯的原因,在 Mac 系統(tǒng)上啟動新的文件選擇器時,Buoy 默認啟動的是根目錄。我做了一個詳盡的 bug 報告,是關于它看起來是怎樣遺漏大量文件的,但是隨后我就認識到我已經把我的主目錄從 /Users/seebs 移動到了 /Volumes/Home/seebs,而文件選擇器確實顯示了磁盤上的東西。分數(shù):Buoy 1,Seebs 0。

  我仍然想知道為什么它要從文件系統(tǒng)的根開始。這也許是 JVM 的 Mac 實現(xiàn)的毛病。

  結束語

  Buoy 遵循著名的古老的 UNIX 哲學:百分之十的工作解決百分之九十的問題。Buoy 并不想為所有人解決所有的問題,但是它可以完成界面用戶或設計師需要的大部分工作。它擁有可能是最好的許可條款,而且還在不斷發(fā)展。最好的是,如果發(fā)現(xiàn)它不能讓您做自己確實需要做的事情,您可以隨心所欲地研究它、修改它,要么修改 Buoy 的源代碼,要么調用 getComponent() 并編寫自己的 Swing 代碼。

  如果覺得較大的 UI 工具包太可怕,那么 Buoy 是個不錯的選擇。它可以讓簡單的 UI 繼續(xù)簡單,把復雜的代碼留到需要的時候。在實踐中,對于少數(shù) Swing 比 Buoy 有優(yōu)勢的情況,直接在 Buoy 構建的程序中編寫少數(shù)代碼就能處理。這是一個讓我值得花時間用 Java 進行 UI 編程的工具包。