問題背景:
我的畢業設計中需要在Windows平台上面跨進程操作窗口。實際上是獲取瀏覽器上面的網頁中的文本框元素,還有windows32窗體上面的編輯框。然后進行自動填值等的操作。
我能想到的一共有這么幾種方法:
-
使用C#編寫窗體應用程序,然后使用WebBrowser瀏覽器控件或者嵌入其他應用程序窗口。如果使用WebBrowser控件,只能強制用戶使用該C#應用程序上網,影響用戶體驗,不切實際。如果使用嵌入其他應用程序窗口的方式,其實就轉化為了跨進程獲取窗口的方法了。
-
使用瀏覽器插件的方式,針對不同瀏覽器編寫不同插件,然后讓用戶安裝。當瀏覽器頁面載入后,使用駐留程序(這是我畢設的核心進程)向瀏覽器發消息,執行瀏覽器插件中的JS代碼操作網頁DOM元素。但是缺點是需要編寫很多插件,且調試起來,真正執行起來很艱難。
-
先使用遠程線程注入到目標進程的線程空間,創建一個虛擬線程,然后執行這個虛擬線程,向擁有這個窗口的界面線程發送消息。實際上這個方法和上面的方法大同小異。只不過進程注入行為會被用戶系統的安全機制檢測到,類似360安全衛士這種神經質的安全軟件會讓用戶把我們的程序查殺掉。另外需要針對各種瀏覽器,各種程序窗體做特定的分析處理,代價太大,而我只不過是完成一個畢設,沒必要用牛刀吧。
-
使用模擬用戶操作方式。先拿簡單的方法說,很多腳本語言例如在Windows上面的VBS腳本執行時會啟動WScript駐留進程,使用VBS的
sendKey
命令可以模擬用戶的輸入,甚至VBS能模擬用戶鼠標的點擊。還可以使用Python,JS(需要先讓用戶下載python)等都可以。他們的核心其實都是調用Windows系統API來完成功能,從結構上來看都是要運行一個本地即時解釋器,它可以調用WindowsAPI,然后解釋腳本執行操作。再說深層次一點就是先獲取目標窗口的句柄,然后對該窗體的消息處理隊列發送WM_SET_TEXT,WM_GET_TEXT,WM_EXIT等各種消息。 -
本文考慮到畢設需要具有跨平台的特性,並且最好能夠兼容各種不同版本的Windows。因此使用Java語言的JNA包提供的方便的功能調用WindowsAPI。而是用JNI也可以。只不過還要編寫DLL,編譯再加調試,會浪費很長時間。如果不是針對特定問題,使用成熟的JNA況且會幫助你解決低層調用的各種問題,何樂而不為呢。
摘取一些JNA簡介:
JNA提供Java程序輕松訪問本機共享庫,而不需要編寫任何Java代碼 - 不需要JNI或本機代碼。這個功能與Windows的Platform / Invoke和Python的ctypes類似。
JNA允許您使用Java的方法調用來直接調用本機函數。調用看起來就像本機代碼中的調用一樣。大多數的方法調用不需要特殊的處理或配置。
JNA使用一個小的JNI庫存根來動態調用本地代碼。開發人員使用Java接口描述目標本機庫中的函數和結構。這使得很容易利用本機平台功能,而不會導致為多個平台配置和構建JNI代碼的高開銷。
因此,JNA提供了相比較性能來說更關注平台適應以及便利性,節省使用者需要面對多版本,多平台開發程序的時間。
除了Windows, JNA還支持多種其他的平台。例如ARM,安卓,Linux等。
JNA可以通過Maven包管理下載。
如果不適用Maven管理包,可以自己下載下面的兩個包放到項目中:
http://repo1.maven.org/maven2/net/java/dev/jna/jna/4.4.0/jna-4.4.0.jar
http://repo1.maven.org/maven2/net/java/dev/jna/jna-platform/4.4.0/jna-platform-4.4.0.jar
這個是必備的參考文檔:
JNA的GitHub地址:
為了示范其簡單性,看下面的代碼。
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.platform.win32.WinUser;
/**
* Created by lenovo on 2017/4/27.
* 使用winID來獲得窗口的類型和標題,然后發送消息或者其他操作
*
*/
public class jnaTest {
public static void main(String[] args) {
HWND hwnd = User32.INSTANCE.FindWindow
(null, "QQ"); // 第一個參數是Windows窗體的窗體類,第二個參數是窗體的標題。不熟悉windows編程的需要先找一些Windows窗體數據結構的知識來看看,還有windows消息循環處理,其他的東西不用看太多。
if (hwnd == null) {
System.out.println("QQ is not running");
}
else{
User32.INSTANCE.ShowWindow(hwnd, 9 ); // SW_RESTORE
User32.INSTANCE.SetForegroundWindow(hwnd); // bring to front
//User32.INSTANCE.GetForegroundWindow() //獲取現在前台窗口
WinDef.RECT qqwin_rect = new WinDef.RECT();
User32.INSTANCE.GetWindowRect(hwnd, qqwin_rect);
int qqwin_width = qqwin_rect.right-qqwin_rect.left;
int qqwin_height = qqwin_rect.bottom-qqwin_rect.top;
User32.INSTANCE.MoveWindow(hwnd, 700, 100, qqwin_width, qqwin_height, true);
for(int i = 700; i > 100; i -=10) {
User32.INSTANCE.MoveWindow(hwnd, i, 100, qqwin_width, qqwin_height, true); // bring to front
try {
Thread.sleep(80);
}catch(Exception e){}
}
//User32.INSTANCE.PostMessage(hwnd, WinUser.WM_CLOSE, null, null); // can be WM_QUIT in some occasio
}
//在Windows中,User32.dll文件擁有大量的操作用戶界面的API。可以看到JNA在包命名上也遵照了DLL的命名規律。
如果我們事先打開QQ程序的登陸界面,當我們運行上面的程序時,就會將QQ登陸窗體置於前台顯示同時將他從屏幕的右邊移動到屏幕的左面。
另外,學過windows編程的都知道,一個windows32程序一般都會有自己獨有的窗體類,即叫做 Window Class,例如 windows下的圖片查看器的主窗口類為"Photo_lightweight_Viewer", 記事本窗口的窗體類叫做"Notepad"。一個窗口類是一個窗體風格,程序中可以定義多個窗體類。當然,WIndows32程序也可以使用其他程序的窗體類。上面的 FindWindow
函數的第一個參數可以傳入一個窗體類名,這樣可以縮小低層JNA調用 FindWindowEX
函數查找的范圍。對於Windows窗體的信息,可以使用 WinID
這個軟件來查詢。VS編程的同學可以使用Spy++工具查看。
下面來解決我上面說的主要問題:
import com.sun.jna.platform.win32.BaseTSD;
import com.sun.jna.platform.win32.User32;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinUser;
/**
* Created by lenovo on 2017/4/27.
* 使用winID來獲得窗口的類型和標題,然后發送消息或者其他操作
*
*/
public class jnaTest {
public static void main(String[] args) {
WinDef.HWND hwnd = User32.INSTANCE.FindWindow
(null, "QQ"); // 第一個參數是Windows窗體的窗體類,第二個參數是窗體的標題。不熟悉windows編程的需要先找一些Windows窗體數據結構的知識來看看,還有windows消息循環處理,其他的東西不用看太多。
if (hwnd == null) {
System.out.println("Excel is not running");
}
else{
User32.INSTANCE.ShowWindow(hwnd, 9 ); // SW_RESTORE
User32.INSTANCE.SetForegroundWindow(hwnd); // bring to front
String username = "yourQQnumber";
for(Character c: username.toCharArray())
sendChar(c);
}
}
static WinUser.INPUT input = new WinUser.INPUT( );
static void sendChar(char ch){
input.type = new WinDef.DWORD( WinUser.INPUT.INPUT_KEYBOARD );
input.input.setType("ki"); // Because setting INPUT_INPUT_KEYBOARD is not enough: https://groups.google.com/d/msg/jna-users/NDBGwC1VZbU/cjYCQ1CjBwAJ
input.input.ki.wScan = new WinDef.WORD( 0 );
input.input.ki.time = new WinDef.DWORD( 0 );
input.input.ki.dwExtraInfo = new BaseTSD.ULONG_PTR( 0 );
// Press
input.input.ki.wVk = new WinDef.WORD( Character.toUpperCase(ch) ); // 0x41
input.input.ki.dwFlags = new WinDef.DWORD( 0 ); // keydown
User32.INSTANCE.SendInput( new WinDef.DWORD( 1 ), ( WinUser.INPUT[] ) input.toArray( 1 ), input.size() );
// Release
input.input.ki.wVk = new WinDef.WORD( Character.toUpperCase(ch) ); // 0x41
input.input.ki.dwFlags = new WinDef.DWORD( 2 ); // keyup
User32.INSTANCE.SendInput( new WinDef.DWORD( 1 ), ( WinUser.INPUT[] ) input.toArray( 1 ), input.size() );
}
}
注意,使用前需要先選定目標焦點。
參考網站:
http://www.rgagnon.com/topics/java-jni.html 這個網站上有幾個JNA的實例,熟悉Windows窗體編程的朋友們看起來應該很容易。
https://github.com/java-native-access/jna#readme
http://www3.ntu.edu.sg/home/ehchua/programming/java/JavaNativeInterface.html 這個是新加坡理工大學的網站,想入門JNI的可以去看看。
http://stackoverflow.com/questions/28538234/sending-a-keyboard-input-with-java-jna-and-sendinput 包含sendkey方法的使用
https://coderanch.com/t/635463/java/JNA-SendInput-function 包含sendkey方法的使用
感謝強大的谷歌