Как избавиться от рывков в анимации прокрутки 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; строка, чтобы добавить новый шаг...
На самом деле, вы даже можете добавить свойство к элементу управления, чтобы настроить количество шагов.