読者です 読者をやめる 読者になる 読者になる

ほんじゃら堂

めんどくさい仕事をラクにする作業自動化レシピ集

昨日食べたものも思い出せなくなってきたので食事履歴記録アプリをつくる

clojure IT系・技術系 javafx

f:id:piro_suke:20160320014313j:plain

おじさんになると、

よっぽど興味を持ったこと以外はすぐに忘れてしまうようだ。

忘れるというよりも、覚えてるけどうまく思い出せない、というべきか。

今週ランチで食べたものを思い出してみようとしても、

昨日のメニューすらなかなか出てこないことがある。

頭で覚えられないならしかたない、頭の外でデータ化しよう、

ということで今回は食べたものを登録できるツールを作ってみる。

まずはシンプルな機能で

まずは食事内容を入力したらデータベースに登録してくれる

シンプルな機能として実装してみよう。

登録内容は

  • 年月日
  • 食事区分 (朝食/昼食/夕食)
  • 主食
  • 主菜(メインディッシュ)
  • 副菜(サブディッシュ。複数)

くらいで始める。

いつもはコマンドを流すだけのスクリプトを作ることが多いけど、

今回は入力しやすいようにGUIアプリに挑戦する。

こんな入力画面を作るイメージ:

f:id:piro_suke:20160806012411p:plain

Clojure + JavaFX + PostgreSQLで実装する

スマホアプリではなく、デスクトップアプリ。

環境は、

  • Java 8
  • Clojure 1.8
  • PostgreSQL 9.5

Clojureベースで作りたいので、JavaのGUIライブラリである

JavaFXと組み合わせる。

JavaFXはJava8から追加されたSwingに代わる標準GUIライブラリらしく、

初めて使ったけど、思ったより作りやすかった。

初心者のためのJavaFXプログラミング入門

JavaDoc API

JavaFX 8

HTML風のFXMLと呼ばれるレイアウト定義ファイルと独自のCSSを

組み合わせてレイアウトを外部化できる。

Scene Builderという無料ツールも提供されており、これを使うと

GUIでレイアウトを組み立てていくことができる。楽。

JavaFX Scene Builder Information

Scene BuilderでFXMLファイルを作成する

Scene Builderで上の入力画面イメージを作って保存したFXMLファイルがこちら。

app.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>


<VBox xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane prefHeight="253.0" prefWidth="574.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" maxWidth="282.0" minWidth="10.0" prefWidth="187.0" />
          <ColumnConstraints hgrow="SOMETIMES" maxWidth="391.0" minWidth="10.0" prefWidth="391.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints maxHeight="45.0" minHeight="10.0" vgrow="SOMETIMES" />
          <RowConstraints maxHeight="84.0" minHeight="10.0" vgrow="SOMETIMES" />
          <RowConstraints maxHeight="109.0" minHeight="10.0" vgrow="SOMETIMES" />
            <RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
            <RowConstraints maxHeight="154.0" minHeight="10.0" prefHeight="74.0" vgrow="SOMETIMES" />
            <RowConstraints maxHeight="104.0" minHeight="0.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="日付" />
            <Label text="食事区分" GridPane.rowIndex="1" />
            <Label text="主食" GridPane.rowIndex="2" />
            <Label text="メインディッシュ" GridPane.rowIndex="3" />
            <Label text="サイドディッシュ" GridPane.rowIndex="4" />
            <DatePicker fx:id="daySelect" GridPane.columnIndex="1" />
            <TextField fx:id="stapleTxt" GridPane.columnIndex="1" GridPane.rowIndex="2" />
            <TextField fx:id="mainDishTxt" GridPane.columnIndex="1" GridPane.rowIndex="3" />
            <ComboBox fx:id="mealDivSelect" prefWidth="150.0" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <Button fx:id="saveBtn" mnemonicParsing="false" text="登録" GridPane.columnIndex="1" GridPane.rowIndex="5" />
            <TextArea fx:id="sideDishesTxt" prefHeight="49.0" prefWidth="391.0" GridPane.columnIndex="1" GridPane.rowIndex="4" />
         </children>
      </GridPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

ここでのポイントは、

それぞれの入力項目と登録ボタンにつけた「fx:id」属性。

これをClojure側で参照してイベント処理したり入力値を取得したりする。

FXML側でイベント定義もできるのだけど、

今回はClojure側で処理しやすいようにレイアウト定義だけを行い、

イベントの設定はClojure側で行った。

データ登録用テーブルを作成する

テーブルは下記のように定義する。

サイドディッシュは複数登録できるように

PostgreSQLのJSONB型カラムにJSON配列として登録する。

create table meshilogs (
    id serial not null,
    day date not null default current_date,
    meal_div int not null,
    staple varchar(255) not null,
    main_dish varchar(255) not null,
    side_dishes jsonb default '[]'::jsonb,
    created timestamp not null default current_timestamp,
    primary key(id)
);

Leiningenプロジェクトを作成する

ここからメインのClojureコーディング。

食事履歴なので「meshilog」という名前で作成する。

project.clj

(defproject meshilog "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.8.0"]
                 [cheshire "5.6.3"]
                 [korma "0.4.2"]
                 [org.clojure/java.jdbc "0.4.2"]
                 [org.postgresql/postgresql "9.4-1203-jdbc42"]]
  :resource-paths ["resources"] ; 追加1
  :aot :all ; 追加2
  :main ^:skip-aot meshilog.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

JavaFXのライブラリはJava8標準ライブラリなのでここでは特別な指定は不要。

コメントの「追加1」の行を追加しておくと、

resourcesフォルダに置いたfxmlファイルを相対的パスで取得できる。

「追加2」の行はmain関数からApplication/launchした時に

指定したcljファイルのstart関数を呼び出すために必要。

続いて、メインのcljファイル。

meshilog/core.clj

(ns meshilog.core
  (:gen-class
     :extends javafx.application.Application)
  (:import 
     (java.sql Date)
     (javafx.application Application)
     (javafx.event ActionEvent EventHandler)
     (javafx.scene Scene)
     (javafx.scene.control Button)
     (javafx.scene.layout StackPane)
     (javafx.stage Stage)
     (javafx.fxml FXMLLoader)
     (javafx.collections FXCollections))
  (:require [clojure.java.io :as io])
  (:require [cheshire.core :as cheshire])
  (:require [korma.db])
  (:require [korma.core :as kc]))

(korma.db/defdb db
                {:user "<DBユーザー名>"
                 :password "<DBパスワード>"
                 :subname "//localhost:5432/<DB名>"
                 :port "5432"
                 :subprotocol "postgresql"})
(kc/defentity meshilogs)

(defn -start
  [this ^Stage stage]
  (let [root (-> "app.fxml"
               (io/resource)
               (FXMLLoader/load))
        day-select (.lookup root "#daySelect")
        meal-div-select (.lookup root "#mealDivSelect")
        staple-txt (.lookup root "#stapleTxt")
        main-dish-txt (.lookup root "#mainDishTxt")
        side-dishes-txt (.lookup root "#sideDishesTxt")
        save-btn (.lookup root "#saveBtn")
        meal-divs {:朝食 1
                   :昼食 2
                   :夕食 3}] 
    (doto meal-div-select
      (.setItems (FXCollections/observableArrayList (map name (keys meal-divs)))))
    (.setOnAction save-btn (proxy [EventHandler] []
                             (handle [_] 
                                     (kc/insert meshilogs
                                                (kc/values {:day (Date/valueOf (.getValue day-select))
                                                         :meal_div (get meal-divs (keyword (.getValue meal-div-select)))
                                                         :staple (.getText staple-txt)
                                                         :main_dish (.getText main-dish-txt)
                                                         :side_dishes (kc/raw (str "'" (cheshire/generate-string (clojure.string/split (.getText side-dishes-txt) #"\n")) "'::jsonb"))}))
                                     (println "log saved"))))
    (doto stage
      (.setTitle "Meshilog")
      (.setScene (Scene. root 500 350))
      .show)))

(defn -main
  [& args]
  (Application/launch meshilog.core args))

基本はJavaでJavaFXプログラムを作成するのと同じ流れとなる。

main関数でApplication/launchメソッドを実行すると

JavaFXが立ち上がってstart関数が呼び出される。

start関数の中では、

  • fxmlファイルをロード
  • fx:idを指定してfxml内の入力項目、ボタンオブジェクトを取得
  • コンボボックス(meal-div-select)に選択肢を設定
  • ボタン(save-btn)にイベントハンドラをセット
  • ステージウィンドウを表示

という手順で処理を行っている。

今回はボタンのイベントハンドラでそのまま入力値を

データベースにinsertしてる。

ここまで書いて

lein run

すると、ウィンドウが立ち上がって食事履歴登録ができる。

lein uberjar

で実行可能jar化することで、

DBにアクセス可能なマシンなら

どこからでも実行できるアプリになる。

おわり

これで基本的な構成ができたので、

ここから食事履歴検索できるようにしたり、

メニューをマスタ化してカロリー計算したり色々応用できそう。

これからも年をとってうまくできなくなってきたことを

プログラムで補っていきたい。

JavaFX GUIプログラミング〈Vol.1〉

JavaFX GUIプログラミング〈Vol.1〉