这一部分是我也看了好久,才决定用Java的Robot + User32来实现。Robot是java.awt下的一个类,该类用于为测试自动化、自运行演示程序和其他需要控制鼠标和键盘的应用程序生成本机系统输入事件,因此可试用该类进行模拟鼠标键盘操作。User32是JNA下的一个类,该类提供对W32 USER32库的访问,也就是说可以试用该类来进行windows的一些操作,而这里我用来选择对话窗口。
其实我这里用到的awt和JNA都是它们其中的一个小功能点而已,还有很多其他的功能可以去学习使用。下面放一下我发送消息的方法。
/**
* @Function: SendMessageService.java
* @Description: 向指定QQ群发送消息
* @param: communityName 为必填的非空参数,用于发送消息。
* @param: message 要发送的消息内容。
* @param: communityId 为非必填参数,可为空,用于记录日志。
* @author: JuFF_白羽
* @date: 2018年7月15日 上午2:36:11
*/
private void sendQQMessage(String message, Long communityId, String communityName) throws AWTException {
WinDef.HWND hwnd = User32.INSTANCE.FindWindow(null, communityName); // 第一个参数是Windows窗体的窗体类,第二个参数是窗体的标题。不熟悉windows编程的需要先找一些Windows窗体数据结构的知识来看看,还有windows消息循环处理,其他的东西不用看太多。
if (hwnd == null) {
LOGGER.info("找不到[{}]聊天窗口", communityId);
} else {
Robot robot = new Robot();
boolean showWindow = User32.INSTANCE.ShowWindow(hwnd, 9); // SW_RESTORE
if (showWindow) {
boolean setForegroundWindow = User32.INSTANCE.SetForegroundWindow(hwnd);
if (setForegroundWindow) {
robot.delay(3000);// 等3秒
KeyboardUtil.keyPressString(robot, message);// 输入内容
KeyboardUtil.keyPress(robot, KeyEvent.VK_ENTER);// 按下回车
LOGGER.info("新消息已发送到[{}]聊天窗口", communityId);
} else {
LOGGER.info("设置前景窗口失败");
}
} else {
LOGGER.info("显示窗口失败");
}
}
}
从上面的代码来看,并没有什么复杂,而其实里面Robot和User32的调用过程都各有一个坑,先从调用Robot发现的问题说起好了。在JUnit Test运行时,Robot能正常实例化,然后模拟键盘按键和鼠标点击等操作,但打包成jar后部署到服务器上时,运行到实例化Robot对象时却报了 java.awt.AWTException: headless environment 这个异常。而这个异常我查了百度,发现很多都没说清楚具体原因,于是又去查了谷歌,相关解释见Java SE使用无头模式。我下面稍微说说我的理解,不一定准确。
在awt中,java.awt.Tookit类是抽象窗口工具包(AWT)的所有实际实现的抽象超级父类,而该工具包下的子类用于将各种AWT组件绑定到特定的本地工具包而实现的,如果不支持显示设备、键盘或鼠标,则会受到影响从而抛出一个无头异常。换句话我是这么理解的,在我本地电脑上JUnit Test时,由于我本地电脑有正常的显示设备、键盘和鼠标,所有默认设置运行并没有被影响,而像这种云服务器上,一般不会正常提供有显示设备和键鼠的,于是就会抛出这个异常。但awt并不是一定需要有相关外设才能被正常运行,而是和计算机系统是否支持相关属性。
GraphicsEnvironment还提供了方法来检测当前系统是否无头:
public static boolean
isHeadless()
测试此环境中是否支持显示、键盘和鼠标。如果此方法返回true,那么依赖于显示器、键盘或鼠标的Toolkit和GraphicsEnvironment将会抛出HeadlessException。
public boolean isHeadlessInstance()
返回此图形环境中是否支持显示、键盘和鼠标。如果返回true,那么依赖于显示器、键盘或鼠标的GraphicsEnvironment将会抛出HeadlessException。
从上两个方法可知,返回false时就能正常调用,返回true时则会在调用时抛出HeadlessException。而我部署在服务器上的程序抛出的是无头环境异常,那么解决方法是什么呢?在原文里有说明:
System Properties Setup
To set up headless mode, set the appropriate system property by using the setProperty()
method. This method enables you to set the desired value for the system property that is indicated by the specific key.
System.setProperty("java.awt.headless", "true"); |
In this code, java.awt.headless
is a system property, and true
is a value that is assigned to it.
You can also use the following command line if you plan to run the same application in both a headless and a traditional environment:
java -Djava.awt.headless=true; |
用System调用setProperty(),而这个方法能够为特定key所指示的系统属性设置期望的value。设置无头模式时,key为java.awt.headless,value为true或false。我这里遇到的是因为处于无头环境,所以要转成用传统环境运行。因此需要禁用掉无头环境,即,添加此静态代码块即可。如果计划在无头和传统环境中运行相同的应用程序,还可以使用下面那个命令。至此,Robot在服务器上遇到的问题算是解决了。
接下来说明一下Robot的具体使用方法。模拟键鼠操作其实就是调用一些Robot提供的固定方法,传入按键对应的值,来实现按键的操作,并且基本都有一个共同点,就是方法基本包括“按下”和“弹起”,当先执行一次“按下”的方法,再执行一次“弹起”的方法,就完成了一个按键的模拟;组合键则为先执行完要“按下”的方法,例如撤销操作为ctrl+z,则调用keyPress(KeyEvent.VK_CONTROL)、keyPress(KeyEvent.VK_Z)两个方法后,再调用keyRelease(KeyEvent.VK_Z)、keyRelease(KeyEvent.VK_CONTROL),最好保证先按下的后弹起。
为了简化操作,写了一个工具类供调用,工具类如下。按键对应的值可查看类java.awt.event.KeyEvent。
package com.gnz48.zzt.util;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.KeyEvent;
/**
* @Description: 键盘工具类
*
* 模拟键盘操作的工具。
* @author JuFF_白羽
* @date 2018年7月12日 下午6:08:30
*/
public class KeyboardUtil {
public static void main(String[] args) throws Exception {
Robot robot = new Robot();
// 调用系统方法打开记事本
Runtime.getRuntime().exec("notepad");
robot.delay(2000);
// 全屏显示
// keyPressWithAlt(robot,KeyEvent.VK_SPACE);
// 输入x
keyPress(robot, KeyEvent.VK_X);
// 输入回车
keyPress(robot, KeyEvent.VK_ENTER);
robot.delay(1000);
// 输入字符串
keyPressString(robot, "哈哈哈哈哈哈哈哈哈嗝");
}
/**
* @Description: Shift组合键
* @author JuFF_白羽
* @param r
* @param key
*/
public static void keyPressWithShift(Robot r, int key) {
// 按下Shift
r.keyPress(KeyEvent.VK_SHIFT);
// 按下某个键
r.keyPress(key);
// 释放某个键
r.keyRelease(key);
// 释放Shift
r.keyRelease(KeyEvent.VK_SHIFT);
// 等待100ms
r.delay(100);
}
/**
* @Description: Ctrl组合键
* @author JuFF_白羽
* @param r
* @param key
*/
public static void keyPressWithCtrl(Robot r, int key) {
r.keyPress(KeyEvent.VK_CONTROL);
r.keyPress(key);
r.keyRelease(key);
r.keyRelease(KeyEvent.VK_CONTROL);
r.delay(100);
}
/**
* @Description: Alt组合键
* @author JuFF_白羽
* @param r
* @param key
*/
public static void keyPressWithAlt(Robot r, int key) {
r.keyPress(KeyEvent.VK_ALT);
r.keyPress(key);
r.keyRelease(key);
r.keyRelease(KeyEvent.VK_ALT);
r.delay(100);
}
/**
* @Title: keyPressString
* @Description: 将文本输入到文本框中
*
* 实现原理是使用剪切板,将字符串参数放入剪切板中,然后模拟粘贴(Ctrl+V)。
* @author JuFF_白羽
* @param r
* Java的自动化系统输入事件对象
* @param str
* 要写入文本框的字符串
*/
public static void keyPressString(Robot r, String str) {
// 获取剪切板
Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard();
// 将传入字符串封装下
Transferable tText = new StringSelection(str);
// 将字符串放入剪切板
clip.setContents(tText, null);
// 按下Ctrl+V实现粘贴文本
keyPressWithCtrl(r, KeyEvent.VK_V);
r.delay(100);
}
/**
* @Description: 输入数字
* @author JuFF_白羽
* @param r
* @param number
*/
public static void keyPressNumber(Robot r, int number) {
// 将数字转成字符串
String str = Integer.toString(number);
// 调用字符串的方法
keyPressString(r, str);
}
/**
* @Description: 按一次某个按键
* @author JuFF_白羽
* @param r
* @param key
* @return void 返回类型
*/
public static void keyPress(Robot r, int key) {
// 按下键
r.keyPress(key);
// 释放键
r.keyRelease(key);
r.delay(100);
}
/**
* @Description: 快速打开QQ消息(这个组合键因人而异)
*
* 这里使用的方法是ctrl+alt+z来弹出QQ消息
* @author JuFF_白羽
* @param r
*/
public static void keyPressAtlWithCtrlWithZ(Robot r) {
r.keyPress(KeyEvent.VK_ALT);
r.keyPress(KeyEvent.VK_CONTROL);
r.keyPress(KeyEvent.VK_Z);
r.keyRelease(KeyEvent.VK_Z);
r.keyRelease(KeyEvent.VK_CONTROL);
r.keyRelease(KeyEvent.VK_ALT);
r.delay(100);
}
/**
* @Description: 点击一下鼠标左键
* @author JuFF_白羽
* @param r
*/
public static void mouseLeftHit(Robot r) {
r.mousePress(KeyEvent.BUTTON1_DOWN_MASK);
r.mouseRelease(KeyEvent.BUTTON1_DOWN_MASK);
r.delay(100);
}
}
User32是提供对W32 USER32库的访问的类,具体过程:
SW_FORCEMINIMIZE:在WindowNT5.0中最小化窗口,即使拥有窗口的线程被挂起也会最小化。在从其他线程最小化窗口时才使用这个参数。nCmdShow=11。
SW_HIDE:隐藏窗口并激活其他窗口。nCmdShow=0。
SW_MAXIMIZE:最大化指定的窗口。nCmdShow=3。
SW_MINIMIZE:最小化指定的窗口并且激活在Z序中的下一个顶层窗口。nCmdShow=6。
SW_RESTORE:激活并显示窗口。如果窗口最小化或最大化,则系统将窗口恢复到原来的尺寸和位置。在恢复最小化窗口时,应用程序应该指定这个标志。nCmdShow=9。
SW_SHOW:在窗口原来的位置以原来的尺寸激活和显示窗口。nCmdShow=5。
SW_SHOWDEFAULT:依据在STARTUPINFO结构中指定的SW_FLAG标志设定显示状态,STARTUPINFO 结构是由启动应用程序的程序传递给CreateProcess函数的。nCmdShow=10。
SW_SHOWMAXIMIZED:激活窗口并将其最大化。nCmdShow=3。
SW_SHOWMINIMIZED:激活窗口并将其最小化。nCmdShow=2。
SW_SHOWMINNOACTIVE:窗口最小化,激活窗口仍然维持激活状态。nCmdShow=7。
SW_SHOWNA:以窗口原来的状态显示窗口。激活窗口仍然维持激活状态。nCmdShow=8。
SW_SHOWNOACTIVATE:以窗口最近一次的大小和状态显示窗口。激活窗口仍然维持激活状态。nCmdShow=4。
SW_SHOWNORMAL:激活并显示一个窗口。如果窗口被最小化或最大化,系统将其恢复到原来的尺寸和大小。应用程序在第一次显示窗口的时候应该指定此标志。nCmdShow=1。
通过以上三步获取到窗口并能执行输入操作后,调用Robot,将要发送的消息内容字符串放入剪切板中,然后通过模拟粘贴操作,即ctrl+v组合键,将消息粘贴到聊天输入窗中,最后模拟回车键发送消息,至此就是一个完整的发送消息过程。不过有一个问题就是,云服务器是通过远程桌面进行访问的,在远程桌面访问关闭后,会进入锁屏,这样User32操作就会失败,目前还没找到解决的办法。
【GNZ48-章泽婷应援会】基于Java的SNH48Group应援会机器人(一)项目简介
【GNZ48-章泽婷应援会】基于Java的SNH48Group应援会机器人(二)获取数据