【WPF】一个类似于QQ面板的GroupShelf控件
最近做控件上了瘾,现在把做的一个类似于QQ面板的控件放上来。
【分析】
从整体来看,这个控件应该同ListBox,ListView这类控件一样,是一个ItemsControl,而中间的项,就是它的Item。
因此,为了完成一个这样的控件,至少需要两个东西:
GroupShelf:也就是充当容器角色的控件
GroupShelfItem:即这个控件中的项
其中,GroupShelf需要保证某项的展开同时,其他项被折叠。而GroupShelfItem需要提供Header和Content,同时,需要能支持展开的空能。
【控件的实现】
GroupShelfItem
首先,我们从GroupShelfItem入手,因为它比较单纯,在HeaderedContentControl的基础上提供展开,收缩功能即可:
/// <summary>
/// GroupShelfItem
/// </summary>
public class GroupShelfItem : HeaderedContentControl
{
#region IsExpanded
public bool IsExpanded
{
get { return (bool)GetValue(IsExpandedProperty); }
set { SetValue(IsExpandedProperty, value); }
}
// Using a DependencyProperty as the backing store for IsSelected. This enables animation, styling, binding, etc
public static readonly DependencyProperty IsExpandedProperty = DependencyProperty.Register(
"IsExpanded", typeof(bool), typeof(GroupShelfItem), new PropertyMetadata(false, new PropertyChangedCallback(OnIsExpandedChanged)));
private static void OnIsExpandedChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
GroupShelfItem item = sender as GroupShelfItem;
if (item != null)
{
item.OnIsExpandedChanged(e);
}
}
protected virtual void OnIsExpandedChanged(DependencyPropertyChangedEventArgs e)
{
bool newValue = (bool)e.NewValue;
if (newValue)
{
this.OnExpanded();
}
else
{
this.OnCollapsed();
}
}
#endregion
#region Selection Events
/// <summary>
/// Raised when selected
/// </summary>
public event RoutedEventHandler Expanded
{
add { AddHandler(ExpandedEvent, value); }
remove { RemoveHandler(ExpandedEvent, value); }
}
public static RoutedEvent ExpandedEvent = EventManager.RegisterRoutedEvent(
"Expanded", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(GroupShelfItem));
/// <summary>
/// Raised when unselected
/// </summary>
public event RoutedEventHandler Collapsed
{
add { AddHandler(CollapsedEvent, value); }
remove { RemoveHandler(CollapsedEvent, value); }
}
public static RoutedEvent CollapsedEvent = EventManager.RegisterRoutedEvent(
"Collapsed", RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(GroupShelfItem));
protected virtual void OnExpanded()
{
GroupShelf parentGroupShelf = this.ParentGroupShelf;
if (parentGroupShelf != null)
{
parentGroupShelf.ExpandedItem = this;
}
RaiseEvent(new RoutedEventArgs(ExpandedEvent, this));
}
protected virtual void OnCollapsed()
{
RaiseEvent(new RoutedEventArgs(CollapsedEvent, this));
}
#endregion
#region ExpandCommand
public static RoutedCommand ExpandCommand = new RoutedCommand("Expand", typeof(GroupShelfItem));
private static void OnExecuteExpand(object sender, ExecutedRoutedEventArgs e)
{
GroupShelfItem item = sender as GroupShelfItem;
if (!item.IsExpanded)
{
item.IsExpanded = true;
}
}
private static void CanExecuteExpand(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = sender is GroupShelfItem;
}
#endregion
public GroupShelf ParentGroupShelf
{
get { return ItemsControl.ItemsControlFromItemContainer(this) as GroupShelf; }
}
static GroupShelfItem()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(GroupShelfItem), new FrameworkPropertyMetadata(typeof(GroupShelfItem)));
CommandBinding expandCommandBinding = new CommandBinding(ExpandCommand, OnExecuteExpand, CanExecuteExpand);
CommandManager.RegisterClassCommandBinding(typeof(GroupShelfItem), expandCommandBinding);
}
}
默认的Style
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GroupShelfDemo.Controls">
<Style TargetType="{x:Type local:GroupShelfItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:GroupShelfItem}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel>
<Button DockPanel.Dock="Top"
Content="{TemplateBinding Header}"
ContentTemplate="{TemplateBinding HeaderTemplate}"
ContentTemplateSelector="{TemplateBinding HeaderTemplateSelector}"
ContentStringFormat="{TemplateBinding HeaderStringFormat}"
Command="{Binding Source={x:Static local:GroupShelfItem.ExpandCommand}}"/>
<ContentPresenter x:Name="ContentHost" DockPanel.Dock="Bottom"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
ContentTemplateSelector="{TemplateBinding ContentTemplateSelector}"
ContentStringFormat="{TemplateBinding ContentStringFormat}">
<ContentPresenter.LayoutTransform>
<ScaleTransform x:Name="ContentHostHeightTransform" ScaleY="0.0"/>
</ContentPresenter.LayoutTransform>
</ContentPresenter>
</DockPanel>
</Border>
<ControlTemplate.Resources>
<Storyboard x:Key="OnExpanded">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="ContentHostHeightTransform"
Storyboard.TargetProperty="ScaleY">
<SplineDoubleKeyFrame KeyTime="00:00:00.08" Value="1"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="OnCollapsed">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="ContentHostHeightTransform"
Storyboard.TargetProperty="ScaleY">
<SplineDoubleKeyFrame KeyTime="00:00:00.08" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</ControlTemplate.Resources>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Storyboard="{StaticResource OnExpanded}"/>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard Storyboard="{StaticResource OnCollapsed}"/>
</Trigger.ExitActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
GroupShelfItem提供了一个Command来操作它的展开和收缩。同时,在Expand的时候通知GroupShelf处理。在默认的控件模板中通过按钮来触发这个Command。
GroupShelfPanel
GroupShelf的主要工作就是维护GroupShelfItem展开和收缩时的状态处理。但是,按照WPF的方式,这个布局的工作不应该由它来完成,而是由我们提供一个ItemsPanel给它。所以,在GroupShelf之前,GroupShelfPanel应运而生。
写一个Panel最重要的工作就是重载MeasureOverride和ArrangeOverride两个方法。分析GroupShelfPanel的行为,其实是“指定的孩子填充剩余空间”。就系统提供的Panel来说,DockPanel跟它的行为是最接近的,因为DockPanel提供了LastChildFill的行为。既然如此,我们就打开Reflector,从DockPanel里面“借”点代码过来用用:
public class GroupShelfPanel : Panel
{
/// <summary>
/// 要填充的孩子
/// </summary>
public UIElement ChildToFill
{
get { return (UIElement)GetValue(ChildToFillProperty); }
set { SetValue(ChildToFillProperty, value); }
}
public static readonly DependencyProperty ChildToFillProperty = DependencyProperty.Register(
"ChildToFill", typeof(UIElement), typeof(GroupShelfPanel),
new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.AffectsMeasure));
protected override Size ArrangeOverride(Size arrangeSize)
{
UIElementCollection internalChildren = base.InternalChildren;
int count = internalChildren.Count;
// 未指定ChildToFill,则最后一个child填充
int childToFillIndex = ChildToFill == null ? count - 1 : internalChildren.IndexOf(ChildToFill);
double y = 0.0;
Rect rectForFill = new Rect(0, 0, arrangeSize.Width, arrangeSize.Height);
if (childToFillIndex != -1)
{
// 正序排列ChildToFill之前的元素
for (int i = 0; i < childToFillIndex + 1; i++)
{
UIElement element = internalChildren[i];
if (element != null)
{
Size desiredSize = element.DesiredSize;
Rect finalRect = new Rect(0, y, Math.Max(0.0, arrangeSize.Width), Math.Max(0.0, arrangeSize.Height - y));
if (i < childToFillIndex)
{
finalRect.Height = desiredSize.Height;
y += desiredSize.Height;
element.Arrange(finalRect);
}
else
{
// 留给剩下的元素的面积
rectForFill = finalRect;
}
}
}
y = 0.0;
// 逆序排列ChildToFill之后的元素(包括ChildToFill)
for (int i = count - 1; i > childToFillIndex; i--)
{
UIElement element = internalChildren[i];
if (element != null)
{
Size desiredSize = element.DesiredSize;
Rect finalRect = new Rect(0, arrangeSize.Height - y - desiredSize.Height, Math.Max(0.0, arrangeSize.Width), Math.Max(0.0, desiredSize.Height));
element.Arrange(finalRect);
y += desiredSize.Height;
}
}
rectForFill.Height -= y;
InternalChildren[childToFillIndex].Arrange(rectForFill);
}
return arrangeSize;
}
protected override Size MeasureOverride(Size constraint)
{
UIElementCollection internalChildren = base.InternalChildren;
double num = 0.0;
double num2 = 0.0;
double num3 = 0.0;
double num4 = 0.0;
int index = 0;
int count = internalChildren.Count;
while (index < count)
{
UIElement element = internalChildren[index];
if (element != null)
{
Size availableSize = new Size(Math.Max((double)0.0, (double)(constraint.Width - num3)), Math.Max((double)0.0, (double)(constraint.Height - num4)));
element.Measure(availableSize);
Size desiredSize = element.DesiredSize;
num = Math.Max(num, num3 + desiredSize.Width);
num4 += desiredSize.Height;
}
index++;
}
num = Math.Max(num, num3);
return new Size(num, Math.Max(num2, num4));
}
当然,改动还是比较大的。主要的改动集中在ArrangeOverride上。排列的逻辑应该是:把“要填充的孩子”之前的元素从上到下排列,把“要填充的孩子”之后的元素从下往上排列。剩余的空间都留给这个“要填充的孩子”。而对于MeasureOverride,我们要做的就是去掉DockPanel里面对于Left和Right的判断。(当然,如果考虑到以后要提供两种布局的方向:Horizontal和Vertical的话,还是需要保留一下的)
GroupShelf
上面两个都完成后,GroupShelf的工作就很简单了。它就是在Item的Expand发生变化时,通知别的Item Collapse,然后通知GroupPanel去重新布局。
/// <summary>
/// GroupShelf
/// </summary>
[TemplatePart(Name = "PART_ItemsHost", Type = typeof(GroupShelfPanel))]
public class GroupShelf : ItemsControl
{
private GroupShelfPanel _itemsHost;
#region ExpandedItem
public object ExpandedItem
{
get { return (object)GetValue(ExpandedItemProperty); }
set { SetValue(ExpandedItemProperty, value); }
}
// Using a DependencyProperty as the backing store for SelectedItem. This enables animation, styling, binding, etc
public static readonly DependencyProperty ExpandedItemProperty = DependencyProperty.Register(
"ExpandedItem", typeof(object), typeof(GroupShelf),
new UIPropertyMetadata(null, new PropertyChangedCallback(OnExpandedItemChanged)));
private static void OnExpandedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
GroupShelf shelf = sender as GroupShelf;
if (shelf != null)
{
shelf.OnExpandedItemChanged(e.OldValue, e.NewValue);
}
}
protected virtual void OnExpandedItemChanged(object oldValue, object newValue)
{
GroupShelfItem oldItem = this.ItemContainerGenerator.ContainerFromItem(oldValue) as GroupShelfItem;
GroupShelfItem newItem = this.ItemContainerGenerator.ContainerFromItem(newValue) as GroupShelfItem;
if (oldItem != null)
{
oldItem.IsExpanded = false;
}
if (newItem != null)
{
if (this._itemsHost != null)
{
this._itemsHost.ChildToFill = newItem;
}
}
}
#endregion
static GroupShelf()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(GroupShelf), new FrameworkPropertyMetadata(typeof(GroupShelf)));
}
#region Overrides
protected override void ClearContainerForItemOverride(DependencyObject element, object item)
{
base.ClearContainerForItemOverride(element, item);
}
protected override DependencyObject GetContainerForItemOverride()
{
return new GroupShelfItem();
}
protected override bool IsItemItsOwnContainerOverride(object item)
{
return item is GroupShelfItem;
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this._itemsHost = GetTemplateChild("PART_ItemsHost") as GroupShelfPanel;
}
#endregion
}
一个需要注意的地方是它的模板:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:GroupShelfDemo.Controls">
<Style TargetType="{x:Type local:GroupShelf}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:GroupShelf}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<local:GroupShelfPanel x:Name="PART_ItemsHost" IsItemsHost="True"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
我放置了一个GroupPanel,并且指定了它是ItemsHost,而不是放置ItemsPresenter。这一点是比较重要的,因为只有当ItemsPanel是GroupPanel的时候,才能够有指定孩子填充的效果,因此这个GroupPanel需要是TemplatePart。如果修改了这个模板,换成别的Panel,比如StackPanel,则行为会有所不同。
【使用该控件】
这个控件的使用是非常简单的:
<l:GroupShelf>
<l:GroupShelfItem Header="我的好友">
<ListBox>
<TextBlock Text="好友1"/>
<TextBlock Text="好友2"/>
<TextBlock Text="好友3"/>
<TextBlock Text="好友4"/>
</ListBox>
</l:GroupShelfItem>
<l:GroupShelfItem Header="我的同学">
<ListBox>
<TextBlock Text="同学1"/>
<TextBlock Text="同学2"/>
<TextBlock Text="同学3"/>
<TextBlock Text="同学4"/>
</ListBox>
</l:GroupShelfItem>
<l:GroupShelfItem Header="我的同事">
<ListBox>
<TextBlock Text="同事1"/>
<TextBlock Text="同事2"/>
<TextBlock Text="同事3"/>
<TextBlock Text="同事4"/>
</ListBox>
</l:GroupShelfItem>
<l:GroupShelfItem Header="我的家人">
<ListBox>
<TextBlock Text="家人1"/>
<TextBlock Text="家人2"/>
<TextBlock Text="家人3"/>
<TextBlock Text="家人4"/>
</ListBox>
</l:GroupShelfItem>
<l:GroupShelfItem Header="我的老师">
<ListBox BorderThickness="1" BorderBrush="Black">
<TextBlock Text="老师1"/>
<TextBlock Text="老师2"/>
<TextBlock Text="老师3"/>
<TextBlock Text="老师4"/>
</ListBox>
</l:GroupShelfItem>
</l:GroupShelf>
代码下载http://files.cnblogs.com/RMay/AccordianDemo.rar
注:我用的是.Net 3.5 Sp1,如果是3.0-3.5请删除模板中的ContentStringFormat相关的东西
修正一下:
在GroupShelf的模板中,直接写
<local:GroupShelfPanel IsItemsHost="True" ChildToFill="{TemplateBinding ExpandedItem}"/>
即可,而在GroupShelf中的相关字段和方法都可以删除,UI和逻辑解耦。
转载于:https://www.cnblogs.com/RMay/archive/2008/08/22/1273927.html
【WPF】一个类似于QQ面板的GroupShelf控件相关推荐
- 2017-5-5 QQ面板 (用户控件、timer控件,轮询实现聊天功能)
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...
- 【转】WPF从我炫系列3---内容控件的用法
今天我来给大家讲解WPF中内容控件的用法,在WPF中的内容控件,通俗的讲,是指具有Content属性的控件,在content属性里面可以嵌套放置任意其他类型的控件,但是Content只能接受单个元素, ...
- 【愚公系列】2023年07月 WPF+上位机+工业互联 002-WPF布局控件
文章目录 前言 一.WPF布局控件 1.无边框设计 2.理解布局 2.1 WPF的布局处理 2.1 布局原则 2.3 布局过程 3.布局控件 3.1 Grid控件 3.1.1 属性 3.1.2 案例 ...
- WPF 使用依赖属性(DependencyProperty) 定义用户控件中的Image Source属性
原文:WPF 使用依赖属性(DependencyProperty) 定义用户控件中的Image Source属性 如果你要自定义一个图片按钮控件,那么如何在主窗体绑定这个控件上图片的Source呢? ...
- WPF中自定义的DataTemplate中的控件,在Window_Loaded事件中加载机制初探
原文:WPF中自定义的DataTemplate中的控件,在Window_Loaded事件中加载机制初探 最近因为项目需要,开始学习如何使用WPF开发桌面程序.使用WPF一段时间之后,感觉WPF的开发思 ...
- WPF编程,通过KeyFrame 类型制作控件线性动画的一种方法。
WPF编程,通过KeyFrame 类型制作控件线性动画的一种方法. 原文: WPF编程,通过KeyFrame 类型制作控件线性动画的一种方法. 版权声明:我不生产代码,我只是代码的搬运工. https ...
- 想建一个带分隔条的label 控件;
想建一个带分隔条的label 控件: Delphi / Windows SDK/API http://www.delphi2007.net/DelphiBase/html/delphi_2006120 ...
- 一个自定义的安卓验证码输入框控件、银行卡归属类型查询
一个自定义的安卓验证码输入框控件.银行卡归属类型查询. GitHub:https://github.com/longer96/VerifyCode Dependency Gradle dependen ...
- 一个类似于QQ语音聊天时的拖拽移动悬浮小球
原文出处: Booooooooom 闲来无事,分享一个最近在某个地方借鉴的一个demo(原谅我真的忘了在哪里看到的了,不然也就贴地址了)这个demo的逻辑思路并不是很难,推敲一下,很快就能理解, ...
最新文章
- 百度:2020年十大科技趋势
- 面试:说说你对 ThreadLocal 的认识?
- 如何利用TensorFlow.js部署简单AI版「你画我猜」
- 复习----使用链表实现栈(后进先出)及迭代
- 深入学习二叉树(二) 线索二叉树
- 大数据开发初学者学习路线_初学者的Web开发路线图
- maya为什么不能导出fbx_Maya在操作中最容易出现的几个问题,现在注意还来得及...
- 线程安全的几种单例模式
- 学习PLC到底要不要买PLC?
- pic单片机c语言計數,单片机教程:PIC单片机C语言程序设计(三)
- @【基础测绘计算】(坐标正反算)
- 教你打造 Win7 中的高清设备图标
- 荣耀畅玩5a android5.0,华为荣耀畅玩5A有几个版本?华为荣耀5A各版本区别对比介绍...
- 计算机机房通风,机房为什么要装通风系统?
- PostgreSQL12.3——pgAdmin4表格的创建
- C语言课设销售管理系统设计
- RHCA-红帽认证架构师
- stm32中断源有哪些_143条 超详细整理STM32单片机学习笔记(必看)
- 手把手玩转KVM虚拟化--KVM网络管理
- 【OCR】文字检测:传统算法、CTPN、EAST
热门文章
- CMSimple内容管理系统
- PHP常见缓存技术分析,让重复的调用缓存以加快速度
- Node.js: 如何退出node命令或者node server
- Swift中文教程(九) 类与结构
- Objective-C 2.0 with Cocoa Foundation --- 2,从Hello,World!开始
- Python_多项式拟合
- 神奇的python(二)之python打包成应用程序
- spring IOC容器 Bean 管理——基于注解方式
- Python——pip安装报错:is not a supported wheel on this platform
- 【AI视野·今日CV 计算机视觉论文速览 第202期】Thu, 20 May 2021