События C# не запускаются, пока поток графического интерфейса занят (рисование). Как решить?

Краткое введение в мою программу (решено)


Моя программа делает снимок и показывает плитку за плиткой (игра в угадайку). Я решил использовать затухание (вручную) каждой плитки. Поскольку мне нужно, чтобы пользователь мог взаимодействовать с графическим интерфейсом, я выполняю вычисления и блокирую такие вещи, как Thread.Sleep, в другом потоке. Я также добавил событие OnClickEvent в окно изображения (которое перекрывает изображение для отображения). =› если кто-то угадал изображение, пользователь может щелкнуть по картинке, чтобы полностью открыть изображение. (Для затухания я обрезаю область изображения, а затем очищаю ее цветом. Альфа-значение цвета постепенно уменьшается, пока он не станет полностью прозрачным. Затем я перехожу к следующей области.)

Входящая проблема


После каждой итерации мне нужно обновить изображение, чтобы оно отображало новую ситуацию. Поэтому я должен вызвать действие в потоке GUI. Теперь, если время между каждым обновлением становится слишком коротким, например, 10 мс, графический интерфейс, кажется, настолько занят обновлением/рисованием изображения, что больше не запускает мой OnClickEvent.

Функция раскрытия


public void Reveal(int step, int intervalFading, int intervalNextTile)
    {
        StopThread = false; // Changed by the OnClickEvent
        Graphics grx = Graphics.FromImage(Overlay.Image);
        step = 255 / step;
        foreach (RectangleF R in AreasShuffled)
        {
            grx.Clip = new Region(R);
            for (int i = 255; i >= 0; i-=step) //Fading out loop
            {
                Thread.Sleep(intervalFading);  //if intervalFading < 15 GUI is too busy
                if (StopThread) //Condition if someone guessed correctly
                {
                    grx.ResetClip();
                    grx.Clear(Color.FromArgb(0, 0, 0, 0)); //revealing the image
                    ParentControl.BeginInvoke((Action)(() => Overlay.Refresh()));
                    grx.Dispose();
                    return;
                }
                grx.Clear(Color.FromArgb(i, 0, 0, 0)); //Clearing region 
                ParentControl.BeginInvoke((Action)(() => Overlay.Refresh())); //Redrawing image
            }
            grx.Clear(Color.FromArgb(0, 0, 0, 0));
            ParentControl.BeginInvoke((Action)(() => Overlay.Refresh()));
            Thread.Sleep(intervalNextTile);
        }
        grx.ResetClip();
        grx.Clear(Color.FromArgb(0, 0, 0, 0));
        ParentControl.BeginInvoke((Action)(() => Overlay.Refresh()));
        grx.Dispose();
    }

Решение


По совету я использовал асинхронные задачи. Вот обновленная функция. (Да, я не обновлял grx.dispose() ^^)

public async Task Reveal(int step, int intervalFading, int intervalNextTile)
    {
        taskIsRunning = true;
        stopTask = false; // Changed by the OnClickEvent
        Graphics grx = Graphics.FromImage(Overlay.Image);
        step = 255 / step;
        foreach (RectangleF R in AreasShuffled)
        {
            grx.Clip = new Region(R);
            for (int i = 255; i >= 0; i -= step) 
            {
                
                await Task.Delay(intervalFading);  
                if (stopTask) 
                {
                    grx.ResetClip();
                    grx.Clear(Color.FromArgb(0, 0, 0, 0)); 
                    Overlay.Refresh();
                    grx.Dispose();
                    taskIsRunning = false;
                    return;
                }
                grx.Clear(Color.FromArgb(i, 0, 0, 0)); 
                Overlay.Refresh();
            }
            grx.Clear(Color.FromArgb(0, 0, 0, 0));
            Overlay.Refresh();
            await Task.Delay(intervalNextTile);
        }
        grx.ResetClip();
        grx.Clear(Color.FromArgb(0, 0, 0, 0));
        Overlay.Refresh();
        grx.Dispose();
        taskIsRunning = false;
    }

и вызывающая функция, которая проверяет, запущена задача или нет

private void pictureBoxOverlay_Click(object sender, EventArgs e)
    {
        if (UCM != null && UCM.taskIsRunning)                                              //if it is running the function is being notified
        {                                                                              //and reveals the image. 
            UCM.stopTask = true;
        }
        else                            //makes sure that the user has to click again to start with the next image
        {
            if (index < Images.Count - 1)
            {
                PrepareNextImage();
                UCM.Reveal(Properties.Settings.Default.steps, Properties.Settings.Default.fadeInterval, Properties.Settings.Default.nextTileInterval); 
            }
            else
                MessageBox.Show("End of presentation.");
        }
    }

Спасибо за вашу помощь ;)


person lorenz albert    schedule 04.03.2016    source источник


Ответы (1)


Здесь есть краткий ответ. Не вызывайте Thread.Sleep в потоке пользовательского интерфейса, если вы ожидаете, что ваш пользовательский интерфейс будет отзывчивым

Обновить

Похоже, вы не запускаете код анимации потока пользовательского интерфейса. Хорошая вещь! Так в чем может быть проблема? Я подозреваю, что 4 вызова BeginInvoke много раз в секунду приводят к тому, что насос сообщений приложения переполняется событиями вызова и задерживает обновления графического интерфейса при их обслуживании.

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

Этот короткий пример вызывает контекст вызова только один раз за каждый интервал. Вы должны вызывать его из пользовательского интерфейса.

async Task Animate(Control control, int interval)
{
    while(true)
    {
        // this line causes the method to pause by queueing
        // everything after await similarly to `BeginInvoke`
        await Task.Delay(interval);

        // all of this still happens on the UI thread

        // increment control properties here

        // check to see if the animation should end.
        if (END STATE IS MET)
        {
            return;
        }
    }
}

В качестве примечания: вы звоните grx.Dispose несколько раз. Возможно, будет лучше обернуть весь блок кода в using(grx){ }. Это все еще работает с асинхронностью! Как? Самая темная магия.

person Gusdor    schedule 04.03.2016
comment
Является ли Delay(int):Task частью API WinForms, и если да, то есть ли у него какие-либо преимущества перед простым ожиданием Task.Delay(TimeSpan):Task? - person sara; 04.03.2016
comment
@kai Task.Delay было моим намерением, но я допустил ошибку. Ой! Спасибо, что указали на это. Я отредактировал ответ. - person Gusdor; 04.03.2016
comment
не волнуйтесь! Хотя я предпочитаю использовать перегрузку, принимающую TimeSpan, поскольку она более семантически значима. По крайней мере, я предпочитаю, чтобы параметр int назывался в соответствии с ожидаемой единицей измерения (миллис) - person sara; 04.03.2016
comment
Привет, я просто хотел прояснить, что я использую Thread.Sleep не в своем потоке графического интерфейса, а в фоновом потоке, который я создал. И функция, которую вы видите, обрабатывается фоновым потоком. - person lorenz albert; 04.03.2016
comment
@kai Я также предпочитаю использовать TimeSpan, где это возможно, но хотел оставаться как можно ближе к примеру спрашивающего. - person Gusdor; 04.03.2016
comment
@lorenzalbert, в этом случае вполне вероятно, что ваши BeginInvoke вызовы переполняют насос сообщений и вызывают задержку, поскольку цикл приложения пытается обслужить их все. Я видел, как это происходило несколько раз, даже в WPF. Вы должны попытаться сделать все обновления в одном Invoke. Этот ответ предоставит эту возможность, но вы должны выполнить ее из пользовательского интерфейса. Можешь попробовать и отчитаться? - person Gusdor; 04.03.2016
comment
@Gusdor Я постараюсь сделать это, когда вернусь. И спасибо за вашу помощь. - person lorenz albert; 04.03.2016
comment
@Gusdor Кажется, все работает нормально, хотя перетаскивание окна могло бы быть более плавным ^^. Я не знаю, правильно ли я все использовал, но изучение TPL и асинхронных вызовов входит в мой список дел. - person lorenz albert; 04.03.2016
comment
@lorenzalbert Приятно слышать. Окно по-прежнему будет прерываться из-за количества происходящих перерисовок. Это симптом схемы рендеринга в немедленном режиме, которую использует WInforms, и я не знаю, как это обойти. Для всего, что связано с анимацией, я бы рекомендовал смотреть на WPF для будущих приложений, хотя кривая обучения здесь крутая. Приятного чтения книги! - person Gusdor; 04.03.2016