应用程序的生命周期
在 WPF 中,应用程序会经历简单的生命周期。在应用程序启动后,将立即创建应用程序对象。在应用程序运行时触发各种应用程序事件,您可以选择监视其中的某些事件。最后,当释放应用程序对象时,应用程序将结束。
创建Application对象
手动创建Application对象启动Wpf应用
-
设置项目属性的
EnableDefaultApplicationDefinition
属性为false使WPF应用不自动生成Main函数1
2
3
4
5
6<PropertyGroup>
...
<!-- +++ -->
<EnableDefaultApplicationDefinition>false</EnableDefaultApplicationDefinition>
<!-- +++ -->
</PropertyGroup> -
新建自己的文件
1
2
3
4
5
6
7
8
9
10class Program
{
[ ]
public static void Main(string[] args)
{
App app = new App();
MainWindow mainWindow = new MainWindow();
app.Run(mainWindow);
}
}
应用程序的关闭方式
通常,只要还有窗口尚未关闭,Application类就保持应用程序处于有效状态。如果这不是期望的行为,可调整Application.ShutdownMode
属性和。如果手动实例化Application对象,就需要在调用Run()方法之前设置ShutdownMode属性。如果使用App.xaml文件,那么可在XAML文件中简单设置ShutdownMode属性
名称 | 说明 |
---|---|
OnLastWindowClose | 这是默认行为——只要至少还有一个窗口存在,应用程序就保持运行状态 |
OnMainWindowClose | 这是传统方式——只要主窗口还处于打开状态,应用程序就保持运行状态 |
OnExplicitShutdown | 除非调用Application.Shutdown() ,否则应用程序就不会结束 |
应用程序事件
名称 | 说明 |
---|---|
Startup | 该事件在调用Application.Run() 之后,并且在主窗口显示之前发生。可使用该事件检查所有命令行参数,命令行参数时通过StartupEventArg.Args 属性作为数组提供的 |
Exit | 该事件在应用关闭时,并在Run() 即将返回之前发生。此时不能取消关闭 |
SessionEnding | 该事件在Windows对话结束时发生——例如,当用户注销或关闭计算机时(通过检查SesstionEndingCancelEventArgs.ReasonSessionEnding 属性可以确定原因)。也可以通过将SessionEndingEventArgs.Cancel 属性设置为true来取消关闭应用程序。否则,当事件处理程序结束时,WPF将调用Application.Shutdown() 方法 |
Activated | 当激活应用程序中的窗口时发生该事件。当从另一个Windows程序切换到该应用时会发生该事件。当第一次显示窗口时也会发生该事件 |
Deactivated | 当取消激活应用程序中的窗口时发生该事件。当切换到另一个Windows程序时耶夫一发生该事件 |
DispatcherUnhandledException | 在应用程序(主应用程序线程)中的任何位置,只要发生未处理的异常,就会发生该事件(应用程序会驳货这些异常)。通过响应该事件,可记录重要错误,甚至可选择不处理这些异常,并通过将DispatcherUnhandledExceptionEventArgs.Handled 属性设置为true继续运行应用程序。只有当可以确保应用程序仍然处于合法状态时可以继续运行时,才这样处理 |
下面时一个自定义的应用程序类,他重写了OnSessionEnding()
方法,并且给如果设置了相应的标志,该方法会阻止关闭系统和应用程序自身
1 | public partial class App : Application |
Application类的任务
显示初始界面
使用WPF提供的简单初始界面特性:
- 为项目添加图像文件(通常时.bmp、.png或.jpg文件)
- 在Solution Explorer 中选择图像文件
- 将Build Action修改为
SplashScreen
下次运行应用程序时,图像会立即在屏幕中央显示出来。一旦准备好运行时环境,而且Application_Startup
方法执行完毕,应用程序的第一个窗口就将显示出来,这是初始界面图形会很快消失。以上添加初始界面的朝族哟,WPF编译器为自动生成的App.g.cs文件添加与下面类似的代码:
1 | SplashScreen splashScreen = new SplashScreen("splashScreenImage.png"); |
处理命令行参数
为处理命令行参数,需要相应Aplication.Startup
事件。命令行参数时通过StartupEventArgs.Args
属性作为字符串数组提供的
1 | protected override void OnStartup(StartupEventArgs e) |
访问当前Application对象
通过静态的Application.Current
属性,可在应用程序的任何位置获取当前应用程序实例,从而在窗口之间进行基本交付,因为任何窗口都有可以访问当前Application对象,并通过Application对象获取主窗口的应用:
1 | Window mainWindow = Application.Current.MainWindow; |
子啊窗口中还可以检查Application.Windows
集合的内容,该集合提供了所有当前打开窗口的引用:
1 | foreach(Window window in Application.Current.Windows) |
在窗口之间进行交互
正如在前面已经看到的,自定义应用程序类是放置响应不同应用程序事件的代码的好地方。应用程序类还可以很好地达到另一个目的:保存重要窗口的引用,使一个窗口可访问另一个窗口。
1 | public partial class App : Application |
单实例应用程序
通常,只要愿意就可以加载 WPF 应用程序的任意多个副本。某些情况下,这种设计是非常合理的。但在另外一些情况下,这可能会成为问题,当构建基于文档的应用程序时更是如此。
对于单实例应用程序,WPF本身并未提供自带的解决方法,但可使用几种变通方法。基本技术时当出发Application.Startup
事件时,检查另一个应用程序实例时候已在运行。最简单的方法时使用全局的mutex对象
(mutex对象时操作系统提供的用于进程间通信的同步对象)。这种方法很简单,但功能有限。最重要的是,应用程序的新实例无法与已经存在的应用程序实例进行通信。
第一种方法较为简单直接,适合大多数场景。第二种方法更为复杂,但可以处理一些特定的需求,例如在已有实例中显示新的内容。
使用Mutex
使用Mutex可以确保只有一个实例在运行。如果尝试启动第二个实例,新的实例将检测已有的实例在运行并退出
1 | public partial class App : Application |
使用Windows API
通过P/Invoke使用Windows API来实现单实例
-
创建一个
NativeMethods
类1
2
3
4
5
6
7
8
9
10
11
12
13
14internal static class NativeMethods
{
public const int HWND_BROADCAST = 0xffff;
public static readonly int WM_SHOWFIRSTINSTANCE = RegisterWindowMessage("WM_SHOWFIRSTINSTANCE|" + App.AppGuid);
[ ]
public static extern int RegisterWindowMessage(string message);
[ ]
public static extern bool PostMessage(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam);
[ ]
public static extern IntPtr FindWindow(string classname, string windowname);
} -
更新
App.xaml.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41public partial class App : Application
{
private static Mutex _mutex = null;
public const string AppGuid = "your-app-guid-here";
protected override void OnStartup(StartupEventArgs e)
{
bool createdNew;
_mutex = new Mutex(true, "Global\\" + AppGuid, out createdNew);
if (!createdNew)
{
NativeMethods.PostMessage((IntPtr)NativeMethods.HWND_BROADCAST, NativeMethods.WM_SHOWFIRSTINSTANCE, IntPtr.Zero, IntPtr.Zero);
Application.Current.Shutdown();
return;
}
base.OnStartup(e);
ShowMainWindow();
}
private void ShowMainWindow()
{
MainWindow mainWindow = new MainWindow();
mainWindow.Show();
IntPtr handle = (new WindowInteropHelper(mainWindow)).Handle;
HwndSource source = HwndSource.FromHwnd(handle);
source.AddHook(new HwndSourceHook(WndProc));
}
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
if (msg == NativeMethods.WM_SHOWFIRSTINSTANCE)
{
ShowMainWindow();
}
return IntPtr.Zero;
}
}
程序集资源
程序集资源又称为二进制资源,因为它们作为不透明的二进制数据被嵌入到已编译的程序集中
添加资源
可同构项目添加文件,并在Properties窗口中将其Build Action属性设置为Resource来添加自己的资源。为成功地使用程序集资源,无比注意以下两点:
- 不能将Build Action属性错误地设置为 Embedded Resource。尽管所有程序集资源都被定义为嵌入的资源,当Embedded Resource生成操作会在另一个更难访问的位置放置二进制数据。在WPF应用程序中,假定总是使用Resource生成类型
- 不要在Propect Properties窗口中使用Resource选项卡。WPF不支持这种类型的资源URI
WPF将程序集资源和其他BAML资源合并到单独的流中。单独的资源流是哟个以下格式命名:AssemblyName.g.resources
检索资源
显然,添加资源非常容易,但到底如何使用它们了?可以采用多种方法来使用资源
-
通过静态方法
Application.GetResourceStream()
1
StreamResourceInfo sri = Application.GetResourceStream(new Uri("images/winter.jpg",UriKind.Relative));
-
自行访问
AssemblyName.g.resource
资源流,并查询所需的对象1
2
3
4
5
6
7
8Assembly assembly = Assembly.GetAssembly(this.GetType());
string resourceName =assembly.GetName().Name + ".g";
ResourceManager rm = new ResourceManager(resourceName,assembly);
using(ResourceSet set = rm.GetResourceSet(CaltureInfo.CurrentCulture,true,true))
{
UnmanageMemoryStream s = (UnmanageMemoryStream)set.GetObject("images/winter.jpg",true);
}
pack URI
WPF使用pack URI语法寻址编译过的资源,其完整语法如下:
1 | pack://application:,,,/AssemblyName;component/ResourceName |
例如:
- 当前程序集下
1 | <!-- 以下写法等价 --> |
- 其他程序集下
1 | <!-- 资源图片位于WpfResources程序集的images目录下 --> |
内容文件
当嵌入文件作为资源时,会将文件放到编译过的程序集中,并且可以确保文件总是可用的。对于部署而言这是理想选择,并且可避免可能存在的问题。然而在有些情况下,使用这种方法并不方便:
- 希望改变资源文件,又不想重新编译应用程序
- 资源文件非常大
- 资源文件是可选的,并且可以不随程序集一起部署
- 资源是声音文件
WPF声音不支持程序集资源。因此,无法从资源流中析取音频文件并播放它们——至少,如果没有首先保存音频文件,就不能播放它们。这一局限是由于这些类使用的技术基础(Win32API和媒体播放器)造成的
显然,可使用应用程序部署文件,并为应用程序添加代码,进而从硬盘驱动器中读取这些文件来解决该问题。然而,WPF还有更方便的选择,使这一过程更加容易管理。可将这些未编译的文件专门标记为内容文件。
不能将内容文件嵌入到程序集中。然而,WPF为程序集添加了AssemblyAssociated-ContentFile 特性,公告每个内容文件的存在。该特性还记录了每个内容文件相对于可执行文件的位置(指示内容文件是否和可执行文件位于同一个文件夹中,或者位于某个子文件夹中)。最方便的是,当为能够理解资源的元素(如 Image 类)使用内容文件时,可使用相同的 URI 系统。
为测试该技术,为项目添加声音文件,在SolutionExplorer 中选择该文件,并在Properties窗口中将 Build Action 属性改为 Content
。确保将 Copy to Output Directory 属性设置为 CopyAlways
,以保证当生成项目时将声音文件复制到输出目录中。
1 | <MediaELement Name="Sound" Source="Sounds/start.wav" LoadedBehavior="Manual"/> |
本地化
实现本地化的方法主要有以下三种:
- 通过编译项目以设置
x:Uid
并使用LocBaml工具实现 - 通过
DynamicResource
实现 - 通过
Resx
文件实现
LocBaml工具
官方介绍的方法,考虑到实现步骤略为复杂,所以直接忽略
DynamicResourc方式
主要是在程序中添加Resource Dictionary
类型的文件,并在其中放入本地化资源字符串;在XAML代码中,直接使用{DynamicReource XXX}
来实现;这种方法比较方便,不过也有两个缺点:
- 在XAML中,应用DynamicResource的属性必须为依赖属性,否则会出错
- 在C#代码中引用稍微有点麻烦,需要从Resource Dictionary中获取并转化为字符串
Resx文件
Visual Studio搜索并安装扩展插件(ResXManager)[https://marketplace.visualstudio.com/items?itemName=TomEnglert.ResXManager]
- 在项目内
Properties
文件夹内添加新建项,资源文件Resource.resx
- 手动编译项目,然后
Resource.resx
右键菜单→在ResX Manager
中打开
- 打开后界面如下
- 添加新语言
- 运行时切换语言
- 新建类
ResourceService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23public class ResourceService : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private static readonly ResourceService _current = new ResourceService();
public static ResourceService Current => _current;
readonly Properties.Resource resource = new Properties.Resource();
public Properties.Resource Resources => resource;
protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = this.PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
public void ChangedCulture(string name)
{
Properties.Resource.Culture = CultureInfo.GetCultureInfo(name);
this.RaisePropertyChanged("Resources");
}
} - XAML使用
1
<Button Content="{Binding Resources.Hello, Source={x:Static local:ResourceService.Current}}"/>
- 切换语言
1
2
3
4private void Changed()
{
ResourceService.Current.ChangedCulture("en-US");
}
- 新建类