更好的算法来实现WinForm淡入淡出效果

8

在寻找淡入winform的代码时,我发现了这个MSDN论坛上的页面

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

for循环中使用非整数增量是不好的编程技巧(由于大多数小数的不精确表示)。我想出了另一种替代方案。

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

以下哪种更有效率?

如果有更好的淡化表单的算法,如果能加入就非常感激。

谢谢。


在Win32上,如果我没记错的话,Sleep(0)是为了将线程剩余的时间返回给调度程序,以便让其他线程有机会运行。也许在.NET上也是一样的。MSDN指出:“_如果您指定0毫秒,则线程将放弃其时间片的剩余部分,但仍然保持就绪状态._” - Uwe Keim
直接将“int”更改为“double”怎么样? - Uwe Keim
在VB.NET中,Single对应的是C#中的"float",而不是int。 - Hans Passant
你可以查看Windows API - http://www.codeproject.com/Articles/43058/Fading-Forms-and-Setting-Opacity-Without-Flicker-U - JMK
7个回答

23

忘掉计时器(此处有双关语)。

使用 Visual Studio 4.5 或更高版本,你可以直接await一个延迟的任务。这种方法的优势在于它是异步的,不像线程的SleepDoEvents循环,在淡出期间会阻塞应用程序(以及其他上述的DoEvents问题)。

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       
}

用法:

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

在对对象应用透明度之前,您应该检查该对象是否已被处理。我使用了一个窗体作为对象,但只要正确转换,您可以传递支持透明度的任何对象。


当我从后台工作完成事件调用FadeIn时,'await'行会导致死锁。我该如何避免这种情况? - Russell Horwood
1
@NineTails,您是否尝试将FadeIn方法封装在this.BeginInvoke(new Action(() => FadeIn(...)))中? - Drakarah
1
@drake7707 感谢您。 但是,我之前使用'SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());'解决了我的问题。 - Russell Horwood
1
我在WPF中尝试了上述方法,没有进行任何线程修改就完美地运行了(我确实需要删除对Windows Forms的引用,但那很容易)。我只需要先将窗口不透明度设置为0,淡入效果就能正常工作。 - Frecklefoot

14

首先,除非你真的知道自己在做什么并且确定这是使用它的适当方式,而且正在正确地使用它,否则应避免使用application.DoEvents。我相当确定这两种情况都不适用于此。

接下来,您如何控制淡入淡出的速度?实际上,您只是让计算机尽可能快地进行淡化,并依靠操作(和后台进程)的固有开销使其需要更长时间。那样的设计真的不太好。最好从一开始就指定淡化所需的时间,以便在各台机器之间保持一致。您可以使用Timer在适当的时间间隔执行代码,并确保UI线程在淡化期间未被阻塞(而无需使用DoEvents)。

只需修改下面的duration以更改淡化所需的时间,并修改steps以确定其“颤抖”程度。我将其设置为100,因为这实际上就是您的代码之前所做的事情。实际上,您可能不需要那么多,可以将其降低到刚好在变得杂乱之前。 (步数越低,性能就越好。)

此外,您不应该过于担心像这样的性能问题。淡入淡出是需要在约一秒钟或更短时间尺度上进行测量的东西(才能被人类感知),而对于任何计算机来说,在一秒钟内它可以做得比这更多,甚至有些荒谬。在一个周期内,这几乎不会消耗任何CPU计算能力,因此试图对其进行优化肯定是微观优化。

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();
}

2
这是一个明智的评论,我特别喜欢你坚持“应该避免使用DoEvents”这一事实......确实应该避免。我也做了类似的事情,但你忽略了一些重要的东西:你应该在某个时刻停止计时器!!!在Tick处理程序中,包括检查if (Opacity >= 1.0) { timer.Stop(); timer.Dispose(); } - Fernando Espinosa
@khovanskiiªn 谢谢,我完全忘记停止计时器了;已经编辑好了。 - Servy

4

示例表单淡入效果

我写了一个专门用于渐变窗体的类,甚至支持ShowDialog和DialogResults。

随着需要新功能,我已经对其进行了扩展,并且乐于接受建议。您可以在这里查看:

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

示例用法

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

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

随着浮点除法的数量比浮点加法更加耗费机器资源,因此使用浮点加法(不影响vm-flags)可以使代码更加高效。也就是说,您可以将迭代次数减少1/2(即将步长改为i += 0.02)。对于人类大脑来说,1%的透明度降低并不会被察觉,而且成本更低,可以将速度提高近100%。

编辑:

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

2
我应用了Victor Stoddard的方法来实现一个启动画面。我在Form_Load事件中使用fadeIn方法,在FormClosing事件中使用fadeOut方法。 注意:在调用fadeIn方法之前,我必须将窗体的不透明度设置为0。 在这里,你可以看到winform的事件顺序(生命周期): 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       
}

1
过去,我使用AnimateWindow来淡入/淡出一个生成的表单,该表单覆盖了我的整个应用程序中的SystemColor.WindowColor
这个小技巧可以实现在向导式界面中隐藏/交换/显示屏幕的效果。我已经有一段时间没有做这样的事情了,但我在VB中使用P/Invoke,并在其自己的线程中运行API。
我知道你的问题是关于C#的,但它大致相同。这是我挖出来的一些可爱的VB代码,自2006年以来就没有看过!显然,很容易将其调整为淡入/淡出您自己的表单。
<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

1
这是Visual Basic。 - carefulnow1
问题是关于C#的。 - Luca Ziegler

1

Victor Stoddard已经接近了,但我发现如果淡入开始时透明度增加得更快,并在接近完全不透明度(1)时减缓,那么效果会更好。这是对他代码的轻微修改:

    using System.Threading.Tasks;
    using System.Windows.Forms;

    public partial class EaseInForm : Form
    {
        public EaseInForm()
        {
            InitializeComponent();
            this.Opacity = 0;
        }

        private async void Form_Load(object sender, EventArgs e)
        {
            while (this.Opacity < 1.0)
            {
                var percent = (int)(this.Opacity * 100);
                await Task.Delay(percent);
                this.Opacity += 0.04;
            }
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接