TOCBACKFORWARD

Charlie Calvert's C++ Builder Unleashed

- 25 -

Using WININET to Create FTP Applications

Overview

In this chapter, you will look at techniques for building FTP clients. Most of the chapter will be dedicated to a discussion of WININET, which is a relatively simple Windows API for creating FTP, Gopher, and HTTP applications. This API is especially appealing because it is built into the operating system, ships with all versions of Windows after Windows NT 4.0, and allows you to create small, fast applications. If you don't have WININET.DLL on your system, you can download it for free from Microsoft's Web site. It works with all 32-bit versions of Windows.

I will also briefly discuss the FTP ActiveX component that ships with BCB. The FTP component provides a wide range of services that are easily accessible. However, it is not as light, nor as flexible, a tool as WININET.

A standard FTP component wrapped around WININET will be the central focus of this chapter. As a result, you will get a chance to take another look at building components. Included in the chapter will be some discussion about how to create components that are easy to use.

Another topic I touch on in this chapter is how to make an owner draw list box. This subject comes up in the course of creating an application that can display the files shown in an FTP directory search.

Requirements

BCB ships with WININET.h in the Include\Win32 directory. WININET.h contains manifests, functions, types, and prototypes for the Microsoft Windows Internet extensions. Therefore, you can now easily add FTP, Gopher, and HTTP support to your programs. Microsoft's WININET.DLL is freely distributable, and is available from Microsoft if it is not already installed in your Windows/System or Windows/System32 directory. Windows NT 4.0 ships with WININET.DLL, as will Windows 97. WININET.DLL runs fine on Windows 95.

One of the best places to get help on this subject is in the ActiveX SDK that ships as part of the MSDN and that has frequently been available for downloading from www.microsoft.com. Here is how, at the time of this writing, to get the Windows help file for WININET.h:

http://www.microsoft.com/intdev/sdk/docs/WININET/default.htm

In general, the INTDEV section of the Microsoft Web site is home ground for MS Internet developers.


NOTE: When working with WININET.h, you might find that you need to edit some of the code in the file. You should try simply including the file in your project first, and if you have problems, then consider making the following changes.

If you come across a #pragma pack, you can replace it with the pshpack4.h file:

// #pragma pack(push, WININET, 4)

#include <pshpack4.h>

At the end of the file, you will have to make the following substitutions:

// #pragma pack(pop, WININET)

#include <poppack.h>



You will need an FTP server of some kind to be able to use this control. Windows NT 4.0 comes with an excellent server, or you can usually download the Personal Web Server from the Microsoft Web site or get it with a copy of FrontPage. Personal Web Server supports FTP and ISAPI, and it runs on Windows 95. I have heard numerous complaints about the Personal Web Server's robustness. These may or may not be well founded, but there is no denying the usefulness of the tool when you are developing an application on a Win95 machine. If need be, you can copy your finished files to a more robust server after you complete the development cycle.

Making Sure FTP Is Working on Your System

If you're connected to the Internet, you should be able to FTP into various sites. For example, to connect to the Borland FTP site, type the following at the command prompt:

ftp ftp.borland.com

When the system asks for a username, type in anonymous. When it requests a password, type in your e-mail address. Figure 25.1 shows a screen shot of a typical old-fashioned, command-line-based FTP session.

FIGURE 25.1. FTP from the command line.

This chapter will show how to do the same thing in a Windows program that uses graphical controls.

If you can't FTP into borland.com, microsoft.com, or some site on your intranet, something is wrong with your Windows setup. You should clear up that problem first, before tackling the material in this chapter. I provide some help in this regard in Chapter 8, "Database Basics and Database Tools," in the section called "Some Notes on Installing TCP/IP."

Figure 25.2 shows a commercial program that works from inside Windows. The program you will create in this chapter, called WININET, is not quite this fancy, but it does allow you to use standard Windows controls to make and maintain your connections. FTPWININET has provisions for copying files from and to FTP sites, and for using the mouse to navigate through directories.

FIGURE 25.2. An example of a shareware FTP program logging onto the Borland FTP site.

FTP Using WININET

Now you're ready to look at the code needed to use the WININET DLL in an FTP session. This study will not be exhaustive, but it should help to get you up and running. The first fact you need to know about this technology is that some of the functions in WININET.h return a pointer variable declared to be of type HINTERNET:

typedef LPVOID HINTERNET;

This pointer acts as a handle to the various Internet services you employ. After retrieving the handle, you will pass it in as the first parameter to many of the other WININET functions you call throughout the life of a single session.

You need to remember to return the handle to the system when you're through using it, usually by calling the WININET function called InternetCloseHandle:

BOOL InternetCloseHandle(

  HINTERNET hInet  // Valid Internet handle to be closed.

);


NOTE: Just hearing this much information should tip you off to the fact that you ought to have an object to use this material and should consider creating a component. The tip-off here is the need to perform housekeeping chores with pointers.

I no longer believe in trying to write complex cleanup code on-the-fly. Most of the time I remember to deallocate memory that I have allocated, and I almost always remember to allocate memory before trying to use it. However, computers aren't very considerate about human weaknesses, even infrequent weaknesses. You want to get these things right all the time, and almost just isn't good enough!

Possible solutions involve using a language such as Java or Visual Basic. These languages generally take allocation chores out of your hands. Of course, you almost always have to pay a price for using tools of that kind, and it generally involves a severe performance penalty. If you want speed and flexibility, working in a language such as C++ is always better.

Objects and components are the tools you can use to make C++ safe. If you build an object properly, it will always take care of chores such as allocating and deallocating memory for you. You get the job done right once or find someone who has done it right once, and then you can reuse the object over and over without concern for petty housekeeping chores.

The key point to absorb is that some developers, myself included, believe that almost any moderately complicated chore that involves allocating and deallocating handles is a strong candidate for wrapping in an object. Even better, put the code in a component; then there is almost no chance you will misuse it. The great thing about BCB components is that they are only marginally larger than regular objects, and they're every bit as fast.

In the next few pages, you will learn how to create a component that wraps the FTP calls found in WININET. I will present the WININET calls to you at the same time that I slowly construct the pieces of a component called TMyFTP.

Using InternetOpen

To get a WININET session started, you call InternetOpen:

HINTERNET InternetOpen(

    LPCTSTR lpszAgent,        // Name of app opening the session

    DWORD dwAccessType,       // 
The access type, usually set to 0

    LPCTSTR lpszProxyName,    // For use in specifying a proxy, pass 0

    LPCSTR lpszProxyBypass,   // For use in specifying a proxy, pass NULL

    DWORD dwFlags             // You can set up a callback here.

);


The first parameter is the name of the application opening the session. You can pass in any string you want in this parameter. Microsoft documentation states "This name is used as the user agent in the HTTP protocol." The remaining parameters can be set to 0 or NULL.

The following are some options for use in the dwAccessType parameter: LOCAL_INTERNET_ACCESS Connects only to local Internet sites.

GATEWAY_INTERNET_ACCESS Allows connections to any site on the Web.

CERN_PROXY_INTERNET_ACCESS Uses a CERN proxy to access the Web.

Here are the options as they appear in WININET.h:

#define INTERNET_OPEN_TYPE_PRECONFIG    0   // use registry configuration

#define INTERNET_OPEN_TYPE_DIRECT       1   // direct to net

#define INTERNET_OPEN_TYPE_PROXY        3   // via named proxy


#define PRE_CONFIG_INTERNET_ACCESS      INTERNET_OPEN_TYPE_PRECONFIG

#define LOCAL_INTERNET_ACCESS           INTERNET_OPEN_TYPE_DIRECT

#define GATEWAY_INTERNET_ACCESS         2   // Internet via gateway

#define CERN_PROXY_INTERNET_ACCESS      
INTERNET_OPEN_TYPE_PROXY

As you can see, passing in zero means that you will use information already stored in the Registry. The rest of the parameters are involved with setting up a proxy server, except for the last one, which can be used to set up a callback if you need it. The last parameter has only one possible flag:

INTERNET_FLAG_ASYNC

Refer to the Microsoft documentation for additional information.

Here is an example of a typical call to InternetOpen:

FINet = InternetOpen("WININET1", 0, NULL, 0, 0);

Using InternetConnect

After you open the session, the next step is to connect to the server using InternetConnect:

HINTERNET InternetConnect(

  HINTERNET hInternetSession, // Handle from InternetOpen

  LPCTSTR lpszServerName,     // Server: e.g., 
www.borland.com

  INTERNET_PORT nServerPort,  // Usually 0

  LPCTSTR lpszUsername,       // usually anonymous

  LPCTSTR lpszPassword,       // usually your email address

  DWORD dwService,            // FTP, HTTP, or Gopher?

  DWORD dwFlags,              
// Usually 0

  DWORD dwContext             // User defined number for callback

);

Here are the three possible self-explanatory and mutually exclusive flags that can be passed in the dwService parameter:

INTERNET_SERVICE_FTP

INTERNET_SERVICE_GOPHER

INTERNET_SERVICE_HTTP

Here is the option for the dwFlags parameter:

INTERNET_CONNECT_FLAG_PASSIVE

This option is valid only if you passed INTERNET_SERVICE_FTP in the previous parameter. At this time, no other flags are valid for this parameter.

If the session succeeds, InternetOpen returns a valid pointer; otherwise, it returns NULL. Remember that you will have to deallocate this memory later. Doing so in the destructor for an object that takes control of the entire FTP session is probably best.

Here are the two sections' methods in TMyFTP that use InternetOpen and InternetConnect:

__fastcall TMyFtp::TMyFtp(Classes::TComponent* AOwner): TComponent(AOwner)

{

  FCurFiles = new TStringList();

  FINet = InternetOpen("WinINet1", 0, NULL, 
0, 0);

}

bool __fastcall TMyFtp::Connect(void)

{

  AnsiString S;

  AnsiString CR1("\x00D\x00A");

  FContext = 255;

  FFtpHandle = InternetConnect(FINet, FServer.c_str(), 0,

   FUserID.c_str(), FPassword.c_str(),

   
INTERNET_SERVICE_FTP, 0, FContext);

  if (FFtpHandle == NULL)

  {

    S = "Connection failed" + CR1 +

         "Server: " + FServer + CR1 +

         "UserID: " + FUserID + CR1 +

         "Password: " + 
FPassword;

    ShowMessage(S);

    return FALSE;

  }

  else

    SetUpNewDir();

  return TRUE;

}

Besides calling InternetOpen, the constructor also allocates memory for a TStringList. This list will be used to hold the names of the files in the directories visited by the FTP session. The connect method provides code that pops up a message explaining exactly what went wrong in case of error. This function would probably be stronger if it contained exception-handling code:

void __fastcall TMyFtp::Connect(void)

{

  AnsiString S;

  AnsiString CR1("\x00D\x00A");

  FContext = 255;

  FFtpHandle = InternetConnect(FINet, FServer.c_str(), 0,

   
FUserID.c_str(), FPassword.c_str(),

   INTERNET_SERVICE_FTP, 0, FContext);

  if (FFtpHandle == NULL)

  {

    S = "Connection failed" + CR1 +

         "Server: " + FServer + CR1 +

         "UserID: " + FUserID + CR1 
+

         "Password: " + FPassword;

    throw Exception(S);

  }

  else

    SetUpNewDir();

}

The key difference to note about this new version of the function is that it does not return a value. You don't have to concern yourself with whether the function succeeds because none of the code after the exception is raised will be executed. Your program itself won't end, but you will automatically be popped out of the current process and sent back to the message loop if something goes wrong. The only way to stop that process is to catch the exception. As you learned in Chapter 5, "Exceptions," it is usually best not to try to handle the exception in a catch block, but instead to let the exception-handling process resolve the problem for you automatically.

When you call the Connect function, you might want to do so in a function that looks like this:

void __fastcall 
TForm1::ConnectBtnClick(TObject *Sender)

{

  if (FTPNames->GetConnectionData())

  {

    Application->ProcessMessages();

    Ftp->Server = FTPNames->Server;

    Ftp->UserID = FTPNames->UserID;

    Ftp->Password = 
FTPNames->Password;

    Screen->Cursor = TCursor(crHourGlass);

    Ftp->Connect();

    Screen->Cursor = TCursor(crDefault);

  }

}

The GetConnectionData function makes sure that the Server, UserID, and Password properties are filled in correctly. Notice that the function calls ProcessMessages to be sure that the screen is properly redrawn before handing control over to the system. Care is also taken to put up a cursor that asks the user to wait. Especially if something goes wrong, the system could take a minute or more to return from a call to InternetConnect. While you're waiting for the system to either time out or resolve the call, you want the screen to look right, and you want to tell users that all is well and that they should sit tight.

After Connecting

After you are connected, you can call the GetCurrentDirectory to retrieve the name of the current directory:

System::AnsiString __fastcall TMyFtp::GetCurrentDirectory(void)

{

  DWORD Len = 0;

  FtpGetCurrentDirectory(FFtpHandle, FCurDir.c_str(), &Len);

  
FCurDir.SetLength(Len);

  FtpGetCurrentDirectory(FFtpHandle, FCurDir.c_str(), &Len);

  return FCurDir;

}

This function is declared as follows:

BOOL FtpGetCurrentDirectory(

    IN HINTERNET 
hFtpSession,           // handle from InternetConnect

    OUT LPCTSTR lpszCurrentDirectory,   // directory returned here

    IN OUT LPDWORD lpdwCurrentDirectory // buf size of 2nd parameter

); // True on success

If you set the last parameter to zero, then WININET will use this parameter to return the length of the directory string. You can then allocate memory for your string and call the function a second time to retrieve the directory name. This process is shown earlier in the GetCurrentDirectory method. (Notice the call to SetLength. C++Builder requires that you allocate memory for the new long strings in situations like this. The issue here is that the string will be assigned a value inside the operating system, not inside your C++Builder application. As a result, C++Builder can't perform its usual surreptitious string allocations in these circumstances.)

The following set of functions returns the currently available files in a particular directory:

Classes::TStringList* __fastcall TMyFtp::FindFiles(void)

{

  WIN32_FIND_DATA FindData;

  HINTERNET FindHandle;

  AnsiString Temp;

   FCurFiles->Clear();

   FindHandle = 
FtpFindFirstFile(FFtpHandle, "*.*", &FindData, 0, 0);

   if (FindHandle == NULL)

   {

     return FCurFiles;

   }

   GetFindDataStr(FindData, &Temp);

   FCurFiles->Add(Temp);

   while (InternetFindNextFile(FindHandle, 
&FindData))

   {

     GetFindDataStr(FindData, &Temp);

     FCurFiles->Add(Temp);

   }

   InternetCloseHandle(FindHandle);

   return FCurFiles;

}

The key functions to notice here are FtpFindFirstFile, InternetFindNextFile, and InternetCloseHandle. You use these functions in a manner similar to that employed when calling the C++Builder functions FindFirst, FindNext, and FindClose. In particular, you use FtpFindFirstFile to get the first file in a directory. You then call InternetFindNextFile repeatedly, until the function returns False. After finishing the session, call InternetCloseHandle to inform the operating system that it can deallocate the memory associated with this process.

The following function returns a simple string designating what type of file is retrieved by a call to ftpFindFirstFile or InternetFindNextFile:

AnsiString *GetFindDataStr(WIN32_FIND_DATA FindData, AnsiString *S)

{

  AnsiString Temp;

  switch (FindData.dwFileAttributes)

  {

    case FILE_ATTRIBUTE_ARCHIVE:

    {

      *S = `A';

      break;

    }

    case 
FILE_ATTRIBUTE_COMPRESSED:

      *S = `C';

      break;

    case FILE_ATTRIBUTE_DIRECTORY:

      *S = `D';

      break;

    case FILE_ATTRIBUTE_HIDDEN:

      *S = `H';

      break;

    case FILE_ATTRIBUTE_NORMAL:

      *S = `N';

      
break;

    case FILE_ATTRIBUTE_READONLY:

      *S = `R';

      break;

    case FILE_ATTRIBUTE_SYSTEM:

      *S = `S';

      break;

    case FILE_ATTRIBUTE_TEMPORARY:

      *S = `T';

      break;

    default:

      *S = 
IntToStr(FindData.dwFileAttributes);

  }

  *S = *S + GetDots(75);

  S->Insert(FindData.cFileName, 6);

  Temp = IntToStr(FindData.nFileSizeLow);

  S->Insert(Temp, 25);

  return S;

}

I use this information to create a simple string I can show to the user explaining the type of file currently under examination. For example, if I find a directory, the string might look like this:

D WINDOWS

If I find a file, the string might look like this:

F AUTOEXEC.BAT

One final note: Unlike the functions and structures mentioned in the preceding few paragraphs, WIN32_FIND_DATA is not defined in WININET.h, but instead can be found in WinBase.h and other standard Windows files. Detailed information on this structure is available in the WIN32 help file that ships with C++Builder. A second constant called TWin32FindData mapped to the same value is declared in the \include\vcl\Windows.hpp file that ships with C++Builder.

Retrieving a File

You can use the ftpGetFile function from WININET.h to retrieve a file via FTP:

BOOL FtpGetFile(

    IN HINTERNET hFtpSession, // Returned by InternetConnect

    IN LPCTSTR lpszRemoteFile, // File to get

    IN LPCTSTR lpszNewFile, // Where to put it on your PC

    IN 
BOOL fFailIfExists, // Overwrite existing files?

    IN DWORD dwFlagsAndAttributes, // File attribute-See CreateFile.

    IN DWORD dwFlags, // Binary or ASCII transfer

    IN DWORD dwContext// Usually zero

); // True on success

The following is an example of how to use this call:

bool __fastcall TMyFtp::GetFile(System::AnsiString FTPFile,  System::AnsiStringNewFile)

{

  return FtpGetFile(FFtpHandle, FTPFile.c_str(), NewFile.c_str(),

                   
False, FILE_ATTRIBUTE_NORMAL,

                   FTP_TRANSFER_TYPE_BINARY, 0);

}

To learn about the parameters that can be passed in the dwFlagsAndAttributes parameter, look up CreateFile in the WIN32 help file that ships with C++Builder. The dwFlags parameter can be set to either FTP_TRANSFER_TYPE_BINARY or FTP_TRANSFER_TYPE_ASCII.

Sending Files to an FTP Server

When you're sending files to an NT site, remember that you probably don't have rights in the default FTP directory. Instead, you should change to another directory where your user has rights. You can usually configure what rights a particular user has on a server through the server- side tools provided for administrating user accounts.

This function copies a file to a server:

bool __fastcall TMyFtp::SendFile1(System::AnsiString FTPFile,

                                  
System::AnsiString NewFile)

{

  DWORD Size = 3000;

  AnsiString S;

  BOOL Transfer = FtpPutFile(FFtpHandle,

                       FTPFile.c_str(),

                       NewFile.c_str(),

                       FTP_TRANSFER_TYPE_BINARY, 0);

  
if (!Transfer)

  {

    int Error = GetLastError();

    S = Format("Error Number: %d. Hex: %x", OPENARRAY(TVarRec, (Error, Error)));

    ShowMessage(S);

    S.SetLength(Size);

    if (!InternetGetLastResponseInfo(&(DWORD)Error, 
S.c_str(), &Size))

    {

      Error = GetLastError();

      ShowMessage(Format("Error Number: %d. Hex: %x", OPENARRAY(TVarRec, (Error,Error))));

    }

    ShowMessage(Format("Error Number: %d. Hex: %x Info: %s",

                
OPENARRAY(TVarRec, (Error, Error, S))));

  }

  else

    ShowMessage("Success");



  return Transfer;

}

The core function looks like this:

BOOL Transfer = FtpPutFile(FFtpHandle,

                       
FTPFile.c_str(),

                       NewFile.c_str(),

                       FTP_TRANSFER_TYPE_BINARY, 0);

FtpPutFile takes

The rest of the code in the SendFile1 function is dedicated to error handling. Call GetLastError to retrieve the error code, and call InternetGetLastResponseInfo to retrieve a human-readable description of the error.

Deleting Files

The act of deleting a file on a server is extremely simple:

bool __fastcall 
TMyFtp::DeleteFile(System::AnsiString S)

{

  return FtpDeleteFile(FFtpHandle, S.c_str());

}

FtpDeleteFile takes a handle to the current FTP session in the first parameter and a string specifying the file to delete in the second parameter. I find it hard to imagine how the call could be much simpler.

Creating and Removing Directories

WININET makes the process of creating and deleting directories trivial. Each purpose has one function, and each takes HINTERNET for your connection in the first parameter and the name of the directory you want to create or destroy in the second parameter:

BOOL FtpCreateDirectory(

    
HINTERNET hFtpSession,   // Handle to session

    LPCTSTR lpszDirectory    // Name of directory

);

BOOL FtpRemoveDirectory(

    HINTERNET hFtpSession,   // Handle to session

    LPCTSTR lpszDirectory    // Name of directory



);

The following two simple functions demonstrate how to use the routines:



bool __fastcall TMyFtp::CreateDirectory(AnsiString S)

{

  return 
FtpCreateDirectory(FFtpHandle, S.c_str());

}

bool __fastcall TMyFtp::RemoveDir(System::AnsiString S)

{

  return FtpRemoveDirectory(FFtpHandle, S.c_str());

}

Assuming the presence of these routines, you can then write a function like the following to provide an interface with which the user can interact:

void __fastcall TForm1::RemoveDirectory1Click(TObject *Sender)

{

  AnsiString Title("Delete Directory?");

  AnsiString S = 
ListBox1->Items->Strings[ListBox1->ItemIndex];

  S = *Ftp->CustomToFileName(&S);

  if (MessageBox((HWND)Handle, S.c_str(), Title.c_str(),

      MB_YESNO | MB_ICONQUESTION) == ID_YES)

  {

    Ftp->RemoveDir(S);

    
NewDirClick(NULL);

  }

}

This routine first retrieves the name of the directory you want to delete from a list box. It then calls the CustomToFileName routine, which converts the string shown in the list box into a simple directory name by stripping off information about the size of the directory and the time it was created. The MessageBox function is then used to check with the user to be sure that this action is really what he or she wants to do. If the user replies in the affirmative, the directory is deleted, and the new state of the directory is shown to the user.

Here is a similar function used to create a directory:

void __fastcall 
TForm1::CreateDirectory1Click(TObject *Sender)

{

  AnsiString S;

  if (InputQuery("Create Directory", "Directory Name", S))

  {

    Ftp->CreateDirectory(S);

    NewDirClick(NULL);

  }

}

In this case, the VCL InputQuery dialog is invoked. This function takes a title in the first parameter, a prompt in the second parameter, and the string you want the user to edit in the third parameter. If the user clicks the OK button in the dialog, then the directory is created, and the user's view of the directory is refreshed by a call to NewDirClick.

A Sample FTP Control

You now know the basics about the process of writing an FTP control with WININET. Listings 25.1 and 25.2 show the complete code to a simple control that can set up an FTP session for you. As is, the control lets you use the Object Inspector to define the RemoteServer, UserID, and Password. The code also automatically returns the current directory in a TStringList and allows you to perform file transfers.

In Listings 25.3 through 25.6, you will find a sample program that uses the control. The main screen for the program is shown in Figure 25.3, and a form in which users can select FTP connections is shown in Figure 25.4.


NOTE: FTP2.CPP has a dependency on CODEBOX.CPP. Both files are stored in the UTILS directory on the CD-ROM that accompanies this book. Due to the nature of the C++ linker, CODEBOX.CPP must be compiled to binary form before you can link the control into the Component Library. To build CODEBOX.CPP, compile the BUILDOBJS.MAK project in the UTILS directory. If you do this, and the linker still complains about CODEBOX, then make a small change to FTP2.CPP, such as adding and then deleting a space, then save your changes and try to compile the component library again. See the readme file on the CD-ROM that accompanies this book for additional information.

FTP2.CPP needs the CUNLEASHED alias set up in the database tools. The readme file on the disc discusses this alias at some length. Basically, it's a Paradox alias pointing at the Data directory from the CD that accompanies this book. As always, make sure the source code and data are copied from the CD to your hard drive before trying to compile or run this program.

FIGURE 25.3. The main form for the FTPWININET program. Here I'm connected to ftp.download.com, in the all-important Games directory.

FIGURE 25.4. A form used by the FTPWININET program to allow users to select an FTP connection from a table.

Listing 25.1. The header file for the FTP component.

///////////////////////////////////////

// FTP.h

// FTP Component

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef Ftp2H

#define Ftp2H



class TMyFtp : public Classes::TComponent

{

  typedef Classes::TComponent* 
inherited;

private:

  int FContext;

  bool FConnected;

  void *FINet;

  void *FFtpHandle;

  Classes::TStringList* FCurFiles;

  System::AnsiString FServer;

  Classes::TNotifyEvent FOnNewDir;

  System::AnsiString FCurDir;

  System::AnsiString 
FUserID;

  System::AnsiString FPassword;

  System::AnsiString __fastcall GetCurrentDirectory(void);

  void __fastcall SetUpNewDir(void);

protected:

  fastcall virtual ~TMyFtp(void);

public:

  fastcall TMyFtp(Classes::TComponent* AOwner);

  
void __fastcall Connect(void);

  Classes::TStringList* __fastcall FindFiles(void);

  bool __fastcall BackOneDir(void);

  bool __fastcall ChangeDir(System::AnsiString *S);

  bool __fastcall ChangeDirCustom(System::AnsiString *S);

  bool __fastcall 
CreateDirectory(AnsiString S);

  bool __fastcall RemoveDir(System::AnsiString S);

  bool __fastcall RemoveDirCustom(System::AnsiString S);

  bool __fastcall DeleteFile(System::AnsiString S);

  bool __fastcall DeleteFileCustom(System::AnsiString 
S);

  bool __fastcall GetFile(System::AnsiString FTPFile,  System::AnsiString NewFile);

  bool __fastcall SendFile1(System::AnsiString FTPFile,  System::AnsiStringNewFile);

  bool __fastcall SendFile2(System::AnsiString FTPFile,  
System::AnsiStringNewFile);

  System::AnsiString *__fastcall CustomToFileName(System::AnsiString *S);

__published:

  __property Classes::TStringList* CurFiles = {read=FCurFiles, nodefault};

  __property System::AnsiString CurDir = 
{read=GetCurrentDirectory, nodefault};

  __property System::AnsiString UserID = {read=FUserID, write=FUserID, nodefault};

  __property System::AnsiString Password = {read=FPassword, write=FPassword,nodefault};

  __property System::AnsiString Server 
= {read=FServer, write=FServer, nodefault};

  __property Classes::TNotifyEvent OnNewDir = {read=FOnNewDir, write=FOnNewDir};

};



#endif



Listing 25.2. The source for the FTP component.

///////////////////////////////////////

// FTP.cpp

// FTP Component

// Copyright (c) 1997 by Charlie Calvert

//

///////////////////////////////////////

// Add a "#pragma comment(lib, 
"Inet.lib")" statement to the

// module that requires the lib.

// Add a "#pragma link "codebox.obj"" into the module that needs

// an obj.

///////////////////////////////////////

#include <vcl\vcl.h>


#include <wininet.h>

#pragma hdrstop

#pragma comment(lib, "Inet.lib")

#pragma link "codebox.obj"

#include "codebox.h"

#include "Ftp2.h"



__fastcall TMyFtp::~TMyFtp(void)

{

  if (FINet != NULL)

    
InternetCloseHandle(FINet);

  if (FFtpHandle != NULL)

    InternetCloseHandle(FFtpHandle);

}



__fastcall TMyFtp::TMyFtp(Classes::TComponent* AOwner)

: TComponent(AOwner)

{

  FCurFiles = new TStringList();

  FINet = 
InternetOpen("WinINet1", 0, NULL, 0, 0);

  FFtpHandle = NULL;

  FConnected = False;

}



void __fastcall TMyFtp::Connect(void)

{

  AnsiString S;

  AnsiString CR1("\x00D\x00A");



  FContext = 255;

  FFtpHandle = 
InternetConnect(FINet, FServer.c_str(), 0,

   FUserID.c_str(), FPassword.c_str(),

   INTERNET_SERVICE_FTP, 0, FContext);

  if (FFtpHandle == NULL)

  {

    S = "Connection failed" + CR1 +

         "Server: " + FServer + CR1 +

         
"UserID: " + FUserID + CR1 +

         "Password: " + FPassword;

    throw Exception(S);

  }

  else

  {

    FConnected = True;

    SetUpNewDir();

  }

}



System::AnsiString __fastcall TMyFtp::GetCurrentDirectory(void)

{

  
DWORD Len = 0;



  FtpGetCurrentDirectory(FFtpHandle, FCurDir.c_str(), &Len);

  FCurDir.SetLength(Len);

  FtpGetCurrentDirectory(FFtpHandle, FCurDir.c_str(), &Len);

  return FCurDir;

}



void __fastcall TMyFtp::SetUpNewDir(void)

{

  
FCurDir = GetCurrentDirectory();

  if (FOnNewDir != NULL)

    FOnNewDir(this);

}



AnsiString GetDots(int NumDots)

{

  AnsiString S;

  int i;

  for (i = 1; i <= NumDots; i++)

    S = S + " ";

  return S;

}



AnsiString 
*GetFindDataStr(WIN32_FIND_DATA FindData, AnsiString *S)

{

  AnsiString Temp;

  switch (FindData.dwFileAttributes)

  {

    case FILE_ATTRIBUTE_ARCHIVE:

    {

      *S = `A';

      break;

    }



    case FILE_ATTRIBUTE_COMPRESSED:

      *S 
= `C';

      break;



    case FILE_ATTRIBUTE_DIRECTORY:

      *S = `D';

      break;



    case FILE_ATTRIBUTE_HIDDEN:

      *S = `H';

      break;



    case FILE_ATTRIBUTE_NORMAL:

      *S = `N';

      break;



    case 
FILE_ATTRIBUTE_READONLY:

      *S = `R';

      break;



    case FILE_ATTRIBUTE_SYSTEM:

      *S = `S';

      break;



    case FILE_ATTRIBUTE_TEMPORARY:

      *S = `T';

      break;



    default:

      *S = 
IntToStr(FindData.dwFileAttributes);

  }



  *S = *S + GetDots(75);

  S->Insert(FindData.cFileName, 6);

  Temp = IntToStr(FindData.nFileSizeLow);

  S->Insert(Temp, 25);

  return S;

}



Classes::TStringList* __fastcall 
TMyFtp::FindFiles(void)

{

  WIN32_FIND_DATA FindData;

  HINTERNET FindHandle;

  AnsiString Temp;



   FCurFiles->Clear();

   FindHandle = FtpFindFirstFile(FFtpHandle, "*.*", &FindData, 0, 0);

   if (FindHandle == NULL)

   {

     
return FCurFiles;

   }



   GetFindDataStr(FindData, &Temp);

   FCurFiles->Add(Temp);



   while (InternetFindNextFile(FindHandle, &FindData))

   {

     GetFindDataStr(FindData, &Temp);

     FCurFiles->Add(Temp);

   }

   
InternetCloseHandle(FindHandle);



   return FCurFiles;

}



bool __fastcall TMyFtp::ChangeDir(System::AnsiString *S)

{

  if(!FConnected)

    throw Exception("You must connect first!");

  if (S->Length() != 0)

    if 
(!FtpSetCurrentDirectory(FFtpHandle, S->c_str()))

    {

      ShowMessage("Could not change to: " + *S);

      return FALSE;

    }

  FindFiles();

  SetUpNewDir();

  return TRUE;

}



System::AnsiString *__fastcall 
TMyFtp::CustomToFileName(System::AnsiString *S)

{

  const int PreSize = 5;

  AnsiString Temp;

  int TempSize;



  TempSize = S->Length() - PreSize;

  Temp.SetLength(TempSize);

  *S = StripFromFront(*S, PreSize);

  memcpy(Temp.c_str(), 
S->c_str(), TempSize);

  *S = GetFirstToken(Temp, ` `);

  return S;

}



bool __fastcall TMyFtp::ChangeDirCustom(System::AnsiString *S)

{

  AnsiString Temp = *CustomToFileName(S);

  return ChangeDir(&Temp);

}



bool __fastcall 
TMyFtp::CreateDirectory(AnsiString S)

{

  return FtpCreateDirectory(FFtpHandle, S.c_str());

}



bool __fastcall TMyFtp::RemoveDir(System::AnsiString S)

{

  return FtpRemoveDirectory(FFtpHandle, S.c_str());

}



bool __fastcall 
TMyFtp::RemoveDirCustom(System::AnsiString S)

{

  AnsiString Temp = *CustomToFileName(&S);

  return RemoveDir(Temp);

}



bool __fastcall TMyFtp::BackOneDir(void)

{

  AnsiString S;

  S = FCurDir;

  S = StripLastToken(S, `/');

  if (S == 
`/')

  {

    return FALSE;

  }



  if (S.Length() != 0)

  {

    ChangeDir(&S);

  }

  else

  {

    S = `/';

    ChangeDir(&S);

  }

  return TRUE;

}



bool __fastcall TMyFtp::DeleteFile(System::AnsiString S)

{

  return 
FtpDeleteFile(FFtpHandle, S.c_str());

}



bool __fastcall TMyFtp::DeleteFileCustom(System::AnsiString S)

{

  S = *CustomToFileName(&S);

  return DeleteFile(S);

}



bool __fastcall TMyFtp::GetFile(System::AnsiString FTPFile,

  
System::AnsiString NewFile)

{

  return FtpGetFile(FFtpHandle, FTPFile.c_str(), NewFile.c_str(),

    False, FILE_ATTRIBUTE_NORMAL, FTP_TRANSFER_TYPE_BINARY, 0);

}



bool __fastcall TMyFtp::SendFile1(System::AnsiString FTPFile,

  
System::AnsiString NewFile)

{

  DWORD Size = 3000;

  AnsiString S;



  BOOL Transfer = FtpPutFile(FFtpHandle,

                       FTPFile.c_str(),

                       NewFile.c_str(),

                       FTP_TRANSFER_TYPE_BINARY, 0);

  
if (!Transfer)

  {

    int Error = GetLastError();

    S = Format("Error Number: %d. Hex: %x",

      OPENARRAY(TVarRec, (Error, Error)));

    ShowMessage(S);

    S.SetLength(Size);

    if 
(!InternetGetLastResponseInfo(&(DWORD)Error, S.c_str(), &Size))

    {

      Error = GetLastError();

      ShowMessage(Format("Error Number: %d. Hex: %x",

        OPENARRAY(TVarRec, (Error, Error))));

    }

    
ShowMessage(Format("Error Number: %d. Hex: %x Info: %s",

                OPENARRAY(TVarRec, (Error, Error, S))));

  }

  else

    ShowMessage("Success");

    

  return Transfer;

}



bool __fastcall 
TMyFtp::SendFile2(System::AnsiString FTPFile,

  System::AnsiString NewFile)

{

  HINTERNET FHandle;



  FHandle = FtpOpenFile(FFtpHandle, "sam.txt", GENERIC_READ,

                        FTP_TRANSFER_TYPE_BINARY, 0);

  if (FHandle != 
NULL)

    InternetCloseHandle(FHandle);

  else

    ShowMessage("Failed");

  return TRUE;

}



namespace Ftp2

{

  void __fastcall Register()

  {

    TComponentClass classes[1] = {__classid(TMyFtp)};

    
RegisterComponents("Unleash", classes, 0);

  }



}



Listing 25.3. Header for the main module of the FTPWININET program showing how to use the FTP component.

///////////////////////////////////////

// File: Main.h

// Project: FtpWinINet

// copyright (c) 1997 by Charlie Calvert

//

#ifndef MainH

#define MainH

#include <vcl\Classes.hpp>

#include 
<vcl\Controls.hpp>

#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include <vcl\ExtCtrls.hpp>

#include <vcl\Buttons.hpp>

#include <vcl\Menus.hpp>

#include <vcl\Dialogs.hpp>

#include 
"Ftp2.h"



class TForm1 : public TForm

{

__published:

  TListBox *ListBox1;

  TImage *Image1;

  TSpeedButton *SpeedButton1;

  TMainMenu *MainMenu1;

  TMenuItem *File1;

  TMenuItem *Connect1;

  TMenuItem *CopyFile1;

  TMenuItem 
*N1;

  TMenuItem *Exit1;

  TMenuItem *SendToSite;

  TSaveDialog *SaveDialog1;

  TMenuItem *Options1;

  TMenuItem *ChangeDirectory1;

  TMenuItem *RemoveDirectory1;

  TMenuItem *CreateDirectory1;

  TMenuItem *DeleteFile1;

  TMenuItem 
*EditConnectionData1;

  TMyFtp *Ftp;

  void __fastcall ConnectBtnClick(TObject *Sender);

  void __fastcall ListBox1DblClick(TObject *Sender);

  void __fastcall BackOneDirectoryBtnClick(TObject *Sender);

  void __fastcall CopyFileClick(TObject 
*Sender);

  // void __fastcall NewDirectory(TObject *Sender);

  void __fastcall NewDirClick(TObject *Sender);

  void __fastcall Exit1Click(TObject *Sender);

  void __fastcall SendToSiteClick(TObject *Sender);

  void __fastcall 
ChangeDirectory1Click(TObject *Sender);

  void __fastcall RemoveDirectory1Click(TObject *Sender);

  void __fastcall CreateDirectory1Click(TObject *Sender);

  void __fastcall DeleteFile1Click(TObject *Sender);

  void __fastcall 
ListBox1DrawItem(TWinControl *Control, int Index,

  const TRect &Rect, TOwnerDrawState State);

  void __fastcall EditConnectionData1Click(TObject *Sender);

  void __fastcall ListBox1MouseDown(TObject *Sender, TMouseButton Button,

  TShiftState 
Shift, int X, int Y);

private:

  // TMyFtp *Ftp;

  AnsiString FStartCaption;

  Graphics::TBitmap *FFileBitmap;

  Graphics::TBitmap *FFolderBitmap;

  virtual __fastcall ~TForm1(void);

  void ActivateMenus();

public:

  virtual __fastcall 
TForm1(TComponent* Owner);

};



extern TForm1 *Form1;



#endif



Listing 25.4. The main module of the FTPWININET program showing how to use the FTP component.

///////////////////////////////////////

// File: MAIN.CPP

// Project: FtpWinINet

// copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#pragma hdrstop

#include "Ftp2.h"

#include 
"Main.h"

#include "FtpNames1.h"

#pragma link "Ftp2"

#pragma resource "*.dfm"

#pragma resource "listicon.res"



TForm1 *Form1;



__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

  
FStartCaption = Caption;

  FFolderBitmap = new Graphics::TBitmap;

  FFileBitmap = new Graphics::TBitmap;

  FFolderBitmap->Handle = LoadBitmap((HINSTANCE)HInstance, "FolderBmp");

  FFileBitmap->Handle = 
LoadBitmap((HINSTANCE)HInstance, "FileBmp");

}



__fastcall TForm1::~TForm1()

{

  FFolderBitmap->Free();

  FFileBitmap->Free();

}



void TForm1::ActivateMenus()

{

  int i;



  for (i = 0; i < ComponentCount; i++)

  {

    
if(dynamic_cast<TMenuItem *>(Components[i]))

      dynamic_cast<TMenuItem *>(Components[i])->Enabled = True;

  }

}



void __fastcall TForm1::ConnectBtnClick(TObject *Sender)

{

  Ftp->OnNewDir = NewDirClick;

  if 
(FTPNames->GetConnectionData())

  {

    Application->ProcessMessages();

    Ftp->Server = FTPNames->Server;

    Ftp->UserID = FTPNames->UserID;

    Ftp->Password = FTPNames->Password;

    Screen->Cursor = 
TCursor(crHourGlass);

    Ftp->Connect();

    Screen->Cursor = TCursor(crDefault);

    ActivateMenus();

  }

}



void __fastcall TForm1::ListBox1DblClick(TObject *Sender)

{

  int i = ListBox1->ItemIndex;

  AnsiString S = 
ListBox1->Items->Strings[i];

  Ftp->ChangeDirCustom(&S);

}



void __fastcall TForm1::BackOneDirectoryBtnClick(TObject *Sender)

{

  Screen->Cursor = Controls::TCursor(crHourGlass);

  Ftp->BackOneDir();

  Screen->Cursor = 
Controls::TCursor(crDefault);

}



void __fastcall TForm1::CopyFileClick(TObject *Sender)

{

  AnsiString S = ListBox1->Items->Strings[ListBox1->ItemIndex];

  Ftp->CustomToFileName(&S);

  SaveDialog1->FileName = "C:\\" 
+ S;

  if (SaveDialog1->Execute())

    Ftp->GetFile(S, SaveDialog1->FileName);

}



void __fastcall TForm1::NewDirClick(TObject *Sender)

{

  Caption = FStartCaption + " ->> " + Ftp->CurDir;

  ListBox1->Items = 
Ftp->FindFiles();

}



void __fastcall TForm1::Exit1Click(TObject *Sender)

{

  Close();

}



void __fastcall TForm1::SendToSiteClick(TObject *Sender)

{

  if (SaveDialog1->Execute())

  {

    AnsiString 
SaveFile(ExtractFileName(SaveDialog1->FileName));

    Ftp->SendFile1(SaveDialog1->FileName, SaveFile);

  }

}



void __fastcall TForm1::ChangeDirectory1Click(TObject *Sender)

{

  AnsiString S;

  if (InputQuery("Change 
Directory", "Enter Directory", S))

    Ftp->ChangeDir(&S);

}



void __fastcall TForm1::RemoveDirectory1Click(TObject *Sender)

{

  AnsiString Title("Delete Directory?");



  AnsiString S = 
ListBox1->Items->Strings[ListBox1->ItemIndex];

  S = *Ftp->CustomToFileName(&S);

  if (MessageBox((HWND)Handle, S.c_str(), Title.c_str(),

      MB_YESNO | MB_ICONQUESTION) == ID_YES)

  {

    Ftp->RemoveDir(S);

    
NewDirClick(NULL);

  }

}



void __fastcall TForm1::CreateDirectory1Click(TObject *Sender)

{

  AnsiString S;

  if (InputQuery("Create Directory", "Directory Name", S))

  {

    Ftp->CreateDirectory(S);

    
NewDirClick(NULL);

  }

}



void __fastcall TForm1::DeleteFile1Click(TObject *Sender)

{

  AnsiString S = ListBox1->Items->Strings[ListBox1->ItemIndex];

  S = *Ftp->CustomToFileName(&S);

  if (MessageBox((HWND)Handle, S.c_str(), 
"DeleteFile?",

      MB_YESNO | MB_ICONQUESTION) == ID_YES)

  {

    Ftp->DeleteFile(S);

    NewDirClick(NULL);

  }

}



void __fastcall TForm1::ListBox1DrawItem(TWinControl *Control, int Index, const 

                                         
Windows::TRect &Rect, TOwnerDrawState State)

{

  ListBox1->Canvas->FillRect(Rect);

  AnsiString S = ListBox1->Items->Strings[Index];

  ListBox1->Canvas->TextOut(Rect.Left, Rect.Top, S);

  char ch = S[1];

  if (ch == `N')

    
ListBox1->Canvas->Draw(Rect.Left, Rect.Top, FFileBitmap);

  else

    ListBox1->Canvas->Draw(Rect.Left, Rect.Top, FFolderBitmap); 

}



void __fastcall TForm1::EditConnectionData1Click(TObject *Sender)

{

  FTPNames->ShowModal(); 

}




void __fastcall TForm1::ListBox1MouseDown(TObject *Sender, TMouseButton Button,

                                         TShiftState Shift, int X, int Y)

{

  if (Shift.Contains(ssRight))

  {

    if (ListBox1->ItemIndex >= 0)

      
ShowMessage(ListBox1->Items->Strings[ListBox1->ItemIndex]);

  }

}



Listing 25.5. The header for the module for picking the next connection from a database.

///////////////////////////////////////

// FTPNames.h

// Project: FTPWININET

// Copyright (c) 1997 by Charlie Calvert

//

#ifndef FtpNames1H

#define FtpNames1H

#include <vcl\Classes.hpp>

#include 
<vcl\Controls.hpp>

#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include <vcl\DBTables.hpp>

#include <vcl\DB.hpp>

#include <vcl\DBGrids.hpp>

#include <vcl\Grids.hpp>

#include 
<vcl\ExtCtrls.hpp>

#include <vcl\DBCtrls.hpp>

#include <vcl\Buttons.hpp>



class TFTPNames : public TForm

{

__published:

 TTable *FTPTable;

 TDataSource *FTPSource;

 TAutoIncField *FTPTableCode;

 TStringField 
*FTPTableServer;

 TStringField *FTPTableUserID;

 TStringField *FTPTablePassword;

 TDBGrid *DBGrid1;

 TPanel *Panel1;

 TDBNavigator *DBNavigator1;

 TPanel *Panel2;

 TBitBtn *BitBtn1;

 TBitBtn *BitBtn2;

private:

  AnsiString FServer;

  
AnsiString FUserID;

  AnsiString FPassword;

public:

 virtual __fastcall TFTPNames(TComponent* Owner);

 BOOL GetConnectionData(void);

 __property System::AnsiString Server = {read=FServer};

 __property System::AnsiString UserID = {read=FUserID};

 
__property System::AnsiString Password = {read=FPassword};

};



extern TFTPNames *FTPNames;



#endif



Listing 25.6. The source for the module that lets the user pick the next FTP connection from a database.

///////////////////////////////////////

// FTPNames.cpp

// Project: FTPWININET

// Copyright (c) 1997 by Charlie Calvert

//

#include <vcl\vcl.h>

#pragma hdrstop

#include 
"FtpNames1.h"

#pragma resource "*.dfm"

TFTPNames *FTPNames;



__fastcall TFTPNames::TFTPNames(TComponent* Owner): TForm(Owner)

{

}



BOOL TFTPNames::GetConnectionData(void)

{

  if (ShowModal() == mrOk)

  {

    FServer = 
FTPTableServer->Value;

    FUserID = FTPTableUserID->Value;

    FPassword = FTPTablePassword->Value;

    return TRUE;

  }

  else

    return FALSE;

}

This component is used in the FTPWININET program found on the CD that comes with this book. To use the component, simply drop it on a form and then use the Object Inspector to fill in the Server, UserID, and Password.

To start a session, simply call the Connect method:

void __fastcall TForm1::ConnectBtnClick(TObject *Sender)

{

  MyFtp1->Connect();

}

If you choose not to add the component to the Component Palette, you can create and initialize the FTP component with the following code:

void __fastcall TForm1::ConnectBtnClick(TObject *Sender)

{

  Ftp = new TMyFtp(this);

  Ftp->OnNewDir = NewDirClick;

  Ftp->Server = "devinci";

  Ftp->UserID = 
"ccalvert";

  Ftp->Password = "flapper";

  Ftp->Connect();

  ListBox1->Items = Ftp->FindFiles();



}



void __fastcall 
TForm1::NewDirClick(TObject *Sender)

{

  Caption = FStartCaption + " ->> " + Ftp->CurDir;

  ListBox1->Items = Ftp->FindFiles();

}



For this code to work, you must #include ftp2.h at the top of your file, and you must declare the variable Ftp as a field of TForm1: TMyFtp *Ftp. You should also add NewDirClick to your class declaration in the header file.

The difference between these two versions of the ConnectBtnClick method shows how much simpler using the RAD paradigm is rather than slogging your way through the old coding techniques. Note in particular the second line in the preceding method; this line explicitly sets up the event handler (closure) for the OnNewDir event.

If you respond to the OnNewDir event, you can get a directory listing for the current FTP site mirrored in a ListBox by writing the following line of code:

void __fastcall TForm1::NewDirClick(TObject *Sender)

{

  Caption = FStartCaption + " ->> " + Ftp->CurDir;

  ListBox1->Items = Ftp->FindFiles();

}

The first line of code shown here is optional; it does nothing more than show the current directory in the caption of the program. The FStartCaption variable is a field of the TForm object that is initialized in the constructor for the form:

__fastcall TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

  FStartCaption = Caption;

}

The technique of saving the default caption in a global variable for later reuse is a common one in C++Builder programming.

The FindFile method called in the NewDirClick routine was explained in the section "After Connecting."

After you have displayed a directory of files, the program still needs to provide a technique for letting the user change directories. One simple method is to respond to double-clicks on a directory name by changing into the selected directory.

Creating User Draw List Boxes

When displaying a list of files to the user, you need to provide some clear way of distinguishing files from directories. One simple way to make this distinction is with an owner draw list box that provides different icons for the different types of files and directories you want to show the user.

To get started creating an owner draw list box, change the Style property for the list box to lbOwnerDrawFixed. This means that you want all the items in the list box to have the same height. You can now associate a graphic item with the Object field of each string in the TStringList. Next, you respond to OnDrawItem events.

Here is the constructor for the form, which is used to load the bitmaps to be displayed in the list box from a resource:

fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

  FStartCaption = Caption;

  FFolderBitmap = new Graphics::TBitmap;

  FFileBitmap = new Graphics::TBitmap;

  
FFolderBitmap->Handle = LoadBitmap((HINSTANCE)HInstance, "FolderBmp");

  FFileBitmap->Handle = LoadBitmap((HINSTANCE)HInstance, "FileBmp");

}



The code shown here explicitly sets the OnDrawItem event to the ListBox1DrawItem method, but, of course, you could also do this visually, from the Events page of the Object Inspector. In particular, you could turn to the Events page for the ListBox1 control; then you could set the OnDrawItem event to ListBox1DrawItem or to some other method that you choose. The only requirement, of course, is that the method be of type TNotifyEvent; that is, it must take TObject Sender as its sole parameter.

The code goes on to create two bitmaps and then loads the bitmaps from a resource with the LoadBitmap function. LoadBitmap is a Windows API routine that takes the HInstance for the program in its first parameter and the name of the resource you want to retrieve in its second parameter. You can then assign the handle returned by LoadBitmap to the handle of the native VCL TBitmap object. After it is assigned to TBitmap, you no longer have to worry about disposing the handle, as it will be cleaned up when TBitmap is destroyed. You will, however, have to explicitly destroy the TBitmap objects in the destructor for the form:

__fastcall TForm1::~TForm1()

{

  FFolderBitmap->Free();

  FFileBitmap->Free();

}

The custom RC file called LISTICON.RC looks like this:

FolderBmp BITMAP 
"FOLDER.BMP"

FileBmp BITMAP "FILE.BMP"

You can include this file in your project by choosing Project | Add to Project and then browsing for RC files and adding your custom file to the project, as shown Figure 25.5. This way, you add the following macro to your project file:

USERC("ListIcon.rc");

FIGURE 25.5. Adding an RC file to a BCB project.


Remember that you don't want to add this code into the pre-made RES file that C++Builder makes automatically for your program. Instead, you should create a separate RES file, using the techniques I've described here. The two bitmaps included here are shown in Figure 25.6.

FIGURE 25.6. The two 16x16 16-color folder and file bitmaps used in FTPWININET.


If you don't want to have the C++Builder IDE add the RC file to your project, you can do so explicitly with only a few seconds' work. Here is how to proceed.

Compile the RC file at the command line by passing it as the sole parameter to the BRCC32.EXE utility that ships with C++Builder.

Add the following code at the top of your main form:

#include <vcl\vcl.h>

#pragma hdrstop


#include "Ftp2.h"

#include "Main.h"

#include "FtpNames1.h"

#pragma resource "*.dfm"

#pragma resource "listicon.res"   // Here is the line you should add to your project!

TForm1 *Form1;

__fastcall 
TForm1::TForm1(TComponent* Owner)

: TForm(Owner)

{

  ... // etc

The only line that is important here is the seventh, but I have shown you additional lines from the program so that you can find the context in which to place the file. Both techniques work fine, though the first is probably preferred.

Now that all the resource issues are finally out of the way, you can write the following code to display the bitmaps in a list box:

void 
__fastcall TForm1::ListBox1DrawItem(TWinControl *Control, int Index,

  const Windows::TRect &Rect, TOwnerDrawState State)

{

  ListBox1->Canvas->FillRect(Rect);

  AnsiString S = ListBox1->Items->Strings[Index];

  
ListBox1->Canvas->TextOut(Rect.Left, Rect.Top, S);

  char ch = S[1];

  if (ch == `N')

    ListBox1->Canvas->Draw(Rect.Left, Rect.Top, FFileBitmap);

  else

    ListBox1->Canvas->Draw(Rect.Left, Rect.Top, FFolderBitmap);

}


The header for this method is created automatically for you when you click the TListBox OnDrawItem event in the Object Inspector. Alternatively, you can add the method manually, being sure to modify both the header file and the CPP file. Obviously, you can add additional code if you want to handle distinctions between normal files and system files, and so on.

OnDrawItem event handlers like ListBox1DrawItem get four parameters:

The final TOwnerDrawState parameter is a set used to designate whether the current item is in one of the following states:

odSelected: The item is selected.

odDisabled: The entire list box is disabled.

odFocused: The item currently has focus.

To find out more about TOwnerDrawState, browse for that value in StdCtrls.hpp.

The code for the event handler first blanks out the Rect in white, so no artifacts appear in the control:

ListBox1->Canvas->FillRect(Rect);

The code then gets the appropriate string to display from the list box's string list. After the code retrieves the string, the string is displayed with the TextOut function from the Canvas field of the TListBox:

AnsiString S = ListBox1->Items->Strings[Index];

ListBox1->Canvas->TextOut(Rect.Left, Rect.Top, S);

The TRect structure passed to the OnDrawItem event handler is used to calculate the location in the list box where the information should be displayed. Don't forget that the VCL ensures that the font to be used is already selected in the Canvas property for the list box, so you don't have to worry about that either.

The last step is to actually draw the bitmap to the screen:

char ch = S[1];

if (ch == `N')

  
ListBox1->Canvas->Draw(Rect.Left, Rect.Top, FFileBitmap);

else

  ListBox1->Canvas->Draw(Rect.Left, Rect.Top, FFolderBitmap);

The strings that I display in the list box begin with either an N or a D, depending on whether they reference a file or a directory. This display would be an aesthetic atrocity, were it not for the fact that the bitmap obliterates this letter, replacing it with a "pretty" image. Keeping the letter in there is nice though, as I can sort the list box on this letter, thereby ensuring that all the directories are displayed at the top of the list box and the files next.

Old hands at Windows are no doubt dreading the moment when they will have to actually use BitBlt to place the bitmap on the screen. However, all the traditional "horror" is taken out of the process by the VCL, which allows you to display the bits through a simple call to the Draw method of the canvas for TListBox. Notice that you can just pass in the TBitmap object made in the form's constructor as the third parameter to Draw. Internally, the VCL will calculate the size of the bitmap and call BitBlt for you.


NOTE: The VCL really shines here. Notice how simple it is to dovetail the TBitmap object with the Windows API LoadBitmap function. Then, after you have the bitmap in your hands, you can just pass it to the ListBox itself when you need to display it. In effect, all you have to say is "Here, Mr. List Box, you take this and draw it to the screen for me." The list box, being a well-designed object, is happy to do your bidding.

The point here is that all the pieces fit together very nicely. The VCL always walks a fine line between making tasks too simple and making them too hard. If the VCL makes the process too simple, the code would almost certainly be inefficient from a memory and performance point of view and also difficult to customize. If this process were too difficult, then programmers would have to spend too much time selecting fonts into device contexts and deciding which resources need to be destroyed and how. The art of the VCL is to take you right down the middle of this obstacle course. It gives you enough control so that you can customize to your liking, but does not leave you with so many details to manage that you are likely to end up with a program that leaks resources like a ship with a bad case of dry rot!

I often study VCL code when I want to pick up tips on how to construct safe, easy-to-use objects. As I mentioned earlier, one of the useful chores an object can do for you is to help with the cleanup of resources. The VCL does this job in many cases, and studying how this job is done is often worthwhile.

This section on owner draw list boxes has turned out to be longer than I expected, but I hope you see that the actual process described is not very difficult. The key here is just to understand a little about resources and bitmaps, and then to understand the parameters passed to the OnDrawItem event. If you have those pieces down, then the rest should fall in place fairly easily.

A Few Words on the FTP OCX Control

If you don't want to create your own FTP component, you can use the TFTP component that resides on the Internet page of the Component Palette. The disadvantages of this system include having to ship a separate OCX file and the fact that OCXs are larger than native components. (The FTP OCX is about 250KB, and the WININET component I built is around 50KB with debug information.) The advantages of using an OCX component are that it is probably already debugged and that a company stands behind it and can give you support.

To get help on the FTP component, right-click it and select Properties. Then click the Help button. A standard Windows help file is launched, with reasonably complete help. These help files are stored in the Windows system directory.

Listings 25.7 and 25.8 contain the source to a test program that uses the OCX controls that ship with BCB. Needless to say, this program will not work correctly unless the OCX controls are installed on the Component Palette. By default, these controls are installed automatically by BCB on the Internet page of the Component Palette.

Listing 25.7. The FTPICP program shows how to use the FTP ActiveX component from the Internet page of the Component Palette.

///////////////////////////////////////

// main.h

// FtpIcp: Use Active X Control for Ftp

// Copyright (c) 1997 by Charlie calvert

//

#ifndef MainH

#define MainH


#include <vcl\Classes.hpp>

#include <vcl\Controls.hpp>

#include <vcl\StdCtrls.hpp>

#include <vcl\Forms.hpp>

#include <vcl\ISP.hpp>

#include <vcl\OleCtrls.hpp>

class TForm1 : public TForm

{

__published:

  
TFTP *Ftp1;

  TListBox *ListBox1;

  TListBox *ListBox2;

  void __fastcall FormCreate(TObject *Sender);

  void __fastcall FormDestroy(TObject *Sender);

  void __fastcall Ftp1StateChanged(TObject *Sender, short State);

  void __fastcall 
Ftp1ProtocolStateChanged(TObject *Sender, short ProtocolState);

  void __fastcall Ftp1ListItem(TObject *Sender, const Variant &Item);

private:

public:

  virtual __fastcall TForm1(TComponent* Owner);

};

extern TForm1 *Form1;

#endif




Listing 25.8. The main source file for the FTPICP program.

///////////////////////////////////////

// main.cpp

// FtpIcp: Use Active X Control for Ftp

// Copyright (c) 1997 by Charlie calvert

//

#include <vcl\vcl.h>

#include "icpbox.h"

#pragma hdrstop

#include 
"Main.h"

#pragma resource "*.dfm"

TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)

  : TForm(Owner)

{

}

void __fastcall TForm1::FormCreate(TObject *Sender)

{

  Ftp1->Connect(Ftp1->RemoteHost, 
Ftp1->RemotePort);

}

void __fastcall TForm1::FormDestroy(TObject *Sender)

{

  Ftp1->Cancel();

  Ftp1->Quit();

}

void __fastcall TForm1::Ftp1StateChanged(TObject *Sender, short State)

{

  AnsiString S;

  switch(State)

  {

    case 
prcConnecting: S = "Connecting"; break;

    case prcConnected: S = "Connected"; break;

    case prcResolvingHost: S = "Resolving Host"; break;

    case prcHostResolved: S = "Host Resolved"; break;

    
default:

      S = "State Unknown";

  }

  ListBox1->Items->Add(S);

}

void __fastcall TForm1::Ftp1ProtocolStateChanged(TObject *Sender,

                                                 short ProtocolState)

{

  AnsiString S;

  
switch(ProtocolState)

  {

    case ftpAuthentication:

     S = "Authenticate";

     Ftp1->Authenticate(Ftp1->UserId, Ftp1->Password);

     break;

    case ftpTransaction:

      S = "Transaction";

      
Ftp1->List("/");

      break;

  }

  ListBox1->Items->Add(S);

}

void __fastcall TForm1::Ftp1ListItem(TObject *Sender, const Variant &Item)

{

  ListBox2->Items->Add(ParseFtpItem(Item));

}

This sample program will automatically connect you to sites on the Internet or intranet where you can download files. Before running it, check to make sure you know which site you will be connected to, as explained in the next few paragraphs.

FIGURE 25.7. The main screen from the FTPICP program that uses an ActiveX control to connect to the Internet via FTP.


You can use the Object Inspector to set the Password, RemoteHost, RemotePort, and UserId for the component. For example, you might set them like this if you want to connect to a server on your current machine:

RemoteHost = 127.0.0.1

RemotePort = 21

UserId = Anonymous

Password = ccalvert@wpo.borland.com

To connect to Microsoft's FTP site, use ftp.microsoft.com rather than 127.0.0.1. Needless to say, you should enter your own e-mail address, not mine, when you're connecting to a site.

To connect to a server, you write the following:

Ftp1->Connect(Ftp1->RemoteHost, 
Ftp1->RemotePort);

When you're finished, you should close out the session:

void __fastcall TForm1::FormDestroy(TObject *Sender)

{

  Ftp1->Cancel();

  Ftp1->Quit();

}

The Cancel command cancels whatever command might currently be executing. This command is helpful because users will often quit a program when a command is taking too long to execute. Long delays are common in FTP communications, not because the control is slow, but because the Internet is slow.

The FTP control will use events to notify you when events occur. In particular, you can use the OnStateChanged event to keep the user informed about what is happening:

void __fastcall TForm1::Ftp1StateChanged(TObject *Sender, short State)

{

  AnsiString S;

  switch(State)

  {

    case prcConnecting: S = "Connecting"; break;

    case prcConnected: S = "Connected"; break;

    
case prcResolvingHost: S = "Resolving Host"; break;

    case prcHostResolved: S = "Host Resolved"; break;

    default:

      S = "State Unknown";

  }

  ListBox1->Items->Add(S);

}

The State variable passed to the control can be set to one of the following values:

#define prcConnecting (unsigned char)(1)

#define prcResolvingHost (unsigned char)(2)

#define prcHostResolved (unsigned char)(3)


#define prcConnected (unsigned char)(4)

#define prcDisconnecting (unsigned char)(5)

#define prcDisconnected (unsigned char)(6)

#define prcConnectTimeout (unsigned char)(1)

#define prcReceiveTimeout (unsigned char)(2)

#define prcUserTimeout 
(unsigned char)(65)

#define prcGet (unsigned char)(1)

#define prcHead (unsigned char)(2)

#define prcPost (unsigned char)(3)

#define prcPut (unsigned char)(4)

You can find these values in ISP.hpp in the Include/VCL subdirectory.

During the process of connecting to the server, you must respond to certain events:

void __fastcall TForm1::Ftp1ProtocolStateChanged(TObject *Sender,

  short ProtocolState)

{

  
AnsiString S;

  switch(ProtocolState)

  {

    case ftpAuthentication:

      S = "Authenticate";

      Ftp1->Authenticate(Ftp1->UserId, Ftp1->Password);

      break;

    case ftpTransaction:

      S = "Transaction";

      
Ftp1->List("/");

      break;



  }

  ListBox1->Items->Add(S);

}

The ftpAuthentication protocol can have the following values: ftpBase = 0 The base state before the connection to the server is established.

ftpAuthorization = 1 Authorization is performed.

ftpTransaction = 2
The client has been successfully identified.

Here you're stepping through the process of signing the user onto the site. You get an ftpAuthorization message when it's time to begin the authorization process. In response to this message, you can call Authenticate:

Ftp1->Authenticate(Ftp1->UserId, Ftp1->Password);

Here you pass in the UserID and Password so that the process of authentication can be completed.

After you have been successfully authenticated, then you get an ftpTransaction message. In response to this message, you can show the user a listing of the current directory, which is the root:

Ftp1->List("/");

This command causes an OnListItem event to occur, as long as the ListItemNotify property is set to True, which is the default state. These events send you a Variant with a listing of the current directory in it:

void __fastcall TForm1::Ftp1ListItem(TObject *Sender, const Variant &Item)

{

  ListBox2->Items->Add(ParseFtpItem(Item));

}

The Item contains an object replete with information about each file or directory to be listed. In particular, it lists the filename, size, date, and attributes of the current file or directory. The OnListItem event gets fired multiple times until all the files or directories are listed:

I use a custom procedure called ParseFtp:

AnsiString ParseFtpItem(Variant &V)

{

  AnsiString S;

  AnsiString Result;

  AnsiString Temp;

  S.SetLength(StrLen);

  memset(S.c_str(), ` `, StrLen - 1);

  
Temp = V.OleFunction("FileName");

  S.Insert(Temp, 1);

  Temp = V.OleFunction("Date");

  S.Insert(Temp, 20);

  Temp = V.OleFunction("Attributes");

  if (Temp == "1")

    Temp = "<DIR>";

  
if (Temp == "2")

    Temp = "File";

  S.Insert(Temp, 40);



  return S;

}

To call the FileName function of the object, you can write the following code:

Temp = 
V.OleFunction("FileName");

OleFunction is a method of the variant object that takes one or more parameters.

That's all I'm going to say about the FTP component. You should now have enough infor- mation to get up and running. If you want more details, you might find an example of using the component that ships with BCB in the Examples\Internet subdirectory. If the FTP component does not suit your needs, and you don't want to roll your own with WININET, then you can buy a component from a third party. You can usually find components on the Internet. If you're having trouble getting started looking for them, visit my Web site at users.aol.com/charliecal and look for links that might help you get started.

Summary

This chapter focuses mostly on WININET and FTP. WININET turns out to be a fairly simple API to use. It provides a great means for creating small, powerful objects that allow you to access the key features of the Internet. You should visit Microsoft's Web site to download additional information about WININET. The DLL that makes this all possible is called, naturally enough, WININET.DLL. Starting with Windows NT 4.0, it ships with all versions of Windows, and is freely available for distribution with your applications. It works fine on Windows 95.

Other subjects covered in this chapter include owner draw list boxes, as well as the FTP ActiveX control that ships with BCB. As a rule, it is better to use WININET rather than the ActiveX control because WININET is so small and fast. On the other hand, canned code of the kind you find in the FTP ActiveX is easy to use, and can help you construct safe, reliable programs. Before you decide to rely on a control made by a third party, you should always test it carefully to be sure that it meets your needs.

TOCBACKFORWARD

©Copyright, Macmillan Computer Publishing. All rights reserved.