Подтвердить что ты не робот

Элементы управления измерением, созданные во время выполнения в WPF

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

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

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

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

UPDATE:

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

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

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

    <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 Gremlins просто ненавидят меня?

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

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

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

4b9b3361

Ответ 1

Решил!

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

Ответ 2

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

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

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

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

Ответ 3

Я новичок в 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...