虽然已经有类似的用C++写的程序,但是本文讲述的是用C#来实现这些功能,并且本文所讲的方案在查找窗口上的方法要比更快一些。
这是一个崭新的话题,在Internet上我们还可以看到许多类似的程序。但是我也还是要借这个机会来讲述一些下面的技术在C#中如何实现:
■系统托盘
■程序切换
■计时控件
■查找窗口
■系统热键
生成一个系统托盘程序
首先,产生一个新的C# Windows Form程序,将NotifyIcon控件从工具箱中拖到窗体中,为了保证系统托盘的图标和应用程序的图标一致,我们用一个共同的图标文件a.ico来设置系统托盘的图标和应用程序的图标。
为了使程序不显示在工具栏上,我们可以设置窗体的visible属性为false。这个可以在窗体属性窗口中直接实现:
this.ShowInTaskbar = false;
到目前为止,系统托盘已基本好了,但是我们还没有设置右键菜单,也没有使程序显示和隐藏的功能。
程序切换
首先,程序的主窗体可以根据不同的状态来选择显示或者是隐藏,除此之外,我们可以用WindowState设置窗体的状态:
public void HideApp() { this.WindowState = FormWindowState.Minimized; Hide(); } public void ShowApp() { Show(); this.WindowState = FormWindowState.Normal; } |
一个非常有趣的功能是让用户关闭窗体的时候程序并不是退出,为了实现这个功能,我们必须要重写窗体的OnClosing事件。
protected override void OnClosing(CancelEventArgs e) { // 用最小化来代替关闭操作d e.Cancel = true; // 最小化,并且隐藏窗体 this.WindowState = FormWindowState.Minimized; Hide(); } |
当然,我们必须要提供一个必须的退出方法。这个可以在托盘的右键菜单的exit中实现:
private void menu_App_Exit(object sender, System.EventArgs e) { NativeWIN32.UnregisterHotKey(Handle, 100); //隐藏托盘 notifyIcon1.Visible = false; Application.Exit(); } |
添加右键菜单
添加一个右键菜单和添加托盘基本一样,从工具箱中添加context menu就可以。右键菜单在你鼠标右键按下的时候是会自动弹出的。
当设置好右键菜单以后,我们必要要根据不同的情况来启用或停用右键菜单,这个可以通过在菜单的BeforePopup设置.Enabled属性来实现。
private void menu_App_BeforePopup(object sender, System.EventArgs e) { if ( this.WindowState == FormWindowState.Minimized ) { App_Show.Enabled = true; App_Hide.Enabled = false; } else { App_Show.Enabled = false; App_Hide.Enabled = true; } } |
计时工具
.Net Framework的Timer能和系统的Win32 timer实现一样的功能,我们要做的就是设置一个timer,然后合理的设置属性。
m_Timer = new System.Timers.Timer(); // explicit namespace (Timer also in System.Threading) m_Timer.Elapsed += new ElapsedEventHandler(OnTimerKillPopup); m_Timer.Interval = m_nInterval; // for instance 3000 milliseconds m_Timer.Enabled = true; // start timer protected void OnTimerKillPopup(Object source, ElapsedEventArgs e) { m_Timer.Enabled = false; // pause the timer FindPopupToKill(); m_Timer.Enabled = true; } |
本地win32窗体查找
本程序的实现原理是这样,先检查所有的IE窗口标题,然后于已经有的列表来比较,如果有相同的,我们就关闭这个窗口。
按照上面的方法,我们每n妙使用KillPopup()来检查。比较遗憾的是我们无法使用安全代码来完成所有的工作。我们可以使用System.Diagnostics.Proces来检查所有的IE进程,然后得到主窗体。但是每一个IE进程可以打开好几个窗口,虽然每一个窗体都于一个进程相关,但是还没有办法来使每一个窗体于进程对应起来。
一个可行的办法使用System.Diagnostics.Process列举出所有的运行的进程,然后System.Diagnostics.ProcessThreadCollection来得到他们的.Threads属性,为了得到thread Id,我们使用Win32 API EnumThreadWindows(DWORD threadId,WNDENUMPROC lpfn,LPARAM lParam) 来实现,这是一个回调(call back)函数,他可以列举出于进程相关的窗体。当我们得到了窗体的句柄以后,我们可以使用另一个API函数GetWindowText(HWND hwnd,/*out*/LPTSTR lpString,int nMaxCount)来得到窗体的标题,然后根据已经有的窗体,调用API函数SendMessage(HWND hWnd,int msg,int wParam,int lParam)来关闭窗口。
下面是演示代码:
Process[] myProcesses = Process.GetProcessesByName("IEXPLORE"); foreach(Process myProcess in myProcesses) { FindPopupToKill(myProcess); } protected void FindPopupToKill(Process p) { // traverse all threads and enum all windows attached to the thread foreach (ProcessThread t in p.Threads) { int threadId = t.Id; NativeWIN32.EnumThreadProc callbackProc = new NativeWIN32.EnumThreadProc(MyEnumThreadWindowsProc); NativeWIN32.EnumThreadWindows(threadId, callbackProc, IntPtr.Zero /*lParam*/); } } // callback used to enumerate Windows attached to one of the threads bool MyEnumThreadWindowsProc(IntPtr hwnd, IntPtr lParam) { public const int WM_SYSCOMMAND = 0x0112; public const int SC_CLOSE = 0xF060; // get window caption NativeWIN32.STRINGBUFFER sLimitedLengthWindowTitle; NativeWIN32.GetWindowText(hwnd, out sLimitedLengthWindowTitle, 256); String sWindowTitle = sLimitedLengthWindowTitle.szText; if (sWindowTitle.Length==0) return true; // find this caption in the list of banned captions foreach (ListViewItem item in listView1.Items) { if ( sWindowTitle.StartsWith(item.Text) ) NativeWIN32.SendMessage(hwnd, NativeWIN32.WM_SYSCOMMAND, NativeWIN32.SC_CLOSE, IntPtr.Zero); // try soft kill } return true; } public class NativeWIN32 { public delegate bool EnumThreadProc(IntPtr hwnd, IntPtr lParam); [DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern bool EnumThreadWindows(int threadId, EnumThreadProc pfnEnum, IntPtr lParam); // used for an output LPCTSTR parameter on a method call [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)] public struct STRINGBUFFER { [MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public string szText; } [DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int GetWindowText(IntPtr hWnd, out STRINGBUFFER ClassName, int nMaxCount); [DllImport("user32.dll", CharSet=CharSet.Auto)] public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, int lParam); } |
上面的方法在性能上是不错的,因为他过滤了其他非IE的窗口。但是我们可以用一个更简单的方法来实现,就是调用API FindWindowEx(HWND hWndParent,HWND hWndNext,/*in*/LPCTSTR szClassName,/*in*/LPCTSTR szWindowTitle)方法。比较有用的是这句,我们可以使用registered window class name来找到IE窗口(IEFrame是所有打开的IE的标识)。
注册系统热键
系统热键用在像弹出窗口杀手这种应用程序非常有用,Ctrl+Shift+J是缺省热键。
说道实现,我们继续用RegisterHotkey(HWND hWnd, int id, UINT fsModifiers, UINT vkey)完成,代码如下:
一般的,注册热键要以下几步:
/* ------- using HOTKEYs in a C# application -------
-- code snippet by James J Thompson --
在Form的load 中 : Ctrl+Shift+J
bool success = RegisterHotKey(Handle,
100,
KeyModifiers.Control | KeyModifiers.Shift,
Keys.J);
*** 在form的closing中:
UnregisterHotKey(Handle, 100);
如何处理热键:
当我们按下热键以后,流程是这样:首先用HWND GetForegroundWindow()来得到窗体,然后要抓出窗体的标题,GetWindowText(HWND hwnd, /*out*/LPTSTR lpString, int nMaxCount)。
具体如下:
protected void ProcessHotkey()
{
IntPtr hwnd = NativeWIN32.GetForegroundWindow();
if (!hwnd.Equals(IntPtr.Zero))
{
NativeWIN32.STRINGBUFFER sWindowTitle;
NativeWIN32.GetWindowText(hwnd, out sWindowTitle, 256);
if (sWindowTitle.szText.Length>0)
AddWindowTitle( sWindowTitle.szText ); // add to the ListView (Form)
}
}