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 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.
VMS Desenvolvimentos
Diversas Dicas, Apostilas, Arquivos Fontes,
Tutoriais, Vídeo Aula, Download de Arquivos Relacionado a Programação em C++
Builder.
|