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监听,并根据规则进行合适处理即可。