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
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.
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.
[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.
Related
I'm re-visiting a tool that I wrote in VB.Net for my helpdesk team a while back and want to add a couple of checkboxes to replicate the same function that Windows uses to show hidden files and folders / re-hide, as well as protected operating system files.
I know I can do this by editing a registry entry and restarting explorer.exe, but that closes all open Explorer Windows and I don't want that.
Does anyone know how Windows is able to do this by a simple click of a checkbox and how I may be able to code it in VB.net?
Any input on this is greatly appreciated in advance.
EDIT: So it looks like I have found a refresh method that works to refresh Windows Explorer / File Explorer which can be applied to Drarig's answer below but I am having trouble converting it to VB.net as the original example is in C#.
'Original at http://stackoverflow.com/questions/2488727/refresh-windows-explorer-in-win7
Private Sub refreshExplorer(ByVal explorerType As String)
Dim CLSID_ShellApplication As Guid = Guid.Parse("13709620-C279-11CE-A49E-444553540000")
Dim shellApplicationType As Type = Type.GetTypeFromCLSID(CLSID_ShellApplication, True)
Dim shellApplication As Object = Activator.CreateInstance(shellApplicationType)
Dim windows As Object = shellApplicationType.InvokeMember("Windows", Reflection.BindingFlags.InvokeMethod, Nothing, shellApplication, New Object() {})
Dim windowsType As Type = windows.GetType()
Dim count As Object = windowsType.InvokeMember("Count", Reflection.BindingFlags.GetProperty, Nothing, windows, Nothing)
For i As Integer = 0 To CType(count, Integer)
Dim item As Object = windowsType.InvokeMember("Item", Reflection.BindingFlags.InvokeMethod, Nothing, windows, New Object() {i})
Dim itemType As Type = item.GetType()
'Only fresh Windows explorer Windows
Dim itemName As String = CType(itemType.InvokeMember("Name", Reflection.BindingFlags.GetProperty, Nothing, item, Nothing), String)
If itemName = explorerType Then
itemType.InvokeMember("Refresh", Reflection.BindingFlags.InvokeMethod, Nothing, item, Nothing)
End If
Next
End Sub
I am getting an exception Object reference not set to an instance of an object when I set itemType as Type = item.GetType() above. I can't figure out which object isn't being created. When I step through the code it looks like windowsType contains an object for windows. Does anyone have any idea on this? Once this is worked out I can then apply it to Drarig's solution below.
Alright I wish I could have got this to you sooner, but busy lately at work. I took a little time today to figure this out as I love digging into something I have not done before. This is the whole class from a new project; didn't have time to wrap it up in a separate class. I am sure this will get you what you need. It was a little harder than I thought as getting the correct handle and then send the command, but I got it. I hope you find it useful.
P.S. Some of the things you can leave out, specifically the boolean used for loading, this was so I can pull the current value back on load and either check/uncheck the CheckBox.
Note: This is tried and tested on Windows 7, 8 and 10
Imports Microsoft.Win32
Imports System.Reflection
Imports System.Runtime.InteropServices
Public Class Form1
<Flags()> _
Public Enum KeyboardFlag As UInteger
KEYBOARDF_5 = &H74
End Enum
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function GetWindow(ByVal hl As Long, ByVal vm As Long) As IntPtr
End Function
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function PostMessage(ByVal hWnd As IntPtr, ByVal Msg As UInteger, ByVal wParam As IntPtr, ByVal lParam As IntPtr) As Boolean
End Function
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function FindWindow(ByVal lpClassName As String, ByVal lpWindowName As String) As IntPtr
End Function
Private blnLoading As Boolean = False
Private Sub CheckBox1_CheckedChanged(sender As Object, e As EventArgs) Handles CheckBox1.CheckedChanged
Form1.HideFilesExtension(Me.CheckBox1.Checked)
If Not blnLoading Then NotifyFileAssociationChanged()
RefreshExplorer()
End Sub
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim name As String = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
Dim key As RegistryKey = Registry.CurrentUser.OpenSubKey(name, False)
blnLoading = True
Me.CheckBox1.Checked = CBool(key.GetValue("Hidden"))
key.Close()
blnLoading = False
End Sub
Private Shared Sub HideFilesExtension(ByVal Hide As Boolean)
Dim name As String = "Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
Dim key As RegistryKey = Registry.CurrentUser.OpenSubKey(name, True)
key.SetValue("Hidden", If(Hide, 1, 0))
key.Close()
End Sub
Public Shared Sub RefreshExplorer()
Dim clsid As New Guid("13709620-C279-11CE-A49E-444553540000")
Dim typeFromCLSID As Type = Type.GetTypeFromCLSID(clsid, True)
Dim objectValue As Object = Activator.CreateInstance(typeFromCLSID)
Dim obj4 As Object = typeFromCLSID.InvokeMember("Windows", BindingFlags.InvokeMethod, Nothing, objectValue, New Object(0 - 1) {})
Dim type1 As Type = obj4.GetType
Dim obj2 As Object = type1.InvokeMember("Count", BindingFlags.GetProperty, Nothing, obj4, Nothing)
If (CInt(obj2) <> 0) Then
Dim num2 As Integer = (CInt(obj2) - 1)
Dim i As Integer = 0
Do While (i <= num2)
Dim obj5 As Object = type1.InvokeMember("Item", BindingFlags.InvokeMethod, Nothing, obj4, New Object() {i})
Dim type3 As Type = obj5.GetType
Dim str As String = CStr(type3.InvokeMember("Name", BindingFlags.GetProperty, Nothing, obj5, Nothing))
If (str = "File Explorer") Then
type3.InvokeMember("Refresh", BindingFlags.InvokeMethod, Nothing, obj5, Nothing)
End If
i += 1
Loop
End If
End Sub
Public Shared Sub NotifyFileAssociationChanged()
'Find the actual window...
Dim hwnd As IntPtr = FindWindow("Progman", "Program Manager")
'Get the window handle and refresh option...
Dim j = GetWindow(hwnd, 3)
'Finally post the message...
PostMessage(j, 256, KeyboardFlag.KEYBOARDF_5, 3)
End Sub
End Class
Here's a solution for everything excepting the refreshing of the explorer.
I've translated the code, but I'm unable to find how to refresh the explorer/desktop without restarting it.
Const keyName As String = "HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
Const Hidden As String = "Hidden"
Const SHidden As String = "ShowSuperHidden"
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim St As Integer = GetRegValue(Hidden)
If St = 2 Then
SetRegValue(Hidden, 1)
SetRegValue(SHidden, 1)
Else
SetRegValue(Hidden, 2)
SetRegValue(SHidden, 0)
End If
End Sub
Private Function GetRegValue(valueName As String) As Integer
Return CInt(My.Computer.Registry.GetValue(keyName, valueName, 0))
End Function
Private Sub SetRegValue(valueName As String, value As Integer)
My.Computer.Registry.SetValue(keyName, valueName, value, Microsoft.Win32.RegistryValueKind.DWord)
End Sub
I have a few ideas to refresh the desktop :
Send a key to a running process. I tried this (source) :
Dim pp As Process() = Process.GetProcessesByName("explorer")
If pp.Length > 0 Then
For Each p In pp
AppActivate(p.Id)
SendKeys.SendWait("{F5}")
Next
End If
Refresh using SHChangeNotify (source),
Refresh broadcasting a WM_SETTINGCHANGE message (source),
etc.
I think you'll be forced to manually refresh or restart the explorer.
I have a scaling program used in our shipping department. The user scans a carton, it handles some database processing, and inserts a record identifying that carton into a "queue" table. The scale program also starts up a separate .exe which handles label processing (making calls to FedEx or UPS, or building custom ZPLs depending on what is needed, then sends to the printers attached to the machine). These PCs are Windows 7, if that makes a difference.
Occasionally a bad piece of data will cause an error in the label program. We recently discovered that one of our customers do not require a valid phone number when one of their customers place an order, and if the carton ends up shipping FedEx Home Delivery or SmartPost where a phone number is required, the API returns an error.
The print label program's error messages are popping up behind the scale program. Is there a way to force the error messages from the print label program to the top of any other open windows?
EDIT: I am calling the label program by first checking to see if there is a process running with the .exe's name. If not, it calls a launcher utility which looks on our app server to see if an updated exe is available, and copies to the c:\tmp directory if it is. Then runs the local c:\tmp copy of the program for that machine.
One way to accomplish this is to set your error form as a TopMost window. If you would always like the form to be above all other windows (including windows from other processes), add this to your form:
Private Const WS_EX_TOPMOST As Integer = &H00000008
Protected Overrides ReadOnly Property CreateParams() As CreateParams
Get
Dim cp As CreateParams = MyBase.CreateParams
cp.ExStyle = cp.ExStyle Or WS_EX_TOPMOST
Return cp
End Get
End Property
This code overrides CreateParams for the window, adding in the TOPMOST flag. You don't have to do anything after this - the window will always be topmost.
You can also use the p/Invoke SetWindowPos to make the window topmost on-demand:
<DllImport("user32.dll", SetLastError:=True)>
Private Shared Function SetWindowPos(ByVal hWnd As IntPtr, ByVal hWndInsertAfter As IntPtr, ByVal X As Integer, ByVal Y As Integer, ByVal cx As Integer, ByVal cy As Integer, ByVal uFlags As Integer) As Boolean
End Function
Private Shared ReadOnly HWND_TOPMOST As New IntPtr(-1)
Private Shared ReadOnly HWND_NOTOPMOST As New IntPtr(-2)
Private Const SWP_NOSIZE As Integer = &H1
Private Const SWP_NOMOVE As Integer = &H2
Public Sub SetWindowAsTopMost()
SetWindowPos(Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE Or SWP_NOSIZE)
End Sub
So, I've made an iterative Towers of Hanoi algorithm in Visual Basic, that runs in a while loop (recursion is slow in VB). The catch is it compiles okey, it even runs okey when launched through Visual Studio, but when launched though the Debug and Release generated execs the animation stops with the following message:
After a while, I just see all the pieces moved to the destination pole and the message disappears. So its not a crash per say, as the application is still running in the background, its just this message that pops out, ruining the animation. I just want my program to run just as it runs when launched directly from Visual Studio.
After a bit of thinking ...
I'm starting to believe this happens because Win7 treats the fact the application runs in a while loop as unresponsive (7 pieces in Towers of Hanoi ca take a while to rearrange), therefore it tries to close it.
How can I just make my application ignore Window's advertisements ?
I suggest that you do the calculation in the application idle event just like you do when creating a windows game. This way you ensure that the message queue is not blocked.
Public Class Form1
Public Sub New()
Me.InitializeComponent()
AddHandler Application.Idle, AddressOf Me.OnApplicationIdle
End Sub
Private Sub OnApplicationIdle(sender As Object, e As EventArgs)
Static rnd As New Random()
Dim message As MSG = Nothing
Do While (Not PeekMessage(message, IntPtr.Zero, 0, 0, 0))
'...
Me.BackColor = Color.FromArgb(rnd.Next(0, 256), rnd.Next(0, 256), rnd.Next(0, 256))
'...
Loop
End Sub
<DllImport("user32.dll", CharSet:=CharSet.Auto)> _
Friend Shared Function PeekMessage(<[In](), Out()> ByRef msg As MSG, ByVal hwnd As IntPtr, ByVal msgMin As Integer, ByVal msgMax As Integer, ByVal remove As Integer) As Boolean
End Function
<StructLayout(LayoutKind.Sequential)> _
Friend Structure MSG
Public hwnd As IntPtr
Public message As Integer
Public wParam As IntPtr
Public lParam As IntPtr
Public time As Integer
Public pt_x As Integer
Public pt_y As Integer
End Structure
End Class
How do I change the Window Title after starting something with Process.Start?
Dim myProc as Process
myProc = myProc.Start("NotePad.exe")
Unfortunately myProc.MainWindowTitle = "Fancy Notepad" doesn't work as this is read only. So how can it be done?
You can't change the window title using Process.MainWindowTitle because the property is readonly.
In order to change the window title you will firstly need to obtain a handle to target window and then instruct the Operating System to change the title of the window associated with that handle using the Win32 API function SetWindowsText like this
<DllImport("user32.dll")> _
Shared Function SetWindowText(ByVal hwnd As IntPtr, ByVal windowName As String) As Boolean
End Function
Once you have defined the function above you can proceed to manipulate the window title using the following code:
Dim process As New Process()
process.StartInfo.FileName = "notepad.exe"
process.Start()
Thread.Sleep(100)
SetWindowText(process.MainWindowHandle, "Fancy Notepad")
You need to wait a short few milliseconds before changing the window title otherwise the window title will not change.
You'll need to use Win32API call SetWindowText()
The VB.Net import:
<DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
Private Shared Function SetWindowText(ByVal hwnd As IntPtr, ByVal lpString As String) As Boolean
End Function
Usage example:
myProc.Start("notepad.exe")
'Note #1
SetWindowText(myProc.MainWindowHandle, "h4x3d title")
#1: you'll need to allow time for the process to start before trying to set the window text. If you set the text before the window is created, it will appear to do nothing. The easiest way is to thread sleep an arbitrary amount of time (eg 1 second). A better way would be to actively detect when the window has been created, but that's outside the scope of this question.
All the above fail for various reasons - no HWND can be found, or on slow PC's the sleep is not long enough. Call it like this. It retries until is reads back the title:
<DllImport("user32.dll")>
Shared Function SetWindowText(ByVal hwnd As IntPtr, ByVal windowName As String) As Boolean
End Function
SetWindowTextCall(SomeProcess.MainWindowHandle, "Name of Windows")
''' SetWindowTextCall is here to wrap the SetWindowtext API call. This call fails when there is no
''' hwnd as Windows takes its sweet time to get that. It has a counter to make sure we do not get stuck
''' </summary>
''' <param name="hwnd">Handle to the window to change the text on</param>
''' <param name="windowName">the name of the Window </param>
'''
Public Function SetWindowTextCall(hwnd As IntPtr, windowName As String) As Boolean
Dim status As Boolean = False
Dim WindowCounter As Integer = 0
While Not status
Try
Thread.Sleep(100)
status = SetWindowText(hwnd, windowName)
Catch ' can fail to be a valid window handle
Return False
End Try
WindowCounter = WindowCounter + 1
If WindowCounter > 200 Then ' 20 seconds
status = True
End If
End While
Return True
End Function
If you have ever noticed in the Task Manager, when you right-click on the running task, you have many options which include 'Minimize' and 'Maximize'. Is there anyway to do achieve this in vb?
Here is an example of the code you are looking for. It will loop through all the active processes and minimize all the windows.
In your app you will probably want to use something like Process.GetProcessesByName to find the specific window you want to manipulate.
Imports System.Runtime.InteropServices
Module ManipulateWindows
Const SW_HIDE As Integer = 0
Const SW_RESTORE As Integer = 1
Const SW_MINIMIZE As Integer = 2
Const SW_MAXIMIZE As Integer = 3
<DllImport("User32")> _
Private Function ShowWindow(ByVal hwnd As Integer, ByVal nCmdShow As Integer) As Integer
End Function
Public Sub Main()
'iterate through all the open processes.
For Each p As Process In Process.GetProcesses
'Get the Window Handle
Dim hWnd as integer = CType(p.MainWindowHandle, Integer)
'Write out the title of the main window for the process.
System.Console.WriteLine(p.MainWindowTitle)
'Minimize the Window
ShowWindow(hWnd, SW_MINIMIZE)
Next p
End Sub
End Module