As the Internet in general and the Web in particular grow in popularity and usage, more and more financial institutions are finding them to be very effective means of distributing and collecting financial data. This is true for everything from electronic banking packages to stock brokerages that allow trades to be conducted using a Web browser. This chapter guides you through the process of creating an interactive Web agent that harnesses a small piece of the financial data available on the Web: stock quotes.
The quotes are retrieved from a server run by PC Quote, Inc., a provider of real-time securities quotations and news. Its home page is located at http://www.pcquote.com, and there, you'll find background information about the company as well as information on its subscription-based services and Visual Basic market data controls. The quotes that QuoteWatcher accesses are 20-minute delayed quotes, but because the application is for educational purposes, I think we can all live with that limitation.
Another feature of QuoteWatcher is that it runs as a tray icon application. In proper Windows 95 lingo, this "tray" I'm speaking of is the taskbar notification area (see Figure 12.1) where applications can place icons to indicate their status or allow users to perform basic functions. The taskbar notification area is the area at the end of the Windows 95 taskbar that appears sunken (the right end, if the taskbar is oriented horizontally; it's at the bottom, if the taskbar is oriented vertically). Because of the sunken appearance, it looks like a tray, hence the term tray icon. Figure 12.1 shows four icons in the tray, in addition to the current time. The icons can even have a dynamic tooltip to provide further feedback to the user.
Figure 12.1. The Windows 95 taskbar notification (tray icon) area.
Because QuoteWatcher is designed to automatically update stock prices at a predetermined interval, it's a natural to become a tray icon application. When the application launches, it creates the tray icon and makes itself invisible. The user right-clicks the tray icon to pop up a shortcut menu, which provides a way to restore the window for viewing and editing. As quotes are retrieved, the tooltip and icon change to provide an updated status.
This chapter builds on the material covered in Chapter 5, "Retrieving Web-Based Information." Most of the code presented here closely resembles the code from Chapter 5. A few routines have been added to handle the HTML results obtained from the PC Quote server. If you haven't read Chapter 5 yet, it would probably help to skim the sections covering the dsSocket control. This is the TCP/IP control used in this chapter to communicate with the HTTP server that provides the quotes.
The QuoteWatcher application's basic task is to retrieve and display stock and mutual fund price quotes to the user. Figure 12.2 shows a screen shot with some sample stocks and mutual funds and their prices at the time. Figure 12.3 shows QuoteWatcher taskbar notification area icon (the wristwatch) and default tooltip.
Figure 12.2. The QuoteWatcher application in action.
Figure 12.3. The QuoteWatcher tray icon.
As Figure 12.2 shows, the pricing data is presented to the user with a grid control. The application is built around a simple Microsoft Access database that stores the names of the symbols to be retrieved and the pricing information retrieved. The Symbol column can be edited; the other columns are read-only. The grid displays the symbol, the previous closing price, high and low prices for the day, the latest price, and the trendwhether the stock price is up, down, or unchanged from the previous closing price. The user is allowed to add, edit, and delete symbols to modify the list of stock and mutual fund prices to retrieve.
The user specifies how often to check for prices using the Minutes Between Lookups textbox and spin button. Automatic checking can suspended if desired. There is also a button which, when pressed, manually starts the retrieval process. The textbox in the middle of the screen provides status feedback as quotes are being retrieved.
If the application performs a pricing retrieval while the main window is hidden and there is a change in one of the prices, the tray icon changes to a light bulb. This alerts the user to the fact that the price of at least one of the symbols being tracked has changed. The icon reverts to its initial value when the user displays the main window.
A hidden menu serves as the tray icon's popup menu. It contains options for showing or hiding the main window, suspending or activating the retrieval process, manually starting a retrieval, and exiting the program. The tray icon's tooltip displays information about the current state of the program.
Ever since I first installed Windows 95, I have marveled at taskbar notification area applications. In my opinion, they are one of the best user interface enhancements Microsoft made in Windows 95. As soon as Microsoft Press published Programmer's Guide to Microsoft Windows 95, I bought a copy in hopes of creating hundreds of these tray icon applications.
I was quickly disappointed, however, after reading the chapter covering the taskbar notification area. The taskbar notification area was named that for a reason: Windows 95 wants to notify those applications whenever mouse events take place within their spots in the notification area. This, of course, meant that my programs would have to intercept a callback message from Windows 95. As most experienced VB programmers know, Visual Basic does not provide a direct mechanism for capturing these messages. It seemed that I'd have to dust off my copy of Visual C++ and actually try to do something useful with it if I wanted to write a tray icon application. Most of the programs I wanted to add tray icon functionality to were written in Visual Basic, though.
All hope seemed lost for this lowly VB programmer. My hopes had been quickly dashed on the rocks of the Windows 95 messaging system. That is, until the Microsoft Systems Journal published an article in the February, 1996 issue titled "Create Tray Icons In Visual Basic." This article, written by Joshua Trupin, was the answer to my dilemma. Joshua provided the missing piece to the puzzle: a callback control that creates an invisible window, which can receive those elusive callback messages. The control then fires a Visual Basic event, which is where the code for handling the tray icon's user interaction is placed.
The next section discusses the CallBack control. The section following, "The Basics of Programming for the Taskbar Notification Area," covers the basics of the taskbar notification area interface and provides some routines to make life easier when writing tray icon applications.
In this section, I'll cover the basics of using the CallBack control. The best source of information about the CallBack control is, of course, the original published article, mentioned in the preceding section.
The CallBack control is provided in OCX form on the CD-ROM. The original C++ source code can be found in the Microsoft Systems Journal article mentioned earlier.
The CallBack control is provided in OCX form on the CD-ROM. The original C++ source code can be found in the Microsoft Systems Journal article mentioned above.
After you have the control installed on your system, it is simple to use. The control has one useful method (WatchMsg), one useful property (hWnd), and one event (CallBack).
The WatchMsg method is used to register the Windows messages that, when they are received by the control, cause the CallBack event to fire. As you'll see in the next section, the taskbar notification area interface requires you to specify which message Windows 95 is to use when notifying the tray icon application that an event has occurred. The message specified for this notification purpose should also be registered with the CallBack control using WatchMsg. Because this message is a user-defined message, it should be assigned a value greater than the Windows API constant WM_USER (&H400). Other than that restriction, the message number assigned can be any value and doesn't have to be particularly unique in any way. Using a value greater than WM_USER ensures that you won't conflict with any existing Windows messages.
The hWnd property is the window handle of the control's window. The control makes itself invisible when it is created, but it still has a window handle.
The CallBack event fires whenever a message registered using WatchMsg is received by the control. The parameters of the event are msg, wParam, and lParam. These are the original msg/wParam/lParam that the control's message handler received from Windows 95. They are the message received, the short parameter, and the long parameter. For example, when a window receives a "left mouse button down" notification message, the value of msg is WM_LBUTTONDOWN (&H201). The value of wParam specifies which, if any, special keys were down at the same time the message was sent, and lParam specifies the X and Y coordinates of the cursor when the button was pressed. These parameters are used by the VB code to determine what action should be taken.
When an application wishes to register a tray icon for itself or modify an existing tray icon, the Shell_NotifyIcon() API function is called. This function handles all the tasks related to tray icons: adding, deleting, or modifying them. The VB declaration for this function is:
Declare Function Shell_NotifyIcon Lib "shell32.dll" Alias "Shell_NotifyIconA" (ByVal dwMessage As Long, lpData As NOTIFYICONDATA) As Long
where dwMessage is a value that specifies the operation to perform (add, modify, or delete), and the lpData value is a variable whose type is the user-defined type NOTIFYICONDATA.
The dwMessage parameter can have one of three values: NIM_ADD (&H0), NIM_DELETE (&H1), or NIM_MODIFY (&H1).
The NOTIFYICONDATA structure shown in Listing 12.1 specifies the information Windows requires to handle a tray icon application. This includes the handle to the window that receives the notification messages (you'll be using the CallBack control's hWnd property) and an application-defined identifier for the tray icon. The value of the callback message to be used when notifying the application is also specified within the structure. A handle to the pictorial representation of the icon to be displayed and the text of the tooltip are also in this structure, and this is how you'll modify them within your applications. Finally, a flag variable indicates which of the three modifiable properties (callback message, icon, and tooltip) are currently valid (this is used when modifying the tray icon's properties). The flag consists of an OR'ed combination of the following constants: NIF_MESSAGE (&H1), NIF_ICON (&H2), and NIF_TIP (&H4). There is also a size element (cbSize) that should be set to Len(myStructure), if myStructure is the name of your NOTIFYICONDATA structure.
To add an icon to the tray, you first must fill a variable of type NOTIFYICONDATA with the appropriate information. You must specify the application-defined identifier, the window handle, the size, and a handle to the icon to be displayed. You do not have to specify the tooltip or the callback message. If you do not specify the callback message, Windows 95 will not notify the CallBack control specified of any mouse events. After the structure has been filled with data, you then call Shell_NotifyIcon() with NIM_ADD as the dwMessage parameter.
To delete an icon, you need only to specify the size, the window handle, and the application-defined identifier elements. You then call Shell_NotifyIcon() with NIM_DELETE as the dwMessage parameter.
Be sure you delete any icons you create before exiting the application. Windows 95 does not clean up after the tray icon applications until the user moves the mouse over the area the icon occupies. This obviously isn't disastrous but does leave the user wondering whether or not the application has really ended.
To modify an existing icon, fill in the required information of the NOTIFYICONDATA structure (handle, identifier, and size) and then whichever combination of message, icon handle, and tooltip you want to modify. Next, set the flag element to match the combination of properties being modified. Finally, call Shell_NotifyIcon() with NIM_MODIFY as the dwMessage parameter. For example, if you wanted to change the icon being displayed and the tooltip, you would set those elements in the NOTIFYICONDATA structure and call Shell_NotifyIcon() with dwMessage equal to NIF_ICON + NIF_TIP.
When the CallBack control receives a taskbar mouse event notification message, it passes the parameters of that message straight to its CallBack event. The wParam parameter contains the application-defined identifier for the particular tray icon that received the event (in case there are more than one such icons attached to the application). The lParam parameter specifies which mouse event occurred (mouse move, left button down, etc.). The msg parameter contains the callback message specified when the tray icon was created.
Typically, the application will use only the lParam parameter. If the application has created multiple tray icons or changes the callback message being used, however, it does become necessary to examine the contents of the other parameters.
The Microsoft Systems Journal article discussed earlier provides a small module named VBTRAY.BAS that contains the function declaration, the definition of the NOTIFYICONDATA structure, and the constants used when creating tray icon applications. I have expanded that module to add some useful procedures. This section describes VBTRAY.BAS, all of which is shown in Listing 12.1. This module is used in the QuoteWatcher application. It doesn't include all the procedures that could be defined when using tray icons, but it does have enough to get through the development of QuoteWatcher.
Listing 12.1. The VBTRAY.BAS module.
Public Const WM_USER = &H400 Public Const NIF_ICON = &H2 Public Const NIF_MESSAGE = &H1 Public Const NIF_TIP = &H4 Public Const NIM_ADD = &H0 Public Const NIM_DELETE = &H2 Public Const NIM_MODIFY = &H1 Public Const WM_MOUSEMOVE = &H200 Public Const WM_LBUTTONUP = &H202 Public Const WM_LBUTTONDOWN = &H201 Public Const WM_LBUTTONDBLCLK = &H203 Public Const WM_RBUTTONDOWN = &H204 Type NOTIFYICONDATA cbSize As Long hwnd As Long uID As Long uFlags As Long uCallbackMessage As Long hIcon As Long szTip As String * 64 End Type Declare Function Shell_NotifyIcon Lib "shell32.dll" Alias "Shell_NotifyIconA" _ (ByVal dwMessage As Long, lpData As NOTIFYICONDATA) As Long Public Sub RemoveIcon(CBWnd As CallBack, puID as Long) Dim tnd As NOTIFYICONDATA tnd.uID = puID tnd.cbSize = Len(tnd) tnd.hwnd = CBWnd.hwnd rc = Shell_NotifyIcon(NIM_DELETE, tnd) End Sub Public Sub UpdateTip(CBWnd As CallBack, puID as Long, ptNewTip$) Dim tnd As NOTIFYICONDATA tnd.uFlags = NIF_TIP tnd.uID = puID tnd.cbSize = Len(tnd) tnd.hwnd = CBWnd.hwnd tnd.szTip = ptNewTip$ & Chr$(0) rc = Shell_NotifyIcon(NIM_MODIFY, tnd) End Sub Public Sub ChangeIcon(pvNewIcon, CBWnd As CallBack, puID as Long) Dim tnd As NOTIFYICONDATA ' Fill the data structure with necessary information tnd.uFlags = NIF_ICON tnd.uID = puID tnd.cbSize = Len(tnd) tnd.hwnd = CBWnd.hwnd tnd.hIcon = pvNewIcon rc = Shell_NotifyIcon(NIM_MODIFY, tnd) End Sub Public Sub CreateIcon(CBWnd As CallBack, puID as Long, _ plMsg As Long, ptInitTip$, pzInitIcon) Dim tnd As NOTIFYICONDATA ' The Shell_NotifyIcon data structure ' Fill in tnd with appropriate values tnd.szTip = ptInitTip$ & Chr$(0) ' Flags: the message, icon, and tip are valid and should be ' paid attention to. tnd.uFlags = NIF_MESSAGE + NIF_ICON + NIF_TIP tnd.uID = puID tnd.cbSize = Len(tnd) ' The window handle of our callback control tnd.hwnd = CBWnd.hwnd ' The message CBWnd will receive when there's an icon event tnd.uCallbackMessage = plMsg tnd.hIcon = pzInitIcon ' Make the callback window wait for our defined message CBWnd.WatchMsg (plMsg) ' Add the icon to the taskbar tray rc = Shell_NotifyIcon(NIM_ADD, tnd) End Sub
The declarations section contains the Shell_NotifyIcon() function declaration, the type definition for NOTIFYICONDATA, the flag and dwMessage constants, and some useful mouse event message constants.
The RemoveIcon procedure is used to remove the icon from the tray. The parameters specify the CallBack control and the application-defined identifier. The procedure builds the NOTIFYICONDATA structure and calls Shell_NotifyIcon() using NIM_DELETE.
The UpdateTip procedure is used to change the tray icon's tooltip. It takes the CallBack control, the identifier, and the new text for the tooltip as parameters. It builds the structure, setting the flags parameter to NIF_TIP because the tooltip is being changed, and calls Shell_NotifyIcon() using NIM_MODIFY.
The ChangeIcon procedure changes the icon being displayed in the tray. The parameters to the procedure are a reference to the new icon (the Picture or Icon properties of picture boxes and forms can be sent as the parameter), the CallBack control, and the identifier. The procedure fills up a NOTIFYICONDATA variable, setting the flag element to NIF_ICON, and calls Shell_NotifyIcon() using NIM_MODIFY.
Finally, the CreateIcon procedure does all the work of creating a tray icon. It takes the necessary parameters, builds the NOTIFYICONDATA structure, and calls Shell_NotifyIcon() using NIM_ADD.
CreateIcon also invokes the CallBack control's WatchMsg method, specifying the callback message as the method's parameter. This instructs the CallBack control to fire it's CallBack event whenever it receives the callback message from Windows.
As mentioned in the section "QuoteWatcher's Functionality" near the beginning of this chapter, QuoteWatcher stores the pricing information for the funds and stocks being tracked in an Access database. The database is very simple, consisting of a single table named Symbols, which only has a few fields. The final section of this chapter, "Modifying the Application," discusses how the application and database can be expanded to create a much more useful application. For the purposes of this chapter, however, the simple one-table database is quite suitable.
The table's layout is shown in Table 12.1. This table shows field names, data types, and field length. Because the user will probably not track a large list of prices and because there are no related tables, a primary key is not really necessary for this table.
Field Name |
Data Type |
Length |
Symbol
|
Text
|
10
|
PrevClose
|
Numeric
|
Double
|
High
|
Numeric
|
Double
|
Low
|
Numeric
|
Double
|
Price
|
Numeric
|
Double
|
Trend
|
Text
|
2 |
The two text fields are set to allow zero-length strings. The Symbol field is the only required field. No validation rules are defined, and the numeric fields all default to zero.
You can use whatever means you like to create this database and tableMicrosoft Access 95, through JET engine code, or by using the Data Manager add-in that ships with VB. The remainder of this section guides you through the process of creating the database and table using the Data Manager. You can skip to the next section if you already know how to do this.
To use the Data Manager add-in, start Visual Basic (if it isn't already running). Select the Add-Ins | Data Manager menu option to start the Data Manager. After the Data Manager has started, follow these steps to create the database and the Symbols table:
Figure 12.4. The Data Manager with the new QUOTES.MDB.
Figure 12.5. Data Manager's Add Table dialog box.
Figure 12.6. Data Manager's Table Editor dialog box.
Now that the database has been created, it's time to create the actual Visual Basic project. Because it is assumed that you have experience with creating Visual Basic applications, I won't present step-by-step instructions in this section. Instead, I'll just discuss the components necessary for the project.
Start a new project. Then, go to the Custom Controls dialog and remove all the controls except for the Apex Data Bound Grid, the CallBack OLE Control Module, the Dolphin Systems dsSocket TCP/IP control, and the Outrider SpinButton control. If any of these controls are missing from the dialog's listbox, they are probably not installed. A demo version of the dsSocket control and a working version of the CallBack control are included on the CD-ROM accompanying the book. The other controls are shipped with Visual Basic.
A demo version of the dsSocket control and a working version of the CallBack control are included on the CD-ROM accompanying the book.
If the controls on the CD-ROM haven't been installed, simply copy the OCX files to your Windows System directory. Then use regsvr32 callback.ocx to properly register the CallBack control (the dsSocket control doesn't need to be registered). On the VB Custom Controls dialog use the Browse button, if necessary, to locate the controls.
The project uses just one form, which you can name anything you desire. Next, add the VBTRAY.BAS module discussed earlier in this chapter. It can be found on the CD-ROM with the code accompanying this chapter.
Next, add the VBTRAY.BAS module discussed earlier in this chapter. It can be found on the CD-ROM with the code accompanying this chapter.
Save the project in the same directory that you used when you created the quotes database. Name the project quotes.vbp.
The user interface design comes next. The screen shot in Figure 12.7 shows all the controls on the form, as well as the Visual Basic Toolbox and the Properties window for the form.
In describing the user interface, I'll move from the top of the form to the bottom. When there are multiple controls horizontally, I'll move from left to right.
Figure 12.7. QuoteWatcher's user interface in design mode.
The first control to add is the Data Bound Grid control. Add it with roughly the size and position as shown in Figure 12.7. When you first add the control, it has two columns with no headings. That's fine for now; you'll come back to the grid control after you add the data control. Name the grid dbgQuotes, and for now, leave the rest of the properties at their default values.
Next, add the textbox that appears below the grid. This will be used as a status box and is named txtStatus. Size it to the width of dbgQuotes and position it just below the grid. Set its Locked property to True to prevent the user from typing in it.
Add the Minutes Between Lookups: label below the status box. All properties except for the Caption can be left at their default values. Then, add the textbox to the right of the label. Name it txtDelay and set its Text property to 5 and its MaxLength property to 4. This will set the initial time delay to five minutes between price checks and allow for up to 9999 minutes between checks.
Immediately to the right of the txtDelay textbox, place a SpinButton control. After it is properly sized, you can leave the rest of its properties at their default values.
Further to the right of the form but at the same horizontal position, add a command button. Name the button cmdRetrieve and set its caption to Retrieve Now.
Below the Minutes Between Lookups label place a checkbox control. Leave its name at the default value (Check1) and set its caption to Suspend Automatic Lookup.
This is the last of the controls that the user will have access to. When you're ready to run the application, you'll shrink the form's height so that only these controls are visible. A few controls still need to be added but the user doesn't have access to these.
Add a dsSocket control and leave its name as dsSocket1. Add two time controls. Name one tmrCheckQuotes and the other tmrTimeOut. Set the Interval property of both to 60000. This means that the timer will fire at one minute intervals. Set the Enabled property for tmrTimeOut to False (leave tmrCheckQuotes enabled).
Add another textbox. Name this one txtInternalStatus and set its Text property to an empty string. This control will be used in the applications state machine to trigger a change of state. As you'll see in the next section, there are several states in which the application can be while quotes are being retrieved. Each state is represented by a string constant. When the application is ready to change state, the next state's string constant is assigned to the Text property of this textbox. This fires the textbox's Change event, which contains the code that makes the state machine work (and is actually where half of the program's code resides). Because the textbox is in a portion of the form that won't be visible to the user, you can leave the Visible property set to True.
Next, add a data control. Set the DatabaseName property to point to the database you created earlier in this chapter. Then, set the RecordSource property to Symbols to point to the Symbols table. Leave the default name (Data1) intact.
Finally, add a picture box control. Set its name to picLight and set the AutoSize property to True. I used the file lighton.ico as the Picture property for this control. If you installed the icon files when Visual Basic was installed, this icon was installed in the \icons\misc directory beneath the directory Visual Basic was installed to (it is also included on the CD-ROM). Obviously, you can use any picture you wish. The Picture property of this control will be used as the tray icon when a quote has changed since the last retrieval.
If you installed the icon files when Visual Basic was installed, this icon was installed in the \icons\misc directory beneath the directory Visual Basic was installed to (it is also included on the CD-ROM). Obviously, you can use any picture you wish. The Picture property of this control will be used as the tray icon when a quote has changed since the last retrieval.
The last control to be added is the CallBack control. It's an invisible control, which is why it can't be seen in Figure 12.7. Select the tool labeled OCX in the Toolbox and place it on the form. Or, simply double-click the tool, and it will be placed on the form for you. Change its name to CBWnd.
Now that you have all the controls in place, you can finish setting the design-properties of the grid. Select the dbgQuotes control again. Set its DataSource property to Data1. Open the control's property page by clicking the Custom property and then clicking the button with the ellipses. On the property page, select the Columns tab. With Column1 selected in the Column drop-down listbox, type Symbol in the Caption box. Select Symbol from the DataField drop-down listbox. Then, select Column2 in the Column listbox. Enter Previous in the Caption box and select PrevClose in the DataField listbox. Select Center in the Alignment listbox. Click OK to save the property changes. The grid should now appear as in Figure 12.7.
Finally, select the form so that you can modify its properties. First, shrink the form so that it hugs the user interface controls. Size it so that it resembles Figure 12.2.
Set the BorderStyle property to 3 - Fixed Dialog. This sets the MinButton and MaxButton properties to False. That's OK for MaxButton because you don't want the user to maximize the application (there's no resize support in this version), but you do want the user to minimize the application. Set the MinButton property back to True. When the user clicks the Minimize button, the code hides the form, making it appear that the application has returned to its tray area.
Set the form's caption to QuoteWatcher.
The form's icon will be used as the default tray icon. I used the file watch02.ico, which is in the same directory as the picLight icon. Of course, you are free to choose your own icon.
Finally, it's time to create the menu that appears when the user right-clicks over the tray icon's spot in the tray area. Table 12.2 lists the menu captions, names, and indention levels for the menu.
Caption |
Name |
Indention Level |
Main
|
mnuMain
|
0
|
Suspend
|
mnuSuspend
|
1
|
Show
|
mnuShow
|
1
|
Retrieve Now
|
mnuRetrieve
|
1
|
Exit
|
mnuExit
|
1 |
An indention level of one means that you should click the right arrow on the Menu Editor dialog for that menu entry. This makes the menu item a subitem of the previous menu item whose indention level is one less. In this application, all menu items are subitems of mnuMain.
Make mnuMain invisible by unchecking its Visible checkbox on the Menu Editor dialog box. This is the top-level menu that will be used with the PopupMenu statement executed when the user right-clicks on the tray icon.
Finally, the user interface is finished. It's now time to move on and actually attach some code to all these objects.
This section will delve into the details of the code behind the QuoteWatcher application. All the code is available on the accompanying CD-ROM in addition to being presented in this chapter. I don't go into a lot of details when describing most of the code because it's pretty self-documenting.
This section will delve into the details of the code behind the QuoteWatcher application. All the code is available on the accompanying CD-ROM in addition to being presented in this chapter. I don't go into a lot of details when describing most of the code because it's pretty self-documenting.
The database interface is handled almost exclusively by the Data Bound Grid control. The Form_Load load event sets Data1.DatabaseName to either the value of the command-line parameter (if provided) or to quotes.mdb (if the command-line parameter is not provided). The dsSocket1_Close event is responsible for editing the recordset and updating the values for each symbol as it is retrieved from the server. Finally, code in the dbgQuotes_BeforeColUpdate event prevents the user from editing any column except the Symbol column.
All QuoteWatcher's HTTP messaging code is taken from the dsSocket sample application in Chapter 5. The only difference is that instead of enabling the user to enter the URL of a Web-based resource, the application uses a hard-coded URL that executes a query on PC Quote's Web server.
There are some differences in the code in the dsSocket's Receive and Close events as well. Because QuoteWatcher is concerned only with the pricing information contained in the HTML document that the PC Quote server returns, the Receive event ignores any received characters that aren't part of the pricing information. When it is receiving the portion of the document that relates to pricing, the event appends the received characters to a form level variable named gtQuote. Upon the closing of the connection by the PC Quote server, QuoteWatcher parses the data in gtQuote to pull out the information that will be displayed in the dbgQuotes grid. This is done with two new functions: szStripHTML(), which removes the HTML tags from the data, and Extract(), which extracts a substring from a larger string based on the parameters provided.
To see just what the HTML document the PC Quote server returns look like, run either sample application from Chapter 5 and enter http://www.pcquote.com/cgi-bin/getquote.exe?TICKER=x where x is the ticker symbol for any stock or fund. You can also get the latest Dow Jones Industrials Average by using the ticker symbol $INDU. Figure 12.8 shows the HTML received when I requested the latest price for the Janus Mercury Fund (JAMRX).
Figure 12.8. The HTML received from the PC Quote server.
As you can see, there are a few pieces of the HTML text that you can use as delimiters to separate the pricing information. The <H3>...</H3> tags on the second line of the displayed HTML are the first occurrence of these tags. This line always ends with the </H3> tag. In the Receive event, you can use their presence to indicate the beginning of the pricing information. Note also the <PRE> tag. This marks the actual beginning of the pricing information. There is a corresponding </PRE> tag at the end of the pricing information. You'll use the </PRE> tag to signal the end of the Receive event's need to keep received characters.
We're lucky the folks at PC Quote have made it easy to parse the desired pricing information out of the HTML document their server returns. Each piece of data is on its own line, and the numeric data is separated from the label by a colon character. You can use these facts when you call Extract().
As I've mentioned several times, QuoteWatcher uses a state machine paradigm when it's retrieving quotes. Figure 12.9 shows a flow chart of the state machine and what happens during each state. The STATUS_x constants are defined in the form's declarations section. They represent each of the possible states that can be entered. When the application wishes to enter a new state, it simply sets txtInternalStatus.Text to the constant representing the state to be entered. For example, when the user clicks the Retrieve Now button, the application sets txtInternalStatus.Text to STATUS_START, which kicks off the retrieval process.
Figure 12.9. A flow chart showing the QuoteWatcher state machine.
All the code for the form is presented in Listing 12.2. The remainder of this section explains the code behind some of the routines found in Listing 12.2. The procedures that are not discussed here are well commented and should be self-explanatory.
The CallBack control's CallBack event is fired whenever the tray icon receives a mouse event notification message from Windows. The event's lParam parameter specifies which message has been received.
QuoteWatcher responds to right mouse button clicks and double-clicks in its tray icon area. When a right button click is detected, lParam will be WM_RBUTTONDOWN. The application then displays the popup menu (mnuMain). The menu is displayed at the current mouse position. The mnuShow subitem is displayed as the default menu itemits caption is displayed in bold typeface.
When the user double-clicks the tray icon, QuoteWatcher executes the code for the mnuShow_Click event. This event toggles the visible state of the form.
This event occurs once per minute (the tmrCheckQuotes.Interval property is set to 60000) whenever the timer is enabled. The procedure uses a static variable as a loop counter. This counter keeps track of the number of minutes that have passed since the end of the last retrieval.
When the value of the loop counter matches the value in the txtDelay textbox and there are symbols in the database, the procedure starts the retrieval process. First, the timer disables itself to prevent it from firing while a retrieval is in process. Then the application sets txtInternalStatus.Text to STATUS_START. Setting the textbox to this value starts the retrieval state machine, described in the next section.
The textbox txtInternalStatus is used to store the current state of the retrieval state machine and to trigger a change of state. By setting the Text property to a new value, the state is changed. The valid values of the Text property are STATUS_START, STATUS_NEXT, STATUS_FINISHED, STATUS_SEND, and STATUS_ERROR. This section discusses what happens in each state. Figure 12.9 shows a flow chart documenting the state machine.
The initial state of the retrieval process is STATUS_START. When this state is entered, the procedure first checks to make sure there is not an open connection. If a connection is open, the state is changed to STATUS_ERROR, and the procedure exits. If no connection is open, the procedure sets up the socket control to prepare it to connect to the PC Quote server. If no error occurs during this process, the procedure moves the data control's record pointer to the first record, updates the tray icon tooltip, and changes the state to STATUS_SEND. If an error does occur while setting up the connection, the state changes to STATUS_ERROR, and the procedure exits.
The next state is STATUS_SEND. The status box and tray icon tooltip are updated to reflect the current symbol being retrieved. The gtQuote variable is reset to the empty string in preparation for new pricing information. Then the Connect method of dsSocket1 is invoked. This starts the connection to the server. If no errors occur while attempting to open the connection, some status flags are reset, and the timeout timer (tmrTimeOut) is enabled. The procedure then enters a Do Until... loop. This loop continues until one of the other procedures sets either gfClosed or gfTimeout to True. If gfClose gets set, the connection has closed properly. If gfTimeout gets set, a timeout has occurred. In this case, the procedure updates the status controls to show that a timeout has occurred and then changes the state to STATUS_ERROR. If the loop ends because the connection has closed, the procedure turns off the timeout timer and changes the state to STATUS_NEXT.
The STATUS_NEXT state is used to move to the next symbol in the database. If there are no more symbols to be retrieved, the procedure moves the record pointer back to the first record and changes the state to STATUS_FINISHED. If there are more symbols, the state is changed back to STATUS_SEND to retrieve that symbol's pricing information.
The STATUS_FINISHED state updates the status box, enables the disabled controls, and stops the state machine. Likewise, the STATUS_ERROR state closes the connection if it's still open, updates the status box, enables the disabled controls, and stops the state machine.
After the state machine starts the connection process, it is up to the server to accept the connection. If the server does allow the connection, the dsSocket's Connect and SendReady events are fired. The SendReady event is where QuoteWatcher creates and sends the request message that retrieves the quote information for the current symbol.
The Receive event is used to capture the incoming characters that the PC Quote server sends in response to the GET request message generated in the SendReady event. The procedure discards all data until it has received a line containing an <H3> HTML tag. This marks the beginning of the pricing information. From that point on, the procedure appends any received data to the gtQuote variable. This continues until a line that contains the </PRE> HTML tag is received. After this line is received, the procedure ignores any further data received.
The Receive event fires whenever a complete line is received from the server (dsSocket1.LineMode is set to True). This continues until the PC Quote server closes the HTTP connection, firing the dsSocket's Close event.
The Close event is where all the pricing information is parsed from the gtQuote variable. The procedure first calls szStripHTML() to remove all the HTML tags from gtQuote. The variable now resembles text shown in the Debug window in Figure 12.10. The remainder of the procedure extracts the desired information from gtQuote and updates the database accordingly. If the new current price is different from the previously retrieved price and the form is not visible, the tray icon is changed to the contents of the picLight picture box. This indicates a change in price for at least one symbol.
Figure 12.10. An example of gtQuote's contents.
After the database is updated for the current symbol, the procedure sets the gfClosed flag to True. This causes the Do Until loop in the txtInternalState_Change event to end.
Listing 12.2. The code for QuoteWatcher's form.
' where pricing information is stored as it is retrieved Dim gtQuote As String ' a flag signifying that the connection has been closed Dim gfClosed As Integer ' a flag signifying that a price has changed since the last retrieval Dim gfChange As Integer ' a flag signifying that a timeout has occurred Dim gfTimeout As Integer Const TIMEOUT_MINUTES = 2 'how long to wait for timeout to occur ' state machine constants Const STATUS_START = "1" 'start Const STATUS_NEXT = "2" 'move to the next symbol Const STATUS_FINISHED = "3" 'all done! Const STATUS_SEND = "4" 'send the message Const STATUS_ERROR = "5" 'oops! ' dsSocket State property constants Const SOCK_STATE_CLOSED = 1 Const SOCK_STATE_CONNECTED = 2 Const SOCK_STATE_LISTENING = 3 Const SOCK_STATE_CONNECTING = 4 Const SOCK_STATE_ERROR = 5 Const SOCK_STATE_CLOSING = 6 Const SOCK_STATE_UNKNOWN = 7 Const SOCK_STATE_BUSY = 8 ' dsSocket Action property constants Const SOCK_ACTION_CLOSE = 1 Const SOCK_ACTION_CONNECT = 2 Const SOCK_ACTION_LISTEN = 3 Private Sub CBWnd_CallBack(ByVal msg As Integer, _ ByVal wParam As Long, ByVal lParam As Long) ' this event is fired when our tray icon receives a ' mouse event message from Windows If (lParam = WM_RBUTTONDOWN) Then ' the right mouse button was pressed, ' display the menu PopupMenu mnuMain, , , , mnuShow ElseIf lParam = WM_LBUTTONDBLCLK Then ' the left mouse button was double-clicked, ' execute mnuShow_Click Call mnuShow_Click End If End Sub Private Sub Check1_Click() ' this is for enabling/disabling the automatic retrieval ' enable/disable timer based on checkbox setting tmrCheckQuotes.Enabled = (Check1.Value = 0) ' if the box is checked, If Check1.Value = 1 Then 'update the tooltip to show suspended state Call UpdateTip(CBWnd, 100, "QuoteWatcher Suspended") ' change the menu caption mnuSuspend.Caption = "Activate" Else 'otherwise ' update the tooltip to show active state Call UpdateTip(CBWnd, 100, "QuoteWatcher Active") ' update the menu caption mnuSuspend.Caption = "Suspend" End If End Sub Private Sub cmdRetrieve_Click() ' if the state machine isn't already running and there are symbols If (Len(txtInternalStatus) = 0) And Data1.Recordset.RecordCount Then ' kick off the retrieval process txtInternalStatus.Text = STATUS_START End If End Sub Private Sub dbgQuotes_BeforeColUpdate(ByVal ColIndex As Integer, _ OldValue As Variant, Cancel As Integer) ' if the user edited any column but the Symbol column ' (column 0), cancel the update If ColIndex <> 0 Then Cancel = True Else Cancel = False End If End Sub Private Sub dsSocket1_Close(ErrorCode As Integer, ErrorDesc As String) Dim ltTemp$ ' strip all of the HTML tags from gtQuote gtQuote = szStripHTML(gtQuote) ' edit the recordset Data1.Recordset.Edit ' get the Price field from gtQuote ltTemp$ = Extract(gtQuote, "PRICE", ":", "*") ' if fraction is present - convert to decimal If InStr(ltTemp$, "/") Then ltTemp$ = Str$(ConvertFraction(ltTemp$)) End If 'if the price has changed AND the change hasn't already ' been recognized AND the form is invisible, ' update the tray icon to the lighbulb If (Val(Trim$(ltTemp)) <> Data1.Recordset("Price")) _ And (gfChange = False) And (Me.Visible = False) Then gfChange = True Call ChangeIcon(picLight.Picture, CBWnd, 100) End If ' update the field Data1.Recordset("Price") = Val(Trim$(ltTemp$)) ' repeat most of that for the PrevClose field ltTemp$ = Extract(gtQuote, "PREVIOUS CLOSE", ":", Chr$(13)) If InStr(ltTemp$, "/") Then ltTemp$ = Str$(ConvertFraction(ltTemp$)) End If Data1.Recordset("PrevClose") = Val(Trim$(ltTemp$)) ' and the High field ltTemp$ = Extract(gtQuote, "HIGH", ":", Chr$(13)) If InStr(ltTemp$, "/") Then ltTemp$ = Str$(ConvertFraction(ltTemp$)) End If Data1.Recordset("High") = Val(Trim$(ltTemp$)) ' and the Low field ltTemp$ = Extract(gtQuote, "LOW", ":", Chr$(13)) If InStr(ltTemp$, "/") Then ltTemp$ = Str$(ConvertFraction(ltTemp$)) End If Data1.Recordset("Low") = Val(Trim$(ltTemp$)) ' set the Trend field based on the current price and ' the previous closing price If Data1.Recordset("Price") > Data1.Recordset("PrevClose") Then ' price has gone up Data1.Recordset("Trend") = "UP" ElseIf Data1.Recordset("Price") < Data1.Recordset("PrevClose") Then ' price has gone down Data1.Recordset("Trend") = "DN" Else ' price is unchanged Data1.Recordset("Trend") = "--" End If ' udpate the recordset Data1.Recordset.Update ' set the connection closed flag gfClosed = True End Sub Private Sub dsSocket1_Connect() txtStatus = "Connected..." End Sub Private Sub dsSocket1_Receive(ReceiveData As String) Static bReceivingQuote txtStatus = "Receiving data..." 'send everything to the Debug window for testing Debug.Print ReceiveData ' are we in the middle of the pricing data? If bReceivingQuote Then ' yes: ' append the current data to gtQuote gtQuote = gtQuote & ReceiveData ' check to see if this is the last line If InStr(UCase$(ReceiveData), "</PRE>") Then ' it is the last line, clear he bReceivingQuote flag bReceivingQuote = False End If End If ' if we're not in the middle of the pricing data, see if ' the line we just received contains the start marker (<H3>) If Not (bReceivingQuote) And InStr(UCase$(ReceiveData), "<H3>") Then ' the line does contain <H3>, turn on bReceivingQuote bReceivingQuote = True End If End Sub Private Sub dsSocket1_SendReady() 'after a successful connect, the SendReady event fires Dim ltSendStr$ ' create the HTTP request message: ltSendStr$ = "GET /cgi-bin/getquote.exe?TICKER=" ' the Symbol field contains the ticker symbol ltSendStr$ = ltSendStr$ & UCase$(Trim$(Data1.Recordset("Symbol"))) ' finish the message ltSendStr$ = ltSendStr$ & " HTTP/1.0" & vbCrLf ' and send it dsSocket1.Send = ltSendStr$ & vbCrLf ' now we wait for the Receive event to kick in End Sub Private Sub Form_Load() Dim i% ' add the additional columns to the grid Call AddColumn("High", 1000) Call AddColumn("Low", 1000) Call AddColumn("Price", 1000) Call AddColumn("Trend", 500) ' adjust the width of the Previous Close column dbgQuotes.Columns(1).Width = 1000 ' the database name can be specified on the command line If Len(Command$) Then Data1.DatabaseName = Command$ ' or we can use the default name Else Data1.DatabaseName = App.Path & "\quotes.mdb" End If ' do a refresh to make sure DatabaseName is valid, ' Err will be > 0 if it's not valid On Error Resume Next Data1.Refresh If Err Then MsgBox "Error opening quotes database:" & Chr$(13) & Err.Description End End If ' if there are records in the database, move to the first one If Data1.Recordset.RecordCount Then Data1.Recordset.MoveFirst 'create the tray icon for QuoteWatcher Call CreateIcon(CBWnd, 100, WM_USER + 1, "QuoteWatcher Active", Me.Icon) ' hide until the user wants to see me Me.Visible = False End Sub Public Function Extract(ptBigString$, ptAfter$, ptBegin$, ptEnd$) As String ' get the substring from ptBigString$ Extract = "" ' look for the substring after we find ptAfter$ liAfterPos% = InStr(UCase$(ptBigString), UCase$(ptAfter$)) If liAfterPos% Then ' get the position of ptBegin$ liPos2% = InStr(liAfterPos%, ptBigString, ptBegin$) If liPos2% Then ' get the position of ptEnd$ liPos3% = InStr(liPos2%, ptBigString, ptEnd$) ' if end was found, extract the substring If liPos3% Then Extract = Trim$(Mid$(ptBigString, liPos2% + 1, _ liPos3% - 1 - liPos2%)) End If End If End If End Function Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer) Call RemoveIcon(CBWnd, 100) End Sub Private Sub Form_Resize() ' if the user clicked minimize If Me.WindowState = 1 Then ' hide once again Me.Visible = False ' reset to NORMAL Me.WindowState = 0 ' update the menu caption mnuShow.Caption = "Show" End If End Sub Private Sub Form_Unload(Cancel As Integer) Call RemoveIcon(CBWnd, 100) End Sub Private Sub mnuExit_Click() ' remove the tray icon Call RemoveIcon(CBWnd, 100) ' end End End Sub Private Sub mnuRetrieve_Click() 'menu item equivalent to Retrieve Now ' if not visible, If Me.Visible = False Then ' show myself Call mnuShow_Click End If ' click the Retriev Now button Call cmdRetrieve_Click End Sub Private Sub mnuShow_Click() ' if the caption is "Show" then the form ' is invisible and should be shown If mnuShow.Caption = "Show" Then ' show the form Me.Show ' change the caption mnuShow.Caption = "Hide" ' if a price has changed since last time ' form was visible, reset the tray icon If gfChange Then gfChange = False Call ChangeIcon(Me.Icon, CBWnd, 100) End If Else ' the form must be visible, hide it & update caption Me.Visible = False mnuShow.Caption = "Show" End If End Sub Private Sub mnuSuspend_Click() ' if the caption is "Suspend", check the box If mnuSuspend.Caption = "Suspend" Then Check1.Value = 1 Else ' otherwise the caption was "Activate", ' un-check the box Check1.Value = 0 End If End Sub Private Sub SpinButton1_SpinDown() ' spinning down, smallest value allowed is 1 If Val(txtDelay) > 1 Then txtDelay = Val(txtDelay) - 1 Else Beep End If End Sub Private Sub SpinButton1_SpinUp() ' spinning up, largest value allowed is 9999 If Val(txtDelay) < 9999 Then txtDelay = Val(txtDelay) + 1 Else Beep End If End Sub Private Sub tmrCheckQuotes_Timer() 'define a static loop counter Static iLoopCount% ' increment the loop counter iLoopCount% = iLoopCount% + 1 ' if the loop counter exceeds the user-defined ' time between retrievals If iLoopCount% > Val(txtDelay) Then ' if there are symbols to retrieve If Data1.Recordset.RecordCount Then ' disable this timer tmrCheckQuotes.Enabled = False ' kick off the state machine txtInternalStatus.Text = STATUS_START End If ' reset the loop counter iLoopCount% = 0 End If End Sub Private Sub tmrTimeout_Timer() ' define a static loop counter Static iLoopCount% ' increment the loop counter iLoopCount% = iLoopCount% + 1 ' if the loop counter matches the timeout value If iLoopCount% = TIMEOUT_MINUTES Then ' reset the loop counter iLoopCount% = 0 ' disable this timer tmrTimeout.Enabled = False ' set the gfTimeout flag to signal the timeout ' (the state machine is checking for this flag ' being set to True to signal a timeout) gfTimeout = True End If End Sub Private Sub txtDelay_KeyPress(KeyAscii As Integer) ' only allow numbers If KeyAscii < Asc("0") Or KeyAscii > Asc("9") Then KeyAscii = 0 End If End Sub Private Sub txtDelay_LostFocus() ' if the value is zero, set it to 1 If Val(txtDelay) = 0 Then txtDelay = 1 End Sub Private Sub txtInternalStatus_Change() Select Case txtInternalStatus.Text Case STATUS_START 'let's rock-and-roll! ' if the socket is alread connected, error! If dsSocket1.State = SOCK_STATE_CONNECTED Then txtInternalStatus.Text = STATUS_ERROR Exit Sub End If 'disable some controls dbgQuotes.Enabled = False cmdRetrieve.Enabled = False tmrCheckQuotes.Enabled = False ' set up the socket's properties dsSocket1.LineMode = True dsSocket1.EOLChar = 10 dsSocket1.RemoteHost = "www.pcquote.com" dsSocket1.RemotePort = 80 ' do a forward lookup to get the IP address On Error Resume Next dsSocket1.FwdLookup If Err Then txtStatus = "Error connecting: " & Error$ txtInternalStatus.Text = STATUS_ERROR End If ' move to the first symbol Data1.Recordset.MoveFirst ' update the tray icon tooltip Call UpdateTip(CBWnd, 100, "Retrieving Quotes") ' move to the next state txtInternalStatus.Text = STATUS_SEND Case STATUS_SEND ' time to connect ' if the Symbol field is NULL, move to the next record ' (this shouldn't happen if the database is set up properly) If IsNull(Data1.Recordset("Symbol")) Then txtInternalStatus.Text = STATUS_NEXT Exit Sub End If ' update the status box and tray icon tooltip txtStatus = "Retrieving " & Data1.Recordset("Symbol") & "..." Call UpdateTip(CBWnd, 100, "Retrieving " & Data1.Recordset("Symbol")) ' reset the gtQuote string gtQuote = "" 'connect to the server On Error Resume Next dsSocket1.Connect If Err Then txtStatus = "Error connecting: " & Error$ txtInternalStatus.Text = STATUS_ERROR End If 'reset the connection flags gfClosed = False gfTimeout = False ' enable the timeout timer tmrTimeout.Enabled = True ' loop here until the connection closes ' or a timeout occurs Do Until gfClosed Or gfTimeout DoEvents Loop ' did a timeout occur? If gfTimeout Then ' yes, update status and tooltip txtStatus = "Timeout occurred..." Call UpdateTip(CBWnd, 100, "Suspended due to timeout") ' suspend firther retrievals Check1.Value = 1 ' go to the error state txtInternalStatus = STATUS_ERROR Else ' no timeout, move to the next symbol tmrTimeout.Enabled = False txtInternalStatus = STATUS_NEXT End If Case STATUS_NEXT On Error Resume Next ' get the next symbol Data1.Recordset.MoveNext ' have we reached the end? If Data1.Recordset.EOF Then ' yes, reset to the first record Data1.Recordset.MoveFirst ' and move to the finished state txtInternalStatus.Text = STATUS_FINISHED Else ' no, retrieve the new symbol txtInternalStatus.Text = STATUS_SEND End If Case STATUS_ERROR ' ERROR!!!! On Error Resume Next ' close the socket if it's open If dsSocket1.State = SOCK_STATE_CONNECTED Then dsSocket1.Close End If ' stop the state machine txtInternalStatus = "" ' enable the controls and update the tooltip Call ToggleControls Call UpdateTip(CBWnd, 100, "QuoteWatcher Active") Case STATUS_FINISHED ' Whew, we made it! txtStatus = "Finished retrieving quotes!" txtInternalStatus = "" Call ToggleControls Call UpdateTip(CBWnd, 100, "QuoteWatcher Active") End Select End Sub Public Function szStripHTML(szString As String) As String Dim szTemp As String Dim szResult As String Dim nPos As Integer Dim nMarker As Integer '-- Copy the argument into a local ' string so the original does not ' get whacked. szTemp = szString '-- Remove HTML codes Do nPos = InStr(szTemp, "<") If nPos = False Then Exit Do Else '-- szResult contains the final ' product of this routine. szResult = szResult & _ Left$(szTemp, nPos - 1) '-- szTemp is the working string, ' which is continuously ' shortened as new codes ' are found szTemp = Mid$(szTemp, nPos + 1) nPos = InStr(szTemp, ">") If nPos = False Then '-- No complimentary arrow ' was found. Exit Do Else '-- Shorten the working ' string szTemp = Mid$(szTemp, _ nPos + 1) End If End If Loop '-- Find a marker byte by looking for ' a char that does not already exist ' in the string. For nMarker = 255 To 1 Step -1 If InStr(szResult, Chr$(nMarker)) _ = 0 Then Exit For End If Next '-- Remove carriage returns Do nPos = InStr(szResult, Chr$(13)) If nPos Then szResult = Left$(szResult, _ nPos - 1) & Mid$(szResult, _ nPos + 1) Else Exit Do End If Loop '-- Replace linefeeds with Marker bytes Do nPos = InStr(szResult, Chr$(10)) If nPos Then szResult = Left$(szResult, _ nPos - 1) & Chr$(nMarker) _ & Mid$(szResult, nPos + 1) Else Exit Do End If Loop '-- Replace marker bytes with CR/LF pairs Do nPos = InStr(szResult, Chr$(nMarker)) If nPos Then szResult = Left$(szResult, _ nPos - 1) & Chr$(13) & Chr$(10) _ & Trim$(Mid$(szResult, nPos + 1)) Else Exit Do End If Loop '-- Thats all for this routine! szStripHTML = szResult End Function Private Function ConvertFraction(ptNumber$, Optional ptSep) As Double ' converts a string containing fraction to a decimal ' ptSep can be specified if the fraction part of the ' string ptNumber$ is separated from the whole number ' with any character besides a space Dim ltSep$, ltString$, liSepPos%, liDivPos% Dim lNumerator%, lDenominator% Dim ldTemp As Double If IsMissing(ptSep) Then ltSep$ = " " Else ltSep$ = ptSep End If ltString$ = Trim$(ptNumber$) 'find the separator liSepPos% = InStr(ltString$, ltSep$) 'find the fraction sign liDivPos% = InStr(ltString$, "/") If (liSepPos% = 0) Or (liDivPos% = 0) Then ConvertFraction = Val(ltString$) Exit Function End If 'get the whole number portion ldTemp = Val(Left$(ltString$, liSepPos% - 1)) lNumerator% = Val(Trim$(Mid$(ltString$, liSepPos%, liDivPos% - liSepPos%))) lDenominator% = Val(Trim$(Mid$(ltString$, liDivPos% + 1))) If lDenominator% = 0 Then 'invalid fraction! ConvertFraction = ldTemp Else ConvertFraction = ldTemp + lNumerator% / lDenominator% End If End Function Private Sub ToggleControls() 'reset the control states tmrCheckQuotes.Enabled = True dbgQuotes.Enabled = True tmrTimeout.Enabled = False cmdRetrieve.Enabled = True End Sub Public Sub AddColumn(ptCaption As String, pdWidth As Double, _ Optional ptDataField) Dim ptField$ 'check for the option parameter If IsMissing(ptDataField) Then 'if not present, use the ptCaption parameter ptField$ = ptCaption Else ptField$ = ptDataField End If ' add the column as the right-most column dbgQuotes.Columns.Add (dbgQuotes.Columns.Count) ' set the new column's properties With dbgQuotes.Columns(dbgQuotes.Columns.Count - 1) .Visible = True .Caption = ptCaption .Locked = True .DataField = ptField$ .Width = pdWidth End With End Sub
If you've entered all the code or, better yet, copied it from the CD-ROM, you can now test the application. QuoteWatcher is simple to use.
First, make sure you have a solid connection to the Internet. QuoteWatcher won't get very far if it can't communicate with the PC Quote Web server. After your connection is established, run the application.
At first it will appear that nothing has happened. However, if you look at the taskbar notification area (the tray), you'll notice that the QuoteWatcher icon has been added. To make the window visible, double-click the QuoteWatcher icon. The QuoteWatcher form appears with an empty grid (see Figure 12.11).
Figure 12.11. Running QuoteWatcher for the first time.
Enter some symbols in the database by clicking in the first column of the grid row that has an asterisk in the row header column. Type a ticker symbol for a stock or mutual fund for which you'd like to retrieve the pricing information. As soon as you start typing, the grid moves the text to the first row of the grid. After you've entered all the ticker symbols, press the Enter key and then the down arrow. This permanently stores that symbol into the database. Enter other symbols in the same manner.
After you have all the symbols entered, click the Retrieve Now button. The retrieval process starts, and you should see the status textbox get updated as the state machine operates. The rows of the grid will update after each symbol's pricing information is retrieved. While executing the application within Visual Basic, the Receive event sends all the received data to the Debug window for you to view. This is useful in case you're having connection or timeout problemsyou can see exactly what has been received by the application.
Now, adjust the Minutes Between Lookups value to a smaller number. Click the form's Minimize button. The form disappears. Move the mouse pointer over the QuoteWatcher tray icon. Leave it stationary until the tooltip appears. The tooltip should read QuoteWatcher Active. Right-click the icon. The popup menu appears. Click the Suspend menu item. Right-click again. The Suspend menu item has changed to Activate. Select the Show menu item. The form appears, and now the Suspend Automatic Lookup checkbox is checked. Click the checkbox again to enable automatic checking. Minimize the form and wait.
After the time interval specified in Minutes Between Lookup, the retrieval process starts again. Note how the tooltip is updated to show which symbol is currently being retrieved. After the retrieval is completed, the tray icon may change to a different picture. This indicates that one of the prices has changed. Double-click the icon to view the form. The tray icon changes back to the default icon. However, if you are doing this after market hours or if your list of symbols only includes mutual funds, you probably won't see the different icon because it is unlikely that the price will change.
When you've finished testing the application, select Exit from the popup menu or click the form's close button.
The QuoteWatcher application is a very simple agent. However, the code can be easily expanded to add new features. It's also easy to add new columns to the database table and grid in order to use more of the data retrieved from the PC Quote server.
For example, you may wish to add a history table that allows you to track the prices through time. You could even create graphs of that historical data and display those to the user.
Another useful addition would be the ability to set price break points. The user could specify a high and low price limit for each symbol. Then, when the application detects that these limits have been exceeded, it could change the tray icon to reflect this fact. You could even program the application to e-mail your broker with a buy or sell order in such cases. However, you'll probably want to obtain a subscription account from PC Quote so you can retrieve real-time quotes. The server that QuoteWatcher accesses supplies quotes on a twenty minute delay basis.
The possibilities are almost limitless for QuoteWatcher. After you know how to retrieve the data from the Web server, you can do whatever you desire with it.
This chapter walked you through the process of creating an interactive Web agent. The QuoteWatcher application is just the beginning in Web agent development. Such a vast amount of information is present on the Web that it will be a long time before all possible information agents have been written. By that time, the world of interactive multimedia will present the application developer with an even wider variety of needed information processing applications.
The remaining chapters in this book present more complicated Web-based agents. I hope this chapter has given enough background to assist you in understanding these advanced applications.