60

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

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

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

4

12 回答 12

46

诀窍是创建负责启动屏幕显示的单独线程。
当您运行您的应用程序 .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();
}
于 2008-09-08T00:53:09.557 回答
46

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

  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无论如何,为什么不使用它呢?

于 2008-09-08T01:28:03.933 回答
14

在查看了谷歌和 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();
    }
}

程序.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 搜索到的另一种蠕虫病毒。

于 2011-02-14T17:01:42.570 回答
10

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

这是我最终拼凑起来的:

应用程序.XAML:

<Application Startup="ApplicationStart" …

应用程序.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();
  }
于 2010-07-21T04:44:53.703 回答
9

我建议在 aku 提供的答案Activate();的最后一个之后直接打电话。Show();

引用 MSDN:

如果这是活动应用程序,则激活表单会将其置于最前面,如果这不是活动应用程序,则它会闪烁窗口标题。表单必须可见,此方法才能生效。

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

于 2010-01-22T12:16:12.017 回答
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——它通过成员函数接收启动进度更新。这将允许您根据需要轻松地换入/换出任一类,并很好地解耦代码。如果您在启动完成时发出通知,则启动表单也可以知道何时关闭。

于 2008-09-08T02:18:28.663 回答
5

一种简单的方法是使用类似这样的东西作为 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)。

于 2008-09-08T00:51:53.787 回答
4

我在 codeproject 的应用程序中发布了一篇关于启动屏幕合并的文章。它是多线程的,可能会引起您的兴趣

C# 中的另一个启动画面

于 2010-01-22T12:41:42.363 回答
4
private void MainForm_Load(object sender, EventArgs e)
{
     FormSplash splash = new FormSplash();
     splash.Show();
     splash.Update();
     System.Threading.Thread.Sleep(3000);
     splash.Hide();
}

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

于 2011-07-19T08:08:12.283 回答
3

我非常喜欢 Aku 的回答,但代码适用于 C# 3.0 及更高版本,因为它使用 lambda 函数。对于需要使用 C# 2.0 中的代码的人,这里是使用匿名委托而不是 lambda 函数的代码。你需要一个最顶层的 winform调用formSplashFormBorderStyle = None表单的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();
  }
于 2011-11-22T21:27:34.640 回答
2

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

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

但假设您使用 WindowsFormsApplicationBase. 您可能认为MinimumSplashScreenDisplayTime3 秒是明智的,不会减慢您的应用程序。但是,试试看,你的应用现在需要 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();
}

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

于 2013-08-22T09:30:07.337 回答
0

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

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

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

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

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

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

private void SomethingChanged(object sender, MyEventArgs e)
{
    formSplash.Update(e);
    Application.DoEvents(); //this will update any animation
}
于 2017-09-04T08:49:06.330 回答