如何在VB.Net中获取外部ComboBox插入前的原始未排序项?
获取外部ComboBox原始插入顺序的VB.Net实现
首先得明白为什么你现在的方法拿不到原始顺序:如果目标ComboBox设置了CBS_SORT样式,它内部会自动对插入的项排序,CB_GETCOUNT和CB_GETLBTEXT返回的都是排序后的列表,自然看不到最初的插入顺序。
Spy++能做到是因为它捕获了ComboBox收到的插入类消息(比如CB_ADDSTRING、CB_INSERTSTRING),这些消息是项被添加时发送的,记录这些消息的参数就能还原原始顺序。下面是具体的VB.Net实现方案:
核心思路
通过Windows钩子监视目标ComboBox所在线程发送的窗口消息,捕获CB_ADDSTRING和CB_INSERTSTRING消息,读取消息携带的项文本并按插入顺序记录。由于是跨进程操作,需要用到进程内存读取和钩子注入的相关API,这里推荐用EasyHook简化托管代码的跨进程注入(可以通过NuGet直接安装)。
完整代码实现
Imports System.Runtime.InteropServices Imports EasyHook Public Class ComboBoxOriginalOrderTracker ' ComboBox消息常量 Private Const CB_ADDSTRING As Integer = &H143 Private Const CB_INSERTSTRING As Integer = &H14A ' 钩子类型:监视发送到窗口的消息 Private Const WH_CALLWNDPROC As Integer = 4 ' Windows API声明 <DllImport("user32.dll", SetLastError:=True)> Private Shared Function GetWindowThreadProcessId(hWnd As IntPtr, ByRef lpdwProcessId As UInteger) As UInteger End Function <DllImport("kernel32.dll", SetLastError:=True)> Private Shared Function OpenProcess(dwDesiredAccess As UInteger, bInheritHandle As Boolean, dwProcessId As UInteger) As IntPtr End Function <DllImport("kernel32.dll", SetLastError:=True)> Private Shared Function ReadProcessMemory(hProcess As IntPtr, lpBaseAddress As IntPtr, lpBuffer As Byte(), dwSize As Integer, ByRef lpNumberOfBytesRead As Integer) As Boolean End Function <DllImport("user32.dll", SetLastError:=True)> Private Shared Function SetWindowsHookEx(idHook As Integer, lpfn As HookProc, hMod As IntPtr, dwThreadId As UInteger) As IntPtr End Function <DllImport("user32.dll", SetLastError:=True)> Private Shared Function UnhookWindowsHookEx(hhk As IntPtr) As Boolean End Function <DllImport("user32.dll")> Private Shared Function CallNextHookEx(hhk As IntPtr, nCode As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr End Function <DllImport("kernel32.dll", SetLastError:=True)> Private Shared Function CloseHandle(hObject As IntPtr) As Boolean End Function ' 钩子回调委托 Private Delegate Function HookProc(nCode As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr ' 钩子句柄与目标对象 Private hookHandle As IntPtr = IntPtr.Zero Private targetComboHwnd As IntPtr Private targetProcessHandle As IntPtr = IntPtr.Zero ' 存储原始顺序的项列表 Public OriginalItems As New List(Of String) Public Sub New(comboHwnd As IntPtr) targetComboHwnd = comboHwnd ' 获取目标进程ID和线程ID Dim processId As UInteger Dim threadId As UInteger = GetWindowThreadProcessId(comboHwnd, processId) ' 打开目标进程(获取内存读取权限) targetProcessHandle = OpenProcess(&H1F0FFF, False, processId) If targetProcessHandle = IntPtr.Zero Then Throw New Exception("无法打开目标进程,请确保程序以管理员身份运行") End If ' 安装线程钩子 hookHandle = SetWindowsHookEx(WH_CALLWNDPROC, AddressOf HookCallback, IntPtr.Zero, threadId) If hookHandle = IntPtr.Zero Then Throw New Exception("无法安装钩子,请检查目标线程是否存在") End If End Sub ' 停止跟踪并清理资源 Public Sub StopTracking() If hookHandle <> IntPtr.Zero Then UnhookWindowsHookEx(hookHandle) hookHandle = IntPtr.Zero End If If targetProcessHandle <> IntPtr.Zero Then CloseHandle(targetProcessHandle) targetProcessHandle = IntPtr.Zero End If End Sub ' 钩子回调函数 Private Function HookCallback(nCode As Integer, wParam As IntPtr, lParam As IntPtr) As IntPtr If nCode >= 0 Then ' 解析窗口消息结构体 Dim cwp As CWPSTRUCT = Marshal.PtrToStructure(Of CWPSTRUCT)(lParam) ' 过滤目标ComboBox的消息 If cwp.hWnd = targetComboHwnd Then Select Case cwp.message Case CB_ADDSTRING ' 读取目标进程中的字符串 Dim itemText = ReadStringFromProcess(cwp.lParam) If Not String.IsNullOrEmpty(itemText) Then OriginalItems.Add(itemText) End If Case CB_INSERTSTRING Dim itemText = ReadStringFromProcess(cwp.lParam) If Not String.IsNullOrEmpty(itemText) Then Dim insertIndex = cwp.wParam.ToInt32() ' 处理插入位置超出当前列表的情况 If insertIndex >= OriginalItems.Count Then OriginalItems.Add(itemText) Else OriginalItems.Insert(insertIndex, itemText) End If End If End Select End If End If ' 传递消息给下一个钩子 Return CallNextHookEx(hookHandle, nCode, wParam, lParam) End Function ' 从目标进程读取Unicode字符串 Private Function ReadStringFromProcess(strPtr As IntPtr) As String If strPtr = IntPtr.Zero Then Return "" Dim buffer As New List(Of Byte)() Dim bytesRead As Integer Dim tempBuffer(1) As Byte ' 每次读取2个字节(Unicode字符) Do If Not ReadProcessMemory(targetProcessHandle, strPtr + buffer.Count, tempBuffer, 2, bytesRead) Then Exit Do If bytesRead < 2 Then Exit Do ' 检查是否是字符串终止符(两个0字节) If tempBuffer(0) = 0 AndAlso tempBuffer(1) = 0 Then Exit Do buffer.AddRange(tempBuffer) Loop Return If(buffer.Count > 0, System.Text.Encoding.Unicode.GetString(buffer.ToArray()), "") End Function ' 窗口消息结构体定义 <StructLayout(LayoutKind.Sequential)> Private Structure CWPSTRUCT Public lParam As IntPtr Public wParam As IntPtr Public message As Integer Public hWnd As IntPtr End Structure End Class
使用示例
' 假设你已经获取到目标ComboBox的句柄ComboHwnd Dim tracker As ComboBoxOriginalOrderTracker = Nothing Try tracker = New ComboBoxOriginalOrderTracker(ComboHwnd) ' 等待目标程序完成项插入(这里示例等待5秒,实际可根据需求调整) System.Threading.Thread.Sleep(5000) ' 输出原始顺序的项 For Each item In tracker.OriginalItems Debug.WriteLine($"原始插入项:{item}") Next Finally ' 记得清理资源 tracker?.StopTracking() End Try
注意事项
- 权限要求:跨进程操作需要管理员权限,运行程序时右键选择「以管理员身份运行」。
- 时机问题:钩子需要在目标ComboBox插入项之前安装,否则无法捕获已经插入的项。如果目标程序启动时就完成了项插入,你需要在程序启动初期就挂载钩子。
- 编码兼容:如果目标程序是ANSI编码(老程序),需要把
ReadStringFromProcess中的Encoding.Unicode改成Encoding.Default。
内容的提问来源于stack exchange,提问作者ezio
相关产品推荐
相关产品推荐

