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);
}
}
}