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 session handle in the first parameter.
- The file to copy from your hard drive in the second parameter.
- The name the file will have on the server in the third parameter.
- Whether to conduct a binary or ASCII transfer in the fourth parameter.
- Information about the context of the transfer. You can usually set this
parameter to zero.
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 first is the TListBox itself.
- The second is the index of the current item into the list of strings shown
in the list box. For example, if you have five strings in the list box, this
event handler is called five times, with the index set to numbers ranging from
0 to 4.
- The second parameter is the area in the list box in which you are free to
draw. Remember that this example covers list boxes of a fixed size. You must
respond to OnMeasureItem events if you want to vary the size of the
items in the list box.
- The final parameter is of type TOwnerDrawState.
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.
|