这一章介绍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中。
在调试这个示例的时候,一个问题困扰我很久,就是
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>
<Button.LayoutTransform>
<SkewTransform CenterX="0" CenterY="0" AngleX="45" AngleY="0"/>
</Button.LayoutTransform>
<Button.LayoutTransform>
<ScaleTransform CenterX="25" CenterY="25" ScaleX="2" ScaleY="2"/>
</Button.LayoutTransform>
<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>
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)这个尺寸。
注意到这个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
这个例子也是自定义布局,具体的逻辑就不多说了。效果如下图所示:
最终的效果如下图:两个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。
我们在类级别设置了
visualAdded和visualRemoved这两个参数分别用来标识添加的可视子级和移除的可视子级。
那么,MeasureOverride方法的逻辑又是什么呢?
1)首先要判断这次调用是要处理OnVisualChildrenChanged之后的新改动,而不是来自窗体的拖曳事件:
if (_updateChildenIndices || _columnCount != base.ColumnDefinitions.Count)
来自窗体的拖曳事件直接调用基类的MeasureOverride方法:
return (base.MeasureOverride(constraint));
2)如果列的增加导致了行的增加,对行的增加是使用while循环完成的:
3)随着列的减少,一些行不再有效,对这些多余Row的删除是批量进行的:
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编辑器,涉及的技术很多,我会在稍后的章节详细介绍其实现。这里从略。