Subclassing NETCF Applications

Some time ago I handled a request coming from a developer that wanted to intercept Windows Messages sent to another application, in other terms "subclass" the other application's main window. On Desktop's Win32 you can do that by setting a hook through SetWindowsHookEx (which exists on Windows CE but it's a private API), but due to the specific nature of the Win32 implementation under Windows CE before release 6.0, a process can access the memory space of another one: this has been done in the past due to the very tight limitations about Virtual Memory imposed on a Windows CE platform (32 processes, each of them 32MB of virtual space). You might read this great article written by Doug Boling some years ago, but still valid for Windows Mobile as long as it's based on platforms before Windows Embedded CE 6.0 (the version of the same article for Windows CE 6.0 is here).

Therefore on Windows Mobile 5.0 and 6 (both based on Windows CE 5.0 despite of the name! Smile) GetWindowLong() API with GWL_WNDPROC flag works even if you call it for a different process (contrarily to a real Win32-based OS, like desktops and Windows CE 6.0). This allows us to intercept messages in this way, in a managed application:

 IntPtr hwnd = IntPtr.Zero;
hwnd = FindWindow(strClassName, strWindowName);
newWndProc = new WndProcDelegate (NewWndProc);
oldWndProc = GetWindowLong(hwnd, GWL_WNDPROC);
int success = SetWindowLong(hwnd, GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(newWndProc));

being for example, in case you want to intercept WM_ACTIVATE:

 public IntPtr NewWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
{
    if (msg == WM_ACTIVATE)
    {
        MessageBox.Show("hook!");
    }
    return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam);
}

 

This approach is based on the assumption that your application knows target's window's Class Name and Window Name, but this might not be always true! Moreover, it works with NATIVE applications, because native applications have a predefined and set ClassName and WindowName, so FindWindow() API can easily return the window handle of the process you want to subclass. This is not true with NETCF applications: the 1st managed launched application has "#NETCF_AGL_BASE_" as ClassName, the second one an empty string; there are some other patterns as well, and they are UNDOCUMENTED, thus meaning that they can change for future releases, precisely as they did from v1 to v2 (see Daniel Moth's post about this).

So, in case you want to subclass NETCF applications and you only know the executable's name, a possible approach is to enumerate active processes to get the ID of the one you’re interested on, so that you can use System.Diagnostic.Process.MainWindowHandle property to retrieve the window handle you want to subclass. Unfortunately NETCF's System.Diagnostic.Process class doesn’t implement all the methods that would have helped, for example .GetProcesses(): however, you can mix up a “Process” class exposing a .GetProcesses() method with the System.Diagnostic.Process to exploit its .MainWindowHandle. Such custom class is provided as a sample by the following MSDN article: Creating a Microsoft .NET Compact Framework-based Process Manager Application (and I imagine it was the internal implementation of SDF v1.4's OpenNetcf.Diagnostics.Process).

 

The result is the sample code below, as usual provided "AS IS" (this is not "production-code", it's meant to have didactic\testing purposes):

 using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
using System.Reflection;
using System.IO;
using System.Diagnostics;

namespace SubclassDemo
{
    public partial class Form1 : Form
    {
        private static IntPtr oldWndProc = IntPtr.Zero;
        private static WndProcDelegate newWndProc;
        private static IntPtr hWndTarget = IntPtr.Zero;
        private string strClassName = string.Empty;
        private string strWindowName = string.Empty;

        public Form1()
        {
            InitializeComponent();

            ////txtTargetExeName is a TextBox where user enters target application's name
            //LaunchSecondApp(txtTargetExeName.Text);

            //make sure that our application has the focus
            SetForegroundWindow(this.Handle);
        }

        ////assumption: target application is under the same path of running application
        //private void LaunchSecondApp(string strExeName)
        //{
        //    string strAppPath = Assembly.GetExecutingAssembly().GetModules()[0].FullyQualifiedName;
        //    strAppPath = Path.GetDirectoryName(strAppPath);
        //    strAppPath = strAppPath.Trim();
        //    if (!strAppPath.EndsWith("\\")) 
        //        strAppPath += "\\";
        //    Process.Start(strAppPath + strExeName, string.Empty);
        //}

        // SET if you know strClassName and strWindowName of the window you want to subclass
        private void btnSetHookKnowingNames_Click(object sender, EventArgs e)
        {
            hWndTarget = FindWindow(strClassName, strWindowName);
            newWndProc = new WndProcDelegate(NewWndProc);
            oldWndProc = GetWindowLong(hWndTarget, GWL_WNDPROC);
            int success = SetWindowLong(hWndTarget, GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(newWndProc));
        }

        //RESET (before closing) if you know strClassName and strWindowName of the window you want to subclass
        private void btnRemoveHookKnowingNames_Click(object sender, EventArgs e)
        {
            int success = SetWindowLong(hWndTarget, GWL_WNDPROC, oldWndProc);
        }
        
        //SUBCLASSING PROCEDURE
        public IntPtr NewWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
        {
            //defined WM_RAFFAEL for testing purposes
            if (msg == WM_RAFFAEL)
            {
                MessageBox.Show("hook!");
                return IntPtr.Zero;
            }
            return CallWindowProc(oldWndProc, hWnd, msg, wParam, lParam);
        }

        //TEST
        private void btnTest_Click(object sender, EventArgs e)
        {
            SendMessage(hWndTarget, WM_RAFFAEL, 0, 0);
        }

        //private string strTargetAppExeName = "TestMANAGED".ToLower();

        //SET if you don't know strClassName and strWindowName: this is true for managed applications
        private void btnManagedSet_Click(object sender, EventArgs e)
        {
            //You know the process's executable name (and path, if required)
            //of the 2nd managed application. --> strTargetAppExeName

            ////Now:
            ////Enumerate all the processes, in order to get their ProcessID
            ////For each ProcessID, check if its executable is the one we're interested on
            ////Stop when we find the right Process ID
            ////For example something similar to:
            //IntPtr pID = GetProcessIDOfTheManagedApp(strTargetAppExeName);

            ////Enumerate all the windows and for each window use
            ////GetWindowThreadProcessID to see if it's associated to the
            ////ProcessID we found
            ////stop when finding a window associated to that process
            ////then use GetWindowLong(PARENT) to verify it's the main window of that process
            ////For example something similar to:
            //hWndTarget = GetMainWindowOfTheManagedApp(pID);

            ////BUT...
            //Utilities.Process class exposes .GetProcesses()
            //System.Diagnostic.Process exposes .MainWindowHandle

            Utilities.Process[] processes = Utilities.Process.GetProcesses();
            foreach (Utilities.Process p in processes)
            {
                //string name = p.ToString().ToLower();
                //strTargetAppExeName = strTargetAppExeName.ToLower();
                //if (p.ToString().ToLower() == strTargetAppExeName)

                //since we're comparing 2 strings, both potentially manually entered by user
                //use .ToLower() to modify the capital letters
                //use .TrimEnd(".exe".ToCharArray() to remove, if any, the ".exe" from the application name
                //this is also because in some cases managed applications are reported as without .exe at the end in the name
                if (p.ToString().ToLower().TrimEnd(".exe".ToCharArray()) == txtTargetExeName.Text.ToLower().TrimEnd(".exe".ToCharArray()))
                {
                    System.Diagnostics.Process pp = System.Diagnostics.Process.GetProcessById((int)p.Handle); //Handle and PID are the same in WINCE
                    hWndTarget = pp.MainWindowHandle;
                }
            }
            
            //as per native apps, associate new WndProc
            newWndProc = new WndProcDelegate(NewWndProc);
            oldWndProc = GetWindowLong(hWndTarget, GWL_WNDPROC);
            int success = SetWindowLong(hWndTarget, GWL_WNDPROC, Marshal.GetFunctionPointerForDelegate(newWndProc));
        }


        //private void GetProcessIDOfTheManagedApp(string strTargetAppExeName)
        //{
        //}

        //private void GetMainWindowOfTheManagedApp(IntPtr processID)
        //{
        //}

        //REMOVE the hook if you're closing!
        private void Form1_Closing(object sender, CancelEventArgs e)
        {
            int success = SetWindowLong(hWndTarget, GWL_WNDPROC, oldWndProc);
        }

        #region DllImport
        public const int GWL_WNDPROC = (-4);
        public const int WM_RAFFAEL = 123456789;

        public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);

        [DllImport("coredll", SetLastError = true)]
        public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);

        [DllImport("coredll", SetLastError = true)]
        public static extern int SetWindowLong(IntPtr hWnd, int nIndex, IntPtr newWndProc);

        [DllImport("coredll", SetLastError = true)]
        public static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

        [DllImport("coredll.dll", SetLastError = true)]
        private static extern IntPtr FindWindow(string _ClassName, string _WindowName);

        [DllImport("coredll.dll", SetLastError = true)]
        public static extern int SendMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);

        [DllImport("coredll.dll", SetLastError = true)]
        private static extern bool SetForegroundWindow(IntPtr hWnd);
        #endregion DllImport
    }
}