在其他应用程序中托管和更改控件






4.98/5 (33投票s)
如何通过在托管的 C# 中添加和更改控件来更改其他进程的视觉外观
引言
本文的主要动机是 Winforms 在自身进程中的局限性。如果在其生命周期内能够更改其他进程的外观,那不是很好吗?
这行不通
Process[] processes = null;
processes = Process.GetProcessesByName("notepad");
Process Proc = processes[0];
Control notepad = Control.FromHandle(Proc.MainWindowHandle);
这将
using System.Windows
Win32Window control = Win32Window.FromProcessName("notepad");
control.Children[0].Text = "HELLO WORLD";
背景
您在桌面上看到的所有窗口都由操作系统管理。那些窗口中的每个按钮(至少在大多数应用程序中)以及所有其他控件都只是另一个窗口。许多事情只有在调用 Windows.h 中的过程时才可能实现,例如写入进程内存或更改其他窗口。
这个类就是这样。它是许多 Pinvoke 调用的包装器,包含一些额外的事件,并用原生 C# 编写。
它也是一种方法,可以在托管的 C# 中创建原生 WinAPI 窗口、按钮、列表框等,以及操纵其他窗口的外观。
.NET 中,主窗口称为 Form
。它使用一个类型为 IntPtr
的指针,称为 MainwindowHandle
,或 C++ 中的 HWND
,来标识一个窗口。您在旧应用程序(不使用自定义按钮或其他非标准 GUI 元素)中看到的每个按钮和每个文本控件也都是窗口。
任何应用程序都可以通过 Windows API 从任何其他应用程序更改任何窗口。
当然,在进程权限方面存在一些安全限制,但这些限制不影响大多数进程。请记住,这其中大部分与 Windows NT(1993 年)一样古老。
Windows NT 使用一个名为 Message
的结构来启用用户与计算机的交互。每次您使用鼠标或按下键时,都会生成一条消息。此消息被放入当前活动窗口的消息队列中。
例如:如果您按下鼠标按钮,会向您的应用程序发送一条 WM_LBUTTONDOWN
消息。如果您释放鼠标按钮,会生成一条 WM_LBUTTONUP
消息。如果以上两条消息都在一个按钮上方生成,则该按钮会向其父窗口发送一条 WM_COMMAND
消息。
任何有窗口的应用程序都需要某种形式的 messageloop
(一个处理鼠标和键盘事件的地方)。
while(GetMessage(&Msg, NULL, 0, 0) > 0)
{
TranslateMessage(&Msg);
DispatchMessage(&Msg);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
//Do stuff here
}
在 C++ 中,这将是一个窗口应用程序的主循环。
在 C# 中,此循环对程序员是隐藏的,但您仍然可以在主窗体中重写它。
protected override void WndProc(ref Message m)
{
//Do stuff here
base.WndProc(ref m);
}
现在,如果您知道某个窗口的 HWND
或 MainwindowHandle
,就可以做一些有趣的事情了。
- 向窗口发送您自己的消息
- 更改窗口样式(工具箱,无最小化按钮,无 X 按钮)
- 更改窗口标题
- 更改窗口文本
- 更改窗口状态(即使对于任务管理器等受限应用程序也是如此)
- 使其不可见
Using the Code
添加控件
这个类称为 Win32Window
,旨在像 Windows Forms 应用程序(它是 Win32 API 的包装器等)一样直观。
例如,您可以通过调用以下命令从任何现有进程(必须有 mainwindow
)中获取一个 Win32Window
:
using System.Windows;
//inside main:
Win32Window.FromProcessName(name);
为了看到它的实际效果,让我们在 CMD 中添加一个额外的按钮。
var window = Win32Window.FromProcessName("cmd");
window.Title = "A CMD WINDOW";
var Button= new Win32Button();
Button.Pos_X=window.Width/2-Button.Width;
Button.Pos_Y=80;
Button.Text = "Press Me";
window.AddControl(Button);
这使我们得到了结果。
每个按钮都有预期的点击事件。这可以用来为现有的对话框选项或任何其他应用程序添加额外的按钮。
此类 Win32Window
包含一些派生类。
Win32Button
Win32CheckBox
Win32Label
Win32ListBox
Win32TextBox
这些类中的每一个都具有一些额外的属性,但可以用作非常简化的、超向后兼容的控件对象。
移除控件
现在我们来看看如何从其他应用程序中移除 GUI 元素。让我们看看 Skype。
我不知道这是否在全球范围内都一样,但 Windows 8.1 上的桌面应用程序顶部有广告。
现在我们可以使用本文附带的 WindowExplorer
项目,通过一些属性来识别广告横幅。
我们可以清楚地看到 ClassName
是 Shell Embedding
。我们可以使用任意数量的已知属性来查找我们的窗口。
要从 Skype 中删除此项,只需调用:
using System;
using System.Windows.Forms;
using System.Windows; // WINAPI and all Win32Controls/events are here
namespace System.Windows.Native
{
static class Program
{
[STAThread]
static void Main()
{
var window = Win32Window.FromWindowWhere(x =>x.ClassName == "Shell Embedding");
window.Visible = false;
//or --> window.Destroy();
}
}
}
现在,您可以用两行代码删除其他应用程序中的广告。
我不建议破坏其他应用程序中的控件,因为这可能会使其他软件不稳定。
无论如何,将 Visibility 设置为 false 会给我们类似的结果。
类名在其他应用程序中可能相同,因此我建议仔细检查或使用 && x.Process.Id == WantedProcess.Id
扩展谓词。
瞧
如果窗口尚不存在怎么办?
当您想阻止用户打开防病毒软件的设置(无论出于何种原因)或其他窗口时,因为窗口尚未创建,所以没有 HWND
可以获取。幸运的是,Windows 有一个名为 WindowsEventHook
的方法。
不要将它们与 Window Hooks 混淆。
我已经从 Pinvoke.net 复制了正确的 Pinvoke 调用,并将其放入辅助类 Win32WindowEvent
中。
有许多通知非常有用,如果您想在另一个应用程序最小化或您开始拖放时执行某些操作。以下是事件类型描述:
细节对下一个使用者隐藏了,所以如果您想等待满足谓词的窗口,只需调用:
Win32WindowEvents.WaitForWindowWhere(x => x.Text=="I like pie");
void Win32WindowEvents_WindowFound(Process Process, Win32Window Window,
Win32WindowEvents.EventTypes type)
{
Window.Text = "No you dont";
}
因此,每当存在一个与“I like pie
”相等的记事本(或按钮或标题)时,文本就会被更改为“No you dont
”。只要应用程序正在运行,并且应用程序的安全级别不是更高,此方法就会生效。如果您想查看所有应用程序的每个 WindowEvent
,只需调用:
Win32WindowEvents.GlobalWindowEvent += Win32WindowEvents_GlobalWindowEvent;
void Win32WindowEvents_GlobalWindowEvent(Process Process, Win32Window Window,
Win32WindowEvents.EventTypes type)
{
//do stuff here. Maybe called 1000x a second if a new application is started
}
工作原理(新窗口)
每个 Win32Window
的构造函数都与使用 Windows API 在原生 C++ 中创建窗口所需的代码完全相同。当然,类代码几乎有 70% 是由 pinvoke 声明组成的。以下代码用于生成任何窗口(如前所述,按钮和其他控件只是具有特殊样式的窗口)。这将创建一个 IntPtr
或 Windowhandle
(HWND
)。
static IntPtr CreateWindow(string class_name, IntPtr hWndParent,
WndProc CallBack, WindowStyles Style,
WindowStylesEx ExStyle = WindowStylesEx.WS_EX_CLIENTEDGE)
{
//checks if classname is a standard system class like BUTTON or EDIT
IntPtr hWnd;
if (class_name == null) throw new System.Exception("class_name is null");
if (class_name == String.Empty) throw new System.Exception("class_name is empty");
var n = typeof(Win32ControlType).GetFields(BindingFlags.Static | BindingFlags.Public);
for (int i = 0; i < n.Length;i++)
{
if (n[i].GetValue(null).ToString() == class_name) { goto Create;}
}
WNDCLASS wc = new WNDCLASS();
//If class is not there register as window
wc.style = ClassStyles.HorizontalRedraw | ClassStyles.VerticalRedraw;
wc.lpfnWndProc = System.Runtime.InteropServices.Marshal.GetFunctionPointerForDelegate
(CallBack);
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = Process.GetCurrentProcess().Handle;
wc.hIcon = LoadIcon(Process.GetCurrentProcess().Handle, "IDI_APPLICATION");
wc.hCursor = LoadCursor(IntPtr.Zero, 32512);
wc.hbrBackground = new IntPtr(6);
wc.lpszMenuName = "Menu";
wc.lpszClassName = class_name;
UInt16 class_atom = RegisterClassW(ref wc);
if (class_atom == 0) { throw new Win32Exception(Marshal.GetLastWin32Error()); }
// Create window
Create:
hWnd = CreateWindowExW(
(uint)ExStyle,
class_name,
String.Empty,
(uint)Style,
0,
0,
0,
0,
hWndParent,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero
);
if (hWnd == IntPtr.Zero) { throw new Win32Exception(Marshal.GetLastWin32Error()); }
return hWnd;
}
这是在 C++ 中创建窗口所需的相同代码。请注意,messageloop
仍然缺失。所以,如果我们想创建一个 Button
:
public Win32Button() : base(IntPtr.Zero)
{
var Wait = new ManualResetEvent(false);
new Thread(() =>
{
base.hWnd = WinAPI.CreateWindow(WinAPI.WindowTypes.Button, base.WindowProcedure);
Wait.Set();
base.MessageLoop();
base.Destroy();
}).Start();
Wait.WaitOne();
}
现在您可能会想,为什么每个按钮都要创建一个新线程。您需要知道 messageloop
是一个阻塞方法,直到窗口(button
)被销毁。此设计有一个主要的缺点。WM_COMMAND
消息发送到父窗口。我们不拥有父窗口,因此在 messageloop
中没有 WM_BUTTONCLICK
消息。这就是为什么实际的点击事件代码是通过保存最后一个 buttonclick
以及更多内容生成的。有关实际实现,请参阅演示项目。
public void MessageLoop()
{
MSG msg;
while (GetMessage(out msg, hWnd, 0, 0) != 0)
{
WndProc(this.hWnd, msg.message, msg.wParam, msg.lParam);
TranslateMessage(ref msg);
DispatchMessage(ref msg);
switch((WinAPI.WM)msg.message)
{
//create all messages here (see in demo project)
}
}
}
现在我们已经创建了一个窗口并获得了一个唯一的 HWND
。创建部分对于现有窗口不是必需的。Win32Window
的类构造函数实际上只是 HWND
属性的设置器。
工作原理(现有窗口)
如前所述,任何窗口都有一个唯一的 HWND
,它是一个 IntPtr
。如果我们想更改窗口的标题,我们需要向窗口发送一条特殊的消息。
每个属性的实现方式略有不同,但大多数都使用原生的 WinApi
调用。这可能是一种糟糕的做法,因为属性通常不应该有副作用,但这样您就可以调用:
Mywindow.Title="MY TEXT HERE";
public string Title
{
get
{
StringBuilder sb = new StringBuilder(GetWindowTextLength(hWnd) + 1);
GetWindowText(hWnd, sb, sb.Capacity);
return sb.ToString();
}
set
{
SetWindowText(hWnd, value);
UpdateWindow(hWnd);
}
}
public string Text
{
get
{
StringBuilder data = new StringBuilder(32768);
SendMessage(hWnd, WM_GETTEXT, data.Capacity, data);
return data.ToString();
}
set
{
SendMessage(hWnd, WM_SETTEXT, 0, value);
}
}
演示项目
演示项目附带一个出色的窗口浏览器。在这里,您可以探索在哪个窗口中可以更改哪些内容。
第二个应用程序更多的是一个有趣的应用程序,您可以与任何记事本进行“对话”。只需键入任何以句号结尾的句子,应用程序就会做出回应。
两者的源代码都在顶部。
实际应用场景
- 自动化某些 GUI 流程
- 向源代码丢失或非常老的应用程序添加按钮
- 使 Windows 对静默打印不可见
- 当用户打开任何记事本并输入一个秘密通行语时,打开秘密表单。
- 移除任务栏或不必要的广告
未完成的工作
此版本中有许多未完成的工作。
现代视觉样式
要使您的应用程序能够使用视觉样式,您必须使用 ComCtl32.dll 版本 6 或更高版本。
如果您使用 C# 编写 Winforms 应用程序,您可以调用 Application.EnableVisualStyles();
。如果您使用 C++,您必须为您的应用程序使用一个清单。如果您使用 C# 并通过 Pinvoke 创建控件,您只能获得 Windows 2000 风格的控件。我还没有找到解决方案。
背景颜色属性
虽然可以通过调用相应的 WinApi 方法来设置其他窗口的许多属性,但我还没有找到一种方法来更改像背景颜色这样明显的东西。
任务栏中的图标更改
更改小图标和大图标没问题,但它不会自动更改任务栏中可见的图标。
如果您找到了解决以上任何问题的方法或有任何疑问,请留下评论。
历史
- 2014 年 11 月 9 日:初始版本