Evil 域

当Evil遇上先知

导航

实战 WPF Jump List 编程

Posted on 2011-09-22 06:45  Saar  阅读(1993)  评论(7编辑  收藏  举报

image

(图片来自Bing搜索)

Windows 7提供了JumpList,可以通过右击任务栏上的图标,跳出快捷菜单,为用户操作应用程序提供一个捷径。例如,Live Messenger为我们提供了注销和退出的功能:

image

WPF为Jump List提供了支持。

 

我们来实现如下一个Jump List:

  1. 这个JumpList有三个分组,分别是:Function,3rd Party Applications和Window Control。
  2. Function里提供一个Hello的项,弹出一个Hello JumpList对话框;
  3. 3rd Party Appliation里,我们可以直接调用计算器;
  4. Window Control里提供两个功能,另外是最大化和最小化窗口。

如果没有接触过Jump List的编程,现在,请花2分钟时间大概构想一下实现方法吧……(2分钟过去了)

来看看结果是不是跟你想像的一样。

 

基础

明白JumpList的结构

这个不多说,请大家看图:

image

我们将结合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的对勾

image

  • F5调试
  • 右击应用任务栏上的图标,是不是已经看到对应的JumpList项了?

image

  • 点击调用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:          }

请大家按照这个思路编写代码,看看执行效果:

image

 

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。

image

 

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中,包括了最大化、最小化。当用户点击它们的时候,肯定不会期望应用程序新建一个实例,并且最大化、最小化新实例。

image

要实现这样的功能,需要涉及到两个常用的WPF编程技巧,分别是:

  • 单一应用程序实例
  • 进程间交互

 

提高

image

(图片来自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复制出来即可。

image

接下来,我们在OnStartup中,创建信号量,并且在OnExit中,销毁它。

 

image

主要代码如下:

   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中,进程之间的资源是相互独立的。要让进程之间进行通讯,必须付出努力Smile

以前,在.NET中,我们通过Remoting来进行进程间交互。(我们可以通过维基百科来查查Remoting的底细Smile。)

现在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。

image

   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在哪儿?Smile

 

第二步:通过CreateChannel方法,创建一个在客户端的代理类,并且调用Max/Min的方法:

   1:                  var proxy = channelFactory.CreateChannel();
   2:                  proxy.Max();
 
这样,我们就可以F5来调试当前的应用程序了。大家会发现,如果保持现在的代码,第二次调用当前应用程序实例时,第一个实例会被最大化。
 
这跟我们预想的效果还有一点点的差距……但不难解决,限于篇幅,就不再赘述了。

 

资源:

残缺版源代码(无论点击最大化还是最小化,窗口都最大化):

改进版二进制文件:

(改进版中仍然有一个明显的bug哦)