用于日期选择的 WinForms 用户控件
一个WinForms用户控件,可为已分配的日期提供彩色编码反馈。
引言
我多年来一直设计和开发共享软件,并且很荣幸成为共享软件专业协会的终身会员。过去六年,我一直在使用Microsoft .NET技术,因此,我有一天会开发一款新的.NET平台共享软件应用程序。我最近的网站上提供的软件产品就是如此。
我必须诚实地说。在我深入了解.NET之后,我期望我的工具箱中会有一个具有所有日期管理功能的.NET组件。我感到惊讶,因为似乎没有什么能提供我想要的功能。我想要的相当简单 - 一个.NET工具箱组件,它不仅可以让用户从方便的菜单/对话框安排中选择日期,还可以为已安排约会的日期以不同的颜色显示。我简直不敢相信,但没有这样的组件可用 - 除非我花几百美元购买商业产品。
所以,我想我应该开发自己的组件来按照我想要的方式管理日期。没什么花哨的 - 通常是以一个月的天数矩阵,用户可以选择他们想要的日期,但同时矩阵也会指示哪些日期已经有分配。
TestCal是一个用C# 2.0编写的.NET用户控件。它使用DataGridView
组件进行展示。您可以将其直接放入.NET项目中使用,或者如果您愿意,可以进行一些非常简单的更新以提供其他功能。
背景
尽管Visual Studio提供了非常丰富的日期/时间选择器控件,但它们没有提供我真正需要的功能。我需要的功能是让用户能够看到哪些日期已经有承诺。信不信由你,微软提供的这些默认组件并没有提供该功能。
Using the Code
解压zip文件,确保保持.NET项目的文件夹结构。然后,从Visual Studio 2005(或2008)中加载TestCal.sln文件,然后继续操作。
提供的项目包含一个简单的窗体,在该窗体上,通过单击按钮实例化日历用户控件。实现过程并不难,任何看过“21天成为.NET开发者”前两页的人应该很快就能掌握。
TestCal使用的日期定义为简单的整数,格式为“YYYYMMDD”,例如“20090121”表示2009年1月21日。我决定避免使用实际的DateTime
对象,因为如果有什么东西可能非常令人困惑,那就是DateTime
对象。在DateTime
和整数之间进行转换很简单 - 代码中提供了执行此操作的函数。
没有必要列出项目中提供的每一段源代码;所有文件都在包含源代码的zip文件中。只需将该文件解压到硬盘上的一个文件夹,然后加载TestCal.sln解决方案文件,您应该会得到一个完全可运行的测试项目,您可以在其中随意调试。但是,为了完整起见,下面列出了使用DataGridView
组件的UsrCalendar
控件。
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
namespace TestCal
{
public partial class UsrCalendar : UserControl
{
#region Declarations
private const int DEF_YEARRANGE_LO = 50;
// Sets the lowest year relative to the current year.
private const int DEF_YEARRANGE_HI = 3;
// Sets the highest year relative to the current year.
private Color DEF_BACKCOLOR = Color.LightBlue;
// Defines the background colour for the control.
private Color DEF_WEEKEND = Color.LightGreen;
// Defines the weekend colour for the control.
private Color DEF_OTHERMONTH_BACK = Color.LightGray;
// Defines the background colour for dates not in current month.
private Color DEF_OTHERMONTH_FORE = Color.SteelBlue;
// Defines the foreground colour for dates not in current month.
private Color DEF_COLOR1 = Color.Cyan;
// Defines background colour for reserved dates #1.
private Color DEF_COLOR2 = Color.DarkGreen;
// Defines background colour for reserved dates #2.
private Color DEF_COLOR3 = Color.DarkKhaki;
// Defines background colour for reserved dates #3.
private int m_CurrentDate;
// The current date that the user is selecting.
private int m_SelectedDate;
// The date selected by the user.
private Boolean bNoRedraw = false;
// Set true if we don't want to update the control.
public event EventHandler DateSelected;
// Delegate for handling completion events.
ArrayList ReservedDates;
// List of reserved dates (these are coloured to
// indicate their status).
#endregion
#region Properties
/// <summary>
/// Sets and retrieves the date by/for the owner of the control.
/// </summary>
internal int SelectedDate
{
get
{
return m_SelectedDate;
}
set
{
m_SelectedDate = value;
m_CurrentDate = m_SelectedDate;
}
}
#endregion
#region Public methods
/// <summary>
/// Sets the position of the control on the parent form.
/// </summary>
/// <param name="pt"></param>
public void SetControlPosition(Point pt)
{
this.Left = pt.X;
this.Top = pt.Y;
}
/// <summary>
/// Reserve date. These dates will be shown with a different background
/// colour. Note that only 3 different colours are defined in the control
/// as supplied - this could very easily be expanded upon if required so that
/// the control could show a wide range of different status values for different
/// dates.
/// </summary>
/// <param name="intYyyyMmDd">Date to reserve</param>
/// <param name="intColor">Colour to reserve (1, 2 or 3)</param>
public void ReserveDate(int intYyyyMmDd, int intColor)
{
Boolean bExists = false;
// Walk thru the current reserved list to see whether we already have
// this date. If we do then we don't bother saving a new copy.
for (int RowNo = 0; RowNo < ReservedDates.Count; RowNo++)
{
ClsDataItem oItm = (ClsDataItem)ReservedDates[RowNo];
if (oItm.ID.ToString() == intYyyyMmDd.ToString())
{
// We've already got the requested date, so we don't save it
// again.
bExists = true;
break;
}
}
if (!bExists)
{
// Requested date does not already exist in our list, so add
// this to our arraylist.
ClsDataItem oItm = new ClsDataItem(intYyyyMmDd.ToString(),
intColor.ToString());
ReservedDates.Add(oItm);
}
}
#endregion
#region Private methods
/// <summary>
/// Select the requested value for the combobox.
/// </summary>
/// <param name="oCmb">The combobox to operate on</param>
/// <param name="strVal">The value to select</param>
private void ComboSetValue(ComboBox oCmb, string strVal)
{
// De-select the current selection.
oCmb.SelectedIndex = -1;
// Walk thru the combobox items to identify the one
// we require.
for (int RowNo = 0; RowNo < oCmb.Items.Count; RowNo++)
{
ClsDataItem oItm = (ClsDataItem)oCmb.Items[RowNo];
if (oItm.ID == strVal)
{
// This is the item we want.
oCmb.SelectedIndex = RowNo;
break;
}
}
// If we haven't found the item we want and there are items
// available, select the first one.
if ((oCmb.SelectedIndex < 0) && (oCmb.Items.Count > 0))
oCmb.SelectedIndex = 0;
}
/// <summary>
/// Get the value of the currently selected item in the combobox.
/// Defaults to zero if no item is currently selected.
/// </summary>
/// <param name="oCmb">The combobox to operate on</param>
/// <returns>Return value (zero if none currently selected)</returns>
private int ComboGetValue(ComboBox oCmb)
{
int intRetVal = 0;
if (oCmb.SelectedIndex >= 0)
{
// We do have a selected item in the combobox.
ClsDataItem oItm = (ClsDataItem)oCmb.Items[oCmb.SelectedIndex];
intRetVal = Str2Int(oItm.ID);
}
return intRetVal;
}
/// <summary>
/// Get the year from the YyyyMmDd parameter.
/// </summary>
/// <param name="YyyyMmDd">Date value to extract year from</param>
/// <returns>Year number</returns>
private int YyyyMmDdGetYear(int YyyyMmDd)
{
int intRetVal = 0;
intRetVal = YyyyMmDd / 10000;
return intRetVal;
}
/// <summary>
/// Get the month from the YyyyMmDd parameter.
/// </summary>
/// <param name="YyyyMmDd">Date value to extract month from</param>
/// <returns>Month number (zero default)</returns>
private int YyyyMmDdGetMonth(int YyyyMmDd)
{
int intRetVal = 0;
intRetVal = YyyyMmDd / 100 - (100 * YyyyMmDdGetYear(YyyyMmDd));
return intRetVal;
}
/// <summary>
/// Get the day from the YyyyMmDd parameter.
/// </summary>
/// <param name="YyyyMmDd">Date value to extract day from</param>
/// <returns>Day number (zero default)</returns>
private int YyyyMmDdGetDay(int YyyyMmDd)
{
int intRetVal = 0;
intRetVal = YyyyMmDd - (10000 * YyyyMmDdGetYear(YyyyMmDd)) - (100 *
YyyyMmDdGetMonth(YyyyMmDd));
return intRetVal;
}
/// <summary>
/// Convert string to an integer. If the string doesn't contain a valid
/// definition for an integer zero is returned instead.
/// </summary>
/// <param name="strVal"></param>
/// <returns></returns>
private int Str2Int(string strVal)
{
int intRetVal = 0;
if (!int.TryParse(strVal, out intRetVal))
intRetVal = 0;
return intRetVal;
}
/// <summary>
/// Show calendar contents.
/// </summary>
private void ShowMonth()
{
int ColNo = 0;
DateTime DateVal;
int YearNo = YyyyMmDdGetYear(m_CurrentDate);
int MonthNo = YyyyMmDdGetMonth(m_CurrentDate);
int DoW;
int RowNo;
// Clear current rows from grid.
gridCalendar.Rows.Clear();
DateVal = new DateTime(YearNo, MonthNo, 1);
gridCalendar.Rows.Add();
// We start painting the day numbers according to the day of the week
// on which the 1st of the month falls.
DoW = (int)DateVal.DayOfWeek;
ColNo = DoW - 1;
if (ColNo < 0)
ColNo = 6;
// Walk thru the days of the month, defining the day numbers on the grid.
for (int DayNo = 1; DayNo <= DateTime.DaysInMonth(YearNo, MonthNo); DayNo++)
{
RowNo = gridCalendar.Rows.Count - 1;
gridCalendar.Rows[RowNo].Cells[ColNo].Value = DateVal.Day.ToString();
gridCalendar.Rows[RowNo].Cells[ColNo].Tag = DateVal.ToString("yyyyMMdd");
ColNo++;
DateVal = DateVal.AddDays(1);
if ((ColNo > 6) && (DayNo < DateTime.DaysInMonth(YearNo, MonthNo)))
{
// We finished the columns in the current row. Add another row
// to the grid.
ColNo = 0;
gridCalendar.Rows.Add();
}
}
// If the 1st of the month isn't a Monday show dates for previous month.
ColNo = 0;
while (gridCalendar.Rows[0].Cells[ColNo].Value == null)
ColNo++;
DateVal = new DateTime(YyyyMmDdGetYear(m_CurrentDate),
YyyyMmDdGetMonth(m_CurrentDate), 1);
while (ColNo > 0)
{
DateVal = DateVal.AddDays(-1);
ColNo--;
gridCalendar.Rows[0].Cells[ColNo].Value = DateVal.Day.ToString();
gridCalendar.Rows[0].Cells[ColNo].Style.BackColor = DEF_OTHERMONTH_BACK;
gridCalendar.Rows[0].Cells[ColNo].Style.ForeColor = DEF_OTHERMONTH_FORE;
gridCalendar.Rows[0].Cells[ColNo].Tag = DateVal.ToString("yyyyMMdd");
}
// If the last day of the month doesn't fall on a Sunday show dates
// for next month.
RowNo = gridCalendar.Rows.Count - 1;
ColNo = 0;
while (gridCalendar.Rows[RowNo].Cells[ColNo].Value != null)
{
DateVal = new DateTime(YyyyMmDdGetYear(m_CurrentDate),
YyyyMmDdGetMonth(m_CurrentDate),
Str2Int(gridCalendar.Rows[RowNo].Cells[ColNo].Value.ToString()));
ColNo++;
if (ColNo > 6)
break;
}
while (ColNo < 7)
{
DateVal = DateVal.AddDays(1);
gridCalendar.Rows[RowNo].Cells[ColNo].Value = DateVal.Day.ToString();
gridCalendar.Rows[RowNo].Cells[ColNo].Style.BackColor = DEF_OTHERMONTH_BACK;
gridCalendar.Rows[RowNo].Cells[ColNo].Style.ForeColor = DEF_OTHERMONTH_FORE;
gridCalendar.Rows[RowNo].Cells[ColNo].Tag = DateVal.ToString("yyyyMMdd");
ColNo++;
}
// Set background colour for weekends.
for (RowNo = 0; RowNo < gridCalendar.Rows.Count; RowNo++)
{
gridCalendar.Rows[RowNo].Cells[5].Style.BackColor = DEF_WEEKEND;
gridCalendar.Rows[RowNo].Cells[6].Style.BackColor = DEF_WEEKEND;
}
// Show reserved dates by defining background colour for cells.
for (RowNo = 0; RowNo < gridCalendar.Rows.Count; RowNo++)
{
for (ColNo = 0; ColNo < 7; ColNo++)
{
for (int ItmNo = 0; ItmNo < ReservedDates.Count; ItmNo++)
{
ClsDataItem oItm = (ClsDataItem)ReservedDates[ItmNo];
if (oItm.ID == gridCalendar.Rows[RowNo].Cells[ColNo].Tag.ToString())
{
switch (oItm.Text)
{
case "1":
gridCalendar.Rows[RowNo].Cells[ColNo].Style.BackColor =
DEF_COLOR1;
break;
case "2":
gridCalendar.Rows[RowNo].Cells[ColNo].Style.BackColor =
DEF_COLOR2;
break;
case "3":
gridCalendar.Rows[RowNo].Cells[ColNo].Style.BackColor =
DEF_COLOR3;
break;
}
}
}
}
}
// Select calendar cell if the current date is current displayed in the grid.
if (gridCalendar.SelectedCells.Count > 0)
gridCalendar.SelectedCells[0].Selected = false;
for (RowNo = 0; RowNo < gridCalendar.Rows.Count; RowNo++)
{
for (ColNo = 0; ColNo < 7; ColNo++)
{
if (gridCalendar.Rows[RowNo].Cells[ColNo].Tag != null)
{
if (gridCalendar.Rows[RowNo].Cells[ColNo].Tag.ToString() ==
m_SelectedDate.ToString())
{
gridCalendar.Rows[RowNo].Cells[ColNo].Selected = true;
break;
}
}
}
}
// Set up physical dimensions according to the data shown in the grid.
gridCalendar.Left = 2;
cmbMonth.Left = 2;
gridCalendar.Width = 14 + gridCalendar.Columns.GetColumnsWidth(
DataGridViewElementStates.Visible);
gridCalendar.Height = 25 + gridCalendar.Rows.GetRowsHeight(
DataGridViewElementStates.Visible);
this.Height = cmbMonth.Height + gridCalendar.Height;
cmbYear.Left = gridCalendar.Right - cmbYear.Width - 13;
// Set background colour for control.
this.BackColor = DEF_BACKCOLOR;
gridCalendar.BackgroundColor = DEF_BACKCOLOR;
}
#endregion
#region Form events
/// <summary>
/// Form instantiator.
/// </summary>
public UsrCalendar()
{
InitializeComponent();
ReservedDates = new ArrayList();
// Set default selected date to today's date. The user can change this
// by setting
// the current date property.
m_SelectedDate = Str2Int(DateTime.Now.ToString("yyyyMMdd"));
}
/// <summary>
/// Form load event. Prepare control.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void UsrCalendar_Load(object sender, EventArgs e)
{
// To begin with the currently displayed month is the same as the
// date handed down by the form on which the control resides.
m_CurrentDate = m_SelectedDate;
// Set up month combobox.
cmbMonth.Items.Clear();
for (int MonthNo = 1; MonthNo < 13; MonthNo++)
{
DateTime oDate = new DateTime(2009, MonthNo, 1);
cmbMonth.Items.Add(new ClsDataItem(MonthNo.ToString().PadLeft(2, '0'),
oDate.ToString("MMMM")));
}
// Set up year combobox.
cmbYear.Items.Clear();
for (int YearNo = DateTime.Now.Year - DEF_YEARRANGE_LO; YearNo <=
DateTime.Now.Year + DEF_YEARRANGE_HI; YearNo++)
cmbYear.Items.Add(new ClsDataItem(YearNo.ToString(), YearNo.ToString()));
// Define columns for grid.
gridCalendar.Left = this.Left;
gridCalendar.Columns.Add("Mon", "Mo");
gridCalendar.Columns.Add("Tue", "Tu");
gridCalendar.Columns.Add("Wed", "We");
gridCalendar.Columns.Add("Thu", "Th");
gridCalendar.Columns.Add("Fri", "Fr");
gridCalendar.Columns.Add("Sat", "Sa");
gridCalendar.Columns.Add("Sun", "Su");
for (int ColNo = 0; ColNo < 7; ColNo++)
{
gridCalendar.Columns[ColNo].SortMode = DataGridViewColumnSortMode.NotSortable;
gridCalendar.Columns[ColNo].Width = gridCalendar.Width / 7;
gridCalendar.Columns[ColNo].DefaultCellStyle.Alignment =
DataGridViewContentAlignment.MiddleCenter;
}
gridCalendar.ColumnHeadersDefaultCellStyle.Alignment =
DataGridViewContentAlignment.MiddleCenter;
// Set initial values for comboboxes. This action will automatically draw the
// grid.
ComboSetValue(cmbMonth, YyyyMmDdGetMonth(m_CurrentDate).ToString().PadLeft(2, '0'));
ComboSetValue(cmbYear, YyyyMmDdGetYear(m_CurrentDate).ToString());
}
#endregion
#region Button events
/// <summary>
/// Go to previous year.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnYearPrev_Click(object sender, EventArgs e)
{
if (cmbYear.SelectedIndex > 0)
{
cmbYear.SelectedIndex--;
}
}
/// <summary>
/// Go to previous month.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnMonthPrev_Click(object sender, EventArgs e)
{
if (cmbMonth.SelectedIndex > 0)
{
cmbMonth.SelectedIndex--;
}
else
{
cmbMonth.SelectedIndex = cmbMonth.Items.Count - 1;
btnYearPrev_Click(sender, e);
}
}
/// <summary>
/// Go to today.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnToday_Click(object sender, EventArgs e)
{
m_CurrentDate = Str2Int(DateTime.Now.ToString("yyyyMMdd"));
bNoRedraw = true;
ComboSetValue(cmbMonth, DateTime.Now.Month.ToString().PadLeft(2, '0'));
ComboSetValue(cmbYear, DateTime.Now.Year.ToString());
bNoRedraw = false;
ShowMonth();
}
/// <summary>
/// Go to next month.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnMonthNext_Click(object sender, EventArgs e)
{
if (cmbMonth.SelectedIndex < (cmbMonth.Items.Count - 1))
{
cmbMonth.SelectedIndex++;
}
else
{
cmbMonth.SelectedIndex = 0;
btnYearNext_Click(sender, e);
}
}
/// <summary>
/// Go to next year.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnYearNext_Click(object sender, EventArgs e)
{
if (cmbYear.SelectedIndex < (cmbYear.Items.Count - 1))
{
cmbYear.SelectedIndex++;
}
}
/// <summary>
/// Cancel date selection. We signify this action by setting the
/// selected date to zero.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnCancel_Click(object sender, EventArgs e)
{
// Set selected date to zero to signify cancellation.
m_SelectedDate = 0;
if (DateSelected != null)
DateSelected(this, e);
}
/// <summary>
/// Select the date and return control to the parent form.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void btnSelect_Click(object sender, EventArgs e)
{
if (DateSelected != null)
DateSelected(this, e);
}
#endregion
#region Combobox events
/// <summary>
/// Changed month.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void cmbMonth_SelectedIndexChanged(object sender, EventArgs e)
{
if (!bNoRedraw)
{
m_CurrentDate = Str2Int(string.Format("{0}{1}01",
YyyyMmDdGetYear(m_CurrentDate),
ComboGetValue(cmbMonth).ToString().PadLeft(2, '0')));
ShowMonth();
}
}
/// <summary>
/// Changed year.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void cmbYear_SelectedIndexChanged(object sender, EventArgs e)
{
if (!bNoRedraw)
{
m_CurrentDate = Str2Int(string.Format("{0}{1}01",
ComboGetValue(cmbYear).ToString(),
YyyyMmDdGetMonth(m_CurrentDate).ToString().PadLeft(2, '0')));
ShowMonth();
}
}
#endregion
#region Grid events
/// <summary>
/// User has clicked on a date.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void gridCalendar_CellClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
{
DateTime oDate = new DateTime(ComboGetValue(cmbYear), ComboGetValue(cmbMonth),
Str2Int(
gridCalendar.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString()));
m_SelectedDate = Str2Int(oDate.ToString("yyyyMMdd"));
}
}
/// <summary>
/// User has double-clicked on a date.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void gridCalendar_CellDoubleClick(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex >= 0)
{
DateTime oDate = new DateTime(ComboGetValue(cmbYear), ComboGetValue(cmbMonth),
Str2Int(
gridCalendar.Rows[e.RowIndex].Cells[e.ColumnIndex].Value.ToString()));
m_SelectedDate = Str2Int(oDate.ToString("yyyyMMdd"));
}
btnSelect_Click(sender, e);
}
#endregion
}
}
上面所示的UsrCalendar
类是该项目的核心。它包含了创建和维护弹出日历所需的所有功能,用户通过该日历与之交互以选择日期。
请注意,用户控件使用委托事件来告知其宿主窗体用户已从弹出日历中完成了日期选择。您不需要花很多时间去理解委托的工作原理;即使是像我这样的经验丰富的开发人员,涉及的逻辑也可能有点吓人 - 只需遵循提供的源代码中的注释,一切都会迎刃而解。
这是一个可以在窗体上弹出的用户控件。它并没有做什么特别花哨的事情,这不是一个可以让你赚大钱的组件(如果您做到了,请记住是谁帮助实现的!);它只是利用了Visual Studio 2005自带的DataGridView
组件。它有很大的潜力可以添加新功能,例如,通过属性来暴露用于绘制控件的颜色 - 就像现在这样,这些颜色是硬编码在UsrCalender
类中的,但可以很容易地实现为属性,使功能更加有用。
我将此提交给CodeProject网站的主要目标是遵循KISS原则,大家都知道它的意思是“保持简单,愚蠢!”。这是一个基础项目,几乎肯定需要用户进行一些细致的调整。然而,它也是一个完全可工作的项目,用户应该可以无需额外努力就能运行。我希望它能成为那些因为在Visual Studio工具箱中找不到一个至少能给用户最简单的反馈,关于哪些日期已经被分配了的基本组件而感到沮丧的人的起点。
正如开发者社区所应有的,只剩下一句话了 - **享受**!
关注点
实现此项目所需的时间不到一天。其中相当一部分时间用于定义单个月份日历天矩阵的显示逻辑 - 其他所有内容都相当简单。