用JavaFX編寫(xiě)用戶(hù)界面控制器
在本文中,我們關(guān)心的是BlueBill Mobile類(lèi),尤其是管理所有Search Species屏幕之后邏輯的控制器;因此本文有助于你了解JavaFX的語(yǔ)言性能。而且我們會(huì)舉出一些實(shí)例來(lái)闡述要介紹的技巧和典型JavaFX結(jié)構(gòu)的陷阱。
筆者想應(yīng)用程序中嵌入了更新的屏播。視頻播放要求使用QucikTime。
這里的概念是在搜索框中鍵入查詢(xún)時(shí),英文函數(shù)或科學(xué)名稱(chēng)函數(shù)會(huì)對(duì)清單過(guò)濾。此外,當(dāng)這些生效的時(shí)候,BlueBill Mobile還可以執(zhí)行自動(dòng)完成輸入。例如,如果在鍵入查詢(xún)的時(shí)候你仔細(xì)查看視頻會(huì)發(fā)現(xiàn)只輸入了"a-r-d-a-c"來(lái)選擇"Ardea Cinerea";或用于"Pied Avocet"的"p-i-e-< space>-a"。BlueBill Mobile 會(huì)自動(dòng)會(huì)剩余部分進(jìn)行補(bǔ)充因?yàn)樵谀承┣闆r下,不存在其他選擇。這是用來(lái)改善移動(dòng)設(shè)備性能的重要功能:你可以以較少的輸入達(dá)到相同目的。
按照MVC模式,就非常有必要在單獨(dú)的控制器中概括這種模式;此外,也很容易對(duì)這種模式進(jìn)行單元測(cè)試。
首先,讓我們看一下代表了分類(lèi)群的模式類(lèi):
- package it.tidalwave.bluebillmfx.taxon.model;
- import java.lang.Comparable;
- public class Taxon extends Comparable
- {
- public-read protected var displayName : String;
- public-read protected var scientificName : String;
- public-read protected var id : String;
- override function compareTo (other : Object)
- {
- return displayName.compareTo((other as Taxon).displayName);
- }
- override function toString()
- {
- return "{displayName} ({scientificName}) ({id})"
- }
- }
- public function displayNameGetter (taxon : Taxon): String
- {
- return taxon.displayName;
- }
- public function scientificNameGetter (taxon : Taxon): String
- {
- return taxon.scientificName;
- }
- public def namePropertyGetters = [displayNameGetter, scientificNameGetter];
類(lèi)托架外面定義的函數(shù)和變量相當(dāng)于Java靜態(tài)分析。
這里我們省略了一些不相關(guān)的實(shí)際項(xiàng)目。基本上,該模式暴露了三個(gè)屬性,其中有意思的兩個(gè)分別是displayName和scientificName。我們也可以定義兩個(gè)函數(shù)來(lái)處理這兩個(gè)問(wèn)題,我們會(huì)把這些函數(shù)放在namePropertyGetters序列中。
- package it.tidalwave.bluebillmfx.taxon.controller;
- import it.tidalwave.bluebillmfx.taxon.model.Taxon;
- public class TaxonSearchController
- {
- public var selectedTaxon = bind if (selectedTaxonIndex < 0) then null else filteredTaxons[selectedTaxonIndex];
- public var selectedTaxonIndex : Integer = -1;
- public var taxons: Taxon[];
- public var filter = "" on replace
- {
- filteredTaxons = taxons[taxon | matches(taxon, filter)];
- update();
- }
- public-read var autoCompleted = "";
- public var filteredTaxons: Taxon[];
- protected function matches (taxon : Taxon, string: String) : Boolean
- {
- if (string == "")
- {
- return true;
- }
- for (propertyGetter in Taxon.namePropertyGetters)
- {
- if (propertyGetter(taxon).toLowerCase().startsWith(filter.toLowerCase()))
- {
- return true;
- }
- }
- return false;
- }
- protected function update(): Void
- {
- def autoCompletedTry = commonLeadingSubstring(filteredTaxons, findMatchingPropertyGetter());
- //
- // Sometimes it can't find a better auto-completion than the current filter, since it searches the displayName
- // and the scientificName at the same time. In this case, we just ignore the new value.
- //
- if (autoCompletedTry.length() > filter.length())
- {
- autoCompleted = autoCompletedTry;
- }
- selectedTaxonIndex = if (sizeof filteredTaxons == 1) then 0 else -1;
- println("selectedTaxonIndex: {selectedTaxonIndex}")
- }
- protected function findMatchingPropertyGetter(): function (:Taxon): String
- {
- for (taxon in filteredTaxons)
- {
- for (propertyGetter in Taxon.namePropertyGetters)
- {
- if (propertyGetter(taxon).toLowerCase().startsWith(filter.toLowerCase()))
- {
- return propertyGetter;
- }
- }
- }
- return null;
- }
- // some stuff later
- }
這個(gè)類(lèi)揭示了以下的屬性:
◆taxons:你需要用完整的鳥(niǎo)類(lèi)列表來(lái)填充
◆filter: 字符串包括需要輸入到搜索欄中的文本
◆filteredTaxons: 種類(lèi)由filter字符串過(guò)濾
◆autoCompleted: 控制器猜測(cè)的自動(dòng)完成輸入字符串
◆selectedTaxon: 如果filter向下細(xì)分種類(lèi),它就會(huì)分配到這個(gè)變量
◆selectedTaxonIndex: -1如果無(wú)法獲取時(shí),selectedTaxon的索引。
最新的四種屬性由客戶(hù)代碼來(lái)綁定,這樣做可以獲取更改提示。#p#
Filter獲取了一個(gè)觸發(fā)事件,也就是變量值更改時(shí)所執(zhí)行的代碼。觸發(fā)器用JavaFX運(yùn)算符 ︳執(zhí)行了過(guò)濾操作:我們可以將觸發(fā)事件的第一行當(dāng)作分配到taxons序列中的filteredTaxons來(lái)讀取,在這一序列中,matches()函數(shù)返回值為true。第二行的代碼調(diào)用了接下來(lái)要介紹的update()函數(shù)。
出于某些原因,這種方法并不一定奏效,因?yàn)閒ilteredTaxons通常會(huì)被整體掃描。有多種方法可用來(lái)加速選擇過(guò)程,但是本文不會(huì)在這一方法真正應(yīng)用到手機(jī)前前作出過(guò)早的優(yōu)化。在筆記本上,它可以加快1000個(gè)項(xiàng)目的速度。
Matches()函數(shù)在所有屬性上執(zhí)行了一次迭代以獲取函數(shù)并檢查看相關(guān)屬性是否以過(guò)濾值啟動(dòng)。
創(chuàng)建獲得屬性值函數(shù)的序列的一大好處是我們可以通過(guò)定義新的函數(shù)輕松添加新的匹配標(biāo)準(zhǔn):例如,其他語(yǔ)言中的本地化名稱(chēng)。控制器可能會(huì)使用在搜索過(guò)程中使用這些名稱(chēng),而我們則不需要再做多余修改。
Update()函數(shù)運(yùn)算出了自動(dòng)完成輸入提示。它會(huì)提取filteredTaxons序列以及用于當(dāng)前選擇的獲取屬性函數(shù),還會(huì)調(diào)用剛剛在字符串屬性的序列中找到了通用子字符串的commonLeadingSubstring()。它不是每次都會(huì)作出很好的自動(dòng)完成輸入猜想,因此有時(shí)建議甚至比當(dāng)前過(guò)濾器還短,而這種情況我們大可忽略不計(jì)。請(qǐng)不要忽視指定臨時(shí)變量的重要性:由于自動(dòng)完成輸入可能被綁定,因此我們不想為其指定一個(gè)會(huì)迅速失效的值。
要明白這一點(diǎn)的重要性,這不僅僅是避免無(wú)用更新,還能避免程序被破壞。在實(shí)際程序中,自動(dòng)完成輸入更改時(shí),TextBox會(huì)更新,因此過(guò)濾器也會(huì)隨之更新:已經(jīng)輸入了"cal"后,再輸入一個(gè)"i",那么TextBox暫時(shí)會(huì)顯示"cali",然后自動(dòng)輸入完成的猜測(cè)失敗,它會(huì)返回一個(gè)"cal",TextBox中的字符串會(huì)變?yōu)?cal":這時(shí)候你要堅(jiān)持自己的想法!綁定確實(shí)很強(qiáng)大,但是它也同時(shí)具有負(fù)面效應(yīng)。
最后一步操作中,代碼會(huì)檢查看我們是否獲取單獨(dú)的已選定鳥(niǎo)類(lèi)。
或許,你對(duì)于自動(dòng)完成輸入失敗的原因仍然感到很困惑。畢竟,我們正在逐步縮小項(xiàng)目列表。因此,如果你已經(jīng)輸入了"cali",那么所有經(jīng)過(guò)過(guò)濾的種類(lèi)會(huì)以"cali"開(kāi)頭,對(duì)嗎?如果你過(guò)濾的是一套單一名稱(chēng),情況就應(yīng)該是這樣;但是我們是同時(shí)對(duì)兩套名稱(chēng)執(zhí)行搜索,那么就會(huì)產(chǎn)生矛盾。看看下例由"cali"過(guò)濾器選取的名稱(chēng)組(英語(yǔ),科學(xué)的):("Calandra Lark", "Melanocorypha calandra"), ("Dunlin", "Calidris alpina"), ("California Quail", "Callipepla californica")
另一個(gè)有意思的地方是findMatchPropertyGetter()。它必須猜測(cè)當(dāng)前過(guò)濾器是否是以"英語(yǔ)"或"科學(xué)"名稱(chēng)運(yùn)行,而且它還會(huì)返回相關(guān)的屬性獲取函數(shù)。基本上,控制器已經(jīng)獲取了matches()函數(shù)中的這一信息,但是我們會(huì)將其移走。可能會(huì)有人思考讓matches()函數(shù)返回一個(gè)以上的布林值,但是這是不可能的,因?yàn)樗怯蛇\(yùn)算符 ︳過(guò)濾序列的時(shí)候使用的:該運(yùn)算符需要一個(gè)布林值。或許我們可以為稍后調(diào)用信息的操作指定一個(gè)成員變量,不過(guò)此時(shí)的代碼應(yīng)該會(huì)更具可讀性。
為了對(duì)文章進(jìn)一步作補(bǔ)充說(shuō)明,這里給大家列出了最后兩個(gè)忽略的函數(shù):
- protected function commonLeadingSubstring (taxons: Taxon[], propertyGetter: function (:Taxon): String): String
- {
- if (sizeof taxons == 0)
- {
- return "";
- }
- if (sizeof taxons == 1)
- {
- return propertyGetter(taxons[0]);
- }
- var common = propertyGetter(taxons[0]);
- for (other in taxons[1..])
- {
- common = commonLeadingSubstring(common, propertyGetter(other));
- if (common == "")
- {
- break; // don't waste time in further iterations, "" it's for sure the final result
- }
- }
- return root;
- }
- function commonLeadingSubstring (string1 : String, string2 : String): String
- {
- return if (string1.length() > string2.length())
- {
- commonLeadingSubstring(string2, string1);
- }
- else if (string1 == "")
- {
- "";
- }
- else if (string2.startsWith(string1))
- {
- string1;
- }
- else
- {
- commonLeadingSubstring(string1.substring(0, string1.length() - 1), string2);
- }
- }
這里的邏輯很簡(jiǎn)單。通常主要的字符串搜索被分解成了臨近字符串對(duì);而對(duì)于單一對(duì)的搜索則有遞歸執(zhí)行。
這里顯示了視圖類(lèi)是如何綁定到控制器的:
- package it.tidalwave.bluebillmfx.taxon.view;
- public class TaxonSearchScreen
- {
- public var taxons : Taxon[];
- var filter = "";
- public-read def controller = TaxonSearchController
- {
- taxons: bind taxons
- filter: bind filter
- }
- def autoCompleted = bind controller.autoCompleted on replace
- {
- if (autoCompleted != "")
- {
- filter = autoCompleted;
- }
- }
- def list = ListBox
- {
- items: bind controller.filteredTaxons
- };
- def searchBox = TextBox
- {
- text: bind filter with inverse
- };
- }
你必須用所有可得的種類(lèi)加載taxon;ListBox會(huì)隨著過(guò)濾的種類(lèi)自動(dòng)更新,TextBox與過(guò)濾器是雙重指令型綁定。之所以需要雙重綁定是因?yàn)橄蛩阉鳈谥休斎霑r(shí),一個(gè)指令用于給控制器發(fā)出新的選擇命令,另一個(gè)則是自動(dòng)完成輸入時(shí)的更新。
【編輯推薦】