namespace CC_Functions.W32.Forms { public class DataGridViewNumericUpDownCell : DataGridViewTextBoxCell { // 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; // 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; // Type of this cell's editing control private static readonly 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 readonly 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 maximum; // Caches the value of the Maximum property private decimal minimum; // Caches the value of the Minimum 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 => 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 => DataGridView.EditingControl as DataGridViewNumericUpDownEditingControl; /// /// Define the type of the cell's editing control /// public override Type EditType => defaultEditType; // the type is DataGridViewNumericUpDownEditingControl /// /// The Increment property replicates the one from the NumericUpDown control /// public decimal Increment { get => 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 => maximum; set { if (maximum != value) { SetMaximum(RowIndex, value); OnCommonChange(); } } } /// /// The Minimum property replicates the one from the NumericUpDown control /// public decimal Minimum { get => 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 => 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; } } // Used in KeyEntersEditMode function [DllImport("USER32.DLL", CharSet = CharSet.Auto)] private static extern short VkKeyScan(char key); /// /// 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); } 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) => (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() => $"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; if ((align & anyCenter) != 0) return HorizontalAlignment.Center; return HorizontalAlignment.Left; } } }