65.9K
CodeProject 正在变化。 阅读更多。
Home

将您的控件添加到另一个应用程序的顶部

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (76投票s)

2010年5月10日

Ms-PL

5分钟阅读

viewsIcon

130586

downloadIcon

4270

如何使用 Win32 将您的控件添加到另一个应用程序的顶部,使用 Win32 挂钩。

引言

几天前,我收到一封电子邮件,要求我帮助创建一个置顶所有已打开应用程序的按钮,这让我想起了 CodedUI Recorder
从图片中可以看到,在使用 CodedUI 测试时,您会在正在录制的每个活动应用程序上看到“正在录制”通知。

如何做到?

设计思路是将目标应用程序的标题栏窗口设置为要添加的控件的父/所有者窗口。
但这还不够,我们需要监听许多事件,例如大小更改、样式等。但我稍后会讲到。

这是我们需要做的

  1. 使用 FindWindow 查找窗口句柄
  2. 使用 GetTitleBarInfo 获取窗口位置和标题栏信息
  3. 使用 SetWindowLong 将窗口设置为所有者/父窗口
  4. 为目标应用程序的几个事件设置 SetWinEventHook

Using the Code

步骤 1:创建项目

创建一个 WinForm 或 WPF 项目并添加以下类

  • 我们将使用此类来组合本地方法,以便我们可以更有效地编写代码。
    public static class Helpers
  • 本地方法集合。
    static class NativeMethods 

步骤 2:获取运行中的进程

创建一个名为“PItem”的新类并复制此代码

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);
    }
}

现在,获取所有活动的进程

Process[] pro_list = e.Result as Process[];
foreach (Process pro in pro_list)
{
    try
    {
        //When using 64bit OS pro.MainModule.ModuleName will throw exception
        // for each 32bit, so Instead of ModuleName I've used ProcessName
        ProcessList.Items.Add(new PItem(pro.ProcessName, pro.MainWindowTitle));
    }
    catch (Exception)
    {
        //Security\ Permissions Issue
    }
}

步骤 3:添加查找本地方法

FindWindow 函数检索具有类名和窗口名与指定 strings 匹配的顶级窗口的句柄。此函数不搜索子窗口。此函数不执行区分大小写的搜索。将以下方法添加到 NativeMethods

using System.Runtime.InteropServices;

// Get a handle to an application window.
[DllImport("user32.dll", SetLastError = true)]
internal static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
 
// Find window by Caption only. Note you must pass IntPtr.Zero as the first parameter.
[DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)]
internal static extern IntPtr FindWindowByCaption(IntPtr ZeroOnly, string lpWindowName); 

将以下代码添加到 Helpers。我创建了一个 Find 方法来处理 FindWindow FindWindowByCaption (这样搜索起来更容易 :-D)

public static IntPtr Find(string ModuleName, string MainWindowTitle)
{
    //Search the window using Module and Title
    IntPtr WndToFind = NativeMethods.FindWindow(ModuleName, MainWindowTitle);
    if (WndToFind.Equals(IntPtr.Zero))
    {
        if (!string.IsNullOrEmpty(MainWindowTitle))
        {
            //Search window using TItle only.
            WndToFind = NativeMethods.FindWindowByCaption(WndToFind, MainWindowTitle);
            if (WndToFind.Equals(IntPtr.Zero))
                return new IntPtr(0);
        }
    }
    return WndToFind;
}

步骤 4:从进程查找窗口句柄

使用 PItem,我们可以使用 ModuleName/进程名,如果窗口存在,我们还可以使用窗口标题。调用我们的 Helpers 类并使用 Find 方法与 ProcessName 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()); // We Found The Window
else
    Log(ModuleName + " Not found"); // No Window Found

现在我可以假设我们有了窗口句柄(如果没有,请阅读第 1 部分)。现在我们需要从 TargetWindow 获取 TitleBarInfo 。使用这些数据,我们可以获取窗口的位置等等。

步骤 5:添加 GetTitleBarInfo 和 GetLastError

使用 GettitleBarInfo 将允许我们从目标应用程序获取信息。

[return: MarshalAs(UnmanagedType.Bool)]
[DllImport("user32.dll")]
internal static extern bool GetTitleBarInfo(IntPtr hwnd, ref TITLEBARINFO pti);
 
//GetLastError- retrieves the last system error.
[DllImport("coredll.dll", SetLastError = true)]
internal static extern Int32 GetLastError();

步骤 6:添加 TitleBarInfo & RECT 属性

在使用 GetTitleBarInfo 之前,让我们将以下属性添加到 NativeMethods 类中。

[StructLayout(LayoutKind.Sequential)]
internal struct TITLEBARINFO
{
    public const int CCHILDREN_TITLEBAR = 5;
    public uint cbSize; //Specifies the size, in bytes, of the structure. 
    //The caller must set this to sizeof(TITLEBARINFO).
 
    public RECT rcTitleBar; //Pointer to a RECT structure that receives the 
    //coordinates of the title bar. These coordinates include all title-bar elements
    //except the window menu.
 
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 6)]
 
    //Add reference for System.Windows.Forms
    public AccessibleStates[] rgstate;
    //0    The title bar itself.
    //1    Reserved.
    //2    Minimize button.
    //3    Maximize button.
    //4    Help button.
    //5    Close button.
}
 
[StructLayout(LayoutKind.Sequential)]
internal struct RECT
{
    internal int left;
    internal int top;
    internal int right;
    internal int bottom;
}

步骤 7:添加 GetWindowPosition

GetWindowPosition (如下)添加到 Helpers 类中,GetWindowPosition 将首先初始化 TITLEBARINFO (确保设置 sbSize),然后使用 GetTitleBarInfo 来获取屏幕上的 TitleBar 位置。

public static WinPosition GetWindowPosition(IntPtr wnd)
{
    NativeMethods.TITLEBARINFO pti = new NativeMethods.TITLEBARINFO();
    pti.cbSize = (uint)Marshal.SizeOf(pti);	//Specifies the size, in bytes, 
					//of the structure. 
    //The caller must set this to sizeof(TITLEBARINFO).
    bool result = NativeMethods.GetTitleBarInfo(wnd, ref pti);
 
    WinPosition winpos;
    if (result)
        winpos = new WinPosition(pti);
    else
        winpos = new WinPosition();
 
    return winpos;
}

现在让我们看看如何使用这些信息在另一个应用程序上方添加我们自己的控件(请参阅图片)。

步骤 8:创建 HoverControl

HoverControl 基本上是一个控件。我创建了一个名为 HoverControl 的新 Windows 控件,具有以下属性

WindowStyle="None" AllowsTransparency="True" Background="{Binding Null}" 

这很重要,因为我们希望窗口是透明的,并且没有窗口边框。以下是 HoverControl 的完整 XAML。

<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> 

鼠标按下事件应关闭 HoverControl

步骤 9:将 SetWindowLong 添加到 NativeMethod 类

SetWindowLongPtr 函数更改指定窗口的属性。该函数还将值设置在指定偏移量的额外窗口内存中。为了编写与 32 位和 64 位 Windows 版本兼容的代码,请同时添加 SetWindowLongPtr
///The SetWindowLongPtr function changes an attribute of the specified window
[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);

步骤 10:将 SetWindowLong 添加到 Helpers 类

在这种情况下,我们唯一需要更改的属性是 - GWL_HWNDPARENT,我们的目标是将目标窗口设置为我们 HoverControl 的父/所有者。

//Specifies the zero-based offset to the value to be set.
//Valid values are in the range zero through the number of bytes of extra window memory, 
//minus the size of an integer.
public enum GWLParameter
{
    GWL_EXSTYLE = -20, //Sets a new extended window style
    GWL_HINSTANCE = -6, //Sets a new application instance handle.
    GWL_HWNDPARENT = -8, //Set window handle as parent
    GWL_ID = -12, //Sets a new identifier of the window.
    GWL_STYLE = -16, // Set new window style
    GWL_USERDATA = -21, //Sets the user data associated with the window. 
                        //This data is intended for use by the application 
                        //that created the window. Its value is initially zero.
    GWL_WNDPROC = -4 //Sets a new address for the window procedure.
}

而不是从我们的 UI 类中检查它是 64 位还是 32 位,请将 SetWindowLong 添加到 helpers 类中。

public static int SetWindowLong(IntPtr windowHandle, GWLParameter nIndex, int dwNewLong)
{
    if (IntPtr.Size == 8) //Check if this window is 64bit
    {
        return (int)NativeMethods.SetWindowLongPtr64
		(windowHandle, nIndex, new IntPtr(dwNewLong));
    }
    return NativeMethods.SetWindowLong32(windowHandle, nIndex, dwNewLong);
}

步骤 11:将 HoverControl 添加到目标应用程序的顶部

首先,我们需要创建我们新的 HoverControl 的新实例,根据目标窗口的 TitleBar 位置(第 2 部分)设置位置,并将 HoverControl 设置为目标窗口的子项。添加按钮点击事件,并添加以下代码

if (OnTopControl != null)
    OnTopControl.Close();
//Creates new instance of HoverControl
HoverControl OnTopControl = new HoverControl();
OnTopControl.Show();
//Search for HoverControl handle
IntPtr OnTopHandle = Helpers.Find(OnTopControl.Name, OnTopControl.Title);
 
//Set the new location of the control (on top the titlebar)
OnTopControl.Left = left;
OnTopControl.Top = top;
 
//Change target window to be parent of HoverControl.
Helpers.SetWindowLong(OnTopHandle, Helpers.GWLParameter.GWL_HWNDPARENT, 
TargetWnd.ToInt32());
 
Log("Hover Control Added!");

好了,这是本系列最后一篇——如何使用 SetWinEventHook 设置窗口事件。唯一剩下的是监听目标窗口事件(例如:LocationChange),以便我们可以相应地移动我们的 HoverControl 。我们将使用更多 NativeMethods 来完成这项任务。

步骤 12:将 SetWinEventHook & UnhookWinEvent 添加到 NativeMethods 类

我们需要使用 SetWinEventHook ,因为此函数允许客户端指定它们感兴趣的进程和线程。客户端可以多次调用 SetWinEventHook ,如果它们想注册其他挂钩函数或监听其他事件。

HoverControlEvent.gif

[DllImport("user32.dll")]
internal static extern IntPtr SetWinEventHook(
    AccessibleEvents eventMin, 	//Specifies the event constant for the 
				//lowest event value in the range of events that are 
				//handled by the hook function. This parameter can 
				//be set to EVENT_MIN to indicate the 
				//lowest possible event value.
    AccessibleEvents eventMax, 	//Specifies the event constant for the highest event 
				//value in the range of events that are handled 
				//by the hook function. This parameter can be set 
				//to EVENT_MAX to indicate the highest possible 
				//event value.
    IntPtr eventHookAssemblyHandle, 	//Handle to the DLL that contains the hook 
				//function at lpfnWinEventProc, if the 
				//WINEVENT_INCONTEXT flag is specified in the 
				//dwFlags parameter. If the hook function is not 
				//located in a DLL, or if the WINEVENT_OUTOFCONTEXT 
				//flag is specified, this parameter is NULL.
    WinEventProc eventHookHandle, 	//Pointer to the event hook function. 
				//For more information about this function
    uint processId, 		//Specifies the ID of the process from which the 
				//hook function receives events. Specify zero (0) 
				//to receive events from all processes on the 
				//current desktop.
    uint threadId,			//Specifies the ID of the thread from which the 
				//hook function receives events. 
				//If this parameter is zero, the hook function is 
				//associated with all existing threads on the 
				//current desktop.
    SetWinEventHookParameter parameterFlags //Flag values that specify the location 
				//of the hook function and of the events to be 
				//skipped. The following flags are valid:
    );

删除由先前调用创建的事件挂钩函数。

[return: MarshalAs(UnmanagedType.Bool)] 
[DllImport("user32.dll")] 
internal static extern bool UnhookWinEvent(IntPtr eventHookHandle);

WinEventProc - ** 重要 **

一个应用程序定义的 callback(或 hook)函数,当发生由可访问对象生成的事件时,系统会调用该函数。挂钩函数根据需要处理事件通知。客户端通过调用 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
} 

步骤 13:创建 SetControl & GetWindowPosition 方法

在此步骤中,您只需要将 btn_get_pos_Click btn_add_Click 方法中的代码提取到外部方法中,这将为我们以后服务。

void SetControl(bool log)
{
    //Creates new instance of HoverControl
    if (OnTopControl != null)
        OnTopControl.Close();
    OnTopControl = new HoverControl();
    OnTopControl.Show();
    //Search for HoverControl handle
    IntPtr OnTopHandle = Helpers.Find(OnTopControl.Name, OnTopControl.Title);
 
    //Set the new location of the control (on top the titlebar)
    OnTopControl.Left = left;
    OnTopControl.Top = top;
 
    if(log)
        Log("Hover Control Added!");
 
    //Change target window to be parent of HoverControl.
    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));
 
    //retrieves the last system error.
    Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error());
}

步骤 14:添加事件和回调

首先,我们需要定义我们想要监听的事件类型,然后创建一个包含 AccessibleEvents 和特定回调(或通用回调)的字典。您可以在此处 了解更多关于 WinEvents 的信息。我添加了 LocationChanged Destroy 的事件,LocationChanged 将帮助我在用户更改位置时每次找到目标窗口上的位置,而 Destroy 则在目标窗口关闭时关闭我们的 HoverControl

private Dictionary<AccessibleEvents, NativeMethods.WinEventProc> 
	InitializeWinEventToHandlerMap()
{
    Dictionary<AccessibleEvents, NativeMethods.WinEventProc> dictionary = 
		new Dictionary<AccessibleEvents, NativeMethods.WinEventProc >();
    //You can add more events like ValueChanged - for more info please read - 
    //http://msdn.microsoft.com/en-us/library/system.windows.forms.accessibleevents.aspx
    //dictionary.Add(AccessibleEvents.ValueChange, 
		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)
{
    //Make sure AccessibleEvents equals to LocationChange and the 
    //current window is the Target Window.
    if (accEvent == AccessibleEvents.Destroy && windowHandle.ToInt32() == 
		TargetWnd.ToInt32())
    {
        //Queues a method for execution. The method executes when a thread pool 
        //thread becomes available.
        ThreadPool.QueueUserWorkItem(new WaitCallback(this.DestroyHelper));
    }
}
 
private void DestroyHelper(object state)
{
    Execute ex = delegate()
    {
        //Removes an event hook function created by a previous call to 
        NativeMethods.UnhookWinEvent(g_hook);
        //Close HoverControl window.
        OnTopControl.Close();
    };
    this.Dispatcher.Invoke(ex, null);
}
 
private void LocationChangedCallback(IntPtr winEventHookHandle, 
	AccessibleEvents accEvent, IntPtr windowHandle, int objectId, 
	int childId, uint eventThreadId, uint eventTimeInMilliseconds)
{
    //Make sure AccessibleEvents equals to LocationChange and the 
    //current window is the Target Window.
    if (accEvent == AccessibleEvents.LocationChange && windowHandle.ToInt32() == 
		TargetWnd.ToInt32())
    {
        //Queues a method for execution. The method executes when a thread pool 
        //thread becomes available.
        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);
}

步骤 15:设置 WinEventHook

这是最后一步,也是最重要的一步。您可以设置任意数量的事件,但请确保使用 GCHandle,这样垃圾回收器就不会移动回调,否则您将遇到许多错误。

IntPtr g_hook;
private void btn_set_event_Click(object sender, RoutedEventArgs e)
{
    Dictionary<AccessibleEvents, NativeMethods.WinEventProc> <accessibleevents>events = 
					InitializeWinEventToHandlerMap();
 
    //initialize the first event to LocationChanged
    NativeMethods.WinEventProc eventHandler =
        new NativeMethods.WinEventProc(events[AccessibleEvents.LocationChange].Invoke);
 
    //When you use SetWinEventHook to set a callback in managed code, 
    //you should use the GCHandle
    //(Provides a way to access a managed object from unmanaged memory.) 
    //structure to avoid exceptions. 
    //This tells the garbage collector not to move the callback.
    GCHandle gch = GCHandle.Alloc(eventHandler);
 
    //Set Window Event Hool on Location changed.
    g_hook = NativeMethods.SetWinEventHook(AccessibleEvents.LocationChange,
        AccessibleEvents.LocationChange, IntPtr.Zero, eventHandler
        , 0, 0, NativeMethods.SetWinEventHookParameter.WINEVENT_OUTOFCONTEXT);
 
    //Hook window close event - close our HoverContorl on Target window close.
    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);
 
    //AccessibleEvents -> 
    //http://msdn.microsoft.com/en-us/library/system.windows.forms.accessibleevents.aspx
    //SetWinEventHookParameter -> 
    //http://msdn.microsoft.com/en-us/library/dd373640(VS.85).aspx
}
© . All rights reserved.