关于.net:C#中的多线程启动画面?

关于.net:C#中的多线程启动画面?

Multi-threaded splash screen in C#?

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

我已经阅读了很多有关线程的文章,但是我对应该在哪里控制(main()方法?)感到迷茫。我也缺少Application.Run()的工作方式,这是应该从中创建线程的地方吗?现在,如果带有系统任务栏控件的表单是"活动"表单,那么飞溅应该从那里来吗?反正要等到表格完成才加载吗?

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


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

  • 具有从Microsoft.VisualBasic.WindowsFormsApplicationBase继承的主要形式
  • 覆盖" OnCreateSplashScreen"方法,如下所示:

    1
    2
    3
    4
    5
    protected override void OnCreateSplashScreen()
    {
        this.SplashScreen = new SplashForm();
        this.SplashScreen.TopMost = true;
    }
  • 非常简单,它会在加载过程中显示您的SplashForm(您需要创建),然后在主表单完成加载后自动将其关闭。

    这确实使事情变得简单,并且VisualBasic.WindowsFormsApplicationBase当然已通过Microsoft的良好测试,并且具有许多功能,即使在100%C#的应用程序中,也可以使您在Winforms中的工作变得更加轻松。

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


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

    假设Form1-是您的主要表单,而SplashForm是顶级表单,则边框很漂亮:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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();
    }


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

    FormSplash.cs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
        ...

        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 Terminal Server上,主应用程序表单将在后台显示(在初始屏幕之后),并且任务栏将闪烁。将窗口置于代码的前景/焦点是Google / SO可以利用的另一种蠕虫。


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

    这是我最终拼凑而成的内容:

    App.XAML:

    1
    <Application Startup="ApplicationStart"

    App.XAML.cs:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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();
      }

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

    引用MSDN:

    Activating a form brings it to the
    front if this is the active
    application, or it flashes the window
    caption if this is not the active
    application. The form must be visible
    for this method to have any effect.

    如果您不激活主窗体,则该窗体可能会显示在其他任何打开的窗口后面,从而显得有些愚蠢。


    我认为使用aku或Guy这类方法是可行的方法,但是有一些需要从特定示例中删除的东西:

  • 基本前提是尽快将您的飞溅显示在单独的线程上。这就是我学习的方式,类似于aku所说明的那样,因为这是我最熟悉的方式。我不知道Guy提到的VB功能。而且,甚至认为这是一个VB库,他是对的-最后都是IL。因此,即使感觉很脏也不是那么糟糕! :)我想您将要确保VB为该覆盖提供单独的线程,或者您自己创建一个线程-一定要对此进行研究。

  • 假设您创建另一个线程来显示此启动画面,则需要注意跨线程UI更新。我提到这一点是因为您提到了更新进度。基本上,为了安全起见,您需要使用委托在启动表单上调用一个(创建的)更新函数。您可以将该委托传递给初始屏幕的表单对象上的Invoke函数。实际上,如果直接调用启动表单以更新其上的进度/ UI元素,则将在运行.Net 2.0 CLR的情况下出现异常。根据经验,表单上的任何UI元素都必须由创建该表单的线程进行更新-这就是Form.Invoke确保的内容。

  • 最后,我可能会选择在代码的main方法中创建启动文件(如果不使用VB重载)。对我来说,这比让主窗体执行对象的创建并将其紧密绑定更好。如果采用这种方法,建议您创建一个由初始屏幕实现的简单接口,例如IStartupProgressListener之类,该接口通过成员函数接收启动进度更新。这将使您可以根据需要轻松地换入/换出两个类,并很好地解耦代码。如果您在启动完成时发出通知,启动表还可以知道何时关闭自身。


    一种简单的方法是将以下内容用作main():

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <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)做两件事:

  • 使用已经执行main()的线程作为GUI线程,启动Windows"消息泵"。
  • 将您的"主表单"指定为应用程序的"关机表单"。如果用户关闭该窗体,则Application.Run()终止,控制权返回到main(),您可以在其中执行所需的任何关闭操作。
  • 不需要产生线程来维护启动窗口,实际上这是一个坏主意,因为那样的话,您将必须使用线程安全的技术来从main()更新启动内容。

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


    1
    2
    3
    4
    5
    6
    7
    8
    private void MainForm_Load(object sender, EventArgs e)
    {
         FormSplash splash = new FormSplash();
         splash.Show();
         splash.Update();
         System.Threading.Thread.Sleep(3000);
         splash.Hide();
    }

    我从某个地方的互联网上获得了此信息,但似乎无法再次找到它。简单但有效。


    我在codeproject的应用程序中发布了有关启动画面合并的文章。它是多线程的,也许您感兴趣

    C#中的另一个启动画面


    我非常喜欢Aku的答案,但是该代码适用于C#3.0及更高版本,因为它使用了lambda函数。对于需要使用C#2.0中的代码的人,这里的代码使用匿名委托而不是lambda函数。您需要一个名为formSplashFormBorderStyle = None的最高Winform。表单的TopMost = True参数很重要,因为初始屏幕可能看起来像它出现的样子,如果不是最顶部的则迅速消失。我还选择了StartPosition=CenterScreen,因此它看起来像专业应用程序对启动屏幕的处理方式。如果想要更凉爽的效果,可以使用TrasparencyKey属性制作不规则形状的启动画面。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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();
      }

    我不同意推荐WindowsFormsApplicationBase的其他答案。以我的经验,它可能会使您的应用程序变慢。确切地说,当它与初始屏幕并行运行窗体的构造函数时,它将推迟窗体的Shown事件。

    考虑一个应用程序(没有启动屏幕),该应用程序的构造函数需要1秒,而Shown上的事件处理程序则需要2秒。此应用程序在3秒钟后可用。

    但是,假设您使用WindowsFormsApplicationBase安装了启动屏幕。您可能会认为3秒的MinimumSplashScreenDisplayTime是明智的,并且不会减慢您的应用程序的速度。但是,尝试一下,您的应用程序现在将需要5秒钟加载。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class App : WindowsFormsApplicationBase
    {
        protected override void OnCreateSplashScreen()
        {
            this.MinimumSplashScreenDisplayTime = 3000; // milliseconds
            this.SplashScreen = new Splash();
        }

        protected override void OnCreateMainForm()
        {
            this.MainForm = new Form1();
        }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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事件并行运行启动程序。


    实际上,这里不需要进行多线程。

    每当您想更新初始屏幕时,让您的业务逻辑生成一个事件。

    然后,让您的窗体使用挂钩到事件处理程序的方法相应地更新启动屏幕。

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

    这样一来,您可以轻松更改初始屏幕,而不会出现多线程麻烦。

    实际上,您甚至可以支持,例如,初始表格上的gif图像。为了使其工作,请在处理程序中调用Application.DoEvents():

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


    推荐阅读