CD Deployment
Making your CD idiot proof.
by Ronnie MacGregor — Date: March  2000


 
   

You can download the files for this article for your own use here (it’s a 140 Kb zipped file).


Introduction
One of the biggest problems in developing applications, is to try and see things, and use things, from the point of view of the user with little computing knowledge.  It is difficult in the extreme, if you have knowledge of something, to behave as if you don’t.  The subjects which I cover here are the result of discussions with a potential client, who required that his catalogue application on CD should be useable by someone with virtually no knowledge of computers.

This article is aimed at the beginner tackling CD deployment for the first time. I am sure that there will be other solutions to these problems, but if this is a new subject area for you, then I hope that the solutions which I have found will provide a good head start.

back to index

Normal Deployment

Visual dBASE deployment has never been easier, with InstallShield Express, and if you are looking for more information on this subject, have a look at Ken Mayer’s Deploy7.How.

Although InstallShield will create a 650MB directory for CD installation, my preference is to stick to 1.44MB floppy sized directories and this means that if you ever have to provide your application on floppy it is easy to take this from your CD. Irrespective of your choice you will have achieved the production of one or more directories containing all of the files required to deploy your application.

back to index

Windows Add/Remove Programs

Not everyone knows that you can use Windows Explorer to find and double click on Setup.exe residing in the Disk 1 directory on your CD.  The user with a little bit of knowledge, and doing things “by the book”, will expect to be able to install a new application from the Add|Remove Programs applet in the Control Panel. Unfortunately when this applet scans the user’s disk drives, it will not look in any directory other than the root directory of any disk it finds, and will therefore fail to find your application.

What should we do about this? Well there seem to be two options, either empty the contents of your DISK 1 directory into the root directory which is untidy and undesirable, or put something in the root directory for the applet to find. One thing which will not work is simply copying InstallShield’s Setup.exe to the root directory.

Windows Add|Remove Programs is looking for a file called either Setup or Install. In order to avoid confusion with InstallShield’s Setup.exe I have opted to produce Install.exe. You can use the CD.ICO icon file for this exe to make it look pretty!! This could be one of the shortest programs you’ll ever use, and needs to be compiled to Install.exe.
 
 
Function CDInstall
   nInstallResult = run(true,"DISK1\Setup.exe")
   if nInstallResult # 0  // 0 success, 2 fail
      msgbox("Launch of DISK1\Setup.exe failed.","CD Error",16)
   endif
return
   

In order that this will run from the root directory of your CD you need to ensure that the following files from the \BIN directory of your Visual dBASE installation are also found there: Vdb7run.exe, Vdb70009.dll (or other language version) and Resource.dll.

We now have a file that can be found by Windows Install|Remove Programs or by the using Windows Explorer. This is an improvement, but the problem now is that it takes some time for our exe to load up. This is partly because of the fact that we are running it from CD, and partly because of the Vdb7run overhead. The problem with this is that the user may think that nothing is happening, and double click again, producing multiple instances of our Install.exe. I think the best solution here is to use the ability of a Visual dBASE exe to display a splash screen while it is loading, letting the user know that something is in fact happening. This can easily be done by simply typing msgbox code into the Command window, screengrabbing the msgbox produced (I used PaintShop Pro) and chopping out the button to produce something like this:

The options to include an icon and a splash screen are to be found in your project properties dialogue as follows:

bu08cd03.gif

These two image files do not need to be “included” in the build of the exe, all this will do is bloat the size of the file.

back to index

AutoRun your CD

Most users now expect something to happen when they put a CD into a drive, so the next step in helping the user is to arrange for the CD to AutoRun. As far as I am aware the Windows installed default is to have a CD drive AutoRun, so the user with enough knowledge to change this will have enough knowledge to find Install.exe. It is very easy to have your CD AutoRun. All you need is a simple text file created with Notepad, called AUTORUN.INF which you place in the root directory of your CD. At it’s most basic this is what it needs to contain:
 
 
[autorun]
OPEN=Install.exe
ICON=My.ico
   

back to index

CD Launcher
The simple AutoRun above is all very well if you really do want to initiate the install every single time the CD is put into the drive, but what do you do if you have other things on your CD as well. For example a catalogue application may have a large amount of data which it is not desirable (or possible) to copy to the user’s hard drive, and what we want is to install the application to hard drive, but have it access data on the CD. In this instance we are going to need a way of deciding whether our application is already installed or not, and on AutoRun, decide whether to run the pre-existing application, or initiate the install. In order to do this we need another small exe on the root directory of the CD, and I’ve called it CDLaunch.exe. Now we want our AUTORUN.INF to launch this exe, and we need to pass a parameter to it with the name of our application exe. It will now look something like this:
 
 
[autorun]
OPEN=CDLaunch.exe MyApp.exe
ICON=My.ico
   

The logic used here is simple. Look for a pre-existing installation of our application anywhere on the users system. If we find it, we’ll run it. If not we will initiate the installation.

Again some time is taken for this exe to load up, so it’s probably best to give the user something to look at, again by way of a splash screen, perhaps something like this:

In order to ensure that we can find our application, we need to ensure that as part of the InstallShield Express installation we write a couple of bits of information into the Windows registry. Some developers seem to avoid the use of the Registry, or treat it as some sort of black art, but it’s actually very easy to accomplish. InstallShield will do everything for us, all we need to do is Add Keys to match the path shown below leading to an entry called MyApp.exe (whatever you have called it!) as in the following example:

bu08cd05.gif

In this Key we want to add the following values:

bu08cd06.gif

Having done this, our information will be written to the registry on installation and removed on un-install. This means that wherever the user decides to install our application, we can find it, simply by looking in the registry.

In order to retrieve this information we can use Registry.prg, which can be found in the \Samples directory of your Visual dBASE installation. This program in turn needs winreg.h and windef.h which can be found in the \Include directory of your Visual dBASE installation. The best way I have found to use these is simply to copy them into the project directory you are working in, and then we can do the following:
 
 
Function CDLaunch(cExeName)
   local cPath, nExeResult, nSetupResult
   //
   set procedure to registry.prg additive
   //
   // Check parameter
   if empty(argvector(1))
      msgbox("Required parameter not passed.","Call Error",16)
      return
   endif
   // Try to get app path from registry
   try
      reg = new Registry((0x80000002),;
         "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\";
         + cExeName)
      cPath = reg.queryValue("Path")
   catch(Exception e)
      // Just continue !!
   endtry
   // Check app not already running
   try
      fClose(fOpen(cPath + "\" + cExeName,"RW"))
   catch(Exception e )
      if e.code == 108  // file in use by another
         return  // quit —- quit causes screen flash
      endif
   endtry
   // Try to run app
   if(reg.error == 0) // No error
      cd (cPath)
      nExeResult = run(true,cPath + "\" + cExeName)
   else
      nExeResult = run(true,cExeName)
   endif
   // If app didn't run initiate install
   if nExeResult # 0       // 0 success, 2 fail
      nSetupResult = run(true,"DISK1\Setup.exe")
         if nSetupResult # 0  // 0 success, 2 fail
         msgbox("Automatic CD launch failed.","CD Error",16)
         endif
   endif
   //
return
   

We have checked to see if the application is already running before we try to run it, and this is to prevent a second instance of the application being run in the event that the application is started from the Windows Start menu, and the CD is subsequently inserted to provide the data, whereupon it will AutoRun. The technique of establishing whether our application is already running is from Romain Streiff’s IsExeOpen.prg in the dUFLP.

It is also worth pointing out that prior to running our pre-installed application, we have changed directory to the application directory. If we did not do this the current directory would be the root directory of our CD drive, and that might make it difficult for our application to find files it expects to find in the current directory. It is also of importance with regard to our application’s INI file (see later).

Note that if we find that an install is required we bypass our Install.exe and go straight to InstallShield’s Setup.exe for speed.

The above code can be compiled to CDLaunch.exe, and again in order that this will run from the root directory of your CD you need to ensure that the following files from the \BIN directory of your Visual dBASE installation are also found there: vdb7run.exe, Vdb70009.dll (or other language version) and Resource.dll.

back to index

The INI file
The ini file is absolutely critical in ensuring that the behaviour of your deployed application matches that of the development version. If you are looking for more information on this subject, again have a look at Ken Mayer’s Deploy7.How.

The INI file for your application should have the same name as the exe, and should be deployed using InstallShield. The part of the INI file that we are interested in here is the Directories section. There are two problems with this section. We do not know where the user might choose to install the application, so we cannot hard code the directory path prior to deployment. If we remove this section altogether from the INI file then we may have problems getting our application to run. The answer is to include the heading, but not to include any information after it, like so:
 
 
[Directories]
   

When the application is run, Visual dBASE will write the current directory information back to your INI file when it closes so that it might now look like this:
 
 
[Directories]
0=C:\MyAppDir
Current=0
   

This is of critical importance when we realise that we may be launching our hard drive installed application from the root directory of the CD. If we are not careful the CD drive root directory will be written to our INI file and subsequent attempts to run the application will fail. This is why in our CDLaunch.exe we change directory to that of the application before running it.

back to index

CD Finder
The next thing that we have to deal with is accessing our data when it remains on the CD. The problems here are that we don’t know the drive letter of the user’s CD drive, and we don’t know if our CD is in the drive.

The first task is to get the users CD drive, and in the case of multiple CD drives we will use the first drive that we find. I have done this by using the GetDrives() function which is available in the dUFLP, and have incorporated it into my code. Having found a CD drive, then if an optional object reference is passed to the function, then a custom property CDdrive will be attached to that object, containing the drive letter for the CD Drive. This is then available to be used programmatically.

We now have to look for our CD in the drive, and we do this by looking for the existence of a unique file which will only be found on this CD. Again we have the task of holding the users hand, so if we don’t find the CD, we pop up a msgbox requesting that “CD Name” is placed in drive “Drive Letter”, and we repeat this until the user complies or quits. If the correct CD is successfully found then the CD drive letter is returned, otherwise the space character is returned.

There is nothing particularly complicated about this code, but if you are new to this subject then it is probably worth working through it to see how it works.
 
 
Function CDFinder(cFile,cCDname,oRef)
   local cReturn, nChoice
   //
   // Check parameters
   if empty(argvector(1))
      MsgBox("File name parameter not passed","Call Error")
      cReturn = " "
      return cReturn
   else
      // strip off any drive letters etc
      cFile = substr(cFile,(at(":",cFile,1)+1),len(cFile))
      if substr(cFile,1,1) = "\"
         cFile = substr(cFile,2,len(cFile))
      endif
   endif
   if empty(argvector(2))
      cCDname = "required"
   endif
   //
   // Find CDROM Drives
   aCDdrives=Getdrives(5)  // returns array containing all the CD drives
   // Next 3 lines output the drives found in the results pane
   // for n=1 to aCDdrives.size
   //    ? aCDdrives[n]
   // next
   if aCDdrives.size = 0
      MsgBox("CD ROM drive not found or faulty ","CD ROM drive error")
      cReturn = " "
   else
      // Check object flagging parameter
      if not empty(argvector(3))
         try
            oRef.CDdrive = aCDdrives[1]
         catch ( Exception e )
            msgbox("The calling program did not pass" +chr(13)+;
            "a valid object reference parameter.","Call Error",16)
         endtry
      endif
      //
      // Look for CD in drive
      nChoice = 0
      // Loop exit control is inside loop
      do while nChoice < 3
         if not file(aCDdrives[1] + ":\" + cFile)  // file only on this CD
            nChoice = MsgBox("Please insert the " + cCDname ;
               + " CD into Drive " + aCDdrives[1],"CD ROM drive error",1)
            if nChoice = 1
               loop  // Until correct CD or the user cancels
            endif
            if nChoice = 2
               cReturn = " "  // User cancel
               exit  // Exit loop
            endif
         endif
         cReturn = aCDdrives[1]
         exit  // Exit loop if CD is found
      enddo
   endif
return cReturn
   

CDfinder.prg and a demo/test form CDfinder.wfm can be downloaded from the top of this document. Further information can be found in the program header.

back to index

Path Changer
We now know the drive letter of the users CD drive, and we know that the CD is in the drive ready for use, but the question is, how do we ensure that our datapath is correct, and ensure that we can access our tables? Again there are probably many ways to do this, and one of those is to use the CDfinder() function to attach a custom property somewhere, and this could be used programmatically to create an sql on the fly. Here is an alternative method which I find to be simple and effective.

Most people will use an .sql or preferably a .dmd file to set up their data access. These files can be included in the build of your exe, but lets make a decision here to leave them outside the exe, and deploy them as stand alone files in the application’s directory. These files contain hard coded data paths which are fine if the data is in the application directory, but does not help us much when our data is on a CD. It is however a simple matter to use the PathChanger() function to open your sql or dmd file, search for drive letters, and substitute any occurrences with the drive letter you have just established as containing your CD.
 
 
Function PathChanger(cFile, cDrive)
   local cThisChar
   // Check parameters
   if empty(argvector(1))
      MsgBox("File Name parameter not passed","Call Error")
      return False
   endif
   if empty(argvector(2))
      MsgBox("Drive Letter parameter not passed","Call Error")
      return False
   endif
   // Find and change Drive Letters
   fChange = new File()
   try
      fChange.Open(cFile,"RW")
      do while not fChange.eof()
         cThisChar = fChange.Read(1)
         if cThisChar = ":"
            cThisChar = fChange.Read(1)
            if cThisChar = "\"
               fChange.Seek(-3,1)
               fChange.Write(cDrive)
               fChange.Seek(3,1)
            endif
         endif
      enddo
      fChange.Close()
   catch(Exception e)
      return False
   endtry
return True
   

A bit unorthodox ? . . . Well it works !

back to index

Screen Resolution
How do you know if the end user has a sufficiently high screen resolution or colour setting such that your application will display properly? Some people will argue that you should design for the lowest common denominator, but if you do that there will never be any advances made. I personally find it hard to believe that in this day and age people are still working at 640x480, but it’s true. The user’s  computer arrives on their desk that way, and they don’t even think about it let alone have a clue how to change their settings.

This issue was brought to my attention when a potential client reported that they couldn’t use my demo disk because the 800x600 forms didn’t fit on the screen. To be told on the CD inlay that this was the required resolution wasn’t good enough, and he requested that I did something about it. My immediate thought was to put a txt file on the CD with instructions on how to access the Windows Screen Settings dialogue, but I realised that someone who can’t change their screen settings is unlikely to be able to find that file or access it.

My solution was to write a program which can be called by an application on startup, passing the required resolution and colour depth information to it. The two main functions used to retrieve this information from the users system, GetScreenRes() and GetColourDepth() are derived from Gary White’s approach in SysInfo.cc in the dUFLP.

The next task was how to deal with things if the user’s display settings are not high enough. For the user with no knowledge, we need a method of giving them a little bit of knowledge, and holding their hand at the same time. If the user’s settings are not high enough then the following form will be opened:

bu08cd07.gif

If the user chooses to change their display properties, then the Windows Display Properties dialogue is opened up with the settings tab already selected. This is achieved with the following single line of code:
 
 
run(true,"rundll32.exe shell32.dll,Control_RunDLL desk.cpl,,3")
   

I don’t think you can really help the user much more than that!

The function returns a numerical code covering every possible outcome/user choice, including the use of the Window close button “X”. The function can easily be used in your application header by coding along the lines of the following:
 
 
// Check display settings
set procedure to ScrCheck.prg additive
local nScrCheck
nScrCheck = ScrCheck("800x600",16)  // use 1600x1200 to test
if nScrCheck <> 0  // 0 current settings OK
   do case
      case nScrCheck = -1  // Problem with passed parameters
         msgbox("Problem with passed parameters",;
            "Screen Check",16)
         quit
      case nScrCheck = 1  // Change Settings clicked
         msgbox("Please change your display settings in the";
            + chr(13) +;
             "Windows dialogue that has just popped up, and";
            + chr(13) +;
             "re-launch this application.","Screen Check",64)
         quit
      case nScrCheck = 2  // Continue unchanged clicked
         msgbox("Correct display may not be possible with;
            your present screen settings.","Screen Check",16)
      case nScrCheck = 3  // Quit clicked
         msgbox("You have chosen to Quit.","Screen Check",16)
         quit
      case nScrCheck = 4  // Windows Close "x" clicked
         msgbox("The application will quit.","Screen Check",16)
         quit
      endcase
endif
release nScrCheck
   

ScrCheck.prg and a demo/test form ScrDemo.prg can be downloaded from the top of this document. Further information can be found in the program header.

back to index

Conclusion

The functions, programs, and exes we have looked at here go together to form an instantly re-useable set with which it is very easy to deploy your application on CD while making your CD idiot proof. I hope that for the beginner this provides a good head start, and that for the seasoned developer there might be something in there to think about.

The next logical step would be to provide a UI form with the CD Launcher where the user could choose to Install the application, Run the installed version, or run the application completely on the CD. I haven’t yet had the need to do this, but I can see that the day may come.

You can download all of the files for this article from the top of this page, and are free to use them as you wish. I will be happy to receive any feedback by e-mail, or by posting in the Visual dBASE Newsgroups.

Have Fun !

Acknowledgements: Thanks to Romain Strieff, Gary White, Ken Mayer, Dan Howard and Ivar B Jessen for their help via the Visual dBASE Newsgroups in helping me overcome some of the issues here which in turn have given rise to this article. Thanks to my wife Sarah for proof reading.

Ronnie MacGregor started developing dBASE IV applications around 15 years ago to help administer his businesses in Scotland. Many of these applications are still in daily use, but are gradually being replaced with 32bit Visual dBASE apps. Although with no formal programming training, and essentially a “hobby” programmer, some of his niche applications are used by similar businesses in Britain. He can be contacted at:

  Ronnie_dBulletin_@High-Lugtonridge.co.uk (take out “_dBulletin_”).