clojure GUI编程-1

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请求的实现

新建src/api.clj,根据okex API文档实现需要的API:

(ns api
  (:require [clj-http.client :as http]
            [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/")

(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 {
                 ;; 本地代理设置
                 :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界面的实现

创建界面文件src/core.clj,首先用回调的方式实现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进行获取数据并刷新,会造成界面严重卡死。

作者: ntestoc

Created: 2019-05-29 周三 09:13

猜你喜欢

转载自www.cnblogs.com/ntestoc/p/10941670.html