Search code examples
vb.netwinformsanimationgraphicstransition

Is it possible to generate animated transitions with standard Controls?


Is it possible to make animations, like the transitions generated by Bunifu, with standard WinForms Controls in the VB.NET language?

Public Class Form1
    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        PanelForm.Visible = False
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        BunifuTransition1.Show(PanelForm)
    End Sub

    Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
        BunifuTransition1.Hide(PanelForm)
    End Sub
End Class

PROPERTIES BUNIFU TRANSITION

BUNIFU TRANSITION


Solution

  • To add some animation effects to a window (Controls are windows), you can use the Win32 AnimateWindow function.
    This function allows to generate some predefined effects: roll, slide, collapse or expand and alpha-blended fade (which can only be applied to a top-level window, though).

    It's missing any effect that scale the content of the window, as shown in the question.
    I've added this missing effect (along with the already supported ones) in an extension method, Animate() that is applied to all classes that derive from Control.
    Which means you can also animate a Form that is not a top-level window (.TopLevel = False)

    For example, to animate a Panel (and the Controls it hosts), write:

    SomePanel.Animate(AnimationMode.OpenScale, 500)
    

    to add an opening animation that lasts 500 milliseconds. Or, e.g.,

    SomePanel.Animate(AnimationMode.CloseScale, 500)
    

    to add a closing animation with the same duration.
    Try out the other effects you can select, settings a different AnimationMode.

    This is how it works:

    AnimateWindow Extension Method

    The generated GIF animation makes it a little sluggish


    Add this Module to any WinForms Project.

    Imports System.Drawing
    Imports System.Drawing.Imaging
    Imports System.Runtime.CompilerServices
    Imports System.Runtime.InteropServices
    
    Module ExtensionMethods
        ''' <summary>
        ''' Add opening or closing effects to a Control
        ''' </summary>
        ''' <param name="window">The Control to animate</param>
        ''' <param name="mode">The animation effect</param>
        ''' <param name="animationSpeed">The overall time, in milliseconds, of the animation</param>
        <Extension()>
        Public Sub Animate(window As Control, mode As AnimationMode, animationSpeed As Integer)
            If isAnimating Then Return
            isAnimating = True
            If mode.HasFlag(AnimationMode.OpenScale) OrElse mode.HasFlag(AnimationMode.CloseScale) Then
                AnimateScale(window, mode, animationSpeed)
            Else
                window.Show()
                If Not mode.HasFlag(AnimationMode.Hide) Then window.Hide()
                AnimateWindow(window.Handle, CType(animationSpeed, UInteger), mode)
                If Not mode.HasFlag(AnimationMode.Hide) Then window.Show()
            End If
        End Sub
    
        Private isAnimating As Boolean = False
        Private animateBitmap As Bitmap = Nothing
        Private animateBitmapBounds As Rectangle = Rectangle.Empty
    
        Private Sub AnimateScale(window As Control, mode As AnimationMode, Optional animationSpeed As Integer = 200)
            Dim steps = 10
            If animationSpeed < 200 Then animationSpeed = 200
            Dim timerSteps = animationSpeed \ (steps + 1)
            Dim animationFrames As List(Of Bitmap) = Nothing
    
            Using bmpSource = New Bitmap(window.Width, window.Height, PixelFormat.Format32bppArgb)
                window.Show()
                window.DrawToBitmap(bmpSource, New Rectangle(Point.Empty, window.Size))
                window.Hide()
                animationFrames = GetAnimationFrames(steps, bmpSource, mode.HasFlag(AnimationMode.CloseScale))
            End Using
    
            Dim windowParent = If(window.Parent Is Nothing, window, window.Parent)
            animateBitmapBounds = window.Bounds
            Dim timer = New System.Windows.Forms.Timer() With {.Interval = timerSteps}
            AddHandler timer.Tick,
            Sub()
                If steps < 0 Then
                    timer.Stop()
                    RemoveHandler windowParent.Paint, AddressOf AnimateWindowParent
                    animationFrames.ForEach(Sub(img) img.Dispose())
                    animationFrames = Nothing
                    animateBitmap = Nothing
                    timer.Dispose()
                    isAnimating = False
                Else
                    animateBitmap = animationFrames(steps)
                    windowParent.Invalidate(animateBitmapBounds)
                    If steps = 0 AndAlso mode.HasFlag(AnimationMode.OpenScale) Then window.Show()
                End If
                steps -= 1
            End Sub
            AddHandler windowParent.Paint, AddressOf AnimateWindowParent
            window.Hide()
            timer.Start()
        End Sub
    
        Private Function GetAnimationFrames(framesCount As Integer, sourceImage As Image, collapse As Boolean) As List(Of Bitmap)
            Dim animationFrames As New List(Of Bitmap)(framesCount)
            Dim scaleWidth = sourceImage.Width \ framesCount
            Dim scaleHeight = sourceImage.Height \ framesCount
    
            For frame As Integer = 1 To framesCount
                Dim imageSize = New Size(sourceImage.Width - frame * scaleWidth, sourceImage.Height - frame * scaleHeight)
                Dim imagePos = New Point((sourceImage.Width - imageSize.Width) \ 2, (sourceImage.Height - imageSize.Height) \ 2)
                Dim imageFrame = New Bitmap(sourceImage.Width, sourceImage.Height, PixelFormat.Format32bppArgb)
                Using g = Graphics.FromImage(imageFrame)
                    g.DrawImage(sourceImage, New RectangleF(imagePos, imageSize),
                                sourceImage.GetBounds(GraphicsUnit.Pixel), GraphicsUnit.Pixel)
                    If collapse Then
                        animationFrames.Insert(0, imageFrame)
                        If frame = framesCount Then animationFrames.Insert(0, New Bitmap(2, 2))
                    Else
                        animationFrames.Add(imageFrame)
                        If frame = framesCount Then animationFrames.Add(New Bitmap(2, 2))
                    End If
                End Using
            Next
            Return animationFrames
        End Function
    
        Private Sub AnimateWindowParent(sender As Object, e As PaintEventArgs)
            If animateBitmap Is Nothing Then Return
            e.Graphics.DrawImage(animateBitmap, animateBitmapBounds)
        End Sub
    
        <Flags()>
        Public Enum AnimationMode As UInteger
            CloseCentered = AW_CENTER Or AW_HIDE
            CloseDownwards = AW_VER_POSITIVE Or AW_HOR_POSITIVE Or AW_HIDE
            CloseUpwards = AW_VER_NEGATIVE Or AW_HOR_NEGATIVE Or AW_HIDE
            OpenCentered = AW_CENTER
            OpenDownwards = AW_HOR_POSITIVE Or AW_VER_POSITIVE
            OpenUpwards = AW_HOR_NEGATIVE Or AW_VER_NEGATIVE
            OpenScale = &H20
            CloseScale = &H40 Or Hide
            SlideToRight = AW_SLIDE Or AW_HOR_POSITIVE
            SlideToLeft = AW_SLIDE Or AW_HOR_NEGATIVE
            SlideDownwards = AW_SLIDE Or AW_VER_POSITIVE
            SlideUpwards = AW_SLIDE Or AW_VER_NEGATIVE
            Hide = AW_HIDE
        End Enum
    
        Private Const AW_HOR_POSITIVE As UInteger = &H1
        Private Const AW_HOR_NEGATIVE As UInteger = &H2
        Private Const AW_VER_POSITIVE As UInteger = &H4
        Private Const AW_VER_NEGATIVE As UInteger = &H8
        Private Const AW_CENTER As UInteger = &H10
        Private Const AW_HIDE As UInteger = &H10000
        Private Const AW_ACTIVATE As UInteger = &H20000
        Private Const AW_SLIDE As UInteger = &H40000
        Private Const AW_BLEND As UInteger = &H80000
    
        <DllImport("user32.dll", SetLastError:=True)>
        Private Function AnimateWindow(hwnd As IntPtr, time As UInteger, flags As AnimationMode) As Boolean
        End Function
    End Module
    

    C# version, just in case:

    using System.Drawing;
    using System.Drawing.Imaging;
    using System.Runtime.InteropServices;
    
    
    public static class ExtensionMethods {
        public static void Animate(this Control window, AnimationMode mode, int animationSpeed) {
            if (isAnimating) return; 
            isAnimating = true;
            if (mode.HasFlag(AnimationMode.OpenScale) || mode.HasFlag(AnimationMode.CloseScale)) {
                AnimateScale(window, mode, animationSpeed);
            }
            else {
                window.Show();
                if (!mode.HasFlag(AnimationMode.Hide)) window.Hide();
                AnimateWindow(window.Handle, (uint)animationSpeed, mode);
                if (!mode.HasFlag(AnimationMode.Hide)) window.Show();
            }
        }
    
        private static bool isAnimating = false;
        private static Bitmap? animateBitmap = null;
        private static Rectangle animateBitmapBounds = Rectangle.Empty;
    
        private static void AnimateScale(Control window, AnimationMode mode, int animationSpeed = 200) {
            int steps = 10;
            if (animationSpeed < 200) animationSpeed = 200;
            int timerSteps = animationSpeed / (steps + 1);
            List<Bitmap>? animationFrames = null;
    
            using (var bmpSource = new Bitmap(window.Width, window.Height, PixelFormat.Format32bppArgb)) {
                window.Show();
                window.DrawToBitmap(bmpSource, new Rectangle(Point.Empty, window.Size));
                window.Hide();
                animationFrames = GetAnimationFrames(steps, bmpSource, mode.HasFlag((Enum)AnimationMode.CloseScale));
            }
    
            var windowParent = window.Parent is null ? window : window.Parent;
            animateBitmapBounds = window.Bounds;
            var timer = new System.Windows.Forms.Timer() { Interval = timerSteps };
            timer.Tick += (_, _) =>
            {
                if (steps < 0) {
                    timer.Stop();
                    windowParent.Paint -= AnimateWindowParent;
                    animationFrames.ForEach(img => img.Dispose());
                    animationFrames = null;
                    animateBitmap = null;
                    timer.Dispose();
                    isAnimating = false;
                }
                else {
                    animateBitmap = animationFrames[steps];
                    windowParent.Invalidate(animateBitmapBounds);
                    if (steps == 0 && mode.HasFlag(AnimationMode.OpenScale))
                        window.Show();
                }
                steps -= 1;
            };
            windowParent.Paint += AnimateWindowParent;
            window.Hide();
            timer.Start();
        }
    
        private static List<Bitmap> GetAnimationFrames(int framesCount, Image sourceImage, bool collapse) {
            var animationFrames = new List<Bitmap>(framesCount);
            var scaleWidth = sourceImage.Width / framesCount;
            var scaleHeight = sourceImage.Height / framesCount;
    
            for (int frame = 1, loopTo = framesCount; frame <= loopTo; frame++) {
                var imageSize = new Size(sourceImage.Width - frame * scaleWidth, sourceImage.Height - frame * scaleHeight);
                var imagePos = new Point((sourceImage.Width - imageSize.Width) / 2, (sourceImage.Height - imageSize.Height) / 2);
                var imageFrame = new Bitmap(sourceImage.Width, sourceImage.Height, PixelFormat.Format32bppArgb);
                using (var g = Graphics.FromImage(imageFrame)) {
                    var unit = GraphicsUnit.Pixel;
                    g.DrawImage(sourceImage, new RectangleF(imagePos, imageSize), sourceImage.GetBounds(ref unit), unit);
                    if (collapse) {
                        animationFrames.Insert(0, imageFrame);
                        if (frame == framesCount) animationFrames.Insert(0, new Bitmap(2, 2));
                    }
                    else {
                        animationFrames.Add(imageFrame);
                        if (frame == framesCount) animationFrames.Add(new Bitmap(2, 2));
                    }
                }
            }
            return animationFrames;
        }
    
        private static void AnimateWindowParent(object? sender, PaintEventArgs e) {
            if (animateBitmap is null) return;
            e.Graphics.DrawImage(animateBitmap, animateBitmapBounds);
        }
    
        [Flags()]
        public enum AnimationMode : uint {
            CloseCentered = AW_CENTER | AW_HIDE,
            CloseDownwards = AW_VER_POSITIVE | AW_HOR_POSITIVE | AW_HIDE,
            CloseUpwards = AW_VER_NEGATIVE | AW_HOR_NEGATIVE | AW_HIDE,
            OpenCentered = AW_CENTER,
            OpenDownwards = AW_HOR_POSITIVE | AW_VER_POSITIVE,
            OpenUpwards = AW_HOR_NEGATIVE | AW_VER_NEGATIVE,
            OpenScale = 0x20U,
            CloseScale = 0x40U | Hide,
            SlideToRight = AW_SLIDE | AW_HOR_POSITIVE,
            SlideToLeft = AW_SLIDE | AW_HOR_NEGATIVE,
            SlideDownwards = AW_SLIDE | AW_VER_POSITIVE,
            SlideUpwards = AW_SLIDE | AW_VER_NEGATIVE,
            Hide = AW_HIDE
        }
    
        #region NativeMethods
    
        private const uint AW_HOR_POSITIVE = 0x1U;
        private const uint AW_HOR_NEGATIVE = 0x2U;
        private const uint AW_VER_POSITIVE = 0x4U;
        private const uint AW_VER_NEGATIVE = 0x8U;
        private const uint AW_CENTER = 0x10U;
        private const uint AW_HIDE = 0x10000U;
        private const uint AW_ACTIVATE = 0x20000U;
        private const uint AW_SLIDE = 0x40000U;
        private const uint AW_BLEND = 0x80000U;
    
        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool AnimateWindow(IntPtr hwnd, uint time, AnimationMode flags);
    
        #endregion
    
    }