using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Windows.Forms; using System.Windows.Forms.VisualStyles; using Vanara.Extensions; using static Vanara.PInvoke.Gdi32; using static Vanara.PInvoke.UxTheme; using static Vanara.PInvoke.WinBase; namespace Vanara.Drawing { /// Represents the types of trigger which can change the visual state of a control. public enum VisualStateTriggerTypes { /// The control receives input focus. Focused, /// The mouse is over the control. Hot, /// The left mouse button is pressed on the control. Pushed, /// The control is disabled. Disabled } /// /// Attaches to a System.Windows.Forms.Control and provides buffered painting functionality. /// Uses TState to represent the visual state of the control. Animations are attached to transitions between states. /// /// Any type representing the visual state of the control. public class BufferedPainter : IDisposable { private bool animationsNeedCleanup; private TState currentState; private TState defaultState; private TState newState; private Size oldSize; private bool focused, disabled, hot, pushed; /// Initializes a new instance of the BufferedPainter class. /// /// Control this instance is attached to. /// For best results, use a control which does not paint its background. /// Note: Buffered painting does not work if the OptimizedDoubleBuffer newDS is set for the control. /// public BufferedPainter(Control control, TState defaultState, TState hotState, TState pressedState, TState disabledState) { defaultState = currentState = newState = default(TState); Control = control; BufferedPaintSupported = Environment.OSVersion.Version.Major >= 6 && VisualStyleRenderer.IsSupported && Application.RenderWithVisualStyles && !Control.IsDesignMode(); oldSize = Control.Size; Control.Resize += Control_Resize; Control.Disposed += Control_Disposed; Control.Paint += Control_Paint; Control.HandleCreated += Control_HandleCreated; Control.EnabledChanged += EvalTriggers; Control.MouseEnter += EvalTriggers; Control.MouseLeave += EvalTriggers; Control.MouseMove += EvalTriggers; Control.GotFocus += EvalTriggers; Control.LostFocus += EvalTriggers; Control.MouseDown += EvalTriggers; Control.MouseUp += EvalTriggers; } /// Fired when the control must be painted in a particular state. public event EventHandler> PaintVisualState; /// Gets whether buffered painting is supported for the current OS/configuration. public bool BufferedPaintSupported { get; } /// Gets the control this instance is attached to. public Control Control { get; } /// Gets or sets the default animation duration (in milliseconds) for state transitions. The default is zero (not animated). public int DefaultDuration { get; set; } /// Gets or sets the default visual state. The default value is 'default(TState)'. public TState DefaultState { get { return defaultState; } set { var usingOldDefault = Equals(currentState, defaultState); defaultState = value; if (usingOldDefault) currentState = newState = defaultState; } } /// Gets or sets the current visual state. public TState State { get { return currentState; } set { var diff = !Equals(currentState, value); newState = value; if (diff) { if (animationsNeedCleanup && Control.IsHandleCreated) BufferedPaintStopAllAnimations(new HandleRef(Control, Control.Handle)); Control.Invalidate(); } } } /// Gets the collection of state transitions and their animation durations. Only one item for each unique state transition is permitted. public ICollection> Transitions { get; } = new HashSet>(); /// Short-hand method for adding a state transition. /// The previous visual state. /// The new visual state. /// Duration of the animation (in milliseconds). public void AddTransition(TState fromState, TState toState, int duration) { Transitions.Add(new BufferedPaintTransition(fromState, toState, duration)); } /// /// Adds the transition matrix. /// /// The matrix. public void AddTransitionMatrix(int[,] matrix) { var eVals = Enum.GetValues(typeof(TState)); var eValCnt = eVals.Length; if (matrix.Length != eValCnt * eValCnt) throw new ArgumentOutOfRangeException(nameof(matrix), $"The array for {nameof(matrix)} must be [{eValCnt},{eValCnt}]."); for (var i = 0; i < eValCnt; i++) for (var j = 0; j < eValCnt; j++) AddTransition((TState)eVals.GetValue(i), (TState)eVals.GetValue(j), matrix[i, j]); } public void Dispose() { if (Control == null) return; Control.Resize -= Control_Resize; Control.Disposed -= Control_Disposed; Control.Paint -= Control_Paint; Control.HandleCreated -= Control_HandleCreated; Control.EnabledChanged -= EvalTriggers; Control.MouseEnter -= EvalTriggers; Control.MouseLeave -= EvalTriggers; Control.MouseMove -= EvalTriggers; Control.GotFocus -= EvalTriggers; Control.LostFocus -= EvalTriggers; Control.MouseDown -= EvalTriggers; Control.MouseUp -= EvalTriggers; } /// Raises the PaintVisualState event. /// BufferedPaintEventArgs instance. protected virtual void OnPaintVisualState(BufferedPaintEventArgs e) { PaintVisualState?.Invoke(this, e); } /// Helper method for EvalTriggers(). /// Type of trigger to search for. /// Reference to the visual state variable to update (if the trigger occurs). private void ApplyCondition(VisualStateTriggerTypes type, ref TState stateIfTrue) { foreach (var trigger in Triggers.Where(x => x.Type == type)) { var bounds = trigger.Bounds != Rectangle.Empty ? trigger.Bounds : Control.ClientRectangle; var inRect = bounds.Contains(Control.PointToClient(Cursor.Position)); var other = true; switch (type) { case VisualStateTriggerTypes.Disabled: other = !Control.Enabled; inRect = true; break; case VisualStateTriggerTypes.Focused: other = Control.Focused; inRect = true; break; case VisualStateTriggerTypes.Pushed: other = (Control.MouseButtons & MouseButtons.Left) == MouseButtons.Left; break; } if (other && inRect) stateIfTrue = trigger.State; } } /// Deactivates buffered painting. private void CleanupAnimations() { if (Control.InvokeRequired) { Control.Invoke(new MethodInvoker(CleanupAnimations)); } else if (animationsNeedCleanup) { if (Control.IsHandleCreated) BufferedPaintStopAllAnimations(new HandleRef(Control, Control.Handle)); BufferedPaintUnInit(); animationsNeedCleanup = false; } } private void Control_Disposed(object sender, EventArgs e) { if (animationsNeedCleanup) { BufferedPaintUnInit(); animationsNeedCleanup = false; } } private void Control_HandleCreated(object sender, EventArgs e) { if (BufferedPaintSupported) { BufferedPaintInit(); animationsNeedCleanup = true; } } private void Control_Paint(object sender, PaintEventArgs e) { if (BufferedPaintSupported) { var stateChanged = !Equals(currentState, newState); using (var hdc = new SafeDCHandle(e.Graphics)) { if (!hdc.IsInvalid) { // see if this paint was generated by a soft-fade animation if (!BufferedPaintRenderAnimation(new HandleRef(Control, Control.Handle), hdc)) { var animParams = new BP_ANIMATIONPARAMS(BP_ANIMATIONSTYLE.BPAS_LINEAR); // get appropriate animation time depending on state transition (or 0 if unchanged) if (stateChanged) { var transition = Transitions.SingleOrDefault(x => Equals(x.FromState, currentState) && Equals(x.ToState, newState)); animParams.Duration = transition?.Duration ?? DefaultDuration; } using (var h = new BufferedPaintHandle(Control, hdc, Control.ClientRectangle, animParams, BP_PAINTPARAMS.NoClip)) { if (!h.IsInvalid) { if (h.SourceGraphics != null) OnPaintVisualState(new BufferedPaintEventArgs(currentState, h.SourceGraphics)); if (h.Graphics != null) OnPaintVisualState(new BufferedPaintEventArgs(newState, h.Graphics)); } else { currentState = newState; OnPaintVisualState(new BufferedPaintEventArgs(currentState, e.Graphics)); } } } } } } else { // buffered painting not supported, just paint using the current state currentState = newState; OnPaintVisualState(new BufferedPaintEventArgs(currentState, e.Graphics)); } } private void Control_Resize(object sender, EventArgs e) { // resizing stops all playing animations if (animationsNeedCleanup && Control.IsHandleCreated) BufferedPaintStopAllAnimations(new HandleRef(Control, Control.Handle)); // update trigger bounds according to anchor styles foreach (var trigger in Triggers) { if (trigger.Bounds == Rectangle.Empty) continue; var newBounds = trigger.Bounds; if ((trigger.Anchor & AnchorStyles.Left) != AnchorStyles.Left) newBounds.X += Control.Width - oldSize.Width; if ((trigger.Anchor & AnchorStyles.Top) != AnchorStyles.Top) newBounds.Y += Control.Height - oldSize.Height; if ((trigger.Anchor & AnchorStyles.Right) == AnchorStyles.Right) newBounds.Width += Control.Width - oldSize.Width; if ((trigger.Anchor & AnchorStyles.Bottom) == AnchorStyles.Bottom) newBounds.Height += Control.Height - oldSize.Height; trigger.Bounds = newBounds; } // save old size for next resize oldSize = Control.Size; } /// Evaluates all state change triggers. private void EvalTriggers(object sender, EventArgs e) { if (Triggers.Count == 0) return; var nState = DefaultState; ApplyCondition(VisualStateTriggerTypes.Focused, ref nState); ApplyCondition(VisualStateTriggerTypes.Hot, ref nState); ApplyCondition(VisualStateTriggerTypes.Pushed, ref nState); ApplyCondition(VisualStateTriggerTypes.Disabled, ref nState); State = nState; } } /// EventArgs class for the BufferedPainter.PaintVisualState event. /// Any type representing the visual state of the control. public class BufferedPaintEventArgs : EventArgs { /// Initializes a new instance of the BufferedPaintEventArgs class. /// Visual state to paint. /// Graphics object on which to paint. public BufferedPaintEventArgs(TState state, Graphics graphics) { State = state; Graphics = graphics; } /// Gets the Graphics object on which to paint. public Graphics Graphics { get; } /// Gets the visual state to paint. public TState State { get; } } /// /// Represents a transition between two visual states. Describes the duration of the animation. Two transitions are considered equal if they represent the /// same change in visual state. /// /// Any type representing the visual state of the control. public class BufferedPaintTransition : IEquatable> { /// Initializes a new instance of the BufferedPaintTransition class. /// The previous visual state. /// The new visual state. /// Duration of the animation (in milliseconds). public BufferedPaintTransition(TState fromState, TState toState, int duration) { FromState = fromState; ToState = toState; Duration = duration; } /// Gets or sets the duration (in milliseconds) of the animation. public int Duration { get; set; } /// Gets the previous visual state. public TState FromState { get; } /// Gets the new visual state. public TState ToState { get; } /// Determines if two instances are equal. /// The object to compare. /// public override bool Equals(object? obj) { var other = obj as BufferedPaintTransition; return other != null ? ((IEquatable>)this).Equals(other) : base.Equals(obj); } /// Serves as a hash function for a particular type. /// public override int GetHashCode() => ((object)FromState ?? 0).GetHashCode() ^ ((object)ToState ?? 0).GetHashCode(); bool IEquatable>.Equals(BufferedPaintTransition other) { return other != null && Equals(FromState, other.FromState) && Equals(ToState, other.ToState); } } /// Represents a trigger for a particular visual state. Two triggers are considered equal if they are of the same type and visual state. /// Any type representing the visual state of the control. public class VisualStateTrigger : IEquatable> { /// Initializes a new instance of the VisualStateTrigger class. /// Type of trigger. /// Visual state applied when the trigger occurs. /// Bounds within which the trigger applies. /// Anchor for drawn items. public VisualStateTrigger(VisualStateTriggerTypes type, TState state, Rectangle bounds = default(Rectangle), AnchorStyles anchor = AnchorStyles.Top | AnchorStyles.Left) { Type = type; State = state; Bounds = bounds; Anchor = anchor; } /// Gets or sets how the bounds are anchored to the edge of the control. public AnchorStyles Anchor { get; set; } /// Gets or sets the bounds within which the trigger applies. public Rectangle Bounds { get; set; } /// Gets the visual state applied when the trigger occurs. public TState State { get; } /// Gets the type of trigger. public VisualStateTriggerTypes Type { get; } /// Determines if two instances are equal. /// The object to compare. /// public override bool Equals(object? obj) { var other = obj as VisualStateTrigger; return other != null ? ((IEquatable>)this).Equals(other) : base.Equals(obj); } /// Serves as a hash function for a particular type. /// public override int GetHashCode() => Type.GetHashCode() ^ ((object)State ?? 0).GetHashCode(); bool IEquatable>.Equals(VisualStateTrigger other) { return other != null && (Type == other.Type) && Equals(State, other.State); } } }