【GNZ48-章澤婷應援會】基於Java的SNH48Group應援會機器人(三)發送消息

這一部分是我也看了好久,才決定用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使用無頭模式。我下面稍微說說我的理解,不一定準確。

 

--Robot

在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: 鍵盤工具類
 *               <p>
 *               模擬鍵盤操作的工具。
 * @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: 將文本輸入到文本框中
	 *               <p>
	 *               實現原理是使用剪切板,將字符串參數放入剪切板中,然後模擬粘貼(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消息(這個組合鍵因人而異)
	 *               <p>
	 *               這裏使用的方法是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

User32是提供對W32 USER32庫的訪問的類,具體過程:

  1. 先獲取一個User32實例,通過這個實例調用 HWND FindWindow(String lpClassName, String lpWindowName) 檢索頂級窗口,通過窗口名字符串匹配,並且不會檢索子窗口。lpClassName爲窗口類字符串或空;lpWindowName爲窗口的標題。返回的HWND爲null則說明未找到該窗口。
  2. 獲得該窗口後,就可以通過User32調用 boolean ShowWindow(HWND hWnd, int nCmdShow) 設置指定窗口的顯示狀態,設置成功將返回true,否則返回false。hWnd爲上一步獲取的窗口句柄;nCmdShow爲控制窗口的顯示方式,值爲WinMain函數在nCmdShow其中獲得的值。對應值如下所示:

    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。


     

  3. 在設置成功顯示狀態操作後,通過User32調用 boolean SetForegroundWindow(HWND hWnd) 將窗口置爲最前並獲取焦點,即激活輸入框光標,設置成功返回ture,否則返回false。hWnd爲第一步獲取的窗口句柄。

通過以上三步獲取到窗口並能執行輸入操作後,調用Robot,將要發送的消息內容字符串放入剪切板中,然後通過模擬粘貼操作,即ctrl+v組合鍵,將消息粘貼到聊天輸入窗中,最後模擬回車鍵發送消息,至此就是一個完整的發送消息過程。不過有一個問題就是,雲服務器是通過遠程桌面進行訪問的,在遠程桌面訪問關閉後,會進入鎖屏,這樣User32操作就會失敗,目前還沒找到解決的辦法。

 


【GNZ48-章澤婷應援會】基於Java的SNH48Group應援會機器人(一)項目簡介

【GNZ48-章澤婷應援會】基於Java的SNH48Group應援會機器人(二)獲取數據

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章