[英]Multi-threaded splash screen in C#?
我希望在加载应用程序时显示启动画面。 我有一个带有系统托盘控件的表单。 我希望在加载此表单时显示启动画面,这需要一些时间,因为它正在访问 Web 服务 API 以填充一些下拉列表。 我还想在加载之前对依赖做一些基本的测试(即web服务可用,配置文件可读)。 随着启动的每个阶段的进行,我想随着进度更新启动画面。
我已经阅读了很多关于线程的文章,但是我迷失了应该从哪里控制它( main()
方法?)。 我也缺少Application.Run()
工作方式,这是应该从哪里创建线程的? 现在,如果带有系统托盘控件的表单是“活的”表单,那么飞溅应该来自那里吗? 无论如何,它不会在表格完成之前加载吗?
我不是在寻找代码讲义,而是更多的算法/方法,所以我可以一劳永逸地解决这个问题:)
诀窍是创建负责启动屏幕显示的单独线程。
当您运行 app .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();
}
好吧,对于我过去部署的 ClickOnce 应用程序,我们使用Microsoft.VisualBasic
命名空间来处理启动画面线程。 您可以在 .NET 2.0 中从 C# 引用和使用Microsoft.VisualBasic
程序集,它提供了许多不错的服务。
Microsoft.VisualBasic.WindowsFormsApplicationBase
继承像这样覆盖“OnCreateSplashScreen”方法:
protected override void OnCreateSplashScreen() { this.SplashScreen = new SplashForm(); this.SplashScreen.TopMost = true; }
非常简单,它在加载过程中显示您的 SplashForm(您需要创建),然后在主表单完成加载后自动关闭它。
这确实使事情变得简单,而且VisualBasic.WindowsFormsApplicationBase
当然经过了 Microsoft 的充分测试,并且具有许多功能,可以使您在 Winforms 中的生活变得更加轻松,即使在 100% C# 的应用程序中也是如此。
归根结底,无论如何都是 IL 和bytecode
,那么为什么不使用它呢?
在 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();
}
}
程序.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 搜索的另一类蠕虫。
这是一个老问题,但在尝试为 WPF 寻找可能包含动画的线程闪屏解决方案时,我不断遇到它。
这是我最终拼凑出来的:
应用程序.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();
}
我建议调用Activate();
紧接在最后一个Show();
在aku提供的答案中。
引用 MSDN:
如果这是活动应用程序,则激活表单会将其置于最前面,如果这不是活动应用程序,则它会闪烁窗口标题。 表单必须可见,此方法才能生效。
如果您不激活主窗体,它可能会显示在任何其他打开的窗口后面,使其看起来有点傻。
我认为使用像aku's或Guy's这样的方法是可行的方法,但从具体示例中需要注意以下几点:
基本前提是尽快在单独的线程上显示您的飞溅。 这就是我的倾斜方式,类似于 aku 的插图,因为这是我最熟悉的方式。 我不知道 Guy 提到的 VB 函数。 而且,即使认为它是一个VB库,他也是对的——最终都是 IL。 所以,即使感觉很脏,也不是那么糟糕! :) 我想你会想确定要么VB为该覆盖提供了一个单独的线程,要么你自己创建一个——当然要研究一下。
假设您创建另一个线程来显示此启动画面,您将需要小心跨线程 UI 更新。 我提出这个是因为你提到了更新进度。 基本上,为了安全起见,您需要使用委托在启动表单上调用更新函数(您创建的)。 您将该委托传递给初始屏幕表单对象上的Invoke函数。 事实上,如果您直接调用启动表单来更新它的进度/UI 元素,如果您在 .Net 2.0 CLR 上运行,您将得到一个异常。 根据经验,表单上的任何 UI 元素都必须由创建它的线程更新——这就是 Form.Invoke 的保证。
最后,我可能会选择在代码的 main 方法中创建启动(如果不使用 VB 重载)。 对我来说,这比让主窗体执行对象的创建并与它紧密绑定要好。 如果您采用这种方法,我建议您创建一个启动画面实现的简单界面——类似于 IStartupProgressListener——它通过成员函数接收启动进度更新。 这将允许您根据需要轻松地换入/换出任一类,并很好地解耦代码。 如果您在启动完成时发出通知,则飞溅表单还可以知道何时关闭自身。
一种简单的方法是使用这样的东西作为 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) 做了两件事:
没有必要产生一个线程来处理启动窗口,实际上这是一个坏主意,因为那样你就必须使用线程安全技术来更新 main() 中的启动内容。
如果您需要其他线程在您的应用程序中执行后台操作,您可以从 main() 中生成它们。 只需记住将 Thread.IsBackground 设置为 True,这样它们就会在主 / GUI 线程终止时死亡。 否则,您将不得不自己安排终止所有其他线程,否则当主线程终止时,它们将使您的应用程序保持活动状态(但没有 GUI)。
我在 codeproject 的应用程序中发布了一篇关于初始屏幕合并的文章。 它是多线程的,您可能会感兴趣
private void MainForm_Load(object sender, EventArgs e)
{
FormSplash splash = new FormSplash();
splash.Show();
splash.Update();
System.Threading.Thread.Sleep(3000);
splash.Hide();
}
我从某个地方的互联网上得到了这个,但似乎无法再次找到它。 简单但有效。
我非常喜欢 Aku 的回答,但代码适用于 C# 3.0 及更高版本,因为它使用了 lambda 函数。 对于需要使用 C# 2.0 中的代码的人,这里是使用匿名委托而不是 lambda 函数的代码。 您需要一个名为formSplash
的最顶层 winform,其中FormBorderStyle = 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();
}
我不同意其他推荐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 事件并行运行飞溅。
实际上这里的多线程是没有必要的。
每当您想要更新启动画面时,让您的业务逻辑生成一个事件。
然后让您的表单在挂钩到事件处理程序的方法中相应地更新启动画面。
为了区分更新,您可以触发不同的事件或在从 EventArgs 继承的类中提供数据。
通过这种方式,您可以在没有任何多线程问题的情况下拥有漂亮的更改启动画面。
实际上,您甚至可以支持,例如,启动窗体上的 gif 图像。 为了让它工作,在你的处理程序中调用 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.