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

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

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

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


Обновление:

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

Ответы [ 4 ]

19 голосов
/ 18 октября 2011

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

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

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

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

10 голосов
/ 18 октября 2011

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

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

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

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 и Боб дядя! :)

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

2 голосов
/ 08 июня 2017

Вы не должны звонить 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
}
1 голос
/ 18 октября 2011

Вот еще один хак: http://geekswithblogs.net/NewThingsILearned/archive/2008/08/25/refresh--update-wpf-controls.aspx

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

...