内容

我的WPF笔记

第一章 XAML

在上一章节中,我们介绍了XAML是什么,它有什么用,那么该如何用它来创建一个组件呢?你会下下一个例子中看到,创建一个组件是多么简单,就像写出名字一样,比如创建一个按钮,就像这样:

<Button>

XAML标签必须有结尾, 在起始标签尾部用斜杠或者使用结束标签都可以.

<Button></Button>

或者

<Button />

多数的控件允许你在开始和结束之间放置内容, 这就是控件的内容(content). 比如,Button控件允许你在开始和结束之间指定显示的文字.

<Button>A button</Button>

HTML不区分大小写,但XAML区分. 因为控件的名字最终会有对应到.NET框架下的类型(Type). 同理XAML标签的属性也区分大小写,因为这是控件的属性. 我们可以定义一个带有属性的Button控件:

<Button FontWeight="Bold" Content="A button" />

我们设定了粗细(FontWeight)为粗体(Bold). 我们设定了内容(Content), 这和在Button控件标签的开始和结束间留下文字是一样的. 另外, 我们还可以通过用标签来指定控件的属性内容, 标签名为点号连接控件名和属性名:

<Button>
    <Button.FontWeight>Bold</Button.FontWeight>
    <Button.Content>A button</Button.Content>
</Button>

上面两种方法的结果是一样的,只是形式的差别. 但是,很多的控件允许使用文字以外的作为内容, 比如, 我们可以在按钮中嵌套TextBlock控件来显示3中不同颜色的文字:

<Button>
    <Button.FontWeight>Bold</Button.FontWeight>
    <Button.Content>
        <WrapPanel>
            <TextBlock Foreground="Blue">Multi</TextBlock>
            <TextBlock Foreground="Red">Color</TextBlock>
            <TextBlock>Button</TextBlock>
        </WrapPanel>
    </Button.Content>
</Button>

内容(Content)属性只接受一个元素, 所以我们用WrapPanel控件把三个TextBlock控件包起来了. 这样的Panel控件有很多的用途, 这个我们以后再讲.

当然, 你也可以这样写:

<Button FontWeight="Bold">
    <WrapPanel>
        <TextBlock Foreground="Blue">Multi</TextBlock>
        <TextBlock Foreground="Red">Color</TextBlock>
        <TextBlock>Button</TextBlock>
    </WrapPanel>
</Button>

代码 vs. XAML

在上面的例子中我们看到XAML还是很好写的,但这不是唯一的办法, 你也可以用C#来达到和上面XAML一样的效果.

Button btn = new Button();
btn.FontWeight = FontWeights.Bold;

WrapPanel pnl = new WrapPanel();

TextBlock txt = new TextBlock();
txt.Text = "Multi";
txt.Foreground = Brushes.Blue;
pnl.Children.Add(txt);

txt = new TextBlock();
txt.Text = "Color";
txt.Foreground = Brushes.Red;
pnl.Children.Add(txt);

txt = new TextBlock();
txt.Text = "Button";
pnl.Children.Add(txt);

btn.Content = pnl;
pnlMain.Children.Add(btn);

当然csharp的例子中可以使用更多的语法糖来写的没那么繁琐, 但你应该承认XAML要精炼的多.

现在多数的UI框架都是事件驱动的, WPF也是. 所有的控件, 包括窗体(Window控件)都提供了大量的事件可以订阅. 你可以订阅这些事件,这意味着你的应用程序将在事件发生的时候接受到通知并且你可以对这些事件做成响应。

有很多不同类型的事件, 大量的事件用于在用户使用鼠标键盘和你的应用互动的时候. 在多数的控件上你会找到 KeyDown, KeyUp, MouseDown, MouseEnter, MouseLeave, MouseUp 之类的事件.

我们来看看事件怎么工作的吧. 这有点复杂, 但现在我们只要知道怎么在XAML和代码中通过事件连接起来就可以了, 看个例子:

<Window x:Class="WpfTutorialSamples.XAML.EventsSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="EventsSample" Height="300" Width="300">
    <Grid Name="pnlMainGrid" MouseUp="pnlMainGrid_MouseUp" Background="LightBlue">        
        
    </Grid>
</Window>

注意我们在Grid中订阅了MouseUp事件, 指向了方法名pnlMainGrid_MouseUp. 这个方法会在代码中定义成这样:

private void pnlMainGrid_MouseUp(object sender, MouseButtonEventArgs e)
{
    MessageBox.Show("You clicked me at " + e.GetPosition(this).ToString());
}

MouseUp事件使用的是一个名为MouseButtonEventHandler的委托, 它有两个参数, sender(发生事件的控件), MouseButtonEventArgs(一些有用的信息, 我们在上面的例子中通过它获取了鼠标的位置).

有些事件会使用同一个委托, 比如MouseUp和MouseDown共用MouseButtonEventHandler, 注意MouseMove用的是MouseEventHandler. 当你定义事件处理方法的时候, 你需要知道事件使用的委托, 文档里可以找到.

幸运的是, Visual Studio 可以帮助我们生成正确的事件处理方法. 最简单的办法是, 在XAML中输入事件的名字, VS的IntelliSense就会提供生成新事件处理方法的选项:

Visual Studio 提示可以创建新的事件处理方法

当你选中<New Event Handler>Visual Studio 会在代码文件中生成合适的事件处理方法, 名字会长这样: <control name>_<event name>, 这个例子中就是 pnlMainGrid_MouseDown. 右键事件的名字并在弹出的菜单中选中Navigate to Event Handler你就会看到新生成的方法了.

上面的就是最常见的事件订阅方法, 但有的时候你想直接在代码中订阅事件. 在csharp中这会使用到+=语法, 你可以将一个事件处理方法直接添加到对象中. 详细的解释需要一个单独的C#例子, 作为比较, 这是一个例子:

using System;
using System.Windows;
using System.Windows.Input;


namespace WpfTutorialSamples.XAML
{
    public partial class EventsSample : Window
    {
        public EventsSample()
        {
            InitializeComponent();
            pnlMainGrid.MouseUp += new MouseButtonEventHandler(pnlMainGrid_MouseUp);
        }

        private void pnlMainGrid_MouseUp(object sender, MouseButtonEventArgs e)
        {
            MessageBox.Show("You clicked me at " + e.GetPosition(this).ToString());
        }

    }
}

一样的,你需要知道使用哪个委托, 还是一样的, Visual Studio 会帮你, 当你输入完:

pnlMainGrid.MouseDown +=

Visual Studio 会显示:

Visual Studio 正在提示可以在代码中创建新的事件处理方法.

按下 Tab 键两次 Visual Studio 就会生成正确的事件处理方法, 就在当前方法的下面. 你需要实现方法的内容. 当你使用以上方法订阅事件的时候, 就不需要再通过XAML订阅了.

第二章 WPF应用程序

APP.xaml是你的应用程序定义的起点。当你创建一个新的WPF应用时,Visual Studio将自动为你创建它,同时还包括一个名为App.xaml.cs的后置代码文件。跟Window类似,这两个文件里面定义的是部分类,它们允许你同时在XAML标记和后置代码中完成工作。

App.xaml.cs 继承自Application类,在WPF Windows应用程序中是一个中心类。.NET会进入这个类,从这里启动需要的窗口或页面。这也是一个订阅一些重要应用程序事件的地方,例如,应用程序启动事件,未处理的异常事件等,更多内容会在后面提到。

App.xaml文件最常用的功能之一是定义全局资源,这些资源(例如全局样式)将可以从整个应用程序中使用和访问。这一点将在后面详细讨论。

在创建一个新的应用程序时,可能会在自动生成的App.xaml中看到这样的内容:

<Application x:Class="WpfTutorialSamples.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">
    <Application.Resources>

    </Application.Resources>
</Application>

这里要注意的主要是StartupUri属性,它实际上指定了当应用程序启动时应该被加载的Window或Page。在这个例子中,MainWindow.xaml会被启动,但是如果你想使用另外一个window作为启动入口点,你只需要修改一下这个属性即可。

在某些场景下,你可能需要对第一个窗口怎样或何时显示加以控制。在这个例子中,你可以移除StartupUri属性和值,然后把对它做的所有工作使用后端代码替代。这将在下面进行演示。

通常情况下,对于一个新创建应用程序,与之匹配的App.xaml.cs 可能看起来像这个样子:

using System;
using System.Collections.Generic;
using System.Windows;

namespace WpfTutorialSamples
{
    public partial class App : Application
    {

    }
}

你会看到这个类继承自Application类,它允许我们在应用级别去做一些事情。举个例子,你可以订阅Startup事件,然后手动创建你的启动窗口。

这是一个示例:

<Application x:Class="WpfTutorialSamples.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Startup="Application_Startup">
    <Application.Resources></Application.Resources>
</Application>

注意StartupUri是如何被Starup订阅事件所替换的(通过XAML订阅事件将在别的章节中解释)。在其背后的代码中,你可以像这样使用事件:

using System;
using System.Collections.Generic;
using System.Windows;

namespace WpfTutorialSamples
{
    public partial class App : Application
    {

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            // Create the startup window
            MainWindow wnd = new MainWindow();
            // Do stuff here, e.g. to the window
            wnd.Title = "Something else";
            // Show the window
            wnd.Show();
        }
    }
}

比起使用StartupUri属性,在这个示例中比较酷的事是我们可以在显示启动窗口之前对它进行操纵。在这里,我们改变了它的title,虽然它不是特别有用,但是你还可以对其他事件进行订阅,或者显示一个启动屏幕(飞溅窗口)。当你可以控制所有东西的时候,你就可以做很多事情了。我们会在接下来的章节中深入研究其中的几个。

WPF引入了一个非常方便的概念:将数据作为一种资源来存储,既可以是控件的本地数据,也可以是整个窗口的本地数据,亦或是整个应用程序的全局数据。这些数据可以是你想要的任何东西,从实际信息到WPF控件的层次结构。这使你能够将数据一处存放,随处使用,非常实用。

这个概念经常用于样式和模板,这些我们将在本教程的后面讨论,但正如本章所说明的那样,你也可以把它用于许多其他方面。请允许我通过一个简单的例子来说明:

<Window x:Class="IcveTools.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:IcveTools"
        mc:Ignorable="d"
        Title="MainWindow" Height="150" Width="350">
    <Window.Resources>
        <sys:String x:Key="strHelloWorld">Hello,World</sys:String>
    </Window.Resources>
    <StackPanel>
        <TextBlock Text="{StaticResource strHelloWorld}" FontSize="56" />
        <TextBlock>Just Another <TextBlock Text="{StaticResource strHelloWorld}" /> example,but with resources!   </TextBlock>
    </StackPanel>
</Window>

image-20220703102335124

各個資源將透過「x:Key」屬性給予一個鍵值,這允許你在應用程式的其他地方透過該鍵值結合「StaticResource」之標記延伸來參照該資源。在這個例子中,我只儲存了一個間單的字串,之後使用在兩個不同的「TextBlock」控制項中。

目前为止的例子中,我使用了「StaticResource」之标记延伸来参照资源,然而有另外一个以「DynamicResource」形式的选择。

主要的差异即是静态资源仅会在XAML载入的时间点被设定一次,如果这个资源之后被改变,这项改变将不会反映到你使用「StaticResource」的地方。

另一方面,一个「DynamicResource」会在它实际需要时设定一次,并且当资源改变时再次设定。可以想成「系结一个静态值」与「系结一个监视这个值并且每当该值改变时将它传送给你的函式」的差异,这并不是它实际上运作的方式,不过可以让你比较容易了解何时该使用哪个。动态资源也允许你使用设计阶段尚未存在的资源,例如你在后置程式码中于应用程式启动时加入的资源。

共用一个简单字符串非常的容易,但你可以做的更多。在下一个例子中,我同时会存储一个完整的字串阵列搭配一个用于背景的渐层笔刷,这应该能非常好的让你了解你可以透过资源做到多少事:

<Window x:Class="IcveTools.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:IcveTools"
        mc:Ignorable="d"
        Background="{DynamicResource WindowBackgroundBrush}"
        Title="MainWindow" Height="150" Width="350">
    <Window.Resources>
        <sys:String x:Key="title">item:</sys:String>
        <x:Array x:Key="comboxItem" Type="sys:String">
            <sys:String>Item #1</sys:String>
            <sys:String>Item #2</sys:String>
            <sys:String>Item #3</sys:String>
        </x:Array>
        <LinearGradientBrush x:Key="WindowBackgroundBrush">
            <GradientStop Offset="0" Color="Silver" />
            <GradientStop Offset="1" Color="Gray" />
        </LinearGradientBrush>
    </Window.Resources>
    <StackPanel Margin="10">
        <TextBlock Text="{StaticResource title}" FontSize="26" />
        <ComboBox ItemsSource="{StaticResource comboxItem}" />
    </StackPanel>
</Window>

image-20220703103731665

這次我們加入了一些額外的資源,所以我們的視窗現在包含了一個簡單字串、一個字串陣列以及一個「LinearGradientBrush」。字串使用在標籤(label)上,而字串陣列作為「ComboBox」控制項的項目,漸層筆刷則用於整個視窗的背景。如你所見,幾乎任何東西都可以儲存為資源。

目前我們將資源儲存在視窗階層,也就是你可以在該視窗的任何地方存取這些資源。

如果你需要一個資源僅僅用在特定的控制項上,你可以藉由將資源加在該特定控制項內而非視窗內,來讓資源更區域性。使用方法完全一樣,唯一的差別是你現在只能在你加入資源的控制項範圍內存取該資源。

<StackPanel Margin="10">
    <StackPanel.Resources>
        <sys:String x:Key="ComboBoxTitle">Items:</sys:String>
    </StackPanel.Resources>
    <Label Content="{StaticResource ComboBoxTitle}" />
</StackPanel>

在這個例子中,我們將資源加在一個「StackPanel」中並在它的子控制項「Label」中使用該資源,而其他在「StackPanel」中的控制項也能使用該資源,就如同該子控制項的子項有權存取它一樣。反之,在該特定「StackPanel」外的控制項則無法存取。

如果你需要從多個視窗存取資源也是可行的,在App.xaml檔案中可以容納像是視窗及任何其他種類的WPF控制項資源,而當你將這些資源儲存於App.xaml,專案中的所有視窗及使用者控制項都能夠全域地存取它們,使用方法也和在視窗內儲存及使用時完全一樣。

<Application x:Class="WpfTutorialSamples.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             StartupUri="WPF application/ExtendedResourceSample.xaml">
    <Application.Resources>
        <sys:String x:Key="ComboBoxTitle">Items:</sys:String>
    </Application.Resources>
</Application>

使用時也一樣:WPF會自動地從區域控制項到視窗再到App.xaml向上尋找指定的資源。

<Label Content="{StaticResource ComboBoxTitle}" />

目前為止,我們已經直接從XAML透過標記延伸存取所有我們的資源,然而你當然也可以從後置程式碼中存取你的資源,這在很多情況下非常實用。在前面的例子中,我們看到了我們如何將資源儲存在多個不同的地方,所以在這個例子中,我們將從後置程式碼存取分別儲存在不同範圍內的三個不同的資源:

App.xaml:

<Application x:Class="IcveTools.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:IcveTools"
             xmlns:sys="clr-namespace:System;assembly=mscorlib"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <sys:String x:Key="title3">我是全局级资源</sys:String>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.DeepPurple.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Accent/MaterialDesignColor.Lime.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

視窗:

<Window x:Class="IcveTools.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:local="clr-namespace:IcveTools"
        mc:Ignorable="d"
        Title="MainWindow" Height="150" Width="350">
    <Window.Resources>
        <sys:String x:Key="title1">我是窗口级资源</sys:String>
    </Window.Resources>
    <DockPanel Name="dockPanel">
        <DockPanel.Resources>
            <sys:String x:Key="title2">我是面板级资源</sys:String>
        </DockPanel.Resources>
        <WrapPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="10" DockPanel.Dock="Top">
            <Button Name="btnClickMe" Click="btnClickMe_Click" Content="ClickMe"/>
        </WrapPanel>
        <ListBox Name="lbList" />
    </DockPanel>
</Window>

後置程式碼:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
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 IcveTools
{
    /// <summary>
    /// MainWindow.xaml 的交互逻辑
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }


        private void btnClickMe_Click(object sender, RoutedEventArgs e)
        {
            lbList.Items.Add(dockPanel.FindResource("title2").ToString());
            lbList.Items.Add(this.FindResource("title1").ToString());
            lbList.Items.Add(Application.Current.FindResource("title3").ToString());
        }
    }
}

image-20220703111930373

誠如你所見,我們儲存了三個不同的訊息:一個在App.xaml中、另一個在視窗內、而最後一個是主面板的區域資源。介面則由一個按鈕及一個「ListBox」組成。

在後置程式碼中,我們處理了按鈕的點擊事件,如同截圖所示,我們將各個文字字串加入到「ListBox」中。我們使用「FindResource()」方法,這個方法將會將資源回傳為一個物件(如果有找到),接著我們使用眾所皆知的方法「ToString()」將其轉換為字串。

請注意我們如何對不同的範圍使用「FindResource()」方法:首先對於面板(panel)、再來對於視窗、最後對於當前應用程式物件。在我們已知的位置找尋資源是很合理的,不過就如同前面提到的,如果資源沒有找到,搜尋程序將會往更高的階層找尋。所以原則上在上面三個情況下,由於在找不到的時候程序都會繼續向上對於視窗階層、接著對於應用程式階層找尋,因此我們可以都對於面板使用「FindResource()」方法。

相反的情況則不成立,搜尋將不會反向往下遍歷樹狀結構,所以如果你的資源定義在控制項或視窗,你不能從應用程式階層開始尋找資源。

如果你熟悉C#或是其他任何你可以配合WPF使用的.NET語言,那麼例外處理對你而言將不陌生:當你有一段程式碼有機會擲回例外,那麼你應該透過「try-catch」區塊包住它來優雅地處理例外。舉例來說,請思考下面的範例:

private void Button_Click(object sender, RoutedEventArgs e)
{
    string s = null;
    s.Trim();
}

顯而易見,因為我試著對一個當下是null的變數使用「Trim()」方法,這將會產生錯誤。如果你不處理這個例外,你的程式將會中止而Windows將需要去處理這個問題。如你所見,這對使用者相當不友善:

一個未處理而留給Windows處裡的例外

在這個場合,使用者將會因為如此單純而輕易就能避免的錯誤被迫中止你的應用程式。所以如果你知道某些東西可能出錯,你就應該使用「try-catch」區塊,例如:

private void Button_Click(object sender, RoutedEventArgs e)
{
    string s = null;
    try
    {
        s.Trim();
    }
    catch(Exception ex)
    {
        MessageBox.Show("A handled exception just occurred: " + ex.Message, "Exception Sample", MessageBoxButton.OK, MessageBoxImage.Warning);
    }
}

然而有時候就算是最單純的程式碼也可能擲出例外,而WPF讓你全域性地處理所有未處理的例外,取代以「try-catch」區塊包覆每一行程式碼。這樣功能透過在「Application 」類別裡的「DispatcherUnhandledException」事件來達成。如果訂閱了該事件,每當有在你本身程式碼中未處理例外遭擲出時,WPF將會呼叫訂閱的方法。以下是一個基於我們剛剛討論過東西的完整例子:

<Window x:Class="WpfTutorialSamples.WPF_Application.ExceptionHandlingSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ExceptionHandlingSample" Height="200" Width="200">
    <Grid>
        <Button HorizontalAlignment="Center" VerticalAlignment="Center" Click="Button_Click">
            Do something bad!
        </Button>
    </Grid>
</Window>
using System;
using System.Windows;

namespace WpfTutorialSamples.WPF_Application
{
    public partial class ExceptionHandlingSample : Window
    {
        public ExceptionHandlingSample()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            string s = null;
            try
            {
                s.Trim();
            }
            catch(Exception ex)
            {
                MessageBox.Show("A handled exception just occurred: " + ex.Message, "Exception Sample", MessageBoxButton.OK, MessageBoxImage.Warning);
            }
            s.Trim();
        }
    }
}

請注意這邊我在「try-catch」區塊外面額外呼叫了一次「Trim()」方法,所以第一次呼叫有被處理,而第二次則沒有。為了這個第二次呼叫,我們需要「App.xaml」的魔法。

<Application x:Class="WpfTutorialSamples.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             DispatcherUnhandledException="Application_DispatcherUnhandledException"
             StartupUri="WPF Application/ExceptionHandlingSample.xaml">
    <Application.Resources>
    </Application.Resources>
</Application>
using System;
using System.Windows;

namespace WpfTutorialSamples
{
    public partial class App : Application
    {
        private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
        {
            MessageBox.Show("An unhandled exception just occurred: " + e.Exception.Message, "Exception Sample", MessageBoxButton.OK, MessageBoxImage.Warning);
            e.Handled = true;
        }
    }
}

一個區域性例外處理一個全域性例外處理

我們處理此例外與區域性之處理方式相當接近,僅在訊息框有非常小的文字以及圖片差異。也請留意我將「e.Handled」屬性設定為「true」,這將告知WPF我們已處理完此例外且沒有其他相關事項需完成。

例外處理是應用程式非常重要的一個部分,而幸運地WPF及.NET使不論區域或全域的例外處理都變得非常容易。由於區域性例外處理允許你更精確且透過更專門的方式處理問題,你應該在合理的狀況下採用區域性例外處理,並且只使用全域性例外處理作為最後防線機制。

第三章 基础控件

TextBlock是WPF中最基本的控制项,但非常好用。它允许你展示文字在屏幕上,像是Label控制项一样,但使用起来比Label更简单、使用更少资源。一般来说,Label用于短、单行的文字(但可能包含其他,像是图片等...),而TextBlock对于多行的文字非常适合(但只能是文字形式)。Label和TextBlock都有自己独特的优点,所以怎么使用就取决于你的情况。

在"Hello,WPF!"的章节中,我们已经使用了TextBlock控制项,但是现在,我们要看看TextBlock最简单的形式:

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBlockSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBlockSample" Height="100" Width="200">
    <Grid>
        <TextBlock>This is a TextBlock</TextBlock>
    </Grid>
</Window>

一個簡單的TextBlock控制項

如果你已經閱讀了前面的章節,你會覺得這非常簡單,那這裡應該沒有什麼新東西了。在TextBlock標籤中的文字,是表示TextBlock的Text 屬性的簡短方法。

在下一個例子中,我們用更長的文字來展示TextBlock如何處理它。同時也增加一點margin讓它看起來更好一點。

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBlockSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBlockSample" Height="100" Width="200">
    <Grid>
        <TextBlock Margin="10">This is a TextBlock control and it comes with a very long text</TextBlock>
    </Grid>
</Window>

一個文字太長無法完整顯示的TextBlock控制項

如截圖所示,TextBlock非常適合用來處理長或多行文字,但預設不會進行其他動作。在這個例子中,文字太長以至於無法完整呈現在視窗中,所以WPF盡可能的將文字呈現到停止。

幸運地,有許多方法可以處理這個問題。在下一個例子中,我將告訴你所有的方法,並在之後一一說明。

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBlockSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBlockSample" Height="200" Width="250">
    <StackPanel>
        <TextBlock Margin="10" Foreground="Red">
            This is a TextBlock control<LineBreak />
            with multiple lines of text.
        </TextBlock>
        <TextBlock Margin="10" TextTrimming="CharacterEllipsis" Foreground="Green">
            This is a TextBlock control with text that may not be rendered completely, which will be indicated with an ellipsis.
        </TextBlock>
        <TextBlock Margin="10" TextWrapping="Wrap" Foreground="Blue">
            This is a TextBlock control with automatically wrapped text, using the TextWrapping property.
        </TextBlock>
    </StackPanel>
</Window>

一個有多種處理長字串方法的TextBlock控制項

我們有三個不同顏色的TextBlock(使用Foreground屬性)方便觀看,它們各以不同的方法處理文字太長的問題。

紅色的TextBlock使用了LineBreak標籤在指定的地方手動換行。這讓你可以絕對的控制在你想換行的地方換行,但在許多情況下用起來並不靈活。如果使用者放大視窗,有了足夠的空間可以完整顯示整行文字,文字依然在同樣的地方換行。

綠色的TextBlock 使用了 TextTrimming屬性並設為CharacterEllipsis,讓TextBlock在沒有足夠空間的時候顯示...。在沒有足夠空間顯示這麼多文字時,這是一種常見的方法。這也很適合用在當空間不足而且你不想使用多行的時候。你也可以使用CharacterEllipsis的另一種WordEllipsis,當空間不足的時候,以最後一個單字為單位顯示,就不會有單字只顯示一部分的問題發生。

藍色的TextBlock使用了TextWrapping 屬性並設為Wrap,讓TextBlock在沒有足夠空間顯示時自動換行。與第一個我們手動換行的TextBlock相反,它是完全自動的,甚至在TextBlock的空間變動時也會自動調整。試著在這個例子中放大縮小視窗,你將會看到在不同情況下它是如何調適的。

這是在TextBlock中,所有簡單的處理法。在下一個章節中,我們將探討一些TextBlock更高級的功能,讓我們可以創造更多樣式的TextBlock。

在前一篇文章中我们关注了TextBlock控件的核心功能:显示一个简单的字符串并在有需要的时候换行,我们甚至使用了预设以外的其他颜色来呈现文字,但如果你想要不仅仅只是对于所有TextBlock内的文字定义一个静态颜色呢?

幸运的是,TextBlock控件支持内联内容。 这些类似控件的小构造都继承自Inline类,这意味着它们可以作为较大文本的一部分进行内联呈现。 在撰写时,支持的元素包括AnchoredBlock,Bold,Hyperlink,InlineUIContainer,Italic,LineBreak,Run,Span和Underline。 在以下示例中,我们将大致了解它们。

这些可能是最简单的内联元素类型。 这些名字应该告诉你很多关于它们的作用,但我们仍然会给你一个关于如何使用它们的简单例子:

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBlockInlineSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBlockInlineSample" Height="100" Width="300">
    <Grid>
        <TextBlock Margin="10" TextWrapping="Wrap">
            TextBlock with <Bold>bold</Bold>, <Italic>italic</Italic> and <Underline>underlined</Underline> text.
        </TextBlock>
    </Grid>
</Window>

带有内联粗体、斜体及下划线元素的 TextBlock 控件

和HTML语言很相似,你只需将文字写在Bold标签内部,就可以使其粗体显示。斜体和下划线也类似。这一特性让你可以在你的应用中创建并显示风格多变的文字。

这三个标签只是Span元素的子类。他们各自设置了Span元素的一个属性,以显示所需的效果。例如,Bold标签设置了FontWeight属性,而Italic标签设置了FontStyle属性。

簡單的在文本中插入換行符即可。有關我們使用LineBreak元素的範例,請參閱之前的章節。

超链接(Hyperlink)元素允许您在文本中包含链接。它使用适合您当前Windows主题的样式进行渲染,该主题通常是带下引线的蓝色文本,并带有滑鼠的手形指标和红色悬停效果。您可以使用NavigateUri属性来定义要遵循的URL,这是一个例子:

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBlockHyperlinkSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBlockHyperlinkSample" Height="100" Width="300">
    <Grid>
        <TextBlock Margin="10" TextWrapping="Wrap">
            This text has a <Hyperlink RequestNavigate="Hyperlink_RequestNavigate" NavigateUri="https://www.google.com">link</Hyperlink> in it.
        </TextBlock>
    </Grid>
</Window>

使用Hyperlink元素的TextBlock控件可创建一个可点击的链接

超链接也可以使用 WPF 内部链接来实现 page 之间的切换。在例子中,你不需明确的 handle RequestNavigate 事件,而是启动标准 WPF 应用程式的内部链接,我们需要透过这个事件和 Process class得到一点帮助。我们注册 RequestNavigate 事件,它允许我们用一个简单的事件驱动来启动一个使用者预设浏览器的链接网址,像是下面这个程式码:

private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
{
    System.Diagnostics.Process.Start(e.Uri.AbsoluteUri);
    e.Handled = true;
}

Run元素允许你使用所有可以在Span元素中使用的属性来定义文本的样式。但相对于Span可以包含其他行内元素,Run只能包含纯文本。比较起来很显然Span元素更灵活而且在绝大多数情况上都是一个更合理的选择

Span元素本身并没有任何默认的显示效果,但允许你设置几乎所有的显示效果,包括字体大小、字体样式和粗细,以及背景和前景颜色等等。Span元素最伟大的地方在于它能包含其他行内元素在其中,使得构建更为复杂的文本以及样式非常容易。在接下来的例子当中,我给大家展示了一些Span元素能做到的事情:

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBlockSpanSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBlockSpanSample" Height="100" Width="300">
    <Grid>
        <TextBlock Margin="10" TextWrapping="Wrap">
            This <Span FontWeight="Bold">is</Span> a
            <Span Background="Silver" Foreground="Maroon">TextBlock</Span>
            with <Span TextDecorations="Underline">several</Span>
            <Span FontStyle="Italic">Span</Span> elements,
            <Span Foreground="Blue">
                using a <Bold>variety</Bold> of <Italic>styles</Italic>
            </Span>.
        </TextBlock>
    </Grid>
</Window>

一个 TextBlock 控件,使用了多种样式的 Span 元素以自定义文本格式

如您所見,如果沒有其他元素可以更符合您的需求,或者您只想要一個空白畫布來開始格式化文本,Span元素是一個很好的選擇。

如您所見,使用XAML格式化文本非常簡單,但在某些情況下,您可能更喜歡甚至需要從C#後置程式碼的檔案中執行此操作。 這有點麻煩,但這裡有一個關於如何做到這一點的例子:

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBlockCodeBehindSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBlockCodeBehindSample" Height="100" Width="300">
    <Grid></Grid>
</Window>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;

namespace WpfTutorialSamples.Basic_controls
{
    public partial class TextBlockCodeBehindSample : Window
    {
        public TextBlockCodeBehindSample()
        {
            InitializeComponent();
            TextBlock tb = new TextBlock();
            tb.TextWrapping = TextWrapping.Wrap;
            tb.Margin = new Thickness(10);
            tb.Inlines.Add("An example on ");
            tb.Inlines.Add(new Run("the TextBlock control ") { FontWeight = FontWeights.Bold });
            tb.Inlines.Add("using ");
            tb.Inlines.Add(new Run("inline ") { FontStyle = FontStyles.Italic });
            tb.Inlines.Add(new Run("text formatting ") { Foreground = Brushes.Blue });
            tb.Inlines.Add("from ");
            tb.Inlines.Add(new Run("Code-Behind") { TextDecorations = TextDecorations.Underline });
            tb.Inlines.Add(".");
            this.Content = tb;
        }
    }
}

一个带有自定义文本格式的 TextBlock 控件,通过 C# 代码而不是 XAML 生成

有這種可能性很好,在某些情況下可能必需要這樣做,但這個例子可能會讓你更加欣賞XAML。

Label控件在最简单的形式下和我们在另外一篇文章中用到的TextBlock看起来非常像。但你很快就会发现,Label使用的是Content属性而不是Text属性。这是因为Label内部可以放置任意类型的控件而不仅仅是文本。当然这个内容也可以是一个字符串,你马上会在我们的第一个基本的例子中看到这个用法。

<Window x:Class="WpfTutorialSamples.Basic_controls.LabelControlSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LabelControlSample" Height="100" Width="200">
    <Grid>
        <Label Content="This is a Label control." />
    </Grid>
</Window>

一个简单的Label控件

另外一件你可能會注意到的是,事實上,Label 在預設中會有一點 padding,讓文字距離左上角幾個 pixels。這不像 TextBlock 控制項允許你手動指定。

在這個簡單的例子中,內容只是一個string,Label實際上會在內部創建一個TextBlock並在其中顯示您的string。

那麼為什麼要使用Label呢? 好吧,Label和TextBlock之間有一些重要的區別。 TextBlock仅允许您呈现文字字串,而Label還允許您做下列的事情:

  • 設定邊界(border)
  • 渲染其他控件,例如一张图片
  • 通过ContentTemplate属性使用模板化的内容
  • 使用访问键聚焦到相关的控件上

最后一个点是使用Label取代TextBlock控件的其中一个主要原因.当你只是需要渲染简单的文本内容时,你应该使用TextBlock空间,因为它更轻量并且在大多数场景下性能比Label好.

在 Windows 和其他系統,這是常見的做法:你可以按下 Alt 然後按下你想要訪問的控制項字元,來訪問 dailog 中的控制項。當你按住Alt 時,字元會被高亮顯示。TextBlock 沒有提供這個功能,但 Label 有。所以對於控制標籤, Label 控制項常常是很好的選擇。看看實際的例子:

<Window x:Class="WpfTutorialSamples.Basic_controls.LabelControlSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LabelControlSample" Height="180" Width="250">
    <StackPanel Margin="10">
        <Label Content="_Name:" Target="{Binding ElementName=txtName}" />
        <TextBox Name="txtName" />
        <Label Content="_Mail:" Target="{Binding ElementName=txtMail}" />
        <TextBox Name="txtMail" />
    </StackPanel>
</Window>

使用访问键标记控件

按下Alt 键时会显示如截图所示示例对话框。尝试运行它,按住Alt 键,然后按N和M.您将看到两个文本框之间的焦点移动方式。

所以,这里有几个新概念。首先,我们通过在字符前放置下划线(_)来定义访问键。它不必是第一个字符,它可以在标签内容中的任何字符之前。通常的做法是使用尚未用作另一个控件的访问键的第一个字符。

我们使用Target属性来连接 Label和指定的控件。我们使用一个标准的WPF绑定,使用ElementName属性,我们将在本教程后面描述所有这些内容。绑定是基于控件的名称,因此,如果更改此名称,还必须记住更改绑定。

正如已经提到的,Label控件允许您托管其他控件,同时仍然保持其他优点。让我们尝试一个示例,其中在Label中包含图像和文本,同时为每个标签提供access key:

<Window x:Class="WpfTutorialSamples.Basic_controls.LabelControlAdvancedSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LabelControlAdvancedSample" Height="180" Width="250">
    <StackPanel Margin="10">
        <Label Target="{Binding ElementName=txtName}">
            <StackPanel Orientation="Horizontal">
                <Image Source="http://cdn1.iconfinder.com/data/icons/fatcow/16/bullet_green.png" />
                <AccessText Text="_Name:" />
            </StackPanel>
        </Label>
        <TextBox Name="txtName" />
        <Label Target="{Binding ElementName=txtMail}">
            <StackPanel Orientation="Horizontal">
                <Image Source="http://cdn1.iconfinder.com/data/icons/fatcow/16/bullet_blue.png" />
                <AccessText Text="_Mail:" />
            </StackPanel>
        </Label>
        <TextBox Name="txtMail" />
    </StackPanel>
</Window>

Label控件使用访问键和图像子控件

这只是前一个示例的扩展版本——代替简单的文本字符串,我们的Label现在将同时托管图像和文本(在AccessText控件中,它允许我们仍然使用标签的访问键)。这两个控件都在水平StackPanel中,因为Label,就像任何其他ContentControl派生控件一样,只能托管一个直接子控件。

本教程后面描述的Image控件使用远程图像——这仅用于演示目的,对于大多数实际应用程序来说并不是一个好主意。

在大多数情况下,标签控件完全按照名称所示:它充当另一控件的文本标签。这就是它的主要目的。对于大多数其他情况,您应该使用TextBlock控件或WPF提供的其他文本容器之一。

TextBox控件是WPF中最基本的文字输入控件。它允许最终用户在一行、对话输入、或多行编写,就像是个编辑器。

TextBox控件非常常用。你可以不使用任何属性,就能有一个完整并可编辑的文本字段。这里有一个简单的示例:

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBoxSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBoxSample" Height="80" Width="250">
    <StackPanel Margin="10">
        <TextBox />
    </StackPanel>
</Window>

一个简单的TextBox控件

这就是你获取一个文本字段所需要的全部了,在运行这个示例之后,以及在截屏之前,我加入了一些文本,你也可以通过标签的方式来做,通过使用Text属性去预先为文本框填充内容。

<TextBox Text="Hello, world!" />

尝试在文本框里鼠标右击。你会得到一个选项菜单,允许你和Windows剪贴板一块使用这个TextBox。默认的撤销和重做的键盘快捷方式(Ctrl + Z 和 Ctrl + Y)也应该是起作用的,并且所有这些功能你能夠不受限制的使用。

如果你运行上面的例子,你会注意到,文本框控件默认是一个单行控件。当你按下 Enter时,啥事也不会发生,并且如果你添加比一个单行文本框所能容纳的长度还要多的内容时,控件就出滚动条了。不过,使一个TextBox控件变成一个多行编辑器是非常简单的:

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBoxSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBoxSample" Height="160" Width="280">
    <Grid Margin="10">
        <TextBox AcceptsReturn="True" TextWrapping="Wrap" />
    </Grid>
</Window>

一个多行文本框控件

我添加了两个属性:AcceptsReturn使得TextBox变成一个多行控件,允许使用 回车/返回键进入到下一行, 和TextWrapping属性,当内容到达一行的尾部时,它会使文本能够自动被包裹起来。

作为额外的好处,TextBox控件实际上带有英语和其他几种语言(as of writing、英语、法语、德语和西班牙语)的自动拼写检查。

它非常类似于微软Word,其中拼写错误被划线,您可以右键单击它的建议替代品。启用拼写检查非常容易:

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBoxSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBoxSample" Height="160" Width="280">
    <Grid Margin="10">
        <TextBox AcceptsReturn="True" TextWrapping="Wrap" SpellCheck.IsEnabled="True" Language="en-US" />
    </Grid>
</Window>

启用了自动拼写检查的TextBox控件

我们使用前面的多行文本框示例作为基础,然后添加了两个新属性:SpellCheck类中名为IsEnabled的附加属性,该属性仅支持对父控件进行拼写检查,以及Language属性,该属性指示拼写检查器使用的语言。

就像Windows中的任何其他可编辑控件一样,TextBox允许选择文本,例如一次删除整个单词或将文本的一部分复制到剪贴板。WPF文本框具有用于处理选定文本的多个属性,所有这些属性都可以读取或修改。在下一个示例中,我们将读取这些属性:

<Window x:Class="WpfTutorialSamples.Basic_controls.TextBoxSelectionSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextBoxSelectionSample" Height="150" Width="300">
    <DockPanel Margin="10">
        <TextBox SelectionChanged="TextBox_SelectionChanged" DockPanel.Dock="Top" />
        <TextBox Name="txtStatus" AcceptsReturn="True" TextWrapping="Wrap" IsReadOnly="True" />

    </DockPanel>
</Window>

该示例由两个文本框控件组成:一个用于编辑,另一个用于输出当前的选择状态。为此,我们将IsReadOnly属性设置为true,以防止对状态文本框的编辑。我们在第一个文本框中订阅SelectionChanged事件,我们在后面的代码中处理:

using System;
using System.Text;
using System.Windows;
using System.Windows.Controls;

namespace WpfTutorialSamples.Basic_controls
{
    public partial class TextBoxSelectionSample : Window
    {
        public TextBoxSelectionSample()
        {
            InitializeComponent();
        }

        private void TextBox_SelectionChanged(object sender, RoutedEventArgs e)
        {
            TextBox textBox = sender as TextBox;
            txtStatus.Text = "Selection starts at character #" + textBox.SelectionStart + Environment.NewLine;
            txtStatus.Text += "Selection is " + textBox.SelectionLength + " character(s) long" + Environment.NewLine;
            txtStatus.Text += "Selected text: '" + textBox.SelectedText + "'";
        }
    }
}

具有选择状态的TextBox控件

我们使用三个相关的属性来实现:

SelectionStart,它给出了当前光标位置或是否有选择:它从什么位置开始。

SelectionLength,它给出了当前选择的长度,如果有的话。 否则它将返回0。

SelectedText,如果有选择,它会给我们当前选择的字符串。 否则返回一个空字符串。

所有这些属性都是可读的和可写的,这意味着您也可以修改它们。例如,您可以设置SelectionStart和SelectionLength属性以选择自定义文本范围,或者可以使用SelectedText属性插入和选择字符串。请记住,文本框必须具有焦点,例如首先调用Focus()方法,以便工作。

没有按钮控件的界面框架是不完整的。所以,WPF当然也有自己的按钮控件。就像WPF其它的控件一样,按钮控件的使用非常灵活,几乎可以让你实现任何东西。就让我们从以下几个简单例子开始吧:

像其他的WPF控件一样,您可以用Button标记定义按钮控件。如果你在标记定义中间加入文字,文字的内容就是按钮的文字内容。

<Button>Hello, world!</Button>

简单的按钮

很简单吧?当然,上面这个例子里的按钮并没有任何实际的逻辑在里面。不过,如果你把鼠标移上去,就能发现它周围一圈还是有一个不错的悬浮效果的。下面就让我们给它加点事件逻辑进去,订阅它的Click事件(关于事件流程的细节,您可以在订阅XAML的事件章节找到更多内容):

<Button Click="HelloWorldButton_Click">Hello, World!</Button>

在后台代码,你需要一个对应的方法处理 click 事件:

private void HelloWorldButton_Click(object sender, RoutedEventArgs e)
{
    MessageBox.Show("Hello, world!");
}

现在你有了一个非常简单的按钮,当你点击它时,一条消息会显示出来!

控件内部,Button 控件的 Content 文字内容会被转换为 TextBlock 控件,也就是说你可以按照 TextBlock 的方式控制 Button 控件文字的样式。在 Button 控件中,你会找到几个这样的特性,包括(但不限于)Foreground, Background, FontWeight 等等。换一种说法,改变 Button 控件文本格式是非常简单的:

<Button Background="Beige" Foreground="Blue" FontWeight="Bold">Formatted Button</Button>

格式化文本后的 Button

通过直接在Button上设置这些属性,您当然只能对所有内容应用相同的格式,但如果这还不够好,请继续阅读以获得更高级的内容格式。

我们已经多次讨论过这个问题,但是关于WPF的一个非常酷的事情是能够用其他WPF控件替换控件中的简单文本。 这也意味着您不必将按钮限制为简单文本,格式相同 - 您可以添加多个具有不同格式的文本控件。 WPF Button仅支持一个直接子控件,但您可以将其设置为Panel,然后将根据需要放入任意数量的控件。 您可以使用它来创建具有各种格式的按钮:

<Button>
    <StackPanel Orientation="Horizontal">
    <TextBlock>Formatted </TextBlock>
    <TextBlock Foreground="Blue" FontWeight="Bold" Margin="2,0">Button</TextBlock>
    <TextBlock Foreground="Gray" FontStyle="Italic">[Various]</TextBlock>
    </StackPanel>
</Button>

具有各种文本格式的按钮

但是,不仅仅局限于文字 - 你可以把任何你想要的东西放在你的按钮里面,接下来到一个我知道很多人会要求的主题。 带图片的按钮!

在许多UI框架中,您将找到常规Button,然后是一个或多个其他变体,它们将提供额外的功能。 最常用的变体之一是ImageButton,顾名思义,它是一个Button,它通常允许您在文本之前包含图片。 但是在WPF中,不需要单独的控件来实现这一点 - 正如您刚才看到的,我们可以在Button中放置几个控件,这样您就可以轻松地向它添加一个Image控件,如下所示:

<Button Padding="5">  
    <StackPanel Orientation="Horizontal">  
    <Image Source="/WpfTutorialSamples;component/Images/help.png" />  
    <TextBlock Margin="5,0">Help</TextBlock>  
    </StackPanel>  
</Button>

WPF的ImageButton

在WPF中创建一个ImageButton真的很简单,你当然可以自由移动,例如 如果你想在文本之后而不是之前的图片等。

您可能已经注意到WPF框架中的按钮默认情况下没有任何填充。 这意味着文本非常接近边框,这可能看起来有点奇怪,因为在其他地方找到的大多数按钮(web,其他应用程序等)确实在侧面至少有一些填充。 不用担心,因为Button带有Padding属性:

<Button Padding="5,2">Hello, World!</Button>

这将在边上填充5个像素,在顶部和底部填充2个像素。 但是必须在每一个按钮上应用padding会很麻烦,所以这里有一个技巧:可以使用样式在整个应用程序或整个应用程序中全局应用填充(后面的样式有更多)。 这个实例使用Window.Resources属性来将样式应用于Window:

<Window.Resources>
    <Style TargetType="{x:Type Button}">
        <Setter Property="Padding" Value="5,2"/>
    </Style>
</Window.Resources>

此填充现在将应用于所有按钮,但您当然可以通过在Button上专门定义Padding属性来覆盖它。 以下是此示例的所有按钮使用公共填充:

带有公共填充的多个按钮

正如您在本文中所看到的,使用WPF框架中的按钮非常简单,您可以无限制地自定义此重要控件。

CheckBox 控件允许用户启用或禁用一个选项,这通常在逻辑代码中对应一个布尔值。如果你不熟悉 CheckBox 的样式,让我们来看看这个例子:

<Window x:Class="WpfTutorialSamples.Basic_controls.CheckBoxSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CheckBoxSample" Height="140" Width="250">
    <StackPanel Margin="10">
        <Label FontWeight="Bold">Application Options</Label>
        <CheckBox>Enable feature ABC</CheckBox>
        <CheckBox IsChecked="True">Enable feature XYZ</CheckBox>
        <CheckBox>Enable feature WWW</CheckBox>
    </StackPanel>
</Window>

一个简单的 CheckBox 控件

如你所见,CheckBox 简单易用。在第二个 CheckBox 中我使用了 IsChecked 属性来让它默认就被选中。除此之外,使用 CheckBox 不需要其他的属性。如果你想检查一个 CheckBox 是否被选中,也应在逻辑代码中检查 IsChecked 属性。

CheckBox 控件继承自 ContentControl 类,这意味着它可以接受自定义内容并把它显示在旁边。如果你像我在上面的例子中一样,指定了一些文本,WPF 就会把它放入一个 TextBlock 控件并显示出来。这比手工创建 TextBlock 控件要方便。在 CheckBox 中可以使用任何控件,请见下面的例子:

<Window x:Class="WpfTutorialSamples.Basic_controls.CheckBoxSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CheckBoxSample" Height="140" Width="250">
    <StackPanel Margin="10">
        <Label FontWeight="Bold">Application Options</Label>
        <CheckBox>
            <TextBlock>
                Enable feature <Run Foreground="Green" FontWeight="Bold">ABC</Run>
            </TextBlock>
        </CheckBox>
        <CheckBox IsChecked="True">
            <WrapPanel>
                <TextBlock>
                    Enable feature <Run FontWeight="Bold">XYZ</Run>
                </TextBlock>
                <Image Source="/WpfTutorialSamples;component/Images/question.png" Width="16" Height="16" Margin="5,0" />
            </WrapPanel>
        </CheckBox>
        <CheckBox>
            <TextBlock>
                Enable feature <Run Foreground="Blue" TextDecorations="Underline" FontWeight="Bold">WWW</Run>
            </TextBlock>
        </CheckBox>
    </StackPanel>
</Window>

一个带有自定义内容的 CheckBox 控件

如你所见,你在内容中想放什么都可以。三个 CheckBox 里每一个的文本格式都不一样,我甚至在第二个里放了一个 Image 控件。如果我们用控件而非文本作为 CheckBox 的内容,我们就能随意控制样式。更酷的是,无论你点击内容的哪一部分,都可以开关 CheckBox 本身。

之前提到,CheckBox 通常对应一个布尔值,也就是说它只有两种状态——真与假,即启用与禁用。然而,布尔值也可以是空值,这就带来了第三种状态(真,假或空)。CheckBox 也支持第三种状态。如果把 IsThreeState 属性设为真,CheckBox 就会拥有第三种状态,被称作“不定态”。

IsThreeState 的一般用法是创建一个“全部启用”的 CheckBox,让它控制一系列的子 CheckBox,并显示它们作为一个整体的状态。我们下面的例子展示了如何创建几个可开关的功能,并在窗口最上面有一个“全部启用”的 CheckBox:

<Window x:Class="WpfTutorialSamples.Basic_controls.CheckBoxThreeStateSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CheckBoxThreeStateSample" Height="170" Width="300">
    <StackPanel Margin="10">
        <Label FontWeight="Bold">Application Options</Label>
        <StackPanel Margin="10,5">
            <CheckBox IsThreeState="True" Name="cbAllFeatures" Checked="cbAllFeatures_CheckedChanged" Unchecked="cbAllFeatures_CheckedChanged">Enable all</CheckBox>
            <StackPanel Margin="20,5">
                <CheckBox Name="cbFeatureAbc" Checked="cbFeature_CheckedChanged" Unchecked="cbFeature_CheckedChanged">Enable feature ABC</CheckBox>
                <CheckBox Name="cbFeatureXyz" IsChecked="True" Checked="cbFeature_CheckedChanged" Unchecked="cbFeature_CheckedChanged">Enable feature XYZ</CheckBox>
                <CheckBox Name="cbFeatureWww" Checked="cbFeature_CheckedChanged" Unchecked="cbFeature_CheckedChanged">Enable feature WWW</CheckBox>
            </StackPanel>
        </StackPanel>
    </StackPanel>
</Window>
using System;
using System.Windows;

namespace WpfTutorialSamples.Basic_controls
{
    public partial class CheckBoxThreeStateSample : Window
    {
        public CheckBoxThreeStateSample()
        {
            InitializeComponent();
        }


        private void cbAllFeatures_CheckedChanged(object sender, RoutedEventArgs e)
        {
            bool newVal = (cbAllFeatures.IsChecked == true);
            cbFeatureAbc.IsChecked = newVal;
            cbFeatureXyz.IsChecked = newVal;
            cbFeatureWww.IsChecked = newVal;
        }

        private void cbFeature_CheckedChanged(object sender, RoutedEventArgs e)
        {
            cbAllFeatures.IsChecked = null;
            if((cbFeatureAbc.IsChecked == true) && (cbFeatureXyz.IsChecked == true) && (cbFeatureWww.IsChecked == true))
                cbAllFeatures.IsChecked = true;
            if((cbFeatureAbc.IsChecked == false) && (cbFeatureXyz.IsChecked == false) && (cbFeatureWww.IsChecked == false))
                cbAllFeatures.IsChecked = false;
        }

    }
}

一个三状态的 CheckBox 控件,处于不定态一个三状态的 CheckBox 控件,处于勾选状态一个三状态的 CheckBox 控件,处于未勾选状态

这个例子有两个角度:如果你勾选或取消“全部启用”的 CheckBox,则那些代表一个个功能的子 CheckBox 也会一起被勾选或取消。反过来看也成立:单独操作子 CheckBox 也会影响“全部启用” CheckBox 的状态。如果子 CheckBox 全部被勾选或取消,“全部启用” CheckBox 就会获得对应的状态。但要是子 CheckBox 的状态不统一,“全部启用” CheckBox 的值就会为空值,令它进入不定态。

Make correction

所有这些特性都在上方的截图里,通过订阅 CheckBox 控件的 Checked 和 Unchecked 事件来实现。在现实世界中,你可能会直接绑定这些值,但这个例子是一个最基本的、利用 IsThreeState 属性来创建“全部启用”效果的实现。

RadioButton 控件允许你向用户提供一列可能的选项,而同时只允许选中一个。你可以用 Combobox 来占用更少的空间实现同样的效果,但一组单选框会令用户更直观地看到他们的可用选项。

<Window x:Class="WpfTutorialSamples.Basic_controls.RadioButtonSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="RadioButtonSample" Height="150" Width="250">
    <StackPanel Margin="10">
        <Label FontWeight="Bold">Are you ready?</Label>
        <RadioButton>Yes</RadioButton>
        <RadioButton>No</RadioButton>
        <RadioButton IsChecked="True">Maybe</RadioButton>
    </StackPanel>
</Window>

一个简单的 RadioButton 控件

我们所做的就是添加一个写有问题的 Label 和三个单选框,每一个都代表一个可选答案。我们通过最后一个 RadioButton 上的 IsChecked 属性来定义默认选项,而用户点击其他的单选框便可以更改选择。如果你想从逻辑代码检查 RadioButton 是否被选中,也应检查这个属性。

如果你试着运行上面的例子,你会发现这些 RadioButton 同时只能被选中一个,正如期望。但如果你需要多组 RadioButton,每一组都能有它们自己的选择呢?这时就要用到 GroupName 属性,允许你指定 RadioButton 的分组。下面是一个例子:

<Window x:Class="WpfTutorialSamples.Basic_controls.RadioButtonSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="RadioButtonSample" Height="230" Width="250">
    <StackPanel Margin="10">
        <Label FontWeight="Bold">Are you ready?</Label>
        <RadioButton GroupName="ready">Yes</RadioButton>
        <RadioButton GroupName="ready">No</RadioButton>
        <RadioButton GroupName="ready" IsChecked="True">Maybe</RadioButton>

        <Label FontWeight="Bold">Male or female?</Label>
        <RadioButton GroupName="sex">Male</RadioButton>
        <RadioButton GroupName="sex">Female</RadioButton>
        <RadioButton GroupName="sex" IsChecked="True">Not sure</RadioButton>
    </StackPanel>
</Window>

使用了 GroupName 属性的两组单选框

给每一个单选框设定了 GroupName 属性以后,两组单选框里都可以各有一个选择了。如果没有这个属性,六个单选框里只能有一个选择。

RadioButton 继承自 ContentControl 类,这意味着它可以接受自定义内容并把它显示在旁边。如果你像我在上面的例子中一样,指定了一些文本,WPF 就会把它放入一个 TextBlock 控件并显示出来。这比手工创建 TextBlock 控件要方便。在 RadioButton 中可以使用任何控件,请见下面的例子:

<Window x:Class="WpfTutorialSamples.Basic_controls.RadioButtonCustomContentSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="RadioButtonCustomContentSample" Height="150" Width="250">
    <StackPanel Margin="10">
        <Label FontWeight="Bold">Are you ready?</Label>
        <RadioButton>
            <WrapPanel>
                <Image Source="/WpfTutorialSamples;component/Images/accept.png" Width="16" Height="16" Margin="0,0,5,0" />
                <TextBlock Text="Yes" Foreground="Green" />
            </WrapPanel>
        </RadioButton>
        <RadioButton Margin="0,5">
            <WrapPanel>
                <Image Source="/WpfTutorialSamples;component/Images/cancel.png" Width="16" Height="16" Margin="0,0,5,0" />
                <TextBlock Text="No" Foreground="Red" />
            </WrapPanel>
        </RadioButton>
        <RadioButton IsChecked="True">
            <WrapPanel>
                <Image Source="/WpfTutorialSamples;component/Images/question.png" Width="16" Height="16" Margin="0,0,5,0" />
                <TextBlock Text="Maybe" Foreground="Gray" />
            </WrapPanel>
        </RadioButton>
    </StackPanel>
</Window>

带有自定义内容的 RadioButton

从 XAML 标记语法看来这个例子有点复杂,但概念很简单。我们为每个 RadioButton 加上一个带有图片和文本的 WrapPanel。既然我们使用 TextBlock 获得了文本控制,我们便可以随意指定文本的格式。在这个例子中,我为每个选项都加了对应的颜色。每个选项都有一个 Image 控件(后文有更多讨论)来显示图像。

你可以点击 RadioButton 的任何部分,甚至是图像和文本来选中它,因为我们将图像和文本指定为 RadioButton 的内容。如果你把这些内容放在 RadioButton 旁的一个独立的 Panel 上,用户便只能点击 RadioButton 的圆形部分来激活它,令界面变得不那么易用。

要在 WPF 里编辑一般文本的话,TextBox 就够了;但要是输入密码呢?两者的功能几乎一样,但我们不能让 WPF 把密码明文显示出来,防止有人爱管闲事在你身后偷窥。为这个目的,WPF 有 PasswordBox 控件,它和 TextBox 一样简单易用。来看看下面的例子:

<Window x:Class="WpfTutorialSamples.Basic_controls.PasswordBoxSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="PasswordBoxSample" Height="160" Width="300">
    <StackPanel Margin="10">
        <Label>Text:</Label>
        <TextBox />
        <Label>Password:</Label>
        <PasswordBox />
    </StackPanel>
</Window>

 title=

在截图中,两个文本框的内容完全一样,但在密码框里字符都显示为小点。实际上,如果你想指定显示什么字符来替代实际内容,可以使用 PasswordChar 属性:

<PasswordBox PasswordChar="X" />

这么一来,密码字符就会显示为 "X",而不是小点了。如果你想控制密码的最大长度,可以使用 MaxLength 属性:

<PasswordBox MaxLength="6" />

在下面这个例子里,我同时用到了上面两个属性:

<Window x:Class="WpfTutorialSamples.Basic_controls.PasswordBoxSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="PasswordBoxSample" Height="160" Width="300">
    <StackPanel Margin="10">
        <Label>Text:</Label>
        <TextBox />
        <Label>Password:</Label>
        <PasswordBox MaxLength="6" PasswordChar="X" />
    </StackPanel>
</Window>

一个简单的 PasswordBox 控件,并且额外设定了几个属性

你可以看到,字符现在都显示为 X,而且我最多只能在文本框里输入 6 个字符了。

当你需要从 PasswordBox 获得密码,你可以在逻辑代码里使用 Password 属性。然而,为了安全,Password 属性没有作为依赖属性实现,也就是说你不能绑定到它。

这不一定会影响到你——就像上文所说,你仍旧可以在逻辑代码里读取密码。但如果你想实现 MVVM 或者你就是喜欢数据绑定,这里有另一个解决方案。你可以在这里读到更详细的内容: http://blog.functionalfun.net/2008/06/wpf-passwordbox-and-data-binding.html

WPF的Image控件允许您在应用程序内显示图片。 它是一个非常通用的控件,有许多有用的选项和方法,正如您将在本文中学到的。 但首先,让我们看一下在窗口中包含图片的最基本示例:

<Image Source="https://upload.wikimedia.org/wikipedia/commons/3/30/Googlelogo.png" />

结果将如下所示:

使用远程URL源的图片控件

我们在此示例中用于指定应显示图片的Source属性可能是此控件的最重要属性,因此我们首先深入研究该主题。

从我们的第一个例子中可以看出,Source属性可以很容易地指定在Image控件中应该显示哪个图片 - 在这个特定的例子中,我们使用了一个远程图片,Image控件会自动获取和显示它。 这是Image控件功能多样化的一个很好的例子,但在很多情况下,您可能希望将图片与应用程序捆绑在一起,而不是从远程源加载它。 这可以很容易地完成!

您可能知道,您可以将资源文件添加到项目中 - 它们可以存在于您当前的Visual Studio项目中,并且可以像解决任何其他WPF相关文件(窗口,用户控件等)一样在解决方案资源管理器中查看。 资源文件的相关示例是一个图片,您只需将其复制到项目的相关文件夹中即可将其包含在内。 然后它将被编译到您的应用程序中(除非您特别要求VS不要这样做),然后可以使用URL格式访问资源。 因此,如果您在名为“Images”的文件夹中有一个名为“google.png”的图片,则语法可能如下所示:

<Image Source="/WpfTutorialSamples;component/Images/google.png" />

这些URI通常被称为“Pack URI's”,是一个含有更多细节的重要主题,但是现在,请注意它基本上由两部分组成:

  • 第一部分(/WpfTutorialSamples;component),其中程序集名称(我的应用程序中的WpfTutorialSamples)与单词“component”组合
  • 第二部分,指定资源的相对路径:/Images/google.png

使用此语法,您可以轻松引用应用程序中包含的资源。 为简化起见,WPF框架也接受简单的相对URL - 这在大多数情况下都是足够的,除非你在应用程序中做了一些更复杂的资源。 使用简单的相对URL,它看起来像这样:

<Image Source="/Images/google.png" />

直接在XAML中指定图片源可以解决很多情况,但有时您需要动态加载图片,例如 基于用户选择。 这可以从后置代码做到。 以下是根据从OpenFileDialog中选择的方式加载用户计算机上的图片的方法:

private void BtnLoadFromFile_Click(object sender, RoutedEventArgs e)
{
    OpenFileDialog openFileDialog = new OpenFileDialog();
    if(openFileDialog.ShowDialog() == true)
    {
    Uri fileUri = new Uri(openFileDialog.FileName);
    imgDynamic.Source = new BitmapImage(fileUri);
    }
}

请注意我是如何根据对话框中选定的路径创建一个传递Uri对象的BitmapImage实例。 我们可以使用完全相同的技术将应用程序中包含的图片作为资源加载:

private void BtnLoadFromResource_Click(object sender, RoutedEventArgs e)
{
    Uri resourceUri = new Uri("/Images/white_bengal_tiger.jpg", UriKind.Relative);
    imgDynamic.Source = new BitmapImage(resourceUri);        
}

我们使用与前面一个示例中使用的相同的相对路径 - 只需确保在创建Uri实例时传入UriKind.Relative值,因此它知道提供的路径不是绝对路径。 以下是我们的后置代码示例的XAML源代码以及屏幕截图:

<Window x:Class="WpfTutorialSamples.Basic_controls.ImageControlCodeBehindSample"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfTutorialSamples.Basic_controls"
    mc:Ignorable="d"
    Title="ImageControlCodeBehindSample" Height="300" Width="400">
    <StackPanel>
    <WrapPanel Margin="10" HorizontalAlignment="Center">
        <Button Name="btnLoadFromFile" Margin="0,0,20,0" Click="BtnLoadFromFile_Click">Load from File...</Button>
        <Button Name="btnLoadFromResource" Click="BtnLoadFromResource_Click">Load from Resource</Button>
    </WrapPanel>
    <Image Name="imgDynamic" Margin="10"  />
    </StackPanel>
</Window>

从后置代码加载图片的图片示例

在Source属性之后,显而易见这很重要,我认为Image控件的第二个最有趣的属性可能是Stretch属性。 它控制当加载的图片尺寸与Image控件的尺寸不完全匹配时怎么处理。 这将经常发生,因为窗口的大小可以由用户控制,除非您的布局非常静态,这意味着Image控件的大小也会改变。

从下一个示例中可以看出,Stretch属性可以使图片的显示方式有很大差异:

使用Image控件的Stretch属性

<Window x:Class="WpfTutorialSamples.Basic_controls.ImageControlStretchSample"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfTutorialSamples.Basic_controls"
    mc:Ignorable="d"
    Title="ImageControlStretchSample" Height="450" Width="600">
    <Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Label Grid.Column="0" HorizontalAlignment="Center" FontWeight="Bold">Uniform</Label>
    <Label Grid.Column="1" HorizontalAlignment="Center" FontWeight="Bold">UniformToFill</Label>
    <Label Grid.Column="2" HorizontalAlignment="Center" FontWeight="Bold">Fill</Label>
    <Label Grid.Column="3" HorizontalAlignment="Center" FontWeight="Bold">None</Label>
    <Image Source="/Images/white_bengal_tiger.jpg" Stretch="Uniform" Grid.Column="0" Grid.Row="1" Margin="5" />
    <Image Source="/Images/white_bengal_tiger.jpg" Stretch="UniformToFill" Grid.Column="1" Grid.Row="1" Margin="5" />
    <Image Source="/Images/white_bengal_tiger.jpg" Stretch="Fill" Grid.Column="2" Grid.Row="1" Margin="5" />
    <Image Source="/Images/white_bengal_tiger.jpg" Stretch="None" Grid.Column="3" Grid.Row="1" Margin="5" />
    </Grid>
</Window>

它可能有点难以辨别,但所有四个Image控件都显示相同的图片,但Stretch属性的值不同。 以下是各种模式的工作原理:

  • Uniform: 这是默认模式。 图片将自动缩放,以便它适合图片区域。 将保留图片的宽高比)。
  • UniformToFill: 图片将被缩放,以便完全填充图片区域。 将保留图片的宽高比。
  • Fill: 图片将缩放以适合图片控件的区域。 可能无法保留宽高比,因为图片的高度和宽度是独立缩放的。
  • None: 如果图片小于图片控件,则不执行任何操作。 如果它比图片控件大,则会裁剪图片以适合图片控件,这意味着只有部分图片可见。

WPF的Image控件使您可以轻松地在应用程序中显示图片,无论是来自远程源,嵌入式资源还是本地计算机,如本文所示。

第四章 控件概念

工具提示,信息提示或提示 - 各种名称,但概念保持不变:能够通过将鼠标悬停在它上面,获得有关特定控件或链接的额外信息。WPF显然也支持这个概念,使用FrameworkElement类中的ToolTip属性,几乎所有WPF控件都继承自该属性。

为控件指定工具提示非常简单,您将在第一个非常基本的示例中看到:

<Window x:Class="WpfTutorialSamples.Control_concepts.ToolTipsSimpleSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ToolTipsSimpleSample" Height="150" Width="400">
    <Grid VerticalAlignment="Center" HorizontalAlignment="Center">

        <Button ToolTip="Click here and something will happen!">Click here!</Button>

    </Grid>
</Window>

一个简单的ToolTip示例

正如您在屏幕截图中看到的那样,一旦鼠标悬停在按钮上,就会产生带有指定字符串的浮动框。这就是大多数UI框架提供的 - 显示文本字符串,仅此而已。

但是,在WPF中,ToolTip属性实际上不是字符串类型,而是一个物件類型,这意味着我们可以在那里放任何我们想要的东西。这开启了一些非常酷的可能性,我们可以为用户提供更丰富,更有用的工具提示。例如,考虑这个例子并将其与第一个例子进行比较

<Window x:Class="WpfTutorialSamples.Control_concepts.ToolTipsAdvancedSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ToolTipsAdvancedSample" Height="200" Width="400" UseLayoutRounding="True">
    <DockPanel>
        <ToolBar DockPanel.Dock="Top">
            <Button ToolTip="Create a new file">
                <Button.Content>
                    <Image Source="/WpfTutorialSamples;component/Images/page_white.png" Width="16" Height="16" />
                </Button.Content>
            </Button>
            <Button>
                <Button.Content>
                    <Image Source="/WpfTutorialSamples;component/Images/folder.png" Width="16" Height="16" />
                </Button.Content>
                <Button.ToolTip>
                    <StackPanel>
                        <TextBlock FontWeight="Bold" FontSize="14" Margin="0,0,0,5">Open file</TextBlock>
                        <TextBlock>
                        Search your computer or local network
                        <LineBreak />
                        for a file and open it for editing.
                        </TextBlock>
                        <Border BorderBrush="Silver" BorderThickness="0,1,0,0" Margin="0,8" />
                        <WrapPanel>
                            <Image Source="/WpfTutorialSamples;component/Images/help.png" Margin="0,0,5,0" />
                            <TextBlock FontStyle="Italic">Press F1 for more help</TextBlock>
                        </WrapPanel>
                    </StackPanel>
                </Button.ToolTip>
            </Button>
        </ToolBar>

        <TextBox>
            Editor area...
        </TextBox>
    </DockPanel>
</Window>

一个更高级的ToolTip示例

请注意,此示例中为第一个按钮使用了简单的字符串提示工具,然后为第二个按钮使用了更高级的提示工具。在高级案例中,我们使用面板作为根控件,然后我们可以随意添加控件。结果很酷,有标题,描述文字和提示您可以按F1获取更多帮助,包括帮助图标。

ToolTipService类有一堆有趣的属性会影响提示工具的行为。你可以直接在控件上设置它们有工具提示,例如像这里,我们使用 ShowDuration 属性扩展工具提示的时间(我们将其设置为5.000毫秒或5秒):

<Button ToolTip="Create a new file" ToolTipService.ShowDuration="5000" Content="Open" />

你还可以使用 HasDropShadow 属性控制弹出窗口是否应该有阴影,或者使用 ShowOnDisabled 属性,决定是否应该为已禁用的控件显示提示工具。还有其他一些有趣的属性,想了解完整清单,请参阅文档: http://msdn.microsoft.com/en-us/library/system.windows.controls.tooltipservice.aspx

提示工具对用户来说是一个很好的帮助,在WPF中,它们既易于使用又非常灵活。结合您可以完全控制提示工具的设计和内容,以及来自 ToolTipService 类的属性的事实,可以在您的工具提示中创建更加用户友好的内联帮助应用。

在这篇文章中,我们将会讨论为什么有时候 WPF 的文本渲染模糊不清,如何修复这个问题以及如何自己控制文本渲染。

此教程中已经提到,相比其他 UI 框架,例如 WinForms 大量使用 Windows API,WPF 自己做了很多额外的事情。渲染文本的时候这个对比也很明显——WinForms 使用 Windows 的 GDI API,而 WPF 有它自己的文本渲染实现。这样就能使 WPF 更好地支持动画,也使得它不受设备依赖。

不幸的是,这使得 WPF 渲染的文本变得有些模糊,特别是在小号字体上。这曾经是令 WPF 程序员很头疼的一个问题,但幸运的是 Microsoft 在 .NET 框架 4.0 中为 WPF 文本渲染引擎做了很多优化。这意味着如果你使用 4.0 或更高的版本,你的文本看起来应该完美无瑕。

在 .NET 框架 4.0 中,Microsoft 引入了 TextOptions 类和 TextFormattingModeTextRenderingMode 属性来给予程序员更多的文本渲染控制。这让你可以在控制层面指定文本格式化与渲染的方式。用一个例子来演示可能更为直观,所以看看下面的代码和截图,了解一下你如何能用这些属性影响文本渲染。

使用 TextFormattingMode 属性,你便可以决定文本格式化所用的算法。你可以选择用 Ideal(默认值)或 Display。一般你不需要改动这个属性,因为 Ideal 设置在大多数情况下都是最好的。但如果你需要渲染非常小的文本,Display 设置有时候更好。下面是一个例子,你可以看出两者的微小差距:

<Window x:Class="WpfTutorialSamples.Control_concepts.TextFormattingModeSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextFormattingModeSample" Height="200" Width="400">
    <StackPanel Margin="10">
        <Label TextOptions.TextFormattingMode="Ideal" FontSize="9">TextFormattingMode.Ideal, small text</Label>
        <Label TextOptions.TextFormattingMode="Display" FontSize="9">TextFormattingMode.Display, small text</Label>
        <Label TextOptions.TextFormattingMode="Ideal" FontSize="20">TextFormattingMode.Ideal, large text</Label>
        <Label TextOptions.TextFormattingMode="Display" FontSize="20">TextFormattingMode.Display, large text</Label>
    </StackPanel>
</Window>

使用 TextFormattingMode 属性

TextRenderingMode 属性可以让你控制显示文本所用的抗锯齿算法。当 TextFormattingMode 属性被设为 Display 时,它的效果最明显。请看下面的例子:

<Window x:Class="WpfTutorialSamples.Control_concepts.TextRenderingModeSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextRenderingModeSample" Height="300" Width="400">
    <StackPanel Margin="10" TextOptions.TextFormattingMode="Display">
        <Label TextOptions.TextRenderingMode="Auto" FontSize="9">TextRenderingMode.Auto, small text</Label>
        <Label TextOptions.TextRenderingMode="Aliased" FontSize="9">TextRenderingMode.Aliased, small text</Label>
        <Label TextOptions.TextRenderingMode="ClearType" FontSize="9">TextRenderingMode.ClearType, small text</Label>
        <Label TextOptions.TextRenderingMode="Grayscale" FontSize="9">TextRenderingMode.Grayscale, small text</Label>
        <Label TextOptions.TextRenderingMode="Auto" FontSize="18">TextRenderingMode.Auto, large text</Label>
        <Label TextOptions.TextRenderingMode="Aliased" FontSize="18">TextRenderingMode.Aliased, large text</Label>
        <Label TextOptions.TextRenderingMode="ClearType" FontSize="18">TextRenderingMode.ClearType, large text</Label>
        <Label TextOptions.TextRenderingMode="Grayscale" FontSize="18">TextRenderingMode.Grayscale, large text</Label>
    </StackPanel>
</Window>

使用 TextRenderingMode 属性

如你所见,显示的文本效果差别很大。和上文一样,只有在特殊情况下才需要修改这个属性。

如果您为了学习编程而使用了计算机足够长时间,您也许知道可以使用键盘上的Tab按键以在窗口/对话框间切换。这使得您可以在填写表格或执行其他类似事情时双手不需要离开键盘,而不是使用鼠标选择下一个控件。

WPF直接支持这种行为,甚至更好:它将自动建立从一个字段移动到另一个字段时使用的顺序,因此通常,您根本不必担心这一点。 但是,有时窗口/对话框的设计会导致WPF使用您可能不同意的Tab键顺序,原因有多种。 此外,您可以决定某些控件不应该是Tab键顺序的一部分。 请允许我用一个例子来说明这一点:

Tab顺序示例对话框

此对话框包含一个从中间分隔的Grid控件,两边各有一个包含有Label控件和TextBox控件的StackPanel控件。默认的Tab键顺序从该窗口的第一个控件开始,然后依次为其内部的子控件,最后跳转至下一个控件。由于对话框包含有纵向的StackPanel控件,意味着顺序是从First name部分开始,然后是Street name部分,然后是City部分,最后才是第二个包含有Last nameZip code部分的StackPanel控件。当Tab键离开第二个StackPanel后,才是两个按钮。

但是,对于此对话框,这不是我想要的行为。 相反,我想使用Tab从First nameLast name(所以基本上是水平移动而不是垂直移动),最重要的是,我不希望使用Tab进入City字段,因为将自动填写在这个假设的对话框中的Zip code上,因此已经只读了。 为了完成所有这些,我将使用两个属性:TabIndexIsTabStop。 TabIndex用于定义顺序,而IsTabStop属性将强制WPF在这个窗口按Tab时跳过这个控件。 这是用于创建对话框的标记:

<Window x:Class="WpfTutorialSamples.Control_concepts.TabOrderSample"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfTutorialSamples.Control_concepts"
    mc:Ignorable="d"
    Title="TabOrderSample" Height="250" Width="400">
    <Grid Margin="20">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="20" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <StackPanel>
        <Label>First name:</Label>
        <TextBox TabIndex="0" />
        <Label>Street name:</Label>
        <TextBox TabIndex="2" />
        <Label>City:</Label>
        <TextBox TabIndex="5" IsReadOnly="True" IsTabStop="False" Background="Gainsboro" />
    </StackPanel>
    <StackPanel Grid.Column="2">
        <Label>Last name:</Label>
        <TextBox TabIndex="1" />
        <Label>Zip Code:</Label>
        <TextBox TabIndex="4" />
    </StackPanel>
    <Button Grid.Row="1" HorizontalAlignment="Right" Width="80">Add</Button>
    <Button Grid.Row="1" Grid.Column="2" HorizontalAlignment="Left" Width="80">Cancel</Button>
    </Grid>
</Window>

注意我如何简单地为每个相关控件的TabIndex属性提供一个数字,然后指定City的TextBox的IsTabStop属性 - 在对话框中控制Tab键顺序就这么简单!

控制对话框的Tab键顺序非常重要,但幸运的是,WPF可以很好地为您自动定义正确的Tab键顺序。 但是,在某些情况下,使用TabIndexIsTabStop属性进行控制是有意义的,如上例所示。

访问键的概念(有时称为加速键键盘加速器)允许您通过按住Alt键然后按键盘上的其他键来到达窗口内的特定控件。 这增强了窗口的可用性,因为它允许用户使用键盘导航窗口,而不必使用鼠标。

为WPF控件定义访问键非常简单,但该方法可能会让您感到惊讶。 通常,会有一个属性,但不适用于访问键。 相反,您可以通过在控件的Text / Content属性中使用添加下划线前缀来定义访问键。 例如,像这样:

<Button Content="_New"></Button>

注意N字符前面的下划线(_) - 这会将N键转换为此Button控件的指定访问键。 默认情况下,控件的外观不会改变,正如您在此示例中所示,我已为所有按钮定义了访问键:

默认情况下,访问键不可见

但是,只要按下键盘上的Alt键,可用的下划线就会突出显示可用的访问键:

访问键可见

按住Alt键的同时,您现在可以按其中一个访问键(例如N,O或S)来激活特定按钮。 它会像用鼠标点击一样反应。

访问键适用于对话框/窗口中的单个元素,但它们在传统的Windows菜单中更有用,您通常需要在到达所需的菜单项之前单击菜单项层次结构。 以下是Visual Studio的示例:

Visual Studio中的访问键

在这种情况下,当我想要启动一个新项目时,我可以按住Alt键,然后按F(对于文件),然后按N(对于),而不必通过几次鼠标移动和单击来浏览菜单。 然后是P(对于项目)。 当然,这也可以通过常规键盘快捷键(Ctrl + Shift + N)完成,但是在您到达菜单层次结构的最后一级之前,该快捷方式是不可见的,因此除非您已将其记忆,否则可能更容易 使用访问键,因为只要按下Alt键,它们就会以视觉方式突出显示。

您可能想要使用控制文本/内容中找到的任何字符,但实际上有选择正确字符的准则。 最重要的规则当然是选择一个未被其他控件使用的字符,但除此之外,您应该使用以下指南:

  • 使用第一个单词第一个字符
  • 如果不可能,请使用第二个或第三个单词的第一个字符(例如,Save As为中的A
  • 如果那是不可能的,请使用第一个单词的第二个字符(例如Open中的P
  • 如果那是不可能的,请使用第二个或第三个单词的第二个字符(例如,在Save All中的l
  • 一般来说,你可能想要避免像il这样的窄字符,并选择更宽的字符,如m, s, w等。

在我们到目前为止看到的示例中,我们已经能够直接在我们想要访问的控件上定义Access Key。 但至少有一个例子,这是不可能直接实现的:当你有一个输入控件,例如 一个TextBox,指示其用途的文本在实际的TextBox控件中不存在。 相反,您通常会使用第二个控件来指示TextBox控件的用途。 这通常是Label控件。

因此,在此示例中,Label控件将保存描述性文本,因此也包含Access Key,但您要注意的控件将是TextBox控件。 没问题 - 我们可以使用Label的Target属性将它与TextBox(或任何其他控件)绑定在一起,如下所示:

<StackPanel Margin="20">
    <Label Content="_First name:" Target="{Binding ElementName=txtFirstName}" />
    <TextBox Name="txtFirstName" />
    <Label Content="_Last name:" Target="{Binding ElementName=txtLastName}" />
    <TextBox Name="txtLastName" />
    <Button Content="_Save" Margin="20"></Button>
</StackPanel>

注意如何为Label控件指定Access Key,然后使用Target属性绑定到相关的TextBox控件,我们使用基于ElementNameBinding来执行实际工作。 现在我们可以使用Alt + F和Alt + L访问两个TextBox控件,使用Alt + S访问Button。 以下是它的外观:

标签使用访问键与TextBox控件绑定在一起

通过在窗口/对话框中使用快捷鍵,您可以很輕鬆的使用键盤進行導航。 这在高级用户中尤其受欢迎,他们会盡可能使用键盤来支持鼠标。 您应该使用快捷鍵,尤其在您的菜单。

第五章 面板

面板是WPF裡其中一個很重要的控件。面板扮演著裝載其他控件的容器的角色,同時也控制著頁面和視窗的佈局。由於一個視窗只允許一個子控件,因此面板經常會被使用於分隔空間,這樣每個空間就會有一個控件或者面板。

面板有多种不同样式,每种样式都有他各自的布局和控件处理方式。因此,要实现你需要的功能和布局,必须要选择正确的面板样式,这对初学WPF编程者可能会有些困难。下一节将会对每种面板进行简要的介绍,以便提供应用面板的基本概念。接下来,再对每种面板进行详细的了解。

序号面板名称描述
1Canvas这是一种简单的面板,与WinForms应用处理方式类似。该面板可以设置每个子控件的坐标,容许完全的布局控制。但是该面板不够弹性,因为你必须手动移动子控件以保证他们按照你需要的位置和方式排列。推荐在你想要完全自己布置子控件时选用。
2WrapPanelWrapPanel 面板将每个子控件按照水平(默认方式)或者竖直的方式满布一行或一列,让后再布置下一行或者下一列。当你需要水平或者竖直排列子控件且能自动滚动进入下一行(列)时采用他。
3StackPanelStackPanel 的行为与 WrapPanel 很相似,但与 WrapPanel 會包裝過長的子控制項行為不同,它會盡量延長自己。与WrapPanel类似,它的方向可以是水平或垂直,但每个项目拉伸占满全宽或全高,而不是基于最大的项目调整宽度或高度。当你想要一连串控制项尽可能填满空间而不是被包裝,请使用 StackPanel 。
4DockPanelDockPanel允许您将子控件停靠在顶部、底部、左侧或右侧。默认情况下,如果没有给定特定的dock位置,最后一个控件将填充剩余的空间。您可以使用Grid面板实现相同的操作,但是对于更简单的情况,DokPanel将更易于使用。每当需要将一个或多个控件停靠到一个侧边时,使用DockPanel,比如将窗口划分为特定区域。
5GridGrid可能是面板类型中最复杂的。Grid可以包含多行和多个列。您为每行定义一个高度,为每列定义一个宽度,以像素的绝对数量、可用空间的百分比或自动方式,其中行或列将根据内容自动调整其大小。当其他面板不适合使用时, 使用Grid,例如,当您需要多个列并且经常与其他面板组合时。
6UniformGridUniformGrid就像Grid一样,具有多行和多列的可能性,但有一个重要的区别:所有行和列将具有相同的大小!当您需要网格行为而不需要为行和列指定不同的大小时,使用此方法。

Canvas(画布)可能是所有面板中最简单的。实际上,它不会默认做任何事,而是允许你把控件放在它的内部,然后显式地给这些控件定位。

如果您曾经使用过像WinForms这样的其他UI库,这可能会让您感觉很自在,但是尽管对所有的子控件都具有绝对的控制权是很诱人的,但这也意味着一旦用户开始调整窗口大小,Panel就不会为您做任何事情,比如绝对定位的文本或内容缩放。

更多的内容将在后面解释,我们现在看一个简单的示例。这个示例主要演示了,Canvas默认只会做很少的工作。

<Window x:Class="WpfTutorialSamples.Panels.Canvas"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Canvas" Height="200" Width="200">
    <Canvas>
        <Button>Button 1</Button>
        <Button>Button 2</Button>
    </Canvas>
</Window>

一个简单的画布

你可以看到,尽管我们有两个按钮,但它们都被放在完全相同的位置,因此只有后一个是可见的。Canvas完全不做任何事情,除非你给出了子控件的坐标。这可以使用Canvas控件中的Left,Right,Top和Bottom(左,右,顶和底)的关联属性完成。

这些属性允许你指定与Canvas的四条边相关的位置。默认情况下,它们被设置为NaN(Not a Number,非数字),这会让Canvas将它们放在左上角,但是如前所述,你可以简单地修改:

<Window x:Class="WpfTutorialSamples.Panels.Canvas"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Canvas" Height="200" Width="200">
    <Canvas>
        <Button Canvas.Left="10">Top left</Button>
        <Button Canvas.Right="10">Top right</Button>
        <Button Canvas.Left="10" Canvas.Bottom="10">Bottom left</Button>
        <Button Canvas.Right="10" Canvas.Bottom="10">Bottom right</Button>
    </Canvas>
</Window>

一个简单的Canvas,我们定位子元素

请注意,我只设置我需要的属性。对于前两个按钮,我只希望指定X轴的值,因此我指定左和右属性的值,将按钮从相应的方向推向中心。

对于底部的两个按钮,我使用左/右和底部两个属性将它们从对应的方向推向中心。 您通常要指定“上”或“下”值和/或“左”或“右”值。

如上所述,由于Canvas可以让您完全控制控件位置,因此不会真正关心是否有足够的空间容纳所有控件或者是否有一个位于另一个控件之上。这种行为使得Canvas对于几乎任何类型的对话框设计来说都是一个糟糕的选择。但正如其名称所暗示的那样,Canvas (画布) 至少对于一件事情非常有用:绘画。WPF有一堆可以放在Canvas中的控件,使得Canvas可以做出漂亮的插图。

在下一个示例中,我们将使用WPF的几个形状相关控件来说明使用画布时另一个重要的概念:Z轴。通常,如果画布中的两个控件重叠,则标记中最后定义的控件将优先并覆盖其他控件。但是,通过在面板类上使用附加的Z坐标属性,可以轻松更改此行为。

首先,我们根本不使用z轴的示例:

<Window x:Class="WpfTutorialSamples.Panels.CanvasZIndex"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CanvasZIndex" Height="275" Width="260">
    <Canvas>
        <Ellipse Fill="Gainsboro" Canvas.Left="25" Canvas.Top="25" Width="200" Height="200" />
        <Rectangle Fill="LightBlue" Canvas.Left="25" Canvas.Top="25" Width="50" Height="50" />
        <Rectangle Fill="LightCoral" Canvas.Left="50" Canvas.Top="50" Width="50" Height="50" />
        <Rectangle Fill="LightCyan" Canvas.Left="75" Canvas.Top="75" Width="50" Height="50" />
    </Canvas>
</Window>

具有重叠元素的Canvas,不使用ZIndex属性

可以看到,因为每个矩形都是在圆之后定义的,所以它们都与圆重叠,并且每个矩形都覆盖在先前定义的圆上。 我们试着改变一下:

<Window x:Class="WpfTutorialSamples.Panels.CanvasZIndex"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="CanvasZIndex" Height="275" Width="260">
    <Canvas>
        <Ellipse Panel.ZIndex="2" Fill="Gainsboro" Canvas.Left="25" Canvas.Top="25" Width="200" Height="200" />
        <Rectangle Panel.ZIndex="3" Fill="LightBlue" Canvas.Left="25" Canvas.Top="25" Width="50" Height="50" />
        <Rectangle Panel.ZIndex="2" Fill="LightCoral" Canvas.Left="50" Canvas.Top="50" Width="50" Height="50" />
        <Rectangle Panel.ZIndex="4" Fill="LightCyan" Canvas.Left="75" Canvas.Top="75" Width="50" Height="50" />
    </Canvas>
</Window>

具有重叠元素的Canvas,使用ZIndex属性

默认的Z轴值为0,但我们为每个形状分配一个新值。规则是具有较高z坐标的元素会覆盖具有较低值的元素。 如果两个值相同,则最后定义的元素“胜”。从截图中可以看出,更改Z坐标属性改变了图形的外观。

WrapPanel将把每个子控件定位在另一个子控件旁边,水平地(默认)或垂直地,直到没有更多的空间为止,在那里它将包装到下一行,然后继续。当您需要一个垂直或水平列表控件时,当没有更多空间时自动使用它。

当WrapPanel使用水平方向时,基于最高的项,子控件将被赋予相同的高度。当WrapPanel是垂直方向时,基于最宽的项,子控件将被赋予相同的宽度。

在第一个示例中,我们将验证一个具有默认(水平)方向的WrapPanel:

<Window x:Class="WpfTutorialSamples.Panels.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>Test button 1</Button>
        <Button>Test button 2</Button>
        <Button>Test button 3</Button>
        <Button Height="40">Test button 4</Button>
        <Button>Test button 5</Button>
        <Button>Test button 6</Button>
    </WrapPanel>
</Window>

WrapPanel处于水平模式

注意这里是如何在第二行中的一个按钮上设置特定的高度的。在得到的屏幕截图中,你能看到,这会导致整行按钮具有相同的高度,而不是像第一行中所看到的那样具有所需的高度。你也会注意到,面板完全按照名称所暗示的那样进行:当内容放不下去时,它会换行[wrap: (使文字)换行]。在这种情况下,第四个按钮不能匹配第一行,因此它自动到下一行。

如果你操作窗口,使可用空间更小,你会看到面板如何立即调整它:

WrapPanel处于水平模式

将“方向”设置为“垂直”时,所有这些行为也都是如此。这是与之前完全相同的示例,但使用垂直WrapPanel:

<Window x:Class="WpfTutorialSamples.Panels.WrapPanel"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WrapPanel" Height="120" Width="300">
    <WrapPanel Orientation="Vertical">
        <Button>Test button 1</Button>
        <Button>Test button 2</Button>
        <Button>Test button 3</Button>
        <Button Width="140">Test button 4</Button>
        <Button>Test button 5</Button>
        <Button>Test button 6</Button>
    </WrapPanel>
</Window>

WrapPanel处于垂直模式

您可以看到按钮在换行之前是垂直排列而不是水平排列,直到它们到达窗口的底部。 在这个例子中,我为第四个按钮提供了更宽的宽度,您将看到同一列中的按钮也获得相同的宽度,就像我们在水平示例中看到的按钮高度一样。

请注意,虽然水平WrapPanel将匹配同一行中的高度,而垂直WrapPanel将匹配同一列中的宽度,但垂直WrapPanel中的高度不匹配,并且水平WrapPanel中的宽度不匹配。 看一下这个例子,它是垂直WrapPanel,但第四个按钮自定义了宽度和高度:

<Button Width="140" Height="44">Test button 4</Button>

它看起来像这样:

WrapPanel处于垂直模式,具有特定的宽度/高度

注意按钮5仅使用了宽度 - 它不关心高度,尽管它会导致第六个按钮被推到一个新列。

StackPanel與WrapPanel是非常相似的,但是至少有個重要的不同點: StackPanel不會對內容換行。只會對一個方向做延伸,讓你可以對物件做彼此的堆疊。讓我們先從簡單的例子試試,就像我們做WrapPanel一樣:

<Window x:Class="WpfTutorialSamples.Panels.StackPanel"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="StackPanel" Height="160" Width="300">
    <StackPanel>
        <Button>Button 1</Button>
        <Button>Button 2</Button>
        <Button>Button 3</Button>
        <Button>Button 4</Button>
        <Button>Button 5</Button>
        <Button>Button 6</Button>
    </StackPanel>
</Window>

垂直模式的简单StackPanel

首先你會注意到StackPanel是如何地不在意內容是否有足夠的空間。無論如何不會對內容換行並且不會自動地提供捲軸的功能 ( 你可使用捲軸顯示控制來實現捲軸的功能 -- 後面的章節會有介紹 )。

你或許也注意到StackPanel初始的排列是垂直方向,不像WrapPanel始的排列是水平方向。但是就像WrapPanel一樣,可以容易地針對排列方向性質做修改:

<StackPanel Orientation="Horizontal">

水平模式的简单StackPanel

另一件事或許你也注意到 StackPanel 默認地針對其子控制做延展。垂直排列的 StackPanel, 就像第一個例子, 所有的子控制被做水平的延展。水平排列的 StackPanel,所有的子控制被做垂直的延展, 就如上面的例子一樣。StackPanel 有這樣延展的效果,因為子控制的水平對齊或者垂直對齊的屬性設定為延展(Stretch),但是如果你需要,你可以很容易的覆蓋這設定。下一個例子中,就如同前一個例子所使用的標記,我們針對所有子控制中的垂直對齊來改變設定值:

<Window x:Class="WpfTutorialSamples.Panels.StackPanel"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="StackPanel" Height="160" Width="300">
    <StackPanel Orientation="Horizontal">
        <Button VerticalAlignment="Top">Button 1</Button>
        <Button VerticalAlignment="Center">Button 2</Button>
        <Button VerticalAlignment="Bottom">Button 3</Button>
        <Button VerticalAlignment="Bottom">Button 4</Button>
        <Button VerticalAlignment="Center">Button 5</Button>
        <Button VerticalAlignment="Top">Button 6</Button>
    </StackPanel>
</Window>

垂直模式的StackPanel,具有不同对齐的子控件

我們使用上,中, 下的值來排列好按鍵的位置。就像上述垂直排列的StackPanel, 你可以在子控制使用水平對齊。

<Window x:Class="WpfTutorialSamples.Panels.StackPanel"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="StackPanel" Height="160" Width="300">
    <StackPanel Orientation="Vertical">
        <Button HorizontalAlignment="Left">Button 1</Button>
        <Button HorizontalAlignment="Center">Button 2</Button>
        <Button HorizontalAlignment="Right">Button 3</Button>
        <Button HorizontalAlignment="Right">Button 4</Button>
        <Button HorizontalAlignment="Center">Button 5</Button>
        <Button HorizontalAlignment="Left">Button 6</Button>
    </StackPanel>
</Window>

水平模式的StackPanel,具有不同对齐的子控件

如同你所看到的,所有的控制仍然是由上往下排列, 但是每個控制是對左,右,或者中對齊。

DockPanel使得在所有四个方向(顶部、底部、左侧和右侧)都可以很容易地停靠内容。这在很多情况下都是一个很好的选择,其中您希望将窗口划分为特定的区域,尤其是因为默认情况下,DockPanel内的最后一个元素(除非该特性被特别禁用)将自动填充剩余的空间(中心)。

正如我们在WPF中看到的,通过使用面板的附加属性(本例中为DockPanel.Dock属性),您可以开始利用面板的可能性,DockPanel属性决定子控件要停靠到哪个方向。如果你不使用这个,第一个控件将停靠在左边,最后一个控件占用剩余的空间。下面是一个你如何使用它的例子:

<Window x:Class="WpfTutorialSamples.Panels.DockPanel"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DockPanel" Height="250" Width="250">
    <DockPanel>
        <Button DockPanel.Dock="Left">Left</Button>
        <Button DockPanel.Dock="Top">Top</Button>
        <Button DockPanel.Dock="Right">Right</Button>
        <Button DockPanel.Dock="Bottom">Bottom</Button>
        <Button>Center</Button>
    </DockPanel>
</Window>

一个简单的DockPanel

就像早已提到的,我们不给最后一个子控件分配停靠位置,因为它自动居中控件,所以会允许它填满剩余的空间。您还会注意到中心周围的控件只占用了他们需要的空间 - 其他所有内容都留给了中心位置。这就是为啥你看右按钮比左按钮多占用一点空间--只是额外的字符需要更多的像素。

你可能会注意到的最后一件事是空间是如何分配的。 例如,Top按钮没有获得所有顶部空间,因为Left按钮占用了它的一部分。 DockPanel根据它们在标记中的位置来决定优先哪个控件。 在这种情况下,Left按钮优先,因为它首先放在标记中。 幸运的是,这也意味着它很容易改变,正如我们在下一个例子中所看到的那样。我们还可以通过为子控件指定宽度/高度来调整空间:

<Window x:Class="WpfTutorialSamples.Panels.DockPanel"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DockPanel" Height="250" Width="250">
    <DockPanel>
        <Button DockPanel.Dock="Top" Height="50">Top</Button>
        <Button DockPanel.Dock="Bottom" Height="50">Bottom</Button>
        <Button DockPanel.Dock="Left" Width="50">Left</Button>
        <Button DockPanel.Dock="Right" Width="50">Right</Button>    
        <Button>Center</Button>
    </DockPanel>
</Window>

已为子控件指定宽度或高度的DockPanel

顶部和底部控件现在优先于左右控件,它们都在高度或宽度上占用50个像素。如果你使窗口变大或变小,你也会看到这个静态宽度/高度保持不变,不管怎样——只有当你调整窗口的大小时,中心区域的大小会增加或减小。

如前所述,默认行为是,DockPanel的最后一个子节点占用其余空间,但是可以使用LastChildFill禁用此操作。这里有一个例子,我们禁用它,同时我们将展示将多个控件对接到同一个方面的能力:

<Window x:Class="WpfTutorialSamples.Panels.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 LastChildFill="False">
        <Button DockPanel.Dock="Top" Height="50">Top</Button>
        <Button DockPanel.Dock="Bottom" Height="50">Bottom</Button>
        <Button DockPanel.Dock="Left" Width="50">Left</Button>
        <Button DockPanel.Dock="Left" Width="50">Left</Button>
        <Button DockPanel.Dock="Right" Width="50">Right</Button>
        <Button DockPanel.Dock="Right" Width="50">Right</Button>
    </DockPanel>
</Window>

DockPanel的LastChildFill属性已被禁用

在这个例子中,我们将两个控件停靠在左侧,两个控件停靠在右侧,同时我们关闭LastChildFill属性。 这使我们在中心留下空的空间,这在某些情况下可能更好。

Grid是面板类型中最复杂的。Grid可以包含多行和多个列。您为每行定义一个高度,为每列定义一个宽度,以像素的绝对数量、可用空间的百分比或自动方式,其中行或列将根据内容自动调整其大小。当其他面板不能胜任时使用Grid,例如,当您需要多个列并且经常与其他面板组合时。

在最基本的形式中,Grid将简单地接受您放入其中的所有控件,将它们拉伸以使用最大可用空间并将他们堆叠:

<Window x:Class="WpfTutorialSamples.Panels.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">
    <Grid>
        <Button>Button 1</Button>
        <Button>Button 2</Button>
    </Grid>
</Window>

一个简单的Grid

正如你所看到的,最后一个控件得到顶部位置,在这种情况下意味着你甚至看不到第一个按钮。不过,对于大多数情况来说,这并不是非常有用,所以让我们试着划分空间,这就是网格所能做到的。我们通过使用ColumnDefinitions和RowDefinitions.定义来实现这一点。在第一个例子中,我们将划分列:

<Window x:Class="WpfTutorialSamples.Panels.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">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Button>Button 1</Button>
        <Button Grid.Column="1">Button 2</Button>
    </Grid>
</Window>

Grid分为两列

在这个示例中,我们简单地将可用空间划分为两列,它们将使用“星形宽度”(这将在后面解释)来平均共享空间。在第二个按钮上,我使用所谓的Attached属性将按钮放置在第二列中(0是第一列,1是第二列,依此类推)。我本来也可以在第一个按钮上使用这个属性,但是它会自动分配到第一列和第一行,这正是我们在这里想要的。

如您所见,控件占据了所有可用空间,这是网格排列其子控件时的默认行为。它通过在其子控件上设置水平对齐和垂直对齐来拉伸。

在某些情况下,您可能希望它们只占用它们所需的空间,和/或控制它们如何放置在网格中。最简单的方法是直接在希望操作的控件上设置水平对齐和垂直对齐。下面是上面例子的修改版本:

<Window x:Class="WpfTutorialSamples.Panels.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">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>        
        <Button VerticalAlignment="Top" HorizontalAlignment="Center">Button 1</Button>
        <Button Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Right">Button 2</Button>
    </Grid>
</Window>

从结果屏幕截图中可以看到,第一个按钮现在位于顶部并居中。 第二个按钮位于中间,与右侧对齐。

从结果屏幕截图中可以看到,第一个按钮现在位于顶部并居中。 第二个按钮位于中间,与右侧对齐。

在上一章中,我们向您介绍了强大的网格面板,并展示了几个关于如何使用它的基本示例。在本章中,我们将做一些更高级的布局,因为这是网格真正闪耀的地方。首先,让我们输入更多的列,甚至是一些行,用于一个真正的表格布局:

<Window x:Class="WpfTutorialSamples.Panels.TabularGrid"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TabularGrid" Height="300" Width="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2*" />
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="1*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="2*" />
            <RowDefinition Height="1*" />
            <RowDefinition Height="1*" />
        </Grid.RowDefinitions>
        <Button>Button 1</Button>
        <Button Grid.Column="1">Button 2</Button>
        <Button Grid.Column="2">Button 3</Button>
        <Button Grid.Row="1">Button 4</Button>
        <Button Grid.Column="1" Grid.Row="1">Button 5</Button>
        <Button Grid.Column="2" Grid.Row="1">Button 6</Button>
        <Button Grid.Row="2">Button 7</Button>
        <Button Grid.Column="1" Grid.Row="2">Button 8</Button>
        <Button Grid.Column="2" Grid.Row="2">Button 9</Button>
    </Grid>
</Window>

具有多个列和行的Grid,创建表格布局

总共有九个按钮,每个按钮放置在自己的网格中,包含三行和三列。我们再次使用基于星形的宽度,但是这次我们也分配了一个数字——第一行和第一列的宽度是2,这基本上意味着它使用的空间量是1(或者只是*,这是相同的。)的行和列的两倍

您还会注意到,我使用Attached属性Grid.Row和Grid.Column将控件放置在网格中,并且您将再次注意到我省略了控件上的这些属性,其中我想使用第一行或第一列(或两者)。这与指定一个零基本上是一样的。这节省了一些打字,但你可能更喜欢分配他们,以便更好的概述-这完全取决于你!

到目前为止,我们主要使用星形宽度/高度,它指定一行或一列应该占据组合空间的一定百分比。但是,还有两种指定列或行的宽度或高度的方法:绝对单元和自动宽度/高度。让我们尝试创建一个网格,我们将它们混合在一起:

<Window x:Class="WpfTutorialSamples.Panels.GridUnits"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="GridUnits" Height="200" Width="400">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="100" />
        </Grid.ColumnDefinitions>
        <Button>Button 1</Button>
        <Button Grid.Column="1">Button 2 with long text</Button>
        <Button Grid.Column="2">Button 3</Button>
    </Grid>
</Window>

具有不同宽度的列的网格

在这个示例中,第一个按钮具有星形宽度,第二个按钮将其宽度设置为Auto,最后一个按钮具有100像素的静态宽度。

结果可以在截屏上看到,其中第二个按钮仅占用它渲染较长文本所需的空间量,第三个按钮占用它承诺的100个像素,而第一个按钮(具有可变的宽度)占用剩余的空间。

在一个网格中,一个或多个列(或行)具有可变的(星形)宽度,它们自动地共享未使用的绝对宽度或自动宽度/高度的列/行所使用的宽度/高度。当我们调整窗口大小时,这一点变得更加明显:

具有不同宽度的列的网格,调整为较小的大小具有不同宽度的列的网格,调整为更大的大小

在第一个屏幕快照中,您将看到Grid为最后两个按钮保留空间,即使这意味着第一个按钮没有获得正确呈现所需的所有空间。在第二个屏幕快照中,您将看到最后两个按钮保持完全相同的空间量,将剩余空间留给第一个按钮。

这可以是一个非常有用的技术,当设计一个广泛的对话。例如,考虑一个简单的联系人表单,其中用户输入姓名、电子邮件地址和评论。前两个字段通常具有固定的高度,而最后一个字段可能占用尽可能多的空间,留下空间来键入更长的注释。在下一章中,我们将尝试使用不同高度和宽度的网格和行和列构建联系表单。

默认的网格行为是每个控件占用一个单元格,但有时您希望某个控件占用更多的行或列。幸运的是,Grid使用附加属性ColumnSpan和RowSpan使这非常简单。此属性的默认值显然为1,但您可以指定一个更大的数字,以使控件跨越更多行或列。

这是一个非常简单的示例,我们使用ColumnSpan属性:

<Window x:Class="WpfTutorialSamples.Panels.GridColRowSpan"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="GridColRowSpan" Height="110" Width="300">
    <Grid>
        <Grid.ColumnDefinitions>            
            <ColumnDefinition Width="1*" />
            <ColumnDefinition Width="1*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Button>Button 1</Button>
        <Button Grid.Column="1">Button 2</Button>
        <Button Grid.Row="1" Grid.ColumnSpan="2">Button 3</Button>
    </Grid>
</Window>

具有列跨越的网格应用于其中一个控件

我们只定义了两列和两行,它们都占据了相同的位置。前两个按钮只是正常使用列,但是使用第三个按钮,我们使用ColumnSpan属性在第二行占用两列空格。

这一切都非常简单,我们可以使用面板组合来实现相同的效果,但对于稍微更高级的情况,这非常有用。让我们尝试更好地展示这是多么强大的东西:

<Window x:Class="WpfTutorialSamples.Panels.GridColRowSpanAdvanced"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="GridColRowSpanAdvanced" Height="300" Width="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Button Grid.ColumnSpan="2">Button 1</Button>
        <Button Grid.Column="3">Button 2</Button>
        <Button Grid.Row="1">Button 3</Button>
        <Button Grid.Column="1" Grid.Row="1" Grid.RowSpan="2" Grid.ColumnSpan="2">Button 4</Button>
        <Button Grid.Column="0" Grid.Row="2">Button 5</Button>
    </Grid>
</Window>

具有列和行跨度的网格应用于多个子控件

三行三列通常会有九个单元格,但在这个例子中,通过使用行列跨度,我们仅用5个按钮就填充了所有的可用空间。正如你看到的那样,一个控件能够跨越额外的列/行,或者像按钮4一样跨越全部。

如您所见,跨越网格中的多个列和/或行非常容易。 在后面的文章中,我们将在更实际的示例中使用跨越以及所有其他网格技术。

正如您在前面的文章中看到的那样,Grid面板可以很容易地将可用空间划分为单个单元格。使用列和行定义,您可以轻松确定每行或列应占用多少空间,但如果您希望允许用户更改此内容,该怎么办?这是GridSplitter控件发挥作用的地方。

只需将GridSplitter添加到Grid中的列或行中,并使用适当的空间量,例如5个像素。然后,它将允许用户将其从一侧向另一侧或上下拖动,同时更改其每侧上的列或行的大小。这是一个例子:

<Window x:Class="WpfTutorialSamples.Panels.GridSplitterSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="GridSplitterSample" Height="300" Width="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="5" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBlock FontSize="55" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap">Left side</TextBlock>
        <GridSplitter Grid.Column="1" Width="5" HorizontalAlignment="Stretch" />
        <TextBlock Grid.Column="2" FontSize="55" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap">Right side</TextBlock>
    </Grid>
</Window>

具有GridSplitter控件的Grid面板具有GridSplitter控件的Grid面板

正如您所看到的, 我只是创建了一个网格, 其中包含两个同样宽的列, 中间有一个5像素列。每边都只是一个 textblock 控件来说明这一点。从屏幕截图中可以看到, 网格拆分器将呈现为两列之间的分界线, 一旦鼠标在其上方, 光标就会被更改以反映其大小。

网格拆分器是非常容易使用, 当然, 它也支持水平分裂。事实上, 您几乎不需要更改任何内容, 使其水平而不是垂直工作, 如下例所示:

<Window x:Class="WpfTutorialSamples.Panels.GridSplitterHorizontalSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="GridSplitterHorizontalSample" Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="5" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <TextBlock FontSize="55" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap">Top</TextBlock>
        <GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch" />
        <TextBlock Grid.Row="2" FontSize="55" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap">Bottom</TextBlock>
    </Grid>
</Window>

一个网格面板,其中有一个水平GridSplitter控件

正如您所看到的, 我只是将列更改为行, 在网格拆分器上, 我定义了 '高度' 而不是 '宽度'。网格拆分器自己计算其余的部分, 但如果没有, 则可以在其上使用ResizeDirection 属性将其强制进入 Rows 或 Columns 模式。

在最后几章中, 我们讨论了很多理论信息, 每个信息都有一些非常理论的例子。在本章中, 我们将把到目前为止学到的关于网格的知识结合到一个可以在现实世界中使用的例子中: 一个简单的contact form。

联系人窗体的好处是, 它只是一个常用对话框的示例-您可以使用使用的技术并将其应用于您需要创建的几乎任何类型的对话框。

第一次完成这项任务非常简单,它将向您展示一个非常基本的联系表格。它使用三行,其中两行具有自动高度,最后一行具有星高,因此它消耗了剩余的可用空间:

<Window x:Class="WpfTutorialSamples.Panels.GridContactForm"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="GridContactForm" Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>        
        <TextBox>Name</TextBox>
        <TextBox Grid.Row="1">E-mail</TextBox>
        <TextBox Grid.Row="2" AcceptsReturn="True">Comment</TextBox>        
    </Grid>
</Window>

使用Grid的简单联系人窗口

如您所见,最后一个TextBox只占用剩余空间,而前两个只占用它们所需的空间。尝试调整窗口大小,您将看到注释TextBox随之调整大小。

在这个非常简单的示例中,没有标签来指定每个字段的用途。相反,解释性文本位于TextBox内部,但这通常不是Windows对话框的外观。让我们尝试一下改善外观和可用性:

<Window x:Class="WpfTutorialSamples.Panels.GridContactFormTake2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="GridContactFormTake2" Height="300" Width="300">
    <Grid Margin="10">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Label>Name: </Label>
        <TextBox Grid.Column="1" Margin="0,0,0,10" />
        <Label Grid.Row="1">E-mail: </Label>
        <TextBox Grid.Row="1" Grid.Column="1" Margin="0,0,0,10" />
        <Label Grid.Row="2">Comment: </Label>
        <TextBox Grid.Row="2" Grid.Column="1" AcceptsReturn="True" />
    </Grid>
</Window>

使用Grid的简单联系人窗口 - 第二个

但是可能你有时候也想让评论部分突出, 这时候, 让我们跳过Label并使用ColumnSpan来让评论的TextBox获得更多的空间:

<TextBox Grid.ColumnSpan="2" Grid.Row="2" AcceptsReturn="True" />

使用Grid的简单联系人窗口 - 第三个

如您所见,Grid是一个非常强大的面板。希望您在设计自己的对话框时可以使用所有这些技巧。

成品:

image-20220707231250467

第六章 用户控件和自定义控件

到目前为止,在本教程中,我们只使用了WPF框架中的内置控件。 它们非常灵活,样式和模板几乎可以做任何事情。 但是,在某些时候,您可能想要创建自己的控件。 在其他UI框架中,这可能非常麻烦,但WPF使它非常简单,为您提供了两种完成此任务的方法:用户控件自定义控件

WPF UserControl 继承UserControl类,其行为与WPF窗口非常相似:有一个XAML文件和一个代码后置文件。 在XAML文件中,您可以添加现有的WPF控件以创建所需的外观,然后将其組合代码后置文件中的代码,以实现所需的功能。 然后,WPF将允许您在应用程序的一个或多个位置嵌入此功能集,從而允许您輕鬆地在应用程序中分组和重用功能。

自定义控件比用户控件更低级别。 创建自定义控件时,将根据需要的深度继承现有类。 在许多情况下,您可以继承其他WPF控件继承的Control类(例如TextBox),但如果您需要更深入,则可以继承FrameworkElement甚至UIElement。 你越深入,你得到的控制就越多,继承的功能就越少。

自定义控件的外观通常通过主题文件中的样式进行控制,而UserControl的外观则遵循应用程序部分的外观。这也强调了用户控件和自定义控件之间的主要区别之一:自定义控件可以设置样式/模板,而用户控件则不能。

在WPF中创建可重复使用的控件非常简单,尤其是在采用UserControl方法的情况下。在下一篇文章中,我们将研究创建UserControl然后在您自己的应用程序中使用它是多么容易。

在WPF中由UserControl类表示,用户控件是将标记和代码分组到可重用容器中的概念,因此具有相同界面相同功能,可以在几个不同的位置使用,甚至可以在多个应用程序中使用。

用户控件的行为很像WPF窗口 - 您可以放置其他控件的区域,然后是可以与这些控件交互的代码后置文件。 包含用户控件的文件也以.xaml结尾,而代码后置以.xaml.cs结尾 - 就像一个Window。 起始标记虽然看起来有点不同:

<UserControl x:Class="WpfTutorialSamples.User_Controls.LimitedInputUserControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
        
    </Grid>
</UserControl>

没什么太奇怪的 - 一个根UserControl元素而不是Window元素,然后是DesignHeight和DesignWidth属性,它们在设计时控制用户控件的大小(在运行时,用户控件的大小将由容纳它的容器决定)。 你会在后置代码中注意到同样的事情,它继承UserControl而不是Window

通过右键单击要添加它的项目或文件夹名称,向项目添加用户控件,就像添加另一个Window一样,如此截图所示(可能看起来有点不同,具体取决于您正在使用的Visual Studio版本):

将UserControl添加到项目中

在这篇文章中,我们将创建一个有用的用户控件,能够将TextBox中的文本数量限制为特定数量的字符,同时向用户显示已使用的字符数以及可以使用的字符数。 这很简单,并且在许多Web应用程序(如Twitter)中使用。 将这个功能添加到常规窗口很容易,但由于它可能在应用程序的多个位置使用,因此将它包装在一个易于重用的UserControl中是有意义的。

在我们深入研究代码之前,让我们看一下我们的最终结果:

限制输入的UserControl

这是用户控件本身的代码:

<UserControl x:Class="WpfTutorialSamples.User_Controls.LimitedInputUserControl"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         mc:Ignorable="d" 
         d:DesignHeight="300" d:DesignWidth="300">
    <Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>      
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="Auto" />
    </Grid.ColumnDefinitions>
    <Label Content="{Binding Title}" />
    <Label Grid.Column="1">
        <StackPanel Orientation="Horizontal">
        <TextBlock Text="{Binding ElementName=txtLimitedInput, Path=Text.Length}" />
        <TextBlock Text="/" />
        <TextBlock Text="{Binding MaxLength}" />
        </StackPanel>
    </Label>
    <TextBox MaxLength="{Binding MaxLength}" Grid.Row="1" Grid.ColumnSpan="2" Name="txtLimitedInput" ScrollViewer.VerticalScrollBarVisibility="Auto" TextWrapping="Wrap" />
    </Grid>
</UserControl>
using System;
using System.Windows.Controls;

namespace WpfTutorialSamples.User_Controls
{
    public partial class LimitedInputUserControl : UserControl
    {
    public LimitedInputUserControl()
    {
        InitializeComponent();
        this.DataContext = this;
    }

    public string Title { get; set; }

    public int MaxLength { get; set; }
    }
}

标记非常简单:一个有两列两行的Grid。 Grid的上半部分包含两个标签,一个显示标题,另一个显示统计数据。 它们中的每一个都使用数据绑定来获取所需的所有信息 - TitleMaxLength来自后置代码的属性,我们已将其定义为常规类的常规属性。

当前字符计数是通过直接绑定到TextBox控件上的Text.Length属性获得的,该控件使用用户控件的下半部分。 结果可以在上面的截图中看到。 请注意,由于所有这些绑定,我们不需要任何C#代码来更新标签或在TextBox上设置MaxLength属性 - 相反,我们只是直接绑定到属性。

有了上面的代码,我们所需要的就是在Window中消费(使用)用户控件。 我们将通过在Window的XAML代码的顶部添加对UserControl所在的命名空间的引用来实现:

xmlns:uc="clr-namespace:WpfTutorialSamples.User_Controls"

之后,我们可以使用uc前缀将控件添加到我们的Window,就像任何其他WPF控件一样:

<uc:LimitedInputUserControl Title="Enter title:" MaxLength="30" Height="50" />

请注意我们如何直接在XAML中使用TitleMaxLength属性。 这是我们窗口的完整代码示例:

<Window x:Class="WpfTutorialSamples.User_Controls.LimitedInputSample"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:uc="clr-namespace:WpfTutorialSamples.User_Controls"
    Title="LimitedInputSample" Height="200" Width="300">
    <Grid Margin="10">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    
    <uc:LimitedInputUserControl Title="Enter title:" MaxLength="30" Height="50" />
    <uc:LimitedInputUserControl Title="Enter description:" MaxLength="140" Grid.Row="1" />
    
    </Grid>
</Window>

有了它,我们可以在一行代码中重用这整个功能,如本例所示,我们有两个限制文本输入的控件。 如前所示,最终结果如下所示:

正在输入的,已限制输入的UserControl

强烈建议在用户控件中放置常用的界面和功能,正如您在上面的示例中所看到的,它们非常易于创建和使用。

第七章 WPF数据绑定

資料綁定是一種將兩個資料/訊息 資源綁定再一起並保持資料同步的普遍技術

在WPF中,微软把数据绑定放到了非常重要的位置,一旦你开始学习WPF你就会发现,数据绑定是一个非常重要的概念,它几乎和你所有操作都有关。如果你是从WinForm转过来的,和数据绑定相关的大量关注点可能会有一点吓到你,但是一旦你习惯了使用数据绑定,你会爱上它,因为它让许多的事情变的清晰,并且使得代码更加易于维护。

数据绑定是将数据从后台代码输送到界面层的首选方法。当然,你也可以通过设置控件的属性或者通过一个循环将数据项填充到ListBox的方法来显示数据,但是,在数据源和目标界面元件之间建立一个绑定的方式是最纯净的。

在下一个章节,我们将观察一个简单的使用了数据绑定的例子,再接下来,我们会更多的讨论所有可能性。在本教程的早期部分就已经包含了数据绑定的概念了,因为数据绑定是使用WPF时不能绕过的一部分,当你一旦开始学习后面的章节的时候你就发现,我们无时无刻都是使用它。

当然,如果你一开始只是想创建一个简单的WPF应用的话,数据绑定的理论部分的内容或许会太沉重了。在这种情况下,我建议你先看一下“Hello,Bound World!”这篇文章,了解一下数据绑定是如何工作的,并将剩下的数据绑定的文章放到晚些时候,当你准备好学习更多理论知识的时候再看。

就像我们用经典的 "Hello, world!"例子开始这个教程一样,我们会用一个"Hello, bound world!" 的例子让你看到在WPF中使用数据绑定是多么的简单。让我们先直接看例子,后面我会再给你们解释。

<Window x:Class="WpfTutorialSamples.DataBinding.HelloBoundWorldSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="HelloBoundWorldSample" Height="110" Width="280">
    <StackPanel Margin="10">
        <TextBox Name="txtValue" />
        <WrapPanel Margin="0,10">
            <TextBlock Text="Value: " FontWeight="Bold" />
            <TextBlock Text="{Binding Path=Text, ElementName=txtValue}" />
        </WrapPanel>
    </StackPanel>
</Window>

控件之间的简单数据绑定

這個簡單的範例展示了我們如何挷定一個TextBlock的Value與TextBox的text屬性相匹配。如同你從截圖上看到的,當你在TextBox輸入文字,TextBlock會自動的更新。在沒有使用挷定方法的時候,要做到這個功能,我們必需要監聽TextBox的事件並且在每次輸入文字時更新TextBlock。但是有了資料挷定功能之後,這個連結透過使用標記就可以被建立了。

所有的魔法都發生在這括號中,它在XAML封裝的標記延伸(Markup Extension)中。 在資料挷定時,我們使用Binding這個延伸標記來描述與Text屬性綁定的關係,在最簡單的格式中,一個Binding可能長的像這樣:

{Binding}

這個描述簡單的回傳一個Data Context(稍後的內容會再提到)。這當然是有用的,但一般的情況下,你應該會試著把目標屬性綁定至另一個在Data Context裡的來源屬性。那麼這個綁定的宣告將會如下所示:

{Binding Path=NameOfProperty}

Path指出了你所想要綁定的屬性。由於Path本身是資料綁定的預設屬性,因此你也可以如下面的方式省略它:

{Binding NameOfProperty}

你將會看到許多不同的例子,有的會明確的定義Path屬性,而有的則會選擇省略它;這些完全取決於你的決定。

資料綁定還包含有許多其他的屬性,其中ElementName就出現在我們的例子中。它允許我們直接連結其他UI元素為資料的來源。我們透過逗號來區隔每一個屬性的設定:

{Binding Path=Text, ElementName=txtValue}

這只是WPF所有綁定可能性的簡述。在接下來的章節中,我們將探索更多內容,向您展示數據綁定的強大功能。

DataContext属性是绑定的默认源,除非你像我们再上一章节做的那样,使用ElementName属性单独声明了其他源。这个属性定义在FrameworkElement类中,这是包括WPF Window在内的大多数UI控件的基类。简单来说,它允许你指定绑定的源。

起始时默认DataContext 都是null. DataContext是可以通过层次关系继承下去的 . 只要Window 被设置了DataContext ,我们就能在任意的子控件里使用它. 设想这么一种情况:

<Window x:Class="WpfTutorialSamples.DataBinding.DataContextSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DataContextSample" Height="130" Width="280">
    <StackPanel Margin="15">
        <WrapPanel>
            <TextBlock Text="Window title:  " />
            <TextBox Text="{Binding Title, UpdateSourceTrigger=PropertyChanged}" Width="150" />
        </WrapPanel>
        <WrapPanel Margin="0,10,0,0">
            <TextBlock Text="Window dimensions: " />
            <TextBox Text="{Binding Width}" Width="50" />
            <TextBlock Text=" x " />
            <TextBox Text="{Binding Height}" Width="50" />
        </WrapPanel>
    </StackPanel>
</Window>
using System;
using System.Windows;

namespace WpfTutorialSamples.DataBinding
{
    public partial class DataContextSample : Window
    {
        public DataContextSample()
        {
            InitializeComponent();
            this.DataContext = this;
        }
    }
}

使用DataContext的几个数据绑定

上面只在InitalizeComponent() 的后面加了一句交互代码. 我们把this赋值给DataContext,就是告诉window我们把自己作为数据源.

在XAML中,我们会把各种信息绑定到Window的多种属性上,比如Title/Width/Height. 一旦为window指定了DataContext, 我们可以在所有的子控件上向全局变量一样使用它,不用一个个的绑定.

尝试运行一下这个例子并缩放窗口,你会看到窗口大小的缩放会即时反映在文本框中。你也可以尝试在第一个文本框中输入其他标题,但你会惊奇地发现这个变化不会即时反映在窗口上。相反,你需要将焦点移至其他控件后更改才会生效。这是为什么呢?这就是下一章节的主题了。

使用DataContext属性是在控件层次结构中设置所有绑定的基础。 这为您节省了为手动定义每个绑定源的麻烦,一旦您真正开始使用数据绑定,您就会体验到这个好处。

但是,这并不意味着您必须对Window中的所有控件使用相同的DataContext。 由于每个控件都有自己的DataContext属性,因此您可以轻松地破坏继承链并使用新值覆盖DataContext。 这允许你做一些事情,比如在窗口上有一个全局DataContext,然后在一个Panel上指定一个特定的DataContext上,这个Panel所有的子都使用这个特定的DataContext。

正如我们在之前的数据绑定示例中看到的那样,使用XAML定义绑定非常简单,但是对于某些情况,您可能希望从后置代码中执行此操作。 这也非常简单,并提供与使用XAML时完全相同的可能性。 让我们尝试“Hello,bound world”示例,但这一次从后置代码创建所需的绑定:

<Window x:Class="WpfTutorialSamples.DataBinding.CodeBehindBindingsSample"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="CodeBehindBindingsSample" Height="110" Width="280">
    <StackPanel Margin="10">
    <TextBox Name="txtValue" />
    <WrapPanel Margin="0,10">
        <TextBlock Text="Value: " FontWeight="Bold" />
        <TextBlock Name="lblValue" />
    </WrapPanel>
    </StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfTutorialSamples.DataBinding
{
    public partial class CodeBehindBindingsSample : Window
    {
    public CodeBehindBindingsSample()
    {
        InitializeComponent();

        Binding binding = new Binding("Text");
        binding.Source = txtValue;
        lblValue.SetBinding(TextBlock.TextProperty, binding);
    }
    }
}

来自后置代码的数据绑定

它的工作原理是创建一个Binding实例。 我们直接在构造函数中指定我们想要的路径,在本例中为“Text”,因为我们要绑定到Text属性。 然后我们指定一个Source,对于这个例子,它应该是TextBox控件。 现在WPF知道它应该使用TextBox作为源控件,而且我们指定找包含在其Text属性的值。

在最后一行中,我们使用SetBinding方法将新创建的Binding对象与目的/目标控件,在本例中为TextBlock (lblValue)组合在一起。 SetBinding()方法接受两个参数,一个用于指示我们要绑定到哪个依赖项属性,另一个用于保存我们希望使用的绑定对象。

正如您所看到的,与在XAML中内创建它们的语法相比,在C#代码中创建绑定也很容易,对于刚接触数据绑定的人来说可能更容易掌握。 你使用哪种方法取决于你 - 它们都工作得很好。

在上一篇文章中,我们看到了TextBox中的更改是如何不立即发送回源的。相反,只有在TextBox上丢失焦点后才更新源。此行为由binding的UpdateSourceTrigger属性控制。该属性默认值为“Default”,表示根据您绑定的属性来更新源。在输入时,除了Text属性之外的所有属性在属性更改时立即更新(PropertyChanged),而当目标元素丢失焦点时(LostFocus),Text属性才会更新。

显然,Default是UpdateSourceTrigger的默认值。其他选项是PropertyChanged, LostFocusExplicit。前两个已经描述过,而最后一个只是意味着必须手动推送更新,使用Binding上的UpdateSource调用。

为了了解所有这些选项的工作原理,我更新了上一章中的示例,向您展示了所有这些选项:

<Window x:Class="WpfTutorialSamples.DataBinding.DataContextSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="DataContextSample" Height="130" Width="310">
    <StackPanel Margin="15">
        <WrapPanel>
            <TextBlock Text="Window title:  " />
            <TextBox Name="txtWindowTitle" Text="{Binding Title, UpdateSourceTrigger=Explicit}" Width="150" />
            <Button Name="btnUpdateSource" Click="btnUpdateSource_Click" Margin="5,0" Padding="5,0">*</Button>
        </WrapPanel>
        <WrapPanel Margin="0,10,0,0">
            <TextBlock Text="Window dimensions: " />
            <TextBox Text="{Binding Width, UpdateSourceTrigger=LostFocus}" Width="50" />
            <TextBlock Text=" x " />
            <TextBox Text="{Binding Height, UpdateSourceTrigger=PropertyChanged}" Width="50" />
        </WrapPanel>
    </StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace WpfTutorialSamples.DataBinding
{
    public partial class DataContextSample : Window
    {
        public DataContextSample()
        {
            InitializeComponent();
            this.DataContext = this;
        }

        private void btnUpdateSource_Click(object sender, RoutedEventArgs e)
        {
            BindingExpression binding = txtWindowTitle.GetBindingExpression(TextBox.TextProperty);
            binding.UpdateSource();
        }
    }
}

几个数据绑定,每个绑定使用不同的UpdateSourceTrigger值

如您所见,三个文本框中的每一个现在都使用不同的UpdateSourceTrigger.

第一个设置为Explicit,这基本上意味着除非您手动执行,否则不会更新源。出于这个原因,我在TextBox旁边添加了一个按钮,它将根据需要更新源值。在Code-behind中,您将找到Click处理程序,我们使用几行代码从目标控件获取绑定,然后在其上调用UpdateSource()方法。

第二个TextBox使用LostFocus值,这实际上是Text绑定的默认值。这意味着每次目标控件失去焦点时都会更新源值。

第三个也是最后一个TextBox使用PropertyChanged值,这意味着每次绑定属性更改时都会更新源值,在这种情况下,只要文本更改,它就会更新。

尝试在您自己的计算机上运行该示例,并查看三个文本框的行为完全不同:第一个值在您单击按钮之前不会更新,第二个值在您离开TextBox之前不会更新,而第三个值会自动更新每次击键,文字更改等

绑定的UpdateSourceTrigger属性控制将更改的值发送回源的方式和时间。 但是,由于WPF非常擅长为您控制,因此默认值应该足以满足大多数情况,在这种情况下,您将获得不断更新的UI和良好性能的最佳组合。

对于那些需要更多控制过程的情况,这个属性肯定会有所帮助。 只需确保不会比实际需要更频繁地更新源值。 如果您想要完全控制,可以使用Explicit值,然后手动执行更新,但这确实会失去一些使用数据绑定的乐趣。

在之前的教程中,我們大多是在UI元件與現有的類別做綁定,但在現實生活的應用程式中,明顯的你會綁定到你現有的資料物件。這時容易,不過當你開始做了之後,你可能會發現一件讓你失望的事---就像之前的例子一樣,變化不會自動的反應。你將會在這篇文章中學習到,你需要額外費一點功夫來達成這件事,不過很幸運的,WPF讓這件事情變的相當簡單。

在处理数据源更改时,您可能想要或可能不想处理两种不同的方案:更改项目列表以及每个数据对象中绑定属性的更改。如何处理它们可能会有所不同,具体取决于您正在做什么以及您希望实现的目标,但WPF提供了两个非常简单的解决方案:ObservableCollectionINotifyPropertyChanged接口。

以下示例将向您展示我们为什么需要这两件事:

<Window x:Class="WpfTutorialSamples.DataBinding.ChangeNotificationSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ChangeNotificationSample" Height="150" Width="300">
    <DockPanel Margin="10">
        <StackPanel DockPanel.Dock="Right" Margin="10,0,0,0">
            <Button Name="btnAddUser" Click="btnAddUser_Click">Add user</Button>
            <Button Name="btnChangeUser" Click="btnChangeUser_Click" Margin="0,5">Change user</Button>
            <Button Name="btnDeleteUser" Click="btnDeleteUser_Click">Delete user</Button>
        </StackPanel>
        <ListBox Name="lbUsers" DisplayMemberPath="Name"></ListBox>
    </DockPanel>
</Window>
using System;
using System.Collections.Generic;
using System.Windows;

namespace WpfTutorialSamples.DataBinding
{
    public partial class ChangeNotificationSample : Window
    {
        private List<User> users = new List<User>();

        public ChangeNotificationSample()
        {
            InitializeComponent();

            users.Add(new User() { Name = "John Doe" });
            users.Add(new User() { Name = "Jane Doe" });

            lbUsers.ItemsSource = users;
        }

        private void btnAddUser_Click(object sender, RoutedEventArgs e)
        {
            users.Add(new User() { Name = "New user" });
        }

        private void btnChangeUser_Click(object sender, RoutedEventArgs e)
        {
            if(lbUsers.SelectedItem != null)
                (lbUsers.SelectedItem as User).Name = "Random Name";
        }

        private void btnDeleteUser_Click(object sender, RoutedEventArgs e)
        {
            if(lbUsers.SelectedItem != null)
                users.Remove(lbUsers.SelectedItem as User);
        }
    }

    public class User
    {
        public string Name { get; set; }
    }
}

没有收到更改通知

尝试自己运行它并观察, 即使您向列表添加内容或更改其中一个用户的名称,UI中的任何内容都不会更新。这个例子非常简单,一个保存着用户姓名的User类,一个用来显示它们的ListBox,一些用于操作列表及其内容的按钮。列表的ItemsSource被分配为我们在窗口构造函数中创建的几个用户的快速列表。问题是似乎没有一个按钮能正确工作。让我们通过两个简单的步骤来解决这个问题。

第一步是让UI响应列表源(ItemsSource)中的更改,就像我们添加或删除用户时一样。我们需要的是一个列表,通知任何目的地其内容的更改,幸运的是,WPF提供了一种类型的列表。它被称为ObservableCollection,你使用它就像常规的List 一样,只有一些区别。

在下面的最后一个例子中,我们简单地用一个ObservableCollection <user>替换了List <user> - 这就是全部!这将使“添加”和“删除”按钮起作用,但它不会对“更改名称”按钮执行任何操作,因为更改将发生在绑定数据对象本身而不是源列表上 - 第二步将处理该场景。

第二步是让我们的自定义User类实现INotifyPropertyChanged接口。通过这样做,我们的User对象能够警告UI层对其属性的更改。这比仅更改列表类型更麻烦,就像我们上面所做的那样,但它仍然是完成这些自动更新的最简单方法之一。

通过上述两个更改,我们现在有一个示例将反映数据源中的更改。它看起来像这样:

<Window x:Class="WpfTutorialSamples.DataBinding.ChangeNotificationSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ChangeNotificationSample" Height="135" Width="300">
    <DockPanel Margin="10">
        <StackPanel DockPanel.Dock="Right" Margin="10,0,0,0">
            <Button Name="btnAddUser" Click="btnAddUser_Click">Add user</Button>
            <Button Name="btnChangeUser" Click="btnChangeUser_Click" Margin="0,5">Change user</Button>
            <Button Name="btnDeleteUser" Click="btnDeleteUser_Click">Delete user</Button>
        </StackPanel>
        <ListBox Name="lbUsers" DisplayMemberPath="Name"></ListBox>
    </DockPanel>
</Window>
using System;
using System.Collections.Generic;
using System.Windows;
using System.ComponentModel;
using System.Collections.ObjectModel;

namespace WpfTutorialSamples.DataBinding
{
    public partial class ChangeNotificationSample : Window
    {
        private ObservableCollection<User> users = new ObservableCollection<User>();

        public ChangeNotificationSample()
        {
            InitializeComponent();

            users.Add(new User() { Name = "John Doe" });
            users.Add(new User() { Name = "Jane Doe" });

            lbUsers.ItemsSource = users;
        }

        private void btnAddUser_Click(object sender, RoutedEventArgs e)
        {
            users.Add(new User() { Name = "New user" });
        }

        private void btnChangeUser_Click(object sender, RoutedEventArgs e)
        {
            if(lbUsers.SelectedItem != null)
                (lbUsers.SelectedItem as User).Name = "Random Name";
        }

        private void btnDeleteUser_Click(object sender, RoutedEventArgs e)
        {
            if(lbUsers.SelectedItem != null)
                users.Remove(lbUsers.SelectedItem as User);
        }
    }

    public class User : INotifyPropertyChanged
    {
        private string name;
        public string Name {
            get { return this.name; }
            set
            {
                if(this.name != value)
                {
                    this.name = value;
                    this.NotifyPropertyChanged("Name");
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void NotifyPropertyChanged(string propName)
        {
            if(this.PropertyChanged != null)
                this.PropertyChanged(this, new PropertyChangedEventArgs(propName));
        }
    }
}

接收更改通知

正如您所看到的,实现INotifyPropertyChanged非常简单,但它确实会在您的类上创建一些额外的代码,并为您的属性添加一些额外的逻辑。如果您想要绑定到自己的类并立即在UI中反映更改,那么这是您必须付出的代价。显然,您只需要在绑定的属性的setter中调用NotifyPropertyChanged - 其余的可以保持原样。

另一方面,ObservableCollection非常容易处理 - 它只是要求您在需要更改绑定目标中反映的源列表的情况下使用此特定列表类型。

到目前为止我们已经使用了一些简单的,可以同步属性的数据绑定。然而,你将会遇见想要使用一种类型,但需要以不同方式呈现的场景。

值转换器经常与数据绑定一起使用。以下是一些基本示例:

  • 您有一个数值,但您希望以一种方式显示零值,而以另一种方式显示正数
  • 您想要根据值检查CheckBox,但值是一个字符串,如“是”或“否”而不是布尔值
  • 您有一个以字节为单位的文件大小,但您希望根据它的大小显示为字节,千字节,兆字节或千兆字节。

这些是一些简单的案例,但还有更多。例如,您可能希望根据布尔值检查复选框,但是您希望它反转,以便检查CheckBox是否为false,如果值为true则不检查。您甚至可以使用转换器根据值生成一个ImageSource的图像,如绿色标志为true或红色标志为false - 可能性几乎无穷无尽!

对于这种情况,您可以使用值转换器。这些实现IValueConverter接口的小类将像中间人一样工作,并在源和目标之间转换值。因此,在任何需要在值到达目的地之前转换或再次返回其源的情况下,您可能需要转换器。

如上所述,WPF值转换器需要实现IValueConverter接口,或者IMultiValueConverter接口(稍后将详细介绍)。这两个接口只需要您实现两个方法:Convert()和ConvertBack()。顾名思义,这些方法将用于将值转换为目标格式,然后再返回。

让我们实现一个简单的转换器,它将一个字符串作为输入,然后返回一个布尔值,以及相反的方式。如果您是WPF的新手, 并且您可能是因为您正在阅读本教程,那么您可能不知道示例中使用的所有概念,但不要担心,它们都将在代码之后被解释列表:

<Window x:Class="WpfTutorialSamples.DataBinding.ConverterSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfTutorialSamples.DataBinding"
        Title="ConverterSample" Height="140" Width="250">
    <Window.Resources>
        <local:YesNoToBooleanConverter x:Key="YesNoToBooleanConverter" />
    </Window.Resources>
    <StackPanel Margin="10">
        <TextBox Name="txtValue" />
        <WrapPanel Margin="0,10">
            <TextBlock Text="Current value is: " />
            <TextBlock Text="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource YesNoToBooleanConverter}}"></TextBlock>
        </WrapPanel>
        <CheckBox IsChecked="{Binding ElementName=txtValue, Path=Text, Converter={StaticResource YesNoToBooleanConverter}}" Content="Yes" />
    </StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Data;

namespace WpfTutorialSamples.DataBinding
{
    public partial class ConverterSample : Window
    {
        public ConverterSample()
        {
            InitializeComponent();
        }
    }

    public class YesNoToBooleanConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            switch(value.ToString().ToLower())
            {
                case "yes":
                case "oui":
                    return true;
                case "no":
                case "non":
                    return false;
            }
            return false;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if(value is bool)
            {
                if((bool)value == true)
                    return "yes";
                else
                    return "no";
            }
            return "no";
        }
    }
}

使用IValueConverter,此处为空值使用IValueConverter,这里有一个转换为false的值使用IValueConverter,这里有一个转换为true的值

所以,让我们从后面开始,然后通过这个例子。我们在代码隐藏文件中实现了一个名为YesNoToBooleanConverter的转换器。正如所宣传的那样,它只实现了两个必需的方法,称为Convert()和ConvertBack()。 Convert()方法假定它接收一个字符串作为输入(value参数),然后将其转换为布尔值true或false值,后备值为false。为了好玩,我添加了从法语单词进行转换的可能性。

ConvertBack()方法显然正好相反:它假设一个布尔类型的输入值,然后返回英文单词“yes”或“no”作为回报,后退值为“no”。

您可能想知道这两种方法采用的其他参数, 但在此示例中不需要它们。我们将在接下来的一章中使用它们, 在那里我们将对它们进行解释。

在程序的XAML部分,我们首先将转换器的实例声明为窗口的资源。然后我们有一个TextBox,一些TextBlocks和一个CheckBox控件,这就是有趣的事情发生的地方:我们将TextBox的值绑定到TextBlock和CheckBox控件,并使用Converter属性和我们自己的转换器引用,我们根据需要,在字符串和布尔值之间来回处理值。

如果您尝试运行此示例,您将能够在两个位置更改值:在TextBox中写入“yes”(或任何其他值,如果您想要false)或通过选中CheckBox。无论您做什么,更改都将反映在其他控件以及TextBlock中。

这是一个简单的值转换器的例子,比说明目的所需的时间稍长。在下一章中,我们将研究一个更高级的示例,但在您出去编写自己的转换器之前,您可能想要检查WPF是否已包含一个用于此目的的转换器。在编写时,您可以利用20多个内置转换器,但您需要知道它们的名称。我发现以下列表可能会派上用场: http://stackoverflow.com/questions/505397/built-in-wpf-ivalueconverters

就像我们在上一章中看到的,通常用来修改绑定输出的方式是通过使用转换器.转换器的强大之处在于它允许你将任意数据类型转换为另一个完全不同的数据类型.然而,相对于大多数应用场景,你只是想改变某些值的显示方式而没有必要将其转换成另一个不同的类型,StringFormat属性则可以很好的做到这一点.

使用一个绑定的 StringFormat 属性时,你会丢失一些使用转换器时的灵活性,但相应地,它会更简单易用且不会在新文件中创建一个新类.

StringFormat 属性的的功能就如同它的名字所表达的: 它通过简单的调用 String.Format 方法来格式化输出字符串.我们先来看一个简短的示例:

<Window x:Class="WpfTutorialSamples.DataBinding.StringFormatSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        Title="StringFormatSample" Height="150" Width="250"
        Name="wnd">
    <StackPanel Margin="10">
        <TextBlock Text="{Binding ElementName=wnd, Path=ActualWidth, StringFormat=Window width: {0:#,#.0}}" />
        <TextBlock Text="{Binding ElementName=wnd, Path=ActualHeight, StringFormat=Window height: {0:C}}" />
        <TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat=Date: {0:dddd, MMMM dd}}" />
        <TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, StringFormat=Time: {0:HH:mm}}" />
    </StackPanel>
</Window>

几个数据绑定使用StringFormat属性来控制输出

前几个文本块通过绑定到它们的父窗口,得到窗口的高度和宽度来作为它们的值.这些值已经用 StringFormat 属性格式化. 对于宽度,我们指定了一个自定义的格式化字符串;对于高度,我们对它使用货币格式,只是为了有趣. 值是保存为double 类型的,所以如果我们调用了double.ToString(),我们可以使用所有相似的格式指示符. 您可以在这里找到一个格式指示符的列表: http://msdn.microsoft.com/en-us/library/dwhawy9k.aspx

另请注意我如何在StringFormat中包含自定义文本 - 这允许您根据需要使用文本预先/后固定绑定值。当引用格式字符串中的实际值时,我们用一组花括号括起来,它包括两个值:对我们要格式化的值的引用(值0,这是第一个可能的值)和格式字符串,用冒号分隔。

对于最后两个值,我们只是绑定到当前日期(DateTime.Now)并将其作为日期,以特定格式输出,然后再作为时间(小时和分钟),再次使用我们自己的,预先定义的格式。您可以在此处阅读有关DateTime格式的更多信息:http://msdn.microsoft.com/en-us/library/az4se3k1.aspx

请注意,如果您指定的格式字符串不包含任何自定义文本(上述所有示例都适用),那么在XAML中定义时,您需要添加一组额外的花括号。原因是WPF可能会将语法与用于标记扩展的语法混淆。这是一个例子:

<Window x:Class="WpfTutorialSamples.DataBinding.StringFormatSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        Title="StringFormatSample" Height="150" Width="250"
        Name="wnd">
    <WrapPanel Margin="10">
        <TextBlock Text="Width: " />
        <TextBlock Text="{Binding ElementName=wnd, Path=ActualWidth, StringFormat={}{0:#,#.0}}" />
    </WrapPanel>
</Window>

如果您需要根据特定文化输出绑定值,那没问题。 Binding将使用为父元素指定的语言,或者您可以使用ConverterCulture属性直接为绑定指定它。这是一个例子:

<Window x:Class="WpfTutorialSamples.DataBinding.StringFormatCultureSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        Title="StringFormatCultureSample" Height="120" Width="300">
    <StackPanel Margin="10">
        <TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, ConverterCulture='de-DE', StringFormat=German date: {0:D}}" />
        <TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, ConverterCulture='en-US', StringFormat=American date: {0:D}}" />
        <TextBlock Text="{Binding Source={x:Static system:DateTime.Now}, ConverterCulture='ja-JP', StringFormat=Japanese date: {0:D}}" />
    </StackPanel>
</Window>

有些数据绑定使用 StringFormat 属性时会使用特定的 ConverterCulture 来控制输出.

Make correction

它非常简单:通过组合使用D说明符(长日期模式)和ConverterCulture属性的StringFormat属性,我们可以根据特定的文化输出绑定值。太漂亮了!

第八章 WPF命令

第九章 对话框

第十章 通用界面控件

第十一章 Rich Text控件

第十二章 杂项控件

第十三章 TabControl

第十四章 列表控件

组合框(ComboBox)控件在很多方面类似于列表框(ListBox)控件,但占用的空间更少,因为该控件在不需要时可以隐藏项列表。该控件在Windows中使用很多,但为了确保每个人都了解它的外观和工作方式,我们将直接跳到一个简单的例子:

<Window x:Class="WpfTutorialSamples.ComboBox_control.ComboBoxSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ComboBoxSample" Height="150" Width="200">
    <StackPanel Margin="10">
        <ComboBox>
            <ComboBoxItem>ComboBox Item #1</ComboBoxItem>
            <ComboBoxItem IsSelected="True">ComboBox Item #2</ComboBoxItem>
            <ComboBoxItem>ComboBox Item #3</ComboBoxItem>
        </ComboBox>
    </StackPanel>
</Window>

一个简单的ComboBox控件

在屏幕截图中,我通过单击来激活控件,从而显示项列表。 从代码中可以看出,ComboBox简单的形式让使用时很容易。 我在这里只是手动添加一些项,通过在其上设置IsSelected属性使其中一个项成为默认选中。

在第一个例子中,我们只显示了文本项,这对于ComboBox控件来说很常见,但由于ComboBoxItem是一个ContentControl,我们其实可以使用任何东西作为内容。 让我们尝试制作一个稍微复杂的项列表:

<Window x:Class="WpfTutorialSamples.ComboBox_control.ComboBoxCustomContentSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ComboBoxCustomContentSample" Height="150" Width="200">
    <StackPanel Margin="10">
        <ComboBox>
            <ComboBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Image Source="/WpfTutorialSamples;component/Images/bullet_red.png" />
                    <TextBlock Foreground="Red">Red</TextBlock>
                </StackPanel>
            </ComboBoxItem>
            <ComboBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Image Source="/WpfTutorialSamples;component/Images/bullet_green.png" />
                    <TextBlock Foreground="Green">Green</TextBlock>
                </StackPanel>
            </ComboBoxItem>
            <ComboBoxItem>
                <StackPanel Orientation="Horizontal">
                    <Image Source="/WpfTutorialSamples;component/Images/bullet_blue.png" />
                    <TextBlock Foreground="Blue">Blue</TextBlock>
                </StackPanel>
            </ComboBoxItem>
        </ComboBox>
    </StackPanel>
</Window>

自定义内容的ComboBox控件

对于每个ComboBoxItem,我们添加一个StackPanel,我们在里面添加一个Image和一个TextBlock。 我们可以完全控制内容和文本渲染,如截图所示,文本颜色和图像都指示颜色值。

从第一个示例中可以看出,使用XAML可以很容易地手动定义ComboBox控件的项,但是很可能会遇到需要来自某种数据源的项的情况,比如数据库或者内存列表。 使用WPF数据绑定和自定义模板,我们可以轻松渲染颜色列表,包括颜色预览:

<Window x:Class="WpfTutorialSamples.ComboBox_control.ComboBoxDataBindingSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ComboBoxDataBindingSample" Height="200" Width="200">
    <StackPanel Margin="10">
        <ComboBox Name="cmbColors">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Rectangle Fill="{Binding Name}" Width="16" Height="16" Margin="0,2,5,2" />
                        <TextBlock Text="{Binding Name}" />
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
    </StackPanel>
</Window>
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;

namespace WpfTutorialSamples.ComboBox_control
{
    public partial class ComboBoxDataBindingSample : Window
    {
        public ComboBoxDataBindingSample()
        {
            InitializeComponent();
            cmbColors.ItemsSource = typeof(Colors).GetProperties();
        }
    }
}

使用数据绑定的ComboBox控件

它其实非常简单:在后台代码中,我使用反射Colors类的方法和获得所有颜色的列表。 我将它分配给ComboBox的ItemsSource属性,然后使用我在XAML部分中定义的模板呈现每个颜色。

ItemTemplate定义的每个项都包含一个带有Rectangle和TextBlock的StackPanel,每个项都绑定到颜色值。 这给了我们一个完整的颜色列表,只需要很少的工作量 - 它看起来很不错,对吧?

在第一个示例中,用户只能从我们的项列表中进行选择,但ComboBox的一个很酷的事情是它支持让用户从项列表中选择或输入自己的值。 在您希望通过为用户提供预定义选项集来帮助用户的情况下非常有用,同时仍然为他们提供手动输入所需值的选项。 这全部由IsEditable属性控制,它可以更改ComboBox的行为和外观:

<Window x:Class="WpfTutorialSamples.ComboBox_control.ComboBoxEditableSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ComboBoxEditableSample" Height="150" Width="200">
    <StackPanel Margin="10">
        <ComboBox IsEditable="True">
            <ComboBoxItem>ComboBox Item #1</ComboBoxItem>
            <ComboBoxItem>ComboBox Item #2</ComboBoxItem>
            <ComboBoxItem>ComboBox Item #3</ComboBoxItem>
        </ComboBox>
    </StackPanel>
</Window>

可编辑的ComboBox控件

如您所见,我可以随意输入什么值或从列表中选择一个。 如果从列表中选中,它会覆盖ComboBox的文本。

还有自动完成功能,ComboBox将自动尝试帮助用户在输入时选择现有值,如下一个屏幕截图所示,我刚刚开始输入“Co”:

具有自动完成功能的ComboBox控件

默认情况下,匹配不区分大小写,但您可以通过将IsTextSearchCaseSensitive设置为True来区分。 如果您根本不想要自动完成功能,可以通过将IsTextSearchEnabled设置为False来禁用它。

使用ComboBox控件的关键部分是能够读取用户选择,甚至可以使用代码控制它。 在下一个示例中,我重新使用了数据绑定的ComboBox示例,但添加了一些用于控制选择的按钮。 我还使用SelectionChanged事件来捕获所选项的变化,无论用代码还是用户,并对其进行操作。

这是例子:

<Window x:Class="WpfTutorialSamples.ComboBox_control.ComboBoxSelectionSample"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ComboBoxSelectionSample" Height="125" Width="250">
    <StackPanel Margin="10">
        <ComboBox Name="cmbColors" SelectionChanged="cmbColors_SelectionChanged">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Rectangle Fill="{Binding Name}" Width="16" Height="16" Margin="0,2,5,2" />
                        <TextBlock Text="{Binding Name}" />
                    </StackPanel>
                </DataTemplate>
            </ComboBox.ItemTemplate>
        </ComboBox>
        <WrapPanel Margin="15" HorizontalAlignment="Center">
            <Button Name="btnPrevious" Click="btnPrevious_Click" Width="55">Previous</Button>
            <Button Name="btnNext" Click="btnNext_Click" Margin="5,0" Width="55">Next</Button>
            <Button Name="btnBlue" Click="btnBlue_Click" Width="55">Blue</Button>
        </WrapPanel>
    </StackPanel>
</Window>
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Windows;
using System.Windows.Media;

namespace WpfTutorialSamples.ComboBox_control
{
    public partial class ComboBoxSelectionSample : Window
    {
        public ComboBoxSelectionSample()
        {
            InitializeComponent();
            cmbColors.ItemsSource = typeof(Colors).GetProperties();
        }

        private void btnPrevious_Click(object sender, RoutedEventArgs e)
        {
            if(cmbColors.SelectedIndex > 0)
                cmbColors.SelectedIndex = cmbColors.SelectedIndex - 1;
        }

        private void btnNext_Click(object sender, RoutedEventArgs e)
        {
            if(cmbColors.SelectedIndex < cmbColors.Items.Count-1)
                cmbColors.SelectedIndex = cmbColors.SelectedIndex + 1;
        }

        private void btnBlue_Click(object sender, RoutedEventArgs e)
        {
            cmbColors.SelectedItem = typeof(Colors).GetProperty("Blue");
        }

        private void cmbColors_SelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
        {
            Color selectedColor = (Color)(cmbColors.SelectedItem as PropertyInfo).GetValue(null, null);
            this.Background = new SolidColorBrush(selectedColor);
        }
    }
}

一个ComboBox控件,我们使用选择

这个例子的有趣部分是三个按钮的事件处理程序,以及SelectionChanged事件处理程序。 前两个按钮中,我们通过读取SelectedIndex属性然后减去或加上一来选择上一个或下一个项。 非常简单易用。

在第三个事件处理程序中,我们使用SelectedItem根据值选择特定项。 我在这里做了一些额外的工作(使用.NET反射),因为ComboBox被绑定到一个属性列表,每个属性都是一种颜色,而不是一个简单的颜色列表,基本上就是将一个项包含的值赋予SelectedItem属性。

在第四个和最后一个事件处理程序中,我响应所选项的变化。 变化时,我会读取所选颜色(再次使用反射,如上所述),然后使用所选颜色为窗口创建新的背景画笔。 可以在屏幕截图中看到效果。

Make correction

如果您使用可编辑的ComboBox(IsEditable属性设置为true),可以读取Text属性以了解用户输入或选择的值。

第十五章 ListView控件

第十六章 TreeView控件

第十七章 DataGrid 控件

第十八章 WPF样式

第十九章 音频与视频

第二十章 异步杂项

第二十一章 创建一个游戏:WPF贪吃蛇