Introduction
Couple of days ago, I got an email asked me to help with creating a button on top all open applications, this reminded me of the CodedUI Recorder.
As you can see from the picture, when using CodedUI Testing, you will see the “Currently Recording” notification on every active application you are recording.
How?
The design is to make the title bar window of the target application as the parent\owner window of the Control you want to add.
But this is not enough, we need to listen to many events like size change, style etc.. But I’ll get there later.
Here is what we need to do:
- Find window handle with
FindWindow
- Get window position and title bar info using
GetTitleBarInfo
- Set window as owner\parent using
SetWindowLong
SetWinEventHook
for couple of events of the target application.
Using the Code
Step 1: Create Project
Create a WinForm or WPF project and add the following classes:
Step 2: Get Running Processes
Create new class called “PItem
” and copy this code:
public class PItem
{
public string ProcessName { get; set; }
public string Title { get; set; }
public PItem(string processname, string title)
{
this.ProcessName = processname;
this.Title = title;
}
public override string ToString()
{
if (!string.IsNullOrEmpty(Title))
return string.Format("{0} ({1})", this.ProcessName, this.Title);
else
return string.Format("{0}", this.ProcessName);
}
}
Now, getting all active processes:
Process[] pro_list = e.Result as Process[];
foreach (Process pro in pro_list)
{
try
{
ProcessList.Items.Add(new PItem(pro.ProcessName, pro.MainWindowTitle));
}
catch (Exception)
{
}
}
Step 3: Add Find Native Methods
FindWindow
function retrieves a handle to the top-level window whose class name and window name match the specified string
s. This function does not search child windows. This function does not perform a case-sensitive search. Add below methods to NativeMethods
.
using System.Runtime.InteropServices;
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
[DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)]
internal static extern IntPtr FindWindowByCaption(IntPtr ZeroOnly, string lpWindowName);
Add the below code to Helpers. I’ve created a Find
method to handle both FindWindow
and FindWindowByCaption
(much easier to search that way:-D)
public static IntPtr Find(string ModuleName, string MainWindowTitle)
{
IntPtr WndToFind = NativeMethods.FindWindow(ModuleName, MainWindowTitle);
if (WndToFind.Equals(IntPtr.Zero))
{
if (!string.IsNullOrEmpty(MainWindowTitle))
{
WndToFind = NativeMethods.FindWindowByCaption(WndToFind, MainWindowTitle);
if (WndToFind.Equals(IntPtr.Zero))
return new IntPtr(0);
}
}
return WndToFind;
}
Step 4: Find Window Handle From Process
Using PItem
, we can use the ModuleName
\Process Name and if the window exists, we can also use window title. Calling our Helpers
class and using Find
method with ProcessName
and WindowTitle
.
PItem pro = ProcessList.SelectedItem as PItem;
string ModuleName = pro.ProcessName;
string MainWindowTitle = pro.Title; ;
TargetWnd = Helpers.Find(ModuleName, MainWindowTitle);
if (!TargetWnd.Equals(IntPtr.Zero))
Log(ModuleName + " Window: " + TargetWnd.ToString()); else
Log(ModuleName + " Not found");
Now I can assume that we have the window handle (If not, read Part 1). Now we need to get TitleBarInfo
from the TargetWindow
. Using that data, we can get the position of the window and more.
Step 5: Add GetTitleBarInfo and GetLastError
Using GettitleBarInfo
will allow us to get information from the Target application.
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll")]
internal static extern bool GetTitleBarInfo(IntPtr hwnd, ref TITLEBARINFO pti);
[DllImport("coredll.dll", SetLastError = true)]
internal static extern Int32 GetLastError();
Step 6: Add TitleBarInfo & RECT Properties
Before we can use GetTitleBarInfo
, let’s add the following properties to the NativeMethods
class.
[StructLayout(LayoutKind.Sequential)]
internal struct TITLEBARINFO
{
public const int CCHILDREN_TITLEBAR = 5;
public uint cbSize;
public RECT rcTitleBar;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
public AccessibleStates[] rgstate;
}
[StructLayout(LayoutKind.Sequential)]
internal struct RECT
{
internal int left;
internal int top;
internal int right;
internal int bottom;
}
Step 7: Add GetWindowPosition
Into the Helpers
class, add GetWindowPosition
(below), GetWindowPosition
first will initialize TITLEBARINFO
(Make sure you set the sbSize
) than use GetTitleBarInfo
to get the TitleBar
position on the screen.
public static WinPosition GetWindowPosition(IntPtr wnd)
{
NativeMethods.TITLEBARINFO pti = new NativeMethods.TITLEBARINFO();
pti.cbSize = (uint)Marshal.SizeOf(pti); bool result = NativeMethods.GetTitleBarInfo(wnd, ref pti);
WinPosition winpos;
if (result)
winpos = new WinPosition(pti);
else
winpos = new WinPosition();
return winpos;
}
Now let’s see how to use this information to add our own control on top of another application (See picture).
Step 8: Create HoverControl
The HoverControl
is basically a control. I’ve created a new Windows control called HoverControl
with the following attributes:
WindowStyle="None" AllowsTransparency="True" Background="{Binding Null}"
This is important because we want the window to be transparent and without window borders. Below is the full XAML of the HoverControl
.
<Window x:Class="Win32HooksDemo.HoverControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="HoverControl" Height="10" Width="150"
WindowStyle="None" AllowsTransparency="True" Background="Transparent">
<Grid>
<Rectangle ToolTip="Click To Close" MouseDown="rectangle1_MouseDown"
Name="rectangle1"
Stroke="#FFC7FF00" Fill="Red" RadiusY="10" RadiusX="10" StrokeThickness="2"/>
<TextBlock FontSize="9" FontWeight="Bold" HorizontalAlignment="Center"
Text="This is my Hover Control" />
</Grid>
</Window>
Mouse down event should close the HoverControl
.
Step 9: Add SetWindowLong To NativeMethod Class
The
SetWindowLongPtr
function changes an attribute of the specified window. The function also sets a value at the specified offset in the extra window memory. To write code that is compatible with both 32-bit and 64-bit versions of Windows, also add
SetWindowLongPtr
.
[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
internal static extern int SetWindowLong32
(HandleRef hWnd, int nIndex, int dwNewLong);
[DllImport("user32.dll", EntryPoint = "SetWindowLong")]
internal static extern int SetWindowLong32
(IntPtr windowHandle, Win32HooksDemo.Helpers.GWLParameter nIndex, int dwNewLong);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
internal static extern IntPtr SetWindowLongPtr64
(IntPtr windowHandle, Win32HooksDemo.Helpers.GWLParameter nIndex, IntPtr dwNewLong);
[DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")]
internal static extern IntPtr SetWindowLongPtr64
(HandleRef hWnd, int nIndex, IntPtr dwNewLong);
Step 10: Add SetWindowLong In Helpers Class
The only attribute we need to change in this case is - GWL_HWNDPARENT
, our agenda is to set target window as parent\owner of our HoverControl
.
public enum GWLParameter
{
GWL_EXSTYLE = -20, GWL_HINSTANCE = -6, GWL_HWNDPARENT = -8, GWL_ID = -12, GWL_STYLE = -16, GWL_USERDATA = -21, GWL_WNDPROC = -4 }
Instead of checking if this is 64bit or 32bit from our UI class, add SetWindowLong
inside the helpers class.
public static int SetWindowLong(IntPtr windowHandle, GWLParameter nIndex, int dwNewLong)
{
if (IntPtr.Size == 8) {
return (int)NativeMethods.SetWindowLongPtr64
(windowHandle, nIndex, new IntPtr(dwNewLong));
}
return NativeMethods.SetWindowLong32(windowHandle, nIndex, dwNewLong);
}
Step 11: Add HoverControl On Top Target Application
First, we need to create new instance of our new HoverControl
, set the position based on Target window TitleBar
position (Part 2) and set the HoverControl
as Child of Target window. Add button click event and let’s add the following code:
if (OnTopControl != null)
OnTopControl.Close();
HoverControl OnTopControl = new HoverControl();
OnTopControl.Show();
IntPtr OnTopHandle = Helpers.Find(OnTopControl.Name, OnTopControl.Title);
OnTopControl.Left = left;
OnTopControl.Top = top;
Helpers.SetWindowLong(OnTopHandle, Helpers.GWLParameter.GWL_HWNDPARENT,
TargetWnd.ToInt32());
Log("Hover Control Added!");
And here we are, the last part in this series – How to set Window Event using SetWinEventHook
. the only thing that is left is to listen to Target window events (Example: LocationChange
) so we can move our HoverControl
accordingly. We are going to use some more NativeMethods
to complete this task.
Step 12: Add SetWinEventHook & UnhookWinEvent To NativeMethods Class
We need to use SetWinEventHook
because this function allows clients to specify which processes and threads they are interested in. Clients can call SetWinEventHook
multiple times if they want to register additional hook functions or listen for additional events.
[DllImport("user32.dll")]
internal static extern IntPtr SetWinEventHook(
AccessibleEvents eventMin, AccessibleEvents eventMax, IntPtr eventHookAssemblyHandle, WinEventProc eventHookHandle, uint processId, uint threadId, SetWinEventHookParameter parameterFlags );
Removes an event hook function created by a previous call to.
[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll")]
internal static extern bool UnhookWinEvent(IntPtr eventHookHandle);
WinEventProc - ** Important **
An application-defined callback (or hook) function that the system calls in response to events generated by an accessible object. The hook function processes the event notifications as required. Clients install the hook function and request specific types of event notifications by calling SetWinEventHook
.
internal delegate void WinEventProc(IntPtr winEventHookHandle, AccessibleEvents accEvent,
IntPtr windowHandle, int objectId, int childId, uint eventThreadId,
uint eventTimeInMilliseconds);
[DllImport("user32.dll")]
internal static extern IntPtr SetFocus(IntPtr hWnd);
[Flags]
internal enum SetWinEventHookParameter
{
WINEVENT_INCONTEXT = 4,
WINEVENT_OUTOFCONTEXT = 0,
WINEVENT_SKIPOWNPROCESS = 2,
WINEVENT_SKIPOWNTHREAD = 1
}
Step 13: Create SetControl & GetWindowPosition Methods
What you need to go in this step is just to extract the code from btn_get_pos_Click
& btn_add_Click
methods to external method, this will serve us later.
void SetControl(bool log)
{
if (OnTopControl != null)
OnTopControl.Close();
OnTopControl = new HoverControl();
OnTopControl.Show();
IntPtr OnTopHandle = Helpers.Find(OnTopControl.Name, OnTopControl.Title);
OnTopControl.Left = left;
OnTopControl.Top = top;
if(log)
Log("Hover Control Added!");
Helpers.SetWindowLong(OnTopHandle,
Helpers.GWLParameter.GWL_HWNDPARENT, TargetWnd.ToInt32());
}
void GetWindowPosition(bool log)
{
var pos = Helpers.GetWindowPosition(TargetWnd);
left = pos.Left;
right = pos.Right;
bottom = pos.Bottom;
top = pos.Top;
if(log)
Log(string.Format("Left:{0} , Top:{1} , Top:{2} , Top:{3}",
left, top, right, bottom));
Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
}
Step 14: Add Events And Callbacks
First, we need to define what types of events we want to listen too and than create a dictionary with AccessibleEvents
and Specific CallBack (or generic to all events). You can find more about WinEvents
here. I’ve add event for LocationChanged
and Destroy
, LocationChanged
will help me to find the position on the target window each time the user changes the position and Destroy when the target window is closed and we need to close our HoverControl
.
private Dictionary<AccessibleEvents, NativeMethods.WinEventProc>
InitializeWinEventToHandlerMap()
{
Dictionary<AccessibleEvents, NativeMethods.WinEventProc> dictionary =
new Dictionary<AccessibleEvents, NativeMethods.WinEventProc >();
new NativeMethods.WinEventProc(this.ValueChangedCallback));
dictionary.Add(AccessibleEvents.LocationChange,
new NativeMethods.WinEventProc(this.LocationChangedCallback));
dictionary.Add(AccessibleEvents.Destroy,
new NativeMethods.WinEventProc(this.DestroyCallback));
return dictionary;
}
private void DestroyCallback(IntPtr winEventHookHandle,
AccessibleEvents accEvent, IntPtr windowHandle, int objectId,
int childId, uint eventThreadId, uint eventTimeInMilliseconds)
{
if (accEvent == AccessibleEvents.Destroy && windowHandle.ToInt32() ==
TargetWnd.ToInt32())
{
ThreadPool.QueueUserWorkItem(new WaitCallback(this.DestroyHelper));
}
}
private void DestroyHelper(object state)
{
Execute ex = delegate()
{
NativeMethods.UnhookWinEvent(g_hook);
OnTopControl.Close();
};
this.Dispatcher.Invoke(ex, null);
}
private void LocationChangedCallback(IntPtr winEventHookHandle,
AccessibleEvents accEvent, IntPtr windowHandle, int objectId,
int childId, uint eventThreadId, uint eventTimeInMilliseconds)
{
if (accEvent == AccessibleEvents.LocationChange && windowHandle.ToInt32() ==
TargetWnd.ToInt32())
{
ThreadPool.QueueUserWorkItem(new WaitCallback(this.LocationChangedHelper));
}
}
private void LocationChangedHelper(object state)
{
Execute ex = delegate()
{
if(OnTopControl!=null)
OnTopControl.Close();
GetWindowPosition(false);
SetControl(false);
};
this.Dispatcher.Invoke(ex, null);
}
Step 15: Set WinEventHook
This is the last step and the most important one. You can set as many events as you like but make sure to use GCHandle
garbage collector will not move the callback and you will get many errors.
IntPtr g_hook;
private void btn_set_event_Click(object sender, RoutedEventArgs e)
{
Dictionary<AccessibleEvents, NativeMethods.WinEventProc> <accessibleevents>events =
InitializeWinEventToHandlerMap();
NativeMethods.WinEventProc eventHandler =
new NativeMethods.WinEventProc(events[AccessibleEvents.LocationChange].Invoke);
GCHandle gch = GCHandle.Alloc(eventHandler);
g_hook = NativeMethods.SetWinEventHook(AccessibleEvents.LocationChange,
AccessibleEvents.LocationChange, IntPtr.Zero, eventHandler
, 0, 0, NativeMethods.SetWinEventHookParameter.WINEVENT_OUTOFCONTEXT);
eventHandler = new NativeMethods.WinEventProc
(events[AccessibleEvents.Destroy].Invoke);
gch = GCHandle.Alloc(eventHandler);
g_hook = NativeMethods.SetWinEventHook(AccessibleEvents.Destroy,
AccessibleEvents.Destroy, IntPtr.Zero, eventHandler
, 0, 0, NativeMethods.SetWinEventHookParameter.WINEVENT_OUTOFCONTEXT);
}