章 15. 農民曆

農民曆目前還是常常會用到. 在這裡製做一個計算農曆的程式, 主要是介紹 NSMatrix 的用法.

在這裡做一個顯示日期的 View, 如下:

圖形 15.1. CalendarView

CalendarView

太陽曆轉農曆不是這裡的重點, 有興趣人可以參考程式碼. 在這裡只列出其 Interface.

LunarCalendarDate.h

#ifndef _LunarCalendarDate_
#define _LunarCalendarDate_

#include <Foundation/NSObject.h>

@interface LunarCalendarDate: NSObject
{
  int lunarDay, lunarMonth;
}

- (void) setDate: (NSCalendarDate *) date;
- (int) dayOfMonth;
- (int) monthOfYear;
@end

#endif /* _LunarCalendarDate_ */

在 LunarCalendarDate 中設定日期, 即可得知農曆的月, 日. 這個轉換程式不能保證完全正確. 只能計算 1998-2003 年的日期.

接著, 使用 Gorm 建立如下的使用者介面:

圖形 15.2. 農民曆的使用者介面

農民曆的使用者介面

繼承自 NSView, 製做一個 CalendarView 類別. 使用這個類別做為 Custom Class 的類別. 設定大小為寬 240, 高 270. 在最上面加個 NSTextField, 用來顯示農曆.

在 CalendarView 中加入一個 "label" outlet.

圖形 15.3. 增加 outlet

增加 outlet

連結 "label" outlet 至 NSTextField

圖形 15.4. 連結 outlet

連結 outlet

將檔案存成 LunarCalendar.gorm. 接著要實作 CalendarView.

CalendarView.h

#ifndef _CalendarView_
#define _CalendarView_

#include <AppKit/AppKit.h>

@interface CalendarView : NSView
{
  NSBox *calendarBox;
  NSTextField *yearLabel;
  NSButton *lastYearButton, *nextYearButton;
  NSMatrix *monthMatrix, *dayMatrix;
  NSCalendarDate *date;
  NSArray *monthArray;

  /* Outlet */
  id label;
}

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

/* Used by interface */
- (void) updateDate: (id) sender;

@end

#endif /* _CalendarView_ */

CalendarDate.m

Setup basic header and functions

#include "CalendarView.h"
#include "LunarCalendarDate.h"

@implementation CalendarView

#define isLeapYear(year) (((year % 4) == 0 && ((year % 100) != 0)) || (year % 400) 
== 0)

static short numberOfDaysInMonth[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 3
1};

在這裡使用 NSMatrix 來顯示月份及日期.

- (id) initWithFrame: (NSRect) rect
{
   int i, j, count=0;
   NSImage *rightArrow, *leftArrow;
   NSButtonCell *monthCell, *dayCell, *tempCell;
   NSArray *weekArray;

   [super initWithFrame: rect];

增加一個 NSBox, 等下所有的圖形介面都會加入這個 NSBox.

   calendarBox = [[NSBox alloc] initWithFrame: NSMakeRect(0, 0, 240, 270)];
   [calendarBox setBorderType: NSGrooveBorder];
   [calendarBox setTitlePosition: NSAtTop];
   [calendarBox setTitle: @"Calendar"];

使用兩個按鈕及一個字串來顯示及調整年份. 其中的圖檔是 GNUstep 附加的. 當使用者按下按鈕時, -updateDate: 會被呼叫.

   yearLabel = [[NSTextField alloc] initWithFrame: NSMakeRect(85, 220, 60, 20)];
   [yearLabel setStringValue: @"This Year"];
   [yearLabel setBezeled: NO];
   [yearLabel setBackgroundColor: [NSColor windowBackgroundColor]];
   [yearLabel setEditable: NO];
   [yearLabel setSelectable: NO];
   [yearLabel setAlignment: NSCenterTextAlignment];

   leftArrow = [NSImage imageNamed: @"common_ArrowLeft.tiff"];
   rightArrow = [NSImage imageNamed: @"common_ArrowRight.tiff"];

   lastYearButton = [[NSButton alloc] initWithFrame: NSMakeRect(10, 220, 22, 22)];
   [lastYearButton setImage: leftArrow];
   [lastYearButton setImagePosition: NSImageOnly];
   [lastYearButton setBordered: NO];

   nextYearButton = [[NSButton alloc] initWithFrame: NSMakeRect(198, 220, 22, 22)];
   [nextYearButton setImage: rightArrow];
   [nextYearButton setImagePosition: NSImageOnly];
   [nextYearButton setBordered: NO];

   [lastYearButton setTarget: self];
   [lastYearButton setAction: @selector(updateDate:)];

   [nextYearButton setTarget: self];
   [nextYearButton setAction: @selector(updateDate:)];

   [calendarBox addSubview: yearLabel];
   [calendarBox addSubview: lastYearButton];
   [calendarBox addSubview: nextYearButton];
   RELEASE(yearLabel);
   RELEASE(lastYearButton);
   RELEASE(nextYearButton);

Matrix 可以將 NSCell 排成棋盤狀. NSCell 是精簡版的 NSView, 可參考 Introduction to Controls and Cells. 首先是要定義 NSMatrix 中的 Cell 的屬性, NSMatrix 會用這個 Cell 來顯示所有的 Cell. 每個 Cell 都有個標箋 (Tag), 以用來區份各個 Cell.

   monthArray = [[NSArray alloc] initWithObjects: 
                   @"Jan", @"Feb", @"Mar", @"Apr", @"May", @"Jun", 
                   @"Jul", @"Aug", @"Sep", @"Oct", @"Nov", @"Dec", nil];

   monthCell = [[NSButtonCell alloc] initTextCell: @""];
   [monthCell setBordered: NO];
   [monthCell setShowsStateBy: NSOnOffButton];
   [monthCell setAlignment: NSCenterTextAlignment];

   monthMatrix = [[NSMatrix alloc] initWithFrame: NSMakeRect(10, 165, 210, 50)
                                            mode: NSRadioModeMatrix
                                       prototype: monthCell
                                    numberOfRows: 2
                                 numberOfColumns: 6];

   for (i = 0; i < 2; i++)
      for (j = 0; j < 6; j++)
      {
         tempCell = [monthMatrix cellAtRow: i column: j];
         [tempCell setTag: count];
         [tempCell setTitle: [monthArray objectAtIndex: count]];
         count++;
      }
   RELEASE(monthCell);

   weekArray = [NSArray arrayWithObjects: @"Sun", @"Mon" @"Tue", @"Wed",
                                          @"Thr", @"Fri", @"Sat", nil];

   dayCell = [[NSButtonCell alloc] initTextCell: @""];
   [dayCell setBordered: NO];
   [dayCell setShowsStateBy: NSOnOffButton];
   [dayCell setAlignment: NSCenterTextAlignment];

   dayMatrix = [[NSMatrix alloc] initWithFrame: NSMakeRect(10, 20, 210, 120)
                                          mode: NSRadioModeMatrix
                                     prototype: dayCell
                                  numberOfRows: 7
                               numberOfColumns: 7];

   for (j = 0; j < 7; j++)
   {
      tempCell = [dayMatrix cellAtRow: 0 column: j];
      [tempCell setTitle: [weekArray objectAtIndex: j]];
      [tempCell setAlignment: NSCenterTextAlignment];
      [tempCell setEnabled: NO];
   }

   RELEASE(dayCell);

   count = 0;

   for (i = 1; i < 7; i++)
      for (j = 0; j < 7; j++)
         {
           [[dayMatrix cellAtRow: i column: j] setTag: count++];
         }

當 NSMatrix 被按下時, 會呼叫 Matrix 的 action, 在這裡設為 -updateDate:.

   [monthMatrix setTarget: self];
   [monthMatrix setAction: @selector(updateDate:)];

   [dayMatrix setTarget: self];
   [dayMatrix setAction: @selector(updateDate:)];

   [calendarBox addSubview: monthMatrix];
   [calendarBox addSubview: dayMatrix];
   RELEASE(monthMatrix);
   RELEASE(dayMatrix);

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

   return self;
}

在 dayMatrix 的內容因月份而改變, 所以當日期變時, 其日期的顯示要隨之改變.

- (void) setDate: (NSCalendarDate *) newDate
{
   int i, currentDay, currentMonth, currentYear;
   int daysInMonth, startDayOfWeek, day;
   NSCalendarDate *firstDayOfMonth;
   NSButtonCell *tempCell;
   LunarCalendarDate *lunarDate;

在這裡保留日期, 在 -dealloc 中釋放.

   ASSIGN(date, newDate);

更新年份.

   [yearLabel setStringValue: [date descriptionWithCalendarFormat: @"%Y"]];

更新月份.

   currentMonth = [date monthOfYear];
   [monthMatrix selectCellWithTag: currentMonth-1];

更新日期.

   currentYear = [date yearOfCommonEra];
   firstDayOfMonth = [NSCalendarDate dateWithYear: currentYear
                                            month: currentMonth
                                              day: 1
                                             hour: 0
                                           minute: 0
                                           second: 0
                                         timeZone: [NSTimeZone localTimeZone]];

   daysInMonth = numberOfDaysInMonth[currentMonth - 1];

   if ((currentMonth == 2) && (isLeapYear(currentYear)))
      daysInMonth++;

   startDayOfWeek = [firstDayOfMonth dayOfWeek];

   day = 1;

   for (i = 0; i < 42; i++)
   {
      tempCell = [dayMatrix cellWithTag: i];
      if (i < startDayOfWeek || i >= (daysInMonth + startDayOfWeek))
      {
         [tempCell setEnabled: NO];
         [tempCell setTitle: @""];
      }
      else
      {
         [tempCell setEnabled: YES];
         [tempCell setTitle: [NSString stringWithFormat: @"%d", day++]];
      }
   }

   currentDay = [date dayOfMonth];
   [dayMatrix selectCellWithTag: startDayOfWeek + currentDay - 1];

使用 LunarCalendarDate 計算農曆日期, 並在 label 中更新.

   /* Update label */
   lunarDate = [LunarCalendarDate new];
   [lunarDate setDate: date];
   [label setStringValue: [NSString stringWithFormat: @"%@ %d", [monthArray objectA
tIndex: [lunarDate monthOfYear]-1], [lunarDate dayOfMonth]]];
   RELEASE(lunarDate);
}

當這個程式開始時, 需要預設成當日時間. 但 CalendarView 不是 NSApp 的代理者, 所以無法知道程式何時始動. 但當 Gorm 被載入時, 會呼叫 -awakeFromNib, 因此可以使用 -awakeFromNib 來知道程式開始了.

- (void) awakeFromNib
{
  [self setDate: [NSCalendarDate calendarDate]];
}

最後, 要處理使用者輸入的部份. 所有使用者輸入都會進到 -updateDate:, 所以要依 sender 來做區別.

- (void) updateDate: (id) sender
{
   int i=0, j=0, k=0;
   NSCalendarDate *newDate;

   if (sender == lastYearButton)
   {
      i = -1;
   }
   else if (sender == nextYearButton)
   {
      i = 1;
   }
   else if (sender == monthMatrix)
   {
      j = [[[sender selectedCells] lastObject] tag] + 1 - [date monthOfYear];
   }
   else if (sender == dayMatrix)
   {
      k = [[[[sender selectedCells] lastObject] stringValue] intValue] - [date dayO
fMonth];
   }

   newDate = [date addYear: i
                     month: j
                       day: k
                      hour: 0
                    minute: 0
                    second: 0];

LunarCalendarDate 只支援 1998-2031, 因為超過的年份不與反應.

   if (([newDate yearOfCommonEra] > 1998) && ([newDate yearOfCommonEra] < 2031))
     [self setDate: newDate];
}

程式碼在此: LunarCalendar-src.tar.gz