WPF:如何使画布自动调整大小?

54

我希望我的Canvas可以自动调整大小以适应其项目的大小,这样ScrollViewer滚动条就会有正确的范围。 这是否可以在XAML中完成?

<ScrollViewer HorizontalScrollBarVisibility="Auto" x:Name="_scrollViewer">
    <Grid x:Name ="_canvasGrid" Background="Yellow">
        <Canvas x:Name="_canvas" HorizontalAlignment="Left" VerticalAlignment="Top" Background="Green"></Canvas>
        <Line IsHitTestVisible="False" .../>
    </Grid>
</ScrollViewer>
在上面的代码中,canvas 的大小始终为 0,尽管它不会剪裁其子元素。
14个回答

54

这是不可能的(请参见下面MSDN的片段)。但是,如果您想要滚动条和自动调整大小,请考虑使用 Grid(网格) ,并使用Margin属性在此Grid上定位您的项目。Grid会告诉ScrollViewer他想要多大,您将获得滚动条。Canvas始终告诉ScrollViewer它不需要任何大小。:)

Grid让您同时享受两个世界 - 只要将所有元素放入单个单元格中,您就可以同时获得:任意定位和自动调整大小。总的来说,最好记住大多数面板控件(DockPanel、StackPanel等)都可以通过Grid控件实现。

来自MSDN

Canvas是唯一一个没有固有布局特性的面板元素。除非它是自动调整其子元素大小的元素的子级,否则Canvas具有默认高度和宽度属性为零。Canvas的子元素永远不会被重新调整大小,它们只是被定位在它们指定的坐标上。这为不需要或不希望固有大小约束或对齐的情况提供了灵活性。对于希望自动调整大小并对齐子内容的情况,通常最好使用Grid元素。

希望这能帮到您。


9
我从Canvas切换到Grid后成功了,虽然需要一些微调。我必须进行两个更改:(1)所有原来使用的Canvas.Left和Canvas.Top属性都需要改为使用Margin.Left和Margin.Top属性(Margin.Right和Margin.Bottom可以保持为0);(2)在Grid中的每个元素上使用HorizontalAlignment="Left"和VerticalAlignment="Top"属性。默认的“Stretch”模式可能会导致当边距为0时元素居中。 - Qwertie
3
如果你想让子元素自动调整大小和对齐,通常最好使用Grid元素。但原问题是关于调整Canvas大小,而不是子元素。我认为illef提供的解决方案更好地回答了这个问题,并避免了在所有子元素上设置这么多属性。使用illef的答案,你只需设置附加属性Top和Left,这是一个更整洁的解决方案。一旦定义了新的Canvas对象,它就是一个可重用的解决方案,可以在项目中的其他地方使用。 - MikeKulls
我对此的问题在于,当渲染时,它不会重叠控件,而是会将它们推到各个地方,除非我做错了什么。此外,illef解决方案由于某种原因计算错误。 - Geoff Scott

44

我这里只是在复制 illef 的回答,但是针对 PilotBob 的问题,你只需要像这样定义一个画布对象:

public class CanvasAutoSize : Canvas
{
    protected override System.Windows.Size MeasureOverride(System.Windows.Size constraint)
    {
        base.MeasureOverride(constraint);
        double width = base
            .InternalChildren
            .OfType<UIElement>()
            .Max(i => i.DesiredSize.Width + (double)i.GetValue(Canvas.LeftProperty));

        double height = base
            .InternalChildren
            .OfType<UIElement>()
            .Max(i => i.DesiredSize.Height + (double)i.GetValue(Canvas.TopProperty));

        return new Size(width, height);
    }
}

然后在你的XAML中使用CanvasAutoSize。

            <local:CanvasAutoSize VerticalAlignment="Top" HorizontalAlignment="Left"></local:CanvasAutoSize>

相比于上面使用网格作为解决方案,我更喜欢这个方案,因为它通过附加属性的方式工作,只需要在元素上设置较少的属性。


1
为什么我在 MeasureOverride 中关于 'width' 和 'height' 的运行时错误显示两者都是 NaN? - jondinham
5
我找到原因了, CanvasAutoSize 中的所有元素必须设置 Canvas.Left 和 Canvas.Top。 - jondinham
5
我同意,如果你在布局方面做任何复杂的事情,这比使用网格(Grid)更好的解决方案。例如,我将一个控件的高度绑定到另一个控件上,添加边距(margin)会导致某种无限布局递归。不过,就像Paul Dinh所说,如果你没有在每个项上设置Canvas.Left和Canvas.Top,这个解决方案会出现问题,如果你有几个项在(0, 0)位置,这会很让人恼火。我将Max() lambda函数的主体更改为将Canvas.Left/Top分配给一个double,并检查double.IsNaN(),如果是则使用0.0代替。这很有效。 - Dana Cartwright
1
如果没有子元素,也会抛出异常。 - Robin Davies
2
@RobinDavies:我遇到了同样的问题 - 添加了 if (!base.InternalChildren.OfType<UIElement>().Any()) return new System.Windows.Size(1, 1);,虽然可能有更优雅的处理方法。 - Tony Delroy
显示剩余2条评论

9

我需要做这个,但是我遇到了代码上的问题。你所说的“定义新画布”是什么意思?你是指从Canvas派生的类吗?如果是这样,我得到了“InternalChildren没有def”并且无法覆盖继承的成员。 - PilotBob
+1 对于如何处理需求的好主意!此外,我建议在您的代码中添加 double.IsNaN 检查来检查顶部和左侧的值,并在它们为 NaN 时将它们设置为零。 - HCL

7
基本上它需要完全重写Canvas。之前提出的覆盖MeasureOverride的解决方案会失败,因为默认的Canvas.Left/.Top及其它属性使Arrangment失效,但也需要使measure失效。(第一次可以获得正确的大小,但如果在初始布局之后移动元素,则大小不会改变)。网格视图的解决方案更或多或少是合理的,但绑定边距以获得x-y位移可能会对其他代码(特别是MVVM中的代码)造成很大麻烦。我曾努力尝试使用网格视图的解决方法,但与View/ViewModel交互和滚动行为的复杂性最终驱使我选择了这种解决方法。这种方法简单而直接,并且有效。

重写ArrangeOverride和MeasureOverride并不那么复杂。你还必须编写至少同样数量的代码来处理Grid/Margin的愚蠢。所以就是这样。

以下是一个更完整的解决方案。非零边距行为未经测试。如果你需要除了Left和Top之外的任何东西,那么这至少提供了一个起点。

警告:你必须使用AutoResizeCanvas.Left和AutoResizeCanvas.Top附加属性,而不是Canvas.Left和Canvas.Top。剩余的Canvas属性尚未实现。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace Mu.Controls
{
    public class AutoResizeCanvas : Panel
    {



        public static double GetLeft(DependencyObject obj)
        {
            return (double)obj.GetValue(LeftProperty);
        }

        public static void SetLeft(DependencyObject obj, double value)
        {
            obj.SetValue(LeftProperty, value);
        }

        public static readonly DependencyProperty LeftProperty =
            DependencyProperty.RegisterAttached("Left", typeof(double),
            typeof(AutoResizeCanvas), 
            new FrameworkPropertyMetadata(0.0, OnLayoutParameterChanged));

        private static void OnLayoutParameterChanged(
                DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // invalidate the measure of the enclosing AutoResizeCanvas.
            while (d != null)
            {
                AutoResizeCanvas canvas = d as AutoResizeCanvas;
                if (canvas != null)
                {
                    canvas.InvalidateMeasure();
                    return;
                }
                d = VisualTreeHelper.GetParent(d);
            }
        }




        public static double GetTop(DependencyObject obj)
        {
            return (double)obj.GetValue(TopProperty);
        }

        public static void SetTop(DependencyObject obj, double value)
        {
            obj.SetValue(TopProperty, value);
        }

        public static readonly DependencyProperty TopProperty =
            DependencyProperty.RegisterAttached("Top", 
                typeof(double), typeof(AutoResizeCanvas),
                new FrameworkPropertyMetadata(0.0, OnLayoutParameterChanged));





        protected override Size MeasureOverride(Size constraint)
        {
            Size availableSize = new Size(double.MaxValue, double.MaxValue);
            double requestedWidth = MinimumWidth;
            double requestedHeight = MinimumHeight;
            foreach (var child in base.InternalChildren)
            {
                FrameworkElement el = child as FrameworkElement;

                if (el != null)
                {
                    el.Measure(availableSize);
                    Rect bounds, margin;
                    GetRequestedBounds(el,out bounds, out margin);

                    requestedWidth = Math.Max(requestedWidth, margin.Right);
                    requestedHeight = Math.Max(requestedHeight, margin.Bottom);
                }
            }
            return new Size(requestedWidth, requestedHeight);
        }
        private void GetRequestedBounds(
                            FrameworkElement el, 
                            out Rect bounds, out Rect marginBounds
                            )
        {
            double left = 0, top = 0;
            Thickness margin = new Thickness();
            DependencyObject content = el;
            if (el is ContentPresenter)
            {
                content = VisualTreeHelper.GetChild(el, 0);
            }
            if (content != null)
            {
                left = AutoResizeCanvas.GetLeft(content);
                top = AutoResizeCanvas.GetTop(content);
                if (content is FrameworkElement)
                {
                    margin = ((FrameworkElement)content).Margin;
                }
            }
            if (double.IsNaN(left)) left = 0;
            if (double.IsNaN(top)) top = 0;
            Size size = el.DesiredSize;
            bounds = new Rect(left + margin.Left, top + margin.Top, size.Width, size.Height);
            marginBounds = new Rect(left, top, size.Width + margin.Left + margin.Right, size.Height + margin.Top + margin.Bottom);
        }


        protected override Size ArrangeOverride(Size arrangeSize)
        {
            Size availableSize = new Size(double.MaxValue, double.MaxValue);
            double requestedWidth = MinimumWidth;
            double requestedHeight = MinimumHeight;
            foreach (var child in base.InternalChildren)
            {
                FrameworkElement el = child as FrameworkElement;

                if (el != null)
                {
                    Rect bounds, marginBounds;
                    GetRequestedBounds(el, out bounds, out marginBounds);

                    requestedWidth = Math.Max(marginBounds.Right, requestedWidth);
                    requestedHeight = Math.Max(marginBounds.Bottom, requestedHeight);
                    el.Arrange(bounds);
                }
            }
            return new Size(requestedWidth, requestedHeight);
        }

        public double MinimumWidth
        {
            get { return (double)GetValue(MinimumWidthProperty); }
            set { SetValue(MinimumWidthProperty, value); }
        }

        public static readonly DependencyProperty MinimumWidthProperty =
            DependencyProperty.Register("MinimumWidth", typeof(double), typeof(AutoResizeCanvas), 
            new FrameworkPropertyMetadata(300.0,FrameworkPropertyMetadataOptions.AffectsMeasure));



        public double MinimumHeight
        {
            get { return (double)GetValue(MinimumHeightProperty); }
            set { SetValue(MinimumHeightProperty, value); }
        }

        public static readonly DependencyProperty MinimumHeightProperty =
            DependencyProperty.Register("MinimumHeight", typeof(double), typeof(AutoResizeCanvas), 
            new FrameworkPropertyMetadata(200.0,FrameworkPropertyMetadataOptions.AffectsMeasure));



    }


}

2
知道何时放弃hackish解决方案是无价的!在许多情况下,实现自定义面板是正确的解决方案。 - N_A
这是最好的答案。 - The One

6

我看到你有一个可行的解决方案,但是我想分享一下我的想法。

<Canvas x:Name="topCanvas">
    <Grid x:Name="topGrid" Width="{Binding ElementName=topCanvas, Path=ActualWidth}" Height="{Binding ElementName=topCanvas, Path=ActualHeight}">
        ...Content...
    </Grid>
</Canvas>

以上技术将允许您在画布内嵌套网格并进行动态调整大小。通过使用维度绑定,可以将动态材料与静态材料混合使用,进行分层等操作。有太多可能性可以提到,有些比其他的更难。例如,我使用这种方法模拟内容从一个网格位置移动到另一个网格位置的动画效果-在动画完成事件中进行实际放置。祝你好运。


4
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    autoSizeCanvas(canvas1);
}

void autoSizeCanvas(Canvas canv)
{
    int height = canv.Height;
    int width = canv.Width;
    foreach (UIElement ctrl in canv.Children)
    {
        bool nullTop = ctrl.GetValue(Canvas.TopProperty) == null || Double.IsNaN(Convert.ToDouble(ctrl.GetValue(Canvas.TopProperty))),
                nullLeft = ctrl.GetValue(Canvas.LeftProperty) == null || Double.IsNaN(Convert.ToDouble(ctrl.GetValue(Canvas.LeftProperty)));
        int curControlMaxY = (nullTop ? 0 : Convert.ToInt32(ctrl.GetValue(Canvas.TopProperty))) +
            Convert.ToInt32(ctrl.GetValue(Canvas.ActualHeightProperty)
            ),
            curControlMaxX = (nullLeft ? 0 : Convert.ToInt32(ctrl.GetValue(Canvas.LeftProperty))) +
            Convert.ToInt32(ctrl.GetValue(Canvas.ActualWidthProperty)
            );
        height = height < curControlMaxY ? curControlMaxY : height;
        width = width < curControlMaxX ? curControlMaxX : width;
    }
    canv.Height = height;
    canv.Width = width;
}

在这个函数中,我试图找到画布中控件可以存在的最大X位置和Y位置。
请仅在Loaded事件或之后使用该函数,而不要在构造函数中使用。窗口必须在加载前测量。

3

将控件的高度/宽度绑定到画布中实际大小对我很有帮助:

        <ScrollViewer VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Visible">
            <Canvas Height="{Binding ElementName=myListBox, Path=ActualHeight}"
                    Width="{Binding ElementName=myListBox, Path=ActualWidth}">
                <ListBox x:Name="myListBox" />
            </Canvas>
        </ScrollViewer>

4
如果你只需要一个单独的子元素来控制画布大小,那么这样做可能是可行的。但是为什么不仅使用ListBox而不是将它放在Canvas和ScrollViewer上呢? - Qwertie

3
作为对@MikeKulls答案的改进,这里提供了一个版本,在画布中没有UI元素或者有没有Canvas.Top或Canvas.Left属性的UI元素时不会抛出异常:
public class AutoResizedCanvas : Canvas
{
    protected override System.Windows.Size MeasureOverride(System.Windows.Size constraint)
    {
        base.MeasureOverride(constraint);
        double width = base
            .InternalChildren
            .OfType<UIElement>()
            .Where(i => i.GetValue(Canvas.LeftProperty) != null)
            .Max(i => i.DesiredSize.Width + (double)i.GetValue(Canvas.LeftProperty));

        if (Double.IsNaN(width))
        {
            width = 0;
        }

        double height = base
            .InternalChildren
            .OfType<UIElement>()
            .Where(i => i.GetValue(Canvas.TopProperty) != null)
            .Max(i => i.DesiredSize.Height + (double)i.GetValue(Canvas.TopProperty));

        if (Double.IsNaN(height))
        {
            height = 0;
        }

        return new Size(width, height);
    }
}

0

使用 Grid 可以自动调整大小以适应内容,而无需设置其他参数,只需要设置 VerticalAlignmentHorizontalAlignment。例如:

<Grid VerticalAlignment="Top" HorizontalAlignment="Center">
    <Polygon Points="0, 0, 0, 100, 80, 100" Panel.ZIndex="6" Fill="Black" Stroke="Black" StrokeStartLineCap="Round" StrokeDashCap="Round" StrokeLineJoin="Round" StrokeThickness="10"/>
</Grid>

0

我也遇到了这个问题,我的问题是当Canvas调整大小时,网格没有自动调整大小,这要归功于重写的MeasureOverride函数。

我的问题: WPF MeasureOverride loop


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接