Многопоточный экран-заставка в C#?
Я хочу, чтобы во время загрузки приложения отображался заставочный экран. У меня есть форма с привязкой к панели управления системой. Я хочу, чтобы заставка отображалась во время загрузки этой формы, что занимает некоторое время, так как он получает доступ к API веб-службы, чтобы заполнить некоторые раскрывающиеся списки. Я также хочу провести некоторое базовое тестирование зависимостей перед загрузкой (то есть веб-сервис доступен, файл конфигурации доступен для чтения). Поскольку каждая фаза запуска идет, я хочу обновить заставку с прогрессом.
Я много читал о потоках, но заблудился из-за того, откуда это должно контролироваться (метод main()?). Мне тоже не хватает как Application.Run()
работает, это где темы для этого должны быть созданы? Теперь, если форма с управлением в системном трее является "живой" формой, должен ли всплеск исходить оттуда? Разве он не загрузится, пока форма не будет заполнена в любом случае?
Я не ищу раздаточный материал, скорее алгоритм / подход, чтобы я мог понять это раз и навсегда:)
12 ответов
Что ж, для приложения ClickOnce, которое я развернул в прошлом, мы использовали пространство имен Microsoft.VisualBasic для обработки потоков на заставке. Вы можете ссылаться и использовать сборку Microsoft.VisualBasic из C# в.NET 2.0, и она предоставляет множество полезных сервисов.
- Основная форма наследуется от Microsoft.VisualBasic.WindowsFormsApplicationBase
Переопределите метод "OnCreateSplashScreen" следующим образом:
protected override void OnCreateSplashScreen() { this.SplashScreen = new SplashForm(); this.SplashScreen.TopMost = true; }
Очень просто, он показывает вашу SplashForm (которую вы должны создать) во время загрузки, а затем автоматически закрывает ее после завершения загрузки основной формы.
Это действительно упрощает задачу, и VisualBasic.WindowsFormsApplicationBase, конечно, хорошо протестирован Microsoft и обладает множеством функциональных возможностей, которые могут значительно облегчить вашу жизнь в Winforms, даже в приложении, которое на 100% C#.
В конце концов, это все равно IL и байт-код, так почему бы не использовать его?
Хитрость заключается в том, чтобы создать отдельный поток, отвечающий за показ заставки.
При запуске приложения.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();
}
После того, как я поищу решения в 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 Terminal Server, основная форма приложения будет отображаться (после заставки) в фоновом режиме, и панель задач будет мигать. А вывод окна на передний план / фокус в коде - это еще одна банка червей, для которой вы можете использовать Google/SO.
Я рекомендую звонить Activate();
сразу после последнего Show();
в ответе предоставил аку.
Цитирование MSDN:
Активация формы выводит ее на передний план, если это активное приложение, или мигает заголовок окна, если это не активное приложение. Форма должна быть видимой, чтобы этот метод имел какой-либо эффект.
Если вы не активируете свою основную форму, она может отображаться за любыми другими открытыми окнами, что делает ее немного глупой.
Это старый вопрос, но я продолжал сталкиваться с ним, пытаясь найти многопоточное решение для заставки 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();
}
Я думаю, что использование какого-либо метода, такого как aku или Guy's, - это путь, но есть пара вещей, которые нужно увести от конкретных примеров:
Основная предпосылка - показать ваш всплеск в отдельном потоке как можно скорее. Вот как я склоняюсь, подобно тому, как проиллюстрирован аку, так как это то, с чем я больше всего знаком. Я не знал о функции VB, о которой говорил Гай. И, даже если он думал, что это библиотека VB, он прав - в конце концов, это все IL. Так что, даже если это кажется грязным, это не так уж и плохо!:) Я думаю, вы захотите убедиться, что VB предоставляет отдельный поток для этого переопределения или что вы создаете его самостоятельно - определенно исследуйте это.
Предполагая, что вы создаете другой поток для отображения этого всплеска, вы должны быть осторожны с обновлениями интерфейса между потоками. Я поднял это, потому что вы упомянули прогресс обновления. По сути, для безопасности вам нужно вызвать функцию обновления (которую вы создаете) в форме заставки с использованием делегата. Вы передаете этот делегат функции Invoke в объекте формы вашего экрана-заставки. Фактически, если вы вызовете форму-заставку напрямую, чтобы обновить на ней элементы progress/UI, вы получите исключение, если вы работаете в.Net 2.0 CLR. Как правило, любой элемент пользовательского интерфейса в форме должен обновляться потоком, который его создал - это то, что обеспечивает Form.Invoke.
Наконец, я бы предпочел создать всплеск (если не использовать перегрузку 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) в конце делает две вещи:
- Запускает "насос сообщений" Windows, используя поток, который выполнял main () в качестве потока GUI.
- Обозначает вашу "основную форму" как "форму выключения" для приложения. Если пользователь закрывает эту форму, то Application.Run() завершается, и управление возвращается к вашей функции main(), где вы можете выполнить любое выключение по своему усмотрению.
Нет необходимости порождать поток, чтобы заботиться о всплывающем окне, и на самом деле это плохая идея, потому что тогда вам придется использовать поточно-ориентированные методы для обновления содержимого всплеска из main().
Если вам нужны другие потоки для выполнения фоновых операций в вашем приложении, вы можете порождать их из main(). Просто не забудьте установить Thread.IsBackground в True, чтобы они умирали, когда основной поток / GUI-поток завершается. В противном случае вам придется самостоятельно завершить все остальные потоки, или они будут поддерживать ваше приложение живым (но без графического интерфейса), когда завершится основной поток.
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# 3.0 и выше, так как он использует лямбда-функцию. Для людей, которым нужно использовать код в C# 2.0, вот код, использующий анонимный делегат вместо лямбда-функции. Вам нужна самая верхняя форма win под названием formSplash
с 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
, Ты можешь подумать MinimumSplashScreenDisplayTime
3 секунды разумно и не будет замедлять ваше приложение. Но попробуйте, ваше приложение теперь будет загружаться за 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.
На самом деле многопоточность здесь не нужна.
Пусть ваша бизнес-логика генерирует событие, когда вы хотите обновить заставку.
Затем позвольте вашей форме обновить заставку соответствующим образом в методе, привязанном к обработчику событий.
Чтобы дифференцировать обновления, вы можете запускать разные события или предоставлять данные в классе, унаследованном от EventArgs.
Таким образом, вы можете легко менять заставку без многопоточности.
На самом деле с этим вы можете даже поддерживать, например, GIF-изображение в виде заставки. Чтобы это работало, вызовите Application.DoEvents() в вашем обработчике:
private void SomethingChanged(object sender, MyEventArgs e)
{
formSplash.Update(e);
Application.DoEvents(); //this will update any animation
}