×
Namespaces

Variants
Actions

在Windows Phone上建颜色选择器

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

代码示例
兼容于
文章
WS_YiLunLuo 在 13 Jan 2012 创建
最后由 hamishwillee 在 03 Jul 2013 编辑

你可以自File:WPColorPicker.zip下载代码。

Contents

简介

很多程序都需要让用户选择一个颜色。我在网上看到的大多数颜色选择器都很简单,就是列出一组颜色,供用户选择。这对于某些场合已经够用了,例如给每个任务使用一种颜色标识。但是也有很多场合,需要更高级的功能。

Windows Phone支持32位色彩,而提供一组颜色列表通常只会列出10来种颜色,为了让用户能选择更丰富的色彩,列表形式显然是不行的。使用过Expression Studio和Visual Studio的人都知道,这些产品提供了一个强大的颜色选择器。

Expression Studio Color Picker.PNG


今天我们就来看看,如何在Windows Phone上创建一个功能类似的颜色选择器。在这个过程中,我们会看到如何创建custom control,如何自定义控件的外观,以及如何使用常见的数学公式。

WPColorPicker.png

UserControl和Custom Control

很多过来人都喜欢开发系统组件,但是Windows Phone不允许你开发系统组件,只有微软和像诺基亚这样的OEM才有这个权限。然而事实上,在这个互联网时代,很多时候为了让其它程序调用你的程序的功能,根本不需要通过系统组件实现。你可以使用web service(推荐REST,因为更通用),开发类库,等形式。本文不会涉及到web service,只是看看开发类库中常见的两种形式:UserControl和custom control的异同。

从功能上讲,UserControl通常用于某个程序内部,而不是给其它程序使用。UserControl的主要作用在于将过于冗长的代码分散成几个小文件,每个小文件代表一个组件,它们共同组成了应用程序。有些情况下UserControl也可以被复用,但是复用代码并不是UserControl最根本的作用。

Custom control是真正用来做组件的,事实上在系统提供的类库中,除了UserControl之外,所有继承自Control的类,都可以被认为是custom control。当你创建一个custom control时,这个控件就像系统提供的Button这类控件一样,可以被其它程序调用,只要它们引用了你的类库。Custom control同时还支持sytle和control template,让使用者可以自定义其外观。

从开发方式上讲,UserControl很简单,Visual Studio已经提供了现成的模板:

WPUserControl.png

在你通过该模板新建一个UserControl后,你会得到一个XAML文件和一个code benind,基本上就和一般的XAML文件完全一致。在这里我们就不详细说明了。在本文提供的示例代码中,你会在WPColorPickerTest项目下找到一个名为ColorListUserControl的UserControl,实现了简单通过列表选择颜色的功能。

Custom control稍微繁琐一点。Visual Studio目前没有提供现成的模板。一个custom control的显示由 control template负责,用户可以随意修改style和control template。所以custom control并没有XAML,也不存在code behind的概念。当然有些控件依赖于特定的界面元素,在这种情况下可以使用GetTemplateChild方法来获取template中有名字的元素。

为了创建custom control,首先要创建一个类,这个类必须继承自Control或某个Control的子类(但不能是UserControl)。然后在项目根目录下建一个名叫Themes文件夹,在这个文件夹下创建一个名为Generic.xaml的文件。注意你必须使用Themes和Generic.xaml这样的名字。这个XAML文件是没有code behind的,它的根元素为ResouceDictionary,在这里你可以添加一个default style,就是没名字的style,会自动被应用到所有未显示指定style的同类控件上。在style中你可以设置Template以及其它各种属性,就像在普通的场合中使用style完全一样。

<ResourceDictionary
 
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
 
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
 
xmlns:WPColorPicker="clr-namespace:WPColorPicker">
 
<Style TargetType="WPColorPicker:ColorPicker">
 
<Style.Setters>
 
<Setter Property="Template">
 
<Setter.Value>
 
<ControlTemplate TargetType="WPColorPicker:ColorPicker">
 
</ControlTemplate>
 
</Setter.Value>
 
</Setter>
 
</Style.Setters>
 
</Style>
 
</ResourceDictionary>

在这里你可以任意修改style和control template。但是请注意,这只是你自己给你的控件提供的默认外观,使用控件的人可以将该外观改得面目全非,就好像你可以修改Button的外观,让它显示一把枪,按下按钮时枪的扳手转动,一样。


为了使用默认的control template,你需要在构造函数中设置DefaultStyleKey。此外,刚才说过有些控件可能需要一些特定的UI元素才能正常工作,所以要使用GetTemplateChild访问control template中的元素。这件事是在OnApplyTemplate中做的:

    public class ColorPicker : Control
 
{
 
private Rectangle _saturationRectangle;
 
 
 
public ColorPicker()
 
{
 
this.DefaultStyleKey = typeof(ColorPicker);
 
}
 
 
 
public override void OnApplyTemplate()
 
{
 
base.OnApplyTemplate();
 
 
 
this._saturationRectangle = this.GetTemplateChild("saturationRectangle") as Rectangle;
 
if (this._saturationRectangle == null)
 
{
 
throw new ArgumentNullException("saturationRectangle");
 
}
 
}
 
}

之后你可以添加任意代码实现你的控件,例如通过代码为某些关键UI元素添加事件处理程序(而不能通过XAML关联)。

有关更多信息,请参考示例代码中的WPColorPicker项目,这里实现了一个类似于Expression Studio中的颜色选择器的控件,使用的就是custom control。

使用UserControl和custom control的方法和一般控件(例如官方toolkit中的那些)一样。指定一个namespace:

xmlns:WPColorPicker="clr-namespace:WPColorPicker;assembly=WPColorPicker"

然后就可以用了:

<WPColorPicker:ColorPicker x:Name="colorPicker"/>

RGB和HSB

接下来我们就开始介绍如何制作颜色选择器吧。为此首先还是要简单了解一下RGB和HSB。你可以自Wikipedia找到详细说明。这边我们简单解释一下HSB的理念。

RGB相信每个人都很清楚了,事实上RGB色表可以被视为一个立方体,八个顶点分别是黑,红,粉红,蓝,绿,黄,白,青。将这个立方体如Wikipedia上的图示(为了不盗链,请点击这里)以斜角45度放置,再映射到水平面上,就形成了一个正六边形,每个颜色也被映射到了六边形内的一点。

我们定义色度C(chroma)为从六边形中心(O)点到某颜色所在的点(P)所组成的连线的长度,与O到六边形边缘上的点(P’)组成的连线的长度,的比例,也就是OP/OP’。

接下来,给六边形画一个外接圆,定义色调H(hue)为圆周上的点和中心点的连线与水平线的夹角,范围是0到360。具体计算方式可参考Wikipedia上的公式

然后,定义值V(value)为RGB的最大值(Max(R,G,B)),V越小,颜色就越黑(黑色的RGB都为0)。通常V也被称作亮度B(brightness)。

最后定义色饱和度(saturation)S为C/V。S的值越小,颜色就越白。

若是你小时候玩过古老的彩电,以上概念应该很熟悉才对。Hue, Saturation, Brightness在一起,被统称为HSB。由上述定义可知,HSB是可以和RGB一一对应的,除非S或B的值为0,这时不管H的值是多少,都一律是白色(S=0)或黑色(B=0)。

反之,已知HSV,也可以推算出RGB。Wikipedia上提供了详细的公式:C = V * S, 还有几步在这里这里

实现公式

在创建控件前,首先我们要实现RGB与HSB互转的公式。为了简单起见,我们写一个console程序,并且暂时不care检查数值范围。这个程序基本上完全是根据Wikipedia上的公式写的:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace RgbHsbConverter
{
class Program
{
static void Main(string[] args)
{
TestToRGB(new HSB() { H = 60, S = 1, B = 1 });
 
// 如果转回HSB后H不再是45,那是正常的。S = 0意味着不管H是多少,总是白色。
TestToRGB(new HSB() { H = 45, S = 0, B = 1 });
TestToRGB(new HSB() { H = 81, S = 0.74, B = 0.57 });
TestToRGB(new HSB() { H = 35, S = 0.36, B = 0.38 });
TestToRGB(new HSB() { H = 183, S = 0.18, B = 0.81 });
TestToRGB(new HSB() { H = 294, S = 0.64, B = 0.27 });
 
// 如果转回HSB后H不再是45,那是正常的。B = 0意味着不管H是多少,总是黑色。
TestToRGB(new HSB() { H = 165, S = 1, B = 0 });
 
TestToHSB(new RGB() { R = 1, G = 0.14, B = 1 });
TestToHSB(new RGB() { R = 0.51, G = 0.35, B = 0.28 });
TestToHSB(new RGB() { R = 0.54, G = 0.39, B = 0.76 });
TestToHSB(new RGB() { R = 0, G = 0.25, B = 0.12 });
TestToHSB(new RGB() { R = 1, G = 0.78, B = 0.24 });
TestToHSB(new RGB() { R = 0.97, G = 0.45, B = 0.62 });
 
Console.ReadKey();
}
 
private static void TestToRGB(HSB hsb)
{
Console.WriteLine("Converting:");
Console.WriteLine("H: " + hsb.H);
Console.WriteLine("S: " + hsb.S);
Console.WriteLine("B: " + hsb.B);
RGB rgb = ConvertToRGB(hsb);
Console.WriteLine("Result:");
Console.WriteLine("R: " + rgb.R);
Console.WriteLine("G: " + rgb.G);
Console.WriteLine("B: " + rgb.B);
HSB hsb2 = ConvertToHSB(rgb);
Console.WriteLine("Converting back:");
Console.WriteLine("H: " + hsb2.H);
Console.WriteLine("S: " + hsb2.S);
Console.WriteLine("B: " + hsb2.B);
Console.WriteLine("Done.\r\n");
}
 
private static void TestToHSB(RGB rgb)
{
Console.WriteLine("Converting:");
Console.WriteLine("R: " + rgb.R);
Console.WriteLine("G: " + rgb.G);
Console.WriteLine("B: " + rgb.B);
HSB hsb = ConvertToHSB(rgb);
Console.WriteLine("Result:");
Console.WriteLine("H: " + hsb.H);
Console.WriteLine("S: " + hsb.S);
Console.WriteLine("B: " + hsb.B);
RGB rgb2 = ConvertToRGB(hsb);
Console.WriteLine("Converting back:");
Console.WriteLine("H: " + rgb2.R);
Console.WriteLine("S: " + rgb2.G);
Console.WriteLine("B: " + rgb2.B);
Console.WriteLine("Done.\r\n");
}
 
private static RGB ConvertToRGB(HSB hsb)
{
double chroma = hsb.S * hsb.B;
double hue2 = hsb.H / 60;
double x = chroma * (1 - Math.Abs(hue2 % 2 - 1));
double r1 = 0d;
double g1 = 0d;
double b1 = 0d;
if (hue2 >= 0 && hue2 < 1)
{
r1 = chroma;
g1 = x;
}
else if (hue2 >= 1 && hue2 < 2)
{
r1 = x;
g1 = chroma;
}
else if (hue2 >= 2 && hue2 < 3)
{
g1 = chroma;
b1 = x;
}
else if (hue2 >= 3 && hue2 < 4)
{
g1 = x;
b1 = chroma;
}
else if (hue2 >= 4 && hue2 < 5)
{
r1 = x;
b1 = chroma;
}
else if (hue2 >= 5 && hue2 <= 6)
{
r1 = chroma;
b1 = x;
}
double m = hsb.B - chroma;
return new RGB()
{
R = r1 + m,
G = g1 + m,
B = b1 + m
};
}
 
private static HSB ConvertToHSB(RGB rgb)
{
double r = rgb.R;
double g = rgb.G;
double b = rgb.B;
 
double max = Max(r, g, b);
double min = Min(r, g, b);
double chroma = max - min;
double hue2 = 0d;
if (chroma != 0)
{
if (max == r)
{
hue2 = (g - b) / chroma;
}
else if (max == g)
{
hue2 = (b - r) / chroma + 2;
}
else
{
hue2 = (r - g) / chroma + 4;
}
}
double hue = hue2 * 60;
if (hue < 0)
{
hue += 360;
}
double brightness = max;
double saturation = 0;
if (chroma != 0)
{
saturation = chroma / brightness;
}
return new HSB()
{
H = hue,
S = saturation,
B = brightness
};
}
 
private static double Max(double d1, double d2, double d3)
{
if (d1 > d2)
{
return Math.Max(d1, d3);
}
return Math.Max(d2, d3);
}
 
private static double Min(double d1, double d2, double d3)
{
if (d1 < d2)
{
return Math.Min(d1, d3);
}
return Math.Min(d2, d3);
}
}
 
public struct RGB
{
public double R;
public double G;
public double B;
}
 
public struct HSB
{
public double H;
public double S;
public double B;
}
}

运行这个程序,你会发现结果稍有误差。转换回HSB之后,小数点后的第二位开始可能与原始数值不同。事实上Wikipedia上提供的公式是HSB最初的定义(它的发明甚至在我们中绝大多数人出生之前!),的确存在一些误差。在几十年的时间内,该公式得到了优化,例如brightness通常会取(Min(R,G,B)+Max(R,G,B))/2,而不是简单地Max(R,G,B)。Expression Studio正是使用了优化后的公式。

不过今天我们不讨论优化,因为大多数Windows Phone程序都是针对消费者开发的,对他们而言,小数点后第二位有点误差在允许范围之内。上述公式已经可以让他们选择丰富多彩的颜色了。如果你是针对专业设计人式开发程序,就要考虑使用优化的公式了。

实现控件

接下来我们把console程序中的代码迁移到Windows Phone。大多数代码都可以复用。不过在生产环境中,除了简单实现算法,我们也必须考虑其它各种因素,例如检查每个值的范围。由于代码比较长,就不贴在文章里了。你们可以参考本文附带的示例程序。有几个注意点:

使用设计模式

在生产环境中要开发程序,我们推荐使用良好的设计模式。在Windows Phone中常见的一种设计模式是MVVM。示例代码中的ColorPickerViewModel类是一个view model,提供view所需要的数据,并封装了控件的实现逻辑,例如上述公式。控件本身有ColorPicker类提供,这是一个view。View通常不应该包括实现逻辑,而只包括显示逻辑。这样做的好处是当你需要修改显示时,不需要担心可能会破坏实现逻辑。此外,若是你并不是在开发控件,你的view也会包括XAML。在XAML中你可以通过数据绑定的方式将控件的属性直接绑定到view model中,从而不需要写代码去修改控件属性。你可以从这里找到更多关于MVVM的信息。

检查参数范围

在生产环境中,检查参数范围是很重要的,否则很容易产生bug。例如,Hue的值必须在0到360之间,所以在设置Hue的值时必须做好检查:

        public double Hue
 
{
 
get { return this._hue; }
 
set
 
{
 
if (value < 0 || value > 360)
 
{
 
throw new ArgumentOutOfRangeException("value");
 
}
 
 
 
if (value != this._hue)
 
{
 
this._hue = value;
 
this.ConvertToRGB();
 
this.NotifyPropertyChanged("Hue");
 
}
 
}
 
}

尽量多用style

会写正规的HTML程序的人都知道,样式应该放在一个单独的CSS文件中,而不是嵌入在HTML文件本身,更不能直接写在某个元素的标签上。同样的道理也适用于XAML。只要可能,请尽量使用style,而不要直接在XAML的某个元素上定义属性,除非该属性和逻辑有关,而不仅仅涉及到显示和布局。使用Expression Blend可以从某个控件中提取style,当然你也可以手工优化工具生成的代码。

将需要用户操作的元素设置得大一些

Windows Phone不支持鼠标,只支持触摸屏。手指通常没有鼠标那样精确,所以推荐将需要用户操作的元素设置得大一些,否则很难用手指点中。如果你比较我们的示例程序和Expression Studio中的颜色选择器,会发现用于拖拽的椭圆和slider上的thumb明显变大了。

更多参考资料

This page was last modified on 3 July 2013, at 06:51.
102 page views in the last 30 days.
×