From 258bb621b33c130c155f820139bd42bece0b15a6 Mon Sep 17 00:00:00 2001 From: "13997737+wolframhaussig@users.noreply.github.com" <13997737+wolframhaussig@users.noreply.github.com> Date: Wed, 9 Aug 2023 19:54:18 +0200 Subject: [PATCH 1/5] First proposal for DateTimePicker --- .../FrmDefault.Designer.cs | 15 +- .../FrmDefault.resx | 12 + .../Controls/StylableDateTimePicker.cs | 298 ++++++++++++++++-- .../Extensions/ColorExtensions.cs | 58 ++++ 4 files changed, 357 insertions(+), 26 deletions(-) create mode 100644 StylableWinFormsControls/StylableWinFormsControls/Extensions/ColorExtensions.cs diff --git a/StylableWinFormsControls/StylableWinFormsControls.Example/FrmDefault.Designer.cs b/StylableWinFormsControls/StylableWinFormsControls.Example/FrmDefault.Designer.cs index 6ef81ff..0bf3ba9 100644 --- a/StylableWinFormsControls/StylableWinFormsControls.Example/FrmDefault.Designer.cs +++ b/StylableWinFormsControls/StylableWinFormsControls.Example/FrmDefault.Designer.cs @@ -28,11 +28,11 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { - ListViewItem listViewItem1 = new ListViewItem("Content"); - ListViewItem listViewItem2 = new ListViewItem("Content"); - ListViewItem listViewItem3 = new ListViewItem(new string[] { "Content", "Content" }, -1); - ListViewItem listViewItem4 = new ListViewItem("Content"); - ListViewItem listViewItem5 = new ListViewItem("Content"); + ListViewItem listViewItem6 = new ListViewItem("Content"); + ListViewItem listViewItem7 = new ListViewItem("Content"); + ListViewItem listViewItem8 = new ListViewItem(new string[] { "Content", "Content" }, -1); + ListViewItem listViewItem9 = new ListViewItem("Content"); + ListViewItem listViewItem10 = new ListViewItem("Content"); stylableButton1 = new StylableButton(); stylableCheckBox1 = new StylableCheckBox(); stylableComboBox1 = new StylableComboBox(); @@ -171,7 +171,7 @@ private void InitializeComponent() // stylableListView1 // stylableListView1.Columns.AddRange(new ColumnHeader[] { columnHeader1, columnHeader2 }); - stylableListView1.Items.AddRange(new ListViewItem[] { listViewItem1, listViewItem2, listViewItem3, listViewItem4, listViewItem5 }); + stylableListView1.Items.AddRange(new ListViewItem[] { listViewItem6, listViewItem7, listViewItem8, listViewItem9, listViewItem10 }); stylableListView1.Location = new Point(6, 18); stylableListView1.Name = "stylableListView1"; stylableListView1.Size = new Size(242, 178); @@ -225,7 +225,7 @@ private void InitializeComponent() stylableTextBox1.BorderColor = Color.Blue; stylableTextBox1.BorderStyle = BorderStyle.None; stylableTextBox1.DelayedTextChangedTimeout = 900; - stylableTextBox1.ForeColor = Color.Black; + stylableTextBox1.ForeColor = Color.Gray; stylableTextBox1.Hint = "Hello, my name is ..."; stylableTextBox1.HintForeColor = Color.Gray; stylableTextBox1.IsDelayActive = true; @@ -233,6 +233,7 @@ private void InitializeComponent() stylableTextBox1.Name = "stylableTextBox1"; stylableTextBox1.Size = new Size(207, 16); stylableTextBox1.TabIndex = 8; + stylableTextBox1.Text = "Hello, my name is ..."; stylableTextBox1.TextForeColor = Color.Black; // // lbl_description diff --git a/StylableWinFormsControls/StylableWinFormsControls.Example/FrmDefault.resx b/StylableWinFormsControls/StylableWinFormsControls.Example/FrmDefault.resx index 266fef6..9563326 100644 --- a/StylableWinFormsControls/StylableWinFormsControls.Example/FrmDefault.resx +++ b/StylableWinFormsControls/StylableWinFormsControls.Example/FrmDefault.resx @@ -129,4 +129,16 @@ True + + True + + + True + + + True + + + True + \ No newline at end of file diff --git a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs index c4e5382..3be3187 100644 --- a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs +++ b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs @@ -1,5 +1,8 @@ -using System.ComponentModel; +using StylableWinFormsControls.Extensions; +using System.ComponentModel; using System.Drawing.Text; +using System.Globalization; +using System.Text.RegularExpressions; using System.Windows.Forms.VisualStyles; namespace StylableWinFormsControls; @@ -9,24 +12,35 @@ namespace StylableWinFormsControls; /// public class StylableDateTimePicker : DateTimePicker { - public Color EnabledBackColor { get; set; } = Color.White; - public Color DisabledBackColor { get; set; } = Color.Gray; - public Color EnabledForeColor { get; set; } = Color.Black; - public Color DisabledForeColor { get; set; } = Color.Black; + /// + /// separate two spaces with the given width + /// + public const int PART_SPACE = 5; + + /// + /// contains all DateTime parts and their positions + /// + private readonly List _dateParts = new List(); + + /// + /// when the format is changed we need to recalculate the positions of the parts + /// + private string _oldFormat = ""; + + /// + /// when the width is changed we need to recalculate the positions of the parts + /// + private int _oldWidth = 0; + public StylableDateTimePicker() { this.SetStyle(ControlStyles.UserPaint, true); + this.MouseDown += StylableDateTimePicker_MouseDown; + this.Leave += StylableDateTimePicker_Leave; + this.MouseWheel += StylableDateTimePicker_MouseWheel; + this.KeyDown += StylableDateTimePicker_KeyDown; } - protected override CreateParams CreateParams - { - get - { - CreateParams handleParam = base.CreateParams; - //prevent flickering of the control - handleParam.ExStyle |= 0x02000000; // WS_EX_COMPOSITED - return handleParam; - } - } + /// /// Gets or sets the background color of the control /// @@ -43,7 +57,23 @@ public override Color BackColor } } - protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) + public Color DisabledBackColor { get; set; } = Color.Gray; + public Color DisabledForeColor { get; set; } = Color.Black; + public Color EnabledBackColor { get; set; } = Color.White; + public Color EnabledForeColor { get; set; } = Color.Black; + + protected override CreateParams CreateParams + { + get + { + CreateParams handleParam = base.CreateParams; + //prevent flickering of the control + handleParam.ExStyle |= 0x02000000; // WS_EX_COMPOSITED + return handleParam; + } + } + + protected override void OnPaint(PaintEventArgs e) { Graphics g = this.CreateGraphics(); g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; @@ -57,7 +87,7 @@ protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) ComboBoxState visual_state; - // When enabled the brush is set to Backcolor, + // When enabled the brush is set to Backcolor, // otherwise to color stored in _disabled_back_Color if (this.Enabled) { @@ -75,8 +105,8 @@ protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) // Filling the background g.FillRectangle(bb, 0, 0, ClientRectangle.Width, ClientRectangle.Height); - // Drawing the datetime text - g.DrawString(this.Text, this.Font, fb, 5, 2); + // Drawing the datetime + DrawDateTime(g); // Drawing the dropdownbutton using ComboBoxRenderer ComboBoxRenderer.DrawDropDownButton(g, ddb_rect, visual_state); @@ -85,4 +115,234 @@ protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) bb.Dispose(); fb.Dispose(); } + + /// + /// this draw the datetime text manually so we can save the information where day, month and year is + /// + /// + private void DrawDateTime(Graphics g) + { + // Drawing the datetime text + string format; + DateTimeFormatInfo dtfi = (new CultureInfo("hr-HR")).DateTimeFormat; + switch (Format) + { + case DateTimePickerFormat.Long: + format = dtfi.LongDatePattern; + break; + + case DateTimePickerFormat.Short: + format = dtfi.ShortDatePattern; + break; + + case DateTimePickerFormat.Time: + format = dtfi.LongTimePattern; + break; + + default: + format = CustomFormat; + break; + } + //recalculate the positions of the DateTime parts within the DateTimePicker + RecalcPartPositions(format); + //draw the DateTime parts + var brush = new SolidBrush(this.ForeColor); + var highlightBrush = new SolidBrush(this.ForeColor.Highlight()); + foreach (var part in _dateParts) + { + if (part.Selected) + g.DrawString(this.Value.ToString(part.Format), this.Font, highlightBrush, part.Start, 2); + else + g.DrawString(this.Value.ToString(part.Format), this.Font, brush, part.Start, 2); + } + } + + /// + /// increment/decrement the selected part + /// + /// + private void IncrementDecrement(Keys key) + { + if (key == Keys.Up) + UpdatePart(1); + else if (key == Keys.Down) + UpdatePart(-1); + } + + /// + /// navigate through the DateTime parts with the arrow keys + /// + /// + /// + private void NavigateParts(Keys keyCode) + { + var part = _dateParts.FirstOrDefault(p => p.Selected); + if (part == null && keyCode == Keys.Left) + { + _dateParts.Last().Selected = true; + } + else if (part != null && keyCode == Keys.Left) + { + part.Selected = false; + var index = _dateParts.IndexOf(part); + if (index == 0) + _dateParts.Last().Selected = true; + else + _dateParts[index - 1].Selected = true; + } + else if (part == null && keyCode == Keys.Right) + { + _dateParts.First().Selected = true; + } + else if (part != null && keyCode == Keys.Right) + { + part.Selected = false; + var index = _dateParts.IndexOf(part); + if (index == _dateParts.Count - 1) + _dateParts.First().Selected = true; + else + _dateParts[index + 1].Selected = true; + } + } + + /// + /// recalculate the positions of the DateTime parts + /// + /// + private void RecalcPartPositions(string format) + { + if (_oldFormat != format || _oldWidth != this.Width) + { + //reset old list + _dateParts.Clear(); + + //split the format into parts + var parts = Regex.Split(format, @"(?<=[^0-9a-zA-Z]+)"); + int startPos = 5; + foreach (var part in parts) + { + if (String.IsNullOrWhiteSpace(part)) continue; + //get the size of the part + var size = TextRenderer.MeasureText(this.Value.ToString(part), this.Font); + //add the part to the list + _dateParts.Add(new DatePartInfo() + { + Format = part, + Start = startPos, + End = startPos + size.Width + PART_SPACE + }); + startPos += size.Width + PART_SPACE; + } + + //store the format and width so we do not have to recalculate the positions if nothing changed + _oldFormat = format; + _oldWidth = this.Width; + } + } + + /// + /// allow navigating through the DateTime parts with the arrow keys and increment/decrement the selected part or + /// specify the value with the number keys + /// + /// + /// + /// + private void StylableDateTimePicker_KeyDown(object? sender, KeyEventArgs e) + { + if (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down) + { + IncrementDecrement(e.KeyCode); + } + else if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right) + { + NavigateParts(e.KeyCode); + } + } + + /// + /// reset select part of the format when the control lost focus + /// + /// + /// + /// + private void StylableDateTimePicker_Leave(object? sender, EventArgs e) + { + _dateParts.ForEach(p => p.Selected = false); + this.Invalidate(); + } + + /// + /// support selecting the DateTime parts with the mouse to change them + /// + /// + /// + /// + private void StylableDateTimePicker_MouseDown(object? sender, MouseEventArgs e) + { + _dateParts.ForEach(p => p.Selected = false); + var part = _dateParts.FirstOrDefault(p => p.Start <= e.X && p.End >= e.X); + if (part != null) + { + part.Selected = true; + this.Invalidate(); + } + } + + /// + /// Allow changing the current value with the mouse wheel + /// + /// + /// + /// + private void StylableDateTimePicker_MouseWheel(object? sender, MouseEventArgs e) + { + UpdatePart(e.Delta / 100); + } + + /// + /// update the selected part of the DateTime after user change + /// + /// + private void UpdatePart(int num = 1) + { + var part = _dateParts.FirstOrDefault(p => p.Selected); + if (num != 0 && part != null) + { + //create a timespan with the given number of days, months, years, hours, minutes or seconds + int absNumber = Math.Abs(num); + DateTime tmp = new(absNumber, absNumber, absNumber, absNumber, absNumber, absNumber); + var tsFmt = Regex.Replace(part.Format, @"([^a-zA-Z0-9]+)", "\\$1").ToLower(); + TimeSpan ts = TimeSpan.ParseExact(tmp.ToString(part.Format, CultureInfo.CurrentCulture), tsFmt, CultureInfo.CurrentCulture); + if (num > 0) + this.Value = this.Value.Add(ts); + else + this.Value = this.Value.Subtract(ts); + } + } + + /// + /// contains the information about the DateTime part and its position + /// + private class DatePartInfo + { + /// + /// the end X position of the DateTime part + /// + public int End { get; set; } + + /// + /// the format of the DateTime part + /// + public string Format { get; set; } + + /// + /// if true, this part is selected and can be changed by the user + /// + public bool Selected { get; set; } + + /// + /// the start X position of the DateTime part + /// + public int Start { get; set; } + } } \ No newline at end of file diff --git a/StylableWinFormsControls/StylableWinFormsControls/Extensions/ColorExtensions.cs b/StylableWinFormsControls/StylableWinFormsControls/Extensions/ColorExtensions.cs new file mode 100644 index 0000000..48dcdfe --- /dev/null +++ b/StylableWinFormsControls/StylableWinFormsControls/Extensions/ColorExtensions.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StylableWinFormsControls.Extensions +{ + /// + /// Extensions for interaction with colors + /// + internal static class ColorExtensions + { + /// + /// the middle value of the rgb range between light and dark: + /// FF + FF + FF = 2FD + /// 2FD /2 = 17E + /// 17E => 382 + /// + public const int RGB_MIDDLE_VALUE = 382; + /// + /// makes a color lighter + /// + /// + /// + internal static Color Lighter(this Color c) + { + return ControlPaint.Light(c, 1f); + } + /// + /// makes a color darker + /// + /// + /// + internal static Color Darker(this Color c) + { + return ControlPaint.Dark(c, 1f); + } + /// + /// highlights a color + /// + /// + /// + internal static Color Highlight(this Color c) + { + //ignore alpha value in checking if we need to make it darker or lighter + int rgb = c.R + c.G + c.B; + if(rgb > RGB_MIDDLE_VALUE) + { + return c.Darker(); + } + else + { + return c.Lighter(); + } + } + } +} From 6b4775dac832043e3e262c172ec454ac21859c2f Mon Sep 17 00:00:00 2001 From: "13997737+wolframhaussig@users.noreply.github.com" <13997737+wolframhaussig@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:28:24 +0200 Subject: [PATCH 2/5] fix Up/Down-Buttons seem to increment/decrement twice --- .../Controls/StylableDateTimePicker.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs index 3be3187..8eca2df 100644 --- a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs +++ b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs @@ -156,7 +156,7 @@ private void DrawDateTime(Graphics g) g.DrawString(this.Value.ToString(part.Format), this.Font, brush, part.Start, 2); } } - + int counter = 0; /// /// increment/decrement the selected part /// @@ -257,6 +257,7 @@ private void StylableDateTimePicker_KeyDown(object? sender, KeyEventArgs e) { NavigateParts(e.KeyCode); } + e.Handled = true; } /// @@ -313,6 +314,7 @@ private void UpdatePart(int num = 1) DateTime tmp = new(absNumber, absNumber, absNumber, absNumber, absNumber, absNumber); var tsFmt = Regex.Replace(part.Format, @"([^a-zA-Z0-9]+)", "\\$1").ToLower(); TimeSpan ts = TimeSpan.ParseExact(tmp.ToString(part.Format, CultureInfo.CurrentCulture), tsFmt, CultureInfo.CurrentCulture); + counter += num; if (num > 0) this.Value = this.Value.Add(ts); else From bc9f5918d736a0838a9476e0cb461ca2244d45d5 Mon Sep 17 00:00:00 2001 From: "13997737+wolframhaussig@users.noreply.github.com" <13997737+wolframhaussig@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:44:12 +0200 Subject: [PATCH 3/5] support dates --- .../Controls/StylableDateTimePicker.cs | 63 ++++++++++++++++--- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs index 8eca2df..5f1a736 100644 --- a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs +++ b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs @@ -256,6 +256,7 @@ private void StylableDateTimePicker_KeyDown(object? sender, KeyEventArgs e) else if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right) { NavigateParts(e.KeyCode); + this.Invalidate(); } e.Handled = true; } @@ -309,17 +310,61 @@ private void UpdatePart(int num = 1) var part = _dateParts.FirstOrDefault(p => p.Selected); if (num != 0 && part != null) { - //create a timespan with the given number of days, months, years, hours, minutes or seconds - int absNumber = Math.Abs(num); - DateTime tmp = new(absNumber, absNumber, absNumber, absNumber, absNumber, absNumber); - var tsFmt = Regex.Replace(part.Format, @"([^a-zA-Z0-9]+)", "\\$1").ToLower(); - TimeSpan ts = TimeSpan.ParseExact(tmp.ToString(part.Format, CultureInfo.CurrentCulture), tsFmt, CultureInfo.CurrentCulture); - counter += num; - if (num > 0) - this.Value = this.Value.Add(ts); + if (part.Format.StartsWith("M")) + { + UpdatePartMonth(num); + } + else if (part.Format.StartsWith("y")) + { + UpdatePartYear(num); + } else - this.Value = this.Value.Subtract(ts); + { + UpdatePartGeneric(num, part); + } + this.Invalidate(); + } + } + /// + /// increment or decrement the month part of the DateTime + /// + /// + private void UpdatePartMonth(int num) + { + this.Value = this.Value.AddMonths(num); + } + /// + /// increment or decrement the year part of the DateTime + /// + /// + private void UpdatePartYear(int num) + { + this.Value = this.Value.AddYears(num); + } + /// + /// generically increment or decrement the selected part of the DateTime. Does not work for months and years + /// + /// + /// + private void UpdatePartGeneric(int num, DatePartInfo part) + { + //create a timespan with the given number of days, hours, minutes or seconds + int absNumber = Math.Abs(num); + DateTime tmp = new(absNumber, absNumber, absNumber, absNumber, absNumber, absNumber); + var dFmt = part.Format; + var tsFmt = Regex.Replace(dFmt, @"([^a-zA-Z0-9]+)", "\\$1").ToLower(); + //special handling for fullname of days as we want to get a numeric value to add or subtract + if (dFmt.StartsWith("d") && dFmt.Length > 3) + { + dFmt = "dd"; + tsFmt = "dd"; } + TimeSpan ts = TimeSpan.ParseExact(tmp.ToString(dFmt, CultureInfo.CurrentCulture), tsFmt, CultureInfo.CurrentCulture); + counter += num; + if (num > 0) + this.Value = this.Value.Add(ts); + else + this.Value = this.Value.Subtract(ts); } /// From 1753730a5d4757e15249465e465d538966e63d95 Mon Sep 17 00:00:00 2001 From: "13997737+wolframhaussig@users.noreply.github.com" <13997737+wolframhaussig@users.noreply.github.com> Date: Wed, 9 Aug 2023 21:49:07 +0200 Subject: [PATCH 4/5] fix text overlapping each other for longer names in dates --- .../Controls/StylableDateTimePicker.cs | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs index 5f1a736..807a04c 100644 --- a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs +++ b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs @@ -150,13 +150,14 @@ private void DrawDateTime(Graphics g) var highlightBrush = new SolidBrush(this.ForeColor.Highlight()); foreach (var part in _dateParts) { + Rectangle bounds = new Rectangle(part.Start, 2, part.End - part.Start, this.Height - 2); if (part.Selected) - g.DrawString(this.Value.ToString(part.Format), this.Font, highlightBrush, part.Start, 2); + TextRenderer.DrawText(g, this.Value.ToString(part.Format), Font, bounds, this.ForeColor.Highlight(), TextFormatFlags.EndEllipsis); else - g.DrawString(this.Value.ToString(part.Format), this.Font, brush, part.Start, 2); + TextRenderer.DrawText(g, this.Value.ToString(part.Format), Font, bounds, this.ForeColor, TextFormatFlags.EndEllipsis); } } - int counter = 0; + /// /// increment/decrement the selected part /// @@ -325,22 +326,7 @@ private void UpdatePart(int num = 1) this.Invalidate(); } } - /// - /// increment or decrement the month part of the DateTime - /// - /// - private void UpdatePartMonth(int num) - { - this.Value = this.Value.AddMonths(num); - } - /// - /// increment or decrement the year part of the DateTime - /// - /// - private void UpdatePartYear(int num) - { - this.Value = this.Value.AddYears(num); - } + /// /// generically increment or decrement the selected part of the DateTime. Does not work for months and years /// @@ -360,13 +346,30 @@ private void UpdatePartGeneric(int num, DatePartInfo part) tsFmt = "dd"; } TimeSpan ts = TimeSpan.ParseExact(tmp.ToString(dFmt, CultureInfo.CurrentCulture), tsFmt, CultureInfo.CurrentCulture); - counter += num; if (num > 0) this.Value = this.Value.Add(ts); else this.Value = this.Value.Subtract(ts); } + /// + /// increment or decrement the month part of the DateTime + /// + /// + private void UpdatePartMonth(int num) + { + this.Value = this.Value.AddMonths(num); + } + + /// + /// increment or decrement the year part of the DateTime + /// + /// + private void UpdatePartYear(int num) + { + this.Value = this.Value.AddYears(num); + } + /// /// contains the information about the DateTime part and its position /// From 4b3bd2d0687b2a6f19895387e05b63b41a2959ce Mon Sep 17 00:00:00 2001 From: "13997737+wolframhaussig@users.noreply.github.com" <13997737+wolframhaussig@users.noreply.github.com> Date: Thu, 10 Aug 2023 18:35:38 +0200 Subject: [PATCH 5/5] Support entering the numeric values for StylableDateTimePicker (e.g. not full day of week or month names) --- .../Controls/StylableDateTimePicker.cs | 137 +++++++++++++----- 1 file changed, 100 insertions(+), 37 deletions(-) diff --git a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs index 807a04c..a58e8b6 100644 --- a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs +++ b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs @@ -20,7 +20,7 @@ public class StylableDateTimePicker : DateTimePicker /// /// contains all DateTime parts and their positions /// - private readonly List _dateParts = new List(); + private readonly List _dateParts = new(); /// /// when the format is changed we need to recalculate the positions of the parts @@ -39,6 +39,7 @@ public StylableDateTimePicker() this.Leave += StylableDateTimePicker_Leave; this.MouseWheel += StylableDateTimePicker_MouseWheel; this.KeyDown += StylableDateTimePicker_KeyDown; + this.KeyPress += StylableDateTimePicker_KeyPress; } /// @@ -116,6 +117,31 @@ protected override void OnPaint(PaintEventArgs e) fb.Dispose(); } + /// + /// replace a part of the date with the manually entered input + /// + /// the old date + /// the full format + /// the part format + /// the new part value + /// + private static DateTime Replace(DateTime date, string format, string partFormat, string value) + { + //remove the day of week from the format as DateTime.ParseExact throws if date and day of week do not match + format = format.Replace("dddd", ""); + format = format.Replace("ddd", ""); + //remove the ful month name as this is not numeric and we do not support it currently + format = format.Replace("MMMM", ""); + format = format.Replace("MMM", ""); + //convert the old date to a string where the part to replace is XXXX + string partFormatRegex = @"\b" + Regex.Replace(partFormat, "[^a-zA-Z0-9]+", "") + @"\b"; + string formattedDate = date.ToString(Regex.Replace(format, partFormatRegex, @"\X\X\X\X")); + //now we can simply replace the XXXX with the new value and reparse the date + string replacedDateString = formattedDate.Replace("XXXX", value); + var replacedDate = DateTime.ParseExact(replacedDateString, format, CultureInfo.CurrentCulture); + return replacedDate; + } + /// /// this draw the datetime text manually so we can save the information where day, month and year is /// @@ -123,26 +149,7 @@ protected override void OnPaint(PaintEventArgs e) private void DrawDateTime(Graphics g) { // Drawing the datetime text - string format; - DateTimeFormatInfo dtfi = (new CultureInfo("hr-HR")).DateTimeFormat; - switch (Format) - { - case DateTimePickerFormat.Long: - format = dtfi.LongDatePattern; - break; - - case DateTimePickerFormat.Short: - format = dtfi.ShortDatePattern; - break; - - case DateTimePickerFormat.Time: - format = dtfi.LongTimePattern; - break; - - default: - format = CustomFormat; - break; - } + string format = GetFormat(); //recalculate the positions of the DateTime parts within the DateTimePicker RecalcPartPositions(format); //draw the DateTime parts @@ -150,7 +157,7 @@ private void DrawDateTime(Graphics g) var highlightBrush = new SolidBrush(this.ForeColor.Highlight()); foreach (var part in _dateParts) { - Rectangle bounds = new Rectangle(part.Start, 2, part.End - part.Start, this.Height - 2); + Rectangle bounds = new(part.Start, 2, part.End - part.Start, this.Height - 2); if (part.Selected) TextRenderer.DrawText(g, this.Value.ToString(part.Format), Font, bounds, this.ForeColor.Highlight(), TextFormatFlags.EndEllipsis); else @@ -158,6 +165,19 @@ private void DrawDateTime(Graphics g) } } + private string GetFormat() + { + DateTimeFormatInfo dtfi = (CultureInfo.CurrentCulture).DateTimeFormat; + string format = Format switch + { + DateTimePickerFormat.Long => dtfi.LongDatePattern, + DateTimePickerFormat.Short => dtfi.ShortDatePattern, + DateTimePickerFormat.Time => dtfi.LongTimePattern, + _ => CustomFormat, + }; + return format; + } + /// /// increment/decrement the selected part /// @@ -170,11 +190,28 @@ private void IncrementDecrement(Keys key) UpdatePart(-1); } + /// + /// move the selection to the next part + /// + /// + /// the newly selected part + private DatePartInfo NavigatePartRight(DatePartInfo part) + { + part.Reset(); + var index = _dateParts.IndexOf(part); + DatePartInfo newPart; + if (index == _dateParts.Count - 1) + newPart = _dateParts.First(); + else + newPart = _dateParts[index + 1]; + newPart.Selected = true; + return newPart; + } + /// /// navigate through the DateTime parts with the arrow keys /// /// - /// private void NavigateParts(Keys keyCode) { var part = _dateParts.FirstOrDefault(p => p.Selected); @@ -184,7 +221,7 @@ private void NavigateParts(Keys keyCode) } else if (part != null && keyCode == Keys.Left) { - part.Selected = false; + part.Reset(); var index = _dateParts.IndexOf(part); if (index == 0) _dateParts.Last().Selected = true; @@ -197,12 +234,7 @@ private void NavigateParts(Keys keyCode) } else if (part != null && keyCode == Keys.Right) { - part.Selected = false; - var index = _dateParts.IndexOf(part); - if (index == _dateParts.Count - 1) - _dateParts.First().Selected = true; - else - _dateParts[index + 1].Selected = true; + NavigatePartRight(part); } } @@ -247,7 +279,6 @@ private void RecalcPartPositions(string format) /// /// /// - /// private void StylableDateTimePicker_KeyDown(object? sender, KeyEventArgs e) { if (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down) @@ -262,15 +293,38 @@ private void StylableDateTimePicker_KeyDown(object? sender, KeyEventArgs e) e.Handled = true; } + /// + /// allow directly editing a value with the number keys + /// + /// + /// + private void StylableDateTimePicker_KeyPress(object? sender, KeyPressEventArgs e) + { + if (Char.IsNumber(e.KeyChar)) + { + var part = _dateParts.FirstOrDefault(p => p.Selected); + if (part != null) + { + part.NewValue += e.KeyChar; + if (part.NewValue.Length >= part.Format.Length) + { + this.Value = Replace(this.Value, GetFormat(), part.Format, part.NewValue); + part = NavigatePartRight(part); + } + this.Invalidate(); + } + } + e.Handled = true; + } + /// /// reset select part of the format when the control lost focus /// /// /// - /// private void StylableDateTimePicker_Leave(object? sender, EventArgs e) { - _dateParts.ForEach(p => p.Selected = false); + _dateParts.ForEach(p => p.Reset()); this.Invalidate(); } @@ -279,10 +333,9 @@ private void StylableDateTimePicker_Leave(object? sender, EventArgs e) /// /// /// - /// private void StylableDateTimePicker_MouseDown(object? sender, MouseEventArgs e) { - _dateParts.ForEach(p => p.Selected = false); + _dateParts.ForEach(p => p.Reset()); var part = _dateParts.FirstOrDefault(p => p.Start <= e.X && p.End >= e.X); if (part != null) { @@ -296,7 +349,6 @@ private void StylableDateTimePicker_MouseDown(object? sender, MouseEventArgs e) /// /// /// - /// private void StylableDateTimePicker_MouseWheel(object? sender, MouseEventArgs e) { UpdatePart(e.Delta / 100); @@ -383,7 +435,9 @@ private class DatePartInfo /// /// the format of the DateTime part /// - public string Format { get; set; } + public string Format { get; set; } = string.Empty; + + public string NewValue { get; set; } = string.Empty; /// /// if true, this part is selected and can be changed by the user @@ -394,5 +448,14 @@ private class DatePartInfo /// the start X position of the DateTime part /// public int Start { get; set; } + + /// + /// + /// + public void Reset() + { + NewValue = string.Empty; + Selected = false; + } } } \ No newline at end of file