(图片来自Bing搜索)
Windows 7提供了JumpList,可以通过右击任务栏上的图标,跳出快捷菜单,为用户操作应用程序提供一个捷径。例如,Live Messenger为我们提供了注销和退出的功能:
WPF为Jump List提供了支持。
我们来实现如下一个Jump List:
- 这个JumpList有三个分组,分别是:Function,3rd Party Applications和Window Control。
- Function里提供一个Hello的项,弹出一个Hello JumpList对话框;
- 3rd Party Appliation里,我们可以直接调用计算器;
- Window Control里提供两个功能,另外是最大化和最小化窗口。
如果没有接触过Jump List的编程,现在,请花2分钟时间大概构想一下实现方法吧……(2分钟过去了)
来看看结果是不是跟你想像的一样。
基础
明白JumpList的结构
这个不多说,请大家看图:
我们将结合WPF来看看JumpTask的开发。
JumpList的XAML支持
WPF提供了叫JumpList的Attach属性来支持JumpList。大概的样子如下:
<Application … <JumpList.JumpList> <JumpList> <JumpTask Title="Say Hello" Arguments="hello" /> </JumpList> </JumpList.JumpList> </Application>
仔细观察高亮的代码行,它有一个Title属性,大家一定都知道它是干什么的了。Arguments属性也可以理解,传入参数。
读到这里是不是有一个疑问:为什么没有类名或者函数名称之类的?这个JumpTask将由谁哪个函数来执行呢?
JumpTask执行逻辑
卖个关子,先。请大家新建一个WPF应用程序:
- 打开App.XAML,复制、粘贴从以上代码
- 在项目属性中去掉Enable the Visual Studio hosting process的对勾
- F5调试
- 右击应用任务栏上的图标,是不是已经看到对应的JumpList项了?
- 点击调用Say Hello。
看到了什么?没错,一个新的应用程序实例被打开了。因此,Arguments的调用者,就是应用程序本身,再精确点说,是Main函数。
如果应用程序名叫JumpList.exe,那么当用户点击Say Hello时,相当于调用了JumpList.exe hello。
既然每次点击JumpList上的项都会产生一个新的应用程序实例,那么如何实现让它弹出Hello JumpList的对话框?
这里提供一种简单实现:
我们重写App类的OnStartup方法,利用Environment.GetCommandLineArgs()方法来获取参数,如果参数为hello,那么显示对话框,然后马上结束当前实例。
为了减小干扰,这里用了硬编码来读取第一个参数。实际项目慎用。
1: protected override void OnStartup(StartupEventArgs e)
2: {
3: base.OnStartup(e);
4: // Get all command arguments
5: var arguments = Environment.GetCommandLineArgs();
6: // Execute when there are arguments other than the application itself.
7: if (arguments.Length > 1)
8: {
9: // Lower the character.
10: var argument = arguments[1];
11: if (argument.Equals("hello", StringComparison.InvariantCultureIgnoreCase))
12: {
13: // Execute when the parameter equals hello.
14: MessageBox.Show("Hello JumpList");
15: // Shutdown the new instance.
16: App.Current.Shutdown();
17: }
18: }
19: }
请大家按照这个思路编写代码,看看执行效果:
JumpTask调用第三方应用
根据我们对JumpList的理解,不难推测,JumpTask调用第三方应用将异常的简单,例如,我们可以通过以下代码调用计算器(calc.exe):
<JumpTask Title="Calculator" ApplicationPath="calc" />
利用ApplicationPath属性来指定要执行的应用程序名称。
Arguments属性仍然有效。这也就是说,如果我们把ApplicationPath指向自己,并且传入对应的参数,即可达到前一个例子的效果。
为JumpTask项目分组
对JumpTask的分组也相当简单,只要设置JumpTask项的CustomCategory属性即可。例如:
<JumpList.JumpList> <JumpList> <JumpTask Title="Calculator" ApplicationPath="calc" CustomCategory="3rd Party Applications" /> <JumpTask Title="Say Hello" Arguments="hello" CustomCategory="Function" /> </JumpList> </JumpList.JumpList>
需要注意的是,分组出现的顺序是从后向前的。例如,上面的JumpList中,最后定义的分组是Function,那么,在JumpList中,最先出现的分组就是Function。
JumpTask项的图标设置
JumpTask有两个属性用以控制图标:
- IconResourcePath指向图标所在的位置
- IconResourceIndex设置图标的序号(0为起始序号)
例如:
<JumpTask Title="Calculator" ApplicationPath="calc" CustomCategory="3rd Party Applications" IconResourcePath="%windir%\system32\calc.exe" /> <JumpTask Title="Say Hello" Arguments="hello" CustomCategory="Function" IconResourcePath="%windir%\System32\imageres.dll" IconResourceIndex="18" />
分别取到了计算器本身的第0个图标和imageres.dll的第18个图标。
如果不想有图标,那就把IconResourceIndex设置成-1。
至此,我们了解了WPF中JumpTask编程的基本内容,包括JumpList的结构,调用逻辑等。
但是,从用户的角度出发,JumpList往往应该是针对当前应用实例的。例如,我们列出的目标JumpList中,包括了最大化、最小化。当用户点击它们的时候,肯定不会期望应用程序新建一个实例,并且最大化、最小化新实例。
要实现这样的功能,需要涉及到两个常用的WPF编程技巧,分别是:
- 单一应用程序实例
- 进程间交互
提高
(图片来自Bing搜索)
实现思路:
首先,当用户点击JumpTask项时,如果已经有进程实例存在,那么,新实例将终止自己——即实现单一应用程序实例;
其次,新实例终止自己前,需要告诉原有实例,用户调用了什么操作——新老进程间交互;
我们将问题分解,分两部分实现:
单一应用程序实例的实现
参考:Single Instance WPF Application in .NET 3.x介绍了两种实现方法:
- 检查进程名——有的项目通过此方法检查安装程序,结果导致与系统进程名冲突,不推荐使用。
- 使用WindowsFormsApplicationBase——文章里有源代码,大家可以试试。
这里另外介绍使用信号量(Mutex)的方法,来实现WPF应用程序单一实例。
.NET提供了Mutex类(System.Threading.Mutex),它的构造方法中有一个重载,接受一个名称,并且输出是否新建了信号量。我们可以利用这个重载,为应用程序找一个唯一的标识。
最容易想到的方法就是生成一个GUID。
Visual Studio自带了一个GUID在的生成器,可以通过Tools | Create GUID打开,把对应的GUID复制出来即可。
接下来,我们在OnStartup中,创建信号量,并且在OnExit中,销毁它。
主要代码如下:
1: using System;
2: using System.Threading;
3: using System.Windows;
4:
5: namespace WpfApplication20
6: {
7: public partial class App : Application
8: {
9: Mutex _singleInstanceMutex;
10: private const string _applicationGUID = "38038762-99E1-4F8A-A937-1AE8A3308298";
11:
12: protected override void OnStartup(StartupEventArgs e)
13: {
14: base.OnStartup(e);
15: bool isNewMutex;
16: _singleInstanceMutex = new Mutex(false, _applicationGUID, out isNewMutex);
17: if (isNewMutex)
18: {
19: //1st instance of current application
20: }
21: else
22: {
23: // Not the 1st instance
24: var arguments = Environment.GetCommandLineArgs();
25: // Execute when there are arguments other than the application itself.
26: if (arguments.Length > 1)
27: {
28: // Deal with the command argumens.
29: // ...
30: }
31: else
32: {
33: MessageBox.Show("Sorry, only one instance of the applciation is allowed!", "Multiple Instances", MessageBoxButton.OK, MessageBoxImage.Error);
34: }
35: // Shutdown the new instance.
36: App.Current.Shutdown();
37: }
38: }
39: }
40: }
用WCF实现进程间交互
要实现通过JumpTask来最大化、最小化,除了不让一个以上的应用程序实例出现以外,我们还需要在新进程被Shutdown之前,告诉第一个实例的进程,进行最大化或者最小化操作。这就涉及到进程间交互。
我们知道,在OS中,进程之间的资源是相互独立的。要让进程之间进行通讯,必须付出努力。
以前,在.NET中,我们通过Remoting来进行进程间交互。(我们可以通过维基百科来查查Remoting的底细。)
现在Remoting已被WCF代替。我们下面就来看如何用WCF,实现最大化、最小化窗口的方法。
你不需要太多的关于WCF的知识。如果你没有接触过WCF,那么,记住下面两件事情和一些基本概念就可以了:
- 逻辑上,WCF分客户端和服务端;
- 客户端和服务端通过端点(EndPoint)进行通讯。ABC进行通讯。什么是ABC呢?ABC就是Address,Binding和Contract。
这里有一些基本概念,大家留一个印象(描述并不一定很精确,大家参考MSDN啊):
- Address:一个Uri,用于表达服务所在位置;
- Binding:定义通讯的方法,其实就是在服务端与客户端约定好,怎么通讯,是http呢还是namedpipe呢还是TCP……不明白细节没有关系,只要在服务器与客户端使用一致的Binding类型即可。
- Contract:定义服务提供的具体的方法及参数——我们往往使用.NET接口作为协议。
有了这些基本概念以后,我们就可以着手实现功能了。
首先,我们要想清楚,在我们的例子中,谁是客户端,谁是服务端?
……(想ing…)
- 由于是第二个应用程序实例要调用第一个应用程序实例功能(最大化、最小化),因此,第一个应用程序实例需要提供服务;而第二个应用程序实例作为客户端来调用服务。
- 第一个应用程序的实例的代码和第二个应用程序的实例的代码……是同一份,因此,无论是服务还是客户端代码,都应该在同一个项目中。
- 通过前面对单一应用程序实例的实践,我们有办法判断代码是第一个实例还是后来的实例了,因此,我们可以在第一个实例的时候作为服务端创建服务;以后每次调用服务,并结束进程。
这样,终于可以动手了,先写服务端代码:
第一步:添加引用:System.ServiceModel和System.Runtime.Serialization。
第二步:实现服务——编写实现最大化和最小化的类:
1: public class MMService
2: {
3: public void Max()
4: {
5: App.Current.MainWindow.WindowState = System.Windows.WindowState.Maximized;
6: }
7:
8: public void Min()
9: {
10: App.Current.MainWindow.WindowState = System.Windows.WindowState.Minimized;
11: }
12: }
第三步,抽象出服务接口,作为服务协议(ABC中的C):
我们可以通过Visual Studio提供的重构功能,自动抽象出接口代码,然后,为接口添加协议所需要的属性:ServiceContract和OperationContract。
1: using System;
2: using System.ServiceModel;
3: namespace WpfApplication20
4: {
5: [ServiceContract]
6: public interface IMMService
7: {
8: [OperationContract]
9: void Max();
10:
11: [OperationContract]
12: void Min();
13: }
14: }
第四步,为服务添加一个Host,并且在实用实例启动时,把服务打开。
1: private ServiceHost _serviceHost;
2: protected override void OnStartup(StartupEventArgs e)
3: {
4: base.OnStartup(e);
5: // ...
6: if (isNewMutex)
7: {
8: //1st instance of current application
9: _serviceHost = new ServiceHost(typeof(MMService), new Uri("net.pipe://localhost"));
10: _serviceHost.AddServiceEndpoint(typeof(IMMService), new NetNamedPipeBinding(), "MMService");
11: _serviceHost.Open();
12: }
13: else
14: {
15: // Not the 1st instance
16: // ...
17: }
18: }
关注一下第10行,其实就是定义了:C – IMMService, B – NetNamePipeBinding 和 A – net.pipe://localhost/MMService
地址其实是分成两部分定义的,一部分是baseAddress,是host的地址;另一部分是服务地址:MMService。
这样,服务器端就暴露了一个叫MMService的EndPoint。
客户端代码写在不是第一个实例中:
第一步:根据ABC,创建一个ChannelFactory对象:
ChannelFactory<IMMService> channelFactory = new ChannelFactory<IMMService>(new NetNamedPipeBinding(), "net.pipe://localhost/MMService");
这行代码中,Binding和Address显而易见了,Contract在哪儿?
第二步:通过CreateChannel方法,创建一个在客户端的代理类,并且调用Max/Min的方法:
1: var proxy = channelFactory.CreateChannel();
2: proxy.Max();
这样,我们就可以F5来调试当前的应用程序了。大家会发现,如果保持现在的代码,第二次调用当前应用程序实例时,第一个实例会被最大化。
这跟我们预想的效果还有一点点的差距……但不难解决,限于篇幅,就不再赘述了。
资源:
残缺版源代码(无论点击最大化还是最小化,窗口都最大化):
改进版二进制文件:
(改进版中仍然有一个明显的bug哦)
Little knowledge is dangerous.