2008年10月10日 星期五

Java dnd 拖拽實現分析紀要

既有的Swing組件都內置了拖拽的支持,是怎麼樣支持呢?

首先,在Windows 環境的jvm進程中,一個gui程序將啟動兩個線程:AWT-WINDOWS(AWT)和Event-Dispatch-Thread(EDT)。 AWT-WINDOWS線程不斷從windows操作系統中獲取GUI事件並進行初步的底層處理;其中一些事件會被包裝成高級的AWTEvent置入一個 地方,而EDT線程的處理過程就包括不斷在的適當時機從這個地方獲取這些AWTEvent並進行高級處理。

然後,拖拽的效果就是由以下幾個GUI操作事件及相應程序處理完成的。

1.拖拽開始,拖拽結束。

2.拖拽進入(源組件/目的組件),拖拽經過(源組件/目的組件),拖拽離開(源組件/目的組件),拖拽中鼠標的移動。

3.拖拽操作式樣變換。即隨鍵盤操作,可以指示3種操作式樣:剪切式,複製式,鏈接式。

這 些事件發生後,AWT線程即會獲取到事件通知並處理,底層處理後會包裝交給EDT繼續處理。而處理的程序邏輯一般設計為,對於鼠標圖標,會被設計為隨拖拽 的起始及移動的整個過程中不同事件的發生而發生變化(比如在dragover事件中可能根據當時情況將圖標變為一個叉表示不能拖入);同時,對於拖拽源組 件和目的組件,隨不同的事件通知也會被程序設計為產生不同的變化響應(比如,拖拽結束的事件處理中可能指令目的組件複製源組件的文字內容),從而最終實現 了拖拽的效果。

最後,看一些JRE7中拖拽的實現類。

Swing的JComponent組件將一些功能委託給其成員ComponentUI代理。比如JTextField在構造方法中即會通過 UIManager獲得合適的TextUI實例(UIManager將根據當前look and feel設置獲取)(此後JTextField的paint方法會調用該UI實例的update方法從而完成組件繪畫),並調用該UI實例的 installUI方法(在installUI中則包括給JTextField對象安裝一些監聽器,安裝TransferHandler這個支持拖拽的關 鍵成員,它包含DragHandler,DropHandler兩個內部類,而這兩個內部類是實現拖拽效果的核心邏輯,安裝droptarget)。

Swing將ui類劃分在幾個包中,其中javax.swing.plaf中存放一些接口;javax.swing.plaf.basic中存放對接口的 基本實現,即多種LAF的通用實現;javax.swing.plaf .metal中存放java默認LAF實現;另外還有javax.swing.plaf.multi用來實現多個LAF的綜合效果實 現;javax.swing.plaf.synth用來實現可通過配置xml文件更換顏色等皮膚的實現。

對於拖拽方面,BasicUI在installUI中一般會對組件安裝mouseListener:

editor.addMouseListener(dragListener);

editor.addMouseMotionListener(dragListener);

該 dragListener將監聽發生在組件上的鼠標事件,當發現可能是新啟動的拖拽鼠標動作並且組件dragEnable時,則立刻通知 DragRecognitionSupport單例進行組件拖拽識別。該support單例將辨別鼠標動作本身,確認是組件拖拽開始,再通知組件的 TransferHandler成員對象進行拖拽初始化,即,經其辨別headless環境和action支持後將初始化建立並委託調用 TransferHandler.SwingDragGestureRecognizer的全局單例(成員包括全局單例dragsource及 draghandler對象),該實例註冊拖拽識別listener及設置sourceAction,最終將通知 TransferHandler.DragHandler對象的dragGestureRecognized。在該方法中,將創建 transferable及初始化autoscroll;並通過dragSource全局單例完成創建DragContext,並獲取及初始化 DragContextPeer全局單例(給該單例註冊上該component作為拖拽trigger,以供native code可以在處理底層事件時,可以通過x,y判定是否contains,從而縮小事件處理範圍),並通知DragContextPeer單例拖拽開始, 而DragContextPeer單例則會調用底層native code進行進一步的處理。

此後AWT線程將通過windows- api獲取到系統底層的各種拖拽事件並進行底層處理,處理過程將會隨時引用DragContextPeer單例(處理邏輯包括根據trigger過濾), 並最終通知該單例合適的事件通知。 Peer會將這些事件封裝成合適的DragEvent並提交給EDT處理,提交後將促使AWT線程模擬等待EDT處理完該事件。 EDT的處理邏輯是將事件交給拖拽開始時給組件創建的DragContext處理,而該context對象的處理則會調用其dragHandler成員的 對應方法進行事件處理,以及給dragSource單例相應的監聽通知,最後updateCurrentCursor等,最後EDT返回到 AWT,peer處理返回給native code繼續處理。

當拖拽開始後,鼠標圖標在一個swing組件上游晃時,首先 windows會對其頂層容器(如jwindows)視作拖拽源,經native code過濾後,如果該swing組件是此次拖拽trigger(源),則DragContextPeer能得到dragover的通知,進而進行後續處 理;同時從另一個方面,這個也被視作一個拖拽目的,即AWT線程還會對每次拖拽啟動建立一個DropContextPeer (為將來支持並發),並調用該peer的handle事件方法,該方法會將此底層事件包裝後提交給EDT並促使AWT等待;EDT處理該事件時將 track到該事件發生時的頂層組件,以及事件發生的坐標位置,由頂層容器組件的LightweightDispatcher進行初步處理。這個處理將由 該事件產生新事件,其event source將從container(比如JFRAME)變為精確的jcomponent(比如JTextField),同時對於 eventId=dragover的事件,有可能根據具體情況再增加dragExit,dragEnter兩個事件(比如對jframe窗體是 dragover,但鼠標實際是從一個jtextfield到另外一個jtextfield),這些精確的事件的處理會再次回到 DropContextPeer中得到對應的process。這時的process會處理本身的一些成員數據(當前context,當前 droptarget等),再將事件委託給source jcomponent的droptarget進行處理(如果已安裝),此時的處理將是傳遞給對應組件的drophandler進行處理,同時會通知 droptarget的註冊監聽,最後initializeAutoscrolling等。 drophandler在處理過程中對event可以進行accept或reject,這兩個動作會再去調用dropcontext進行處理,並最終轉到 peer處理成員數據。最後edt返回到AWT,peer handle處理結束返回native code當前drop action值,native code繼續處理。

通過以上的分析,如果需要定制swing組件的拖拽邏輯,一個比較基礎的入口是 transferhandler;因為所有事件的處理都將經過其兩個內部類邏輯處理(DragHandler和DropHandler);而swing包 中的TransferHandler的具體實現,是這兩個內部類的方法都把一些控制劃分給了componet的一些屬性設置或 TransferHandler本身的回調方法,所以只需對組件設置合適的屬性,或繼承並override TransferHandler合適的方法並給此swing組件重新setTransferHandler,即可以定制新的邏輯。如果需要更深層次的定 制,則需要細緻考慮上述分析,選擇合適的定制點。

1 則留言:

Maxisam 提到...

Bravo ! thanks for ur sharing