测量 WPF 运行时创建的控件

发布于 2024-09-12 22:06:34 字数 6707 浏览 4 评论 0原文

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

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

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

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

I recognise this is a popular question but I couldn't find anything that answered it exactly, but I apologise if I've missed something in my searches.

I'm trying to create and then measure a control at runtime using the following code (the measurements will then be used to marquee-style scroll the controls - each control is a different size):

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;

The code creates a Label control and applies a style to it. The style contains my control template which binds to the object "task". When I create the item it appears perfectly, however when I try to measure the control using any of the properties above I get the following results (I step through and inspect each property in turn):

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

Is there any way to get the rendered height and width of a control you create at runtime?

UPDATE:

Sorry for deselecting your solutions as answers. They work on a basic example system I set up, but seemingly not with my complete solution.

I think it may be something to do with the style, so sorry for the mess, but I'm pasting the entire thing here.

First, resources:

    <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>

And the style itself:

    <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>

Also, the code I'm using:

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));
  }

Each time any of those is called the value is returned as either 0 or not a number, yet when the label is rendered it clearly has a size as it is visible. I have tried running this code from a button press when the label is visible on-screen, but that produces the same results.

Is it because of the style I'm using? Databindings perhaps? Is what I've done wrong blindingly obvious or do the WPF Gremlins just really hate me?

Second Update:

After further searching I've discovered that Measure() only applies to the original element size. If a control template modifies this by actually adding controls then the best method would probably be to measure each one, but I admit that's more than a little messy.

The compiler must have some way of measuring all of the contents of a control as it must use that to place items in, for example, stack-panels. There must be some way to access it, but for now I'm entirely out of ideas.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

能否归途做我良人 2024-09-19 22:06:34

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

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

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

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

The control won't have a size until WPF does a layout pass. I think that happens asynchronously. If you're doing this in your constructor, you can hook the Loaded event -- the layout will have happened by then, and any controls you added in the constructor will have been sized.

However, another way is to ask the control to calculate what size it wants to be. To do that, you call Measure, and pass it a suggested size. In this case, you want to pass an infinite size (meaning the control can be as large as it likes), since that's what Canvas is going to do in the layout pass:

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

The call to Measure doesn't actually change the control's Width, RenderSize, or ActualWidth. It just tells the Label to calculate what size it wants to be, and put that value in its DesiredSize (a property whose sole purpose is to hold the result of the last Measure call).

蓝礼 2024-09-19 22:06:34

我对 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...

I am pretty new to WPF, but here's my understanding.

When you update or create controls programatically, there is a non-obvious mechanism that is also firing (for a beginner like me anyway - although I've done windows message processing before, this caught me out...). Updates and creation of controls queue associated messages onto the UI dispatch queue, where the important point is that these will get processed at some point in the future. Certain properties depend on these messages being processed e.g. ActualWidth. The messages in this case cause the control to be rendered and then the properties associated with a rendered control to be updated.

It is not obvious when creating controls programatically that there is asynchronous message processing happening and that you have to wait for these messages to be processed before some of the properties will have been updated e.g. ActualWidth.

If you wait for existing messages to be processed before accessing ActualWidth, then it will have been updated:

    //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;    
    }));

Update

In response to your comment. If you wish to add other code, you can organize your code as follows. The important bit is that the dispatcher call ensures that you wait until the controls have been rendered before your code that depends on them being rendered executes:

    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...
暮光沉寂 2024-09-19 22:06:34

解决了!

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

Solved it!

The issue was in my XAML. At the highest level of my label's template there was a parent canvas that had no height or width field. Since this did not have to modify its size for its children it was constantly set to 0,0. By removing it and replacing the root node with a border, which must resize to fit its children, the height and width fields are updated and propagated back to my code on Measure() calls.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文