mirror of https://github.com/dahall/Vanara.git
438 lines
16 KiB
C#
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);
|
|
}
|
|
}
|
|
} |