【Java】JNIを使ったデスクトップアプリの操作

こんにちは。自動化エンジニアの水谷です。
今日はJNIを使ったデスクトップリの操作についてご紹介します。

JNIとは

Java Native Interface (JNI) は、Javaプラットフォームにおいて、Javaで記述されたプログラムと、他のプログラミング言語(たとえばCやC++など)で書かれた、実際のCPU上で動作するコード(ネイティブコード)とを連携するためのインタフェース仕様です。これを使うことでRacineから操作できないデスクトップアプリやブラウザがポップアップ表示するダイアログボックスウィンドウを操作することができ、うまく使えばRPAなどのツールを使わずにブラウザとデスクトップアプリが連携して動作するようなアプリの自動化の可能性が開けます。

JNIの入手

ビルドにはJNAとJNA-Platformが必要です。gradleの場合、build.gradleのdependenciesに以下を追加してください。

// https://mvnrepository.com/artifact/net.java.dev.jna/jna-platform
compile group: 'net.java.dev.jna', name: 'jna-platform', version: '5.5.0'
// https://mvnrepository.com/artifact/net.java.dev.jna/jna
compile group: 'net.java.dev.jna', name: 'jna', version: '5.5.0'

あるいはjarファイルを以下からダウンロードしてください。
https://mvnrepository.com/artifact/net.java.dev.jna/jna
https://mvnrepository.com/artifact/net.java.dev.jna/jna-platform
注:JNAおよびJNA-Platform両方もjarファイルをダウンロードしてください。

Interfaceの定義

まず、com.sun.jna以下のこれらをimportしておきます。

import com.sun.jna.Native;
import com.sun.jna.Structure;
import com.sun.jna.WString;
import com.sun.jna.Pointer;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinDef.HWND;
import com.sun.jna.win32.StdCallLibrary;

User32が持つWin32APIを以下のように定義します(本Wikiでは取り上げないAPIも含んでいます)。

   public static interface User32 extends StdCallLibrary {
       User32 INSTANCE = (User32)Native.load("user32", User32.class);
       HWND FindWindowW(WString className, WString windowName);
       HWND FindWindowExW(HWND hwndParent, int childAfer, WString className, WString windowName);
       HWND GetWindow(HWND hwnd, int i);
       HWND GetForegroundWindow();
       boolean SetForegroundWindow(HWND hwnd);
       HWND GetWindowTextW(HWND hwnd, char[] str, int size);
       boolean IsWindowVisible(HWND hwnd);
       HWND WindowFromPhysicalPoint(POINTByValue pt);
       boolean SetCursorPos(int x, int y);
       HWND SetFocus(HWND hwnd);
       long GetMenu(HWND hwnd);
       int GetMenuStringW(long handle, int item, char[] ws, int ccMax, int flags);
       boolean MoveWindow(HWND hwnd, int X, int Y, int nWidth, int nHeight, boolean bRepaint);
       long SendMessageW(WinDef.HWND hwnd, int msg, int wparam, WString lparam);
       long SendMessageA(WinDef.HWND hwnd, int msg, int wparam, Pointer lparam);
       long SendMessageW(WinDef.HWND hwnd, int msg, int wparam char[] lparam);
       long SendMessageW(WinDef.HWND hwnd, int msg, int wparam, int lparam);
       long SendMessageW(WinDef.HWND hwnd, int msg, int wparam, long lparam);
       long PostMessageW(WinDef.HWND hwnd, int msg, int wparam, int lparam);
       long PostMessageW(WinDef.HWND hwnd, int msg, int wparam, long lparam);
  }


SendMessageやPostMessageは、wParamとlParamと様々な型の値を渡しますので、複数の定義を用意しておくことになります。

Windowハンドルの取得

IEのアドレスバー("Edit"クラスの子ウィンドウ)のウィンドウハンドルを取得する方法は以下のようになります。
VisualStudioに付属しているSPY++などのツールで特定したいウィンドウを探します。

画像1

これを元にFindWindowWおよびFindWindowExWをコールして、目的のウィンドウまで掘り下げていきます。

HWND ieWnd = ieWnd = User32.INSTANCE.FindWindowW(new WString("IEFrame"), null);
HWND tmpWnd = User32.INSTANCE.FindWindowExW(ieWnd, 0, new WString("WorkerW"), null);
tmpWnd = User32.INSTANCE.FindWindowExW(tmpWnd, 0, new WString("ReBarWindow32"), null);
tmpWnd = User32.INSTANCE.FindWindowExW(tmpWnd, 0, new WString("Address Band Root"), null);
HWND addressbarWnd = User32.INSTANCE.FindWindowExW(tmpWnd, 0, new WString("Edit"), null);

ウィンドウ名やクラス名は後に変更される可能性がありますが、よりクラス名の方が変更されにくいと考えられるため、極力クラス名で検索することをお勧めします。

エディットボックスからの文字列取得

エディットボックスのウィンドウハンドルが得られましたら、WM_GETTEXTメッセージを送ることでそこに書かれている文字列を取得することができます。これはラベルコントロールの場合も同様です。

char[] buf = new char[512];
User32.INSTANCE.SendMessageW(addressbarWnd, WM_GETTEXT, 512, buf);
String txtInEditbox = Native.toString(buf);

なお、WM_GETTEXTは適当な場所で以下のように定義しておきます。

public final int WM_GETTEXT = 0x000D;

エディットボックスへの入力

エディットボックスへの入力はWM_SETTEXTメッセージを送ることで可能です。

User32.INSTANCE.SendMessageW(addressbarWnd, WM_SETTEXT, 0, new WString("test text"));


なお、WM_GETTEXTは適当な場所で以下のように定義しておきます。

public final int WM_SETTEXT = 0x000C;

ボタンのクリック

ボタンコントロールをクリックするにはBM_CLICKメッセージを送ればよいと思われますが、試していません。
代わりに以下のようにマウスクリック関連のメッセージを送ることで実現できることは確認しています。

User32.INSTANCE.PostMessageW(currentWnd, WM_LBUTTONDOWN, MK_LBUTTON, 0x100010);
sleep(100);
User32.INSTANCE.PostMessageW(currentWnd, WM_LBUTTONUP, MK_LBUTTON, 0x100010);

lParamはクリックする座標で、このコードではボタンの(16, 16)の位置をクリックすることに相当します。
なお、WM_LBUTTONDOWN等は以下のように定義しておきます。

public final int WM_LBUTTONDOWN = 0x0201;
public final int WM_LBUTTONUP = 0x0202;
public final int MK_LBUTTON = 1;
public final int MK_RBUTTON = 2;

キー操作のシミュレーション

各種コントロールを含むウィンドウに対するキー入力をシミュレーションするには、WM_KEYDOWNおよびWM_KEYUPメッセージを送ります。例えばF5キーを押すには、以下のようなコードを実行します。

User32.INSTANCE.SendMessageW(tcWnd, WM_KEYDOWN, VK_F5, 0);
sleep(100);
User32.INSTANCE.SendMessageW(tcWnd, WM_KEYUP, VK_F5, 0);

WM_KEYDOWN、WM_KEYUPおよびVK_F5の定義は以下のようになります。

public final int WM_KEYDOWN = 0x0100;
public final int WM_KEYUP = 0x0101;
public final int VK_F5 = 0x74;

なお、wParam はvirtual key codeです。
https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes 
等を参照してください。

ListViewへの行の追加

ListViewへの行の追加はlParamに文字列へのポインターを指定してLB_ADDSTRINGメッセージを送ることでできるのですが、操作対象のアプリケーションが別プロセスで動作しているため、lParamにはGlobalAllocで確保したメモリへのポインターを指定する必要があります。

Pointer p = Kernel32.INSTANCE.GlobalAlloc(0x0042, 1024);  // GHND
Pointer pbuf = Kernel32.INSTANCE.GlobalLock(p);
pbuf.setString(0, ”追加”したい文字列");
Kernel32.INSTANCE.GlobalLock(p);
User32.INSTANCE.SendMessageA(originalFileListBoxWnd, LB_ADDSTRING, 0, pbuf);

lobalAlloc等はKernel32内のAPIのため、以下のように定義します。

public static interface Kernel32 extends StdCallLibrary {
   Kernel32 INSTANCE = (Kernel32)Native.load("kernel32", Kernel32.class);
   Pointer GlobalAlloc(int flag, int size);
   Pointer GlobalLock(Pointer handle);
   boolean GlobalFree(Pointer handle);
}

また、LB_ADDSTRINGは以下のように定義しておきます。

public final int LB_ADDSTRING = 0x0180;

TreeViewにおける行の選択

TreeViewで特定の行を選択するには、TVM_GETNEXTITEMメッセージを目的の行に到達するまで、1回または複数回送り、その後TVM_SELECTITEMメッセージを送ります。
以下はTreeのRootから子ノードの3番目のアイテムを選択する例です。

long val = User32.INSTANCE.SendMessageW(treeViewWnd, TVM_GETNEXTITEM, TVGN_ROOT, 0);
val = User32.INSTANCE.SendMessageW(treeViewWnd, TVM_GETNEXTITEM, TVGN_CHILD, val);
val = User32.INSTANCE.SendMessageW(treeViewWnd, TVM_GETNEXTITEM, TVGN_NEXT, val);
val = User32.INSTANCE.SendMessageW(treeViewWnd, TVM_GETNEXTITEM, TVGN_NEXT, val);
if (val != 0) User32.INSTANCE.SendMessageW(treeViewWnd, TVM_SELECTITEM, TVGN_CARET, val);

メニュー選択

メニュー操作はキーボードやマウス操作をエミュレートすることでも実現できますが、(私の経験上では)難易度が高く、確実性も低い場合がよくあります。そこでWM_COMMANDメッセージを送る方法の方がより確実だと思います。

User32.INSTANCE.SendMessageW(tcWnd, WM_COMMAND, menuid, 0);

ここでmenuidはメニューIDなのですが、これがアプリの仕様書等からわかる場合は、その値を使用します。これがわからない場合は、以下のメソッドのように、選択したいメニューアイテムにマッチするメニューIDを検索することになります。

private int GetMenuID(HWND wnd, String menuItemName) {
   long mh = User32.INSTANCE.GetMenu(wnd);
   for (int i = 0; i < 500; i++) {
       char[] buf = new char[256];
       User32.INSTANCE.GetMenuStringW(mh, i, buf, 256, 0);
       if (Native.toString(buf).compareTo(menuItemName) == 0) return i;
   }
   return 0;
}

なお、WM_COMMANDは以下の用に定義しておきます。

public final int WM_COMMAND = 0x0111;

ウィンドウを閉じる

アプリケーションを閉じるには、トップレベルウィンドウに対してWM_CLOSEメッセージを送ります。以下はIEのウィンドウを閉じるサンプルコードです。

HWND ieWnd = ieWnd = User32.INSTANCE.FindWindowW(new WString("IEFrame"), null);
User32.INSTANCE.PostMessageW(ieWnd, WM_CLOSE, 0, 0);

ここで、SendMessageではなくPostMessageを使っているのは、SendMessageでうまく行かなかった例があるためです。
なお、WM_CLOSEメッセージは以下のように定義します。

public final int WM_CLOSE = 0x0010;

操作できないコントロール

JNIでうまくコントロールできないウィンドウやコントロールも存在しますが、工夫次第でなんとかなる場合もあります。

もし、お困りのことがありましたら、お気軽にご相談下さい。

――――――――――――――――――――――――――――――――――

執筆者プロフィール:水谷裕一
大手外資系IT企業で15年間テストエンジニアとして、多数のプロジェクトでテストの自動化作業を経験。その後画像処理系ベンチャーを経てSHIFTに入社。
SHIFTでは、テストの自動化案件を2件こなした後、株式会社リアルグローブ・オートメーティッド(RGA)にPMとして出向中。RGAでは主にAnsibleに関する案件をプレーイングマネジャーとして担当している。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/

みんなにも読んでほしいですか?

オススメした記事はフォロワーのタイムラインに表示されます!