Execute

.


CreateProcess

It is recommended to use the CreateProcess procedure instead WinExec. This is a very low-level procedure, it gives complete control over processes, threads and more.
CreateProcess takes 10 parameters, two of which are pointers to structures containing further parameters. Most parameters are not very interesting for our needs, so I have made a function CreateProcess that only takes the three most important params. The function is described in winfunc.p and can be called like in this example:

  {windows.i}
  DEFINE VARIABLE hProcess AS INTEGER NO-UNDO. 
  hProcess = CreateProcess("notepad.exe c:\config.sys",
                           "",
                           1). 
  IF hProcess=0 THEN
     ShowLastError().
  RUN CloseHandle IN hpApi (hProcess, OUTPUT ReturnValue).

The first parameter is the command line with optional parameters. The second parameter is the working directory for the new process, where "" will result in the current working directory of the Progress application. The third parameter is cmdShow with the same meaning as in WinExec.
The return value is a handle to the new process or 0 if the call failed.

notes

The GetLastError procedure did not work well in Progress 8, but it is fine now.

Do not forget to call CloseHandle. MS-Windows keeps a reference count to each kernel object, a process is a kernel object. A kernel object can not be destroyed when its reference count is larger than zero. Run CloseHandle decrements the reference count by one which only tells the kernel that you are not interested in keeping the new process alive. The process itself also has at least one reference to itself, so this CloseHandle call will not actually terminate the process. Forgetting to call CloseHandle will result in resource loss.

returning a PID

The following procedure also uses CreateProcess, but this time it returns a PID (process identifier) instead of a process handle.
This can be useful because procedure KillProcess expects a PID as input parameter, see terminate a process gently.

{windows.i}
 
PROCEDURE MakeProcess :
  DEFINE INPUT  PARAMETER CommandLine AS CHARACTER    NO-UNDO.
  DEFINE INPUT  PARAMETER WorkingDir  AS CHARACTER    NO-UNDO.
  DEFINE OUTPUT PARAMETER PID         AS INTEGER NO-UNDO.
 
  DEFINE VARIABLE wShowWindow AS INTEGER NO-UNDO INITIAL 0.
  DEFINE VARIABLE bResult     AS INTEGER NO-UNDO.
  DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO.
 
   DEFINE VARIABLE lpStartupInfo AS MEMPTR.
   SET-SIZE(lpStartupInfo)     = 68.
   PUT-LONG(lpStartupInfo,1)   = 68.
   PUT-LONG (lpStartupInfo,45) = 1. /* = STARTF_USESHOWWINDOW */
   PUT-SHORT(lpStartupInfo,49) = wShowWindow.
 
   DEFINE VARIABLE lpProcessInformation AS MEMPTR.
   SET-SIZE(lpProcessInformation)   = 16.
 
   DEFINE VARIABLE lpWorkingDirectory AS MEMPTR.
   IF WorkingDir NE "" THEN DO:
      SET-SIZE(lpWorkingDirectory)     = 256.
      PUT-STRING(lpWorkingDirectory,1) = WorkingDir.
   END.   
 
   RUN CreateProcessA IN hpApi
     ( 0,
       CommandLine,
       0,
       0,
       0,
       0,
       0,
       IF WorkingDir=""
          THEN 0 
          ELSE GET-POINTER-VALUE(lpWorkingDirectory),
       GET-POINTER-VALUE(lpStartupInfo),
       GET-POINTER-VALUE(lpProcessInformation),
       OUTPUT bResult
     ).
 
  IF bResult=0 THEN 
     PID = 0.
  ELSE DO:
     PID      = GET-LONG(lpProcessInformation,9).
     /* release kernel-objects hProcess and hThread: */
     RUN CloseHandle IN hpApi(GET-LONG(lpProcessInformation,1), OUTPUT ReturnValue).
     RUN CloseHandle IN hpApi(GET-LONG(lpProcessInformation,5), OUTPUT ReturnValue).
  END.
 
  SET-SIZE(lpStartupInfo)        = 0.
  SET-SIZE(lpProcessInformation) = 0.
  SET-SIZE(lpWorkingDirectory)   = 0.
 
END PROCEDURE.

execute a program and wait until it becomes visible

Suppose your window has a button that is designed to launch an application, using ShellExecute or CreateProcess, but this application takes a few seconds to launch. Since ShellExecute(Ex) and CreateProcess return immediately after the process is created, an impatient user will have time to press the button several times. To work around this problem you may want to disable the button until the application really becomes visible. WaitForInputIdle does the trick. Well, in reality it waits until the application has its input queue (for mouse and keyboard) empty and waiting, but that's at about the same time anyway.

  
  {windows.i}
  DEFINE VARIABLE hProcess AS INTEGER NO-UNDO. 
  DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO.
  hProcess = CreateProcess("notepad.exe",
                           "",
                           3). 

  IF hProcess = 0 THEN DO:
      getlasterror().
      ShowLastError().
  END.
  ELSE DO:
     RUN WaitForInputIdle IN hpApi (hProcess, 
                                    -1,   /* -1=INFINITE */
                                    OUTPUT ReturnValue).
     RUN CloseHandle IN hpApi (hProcess, OUTPUT ReturnValue).
  END.

Note

CreateProcess does not search for applications as good as ShellExecute does. When CreateProcess does not find an application you may have more luck with ShellExecuteEx (not ShellExecute itself, because that does not return a hProcess).
ShellExecuteEx will return a hProcess when SEE_MASK_NOCLOSEPROCESS is specified in its fMask field.


execute a program and wait until it has finished

16-bit (there is a 32-bit solution near end of page)

This code was found on PEG, posted by Stuart Butler a while ago. It is 16 bit code and needs to be reworked for 32-bit. It compiles in version 8.2 but does not wait as expected.
The example program runs notepad, waits until the user has closed notepad and then displays the results of the user's work with notepad.

 /* If we were being run persistent we would want to 
  *** be interrupted when waiting for notepad to finish
  *** if we are closed so include an on close event trigger */
 DEFINE VARIABLE lgClosing AS LOGICAL INITIAL FALSE.
 ON CLOSE OF THIS-PROCEDURE
 DO:
    lgClosing = TRUE.
 END.
 
/* Run Notepad and keep a record of its task number */
 DEFINE VARIABLE nTask AS INT.
 RUN WinExec IN hpApi( "Notepad c:\temp\trash.txt", 1, OUTPUT nTask ).
 IF nTask GT 0 AND nTask LT 32 
 THEN DO: 
    MESSAGE "WinExec failed" VIEW-AS ALERT-BOX.
    RETURN.
 END.
 
/* Now wait for notepad to finish - when notepad is closed
 *** the task number will become invalid and Module
 *** API functions will fail - we are using GetModuleFileName. 
 *** The WAIT-FOR will process events and not hog all the CPU
 *** - pity the PAUSE could not be in milliseconds but will have to do
 *** with what's available.  If you use PROCESS EVENTS in some 
 *** other loop construction (with say a timer VBX) it is a good idea to 
 *** preceed it with a call to the WaitMessage() API call as 
 *** looping with PROCESS EVENTS tends to hog the CPU.
 *** You might want to disable your user interface at this point 
 *** though that may not be necessary if you have run this .p
 *** persistently to deal with the windows app "in the background".
 */
 DEFINE VARIABLE szName AS CHARACTER.
 DEFINE VARIABLE szNameLength AS INTEGER.
 REPEAT:
    WAIT-FOR CLOSE OF THIS-PROCEDURE PAUSE 1.
    szName = FILL(" ",256).
    RUN GetModuleFileName{&A} IN hpApi( nTask, 
                                        OUTPUT szName,
                                        LENGTH(szName),
                                        OUTPUT szNameLength).
    /* We don't want to know the ModuleFileName, we only want
     *** to know if the function fails. Fails if result=0.
     *** A failure indicates that the task has ended. */
    IF (szNameLength=0) OR lgClosing THEN
        LEAVE.
 END.
 
/* Notepad should have finished at this point - let's 
 *** see what the user input */
 MESSAGE "Notepad finished" VIEW-AS ALERT-BOX.
 INPUT FROM c:\temp\trash.txt.
 DEFINE VARIABLE szLine AS CHARACTER.
 REPEAT:
     IMPORT UNFORMATTED szLine.
     MESSAGE szLine VIEW-AS ALERT-BOX.
 END. 
 INPUT CLOSE.

Unfortunately, the repeat loop that checks for the existence of the executing task may slow down the computer.

32-bit

In a 32-bit environment you can use the procedure WaitForSingleObject. This is a very efficient procedure. The input parameter for WaitForSingleObject is a handle to an object, in this case a kernel process object. This is not the same as the hInstance as returned by ShellExecute.

To obtain a process handle you seem to have to start the process using either CreateProcess or ShellExecuteEx.
Here is a (simplified) example for CreateProcess. The next page, titled "wait until MS-Word finished printing" uses ShellExecuteEx but not specifically for its process handle...

  {windows.i}
  DEFINE VARIABLE hProcess AS INTEGER NO-UNDO. 
  DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO.
  hProcess = CreateProcess("notepad.exe c:\config.sys",
                           "",
                           1). 
  IF hProcess=0 THEN
     ShowLastError().
  ELSE DO:
     RUN WaitForSingleObject IN hpApi (hProcess, 
                                       -1,   /* -1=INFINITE */
                                       OUTPUT ReturnValue).
     RUN CloseHandle IN hpApi (hProcess, OUTPUT ReturnValue).
  END

CreateProcess and ShowLastError are declared in windows.i.

Don't forget CloseHandle!

The process object has an internal reference counter. There are/were at least two references: one from within notepad.exe itself, the other one from your program (obtained by CreateProcess). When notepad.exe terminates, it will decrease the object's reference counter but your program still holds one. The object can not be destroyed until the reference counter decreases to zero, so you must use CloseHandle (which decrements the counter by one and invalidates the handle).

In other words: there is a memory leak if you don't call CloseHandle (although the kernel calls it when the Progress session terminates).
CloseHandle does not destroy the object (yet), it just tells the Kernel that it can destroy the object whenever it wants to (as far as you are concerned). CloseHandle is important for every Kernel object: CreateProcess returns a process object and also a thread object. The thread object is already closed inside the P4GL implementation of function CreateProcess in winfunc.p (version May 9, 1998).


ShellExecute

ShellExecute is the procedure that is called by the Desktop or Windows Explorer when you double-click an item: if the item is an executable it will run it, if the item is a Word-document it will open it in MS-Word, etc.
There is a lot you can do with this easy-to-use procedure. Let's have a look at the parameters:

PROCEDURE ShellExecute{&A} EXTERNAL "shell32" :
     DEFINE INPUT PARAMETER HWND AS LONG.
     DEFINE INPUT PARAMETER lpOperation AS CHARACTER.
     DEFINE INPUT PARAMETER lpFile AS CHARACTER.
     DEFINE INPUT PARAMETER lpParameters AS CHARACTER.
     DEFINE INPUT PARAMETER lpDirectory AS CHARACTER.
     DEFINE INPUT PARAMETER nShowCmd AS LONG.
     DEFINE RETURN PARAMETER hInstance AS LONG.
  END.

The parameters are:
* hwnd : parent window that will receive a possible messagebox. This parameter is usually 0
* lpOperation : "open" or "print"
"print" is only valid if lpFile is a document
* lpFile : name of an executable or name of a document
* lpParameters : command-line parameters for the executable in lpFile
* lpDirectory : default directory to execute lpFile in
* nShowCmd : Same as in winexec: hidden=0 normal=1 minimized=2 maximized=3
* hInstance : If function succeeds hInstance will be the instance handle of the executed program. If the value is >=0 and <=32 the function failed.

As you can see there are different ways to perform "notepad.exe c:\readme.txt" :

RUN ShellExecute{&A} IN hpApi(0,
                              "open",
                              "notepad.exe",
                              "c:\readme.txt",
                              "",
                              1,
                              OUTPUT hInstance).
RUN ShellExecute{&A} IN hpApi(0,
                              "open",
                              "c:\readme.txt",
                              "",
                              "",
                              1,
                              OUTPUT hInstance).

The second example shows an important feature of ShellExecute: opening a document. It uses associations to find the matching executable. Both examples will have the exact same result if the extention ".txt" is associated with "notepad.exe".

Examples

The following examples, submitted by Rob den Boer, show how ShellExecute can be used to send email, or to open the default browser for a particular website:

Sending email

  DEFINE VARIABLE email AS CHARACTER NO-UNDO.
  email =   "mailto:rc.den.boer@hccnet.nl"
          + "?cc=piet@nowhere.nl"
          + "?Subject=Test%20by%20Rob"  /* notice %20 instead of space */
          + "?Body=How are you doing".
 
  RUN ShellExecute{&A} IN hpApi 
                   (0,
                    "open",
                    email,
                    "",
                    "",
                    1,
                    OUTPUT hInstance).

open a website in your default browser

  RUN ShellExecute{&A} IN hpApi 
                   (0,
                    "open",
                    "ftp://zdftp.zdnet.com",
                    "",
                    "",
                    1,
                    OUTPUT hInstance).
/* or: */ 
  RUN ShellExecute{&A} IN hpApi 
                   (0,
                    "open",
                    "http://home.hccnet.nl/rc.den.boer/progress/index.html",
                    "",
                    "",
                    1,
                    OUTPUT hInstance).
 

ShellExecute and OpenAs

improved by Tim Townsend

Documents can be associated with executables, Windows recognizes a document by its file extention.
Thanks to associations, the ShellExecute procedure knows how to "open" a document and the Explorer knows which icon to draw next to a document.
If a document does not have an association yet and you choose "open" in the Explorer, you will be presented a "Open As" dialog where you can choose an application. The following example shows how to do this in Progress.
The procedure first tries to open (or print) the document. If this fails the first time, it will try a second time using the OpenAs dialog.

/*--------------------------------------------------------------------------
       File        : open-doc.p
       Purpose     : Open a windows document using the associated application.
                     If no assocoated application, run the OpenAs dialog to
                     allow the user to pick an application.
 
       Syntax      :
 
       Description :
 
       Author(s)   : TWT
       Created     : 22 Dec 1999
       Notes       :
--------------------------------------------------------------------------*/
 
DEFINE INPUT        PARAM cFileName            AS CHARACTER        NO-UNDO.
DEFINE INPUT        PARAM cParams              AS CHARACTER        NO-UNDO.
DEFINE INPUT        PARAM cDirectory           AS CHARACTER        NO-UNDO.
DEFINE INPUT        PARAM lPrint               AS LOG         NO-UNDO.
 
&SCOPED-DEFINE SE_ERR_NOASSOC 31
&SCOPED-DEFINE SE_ERR_ASSOCINCOMPLETE 27
 
DEFINE VARIABLE iInstance            AS INTEGER                        NO-UNDO.
DEFINE VARIABLE cWorkDirectory       AS CHARACTER                       NO-UNDO.
 
/* in case parameter cDirectory contains a relative path 
   it has to be replaced by a fully-qualified path: */
 
ASSIGN FILE-INFO:FILE-NAME = cDirectory.
IF FILE-INFO:FULL-PATHNAME > "" THEN
  cWorkDirectory = FILE-INFO:FULL-PATHNAME.
 
/* try to execute the document: */
 
RUN ShellExecuteA(INPUT 0,
                  INPUT (IF lPrint THEN "print":u ELSE "open":u),
                  INPUT cFileName,
                  INPUT cParams,
                  INPUT cWorkDirectory,
                  INPUT 1,  /* normal mode */
                  OUTPUT iInstance).
 
/* if no associated application, run OpenAs dialog: */
 
IF (iInstance = {&SE_ERR_NOASSOC} OR 
    iInstance = {&SE_ERR_ASSOCINCOMPLETE}) 
   AND NOT lPrint THEN DO:
 
   /* Ignore cParams because cFileName is a document.
      cParams is only valid with executables */
   RUN ShellExecuteA (INPUT 0,
                      INPUT "open":u,
                      INPUT "rundll32.exe":u,
                      INPUT "shell32.dll,OpenAs_RunDLL ":u + cFileName,
                      INPUT cWorkDirectory,
                      INPUT 1,
                      OUTPUT iInstance).
END.  /* if */
 
/* test for error: */
 
RUN TestErrorCode(iInstance).
IF RETURN-VALUE > "" THEN
  MESSAGE RETURN-VALUE
    VIEW-AS ALERT-BOX ERROR BUTTON OK.
 
/****************************************************************************/
 
PROCEDURE ShellExecuteA EXTERNAL "shell32":U :
  DEFINE INPUT PARAMETER HWND         AS LONG.
  DEFINE INPUT PARAMETER lpOperation  AS CHARACTER.
  DEFINE INPUT PARAMETER lpFile       AS CHARACTER.
  DEFINE INPUT PARAMETER lpParameters AS CHARACTER.
  DEFINE INPUT PARAMETER lpDirectory  AS CHARACTER.
  DEFINE INPUT PARAMETER nShowCmd     AS LONG.
  DEFINE RETURN PARAMETER hInstance   AS LONG.
END PROCEDURE.
 
 
PROCEDURE TestErrorCode :
DEFINE INPUT PARAMETER iCode AS INTEGER.
DEFINE VARIABLE cTxt AS CHARACTER NO-UNDO.
 
IF iCode < 0 OR iCode > 32 THEN RETURN "". /* no error */
 
CASE iCode :
  WHEN  0 THEN cTxt = "The operating system is out of memory or resources.":T132.
  WHEN  2 THEN cTxt = "The specified file was not found":T132.
  WHEN  3 THEN cTxt = "The specified path was not found.":T132.
  WHEN  5 THEN cTxt = "The operating system denied access to the specified file.":T132.
  WHEN  8 THEN cTxt = "There was not enough memory to complete the operation.":T132.
  WHEN 10 THEN cTxt = "Wrong Windows version":T132.
  WHEN 11 THEN cTxt = "The .EXE file is invalid (non-Win32 .EXE or error in .EXE image).":T132.
  WHEN 12 THEN cTxt = "Application was designed for a different operating system.":T132.
  WHEN 13 THEN cTxt = "Application was designed for MS-DOS 4.0.":T132.
  WHEN 15 THEN cTxt = "Attempt to load a real-mode program.":T132.
  WHEN 16 THEN cTxt = "Attempt to load a second instance of an application with non-readonly data segments.":T132.
  WHEN 19 THEN cTxt = "Attempt to load a compressed application file.":T132.
  WHEN 20 THEN cTxt = "Dynamic-link library (DLL) file failure.":T132.
  WHEN 26 THEN cTxt = "A sharing violation occurred.":T132.
  WHEN 27 THEN cTxt = "The filename association is incomplete or invalid.":T132.
  WHEN 28 THEN cTxt = "The DDE transaction could not be completed because the request timed out.":T132.
  WHEN 29 THEN cTxt = "The DDE transaction failed.":T132.
  WHEN 30 THEN cTxt = "The DDE transaction could not be completed because other DDE transactions were being processed.":T132.
  WHEN 31 THEN cTxt = "There is no application associated with the given filename extension.":T132.
  WHEN 32 THEN cTxt = "The specified dynamic-link library was not found.":T132.
  OTHERWISE    cTxt = "Undocumented error code returned":T132.
END.
 
RETURN cTxt.
 
END PROCEDURE.

notes

This example by Tim Townsend replaces the old example, which was published here until 5 januari 2000. For those who still use a copy of the old source: this new one is better because you can now pass a URL, like "www.progress.com", to the cFilename parameter.


ShellExecute return codes

If a call to ShellExecute succeeds, the returned hInstance parameter will be the instance handle of the executed program. But if the hInstance is less then 33, it indicates a failure.
Actually if the hInstance>=0 and hInstance<33 because a very large hInstance will be casted into a signed integer variable and may appear to be negative from Progress' perspective.
This page lists the descriptions for some of the possible failures.

It's actually a combination from different resources, both from WinExec and other procedures so some of the result may never occur when using ShellExecute. There are also some gaps in the list, and moreover: some results should have a different description in 16-bit mode. Where descriptions in 16-bit are different then 32-bit I've picked the 32-bit description

{windows.i}
 
DEFINE VARIABLE hInstance AS INTEGER.
 
RUN ShellExecute{&A} IN hpApi(0,
                              "open",
                              "xyz.doc",
                              "",
                              "",
                              1,
                              OUTPUT hInstance).
 
RUN TestInstance (hInstance).
IF RETURN-VALUE > "" THEN
   MESSAGE "ShellExecute failed, reason: " SKIP
           RETURN-VALUE
           VIEW-AS ALERT-BOX ERROR.
 
 
 
PROCEDURE TestInstance :
 
DEFINE INPUT PARAMETER hInstance AS INTEGER. /* =return value from ShellExecute */
DEFINE VARIABLE txt AS CHARACTER NO-UNDO.
 
IF hInstance<0 OR hInstance>32 THEN RETURN "". /* not failed */
 
CASE hInstance :
  WHEN  0 THEN txt = "The operating system is out of memory or resources.".
  WHEN  2 THEN txt = "The specified file was not found".
  WHEN  3 THEN txt = "The specified path was not found.".
  WHEN  5 THEN txt = "Windows 95 only: The operating system denied " 
                      + "access to the specified file".
  WHEN  8 THEN txt = "Windows 95 only: There was not enough memory to "
                      + "complete the operation.".
  WHEN 10 THEN txt = "Wrong Windows version".
  WHEN 11 THEN txt = "The .EXE file is invalid (non-Win32 .EXE or "
                      + "error in .EXE image).".
  WHEN 12 THEN txt = "Application was designed for a different operating system".
  WHEN 13 THEN txt = "Application was designed for MS-DOS 4.0".
  WHEN 15 THEN txt = "Attempt to load a real-mode program".
  WHEN 16 THEN txt = "Attempt to load a second instance of "
                      + "an application with non-readonly data segments".
  WHEN 19 THEN txt = "Attempt to load a compressed application file".
  WHEN 20 THEN txt = "Dynamic-link library (DLL) file failure".
  WHEN 26 THEN txt = "A sharing violation occurred.".
  WHEN 27 THEN txt = "The filename association is incomplete or invalid.".
  WHEN 28 THEN txt = "The DDE transaction could not be completed " 
                      + "because the request timed out.".
  WHEN 29 THEN txt = "The DDE transaction failed.".
  WHEN 30 THEN txt = "The DDE transaction could not be completed because "
                      + "other DDE transactions were being processed.".
  WHEN 31 THEN txt = "There is no application associated with "
                     + "the given filename extension.".
  WHEN 32 THEN txt = "Windows 95 only: The specified dynamic-link " 
                     + "library was not found.".
  OTHERWISE    txt = "undocumented".
END.
 
RETURN txt.
 
END PROCEDURE.
 

ShellExecuteEx

ShellExecuteEx combines features of ShellExecute and CreateProcess, the most noticable feature is that it returns a PID.

PROCEDURE ShellExecuteExA EXTERNAL "shell32.dll" :
  DEFINE INPUT  PARAMETER lpExecInfo  AS LONG.
  DEFINE RETURN PARAMETER ReturnValue AS LONG.
END PROCEDURE.

lpExecInfo is a memory-pointer to a structure of type SHELLEXECUTEINFO, which is a bit complicated to describe. Perhaps it's best to point at an example: See wait until MS-Word finished printing.


the obsolete WinExec procedure

WinExec is often found in old examples for Visual Basic. It's old... don't use it!
In Windows 3.x you would have used the WinExec procedure to run a program. The procedure is still around in Windows but it is obsolete, you should use CreateProcess or ShellExecute instead. In fact, WinExec itself is nothing but a wrapper to CreateProcess.
Note that WinExec from "krnl386.exe" (the 16 bit version) can not execute a 32-bit program, while WinExec from "kernel32.dll" (the 32 bit version) can't execute a 16-bit program.
So if you are running a Progress version less than 8.2 on Windows 95+, you may end up using two different versions of WinExec (the regular 16-bits version and a thunked 32-bits version).
(Also if you want to call a 16-bit program in Progress 8.2, but that is something you should not want to do!)
The WinExec procedure does not let you control the default directory.
If all these problems don't apply to you, you will find WinExec easy to use. Here is how you call it:

  {windows.i}
  DEFINE VARIABLE hTask AS INTEGER NO-UNDO.
  RUN WinExec IN hpApi( "notepad.exe c:\readme.txt", 1, OUTPUT hTask).
  IF hTask>=0 AND hTask<32 THEN
     MESSAGE "failed" VIEW-AS ALERT-BOX.

Explanation

The second parameter is the CmdShow parameter, the most commonly used values are:
* hidden = 0
* normal = 1
* minimized = 2
* maximized = 3

The output parameter is a handle to the task that WinExec created. According to API documentation this value should be greater than 31. But since it is an unsigned integer and gets casted into a signed Progress integer, it may occur that a valid very large value will seem to be less than 0. Values 0 to 31 represent a documented error status.


wait until MS-Word finished printing

Scenario:

* You have created an RTF file, c:\temp\test.rtf
* You want MS-Word to print this RTF file, but you don't want to use OLE Automation. In other words: you want to call ShellExecute(..., "print", "c:\temp\test.rtf", .....).
* You want to wait until MS-Word has finished printing.

Why:

OLE Automation would be fine but the OLE commands are application specific (and perhaps even version/translation specific) so you rather rely on the Shell "print"-verb.
You can not easily use CreateProcess and WaitForSingleObject, because MS-Word (and many other word-processors) has an MDI-interface. This means MS-Word (the process) will not terminate if it was already opened before you issued the "print"-request.

Solution:

Knowing that the Shell's "print"-verb executes a DDE-conversation with MS-Word, you can use ShellExecuteEx with the option to wait until the DDE conversation terminates.
Procedure PrintAndWait in this example uses the ShellExecuteEx function. I did not include any error-handling.

RUN PrintAndWait ("c:\temp\test.rtf").
 
{windows.i}
 
PROCEDURE PrintAndWait :
  define INPUT PARAMETER FILENAME AS CHARACTER NO-UNDO.
 
  DEFINE VARIABLE lpVerb AS MEMPTR.
  DEFINE VARIABLE lpFile AS MEMPTR.
  DEFINE VARIABLE lpExecInfo AS MEMPTR.
  DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO.
 
  SET-SIZE(lpVerb)         = LENGTH("print") + 1.
  PUT-STRING(lpVerb,1)     = "print".
 
  SET-SIZE(lpFile)         = LENGTH (FILENAME) + 1.
  PUT-STRING(lpFile,1)     = FILENAME.
 
  SET-SIZE (lpExecInfo)    = 60.
  PUT-LONG (lpExecInfo, 1) = GET-SIZE(lpExecInfo).
  PUT-LONG (lpExecInfo, 5) = 256. /* = SEE_MASK_FLAG_DDEWAIT */
  PUT-LONG (lpExecInfo, 9) = 0.   /* hwnd                    */
  PUT-LONG (lpExecInfo,13) = GET-POINTER-VALUE(lpVerb).
  PUT-LONG (lpExecInfo,17) = GET-POINTER-VALUE(lpFile).
  PUT-LONG (lpExecInfo,21) = 0.   /* commandline             */
  PUT-LONG (lpExecInfo,25) = 0.   /* current directory       */
  PUT-LONG (lpExecInfo,29) = 2.   /* wCmdShow                */
 
  RUN ShellExecuteExA IN hpApi(GET-POINTER-VALUE(lpExecInfo),
                               OUTPUT ReturnValue).
 
  SET-SIZE (lpExecInfo)    = 0.
  SET-SIZE (lpFile)        = 0.
  SET-SIZE (lpverb)        = 0.
 
END PROCEDURE.