翻译,原文来自CodeProject,有节选。作者Sacha Barber写文章很用心,希望我能很用心的看。我更关注的是实际工程中的应用。
这篇文章主要讲解WPF中的Layout。
1)控件的属性参数Margin
通过使用Margin,我们可以指定当前的控件与四周的空间大小。WPF提供一个函数ValueConverter,可以接受一个字符串如5,5,5,5,指示当前控件与四周间隔5个像素,分别为left,top,Right,Bottom;
2)Canvas
比较简单的布局控件;是个X/Y位置控制的容器,但子容器需要指定以下四个参数:
Canvas.Left; Canves.Right; Canvas.Top; Canvas.Bottom
这看起来有些奇怪,因为我们已经有了一个Canvas.Left(文中没有更多的解释,只是说这不是我们在.Net 2.0下经常使用到的属性)。
Canvas属性就这些。另外如果碰到两个Canvas有重叠,如何确认一个在另一个上面?这时候就要考虑属性Canvas.ZIndex,用于确认哪个控件在上面,数值大的在上面;如果没有指定数值,则ZIndex的数值是按我们添加控件的顺序来改变的。
下图显示一个Canvas上包含两个子控件:
对应的XAML代码为:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="WPF_Tour_Beginners_Layout.CanvasDEMO" x:Name="Window" Title="CanvasDEMO" WindowStartupLocation="CenterScreen" Width="640" Height="480"> <Canvas Margin="0,0,0,0" Background="White"> <Rectangle Fill="Blue" Stroke="Blue" Width="145" Height="126" Canvas.Left="124" Canvas.Top="122"/> <Ellipse Fill="Green" Stroke="Green" Width="121" Height="100" Panel.ZIndex="1" Canvas.Left="195" Canvas.Top="191"/> </Canvas> </Window>
3)StackPanel
比较简单,只是stack它包含的控件,以竖直或水平方向,属性名为Orientation。
下图显示一个stackPanel包含两个子控件,一个在上,一个在下:
对应的XAML代码为:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="WPF_Tour_Beginners_Layout.StackPanelDEMO" x:Name="Window" Title="StackPanelDEMO" WindowStartupLocation="CenterScreen" Width="640" Height="480"> <StackPanel Margin="0,0,0,0" Background="White" Orientation="Vertical"> <Button Content="Im Top of Stack"/> <Button Content="Im Bottom Of Stack"/> </StackPanel> </Window>
4)WrapPanel
解释为;It simply wraps its contents.
下图显示一个WrapPanel包含10个子控件:
对应的XAML代码:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="WPF_Tour_Beginners_Layout.WrapPanelDEMO" x:Name="Window" Title="WrapPanelDEMO" WindowStartupLocation="CenterScreen" Width="640" Height="480"> <WrapPanel Margin="0,0,0,0" Background="White"> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> <Rectangle Margin="10,10,10,10" Fill ="Blue" Width="60" Height="60"/> </WrapPanel> </Window>
代码中·10个Rectangle一模一样,的确,WrapPanel什么都没做,只是提供一个容器的功能。
5)DockPanel
DockPanel用途比较广,可以作为窗体的基本容器,实现如:menu在上,左右内容显示区域,加底下一个状态显示栏。比较重要的属性是DockPanel.Dock及LastChildFill,后者会改变原来的布局(填满剩下的空间),如果设为true的话。
下图显示一个DockPanel包含两个子控件,一个在上面,另外一个填充了剩余的空间:
对应的XAML代码为:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="WPF_Tour_Beginners_Layout.DockPanelDEMO" x:Name="Window" Title="DockPanelDEMO" WindowStartupLocation="CenterScreen" Width="640" Height="480"> <DockPanel Width="Auto" Height="Auto" LastChildFill="True"> <Rectangle Fill="CornflowerBlue" Stroke="CornflowerBlue" Height="20" DockPanel.Dock="Top"/> <Rectangle Fill="Orange" Stroke="Orange" /> </DockPanel> </Window>
6)Grid
比较复杂,类似于一个HTML中的table控件,可以指定row,column数量及跨行跨列;同时还有一个比较奇怪的语法标识"*",如
<Grid.ColumnDefinitions> <ColumnDefinition Width="40"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="2*"/> </Grid.ColumnDefinitions>
表示3列:第一列固定宽度40pixel;剩下空间被分成两列,第三列宽度是第二列的两倍。语法同样适用于行。
控制Grid上子控件的位置使用(Index从0开始的):
Grid.Column
Grid.Row
控制某一cell所占据的大小使用(Index从1开始):
Grid.ColumnSpan
Grid.RowSpan
下图显示Grid控件包含3列1行,第一个控件Column=1,第二个占据了2-3列,这是由于Grid.ColumnSpan=2的缘故。
对应的XAML代码为:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="WPF_Tour_Beginners_Layout.GridDEMO" x:Name="Window" Title="GridDEMO" WindowStartupLocation="CenterScreen" Width="640" Height="480"> <Grid Width="Auto" Height="Auto" > <Grid.ColumnDefinitions> <ColumnDefinition Width="40"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="2*"/> </Grid.ColumnDefinitions> <Rectangle Fill="Aqua" Grid.Column="0" Grid.Row="0"/> <Rectangle Fill="Plum" Grid.Column="1" Grid.ColumnSpan="2"/> </Grid> </Window>
7)Putting it all Together
考虑上面所说的WinForm经典界面:
包括:菜单栏,按钮区及显示区,外加底部状态栏,用WPF来完成,XAML代码为:
<Window x:Class="WPF_Tour_Beginners_Layout.PuttingItAllTogether" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" WindowStartupLocation="CenterScreen" Title="PuttingItAllTogether" Width="640" Height="480" > <DockPanel Width="Auto" Height="Auto" LastChildFill="True"> <!--Top Menu Area--> <Menu Width="Auto" Height="20" Background="SteelBlue" DockPanel.Dock="Top"> <!-- File Menu --> <MenuItem Header="File"> <MenuItem Header="Save"/> <Separator/> <MenuItem Header="Exit"/> </MenuItem> <!-- About Menu --> <MenuItem Header="Help"> <MenuItem Header="About"/> </MenuItem> </Menu> <!--Bottom Status Bar area, declared before middle section, as I want it to fill entire bottom of Window, which it wouldnt if there was a Left docked panel before it --> <StackPanel Width="Auto" Height="31" Background="LightGray" Orientation="Horizontal" DockPanel.Dock="Bottom"> <Label Width="155" Height="23" Content="Status Bar Message...." FontFamily="Arial" FontSize="10"/> </StackPanel> <!--Left Main Content area--> <StackPanel Width="136" Height="Auto" Background="Gray"> <Button Margin="5,5,5,5" Width="Auto" Height="26" Content="button1"/> <Button Width="126" Height="26" Content="button2" Margin="5,5,5,5"/> <Button Width="126" Height="26" Content="button3" Margin="5,5,5,5"/> </StackPanel> <!--Right Main Content area, NOTE HOW this Grid is the last child so takes all the remaining room --> <Grid Width="Auto" Height="Auto" Background="White"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Rectangle Fill="Aqua" Margin="10,10,10,10" Grid.Row="0" Grid.Column="0"/> <Rectangle Fill="Aqua" Margin="10,10,10,10" Grid.Row="0" Grid.Column="1"/> <Rectangle Fill="Aqua" Margin="10,10,10,10" Grid.Row="1" Grid.Column="0"/> <Rectangle Fill="Aqua" Margin="10,10,10,10" Grid.Row="1" Grid.Column="1"/> </Grid> </DockPanel> </Window>
设计DockPanel时依次是Menu->Bottom Staus -> leftButtons -> Right Shown,注意DockPanel的LastChildFill属性为true;同时status bar的StackPanel必须始于其它控件(声明为DockPanel.Dock=Left或DockPanel.Dock=Right)。
结果界面与WinForm相同(没有展示)。
8)Performance Considerations
考虑父控件中包含大量的子控件,如一个StackPanel中有一个ListBox,而ListBox与一个大型的询问数据库相连。这种情况下,ListBox包含很多子控件(使用StackPanel作为子控件?),看起来这不是个好主意。
然而,在ListBox使用VirtualizingStackPanel.IsVirtualizing,这意味着ListBox控件内部的StackPanel将会可见?
当一个Panel可见,意即可视化部分被创建,如当创建的ListBox与有100000行的数据库相连用于显示图像,这样加载ListBox会耗费很长时间。如果使用Virtualize Panel,只有UI上可见部分的图像被创建。当向下拉动滚动条,当前显示的部分被销毁,新的可见部分会被加载。目前只有一个Panel支持Virtualization,就是VirtualizingStackPanel。如果需要新的Virtualized Panel,就需要自己写。
9)Custom Layouts
创建自己喜欢的Panel,需要从System.Windows.Controls.Panel派生,然后重写函数:MeasureOverride,LayoutOverride,可以参考FishEye Panel。
10)后记
我在运行实例代码时,发现主界面很好看:
点击上面任何一个*.xaml Demo,弹出的窗体(上文有显示)会覆盖这个界面,类似于只看到一个界面,找到对应的XAML代码:
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="WPF_Tour_Beginners_Layout.DEMOLauncher" x:Name="Window" Title="DEMOLauncher" WindowStartupLocation="CenterScreen" Width="640" Height="480"> <DockPanel Margin="0,0,0,0" Background="White" LastChildFill="true" x:Name="dockContainer"> <Label Height="44" Content="From this page you can launch all the DEMO windows" FontFamily="Arial Rounded MT" FontSize="16" Margin="0,0,0,0" VerticalAlignment="Top" HorizontalAlignment="Center" Width="{Binding Path=ActualWidth, ElementName=dockContainer, Mode=Default}" Background="#FF000000" Foreground="#FFFFFFFF" DockPanel.Dock="Top"/> <ListBox x:Name="lstDEMOS" Width="Auto" Height="Auto" IsSynchronizedWithCurrentItem="True"/> </DockPanel> </Window>
代码比较简单,其中Binding Path的Actual Width为DockPanel自身属性值,Element name就是DockPanel。
再看看C#代码:
using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Navigation; namespace WPF_Tour_Beginners_Layout { public partial class DEMOLauncher { public DEMOLauncher() { this.InitializeComponent(); lstDEMOS.SelectionChanged += new SelectionChangedEventHandler(lstDEMOS_SelectionChanged); lstDEMOS.Items.Add("CanvasDEMO.xaml"); lstDEMOS.Items.Add("CanvasDEMOCodeBehind.xaml"); lstDEMOS.Items.Add("StackPanelDEMO.xaml"); lstDEMOS.Items.Add("StackPanelDEMOCodeBehind.xaml"); lstDEMOS.Items.Add("WrapPanelDEMO.xaml"); lstDEMOS.Items.Add("WrapPanelDEMOCodeBehind.xaml"); lstDEMOS.Items.Add("DockPanelDEMO.xaml"); lstDEMOS.Items.Add("DockPanelDEMOCodeBehind.xaml"); lstDEMOS.Items.Add("GridDEMO.xaml"); lstDEMOS.Items.Add("GridDEMOCodeBehind.xaml"); lstDEMOS.Items.Add("PuttingItAllTogether.xaml"); } void lstDEMOS_SelectionChanged(object sender, SelectionChangedEventArgs e) { switch (lstDEMOS.SelectedValue.ToString()) { case "CanvasDEMO.xaml": CanvasDEMO demoWin1 = new CanvasDEMO(); demoWin1.Owner = this; demoWin1.ShowDialog(); break; case "CanvasDEMOCodeBehind.xaml": CanvasDEMOCodeBehind demoWin2 = new CanvasDEMOCodeBehind(); demoWin2.Owner = this; demoWin2.ShowDialog(); break; case "StackPanelDEMO.xaml": StackPanelDEMO demoWin3 = new StackPanelDEMO(); demoWin3.Owner = this; demoWin3.ShowDialog(); break; case "StackPanelDEMOCodeBehind.xaml": StackPanelDEMOCodeBehind demoWin4 = new StackPanelDEMOCodeBehind(); demoWin4.Owner = this; demoWin4.ShowDialog(); break; case "WrapPanelDEMO.xaml": WrapPanelDEMO demoWin5 = new WrapPanelDEMO(); demoWin5.Owner = this; demoWin5.ShowDialog(); break; case "WrapPanelDEMOCodeBehind.xaml": WrapPanelDEMOCodeBehind demoWin6 = new WrapPanelDEMOCodeBehind(); demoWin6.Owner = this; demoWin6.ShowDialog(); break; case "DockPanelDEMO.xaml": DockPanelDEMO demoWin7 = new DockPanelDEMO(); demoWin7.Owner = this; demoWin7.ShowDialog(); break; case "DockPanelDEMOCodeBehind.xaml": DockPanelDEMOCodeBehind demoWin8 = new DockPanelDEMOCodeBehind(); demoWin8.Owner = this; demoWin8.ShowDialog(); break; case "GridDEMO.xaml": GridDEMO demoWin9 = new GridDEMO(); demoWin9.Owner = this; demoWin9.ShowDialog(); break; case "GridDEMOCodeBehind.xaml": GridDEMOCodeBehind demoWin10 = new GridDEMOCodeBehind(); demoWin10.Owner = this; demoWin10.ShowDialog(); break; case "PuttingItAllTogether.xaml": PuttingItAllTogether demoWin11 = new PuttingItAllTogether(); demoWin11.Owner = this; demoWin11.ShowDialog(); break; } } } }
也很简洁,ListBox的SelectionChanged事件也是在C#声明并完成的,使用ShowDialog()显示Demo窗体,这样就看不到原来的窗体(关掉Demo窗体后才能看到)。
真正做到了界面与数据的分离。