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