Java Focus實現(xiàn)紀要一
窗口系統(tǒng)一般包含一個桌面GUI+若干應(yīng)用程序GUI。每個GUI都由組件構(gòu)成,每個組件都可以獲得focus,獲得focus的組件將獲得之后的鍵盤事件,而任意時刻只有一個組件能獲得focus。這個設(shè)計適用在當前所有的窗口系統(tǒng),而跨各種系統(tǒng)的JAVA應(yīng)用,其focus的表現(xiàn)也要遵循這個設(shè)計目標。
JAVA的組件分為重量級和輕量級組件,區(qū)別在于重量級組件實例的成員peer-對等體,其行為緊密依托本地系統(tǒng)的GUI行為函數(shù)庫來進行實現(xiàn)。比如一個JFRAME,當setvisible時,會依托peer.show進行屏幕繪制行為,該行為會通過本地系統(tǒng)GUI行為函數(shù)庫完成;這樣一來,當其被點擊時,本地系統(tǒng)會依據(jù)最初調(diào)用本地GUI函數(shù)繪制時留下的信息,從而能夠經(jīng)底層處理后(比如將該鼠標事件附加peer標記信息,同時可能經(jīng)底層分析需要構(gòu)造出一個可能的focus_gain事件,則在操作系統(tǒng)層面登記當前聚焦GUI組件等)準確將底層GUI事件派送給該JVM進程,該事件因而在jvm進程中的AWT-Windows線程loop獲取到,并通過事件提供的peer標記最終確定目標為重量級組件JFRAME,因此一個source==JFRAME的AWTEvent被構(gòu)造出來并最終分派給EDT進行后續(xù)處理。
事件機制是程序中家喻戶曉的設(shè)計模式了。但是,看java的focus實現(xiàn)中對這個機制似乎多少有些不那么絕對的清晰J。
個人理解,事件的含義就是某種定義的情況發(fā)生了。比如點擊鼠標這個動作可以說觸發(fā)了多個事件,如press,release,click等,分別指發(fā)生了鼠標button1按下,放開,完成點擊的情況。button1按下這個事件比起完成點擊就要更基礎(chǔ)一些,因為完成點擊指的是一個由按下,放開動作序列組合的情況發(fā)生了。
那么對于focus,focus_gained,focus_lost這兩個事件應(yīng)該是指某組件獲得焦點或失去焦點的情況發(fā)生了,反映在機器里,應(yīng)該是某種指向當前聚焦組件的全局變量發(fā)生了更新。
然而在Java awt實現(xiàn)里,概念混亂出現(xiàn)啦。
如果awt_windows loop 到了focus事件,一,這個事件一定是目標向重量級組件的;二,此時,這個事件對于底層系統(tǒng)的對等組件,focus_gainded是發(fā)生了(底層系統(tǒng)標記當前聚焦組件的全局變量已經(jīng)更新;底層操作系統(tǒng)沒有mess,總是在真正focus改變后才分發(fā)focus事件),然而在java層面,截至到awt_windows loop 到底層focus事件并包裝成FocusEvent放置到EVENT QUEUE時,java層面并沒有更新jvm里的全局變量。所以我個人認為這個時候就不應(yīng)該包裝成FocusEvent,至少不應(yīng)該叫這個名字,應(yīng)該叫PrepareFocusEvent,嘿嘿。
澄清事件機制的概念后,回頭看java focus 要實現(xiàn)的目標。
1. 最簡單的設(shè)計思路是提供一個setfocus調(diào)用API,該API來更新一個全局變量。EDT每次處理一個keyevent將根據(jù)當前全局變量進行target。最后給各類組件注冊合適的事件監(jiān)聽,比如mouse press listener,在listen響應(yīng)處理中調(diào)用setfocus。
要提供setfocus指定某組件聚焦。Setfocus一旦成功返回,該組件將接受后繼發(fā)生的所有的鍵盤事件,直到再次失去焦點。
然而問題是輕量級組件的容器是一個重量級組件,而在對輕量級組件調(diào)用setfocus時它的本地對等組件在系統(tǒng)中很可能還沒有獲得焦點。若實現(xiàn)上只是簡單的把java的全局變量更新了,那系統(tǒng)就會出現(xiàn)兩個聚焦組件:一個是底層系統(tǒng)承認的原來的某底層對等組件,一個是java里認為的現(xiàn)在的jtextfield。而本地系統(tǒng)始終把鍵盤事件派發(fā)到它認可的聚焦組件上,如果這個聚焦組件屬于另外一個C++進程,那么這些鍵盤事件就會分發(fā)給C++進程,而不會被JVM的awt-windows loop到。也就是說,雖然setfocus成功返回了,但并不代表隨后的鍵盤事件會target到這個組件上。所以不能采用這樣的設(shè)計思路。
盡管如此,實際上我們的組件的監(jiān)聽一般是在mouse_press上。而這個鼠標按下動作各類底層操作系統(tǒng)處理時一般首先分發(fā)mouse_press底層事件,然后切換焦點,再分發(fā)focus事件。隨后的鍵盤事件會在底層切換焦點后分發(fā)出去。假如我們確定下來所有GUI應(yīng)用只在EDT線程在mouse_press監(jiān)聽處理中setfocus,實際上不會丟失鍵盤事件。但是如果我們要在其他情況,比如某worker 線程中setfocus,那么setfocus就不再可靠了。
那么,根據(jù)前面的分析,現(xiàn)在更改設(shè)計,在setfocus處理中調(diào)用底層API要求其重量級容器對應(yīng)的本地對等組件聚焦并等到它確實聚焦完成了再更新JAVA的全局變量。但這樣也有問題。即使底層系統(tǒng)根據(jù)底層調(diào)用通知更新了focus,馬上還會繼續(xù)對可能的焦點切換操作響應(yīng)(可以認為有一個系統(tǒng)進程在處理外設(shè)的響應(yīng)),很有可能別的C++應(yīng)用就在此時再要求focus,于是接著就更新了底層的focus登記;而我們的setfocus調(diào)用卻是在jvm進程的某線程中,顯然這就是個并發(fā)的情景,這樣,很有可能我們的對本地對等組件的通知發(fā)過去并返回了,那邊底層系統(tǒng)就馬上切換到了C++的某個組件focus,而我們的線程繼續(xù)更新JAVA的全局focus變量,于是雖然setfocus成功返回了,但并不代表隨后的鍵盤事件會target到這個組件上。
現(xiàn)在看來,除非我們同步這兩個進程,讓系統(tǒng)進程等待我們的調(diào)用setfocus的線程返回,顯然那樣是不合理的。(JAVA只能服從OS,不能讓OS服從JAVA。---出自《英雄亂語》J)。
鑒于以上的分析,根本無法實現(xiàn)一個setfocus來完成一個切換焦點的原子性操作。jre1.7的實現(xiàn)為不存在setfocus,而只有requestfocus,意思是只是將這個切換焦點的請求登記上但并不進行實際切換focus;隨后等收到相應(yīng)的事件通知后再處理request并徹底完成一次focus切換。
2. 聚焦組件后馬上獲得隨后的鍵盤事件。
難點是按用戶的實際想法,mouse_press后,馬上就要鍵盤拼寫,鍵盤的輸入應(yīng)該target到mouse_press的jtextfield。根據(jù)前面的分析,mouse_press響應(yīng)中requestfocus/setfocus后并沒有意味著切換焦點已經(jīng)完成。若實現(xiàn)上對于后續(xù)的鍵盤事件只是簡單地根據(jù)JAVA的那個全局focus變量target,則這些鍵盤事件將不會target到期待的組件上。
鑒于以上的分析,jre1.7的實現(xiàn)是requestfocus時,只要這個請求滿足必要條件,那么在其返回前就登記一個時間戳,在這個時間戳之后在下一個requestfocus時間戳之前,EDT 逐個取的keyevent都將target到該組件并登記,直到該組件徹底聚焦完成后,馬上把這些keyevent dispatch。
3. 需要支持TAB鍵等焦點遍歷操作。
這一點JAVA有一個遍歷模型,如下:
具體參照http://java.sun.com/javase/6/docs/api/java/awt/doc-files/FocusSpec.html
該要 求并沒有難點。實現(xiàn)上只要對keyevent監(jiān)聽,并根據(jù)規(guī)則進行合適處理即可。