WPF程序设计指南读书笔记(3-1)

 

Window类最重要的属性就是Content。你想要在窗口的客户区显示什么东西,将它设定给Content属性即可。
你可以将Content设定成一个字符串,也可以将它设定成位图,还可以将它设定成一张矢量图,或者是WPF所支持的50多个控件。你几乎可以将Content属性设定成任何东西,但是有一个小问题:你只能设定“一个”东西给Content属性,不过别急,后面自然会教你如何把多个“东西”都装到Content里。
Window类的Content属性是从ContentControl类继承来的。ContentControl继承自Control,而Window直接继承自ContentControl。
Content属性是一个Object类型,这暗示着它可以是任何对象,不过你要记住的是,Content不能被设定为Window的对象,否则会引发运行时异常。
下面的例子告诉你,你可以把它设定为一个字符串:
//*********************************************************
//DisplaySomeText.cs 2010 1st Auguest by mouyong
//*********************************************************
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
 
namespace part1.ch03
{
    class DisplaySomeText:Window
    {
        [STAThread]
        public static void Main()
        {
            Application app = new Application();
            app.Run(new DisplaySomeText());
        }
 
        public DisplaySomeText()
        {
            Title = "显示文字";
            Content = "Content可以用来显示字符串";
        }
    }
}
此程序在客户区的左上角显示“Content可以用来显示字符串”。文字不会自动换到下一行,除非你使用转义字符“/n”或“/r/n”进行强制换行。
你还可以任意改变字体和它的大小,如下:
FontFamily = new FontFamily("隶书");
FontSize = 48;
 在WPF中,利用FontSize属性来指定字体的大小,单位是em。它也是一个与设备无关的单位。
如果你习惯使用像素点,那只要把像素点的尺寸乘以4除以3(也可以除以0.75)就可以了。
比较惯用的方式,是在FontFamily构造函数中指定字体的名称,然后在FontStyle和FontWeight属性中指定粗斜体。
FontFamily=new FontFamily(“宋体”);
FontSize=32;
FontStyle=FontStyles.Italic;
FontWeight=FontWeights.Bold;
你已经熟悉了用来将客户区着色的Background属性,而Foreground属性的着色对象则是文字本身。试试下面的代码:
Brush brush = new LinearGradientBrush(Colors.Black,Colors.White,new Point(0,0),new Point(1,1));
Background = brush;
 Foreground = brush;
前景和背景都使用相同的画刷,你可能会担心看不到文字,其实不会发生这样的事情。因为这里我们使用的是相对的坐标,对于前景来说,它会自动地根据内容(也就是文字)的尺寸进行调整,并不受窗口大小的影响。不过,如果你把窗口大小调整得和文字的大小一样,那么两个画刷就会重叠,文字就会看不到 。
试试这个:
SizeToContent = SizeToContent.WidthAndHeight;
Window类的SizeToContent属性可以规定窗口的大小根据内容进行调整,这里就是指定了窗口调整得和文字一样大小,于是你就会发现文字看不到了。这个属性很简单,通常对话框等窗口中都会用到。
SizeToContent会考虑到边框,以保证内容总是能完全显示在边框以内,而不会被边框所遮挡。
Content属性其实是调用了对象的ToString()方法来展示对象的,试着把别的对象赋值给Content你就会相信了:
Content = Math.PI;
或者:
 Content = DateTime.Now;
上面两个例子,窗口所显示的内容都和ToString的结果一样。
下面的程序设定Content属性为空字符串,然后将键盘输入的字符加入其中。这个程序类似第1章的TypeYourTitle,不过这个版本能让你输入换行和Tab。
//*********************************************************
//RecordKeystrokes.cs 2010 8th Auguest by mouyong
//*********************************************************
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
 
namespace part1.ch03
{
    class RecordKeystrokes:Window
    {
        [STAThread]
        public static void Main()
        {
            Application app = new Application();
            app.Run(new RecordKeystrokes());
        }
 
        public RecordKeystrokes()
        {
            Title = "记录键盘输入";
            Content = "";
        }
 
        protected override void OnTextInput(TextCompositionEventArgs e)
        {
            base.OnTextInput(e);
            string str = Content as string;
            if (e.Text == "/b")
            {
                if (str.Length > 0)
                {
                    str = str.Substring(0, str.Length - 1);
                }
            }
            else
            {
                str += e.Text;
            }
            Content = str;
        }
    }
}
这个程序之所以有这样的效果,是因为每当有按键被按下,Content属性就会被改变,且Window类会负责重绘客户区,这样,内容的改变会被及时刷新,但如果做一点小改动,程序就不能如愿运行了。比如,我们可以在类里声明一个string类型变量str并初始化它为空字符串(””),然后在构造函数中把str赋值给Content。
Content=str;
之后将OnTextInput里的以下两句代码注释了:
string str = Content as string;
 
 
Content = str;
这时你就会发现,效果没有了。这是因为字符串加法的结果是一个新的字符串对象,虽然你的代码里时行了字符串加法,但Content所指向的字符串依然是原来的字符串(不要忘记,字符串是固定不变的),所以,你所输入的字符并没有被刷新到客户区中。所以这段程序中能让Window重绘客户区的关键语句是
Content = str;
默然说话:因为重绘客户区的关键是要让 Window知道内容发生了改变,换句话说,就是内容改变的事件要被触发。所以,只有再次赋值给Content属性才会触发内容改变的事件,Window才会重绘客户区,如果Window不知道内容发生了改变,那么客户区是不会被重绘的,我们也就看不到窗口有任何的变化。
下面是被修改过的程序源码:
//*********************************************************
//RecordKeystrokes.cs 2010 8th Auguest by mouyong
//*********************************************************
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
 
namespace part1.ch03
{
    class RecordKeystrokes:Window
    {
        string str="";//新添加的字符串变量
        [STAThread]
        public static void Main()
        {
            Application app = new Application();
            app.Run(new RecordKeystrokes());
        }
 
        public RecordKeystrokes()
        {
            Title = "记录键盘输入";
            Content = str;//修改过的地方
        }
 
        protected override void OnTextInput(TextCompositionEventArgs e)
        {
            base.OnTextInput(e);
            //str = Content as string;//注释掉
            if (e.Text == "/b")
            {
                if (str.Length > 0)
                {
                    str = str.Substring(0, str.Length - 1);
                }
            }
            else
            {
                str += e.Text;
            }
            //Content = str;//注释掉
        }
    }
}
那有的同学可能就会想到,StringBuilder不就是一个可以改变的字符串么?如果把string换成StringBuilder会不会有效果呢?我们可以来试试,把类里原来声明的string类型变量str的类型更改为StringBuilder(你需要导入System.Text命名空间),如下:
StringBuilder build=new StringBuilder("");
之后,我们再把OnTextInput事件中的字符串移除和添加的代码做如下修改:
if (e.Text == "/b")
            {
                if (build.Length > 0)
                {
                    build.Remove(0, build.Length - 1);
                }
            }
            else
            {
                build.Append(e.Text);
            }
再次运行程序,我们惊奇的发现,还是不起作用。
虽然此程序使用了StringBuilder对象,但Window却无法知道StringBuilder对象内的字符串何时发生了改变,换句话说,StringBuilder对象的改变不会触发内容改变的事件,所以,我们仍然不能看到客户区内有任何的改变。
这是第二次修改后的代码:
//*********************************************************
//RecordKeystrokes.cs 2010 8th Auguest by mouyong
//*********************************************************
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Threading;
using System.Text;
 
namespace part1.ch03
{
    class RecordKeystrokes:Window
    {
        //string str = "";
        StringBuilder build=new StringBuilder("");//再次修改
        [STAThread]
        public static void Main()
        {
            Application app = new Application();
            app.Run(new RecordKeystrokes());
        }
 
        public RecordKeystrokes()
        {
            Title = "记录键盘输入";
            Content = build;//再次修改
        }
 
        protected override void OnTextInput(TextCompositionEventArgs e)
        {
            base.OnTextInput(e);
            //str = Content as string;
            if (e.Text == "/b")
            {
                if (build.Length > 0)
                {
                    //str = str.Substring(0, str.Length - 1);
                    build.Remove(0, build.Length - 1);//再次修改
                }
            }
            else
            {
                //str += e.Text;
                build.Append(e.Text);//再次修改
            }
            //Content = str;
        }
    }
}
通过这个例子,大家可以看到,WPF的对象似乎会很神奇的自动更新自己。其实这里面动作的原理一点也不神奇,一定是某事件通知了某个方法,画面才会被更新。所以在完成了字符串的添加和移除之后,我们必须要重新给Content属性赋值,这样才能触发内容改变的事件,Window也才会重绘窗体,我们也才能看到变化。
在第一版的RecordKeystrokes程序中,我们使用下面的代码就可以让Window重绘窗口:
Content = str;
但是,当我们使用类似代码对第三版的RecordKeystrokes程序进行修改时,却不起作用了:
Content = build;
原来窗口很聪明,它知道你指定了一个相同的对象(它不管有没有改变了这个对象的属性值),所以它不会进行更新,解决的办法如下:
Content=null;
Content=build;
程序终于恢复正常了。
我们看到窗口内容可以是纯文字,但Content属性存在的目的仅在于此么?当然不是!Contnet属性存在的意义,在本质上应该是更图形化的东西,由UIElement继承而来的实例。
在WPF中,UIElement是一个极为重要的类。它实现键盘、鼠标以及手写笔事件的处理。UIElement类也包含一个很重要的方法,名为OnReader。OnReader方法被用于绘制出对象的外观。
在Content属性的世界里,分为两种对象:一种继承自UIElement,另一种则不是。后一种的显示结果,就是ToString方法的返回值,而前一种,则是利用OnReader来显示。所有继承自UIElement的类我们称它们为element。
唯一直接继承自UIElement的类是FrameworkElement,在WPF中所有的element都是继承自FrameworkElement。理论上,UIElement提供关于用户界面和屏幕显示的必要结构,可以支持各种各样的编程框架。WPF正是这样的框架,它包含继承自FrameworkElement的所有类。
Image是继承自FrameworkElement的一个很常见的类型。下面的程序从网站上获得一幅位图,然后在窗口上显示出来:
//*********************************************************
//ShowMyFace.cs 2010 8th Auguest by mouyong
//*********************************************************
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Media.Imaging;
 
namespace part1.ch03
{
    class ShowMyFace:Window
    {
        [STAThread]
        public static void Main()
        {
            Application app = new Application();
            app.Run(new ShowMyFace());
        }
 
        public ShowMyFace()
        {
            Title = "看看我的脸";
            //首先构造URI
            Uri uri = new Uri("http://www.charlespetzold.com/PetzoldTattoo.jpg");
            //将图片加载入内存
            BitmapImage bitmap=new BitmapImage(uri);
            Image img = new Image();
            img.Source = bitmap;
            Content = img;//赋值给Content属性
        }
    }
}
你可以在System.Windows.Controls命名空间找到Image类,而BitmapImage位于System.Windows.Imaging命名空间。
WPF支持许多种格式的图像,包括GIF、TIFF、JPEG和PNG。
你也可以从本地硬盘获得图像。Uri的构造函数默认使用文件的绝对路径,可以在绝对路径前面加上”file://”,也可以不加( 默然说话:这段内容经实际测试并与英文版进行对照后,确定中文版翻译错误,不过这个外国人说的话,要是你没有技术背景,还真不容易看懂,对非专业翻译人员报以同情 ,但对本书译者。。。。不理解为啥一个学计算机20多年的硕士生还会翻译错误呢?)。如果你需要使用相对路径,你必须使用Uri类提供的两个参数的构造函数,第一个参数指定相对路径,第二个参数指定Uri为相对路径,如下:
Uri uri = new Uri("../../ch03/image/5.jpg", UriKind.RelativeOrAbsolute);
其中第二个参数由UriKind枚举提供,它包括三个值:Absolute、Relative、RelativeOrAbsolute,默认是Absolute,这里将它更改为了RelativeOrAbolute,以便自适应相对路径或绝对路径( 默然说话:经测试,相对路径的前面是不可以加 file://前缀的)。
关于BitmapImage有一点需要说明:你也可以不用将Uri对象传递给Bitmap的构造函数,而是将Uri对象赋值给BitmapImage的UriSource属性。不过你在设定此属性的前后,要分别调用BitmpaImage的BeginInit和EndInit方法( 默然说话:好麻烦呀,还是传给构造函数好了):
bitmap.BiginInit();
bitmap=uri;
bitmap.EndInit();
BitmapImage还有人BitmapSource类继承到的只读属性PixelWidth和PixelHeight属性,它们是以像素为单位的图像宽高信息,你还可以从只读的DpiX和DpiY属性得到以DPI为单位的分辨率信息,它们的类型是double。另外,如果你想得到的是与设备无关为单位的宽高,则可以使用只读的Width和Height属性。我们还可以通过Format属性知道图像的格式类型,对于那些使用“调色板”的位图( 默然说话:例如 GIF就是使用调色板的位图),Palette属性可以让我们取得调色板。
无论ShowMyFace显示何种图片,你都会发现,图片会维持其显示的宽高比例,以尽量占满窗口的方式进行显示,并且,图片的全部内容都会被显示出来,而不是只显示图片的一部分( 默然说话:即窗口会对图片进行自动缩放)。这是因为Image的Stretch属性控制的结果。默认情况下,它的值是Stretch.Uniform。意思就是维持宽高比例不变进行缩放。别的可选值有Stretch.Fill,不维持宽高比例,全部填充整个窗口,但这样图片也许会出现变形。还有就是Stretch.UniFormFill,它的意思就是在全部填满整个窗口的前提下维持宽高比例进行缩放,如果窗口的宽高比与图片的宽高比不一致,那么就会有超出的部分,它们将被截去,不会被显示出来( 默然说话:我说的这句话真不容易理解,但自认为还是比中文版的翻译清楚得多。大家都试一下,看看程序运行的效果就能明白我说的是什么意思了。
只要Stretch属性不设置为Stretch.None( 默然说话:这个属性就是不进行任何的缩放,按图片的原始大小进行显示),我们就可以设定Image的StretchDirection属性。此属性的默认值是StretchDirection.Both,这表示图像在缩放时可以大小或者小于其原始尺寸。
另外的两个值就是StretchDiretion.DownOnly,它表示图象不可以比原始尺寸大,而StretchDiretion.UpOnly则表示不可以比原始尺寸小。
在默认情况下,图片总是居中放置的,你可以通过设定Image的HorizontalAlignment和VerticalAlignment属性来改变这一点。例如,下面的代码将把图片移动到右上角。( 默然说话:如果你想看到比较明显的效果,你应该找一张比较小的图片,并把 Stretch的值设置为Stretch.None
img.HorizontalAlignment = HorizontalAlignment.Right;
img.VerticalAlignment = VerticalAlignment.Top;
这两个属性在WPF的布局中是非常重要的,你会一再遇到它们。
如果你希望你的图片不要紧贴着客户区的边界,你可以在Image对象的周围设定边界:
img.margin=new Thickness(10);
Margin是FrameworkElement所定义的属性,常常用来在element之间插入间隔。你可以使用Thickness结构体来定义边界,如果你只传入一个参数,则表示上下左右的边界都一样,如果你传入了四个不同的参数,则表示上下左右边界各不相同。
Image也有Width和Height属性,不过它们是可读可写的,类型为double。如果你查看它们的值,你会发现返回的是NaN(和Window的两个同名属性一样),你也可以设定精确的Width和Height,不过它们会受到Stretch属性的影响。
你还可以将窗口的尺寸调整到与图片的大小一致:
SizeToContent = SizeToContent.WidthAndHeight;
设定Window对象的Foreground属性,对于图像的显示不会有影响。Foreground只会对于文字内容有影响,或者对于会显示文字的element有影响。
通常,为Window设定Background不会对图像造成影响,只能影响到图像没有覆盖到的部分。
Background=new LinearGradientBrush(Colors.Red,Colors.White,new Point(0,0),new Point(1,0));
但是,如果加上下面这一句代码,情况就不同的。
img.Opacity = 0.3;
由于Opacity属性(Image从UIElement继承而来)能将图片设置为一种透明效果,默认为1,表示不透明。你可以设定为0到1之间的任何值。当它不是1的时候,你就会看到画刷对图像造成了影响( 默然说话:这是可以理解的,透明了,就可以看到背景。另外, Window对象也有这个属性,但是当设置它小于1之后,它不是变透明了,而是变黑了。离0越近,就越黑)。
如果你要转动一个图片是相当容易的,只要象下面这样:(关于图形变换,我们29章再讨论)
img.LayoutTransform = new RotateTransform(45);
Image不具有自己的Background和Foreground属性,因为这两个是Control的属性,而Image没有继承自Control。Control是可视对象,它们都会响应用户的操作。当然Image也可以获得用户的操作,因为所有键盘、鼠标的输入事件都来自UIElement。
接下来介绍图形化元素的另一组类:形状。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

默然说话

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

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

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

打赏作者

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

抵扣说明:

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

余额充值