每个运行中的WPF应用程序都由System.Windows.Application类的一个实例来表示。该类跟踪在应用程序中打开的所有窗口,决定何时关闭应用程序,并引发可执行初始化和清除操作的应用程序事件。
7.1 应用程序的生命周期
在WPF中,应用程序会经理简单的声明周期。在应用程序启动后,将立即创建应用程序对象。在应用程序运行时触发各种应用程序事件,可以选择监视其中的某些事件。最后,当释放应用程序对象时,应用程序将结束。
7.1.1 创建Application对象
下面的示例演示了最小的程序:在应用程序入口处创建名为Window1的窗口,并启动一个新的应用程序。
using System;
using System.Windows;
public class Startup
{
[STAThread()]
static void Main()
{
Application app = new Application();
Window1 win = new Window1();
app.Run(win);
}
}
当向Application.Run()方法传递一个窗口时,该窗口就被设置为主窗口,并且可以通过Application.MainWindow属性在应用程序中访问这个窗口。然后Run()方法触发Application.Startup事件并显示主窗口。
7.1.2 派生一个自定义的Application类
VS从Application类派生一个自定义的类。在简单的应用程序中,这种方法没有什么有意义的效果 。但是,如果计划处理应用程序事件,那么这种方法就可以提供一个更整洁的模型,因为可以在派生自Application的类种方式所有 事件处理代码。
在本质上,VS为Application类使用的模型与用于窗口的模型相同。起点时XAML模板,默认情况下该模板被命名为App.xaml,如下所示:
<Application x:Class="AssemblyResources.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml">
<Application.Resources>
</Application.Resources>
</Application>
在XAML种使用Class特性创建派生自元素的类。因此,该类创建派生自Application的类,类名为AssemblyResources.App。
Application标签不仅创建自定义的应用程序类,还设置了StartupUri属性来确定代表主窗口的XAML文档。因此,不需要使用代码显示地实例化这个窗口。
与窗口一样,应用程序类也在两个独立部分种进行定义,在编译时融合到一起。自动生成的部分项目种时不可见的,但该部分包含Main()入口处以及启动应用程序的代码。该部分看起来如下所示:
public partial class App : System.Windows.Application
{
public void InitializeComponent()
{
this.StartupUri = new System.Uri("Window1.xaml", System.UriKind.Relative);
}
public static void Main()
{
AssemblyResources.App app = new AssemblyResources.App();
app.InitializeComponent();
app.Run();
}
}
7.1.3 应用程序的关闭方式
通常,只要还有窗口尚未关闭,Application类就保持应用程序处于有效状态。如果这不是 期望的行为,可调整Application.ShutdownMode属性。如果手动实例化Application对象,就要在调用Run()方法之前设置ShutdownMode属性。如果使用App.xaml文件,那么可在XAML文件种简单地设置ShutdownMode属性。
对于关闭模式有三种选择,如下表所示:
名称 | 说明 |
---|---|
OnLastWindowClose | 这是默认行为——只要至少还有一个窗口存在,应用程序就保持运行状态。如果关闭了主窗口,Application.MainWindow属性仍引用代表已关闭窗口的对象 |
OnMainWindowsClose | 这是传统方式——只要主窗口还处于打开状态,应用程序就保持运行状态 |
OnExplicitShutdwon | 除非调用Application.Shuwdown()方法,否则应用程序不会结束(即使所有窗口都已经关闭)。如果应用程序时场地运行的后台任务的前端,或者指示希望使用更复杂的逻辑来决定应用程序应当合适关闭,使用这种方法可能会有意义 |
如果希望使用OnMainWindowsClose方式,并且正在使用App.xaml文件,那么需要添加以下内容:
<Application x:Class="AssemblyResources.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Window1.xaml" Shutdown="OnMainWindowClose">
<Application.Resources>
</Application.Resources>
</Application>
不管选择哪种关闭方法,总是可以使用Application.Shutdown()方法立即终止应用程序。
7.1.4 应用程序事件
下表给出了Application类提供的几个重要事件。
名称 | 说明 |
---|---|
Startup | 该事件在调用Application.Run()方法之后,并且在主窗口显示之前发生。可使用该事件检查所有命令行参数,命令行参数是通过StartupEventArgs.Args属性作为数组提供的。 还可使用该事件创建和显示主窗口。 |
Exit | 该事件在应用程序关闭时(不管时什么原因),并在Run()方法即将返回之前发证。此时不能取消关闭,但可以通过代码在Main()方法种重新启动应用程序。可使用Exit事件设置从Run()方法返回的整数类型的退出代码 |
SessionEnding | 该事件在Windows对话结束时发生——例如,当用户注销或关闭计算机时(通过检查SessionEndingCancelEventArgs.ReasonSessionEnding属性可以确定原因) 。也可通过将SessionEndingCancelEventArgs.Cancel舒总设置为true来取消关闭应用程序。否则,当事件处理程序结束时,WPF将调用Application.Shutdown()方法。 |
Activated | 当激活应用程序种的窗口时发生。当从另一个Windows程序切换到该应用程序时会发生该事件。当第一次显示窗口时,也会发生该事件 |
Deactivated | 当取消激活应用程序中的窗口时发生该事件。当切换到另一个Windows程序时也会发生该事件 |
DispatcherUnhandledException | 在应用程序(主应用程序线程)中的任何位置,只要佛安生未处理的异常,就发生该事件(应用程序调度程序会捕获这些异常)。通常响应该事件,可记录重要错误,甚至可选择不处理这些异常,并通过将DispatcherUnhandledExceptionEventArgs.Handled属性设置为true继续运行应用程序。只有当可以确保应用程序仍然处于合肥状态并且可以继续运行时,才可以这样处理 |
7.2 Application类的任务
以下将介绍如何显示初始界面、如何处理命令行参数,如何支持窗口之间的交互、如何添加跟踪文档以及如何创建单实例应用程序。
7.2.1 显示初始界面
WPF应用程序的运行速度快,但并不能在瞬间启动。当第一次启动应用程序时,会有一些延迟,因为公共语言运行时首先需要初始化.NET环境,然后启动应用程序。
这一延迟未必成为问题。通常,只需要经理很短的事件,就会出现第一个窗口。但如果具有更耗时的初始化步骤,或者如果只是希望通过显示打开的图形使应用程序显得更加专业,这时可使用WPF提供的简单初始界面特性。下面时添加初始界面的方法:
- 为项目添加图像文件(bmp、png、jpg)
- 在Solution Explorer中选择文件
- 将Build Action修改为SplashScreen
下次运行应用程序时,图形会立即在屏幕中央显示出来。一旦准备好运行时环境,而且Application_Startup方法执行完毕,应用程序的第一个窗口就将显示出来,这是初始界面图形会很快消失。
7.2.2 处理命令行参数
为了处理命令行参数,需要响应Application.Startup事件。命令行参数是通过StartupEventArgs.Args属性作为数组提供的。
<Application x:Class="LoadFromCommandLine.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Startup="App_Startup">
<Application.Resources>
</Application.Resources>
</Application>
public partial class App
{
// 在该方法结束后,显示主窗口
private void App_Startup(object sender, StartupEventArgs e)
{
var win = new FileViewer();
if (e.Args.Length > 0)
{
string file = e.Args[0];
if (File.Exists(file))
{
win.LoadFile(file);
}
}
win.Show();
}
}
7.2.3 访问当前Application对象
通过静态的Application.Current属性,可在应用程序的任何位置获取当前应用程序实例,从而在窗口之间进行基本交互,因为任何窗口都可以访问当前Application对象,并通过Application对象获取主窗口的引用。
var main = Application.Current.MainWindow;
MessageBox.Show("The main window is " + main.Title);
当然,如果希望访问在自定义窗口类中添加的任意方法、属性或事件,需要将窗口对象转换为正确的类型。如果主窗口时自定义类MainWindow的实例,可使用与下面类似的代码。
var main = (MainWindow)Application.Current.MainWindow;
main.DoSomething();
在窗口中还可以检查Application.Windows集合的内容,该集合提供了所有当前打开窗口的引用:
foreach(var win in Application.Windows)
{
MessageBox.SHow(Win.Title + " is open.");
}
7.2.4 在窗口之间进行交互
自定义应用程序类时昂之响应不同应用程序事件的代码的好地方。应用程序类还可很好地达到另一个目的:保存重要窗口的引用,使一个窗口可访问另一个窗口。
例如,假设希望跟踪应用程序使用的所有文档传教港口。谓词,可在自定义应用程序类中创建专门的集合。下面是使用泛型列表集合保存一组自定义窗口对象的实例。在这个示例中,每个文档窗口由名为Document的类的实例表示。在创建文档时,将其添加到文档列表中即可。
public partial class App : Application
{
public List<Document> Documents {get; set;} = new List<Document>();
}
7.2.5 单实例应用程序
对于单实例应用程序,WPF本身并未提供自带结局方法,但可使用几种变通方法。基本技术时当初发Application.Startup事件时,检查另一个应用程序实例是否已在运行。最简单的方法时使用全局Mutex对象。这种方法很简单,但功能有限。最重要的时,应用程序的新实例无法与已存在的实例进行通讯。对于基于文档的应用程序而且这确实是一个问题,因为新实例可能需要告诉已经存在的应用程序实例打开某个特定的文档。
最简单同时也是WPF团队推荐的方法是:使用Windows窗体提供的内置支持,这一内置支持住处是VB应用程序的。这种方法在后台处理杂乱的问题。
使用这种方法的第一步是添加对Microsoft.VisualBasic.dll 程序集的引用,并从Microsoft.VisualBasic.ApplicationServices.WindowsFormsApplicationBase类继承自定义类型。自定义类型提供了三个用于管理实例的重要成员:
- IsSingleInstance属性启动单实例应用程序。在构造函数中将该属性设置为true。
- 当应用程序启动时触发OnStartup()方法。此时重写该方法并创建WPF应用程序对象。
- 当零一应用程序实例启动时触发OnStartupNextInstance()方法。该方法提供了访问命令行参数的功能。稀释,可调用WPF应用程序类中的方法来显示新的窗口,但不创建另一个应用程序实例。
public class SingleInstanceApplicationWrapper : WindowsFormsApplicationBase
{
// Create the WPF application class.
private WpfApp app;
public SingleInstanceApplicationWrapper()
{
// Enable single-instance mode.
IsSingleInstance = true;
}
protected override bool OnStartup(StartupEventArgs e)
{
var extension = ".testDoc";
var title = "SingleInstanceApplication";
var extensionDescription = "A Test Document";
// Uncomment this line to create the file registration.
// In Windows Vista, you'll need to run the application
// as an administrator.
//FileRegistrationHelper.SetFileAssociation(
// extension, title + "." + extensionDescription);
app = new WpfApp();
app.Run();
return false;
}
// Direct multiple instances
protected override void OnStartupNextInstance(
StartupNextInstanceEventArgs e)
{
if (e.CommandLine.Count > 0) app.ShowDocument(e.CommandLine[0]);
}
}
public class WpfApp : Application
{
// An ObservableCollection is a List that provides notification
// when items are added, deleted, or removed. It's preferred for data binding.
public ObservableCollection<DocumentReference> Documents { get; set; } = new ObservableCollection<DocumentReference>();
protected override void OnStartup(System.Windows.StartupEventArgs e)
{
base.OnStartup(e);
// Load the main window.
var list = new DocumentList();
MainWindow = list;
list.Show();
// Load the document that was specified as an argument.
if (e.Args.Length > 0) ShowDocument(e.Args[0]);
}
public void ShowDocument(string filename)
{
try
{
var doc = new Document();
var docRef = new DocumentReference(doc, filename);
doc.LoadFile(docRef);
doc.Owner = MainWindow;
doc.Show();
doc.Activate();
Documents.Add(docRef);
}
catch
{
MessageBox.Show("Could not load document.");
}
}
}
public class Startup
{
[STAThread]
public static void Main(string[] args)
{
var wrapper = new SingleInstanceApplicationWrapper();
wrapper.Run(args);
}
}
7.3 程序集资源
WPF应用程序中的程序集资源与其他.NET应用程序中的程序集资源在本质上是相同的。基本概念是为项目添加文件,从而VS可将其嵌入到编译过的应用程序的EXE或DLL文件中。WPF程序集资源与其他应用程序中的程序集资源之间的重要区别是引用它们的寻址系统不同。
7.3.1 添加资源
可通过向项目添加文件,并在属性窗口中将其Build Action属性设置为Resource来添加自己的资源。为了更加合理地组织资源,可在项目中创建子文件夹,然后使用这些子文件夹组织不同类型的资源。为成功地使用程序集资源,务必注意以下两点:
- 不能将Build Action属性错误的设置为Embedded Resource。尽管所有程序集资源都被定义为嵌入的资源,但Embedded Resource生成操作会在另一个更难访问的位置放置二进制数据。在WPF应用程序中,假定总是使用Resource生成类型。
- 不用在项目属性中使用Resouce选项卡。WPF不支持这种类型的资源URI。
7.3.1 检索资源
低级方法是检索封装数据的StreamResourceInfo对象,然后决定该如何使用该对象。可通过代码,使用静态方法Application.GetResourceStream()完成该工作。
var sri = Application.GetResourceStream(new Uri("images/winter.jpg"), UriKind.Relative);
一旦得到StreamResourceInfo对象,就可得到两部分信息。ContentType属性返回一个描述数据类型的字符转——在该例中是image/jpg。Stream属性返回一个UnmannedMemorySteam对象,可使用该兑现官渡区数据,一次读取一个字节。
GetResourceStream()的爹是一个很有用的辅助方法,它封装了ResourceManager类和ResourceSet类。这些类是.NET Framework资源系统的核心。如果不使用GetResourceStream()方法,就需要具体访问AssemblyName.g.resources资源流(这是WPF存储所有WPF资源的地方),并查找所需的对象。下面是完成这一操作的覅长简单的代码:
Assembly assembly = Assembly.GetAssembly(this.GetType());
string resourceName = Assembly.GetName().Name + ".g";
ResourceManager rm = new ResourceManager(resourceName, assembly);
using(ResourceSet set = rm.GetResourceSet(CultrueInfo.CurrentCultrue, true, true))
{
var s = (UnmannedMemorySteam)set.GetObject("image/winter.jpg", true);
}
7.3.3 pack URI
WP使用pack URI语法寻址编译过的资源。
images/wrinter.jpg
packL//application:,,,/images/winter.jpg
7.3…3.1 位于其他程序集中的资源
使用pack URI 还可以检索嵌入到另一个库中的资源。这种情况下需要使用以下语法:
pack://application:,,,/AssemblyName;compoent/ResourceName
例如,如果图像被嵌入到引用的名为ImageLibrary的程序集中,将需要使用如下的URI:
img.Source = new BitmapImage(new Uri("pack://application:,,,/ImageLibrary;compoent/images/winter.jpg"));
img.Source = new BitmapImage(new Uri("ImageLibrary;compoent/images/winter.jpg", UriKind.Relatice));
如果使用强命名的程序集i,可使用包含版本和/或公钥标记的限定程序集引用代替程序集的名称。使用分号隔离每段信息,并在版本号数字之前添加字母v。如下所示 :
img.Source = new BitmapImage(new Uri("ImageLibrary;v1.25;compoent/images/winter.jpg", UriKind.Relatice));
img.Source = new BitmapImage(new Uri("ImageLibrary;v1.25;dc642a7f5bd64912;compoent/images/winter.jpg", UriKind.Relatice));
7.3.4 内容文件
当嵌入文件作为资源时,会将文件放到编译过的程序集中,并且可以确保文件总是可用的。对于部署而言这是理想选择,并且可避免可能存在的问题。然后在有些情况下,使用这种方法并不方便:
- 希望改变资源文件,又不想重新编译应用程序
- 资源文件非常大
- 资源文件是可选的,并且可以不随程序集一起部署
- 资源是声音文件
不能见改内容该文件嵌入到程序集中。然而,WPF为程序集添加了AssemblyAssociatedContentFile特性,公告每个内容文件的存在。该特性还记录了每个内容文件相对于可执行文件的位置。最方便的是,当为能够理解资源的元素使用内容文件时,可使用相同的URI系统。
为测试该技术,为项目添加声音文件,在解决方案管理器中选择该文件,并在属性窗口将Build Action属性改为Content。确保Copy to Output Directory属性设置为Copy Always。以保证当生成项目时将声音文件复制到输入目录。现在可使用相对URI,将MediaElement元素指向内容文件:
<MediaElement Name="Sound" Source="Sounds/start.wav" LoadedBehavior="Manual" />
7.4 本地化
当需要本地化窗口时,程序集资源也可以提供方便。使用资源,可根据Windows操作系统的当前文化设置改变控件。对于需要翻译为不同语言的文本标签和图像,这尤其有用。
在WPF中,使用XAML文件作为本地化单元。如果希望支持三种不同的语言,需要包含三个BAML资源。WPF会根据执行应用程序的计算机的当前文化设置,选择正确的资源。
一般会但单独的附属程序集中放置每个本地化了的BAML资源。为让应用程序能使用该程序集,需将它们放在主应用程序文件夹的子文件夹中,例如,fr-FR文件夹用于法语。然后,应用程序可使用探测技术,自动绑定到附属程序集。
本地化应用程序的挑战在于整个工作过程——换句话说,是指如何从项目中提取XAML文件,如何本地化这些XAML文件,如何将它们编译进附属程序集中,然后如何在应用程序中使用本地化的资源。这是WPF本地化过程中最不稳定的部分,因为诸如VS的工具本身支持本地化。
7.4.1 构建能够本地化的用户界面
在开是翻译任何内容前,首先需要考虑应用程序会如何响应内容变化。例如,如果用户界面中所有的文本的长度都变为原来的两倍,如果调整整个窗口的布局?如果已经构建了真正自适应的布局,这就不成问题。用户加密那应该能够调整自身以适应动态的内容。下面列出建议采用的一些原则:
- 不使用硬编码的宽度或高度
- 将Windows.SizeToContent 属性设置为Width、Height或WidthAndHeight,使窗口尺寸能够 根据需要扩大
- 使用ScrollViewer控件封装大量文本
7.4.2 使应用程序为本地化做好准备
下一步使让项目支持本地化。为此只需要进行一处修改——需要为项目的.csproj文件,在第一个<PropertyGroup>元素中的任意地方添加以下元素:
<UICultrue>en_US</UICulture>
上面的标记噶偶编译器,应用程序的默认文化使美式英语。一旦进行了这一项修改,生成过程就会发生变化。下次编译应用程序时,最后会生成名为en-US的子文件夹。在该文件夹中包含的是附属程序集,附属程序集与应用程序同名,而且拓展名为.resource.dll。附属程序集包含了应用程的所有编译过的BAML资源,以前这些资源保存在主应用程序集中。
现在,当运行该程序是,CLR会根据 计算机的区域设置自动在正确的目录中查找附属 程序集,并加载正确的已本地化的资源。例如,如果使用fr-FR文化运行该程序,CLR会查找fr-FR子目录,并使用查找大的附属程序集。因此,如果希望为更多的文化台南佳应用程序本地化支持,只需要添加更多子文件夹和附属程序集,而不感染原来的可执行的应用程序。当CLR开始然查附属程序集时,遵循以下几条简单的优先原则:
- 首先检查可用的最具体目录,这意味着查找针对当前语言和区域的附属程序集
- 如果在上面的目录中没有找到,接下来查找针对当前语言的附属程序集
- 如果在上米娜的目录中还没找打,就抛出IOException异常
7.4.3 管理翻译过程
现在已经具备了本地化需要的全部基础架构。现在需要做的全部工作就是以BAML形式创建适应其他版本窗口的附属程序集,并将这些程序集放到正确的文件夹中。如果手动完成该任务 ,显然需要完成大量工作。此外,本地化通常设计第三方翻译服务,需要使用该服务对原始文本进行处理。现场,不能期望翻译者是能在VS项目中找到这些原始文本的熟练程序员。因此需要一种管理本地化过程的方法。
目前,WPF提供了部分解决方案。该方案能够奏效,单需要使用一些命令行执行,并且有一个任务不能完成。基本的处理过程如下:
- 标识应用程序中需要本地化的元素。
- 提取能够本地化的内容的细节并保存到.csv文件中
- 一旦接收到该文件翻译后的版本,就再次运行LocBaml命令行工具,生成所需的附属程序集。
7.4.3.1 为本地化准备标记元素
第一步是针对所有希望本地化的元素,添加专门的Uid特性。如下:
<Button x:Uid="Button_1" Margin="10" Padding="3">A Button</Button>
Uid特性扮演与Name特性类似的角色——在单个XAML文档上下文中唯一地标识一个按钮 。通过这种方法,可以只为该按钮指定本地化的文本。
应当为本地化应用程序的每个窗口中的每个元素添加Uid,尽管这不是必须的。这可能会增加大量额外的工作,不过,MsBuild工具可自动完成该项工作。使用该工具的方法如下所示:
msbuild /t :updateuid LocalizeableApplication.csproj
如果希望检查是否所有元素都具有Uid(并确保没有意外地多复制了一个Uid),可按如下方法进行检查:
msbuild /t :checkuid LocalizeableApplication.csproj
当使用MsBuild生成Uid时,Uid与响应弓箭的名称时匹配的。下面时一个示例:
<Button x:Uid="cmdDoSomething" Name="cmdDoSomething" .../>
如果元素没有名称,MsBuild会根据类名生成Uid,这个Uid使用数字后缀。
<Button x:Uid="Button_1" .../>
7.4.3.2 提取可被本地化的内容
要为所有元素提取能够被本地化的内容,需要使用LocBaml命令工具。目前,LocBaml不是已经编译过的工具。可通过搜索源码,并进行编译。
当使用LocBaml命令行工具时,必须位于包含编译过的程序集的文件夹中。为提取能被本地化的细节的列表,需要将LocBaml命令行工具指向附属程序集,并使用 /parse参数,如下所示:
locbaml /parse en-US\LocalizeableApplication.resources.dll
LocBaml 工具为所有编译过的BAML资源查找附属程序集,并生成包含细节内容的.csv文件。在该例中,这个.csv文件被命令为LocalizeableApplication.resources.csv。
提取出的文件中的每一行代表一个在XAML文档中应用于元素的可被本地化的属性。每行都包含以下七个值:
- BAML资源名称(例如,LocalizableApplication.g.en-US.resources:Window1.baml)
- 元素的Uid和要本地化的属性的名称(例如,StackPanel_1:System.WIndows.FrameworkElement.Margin)
- 本地化累些。该值来自LocalizationCategory枚举,用于帮助识别属性标识的内容的类型(长文本、标题、字体、按钮标题和工具提示)
- 属性是不是可读的(实质上是指能否在用户界面上显示为文本)。所有刻度的值总是需要被本地化,单不可读的值可能需要本地化,也可能不需要
- 属性值是否可以被翻译者修改。除非专门为其指定了其他值,该属性总是为true
- 为翻译者提供的额外注释。试过尚未提供注释,该值为空
- 属性值。这是需要被本地化的细节
7.4.3.3 生成附属程序集
locbaml /generate en-US\LocalizeableApplication.resources.dll
/trans:LocalizableApplication.resources.French.csv
/cul:fr-FR
/out:fr-FR