clojure GUI编程-1
1 简介
2 实现过程
2.1 添加依赖包
新建deps.edn文件,添加依赖项: python
1: {:aliases 2: { 3: ;; 运行初始代码 clj -A:run 4: :run {:main-opts ["-m" "core"]} 5: 6: ;; 用于运行改进后的代码 clj -A:run2 7: :run2 {:main-opts ["-m" "core2"]}}, 8: 9: :deps 10: { 11: org.clojure/clojure {:mvn/version "1.10.0"}, 12: com.cemerick/url {:mvn/version "0.1.1"}, ;; uri处理 13: slingshot {:mvn/version "0.12.2"}, ;; try+ catch+ 14: com.taoensso/timbre {:mvn/version "4.10.0"}, ;; logging 15: cheshire/cheshire {:mvn/version "5.8.1"}, ;; json处理 16: clj-http {:mvn/version "3.9.1"}, ;; http client 17: com.rpl/specter {:mvn/version "1.1.2"}, ;; map数据结构查询 18: camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.0"}, ;; 命名转换 19: seesaw {:mvn/version "1.5.0"} ;; GUI框架 20: }, 21: 22: ;; 把src文件夹添加到class path 23: :paths ["src"] 24: }
2.2 API请求的实现
新建src/api.clj,根据okex API文档实现须要的API: web
1: (ns api 2: (:require [clj-http.client :as http] 3: [cheshire.core :as json] 4: [cemerick.url :refer [url url-encode]] 5: [taoensso.timbre :as log] 6: [camel-snake-kebab.core :refer :all]) 7: (:use [slingshot.slingshot :only [throw+ try+]] 8: com.rpl.specter)) 9: 10: (def base-api-host "https://www.okex.com/") 11: 12: (defn snake-case-keys 13: "把map m的key转换为snake_string" 14: [m] 15: (transform [MAP-KEYS] ->snake_case_string m)) 16: 17: (defn api-request 18: "okex api请求 19: `args` 为请求参数, " 20: ([path] (api-request path nil)) 21: ([path args] 22: (let [args (snake-case-keys args) 23: u (-> (url base-api-host path) 24: (assoc :query args) 25: str) 26: header { 27: ;; 本地代理设置 28: :proxy-host "127.0.0.1" 29: :proxy-port 8080 30: 31: :cookie-policy :standard 32: 33: ;; 跳过https证书验证 34: :insecure? true 35: :accept :json}] 36: (try+ 37: (some-> (http/get (str u) header) 38: :body 39: (json/decode ->kebab-case-keyword)) 40: (catch (#{400 401 403 404} (get % :status)) {:keys [status body]} 41: (log/warn :api-req "return error" status body) 42: {:error (json/decode body ->kebab-case-keyword)}) 43: (catch [:status 500] {:keys [headers]} 44: (log/warn :api-req "server error" headers) 45: {:error {:code 500 46: :message "remote server error!"}}) 47: (catch Object _ 48: (log/error (:throwable &throw-context) "unexpected error") 49: (throw+)))))) 50: 51: (defn get-instruments 52: "获取币对信息" 53: [] 54: (api-request "/api/spot/v3/instruments")) 55: 56: (defn format-depth-data 57: "格式化深度数据" 58: [data] 59: (transform [(multi-path :asks :bids) INDEXED-VALS] 60: (fn [[idx [price amount order-count]]] 61: [idx {:pos idx 62: :price price 63: :amount amount 64: :order-count order-count}]) 65: data)) 66: 67: (defn get-spot-instrument-book 68: "获取币对深度数据" 69: ([instrument-id] (get-spot-instrument-book instrument-id nil)) 70: ([instrument-id opt] 71: (-> (format "/api/spot/v3/instruments/%s/book" instrument-id) 72: (api-request opt) 73: format-depth-data)))
2.3 gui界面的实现
建立界面文件src/core.clj,首先用回调的方式实现gui的数据刷新。 sql
1: (ns core 2: (:require [seesaw.core :as gui] 3: [seesaw.table :as table] 4: [seesaw.bind :as bind] 5: [seesaw.table :refer [table-model]] 6: [api] 7: [taoensso.timbre :as log]) 8: (:use com.rpl.specter)) 9: 10: (def coin-pairs "全部交易对信息" (api/get-instruments)) 11: (def base-coins "全部基准货币" 12: (-> (select [ALL :base-currency] coin-pairs) 13: set 14: sort)) 15: 16: (defn get-quote-coins 17: "获取基准货币支持的计价货币" 18: [base-coin] 19: (select [ALL #(= (:base-currency %) base-coin) :quote-currency] coin-pairs)) 20: 21: (defn get-instrument-id 22: "根据基准货币和计价货币得到币对名称" 23: [base-coin quote-coin] 24: (select-one [ALL 25: #(and (= (:base-currency %) base-coin) 26: (= (:quote-currency %) quote-coin)) 27: :instrument-id] 28: coin-pairs)) 29: 30: ;;; 设置form的默认值 31: (let [first-base (first base-coins)] 32: (def coin-pair-data (atom {:base-coin first-base 33: :quote-coin (-> (get-quote-coins first-base) 34: first)}))) 35: 36: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 37: 38: (defn depth-data-model 39: "深度数据table模型" 40: [data] 41: (table-model :columns [{:key :pos :text "价位"} 42: {:key :price :text "价格"} 43: {:key :amount :text "数量"} 44: {:key :order-count :text "订单数"}] 45: :rows data)) 46: 47: (defn make-depth-view 48: [] 49: (let [bids-view (gui/vertical-panel 50: :items [(gui/label "买入信息") 51: (gui/scrollable 52: (gui/table 53: :id :bids-table 54: :model (depth-data-model [])))]) 55: 56: asks-view (gui/vertical-panel 57: :items [(gui/label "卖出信息") 58: (gui/scrollable 59: (gui/table 60: :id :asks-table 61: :model (depth-data-model [])))]) 62: 63: coin-pair-selector (gui/horizontal-panel 64: :items [(gui/label "基准币种:") 65: (gui/combobox :id :base-coin 66: :model base-coins) 67: (gui/label "计价币种:") 68: (gui/combobox :id :quote-coin)])] 69: (gui/border-panel 70: :north coin-pair-selector 71: :center (gui/horizontal-panel 72: :items [bids-view 73: asks-view]) 74: :vgap 5 :hgap 5 :border 3))) 75: 76: (defn update-quote-coin-model! 77: "更新计价货币的模型" 78: [f model] 79: (let [quote-coin (gui/select f [:#quote-coin])] 80: (gui/config! quote-coin :model model))) 81: 82: (defn depth-table-update! 83: "更新depth数据显示" 84: [root] 85: (let [coin-p @coin-pair-data 86: instrument-id (get-instrument-id (:base-coin coin-p) 87: (:quote-coin coin-p)) 88: data (api/get-spot-instrument-book instrument-id) 89: bids-table (gui/select root [:#bids-table]) 90: asks-table (gui/select root [:#asks-table])] 91: (->> (:bids data) 92: depth-data-model 93: (gui/config! bids-table :model)) 94: (->> (:asks data) 95: depth-data-model 96: (gui/config! asks-table :model)))) 97: 98: (defn add-behaviors 99: "添加事件处理" 100: [root] 101: (let [base-coin (gui/select root [:#base-coin]) 102: quote-coin (gui/select root [:#quote-coin])] 103: ;; 基准货币选择事件绑定 104: (bind/bind 105: (bind/selection base-coin) 106: (bind/transform get-quote-coins) 107: (bind/tee 108: (bind/property quote-coin :model) 109: (bind/b-swap! coin-pair-data assoc :base-coin))) 110: 111: ;; 计价货币选择事件绑定 112: (bind/bind 113: (bind/selection quote-coin) 114: (bind/b-swap! coin-pair-data assoc :quote-coin)) 115: 116: ;; 定时更新depth-view 117: (gui/timer (fn [_] 118: (depth-table-update! root)) :delay 100) 119: 120: ;; coin-pair-data修改就更新depth-view 121: (add-watch coin-pair-data :depth-view (fn [k _ _ new-data] 122: (depth-table-update! root))))) 123: 124: (defn -main [& args] 125: (gui/invoke-later 126: (let [frame (gui/frame :title "okex 行情信息" 127: :on-close :exit ;; 窗口关闭时退出程序 128: :content (make-depth-view))] 129: (update-quote-coin-model! frame (-> (:base-coin @coin-pair-data) 130: get-quote-coins)) 131: (gui/value! frame @coin-pair-data) 132: (add-behaviors frame) 133: (-> frame gui/pack! gui/show!))))
因为使用了swing的Timer进行获取数据并刷新,会形成界面严重卡顿。 而且内存占用很高,使用clj -A:run运行程序。 shell
图1 运行时界面和内存占用截图数据库
2.4 界面实时刷新的改进
把定时执行的代码放到独立的线程中获取数据,而后在swing线程中更新界面。 修改depth-table-update!的实现: 编程
1: (defn depth-table-update! 2: "更新depth table数据显示" 3: [root] 4: (let [coin-p @coin-pair-data 5: instrument-id (get-instrument-id (:base-coin coin-p) 6: (:quote-coin coin-p)) 7: data (api/get-spot-instrument-book instrument-id) 8: bids-table (gui/select root [:#bids-table]) 9: asks-table (gui/select root [:#asks-table])] 10: ;; 在gui线程中更新model 11: (gui/invoke-later 12: (->> (:asks data) 13: depth-data-model 14: (gui/config! asks-table :model)) 15: (->> (:bids data) 16: depth-data-model 17: (gui/config! bids-table :model))))) 18:
修改add-behaviors中的timer,使用独立线程: json
1: (defn add-behaviors 2: "添加事件处理" 3: [root] 4: (let [base-coin (gui/select root [:#base-coin]) 5: quote-coin (gui/select root [:#quote-coin])] 6: ;; 基准货币选择事件绑定 7: (bind/bind 8: (bind/selection base-coin) 9: (bind/transform get-quote-coins) 10: (bind/tee 11: (bind/property quote-coin :model) 12: (bind/b-swap! coin-pair-data assoc :base-coin))) 13: 14: ;; 计价货币选择事件绑定 15: (bind/bind 16: (bind/selection quote-coin) 17: (bind/b-swap! coin-pair-data assoc :quote-coin)) 18: 19: ;; 定时更新depth-view 20: (future (loop [] 21: (depth-table-update! root) 22: (Thread/sleep 100) 23: (recur))) 24: 25: ;; coin-pair-data修改就更新depth-view 26: (add-watch coin-pair-data :depth-view (fn [k _ _ new-data] 27: (depth-table-update! root)))))
运行(-main),能够看到界面仍是比较卡顿。
2.5 改进方法2
把数据请求的代码独立出来,用atom保存(也能够用数据库持久化),至关于把model分离出来。 文件保存为src/core2.clj,完整代码:
1: (ns core2 2: (:require [seesaw.core :as gui] 3: [seesaw.table :as table] 4: [seesaw.bind :as bind] 5: [seesaw.table :refer [table-model]] 6: [api] 7: [taoensso.timbre :as log]) 8: (:use com.rpl.specter)) 9: 10: (def coin-pairs "全部交易对信息" (api/get-instruments)) 11: (def base-coins "全部基准货币" 12: (-> (select [ALL :base-currency] coin-pairs) 13: set 14: sort)) 15: 16: (defn get-quote-coins 17: "获取基准货币支持的计价货币" 18: [base-coin] 19: (select [ALL #(= (:base-currency %) base-coin) :quote-currency] coin-pairs)) 20: 21: (defn get-instrument-id 22: "根据基准货币和计价货币得到币对名称" 23: [base-coin quote-coin] 24: (select-one [ALL 25: #(and (= (:base-currency %) base-coin) 26: (= (:quote-currency %) quote-coin)) 27: :instrument-id] 28: coin-pairs)) 29: 30: (def instruments-info "交易对的深度数据"(atom {})) 31: 32: (defn run-get-instrument-services! 33: "启动获取交易对深度信息的服务 34: 没有提供中止功能" 35: [instrument-id] 36: (when (and instrument-id 37: (not (contains? @instruments-info instrument-id))) 38: (future (loop [] 39: (let [data (api/get-spot-instrument-book instrument-id)] 40: (setval [ATOM instrument-id] data instruments-info)) 41: (Thread/sleep 200) 42: (recur))))) 43: 44: ;; 设置form的默认值 45: (let [first-base (first base-coins)] 46: (def coin-pair-data (atom {:base-coin first-base 47: :quote-coin (-> (get-quote-coins first-base) 48: first)}))) 49: 50: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 51: 52: (defn depth-data-model 53: "深度数据table模型" 54: [data] 55: (table-model :columns [{:key :pos :text "价位"} 56: {:key :price :text "价格"} 57: {:key :amount :text "数量"} 58: {:key :order-count :text "订单数"}] 59: :rows data)) 60: 61: (defn make-depth-view 62: [] 63: (let [bids-view (gui/vertical-panel 64: :items [(gui/label "买入信息") 65: (gui/scrollable 66: (gui/table 67: :id :bids-table 68: :model (depth-data-model [])))]) 69: 70: asks-view (gui/vertical-panel 71: :items [(gui/label "卖出信息") 72: (gui/scrollable 73: (gui/table 74: :id :asks-table 75: :model (depth-data-model [])))]) 76: 77: coin-pair-selector (gui/horizontal-panel 78: :items [(gui/label "基准币种:") 79: (gui/combobox :id :base-coin 80: :model base-coins) 81: (gui/label "计价币种:") 82: (gui/combobox :id :quote-coin)])] 83: (gui/border-panel 84: :north coin-pair-selector 85: :center (gui/horizontal-panel 86: :items [bids-view 87: asks-view]) 88: :vgap 5 :hgap 5 :border 3))) 89: 90: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 91: (defn update-quote-coin-model! 92: "更新计价货币的模型" 93: [f model] 94: (let [quote-coin (gui/select f [:#quote-coin])] 95: (gui/config! quote-coin :model model))) 96: 97: (defn get-current-instrument-id 98: "获取当前币对的id" 99: [] 100: (let [coin-p @coin-pair-data] 101: (get-instrument-id (:base-coin coin-p) 102: (:quote-coin coin-p)))) 103: 104: (defn bind-transfrom-set-model 105: [trans-fn frame id] 106: (bind/bind 107: (bind/transform #(trans-fn %)) 108: (bind/property (gui/select frame [id]) :model))) 109: 110: (defn add-behaviors 111: "添加事件处理" 112: [root] 113: (let [base-coin (gui/select root [:#base-coin]) 114: quote-coin (gui/select root [:#quote-coin])] 115: ;; 基准货币选择事件绑定 116: (bind/bind 117: (bind/selection base-coin) 118: (bind/transform get-quote-coins) 119: (bind/tee 120: ;; 设置quote-coin的选择项 121: (bind/property quote-coin :model) 122: (bind/bind 123: (bind/transform first) 124: (bind/selection quote-coin)))) 125: 126: ;; 绑定基准货币和计价货币的选择事件 127: (bind/bind 128: (bind/funnel 129: (bind/selection base-coin) 130: (bind/selection quote-coin)) 131: (bind/transform (fn [[base-coin quote-coin]] 132: {:base-coin base-coin 133: :quote-coin quote-coin})) 134: coin-pair-data) 135: 136: ;; 绑定交易对深度信息, 一旦更改就更新depth-view 137: (bind/bind 138: instruments-info 139: (bind/transform #(% (get-current-instrument-id))) 140: (bind/notify-later) 141: (bind/tee 142: (bind-transfrom-set-model #(-> (:bids %) 143: depth-data-model) root :#bids-table) 144: (bind-transfrom-set-model #(-> (:asks %) 145: depth-data-model) root :#asks-table))) 146: 147: ;; 当前选择的交易对修改就启动新的深度信息服务 148: (add-watch coin-pair-data :depth-view (fn [k _ _ new-data] 149: (-> (get-current-instrument-id) 150: run-get-instrument-services!))))) 151: 152: (defn -main [& args] 153: (gui/invoke-later 154: (let [frame (gui/frame :title "okex 行情信息" 155: :on-close :exit ;; 窗口关闭时退出程序 156: :content (make-depth-view))] 157: 158: ;; 更新quote-coin的model 159: (update-quote-coin-model! frame (-> (:base-coin @coin-pair-data) 160: get-quote-coins)) 161: ;; 先绑定事件,再设置默认值 162: (add-behaviors frame) 163: (gui/value! frame @coin-pair-data) 164: 165: ;; 显示frame 166: (-> frame gui/pack! gui/show!))))
使用clj -A:run2运行程序, 能够看到,把数据请求和界面更新分开以后,界面的操做比较流畅。
3 总结
经过分离数据请求部分,整个界面的逻辑就变成发布/订阅的模式,经过下降数据获取与展现的耦合,界面响应也比较流畅。 这和clojurescript的re-frame框架的理念也类似,re-frame经过reg-sub和<sub来进行数据的发布与订阅,下一次用re-frame写一个web端的界面做为比较。