SendMessage between WinForms Applications - form needs focus

4.2k views Asked by At

I'm using the Windows Messages API to communicate between two Windows Forms Apps. I took out a form that was a part of the application and made it into it's own app so that when it is loading the user can still work on the other forms now in a separate app from the one form.

I need to be able to communicate between the two apps so that the now separate app form can tell the main app what to open.

I use this code on the main form of the main app and it works great... Except when the main form doesn't have focus.

Protected Overrides Sub WndProc(ByRef m As System.Windows.Forms.Message)
    Try
        Select Case m.Msg
            Case &H400
                BS.BuildString(m.LParam)
            Case Else
                MyBase.WndProc(m)
        End Select
    Catch ex As Exception

    End Try
End Sub

I've never used the Windows Message API before, I just grabbed this from looking up how to communicate between forms, so I'm new at this. What I do know from doing some more research is that I need a Message only Window to handle the messages. I don't understand how to do this. I've looked at a few articles and solutions like this one and I think the problem I'm having is that I don't know how to implement it.

Edit 1

Here is how the second app finds and sends to the main app.

                'used to send a message using SendMessage API to the 
                'main app to open the ID
                Private WithEvents BS As New BuildString 
                'get this running process
                Dim proc As Process = Process.GetCurrentProcess()
                'get all other (possible) running instances
                Dim processes As Process() = Process.GetProcessesByName("ProcessName")

                If processes.Length > 0 Then
                    'iterate through all running target applications
                    For Each p As Process In processes
                        'now send the ID to the running instance of the main app
                        BS.PostString(p.MainWindowHandle, &H400, 0, "ID:" & ID)
                    Next
                Else
                    MessageBox.Show("Main application not running")
                End If

Here's the class for the BuildString that is in both apps.

Imports System.Text

Public Class BuildString

Private Declare Function PostMessage Lib "user32.dll" Alias "PostMessageA" (ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As Integer) As Integer
Public Event StringOK(ByVal Result As String)
Private hwnd As Integer = 0
Private wMsg As Integer = 0
Private wParam As Integer = 0
Private lParam As String = ""
Private tempA(-1) As Byte
Private enc As Encoding = Encoding.UTF8

Public Property Encode() As Encoding
    Get
        Return enc
    End Get
    Set(ByVal value As Encoding)
        enc = value
    End Set
End Property

Public Sub BuildString(ByVal b As IntPtr)
    If b <> 0 Then
        'build temp array
        Dim tempB(tempA.Length) As Byte
        tempA.CopyTo(tempB, 0)
        tempB(tempA.Length) = b
        ReDim tempA(tempB.Length - 1)
        tempB.CopyTo(tempA, 0)
    Else
        'decode byte array to string
        Dim s As String
        If enc Is Encoding.UTF8 Then
            s = Encoding.UTF8.GetString(tempA)
        ElseIf enc Is Encoding.Unicode Then
            s = Encoding.Unicode.GetString(tempA)
        ElseIf enc Is Encoding.ASCII Then
            s = Encoding.ASCII.GetString(tempA)
        Else
            s = Encoding.Default.GetString(tempA)
        End If
        'send out result string via event
        RaiseEvent StringOK(s)
        ReDim tempA(-1)
    End If
End Sub

Public Sub PostString(ByVal hwnd As Integer, ByVal wMsg As Integer, ByVal wParam As Integer, ByVal lParam As String)
    Me.hwnd = hwnd
    Me.wMsg = wMsg
    Me.wParam = wParam
    Me.lParam = lParam
    'create a new thread to post window message
    Dim t As Threading.Thread
    t = New Threading.Thread(AddressOf SendString)
    t.Start()
End Sub

Private Sub SendString()
    'create byte array
    Dim ba() As Byte
    'encode string to byte array
    If enc Is Encoding.UTF8 Then
        ba = Encoding.UTF8.GetBytes(lParam)
    ElseIf enc Is Encoding.Unicode Then
        ba = Encoding.Unicode.GetBytes(lParam)
    ElseIf enc Is Encoding.ASCII Then
        ba = Encoding.ASCII.GetBytes(lParam)
    Else
        ba = Encoding.Default.GetBytes(lParam)
    End If
    Dim i As Integer
    For i = 0 To ba.Length - 1
        'start post message
        PostMessage(hwnd, wMsg, wParam, ba(i))
    Next
    'post a terminator message to destination window
    PostMessage(hwnd, wMsg, wParam, 0)
End Sub

End Class

Here is the code on the main app that is run once the message gets decoded.

Private Sub SB_StringOK(ByVal Result As String) Handles BS.StringOK
    Dim sArray() As String = Result.Split(";")
    'rest of the code to open the ID
End Sub
3

There are 3 answers

2
Keevan On BEST ANSWER

Thanks to @JustinRyan I figured it out using FindWindow.

I added this to my second application that send the message.

Private Declare Function FindWindow1 Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As Integer

Instead of finding the application process I find the window I want and send it directly to it in the send message.

Dim parenthwnd As Integer = FindWindow1(Nothing, "Form Name")
BS.PostString(parenthwnd, &H400, 0, "ID:" & ID)

It works just as I needed.

I'm not sure of how else to show the correct answer since the answer was in the comments. Please let me know of the proper etiquette in this situation to give the user his due credit.

0
ChaosOverlord On

Instead of using this API, you could try using a new thread and having your second form created on this second thread, so both forms run on separate threads and can still be part of the same project

Dim Form2Thread As New System.Threading.Thread(Address of MethodName)


Sub Form1Load() Handles Form1.Shown
 Form2Thread.Start()
End Sub
Sub MethodName()
 'All your form creation code here
 'Form2.Show()
End Sub

This is simple enough, but the downside is you can not directly edit a control or property of Form2 from a method running on your original thread. All changes to Form2 have to be made through your second thread.

(There is an exception to this but things start to get more complicated, search for how to do cross-thread operations)

The other solution is to use a Background Worker component. Plenty of tutorials around on using those.

0
Justin Ryan On

[To add a bit more information:]

According to Process.MainWindowHandle,

The main window is the window opened by the process that currently has the focus (the TopLevel form). You must use the Refresh method to refresh the Process object to get the current main window handle if it has changed.

TopLevel is defined here (as linked from MainWindowHandle) as,

A top-level form is a window that has no parent form, or whose parent form is the desktop window. Top-level windows are typically used as the main form in an application.

This would explain why messages are being sent elsewhere when the form is inactive. So, while using the Process properties may work for getting the window handle of single form applications, this makes it unreliable otherwise.

Coincidentally, FindWindow also states,

Retrieves a handle to the top-level window whose class name and window name match the specified strings. This function does not search child windows.

However, FindWindow (and FindWindowEx) is agnostic to focus (activation) and will therefore return a specific window's handle without concern of whether or not it is on top.