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

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

Мне нужно перерисовать экран в каждом событии MouseWheel, и я понял, что производительность немного медленная, поэтому я измерил время создания объектов и было меньше 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;
    }

Ответ 1

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

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

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

Затем я попытался опросить позицию мыши с помощью службы PInvoke GetCursorPos как в этом SO-ответе, например:

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

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


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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

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

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

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