Using the Windows Shell API and Internet Explorer Controls in Visual FoxPro
Desktop
applications
By Rick Strahl
www.west-wind.com
Last Update:
June 26, 2009
Source Code for this article:
http://www.west-wind.com/presentations/shellapi/shellapi.zip
Source Code for DevCon WebBrowser Session:
http://www.west-wind.com/presentations/shellapi/WebBrowser_Session.zip
Internet enabling applications doesn’t have to be difficult or
involve a complete rewrite of applications. Instead you can use some of the
tools built into the Windows Shell for quickly integrating Internet and
Internet related content easily into your own applications. In this article
Rick shows several ways that you can utilize the Windows Shell API by:
When people think about Internet development the first thing
that comes to mind is rebuilding an application as a dynamic Web application
with an HTML front end. But there’s lots of life left in desktop applications.
Extending these applications with Internet content provides enormous benefits
in enhancing and enabling existing applications with new features that don’t
take much to implement.
In this article I’ll describe a few features of the built in
Shell interfaces that Windows provides out of the box – mostly through Internet
Explorer and with ways that you can use to integrate this functionality into
your own applications with very little code. Most of the concepts and samples
are very short and can be integrated with a few lines and yet provide big
benefits for existing applications without much fuss.
Although the Shell API is nothing new, it's one of those
useful features of Windows that you can always find new uses for. The Shell API
is essentially an extension of the Windows Shell that provides most of the same
functionality you have to launch applications in Windows Explorer through a
programmatic interface which includes:
- Plain
executable files like .exe, .bat, .vbs etc.
- Any file on
the local system or network with a registered extension (.doc, .pdf, .txt
etc.)
- Web URLs like
http://www.west-wind.com/
- FTP Urls
(ftp://ftp.west-wind.com/)
- Email clients
using the mailto: directive
- Any other
‘monikers’ that Windows is familiar with
What makes the Shell API handy is that it looks at the file
or moniker (which are ‘addresses’ that are prefixed by a protocol prefix such
as http:, ftp:, mailto: etc.) you pass, and then ‘runs’ it automatically by
launching the application that is associated with it. For example, if you pass
a Web URL it will automatically launch Internet Explorer or the default browser.
If you pass a Word document Word is loaded, or if it’s not installed Word Pad.
If you aren’t familiar with the Shell API, the easiest way
to access it is through a simple API call to the ShellExecute function which is
declared like this:
DECLARE INTEGER ShellExecute ;
IN SHELL32.dll ;
INTEGER nWinHandle,;
STRING cOperation,;
STRING cFileName,;
STRING cParameters,;
STRING cWorkingDirectory,;
INTEGER nShowWindow
This function expects a handful of simple parameters that
you can pass easily from VFP. Typically the WinHandle (Window Handle) can be
passed as 0 which means the launched application is launched on the Windows
Desktop. Alternately you can use the FindWindow API (as shown in Listing 1 to
pass the active VFP window) to pass the active VFP application window. The
Operation is used to use any of the associated word with the extension. For
example a Word document allows, “Open” and “Print” as options. You can find
what verbs are supported in the registry by looking up the document extension.
Filenames should always be specified as full paths, and Parameters specifies
any optional parameters. If you don’t pass any parameters the filename itself
is used as a parameter (for example, SomeFile.doc becomes the parameter that is
passed to Word). The Parameters option is mainly for real executable files that
require a command line. The WorkingDirectory specifies were the program runs.
Finally ShowWindow lets you specifies the WindowMode as to how the application
is launched which should be left as 1 usually – active and topmost, although
this is only respected if the application isn’t already running. Most applications
reuse windows for launching new content so when you use ShellExecute for
multiple calls to URLs they all go to the same browser instance instead of into
a new window each. However, this behavior is determined by the host
application.
To make life easier and address the most common scenario of
displaying content especially in a browser I always create a wrapper function
named GoUrl() shown in Listing 1.
FUNCTION GoUrl(tcUrl, tcAction, tcDirectory, tcParms)
IF EMPTY(tcUrl)
RETURN -1
ENDIF
IF EMPTY(tcAction)
tcAction = "OPEN"
ENDIF
IF EMPTY(tcDirectory)
tcDirectory = SYS(2023)
ENDIF
IF EMPTY(tcParms)
tcParms = ""
ENDIF
DECLARE INTEGER ShellExecute ;
IN SHELL32.dll ;
INTEGER nWinHandle,;
STRING cOperation,;
STRING cFileName,;
STRING cParameters,;
STRING cDirectory,;
INTEGER nShowWindow
DECLARE INTEGER FindWindow ;
IN WIN32API STRING cNull,STRING cWinName
RETURN ShellExecute(FindWindow(0,_SCREEN.caption),;
tcAction,tcUrl,;
tcParms, tcDirectory,1)
The code for the sample is found in the ShellExec.prg file
included in the source from this article. To load this function and the others
I'll shortly describe just DO ShellExec.
Accessing Web Content
Let’s start by accessing some Web content. To open a Web Url
try:
DO ShellExec
GoUrl("http://www.west-wind.com/")
This is quite useful if you have applications of your own
and you want people to go to your Web site for certain operations. Alternately
you can link to something a little more precise on a Web site. Check out Figure
1 which shows the About form of a shareware application. On this page it might
be useful to link to registration links directly from the page with a dynamic
URL like this:
GoUrl("http://www.west-wind.com/wwHelp/" + ;
"wwHelpInfo.asp#Pricing")
GoUrl("http://www.west-wind.com/wwStore/" + ;
"item.wws?sku=wwhelp30")
The latter Url takes the user directly to the registration page
in the dynamic company Web site from which they can place the order.
Figure 1 – Using ShellAPI links from the buttons
brings up relevant Urls on the West Wind Web site. Here the pops up the browser
to register a shareware product online. You can link back to information, sales
and support links directly with a single line of code and attach it to
anything: Buttons, menu items, labels…
You can call this code from anywhere in VFP: Buttons, menu
options, labels, images – unlike the Hyperlink control that ships with VFP
since version 6.0. Although the Hyperlink control also provides this
functionality I’ve always disliked it because it works only through Internet
Explorer and always wants to open everything in new browser windows which
becomes unmanageable quickly if users frequently click on links. It also always
uses IE even if the user’s default browser is another browser like Mozilla or
Netscape. ShellExecute always respects the system settings.
For something a little more generic try adding some search
functionality to your application by linking directly to the Google search
engine with a search phrase:
lcSearchString = "FoxPro Tools"
GoUrl("http://www.google.com/search?hl=en&ie=UTF-8&oe=UTF-8&q="
+ lcSearchString )
And off you go directly to a Google search with the
specified search parameters. In fact, Google lets you even limit searching to a
specific site, like your own company's site for example. If I want it to search
only the West Wind site I can use:
lcSearchString = "Html Help site:west-wind.com"
GoUrl("http://www.google.com/search?hl=en&ie=UTF-8&oe=UTF-8&q="
+ lcSearchString )
If you do this a lot, you’ll probably want to create a small
wrapper function like this:
FUNCTION SearchGoogle(lcSearch, lcSite)
IF EMPTY(lcSite)
lcSite = ""
ELSE
lcSite = " " + lcSite
ENDIF
RETURN GoUrl("http://www.google.com/search?hl=en&"+;
"ie=UTF-8&oe=UTF-8&q=" + ;
lcSearch + lcSite)
Here’s another useful example. Do you have addresses in your
applications and do you frequently need to show a map to the location in
question? How about linking to one of the map sites and automatically
displaying the map content when right clicking on the Address field? Listing 2 goes
to the MapQuest site and displays a map for the location specified.
***********************************************************
FUNCTION ShowMap(lcStreet,lcCity,lcState,lcZip,lcCountry)
***********************************************************
LPARAMETERS IF EMPTY(lcStreet)
lcStreet = ""
ENDIF
IF EMPTY(lcState)
lcState = ""
ENDIF
IF EMPTY(lcZip)
lcZip = ""
ENDIF
IF EMPTY(lcCountry)
lcCountry = "US"
ENDIF
IF EMPTY(lcCity)
lcCity=""
ENDIF
GoUrl("http://www.mapquest.com/maps/map.adp?country="
+ ;
lcCountry +
"&addtohistory=&address=" + ;
UrlEncode(lcStreet) + ;
"&city=" + UrlEncode(lcCity) +
"&state=" + ;
UrlEncode(lcState) + "&zipcode=" + ;
UrlEncode(lcZip) +
"&homesubmit=Get+Map&size=big")
I’ve used this in several applications where the address is
then handled with a right click menu or double-click event where the operation
performed is just a couple of lines:
ShowMap( "32 Kaiea Place","Paia","HI",;
"96779","US")
Note that the CountryID is a two letter ISO country code
like US, CA, DE etc. This pops up a map as shown in Figure 2.
Figure 2 – Popping up a dynamic map as easy as
passing the right URL Parameters to ShellExecute() or calling the ShowMap()
wrapper function.
The mapping function requires another function called
Urlencode which is also included in the ShellExec.prg source. It’s necessary
for addresses to be encoded in URL format, which is especially important if
you’re using international addresses. Without this some lookups may fail due to
misinterpretation of the data. URLEncoding replaces spaces with + signs and
replaces extended characters (other than A-Z and digits to hex values like %0A
(for a carriage return). We’ll reuse this function later in a few other places
as well.
Function UrlEncode( tcValue, llNoPlus )
LOCAL lcResult, lcChar, lnSize, lnX
*** Do it in VFP Code
lcResult=""
FOR lnX=1 to len(tcValue)
lcChar = SUBSTR(tcValue,lnX,1)
IF ATC(lcChar,"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
> 0
lcResult=lcResult + lcChar
LOOP
ENDIF
IF lcChar=" " AND !llNoPlus
lcResult = lcResult + "+"
LOOP
ENDIF
*** Convert others to Hex equivalents
lcResult = lcResult + "%" + RIGHT(transform(ASC(lcChar),"@0"),2)
ENDFOR
RETURN lcResult
This mechanism can be easily applied to any number of
applications and content on the Web. Although this type of content integration
is pretty high level and doesn’t automate everything about the Web access it
gets a lot of mileage out of very little effort. It’s especially useful if you
can integrate with content that exists on your own Web site to aid customers.
Keep in mind that if you use this sort of interface to a Web
page in your own applications you are potentially at the mercy of the provider
of this Web page. If MapQuest or Google decide to change their URL structure
it's possible that lookups stop working all of a sudden. For large sites like
Google or MapQuest this is not very likely – they actually don't mind if you
link to them since when you do, you still end up with the advertisements that
pay their bills. However, if you deal with smaller Web sites, they might not
even consider their pages being used as an 'API' through external applications
and change frequently.
You have to weigh the importance of the functionality
yourself. I tend use functionality like this for convenience features that are
useful and add value, but aren't necessarily mission critical. If they are
critical it's wise to check with the provider and see if you can work out some
guarantee of service deal.
FTP access
You can also use the ShellAPI to access FTP. As you probably
know Explorer/Internet Explorer can directly handle FTP access from within the
shell and you can take advantage of this functionality simply by pointing
ShellExecute at an FTP Url like this:
GoUrl("ftp://www.west-wind.com/")
And voila, you now have access to a basic FTP client that
can upload and download data with via drag and drop into and out of Explorer. Figure
3 shows an example of Explorer brought up through ShellExecute in FTP mode.
Figure 3 – Integrating
Explorer's FTP access is as easy as popping open an FTP link through
ShellExecute. Here
ftp://www.west-wind.com/ takes you to the West
Wind FTP site where you can use Explorer's drag and drop file moving to
up and download from the FTP site.
Note that you can also force a login to occur automatically
via the ftp: moniker:
GoUrl("ftp://ricks:password@www.west-wind.com/uploads")
So if a login is required your application could optionally
ask for or store a username and password and then pass this on to the FTP link.
This is obviously not a fully automated solution but if your application
requires that users upload or download a number of files having a full featured
FTP client is often useful as opposed to having the application provide the UI
for the FTP transfer itself.
Document Access
As mentioned earlier, ShellExecute can also 'run' documents
by identifying extensions of file requests and mapping the extension to the
application that it belongs to. So you can do things like:
GoUrl("d:\articles\fpa\shellexecute.doc")
GoUrl("d:\articles\fpa\shellexecute_SourceCode.zip")
GoUrl("d:\articles\fpa\shellExecute.pdf")
GoUrl("d:\Articles\fpa\ShellExecute.htm")
GoUrl("d:\Articles\fpa\ShellExecute_Data.xml")
Each of these files is brought up in it's native viewer
which is Word, WinZip, Adobe PDF Reader and IE in both of the last cases
respectively. This is useful when your application needs to manipulate files of
various types and let the user interact with it. But keep in mind that all you
can do here is basically bring up a document and view it or in a few instances
of document types that support it do things like Print.
The nice thing about this is that the document comes up in
its native environment so you can continue to use that environment to perform
common tasks. For example, you might pop up a Word document and let the user
modify the document and save it using Word. The application at a later point
might pick up the change and then further act on that document such as sending
it to a Web site, or updating a database with the data.
Displaying text as Text, HTML and XML
Here's a simple example of how I use this functionality in
my own development. I often generate large strings in my code such as HTML or
XML that is either generated or downloaded from a Web site. This textual data
is usually stored inside of string which can get quite large. It's often difficult
to see what's stored in these strings inside of VFP, so it's nice to display
the data in a way that you can look at it as a whole. It's quite useful to look
at what was generated both in rendered HTML format as well as text. To do this
I use a simple function shown in Listing 4 which renders the HTML string passed
to it into the configured Web Browser for the system.
FUNCTION ShowHtml(lcHTML, lcFile)
lcHTML=IIF(EMPTY(lcHTML),"",lcHTML)
lcFile=IIF(EMPTY(lcFile),;
SYS(2023)+ "\ww_HTMLView.htm",lcFile)
STRTOFILE(lcHTML,lcFile)
RETURN GoUrl(lcFile)
All this function does is take an HTML input string and
copies it to a file, then uses ShellExecute() via the GoUrl() function I showed
earlier and displays this rendered HTML in the browser. You can optionally pass
a filename to copy the file to, which is useful if you are depending on
relative files, such as images or stylesheets that may be referenced within the
HTML document. If you generate into the default TEMP directory those files are
not likely to be found and cause broken links to images. This is a great
debugging tool when generating HTML so that you can quickly see what you're
generating.
I use this functionality in many places. For example, my
inventory management application for my Web site lets me edit item descriptions
in HTML format and the user gets the option to preview the output of the text
he's written simply by passing the contents of the textbox to ShowHtml(). Easy
and very useful as an HTML preview.
You can also display Text and XML strings in this fashion.
Listing 5 shows the ShowText() and ShowXML() functions which in turn call
ShowHtml with predefined filename extensions that bring up either the
configured text editor (NotePad by default or in my case TextPad) or Internet
Explorer in its XML view.
FUNCTION ShowText(lcText, lcFile)
IF EMPTY(lcFile)
lcFile=IIF(EMPTY(lcFile),SYS(2023)+"\ww_HTMLView.txt",lcFile)
ENDIF
RETURN ShowHTML(lcText,lcFile)
FUNCTION ShowXML(lcXML, lcFile)
IF EMPTY(lcFile)
lcFile=IIF(EMPTY(lcFile),SYS(2023)+"\ww_HTMLView.xml",lcFile)
ENDIF
RETURN ShowHTML(lcXML,lcFile)
I find these function indispensable during debugging of
applications especially distributed and Web applications where I often deal
with large amounts of HTML and XML as shown in Figure 4. Because strings
generated are often difficult to display inside of VFP it's nice to be able to
quickly view what a variable contains or whether the output generated renders
correctly in HTML. With XML viewing in IE is a quick way to see if the XML
document is valid.
Figure 4 – Displaying large strings of HTML and
text in an external editor is often the best way to see what the strings
contain while debugging or demonstrating code. Here ShowHtml() was called from
the code and ShowText() from the command window while sitting on a breakpoint.
Driving the Email client
You can also use the Shell API to allow users to send email
messages through their configured email client. To bring up an email client you
need nothing more than this:
GoUrl("mailto:rstrahl@west-wind.com")
This pops up the email client with an empty message and
subject area. You can even force a little more information to the client by
providing additional information via 'querystring' parameters such as this:
GoUrl("mailto:rstrahl@west-wind.com?subject=Surf's
up," + ;
" Rick&Body=Drop everything, it's going big!")
To make this more generic you might want to use the function
shown in Listing 6.
FUNCTION ShowEmail( lcRecipient,lcSubject,lcBody )
RETURN GoUrl("mailto:" + lcRecipient + ;
"&Subject=" +
STRTRAN(UrlEncode(lcSubject),"+"," ") +;
"&Body=" +
STRTRAN(UrlEncode(lcBody),"+"," "))
Note that UrlEncoding the content is required to handle
extended characters, specifically carriage returns, which are dropped if you
don't use UrlEncode(). I use this functionality in a number of applications for
sending out email confirmations from within my applications.
Listing 7 shows a snippet from a Point of Sale application
where credit cards are validated. If the credit card validation fails the
customer needs to be notified via email that the card failed and that he needs
to enter a new card or check with his CC provider. In this case the message to
be sent is a text merge document stored on disk into which information of the
invoice business object is merged. For example, <<poInv.InvNo>>
holds the invoice number, <<poInv.InvTotal>> holds the total and
<<poCust.FirstName>> holds the first name which is used in the
greeting. The result of this merge and call to ShowMail() is shown in Figure 5.
poInv = THISFORM.oInv
poInv = poInv.oData
poCust = poCust.oData
lcEmail = TRIM(poCust.Email)
*** Merge the declined order message into the template
lcMessage = TextMerge( FILETOSTR("DeclinedOrder.wc")
)
*** display the email message
ShowEmail(lcEmail, "Re: West Wind Technologies "
+ ;
"Order Confirmation - Declined.
#" + ;
TRIM(poInv.Invno), lcMessage)
Figure 5 – By using the mailto: protocol it's
possible to pre-seed an entire email message with ShellExecute so all you have
to do is press the Send key. Alternately you can further edit the message
before sending – a common scenario with messages.
For standalone applications this type of Email automation is
often sufficient and actually preferred over using a fully automated email
interface such as MAPI or an SMTP client. The advantage here is that you can
rely on the user's email settings rather than explicitly configuring an email
connection. In addition, if you need to display a message for review or
editing, why reinvent the wheel? Your email client already provides a familiar
interface to the user and your message can be tracked in the Sent Items folder
like regular respondence. Note that this feature does not require MAPI – all
you need is an installed mail client that supports this protocol (Outlook and
Outlook Express and most other Web browser based email clients do).
ShellExecute summary
ShellExecute provides a great way to easily link to all
sorts of content easily from your applications. The interface is flexible
enough so that you can often pass additional information to finely tune the
information that pops directly to the context of your document application.
It's easy to use and is a totally non-intrusive way to integrate new
functionality into your existing applications. But it's important to understand
that this is a 'Show and Forget' type of technology that's meant for interactive
use. You can display content but once displayed your application looses the
connection with the displayed content and can't be further automated directly.
If you need to more control after the document as been
opened, you'll need more sophisticated tools like Automation (like Office
Automation, Internet Explorer Automation, MAPI etc.). In the next section I'll
show you how to automate some of the functionality we've discussed here by
using Automation with Internet Explorer and then using the Web Browser control
to more closely integrate the content with your own application.
Internet Explorer Automation
Internet Explorer is really the core of the Windows Shell in
and through it you can expose all of the various documents and monikers I've
shown with ShellExecute. Internet Explorer exposes Automation interfaces in a
couple of different yet similar ways:
- The
InternetExplorer.Application Automation Object
- The Microsoft
Web Browser Control
Like ShellExectute() you can use both mechanisms to display
a variety of content, but the content always loads through the Web Browser, not
in in the specified application. This is especially useful for HTML based
content, which can be fully automated through the rich object model exposed by
these components.
Both provide more or less the same functionality, exposing
most of Internet Explorer's capabilities to your applications for
programmatically controlling the content displayed. The Automation object works
best if you want to display the content externally to your application in a
separate window, while the Web Browser control works best for embedding content
directly into your own forms. Although the latter may seem a strange
proposition at first, I'll show you some examples that make good use of HTML
based content generated from local applications as well as displaying content
retrieved from the Internet.
The other important feature that both controls provide is
the ability to manipulate HTML content within the components which means you
can automate many HTML based tasks and drive Web content from within your
applications. The rich object model allows you for accessing each individual
element, modifying it and even the ability to visually edit the content inside
of an HTML document. It's a very large object model to get familiar with, but
it also very powerful and allows for many advanced concepts that make it
possible to share data between the Web and desktop application interfaces.
COM Automation 101
Let's start with the InternetExplorer.Application Automation
object. Here's the basic syntax you need to do to open a URL in IE:
oIE =
CREATEOBJECT("InternetExplorer.Application")
oIE.Visible = .t.
oIE.Navigate("http://www.west-wind.com/")
… do whatever you need to until you're done with it
oIE.Visible = .f.
oIE = null
This pops up a new instance of Internet Explorer and then
displays the Web site that I specify in the Navigate method. The instance of
Internet Explorer sticks around as long as you have a reference to it. Note
that you have to set Visible to .F. to completely release the instance of
Internet Explorer. If you don't set Visible to .F. your reference goes away but
the IE window remains open and active, but now disconnected from your
application.
As before you can also load non-Web content through this
interface so the following navigations also work as you would expect:
oIE.Navigate("d:\articles\fpa\IE.doc")
oIE.Navigate("d:\articles\fpa\IE.pdf")
oIE.Navigate("ftp://www.west-wind.com/")
Unlike the ShellAPI calls though any supported application
will open inside of Internet Explorer, so a Word document will show as an
ActiveDoc inside of the IE shell as shown in Figure 6. As you can see this
causes the Word Interface to be merged with the Internet Explorer interface and
menus. In this case with a local file you can edit the document and save it
right back to the original file.
Figure 6 – Loading a Word document through
Internet Explorer causes the Word document to display as an ActiveDoc inside of
the IE shell.
In these examples, I used local disk content, but you can
also retrieve this same type of content from the Web. So the following would
produce the same results:
oIE.Navigate("http://www.west-wind.com/files/ShellInterfaces.doc")
It's important to understand that when you bring up files
from a URL (http://www.west-wind.com/somefile.doc) as opposed to a local file
(d:\temp\somefile.doc), you can still edit the text, but you cannot save it
back to its original location on the Web. When you use the Save As…
option in Word for example, your save locations can only go to the local or
network drives.
If you load a document type that can't be viewed directly
through the shell such as a Zip file for example you get a download dialog as
shown in Figure 7. To do this you'd use:
oIE.Navigate("http://www.west-wind.com/files/wwIPStuff.zip")
Figure 7 – Accessing a URL that loads a
'document' or file that can't be directly opened through Internet Explorer
gives a download dialog just like you would get if you clicked on a hyperlink
from a Web page.
From here the user can either open the zip file directly in
their Zip editor configured or save the file to disk. This save operation cannot
be automated through code and requires manual user intervention.
Basic Automation with Web Content
At this point we've really seen little that is different
from what that ShellExecute provides with the exception that you can control
the lifetime of the browser window. However, the InternetExplorer.Application
object includes a rich object model that allows you to interact with the HTML
document loaded into a browser instance. It also provides you an Interface for
capturing a rich set of events. This is true both for the Application object as
well as the Web browser control.
To demonstrate the basics of document access try the following
from the command window:
oIE =
CREATEOBJECT("InternetExplorer.Application")
oIE.Visible = .t.
oIE.Height = 600
oIE.Width = 800
oIE.ToolBar = .F.
oIE.AddressBar= .F.
oIE.Navigate("http://www.west-wind.com/")
*** Retrieve the entire HTML document as a string
lcHtml = oIE.Document.DocumentElement.OuterHtml
If you'd rather retrieve just the Body (inside of the
<body></body> tags) of the HTML document you can use:
lcHtml = oIE.Document.Body.OuterHtml
This will work fine from the Command Window but if you run
this code in a program you'll likely run into an error that says that the
document object was not found. You have to be careful in application code
because the HTML document loads in the background on another thread and might
not be immediately available. Navigate() doesn't wait for the document loading
to complete. To be sure the document is complete you can check the ReadyState
property for load completion in loop like this:
oIE.Navigate("http://www.west-wind.com/")
DECLARE INTEGER Sleep IN WIN32API INTEGER nTimeout
DO WHILE oIE.ReadyState # 4
Sleep(100)
ENDDO
Since this is a common task you might want to build a more
generic function that returns .T. or .F. and also lets you specify a load timeout
to avoid hung applications if the page load fails for some reason. We'll reuse
the WaitForReadyStateFunction shown in Listing 7.5 again later.
FUNCTION WaitForReadyState
LPARAMETERS lnReadyState, lnMilliSeconds
IF EMPTY(lnReadyState)
lnReadyState = 4
ENDIF
IF EMPTY(lnMilliSeconds)
lnMilliSeconds = 4000
ENDIF
DECLARE INTEGER Sleep IN WIN32API INTEGER nMSecs
lnX = 0
DO WHILE this.oBrowser.ReadyState # lnReadyState AND ;
lnX < lnMilliSeconds
DOEVENTS
lnX = lnX + 1
Sleep(1)
ENDDO
IF lnX < lnMilliSeconds
RETURN .T. && Not timed out
ENDIF
RETURN .F.
With the document loaded you can access just about every
detail of the HTML page with a very rich object model that lets you access every
HTML tag and control in the document. From here you can retrieve and change
content in the HTML document, even set values in a form and submit the form
automatically. I'll talk more about accessing the HTML document later, but for
now just understand that you can read and manipulate all parts of the HTML
document directly through the object model through both the IE Application
object and the Web Browser control.
Special Operations
The Web Browser can be accessed and controlled pretty
cleanly through several command interfaces. For example the ExecWB method allows
command access to most operations that the User Interface provides. For example
the following code allows you to print the currently loaded document:
oIE.ExecWb(6,0,0,0)
ExecWB has a number of parameters that can be passed the
second of which usually controls whether IE just does it or goes through its
usual set of prompts. The above prompts to print without a prompt you can use:
oIE.ExecWb(6,2,0,0)
The parameter of 2 -
LECMDEXECOPT_DONTPROMPTUSER - causes the UI to be suppressed.
There are OleCommandIds (which
you can look up on MSDN by looking up ExecWB) that deal with Printing,
Previewing, Saving, Cut and Paste operation – most of the things that you see on
the IE menu.
Along the same lines is the HTML
Document’s execCommand() method which allows execution of document specific
commands. This command has fewer control UI options and in this case belongs to
the MSHTML browser model, so it will only work on HTML documents and is subject
to the browser's security restrictions. For example, the following allows you to
save the currently loaded HTML document to disk (with prompt).
oIE.Document.execCommand("saveas")
Both of these commands are highly useful if you need to
interactively drive IE’s user interface.
Capturing Browser Events
Using the Application object you can also capture the
various events that the IE object exposes. Events allow you to trap things like
statusbar messages, completion events, link submission events and many other
operations. To capture and handle events you need to implement the following
interface:
DEFINE CLASS WebBrowserEvents as Custom
IMPLEMENTS DWebBrowserEvents2 IN
"SHDOCVW.DLL"
… implementation of event methods here
ENDDEFINE
The implementation of this class consists of about 30
handler methods that must be provided. The easiest way to do this is by using
the Object Browser which will automatically generate the complete interface for
you in source code. To do this:
- Open a new PRG
file and leave it open
- Open the
Object Browser
- Go to the COM
Libraries tab and select Browse
- Select <your
system dir>\shdocvw.dll
- Open the class
and select the Interfaces tree
- Select the
DWebBrowserEvents2 interface
- Drag and Drop
the Interface into the new empty PRG file (Figure 8)
- Change the
classname of the dropped interface to WebBrowserEvents
- Change the
path to the SHDOCVW.DLL file by stripping off the path leaving just the
filename
Figure 8 - Dragging and dropping the
DBWebBrowserEvents2 interface to a PRG file generates an implementation for the
interface with all empty methods that you can override.
Finally let's create the calling code above the generated
interface that you can use to hook up the events and test operation. The code
in Listing 8 shows the calling code, and the event interface class with a
couple of implemented handler methods.
LPARAMETERS lcURL
IF EMPTY(lcUrl)
lcURl = "http://localhost/"
ENDIF
PUBLIC oIe as InternetExplorer.Application
IF TYPE("oIe.Height") # "N"
oIE =
CREATEOBJECT("InternetExplorer.Application")
oIEEvents = CREATEOBJECT("WebBrowserEvents")
EVENTHANDLER(oIE,oIEEvents)
TRY
oIE.Navigate("about:blank")
CATCH
*** Ignore Default interface error
ENDTRY
ENDIF
oIE.Height = 600
oIE.Width = 800
oIE.ToolBar = .F.
oIE.AddressBar= .F.
oIE.Visible = .T.
oIE.Navigate(lcURl)
RETURN oIe
DEFINE CLASS WebBrowserEvents AS Custom
IMPLEMENTS DWebBrowserEvents2 IN
"SHDOCVW.DLL"
PROCEDURE DWebBrowserEvents2_StatusTextChange(;
Text AS STRING) AS VOID
WAIT WINDOW NOWAIT Text
ENDPROC
PROCEDURE DWebBrowserEvents2_BeforeNavigate2(;
pDisp AS VARIANT, URL AS VARIANT, ;
Flags AS VARIANT, TargetFrameName AS VARIANT,;
PostData AS VARIANT, Headers AS VARIANT,
Cancel AS LOGICAL @) AS VOID
IF ATC("www.west-wind.com",Url) = 0 AND
ATC("about:blank",URL) = 0
MESSAGEBOX("Not allowed to go to " + URL)
Cancel = .T. && Don't allow the navigation
ENDIF
ENDPROC
… other inteface methods required but excluded here
ENDDEFINE
There are a couple of important things to note about this
code. First note how the event interface is hooked up:
oIE =
CREATEOBJECT("InternetExplorer.Application")
oIEEvents = CREATEOBJECT("WebBrowserEvents")
EVENTHANDLER(oIE,oIEEvents)
You first create the Application object, then the event
object and use VFP's EVENTHANDLER() function to tell oIEEvents to handle the
events fired by oIE. Next notice the TRY/CATCH error handler used to navigate
to about:blank which is IE's default blank page. There's a bug generated in the
FileDownload() event handler due to a unusual parameter being passed to VFP
that it can't handle. The first navigation always fails and the TRY/CATCH code
handles this by navigating to the default page and simply ignoring the error,
and only then navigating to the real page.
The event implementation here does two things: It captures
the StatusTextChange event and simply echos the text into a Wait Window. You'll
see the wait window change frequently as a page loads.
The second event handled is probably the most useful event surfaced
by the IE object model. BeforeNavigate2 fires before IE navigates to a new URL.
This method is passed a URL and all information about the page submitted
(although the other parameters are always empty – so don't count on capturing
the POST data for example). There's also a Cancel parameter which is passed by
reference and can be set to .T. to disallow the URL to be navigated. The sample
code (Listing 7.5) disallows all access to non-West Wind links and displays a
message box if another URL is accessed which might be useful if you want to
control where users can browse to from your application.
Object Lifetime
Lifetime management for the IE object is somewhat tricky
because the object lives outside of your application and you can close Internet
Explorer externally. Note that I have this code:
PUBLIC oIE as InternetExplorer.Application
IF TYPE("oIe.Height") # "N"
oIE =
CREATEOBJECT("InternetExplorer.Application")
to determine whether IE exists. The reference is loaded as
PUBLIC object so if you run this multiple times with different URLs the same IE
object instance is reused. However, if you close Internet Explorer interactively
(Alt-F4 or clicking the X) your Fox code still leaves oIE set as an object
reference, but then this object now points at an invalid instance of IE. By
checking a property to on the IE object you can see if the object reference is
still good and if it isn't you can recreate it. Another option is to use the
OnQuit event and in it release the reference, but this works only if you use a
PUBLIC var as I've done here.
In a real application you also have to figure out how to
hang on to the instance you have created since you likely want to use it and
keep it around while the main application runs. Here I've used a PUBLIC
variable, which, even though I hate using them, is reasonable in this case of
a truly independent object from the application. Alternately you can attach the
instance to an Application object, or if it's specific to a Form to the Form
instance.
Just make sure that you always account for closing the
object properly by first setting Visible to .F. then releasing the instance.
One way to do this is to create a wrapper object that manages the lifetime for
you properly by firing the Destroy event on the wrapper which properly releases
the underlying IE object reference.
The Web Browser Control
The Microsoft Web Browser ActiveX control also provides all
of the functionality I've described thus far for the Internet Explorer Application
object. The major difference is that the control lets you embed the Internet
Explorer directly into your own Visual FoxPro forms as opposed to an external
desktop window. Also the events that IE publishes are exposed as native events
of the ActiveX control so you don't need to implement an interface to handle
events. Which one of these components you use is really up to your application
scenario, but in general I find that I have more control over the lifetime of
the control if I use the ActiveX control and more options in how I can mix VFP
and Web browser content.
To use the Web Browser control on a Fox form, use the
following steps:
- Open a new
FoxPro Form
- Use the
ActiveX control picker and drop Microsoft Web Browser Control onto
the form
- Name the
control oBrowser
- Set the Refresh
Event code to NODEFAULT
The last step is necessary as the Refresh event causes an
error in VFP to occur. Figure 9 shows the sample form containing the Web
Browser control.
Figure 9 – The Web Browser control on a FoxPro
form in design mode. Once on the form the control can be fully automated via
code, and events handled by simple event methods. This example demonstrates
several features of the control.
When first run without a URL the control shows an empty
surface and an empty HTML document and THISFORM.oBrowser.Document
= null. You can use the Navigate() method to then navigate to a
specific URL:
THISFORM.oBrowser.Navigate("http://www.west-wind.com")
If you end up modifying the HTML document in your code or if
you want to display an emty document you'll always want to make sure load at
least an empty document with:
THISFORM.oBrowser.Navigate("about:blank")
about:blank is an HTML document that is valid, but contains
only <html><body></body></html>
and you can then continue to modify the document. For example:
THISFORM.oBrowser.Document.Body.InnerHtml =
"<h1>Hello World</h1>"
Alternately, your Init() of the form can already navigate to
a specific URL or file. Remember as with the IE Application object you need to
make sure that the document is loaded completely before you start modifying any
part of it via code. Use WaitForReadyState() to do this as necessary.
In this case the form can be passed a URL parameter which
looks like this in the form's Init():
* WebBrowser.Init()
LPARAMETERS lcUrl
IF !EMPTY(lcUrl)
THISFORM.Navigate(lcUrl)
ELSE
THISFORM.Navigate("http://www.west-wind.com/")
ENDIF
THISFORM.Resize()
The Resize of the form resizes the Web Browser control to
fit the form and the end result of the running form without a parameter is
shown in Listing 10.
Figure 10 – The Web Browser control once
navigated displays a full IE client frame inside of the FoxPro form. You can
now use events like BeforeNavigate2 to trap navigation to other links.
As you can see from the code above the form includes a
Navigate() method which in turn calls the browser's Navigate method. Although
not essential in this application, in most application contexts this is a good
idea to properly wrap the calls. For example, trimming the URL properly of
spaces, or performing any fixups of the URL before passing it to the navigate
method. In this form the Navigate method deals with checking what the current
URL is and only navigating to it if the URL has changed.
* Navigate Method of the form
LPARAMETERS lcUrl
IF EMPTY(lcUrl)
lcUrl = TRIM(THISFORM.txtUrl.DisplayValue)
ENDIF
*** Don't navigate if we're on the same URL
IF lcUrl = THISFORM.cCurrentUrl
RETURN
ENDIF
IF !EMPTY(lcUrl)
THISFORM.oBrowser.Navigate(lcUrl)
ELSE
THISFORM.oBrowser.Navigate("http://www.west-wind.com/")
ENDIF
IF EMPTY(lcUrl)
thisform.cCurrentUrl = "asasdasd"
ELSE
THISFORM.cCurrentUrl = lcUrl
ENDIF
THISFORM.Resize()
The form also captures a URL history in the combo box by
using the NavigateComplete2 event which fires when the page is done loading.
When the form is shutdown the list is then written into a DBF file which is
reloaded if it exists so you have a history that persists.
*** NavigateComplete2 Event ***
LPARAMETERS pdisp, url
IF EMPTY(Url)
RETURN
ENDIF
thisform.txtUrl.AddItem(Url,1)
THISFORM.txtUrl.Value = Url
*** Drop off the last item if list is too large
IF thisform.txtUrl.ListCount > 25
thisform.txtUrl.RemoveItem(26)
ENDIF
The Web Browser control also includes native methods for
things like GoBack(), GoForward(), GoSearch (), GoHome() that you are familiar
with in the browser's user interface.
It's not just for Web Content
The Web browser control has many uses that go way beyond the
Web. HTML is not a good tool for everything, but it's nice for certain
applications that require a rich graphical experience. Sometimes it's just easier
to display information as HTML—sometimes it's a good idea to build output
dynamically with VFP code into HTML format and use the browser control as the
preview mechanism to see what it will actually look like. Figure 11 shows a VFP
Desktop application for building HTML Help Files which uses local HTML content
dynamically generated to disk to display a preview of the final HTML topic
layout in the Web Browser control.
Figure 11 – Displaying HTML content doesn't have
to come from the Web. In this application the HTML is generated dynamically to
disk and previewed from a file for each topic displayed. The rich display of
HTML provides interface options that are difficult to achieve with VFP forms.
The concept behind the code for this kind of interface is
pretty straight forward. The form contains a Web Browser control, which is
refreshed based on an action in the VFP user interface. In this case if a user
clicks on one of the topics, the data that is stored in the particular Help
Interface record is rendered into HTML into a static file called
_wwHelp_Preview.htm. The Web Browser control is then Navigated to this page to
display the content as HTML in the control.
The high level code, triggered by the treeview's NodeClick
event is pretty simple and contained in a method called GoTopic():
lcHTML = oHelp.RenderTopic() && Create the HTML
output
STRTOFILE(lcHTML,this.oHelp.cProjectPath +
"_preview.htm")
THISFORM.oBrowser.Navigate("file://" + ;
this.oHelp.ProjectPath +
"_preview.htm")
The Help Builder application is an extreme example for HTML
content because HTML Help is, well, all about HTML. But this sort of interface
comes in handy in many situations where you want to display data dynamically.
For example, I frequently use HTML lists in applications to display lineitem
lists where the lineitems have varying numbers of fields to display. Take a
look at Figure 12 which shows a list of lineitems with an optional discount.
This would be difficult to display using standard VFP controls.
Figure 12 – Using HTML inside of standard VFP
forms can also provide a clean, modern look as well as help with varying data
such as the discount above. HTML also works well for lists displaying memo data
which is nearly impossible with standard VFP controls.
Any kind of output that needs to stream and has varying
sizes per row – memo fields for example – are also good candidates for HTML
display. Nested relations (such as many-to-many) are also easier to handle with
a streamable output source like HTML, rather than a fixed-field-based format
like input fields and grids.
Capturing Navigation Events
This brings up another point that is fairly important if you
plan to integrate the Web Browser control. How do you capture clicks on a form
and let VFP handle these events? The key to this task is the BeforeNavigate2
event of the Web Browser control. This event fires before the browser navigates
to the requested page when you click on any link and lets you trap the URL that
you are accessing. It gives you a hook to check the URL and if necessary
perform actions in your VFP code to act in response of the URL.
For stand-alone applications it's even more important to
capture navigation events. Rather than going out to the Internet, applications
typically must perform operations before displaying data. For example, in the
invoice form above I want to capture an item link the user clicks on, and then bring
up the appropriate lineitem form.
The trick to this feat is to use a special syntax for
'application' URLs that are structured as directives. In order for a URL to be
valid it must have a protocol, followed by a path. Links in the Lineitem list
above look like this:
VFPS://EDITITEM/10121
All Web browser links in the application start with a
VFPS:// (VFP Script) protocols, and have an EventId (EDITITEM) and optional
parameters that are passed. There's nothing special about the VFPS:// protocol prefix
– it's a name I made up, but can be anything as long as it has the :// at the
end. The EventID and parameters are then parsed in the BeforeNavigate2 method
and call the appropriate forms or functions. Here's the code for
BeforeNavigate2() that fires the EditItem() method on the form:
* BeforeNavigate2 event handler
LPARAMETERS pdisp, lcUrl, flags, targetframename, ;
postdata, headers, cancel
LOCAL lnPK, lcCommand
DO CASE
CASE LEFT(UPPER(lcUrl),7) = "VFPS://"
lcCommand = UPPER(STREXTRACT(lcUrl,"//","/"))
DO CASE
CASE lcCommand = "EDITITEM"
lnPK = VAL(SUBSTR(lcUrl,RAT("/",lcUrl)+1))
THISFORM.EditItem(lnPK)
CANCEL = .T. && don't want to actually show
lcUrl
CASE lcCommand = "DELETEITEM"
lnPK = VAL(SUBSTR(lcUrl,RAT("/",lcUrl)+1))
THISFORM.RemoveLineItem(lnPK)
CANCEL = .T. && don't want to actually show
lcUrl
ENDCASE
ENDCASE
The key here is to keep these 'application' URLs simple so
you don't have to do extensive parsing. Here only a PK is passed as a
parameter. In each case the navigation is cancelled by setting the CANCEL flag
to .T. This is important as you don't want to update the Web Browser control
with new data. Instead you want to pop up a new FoxPro form to display data –
the initial display of the Web Browser control is only changed when the invoice
is refreshed (ie. An item was added or deleted or a new invoice was selected
altogher).
If you plan on using the Web Browser control frequently with
link handlers you might want to subclass the control as shown in Listing 11.5.
FUNCTION BeforeNavigate2(pdisp, url, flags, ;
targetframename, postdata, headers, cancel)
LOCAL lcParms
LOCAL ARRAY laParms[1]
DO CASE
CASE LEFT(UPPER(url),7) = "VFPS://"
lcParms = SUBSTR(Url,8)
ALINES(laParms,lcParms,"/")
THIS.OnVFPScript(@url,@cancel,@laParms)
ENDCASE
FUNCTION OnVfpScript(Url, Cancel, Parms)
ENDFUNC
FUNCTION Refresh
NODEFAULT
ENDFUNC
By doing so you can now easily create VFPS:// links and
simply look at the Parms object to retrieve the keyword and any other
additional parameters. The handler code in OnVfpScript() for an invoice click
now reduces to:
CASE UPPER(Parms[1]) = "EDITITEM"
cancel = .T.
THISFORM.EditItem( val(Parms[2]) )
The HTML Object Model
The ability to manipulate the document from the container
application is extremely powerful. To run the following examples from the
command window, you can do the following:
DO FORM WebBrowser NAME oForm LINKED
*** for space store the browser in o
o = oForm.oBrowser && Use o for brevity
Note that what follows will work with any reference to a Web
Browser control (o for brevity here) or an InternetExplorer.Application object,
assuming a document is loaded. For example, once you have a document loaded,
you can access and manipulate the HTML content with code like this:
*** Return HTML minus the header
? o.Document.Body.InnerHTML
*** Return text without HTML tags
? o.Document.Body.InnerText
Both commands access the HTML Document object of the
browser, which allows reading and writing HTML directly from and to the browser
window. The Body object encapsulates all of the HTML between the <body>
and </body> tags in the document, and the InnerHTML property returns all
of that HTML text. InnerText retrieves the same text with all HTML formatting
removed. You can also get the OuterHTML:
*** Return HTML minus the header
? o.Document.Body.OuterHTML
which includes the actual <body></body> tags,
while InnerHTML does not. InnerHTML and OuterHTML are supported by most objects
in the Document object. You can also retrieve the entire document (in IE 5.0
and later) with:
? o.Document.DocumentElement.OuterHtml
In addition to reading, you can also write to the document, which
allows you to dynamically change the HTML of a document:
o.Document.Body.InnerHTML = "<h1>Hello World from
Visual FoxPro</h1>"
The HTML on the right replaces the previously loaded
document body, effectively giving the appearance of a new document. Note
that you can't replace the entire HTML document, but only the body of
the document. You can modify individual header tags using the various header
collections, but there's no way to set the full HTML document short of
reloading from a new document. You can't assign text to the DocumentElement
object.
This explains why I used file output in my HTML Help project
above, because the user could customize the output template, including the HTML
headers, which include complex style sheets that might be stored in the header
of the HTML template. While the document exposes the headers as separate
objects that can be modified, it gets a lot more complicated than simply
replacing the entire document text. Because the templates have extensive style
sheets, meta tags and other header elements, I opted to go the simpler route of
simply writing to file and reloading the document. You might think writing to
file is slow, but the difference between writing the file and changing the
document is not noticeable for anything but the smallest documents as most of
the overhead is actually in rendering the HTML not loading it.
Tag collections
Your access is not limited to the entire document as a
whole, but you can directly manipulate any portion thereof. All HTML tags in a
document are accessible via collections that allow iterating through all
instances of each tag. For example, if you wanted to get all the bold text in a
document, you could do this:
FOR EACH loTag IN o.Document.All
IF UPPER(loTag.Tagname) == "B"
? loTag.InnerHTML
ENDIF
ENDFOR
Document.All is the master collection that allows you to go
through every single tag in the document—which is exactly what the code above
does. It runs through every tag and checks to see whether it's a bold tag, and
if so, retrieves it for display.
Alternately you can also use the GetElementsByTagName()
function to retrieve specific types of tags which is a little more efficient.
For example, it might be useful to parse all HREF links in a page which can do
simply with:
loNodes = o.Document.getElementsByTagName("A")
FOR EACH loNode IN loNodes
? loNode.href
ENDFOR
If you've ever wanted to build a Web Spider or a link
validator this should make your job rather easy! There are also a number of
other predefined collections, such as Forms, Anchors, Images, Frames and
Scripts.
Custom ID tags and modifying Form content
Internet Explorer also supports the concept of custom ID
tags, which are used by the HTML object model to parse the HTML text. For
example, take a look at the following HTML code:
Visual FoxPro Version:
<b ID="VFPVersion">n/a</b>
<form
method="POST" action="CustomBrowserTags.htm">
<p>Name:
<input type="text" name="txtName"
ID="txtName"
size="20"></p>
<p>Company:
<input type="text" name="txtCompany"
ID="txtCompany"
size="20"></p>
<p><input
type="submit" value="Save" name="btnSubmit">
</form>
Notice the ID tags VFPVersion, txtName and txtCompany in the
document. With this document loaded in the browser, you can easily get at these
tags. Notice that the original document has n/a as the version number.
To insert the real VFP version number from your code, do this:
o.Document.all.VFPVersion.innertext = Version()
When you do, n/a changes to the VFP version number in
the HTML document! Think about this for a second—this mechanism allows you to
build very easy data binding from a VFP host container into an HTML document,
if necessary, simply by delimiting every piece of dynamic data in the HTML page
with its own custom ID tag. If you use a consistent mapping naming scheme
between properties and using Assign methods on your objects you can have an
HTML document update whenever a property is changed for example.
Using a custom ID tag is an easy way to "bookmark"
any area of the HTML document. All you need is Document.all.IDTag, and you get
back an object reference to that tag. Once you have a reference, you can read
and write the content and call methods on the object. The type of the object
and the functionality available to it will vary based on the type of HTML
object being referenced. The above code demonstrates how you can easily update
an HTML form in a form's Refresh event with values from the currently selected
record in a table. This is more impressive with the HTML form above. From VFP
you can now do this:
*** Retrive the HTML form variable
lcName = o.Document.all.txtName.value
lcCompany = o.Document.all.txtCompany.value
*** Set the HTML form variable
o.Document.all.txtName.value = "Rick Strahl"
o.Document.all.txtCompany.value = "West Wind
Technologies"
This allows you to read and write values inside the HTML
form! Using this mechanism, you can essentially bind Fox data directly to an
HTML document displayed through your application. To find out what type of
object you're dealing with, you can always use the following:
? o.Document.All.txtName.ID
? o.Document.All.txtName.classname
? o.Document.All.txtName.tagname
although the classname often is not set. You should also get
into the habit of checking for types before actually accessing these objects.
If something is mistyped in your document or can't be found, or if you have a
duplicate tag entry, you can get unexpected results. Trying to access an
invalid object will result in a COM exception, so the up-front check will save
you from having to catch those errors later. Use the TYPE() function to check
for type “O”.
DIV and SPAN tags for blocks of HTML
You can also mark entire sections of HTML with an ID. Above
I used the ID attribute on a <B> bold tag. ID tags can be applied to any
HTML element on a page. This includes range tags like DIV and SPAN, which are
used to delimit blocks of HTML.
<DIV ID="Detail"
Style="display:none">
Some HTML text goes here…
</DIV>
This HTML snippet won’t be visible when first loaded, but
you can access that range with to display it:
o.Document.All.Detail.display = ""
To hide the text again:
o.Document.All.Detail.display = "none"
You can also use this mechanism to build HTML into a string
via your own code and then assign the code directly to the Div tag area.
lcHTML = loHelp.RenderTopic()
o.Document.All.Detail.innerHTML = lcHTML
You’ll probably use DIV tags extensively in dynamic
documents that show and hide data frequently and documents that you want to
embed generated HTML into. DIV tags make great placeholders for blocks of HTML
code.
DIV is a block based tag, while SPAN is a paragraph based tag. If you want to
delimit some text within a paragraph use the SPAN tag. SPAN will not cause an
HTML line break to occur, while DIV does. Otherwise the concept of SPAN tags is
identical to DIV.
Selections and text ranges
It's also possible to access HTML elements as selected text
inside the HTML document. For example, the user could be looking at a large
text document and highlight an article that needs to be imported into a
specific field of the database. You could do:
REPLACE kb.article WITH
o.Document.Selection.CreateRange.htmlText
The Selection object (IHTMLSelectionObject) is not the most
intuitive—you explicitly have to create a range (IHTMLtxtRange) of selected
text. If you pass no parameters to CreateRange, the default selected text is
used from the HTML document. Once you have a reference to the selected text,
the htmlText property retrieves the actual text.
To replace text in a selection, use this:
o.Document.Selection.createrange.PasteHTML("My HTML
Text here")
With this functionality you can almost build your own HTML
text editor. But there's an easier way and I'll come back to editing HTML a
little later.
Accessing HTML script from your VFP code
Not only can you manipulate the document, but you can also
cause things to happen in the HTML page. Once you get an object
reference to an object, you can call any of its methods and cause it to do
something. For example, you can get a reference to a button and call its Click
method:
o.Document.all.btnClear.Click()
You can also call any script code directly using the
document's Script object. Assume you have a method called ValidateInput()
inside the HTML document as VBScript or JavaScript. You can cause that to fire
with the following code:
o.Document.Script.ValidateInput("parameter")
This essentially allows you to control an HTML application
through code fired from a VFP host application. Note that any script code must
be in the HTML document's header to be accessible through Document.Script. If
you have multiple blocks of code in script tags, only the first block is
recognized, so keep it organized in one place when creating pages accessed
externally.
Passing an object
Since you can call functions in the HTML page you can also
pass information from VFP to the HTML page including VFP objects. But it's a
little more tricky as you have to make sure that you create a public reference
to the object if you want the script page to be able to use this object
independently. The HTML code in Listing 12 demonstrates a function called
SetVFPObject that accepts a VFP form object and makes it available to the
script page.
<body>
<script
LANGUAGE="VBSCRIPT">
DIM oVFP 'public
variable!
Function
SetVFPObject(loVFP)
Set oVFP = loVFP
'assign to public
End Function
Function MoveParent()
oVFP.Top = 0
oVFP.Left = 0
End Function
</script>
<form
method="POST" action="CustomBrowserTags.htm">
<input id="btnMove"
type="button" value="Move Parent Form to 0"
onclick="MoveParent()"></p>
</form>
</body>
The global variable oVFP will receive the object reference
that is set by the function call to SetVFPObject. Inside Visual FoxPro you can
pass the reference down like this after the document has loaded:
o.Document.Script.SetVFPObject(THISFORM)
The above HTML form has a button that calls the MoveParent
script function which, in turn, uses the object reference set by the previous
call. When you click it inside the browser, the form that hosts the browser
window will move to 0,0 on the VFP desktop. If you want to be really twisted
you can also use VFP to call this script:
o.Document.Script.SetVFPObject(THISFORM)
WaitForReadyState(4,5000) && wait for doc to load
o.Document.btnMove.Click()
This is an obvious mechanism that you can use to capture
nonstandard and control-level events inside the browser and pass them back to
your Visual FoxPro application. You can use any object, not just a form. You
could, for example, pass in a complex business object that could validate input
directly inside the HTML script.
While this is very cool, it's important not to lose sight of
what you want to accomplish. It might simply be easier at times to generate
HTML pages with the data already embedded, rather than to manipulate an
existing page through extensive script code with VFP object references. Simply
put, using the browser to store logic can make applications more difficult
because the logic is split up in multiple places. But there are also good uses
for it, especially when the interface requires a Web front end that must run
with DHTML, or when the display features of HTML provide clear advantages over standard
form-based output.
Document Object Sample Application
To demonstrate many of the HTML Document Object features
discussed here, the source code for this article includes a form called IEDemo.scx
(shown in Figure 13) that displays invoice information from the TASTrade sample
application.
Figure 13 – This sample form contains a Web
Browser control and demonstrates many aspects of manipulating the HTML document
model, handling document events and passing data between the HTML document and
the VFP application.
This application mixes a Visual FoxPro form interface with
an HTML interface that is dynamically generated. The listbox on the left is a
simple VFP list box, and the When() clause of the list causes a method called
ShowRecord() to fire, which generates the HTML for each customer record
dynamically. The HTML includes form fields (Company, Name, Address etc) as well
as the list of invoices. Listing 13 shows the code for the ShowRecord() method.
* Function ShowRecord()
*** Generate HTML for the invoice list
pcInvoiceList = THISFORM.ShowInvoices(customer.cust_id)
SELE Customer
*** Template page containing <%= expression %> tags
lcPage = THISFORM.cHTMLPagePath + "customer.wcs"
*** Read the file and merge the expressions into it
lcContent = FILETOSTR(lcPage)
lcContent =
STRTRAN(lcContent,"<%=","<%")
lcHTML =
TEXTMERGE(lcContent,.F.,"<%","%>")
*** Write the result out to a local file and show in
browser
StrToFile(lcHTML,"_preview.htm")
THISFORM.oBrowser.Navigate(CURDIR() +
"_Preview.htm")
*** Wait for doc to load
IF THISFORM.Waitforreadystate(4,8000)
*** Then pass VFP form to the script page
TRY
THISFORM.oBrowser.document.script.SetRef(THISFORM)
CATCH
WAIT WINDOW NOWAIT "Error loading
customer..."
ENDTRY
ELSE
WAIT WINDOW "Customer Load Failed..."
nowait
ENDIF
You'll notice that this code does not generate HTML directly
but rather stores the HTML in an external page on disk called customer.wcs in
the .\templates directory. This template contains the base HTML which
includes ASP style <%= %> expression tags that hold the data that Visual
FoxPro is providing. For example, the textboxes are loaded like this:
<input name="txtCompany" size="32"
value="<%= customer.company %>
Where customer.company is the customer cursor from of the TasTrade
application. This provides the inbound databinding for the form controls. The
actual Customer.wcs template is loaded from disk into a string and then fixes
up the expression tags so they can be used with VFP's TEXTMERGE() function
which then merges the data into the template. The resulting string is then
written to a file and displayed via the browser's Navigate method.
The invoice listing is generated as HTML via code in the
ShowInvoices() method. This method uses a more brute force approach using the
TEXTMERGE command to generate blocks of HTML with embedded expressions in code.
Listing 14 shows the code used to generate the body of the invoice list inside
of a SCAN loop. Note that here the default << >> delimiters are
used for the embedded VFP expressions.
… more code to generate the table header
SCAN
TEXT TO lcOutput ADDITIVE NOSHOW TEXTMERGE
<TR>
<TD Align=Center><a
href="vfps://Orderid/<<UrlEncode(Order_id)>>">
<<Order_id
>></a></TD>
<TD
Align=Center><<TRANSFORM(Order_date)>></TD>
<TD
Align=right><<TRANSFORM(Order_amt,"999,999.99")>></TD>
<TD
Align=Center><<Transform(Shipped_on)>></TD>
</TR>
<TR ID=Order<<Order_id>></TR>
ENDTEXT
ENDSCAN
… more code to generate the total
Return lcOutput
Note that Listing 13 retrieves the result from
ShowInvoices() in Listing 14 into a variable pcInvoiceList, which is simply embedded
into the Customer.wcs HTML template with:
<%= pcInvoiceList %>
After Navigate() has been called the code needs to wait for
the document to complete loading so we can access the document using WaitForReadyState()
described in Listing 7.5. Once loaded the code goes ahead and tries to call a
script function in the HTML document, passing the VFP form as a parameter. The
purpose of this is to allow the HTML page to call back into the VFP application
when a form field is changed and thus update the database. It does so with a
very simple function in the HTML document called SetRef which assign the VFP
form to a public variable:
<SCRIPT language=VBScript>
Public goForm
Sub SetRef(ByVal loForm)
SET goForm = loForm
End Sub
Sub txtCompany_onchange()
goForm.UpdateField
"customer.company",me.value
End Sub
… additional field OnChange event handlers omitted
</SCRIPT>
The OnChange event handler for each of the form controls is
then created which fires whenever a change is made to one of the input fields.
goForm now points at our VFP form and we can call back into the form in this
case calling the UpdateField method which saves the value and pops up a
MessageBox as shown in Figure 13.
* Function UpdateField
LPARAMETER lcField, lcValue
replace &lcField with lcValue
MessageBox("VFP data updated:" + CHR(13)
+lcField + ;
" with " + CHR(13) + lcValue,64,"IE
Form Demo")
Finally the application handles clicks of the individual
Invoice links through the BeforeNavigate2 event. The links are marked with
VFPS:// protocol prefixes and look like this:
vfps://Orderid/+11011
which is handled by the following code:
*** HANDLE any URL's that have VFPS
CASE LEFT(UPPER(url),7) = "VFPS://"
lcCommand = UPPER(EXTRACT(url,"//","/"))
DO CASE
*** Order Id key - show that order
CASE lcCommand = "ORDERID"
lcOrderId = SUBSTR(url,RAT("/",url)+1)
THISFORM.ShowInvoiceForm(lcOrderid)
CANCEL = .T. && don't want to actually show
URL
ENDCASE
ENDCASE
The BeforeNavigate2 handler method parses the URL and and
retrieves the OrderId and then calls the ShowInvoiceForm() method with the
order Id to display yet another HTML view that displays the specified order.
Although this example may not be a best use case for
utilizing HTML to display data it does demonstrate how you can interact with
HTML once it's been generated and displayed in the Web Browser control. I find
that there are lots of applications where displaying content in HTML is
preferable to displaying standard form interfaces even if it is a little more
work.
Editing HTML
Finally the HTML document object model supports editing of
HTML content. This is surprisingly easy using:
o.Document.DesignMode = "on"
Set the value to "off" and you're back in display
mode. DesignMode essentially modifies the document in memory and you're
responsible for writing out the document text to disk in some fashion after
you're done editing. To save the document you can do:
lcHtml = o.Document.DocumentElement.outerHtml
STRTOFILE(lcHTML,"d:\temp\defaultpage.htm")
Another option with the same result but user interaction is:
o.Document.execCommand("saveas")
which lets you dynamically save the HTML content only to
disk. If you'd like to save the document using IE's native Save As capability
which also saves images, style sheets and other embedded linked content to a
directory with the same name as the page, you can use:
o.ExecWb(4,0)
The sample form Web Browser.scx demonstrates most of these
features and is shown again in Figure 14, displaying a document that is in Edit
mode.
Figure 14 – Editing HTML is as easy as setting
the Document's DesignMode property to "on". Marking up text involves
either using built in editing features like resizing of objects or built in
hotkeys like Ctrl-B, or by manipulating the HTML document via Text Ranges programmatically.
When you are editing a document you can simply position the
cursor anywhere and modify the text as you see fit. IE includes a common set of
built-in hotkeys like Ctrl-B for bold, Ctrl-I for Italic, Ctrl-K for inserting
a hyperlink using common Microsoft application hotkeys. If you need to perform
more complex HTML editing you will need to manipulate the HTML document itself
via Selection Ranges. For example, the red text in Figure 14 was typed in, then
highlighted and then modified via the following click code in the Bold button:
loSelection =WebBrowser.oBrowser.Document.Selection
lcHtml = loSelection.CreateRange.HtmlText
lcNew = "<span style='color:Red;font-weight:bold'>"
+ ;
lcHTML + "</span>")
loRange = loSelection.createRange
loRange.pasteHTML(lcNew)
This code captures the current selection of the document
into a string and marks it up with a <span> tag that provides the
formatting. It then creates a new text range and pastes the new text into it.
You can use this mechanism to paste in new HTML or replace existing HTML.
It's relatively trivial built a reasonably sophisticated
editor that uses these simple tools to embed text into a document. You can even
perform some layout tasks like resizing object such as table columns shown in
Figure 14 or deleting a selected object by pressing Delete. It's even possible
to allow editing in WYSIWYG mode as well as providing Source View to edit HTML
manually if you can save the document between switches from source to WYSIWYG
mode.
The HTML editing works well for editing documents, but I've
found that it's difficult for creating HTML documents from scratch unless you
provide at least a baseline. If you're used to FrontPage or even the VS.Net
editor you'll find that you'll immediately miss the ability to right click and
set properties. To build a truly user friendly HTML editor requires some work,
but for basic layout and editing tasks of existing documents the Web Browser
control as is works actually really well.
Handling MSHTML Document Events
As you probably are aware, the HTML document object fires
events. Every one of the objects displayed inside of the browser also have a
whole long list of events associated with it. Remember earlier we were
implementing the IWebBrowserEvents interface to hook events in the Web Browser
Control (or IE) itself. But these events are container events. In this
section I’ll talk about handling document events. You may not often have a need
to hook event documents but when you do it’s usually a pretty urgent matter.
The document and each of the elements inside of the
document all fire events and you can intercept these events by implementing the
appropriate event interfaces. Keep in mind that the HTML DOM is hierarchical –
there’s the document that contains the header and body, and the body in turn
contains text, images, links and all sorts of other HTML elements. Events that
fire, fire from the inside out, so when you click on an image or hyperlink. Some
controls handle certain events – a hyperlink handles a click (unless you
override the event yourself). But an image doesn’t handle a click by default,
and so the click gets passed up to the next container say a table column, then
to the table row, the table and finally the body, and then the document. The
Click event registers in all of these objects even though most of these objects
pass this event straight through unless a custom handler is implemented.
Anywhere along the line you can use the Window’s event object –
oDoc.parentWindow.Event – and its CancelBubble property to stop this flow of
events.
To intercept these events you have to implement an Event
interface and then hook it to the object or objects that you want to have handle
these events. Get out your trusty VFP Object Browser or better yet Visual
Studio’s OleViewer and look for Disp Interface with names like
HtmlDocumentEvents, HtmlImgEvents, HtmlElementEvents etc. You have to use the
specific handler to handle an event for a particular element type.
Ok time for an example. A common thing you might want to do
when you’re hosting the Web Browser Control is to suppress the Context menu.
Right now in all the samples I’ve shown you can simply right click the document
and see the standard IE context menu that includes View Source, Refresh, Save As
etc. This behavior may be a problem for your application, for example if you
wanted to protect script code that lives inside of the page.
The easiest way to do this is to handle the Document’s
OnContext event. To do this follow these steps:
- Open the Object Browser in VFP
- Open \windows\system\mshtml.tlb
- Go to Interfaces and look for HtmlDocumentEvents and
select
- Open a new or existing Program file and drag the
interface into the source code
An long interface definition should be created that looks
like this:
DEFINE CLASS
HTMLDocumentEvents
AS session
OLEPUBLIC
IMPLEMENTS
HTMLDocumentEvents
IN
"MSHTML.TLB"
PROCEDURE
HTMLDocumentEvents_onclick()
AS
LOGICAL
* add user code
here
ENDPROC
PROCEDURE
HTMLDocumentEvents_ondblclick()
AS
LOGICAL
* add user code
here
ENDPROC
PROCEDURE HTMLDocumentEvents_oncontextmenu()
AS LOGICAL
* add user code
here
ENDPROC
… many more
methods
ENDDEFINE
Make sure you remove the path from the MSHTML.TLB
definition and rename the class from the default MyClass to HtmlDocumentEvents
as I’ve done above. Next I suggest that you leave this interface implementation
as your base and create a subclass of it and implement your code for the
OnContextMenu handler there:
DEFINE CLASS
wwHtmlDocumentEvents
as
HtmlDocumentEvents
PROCEDURE
HTMLDocumentEvents_oncontextmenu()
AS
LOGICAL
* add user code
here
MESSAGEBOX("No
Context Menu for you buddy!")
RETURN .F.
&& Surpress the
context menu
ENDPROC
ENDDEFINE
This is the handler code from which you can fire your own
code to do whatever you need. Notice that in this case the handler has a return
value that determines whether the original functionality fires (.t.) or not
(.f.). The final step is to hook up the event to the document object.
oIE =
CREATEOBJECT("InternetExplorer.Application")
oIE.Visible
= .T.
oIE.Navigate(lcUrl)
IF
!WaitForReadyState(oIE,4,10000)
RETURN
.Null.
ENDIF
oDoc =
oIE.Document
*** Hook up the
events
oEv =
CREATEOBJECT("wwHTMLDocumentEvents")
EVENTHANDLER(oDoc,oEv)
Make sure you don’t hook up events until the document is
loaded so use the WaitForReadyState() function I introduced earlier. Create the
event object and use EVENTHANDLER() to bind the event to the COM object. Now
when you run access the page and right click you will see the MessageBox pop up
and not IE’s context menu.
Event Information
Let’s do one more of this to demonstrate retrieving
information about the event. In the above case we simply respond to the event.
If you need more information about where the event was fired from or information
like screen information or mouseclick info you can get this info from the
generic Event object that exists on the Window object.
Let’s hook another event – a click on an image. To do this
proceed as above but this time create the interface implementation for
HtmlImageEvents. Create a subclass like this:
DEFINE CLASS
wwHtmlImgEvents
as
HtmlImgEvents
oDoc = .null.
PROCEDURE
HTMLImgEvents_onclick()
AS
LOGICAL
* add user code
here
MESSAGEBOX(
this.oDoc.parentWindow.event.srcElement.outerHTML
)
RETURN .F.
ENDPROC
ENDDEFINE
In this handler we’re going to echo back the HTML for the
image when you click on it. Notice that I have a reference to the Document
object here which needs to be set when the object is created. This is so that
the event can find a reference back to the Window and then to the Event object.
To hook this up the code looks like this after the document
has been loaded:
oDoc =
oIE.Document
oLogoEv =
CREATEOBJECT("wwHTMLImgEvents")
oLogoEv.oDoc = oDoc
*** First Image in
Document
LOCAL
loImage
as
MSHTML.HTMLImg
loImage =
oDoc.images[1]
EVENTHANDLER(loImage,oLogoEv)
Run the code again and click on the first image in the
document and you should see a dialog that shows the HTML for that image. What if
you want to hook this functionality to all images in the document? You’d have to
loop through the image collection and hook up this event object:
FOR EACH
Img
in
oDoc.Images
EVENTHANDLER(Img,oLogoEv)
ENDFOR
Easy enough. But keep in mind that whenever the page
reloads you have to reattach some of these event handlers. Yes, some but not
all! The Document object does not reload. But each of the individual elements
do. This means you have to be careful with the document only setting it once.
They go away as soon as the page is reloaded because you’re
basically getting a whole new document object. If you’re using the Web Browser
control this means you have to hook up in NavigateComplete in each and every
navigation. For the Document you probably need a lFirstDocLoad property that
hooks up the events only once, while all other objects must reload on each
navigation.
This gets even more complicated if you navigate to a
non-HTML page. At that point the MSHTML Document gets destroyed and so you loose
the event hookups. Unfortunately I have found no way to easily check and see
whether the document loaded is fresh or a reloaded document.
A few words of caution
The Web Browser control basically hosts an instance of
Internet Explorer in your application. Although most of the functionality I've
shown here works in all versions of IE since 4.0 some functionality, like
editing was introduced only in Version 5.5. So if you provide this
functionality be sure you know what versions of IE are required so you can pass
those requirements on to end users. To be safe require at least IE 5.5.
Also remember that your application is now essentially
hosting Internet Explorer and the resulting memory usage for your application
will rise accordingly. Use of Internet Explorer generally adds at least 10 megs
of memory usage to your application as it appears in Task Manager. Thus if you
have only a couple of small places where you want to utilize the control it may
not be worth to have this overhead. However, the IE object is very forgiving
with memory usage if no longer used and readily returns memory to VFP and/or
Windows and if you were using IE externally (via COM or ShellExecute) you would
still incur the same overhead on the system – it just wouldn't show up in your
app. I find that I have enough uses for the control in most of my apps that I
can usually justify the memory usage without a second thought.
Internet Explorer Controls Control Summary
The Internet Explorer Application object and the Web Browser
control provide a rich environment for utilizing rich content in your own
applications whether it's from Web content or with files from the local disk.
It provides the ability to display a variety of content in a common browser
environment as well as allowing you some control over how the browser behaves
via events. If you're dealing with HTML you get a full tool chest full of
functionality to manipulate and automate just about every aspect of the HTML
document including the ability to edit the HTML. The uses for this technology
are limited only by your imagination…
All of the tools I've shown here make it possible to extend
your application with very little effort. HTML content especially can provide
so much flexibility to applications and can easily become an invaluable tool to
most applications once realized. Most of the features I've shown are really
easy to implement and work without any third party tools, so everything you
need is really for once right at your fingertips.
If you haven't played with this stuff before, by all means
try it out. Believe me you'll find uses for it quickly and it'll be hard to stop
the idea once you get going...
As always you can reach me via email at
rstrahl@west-wind.com or even better on
our Message Board at
http://www.west-wind.com/wwThreads/Default.asp?Forum=Code+Magazine.
Source Code for this article:
http://www.west-wind.com/presentations/shellapi/shellapi.zip
|