Многопоточный экран-заставка в C#?

Я хочу, чтобы во время загрузки приложения отображался заставочный экран. У меня есть форма с привязкой к панели управления системой. Я хочу, чтобы заставка отображалась во время загрузки этой формы, что занимает некоторое время, так как он получает доступ к API веб-службы, чтобы заполнить некоторые раскрывающиеся списки. Я также хочу провести некоторое базовое тестирование зависимостей перед загрузкой (то есть веб-сервис доступен, файл конфигурации доступен для чтения). Поскольку каждая фаза запуска идет, я хочу обновить заставку с прогрессом.

Я много читал о потоках, но заблудился из-за того, откуда это должно контролироваться (метод main()?). Мне тоже не хватает как Application.Run() работает, это где темы для этого должны быть созданы? Теперь, если форма с управлением в системном трее является "живой" формой, должен ли всплеск исходить оттуда? Разве он не загрузится, пока форма не будет заполнена в любом случае?

Я не ищу раздаточный материал, скорее алгоритм / подход, чтобы я мог понять это раз и навсегда:)

12 ответов

Решение

Что ж, для приложения ClickOnce, которое я развернул в прошлом, мы использовали пространство имен Microsoft.VisualBasic для обработки потоков на заставке. Вы можете ссылаться и использовать сборку Microsoft.VisualBasic из C# в.NET 2.0, и она предоставляет множество полезных сервисов.

  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 и байт-код, так почему бы не использовать его?

Хитрость заключается в том, чтобы создать отдельный поток, отвечающий за показ заставки.
При запуске приложения.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, - это путь, но есть пара вещей, которые нужно увести от конкретных примеров:

  1. Основная предпосылка - показать ваш всплеск в отдельном потоке как можно скорее. Вот как я склоняюсь, подобно тому, как проиллюстрирован аку, так как это то, с чем я больше всего знаком. Я не знал о функции VB, о которой говорил Гай. И, даже если он думал, что это библиотека VB, он прав - в конце концов, это все IL. Так что, даже если это кажется грязным, это не так уж и плохо!:) Я думаю, вы захотите убедиться, что VB предоставляет отдельный поток для этого переопределения или что вы создаете его самостоятельно - определенно исследуйте это.

  2. Предполагая, что вы создаете другой поток для отображения этого всплеска, вы должны быть осторожны с обновлениями интерфейса между потоками. Я поднял это, потому что вы упомянули прогресс обновления. По сути, для безопасности вам нужно вызвать функцию обновления (которую вы создаете) в форме заставки с использованием делегата. Вы передаете этот делегат функции 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) в конце делает две вещи:

  1. Запускает "насос сообщений" Windows, используя поток, который выполнял main () в качестве потока GUI.
  2. Обозначает вашу "основную форму" как "форму выключения" для приложения. Если пользователь закрывает эту форму, то 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#

Мне очень нравится ответ Аку, но код для 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
}
Другие вопросы по тегам