System.Windows.Forms.MonthCalendarで日付の色を変える

MonthCalendarで休日の色を分けたいので調査

Nicke Andersson
The writings of a .NET nut. 
http://nickeandersson.blogs.com/blog/2006/05/_modifying_the_.html

このソースを貼り付けて、クラスを作るとあっさり動いた。
と、思ったけど地雷が多くて一般公開したくない人の気持ちがよくわかった。

例えば、1ヶ月表示でないと、正しい範囲が取得できなくて変になる。
あとはフォントサイズ変えると表示がずれる。

CalendarDimensionsで3x4とかはわかるけど、そこから正確な座標と該当月の取得ロジックを書くのは時間がかかりそうなので断念。このコントロール一杯並べて逃げる。

フォントサイズは9ptと12ptだけ対応した補正を入れて回避。

ソース

以下のように、System.Windows.Forms.MonthCalendarを継承したクラスを作る

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO;
using System.Text;
using System.Threading;
using System.Windows.Forms;

namespace MonthCalendarExTest {
	/// <summary>
	/// カレンダーの指定日に色をつける
	/// 
	/// 元ネタ
	/// http://nickeandersson.blogs.com/blog/2006/05/_modifying_the_.html
	/// 
	/// 1ヶ月表示のみ対応。複数月対応やフォントサイズ変更対応は後から来た人に期待。
	/// </summary>
	public class MonthCalendarEx:System.Windows.Forms.MonthCalendar {
		protected static int WM_PAINT = 0x000F;
		private List<DateTime> _warningDates = new List<DateTime>();
		private Color _warningDateBackColor = Color.FromArgb(192,255,192,192);
		private Color _warningDateForeColor = Color.FromArgb(255,128,0,0);

		#region Properties
		public List<DateTime> WarningDates {
			get {
				return _warningDates;
			}
			set {
				_warningDates = value;
			}
		}

		public Color WarningDateBackColor {
			get {
				return _warningDateBackColor;
			}
			set {
				_warningDateBackColor = value;
			}
		}

		public Color WarningDateForeColor {
			get {
				return _warningDateForeColor;
			}
			set {
				_warningDateForeColor = value;
			}
		}
		#endregion

		protected override void WndProc(ref System.Windows.Forms.Message m) {
			base.WndProc(ref m);
			if(m.Msg == WM_PAINT) {
				Graphics graphics = Graphics.FromHwnd(this.Handle);
				PaintEventArgs pe = new PaintEventArgs(graphics,new Rectangle(0,0,this.Width,this.Height));
				OnPaint(pe);
			}
		}

		protected override void OnPaint(PaintEventArgs e) {
			base.OnPaint(e);

			if(WarningDates.Count <= 0)
				return;

			// 1ヶ月表示限定
			if(base.CalendarDimensions.Height != 1 || base.CalendarDimensions.Width != 1)
				return;

			// Create a list of those dates that actually should be marked as warnings. 
			List<DateTime> visibleWarningDates = new List<DateTime>();
			SelectionRange calendarRange = GetDisplayRange(false);
			foreach(DateTime date in WarningDates) {
				if(date >= calendarRange.Start && date <= calendarRange.End)
					visibleWarningDates.Add(date);
			}

			if(visibleWarningDates.Count <= 0)
				return;

			int leftMargin = 0;
			int center = (base.Width / 2);
			while((base.HitTest(leftMargin,center).HitArea != HitArea.PrevMonthDate
				&& base.HitTest(leftMargin,center).HitArea != HitArea.Date)
				&& leftMargin < Width
			) {
				leftMargin++;
			}

			// 上限と下限から、塗りつぶす領域の高さを算出する
			// 表示域の上限を取得
			int firstWeekPosition = 0;
			while((base.HitTest(leftMargin + 25,firstWeekPosition).HitArea != HitArea.PrevMonthDate
				&& base.HitTest(leftMargin + 25,firstWeekPosition).HitArea != HitArea.Date)
				&& firstWeekPosition < Height
			) {
				firstWeekPosition++;
			}

			// 表示域の下限を取得
			int lastWeekPosition = Height;
			while((base.HitTest(25,lastWeekPosition).HitArea != HitArea.NextMonthDate
				&& base.HitTest(25,lastWeekPosition).HitArea != HitArea.Date)
				&& lastWeekPosition >= 0
			) {
				lastWeekPosition--;
			}


			if(firstWeekPosition <= 0 || lastWeekPosition <= 0)
				return;

			// 塗りつぶす範囲のサイズ(1日分)
			int dayBoxWidth = (base.Width - leftMargin * 2) / (base.ShowWeekNumbers ? 8 : 7);
			int dayBoxHeight = (int)(((float)(lastWeekPosition - firstWeekPosition)) / 6.0f);

			// 指定色で背景と文字を描画
			using(Brush warningBrush = new SolidBrush(_warningDateBackColor)) {
				foreach(DateTime visDate in visibleWarningDates) {
					DrawText(e.Graphics,warningBrush,calendarRange,visDate,dayBoxWidth,dayBoxHeight,firstWeekPosition,lastWeekPosition,leftMargin);
				}
			}
		}

		private void DrawText(
			Graphics graphics,
			Brush warningBrush,
			SelectionRange calendarRange,
			DateTime visDate,
			int dayBoxWidth,
			int dayBoxHeight,
			int firstWeekPosition,
			int lastWeekPosition,
			int leftMargin
		) {
			TimeSpan span = visDate.Subtract(calendarRange.Start);
			int row = span.Days / 7;
			int col = span.Days % 7;

			// 微妙にずれるので修正 9pt と 12pt対応
			int tune = leftMargin + (leftMargin / 10) + 1;

			Rectangle fillRect = new Rectangle(
				(col + (ShowWeekNumbers ? 1 : 0)) * dayBoxWidth + tune,
				firstWeekPosition + row * dayBoxHeight + 1,
				dayBoxWidth - 2,
				dayBoxHeight - 2
			);
			graphics.FillRectangle(warningBrush,fillRect);

			// Check if the date is in the bolded dates array 
			bool makeDateBolded = false;
			foreach(DateTime boldDate in BoldedDates) {
				if(boldDate == visDate)
					makeDateBolded = true;
			}

			using(Font textFont = new Font(Font,(makeDateBolded ? FontStyle.Bold : FontStyle.Regular))) {
				TextRenderer.DrawText(
					graphics,
					visDate.Day.ToString(),
					textFont,
					fillRect,
					_warningDateForeColor,
					TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter
				);
			}
		}
	}
}

上記クラスをフォームに貼り付けたら
WarningDates.Add で日付を入れておけば、色が塗られる

DateTime startDate = new DateTime(DateTime.Today.Year,DateTime.Today.Month,1);
DateTime finishDate = startDate.AddMonths(3);

DateTime currentDate = startDate;
for(;;) {
	if(finishDate <= currentDate)
		break;

	if(currentDate.DayOfWeek == DayOfWeek.Sunday || currentDate.DayOfWeek == DayOfWeek.Saturday)
		this.monthCalendarEx1.WarningDates.Add(currentDate);
	currentDate = currentDate.AddDays(1);
}