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 { /// 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) image.Image = value; else 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) image.ImageIndex = value; else 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) image.ImageKey = value; else 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) image.ImageList = value; else 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 Size(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 == Keys.Space || e.KeyCode == 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) 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); } } }