Namespaces

Variants
Actions

Please note that as of October 24, 2014, the Nokia Developer Wiki will no longer be accepting user contributions, including new entries, edits and comments, as we begin transitioning to our new home, in the Windows Phone Development Wiki. We plan to move over the majority of the existing entries over the next few weeks. Thanks for all your past and future contributions.

在Windows Phone上建颜色选择器

From 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 03:51.
153 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×