1.需求
有这么一个需求,有一个声卡插件:ASIO4ALL v2.1,用户有一个程序,是通过这个插件来检验声音硬件,有时候有多个硬件要同时测试,想做个小程序来自动切换ASIO的当前项。如下图所示:
因此需求大概有如下:
- 能够自动切换ASIO插件对应的当前项
了解需求之后,感觉不算太难,安排。
2.分析
由于是小程序,所以代码尽量简单,逻辑尽量简单,有两个思路:
思路1:通过ASIO SDK来进行控制
思路2:找到ASIO进程的窗口,显示此窗口,点击固定位置
思路3:模拟人工操作,双击NotifyIcon,点击某一个位置
3.实验
思路1:通过sdk发现,sdk是用来给当前进程使用的,用来播放声音输出的。并没有切换通道的接口。但是用户软件进程已经启动了ASIO插件,所以sdk再开启会造成用户进程挂掉。思路行不通。
思路2:实验后发现,ASIO是没有主进程的,所以通过进程来查找窗口行不通。且窗口会被关闭,因为ASIO插件只是一个弹出窗体,主进程是用户软件,当设置完毕关闭窗口后,窗口被销毁,SPY++已经找不到此窗口了。思路行不通。
思路3:模拟操作,需要知道system tray bar的每一个iconbutton的信息。点击后需要知道窗口里某一项的信息才能进行操作。搜索github之类的后发现,有现有方案可用(进程注入)。
关于思路3,期间用spy++发现只能看到窗口信息。不能看到窗口详情,如下图:
而想找到更多信息,只能通过其他方式。
codeproject上的一篇文章写的很好(他这个能够获取道通知栏的各个图标了,但是名称读取失败):
https://www.codeproject.com/Articles/10807/Shell-Tray-Info-Arrange-your-system-tray-icons
4.实现和主要代码
获取通知栏图标,并找到ASIO进程图标,并点击:
var trayWindowHandle = FindWindow("Shell_TrayWnd", null); var trayNotifyHandle = FindWindowEx(trayWindowHandle, IntPtr.Zero, "TrayNotifyWnd", null); var sysBarHandle = FindWindowEx(trayNotifyHandle, IntPtr.Zero, "SysPager", null); var wind = FindWindowEx(sysBarHandle, IntPtr.Zero, "ToolbarWindow32", null); GetWindowThreadProcessId((int) wind, out var pid); //注入进程 var process = OpenProcess( (int) (ProcessAccessRights.All | ProcessAccessRights.VirtualMemoryOperation | ProcessAccessRights.VirtualMemoryRead | ProcessAccessRights.VirtualMemoryWrite), false, pid); if (process == 0) MessageBox.Show("注入进程出错"); var rMemAddress = VirtualAllocEx((IntPtr) process, IntPtr.Zero, 1024, (uint) MemAllocation.MEM_COMMIT, (uint) MemProtect.PAGE_EXECUTE_READWRITE); uint TB_BUTTONCOUNT = 0x0400 + 24; uint TB_GETBUTTON = 0x0400 + 23; uint TB_GETBUTTONTEXT = 0x0400 + 45; var nButtonCount = SendMessage(wind, TB_BUTTONCOUNT, IntPtr.Zero, IntPtr.Zero); for (var i = 0; i < (int) nButtonCount - 1; i++) { //请求button var rrr = SendMessage(wind, TB_GETBUTTON, (IntPtr) i, rMemAddress); //取得button var buffer = new byte[20]; var oo = ReadProcessMemory((IntPtr) process, rMemAddress, buffer, buffer.Length, out var pp); var btnSize = Marshal.SizeOf(typeof(TBBUTTON)); var ptr = Marshal.AllocHGlobal(btnSize); Marshal.Copy(buffer, 0, ptr, buffer.Length); var bbb = Marshal.PtrToStructure<TBBUTTON>(ptr); //获取button的文字 var rrr2 = SendMessage(wind, TB_GETBUTTONTEXT, (IntPtr) bbb.idCommand, rMemAddress); var buff2 = new byte[1024]; var oo2 = ReadProcessMemory((IntPtr) process, rMemAddress, buff2, (int) rrr2, out var mm); var s = Encoding.ASCII.GetString(buff2, 0, (int) rrr2); if (s.Contains(windowTrayTooltiptbx.Text)) { // 点击 // MakeWParam(GetDlgCtrlID(h1), BN_CLICKED)//diwei,gaowei var BN_CLICKED = 0; var wP = (bbb.idCommand & 0xffff) | (BN_CLICKED << 16); uint WM_COMMAND = 0x0111; var aaab = SendMessage(wind, WM_COMMAND, (IntPtr) wP, wind); } } VirtualFreeEx((IntPtr) process, rMemAddress, 0, MEM_RELEASE); //释放注入进程 CloseHandle((IntPtr) process);
根据名称找到某一主窗口,通过C++的EnumWindows函数实现(因为FindWindow需要提供实体类名称或者窗口标题,而窗口标题可能会变):
var wndHandle = new List<int>(); EnumWindowsProc ewp = (hWnd, lParam) => { wndHandle.Add(hWnd); return true; }; EnumWindows(ewp, 0); var myWnd = 0; foreach (var w in wndHandle) { //父窗体 var parent = GetParent(w); //窗口标题 var title = new StringBuilder(256); var hasText = GetWindowText((IntPtr) w, title, title.Capacity); if (hasText > 0 && title.ToString().Contains(windowTitleTbx.Text)) { myWnd = w; break; } } return myWnd;
通过FindWindowEx函数找到主窗体的某一子窗体,即treeView窗体
var treeWindowHandle = FindWindowEx((IntPtr) mainWndHandle, IntPtr.Zero, "SysTreeView32", null);
找到子窗体TreeView窗体的每个treeViewItem的文本信息(为了检索需要):
因为是跨进程通信,所以需要进程注入:
//注入进程 GetWindowThreadProcessId(mainWndHandle, out var pid); var process = OpenProcess( (int) (ProcessAccessRights.All | ProcessAccessRights.VirtualMemoryOperation | ProcessAccessRights.VirtualMemoryRead | ProcessAccessRights.VirtualMemoryWrite), false, pid); if (process == 0) MessageBox.Show("注入进程出错");
申请内存
//申请内存 var tviPtr = VirtualAllocEx((IntPtr) process, IntPtr.Zero, 4096, (uint) MemAllocation.MEM_COMMIT, (uint) MemProtect.PAGE_EXECUTE_READWRITE);
TVM_GETNEXTITEM,获取treeView的根节点指针:
var rootptr = SendMessage(treeWindowHandle, TVM_GETNEXTITEM, (IntPtr) TVGN_ROOT, IntPtr.Zero);
得到文本:
var tvi = new TVITEMA(); tvi.mask = TVIF_TEXT; tvi.hItem = rootptr.ToInt32(); tvi.cchTextMax = 256; tvi.pszText = (int) tviPtr + Marshal.SizeOf(typeof(TVITEMA)); var tviLocal = Marshal.AllocHGlobal(tviSize); Marshal.StructureToPtr(tvi, tviLocal, false); var ok = WriteProcessMemory((IntPtr) process, tviPtr, tviLocal, (IntPtr) tviSize, out var p); Marshal.FreeHGlobal(tviLocal); var ok2 = SendMessage(treeWindowHandle, TVM_GETITEM, IntPtr.Zero, tviPtr); var b = new byte[256]; var ok1 = ReadProcessMemory((IntPtr) process, (IntPtr) ((int) tviPtr + Marshal.SizeOf(typeof(TVITEMA))), b, b.Length, out var p1); //去掉空值 var lastOkValue = b.Length - 1; while (b[lastOkValue] == 0 && lastOkValue > 0) lastOkValue = lastOkValue - 1; //得到文本 var txt = Encoding.ASCII.GetString(b, 0, lastOkValue + 1);
考虑到此treeView有两个同级别根节点,所以需要获取兄弟节点:
//获取兄弟节点 var parentPtr = SendMessage(treeWindowHandle, TVM_GETNEXTITEM, (IntPtr) TVGN_ROOT, IntPtr.Zero); rootptr = SendMessage(treeWindowHandle, TVM_GETNEXTITEM, (IntPtr) TVGN_NEXT, parentPtr);
释放进程注入内存
VirtualFreeEx((IntPtr) process, tviPtr, 0, MEM_RELEASE); //释放注入进程 CloseHandle((IntPtr) process);
最后界面大致如下:
当我点击【检索并点击---托盘图标的时候】,底部的图标会根第一个输入框的文字进行检索,找到后会点击,这时候图标对应的窗口出来了。
当我点击第二个按钮【检索并改变选中项】,底部会根据第二个输入框的名称去查找窗体,并根据第三个输入框的内容去点击对应的treeviewItem
至此,结束。界面的前两个输入框可以写死,最后一个因为所有的子项都被记录在字典里,所以可以作为index来传入参数。
将这两个步骤合并后,就得到有1个参数(想要点击的Asio程序索引号或名称之类的)的控制台程序了
参考文档(感谢这些优秀的开发者):
Win32封装:https://github.com/soukoku/CommonWin32
进程注入获取托盘图标(这个项目里的SysTreeView32.cs文件写的太优秀了,比我这个好一百倍,从这个里学习了很多):https://github.com/zhuzemin/OneKeyInstallSoftwares
TB_GETBUTTON:https://docs.microsoft.com/en-us/windows/win32/controls/bumper-toolbar-control-reference-messages
C++获取托盘图标:https://www.codeproject.com/Articles/10807/Shell-Tray-Info-Arrange-your-system-tray-icons
进程注入:https://www.codeproject.com/Articles/5570/Stealing-Program-s-Memory
托盘图标:https://www.codeproject.com/articles/10497/a-tool-to-order-the-window-buttons-in-your-taskbar
获取托盘图标:https://projects.stephenklancher.com/files/Refresh_Notification_Area/RefreshNotificationAreaSource.zip
ASIO-SDK:https://www.steinberg.net/en/company/developers.html
Win32操作窗口:https://www.cnblogs.com/sunnylux/p/10537839.html
还有其他参考文档,也一并表示感谢这些作者的奉献。
最后,本实例的源码下载:https://files.cnblogs.com/files/lizhijian/2020-8-18-win32%E5%AF%BB%E6%89%BE%E5%AD%90%E8%BF%9B%E7%A8%8B%E7%AA%97%E5%8F%A3.rar
感谢阅读