Обновление Force GUI из UI Thread

В WinForms, как заставить принудительное обновление UI из потока пользовательского интерфейса?

То, что я делаю, примерно:

label.Text = "Please Wait..."
try 
{
    SomewhatLongRunningOperation(); 
}
catch(Exception e)
{
    label.Text = "Error: " + e.Message;
    return;
}
label.Text = "Success!";

Текст надписи не будет установлен в "Пожалуйста, подождите..." перед операцией.

Я решил это, используя другой поток для операции, но он становится волосатым, и я хотел бы упростить код.

Ответ 1

Сначала я задавался вопросом, почему OP еще не отметил один из ответов в качестве ответа, но, попробовав его сам и все еще не получив его, я углубился и нашел там гораздо больше этой проблемы, затем я сначала предполагалось.

Лучшее понимание можно получить, прочитав по аналогичному вопросу: Почему не будет контролировать обновление/обновление среднего процесса

Наконец, для записи мне удалось обновить мою метку, выполнив следующие действия:

private void SetStatus(string status) 
{
    lblStatus.Text = status;
    lblStatus.Invalidate();
    lblStatus.Update();
    lblStatus.Refresh();
    Application.DoEvents();
}

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

Ответ 2

Вызов label.Invalidate, а затем label.Update() - обычно обновление происходит только после выхода из текущей функции, но вызов Update заставляет его обновлять в этом конкретном месте в коде. Из MSDN:

Метод Invalidate определяет, что будет раскрашено или перекрашено. Метод обновления определяет, когда происходит окраска или перекраска. Если вы используете методы Invalidate и Update вместе, а не вызываете Refresh, то что перекрашивается, зависит от того, какая перегрузка Invalidate вы используете. Метод Update просто заставляет элемент управления сразу окрашиваться, но метод Invalidate определяет, что будет окрашиваться, когда вы вызываете метод Update.

Ответ 3

Вызовите Application.DoEvents() после установки метки, но вместо этого вы должны выполнить всю работу в отдельном потоке, чтобы пользователь мог закрыть окно.

Ответ 4

Я только что наткнулся на одну и ту же проблему и нашел интересную информацию, и я хотел добавить два цента и добавить их здесь.

Прежде всего, как уже отмечали другие, длительные операции должны выполняться потоком, который может быть фоном, явным потоком, потоком из threadpool или (с .Net 4.0) задачей: fooobar.com/questions/92177/..., чтобы пользовательский интерфейс сохранял отзывчивость.

Но для коротких задач нет никакой реальной потребности в нарезке, хотя, конечно, это не повредит.

Я создал winform с одной кнопкой и одним ярлыком для анализа этой проблемы:

System::Void button1_Click(System::Object^  sender, System::EventArgs^  e)
{
  label1->Text = "Start 1";
  label1->Update();
  System::Threading::Thread::Sleep(5000); // do other work
}

Мой анализ перешагнул код (используя F10) и посмотрел, что произошло. И после прочтения этой статьи Многопоточность в WinForms я нашел что-то интересное. В статье в нижней части первой страницы говорится, что поток пользовательского интерфейса не может перерисовывать пользовательский интерфейс до тех пор, пока выполняемая в данный момент функция не завершится, и окно будет помечено Windows как "не отвечающее", а через некоторое время. Я также заметил, что в моем тестовом приложении сверху, когда он проходит через него, но только в некоторых случаях.

(Для следующего теста важно, чтобы Visual Studio не была установлена ​​в полноэкранном режиме, вы должны иметь возможность видеть небольшое окно приложения одновременно с ним. Вам не нужно переключаться между окном Visual Studio для отладки и окна вашего приложения, чтобы узнать, что произойдет. Запустите приложение, установите точку останова в label1->Text ..., поместите окно приложения рядом с окном VS и поместите курсор мыши над окном VS.)

  • Когда я нажимаю один раз на VS после запуска приложения (чтобы поместить фокусы туда и активировать степпинг) и пройдите через него БЕЗ перемещения мыши, новый текст будет установлен, а метка будет обновлена ​​в функции update(), Это означает, что пользовательский интерфейс перекрашивается, очевидно.

  • Когда я перехожу через первую строку, затем перемещаю мышь вокруг и выбираем где-нибудь, затем шаг дальше, новый текст, вероятно, будет установлен и вызывается функция update(), но пользовательский интерфейс не обновляется /repaintted, и старый текст остается там до тех пор, пока функция button1_click() не закончится. Вместо того, чтобы перекрашивать, окно обозначается как "не реагирующее"! Это также не позволяет добавить this->Update(); для обновления всей формы.

  • Добавление Application::DoEvents(); дает UI возможность обновлять/перерисовывать. В любом случае вам нужно позаботиться о том, чтобы пользователь не мог нажимать кнопки или выполнять другие действия в пользовательском интерфейсе, которые не разрешены! Поэтому: Старайтесь избегать DoEvents()!, лучше использовать потоки (что я считаю довольно простым в .Net). Но (@Jagd, 2 апр. 10 в 19:25) вы можете опустить .refresh() и .invalidate().

Мои объяснения выглядят так: AFAIK winform по-прежнему использует функцию WINAPI. Также Статья MSDN о System.Windows.Forms Control.Update метод относится к функции WINAPI WM_PAINT. В статье MSDN о WM_PAINT говорится в первом предложении, что команда WM_PAINT отправляется системой только тогда, когда очередь сообщений пуста. Но поскольку очередь сообщений уже заполнена во втором случае, она не отправляется, и поэтому ярлык и форма приложения не перекрашиваются.

< > joke > Заключение: так что вам просто нужно заставить пользователя использовать мышь;-) < > /joke >

Ответ 5

вы можете попробовать это

using System.Windows.Forms; // u need this to include.

MethodInvoker updateIt = delegate
                {
                    this.label1.Text = "Started...";
                };
this.label1.BeginInvoke(updateIt);

Посмотрите, работает ли он.

Ответ 6

После обновления пользовательского интерфейса запустите задачу, выполняемую с помощью продолжительной операции:

label.Text = "Please Wait...";

Task<string> task = Task<string>.Factory.StartNew(() =>
{
    try
    {
        SomewhatLongRunningOperation();
        return "Success!";
    }
    catch (Exception e)
    {
        return "Error: " + e.Message;
    }
});
Task UITask = task.ContinueWith((ret) =>
{
    label.Text = ret.Result;
}, TaskScheduler.FromCurrentSynchronizationContext());

Это работает в .NET 3.5 и более поздних версиях.

Ответ 7

Очень заманчиво хотеть "исправить" это и принудительно обновить пользовательский интерфейс, но самое лучшее решение - сделать это в фоновом потоке и не связывать поток пользовательского интерфейса, чтобы он мог реагировать на события.

Ответ 9

Думаю, что у меня есть ответ, отобранный из приведенного выше и немного экспериментов.

progressBar.Value = progressBar.Maximum - 1;
progressBar.Maximum = progressBar.Value;

Я попытался уменьшить значение и экран, обновленный даже в режиме отладки, но это не сработало бы при установке progressBar.Value в progressBar.Maximum, потому что вы не можете установить значение индикатора выполнения выше максимума, поэтому я сначала установил progressBar.Value до progressBar.Maximum - 1, затем установите progressBar.Maxiumum равным progressBar.Valu e. Говорят, что есть более одного способа убить кошку. Иногда я хотел бы убить Билла Гейтса или того, кто он сейчас: o).

С этим результатом мне даже не нужно было Invalidate(), Refresh(), Update() или сделать что-либо в строке выполнения или в контейнере Panel или в родительской форме.

Ответ 10

У меня была та же проблема с свойством Enabled, и я обнаружил, что first chance exception поднят из-за того, что он не является потокобезопасным. Я нашел решение о том, как обновить графический интерфейс из другого потока в С#? здесь fooobar.com/info/4167/... И это работает!