当前位置: 代码迷 >> 综合 >> 应用程序与窗口 - 《Windows Presentation Foundation 程序设计指南》 - 免费试读 - book.csdn.net
  详细解决方案

应用程序与窗口 - 《Windows Presentation Foundation 程序设计指南》 - 免费试读 - book.csdn.net

热度:46   发布时间:2024-01-18 17:34:37.0

导读:


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


 


第1章  应用程序与窗口


为Windows Presentation Foundation(WPF)开发应用程序,一般来说,一开始需要花一点点时间创建Application对象与Window对象。下面是一个很简单的WPF程序:


SayHello.cs


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


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


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


using System;


using System.Windows;


namespace Petzold.SayHello


{


    class SayHello


    {


        [STAThread]


        public static void Main()


        {


            Window win = new Window();


            win.Title = "Say Hello";


            win.Show();


            Application app = new Application();


            app.Run();


        }


    }


}


我假设你已经熟悉System命名空间(namespace)了,如果你还不熟悉此命名空间,那你或许应该到我的网站www.charlespetzold.com读我写的另一本书.NET Book Zero。上面这个SayHello程序利用using编译指示符(directive),将System.Windows命名空间加入工程。这个命名空间包含了所有的基本WPF类别、结构(struct)、接口(interface)、委托(delegate)、以及枚举类型(enum),其中包括Application和Window两个类。其他的WPF命名空间均以System.Window开头,例如System.Windows.Controls、System.Windows.Input、System.Windows.Media。只有System.Windows.Forms是个例外,它主要是Windows.Forms的命名空间。除了System.Windows.Forms.Integration这个命名空间里的类是用来集成Windows.Forms 和 WPF 程序的,其他所有以System.Windows.Forms开头的命名空间,都属于传统的Windows.Forms命名空间。


本书的范例具有一致的命名方式。每个程序都属于一个特定的Microsoft Visual Studio工程。工程中的所有代码都被包含在一个统一的命名空间中。我所使用的命名空间,开头是我的姓氏,然后是工程名称。以第一个范例来说,工程名是SayHello,所以命名空间就是Petzold.SayHello。工程中的每个类都有一个独立的文件,而且文件名一般都会与类名一致。如果工程内只包含一个类(第一个范例正是如此),那么这个类的名字通常会和工程名一样。


任何一个WPF程序,Main的前面都必须有个[STAThread]属性(attribute),否则编译会失败。这个attribute是用来申明该应用程序的初始线程模型为“single threaded apartment”,以便和Component Object Model(COM)兼容。 “single threaded apartment”是旧的COM年代的词汇,是.NET之前的词汇,但基于我们此刻的目的,你可以把它理解成:STAThread表示我们的应用程序不会使用“源自运行环境”的多线程。


在SayHello程序中,Main一开始创建一个Window类的对象,这个类用来创建标准应用程序窗口。Title property 是显示在窗口标题栏里的文字,而Show方法会将窗口显示在屏幕上。


这里最重要的步骤是,调用Application对象的Run方法。在传统Windows编程的思维中,这么做的目的是要建立一个消息循环,让应用程序可以接收用户键盘或鼠标输入。如果此程序是在Tablet PC上执行,此应用也会接收到来自手写笔(stylus)的输入。


你很可能是使用Visual Studio 2005来新建、编译并执行WPF应用程序。果真如此,你可以采取下面的步骤来重新建立SayHello工程:


1.    从“File”菜单,选取“New Project”。


2.    在“New Project”对话框中,选取“Visual C#”、“Windows Presentation Foundation”和“Empty Project”。指定一个存放该工程的目录,将工程命名为“SayHello”并取消“Create Directory For Solution”,然后按下“OK”。


3.    在右边的“Solution Explorer”中,“References”栏必须包含“PresentationCore”、“PresentationFramework”、“System”以及“WindowsBase”。如果在“References”栏里没有出现这些DLL,请手动加上它们,做法很简单,只要用鼠标右键选择“Reference”,然后选取“Add Reference” 就可以了 (另一个做法是, 从“Project”菜单中,选取“Add Reference”)。在这个“Add Reference”对话框中,选择“.NET”项,然后选取必要的DLLS,然后按下“OK”。


4.    在右边的“Solution Explorer”中,用鼠标右键选择SayHello工程名,然后从“Add”菜单中选“New Item”(另一个做法是,从“Project”菜单中,选取“Add New Item”)。在“Add New Item”对话框中,选择“Code File”,键入代码文件名“SayHello.cs”, 最后按下“OK”。


5.    将本章前面的代码输入到SayHello.cs文件中。


6.    从“Debug”菜单中,选取“Start Without Debugging”(另一种做法是,直接按下Ctrl+F5),就可以编译并且执行该程序。


本书Part I 所示的大部分程序,都采用和上面相同的步骤来建立工程,只有某些涉及多个源代码文件的工程做法不太一样(本章就有一个这样的例子)。


在关闭SayHello创建的窗口时,你会发现一个console 窗口也在运行。这是源自编译选项的设定,你可以在工程的property中,修改此编译选项。用鼠标右击工程名,并从弹出菜单中选择“Properties”(另一个做法是, 从“Project”菜单中选择“Properties”)。现在你可以查看工程的各种设定,也可以改变设定。特别注意,“Output Type”被设定为“Console Application”, 显然,这样的设定并不会阻碍用console 程序来建立GUI窗口。 将“Output Type”设为“Windows Application”,程序同样会顺利执行,而这次就不会再出现console窗口了。我认为在开发阶段,console窗口其实是相当有用的。程序运行时我利用它来显示一些文本信息,以便调试。如果程序的bug太多,甚至无法将GUI窗口显示出来,或者进入无限循环,这个时候,只要在console窗口中键入Ctrl+C, 就可以轻易地关闭程序。这些都是console窗口的附带好处。


SayHello 使用到了Window和Application类,这两个类都是继承自DispatcherObject,但Window在继承树中离DispatcherObject较远,请看下面的类继承树:


Object   


    DispatcherObject (abstract)


         Application


         DependencyObject


               Visual (abstract)


                     UlElemcnt


                          FrameworkElement


                               Control


                                       ContentControl


                                             Window


当然,你目前可能尚未熟悉此类继承树,但随着你一路走向WPF技术,你会一次又一次地遇到这些类。


在一个程序中,只能创建一个Application对象,对程序的其他地方来说,此Application对象的作用就如同固定的锚一般。你在屏幕上是看不见Application对象的,但可以见到Window对象。Window对象出现在屏幕上,这就是正常的Windows系统窗口,具有的标题属性(Title property)的值会变成标题栏上的文字。系统菜单在标题栏的左边,最大化、最小化和关闭窗口图标则在它右边。此窗口有一个可以调整窗口大小的边框,窗口中很大的面积被一个客户区(client area)所占据。


在某些限制下,你可以调整SayHello程序中Main方法里的语句顺序,程序依然可以执行。比方说,你可以在调用完Show之后,才改变Title property。理论上,这样的改变使得窗口一开始显示时标题栏上没有名字,后来才加上名字,但这个时间差距太短,你可能根本看不出来。


你可以在创建Window对象之前,先创建Application对象,但对于Run的调用必须保留在最后。Run方法一旦被调用,就不会返回,直到窗口被关闭为止。Run返回后,Main方法结束,Windows操作系统会做一些清除工作。如果你将源代码中调用Run的那行删除,Window对象依然会被创建并显示在屏幕上,但是会在Main结束之后,立刻被销毁。


想要将窗口显示出来,你也可以不调用Window对象的Show方法,而是直接将Window对象当作参数传递给Run方法:


app.Run(win) ;


在这种做法中,Run方法会去调用Window对象的Show方法。


程序调用Run方法之后才真正开始运行。只有在调用Run之后,Window对象才能响应用户的输入。当用户关闭窗口时,Run方法就会返回,程序也就准备结束。因此,程序运行时几乎所有的时间都是花在Run内。那么程序把所有时间都花在Run内,究竟为了做些什么呢?


在初始化之后,几乎程序所做的一切事情,都是在响应各种事件。这些事件通常是关于键盘、鼠标、或手写笔的输入。UIElement类(顾名思义,是“用户界面元素”的意思)定义了一些和键盘、鼠标、手写笔相关的事件;Window类继承了所有的事件。其中一个事件名为MouseDown。只要用户用鼠标点击(click)窗口的客户区,MouseDown事件就会发生。


下面是一个范例,稍微将Main里的语句次序做了改变,同时也安装了一个事件处理器(event handler),专门处理MouseDown事件:


HandleAnEvent.cs


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


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


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


using System;


using System.Windows;


using System.Windows.Input;


namespace Petzold.HandleAnEvent


{


    class HandleAnEvent


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            Window win = new Window();


            win.Title = "Handle An Event";


            win.MouseDown += WindowOnMouseDown;


            app.Run(win);


        }


        static void WindowOnMouseDown(object sender, MouseButtonEventArgs args)


        {


            Window win = sender as Window;


            string strMessage =


                string.Format("Window clicked with {0} button at point ({1})",


                               args.ChangedButton, args.GetPosition(win));


            MessageBox.Show(strMessage, win.Title);


        }


    }


}


我习惯的事件处理器命名方式是:用负责响应事件的类或对象名作为开头(比如 Window),后面接着“On”, 最后接事件名(所以这里我将事件处理器命名为WindowOnMouseDown)。但是,这只是我个人的习惯,你可以有自己的命名方式。


MouseDown事件所需要的事件处理器,必须符合MouseButtonEventHandler委托,也就是说,第一个参数的类型是object,第二参数的类型是MouseButtonEventArgs。这个类定义在System.Windows.Input命名空间中,所以代码中使用using编译指令来调用此命名空间。因为“使用此事件处理器的Main方法”是static的,所以我们必须把此事件处理器也申明为static。


本书大多数后继的程序,都会利用using指令将Sysgtem.Windows.Input命名空间包含进来,即使没有用到还是这么做。


当用户在窗口的客户区中按下鼠标,MouseDown事件就会发生。其事件处理器的第一个参数是“此事件的来源”,也就是Windows对象。事件处理器可以轻易地将此对象转换成为Window类的对象,然后才加以利用。


在HandleAnEvent类中的事件处理器,之所以需要Window对象,有两个目的:首先,将Window对象作为GetPosition方法的参数(GetPosition方法在MouseButtonEventArgs类中),此方法返回一个Point类型(这是定义在System.Windows命名空间内的结构体类型)


的对象,表示鼠标坐标(相对于GetPosition参数的左上角)。其次,事件处理器读取Window对象的Title property,使用此property作为消息框(Message Box)的名称。


MessageBox类也是定义在System.Windows命名空间中的,它具有12个静态的Show重载方法,让你可以选择显示按钮或是图片。默认情况下,只显示OK按钮。


在HandleAnEvent程序中,Message Box显示出鼠标光标的位置,这个位置是相对于客户区的。你可能会很自然地认定这些坐标是以像素(Pixel)为单位,但事实并非如此,它们是和设备无关的,以1/96英寸为一个单位。 本章稍后,我会再度解释这个奇特的坐标系统(coordinate system)。


HandleAnEvent的事件处理器将sender参数转型为Window对象。对于此事件处理器来说,想得到这个Windows对象,做法不只一种。在Main中所建立的Window对象可以被储存在一个静态字段(static field)中,以便此事件处理器稍后使用。另一种做法,是利用Application类的某些property。Application具有一个static property,名为Current,此property会存放程序所创建的Application对象(我之前说过,一个程序只能创建一个Appliation对象)。Application也包含一个名为MainWindow的instance property,利用此property,就可以得到Window对象。所以此事件处理器可以设定一个Window类型的局部变量,做法如下:


Window win = Application.Current.MainWindow;


如果获取Window对象仅仅是为了取得Title标题文字以提供消息框使用,那么MessageBox.Show方法可以直接调用Application.Current.MainWindow.Title,而不用设置局部变量。


Application类定义了很多有用的事件。在.NET中的习惯是,大多数的事件都有对应的protected方法,可以用来发出事件。Application所定义的Startup事件是利用protected OnStartup方法来产生的。一旦调用Application对象的Run方法,OnStartup方法就会被立刻调用。当Run即将返回时,会调用OnExit方法(并发出对应的Exit事件)。你可以利用接收到这两个事件的时机来进行整个应用程序的初始化和清理工作。


OnSessionEnding方法和SessionEndin事件表示用户已经选择要注销Windows操作系统,或者要关闭电脑。此事件附带一个SessionEndingCancelEventArgs类型的参数,此类型继承了CancelEventArgs的Cancel property,你可以将此property设为“true”,就可以防止Windows操作系统被关闭。你必须将程序编译成“Windows Application”而非“Console Application”,才有可能会收到此事件。


如果你的程序需要Application类的某些事件,你可以为这些事件安装事件处理器,另一个更方便的做法,就是定义一个类继承Application,例如下一个范例InheritTheApp正是如此。


这个类继承Application,并将“负责发出事件的方法” 予以覆盖 (override)。


InheritTheApp.cs


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


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


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


using System;


using System.Windows;


using System.Windows.Input;


namespace Petzold.InheritTheApp


{


    class InheritTheApp : Application


    {


        [STAThread]


        public static void Main()


        {


            InheritTheApp app = new InheritTheApp();


            app.Run();


        }


        protected override void OnStartup(StartupEventArgs args)


        {


            base.OnStartup(args);


            Window win = new Window();


            win.Title = "Inherit the App";


            win.Show();


        }


        protected override void OnSessionEnding(SessionEndingCancelEventArgs args)


        {


            base.OnSessionEnding(args);


            MessageBoxResult result =


                MessageBox.Show("Do you want to save your data?",


                                 MainWindow.Title, MessageBoxButton.YesNoCancel,


                                 MessageBoxImage.Question, MessageBoxResult.Yes);


            args.Cancel = (result == MessageBoxResult.Cancel);


        }


    }


}


InheritTheApp 类继承自Application,且覆盖(override)两个Application类定义的方法:OnStartup与OnSessionEnding。在此程序中,Main方法并没有创建Application类的对象,而是创建了InheritTheApp类对象,而且Main方法本身也是定义在InheritTheApp类中。让Main建立一个“自己所属的类”对象,这看起来可能有点奇怪,不过却是完全合法的,因为Main是一个静态方法,所以即使在InheritTheApp对象尚未创建之前,Main就已经存在了。


InheritTheApp同时覆盖(override)两个方法: OnStartup(在Run被调用后,这个方法就立即被调用)与OnsessionEnding。程序可以利用OnStartup的机会创建一个Window对


象,并把它显示出来。此InheritTheApp类还可以在构造函数内进行此工作。


在override版的OnSessionEnding中,弹出一个“Yes、No、Cancel”对话框。请注意此对话框的标题被设定为MainWindow.Title。因为OnSessionEnding是继承自Application的实例方法,所以只要直接使用MainWindow,就可以得到此Application实例的property。你可以在MainWindow的前面加上this关键字,来更清楚地表示MainWindow是Application对象的property。


当然,此程序在结束前没有文档可以被存储,所以它忽略了Yes和No的响应,直接让此应用程序关闭,让Windows操作系统终结当前用户的进程。如果用户的回应是Cancel,会造成SessionEndingCancelEventArgs对象的Cancel标志被设为true,从而阻止Windows操作系统被关闭,或者注销(log off)。通过SessionEndingCancelEventArgs的Reason- SessionEnding property,你可以分辨到底是关闭还是注销。ReasonSessionEnding的值可以是ReasonSessionEnding.Logoff或ReasonSessionEnding.Shutdown。


不管是OnStartup还是OnSessionEnding, 一开始都是先调用基类(base class)的方法。此调用并非绝对必要,但也不会有坏处。一般来说,我们会去调用基类的方法,除非你有特别的理由不这么做。


你可以从命令提示窗口(Command Prompt Window)执行程序,这样就可以指定命令行参数,Windows程序也不例外。想取得命令行参数(command-line argument),定义Main的方法稍微有点不同:


public static void Main(string[] args)


命令行参数会以字符串数组的形式传入Main中。在OnStartup方法中,也可以利用StartupEventArgs参数的Args property,取得此字符串数组。


Application具有MainWindow property,这表示一个程序可以有多个窗口。一般来说,许多窗口都只是短暂出现的对话框,但对话框其实就是Window对象,只是有些小差异(对话框的显示方式,以及和用户的交互方式)。


下面的程序将几个窗口一起放到桌面上显示:


ThrowWindowParty.cs


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


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


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


using System;


using System.Windows;


using System.Windows.Input;


namespace Petzold.ThrowWindowParty


{


    class ThrowWindowParty: Application


    {


        [STAThread]


        public static void Main()


        {


            ThrowWindowParty app = new ThrowWindowParty();


            app.Run();


        }


        protected override void OnStartup(StartupEventArgs args)


        {


            Window winMain = new Window();


            winMain.Title = "Main Window";


            winMain.MouseDown += WindowOnMouseDown;


            winMain.Show();


            for (int i = 0; i < 2; i++)


            {


                Window win = new Window();


                win.Title = "Extra Window No. " + (i + 1);


                win.Show();


            }


        }


        void WindowOnMouseDown(object sender, MouseButtonEventArgs args)


        {


            Window win = new Window();


            win.Title = "Modal Dialog Box";


            win.ShowDialog();


        }


    }


}


与InheritTheApp类一样,ThrowWindowParty类继承自Application,且在override版本的OnStartup方法中,创建一个Window对象。然后再创建两个Window对象,也将它们显示出来。(我很快就会讨论MouseDown事件处理器内做了什么事。)


你会注意到的第一件事,就是OnStartup所创建的三个窗口,在此应用程序中具有同等的地位。你可以点击任何窗口,然后该窗口就会出现在最上面。你可以用任何次序关闭这些窗口,且只有在最后一个窗口被关闭后,程序才会结束。如果窗口标题上没有注明“Main Window”,你根本无从识别主窗口。


然而,如果你在程序里查看Application对象的MainWindow property, 会发现第一个调用Show的窗口会被当作此程序的主窗口(至少一开始是这样)。


Application类也包含了一个名为Windows(注意字尾有s)的property,其类型是WindowCollection。WindowCollection是常用的.NET collection类型,它实现了ICollection


和IEnumerable接口,顾名思义,WindowCollection用来存储多个Window对象。此类型包含一个名为Count的property,且具有一个indexer。只要窗口调用过Show,且还存在,你可以用此indexer轻易地取得特定窗口。OnStartup覆盖后,Windows.Count property会变成3,且Windows[0]会得到标题为“Main Window”的窗口。


此程序有一个古怪的地方,三个窗口都出现在Windows taskbar上(大部分的用户都将Windows taskbar 放在屏幕下方)。一个程序在Windows taskbar中占用不止一项,大家通常认为这不太理想。想要让这些多余的窗口不要占用taskbar的空间,你必须在for循环里加入下面的语句:


win.ShowInTaskbar = false;


但另一件古怪的事接踵而至:如果你先关闭标题为“Main Window”的窗口,会看到taskbar中的对应项消失了,但是还有两个窗口在桌面,程序依然在执行!


一般来说,在Run方法返回之后,程序就会结束,而且在用户关闭最后一个窗口后,Run方法就会返回。这样的行为受到Application的ShutdownMode property控制,其值为ShutdownMode枚举值,默认为ShutdownMode.OnLastWindowClose,你还可以将其设定为ShutdownMode.OnMainWindowClose。在调用Run方法之前,可先执行下面的语句:


app.ShutdownMode = ShutdownMode.OnMainWindowClose;


或者,你可以试着将下面的语句插入OnStartup内的任何位置。(在Main中你必须在此property的前面冠以此Application对象的名称;在OnStartup方法中,你可以直接使用此property,或者在前面加上“this”关键字也可以。)


ShutdownMode = ShutdownMode.OnMainWindowClose;


现在,当主窗口关闭,Run方法返回之后,程序就会结束。


不要删除对Shutdown property所作的改变,试着将下面的语句加入for循环中:


MainWindow = win;


你应该还记得,MainWindow是Application类的property。你的程序可以利用这个property,将你所选择的窗口指定为主窗口(main window)。在for循环最后,标题为“Extra Window No.2”的窗口成为主窗口,也就是关闭此窗口就会结束程序。


ShutdownMode还有第三个选项:你可以将此property设定为ShutdownMode. OnExplicitShutdown,这么一来,只有当程序呼叫Application的Shutdown方法时,Run方法才会返回。


现在,把你所加入和Application类的ShutdownMode与MainWindow property相关的语句都删除。还有另一个方法可以在多个窗口之间建立层次(hierarchy)关系,这是通过Window类的Owner property来实现的。默认情况下,此property的值是null,表示该窗口没有主人(owner)。你可以设定Owner property为此程序中其他的Window对象。(但是要注意一点:层层往上追查owner,不可以最后追查回自己。也就是说,“拥有”的关系不可以形成回路。)比方说,试着在for循环中,插入以下代码:


win.Owner = winMain;


现在,主窗口拥有这两个子窗口,这三个窗口之间还可以在屏幕上相互切换,但是不管怎么切换,你会发现“被拥有的”窗口一定会出现在“拥有者”窗口的前面。当你将“拥有者”窗口最小化时,“被拥有的”窗口也会从屏幕上消失不见,而当你将“拥有者”窗口关闭时,“被拥有的”窗口也会被自动关闭。这两个子窗口实际上变成了modeless类型的对话框。


对话框可以大致分为两大类:modeless对话框是其中比较不常见的那一种,另一种modal对话框则比较常见。只要你用鼠标在ThrowWindowParty客户区域点一下,就可以看见modal对话框的实际范例。WindowOnMouseDown方法会建立另一个Window对象,并设定好其Title property,但是并不是调用Show,而是调用ShowDialog来将它显示出来 。ShowDialog和Show不一样,ShowDialog不会立刻返回,且利用此方法显示的modal对话框,不会让你切换到同一个程序的其他窗口(但是可以切换到别的程序窗口)。只有在你关闭此对话框之后,ShowDialog的调用才会返回。


另一方面,modeless对话框允许你适当地同时使用主窗口和对话框。Visual Studio的Quick Find对话框就是modeless对话框,用来找寻源代码内的字符串,即使Quick Find对话框尚未关闭,依然可以继续在主窗口中编辑源代码。Modal对话框则会将用户输入的一切事件都捕捉起来,只有在此对话框关闭之后,才能处理程序中的其他窗口。Modeless对话框则没有这样的限制。


试着这么做:回到第一个范例程序SayHello,将源代码中的Show改成ShowDialog,并且将所有引用到此Application 对象的地方,都先注释掉。这个程序依然可以执行,因为ShowDialog实现了自己的消息循环,以处理输入事件。正因为modal对话框没有参与应用程序的消息循环(而是有自己的消息循环),且modal对话框不让应用程序得到输入消息,所以modal对话框具有模式(modal)的作用。


前面的两个程序都定义有一个Application的子类。一般来说,程序经常定义自己的Window子类。下面的程序包含三个类和三个源码文件。要想在Visual Studio 2005的现有工程中,多加入一个空的源代码文件,做法是在“Solution Explorer”的工程名上按鼠标右键,然后从菜单中选取“Add New Item”。或者从“Project”菜单中选择“Add New Item”。不管是哪一种做法,你要加入的项目都是“Code File”,这一开始就是空白的。


下面的工程名称是InheritAppAndWindow, 这也是一个类的名称,此类只包含Main:


InheritAppAndWindow.cs


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


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


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


using System;


using System.Windows;


using System.Windows.Input;


namespace Petzold.InheritAppAndWindow


{


    class InheritAppAndWindow


    {


        [STAThread]


        public static void Main()


        {


            MyApplication app = new MyApplication();


            app.Run();


        }


    }


}


Main创建一个类型为MyApplication的对象,且调用此对象的Run方法。MyApplication类继承自Application,它的定义如下:


MyApplication.cs


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


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


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


using System;


using System.Windows;


using System.Windows.Input;


namespace Petzold.InheritAppAndWindow


{


    class MyApplication : Application


    {


        protected override void OnStartup(StartupEventArgs args)


        {


            base.OnStartup(args);


            MyWindow win = new MyWindow();


            win.Show();


        }


    }


}


在OnStartup方法的override版本中,此类创建一个类型为MyWindow的对象,这是该工程中的第三个类,它继承自Window:


MyWindow.cs


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


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


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


using System;


using System.Windows;


using System.Windows.Input;


namespace Petzold.InheritAppAndWindow


{


    public class MyWindow : Window


    {


        public MyWindow()


        {


            Title = "Inherit App & Window";


        }


        protected override void OnMouseDown(MouseButtonEventArgs args)


        {


            base.OnMouseDown(args);


            string strMessage =


                string.Format("Window clicked with {0} button at point ({1})",


                                   args.ChangedButton, args.GetPosition(this));


            MessageBox.Show(strMessage, Title);


        }


    }


}


继承自Window的类,通常使用Window类的构造函数来初始化窗口本身,唯一需要自己做的初始化工作,就是设定Title property。请注意,此property前面不需要加上任何对象名称,因为MyWindow已经从Window类继承了这个property。前面可以加上关键字this,也可以不加:


this.Title = "Inherit App & Window";


不给MouseDown事件安装事件处理器,而是改将OnMouseDown方法覆盖(override)。因为OnMouseDown是一个实例方法,所以this代表的是Window对象,可以将this关键字当作参数传进GetPosition方法中,且可以直接存取Title property。


虽然刚刚所显示的程序没有什么不对的地方,但其实在只有代码(而没有XAML markup)的WPF程序中,更常见(且更容易)的做法就是定义一个Window的子类,而非定义Application的子类。下面是一个很典型的单一代码文件程序:


InheritTheWin.cs


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


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


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


using System;


using System.Windows;


using System.Windows.Input;


namespace Petzold.InheritTheWin


{


    class InheritTheWin : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new InheritTheWin());


        }


        public InheritTheWin()


        {


            Title = "Inherit the Win";


        }


    }


}


本书的Part I中,我的许多范例程序都是使用上面的结构。这样的代码比较短,而且如果你真的想要将Main方法尽可能精简,还可以将所有的调用都放在同一条语句中:


new Application().Run(new InheritTheWin());


让我们开始把玩这个程序,我会建议如何改造这个程序,你可以跟着做,或者你愿意照自己的想法做,那样更好。


此窗口摆放的位置和尺寸,是由Windows操作系统所决定的,但是你也可以改变它。Window类型从FrameworkElement继承了Width和Height property,你可以在构造函数中设定这些property:


Width = 288;


Height = 192;


设定这两个property时,不限定只能用整型,也可以用双精度浮点型,所以下面的设定方式也是可行的:


Width = 100 * Math.PI;


Height = 100 * Math.E;


Width和Height property一开始都是没有定义的,如果你在程序中没有设定这两个property,那么它们就会一直没有定义,也就是说,它们的值是NaN。NaN是IEEE浮点数的缩写,它的意思是“非数字”(Not a Number)。


因此,如果你需要取得窗口的实际尺寸,不要使用Width和Height property,而是要改用ActualWidth和ActualHeight这两个只读property。然而,在创建窗口的过程中,ActualWidth和ActualHeight可能为0;只有在窗口已经出现在屏幕上时,这两个property才会生效。


你可能会以为,我之前选择设定宽和高的两个数字,应该是随便输入的:


Width = 288;


Height = 192;


其实,这两个数字不是像素(pixel)。如果Width和Height property是以像素为单位,就不可能被指定为double型浮点数值。在WPF中,关于长宽和位置,你所制定的单位有时候被称为“设备无关像素”( device-independent pixel) 或称“逻辑像素”(logical pixel),但是或许最好连提都不要提到“像素”这两个字,免得引起误解,所以我称其为“与设备无关的单位”(device-independent unit)。每个单位是1/96英寸,所以288和192实际上是用来指示此窗口的宽为3英寸,高为2英寸。


如果你真的拿尺子测量你的屏幕,或许会发现实际显示的尺寸是有误差的。像素和英寸之间的关系,是由Windows操作系统所建立,用户也可以主动改变。在Windows桌面点鼠标右键,从菜单中选取“Properties”,选择“Settings”页,然后按下“Advanced”(高级)按钮,再按下“General”(一般)页,就可以看到设定值。


Windows操作系统默认的显示分辨率(display resolution)是每英寸96像素,如果你的电脑也是这样设定的,Width和Height的值分别为288和192,就会精确地对应到288和192个像素。


然而,如果你让显示分辨率率变为120DPI,那么WPF程序如果设定Width和Height property为288,192,就等同于360像素和240像素,也是3英寸和2英寸。


科技持续进步,以后的屏幕分辨率会变得越来越高,即使分辨率改变了,WPF程序还是可以不用改变,执行起来没有误差。比方说,假设你有一个屏幕,每英寸大概有200像素。为了避免屏幕上一切都变得很小,用户需要把“Display Properties”设定成对应的分辨率(或许是192DPI)。当WPF程序设定Width和Height为288和192个单位时,其实就是等同于596像素和384像素,依然是3英寸和2英寸。


在WPF中,普遍地使用这些与设备无关的单位。比方说,本章稍早的某个程序使用对话框来显示鼠标相对于客户区左上角的位置,也不是以像素为单位,而是以“设备无关单位”为单位,这里是以1/96英寸为单位。


如果你做个实验,把Width和Height设定成非常小,你将会发现窗口一定至少显示出标题栏的部分。你可以利用SystemParameters.MinimumWindowWidth和SystemParameters. Minimum WindowHeight这两个只读property,来得知窗口的最小长宽是多少(单位与设备无关)。SystemParameters类有很多这样的静态property。


如果想要将窗口放在屏幕上的特定位置,你可以利用Window类的Left和Top property:


Left = 500;


Top = 250;


这两个property用来指定窗口左上角的位置(相对于屏幕左上角),也是使用设备无关单位(类型为double),如果没有设定这两个property,它们的值也会是NaN。Window类并不具有Right和Bottom property。想知道窗口的右下角位置,可以利用Left与Top以及窗口的长宽推算出来。


假设你的显卡和屏幕可以支持1600 * 1200 像素,而且你在“Display Properties”对话框中的分辨率也是如此设定。如果你查看静态的SystemParameters.PrimaryScreenWidth和SystemParameters.PrimaryScreenHeight,这两个值也会是1600和1200吗?当且仅当你的屏幕DPI为96时。这表示你的屏幕宽为16-2/3英寸,高为12-1/2英寸。


然而,如果你设定的DPI为120,SystemParameters.PrimaryScreenWidth和System- Parameters.PrimaryScreenHeight会是1280和960(与设备无关)单位,这意味着你的屏幕宽高分别为13-1/3英寸和10英寸。


因为SystemParmaeters一律使用设备无关的单位,只有SmallIconWidth和SmallIconHeight这两个property是例外,它们使用像素做单位。因为SystemParameters的property普遍不受分辨率的影响,所以你可以安全地使用大部分的值,不需要转换。比方说,下面的代码把窗口放在屏幕的右下角:


Left = SystemParameters.PrimaryScreenWidth - Width;


Top = SystemParameters.PrimaryScreenHeight - Height;


这段代码运行之前,必须设定好Width和Height property。否则这段代码运行的结果,你可能不大喜欢。如果你的屏幕底部有taskbar,你的窗口会被遮盖住一部分。你可能想要将


窗口放在工作区(work area)的右下角(而非屏幕的右下角)。工作区就是桌面toolbar以外的区域。(最常见的桌面toolbar就是Windows操作系统的taskbar)。


SystemParameters.WorkArea property返回Rect类型的对象,此Rect结构定义一个矩形(内容包括了左上角位置与长度高度)。此WorkArea property必须是Rect类型才完整,不可只有宽和高,因为用户可能会将taskbar放在屏幕左边。如果真的在左边,Rect结构的Left property就是非零值,而Width property就等于屏幕宽度减去Left值。


下面的代码,可以将窗口放在工作区的右下角:


Left = SystemParameters.WorkArea.Width - Width;


Top = Systemparameters.WorkArea.Height - Height;


下面的代码,可以将窗口放在工作区的中间:


Left = (SystemParameters.WorkArea.Width - Width) / 2 +


                  SystemParameters.WorkArea.Left;


Top = (SystemParameters.WorkArea.Height - Height) / 2 +


                  SystemParameters.WorkArea.Top;


如果不这么做,另一个做法就是使用Window类的WindowStartupLocation property。这个property的值是属于WindowStartupLocation枚举类型的,默认为WindowStartup- Location.Manual,这表示如果程序没有设定窗口位置,就由Windows操作系统负责摆放窗口。你只要将此property设定为WindowStartupLocation.CenterScreen,就可以将窗口放在中间。尽管名称为CenterScreen,但其实是放在工作区的中间,而非整个屏幕的中间。(另外还有一个值为WindowStartupLocation.CenterOwner,这是利用modal对话框用的值,将对话框放在“拥有者”的中央。)


下面的一个小程序是将窗口放在工作区的中间,每次按下“向上键”或者“向下键”,就会将窗口的尺寸增减百分之十:


GrowAndShrink.cs


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


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


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


using System;


using System.Windows;


using System.Windows.Input;


namespace Petzold.GrowAndShrink


{


    public class GrowAndShrink : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new GrowAndShrink());


        }


        public GrowAndShrink()


        {


            Title = "Grow & Shrink";


            WindowStartupLocation = WindowStartupLocation.CenterScreen;


            Width = 192;


            Height = 192;


        }


        protected override void OnKeyDown(KeyEventArgs args)


        {


            base.OnKeyDown(args);


            if (args.Key == Key.Up)


            {


                Left -= 0.05 * Width;


                Top -= 0.05 * Height;


                Width *= 1.1;


                Height *= 1.1;


            }


            else if (args.Key == Key.Down)


            {


                Left += 0.05 * (Width /= 1.1);


                Top += 0.05 * (Height /= 1.1);


            }


        }


    }


}


任意键被按下,就会造成OnKeyDown方法(以及和它相关的KeyDown事件)被执行。每次你按下并放开键盘上的某个键,OnKeyDown和OnKeyUp方法就会被调用。你可以覆盖(override)这些方法,进行按键的事件处理。利用KeyEventArgs对象的Key property(其值是Key枚举型),就可以知道牵涉其中的按键是哪一个。因为Left、top、Width与Height property都是浮点型,所以增减窗口尺寸时不会丢失精度。你会达到Windows操作系统所允许的最大和最小值的限制,但是这些property的值依然不会受到影响(依然是计算的结果值)。


想要获取光标移动键(例如:上、下)和功能键(例如:F1、F2)的按键事件时,OnKeyDown和OnKeyUp方法相当有用。但是如果你想要从键盘获得实际的Unicode字符,你应该重载的方法是OnTextInput。TextCompositionEventArgs参数的Text property,是一个Unicode字符串。一般来说,这个字符串里只有一个字符,但是语音和手写输入法也可能会调用OnTextInput,这种情况下,字符串有可能会稍微长一些。


下面的程序没有设定Title property,而是让用户自行输入:


TypeYourTitle.cs


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


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


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


using System;


using System.Windows;


using System.Windows.Input;


namespace Petzold.TypeYourTitle


{


    public class TypeYourTitle : Window


    {


        [STAThread]


        public static void Main()


        {


            Application app = new Application();


            app.Run(new TypeYourTitle());


        }


        protected override void OnTextInput(TextCompositionEventArgs args)


        {


            base.OnTextInput(args);


            if (args.Text == "/b" && Title.Length > 0)


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


            else if (args.Text.Length > 0 && !Char.IsControl(args.Text[0]))


                Title += args.Text;


        }


    }


}


此方法允许的唯一控制字符是倒退键(backspace,“/b”),只有当此Title至少有一个字符长时,倒退键才会生效。除了倒退键之外,此方法就只是将从键盘输入的文字加入到Title 属性的后面。


Window类定义了其他的property,可以影响窗口的外观和行为。你可以将WindowStyle property设定为WindowStyle枚举类型值。默认的值是WindowStyle.SingleBorderWindow。WindowStyle.ThreeDBorderWindow比较炫,但是会使得客户区的面积缩小一点点。通常将对话框设定为WindowStyle.ToolWindow。这样标题栏比较短,而且只有开关钮,没有缩小放大钮。然而,你依然可以最小和最大化窗口,只要按“Atl + Space”就可以调出系统菜单。你也可以调整tool窗口的大小。WindowStyle.None也具有可调整大小的边框,但没有标题栏。你依然可以利用“Atl + Space”来调用系统菜单。没有标题栏,当然无法在窗口上显示Title property,但是Title property在taskbar中还是有用的。


一个窗口有没有“缩放边框”(sizing border),是受到ResizeMode property的影响,此property的值是RezieMode枚举类型,缺省是ResizeMode.CanResize,这表示使用者可以调整窗口大小、最小化(minimize)、或最大化(maxmize)。只要设定成ResizeMode. CanResizeWithGrip,在客户区的右下角就会显示一个小把手(grip)ResizeMode. CanMinimize 会将缩放边框消除掉,且让最大化按钮被禁用(disable),但是窗口依然可以被最小化。这个选项可以让窗口的大小固定。最后,ResizeMode.NoResize去掉了最小化和最大化的按钮,也去掉了缩放边框。.


WindowState property的类型为WindowState枚举类型,用来控制你的窗口一开始如何显示。可能的值有WindowState.Normal、WindowState.Minimized和WindowState. Maximized。


将Topmost property设定为true,表示此窗口在其他窗口的上面(使用这个property的时候,要特别注意,只有对真的有必要的窗口才做这样的设定。当然,也要让用户可以关闭这样的设定)。


Window类还有一个重要的property,就是Background。这是Window从Control继承过来的,负责掌管客户区的颜色。不过对于Background property来说“颜色”不具有足够的表现力。所以,Backgroup property是一个Brush(画刷)类型的对象,“彩绘窗口的画刷”可以有很多变化,包括渐变(gradient)画刷与位图(bitmap)画刷。画刷对于WPF相当重要,所以本书特别用了两章的篇幅来介绍。这两章的第一章,就是下一章。现在就让我们开始讨论画刷吧!

  相关解决方案