WPF程序设计读书笔记(2-1)

第1章    基本画刷

标准窗口的内部,被称为客户区。正是在窗口的这一区域,显示文字、图形、控件,并接收用户的输入。

WPF的颜色被封闭成Color结构体,定义在System.Window.Media命名空间中。和一般的图形环境一样,Color结构体使用RGB三原色来表达颜色。

Color结构包含名为RGB的三个可读写属性,它们的类型都是byte。三个属性都是0时为黑色,都是255时为白色。

Color结构还包含一个“alpha通道”,其属性名为A。它是用来控制颜色的“不透明”度的,0表示完全透明,255表示完全不透明。

和所有结构体一样,Color具有一个无参数的构造函数,它产生一个ARGB都是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类型。分别叫ScAScRScGWcB。这些属性和ARGB会相互影响,改变G会造成ScG的改变,反之亦然。

另外,System.Window.Media也包含一个叫Colors的类,它有141个静态颜色值属性,它们的名称都是好记的颜色名称,从AliceBlueAntiqueWhiteYellowYellowGreen。我们可以这样使用:

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;

下面的程序在执行时,会依据“鼠标指针靠近窗口中心的程度”,来改变客户区的背景颜色。此程序利用usingSystem.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个静态只读的属性,对应于Colors141个颜色属性,名称也是一样的。不过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类,其作用类似于ColorsBrushes,只具有静态的只读属性,返回Color值和SolidColorBrush对象。这些设定存储在Windows注册表中。利用此类,可以得知目前用户的颜色喜欢。比方说,SystemColors.WindowColor用来表示用户对于客户区的颜色喜好,而SystemColors.WindowTextColor是用户对于客户区文字的颜色喜好,而SystemColors.WindowBrusht SystemColors.WindowTextBrush则是返回对应颜色的SolidColorBrush对象。对于大多数的真实应用程序来说,应该使用这些颜色,可以达到统一、协调的视觉效果。

只继承自Freezeable类的对象,才可以被冻结。而Color是一个结构体,所以不存在冻结不冻结的问题。

如果不用单色画刷,可以改 用渐变画刷,将两种(或多种)颜色混合,逐渐改变。对于WPF来说,创建一个渐变画刷是非常容易的,且渐变画刷在现代的色彩设计中也很受欢迎。

渐变画刷最简单的形式是LinearGradientBrush,只需要两个Color(我们不妨称这两种颜色为clr1clr2)对象,和两个Pointpt1pt2)对象。pt1的位置的颜色是clr1,而pt2的位置的颜色是clr2.pt1pt2之间的连线上,则是混合了crl1crl2的颜色,连线中心点是clr1clr2的平均值。垂直于连线的位置,和连线上的点使用相同的颜色。至于超过pt1pt2的两边会是什么颜色,稍后再讨论。

WPF渐变画刷有一个特性,让你不用基于窗口尺寸而调整画刷的点。默认情况下,你指定的点是“相对于窗口面积”的,这里的窗口面积被视为一个单位宽,一个单位高。(默然说话:即无论你的窗口的实际宽高是多少,我们统统认为它们都是一个单位宽,一个单位高,这就叫“相对”)那么,左上角的坐标就是(00),而右下角的坐标就是(11)。

例如,如果你想要让客户区的左上角为红色,右下角为蓝色,是间是渐变色,则使用下面的构造函数,这里需要指定两种颜色和两个点。

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度。另外,窗口右下角也会有一大块超出这个点,这部分会如何处理呢?默认情况下,通常会着上第二种颜色。你可以设置LinearGradientBrushSpreadMethod属性,它是GradientSpreadMethod枚举类型,默认是Pad,表示超出部分延续之前的颜色,不渐变,你还可以设置为ReflectRepeat。试着把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所示,我们的pt1pt2位于窗口的左上和右下角,而虚线代表的是等色线(意思就是在这条线上的颜色都是相等的)。在窗口的宽和高发生改变的时候,等色线的角度也就会随之更改(因为等色线总是垂直于pt1pt2的连线的),但我们可能并不希望这样的一个效果,我们希望无论如何改变窗口的宽和高,等色线总是保持某个角度不变的(如图2-2,洋红的等色线始终保持在对角的连线上),这样,我们就要去调整pt1pt2的位置,以使得等色线始终处于对角线的连线上。

2-1

2-2

  

这样一来,就带来了一个问题,如何随着窗口的调整,动态的求出pt1pt2的位置,好让连接pt1pt2和连线始终垂直于左下到右上的对角线呢?换句话说,我们应该设计一个公式,让pt1pt2的位置与窗口的宽和高关联起来。我们再来看图2-3:我们需要再添加一点标识和一条辅助线,以便得到我们想要的公式。

2-3

 

如图2-3,这个窗口的宽我们用W来表示,高用H来表示,则左下到右上的对角线长度就应该是 默然说话:著名的勾股定理还记得吧?斜边的平方等于两个直角边的平方和,所以斜边长就是两个直角边的平方和开根),计算对角线的长度干嘛?其实这不是我们的目的,我们的目的是为了得到对角线到pt2的距离。这里我们作了一条辅助线L,它平行于pt1pt2的连线,也垂直于对角线。根据定律, L的长度与对角线到pt2的长度是相等的,也就是说,求出了L的长度,也就得到了pt2到对角线的长度。接下来请看演算:(默然说话:下面将要使用三角函数,我就不多罗嗦了,记不得的同学请参考别的书籍

一方面,如果我们把H当作α所在的直角三角形的对边,那就有 成立。

另一方面,如果我们把L当作α所在直角三角形的对边,那就有 成立。

(默然说话:好吧,我就再多罗嗦几句。正弦就是对边比斜边,如果把H作为对边,那么,斜边就是对角线,而对角线长度的计算,前面已经写过了。如果以L为对边,那斜边就是W。所以就有了上面的两个等式)

根据等量代换原则,就有 成立。

整理后得到

这样我们就计算出了L的长度,也就是pt2到对角线的距离。在这里重要的是,我们找到了pt2的位置与长和宽的关系,同理可证pt1的位置与长和宽的关系也是相似的。

默然说话:不用多想α是多少,因为它只是我们的一个引入的中间变量,对于我们研究的问题,毫无关系。我只是想找出pt1pt2的位置和长宽的关系而引入了它而已。

下面的程序在构造函数中建立一个“可以被修改的”LinearGradientBrush对象,其MappingModeAbsolute。构造函数中还委托了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向量垂直于对角线。建立相互垂直的向量很容易,只要把XY属性的值对调,并把其中一个数的正负号反向就可以了。调用Normalize方法是为了正规化这个向量,之后,又把vectPerp乘以L默然说话:就是前面我们计算过的那个公式),这样就得到了一个pt1pt2的一个向量。

(默然说话:是不是还是觉得晕?看不明白?嘿嘿,说实话,我也看不明白这一段呀,实在是超出了我的所学范围,仍然盼望高手解惑。)

最后的步骤是设定StartPointEndPoint属性。这些属性一般是通过画刷的构造函数来设定的,而且除去继承的属性,它们是LinearGradientBrush仅有的两个属性。

WPF 中,使用多线程和定时器可以帮助我们更好地管理界面的响应和处理复杂的任务。下面介绍一些常用的线程和定时器相关的类和方法。 线程相关的类和方法: 1. Thread 类:表示一个独立的线程,可以使用 Start 方法启动线程,使用 Join 方法等待线程结束。 2. ThreadPool 类:表示一个线程池,可以使用 QueueUserWorkItem 方法将任务添加到线程池中执行。 3. Dispatcher 类:表示一个 WPF 程序的消息循环,可以使用 Invoke 或 BeginInvoke 方法在 UI 线程上执行代码。 定时器相关的类和方法: 1. Timer 类:表示一个定时器,可以使用 Start 方法启动定时器,使用 Stop 方法停止定时器。 2. DispatcherTimer 类:表示一个 WPF 程序的定时器,可以使用 Start 方法启动定时器,使用 Stop 方法停止定时器。 下面是一个使用多线程和定时器的示例代码: ```csharp public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); // 创建一个新线程,并将任务添加到线程池中执行 ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork)); // 创建一个定时器,并设置间隔时间为1秒 DispatcherTimer timer = new DispatcherTimer(); timer.Interval = TimeSpan.FromSeconds(1); timer.Tick += Timer_Tick; timer.Start(); } // 定时器的 Tick 事件处理方法 private void Timer_Tick(object sender, EventArgs e) { // 在 UI 线程上更新界面 Dispatcher.Invoke(() => { lblTime.Content = DateTime.Now.ToString(); }); } // 新线程执行的任务 private void DoWork(object state) { // 在新线程上执行耗时操作 Thread.Sleep(5000); // 在 UI 线程上更新界面 Dispatcher.Invoke(() => { lblResult.Content = "任务完成!"; }); } } ``` 在上面的代码中,我们使用了一个新线程来执行一个耗时的任务,并使用定时器在 UI 线程上更新界面。通过多线程和定时器的结合使用,我们可以更好地管理界面的响应和处理复杂的任务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

默然说话

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值