Как избавиться от рывков в анимации прокрутки WinForms? - PullRequest
4 голосов
/ 04 июня 2009

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

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

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

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

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

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

Ответы [ 9 ]

3 голосов
/ 11 июня 2009

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

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

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

EDIT:

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

Итак, с GDI это кажется невозможным.

3 голосов
/ 11 июня 2009

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

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

2 голосов
/ 11 июня 2009

Вам нужно прекратить полагаться на срабатывание события таймера именно тогда, когда вы об этом попросите, и вместо этого определить разницу во времени, а затем определить расстояние для перемещения. Это то, что делают игры и 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);
}
2 голосов
/ 08 июня 2009

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

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

2 голосов
/ 04 июня 2009

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

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

1 голос
/ 11 июня 2009

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

1 голос
/ 11 июня 2009

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

1 голос
/ 11 июня 2009

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

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

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

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

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

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

0 голосов
/ 04 июня 2009

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...