Измерение элементов управления, созданных во время выполнения в WPF - PullRequest
15 голосов
/ 04 августа 2010

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

Я пытаюсь создать, а затем измеритьэлемент управления во время выполнения с использованием следующего кода (измерения будут использоваться для прокрутки элементов управления в стиле выделения - каждый элемент управления имеет свой размер):

Label lb = new Label();
lb.DataContext= task;
Style style = (Style)FindResource("taskStyle");
lb.Style = style;

cnvMain.Children.Insert(0,lb);

width = lb.RenderSize.Width;
width = lb.ActualWidth;
width = lb.Width;

Код создает элемент управления Label и применяет стильк этому.Стиль содержит мой шаблон управления, который привязывается к объекту «задача».Когда я создаю элемент, он выглядит идеально, однако, когда я пытаюсь измерить элемент управления, используя любое из указанных выше свойств, я получаю следующие результаты (я прохожу и проверяю каждое свойство по очереди):

lb.Width = NaN
lb.RenderSize.Width = 0
lb.ActualWidth = 0

IsЕсть ли способ получить визуализированную высоту и ширину создаваемого вами элемента управления во время выполнения?

ОБНОВЛЕНИЕ:

Извините, что отменили выбор решений в качестве ответов.Они работают на базовом примере системы, которую я настроил, но, похоже, не с моим полным решением.

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

Во-первых, ресурсы:

    <Storyboard x:Key="mouseOverGlowLeave">
        <DoubleAnimation From="8" To="0" Duration="0:0:1" BeginTime="0:0:2" Storyboard.TargetProperty="GlowSize" Storyboard.TargetName="Glow"/>
    </Storyboard>

    <Storyboard x:Key="mouseOverTextLeave">
        <ColorAnimation From="{StaticResource buttonLitColour}" To="Gray" Duration="0:0:3" Storyboard.TargetProperty="Color" Storyboard.TargetName="ForeColour"/>
    </Storyboard>

    <Color x:Key="buttonLitColour" R="30" G="144" B="255" A="255" />

    <Storyboard x:Key="mouseOverText">
        <ColorAnimation From="Gray" To="{StaticResource buttonLitColour}" Duration="0:0:1" Storyboard.TargetProperty="Color" Storyboard.TargetName="ForeColour"/>
    </Storyboard>

И сам стиль:

    <Style x:Key="taskStyle" TargetType="Label">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate>
                    <Canvas x:Name="cnvCanvas">
                        <Border  Margin="4" BorderBrush="Black" BorderThickness="3" CornerRadius="16">
                            <Border.Background>
                                <LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
                                    <GradientStop Offset="1" Color="SteelBlue"/>
                                    <GradientStop Offset="0" Color="dodgerBlue"/>
                                </LinearGradientBrush>
                            </Border.Background>
                            <DockPanel Margin="8">
                                <DockPanel.Resources>
                                    <Style TargetType="TextBlock">
                                        <Setter Property="FontFamily" Value="corbel"/>
                                        <Setter Property="FontSize" Value="40"/>
                                    </Style>
                                </DockPanel.Resources>
                                <TextBlock VerticalAlignment="Center" Margin="4" Text="{Binding Path=Priority}"/>
                                <DockPanel DockPanel.Dock="Bottom">
                                    <Border Margin="4" BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
                                        <TextBlock Margin="4" DockPanel.Dock="Right" Text="{Binding Path=Estimate}"/>
                                    </Border>
                                    <Border Margin="4" BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
                                        <TextBlock Margin="4" DockPanel.Dock="Left" Text="{Binding Path=Due}"/>
                                    </Border>
                                </DockPanel>
                                <DockPanel DockPanel.Dock="Top">
                                    <Border Margin="4" BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
                                        <TextBlock  Margin="4" Foreground="LightGray" DockPanel.Dock="Left" Text="{Binding Path=List}"/>
                                    </Border>
                                    <Border Margin="4" BorderBrush="SteelBlue" BorderThickness="1" CornerRadius="4">
                                        <TextBlock Margin="4" Foreground="White" DockPanel.Dock="Left" Text="{Binding Path=Name}"/>
                                    </Border>
                                </DockPanel>
                            </DockPanel>
                        </Border>
                    </Canvas>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

Кроме того, код, который я использую:

Label lb = new Label(); //the element I'm styling and adding
  lb.DataContext= task; //set the data context to a custom class
  Style style = (Style)FindResource("taskStyle"); //find the above style
  lb.Style = style;

  cnvMain.Children.Insert(0,lb); //add the style to my canvas at the top, so other overlays work
  lb.UpdateLayout(); //attempt a full layout update - didn't work
  Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>
  {
      //Synchronize on control rendering
      double width = lb.RenderSize.Width;
      width = lb.ActualWidth;
      width = lb.Width;
  })); //this didn't work either
  lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); //nor did this
  double testHeight = lb.DesiredSize.Height;
  double testWidth = lb.RenderSize.Width; //nor did this
  width2 = lb.ActualWidth; //or this
  width2 = lb.Width; //or this

//positioning for my marquee code - position off-screen to start with
  Canvas.SetTop(lb, 20);
  Canvas.SetTop(lb, -999);

//tried it here too, still didn't work
  lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
  testHeight = lb.DesiredSize.Height;
//add my newly-created label to a list which I access later to operate the marquee
  taskElements.AddLast(lb);
//still didn't work
  lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
  testHeight = lb.DesiredSize.Height;
//tried a mass-measure to see if that would work - it didn't
  foreach (UIElement element in taskElements)
  {
      element.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
  }

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

Это из-за стиля, который я использую?Возможно, данные?То, что я сделал неправильно, ослепительно очевидно, или гремлины WPF просто действительно ненавидят меня?

Второе обновление:

После дальнейшего поиска яобнаружил, что Measure () относится только к исходному размеру элемента.Если шаблон элемента управления изменяет это путем фактического добавления элементов управления, то, вероятно, лучшим методом будет измерение каждого из них, но я признаю, что это более чем грязно.

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

Ответы [ 3 ]

24 голосов
/ 04 августа 2010

Элемент управления не будет иметь размер, пока WPF не выполнит передачу макета.Я думаю, что это происходит асинхронно.Если вы делаете это в своем конструкторе, вы можете перехватить событие Loaded - к ​​тому времени произойдет макет, и все элементы управления, добавленные вами в конструктор, будут иметь размеры.

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

lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
Debug.WriteLine(lb.DesiredSize.Width);

Вызов Measureфактически не изменяет ширину элемента управления, RenderSize или ActualWidth.Он просто указывает Label рассчитать, какой размер он хочет иметь, и поместить это значение в свой DesiredSize (свойство, единственной целью которого является сохранение результата последнего вызова Measure).

3 голосов
/ 04 августа 2010

Я довольно новичок в WPF, но вот мое понимание.

Когда вы обновляете или создаете элементы управления программно, существует неочевидный механизм, который также запускается (для новичка, как я, в любом случае - хотя я уже делал обработку сообщений Windows, это застало меня врасплох ...) Обновления и создание элементов управления связывают сообщения, связанные с очередью отправки пользовательского интерфейса, где важным моментом является то, что они будут обрабатываться в определенный момент в будущем. Определенные свойства зависят от обработки этих сообщений, например, ActualWidth. Сообщения в этом случае приводят к отображению элемента управления, а затем обновляются свойства, связанные с отображаемым элементом управления.

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

Если вы ожидаете обработки существующих сообщений перед доступом к ActualWidth, то они будут обновлены:

    //These operations will queue messages on dispatch queue
    Label lb = new Label();
    canvas.Children.Insert(0, lb);

    //Queue this operation on dispatch queue
    Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>
    {
        //Previous messages associated with creating and adding the control
        //have been processed, so now this happens after instead of before...
        double width = lb.RenderSize.Width;
        width = lb.ActualWidth;
        width = lb.Width;    
    }));

Обновление

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

    Label lb = new Label();
    canvas.Children.Insert(0, lb);

    Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>
    {
        //Synchronize on control rendering
    }));

    double width = lb.RenderSize.Width;
    width = lb.ActualWidth;
    width = lb.Width;

    //Other code...
1 голос
/ 15 августа 2010

Решено!

Проблема была в моем XAML.На самом высоком уровне шаблона моей метки был родительский холст, у которого не было поля высоты или ширины.Так как это не должно было изменять его размер для его детей, это было постоянно установлено в 0,0.Удаляя его и заменяя корневой узел границей, размер которой должен соответствовать его дочерним элементам, поля высоты и ширины обновляются и передаются обратно в мой код при вызовах Measure ().

...