简体   繁体   English

C# 中的多线程启动画面?

[英]Multi-threaded splash screen in C#?

I want a splash screen to show while the application is loading.我希望在加载应用程序时显示启动画面。 I have a form with a system tray control tied to it.我有一个带有系统托盘控件的表单。 I want the splash screen to display while this form loads, which takes a bit of time since it's accessing a web service API to populate some drop-downs.我希望在加载此表单时显示启动画面,这需要一些时间,因为它正在访问 Web 服务 API 以填充一些下拉列表。 I also want to do some basic testing for dependencies before loading (that is, the web service is available, the configuration file is readable).我还想在加载之前对依赖做一些基本的测试(即web服务可用,配置文件可读)。 As each phase of the startup goes, I want to update the splash screen with progress.随着启动的每个阶段的进行,我想随着进度更新启动画面。

I have been reading a lot on threading, but I am getting lost on where this should be controlled from (the main() method?).我已经阅读了很多关于线程的文章,但是我迷失了应该从哪里控制它( main()方法?)。 I am also missing how Application.Run() works, is this where the threads for this should be created from?我也缺少Application.Run()工作方式,这是应该从哪里创建线程的? Now, if the form with the system tray control is the "living" form, should the splash come from there?现在,如果带有系统托盘控件的表单是“活的”表单,那么飞溅应该来自那里吗? Wouldn't it not load until the form is completed anyway?无论如何,它不会在表格完成之前加载吗?

I'm not looking for a code handout, more of an algorithm/approach so I can figure this out once and for all :)我不是在寻找代码讲义,而是更多的算法/方法,所以我可以一劳永逸地解决这个问题:)

The trick is to to create separate thread responsible for splash screen showing.诀窍是创建负责启动屏幕显示的单独线程。
When you run you app .net creates main thread and loads specified (main) form.当您运行 app .net 时,它会创建主线程并加载指定的(主)表单。 To conceal hard work you can hide main form until loading is done.为了隐藏繁重的工作,您可以隐藏主窗体,直到加载完成。

Assuming that Form1 - is your main form and SplashForm is top level, borderles nice splash form:假设 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();
}

Well, for a ClickOnce app that I deployed in the past, we used the Microsoft.VisualBasic namespace to handle the splash screen threading.好吧,对于我过去部署的 ClickOnce 应用程序,我们使用Microsoft.VisualBasic命名空间来处理启动画面线程。 You can reference and use the Microsoft.VisualBasic assembly from C# in .NET 2.0 and it provides a lot of nice services.您可以在 .NET 2.0 中从 C# 引用和使用Microsoft.VisualBasic程序集,它提供了许多不错的服务。

  1. Have the main form inherit from Microsoft.VisualBasic.WindowsFormsApplicationBase让主窗体从Microsoft.VisualBasic.WindowsFormsApplicationBase继承
  2. Override the "OnCreateSplashScreen" method like so:像这样覆盖“OnCreateSplashScreen”方法:

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

Very straightforward, it shows your SplashForm (which you need to create) while loading is going on, then closes it automatically once the main form has completed loading.非常简单,它在加载过程中显示您的 SplashForm(您需要创建),然后在主表单完成加载后自动关闭它。

This really makes things simple, and the VisualBasic.WindowsFormsApplicationBase is of course well tested by Microsoft and has a lot of functionality that can make your life a lot easier in Winforms, even in an application that is 100% C#.这确实使事情变得简单,而且VisualBasic.WindowsFormsApplicationBase当然经过了 Microsoft 的充分测试,并且具有许多功能,可以使您在 Winforms 中的生活变得更加轻松,即使在 100% C# 的应用程序中也是如此。

At the end of the day, it's all IL and bytecode anyway, so why not use it?归根结底,无论如何都是 IL 和bytecode ,那么为什么不使用它呢?

After looking all over Google and SO for solutions, this is my favorite: http://bytes.com/topic/c-sharp/answers/277446-winform-startup-splash-screen在 Google 和 SO 上寻找解决方案之后,这是我最喜欢的: http : //bytes.com/topic/c-sharp/answers/277446-winform-startup-splash-screen

FormSplash.cs: 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:程序.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 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();
    }

I had issues with the Microsoft.VisualBasic solution -- Worked find on XP, but on Windows 2003 Terminal Server, the main application form would show up (after the splash screen) in the background, and the taskbar would blink.我遇到了Microsoft.VisualBasic解决方案的问题——在 XP 上找到了工作,但在 Windows 2003 终端服务器上,主应用程序表单会在后台显示(在启动屏幕之后),并且任务栏会闪烁。 And bringing a window to foreground/focus in code is a whole other can of worms you can Google/SO for.并且在代码中为前景/焦点带来一个窗口是您可以使用 Google/SO 搜索的另一类蠕虫。

This is an old question, but I kept coming across it when trying to find a threaded splash screen solution for WPF that could include animation.这是一个老问题,但在尝试为 WPF 寻找可能包含动画的线程闪屏解决方案时,我不断遇到它。

Here is what I ultimately pieced together:这是我最终拼凑出来的:

App.XAML:应用程序.XAML:

<Application Startup="ApplicationStart" …

App.XAML.cs: 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();
  }

I recommend calling Activate();我建议调用Activate(); directly after the last Show();紧接在最后一个Show(); in the answer provided by aku.在aku提供的答案中。

Quoting MSDN:引用 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.表单必须可见,此方法才能生效。

If you don't activate your main form, it may be displayed behind any other open windows, making it look a bit silly.如果您不激活主窗体,它可能会显示任何其他打开的窗口后面,使其看起来有点傻。

I think using some method like aku's or Guy's is the way to go, but a couple of things to take away from the specific examples:我认为使用像aku'sGuy's这样方法是可行的方法,但从具体示例中需要注意以下几点:

  1. The basic premise would be to show your splash on a separate thread as soon as possible.基本前提是尽快在单独的线程上显示您的飞溅。 That's the way I would lean, similar to what aku's illustrated, since it's the way I'm most familiar with.这就是我的倾斜方式,类似于 aku 的插图,因为这是我最熟悉的方式。 I was not aware of the VB function Guy mentioned.我不知道 Guy 提到的 VB 函数。 And, even thought it's a VB library, he is right -- it's all IL in the end.而且,即使认为它是一个VB库,他也是对的——最终都是 IL。 So, even if it feels dirty it's not all that bad!所以,即使感觉很脏,也不是那么糟糕! :) I think you'll want to be sure that either VB provides a separate thread for in that override or that you create one yourself -- definitely research that. :) 我想你会想确定要么VB为该覆盖提供了一个单独的线程,要么你自己创建一个——当然要研究一下。

  2. Assuming you create another thread to display this splash, you will want to be careful of cross thread UI updates.假设您创建另一个线程来显示此启动画面,您将需要小心跨线程 UI 更新。 I bring this up because you mentioned updating progress.我提出这个是因为你提到了更新进度。 Basically, to be safe, you need to call an update function (that you create) on the splash form using a delegate.基本上,为了安全起见,您需要使用委托在启动表单上调用更新函数(您创建的)。 You pass that delegate to the Invoke function on your splash screen's form object.您将该委托传递给初始屏幕表单对象上的Invoke函数。 In fact if you call the splash form directly to update progress/UI elements on it, you'll get an exception provided you are running on the .Net 2.0 CLR.事实上,如果您直接调用启动表单来更新它的进度/UI 元素,如果您在 .Net 2.0 CLR 上运行,您将得到一个异常。 As a rule of thumb, any UI element on a form must be updated by the thread that created it -- that's what Form.Invoke insures.根据经验,表单上的任何 UI 元素都必须由创建它的线程更新——这就是 Form.Invoke 的保证。

Finally, I would likely opt to create the splash (if not using the VB overload) in the main method of your code.最后,我可能会选择在代码的 main 方法中创建启动(如果不使用 VB 重载)。 To me this is better than having the main form perform creation of the object and to be so tightly bound to it.对我来说,这比让主窗体执行对象的创建并与它紧密绑定要好。 If you take that approach, I'd suggest creating a simple interface that the splash screen implements -- something like IStartupProgressListener -- which receives start-up progress updates via a member function.如果您采用这种方法,我建议您创建一个启动画面实现的简单界面——类似于 IStartupProgressListener——它通过成员函数接收启动进度更新。 This will allow you to easily swap in/out either class as needed, and nicely decouples the code.这将允许您根据需要轻松地换入/换出任一类,并很好地解耦代码。 The splash form can also know when to close itself if you notify when start-up is complete.如果您在启动完成时发出通知,则飞溅表单还可以知道何时关闭自身。

One simple way is the use something like this as main():一种简单的方法是使用这样的东西作为 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

When the .NET CLR starts your application, it creates a 'main' thread and starts executing your main() on that thread.当 .NET CLR 启动您的应用程序时,它会创建一个“主”线程并开始在该线程上执行您的 main()。 The Application.Run(myMainForm) at the end does two things:最后的 Application.Run(myMainForm) 做了两件事:

  1. Starts the Windows 'message pump', using the thread that has been executing main() as the GUI thread.使用一直在执行 main() 作为 GUI 线程的线程启动 Windows '消息泵'。
  2. Designates your 'main form' as the 'shutdown form' for the application.将您的“主表单”指定为应用程序的“关闭表单”。 If the user closes that form, then the Application.Run() terminates and control returns to your main(), where you can do any shutdown you want.如果用户关闭该表单,则 Application.Run() 终止并且控制权返回到您的 main(),您可以在其中执行任何您想要的关闭操作。

There is no need to spawn a thread to take care of the splash window, and in fact this is a bad idea, because then you would have to use thread-safe techniques to update the splash contents from main().没有必要产生一个线程来处理启动窗口,实际上这是一个坏主意,因为那样你就必须使用线程安全技术来更新 main() 中的启动内容。

If you need other threads to do background operations in your application, you can spawn them from main().如果您需要其他线程在您的应用程序中执行后台操作,您可以从 main() 中生成它们。 Just remember to set Thread.IsBackground to True, so that they will die when the main / GUI thread terminates.只需记住将 Thread.IsBackground 设置为 True,这样它们就会在主 / GUI 线程终止时死亡。 Otherwise you will have to arrange to terminate all your other threads yourself, or they will keep your application alive (but with no GUI) when the main thread terminates.否则,您将不得不自己安排终止所有其他线程,否则当主线程终止时,它们将使您的应用程序保持活动状态(但没有 GUI)。

I posted an article on splash screen incorporation in the application at codeproject.我在 codeproject 的应用程序中发布了一篇关于初始屏幕合并的文章。 It is multithreaded and might be of your interest它是多线程的,您可能会感兴趣

Yet Another Splash Screen in C# C# 中的另一个启动画面

private void MainForm_Load(object sender, EventArgs e)
{
     FormSplash splash = new FormSplash();
     splash.Show();
     splash.Update();
     System.Threading.Thread.Sleep(3000);
     splash.Hide();
}

I got this from the Internet somewhere but cannot seem to find it again.我从某个地方的互联网上得到了这个,但似乎无法再次找到它。 Simple but yet effective.简单但有效。

I like Aku's answer a lot, but the code is for C# 3.0 and up since it uses a lambda function.我非常喜欢 Aku 的回答,但代码适用于 C# 3.0 及更高版本,因为它使用了 lambda 函数。 For people needing to use the code in C# 2.0, here's the code using anonymous delegate instead of the lambda function.对于需要使用 C# 2.0 中的代码的人,这里是使用匿名委托而不是 lambda 函数的代码。 You need a topmost winform called formSplash with FormBorderStyle = None .您需要一个名为formSplash的最顶层 winform,其中FormBorderStyle = None The TopMost = True parameter of the form is important because the splash screen might look like it appears then disappears quickly if it's not topmost.表单的TopMost = True参数很重要,因为初始屏幕可能看起来像它出现后很快消失,如果它不是最顶层。 I also choose StartPosition=CenterScreen so it looks like what a professional app would do with a splash screen.我还选择了StartPosition=CenterScreen因此它看起来就像专业应用程序对启动画面所做的那样。 If you want an even cooler effect, you can use the TrasparencyKey property to make an irregular shaped splash screen.如果您想要更酷的效果,可以使用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();
  }

I disagree with the other answers recommending WindowsFormsApplicationBase .我不同意其他推荐WindowsFormsApplicationBase答案。 In my experience, it can slow your app.根据我的经验,它会减慢您的应用程序的速度。 To be precise, while it runs your form's constructor in parallel with the splash screen, it postpone your form's Shown event.准确地说,当它与初始屏幕并行运行表单的构造函数时,它会推迟表单的 Shown 事件。

Consider an app (without splashs screen) with a constructor that takes 1 second and a event handler on Shown that takes 2 seconds.考虑一个应用程序(没有启动画面),其构造函数需要 1 秒,而 Shown 上的事件处理程序需要 2 秒。 This app is usable after 3 seconds.此应用程序在 3 秒后可用。

But suppose you install a splash screen using WindowsFormsApplicationBase .但是假设您使用WindowsFormsApplicationBase安装启动画面。 You might think MinimumSplashScreenDisplayTime of 3 seconds is sensible and won't slow your app.您可能认为 3 秒的MinimumSplashScreenDisplayTime是明智的,并且不会减慢您的应用程序的速度。 But, try it, your app will now take 5 seconds to load.但是,尝试一下,您的应用现在需要 5 秒才能加载。


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

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

and

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

Conclusion: don't use WindowsFormsApplicationBase if your app has a handler on the Slown event.结论:如果您的应用程序有处理 Slown 事件的处理程序,请不要使用WindowsFormsApplicationBase You can write better code that runs the splash in parallel to both the constructor and the Shown event.您可以编写更好的代码,与构造函数和 Shown 事件并行运行飞溅。

Actually mutlithreading here is not necessary.实际上这里的多线程是没有必要的。

Let your business logic generate an event whenever you want to update splash screen.每当您想要更新启动画面时,让您的业务逻辑生成一个事件。

Then let your form update the splash screen accordingly in the method hooked to eventhandler.然后让您的表单在挂钩到事件处理程序的方法中相应地更新启动画面。

To differentiate updates you can either fire different events or provide data in a class inherited from EventArgs.为了区分更新,您可以触发不同的事件或在从 EventArgs 继承的类中提供数据。

This way you can have nice changing splash screen without any multithreading headache.通过这种方式,您可以在没有任何多线程问题的情况下拥有漂亮的更改启动画面。

Actually with this you can even support, for example, gif image on a splash form.实际上,您甚至可以支持,例如,启动窗体上的 gif 图像。 In order for it to work, call Application.DoEvents() in your handler:为了让它工作,在你的处理程序中调用 Application.DoEvents() :

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM