2017-11-27 13:11:20 -05:00
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>
2023-09-11 12:04:54 -04:00
public override bool Equals ( object? obj )
2017-11-27 13:11:20 -05:00
{
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>
2023-09-11 12:04:54 -04:00
public override bool Equals ( object? obj )
2017-11-27 13:11:20 -05:00
{
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 ) ;
}
}
}