一、前言
.NET框架是Windows应用领域中一个非常新的技术,可以肯定在未来的一段时间内,.NET应用必须与现存的Windows技术交互作用。这种交互作用主要体现在两个领域:COM和应用编程接口(API)。为此,.NET框架在Windows API之上提供了一个OO层,但是有时候可能需要使用一个.NET不可到达的API调用。在这种情况下,可以使用.NET平台调用(P/Invoke)机制从.NET中调用C或C++函数。因为Windows API函数在DLL中,所以,P/Invoke为从.NET代码调用DLL中的C或C++函数提供了一种通用机制。
本文针对C#.NET中没有提供直接的类似SystemMenu的属性或类似GetSystemMenu的成员函数的实际,编写了一个C#类SystemMenu,从而实现了传统的对于系统菜单的操作,这是通过调用本地Windows API来完成的。
二、系统菜单简介
当你单击窗口图标或右击窗口标题栏时系统菜单即弹出。它包含当前窗口的默认行为。不同窗口的系统菜单看起来有些不同,如一个正常的窗口的系统菜单看起来与一个工具栏子对话框窗口的菜单就不一样。
修改系统菜单的好处:
·添加应用程序自己定义的菜单项。
·在WW被最小化时,SS是一个很好的地方来放置动作,可以被存取,因为SS可以显示,通过在任务栏窗口图标上单击右键。
·使某菜单项失去能力,如从系统菜单中移去“最大化”,“最小化”“关闭”等。由于这种改动还影响到窗口右上角的三个按钮,所以这是一个使窗口右上角“X”失去能力的不错的办法。
操纵系统菜单
通过调用 API函数GetSystemMenu,你就检索到了系统菜单的一个拷贝。该函数的第二个参数指明是否你要复位系统菜单到它的缺省状态。再加上另外几个API菜单函数如AppendMenu, InsertMenu等,你就能实现对于系统菜单的灵活控制。
下面我仅简单介绍如何添加菜单项以及如何实现新项与用户的交互。
三、SystemMenu 类介绍
SystemMenu类的实现使得整个系统菜单存取容易许多。你可以使用这个类来修改一个窗口的菜单。 通过调用静态成员函数FromForm你得到一个对象,该函数要求一个Form对象或一个从Form继承的类作为它的参数。然后它创建一个新的对象,当然如果GetSystemMenu API调用失败的话,将引发一个NoSystemMenuException例外。
注意,每一个Windows API菜单函数要求一个菜单句柄以利于操作。因为菜单句柄实际上是一个C++指针,所以在.NET中你要使用IntPtr来操作它。许多函数还需要一个位掩码标志来指明新菜单项的动作或形式。幸运的是,你不必象在VC++中那样,通过某个头文件的包含来使用一系列的位掩码标志定义,.NET中已经提供了一个现成的公共枚举类ItemFlags。下面对这个类的几个重要成员作一说明:
·mfString―― 告诉子系统将显示由菜单项中的“Item”参数传递的字符串。
·mfSeparator――此时 "ID" 与 "Item" 参数被忽略。
·MfBarBreak―― 当用于菜单条时,其功能与mfBreak一样;当用于下拉菜单,子菜单或快捷菜单时,新的一列与旧有的一列由一线垂直线所隔开。
·MfBreak――把当前项目放在一个新行(菜单条)或新的一列(下拉菜单,子菜单或快捷菜单)。
注意:如果指定多个标志,应该用位操作运算符|(或)连接。例如:
//将创建一个菜单项 "Test" ,且该项被选中(checked)
mySystemMenu.AppendMenu(myID, "Test", ItemFlags.mfString|ItemFlags.mfChecked); |
“Item”参数指定了新项中要显示的文本,其ID必须是唯一的数字――用来标志该菜单项。
注意:确保新项的ID大于0小于0XF000。因为大于等于0XF000的范围为系统命令所保留使用。你也可以调用类SystemMenu的静态方法VerifyItemID来核验是否你的ID正确。
另外,还有两个需要解释的常量:mfByCommand和mfByPosition。
第一,在缺省情况下,使用mfByCommand。第二,“Pos”的解释依赖于这些标志:如果你指定mfByCommand,“Pos”参数就是在新项目插入前项目的ID;如果你指定mfByPosition,“Pos”参数就是以0索引为开头的新项的相对位置;如果是-1并且指定mfByPosition,该项目将被插入到最后。这也正是为什么AppendMenu()可以为InsertMenu()所取代的原因。
四、SystemMenu 类代码分析
using System; using System.Windows.Forms; using System.Diagnostics; using System.Runtime.InteropServices; public class NoSystemMenuException : System.Exception //这些值来自于MSDN public enum ItemFlags mfUnchecked = 0x00000000, // ... is not checked // menu handle of the popup mfBarBreak = 0x00000020, // ... is a bar break // ID parameters are ignored). public enum WindowMessages // public class SystemMenu [DllImport("USER32", EntryPoint="GetSystemMenu", SetLastError=true, // 还需要AppendMenu()。 既然 .NET 使用Unicode, [DllImport("USER32", EntryPoint="AppendMenuW", SetLastError=true, //还可能需要InsertMenu() [DllImport("USER32", EntryPoint="InsertMenuW", SetLastError=true, private IntPtr m_SysMenu = IntPtr.Zero; // 系统菜单句柄 public SystemMenu( ) // 在给定的位置(以0为索引开始值)插入一个分隔条 public bool InsertSeparator ( int Pos ) // 简化的InsertMenu(),前提――Pos参数是一个0开头的相对索引位置 public bool InsertMenu ( int Pos, int ID, String Item ) // 在给定位置插入一个菜单项。具体插入的位置取决于Flags public bool InsertMenu ( int Pos, ItemFlags Flags, int ID, String Item ) // 添加一个分隔条 public bool AppendSeparator ( ) // 使用ItemFlags.mfString 作为缺省值 public bool AppendMenu ( int ID, String Item ) // 被取代的函数 public bool AppendMenu ( int ID, String Item, ItemFlags Flags ) //从一个Form对象检索一个新对象 public static SystemMenu FromForm ( Form Frm ) if ( cSysMenu.m_SysMenu == IntPtr.Zero ) // 当前窗口菜单还原 public static void ResetSystemMenu ( Form Frm ) // 检查是否一个给定的ID在系统菜单ID范围之内 public static bool VerifyItemID ( int ID ) |
你可以使用静态方法ResetSystemMenu把窗口的系统菜单设置为原来状态――这在应用程序遇到错误或没有正确修改菜单时是很有用的。
五、使用SystemMenu类
// SystemMenu 对象
private SystemMenu m_SystemMenu = null; // ID 常数定义 private const int m_AboutID = 0x100; private void frmMain_Load(object sender, System.EventArgs e) m_SystemMenu.AppendSeparator(); m_SystemMenu.InsertSeparator(0); |
六、检测自定义的菜单项是否被点击
这是较难实现的部分。因为你必须重载你的从Form或Control继承类的WndProc成员函数。你可以这样实现:
protected override void WndProc ( ref Message msg ) { base.WndProc(ref msg); } |
注意,必须调用基类的WndProc实现;否则,不能正常工作。
现在,我们来分析一下如何重载WndProc。首先应该截获WM_SYSCOMMAND消息。当用户点击系统菜单的某一项或者选择“最大化”按钮,“最小化”按钮或者“关闭”按钮时,我们要检索该消息。特别注意,消息对象的WParam参数正好包含了被点击菜单项的ID。于是我们可以实现如下重载:
protected override void WndProc ( ref Message msg ) { // 通过截取WM_SYSCOMMAND消息并进行处理 // 注意,消息WM_SYSCOMMAND被定义在WindowMessages枚举类中 // 消息的WParam参数包含点击的项的ID // 该值与通过上面类的InsertMenu()或AppendMenu()成员函数传递的一样 if ( msg.Msg == (int)WindowMessages.wmSysCommand ) base.WndProc(ref msg); |
七、总结
实现上述目标的另一个可能的方法是,通过创建一个事件OnSysCommand并当消息WM_SYSCOMMAND传来时激活它,然后把属性WParam传递给该事件的句柄。读者可以自行编程验证。
总之,本文通过一个简单的系统菜单修改例子,分析了用C#使用.NET平台调用机制来调用DLL中的非托管函数的基本步骤及注意事项。另,所附源程在Windows2000 Server/ VS .NET2003下调试通过。