集成 Windows 本地應用到 Eclipse RCP 程序中
Windows 應用程序非常豐富,而有時我們的 Eclipse RCP 程序所需要的一些功能已經有一些現有的 Windows 本地應用程序的實現,我們希望能夠在我們的 RCP 程序中重用這些功能。一種最簡單的重用方法就是直接在我們 RCP 窗口中嵌入本地應用程序窗口。要使得一個 Windows 本地應用程序能夠在我們的 RCP 程序中運行,我們可以使用 Windows 提供的 reparent 機制。利用這種機制實現窗口嵌入的主要過程是:首先要在我們的程序中啓動要嵌入的 Windows 程序,然後我們設法獲取程序啓動後的主窗口句柄,再將我們RCP程序的窗口設置成 Windows 程序主窗口的父窗口。
由於我們需要啓動 Windows 本地程序並且獲取它的主窗口句柄,這些只能使用 Windows 本地調用來實現,所以我們先用 Windows 本地調用實現相應的功能,然後我們再用 JNI 進行調用。
JNI 的全稱是 Java Native Interface,JNI 標準是 Java 平臺的一部分,它用來將 Java 代碼和其他語言寫的代碼進行交互。下面簡單介紹一下使用 JNI 的步驟:
這裏以 HelloWorld 爲例:
public class HelloWorld { static { System.loadLibrary(“helloworld”); } public native void print(); public static void main(String[] args) { HelloWorld hello = new HelloWorld(); hello.print(); } } |
先編譯這個 java 類: javac HelloWorld.java,然後再生成擴展名爲 .h 的頭文件,java 提供了命令 javah 來生成頭文件:javah –jni HelloWorld,下面的清單顯示了生成的頭文件的內容:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class HelloWorld */ #ifndef _Included_HelloWorld #define _Included_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: HelloWorld * Method: print * Signature: ()V */ JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif |
前面已經生成了 c/c++ 的頭文件,下面要實現頭文件中聲明的函數,具體的實現代碼如下面的清單所示,示例代碼中僅僅是輸出一行文字“HelloWorld”:
#include "HelloWorld.h" #include <stdio.h> JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv * env, jobject obj) { printf("Hello World"); } |
接下來要做的就是將這個 c++ 的代碼編譯成動態庫文件,在 HelloWorld.cpp 文件目錄下面,使用 VC 的編譯器 cl 命令來編譯:
cl -I%java_home%\include -I%java_home%\include\win32 -LD HelloWorld.cpp –Fehelloworld.dll
注意:生成的 dll 文件名在選項 -Fe 後面配置,這裏是 helloworld.dll,因爲前面我們在 HelloWorld.java 文件中 loadLibary 的時候使用的名字是 helloworld。所以要保證這裏的名字和前面 load 的名字一致。另外需要將 -I%java_home%\include -I%java_home%\include\win32 參數加上,因爲在第四步裏面編寫本地方法的時候引入了 jni.h 文件,所以在這裏需要加入這些頭文件的路徑。
完成了這些步驟之後就可以運行這個程序:java HelloWorld,運行的結果就是在控制檯輸出字符串“HelloWorld”。
前面部分介紹瞭如何使用 JNI,接下來介紹如何通過 JNI 啓動一個 Windows 的本地應用程序並且將其主窗口設置爲指定窗口的子窗口。首先創建一個 Java 類,如下面的清單所示:
public class ReparentUtil { static{ System.loadLibrary("reparent"); } public static native int startAndReparent(int parentWnd, String command,String wndClass); } |
其中 System.loadLibrary("reparent") 是用來加載名爲 reparent 的動態庫,我們會在這個動態庫中具體實現方法 startAndReparent(…)。
startAndReparent 定義方法來啓動 Windows 程序,並且將其窗口 reparent 到我們指定的窗口。其中:
- int parentWnd: 父窗口句柄
- String command:Windows 程序啓動命令
- String wndClass:Windows 程序主窗口類型
由於有的程序啓動後會創建多個頂級窗口,所以我們在這裏要指定一個主窗口類型來區分不同的頂級窗口。這個方法是一個本地方法,我們會用 C++ 生成爲一個叫 reparent.dll 的動態庫,這個方法即存在於這個動態庫中。
這個 Java 函數對應的的 C++ 函數是 Java_com_reparent_ReparentUtil_startAndReparent(JNIEnv *env, jclass classobj, jint parent, jstring command, jstring wndClass), 這個函數主要實現兩部分的功能:
- 啓動 Windows 應用程序;
- 獲取 Windows 應用程序的主窗口句柄;
- 將 Windows 應用主窗口設置成指定窗口的子窗口。
下面我們來看看啓動 Windows 應用程序的實現. 我們先將函數傳入的 Java 字符串參數轉化成 C 字符串。這個過程主要通過 GetStringChars() 來實現。
JNIEXPORT jint JNICALL Java_com_reparent_ReparentUtil_startAndReparent (JNIEnv *env, jclass classobj, jint parent, jstring command, jstring wndClass){ jboolean isCopy=FALSE; PROCESS_INFORMATION pInfo; STARTUPINFO sInfo; int hParentWnd; jsize len = ( *env ).GetStringLength(command); const jchar *commandstr = (*env).GetStringChars(command,&isCopy); const jchar *wndClassStr = NULL; char commandcstr[200]; int size = 0; size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)commandstr, len, commandcstr,(len*2+1), NULL, NULL ); (*env).ReleaseStringChars(command, commandstr); if(size==0){ return 0; } commandcstr[size] = 0; if(wndClass!=NULL){ wndClassStr = (*env).GetStringChars(wndClass,&isCopy); if(wndClassStr!=NULL){ len = (*env).GetStringLength(wndClass); size = WideCharToMultiByte( CP_ACP, 0, (LPCWSTR)wndClassStr, len, wndClassName,(len*2+1), NULL, NULL ); wndClassName[size] = 0; (*env).ReleaseStringChars(wndClass, wndClassStr); } } |
接着,我們使用 Windows 的 API:CreateProcess 函數來啓動我們要集成的應用程序。
sInfo.cb = sizeof(STARTUPINFO); sInfo.lpReserved = NULL; sInfo.lpReserved2 = NULL; sInfo.cbReserved2 = 0; sInfo.lpDesktop = NULL; sInfo.lpTitle = NULL; sInfo.dwFlags = 0; sInfo.dwX = 0; sInfo.dwY = 0; sInfo.dwFillAttribute = 0; sInfo.wShowWindow = SW_HIDE; if(!CreateProcess(NULL,commandcstr,NULL,NULL, TRUE,0,NULL,NULL,&sInfo,&pInfo)) { printf("ERROR: Cannot launch child process\n"); release(); return 0; } |
CreateProcess 函數的定義是:
BOOL CreateProcess ( LPCTSTR lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpProcessAttributes。 LPSECURITY_ATTRIBUTES lpThreadAttributes, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcessInformation ); |
其中 lpApplicationName:指向一個 NULL 結尾的、用來指定可執行模塊的字符串。lpCommandLine:指向一個 NULL 結尾的、用來指定要運行的命令行。lpProcessAttributes: 指向一個 SECURITY_ATTRIBUTES 結構體,這個結構體決定是否返回的句柄可以被子進程繼承。lpThreadAttributes: 指向一個 SECURITY_ATTRIBUTES 結構體,這個結構體決定是否返回的句柄可以被子進程繼承。bInheritHandles:指示新進程是否從調用進程處繼承了句柄。dwCreationFlags:指定附加的、用來控制優先類和進程的創建的標誌。lpEnvironment:指向一個新進程的環境塊。lpCurrentDirectory:指向一個以 NULL 結尾的字符串,這個字符串用來指定子進程的工作路徑。lpStartupInfo:指向一個用於決定新進程的主窗體如何顯示的 STARTUPINFO 結構體。lpProcessInformation:指向一個用來接收新進程的識別信息的 PROCESS_INFORMATION 結構體。
爲了獲取啓動後的程序的主窗口句柄,在調用 CreateProcess() 之前,我們需要使用一個 Windows 的系統鉤子來截獲窗口創建的事件:
hHook = SetWindowsHookEx(WH_SHELL, ShellProc,(HINSTANCE)hDllHandle,NULL); |
這裏,我們使用的鉤子類型是 WH_SHELL。這種鉤子可以截獲所有頂級窗口創建或者激活的事件。函數的第二個參數是事件處理函數。我們的處理函數叫 ShellProc。我們之後會介紹。
啓動應用程序之後,我們需要獲取應用程序的主窗口之後才能繼續運行。這裏需要實現進程間的同步。在我們的主進程中,我們需要等待,當應用程序的主窗口創建之後,我們發一個消息,通知我們的主進程繼續執行。
我們這裏使用 Windows 的 Event 來實現同步。我們首先調用 CreateEvent 來創建一個事件,然後調用 WaitForSingleObject()等待事件的狀態改變。在我們的 ShellProc 處理函數中,我們一旦獲取應用程序主窗口句柄,我們會改變事件的狀態以通知主進程繼續執行。
以下是創建事件的代碼,我們創建了一個名爲 Global\WaitWindowCreatedEvent 的事件:
SECURITY_ATTRIBUTES secuAtt; secuAtt.bInheritHandle = TRUE; secuAtt.lpSecurityDescriptor = NULL; secuAtt.nLength = sizeof(SECURITY_ATTRIBUTES); hEvent = CreateEvent(&secuAtt,FALSE,FALSE,TEXT("Global\WaitWindowCreatedEvent")); |
等待事件狀態變化可以調用以下代碼:
WaitForSingleObject(hEvent,1000*60); |
爲了避免無限的等待下去,我們設置了一個最長的等待時間,爲60秒。
下面我們再來看 ShellProc 的處理代碼。這個函數中,我們主要是要獲取應用程序的主窗口。根據 Windows 系統 WH_SHELL 鉤子的定義,鉤子的處理函數的第一個參數是事件類型,第二個參數是窗口句柄。我們首先判斷窗口的類型是否是 HSHELL_WINDOWCREATED,然後判斷對應窗口所屬的進程號是否等於我們所啓動的應用程序,如果需要還要判斷窗口類型。一旦我們找到了應用程序主窗口,我們通過調用 SetEvent 來通知主進程繼續執行。
LRESULT CALLBACK ShellProc(int nCode,WPARAM wParam,LPARAM lParam){ if(nCode==HSHELL_WINDOWCREATED && childInstanceId!=0){ HWND hwnd=HWND(wParam); DWORD pid; HANDLE childEvent; char classname[100]; GetWindowThreadProcessId(hwnd,&pid); if(pid==childInstanceId){ if(wndClassName[0]!=0){ int count = GetClassName(hwnd,classname,100); classname[count] = 0; if(strcmp(classname,wndClassName)!=0){ return CallNextHookEx(hHook, nCode, wParam, lParam); } } hChildWnd = hwnd; ShowWindow(hChildWnd,SW_HIDE); childEvent = OpenEvent(EVENT_ALL_ACCESS, TRUE,TEXT("Global\WaitWindowCreatedEvent")); if(childEvent!=0){ SetEvent(childEvent); } } } return CallNextHookEx(hHook, nCode, wParam, lParam); } |
獲取應用程序的主窗口句柄之後,在 Java_com_reparent_ReparentUtil_startAndReparent 函數的最後,我們通過調用 Windows 的 SetParent 函數將其設置成我們的子窗口,同時調整一下應用程序窗口的大小以使其能剛好顯示在我們的窗口中。爲了避免窗口的閃爍,我們先將窗口隱藏,reparent 之後再顯示。爲了去掉應用程序的窗口欄,我們需要將應用程序的窗口類型改爲 WS_POPUP。
if(hChildWnd!=0){ RECT rect; GetWindowRect((HWND)hParentWnd,&rect); ShowWindow(hChildWnd,SW_HIDE); SetParent(hChildWnd,(HWND)hParentWnd); SetWindowPos(hChildWnd,(HWND)0,0,0, rect.right-rect.left,rect.bottom-rect.top, SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS | SWP_SHOWWINDOW | SWP_NOSENDCHANGING | SWP_DEFERERASE); SetWindowLong(hChildWnd,GWL_STYLE,WS_POPUP); ShowWindow(hChildWnd,SW_SHOW); } |
實現了 startAndReparent 方法後,只要將我們 SWT 窗口句柄傳入,我們就可以將一個 Windows 本地應用嵌到我們的 SWT 窗口中了。爲了方便使用,我們可以將 Windows 本地應用包裝到一個 SWT Control 中,這樣我們就可以象使用普通 SWT Control 一樣使用 Windows 應用程序的窗口。下面我們來看如何實現對 Windows 應用程序窗口的包裝。
首先我們定義一個 Control,它從 Canvas 繼承而來。我們用它來作爲本地應用程序窗口的父窗口,同時實現對它的管理。我們主要要實現以下幾個方面的管理:
- 窗口的創建:當我們 SWT 窗口創建時,我們需要將本地應用程序窗口創建出來
- 窗口的銷燬:當我們 SWT 窗口銷燬時,我們也要將本地應用程序窗口銷燬。
- 焦點控制:當我們的 SWT 窗口獲取到焦點時,我們要將焦點設置到本地應用程序窗口中。
- 窗口大小的變化:當我們的 SWT 窗口的位置或大小發生變化時,我們要通知本地應用程序窗口改變它的位置或大小。
首先我們來看窗口的創建和銷燬。我們需要監聽 SWT 窗口的 Paint 事件和 Dispose 事件,在響應 Paint 事件中創建本地應用程序窗口,在響應 Dispose 事件中關閉本地應用程序窗口。需要注意的是,我們創建本地應用窗口可能需要花較長的時間,爲了避免阻塞 UI 線程,我們將其放在一個線程中執行。如下面的清單所示:
public class NativeControl extends Canvas{ private int childWnd = 0; private String startCommand = null; private String wndClassName = null; private boolean isCreatingNative = false; public NativeControl(Composite parent, int style) { super(parent, style); this.addPaintListener(new PaintListener(){ public void paintControl(PaintEvent arg0) { this.addPaintListener(new PaintListener(){ public void paintControl(PaintEvent arg0) { if(childWnd==0 && !isCreatingNative){ isCreatingNative = true; Thread thread = new Thread(){ public void run(){ childWnd = ReparentUtil.startAndReparent( NativeControl.this.handle,startCommand,wndClassName); } }; thread.start(); } } }); } }); this.addDisposeListener(new DisposeListener(){ public void widgetDisposed(DisposeEvent arg0) { if(childWnd!=0){ OS.SendMessage(childWnd, OS.WM_CLOSE, 0, 0); } } }); |
在 paintControl(PaintEvent arg0) 函數中調用 ReparentUtil.startAndReparent(NativeControl.this.handle,startCommand,wndClassName) 來啓動 Windows 應用程序並將應用程序窗口顯示到 SWT 控件中。當 SWT 空間銷燬的時候也要將 Windows 應用程序的窗口銷燬。SWT 的 OS 類提供了 SendMessage 方法來實現將窗口銷燬:OS.SendMessage(childWnd, OS.WM_CLOSE, 0, 0);childWnd 就是要銷燬的窗口的句柄。
窗口焦點的控制和窗口的銷燬比較類似,我們先監聽父窗口的焦點事件,一旦獲取焦點,我們將焦點設置到本地應用程序的窗口中。同時,我們需要加一個鍵盤事件監聽器,這樣當用戶按“Tab”鍵時,焦點才能跳轉到我們的父窗口控件。如下面的清單所示:
this.addFocusListener(new FocusListener(){ public void focusGained(FocusEvent arg0) { if(childWnd!=0){ OS.SetForegroundWindow(childWnd); } } public void focusLost(FocusEvent arg0) { } }); this.addKeyListener(new KeyListener(){ public void keyPressed(KeyEvent arg0) { } public void keyReleased(KeyEvent arg0) { } }); |
SWT 的 OS 類提供了 SetForegroundWindow 函數來將焦點設置到某個窗口上,函數的參數指定要設置焦點的窗口句柄。
窗口的大小的控制也是類似的。我們需要監聽父窗口的窗口事件,一旦有窗口大小變化,我們就調整本地應用程序的窗口大小。
this.addControlListener(new ControlListener(){ public void controlMoved(ControlEvent arg0) { } public void controlResized(ControlEvent arg0) { if(childWnd!=0){ Rectangle rect = ((Composite)(arg0.widget)).getClientArea(); OS.SetWindowPos(childWnd, 0, rect.x, rect.y, rect.width, rect.height, OS.SWP_NOZORDER| OS.SWP_NOACTIVATE | OS.SWP_ASYNCWINDOWPOS); } } }); |
同樣的我們利用 SWT 提供的函數來設置窗口的大小和位置,SetWindowPos 的參數分別是要設置的窗口句柄以及窗口位置大小。
最後我們需要添加一些方法,讓用戶可以設置啓動應用程序的命令以及應用程序的窗口類型。
public void setStartParameters(String startCommand,String wndClassName){ this.startCommand = startCommand; this.wndClassName = wndClassName; } public String getStartCommand() { return startCommand; } public void setStartCommand(String startCommand) { this.startCommand = startCommand; } public String getWndClassName() { return wndClassName; } public void setWndClassName(String wndClassName) { this.wndClassName = wndClassName; } |
這樣我們就開發了一個 SWT 的控件,它可以將指定的 Windows 本地應用程序啓動並將程序的窗口嵌入到控件中。對這個控件的使用和普通 SWT 的控件一樣,唯一的區別就是要在窗口顯示前調用 setStartParameters() 方法設置 Windows 本地應用程序的啓動命令和窗口的類型。
下面是一個簡單的例子,把 Windows Messager 嵌入到了我們的 SWT 的窗口中。
public class ReparentTest { /** * @param args */ public static void main(String[] args) { Display display = new Display(); Shell shell = new Shell(display); shell.setText("Test dialog"); GridLayout layout = new GridLayout(); layout.numColumns = 1; shell.setLayout(layout); Button button = new Button(shell,SWT.None); button.setLayoutData(new GridData()); button.setText("Test"); NativeControl control = new NativeControl(shell,SWT.NONE); GridData data = new GridData(GridData.FILL_BOTH); data.widthHint = 200; data.heightHint = 200; data.grabExcessHorizontalSpace = true; data.grabExcessVerticalSpace = true; control.setLayoutData(data); control.setStartParameters ("C:\\Program Files\\Messenger\\Msmsgs.exe","MSBLClass"); shell.open(); while(!shell.isDisposed()){ if(!display.readAndDispatch()){ display.sleep(); } } } } |
通過 setStartParameters() 方法來設置要啓動的程序的路徑以及該程序的窗口類型,在這裏我們啓動 MSN,對應的窗口類型是 MSBLClass:
control.setStartParameters("C:\\Program Files\\Messenger\\Msmsgs.exe","MSBLClass"); |
以下是代碼顯示的結果。我們可以拉伸改變窗口的大小,這時裏面的 Messager 的窗口大小也會隨之而變化。當焦點在 Test 按鈕上時,按“Tab”鍵,焦點也會跳轉到 Messager 的窗口上。
本文介紹了將一個本地應用程序窗口集成到 Eclipse RCP 窗口中的相關技術。文中主要討論的集成第三方的應用程序,由於我們不掌握第三方應用程序的代碼,這種集成方式還是比較簡單。例如本地應用程序的菜單還是顯示在我們的SWT父窗口中,而不是顯示在 Eclipse RCP 應用程序的主菜單中。有時,我們也需要將我們自己開發本地應用程序集成到 Eclipse RCP 程序中。其實現原理也和本文講述的一樣。不同的是,我們可以實現更多的對我們本地應用程序的控制,從而實現更緊密的集成。例如,我們的本地應用程序可以提供 API 讓 RCP 程序獲取自己的主菜單,並且將其主菜單顯示在 RCP 程序的主菜單中。