Как я могу вручную указать нарисованный владельцем элемент управления WPF для обновления / перерисовки без выполнения измерения или организации проходов?

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

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

Опять же, мы просто ищем простой способ сообщить элементу управления, что ему нужно выполнить повторно OnRender, Я видел один "взлом", в котором вы создаете новый DependencyProperty и зарегистрируйте его в "AffectsRender", для которого вы просто устанавливаете какое-то значение, когда хотите обновить элемент управления, но меня больше интересует, что происходит внутри реализации по умолчанию для этих свойств: что они вызывают, чтобы повлиять на это поведение.


Обновить:

Ну, похоже, такого вызова нет, даже AffectsRender flag все еще вызывает внутреннее прохождение Arrange (согласно ответу CodeNaked ниже), но я опубликовал второй ответ, который показывает встроенное поведение, а также обходной путь, чтобы подавить выполнение кода прохода макета с простым обнуляемым размером, как флаг. Увидеть ниже.

4 ответа

Решение

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

Обновление: с тех пор я перенес принятый ответ сюда по двум причинам. Во-первых, я хочу, чтобы люди знали, что есть решение этой проблемы (большинство людей только читают принятый ответ и идут дальше), и во-вторых, учитывая, что у него повторение 25 КБ, я не думаю, что он будет против, если я возьму его обратно!:)

Вот что я сделал. Чтобы проверить это, я создал этот подкласс...

public class TestPanel : DockPanel
{
    protected override Size MeasureOverride(Size constraint)
    {
        System.Console.WriteLine("MeasureOverride called for " + this.Name + ".");
        return base.MeasureOverride(constraint);
    }

    protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
    {
        System.Console.WriteLine("ArrangeOverride called for " + this.Name + ".");
        return base.ArrangeOverride(arrangeSize);
    }

    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        System.Console.WriteLine("OnRender called for " + this.Name + ".");
        base.OnRender(dc);
    }

}

... который я выложил так (обратите внимание, что они вложены):

<l:TestPanel x:Name="MainTestPanel" Background="Yellow">

    <Button Content="Test" Click="Button_Click" DockPanel.Dock="Top" HorizontalAlignment="Left" />

    <l:TestPanel x:Name="InnerPanel" Background="Red" Margin="16" />

</l:TestPanel>

Когда я изменил размер окна, я получил это...

MeasureOverride called for MainTestPanel.
MeasureOverride called for InnerPanel.
ArrangeOverride called for MainTestPanel.
ArrangeOverride called for InnerPanel.
OnRender called for InnerPanel.
OnRender called for MainTestPanel.

но когда я позвонил InvalidateVisual на "MainTestPanel" (в событии "Click") я получил это вместо...

ArrangeOverride called for MainTestPanel.
OnRender called for MainTestPanel.

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

Это не идеально, как будто у вас есть очень тяжелый расчет внутри ArrangeOverride в вашем подклассе (что, к сожалению, мы делаем) это все еще выполняется (пере), но по крайней мере дети не попадают в ту же судьбу.

Однако, если вы знаете, что ни у одного из дочерних элементов управления нет свойства с установленным битом AffectsParentArrange (опять-таки, что мы и делаем), вы можете сделать это лучше и использовать Nullable. Size в качестве флага для подавления логики ArrangeOverride от повторного входа, кроме случаев, когда это необходимо, например...

public class TestPanel : DockPanel
{
    Size? arrangeResult;

    protected override Size MeasureOverride(Size constraint)
    {
        arrangeResult = null;
        System.Console.WriteLine("MeasureOverride called for " + this.Name + ".");
        return base.MeasureOverride(constraint);
    }

    protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
    {
        if(!arrangeResult.HasValue)
        {
            System.Console.WriteLine("ArrangeOverride called for " + this.Name + ".");
            // Do your arrange work here
            arrangeResult = base.ArrangeOverride(arrangeSize);
        }

        return arrangeResult.Value;
    }

    protected override void OnRender(System.Windows.Media.DrawingContext dc)
    {
        System.Console.WriteLine("OnRender called for " + this.Name + ".");
        base.OnRender(dc);
    }

}

Теперь, если что-то определенно не нужно повторно выполнить логику упорядочения (как это делает вызов MeasureOverride), вы получите только OnRender, и если вы хотите явно принудительно применить логику упорядочения, просто обнулите размер, вызовите InvalidateVisual и Боб - ваш дядя!:)

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

К сожалению, вы должны вызвать InvalidateVisual, который вызывает InvalidateArrange внутренне. OnRender метод вызывается как часть фазы упорядочения, поэтому вам нужно указать WPF переупорядочить элемент управления (что делает InvalidateArrange) и что ему нужно перерисовать (что делает InvalidateVisual).

FrameworkPropertyMetadata.AffectsRender опция просто говорит WPF звонить InvalidateVisual когда связанное свойство изменяется.

Если у вас есть элемент управления (давайте назовем этот MainControl), который переопределяет OnRender и содержит несколько дочерних элементов управления, то вызов InvalidateVisual может потребовать перераспределения или даже повторного измерения дочерних элементов управления. Но я считаю, что в WPF предусмотрена оптимизация, чтобы предотвратить перестановку дочерних элементов управления, если их доступное пространство не изменилось.

Вы можете обойти это, переместив логику рендеринга в отдельный элемент управления (скажем, NestedControl), который будет визуальным потомком MainControl. MainControl может добавить это как визуальный дочерний элемент автоматически или как часть его ControlTemplate, но он должен быть самым низким дочерним элементом в z-порядке. Вы могли бы тогда выставить InvalidateNestedControl метод типа MainControl, который будет вызывать InvalidateVisual для NestedControl.

Вы не должны звонить InvalidateVisual() если размер вашего элемента управления не изменится, и даже тогда есть другие способы вызвать изменение макета.

Для эффективного обновления визуального элемента управления без изменения его размера. Использовать DrawingGroup, Вы создаете DrawingGroup и помещаете его в DrawingContext в течение OnRender() а затем в любое время после этого вы можете Open() DrawingGroup изменить его команды визуального рисования, и WPF автоматически и эффективно отрендерит эту часть пользовательского интерфейса. (Вы также можете использовать эту технику с RenderTargetBitmap если вы предпочитаете иметь растровое изображение, в которое вы можете вносить дополнительные изменения, а не перерисовывать каждый раз)

Вот как это выглядит:

DrawingGroup backingStore = new DrawingGroup();

protected override void OnRender(DrawingContext drawingContext) {      
    base.OnRender(drawingContext);            

    Render(); // put content into our backingStore
    drawingContext.DrawDrawing(backingStore);
}

// I can call this anytime, and it'll update my visual drawing
// without ever triggering layout or OnRender()
private void Render() {            
    var drawingContext = backingStore.Open();
    Render(drawingContext);
    drawingContext.Close();            
}

private void Render(DrawingContext drawingContext) {
    // put your render code here
}

Вот еще один взлом: http://geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx

Короче говоря, вы вызываете вызывать некоторый фиктивный делегат с приоритетом DispatcherPriority.Render, который вызовет вызов всего с таким приоритетом или выше, что вызовет повторное рендеринг.

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