Java
Windows
MacOSX

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

はじめに

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

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

システムトレイ

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

環境

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

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

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

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

本記事では、いらすとや様の「あ」の文字イラストをサンプルアイコンicon.pngとして使用します。

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

コード

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();
        }
    }
}

実行結果

実行結果

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

-Dapple.awt.UIElement=true

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

java -Dapple.awt.UIElement=true IconDemo

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

ツールチップ

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

※1 TrayIcon

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

/*
第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がシステムトレイを操ることができるかを確認することができます。

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を利用されている場合は、下記のように起動します。

java -Djava.awt.headless=false IconDemo

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

/***** 変更する前 *****/
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とは少しだけ書き方を変えています。

コード

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の場合はクリックしてみましょう。

実行結果

「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のitem1actionPerformed()メソッドの中身を以下の通り変更します。

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」メニューをクリックしてみます。

実行結果

残念ながら、Macでは表示されませんでした。Macの場合、Macの通知センターを利用してメッセージを表示するようですが、Javaから直接利用する方法は無いようです。こちらで回答されている方が、解決法を紹介してくれていますが、未検証です。

まとめ

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

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

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