简体   繁体   中英

How do I send a struct from C# to VB6, and from VB6 to C#?

I need to send a struct from C# to a VB6 app, modify the data in VB6, and send the result back via windows messaging. How do I do this?

I am able to send basic ints back and forth with PostMessage (using DllImport in C# and registering the vb6 app with windows messaging), but need to send more structured data, consisting of strings, ints, and decimal.

Looking for the easiest solution to implement passing of structures data back and forth.

Basic sample of VB6 type

Public Type udtSessionData
    SessionID As Integer
    SessionName As String
    MinVal As Currency
    PctComplete As Double
    NVal As Integer
    ProcessedFlag As Boolean
    ProcessedDate As Date
    Length As Integer
End Type

You have to assign GUIDs and use the MarshalAs attribute. .NET COM Interop handles the translation. Not too much different than a class. This series of posts illustrates what you need to do.

通过在.NET上使用P / Invoke并在VB6中导入CopyMemory,您可以完成这项工作,但这对维护造成了很大的影响,我建议您使用类似的方法运行。

Caveat:

Before I begin, people may be interested in noting items from your other question.

REF: How do I send/receive windows messages from VB6 and C#?

As mentioned here and in your other post, you should really reconsider trying to make this work. In particular, even if you are fine with the techniques here, your coworkers or other people who may have to maintain your code will be in for a really bad time. Note that the debugging process is also very tough in VB6- save often and use lots of breakpoints! Expect lots of crashes if there are errors in your code.

Also, PostMessage should not be used with this technique. It will require a lot more cleanup.

Solution:

I've enclosed a sample that can pass back a structure containing only a string and integer type. Making this work requires a lot of moving parts. We'll cover going from C# to VB in depth, as that is more tricky. The reverse is not as hard once you know how to do this.

First, on the C# side, you should declare your structure. Packaging up the structure is actually not bad in C#. Here is a C# sample class that is COM-visible that demonstrates how to wrap the structure up. The key is to use Marshal.StructureToPtr and Marshal.DestroyStructure on opposing sides of your call. Depending on the types involved, you may not even have to write code to map types, either. Use the MarshalAs attribute to flag the correct mappings for VB6. Most of the items in the enum used in MarshalAs correspond to different variable types used in VARIANTs and COM automation.

The buffer that is used here is supported by a HGlobal, which needs to be freed after the call is over. It also may be possible to use GCHandle here, which also requires similar cleanup.

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);
    }
}

On the VB6 side, you will need to setup a mechanism to intercept messages. As the details are covered in your other question and in other places, I will skip over the topic of subclassing.

To unwrap a struct on the VB6 side, you will need to do this per-field, as there is no mechanism readily available to dereference a pointer value and cast it into a structure. Fortunately, you can expect field members to be aligned on 4-byte boundaries in VB6, provided you haven't specified otherwise in C#. This allows us to work field by field, mapping items from one representation to the other.

First, some module code to do all the support work. Here are functions and items of note.

TestInfo type - A mirror definition of the structure used on both sides.
CopyMemory - A win32 function that may be used to copy bytes.
ZeroMemory - A win32 function that resets memory to zero byte values.

In addition to those items, we make use of the undocumented VarPtr() function in VB6 to get the address of items. We can use this to index into the structure on the VB6 side. See the following link for details on this function.

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

Note that this solution requires the cooperation of the sender and the recipient. Because we do not wish to free the string field twice, we empty out the copy made on the VB6 side before returning control. It is undefined what will happen here if you attempt to assign a new value to field members, so avoid editing fields in the structure.

In mapping fields, UnmanagedType.BStr in C# is directly analogous to string in VB6.
UnmanagedType.I4 maps to Integer and Long in VB6. The other fields you have specified in your UDT also have equivalents, although I am unsure about DateTime in VB6.

The remainder of the VB6 app (Form source code) is straightforward.

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

Now, in sending a structure from VB6 to C#, you need to do the reverse. For some simple structures, you may even be able to send just the address of the structure itself. If you need memberwise control, you can obtain suitable buffer memory by using GlobalAlloc, and then release it with GlobalFree. Memberwise copies may be performed the same way as parameters were unwrapped from C#, for each field. However, cleanup is simpler after the call. If you used a buffer, you only need to zero out the memory in the buffer before handing it over to GlobalFree.

GlobalAlloc Function (Windows) @ MSDN
GlobalFree Function (Windows) @ MSDN

When the message arrives on the C# side, use Marshal.PtrToStructure() to map an IntPtr into a .NET structure.

Marshal.PtrToStructure Method @ MSDN

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM