clojure GUI编程-1
目录
1 简介
最近了解了下GUI编程,测试了实时刷新GUI的编程方法,作为总结,记录下来。
具体示例以okex交易行情为例子,写一个GUI程序,界面要实时刷新当前行情。 参考官方地址。 okex的API地址。 主要用到获取币对信息,和深度数据。
2 实现过程
2.1 添加依赖包
新建deps.edn文件,添加依赖项:
{:aliases {:run {:main-opts ["-m" "core"]}}, :deps { org.clojure/clojure {:mvn/version "1.10.0"}, com.cemerick/url {:mvn/version "0.1.1"}, ;; uri处理 slingshot {:mvn/version "0.12.2"}, ;; try+ catch+ com.taoensso/timbre {:mvn/version "4.10.0"}, ;; logging cheshire/cheshire {:mvn/version "5.8.1"}, ;; json处理 clj-http {:mvn/version "3.9.1"}, ;; http client com.rpl/specter {:mvn/version "1.1.2"}, ;; map数据结构查询 camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.0"}, ;; 命名转换 seesaw {:mvn/version "1.5.0"} ;; GUI框架 }, ;; 把src文件夹添加到class path :paths ["src"] }
2.2 API请求的实现
新建api.clj,根据okex API文档实现需要的API:
(ns api (:require [clj-http.client :as http] [clj-http.cookies :as cookies] [cheshire.core :as json] [cemerick.url :refer [url url-encode]] [taoensso.timbre :as log] [camel-snake-kebab.core :refer :all]) (:use [slingshot.slingshot :only [throw+ try+]] com.rpl.specter)) (def base-api-host "https://www.okex.com/") (def cs (cookies/cookie-store)) (defn snake-case-keys "把map m的key转换为snake_string" [m] (transform [MAP-KEYS] ->snake_case_string m)) (defn api-request "okex api请求 `args` 为请求参数, " ([path] (api-request path nil)) ([path args] (let [args (snake-case-keys args) u (-> (url base-api-host path) (assoc :query args) str) header {:cookie-store cs ;; 代理设置 :proxy-host "127.0.0.1" :proxy-port 8080 :cookie-policy :standard ;; 跳过https证书验证 :insecure? true :accept :json}] (try+ (some-> (http/get (str u) header) :body (json/decode ->kebab-case-keyword)) (catch (#{400 401 403 404} (get % :status)) {:keys [status body]} (log/warn :api-req "return error" status body) {:error (json/decode body ->kebab-case-keyword)}) (catch [:status 500] {:keys [headers]} (log/warn :api-req "server error" headers) {:error {:code 500 :message "remote server error!"}}) (catch Object _ (log/error (:throwable &throw-context) "unexpected error") (throw+)))))) (defn get-instruments "获取币对信息" [] (api-request "/api/spot/v3/instruments")) (defn format-depth-data "格式化深度数据" [data] (transform [(multi-path :asks :bids) INDEXED-VALS] (fn [[idx [price amount order-count]]] [idx {:pos idx :price price :amount amount :order-count order-count}]) data)) (defn get-spot-instrument-book "获取币对深度数据" ([instrument-id] (get-spot-instrument-book instrument-id nil)) ([instrument-id opt] (-> (format "/api/spot/v3/instruments/%s/book" instrument-id) (api-request opt) format-depth-data)))
2.3 gui界面的实现
首先用回调的方式实现gui的数据刷新。
(ns core (:require [seesaw.core :as gui] [seesaw.table :as table] [seesaw.bind :as bind] [seesaw.icon :as icon] [seesaw.border :as border] [seesaw.table :refer [table-model]] [seesaw.color :refer [color]] [seesaw.mig :refer [mig-panel]] [api] [taoensso.timbre :as log]) (:use com.rpl.specter)) (def coin-pairs "所有交易对信息" (api/get-instruments)) (def base-coins "所有基准货币" (-> (select [ALL :base-currency] coin-pairs) set sort)) (defn get-quote-coins "获取基准货币支持的计价货币" [base-coin] (select [ALL #(= (:base-currency %) base-coin) :quote-currency] coin-pairs)) (defn get-instrument-id "根据基准货币和计价货币获得币对名称" [base-coin quote-coin] (select-one [ALL #(and (= (:base-currency %) base-coin) (= (:quote-currency %) quote-coin)) :instrument-id] coin-pairs)) ;;; 设置form的默认值 (let [first-base (first base-coins)] (def coin-pair-data (atom {:base-coin first-base :quote-coin (-> (get-quote-coins first-base) first)}))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn depth-data-model "深度数据table模型" [data] (table-model :columns [{:key :pos :text "价位"} {:key :price :text "价格"} {:key :amount :text "数量"} {:key :order-count :text "订单数"}] :rows data)) (defn make-depth-view [] (let [bids-view (gui/vertical-panel :items [(gui/label "买入信息") (gui/scrollable (gui/table :id :bids-table :model (depth-data-model [])))]) asks-view (gui/vertical-panel :items [(gui/label "卖出信息") (gui/scrollable (gui/table :id :asks-table :model (depth-data-model [])))]) coin-pair-selector (gui/horizontal-panel :items [(gui/label "基准币种:") (gui/combobox :id :base-coin :model base-coins) (gui/label "计价币种:") (gui/combobox :id :quote-coin)])] (gui/border-panel :north coin-pair-selector :center (gui/horizontal-panel :items [bids-view asks-view]) :vgap 5 :hgap 5 :border 3))) (defn update-quote-coin-model! "更新计价货币的模型" [f model] (let [quote-coin (gui/select f [:#quote-coin])] (gui/config! quote-coin :model model))) (defn depth-data-update! [root] (let [coin-p @coin-pair-data instrument-id (get-instrument-id (:base-coin coin-p) (:quote-coin coin-p)) data (api/get-spot-instrument-book instrument-id) bids-table (gui/select root [:#bids-table]) asks-table (gui/select root [:#asks-table])] (->> (:bids data) depth-data-model (gui/config! bids-table :model)) (->> (:asks data) depth-data-model (gui/config! asks-table :model)))) (defn add-behaviors [root] (let [base-coin (gui/select root [:#base-coin]) quote-coin (gui/select root [:#quote-coin])] ;; 基准货币选择事件绑定 (bind/bind (bind/selection base-coin) (bind/transform get-quote-coins) (bind/tee (bind/property quote-coin :model) (bind/b-swap! coin-pair-data assoc :base-coin))) ;; 计价货币选择事件绑定 (bind/bind (bind/selection quote-coin) (bind/b-swap! coin-pair-data assoc :quote-coin)) (gui/timer (fn [_] (depth-data-update! root)) :delay 100) (add-watch coin-pair-data :depth-view (fn [k _ _ new-data] (depth-data-update! root))))) (defn -main [& args] (gui/invoke-later (let [frame (gui/frame :title "okex 行情信息" :content (make-depth-view))] (update-quote-coin-model! frame (-> (:base-coin @coin-pair-data) get-quote-coins)) (gui/value! frame @coin-pair-data) (add-behaviors frame) (-> frame gui/pack! gui/show!))))
由于使用了swing的Timer进行获取数据并刷新,会造成界面严重卡死。
Created: 2019-05-29 周三 08:50