警告:
在我开始之前,人们可能会对您的其他问题中的项目感兴趣。
REF: How do I send/receive windows messages from VB6 and C#?
正如这里和您的其他帖子中所提到的,您真的应该重新考虑尝试使这项工作发挥作用。特别是,即使您对这里的技术很好,您的同事或其他可能必须维护您的代码的人也会陷入非常糟糕的时期。请注意,VB6 中的调试过程也非常艰难——经常保存并使用大量断点!如果您的代码中有错误,预计会发生很多崩溃。
此外,PostMessage 不应与此技术一起使用。这将需要更多的清理工作。
解决方案:
我附上了一个示例,它可以传回仅包含字符串和整数类型的结构。完成这项工作需要很多活动部件。我们将深入介绍从 C# 到 VB,因为这更棘手。一旦你知道如何做到这一点,反过来就不那么难了。
首先,在 C# 方面,您应该声明您的结构。在 C# 中封装结构实际上还不错。这是一个 COM 可见的 C# 示例类,它演示了如何包装结构。关键是在通话的对面使用 Marshal.StructureToPtr 和 Marshal.DestroyStructure。根据所涉及的类型,您甚至可能不必编写代码来映射类型。使用 MarshalAs 属性来标记 VB6 的正确映射。 MarshalAs 中使用的枚举中的大多数项对应于 VARIANT 和 COM 自动化中使用的不同变量类型。
这里使用的缓冲区由 HGlobal 支持,需要在调用结束后释放。也可以在这里使用 GCHandle,这也需要类似的清理。
MarshalAsAttribute Class @ MSDN
Marshal.StructureToPtr Method @ MSDN
Marshal.DestroyStructure Method @ MSDN
Marshal.AllocHGlobal Method @ MSDN
Marshal.FreeHGlobal Method @ MSDN
using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
namespace HostLibrary
{
public struct TestInfo
{
[MarshalAs(UnmanagedType.BStr)]
public string label;
[MarshalAs(UnmanagedType.I4)]
public int count;
}
[ComVisible(true)]
public interface ITestSender
{
int hostwindow {get; set;}
void DoTest(string someParameter);
}
[ComVisible(true)]
public class TestSender : ITestSender
{
public TestSender()
{
m_HostWindow = IntPtr.Zero;
m_count = 0;
}
IntPtr m_HostWindow;
int m_count;
#region ITestSender Members
public int hostwindow {
get { return (int)m_HostWindow; }
set { m_HostWindow = (IntPtr)value; } }
public void DoTest(string strParameter)
{
m_count++;
TestInfo inf;
inf.label = strParameter;
inf.count = m_count;
IntPtr lparam = IntPtr.Zero;
try
{
lparam = Marshal.AllocHGlobal(Marshal.SizeOf(inf));
Marshal.StructureToPtr(inf, lparam, false);
// WM_APP is 0x8000
IntPtr retval = SendMessage(
m_HostWindow, 0x8000, IntPtr.Zero, lparam);
}
finally
{
if (lparam != IntPtr.Zero)
{
Marshal.DestroyStructure(lparam, typeof(TestInfo));
Marshal.FreeHGlobal(lparam);
}
}
}
#endregion
[DllImport("user32.dll", CharSet = CharSet.Auto)]
extern public static IntPtr SendMessage(
IntPtr hwnd, uint msg, IntPtr wparam, IntPtr lparam);
}
}
在 VB6 方面,您需要设置一种机制来拦截消息。由于您的其他问题和其他地方已经介绍了详细信息,因此我将跳过子类化的主题。
要在 VB6 端展开结构,您需要按字段执行此操作,因为没有现成的机制可以取消引用指针值并将其转换为结构。幸运的是,您可以期望在 VB6 中字段成员在 4 字节边界上对齐,前提是您没有在 C# 中另外指定。这允许我们逐个字段地工作,将项目从一种表示映射到另一种表示。
首先,一些模块代码来完成所有的支持工作。以下是功能和注意事项。
TestInfo 类型 - 双方使用的结构的镜像定义。
CopyMemory - 一个可用于复制字节的 win32 函数。
ZeroMemory - 一个将内存重置为零字节值的 win32 函数。
除了这些项目之外,我们还利用 VB6 中未记录的 VarPtr() 函数来获取项目的地址。我们可以使用它来索引 VB6 端的结构。有关此功能的详细信息,请参见以下链接。
How to get the Address of Variables in Visual Basic @ support.microsoft.com
Public Const WM_APP As Long = 32768
Private Const GWL_WNDPROC = (-4)
Private procOld As Long
Type TestInfo
label As String
count As Integer
End Type
Private Declare Function CallWindowProc Lib "USER32.DLL" Alias "CallWindowProcA" _
(ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
Private Declare Function SetWindowLong Lib "USER32.DLL" Alias "SetWindowLongA" _
(ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Declare Sub CopyMemory Lib "KERNEL32.DLL" Alias "RtlMoveMemory" _
(ByVal pDst As Long, ByVal pSrc As Long, ByVal ByteLen As Integer)
Private Declare Sub ZeroMemory Lib "KERNEL32.DLL" Alias "RtlZeroMemory" _
(ByVal pDst As Long, ByVal ByteLen As Integer)
Public Sub SubclassWindow(ByVal hWnd As Long)
procOld = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf SubWndProc)
End Sub
Public Sub UnsubclassWindow(ByVal hWnd As Long)
procOld = SetWindowLong(hWnd, GWL_WNDPROC, procOld)
End Sub
Private Function SubWndProc( _
ByVal hWnd As Long, _
ByVal iMsg As Long, _
ByVal wParam As Long, _
ByVal lParam As Long) As Long
If hWnd = Form1.hWnd Then
If iMsg = WM_APP Then
Dim inf As TestInfo
' Copy First Field (label)
Call CopyMemory(VarPtr(inf), lParam, 4)
' Copy Second Field (count)
Call CopyMemory(VarPtr(inf) + 4, lParam + 4, 4)
Dim strInfo As String
strInfo = "label: " & inf.label & vbCrLf & "count: " & CStr(inf.count)
Call MsgBox(strInfo, vbOKOnly, "WM_APP Received!")
' Clear the First Field (label) because it is a string
Call ZeroMemory(VarPtr(inf), 4)
' Do not have to clear the 2nd field because it is an integer
SubWndProc = True
Exit Function
End If
End If
SubWndProc = CallWindowProc(procOld, hWnd, iMsg, wParam, lParam)
End Function
请注意,此解决方案需要发件人和收件人的合作。因为我们不希望两次释放字符串字段,所以我们在返回控制之前清空在 VB6 端制作的副本。如果您尝试为字段成员分配新值,此处将发生什么尚未定义,因此请避免编辑结构中的字段。
在映射字段中,C#中的UnmanagedType.BStr直接类似于VB6中的字符串。
UnmanagedType.I4 映射到 VB6 中的 Integer 和 Long。您在 UDT 中指定的其他字段也有等效项,尽管我不确定 VB6 中的 DateTime。
VB6 应用程序的其余部分(表单源代码)很简单。
Dim CSharpClient As New HostLibrary.TestSender
Private Sub Command1_Click()
CSharpClient.DoTest ("Hello World from VB!")
End Sub
Private Sub Form_Load()
CSharpClient.hostwindow = Form1.hWnd
Module1.SubclassWindow (Form1.hWnd)
End Sub
Private Sub Form_Unload(Cancel As Integer)
CSharpClient.hostwindow = 0
Module1.UnsubclassWindow (Form1.hWnd)
End Sub
现在,在将结构从 VB6 发送到 C# 时,您需要执行相反的操作。对于一些简单的结构,您甚至可以只发送结构本身的地址。如果需要按成员控制,可以通过GlobalAlloc获取合适的缓冲内存,然后用GlobalFree释放。对于每个字段,可以按照与从 C# 解包参数相同的方式执行成员复制。但是,调用后清理更简单。如果您使用了缓冲区,则只需将缓冲区中的内存清零,然后再将其交给 GlobalFree。
GlobalAlloc Function (Windows) @ MSDN
GlobalFree Function (Windows) @ MSDN
当消息到达 C# 端时,使用 Marshal.PtrToStructure() 将 IntPtr 映射到 .NET 结构中。
Marshal.PtrToStructure Method @ MSDN