WPF程序设计读书笔记(2-1)
热9 已有 746 次阅读 2010-07-30 16:54
第2章 基本画刷
标准窗口的内部,被称为客户区。正是在窗口的这一区域,显示文字、图形、控件,并接收用户的输入。
WPF的颜色被封闭成Color结构体,定义在System.Window.Media命名空间中。和一般的图形环境一样,Color结构体使用RGB三原色来表达颜色。
Color结构包含名为R、G、B的三个可读写属性,它们的类型都是byte。三个属性都是0时为黑色,都是255时为白色。
Color结构还包含一个“alpha通道”,其属性名为A。它是用来控制颜色的“不透明”度的,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对象:
Color color=Color.FromRgb(r,g,b);
这里会得到你所指定的颜色,其Alpha值是255。你还可以这样:
Color color=Color.FromArgb(a,r,g,b);
由你来指定Alpha值。
前面我们所使用的RGB颜色空间,也被称为sRGB颜色空间,“s”就是标准的意思。而Color结构也支持另一种被称为scRGB的颜色空间,这种颜色空间通常又被称为sRGB64,因为它不是使用一个字节而是8个字节来存储颜色值。在Color结构中,scRGB被储存为了float类型。分别叫ScA、ScR,ScG,WcB。这些属性和A、R、G、B会相互影响,改变G会造成ScG的改变,反之亦然。
另外,System.Window.Media也包含一个叫Colors的类,它有141个静态颜色值属性,它们的名称都是好记的颜色名称,从AliceBlue和AntiqueWhite到Yellow和YellowGreen。我们可以这样使用:
Color color=Colors.PapayaWhip;
这些颜色的名称和Web浏览器常用的颜色名称是一样的。另外需要注意的一个问题是,140个颜色属性的Alpha值都是255,有一个颜色属性的Alpha值是0,它就是Transparent属性。
程序可以设定Background属性,但这个属性的类型却不是Color,它是一个Brush对象。
Brush是一个抽象类,只有它的子类实例才能用来设定Window对象的Background属性,而所有这些子类都在System.Window.Media命名空间里。本章稍后将讨论SolidColorBrush(单色画刷)类和两个继承自GradientBrush(渐变画刷)的类
SolidColorBrush是最简单的画刷,只使用单一的颜色。你可以在第1章后面的程序中加入以下代码来改变窗口的背景色。
Color backColor = Color.FromRgb(0,255,255);
SolidColorBrush brush = new SolidColorBrush(backColor);
this.Background = brush;
下面的程序在执行时,会依据“鼠标指针靠近窗口中心的程度”,来改变客户区的背景颜色。此程序利用using将System.Window.Media命名空间加进来,本书后面大部分的程序也都会用到这个命名空间。
//*********************************************************
//VaryTheBackground.cs 2010 18th July by mouyong
//*********************************************************
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace part1.ch01
{
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 = "改变背景色";
Width = 384;
Height = 384;
Background = brush;
}
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
//得到客户区的实际宽和高
double height = ActualHeight - 2 * SystemParameters.ResizeFrameHorizontalBorderHeight - SystemParameters.CaptionHeight;
double width = ActualWidth - 2 * SystemParameters.ResizeFrameVerticalBorderWidth;
//得到鼠标的坐标
Point ptMouse = e.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 color = brush.Color;
color.R = color.G = color.B = byLevel;
//这句代码一定要有,否则背景不会重绘
brush.Color = color;
}
}
}
当你向客户区中心点移动鼠标时,背景变成较亮的白色,而鼠标超过内切椭圆边线时,背景会变成黑色(
默然说话:的确是一个椭圆,你可以把鼠标移向窗体的四个角,你会发现,在还没有把鼠标移出窗体外的时候,窗口已经变成黑色,没有更多的变化了。)。
这个变化在鼠标每次移动时都会发生,那是因为只要brush一有改变,客户区就会被重绘,但这一切都是幕后进行的。这所以会有动态的反应,是因为Brush继承自Freezable类,而Freezable类实现了一个名为Changed的事件(event),Brush对象只要一有改变,这个事件就会被触发。所以只要一改变brush,背景就会被重绘。
WPF底层大量使用Changed事件和类似机制,以实现动画和其他特性。
画刷也有一个与Colors相似的类Brushes类。它也提供了141个静态只读的属性,对应于Colors的141个颜色属性,名称也是一样的。不过Brushes返回的是SolidColorBrush对象。你可以用下面的方式设定Background:
Background=Brushes.PaleGoldenrod;
但是Brushes下面所有的SolidColorBrush对象都是处于冻结状态,也就是说,不能再被改变。它是通过把Freeable对象的CanFreeze属性设为true来实现的。你可以通过调用Freeze方法来实现对象的冻结和不可变动。IsFrozen属性如果变成true,就表示已经被冻结。冻结的对象可以提高效率,还可以在多个线程间共享,没有被冻结的则不行。虽然无法将冻结的对象解冻,但是你可以做出一个没冻结的复制版本。下面的代码可以定义VaryTheBackground中的brush字段:
SolidColorBrush brush=Brushes.Black.Clone();
如果你想看到这141个画刷出现在同一个窗口的客户区,FlipThroughTheBrushes程序可以达成你的愿望,你可以用上下箭头来改变画刷。
//*********************************************************
//FlipThroughTheBrushes.cs 2010 18th July by mouyong
//*********************************************************
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Reflection;
namespace part1.ch01
{
namespace part1.ch01
{
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();
}
private void SetTitleAndBackground()
{
Title = "变化笔刷到 - " + props[index].Name;//获得属性的名称
Background = (Brush)props[index].GetValue(null, null);//获得实际的SolidColorBrush对象
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Key == Key.Down || e.Key == Key.Up)
{
index += e.Key == Key.Up ? 1 : props.Length - 1;
index %= props.Length;
SetTitleAndBackground();
}
}
}
}
}
此程序使用反射(reflection)来取得Brushes类的成员。构造函数第一行使用typeof(Brushes)来得到一个Type类的对象。Type类有一个方法,叫GetProperties,返回PropertyInfo对象的数组,数组内的每个元素都对应到Brushes类里的一个属性。调用GetProperties时,可以通过BinddingFlags来限制获得的属性状态,这里就限制只获得公开和静态的属性。
在构造函数和重写的OnKeyDown方法中,程序都调用了SetTitleAndBackground,以便将Title属性和Background属性设定为Brushes类的某个成员。Name会返回属性的名称,这里一开始就是“AliceBlue”。GetValue方法返回实际的SolidColorBrush对象。它需要两个参数,第一个参数需要属性所在的对象,因为我们现在取到的都是静态属性,所以传入null;第二个参数只有属性是一个数组时才有必要传入,所以我们也传入null。
System.Windows命名空间具有SystemColors类,其作用类似于Colors和Brushes,只具有静态的只读属性,返回Color值和SolidColorBrush对象。这些设定存储在Windows注册表中。利用此类,可以得知目前用户的颜色喜欢。比方说,SystemColors.WindowColor用来表示用户对于客户区的颜色喜好,而SystemColors.WindowTextColor是用户对于客户区文字的颜色喜好,而SystemColors.WindowBrusht 和SystemColors.WindowTextBrush则是返回对应颜色的SolidColorBrush对象。对于大多数的真实应用程序来说,应该使用这些颜色,可以达到统一、协调的视觉效果。
只继承自Freezeable类的对象,才可以被冻结。而Color是一个结构体,所以不存在冻结不冻结的问题。
如果不用单色画刷,可以改 用渐变画刷,将两种(或多种)颜色混合,逐渐改变。对于WPF来说,创建一个渐变画刷是非常容易的,且渐变画刷在现代的色彩设计中也很受欢迎。
渐变画刷最简单的形式是LinearGradientBrush,只需要两个Color(我们不妨称这两种颜色为clr1和clr2)对象,和两个Point(pt1和pt2)对象。pt1的位置的颜色是clr1,而pt2的位置的颜色是clr2.在pt1和pt2之间的连线上,则是混合了crl1和crl2的颜色,连线中心点是clr1和clr2的平均值。垂直于连线的位置,和连线上的点使用相同的颜色。至于超过pt1和pt2的两边会是什么颜色,稍后再讨论。
WPF渐变画刷有一个特性,让你不用基于窗口尺寸而调整画刷的点。默认情况下,你指定的点是“相对于窗口面积”的,这里的窗口面积被视为一个单位宽,一个单位高。(默然说话:即无论你的窗口的实际宽高是多少,我们统统认为它们都是一个单位宽,一个单位高,这就叫“相对”)那么,左上角的坐标就是(0,0),而右下角的坐标就是(1,1)。
例如,如果你想要让客户区的左上角为红色,右下角为蓝色,是间是渐变色,则使用下面的构造函数,这里需要指定两种颜色和两个点。
LinearGradientBrush brush = new LinearGradientBrush(Colors.Red,Colors.Blue,new Point(0,0),new Point(1,1));
下面是完整的程序:
//*********************************************************
//GradiateTheBrush.cs 2010 17th July by mouyong
//*********************************************************
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace part1.ch01
{
class GradiateTheBrush:Window
{
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new GradiateTheBrush());
}
public GradiateTheBrush()
{
Title = "渐变画刷";
LinearGradientBrush brush = new LinearGradientBrush(Colors.Red,Colors.Blue,new Point(0,0),new Point(1,1));
Background = brush;
}
}
}
当你改变客户区的尺寸时,渐变画刷会随之改变。这要归功于Freezable所实现的Changed事件。
使用相对坐标系统来设定点很方便,但这不是唯一的做法。GradientBrush类有一个MappingMode属性,类型为BrushMappingMode枚举。此枚举只有两种值,分别为RelativeToBoundingBox(使用相对坐标,默认值)和Absolute(使用“设备无关单位”)。
如果你需要建立水平或垂直的渐变,还可以使用LinearGradientBrush的构造函数:
new LinearGradientBrush(clr1,clr2,angle);
指定角度。0度是水平渐变,clr1在左边,等同于:
new LinearGradientBrush(clr1,clr2,new Point(0,0),new Point(1,0));
90度是垂直渐变,clr1在上面,等同于:
new LinearGradientBrush(clr1,clr2,new Point(0,0),new Point(0,1));
其他的角度用起来可能需要一点技巧,就一般的例子来说,第一个点一定是原点,第二个点计算如下:
new Point(cos(angle),sin(angle));
以45度为例,第二个点逼近(0.707,0.707)。别忘了这是“相对于”客户区的点,所以,如果客户区不是正方形(通常都不是),这两个点之间的连线就不会是45度。另外,窗口右下角也会有一大块超出这个点,这部分会如何处理呢?默认情况下,通常会着上第二种颜色。你可以设置LinearGradientBrush的SpreadMethod属性,它是GradientSpreadMethod枚举类型,默认是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;
Background = brush;
在(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)之间,从蓝到红渐变。
如图2-1所示,我们的pt1和pt2位于窗口的左上和右下角,而虚线代表的是等色线(意思就是在这条线上的颜色都是相等的)。在窗口的宽和高发生改变的时候,等色线的角度也就会随之更改(因为等色线总是垂直于pt1和pt2的连线的),但我们可能并不希望这样的一个效果,我们希望无论如何改变窗口的宽和高,等色线总是保持某个角度不变的(如图2-2,洋红的等色线始终保持在对角的连线上),这样,我们就要去调整pt1和pt2的位置,以使得等色线始终处于对角线的连线上。
图2-1
图2-2
这样一来,就带来了一个问题,如何随着窗口的调整,动态的求出pt1和pt2的位置,好让连接pt1和pt2和连线始终垂直于左下到右上的对角线呢?换句话说,我们应该设计一个公式,让pt1和pt2的位置与窗口的宽和高关联起来。我们再来看图2-3:我们需要再添加一点标识和一条辅助线,以便得到我们想要的公式。
图2-3
如图2-3,这个窗口的宽我们用W来表示,高用H来表示,则左下到右上的对角线长度就应该是 (
默然说话:著名的勾股定理还记得吧
?斜边的平方等于两个直角边的平方和,所以斜边长就是两个直角边的平方和开根),计算对角线的长度干嘛?其实这不是我们的目的,我们的目的是为了得到对角线到pt2的距离。这里我们作了一条辅助线L,它平行于pt1到pt2的连线,也垂直于对角线。根据定律, L的长度与对角线到pt2的长度是相等的,也就是说,求出了L的长度,也就得到了pt2到对角线的长度。接下来请看演算:(
默然说话:下面将要使用三角函数,我就不多罗嗦了,记不得的同学请参考别的书籍)
一方面,如果我们把H当作α所在的直角三角形的对边,那就有 成立。
另一方面,如果我们把L当作α所在直角三角形的对边,那就有 成立。
(
默然说话:好吧,我就再多罗嗦几句。正弦就是对边比斜边,如果把H作为对边,那么,斜边就是对角线,而对角线长度的计算,前面已经写过了。如果以L为对边,那斜边就是W。所以就有了上面的两个等式)
根据等量代换原则,就有 成立。
整理后得到: 。
这样我们就计算出了L的长度,也就是pt2到对角线的距离。在这里重要的是,我们找到了pt2的位置与长和宽的关系,同理可证pt1的位置与长和宽的关系也是相似的。
(
默然说话:不用多想α是多少,因为它只是我们的一个引入的中间变量,对于我们研究的问题,毫无关系。我只是想找出
pt1或pt2的位置和长宽的关系而引入了它而已。)
下面的程序在构造函数中建立一个“可以被修改的”LinearGradientBrush对象,其MappingMode是Absolute。构造函数中还委托了SizeChanged事件的处理器,只要窗口尺寸改变,就会跟着发生SizeChanged事件。
//*********************************************************
//AdjustTheGradient.cs 2010 17th July by mouyong
//*********************************************************
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace part1.ch01
{
class AdjustTheGradient:Window
{
LinearGradientBrush brush;
[STAThread]
public static void Main()
{
Application app = new Application();
app.Run(new AdjustTheGradient());
}
public AdjustTheGradient()
{
Title = "调整渐变";
SizeChanged += WindowOnSizeChanged;
brush = new LinearGradientBrush(Colors.Red,Colors.Blue,0);
brush.MappingMode = BrushMappingMode.Absolute;
Background = brush;
}
private void WindowOnSizeChanged(object sender, SizeChangedEventArgs e)
{
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;
}
}
}
(
默然说话:你可以看到,无论你如何调整窗口的大小,两个颜色的中间过渡色总是处于左下至右上角的对角线上,太神奇了!而这一切都发生在
WindowOnSizeChanged事件方法里)
事件处理器一开始是计算客户区的宽度和高度,如同本章稍早的VaryTheBackground程序做法一样。用Vector对象vectDiag来表示对角线的向量(从左下到右上)。也可以利用右上角坐标减左下角坐标,来计算得到:
vectDiag=new Point(width,0)-new Point(0,height);
vectPerp向量垂直于对角线。建立相互垂直的向量很容易,只要把X和Y属性的值对调,并把其中一个数的正负号反向就可以了。调用Normalize方法是为了正规化这个向量,之后,又把vectPerp乘以L(
默然说话:就是前面我们计算过的那个公式),这样就得到了一个pt1到pt2的一个向量。
(
默然说话:是不是还是觉得晕?看不明白?嘿嘿,说实话,我也看不明白这一段呀,实在是超出了我的所学范围,仍然盼望高手解惑。)
最后的步骤是设定StartPoint和EndPoint属性。这些属性一般是通过画刷的构造函数来设定的,而且除去继承的属性,它们是LinearGradientBrush仅有的两个属性。