SWT: 深入内幕之消息机制探秘(上篇)

前言
 
作为一个专注于 C/S 方面开发的程序员,我一直对“面向对象的编程框架如何与 Windows 操作系统的消息机制打交道”这个问题有着相当大的兴趣。读者想必知道,象 MFC 、 VCL 和 SWT 这样的类库在实现界面处理的时候,有几个主要问题是不得不考虑的。首先是如何为窗口和控件这样的界面以面向对象方式进行包装——这一方面可以说没多少技术上的难题;从一般意义上讲,不过是把 HWND 作为第一个参数的函数分类整理一下而已。当然,具体作起来还是有不少东西需要认真考虑,只是这些问题多半是在设计的层面,考虑包装是否完善、维护和扩展起来是否方便等等;在实现上基本上就没什么需要克服的技术障碍了。而另一方面——即如何处理系统消息机制,则是一个颇费脑筋的问题了。其中最大的难点之一,就是 Windows 的消息系统依赖于窗口过程(术语叫做 Window Procedure ),而这个窗口过程却是一个非面向对象的、普通的全局函数,它完全不理解对象是什么;而为了让整个程序 OO 起来,你还非得让它去操纵对象不可。因此,如何将窗口过程用面向对象的方法完美的封装起来,就成为各种类库面临的最大挑战之一。当然,这也理所当然的成为各个开发小组展示自身功力的绝好舞台。
 
据我所知,在此一问题上,不同的类库采纳了不同的做法。较早的 MFC 使用了窗口查找表的技术,即为每个窗口和对应的窗口过程建立一个映射;需要处理消息的时候,则是映射表中找到窗口所对应的过程,并调用之。这样会带来几个问题。首先是每次进行查表势必浪费时间,为此 MFC 不惜在关键处使用 Cache 映射和内联汇编的方法以提高效率。第二个问题:映射表是和线程相关联的,如果你将窗口传递给另外一个线程, MFC 无法在该线程中找到窗口的映射项,也就不知该如何是好,于是只能出错。我已经在很多地方看到有人问跨线程传递窗口指针的疑问,多半都是因为不理解 MFC 的消息处理机制。正因为如此, MFC 的使用者必须强制遵守一些调用方面的约定,否则会出现很多莫名其妙的错误,这无疑是框架不够友好的表现。而稍晚出现的 VCL 和 ATL 则使用了一种比较巧妙的 Thunk 技术,利用函数调用过程中使用堆栈的原理,巧妙的将对象指针“暗度陈仓”地偷偷传递进去,并通过一些内存中的“小动作”越过了通常的处理机制。这样做的好处是节省了额外维护映射表的开销,速度相当快,同时也不存在线程传递的问题。当然,这个过程因为大量使用汇编,而且需要对函数调用的底层机制有深刻的理解,所以很难为一般程序员所理解和运用。(相应的维护起来也难度也比较高——还记得 Anders 离开 Borland 以后相当长时间没有人敢改动 Delphi 底层代码的往事吗?)
 
在众多框架中, SWT 算是比较年轻的一个,也是颇为独特的一个。之所以说它特殊,因为它是用 Java 编写的。我们知道,和 Windows 平台上的本地开发工具不同, Java 程序是生活在自己的虚拟机中的,除非通过 JNI 这个后门,否则它对底下的操作系统根本一无所知。这显然为设计者提出了更高的挑战。那么, SWT 又是如何实现这一点的呢?非常幸运, SWT 是完全开放源代码的(当然, MFC 和 VCL 也是开放的,不过这种开放就比较小家子气——许多时候只有你购买昂贵的企业版以后才能看到这些宝贵的源码, D 版且不论)。开放源代码为我们研究其实现扫清了障碍。
 
准备工作
 
在上路之前,我们应当准备好足够的武器。当然, Eclipse 是必不可少的——我使用的是最新的 Eclipse 3.2 RC6 版本,不过只要是 3.x 的版本,在核心代码方面应该不会有很大差别,所以对本文的目的而言, Eclipse 3.0 以上的任何版本都是够用的。此外,如果你还没有安装任何界面开发方面的插件的话,我强烈建议你安装一个 Eclipse.org 官方的 Visual Editor 。这倒不是说我认为该插件对界面开发有多大的助力——事实上从功能上来说它要比 SWT Designer 等同类产品逊色;但是该插件最大的好处在于可以非常简单的设定好 SWT 程序所运行的环境,还包括源代码支持,这样你就可以很轻松的跟踪到 SWT 源代码内部去了。并且这个工具是没有使用限制的,也不需要注册激活,这一点要比 SWT Designer 来得方便。
 
安装 Visual Editor 以后,你可以在创建项目的过程中使用 Java Settings 页面,或者在项目创建以后再选择项目属性,从 Java Build Path 分支下的 Libraries 页面访问同样的界面:
然后按下 Add Library 按钮。如果 Visual Editor 安装正确,这里会多出一个 Standard Widget Toolkit 项。选择它然后 Next 。

默认选中的 IDE Platform 不用变,不过最好也勾选上 Include support for JFace library 。


然后按 Finish 。这样准备工作就完成了。
 
 
 
现在我们可以对 SWT 的源代码着手进行分析了。不过,应当从哪里开始下手呢?答案取决于对消息机制的理解。我们知道,任何 Windows 程序(严格地说,应当是有用户界面的程序,而不包括控制台应用和系统服务程序)都是从 WinMain 开始的;而 WinMain 中最重要的部分则是消息循环,这也是任何 Windows 程序得以持续运行的生命之源,所以有人称之为“消息泵”,就是因为它象心脏一样为应用程序的生命源源不断的输送动力。通常,在用 SDK 编写的程序中会有如下的调用:
while  ( GetMessage( & msg, NULL,  0 0 ) )

{

   TranslateMessage( 
& msg );

   DispatchMessage( 
& msg );
}
 
而 SWT 应用程序,尽管实现方法不同,但是看起来非常相似:
while  (  ! shell.isDisposed() )

{

    
if  (  ! display.readAndDispatch() )

       display.sleep();

}

仅从文字上推断,也很容易猜想:Display.readAndDispatch()方法所作的和SDK程序中Translate/Dispatch两行所作的事情应该是类似的;而sleep方法,则在SDK程序中没有直接的对应物。接下来,我们可以按住Ctrl键然后点击readAndDispatch方法,去探查一下它内部是如何实现的。
public   boolean  readAndDispatch () {

    checkDevice ();

    drawMenuBars ();

    runPopups ();

    
if  (OS.PeekMessage (msg,  0 0 0 , OS.PM_REMOVE)) {

       
if  ( ! filterMessage (msg)) {

           OS.TranslateMessage (msg);

           OS.DispatchMessage (msg);

       }

       runDeferredEvents ();

       
return   true ;

    }

    
return  runMessages  &&  runAsyncMessages ( false );


虽然这里有一些新鲜的东西,不过总体上来说没有太大意外。我们如预想的那样看到了对Translate/DispatchMessage方法的调用,这证明SWT的消息循环和一般的本地程序是没有本质差别的。不过和SDK程序有所不同的是,这里使用了PeekMessage,而非传统SDK程序中所使用的GetMessage。(事实上,现代的大多数UI框架也倾向于采用PeekMessage而非GetMessage,不信的话你可以自己去查查看。)
 
为什么是 PeekMessage 而非 GetMessage 呢?这是因为:除了操作系统通过正常途径发送来的消息以外,应用程序通常还要额外使用一些内部的消息,这些消息需要通过“非常规”的途径进行处理。如果使用 GetMessage 的话,它只有在应用程序消息队列中存在消息的时候才会被唤醒,那些“非常”消息就失去了获得及时处理的机会。例如, SWT 就创建了一些用于线程通信的内部消息,这些消息是 Display.syncExec 和 Display.asyncExec 得以正常运作的基础。上面 filterMessage 和 runDeferredEvents 方法就对此有所涉及。不过因为这些辅助方法和本文的主题没有直接关系,所以我不打算对它们作什么说明;如果你有兴趣的话,可以自己去研究一下这些函数内部究竟做了些什么。
 
接下来我们看看 SWT 消息循环中另外一个意义不明的方法: sleep 。
public   boolean  sleep () {

    checkDevice ();

    
if  (runMessages  &&  getMessageCount ()  !=   0 return   true ;

    
if  (OS.IsWinCE) {

       OS.MsgWaitForMultipleObjectsEx (
0 0 , OS.INFINITE, OS.QS_ALLINPUT, OS.MWMO_INPUTAVAILABLE);

       
return   true ;

    }

    
return  OS.WaitMessage ();

}
中间的代码明显是针对WinCE系统的,可以不去管它。有点意外的是这里出现了WaitMessage,这是一般程序中比较少见的一个函数调用。不过认真想想,原因大概也可以理解。PeekMessage和GetMessage的不同之处在于:如果消息队列中没有消息可抓,那么GetMessage会释放控制权让其他程序运行,而PeekMessage却不会。即使是在抢占式多任务操作系统中,一个程序总是攥着控制权不放也不是好事。因此,如果真的没有任何消息需要处理,那么WaitMessage将使线程处于睡眠状态,直到下个消息到来才再次唤醒——这也是SWT为什么把该方法定名为sleep的原因。
 
通过上面的研究我们看到:抛开无关的细节,消息循环的处理本身是非常简单的。然而,这些研究尚不足以解决我们的疑惑。最关键的窗口过程究竟是在哪定义的呢?很显然,我们需要追踪窗口的创建过程,来找到定义窗口过程的地方。所以接下来的研究对象就是 Shell 。
 
Shell 类并没有类似 create 这样的方法,因此我们可以合理的猜想:创建窗口的过程大概就放在构造函数中。
 
接下来我们跟踪 Shell 的实现代码来证实此猜想。不过有一点值得先作个说明:你可能已经知道, Shell 对象具有一个很深的继承层次——它的直接父类是 Decoration ,而这个类的父类又是 Canvas , Canvas 的父类是 Composite ,依此类推。你必须知道这个层次的原因是: Shell 创建过程中经常会用到祖先类中的一些方法,同时也会重载祖先类中的部分方法,因此在跟踪代码的时候,你也得根据方法的调用者实际所在的类,在这个类层次中上下移动。 Eclipse 提供的 Hierarchy 视图是个不错的工具,可以让它来帮助你,如下图所示。小心不要迷路!

 
经过一番跟踪,我们有了如下的发现:

l        通常,我们调用的是型如Shell(Display)或者Shell(Display, style)这样的构造函数。这两个构造函数都会调用内部的其他一些形式的构造函数,最终调用如下的形式:

Shell(Display, Shell parent, int style, int handle);
l         上述方法的最后一步调用了createWidget()。这个方法的名字应该让你马上有一种“我找到了”的感觉;
l         Shell本身并没有定义createWidget()方法,实际上它调用的是Decorations.createWidget;
l         Decorations.createWidget其实并没有做什么事,只是简单的调用上级(Canvas)的实现,然后修改一些内部状态。不过,Canvas并没有重载createWidget,因此控制继续向上,来到Scrollable;
l        同样,Scrollable.createWidget也是简单的向上调用。Control类才是完成真正工作的地方。我们可以从代码中看到,这个类作了相当多的工作:
void  createWidget () {

    foreground 
=  background  =  - 1 ;

    checkOrientation (parent);

    createHandle ();

    checkBackground ();

    checkBuffered ();

    register ();

    subclass ();

    setDefaultFont ();

    checkMirrored ();

    checkBorder ();

    
if  ((state  &  PARENT_BACKGROUND)  !=  0 ) {

       setBackground ();

    }

}

有经验的读者从名字应当能够猜到,上面这么多方法中,createHandle才应当是真正值得我们关心的。
void  createHandle () {

    
int  hwndParent  =  widgetParent ();

    handle 
=  OS.CreateWindowEx (

       widgetExtStyle (),

       windowClass (),

       
null ,

       widgetStyle (),

       OS.CW_USEDEFAULT, 
0 , OS.CW_USEDEFAULT,  0 ,

       hwndParent,

       
0 ,

       OS.GetModuleHandle (
null ),

       widgetCreateStruct ());

    ….

}

我没有把完整的代码列出来;因为,既然已经看到了CreateWindowEx,就知道我们想找的东西已经就在眼前,没有必要再找下去了。
 
createWindowEx方法必须指定要创建的窗口类名字,也就是上面代码中windowClass()方法所作的事情。我们接着看看这个类名应当是什么。然而,我们发现windowClass()在Control类中定义为抽象方法:
abstract TCHAR windowClass ();
这意味着实际上类的名字是由具体的子类来指定的。所以我们还要继续跟踪下去。因为继承层次上每个类都能够改写这个方法,所以我们不应该从现在的位置回头向下,而是应当从最底层的Shell开始向上找——这样,你找到的第一个被重载的地方就是最终的实现。
 
Shell的确实现了windowClass()方法,方法如下:
TCHAR windowClass () {

    
if  (OS.IsSP)  return  DialogClass;

    
if  ((style  &  SWT.TOOL)  !=  0 ) {

       
int  trim  =  SWT.TITLE  |  SWT.CLOSE  |  SWT.MIN  |  SWT.MAX  |  SWT.BORDER  |  SWT.RESIZE;

       
if  ((style  &  trim)  ==  0 return  display.windowShadowClass;

    }

    
return  parent  !=  null  ?  DialogClass :  super .windowClass ();

}


因为这里涉及到其他一些变量,所以其意图最初看上去可能不是很明确。总体的逻辑大概是这样的:如果Shell发现用户要创建的是一个对话框,那么将返回Dialog的内部类名。否则,调用上级类的实现(shadowClass则是SWT内部维护的一个需要特殊处理的类)。
 
因为Shell的实现调用了基类,所以我们还是要往上走。Decorations、Canvas、Composite都没有重载windowClass()方法。继续来到Scrollable类中,这个方法具有如下的实现:
TCHAR windowClass () {

    
return  display.windowClass;

}

现在线索转到了Display类。然而,windowClass只是Display类的一个字段,而非方法,这个字段一定是在哪个地方得到了初始化。问题就是:究竟在哪初始化的呢?
 
好在,我们只需要在Display类查找哪里修改了windowClass字段就可以了。很快可以发现如下的方法:
protected  void  init () {

    
super .init ();

       

    
/*  Create the callbacks  */

    windowCallback 
=  new  Callback ( this " windowProc " 4 );  // $NON-NLS-1$

    windowProc 
=  windowCallback.getAddress ();

    
if  (windowProc  ==  0 ) error (SWT.ERROR_NO_MORE_CALLBACKS);

    …

    
/*  Use the character encoding for the default locale  */

    windowClass 
=  new  TCHAR ( 0 , WindowName  +  WindowClassCount,  true );

    windowShadowClass 
=  new  TCHAR ( 0 , WindowShadowName  +  WindowClassCount,  true );

    WindowClassCount
++ ;
上面代码中用到了两个相关字段:windowName是一个实例变量,其值为“SWT_Window”;而windowClassCount则是一个静态变量,没有说明初始值,那么就是默认值0。
 
稍稍分析一下就能明白:当init()方法第一次被调用的时候,windowClass将被设置为字符串“SWT_Window0”(你可以将TCHAR对象视为和字符串等同的东西),然后windowClassCount递增。如果init()方法第二次被调用,那么下一个类名将会是SWT_Window1。不过,通常情况下我们的SWT程序仅有一个Display对象,也仅会初始化一次。也因此,所有顶层窗口的类名都应当是“SWT_Window0”。
 
你可以用SPY++或者Winsight32之类的工具来证实这一点(如下图)。
知道了类名以后怎么办呢?还是要从消息机制的原理上找到线索。而在Windows中将一个窗口类和窗口过程连接起来的关键是:调用RegisterClass或者RegisterClassEx,并将类名和窗口过程的地址作为参数一并传入。所以,下面我们的目标是查找在哪里调用了RegisterClass。
 
因为windowClass是定义在Display类中的,按照就近的原则,我们就从这里找起。果然不出所料,在init()方法接下来的部分就有这样的代码:
/*  Register the SWT window class  */

    
int  hHeap  =  OS.GetProcessHeap ();

    
int  hInstance  =  OS.GetModuleHandle ( null );

    WNDCLASS lpWndClass 
=  new  WNDCLASS ();

    lpWndClass.hInstance 
=  hInstance;

    lpWndClass.lpfnWndProc 
=  windowProc;

    lpWndClass.style 
=  OS.CS_BYTEALIGNWINDOW  |  OS.CS_DBLCLKS;

    lpWndClass.hCursor 
=  OS.LoadCursor ( 0 , OS.IDC_ARROW);

    
int  byteCount  =  windowClass.length ()  *  TCHAR.sizeof;

    lpWndClass.lpszClassName 
=  OS.HeapAlloc (hHeap, OS.HEAP_ZERO_MEMORY, byteCount);

    OS.MoveMemory (lpWndClass.lpszClassName, windowClass, byteCount);

    OS.RegisterClass (lpWndClass);

init() 方法的其他部分还注册了另外一些辅助窗口,比如阴影窗口等;此外还注册了一个全局钩子。这些部分和消息机制的核心没有直接关系,可以不去管它。关键在于这一行:
    lpWndClass.lpfnWndProc  =  windowProc;
 
回头看看,在init()方法的开头部分,windowProc成员是这样初始化的:
     /*  Create the callbacks  */

    windowCallback 
=  new  Callback ( this " windowProc " 4 );  // $NON-NLS-1$

    windowProc 
=  windowCallback.getAddress ();

    
if  (windowProc  ==  0 ) error (SWT.ERROR_NO_MORE_CALLBACKS);

这里出现了一个神秘的类:Callback。有Windows 编程经验的读者大概会回想起,在Windows消息机制中,Callback是一个非常核心的概念。虽然Java程序员或许不熟悉它,不过事实上它可谓是Windows中的“控制反转”或曰“依赖注入”——早在Java和模式大行其道之前很久,Windows中的一些手法已经暗合了最新的编程范式,只是当时没有人给它起一个听上去比较吓人的名字而已。
 
跑题了,回到正文上来。先不看 Callback 的实现,从这段代码我们大概可以猜到:
l         Callback 类就是将 OO 的世界和非 OO 的世界连接起来的桥梁;
l         Callback 的构造函数中,提供了处理消息的目标对象和处理消息的方法名称。最后那个参数 4 你不妨先猜猜看是什么意思;
l         Callback的getAddress()返回的应该是一个地址,也就是——你应当猜到了——正是回调函数的地址;
l         Callback背后一定有某种魔法,把传入的对象方法和getAddress返回的回调函数巧妙的连接起来。
 
接下来,我们要进行的是这个历程中最艰苦的部分:揭示Callback类背后的神秘魔法。
 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是使用 Eclipse SWT 实现在两个树之间绘制连线的示例代码: ```java import org.eclipse.swt.SWT; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; public class TreeLinkExample { private static TreeItem sourceItem = null; private static TreeItem targetItem = null; public static void main(String[] args) { Display display = new Display(); Shell shell = new Shell(display); shell.setLayout(new FillLayout()); Tree tree1 = new Tree(shell, SWT.BORDER); for (int i = 0; i < 5; i++) { TreeItem item = new TreeItem(tree1, SWT.NONE); item.setText("Item " + i); } Tree tree2 = new Tree(shell, SWT.BORDER); for (int i = 0; i < 5; i++) { TreeItem item = new TreeItem(tree2, SWT.NONE); item.setText("Item " + i); } Canvas canvas = new Canvas(shell, SWT.NONE); canvas.addPaintListener(new PaintListener() { @Override public void paintControl(PaintEvent e) { if (sourceItem != null && targetItem != null) { GC gc = e.gc; Rectangle sourceBounds = sourceItem.getBounds(); Rectangle targetBounds = targetItem.getBounds(); Point sourceCenter = new Point(sourceBounds.x + sourceBounds.width / 2, sourceBounds.y + sourceBounds.height / 2); Point targetCenter = new Point(targetBounds.x + targetBounds.width / 2, targetBounds.y + targetBounds.height / 2); gc.setLineWidth(2); gc.setForeground(new Color(e.display, 0, 0, 255)); gc.drawLine(sourceCenter.x, sourceCenter.y, targetCenter.x, targetCenter.y); } } }); tree1.addListener(SWT.Selection, event -> { sourceItem = (TreeItem) event.item; redrawCanvas(canvas); }); tree2.addListener(SWT.Selection, event -> { targetItem = (TreeItem) event.item; redrawCanvas(canvas); }); shell.setSize(400, 300); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) { display.sleep(); } } display.dispose(); } private static void redrawCanvas(Canvas canvas) { canvas.redraw(); canvas.update(); } } ``` 这个示例程序创建了两个树控件和一个 `Canvas` 控件,并在树控件上添加了 `Selection` 监听器,在监听器中记录了选中的项。同时,在 `Canvas` 上添加了 `PaintListener`,在监听器中根据选中的两个树项绘制了一条连接线。当有树项被选中时,先记录下来,然后调用 `redrawCanvas` 方法重绘 `Canvas` 控件。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值