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类,与单个的样式设置对应,里面包括字体、字体大小、字体颜色、样式应用的起始索引、应用的总长度,具体的类实现如下:
TextFormat
///
<summary>
///
文本格式
///
</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
>
{
}
之后是依赖属性的定义,除了之前提到过的文本格式集合以及当前选择的模式之外,还包括对外提供的文本和是否允许编辑选项,同时在文本格式集合以及当前选择的模式改变时进行文本格式化处理,依赖属性的定义如下:
Dependency Properties
#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
由于使用了TemplatePart定义实现了界面与控件行为逻辑之间的解耦,那么自然的需要在运行时拿到在样式中所定义的TemplatePart。这里通过重载OnApplyTemplate方法来实现子控件的查找,以及相应的处理事件的附加,实现代码如下:
OnApplyTemplate
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();
}
在实际测试时,这里发现了一个问题,当我在上面的方法中设置TextBox的Text属性后,对应控件中注册的TextChanged事件并没有触发,经过多次的调试,发现似乎只有在控件Load完之后进行的Text属性赋值操作,才会引起TextChanged事件;然而测试了WPF中的TextBox,并没有发现有一样的问题,在网上也没有发现有类似的讨论,只好作罢。最后通过注册TextBox的Loaded事件,并在里面重新进行了文本格式的处理。如果有对这个问题有所了解的朋友,希望能够给我答疑解惑~
接下来是最重要的文本格式的处理,这部分的具体思路是这样的:
1. 判断是否处于展示模式;
2. 清楚原有的Inlines集合;
3. 将TextFormats集合中的元素按照StartIndex从小到大进行排序;
4. 循环处理TextFormats集合中的元素;
5. 如果当前格式覆盖了前面的格式(StartIndex>LastIndex),则抛出异常;
6. 如果当前格式与前面的格式之间有空隙,则将空隙单独设置为默认格式;
7. 按照当前格式进行设置;
8. 循环结束,如果还有剩余的文本,则全部用默认格式处理。
最后附上完整的代码以及默认的控件样式:
EditorBox
[TemplatePart(Name
=
"
PART_Editor
"
, Type
=
typeof
(TextBox))]
[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;
}
}
默认样式
<
ResourceDictionary
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方面的技术讨论。