Silverlight开发中的疑难杂症-控件设计篇-如何实现一个EditorBox
EditorBox就是一个具有编辑和展示两种状态的TextBox,因为在最近的工作和学习项目中,多次碰到了需要将一个TextBox以编辑和展示两种不同的样式存在,于是就想到了制作一个这样的控件来提高生产效率,同时也尝试一下自定义控件的开发。该控件包括如下功能:
l 能在编辑和展示状态之间切换;
l 可以设置是否能够编辑;
l 在展示状态双击控件,进入到编辑状态(如果支持编辑);
l 在编辑状态,输入完文本,回车后进入展示状态;
l 提供一个Text属性供外部使用;
l 能够设置展示状态下文本样式(设置指定区间的文本的字体、字体大小、字体颜色等);
基本的实现思路是这样的:首先,定义两个TemplatePart,分别为TextBox和TextBlock类型,用来表示编辑框跟展示框,文本格式的处理通过动态计算所设置的格式,然后添加多个Run元素来实现;然后,定一个两个TemplateVisualState,用来实现编辑状态和展示状态之间的切换。附加的Attribute声明如下:
[TemplatePart(Name = "PART_Editor", Type = typeof(TextBox))]
[TemplatePart(Name = "PART_View", Type = typeof(TextBlock))]
[TemplateVisualState(Name = "Edit", GroupName = "CommonStates")]
[TemplateVisualState(Name = "View", GroupName = "CommonStates")]
为了使控件使用者能够对样式进行更好的控制,这里定义了一个TextFormat类,与单个的样式设置对应,里面包括字体、字体大小、字体颜色、样式应用的起始索引、应用的总长度,具体的类实现如下:
/// 文本格式
/// </summary>
public class TextFormat : DependencyObject
{
/// <summary>
/// 字体
/// </summary>
public FontFamily FontFamily
{
get { return (FontFamily)GetValue(FontFamilyProperty); }
set { SetValue(FontFamilyProperty, value); }
}
public static readonly DependencyProperty FontFamilyProperty =
DependencyProperty.Register("FontFamily", typeof(FontFamily), typeof(TextFormat), new PropertyMetadata(default(FontFamily)));
/// <summary>
/// 字体大小
/// </summary>
public double FontSize
{
get { return (double)GetValue(FontSizeProperty); }
set { SetValue(FontSizeProperty, value); }
}
public static readonly DependencyProperty FontSizeProperty =
DependencyProperty.Register("FontSize", typeof(double), typeof(TextFormat), new PropertyMetadata(10.0));
/// <summary>
/// 字体颜色
/// </summary>
public Brush Foreground
{
get { return (Brush)GetValue(ForegroundProperty); }
set { SetValue(ForegroundProperty, value); }
}
public static readonly DependencyProperty ForegroundProperty =
DependencyProperty.Register("Foreground", typeof(Brush), typeof(TextFormat), new PropertyMetadata(new SolidColorBrush(Colors.Black)));
/// <summary>
/// 样式应用的起始索引
/// </summary>
public int StartIndex
{
get { return (int)GetValue(StartIndexProperty); }
set { SetValue(StartIndexProperty, value); }
}
public static readonly DependencyProperty StartIndexProperty =
DependencyProperty.Register("StartIndex", typeof(int), typeof(TextFormat), new PropertyMetadata(0));
/// <summary>
/// 样式应用的长度
/// </summary>
public int Length
{
get { return (int)GetValue(LengthProperty); }
set { SetValue(LengthProperty, value); }
}
public static readonly DependencyProperty LengthProperty =
DependencyProperty.Register("Length", typeof(int), typeof(TextFormat), new PropertyMetadata(0));
}
/// <summary>
/// 文本格式集合
/// </summary>
public class TextFormatCollection : ObservableCollection<TextFormat>
{
}
之后是依赖属性的定义,除了之前提到过的文本格式集合以及当前选择的模式之外,还包括对外提供的文本和是否允许编辑选项,同时在文本格式集合以及当前选择的模式改变时进行文本格式化处理,依赖属性的定义如下:
/// <summary>
/// 文本格式集合
/// </summary>
public TextFormatCollection TextFormats
{
get { return (TextFormatCollection)GetValue(TextFormatsProperty); }
set { SetValue(TextFormatsProperty, value); }
}
public static readonly DependencyProperty TextFormatsProperty =
DependencyProperty.Register("TextFormats", typeof(TextFormatCollection), typeof(EditorBox), new PropertyMetadata(new PropertyChangedCallback(OnTextFormatsChanged)));
/// <summary>
/// 文本
/// </summary>
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(EditorBox), new PropertyMetadata(string.Empty));
/// <summary>
/// 是否允许编辑
/// </summary>
public bool CanEdit
{
get { return (bool)GetValue(CanEditProperty); }
set { SetValue(CanEditProperty, value); }
}
public static readonly DependencyProperty CanEditProperty =
DependencyProperty.Register("CanEdit", typeof(bool), typeof(EditorBox), new PropertyMetadata(true));
/// <summary>
/// 当前模式
/// </summary>
public Mode CurrentMode
{
get { return (Mode)GetValue(CurrentModeProperty); }
set { SetValue(CurrentModeProperty, value); }
}
public static readonly DependencyProperty CurrentModeProperty =
DependencyProperty.Register("CurrentMode", typeof(Mode), typeof(EditorBox), new PropertyMetadata(Mode.View, new PropertyChangedCallback(OnCurrentModeChanged)));
#region Property Change Handler
private static void OnTextFormatsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
(obj as EditorBox).OnTextFormatsChanged(e.OldValue as TextFormatCollection, e.NewValue as TextFormatCollection);
}
/// <summary>
/// 文本格式设置改变时,重新计算文本格式
/// </summary>
/// <param name="oldCollection"></param>
/// <param name="newCollection"></param>
protected virtual void OnTextFormatsChanged(TextFormatCollection oldCollection, TextFormatCollection newCollection)
{
if (oldCollection != null)
{
oldCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(TextFormats_CollectionChanged);
}
if (newCollection != null)
{
newCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(TextFormats_CollectionChanged);
}
//集合改变时重新计算文本格式
ProcessTextFormat();
}
/// <summary>
/// 集合项改变时,重新计算文本格式
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void TextFormats_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
ProcessTextFormat();
}
private static void OnCurrentModeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
(obj as EditorBox).OnCurrentModeChanged((Mode)e.OldValue, (Mode)e.NewValue);
}
/// <summary>
/// 从编辑模式切换到视图模式,进行文本格式计算
/// </summary>
/// <param name="oldMode"></param>
/// <param name="newMode"></param>
protected virtual void OnCurrentModeChanged(Mode oldMode, Mode newMode)
{
if (newMode == Mode.View)
{
ProcessTextFormat();
}
}
#endregion
#endregion
由于使用了TemplatePart定义实现了界面与控件行为逻辑之间的解耦,那么自然的需要在运行时拿到在样式中所定义的TemplatePart。这里通过重载OnApplyTemplate方法来实现子控件的查找,以及相应的处理事件的附加,实现代码如下:
{
base.OnApplyTemplate();
AttachToVisualTree();
}
/// <summary>
/// 获取模板中的子控件,并附加处理
/// </summary>
void AttachToVisualTree()
{
//获取模板中的子控件
_editor = GetChildControl<TextBox>(PART_Editor);
_viewer = GetChildControl<TextBlock>(PART_View);
if (_editor != null)
{
//由于Silverlight的TextChanged事件只在Load之后才会触发,所以需要在Load之后初始化文本格式
_editor.Loaded += new RoutedEventHandler(InitTextFormat);
//按下回车回到视图模式
_editor.KeyDown += new KeyEventHandler(EnterViewMode);
//设置绑定关系
_editor.Text = this.Text;
this.SetBinding(TextProperty, new Binding("Text") { Source = _editor, Mode = BindingMode.TwoWay });
}
ProcessTextFormat();
}
在实际测试时,这里发现了一个问题,当我在上面的方法中设置TextBox的Text属性后,对应控件中注册的TextChanged事件并没有触发,经过多次的调试,发现似乎只有在控件Load完之后进行的Text属性赋值操作,才会引起TextChanged事件;然而测试了WPF中的TextBox,并没有发现有一样的问题,在网上也没有发现有类似的讨论,只好作罢。最后通过注册TextBox的Loaded事件,并在里面重新进行了文本格式的处理。如果有对这个问题有所了解的朋友,希望能够给我答疑解惑~
接下来是最重要的文本格式的处理,这部分的具体思路是这样的:
1. 判断是否处于展示模式;
2. 清楚原有的Inlines集合;
3. 将TextFormats集合中的元素按照StartIndex从小到大进行排序;
4. 循环处理TextFormats集合中的元素;
5. 如果当前格式覆盖了前面的格式(StartIndex>LastIndex),则抛出异常;
6. 如果当前格式与前面的格式之间有空隙,则将空隙单独设置为默认格式;
7. 按照当前格式进行设置;
8. 循环结束,如果还有剩余的文本,则全部用默认格式处理。
最后附上完整的代码以及默认的控件样式:
[TemplatePart(Name = "PART_View", Type = typeof(TextBlock))]
[TemplateVisualState(Name = "Edit", GroupName = "CommonStates")]
[TemplateVisualState(Name = "View", GroupName = "CommonStates")]
public class EditorBox : Control
{
public const string PART_Editor = "PART_Editor";
public const string PART_View = "PART_View";
public const string VisualState_Edit = "Edit";
public const string VisualState_View = "View";
/// <summary>
/// 模式
/// </summary>
public enum Mode
{
/// <summary>
/// 查看模式
/// </summary>
View,
/// <summary>
/// 编辑模式
/// </summary>
Edit
}
#region Private Fields
private TextBox _editor;
private TextBlock _viewer;
#endregion
#region Dependency Properties
/// <summary>
/// 文本格式集合
/// </summary>
public TextFormatCollection TextFormats
{
get { return (TextFormatCollection)GetValue(TextFormatsProperty); }
set { SetValue(TextFormatsProperty, value); }
}
public static readonly DependencyProperty TextFormatsProperty =
DependencyProperty.Register("TextFormats", typeof(TextFormatCollection), typeof(EditorBox), new PropertyMetadata(new PropertyChangedCallback(OnTextFormatsChanged)));
/// <summary>
/// 文本
/// </summary>
public string Text
{
get { return (string)GetValue(TextProperty); }
set { SetValue(TextProperty, value); }
}
public static readonly DependencyProperty TextProperty =
DependencyProperty.Register("Text", typeof(string), typeof(EditorBox), new PropertyMetadata(string.Empty));
/// <summary>
/// 是否允许编辑
/// </summary>
public bool CanEdit
{
get { return (bool)GetValue(CanEditProperty); }
set { SetValue(CanEditProperty, value); }
}
public static readonly DependencyProperty CanEditProperty =
DependencyProperty.Register("CanEdit", typeof(bool), typeof(EditorBox), new PropertyMetadata(true));
/// <summary>
/// 当前模式
/// </summary>
public Mode CurrentMode
{
get { return (Mode)GetValue(CurrentModeProperty); }
set { SetValue(CurrentModeProperty, value); }
}
public static readonly DependencyProperty CurrentModeProperty =
DependencyProperty.Register("CurrentMode", typeof(Mode), typeof(EditorBox), new PropertyMetadata(Mode.View, new PropertyChangedCallback(OnCurrentModeChanged)));
#region Property Change Handler
private static void OnTextFormatsChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
(obj as EditorBox).OnTextFormatsChanged(e.OldValue as TextFormatCollection, e.NewValue as TextFormatCollection);
}
/// <summary>
/// 文本格式设置改变时,重新计算文本格式
/// </summary>
/// <param name="oldCollection"></param>
/// <param name="newCollection"></param>
protected virtual void OnTextFormatsChanged(TextFormatCollection oldCollection, TextFormatCollection newCollection)
{
if (oldCollection != null)
{
oldCollection.CollectionChanged -= new NotifyCollectionChangedEventHandler(TextFormats_CollectionChanged);
}
if (newCollection != null)
{
newCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(TextFormats_CollectionChanged);
}
//集合改变时重新计算文本格式
ProcessTextFormat();
}
/// <summary>
/// 集合项改变时,重新计算文本格式
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void TextFormats_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
ProcessTextFormat();
}
private static void OnCurrentModeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
(obj as EditorBox).OnCurrentModeChanged((Mode)e.OldValue, (Mode)e.NewValue);
}
/// <summary>
/// 从编辑模式切换到视图模式,进行文本格式计算
/// </summary>
/// <param name="oldMode"></param>
/// <param name="newMode"></param>
protected virtual void OnCurrentModeChanged(Mode oldMode, Mode newMode)
{
if (newMode == Mode.View)
{
ProcessTextFormat();
}
}
#endregion
#endregion
public EditorBox()
{
this.DefaultStyleKey = typeof(EditorBox);
TextFormats = new TextFormatCollection();
//通过附加属性增加鼠标双击事件
this.SetValue(MouseEventHelper.MouseDoubleClickProperty, new MouseButtonEventHandler(MouseDoubleClick));
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
AttachToVisualTree();
}
/// <summary>
/// 获取模板中的子控件,并附加处理
/// </summary>
void AttachToVisualTree()
{
//获取模板中的子控件
_editor = GetChildControl<TextBox>(PART_Editor);
_viewer = GetChildControl<TextBlock>(PART_View);
if (_editor != null)
{
//由于Silverlight的TextChanged事件只在Load之后才会触发,所以需要在Load之后初始化文本格式
_editor.Loaded += new RoutedEventHandler(InitTextFormat);
//按下回车回到视图模式
_editor.KeyDown += new KeyEventHandler(EnterViewMode);
//设置绑定关系
_editor.Text = this.Text;
this.SetBinding(TextProperty, new Binding("Text") { Source = _editor, Mode = BindingMode.TwoWay });
}
ProcessTextFormat();
}
/// <summary>
/// 第一次加载时,初始化文本格式
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void InitTextFormat(object sender, RoutedEventArgs e)
{
ProcessTextFormat();
}
/// <summary>
/// 进入视图模式
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void EnterViewMode(object sender, KeyEventArgs e)
{
//按回车进入查看状态
if (e.Key == Key.Enter)
{
VisualStateManager.GoToState(this, VisualState_View, false);
CurrentMode = Mode.View;
}
}
/// <summary>
/// 双击进入编辑模式(如果允许编辑)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
//更换VisualStatus 双击进入编辑状态
if (CanEdit)
{
VisualStateManager.GoToState(this, VisualState_Edit, false);
CurrentMode = Mode.Edit;
}
}
/// <summary>
/// 处理文本格式
/// </summary>
void ProcessTextFormat()
{
if (_viewer != null && CurrentMode == Mode.View && this.TextFormats != null)
{
_viewer.Inlines.Clear();
//先按照StartIndex排序
var formats = this.TextFormats.OrderBy((format) => { return format.StartIndex; }).ToList();
int startIndex = 0;
int length = 0;
for (int i = 0; i < formats.Count; i++)
{
if (startIndex >= this.Text.Length)
break;
TextFormat format = formats[i];
Run run = new Run();
//重叠部分
if (format.StartIndex < startIndex)
{
throw new Exception("StartIndex不能重叠");
}
//不要求格式部分
else if (format.StartIndex > startIndex)
{
length = Math.Min(format.StartIndex - startIndex, this.Text.Length - startIndex);
run.Text = this.Text.Substring(startIndex, length);
startIndex += length;
i--;
}
//要求格式部分
else if (format.StartIndex == startIndex)
{
length = Math.Min(format.Length, this.Text.Length - startIndex);
run.Text = this.Text.Substring(startIndex, length);
if (format.FontFamily != null)
run.FontFamily = format.FontFamily;
run.FontSize = format.FontSize;
run.Foreground = format.Foreground;
startIndex += length;
}
_viewer.Inlines.Add(run);
}
//处理尾部的剩余部分
if (startIndex < this.Text.Length)
{
Run run = new Run();
length = this.Text.Length - startIndex;
run.Text = this.Text.Substring(startIndex, length);
_viewer.Inlines.Add(run);
}
}
}
/// <summary>
/// 获取指定名字的控件,并转换为对应类型
/// </summary>
/// <typeparam name="T">控件类型</typeparam>
/// <param name="ctrlName">控件名</param>
/// <returns>转换后的控件</returns>
protected T GetChildControl<T>(string ctrlName) where T : class
{
T ctrl = GetTemplateChild(ctrlName) as T;
return ctrl;
}
}
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:YQL.CustomControlLibs" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" >
<Style TargetType="local:EditorBox">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:EditorBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Edit">
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="PART_View" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="PART_Editor" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="View">
<Storyboard>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="PART_View" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames BeginTime="00:00:00" Duration="00:00:00.0010000" Storyboard.TargetName="PART_Editor" Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Grid>
<TextBlock x:Name="PART_View"/>
<local:NumericBox x:Name="PART_Editor" Visibility="Collapsed" MaxFractionDigits="4"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
http://www.bbniu.com/matrix/ShowApplication.aspx?id=70
再此特别感谢bbniu提供的Silverlight Host作品秀平台:)
欢迎大家访问www.bbniu.com进行关于WPF和Silverlight方面的技术讨论。