Quantcast
Channel: VBForums - CodeBank - Visual Basic 6 and earlier
Viewing all articles
Browse latest Browse all 1529

Completely Portable and Clean VB6 Projects

$
0
0
The objective here is to create a completely portable, dependency free, no registration needed, no installation needed, VB6 executable. Also, there's the added task that it should "clean up after itself".

Also, there's the criteria that we may use various ActiveX (OCX) controls. For purposes of this example, I've assumed that we're using the mscomctl.ocx (and only that one), but the concept can be easily extended to other ActiveX controls (and even DLL libraries).

This idea is perfect for a small application with no datafiles, possibly like a calculator. From the VB6 IDE, it actually takes two projects. I'll call them Project1 and Project2. Project1 is actually a "loader", "execute", and "clean-up" program. Project2 is the actual program that the user will see.

Alright, here are the steps to do it. Let's start with Project2 (your actual program):
  • Start a new project.
  • Rename the project name to Project2 under Project Properties.
  • Rename the default form to Form2. (There's no Form1 in this project.)
  • Add a component reference to mscomctl.ocx (Microsoft Windows COmmon Controls 6.0 (SP6)).
  • Throw several controls from mscomctl.ocx onto the default form (see picture below).
  • Save the project, saving it as Project2 and Form2.
  • Compile the project as Project2.exe.


It really doesn't matter what code you put into this project. It's just a demonstration. But we will need this Project2.exe to continue.

Here's a picture of my Form2 (with some mscomctl.ocx controls on it):

Name:  Form2.gif
Views: 67
Size:  9.4 KB

Now for the Project1 (loader) project. Here are the steps for it:
  • Start a new project.
  • (No need for any additional references. In fact, it's important to not have any.)
  • Place the Form1 code (see below) into Form1.
  • I also placed a label on this form, just so I'd know what it was (see picture below).
  • Using the resource editor, add mscomctl.ocx, Project2.exe, and Project2.exe.manifest files.
    • To actually use the resource editor, you must load it within the VB6 IDE. It is found on the menu under Add-Ins / Add-In Manager....
    • I use the mscomctl.ocx version 6.01.9545. The manifest is set up for this version. If you use a different version, you can download this version here. So long as you don't register it on your computer (and nothing I suggest does this), it won't interfere with any other programs. Just save it in the same folder with your source code for this project.
    • You'll also need the XML code that goes into the Project2.exe.manifest file for this demo. Note that this file is always named the same as your executable, with the addition of the .manifest extension. This is an ASCII/ANSI file that can be created with Notepad (or any other ASCII/ANSI editor). It was a bit long to place in a code section of this post, so here's a link to download this file as well.
    • Also, I chose to make a resource category named "Dependencies". Once your done adding the three files to Project1's resources, you should see these three files (see picture below of resource manager window).
    • When done, save the resources and close the resource editor.
  • Save the project as Project1 and Form1.
  • Compile this Project1 as Project1.exe. It's important to not place this Project1.exe in with your source code, because it will attempt to delete any Project2.exe, Project2.exe.manifest, and mscomctl.ocx file in it's folder when it terminates.
  • Once it's compiled, execute it, and watch what happens in the folder its in. The three files in the resources will be unpacked, and Project2 will be executed. When Project2 terminates, Project1 will clean up everything in the folder. And this works regardless of whether mscomctl.ocx was previously registered on the machine or not. In fact, nothing here will even interfere or use this other copy of mscomctl.ocx on the machine.


Here's the code for Form1. This is the code that does all the magic.

Code:

Option Explicit
'
Private Declare Function OpenProcess Lib "kernel32" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessId As Long) As Long
Private Declare Function WaitForSingleObject Lib "kernel32" (ByVal hHandle As Long, ByVal dwMilliseconds As Long) As Long
Private Declare Function CloseHandle Lib "kernel32" (ByVal hObject As Long) As Long
'

Private Sub Form_Activate()
    If Not bCompiled Then
        MsgBox "Sorry, but this entire idea is meant to be used from a compiled program."
        Exit Sub
    End If
   
    RetrieveResourceFileAndSaveToDisk App.Path, "Project2.exe.manifest", "Dependencies", True
    RetrieveResourceFileAndSaveToDisk App.Path, "mscomctl.ocx", "Dependencies", True
    RetrieveResourceFileAndSaveToDisk App.Path, "Project2.exe", "Dependencies", True
   
    'Shell App.Path & "\Project2.exe", vbNormalFocus
    ShellAndWait App.Path & "\Project2.exe", Command$
   
    On Error Resume Next ' Swallow errors so that, when we're executing two copies, this doesn't cause problems.
    Kill App.Path & "\Project2.exe.manifest"
    Kill App.Path & "\mscomctl.ocx"
    Kill App.Path & "\Project2.exe"
    On Error GoTo 0

    Unload Me
End Sub

Private Sub ShellAndWait(sProgram As String, Optional sCommand As String, Optional iFocus As VBA.VbAppWinStyle = vbNormalFocus)
    ' The WaitForSingleObject function returns when one of the following occurs:
    '  -  The specified object is in the signaled state.
    '  -  The time-out interval elapses.
    '
    ' The dwMilliseconds parameter specifies the time-out interval, in milliseconds.
    ' The function returns if the interval elapses, even if the object’s state is
    ' nonsignaled. If dwMilliseconds is zero, the function tests the object’s state
    ' and returns immediately. If dwMilliseconds is INFINITE, the function’s time-out
    ' interval never elapses.
    '
    ' This example waits an INFINITE amount of time for the process to end. As a
    ' result this process will be frozen until the shelled process terminates. The
    ' down side is that if the shelled process hangs, so will this one.
    '
    ' A better approach is to wait a specific amount of time.  Once the time-out
    ' interval expires, test the return value. If it is WAIT_TIMEOUT, the process
    ' is still not signaled.  Then you can either wait again or continue with your
    ' processing.
    '
    ' DOS Applications:
    '    Waiting for a DOS application is tricky because the DOS window never goes
    '    away when the application is done.  To get around this, prefix the app that
    '    you are shelling to with "command.com /c".
    '
    '    For example: lPid = Shell("command.com /c " & txtApp.Text, vbNormalFocus)
    '
    Dim lPid As Long
    Dim lHnd As Long
    Dim lRet As Long
    '
    Const SYNCHRONIZE = &H100000
    Const INFINITE = &HFFFF    'Wait forever
    '
    If Trim$(sProgram) = "" Then Exit Sub
    If Len(sCommand) Then
        lPid = Shell("""" & sProgram & """ " & sCommand, iFocus)
    Else
        lPid = Shell("""" & sProgram & """", iFocus)
    End If
    If lPid <> 0 Then
        lHnd = OpenProcess(SYNCHRONIZE, 0, lPid)      ' Get a handle to the shelled process.
        If lHnd <> 0 Then                              ' If successful, wait for the application to end.
            lRet = WaitForSingleObject(lHnd, INFINITE)
            CloseHandle lHnd                          ' Close the handle.
        End If
    End If
End Sub

Private Sub RetrieveResourceFileAndSaveToDisk(sFilePath As String, sFileName As String, sResourceType As String, Optional bOverwriteIfExists As Boolean = True)
    ' Loads the specified file from the .RES (or .EXE) and then saves it to disk.
    Dim bb() As Byte
    Dim sSaveSpec As String
    '
    ' Build filespec.
    If Right$(sFilePath, 1) = "\" Then
        sSaveSpec = sFilePath & sFileName
    Else
        sSaveSpec = sFilePath & "\" & sFileName
    End If
    '
    ' Check if the file exists.  Get out if it does and we don't want to overwrite.
    ' It saves time and memory to go ahead and do this first.
    If bFileExists(sSaveSpec) Then
        If Not bOverwriteIfExists Then Exit Sub
        On Error Resume Next ' Swallow errors so that running two copies won't cause problems.
        Kill sSaveSpec
        On Error GoTo 0
    End If
    '
    ' Actually retrieve and save the file.
    SaveResourceFileToDisk LoadResourceFileFromName(sFileName, sResourceType), sSaveSpec, bOverwriteIfExists
End Sub

Private Function LoadResourceFileFromName(sResourceName As String, sResourceType As String) As Byte()
    ' Loads the specified data file resource from the current project's resource (.RES) file, actually the .EXE once compiled.
    '  sResourceName        Specifies the unique name of the data file resource.
    '  sResourceType        Optional. Specifies the "type" of data file being returned.  The Visual Basic default for data files is "CUSTOM".
    '
    ' Will error if not found.  Note that CASE doesn't matter in the LoadResData call.
    ' However, spaces are not allowed in resource names, so they are removed.
    LoadResourceFileFromName = LoadResData(Replace$(sResourceName, " ", ""), sResourceType) ' Returns a Byte array.
End Function

Private Sub SaveResourceFileToDisk(DataArray() As Byte, sSaveSpec As String, Optional bOverwriteIfExists As Boolean = True)
    ' This function takes the specified data file in the form of a BYTE array and saves it out to the specified file.
    '
    ' sSaveSpec            Specifies the full path of the file to save out to.
    ' DataArray            Specifies the BYTE array that represents the data file to save out.
    ' bOverwriteIfExists  Optional. If set to TRUE and the file specified in the "sSavePath" parameter already
    '                      exists, the existing file will be overwritten with the new one.  If set to FALSE, the
    '                      existing file is left alone and the specified data file is not written out.
    '
    ' Return FALSE on error, TRUE if successful.
    Dim FileNum As Integer
    Dim sFileName As String
    Dim i As Long
    '
    ' Check if the file exists
    If bFileExists(sSaveSpec) Then
        If Not bOverwriteIfExists Then Exit Sub
        On Error Resume Next ' Swallow errors so that running two copies won't cause problems.
        Kill sSaveSpec
        On Error GoTo 0
    End If
    '
    sFileName = Mid$(sSaveSpec, InStrRev(sSaveSpec, "\") + 1)
    '
    ' Save the information to file.
    FileNum = FreeFile
    On Error Resume Next ' Swallow errors so that running two copies won't cause problems.
    Open sSaveSpec For Binary As #FileNum
    Put #FileNum, 1, DataArray()
    Close #FileNum
    On Error GoTo 0
End Sub

Private Function bFileExists(fle As String) As Boolean
    On Local Error GoTo FileExistsError
    ' If no error then something existed.
    If Len(fle) <> 0 Then
        bFileExists = (GetAttr(fle) And vbDirectory) = 0
    Else
        bFileExists = False
    End If
    Exit Function
FileExistsError:
    bFileExists = False
End Function

Private Function bCompiled() As Boolean
    On Error Resume Next
    Debug.Print 1 / 0
    bCompiled = Err = 0
    On Error GoTo 0
End Function

Here's a picture of my Form1:

Name:  Form1.gif
Views: 89
Size:  9.3 KB

Regarding adding the files to the resources of Project1, I chose to make a "Dependencies" category type for these kinds of files. The following is a screen-shot of my resource editor (from within VB6) after the files are added to the project:

Name:  res.gif
Views: 85
Size:  12.6 KB

Note that none of this applies when executing the program from within the VB6 IDE, and I've put checks in for that.

Also, there may need to be a tad more error checking in some of the code. For instance, if you attempt to execute a second copy of Project1.exe before the first one has terminated, you'll run into problems. I'll leave it to you to sort that one though. (Or maybe I'll come back later and do that.) Fixed this. It now works fine even if you want/need to execute two simultaneous copies.

Another enhancement would be to "pass through" any command line arguments received by Program1 into Program2. That's a pretty easy enhancement, and maybe I'll do it later. Fixed this. Any command line arguments passed into Program1 will be passed through to Program2.

Here's a copy of Project1.exe all compiled, with Project2.exe, Project2.exe.manifest, and mscomctl.ocx all wrapped up into it. It's just what's outlined above all compiled. Download it here if you like. Any fixes after the initial post will probably not be included in this executable.

Best Of Luck to Everyone,
Elroy

EDIT1: Also, everything in that Project1 (and Form1) is probably better done in a Sub Main and a project that has no user-interface. I just did it this way for illustration purposes.

EDIT2: If your project needs other ActiveX (OCX) controls, I've posted a .manifest file that covers the more commonly used OCX controls here.
Attached Images
   

Viewing all articles
Browse latest Browse all 1529

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>