当前位置: 代码迷 >> 综合 >> 内容的概念 - 《Windows Presentation Foundation 程序设计指南》 - 免费试读 - book.csdn.net
  详细解决方案

内容的概念 - 《Windows Presentation Foundation 程序设计指南》 - 免费试读 - book.csdn.net

热度:37   发布时间:2024-01-18 17:34:21.0

导读:


本文转自
http://book.csdn.net/bookfiles/591/10059119377.shtml


 


第3章  内容的概念


Window类有超过100个公开的(public)property,其中有一些(像是对应到标题的Title property)相当重要。但是,迄今为止,Window最重要的property是Content。你想要在窗口的客户区显示什么东西,将它设定给Content property即可。


你可以将Content设定成一个字符串,也可以将它设定成位图(bitmap),还可以将它设定成一张矢量图(drawing),设定成按钮、滚动条(scrollbar),或者是WPF所支持的50多个控件。你几乎可以将Content property设定成任何东西,但是有一个小问题:


你只能设定“一个”东西给Content property。


刚开始使用Content时,这个限制会让你觉得沮丧。当然,后来你会学会如何让Content property的对象当宿主(host),以容纳多个其他的对象。至于现在,光是单一内容对象就够我们研究好一阵子。


Window类的Content property是从ContentControl类继承来的。CotentControl继承自Control,而Window直接继承自ContentControl。ContentControl类存在的目的,几乎就只是为了要定义Content property以及几个相关的property和方法。


Content property被定义为Object类型,这似乎暗示着它可以是任何对象,事实也相去不远。我说“相去不远”,是因为你不可以把Content property设定成另一个Window类型的对象。这么做会使运行时期出现异常,异常信息提醒开发者:Window必须是“树根”,而不可以是另一个Window对象的分支。


你可以将Content property设为字符串,如下面的例子:


DisplaySomeText.cs


//------------------------------------------------


// DisplaySomeText.cs (c) 2006 by Charles Petzold


//------------------------------------------------


using System;


using System.Windows;


using System.Windows.Input;


using System.Windows.Media;


namespace Petzold.DisplaySomeText


{


    public class DisplaySomeText : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new DisplaySomeText());


        }


        public DisplaySomeText()


        {


            Title = "Display Some Text";


            Content = "Content can be simple text!";


        }


    }


}


此程序在客户区左上角显示文字“Content can be simple text!”。如果此窗口太窄而无法容纳所有的文字,你会看到文字尾端被截掉,而不是被自动放到下一行,但是你可以在文字中主动插入换行符,可以是CR字符(“/r”)或者是LF字符(“/n”),或者两者一起出现(“/r/n”)。


此程序包含一个using指示符来加入System.Windows.Media命名空间,以方便使用那些可以影响文字颜色和字体的property。这些property都是Window从Control类所继承来的。


除了你品味的高低,没什么东西可以阻碍你设定用何种字体来显示客户区文字,作法如下:


FontFamily = new FontFamily("Comic Sans MS");


FontSize = 48;


这段代码可以放在此类的构造函数内的任何地方。


在WPF中,没有所谓的Font类。这里第一行使用到一个FontFamily对象。Font family(也称为type family)是相关字体的collection。在Windows操作系统中,font family有一些大家相当熟悉的名字,像是Courier New、Times New Roman、Arial、Palatino Linotype、Verdana和Comic Sans MS等。


Typeface(也称为face name)是“font family”和“变化”的结合,例如Time New Roman Bold(粗)、Times New Roman Italic(斜)和Times New Roman Bold Italic(粗斜)。并非所有的font family都支持每一种变化;某些font family具有“影响字符宽度”的变化,例如Arial Narrow(窄)。


Font这个词一般来说表示特定的typeface和尺寸的结合。常用的字型测量法是“em size”(这个名称是有典故的,在早期活字印刷时代,大写的M是正方形金属,用M的尺寸来作代表。M的发音正是em)。通常用em size来代表拉丁字母(拉丁字母是大写的和小写从A到Z的字母,不包含那些使用“区别符号”的字符)的字符高度,从高部(ascender)的顶端,到低部(descender)的底端。(译者注:高部指的是比小写x高的部分,低部指的是比小写x低的部分。)然而,em size并非公制(metrical)的概念,只是一种设计的概念。在特定的font


中,字符的实际尺寸会比em size的值略大或略小。


一般来说,em size的指定,是以“点”为单位的。在传统排版中一个点是0.01384英寸,以电脑排版来说,点被精确地认定为1/72英寸。因此,36点的em size(常常缩略的写成“36点font”)代表的是“字符的高度约为1/2英寸”。


在WPF中,利用FontSize property指定em size,但是不用点作单位。和所有的WPF度量方式一样,FontSize使用“与设备无关的单位”,也就是1/96英寸。将FontSize property设定成48,会导致em size为1/2英寸,等同于36点。


如果你习惯用点(point)当单位来指定em size,在指定FontSize property时,只要将点的尺寸乘以4/3(或者除以0.75)就可以了;如果你还不习惯用点来指定em size,那么你应该要习惯这么做,只要在指定FontSize property时,将点的尺寸乘以4/3即可。


默认的FontSize property是11,也就是8.25点。纽约时报大量使用8点当作印刷字的大小;新闻周刊则是使用9点的字。本书的原文使用10点的字。


你应该在FontFamily的构造函数中使用完整的type face名称:


FontFamily = new FontFamily("Times New Roman Bold Italic");


FontSize = 32;


这是24点的Times New Roman Bold Italic字体。然而,比较惯用的方式,是在FontFamily构造函数中指定font family名称,然后在FontStyle和FontWeight property中指定粗体和斜体:


FontFamily = new FontFamily("Times New Roman");


FontSize = 32;


FontStyle = FontStyles.Italic;


FontWeight = FontWeights.Bold;


请注意,FontStyle和FontWeight property值是设定成FontStyles(复数)类和FontWeights(复数)类的静态只读property。这些静态property都会返回FontStyle和FontWeight类型的对象,这些对象都是“限定自己使用”的结构体。


下面是一个有趣的小改变:


FontStyle = FontStyles.Oblique;


斜体(Italic)的typeface常常会在文体上和非斜体(或称正体roman)的typeface有差异。看看小写的“a”你就会知道怎么回事了。但是倾斜(oblique)的typeface却是直接拿正体字母来作向右倾斜处理。某些font family可以让你用FontStretches类的静态property,来设定FontStretch property。


你已经熟悉了用来将客户区着色的Background property,而Foregound property的着色对象则是文字本身。试试下面的代码:


Brush brush = new LinearGradientBrush(Colors.Black, Colors.White,


                                      new Point(0, 0), new Point(1, 1));


Background = brush;


Foreground = brush;


前景和背景都使用相同的画刷,你可能会担心看不见文字,然而,不会有这样的事发生。在第2章我们就看到过,渐变画刷用来进行背景着色时,会默认地根据客户区的尺寸作调整。类似的,前景画刷会自动地根据内容(实际的字符串)的尺寸作调整。改变窗口的尺寸不会影响前景画刷,但是当你让客户区和文字的尺寸完全一样时,这两个画刷会完全重叠,造成文字和背景之间完全融合,看不见文字。


现在试试这个:


SizeToContent = SizeToContent.WidthAndHeight;


Window类的SizeToContent property造成窗口大小会根据内容的尺寸作调整。如果还是让背景和前景使用相同的LinearGradientBrush,你会看不见文字。你可以将SizeToContent property设定为某个SizeToContent枚举值:Manual(默认)、Width、Height或WidthAndHeight。后面3个会让窗口根据内容的宽和高来作调整。这是一个相当简单的property,常常用在对话框或者表单(form)中。当设定窗口大小符合内容的尺寸时,你也常常会想要用下面的方式除去“调整尺寸的边框”:


ResizeMode = ResizeMode.CanMinimize;



ResizeMode = ResizeMode.NoResize;


你可以为第2章最后的代码加上客户区内的边框:


BorderBrush = Brushes.SaddleBrown;


BorderThickness = new Thickness(25, 50, 75, 100);


你会看到前景画刷和SizeToContent都会考虑到边框。内容一定会出现在此边框以内。


DisplaySomeText程序所显示出来的问题,其实比乍看之下更一般化。你知道,所有的对象都具备ToString方法,此方法会返回一个“代表对象”的字符串。这里的窗口正是使用ToString方法来展示对象。试着把Content property设定成别的对象,你就会相信了:


Content = Math.PI;


或者试试下面的:


Content = DateTime.Now;


不管是上面哪一个例子,窗口所显示出来的内容都会和ToString传出来的字符串一模一样。如果这里的对象没有覆盖(override)ToString方法,那么默认的ToString方法会显示出完整的类型名称(包含命名空间)。例如:


Content = EventArgs.Empty;


这会显示出“System.EventArgs”字符串。我只发现了一个例外,即如果你用下面的做法:


Content = new int[57];


窗口会显示出字符串“Int32[ ]Array”,而ToString方法返回的却是“System.Int32[]”。


下面的程序设定Content property为空字符串,然后将键盘输入的字符加入其中。这个程序类似第1章的TypeYourTitle程序,不过这个版本还可以让你输入CR(换行)和Tab(跳格)。


RecordKeystrokes.cs


//-------------------------------------------------


// RecordKeystrokes.cs (c) 2006 by Charles Petzold


//-------------------------------------------------


using System;


using System.Windows;


using System.Windows.Input;


using System.Windows.Media;


namespace Petzold.RecordKeystrokes


{


    public class RecordKeystrokes : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new RecordKeystrokes());


        }


        public RecordKeystrokes()


        {


            Title = "Record Keystrokes";


            Content = "";


        }


        protected override void OnTextInput(TextCompositionEventArgs args)


        {


            base.OnTextInput(args);


            string str = Content as string;


            if (args.Text == "/b")


            {


                if (str.Length > 0)


                    str = str.Substring(0, str.Length - 1);


            }


            else


            {


                str += args.Text;


            }


            Content = str;


        }


    }


}


RecordKeystrokes程序之所以能有这样的效果,是因为每当有按键被按下,Content property就会被改变,且Window类会负责把新的内容绘制到客户区。稍微做一个小改动,可能就会让这个程序无法如我们所愿地正常运行。比方说,定义空字符串字段(field):


string str = "";


在构造函数中,将Content property设定成这个变量:


Content = str;


从OnTextInput中删除相同的语句,并且也删除(原来的)str的定义。现在,这个程序就不正常了。看看OnTextInput内的一条复合赋值语句:


str += args.Text;


它等同于下面的语句:


str = str + args.Text;


这两条语句的问题在于:字符串加法(字符串连接)的结果是一个新的字符串对象,而不是原来的字符串对象。别忘了,字符串是固定不变的(immutable)。字符串连接之后产生新字符串,但是Content property依然指向原来的字符串。


现在,试着做一些改变。你需要将System.Text命名空间加进来,定义一个StringBuilder对象类型的字段:


StringBuilder build = new StringBuilder("text");


在构造函数中,将Content设定为该对象:


Content = build;


你应该知道窗口会显示出“text”,因为StringBuilder对象的ToString方法会返回它


内部的字符串,将OnTextInput方法内的代码块替换成:


if (args.Text == "/b")


{


    if (build.Length > 0)


         build.Remove(build.Length - 1, 1);


}


else


{


    build.Append(args.Text);


}


又一次,程序不能正常执行了。虽然此程序只有一个StringBuilder对象,但窗口无法知道StringBuilder对象内的字符串何时发生了改变,所以无法用新的字符串更新窗口。


我之所以指出这些例子,是因为有时候WPF的对象似乎会很神奇地自动更新自己。其实,里面运作的原理一点都不神奇,一定是某事件通知某个方法,画面才会被更新。知道将会采用的运作方式,可以帮助你更了解环境。使用StringBuilder对象的改版RecordKeystrokes程序根本不能运行,如果在OnTextInput方法的底部插入下面的代码:


Content = build;


窗口够聪明,它知道你指定相同的对象给Content(多此一举),所以它不会进行更新,但如果你这么做:


Content = null;


Content = build;


程序就恢复正常了。


我们看到窗口内容可以是纯文字,但是Content property存在的目的只是显示简单的无格式字符串吗?不,不是,当然不是!Content property真正需要的是本质上更图形化的东西,由UIElement继承而来的类的实例。


在WPF中,UIElement是一个极为重要的类。它实现键盘、鼠标以及手写笔(stylus)事件的处理。UIElement类也包含一个很重要的方法,名为OnRender。OnRender方法被调用,以显示出对象的外观。(本章结束时会有这样的例子)


在Content property的世界中,分成两组对象:一组继承自UIElement,另一组则不是。后面这一组的显示结果,就是ToString方法的返回值;前面这一组,则是利用OnRender来显示。继承自UIElement的类(以及其对象)被称为element。


唯一直接继承UIElement的类是FrameworkElement,在WPF中所有的element都是继承自FrameworkElement。理论上,UIElement提供关于用户界面和屏幕显示的必要结构,可以支持各种各样的编程框架(programming framework)。WPF正是这样的框架,它包含继承自FrameworkElement的所有类。实际上,你不太可能会去区分到底哪些property、方法、事件是由UIElement所定义,哪些又是由FrameworkElement所定义的。


Image是继承自FrameworkElement的一个很常见的类型。下面是Image的继承层次图:


Object


    DispatcherObject (abstract)


          DependencyObject


                Visual (abstract)


                      UIElement


                           FrameworkElement


                                Image


Image(图像)类让你可以轻易地在文件(document)或者应用内包含影像。下面的程序从网站上获得一幅位图,然后在窗口上显示出来:


ShowMyFace.cs


//-------------------------------------------


// ShowMyFace.cs (c) 2006 by Charles Petzold


//-------------------------------------------


using System;


using System.Windows;


using System.Windows.Controls;


using System.Windows.Input;


using System.Windows.Media;


using System.Windows.Media.Imaging;


namespace Petzold.ShowMyFace


{


    class ShowMyFace : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new ShowMyFace());


        }


        public ShowMyFace()


        {


            Title = "Show My Face";


            Uri uri = new Uri("http://www.charlespetzold.com/PetzoldTattoo.jpg");


            BitmapImage bitmap = new BitmapImage(uri);


            Image img = new Image();


            img.Source = bitmap;


            Content = img;


        }


    }


}


想要显示图像,需要几个步骤。此程序先建立一个Uri对象,用来表示位图的位置。然后将Uri对象传进BitmapImage构造函数当参数,构造函数会将此图像载入内存(WPF支持许多格式,包括GIF、TIFF、JPEG与PNG)。想要利用Image类在窗口上显示出图像,你只要将Image类的实例(instance)指定给窗口的Content property即可。


你可以在System.Windows.Controls命名空间找到Image类。严格来说,Image不算是控件(control),它并非继承自Control类。但是System.Windows.Controls命名空间是如此的重要,所以从这里开始,我几乎把它加到后面所有的程序中。BitmapImage类位于System.Windows.Media.Image命名空间。如果你会用到位图,这就是很重要的命名空间。然而,除非真的需要,否则我不会在程序中把此命名空间加进来。


另外,你也可以从本地硬盘获得图像。这时候需要用文件的绝对路径,作为Uri构造函数的参数,也可以用相对文件名,但前面必须写上“file://”。 下面是替换的代码,用来动态地获得渔夫处理鳟鱼的图像:


Uri uri = new uri (


    System.IO.Path.Combine(


        Environment.GetEnvironmentVariable("windir"), "Gone Fishing.bmp")) ;


Environment.GetEvironmentVariable方法取出“windir”环境变量,其值看起来像是“C:/WINDOWS”这样的字符串。Path Combine方法结合图片文件的路径名称和文件名,这样我(懒惰的程序员)就不用费心正确地插入斜杠了。你可以在Path类型名称的前面冠上System.IO(我就是这么做的)或者利用using指示符将此命名空间加进来。


你可以不用将Uri对象传给BitmapImage构造函数,而是将Uri对象设定给BitmapImage的UriSource property。不过,建议你在设定此property的前后,要分别调用BitmapImage的BeginInit和EndInit方法:


bitmap.BeginInit();


bitmap.UriSource = uri;


bitmap.EndInit();


你已经知道,位图具有以像素(pixel)为单位的宽和高,这样的信息被编码在文件中。BitmapImage类从BitmapSource类继承到整数类型,只读的PixelWidth和PixelHeight property。Bitmap常常(但并非绝对)具有内置的分辨率消息。有时候此分辨率消息非常重要,有时候则不重要。你可以从DpiX和DpiY property得到以DPI(dot per inch,一英寸的点数)为单位的分辨率信息,这是只读的,且类型为double。BitmapImage类也包含只读的Width与Height。这些值是用下面的式子来计算的:




分子如果没有96,这些式子计算出的宽和高以英寸为单位。乘以96就可以将英寸转成设备无关单位。Height和Width property描述的位图尺寸使用和设备无关单位。BitmapImage也从BitmapSource继承一个名为Format的property,让我们可以得知位图的格式;对于那些使用“调色板”的位图,Palette property让我们可以取得调色板。


不管ShowMyFace程序是显示我的脸,还是鳟鱼和渔夫,或者是你选择的任何图像,你会注意到在窗口的边界限制之下,图像会尽量用最大的方式显示,而且不会失真(也就是会维持一样的长宽比)。除非客户区的长宽比(aspect ratio)和图像的长宽比一样,否则你会看到客户区的一部分背景(不是在图像的上下,就是在左右)。


窗口内的图像的尺寸受到Image自己的几个property的控制,其中一个是Stretch(拉伸)。默认情况下,Stretch property等于Stretch.Uniform,这表示图像会均匀的(uniformly)增大或者缩小(也就是说,水平和垂直方向会一样),以填满客户区。


你也可以将Stretch property设定成Stretch.Fill:


img.Stretch = Stretch.Fill;


这样的设定方式,会造成图像填满整个窗口,一般来说,图像会失真,因为水平和垂直增加的比例不同。Stretch.UniformToFill则是将图像均匀地拉伸,但是会把整个客户区都完全覆盖,超出的部分会被截掉。


Stretch.None选项造成图像以原始尺寸显示,也就是依照BitmapSource的Width和Height property。


只要Stretch property不是Stretch.None,你也可以设定Image的StretchDirection property。此property默认值是StretchDirection.Both,这表示图像可以大于或者小于其原始尺寸。


StretchDirection.DownOnly表示图像不可以比原始尺寸大,StretchDirection.UpOnly则表示不可以比原始尺寸小。


不管你的图像尺寸如何,图像总是放在窗口中央(除非Stretch.UniformToFill)。你可以改变这一点——设定Image的HorizontalAlignment和VerticalAlignment property(这是从FrameworkElement继承来的property)。比方说,下面的代码将图像移动到客户区的右上角:


img.HorizontalAlignment = HorizontalAlignment.Right;


img.VerticalAlignment = VerticalAlignment.Top;


HorizontalAlignment与VertialAlignment property在WPF的layout(版面布局)中扮演相当重要的角色。你将会一再遇到这些property。如果你希望Image对象出现在右上角,但是不要紧贴着边界,你可以在Image对象的周围设定边界(margin):


img.Margin = new Thickness(10);


Margin是FrameworkElement所定义的property,常常用来在element之间插入隔离空间。你可以使用Thickness结构体来定义边界,每边都一样(在本例中,10/96英寸或约0.1英寸),或者每边都不一样。你应该记得,Thickness构造函数有4个参数,依次用来设定上、下、左、右:


img.Margin = new Thickness(192, 96, 48, 0);


现在边界是左边2英寸,上面1英寸,右边1/2英寸,下面没有边界。如果你将窗口调整到很小,图像会比边界先消失,显然边界比较受重视。


Image对象也具有Width和Height property(从FrameworkElement继承来的),它们是可读写的double值。如果你查看它们的值,你会发现他们没有定义,值为NaN(这和Window对象的这两个同名property一样)。你也可以为Image对象设定精确的Width和Height,不过有可能会和某些Stretch的设定不一致。


你也可以将窗口的尺寸调整到图像的原始尺寸:


SizeToContent = SizeToContent.WidthAndHeight;


设定Window对象的Foreground property,对于图像的显示不会有影响。Foreground property只会对于文字内容有影响,或者对于会显示文字的element有影响(稍后有例子)。


正常的情况下,为Window设定Background property,只会影响没有被图像覆盖的区域。但是,试试下面的代码:


img.Opacity = 0.5;


Background = new LinearGradientBrush(Colors.Red, Colors.Blue,


                         new Point (0, 0), new Point (1, 1));


现在通过图像,还是会看到画刷。Opacity property(这是Image从UIElement继承而来的property)默认是1,但是你可以将它设定成0到1之间的任何值,让element变得透明。(然而,如果是Window对象,这个property是无效的。)


第29章对于图形变换(transform)有完整的讨论,不过目前你可以看到转动一个位图相当容易:


img.LayoutTransform = new RotateTransform(45);


Image类不具有自己的Background和Foreground property,因为这两个property是由Control类所定义的,而Image并非继承自Control。一开始可能不太容易区别。在早期的Windows API(application programming interface)中,几乎屏幕上所有的东西,都被认为是控件(control),现在却不是如此。控件是视觉对象(visual object),其特征是对用户的输入有反应。像Image这样的element,当然可以得到用户的输入,因为所有的键盘、鼠标、手写笔的输入事件都是由UIElement所定义的。


看一下System.Windows.Shapes这个命名空间,它包含了名为Shape的抽象类,以及6个子类。这些类也继承自UIElement、FrameworkElement:


Object


    DispatcherObject (abstract)


         DependencyObject


              Visual (abstract)


                    UIElement


                         FrameworkElement


                              Shape (abstract)


                                    Ellipse


                                    Line


                                    Path


                                    Polygon


                                    Polyline


                                    Rectangle


虽然Image是显示点阵图像(raster图像)的标准做法,这些Shape类实现了简单的二维(two-dimensional)矢量图(vector graphics)。下面的程序创建一个椭圆(Ellipse)类的对象:


ShapeAnEllipse.cs


//-----------------------------------------------


// ShapeAnEllipse.cs (c) 2006 by Charles Petzold


//-----------------------------------------------


using System;


using System.Windows;


using System.Windows.Controls;


using System.Windows.Input;


using System.Windows.Media;


using System.Windows.Shapes;


namespace Petzold.ShapeAnEllipse


{


    class ShapeAnEllipse : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new ShapeAnEllipse());


        }


        public ShapeAnEllipse()


        {


            Title = "Shape an Ellipse";


            Ellipse elips = new Ellipse();


            elips.Fill = Brushes.AliceBlue;


            elips.StrokeThickness = 24; // 1/4 inch


            elips.Stroke =


                new LinearGradientBrush(Colors.CadetBlue, Colors.Chocolate,


                                        new Point(1, 0), new Point(0, 1));


            Content = elips;


        }


    }


}


该椭圆会填满客户区。周长线是1/4英寸粗,且使用渐变画刷。椭圆内部(interior)使用AliceBlue画刷来着色。这个颜色以是美国总统罗斯福(Teddy Roosevelt)的女儿来命名。


不管是Shape类还是Ellipse类,都没有定义任何property可以让我们用来设定椭圆的尺寸,但是Ellipse类从FrameworkElement类继承到Width和Height property,这两个property正是作此用途的:


elips.Width = 300;


elips.Height = 300;


和Image一样,你现在可以使用HorizontalAlignment和VerticalAlignment property来设定椭圆的位置,是在中间,水平对齐于左、右,或者垂直对齐于上、下:


elips.HorizontalAlignment = HorizontalAlignment.Left;


elips.VerticalAlignment = VerticalAlignment.Bottom;


HorizontalAlignment与VerticalAlignment枚举类型都有名为Center的成员,也有名为Stretch的成员。对于许多element来说,默认使用Stretch。Ellipse的默认就是Stretch,这也就是为什么Ellipse一开始会填满整个客户区,Element会拉伸到容器的边界。事实上,如果你设定HorizontalAlignment和VerticalAlignment为Stretch以外的值,且没有同时设定Width和Height property,此椭圆将会缩小(collapse)成1/4英寸的小球,只看得到圆周。


然而,如果你没有设定Width和Height property,你可以设定MinWidth、MaxWidth、MinHeight和MaxHeight property(全都是从FrameworkElement继承而来的),其中任何一个,或者全部,以限制椭圆尺寸在特定的范围内。默认状况下,这些property都没有被定义。任何时候(除了窗口构造函数内不可以),程序可以利用只读的ActualWidth和ActualHeight property来得知椭圆的尺寸。


如果你认为我对于“作为窗口内容的element”的尺寸大小表现得过度关心,那是因为它是一个重要议题。你或许习惯为控件和图形对象指定特定的尺寸,但是WPF不需要如此死板,所以你需要好好地了解视觉元件是如何被调整尺寸的。


在Ellipse类内,你找不到可以让你将椭圆指定到客户区特定位置的property,HorizontalAlignment和VerticalAlignment算是最接近这种目的的property了。


之前我向你演示过如何将Window的Content property设定为字符串,以及如何设定此文字的font。然而,你直接设定到Content property的文字,总具有相同的格式,比方说,你无法将其中几个字设定为粗体或斜体。


如果你需要这样,就不要将Content property 设定成字符串,而是设定成一个TextBlock类型的对象:


FormatTheText.cs


//----------------------------------------------


// FormatTheText.cs (c) 2006 by Charles Petzold


//----------------------------------------------


using System;


using System.Windows;


using System.Windows.Controls;


using System.Windows.Input;


using System.Windows.Media;


using System.Windows.Documents;


namespace Petzold.FormatTheText


{


    class FormatTheText : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new FormatTheText());


        }


        public FormatTheText()


        {


            Title = "Format the Text";


            TextBlock txt = new TextBlock();


            txt.FontSize = 32; // 24 points


            txt.Inlines.Add("This is some ");


            txt.Inlines.Add(new Italic(new Run("italic")));


            txt.Inlines.Add(" text, and this is some ");


            txt.Inlines.Add(new Bold(new Run("bold")));


            txt.Inlines.Add(" text, and let’s cap it off with some ");


            txt.Inlines.Add(new Bold(new Italic(new Run("bold italic"))));


            txt.Inlines.Add(" text.");


            txt.TextWrapping = TextWrapping.Wrap;


            Content = txt;


        }


    }


}


虽然,这是本书第一个主动创建TextBlock对象的程序,但其实你以前就见过TextBlock了。如果你将Content property设定成字符串,那么ContentControl(这是Window的祖先类)会先创建一个Text Block类型的对象,以实际显示出此字符串。Text Block类直接继承自FrameworkElement,它定义了Inlines property(类型为InlineCollection,这是Inline对象的collection)。


TextBlock本身是属于System.Windows.Controls命名空间。但是Inline是属于System.Windows.Documents命名空间,而且甚至不是继承自UIElement。下面是部分的类集成图,显示出Inline和它的某些后代:


Object


    DispatcherObject (abstract)


          DependencyObject


              ContentElement


                   FrameworkContentElement


                         TextElement (abstract)


                        Inline (abstract)


                              Run


                              Span


                                  Bold


                                  Hyperlink


                                  Italic


                                  Underline


你可能注意到这个类层次跟之前的似乎有类似的结构——ContentElement和FrameworkContentElement类对比于UIElement和FrameworkElement类。然而,ContentElement类不包含OnRender方法。继承自ContentElement的类所产生的对象,不会在屏幕上把自己画出来。它们却是要通过“继承自UIElement的类” 才能在屏幕上达到视觉的演示,由“继承自UIElement的类”提供它们所欠缺的OnRender方法。


更明确地说,Bold和Italic对象不会自己绘制自己。在FormatTheText程序中,这些Bold和Italic对象都是通过TextBlock对象才能显示出来的。


不要把ContentElement类和ContentControl混为一谈。ContentControl是控件,像Window一样,可以具有Content property。就算Content property是空的,ContentControl对象还是会在屏幕上显示自己。而ContentElement对象一定要是其他可以显示的对象的一部分(也就是说,必须是控件的内容),才能得以显示。


FormatTheText程序用来组合TextBlock的Inlines collection。InlineCollection类实现了Add方法,可以加入字符串对象、Inline对象以及UIElement对象(最后一个让你在TextBlock中嵌入其他element)。然而,Bold和Italic构造函数只接受Inline对象,而不接受字符串对象,所以程序先为每个Bold或Italic对象使用Run构造函数。


下面的代码设定TextBlock的FontSize property:


txt.FontSize = 32;


然而,如果改变窗口的FontSize property,效果也会一样:


FontSize = 32;


类似的,下面的代码可以设定窗口的Foreground property,然后TextBlock的文字也会变成这个颜色:


Foreground = Brushes.CornflowerBlue;


屏幕上的element,呈现树状的组织方式。窗口是TextBlock的父亲(parent),而TextBlock又是数个Inline element的父亲。除非孩子明确地设定某些property,否则直接从此树状结构中,沿袭父亲的Foreground property和所有文字相关的property。在第8章,你会看到实际的作用。


类似UIElement类,ContentElement类定义了许多用户输入事件,可以把事件处理器(event handler)安装到“由TextBlock显示出来的”Inline elements上。下面的程序展示此技巧。


ToggleBoldAndItalic.cs


//----------------------------------------------------


// ToggleBoldAndItalic.cs (c) 2006 by Charles Petzold


//----------------------------------------------------


using System;


using System.Windows;


using System.Windows.Controls;


using System.Windows.Documents;


using System.Windows.Input;


using System.Windows.Media;


namespace Petzold.ToggleBoldAndItalic


{


    public class ToggleBoldAndItalic : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new ToggleBoldAndItalic());


        }


        public ToggleBoldAndItalic()


        {


            Title = "Toggle Bold & Italic";


            TextBlock text = new TextBlock();


            text.FontSize = 32;


            text.HorizontalAlignment = HorizontalAlignment.Center;


            text.VerticalAlignment = VerticalAlignment.Center;


            Content = text;


            string strQuote = "To be, or not to be, that is the question";


            string[] strWords = strQuote.Split();


            foreach (string str in strWords)


            {


                Run run = new Run(str);


                run.MouseDown += RunOnMouseDown;


                text.Inlines.Add(run);


                text.Inlines.Add(" ");


            }


        }


        void RunOnMouseDown(object sender, MouseButtonEventArgs args)


        {


            Run run = sender as Run;


            if (args.ChangedButton == MouseButton.Left)


                run.FontStyle = run.FontStyle == FontStyles.Italic ?


                    FontStyles.Normal : FontStyles.Italic;


            if (args.ChangedButton == MouseButton.Right)


                run.FontWeight = run.FontWeight == FontWeights.Bold ?


                    FontWeights.Normal : FontWeights.Bold;


        }


    }


}


此构造函数将哈姆雷特(Hamlet)中的一句话分割成为多个单词(word),然后根据每个单词创建一个Run对象,再将这些单词放在TextBlock对象的Inlines collection中。在此过程中,程序也会将RunOnMouseDown事件处理器连接到每个Run对象的MouseDown事件中。


Run类从TextElement类继承了FontStyle和FontWeight property,且事件处理器会根据被按下的鼠标按钮类型,来改变这些property。如果是是鼠标左键,并且FontStyle property目前是FontStyles.Italic,那么事件处理就会将此property设定成FontStyle.Normal;如果此property目前是FontStyle.Normal, 那么事件处理器会将它改变成FontStyle.Italic。类似地,FontWeight property也会在FontWeights.Normal和FontWeights.Bold之间切换。


我在前面曾经提到,窗口的Content property其实想要的是继承自UIElement类的实例,因为此类定义一个名为OnRender的方法,负责在屏幕上显示此对象。本章最后一个程序名为RenderTheGraphic,需要两个源代码文件:第一个文件是一个自定义(custom)的element类,第二个文件将该类的实例设置为窗口的content。下面的类继承自FrameworkElement(这是直接继承自UIElement的唯一类别),它将相当重要的OnRender方法予以override,以获得一个DrawingContext对象。通过此对象和DrawEllipse方法就可以绘制椭圆。此类单纯地模仿System.Windows.Shapes命名空间的Ellipse类。


SimpleEllipse.cs


//----------------------------------------------


// SimpleEllipse.cs (c) 2006 by Charles Petzold


//----------------------------------------------


using System;


using System.Windows;


using System.Windows.Media;


namespace Petzold.RenderTheGraphic


{


    class SimpleEllipse : FrameworkElement


    {


        protected override void OnRender(DrawingContext dc)


        {


            dc.DrawEllipse(Brushes.Blue, new Pen(Brushes.Red, 24),


                new Point(RenderSize.Width / 2, RenderSize.Height / 2),


                RenderSize.Width / 2, RenderSize.Height / 2);


        }


    } 


}


在OnRender还没有被调用之前,根据可能的Width和Height设定值以及该类和它的容器之间的协商,RenderSize property可以被确定下来。


如果你之前就有Windows编程经验(或者即便你没有什么经验),你可能会认为此方法直接在屏幕上绘制椭圆,其实不是这样的。DrawEllipse的参数会被保留,以供以后在屏幕上显示椭圆。所谓“以后”,也有可能是马上,但是会将不同来源的图形集中在一起,然后在屏幕上进行组合。WPF可以变出更多这样子的图形把戏。


下面的程序,创建一个SimpleEllipse对象,然后将它设定给Content property:


RenderTheGraphic.cs


//-------------------------------------------------


// RenderTheGraphic.cs (c) 2006 by Charles Petzold


//-------------------------------------------------


using System;


using System.Windows;


namespace Petzold.RenderTheGraphic


{


    class RenderTheGraphic : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new ReplaceMainWindow());


        }


        public ReplaceMainWindow()


        {


            Title = "Render the Graphic";


            SimpleEllipse elips = new SimpleEllipse();


            Content = elips;


        }


    }


}


椭圆会填满客户区。当然,你也会想要实验一下SimpleEllipse的Width与Height property,以及HorizontalAlignment与VerticalAlignment property,做不同设定时的影响会如何。


虽然本章使用到了System.Windows.Controls命名空间的element,但是没有使用任何继承自Control的类(当然Window类本身是个例外)。控件是设计来取得用户的输入,并作出反应的。在下一章,你会明白这是怎么一回事。

  相关解决方案