C#实现自定义消息处理

C#实现自定义消息处理

众所周知,委托和事件机制是C#应用程序的一个很重要的方面。

Microsoft 的 BCL 类库对Windows的控件进行了几乎全面的封装,使应用程序 开发人员甚至不用了解消息循环就能写出相样的程序。

然而,甚至Windows UI编程到了 WPF 时代,消息机制仍然占据着举足轻重的 作用。可以这么说,没有消息循环就没有Windows。(当然WPF很大程度上是对D3D 的封装,它本身并不是基于Win32的消息循环, 但是Micorsoft仍然在WPF中提供了 对Win32消息机制的支持)。

BCL2.0 的System.Windows.Forms 命名空间的控件则完全是对 Win32 控件的 进一步封装,因此C#2.0本身也是基于Win32消息循环机制的,在编写C#桌面应用 时,在一些地方,甚至还必须借助于消息,否则可能无法实现一些功能,或者很 难实现。比如Aplication类提供了对添加 AddMessageFilter 的支持,Control类 放出了几个类似于 ProcessCmdKey 的虚方法,另外,Control类还把 WndProc 实 现为虚拟的,以便控件开发人员能够方便地重写。

废话不多说了,考虑这样一种情况:一个应用程序关联一种文件类型,双击这 样类型的文件,则使用该应用程序打开这个文档。这很容易,实现,程序把文件 (可能是多个)当作命令行参数读取,现在假设这是一个多文档应用程序,如果 应用程序正在运行,那么一般希望用这个已经存在的程序来打开这些文件,而不 是重新启用一个新的程序实例。这在Windows应用中很常见,比如用Visual Studio 打开程序源文件,用IE7打开另外一个html文档(当然这个可以配置是启 用新实例还是用已有实例打开它)等。假设这个软件是用Win32或者VC开发的,那 很容易实现,查找已经存在的实例(进程),如果找到,则进一步查找它的主窗 口,并向其发送消息,传送文件列表,让它去处理,当前例程则结束。主窗口则 要处理这个自定义的消息,但是如果使用的是C#,你可能犯傻了,怎么办呢?

实际上,上述方法在C#应用程序中仍然适用,你仍然可以在C#中自定义消息。 下面演示这一设想。假设最后生成的可执行程序叫 “MyApplication.exe”。

1using System;
2using System.Collections.Generic;
3using System.Diagnostics;
4using System.Runtime.InteropServices;
5using System.IO;
6using System.Reflection;
7using System.Windows.Forms;
8
9namespace MyApplication {
10  internal static class Program {
11    [STAThread]
12    private static void Main(string[] args) {
13      Application.EnableVisualStyles();
14      Application.SetCompatibleTextRenderingDefault (false);
15
16      List<string> fileList = new List<string>();
17      foreach (string filename in args) {
18        if (File.Exists(filename)) {
19          fileList.Add(filename);
20        }
21      }
22
23      if (! SingleInstanceHelper.OpenFilesInPreviousInstance(fileList.ToArray())) {
24        Application.Run(new MyWindow());
25      }
26    }
27  }
28
29  public class MyWindow : Form {
30    protected override void WndProc(ref Message msg) {
31      if (!SingleInstanceHelper.PreFilterMessage(ref msg, this)) {
32        base.WndProc(ref msg);
33      }
34    }
35  }
36
37  internal static class SingleInstanceHelper {
38    private static readonly int CUSTOM_MESSAGE = 0X400 + 2;
39    private static readonly int RESULT_FILES_HANDLED = 2;
40
41    [DllImport("user32.dll")]
42    private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
43
44    [DllImport("user32.dll")]
45    private static extern IntPtr SetForegroundWindow (IntPtr hWnd);
46
47    public static bool OpenFilesInPreviousInstance (string[] fileList) {
48      int currentProcessId = Process.GetCurrentProcess ().Id;
49      string currentFile = Assembly.GetEntryAssembly ().Location;
50      int number = new Random().Next();
51      string fileName = Path.Combine(Path.GetTempPath (), "sd" + number + ".tmp");
52      try {
53        File.WriteAllLines(fileName, fileList);
54        List<IntPtr> alternatives = new List<IntPtr>();
55        foreach (Process p in Process.GetProcessesByName("MyApplication")) {
56          if (p.Id == currentProcessId) continue;
57
58          if (string.Equals(currentFile, p.MainModule.FileName, StringComparison.CurrentCultureIgnoreCase)) {
59            IntPtr hWnd = p.MainWindowHandle;
60            if (hWnd != IntPtr.Zero) {
61              long result = SendMessage(hWnd, CUSTOM_MESSAGE, new IntPtr(number), IntPtr.Zero).ToInt64();
62              if (result == RESULT_FILES_HANDLED) {
63                return true;
64              }
65            }
66          }
67        }
68        return false;
69      } finally {
70        File.Delete(fileName);
71      }
72    }
73
74    internal static bool PreFilterMessage(ref Message m, Form mainForm) {
75      if (m.Msg != CUSTOM_MESSAGE)
76        return false;
77
78      long fileNumber = m.WParam.ToInt64();
79      m.Result = new IntPtr(RESULT_FILES_HANDLED);
80      try {
81        MethodInvoker invoker = delegate {
82          SetForegroundWindow(mainForm.Handle) ;
83        };
84        mainForm.Invoke(invoker);
85        string tempFileName = Path.Combine (Path.GetTempPath(), "sd" + fileNumber + ".tmp");
86        foreach (string file in File.ReadAllLines (tempFileName)) {
87          invoker = delegate {
88            // Open the file
89          };
90          mainForm.Invoke(invoker);
91        }
92      } catch (Exception) {
93        return false;
94      }
95
96      return true;
97    }
98  }
99}

该示例程序通过检查进程只允许应用程序运行一个实例,然后自定义消息,通 过 SendMessage 向程序的另一个实例发送消息,由于牵涉到进程间发送数据,如 果直接发送则必须使用类型的 Marshal, 而Marshal过的自定义类型在C#中是不允 许传递的(除非使用了Hook)。最简单的方式是直接发送一个整形随机数字,使 用这个数字在临时文件夹在建立一个临时文件,写入数据,程序的另一个实例接 收到消息后,读取这个文件获得数据,进行相应的处理,然后临时文件被删除, 从而实现了进程间的数据交换。