Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations strongm on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

can GUI respond to events in another program? 9

Status
Not open for further replies.

ctlin

Technical User
Apr 17, 2002
77
US
i'm trying to build a front end for a fortran program. you guys helped me get info going from VB into the fortran program yesterday... is there anyway it can go the other way?
is there such thing as having a Fortran program trigger a system event at certain points during execution and VB responding to those? what i need is a way for VB to know when it needs to send input to Fortran. Thanks in advance
 
Just a short time to go 'til I'm back in front of the right PC...
 
Ok - first off just the code (with a few comments thrown in for luck...) I should note that this code suffers from similar shortcomings to the wshSell solution proposed much earlier in this thread
Create a form with a textbox and a command button on it.
Drop in following code:
[tt]
Option Explicit

Private Sub Command1_Click()


mvarConsoleAllocated = False ' Global flag indicating whether VB has a console allocated or not
InheritableConsole "c:\consoleiotest.bat" ' Give VB a console, and launch the named process

Sleep 500 ' Give new console a chance to respond
Text1.Text = ReadIOConsole ' read any output from console
WriteIOConsole "" ' Effectively just presses a key
Sleep 500 ' Give console a chance to respond
Text1.Text = Text1.Text + ReadIOConsole ' get any additional output from console
CloseConsole ' Free the VB console (which will also close it)

End Sub


Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
If mvarConsoleAllocated = True Then CloseConsole
End Sub
[/tt]
Create a module and drop in the following:
[tt]
Option Explicit

' Console allocation and deallocation
Public Declare Function AllocConsole Lib "kernel32" () As Long
Public Declare Function FreeConsole Lib "kernel32" () As Long

Public Declare Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long)

' Declarations for standard handle functions
Private Declare Function SetStdHandle Lib "kernel32" (ByVal nStdHandle As Long, ByVal nHandle As Long) As Long
Private Declare Function GetStdHandle Lib "kernel32" (ByVal nStdHandle As Long) As Long
Private Declare Function DuplicateHandle Lib "kernel32" (ByVal hSourceProcessHandle As Long, ByVal hSourceHandle As Long, ByVal hTargetProcessHandle As Long, lpTargetHandle As Long, ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwOptions As Long) As Long

Private Declare Function GetCurrentProcess Lib "kernel32" () As Long

' Declarations for standard streams
Private Const STD_ERROR_HANDLE = -12&
Private Const STD_INPUT_HANDLE = -10&
Private Const STD_OUTPUT_HANDLE = -11&

Private Const DUPLICATE_SAME_ACCESS = &H2

' Declarations for all our pipe handles
Private OldStdOut As Long
Private OldStdIn As Long
Private OldStdErr As Long

Private hInReadPipe As Long
Private hInWritePipe As Long
Private hInWritePipeDup As Long

Private hOutReadPipe As Long
Private hOutWritePipe As Long
Private hOutReadPipeDup As Long

Private hErrorReadPipe As Long
Private hErrorWritePipe As Long
Private hErrorReadPipeDup As Long

Private Declare Function CreatePipe Lib "kernel32" (phReadPipe As Long, phWritePipe As Long, lpPipeAttributes As SECURITY_ATTRIBUTES, ByVal nSize As Long) As Long
Private Declare Function ReadFile Lib "kernel32" (ByVal hFile As Long, lpBuffer As Any, ByVal nNumberOfBytesToRead As Long, lpNumberOfBytesRead As Long, ByVal lpOverlapped As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
Private Declare Function WriteFile Lib "kernel32" (ByVal hFile As Long, ByVal lpBuffer As String, ByVal nNumberOfBytesToWrite As Long, lpNumberOfBytesWritten As Long, ByVal lpOverlapped As Long) As Long

Public Declare Function ReadConsole Lib "kernel32" Alias "ReadConsoleA" (ByVal hConsoleInput As Long, lpBuffer As Any, ByVal nNumberOfCharsToRead As Long, lpNumberOfCharsRead As Long, ByVal lpReserved As Long) As Long

Public Type OVERLAPPED
Internal As Long
InternalHigh As Long
offset As Long
OffsetHigh As Long
hEvent As Long
End Type

Private Type SECURITY_ATTRIBUTES
nLength As Long
lpSecurityDescriptor As Long
bInheritHandle As Long
End Type

Public mvarConsoleAllocated As Boolean

Public Sub InheritableConsole(strCommand As String)
Dim result As Long
Dim OL As OVERLAPPED
Dim SA As SECURITY_ATTRIBUTES

SA.nLength = Len(SA)
SA.bInheritHandle = True
SA.lpSecurityDescriptor = 0&

OL.offset = 0
OL.OffsetHigh = 0
OL.hEvent = vbNull

AllocConsole ' gives VB STDIN, STDOUT and STDERR

mvarConsoleAllocated = True

' Create three anonomous pipes
result = CreatePipe(hInReadPipe, hInWritePipe, SA, 0&)
result = CreatePipe(hOutReadPipe, hOutWritePipe, SA, 0&)
result = CreatePipe(hErrorReadPipe, hErrorWritePipe, SA, 0&)

' retain just in case (we don't actually use these in this example)
OldStdIn = GetStdHandle(STD_INPUT_HANDLE)
OldStdOut = GetStdHandle(STD_OUTPUT_HANDLE)
OldStdErr = GetStdHandle(STD_ERROR_HANDLE)

' Create uninheritable duplicates of one end of our three pipes
result = DuplicateHandle(GetCurrentProcess, hInWritePipe, GetCurrentProcess, hInWritePipeDup, 0&, False, DUPLICATE_SAME_ACCESS)
CloseHandle hInWritePipe
result = DuplicateHandle(GetCurrentProcess, hOutReadPipe, GetCurrentProcess, hOutReadPipeDup, 0&, False, DUPLICATE_SAME_ACCESS)
CloseHandle hOutReadPipe
result = DuplicateHandle(GetCurrentProcess, hErrorReadPipe, GetCurrentProcess, hErrorReadPipeDup, 0&, False, DUPLICATE_SAME_ACCESS)
CloseHandle hErrorReadPipe

' Ok, now patch in our redirections to standard streams
result = SetStdHandle(STD_OUTPUT_HANDLE, hOutWritePipe)
result = SetStdHandle(STD_INPUT_HANDLE, hInReadPipe)
result = SetStdHandle(STD_ERROR_HANDLE, hErrorWritePipe)

' Launch our console app, using our defined pipes for STDIN, STDOUT and STDERR
StartProcess strCommand, hInReadPipe, hOutWritePipe, hErrorWritePipe

End Sub

Public Sub WriteIOConsole(strString As String)
Dim result As Long
Dim ConsoleOutBuffer As String
Dim cbRead As Long
ConsoleOutBuffer = strString + vbCrLf
cbRead = Len(ConsoleOutBuffer)
result = WriteFile(hInWritePipeDup, ByVal ConsoleOutBuffer, cbRead, cbRead, 0&) ' Write to child's StdIn
End Sub

Public Sub CloseConsole()
FreeConsole
End Sub


Public Function ReadIOConsole() As String
Dim result As Long
Dim ConsoleOutBuffer As String
Dim cbRead As Long
Dim cbBytesToRead As Long
Dim OL As OVERLAPPED

OL.offset = 0
OL.OffsetHigh = 0
OL.hEvent = vbNull
ConsoleOutBuffer = Space(1024)
cbRead = Len(ConsoleOutBuffer)
result = ReadFile(hOutReadPipeDup, ByVal ConsoleOutBuffer, Len(ConsoleOutBuffer), cbRead, 0&) ' read contents of child's StdOut
If result Then
ReadIOConsole = Left(ConsoleOutBuffer, cbRead)
End If
End Function

' Creates a temporary test batch file to interact with (has output and input)
Public Sub MakeBatchFile()
Dim hFile As Long

hFile = FreeFile

Open "c:\consoleiotest.bat" For Output As hFile
Print #hFile, "@echo off"
Print #hFile, "echo This is a demonstration of VB interaction with a console"
Print #hFile, "pause"
Print #hFile, "echo If you can read this line, then VB successfully interacted with a console app"
Print #hFile, "echo All done!"
Close #hFile
End Sub

' get rid of out temporary batch file
Public Sub KillBatchFile()
Kill "c:\consoleiotest.bat"
End Sub
[/color blue][/tt]
Add one additional module (just for a helper function) and drop in the following:
[tt]
Option Explicit

Private Type STARTUPINFO
cb As Long
lpReserved As String
lpDesktop As String
lpTitle As String
dwX As Long
dwY As Long
dwXSize As Long
dwYSize As Long
dwXCountChars As Long
dwYCountChars As Long
dwFillAttribute As Long
dwFlags As Long
wShowWindow As Integer
cbReserved2 As Integer
lpReserved2 As Long
hStdInput As Long
hStdOutput As Long
hStdError As Long
End Type

Private Type PROCESS_INFORMATION
hProcess As Long
hThread As Long
dwProcessID As Long
dwThreadID As Long
End Type

Public Type PROCESS_INFORMATION_EXT
hProcess As Long
hThread As Long
hwnd As Long
dwProcessID As Long
dwThreadID As Long
End Type

Private Const NORMAL_PRIORITY_CLASS = &H20

Private Declare Function CreateProcessA Lib "kernel32" (ByVal lpApplicationName As String, ByVal lpCommandLine As String, ByVal lpProcessAttributes As Long, ByVal lpThreadAttributes As Long, ByVal bInheritHandles As Long, ByVal dwCreationFlags As Long, ByVal lpEnvironment As Long, ByVal lpCurrentDirectory As String, lpStartupInfo As STARTUPINFO, lpProcessInformation As PROCESS_INFORMATION) As Long

Private Const INFINITE = &HFFFF
Private Declare Function WaitForInputIdle Lib "user32" (ByVal hProcess As Long, ByVal dwMilliseconds As Long) As Long


Public Function StartProcess(strProgram As String, hStdIn As Long, hStdOut As Long, hStdErr As Long) As Long 'PROCESS_INFORMATION_EXT
Dim piProcess As PROCESS_INFORMATION
Dim siStartUp As STARTUPINFO
Dim lResult As Long

siStartUp.hStdInput = hStdIn
siStartUp.hStdOutput = hStdOut
siStartUp.hStdError = hStdErr
lResult = CreateProcessA(vbNullString, strProgram, 0&, 0&, 1&, NORMAL_PRIORITY_CLASS, 0&, vbNullString, siStartUp, piProcess)
WaitForInputIdle piProcess.hProcess, INFINITE 'Let it initialise properly before continuing
StartProcess = lResult
End Function
[/tt]
Right, any questions?
 
wow, it's going to take me a while to go through and try to understand all this. thanks very much - i'm sure i will have questions about it soon.
 
holy jeez... this is a whole 'nother level of programming that i don't get. somebody chose the red pill... (;

oh and just for your peace of mind, i'll credit you in the code if i use it(although the application is purely non-commercial)

a million thank yous.
 
hmm i am having problems running it- i took out the '[/color blue]' tag left in the post.
when i run it, the form opens a command window, but the only thing that happens in that window is a blinking cursor and the form window never refreshes after that. perhaps it's because i am running WinNT 4? anyways, i will try to figure it out- please feel free to move on to other projects ( ;
 
My fault - forgot to mention that you need to call MakeBatchFile (you can do this from the immediate window) first, otherwise you just get a blank console as you have found. Oh, you should also set the textbox's multiline property to true.

Note that this is not an industrial-strength programme, merely a proof of concept - so there's no error checking or anything...

(I've had the code working successfully on W98 and NT4)
 
works! thanks! now to adapt it... muhahaah
 
Excellent - now all that is outstanding is a code walkthrough...
 
the general flow of the program i get thanks to the commenting, but the individual calls and variables are a bit overwhelming.
 
The Commentary

Assumption
A console application has three standard streams, STDIN, STDOUT and STDERR which they use to get input from, and send output and error messages to (this is generally true, although plenty of console applications use other techniques for this which bypass the streams entirely. The program commented on here will only work reliably for those appas that stick to using the streams.

A Coupe of Comments
1. A processes STDIN stream is read-only, whilst its STDOUT and STDERR streams are write-only.
2. We want to do is write to a processes STDIN (thus providing it with input), and read from its STDOUT (thus grabbing its output); potentially we may also want to read from its STDERR, but this is essentially the same case as reading STDOUT so we won't bother considering it seperately.

From points 1 and 2 above we can see there is a problem - even if we can somehow get a reference to a given processes standard streams, the file mode on them would be wrong. So we need to figure out some way of getting hold of the other end of those streams.

Given the above, the full challenge so far is to get hold of a processes standard streams, and then to find the other end of those streams.

Now for a little bit of Windows inside information: when one process (the parent) launches another (the child) that child process, by default, inherits the standard streams of the parent. So one approach to resolving the problem of getting hold of the a processes standard streams might be to actually get hold of the handles of the standard streams of our own process (in this case our VB program), and launching the other process as a child, which inherits streams whose handles we have already got. And the Win32 API actually provides a nice function for getting those handles: [tt]GetStandardHandle()[/tt]

So some pseudocode at this stage might be:

Use GetStandardHandle to return handles for STDIN, STDOUT, STDERR for current process
Launch child process, which will inherit our STDIN, STDOUT, STDERR for which we have file handles
Read and write stuff using the handles

Excellent - except for two problems. The first problem, which we noted above, is that the file modes on the streams are wrong. The second is that our parent process needs to have STDIN, STDOUT and STDERR streams in the first place for us to be able to successfully use [tt]GetStandardHandle[/tt], and unfortunately VB itself is a GUI application, rather than a console application, which in turn means that whenever a VB application is launched the underlying startup code actually deletes the standard streams; in other words a VB application does not have any standard streams.

Let's solve this second problem first. Is there any way of giving a VB application back the standard streams that it has had removed? It turns out that there is. [tt]AllocConsole[/tt] does precisely that (and is matched by [tt]FreeConsole[/tt] for neatness sake.

So now our pseudocode might look like this:

Use AllocConsole to give our VB process the standard streams
Use GetStandardHandle to return handles for STDIN, STDOUT, STDERR for current process
Launch child process, which will inherit our STDIN, STDOUT, STDERR for which we have file handles
Read and write stuff using the handles
Use FreeConsole to clean up

Right, the remaining problem is the file access mode. We don't have a way of directly changing this - but what we can do is create some pipes. A pipe is essentially a special FIFO file which has two handles, one for reading and one for writing. If you pump data in through the writeable end you can then suck it out at the readable end; they are often used for interprocess communication. You can create a pipe by using the [tt]CreatePipe[/tt] API call.
Now, if there was someway of attaching the standard streams to the appropriate end of our pipe (eg get STDIN to actually point to the readable end of a pipe, which means that anything we write to the writeable end of the pipe is subsequently squirted to STDIN via the readable end...hmm, that kind of makes sense) we could provide a mechanism by which we end up with the ability to write to STDIN and read from STDOUT via the pipes. And once again, it turns out that there is a nice Win32 API function for this: [tt]SetStdHandle()[/tt]

Pseudocode time again:

Use AllocConsole to give our VB process the standard streams
Use GetStandardHandle to return handles for STDIN, STDOUT, STDERR for current process
Use CreatePipe to creae three pipes, one each for STDIN, STDOUT, STDERR
Use SetStdHandle to redirect STDIN, STDOUT and STDERR through the pipes we created
Launch child process, which will inherit our redirected STDIN, STDOUT, STDERR for which we have pipe file handles
Read and write stuff using the pipe file handles, rather than direct handles to STDIN, STDOUT, STDERR, which give us the ability to write to child processes STDIN, and read from child processes STDOUT (hey, hey! That was the goal!)
Use FreeConsole to clean up

And that, in a nutshell, is how the program works. There is a little additional jiggery-pokery going on (some in-process duplication of some of the pipe handles, those duplicates being non-inheritable by the child), and the use of CreateProcess to launch our child process which actually allows us to be specific about which handles are inherited (it alsoconveniently allows us to launch the process synchronously, and fills in a PROCESS_INFORMATION structure for us in case we are interested in other information about the launched process)


 
very well written. i'm sure i will be coming back to your code and writeup in the future (not just for this project) and i hope other people find it as useful as i have. thanks and best of luck.
 
Strongm, I'm having problems making your code work on Win98 machines. It causes exceptions that leave the systems in an unstable state.

Any thoughts on a work-around?
VCA.gif
 
Do the erros occur in the IDE? Or in compiled programs?

Note that I did say "this is not an industrial-strength programme, merely a proof of concept - so there's no error checking or anything..."

One issue for example is that if you exit the console manually (eg using it's close button) you can definitely leave the system in an unstable state. There are some console API calls that can eliminate/reduce this.

 
It crashes the IDE with an invalid page exception. I wasn't able to re-open the project because VB reported it was already open. That is probably why I didn't notice this peculiar behavior: executing the compiled program results in the same fault (at the same location - 0028:0009). After terminating "winoldap" (not responding) using the close program dialog, your program will run without error. It always crashes the first time it executes and then runs correctly thereafter.

I could envision a hasty way to work around the problem if it generated a trappable error... but I wouldn't know how to trap a "page exception". And the Windows Explorer invariably dies within minutes of the program's first execution.

Thanks for the response.
VCA.gif
 
>terminating "winoldap"

There should be a difference here if you are running under W9x/Me and NT4/W2x, because of what a command line actually is. i.e. under W9x/Me you basically have no option; you get "command.com", which is actually 16-bit. Under NT4/W2k and up you also have the option of cmd.exe, which is 32-bit.

Now AllocConsole doesn't know the difference - and under W9x etc you get the old 16-bit version (and hence winoldapp) an dunder NT4 ans later you get cmd.exe, which is a genuine 32-bit console.

In BOTH cases you have the problem that VB is NOT a console application, and therefore in a real world situation you have to be very careful about how/when you close the console of a console application.
 
Hi, does VB 6 seems to lack ways to deal with console type programs. Does VB.net make it easier to deal with console programs? For anyone that are uins VB.net ofcourse.
 
strongm, is sending a return keystroke, WriteIOConsole "{Enter}" in this program?
Thanks.
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top