面板

面板是視窗的一種, 可參考 Cocoa 的介紹 (英文): Windows and Panels. 在這個範例中, 當使用者在 NSBox 上單點時, 會出現一個面板, 讓使用者輸入時區, 以顯示不同時區的時間. NSPanel 是 NSWindow 的子類別 (Subclass), 因此在使用上和 NSWindow 相同, 也是需要一個控制者 (Controller) 來控制這個使用者介面 (View), 並處理由使用者介面傳入的動作 (Action). 在這裡也示範如何動態載入 Gorm 檔. GNUstep 內建許多常用的面板, 這裡也一併示範.

開啟 Gorm, 選擇 "Document -> New Module -> New Empty". 在 Palettes 中找到面板 (Panel).

圖形 12.22. Panel in Gorm

Panel in Gorm

從 Palettes 中拖出一個面板. 做出如下的使用者介面.

圖形 12.23. Interface of time zone panel

Interface of time zone panel

可以使用 Inspector 改變面板的大小. 以下是面板的屬性.

圖形 12.24. Panel attributes

Panel attributes

使用者介面到此完成, 現在要製做控制者 (Controller). 一般的作法是建立新的物件, 但是在這個簡單的程式中, 可以使用 TimeView 來控制面板. 這時 TimeView 這個類別, 一方面是主程式的使用者介面之一, 一方面又是這個面板的控制者. 首先產生 TimeView 類別 (繼承自 NSObject), 加入 zonePanel 和zoneField 兩個 outlets, 及 okAction: 和 cancelAction: 兩個 actions.

圖形 12.25. Outlets for time zone panel

Outlets for time zone panel

圖形 12.26. Actions for time zone panel

Actions for time zone panel

在這裡, 不產生 TimeView 物件 (Instance), 而是將面板的 NSOwer 設定為 TimeView.

在主視窗中選 NSOwner, 在 Inspector 中的 Attributes 中選 TimeView 即可.

圖形 12.27. Set NSOwner to TimeView class

Set NSOwner to TimeView class
Set NSOwner to TimeView class

如此便可以連結 NSOwner (TimeView 物件) 及面板. 連結 zonePanel 到面板, zoneField 到文字欄, 兩個按鈕到 okAction: 及 cancelAction:. 要注意連結面板的方式, 連結主視窗中的 NSOwner 及 GormNSPanel.

圖形 12.28. Connect outlet

Connect outlet
Connect outlet

存檔成 TimeZonePanel.gorm" 並離開. 在此不要產生 TimeView 的程式碼, 因為程式碼已經存在了. 在此只要把新的 outlet 及 action 加入程式碼中即可.

在 TimeView 的檔頭 (header) 中加入新的 outlet 及 action.

TimeView.h:

#import <AppKit/AppKit.h>
#import "ClockView.h"

@interface TimeView : NSControl
{
   id zonePanel;
   id zoneField;
   NSBox *box;
   NSTextField *labelDate, *labelTime;
   NSTextField *localDate, *localTime;
   NSCalendarDate *date;
   ClockView *clockView;
}

- (NSCalendarDate *) date;
- (void) setDate: (NSCalendarDate *) date;

- (void) okAction: (id) sender;
- (void) cancelAction: (id) sender;

@end

TimeView.m:

- (void) mouseDown: (NSEvent *) event
{
   NSRect titleFrame = [box titleRect];
   NSPoint windowLocation = [event locationInWindow];
   NSPoint viewLocation = [self convertPoint: windowLocation fromView: [self superview]];
   BOOL status = NSMouseInRect(viewLocation, titleFrame, NO);
   if (status == YES)
     {
       [NSBundle loadNibNamed: @"TimeZonePanel.gorm" owner: self]; 
       [NSApp runModalForWindow: zonePanel];
     }
}

當使用者在 TimeView 中按下滑鼠時, 會呼叫 TimeView 中的 -mouseDown: (繼承自 NSView). 在這裡計算滑鼠的位置及 NSBox 標題的位置. 如果滑鼠點在 NSBox 的標題內, 則使用 [NSBundle loadNibName: owner:] 載入面板, 並使用 [NSApp runModalForWindow] 等待使用者輸入, 可參考 Cocoa 的文件 (英文): "How Modal Windows Work".

最後完成 action:

TimeView.m:

- (void) cancelAction: (id) sender
{
   [NSApp abortModal];
   [zonePanel close];
}

- (void) okAction: (id) sender
{
   NSTimeZone *tempZone;
   tempZone = [NSTimeZone timeZoneWithName: [zoneField stringValue]];
   [NSApp stopModal];
   [zonePanel close];
   if (tempZone == nil)
      {
         NSRunAlertPanel(@"Warning!",
                         @"Wrong Time Zone !!",
                         @"OK", nil, nil);
      }
   else
      {
         [date setTimeZone: tempZone];
         [box setTitle: [tempZone description]];
         [self setDate: date];
      }
}

在 okAction: 中, 使用 GNUstep 內建的 NSRunAlertPanel, 提醒使用者輸入的時區不正確.

這個面版在使用上其實不很方便, 必需先用滑鼠點選 NSTextField 才能輸入. 有時使用鍵盤來控制使用者介面比起滑鼠來得方便. 在這裡做點改變, 讓使用者介面更容易使用.

當視窗出現時, 視窗中第一個接收使用者輸入的稱為 "First Responder" (第一反應者). 通常視窗本身即為 First Responder, 但在此希望 NSTextField 為第一反應者. 如此常視窗出現時, 鍵盤的輸入會直接進到 NSTextField, 而不是視窗. 視窗本身不接受鍵盤輸入. 因此, 可以使用 NSWindow 中的 -makeFirstResonpder: 來改變 First Responder.

在一般的程式中, Tab 鍵是用來在各圖形元件中切換. 在 GNUstpe 中, 指定圖形元件的 "nextKeyView", 即可使用 Tab 鍵來切換至下一個圖形元件. 藉由串連 "nextKeyView" 即可達到使用 Tab 鍵切換的功能.

最後, 每次在 NSTextField 中輸入完, 要用滑鼠選 "O.K." 按鈕很麻煩. 如果能在 NSTextField 中直接按 "ENTER" 鍵就方便許多. 其實 NSTextField 與 NSButton 都是繼承自 NSControl. 所以比照 NSButton, 將 NSTextField 的 target 及 action 指定好, 在 NSTextField 中按下 ENTER 鍵時就如果按下 NSButton 按鈕.

這些都是小改進, 但能使一個程式更方便使用許多.

首先, 在視窗中指定其 First Responder 為 NSTextField:

TimeView.m:

- (void) mouseDown: (NSEvent *) event
{
   NSRect titleFrame = [box titleRect];
   NSPoint windowLocation = [event locationInWindow];
   NSPoint viewLocation = [self convertPoint: windowLocation fromView: [self superview]];
   BOOL status = NSMouseInRect(viewLocation, titleFrame, NO);
   if (status == YES)
      {
         [NSBundle loadNibNamed: @"TimeZonePanel.gorm" owner: self]; 
         [zonePanel makeFirstResponder: zoneField];
         [NSApp runModalForWindow: zonePanel];
      }
}

一行程式碼即可. 如此, 當視窗出現時, 游標會自動出現在 NSTextField 等待輸入.

其次, 希望在 NSTextField 中按 ENTER 時, 等同於按下 O.K. 按鈕. 開啟 TimeZonePanel.gorm, 連接 NSTextField 至 NSOwner 的 -okAction:. 如此便完成了.

圖形 12.29. Connection NSTextField action

Connection NSTextField action
Connection NSTextField action
Connection NSTextField action

最後, 要串連各圖形元件, 以方便使用 Tab 切換. 在此將 NSTextField 的 nextKeyView 連結到 O.K. 按鈕, 將 O.K. 按鈕的 nextKeyView 連結到 Cancel 按鈕, 將 Cancel 按鈕的 nextKeyView 連結到 NSTextField. 如此便可以使用 Tab 來回切換了.

圖形 12.30. Connect nextKeyView

Connect nextKeyView
Connect nextKeyView

程式碼: Panel-src.tar.gz.