本文介绍如何在Windows平板触摸屏环境下,使用Java Swing框架获取触摸屏原始输入信息,包括触摸点位坐标、触摸状态(按下/移动/抬起)以及触点标识符等底层数据。
概述
对于需要在触摸屏上进行自定义交互处理的桌面应用,获取触摸屏的原始点位信息是基础需求。Java Swing框架本身不直接提供多点触控支持,但通过JNA (Java Native Access) 调用Windows原生触摸API,可以捕获触摸点的坐标、触摸状态和触点标识符等底层输入数据,为后续自定义手势识别等高级功能提供数据基础。
技术架构
1. 核心组件
1.1 Windows原生API集成
使用JNA (Java Native Access)调用Windows User32 API实现触摸事件处理:
import com.sun.jna.Native;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinNT;
import com.sun.jna.platform.win32.WinUser;
public interface User32 extends com.sun.jna.platform.win32.User32 {
User32 INSTANCE = Native.load("user32", User32.class);
/**
* 将窗口注册为支持触摸.
*
* @param hWnd 要注册的窗口句柄
* @param ulFlags 注册选项,可设置为 0 或 TWF_FINETOUCH、TWF_WANTPALM
* @return 如果函数成功,则返回值为非零值
*/
WinDef.BOOL RegisterTouchWindow(WinDef.HWND hWnd, int ulFlags);
/**
* <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-unregistertouchwindow">将窗口注册为不再支持触摸。</a>
*
* @param hwnd 窗口的句柄。 如果调用线程不拥有指定窗口,函数将失败并 ERROR_ACCESS_DENIED 。
* @return 如果函数失败,则返回值为零。 若要获取扩展错误信息,请使用 GetLastError 函数。
*/
WinDef.BOOL UnregisterTouchWindow(WinDef.HWND hwnd);
/**
* <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-findwindowa">检索其类名和窗口名称与指定字符串匹配的顶级窗口的句柄。 此函数不搜索子窗口。 此函数不执行区分大小写的搜索。</a>
*
* @param classname 类名
* @param name 窗口名称 (窗口标题) 。 如果此参数为 NULL,则所有窗口名称都匹配。
* @return 如果函数成功,则返回值是具有指定类名称和窗口名称的窗口的句柄。如果函数失败,则返回值为 NULL。 此函数不会修改最后一个错误值。
*/
WinDef.HWND FindWindowA(String classname, String name);
/**
* <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-sendmessagea">将指定的消息发送到一个或多个窗口。 SendMessage 函数调用指定窗口的窗口过程,在窗口过程处理消息之前不会返回 。</a>
*
* @param handle 窗口的句柄,其窗口过程将接收消息。 如果此参数 HWND_BROADCAST ( (HWND) 0xffff) ,则消息将发送到系统中的所有顶级窗口,包括禁用或不可见的无所有者窗口、重叠窗口和弹出窗口;但消息不会发送到子窗口。
* @param Msg 要发送的消息
* @param ICallback 回调
* @param lparam 其他的消息特定信息。
* @return 返回值指定消息处理的结果;这取决于发送的消息。
*/
WinDef.LRESULT SendMessageA(WinDef.HWND handle, WinDef.UINT Msg, ICallback ICallback, WinDef.LPARAM lparam);
/**
* <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-setwindowshookexa">将应用程序定义的挂钩过程安装到挂钩链中。 你将安装挂钩过程来监视系统的某些类型的事件。 这些事件与特定线程或与调用线程位于同一桌面中的所有线程相关联。</a>
*
* @param idHook 要安装的挂钩过程的类型。
* @param lpfn 指向挂钩过程的指针。 如果 dwThreadId 参数为零或指定由其他进程创建的线程的标识符, 则 lpfn 参数必须指向 DLL 中的挂钩过程。 否则, lpfn 可以指向与当前进程关联的代码中的挂钩过程。
* @param hMod 包含挂钩过程的 DLL 句柄,如果 lpfn 指向当前进程中的回调过程且 dwThreadId 为当前线程的标识符,则必须设置为 null。
* @param dwThreadId 要与挂钩过程关联的线程的标识符。 对于桌面应用,如果此参数为零,则挂钩过程与调用线程在同一桌面中运行的所有现有线程相关联。 对于 Windows 应用商店应用,请参阅“备注”部分。
* @return 如果函数成功,则返回值是挂钩过程的句柄。
*/
WinUser.HHOOK SetWindowsHookExA(int idHook, HOOKPROC lpfn, WinDef.HINSTANCE hMod, WinDef.DWORD dwThreadId);
/**
* <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-gettouchinputinfo">检索有关与特定触摸输入句柄关联的触摸输入的详细信息。</a>
*
* @param hTouchInput 在触摸消息 的 LPARAM 中收到的触摸输入句柄。 如果此句柄无效,函数将失败并 ERROR_INVALID_HANDLE 。 请注意,在成功调用 CloseTouchInputHandle 时使用句柄后,或者传递到 DefWindowProc、PostMessage、SendMessage 或其变体之一后,句柄无效。
* @param cInputs pInputs 数组中的结构数。 理想情况下,这至少应等于消息 WPARAM 中指示的与消息关联的触摸点数。 如果 cInputs 小于触摸点数,函数仍将成功,并使用有关 cInputs 触摸点的信息填充 pInputs 缓冲区。
* @param pInputs 指向 TOUCHINPUT 结构的数组的指针,用于接收有关与指定触摸输入句柄关联的触摸点的信息。
* @param cbSize 单个 TOUCHINPUT 结构的大小(以字节为单位)。 如果 cbSize 不是单个 TOUCHINPUT 结构的大小,则函数将失败并 ERROR_INVALID_PARAMETER。
* @return 如果该函数成功,则返回值为非零值。 如果函数失败,则返回值为零。 若要获取扩展错误信息,请使用 GetLastError 函数。
*/
WinDef.BOOL GetTouchInputInfo(WinNT.HANDLE hTouchInput, int cInputs, TouchInput[] pInputs, int cbSize);
/**
* <a href="https://learn.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-closetouchinputhandle">关闭触摸输入句柄,释放与其关联的进程内存,并使句柄失效。</a>
*
* @param hTouchInput 在触摸消息 的 LPARAM 中收到的触摸输入句柄。 如果此句柄无效,函数将失败并 ERROR_INVALID_HANDLE 。 请注意,在成功调用 CloseTouchInputHandle 时使用句柄后,或者传递到 DefWindowProc、PostMessage、SendMessage 或其变体之一后,句柄无效。
* @return 如果该函数成功,则返回值为非零值。
* <p>
* 如果函数失败,则返回值为零。 若要获取扩展错误信息,请使用 GetLastError 函数。
*/
WinDef.BOOL CloseTouchInputHandle(WinNT.HANDLE hTouchInput);
}1.2 触摸输入数据结构
TouchInput类封装了Windows触摸输入数据结构:
import com.sun.jna.Structure;
import com.sun.jna.platform.win32.BaseTSD;
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinNT;
import java.util.Arrays;
import java.util.List;
public class TouchInput extends Structure {
/**
* x 坐标 (触摸输入的水平点) 。 此成员以物理屏幕坐标的百分之一像素表示。
*/
public WinDef.LONG x;
/**
* y 坐标 (触摸输入的垂直点) 。 此成员以物理屏幕坐标的百分之一像素表示。
*/
public WinDef.LONG y;
/**
* 源输入设备的设备句柄。 触摸输入提供程序在运行时为每个设备提供唯一的提供程序。 请参阅下面的示例部分。
*/
public WinNT.HANDLE hSource;
/**
* 用于区分特定触摸输入的触摸点标识符。 此值在触摸接触序列中保持一致,从接触点下降到它恢复。 稍后可能会对后续联系人重复使用 ID。
*/
public WinDef.DWORD dwID;
/**
* 一组位标志,用于指定触摸点按下、释放和运动的各个方面。 此成员中的位可以是“备注”部分中值的任意合理组合。
*/
public WinDef.DWORD dwFlags;
/**
* 一组位标志,用于指定结构中的哪些可选字段包含有效值。 可选字段中有效信息的可用性特定于设备。 仅当在 dwMask 中设置了相应的位时,应用程序才应使用可选字段值。 此字段可能包含“备注”部分中提到的 dwMask 标志的组合。
*/
public WinDef.DWORD dwMask;
/**
* 事件的时间戳(以毫秒为单位)。 使用方应用程序应注意,系统不对此字段执行验证;如果未设置 TOUCHINPUTMASKF_TIMEFROMSYSTEM 标志,则此字段中值的准确性和顺序完全取决于触摸输入提供程序。
*/
public WinDef.DWORD dwTime;
/**
* 与触摸事件关联的附加值。
*/
public BaseTSD.ULONG_PTR dwExtraInfo;
/**
* 触摸接触区域的宽度,以物理屏幕坐标中的百分之一像素为单位。 仅当 dwMask 成员设置了 TOUCHEVENTFMASK_CONTACTAREA 标志时,此值才有效。
*/
public WinDef.DWORD cxContact;
/**
* 触摸接触区域的高度(以物理屏幕坐标中的百分之一像素为单位)。 仅当 dwMask 成员设置了 TOUCHEVENTFMASK_CONTACTAREA 标志时,此值才有效。
*/
public WinDef.DWORD cyContact;
protected List<String> getFieldOrder() {
return Arrays.asList("x", "y", "hSource", "dwID", "dwFlags", "dwMask", "dwTime", "dwExtraInfo", "cxContact", "cyContact");
}
}1.3 回调接口
import com.sun.jna.platform.win32.WinDef;
import com.sun.jna.platform.win32.WinUser;
public interface HOOKPROC extends WinUser.HOOKPROC {
WinDef.LRESULT callback(int nCode, WinDef.WPARAM wParam, WinUser.CWPSTRUCT lParam);
}public interface ICallback extends com.sun.jna.Callback {
void callback();
}2. 系统架构
2.1 判断是否存在触摸设备
单例模式管理触摸窗口注册和回调:
// 引入上方定义的 User32 接口
private final User32 user32;
// 通过 Windows PowerShell 获取并返回当前电脑上触摸屏设备的运行状态。
public static boolean isTouchScreenAvailable() {
String command = "powershell -command \"Get-PnpDevice | " +
"Where-Object { $_.Class -eq \\\"HIDClass\\\" " +
"&& $_.FriendlyName -like \\\"*Touch Screen*\\\" }\"";
try {
Process process = Runtime.getRuntime().exec(command);
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.trim().equalsIgnoreCase("OK")) {
return true;
}
}
process.waitFor();
return false;
} catch (IOException | InterruptedException e) {
return false;
}
}
// 通过GetSystemMetrics方法判断
public boolean GetSystemMetrics94(){
return user32.GetSystemMetrics(94) != 0;
}94 参数:如果当前操作系统是 Windows 7 或 Windows Server 2008 R2 并且平板电脑输入服务已启动,则为非零;否则为 0。 返回值是一个位掩码,用于指定设备支持的数字化器输入的类型。 有关详细信息,请参阅“备注”。Windows Server 2008、Windows Vista 和 Windows XP/2000: 不支持此值。
注意: 因为 Java 是跨平台的,所以 Swing 可以在任意可运行 Java 的环境中启动,建议增加 Windows 系统的判断以防止抛出异常
2.2 与 Swing 建立关系
负责设置Windows钩子和处理触摸消息:
public class TouchService {
private final Logger log = LoggerFactory.getLogger(TouchService.class);
private final User32 user32;
private final HOOKPROC hookProc;
private final WinDef.DWORD dword;
private final WinDef.HWND hwnd;
private final WinDef.HWND frameHwnd;
private WinUser.HHOOK hhook;
private volatile boolean disposed = false;
private TouchService(final User32 user32, final Window window) {
this.user32 = user32;
this.hwnd = user32.FindWindowA("SunAwtToolkit", "theAwtToolkitWindow");
this.hookProc = (nCode, wParam, lParam) -> {
try {
if (lParam.message == 576 && lParam.wParam.intValue() == 2) {
final int size = lowWord(lParam.wParam.intValue());
TouchInput[] touchInputs = touchInputProcessor.getTouchArray(size);
WinDef.HBITMAP hTouchInput = new WinDef.HBITMAP(lParam.lParam.toPointer());
WinDef.BOOL getTouchInputInfo = user32.GetTouchInputInfo(hTouchInput, size, touchInputs, touchInputs[0].size());
if (getTouchInputInfo.booleanValue()) {
// touchInputs 为触点信息,通过判断 touchInputs 的 size 可以判断出具体触点信息
}
user32.CloseTouchInputHandle(hTouchInput);
}
} catch (Exception e) {
log.error("SetWindowsHookExA callback 发生异常!", e);
}
return user32.CallNextHookEx(null, nCode, wParam, lParam.lParam);
};
Pointer pointer = Native.getWindowPointer(window);
frameHwnd = new WinDef.HWND(pointer);
dword = new WinDef.DWORD(user32.GetWindowThreadProcessId(new WinDef.HWND(pointer), null));
user32.SendMessageA(hwnd, new WinDef.UINT(0x982a), () -> {
hhook = user32.SetWindowsHookExA(WinUser.WH_CALLWNDPROC, hookProc, null, dword);
}, new WinDef.LPARAM(0L));
}
}逐行解析
Window 参数为 java.awt.Windowthis.hwnd = user32.FindWindowA("SunAwtToolkit", "theAwtToolkitWindow");- 目的:找到 Java AWT 的内部工具窗口
- 类名:"SunAwtToolkit" - Java AWT 的工具包窗口类名
- 窗口名:"theAwtToolkitWindow" - 固定的窗口标题
- 作用:这个窗口是 AWT 接收底层输入的核心窗口,需要在这里安装 Hook
this.hookProc = (nCode, wParam, lParam) -> {
try {
if (lParam.message == 576 && lParam.wParam.intValue() == 2) {
// ... 触摸处理逻辑
}
} catch (Exception e) {
...
}
return user32.CallNextHookEx(null, nCode, wParam, lParam.lParam);
};这是整个类的核心逻辑,一个 Lambda 表达式实现的 Hook 过程: 参数说明:
- nCode:Hook 代码,表示如何处理消息
- wParam:附加参数(包含触摸点数量)
- lParam:指向 CWPSTRUCT 结构的指针,包含消息详情
if (lParam.message == 576 && lParam.wParam.intValue() == 2) {- 576 是 Windows 消息
WM_TOUCH的值(十六进制为 0x0240) - wParam == 2 表示这是一个触摸输入消息(GID_TOUCH)
- 这个条件判断用于过滤出触摸相关的消息
final int size = lowWord(lParam.wParam.intValue());
public int lowWord(int num) {
return num & 0xFFFF;
}- 从 wParam 的低 16 位提取触摸点的数量
lowWord()方法通过& 0xFFFF操作获取低字部分
TouchInput[] touchInputs = touchInputProcessor.getTouchArray(size);- 根据触摸点数量创建对应大小的
TouchInput数组 TouchInput是用于存储单个触摸点信息的结构体
WinDef.HBITMAP hTouchInput = new WinDef.HBITMAP(lParam.lParam.toPointer());- 从 lParam 中获取触摸输入句柄
- 这个句柄指向系统分配的触摸输入数据
WinDef.BOOL getTouchInputInfo = user32.GetTouchInputInfo(hTouchInput, size, touchInputs, touchInputs[0].size());- 调用 Windows API
GetTouchInputInfo获取详细的触摸信息 - 参数包括:触摸句柄、触摸点数量、接收数据的数组、结构体大小
- 返回布尔值表示是否成功获取
if (getTouchInputInfo.booleanValue()) {
touchInputProcessor.touchInputProcessor(touchInputs);
}- 如果成功获取触摸信息,则交给
touchInputProcessor进行处理 - 这里会触发注册的触摸回调函数
user32.CloseTouchInputHandle(hTouchInput);- 重要:关闭触摸输入句柄,释放系统资源
- 这是必须的操作,否则会造成资源泄漏
return user32.CallNextHookEx(null, nCode, wParam, lParam.lParam);- 必须调用,确保其他 Hook 也能接收到消息
- Windows Hook 链的要求
Pointer pointer = Native.getWindowPointer(window);- 从 Java Window 对象提取原生窗口指针
frameHwnd = new WinDef.HWND(pointer);- 封装为 Windows HWND 句柄
dword = new WinDef.DWORD(user32.GetWindowThreadProcessId(new WinDef.HWND(pointer), null));- 获取窗口所属线程的 ID,将 Hook 绑定到特定线程
user32.SendMessageA(hwnd, new WinDef.UINT(0x982a), () -> {
hhook = user32.SetWindowsHookExA(WinUser.WH_CALLWNDPROC, hookProc, null, dword);
}, new WinDef.LPARAM(0L));这是最复杂也最关键的部分:
- 为什么要用 SendMessage?
- 直接在当前线程安装 Hook 可能失败或造成死锁
- AWT 有自己的事件调度线程
- 需要在 AWT 线程上下文中执行 Hook 安装
- 消息 0x982a 的含义:
- 这是 AWT 内部的自定义消息
- 用于在 AWT 线程中执行回调
- 相当于"在 AWT 线程中运行这段代码" Lambda 回调:
() -> {
hhook = user32.SetWindowsHookExA(WinUser.WH_CALLWNDPROC, hookProc, null, dword);
}- 这个 Lambda 会在 AWT 线程中被执行
- 实际安装 Hook 的地方
SetWindowsHookExA 参数详解:
- WinUser.WH_CALLWNDPROC:Hook 类型,拦截窗口过程收到的消息
- hookProc:前面定义的回调函数
- null:模块句柄,当前进程的 Hook 不需要
- dword:目标线程 ID,只 Hook 这个线程的消息
整体流程
应用启动
↓
创建 TouchService
↓
查找 AWT Toolkit 窗口 (hwnd)
↓
创建 Hook 回调函数 (hookProc)
↓
获取 Java 窗口句柄和线程 ID
↓
SendMessageA 到 AWT 窗口
↓
AWT 线程执行 SetWindowsHookExA
↓
Hook 安装成功,开始监听
↓
用户触摸屏幕
↓
Windows 生成 WM_TOUCH 消息
↓
Hook 拦截消息
↓
解析触摸数据
↓
触发 Java 回调
↓
应用响应触摸事件3. 释放资源
public void dispose() {
if (disposed) {
log.warn("TouchService 已经被销毁,跳过重复清理");
return;
}
synchronized (this) {
if (disposed) {
return;
}
disposed = true;
}
log.info("开始清理 TouchService 资源...");
try {
if (hwnd != null) {
user32.SendMessageA(hwnd, new WinDef.UINT(0x982a), () -> {
boolean unhookSuccess = false;
boolean unregisterSuccess = false;
try {
if (hhook != null) {
unhookSuccess = user32.UnhookWindowsHookEx(hhook).booleanValue();
if (unhookSuccess) {
log.info("✓ UnhookWindowsHookEx 成功");
hhook = null;
} else {
int errorCode = Kernel32.INSTANCE.GetLastError();
log.error("✗ UnhookWindowsHookEx 失败, 错误码: {}, 错误信息: {}",
errorCode, Kernel32Util.getLastErrorMessage());
}
} else {
log.warn("Hook 句柄为空,跳过卸载");
}
if (frameHwnd != null) {
unregisterSuccess = user32.UnregisterTouchWindow(frameHwnd).booleanValue();
if (unregisterSuccess) {
log.info("✓ UnregisterTouchWindow 成功");
} else {
int errorCode = Kernel32.INSTANCE.GetLastError();
log.error("✗ UnregisterTouchWindow 失败, 错误码: {}, 错误信息: {}",
errorCode, Kernel32Util.getLastErrorMessage());
}
} else {
log.warn("窗口句柄为空,跳过注销触摸窗口");
}
} catch (Exception e) {
log.error("清理 Windows 资源时发生异常", e);
}
if (unhookSuccess && unregisterSuccess) {
log.info("Windows 资源清理完成");
} else {
log.warn("Windows 资源清理部分失败");
}
}, new WinDef.LPARAM(0L));
} else {
log.warn("AWT 窗口句柄为空,无法执行清理回调");
}
} catch (Exception e) {
log.error("发送清理消息时发生异常", e);
}
log.info("TouchService 资源清理完成");
}整体流程
应用关闭 / 服务销毁
↓
调用 dispose()
↓
SendMessageA 到 AWT 窗口(同步等待)
↓
AWT 线程执行清理回调
↓
┌─────────────────────────────┐
│ UnhookWindowsHookEx │ ← 卸载 Hook,停止消息拦截
│ ↓ │
│ UnregisterTouchWindow │ ← 注销触摸支持
│ ↓ │
│ 记录错误信息(调试用) │
└─────────────────────────────┘
↓
SendMessageA 返回
↓
├─ 停止定时任务
├─ 清空触摸数据映射
└─ 关闭线程池
↓
资源完全释放 ✓