论坛首页 编程语言技术论坛

Java dnd拖拽实现分析纪要

浏览 3099 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2008-11-28   最后修改:2009-10-25
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,即可以定制新的逻辑。如果需要更深层次的定制,则需要细致考虑上述分析,选择合适的定制点。
JDK6中transferhandler几个关键方法的回调序列在此列一下:
getSourceActions(JComponent) — 当拖拽一开始时就会被调用,此后不再回调
createTransferable(JComponent) — 当拖拽一开始时就会被调用,此后不再回调
exportDone(JComponent, Transferable, int) — 当拖拽一结束就会被调用,不管目的组件有没有接受。
canImport(TransferHandler.TransferSupport) — 当拖拽的位置处在本组件可见范围内时将不断得到调用。
importData(TransferHandler.TransferSupport)-当拖拽结束在本组件可见范围时被调用一次。
TransferSupport有一个setDropAction(int action),对于目的组件,通过调用一次该方法,则可指示出在将要拖拽结束那一刻确认的action。如果该组件作为拖拽目的,并且在importData里返回了true,则源组件的transferhandler中的exportDone方法将最终得到该指定的action作为一个参数;如果该组件作为拖拽目的,但importData里返回了false,则exportDone方法将得到NONE。
特别注意,在importData方法里setDropAction是不会起作用的,这样做的理由的比较不易理解。
这样处理是因为swing认为从设计语义角度,拖拽结束的那一刻需要同时通知源组件和目的组件进行最后的处理;而就在那一刻,最终准备接受什么类型的dropAction已经得到了语义上的明确;尽管如此,Swing被要求在通知exportDone前应该再做一次处理,即确认拖拽到底有没有被目的组件接受,如果没有的话,这个确定的dropAction参数请swing自动处理为NONE后再传进来。以上的这些语义要求,其实也就限定了如果目的组件和源组件都是同一个AWT环境(比如一个JVM下的application)的swing组件,则源组件的transferhandler.exportData将等着目的组件transferhandler.importData执行结束得到其返回值经swing处理后再执行。
这样的一个隐含的执行顺序很容易让人想到是否可以在importData里setDropAction.但是在importData里再去调用setDropAction是违反语义的,因为dropAction的含义就是在importData执行前的那一刻确定下来的哪个action,而setDropAction这个方法本身的含义是设置在那一刻将会确定是哪个action,而在importData执行的时候那一刻已经过去了。
所以结论是在importData方法里setDropAction应该被禁止,应该在运行时抛出异常,可惜,截至到JDK6,也没有得到这样的支持。这应该算是一个设计BUG.







论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics