Javaでシステムトレイにアイコン表示（Windows:通知領域/Mac:メニューバー）

# はじめに

最近は（趣味の領域で）Java Spring Bootフレームワークを使ってサーバサイドのコードをいろいろ書くことが多いのですが、ふと、システムトレイにアイコンを表示させて、常駐化させたいと思いました。情報系に無縁の方に、サーバ系アプリを配布することを考えると、ターミナル等のコンソールを出すのはあまり親切でないと考え、システムトレイのアイコンで、最低限の操作ができればいいなぁと思ったのがきっかけです。

この記事で言うシステムトレイとは、下記画像の赤色の部分です。

![システムトレイ](https://qiita-image-store.s3.amazonaws.com/0/84550/de25ba54-760f-93c7-a853-25bd4ee22845.png)

Windowsでいう「通知領域」、Macでいう「メニューバー」です。他のOSでも同様のものはあると思います。（今のWindowsは、もうタスクトレイという呼び方はしません・・・）

# 環境

今回は2つのOSで試してみます。

* OS: Windows 10 Pro / macOS High Sierra
* Java: 1.8

※言及の無い限りSpring Bootに特化したものは使用しません。

# STEP1:とりあえずアイコンを出す

本記事では、[いらすとや様の「あ」の文字イラスト](https://www.irasutoya.com/2013/02/50.html)をサンプルアイコン`icon.png`として使用します。

まず、ただアイコンを出すだけのコード。

## コード

```java:IconDemo.java

import java.awt.AWTException;
import java.awt.Image;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;

public class IconDemo {

    public static void main(String[] args) {
        IconDemo app = new IconDemo();
        app.run();
    }

    /**
     * システムトレイにアイコンを出すメソッド
     */
    private void run() {
        Image image = Toolkit.getDefaultToolkit().createImage( ClassLoader.getSystemResource("icon.png") ); // アイコン画像を準備
        TrayIcon icon = new TrayIcon(image, "Sample Java App"); // ※1 トレイアイコンとして生成
        icon.setImageAutoSize(true); // リサイズ
        
        try {
			SystemTray.getSystemTray().add(icon); // ※2 システムトレイに追加
		} catch (AWTException e) {
			e.printStackTrace();
		}
    }
}
```

## 実行結果

![実行結果](https://qiita-image-store.s3.amazonaws.com/0/84550/8af7025f-7707-b993-bcb5-551c6ce3dfeb.png)

このように追加されました。ただし、Macの場合は、DockにJavaのアイコンも表示されてしまいました。もし、これを意図しない場合は、プログラム起動時にVM引数として下記のオプションをつけることで回避できるようです。

```

-Dapple.awt.UIElement=true
```

つまり、素のjavaを利用されている場合は、下記のように起動します。

```bash

java -Dapple.awt.UIElement=true IconDemo
```

また、アイコンの上にマウスカーソルを重ねて見ましょう。

![ツールチップ](https://qiita-image-store.s3.amazonaws.com/0/84550/639991bb-596d-4ec2-985b-7df580c4f15d.png)

「Sample Java App」と表示されましたね。これをツールチップと呼び、上記のコードで指定文字列を表示するように指示しています。

## ※1 `TrayIcon`

`TrayIcon`クラスは、システムトレイに追加できるアイコンを表します。上記のコードでは、引数を2つとっていますが、最大で3つの引数をとることができます。

```java

/*
第1引数: image アイコンのイメージ（必須）
第2引数: tooltip アイコンの上にマウスカーソルを重ねると表示される文字列（任意）
第3引数: popup ポップアップメニュ（任意）この後のSTEPで使用
*/
TrayIcon icon = new TrayIcon(Image image, String tooltip, PopupMenu popup);
```

## ※2 `SystemTray`

`SystemTray`クラスは、各OSのシステムトレイを表します。`SystemTray.getSystemTray()`で、システムトレイの実体を取得し、それに対して`add()`メソッドを使って、トレイアイコンを追加します。

また、下記のコードのように、`SystemTray.isSupported()`メソッドを使って、そもそも実行環境において、javaがシステムトレイを操ることができるかを確認することができます。

```java:IconDemo.java

import java.awt.SystemTray;

public class IconDemo {

    public static void main(String[] args) {
        IconDemo app = new IconDemo();
        app.check();
    }
    
    /**
    * 実行環境がシステムトレイをサポートしているかどうかをチェック
    */
    private void check() {
        Boolean check = SystemTray.isSupported();
        System.out.println("Support status: " + check);
    }

}
```


ただし、これが`false`になったとして、**必ずしもシステムトレイを利用できないわけではない**ようです。ソースは自分です。私がSpring Bootで同様のことをしようとしたら、後述するHeadless Exceptionが発生し、かつ、`isSupported()`メソッドが`false`になりました。しかし、後述する対処法を行うことで、`false`のままでも表示させることができました。

## HeadlessExceptionが発生した場合

私の場合、Spring Bootを利用していると発生しました。原因は、AWTのヘッドレスモードが有効になっているからで、これが有効になっているとAWT関係の機能がほとんど利用できません。

VM引数で下記の通り指定することで、無効にすることができます。

```
-Djava.awt.headless=false
```

つまり、素のjavaを利用されている場合は、下記のように起動します。

```bash

java -Djava.awt.headless=false IconDemo
```


また、Spring Bootを使用している場合は、`main()`メソッドを下記のように変更することでも、無効にすることができます。

```java

/***** 変更する前 *****/
public static void main(String[] args) {
	SpringApplication.run(IconDemo.class, args);
}

/***** 変更した後 *****/
public static void main(String[] args) {
	SpringApplicationBuilder builder = new SpringApplicationBuilder(IconDemo.class);
	builder.headless(false).run(args);		
}
```

（ただ、Spring Boot等で、もともと有効になっていたものを無効にすることによる弊害は分かりません。。。）

# STEP2:メニューを表示させる

Windowsでは、アイコンを右クリックした場合、Macではアイコンをクリックした場合に表示されるメニューを作ります。また、各メニューをクリックしたときの動作も記述します。なお、STEP1とは少しだけ書き方を変えています。

## コード

```java:IconDemo.java

import java.awt.AWTException;
import java.awt.Image;
import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.SystemTray;
import java.awt.Toolkit;
import java.awt.TrayIcon;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class IconDemo {

    public static void main(String[] args) {
        IconDemo app = new IconDemo();
        app.run();
    }

    /**
     * システムトレイにアイコンを出すメソッド
     */
    private void run() {
        SystemTray tray = SystemTray.getSystemTray(); // システムトレイを取得
        PopupMenu popup = new PopupMenu(); // ※4 ポップアップメニューを生成
        Image image = Toolkit.getDefaultToolkit().createImage( ClassLoader.getSystemResource("icon.png") ); // アイコン画像を準備
        TrayIcon icon = new TrayIcon(image, "Sample Java App", popup); // ※4 トレイアイコンとして生成
        icon.setImageAutoSize(true); // リサイズ

        // ※3 ポップアップメニューの中身を作成
        MenuItem item1 = new MenuItem("Hello");
        item1.addActionListener(new ActionListener(){
            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("Hello world!!");
            }
        });

        MenuItem item2 = new MenuItem("Exit");
        item2.addActionListener(new ActionListener(){
            @Override
            public void actionPerformed(ActionEvent e) {
                tray.remove(icon);
                // System.exit(0);
            }
        });
        popup.add(item1); // ※4
        popup.add(item2); // ※4
        
        try {
            tray.add(icon); // システムトレイに追加
		} catch (AWTException e) {
			e.printStackTrace();
		}
    }
}
```
## 実行結果

Windowsの場合はアイコンを右クリック、Macの場合はクリックしてみましょう。

![実行結果](https://qiita-image-store.s3.amazonaws.com/0/84550/04c5ea9e-8cf1-2dcf-a8df-ae7dd96ba0ff.png)

「Hello」をクリックすると、コンソールに「Hello world!!」が表示され、「Exit」をクリックすると、プログラムが終了します。

## ※3 `MenuItem`

ポップアップメニューの中の、一つ一つの項目は`MenuItem`クラスで作成します。コンストラクタの引数には、メニュー上の表示名の文字列を指定します。

また、メニューをクリックした後の処理は`addActionListener()`メソッドを使って追加します。このあたりの書き方はGUIアプリケーションを作ったことのある方ならお馴染みでしょうか。`addActionListener()`メソッドの引数は`ActionListener`インターフェースを実装したインスタンスです。上記のコードでは無名クラスを使って実装しています。`ActionListener`インターフェースは、`actionPerformed(ActionEvent e)`メソッドを必ずオーバーライドする必要があり、このメソッドの中に、メニューがクリックされた後の処理を書きます。

「Exit」メニューには、プログラムを終了させる処理を書いています。無理やり終了させるには`System.exit(0);`を使ってもよいですが、あまり使うのはよろしくないようです。このプログラムでいえば、`tray.remove(icon);`と書いて、システムトレイからトレイアイコンを消すだけでも、プログラムは終了します。

## ※4 `PopupMenu`

`PopupMenu`クラスは、1つのポップアップメニューを表します。`TrayIcon`生成時に、第3引数でこのPopupMenuを指定することで、アイコンを右クリックorクリックしたときに、メニューを表示させることができます。

さきほど作った`MenuItem`は、このPopupMenuの`add()`メソッドを使うことで追加できます。

# STEP3:メッセージ（通知）を表示する

さきほどのSTEP2のコードでは、「Hello」メニューをクリックすると、コンソールに「Hello world!!」が表示されましたが、コンソールではなく、システムトレイのメッセージとして何か表示させてみましょう。

## コード

STEP2の`item1`の`actionPerformed()`メソッドの中身を以下の通り変更します。

```java

import java.awt.TrayIcon.MessageType; // importに追加

/**
* 変更する前
*/
@Override
public void actionPerformed(ActionEvent e) {
    System.out.println("Hello world!!");
}

/**
* 変更した後
*/
@Override
public void actionPerformed(ActionEvent e) {
    // ※5
    icon.displayMessage("サンプルプログラム", "こんにちは世界！これはサンプルメッセージです。", MessageType.INFO);
}
```

## 実行結果

「Hello」メニューをクリックしてみます。

![実行結果](https://qiita-image-store.s3.amazonaws.com/0/84550/c25aff64-71f5-21b8-f4c9-4f9a6b567314.png)

残念ながら、Macでは表示されませんでした。Macの場合、Macの通知センターを利用してメッセージを表示するようですが、Javaから直接利用する方法は無いようです。[こちら](https://stackoverflow.com/questions/11720652/using-10-8-notification-center-in-java)で回答されている方が、解決法を紹介してくれていますが、未検証です。

# まとめ

あまり今風なやり方では無いような気がぷんぷんしています。

もっといいやり方をご存知の方はお教えいただけると幸いです。

# 参考にさせていただいたページ

* [Javaでかんたんタスクトレイ常駐アプリ](http://d.hatena.ne.jp/fuzzhead/20081027/p1)
* [SystemTrayにアイコンを表示](https://ateraimemo.com/Swing/SystemTray.html)
* [How to make a Windows Notification in Java - stack overflow](https://stackoverflow.com/questions/34490218/how-to-make-a-windows-notification-in-java)
* [Spring Boot and AWT headless](http://iryndin.net/post/spring_boot_and_awt_headless/)
* [Java アプリで Mac OS X の Dock アイコンを消す方法](http://d.hatena.ne.jp/shootaroo/20091123/1258980198)
* [Using 10.8 Notification Center in Java](https://stackoverflow.com/questions/11720652/using-10-8-notification-center-in-java)