[JAVA] JAVA (JNA) KEYBD_EVENT 구현하기
JAVA 에서 JNA 를 통해서 KEYBD_EVENT 구현하는 방법.
자바 키입력은 Robot 클래스를 사용할 수도 있지만, JNA를 통하면 윈도우 user32 라이브러리의 KEYBD_EVENT 를 구현해서 쓸 수 있다.
Hwnd 를 다룰 일이 은근히 많아서 깃헙에 Wrapper 형태로 유지보수할 생각이다.
아래는 자바에서 특정한 윈도우 핸들(hWnd)을 포커싱하고 키입력하는 기능을 제공하는 코드다.
0. 필요한 라이브러리
jna-4.5.0.jar, jna-platform-4.5.0.jar
1. MainClass.java
현재 실행 중인 윈도우 핸들(ex : 메모장)을 열어서 Hello World 라는 문자열을 입력하도록 만들어봤다.
|
package com.thkmon.hwndtest;
import java.awt.event.KeyEvent;
import com.sun.jna.platform.win32.WinDef.HWND;
public class MainClass {
public static void main(String[] args) { System.out.println(“시작”); // 현재 실행 중인 메모장 윈도우 핸들을 열어서 Hello World 를 입력하기 try { String hwndText = “메모장”; HwndFinder hwndFinder = new HwndFinder(); HWND hwnd = hwndFinder.getSingleHwnd(hwndText); if (hwnd != null && HwndUtil.setFocusHandle(hwnd)) { HwndUtil.inputKey(hwnd, KeyEvent.VK_H); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_E); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_L); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_L); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_O); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_SPACE); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_W); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_O); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_R); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_L); sleep(100); HwndUtil.inputKey(hwnd, KeyEvent.VK_D); sleep(100); sleep(100);
} else { System.out.println(“윈도우 핸들(“ + hwndText + “)을 찾을 수 없습니다.”); } } catch (Exception e) { e.printStackTrace(); } System.out.println(“끝”); } private static void sleep(int i) { try { if (i > 0) { Thread.sleep(i); } } catch (InterruptedException e) { } catch (Exception e) {} } }
|
이때 몇몇 키는 KeyEvent 클래스에 정의되어있지 않다.
대표적으로 엔터키의 값이 정의되어 있지 않은데, 이런 키 값들은 인터넷에서 검색해서 값을 찾아 사용하면 된다.
예를 들어 엔터키 입력이 필요하면 아래와 같이 작성하면 된다.
|
private static final int VK_RETURN = 0x0D;
// 중략
HwndUtil.inputKey(hwnd, VK_RETURN);
sleep(100);
|
2. HwndFinder.java
윈도우 핸들(Hwnd)을 찾기 위한 클래스이다. getSingleHwnd 메서드는 1개의 Hwnd만을 리턴한다.
Hwnd 를 n개 리턴받고 싶다면 아래 코드를 변경해서 ArrayList<Hwnd> 에 담으면 되겠다.
|
package com.thkmon.hwndtest;
import com.sun.jna.Native; import com.sun.jna.Pointer; import com.sun.jna.platform.win32.User32; import com.sun.jna.platform.win32.WinDef.HWND; import com.sun.jna.platform.win32.WinDef.RECT; import com.sun.jna.platform.win32.WinUser.WNDENUMPROC;
public class HwndFinder { private HWND handle = null; public HWND getSingleHwnd(String nameOrClass) throws NullPointerException, Exception { handle = null; setSingleHwnd(nameOrClass, true); if (handle != null) { return handle; } setSingleHwnd(nameOrClass, false); if (handle != null) { return handle; } return null; } private void setSingleHwnd(final String nameOrClass, final boolean bEquals) throws NullPointerException, Exception { try { User32.INSTANCE.EnumWindows(new WNDENUMPROC() { public boolean callback(HWND hWnd, Pointer arg1) {
// 이미 찾았으면 스킵 if (handle != null) { return true; }
char[] windowText = new char[512]; User32.INSTANCE.GetWindowText(hWnd, windowText, 512); String wText = Native.toString(windowText); RECT rectangle = new RECT(); User32.INSTANCE.GetWindowRect(hWnd, rectangle); // 숨겨져 있는 창은 찾지 않는다. // 단, 최소화 되어있는 창은 찾는다. rectangle.left값이 -32000일 경우 최소화되어 있는 창이다. // if (wText.isEmpty() || !(User32.INSTANCE.IsWindowVisible(hWnd) && rectangle.left > -32000)) { if (wText.isEmpty() || !(User32.INSTANCE.IsWindowVisible(hWnd))) { return true; }
// 핸들의 클래스 네임 얻기 char[] c = new char[512]; User32.INSTANCE.GetClassName(hWnd, c, 512); String clsName = String.valueOf(c).trim();
// int count = 0; // System.out.println( // // “hwnd:”+hWnd+“,”+ // “번호:” + (++count) + “,텍스트:” + wText + “,” + “위치:(“ + rectangle.left + “,” + rectangle.top // + “)~(“ + rectangle.right + “,” + rectangle.bottom + “),” + “클래스네임:” + clsName); if (bEquals) { if (clsName != null && clsName.equals(nameOrClass)) { handle = hWnd; } if (wText != null && wText.equals(nameOrClass)) { handle = hWnd; } } else { if (clsName != null && clsName.indexOf(nameOrClass) > -1) { handle = hWnd; } if (wText != null && wText.indexOf(nameOrClass) > -1) { handle = hWnd; } }
return true; } }, null);
} catch (Exception e) { throw e; } } }
|
3. HwndUtil.java
Hwnd 에 관련해서 필요한 메서드들을 한 군데 모은 클래스다.
Hwnd 윈도우의 텍스트 가져오기, Hwnd 윈도우의 클래스명 가져오기, Hwnd 윈도우의 프로세스 아이디(pid)가져오기, Hwnd 윈도우 포커스, Hwnd 윈도우 최소화 여부 판단, Hwnd 윈도우 닫기, 2개의 Hwnd 윈도우 일치하는지 판단, 특정키를 입력하는 keybd_event 메서드 등의 기능을 제공한다.
|
package com.thkmon.hwndtest;
import com.sun.jna.Native; import com.sun.jna.platform.win32.User32; import com.sun.jna.platform.win32.WinDef.HWND; import com.sun.jna.platform.win32.WinDef.RECT; import com.sun.jna.platform.win32.WinUser; import com.sun.jna.ptr.IntByReference; import com.sun.jna.win32.StdCallLibrary;
public class HwndUtil {
/** * GetForegroundWindow 메서드, keybd_event 메서드 구현을 위한 인터페이스 정의 * * @author bbmon * */ public interface CustomUser32 extends StdCallLibrary { CustomUser32 INSTANCE = (CustomUser32) Native.loadLibrary(“user32”, CustomUser32.class); HWND GetForegroundWindow(); void keybd_event(byte bVk, byte bScan, int dwFlags, int dwExtraInfo); } /** * 핸들의 텍스트 가져오기 * * @param hwnd * @return */ public static String getHandleText(HWND hwnd) { char[] windowText = new char[512]; User32.INSTANCE.GetWindowText(hwnd, windowText, 512); String wText = Native.toString(windowText); return wText; }
/** * 핸들의 클래스명 가져오기 * @param hwnd * @return */ public static String getHandleClassName(HWND hwnd) { char[] c = new char[512]; User32.INSTANCE.GetClassName(hwnd, c, 512); String clsName = String.valueOf(c).trim(); return clsName; } /** * 핸들의 pid 가져오기 * @param hwnd * @return */ public static int getHandlePid(HWND hwnd) { IntByReference pidByRef = new IntByReference(0); User32.INSTANCE.GetWindowThreadProcessId(hwnd, pidByRef); int pid = pidByRef.getValue(); return pid; }
/** * 핸들 포커스 * * @param hwnd */ public static boolean setFocusHandle(HWND hwnd) { // 최소화 되어있을 경우 복원 if (isMinimizedHandle(hwnd)) { User32.INSTANCE.ShowWindow(hwnd, 9); } User32.INSTANCE.SetForegroundWindow(hwnd); try { Thread.sleep(100); } catch (Exception e) {} // 포커스 되었는지 확인해야 한다. HWND foregroundHwnd = CustomUser32.INSTANCE.GetForegroundWindow(); boolean bFocused = checkHandlesAreSame(hwnd, foregroundHwnd); return bFocused; } /** * 최소화되어 있는 창인지 검사한다. rectangle.left값이 -32000일 경우 최소화되어 있는 창이다. * * @param hwnd */ public static boolean isMinimizedHandle(HWND hwnd) { if (hwnd == null) { return false; } RECT rectangle = new RECT(); User32.INSTANCE.GetWindowRect(hwnd, rectangle); if (rectangle.left <= -32000) { return true; } return false; }
/** * 창을 강제로 닫는다. * * @param hwnd */ public static void closeHwnd(HWND hwnd) { User32.INSTANCE.PostMessage(hwnd, WinUser.WM_CLOSE, null, null); } /** * 두 개의 핸들 객체가 일치하는지 확인한다. * * @param hwnd1 * @param hwnd2 * @return */ public static boolean checkHandlesAreSame(HWND hwnd1, HWND hwnd2) { if (hwnd1 == null) { return false; } if (hwnd2 == null) { return false; } // pid가 같아도 핸들은 다를 수 있다. 예를 들어 엑셀 시트 창과 엑셀의 오류 메시지 창은 같은 pid를 갖지만 핸들은 다르다. int pid1 = HwndUtil.getHandlePid(hwnd1); int pid2 = HwndUtil.getHandlePid(hwnd2); if (pid1 != pid2) { return false; } String className1 = HwndUtil.getHandleClassName(hwnd1); String className2 = HwndUtil.getHandleClassName(hwnd2); if (className1 == null || className2 == null || !className1.equals(className2)) { return false; } String text1 = HwndUtil.getHandleText(hwnd1); String text2 = HwndUtil.getHandleText(hwnd2); if (text1 == null || text2 == null || !text1.equals(text2)) { return false; } return true; } /** * 특정 키를 누른다. * * @param customUser32 * @param hwnd * @param vkCode * @throws Exception */ public static void inputKey(HWND hwnd, int vkCode) throws Exception { CustomUser32.INSTANCE.keybd_event((byte) vkCode /* KeyEvent.VK_ESCAPE */, (byte) 0, 0, 0); CustomUser32.INSTANCE.keybd_event((byte) vkCode /* KeyEvent.VK_ESCAPE */, (byte) 0, 2 /* KEYEVENTF_KEYUP */, 0); } }
|
4. 실행결과
현재 실행 중인 메모장 윈도우를 찾아서 자동으로 hello world 문자열을 입력한 모습이다.

참고사이트 : https://develop88.tistory.com/entry/JNA-%ED%82%A4%EB%B3%B4%EB%93%9C%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EB%B0%9C%EC%83%9D-keybdevent