WPF中如何为ItemsControl添加ScrollViewer并显示ScrollBar
今天在开发的过程中突然碰到了一个问题,本来的意图是想当ItemsControl中加载的Item达到一定数量时,会出现ScrollViewer并出现垂直的滚动条,但是实际上并不能够达成目标,对于熟手来说这个问题非常简单,但是如果不了解WPF的模板的原理,可能并不清楚这些,这里举出一个例子来论证。
<Window x:Class="TestItemsControl.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow" Height="350" Width="525"> <Window.Resources> <Style x:Key="ItemsControlStyle1" TargetType="{x:Type ItemsControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ItemsControl}"> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True"> <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> </Window.Resources> <Grid> <ItemsControl Width="100" Height="100" Background="Teal" Style="{DynamicResource ItemsControlStyle1}"> <TextBox Text="1" Foreground="Red" Height="20" TextAlignment="Center"></TextBox> <TextBox Text="1" Foreground="Red" Height="20" TextAlignment="Center"></TextBox> <TextBox Text="2" Foreground="Red" Height="20" TextAlignment="Center"></TextBox> <TextBox Text="3" Foreground="Red" Height="20" TextAlignment="Center"></TextBox> <TextBox Text="4" Foreground="Red" Height="20" TextAlignment="Center"></TextBox> <TextBox Text="5" Foreground="Red" Height="20" TextAlignment="Center"></TextBox> <TextBox Text="6" Foreground="Red" Height="20" TextAlignment="Center"></TextBox> </ItemsControl> </Grid> </Window>
执行上述代码我们会发现不会出现ScrollBar,我们定义了ItemsControl的高度为100,当下面的Item超过了这个高度后多出的部分直接被剪切掉了,通过查看Window.Resources中的模板,那么我们可以很好理解,因为ItemsControl的结构是一个Border里面嵌套了一个ItemsPresenter,根本么有ScrollViewer,所以当然不会出现ScrollBar。这个问题非常好解决,直接修改ItemsControl的模板,在Border里面加上一个ScrollViewer,问题解决。
<Style x:Key="ItemsControlStyle1" TargetType="{x:Type ItemsControl}"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ItemsControl}"> <Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True"> <ScrollViewer> <ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"/> </ScrollViewer> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
效果如下所示:
下面来一步步深入,讨论一些复杂的问题。
在进行最终的问题解释之前,首先来探讨一下ItemsControl这个控件。
MSDN的解释是:表示用于呈现的项的集合的控件。
再看一下它的集成关系:
System.Object
System.Windows.Threading.DispatcherObject
System.Windows.DependencyObject
System.Windows.Media.Visual
System.Windows.UIElement
System.Windows.FrameworkElement
System.Windows.Controls.Control
System.Windows.Controls.ItemsControl
System.Windows.Controls.HeaderedItemsControl
System.Windows.Controls.Primitives.DataGridCellsPresenter
System.Windows.Controls.Primitives.DataGridColumnHeadersPresenter
System.Windows.Controls.Primitives.MenuBase
System.Windows.Controls.Primitives.Selector
System.Windows.Controls.Primitives.StatusBar
System.Windows.Controls.Ribbon.RibbonContextualTabGroupItemsControl
System.Windows.Controls.Ribbon.RibbonControlGroup
System.Windows.Controls.Ribbon.RibbonGallery
System.Windows.Controls.Ribbon.RibbonQuickAccessToolBar
System.Windows.Controls.Ribbon.RibbonTabHeaderItemsControl
System.Windows.Controls.TreeView
通过这些继承关系,我们可以发现ItemsControl是很多包含Items的集合的控件的基类,比如ListBox还有TreeView等等。
关于ItemsControl中有几个非常重要的概念需要理解:
1 Template 这个不用说ItemsControl的模板,用于展现ItemsControl最终由什么构成,即外表呈现。
<ItemsControl.Template> <ControlTemplate TargetType="ItemsControl"> <ScrollViewer x:Name="scrollViewer" VerticalScrollBarVisibility="Auto" Padding="5"> <ItemsPresenter ></ItemsPresenter> </ScrollViewer> </ControlTemplate> </ItemsControl.Template>
2 ItemsPanel属性,这个非常重要,这个是Items项的父容器,它决定了Items以何种方式去呈现,比如常用的Grid、 StackPanel、WrapPanel、UniformGrid、DockPanel等,甚至可以是自定义的Panel。
<ItemsControl.ItemsPanel> <ItemsPanelTemplate> <WrapPanel Width="Auto" Height="Auto" MaxWidth="500" IsItemsHost="True" HorizontalAlignment="Left" VerticalAlignment="Center"> </WrapPanel> </ItemsPanelTemplate> </ItemsControl.ItemsPanel>
3 ItemTemplate,这个属性表示每个Item将以何种方式呈现,有了这三种属性我们就可以定义我们需要的各种形式的界面。(下面的代码稍稍复杂一些)
<ItemsControl.ItemTemplate> <DataTemplate> <Grid Margin="0,0,10,3" HorizontalAlignment="Left" VerticalAlignment="Center"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="*"></ColumnDefinition> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <Border Name="DutyPerson" Grid.Row="0" Grid.RowSpan="2" Grid.Column="0" Grid.ColumnSpan="2" BorderBrush="#bdbdbd" BorderThickness="1" Padding="0" Width="70" Height="32" ContextMenu="{StaticResource SetLeader}"> <StackPanel> <TextBox x:Name="dutyPersonTextBox" Text="{Binding DutyPersonName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}" BorderThickness="0" Height="30"> <TextBox.ToolTip> <ToolTip HorizontalOffset="-18" VerticalOffset="5" BorderBrush="Transparent" Background="Transparent" HasDropShadow="False" Placement="Top" Visibility="{Binding IsLeader,Converter={StaticResource BoolToVisibility}}"> <Grid Margin="0"> <Image x:Name="personToolTipImage" Stretch="Uniform" RenderOptions.BitmapScalingMode="NearestNeighbor" Width="88" Height="36" VerticalAlignment="Bottom" Source="/AIPAnnouncement;component/ControlViews/Sources/Images/气泡.png"> </Image> <TextBlock Text="领导" FontSize="13" HorizontalAlignment="Center" VerticalAlignment="Center"> </TextBlock> </Grid> </ToolTip> </TextBox.ToolTip> <i:Interaction.Triggers> <i:EventTrigger EventName="TextChanged"> <interactive:ExInvokeCommandAction Command="{Binding DataContext.ModifyDutyPersonCommand,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=my:AnnouncementApp}}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TextBox}}"> </interactive:ExInvokeCommandAction> </i:EventTrigger> <i:EventTrigger EventName="GotFocus"> <interactive:ExInvokeCommandAction Command="{Binding DataContext.TextBoxGotFocus,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=my:AnnouncementApp}}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=TextBox}}"> </interactive:ExInvokeCommandAction> </i:EventTrigger> </i:Interaction.Triggers> </TextBox> <Popup x:Name="popup" PlacementTarget="{Binding ElementName=dutyPersonTextBox}" Width="{Binding ActualWidth,ElementName=dutyPersonTextBox}" IsOpen="{Binding ElementName=dutyPersonTextBox,Path=IsKeyboardFocused, Mode=OneWay}" StaysOpen="True"> <Grid Background="Red"> <ListBox x:Name="lb_selecthistorymembers" SnapsToDevicePixels="true" ItemsSource="{Binding DataContext.SpecificHistoryMembers,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=my:AnnouncementApp},Mode=TwoWay}" HorizontalAlignment="Stretch" ScrollViewer.HorizontalScrollBarVisibility="Disabled" Background="#fff" BorderThickness="1"> <i:Interaction.Triggers> <i:EventTrigger EventName="SelectionChanged"> <interactive:ExInvokeCommandAction Command="{Binding DataContext.OnSelectHistoryMembersListBoxSelected,RelativeSource={RelativeSource Mode=FindAncestor,AncestorType=my:AnnouncementApp},Mode=TwoWay}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListBox}}"> </interactive:ExInvokeCommandAction> </i:EventTrigger> </i:Interaction.Triggers> <ListBox.ItemContainerStyle> <Style TargetType="ListBoxItem"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBoxItem}"> <Border x:Name="Bd" Height="Auto" Width="Auto" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="0" Background="{TemplateBinding Background}" SnapsToDevicePixels="true"> <ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> </Border> <ControlTemplate.Triggers> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="HorizontalAlignment" Value="Stretch"></Setter> <Setter Property="VerticalAlignment" Value="Center"></Setter> <Setter Property="HorizontalContentAlignment" Value="Stretch"></Setter> </Style> </ListBox.ItemContainerStyle> <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel IsItemsHost="True" HorizontalAlignment="Left" VerticalAlignment="Center" Width="{Binding ActualWidth,ElementName=dutyPersonTextBox}" > </StackPanel> </ItemsPanelTemplate> </ListBox.ItemsPanel> <ListBox.ItemTemplate> <DataTemplate> <Border Name="Border" BorderThickness="0"> <Grid Margin="2,1,1,1"> <Label x:Name="label" Content="{Binding SpecificHistoryDutyPersonName}" HorizontalAlignment="Stretch" HorizontalContentAlignment="Center" FontSize="13"> </Label> </Grid> </Border> <DataTemplate.Triggers> <Trigger Property="IsMouseOver" Value="true"> <Setter Property="Background" Value="#00a3d9" TargetName="Border"> </Setter> <Setter Property="Background" Value="#f8f3f0" TargetName="label"> </Setter> </Trigger> </DataTemplate.Triggers> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid> </Popup> </StackPanel> </Border> <xui:Button x:Name="deleteAnnouncementItem" Grid.Row="0" Grid.Column="1" Height="14" Width="14" Opacity="0" HorizontalAlignment="Right" VerticalAlignment="Top"> <i:Interaction.Triggers> <i:EventTrigger EventName="Click"> <interactive:ExInvokeCommandAction Command="{Binding DataContext.DutyPersonDeleteCommand,RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=my:AnnouncementApp}}" CommandParameter="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=xui:Button}}"> </interactive:ExInvokeCommandAction> </i:EventTrigger> </i:Interaction.Triggers> <Button.Background> <ImageBrush ImageSource="/AIPAnnouncement;Component/ControlViews/Sources/Images/关闭.png"> </ImageBrush> </Button.Background> </xui:Button> </Grid> <DataTemplate.Triggers> <DataTrigger Binding="{Binding IsLeader}" Value="true"> <Setter Property="BorderBrush" Value="#f00" TargetName="DutyPerson"> </Setter> </DataTrigger> <Trigger Property="IsMouseOver" Value="true" SourceName="DutyPerson"> <Setter Property="BorderThickness" Value="0" TargetName="DutyPerson"> </Setter> </Trigger> <Trigger Property="IsMouseOver" Value="true" SourceName="deleteAnnouncementItem"> <Setter Property="Opacity" Value="1" TargetName="deleteAnnouncementItem"> </Setter> </Trigger> </DataTemplate.Triggers> </DataTemplate> </ItemsControl.ItemTemplate>
这里面的每一个Item定义成了类似于百度的搜索框一样的东西,当我们在文本输入框中输入文字时,会弹出一个Popup,里面是一个ListBox,我们可以从中挑选我们需要的选项,最后加入到TextBox中去,这里做了一个模板,在每一个TextBox下面添加一个Popup,当输入文字时会自动检索当前历史记录中是否存在当前项,这里面的核心是 IsOpen="{Binding ElementName=dutyPersonTextBox,Path=IsKeyboardFocused, Mode=OneWay}" StaysOpen=”true” 这句的意思表示当前的Popup是否打开是取决于dutyPersonTextBox(一个TextBox控件)是否获得鼠标的焦点,这里使用IsKeyboardFocused来表示鼠标是否获取到焦点,后面我们会看一看具体效果的图片。
刚开始的时候,没有很多思考,当自己定义ItemsControl的ItemsPanel时,给它赋了一个定值,这里就埋下了一个很大的隐患,所以我们在不断的往ItemsControl中添加Item的项时,ItemsControl的高度只会维持在70,因为Items的容器ItemsPanel的高度就决定了ItemsControl的高度,当超过这个高度的时候会自动地去剪裁掉多余的部分,这是WPF的一个基本机理,所以我们在设置ItemsPanel的容器WrapPanel的时候一定要将Height设置为Auto,这样我们就能看到ItemsPanel的高度自动增加,这是其中一个方面,另外一个方面就是当我们必须设置ItemsPanel的高度或者是其父容器的高度为一个固定值假设为FixHeight,这样当随着Item的项的增多,ItemsPanel容器的高度超过FixHeight时,我们就会发现ScrollBar会出现,这些东西都是需要我们去不断地思考和总结的一些结论。 WPF的这种机理在很多的地方都是可以看到的,例如当我们往WrapPanel中添加项目时,为了保证能够使添加的项自动添加到第二行,那么我们必须为WrapPanel设置一个宽度,这样当我们添加项时才会自动跳转到下一行,因为如果我们不设置这个值,默认的高度和宽度都是Auto,这个在使用中必须要十分注意,并且平时多积累,才能真正地学以致用。
今天就总结这么多,最后看一看最终的效果,其中第三行人员这一行就是使用ItemsControl做出来的效果。