diff --git a/Misc/DataGridViewNumericUpDownCell.cs b/Misc/DataGridViewNumericUpDownCell.cs new file mode 100644 index 0000000..c6e2f91 --- /dev/null +++ b/Misc/DataGridViewNumericUpDownCell.cs @@ -0,0 +1,775 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Drawing; +using System.Globalization; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace GradeCalc +{ + public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell + { + // Used in KeyEntersEditMode function + [DllImport("USER32.DLL", CharSet = CharSet.Auto)] + private static extern short VkKeyScan(char key); + + // Used in TranslateAlignment function + private static readonly DataGridViewContentAlignment anyRight = DataGridViewContentAlignment.TopRight | + DataGridViewContentAlignment.MiddleRight | + DataGridViewContentAlignment.BottomRight; + + private static readonly DataGridViewContentAlignment anyCenter = DataGridViewContentAlignment.TopCenter | + DataGridViewContentAlignment.MiddleCenter | + DataGridViewContentAlignment.BottomCenter; + + // Default dimensions of the static rendering bitmap used for the painting of the non-edited cells + private const int DATAGRIDVIEWNUMERICUPDOWNCELL_defaultRenderingBitmapWidth = 100; + + private const int DATAGRIDVIEWNUMERICUPDOWNCELL_defaultRenderingBitmapHeight = 22; + + // Default value of the DecimalPlaces property + internal const int DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces = 0; + + // Default value of the Increment property + internal const decimal DATAGRIDVIEWNUMERICUPDOWNCELL_defaultIncrement = decimal.One; + + // Default value of the Maximum property + internal const decimal DATAGRIDVIEWNUMERICUPDOWNCELL_defaultMaximum = (decimal)100.0; + + // Default value of the Minimum property + internal const decimal DATAGRIDVIEWNUMERICUPDOWNCELL_defaultMinimum = decimal.Zero; + + // Default value of the ThousandsSeparator property + internal const bool DATAGRIDVIEWNUMERICUPDOWNCELL_defaultThousandsSeparator = false; + + // Type of this cell's editing control + private static Type defaultEditType = typeof(DataGridViewNumericUpDownEditingControl); + + // Type of this cell's value. The formatted value type is string, the same as the base class DataGridViewTextBoxCell + private static Type defaultValueType = typeof(decimal); + + // The bitmap used to paint the non-edited cells via a call to NumericUpDown.DrawToBitmap + [ThreadStatic] + private static Bitmap renderingBitmap; + + // The NumericUpDown control used to paint the non-edited cells via a call to NumericUpDown.DrawToBitmap + [ThreadStatic] + private static NumericUpDown paintingNumericUpDown; + + private int decimalPlaces; // Caches the value of the DecimalPlaces property + private decimal increment; // Caches the value of the Increment property + private decimal minimum; // Caches the value of the Minimum property + private decimal maximum; // Caches the value of the Maximum property + private bool thousandsSeparator; // Caches the value of the ThousandsSeparator property + + /// + /// Constructor for the DataGridViewNumericUpDownCell cell type + /// + public DataGridViewNumericUpDownCell() + { + // Create a thread specific bitmap used for the painting of the non-edited cells + if (renderingBitmap == null) + { + renderingBitmap = new Bitmap(DATAGRIDVIEWNUMERICUPDOWNCELL_defaultRenderingBitmapWidth, DATAGRIDVIEWNUMERICUPDOWNCELL_defaultRenderingBitmapHeight); + } + + // Create a thread specific NumericUpDown control used for the painting of the non-edited cells + if (paintingNumericUpDown == null) + { + paintingNumericUpDown = new NumericUpDown(); + // Some properties only need to be set once for the lifetime of the control: + paintingNumericUpDown.BorderStyle = BorderStyle.None; + paintingNumericUpDown.Maximum = decimal.MaxValue / 10; + paintingNumericUpDown.Minimum = decimal.MinValue / 10; + } + + // Set the default values of the properties: + decimalPlaces = DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces; + increment = DATAGRIDVIEWNUMERICUPDOWNCELL_defaultIncrement; + minimum = DATAGRIDVIEWNUMERICUPDOWNCELL_defaultMinimum; + maximum = DATAGRIDVIEWNUMERICUPDOWNCELL_defaultMaximum; + thousandsSeparator = DATAGRIDVIEWNUMERICUPDOWNCELL_defaultThousandsSeparator; + } + + /// + /// The DecimalPlaces property replicates the one from the NumericUpDown control + /// + [ + DefaultValue(DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces) + ] + public int DecimalPlaces + { + get { + return decimalPlaces; + } + + set { + if (value < 0 || value > 99) + { + throw new ArgumentOutOfRangeException("The DecimalPlaces property cannot be smaller than 0 or larger than 99."); + } + if (decimalPlaces != value) + { + SetDecimalPlaces(RowIndex, value); + OnCommonChange(); // Assure that the cell or column gets repainted and autosized if needed + } + } + } + + /// + /// Returns the current DataGridView EditingControl as a DataGridViewNumericUpDownEditingControl control + /// + private DataGridViewNumericUpDownEditingControl EditingNumericUpDown + { + get { + return DataGridView.EditingControl as DataGridViewNumericUpDownEditingControl; + } + } + + /// + /// Define the type of the cell's editing control + /// + public override Type EditType + { + get { + return defaultEditType; // the type is DataGridViewNumericUpDownEditingControl + } + } + + /// + /// The Increment property replicates the one from the NumericUpDown control + /// + public decimal Increment + { + get { + return increment; + } + + set { + if (value < (decimal)0.0) + { + throw new ArgumentOutOfRangeException("The Increment property cannot be smaller than 0."); + } + SetIncrement(RowIndex, value); + // No call to OnCommonChange is needed since the increment value does not affect the rendering of the cell. + } + } + + /// + /// The Maximum property replicates the one from the NumericUpDown control + /// + public decimal Maximum + { + get { + return maximum; + } + + set { + if (maximum != value) + { + SetMaximum(RowIndex, value); + OnCommonChange(); + } + } + } + + /// + /// The Minimum property replicates the one from the NumericUpDown control + /// + public decimal Minimum + { + get { + return minimum; + } + + set { + if (minimum != value) + { + SetMinimum(RowIndex, value); + OnCommonChange(); + } + } + } + + /// + /// The ThousandsSeparator property replicates the one from the NumericUpDown control + /// + [ + DefaultValue(DATAGRIDVIEWNUMERICUPDOWNCELL_defaultThousandsSeparator) + ] + public bool ThousandsSeparator + { + get { + return thousandsSeparator; + } + + set { + if (thousandsSeparator != value) + { + SetThousandsSeparator(RowIndex, value); + OnCommonChange(); + } + } + } + + /// + /// Returns the type of the cell's Value property + /// + public override Type ValueType + { + get { + Type valueType = base.ValueType; + if (valueType != null) + { + return valueType; + } + return defaultValueType; + } + } + + /// + /// Clones a DataGridViewNumericUpDownCell cell, copies all the custom properties. + /// + public override object Clone() + { + DataGridViewNumericUpDownCell dataGridViewCell = base.Clone() as DataGridViewNumericUpDownCell; + if (dataGridViewCell != null) + { + dataGridViewCell.DecimalPlaces = DecimalPlaces; + dataGridViewCell.Increment = Increment; + dataGridViewCell.Maximum = Maximum; + dataGridViewCell.Minimum = Minimum; + dataGridViewCell.ThousandsSeparator = ThousandsSeparator; + } + return dataGridViewCell; + } + + /// + /// Returns the provided value constrained to be within the min and max. + /// + private decimal Constrain(decimal value) + { + Debug.Assert(minimum <= maximum); + if (value < minimum) + { + value = minimum; + } + if (value > maximum) + { + value = maximum; + } + return value; + } + + /// + /// DetachEditingControl gets called by the DataGridView control when the editing session is ending + /// + [ + EditorBrowsable(EditorBrowsableState.Advanced) + ] + public override void DetachEditingControl() + { + DataGridView dataGridView = 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) + { + // Editing controls get recycled. Indeed, when a DataGridViewNumericUpDownCell cell gets edited + // after another DataGridViewNumericUpDownCell cell, the same editing control gets reused for + // performance reasons (to avoid an unnecessary control destruction and creation). + // Here the undo buffer of the TextBox inside the NumericUpDown control gets cleared to avoid + // interferences between the editing sessions. + TextBox textBox = numericUpDown.Controls[1] as TextBox; + if (textBox != null) + { + textBox.ClearUndo(); + } + } + + base.DetachEditingControl(); + } + + /// + /// Adjusts the location and size of the editing control given the alignment characteristics of the cell + /// + private Rectangle GetAdjustedEditingControlBounds(Rectangle editingControlBounds, DataGridViewCellStyle cellStyle) + { + // Add a 1 pixel padding on the left and right of the editing control + editingControlBounds.X += 1; + editingControlBounds.Width = Math.Max(0, editingControlBounds.Width - 2); + + // Adjust the vertical location of the editing control: + 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; + } + + /// + /// Customized implementation of the GetErrorIconBounds function in order to draw the potential + /// error icon next to the up/down buttons and not on top of them. + /// + protected override Rectangle GetErrorIconBounds(Graphics graphics, DataGridViewCellStyle cellStyle, int rowIndex) + { + const int ButtonsWidth = 16; + + Rectangle errorIconBounds = base.GetErrorIconBounds(graphics, cellStyle, rowIndex); + if (DataGridView.RightToLeft == RightToLeft.Yes) + { + errorIconBounds.X = errorIconBounds.Left + ButtonsWidth; + } + else + { + errorIconBounds.X = errorIconBounds.Left - ButtonsWidth; + } + return errorIconBounds; + } + + /// + /// Customized implementation of the GetFormattedValue function in order to include the decimal and thousand separator + /// characters in the formatted representation of the cell value. + /// + protected override object GetFormattedValue(object value, + int rowIndex, + ref DataGridViewCellStyle cellStyle, + TypeConverter valueTypeConverter, + TypeConverter formattedValueTypeConverter, + DataGridViewDataErrorContexts context) + { + // By default, the base implementation converts the Decimal 1234.5 into the string "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 = Convert.ToDecimal(value); + decimal formattedDecimal = Convert.ToDecimal(formattedNumber); + if (unformattedDecimal == formattedDecimal) + { + // The base implementation of GetFormattedValue (which triggers the CellFormatting event) did nothing else than + // the typical 1234.5 to "1234.5" conversion. But depending on the values of ThousandsSeparator and DecimalPlaces, + // this may not be the actual string displayed. The real formatted value may be "1,234.500" + return formattedDecimal.ToString((ThousandsSeparator ? "N" : "F") + DecimalPlaces.ToString()); + } + } + return formattedValue; + } + + /// + /// Custom implementation of the GetPreferredSize function. This implementation uses the preferred size of the base + /// DataGridViewTextBoxCell cell and adds room for the up/down buttons. + /// + protected override Size GetPreferredSize(Graphics graphics, DataGridViewCellStyle cellStyle, int rowIndex, Size constraintSize) + { + if (DataGridView == null) + { + return new Size(-1, -1); + } + + Size preferredSize = base.GetPreferredSize(graphics, cellStyle, rowIndex, constraintSize); + if (constraintSize.Width == 0) + { + const int ButtonsWidth = 16; // Account for the width of the up/down buttons. + const int ButtonMargin = 8; // Account for some blank pixels between the text and buttons. + preferredSize.Width += ButtonsWidth + ButtonMargin; + } + return preferredSize; + } + + /// + /// Custom implementation of the InitializeEditingControl function. This function is called by the DataGridView control + /// at the beginning of an editing session. It makes sure that the properties of the NumericUpDown editing control are + /// set according to the cell properties. + /// + public override void InitializeEditingControl(int rowIndex, object initialFormattedValue, DataGridViewCellStyle dataGridViewCellStyle) + { + base.InitializeEditingControl(rowIndex, initialFormattedValue, dataGridViewCellStyle); + NumericUpDown numericUpDown = DataGridView.EditingControl as NumericUpDown; + if (numericUpDown != null) + { + numericUpDown.BorderStyle = BorderStyle.None; + numericUpDown.DecimalPlaces = DecimalPlaces; + numericUpDown.Increment = Increment; + numericUpDown.Maximum = Maximum; + numericUpDown.Minimum = Minimum; + numericUpDown.ThousandsSeparator = ThousandsSeparator; + string initialFormattedValueStr = initialFormattedValue as string; + if (initialFormattedValueStr == null) + { + numericUpDown.Text = string.Empty; + } + else + { + numericUpDown.Text = initialFormattedValueStr; + } + } + } + + /// + /// Custom implementation of the KeyEntersEditMode function. This function is called by the DataGridView control + /// to decide whether a keystroke must start an editing session or not. In this case, a new session is started when + /// a digit or negative sign key is hit. + /// + public override bool KeyEntersEditMode(KeyEventArgs e) + { + NumberFormatInfo numberFormatInfo = 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; + } + + /// + /// Called when a cell characteristic that affects its rendering and/or preferred size has changed. + /// This implementation only takes care of repainting the cells. The DataGridView's autosizing methods + /// also need to be called in cases where some grid elements autosize. + /// + private void OnCommonChange() + { + if (DataGridView != null && !DataGridView.IsDisposed && !DataGridView.Disposing) + { + if (RowIndex == -1) + { + // Invalidate and autosize column + DataGridView.InvalidateColumn(ColumnIndex); + + // TODO: Add code to autosize the cell's column, the rows, the column headers + // and the row headers depending on their autosize settings. + // The DataGridView control does not expose a public method that takes care of this. + } + else + { + // The DataGridView control exposes a public method called UpdateCellValue + // that invalidates the cell so that it gets repainted and also triggers all + // the necessary autosizing: the cell's column and/or row, the column headers + // and the row headers are autosized depending on their autosize settings. + DataGridView.UpdateCellValue(ColumnIndex, RowIndex); + } + } + } + + /// + /// Determines whether this cell, at the given row index, shows the grid's editing control or not. + /// The row index needs to be provided as a parameter because this cell may be shared among multiple rows. + /// + private bool OwnsEditingNumericUpDown(int rowIndex) + { + if (rowIndex == -1 || DataGridView == null) + { + return false; + } + DataGridViewNumericUpDownEditingControl numericUpDownEditingControl = DataGridView.EditingControl as DataGridViewNumericUpDownEditingControl; + return numericUpDownEditingControl != null && rowIndex == ((IDataGridViewEditingControl)numericUpDownEditingControl).EditingControlRowIndex; + } + + /// + /// Custom paints the cell. The base implementation of the DataGridViewTextBoxCell type is called first, + /// dropping the icon error and content foreground parts. Those two parts are painted by this custom implementation. + /// In this sample, the non-edited NumericUpDown control is painted by using a call to Control.DrawToBitmap. This is + /// an easy solution for painting controls but it's not necessarily the most performant. An alternative would be to paint + /// the NumericUpDown control piece by piece (text and up/down buttons). + /// + 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 (DataGridView == null) + { + return; + } + + // First paint the borders and background of the cell. + base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, cellStyle, advancedBorderStyle, + paintParts & ~(DataGridViewPaintParts.ErrorIcon | DataGridViewPaintParts.ContentForeground)); + + Point ptCurrentCell = DataGridView.CurrentCellAddress; + bool cellCurrent = ptCurrentCell.X == ColumnIndex && ptCurrentCell.Y == rowIndex; + bool cellEdited = cellCurrent && DataGridView.EditingControl != null; + + // If the cell is in editing mode, there is nothing else to paint + if (!cellEdited) + { + if (PartPainted(paintParts, DataGridViewPaintParts.ContentForeground)) + { + // Paint a NumericUpDown control + // Take the borders into account + Rectangle borderWidths = BorderWidths(advancedBorderStyle); + Rectangle valBounds = cellBounds; + valBounds.Offset(borderWidths.X, borderWidths.Y); + valBounds.Width -= borderWidths.Right; + valBounds.Height -= borderWidths.Bottom; + // Also take the padding into account + if (cellStyle.Padding != Padding.Empty) + { + if (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; + } + // Determine the NumericUpDown control location + valBounds = GetAdjustedEditingControlBounds(valBounds, cellStyle); + + bool cellSelected = (cellState & DataGridViewElementStates.Selected) != 0; + + if (renderingBitmap.Width < valBounds.Width || + renderingBitmap.Height < valBounds.Height) + { + // The static bitmap is too small, a bigger one needs to be allocated. + renderingBitmap.Dispose(); + renderingBitmap = new Bitmap(valBounds.Width, valBounds.Height); + } + // Make sure the NumericUpDown control is parented to a visible control + if (paintingNumericUpDown.Parent == null || !paintingNumericUpDown.Parent.Visible) + { + paintingNumericUpDown.Parent = DataGridView; + } + // Set all the relevant properties + paintingNumericUpDown.TextAlign = TranslateAlignment(cellStyle.Alignment); + paintingNumericUpDown.DecimalPlaces = DecimalPlaces; + paintingNumericUpDown.ThousandsSeparator = ThousandsSeparator; + paintingNumericUpDown.Font = cellStyle.Font; + paintingNumericUpDown.Width = valBounds.Width; + paintingNumericUpDown.Height = valBounds.Height; + paintingNumericUpDown.RightToLeft = 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) + { + // The NumericUpDown control does not support transparent back colors + backColor = Color.FromArgb(255, backColor); + } + paintingNumericUpDown.BackColor = backColor; + } + // Finally paint the NumericUpDown control + 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)) + { + // Paint the potential error icon on top of the NumericUpDown control + base.Paint(graphics, clipBounds, cellBounds, rowIndex, cellState, value, formattedValue, errorText, + cellStyle, advancedBorderStyle, DataGridViewPaintParts.ErrorIcon); + } + } + } + + /// + /// Little utility function called by the Paint function to see if a particular part needs to be painted. + /// + private static bool PartPainted(DataGridViewPaintParts paintParts, DataGridViewPaintParts paintPart) + { + return (paintParts & paintPart) != 0; + } + + /// + /// Custom implementation of the PositionEditingControl method called by the DataGridView control when it + /// needs to relocate and/or resize the editing control. + /// + 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); + DataGridView.EditingControl.Location = new Point(editingControlBounds.X, editingControlBounds.Y); + DataGridView.EditingControl.Size = new Size(editingControlBounds.Width, editingControlBounds.Height); + } + + /// + /// Utility function that sets a new value for the DecimalPlaces property of the cell. This function is used by + /// the cell and column DecimalPlaces property. The column uses this method instead of the DecimalPlaces + /// property for performance reasons. This way the column can invalidate the entire column at once instead of + /// invalidating each cell of the column individually. A row index needs to be provided as a parameter because + /// this cell may be shared among multiple rows. + /// + internal void SetDecimalPlaces(int rowIndex, int value) + { + Debug.Assert(value >= 0 && value <= 99); + decimalPlaces = value; + if (OwnsEditingNumericUpDown(rowIndex)) + { + EditingNumericUpDown.DecimalPlaces = value; + } + } + + /// Utility function that sets a new value for the Increment property of the cell. This function is used by + /// the cell and column Increment property. A row index needs to be provided as a parameter because + /// this cell may be shared among multiple rows. + internal void SetIncrement(int rowIndex, decimal value) + { + Debug.Assert(value >= (decimal)0.0); + increment = value; + if (OwnsEditingNumericUpDown(rowIndex)) + { + EditingNumericUpDown.Increment = value; + } + } + + /// Utility function that sets a new value for the Maximum property of the cell. This function is used by + /// the cell and column Maximum property. The column uses this method instead of the Maximum + /// property for performance reasons. This way the column can invalidate the entire column at once instead of + /// invalidating each cell of the column individually. A row index needs to be provided as a parameter because + /// this cell may be shared among multiple rows. + internal void SetMaximum(int rowIndex, decimal value) + { + maximum = value; + if (minimum > maximum) + { + minimum = maximum; + } + object cellValue = GetValue(rowIndex); + if (cellValue != null) + { + decimal currentValue = Convert.ToDecimal(cellValue); + decimal constrainedValue = Constrain(currentValue); + if (constrainedValue != currentValue) + { + SetValue(rowIndex, constrainedValue); + } + } + Debug.Assert(maximum == value); + if (OwnsEditingNumericUpDown(rowIndex)) + { + EditingNumericUpDown.Maximum = value; + } + } + + /// Utility function that sets a new value for the Minimum property of the cell. This function is used by + /// the cell and column Minimum property. The column uses this method instead of the Minimum + /// property for performance reasons. This way the column can invalidate the entire column at once instead of + /// invalidating each cell of the column individually. A row index needs to be provided as a parameter because + /// this cell may be shared among multiple rows. + internal void SetMinimum(int rowIndex, decimal value) + { + minimum = value; + if (minimum > maximum) + { + maximum = value; + } + object cellValue = GetValue(rowIndex); + if (cellValue != null) + { + decimal currentValue = Convert.ToDecimal(cellValue); + decimal constrainedValue = Constrain(currentValue); + if (constrainedValue != currentValue) + { + SetValue(rowIndex, constrainedValue); + } + } + Debug.Assert(minimum == value); + if (OwnsEditingNumericUpDown(rowIndex)) + { + EditingNumericUpDown.Minimum = value; + } + } + + /// Utility function that sets a new value for the ThousandsSeparator property of the cell. This function is used by + /// the cell and column ThousandsSeparator property. The column uses this method instead of the ThousandsSeparator + /// property for performance reasons. This way the column can invalidate the entire column at once instead of + /// invalidating each cell of the column individually. A row index needs to be provided as a parameter because + /// this cell may be shared among multiple rows. + internal void SetThousandsSeparator(int rowIndex, bool value) + { + thousandsSeparator = value; + if (OwnsEditingNumericUpDown(rowIndex)) + { + EditingNumericUpDown.ThousandsSeparator = value; + } + } + + /// + /// Returns a standard textual representation of the cell. + /// + public override string ToString() + { + return "DataGridViewNumericUpDownCell { ColumnIndex=" + ColumnIndex.ToString(CultureInfo.CurrentCulture) + ", RowIndex=" + RowIndex.ToString(CultureInfo.CurrentCulture) + " }"; + } + + /// + /// Little utility function used by both the cell and column types to translate a DataGridViewContentAlignment value into + /// a HorizontalAlignment value. + /// + internal static HorizontalAlignment TranslateAlignment(DataGridViewContentAlignment align) + { + if ((align & anyRight) != 0) + { + return HorizontalAlignment.Right; + } + else if ((align & anyCenter) != 0) + { + return HorizontalAlignment.Center; + } + else + { + return HorizontalAlignment.Left; + } + } + } +} \ No newline at end of file diff --git a/Misc/DataGridViewNumericUpDownColumn.cs b/Misc/DataGridViewNumericUpDownColumn.cs new file mode 100644 index 0000000..28cf13c --- /dev/null +++ b/Misc/DataGridViewNumericUpDownColumn.cs @@ -0,0 +1,304 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Text; +using System.Windows.Forms; + +namespace GradeCalc +{ + /// + /// Custom column type dedicated to the DataGridViewNumericUpDownCell cell type. + /// + public class DataGridViewNumericUpDownColumn : DataGridViewColumn + { + /// + /// Constructor for the DataGridViewNumericUpDownColumn class. + /// + public DataGridViewNumericUpDownColumn() : base(new DataGridViewNumericUpDownCell()) + { + } + + /// + /// Represents the implicit cell that gets cloned when adding rows to the grid. + /// + [ + 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; + } + } + + /// + /// Replicates the DecimalPlaces property of the DataGridViewNumericUpDownCell cell type. + /// + [ + Category("Appearance"), + DefaultValue(DataGridViewNumericUpDownCell.DATAGRIDVIEWNUMERICUPDOWNCELL_defaultDecimalPlaces), + Description("Indicates the number of decimal places to display.") + ] + public int DecimalPlaces + { + get { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + return NumericUpDownCellTemplate.DecimalPlaces; + } + set { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + // Update the template cell so that subsequent cloned cells use the new value. + NumericUpDownCellTemplate.DecimalPlaces = value; + if (DataGridView != null) + { + // Update all the existing DataGridViewNumericUpDownCell cells in the column accordingly. + DataGridViewRowCollection dataGridViewRows = DataGridView.Rows; + int rowCount = dataGridViewRows.Count; + for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) + { + // Be careful not to unshare rows unnecessarily. + // This could have severe performance repercussions. + DataGridViewRow dataGridViewRow = dataGridViewRows.SharedRow(rowIndex); + DataGridViewNumericUpDownCell dataGridViewCell = dataGridViewRow.Cells[Index] as DataGridViewNumericUpDownCell; + if (dataGridViewCell != null) + { + // Call the internal SetDecimalPlaces method instead of the property to avoid invalidation + // of each cell. The whole column is invalidated later in a single operation for better performance. + dataGridViewCell.SetDecimalPlaces(rowIndex, value); + } + } + DataGridView.InvalidateColumn(Index); + // TODO: Call the grid's autosizing methods to autosize the column, rows, column headers / row headers as needed. + } + } + } + + /// + /// Replicates the Increment property of the DataGridViewNumericUpDownCell cell type. + /// + [ + Category("Data"), + Description("Indicates the amount to increment or decrement on each button click.") + ] + public decimal Increment + { + get { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + return NumericUpDownCellTemplate.Increment; + } + set { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + NumericUpDownCellTemplate.Increment = value; + if (DataGridView != null) + { + DataGridViewRowCollection dataGridViewRows = DataGridView.Rows; + int rowCount = dataGridViewRows.Count; + for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) + { + DataGridViewRow dataGridViewRow = dataGridViewRows.SharedRow(rowIndex); + DataGridViewNumericUpDownCell dataGridViewCell = dataGridViewRow.Cells[Index] as DataGridViewNumericUpDownCell; + if (dataGridViewCell != null) + { + dataGridViewCell.SetIncrement(rowIndex, value); + } + } + } + } + } + + /// Indicates whether the Increment property should be persisted. + private bool ShouldSerializeIncrement() + { + return !Increment.Equals(DataGridViewNumericUpDownCell.DATAGRIDVIEWNUMERICUPDOWNCELL_defaultIncrement); + } + + /// + /// Replicates the Maximum property of the DataGridViewNumericUpDownCell cell type. + /// + [ + Category("Data"), + Description("Indicates the maximum value for the numeric up-down cells."), + RefreshProperties(RefreshProperties.All) + ] + public decimal Maximum + { + get { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + return NumericUpDownCellTemplate.Maximum; + } + set { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + NumericUpDownCellTemplate.Maximum = value; + if (DataGridView != null) + { + DataGridViewRowCollection dataGridViewRows = DataGridView.Rows; + int rowCount = dataGridViewRows.Count; + for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) + { + DataGridViewRow dataGridViewRow = dataGridViewRows.SharedRow(rowIndex); + DataGridViewNumericUpDownCell dataGridViewCell = dataGridViewRow.Cells[Index] as DataGridViewNumericUpDownCell; + if (dataGridViewCell != null) + { + dataGridViewCell.SetMaximum(rowIndex, value); + } + } + DataGridView.InvalidateColumn(Index); + // TODO: This column and/or grid rows may need to be autosized depending on their + // autosize settings. Call the autosizing methods to autosize the column, rows, + // column headers / row headers as needed. + } + } + } + + /// Indicates whether the Maximum property should be persisted. + private bool ShouldSerializeMaximum() + { + return !Maximum.Equals(DataGridViewNumericUpDownCell.DATAGRIDVIEWNUMERICUPDOWNCELL_defaultMaximum); + } + + /// + /// Replicates the Minimum property of the DataGridViewNumericUpDownCell cell type. + /// + [ + Category("Data"), + Description("Indicates the minimum value for the numeric up-down cells."), + RefreshProperties(RefreshProperties.All) + ] + public decimal Minimum + { + get { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + return NumericUpDownCellTemplate.Minimum; + } + set { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + NumericUpDownCellTemplate.Minimum = value; + if (DataGridView != null) + { + DataGridViewRowCollection dataGridViewRows = DataGridView.Rows; + int rowCount = dataGridViewRows.Count; + for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) + { + DataGridViewRow dataGridViewRow = dataGridViewRows.SharedRow(rowIndex); + DataGridViewNumericUpDownCell dataGridViewCell = dataGridViewRow.Cells[Index] as DataGridViewNumericUpDownCell; + if (dataGridViewCell != null) + { + dataGridViewCell.SetMinimum(rowIndex, value); + } + } + DataGridView.InvalidateColumn(Index); + // TODO: This column and/or grid rows may need to be autosized depending on their + // autosize settings. Call the autosizing methods to autosize the column, rows, + // column headers / row headers as needed. + } + } + } + + /// Indicates whether the Maximum property should be persisted. + private bool ShouldSerializeMinimum() + { + return !Minimum.Equals(DataGridViewNumericUpDownCell.DATAGRIDVIEWNUMERICUPDOWNCELL_defaultMinimum); + } + + /// + /// Replicates the ThousandsSeparator property of the DataGridViewNumericUpDownCell cell type. + /// + [ + Category("Data"), + DefaultValue(DataGridViewNumericUpDownCell.DATAGRIDVIEWNUMERICUPDOWNCELL_defaultThousandsSeparator), + Description("Indicates whether the thousands separator will be inserted between every three decimal digits.") + ] + public bool ThousandsSeparator + { + get { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + return NumericUpDownCellTemplate.ThousandsSeparator; + } + set { + if (NumericUpDownCellTemplate == null) + { + throw new InvalidOperationException("Operation cannot be completed because this DataGridViewColumn does not have a CellTemplate."); + } + NumericUpDownCellTemplate.ThousandsSeparator = value; + if (DataGridView != null) + { + DataGridViewRowCollection dataGridViewRows = DataGridView.Rows; + int rowCount = dataGridViewRows.Count; + for (int rowIndex = 0; rowIndex < rowCount; rowIndex++) + { + DataGridViewRow dataGridViewRow = dataGridViewRows.SharedRow(rowIndex); + DataGridViewNumericUpDownCell dataGridViewCell = dataGridViewRow.Cells[Index] as DataGridViewNumericUpDownCell; + if (dataGridViewCell != null) + { + dataGridViewCell.SetThousandsSeparator(rowIndex, value); + } + } + DataGridView.InvalidateColumn(Index); + // TODO: This column and/or grid rows may need to be autosized depending on their + // autosize settings. Call the autosizing methods to autosize the column, rows, + // column headers / row headers as needed. + } + } + } + + /// + /// Small utility function that returns the template cell as a DataGridViewNumericUpDownCell + /// + private DataGridViewNumericUpDownCell NumericUpDownCellTemplate + { + get { + return (DataGridViewNumericUpDownCell)CellTemplate; + } + } + + /// + /// Returns a standard compact string representation of the column. + /// + public override string ToString() + { + StringBuilder sb = new StringBuilder(100); + sb.Append("DataGridViewNumericUpDownColumn { Name="); + sb.Append(Name); + sb.Append(", Index="); + sb.Append(Index.ToString(CultureInfo.CurrentCulture)); + sb.Append(" }"); + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/Misc/DataGridViewNumericUpDownEditingControl.cs b/Misc/DataGridViewNumericUpDownEditingControl.cs new file mode 100644 index 0000000..9b1722b --- /dev/null +++ b/Misc/DataGridViewNumericUpDownEditingControl.cs @@ -0,0 +1,353 @@ +using System; +using System.Drawing; +using System.Runtime.InteropServices; +using System.Windows.Forms; + +namespace GradeCalc +{ + /// + /// Defines the editing control for the DataGridViewNumericUpDownCell custom cell type. + /// + internal class DataGridViewNumericUpDownEditingControl : NumericUpDown, IDataGridViewEditingControl + { + // Needed to forward keyboard messages to the child TextBox control. + [DllImport("USER32.DLL", CharSet = CharSet.Auto)] + private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); + + // The grid that owns this editing control + private DataGridView dataGridView; + + // Stores whether the editing control's value has changed or not + private bool valueChanged; + + // Stores the row index in which the editing control resides + private int rowIndex; + + /// + /// Constructor of the editing control class + /// + public DataGridViewNumericUpDownEditingControl() + { + // The editing control must not be part of the tabbing loop + TabStop = false; + } + + // Beginning of the IDataGridViewEditingControl interface implementation + + /// + /// Property which caches the grid that uses this editing control + /// + public virtual DataGridView EditingControlDataGridView + { + get { + return dataGridView; + } + set { + dataGridView = value; + } + } + + /// + /// Property which represents the current formatted value of the editing control + /// + public virtual object EditingControlFormattedValue + { + get { + return GetEditingControlFormattedValue(DataGridViewDataErrorContexts.Formatting); + } + set { + Text = (string)value; + } + } + + /// + /// Property which represents the row in which the editing control resides + /// + public virtual int EditingControlRowIndex + { + get { + return rowIndex; + } + set { + rowIndex = value; + } + } + + /// + /// Property which indicates whether the value of the editing control has changed or not + /// + public virtual bool EditingControlValueChanged + { + get { + return valueChanged; + } + set { + valueChanged = value; + } + } + + /// + /// Property which determines which cursor must be used for the editing panel, + /// i.e. the parent of the editing control. + /// + public virtual Cursor EditingPanelCursor + { + get { + return Cursors.Default; + } + } + + /// + /// Property which indicates whether the editing control needs to be repositioned + /// when its value changes. + /// + public virtual bool RepositionEditingControlOnValueChange + { + get { + return false; + } + } + + /// + /// Method called by the grid before the editing control is shown so it can adapt to the + /// provided cell style. + /// + public virtual void ApplyCellStyleToEditingControl(DataGridViewCellStyle dataGridViewCellStyle) + { + Font = dataGridViewCellStyle.Font; + if (dataGridViewCellStyle.BackColor.A < 255) + { + // The NumericUpDown control does not support transparent back colors + Color opaqueBackColor = Color.FromArgb(255, dataGridViewCellStyle.BackColor); + BackColor = opaqueBackColor; + dataGridView.EditingPanel.BackColor = opaqueBackColor; + } + else + { + BackColor = dataGridViewCellStyle.BackColor; + } + ForeColor = dataGridViewCellStyle.ForeColor; + TextAlign = DataGridViewNumericUpDownCell.TranslateAlignment(dataGridViewCellStyle.Alignment); + } + + /// + /// Method called by the grid on keystrokes to determine if the editing control is + /// interested in the key or not. + /// + public virtual bool EditingControlWantsInputKey(Keys keyData, bool dataGridViewWantsInputKey) + { + switch (keyData & Keys.KeyCode) + { + case Keys.Right: + { + TextBox textBox = Controls[1] as TextBox; + if (textBox != null) + { + // If the end of the selection is at the end of the string, + // let the DataGridView treat the key message + if (RightToLeft == RightToLeft.No && !(textBox.SelectionLength == 0 && textBox.SelectionStart == textBox.Text.Length) || + RightToLeft == RightToLeft.Yes && !(textBox.SelectionLength == 0 && textBox.SelectionStart == 0)) + { + return true; + } + } + break; + } + + case Keys.Left: + { + TextBox textBox = Controls[1] as TextBox; + if (textBox != null) + { + // If the end of the selection is at the begining of the string + // or if the entire text is selected and we did not start editing, + // send this character to the dataGridView, else process the key message + if (RightToLeft == RightToLeft.No && !(textBox.SelectionLength == 0 && textBox.SelectionStart == 0) || + RightToLeft == RightToLeft.Yes && !(textBox.SelectionLength == 0 && textBox.SelectionStart == textBox.Text.Length)) + { + return true; + } + } + break; + } + + case Keys.Down: + // If the current value hasn't reached its minimum yet, handle the key. Otherwise let + // the grid handle it. + if (Value > Minimum) + { + return true; + } + break; + + case Keys.Up: + // If the current value hasn't reached its maximum yet, handle the key. Otherwise let + // the grid handle it. + if (Value < Maximum) + { + return true; + } + break; + + case Keys.Home: + case Keys.End: + { + // Let the grid handle the key if the entire text is selected. + TextBox textBox = Controls[1] as TextBox; + if (textBox != null) + { + if (textBox.SelectionLength != textBox.Text.Length) + { + return true; + } + } + break; + } + + case Keys.Delete: + { + // Let the grid handle the key if the carret is at the end of the text. + TextBox textBox = Controls[1] as TextBox; + if (textBox != null) + { + if (textBox.SelectionLength > 0 || + textBox.SelectionStart < textBox.Text.Length) + { + return true; + } + } + break; + } + } + return !dataGridViewWantsInputKey; + } + + /// + /// Returns the current value of the editing control. + /// + public virtual object GetEditingControlFormattedValue(DataGridViewDataErrorContexts context) + { + bool userEdit = UserEdit; + try + { + // Prevent the Value from being set to Maximum or Minimum when the cell is being painted. + UserEdit = (context & DataGridViewDataErrorContexts.Display) == 0; + return Value.ToString((ThousandsSeparator ? "N" : "F") + DecimalPlaces.ToString()); + } + finally + { + UserEdit = userEdit; + } + } + + /// + /// Called by the grid to give the editing control a chance to prepare itself for + /// the editing session. + /// + public virtual void PrepareEditingControlForEdit(bool selectAll) + { + TextBox textBox = Controls[1] as TextBox; + if (textBox != null) + { + if (selectAll) + { + textBox.SelectAll(); + } + else + { + // Do not select all the text, but + // position the caret at the end of the text + textBox.SelectionStart = textBox.Text.Length; + } + } + } + + // End of the IDataGridViewEditingControl interface implementation + + /// + /// Small utility function that updates the local dirty state and + /// notifies the grid of the value change. + /// + private void NotifyDataGridViewOfValueChange() + { + if (!valueChanged) + { + valueChanged = true; + dataGridView.NotifyCurrentCellDirty(true); + } + } + + /// + /// Listen to the KeyPress notification to know when the value changed, and + /// notify the grid of the change. + /// + protected override void OnKeyPress(KeyPressEventArgs e) + { + base.OnKeyPress(e); + + // The value changes when a digit, the decimal separator, the group separator or + // the negative sign is pressed. + bool notifyValueChange = false; + if (char.IsDigit(e.KeyChar)) + { + notifyValueChange = true; + } + else + { + System.Globalization.NumberFormatInfo numberFormatInfo = System.Globalization.CultureInfo.CurrentCulture.NumberFormat; + string decimalSeparatorStr = numberFormatInfo.NumberDecimalSeparator; + string groupSeparatorStr = numberFormatInfo.NumberGroupSeparator; + string negativeSignStr = numberFormatInfo.NegativeSign; + if (!string.IsNullOrEmpty(decimalSeparatorStr) && decimalSeparatorStr.Length == 1) + { + notifyValueChange = decimalSeparatorStr[0] == e.KeyChar; + } + if (!notifyValueChange && !string.IsNullOrEmpty(groupSeparatorStr) && groupSeparatorStr.Length == 1) + { + notifyValueChange = groupSeparatorStr[0] == e.KeyChar; + } + if (!notifyValueChange && !string.IsNullOrEmpty(negativeSignStr) && negativeSignStr.Length == 1) + { + notifyValueChange = negativeSignStr[0] == e.KeyChar; + } + } + + if (notifyValueChange) + { + // Let the DataGridView know about the value change + NotifyDataGridViewOfValueChange(); + } + } + + /// + /// Listen to the ValueChanged notification to forward the change to the grid. + /// + protected override void OnValueChanged(EventArgs e) + { + base.OnValueChanged(e); + if (Focused) + { + // Let the DataGridView know about the value change + NotifyDataGridViewOfValueChange(); + } + } + + /// + /// A few keyboard messages need to be forwarded to the inner textbox of the + /// NumericUpDown control so that the first character pressed appears in it. + /// + protected override bool ProcessKeyEventArgs(ref Message m) + { + TextBox textBox = Controls[1] as TextBox; + if (textBox != null) + { + SendMessage(textBox.Handle, m.Msg, m.WParam, m.LParam); + return true; + } + else + { + return base.ProcessKeyEventArgs(ref m); + } + } + } +} \ No newline at end of file diff --git a/Misc/Misc.csproj b/Misc/Misc.csproj index e157d72..624dfe6 100644 --- a/Misc/Misc.csproj +++ b/Misc/Misc.csproj @@ -34,6 +34,7 @@ + @@ -46,6 +47,11 @@ + + + + Component +