Help us understand the problem. What is going on with this article?

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

変更履歴

  • 2020/2/23

    • タイトルをJavafxからJavaFXに変更。
  • 2020/2/26

    • 次の記事へのリンクを追加。

初めに

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

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

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

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

環境

  • macOS Catalina 10.15.2
  • IntelliJ IDEA、JavaFX Scene Builder 8.5.0

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

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

プロジェクト作成

IntelliJを起動して新規でプロジェクトを作成します。
ファイル→新規→プロジェクトからJavaのkotlin/JVMを選択して次へ。
スクリーンショット 2020-02-02 15.56.19.png
プロジェクト名は適当に決めて完了。
スクリーンショット 2020-02-02 15.58.15.png
プロジェクト完成。
スクリーンショット 2020-02-02 16.00.40.png
新規でモジュールを追加します。
スクリーンショット 2020-02-02 16.03.31.png
モジュール名を入力してOK。
スクリーンショット 2020-02-02 16.05.25.png
とりあえず、共通系(common)と実際のアプリを入れる方(test:名前は後で変えようと思います)を作成します。
スクリーンショット 2020-02-02 16.08.32.png
モジュールができたらtestの方にパッケージを作成します。
スクリーンショット 2020-02-02 16.10.36.png
同じような手順でディレクトリの作成を行い、右クリックから「ディレクトリをマーク」でリソースルートとテストソースのルートを設定をします。テストソースにはパッケージも作成しておきます。
スクリーンショット 2020-02-02 16.36.27.png
スクリーンショット 2020-02-02 16.37.26.png
最終形。(なんか色々見えてますが気にしない。)
スクリーンショット 2020-02-03 23.18.00.png

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

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

JavaFXでGUIを作成するため、画面はfxmlで作成します。
右クリックの新規から「FXMLファイル」を選択します。ファイル名は適当につけます。
スクリーンショット 2020-02-02 16.42.27.png
作成したファイルを右クリックで「SceneBuilderで開く」を選択。
(最初に開く場合はアプリの場所を聞かれるので、対象の場所を選択する)
スクリーンショット 2020-02-02 16.43.34.png
スクリーンショット 2020-02-02 16.44.27.png
元々あるAnchoPaneは削除し、左のオブジェクトがいっぱいあるところからPaneに変更。
その子要素としてImageViewを追加。
(画像を表示するだけなので、Paneはなんでもいいような気がしますがプレーンな感じのものを選びました)

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

画像はアプリ内で使用するもののため先ほど作成したresources配下にidleというフォルダを作成して格納したものを指定しています。
スクリーンショット 2020-02-02 16.47.51.png
ウィンドウ幅はいい感じに合わせておきます。
スクリーンショット 2020-02-02 17.24.40.png
ここまでできたら保存して、IntelliJに戻ります。
戻ってきたらこんな感じ。今回開発で使用するJavaFXについて、使用しているJDKがJava11であるため、標準ライブラリには含まれず別となっているため、個別にクラスパスを追加する必要があるそうです。
(ウィンドウの上あたりにでている文章がそのままの意味です。)
スクリーンショット 2020-02-02 16.51.09.png
ライブラリ自体は事前にダウンロードしたものがあるため、そいつをクラスパスに通します。
(ダウンロードする手順とかはチョロなので割愛します)

「モジュールの設定を開く」を選択。
スクリーンショット 2020-02-02 16.54.02.png
ライブラリーの+ボタンをクリックしてJavaを選択。
スクリーンショット 2020-02-02 16.55.19.png
対象のjarファイルが入っている場所を選択したらクラスパスに通す対象を選ぶダイアログが出るので、選択してOKをクリック。
スクリーンショット 2020-02-02 16.55.54.png
エラー文が消えました。
画像ではtest直下にディレクトリを置いてそこに格納してクラスパスを通しましたが、commonの方でも使用するので後でプロジェクト直下に置き直してクラスパスを変更しました。
スクリーンショット 2020-02-02 16.56.18.png

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

ウィンドウを透過してマスコットを画面に表示する

作成したFXMLをロードして画面を構築するコードを書いていきます。
画像を構築するクラスをパッケージdmms.view配下にクラスを作成します。

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

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(ウィンドウ)に設定するという内容です。ウィンドウの枠を消したり背景色を透明にしたりするコードもおよそ2行程度で実現できました。FXMLのファイルパスが違うのは後でクラスファイルとFXMLファイルの場所を変えたくなったので変更しています。記載誤りではないですよ?

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

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アノテーションを付加する必要があります)

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メソッドを書いてもいいような気がしてきました)

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

とりあえず、これだけ。右クリックで「実行」を選択します。
スクリーンショット 2020-02-05 22.49.04.png
無事に起動でき、マウスで操作できました。
qiita_20200205.gif

終わりに

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

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

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

リンク

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした