WPF控件和布局,根據劉鐵猛《深入淺出WPF》書籍講解內容,主要記錄控件和布局的原理,如果有不足的地方,請大牛們鍵盤下留情--輕噴!如果還算有用,請給點動力,支持一把!
一、WPF里的控件
1.1 控件的實質
我們先從UI上分析,UI的功能是讓用戶觀察和操作數據,為了能顯示數據和響應用戶的操作通知程序(通過事件來通知,如何處理事件又是一系列的算法),所以控件就是顯示數據和響應用戶操作的UI元素,也即:控件就是數據和行為的載體。
1.2 WPF中的一個重要概念--數據驅動UI
什么是數據驅動UI呢?我們知道傳統的GUI界面都是由windows消息通過事件傳遞給程序,程序根據不同的操作來表達出不同的數據體現在UI界面上,這樣數據在某種程度上來說,受到很大的限制。WPF中是數據驅動UI,數據是核心,處於主動的,UI從屬於數據並表達數據,是被動的。因為以后的章節會重點介紹,在此不做過多的說明,只要記着,WPF數據第一,控件第二。
1.3 WPF中控件的知多少
雖然控件沒有數據重要,但是還是比較重要的,畢竟是門面啊,只是在數據面前,它比較"有禮貌"。控件有很多,但是如果仔細去分析,也是有規律可循的,根據其作用,我們可以把控件分為6類:
- 布局控件:是可以容納多個控件或者嵌套其他布局的控件,用於在UI上組織和排列控件。其父類為Panel。
- 內容控件:只能容納一個控件或者布局控件作為他的內容。所以經常借助布局控件來規划其內容。其父類為ContentControl。
- 帶標題內容控件:相當於一個內容控件,但是可以加一個標題,標題部分也可以容納一個控件或者布局,其父類為HeaderedContentControl。
- 條目控件:可以顯示一列數據,一般情況下,是數據的類型是相同的。其共同的基類為ItemsControl。
- 帶標題的條目控件:和上面的帶標題內容控件類同,其基類為HeaderdeItemsControl。
- 特殊內容控件:這類控件比較獨立,但也比較常用,如TextBox,TextBlock,Image等(由於其常用性和相對比較簡單,本篇筆記不做說明)。
上面的控件的派生關系如圖1:
圖1
二、各類控件模型詳解
2.1 WPF中的內容模型
為了理解各個控件的模型,還是先了解一下WPF中的內容模型。在上述各類控件里,至少可以容納一個內容,主要原因是由於每個控件對象都會有一個重要又不常寫出來的屬性--Content Property(有Content,Child,Items,Children幾個屬性,如Grid可以容納多個控件,用的是Children)。內容模型就是每一族的控件都含有一個或者多個元素作為其內容(其下面的元素可能是其他控件)。為什么可以不常寫出來呢?先讓我們看下面兩段代碼:

<Window x:Class="Chapter_03.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="內容屬性測試" Height="350" Width="525"> <Grid> <Grid.Children> <Button Content="1" Margin="120,146,0,146" HorizontalAlignment="Left" Width="82" /> <Button Content="2" x:Name="btn2" Margin="0,146,142,145" HorizontalAlignment="Right" Width="82" /> </Grid.Children> </Grid> </Window>

<Window x:Class="Chapter_03.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="內容屬性測試" Height="350" Width="525"> <Grid> <Button Content="1" Margin="120,146,0,146" HorizontalAlignment="Left" Width="82" /> <Button Content="2" x:Name="btn2" Margin="0,146,142,145" HorizontalAlignment="Right" Width="82" /> </Grid> </Window>
運行兩段代碼效果一樣。充分說明了重要而有不常見的原因。因為省略的省時,而且簡潔明了。所以多數引用時都省去了。
2.2ContentControl族
先說一下其特點:他們內容屬性的名稱為Content,只能有單一元素充當其內容。下面通過例子說明其特點:
<Button Margin="120,146,0,76" HorizontalAlignment="Left" Width="100"> <TextBox Text="測試"/> <TextBox Text="測試"/> <TextBox Text="測試"/> </Button>
上面的會報錯,原因是Button里面只能有單一元素充當其內容。去掉后面的兩個TextBox,效果如圖2:
圖2
發現button里面不僅可以顯示文字還可以用一個控件來當其內容。其他的控件不在一一舉例。在此列出此類的主要控件:
Button、ButtonBase、CheckBox、ComboBoxItem、ContentControl、Frame、GridViewColumnHeader、GropItem、Label、ListBoxItem、ListViewItem、NavigationWindow、RadioButton、ScrollViewer、StatusBarItem、ToggleButton、ToolTip、UserControl、Window。
2.3 HeaderedContentControl族
特點:可以顯示帶標題的數據,內容屬性為Content和Header,其這兩個屬性都只能容納一個元素。在此舉例說明GroupBox的用法,然后列出其他屬於此類的控件。XAML代碼為:

<Window x:Class="Chapter_03.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="內容屬性測試" Height="200" Width="300"> <Grid Background="Gold"> <GroupBox Margin="42,0,96,26"> <GroupBox.Header> <Label Content="我是標題"/> </GroupBox.Header> <Button HorizontalAlignment="Left" Width="117" Height="45"> <TextBox Text="測試"/> </Button> </GroupBox> </Grid> </Window>
效果圖如圖3:
圖3
是不是看着很還好呢?現在列出同類主要的控件:Expender,GroupBox,HeaderedContentControl,TabItem。
2.4 ItemsControl族
特點:該類控件用於顯示列表化的數據,內容屬性為Items或ItemsSource,每種ItemsControl都對應有自己的條目容器(Item Container)。本類元素可能會用的比較多些,也比較靈活,所以這里不做過多記錄,以后的記錄會經常用到,具體的再詳細說明。下面就用一個ListBox控件來小試牛刀吧!XAML代碼、Cs代碼如下:

<Window x:Class="Chapter_03.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="內容屬性測試" Height="260" Width="408"> <Grid Background="Gold"> <ListBox x:Name="listbox" Margin="0,0,198,55"> <CheckBox x:Name="cb1" Content="選擇"/> <CheckBox x:Name="cb2" Content="選擇"/> <CheckBox x:Name="cb3" Content="選擇"/> <CheckBox x:Name="cb4" Content="選擇"/> <Button x:Name="btn1" Content="按鈕1"/> <Button x:Name="btn2" Content="按鈕1"/> <Button x:Name="btn3" Content="按鈕1"/> </ListBox> </Grid> </Window>

using System; using System.Collections.Generic; using System.Linq; using System.Text; 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 Chapter_03 { /// <summary> /// MainWindow.xaml 的交互邏輯 /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); Button btn=new Button(); btn.Content="另外添加一個"; btn.Click += new RoutedEventHandler(btn_Click); this.listbox.Items.Add(btn); btn3.Click+=new RoutedEventHandler(btn_Click); } /// <summary> /// 用來找到button的父級元素類型 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void btn_Click(object sender, RoutedEventArgs e) { Button btn=(sender) as Button; DependencyObject level1 = VisualTreeHelper.GetParent(btn); DependencyObject level2 = VisualTreeHelper.GetParent(level1); DependencyObject level3 = VisualTreeHelper.GetParent(level2); if (btn != null) MessageBox.Show(level3.GetType().ToString()); else MessageBox.Show("無找到!"); } } }
效果圖如圖4:
圖4
先來說明一下代碼:在listBox里面放了幾個checkbox和button,說明ListBoxI的Item不僅支持類型相同的元素,還支持類型不同的元素。這是因為,Listbox的每一項都是經過“ListBoxItem”加工廠處理的,最終放入當做自己的內容--放入自己的容器內。這里通過后台代碼說明了每一個條目都被ListboxItem包裝過了,完全沒有必要每一個條目都在xmal文件按照如下寫法:
<ListBoxItem> <Button x:Name="btn3" Content="按鈕1"/> </ListBoxItem>
在實際項目中,很少像上面那樣把代碼寫死,可以動態的綁定ListBox。把數據源賦給ListBox的ItemsSource,通過DisplayMemberPath屬性來顯示string類型的數據源里面的字段條目(如果想顯示復雜的數據的話,要使用DataTemplate,具體在模板再記錄,在此知道有這么一回事就好了);通過SelectedItem和SelectionChanged來觀察選中的項。下面的例子實現在listbox上綁定指定數據,然后彈出選中人的年齡。直接給出代碼:

<Window x:Class="Chapter_03.ListBoxTest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="ListBoxTest" Height="300" Width="300"> <Grid> <ListBox x:Name="listbox1" Margin="0,0,60,31" SelectionChanged="listbox1_SelectionChanged"></ListBox> </Grid> </Window>

using System; using System.Collections.Generic; using System.Linq; using System.Text; 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.Shapes; namespace Chapter_03 { /// <summary> /// ListBoxTest.xaml 的交互邏輯 /// </summary> public partial class ListBoxTest : Window { public ListBoxTest() { InitializeComponent(); InitData(); } protected void InitData() { List<People> peopleList = new List<People>() { new People(){Id=1,Name="Tim",Age=30}, new People(){Id=2,Name="Tom",Age=30},
顯示結果如圖5:
圖5
下面列出屬於ItemsControl族元素和其對應的Item Container有ComboBox——ComboBoxItem,ContextMenu——MenuItem,ListBox——ListBoxItem,ListView——ListViewItem,Menu——MenuItem,StatusBar——StatusBarItem,TabControl——TabItem,TreeView——TreeViewItem.
由於已經演示了HeaderedContentControl和ItemsControl的功能,另外HeaderedItemsControl的用法就不再記錄了,僅僅列出屬於其族的控件:
MemuItem、TreeViewItem、TooBar。
三、 UI布局
在介紹布局之前還是先記錄一下布局控件的特點與屬於Panel族的控件。
panel族控件內容屬性為Children,所以內容可以是多個元素,這對布局來說是很重要的特征。布局控件與ItemControl的區別是:前者強調的是對元素的布局,后者強調的是條目。屬於Panel類的控件有:Canvas,DockPanel,Grid,TabPanel,ToolBarOverflowPanel,StackPanel,ToolBarPanel,UniformGrid,VirtualizingPanel,VirtualizingStackPanel,WrapPanel。這么多控件不可能一個個去介紹,找幾個比較重要的實踐一下。回頭如果有用到的話再逐一研究。
3.1 主要布局控件的特性
在WPF里面控件與控件的關系除了相鄰和重疊(用Opacity來控制哪個控件在上面,哪個在下面),還有一個包含。正因為如此,才有了以window為根的樹形結構的XAML。下面介紹一下主要布局元素的特性:
- Grid:網格。可以自定義行和列,並通過行列的數量、行高和列寬來調整控件的布局,有點類似於html中的Table。
- StackPanel:棧式面板。可以將包含元素排成一條直線,當添加或移除包含元素時,后面的元素會自動向下或向上移動。
- Canvas:畫布。可以指定包含元素的絕對坐標位置。
- DockPanel:泊靠式面板。內部元素可以選擇泊靠方式。
- WarpPanel:自動折行面板。當一行元素排滿后會自動換行。類似html中的流式布局。
3.2 Grid
Grid的特點如下:
- 可以定義任意數量的行和列
- 行高與列寬可以使用絕對值,相對比以及最大值和最小值
- 內部元素可以設置自己的所在列和行,還可以設置自己跨幾列和行。
- 可以設置Children元素的對齊方式
現在給出定義行與列的代碼(記得在后台代碼上加上 this.grid.ShowGridLines=true以便顯示出網格):

<Window x:Class="Chapter_03.Grid" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Grid" Height="300" Width="300" MinHeight="300" MaxWidth="500"> <!--MinHeight="300" MaxWidth="500"限制窗口的最小高度和最大寬度--> <Grid x:Name="grid"> <!--定義行--> <Grid.RowDefinitions> <RowDefinition Height="25" ></RowDefinition> <RowDefinition Height="50"/> <RowDefinition Height="1*"/> <RowDefinition Height="*"/> <RowDefinition Height="auto"> </RowDefinition> </Grid.RowDefinitions> <!--定義列--> <Grid.ColumnDefinitions> <ColumnDefinition Width="25" ></ColumnDefinition> <ColumnDefinition Width="50"/> <ColumnDefinition Width="1*"/> <ColumnDefinition Width="*"/> <ColumnDefinition Width="auto"/> </Grid.ColumnDefinitions> <!--在指定的行列中布置控件--> <TextBox Grid.Column="1" Grid.Row="2" Grid.ColumnSpan="2" Text="布局" Background="Gray"/> </Grid> </Window>
運行效果圖如圖6,可以放大觀察效果(是因為Width="*"的原因,本例子中利用了兩個*其中第三行是一個*,所以占剩余的二分之一,可以試着改成2*,就是三分之二了,可以試着觀察效果):
圖6
3.3 StackPanel
StackPanel可以把內部的元素在縱向或者橫向上緊密排列,形成棧式布局。先介紹一下其三個屬性:
- Orientation 決定內部元素是橫向還是縱向累積。可取值為Horizontal,Vertical。
- HorizontalAlignment 決定內部元素水平方向上的對齊方式。可取值Left,Center,Right,Stretch。
- VerticalAlignment 決定內部元素豎直方向上的對齊方式。可取Top,Center,Bottom,Stretch。
StackPanel也是布局中比較常見的控件,下面舉例:添加按鈕,其他內容控件會自動下移。效果如圖7:
圖7
下面上代碼:

<Window x:Class="Chapter_03.StackPanel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="StackPanel" Height="338" Width="423"> <Grid Height="286" Width="382"> <GroupBox Header="測試StackPanel" BorderBrush="Black" Margin="5"> <StackPanel Margin="5" x:Name="stackpanel"> <StackPanel Orientation="Vertical" x:Name="btnList"></StackPanel> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <TextBlock Text="填寫添加按鈕名稱: " Height="20" /> <TextBox Name="btnName" Width="102" Height="20" /> <Button Content="添加" Width="60" Margin="5" Click="Button_Click" /> </StackPanel> </StackPanel> </GroupBox> </Grid> </Window>

using System; using System.Collections.Generic; using System.Linq; using System.Text; 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.Shapes; namespace Chapter_03 { /// <summary> /// StackPane_.xaml 的交互邏輯 /// </summary> public partial class StackPanel : Window { public StackPanel() { InitializeComponent(); } private void Button_Click(object sender, RoutedEventArgs e) { if (!string.IsNullOrEmpty(this.btnName.Text)) { Button btn = new Button(); btn.Content = this.btnName.Text; this.btnList.Children.Add(btn); } else MessageBox.Show("請輸入按鈕名稱!"); } } }
當輸入按鈕名稱的話,點擊添加,原有的內容會下移。
3.4 Canvas
畫布:內容控件可以准確定位到指定坐標,但是不足的地方是,如果要修改的話可能會關系到很多的控件,所以如果不需要經常修改的窗體,使用該控件布局,或者是藝術性比較強(用來實現依賴於橫縱坐標的動畫等功能)的布局使用此控件布局。
在此制作一個登陸頁面主要來看一下Canvas.Left與Canvas.Top的用法。效果圖如圖8,直接上代碼:

<Window x:Class="Chapter_03.Canvas" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="登陸" Height="145" Width="300"> <Canvas Background="Sienna"> <TextBlock Canvas.Left="0" Canvas.Top="13" Margin="5" Text="用戶名:"/> <TextBox Canvas.Left="50" Canvas.Top="13" Width="160" /> <TextBlock Canvas.Left="0" Canvas.Top="47" Margin="5" Text="密 碼:"/> <TextBox Canvas.Left="50" Canvas.Top="47" Width="160" /> <Button Content="確定" Canvas.Left="70" Canvas.Top="77" Width="63" Height="22" /> <Button Canvas.Left="150" Canvas.Top="77" Content="清除" Width="63" Height="22" /> </Canvas> </Window>
圖8
3.5 DockPanel
這個控件主要有個最后一個內容控件實現填充所有剩余部分的功能。主要用到LastChildFill=True屬性。下面給出一個例子,先看一下把LastChildFill分別設置為True和False的結果對比圖如圖9:
圖9
XAML代碼給出:

<Window x:Class="Chapter_03.DockPanel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="DockPanel" Height="300" Width="300"> <DockPanel Name="dockpanel" LastChildFill="True"> <Button Name="button1" DockPanel.Dock="Top">1</Button> <Button Name="button2" DockPanel.Dock="Bottom" >2</Button> <Button Name="button3" DockPanel.Dock="Left">3</Button> <Button Name="button4" DockPanel.Dock="Right">4</Button> <Button DockPanel.Dock="Top">剩余空間</Button> </DockPanel> </Window>
在此說明一下,如果LastChildFill=True,最后一個元素 <Button >剩余空間</Button>就會充滿其剩余部分。上面的只能填充,但是不能通過拖拽的方式改變控件的寬度。下面給出一個實現拖拽功能的代碼。不過是在Grid里面的通過GridSplitter(可以改變Grid初始設置的行高或列寬)控件實現。直接給出代碼:

<Window x:Class="Chapter_03.GridSplitter" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="GridSplitter" Height="300" Width="300"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="5"/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="150"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition/> </Grid.ColumnDefinitions> <TextBox Grid.ColumnSpan="3" BorderBrush="Black"/> <TextBox Grid.Row="1" BorderBrush="Black"/> <GridSplitter Grid.Row="1" Grid.Column="1" VerticalAlignment="Stretch" HorizontalAlignment="Center" Width="5" Background="Gray" ShowsPreview="True"/> <TextBox Grid.Row="1" Grid.Column="2" BorderBrush="Black"/> </Grid> </Window>
具體的GridSplitter的屬性見http://www.cnblogs.com/luluping/archive/2011/08/26/2155218.html。
3.6 WrapPanel
此控件會根據布局的大小來控制內容元素的排列。不會因為窗體沒有放大,影響到其他內容的顯示。在此只舉一個例子,來理解WrapPanel。上代碼了:

<Window x:Class="Chapter_03.WrapPanel" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="WrapPanel" Height="300" Width="300"> <WrapPanel> <Button Width="50" Height="50"/> <Button Width="50" Height="50"/> <Button Width="50" Height="50"/> <Button Width="50" Height="50"/> <Button Width="50" Height="50"/> <Button Width="50" Height="50"/> <Button Width="50" Height="50"/> </WrapPanel> </Window>
效果圖如圖10:
圖10
四、總結
布局一直是自己的弱項,所以可能這篇記錄的會比較差點,但是重在理解控件的作用以及能舉一反三。 雖然控件沒有一一列出,但是對於每一族的控件都給出了一個實例,可以通過實例加深對各個控件的理解,具體的運用還需多加強練習和查閱msdn。下一篇:深入淺出話Binding。