Причина низкой производительности в 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;
}
3 ответа
В настоящее время я имею дело с той же проблемой, и обнаружил нечто совершенно неожиданное. Я рендеринг в WriteableBitmap и позволяю пользователю прокручивать (масштабировать) и панорамировать, чтобы изменить то, что отображается. Движение казалось изменчивым как для масштабирования, так и для панорамирования, поэтому я, естественно, решил, что рендеринг занимает слишком много времени. После некоторого инструментирования я проверил, что я рендеринг на 30-60 кадров в секунду. Время рендеринга не увеличивается независимо от того, как пользователь масштабирует или панорамирует изображение, поэтому изменение должно происходить откуда-то еще.
Вместо этого я посмотрел на обработчик события OnMouseMove. В то время как WriteableBitmap обновляется 30-60 раз в секунду, событие MouseMove вызывается только 1-2 раза в секунду. Если я уменьшу размер WriteableBitmap, событие MouseMove срабатывает чаще, и операция панорамирования выглядит более плавной. Таким образом, прерывистость на самом деле является результатом прерывистого события MouseMove, а не рендеринга (например, WriteableBitmap отображает 7-10 кадров, которые выглядят одинаково, событие MouseMove запускается, затем WriteableBitmap отображает 7-10 кадров вновь панорамированного изображения, так далее).
Я попытался отследить операцию панорамирования, опрашивая положение мыши каждый раз, когда WriteableBitmap обновляется с помощью Mouse.GetPosition(this). Однако это дало тот же результат, потому что возвращаемая позиция мыши была бы одинаковой для 7-10 кадров, прежде чем перейти к новому значению.
Затем я попытался опросить положение мыши, используя сервис GetInursPos PInvoke, как в этом ответе 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, и я понятия не имею, как его обойти.
Итак, хотя все вышеприведенные советы по эффективному ведению вашего визуального дерева - это хорошая практика, я подозреваю, что ваши проблемы с производительностью могут быть результатом того, что что-то мешает частоте событий мыши. В моем случае, похоже, что по какой-то причине рендеринг вызывает обновление событий мыши и их запуск происходит гораздо медленнее, чем обычно. Я обновлю это, если найду верное решение, а не этот частичный обход.
Редактировать: Хорошо, я углубился в это немного больше, и я думаю, что теперь я понимаю, что происходит. Я объясню с более подробными примерами кода:
Я выполняю рендеринг своего растрового изображения для каждого кадра, регистрируясь для обработки события 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 выполняет свой путь рендеринга, все, что ему нужно сделать, это переписать эти растровые изображения, а не реконструировать пиксельные данные из необработанных геометрических данных.
@Vahid: система WPF использует [сохраненную графику]. В конечном итоге вы должны разработать систему, в которую вы отправляете только "то, что изменилось по сравнению с предыдущим кадром" - ни больше, ни меньше, вам вообще не нужно создавать новые объекты. Дело не в том, что "создание объектов занимает ноль секунд", а в том, как это влияет на рендеринг и время. Речь идет о том, чтобы позволить WPF выполнять свою работу с использованием кэширования.
Отправка новых объектов в графический процессор для рендеринга =медленно. Отправка только обновлений в графический процессор, который сообщает, какие объекты перемещены =быстро.
Кроме того, можно создавать визуалы в произвольном потоке для повышения производительности ( многопоточный пользовательский интерфейс: HostVisual - Dwayne Need). Тем не менее, если ваш проект довольно сложен в 3D-графике - есть хороший шанс, что WPF не просто урежет его. Использование DirectX.. напрямую, намного, намного эффективнее!
Некоторые статьи, которые я предлагаю вам прочитать и понять:
[Написание более эффективных ItemsControls - Чарльз Петцольд] - понять процесс достижения лучшей скорости рисования в WPF.
Что касается того, почему ваш пользовательский интерфейс отстает, Дэн ответ, кажется, на месте. Если вы пытаетесь визуализировать больше, чем может обработать WPF, система ввода пострадает.
Вероятным виновником является тот факт, что вы очищаете и перестраиваете свое визуальное дерево на каждом событии колеса. Согласно вашему посту, это дерево содержит "большое количество" текстовых элементов. Для каждого наступающего события каждый из этих текстовых элементов должен быть воссоздан, переформатирован, измерен и, в конечном итоге, отрендерен. Это не способ добиться простого масштабирования текста.
Вместо того, чтобы устанавливать ScaleTransform
на каждой FormattedText
элемент, установите один элемент, содержащий текст. В зависимости от ваших потребностей, вы можете установить RenderTransform
или же LayoutTransform
, Затем, когда вы получаете события колеса, отрегулируйте Scale
собственность соответственно. Не перестраивайте текст на каждом событии.
Я также сделал бы то, что другие рекомендовали и связал бы ItemsControl
к списку столбцов и генерировать текст таким образом. Нет причин, по которым вам нужно делать это вручную.