原文:http://www.ibm.com/developerworks/cn/opensource/os-cn-eclrcp/index.html?ca=drs-cn-0605
Windows 應用程式非常豐富,而有時我們的 Eclipse RCP 程式所需要的一些功能已經有一些現有的 Windows 本地應用程式的實現,我們希望能夠在我們的 RCP 程式中重用這些功能。一種最簡單的重用方法就是直接在我們 RCP 視窗中嵌入本地應用程式視窗。要使得一個 Windows 本地應用程式能夠在我們的 RCP 程式中運行,我們可以使用 Windows 提供的 reparent 機制。利用這種機制實現視窗嵌入的主要過程是:首先要在我們的程式中啟動要嵌入的 Windows 程式,然後我們設法擷取程式啟動後的主視窗控制代碼,再將我們RCP程式的視窗設定成 Windows 程式主視窗的父視窗。
由於我們需要啟動 Windows 本地程式並且擷取它的主視窗控制代碼,這些只能使用 Windows 本地調用來實現,所以我們先用 Windows 本地調用實現相應的功能,然後我們再用 JNI 進行調用。
JNI 簡介
JNI 的全稱是 Java Native Interface,JNI 標準是 Java 平台的一部分,它用來將 Java 代碼和其他語言寫的代碼進行互動。下面簡單介紹一下使用 JNI 的步驟:
編寫帶有 native 聲明的 java 方法
這裡以 HelloWorld 為例:
清單 1. Hello World Java 代碼
public class HelloWorld { static { System.loadLibrary(“helloworld”); }
public native void print();
public static void main(String[] args) { HelloWorld hello = new HelloWorld(); hello.print(); } }
|
編譯 Java 代碼以及產生 c/c++ 標頭檔:
先編譯這個 java 類: javac HelloWorld.java,然後再產生副檔名為 .h 的標頭檔,java 提供了命令 javah 來產生標頭檔:javah –jni HelloWorld,下面的清單顯示了產生的標頭檔的內容:
清單 2. Hello World C++ 標頭檔
/* 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++ 實現本地方法並編譯成動態庫檔案
前面已經產生了 c/c++ 的標頭檔,下面要實現標頭檔中聲明的函數,具體的實現代碼如下面的清單所示,範例程式碼中僅僅是輸出一行文字“HelloWorld”:
清單 3. Hello World C++ 實現代碼
#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”。
實現視窗 Reparent
前面部分介紹了如何使用 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 應用程式
下面我們來看看啟動 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); }
|
將 Windows 應用主視窗設定成指定視窗的子視窗
獲 取應用程式的主視窗控制代碼之後,在 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); }
|
封裝 Windows 應用程式視窗到 SWT 控制項
實 現了 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 的視窗上。
圖 1. 圖片樣本
小結
本 文介紹了將一個本地應用程式視窗整合到 Eclipse RCP 視窗中的相關技術。文中主要討論的整合第三方的應用程式,由於我們不掌握第三方應用程式的代碼,這種整合方式還是比較簡單。例如本地應用程式的菜單還是顯 示在我們的SWT父視窗中,而不是顯示在 Eclipse RCP 應用程式的主菜單中。有時,我們也需要將我們自己開發本地應用程式整合到 Eclipse RCP 程式中。其實現原理也和本文講述的一樣。不同的是,我們可以實現更多的對我們本地應用程式的控制,從而實現更緊密的整合。例如,我們的本地應用程式可以提 供 API 讓 RCP 程式擷取自己的主菜單,並且將其主菜單顯示在 RCP 程式的主菜單中。