clojure GUI编程-2
1 简介
接上一篇GUI开发,每次手写GUI布局代码比较不方便,可以使用netbeans的form designer设计好界面,然后从clojure中加载界面,绑定事件来进行GUI设计。
2 实现过程
由于要编译java代码,使用leiningen进行项目管理比较方便。先创建一个空项目, lein new okex 创建项目。
2.1 添加依赖
修改项目文件夹下的project.clj如下
1: (defproject okex "0.1.0-SNAPSHOT" 2: :description "FIXME: write description" 3: :url "http://example.com/FIXME" 4: :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0" 5: :url "https://www.eclipse.org/legal/epl-2.0/"} 6: 7: ;; 使用utf-8编码编译java代码,默认会使用windows系统的默认编码gbk 8: :javac-options ["-encoding" "utf-8"] 9: :java-source-paths ["src"] 10: 11: :dependencies [[org.clojure/clojure "1.10.0"] 12: [com.cemerick/url "0.1.1"] ;; uri处理 13: [slingshot "0.12.2"] ;; try+ catch+ 14: [com.taoensso/timbre "4.10.0"] ;; logging 15: [cheshire/cheshire "5.8.1"] ;; json处理 16: [clj-http "3.9.1"] ;; http client 17: [com.rpl/specter "1.1.2"] ;; map数据结构查询 18: [camel-snake-kebab/camel-snake-kebab "0.4.0"] ;; 命名转换 19: [seesaw "1.5.0"] ;; GUI框架 20: ] 21: :main ^:skip-aot okex.core 22: :aot :all 23: :target-path "target/%s" 24: :repl-options {:init-ns okex.core})
2.2 复制文件
把上一篇创建的core2.clj和api.clj复制到src/okex文件夹下,改名core2.clj为core.clj。 并修改命名空间与文件名对应。
2.3 设计gui界面
使用netbeans新建JFrame form,并设计窗体,修改要用到的widget的name属性为对应的swing id名,然后保存这个文件到src/okex文件夹下,注意包名要用okex,文件内容如下:
1: /* 2: * To change this license header, choose License Headers in Project Properties. 3: * To change this template file, choose Tools | Templates 4: * and open the template in the editor. 5: */ 6: package okex; 7: /** 8: * 9: */ 10: public class DepthWindow extends javax.swing.JFrame { 11: 12: /** 13: * Creates new form DepthWindow 14: */ 15: public DepthWindow() { 16: initComponents(); 17: } 18: 19: /** 20: * This method is called from within the constructor to initialize the form. 21: * WARNING: Do NOT modify this code. The content of this method is always 22: * regenerated by the Form Editor. 23: */ 24: @SuppressWarnings("unchecked") 25: // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents 26: private void initComponents() { 27: 28: jPanel1 = new javax.swing.JPanel(); 29: jPanel2 = new javax.swing.JPanel(); 30: jLabel1 = new javax.swing.JLabel(); 31: jComboBox1 = new javax.swing.JComboBox<>(); 32: jLabel2 = new javax.swing.JLabel(); 33: jComboBox2 = new javax.swing.JComboBox<>(); 34: jScrollPane1 = new javax.swing.JScrollPane(); 35: jTable1 = new javax.swing.JTable(); 36: jScrollPane2 = new javax.swing.JScrollPane(); 37: jTable2 = new javax.swing.JTable(); 38: jLabel3 = new javax.swing.JLabel(); 39: jLabel4 = new javax.swing.JLabel(); 40: 41: setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE); 42: setTitle("okex 行情信息"); 43: 44: jPanel1.setLayout(new java.awt.BorderLayout(3, 3)); 45: 46: jLabel1.setText("基准币种:"); 47: 48: jComboBox1.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); 49: jComboBox1.setName("base-coin"); // NOI18N 50: 51: jLabel2.setText("计价币种:"); 52: 53: jComboBox2.setModel(new javax.swing.DefaultComboBoxModel<>(new String[] { "Item 1", "Item 2", "Item 3", "Item 4" })); 54: jComboBox2.setToolTipText(""); 55: jComboBox2.setName("quote-coin"); // NOI18N 56: 57: jTable1.setModel(new javax.swing.table.DefaultTableModel( 58: new Object [][] { 59: {null, null, null, null}, 60: {null, null, null, null}, 61: {null, null, null, null}, 62: {null, null, null, null} 63: }, 64: new String [] { 65: "Title 1", "Title 2", "Title 3", "Title 4" 66: } 67: )); 68: jTable1.setName("bids-table"); // NOI18N 69: jScrollPane1.setViewportView(jTable1); 70: 71: jTable2.setModel(new javax.swing.table.DefaultTableModel( 72: new Object [][] { 73: {null, null, null, null}, 74: {null, null, null, null}, 75: {null, null, null, null}, 76: {null, null, null, null} 77: }, 78: new String [] { 79: "Title 1", "Title 2", "Title 3", "Title 4" 80: } 81: )); 82: jTable2.setName("asks-table"); // NOI18N 83: jScrollPane2.setViewportView(jTable2); 84: 85: jLabel3.setFont(new java.awt.Font("sansserif", 1, 12)); // NOI18N 86: jLabel3.setText("买入信息"); 87: 88: jLabel4.setFont(new java.awt.Font("sansserif", 1, 12)); // NOI18N 89: jLabel4.setText("卖出信息"); 90: 91: javax.swing.GroupLayout jPanel2Layout = new javax.swing.GroupLayout(jPanel2); 92: jPanel2.setLayout(jPanel2Layout); 93: jPanel2Layout.setHorizontalGroup( 94: jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) 95: .addGroup(jPanel2Layout.createSequentialGroup() 96: .addContainerGap() 97: .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) 98: .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) 99: .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE) 100: .addContainerGap()) 101: .addGroup(jPanel2Layout.createSequentialGroup() 102: .addGap(18, 18, 18) 103: .addComponent(jLabel1) 104: .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) 105: .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, 171, javax.swing.GroupLayout.PREFERRED_SIZE) 106: .addGap(125, 125, 125) 107: .addComponent(jLabel2, javax.swing.GroupLayout.PREFERRED_SIZE, 69, javax.swing.GroupLayout.PREFERRED_SIZE) 108: .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) 109: .addComponent(jComboBox2, javax.swing.GroupLayout.PREFERRED_SIZE, 141, javax.swing.GroupLayout.PREFERRED_SIZE) 110: .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) 111: .addGroup(jPanel2Layout.createSequentialGroup() 112: .addGap(151, 151, 151) 113: .addComponent(jLabel3, javax.swing.GroupLayout.PREFERRED_SIZE, 113, javax.swing.GroupLayout.PREFERRED_SIZE) 114: .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) 115: .addComponent(jLabel4, javax.swing.GroupLayout.PREFERRED_SIZE, 83, javax.swing.GroupLayout.PREFERRED_SIZE) 116: .addGap(202, 202, 202)) 117: ); 118: 119: jPanel2Layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {jLabel1, jLabel2}); 120: 121: jPanel2Layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {jComboBox1, jComboBox2}); 122: 123: jPanel2Layout.setVerticalGroup( 124: jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) 125: .addGroup(jPanel2Layout.createSequentialGroup() 126: .addContainerGap() 127: .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) 128: .addComponent(jLabel1) 129: .addComponent(jComboBox1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) 130: .addComponent(jLabel2) 131: .addComponent(jComboBox2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)) 132: .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) 133: .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) 134: .addComponent(jLabel3, javax.swing.GroupLayout.PREFERRED_SIZE, 20, javax.swing.GroupLayout.PREFERRED_SIZE) 135: .addComponent(jLabel4, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)) 136: .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) 137: .addGroup(jPanel2Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) 138: .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 284, Short.MAX_VALUE) 139: .addComponent(jScrollPane2, javax.swing.GroupLayout.PREFERRED_SIZE, 0, Short.MAX_VALUE)) 140: .addContainerGap()) 141: ); 142: 143: jComboBox1.getAccessibleContext().setAccessibleName(""); 144: 145: jPanel1.add(jPanel2, java.awt.BorderLayout.CENTER); 146: 147: javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); 148: getContentPane().setLayout(layout); 149: layout.setHorizontalGroup( 150: layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) 151: .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() 152: .addContainerGap() 153: .addComponent(jPanel1, javax.swing.GroupLayout.PREFERRED_SIZE, 468, Short.MAX_VALUE) 154: .addContainerGap()) 155: ); 156: layout.setVerticalGroup( 157: layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) 158: .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() 159: .addContainerGap() 160: .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) 161: .addContainerGap()) 162: ); 163: 164: pack(); 165: }// </editor-fold>//GEN-END:initComponents 166: 167: /** 168: * @param args the command line arguments 169: */ 170: public static void main(String args[]) { 171: /* Set the Nimbus look and feel */ 172: //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) "> 173: /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel. 174: * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 175: */ 176: try { 177: for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) { 178: if ("Nimbus".equals(info.getName())) { 179: javax.swing.UIManager.setLookAndFeel(info.getClassName()); 180: break; 181: } 182: } 183: } catch (ClassNotFoundException ex) { 184: java.util.logging.Logger.getLogger(DepthWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); 185: } catch (InstantiationException ex) { 186: java.util.logging.Logger.getLogger(DepthWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); 187: } catch (IllegalAccessException ex) { 188: java.util.logging.Logger.getLogger(DepthWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); 189: } catch (javax.swing.UnsupportedLookAndFeelException ex) { 190: java.util.logging.Logger.getLogger(DepthWindow.class.getName()).log(java.util.logging.Level.SEVERE, null, ex); 191: } 192: //</editor-fold> 193: 194: /* Create and display the form */ 195: java.awt.EventQueue.invokeLater(new Runnable() { 196: public void run() { 197: new DepthWindow().setVisible(true); 198: } 199: }); 200: } 201: 202: // Variables declaration - do not modify//GEN-BEGIN:variables 203: private javax.swing.JComboBox<String> jComboBox1; 204: private javax.swing.JComboBox<String> jComboBox2; 205: private javax.swing.JLabel jLabel1; 206: private javax.swing.JLabel jLabel2; 207: private javax.swing.JLabel jLabel3; 208: private javax.swing.JLabel jLabel4; 209: private javax.swing.JPanel jPanel1; 210: private javax.swing.JPanel jPanel2; 211: private javax.swing.JScrollPane jScrollPane1; 212: private javax.swing.JScrollPane jScrollPane2; 213: private javax.swing.JTable jTable1; 214: private javax.swing.JTable jTable2; 215: // End of variables declaration//GEN-END:variables 216: }
2.4 clojure中加载java gui代码
修改core.clj,导入gui界面的类,并加载,代码如下:
1: (ns okex.core 2: (:require [seesaw.core :as gui] 3: [seesaw.table :as table] 4: [seesaw.bind :as bind] 5: [seesaw.selector :as selector] 6: [seesaw.table :refer [table-model]] 7: [okex.api :as api] 8: [taoensso.timbre :as log]) 9: (:use com.rpl.specter) 10: (:gen-class) 11: (:import okex.DepthWindow)) 12: 13: 14: ;;;;;;;;;;;;;;;;;;;;; Window-Builder binding 15: 16: (defn identify 17: "设置root下所有控件的seesaw :id 18: 只要有name属性的,全部绑定到id" 19: [root] 20: (doseq [w (gui/select root [:*])] 21: (if-let [n (.getName w)] 22: (selector/id-of! w (keyword n)))) 23: root) 24: 25: ;;;;;;;;;;;;;;;;;;;;;; 初始化值 26: 27: (def coin-pairs "所有交易对信息" (api/get-instruments)) 28: (def base-coins "所有基准货币" 29: (-> (select [ALL :base-currency] coin-pairs) 30: set 31: sort)) 32: 33: (defn get-quote-coins 34: "获取基准货币支持的计价货币" 35: [base-coin] 36: (select [ALL #(= (:base-currency %) base-coin) :quote-currency] coin-pairs)) 37: 38: (defn get-instrument-id 39: "根据基准货币和计价货币获得币对名称" 40: [base-coin quote-coin] 41: (select-one [ALL 42: #(and (= (:base-currency %) base-coin) 43: (= (:quote-currency %) quote-coin)) 44: :instrument-id] 45: coin-pairs)) 46: 47: ;; 设置form的默认值 48: (let [first-base (first base-coins)] 49: (def coin-pair-data (atom {:base-coin first-base 50: :quote-coin (-> (get-quote-coins first-base) 51: first)}))) 52: 53: ;;;;;;;;;;;;;;;;;;;;;; 服务 54: (def instruments-info "交易对的深度数据"(atom {})) 55: 56: (defn run-get-instrument-services! 57: "启动获取交易对深度信息的服务 58: 没有提供停止功能" 59: [instrument-id] 60: (when (and instrument-id 61: (not (contains? @instruments-info instrument-id))) 62: (future (loop [] 63: (let [data (api/get-spot-instrument-book instrument-id)] 64: (setval [ATOM instrument-id] data instruments-info)) 65: (Thread/sleep 200) 66: (recur))))) 67: 68: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; 辅助函数 69: 70: (defn depth-data-model 71: "深度数据table模型" 72: [data] 73: (table-model :columns [{:key :pos :text "价位"} 74: {:key :price :text "价格"} 75: {:key :amount :text "数量"} 76: {:key :order-count :text "订单数"}] 77: :rows data)) 78: 79: (defn update-quote-coin-model! 80: "更新计价货币的模型" 81: [f model] 82: (let [quote-coin (gui/select f [:#quote-coin])] 83: (gui/config! quote-coin :model model))) 84: 85: (defn get-current-instrument-id 86: "获取当前币对的id" 87: [] 88: (let [coin-p @coin-pair-data] 89: (get-instrument-id (:base-coin coin-p) 90: (:quote-coin coin-p)))) 91: 92: (defn bind-transfrom-set-model 93: [trans-fn frame id] 94: (bind/bind 95: (bind/transform #(trans-fn %)) 96: (bind/property (gui/select frame [id]) :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: ;; 设置quote-coin的选择项 109: (bind/property quote-coin :model) 110: (bind/bind 111: (bind/transform first) 112: (bind/selection quote-coin)))) 113: 114: ;; 绑定基准货币和计价货币的选择事件 115: (bind/bind 116: (bind/funnel 117: (bind/selection base-coin) 118: (bind/selection quote-coin)) 119: (bind/transform (fn [[base-coin quote-coin]] 120: {:base-coin base-coin 121: :quote-coin quote-coin})) 122: coin-pair-data) 123: 124: ;; 绑定交易对深度信息, 一旦更改就更新depth-view 125: (bind/bind 126: instruments-info 127: (bind/transform #(% (get-current-instrument-id))) 128: (bind/notify-later) 129: (bind/tee 130: (bind-transfrom-set-model #(-> (:bids %) 131: depth-data-model) root :#bids-table) 132: (bind-transfrom-set-model #(-> (:asks %) 133: depth-data-model) root :#asks-table))) 134: 135: ;; 当前选择的交易对修改就启动新的深度信息服务 136: (add-watch coin-pair-data :depth-view (fn [k _ _ new-data] 137: (-> (get-current-instrument-id) 138: run-get-instrument-services!))))) 139: 140: ;;;;;;;;;;;;;;;;;; 以下为新加的gui加载代码 141: 142: (defn my-form 143: "加载form" 144: [] 145: (let [form (identify (DepthWindow.))] 146: 147: ;; 更新quote-coin的model 148: (gui/config! (gui/select form [:#base-coin]) :model base-coins) 149: (update-quote-coin-model! form (-> (:base-coin @coin-pair-data) 150: get-quote-coins)) 151: 152: ;; 先绑定事件,再设置默认值 153: (add-behaviors form) 154: (gui/value! form @coin-pair-data) 155: 156: form)) 157: 158: (defn -main [& args] 159: (gui/invoke-later 160: (let [form (my-form)] 161: (-> form gui/pack! gui/show!))))
clojure从java加载代码还是非常简单的,这里多了一个绑定控件的name到swing id的动作。
3 总结
使用netbeans设计GUI,然后从clojure中加载界面代码还是非常方便的。主要是从clojure中调用java非常方便,参考Clojure is a better Java than Java。