zoukankan      html  css  js  c++  java
  • clojure GUI编程-1

    clojure GUI编程-1

    clojure GUI编程-1

    1 简介

    最近了解了下GUI编程,测试了实时刷新GUI的编程方法,作为总结,记录下来。

    具体示例以okex交易行情为例子,写一个GUI程序,界面要实时刷新当前行情。 参考官方地址

    okex的API地址, 主要用到获取币对信息,和深度数据。

    2 实现过程

    2.1 添加依赖包

    新建deps.edn文件,添加依赖项:

     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:

     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的数据刷新。

      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运行程序。

    https://img2018.cnblogs.com/blog/1545892/201905/1545892-20190529220701538-2035973591.jpg

    图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,使用独立线程:

     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端的界面作为比较。

    作者: ntestoc

    Created: 2019-05-29 周三 22:06

  • 相关阅读:
    第 45 届国际大学生程序设计竞赛(ICPC)亚洲区域赛(南京)K Co-prime Permutation
    2020 CCPC绵阳站赛后回忆+总结
    CF 1473D Program
    Educational Codeforces Round 100 (Rated for Div. 2) 补题情况
    Codeforces Round #690 (Div. 3) (补题情况)
    这个博客停用,新博客地址:www.baccano.fun
    炫酷路径(牛客)
    洛谷 P1123 取数游戏
    洛谷P2802 回家
    cf上分的失落->高兴->更失落
  • 原文地址:https://www.cnblogs.com/ntestoc/p/10941670.html
Copyright © 2011-2022 走看看