Table of Contents
本文旨在自己动手实现一个类似于“按键精灵”的桌面软件。第一部分介绍了简单的模拟方式,但是有些软件能够屏蔽掉这种简单模拟带来的效果,因此第二部分将介绍如何从驱动级层面进行模拟。
游戏外挂一般分为三个级别:
初级是鼠标、键盘模拟-鼠标键盘模拟的外挂,就是SendMessage或者Key_event
中级是Call游戏内部函数,读写内存-就是Hook进入程序内部操作
高级是抓包,封包的“脱机挂”(完全模拟客户端网络数据,不用运行游戏)。
用C#写外挂的不是很多,大部分是C++,主要原因是MS的C#目前不支持内联汇编功能。因此用C++写底层库,然后用C#调用成为DONET爱好者开发外挂的首选。
简单的模拟键盘鼠标方式(c#)
初级是鼠标、键盘模拟-鼠标键盘模拟的外挂,就是SendMessage或者Key_event
发送消息 模拟鼠标键盘定时 自动下载dzh 6.03 分笔数据(c#)- SendMessage
path
F:\stock\ReadDZHRealTime\src\F10\DownLoadF0FormsTimer\Form1.cs
/// <summary>
/// 发送消息 模拟鼠标键盘定时 自动下载dzh 6.03 分笔数据
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button_DownloadReportData_Click(object sender, EventArgs e)
{
Process[] Precess = Process.GetProcesses();
foreach (Process p in Precess)
{
if (p.ProcessName == "dzh2")
{
SetForegroundWindow(p.MainWindowHandle);
if (p.MainWindowTitle.ToUpper().Contains("大智慧") || p.MainWindowTitle.ToUpper().Contains("下载数据"))
{
}
IntPtr msgHandle = FindWindow(null, "下载数据"); //主窗口标题
//把下载窗口显示到前台 20190303
SetForegroundWindow(msgHandle);
if (msgHandle != IntPtr.Zero)
{
//找到Button
// 判断按钮是否可用
IntPtr btnHandle = FindWindowEx(msgHandle, 0, "Button", "开始");
IntPtr btnHandle2 = FindWindowEx(msgHandle, 0, "Button", "取消");
// 判断按钮是否可用
bool a = IsWindowEnabled(btnHandle);
int i = 0;
if (btnHandle != IntPtr.Zero)
{
i = SendMessage(btnHandle, BM_CLICK, 0, 0); //const int MOUSEEVENTF_LEFTDOWN = 0x0002; //模拟鼠标左键按下
}
}
// 等待下载完成 弹出窗口 出现
// 判断弹出下载成功窗口
int ii = 0;
while (true)
{
System.Threading.Thread.Sleep(1000);
// 判断弹出下载成功警告框
IntPtr msgHandleDownloadSuccessfulResult = FindWindow(null, "SuperStk"); //主窗口标题
if (msgHandleDownloadSuccessfulResult != IntPtr.Zero)
{
//找到Button
// 判断按钮是否可用
IntPtr btnHandleOK = FindWindowEx(msgHandleDownloadSuccessfulResult, 0, "Button", "确定");
// 判断按钮是否可用
bool a = IsWindowEnabled(btnHandleOK);
int i = 0;
if (btnHandleOK != IntPtr.Zero)
{
//把下载窗口显示到前台 20190303
SetForegroundWindow(msgHandleDownloadSuccessfulResult);
i = SendMessage(btnHandleOK, BM_CLICK, 0, 0); //模拟鼠标左键按下
break;
// 等待下载完成
//while (!IsWindowEnabled(btnHandleOK))
//{
// int ddd = 0;
// Application.DoEvents();
//}
}
}
ii++;
// Console.WriteLine("第"+i.ToString()+"次检查"); 5秒都没显示出来就推出循环
if (ii > 15)
{
break;
}
int ddd = 0;
Application.DoEvents();
}
// step 3 下载完成,处理弹出下载成功窗口
}
}
}
How to use
鼠标右键点击 DownLoadF0FormsTimer.exe,用Administrator 权限 运行程序,确保其他应用程序能收到 发送的消息
模拟按键股票代码下载大智慧6.03 F10 (c#)- Key_event SendKeys
path:
F:\stock\ReadDZHRealTime\src\F10\DownLoadF10FormsSimulateMouseKeyFromDzh603\DownLoadF10FormsSimulateMouseKeyFromDzh603.csproj
namespace DownLoadF10FormsSimulateMouseKeyFromDzh603
{
public partial class Form1 : Form
{
//http://it.china-b.com/cxsj/cs/20090824/163177_1.html
[DllImport("msvcrt.dll")]
public static extern int _getch();
[DllImport("msvcrt.dll")]
public static extern int _kbhit();
[DllImport("user32.dll")]
private static extern bool
SetForegroundWindow(IntPtr hWnd);
// [System.Runtime.InteropServices.DllImport("user32")]
// private static extern int mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo);
[DllImport("user32", EntryPoint = "mouse_event")]
private static extern int mouse_event(
int dwFlags,// 下表中标志之一或它们的组合
int dx,
int dy, //指定x,y方向的绝对位置或相对位置
int cButtons,//没有使用
int dwExtraInfo//没有使用
);
const int MOUSEEVENTF_MOVE = 0x0001; // 移动鼠标
const int MOUSEEVENTF_LEFTDOWN = 0x0002; //模拟鼠标左键按下
const int MOUSEEVENTF_LEFTUP = 0x0004; //模拟鼠标左键抬起
const int MOUSEEVENTF_RIGHTDOWN = 0x0008; //模拟鼠标右键按下
const int MOUSEEVENTF_RIGHTUP = 0x0010; //模拟鼠标右键抬起
const int MOUSEEVENTF_MIDDLEDOWN = 0x0020;// 模拟鼠标中键按下
const int MOUSEEVENTF_MIDDLEUP = 0x0040;// 模拟鼠标中键抬起
const int MOUSEEVENTF_ABSOLUTE = 0x8000; //标示是否采用绝对坐标
[DllImport("user32.dll")]
static extern bool SetCursorPos(int X, int Y);
public Form1()
{
InitializeComponent();
}
private void button_DownLoadF10FormsSimulateMouseKeyFromDzh603_Click(object sender, EventArgs e)
{
//创建一个矩形对象
System.Drawing.Rectangle rect = new System.Drawing.Rectangle();
//通过一个函数对这个矩形对象赋值,这些值就是屏幕的工作区域
rect = Screen.GetBounds(this);
// MessageBox.Show("本机器的分辨率是" + rect.Width.ToString() + "*" + rect.Height.ToString());
int xstep = rect.Width / 12;
int ystep = rect.Height / 35;
int startx = 140;
int starty = 80;
bool a = false;
ConsoleKeyInfo keyInfo;
foreach (Process p in Process.GetProcesses())
{
if (p.ProcessName == "dzh2")
{
}
if (p.MainWindowTitle.ToUpper().Contains("大智慧") || p.MainWindowTitle.ToUpper().Contains("模拟精灵"))
{
SetForegroundWindow(p.MainWindowHandle);
System.Threading.Thread.Sleep(1000);
SendKeys.SendWait("SZ300009");
SendKeys.SendWait("{ENTER}");
SendKeys.SendWait("{F10}");
SendKeys.Flush();
mouse_event(MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE, startx, starty, 0, 0);
System.Threading.Thread.Sleep(2000);
DzhDataProvider dzhDataProvider = new DzhDataProvider();
//从财务数据中读取股票代码表
var codeTableList = dzhDataProvider.GetCodetableFromFullFin();
foreach (string item in codeTableList)
{
//在while (true)循环中,按a键退出循环 http://it.china-b.com/cxsj/cs/20090824/163177_1.html
if (_kbhit() > 0)//如果有键按下
{
if ((char)_getch() == 0x1b)//判断按键ESc,ESC的键盘扫描码是0x1b
break;
}
//获取鼠标位置
int y1 = Control.MousePosition.Y;
//鼠标移到屏幕下方退出
if (y1 >= rect.Height / 2)
break;
//股票代码
string stockcode = item.ToString();
//非股票,下一个
if (Common.DzhData.Utility.StockCategory.GetStockCodeType(stockcode) != Common.DzhData.Model.StockCodeType.ISGP)
continue;
//模拟键盘输入股票代码,回车,按F10键
for (int i = 0; i < stockcode.Length; i++)//逐字节变为16进制字符
{
var code = stockcode[i];
SendKeys.SendWait($"{code}");
System.Threading.Thread.Sleep(500);
}
SendKeys.SendWait("{ENTER}");
System.Threading.Thread.Sleep(500);
SendKeys.SendWait("{F10}");
SendKeys.Flush();
string Market = stockcode.Substring(0, 2);
string filename = stockcode.Replace(Market, "") + ".F10";
string datafile = dzhDataProvider.InstalllPath + "\\data\\" + Market + "\\base\\" + filename;
//确保F0文件生成-判断文件.F10是否生成,如果按键后没生成文件,再按一次
int failcount;
failcount = 0;
while (!File.Exists(datafile) && failcount < 4)
{
SendKeys.SendWait($"{stockcode}");
SendKeys.SendWait("{ENTER}");
SendKeys.SendWait("{F10}");
SendKeys.Flush();
System.Threading.Thread.Sleep(1000);
failcount++;
}
if (File.Exists(datafile))
{
int i = 0;
DataFileStruct.F10File_Data_RecordStruct[] subitem = dzhDataProvider.GetF10SubItem(stockcode);
//菜单为2行,每行8个子栏目
int count = 1;
for (int row = 0; row < 2; row++)
{
//后面8个子栏目
for (int col = 1; col < 9; col++)
{
if (count > subitem.Length) break;
SetCursorPos(startx + col * xstep, starty + row * ystep);
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
datafile = dzhDataProvider.InstalllPath + "\\data\\" + Market + "\\base\\" + subitem[count - 1].SubItemFileName;
failcount = 0;
while (!File.Exists(datafile) && failcount < 4)
{
SetCursorPos(startx + col * xstep, starty + row * ystep);
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
System.Threading.Thread.Sleep(1000);
failcount++;
}
//判断子目录文件是否生成,如未生成再按一次
if (!File.Exists(datafile))
{
SendKeys.SendWait(stockcode);
SendKeys.SendWait("{ENTER}");
SendKeys.SendWait("{F10}");
SendKeys.Flush();
SetCursorPos(startx + col * xstep, starty + row * ystep);
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
}
System.Threading.Thread.Sleep(100);
count++;
}
}
}
}
mouse_event(MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_LEFTUP, 0, 0, 0, 0);
mouse_event(MOUSEEVENTF_LEFTDOWN, 500, 400, 0, 0);
mouse_event(MOUSEEVENTF_LEFTUP, 500, 400, 0, 0);
//SendKeys.SendWait("SZ300006");
//SendKeys.SendWait("{ENTER}");
//SendKeys.SendWait("SZ300007");
//SendKeys.SendWait("{ENTER}");
}
else
{
}
}
}
中级是Call游戏内部函数,读写内存,Hook进入程序内部
1.使用Spy++工具,查找窗口,查看 主窗口及其下控件的句柄、控件类型、Caption。
2.对于给定的对话框窗口,用FindWindow函数。
对话框窗口其中的任何控件,如图标、文本、确定、取消按钮等都是它的子窗口,本质上还是窗口,查找子窗口时用FindWindowEx。
凡运行于Windows上的窗口,都具有句柄。窗口上的文本框,按钮之类的,也有其句柄(可看作子窗口句柄)。这些句柄的类型可以通过Spy++进行查询.
用EnumWindows,遍历所有的顶级父窗口,用EnumChildWindows遍历主窗口下的所有子窗口。
vs 2019 tool spy++ find 找到窗口名称
用findWindow 找到主窗口句柄、用findWindowEx 找到主窗口内控件的句柄
用SendMessage 向句柄发送 按键 或 鼠标点击消息
鼠标右键点击 .EXE,用Administrator 权限 运行程序,确保其他应用程序能收到 发送的消息
首先: 要确保 hWnd 是你希望操作的”日常工作软件“的主窗口句柄的ID号码 —— 这是很古老的Win32API的概念,每个桌面上的窗口(包括不可见的),甚至一个按钮,都对应一个唯一的hWnd句柄ID。。。。 如果不能获取或者获取的不对,你就趁早放弃吧。。。
/// <summary>
/// 找到窗口
/// </summary>
/// <param name="lpClassName">窗口类名(例:Button)</param>
/// <param name="lpWindowName">窗口标题</param>
/// <returns></returns>
[DllImport("user32.dll", EntryPoint = "FindWindow")]
private extern static IntPtr FindWindow(string lpClassName, string lpWindowName);
/// <summary>
/// 找到窗口
/// </summary>
/// <param name="hwndParent">父窗口句柄(如果为空,则为桌面窗口)</param>
/// <param name="hwndChildAfter">子窗口句柄(从该子窗口之后查找)</param>
/// <param name="lpszClass">窗口类名(例:Button</param>
/// <param name="lpszWindow">窗口标题</param>
/// <returns></returns>
[DllImport("user32.dll", EntryPoint = "FindWindowEx")]
private extern static IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
/// <summary>
/// 发送消息
/// </summary>
/// <param name="hwnd">消息接受窗口句柄</param>
/// <param name="wMsg">消息</param>
/// <param name="wParam">指定附加的消息特定信息</param>
/// <param name="lParam">指定附加的消息特定信息</param>
/// <returns></returns>
[DllImport("user32.dll", EntryPoint = "SendMessageA")]
private static extern int SendMessage(IntPtr hwnd, uint wMsg, int wParam, int lParam);
//窗口发送给按钮控件的消息,让按钮执行点击操作,可以模拟按钮点击
private const int BM_CLICK = 0xF5;
其次:右键点击 .EXE,用Administrator 权限 运行程序,确保其他应用程序能收到 发送的消息
在现在Windows的UAC的管制之下,以及各种安全机制、沙箱隔离的现代防恶意软件的保护机制的呵护下,你要确定你的程序有系统管理员级别的权限,能够给其他”日常工作软件“的窗体发送消息才可以
最后:如其他楼层回复所述,如果是要发送键盘,那第二个参数应该是个类似于:WM_KEYDOWN 这样的值。
通过窗体标题,循环查找该窗体,然后找到确定按钮,通过句柄发送点击消息。
private void Form1_Load(object sender, EventArgs e)
{
Task task = new Task(() =>
{
while (true)
{
//测试警告框
IntPtr maindHwnd = FindWindow(null, "提示");//主窗口标题
if (maindHwnd != IntPtr.Zero)
{
IntPtr childHwnd = FindWindowEx(maindHwnd, IntPtr.Zero, null, "确定");//按钮控件标题
if (childHwnd != IntPtr.Zero)
{
SendMessage(childHwnd, BM_CLICK, 0, 0);
}
}
}
});
task.Start();
}
Note:发送键盘(WM_KEYDOWN, WM_KEYUP)事件和发送鼠标事件(WM_MOUSEDOWN, WM_MOUSEUP)时候,最后两个参数的用途是不一样的
SysListView32
读取其他软件listview控件的内容 https://www.cnblogs.com/szyicol/p/4872094.html
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace 读取其他软件listview控件的内容
{
public partial class Form1 : Form
{
int hwnd; //窗口句柄
int process;//进程句柄
int pointer;
private const uint LVM_FIRST = 0x1000;
private const uint LVM_GETHEADER = LVM_FIRST + 31;
private const uint LVM_GETITEMCOUNT = LVM_FIRST + 4;//获取列表行数
private const uint LVM_GETITEMTEXT = LVM_FIRST + 45;//获取列表内的内容
private const uint LVM_GETITEMW = LVM_FIRST + 75;
private const uint HDM_GETITEMCOUNT = 0x1200;//获取列表列数
private const uint PROCESS_VM_OPERATION = 0x0008;//允许函数VirtualProtectEx使用此句柄修改进程的虚拟内存
private const uint PROCESS_VM_READ = 0x0010;//允许函数访问权限
private const uint PROCESS_VM_WRITE = 0x0020;//允许函数写入权限
private const uint MEM_COMMIT = 0x1000;//为特定的页面区域分配内存中或磁盘的页面文件中的物理存储
private const uint MEM_RELEASE = 0x8000;
private const uint MEM_RESERVE = 0x2000;//保留进程的虚拟地址空间,而不分配任何物理存储
private const uint PAGE_READWRITE = 4;
private int LVIF_TEXT = 0x0001;
[DllImport("user32.dll")]//查找窗口
private static extern int FindWindow(
string strClassName, //窗口类名
string strWindowName //窗口标题
);
[DllImport("user32.dll")]//在窗口列表中寻找与指定条件相符的第一个子窗口
private static extern int FindWindowEx(
int hwndParent, // handle to parent window
int hwndChildAfter, // handle to child window
string className, //窗口类名
string windowName // 窗口标题
);
[DllImport("user32.DLL")]
private static extern int SendMessage(int hWnd, uint Msg, int wParam, int lParam);
[DllImport("user32.dll")]//找出某个窗口的创建者(线程或进程),返回创建者的标志符
private static extern int GetWindowThreadProcessId(int hwnd, out int processId);
[DllImport("kernel32.dll")]//打开一个已存在的进程对象,并返回进程的句柄
private static extern int OpenProcess(uint dwDesiredAccess, bool bInheritHandle, int processId);
[DllImport("kernel32.dll")]//为指定的进程分配内存地址:成功则返回分配内存的首地址
private static extern int VirtualAllocEx(int hProcess, IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]//从指定内存中读取字节集数据
private static extern bool ReadProcessMemory(
int hProcess, //被读取者的进程句柄
int lpBaseAddress,//开始读取的内存地址
IntPtr lpBuffer, //数据存储变量
int nSize, //要写入多少字节
ref uint vNumberOfBytesRead//读取长度
);
[DllImport("kernel32.dll")]//将数据写入内存中
private static extern bool WriteProcessMemory(
int hProcess,//由OpenProcess返回的进程句柄
int lpBaseAddress, //要写的内存首地址,再写入之前,此函数将先检查目标地址是否可用,并能容纳待写入的数据
IntPtr lpBuffer, //指向要写的数据的指针
int nSize, //要写入的字节数
ref uint vNumberOfBytesRead
);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(int handle);
[DllImport("kernel32.dll")]//在其它进程中释放申请的虚拟内存空间
private static extern bool VirtualFreeEx(
int hProcess,//目标进程的句柄,该句柄必须拥有PROCESS_VM_OPERATION的权限
int lpAddress,//指向要释放的虚拟内存空间首地址的指针
uint dwSize,
uint dwFreeType//释放类型
);
/// <summary>
/// LVITEM结构体,是列表视图控件的一个重要的数据结构
/// 占空间:4(int)x7=28个byte
/// </summary>
private struct LVITEM //结构体
{
public int mask;//说明此结构中哪些成员是有效的
public int iItem;//项目的索引值(可以视为行号)从0开始
public int iSubItem; //子项的索引值(可以视为列号)从0开始
public int state;//子项的状态
public int stateMask; //状态有效的屏蔽位
public IntPtr pszText; //主项或子项的名称
public int cchTextMax;//pszText所指向的缓冲区大小
}
public Form1()
{
InitializeComponent();
}
/// <summary>
/// LV列表总行数
/// </summary>
private int ListView_GetItemRows(int handle)
{
return SendMessage(handle, LVM_GETITEMCOUNT, 0, 0);
}
/// <summary>
/// LV列表总列数
/// </summary>
private int ListView_GetItemCols(int handle)
{
return SendMessage(handle, HDM_GETITEMCOUNT, 0, 0);
}
private void button1_Click(object sender, EventArgs e)
{
int headerhwnd; //listview控件的列头句柄
int rows, cols; //listview控件中的行列数
int processId; //进程pid
hwnd = FindWindow("#32770", "Windows 任务管理器");
hwnd = FindWindowEx(hwnd, 0, "#32770", null);
hwnd = FindWindowEx(hwnd, 0, "SysListView32", null);//进程界面窗口的句柄,通过SPY获取
//hwnd = 0xC1CC0;
headerhwnd = SendMessage(hwnd, LVM_GETHEADER, 0, 0);//listview的列头句柄
rows = ListView_GetItemRows(hwnd);//总行数,即进程的数量
cols = ListView_GetItemCols(headerhwnd);//列表列数
//cols = 2;
GetWindowThreadProcessId(hwnd, out processId);
//打开并插入进程
process = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, false, processId);
//申请代码的内存区,返回申请到的虚拟内存首地址
pointer = VirtualAllocEx(process, IntPtr.Zero, 4096, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
string[,] tempStr;//二维数组
string[] temp = new string[cols];
tempStr = GetListViewItmeValue(rows, cols);//将要读取的其他程序中的ListView控件中的文本内容保存到二维数组中
listView1.Items.Clear();//清空LV控件信息
//输出数组中保存的其他程序的LV控件信息
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
temp[j] = tempStr[i, j];
}
ListViewItem lvi = new ListViewItem(temp);
listView1.Items.Add(lvi);
}
}
/// <summary>
/// 从内存中读取指定的LV控件的文本内容
/// </summary>
/// <param name="rows">要读取的LV控件的行数</param>
/// <param name="cols">要读取的LV控件的列数</param>
/// <returns>取得的LV控件信息</returns>
private string[,] GetListViewItmeValue(int rows, int cols)
{
string[,] tempStr = new string[rows, cols];//二维数组:保存LV控件的文本信息
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
byte[] vBuffer = new byte[256];//定义一个临时缓冲区
LVITEM[] vItem = new LVITEM[1];
vItem[0].mask = LVIF_TEXT;//说明pszText是有效的
vItem[0].iItem = i; //行号
vItem[0].iSubItem = j; //列号
vItem[0].cchTextMax = vBuffer.Length;//所能存储的最大的文本为256字节
vItem[0].pszText = (IntPtr)((int)pointer + Marshal.SizeOf(typeof(LVITEM)));
uint vNumberOfBytesRead = 0;
//把数据写到vItem中
//pointer为申请到的内存的首地址
//UnsafeAddrOfPinnedArrayElement:获取指定数组中指定索引处的元素的地址
WriteProcessMemory(process, pointer, Marshal.UnsafeAddrOfPinnedArrayElement(vItem, 0), Marshal.SizeOf(typeof(LVITEM)), ref vNumberOfBytesRead);
//发送LVM_GETITEMW消息给hwnd,将返回的结果写入pointer指向的内存空间
SendMessage(hwnd, LVM_GETITEMW, i, pointer);
//从pointer指向的内存地址开始读取数据,写入缓冲区vBuffer中
ReadProcessMemory(process, ((int)pointer + Marshal.SizeOf(typeof(LVITEM))), Marshal.UnsafeAddrOfPinnedArrayElement(vBuffer, 0), vBuffer.Length, ref vNumberOfBytesRead);
string vText = Encoding.Unicode.GetString(vBuffer, 0, (int)vNumberOfBytesRead); ;
tempStr[i, j] = vText;
}
}
VirtualFreeEx(process, pointer, 0, MEM_RELEASE);//在其它进程中释放申请的虚拟内存空间,MEM_RELEASE方式很彻底,完全回收
CloseHandle(process);//关闭打开的进程对象
return tempStr;
}
}
}
驱动级模拟键盘鼠标(C#实现)- WINIO驱动
https://www.cnblogs.com/vitaminVIP/p/11733954.html
WINIO驱动
Useful links
Getting & Setting SysListView32 control items c#
https://www.codeproject.com/questions/238246/getting-setting-syslistview32-control-items
https://stackoverflow.com/questions/4857602/get-listview-items-from-other-windows
https://github.com/sahajkoka/ListOpenWindows/blob/master/ListOpenWindows/Program.cs(获取Windows窗口列表c#)
EnumChildWindows find all child windows c#
https://www.codeproject.com/Questions/816920/EnumChildWindows-doesnt-find-all-child-windows
c#自己动手实现一个类似于“按键精灵”的桌面软件。第一部分介绍了简单的模拟方式,但是有些软件能够屏蔽掉这种简单模拟带来的效果,因此第二部分将介绍如何从驱动级层面进行模拟。
https://blog.csdn.net/ryuenkyo/article/details/106207047