Vanara/Windows.Forms/_InProgress_/BufferedPainter.cs

438 lines
16 KiB
C#

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
{
/// <summary>Represents the types of trigger which can change the visual state of a control.</summary>
public enum VisualStateTriggerTypes
{
/// <summary>The control receives input focus.</summary>
Focused,
/// <summary>The mouse is over the control.</summary>
Hot,
/// <summary>The left mouse button is pressed on the control.</summary>
Pushed,
/// <summary>The control is disabled.</summary>
Disabled
}
/// <summary>
/// Attaches to a System.Windows.Forms.Control and provides buffered painting functionality.
/// <para>Uses TState to represent the visual state of the control. Animations are attached to transitions between states.</para>
/// </summary>
/// <typeparam name="TState">Any type representing the visual state of the control.</typeparam>
public class BufferedPainter<TState> : IDisposable
{
private bool animationsNeedCleanup;
private TState currentState;
private TState defaultState;
private TState newState;
private Size oldSize;
private bool focused, disabled, hot, pushed;
/// <summary>Initializes a new instance of the BufferedPainter class.</summary>
/// <param name="control">
/// Control this instance is attached to.
/// <para>For best results, use a control which does not paint its background.</para>
/// <para>Note: Buffered painting does not work if the OptimizedDoubleBuffer newDS is set for the control.</para>
/// </param>
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;
}
/// <summary>Fired when the control must be painted in a particular state.</summary>
public event EventHandler<BufferedPaintEventArgs<TState>> PaintVisualState;
/// <summary>Gets whether buffered painting is supported for the current OS/configuration.</summary>
public bool BufferedPaintSupported { get; }
/// <summary>Gets the control this instance is attached to.</summary>
public Control Control { get; }
/// <summary>Gets or sets the default animation duration (in milliseconds) for state transitions. The default is zero (not animated).</summary>
public int DefaultDuration { get; set; }
/// <summary>Gets or sets the default visual state. The default value is 'default(TState)'.</summary>
public TState DefaultState
{
get { return defaultState; }
set
{
var usingOldDefault = Equals(currentState, defaultState);
defaultState = value;
if (usingOldDefault) currentState = newState = defaultState;
}
}
/// <summary>Gets or sets the current visual state.</summary>
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();
}
}
}
/// <summary>Gets the collection of state transitions and their animation durations. Only one item for each unique state transition is permitted.</summary>
public ICollection<BufferedPaintTransition<TState>> Transitions { get; } = new HashSet<BufferedPaintTransition<TState>>();
/// <summary>Short-hand method for adding a state transition.</summary>
/// <param name="fromState">The previous visual state.</param>
/// <param name="toState">The new visual state.</param>
/// <param name="duration">Duration of the animation (in milliseconds).</param>
public void AddTransition(TState fromState, TState toState, int duration)
{
Transitions.Add(new BufferedPaintTransition<TState>(fromState, toState, duration));
}
/// <summary>
/// Adds the transition matrix.
/// </summary>
/// <param name="matrix">The matrix.</param>
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;
}
/// <summary>Raises the PaintVisualState event.</summary>
/// <param name="e">BufferedPaintEventArgs instance.</param>
protected virtual void OnPaintVisualState(BufferedPaintEventArgs<TState> e)
{
PaintVisualState?.Invoke(this, e);
}
/// <summary>Helper method for EvalTriggers().</summary>
/// <param name="type">Type of trigger to search for.</param>
/// <param name="stateIfTrue">Reference to the visual state variable to update (if the trigger occurs).</param>
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;
}
}
/// <summary>Deactivates buffered painting.</summary>
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<TState>(currentState, h.SourceGraphics));
if (h.Graphics != null)
OnPaintVisualState(new BufferedPaintEventArgs<TState>(newState, h.Graphics));
}
else
{
currentState = newState;
OnPaintVisualState(new BufferedPaintEventArgs<TState>(currentState, e.Graphics));
}
}
}
}
}
}
else
{
// buffered painting not supported, just paint using the current state
currentState = newState;
OnPaintVisualState(new BufferedPaintEventArgs<TState>(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;
}
/// <summary>Evaluates all state change triggers.</summary>
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;
}
}
/// <summary>EventArgs class for the BufferedPainter.PaintVisualState event.</summary>
/// <typeparam name="TState">Any type representing the visual state of the control.</typeparam>
public class BufferedPaintEventArgs<TState> : EventArgs
{
/// <summary>Initializes a new instance of the BufferedPaintEventArgs class.</summary>
/// <param name="state">Visual state to paint.</param>
/// <param name="graphics">Graphics object on which to paint.</param>
public BufferedPaintEventArgs(TState state, Graphics graphics)
{
State = state;
Graphics = graphics;
}
/// <summary>Gets the Graphics object on which to paint.</summary>
public Graphics Graphics { get; }
/// <summary>Gets the visual state to paint.</summary>
public TState State { get; }
}
/// <summary>
/// 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.
/// </summary>
/// <typeparam name="TState">Any type representing the visual state of the control.</typeparam>
public class BufferedPaintTransition<TState> : IEquatable<BufferedPaintTransition<TState>>
{
/// <summary>Initializes a new instance of the BufferedPaintTransition class.</summary>
/// <param name="fromState">The previous visual state.</param>
/// <param name="toState">The new visual state.</param>
/// <param name="duration">Duration of the animation (in milliseconds).</param>
public BufferedPaintTransition(TState fromState, TState toState, int duration)
{
FromState = fromState;
ToState = toState;
Duration = duration;
}
/// <summary>Gets or sets the duration (in milliseconds) of the animation.</summary>
public int Duration { get; set; }
/// <summary>Gets the previous visual state.</summary>
public TState FromState { get; }
/// <summary>Gets the new visual state.</summary>
public TState ToState { get; }
/// <summary>Determines if two instances are equal.</summary>
/// <param name="obj">The object to compare.</param>
/// <returns></returns>
public override bool Equals(object? obj)
{
var other = obj as BufferedPaintTransition<TState>;
return other != null ? ((IEquatable<BufferedPaintTransition<TState>>)this).Equals(other) : base.Equals(obj);
}
/// <summary>Serves as a hash function for a particular type.</summary>
/// <returns></returns>
public override int GetHashCode() => ((object)FromState ?? 0).GetHashCode() ^ ((object)ToState ?? 0).GetHashCode();
bool IEquatable<BufferedPaintTransition<TState>>.Equals(BufferedPaintTransition<TState> other)
{
return other != null && Equals(FromState, other.FromState) && Equals(ToState, other.ToState);
}
}
/// <summary>Represents a trigger for a particular visual state. Two triggers are considered equal if they are of the same type and visual state.</summary>
/// <typeparam name="TState">Any type representing the visual state of the control.</typeparam>
public class VisualStateTrigger<TState> : IEquatable<VisualStateTrigger<TState>>
{
/// <summary>Initializes a new instance of the VisualStateTrigger class.</summary>
/// <param name="type">Type of trigger.</param>
/// <param name="state">Visual state applied when the trigger occurs.</param>
/// <param name="bounds">Bounds within which the trigger applies.</param>
/// <param name="anchor">Anchor for drawn items.</param>
public VisualStateTrigger(VisualStateTriggerTypes type, TState state, Rectangle bounds = default(Rectangle), AnchorStyles anchor = AnchorStyles.Top | AnchorStyles.Left)
{
Type = type;
State = state;
Bounds = bounds;
Anchor = anchor;
}
/// <summary>Gets or sets how the bounds are anchored to the edge of the control.</summary>
public AnchorStyles Anchor { get; set; }
/// <summary>Gets or sets the bounds within which the trigger applies.</summary>
public Rectangle Bounds { get; set; }
/// <summary>Gets the visual state applied when the trigger occurs.</summary>
public TState State { get; }
/// <summary>Gets the type of trigger.</summary>
public VisualStateTriggerTypes Type { get; }
/// <summary>Determines if two instances are equal.</summary>
/// <param name="obj">The object to compare.</param>
/// <returns></returns>
public override bool Equals(object? obj)
{
var other = obj as VisualStateTrigger<TState>;
return other != null ? ((IEquatable<VisualStateTrigger<TState>>)this).Equals(other) : base.Equals(obj);
}
/// <summary>Serves as a hash function for a particular type.</summary>
/// <returns></returns>
public override int GetHashCode() => Type.GetHashCode() ^ ((object)State ?? 0).GetHashCode();
bool IEquatable<VisualStateTrigger<TState>>.Equals(VisualStateTrigger<TState> other)
{
return other != null && (Type == other.Type) && Equals(State, other.State);
}
}
}