Udostępnij za pośrednictwem


Проблемы создания Splash-screen в .Net (и не только)

Splash-screen (по-русски: экран-заставка) - это лицо вашего приложения.
И чтобы не ударить в грязь лицом, следует позаботится, чтобы работа этого экрана-заставки отвечала ожиданиям пользователей.
А пользователи, как правило, ожидают, что пока им демонстрируется приятная графическая картинка с номером версии и названием программы сама программа не тратит времени по напрасну и загружает необходимые данные. После того как экран-заставка закроется пользователи ожидают, что программа будет готова к работе, а не продолжать процесс инициализации.

Эти рассуждения наводят на мысль о необходимости распараллеливания показа заставки и процесса подготовки приложения к работе. И тут начинаются проблемы.

Подход №1. Создаем экран-заставку и в том же потоке исполнения занимаемся нашими вычислениями, загрузкой файлов, вызовами веб-сервисов и т.п. По окончании инициализации закрываем окно и возващаемся в приложения, откуда уже создаем основное окно приложения.

Пример: https://www.codeproject.com/csharp/applicationcontextsplash.asp

Проблема: Если инициализация занимает много времени, окно заставки перестает перерисовываться, переключение между программами проецирует на наше окно куски других окон. Ну вы видели такое.

Подход №2. Создаем два потока - главный и вспомогательный. Вспомогательный поток отображает заставку. Главный производит инициализацию. По окончании инициализации в главном потоке завершаем вспомогательный поток. В главном потоке создаем основное окно приложения.

Пример: https://www.codeproject.com/useritems/usesplashscreen.asp

Проблема: Основное окно оказывается "глубоко" на заднем фоне. Это связано с особенностью оконного менеджера Windows - для него окна созданные в разных потоках приложения не имеют (почти) ничего общего - поэтому их z-ордер оказывается сильно отличным.

Подход №2а. Повторяем описанное в подходе 2, но дополнительно при создании основного окна вызываем функцию this.Activate(); (или SetForegroundWindow() в неуправляемом коде). Этот подход предложен, например, здесь: https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=115850. Кстати, интересные попытки обойти логику работы функции SetForegroundWindow() встречаются здесь https://www.rsdn.ru/article/qna/ui/wndsetfg.xml.

Пример: https://www.codeproject.com/csharp/apploadingarticle.asp (здесь следует читать также и комментарии, где как раз и предлагается вызывать this.Activate();)

Проблема: Ситуация улучшилась - основное окно остается на переднем плане. Если только мы не щелкнем на каком-либо другом окне. В этом случае основное окно снова окажется на задворках (а не под этим другим окном) и обреченно замигает на панели задач. Обреченно, потому, что на этом обычно все и останавливаются, тем не менее существует Подход №3. И у него тоже есть своя проблема.

Подход №3. Создаем основной и вспомогательный поток приложения. В основном потоке создаем окно экрана-заставки. Во вспомогательном производим всю необходимую инициализацию. После этого закрываем заставку и, используя подготовленные данные, без задержек открываем главное окно.

Пример: На вскидку ничего не нашел, поэтому оставляется читателю в качестве упражнения.

Проблема: Для реализации такого подхода от нас потребуется отделить инициализационные код в отдельную функцию. А потом передать результаты в другой поток. Это не так весело, как, например, запихнуть весь код в событие OnLoad. Тем не менее это самый красивый с точки зрения пользователей, а также эстетствующих менеджеров проекта :), подход.

Где-то по середине между 1 и 3 подходом находится подход с периодическим просмотром очереди сообщений в перемешку с инициализацией. Что-то вроде этого реализовано, кажется, здесь https://www.codeproject.com/useritems/CustomSplashScreen.asp. Этот подход разумеется не нов, минусы у него достаточно очевидны.

Факультативно, можно почитать рассказ Джастина Роджерса, разработчика из команды IE, о том, как работает ApplicationContext (эдакий wrapper для исполнения приложения) https://weblogs.asp.net/justin_rogers/archive/2004/04/11/111162.aspx.

Да, кстати, ложный путь, указанный в одной из статей на сайте codeproject, стоил мне пары часов анализа исходных текстов CLR в поисках того самого места, где происходит описанное "безобразие" (так и не нашел ;) ):

Solving the z-order problem
The problem with the z-order was caused by running two message loops within the same AppDomain. As soon as the AppDomain recieves the message loop exited message it deactivates the Application sending it to the back of the z-order. This is not normaly a problem as this exited message would indicate the application is closing, but not in this case. Fortunatly, this implementation of a splash screen avoids this problem by never starting a message loop for the splash screen, but rather pumping the messages manually.