Skip to Content
Plugin 開發Widget 模式深入

Widget 模式深入

第一個 Plugin 4 你已經做出一個有按鈕與數值的面板。這一章把 Widget 模式的規則講完整: 你的版面檔寫什麼、主程式怎麼排、使用者操作之後你會收到什麼、什麼時候畫面會重建。 共有 15 種控制項,各自的欄位與寫法在下一章:Widget 型別詳解。

視窗版的 Widget 面板
widget_demo 在視窗版

版面與狀態是兩份東西

每個控制項有兩半:

內容誰擁有什麼時候變
版面(.nxlay)型別、列 y、min_w/max_w/weight、高度 h、標籤、選項數、數值範圍、旗標你用 LoadLayout 交給 Core很少變:版面結構
狀態(屬性值)文字框裡的字、勾選與否、選了第幾項、進度多少、選項文字、表格列Core隨時:你用 SetWidgetProp 改,或使用者操作

狀態的唯一真相在 Core,主程式不自己記。 畫面上的控制項只是把 Core 的快照畫出來。 這就是 Widget 模式不必擔心繪圖執行緒與逾時的原因:繪圖時完全不會呼叫你的程式。

版面的規則:

  • 一個面板最多 256 個控制項(超過 LoadLayout 回 ERR_FULL)。
  • 每個控制項有你自訂的 widget_id(.nxlay 裡 [w.N] 的 N,0~65534),同一面板內不可重複。之後所有事件與 SetWidgetProp 都用它辨認, 不用陣列索引。
  • 在 init 裡呼叫 host->LoadLayout(ctx, 0, "資源名稱", 0)。第二個參數是面板編號, 目前每個掛載只有一個邏輯面板,編號恆為 0,傳別的值回 ERR_INVALID_PARAM。.nxlay 以 RCDATA 資源嵌進 DLL (開發時可用 NX_LAYOUT_F_FILE 讀檔,沒有資源可用時用 LoadLayoutData 吃記憶體內容),見第一個 Plugin 4。
  • 版面違反規則(例如 y 遞減、widget_id 重複、METER 沒有 num_max、label 超過 31 位元組)時,LoadLayout 回 ERR_INVALID_PARAM; 含主程式不認得或尚未實作的 type= 時回 ERR_NOT_SUPPORTED,整份載入失敗,不會載一半。 驗證失敗的原因(規則編號與 widget_id)會寫進你的 Plugin 的 Log(WARN),回傳碼本身只有錯誤類別。所以要檢查回傳值並自己 LogWrite, 並且在存檔前用 nxlayout --solve 或視覺化編輯器先驗證好版面。
  • .nxlay 是版面的唯一來源;要在 C 裡用 widget 編號時,nxlayout --emit-header 從它產生只含 WIDGET_ID_* 常數的 .h, .c 用 #include 引進,改了 .nxlay 一定要重新產生。
  • 版面載入一次就好。內容逐位元組相同的重新載入是 no-op;內容真的不同則整棵樹重建,已存在的控制項的值不保留。

版面怎麼決定:列、寬度、高度

你不寫座標。你只說:這個控制項在第幾列、至少要多寬、多出來的空間怎麼分。主程式依面板大小算出實際位置。 單位是抽象格:終端機版 1 格就是 1 個字元格,視窗版 1 格約等於一個平均字元寬。

列:y

  • y 是列號(0 起算),不是像素、也不是終端機座標。
  • y 相同的控制項在同一列並排。 宣告順序中,y 必須不遞減(不會自動幫你排序)。
  • y 可以跳號:中間就留一列空白。
  • 同一列的控制項,x 是「這個控制項左邊界」,後一個必須放在前一個的 x + min_w 之後。 兩個相鄰控制項之間的空隙就是你宣告的差距:後一個的 x − 前一個的 x + min_w。慣例留 1 格,所以第二個的 x 通常是「前一個的 x + min_w + 1」(widget_demo 全部這樣寫)。

寬度:min_w、weight、max_w

主程式對每一列這樣算:

  1. 先給每個控制項它的 min_w。
  2. 一列的 min_w 總和加上空隙超過面板寬度時,容不下,見下面「空間不夠怎麼辦」。
  3. 有剩餘寬度時,依 weight 的比例分給 weight > 0 的控制項;weight = 0 的永遠維持 min_w。
  4. max_w 非 0 時是該控制項長大的上限(必須 >= min_w)。
  5. 分不整除的餘數給宣告順序最前面的那個加權控制項,所以兩個版本算出來逐格相同。

兩個常見的新手錯誤:

  • SEPARATOR 若 min_w = 0 又沒有 weight,寬度就是 0,什麼都畫不出來。分隔線請給 weight=1。
  • 該長大的控制項(文字框、數值)記得給 weight,否則面板再寬它也只有 min_w。

高度:h

  • 大多數控制項一列高,h 留 0(自動)即可:自動=「到下一個有控制項的 y 為止」,最後一列則延伸到面板底。
  • GROUPBOX 一定要明確給 h(h = 0 會被拒絕),涵蓋它要框住的列數。
  • TABLE 的高度不是宣告的 h:主程式一律給它「到下一列 y 的距離」,且至少 3 列(標題列、分隔列、至少一列資料); 最後一列的話延伸到面板底。所以想讓清單多高,靠把下一列的 y 排在多遠來控制,而不是靠 h。

空間不夠怎麼辦

「完整容得下才可見」。 一個控制項只有整個矩形都放得進面板才會畫出來,沒有畫一半的情況—— 半個輸入框無法閱讀,也無法正確操作。放不下就整個隱藏。

隱藏的順序:

  1. 寬度不夠:先把這一列控制項之間的空隙全部擠成 0(不隱藏任何東西,一旦擠了就用零空隙排完這一列);還是不夠,由後往前隱藏標了 OPTIONAL 的控制項;再不夠,由後往前讓控制項不可見;連零空隙都放不下就整列不可見。
  2. 高度不夠:由後往前先隱藏「整列都標了 OPTIONAL」的列;還是不夠,再由後往前讓整列不可見。

因此宣告順序就是重要性順序:愈後面的愈先被犧牲。把重要的放前面,把可有可無的(例如次要說明、進階選項)標 OPTIONAL 並放後面。 你不能假設自己一定看得到:面板窄到某個程度,你的控制項就是不在。設計時用編輯器的預覽或 nxlayout --solve --w 40 --h 10 之類的參數確認窄面板下什麼還在。

還有一種隱藏是你主動要求的:SetWidgetProp(ctx, 0, widget_id, NX_WP_HIDDEN, &v)(v 是 NX_WKIND_I64,1 隱藏、0 顯示)。 被隱藏的控制項仍保留它的版面空間(不畫、不能聚焦、不能點)。它與「空間不夠」是兩件事,互不覆寫。

欄導引線(進階)

一個面板可以再宣告幾條欄導引線(最多 8 條),把不同列的控制項對齊到同樣的欄位,並讓各欄各自決定要不要長大、長大到多寬。 在 .nxlay 的 [panel] 區段宣告,控制項用 x_guide 綁到某一條線;執行期則是設在面板根(NX_WIDGET_ROOT)的 NX_WP_GUIDES 屬性。 只有在你需要「上下兩列對齊」時才用;沒有 [panel] 區段的版面照常排。 如何在編輯器裡設定導引線,見nxlayout 工具與視覺化編輯器的 Inspector 一節。

使用者操作之後:on_widget_event

使用者操作控制項,Core 先把狀態更新好並立刻返回,然後把 on_widget_event(ctx, panel_id, widget_id, evt) 排到 I2C Worker Thread 上呼叫你 (不是 UI Thread,見生命週期與執行緒)。所以你的處理程式裡可以直接 ExecSync 讀寫硬體,這正是設計目的。

事件是非同步交付的(操作當下你還沒被呼叫),且只有兩種 evt->kind(不認得的 kind 一律忽略):

evt->kind意思evt->val
NX_WEVT_CHANGED使用者改了值(evt->prop 是 NX_WP_VALUE)新值
NX_WEVT_ACTIVATED使用者觸發了它(按鈕被按下,或帶 ACTIVATABLE 旗標的唯讀控制項被點)一律是 NX_WKIND_U64 的點擊位置封裝

evt->val 怎麼讀:

型別怎麼讀
TEXTBOX、FILEPICKevt->val.u.str.ptr/len:新的字串。只在提交時才送:按 Enter,或把焦點移到別處;不是每打一個字送一次
CHECKBOXevt->val.u.i64:0 或 1
DROPDOWN、CHOICEevt->val.u.i64:選項索引(從 0 起算)
SPINevt->val.u.i64:新數值,是有號的,下限可以是負的
BUTTON(ACTIVATED)點擊位置封裝(用不到就忽略);按鈕沒有持續的狀態
帶 ACTIVATABLE 的 METER/CHART/IMAGE/LABEL/VALUE(ACTIVATED)點擊位置封裝 (v << 16) | h:高 16 位是垂直比例、低 16 位是水平比例(各 0~65535),鍵盤觸發時固定 NX_WPOS_KEYBOARD(0x80008000)。是無號值,先用 (pos >> 16) & 0xFFFF 與 pos & 0xFFFF 拆開

字串值 evt->val.u.str.ptr 只在這次呼叫期間有效,要留著請自己複製,不要存指標。

事件的順序與合併:

  • 同一個 widget 連續的 NX_WEVT_CHANGED 只在佇列尾端合併(只有最後一個值有意義);中間夾了別的事件就依序都送,順序與使用者操作一致。
  • 面板重新載入(內容不同的 LoadLayout)或 DestroyWidget 之前排入的事件作廢,不會送達。
  • evt->user_data 是排入佇列當下該 widget 的 NX_WP_USER_DATA。

事件回報用的是 widget_id,所以你的處理程式通常是一串 if (widget_id == WIDGET_ID_xxx && evt->kind == ...)。

改值:SetWidgetProp

host->SetWidgetProp(ctx, 0, widget_id, NX_WP_VALUE, &v) 改某個控制項的顯示值;v 是 NxPropVal,先把整個結構清成 0,再填 SizeOfStruct 與 kind。

  • kind 必須符合那個屬性,否則回 ERR_INVALID_PARAM(這個欄位存在就是讓你自我檢查):
型別kind 與內容
TEXTBOX、VALUE、FILEPICKNX_WKIND_STR(u.str.ptr/len,UTF-8)
CHECKBOXNX_WKIND_I64(非 0 就是勾選)
DROPDOWNNX_WKIND_I64=索引,必須小於選項數
CHOICENX_WKIND_I64=索引,必須小於宣告的 item_count
SPIN、METERNX_WKIND_I64,超出 num_min~num_max 會被夾到範圍內

其他型別(BUTTON、LABEL、SEPARATOR、TABLE、GROUPBOX、CHART、IMAGE)沒有可以這樣寫入的 NX_WP_VALUE,回 ERR_INVALID_PARAM; 它們各有自己的屬性,見Widget 型別詳解。

文字欄位上限:NX_WP_LABEL、NX_WP_AUX 每段最多 31 位元組(NX_WLABEL_MAX_BYTES),超過時 SetWidgetProp 回 ERR_FULL(容量不足,舊值保留); NX_WP_ITEMS 每個選項、NX_WP_ROWS 每列的名稱與值各最多 255 位元組,超過回 ERR_INVALID_PARAM(舊值保留)。影子表另有文字配額(每面板 512 KiB、全體 4 MiB),用盡是 ERR_FULL。

SetWidgetProp 不會觸發你自己的 on_widget_event:那個事件只描述使用者的行為。所以你不會因此陷入「改值 → 事件 → 又改值」的迴圈 (唯一例外:把 TEXTBOX 的 NX_WP_MAX_LENGTH 調小而截短了目前的字,會送一次 CHANGED,且在 SetWidgetProp 返回之後才交付)。

要讀回目前的值用 GetWidgetProp(任何執行緒都可呼叫,SetWidgetProp 返回後立刻讀得到新值;字串型屬性可先以 buf == NULL、buf_cap == 0 問大小)。

你可以在 init(UI Thread)設初值,也可以在 Worker 的任何 callback 裡更新;主程式下一次更新畫面時就會讀到新值;Widget 模式不需要另外呼叫 NotifyUiRefresh(第一個 Plugin 第 4 步就沒有呼叫)。

什麼時候畫面會重建

視窗版對「版面有沒有變」很在意:版面沒變,畫面只是就地更新內容;版面一變,正在編輯的文字框會先把打到一半的字提交(你會收到一次 NX_WEVT_CHANGED)再重新開始,輸入法的組字狀態會被取消。

你需要知道的規則:

  • 內容逐位元組相同的重新載入是 no-op。 所以「每秒重新載入一次相同版面」不會害使用者打不完字。 但更好的做法仍是 init 載入一次就不再載入。
  • 改變結構的是 LoadLayout/LoadLayoutData(內容真的不同)、CreateWidget、DestroyWidget;SetWidgetProp(值、選項、表格列、圖表樣本、隱藏等)都是狀態,不重建面板。
  • DestroyWidget 與內容不同的 LoadLayout 會使該面板在途的事件作廢(widget_id 之後可能被重用成別的型別);CreateWidget 不會。

實際的建議:不要在輪詢裡反覆呼叫會改變結構的函式。 要更新的是值,就用 SetWidgetProp。

視窗版的自繪 widget

視窗版的 widget 全部自繪(沒有任何系統原生控件),兩個版本畫出來的東西不同,但版面與行為一致(排版是同一份程式算的):

型別視窗版終端機版
TEXTBOX、CHECKBOX、DROPDOWN、BUTTON、SPIN、CHOICE、FILEPICK自繪(可互動)字元自繪
LABEL、VALUE、TABLE、SEPARATOR、METER、GROUPBOX、IMAGE自繪字元自繪
CHART沒有繪圖器,畫成 [?]字元長條圖

對你的影響:

  • 面板放不下時,放不下的 widget 整個不顯示(不會被裁成半個)。
  • 視窗版的 on_key_press 在 widget 吃掉的鍵不會送給你(文字框有鍵盤焦點時所有鍵都由它處理),見生命週期與執行緒。
  • 頻繁 SetWidgetProp 相同的值不會讓畫面閃;使用者正在輸入的文字框不會被你的 SetWidgetProp 覆寫(提交時以使用者輸入為準)。
  • 想在視窗版畫圖表,目前不行:CHART 在視窗版是 [?]。要在兩個版本都能看的圖,請改用Canvas 模式,或用 METER/VALUE 表達。

絕對不要做的事

  • 不要假設座標、格子大小或是哪個版本。 你在 on_widget_event 裡不知道也不該知道控制項被畫在哪裡。
  • 不要把「我看得到它」當前提。 面板變窄,OPTIONAL 與後面的控制項就消失了。重要資訊放前面。
  • 不要在事件處理裡想做很久的事。 它在 Worker 上,佔住它就佔住這個後端的所有硬體存取。

逐支函式的精確契約見 API 參考的 Widget 一節與Widget 詞彙;同一份文件也隨 PDK 附在 docs\NEXUS_TUI_PLUGIN_API_REFERENCE.md。