【引言】
Windows Forms 2.0为DataGridView提供了多种单元格和表格列类型。例如,文本框单元格和表格列(DataGridViewTextBoxCell/ DataGridViewTextBoxColumn)和一对基于复选框的组合( DataGridViewCheckBoxCell/ DataGridViewCheckBoxColumn)。虽然单元格和表格列类型较丰富,一些开发者还是要创建自己的单元格和列类型以便扩展表格的功能。庆幸的是,DataGridView控件架构是可扩展的,可以满足表格中定制单元格和表格列的需求。本文件描述如何创建和使用单元格和表格列,使得用户在表格中轻松输入数值型数据。
【单元格的特征】
定制单元格DataGridViewNumericUpDownCell重用了Windows Forms控件NumericUpDown的用户界面(见图1)。
图1.Windows Forms的NumericUpDown控件
好处
标准的DataGridViewTextBoxCell单元格类型允许用户在表格中查看和输入数值型数据。然而,定制的DataGridViewNumericUpDownCell单元格控件提供了一些额外的优点:
- 自动限制输入为数值型数据;
- 自动限制输入在一定范围之内;
- 输入精密(即逗号后希望的数字位数)可以预先确定。
从文本单元格类型派生
由于DataGridViewNumericUpDownCell单元格类型在功能和外观上与标准DataGridViewTextBoxCell单元格类型相似,那么从该类型派生是合理的。这样,定制单元格可以利用其基类的许多特点,也将大大减少创建单元格的工作量。当然,直接从Windows Forms 2.0的单元格基类DataGridViewCell派生也是可能的。
编辑行为
从编辑行为看,DataGridView控件使用的单元格可分为三类:
- 无编辑行为的单元格。这些是只读单元格,不接受用户输入。例如,标准的DataGridViewButtonCell;
- 简单编辑行为的单元格。这些是接受用户限制输入的单元格,如标准的DataGridViewCheckBoxCell。对于这类单元格,改变其值的唯一方式就是点击复选框或敲击空格键;
- 复杂编辑行为的单元格。这些单元格提供了丰富的值修改用户交互,需要一个可视的Windows Forms控件,使得用户可以进行复杂输入行为。例如,DataGridViewTextBoxCell单元格和DataGridViewComboBoxCell单元格。
由于DataGridViewNumericUpDownCell单元格提供了丰富的用户交互,它属于上述的第三类。然而,当用户想要改变单元格值的时候需要一个承载控件——称为编辑控件(Editing control),它派生于NumericUpDown控件。此外,所有的编辑控件必须实现IDataGridViewEditingControl接口,它规范了表格/单元格与编辑控件的交互。
【单元格、表格列与编辑控件类】
定制单元格类型至少需要开发一个该类型的类。如果它拥有丰富的编辑行为(上述第3种情况),且不能用任何标准编辑控件——DataGridViewTextBoxEditingControl和DataGridViewComboBoxEditingControl等,那么还需要为编辑控件创建第二个类。最后,创建一个定制的表格列类是可以选的,因为任何单元格类型可用于任何表格列类型或基类DataGridViewColumn——表格列可以是不同类型的。例如,DataGridViewNumericUpDownCell单元格可以用于DataGridViewLinkColumn或DataGridViewColumn表格列。然而,在许多情况下,创建一个特殊的表格列类型将使定制单元格类型更容易使用。通常情况下,定制表格列复制了定制单元格的一些指定属性。
针对DataGridViewNumericUpDownCell类型,共创建了三个类并保存在三个文件中:DataGridViewNumericUpDownCell在DataGridViewNumericUpDownCell.cs中、DataGridViewNumericUpDownEditingControl在DataGridViewNumericUpDownEditingControl.cs中以及DataGridViewNumericUpDownColumn在DataGridViewNumericUpDownColumn.cs中。这些类都在DataGridViewNumericUpDownElements名称空间中。
【单元格实现详解】
首先关注如何创建定制的DataGridViewNumericUpDownCell单元格本身。
类定义与构造函数
如前所述,DataGridViewNumericUpDownCell类派生自Windows Forms 2.0的DataGridViewTextBoxCell类。
namespace DataGridViewNumericUpDownElements
{
public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
{
public DataGridViewNumericUpDownCell()
{
...
}
}
}
定义单元格属性
DataGridViewNumericUpDownCell复制了NumericUpDown控件公开的一些关键属性:DecimalPlaces、Increment、Maximum、Minimum与ThousandSeparator。
提示:通过公开NumericUpDown控件的Hexadecimal属性,DataGridViewNumericUpDownCell将更加多样化。
通常的定制单元格方法是:基于一个特定Windows Forms控件并复制和公开它的一些属性。例如,DataGridViewComboBoxCell公开了MaxDropDownItems属性——类似于ComboBox控件。
作为一个单元格属性实现的一个例子,下面给出了DecimalPlaces属性的实现代码:
public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
{
private int decimalPlaces; // 缓存DecimalPlaces属性的值
public DataGridViewNumericUpDownCell()
{
...
this.decimalPlaces = DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces;
}
[DefaultValue(DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces)
DefaultValue]
public int DecimalPlaces
{
get
{
return this.decimalPlaces;
}
set
{
if (value < 0 || value > 99)
{
throw new ArgumentOutOfRangeException("The DecimalPlaces property
cannot be smaller than 0 or larger than 99.");
}
if (this.decimalPlaces != value)
{
SetDecimalPlaces(this.RowIndex, value);
OnCommonChange(); // 确保需要时单元格或表格列重绘制及自动调整大小
}
}
}
internal void SetDecimalPlaces(int rowIndex, int value)
{
this.decimalPlaces = value;
if (OwnsEditingNumericUpDown(rowIndex))
{
this.EditingNumericUpDown.DecimalPlaces = value;
}
}
}
注意:在DataGridViewNumericUpDownColumn类中访问SetDecimalPlaces函数(而不是设置属性值——译者注)。参考OnCommonChange(), OwnsEditingNumericUpDown(int rowIndex)和EditingNumericUpDown属性的完整代码。
重写的关键属性
创建DataGridView控件的定制单元格类型时,通常需要重写DataGridViewCell的如下基类属性:
EditType属性
EditType属性指向关联单元格的编辑控件。DataGridViewCell的默认实现是返回System.Windows.Forms.DataGridViewTextBoxEditingControl类型。没有编辑行为的单元格或只有简单编辑行为的单元格(即不使用编辑控件)类型,必须重写该属性并返回null。有复杂编辑行为的单元格类型必须重写这个属性并返回它们的编辑控件的类型。
DataGridViewNumericUpDownCell类的实现如下:
public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
{
// 单元格编辑控件的类型
private static Type defaultEditType =
typeof(DataGridViewNumericUpDownEditingControl);
public override Type EditType
{
get
{
return defaultEditType; // 类型是DataGridViewNumericUpDownEditingControl
}
}
}
FormattedValueType属性
FormattedValueType属性表示屏幕上显示的数据类型,即单元格FormattedValue属性的类型。例如,对于DataGridViewTextBoxCell类而言它是System.String ,至于DataGridViewImageCell类则是System.Drawing.Image或System.Drawing.Icon类型。DataGridViewNumericUpDownCell显示的是文本,因此它的FormattedValueType是System.String,与其基类DataGridViewTextBoxCell的相同。于是,该属性不需要重写。
ValueType属性
ValueType属性表示内部数据的类型,即单元格Value属性的类型。DataGridViewNumericUpDownCell类存储System.Decimal类型的值。于是,ValueType属性的实现如下:
public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
{
// 单元格Value的类型
private static Type defaultValueType = typeof(System.Decimal);
public override Type ValueType
{
get
{
Type valueType = base.ValueType;
if (valueType != null)
{
return valueType;
}
return defaultValueType;
}
}
}
重写的关键方法
当开发一个自定义的单元格类型时,检查如下一些虚方法是否需要重写也是十分关键的。
Clone()方法
基类DataGridViewCell实现了ICloneable接口,每个定制的单元格类型通常也需要重写Clone()方法用以复制自定义的属性。单元格是可克隆的,缘由某一特定单元格实例可用于表格中的多行——单元格属于一共享行的情形。当行取消共享时,它的单元格需要被克隆。下面是DataGridViewNumericUpDownCell的实现代码:
public override object Clone()
{
DataGridViewNumericUpDownCell dataGridViewCell = base.Clone() as
DataGridViewNumericUpDownCell;
if (dataGridViewCell != null)
{
dataGridViewCell.DecimalPlaces = this.DecimalPlaces;
dataGridViewCell.Increment = this.Increment;
dataGridViewCell.Maximum = this.Maximum;
dataGridViewCell.Minimum = this.Minimum;
dataGridViewCell.ThousandsSeparator = this.ThousandsSeparator;
}
return dataGridViewCell;
}
KeyEntersEditMode(KeyEventArgs)方法
具有复杂编辑行为的单元格类型(即使用编辑控件的单元格)需要重写该方法来确定哪些击键激发编辑控件的显示(当表格的EditMode属性是EditOnKeystrokeOrF2时)。对于DataGridViewNumericUpDownCell类型,数字和负号(应该包括小数点——译者注)将激活编辑控件。该方法的实现代码如下:
public override bool KeyEntersEditMode(KeyEventArgs e)
{
NumberFormatInfo numberFormatInfo = System.Globalization.CultureInfo
.CurrentCulture.NumberFormat;
Keys negativeSignKey = Keys.None;
string negativeSignStr = numberFormatInfo.NegativeSign;
if (!string.IsNullOrEmpty(negativeSignStr) && negativeSignStr.Length == 1)
{
negativeSignKey = (Keys)(VkKeyScan(negativeSignStr[0]));
}
if ((char.IsDigit((char)e.KeyCode) ||
(e.KeyCode >= Keys.NumPad0 && e.KeyCode <= Keys.NumPad9) ||
negativeSignKey == e.KeyCode ||
Keys.Subtract == e.KeyCode) &&
!e.Shift && !e.Alt && !e.Control)
{
return true;
}
return false;
}
InitializeEditingControl(int, obj, DataGridViewCellStyle)方法
当编辑控件将要显示时,表格控件调用该方法。当然,仅仅是复杂编辑行为的单元格有这种情况。这使得单元格有机会来初始化它编辑控件——基于单元格自己的属性与提供的格式化值。DataGridViewNumericUpDownCell的实现代码如下:
public override void InitializeEditingControl(int rowIndex,
object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle)
{
base.InitializeEditingControl(rowIndex, initialFormattedValue,
dataGridViewCellStyle);
NumericUpDown numericUpDown = this.DataGridView.EditingControl as NumericUpDown;
if (numericUpDown != null)
{
numericUpDown.BorderStyle = BorderStyle.None;
numericUpDown.DecimalPlaces = this.DecimalPlaces;
numericUpDown.Increment = this.Increment;
numericUpDown.Maximum = this.Maximum;
numericUpDown.Minimum = this.Minimum;
numericUpDown.ThousandsSeparator = this.ThousandsSeparator;
string initialFormattedValueStr = initialFormattedValue as string;
if (initialFormattedValueStr == null)
{
numericUpDown.Text = string.Empty;
}
else
{
numericUpDown.Text = initialFormattedValueStr;
}
}
}
PositionEditingControl(bool, bool, Rectangle, Rectangle, DataGridViewCellStyle, bool, bool, bool, bool)方法
单元格控制编辑控件的位置和大小——通常根据自己的大小和样式(特别是对齐方式)。当编辑控件需要重定位或改变大小时,表格控件将调用该方法。
public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell
{
private Rectangle GetAdjustedEditingControlBounds(Rectangle
editingControlBounds,DataGridViewCellStyle cellStyle)
{
// 在编辑控件的左边和右边加1填充像素
editingControlBounds.X += 1;
editingControlBounds.Width = Math.Max(0, editingControlBounds.Width - 2);
// 调整编辑控件的垂直位置
int preferredHeight = cellStyle.Font.Height + 3;
if (preferredHeight < editingControlBounds.Height)
{
switch (cellStyle.Alignment)
{
case DataGridViewContentAlignment.MiddleLeft:
case DataGridViewContentAlignment.MiddleCenter:
case DataGridViewContentAlignment.MiddleRight:
editingControlBounds.Y += (editingControlBounds.Height
- preferredHeight) / 2;
break;
case DataGridViewContentAlignment.BottomLeft:
case DataGridViewContentAlignment.BottomCenter:
case DataGridViewContentAlignment.BottomRight:
editingControlBounds.Y +=
editingControlBounds.Height - preferredHeight;
break;
}
}
return editingControlBounds;
}
public override void PositionEditingControl(bool setLocation,
bool setSize,
Rectangle cellBounds,
Rectangle cellClip,
DataGridViewCellStyle cellStyle,
bool singleVerticalBorderAdded,
bool singleHorizontalBorderAdded,
bool isFirstDisplayedColumn,
bool isFirstDisplayedRow)
{
Rectangle editingControlBounds = PositionEditingPanel(cellBounds,
cellClip,
cellStyle,
singleVerticalBorderAdded,
singleHorizontalBorderAdded,
isFirstDisplayedColumn,
isFirstDisplayedRow);
editingControlBounds = GetAdjustedEditingControlBounds(
editingControlBounds, cellStyle);
this.DataGridView.EditingControl.Location = new Point(
editingControlBounds.X, editingControlBounds.Y);
this.DataGridView.EditingControl.Size = new Size(
editingControlBounds.Width, editingControlBounds.Height);
}
}
DetachEditingControl()方法
编辑任务结束后就不需要编辑控件了,此时表格控件调用该方法。定制单元格可能需要重写该方法以便做一些清理工作。下面是DataGridViewNumericUpDownCell的实现代码:
[EditorBrowsable(EditorBrowsableState.Advanced)]
public override void DetachEditingControl()
{
DataGridView dataGridView = this.DataGridView;
if (dataGridView == null || dataGridView.EditingControl == null)
{
throw new InvalidOperationException("Cell is detached or
its grid has no editing control.");
}
NumericUpDown numericUpDown = dataGridView.EditingControl as NumericUpDown;
if (numericUpDown != null)
{
// 编辑控件被回收。事实上,当一个DataGridViewNumericUpDownCell在
// 另一个DataGridViewNumericUpDownCell之后得到编辑行为时,
// 因为性能原因,该编辑控件将再次使用(避免不必要的控件析构和创建)。
// 下面,NumericUpDown控件中文本框的undo缓冲区被清除,避免编辑任务之间的干扰。
TextBox textBox = numericUpDown.Controls[1] as TextBox;
if (textBox != null)
{
textBox.ClearUndo();
}
}
base.DetachEditingControl();
}
The GetFormattedValue(object, int, ref DataGridViewCellStyle, TypeConverter, TypeConverter, DataGridViewDataErrorContexts)方法
当表格控件需要提供单元格值的格式化显示时,将调用该方法。对于DataGridViewNumericUpDownCell,Value是System.Decimal类型、FormattedValue是System.String类型。因此,该方法需要转换decimal为字符串。基类执行这个转换,但DataGridViewNumericUpDownCell仍然需要重写其默认行为,确保返回的字符串完全符合NumericUpDown控件需要的显示形式。
protected override object GetFormattedValue(object value,
int rowIndex,
ref DataGridViewCellStyle cellStyle,
TypeConverter valueTypeConverter,
TypeConverter formattedValueTypeConverter,
DataGridViewDataErrorContexts context)
{
// 默认情况下,基类转换1234.5为字符串"1234.5"
object formattedValue = base.GetFormattedValue(value, rowIndex, ref cellStyle,
valueTypeConverter, formattedValueTypeConverter, context);
string formattedNumber = formattedValue as string;
if (!string.IsNullOrEmpty(formattedNumber) && value != null)
{
Decimal unformattedDecimal = System.Convert.ToDecimal(value);
Decimal formattedDecimal = System.Convert.ToDecimal(formattedNumber);
if (unformattedDecimal == formattedDecimal)
{
// 基类实现的GetFormattedValue(触发CellFormattingevent)
// 通常转换1234.5为"1234.5"。
// 但是,根据ThousandsSeparator与DecimalPlaces的值,
// 它可能不是实际显示的字符串。
// 真正格式化的值可能是"1,234.500"
return formattedDecimal.ToString((this.ThousandsSeparator ? "N" : "F")
+ this.DecimalPlaces.ToString());
}
}
return formattedValue;
}The GetPreferredSize(Graphics, DataGridViewCellStyle, int, Size)方法
当表格自动调整大小时,一些单元格将要确定其最合适的高度、宽度或大小。定制单元格类型可以重写GetPreferredSize方法,根据其内容、风格、属性等计算最合适的尺寸。由于DataGridViewNumericUpDownCell单元格与其基类DataGridViewTextBoxCell略有不同,一旦获得显示,它将调用基类实现并传回一个改正值——考虑了上/下按钮。
protected override Size GetPreferredSize(Graphics graphics,
DataGridViewCellStyle cellStyle, int rowIndex, Size constraintSize)
{
if (this.DataGridView == null)
{
return new Size(-1, -1);
}
Size preferredSize = base.GetPreferredSize(
graphics, cellStyle, rowIndex, constraintSize);
if (constraintSize.Width == 0)
{
const int ButtonsWidth = 16; // 上/下按钮的宽度。
const int ButtonMargin = 8; // 文字和按钮之间的一些空白像素。
preferredSize.Width += ButtonsWidth + ButtonMargin;
}
return preferredSize;
}GetErrorIconBounds(Graphics, DataGridViewCellStyle, int)方法
定制单元格类型可以重写该方法来确定错误图标的位置。默认情况下,错误图标显示位置靠近单元格右边。由于DataGridViewNumericUpDownCell的上/下按钮也在其右边,于是需要重写该方法使得错误图标和按钮不重叠。
protected override Rectangle GetErrorIconBounds(Graphics graphics,
DataGridViewCellStyle cellStyle, int rowIndex)
{
const int ButtonsWidth = 16;
Rectangle errorIconBounds = base.GetErrorIconBounds(
graphics, cellStyle, rowIndex);
if (this.DataGridView.RightToLeft == RightToLeft.Yes)
{
errorIconBounds.X = errorIconBounds.Left + ButtonsWidth;
}
else
{
errorIconBounds.X = errorIconBounds.Left - ButtonsWidth;
}
return errorIconBounds;
}Paint(Graphics, Rectangle, Rectangle, int, DataGridViewElementStates, object, object, string, DataGridViewCellStyle, DataGridViewAdvancedBorderStyle, DataGridViewPaintParts)方法
对任何单元格类型而言,这个方法都是关键的——负责绘制单元格。DataGridViewNumericUpDownCell使用NumericUpDown的绘制实现,它设置控件上的各种属性,并调用NumericUpDown.DrawToBitmap(...)函数,然后调用Graphics.DrawImage(...)绘制一个单元格。对于直接模仿某一特定控件的单元格类型,这个方法是方便的。然而,对一些不支持WM_PRINT(Windows消息——译者注)的控件如RichTextBox或自定义控件,它不是很有效也不管用。一个更有效的替代办法是一点一点地绘制单元格,使它看起来像NumericUpDown控件。
protected override void Paint(Graphics graphics, Rectangle clipBounds,
Rectangle cellBounds, int rowIndex, DataGridViewElementStates cellState,
object value, object formattedValue, string errorText,
DataGridViewCellStyle cellStyle,
DataGridViewAdvancedBorderStyle advancedBorderStyle,
DataGridViewPaintParts paintParts)
{
if (this.DataGridView == null)
{
return;
}
// 首先绘制单元格的边界和背景。
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value,
formattedValue, errorText, cellStyle, advancedBorderStyle,
paintParts & ~(DataGridViewPaintParts.ErrorIcon
| DataGridViewPaintParts.ContentForeground));
Point ptCurrentCell = this.DataGridView.CurrentCellAddress;
bool cellCurrent = ptCurrentCell.X == this.ColumnIndex
&& ptCurrentCell.Y == rowIndex;
bool cellEdited = cellCurrent && this.DataGridView.EditingControl != null;
// 如果单元格是在编辑模式,不需要绘制
if (!cellEdited)
{
if (PartPainted(paintParts, DataGridViewPaintParts.ContentForeground))
{
// 绘制NumericUpDown控件
// 考虑边界
Rectangle borderWidths = BorderWidths(advancedBorderStyle);
Rectangle valBounds = cellBounds;
valBounds.Offset(borderWidths.X, borderWidths.Y);
valBounds.Width -= borderWidths.Right;
valBounds.Height -= borderWidths.Bottom;
// 还考虑填充
if (cellStyle.Padding != Padding.Empty)
{
if (this.DataGridView.RightToLeft == RightToLeft.Yes)
{
valBounds.Offset(cellStyle.Padding.Right,
cellStyle.Padding.Top);
}
else
{
valBounds.Offset(cellStyle.Padding.Left,
cellStyle.Padding.Top);
}
valBounds.Width -= cellStyle.Padding.Horizontal;
valBounds.Height -= cellStyle.Padding.Vertical;
}
// 确定NumericUpDown控件的位置
valBounds = GetAdjustedEditingControlBounds(valBounds, cellStyle);
bool cellSelected = (cellState & DataGridViewElementStates.Selected) != 0;
if (renderingBitmap.Width < valBounds.Width ||
renderingBitmap.Height < valBounds.Height)
{
// 静态图太小了,需要分配一个更大的。
renderingBitmap.Dispose();
renderingBitmap = new Bitmap(valBounds.Width, valBounds.Height);
}
// 确保NumericUpDown控件的父控件是一个可视控件
if (paintingNumericUpDown.Parent == null ||
!paintingNumericUpDown.Parent.Visible)
{
paintingNumericUpDown.Parent = this.DataGridView;
}
// 设置所有的相关属性
paintingNumericUpDown.TextAlign =
DataGridViewNumericUpDownCell.TranslateAlignment(cellStyle.Alignment);
paintingNumericUpDown.DecimalPlaces = this.DecimalPlaces;
paintingNumericUpDown.ThousandsSeparator = this.ThousandsSeparator;
paintingNumericUpDown.Font = cellStyle.Font;
paintingNumericUpDown.Width = valBounds.Width;
paintingNumericUpDown.Height = valBounds.Height;
paintingNumericUpDown.RightToLeft = this.DataGridView.RightToLeft;
paintingNumericUpDown.Location =
new Point(0, -paintingNumericUpDown.Height - 100);
paintingNumericUpDown.Text = formattedValue as string;
Color backColor;
if (PartPainted(paintParts,
DataGridViewPaintParts.SelectionBackground) && cellSelected)
{
backColor = cellStyle.SelectionBackColor;
}
else
{
backColor = cellStyle.BackColor;
}
if (PartPainted(paintParts, DataGridViewPaintParts.Background))
{
if (backColor.A < 255)
{
// NumericUpDown控件不支持透明的背景颜色
backColor = Color.FromArgb(255, backColor);
}
paintingNumericUpDown.BackColor = backColor;
}
// 最后绘制NumericUpDown控件
Rectangle srcRect =
new Rectangle(0, 0, valBounds.Width, valBounds.Height);
if (srcRect.Width > 0 && srcRect.Height > 0)
{
paintingNumericUpDown.DrawToBitmap(renderingBitmap, srcRect);
graphics.DrawImage(renderingBitmap,
new Rectangle(valBounds.Location, valBounds.Size),
srcRect, GraphicsUnit.Pixel);
}
}
if (PartPainted(paintParts, DataGridViewPaintParts.ErrorIcon))
{
// 在NumericUpDown控件上绘制潜在的错误图标
base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState,
value, formattedValue, errorText,cellStyle,
advancedBorderStyle, DataGridViewPaintParts.ErrorIcon);
}
}
}ToString()方法
该方法返回单元格的紧凑字符串表示。DataGridViewNumericUpDownCell按标准单元格的标准实现该方法。
public override string ToString()
{
return "DataGridViewNumericUpDownCell {
ColumnIndex=" + ColumnIndex.ToString(CultureInfo.CurrentCulture) + ",
RowIndex=" + RowIndex.ToString(CultureInfo.CurrentCulture) + " }";
}编辑控件实现详解
现在深入探讨定制单元格的编辑控件DataGridViewNumericUpDownEditingControl的构建。
类定义与构造函数
所有的编辑控件必须实现IDataGridViewEditingControl接口,它规范了表格或单元格与编辑控件之间的交互行为,同时它们必须派生自System.Windows.Forms.Control。
namespace DataGridViewNumericUpDownElements
{
class DataGridViewNumericUpDownEditingControl
: NumericUpDown, IDataGridViewEditingControl
{
public DataGridViewNumericUpDownEditingControl()
{
// 编辑控件不能是Tab循环的一部分
this.TabStop = false;
}
}
}实现IDataGridViewEditingControl接口
通过虚属性和虚方法,DataGridViewNumericUpDownEditingControl控件实现了这个接口,使得开发者可以派生并定制其行为。 属性和方法是:
// 捕获使用编辑控件的表格的属性
public virtual DataGridView EditingControlDataGridView
// 表示编辑控件的当前格式化值的属性
public virtual object EditingControlFormattedValue
// 表示编辑控件所在行的属性
public virtual int EditingControlRowIndex
// 指示编辑控件的值是否改变的属性
public virtual bool EditingControlValueChanged
// 确定编辑板(即编辑控件的父控件)使用哪个光标的属性。
public virtual Cursor EditingPanelCursor
// 指示当编辑控件的值改变时是否需要重定位的属性。
public virtual bool RepositionEditingControlOnValueChange
// 编辑控件显示前表格调用的方法,使其能够适应所提供的单元格风格。
public virtual void ApplyCellStyleToEditingControl
(DataGridViewCellStyle dataGridViewCellStyle)
// 当按键时表格调用的方法,确定编辑控件是否对该键感兴趣。
public virtual bool EditingControlWantsInputKey
(Keys keyData, bool dataGridViewWantsInputKey)
// 返回编辑控件的当前值。
public virtual object GetEditingControlFormattedValue
(DataGridViewDataErrorContexts context)
// 表格调用的方法,让编辑控件有机会为编辑任务作好准备。
public virtual void PrepareEditingControlForEdit(bool selectAll)值变化时通知表格
IDataGridViewEditingControl接口最重要的实现是,编辑控件一般需要将内容改变的消息转发给表格——借由DataGridView.NotifyCurrentCellDirty (...)方法。通常,编辑控件必须重写受保护的虚拟方法以获得内容改变的通知,并能转发消息给表格。此时,DataGridViewNumericUpDownEditingControl重写两个方法:
protected override void OnKeyPress(KeyPressEventArgs e)
protected override void OnValueChanged(EventArgs e)【表格列实现详解】
如前所述,创建一个定制表格列是可选的,因为DataGridViewNumericUpDownCell可用于任何表格列类型,包括基类DataGridViewColumn:
DataGridViewColumn dataGridViewColumn = DataGridViewColumn
new DataGridViewColumn
(new DataGridViewNumericUpDownElements.DataGridViewNumericUpDownCell());
...
DataGridViewNumericUpDownCell dataGridViewNumericUpDownCell =
dataGridViewColumn.CellTemplate as DataGridViewNumericUpDownCell;
dataGridViewNumericUpDownCell.DecimalPlaces = 3;定制表格列通常公开与之关联的单元格类型的特殊属性。例如,DataGridViewNumericUpDownColumn公开了DecimalPlaces、Increment、Maximum、Minimum与ThousandSeparator属性。
类定义与构造函数
DataGridViewNumericUpDownColumn类简单派生自DataGridViewColumn类,它的构造函数中,单元格模板使用了一个默认的DataGridViewNumericUpDownCell单元格。
namespace DataGridViewNumericUpDownElements
{
public class DataGridViewNumericUpDownColumn : DataGridViewColumn
{
public DataGridViewNumericUpDownColumn()
: base(new DataGridViewNumericUpDownCell())
{
}
}
}定义表格列属性
现仔细考虑表格列类型如何实现一个属性。DataGridViewNumericUpDownColumn类的DecimalPlaces属性实现代码如下:
[
Category("Appearance"),
DefaultValue(DataGridViewNumericUpDownCell
.DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces),
Description("Indicates the number of decimal places to display.")
]
public int DecimalPlaces
{
get
{
if (this.NumericUpDownCellTemplate == null)
{
throw new InvalidOperationException("Operation cannot be completed
becausethis DataGridViewColumn does not have a CellTemplate.");
}
return this.NumericUpDownCellTemplate.DecimalPlaces;
}
set
{
if (this.NumericUpDownCellTemplate == null)
{
throw new InvalidOperationException("Operation cannot be completed
because this DataGridViewColumn does not have a CellTemplate.");
}
// 更新模板单元格,使得随后克隆的单元格使用新值。
this.NumericUpDownCellTemplate.DecimalPlaces = value;
if (this.DataGridView != null)
{
// 随后,更新该列中所有存在的DataGridViewNumericUpDownCell单元格。
DataGridViewRowCollection dataGridViewRows = this.DataGridView.Rows;
int rowCount = dataGridViewRows.Count;
for (int rowIndex = 0; rowIndex < rowCount; rowIndex++)
{
// 注意,不要不必要地取消行共享。
// 这将导致严重的性能影响。
DataGridViewRow dataGridViewRow =
dataGridViewRows.SharedRow(rowIndex);
DataGridViewNumericUpDownCell dataGridViewCell =
dataGridViewRow.Cells[this.Index] as DataGridViewNumericUpDownCell;
if (dataGridViewCell != null)
{
// 调用内部的SetDecimalPlaces方法而不是属性,以避免每个单元格失效。
// 在单个操作中,让整个表格列失效将有更好的性能。
dataGridViewCell.SetDecimalPlaces(rowIndex, value);
}
}
this.DataGridView.InvalidateColumn(this.Index);
// 要做:如果必要,调用表格的autosizing方法调整表格列、行、列头/行头的大小。
}
}
}关于最后的自动调整大小的说明。定制表格列可能需要自动调整影响到的表格元素的大小,如同标准表格类的处理方法。DecimalPlaces属性影响单元格外观和它的最合适大小。结果是,改变属性值可能要求调整列宽度、一些行高度、列头高度以及行头宽度,这些都依据它们单个的自调整大小设置。 例如,如果表格列继承的auto-size模式是DataGridViewAutoSizeColumnMode.AllCells,那么调用保护方法DataGridView.AutoResizeColumn(int columnIndex, DataGridViewAutoSizeColumnMode autoSizeColumnMode, bool fixedHeight)时需要该参数值。类似地,如果DataGridView.ColumnHeadersHeightSizeMode属性设置为DataGridViewColumnHeadersHeightSizeMode.AutoSize,那么需要调用保护方法DataGridView.AutoResizeColumnHeadersHeight(int columnIndex, bool fixedRowHeadersWidth, bool fixedColumnWidth),等等。 因为它们是保护方法,仅当从DataGridView控件派生时自动的auto-sizing才能正确执行。然后,定制表格列类型才能调用派生控件的公共方法并做全部的auto-sizing工作:
((MyDataGridView) this.dataGridView).OnGlobalColumnAutoSize(this.Index);派生类MyDataGridView定义了一个公共方法OnGlobalColumnAutoSize(int columnIndex),需要时它调用AutoResizeColumn(...)、AutoResizeColumnHeadersHeight(...)、AutoResizeRows(...)与AutoResizeRowHeadersWidth(...)等方法。
提示: 实现OnGlobalColumnAutoSize(int columnIndex)方法需要做许多工作,可以作为一篇文章的主题。 改变表格列的DefaultCellStyle<属性时内部触发所有的auto-sizing调整,下面是使用这个性质的实现代码。
// MyDataGridView.OnGlobalColumnAutoSize的实现
public void OnGlobalColumnAutoSize(int columnIndex)
{
if (columnIndex < -1 || columnIndex >= this.Columns.Count)
{
throw new ArgumentOutOfRangeException("columnIndex");
}
OnColumnDefaultCellStyleChanged(new DataGridViewColumnEventArgs
(this.Columns[columnIndex]));
}除了DecimalPlaces、Increment、Maximum、Minimum与ThousandSeparator属性,表格列还定义了关键的CellTemplate 属性,见下面代码:
[
Browsable(false),
DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)
]
public override DataGridViewCell CellTemplate
{
get
{
return base.CellTemplate;
}
set
{
DataGridViewNumericUpDownCell dataGridViewNumericUpDownCell =
value as DataGridViewNumericUpDownCell;
if (value != null && dataGridViewNumericUpDownCell == null)
{
throw new InvalidCastException(
"Value provided for CellTemplate must be of type
DataGridViewNumericUpDownElements.DataGridViewNumericUpDownCell
or derive from it.");
}
base.CellTemplate = value;
}
}例如,当DataGridViewRowCollection.Add()调用时使用CellTemplate属性。因为没有提供直接的单元格,于是增加了一个DataGridView.RowTemplate的克隆对象。默认情况下,RowTemplate随每个表格列的克隆而移住(populated)。
重写的关键方法
定制表格列一般需要重写ToString()方法。
// 返回表示表格列的标准紧凑字符串。
public override string ToString()
{
StringBuilder sb = new StringBuilder(100);
sb.Append("DataGridViewNumericUpDownColumn { Name=");
sb.Append(this.Name);
sb.Append(", Index=");
sb.Append(this.Index.ToString(CultureInfo.CurrentCulture));
sb.Append(" }");
return sb.ToString();
}极少情况下,表格列类型需要公开那些单元格层没有等价的属性,例如:DataGridViewLinkColumn.Text与DataGridViewImageColumn.Image。这时,表格列类需要重写Clone方法以复制这些属性。
// 定制的Clone实现需要复制表格列的全部指定属性——关联单元格没有公开的。
public override object Clone()
{
DataGridViewXXXColumn dataGridViewColumn =
base.Clone() as DataGridViewXXXColumn;
if (dataGridViewColumn != null)
{
dataGridViewColumn.YYY = this.YYY;
}
return dataGridViewColumn;
}DataGridViewNumericUpDownColumn没有这样的属性,因而不需要重写Clone方法。
DataGridViewNumericUpDownColumn的运行截图
图2是定制单元格和表格列的样例程序的屏幕截图。
图2. 有一些单元格的DataGridViewNumericUpDownColumn结论
本文介绍了如何基于Windows Forms NumericUpDown控件构建DataGridView的定制单元格和表格列,它扩展了表格的功能,使得用户在表格中容易输入数值型数据。