大家好。感謝各位觀看本次示範。我將介紹一些 JMP 類,以及如何在 JSL 中使用它們來實作一些動態且可擴展的介面行為。
我叫本‧巴雷特,在IDEXX Laboratories工作。我在那裡當工程師已經大約10年了。我使用過各種應用程序,包括LabVIEW、MATLAB,也接觸過C#。總的來說,在所有這些語言中,我一直都傾向於使用物件導向的程式設計風格。
不知為何,這對我來說感覺很直觀。然而,當我開始在 JSL 中使用物件時,我發現這種直覺並不適用。我需要克服一些細微差別,但一旦我弄清楚如何使用它們,就發現它們非常有用。
我先簡單概述一下。我不會在這裡深入講解JMP類,只想分享我剛開始使用它們時的探索過程。我會分享一些我遇到的難題、遇到的困擾以及我是如何克服的,最後還會舉一些我實際應用這些類別的精彩例子。
以下是大致的議程,看看我能不能嚴格按照計劃進行。我想先簡單談談作用域,因為我發現這在我實現某些類別時一直是個問題,而這也會引出我總結的一些最佳實踐。我還想談談JSL中的類別與其他語言(例如C#)中的類別有何不同。之後,我會展示一些函數式範例程式碼。
不要指望在這裡深入講解JSL中的類,甚至也不會講解物件導向設計原則。希望這篇文章能幫助你避免一些在深入研究類時遇到的難題。
這是議程,我已經全部講解過了。這裡快速概述一下JSL中可用的不同作用域。我不敢說我完全理解它們之間的互動方式以及如何使用它們,但我有一些基本的概念。
這裡有四種通用的作用域:本地作用域、全域作用域,以及自訂命名空間。本文檔概述如何與每種作用域進行互動。如果您想了解更多信息,我強烈建議您查看在線文檔。但總的來說,我發現,在處理類別時,使用命名空間是最佳方法。
如果需要在類別中引用命名空間,請建立自訂命名空間,這幫助我避免了很多作用域和衝突問題。我還發現,在類別中,Here 命名空間不太可靠,所以我盡量避免使用預設名稱為 Here 並將其值設為 1 的情況。此外,使用局部變數也很有幫助,只要避免使用 local Here 指令即可。但這需要更多維護工作,因為您必須明確定義哪些變數是局部變數。不過,隨著我們進行一些演示,這些內容會更加清晰。
我剛才講解了這些最佳實踐。避免使用預設名稱,使用自訂命名空間,以及類別。你可以把類別看成是一個可以複製的命名空間,這樣記憶體中就可以同時存在多個副本。 JSL 類別和你可能熟悉的類別有什麼區別?我確信類中還有更多人們期望看到的特性,但我認為這五點已經相當全面地體現了人們對物體的期望。
封裝、繼承、多型、複製和抽象。繼承、多態和抽象彼此相關,JSL 中的類別對封裝、繼承和複製提供了相當不錯的支持,但在實現多型行為和抽象方面存在一些細微差別。我將簡要討論這五點。
封裝機制得到了很好的支持。定義類別時,你會定義與該類別關聯的屬性和方法。但要注意的是,這些說法都不成立。你無法定義類別的真正私有成員。所有成員都是公共的。這就像命名空間一樣,類別的成員和屬性都可以全域存取。
繼承也是支持的。如果您建立一個派生自另一個類別的類,則該子類別將繼承父類別的成員。關於多態性有一些細微差別,我們稍後會詳細介紹。為了真正實現多態性,需要一種特定的變通方法,才能使父類別在其自身方法中正確地呼叫子方法。
複製,這正是使用類別的關鍵。你可以實例化同一個物件的多個副本,每個副本都有自己獨立封裝的參數和方法,而且我還沒遇到什麼問題。
接下來是抽象。 JSL 類別中並沒有明確的抽象概念,但你可以定義一個父類,並為其添加一些預設方法,這些方法可能會傳回錯誤,或者只是執行一些基本行為。如果你熟悉 C# 或其他語言,這可能更接近虛擬方法而不是抽象方法。以上是 JSL 類別與其他語言類別之間差異的簡要概述。在對 JSL 類別進行如此快速的概述之後,我將切換到其他內容。
我們已經開啟了 JMP 實例。我這裡有一些非常簡單的例子。首先,我想先講一個簡單的例子,然後再講一個稍微複雜一點的例子,最後講一個非常非常複雜的例子。我想講的第一個例子是一個非常基本的進度條。
這些演示都旨在讓 GUI 開發更具可擴展性和模組化,從而充分利用物件的功能。在這個進度條示範中,我將快速運行一下,以便您了解它的功能。進度條的基本實作是我在網路上某個貼文中找到的,當時有人在問相關問題。我會向您展示進度條的詳細製作過程,但這些細節對本次演示來說並不重要。
這段程式碼主要是設定一些東西來定義進度條的外觀。它創建了一個窗口,創建了…你可以看到我們在這裡創建了對象,稍後我們會講解這些對象的具體程式碼。然後它只是把一些東西放到一個視窗中,加入一些隨機元素來表示你正在運行的任務。接下來,它會像進行一些處理一樣進行迭代,並使用這些方法更新進度條,這些方法本身就是類別定義的一部分。
這裡沒有轉折點,但這個物件封裝了創建此視覺化效果的所有行為。讓我打開這個視窗。我們現在進入代碼部分。我將轉到進度條類別的定義,我們先快速最小化這些視窗。您可以看到,使用 `define class` 呼叫來定義類,給它命名,這就是實例化它的方式,然後在類中定義變數也很簡單……定義它們的方式與定義其他變數的方式相同。
現在,這個值被賦給了類別的實例,所以你可以透過類別引用加上類別名稱來存取它。類別中的方法定義方式與函數定義方式相同。它們本質上是名稱不同的函數,並被賦值給對應的類別物件。我之前提到過,類別就像一個命名空間,你可以對其進行操作……創建類別的實例後,你可以操作所有這些內容。你可以加入屬性,這些屬性將被賦值給你所建立的物件。你可以使用 JMP 類別執行各種非常動態的操作。
我不會詳細解釋這一點。我只想重點說明這個進度條是如何創建的。初始化函數會被運行。我把它放到側邊欄視圖裡。在這個例子中,當你建立物件時,這個初始化函數就會運行。新物件的呼叫會自動執行_init函數。
初始化時,這裡會設定一個預設配置,但你也可以根據需要提供任何配置。這段程式碼會設定所有基本內容,最後會呼叫建置指令。建置方法定義在這裡。在建置方法中,我們會建立所有用於實現進度條外觀的物件。我們有容器,還有一些間隔框,這些間隔框會放大和縮小以創建進度效果。再次說明一下,這個功能來自 JMP 社群的一篇帖子,我手邊沒有原文。
這裡設定了進度條的所有細節,包括它的外觀。您可以看到這裡有一些輔助方法,可以更改高度或寬度,您可以呼叫這些方法。接下來,如何與進度條互動並更改進度量呢?這些都是方法,它們具有不同的功能。例如,`set` 方法會將進度百分比明確地設定為一個值。 `step` 方法會將進度條前進一定的步長,然後您可以定義任何其他您想要的功能。
你可以精確地告訴它有多少步,然後在每次迭代時推進它。你可以根據具體任務的需要修改這裡的方法。現在我們有了上下文,再回到之前的例子,這非常簡單明了。我們只是創建了一個對象,然後在…當我們在這裡設定新視窗時,為了利用這個對象,我們必須能夠從中獲取實際的顯示元素。
在進度條中,上層容器就是這個 H 列錶框。但如果我們取得對象,實際上只是傳回了該值或該容器。在新視窗定義中,如果我們想將進度條插入到特定位置,我們可以直接獲取對象,或者獲取我們創建的對象並進行操作……這可以解釋為類似“獲取面板”這樣的操作。這樣會更清晰,但目前我們是這樣寫的。
總之,這就是插入的方法。然後,我們仍然引用這個對象,並逐步執行主欄,將其設定為輔助欄的明確值。我們正在更新這裡的文字。再執行一次,以便查看其效果。同樣,我們只是輸入一些隨機值並進行更新,您可以想像如何將其插入到其他任務中。
還有一點要指出,我發現關閉進度條很有用。如果你這樣做,下次它嘗試迭代時就會失敗,因為它不再擁有對視窗的引用。你已經關閉了進度條,現在當你執行這個更新來嘗試更新視窗時,它就無法做到了。所以它會回傳一個錯誤。
因為所有程式碼都封裝在一個 try 語句中,所以如果執行失敗,就會跳到這裡。因此,如果你需要中斷處理,這是一種快速簡便的方法,我之前就多次使用過。這就是進度條,實現起來非常基礎。你可以用很多方式自訂它,例如從中獲取信息,或做任何你想做的事情。
接下來我想講的內容稍微複雜一些,可能需要一些準備。我想在這裡展示一下這些表格。我使用的是大型班級範例表,並對其進行了一些隨機化處理,因為我想示範虛擬連接的功能。對於不熟悉虛擬連接的人來說,標準連接可以這樣實現……我們有一個包含大量分數的班級。每個學生都有多筆記錄。他們嘗試做某件事 10 次,並獲得了分數。表格裡只有他們的姓名、嘗試次數和分數,但我們還有其他資訊。我們想把這些資訊也導入到這裡。所以我們需要進行連接。我相信任何使用 JMP 的人都做過類似的事情。
這樣就能將這些值匯入這張表中,但還有另一種方法。所有這些名稱都是唯一的。我們可以將它們設定為連結 ID,然後在這張表中,我們可以引用這張表。現在我們實現了虛擬連接,可以看到所有這些資訊。回到這裡,我可以隱藏或顯示,現在我們可以在這張表中訪問這張表這一列的信息,而無需進行連接。我們也可以對這張表執行相同的操作。
這樣做有一些非常棒的優勢。如果我們進入列訊息,找到這裡的連結引用屬性,就可以設定它接受選取或排除狀態。如果我應用此設置,然後我來到這裡,當我選擇這一行時,它會選中所有相關的行。如果您執行匯總並連結到資料表或資料視圖,也會發生類似的情況。
這真是一個很棒的功能。我去年就開始用它了,很快就想在 JSL 腳本中實現自動化,最後我基本上寫了一個連結管理器。讓我清除這些。清除連結 ID。清除連結引用。現在它們只是普通的未連結表。
接下來,我將開啟範例連結管理員。我會運行一些程式碼。我們將設定所有表格,並初始化連結管理器,稍後我會講解背後的程式碼。
好了。不知道之前的錯誤是什麼,但現在連結管理器的基本視窗已經開啟了。圖形使用者介面(GUI)已經啟動,但還沒有連結任何東西。我們可以使用API來設定連結來源,這基本上和我之前設定連結ID的操作相同。運行之後,我們可以看到連結管理器已經更新了。
這裡出現了一個新的圖形使用者介面入口。現在我們可以連結其中一個表格了。又出現了一個新的入口。我們可以連結第二個表格了。又出現了第二個入口,現在我們就可以進入並修改這些連結配置了。
如果我想要這個分發,也就是想要第二個表,我可以設定選擇和排除條件……基本上,如果我在這裡選擇了某些內容,我可以讓它傳播到那裡。我們已經選擇了所有內容。現在,所有這些都應該被選中,此外,原始表中的來源表也應該被選中。
通常情況下,如果你想更新這些信息,你需要右鍵點擊,選擇“列信息”,然後找到“鏈接引用”,再找到“分發”或“接收”,修改你想要更改的內容,然後點擊“應用”,最後再去另一個表格重複操作。我想要的是更動態的方式,能夠讓你即時看到更改過程。你可以添加另一個表格,設定多個資料來源來完成所有這些操作,所有配置都在這裡,你可以隨時關閉某些功能,移除連結等等。
我發現這個工具在處理包含多個表格的大型資料集時特別有用。例如,你可能有一個大型匯總表,並且已經對連續數據進行了一些數據處理,現在你想將各個數據關聯起來,但又不想創建一個龐大的表格,因為那樣會變得難以管理。這個工具就是基於這些需求而發展出來的。
現在,如果我們要深入研究程式碼,這會稍微複雜一些。首先我要說明一點,這些程式碼都沒有經過最佳化。很多部分仍然只是……你可以稱之為初稿。我只是讓它能運作起來,還沒有進行任何清理工作。請記住這一點,因為我接下來會講解一些程式碼。
您可以看到我調用的初始化函數,它只是檢查是否存在對象,如果沒有,則創建一個新對象。可能需要改進的一點是,我最初的想法是,如果我同時運行多個項目,我可能需要多個鏈結管理器同時處於活動狀態,這或許有用,或許沒用,但目前這個類基本上是以單例模式實現的。
如果你願意,可以實例化更多實例,但這樣做在某些具體情況下會不太穩定,我稍後會詳細說明。本質上,連結管理器維護一些數組,這些數組定義了表格之間的連結方式、表格之間的連結關係以及連結的當前狀態。它提供了一些方法來更改連結並執行其他相關操作。
對於圖形使用者介面(GUI),我們在這裡實作了兩個類別。一個是連結配置類別。就是,如果我能打開…讓我打開連結管理器視窗。它在哪裡?就在這裡。
如果我們查看連結管理器窗口,連結配置就是這個可以更改接受/分發、選擇排除項等設定的框,這就是配置部分。然後是選項卡,連結來源選項卡就是其中之一。每個來源都有一個單獨的選項卡,因為連結來源有一些規則,例如哪些列可以鏈接,哪些列可以連結到該列等等。建立連結來源時,連結管理器會建立一個這樣的物件。
還有一大堆其他的事情在進行,例如檢查一些東西,設定一些事件來偵測表格何時關閉等等,你可以忽略這些。這裡才是重點。它創建了一個新的連結來源選項卡對象,並提供了表格。
差不多就是這樣。它會被添加到圖形使用者介面(GUI)中,我想應該就在這裡。這裡有個來源列表,所以這個內容會被加到它後面,它只是一個容器,你可以看到我們用這個函數來取得選項卡,也就是實際的顯示物件。這基本上處理了很多複製操作。每次我們創建一個新來源時,都會建立一個新的這樣的對象,並且會自動獲得所有功能。
如果我們進入「原始碼」選項卡,這裡也有一個初始化過程。它有自己的顯示對象,也就是一個頁面框,裡面有一些對齊操作,還有一個列表對象。這個列表物件包含了你可能用到的所有不同的連結配置。再次提醒,這可能還沒有完全優化,但你可以開始看出它的優勢……在 JMP 中建立動態介面行為可能更容易。我在其他很多地方都用過這個功能。
你們可能已經注意到我這裡有個召回庫,我之前提到過,它有一個更複雜的實作方式。我估計沒太多時間深入研究它。真希望我能多花點時間,因為這其實是我第一個使用 JMP 類別庫的專案。可惜的是,它也還處於初稿階段。這裡面有很多東西其實很容易就能簡化。
我想,我現在主要是希望我已經引導你思考如何利用 JMP 中的類別來實現更動態的行為,並使程式碼更簡潔。因為我知道,我以前嘗試過……我看過一些複雜且動態的 UI 程式碼,它們把所有東西都直接寫在了新建視窗函數裡。
在新視窗中,你會看到 20 個標籤頁,裡面全是各種功能和按鈕,各自執行不同的操作。而實際上,實現相同功能有更簡潔、更物件導向的方法。這種方法更易於維護、可擴展,也更易讀。我認為,在實現使用者介面時,使用類別有很多好處。
我不會深入探討與召回相關的內容。我唯一要補充的是,我將努力整理所有這些範例程式碼和召回庫,並將其放在一個公開的 Git 倉庫中,任何有興趣的人都可以查看、克隆、fork 倉庫,並隨意使用它,嘗試理解如何在自己的程式碼中利用 JMP 類別來提高效率。
我覺得剩下的唯一一點就是快速回顧我之前提到的作用域問題。我們已經看過一些例子,你也了解如何建立結構來實現這些動態 GUI,但現在我們需要了解其中的困難以及如何解決它們。我想一個很好的例子是…我們看看能不能在這裡示範一下。
如果我選擇其中之一,我之前提到的作用域問題就與你呼叫方法的方式以及它們繼承的作用域有關。我見過很多在 Here 命名空間中工作時出現的問題。我見過全域命名空間和方法之間的衝突。為了盡可能簡化問題,我發現一個萬無一失的解決方法,可以避免你在使用類別時遇到的任何作用域問題(這裡我不會示範具體方法,但這是一個很好的例子,可以讓你了解你可能想要如何操作),那就是使用局部修飾符。我喜歡這樣寫。
這裡唯一需要明確定義的變數就是這個。這裡,我們設定這個變數的目的是為了解析並建立用於產生引用的屬性。那段程式碼無關緊要。僅僅因為這個變數沒有被定義為局部變量,它就可能覆蓋另一個作用域中的變數。根據程式碼中呼叫此方法的位置,這個變數可能會導致衝突。
我發現避免類別的作用域衝突最簡潔的方法就是明確定義局部變數。我估計基本上所有方法都需要這樣做,因為大部分程式碼都是在我理解如何解決這些問題之前寫的。除了顯式命名空間標識之外,唯一能幫助你解決這些問題的方法就是使用顯式命名空間標識。
每當我編寫庫時,我基本上都是在一個新的自訂命名空間中編寫,並且我會嘗試將所有變數和方法或變數和函數定義在該命名空間中,這幫助我避免了衝突。
那麼,我想,實際上,還有一點需要討論,雖然這個例子不太恰當,那就是多態性和繼承的一些問題。嗯,或許在 recall 庫中討論這個問題會比較適合。
希望情況不會太複雜……我們可以略過一些實作細節,只關注存在的問題。根據目前的情況來看,這個「召回面板」本質上是一個容器,你可以設定它自動召回欄位的值。它需要被繼承,因此會有子類別呼叫它的一些行為。具體來說,就是這裡提到的這個方法。子類別需要實作自己的 build 方法版本。
現在,多態性的微妙之處就體現在這裡了。如果你嘗試從父類別中呼叫某個方法,例如你想呼叫 `build` 方法,而子類別重寫了 `build` 方法,那麼 `build` 方法就無法正確地傳遞給子類別。我無法解釋具體原因,但我認為這與後端類別的實作方式有關。不過,有一種方法可以繞過這個問題。我認為,最好在另一個庫中示範一下,但可惜的是,我目前不打算深入探討。
我想我就不深入討論細節了,只想說,你最後提到多態性,或者說你可以實現一個子類來重寫行為,但這可能無法按預期運行,這有點可惜,因為我認為繼承是類中最有用的方面之一。
雖然可以繼承行為,但在父類別中呼叫這些繼承的行為會稍微複雜一些。我想我會這樣表達。當我把這些資料放上網時,我會盡量提供一些更具體的例子,說明為什麼這樣做行不通。再次強調,我只是想概述如何在 Junk JSL 中將類別整合到 UI 設計中。
話雖如此,我還是就此打住,免得說太多了。再次聲明,我叫本·巴雷特,如果您有任何疑問或想了解更多相關內容,歡迎隨時與我聯繫。最後,非常感謝。