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/Controls/StylableDateTimePicker.cs b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs index 28d29fc..a58e8b6 100644 --- a/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs +++ b/StylableWinFormsControls/StylableWinFormsControls/Controls/StylableDateTimePicker.cs @@ -1,5 +1,8 @@ +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,77 +12,450 @@ 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(); + + /// + /// 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() { - SetStyle(ControlStyles.UserPaint, true); + this.SetStyle(ControlStyles.UserPaint, true); + this.MouseDown += StylableDateTimePicker_MouseDown; + this.Leave += StylableDateTimePicker_Leave; + this.MouseWheel += StylableDateTimePicker_MouseWheel; + this.KeyDown += StylableDateTimePicker_KeyDown; + this.KeyPress += StylableDateTimePicker_KeyPress; } - + + /// + /// Gets or sets the background color of the control + /// + [Browsable(true)] + public override Color BackColor + { + get + { + return base.BackColor; + } + set + { + base.BackColor = value; + } + } + + 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 + handleParam.ExStyle |= 0x02000000; // WS_EX_COMPOSITED return handleParam; } - } - - /// - /// Gets or sets the background color of the control - /// - [Browsable(true)] - public override Color BackColor - { - get => base.BackColor; - set => base.BackColor = value; } protected override void OnPaint(PaintEventArgs e) { - Graphics g = CreateGraphics(); + Graphics g = this.CreateGraphics(); g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; // Dropdownbutton rectangle - Rectangle ddbRect = new(ClientRectangle.Width - 17, 0, 17, ClientRectangle.Height); + Rectangle ddb_rect = new Rectangle(ClientRectangle.Width - 17, 0, 17, ClientRectangle.Height); // Background brush Brush bb; //foreground brush Brush fb; - ComboBoxState visualState; + 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 (Enabled) + if (this.Enabled) { bb = new SolidBrush(EnabledBackColor); fb = new SolidBrush(EnabledForeColor); - visualState = ComboBoxState.Normal; + visual_state = ComboBoxState.Normal; } else { bb = new SolidBrush(DisabledBackColor); fb = new SolidBrush(DisabledForeColor); - visualState = ComboBoxState.Disabled; + visual_state = ComboBoxState.Disabled; } // Filling the background g.FillRectangle(bb, 0, 0, ClientRectangle.Width, ClientRectangle.Height); - // Drawing the datetime text - g.DrawString(Text, Font, fb, 5, 2); + // Drawing the datetime + DrawDateTime(g); // Drawing the dropdownbutton using ComboBoxRenderer - ComboBoxRenderer.DrawDropDownButton(g, ddbRect, visualState); + ComboBoxRenderer.DrawDropDownButton(g, ddb_rect, visual_state); g.Dispose(); bb.Dispose(); 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 + /// + /// + private void DrawDateTime(Graphics g) + { + // Drawing the datetime text + string format = GetFormat(); + //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) + { + 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 + TextRenderer.DrawText(g, this.Value.ToString(part.Format), Font, bounds, this.ForeColor, TextFormatFlags.EndEllipsis); + } + } + + 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 + /// + /// + private void IncrementDecrement(Keys key) + { + if (key == Keys.Up) + UpdatePart(1); + else if (key == Keys.Down) + 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); + if (part == null && keyCode == Keys.Left) + { + _dateParts.Last().Selected = true; + } + else if (part != null && keyCode == Keys.Left) + { + part.Reset(); + 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) + { + NavigatePartRight(part); + } + } + + /// + /// 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); + this.Invalidate(); + } + 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.Reset()); + 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.Reset()); + 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) + { + if (part.Format.StartsWith("M")) + { + UpdatePartMonth(num); + } + else if (part.Format.StartsWith("y")) + { + UpdatePartYear(num); + } + else + { + UpdatePartGeneric(num, part); + } + this.Invalidate(); + } + } + + /// + /// 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); + 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 + /// + 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; } = string.Empty; + + public string NewValue { get; set; } = string.Empty; + + /// + /// 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; } + + /// + /// + /// + public void Reset() + { + NewValue = string.Empty; + Selected = false; + } + } +} \ 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(); + } + } + } +}