Как избавиться от рывков в анимации прокрутки WinForms?

Я пишу простой элемент управления в C#, который работает как графический блок, за исключением того, что изображение постоянно прокручивается вверх (и снова появляется снизу). Эффект анимации управляется таймером (System.Threading.Timer), который копирует из кэшированного изображения (из двух частей) в скрытый буфер, который затем рисуется на поверхности элемента управления в его событии Paint.

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

Есть ли способ, чтобы я мог плавно прокрутить эту анимацию?

Вы можете скачать образец приложения здесь (запустите его и нажмите "Пуск"), а исходный код здесь. Это не выглядит ужасно вяло, но если вы посмотрите на это внимательно, вы можете увидеть икоты.

ВНИМАНИЕ: эта анимация производит довольно странный эффект оптической иллюзии, который может немного вас утомить. Если вы посмотрите его некоторое время, а затем выключите, он будет выглядеть так, как будто ваш экран растягивается вертикально.

ОБНОВЛЕНИЕ: в качестве эксперимента я попытался создать файл AVI с моими прокручивающимися растровыми изображениями. Результат был менее резким, чем моя анимация WinForms, но все же неприемлемым (и я все еще чувствовал тошноту, чтобы смотреть его слишком долго). Я думаю, что сталкиваюсь с фундаментальной проблемой отсутствия синхронизации с частотой обновления, поэтому мне, возможно, придется придерживаться того, чтобы люди болели своей внешностью и индивидуальностью.

9 ответов

Решение

Вам нужно будет подождать VSYNC, прежде чем рисовать буферизованное изображение.

Есть статья CodeProject, которая предлагает использовать мультимедийный таймер и метод DirectX' IDirectDraw::GetScanLine(),

Я совершенно уверен, что вы можете использовать этот метод через Managed DirectX из C#.

РЕДАКТИРОВАТЬ:

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

Таким образом, с GDI это кажется невозможным.

( http://www.vcskicks.com/animated-windows-form.html)

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

Используйте двойную буферизацию. Вот две статьи: 1 2.

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

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

Возможно, вы захотите попробовать.

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

Допустим, вы знаете, что вам нужно переместить 100 пикселей за 1 секунду (для синхронизации с музыкой), затем вы рассчитываете время с момента запуска вашего последнего события таймера (скажем, это было 20 мс) и определяете расстояние для перемещения в виде дроби. от общего числа (20 мс / 1000 мс * 100 пикселей = 2 пикселя).

Примерный пример кода (не тестировался):

Image image = Image.LoadFromFile(...);
DateTime lastEvent = DateTime.Now;
float x = 0, y = 0;
float dy = -100f; // distance to move per second

void Update(TimeSpan elapsed) {
    y += (elapsed.TotalMilliseconds * dy / 1000f);
    if (y <= -image.Height) y += image.Height;
}

void OnTimer(object sender, EventArgs e) {
    TimeSpan elapsed = DateTime.Now.Subtract(lastEvent);
    lastEvent = DateTime.Now;

    Update(elapsed);
    this.Refresh();
}

void OnPaint(object sender, PaintEventArgs e) {
    e.Graphics.DrawImage(image, x, y);
    e.Graphics.DrawImage(image, x, y + image.Height);
}

Я изменил ваш пример, чтобы использовать мультимедийный таймер с точностью до 1 мс, и большая часть рывков ушла. Тем не менее, остается небольшой разрыв, в зависимости от того, куда именно вы перетаскиваете окно по вертикали. Если вам нужно полное и совершенное решение, GDI/GDI+, вероятно, не ваш путь, потому что (AFAIK) он не дает вам контроля над вертикальной синхронизацией.

Некоторые идеи (не все хорошо!):

  • При использовании таймера потоков убедитесь, что ваше время рендеринга значительно меньше интервала одного кадра (по звуку вашей программы у вас все должно быть в порядке). Если рендеринг занимает более 1 кадра, вы получите повторные вызовы и начнете рендеринг нового кадра до того, как закончите последний. Одним из решений этой проблемы является регистрация только одного обратного вызова при запуске. Затем в вашем обратном вызове установите новый обратный вызов (вместо того, чтобы просто запрашивать повторный вызов каждые n миллисекунд). Таким образом, вы можете гарантировать, что вы только что запланировали новый кадр, когда закончите рендеринг текущего.

  • Вместо использования таймера потока, который перезвонит вам через неопределенное время (единственная гарантия, что он больше или равен указанному вами интервалу), запустите анимацию в отдельном потоке и просто ждите (занятое ожидание) loop или spinwait), пока не наступит время следующего кадра. Вы можете использовать Thread.Sleep для сна в течение более коротких периодов времени, чтобы не использовать 100% CPU или Thread.Sleep(0), просто чтобы получить и получить еще один временной интервал как можно скорее. Это поможет вам получить гораздо более согласованные интервалы кадров.

  • Как упомянуто выше, используйте время между кадрами, чтобы вычислить расстояние для прокрутки, чтобы скорость прокрутки не зависела от частоты кадров. Но учтите, что вы получите временные эффекты сэмплирования / сглаживания, если попытаетесь выполнить прокрутку с частотой не в пикселях (например, если вам нужно выполнить прокрутку на 1,4 пикселя в кадре, лучшее, что вы можете сделать, это 1 пиксель, что даст 40% ошибка скорости). Обходным путем для этого может быть использование большего закадрового растрового изображения для прокрутки, а затем уменьшение его при переходе к экрану, чтобы вы могли эффективно прокручивать их на субпиксельные значения.

  • Используйте более высокий приоритет потока. (действительно противно, но может помочь!)

  • Для рендеринга используйте что-то более управляемое (DirectX), а не GDI. Это можно настроить для замены внеэкранного буфера на vsync. (Я не уверен, что двойная буферизация форм мешает синхронизации)

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

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

Просто измените _T += 1; строка, чтобы добавить новый шаг...

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

Другие вопросы по тегам