iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX系列 第 31 篇

lesson 22(S4): 實現JAVA介面的另一種方式:jmake-proxy

  • 分享至 

  • xImage
  •  

TL;DR

jmake-proxy可以把Java介面方法的實作方式和函式本身分開,並透過lisp-this傳入狀態。implementation可以是函式、hash-table或package;本文以按鈕計數為例,逐步示範這幾種寫法,以及如何依方法名稱分派實作。


在第一個互動元件:Hello Button & Action中,使用jinterface-implementation建立JavaFX的EventHandler:

(jinterface-implementation
 "javafx.event.EventHandler"
 "handle"
 (lambda (event)
   (format t "Action for Event ~A~%" event)))

除了jinterface-implementation外,ABCL還提供jmake-proxy。它同樣可以建立符合EventHandler的物件交給JavaFX使用,但可以讓我們更靈活地決定如何實作介面。

為介面實作加入狀態

Java介面會宣告必須實作的方法規格。例如,JavaFX的EventHandler要求實作handle(Event):

public interface EventHandler<T extends Event> {
    void handle(T event);
}

Button.setOnAction要求一個EventHandler物件:

button.setOnAction(handler);

這也是我們在jinterface-implementation使用的形式:

(jinterface-implementation
 "javafx.event.EventHandler"
 "handle" (lambda (event) ......))

不過,這種寫法把介面方法直接對應到函式;當實作需要保存自己的狀態時,就不夠方便。例如,以下要計算按鈕被按下的次數(make-btn-cnt-action-handler):

(defun create-count-scene ()
  (prog* ((btn-cnt (new "javafx.scene.control.Button" "Count"))
          (box (new "javafx.scene.layout.VBox" 50.5 (jnew-array-from-list (jclass "javafx.scene.Node")
                                                     `(,btn-cnt))))
          (result (new "javafx.scene.Scene" box 640 480)))
     (#"setAlignment" box (jfield "javafx.geometry.Pos" "CENTER"))
     (#"setOnAction" btn-cnt (make-btn-cnt-action-handler))
     (return result)))

可以透過閉包保存計數狀態:

(defun make-btn-cnt-action-handler ()
  (let ((cnt 0))
    (jinterface-implementation
     "javafx.event.EventHandler"
     "handle" (lambda (event)
                (#"show" (make-messagebox
                          (format nil "Clicked ~A times."
                                  (incf cnt))))))))

不過,使用閉包意味著必須把實作細節放在函式內部,不方便另外抽出單純的#'handle函式:

(defun make-btn-cnt-action-handler ()
  (let ((cnt 0))
    (jinterface-implementation
     "javafx.event.EventHandler"
     "handle" #'handle)))

若要將#'handle獨立出來,就必須改變它的參數形式,至少額外傳入一個可變狀態:

- (defun handle (event) ......)
+ (defun handle (state event) ......)

而jmake-proxy允許這種形式的改變。它的基本形式是:

(jmake-proxy interface implementation &optional lisp-this)
  • interface:Java介面名稱,或Java class reference。
  • implementation:負責實作介面方法的物件。ABCL接受函式、hash-table或package。
  • lisp-this:傳給實作函式的額外物件,可用來保存代理物件的狀態;省略時是NIL。

最後一個參數lisp-this就是傳給#'handle的狀態物件。

(defun make-btn-cnt-action-handler ()
  (let ((state (list :click-count 0)))  ;; state 需要可變的
    (jmake-proxy
     "javafx.event.EventHandler"
    #'handle
    state)))  ;; 傳遞給實現

傳入的是可變的 plist,因此可以透過getf讀取和更新狀態:

(defun handle (state method event) 
  (declare (ignore method event))
  (#"show"
    (make-messagebox 
          (format nil "Clicked ~A times."
                     (incf (getf state :click-count))))))  ;; 取得&變更狀態

上面的實作除了event以外,還有一個未使用的method參數;它代表目前呼叫的方法名稱。Java介面不一定只有一個方法,EventHandler只是要求實作handle(Event)。因此,可以先檢查方法名稱,再執行對應邏輯:

(defun handle (state method event) 
  (declare (ignore event))
  (if (string= method "handle")
      (progn
        (#"showAndWait"  (make-messagebox "Yes, it is handle"))
         (#"show"
              (make-messagebox 
                   (format nil "Clicked ~A times."
                        (incf (getf state :click-count))))))
        (#"show"  (make-messagebox "No, it is not handle"))))

除了依據不同的method執行不同邏輯(類似 switch-case),也可以建立上層路由,分派各方法的實作:

(defun method-route (state method &rest args)
  (cond
    ((string= method "handle")
     (apply #'handle state args))
    (t (prog nil
          (#"show" (make-messagebox "No Method Found"))))))

這麼一來#'handle就可以不需要判斷method,將參數移除:

(defun handle (state  event) 
  (declare (ignore  event))
  (#"show"
    (make-messagebox 
          (format nil "Clicked ~A times."
                     (incf (getf state :click-count))))))  ;; 取得&變更狀態

#'make-btn-cnt-action-handler改用分派器method-route:

(defun make-btn-cnt-action-handler ()
  (let ((state (list :click-count 0))) 
    (jmake-proxy
     "javafx.event.EventHandler"
    #'method-route   ;; 換成使用分派器
    state)))

使用 hash-table 或 package

以上示範了以單一函式作為implementation的方式。實際上,jmake-proxy是泛型函式(generic function),implementation也可以是hash-table或package。

使用 hash-table

使用hash-table時,不必自行實作method-route分派器;ABCL會依照方法名稱,在傳入的hash-table中尋找對應的函式。因此只要準備好hash-table即可:

(defvar *btn-cnt-action* (make-hash-table :test #'equal))  ;; 建立 hash-table

(setf (gethash "handle" *btn-cnt-action*)  ;; 設定 handle 對應的函式
  #'handle) 

#'jmake-proxy就可以直接使用定義的hash-table -- *btn-cnt-action*:

(defun make-btn-cnt-action-handler ()
  (let ((state (list :click-count 0)))
    (jmake-proxy
     "javafx.event.EventHandler"
    *btn-cnt-action*  ;; 換成使用hash-table
    state)))

使用 package

使用package時,可直接在指定的package中定義對應函式。由於前面已定義#'handle,也可以直接傳入目前的*package*:

(defun make-btn-cnt-action-handler ()
  (let ((state (list :click-count 0)))  ;; state 需要可變的
    (jmake-proxy
     "javafx.event.EventHandler"
    *package*  ;; 當前package
    state))) 

不過,函式參數更合適的形式是(lisp-this &rest args)。我嘗試省略&rest時會報錯,因此需要改成以下形式:

(defun handle (state  &rest event)   ;; 調整增加&rest
  (declare (ignore  event))
  (#"show"
    (make-messagebox 
          (format nil "Clicked ~A times."
                     (incf (getf state :click-count))))))

或是為了避免符號衝突,建立一個獨立的:btn-handler package:

(defpackage :btn-handler
  (:use :cl)
  (:export :handle))

(in-package :btn-handler)

(defun handle (state &rest event)
  (declare (ignore  event))
  (#"show"
    (abcl-javafx::make-messagebox 
          (format nil "Clicked ~A times."
                     (incf (getf state :click-count))))))
(in-package :abcl-javafx)  ;; 回到:abcl-javafx使用


(defun make-btn-cnt-action-handler ()
  (let ((state (list :click-count 0)))
    (jmake-proxy
     "javafx.event.EventHandler"
    (find-package :btn-handler)  ;; 使用 :btn-handler
    state))) 

我測試後發現,package和hash-table形式也會傳入方法名稱,只是文件中的函式簽名沒有列出。前面的範例忽略了event,所以不影響執行;若要同時處理方法名稱和其餘參數,可以使用&optional搭配&rest:

(defun handle (state  &optional method &rest event) 
  (declare (ignore method event))
  (#"show"
    (make-messagebox 
          (format nil "Clicked ~A times."
                     (incf (getf state :click-count))))))

最後可以建立Stage來檢視看看:

(let* ((scene (create-count-scene))
       (stage (new "javafx.stage.Stage")))
  (#"setScene" stage scene)
  (#"show" stage))

https://ithelp.ithome.com.tw/upload/images/20261014/20112470WyBLwN9Udf.png

本文示範了三種implementation形式:函式適合自行分派方法,hash-table可直接以方法名稱查找函式,package則可利用其中已定義的函式。需要讓代理物件保存資料時,透過lisp-this傳入狀態即可;可以從按鈕計數範例開始,依介面複雜度選擇實作方式。


上一篇
lesson 21 (S3): MessageBox 之外的更多 Dialog 預設類型
下一篇
lesson 23(S4): 單一檔案編譯成JAVA字節碼,保存成ABCL檔案
系列文
玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX 共 33 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言