导读:
本文转自
http://book.csdn.net/bookfiles/591/10059119376.shtml
第2章 基本画刷
标准窗口的内部,被称为客户区(client area)。正是在窗口的这一区域,显示文字、图形、控件,并在此接收用户的输入。
之前范例所创建的窗口,其客户区可能被填上白色,这是因为白色是客户区默认的颜色。你可以使用微软windows操作系统的控制面板(Control Panel)来设定你自己的系统颜色,以彰显你的美学品味或独特风格。更严肃点的原因,有人可能觉得黑底白字会看得更清楚。如果你是这样的人,你大概会希望更多开发者注意到你的需要,且重视你对颜色的自主选择权。
WPF的颜色被封装成Color结构(structure),定义在System.Window.Media命名空间中。和一般的图形环境一样,Color结构使用红、绿、蓝三原色来表达颜色。这三种原色(primary)分别被简称为R、G、B,用这三原色定义出来的三维空间,就是RGB颜色空间(color space)。
Color结构包含名为R、G、B的三个可读写property,它们的类型都是byte。这三种property的值范围从0到255。当这三个property皆为0时,就是黑色;当这三个property皆为255时,就是白色。
除了这三原色之外,Color结构还包括一个“alpha channel”,其property名称为A。所谓“alpha channel”是用来控制颜色的“不透明”(opacity)程度,值越小表示越透明(transparent),0表示完全透明,而255表示不透明。
和所有的结构体一样,Color具有一个无参数(parameterless)的构造函数,但是此构造函数产生的颜色,其A、R、G、B property都是设为0,这样的颜色不但是黑色,也是完全透明的。想要让它变成看得见的颜色,你的程序可以手动地设定这四个property,如同下面的范例所示:
Color cir = new Color();
clr.A = 255;
clr.R = 255;
clr.G = 0;
clr.B = 255;
最后得到的颜色是不透明的洋红色(magenta)。
此Color结构也包含了几个静态的方法,让你可以用一行代码创建Color对象。此方法需要三个byte类型的参数:
Color clr = Color.FromRgb(r, g, b)
最后得到的颜色,其中A的值是255。你也可以自己指定alpha值:
Color clr = Color.FromArgb(a, r, g, b)
由各一个byte的红绿蓝所组成的RGB颜色空间,也被称为sRGB颜色空间,这里的“s”是standard(标准)的意思。sRGB颜色空间将显示点阵图像的惯用做法予以正规化,从扫描仪到电脑显示器,再到数码相机都适用。当用来在屏幕上显示颜色时,sRGB原色的值一般会和“从显卡送到显示器”的电流电压信号成正比。
然而在其他的设备上,颜色相当不适合用sRGB来表现。比方说,如果一个特定的打印机有能力比一般电脑屏幕更绿,这样的绿怎么能用屏幕绿色最大值的255来表达呢?
正是从这方面考虑,有一些其他的RGB颜色空间也被定义出来。WPF的Color结构也支持这些另类颜色空间的一种,那就是scRGB颜色空间,这种颜色空间以前常被称为sRGB64,因为原色是用64位的值来表达的。在Color结构中,scRGB原色其实是被储存成单精度(single-precision)的浮点数。想要容纳scRGB颜色空间,Color结构包含四个主要的property,类型都是float,分别为ScA、ScR、ScG、ScB。这些property和A、R、G、B property会相互影响,改变G property也会造成ScG property的改变,反之亦然。
当G property为0,ScG property也会为0;当G property为255,ScG property就会为1。在这个范围之内,关系并非是线性的,如表2-1所示。
表2-1
scG |
G |
<= 0 |
0 |
0.1 |
89 |
0.2 |
124 |
0.3 |
149 |
0.4 |
170 |
0.5 |
188 |
0.6 |
203 |
0.7 |
218 |
0.8 |
231 |
0.9 |
243 |
>= 1.0 |
255 |
ScR与R之间的关系,ScB与B之间的关系,以及ScG与G之间的关系,也都是一样的。ScG的值可以小于0或者大于1,以容纳超出显示器和sRGB数字范围的颜色。
现在常用的阴极射线管(Cathode Ray Tube)并没有以线性的方式显示光线。光线的强度(intensity, I)和送到显示器的电压(voltage, V),两者之间是幂次关系,如下面的式子所示:
I=V g
这里的伽玛(gamma)的值,受到显示器和环境光的影响。但是以一般常用的显示器和视觉状况来说,gamma的值通常介于2.2和2.5之间(sRGB标准假定这个值为2.2)。
对于光线的强度,人类视觉感知能力也是非线性的,感知能力大致和光强度的1/3次方成正比。幸好,人类感知的非线性和CRT的非线性彼此相互抵消一部分,所以sRGB原色(这和显示器的电压成正比),在感知上差不多可以算是线性。也就是说,RGB的值如果为80-80-80(十六进制表示法),大致对应到人类认为的“中度灰阶”。这就是为什么sRGB会如此广泛的作为标准使用的原因之一。
scRGB原色却是故意设计成“和光强度成线性关系”的,所以scG和G的关系如下:
其中,指数2.2是sRGB标准所假定的gamma值。请注意,上面的式子是逼近式,不是等式。当值不高时,会比较不精确。至于alpha channel的关系,则比较简单:
你可以利用下面的静态方法,以scRGB原色来创建Color对象:
Color clr = Color.FromScRgb(a, r, g, b);
这些参数是浮点数,可以小于0或者大于1。
System.Windows.Media,也包含了一个名为Colors(注意是复数)的类,此类型包含了141个静态的只读property,这些property的名称都是好记的颜色引用方式,从AliceBlue和AntiqueWhite到Yellow和YellowGreen。比方说:
Color clr = Colors.PapayaWhip;
这些颜色的名称和Web浏览器常用的颜色名称是一样的,但Transparent property是个例外,此property返回一个alpha值为0的Color对象。Colors类中其他的140个property
所返回的Color对象,其alpha值都是255。
程序可以设定Background(背景)property,来改变客户区的背景颜色。这个property是Window从Control继承来的。然而,你不可以把Background设定成一个Color对象,而应将Background设定为更多元化的Brush(画刷)对象。
画刷在WPF中使用得相当广泛,所以我们必须早一点注意到它。Brush本身是一个抽象类,如下面的类层次图所示:
Object
DispatcherObject (abstract)
DependencyObject
Freezable (abstract)
Animatable (abstract)
Brush (abstract)
GradientBrush (abstract)
LinearGradientBrush
RadialGradientBrush
SolidColorBrush
TileBrush (abstract)
DrawingBrush
ImageBrush
VisualBrush
只有Brush的子类实例才能用来设定Window对象的Background property。所有和Brush相关的类都放在System.Window.Media命名空间。本章稍后将讨论SolidColorBrush类(单色画刷)和两个继承自GradientBrush的类(渐变画刷)。
顾名思义,SolidColorBrush正是最简单的画刷,只使用单一颜色。在第1章后面的程序中,你可以改变客户区的颜色,做法是用using编译指示符包含System.Windows.Media,然后在此Window类的构造函数中,加入下面的代码片段:
Color clr = Color.FromRgb(0, 255, 255);
SolidColorBrush brush = new SolidColorBrush(clr);
Background = brush;
它造成背景变成青色(cyan)。当然,这三行代码可以缩写成一行:
Background = new So1idCo1orBrush(Co1or. FromRgb(0, 255, 255));
SolidColorBrush也具有一个无参数的构造函数,和一个名为Color的property。让你在创建画刷对象之后可以设定或者改变画刷的颜色。比方说:
SolidColorBrush brush = new SolidColorBrush();
brush.Color = Color.FromRgb(128, 0, 128);
下面的程序执行时,会依据“鼠标指针靠近窗口中心的程度”,来改变客户区的背景颜色。此程序利用using指示符将System.Window.Media命名空间加进来,本书后面大部分的程序也都会用到这个命名空间。
VaryTheBackground.cs
//--------------------------------------------------
// VaryTheBackground.cs (c) 2006 by Charles Petzold
//--------------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.VaryTheBackground
{
public class VaryTheBackground : Window
{
SolidColorBrush brush = new SolidColorBrush(Colors.Black);
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new VaryTheBackground());
}
public VaryTheBackground()
{
Title = "Vary the Background";
Width = 384;
Height = 384;
Background = brush;
}
protected override void OnMouseMove(MouseEventArgs args)
{
double width = ActualWidth
- 2 * SystemParameters.ResizeFrameVerticalBorderWidth;
double height = ActualHeight
- 2 * SystemParameters.ResizeFrameHorizontalBorderHeight
- SystemParameters.CaptionHeight;
Point ptMouse = args.GetPosition(this);
Point ptCenter = new Point(width / 2, height / 2);
Vector vectMouse = ptMouse - ptCenter;
double angle = Math.Atan2(vectMouse.Y, vectMouse.X);
Vector vectEllipse = new Vector(width / 2 * Math.Cos(angle),
height / 2 * Math.Sin(angle));
Byte byLevel = (byte) (255 * (1 - Math.Min(1, vectMouse.Length /
vectEllipse.Length)));
Color clr = brush.Color;
clr.R = clr.G = clr.B = byLevel;
brush.Color = clr;
}
}
}
当你向客户区移动鼠标时,背景变成较亮的灰色。当鼠标超出“客户区为边界的假想椭圆”时,背景会变成黑色。
这一切都是发生在override版本的OnMouseMove方法中,而且只要鼠标在程序的客户区内移动,OnMouseMove方法就会被持续调用。因为一系列问题,导致这个方法有些复杂。首先,这个方法要计算客户区尺寸,但除非客户区里有实际的东西,否则我们无法得知客户区的尺寸。除了自己算,没有别的好的办法来解决这个问题。这个方法一开始使用窗口的ActualWidth和ActualHeight property,然后减去边框和标题栏的尺寸(利用SystemParameters类的静态property,可以取得这些尺寸)。
这里调用MouseEventArgs的GetPosition方法,来取得鼠标的位置,然后将这个Point对象存在ptMouse中,这个位置和客户区的中央点之间是有一点距离的。中央点也是一个Point对象,名为ptCenter。然后将ptMouse减ptCenter。如果你查看一下Point结构的文档,你将会发现两个Point相减会得到一个Vector对象,我们这里将此Vector对象命名为vectMouse。就数学上来说,向量(vector)包括“量”(magnitude)和“方向”(direction)。vectMouse的量就是ptCenter和ptMouse之间的距离。你可以从Vector结构的Length property来取得这个“量”;你可以利用Vector对象的X和Y property,得知“方向”,代表从原点(0,0)指向(X,Y)的方向。在这个例子中,vectMouse.X等于是ptMouse.X减去ptCenter.X,Y也是类似的式子。
Vector对象的方向也可以表示为角度。Vector结构包含名为AngleBetween的静态方法,用来计算两个Vector对象的夹角。VaryTheBackground程序的OnMouseMove方法根据vectMouse的Y和X property比值,以反正切(inverse tangent)函数计算出角度。此角度是从水平轴开始顺时针方向转动,所得到的夹角,以弧度为单位(一周的弧度是2PI)。接着使用刚刚得到的角度,计算另一个代表“从客户端中心点到客户区内切椭圆的点”之间距离的Vecotr对象。灰度和“这两个向量的比率”成正比。
OnMouseMove方法从SolidColorBrush对象的Color property获得一个Color对象,然后设定好适当的灰度之后,再将Color对象设定回SolidColorBrush的Color property,让画刷具有新的值。
当看到这个程序运行时,你可能会吃一惊。显然,每次只要brush一有改变,客户区就会被重绘,但这一切都是幕后进行的。这里之所以会有动态的反应,是因为Brush继承自Freezable类,而Freezable类实现了一个名为Changed的事件(event),Brush对象只要一有改变,这个事件就会被触发。所以只要一改变brush,背景就会被重画。
WPF底层大量使用Changed事件和类似机制,以实现动画(animation)和其他特性。
正如Colors类提供了141个静态只读的property一样,Brushes(也是复数)类也提供了141个静态只读的property,名称和Color的property都一样,但是Brushes的property返回的是SolidColorBrush对象。你可以用下面的方式设定Background:
Background = new SolidColorBrush(Colors.PaleGoldenrod);
也可以改用下面的方式:
Background = Brushes.PaleGoldenrod;
虽然这两行代码都可以把窗口背景填上特定的颜色,但是这两种做法其实有一点儿差异,像VaryTheBackground这样的程序就可以感受到此差异。下面的语句中,将字段(field)定义:
SolidColorBrush brush = new SolidColorBrush(Colors.Black);
改成如下语句:
SolidColorBrush brush = Brushes.Black;
重新编译并执行,现在会弹出一个“Invalid Operation Exception”(无效操作异常)消息框,详细信息是“无法设定‘#FF000000’对象的property,因为此对象为只读状态”。问题发生在OnMouseMove方法的最后一条语句,这里试图设定画刷的Color property。(异常消息的单引号中出现的十六进制数,是目前Color property的值。)
利用Brushes所取得的SolidColorBrush对象是处于冻结(frozen)状态,也就是说,不能再被改变。就像Changed事件一样,Freezable实现了冻结,而Brush的冻结正是从这里继承而来的。如果Freezable对象的CanFreeze property是true,可以调用Freeze方法来实现对象的冻结和不可变动。IsFrozen property如果变成true,就表示(对象)已经被冻结。将对象冻结,可以提高效率,因为被冻结的对象不会被改变,所以不需要监控。冻结的Freezable对象还可以在不同的线程之间共享,没有被冻结的Freezable对象则不行。虽然
无法将冻结的对象解冻(unfreeze),但是你可以做出一个没冻结的复制版本。下面的代码可以定义VaryTheBackground中的brush字段(field):
SolidColorBrush brush = Brushes.Black.Clone() ;
如果你想看到这141个画刷出现在同一个窗口的客户区,FlipThroughTheBrushes程序可以达成你的愿望,你可以用上下箭头来改变画刷。
FlipThroughTheBrushes.cs
//------------------------------------------------------
// FlipThroughTheBrushes.cs (c) 2006 by Charles Petzold
//------------------------------------------------------
using System;
using System.Reflection;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.FlipThroughTheBrushes
{
public class FlipThroughTheBrushes : Window
{
int index = 0;
PropertyInfo[] props;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new FlipThroughTheBrushes());
}
public FlipThroughTheBrushes()
{
props = typeof(Brushes).GetProperties(BindingFlags.Public |
BindingFlags.Static);
SetTitleAndBackground();
}
protected override void OnKeyDown(KeyEventArgs args)
{
if (args.Key == Key.Down || args.Key == Key.Up)
{
index += args.Key == Key.Up ? 1 : props.Length - 1;
index %= props.Length;
SetTitleAndBackground();
}
base.OnKeyDown(args);
}
void SetTitleAndBackground()
{
Title = "Flip Through the Brushes - " + props[index].Name;
Background = (Brush) props[index].GetValue(null, null);
}
}
}
此程序使用reflection(反射)来取得Brushes类的成员。构造函数第一行使用“typeof(Brushes)”来得到Type类的对象。Type类定义了一个方法,名为GetProperties,并返回PropertyInfo对象的数组,数组内的每个元素都对应到Brushes类里的一个property。调用GetProperties时,以BinddingFlags为参数,程序就可以清楚地把自己限制在Brushes公开和静态的property上。因为Brushes的property本来就全部都是public和static,这个例子不需要这样的限制,但是这么做也不会有什么不好的影响。
在构造函数和override版本的OnKeyDown方法中,程序都调用了SetTitleAndBackground,以便将Title property和Backgound property设定为Brushes类的某个成员。“props[0].Name”会返回一个字符串,这是类第一个property的名称,也就是“AliceBlue”。“props[0].GetValue(null, null)”返回实际的SolidColorBrush对象。这里的GetValue方法需要两个null参数:通常第一个参数是property所在的对象,因为Brushes是一个静态property,所以没有对应的对象,因此传入null;第二个参数只有在property是indexer时才有必要。
System.Windows命名空间具有SystemColors类,其作用类似于Colors和Brushes,只具有静态的只读property,返回Color值和SolidColorBrush对象。这些设定存储在Windows 注册表(registry)中。利用此类,可以得知目前用户的颜色喜好(preferences)。比方说,SystemColors.WindowColor用来表示用户对于客户区的颜色喜好,而SystemColors. WindowTextColor是用户对于客户区文字的颜色喜好。SystonColors.WindowBrush和SystemColors.WindowTextBrush则是返回对应颜色的SolidColorBrush对象。对于大多数的真实应用程序来说,应该使用这些颜色,可以达到统一、协调的视觉效果。
从SystemColors返回的画刷对象都是冻结的。你的程序可以改变下面的画刷:
Brush brush = new SystemColorBrush(SystemColors.WindowColor) ;
但是不能改变下面的画刷:
Brush brush = SystemColors.WindowBrush;
只有继承自Freezable类的对象,才可以被冻结。至于Color对象,则没有冻结不冻结的问题,因为Color是个结构体(structure)。
如果不用单色画刷,可以改用渐变(gradient)画刷,将两种(或多种)颜色混合,逐渐改变。一般说来,渐变画刷是高级的程序主题,但是对于WPF来说,创建渐变画刷是非常容易的,且渐变画刷在现代的色彩设计中也很受欢迎。
渐变画刷最简单的形式是LinearGradientBrush,只需要两个Color(我们不妨称这两种颜色为clr1和clr2)对象,和两个Point(pt1和pt2)对象。pt1的位置的颜色是clr1,且pt2的位置的颜色是clr2。在pt1和pt2之间的连线上,则是混合了crl1和clr2的颜色,连线中心点是clr1和clr2的平均值。垂直于连线的位置,和连线上的点使用相同的颜色。至于超过pt1和pt2的两边会是什么颜色,稍后再讨论。
有一个好消息:通常你指定这两个点时,使用的单位是像素(pixel)或者(在WPF中)是“设备无关单位”,如果你又想要窗口背景采用渐变,那么,只要窗口尺寸发生改变,你就必须重新指定这两个点。
而WPF渐变画刷有一个特性,让你不用基于窗口尺寸而调整画刷的点。默认情况下,你指定的点是“相对于表面”的,这里的表面被视为一个单位宽,一个单位高。表面的左上角是(0,0),右下角是(1,1)。
比方说,如果你想要让客户区的左上角为红色,右下角为蓝色,中间则是渐变色,你可以使用下面的构造函数,这里需要指定两种颜色和两个点:
LinearGradientBrush brush = new LinearGradientBrush(Colors.Red, Colors.Blue,
new Point(0, 0), new Point(1, 1));
下面是完整的程序:
GradiateTheBrush.cs
//-------------------------------------------------
// GradiateTheBrush.cs (c) 2006 by Charles Petzold
//-------------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.GradiateTheBrush
{
public class GradiateTheBrush : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new GradiateTheBrush());
}
public GradiateTheBrush()
{
Title = "Gradiate the Brush";
LinearGradientBrush brush =
new LinearGradientBrush(Colors.Red, Colors.Blue,
new Point(0, 0), new Point(1, 1));
Background = brush;
}
}
}
当你改变客户区的尺寸时,渐变画刷会随之改变。这要归功于Freezable所实现的Changed事件。
虽然使用相对坐标系统来设定点很方便,但这不是唯一的做法。GradientBrush类有一个MappingMode property,类型为BrushMappingMode枚举。此枚举只有两种值,分别为RelativeToBoundingBox(使用相对坐标是默认值)和Absolute(使用“设备无关单位”)。
在GradiateTheBrush中,用16进制数表示RGB颜色值,客户区左上角为FF-00-00,且右下角是00-00-FF。你可能认为这个中间的颜色如果不是7F-00-7F就是80-00-80,至于是何者,完全由四舍五入的结果而定。确实如此,因为默认的ColorInterpolationMode property值是ColorInterpolationMode.SRgbLinearInterpolation(线性插值)。此property的值也可以是ColorInterpolationMode.ScRgbLinearInterpolation,这表示中间的颜色scRGB值0.5-0-0.5,等于sRGB的BC-00-BC。
如果你需要建立水平或者垂直的渐变,还可以使用LinearGradientBrush的构造函数:
new LinearGradientBrush(clr1, clr2, angle);
指定角度(以度为单位,也就是一周为360度。)0度是水平渐变,clr1在左边,等同于:
new LinearGradientBrush(clr1, c1r2, new Point(0, 0), new Point(1, 0));
90度是垂直渐变,clr1在上面,等同于:
new LinearGradientBrush(clr1, c1r2, new Point(0, 0), new Point(0, 1));
其他的角度用起来可能需要一点技巧,就一般的例子来说,第一个点一定是原点,第二个点的计算方式如下:
new Point(cos(angle), sin(angle))
以45度为例,第二个点逼近(0.707, 0.707)。别忘了这是“相对于”客户区的点,所以,如果客户区不是正方形(正方形的概率其实比较低),这两个点之间的连线不会是真正的45度。另外,窗口右下角有一大块超出这点之外,这里又会如何?默认状况下,这里会着上第二种颜色。这受SpreadMethod property的控制。此property类型是GradientSpreadMethod枚举,默认是Pad,表示超出的部分就延续之前的颜色,不用变化;除了Pad,还有Reflect和Repeat。你可以试着把GradiateTheBrush程序修改成下面这样:
LinearGradientBrush brush =
new LinearGradientBrush(Colors.Red, Colors.Blue,
new Point(0, 0), new Point(0.25, 0.25));
brush.SpreadMethod = GradientSpreadMethod.Reflect;
在(0,0)和(0.25,0.25)之间,画刷从红到蓝渐变;然后在(0.25,0.25)和(0.5,0.5)之间,从蓝到红渐变;接着在(0.5,0.5)到(0.75,0.75)之间,从红到蓝渐变;最后在(0.75,0.75)和(1,1)之间,从蓝到红渐变。
如果扩大水平和垂直的差异,让窗口变得相当窄,或者相当扁,那么等色线(uniformly colored lines)就会变得几乎垂直或者几乎水平。你或许反而会希望“这两个对角之间的渐变”是以“另两个对角的连线”为等色线。上面这句话有点儿难懂,用图来说明应该可以帮助理解!图2-1是被拉长的GradiateTheBrush的客户区。
图2-1
虚线代表的是等色线(该线上都是同一个颜色),这一定垂直于pt1和pt2的连线。你可能比较希望渐变的方式如图2-2。
图2-2
现在,洋红色(magenta)的等色线是在对角线上。这么做会有一点麻烦,我们必须计算出pt1和pt2,好让连接pt1和pt2的线垂直于左下到右上的对角线。
矩形中心点到pt1(或pt2)的距离(我称它为L),可以这样计算:
其中W是窗口的宽,H是高。为了要让你更清楚,图2-3多了一些线段和文字标识。
图2-3
请注意,标识为L的线段平行于“pt1和pt2的连线”。角度α的正弦值可以用两种方式计算出来,第一种方式,用H除以对角线长度:
另一种方式,如果L是对边(opposite side),W是直角三角形的斜边(hypotenuse):
这两个式子,解方程组,就可以求出L。
下面的程序在构造函数中建立一个“可以被修改的”LinearGradientBrush对象,其MappingMode是Absolute。构造函数也安装了SizeChanged事件的处理器,只要窗口尺寸改变,就会跟着发生SizeChanged事件。
AdjustTheGradient.cs
//--------------------------------------------------
// AdjustTheGradient.cs (c) 2006 by Charles Petzold
//--------------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.AdjustTheGradient
{
class AdjustTheGradient: Window
{
LinearGradientBrush brush;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new AdjustTheGradient());
}
public AdjustTheGradient()
{
Title = "Adjust the Gradient";
SizeChanged += WindowOnSizeChanged;
brush = new LinearGradientBrush(Colors.Red, Colors.Blue, 0);
brush.MappingMode = BrushMappingMode.Absolute;
Background = brush;
}
void WindowOnSizeChanged(object sender, SizeChangedEventArgs args)
{
double width = ActualWidth
- 2 * SystemParameters.ResizeFrameVerticalBorderWidth;
double height = ActualHeight
- 2 * SystemParameters.ResizeFrameHorizontalBorderHeight
- SystemParameters.CaptionHeight;
Point ptCenter = new Point(width / 2, height / 2);
Vector vectDiag = new Vector(width, -height);
Vector vectPerp = new Vector(vectDiag.Y, -vectDiag.X);
vectPerp.Normalize();
vectPerp *= width * height / vectDiag.Length;
brush.StartPoint = ptCenter + vectPerp;
brush.EndPoint = ptCenter - vectPerp;
}
}
}
事件处理器一开始是计算客户区的宽度和高度,如同本章稍早的VaryTheBackground程序做法一样。用Vector对象vectDiag来表示对角线的向量(从左下到右上)。也可以利用右上角坐标减左下角坐标,来计算得到:
vectDiag = new point(width, 0) - new Point(0, height);
vectPerp向量垂直于对角线。建立相互垂直的向量很容易,只要把X和Y property的值对调,并把其中一个数的正负号反向就可以了。Normalize方法将X和Y的值除以Length property值,使vectPerp向量的Length变成1。事件处理器再将vectPerp乘以长度I(就是我们前面计算出的L)。
最后的步骤是设定StartPoint和EndPoint property。这些property一般是通过画刷的构造函数来设定的,而且除去继承的property,它们是LinearGradientBrush仅有的两个property。(当然,LinearGradientBrush也从抽象的GradientBrush类继承一些property)。
再次提醒你,程序只需要改变LinearGradientBrush,窗口就会自动地将结果反映出来。这是Freezable类(以及WPF的其他类)的Changed事件奇妙的地方。
LinearGradientBrush其实变化多端,远比这两个例子所能展示的更多。渐变的颜色不限定两个,可以有更多。想运用这样的特性,需要使用到GradientBrush定义的GradientStops property。
GradientStops property是GradientStopCollection类型,这是GradientStop对象的集合(Collection)。GradientStop具有Color和Offset property,而包含这两个property的构造函数为:
new GradientStop(clr, offset)
Offset property的值正常是在0和1之间,其意义是StartPoint和EndPoint的相对距离。比方说,如果StartPoint是(70,50)且EndPoint是(150,90),Offset property的值为0.25,就表示此点的位置是在StartPoint往EndPoint的方向四分之一的地方,也就是(90,60)。当然,如果你的StartPoint是(0,0),且你的EndPoint是(0,1)或(1,0)或(1,1),那么Offset对应的点在哪里,会比较容易推算。
下面的程序创建一个水平LinearGradientBrush,且针对彩虹的7种颜色设定数个GradientStop对象。它们依次从左到右,每个GradientStop是1/6个窗口宽。
FollowTheRainbow.cs
//-------------------------------------------------
// FollowTheRainbow.cs (c) 2006 by Charles Petzold
//-------------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.FollowTheRainbow
{
class FollowTheRainbow: Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new FollowTheRainbow());
}
public FollowTheRainbow()
{
Title = "Follow the Rainbow";
LinearGradientBrush brush = new LinearGradientBrush();
brush.StartPoint = new Point(0, 0);
brush.EndPoint = new Point(1, 0);
Background = brush;
// 采用Rey G.Biv的彩虹记住系统
brush.GradientStops.Add(new GradientStop(Colors.Red, 0));
brush.GradientStops.Add(new GradientStop(Colors.Orange, .17));
brush.GradientStops.Add(new GradientStop(Colors.Yellow, .33));
brush.GradientStops.Add(new GradientStop(Colors.Green, .5));
brush.GradientStops.Add(new GradientStop(Colors.Blue, .67));
brush.GradientStops.Add(new GradientStop(Colors.Indigo, .84));
brush.GradientStops.Add(new GradientStop(Colors.Violet, 1));
}
}
}
从这里,我们从LinearGradientBrush变到RadialGradientBrush,要做的改变不多,只需要改变画刷的类别名称,且删除指定StartPoint和EndPoint的语句即可:
CircleTheRainbow.cs
//-------------------------------------------------
// CircleTheRainbow.cs (c) 2006 by Charles Petzold
//-------------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.CircleTheRainbow
{
public class CircleTheRainbow : Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new CircleTheRainbow());
}
public CircleTheRainbow()
{
Title = "Circle the Rainbow";
RadialGradientBrush brush = new RadialGradientBrush();
Background = brush;
// 采用Rey G.Biv的彩虹记住系统
brush.GradientStops.Add(new GradientStop(Colors.Red, 0));
brush.GradientStops.Add(new GradientStop(Colors.Orange, .17));
brush.GradientStops.Add(new GradientStop(Colors.Yellow, .33));
brush.GradientStops.Add(new GradientStop(Colors.Green, .5));
brush.GradientStops.Add(new GradientStop(Colors.Blue, .67));
brush.GradientStops.Add(new GradientStop(Colors.Indigo, .84));
brush.GradientStops.Add(new GradientStop(Colors.Violet, 1));
}
}
}
现在画刷从客户区的中心点以红色开始,然后遍历这些颜色,一直到紫色(Violet)定义客户区的“内切椭圆”。在椭圆以外的区域(客户区的四个角落),继续使用紫色,因为SpreadMethod的默认值是Fill。
很明显,RadialGradientBrush的许多property都已经具备实用的默认值。其中三个property用来定义一个椭圆:Center是Point类型,定义为(0.5, 0.5),也就是画刷涵盖区域的中心点。RadiusX以及RadiusY的property皆为double类型,分别代表椭圆的水平和垂直轴半径,默认值是0.5,所以,无论是横向还是纵向,椭圆都达到了当前画刷作用区域的边界。
椭圆的圆周受到Center、RadiusX、RadiusY property的影响,圆周的颜色,正是Offset property值为1时的颜色。(在CircleTheRainbow范例中,此颜色是Violet紫色。)
还有个名为GradientOrigin的property,和Center一样,是Point对象,默认值是(0.5,0.5)。顾名思义,GradientOrigin是渐变开始的原点。在这个点,你会看到Offset为0时的颜色。(在CircleTheRainBow范例中,此颜色是红色。)
在GradientOrigin和椭圆圆周之间,就是发生渐变的地方。如果GradientOrigin等于Center(默认是如此),那么渐变会从椭圆圆心到椭圆圆周之间扩散。如果GradientOrigin和Center之间有一段距离,那么在“GradientOrigin”和“最接近的椭圆圆周”之间,渐变的颜色变化会比较剧烈,相反方向的颜色变化就比较缓和。想看看这样的影响,请在CircleTheRainBow中,插入下面的代码:
brush.GradientOrigin = new Point(0.75, 0.75);
你可能想要自己体验一下Center和GradientOrigin property的变化所造成的视觉效果,那么ClickTheGradientCenter程序来做实验。此程序使用RadialGradientBrush带两个参数的构造函数,定义GradientOrigin和椭圆圆周的颜色,然而,设定RadiusX和RadiuxY值
为0.1,且SpreadMethod为Repeat,所以画刷显示的是一系列的同心渐变圆圈。
ClickTheGradientCenter.cs
//-------------------------------------------------------
// ClickTheGradientCenter.cs (c) 2006 by Charles Petzold
//-------------------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Petzold.ClickTheGradientCenter
{
class ClickTheRadientCenter : Window
{
RadialGradientBrush brush;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new ClickTheRadientCenter());
}
public ClickTheRadientCenter()
{
Title = "Click the Gradient Center";
brush = new RadialGradientBrush(Colors.White, Colors.Red);
brush.RadiusX = brush.RadiusY = 0.10;
brush.SpreadMethod = GradientSpreadMethod.Repeat;
Background = brush;
}
protected override void OnMouseDown(MouseButtonEventArgs args)
{
double width = ActualWidth
- 2 * SystemParameters.ResizeFrameVerticalBorderWidth;
double height = ActualHeight
- 2 * SystemParameters.ResizeFrameHorizontalBorderHeight
- SystemParameters.CaptionHeight;
Point ptMouse = args.GetPosition(this);
ptMouse.X /= width;
ptMouse.Y /= height;
if (args.ChangedButton == MouseButton.Left)
{
brush.Center = ptMouse;
brush.GradientOrigin = ptMouse;
}
else if (args.ChangedButton == MouseButton.Right)
brush.GradientOrigin = ptMouse;
}
}
}
此程序覆盖 (override)了OnMouseDown方法,所以点击客户区,也会有反应。鼠标左键将Center和GradientOrigin设定成相同的值,你会看到整个画刷从客户区中央移动;鼠标右键只会改变GradientOrigin。让GradientOrigin尽量接近Center点,至少停留在圆的内部。现在你可以看到这个渐变如何在一边被挤压,在另一边却比较宽松。
这个效果相当有趣,所以我决定把它做成动画。下面的RotateTheGradientOrigin程序没有使用WPF的任何动画功能,只用计时器(timer)改变GradientOrigin property。
在.NET中,至少有4个计时器类型,其中3个都叫做Timer。System.Threading和System.Timers内的Timer类,在我们这个例子中并不能被使用,因为这些timer事件发生在不同的线程中,而Freezable对象只能被(创建它的)同一个线程所改变,而不能被其他线程改变。System.Windows.Forms内的Timer类型封装了标准的Windows操作系统计时器,但是如果使用它的话,还需要在工程中加入对System.Windows.Forms.dll 组件(assembly)的引用。
如果你需要让事件发生在WPF程序的主线程中,那么System.Windows.Threading命名空间的DispatcherTimer类是最适合的。你可以利用TimeSpan来设定Interval property,但是这有精度限制,最高频率是每10 millisecond发出一次到时消息。
下面的程序创建一个4英寸的正方形窗口,窗口不大,以免占用太多系统时间。
RotateTheGradientOrigin.cs
//--------------------------------------------------------
// RotateTheGradientOrigin.cs (c) 2006 by Charles Petzold
//--------------------------------------------------------
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
namespace Petzold.RotateTheGradientOrigin
{
public class RotateTheGradientOrigin : Window
{
RadialGradientBrush brush;
double angle;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new RotateTheGradientOrigin());
}
public RotateTheGradientOrigin()
{
Title = "Rotate the Gradient Origin";
WindowStartupLocation = WindowStartupLocation.CenterScreen;
Width = 384; // 相当于4英寸
Height = 384;
brush = new RadialGradientBrush(Colors.White, Colors.Blue);
brush.Center = brush.GradientOrigin = new Point(0.5, 0.5);
brush.RadiusX = brush.RadiusY = 0.10;
brush.SpreadMethod = GradientSpreadMethod.Repeat;
Background = brush;
DispatcherTimer tmr = new DispatcherTimer();
tmr.Interval = TimeSpan.FromMilliseconds(100);
tmr.Tick += TimerOnTick;
tmr.Start();
}
void TimerOnTick(object sender, EventArgs args)
{
Point pt = new Point(0.5 + 0.05 * Math.Cos(angle),
0.5 + 0.05 * Math.Sin(angle));
brush.GradientOrigin = pt;
angle += Math.PI / 6; // ie, 30 degrees
}
}
}
在本章,我将焦点放在Window的Background property,但是Window还有另外三个property也是Brush类型的,其中一个是OpacityMask,这个property是从UIElement继承而来的,在第31章讨论到位图(bitmap)时,会对此详细讨论。
Window的另外两个Brush property都是从Control继承而来的。一个是BorderBrush,可以在客户区的周边绘制一个边框。把下面的代码插入到程序中,看看执行结果会如何:
BorderBrush = Brushes.SaddleBrown;
BorderThickness = new Thickness(25, 50, 75, 100);
Thickness结构有四个property,名为Left、Top、Right、Bottom,也具有一个四参数的构造函数,用来设定这些property。这里用和设备无关的单位来指示客户区四边的边界宽度。如果你想要让四边具有相同的边框,可以使用单一参数版本的构造函数:
BorderThickness = new Thickness(50) ;
当然,你也可以在边框使用渐变画刷。
BorderBrush = new GradientBrush(Colors.Red, Colors.Blue,
new Point(0, 0), new Point(1, 1));
这段程序看起来很像是在客户区使用渐变画刷,红色在左上角,蓝色在右下角,不过却不是在客户区,而是在客户区的周边。BorderBrush会让客户区的面积变小。如果你使用BorderBrush且设定Background property为渐变画刷,就很容易看出来。即使BorderBrush和Background都具有相同的渐变画刷,这两个画刷也不会彼此交融。
Background = new GradientBrush(Co1ors.Red, Colors.Blue,
new Point(0, 0), new point(l, 1));
此背景画刷会在“不包含边框”的客户区中,完成全部的渐变。
Window类中,另外一个类型为Brush的property是Foreground,为了要让此property可以发挥效用,我们需要在窗口上放置一些内容。内容的形式有很多种,可以是文字、图、控件(control)等,这正是下一章要开始讨论的主题。