Kikuchy's Second Memory

技術のこととか、技術以外のこととか、思ったことを書き留めています。

Clojure で GUI アプリを書いてみる (一旦の完成を見る・前編)

f:id:kikuchy:20130521134455p:plain
完成品はこんなふうになりました!



昨日今朝の続きです。

  • アイコンを表示する方法
  • 天気予報を取得する方法

この二つができたので、後はこれを組み合わせるだけです。
私は Mac を使っているので、できれば Mac のアプリケーションとしてバンドルしておきたいところですので、それも試してみようと思います。


これまで作って来たプロジェクトは本当に試しにコードを書くために作った物だったので、そのまま使い回さないことにしました。
そのため、今回は Leiningen の app テンプレートを使ってプロジェクトを作る所から始めます。
プロジェクト名、並びにアプリ名は "MyForecast" にすることにします。

$ lein new app myforecast
$ cd myforecast

project.clj に、ライブラリの依存関係を追記します。

(defproject myforecast "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.5.1"]
;; data.json と clj-http を追加
  [org.clojure/data.json "0.2.2"]
  [clj-http "0.7.2"]]
  :main myforecast.core)

依存関係を解決しておきましょう。これで data.json と clj-http がダウンロードされてプロジェクトに組み込まれます。

$ lein deps

src/myforecast/core.clj を以下のように編集。
今回は流石にちょっと長いです。

(ns myforecast.core
  (:require [clj-http.client :as client]
  [clojure.data.json :as json])
  (:gen-class))
(import '(java.awt SystemTray Image TrayIcon PopupMenu MenuItem)
  '(java.awt.event ActionListener)
  '(javax.imageio ImageIO)
  '(java.net URL))

;; 天気予報 API のアドレス
(def tenki-api-addr "http://weather.livedoor.com/forecast/webservice/json/v1?city=130010")

;; 天気を表示するアイコン画像のリスト
(def icon-images-list ["sunny.png" "clowdy.png" "rainy.png" "clowdy_sunny.png"])

(defn telop-to-index
  "テロップの文言から、icon-images-listのインデックスを返します"
  [telop]
  (if (re-find #"晴れ" telop) 0
    (if (re-find #"曇り" telop) 1
      (if (re-find #"雨" telop) 2
        (if (re-find #"[(晴.+曇)(曇.+晴)]" telop) 3)))))

(defn get-tenki
  "天気予報をロードしてJSONを返します"
  []
  (let [tenki-data (client/get tenki-api-addr)]
      (json/read-str (:body tenki-data) :key-fn keyword)))

(defn tenki-today
  "今日の天気部分だけ取り出します"
  [json-tenki]
  (first (:forecasts json-tenki)))

(defn tenki-image
  "今日の天気部分から天気画像を取り出します"
  [forecast]
  (:url (:image forecast)))

(defn tenki-temperature
  "フォーマット済みの最高気温と最低気温を取り出します"
  [forecast]
  (str (:celsius (:max (:temperature forecast))) "/" (:celsius (:min (:temperature forecast)))))

(defn system-tray-popupmenu
  "システムアイコン用のポップアップメニューを生成します"
  []
  (let [popup (PopupMenu.)
    item-exit (MenuItem. "exit")]
      (.addActionListener item-exit (reify ActionListener
        (actionPerformed [_ evt]
          (System/exit 0))))
      (.add popup item-exit)
        popup))

(defn read-image-local
  "ローカルの画像ファイルを読み込みます"
  [filepath]
  (ImageIO/read (.getResourceAsStream (.getContextClassLoader (Thread/currentThread)) filepath)))

(defn read-image-remote
  "リモートの画像ファイルを読み込みます"
  [url]
  (ImageIO/read (URL. url)))

(defn system-tray-icon
  "システムトレイ用のアイコンを生成します"
  [#^Image image #^String tip #^PopupMenu popup]
  (TrayIcon. image tip popup))

(defn add-icon-system-tray
  "システムトレイ用アイコンをシステムトレイに追加します"
  [#^TrayIcon icon]
    (.add (SystemTray/getSystemTray) icon))

(defn -main
  [& args]
  (let [today-forecast (tenki-today (get-tenki))
    icon (system-tray-icon (read-image-local (nth icon-images-list (telop-to-index (:telop today-forecast)))) (tenki-temperature today-forecast) (system-tray-popupmenu))]
    (add-icon-system-tray icon)))

次に画像を用意します。拙いイラレスキルを駆使してそれっぽいアイコンを作ってみました。
f:id:kikuchy:20130521135539p:plain
本当は API についてくる画像を使おうかと思ったのですが、縦横比が1:1でない上に、 Mac のシステムトレイに合わない印象だったので、今回は自作することにしました。
画像は src/ 以下に置きます。

これで準備は整いました。早速テストしてみます。

$ lein run

動きましたね! ツールチップには最高気温と最低気温を表示するようにしてみました。
スクリーンショット撮影時には、なぜか最低気温に null が入っていて表示できませんでしたが…


次は実行可能な jar ファイルを生成します。実行時に classpath 指定が要らないファイルです。

$ lein uberjar
Compiling myforecast.core
Created /Users/kikuchy/tmp/clojure/myforecast/target/myforecast-0.1.0-SNAPSHOT.jar
Including myforecast-0.1.0-SNAPSHOT.jar
Including data.json-0.2.2.jar
Including tools.reader-0.7.3.jar
Including commons-logging-1.1.1.jar
Including jackson-dataformat-smile-2.1.4.jar
Including clj-http-0.7.2.jar
Including httpclient-4.2.3.jar
Including jackson-core-2.1.4.jar
Including httpcore-4.2.4.jar
Including cheshire-5.1.1.jar
Including slingshot-0.10.3.jar
Including commons-io-2.4.jar
Including clojure-1.5.1.jar
Including jsoup-1.7.1.jar
Including commons-codec-1.7.jar
Including crouton-0.1.1.jar
Including httpmime-4.2.3.jar
Created /Users/kikuchy/tmp/clojure/myforecast/target/myforecast-0.1.0-SNAPSHOT-standalone.jar

これで target/ 以下に myforecast-0.1.0-SNAPSHOT-standalone.jar という jar ファイルができているはず!
これを Leiningen 無しで実行して見ましょう。

$ java -jar target/myforecast-0.1.0-SNAPSHOT-standalone.jar

動きました! これで一旦は完成ですね!
ん、「一旦」?


さて、完成、と言っても良いハズですが、まだアプリケーションとしては味気ない。
Mac で実行すると、 Dock にアイコンが出るんです。こんな感じに。
f:id:kikuchy:20130521140646p:plain
これは美しくない。

それに常駐するアプリですから、ログイン項目に追加して、コンピューターの起動時に自動的に立ち上がって欲しい…

そのためには Mac のアプリケーションとしてパッケージングする必要があります。
パッケージングの方法は後編で…




今回の解説。
Clojure でも正規表現が使用できます。 #"" が正規表現リテラルですね。

=> (re-find #"[1-9]\d*" "34298073-45745")
"34298073"
=> (re-find #"[1-9]\d*" "abcdefg")
nil
=> (re-seq #"[1-9]\d*" "34298073-45745")
("34298073" "45745")

今回はとりあえず「マッチしたか否か」だけ分かれば良かったので、かなり好い加減な使い方をしています。


参考