65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的一个漂亮的全屏启动画面

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (297投票s)

2003年11月17日

CPOL

13分钟阅读

viewsIcon

1878150

downloadIcon

57176

一个带有漂亮预测进度条功能的启动屏幕。

引言

每次用户加载您的应用程序时,您都有机会通过启动画面给他们留下深刻印象或让他们失望。一个好的启动画面会

  • 在单独的线程上运行
  • 出现时淡入,消失时淡出
  • 显示一个使用静态方法更新的正在运行的状态消息
  • 显示并更新一个预测性的、自校准的、所有者绘制的平滑渐变进度条
  • 显示加载完成前剩余的秒数

在本教程中,我们将探讨如何创建启动画面并逐一添加这些功能。我们从创建一个简单的启动画面开始,然后进行添加每个功能所需的代码更改。您可以跳到文章底部查看完整的源代码。我还包含了一个演示启动画面的小型测试项目。

背景

编写这篇文章的代码非常有趣,虽然它并不完美适用于所有需求,但我希望它能为您节省一些编码时间。对我来说,最有趣的是看到我的应用程序加载时,一个完全准确且平滑进行的进度条。请随时发布您可能有的任何增强建议、错误或其他评论。

创建简单的启动画面项目

首先创建一个Windows Forms项目。将其命名为SplashScreen。将Form1.cs重命名为SplashScreen.cs

现在获取一个具有浅色背景的产品位图,适合在其上放置文本。如果您幸运的话,一位非常有才华的人(如dzCepheus - 请参阅下面的讨论)会为您提供一个。将其设置为窗体的背景图像。为窗体设置以下属性

FormBorderStyle = None
StartPosition = CenterScreen

在窗体构造函数中,添加以下行

this.ClientSize = this.BackgroundImage.Size;

转到项目属性并将输出类型更改为类库。此时,如果您不将此项目添加到现有解决方案中,您可能需要向解决方案中添加另一个WinForms项目来测试启动画面。您可以构建启动画面时调用该类的公共方法。

使其可以通过静态方法访问

由于启动画面只需要一个实例,您可以使用静态方法来访问它,从而简化您的代码。只需引用SplashScreen项目,一个组件就可以启动、更新或关闭启动画面,而无需对象引用。将以下代码添加到SplashScreen.cs

static SplashScreen ms_frmSplash = null;
// A static entry point to launch SplashScreen.
static public void ShowForm()
{
  ms_frmSplash = new SplashScreen();
  Application.Run(ms_frmSplash);
}
// A static method to close the SplashScreen
static public void CloseForm()
{
  ms_frmSplash.Close();
}

将其放在自己的线程上

启动画面在应用程序加载和初始化其组件时显示有关应用程序的信息。如果您在此期间要显示任何动态信息,则应将其放在单独的线程上,以防止在初始化占用主线程时冻结。

首先使用Threading命名空间

using System.Threading;  

声明一个静态变量来保存线程

static Thread ms_oThread = null; 

现在添加一个方法来在自己的线程上创建和启动启动画面。等待返回,以确保在窗体存在之前不会调用静态方法

static public void ShowSplashScreen()
{
  // Make sure it is only launched once.
  if( ms_frmSplash != null )
    return;
  ms_oThread = new Thread( new ThreadStart(SplashScreen.ShowForm));
  ms_oThread.IsBackground = true;
  ms_oThread.SetApartmentState(ApartmentState.STA);
  ms_oThread.Start();
  while (ms_frmSplash == null || ms_frmSplash.IsHandleCreated == false)
  {
    System.Threading.Thread.Sleep(TIMER_INTERVAL);
  }
}

现在ShowForm()可以设为私有,因为窗体将通过ShowSplashScreen()显示。

// A static entry point to launch SplashScreen.
static private void ShowForm()

添加淡入淡出代码

当启动画面首次出现时淡入,并在应用程序出现时淡出,这可以为您的启动画面增添真正的色彩。窗体的Opacity属性可以轻松实现这一点。

声明定义增量和减量速率的变量。这些变量定义了窗体出现和消失的速度。它们与定时器间隔直接相关,因为它们代表每次定时器滴答时Opacity增加或减少的量,因此如果您修改定时器间隔,则需要按比例更改这些值。

private double m_dblOpacityIncrement = .05;
private double m_dblOpacityDecrement = .08;
private const int TIMER_INTERVAL = 50;

向窗体添加一个定时器,将其重命名为UpdateTimer,然后修改构造函数以启动定时器并将不透明度初始化为零。

this.Opacity = .0;
UpdateTimer.Interval = TIMER_INTERVAL;
UpdateTimer.Start();

修改CloseForm()方法以启动淡出过程,而不是关闭窗体。

static public void CloseForm()
{
  if( ms_frmSplash != null )
  {
    // Make it start going away.
    ms_frmSplash.m_dblOpacityIncrement = -ms_frmSplash.m_dblOpacityDecrement;
  }
  ms_oThread = null;  // we do not need these any more.
  ms_frmSplash = null;
}

添加一个Tick事件处理程序,在窗体淡入或淡出时更改不透明度,并在不透明度达到0时关闭启动画面窗体。

private void UpdateTimer_Tick(object sender, System.EventArgs e)
{
  if( m_dblOpacityIncrement > 0 )
  {
    if( this.Opacity < 1 )
      this.Opacity += m_dblOpacityIncrement;
  }
  else
  {
    if( this.Opacity > 0 )
      this.Opacity += m_dblOpacityIncrement;
    else
      this.Close();
  }
} 

此时,您已经有了一个在调用ShowSplashScreen()方法时淡入的启动画面,并在调用CloseForm()方法时开始淡出的启动画面。

添加显示状态字符串的代码

现在基本启动画面已完成,我们可以向窗体添加状态信息,以便用户可以知道正在进行某些操作。为此,我们向窗体添加了成员变量m_sStatus来存储状态,以及一个标签lblStatus来显示它。然后,我们添加一个访问器方法来设置变量,并修改定时器滴答方法来更新标签。通常,在线程之间更新UI元素是非法的,因此,例如,尝试从主应用程序线程更新启动画面上的标签将导致异常(在.NET 2.0中)。此代码通过仅在状态和进度更新中更改数据来避免此问题。定时器事件(在启动画面线程上)用于根据在更新期间更改的成员变量来执行UI更新。

private string m_sStatus;
...
// A static method to set the status.
static public void SetStatus(string newStatus)
{
  if( ms_frmSplash == null )
    return;
  ms_frmSplash.m_sStatus = newStatus;
}

现在我们修改UpdateTimer_Tick方法来更新标签。

lblStatus.Text = m_sStatus;

现在添加一个进度条

除非您真的想要这种外观,否则没有理由使用标准的WinForms进度条。我们将通过绘制自己的Panel控件来制作一个渐变进度条。为此,向窗体添加一个名为pnlStatus的面板,并将其BackColor设置为Transparent。实际上,如果您期望在多个地方使用它,您可能希望派生自己的控件自Panel。在这里,我们将在定时器事件响应中绘制它。

声明一个变量来保存完成百分比。它是一个双精度值,其值将在进度条进行过程中在0到1之间变化。还声明一个矩形来保存当前的进度矩形。

private double m_dblCompletionFraction = 0;
private Rectangle m_rProgress; 

目前,添加一个公共属性来设置当前的完成百分比。稍后,当我们添加自校准功能时,我们将消除它的需要。

// Static method for updating the progress percentage.
static public double Progress
{
  get 
  {
    if( ms_frmSplash != null )
      return ms_frmSplash.m_dblCompletionFraction; 
    return 1.0;
  } 
  set
  {
    if( ms_frmSplash != null )
      ms_frmSplash.m_dblCompletionFraction = value;
  }
} 

现在我们修改定时器的Tick事件处理程序来计算我们要绘制的Panel的比例。

  ...
  int width = (int)Math.Floor(pnlStatus.ClientRectangle.Width 
     * m_dblCompletionFraction);
  int height = pnlStatus.ClientRectangle.Height;
  int x = pnlStatus.ClientRectangle.X;
  int y = pnlStatus.ClientRectangle.Y;
  if( width > 0 && height > 0 )
  {
    m_rProgress = new Rectangle( x, y, width, height);
  }
  ... 

现在,仍在UpdateTimer_Tick事件处理程序中,绘制渐变进度条。首先添加System.Drawing.Drawing2D命名空间。您可能需要调整RGB值以获得与您的图形匹配的配色方案。

using System.Drawing.Drawing2D; 
   // Draw the progress bar.
   if( e.ClipRectangle.Width > 0 && m_iActualTicks > 1 )
  {
    LinearGradientBrush brBackground = 
      new LinearGradientBrush(m_rProgress, 
                              Color.FromArgb(50, 50, 200),
                              Color.FromArgb(150, 150, 255), 
                              LinearGradientMode.Horizontal);
    e.Graphics.FillRectangle(brBackground, m_rProgress);
  }
}

通过外插进度更新来平滑进度

我不知道你们怎么想,但我一直对进度条的进展方式感到恼火。它们跳跃,在长时间操作期间停止,并且总是让我感到一种模糊的焦虑,认为它们可能已经无响应了。

那么,接下来的这段代码试图通过使进度条即使在长时间操作期间也能移动来缓解这种焦虑。我们通过改变Progress更新的含义来实现这一点。它们不再表示当前的完成百分比,而是表示下一个Progress更新之前当前活动将花费的时间百分比。例如,第一个更新可能表示在第二个更新到来之前将通过总量的25%。这使我们可以在等待下一个更新的同时,使用定时器绘制越来越多的状态栏,直到包括25%(但不超过)。目前,我们将猜测每个定时器滴答的进度。稍后,我们将根据经验计算。

添加成员变量来表示之前的进度以及每个定时器滴答要增加的进度条量。

private double m_dblLastCompletionFraction = 0.0;
private double m_dblPBIncrementPerTimerInterval = .015;

修改Progress属性,在设置新的Progress值之前保存旧值。

ms_frmSplash.m_dblLastCompletionFraction = 
    ms_frmSplash.m_dblCompletionFraction;

修改Timer.Tick事件处理程序以执行渐进式更新

if( m_dblLastCompletionFraction < m_dblCompletionFraction )
{
  m_dblLastCompletionFraction += m_dblPBIncrementPerTimerInterval;
  int width = (int)Math.Floor(pnlStatus.ClientRectangle.Width 
                   * m_dblLastCompletionFraction);
  int height = pnlStatus.ClientRectangle.Height;
  int x = pnlStatus.ClientRectangle.X;
  int y = pnlStatus.ClientRectangle.Y;
  if (width > 0 && height > 0)  // Paint progress bar
  {
    ...
  }
}

现在使进度条自动校准

我们现在可以消除指定进度百分比的需求,通过计算值并在启动画面调用之间记住它们。请注意,这仅在您在启动期间调用SetStatus()SetReferencePoint()的固定序列时才有效。

XML存储

您可以使用任何持久存储机制来记住调用之间的这些值(只有2个字符串需要存储)。在这里,我们将使用存储在用户特定位置的XML文件,该位置即使在非管理员权限用户登录时也是可写的。(在本文的早期版本中,我们使用了Windows注册表。)

using System.Xml;
...
internal class SplashScreenXMLStorage
{
  private static string ms_StoredValues = "SplashScreen.xml";
  private static string ms_DefaultPercents = "";
  private static string ms_DefaultIncrement = ".015";
  // Get or set the string storing the percentage complete at each checkpoint.
  static public string Percents
  {
    get { return GetValue("Percents", ms_DefaultPercents); }
    set { SetValue("Percents", value); }
  }
  // Get or set how much time passes between updates.
  static public string Interval
  {
    get { return GetValue("Interval", ms_DefaultIncrement); }
    set { SetValue("Interval", value); }
  }

  // Don't use the installation directory for the XML file - it's not always writable
  static private string StoragePath
  {
    get {return Path.Combine(Application.UserAppDataPath, ms_StoredValues);}
  }

  // Helper method for getting inner text of named element.
  static private string GetValue(string name, string defaultValue)
  {
    if (!File.Exists(StoragePath))
      return defaultValue;

    try
    {
      XmlDocument docXML = new XmlDocument();
      docXML.Load(StoragePath);
      XmlElement elValue = docXML.DocumentElement.SelectSingleNode(name) as XmlElement;
      return (elValue == null) ? defaultValue : elValue.InnerText;
    }
    catch
    {
      return defaultValue;
    }
  }

  // Helper method to set inner text of named element.  Creates document if it doesn't exist
  static public void SetValue(string name, string stringValue)
  {
    XmlDocument docXML = new XmlDocument();
    XmlElement elRoot = null;
    if (!File.Exists(StoragePath))
    {
      elRoot = docXML.CreateElement("root");
      docXML.AppendChild(elRoot);
    }
    else
    {
      docXML.Load(StoragePath);
      elRoot = docXML.DocumentElement;
    }
    XmlElement value = docXML.DocumentElement.SelectSingleNode(name) as XmlElement;
    if (value == null)
    {
      value = docXML.CreateElement(name);
      elRoot.AppendChild(value);
    }
    value.InnerText = stringValue;
    docXML.Save(StoragePath);
  }
}

成员变量

现在声明变量,用于跟踪每次更新之间花费的时间(这次)以及上次花费的时间(从XML文件中)。声明一些布尔标志,以指示这是第一次启动并且定时器已启动。

// Self-calibration support
private int m_iIndex = 1;
private int m_iActualTicks = 0;
private ArrayList m_alPreviousCompletionFraction;
private ArrayList m_alActualTimes = new ArrayList();
private DateTime m_dtStart;
private bool m_bFirstLaunch = false;
private bool m_bDTSet = false;

参考点

我们需要声明方法来在应用程序启动期间记录各种参考点。参考点对于制作自校准进度条至关重要,因为它们取代了进度条的百分比完成更新。(每个参考点的完成百分比将从上一次调用进度条计算得出。)要充分利用此功能,您应该在应用程序启动期间运行的初始化代码中散布参考点。放置得越多,您的进度条将越平滑和准确。这时静态访问就真正派上用场了,因为您无需SplashScreen的引用即可调用它们。

首先,我们需要一个简单的实用函数来返回自启动画面首次出现以来的毫秒数。这用于计算每次调用ReferencePoint之间的间隔所分配的总时间的百分比。

// Utility function to return elapsed Milliseconds since the 
// SplashScreen was launched.
private double ElapsedMilliSeconds()
{
  TimeSpan ts = DateTime.Now - m_dtStart;
  return ts.TotalMilliseconds;
}

现在我们将修改SetStatus()并添加一个新的SetReferencePoint()方法。两者都调用SetReferenceInternal(),它记录第一次调用的时间,并将每次后续调用的已用时间添加到数组中以供以后处理。它通过引用进度条的先前记录值来设置进度条的值。例如,如果我们正在处理第三次SetReferencePoint()调用,我们将使用上一次调用期间第三次和第四次调用之间发生的总加载时间的实际百分比。首先,添加SetReferencePoint()方法和执行记录自启动画面启动以来所用时间工作的SetReferenceInternal()

// Static method called from the initializing application to 
// give the splash screen reference points.  Not needed if
// you are using a lot of status strings.
static public void SetReferencePoint()
{
  if( ms_frmSplash == null )
    return;
  ms_frmSplash.SetReferenceInternal();
}

// Internal method for setting reference points.
private void SetReferenceInternal()
{
  if (m_bDTSet == false)
  {
    m_bDTSet = true;
    m_dtStart = DateTime.Now;
    ReadIncrements();
  }
  double dblMilliseconds = ElapsedMilliSeconds();
  m_alActualTimes.Add(dblMilliseconds);
  m_dblLastCompletionFraction = m_dblCompletionFraction;
  if (m_alPreviousCompletionFraction != null && m_iIndex < m_alPreviousCompletionFraction.Count)
    m_dblCompletionFraction = (double)m_alPreviousCompletionFraction[m_iIndex++];
  else
    m_dblCompletionFraction = (m_iIndex > 0) ? 1 : 0;
} 

接下来的两个函数,ReadIncrements()StoreIncrements(),分别读取和写入与每个ReferencePoint值关联的计算出的间隔。

// Function to read the checkpoint intervals from the previous invocation of the
// splashscreen from the XML file.
private void ReadIncrements()
{
  string sPBIncrementPerTimerInterval = SplashScreenXMLStorage.Interval;
  double dblResult;

  if (Double.TryParse(sPBIncrementPerTimerInterval, 
        System.Globalization.NumberStyles.Float,
        System.Globalization.NumberFormatInfo.InvariantInfo, out dblResult) == true)
    m_dblPBIncrementPerTimerInterval = dblResult;
  else
    m_dblPBIncrementPerTimerInterval = .0015;

  string sPBPreviousPctComplete = SplashScreenXMLStorage.Percents;

  if (sPBPreviousPctComplete != "")
  {
    string[] aTimes = sPBPreviousPctComplete.Split(null);
    m_alPreviousCompletionFraction = new ArrayList();

    for (int i = 0; i < aTimes.Length; i++)
    {
      double dblVal;
      if (Double.TryParse(aTimes[i],System.Globalization.NumberStyles.Float,
          System.Globalization.NumberFormatInfo.InvariantInfo, out dblVal))
        m_alPreviousCompletionFraction.Add(dblVal);
      else
        m_alPreviousCompletionFraction.Add(1.0);
    }
  }
  else
  {
    m_bFirstLaunch = true;
  }
}

// Method to store the intervals (in percent complete) from the current invocation of
// the splash screen to XML storage.
private void StoreIncrements()
{
  string sPercent = "";
  double dblElapsedMilliseconds = ElapsedMilliSeconds();
  for (int i = 0; i < m_alActualTimes.Count; i++)
  sPercent += ((double)m_alActualTimes[i] /
    dblElapsedMilliseconds).ToString("0.####",
    System.Globalization.NumberFormatInfo.InvariantInfo) + " ";

  SplashScreenXMLStorage.Percents = sPercent;
  m_dblPBIncrementPerTimerInterval = 1.0 / (double)m_iActualTicks;
  SplashScreenXMLStorage.Interval =
     m_dblPBIncrementPerTimerInterval.ToString("#.000000",
     System.Globalization.NumberFormatInfo.InvariantInfo);
} 

我们现在可以修改SetStatus()方法,在更新状态时添加一个Reference。我们还添加了一个重载方法,允许在不调用SetReferenceInternal()的情况下更新状态。如果您处于具有可变状态字符串更新集合的代码部分,则此方法很有用。请注意,根据调用SetStatus()的频率,您可能不需要在启动代码中进行许多SetReference()调用。

// A static method to set the status and update the reference.
static public void SetStatus(string newStatus)
{
  SetStatus(newStatus, true);
}

// A static method to set the status and optionally update the reference.
// This is useful if you are in a section of code that has a variable
// set of status string updates.  In that case, don't set the reference.
static public void SetStatus(string newStatus, bool setReference)
{
  if (ms_frmSplash == null)
    return;
  ms_frmSplash.m_sStatus = newStatus;
  if (setReference)
    ms_frmSplash.SetReferenceInternal();
}

我们还需要修改定时器滴答处理程序,使其仅在m_bFirstLaunchfalse时才进行绘制。这可以防止第一次启动时显示未校准的进度条。

...
// Paint progress bar
if(m_bFirstLaunch == false && m_dblLastCompletionFraction < m_dblCompletionFraction)
... 

添加剩余时间计数器

最后,我们可以通过检查剩余部分的百分比来相当准确地估计初始化的剩余时间。向启动画面窗体添加一个名为lblTimeRemaining的标签来显示它。添加一个成员变量m_sTimeRemaining来保存相应的字符串,然后将以下代码添加到UpdateTimer_Tick()事件处理程序中,以更新SplashScreen窗体上的lblTimeRemaining标签。

private string m_sTimeRemaining;
...
private void UpdateTimer_Tick(object sender, System.EventArgs e)
{
...
    int iSecondsLeft = 1 + (int)(TIMER_INTERVAL * ((1.0 - m_dblLastCompletionFraction) / m_dblPBIncrementPerTimerInterval)) / 1000;
    m_sTimeRemaining = (iSecondsLeft == 1) ? string.Format("1 second remaining") : string.Format("{0} seconds remaining", iSecondsLeft);
...
  lblTimeRemaining.Text = m_sTimeRemaining; 
}

还修改ReadIncrements()方法,该方法清除剩余时间标签,如下所示

private void ReadIncrements()
{
...
    m_sTimeRemaining = "";
...
}

请注意,标签是使用成员变量设置的。这使得在不引起跨线程异常的情况下,跨线程设置剩余时间文本成为可能。

使用启动画面

要使用启动画面,只需在Main()入口点的第一行调用SplashScreen.ShowSplashScreen()。定期调用SetStatus()(如果您有新的状态要报告)或SplashScreen.SetReferencePoint()(如果没有)来校准进度条。当您的初始化完成后,调用SplashScreen.CloseForm()来启动淡出过程。如果您有任何疑问,请查看下载中提供的测试模块。

您可能想尝试调整各种常量以调整淡入淡出的时间。如果将间隔设置为非常短的时间(如10毫秒),您将获得一个美丽平滑的进度条,但您的性能可能会受到影响。

当应用程序首次加载时,您会注意到进度条和剩余时间计数器不会显示。这是因为启动画面需要一次加载来校准进度条。它将在后续的应用程序启动时出现。

SplashScreen.cs 源代码

using System;
using System.Collections;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.IO;
using System.Threading;
using System.Windows.Forms;
using System.Xml;
using Microsoft.Win32;
using System.Runtime.InteropServices;

namespace SplashScreen
{
  // The SplashScreen class definition.  AKO Form
  public partial class SplashScreen : Form
  {
    #region Member Variables
    // Threading
    private static SplashScreen ms_frmSplash = null;
    private static Thread ms_oThread = null;

    // Fade in and out.
    private double m_dblOpacityIncrement = .05;
    private double m_dblOpacityDecrement = .08;
    private const int TIMER_INTERVAL = 50;

    // Status and progress bar
    private string m_sStatus;
    private string m_sTimeRemaining;
    private double m_dblCompletionFraction = 0.0;
    private Rectangle m_rProgress;

    // Progress smoothing
    private double m_dblLastCompletionFraction = 0.0;
    private double m_dblPBIncrementPerTimerInterval = .015;

    // Self-calibration support
    private int m_iIndex = 1;
    private int m_iActualTicks = 0;
    private ArrayList m_alPreviousCompletionFraction;
    private ArrayList m_alActualTimes = new ArrayList();
    private DateTime m_dtStart;
    private bool m_bFirstLaunch = false;
    private bool m_bDTSet = false;

    #endregion Member Variables

    /// <summary>
    /// Constructor
    /// </summary>
    public SplashScreen()
    {
      InitializeComponent();
      this.Opacity = 0.0;
      UpdateTimer.Interval = TIMER_INTERVAL;
      UpdateTimer.Start();
      this.ClientSize = this.BackgroundImage.Size;
    }

    #region Public Static Methods
    // A static method to create the thread and 
    // launch the SplashScreen.
    static public void ShowSplashScreen()
    {
      // Make sure it's only launched once.
      if (ms_frmSplash != null)
        return;
      ms_oThread = new Thread(new ThreadStart(SplashScreen.ShowForm));
      ms_oThread.IsBackground = true;
      ms_oThread.SetApartmentState(ApartmentState.STA);
      ms_oThread.Start();
      while (ms_frmSplash == null || ms_frmSplash.IsHandleCreated == false)
      {
        System.Threading.Thread.Sleep(TIMER_INTERVAL);
      }
    }

    // Close the form without setting the parent.
    static public void CloseForm()
    {
      if (ms_frmSplash != null && ms_frmSplash.IsDisposed == false)
      {
        // Make it start going away.
        ms_frmSplash.m_dblOpacityIncrement = -ms_frmSplash.m_dblOpacityDecrement;
      }
      ms_oThread = null;  // we don't need these any more.
      ms_frmSplash = null;
    }

    // A static method to set the status and update the reference.
    static public void SetStatus(string newStatus)
    {
      SetStatus(newStatus, true);
    }

    // A static method to set the status and optionally update the reference.
    // This is useful if you are in a section of code that has a variable
    // set of status string updates.  In that case, don't set the reference.
    static public void SetStatus(string newStatus, bool setReference)
    {
      if (ms_frmSplash == null)
        return;

      ms_frmSplash.m_sStatus = newStatus;

      if (setReference)
        ms_frmSplash.SetReferenceInternal();
    }

    // Static method called from the initializing application to 
    // give the splash screen reference points.  Not needed if
    // you are using a lot of status strings.
    static public void SetReferencePoint()
    {
      if (ms_frmSplash == null)
        return;
      ms_frmSplash.SetReferenceInternal();

    }
    #endregion Public Static Methods

    #region Private Methods

    // A private entry point for the thread.
    static private void ShowForm()
    {
      ms_frmSplash = new SplashScreen();
      Application.Run(ms_frmSplash);
    }

    // Internal method for setting reference points.
    private void SetReferenceInternal()
    {
      if (m_bDTSet == false)
      {
        m_bDTSet = true;
        m_dtStart = DateTime.Now;
        ReadIncrements();
      }
      double dblMilliseconds = ElapsedMilliSeconds();
      m_alActualTimes.Add(dblMilliseconds);
      m_dblLastCompletionFraction = m_dblCompletionFraction;
      if (m_alPreviousCompletionFraction != null && m_iIndex 
             < m_alPreviousCompletionFraction.Count )
        m_dblCompletionFraction = (double)m_alPreviousCompletionFraction[m_iIndex++];
      else
        m_dblCompletionFraction = (m_iIndex > 0) ? 1 : 0;
    }

    // Utility function to return elapsed Milliseconds since the 
    // SplashScreen was launched.
    private double ElapsedMilliSeconds()
    {
      TimeSpan ts = DateTime.Now - m_dtStart;
      return ts.TotalMilliseconds;
    }

    // Function to read the checkpoint intervals from the previous invocation of the
    // splashscreen from the XML file.
    private void ReadIncrements()
    {
      string sPBIncrementPerTimerInterval = SplashScreenXMLStorage.Interval;
      double dblResult;

      if (Double.TryParse(sPBIncrementPerTimerInterval, 
                System.Globalization.NumberStyles.Float,
                System.Globalization.NumberFormatInfo.InvariantInfo, out dblResult) == true)
        m_dblPBIncrementPerTimerInterval = dblResult;
      else
        m_dblPBIncrementPerTimerInterval = .0015;

      string sPBPreviousPctComplete = SplashScreenXMLStorage.Percents;

      if (sPBPreviousPctComplete != "")
      {
        string[] aTimes = sPBPreviousPctComplete.Split(null);
        m_alPreviousCompletionFraction = new ArrayList();

        for (int i = 0; i < aTimes.Length; i++)
        {
          double dblVal;
          if (Double.TryParse(aTimes[i], 
                  System.Globalization.NumberStyles.Float,
                  System.Globalization.NumberFormatInfo.InvariantInfo, out dblVal) == true)
            m_alPreviousCompletionFraction.Add(dblVal);
          else
            m_alPreviousCompletionFraction.Add(1.0);
        }
      }
      else
      {
        m_bFirstLaunch = true;
        m_sTimeRemaining = "";
      }
    }

    // Method to store the intervals (in percent complete) from the current invocation of
    // the splash screen to XML storage.
    private void StoreIncrements()
    {
      string sPercent = "";
      double dblElapsedMilliseconds = ElapsedMilliSeconds();
      for (int i = 0; i < m_alActualTimes.Count; i++)
        sPercent += ((double)m_alActualTimes[i] / dblElapsedMilliseconds)
                .ToString("0.####", System.Globalization.NumberFormatInfo.InvariantInfo) + " ";

      SplashScreenXMLStorage.Percents = sPercent;

      m_dblPBIncrementPerTimerInterval = 1.0 / (double)m_iActualTicks;

      SplashScreenXMLStorage.Interval = m_dblPBIncrementPerTimerInterval
                .ToString("#.000000", System.Globalization.NumberFormatInfo.InvariantInfo);
    }

    public static SplashScreen GetSplashScreen()
    {
      return ms_frmSplash;
    }

    #endregion Private Methods

    #region Event Handlers
    // Tick Event for the Timer control.  Handle fade in and fade out and paint progress bar. 
    private void UpdateTimer_Tick(object sender, System.EventArgs e)
    {
      lblStatus.Text = m_sStatus;

      // Calculate opacity
      if (m_dblOpacityIncrement > 0)    // Starting up splash screen
      {
        m_iActualTicks++;
        if (this.Opacity < 1)
          this.Opacity += m_dblOpacityIncrement;
      }
      else // Closing down splash screen
      {
        if (this.Opacity > 0)
          this.Opacity += m_dblOpacityIncrement;
        else
        {
          StoreIncrements();
          UpdateTimer.Stop();
          this.Close();
        }
      }

      // Paint progress bar
      if (m_bFirstLaunch == false && m_dblLastCompletionFraction < m_dblCompletionFraction)
      {
        m_dblLastCompletionFraction += m_dblPBIncrementPerTimerInterval;
        int width = (int)Math.Floor(pnlStatus.ClientRectangle.Width
                                    * m_dblLastCompletionFraction);
        int height = pnlStatus.ClientRectangle.Height;
        int x = pnlStatus.ClientRectangle.X;
        int y = pnlStatus.ClientRectangle.Y;
        if (width > 0 && height > 0)
        {
          m_rProgress = new Rectangle(x, y, width, height);
          if (!pnlStatus.IsDisposed)
          {
            Graphics g = pnlStatus.CreateGraphics();
            LinearGradientBrush brBackground = 
                      new LinearGradientBrush(m_rProgress, 
                                  Color.FromArgb(58, 96, 151), 
                                  Color.FromArgb(181, 237, 254), 
                                  LinearGradientMode.Horizontal);
            g.FillRectangle(brBackground, m_rProgress);
            g.Dispose();
          }
          int iSecondsLeft = 1 + (int)(TIMER_INTERVAL * 
              ((1.0 - m_dblLastCompletionFraction) / m_dblPBIncrementPerTimerInterval)) / 1000;
          m_sTimeRemaining = (iSecondsLeft == 1) ? 
                         string.Format("1 second remaining") : 
                         string.Format("{0} seconds remaining", iSecondsLeft);
        }
      }
      lblTimeRemaining.Text = m_sTimeRemaining;
    }

    // Close the form if they double click on it.
    private void SplashScreen_DoubleClick(object sender, System.EventArgs e)
    {
      // Use the overload that doesn't set the parent form to this very window.
      CloseForm();
    }
    #endregion Event Handlers
  }

  #region Auxiliary Classes 
  /// <summary>
  /// A specialized class for managing XML storage for the splash screen.
  /// </summary>
  internal class SplashScreenXMLStorage
  {
    private static string ms_StoredValues = "SplashScreen.xml";
    private static string ms_DefaultPercents = "";
    private static string ms_DefaultIncrement = ".015";


    // Get or set the string storing the percentage complete at each checkpoint.
    static public string Percents
    {
      get { return GetValue("Percents", ms_DefaultPercents); }
      set { SetValue("Percents", value); }
    }
    // Get or set how much time passes between updates.
    static public string Interval
    {
      get { return GetValue("Interval", ms_DefaultIncrement); }
      set { SetValue("Interval", value); }
    }

    // Store the file in a location where it can be written with only User rights.
    // (Don't use install directory).
    static private string StoragePath
    {
      get {return Path.Combine(Application.UserAppDataPath, ms_StoredValues);}
    }

    // Helper method for getting inner text of named element.
    static private string GetValue(string name, string defaultValue)
    {
      if (!File.Exists(StoragePath))
        return defaultValue;

      try
      {
        XmlDocument docXML = new XmlDocument();
        docXML.Load(StoragePath);
        XmlElement elValue = docXML.DocumentElement.SelectSingleNode(name) as XmlElement;
        return (elValue == null) ? defaultValue : elValue.InnerText;
      }
      catch
      {
        return defaultValue;
      }
    }

    // Helper method for setting inner text element.  Creates XML file if it doesn't exist.
    static public void SetValue(string name,
       string stringValue)
    {
      XmlDocument docXML = new XmlDocument();
      XmlElement elRoot = null;
      if (!File.Exists(StoragePath))
      {
        elRoot = docXML.CreateElement("root");
        docXML.AppendChild(elRoot);
      }
      else
      {
        docXML.Load(StoragePath);
        elRoot = docXML.DocumentElement;
      }
      XmlElement value = docXML.DocumentElement.SelectSingleNode(name) as XmlElement;
      if (value == null)
      {
        value = docXML.CreateElement(name);
        elRoot.AppendChild(value);
      }
      value.InnerText = stringValue;
      docXML.Save(StoragePath);
    }
  }
  #endregion Auxiliary Classes
}

更新

本文最初写于10多年前,当时.NET的当前版本是1.1。随着时间的推移,读者在下面的评论中报告了许多尚未纳入本文的缺陷和建议的变通方法。然而,几年前,Mahin Gupta根据这些评论更新了源代码并进行了修复和更改,并提供了一个指向该源代码的链接,许多人下载并使用了它。本次更新借鉴了该源代码,并将更改整合到本文的文本中,同时更新了包含代码的附加zip文件。

我要感谢Mahin以及许多在下面发布了错误报告和修复的读者。本次更新中包含的一些更改包括

  • 修复了跨线程疏忽(lblTimeRemaining标签被直接设置。)
  • 避免使用Windows注册表的替代存储(Colin Stansfield, Lagrange)
  • 延迟更新直到启动画面线程启动(daws)
  • 在关闭窗体之前调用timer.Stop()(kilroytrout)
  • 在定时器滴答事件期间绘制进度条(kilroytrout, cbschuld
  • 剩余时间精度改进(Pacman69
  • 双击关闭启动画面故障(Natural_Demon)

我没有实现一个建议。一些评论指出,当启动画面关闭时(在MDI应用程序中)会出现焦点问题。建议是在关闭窗口的过程中,将启动画面的所有者设置为相应的父窗体。我尝试了所有提出的修复(我认为),但找不到有效的方法。问题似乎是设置窗体的所有者会调用所有者上的代码(AddOwnedForm),这在线程不安全。因此,即使您在调用过程中执行此操作,操作也会因跨线程异常而失败。我在网上找到的唯一“修复”是在操作期间关闭异常(ControlCheckForIllegalCrossThreadCalls = false;)。我决定这可能不明智。因此,要么跨线程窗体所有权不可行,要么我无法弄清楚。任何解决此问题的建议都将不胜感激。在测试项目中,我发现调用this.Activate(),然后在调用SplashScreen.CloseForm()之前就足够了。

历史

  • 2003-11-16 第一版。
  • 2003-11-18 修正了一些拼写错误,并澄清了应用程序首次调用时的行为。更改代码,以便在第一次加载时不显示进度条。
  • 2003-11-20 在Quentin Pouplard的评论(下方)的基础上进行了改进和错误修复。
  • 2003-12-23 添加了dzCepheus提供的图形(下方)。
  • 2013-07-28 审查了评论部分提供的建议和错误修复,并进行了相应的更新。
© . All rights reserved.