Better algorithm to fade a winform

2019-01-12 01:01发布

问题:

While searching for code to fade a winform, I came across this page on the MSDN forum.

for (double i = 0; i < 1; i+=0.01)
{
    this.Opacity = i;
    Application.DoEvents();
    System.Threading.Thread.Sleep(0);
}

The for loop has a non-integer increment and, from a previous question I asked, that's not a good programming technique (due to inexact representation of most decimals).

I came up with this alternative.

for (double i = 0; i < 100; ++i)
{
    this.Opacity = i/100;
    Application.DoEvents();
    System.Threading.Thread.Sleep(0);
}

Which of these is more efficient?

If there's a better algorithm for fading a form, I'll be very glad if it is included.

Thanks.

回答1:

Forget timers (pun intended).

With Visual Studio 4.5 or higher, you can just await a task that is delayed. An advantage of this method is that it's asynchronous, unlike a thread Sleep or DoEvents loop, which blocks the application during the fade (and the other aforementioned DoEvents problems).

private async void FadeIn(Form o, int interval = 80) 
{
    //Object is not fully invisible. Fade it in
    while (o.Opacity < 1.0)
    {
        await Task.Delay(interval);
        o.Opacity += 0.05;
    }
    o.Opacity = 1; //make fully visible       
}

private async void FadeOut(Form o, int interval = 80)
{
    //Object is fully visible. Fade it out
    while (o.Opacity > 0.0)
    {
        await Task.Delay(interval);
        o.Opacity -= 0.05;
    }
    o.Opacity = 0; //make fully invisible       
}

Usage:

private void button1_Click(object sender, EventArgs e)
{
    FadeOut(this, 100);
}

You should check if the object is disposed before you apply any transparency to it. I used a form as the object, but you can pass any object that supports transparency as long as it's cast properly.



回答2:

So, first off, application.DoEvents should be avoided unless you really know what you're doing and are sure that this is both an appropriate use of it, and that you are using it correctly. I'm fairly certain that neither is the case here.

Next, how are you controlling the speed of the fading? You're basically just letting the computer fade as quickly as it can and relying on the the inherent overhead of the operations (and background processes) to make it take longer. That's really not very good design. You're better off specifying how long the fade should take from the start so that it will be consistent between machines. You can use a Timer to execute code at the appropriate set intervals and ensure that the UI thread is not blocked for the duration of the fade (without using DoEvents).

Just modify the duration below to change how long the fade takes, and modify the steps to determine how "choppy" it is. I have it set to 100 because that's effectively what your code was doing before. In reality, you probably don't need that many and you can just lower to just before it starts getting choppy. (The lower the steps the better it will perform.)

Additionally, you shouldn't be so worried about performance for something like this. The fade is something that is going to need to be measured on the scale of about a second or not much less (for a human to be able to perceive it) and for any computer these days it can do so, so much more than this in a second it's not even funny. This will consume virtually no CPU in terms of computation over the course of a second, so trying to optimize it is most certainly micro-optimizing.

private void button1_Click(object sender, EventArgs e)
{
    int duration = 1000;//in milliseconds
    int steps = 100; 
    Timer timer = new Timer();
    timer.Interval = duration / steps;

    int currentStep = 0;
    timer.Tick += (arg1, arg2) =>
    {
        Opacity = ((double)currentStep) / steps;
        currentStep++;

        if (currentStep >= steps)
        {
            timer.Stop();
            timer.Dispose();
        }
    };

    timer.Start();
}


回答3:

for (double i = 0; i < 1; i+=0.01)
{
    this.Opacity = i;
    Application.DoEvents();
    System.Threading.Thread.Sleep(0);
}

is more efficient as the number of floating point divisions are more machine-expensive than compared to floating point additions(which do not affect vm-flags). That said, you could reduce the number of iterations by 1/2(that is change step to i+=0.02). 1% opacity reduction is NOT noticeable by the human brain and will be less expensive too, speeding it up almost 100% more.

EDIT:

for(int i = 0; i < 50; i++){
     this.Opacity = i * 0.02;
     Application.DoEvents();
     System.Threading.Thread.Sleep(0);
}


回答4:

I wrote a class specifically for fading forms in and out. It even supports ShowDialog and DialogResults.

I've expanded on it as I've needed new features, and am open to suggestions. You can take a look here:

https://gist.github.com/nathan-fiscaletti/3c0514862fe88b5664b10444e1098778

Example Usage

private void Form1_Shown(object sender, EventArgs e)
{
    Fader.FadeIn(this, Fader.FadeSpeed.Slower);
}


回答5:

I applied the approach of Victor Stoddard to a splashScreen. I used it in the Form_Load event to fadeIn and FormClosing event to fadeOut. NOTE: I had to set the form's opacity to 0 before call the fadeIn method.

Here you can see the order of events rised by a winform (lifecycle): https://msdn.microsoft.com/en-us/library/86faxx0d(v=vs.110).aspx

private void Splash_Load(object sender, EventArgs e)
{
    this.Opacity = 0.0;
    FadeIn(this, 70);
}


private void Splash_FormClosing(object sender, FormClosingEventArgs e)
{
    FadeOut(this, 30);
}

private async void FadeIn(Form o, int interval = 80)
{
    //Object is not fully invisible. Fade it in
    while (o.Opacity < 1.0)
    {
        await Task.Delay(interval);
        o.Opacity += 0.05;
    }
    o.Opacity = 1; //make fully visible       
}

private async void FadeOut(Form o, int interval = 80)
{
    //Object is fully visible. Fade it out
    while (o.Opacity > 0.0)
    {
        await Task.Delay(interval);
        o.Opacity -= 0.05;
    }
    o.Opacity = 0; //make fully invisible       
}


回答6:

In the past, I've used AnimateWindow to fade in/out a generated form that blanks over my entire application in SystemColor.WindowColor.

This neat little trick gives the effect of hiding/swapping/showing screens in a wizard like interface. I've not done this sort of thing for a while, but I used P/Invoke in VB and ran the API in a thread of its own.

I know your question is in C#, but it's roughly the same. Here's some lovely VB I've dug out and haven't looked at since 2006! Obviously it would be easy to adapt this to fade your own form in and out.

<DllImport("user32.dll")> _
Public Shared Function AnimateWindow(ByVal hwnd As IntPtr, ByVal dwTime As Integer, ByVal dwFlags As AnimateStyles) As Boolean
End Function

Public Enum AnimateStyles As Integer
    Slide = 262144
    Activate = 131072
    Blend = 524288
    Hide = 65536
    Center = 16
    HOR_Positive = 1
    HOR_Negative = 2
    VER_Positive = 4
    VER_Negative = 8
End Enum

Private m_CoverUp As Form

Private Sub StartFade()
    m_CoverUp = New Form()
    With m_CoverUp
        .Location = Me.PointToScreen(Me.pnlMain.Location)
        .Size = Me.pnlMain.Size
        .FormBorderStyle = System.Windows.Forms.FormBorderStyle.None
        .BackColor = Drawing.SystemColors.Control
        .Visible = False
        .ShowInTaskbar = False
        .StartPosition = System.Windows.Forms.FormStartPosition.Manual
    End With
    AnimateWindow(m_CoverUp.Handle, 100, AnimateStyles.Blend) 'Blocks
    Invoke(New MethodInvoker(AddressOf ShowPage))
End Sub

Private Sub EndFade()
    AnimateWindow(m_CoverUp.Handle, 100, AnimateStyles.Blend Or AnimateStyles.Hide)
    m_CoverUp.Close()
    m_CoverUp = Nothing
End Sub