訊息

即然能改變時區, 在這裡可以同時展現許多不同時區的時鐘, 使用者可以自行增加或減少時鐘的數量. 首先是增加選單. 增加一個 "Edit" 選單, 裡面有 "Add Clock" 及 "Delete Clock" 選項. 在 "Controller" 中增加兩個 action: "addClock:" 及 "deleteClock". 連接選項至這兩個在 Controller 物件中的 actions. 至此, 使用者介面的部份就完成了. 當使用者選這兩個選項時, 便會呼叫 -addClock: 及 -deleteClock:

能增減時鐘數目後, 要如何同時更新所有的時鐘呢? 一個做法是記錄所有的時鐘, 一一通知. 這個方法比較麻煩, 又容易出錯. 比較好的方式是使用 GNUstep 的訊息中心.

這裡有一篇相關文章: NSNotificationCenter

首先是隨時鐘的數量調整使用者介面. 在此要記錄時鐘的總數量, 才不會把最後一個時間也刪除了.

Controller.h:

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

@interface Controller : NSObject
{
   id timeView;
   unsigned int totalNumber;
}

- (void) showCurrentTime: (id) sender;

- (void) addClock: (id) sender;
- (void) deleteClock: (id) sender;

@end

totalNumber 用來記錄時鐘的數量. 因為不從 Gorm 中產生程式碼, 所以在此要手動加入新的 actions.

Controller.m:

- (id) init
{
   self = [super init];
   totalNumber = 1;
   return self;
}

- (void) addClock: (id) sender
{
   TimeView *aView;
   NSWindow *mainWindow = [NSApp mainWindow];
   NSRect windowFrame, timeViewFrame;
   windowFrame = [mainWindow frame];
   timeViewFrame = [timeView frame];

   [mainWindow setFrame: NSMakeRect(windowFrame.origin.x,    
                                    windowFrame.origin.y,
                                    windowFrame.size.width+timeViewFrame.size.width, 
                                    windowFrame.size.height)
                display: YES];
   aView = [[TimeView alloc] initWithFrame: NSMakeRect(timeViewFrame.origin.x + totalNumber*timeViewFrame.size.width,
                                                       timeViewFrame.origin.y,
                                                       timeViewFrame.size.width,
                                                       timeViewFrame.size.height)];
   
   [[mainWindow contentView] addSubview: aView];
   RELEASE(aView);
   totalNumber ++;
}

- (void) deleteClock: (id) sender
{
   NSArray *subviews;
   NSWindow *mainWindow = [NSApp mainWindow];
   NSRect windowFrame, timeViewFrame;
   int i;
   windowFrame = [mainWindow frame];
   timeViewFrame = [timeView frame];

   subviews = [[mainWindow contentView] subviews];

   for (i = [subviews count]-1; i > 1; i--) 
     {
       if ([[subviews objectAtIndex: i] isMemberOfClass: [TimeView class]])  
       [[subviews objectAtIndex: i] removeFromSuperview];
       totalNumber--;
       [mainWindow setFrame: NSMakeRect(windowFrame.origin.x,
                                        windowFrame.origin.y,
                                        windowFrame.size.width-timeViewFrame.size.width,
                                        windowFrame.size.height)
                    display: YES];
       break;
     }
}

在 -init 中, 因為 Gorm 檔中已經有一個時鐘了, 所以 totalNumber 的啟始值是 1. 在 -addClock: 中, 計算所需要的視窗大小, 及放置新時鐘的位置. 把新時鐘加入視窗之後, 視窗會保留 (Retain) 這個物件, 因此可以將其釋放 (Release). 在 -deleteClock: 中, 也要隨時鐘數量改變視窗大小. 唯一的問題是: 沒有記錄所有的時鐘, 要如何刪除呢 ? 可以取後視窗所有的 subview, 刪除最後一個 TimeView 物件即可.

現在, 當使用者按下 "Get Current Time" 按鈕時, 只有第一個時鐘會更新時間, 因為這是唯一個有 outlet 連結的. 在此可以呼叫每一個視窗的 subview 以更新時間. 更好的方法是使用 GNUstep 的訊息中心. 可以參考相關的 Cocoa 文章.

基本概念是有一個發言者, 其許多的聽者. 當發言者講話時, 所有有登記的聽者都會收到訊息. 當使用者按下 "Get Current Time" 按鈕時, the "Controller" 便對所有的時鐘發出訊息. 以下是發言的方式:

Controller.h:

- (void) showCurrentTime: (id)sender
{
   [[NSNotificationCenter defaultCenter] postNotificationName: @"TimeViewShouldUpdateCurrentTime"
                                                       object: [NSCalendarDate date]];
}

實際上是對訊息中心 (Notification Center) 發言, 訊息中心再將其訊息轉給聽者. 在此必需指定訊息的名稱, 以便混洧. 每個訊息可以攜帶一個物件, 以方便傳遞需要的資料.

聽者必需先向訊息中心註冊, 才能接收訊息, 同時也必需註冊要接收訊息的名稱. 聽者只會收到相同名稱的訊息.

TimeView.m:

- (id) initWithFrame: (NSRect) frame
{
   self = [super initWithFrame: frame];

   box = [[NSBox alloc] initWithFrame: NSMakeRect(0, 0,
                                                  frame.size.width,
                                                  frame.size.height)];
   [box setBorderType: NSGrooveBorder];
   [box setTitlePosition: NSAtTop];
   [box setTitle: @"Local Time"];

   clockView = [[ClockView alloc] initWithFrame: NSMakeRect(0, 70, 
                                                            frame.size.width,
                                                            frame.size.height)];
   labelDate = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 45, 35, 20)];
   [labelDate setStringValue: @"Date: "];
   [labelDate setBezeled: NO];
   [labelDate setBackgroundColor: [NSColor windowBackgroundColor]];
   [labelDate setEditable: NO];

   labelTime = [[NSTextField alloc] initWithFrame: NSMakeRect(10, 15, 35, 20)];
   [labelTime setStringValue: @"Time: "];
   [labelTime setBezeled: NO];
   [labelTime setBackgroundColor: [NSColor windowBackgroundColor]];
   [labelTime setEditable: NO];

   localDate = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 45, 130, 20)];
   localTime = [[NSTextField alloc] initWithFrame: NSMakeRect(55, 15, 130, 20)];

   [box addSubview: clockView];
   [box addSubview: labelDate];
   [box addSubview: labelTime];
   [box addSubview: localDate];
   [box addSubview: localTime];
   RELEASE(clockView);
   RELEASE(labelDate);
   RELEASE(labelTime);
   RELEASE(localDate);
   RELEASE(localTime);

   [self addSubview: box];
   RELEASE(box);

   [[NSNotificationCenter defaultCenter] addObserver: self
                                            selector: @selector(setDate:)
                                                name: @"TimeViewShouldUpdateCurrentTime"
                                              object: nil];

   [self showCurrentTime: self];
   return self;
}

一行程式碼便完成註冊了. 在這裡指定誰是聽者 (addObserver:), 收到訊息時要呼叫的 method (selector:), 要收聽的訊息名稱(name:), 以及要接收的物件 (object:). 最重要的是發言者及聽者使用相同的訊息名稱, 如此兩者才能溝通. 在此, 發言者發出 TimeViewShouldUpdateCurrentTime 訊息, 所有登記要接收 TimeViewShouldUpdateCurrentTime 的物件都會收到訊號, 並且呼收 selector 所指定的 method. 在此, object: nil 指的是接收所有 TimeViewShouldUpdateCurrentTime 訊息所攜帶來的物件.

至此, TimeView 物件會接收到 TimeViewShouldUpdateCurrentTime 訊息. 一但收到了訊息, -setDate: 便會被呼叫.

TimeView.m:

- (void) setDate: (NSNotification *) not
{
   ASSIGN(date, [not object]);
   [date setTimeZone: [NSTimeZone timeZoneWithName: [box title]]];
   [date setCalendarFormat: @"%a, %b %e, %Y"];
   [localDate setStringValue: [date description]];
   [date setCalendarFormat: @"%H : %M : %S"];
   [localTime setStringValue: [date description]];
   [clockView setDate: date];
}

在這裡重新使用 -setDate:. 之前被圖形元件呼叫時為 -setDate: (id)sender, 但是被訊息中心呼叫時為 -setDate: (Notification *) not. 傳入值不同. 使用 [not object] 可以取得訊息所攜帶的物件.

最後, 釋放時鐘時要記得移除在訊息中心的註冊, 不然會造成程式的不穩定.

TimeView.m:

- (void) dealloc
{
   [[NSNotificationCenter defaultCenter] removeObserver: self];
   RELEASE(date);
   [super dealloc];
}

總結來說, 發言者向訊息中心發出具有特定名稱的訊息, 並可攜帶物件. 聽者向訊息中心登記要接收訊息的名稱. 一旦訊息中心收到發言者發出的訊息, 便會通知有登記的聽者. 這是聽者所登記的 method 就會被呼叫, 並傳入 NSNotification 物件, 內含其攜帶的物件.

因為 -setDate: 的傳入值改變了, 程式中有些部份要隨之修改. 因為相對簡單, 在此就不多述.

即然可以手動更新時間, 也可以自動更新時間. NSTimer 可以定時呼叫特定 method, 並重複呼叫. 在此使用 NSTimer 讓時間自動更新.

首先是增加一個選單 Timer, 並加入兩個選項: Start 及 Stop. 在 Controller 中加入 -startTimer: 及 -stopTimer: 兩個 actions, 並將選項連接到 actions 上.

圖形 12.31. Connect menu action

Connect menu action
Connect menu action
Connect menu action

在 Controller 中加入這兩個新的 actions.

Controller.h:

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

@interface Controller : NSObject
{
   id timeView;
   unsigned int totalNumber;
   NSTimer *timer;
}
- (void) showCurrentTime: (id) sender;
- (void) addClock: (id) sender;
- (void) deleteClock: (id) sender;

- (void) startTimer: (id) sender;
- (void) stopTimer: (id) sender;

@end

Controller.m:

- (void) startTimer: (id) sender
{
  timer = [NSTimer scheduledTimerWithTimeInterval: 1
                                           target: self
                                         selector: @selector(showCurrentTime:)
                                         userInfo: nil
                                          repeats: YES];
}

- (void) stopTimer: (id) sender
{
  [timer invalidate];
}

如此就完成了. 在 NSTimer 中設定好時間的間隔, 目標物件, action, 及是否重複. NSTimer 在固定時間會呼叫 -showCurrentTime:. 使用 -invalidate 可以停止 NSTimer. 使用 NSTimer, 便不用使用多行緒 (Thread), 也不會延遲使用者輸入. 在 gnustep/user-apps/Finger, 是另一個避免使用多行緒, 又不會延遲使用者輸入的程式範例.

注意

在這個例子中, NSTimer 是被自動釋放的 (autoreleased), 因此有可能在任何時間從記憶體中消失, 造成程式的不穩定. 最好是在 -startTimer: 中使用 RETAIN() 將其保留, 並在 -stopTimer: 中將其釋放. 並且在 -startTimer: 中確保只有一個 NSTimer 存在. 這些部份也很簡單, 在此不多述.