最近需求场景需要写一个小软件调用其他软件的某些功能,想用C#的winform来写,具体功能是用WindowsAPI实现的,搜了一些资料和踩了一些小坑,不过总体还算顺利,这里就整理一篇文章记录一下。

本人能力有限,不知道是否对此有更好的封装,一些方法和技巧是经过我各种搜索拼凑后自我认为聪明研究出的笨方法,不一定能作为正确答案与在生产环境中应用,请注意!如有问题和言笑的地方欢迎评论区喷我……

阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴阿巴

部分代码没有测试过能否正常运行,仅代表思路。

引入

先引入相关函数:

1
2
3
4
5
6
7
8
[DllImport("user32.dll")]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

[DllImport("User32.dll", EntryPoint = "SendMessage")]
private static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, string lParam);

[DllImport("User32.dll")]
public static extern IntPtr FindWindowEx(IntPtr parent, IntPtr childe, string strclass, string FrmText);

如上所示我们引入了三个函数,他们的分别功能如下:

  1. FindWinow: 找窗口。
  2. FindWindowEx: 找子组件/组件。
  3. SendMessage: 向窗口发送消息(命令)。
    我们要使用WindowsAPI对相关窗口进行操作也无非就是这三步。

第一步:找窗口

回顾下FindWindow的定义,他有三个参数:

  1. lpClassName:类名。
  2. lpWindowName:窗口名(标题)。

一般情况下,我们用第二的情况较多,第一个则填null;
二者有任意一个就可以去寻找对应参数并获取他的指针。
例如:IntPtr NotePad = FindWindow(null,"无标题 - 记事本");

第二步:找组件

回顾下FindWindowEx的定义,他有四个参数:

  1. parent 母(窗体)指针,填上面我们获取的指针。
  2. chide 子(控件)指针, 一般写IntPtr.Zero。
  3. strclass 类名,例如输入框(”EDIT”)、按钮(“Button”)、标签(”Static”)
  4. FrmText 标题,顾名思义。

“一般”情况下,我们要寻找一个组件是从窗体指针开始找的,假设这个组件是有标题的,像按钮之类的,不重名的情况下是很好找的。

但是如果是重名或无内容的输入框则寻找较为麻烦,如果有下图所示窗口,常规操作可能如下:

例如我们如果想找第二个标题为“测试2”的标签,我们至少要先找到第一个“测试2”

1
2
IntPtr hwnd = FindWindow(null, "测试");
IntPtr TestLabel2_1 = FindWindowEx(hwnd, IntPtr.Zero, "Static", "测试2");

然后从第一个的基础上找第二个。
1
IntPtr TestLabel2_2 = FindWindowEx(hwnd, TestLabel2_1, "Static", "测试2");

因此我们如果想找第二个测试2旁边的输入框,就可以从第二个测试2标签后去寻找。

第三步:发送消息

回顾下SendMessage的定义,他有四个参数:

  1. hWnd 窗口指针
  2. Msg 消息值
  3. wParam 参数1(参数指针,一般为IntPtr.Zero)
  4. lParam 参数2 (一般为null)

特别强调一下Msg对应的消息值,这是一组常量,你需要从微软的文档中查阅,然后定义为常量后使用,例如我们定义:

1
2
3
4
5
const int WM_SETTEXT = 0x000C; //设置文本
const int WM_LBUTTONDOWN = 0x0201; //按钮按下
const int WM_LBUTTONUP = 0x0202;//按钮松开
const int WM_CLOSE = 0x0010;//关闭
const int WM_GETTEXT = 0x000D;//获取文本

比如我们想要某个输入框输入指定文本就
1
SendMessage(test, WM_SETTEXT, IntPtr.Zero, "阿巴阿巴阿巴");

想按一个按钮就:
1
2
SendMessage(button, WM_LBUTTONDOWN, IntPtr.Zero, null);
SendMessage(button, WM_LBUTTONUP, IntPtr.Zero, null);

你会观察到,3、4两个参数在不同场景下并非是固定的,比如在WM_SETTEXT中最后一个变量就是你填写的内容,而在两个按钮的事件中就直接写null了。
因此还记得我们最上面引入的SendMessage的定义吗:
1
2
[DllImport("User32.dll", EntryPoint = "SendMessage")]
private static extern int SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, string lParam);

我们指定了后两个变量的类型为IntPtr和string,但是有些时候他不一定是这样的!
比如我们使用WM_GETTEXT获取文本框的内容:
1
2
3
4
const int buffer_size = 1024;
StringBuilder buffer = new StringBuilder(buffer_size);
SendMessage(input, WM_GETTEXT, buffer_size, buffer);
string get = buffer.ToString();

你肯定会直接得到一堆报错,因为后面的buffer_size和bbuffer都不是指定类型的。
那么我们就需要改函数定义的对应类型了,如果有多个应用场景不妨就弄另一个SendMessage_B出来了。

参考文献

[1]IBAS.C# - 捕获程序句柄并进行操作[EB/OL]. https://www.bilibili.com/read/cv5702086/,2020-4-20.

[2]Graent Hu. C#使用Windows API获取窗口句柄控制其他程序窗口[EB/OL].https://www.wlyc.cn/post-92.html,2017-06-26.

[3]魔尊X. C#使用SendMessage获取其他程序的输入框中的值[EB/OL].
https://blog.csdn.net/mozunx/article/details/79336919,2018-02-19.