using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Design;
using System.Windows.Forms;
using static Vanara.PInvoke.User32;
using ContentAlignment = System.Drawing.ContentAlignment;
namespace Vanara.Windows.Forms;
/// State flags for controls derived from .
[Flags]
public enum ControlState
{
/// A mouse is hovering over the control.
Hot = 1 << 0,
/// The control has been pressed or clicked.
Pressed = 1 << 1,
/// The control is disabled.
Disabled = 1 << 2,
/// The control is in the process of animating.
Animating = 1 << 3,
/// The mouse button is down.
MouseDown = 1 << 4,
/// The mouse button is up.
InButtonUp = 1 << 5,
/// The control is defaulted (used primarily by buttons).
Defaulted = 1 << 6,
/// The control has the focus.
Focused = 1 << 7,
}
///
/// Abstract class for implementing a custom-drawn control that tracks mouse movement and has text and/or an image. It exposes all
/// property changes.
///
///
///
///
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 state;
private ContentAlignment textAlign = ContentAlignment.MiddleCenter;
private TextImageRelation textImageRelation = TextImageRelation.Overlay;
private ToolTip? textToolTip;
private bool useMnemonic = true;
/// Initializes a new instance of the class.
protected CustomDrawBase()
{
image = new ControlImage(this);
SetStyle(ControlStyles.Selectable | ControlStyles.StandardClick | ControlStyles.ResizeRedraw, true);
SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint, true);
}
/// Occurs when the control is double-clicked.
[Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)]
public new event EventHandler? DoubleClick
{
add => base.DoubleClick += value;
remove => base.DoubleClick -= value;
}
/// Occurs when the control is double clicked by the mouse.
[Browsable(false), EditorBrowsable(EditorBrowsableState.Advanced)]
public new event MouseEventHandler? MouseDoubleClick
{
add => base.MouseDoubleClick += value;
remove => base.MouseDoubleClick -= value;
}
/// Occurs when a property value changes.
public event PropertyChangedEventHandler? PropertyChanged;
///
/// 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.
///
///
/// true if the additional label text is to be indicated by an ellipsis; otherwise, false. The default is true.
///
[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(); });
}
/// Gets or sets the value returned to the parent form when the button is clicked.
[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; }
/// Gets or sets the image that is displayed on a button control.
/// The Image displayed on the button control. The default value is null.
[Description(""), Localizable(true), Category("Appearance"), DefaultValue(null)]
public Image? Image
{
get => image.Image;
set
{
if (image.Image == value) return;
image.Image = value;
OnPropertyChanged(nameof(Image));
}
}
/// Gets or sets the alignment of the image on the button control.
/// One of the values. The default value is MiddleCenter.
[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));
}
/// Gets or sets the image list index value of the image displayed on the button control.
/// A zero-based index, which represents the image position in an . The default is -1.
[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) return;
image.ImageIndex = value;
OnPropertyChanged(nameof(ImageIndex));
}
}
/// Gets or sets the key accessor for the image in the .
/// A string representing the key of the image.
[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) return;
image.ImageKey = value;
OnPropertyChanged(nameof(ImageKey));
}
}
/// Gets or sets the that contains the displayed on a button control.
/// An . The default value is null.
[Description(""), Category("Appearance"), DefaultValue(null), RefreshProperties(RefreshProperties.Repaint)]
public ImageList? ImageList
{
get => image.ImageList;
set
{
if (image.ImageList == value) return;
image.ImageList = value;
OnPropertyChanged(nameof(ImageList));
}
}
/// Gets or sets the alignment of the text on the button control.
/// One of the values. The default value is MiddleCenter.
[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));
}
/// Gets or sets the position of text and image relative to each other.
/// One of the values of . The default is Overlay.
[DefaultValue(0), Localizable(true), Description(""), Category("Appearance")]
public TextImageRelation TextImageRelation
{
get => textImageRelation;
set => SetField(ref textImageRelation, value, nameof(TextImageRelation));
}
///
/// Gets or sets a value indicating whether the first character that is preceded by an ampersand (&) is used as the mnemonic key
/// of the control.
///
///
/// true if the first character that is preceded by an ampersand (&) is used as the mnemonic key of the control;
/// otherwise, false. The default is true.
///
[DefaultValue(true), Description(""), Category("Appearance")]
public bool UseMnemonic
{
get => useMnemonic;
set => SetField(ref useMnemonic, value, nameof(UseMnemonic));
}
/// Gets or sets a value indicating whether this is animating.
/// if animating; otherwise, .
[Browsable(false)]
protected virtual bool Animating
{
get => state[ControlState.Animating];
set => SetState(ControlState.Animating, value, false);
}
/// Gets the default size of the control.
protected override Size DefaultSize => new(75, 23);
/// Gets or sets a value indicating whether the button control is the default button.
/// true if the button control is the default button; otherwise, false.
[Browsable(false)]
protected virtual bool IsDefault
{
get => state[ControlState.Defaulted];
set => SetState(ControlState.Defaulted, value);
}
/// Gets the last state of the control.
/// The last state.
protected virtual ControlState LastState => lastState;
/// Gets the current state of the control.
/// The state.
protected virtual ControlState State => state;
private bool ShowToolTip => !DesignMode && AutoEllipsis && textToolTip != null;
/// Notifies a control that it is the default button so that its appearance and behavior is adjusted accordingly.
/// true if the control should behave as a default button; otherwise false.
public virtual void NotifyDefault(bool value)
{
if (IsDefault == value) return;
IsDefault = value;
Invalidate();
}
/// Generates a event for the control.
public void PerformClick()
{
if (CanSelect)
OnClick(EventArgs.Empty);
}
/// Raises the event.
/// An that contains the event data.
protected override void OnEnabledChanged(EventArgs e)
{
base.OnEnabledChanged(e);
state[ControlState.Disabled] = !Enabled;
if (Enabled) return;
SetState(ControlState.MouseDown | ControlState.Pressed | ControlState.Hot, false);
}
/// Raises the event.
/// An that contains the event data.
protected override void OnGotFocus(EventArgs e)
{
System.Diagnostics.Debug.WriteLine($"GotFocus[{Name}]");
base.OnGotFocus(e);
SetState(ControlState.Focused, true);
}
/// Raises the event.
/// A that contains the event data.
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.KeyData == Keys.Space)
{
SetState(ControlState.MouseDown, true);
e.Handled = true;
}
base.OnKeyDown(e);
}
/// Raises the event.
/// A that contains the event data.
protected override void OnKeyUp(KeyEventArgs e)
{
if (state[ControlState.MouseDown])
{
if (SetState(ControlState.MouseDown | ControlState.Pressed, false, false))
Refresh();
if (e.KeyCode is Keys.Space or Keys.Enter)
OnClick(EventArgs.Empty);
e.Handled = true;
}
base.OnKeyUp(e);
}
/// Raises the event.
/// An that contains the event data.
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);
}
/// Raises the event.
/// A that contains the event data.
protected override void OnMouseDown(MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
SetState(ControlState.MouseDown | ControlState.Pressed, true);
base.OnMouseDown(e);
}
/// Raises the event.
/// An that contains the event data.
protected override void OnMouseEnter(EventArgs e)
{
SetState(ControlState.Hot, true);
if (ShowToolTip && textToolTip is not null)
try { textToolTip.Show(Text.RemoveMnemonic(), this); } catch { }
base.OnMouseEnter(e);
}
/// Raises the event.
/// An that contains the event data.
protected override void OnMouseLeave(EventArgs e)
{
System.Diagnostics.Debug.WriteLine($"OnMouseLeave[{Name}]");
SetState(ControlState.Hot, false);
textToolTip?.Hide(this);
base.OnMouseLeave(e);
}
/// Raises the event.
/// A that contains the event data.
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);
}
/// Raises the event.
/// A that contains the event data.
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);
}
///
/// Raises the event when the property value of the control's container changes.
///
/// An that contains the event data.
protected override void OnParentBackColorChanged(EventArgs e)
{
base.OnParentBackColorChanged(e);
Invalidate();
}
///
/// Raises the event when the property value of the control's container changes.
///
/// An that contains the event data.
protected override void OnParentBackgroundImageChanged(EventArgs e)
{
base.OnParentBackgroundImageChanged(e);
Invalidate();
}
/// Raises the event.
/// Name of the property that has changed.
protected virtual void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
/// Raises the event.
/// An that contains the event data.
protected override void OnTextChanged(EventArgs e)
{
base.OnTextChanged(e);
Invalidate();
}
/// Processes a mnemonic character.
/// The character to process.
/// true if the character was processed as a mnemonic by the control; otherwise, false.
protected override bool ProcessMnemonic(char charCode)
{
if (!IsMnemonic(charCode, Text))
return base.ProcessMnemonic(charCode);
OnClick(EventArgs.Empty);
return true;
}
///
/// Sets a field value to the new value. If the value has changed, the event is raised and the control
/// will optionally be invalidated.
///
/// The type of the field.
/// A reference to the field.
/// The new value.
/// The name of the property.
/// if set to true the control is invalidated if this is a changed value.
/// An optional action function that is called when it is determined that this is a changed value.
/// true if the value has been changed; otherwise false.
protected virtual bool SetField(ref T field, T value, string propertyName, bool invalidateOnSet = true, Action? validate = null) where T : struct
{
if (EqualityComparer.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;
}
/// Sets the state of the control.
/// The state value.
///
/// if set to sets the flag in on; otherwise it removes the state.
///
/// if set to , invalidate the control once set.
///
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;
}
/// Processes Windows messages.
/// The Windows to process.
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);
}
}