リファクタリング〜スケジュール管理ソフトをS!アプリで作ってみよう(その8)

前回で、まともなカレンダーが表示できるようになった。

ただし、Canvasのクラスで全てを処理している。いずれは画面は以下の2つの部分からなるようにしたい。

  1. カレンダー表示部

    カレンダーは1ヶ月、2ヶ月、1週間、2週間、1日と表示を切替えられる。
  2. 1日の予定の項目とToDoの表示部
上記の事を、1つのクラスでやろうとすると、クラスが大きくなりすぎてメンテナンス性が悪くなる。1つのクラスは1つの事に集中した方がよい。以下の3つに分割するようにする。
  1. カレンダー表示クラス
  2. 1日の予定とToDo表示クラス
  3. Canvasとキー入力を処理するクラス
1.と2.の部分が見た目(View)の部分を担当する。3.の部分で制御(Control)を行う。
ViewとControlとくれば、Modelの部分は?となるのだが、ここはもう少し後で実装する。前回までで、1.と3.の部分のコードは出来ているので、それを基にクラスの分割を行う。

プログラムの作成

1.の部分のコードとして、MonthlyCalendarという以下のクラスを作成する。


package com.ettem.scheduler;

import java.util.*;
import javax.microedition.lcdui.*;

/**
* 月間カレンダーを表示する。
*/
public class MonthlyCalendar {
private Calendar dateToDisplay; // この日のカレンダーを表示する。
public int x, y; // カレンダーの開始位置。
public int width, height; // カレンダーの幅と高さ。
private Font font; // フォント。

/**
* 月間カレンダーを構築します。
* @param canvas カレンダーを描画するCanvas
* @param x カレンダーの開始X座標
* @param y カレンダーの開始Y座標
* @param width カレンダーの幅
* @param height カレンダーの高さ
* @param dateToDisplay カレンダーで描画すべき日
*/
public MonthlyCalendar(Canvas canvas, int x, int y, int width, int height,
Calendar dateToDisplay) {
this.dateToDisplay = dateToDisplay;
this.x = x;
this.y = y;

// 指定されたX座標、幅では画面からはみ出る場合は右端までにする。
if (x + width > canvas.getWidth()) {
width = canvas.getWidth() - x;
}
this.width = width;

// 必要な画面の高さを求める(月の行 + 曜日カラムヘッダ + 6週間分)
font = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_SMALL);
int needHeight = font.getHeight() * 2 + ((font.getHeight() * 5) / 2) * 6;

// 指定された高さが、必要以下の場合は強制的に高くする。
if (height < needHeight) {
height = needHeight;
}

// 指定されたY座標、高さでは画面からはみ出る場合は下端までにする。
if ( y + height > canvas.getHeight()) {
height = canvas.getHeight() - y;
}
this.height = height;
}

/**
* 描画すべき日付を設定
* @param date
*/
public void setDate(Calendar date) {
dateToDisplay = date;
}

public void paint(Graphics graphics) {
Calendar firstInMonth; // その月の最初の日
Calendar lastInMonth; // その月の最後の日

// 描画前に白で画面をクリアする。
graphics.setColor(0xFFFFFF);
graphics.fillRect(x, y, x + width, y + height);

// その月の最初の日を求める
firstInMonth = Calendar.getInstance();
firstInMonth.setTime(dateToDisplay.getTime());
firstInMonth.set(Calendar.DAY_OF_MONTH, 1);

// その月の最後の日を求める
lastInMonth = Calendar.getInstance();
lastInMonth.setTime(dateToDisplay.getTime());
lastInMonth.set(Calendar.MONTH, lastInMonth.get(Calendar.MONTH) + 1);
lastInMonth.getTime(); // getTimeを呼び出さないと、次の処理が上手く行かない。
// setTimeの処理は遅延され、纏めて評価されるようだ。
lastInMonth.set(Calendar.DAY_OF_MONTH, 0);

// 色を黒に戻す。
graphics.setColor(0x000000);
// フォントの設定
Font font = Font.getFont(Font.FACE_SYSTEM,
Font.STYLE_BOLD, Font.SIZE_SMALL);
graphics.setFont(font);

/*
* 曜日のカラムを作成
*/
int dayOfWeekHeight = font.getHeight();
int dayOfWeekWidth = width / 8;
int dayOfWeekStringOffset = dayOfWeekWidth / 2;
String[] dayOfWeek = {
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};

for (int i = 0; i < 7; i++) {
graphics.drawString(dayOfWeek[i],
x + (i + 1) * dayOfWeekWidth + dayOfWeekStringOffset, y +0,
Graphics.TOP | Graphics.HCENTER);
}

/*
* 日付欄を作成
*/
int cellWidth = width / 8;
int cellHeight = (font.getHeight() * 5) / 2;
int dateStringOffset = 2;

// 線を描画
drawLine:
for (int i = 0, x = 0, y = 0; i < 6; i++) {
for (int j = 0; j < 7; j++) {
if (i >= 5 && j >= 2) {
break drawLine;
}

x = this.x + (j + 1) * cellWidth;
y = this.y + i * cellHeight + dayOfWeekHeight;
graphics.drawRect(x, y, cellWidth, cellHeight);
}
}

// 日付を描画
for (int i = 1, x, y = this.y + dayOfWeekHeight,
tmpDayOfWeek = firstInMonth.get(Calendar.DAY_OF_WEEK);
i <= lastInMonth.get(Calendar.DAY_OF_MONTH);
i++, tmpDayOfWeek++) {

if (tmpDayOfWeek > 7) {
tmpDayOfWeek = 1;
y += cellHeight;
}

x = this.x + tmpDayOfWeek * cellWidth;
graphics.drawString(String.valueOf(i),
x + dateStringOffset, y + dateStringOffset,
Graphics.TOP|Graphics.LEFT);
}

}
}

3.の部分のコードとして、MainCanvasという名前の以下のクラスを作成する。


package com.ettem.scheduler;

import java.util.Calendar;
import javax.microedition.lcdui.*;

/**
* メインの画面
* カレンダーを表示する。
* (その日の予定、ToDoも表示する予定)
*/
public class MainCanvas extends Canvas {
private Calendar now;
private Calendar dateToDisplay;
private MonthlyCalendar calendar;

public MainCanvas() {
now = Calendar.getInstance();
dateToDisplay = Calendar.getInstance();
dateToDisplay.setTime(now.getTime());
calendar = new MonthlyCalendar(this, 0, 0, getWidth(), 0, dateToDisplay);
}

protected void paint(Graphics graphics) {
calendar.paint(graphics);
}

protected void keyPressed(int keyCode) {
Calendar dateTmp = Calendar.getInstance();

switch (keyCode) {

// "*"キーが押されたら、1ヶ月前に戻る。
case KEY_STAR:
dateTmp.setTime(dateToDisplay.getTime());
dateTmp.set(Calendar.MONTH,
dateToDisplay.get(Calendar.MONTH) - 1);
dateTmp.getTime();
if (dateTmp.get(Calendar.MONTH) ==
dateToDisplay.get(Calendar.MONTH)) {
dateTmp.set(Calendar.DAY_OF_MONTH, 0);
}
break;

// "#"キーが押されたら、1ヶ月先に進む。
case KEY_POUND:
dateTmp.setTime(dateToDisplay.getTime());
dateTmp.set(Calendar.MONTH,
dateToDisplay.get(Calendar.MONTH) + 1);
if (dateTmp.get(Calendar.MONTH) >
dateToDisplay.get(Calendar.MONTH) + 1) {
dateTmp.set(Calendar.DAY_OF_MONTH, 0);
}
break;
}

calendar.setDate(dateToDisplay);
repaint();
}
}

Schedulerクラスも、クラス構成の変更に合わせて、クラス名はフィールド名を変更する。


/* 任意の月の月間カレンダーを表示する。
* MonthlyCalendarクラスを使うようにリファクタリング
*/
package com.ettem.scheduler;

import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;

public class Scheduler extends MIDlet implements CommandListener {
private Command exitCommand;
private MainCanvas mainScr;

public Scheduler() {
exitCommand = new Command("Exit", Command.EXIT, 1);
mainScr = new MainCanvas();
mainScr.addCommand(exitCommand);
mainScr.setCommandListener(this);
}

protected void destroyApp(boolean arg0) throws MIDletStateChangeException {

}

protected void pauseApp() {

}

protected void startApp() throws MIDletStateChangeException {
Display.getDisplay(this).setCurrent(mainScr);
}

public void commandAction(Command command, Displayable display) {
if (command == exitCommand) {
try {
destroyApp(false);
notifyDestroyed();
} catch (MIDletStateChangeException e) {

}
}
}
}