WPF SDK研究 之 Layout布局
这一章介绍Layout布局。
本章共计51个示例,全都在VS2008下.NET3.5测试通过,点击这里下载:Layout.rar
一则小技巧:建立名为
在开始本章之前,有必要看一下继承关系:
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的类。
1.BorderChangeProgrammatic
Border用于在另一个元素的周围绘制边框、背景。
Border只能具有一个子级。若要显示多个子元素,需要将一个附加的Panel元素放置在父Border中。然后可以将多个子元素放置在该Panel元素中。所以我们常常看到,Border介于Page(或Window)和布局面板(如Canvas)之间——所以我们不要混淆,Border不是Layout,而是Control。
2.CanvasAttachedProperties
这个例子分别用XAML和C#后台代码演示了Canvas的四个Attached定位属性:
Bottom, Left, Right, Top
如:
<Canvas…>
<Button Canvas.Top="50">Canvas.Top="50"</Button>
在后台C#中表现为:
Canvas.SetTop(myButton1, 50);
注:所谓Attached属性,就是Canvas.Top形式。
3.CanvasCode
这个例子是上个例子的翻版,不再重述。
4.CanvasOvwSample
这个例子是上个例子的翻版,不再重述。
注意到:蓝色Canvas在XAML中的最后,所以盖住了其他两个Canvas,详细解释见下。
5.CanvasPositioningProperties
这个示例演示了在Canvas中使用LengthConverter的方法,同前。
6.CanvasZOrder
这个示例演示了Z-Order属性的使用。
正如我们之前看到的,Grid中的多个元素按照在XAML中的先后顺序,依次覆盖。如果想自定义在Z轴上的位置,可以使用Canvas.ZIndex属性来设置,值大的在上面。
7.DockPanelCode
这个示例演示了如何用C#程序创建DockPanel并对Window窗体中的5个Rectangle进行布局。这里,每个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;
注意:要把元素添加到myDockPanel的Children集合中。
8.DockPanelDockPropertyCode
这个例子演示了DockPanel的LastChildFill属性。这个属性默认是True的,那么DockPanel中的最后一个元素,将会自动填充剩余区域,而不管这个元素是否设置过Dock枚举位置。只有设置该属性为False,才能使该元素的Dock枚举位置生效。
在这个例子的DockPanel中,有两个元素rect1和rect2,无论如何设置Dock位置,都不会生效,只能根据rect1的位置而自动调整——因为默认LastChildFill为True;只有将其改为False,才可以看到效果,当两个元素各居左右时,中间会空出一段空白。
9.DockPanelOvw
这个例子是示例-DockPanelCode的延续,从XAML语法介绍如何使用DockPanel进行布局。
10.DockPanelOvw2
这个例子演示了如何在DockPanel中垂直排列3个Button。
11.DockPanelSetDock
这个例子演示了在C#中如何设定元素的DockPanel位置,以及获取这个值。
也就是DockPanel的两个静态方法DockPanel.SetDock和DockPanel.GetDock。
DockPanel.SetDock(txt1, System.Windows.Controls.Dock.Top);
txt1.Text = "The Dock Property is set to " + DockPanel.GetDock(txt1);
12.FontSizeConverter
这个示例演示了如何改变一段文本的字体和大小。
先看改变文字大小的方法:
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" //相对路径,并且是一个字体数组
13.Grid
这个例子分别使用编程和XAML两种方式建立Grid布局。
在Grid中开始部分,使用Grid.ColumnDefinitions和Grid.RowDefinitions,事先规定Grid中行和列的数量:如下是一个3列4行的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);
14.GridComplex
这是用Grid设计的一个日历。建议参考WPF样式技术和Brush绘图技术。
值得注意的是最后一列<ColumnDefinition Width="*"/>表示将会占用剩余的所有宽度;
而<RowDefinition Height="Auto"/>表示这行将自动调整高度的。
Grid中元素可以跨行或跨列,使用相应的Grid.ColumnSpan和Grid.RowSpan属性:
<Rectangle Grid.ColumnSpan="7" Name="rect"/>
相应的C#代码:
Grid.SetColumnSpan(rect, 7);
15.ColumndefinitionsGrid
这个例子演示了为Grid动态添加、删除Row和Column
添加一行:
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);
判读Row或Column的只读属性:
grid1.ColumnDefinitions.IsReadOnly
注:翻翻IsReadOnly属性,这是一个只读属性(只有get方法),找不到可以设置值的其它地方;另一方面,经过测试,发现这个值总是false,说明ColumnDefinitions永远为只读的。
16.GridConvertValue
这个例子演示了控件的Margin属性。
Margin="10,20,30,40"表示控件距离左、上、右、下的长度,这是一个Thickness类型,如果只用一个数值设置,则左上右下都使用这个相同的长度。为此,提供了ThicknessConverter这个转换器,它的ConvertFromString和ConvertToString两个实例方法,进行双向转换。如下代码:
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)显示出来。
17.GridGetSetMethods
这个例子演示了如何在后台动态修改Grid的行和列,包括之前我们介绍的SetRow、SetColumm以及SetRowSpan、SetColummSpan。这里我们着重介绍属性的get方法。
Grid.GetColumn(rect1).ToString() 获取了rect1所在的列号
Grid.GetColumnSapn(rect1).ToString() 获取了rect1所跨越的列数
18.GridIssharedsizescopeProp
这个示例演示了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,则网格大小共享不起作用。
19.GridlengthConverterGrid
这个例子演示了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类型。
此外,这个例子还介绍了RowDefinition的MaxHeight属性,表示 RowDefinition 的最大高度。具体示例参见下面的示例。
20.GridRunDialog
这个例子以XAML和后台C#编码两种方式建立同样的标准的用户界面:对话框。
注:在前台Window标签中指定Name属性,可以在后台直接使用。
21.GridStarValues
这个例子演示了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*
22.LayoutDataComponent
这是一个很典型的数据绑定的例子。Pane1绑定了页面中的“数据岛”,ListBox中的每一个Item都对应一个Person数据。于是在点击按钮导航到Pane2的时候,
pane2.DataContext = ListBox1.SelectedItem;
将选中的Person数据绑定到了Pane2的DataContext属性。于是Pane2将选中Person的详细数据清单显示在ListBox中。
23.LayoutInformation
在调试这个示例的时候,一个问题困扰我很久,就是
LayoutInformation.GetLayoutSlot(txt1);
始终不能编译通过,最后发现LayoutInformation在这里被认为是一个命名空间,因此必须要使用全称:
System.Windows.Controls.Primitives.LayoutInformation.GetLayoutSlot(txt1);
才可以——这是随着.NET版本的无限升级,导致的很多方法与原先的命名空间具有同样的名称,而不巧,在一个类中,要同时使用这些重名的名称。
——微软需要开始开始考虑这个问题了,这不是个好兆头。
点击按钮后,在Hello World这个TextBlock外围包了一个矩形边框,而且窗体底部显示出这个边框的位置和大小,这是由
LayoutInformation.GetLayoutSlot(txt1)
静态方法返回的一个矩形对象获取到的,其中txt1可以替换为任意的控件。
注:示例中,有关Pen的使用参见绘图一章,这里不再描述。
24.LayoutTransform
这个示例演示了6种布局变形效果,使用了LayoutTransform类。
同时还对比了RenderTransform类产生的效果4。
效果1:旋转45度
<Button.LayoutTransform>
<RotateTransform CenterX="25" CenterY="25" Angle="45" />
</Button.LayoutTransform>
效果2:X轴扭曲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的介绍见绘图一章。
25.MarginPaddingAlignmentSample
这个例子中有很多研究的地方:
这个例子演示了StackPanel的精确布局技术,三个StackPanel位于Grid中的3个单元格中,分别具有不同的布局方式。
注意到Grid的ColumnDefinitions定义:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
这样就定义了三列,其中左右两列根据列中元素自动设置宽度,之后就固定了;而中间列的宽度随着窗体的伸缩而自动调整。
26.MPALayoutHorizontalAlignment
这个例子演示了如何在StackPanel中使用HorizontalAlignment属性定位其中的元素
<StackPanel…HorizontalAlignment="Center" VerticalAlignment="Top">
<TextBlock…HorizontalAlignment="Center">HorizontalAlignment Sample</TextBlock>
<Button HorizontalAlignment="Left">Button 1 (Left)</Button>
可以看到,StackPanel中的元素可以重新设置HorizontalAlignment属性,从而覆盖StackPanel中设置的值。
HorizontalAlignment枚举有四个值:Left、Center、Right、Stretch。其中Stretch表示撑满整个区域。
27.MPALayoutVerticalAlignment
这个例子演示了在如何在Grid中使用VerticalAlignment属性定位其中的元素,原理与前面的例子是一样的。
VerticalAlignment枚举有四个值:Top、Center、Bottom、Stretch。
28.MPALayoutSampleIntro
这个例子演示了Margin和Padding两个属性的使用,前者已经介绍过,后者也是一个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的布局类
29.PlotPanel
本示例定义一个名为PlotPanel的简单的自定义Panel元素,该元素依照两个硬编码的x和y坐标定位子元素。在此示例中,x和y均被设置为50;因此,所有子元素均放置在x和y轴上的该位置处。如下图所示,红色矩形(rect2)在前,因为是后加到PlotPanel中的;蓝色矩形在后(rect1)。它们都位于同一个起点。
如果未定义Background,则Panel元素不会接收鼠标或手写笔事件。如果需要处理鼠标或手写笔事件而不需要对Panel使用背景,请使用Transparent
自定义Panel的子类时,要继承它的默认构造函数:
public class PlotPanel : Panel
{
public PlotPanel() : base() { }
此外一定要重写两个方法MeasureOverride和ArrangeOverride:
protected override Size MeasureOverride(Size availableSize)
protected override Size ArrangeOverride(Size finalSize)
而且这两个方法是有顺序的,先测量(MeasureOverride),再排列(ArrangeOverride)。
1)MeasureOverride方法:测量子元素在布局中所需的大小,然后确定FrameworkElement派生类的大小。
这里参数availableSize是PlotPanel这个派生类的大小——当前示例因为没有设置,就取它的默认值,我的机器上是(944, 522)这个尺寸。
{
Size childSize = availableSize;
foreach (UIElement child in InternalChildren)
{
child.Measure(childSize);
}
return availableSize;
}
注意到这个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;
那么,就需要重新计算这个返回值了,具体细节见下面的示例。
2)ArrangeOverride方法用于为派生类定位子元素并确定大小
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元素的原始大小。
30.RadialPanel
这个示例演示了一种自定义外观布局,添加新的元素到其中,会产生螺旋式的排列。如下图,分别为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原先的尺寸显示,所以返回值还是传进来的参数,没有改变。
31.CustomPanel
这个例子也是自定义布局,具体的逻辑就不多说了。效果如下图所示:
32.AutoGrid
最终的效果如下图:两个Slider,一个控制笑脸的数量,一个控制每行显示几个笑脸。拖动Slider的同时,Grid中的笑脸数量和布局会跟着变动。
这个例子有很多地方需要仔细研究。
首先,LogicalTreeHelper静态类,提供了很多方法来获取一课逻辑树上的元素。这里我们使用了它的FindLogicalNode方法,在一个根上查找指定名称的元素。
Slider childrenCountSlider = (Slider)LogicalTreeHelper.FindLogicalNode(win, "ChildrenCountSlider");
由于我们事先知道这是一个Slider元素,所以将找到的结果进行强制转换。
——这个方法便于我们从XAML中解析出一个元素的实例。
其次是Slider控件的两个拖动方法,OnChildrenCountChanged和OncolumnCountChanged,分别负责增删笑脸和每行的列数,连用了两个while,分别判断增和删两种情况
最后,也是最重要的,自定义了一个AutoIndexingGrid类,派生于Grid,用来显示笑脸。这里重写了MeasureOverride和OnVisualChildrenChanged两个方法,其中后者当Grid元素的可视子级发生更改时调用:
_updateChildenIndices标记,true表示有新的改动还没用被处理。因为MeasureOverride方法总是在OnVisualChildrenChanged方法之后发生,所以,我们会在随后的MeasureOverride方法中将这个标记改为false。
{
// Remember that children collection has changed
_updateChildenIndices = true;
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
}
我们在类级别设置了
visualAdded和visualRemoved这两个参数分别用来标识添加的可视子级和移除的可视子级。
那么,MeasureOverride方法的逻辑又是什么呢?
1)首先要判断这次调用是要处理OnVisualChildrenChanged之后的新改动,而不是来自窗体的拖曳事件:
if (_updateChildenIndices || _columnCount != base.ColumnDefinitions.Count)
来自窗体的拖曳事件直接调用基类的MeasureOverride方法:
return (base.MeasureOverride(constraint));
2)如果列的增加导致了行的增加,对行的增加是使用while循环完成的:
while (base.RowDefinitions.Count < newRowCount)
{
base.RowDefinitions.Add(new RowDefinition());
}
3)随着列的减少,一些行不再有效,对这些多余Row的删除是批量进行的:
{
base.RowDefinitions.RemoveRange(newRowCount, base.RowDefinitions.Count - newRowCount);
}
4)for循环依次为AutoIndexingGrid中的子元素设置Grid位置
这里要注意的是,由于AutoIndexingGrid派生于Grid,而Grid是一个具体类,已经实现了MeasureOverride方法,所以在AutoIndexingGrid的复写MeasureOverride方法中,最后还是要调用基类的MeasureOverride方法作为返回值:
return (base.MeasureOverride(constraint));
这么看来,所谓的复写,只是补充一点自己的小逻辑。
33.ScrollViewer
ScrollViewer作为一个控件,可以作为Page/Window的Content,而将一个布局容器作为它的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枚举有四个值:Visible、Hidden、Disabled和Auto,根据字面意思知道其各自公用。
当然,ScrollViewer也可以位于控件之间,见下面的示例。
34.ScrollViewerMethods
这个示例演示了ScrollViewer的两套属性:
LineUp与LineDown,当存在上下滚动时,每执行一次,滚动条就向上/下移动一行位置
LineRight与LineLeft,当存在左右滚动时,每执行一次,滚动条就向左/右移动一行位置
PageUp与PageDown,当存在上下滚动时,每执行一次,滚动条就向上/下移动一页位置
PageRight与PageLeft,当存在左右滚动时,每执行一次,滚动条就向左/右移动一页位置
究竟是什么导致了滚动?看下面代码,ScrollViewer的父一级为Border,子一级为TextBlock,那么,当TextBlock的大小超过Border时,就会产生滚动效果:
<Border BorderBrush="Black"… Height="220" Width="520">
<ScrollViewer VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Auto">
<TextBlock TextWrapping="Wrap" Width="800" Height="1000" Name="txt1"/>
</ScrollViewer>
</Border>
35.ScrollViewerScrollChanged
这个例子演示了ScrollViewer的ComputedVerticalScrollBarVisibility属性,以及ScrollChanged事件。
先说这个ScrollChanged事件,当检测到对滚动位置、范围或视区大小进行了更改时发生。第一次加载页面时也会激发该事件。
再说ComputedVerticalScrollBarVisibility属性,表示垂直 ScrollBar 是否可见,在调整视区大小的时候,随着滚动条的有无而有Visible和Collapsed两种值——这是在VerticalScrollBarVisibility为Auto的情况下。对于VerticalScrollBarVisibility枚举的其它三个值:
Visible ComputedVerticalScrollBarVisibility恒为Visible
Hidden或Disabled ComputedVerticalScrollBarVisibility恒为Collapsed
36.ScrollchangedeventargsLayout
ScrollViewer控件有一个CanContentScroll属性,默认为False,即不显示滚动条。
<ScrollViewer Name="sv1" CanContentScroll="False" ScrollChanged="sChanged">
点击按钮后,将这个属性设置为True
仍然是观察ScrollChanged事件被触发后,ScrollChangedEventArgs实例e的12个属性的相应变化,可以分为3类:
1)ScrollViewer范围的宽度/高度和其更改值,如ExtentHeight
2)ScrollViewer的水平/垂直偏移量及其更改值,如HorizontalChange
3)ScrollViewer的视区宽度/高度和其更改值,如ViewportWidth
注:FlowDocument技术见Flow一章。
37.ScrollViewerStyle
这个示例很有趣,颠覆了对传统滚动条的视觉观。如图所示,垂直滚动条位于文本的左边。
技术实现在Style中,代码很复杂,一共有5段样式,主要是重写了ScrollViewer中的ScrollBar、Thumb和RepeatButton。
注:根据目前的Style代码,把X、Y轴的坐标以及水平垂直的方向对调,可以将水平滚动条置于文本上方。
注:这个示例的样式细节,一稿实在没时间写了,计划在二稿补齐。
38.IScrollInfoMethods
其实不直接操纵ScrollViewer,也可以实现滚动效果。为此提供了IScrollInfo接口,该接口具有很多类似于ScrollViewer的方法,实现了该接口的类都有滚动效果,比如说本示例中的StackPanel。
我们可以把StackPanel实例进行转换,以使用这些接口方法:
((IScrollInfo)sp1).PageUp();
这样其实是把PageUp这个动作交给了sp1的上一级ScrollViewer来处理——这样做的前提是StackPanel必须在ScrollViewer中,我曾经尝试着去掉ScrollViewer控件,但是发现在运行期,所有的IScrollInfo接口方法都失效了。
所以说,我大胆猜测,这个包装了StackPanel的IScrollInfo接口是典型的适配器模式。如下UML图:
39.StackPanelIntro
这个例子介绍了如何使用StackPanel进行布局。
StackPanel以流的方式布局,为此要指定Orientation属性,这是一个Orientation枚举,有Orientation.Horizontal和Orientation.Vertical两种,而后者Vertical是默认值。
同时还可以指定HorizontalAlignment和VerticalAlignment两个属性,使用方法同前面介绍的Grid。
40.StackPanelOvw4
这个例子演示了StackPanel与DockPanel的区别。
如果内部设定超过容器大小,怎么办?
StackPanel会裁剪越界部分
DockPanel和Grid会智能判断,从而决定换行或者延展扩充整个区域。
41.ThicknessConverter
这个例子演示了两个转换器,BrushConverter和ThicknessConverter,前者已经介绍过,后者的使用方法,也很类似:
BrushConverter myBrushConverter = new BrushConverter();
border1.BorderBrush = (Brush)myBrushConverter.ConvertFromString((string)li2.Content);
仅仅是换了一个实例类型而已。
42.UIElementCollection
这个示例演示了如何在布局面板(如StackPanel)的控件树上添加、移除、清除、插入以及寻找指定index的元素。
StackPanelsp1 = new StackPanel();
添加元素:
sp1.Children.Add(btn);
移除指定index的元素:
sp1.Children.RemoveAt(0);
清除所有元素:
sp1.Children.Clear();
在指定index处添加元素,原先index及以后元素依次后退一位:
sp1.Children.Insert(1, btn2);
获取指定元素的index:
sp1.Children.IndexOf(btn);
获取指定index的元素或StackPanel中元素的数量:
sp1.Children[0]
sp1.Children.Count
43.UIElementCollectionIndexOf
这个例子演示了DockPanel如何获取指定元素的index:
MainDisplayPanel.Children.Add(newText); // MainDisplayPanel为DockPanel实例
MainDisplayPanel.Children.IndexOf(newText);
44.ViewBoxCode
这个例子分别用XAML和C#代码创建了一个ViewBox,其中包含了一个Grid
在Grid外面包一层ViewBox,可以使Grid内的控件填充整个ViewBox,并随着ViewBox的大小变化而同步变化,这是因为ViewBox默认属性Stretch=“Uniform”。
45.ViewboxStretchLayoutSamp
这个例子演示了ViewBox的两个重要属性:Stretch和StretchDirection
Stretch属性是一个枚举,描述如何调整内容的大小以填充为其分配的空间,有四个值:
None 内容保持其原始大小。
Fill 调整内容的大小以填充目标尺寸。不保留纵横比。
Uniform 保留内容原有纵横比的同时调整内容的大小,以适合目标尺寸。
UniformToFill 在保留内容原有纵横比的同时调整内容的大小,以填充目标尺寸。如果目标矩形的纵横比不同于源矩形的纵横比,则对源内容进行剪裁以适合目标尺寸。
StretchDirection属性也是一个枚举,描述缩放如何应用于内容,以及如何将缩放限制到指定的轴类型,有3个值:
UpOnly 仅当内容小于父项时,它才会放大。如果内容大于父项,不会执行任何缩小操作
DownOnly 仅当内容大于父项时,它才会缩小。如果内容小于父项,不会执行任何放大操作
Both 内容根据 Stretch 模式进行拉伸以适合父项的大小。
46.VisibilityLayoutSamp
This sample shows how to change the Visibility property of a UIElement.
这个例子演示了Visibility枚举的三个值:Visible、Hidden、Collapsed。其中Hidden表示不显示元素,但为元素保留布局空间;而Collapsed则表示不显示元素,且不为其保留布局空间。
这个例子介绍的Visibility枚举属于Flow一章的 示例-13 的一部分。
47.HeightMinHeightMaxHeight
这个示例演示了MinHeight、MaxHeight、MaxHeight这三个属性,以及ClipToBounds属性对高度设置的影响。
当ClipToBounds为false时,父元素的Height/Width设置不会剪裁内容。
但如果ClipToBounds为true,则会裁剪内容。裁减将始终基于MaxHeight剪裁内容
在同一个实例上,MinHeight值优先于MaxHeight值,MaxHeight又优先于Height 值。这意味着:
1)如果Height值大于MaxHeight,实际就会表现为:被裁减为MaxHeight高度(Height值小于MaxHeight是正常情况)。
2)如果MaxHeight值小于MinHeight,或是Height值小于MinHeight,实际就会表现为:永远为MinHeight高度,而无论Height值为多少。
3)正常情况下,即MinHeight<Height<MaxHeight,实际表现就是Height高度。
以上都是在理想情况下的表现,设其结果为高度X。
对于子元素,就还要考虑父元素的Height属性(或MaxHeight属性)以及ClipToBounds属性:
1)当ClipToBounds为false时,不会剪裁内容。
2)当ClipToBounds为true时,如果上面X值大于父元素的Height属性,就会发生裁减,大于的那部分会被裁减掉。
所有派生于UIElement基类的元素都有ClipToBounds这个属性——获取或设置一个值,用于表示是否剪裁此元素的内容(或来自此元素的子元素的内容)以适合包含元素的大小。
在示例中,Rectangle的父元素是Canvas,
<Canvas Height="200" MinWidth="200" Name="myCanvas" …>
<Rectangle Name="rect1" Fill="#4682b4" Width="100" Height="100"/>
</Canvas>
可以通过改变父元素Canvas的ClipToBounds属性:
myCanvas.ClipToBounds = false;
来操纵Rectangle的布局行为。我们看到,父元素Canvas的宽度由MinWidth决定
注意到,为了使窗体在改动后立刻更新,可以使用下列语句:
rect1.Width = sz1;
rect1.UpdateLayout();
注:MSDN上的解释实在是够费解,我用这个例子测试了很久,才搞清楚这四个属性之间的关系。
48.WidthMinWidthMaxWidth
这个示例是上一个示例的延续,从Width角度着手。
在同一个实例上,MinWidth值优先于 MaxWidth值,MaxWidth又优先于Width值。
49.WrapPanelIntro
这个例子介绍的是WrapPanel布局。
WrapPanel从左至右按顺序位置定位子元素,在包含框的边缘处将内容断开至下一行。后续排序按照从上至下或从右至左的顺序进行,具体取决于Orientation属性的值
值得关注的是ItemHeight和ItemWidth属性,指定WrapPanel中所含的所有项目的高度/宽度。如果未设置此属性(或者如果它在XAML中设置为Auto,或在代码中设置为Double.NaN),则布局分区的大小将等于子元素的所需大小。
WrapPanel 的子元素可以具有自己的显式设置的高度属性Height。ItemHeight 指定了 WrapPanel 为子元素保留的布局分区的大小,因而,ItemHeight 优先于元素自己的高度。
50.FlowDocumentSamp
有趣的是,这个示例是Layout一章的总结,换句话说,可以把它当作一本很详尽的电子书来读。
当然,这个示例也有值得参考的技术,让我们把目光聚焦在TOC这个xaml上,这是一个导航器,里面那个可伸缩的Menu很酷,介绍如下:
在XAML中,使用了ListBox的嵌套技术,并使用到了SelectionChanged事件,对应expandTOC方法,截取其中一段:
if (node1.IsSelected)
{
if (lb1.Visibility == Visibility.Collapsed)
lb1.Visibility = Visibility.Visible;
else if (lb1.Visibility == Visibility.Visible)
lb1.Visibility = Visibility.Collapsed;
node1.IsSelected = false;
}
node1就是[+/-] Documentatio这个ListBoxItem,如果被选中,就会根据当前伸缩状态改为相反的状态。
再有就是Default.xaml中的XamlPad按钮,这会打开内置在SDK中的XAMLPad编辑器:
System.Diagnostics.Process.Start(@"C:"Program Files"Microsoft SDKs"Windows"v6.0A"bin"XAMLPad.exe");
这里的路径是我的机器上的.NET3.5 SDK的默认安装路径。
51.SampleViewerLite
这个示例是一个可视化XAML编辑器,涉及的技术很多,我会在稍后的章节详细介绍其实现。这里从略。
@default dd = new @default();
但是在XAML中,
x:Class="FlowDocumentSamp.@default"
就是一个错误,编译器解析不出这个@default,所以不要建立default.xaml,可以取代以代首字母大写的形式:Default.xaml,就没有问题了。
因此,作出如下总结:
如果default.xaml仅仅是一个XAML文件,而没有设置x:Class属性,那么可以使用小写的名称。
如果在XAML中设置了x:Class,那么该文件的命名要格外注意,尽量不要使用关键字作为名称。如果非要这么做,那么要同时修改后台代码的类名以及相应隐藏文件的类名称为一个规范名称,同时修改x:Class属性,指定这个新的类名称。