WPF 深入研究 之 Control 控件

 

这一章介绍Control 控件。

本章共计51个示例,全都在VS2008下.NET3.5测试通过,点击这里下载:  Controls.rar

关于在VS2008新建WPF类库时,有Custom ControlUser Control两个选择。

User Control类库会在工程中建立一个XAML文件及其绑定后台代码,前者以<UserControl开头,后者是一个派生自UserControl的类。

Custom Control类库则在工程中创建派生自Control基类的CustomControl1控件,并在构造函数中为CustomControl1指定依赖属性DefaultStyleKeyProperty

    public class CustomControl1 : Control
    
{
        
static CustomControl1()
        
{
      DefaultStyleKeyProperty.OverrideMetadata(
typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
        }

    }

此外,Custom Control类库还会在Theme目录下生成Generic.xaml,这是一个以<ResourceDictionary开头的文件,里面存放着CustomControl1的控件模板。

1AccessText

这个例子介绍了AccessText元素的使用,以及如何指定一个访问键。

AccessText标记可以使用在任何控件中,如ButtonLabel等,通过在字母前加一个下划线指定该字母为访问键。连着两个下划线,可以显示一个下划线。

还有就是本例是以StackPanel开始的,并不是一个传统的PageWindow——所有的布局控件都可以这么使用。相应的后台代码文件要继承StackPanelVS2008目前还不支持创建这样的文件,只有手动设置。例如一个VS2008创建的一个名为Pane1Page,先修改页面标记PageStackPanel,这时,后台的Page1.g.i.cs文件会自动改为Pane1StackPanel,然后再修改Pane1.xaml.cs文件,使Pane1继承于StackPanel。这样就建立了一个所谓的StackPanel控件。

小技巧,在VS2008中,以StackPanel开始的页面的Design视图是没有滚动条的,这就只能看到左上部分——如果xaml内容很多的话,我们可以使用Design视图右上角的“比例尺”,缩小比例可以看到全部试图。

2BtnColor

按钮中可以放入任何控件,如image

注意到OnClick4方法。由于设置了背景色,所以按钮点击后的效果看上去像是一个颜色渐变的动画。这是由WPF本身提供的,即使是不设置背景色,点击后也会有颜色渐变效果,只是不太明显而已。

最后一个按钮的点击事件,创建了一个新的按钮加入到XAML控件树中,这与传统的WinForm模型是一致的。

仍然是最后一个按钮,按钮并没有Name值——这是允许的,也会在编译时被序列化,只是这个按钮的Name值为空,而其它属性都还是存在的。

3BtnStyles

这个示例使用Application级别的资源为Button设定样式。

注意Button标签中Style="{StaticResource SystemResStyle}的使用,以及<Application.Resources>中样式资源,有3种:

一种是直接的键值对,是静态的:

            <Setter Property = "Foreground" Value= "DarkBlue"/>

一种是使用WPF内嵌的资源,是动态的:

            <Setter Property = "FontWeight" Value= "{DynamicResource {x:Static SystemFonts.MessageFontWeightKey}}"/>

还有一种是触发器,当条件满足时,属性会相应改变:

            <Style.Triggers>

                <Trigger Property="Button.IsMouseOver" Value="true">

                    <Setter Property = "Background" Value="Red"/>

                </Trigger>

                <Trigger Property="Button.IsPressed" Value="true">

                    <Setter Property = "Foreground" Value="Green"/>

                </Trigger>

            </Style.Triggers>

4BulletPanel

Bullet用于绘制一组对象的列表。原有的BulletPanel标签已经被BulletDecorator取代。

Bullet中可以放置任何UI元素,如CheckBoxTextBoxRadioButtonImageTextBlock以及任何Shape。下面是一个标准的用法:

        <BulletDecorator >

            <BulletDecorator.Bullet>

                <Image Source="images"apple.jpg"/>

            </BulletDecorator.Bullet>

            <TextBlock Name="FontSizeExample">XXX</TextBlock>

        </BulletDecorator>

这里把Image作为Bullet,而把TextBlock作为BulletDecorator的子一级元素。ImageTextBlock是可以互换的,没有区别,但是标签<BulletDecorator.Bullet>中只能有一个UI元素。

WindowLoaded负责创建一个BulletDecorator控件,也就是图中左下角的那个。其中创建一个BitmapImage的代码如下:

            BitmapImage myBitmapImage = new BitmapImage();

            myBitmapImage.BeginInit();

            myBitmapImage.UriSource = new Uri(@"pack://application:,,/images/apple.jpg");

            myBitmapImage.EndInit();

——这是值得我们学习的。

*补注:面板本身没有文本内容,它们是其他元素的矩形容器。总共有七种面板,每种的布局都不同:BulletPanelCanvasDockPanelGridStackPanelTabPanelToolBarOverflowPanel。面板可以层层嵌套。

5CheckBoxElement

WPF对多选框控件也提供了同样的支持,只是更加简单,每个条目都是一个CheckBox,有自己的Name,以及可以内嵌其它控件。

CheckBoxCheckedUnchecked事件,分别当该条目被选中和被取消选中时触发。

6CheckBoxStyles

这个示例使用Application级别的资源为CheckBox设定样式。

具体参见示例4 BtnStyles

7ComboBoxItems

这个示例分为两部分。

首先是ComboBoxItems的显示,以下是部分截取的代码:

        <ComboBox Name="cb".

             IsEditable="true" Text="Open Combo Box" IsReadOnly="true"

             StaysOpenOnEdit="true" IsDropDownOpen="true">

            <ComboBoxItem>Spain - Item 0</ComboBoxItem>

        </ComboBox>

它的Text属性是默认的显示值,只在初始的时候作为默认选中项显示,当选中列表中一项时,这和个Text属性值就不再存在了。

IsEditableIsReadOnly属性共同决定了选项是否可以更改,但改动过的值不会影响该项原先的值——已经存储在后台,必须写代码进行手动修改。

StaysOpenOnEdit属性决定了在编辑时——也就是光标定位在ComboBox的文本框时,下拉框是否仍然显示。

IsDropDownOpen属性决定了默认下拉框是否为打开的,比如说窗体第一次显示时。

其次,就是那个Menu控件的作用了——通过MenuItemClick事件,根据MenuItemHeader值,也就是ComboBox中相应的索引值,来决定显示哪一个ComboBoxItem

            ComboBoxItem cbi = (ComboBoxItem)

                 (cb.ItemContainerGenerator.ContainerFromIndex(index));

这条语句重新得到了该索引的ComboBoxItem

8ComboBoxSimple

这个例子和示例-8是一样的,从略。

9ComboBoxStyles

这个例子演示了4个不同样式的ComboBox。这些样式来自Apllication级别。

第一个是SimplePlus样式,设定了默认打开下拉列表(IsDropDownOpen=true)和下拉列表的高度(MaxDropDownHeight=30)。

第二个是Simple样式,设定了前景色和背景色。

第三个仍然使用Simple样式,但给其内部的ComboBoxItem使用SimpleComboBoxItem样式,这就覆盖了之前设置的样式。

最后一个使用了触发器。这使得当鼠标移动到ComboBoxItem时,相应的选项会发生变化。

10ContentControl

ContentControl表示包含单项内容的控件,这是一个基类,很多控件都派生于此,如FrameListBoxItemWindow等等。我们也可以直接使用它。

ContentControl具有有限的默认样式,为此我们要额外设计样式以及控件模板:

            <Style x:Key="ContentCtrl" TargetType="{x:Type ContentControl}">

                <Setter Property="Foreground" Value="Green"/>

                <Setter Property="FontSize" Value="20"/>

                <Setter Property="FontWeight" Value="Bold"/>

                <Setter Property="Template">

                    <Setter.Value>

                        <ControlTemplate TargetType="{x:Type ContentControl}">

                            <ContentControl ContentControl.Content="Hello"/>

                        </ControlTemplate>

                    </Setter.Value>

                </Setter>

            </Style>

这就添加了前景色、字体大小和宽度、以及一个指定了显示内容的控件模板,那么第一个ContentControl就使用这套样式:

            <ContentControl Style="{StaticResource ContentCtrl}"/>

或者创建一个新的数据模板,

            <DataTemplate x:Key="template1">

                <TextBlock Text="{Binding}" FontSize="12" FontWeight="Bold" TextWrapping="Wrap"></TextBlock>

            </DataTemplate>

第二个ContentControl使用了这个模板:

            <ContentControl Name="contCtrl" ContentTemplate="{StaticResource template1}"

注:DataTemplate不仅仅用于数据绑定

此外,ContentControl有一个HasContent属性,用以判断控件的Content是否有值。

11ContextMenu

这个例子演示了ContextMenu的种种用法。

我们可以把ContextMenu捆绑到Button上:

        <Button Name="cmButton" Height="30">Button with Context Menu

            <Button.ContextMenu>

                <ContextMenu Name="cm" Opened="OnOpened" Closed="OnClosed" StaysOpen="true">

                    <MenuItem Header="File"/>

                    <MenuItem Header="Recent Files">

                        <MenuItem Header="ReadMe.txt"/>

                    </MenuItem>

         ……

ContextMenu上可以设置OnOpened事件和OnClosed事件,分别发生在上下文菜单弹出和关闭时。StaysOpen属性为false,表示上下文菜只能弹出一次,当鼠标再次右击,不会再弹出;否则,不加限制。

还可以为MenuItem添加tooltip

                        <MenuItem Header="Context Menu item with ToolTip">

                            <MenuItem.ToolTip>

                                <ToolTip>

                                    Some information.

                                </ToolTip>

                            </MenuItem.ToolTip>

                        </MenuItem>

或者在MenuItem中加入图片:

                        <MenuItem>

                            <MenuItem.Header>

                                <Image Source="data/cat.png"/>

                            </MenuItem.Header>

                        </MenuItem>

也可以为一个disabled的控件添加ContextMenu,比如说Button,只要设定它的属性:

ContextMenuService.ShowOnDisabled="True"

最后,就是在代码中动态添加一个ContextMenu,见OnClick方法。

12ContextMenuShared

This example describes how to create a ContextMenu that can be associated with more than one control. For example the ContextMenu is associated with both a Button and a CheckBox.

这个例子演示了两个Button和两个CheckBox共享页面资源中同样的ContextMenu

页面资源如下:

<ContextMenu x:Key="MyContextMenu" x:Shared="true">

    <MenuItem Header="This MenuItem is checkable" IsCheckable="true" />

    <Separator/>

    <MenuItem Header="This is a regular MenuItem" />

</ContextMenu>

这里的关键是x:Shared,当设置为 false时,会修改 WPF资源检索行为,以便资源请求会为每个请求创建一个新实例,而不是所有请求共享同一个实例。

指定x:Shared="true"并不常见,因为这已经是默认行为,意味着任何给定资源请求都始终返回同一个实例。对于x:Shared,没有直接的代码等效项。

x:Shared只有在满足以下条件的情况下才是合法的:

·包含具有x:Shared的项的ResourceDictionary必须已编译。ResourceDictionary不能在松散XAML内或用于主题。(所以本例要指定x:Class

·包含项的ResourceDictionary不得嵌套在另一个 ResourceDictionary内。例如,不能对已经是ResourceDictionary项的Style内部的ResourceDictionary中的项使用 x:Shared

四个控件使用方法都是一样的,以其中一个为例:

<Button Background="LightBlue"

    Content="This Button has a ContextMenu"

    ContextMenu="{DynamicResource MyContextMenu}" />

注意:MenuItemIsCheckable属性,表示菜单项是否可以被挑勾以表示选中。

13ContextMenuStyles

本例与示例-8的大同小异,不再敷述。

14ControlProps

本例依次展示了如何在代码中读写按钮的10个属性。分别是:BackgroundForegroundFontFamilyFontSizeFontStyleFontWeightBorderBrushHorizontalContentAlignmentVerticalContentAlignment,最后,还有Content

15ControlsAll

本例通过按钮的ContextMenu菜单,依次从代码中创建了14个基本控件,分别是

ButtonCheckBoxComboBoxContextMenuListBoxMenuRadioButtonRepeatButtonScrollBarSliderTextBoxThumbToolBarToolTip

以上各种控件的用法散见于本章各示例中。

16ControlTemplateExamples

这个例子演示了如何为控件创建新的样式,这里使用了大量的控件模板。

1

最后,介绍一下ResourceDictionary技术。

74UserControlNumericUpDown

这个示例演示了如何创建一个WPF用户控件。

为此要建立一个派生于UserControlXAML,在其中添加若干元素。这里设计了依赖属性ValueProperty,将其绑定到要调整的数值,并为其添加了OnValueChanged事件,触发于这个值改动的时候,会导致TextBlock中的值相应有所变动。

我们使用到了WPF事件机制的后台编程方式,这是对CLR 基本事件模型的更高级封装。其中涉及到了依赖属性。

而在页面中,可以很简单的使用这个用户控件:

<Window xmlns:uc="clr-namespace:UserControlNumericUpDown">

    <Grid>

        <uc:NumericUpDown />

    </Grid>

</Window>

17CustomControlNumericUpDown

18CustomControlNumericUpDownExternalLibrary

19CustomControlNumericUpDownOneProject

调试不出来

20Expander

Expander表示一个可显示标题的控件,该标题具有一个用于显示内容的可折叠窗口。对折叠的控制表现为IsExpanded属性,可以把Expander中的所有内嵌元素都隐藏起来。

此外,它的ExpandDirection属性的枚举值,Down、Up、LeftRight

                myExpander.ExpandDirection = ExpandDirection.Down;

ExpandDirection属性决定了内容窗口的打开方向,以下是四个枚举分别对应的效果:

注:我们还可以在折叠与展开的时候,使用事件CollapsedExpanded,自定义一些小逻辑。

21ExpanderRichContent

Expander的内容属性为Content,标题属性为Header,都可以嵌入控件元素,如这个示例,Header中放置了一个BulletDecorator

                <Expander.Header>

                    <BulletDecorator>

                        <BulletDecorator.Bullet>

                            <Image Width="10" Source="images"icon.jpg"/>

                        </BulletDecorator.Bullet>

                        <TextBlock Margin="20,0,0,0">My Expander</TextBlock>

                    </BulletDecorator>

               </Expander.Header>

Content中放置了一个ScrollViewer

                <Expander.Content>

                    <ScrollViewer Height="50">

注:如果展开窗口的内容对于窗口而言太大,您可以在 ScrollViewer 控件中换行显示 Expander 的内容,以便提供可滚动的内容。Expander 控件未自动提供滚动功能。

若要使 Expander 能够正确工作,当 ExpandDirection 属性设置为 Down 或 Up 时,请不要在 Expander 控件上指定 Height。同样,当 ExpandDirection 属性设置为 Left 或 Right 时,不要在 Expander 控件上指定 Width。如果在 Expander 控件显示展开内容的方向上设置大小,将显示由大小参数定义的区域,并在其周围显示一个边框。即使在窗口折叠时,仍会显示该区域。若要设置展开窗口的大小,请在 Expander 控件的内容上或在封闭内容的 ScrollViewer 上设置大小维度。

如果 Expander 控件是 DockPanel 中的最后一个元素,Expander 的大小将设置为填满 DockPanel 的其余区域。若要防止此行为,请将 DockPanel 的 LastChildFill 属性设置为 false,或者确保 Expander 不是 DockPanel 中的最后一个元素。

22FrameExample

Frame这个控件,多是用来承载导航的XAMLHTML页面的。

        <Frame Name = "myFrame" Grid.Column="0" Grid.Row="1" Background="LightBlue"/>

以下是导航语句:

                myFrame.Navigate(new System.Uri("http://msdn.microsoft.com/vcsharp/"));

                myFrame.Navigate(new System.Uri("AnotherAvalonPage.xaml",

                                 UriKind.RelativeOrAbsolute));

23GridSplitterProperties

这个示例演示了GridSplitter控件的使用。

GridSplitter派生于Thumb,只能使用于Grid中,而且要指定它所在的rowcolumn位置(以及行和列的span)

            GridSplitter split = new GridSplitter();

            split.Width = 6;

            grid.Children.Add(split);

            Grid.SetColumn(split, 2);

            Grid.SetRow(split, 1);

GridSplitter可以和其它元素共享相同的单元格,这就存在谁遮挡了谁的问题——后出现的元素总是在前面,为此,需要设置margin,以避免重叠:

            split.Margin = new Thickness(10);

一般会让GridSplitter跨越整行或整列,比较合理:

            Grid.SetRowSpan(split, 3);

            Grid.SetColumnSpan(split, 3);

一般把GridSplitter单独放在一个或一组单元格中

在这个例子中,通过设置GridSplitter的左对齐,可以看到遮挡效果,因为在Grid11)这个位置,有两个元素,GridSplitter是后出现的元素:

        <StackPanel Grid.Row="1" Grid.Column="1" Background="Red">

            <TextBlock>Row 1 Col 1</TextBlock>

        </StackPanel>

        <GridSplitter Name="myGridSplitter" Grid.Column="1" Grid.Row="1" Width="5"/>

下面讨论GridSplitter的几个属性:

1ShowsPreview属性,默认为fasle,也就是说拖动GridSplitter时看不到效果。而设置为true,则要等到松开鼠标,鸽子才会改变尺寸。

2DragIncrement属性,设置这个值,可以决定每次鼠标拖动的距离。默认值为1

3KeyboardIncrement属性,设置每按下一次箭头键时移动 GridSplitter 控件的距离。默认值为1

VerticalAlignmentHorizontalAlignment,是用来控制splitter是水平的还是垂直的。默认情况下,HorizontalAlignmentRightVerticalAlignmentStretch,此时splitter位于格子的右边,移动splitter会左右移动它所在列的宽度分配。我们可以改变HorizontalAlignmentLeftCenter,而保持VerticalAlignmentStretch不变。

我们可以通过splitterResizeBehavior枚举属性,改变上述行为,让我们选择哪个列会被splitter影响:

CurrentAndNext     等效于splitter靠右

PreviousAndCurrent等效于splitter靠左

PreviousAndNext    等效于splitter居中

BasedOnAlignment   等效于由splitter的HorizontalAlignment和VerticalAlignment来决定

相应的,设置HorizontalAlignmentStretchVerticalAlignmentTopBottomCenter,从而使splitter为水平的。

我们可以利用ResizeDirection属性,改变splitter影响的方向,有三种枚举值:Auto、Columns、Rows。对于水平splitter,按理说应该是上下移动,但是如果把ResizeDirection属性设为Columns

则可以左右移动splitter,但是splitter的效果就看不到了。

24GridSplitterRowColumn

这个示例演示了让GridSplitter跨越行或列,这是一种比较合理的方式,从外观上:

          <GridSplitter Grid.Row="1"  Grid.ColumnSpan="3"

25GroupBox

GroupBox就是一个很简单的容器,用来盛放多个控件在其中,这样可以同时控制它们的共同属性,如GroupBox不可见时,则其中所有控件也都不再可见。

26HeaderedContentControl

HeaderedContentControl为包含单项内容并具有标头的所有控件提供基实现,有两个关键属性,ContentHeader属性,可设置为任何类型的对象。

HeaderedContentControl具有有限的默认样式,其外观非常简单。如果希望增强控件的外观,可以创建新的ControlTemplate,如下:

            <Style x:Key="{x:Type HeaderedContentControl}" TargetType="{x:Type HeaderedContentControl}">

                <Setter Property="Template">

                    <Setter.Value>

                        <ControlTemplate TargetType="{x:Type HeaderedContentControl}">

                            <StackPanel>

                                <ContentPresenter ContentSource="Header"/>

                                <ContentPresenter ContentSource="Content"/>

                            </StackPanel>

                        </ControlTemplate>

                    </Setter.Value>

                </Setter>

            </Style>

这就重写了所有的HeaderedContentControl控件,使用了ContentPresenter并且规定,Header在上,Content在下。

在本例中,我们可以使用ContentControl来代替表示Content属性,也就是说,以下两种声明方式是相同的:

//声明方式1

<HeaderedContentControl HeaderTemplate="{StaticResource template2}" Header="Header"

ContentTemplate="{StaticResource template3}" Content=" This is the content."/>

//声明方式2

<HeaderedContentControl HeaderTemplate="{StaticResource template2}" Header="Header">

      <ContentControl ContentTemplate="{StaticResource template3}"

           Content="This is the content" />

</HeaderedContentControl>

这里,我们还可以细化到为HeaderContent指定DataTemplate

            <DataTemplate x:Key="template2">

                <TextBlock Text="{Binding}" Foreground="Green" FontSize="16"></TextBlock>

            </DataTemplate>

           <DataTemplate x:Key="template3">

                <TextBlock Text="{Binding}" Foreground="Brown" FontSize="12"></TextBlock>

            </DataTemplate>

注:有关数据模板的技术介绍参见“数据绑定”一章

注:ExpanderGroupBoxTabItem派生于HeaderedContentControl

再有就是,HeaderedContentControl有一个HasHeader只读属性,用来判断Header是否有值。

27HeaderedItemsControl

HeaderedItemsControl表示包含多个项并且具有标题的控件。类似于HeaderedContentControl,它的可视化样式也不多,需要手动创建数据模板和控件模板:

            <Style x