×
Namespaces

Variants
Actions

在Windows Phone上制作一个字体编辑器

From Nokia Developer Wiki
Jump to: navigation, search
WP Metro Icon UI.png
SignpostIcon WP7 70px.png
Article Metadata

代码示例
兼容于
文章
WS_YiLunLuo 在 10 May 2012 创建
最后由 hamishwillee 在 16 Jul 2013 编辑

Contents

简介

正如其它手机一样,Windows Phone适合于消费者,通常Windows Phone上的程序都偏向于显示内容,而不是内容的编辑。例如,你的程序可能会显示天气预报,但是不会提供让管理员修改天气预报信息的功能。

可是,还是有这样一些场景,用户不可避免地需要编辑内容。本文就演示其中的一个场景,制作一个程序让用户编辑字体,包括文本,字体,字号,颜色等等。为了让场景更完整,我们设想一个图片编辑器,允许用户在图片上添加文字。但是出于篇幅考虑,我们的侧重点在于文字的编辑,而不在于图片。

WPFontEditor.png

你可以自File:WPFontEditor.zip下载本文提供的示例。

本文会使用到我们之前制作的颜色选择器。有关详细信息,请参考http://www.developer.nokia.com/Community/Wiki/%E5%9C%A8Windows_Phone%E4%B8%8A%E5%BB%BA%E9%A2%9C%E8%89%B2%E9%80%89%E6%8B%A9%E5%99%A8。

基本思路

我们使用标准的MVVM设计模式。在view model中定义各种属性,例如字体,字号,等等。在view中使用数据绑定将UI上的元素绑定至数据源。这样一来,当用户修改UI元素的值的时候,view model中的属性也会自动修改。

实现

为了简单起见,我们制作一个UserControl,名为CaptionEditor,用于为一幅画添加编辑标题。

单项数据绑定

在这个UserControl中,我们定义一个Canvas,用来显示图片和标题文字。注意到用于显示标题文字的TextBlock中使用了数据绑定,将文本,字体,还有字号,全部绑定至数据源。在这里我们只需要单项绑定,我们会提供其它控件修改这些数值。单向绑定适用于不需要让用户操作的控件。

此外,我们还将文字的颜色通过element name的形式绑定到一个名为fontColorPicker的颜色选择器所选中的颜色上,这个颜色选择器就是我们在 在Windows Phone上建颜色选择器 上面介绍的那个。

可以看到,在Windows Phone中,我们既可以将数据绑定到一些后台代码的数据源上,也可以绑定到当前页面的某个元素上。请不要将思路局限在绑定就是用来在一张表格中显示数据库中的数据,那种思路是10年前的Windows Forms和ASP.NET Web Forms时代的事了。如今新型框架的数据绑定机制已经远比当初来得灵活。

        <Canvas
x:Name="previewCanvas"
Background="Transparent"
SizeChanged="PreviewCanvas_SizeChanged"
ManipulationDelta="PreviewCanvas_ManipulationDelta">
<Image x:Name="previewImage" Source="{Binding ImageSource}" Stretch="Uniform"/>
<TextBlock
x:Name="previewTextBlock"
Text="{Binding Caption}"
FontFamily="{Binding CaptionFont, Converter={StaticResource fontFamilyConverter}}"
FontSize="{Binding CaptionFontSize}"
Foreground="{Binding PickedBrush, ElementName=fontColorPicker}">
<TextBlock.RenderTransform>
<TranslateTransform x:Name="textTranslate"/>
</TextBlock.RenderTransform>
</TextBlock>
</Canvas>

双向绑定

在页面右边我们还有一个Grid,这里提供了大量需要用户输入的控件,来修改view model中的值。这就需要用到双向绑定了。可以看到,在Windows Phone中,只需要轻轻添加Mode=TwoWay,就可以实现双向数据绑定,这是非常方便的。

还请注意,不仅仅是TextBox之类的标准控件可以实现数据绑定,我们自己写的控件,例如ChooseFontComponent的SelectedFont属性,也可以实现数据绑定。 虽然双向绑定使用非常简单,可是请注意一旦使用了双向绑定,每一次UI元素的值发生变化,都会更改数据源,而若是你的代码中有监听数据源发生变化,那部分代码也会被调用。若是这些代码中你做了一些复杂的操作,有些情况下你可能不希望这段代码经常性被调用,这时候就要限制双向绑定的使用了。通常我们会针对一些不是经常性发生变化的属性使用双向绑定,例如TextBox的Text属性就只有在用户输入完毕文本后才会改变。而对于那些经常性需要修改的属性,例如Slider的Value,或者我们的ColorPicker的PickedBrush(用户每次拖动选择颜色的球都会修改这个属性),就要考虑是否限制双向绑定的使用了。

事实上不仅仅是双向绑定,许多其它场景也是如此。也许一个功能实现起来非常简单,但是选择这个简单的方法可能会对性能,安全,等等造成影响。所以不要想当然地认为这一块功能实现了就完事了。作为一个开发人员,我们需要全方位地考虑各种可能性。

        <Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.4*"/>
<ColumnDefinition Width="0.6*"/>
</Grid.ColumnDefinitions>
 
<TextBlock
x:Name="captionTextBlock"
Style="{StaticResource editTextBlockStyle}"
Text="Caption:"/>
<TextBox
x:Name="captionTextBox"
Text="{Binding Caption, Mode=TwoWay}"
Style="{StaticResource editTextBoxStyle}"/>
<TextBlock
x:Name="fontTextBlock"
Style="{StaticResource editTextBlockStyle}"
Text="Font:"
Grid.Row="1"/>
<Border
x:Name="fontComponent"
Style="{StaticResource fontComponentBorderStyle}"
MouseLeftButtonDown="FontChooser_TouchDown">
<TextBlock
x:Name="fontText"
Style="{StaticResource fontTextStyle}"
Text="{Binding CaptionFont, Mode=TwoWay, Converter={StaticResource fontFamilyConverter}}"
FontFamily="{Binding CaptionFont, Converter={StaticResource fontFamilyConverter}}"/>
</Border>
<TextBlock
x:Name="fontSizeTextBlock"
Style="{StaticResource editTextBlockStyle}"
Text="Font Size:"
Grid.Row="2"/>
<TextBox
x:Name="fontSizeText"
Grid.Row="2"
Text="{Binding CaptionFontSize, Mode=TwoWay}"
Style="{StaticResource editTextBoxStyle}"
InputScope="Number"/>
<WPCaptionEditor:ChooseFontComponent
x:Name="fontChooser"
Grid.RowSpan="3"
Grid.ColumnSpan="2"
SelectedFont="{Binding Text, ElementName=fontText, Mode=TwoWay}"
Visibility="Collapsed"
SelectedFontChanged="FontChooser_SelectedFontChanged"/>
<TextBlock
x:Name="fontColorTextBlock"
Style="{StaticResource editTextBlockStyle}"
Text="Font Color:"
Grid.Row="3"/>
<WPColorPicker:ColorPicker
x:Name="fontColorPicker"
Grid.Row="4"
Grid.Column="1"
/>
</Grid>

Converter

当绑定对象的属性类型和数据源中的数据类型不同时,需要使用converter来进行类型转换。例如,我们的项目中就写了一个FontFamilyConverter,用于在FontFamily和string 这两种类型之间进行转换。

每个converter都需要实现IValueConverter接口。若是你只需要支持单项绑定,那么只要实现Convert方法即可。若是想要支持双向绑定,还需要实现ConvertBack方法。

也许你会觉得为了一个小小的绑定就要写一个类这不值得,但是请注意converter是可以被复用的。任何用到数据绑定的场景,只要你需要在FontFamily和string之间做出类型转换,你都可以使用这个类,而不需要写一个新的类。若是你的项目中有100个地方需要同样的类型转换,你只需要一个converter类。所以从宏观角度来看,工作量并没有提升多少。

    public class FontFamilyConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if ((value == null) || !(value is FontFamily))
{
return "Arial";
}
return ((FontFamily)value).Source;
}
 
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (!(value is string) || string.IsNullOrEmpty((string)value))
{
return new FontFamily("Arial");
}
return new FontFamily((string)value);
}
}

为了使用converter,首先将它作为一个resource添加到页面上。这一步也只需要做一次。

       <WPCaptionEditor:FontFamilyConverter x:Key="fontFamilyConverter"/>

然后就可以使用Converter={StaticResource fontFamilyConverter}这样的语法应用它了。这一步对于每个需要用到converter的属性要分别做一次。

view model

说了那么多和数据绑定相关的话题,没有一个数据源可不行。我们使用标准的view model模式定义数据源。View model通常是一个普通的CLR类,定义了一系列的属性,并且实现了INotifyPropertyChanged接口。这样一来,当数据源的值发生修改时,UI元素上对应的属性也会自动更改。当然,实现INotifyPropertyChanged之后代码可能变得有点冗长,不过目前这是没有办法的。

    public class ViewModel : INotifyPropertyChanged
{
private Uri _imageSource;
public Uri ImageSource
{
get
{
return this._imageSource;
}
set
{
if (this._imageSource != value)
{
this._imageSource = value;
this.NotifyPropertyChange("ImageSource");
}
}
}
 
private string _caption;
public string Caption
{
get { return this._caption; }
set
{
if (this._caption != value)
{
this._caption = value;
this.NotifyPropertyChange("Caption");
}
}
}
 
private FontFamily _captionFont = new FontFamily("Arial");
public FontFamily CaptionFont
{
get { return this._captionFont; }
set
{
if (this._captionFont != value)
{
this._captionFont = value;
this.NotifyPropertyChange("CaptionFont");
}
}
}
 
private Brush _captionForeground;
public Brush CaptionForeground
{
get { return this._captionForeground; }
set
{
if (this._captionForeground != value)
{
this._captionForeground = value;
this.NotifyPropertyChange("CaptionForeground");
}
}
}
 
private double _captionFontSize = 72d;
public double CaptionFontSize
{
get { return this._captionFontSize; }
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException("value", "Invalid font size.");
}
if (this._captionFontSize != value)
{
this._captionFontSize = value;
this.NotifyPropertyChange("CaptionFontSize");
}
}
}
 
private double _captionPositionX;
public double CaptionPositionX
{
get { return this._captionPositionX; }
set
{
if (this._captionPositionX != value)
{
this._captionPositionX = value;
this.NotifyPropertyChange("CaptionPositionX");
}
}
}
 
private double _captionPositionY;
public double CaptionPositionY
{
get { return this._captionPositionY; }
set
{
if (this._captionPositionY != value)
{
this._captionPositionY = value;
this.NotifyPropertyChange("CaptionPositionY");
}
}
}
 
public event PropertyChangedEventHandler PropertyChanged;
 
protected void NotifyPropertyChange(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this.PropertyChanged, new PropertyChangedEventArgs(propertyName));
}
}
}

用手指移动元素

到了这里,我们的字体编辑器算是完成了。不过为了让这个场景显得更真实一点,我们将字体编辑器应用于图片的标题编辑场景。在这个场景中,一个常见的功能是让用户用手指拖动标题文字,改变它的位置。

事实上这个功能的实现也非常简单。我们可以处理Canvas的ManipulationDelta事件,并且修改TextBlock的TranslateTransform。请注意我们推荐修改TranslateTransform,而不是Canvas.Left/Top属性,因为那更通用。若是你想要让图片和文字根据周边环境自动调整大小,你可以使用Grid取代Canvas,但是TranslateTransform依然通用(不过Canvas.Left/Top就不能用了)。事实上当你修改Canvas.Left/Top时,Canvas在后台也是在修改TranslateTransform。

        /// <summary>
/// Reposition the preview text.
/// </summary>
private void PreviewCanvas_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
this.textTranslate.X += e.DeltaManipulation.Translation.X;
this.textTranslate.Y += e.DeltaManipulation.Translation.Y;
}

若是你想保存这些修改,可以提供一个方法,在这里通过TransformToVisual来获取TextBlock相对于Image的坐标变幻。这将返回一个MatrixTransform,它的Matrix属性中的TranslateX/Y分别提供了x和y方向上的偏移量。关于矩阵和坐标变幻有很多很多可以说的,它们在图形学以及量子力学中有着广泛的应用。限于篇幅,我们这边就不详细讨论了。

下面代码演示了如何获取x/y方向上的偏移量,并且除以图片的实际大小,将这个比例保存到view model中。保存比例而不是实际大小的原因是帮助你在其它不同大小的环境中重现同样的场景。

        public void Update()
{
MatrixTransform transform = this.previewTextBlock.TransformToVisual(this.previewImage) as MatrixTransform;
ViewModel viewModel = this.DataContext as ViewModel;
viewModel.CaptionForeground = this.fontColorPicker.PickedBrush;
if (transform != null && viewModel != null)
{
// Convert to relative coordinate.
viewModel.CaptionPositionX = transform.Matrix.OffsetX / this.previewImage.ActualWidth;
viewModel.CaptionPositionY = transform.Matrix.OffsetY / this.previewImage.ActualHeight;
}
this.Visibility = System.Windows.Visibility.Collapsed;
}

其它组件

你会注意到我们使用了两个非系统组件:ChooseFontComponent和ColorPicker。ColorPicker我们在http://www.developer.nokia.com/Community/Wiki/%E5%9C%A8Windows_Phone%E4%B8%8A%E5%BB%BA%E9%A2%9C%E8%89%B2%E9%80%89%E6%8B%A9%E5%99%A8中间有详细说明,这边不再重复。而ChooseFontComponent是一个简单的UserControl,它的作用是显示一些字体供用户选择,详细信息请参考示例代码。

总结

本文描述了如何制作一个字体编辑器。看上去似乎是一个比较复杂的场景,但其实可以归结为几点:

  • 使用MVVM模式
  • 使用数据绑定
  • 使用manipulation事件和TranslateTransform
  • 制作自定义控件,例如ColorPicker

事实上我们在写这个示例的过程中发现,绝大多数的精力是放在制作ColorPicker上的。很多时候制作一个可复用的组件往往要比实现一个场景更花时间。所幸的是,Windows Phone以及它的toolkit已经提供了大量的控件给我们使用,因此我们可以省下很多很多精力。

最后再次提醒大家,作为开发人员思路一定要开阔,绝对不要以为数据绑定就是用于在一个表格中显示数据库的数据,或者用一个form更改数据库中的数据。事实上就数据绑定这样一个小小的功能,也有很广泛的用途。

This page was last modified on 16 July 2013, at 10:21.
98 page views in the last 30 days.
×