高仿QQ即时聊天软件开发系列之三登录窗口用户选择下拉框

时间:2023-03-08 15:41:37

上一篇高仿QQ即时聊天软件开发系列之二登录窗口界面写了一个大概的布局和原理

这一篇详细说下拉框的实现原理

先上最终效果图

高仿QQ即时聊天软件开发系列之三登录窗口用户选择下拉框

一开始其实只是想给下拉框加一个placeholder效果,让下拉框在未选择未输入时显示一个提示字符串。由于Background对ComboBox无效,所以直接通过Background来实现是不行了。需要重新写ComboBox的模板,也就是Template,自定义一个模板来实现这个结果。又看了一下QQ的下拉框,这玩意不自定义也难以实现,所以就干脆自定义了。

先上代码,先是ComboBox,再是ComboBoxToggleButton,最后是ComboBoxItem

  <Style TargetType="{x:Type ComboBox}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid Margin="0">
<Border
BorderThickness="1"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="4,4,0,0" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="20" x:Name="colArrow"></ColumnDefinition>
</Grid.ColumnDefinitions>
<ToggleButton
Name="ToggleButton"
Grid.Column="1"
Focusable="false"
Style="{StaticResource ComboBoxToggleButton}"
IsChecked="{Binding Path=IsDropDownOpen,Mode=TwoWay,RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press">
</ToggleButton>
<TextBox x:Name="PART_EditableTextBox"
BorderThickness="0"
VerticalAlignment="Center">
<TextBox.Resources>
<VisualBrush x:Key="tbPlaceHolder" Stretch="None" AlignmentX="Left">
<VisualBrush.Visual>
<Label Content="CC号码/用户名/邮箱" Foreground="Gray" Padding="5,0,0,0"></Label>
</VisualBrush.Visual>
</VisualBrush>
</TextBox.Resources>
<TextBox.Style>
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Background" Value="{DynamicResource tbPlaceHolder}">
</Setter>
</Trigger>
<Trigger Property="Text" Value="">
<Setter Property="Background" Value="{DynamicResource tbPlaceHolder}">
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</TextBox.Style>
</TextBox>
</Grid>
</Border>
<Popup
Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Slide">
<Grid
Name="DropDown"
Width="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border
x:Name="DropDownBorder"
BorderThickness="1"
BorderBrush="{TemplateBinding BorderBrush}">
<ScrollViewer>
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" />
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

有没有觉得头晕?反正我是感觉有点晕吧。我也不是凭空重写出来的,是找的别人写好的示例代码改的,别人的示例代码那可叫真看着晕。现在改成这样已经简单很多了

从上到下,一点点来

第4行,给ComboBox重新定义控件模板,只在当前页面生效,然后下面是模板的内容

第6行,Border用于实现圆角

第10行,好戏开始。这个Grid定义了两列,第一列用于存放下拉框选择的文本,第二列用于存放下拉的箭头

第15行,开始定义下拉的箭头。ToggleButton是用来作为点击后弹出下拉框的按钮,但具体是什么东东?其实自己建一个wpf项目拉一个ToggleButton出来看看就明白了。它跟复选框类似,一般情况有两个值,一个是Checked,一个是UnChecked,但跟复选框不同的是,它只是一个按钮,没有对勾,并且你点击它之后,如果它的状态是Checked,那么它就不会弹起来了,再点一下才能弹起来,想想这个是不是跟下拉框的功能有点相似?下面的Style暂时不说,因为那个是属于ComboBoxToggleButton那块的

第20行,IsChecked,关键的地方。这个值绑定到了一个叫IsDropDownOpen的属性,是双向绑定,绑定的对象是使用这个控件的控件。这什么意思呢?谁会使用这个控件呢?那其实就是ComboBox(TemplatedParent这个属性其实我没太理解透,可能一时间也难以理解透),而如果下拉框是打开的,那么也就意味着isChecked会为True,那么ToggleButton就会是选中的状态。反过来,如果下拉框未打开,那么IsChecked就是未选中状态,这个时候点击ToggleButton,那么IsChecked会为True,由于是双向绑定,所以IsDropDownOpen也会为True,下拉框就自然打开了。不得不说WPF真心强大。

第23行,这个TextBox是用于存放选择后的结果的,也是用于实现可编辑下拉框的。然后嘛,placeholder效果就在这儿实现了。

第50行,Popup,用于弹出一个控件,这里的{TemplateBinding IsDropDownOpen}其实跟上面说的TemplatedParent是一个意思,把IsOpen绑定到了应用了该控件的控件的IsDropDownOpen属性

第65行,StackPanel用于存放下拉框中的每一项,IsItemsHost表示这个控件是否用于Item的容器(好吧其实这个地方我短时间理解不了,只能这么去理解)

OK,这个完毕,下一个,ComboBoxToggleButton

 <Style x:Key="ComboBoxToggleButton" TargetType="ToggleButton">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Path
Panel.ZIndex="1"
x:Name="Arrow"
Grid.Column="1"
Fill="#B1B1B1"
Stroke="#B1B1B1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M0,0L3,3 6,0z">
<Path.RenderTransform>
<ScaleTransform x:Name="stfArrow" CenterY="2"></ScaleTransform>
</Path.RenderTransform>
</Path>
<TextBlock Panel.ZIndex="0"></TextBlock>
</Grid>
<!--</Grid>-->
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Arrow" Property="Fill" Value="Black"></Setter>
<Setter TargetName="Arrow" Property="Stroke" Value="Black"></Setter>
</Trigger>
<EventTrigger RoutedEvent="Checked">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="0:0:0.2"
Storyboard.TargetName="stfArrow"
Storyboard.TargetProperty="ScaleY" From="1"
To="-1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="Unchecked">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation
Duration="0:0:0.2"
Storyboard.TargetName="stfArrow"
Storyboard.TargetProperty="ScaleY" From="-1"
To="1" />
<DoubleAnimation
Duration="0:0:0.2"
Storyboard.TargetName="stfArrow"
Storyboard.TargetProperty="ScaleY" From="-1"
To="1" />
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

很多重复的技术点就跳过

第9行,Path控件用于画出点击下拉框时的那个向下的箭头,其中的Data可能比较难以理解,推荐一篇文章:http://blog.csdn.net/johnsuna/article/details/1885597,我也是在这儿看懂的。

第18行,RenderTransform中的ScaleTransform用于旋转这个Path控件也就是箭头,ScaleTransform有一个属性ScaleY,改为-1时就会180度翻转,可以实现当下拉框被打开时箭头朝上的效果

第22行,呃··怎么不记得有这一行,可能没什么用吧,忽略掉

第45行,注册鼠标经过触发器,经过时把箭头变成黑色

第49行,注册Checked事件触发器,BeginStoryboard开始一段动画,Storyboard创建一个动画,DoubleAnimation创建两个值之间的过渡,TargetName指定给哪个控件应用这个过渡,TargetProperty指定给哪个属性应用,From和To就不用说了。下面的UnChecked跟上面类似。这个主要用于实现点击箭头时的翻转动画。

OK,下一部分

 <Style TargetType="{x:Type ComboBoxItem}">
<Setter Property="RenderTransform">
<Setter.Value>
<ScaleTransform x:Name="stItem"></ScaleTransform>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Grid Background="White" x:Name="spItem" Height="23">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="colImage" Width="20"></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
<ColumnDefinition Width="30"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Margin="0,1" Source="{Binding Image}"></Image>
<Label Grid.Column="1" HorizontalAlignment="Stretch" Content="{Binding CC}" Name="lblItem" VerticalContentAlignment="Center" FontSize="11" HorizontalContentAlignment="Left" Margin="0,1">
</Label>
<Image Cursor="Hand" ToolTip="删除该账号" x:Name="delImage" Visibility="Collapsed" Margin="10,0" Grid.Column="2" Width="9" HorizontalAlignment="Right" Source="Resources/images/deleteAccountNormal.png"></Image>
</Grid>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="ComboBoxItem.MouseEnter" SourceName="spItem">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="spItem" Storyboard.TargetProperty="(Height)" From="23" To="40" Duration="0:0:0.20"></DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="lblItem" Storyboard.TargetProperty="FontSize" From="11" To="14" Duration="0:0:0.20"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<EventTrigger RoutedEvent="ComboBoxItem.MouseLeave" SourceName="spItem">
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="spItem" Storyboard.TargetProperty="(Height)" From="40" To="23" Duration="0:0:0.20"></DoubleAnimation>
<DoubleAnimation Storyboard.TargetName="lblItem" Storyboard.TargetProperty="FontSize" From="14" To="11" Duration="0:0:0.20"></DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
<Trigger Property="IsMouseOver" Value="true">
<Setter TargetName="spItem" Property="Background" Value="#378FCF"></Setter>
<Setter TargetName="lblItem" Property="Foreground" Value="White"></Setter>
<Setter TargetName="colImage" Property="Width" Value="40"></Setter>
<Setter TargetName="delImage" Property="Visibility" Value="Visible"></Setter>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>

终于到最后一个地方了,上面的代码定义了下拉框中的每一项的模板

第10行,定义了一个Grid,三列,第一列是图片,也就是用户头像,第二列是用户名,第三列是删除用户的图标

然后其实下面的就是一堆动画,这个跟上一部分的动画原理类似的。

这些模板定义好之后,直接拖个ComboBox出来就行了,不需要任何设置

这篇文章肯定会有技术错误,毕竟我对WPF并不是太熟,如果有的话请指正