Причина низкой производительности в WPF

Я создаю большое количество текстов в WPF, используя DrawText а затем добавить их в один Canvas.

мне нужно перерисовать экран в каждом MouseWheel event и я понял, что производительность немного медленная, поэтому я измерил время создания объектов, и это было менее 1 миллисекунды!

так в чем же проблема? Давным-давно я, наверное, где-то читал, что это на самом деле Rendering это занимает время, не создавая и не добавляя видеоряд.

вот код, который я использую для создания текстовых объектов, я включил только основные части:

public class ColumnIdsInPlan : UIElement
    {
    private readonly VisualCollection _visuals;
    public ColumnIdsInPlan(BaseWorkspace space)
    {
        _visuals = new VisualCollection(this);

        foreach (var column in Building.ModelColumnsInTheElevation)
        {
            var drawingVisual = new DrawingVisual();
            using (var dc = drawingVisual.RenderOpen())
            {
                var text = "C" + Convert.ToString(column.GroupId);
                var ft = new FormattedText(text, cultureinfo, flowdirection,
                                           typeface, columntextsize, columntextcolor,
                                           null, TextFormattingMode.Display)
                {
                    TextAlignment = TextAlignment.Left
                };

                // Apply Transforms
                var st = new ScaleTransform(1 / scale, 1 / scale, x, space.FlipYAxis(y));
                dc.PushTransform(st);

                // Draw Text
                dc.DrawText(ft, space.FlipYAxis(x, y));
            }
            _visuals.Add(drawingVisual);
        }
    }

    protected override Visual GetVisualChild(int index)
    {
        return _visuals[index];
    }

    protected override int VisualChildrenCount
    {
        get
        {
            return _visuals.Count;
        }
    }
}

и этот код выполняется каждый раз MouseWheel событие:

var columnsGroupIds = new ColumnIdsInPlan(this);
MyCanvas.Children.Clear();
FixedLayer.Children.Add(columnsGroupIds);

что может быть виновником?

у меня также возникают проблемы при панорамировании:

    private void Workspace_MouseMove(object sender, MouseEventArgs e)
    {
        MousePos.Current = e.GetPosition(Window);
        if (!Window.IsMouseCaptured) return;
        var tt = GetTranslateTransform(Window);
        var v = Start - e.GetPosition(this);
        tt.X = Origin.X - v.X;
        tt.Y = Origin.Y - v.Y;
    }

3 ответов


в настоящее время я имею дело с тем, что, вероятно, та же проблема, и я обнаружил что-то совершенно неожиданное. Я рендеринг в WriteableBitmap и позволяет пользователю прокручивать (масштабировать) и панорамировать, чтобы изменить то, что отображается. Движение казалось прерывистым как для масштабирования, так и для панорамирования, поэтому я, естественно, решил, что рендеринг занимает слишком много времени. После некоторых инструментов я проверил, что я рендеринг на 30-60 fps. Нет увеличения времени рендеринга независимо от того, как пользователь масштабирует или панорамирование, значит, прерывистость должна исходить откуда-то еще.

вместо этого я посмотрел на обработчик событий OnMouseMove. В то время как WriteableBitmap обновляется 30-60 раз в секунду, событие MouseMove запускается только 1-2 раза в секунду. Если я уменьшаю размер WriteableBitmap, событие MouseMove срабатывает чаще, и операция панорамирования выглядит более гладкой. Таким образом, прерывистость на самом деле является результатом прерывистого события MouseMove, а не рендеринга (например, WriteableBitmap рендеринг 7-10 кадров, которые выглядят одинаково, событие MouseMove срабатывает, затем WriteableBitmap отображает 7-10 кадров нового панорамированного изображения и т. д.).

Я попытался отслеживать операцию панорамирования, опрашивая позицию мыши каждый раз, когда writeablebitmap обновляется с помощью мыши.GetPosition(это). Это имело тот же результат, однако, потому что возвращенная позиция мыши будет одинаковой для 7-10 кадров перед изменением на новое значение.

затем я попытался опросить позицию мыши использование сервиса PInvoke GetCursorPos вроде в этом так отвечай например:

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool GetCursorPos(out POINT lpPoint);

[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
    public int X;
    public int Y;

    public POINT(int x, int y)
    {
        this.X = x;
        this.Y = y;
    }
}

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

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


редактировать: хорошо, я покопался в этом немного больше, и я думаю, что теперь понимаю что происходит? Я объясню с более подробными образцами кода:

Я рендеринг в мое растровое изображение на основе каждого кадра, зарегистрировавшись для обработки CompositionTarget.Событие рендеринга, как описано в эта статья MSDN. в основном это означает, что каждый раз, когда пользовательский интерфейс отображается, мой код будет вызываться, чтобы я мог обновить свое растровое изображение. Это по существу эквивалентно рендерингу, который вы делаете, просто ваш код рендеринга вызывается за кулисами в зависимости от как вы настроили свои визуальные элементы и мой код рендеринга, где я могу его видеть. Я переопределяю событие OnMouseMove для обновления некоторой переменной в зависимости от положения мыши.

public class MainWindow : Window
{
  private System.Windows.Point _mousePos;
  public Window()
  {
    InitializeComponent();
    CompositionTarget.Rendering += CompositionTarget_Rendering;
  }

  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    // Update my WriteableBitmap here using the _mousePos variable
  }

  protected override void OnMouseMove(MouseEventArgs e)
  {
    _mousePos = e.GetPosition(this);
    base.OnMouseMove(e);
  }
}

проблема в том, что, поскольку рендеринг занимает больше времени, событие MouseMove (и все события мыши, на самом деле) вызывается гораздо реже. Когда код рендеринга занимает 15 мс, событие MouseMove вызывается каждые несколько МС. Когда код отрисовки занимает 30 мс, событие MouseMove получает звонил каждые несколько сто миллисекундах. Моя теория о том, почему это происходит, заключается в том, что рендеринг происходит в том же потоке, где система мыши WPF обновляет свои значения и запускает события мыши. Цикл WPF в этом потоке должен иметь некоторую условную логику, где, если рендеринг занимает слишком много времени в течение одного кадра, он пропускает обновления мыши. Проблема возникает, когда мой код рендеринга занимает "слишком много времени" на каждом кадре. Затем вместо интерфейса появляется замедление немного вниз, потому что рендеринг занимает 15 дополнительных МС на кадр, интерфейс сильно заикается, потому что дополнительные 15 мс времени рендеринга вводят сотни миллисекунд задержки между обновлениями мыши.

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

public class MainWindow : Window
{
  private System.Windows.Point _mousePos;
  public Window()
  {
    InitializeComponent();
    CompositionTarget.Rendering += CompositionTarget_Rendering;
  }

  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    POINT screenSpacePoint;
    GetCursorPos(out screenSpacePoint);

    // note that screenSpacePoint is in screen-space pixel coordinates, 
    // not the same WPF Units you get from the MouseMove event. 
    // You may want to convert to WPF units when using GetCursorPos.
    _mousePos = new System.Windows.Point(screenSpacePoint.X, 
                                         screenSpacePoint.Y);
    // Update my WriteableBitmap here using the _mousePos variable
  }

  [DllImport("user32.dll")]
  [return: MarshalAs(UnmanagedType.Bool)]
  static extern bool GetCursorPos(out POINT lpPoint);

  [StructLayout(LayoutKind.Sequential)]
  public struct POINT
  {
    public int X;
    public int Y;

    public POINT(int x, int y)
    {
      this.X = x;
      this.Y = y;
    }
  }
}

этот подход не исправил остальные мои события мыши (MouseDown, MouseWheel и т. д.), Однако, и я не был заинтересован в принятии этого подхода PInvoke для всего моего ввода мыши, поэтому я решил, что лучше просто прекратить голодать систему ввода мыши WPF. То, что я сделал, было только обновлением WriteableBitmap, когда он действительно нуждался в обновлении. Он должен быть обновлен только тогда, когда какой-то ввод мыши повлиял на него. Таким образом, результат заключается в том, что я получаю ввод мыши один кадр, обновите растровое изображение на следующем кадре, но не получите больше ввода мыши на том же кадре, потому что обновление занимает несколько миллисекунд слишком долго, а затем следующий кадр я получу больше ввода мыши, потому что растровое изображение не нужно было обновлять снова. Это приводит к гораздо более линейному (и разумному) снижению производительности по мере увеличения времени рендеринга, потому что время кадра переменной длины просто среднее.

public class MainWindow : Window
{
  private System.Windows.Point _mousePos;
  private bool _bitmapNeedsUpdate;
  public Window()
  {
    InitializeComponent();
    CompositionTarget.Rendering += CompositionTarget_Rendering;
  }

  private void CompositionTarget_Rendering(object sender, EventArgs e)
  {
    if (!_bitmapNeedsUpdate) return;
    _bitmapNeedsUpdate = false;
    // Update my WriteableBitmap here using the _mousePos variable
  }

  protected override void OnMouseMove(MouseEventArgs e)
  {
    _mousePos = e.GetPosition(this);
    _bitmapNeedsUpdate = true;
    base.OnMouseMove(e);
  }
}

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


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

вместо ScaleTransform в каждом FormattedText элемент, установленный на элемент, содержащий текст. В зависимости от ваших потребностей, вы можете установить RenderTransform или LayoutTransform. Затем, когда вы получаете события колеса, отрегулируйте Scale свойства соответственно. Не перестраивайте текст на каждом событии.

Я бы также сделал то, что другие рекомендовали и привязать ItemsControl в список столбцов и генерировать текст таким образом. Нет причин делать это вручную.


@Vahid: система WPF использует [сохранена графика]. То, что вы в конечном итоге должны сделать, - это разработать систему, в которой вы отправляете только" то, что изменилось по сравнению с предыдущим кадром " - ничего больше, ничего меньше, - вы не должны создавать новые объекты на всех. Речь идет не о "создании объектов занимает ноль секунд", а о том, как это влияет на рендеринг и время. Она позволяет в WPF сделать его работу, используя кэширование.

отправка новых объектов на GPU для рендеринг=медленно. Отправка только обновлений на GPU, который сообщает, какие объекты перемещены=быстро.

кроме того, можно создавать визуальные эффекты в произвольном потоке для повышения производительности (многопоточный пользовательский интерфейс: HostVisual-Dwayne Need). Все сказанное, если ваш проект довольно сложен в 3D-мудром - есть хороший шанс, что WPF не просто сократит его. С Помощью DirectX.. прямо, гораздо, гораздо, более результативно!

некоторые из статьи я предлагаю вам прочитать и понять:

[Написание Более Эффективных ItemsControls - Charles Petzold] - поймите процесс, как достичь лучшей скорости рисования в WPF.

Что касается того, почему ваш пользовательский интерфейс отстает, Dan ответ, кажется, на месте. Если вы пытаетесь отобразить больше, чем может обработать WPF, система ввода пострадает.