JavaFXで作ったCalendarガジェットをJava SE 9に対応させる

# JavaFXで作ったCalendarガジェットをJava SE 9に対応させる

この記事は、[JavaFX Advent Calendar 2017](https://qiita.com/advent-calendar/2017/javafx)に投稿しようとして作成し始めたのですが、仕事とインフルエンザ罹患で時間がとれず、後日公開となったものです。

## はじめに

デスクトップの片隅に、時計やカレンダーなどのちょっとしたGUI機能を表示するデスクトップガジェットがあります。古くはX Window Systemのxclockやoclockなど、Windows Vista/7のガジェットなどです。しかし、Windowsのガジェットは、脆弱性を理由としてWindows 8以降は廃止されてしまいました。

* [ガジェットは廃止になりました～Microsoft Windowsのサポート](https://support.microsoft.com/ja-jp/help/13787/gadgets-have-been-discontinued)

そのため、Windows 8以降（10など）ではデスクトップガジェットが欲しければ別途そのようなプログラムを探して入手し使うということになります。フリーソフトウェアにデスクトップガジェット風なものがいくつもありますが、プログラマーなら自分で作ったガジェットを使ってみるのものよかろうと思い、JavaFXでちょっと作ってみました。このあたりは、JJUG CCC 2017 Springにおいて次のセッションで紹介しました。

* [JavaFXでデスクトップガジェット風プログラムを作る](https://www.slideshare.net/torutk/jjugccc2017spring-m6-javafx)

デスクトップガジェットとしては、時計とカレンダーが欲しかったので、時計ガジェットに続いてカレンダーガジェットを作ってきました。

この今回は、Java SE 8で作成してきたカレンダーガジェットをJava SE 9に対応させるという内容です。

## カレンダーガジェット

カレンダーガジェットは、JavaFXのDatePickerコントールを使ってデスクトップの片隅に小さいカレンダー（月間）を表示するものです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/39a4426a-7836-587e-31b2-498d8a527eba.png)

プログラム（ソースコード）のリポジトリは次です（Github）。
[CalendarGadget](https://github.com/torutk/CalendarGadget)

このプログラムはJava SE 8で作成しています。Java SE 8でカレンダーガジェットを作る道のりは、以下のブログに書いてきました。

* [JavaFXでカレンダー表示プログラムを作る（DatePickerのポップアップ利用）](http://d.hatena.ne.jp/torutk/20160702/p1)
* [JavaFXでカレンダー表示プログラムを作る（DatePickerのポップアップ利用）（続）](http://d.hatena.ne.jp/torutk/20160705/p1)
* [JavaFXでカレンダー表示プログラムを作る（DatePickerのポップアップ利用）（続々）](http://d.hatena.ne.jp/torutk/20160706/p1)
* [JavaFXでカレンダー表示プログラムを作る（DatePickerのポップアップ利用）（続々々）](http://d.hatena.ne.jp/torutk/20160707/p1)
* [JavaFXでカレンダー表示プログラムを作る（DatePickerのポップアップ利用）（続々々々）](http://d.hatena.ne.jp/torutk/20160709/p1)
* [JavaFXでカレンダー表示プログラムを作る（DatePickerのポップアップ利用）（続々々々々）](http://d.hatena.ne.jp/torutk/20160710/p1)
* [JavaFXでカレンダー表示プログラムを作る（DatePickerのポップアップ利用）（続々々々々々）](http://d.hatena.ne.jp/torutk/20160717/p1)

## カレンダーガジェットのJava SE 9対応項目

このカレンダーガジェットをJava SE 9に対応させるのですが、その際次の対応をしていくこととします。

1. DatePickerSkinクラスのパッケージ変更の対応
1. Java SE 9モジュールシステム（JPMS）に対応
1. スリープ・休止状態から復帰時の現在日対応

以下に対応の方針を簡単に記述します。その後で、各項目の対応詳細を記述します。

### DatePickerSkinクラスのパッケージ変更の対応

DatePickerコントロールのカレンダー表示部分は、DatePickerSkinクラスを利用していますが、これはJava SE 8からJava SE 9になる際にパッケージ名が

- com.sun.javafx.scene.control.skin

から、

- javafx.scene.control.skin

に変更となりました。めでたく内部APIから公開APIに格上げになりました。ただ、Java SE 8用と9用とでソースコードを変える必要があります。

### Java SE 9モジュールシステム（JPMS）に対応

Java SE 9からは、従来のクラスファイル・JARファイルをクラスパスで実行時に参照する方法に加えて、モジュールファイルをモジュールパスで実行時に参照する方法が追加されました。

ここでは、せっかくなのでモジュールに対応することとします。

### スリープ・休止状態の復帰時対応

カレンダーガジェットは、現在の日付が分かるよう今日の日付にマーキング（矩形で今日の日を囲って分かるように）しています。そのため、カレンダーガジェットを実行している途中で日付が変わるときは、今日の日付けにつけたマーキングを翌日に変更する必要があります。
Java SE 8で作成時は、カレンダーガジェット起動時にその日の終わりまでの時間を計算し、その時間経過後に今日の日付を更新する処理を実行するようにしています（ScheduledExecutorServiceを用いて実装しています）。

ところが、ガジェットを実行しているPCが途中でスリープや休止状態に入ると、この処理が所望のタイミングで発動しません。そこで、苦肉の策で1時間ごとに日付更新処理のチェックをするようにしています。

Java SE 9からは、PCがスリープや休止状態に入ったこと、およびスリープや休止状態から復元したことをイベントで知る機能が追加されました。

そこで、このイベントを活用することにします。

## DatePickerSkinのJava SE ９対応の実装

最初のJava SE 9対応事項であるDatePickerSkinのパッケージ名変更をします。

```diff:CalendarGadgetApp.java
- import com.sun.javafx.scene.control.skin.DatePickerSkin;
+ import javafx.scene.control.skin.DatePickerSkin;
```

## Javaモジュールシステムへの対応の設定

2つ目のJava SE 9対応事項であるJavaモジュールシステム対応をします。今回はNetBeansでの対応を中心に記述します。

### NetBeans IDE 9開発版を使用

Java SE 9(JDK 9)に対応するNetBeans 9は現時点でまだ正式リリースされていません。そこで、NetBeans 9の開発版を使用します。
http://bits.netbeans.org/download/trunk/nightly/latest/

NetBeans 9では、モジュールシステムに対応するプロジェクト種類はJava Modular Projectになります。JavaFXアプリケーションなどのプロジェクト種類は、モジュールシステムには対応していません。そこで、いったんプロジェクトを作り直します。

### 新規プロジェクト作成

空の作業用ディレクトリを作り、その下に新規プロジェクトを2つ作成します。

1. [ファイル]メニュー > [新規プロジェクト] > [Java] > [Java Modular Project]で、空の作業用ディレクトリの下にプロジェクト名[GadgetSupport]を作成
2. [ファイル]メニュー > [新規プロジェクト] > [Java] > [Java Modular Project]で、空の作業用ディレクトリの下にプロジェクト名[CalendarGadget]を作成

次は「新規プロジェクト」ウィザード画面の最初のステップです。カテゴリ[Java]に、[Java Modular Project]が追加されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/aa122052-e684-aa57-e9ec-c8dc5ea0c7bd.png)

次は「新規プロジェクト」ウィザード画面の次のステップです。プロジェクトを作成する場所とプロジェクト名を指定します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/13135b04-fd74-a51a-b20b-95f394e7513d.png)

Java Modular Projectは、1つのプロジェクトの下に複数のモジュールを収容できます。最初はモジュールが空です。今回は、作成した2つのプロジェクトにそれぞれ1つだけモジュールを収容することとします。

各プロジェクトの中にモジュールを1つ定義します。

1. [GadgetSupport]プロジェクトを右クリックし、[新規] > [Module]を選択し、モジュール名に[com.torutk.gadget.support]を入力します。
2. [CalendarGadget]プロジェクトを右クリックし、[新規] > [Module]を選択し、モジュール名に[com.torutk.gadget.calendar]を入力します。

次は、プロジェクトを選択してモジュールを追加する操作です。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/b0e2f5d6-a585-dda7-0fbd-1f5b16828922.png)

「新規モジュール（New Module）」画面では、モジュール名を入力します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/643d3ae7-601f-e0d2-474f-ca66512e7ba2.png)

モジュール名は、公開するパッケージのうち最も主要なパッケージ名を付けています（Java言語仕様 6.1の命名規約より）。

モジュールを作成すると、module-info.javaが生成されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/c5b6b3ff-5486-159b-d708-af1d3ef6327b.png)

今までのNetBeansプロジェクトの構成とは違って、プロジェクトの下にモジュールのフォルダが作成され、その下にclassesのフォルダが作成され、ここにソースファイルのパッケージが配置されます。module-info.javaはトップレベル（パッケージ名なし）に設けられるので、NetBeansでは<デフォルト・パッケージ>と表現される中に置かれています。

ディレクトリ・ファイル構成は次のようになります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/e8940baa-ca83-1128-19ae-7e5a50d65b82.png)

ここにソースファイルを持ってきます。ソースファイルを持ってくると、import文が軒並みエラーとなっています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/e5d24dea-ac97-de67-93c5-ebc730958479.png)

### モジュール定義の追加（GadgetSupport）

GadgetSupportプロジェクトのcom.torutk.gadget.supportモジュールのモジュール定義を記述していきます。

Java SE 9モジュールシステムでは、java.baseモジュールに属するクラス以外を使用する場合、モジュール定義ファイル（module-info.java）に使用するクラスが含まれるモジュールを明示的に記述する必要があります。

エラー行の先頭にある(:exclamation:)マークにカーソルを重ねるとエラーメッセージがポップされます。次の画面は、java.util.prefs.Preferencesのエラーをポップアップしたものです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/3376a0e8-a274-0453-64b0-a895332dcc84.png)

エラーメッセージに従い、module-info.javaに、java.prefsモジュールを追記します。

```java:module-info.java
module com.torutk.gadget.support {
    requires java.prefs;
}
```

同様に、残りのimport文のエラーに対応するモジュール使用記述を追記します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/b334b310-3bb2-d749-9e67-13e5bea181b8.png)

エラーメッセージからは、javafx.graphicsとjavafx.controlsの2つのモジュールが必要と分かります。この2つのモジュールをmodule-info.javaに記述してもよいのですが、javafx.graphicsとjavafx.controlsには推移的な依存関係があるので、アプリケーション側のモジュール定義ではjavafx.controlsだけ指定すれば解決します。


```java:module-info.java
module com.torutk.gadget.support {
    requires java.prefs;
    requires javafx.controls;
}
```

javafx.controlsのモジュール定義には次の記述があります。

```java
    requires transitive javafx.base;
    requires transitive javafx.graphics;
```

transitiveの指定があるので、javafx.controlsを使用するモジュールにはjavafx.baseとjavafx.graphicsの2つのモジュールも自動的に使用可能となります。

次に、GadgetSupportプロジェクトはユーティリティライブラリなので、利用するアプリケーション（CalendarGadgetなど）にパッケージcom.torutk.gadget.supportを公開する必要があります。

```java:module-info.java
module com.torutk.gadget.support {
    requires java.prefs;
    requires javafx.controls;
    exports com.torutk.gadget.support;
}
```

これでビルドを実施します。コンパイルされたクラスファイルはbuild/modules下に、JARファイルはdist下に生成されます。

```
GadgetSupport
  +-- build
  |     +-- modules
  |           +-- com.torutk.gadget.support
  |                 +-- module-info.class
  |                 +-- com
  |                       +-- torutk
  |                             +-- gadget
  |                                   +-- support
  |                                         +-- TinyGadgetSupport.class
  +-- dist
  :     +-- com.torutk.gadget.support.jar
```

### モジュール定義の追加（CalendarGadget）

CalendarGadgetプロジェクトのcom.torutk.gadget.calendarモジュールのモジュール定義を記述していきます。

今回は、Java SE 9標準API以外に、自分で作成したライブラリ（GadgetSupport）を使用するので、プロジェクト設定でライブラリの依存を追加します。GadgetSupportはモジュールとして作成したのでモジュールの追加で設定します。

CalendarGadgetプロジェクトを右クリックし、[プロパティ]を選択します。
左側ペインで[Libraries]を選択、右側ペインで[Compile]タブ、[Modulepath]の右端の[+]をクリックし、[Add Project...]を選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/33619cac-2880-c5a5-ac5b-afed0f91942b.png)

「プロジェクトの追加」画面が開くので、GadgetSupportプロジェクトを選択します。すると、プロジェクトが生成するJARファイルが表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100353/c5ebbf85-6d05-f498-5868-ea56078811b5.png)

モジュール定義ファイルに、依存するモジュールを記述します。

```java:module-info.java
module com.torutk.gadget.calendar {
    requires java.logging;
    requires java.prefs;
    requires javafx.controls;
    requires com.torutk.gadget.support;
}
```

コンパイルエラーは取れたので、実行してみます。すると、次のエラーが発生します。

```
run:
Exception in Application constructor
Exception in thread "main" java.lang.reflect.InvocationTargetException
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:564)
	at java.base/sun.launcher.LauncherHelper$FXHelper.main(LauncherHelper.java:945)
Caused by: java.lang.RuntimeException: Unable to construct Application instance: class com.torutk.gadget.calendar.CalendarGadgetApp
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:963)
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(LauncherImpl.java:198)
	at java.base/java.lang.Thread.run(Thread.java:844)
Caused by: java.lang.IllegalAccessException: class com.sun.javafx.application.LauncherImpl (in module javafx.graphics) cannot access class com.torutk.gadget.calendar.CalendarGadgetApp (in module com.torutk.gadget.calendar) because module com.torutk.gadget.calendar does not export com.torutk.gadget.calendar to module javafx.graphics
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
	at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:589)
	at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:479)
	at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$8(LauncherImpl.java:875)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(PlatformImpl.java:449)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(PlatformImpl.java:418)
	at java.base/java.security.AccessController.doPrivileged(Native Method)
	at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:417)
	at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
	at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
	at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:175)
	... 1 more

```

ここで、エラーの主原因となっているメッセージが次です。

```
com.sun.javafx.application.LauncherImpl (in module javafx.graphics) cannot access 
class com.torutk.gadget.calendar.CalendarGadgetApp (in module 
com.torutk.gadget.calendar) because module com.torutk.gadget.calendar does not export 
com.torutk.gadget.calendar to module javafx.graphics
``` 

JavaFXアプリケーションクラス CalendarGadgetApp がjavafx.graphicsモジュールからアクセス可能になっている必要があるという内容です。

次の文をモジュール定義ファイルに追加すれば実行できるようになります。

 opens com.torutk.gadget.calendar;

なお、今回はopensではなくexportsでも実行可能ですが、今後FXMLやバインディングを使用するときは、アプリケーション側のクラスにjavafxのモジュールからリフレクションアクセスが必要になるので、exportsではなくopensとする必要があります。そこで、JavaFXアプリケーションのモジュール定義では自身をopensで指定するようにしておきます。

## スリープ・休止状態から復帰時の現在日対応

3つのうち最後の対応項目です。

まず、OSのスリープ/休止状態からの復帰をイベントとして受け取ります。

```java
   private void initResumeProc() {
        Desktop desktop = Desktop.getDesktop();
        desktop.addAppEventListener(new SystemSleepListener() {
            @Override
            public void systemAboutToSleep(SystemSleepEvent e) {
                logger.info("Detect system about to sleep.");
            }

            @Override
            public void systemAwoke(SystemSleepEvent e) {
                logger.info("Detect system awake.");
                if (schedule != null ) {
                    schedule.cancel(true);
                }
                crossoverDate();
            }
        });
    }
```

java.awt.DesktopクラスにJava SE 9から追加されたaddAppEventListenerメソッドに、SystemSleepListenerインタフェースの実装クラスのインスタンスを渡します。すると、OSがスリープまたは休止状態（サスペンド）に入るとき、および復帰するときにSystemSleepListenerの対応するメソッドが呼ばれます。
今回はそのタイミングで日付更新処理（crossoverDateメソッド）を再度設定するようにします。
なお、日付更新処理のスケジュールが既にされている場合はいったんスケジュールを取り消しておきます。

## リポジトリ

今回のJava SE 9対応を入れたCalendarGadgetのリポジトリは次です。

https://github.com/torutk/CalendarGadget/tree/jdk9_module