第二章 基本的Brush画刷类 [App = Code + Markup]

 标准Windows窗体内部的大块区域被称为客户区,程序在该区域里显示文本、图片和空间等,而窗体本身通过它来接收用户输入。
 
 第一章中所创建窗体的客户区可能都是白色的。这里之所以说“可能是”是因为白色是客户区的缺省背景色,而你可能出于个人的审美观或者仅仅是想表示你独特的个性而在控制面板中把系统颜色设置成非缺省的配置。如果是这样的话,你大概会希望开发人员能够察觉并尊重你的个性配置。
 
 WPF中的颜色被封装在System.Windows.Media命名空间下的Color结构体(Struct)中。与图形环境下的惯例一致,Color结构体同样使用红绿蓝三原色来表示颜色。三原色在Color结构体中分别用R、G和B来表示,他们所构成的三维色彩空间被称为RGB颜色空间。
 
 Color结构体中的三个读写属性R、G和B均为byte字节类型,取值范围为0~255。这三个属性的值都为0,表示的是黑色。而当它们都为255时,表示的即为白色。

 除三原色外,Color结构体还有一个属性A,表示的是alpha通道。alpha通道的值控制着颜色的透明度,0表示完全透明,255表示不透明,中间的值表示透明程度。
 
 Color结构体与其他的结构体一样有个无参的构造函数,该构造函数把A、R、G和B属性都置为0也就是说构造了一个完全透明的黑色。如果要得到一个可见的颜色,程序可以像下面的代码一样来自己设置这四个属性: 

  Color clr = new Color();
  clr.A 
= 255;
  clr.R 
= 255;
  clr.G 
= 0;
  clr.B 
= 255;

  上面代码得到的是不透明的红紫色。

 Color结构体还提供了一些静态方法来让你能够仅用一行代码来创建Color对象。比如下面的这个方法,它需要三个byte类型的参数:

  Color clr = Color.FromRgb(r, g, b)

 由该方法创建的颜色A属性值都是255。你也可以选择下面这个方来来自己指定alpha通道值:

  Color clr = Color.FromArgb(a, r, g, b)

 使用字节来表示三颜色值的RGB颜色空间有时也被称为sRGB颜色空间,这里s指的是standard--标准。sRGB颜色空间源于在扫描仪、数码相机显示位图的实践,色值与显像电压成比例。

 然而,sRGB显然不适合在其他输出设备来表示颜色。比如说,电脑显示器使用最大值255来表示其显示的绿色,如果有一个打印机能够打印比显示器所能显示的绿色更绿的颜色,那么该怎么来表示呢?

 为了解决这种问题,其他的RGB颜色空间就应运而生了。WPF中对这些替代方案的其中一种提供了支持:scRGB颜色空间。由于它使用64位值来表示色值,该空间最初被称为scRGB64。这些64位的色值在Color结构体中实际使用单精度的浮点数来表示的,对应的属性名分别为ScA、ScR、ScG和ScB。这些属性与相应的A、R、G和B属性并不是相互独立的,比如改变G属性的值同时也改变了ScG属性的值,反之亦然。
 
 当G属性为0,ScG属性的值同样为0当G属性为255时,ScG属性的值为1。但是两者在最小值和最大值之间的比例关系却并不呈线性,如下表:

 可以看到,ScG的值可以小于0者大于1以表示sRGB颜色空间所无法表示的颜色。ScR/R,ScB/B属性间的关系与ScG/G是一样的。

 当前的主流显像管CRT--阴极射线管是以非线性的方式来显示光,光强度和电压的关系如下:

 这里的gamma指数****[译者偷懒,欢迎Goooogle]****,一般介于2.2和2.5之间(sRGB标准定义的是2.2)。

 
 人类的对光强度的感知度也是非线性的,大概与光强度的1/3次方幂成比例。幸运的是,人类视觉特性的非线性和CRT显示方式的非线性刚好互补,所以sRGB空间下的颜色色值与视觉效果大致呈线性比例。也就是说,RGB值 80-80-80 (十六进制)大致对应于人眼可能识别为“半灰色”的颜色。这也是sRGB标准如此风行的原因之一。

 而scRGB颜色空间被设计成颜色与光强度呈线性关系,因此scG/G属性间的对应关系是:

   
 这里的指数2.2是scRGB标准所采用的gamma校正值。注意,等式中使用的是约等号,数值越小越不精确。alpha通道值的对应关系相对较为简单些:

  

 下面这个静态方法可用于创建一个基于scRGB色值的Color对象: 
  

Color clr = Color.FromScRgb(a, r, g, b);
 


 参数均为浮点数,并且可以小于0或者大于1。

 System.Windows.Media命名空间还包括一个名为Colors(注意是复数形式)的类,这个类包含了141个静态只读的属性分别对应不同的颜色。这些属性的名字按字母序开始于AliceBlue和 AntiqueWhite,结束于Yellow 和 YellowGreen。比如:

Color clr = Color.FromScRgb(a, r, g, b);
 

 这些颜色中仅有一种颜色不被网页浏览器所支持:Transparent(透明),它的alpha值为0。其他140个属性所表示的颜色都基于sRGB颜色空间,alpha值都是255。

 言归正传,程序可以通过设置窗体的Background属性来改变客户区的颜色,这是个从Control类继承来的属性。但是要注意的是,你不能把Backgroud属性设置成Color对象,你应该把它设置成一个更牛*类型--Brush(画刷)类的对象。

 Brush(画刷)类在WPF中使用非常广泛,因此需要在本书中早些来关注它。如下图所示,Brush类本身其实是个抽象类:

  
 
 你实际用来设置窗体Background属性的是继承自Brush类的某个非抽象类的实例。所有与Brush相关的类都属于System.Windows.Media命名空间,我将在本章中讨论它们中的SolidColorBrush类和GradientBrush的两个子类。
 
 从命名上可以看得出来,最简单的画刷类型是SolidColorBrush,它是一个单色画刷。在第一章靠后的一个例子中,你可以添加一个对System.Windows.Media命名空间的引用,然后在Window类的构造函数中象下面这样来设置客户区的颜色:

  Color clr = Color.FromRgb(0255255);
  SolidColorBrush brush 
= new SolidColorBrush(clr);
  Background 
= brush;

 这样会把客户区的颜色变为青色。当然了,这些可以在一行代码中实现:
  

  Background = new SolidColorBrush(Color.FromRgb(0255255));

 SolidColorBrush类还有一个无参的构造函数和一个Color属性,你可以通过它们在创建实例后设置颜色,比如:

  SolidColorBrush brush = new SolidColorBrush();
  brush.Color 
= Color.FromRgb(1280128);

 下面的程序随着鼠标与窗体中心距离的不同而变换客户区的背景色,后面的大部分程序都会象它一样包含一个对System.Windows.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;
        }

    }

}



 随着鼠标向客户区中心移动,背景色逐步变为轻灰色。当鼠标移出覆盖客户区的虚拟椭圆时,背景色将变回黑色。

 所有的改变都发生在重写的OnMouseMove方法中,只要鼠标在程序的客户区上移动,该方法就会被调用。由于某些方面的原因,OnMouseMove方法稍微有点复杂。首先,它必须计算客户区的大小,但是没有什么好的方法来确定客户区的大小,除非客户区里面有些什么东西。上面的实现中首先使用了窗体的ActualWidth和ActualHeight属性,然后分别减去从SystemParameters类中得到的可调边框和标题栏的对应大小。随后,调用MouseEventArgs类的GetPosition实例方法得到鼠标的位置并存放在Point类型的对象ptMouse中。后面的ptCenter表示的是客户区的中心。然后两者相减。Point类的文档规定了两个Point对象相减的结果为Vector(向量)类型的实例--程序中的vectMouse变量。从数学的角度来说,一个向量表示的是一个量和一个方向。vectMouse的量就是ptCenter和ptMouse的距离,由Length属性表示。向量的方向由其X和Y属性表示,具体指的是从原点(0,0)指向点(X,Y)的方向。vectMouse.X就等于ptMouse.X 减去 ptCenter.X,vectMouse.Y等于ptMouse.Y 减去 ptCenter.Y。
 
 也可以使用角度来表示Vector向量的方向。Vector结构体有一个静态方法AngleBetween用于计算两个向量的夹角。上面的代码中直接使用了向量的Y和X属性的比值进行arctan计算来取得表示vectMouse的方向的角度值,使用的单位是从水平坐标开始沿顺时针方向计算的弧度。得到的角度值随后被用来计算表示客户区中心到填充客户区的椭圆上某个点的距离向量,背景色的灰度值与这两个向量的比例大致相符。

 在OnMouseMove中紧接着创建了一个Color实例,然后把它三原色的色值都设置成上面得到的灰度值。最后,Color实例被赋给了Backgroud属性使用的画刷。

 你可能会感到非常的惊讶:这个破程序居然能够正常运行!很明显,有人在画刷改变的时候重绘了客户区,但是这些似乎都悄悄发生在幕后。这种动态的响应得以发生的原因是Brush类继承了Freezable类,而Freezable类实现了一个名为Changed的事件。^_^ 该事件在任何你对Brush类对象作更改的时候触发,因此使得客户区在背景色被更改时自动重绘。

 这个Changed事件和类似的机制在WPF的动画效果和其他特性的幕后实现中有着广泛的应用。

 前面说到Colors类提供了141个静态只读的属性来表示对应的颜色,Brushes类(同样注意其复数形式)也提供了与Colors中名称相同的141个静态只读属性来返回相应的SolidColorBrush对象。因此,你可以这样来设置窗体的Background属性:  

Background = Brushes.PaleGoldenrod;

 而不是: 

 Background = new SolidColorBrush(Colors.PaleGoldenrod);

 虽然这两种写法给你的窗体设置了同样的背景色,但是你可以在VaryTheBackground程序中看到它们背后的差别。把VaryTheBackground程序中的下面一个字段定义: 

SolidColorBrush brush = new SolidColorBrush(Colors.Black);

  替换为:  

SolidColorBrush brush = Brushes.Black;

 重新编译并运行。现在你会得到一个操作无效的异常InvalidOpertionException,异常描述语句为: "Cannot set a property on object '#FF000000' because it is in a read-only state." 问题出在OnMouseMove方法的最后一个语句上,该语句试图去更改brush实例的Color属性。(异常描述语句中的十六进制数指的是当前的Color属性值。)

 Brushes类返回的SolidColorBrush对象都处于冻结状态,这意味着它们不可再被更改。冻结状态与Changed一样是在Freezable类中实现的,由Brush类通过继承获取之。如果一个Freezable对象的CanFreeze属性为true,那么就可以调用Freeze方法来冻结该对象。IsFrozen属性为true,则表示对象处于冻结状态。冻结对象能够提高性能,因为程序不需要再监听他们的变化。被冻结的Freezable对象也可以被多个线程共享,而未被冻结的则不可以。虽然没有办法来为一个冻结对象来解冻,你可以创建一个他的未冻结的拷贝。下面这行代码能够让VaryTheBackground程序重新正常运行:

  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(
nullnull);
        }

    }

}



 本程序使用了反射来获取Brushes类的成员。构造函数的第一行代码使用typeof(Brushes)来获取Type类型的对象,Type类定义了GetProperties方法用于取得一个PropertyInfo类型的数组,每个数组元素对应于一个类的属性。注意,程序在获取Brushes属性列表的时候显式使用BindingFlags参数来指明仅希望得到pulic staic的属性。虽然由于Brushes类中所有的属性都是public static的,这显式的指定条件并不是必须的,但是毕竟也不会带来什么副作用。

 程序在构造函数和重写的OnKeyDown方法中都调用了SetTitleAndBackground方法来设置窗体的标题和背景。表达式props[0].Name返回的是Brushes类的第一个属性的名称--AliceBlue,而表达式props[0].GetValue(null,null)返回的则是实际的SolidColorBrush对象。可以看到,GetValue接收了两个null参数来完成这个任务。一般来说,传给GetValue的第一个参数是需要从中取出属性值得对象,这里要返回的是静态属性,所以传入的是空对象;第二个参数只有返回属性为索引器(indexer)的时候才用得到。

 System.Windows命名空间下有一个跟Colors和Brushes很象的类SystemColors。说与他们很相像,是因为SystemColors类里面只包含一些静态只读、用于返回Color或者SolidColorBrush对象的属性。该类提供了记录在注册表里面的当前用户的颜色使用偏好,比如说:SystemColors.WindowColor属性指出了用户对窗体客户区背景色的偏好,SystemColors.WindowTextColor提供的是用户对在客户区中显示的文本的颜色编号;SystemColors.WindowBrush和SystemColors.WindowTextBrush提供的是对应颜色的SolidColorBrush对象。在大多数的真正应用中,你应当使用这些颜色来显示很简单的文本和单色的图像。

 SystemColors返回的画刷都是冻结的对象,但是你可以这样来改变它们:

  Brush brush = new SystemColorBrush(SystemColors.WindowColor);

 但是按下面方式返回的画刷则都是不可更改的:

  Brush brush = SystemColors.WindowBrush;

 只有继承自Freezable的类可以被冻结,象Color对象就不可以被冻结,因为Color是个结构体。^_^ 

 除了固定颜色的画刷,你还可以选择使用梯度画刷,后者用于显示在两个或者多个颜色间渐变的颜色。一般来说,梯度画刷应该属于一个高级的编程话题,但是在WPF中创建他们非常容易,并且他们在现代颜色配置中也非常流行。

 LinearGradientBrush类画刷对象的最简形式需要2个Color对象(姑且称其为clr1和clr2)和2个Point对象(pt1和pt2)。点pt1的颜色会是clr1,点pt2的颜色会是clr2。两点间连线的颜色会是clr1和clr2的混合色,中点的颜色就是两者的平均色。连线上的每个点的垂直线都是按该点相对于两个端点的比例来上色。我会在稍后的一部分中介绍在两个端点之外发生的事情。

 现在先来看一个好消息:一般来说,你可能需要以像素或者独立于设备的单位来指定两个端点,如果你想给窗体背景应用一个梯度画刷,那么当窗体大小改变的时候你就必须要重新来指定这两个端点了。事实上,WPF中的梯度画刷包含了一个“神奇”的特性使得在窗体大小改变的时候,你不需要重新创建或者修改已有的画刷对象。缺省情况下,梯度画刷要着色的对象被当成了1 * 1大小,也就是说1单位宽1单位高。对象的左上角坐标是(0,0),右下角是(1,1)。

 举例而言,如果你希望客户区的左上角为红色,右下角为蓝色,对角线上的颜色按梯度在两者间变化,那么你可以使用下面这个构造函数:

LinearGradientBrush brush = new LinearGradientBrush(Colors.Red, Colors.Blue,
                                       
new Point(00), new Point(11));

   完整的程序如下:

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
(
00), new Point(11));
            Background 
= brush;
        }

    }

}


 可以看到,随着客户区大小的改变,梯度画刷相应地自动改变,这神奇的幕后推手同样是Freezable类。
 
 虽然一般来说用这种相对坐标系统来指定端点是很方便的,但这并不是唯一的选择。GradientBrush类定义了一个MappingMode属性用来指定坐标的映射方式,你可以赋给该属性一个BrushMappingMode的枚举值。可选的枚举值包括:缺省值RelativeToBoundingBox和指定将使用独立于设备的单位的Absolute。

 按十六进制的表示方法,GradiateTheBrush程序中窗体客户区左上角的颜色是FF-00-00,而右下角的颜色是00-00-FF。你可能会认为中点的颜色要么是7F-00-7F要么是80-00-80,具体取决于舍入的方法。事实上的确如此,因为ColorInterpolationMode(颜色解释模式)属性的缺省值是ColorInterpolationMode.SRgbLinearInterpolation。另一个可选的枚举值是ColorInterpolationMode.ScRgbLinearInterpolation,该值将会使得中点的颜色的scRGB值为0.5-0-0.5,对应于sRGB值BC-00-BC。

 如果你需要的只是一个水平变化或者垂直变化的梯度画刷,那么更简单的做法是使用LinearGradientBrush类的这个构造函数:

  new LinearGradientBrush(clr1, clr2, angle);

 其中的angle角度使用的单位是度。0度表示水平的梯度,左端为clr1颜色,等价于:

  new LinearGradientBrush(clr1, clr2, new Point(00), new Point(10));

 
 90度角表示垂直的梯度,上端为clr1颜色,等价于:   

new LinearGradientBrush(clr1, clr2, new Point(00), new Point(01));

 其他的角度的用法相较而言比较微妙。一般来说,第一个点始终都是原点(0,0),而第二个点的是这样计算得到:

  new Point(cos(angle), sin(angle));

 举个例子来说,对于45度角,第二个点的坐标大约是(0.707,0.707)。

 要牢记的一点是:这些坐标都是相对于客户区而言的。如果客户区不是正方形的,那么两个端点间连线的角度就不是45度角了,而窗体的右下角会有一块处于端点之外。那么,这端点之外的区域会被如何处置呢? 缺省情况下,这些区域会按第二种颜色来上色。这是由SpreadMethod(伸展模式)属性来决定的,该属性是一个GradientSpreadMethod枚举类型的值,缺省为Pad(衬垫),也就是说终点处的颜色会继续被使用到边界。可取得枚举值还有Reflect(反射)和Repeat(重复)。你可能想在GradiateTheBrush程序中试验一下下面的代码:

  LinearGradientBrush brush =
       
new LinearGradientBrush(Colors.Red, Colors.Blue,
                           
new Point(00), new Point(0.250.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)的梯度区间内从蓝到红来着色。

 如果你把窗体变得非常的窄或者非常短,依此来放大水平和垂直元上的区别,那么那些颜色一样的线条将几乎分别变成垂直或者水平状。你也可能会希望反对角线是一个单色的线条。这样说太拗口了,也许图片更能达意,这是一张GradiateTheBrush的客户区被拖成长方形的图片:
 

 虚线表示单色线,他们总是垂直于pt1和pt2的连线。你可能会希望看到这样一个效果: 
 
 

 现在整个反对角线都是红紫色了。那么现在问题就是如何来计算pt1和pt2以让反对角线刚好是他们的垂线。
 
 可以看到,长方形中心到端点pt1和pt2的长度L可以这样来计算:
 
 

 这里W和H分别是窗体的宽和高。下图中添加了一些辅助线,从中可能更容易看出上面的计算公式的来由:
 
 

 注意直线L平行于pt1和pt2间的连线。a角的sin值有两种方法来表示:
 

 
 或者把W作为斜边,L作为其对边:

 
 
 联立两个等式,解出L。

 下面的这个程序在其构造函数中创建了一个MappingMode属性为Absolute的LinearGradientBrush对象,同时故意在运行过程中修改该画刷对象。构造函数还为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程序一样,这里的事件处理程序从计算客户区的宽度和高度开始处理。向量vectDiag表示从左下角到右上角的对角线,也可以通过两角坐标相减来获得: 

vectDiag = new Point(width, 0)  -  new Point(0, height);

 把对角线向量vectDiag的X属性取反并于Y属性值互换,得到与之垂直的向量vectPerp。****[译注:后面是一大堆的推理,老外显然很喜欢卖弄他们的数学^_^]****

 最后一步是设置LinearGradientBrush画刷的StartPoint(起点)和EndPoint(终点)属性。这两个属性通常情况下是在构造函数中设置,它们也是LinearGradientBrush类自己定义的全部属性。(当然了,LinearGradientBrush类还从抽象的GradientBrush类中继承了其他一些属性。)

 可以注意到:你所需要做的全部就是更改画刷的属性值,其他的一切再次由幕后的推手--Freezable类的Changed事件来自动完成。

 LinearGradientBrush类的功能其实远不止上面两个程序中展示的那些,它还能够使用多于两种的颜色。为了使用多种颜色,需要用到继承自GradientBrush类的GradientStops属性。这是个GradientStopCollection类型的属性,可持有多个GradientStop类的实例。GradientStop类定义了两个属性:Color和Offset,构造函数如下:

  new GradientStop(clr, offset);

 Offset属性的值一般介于0和1之间,表示相对于两个端点的距离。举例而言,如果起点的坐标是(70,50),终点是(150,90),那么为0.25的Offset值就表示端点间距离的1/4,或者说对应于坐标为(90,60)的点。当然了,如果你的起点坐标为(0, 0),而终点坐标为(0, 1) 、(1, 0) 或 (1, 1),那么Offset的值就很容易计算了。

 下面这个程序将创建一个水平的LinearGradientBrush画刷,并为它设置了七个GradientStop节点分别对应于彩虹的七彩色。这七个GradientStop节点把窗体的宽六等分。

 

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(00);
            brush.EndPoint 
= new Point(10);
            Background 
= brush;

            
// Rainbow mnemonic is the name Roy 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;

            
// Rainbow mnemonic is the name Roy 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));
        }

    }

}


 


现在,这个画刷从客户区的中心开始绘色,由红至紫地按椭圆状层层向外散开,直到充满整个客户区。又由于SpreadMethod属性的缺省值是Fill(填充),在最外层的椭圆之外的区域也都是紫色。

 RadialGradientBrush类显然给它的一些属性定义了有用的缺省值,其中有三个协作定义了一个椭圆:Center属性是一个Point类的对象,缺省值是(0.5,0.5),恰好为画刷作用区域的中心;RadiusX和RadiusY属性都是double类型,它们分别表示椭圆长短半轴的大小,缺省为0.5,因而使得椭圆在水平和垂直方向上都刚好充满画刷的作用区域。这三个属性对应椭圆的边使用对应于Offset值1的颜色(本例中即为紫色)。

 GradientOrigin(梯度原点)属性是第四个这样的属性,它的缺省值是(0.5,0.5),Point类型。从名字上可以看出来这个属性表示的是梯度起始点的坐标,而所谓的梯度起始点就是对应于Offset值0的点。

 梯度始于GradientOrigin而终于椭圆的圆周。如果GradientOrigin属性的值等于Center属性的值,则梯度从椭圆的中心开始,并扩散到圆周;如果两者不相等,也就是说梯度原点与椭圆圆心不重合的话,那么梯度就会在原点离圆周较近的方向上较为紧凑,在反方向上则相对松散些。可以把下面这行代码插到CircleTheRainbow程序中来看下效果:

  brush.GradientOrigin = new Point(0.750.75);

 你可能想玩玩Center和GradientOrigin属性之间的不同的组合关系,那么可以用下面的这个ClickTheGradientCenter程序。这个程序使用了RadialGradientBrush类的含两个参数的构造函数,这两个参数分别定义了梯度原点和圆周上要使用的颜色。注意与缺省值不同,本程序把RadiusX和RadiusY属性都设置为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;
        }

    }

}



 上面的程序重写了OnMouseDown方法,使得它可以响应点击客户区的动作。点击鼠标左键会把Center和GradientOrigin属性值同时改变为某个值,从而使得画刷看起来只是简单地从客户区中心移开了;而点击右键仅会改变GradientOrigin属性,这样就可以看到压缩/松散的梯度效果。注意:仅应点击比较靠近中心点的区域,至少应该点击在最内层的椭圆之内。

 效果看起来非常有趣,不是么?我甚至决定来做一个这样的动画效果。^_^ 下面的这个RotateTheGradientOrigin程序实现了一个这样的效果,但是它没有使用任何WPF所提供的动画特性,它使用了一个简单的定时器来更改GradientOrigin属性。
 
 其实在.Net中至少有四种定时器,并且有三个都叫Timer。System.Threading和System.Timers命名空间下的Timer类使用的都是一个额外的线程来进行计时,而Freezable类型的对象必须在它们的创建线程内操作,所以这两种定时器都不适合用在这里。System.Windows.Forms命名空间下的Timer类是一个标准的Windows计时器,但是由于必须要增加一个对System.Windows.Forms.dll的引用才能使用它,它也不在考虑之列。
 
 我们要使用的是定时器是System.Windows.Threading命名空间下的DispatcherTimer类,它适用于WPF程序。DispatcherTimer类的Interval属性用于指定计时间隔,这个属性是TimeSpan类型,但是你不能赋给它一个小于10毫秒的时间。

 这个程序为了不消耗太多的处理时间仅创建了一个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;        // ie, 4 inches
            Height = 384;

            brush 
= new RadialGradientBrush(Colors
.White, Colors.Blue);
            brush.Center 
= brush.GradientOrigin =
 
new Point(0.50.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
        }

    }

}

 

 我从本章的开始一直都是在唠叨窗体的Background属性,其实Window窗体类还有三个Brush类型的属性。第一个是从UIElement类继承的OpacityMask属性,但是我们最好在讨论位图的第三十一章里面讨论它。另外的两个属性都是继承自Control类。第一个是BorderBrush属性,负责客户区的边框。你可以把下面两行代码插到上面的程序中来试验一下这个属性:

  BorderBrush = Brushes.SaddleBrown;
  BorderThickness 
= new Thickness(255075100);

 上面用到的Thickness结构体有四个属性:Left、Top、Right和Bottom,还有一个设置这四个属性的构造函数。这些属性都使用独立于设备的单位来表示客户区的四个边长,如果你需要的是一个正方形的边框,那么可以使用这个单参数的构造函数:

  BorderThickness = new Thickness(50);

 
 当然了,你也可以用梯度画刷来绘制边框:

  BorderBrush = new GradientBrush(Colors.Red, Colors.Blue,
                                new Point(0, 0), new Point(1, 1));
  [译注:编译通不过。]

 这行代码的效果看起来像是一个充满整个客户区的梯度画刷:左上角红色,右下角蓝色,所不同的地方仅在于画刷只作用在客户区的周边上。如果你把BorderBrush和Background都设置成梯度画刷,就很容易看出区别来。如果你碰巧把这两个属性都设置成了相同的话刷也用担心他们会混杂在一起:

  Background = new GradientBrush(Colors.Red, Colors.Blue,
                               new Point(0, 0), new Point(1, 1));
  [译注:编译通不过。]

 可以看到,背景画刷仅仅在边框之内涂画。

 Window窗体类唯一一个剩下的Brush类型的属性是Foreground属性。由于需要在窗体上放一些内容才能看出Foreground的作用效果,而窗体的内容的又有很多种形式(如:普通文本、图像、控件等),我们将在下一章里讨论他们。  

posted @ 2007-04-04 11:58  飞扬跋扈  阅读(2489)  评论(2编辑  收藏  举报