Vanara/Windows.Forms/Controls/CustomDrawBase.cs

490 lines
20 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Windows.Forms;
using Vanara.Extensions;
using static Vanara.PInvoke.User32;
using ContentAlignment = System.Drawing.ContentAlignment;
namespace Vanara.Windows.Forms
{
/// <summary>State flags for controls derived from <see cref="CustomDrawBase"/>.</summary>
[Flags]
public enum ControlState
{
/// <summary>A mouse is hovering over the control.</summary>
Hot = 1 << 0,
/// <summary>The control has been pressed or clicked.</summary>
Pressed = 1 << 1,
/// <summary>The control is disabled.</summary>
Disabled = 1 << 2,
/// <summary>The control is in the process of animating.</summary>
Animating = 1 << 3,
/// <summary>The mouse button is down.</summary>
MouseDown = 1 << 4,
/// <summary>The mouse button is up.</summary>
InButtonUp = 1 << 5,
/// <summary>The control is defaulted (used primarily by buttons).</summary>
Defaulted = 1 << 6,
/// <summary>The control has the focus.</summary>
Focused = 1 << 7,
}
/// <summary>
/// Abstract class for implementing a custom-drawn control that tracks mouse movement and has text and/or an image. It exposes all
/// property changes.
/// </summary>
/// <seealso cref="System.Windows.Forms.Control"/>
/// <seealso cref="System.Windows.Forms.IButtonControl"/>
/// <seealso cref="System.ComponentModel.INotifyPropertyChanged"/>
public abstract class CustomDrawBase : Control, IButtonControl, INotifyPropertyChanged
{
private readonly ControlImage image;
private bool autoEllipsis;
private ContentAlignment imageAlign = ContentAlignment.MiddleCenter;
//private bool keyPressed;
private ControlState lastState;
private EnumFlagIndexer<ControlState> state;
private ContentAlignment textAlign = ContentAlignment.MiddleCenter;
private TextImageRelation textImageRelation = TextImageRelation.Overlay;
private ToolTip textToolTip;
private bool useMnemonic = true;
/// <summary>Initializes a new instance of the <see cref="CustomDrawBase"/> class.</summary>
protected CustomDrawBase()
{
image = new ControlImage(this);
SetStyle(ControlStyles.Selectable | ControlStyles.StandardClick | ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
}
/// <summary>Occurs when the control is double-clicked.</summary>
[Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)]
public new event EventHandler DoubleClick
{
add { base.DoubleClick += value; }
remove { base.DoubleClick -= value; }
}
/// <summary>Occurs when the control is double clicked by the mouse.</summary>
[Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)]
public new event MouseEventHandler MouseDoubleClick
{
add { base.MouseDoubleClick += value; }
remove { base.MouseDoubleClick -= value; }
}
/// <summary>Occurs when a property value changes.</summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Gets or sets a value indicating whether the ellipsis character (...) appears at the right edge of the control, denoting that the
/// control text extends beyond the specified length of the control.
/// </summary>
/// <value>
/// <c>true</c> if the additional label text is to be indicated by an ellipsis; otherwise, <c>false</c>. The default is <c>true</c>.
/// </value>
[Category("Behavior"), DefaultValue(true), Browsable(true), EditorBrowsable(EditorBrowsableState.Always), Description("")]
public bool AutoEllipsis
{
get => autoEllipsis; set => SetField(ref autoEllipsis, value, nameof(AutoEllipsis), true, b => { if (b && textToolTip == null) textToolTip = new ToolTip(); });
}
/// <summary>Gets or sets the value returned to the parent form when the button is clicked.</summary>
[Category("Behavior"), DefaultValue(typeof(DialogResult), "None")]
[Description("The dialog result produced in a modal form by clicking the button.")]
public virtual DialogResult DialogResult { get; set; }
/// <summary>Gets or sets the image that is displayed on a button control.</summary>
/// <value>The Image displayed on the button control. The default value is <c>null</c>.</value>
[Description(""), Localizable(true), Category("Appearance"), DefaultValue(null)]
public Image Image
{
get => image.Image; set { if (image.Image != value) image.Image = value; else OnPropertyChanged(nameof(Image)); }
}
/// <summary>Gets or sets the alignment of the image on the button control.</summary>
/// <value>One of the <see cref="ContentAlignment"/> values. The default value is <c>MiddleCenter</c>.</value>
[Category("Appearance"), DefaultValue((int)ContentAlignment.MiddleCenter)]
[Description("The alignment of the image that will be displayed in the face of the control.")]
public virtual ContentAlignment ImageAlign
{
get => imageAlign; set => SetField(ref imageAlign, value, nameof(imageAlign));
}
/// <summary>Gets or sets the image list index value of the image displayed on the button control.</summary>
/// <value>A zero-based index, which represents the image position in an <see cref="ImageList"/>. The default is -1.</value>
[Description(""), Localizable(true), Category("Appearance"), DefaultValue(-1), RefreshProperties(RefreshProperties.Repaint)]
[TypeConverter(typeof(ImageIndexConverter)), Editor("System.Windows.Forms.Design.ImageIndexEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public int ImageIndex
{
get => image.ImageIndex; set { if (image.ImageIndex != value) image.ImageIndex = value; else OnPropertyChanged(nameof(ImageIndex)); }
}
/// <summary>Gets or sets the key accessor for the image in the <see cref="ImageList"/>.</summary>
/// <value>A string representing the key of the image.</value>
[Description(""), Localizable(true), Category("Appearance"), DefaultValue(""), RefreshProperties(RefreshProperties.Repaint)]
[TypeConverter(typeof(ImageKeyConverter)), Editor("System.Windows.Forms.Design.ImageIndexEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))]
public string ImageKey
{
get => image.ImageKey; set { if (image.ImageKey != value) image.ImageKey = value; else OnPropertyChanged(nameof(ImageKey)); }
}
/// <summary>Gets or sets the <see cref="ImageList"/> that contains the <see cref="Image"/> displayed on a button control.</summary>
/// <value>An <see cref="ImageList"/>. The default value is <c>null</c>.</value>
[Description(""), Category("Appearance"), DefaultValue(null), RefreshProperties(RefreshProperties.Repaint)]
public ImageList ImageList
{
get => image.ImageList; set { if (image.ImageList != value) image.ImageList = value; else OnPropertyChanged(nameof(ImageList)); }
}
/// <summary>Gets or sets the alignment of the text on the button control.</summary>
/// <value>One of the <see cref="ContentAlignment"/> values. The default value is <c>MiddleCenter</c>.</value>
[Category("Appearance"), DefaultValue((int)ContentAlignment.MiddleCenter)]
[Description("The alignment of the text that will be displayed in the face of the control.")]
public virtual ContentAlignment TextAlign
{
get => textAlign; set => SetField(ref textAlign, value, nameof(TextAlign));
}
/// <summary>Gets or sets the position of text and image relative to each other.</summary>
/// <value>One of the values of <see cref="TextImageRelation"/>. The default is <c>Overlay</c>.</value>
[DefaultValue(0), Localizable(true), Description(""), Category("Appearance")]
public TextImageRelation TextImageRelation
{
get => textImageRelation; set => SetField(ref textImageRelation, value, nameof(TextImageRelation));
}
/// <summary>
/// Gets or sets a value indicating whether the first character that is preceded by an ampersand (&amp;) is used as the mnemonic key
/// of the control.
/// </summary>
/// <value>
/// <c>true</c> if the first character that is preceded by an ampersand (&amp;) is used as the mnemonic key of the control;
/// otherwise, <c>false</c>. The default is <c>true</c>.
/// </value>
[DefaultValue(true), Description(""), Category("Appearance")]
public bool UseMnemonic
{
get => useMnemonic; set => SetField(ref useMnemonic, value, nameof(UseMnemonic));
}
/// <summary>Gets or sets a value indicating whether this <see cref="CustomDrawBase"/> is animating.</summary>
/// <value><see langword="true"/> if animating; otherwise, <see langword="false"/>.</value>
[Browsable(false)]
protected virtual bool Animating
{
get => state[ControlState.Animating]; set => SetState(ControlState.Animating, value, false);
}
/// <summary>Gets the default size of the control.</summary>
protected override Size DefaultSize => new Size(75, 23);
/// <summary>Gets or sets a value indicating whether the button control is the default button.</summary>
/// <value><c>true</c> if the button control is the default button; otherwise, <c>false</c>.</value>
[Browsable(false)]
protected virtual bool IsDefault
{
get => state[ControlState.Defaulted]; set => SetState(ControlState.Defaulted, value);
}
/// <summary>Gets the last state of the control.</summary>
/// <value>The last state.</value>
protected virtual ControlState LastState => lastState;
/// <summary>Gets the current state of the control.</summary>
/// <value>The state.</value>
protected virtual ControlState State => state;
private bool ShowToolTip => !DesignMode && AutoEllipsis && textToolTip != null;
/// <summary>Notifies a control that it is the default button so that its appearance and behavior is adjusted accordingly.</summary>
/// <param name="value">true if the control should behave as a default button; otherwise false.</param>
public virtual void NotifyDefault(bool value)
{
if (IsDefault == value) return;
IsDefault = value;
Invalidate();
}
/// <summary>Generates a <see cref="E:System.Windows.Forms.Control.Click"/> event for the control.</summary>
public void PerformClick()
{
if (CanSelect)
OnClick(EventArgs.Empty);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.EnabledChanged"/> event.</summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
protected override void OnEnabledChanged(EventArgs e)
{
base.OnEnabledChanged(e);
state[ControlState.Disabled] = !Enabled;
if (Enabled) return;
SetState(ControlState.MouseDown | ControlState.Pressed | ControlState.Hot, false);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.GotFocus"/> event.</summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
protected override void OnGotFocus(EventArgs e)
{
System.Diagnostics.Debug.WriteLine($"GotFocus[{Name}]");
base.OnGotFocus(e);
SetState(ControlState.Focused, true);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.KeyDown"/> event.</summary>
/// <param name="e">A <see cref="T:System.Windows.Forms.KeyEventArgs"/> that contains the event data.</param>
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.KeyData == Keys.Space)
{
SetState(ControlState.MouseDown, true);
e.Handled = true;
}
base.OnKeyDown(e);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.KeyUp"/> event.</summary>
/// <param name="e">A <see cref="T:System.Windows.Forms.KeyEventArgs"/> that contains the event data.</param>
protected override void OnKeyUp(KeyEventArgs e)
{
if (state[ControlState.MouseDown])
{
if (SetState(ControlState.MouseDown | ControlState.Pressed, false, false))
Refresh();
if (e.KeyCode == Keys.Space || e.KeyCode == Keys.Enter)
OnClick(EventArgs.Empty);
e.Handled = true;
}
base.OnKeyUp(e);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.LostFocus"/> event.</summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
protected override void OnLostFocus(EventArgs e)
{
System.Diagnostics.Debug.WriteLine($"LostFocus[{Name}]");
base.OnLostFocus(e);
Capture = false;
SetState(ControlState.MouseDown | ControlState.Pressed | ControlState.Focused, false);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.MouseDown"/> event.</summary>
/// <param name="e">A <see cref="T:System.Windows.Forms.MouseEventArgs"/> that contains the event data.</param>
protected override void OnMouseDown(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
SetState(ControlState.MouseDown | ControlState.Pressed, true);
base.OnMouseDown(e);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.MouseEnter"/> event.</summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
protected override void OnMouseEnter(EventArgs e)
{
SetState(ControlState.Hot, true);
if (ShowToolTip)
try { textToolTip.Show(Text.RemoveMnemonic(), this); } catch { }
base.OnMouseEnter(e);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.MouseLeave"/> event.</summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
protected override void OnMouseLeave(EventArgs e)
{
System.Diagnostics.Debug.WriteLine($"OnMouseLeave[{Name}]");
SetState(ControlState.Hot, false);
textToolTip?.Hide(this);
base.OnMouseLeave(e);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.MouseMove"/> event.</summary>
/// <param name="e">A <see cref="T:System.Windows.Forms.MouseEventArgs"/> that contains the event data.</param>
protected override void OnMouseMove(MouseEventArgs e)
{
if (e.Button != MouseButtons.None && state[ControlState.Pressed])
{
if (!ClientRectangle.Contains(e.X, e.Y))
{
if (state[ControlState.MouseDown])
SetState(ControlState.MouseDown | ControlState.Pressed, false);
}
else if (!state[ControlState.MouseDown])
{
SetState(ControlState.MouseDown, true);
}
}
base.OnMouseMove(e);
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.MouseUp"/> event.</summary>
/// <param name="e">A <see cref="T:System.Windows.Forms.MouseEventArgs"/> that contains the event data.</param>
protected override void OnMouseUp(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left && state[ControlState.Pressed])
{
var mouseWasDown = state[ControlState.MouseDown];
if (SetState(ControlState.MouseDown | ControlState.Pressed, false, false))
Refresh();
if (mouseWasDown && WindowFromPoint(PointToScreen(e.Location)) == Handle)
{
OnClick(e);
OnMouseClick(e);
}
}
base.OnMouseUp(e);
}
/// <summary>
/// Raises the <see cref="E:System.Windows.Forms.Control.BackColorChanged"/> event when the <see
/// cref="P:System.Windows.Forms.Control.BackColor"/> property value of the control's container changes.
/// </summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
protected override void OnParentBackColorChanged(EventArgs e)
{
base.OnParentBackColorChanged(e);
Invalidate();
}
/// <summary>
/// Raises the <see cref="E:System.Windows.Forms.Control.BackgroundImageChanged"/> event when the <see
/// cref="P:System.Windows.Forms.Control.BackgroundImage"/> property value of the control's container changes.
/// </summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
protected override void OnParentBackgroundImageChanged(EventArgs e)
{
base.OnParentBackgroundImageChanged(e);
Invalidate();
}
/// <summary>Raises the <see cref="PropertyChanged"/> event.</summary>
/// <param name="propertyName">Name of the property that has changed.</param>
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
/// <summary>Raises the <see cref="E:System.Windows.Forms.Control.TextChanged"/> event.</summary>
/// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
Invalidate();
}
/// <summary>Processes a mnemonic character.</summary>
/// <param name="charCode">The character to process.</param>
/// <returns>true if the character was processed as a mnemonic by the control; otherwise, false.</returns>
protected override bool ProcessMnemonic(char charCode)
{
if (!IsMnemonic(charCode, Text))
return base.ProcessMnemonic(charCode);
OnClick(EventArgs.Empty);
return true;
}
/// <summary>
/// Sets a field value to the new value. If the value has changed, the <see cref="PropertyChanged"/> event is raised and the control
/// will optionally be invalidated.
/// </summary>
/// <typeparam name="T">The type of the field.</typeparam>
/// <param name="field">A reference to the field.</param>
/// <param name="value">The new value.</param>
/// <param name="propertyName">The name of the property.</param>
/// <param name="invalidateOnSet">if set to <c>true</c> the control is invalidated if this is a changed value.</param>
/// <param name="validate">An optional action function that is called when it is determined that this is a changed value.</param>
/// <returns><c>true</c> if the value has been changed; otherwise <c>false</c>.</returns>
protected virtual bool SetField<T>(ref T field, T value, string propertyName, bool invalidateOnSet = true, Action<T> validate = null) where T : struct
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
validate?.Invoke(value);
if (typeof(T).IsEnum && !Enum.IsDefined(typeof(T), value))
throw new InvalidEnumArgumentException(propertyName, Convert.ToInt32(value), typeof(T));
field = value;
OnPropertyChanged(propertyName);
if (invalidateOnSet && IsHandleCreated)
Invalidate();
return true;
}
/// <summary>Sets the state of the control.</summary>
/// <param name="stateVal">The state value.</param>
/// <param name="value">
/// if set to <see langword="true"/> sets the flag in <paramref name="stateVal"/> on; otherwise it removes the state.
/// </param>
/// <param name="invalidateOnSet">if set to <see langword="true"/>, invalidate the control once set.</param>
/// <returns></returns>
protected virtual bool SetState(ControlState stateVal, bool value, bool invalidateOnSet = true)
{
if (state[stateVal] == value) return false;
lastState = state;
state[stateVal] = value;
OnPropertyChanged(nameof(State));
if (invalidateOnSet && IsHandleCreated)
Invalidate();
return true;
}
/// <summary>Processes Windows messages.</summary>
/// <param name="m">The Windows <see cref="T:System.Windows.Forms.Message"/> to process.</param>
protected override void WndProc(ref Message m)
{
switch (m.Msg)
{
case (int)WindowMessage.WM_ERASEBKGND:
DefWndProc(ref m);
return;
/*case 0x2111:
if (HIWORD(m.WParam) == 0)
{
OnClick(EventArgs.Empty);
return;
}
break;*/
case (int)ButtonMessage.BM_CLICK:
PerformClick();
return;
case (int)ButtonMessage.BM_SETSTATE:
// Ignore BM_SETSTATE -- Windows gets confused and paints things, even though we are ownerdraw.
return;
case (int)WindowMessage.WM_KILLFOCUS:
case (int)WindowMessage.WM_CANCELMODE:
case (int)WindowMessage.WM_CAPTURECHANGED:
if (!state[ControlState.InButtonUp] && state[ControlState.Pressed])
SetState(ControlState.MouseDown | ControlState.Pressed, false);
break;
case (int)WindowMessage.WM_LBUTTONUP:
case (int)WindowMessage.WM_MBUTTONUP:
case (int)WindowMessage.WM_RBUTTONUP:
try
{
state[ControlState.InButtonUp] = true;
base.WndProc(ref m);
return;
}
finally
{
state[ControlState.InButtonUp] = false;
}
}
base.WndProc(ref m);
}
}
}