【问题标题】:Multi-threaded splash screen in C#?C#中的多线程启动画面?
【发布时间】:2010-09-08 02:44:39
【问题描述】:

我希望在加载应用程序时显示启动画面。我有一个带有系统托盘控件的表单。我希望在此表单加载时显示初始屏幕,这需要一些时间,因为它正在访问 Web 服务 API 以填充一些下拉列表。我还想在加载之前对依赖关系做一些基本的测试(即web服务可用,配置文件可读)。随着启动的每个阶段的进行,我想用进度更新启动画面。

我已经阅读了很多关于线程的内容,但是我迷失了应该从哪里控制它(main() 方法?)。我也错过了Application.Run() 的工作原理,这是应该从这里创建线程的地方吗?现在,如果带有系统托盘控件的窗体是“活”窗体,那么飞溅应该来自那里吗?在表单完成之前它不会加载吗?

我不是在寻找代码讲义,更多的是一种算法/方法,所以我可以一劳永逸地解决这个问题:)

【问题讨论】:

标签: c# .net winforms multithreading splash-screen


【解决方案1】:

嗯,对于我过去部署的 ClickOnce 应用程序,我们使用 Microsoft.VisualBasic 命名空间来处理启动屏幕线程。您可以在 .NET 2.0 中引用和使用 C# 中的 Microsoft.VisualBasic 程序集,它提供了很多不错的服务。

  1. 让主窗体继承自Microsoft.VisualBasic.WindowsFormsApplicationBase
  2. 像这样覆盖“OnCreateSplashScreen”方法:

    protected override void OnCreateSplashScreen()
    {
        this.SplashScreen = new SplashForm();
        this.SplashScreen.TopMost = true;
    }
    

非常简单,它会在加载过程中显示您的 SplashForm(您需要创建它),然后在主表单完成加载后自动关闭它。

这确实让事情变得简单,VisualBasic.WindowsFormsApplicationBase 当然已经过 Microsoft 的良好测试,并且有很多功能可以让您在 Winforms 中的生活变得更加轻松,即使在 100% C# 的应用程序中也是如此。

归根结底,都是 IL 和 bytecode,所以为什么不使用它呢?

【讨论】:

  • 这是最优雅的解决方案,因为它使用了由 Visual Basic 库很好地抽象出来的内置功能。我不知道如何创建一个 VB ApplicationBase 程序,所以需要更多的研究......我将添加更多信息以确保完整性。感谢所有伟大的反馈!
  • 我不得不修复一些写得很糟糕的手动启动画面——使用 Guy 建议的受支持的功能绝对是一个很棒的方法。 :)
  • 需要在启动画面中显示进度条。所以我创建了一个公共属性来设置 progressBar1.Value 并从主 frmSplash splash = this.SplashScreen as frmSplashform; 访问它然后, splash.Progress = 50 这将引发 invalidOperation 异常。如何解决这个问题?
【解决方案2】:

诀窍是创建负责启动屏幕显示的单独线程。
当您运行您的应用程序 .net 时,会创建主线程并加载指定的(主)表单。为了隐藏繁重的工作,您可以隐藏主窗体,直到加载完成。

假设 Form1 - 是您的主要表单,SplashForm 是顶级表单,则为漂亮的启动表单设置边框:

private void Form1_Load(object sender, EventArgs e)
{
    Hide();
    bool done = false;
    ThreadPool.QueueUserWorkItem((x) =>
    {
        using (var splashForm = new SplashForm())
        {
            splashForm.Show();
            while (!done)
                Application.DoEvents();
            splashForm.Close();
        }
    });

    Thread.Sleep(3000); // Emulate hardwork
    done = true;
    Show();
}

【讨论】:

  • 这可能是我见过的用于制作启动画面的最佳方法。与我的袖珍 PC 应用程序一起在 .NET compact 上完美运行。
  • @aku:在应用程序中使用 Application.DoEvents() 是个好主意吗?我读了很多关于不在互联网上使用此功能的信息,例如blogs.msdn.com/jfoscoding/archive/2005/08/06/448560.aspx
  • 在非 UI 线程中创建表单可能不是一个好主意...最终可能会陷入痛苦的世界:)
  • +1 该死的多线程很难。 'net 上的大多数解决方案 - 包括两个 codeproject 项目 (google 上的前两个结果) 和这里的大多数其他答案 - 都有 可能调用 CloseSplashScreen 的竞争条件 之前 ShowSplashScreen! 这和接受的解决方案都应该工作,但是我们在接受的解决方案上遇到了问题,所以这个答案有很大帮助。谢谢!
  • Application.DoEvents() 上循环是对 CPU 时间的严重浪费。这个答案应该被使用,不幸的是它的投票率如此之高。 This answer 是一个更好的选择(即使用Application.Run() 来处理消息泵送)。
【解决方案3】:

在通过 Google 和 SO 寻找解决方案后,我最喜欢的是: http://bytes.com/topic/c-sharp/answers/277446-winform-startup-splash-screen

FormSplash.cs:

public partial class FormSplash : Form
{
    private static Thread _splashThread;
    private static FormSplash _splashForm;

    public FormSplash() {
        InitializeComponent();
    }

    /// <summary>
    /// Show the Splash Screen (Loading...)
    /// </summary>
    public static void ShowSplash()
    {
        if (_splashThread == null)
        {
            // show the form in a new thread
            _splashThread = new Thread(new ThreadStart(DoShowSplash));
            _splashThread.IsBackground = true;
            _splashThread.Start();
        }
    }

    // called by the thread
    private static void DoShowSplash()
    {
        if (_splashForm == null)
            _splashForm = new FormSplash();

        // create a new message pump on this thread (started from ShowSplash)
        Application.Run(_splashForm);
    }

    /// <summary>
    /// Close the splash (Loading...) screen
    /// </summary>
    public static void CloseSplash()
    {
        // need to call on the thread that launched this splash
        if (_splashForm.InvokeRequired)
            _splashForm.Invoke(new MethodInvoker(CloseSplash));

        else
            Application.ExitThread();
    }
}

Program.cs:

static class Program
{
    /// <summary>
    /// The main entry point for the application.
    /// </summary>
    [STAThread]
    static void Main(string[] args)
    {
        // splash screen, which is terminated in FormMain
        FormSplash.ShowSplash();

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        // this is probably where your heavy lifting is:
        Application.Run(new FormMain());
    }
}

FormMain.cs

    ...

    public FormMain()
    {
        InitializeComponent();            

        // bunch of database access, form loading, etc
        // this is where you could do the heavy lifting of "loading" the app
        PullDataFromDatabase();
        DoLoadingWork();            

        // ready to go, now close the splash
        FormSplash.CloseSplash();
    }

我遇到了Microsoft.VisualBasic 解决方案的问题——在 XP 上可以找到,但在 Windows 2003 终端服务器上,主应用程序窗体会在后台显示(在启动屏幕之后),并且任务栏会闪烁。在代码中将窗口置于前台/焦点是另一种您可以通过 Google/SO 找到的蠕虫。

【讨论】:

    【解决方案4】:

    这是一个老问题,但在尝试为 WPF 寻找可能包含动画的线程启动屏幕解决方案时,我一直遇到这个问题。

    这是我最终拼凑起来的:

    App.XAML:

    <Application Startup="ApplicationStart" …
    

    App.XAML.cs:

    void ApplicationStart(object sender, StartupEventArgs e)
    {
            var thread = new Thread(() =>
            {
                Dispatcher.CurrentDispatcher.BeginInvoke ((Action)(() => new MySplashForm().Show()));
                Dispatcher.Run();
            });
            thread.SetApartmentState(ApartmentState.STA);
            thread.IsBackground = true;
            thread.Start();
    
            // call synchronous configuration process
            // and declare/get reference to "main form"
    
            thread.Abort();
    
            mainForm.Show();
            mainForm.Activate();
      }
    

    【讨论】:

      【解决方案5】:

      我建议在aku提供的答案中直接在最后一个Show();之后调用Activate();

      引用 MSDN:

      激活表单会将其带到 前面如果这是活动的 应用程序,或者它会闪烁窗口 标题如果这不是活动的 应用。表格必须可见 让这个方法有任何效果。

      如果您不激活您的主窗体,它可能会显示在任何其他打开的窗口之后,使它看起来有点傻。

      【讨论】:

        【解决方案6】:

        我认为使用aku'sGuy's 之类的方法是可行的方法,但有几点需要从具体示例中删除:

        1. 基本前提是尽快在单独的线程上显示您的启动画面。这就是我会倾斜的方式,类似于 aku 的插图,因为这是我最熟悉的方式。我不知道 Guy 提到的 VB 函数。而且,即使认为这是一个 VB 库,他也是对的——最终都是 IL。所以,即使感觉 也没有那么糟糕! :) 我认为您需要确保 VB 为该覆盖提供了一个单独的线程,或者您自己创建了一个线程 - 一定要研究一下。

        2. 假设您创建另一个线程来显示此启动画面,您需要小心跨线程 UI 更新。我提出这个是因为你提到了更新进度。基本上,为了安全起见,您需要使用委托在启动表单上调用(您创建的)更新函数。您将该委托传递给初始屏幕表单对象上的Invoke 函数。事实上,如果您直接调用启动表单来更新其上的进度/UI 元素,那么如果您在 .Net 2.0 CLR 上运行,您将得到一个异常。根据经验,表单上的任何 UI 元素都必须由创建它的线程更新——这就是 Form.Invoke 所保证的。

        最后,我可能会选择在代码的 main 方法中创建启动画面(如果不使用 VB 重载)。对我来说,这比让主窗体执行对象的创建并如此紧密地绑定到它要好。如果您采用这种方法,我建议创建一个启动屏幕实现的简单接口——类似于 IStartupProgressListener——它通过成员函数接收启动进度更新。这将允许您根据需要轻松地换入/换出任一类,并很好地解耦代码。如果您在启动完成时发出通知,则启动表单也可以知道何时关闭。

        【讨论】:

          【解决方案7】:

          一种简单的方法是使用类似这样的东西作为 main():

          <STAThread()> Public Shared Sub Main()
          
              splash = New frmSplash
              splash.Show()
          
              ' Your startup code goes here...
          
              UpdateSplashAndLogMessage("Startup part 1 done...")
          
              ' ... and more as needed...
          
              splash.Hide()
              Application.Run(myMainForm)
          End Sub
          

          当 .NET CLR 启动您的应用程序时,它会创建一个“主”线程并开始在该线程上执行您的 main()。最后的 Application.Run(myMainForm) 做了两件事:

          1. 使用一直在执行 main() 的线程作为 GUI 线程启动 Windows“消息泵”。
          2. 将您的“主表单”指定为应用程序的“关闭表单”。如果用户关闭该表单,则 Application.Run() 将终止并且控制权返回到您的 main(),您可以在其中执行任何您想要的关闭操作。

          没有必要产生线程来处理启动窗口,实际上这是一个坏主意,因为那样您将不得不使用线程安全技术从 main() 更新启动内容。

          如果您需要其他线程在您的应用程序中执行后台操作,您可以从 main() 生成它们。只要记住将 Thread.IsBackground 设置为 True,这样当主/GUI 线程终止时它们就会死掉。否则,您必须自己安排终止所有其他线程,否则当主线程终止时,它们将使您的应用程序保持活动状态(但没有 GUI)。

          【讨论】:

            【解决方案8】:

            我在 codeproject 上发布了一篇关于在应用程序中加入启动画面的文章。它是多线程的,您可能会感兴趣

            Yet Another Splash Screen in C#

            【讨论】:

              【解决方案9】:
              private void MainForm_Load(object sender, EventArgs e)
              {
                   FormSplash splash = new FormSplash();
                   splash.Show();
                   splash.Update();
                   System.Threading.Thread.Sleep(3000);
                   splash.Hide();
              }
              

              我从 Internet 某处获得此信息,但似乎无法再次找到它。简单但有效。

              【讨论】:

                【解决方案10】:

                我非常喜欢 Aku 的回答,但代码适用于 C# 3.0 及更高版本,因为它使用 lambda 函数。对于需要使用 C# 2.0 中的代码的人,这里是使用匿名委托而不是 lambda 函数的代码。您需要一个名为formSplashFormBorderStyle = None 的最顶层winform。表单的TopMost = True 参数很重要,因为启动屏幕可能看起来像它出现然后如果它不是最上面的会很快消失。我也选择了StartPosition=CenterScreen,所以它看起来就像专业应用程序对启动画面所做的那样。如果你想要更酷的效果,你可以使用TrasparencyKey 属性来制作一个不规则形状的闪屏。

                private void formMain_Load(object sender, EventArgs e)
                  {
                
                     Hide();
                     bool done = false;
                     ThreadPool.QueueUserWorkItem(delegate
                     {
                       using (formSplash splashForm = new formSplash())
                       {
                           splashForm.Show();
                           while (!done)
                              Application.DoEvents();
                           splashForm.Close();
                       }
                     }, null);
                
                     Thread.Sleep(2000);
                     done = true;
                     Show();
                  }
                

                【讨论】:

                  【解决方案11】:

                  我不同意推荐WindowsFormsApplicationBase 的其他答案。根据我的经验,它可能会减慢您的应用程序。准确地说,虽然它与启动屏幕并行运行表单的构造函数,但它会推迟表单的 Shown 事件。

                  考虑一个应用程序(没有启动画面),其构造函数需要 1 秒,而 Shown 上的事件处理程序需要 2 秒。 3 秒后即可使用此应用。

                  但假设您使用WindowsFormsApplicationBase 安装启动画面。您可能认为 3 秒的 MinimumSplashScreenDisplayTime 是明智的,不会减慢您的应用程序。但是,试试看,您的应用现在需要 5 秒才能加载。


                  class App : WindowsFormsApplicationBase
                  {
                      protected override void OnCreateSplashScreen()
                      {
                          this.MinimumSplashScreenDisplayTime = 3000; // milliseconds
                          this.SplashScreen = new Splash();
                      }
                  
                      protected override void OnCreateMainForm()
                      {
                          this.MainForm = new Form1();
                      }
                  }
                  

                  public Form1()
                  {
                      InitializeComponent();
                      Shown += Form1_Shown;
                      Thread.Sleep(TimeSpan.FromSeconds(1));
                  }
                  
                  void Form1_Shown(object sender, EventArgs e)
                  {
                      Thread.Sleep(TimeSpan.FromSeconds(2));
                      Program.watch.Stop();
                      this.textBox1.Text = Program.watch.ElapsedMilliseconds.ToString();
                  }
                  

                  结论:如果您的应用在 Slown 事件上有处理程序,请不要使用 WindowsFormsApplicationBase。您可以编写更好的代码,将启动画面与构造函数和 Shown 事件并行运行。

                  【讨论】:

                    【解决方案12】:

                    其实这里不需要多线程。

                    让您的业务逻辑在您想要更新启动画面时生成一个事件。

                    然后让您的表单在与事件处理程序挂钩的方法中相应地更新启动屏幕。

                    为了区分更新,您可以触发不同的事件或在继承自 EventArgs 的类中提供数据。

                    这样您就可以很好地更改启动画面,而不会出现任何多线程问题。

                    实际上,您甚至可以支持,例如,启动表单上的 gif 图像。为了让它工作,在你的处理程序中调用 Application.DoEvents():

                    private void SomethingChanged(object sender, MyEventArgs e)
                    {
                        formSplash.Update(e);
                        Application.DoEvents(); //this will update any animation
                    }
                    

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多