代码之家  ›  专栏  ›  技术社区  ›  Nick Udell

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

  •  15
  • Nick Udell  · 技术社区  · 16 年前

    我知道这是一个很受欢迎的问题,但我找不到任何确切的答案,但如果我在搜索中遗漏了什么,我会道歉。

    我正在尝试创建一个控件,然后在运行时使用以下代码对其进行度量(然后,度量值将用于选取框样式滚动控件-每个控件的大小都不同):

    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;
    

    代码创建一个标签控件,并对其应用样式。样式包含绑定到对象“task”的控件模板。当我创建项时,它看起来很完美,但是当我尝试使用上面的任何属性来度量控件时,我得到了以下结果(我依次执行并检查每个属性):

    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的小把戏? 真正地 恨我?

    第二次更新:

    在进一步搜索之后,我发现measure()只适用于原始元素的大小。如果控件模板通过实际添加控件来修改这一点,那么最好的方法可能是测量每一个控件,但我承认这不仅仅是一个小问题。

    编译器必须有某种方法来测量控件的所有内容,因为它必须使用这些方法来放置项,例如,堆栈面板。一定有什么方法可以接近它,但现在我完全没有主意了。

    3 回复  |  直到 16 年前
        1
  •  24
  •   Joe White    16 年前

    只有在WPF通过布局后,控件才会有大小。我认为这是异步发生的。如果您在构造函数中这样做,您可以钩住加载的事件——布局将在那时发生,并且您在构造函数中添加的任何控件都将被调整大小。

    然而,另一种方法是要求控件计算它想要的大小。为此,你打电话 Measure ,并传递建议的大小。在这种情况下,您希望传递一个无限大的大小(意味着控件可以像它喜欢的那样大),因为这就是画布在布局过程中要做的:

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

    调用Measure实际上不会更改控件的宽度、renderSize或actualWidth。它只是告诉标签计算它想要的大小,并将该值放入它想要的大小(一个唯一目的是保存上一次度量调用结果的属性)。

        2
  •  3
  •   Tim Lloyd    16 年前

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

    当您通过编程更新或创建控件时,有一个不明显的机制也在启动(对于像我这样的初学者来说——尽管我以前做过Windows消息处理,但这让我很困惑…)。将控制队列相关消息更新并创建到UI调度队列中,其中重要的一点是这些消息将在将来的某个时间点得到处理。某些属性取决于正在处理的这些消息,例如实际宽度。在这种情况下,消息会导致呈现控件,然后更新与呈现控件关联的属性。

    以编程方式创建控件时,并不明显存在异步消息处理,您必须等待这些消息被处理,然后才能更新某些属性,例如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;    
        }));
    

    更新

    回应你的评论。如果希望添加其他代码,可以按如下方式组织代码。重要的一点是,Dispatcher调用确保在依赖于所呈现控件的代码执行之前等待控件的呈现:

        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...
    
        3
  •  1
  •   Nick Udell    16 年前

    解决了!

    问题在我的XAML中。在标签模板的最高级别,有一个没有高度或宽度字段的父画布。因为它不必为其子级修改其大小,所以它一直被设置为0,0。通过删除它并将根节点替换为边框(边框必须调整大小以适合其子节点),高度和宽度字段将更新并传播回my code on measure()调用。

    推荐文章