C++ Builder 10.2 Tokyo > カレンダ > 日付けマーク付きカレンダクラスの実装例 (TCalendarクラスの派生)

```txt:動作環境
RAD Studio 10.2 Tokyo Update 3 
```

### 概要

- TCalendarには日付けを色付けする機能はない
- TCalendarViewにもない
- 派生クラス勉強してなかった
- TCalendarクラスから派生して作っちゃえ

### 実装

```CalendarUnit.h
//---------------------------------------------------------------------------

#ifndef CalendarUnitH
#define CalendarUnitH
//---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>
#include <Vcl.Grids.hpp>
#include <Vcl.Samples.Calendar.hpp>
#include <Vcl.WinXCalendars.hpp>
#include <Vcl.ComCtrls.hpp>
//---------------------------------------------------------------------------
// 追加した、日付マーク付きカレンダ用クラス
class PACKAGE TSampleCalendar : public TCalendar
{
protected:
	virtual void __fastcall DrawCell(int ACol, int ARow, const TRect &Rect, TGridDrawState AState);
	bool __fastcall isMarkedDate(int ACol, int ARow);
private:
	static const int kMaxDays = 37; // 月ごとのマーク情報の最大要素数. 最大6行: 5 * 7列 + 30, 31日 (=37)
	bool marked_month[kMaxDays]; // 月ごとのマーク情報
	static const int kFirstYear = 2015;  // インデックスの開始年 (配列のサイズと考えて定義する)
	static const int kNumYears = 30; // 配列用サイズ
	static const int kNumDaysInYear = 366; // うるう年に最大366日
	bool marked_year[kNumYears][kNumDaysInYear];  // 年ごとのマーク情報
	bool assignYearIndex(TDateTime srcDt, int *indexYear);
public:
	__fastcall TSampleCalendar(TComponent *Owner);

	TEdit *E_currentDatePtr; // 選択した日付を表示する対象. NULLだと無視
	TLabel *L_currentYearMonth; // カレンダの年月を表示する対象. NULLだと無視
	void __fastcall UpdateMonthMarkerList(int year, int month);  // marked_month[]を更新する
	void __fastcall YearMaker_init(void);  // marked_year[][]初期化
	void __fastcall YearMaker_addMark(TDateTime addDate);  // marked_year[][]へのマーク日追加
};
//---------------------------------------------------------------------------
// フォームのクラス
//
class TFormCalendar : public TForm
{
__published:	// IDE で管理されるコンポーネント
	TButton *B_prevMonth;
	TEdit *E_currentDate;
	TButton *B_nextMonth;
	TButton *B_preYear;
	TButton *B_nextYear;
	TButton *B_today;
	TLabel *L_yearMonth;
	void __fastcall B_prevMonthClick(TObject *Sender);
	void __fastcall B_nextMonthClick(TObject *Sender);
	void __fastcall B_preYearClick(TObject *Sender);
	void __fastcall B_nextYearClick(TObject *Sender);
	void __fastcall B_todayClick(TObject *Sender);
private:	// ユーザー宣言
	TSampleCalendar *m_sampCal;
public:		// ユーザー宣言
	__fastcall TFormCalendar(TComponent* Owner);
	__fastcall ~TFormCalendar();
};

//---------------------------------------------------------------------------
extern PACKAGE TFormCalendar *FormCalendar;
//---------------------------------------------------------------------------
#endif

```

```CalendarUnit.cpp
//---------------------------------------------------------------------------

#include <vcl.h>
#pragma hdrstop

#include <DateUtils.hpp>
#include "CalendarUnit.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TFormCalendar *FormCalendar;
//---------------------------------------------------------------------------
// コンストラクタとデストラクタ (フォーム)
//
__fastcall TFormCalendar::TFormCalendar(TComponent* Owner)
	: TForm(Owner)
{
	m_sampCal = new TSampleCalendar(this);
	m_sampCal->Name = L"SampleCalendar1";
	m_sampCal->Height = 200;
	m_sampCal->Width = 200;
	m_sampCal->Top = 50;
	m_sampCal->Left = 10;
	m_sampCal->Visible = true;
	m_sampCal->Parent = this;
	m_sampCal->Year = 2019; // 選択日の設定
	m_sampCal->Month = 7;
	m_sampCal->Day = 18;
	m_sampCal->E_currentDatePtr = E_currentDate;  // 選択日のTEdit
	m_sampCal->L_currentYearMonth = L_yearMonth;  // 選択した年月のTlabel

	// 年のうちのマーク付けしたい日付を設定しておく
	m_sampCal->YearMaker_init();
	m_sampCal->YearMaker_addMark(EncodeDate(2012,12, 31)); // out of range (テスト用)
	m_sampCal->YearMaker_addMark(EncodeDate(2018,12, 31));
	m_sampCal->YearMaker_addMark(EncodeDate(2019, 1,  1));
	m_sampCal->YearMaker_addMark(EncodeDate(2019, 1, 31));
	m_sampCal->YearMaker_addMark(EncodeDate(2019, 2, 1));
	m_sampCal->YearMaker_addMark(EncodeDate(2019, 2, 2));
	m_sampCal->YearMaker_addMark(EncodeDate(2019, 2, 28));
	m_sampCal->YearMaker_addMark(EncodeDate(2019, 3, 1));
	m_sampCal->YearMaker_addMark(EncodeDate(2019, 3, 31));
	m_sampCal->YearMaker_addMark(EncodeDate(2019, 7, 15));
	m_sampCal->YearMaker_addMark(EncodeDate(2019, 7, 31));
}

__fastcall TFormCalendar::~TFormCalendar()
{
	delete m_sampCal;
}
//---------------------------------------------------------------------------
// コンストラクタ (日付マーク付きカレンダ用クラス)
//
__fastcall TSampleCalendar::TSampleCalendar(TComponent *Owner) : TCalendar(Owner)
{
	ColCount = 7;
	RowCount = 7;
	FixedCols = 0;
	FixedRows = 1;
	ScrollBars = ssNone;
	Options = (Options >> goRangeSelect) << goDrawFocusSelected;
}

//---------------------------------------------------------------------------
// 日付マーク付きカレンダ描画処理 
void __fastcall TSampleCalendar::DrawCell(int ACol, int ARow, const TRect &ARect, TGridDrawState AState)
{
	// 1. 追加の処理
	if (ACol == 0 && ARow == 0) {
		UpdateMonthMarkerList(this->Year, this->Month);
		if (E_currentDatePtr != NULL) {
			E_currentDatePtr->Text = String().sprintf(L"%d/%d/%d", this->Year, this->Month, this->Day);
		}
		if (L_currentYearMonth != NULL) {
			L_currentYearMonth->Caption = String().sprintf(L"%d/%d", this->Year, this->Month);
		}
	}

	TSampleCalendar *myCal = (TSampleCalendar *)this;

	if (myCal->isMarkedDate(ACol, ARow)) {
		this->Canvas->Brush->Color = clGreen;
		this->Canvas->FillRect(ARect);
	}

	// 2. ベースクラスの処理
	TCalendar::DrawCell(ACol, ARow, ARect, AState);
}

//---------------------------------------------------------------------------
// 日付マーク付きカレンダ用クラス > メソッド
//
bool TSampleCalendar::assignYearIndex(TDateTime srcDt, int *indexYear)
{
	if (indexYear == NULL) {
		return false;  // error
	}

	Word year, month, day;
	DecodeDate(srcDt, year, month, day);
	int iy = year - kFirstYear; // marked_yearの位置に使うインデックス
	if (iy < 0 || iy >= kNumYears) {
		return false;  // error
	}

	*indexYear = iy;
	return true;
}

void __fastcall TSampleCalendar::UpdateMonthMarkerList(int year, int month)
{
	// Note: カレンダ表示前にコールすること

	// 1. クリア
	for(int idx=0; idx < TSampleCalendar::kMaxDays; idx++) {
		marked_month[idx] = false;
	}
	// 2. セット
	TDateTime adt_start = EncodeDate(year, month, 1); // 1: 月始めからコピーするため
	//   月内の日数計算
	TDateTime adt_end = IncMonth(adt_start, 1);
	int days = DaysBetween(adt_start, adt_end);

	// 3. marked_month[]のセット
	int awday = DayOfTheWeek(adt_start); // 年の中の日数 (1: 月曜日)
	int ayday = DayOfTheYear(adt_start); // 年の中の日数
	int iy;
	bool res = assignYearIndex(adt_start, &iy);
	if (res == false) {
		return;  // error
	}
	for(int idx=0; idx < days; idx++) {   // 31: 月ないの最大日数
		marked_month[awday + idx] = marked_year[iy][ayday + idx];
	}
}

void __fastcall TSampleCalendar::YearMaker_init(void)
{
	for(int iy=0; iy < kNumYears; iy++) {  // year
		for(int im=0; im < 366; im++) {  // month   // TODO: 366マジックナンバーをstatic const intにする?
			marked_year[iy][im] = false;
		}
	}
}

void __fastcall TSampleCalendar::YearMaker_addMark(TDateTime addDate)
{
	int ayday = DayOfTheYear(addDate); // 年の中の日数
	int iy; // 年のインデックス
	bool res = assignYearIndex(addDate, &iy);
	if (res == false) {
		return;  // error
	}
	marked_year[iy][ayday] = true;
}

bool __fastcall TSampleCalendar::isMarkedDate(int ACol, int ARow)
{
	int idx = ARow * 7 + ACol - 7; // -7: タイトル文字行は抜く
	if (idx < 0 || idx >= TSampleCalendar::kMaxDays) {
		return false;
	}
	if (marked_month[idx]) {
		return true;
	}
	return false;
}

//---------------------------------------------------------------------------
// フォームのイベント
//  月移動、年移動、今日への移動
void __fastcall TFormCalendar::B_prevMonthClick(TObject *Sender)
{
	if (m_sampCal == NULL) {
		return;
	}
	m_sampCal->PrevMonth();
}

void __fastcall TFormCalendar::B_nextMonthClick(TObject *Sender)
{
	if (m_sampCal == NULL) {
		return;
	}
	m_sampCal->NextMonth();
}

void __fastcall TFormCalendar::B_preYearClick(TObject *Sender)
{
	if (m_sampCal == NULL) {
		return;
	}
	m_sampCal->PrevYear();
}

void __fastcall TFormCalendar::B_nextYearClick(TObject *Sender)
{
	if (m_sampCal == NULL) {
		return;
	}
	m_sampCal->NextYear();
}

void __fastcall TFormCalendar::B_todayClick(TObject *Sender)
{
	Word year, month, day;
	DecodeDate(Now(), year, month, day);

	m_sampCal->Year = year;
	m_sampCal->Month = month;
	m_sampCal->Day = day;
}
//---------------------------------------------------------------------------
```

### デザイン画面

![コメント 2019-07-19 183757.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32870/cb66ddf9-9b7d-6530-ddee-93cfec0c69af.png)

### 実行画面

水色: 今日
緑色: マークを設定した日
上のTEditにはカレンダ選択日が
下のTLabelにはカレンダの年月が表示される。

![2019-07-19_18h38_39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32870/a1e9fba5-e64a-88b7-6922-a5fc17a4c0a9.png)

![2019-07-19_18h38_56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32870/0e6579dd-9a19-9ea6-8972-086385b2c6b4.png)

![2019-07-19_18h39_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32870/9e0b8008-8b7d-fe33-e5ac-5bf8e074b6c3.png)


### 備考 (駄目なところ)

- フォームのコンストラクタで派生クラスのインスタンス化しているけど、プロダクトコードではこういう使い方にはしない
- CalendarUnit.cpp/.hにフォームも派生クラスも実装
    - 分けたほうがいいが、今回は学習用実装のためこのようにしている
- 366のマジックナンバー
    - マジックナンバーにしないほうが可読性が良くなる
- メソッド名や変数名は最適化してない
    - ライブラリ化する時点で最適化する
- メソッドのPublicやPrivateも今後再検討する
- マークの仕方(緑色の塗りつぶし)はDrawCellの実装を変えることで対応できる
    - 丸をつける、など
- E_currentDatePtr, L_currentYearMonthはprivateにしてセッターを用意するほうがお行儀が良い
- DrawCell()はprotectedで定義
    - [こちら](http://docwiki.embarcadero.com/RADStudio/Rio/ja/%E5%88%9D%E6%9C%9F%E5%80%A4%E3%81%AE%E5%A4%89%E6%9B%B4)を参考にしたが、~~protectedである必要がいまいち不明~~
        - 追記: 2019-07-21 Protectedにしているのは、「このクラスから派生されるクラス」での動作を考慮したもの、と[コメント](https://stackoverflow.com/questions/57110582/override-of-drawcell-from-tcustomgrid-why-protected?noredirect=1#comment100759877_57110582)いただいた。

### 関連

- [C++ Builder 10.2 Tokyo > 派生クラス + オーバライド > TCalendarクラスを派生して、DrawCell()をオーバライドする](https://qiita.com/7of9/items/d0ce720f16b6fe4a46e5)
- [Python3 > datetime > カレンダの特定日に印を付ける (C++ BuilderのTCalendarのARow, ACol計算用)](https://qiita.com/7of9/items/b04d427e39995ce29b0f)

### 備考

とあるサイトでみた、実装コードのないアドバイス

> DrawCell()をオーバライドすればいい

コードがないから、そこに到達するのにすごく時間がかかった。

一行のコメントは有用ではあるが、本当に役に立つのは実行可能なコード。そういうものをQiita記事として増やしていきたい。

また、Delphiの実装例(override)は多いが、C++ Builderの実装例はなかなか見つからなかった。この点は公式ドキュメントも改善されたらいいのになぁ。

勉強にはなった。
(まだ間違っているだろう）


### バグ修正
(追記 2019-07-22)

マーク日付けの次の日も色が変わるようになっていた。
下記のように実装を変更して対処した (`this->Canvas->Brush->Color`を元の色に戻す)。


```cpp
//---------------------------------------------------------------------------
// 日付マーク付きカレンダ描画処理 
void __fastcall TSampleCalendar::DrawCell(int ACol, int ARow, const TRect &ARect, TGridDrawState AState)
{
	// 1. 追加の処理
	if (ACol == 0 && ARow == 0) {
		MonthMaker_updateList(this->Year, this->Month);
		if (E_selectedDatePtr != NULL) {
			E_selectedDatePtr->Text = String().sprintf(L"%d/%d/%d", this->Year, this->Month, this->Day);
			// 変更不可能な形にする
			E_selectedDatePtr->Color = cl3DLight;
			E_selectedDatePtr->ReadOnly = true;
			E_selectedDatePtr->TabStop = false;
		}
		if (L_currentYearMonthPtr != NULL) {
			L_currentYearMonthPtr->Caption = String().sprintf(L"%d/%d", this->Year, this->Month);
		}
	}

	TSampleCalendar *myCal = (TSampleCalendar *)this;

	TColor precol = this->Canvas->Brush->Color;
	if (myCal->isMarkedDate(ACol, ARow)) {
		this->Canvas->Brush->Color = clGreen;
		this->Canvas->FillRect(ARect);
	}

	// 2. ベースクラスの処理
	TCalendar::DrawCell(ACol, ARow, ARect, AState);

	this->Canvas->Brush->Color = precol;
}
```