Introduction
The main goal of this project is to create a Windows service in C# which will monitor all appearing Windows and kill unwanted ones.
This sample project is an illustration for two topics:
- Use of PInvoke, and
- Writing a Windows service in C#
Background
Some time ago, I had two phone interviews and one on-site interview with Google. That was fun (you know those crazy puzzles they like to ask at Google interviews!) and I enjoyed that, but when I was denied without any reasonable explanation, I became so mad that I decided to write this service to kill any Internet browser on my PC as soon as user dialed google.com in the address bar. Now I think this project could be useful for those who want to automatically kill some unwanted popups on their computers. For example, this approach could be used to kill some Windows ToolTips after some timeout because they can stay on the desktop forever if there is no user activity.
Using the Code
There are two stages if you want to kill some window:
- Find a handle to this window, and
- Close this window
It looks like to find a window there is an API call FindWindowEx
and that was my first approach, but very soon I figured out that it doesn't work well for all kind of windows, for example, it doesn't work for tooltip popups because their title is their content. So, I've tried another approach - using another API call EnumWindows
. This API call requires two parameters - pointer to callback function and some structure - to be passed. In the code snippet below, we use member m_hwndFound
to return the result of a search:
CallBackPtr callBackPtr = new CallBackPtr(ReportWindow);
m_hwndFound = IntPtr.Zero;
EnumWindows(callBackPtr, new PopupWinow(classToKill, titleToKill));
if (m_hwndFound != IntPtr.Zero)
{... do something if window is found ...}
This code uses class PopupWindow
...
[StructLayout(LayoutKind.Sequential, Pack = 1, CharSet = CharSet.Unicode)]
public class PopupWinow
{
public string wClass = "tooltips_class32";
public string wTitle = "";
public PopupWinow(string wclass, string wtitle)
{
wClass = wclass;
wTitle = wtitle;
}
}
... and callback method ReportWindow
which must return true
to continue enumeration and false
to stop it if a window is found:
private IntPtr m_hwndFound = IntPtr.Zero;
private bool ReportWindow(IntPtr hwnd, PopupWinow lparam)
{
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
string classToKill = lparam.wClass;
string titleToKill = lparam.wTitle;
StringBuilder sbTitle = new StringBuilder(1024, 1024);
IntPtr nbytes = SendMessage(hwnd, 0x000D, new IntPtr(sbTitle.Capacity), sbTitle);
string wTitle = sbTitle.ToString();
StringBuilder sbClass = new StringBuilder(100);
int res = GetClassName(hwnd, sbClass, sbClass.Capacity);
if (res == 0) return true;
string wClass = sbClass.ToString();
if ((wTitle == "" && titleToKill == "") || (wTitle != "" && wClass == classToKill))
{
if (wTitle.IndexOf(titleToKill) != -1)
{
m_hwndFound = hwnd;
return false;
}
}
return true;
}
After the window handle is found we can close it. Note that for different classes of Windows, we have to use different "close window" calls:
switch (popupType)
{
case PopupWindowType.Tooltip:
nbytes = SendMessage(m_hwndFound, 0x0201, 0, IntPtr.Zero);
return;
case PopupWindowType.Edit:
case PopupWindowType.IEFrame:
case PopupWindowType.MozillaUIWindowClass:
case PopupWindowType.MessageBox:
nbytes = SendMessage(m_hwndFound, 0x0010, 0, IntPtr.Zero);
return;
default:
return;
}
If you want to kill some Windows other than default for this solution just change call to KillPopupWindow
:
m_windowFinder.KillPopupWindow(<some word from window title>, <some PopupWindowType>);
To find out a window title and class, you can use Microsoft Spy++ utility which comes with SDK.
To install this service, use regular InstallUtil.exe utility which is a part of the .NET Framework.
Service Startup Parameters
Recently I used this service in my practice. I opened some website in one of the tabs in Internet Explorer and was faced with a lot of error message boxes with 'Yes' / 'No' choices. I was tired of clicking on the 'No' button and did not want to kill Internet Explorer because I did not want to lose many other opened tabs there. So, I decided to use this service to kill all those error Windows automatically. The trick is that it's not possible to close Yes/No Windows just sending them WM_CLOSE
message. To close them, we need to find their 'Yes' or 'No' child button window and send it WM_LBUTTONUP
message. Here is how to do that:
switch (popupType)
{
...
case PopupWindowType.MessageBox:
if (m_buttonToClick == "")
{
nbytes = SendMessage(m_hwndFound, 0x0010, 0, IntPtr.Zero);
}
else
{
IntPtr button = FindWindowEx(m_hwndFound, IntPtr.Zero, "Button", m_buttonToClick);
if (button != IntPtr.Zero)
{
nbytes = SendMessage(button, 0x0201, 0, IntPtr.Zero);
nbytes = SendMessage(button, 0x0202, 0, IntPtr.Zero);
}
}
return;
...
}
I also modified the OnStart
method to parse parameters to make it possible to start the service to kill different types of Windows. For details, see the downloaded solution. Here is the example of service startup parameters if you want to kill popups with title "Error" and buttons "Yes" and "No" (500 is polling interval in ms):
Error #32770 500 &No
Make Service Self-Installable
For me, it's very painful to install/uninstall services using InstallUtil
application. To avoid this, we can use another set of API functions to control Service Control Manager. I added a special class ServiceInstaller
to the solution to manage that. This class is a helpful wrapper for API calls to advapi32.dll to perform install, uninstall, start and stop service.
Here is the most complicated method InstallService
:
public static bool InstallService(string svcPath, string svcName,
string svcDispName, string svcDescription, bool bRun)
{
IntPtr hSCM = OpenSCManager(
null,
null,
(uint)Win32Methods.SCM_ACCESS_TYPE.SC_MANAGER_CREATE_SERVICE
);
if (hSCM == IntPtr.Zero) return false;
IntPtr hService = CreateService(
hSCM,
svcName,
svcDispName,
Win32Methods.SERVICE_ALL_ACCESS,
(uint)Win32Methods.SERVICE_TYPE.SERVICE_WIN32_OWN_PROCESS,
(uint)Win32Methods.SERVICE_START_TYPE.SERVICE_AUTO_START,
(uint)Win32Methods.SERVICE_ERROR_CODE.SERVICE_ERROR_NORMAL,
svcPath,
null,
IntPtr.Zero,
null,
null,
null
);
if (hService == IntPtr.Zero)
{
CloseServiceHandle(hSCM);
return false;
}
if (!ChangeServiceConfig2(
hService,
SERVICE_INFO_LEVEL.SERVICE_CONFIG_DESCRIPTION,
ref svcDescription
))
{
CloseServiceHandle(hSCM);
return false;
}
CloseServiceHandle(hSCM);
OperatingSystem os = Environment.OSVersion;
if (os.Version.Major < 6)
{
System.ServiceProcess.ServiceController serviceController =
new System.ServiceProcess.ServiceController();
serviceController.ServiceName = svcName;
ConnectionOptions coOptions = new ConnectionOptions();
coOptions.Impersonation = ImpersonationLevel.Impersonate;
ManagementScope mgmtScope =
new System.Management.ManagementScope(@"root\CIMV2", coOptions);
mgmtScope.Connect();
ManagementObject wmiService = new ManagementObject
("Win32_Service.Name='" + svcName + "'");
ManagementBaseObject InParam = wmiService.GetMethodParameters("Change");
InParam["DesktopInteract"] = true;
ManagementBaseObject OutParam = wmiService.InvokeMethod("Change", InParam, null);
serviceController.Start();
}
if (bRun) return RunService(svcName);
return true;
}
This code uses calls to advapi32.dll functions implemented in Win32Methods
class (see downloaded solution for details).
Now you can install/uninstall this service calling it from the command line like this...
WindowKiller -a
... where the parameter -a
means "auto" install/uninstall depending on the current service status. To get help on other command line parameters, you can call the service with -h
parameter.