Проблема таймера с GDI+

В настоящее время я застрял в действительно странной проблеме с GDI и таймерами.

Сначала код:

class Graph : UserControl {
  private System.Threading.Timer timer;
  private int refreshRate = 25;              //Hz (redrawings per second)
  private float offsetX = 0;                 //X offset for moving graph

  public Graph() {
    timer = new System.Threading.Timer(timerTick);
  }

  private void timerTick(object data) {
    offsetX -= 1 / refreshRate;
    this.Invalidate();
  }

  public void timerStart() {
    timer.Change(0, 1000 / refreshRate);
  }

  private void onPaint(object sender, PaintEventArgs e) {
    //350 lines of code for drawing the graph
    //Here the offsetX will be used to move the graph
  }
}

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

При первом взгляде этот код работал, но позже я обнаружил следующую проблему: если я использую refreshRate 1 (1 Гц), он будет просто нормально перемещать мой график за 1 шаг 1 (блок графика) влево. Если я увеличу refreshRate, мое движение замедлится. При 20 FPS он немного медленный, при 200 FPS он очень медленный.

Итак, вот что я попробовал:

  1. Я использовал Refresh или Update вместо Invalidate

  2. Я использовал обычную тему (со сном) вместо таймера

Оба изменения кода не изменили результат..

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

Я подумал о проблеме в очереди на рисование, потому что я обновляюсь быстрее, чем рисование? (Но почему я могу плавно перемещать график с помощью мыши?!)

Поэтому мне нужна небольшая помощь здесь. Спасибо

2 ответа

Решение

Вы можете попытаться немного изменить проблему.

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

То, что вы действительно знаете, это количество времени, которое вы хотите, чтобы произошел переход, скажем, 30 секунд, что-то вроде этого:

private MAX_TRANSITION_TIME = 30; //seconds

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

private DateTime _startMoment;

Теперь вы можете убедиться, что у вас есть правильная рутина, и вы рассчитываете свою позицию, основываясь на разнице между startMoment и сейчас.

var elapsedTime = DateTime.Now.Subtract(_startMoment).Milliseconds;
var elapsedPercent = elapsedTime / MAX_TRANSITION_TIME * 1000.0 ;

Теперь вы можете рисовать в соответствии с вашим процентом прошедшего времени.

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

 private void onPaint(object sender, PaintEventArgs e) {
       Timer1.Enabled = false;

     //350 lines of (adjusted)code go here

     If (ElapsedPercent<1)
     {
         Timer1.Enabled=True;
     }
     else
     {
         // you may need to perform the last draw setting the ElapsedPercent 
         // to 1. This is because it is very unlikely that your 
         // last call will happen at the very last millisecond
     }
 }

Где Timer1 должен быть элементом управления Timer.

В интервале событий вы просто пишете

 _startMoment = DateTime.Now();
 this.Invalidate();

Надеюсь это поможет

При 20 FPS он немного медленный, при 200 FPS он очень медленный.

Здесь есть фундаментальная проблема; чтобы получить частоту обновления 200 кадров в секунду, вам нужно будет перекрашивать каждые 5 мс. Это никогда не случится. Независимо от того, какой интервал таймера вы установили, его разрешение ограничено 10-15 мс. Таким образом, ваша лучшая возможная частота кадров составляет около 66-100 кадров в секунду, и это при условии, что ваш код рисования занимает нулевое время, что, конечно, нет.

Кроме того, вы используете System.Threading.Timer который не выполняет свои обратные вызовы в потоке пользовательского интерфейса, поэтому вызов Invalidate() оттуда, вероятно, даже небезопасен. Вы обычно не используете System.Threading.Timer для кода пользовательского интерфейса.

Вы можете попробовать что-то вроде так называемого мультимедийного таймера, показанного здесь, который использует высокопроизводительный таймер процессора и дает разрешение около 1 мс.

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