WPF SDK研究 之 Layout布局

这一章介绍Layout布局。

本章共计51个示例,全都在VS2008下.NET3.5测试通过,点击这里下载:Layout.rar


一则小技巧:建立名为defaultPage/Window时,VS会为我们生成名为@default的类,因为defaultC#中是一个关键字,ok,我们可以这样使用:
            @default dd = new @default();
但是在XAML中,
x:Class="FlowDocumentSamp.@default"
就是一个错误,编译器解析不出这个@default,所以不要建立default.xaml,可以取代以代首字母大写的形式:Default.xaml,就没有问题了。
因此,作出如下总结:
如果default.xaml仅仅是一个XAML文件,而没有设置x:Class属性,那么可以使用小写的名称。
如果在XAML中设置了x:Class,那么该文件的命名要格外注意,尽量不要使用关键字作为名称。如果非要这么做,那么要同时修改后台代码的类名以及相应隐藏文件的类名称为一个规范名称,同时修改x:Class属性,指定这个新的类名称。

在开始本章之前,有必要看一下继承关系:

        System.Windows.UIElement

          System.Windows.FrameworkElement

            System.Windows.Controls.Panel

              System.Windows.Controls.Canvas

              System.Windows.Controls.DockPanel

              System.Windows.Controls.Grid

              System.Windows.Controls.StackPanel

              System.Windows.Controls.VirtualizingPanel

              System.Windows.Controls.WrapPanel

              System.Windows.Controls.Primitives.TabPanel

              System.Windows.Controls.Primitives.ToolBarOverflowPanel

              System.Windows.Controls.Primitives.UniformGrid

本章的主题就是介绍Panel下派生的这些布局面板,以及如何自定义一个派生于Panel的类。

1BorderChangeProgrammatic

Border用于在另一个元素的周围绘制边框、背景。

Border只能具有一个子级。若要显示多个子元素,需要将一个附加的Panel元素放置在父Border中。然后可以将多个子元素放置在该Panel元素中。所以我们常常看到,Border介于Page(或Window)和布局面板(如Canvas)之间——所以我们不要混淆,Border不是Layout,而是Control

2CanvasAttachedProperties

这个例子分别用XAMLC#后台代码演示了Canvas的四个Attached定位属性:

Bottom, Left, Right, Top

如:

    <Canvas>

      <Button Canvas.Top="50">Canvas.Top="50"</Button>

在后台C#中表现为:

    Canvas.SetTop(myButton1, 50);

注:所谓Attached属性,就是Canvas.Top形式。

3CanvasCode

这个例子是上个例子的翻版,不再重述。

4CanvasOvwSample

这个例子是上个例子的翻版,不再重述。

注意到:蓝色CanvasXAML中的最后,所以盖住了其他两个Canvas,详细解释见下。

5CanvasPositioningProperties

这个示例演示了在Canvas中使用LengthConverter的方法,同前。

6CanvasZOrder

这个示例演示了Z-Order属性的使用。

正如我们之前看到的,Grid中的多个元素按照在XAML中的先后顺序,依次覆盖。如果想自定义在Z轴上的位置,可以使用Canvas.ZIndex属性来设置,值大的在上面。

7DockPanelCode

这个示例演示了如何用C#程序创建DockPanel并对Window窗体中的5Rectangle进行布局。这里,每个Rectangle都被设定了Dock枚举,并添加到myDockPanel.Children中,如以下代码:

            DockPanel.SetDock(rect4, Dock.Bottom);

           myDockPanel.Children.Add(rect4);

例外,对于最后一个不需要设定Dock枚举位置的元素——会自动填充剩余区域。

最后,直接将DockPanel实例添加到Window窗体中。

            // Add the DockPanel to the Window as Content and show the Window

            mainWindow.Content = myDockPanel;

注意:要把元素添加到myDockPanelChildren集合中。

8DockPanelDockPropertyCode

这个例子演示了DockPanelLastChildFill属性。这个属性默认是True的,那么DockPanel中的最后一个元素,将会自动填充剩余区域,而不管这个元素是否设置过Dock枚举位置。只有设置该属性为False,才能使该元素的Dock枚举位置生效。

在这个例子的DockPanel中,有两个元素rect1rect2,无论如何设置Dock位置,都不会生效,只能根据rect1的位置而自动调整——因为默认LastChildFillTrue;只有将其改为False,才可以看到效果,当两个元素各居左右时,中间会空出一段空白。

9DockPanelOvw

这个例子是示例-DockPanelCode的延续,从XAML语法介绍如何使用DockPanel进行布局。

10DockPanelOvw2

这个例子演示了如何在DockPanel中垂直排列3Button

11DockPanelSetDock

这个例子演示了在C#中如何设定元素的DockPanel位置,以及获取这个值。

也就是DockPanel的两个静态方法DockPanel.SetDockDockPanel.GetDock

              DockPanel.SetDock(txt1, System.Windows.Controls.Dock.Top);

              txt1.Text = "The Dock Property is set to " + DockPanel.GetDock(txt1);

12FontSizeConverter

这个示例演示了如何改变一段文本的字体和大小。

先看改变文字大小的方法:

            ListBoxItem li = ((sender as ListBox).SelectedItem as ListBoxItem);

            FontSizeConverter myFontSizeConverter = new FontSizeConverter();

            text1.FontSize = (Double)myFontSizeConverter.ConvertFromString(li.Content.ToString());

FontSizeConverter实例负责将String类型的数字转换成一个Object类型的小数,这时候要强制转换成Double,才好设置给FontSize属性。如果参数不是数字,在强制转换时就会抛出异常。

再看变文本字体的方法:

            ListBoxItem li2 = ((sender as ListBox).SelectedItem as ListBoxItem);

            text1.FontFamily = new FontFamily(li2.Content.ToString());

有趣的是,FontFamily居然位于System.Windows.Media命名空间下。FontFamily的构造函数接受一个String类型。

当然也可以指定

FontFamily="file:///d:/MyFonts/#Pericles Light">        //绝对路径

FontFamily="./resources/#Pericles Light, Verdana"       //相对路径,并且是一个字体数组

13Grid

这个例子分别使用编程和XAML两种方式建立Grid布局。

Grid中开始部分,使用Grid.ColumnDefinitionsGrid.RowDefinitions,事先规定Grid中行和列的数量:如下是一个34行的Grid

    <Grid ShowGridLines="True">   // ShowGridLines属性决定了是否显示间隔线

      <Grid.ColumnDefinitions>

        <ColumnDefinition />

        <ColumnDefinition />

        <ColumnDefinition />

      </Grid.ColumnDefinitions>

      <Grid.RowDefinitions>

        <RowDefinition />

        <RowDefinition />

        <RowDefinition />

        <RowDefinition />

      </Grid.RowDefinitions>

然后在控件中指定所在的行和列:

      <TextBlock Grid.Row="1" Grid.Column="0">Quarter 1</TextBlock>

相应的C#代码:

            Grid.SetRow(txt2, 1);

            Grid.SetColumn(txt2, 0);

注意:因为Grid派生于Panel基类,所以在Grid中添加控件的方法同于其它Layout类:

            myGrid.Children.Add(txt2);

14GridComplex

这是用Grid设计的一个日历。建议参考WPF样式技术和Brush绘图技术。

值得注意的是最后一列<ColumnDefinition Width="*"/>表示将会占用剩余的所有宽度;

<RowDefinition Height="Auto"/>表示这行将自动调整高度的。

Grid中元素可以跨行或跨列,使用相应的Grid.ColumnSpanGrid.RowSpan属性:

      <Rectangle Grid.ColumnSpan="7" Name="rect"/>

相应的C#代码:

            Grid.SetColumnSpan(rect, 7);

15ColumndefinitionsGrid

这个例子演示了为Grid动态添加、删除RowColumn

添加一行:

           grid1.RowDefinitions.Add(rowDef1);

删除一行:

           grid1.RowDefinitions.RemoveAt(0);   //删除index为0的Row

批量删除的语法:

           grid1.RowDefinitions.RemoveRange(0, 5);      //从index为0开始,删除5个Row

删除所有行:

           grid1.RowDefinitions.Clear();

获取Row的数量:

 grid1.RowDefinitions.Count

判断Grid是否有这一行:

 grid1.RowDefinitions.Contains(rowDef1)

在指定index插入一行:

           rowDef1 = new RowDefinition();

 grid1.RowDefinitions.Insert(1, rowDef1);

判读RowColumn的只读属性:

 grid1.ColumnDefinitions.IsReadOnly

注:翻翻IsReadOnly属性,这是一个只读属性(只有get方法),找不到可以设置值的其它地方;另一方面,经过测试,发现这个值总是false,说明ColumnDefinitions永远为只读的。

16GridConvertValue

这个例子演示了控件的Margin属性。

Margin="10,20,30,40"表示控件距离左、上、右、下的长度,这是一个Thickness类型,如果只用一个数值设置,则左上右下都使用这个相同的长度。为此,提供了ThicknessConverter这个转换器,它的ConvertFromStringConvertToString两个实例方法,进行双向转换。如下代码:

            ListBoxItem li = ((sender as ListBox).SelectedItem as ListBoxItem);

            ThicknessConverter myThicknessConverter = new ThicknessConverter();

            Thickness th1 = (Thickness)myThicknessConverter.ConvertFromString(li.Content.ToString());

            text1.Margin = th1;

            String st1 = (String)myThicknessConverter.ConvertToString(text1.Margin);

            gridVal.Text = "The Margin property is set to " + st1 + ".";

注意到,我们使用ConvertFromString,将ListBox选中的值如”20”装换为(20, 20, 20, 20)这个Thickness类型实例;反之我们使用ConvertToString,将(20, 20, 20, 20)显示出来。

17GridGetSetMethods

这个例子演示了如何在后台动态修改Grid的行和列,包括之前我们介绍的SetRowSetColumm以及SetRowSpanSetColummSpan。这里我们着重介绍属性的get方法。

Grid.GetColumn(rect1).ToString()     获取了rect1所在的列号

Grid.GetColumnSapn(rect1).ToString() 获取了rect1所跨越的列数

18GridIssharedsizescopeProp

这个示例演示了Grid.IsSharedSizeScope 属性的使用。

在多个Grid外部的控件,如DockPanel,设置这个属性,从而指示这些Grid共享大小信息:

<DockPanel Name="dp1" Grid.IsSharedSizeScope="False"

     <Grid ShowGridLines="True" Margin="0,0,10,0">

     <Grid ShowGridLines="True" Margin="0,0,10,0">

</DockPanel>

当然,在Grid的行与列的定义中,要相应地设置Group

                    <Grid.ColumnDefinitions>

                        <ColumnDefinition SharedSizeGroup="FirstColumn"/>

                        <ColumnDefinition SharedSizeGroup="SecondColumn"/>

                    </Grid.ColumnDefinitions>

这就确保多个Grid中具有相同SharedSizeGroup值的列具有相同配置。同理于RowDefinitions

我们可以在后台动态修改这个属性:

            Grid.SetIsSharedSizeScope(dp1, true);

由于这是一个Attached属性,所以可以直接使用静态方法Grid.GetIsSharedSizeScope访问到:

            txt1.Text = Grid.GetIsSharedSizeScope(dp1).ToString();

1:共享大小的行和列不遵从 Star 大小调整。在这种情况下,Star 大小调整被视为 Auto

2:如果在某个资源模板内 IsSharedSizeScope 设置为 true,同时在该模板外定义了 SharedSizeGroup,则网格大小共享不起作用。

19GridlengthConverterGrid

这个例子演示了GridLengthConverter转换器的使用

            ListBoxItem li = ((sender as ListBox).SelectedItem as ListBoxItem);

            GridLengthConverter myGridLengthConverter = new GridLengthConverter();

            GridLength gl1 = (GridLength)myGridLengthConverter.ConvertFromString(li.Content.ToString());

            col1.Width = gl1;

可以看到,GridLengthConverter的使用方法和前面介绍过的ThicknessConverter相同:

定义了一个名为 changeCol 的自定义方法,该方法将 ListBoxItem 传递给 GridLengthConverter,它将 ListBoxItem Content转换为 GridLength 的实例。转换后的值然后作为 ColumnDefinition 元素的 Width 属性值进行回传。

注意,GridLengthConverter转换器对应的是GridLength类型。

此外,这个例子还介绍了RowDefinitionMaxHeight属性,表示 RowDefinition 的最大高度。具体示例参见下面的示例。

20GridRunDialog

这个例子以XAML和后台C#编码两种方式建立同样的标准的用户界面:对话框。

注:在前台Window标签中指定Name属性,可以在后台直接使用。

21GridStarValues

这个例子演示了GridUnitType枚举——描述 GridLength 对象具有的值的种类:

Auto

大小由内容对象的大小属性决定。默认值

Pixel

该值表示为像素

Star

该值表示为可用空间的加权比例

先看ResetSample按钮,这是一个自动大小的高度:

            rowDef1.Height = new GridLength(1, GridUnitType.Auto);

而后的其他按钮——对应一个星号:

            rowDef1.Height = new GridLength(1, GridUnitType.Star);

在可扩展应用程序标记语言 (XAML) 中,星号值表示为 * 2*。在第一种情况下,行或列将得到一倍的可用空间;在第二种情况下,行或列将得到两倍的可用空间,依此类推。

——分别对应于C#中的:

new GridLength(1, GridUnitType.Star);     // *

new GridLength(2, GridUnitType.Star);     // 2*

22LayoutDataComponent

这是一个很典型的数据绑定的例子。Pane1绑定了页面中的“数据岛”,ListBox中的每一个Item都对应一个Person数据。于是在点击按钮导航到Pane2的时候,

            pane2.DataContext = ListBox1.SelectedItem;

将选中的Person数据绑定到了Pane2DataContext属性。于是Pane2将选中Person的详细数据清单显示在ListBox中。





23LayoutInformation

在调试这个示例的时候,一个问题困扰我很久,就是

LayoutInformation.GetLayoutSlot(txt1);

始终不能编译通过,最后发现LayoutInformation在这里被认为是一个命名空间,因此必须要使用全称:

System.Windows.Controls.Primitives.LayoutInformation.GetLayoutSlot(txt1);

才可以——这是随着.NET版本的无限升级,导致的很多方法与原先的命名空间具有同样的名称,而不巧,在一个类中,要同时使用这些重名的名称。

       ——微软需要开始开始考虑这个问题了,这不是个好兆头。





点击按钮后,在
Hello World这个TextBlock外围包了一个矩形边框,而且窗体底部显示出这个边框的位置和大小,这是由

LayoutInformation.GetLayoutSlot(txt1)

静态方法返回的一个矩形对象获取到的,其中txt1可以替换为任意的控件。

注:示例中,有关Pen的使用参见绘图一章,这里不再描述。

24LayoutTransform

这个示例演示了6种布局变形效果,使用了LayoutTransform类。

同时还对比了RenderTransform类产生的效果4

效果1:旋转45

        <Button.LayoutTransform>

          <RotateTransform CenterX="25" CenterY="25" Angle="45" />

        </Button.LayoutTransform>


效果
2X轴扭曲45

        <Button.LayoutTransform>

          <SkewTransform CenterX="0" CenterY="0" AngleX="45" AngleY="0"/>

        </Button.LayoutTransform>


效果
3:放大2

        <Button.LayoutTransform>

          <ScaleTransform CenterX="25" CenterY="25" ScaleX="2" ScaleY="2"/>

        </Button.LayoutTransform>


效果
4:向左下移动5个距离

        <Button.RenderTransform>

          <TranslateTransform X="5" Y="5" />

        </Button.RenderTransform>


效果
5:没有效果

        <Button.LayoutTransform>

          <TranslateTransform X="5" Y="5" />

        </Button.LayoutTransform>


效果
6:使用MatrixTransform产生的效果

        <Button.LayoutTransform>

          <MatrixTransform Matrix="1,3,3,3,3,3"/>

        </Button.LayoutTransform>



关于
LayoutTransform的介绍见绘图一章。

25MarginPaddingAlignmentSample

这个例子中有很多研究的地方:

这个例子演示了StackPanel的精确布局技术,三个StackPanel位于Grid中的3个单元格中,分别具有不同的布局方式。

注意到GridColumnDefinitions定义:

      <Grid.ColumnDefinitions>

        <ColumnDefinition Width="Auto"/>

        <ColumnDefinition Width="*"/>

        <ColumnDefinition Width="Auto"/>

      </Grid.ColumnDefinitions>

这样就定义了三列,其中左右两列根据列中元素自动设置宽度,之后就固定了;而中间列的宽度随着窗体的伸缩而自动调整。

26MPALayoutHorizontalAlignment

这个例子演示了如何在StackPanel中使用HorizontalAlignment属性定位其中的元素

    <StackPanelHorizontalAlignment="Center" VerticalAlignment="Top">

         <TextBlockHorizontalAlignment="Center">HorizontalAlignment Sample</TextBlock>

        <Button HorizontalAlignment="Left">Button 1 (Left)</Button>

可以看到,StackPanel中的元素可以重新设置HorizontalAlignment属性,从而覆盖StackPanel中设置的值。

HorizontalAlignment枚举有四个值:Left、Center、Right、Stretch。其中Stretch表示撑满整个区域。

27MPALayoutVerticalAlignment

这个例子演示了在如何在Grid中使用VerticalAlignment属性定位其中的元素,原理与前面的例子是一样的。

VerticalAlignment枚举有四个值:Top、Center、Bottom、Stretch

28MPALayoutSampleIntro

这个例子演示了MarginPadding两个属性的使用,前者已经介绍过,后者也是一个Thickness类型,使用方法同Margin

注意到,在XAML中有两种设置Margin的方法:

        <TextBlock Margin="5,0,5,0">Alignment, </TextBlock>

        <Button Margin="20">Button 1</Button>

第二行Margin="20"等价于Margin="20,20,20,20"

以下3个例子演示了如何自定义一个派生于Panel的布局类

29PlotPanel

本示例定义一个名为PlotPanel的简单的自定义Panel元素,该元素依照两个硬编码的xy坐标定位子元素。在此示例中,xy均被设置为50;因此,所有子元素均放置在xy轴上的该位置处。如下图所示,红色矩形(rect2)在前,因为是后加到PlotPanel中的;蓝色矩形在后(rect1)。它们都位于同一个起点。



如果未定义Background,则Panel元素不会接收鼠标或手写笔事件。如果需要处理鼠标或手写笔事件而不需要对Panel使用背景,请使用Transparent

自定义Panel的子类时,要继承它的默认构造函数:

        public class PlotPanel : Panel
        {
            public PlotPanel() : base() { }

此外一定要重写两个方法MeasureOverrideArrangeOverride

            protected override Size MeasureOverride(Size availableSize)

            protected override Size ArrangeOverride(Size finalSize)

而且这两个方法是有顺序的,先测量(MeasureOverride),再排列(ArrangeOverride)

1MeasureOverride方法:测量子元素在布局中所需的大小,然后确定FrameworkElement派生类的大小。

            protected override Size MeasureOverride(Size availableSize)
            
{
                Size childSize 
= availableSize;
                
foreach (UIElement child in InternalChildren)
                
{
                    child.Measure(childSize);
                }

                
return availableSize;
            }

这里参数availableSizePlotPanel这个派生类的大小——当前示例因为没有设置,就取它的默认值,我的机器上是(944 522)这个尺寸。

注意到这个InternalChildren是派生类内的子元素集合,这里我们遍历该集合,对每个子元素调用Measure方法,从而更新每个子元素的DesiredSize

比如说,我在初始化时,设定了rect1的尺寸:

            rect1.Width = 200;
            rect1.Height = 200;

MeasureOverride方法是在初始化之后,窗体显示时(每次改变窗体属性都会激发该方法):

            mainWindow.Show();

Measure方法调用前,child.DesiredSize属性为(0, 0);调用之后,为(200, 200)。标志着测量完毕。

但是如果在初始化时,设定的rect1尺寸大于PlotPanel的大小(944 522):

            rect1.Width = 1000;

            rect1.Height = 1000;

那么Measure方法调用后,child.DesiredSize属性为(944 522),不会超过PlotPanel的大小。这是Measure方法的实际作用。

最后,当遍历结束,返回PlotPanel元素在布局过程中所需的大小,这是由此元素根据对其子元素大小的计算而确定的。

示例中,因为两个Rectangle对象都小于PlotPanel,所以这里直接返回输入参数availableSize,也就是PlotPanel元素的原始大小。

但是如果过界:

            rect1.Width = 1000;

那么,就需要重新计算这个返回值了,具体细节见下面的示例。

2ArrangeOverride方法用于为派生类定位子元素并确定大小

            protected override Size ArrangeOverride(Size finalSize)

            {

                foreach (UIElement child in InternalChildren)

                {

                    double x = 50;

                    double y = 50;

                    child.Arrange(new Rect(new Point(x, y), child.DesiredSize));

                }

                return finalSize; // Returns the final Arranged size

            }

注意到这个InternalChildren是派生类内的子元素集合,这里我们遍历该集合,对每个子元素调用Arrange方法,重新定位,但是并没有改动它的大小(当然也可以改变)。

参数finalSize,和MeasureOverride方法的参数availableSize是同一个。

返回值为所用的实际大小。

由于这里只是简单的向左向下移动了50距离,不会产生越界,所以直接返回了参数finalSize,表示仍使用PlotPanel元素的原始大小。

30RadialPanel

这个示例演示了一种自定义外观布局,添加新的元素到其中,会产生螺旋式的排列。如下图,分别为5个按钮和6个按钮排列时的情形:


让我们看一下这个派生于Panel的自定义RadialPanel

简单的说,这就是一个几何学绘图,使用到一个公式,这里我们不讨论具体的算法,只分析这两个方法的结构:

1)重写MeasureOverride方法:

仍然是遍历InternalChildren,并在恰当时候进行度量:

       uie.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));

但这次的返回值是经过复杂运算后得到的结果:

            return new Size(_squareSize, _squareSize);

注:Double.PositiveInfinity表示正无穷的常数

2)重写ArrangeOverride方法:

在遍历中,重新设置了每个元素的位置和大小,并进行了旋转:

                uie.RenderTransform = new RotateTransform(_currentAngle);

                uie.Arrange(new Rect(new Point(_currentOriginX, _currentOriginY), new Size(uie.DesiredSize.Width, uie.DesiredSize.Height)));

由于还是按照RadialPanel原先的尺寸显示,所以返回值还是传进来的参数,没有改变。

31CustomPanel

这个例子也是自定义布局,具体的逻辑就不多说了。效果如下图所示:



32AutoGrid

最终的效果如下图:两个Slider,一个控制笑脸的数量,一个控制每行显示几个笑脸。拖动Slider的同时,Grid中的笑脸数量和布局会跟着变动。



这个例子有很多地方需要仔细研究。

首先,LogicalTreeHelper静态类,提供了很多方法来获取一课逻辑树上的元素。这里我们使用了它的FindLogicalNode方法,在一个根上查找指定名称的元素。

            Slider childrenCountSlider = (Slider)LogicalTreeHelper.FindLogicalNode(win, "ChildrenCountSlider");

由于我们事先知道这是一个Slider元素,所以将找到的结果进行强制转换。

——这个方法便于我们从XAML中解析出一个元素的实例。

其次是Slider控件的两个拖动方法,OnChildrenCountChangedOncolumnCountChanged,分别负责增删笑脸和每行的列数,连用了两个while,分别判断增和删两种情况

最后,也是最重要的,自定义了一个AutoIndexingGrid类,派生于Grid,用来显示笑脸。这里重写了MeasureOverrideOnVisualChildrenChanged两个方法,其中后者当Grid元素的可视子级发生更改时调用:

        protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved)
        
{
            
// Remember that children collection has changed 
            _updateChildenIndices = true;
            
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
        }

我们在类级别设置了_updateChildenIndices标记,true表示有新的改动还没用被处理。因为MeasureOverride方法总是在OnVisualChildrenChanged方法之后发生,所以,我们会在随后的MeasureOverride方法中将这个标记改为false

visualAddedvisualRemoved这两个参数分别用来标识添加的可视子级和移除的可视子级。

那么,MeasureOverride方法的逻辑又是什么呢?

1)首先要判断这次调用是要处理OnVisualChildrenChanged之后的新改动,而不是来自窗体的拖曳事件:

            if (_updateChildenIndices || _columnCount != base.ColumnDefinitions.Count)

来自窗体的拖曳事件直接调用基类的MeasureOverride方法:

            return (base.MeasureOverride(constraint));

2)如果列的增加导致了行的增加,对行的增加是使用while循环完成的:

                int newRowCount = ((base.Children.Count - 1/ _columnCount + 1);
                
while (base.RowDefinitions.Count < newRowCount)
                
{
                    
base.RowDefinitions.Add(new RowDefinition());
                }

3)随着列的减少,一些行不再有效,对这些多余Row的删除是批量进行的:

                if (base.RowDefinitions.Count > newRowCount)
                
{
                    
base.RowDefinitions.RemoveRange(newRowCount, base.RowDefinitions.Count - newRowCount);
                }

4for循环依次为AutoIndexingGrid中的子元素设置Grid位置

这里要注意的是,由于AutoIndexingGrid派生于Grid,而Grid是一个具体类,已经实现了MeasureOverride方法,所以在AutoIndexingGrid的复写MeasureOverride方法中,最后还是要调用基类的MeasureOverride方法作为返回值:

            return (base.MeasureOverride(constraint));

这么看来,所谓的复写,只是补充一点自己的小逻辑。

33ScrollViewer

ScrollViewer作为一个控件,可以作为Page/WindowContent,而将一个布局容器作为它的Content

            // Add the StackPanel as the lone Child of the Border

            myScrollViewer.Content = myStackPanel;

           

            // Add the Border as the Content of the Parent Window Object

            mainWindow.Content = myScrollViewer;

可以在XAML中设置它的滚动条:

 <ScrollViewer HorizontalScrollBarVisibility="Auto">

HorizontalScrollBarVisibility枚举有四个值: