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端的界面做爲比較。