测量在WPF中在运行时创建的控件

时间:2010-08-04 00:00:14

标签: c# wpf data-binding styles controltemplate

我知道这是一个很受欢迎的问题,但我找不到任何可以回答的问题,但如果我在搜索中遗漏了某些内容,我会道歉。

我正在尝试使用以下代码在运行时创建并测量控件(然​​后测量将用于选框样式滚动控件 - 每个控件的大小不同):

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

有没有办法获得在运行时创建的控件的渲染高度和宽度?

更新

很抱歉取消选择您的解决方案作为答案。他们在我设置的基本示例系统上工作,但似乎没有完整的解决方案。

我认为它可能与风格有关,所以对于这个烂摊子感到抱歉,但我在这里粘贴整个东西。

首先,资源:                               

    <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()仅适用于原始元素大小。如果控件模板通过实际添加控件来修改它,那么最好的方法可能是测量每一个,但我承认这不仅有点凌乱。

编译器必须有某种方法来测量控件的所有内容,因为它必须使用它来放置项目,例如堆栈面板。必须有一些方法来访问它,但现在我完全没有想法。

3 个答案:

答案 0 :(得分:24)

在WPF执行布局传递之前,控件的大小不会很大。我认为这是异步发生的。如果你在构造函数中这样做,你可以挂钩Loaded事件 - 那时布局就会发生,你在构造函数中添加的任何控件都会被调整大小。

然而,另一种方法是让控件计算它想要的尺寸。为此,请致电Measure,并将其传递给建议的尺寸。在这种情况下,你想传递一个无限大小(意味着控件可以像它喜欢的那样大),因为这就是Canvas在布局传递中要做的事情:

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

对Measure的调用实际上并不会更改控件的Width,RenderSize或ActualWidth。它只是告诉Label计算它想要的大小,并将该值放入DesiredSize(一个属性,其唯一目的是保存最后一次Measure调用的结果)。

答案 1 :(得分:3)

我对WPF很新,但这是我的理解。

当你以编程方式更新或创建控件时,有一个非显而易见的机制也会被触发(对于像我这样的初学者来说 - 虽然之前我已经完成了Windows消息处理,但这让我感到震惊......)。控件的更新和创建将关联的消息排队到UI调度队列,其中重要的一点是这些将在未来的某个时刻得到处理。某些属性取决于正在处理的这些消息, 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...

答案 2 :(得分:1)

解决了!

问题出在我的XAML中。在我的标签模板的最高级别,有一个没有高度或宽度字段的父画布。由于这不必为其子项修改其大小,因此它经常设置为0,0。通过删除它并用边框替换根节点,边框必须调整大小以适合其子节点,高度和宽度字段将更新并传播回我在Measure()调用上的代码。