タスクトレイに常駐するツールを作成する


今回作るもの
-
タスクトレイに常駐するツールを作成します。
画面キャプチャの取得をキーフックを利用して出力可能にします。

参考にしたサイト
-
- タスクトレイに関して
https://fuzzhead.hatenadiary.org/entry/20081027/p1

- キーフックに関して
https://qiita.com/Getaji/items/8ad1887761ac222b61a2


用意するもの
-
- java1.8以上
　※関数型インターフェースとラムダを一部使用します
- JNativeHookライブラリ
　※Git上に公開されているものを使用します


実装
-

事前準備は割愛させていただきます。
また、コメントと命名に至らない点がありますがご容赦ください。

作成順序はキーフック、タスクトレイツールの実装となっています。

##1.修飾キーを特定するための列挙型
NativeKeyListenerのキー入力をわかりやすくするため、修飾キーだけ列挙型に変換します。
※分岐が多いので改善できればしたいと思ってます。

```ruby:ModifierKey.java
package application.constants;

import org.jnativehook.keyboard.NativeKeyEvent;

/**
 * 修飾キーの列挙型
 */
public enum ModifierKey {
	NONE,
	CTRL,
	ALT,
	SHIFT,
	CTRL_SHIFT,
	CTRL_ALT,
	SHIFT_ALT,
	CTRL_ALT_SHIFT;
	
    /**
     * キーイベントから修飾キーの列挙型を生成します。
     * 
     * @param e キーイベント
     * @return キーイベントの修飾キーに対応する列挙型
     */
	public static ModifierKey of(NativeKeyEvent e) {
		String modifire = NativeKeyEvent.getModifiersText(e.getModifiers()).toUpperCase();
		if (modifire.contains(CTRL.name()) 
				&& modifire.contains(ALT.name())
				&& modifire.contains(SHIFT.name())) {
			return CTRL_ALT_SHIFT;
		} else if (modifire.contains(CTRL.name()) 
				&& modifire.contains(ALT.name())) {
			return CTRL_ALT;
		} else if (modifire.contains(CTRL.name()) 
				&& modifire.contains(SHIFT.name())) {
			return CTRL_SHIFT;
		} else if (modifire.contains(ALT.name()) 
				&& modifire.contains(SHIFT.name())) {
			return SHIFT_ALT;
		} else if (modifire.contains(CTRL.name())) {
			return CTRL;
		} else if (modifire.contains(ALT.name())) {
			return ALT;
		} else if (modifire.contains(SHIFT.name())) {
			return SHIFT;
		} else {
			return NONE;
		}
	}
}
```


##2.NativeKeyListenerの拡張
インターフェースを少し拡張した抽象クラスを作成します。

キーを押しっぱなしによるキャプチャの大量出力を防ぐための制御をしています。
入力キーの判定と実際の処理を抽象メソッドにして具象クラス側でキーの割り当て的なこととやりたいことを定義します。


```ruby:AbstractNativeKeyListener.java
package application.hook.listener;

import java.util.HashSet;
import java.util.Set;
import java.util.function.Predicate;

import org.jnativehook.keyboard.NativeKeyEvent;
import org.jnativehook.keyboard.NativeKeyListener;

import application.constants.ModifierKey;

/**
 * キー入力リスナーの抽象クラス
 * <p>
 * 押下中のキーに対応する処理がが複数回実行されないための制御を実施します。
 *
 */
public abstract class AbstractNativeKeyListener
        implements Predicate<NativeKeyEvent>, NativeKeyListener {
    /** 入力中のキー */
    private static final Set<Integer> KEEP_KEY = new HashSet<>();

    @Override
    public boolean test(NativeKeyEvent e) {
        return isKeyPressed(e.getKeyCode(), ModifierKey.of(e));
    }

    /**
     * キーが押下中かを判定します。
     * 
     * @param keyCode キーコード
     * @return 入力中のキーである場合{@code true}
     */
    public boolean isKeeping(int keyCode) {
        return KEEP_KEY.contains(keyCode);
    }

    /**
     * 入力中のキーを追加します。
     * 
     * @param keyCode キーコード
     */
    private void addKeepKey(int keyCode) {
        System.out.println("pressed key is " + keyCode);
        KEEP_KEY.add(keyCode);
    }

    /**
     * 入力中のキーを開放します。
     * 
     * @param keyCode キーコード
     */
    private void releaseKey(int keyCode) {
        System.out.println("released key is " + keyCode);
        KEEP_KEY.remove(keyCode);
    }

    @Override
    public final void nativeKeyPressed(NativeKeyEvent e) {
        // 押下中のキーを複数回呼ばれないようにする。
        if (isKeeping(e.getKeyCode())) {
            return;
        }
        addKeepKey(e.getKeyCode());
        if (test(e)) {
            execute();
        }
    }

    @Override
    public void nativeKeyReleased(NativeKeyEvent e) {
        // 押下中のキーを開放する。
        releaseKey(e.getKeyCode());
    }

    @Override
    public final void nativeKeyTyped(NativeKeyEvent e) {
        // 何もしない。（オーバーライド不可にする。）
    }

    /**
     * キー入力を判定します。
     * <p>
     * 指定したキーが入力されているかを判定します。
     * 
     * @param keyCode     キーコード
     * @param modifierKey 修飾キー
     * @return 入力キーが一致する場合{@code true}
     */
    protected abstract boolean isKeyPressed(int keyCode, ModifierKey modifierKey);

    /**
     * キー入力に対応する本体処理を実行します。
     */
    protected abstract void execute();
}
```


##3.画面キャプチャを取得するクラスの作成
2で作成したクラスを継承して画面キャプチャを取得するための処理を実装します。
キー入力は`Shift+Ctrl+F11`にしています。※F11だけだとWindows側でも反応してしまうため。
画面キャプチャの取得は以下で記載した内容とほぼ同じです。
https://qiita.com/mamamap/items/76f8c7cdf11a150c7234

※出力パスは固定となっていますが今後、任意で変更できるようにする予定です。


```ruby:ScreenCaptureKeyListener.java
package application.hook.listener;

import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;

import javax.imageio.ImageIO;

import org.jnativehook.keyboard.NativeKeyEvent;

import application.constants.ModifierKey;
import application.form.TaskTrayItem;
import application.util.FileUtil;

/**
 * 画面キャプチャを取得するキーイベントクラス
 */
public class ScreenCaptureKeyListener extends AbstractNativeKeyListener {

    @Override
    protected boolean isKeyPressed(int keyCode, ModifierKey modifierKey) {
        return keyCode == NativeKeyEvent.VC_F11 && modifierKey == ModifierKey.CTRL_SHIFT;
    }

    @Override
    protected void execute() {

        // TODO : パス情報は設定ファイルで読み込み可能とする。（編集もできるようにする。）
        File dir = new File("C:\\capture");
        if(!dir.exists()) {
            dir.mkdir();
        }
        File file = FileUtil.createUniqueFile(dir, "capture.png");

        try {
            Robot robot = new Robot();
            Rectangle screenSize = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
            BufferedImage screenShot = robot.createScreenCapture(screenSize);
            ImageIO.write(screenShot, "png", file);
        } catch (Exception e) {
            // 出力失敗時はWindows通知にてユーザに通知する。
            TaskTrayItem.getInstance().displayMessage("キャプチャの出力に失敗しました。");
        }
    }
}

```


##4.入力キーに対応するイベント呼び出すクラス
今後様々なショートカットキーを追加することを考慮してキーイベントのクラスをまとめて管理するようなクラスを作成します。

```ruby:NativeKeyListenerExecutor.java
package application.hook;

import java.util.List;
import java.util.Optional;

import org.jnativehook.keyboard.NativeKeyEvent;
import org.jnativehook.keyboard.NativeKeyListener;

import application.hook.listener.AbstractNativeKeyListener;

/**
 * 入力キーに対応する処理を特定するクラス
 */
public class NativeKeyListenerExecutor implements NativeKeyListener{

	/** キー入力イベントのリスト */
	private final List<AbstractNativeKeyListener> nativekeyListeners;

	/**
	 * キー入力イベントのリストを引数に新規構築します。
	 * 
	 * @param nativekeyListeners キー入力イベントのリスト
	 */
	public NativeKeyListenerExecutor (List<AbstractNativeKeyListener> nativekeyListeners) {
		this.nativekeyListeners = nativekeyListeners;
	}
	
	
	@Override
	public void nativeKeyPressed(NativeKeyEvent e) {
		
		Optional<AbstractNativeKeyListener> listener = nativekeyListeners.stream().filter(instance -> instance.test(e)).findAny();
		listener.ifPresent((l) -> l.nativeKeyPressed(e));
	}

	@Override
	public void nativeKeyReleased(NativeKeyEvent e) {
		Optional<AbstractNativeKeyListener> listener = nativekeyListeners.stream().findFirst();
		listener.ifPresent((l)-> l.nativeKeyReleased(e));
	}
	
	@Override
	public void nativeKeyTyped(NativeKeyEvent e) {
	    // 何もしない。
	}
}
```


##5.キーフックの登録
要となるキーフックの登録と4で作成したイベントの登録をします。
このクラスの作成ができればショートカットキーでキャプチャの出力ができるようになります。

```ruby:NativeKeyListenerExecutor.java
package application.hook;

import org.jnativehook.GlobalScreen;
import org.jnativehook.NativeHookException;

public class NativeKeyhook {
    
    /** キー入力イベント実行イベント */
    private NativeKeyListenerExecutor nativeKeyListenerExecutor;

    /**
     * {@link NativeKeyListenerExecutor}を引数に新規構築します。
     * 
     * @param nativeKeyListenerExecutor キー入力イベント実行イベント
     */
    public NativeKeyhook(NativeKeyListenerExecutor nativeKeyListenerExecutor) {
        this.nativeKeyListenerExecutor = nativeKeyListenerExecutor;
    }

    /**
     * グローバルキーフックを登録します。
     */
    public void resisterGlobalKeyhook() {
        if (GlobalScreen.isNativeHookRegistered()) {
            return;
        }
        try {
            GlobalScreen.registerNativeHook();
            GlobalScreen.addNativeKeyListener(nativeKeyListenerExecutor);
        } catch (NativeHookException e) {
            // キーフックの登録が失敗した場合はアプリを終了する。
            System.exit(-1);
        }
    }
}
```

##6.タスクトレイツールのメニューアイテム
ここからタスクトレイに常駐するツールの作成となります。
まず、終了するためのメニューアイテムを作成します。

```ruby:CloseMenuItem.java
package application.form.menuItem;

import java.awt.MenuItem;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
 * 終了メニューアイテムクラス
 */
public class CloseMenuItem extends MenuItem {
    /**
     * シリアライズバージョン
     */
    private static final long serialVersionUID = 1L;

    public CloseMenuItem() {
        super("終了");
        addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                // アプリを終了する。
                System.exit(0);
            }
        });
    }
}
```


##7.タスクトレイツールのメニュー
6で作成したメニューアイテムを持つメニューを作成します。
メニューアイテムを増やしたい場合は独自で作成して本クラスに追加してあげます。

```ruby:TaskTrayMenu.java
package application.form;

import java.awt.PopupMenu;

import application.form.menuItem.CloseMenuItem;

public class TaskTrayMenu extends PopupMenu {
    /**
     * シリアライズバージョン
     */
    private static final long serialVersionUID = 1L;

    public TaskTrayMenu() {
        addMenuItmes();
    }

    /**
     * コンテキストメニューの配置します。
     */
    private void addMenuItmes() {
        this.add(new CloseMenuItem());
    }
}

```


##8.タスクトレイに表示するためのアイテム
タスクトレイに常駐するためのオブジェクトを作成します。
アイコンのイメージはリソースから取得するため、各自で好きなファイルを配置してください。

```ruby:TaskTrayItem.java
package application.form;

import java.awt.Image;
import java.awt.TrayIcon;
import java.io.IOException;
import java.net.URL;

import javax.imageio.ImageIO;

/**
 * タスクトレイに常駐するクラス
 */
public class TaskTrayItem extends TrayIcon {
    /** 本クラスのインスタンス */
    private static TaskTrayItem instance = new TaskTrayItem(getResourceImage());

    /**
     * インスタンスを取得します。
     * 
     * @return インスタンス
     */
    public static TaskTrayItem getInstance() {
        return instance;
    }

    /**
     * イメージを引数として新規構築します。
     * <p>
     * シングルトンクラスのため、外部公開はしません。
     * 
     * @param image イメージ
     */
    private TaskTrayItem(Image image) {
        super(image);
        setPopupMenu(new TaskTrayMenu());
    }

    /**
     * メッセージを表示します。
     * <p>
     * Windowsの通知を利用したメッセージを表示します。
     * 
     * @param text メッセージ内容
     */
    public void displayMessage(String text) {
        super.displayMessage("message", text, MessageType.INFO);
    }

    /**
     * リソースからイメージを取得します。
     * 
     * @return タスクトレイのアイコンに表示するイメージ
     */
    private static Image getResourceImage() {
        Image image = null;
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        URL resource = loader.getResource("images/icon.png");
        try {
            image = ImageIO.read(resource);
        } catch (IOException e) {
            // イメージを取得できない場合はアプリを終了する。
            System.exit(999);
        }
        return image;
    }
}

```

##9.起動クラス

いつものです。
タスクトレイの設定とキーフックの設定をします。
起動したらWindowsの通知でメッセージが表示されます。

```ruby:ApplicationMain.java
package application;

import java.awt.AWTException;
import java.awt.SystemTray;
import java.util.Arrays;
import java.util.List;
import java.util.logging.LogManager;

import application.form.TaskTrayItem;
import application.hook.NativeKeyListenerExecutor;
import application.hook.NativeKeyhook;
import application.hook.listener.AbstractNativeKeyListener;
import application.hook.listener.ScreenCaptureKeyListener;

public class ApplicationMain {
    public static void main(String[] args) {

        // キーフックのコンソールメッセージを抑止する。
        LogManager.getLogManager().reset();
        init();
        try {
            SystemTray.getSystemTray().add(TaskTrayItem.getInstance());
        } catch (AWTException e) {
            // タスクトレイに配置失敗した場合はアプリを終了する。
            System.exit(99);
        }
        TaskTrayItem.getInstance().displayMessage("起動したよ!");
    }

    /**
     * キーフックの登録をします。
     */
    private static void init() {
        
        List<AbstractNativeKeyListener> keyListeners = Arrays.asList(new ScreenCaptureKeyListener());
        NativeKeyListenerExecutor executor = new NativeKeyListenerExecutor(keyListeners);
        NativeKeyhook hook = new NativeKeyhook(executor);
        hook.resisterGlobalKeyhook();
    }
}
```


実行してみる
-

小さいですがこのような感じで表示されるようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1333971/9f6bac59-fff8-053a-234b-2e964d75f912.png)

キャプチャも出力ができました。
キー入力で簡単に出力までできるようになったので実用性が出てきたと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1333971/93296b85-0231-adaf-668f-ac9ae596be6e.png)


一言
-
画像キャプチャだとsnipping toolとかフリーツールが便利だったりしますのでもう少し手を加えたいところです。
今後は画面キャプチャ以外のショートカットを増やしていこうと思います。（あると便利的なもの）

あと、MarkDownをすらすらかけるようになりたい。