JavaFX＋Kotlinでデスクトップマスコットを作ってみる。【マスコットの表示編】

#変更履歴

- 2020/2/23
    - タイトルを**Javafx**から**JavaFX**に変更
- 2020/2/26
    - 次の記事へのリンクを追加
- 2021/3/28
    - リンクを目次へ修正







# 初めの初め
この記事の内容はJavaFX+Kotlinからjavaのみを使用するように変更されました。
[リンク](https://qiita.com/urado/items/4a810c457cb862e370e8)から、関連の記事に移るようにしてください。









#初めに
初めて触ったPCはwindowsXP。
その時にいたあいつ。アザラシみたいなやつ。
（調べてみると東芝のPCに入っているデスクトップアプリケーションみたいです）

あいつを見たときに思いました。
「こういうの面白いな、こういうの作ってみたい」と。（小学生のときの感想ですが）

最近新しい言語を触りたいなと思った時、上記の記憶がふとよぎりました。３年前にWPFでデスクトップマスコットを作ったりしましたが、途中で手付かずになってしまったので、新しい言語を触るついでに作り直してみようと思い立ったわけです。

使用する言語はとりあえず前から気になっていたkotlinを使用してみることにしました。他にも気になる言語はありましたがそれはまた別の機会に触れようと思います。

#環境
- macOS Catalina 10.15.2
- IntelliJ IDEA、JavaFX Scene Builder 8.5.0

eclipseのkotlinプラグインを使用してみたりしましたが、コード補完などjavaだと使用できる機能等が使用できなかったためIntelliJを使用することにしました。

全部使用したことがないもののため、ヘルプを見ながら使用しています・・・。

#プロジェクト作成

IntelliJを起動して新規でプロジェクトを作成します。
ファイル→新規→プロジェクトからJavaのkotlin/JVMを選択して次へ。
<img width="1297" alt="スクリーンショット 2020-02-02 15.56.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/02e59a2f-e20e-36ad-315b-79441a21690f.png">
プロジェクト名は適当に決めて完了。
<img width="1297" alt="スクリーンショット 2020-02-02 15.58.15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/67e75b4c-b5fe-bbfa-f6fc-f0fd6edd6b50.png">
プロジェクト完成。
<img width="1680" alt="スクリーンショット 2020-02-02 16.00.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/3a6988a1-e5f8-b068-b18c-c8127818db96.png">
新規でモジュールを追加します。
<img width="1677" alt="スクリーンショット 2020-02-02 16.03.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/e41dcdbf-989a-0b97-fcd1-5d4b542b1b27.png">
モジュール名を入力してOK。
<img width="1118" alt="スクリーンショット 2020-02-02 16.05.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/1fea1066-b0c5-cae3-f751-ba1069a24cf1.png">
とりあえず、共通系（common）と実際のアプリを入れる方（test：名前は後で変えようと思います)を作成します。
<img width="1118" alt="スクリーンショット 2020-02-02 16.08.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/c1ec0f14-7e98-ffd4-3e82-b24c724e5730.png">
モジュールができたらtestの方にパッケージを作成します。
<img width="1677" alt="スクリーンショット 2020-02-02 16.10.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/c3ef35ae-0f33-824e-1362-045f477a50bd.png">
同じような手順でディレクトリの作成を行い、右クリックから「ディレクトリをマーク」でリソースルートとテストソースのルートを設定をします。テストソースにはパッケージも作成しておきます。
<img width="1677" alt="スクリーンショット 2020-02-02 16.36.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/46961490-a3e2-371c-16d1-ed8cdccbf2e2.png">
<img width="1677" alt="スクリーンショット 2020-02-02 16.37.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/8ac57e3c-a82d-0112-d2a3-52413ac072bf.png">
最終形。（なんか色々見えてますが気にしない。）
<img width="1680" alt="スクリーンショット 2020-02-03 23.18.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/93122baa-331b-e08e-667d-a98c3fb82d26.png">

#マスコット表示用の画面を作成

すごい昔に購入したデスクトップマスコット作成の本では透明のウィンドウを作成して画像を表示するような作成方法をとっていたためWPFで作成した際も透明ウィンドウで実現しました。今回も同様の方法で行っていきます。

JavaFXでGUIを作成するため、画面はfxmlで作成します。
右クリックの新規から「FXMLファイル」を選択します。ファイル名は適当につけます。
<img width="1676" alt="スクリーンショット 2020-02-02 16.42.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/dfb30524-f364-78b4-15c7-0e0b1df36327.png">
作成したファイルを右クリックで「SceneBuilderで開く」を選択。
（最初に開く場合はアプリの場所を聞かれるので、対象の場所を選択する）
<img width="1672" alt="スクリーンショット 2020-02-02 16.43.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/616df003-c07e-8181-f0c9-c3d5b0582841.png">
<img width="1680" alt="スクリーンショット 2020-02-02 16.44.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/5f2d03c2-faf2-3576-0cd3-58e8dcd786ae.png">
元々あるAnchoPaneは削除し、左のオブジェクトがいっぱいあるところからPaneに変更。
その子要素としてImageViewを追加。
（画像を表示するだけなので、Paneはなんでもいいような気がしますがプレーンな感じのものを選びました）

右のImageの「...」ボタンをクリックして表示する画像を選択します。
ここではテスト用画像（けものフレンズのキタキツネちゃん）を選択。

画像はアプリ内で使用するもののため先ほど作成したresources配下にidleというフォルダを作成して格納したものを指定しています。
<img width="1680" alt="スクリーンショット 2020-02-02 16.47.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/5fee7f45-cae8-403b-dc2d-c65b25af4f54.png">
ウィンドウ幅はいい感じに合わせておきます。
<img width="1680" alt="スクリーンショット 2020-02-02 17.24.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/bca6fc45-019a-90bd-181f-9f06556ee8de.png">
ここまでできたら保存して、IntelliJに戻ります。
戻ってきたらこんな感じ。今回開発で使用するJavaFXについて、使用しているJDKがJava11であるため、標準ライブラリには含まれず別となっているため、個別にクラスパスを追加する必要があるそうです。
（ウィンドウの上あたりにでている文章がそのままの意味です。）
<img width="1680" alt="スクリーンショット 2020-02-02 16.51.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/39ca25ce-4866-9989-a1f4-58a03c22b32d.png">
ライブラリ自体は事前にダウンロードしたものがあるため、そいつをクラスパスに通します。
（ダウンロードする手順とかはチョロなので割愛します）

「モジュールの設定を開く」を選択。
<img width="1674" alt="スクリーンショット 2020-02-02 16.54.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/7d093b04-cf85-52f8-504f-5b82abdeae1d.png">
ライブラリーの＋ボタンをクリックしてJavaを選択。
<img width="1676" alt="スクリーンショット 2020-02-02 16.55.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/d9ca74f6-a014-929b-24d0-7f036723f553.png">
対象のjarファイルが入っている場所を選択したらクラスパスに通す対象を選ぶダイアログが出るので、選択してOKをクリック。
<img width="476" alt="スクリーンショット 2020-02-02 16.55.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/24a3a68c-e3ef-5b3a-86cc-e7e05a91fde4.png">
エラー文が消えました。
画像ではtest直下にディレクトリを置いてそこに格納してクラスパスを通しましたが、commonの方でも使用するので後でプロジェクト直下に置き直してクラスパスを変更しました。
<img width="1680" alt="スクリーンショット 2020-02-02 16.56.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/58328c6f-2f7d-9bf1-7f82-d57c4db1cf75.png">

とりあえず、ここまでで画像を表示する画面の作成は完了しました。

#ウィンドウを透過してマスコットを画面に表示する
作成したFXMLをロードして画面を構築するコードを書いていきます。
画像を構築するクラスをパッケージdmms.view配下にクラスを作成します。

クラスの宣言ではApplicationクラスを継承します。
アプリケーションを起動した際に起動するstartメソッドに画面構築のコードを記述します。

```kotlin:Test.kt
class Test : Application() {
    override fun start(stage: Stage) {
        // FXMLファイルのローダーを作成。入力には先ほど作成したFXMLファイルの場所を指定。
        val loader = FXMLLoader(javaClass.getResource("/dmms/view/fxml/test.fxml"))
        // FXMLをロード
        val parent = loader.load<Parent>()
        // 読み込み対象のコントローラを取得 ロードした後じゃないと取れない
        val controller = loader.getController<TestController>()
        // stageを渡しておく
        controller.stage = stage
        // シーンの作成
        val scene = Scene(parent)
        // sceneの背景色を透明にする
        scene.fill = Color.TRANSPARENT
        // ウィンドウに作成したsceneを設定
        stage.scene = scene
        // ウィンドウ枠の装飾をなくす。
        stage.initStyle(StageStyle.TRANSPARENT);
        // 画面表示
        stage.show()
    }
}
```



基本的には作成したFXMLを読み込みStage（ウィンドウ）に設定するという内容です。ウィンドウの枠を消したり背景色を透明にしたりするコードもおよそ２行程度で実現できました。FXMLのファイルパスが違うのは後でクラスファイルとFXMLファイルの場所を変えたくなったので変更しています。記載誤りではないですよ？

続いてコントローラクラスを作成します。
パッケージdmms.controller配下にクラスを作成します。

```kotlin:TestController.kt
class TestController : Initializable {

    /** コントロール対象の画面 */
    lateinit var stage : Stage
    /** ウィンドウクリック位置X */
    private var clickPointX : Double = 0.0
    /** ウィンドウクリック位置Y */
    private var clickPointY : Double = 0.0

    /**
     * 初期化処理
     */
    override fun initialize(p0: URL?, p1: ResourceBundle?) {

    }

    @FXML
    fun moveWindow(e : MouseEvent) {
        // PCのモニターで見たときの位置X
        stage.x = e.screenX - clickPointX
        // PCのモニターで見たときの位置Y
        stage.y = e.screenY - clickPointY
    }
    @FXML
    fun setClickPoint(e : MouseEvent) {
        // scene内のクリック位置：X座標を取得
        clickPointX = e.sceneX
        // scene内のクリック位置：Y座標を取得
        clickPointY = e.sceneY
    }
}
```

Initializableインターフェースを実装することで画面起動時に処理を行うことができますが、とりあえず実装を書いただけで今は使用していません。stageについては先ほどTestクラスからコントローラクラスにstageを渡す箇所があったと思います。

今回のマスコットはマウスで移動ができるようにするつもりのため、マウスで移動ができる処理を作る必要があります。ウィンドウの位置は「モニター上から見たときのイベント位置」から「ウィンドウから見たときのイベント位置」を引き算すると現在のウィンドウ位置相当の値が計算できると思います。マウスのドラッグイベント（マウスの左クリックをした状態で動かす）が発生したタイミングでウィンドウ位置を再計算して新たなウィンドウ位置として適応すればマウスで移動させることが可能になります。

上記を踏まえて、setClickPoint()ではクリックイベントが発生した際に、ウィンドウ内のイベント発生位置を変数に記録します。moveWindow()ではドラッグイベントが発生した際にモニター上から見たイベント位置から記録したイベント位置を引き算して新たなウィンドウ位置として設定しています。これらのメソッドはまだ定義しただけのためFXMLファイルに該当するイベント発生時に上記のメソッドを呼ぶように記述を加える必要があります。
（FXMLとクラスのメンバやメソッドを対応づけするためには@FXMLアノテーションを付加する必要があります）

```XML:test.fxml
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.Pane?>

<Pane stylesheets="@../../css/sample.css"
      xmlns="http://javafx.com/javafx/8.0.171"
      xmlns:fx="http://javafx.com/fxml/1"
      fx:controller="dmms.controller.TestController"
      onMouseDragged="#moveWindow"
      onMousePressed="#setClickPoint">
   <ImageView fitHeight="328.0" fitWidth="199.0" pickOnBounds="true" preserveRatio="true">
      <Image url="/idel/image1.png" />
   </ImageView>
</Pane>
```

fx:controllerには先ほど作成したコントローラクラスを指定。onMouseDraggedはドラッグイベントのため、moveWindow()を指定（メソッド名の前に#をつける）onMousePressedはクリックイベントのため、setClickPoint()
を指定します。この辺の設定はSceneBuilderでもできますが、今回は直接書きました。 SceneBuilderで行う場合は右の方にCodeという欄があるとおもいますのでそちらに同じように記述をするとできます。

###ImageViewのファイルパスについて
SceneBuilderの編集時には触れませんでしたが、SceneBuilderで設定したファイルパスはあくまで端末上でのファイルパスになります。アプリケーションを実行した際に同じファイルパスで実行すると画像が見つからずに何も表示されない状態になります。今回の画像ファイルはresourcesの中に入れましたがこのディレクトリはリソースディレクトリとしてマークしています。そのため、ビルドした際にはsrcディレクトリをルート（パス上では「/」で始まるやつ）として見たときのパスにしておく必要があります。（あると思います。多分。もしかしたら相対パスで書く方法もあるかもですが・・・）

Controllerクラスのソースでもスルーしていましたが、以下の記述も同様の理由でsrcをルートとして以降のパスを記述しています。

>*javaClass.getResource("/dmms/view/fxml/test.fxml")*

ちなみにこのパスで保存してSceneBuilderで開き直すと画像にバツがついて表示できなくなります。（まぁ、そうですよね）このパスはリソースとして取り込んでいるものを対象にしているため、実際に動くアプリケーション（jarファイルとか）の一個上みたいな相対パスで見たい時はこのような指定ではできないため、どこのファイルを見るのかを最初にきめておく必要がありますね。リテラルでおかずに変数にして外部から変更できるようにするなども一つの手段かもしれません。（仕様によっては後でそうするかもしれません）

#起動してみる
必要なコードは記載したので、実際に起動して見ます。
とはいってもmainメソッドをどこにおくかを考えていなかったのでとりあえずテストソースの方に起動する記述を書きました。（構想としてはマスコットのjarを作って別の処理から呼ばれるような感じで考えていますが、中にmainメソッドを書いてもいいような気がしてきました）

```Kotlin:startup.kt
/**
 * テスト用メソッド
 */
fun main(args: Array<String>) {
    // 指定した画面レイアウトを起動する
    Application.launch(Test().javaClass)
}
```

とりあえず、これだけ。右クリックで「実行」を選択します。
<img width="1677" alt="スクリーンショット 2020-02-05 22.49.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/1f96b193-7bf3-73e3-3c05-b2d2b55fafe9.png">
無事に起動でき、マウスで操作できました。
![qiita_20200205.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/555408/bca2e610-d044-81ab-4095-a986e2bc20ea.gif)

#終わりに
記事書くの大変。コード丸あげしたほうが楽じゃないか。と思いつつも自分の書いたコードを再確認するとより理解が深くなったりして振り返りとしていいかもしれないと思いました。
仕事ではこんなに時間かけて振り返ったりしないですよね。（しますか？）

ここまでの内容はWPFのコードを参考に持ってきた感じなのでそんなに問題は起きずに来れました。（WPFのほうがもっと簡単でしたが・・・）こっから先はできるかどうか分からないので、四苦八苦しながら作ることになりそう。途中で頓挫しないように頑張りたいです。

とりあえず、今のままだと実行しても終了できないので次はそこを作っていこうと思います。
ちなみにキタキツネの画像は私が描きました。

#リンク

- [デスクトップマスコットを作ってみる。【目次】](https://qiita.com/urado/items/4a810c457cb862e370e8)