【问题标题】:Measuring controls created at runtime in WPF测量在 WPF 中运行时创建的控件
【发布时间】:2011-03-25 00:25:51
【问题描述】:

我知道这是一个热门问题,但我找不到任何可以准确回答的问题,但如果我在搜索中遗漏了某些内容,我深表歉意。

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

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;

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

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

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

【问题讨论】:

  • 你在哪里运行这段代码?在窗口的构造函数中?如果是这样,您必须等到控件已呈现。

标签: c# wpf data-binding styles controltemplate


【解决方案1】:

解决了!

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

【讨论】:

    【解决方案2】:

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

    当您以编程方式更新或创建控件时,还有一种不明显的机制也在触发(无论如何,对于像我这样的初学者来说——尽管我之前已经完成了 Windows 消息处理,但这让我感到困惑......)。控件队列相关消息的更新和创建到 UI 调度队列,其中重要的一点是这些将在将来的某个时间点得到处理。某些属性取决于正在处理的这些消息,例如实际宽度。这种情况下的消息会导致控件被呈现,然后与呈现的控件关联的属性被更新。

    在以编程方式创建控件时,并不明显会发生异步消息处理,并且您必须等待这些消息被处理,然后才能更新某些属性,例如实际宽度。

    如果您在访问 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...
    

    【讨论】:

    • 啊,太好了,谢谢。是否存在某种事件或布尔值来检查现有消息的处理时间?我有依赖于检查这些值的代码,以及需要在控件创建后按顺序运行的代码。如果没有,我相信我可以找到某种解决方案。
    【解决方案3】:

    在 WPF 完成布局传递之前,控件不会有大小。我认为这是异步发生的。如果您在构造函数中执行此操作,则可以挂钩 Loaded 事件 - 届时布局将发生,并且您在构造函数中添加的任何控件都将调整大小。

    但是,另一种方法是让控件计算它想要的大小。为此,您调用Measure,并传递一个建议的大小。在这种情况下,您想要传递一个无限大小(这意味着控件可以任意大),因为这就是 Canvas 在布局传递中要做的事情:

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

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

    【讨论】:

    • 这是一个非常优雅的解决方案,但两者都适用于不同的情况,所以我将它们都设置为答案。我认为这个最适合我的问题,谢谢。
    • 我在控件上设置了一个 DataContext,这应该会导致控件变得比默认的“首选大小”更高。我创建了一个高度为AutoUserControl。在这种情况下,这个答案似乎对我不起作用。在将 DataContext 设置为“增长”之后,我必须执行 UpdateLayout()
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-06-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多