REALbasic Cross-Platform Application Development

You are probably already familiar with Controls, even if you are not aware of it, because you interact with them every time you use an application.

The base class for all controls is the Control class. The Control class contains properties, events, and methods that are common to all Controls. The RectControl class is a subclass of the Control class, and this is the parent class of all user-interface Controls.

The RectControl Class

All user interface Controls are subclasses of the RectControl class, called such because all Controls are rectangular. Even when the Control doesn't "look" like a rectangle (an Oval Control for instance), it's still shaped like a rectangle as far as REALbasic is concerned.

Top, Left, Height, and Width

Unlike MouseX and MouseY, Top and Left are measured in the coordinates of the Window. Top (obviously) is the y coordinate (up and down) and Left is the x coordinate (left and right).

The height and width of the control are specified by the Height and Width properties. Normally, you can adjust the size and position of the control by dragging it with your mouse, but you can also do it programmatically by setting these properties.

LockBottom, LockLeft, LockRight, and LockTop

When a Control is positioned on a Window, you will need to decide what you want to do in the event that a Window gets resized. These properties determine what happens to the Control. You can lock the bottom, top, left, or right side of the control to the Window's bottom, top, left, or right side. When it is locked, it means that the distance between the Control's edge and the Window's respective edge remains constant. If you lock all four sides, the Control will grow right along with the Window. If you lock the right side, the Control will stay the same size, but move to the right when the Window expands in that direction.

The following figures illustrate how this works. Figure 5.10 shows the Window in its natural state, without having been resized at all:

Figure 5.10. Window position.

In Figure 5.11, the Window has been resized, and the EditField has changed sizes along with it. This is because the EditField.LockTop, LockBottom, LockLeft, and LockRight properties are set to TRue.

Figure 5.11. Expanded Window and EditField.LockTop, LockBottom, LockLeft, and LockRight set to true.

Now, in Figure 5.12, I use the same starting point, but this time the EditField.LockTop and the EditFieldLockLeft properties are set to true, and the others are set to False. This means that when the Window is resized, the EditField stays in the upper-left corner of the Window and does not get resized itself.

Figure 5.12. Window with EditField.LockTop and EditField.LockLeft set to true.

AutoDeactivate and Active

AutoDeactive determines whether the control should be deactivated if the Window is deactivated. This applies to Macintosh computers. Active is a read-only Boolean property that will let you know whether the control is active. A Control is active when the containing Window has been activated and is in the foreground.

Deprecated Properties

There are a few properties that you shouldn't use and that exist only for backward compatibility. One (a property of the Control class) is MacControlHandle, which has been replaced with the more appropriately named Handle, because REALbasic is cross-platform, after all.

Enabled

All RectControl subclasses have an Enabled property, which signifies that the Control is ready to accept user input. This can be set programmatically by simple assignment, like so:

PushButton1.Enabled = True

It can also be set through the use of Binding, which we will discuss in more detail in the section "CustomBinding."

Keeping track of when a PushButton or any other control should be Enabled can get kind of complicated and just a little tedious, especially if you have a lot of controls. Binding is the solution to the problem (most of the time) at least.

I will give examples of Binding with the appropriate controls.

Name

All Controls have a name. Because Controls aren't instantiated like a normal object, the IDE gives each Control a default name when you drag it onto the Window, and this name defaults to the name of the Control class, plus a number that goes up with each Control of the same class that you add.

In other words, if you drag an EditField to the Window, it will be named EditField1. If you drag a second EditField to the Window, it will be called EditField2, and so on. You can set this name to whatever you want it to be in the Properties pane on the right side of the editor.

Index

Another property common to all Controls is the Index property, which is used to access a Control when you are using a Control Array.

Position - MouseX and Mouse Y

MouseX and MouseY are properties that show the position of the mouse relative to the control. The position is measured from the upper-left corner, where MouseX equals 0 and MouseY equals 0.

Open and Close Events

Like the App object and Windows, Controls have Open and Close events, too. The Open and Close events of your controls is determined by their Control Order.

Sample Control: BevelButton

The first Control listed in the Built-in Control list is the BevelButton. To get that Control onto the Window, you need to select it, hold your mouse down, and drag it until you are over the Window and then release the mouse.

The first thing you'll notice is that the BevelButton is labeled Untitled. You can change that by modifying a value in the Properties pane. Now is a good time to take a look at this ListBox and discuss a few of the items you see there.

The properties are group into the headings ID, Position, Appearance, Initial State, Behavior, and Font. Different Controls will have different values, but all of them share the ID and Position groups.

The name of this BevelButton is BevelButton1. REALbasic uses this convention for all Controls. When you drag a Control onto a Window, it adds an integer to the name of the Control to establish the new name. If I were to drag a second BevelButton, it would be called BevelButton2.

Index refers to a Control Array, which is a way of handling a lot of Controls of the same type. See the section on Control Arrays for more information.

Finally, you'll see that value of the Super property is BevelButton. This means that BevelButton is the Super class for BevelButton1, and that means BevelButton1 is a subclass of BevelButton. By dragging a Control onto a Window, you are attaching it to the Window and as a consequence, it will be implicitly instantiated when the Window is instantiated.

Here's an important caveat with respect to subclassing Controls: You can subclass a control just like any other class, and you can create any number of Constructors for it; but the automatic instantiation done by REALbasic uses only the default Constructor. To initialize Controls, you should use the Open event.

Control Order and Control Arrays

Especially with respect to the Windows operating system, users are often able to tab from one control to the next. The Control Order determines the sequence in which this happens. The IDE will automatically populate this value for you, which you can easily override and set in the IDE yourself. If you want a visual representation of the Control Order, you can select the View Menu and then select the Control Order MenuItem. When you do, you will see something like Figure 5.13:

Figure 5.13. View the Control Order.

In the RSSReader application, EditField1 is in the Control Order position of "0", followed by the Pushbutton1 and then ListBox1. When the Window first opens, EditField1 calls the EditField1.setFocus method, which gives EditField1 the focus, as indicated by the FocusRing (I have UseFocusRing set to true) and the fact that the insertion bar will be blinking inside the EditField.

Drawing Guides

As you drag Controls onto the Window, REALbasic will provide guidance as to the best location for controls. In Figure 5.14, I am dragging ListBox1, and two thin aqua-ish lines get drawn that indicate the optimal spot to stop dragging.

Figure 5.14. Guidelines.

Binding

The state of one Control often depends on the state of another one. For example, it would be nice for the Subscribe button to be enabled only when there is some text in EditField1. Likewise, it would be nice for the TabPanel to be enabled only when ListBox1 has a selection. Setting this up for the ListBox1 control is easy using REALbasic's binding facilities. In Window Layout Mode, there should be two buttons on the toolbar, as shown in Figure 5.15:

Figure 5.15. Windows toolbar binding buttons.

If you do not have them, you may need to customize your Toolbar. In earlier versions of REALbasic 2005, you had to have two controls selected on the Window before you could customize the Toolbar, but that should be fixed by the time you read this.

To bind the ListBox to the TabPanel, select both of them (hold down the Shift key and click each one with your mouse). A Window much like the one shown in Figure 5.16 will appear.

Figure 5.16. Visual bindings window.

If you click the CheckBox, HTMLViewer1 will be enabled every time an item is selected in ListBox1. There are different kinds of bindings for different combinations of Controls, so the best way to identify them is to experiment and see what options you are given. If you want to see a list of all your bindings, you can press the List Bindings button on the ToolBar and get a list of active bindings as shown in Figure 5.17.

Figure 5.17. List Bindings window.

Pushbutton Control

The RSSReader application has one PushButtonPushButton1, which is clicked when the user subscribes to a new RSS feed. The Action event of the PushButton class is the most common event you will be doing this. This is the event that is fired when a user clicks the button. To be more precise, the Action event is not fired when a user presses down on the button. It is triggered when the user releases the clicked mouse button.

The PushButton control also implements a MouseDown event and a MouseUp event. Both events return a Boolean, and the way they work can be a little tricky.

When the MouseDown event is fired, the position of the mouse when it was clicked is passed to the event.

Function MouseDown(X As Integer, Y As Integer) as Boolean

If you return true, you are telling REALbasic that you're going to handle things from here. In fact, the only way to get the MouseUp event to be triggered is to return true in the MouseDown event. Where in the midst of all of this does the Action event get triggered?

It's a trick question, because the Action does not get called, presumably because you're handling the MouseUp event. One word of caution: The MouseUp event returns a Boolean. Because you return true in the MouseDown event to get access to the MouseUp event, you might think that returning true or False would have some impact on whether the ActionEvent gets called. It doesn't. In fact, the value returned by the MouseUp event evanescesit just disappears like a popped bubble.

Button State

The Enabled property can be set to true or False, and when true signifies that the PushButton is active and ready to respond to user interaction. When the button is inactive, it takes on a visual representation that varies by platform, but that shows that it is not active.

If you selected EditField1 and PushButton1, you'll see that the Add Binding button is grayed out; in other words, there are no prebuilt bindings to link EditField1 with PushButton1. If you want PushButton1 to become enabled when text is available in EditField1, you will need to implement that code in EditField1. I will do that in the coming pages.

TabPanel

A TabPanel is a subclass of PagePanel, and they both work in separate ways. A PagePanel is a transparent Control that consists of a series of pages; each page can contain its own set of Controls. This allows you to change the layout or composition of a Window without having to swap windows or do a lot of programming. A TabPanel is the same thing, except that it presents a tabbed interface to the user (a PagePanel has to be moved from page to page programmatically).

PagePanel has a property, Value, which identifies which page is at the top. It also allows you to append, insert, or delete pages. A TabPanel inherits these capabilities, plus a few more. You can designate which direction the tabs face, using the following property:

TabPanel.Facing as Integer

The class constants are as follows:

TabPanel.FacingNorth = 0 TabPanel.FacingSouth = 1 TabPanel.FacingEast = 2 TabPanel.FacingWest = 3

Because a TabPanel has tabs, you may want to set the caption for the tabs at runtime; fortunately, you can:

TabPanel.Caption(aTab as Integer) as String

The first tab is numbered 0.

In the RSSReader project, a TabPanel (as shown in Figure 5.18) is used to display the HTMLViewer and the EditField. There is no programming to be donethe initial values for the tabs can be set in the IDE.

Figure 5.18. TabPanel.

One of the problems with the TabPanel (and PagePanel) is that Controls sometimes bleed through. In the case of this application, there were many times when the EditField2 would show up in front of HTMLViewer1, even when it wasn't supposed to. This problem seems to be platform specific, with Macintosh having more problems.

The standard answer given to fix it is to go to each panel, select each Control on the panel, and then select Move To Front. This didn't work for me. There are also the methods Refresh, RefreshRect, and UpdateNow that are supposed to force redrawing of the elements. After all was said and done, I found that everything was fixed if I resized the Window, which forced everything to be redrawn. My ugly hack was to manually resize the Controls on the TabPanel to force them to redraw. I also had better luck when I made sure that the EditField2 and HTMLViewer1 were of the same size and in the same position.

EditField Control

EditFields were designed for data entry, and REALbasic provides several tools to make this as easy as possible. If you are looking for a general-purpose word processing field, you may be disappointed, but although it may lack some features such as indentation of paragraphs, it is very flexible otherwise, and you can do quite a lot with it.

Events

EditFields have similar events to other Controls. The following events are important when tracking what information is being entered into the EditField:

Function EditField.Sub GotFocus() Function EditField.KeyDown(Key as String) As Boolean Sub EditField.SelChange () Sub EditField.TextChange() Sub EditField.LostFocus()

GotFocus generally fires first when the control first gets focus. KeyDown is triggered when any key is pressed and the EditField has the focus. SelChange is triggered by a change in the location of the insertion bar, and TextChange is called when the content of the EditField is actually changed.

Mouse movement is tracked as well:

EditField.MouseDown(X as Integer, Y as Integer) As Boolean EditField.MouseUp(X as Integer, Y as Integer) As Boolean

EditFields have built-in drag and drop, which is billed as a feature; but of course it's a feature only if you don't want to have any control over how things are dragged or dropped. Like several other controls, EditField allows you to override a MouseDown event by returning TRue in the event. This means that whenever you use the mouse to position the cursor, nothing will happen because you are telling REALbasic, "Yes, it's true that I'm handling the event, so don't bother." Unfortunately, that doesn't do you much good, because none of the other events fire while the mouse is down. All this boils down to the fact that you basically have to accept the drag-and-drop features of REALbasic's EditField as is.

Text and Positioning

The text that is displayed in an EditField can be set and accessed through the Text property.

EditField.Text as String

The text itself is made up of a number of characters. To count the characters, you will need to use the Len function.

aLen = Len(EditField1.Text)

As you may recall, I needed to write some code in an EditField1 event to make PushButton1 enabled only when text is available. There are any number of perfectly valid ways to go about this, but my preference is to implement it in the KeyDown event. Every time a key is pressed and the EditField has focus, I will check to see how many characters are displayed in the EditField and then set the appropriate Enabled property for PushButton1:

If Len(me.Text) > 0 Then PushButton1.Enabled = True Else PushButton1.Enabled = False End If

When you are typing into an EditField, you will see an insertion bar that blinks between the characters, wherever it happens to be. If you want to know where the insertion bar is, you use the SelStart property:

EditField.SelStart as Integer

There are three properties pertaining to selected text. SelStart is an integer that identifies the position of the cursor in the EditField. If you have an empty EditField and you have the focus, you'll see the blinking cursor in the upper-left of the EditField, and at that point SelStart would be 0. Sellength would be 0 as well, as long as no other characters were selected.

SelStart can be kind of confusing. The important thing to remember is that SelStart is not the same thing as character position. If you want to select the first character in an EditField, set SelStart = 0 and Sellength = 1. If you do that, you will have selected the first character.

The insertion bar goes between characters and is numbered accordingly, as shown in Figure 5.19.

Figure 5.19. Character Position and EditField.SelStart.

You can still use SelText to insert or delete text, which is handy if you don't want to use the Clipboard to cut and copy text. You would want to avoid that if you were doing a search and replace and you didn't want it to affect or overwrite the content that was in the Clipboard.

If Sellength is greater than 0, you can get access to the selected text as a string with SelText. If Sellength is 0, there is no text to provide, but that doesn't mean you can't use it. If you want to insert text without overwriting any text, you can set Selstart to the position where you want to insert the text, set Sellength to 0, and then set SelText equal to the text you want to insert.

If there is no text in an EditField, and the EditField has the focus, SelStart will be 0. In addition to SelStart, there is also a SelLength property:

EditField.SelLength as Integer

If you have selected text and it is highlighted, SelLength measures the length of the text that is highlighted. If no text is highlighted, the SelLength is 0. The text that is highlighted is accessible through the SelText property:

EditField.SelText as String

You use SelText to get and set selected text. If you want to access the text, you can use simple assignment:

aString = EditField1.SelText

If you want to replace selected text with new text, do this:

EditField1.SelText = "Some new text"

If you want to delete the selected text, assign an empty string to it, like so:

EditField1.SelText = ""

You can still use SelText, even when no text is selected and SelLength is 0. This is especially useful when inserting text or appending text. When appending text, you might be tempted to use string concatenation, like this:

EditField1.Text = EditField1.Text + "Here's some new text"

This will likely cause the screen to flicker and be slow if you are appending a lot of text to the EditField. The solution is to do the following:

Dim aLen as Integer aLen = Len(EditField1.Text) EditField1.SelStart = aLen EditField1.SelText = "Here's some new text"

There are also properties that deal with character positions. The first character in the EditField is at character position 1. If you want to select the first character programmatically, you can use SelStart and SelLength:

EditField1.SelStart = 0 EditField1.SelLength = 1

You can use CharPosAtXY to find a particular character position relative to a coordinate for the EditField.

EditField.CharPosAtXY(X as integer, Y as integer) as Integer

Often, the mouse position is the source of the coordinate. You could use this to find out which word the mouse is over, for example. Suppose that for some reason, you want to select a character at a certain coordinate. Here's how you would do it:

Dim charPos as Integer charPos = EditField1.CharPosAtXY EditField1.SelStart = charPos - 1 EditField1.SelLength = 1

Now suppose you want to know which line a particular character is on. The following method will help:

EditField.LineNumAtCharPos(aPosition as Integer) as Integer

You can do this:

Dim aLine as Integer aLine = EditField1.LineNumAtCharPos(1)

If you are looking at the first character, as in the preceding sample, the line number would be 1. You also might want to know how many lines in total the EditField has:

Dim aLen as Integer Dim aLine as Integer aLen = Len(EditField1.Text) aLine = EditField1.LineNumAtCharPos(aLen)

Finally, you can ascertain the first character position of any given line using the following method:

EditField.CharPosAtLineNum(aLine as Integer) as Integer

The character returned is the first character on the line. Consider this:

Dim charPos as Integer charPos = EditField1.CharPosAtLineNum(1)

Because you are asking for the character position for the first line, charPos would be equal to 1.

Data Entry

EditFields are perhaps most commonly used as data entry fields rather than as a field used for word processing. Generally speaking, when this is the case, there are several fields on the Window and you want the user to be able to navigate through the tabs quickly, with the least amount of trouble. One way to do this is to let users tab through the fields. When they press the Tab key, they automatically go on to the next field in the sequence. There are also times when you want a user to be able to enter tabs into the field itself, so you have the option of setting whether the field accepts tabs:

EditField.AcceptTabs as Boolean

You can control the order in which this tabbing sequence occurs by setting the ControlOrder value for the control in the Window layout editor. You also will want to make certain that the user starts entering text at the right location, so you should automatically set the first to the field with the ControlOrder position of 0. An EditField can call a method to give itself the focus:

EditField1.setFocus

On a typical Windows application, all Controls normally accept focus. In REALbasic applications, this wasn't always the case if the Control used by REALbasic wasn't a native Windows Control. Now Controls have an AcceptFocus property that allows all Controls to accept focus. This is not the case with Macintosh computers, where most Controls do not accept focus, except for ones that accept data entry, such as EditFields.

You have the option of using a focus ring with your Macintosh users; an eerie blue glow lights up around the field with focus:

EditField.UseFocusRing as Boolean

EditFields by default have borders, but you can turn them off using the Border property:

EditField.Border as Boolean

If you are using the fields for data entry, you would normally keep the borders turned on. You can turn them off if you want to draw your own border or, as is more often the case, you want to use the EditField in a way that it doesn't look like an EditField. For example, if you put an EditField on top of a white rectangle, you could mimic the capability to have margins on the page.

EditField.MultiLine as Boolean

You can automatically format numbers that are entered into the EditField using the same techniques as the Format function:

EditField.Format(aFormat as String)

If the EditField is not in MultiLine mode, you can limit the amount of text that can be entered using LimitText:

EditField.LimitText(aLen as Integer)

For more complex control over what gets entered, you can set a mask for the EditField:

EditField.Mask(aMask as String)

The following characters are used to create the mask:

?

Character placeholder.

C

Optional character or space placeholder.

&

Character placeholder. ASCII characters from 32-126 and from the non-ASCII range of 128-255.

#

Required single-digit placeholder.

9

Optional single-digit placeholder .

A

Required alphanumeric character.

a

Optional alphanumeric character.

.

Decimal placeholder (locale specific).

,

Thousands separator (locale specific).

:

Time separator (locale specific).

/

Date separator (locale specific).

>

Uppercase (all following characters).

<

Lowercase (all following characters).

Literal

The actual character to be displayed.

\

Escape character: Forces special characters to be treated as literals.

~

Reserved for future use.

Following are some examples of common masks you may need to use from time to time:

Social Security number:

###-##-####

Phone Number:

(###) ###-####

Currency (up to $99.99)

$9#.##

IP Address

###.###.###.###

Date

##/##/####

Time

9#:## AM

Time, part two

9#:## \AM

If the text entered into a field doesn't match the required match, a ValidationError event will occur:

EditField.ValidationError(InvalidText as String, StartPosition as Integer)

When sensitive information is being entered and you do not want prying eyes to see it (such as when you are entering a password), you can set the Password property to true:

EditField.Password as Boolean

This is disabled if MultiLine is set to true. When Password is set to True, a bullet appears with each keystroke rather than the actual character, which provides at least some privacy when a user is asked to enter a password (in case someone is looking over the user's shoulder).

If you need simply to display text that will not be edited, you can usually use a StaticText control, but you can also use an EditField and set the ReadOnly property to TRue:

EditField.ReadOnly as Boolean

When an EditField.ReadOnly property is true, the text cannot be changed by the user. You can set this property at runtime.

There are a few common patterns of using EditFields. If you use them as fields in a data entry form, the properties are generally set like so:

EditField1.Multiline = False EditField1.ScrollbarVertical = False EditField1.ScrollbarHorizontal = False EditField1.Styled = False EditField1.FocusRing = True EditField1.AcceptTabs = False

If you have an EditField you want people to edit or that displays highlighted text of some sort, then

EditField1.Multiline = True EditField1.ScrollBarVertical = True EditField1.ScrollbarHorizontal = False EditField1.Styled = True EditField1.FocusRing = False EditField1.Accept Tabs = False

One of the more remarkable attributes of REALbasic is how easy it is to set up a database. The following properties are used in that process and will be discussed in more detail when the topic of databases and binding is covered.

EditField.DataField(aField as String) EditField.DataSource(aDataControl as DataControl) EditField.LiveUpdate as Boolean EditField.BindValue(aValue as StringProvider)

Scrolling

EditField.ScrollbarHorizontal (ro) EditField.ScrollBarVertical (ro) EditField.ScrollPosition EditField.ScrollPositionX

Setting Style Properties for All Text

There are many instances when you need to apply the same style to an entire field. You can use these properties to both get and set the values, and they apply to all the text in an EditField:

EditField.TextColor As Color EditField.BackColor as Color EditField.Bold as Boolean EditField.Underline as Boolean EditField.Italic as Boolean

Handling fonts deserves a little extra attention. The following properties are used to get and set the current font and text size:

EditField.TextFont As String EditField.TextSize As Integer

First, the TextSize is an integer, which means that you cannot use font sizes such as 10.5. Second, you may be wondering how to determine what string to use to specify a font. REALbasic provides the predictably easy way to get it:

REALbasic.FontCount as Integer REALbasic.Font(positionas Integer) as String

The following example shows you one way to use it. I loop through all of the fonts and add their name to a ListBox called FontList:

Dim y as Integer Dim x as Integer y = FontCount -1 For x = 0 To y me.FontList.addRow(Font(x)) Next

Controls use a font called System, which is a reference to the default font for each platform. You can override this and set any control to use whatever font you want, but it is recommended to stick with the System font in most cases because that's the font that the user-interface guidelines for each package calls for. They are fonts that are designed to be used in concert with other platform specific controls, and it also tends to look better.

Alignment Constants

The alignment of the EditField is set with the Alignment property:

EditField.Alignment as Integer

You can set the alignment using the following class constants:

EditField.AlignDefault = 0 (left) EditField.AlignLeft = 1 EditField.AlignCenter = 2 EditField.AlignRight = 3

If you are testing the value of Alignment and see that it is -1, it means that the current selection is mixed and spans paragraphs.

Setting Style Properties on Selected Text

There are two basic approaches to setting style values only on selected text, rather than on all the text in the EditField. The first group of methods "toggle" the style value of the selected text. In other words, if the selected text is already bold, when it is toggled it will no longer be bold. Likewise, if it isn't bold, it will be bold.

EditField.ToggleSelectionBold EditField.ToggleSelectionItalic EditField.ToggleSelectionUnderline

There are some Macintosh "Classic" options as well that you might encounter, but that are not in modern Macintosh systems:

EditField.ToggleSelectionShadow EditField.ToggleSelectionOutline EditField.ToggleSelectionCondense EditField.ToggleSelectionExtend

Contrast toggling behavior with the behavior of the following properties that allow you to set the style information directly. One advantage of this method is that you can use these properties to find out the current style as well as to set the style:

EditField.SelBold as Boolean EditField.SelItalic as Boolean EditField.SelUnderline as Boolean

SelAlignment uses the alignment constants discussed previously. Because alignment applies to paragraphs and not just strings of text, the entire paragraph will take on the alignment set here:

EditField.SelAlignment as Integer

The following are valid only for "Classic" Macs:

EditField.SelShadow as Boolean EditField.SelOutline as Boolean EditField.SelCondense as Boolean EditField.SelExtend as Boolean

These set color, font, and size on the selection:

EditField.SelTextColor as Color EditField.SelTextFont as String EditField.SetTextSize as Integer

The following methods let you copy and paste selected text:

EditField.Copy EditField.Paste

Note that there is no Cut, but that's easily fixed because Cut is a combination of Copy and Delete. You can delete the selected text easily, as demonstrated before:

EditField1.SelText = ""

When you copy the text, it's copied to the Clipboard. See the section on MenuItems later in this chapter for a more complete discussion of how to manage copying and pasting of text.

StyledText

EditFields can be either styled or not styled. This is set or accessed through the Styled property:

EditField.Styled as Boolean EditField.StyledText as StyledText

The StyledText class largely replaces TextStyleData, which is a holdover from the pre-Windows, pre-Linux days and was used in Macintosh programming. You access styled text from the StyledText property of the EditField, or you can instantiate StyledText objects independently of an EditField.

ListBox

A ListBox is one of those controls that you will find yourself using all the time. It is used to display lists, grids, and hierarchical data.

Adding Rows

When working with a regular ListBox, you can add rows easily. In fact, if you have a ListBox with a single column, all you need to do is the following:

AddRow("Text for this row")

When working with a hierarchical ListBox, you get access to another method, called AddFolder. (If you use AddFolder without the Hierarchical property set to true, it just functions like the AddRow method.)

Unfortunately, REALbasic is suffering from method-naming inconsistency with respect to how new data is inserted, deleted, and manipulated in the ListBox. You can add "rows" and "folders," and you can "remove" rows as well, and you do this using the following methods:

ListBox.AddRow Item as String ListBox.AddFolder Item as String ListBox.InsertRow Row as Integer, Item as String ListBox.InsertFolder Row as Integer, Item as String ListBox.RemoveRow Row as Integer ListBox.DeleteAllRows

When you use the AddFolder method, don't expect to see any folders anywhere. What you will see is that REALbasic has added a row, plus an indication that this row might contain additional data. On the Macintosh, this is signified by a gray disclosure triangle and on Windows, it's denoted by a little box with a cross in it.

You can also insert folders with the InsertFolder method. There is not a RemoveFolder method, however. You must rely on the RemoveRow to remove any kind of row, hierarchical or not.

The following code from the SubscribedFeeds.open() event shows a simple example of how to add rows to a ListBox:

Dim tis as TextInputStream Dim s as String RSSFile = ApplicationSupportFolder.Child("info.choate.rssreader") If RSSFile.Exists Then tis = RSSFile.OpenAsTextFile If (tis <> Nil) And (List <> Nil) Then While Not tis.EOF s = tis.ReadLine() If s <> "" Then List.AddRow(s) End If Wend End If End If

Selecting Rows and Manipulating Cells

You can identify which row is selected by checking the following property:

ListBox.ListIndex as Integer

You can also readily find out the last row that was added (either through appending or inserting the row) using this similarly named property:

ListBox.LastIndex as Integer

If the ListBox only has one column, you can use the List method to get the text in the cell of a given row. For example, if I want to get the text of the first row in a ListBox, I can do the following:

Dim s as String s = ListBox1.List(0)

To refer to a specific cell in the ListBox, you use the Cell property. There are two parameters: the first is the row number and the second is the column number. The following code refers to the first cell in the first row:

Dim s as String s = ListBox1.Cell(0,0)

There is also a new feature: You can now select ListBox1.Cell(-1,-1) to get and set the contents of the ListBox. If you use only -1 in the position indicating rows, you will get all the rows for that column. Likewise if you use -1 in the position indicating columns, you will get all the columns for that row. The strings are Tab delimited with respect to columns, and EndOfLine delimited with respect to rows.

The following code from the SubscribedFeeds.write() event gives you an example of how to reference Cells in a ListBox:

Dim tos as TextOutputStream Dim x,y as Integer Dim s as String tos = RSSFile.CreateTextFile y = me.List.ListCount - 1 For x = 0 To Y s = me.List.Cell(x,0) If s <> "" Then tos.WriteLine(s) End If Next tos.Close

Hierarchical ListBoxes

A hierarchical ListBox is one that displays hierarchical data, rather than lists. Using REALbasic's hierarchical ListBox features is a lot easier now than it used to be, in large part because of the CellTag property, which allows you to associate a Variant value with each individual cell.

When working with a hierarchical ListBox, you will want the user to be able to click a folder row and reveal the data contained by that folder. For this to happen, theauser has to click the disclosure arrow (or box), not simply the row itself. If you click the text in the row, you will select that row, but that will not expand the row.

When the disclosure area is clicked, the ExpandRow(Row as Integer) event is called.

If you want to see if a particular row is expanded, you can test against the Expanded(Row as Integer) property, which will return true if the row is expanded.

If the row is already expanded, clicking the disclosure area will cause the row to collapse, triggering the CollapseRow(Row as Integer) event.

Fortunately for you, you don't have to worry about the data that is in the rows that disappear; REALbasic takes care of deleting them for you.

One of the hardest parts about dealing with hierarchical ListBoxes before was keeping track of the hierarchical data itself and knowing what to show when a row expanded, and what not to show when it was collapsed. The usual way to do this was to have a separate array of visible values, along with some kind of hierarchical data (nested dictionaries, or a hierarchy of objects) that you manually kept track of with each expansion or contraction of the list. Now it's much easier because of the CellTag property.

You still need to have a set of hierarchical data of some sort (unless you are able to determine the subdata dynamically when the rows are clicked).

The most common set of hierarchical data you'll definitely have around is the hierarchy of the file system on your computer, so I'll show you an example of how to view the folders and files on your hard drive.

File systems and XML documents are among the kind of data that are hierarchical and suitable for display in this way. Here's an example that shows how to create a ListBox that will navigate through the folders and files on your computer.

Drag a ListBox to the Window and make sure the Hierarchical check box is checked. You need to implement two events. The first is Open:

Sub Open() Dim f as FolderItem // Make certain it s hierarchical me.Hierarchical = True f = Volume(0) me.AddFolder(f.DisplayName) me.CellTag(me.LastIndex, 0) = f End Sub

In this event, the root directory of the primary Volume is selected and its name is added to the ListBox, more or less as it was in the previous ListBox example. One difference is that instead of AddRow, AddFolder was used. This indicates that this is a branch node, rather than a leaf node. In other words, it can contain children. The other item of interest is that I assign a reference to the root folder to CellTag, which is a variable that can be set for each individual cell in the ListBox. It's a Variant, so it can be any object or data type and it is used to store a value related to what is being displayed. In this case, I've associated the FolderItem itself. You'll see why I did this in the second event,

ExpandRow: Sub ExpandRow(row as Integer) Dim f as FolderItem Dim x,y as Integer f = me.CellTag(row, 0) y = f.Count For x = 1 To y If f.Item(x).Directory Then me.AddFolder(f.Item(x).DisplayName) Else me.AddRow(f.Item(x).DisplayName) End If me.CellTag(me.LastIndex, 0) = f.Item(x) Next End Sub

The ExpandRow event is fired when a user clicks the disclosure arrow (or box, in the case of Windows) in the ListBox. In this example, the user will click to examine the contents of the root folder. The row number is passed to the event and it is used to get a reference to the FolderItem associated with that Cell. The next step is to iterate through the directory and display the children. Each item within the directory is tested to see whether it is a folder or a file, and AddFolder or AddRow is called accordingly.

You now have a working ListBox that will navigate your computer's file system. There is a CollapseRow event, too, but you do not need to use that to delete the rows that are displayed as children of the row that was clicked. REALbasic takes care of that for you.

In this example, we coded the events in the normal way, but there is another way to go about it, by subclassing the ListBox. In the IDE, go to the Project Tab and add a new class. Call it FolderListBox. Set the Super of the class to ListBox and double-click it to edit it. In the second release of REALbasic 2005, the new class appears, but there are no events, methods, or constants listed at all. This is likely a bug that will be fixed, but it's easy to work around for now. One of the options is to add an event definition. Click that and you'll be able to create a new event definition, but this also causes all the other ListBox events to show up. After this is done, update the Open and ExpandRow events just like you did before. (Don't bother to create a new event. The only reason to click Add Event Definition was to get the other events to be visible).

After that is done, go to the Window of your application and drag a ListBox onto the Window. In the Properties pane, click the PopupArrow to select a new Super class for the ListBox. REALbasic already knows about FolderListBox, so you'll be able to select it from the menu. Now, go to the FolderListBox on the Window and double-click it as if you were going to implement new events and look for the Open and ExpandRow events.

Any luck? The answer is nothere's not an Open or ExpandRow event to be seen; the reason is that you implemented it as a subclass. Remember that when an object is looking for a method, it first checks to see if it implements the method, and then it checks to see if it Super implements the method, and so on. It starts at the lowest level of the inheritance tree and works its way back up to the top. Events work just the oppositethe search for the event implementation starts at the top of the inheritance tree, and not the bottom. After it finds the implementation, it is executed, but no implementations lower down the tree are implemented, unless you specifically tell it to.

To accomplish this, you have to add two Event Definitions to the original FolderListBox subclass. Add an Open and ExpandRow event that looks just like the respective originals. Keep the same name and same signatures; it's not a problem that it is the same name as an event that already exists. The reason is that you will not be implementing these new events. Next, go to the ExpandRow event that you have already implemented and add a line so that it looks like this:

Sub ExpandRow(Row) Dim f as FolderItem Dim x,y as Integer f = me.CellTag(row, 0) y = f.Count For x = 1 To y If f.Item(x).Directory Then me.AddFolder(f.Item(x).DisplayName) Else me.AddRow(f.Item(x).DisplayName) End If me.CellTag(me.LastIndex, 0) = f.Item(x) Next ExpandRow(row) End Row

Every row that is added from within the ExpandRow event is added as a child row of the row that is expanded.

Do the same with Open:

Sub Open Dim f as FolderItem me.Hierarchical = True f = Volume(0) me.AddFolder(f.DisplayName) me.CellTag(me.LastIndex, 0) = f Open End Sub

Now, go back to the Window Editor, double-click the FolderListBox item, and look for the two events. You'll be happy to see that they now exist and you can implement them as you like. In the original class, all I did was "trigger" the new Open and ExpandRow events after I was finished processing so that whatever you implement for the FolderListBox that has been dragged to the Window will be executed afterward. You have defined a new event and called that event. In this example, you did it so that you can add code to the FolderListBox after it has been dragged to the Window, but you can also use the same technique to add events to your own custom classes.

ListBox Pictures

REALbasic also provides a simple way of adding images to ListBoxes. To access images from within the ListBox, you need to drag the images into REALbasic and drop them into the Project Editor under the Project Tab. One shortcut is to create a folder on your computer where all the image resources will be stored and keep it in the same folder as the project. I typically name the folder resources and place all the images I plan on using into it. I then drag the resources folder into the REALbasic IDE and drop it into the Project Editor. The folder and all its contents are automatically imported into the project.

Unlike classes that are imported into a project, images are always imported as external items, which means that if you move the resources folder on your computer, or delete it, REALbasic will prompt you to find it the next time you try to edit the application. That's another reason for keeping all the resources neatly stored in one location, alongside the project.

After the images are imported, you can refer to them by their name (without the extension). If you select one of the images, you can also edit a few properties. You can obviously change the name, but you can also choose whether the color white will be treated as a transparency. If you want more advanced control over how the image is displayed, you can also identify a mask for it, which works just like the masks do when creating icons for your application. Most of the time, setting the color white to transparent is effective, but you have to be careful if you use drop shadows and things like that. The reason is that the only color that is transparent is the color white, something with an RGB value of &cFFFFFF. There are a lot of colors that are almost white, but not actually white, such as &cFFFFFE, which will not be transparent. If you have a drop shadow that looks good against a white background, and then you set white to transparent, you'll find that there is a whitish outline around it when you view it on a Window that has a normal gray background. Creating a mask is probably the best solution for graphics with drop shadows.

In this example, I have imported three images: openfolder.gif, closedfolder.gif, and file.gif. (You can use different image formats, depending on the platform and whether QuickTime is installed. These are GIF images, but BMP, JPEG, and PNG should all work as well). I want to use these in the hierarchical ListBox to more clearly identify folders and files. I will use closedfolder.gif on folders that have not been expanded. I will change that image to openfolder.gif when that row is expanded, and I will use file.gif to represent files.

For this to work, I need to make the following modification to the Open event:

Sub ListBox1.open() Dim f as FolderItem me.Hierarchical = True f = Volume(0) me.AddFolder(f.DisplayName) me.CellTag(me.LastIndex, 0) = f me.RowPicture(me.LastIndex) = closedfolder Open End Sub

As you can see, the process is a simple one. The relevant line is:

me.RowPicture(me.LastIndex) = closedfolder

The argument Me.LastIndex refers to the most recent row that has been added. I assign the closedfolder image to the last row that was added to the ListBox which, in this case, is the first row because I have just added the reference to the root folder.

The process is similar for the ExpandRow event:

Sub ExpandRow(row as Integer) Dim f as FolderItem Dim x,y as Integer me.RowPicture(row) = openfolder f = me.CellTag(row, 0) y = f.Count For x = 1 To y If f.Item(x).Directory Then me.AddFolder(f.Item(x).DisplayName) me.RowPicture(me.LastIndex) = closedfolder Else me.AddRow(f.Item(x).DisplayName) me.RowPicture(me.LastIndex) = file End If me.CellTag(me.LastIndex, 0) = f.Item(x) Next ExpandRow(row) End Sub

In this example, I first changed the image associated with the row that is being expanded. I changed the image to openfolder.gif:

me.RowPicture(row) = openfolder

Further along, I'll add folders or rows according to the type of FolderItem each is. If I am adding a folder, I assign the image closedfolder.gif, and if I am adding a file, I assign file.gif. In both of these instances, I determine the row whose picture I need to change by identifying the most recent row that was added. RowPicture must follow either AddFolder or AddRow.

Finally, I'll add some code to the CollapseRow event to change the image back to closedfolder.gif when the row is collapsed:

Sub CollapseRow(row as Integer) me.RowPicture(row) = closedfolder End Sub

Now when the ListBox is displayed, the folders and files are displayed alongside them. It's a simple task that makes the ListBox look much more professional and easy to read. The visual cues offered by the images make it easier for the user to identify and distinguish folders from files. It would be even better if I assigned different images for different file types, or for different folders. The process is the same, with the exception that I would have to test to see what the underlying file type was and assign an image accordingly.

Inserting and Removing Rows

In addition to adding rows, you can insert and remove rows as well. This works exactly like you would expect it to using the following methods:

ListBox.RemoveRow(row as Integer) ListBox.InsertRow(row as Integer, aString as String)

When a row is inserted, the row that exists at the position being inserted into is moved up. So the following inserts a row at the very beginning of a ListBox and moves all other rows up:

ListBox1.InsertRow(0, "My new row text")

Now consider the hierarchical ListBox shown in Figure 5.20.

Figure 5.20. Hierarchical ListBox with expanded rows.

What happens when you insert a new item into row three?

ListBox1.InsertRow(3, "My new row text")

The row is inserted, but it is not inserted as a child of the second row, "Dog". Next, try to insert a folder:

ListBox1.InsertFolder(3, "My new folder text")

A folder is inserted, just like the row was inserted, but there is a problem. The items that were once children of "Dog" are now indented as if they are children of the new folder, but the new folder is not expanded. Therefore, you should expand the inserted folder immediately:

ListBox1.InsertFolder(3, "My new folder text") ListBox1.Expand(ListBox1.LastIndex) = True

It really should do this automatically and it may be something that's corrected in the future, but for now you will need to expand it yourself. If you fail to expand it, the user will be able to click it and, if there are child elements, they will be displayed along with the child elements that were made children by inserting the folder. This does not produce an error, but it can be confusing from the user perspective because you have a scenario where a folder row is collapsed and still has child elements being shown. After the user collapses the original row, the entire list of child elements will collapse, and from that point on it will behave as expected.

ListBox Drag and Drop

REALbasic provides an implementation of drag and drop for ListBoxes that is powerful and easy to use, and it also gives developers the option of implementing drag and drop themselves for a more highly customized approach. I'll start with REALbasic's implementation first.

There are two properties related to drag and drop for ListBoxes:

ListBox.EnableDrag as Boolean ListBox.EnableDragReorder as Boolean

Both can be set in the IDE. EnableDrag is used to allow dragging items out of a ListBox onto another control. EnableDragReorder is used to allow dragging and dropping within the ListBox, and it can be used for plain ListBoxes as well as hierarchical ListBoxes.

The DragRow event is triggered by clicking down on the mouse and moving the mouse while the mouse button remains down. The DropObject event is triggered when the mouse button is released.

If EnableDrag is true, but EnableDragReorder is not true, you must return TRue in the DragRow event:

ListBox.DragRow(dragItem as DragItem, row as Integer) as Boolean

Although the documentation says that this is not the case, my experience is that you have to have EnableDragReorder and EnableDrag set to TRue to drag data from the ListBox and drop it on another control. If EnableDragReorder is not true, but EnableDrag is, you will get visual feedback that a drag is occurring, but the DropObject event will not be triggered on the other control on which it is dropped.

The simplest scenario is to drag and reorder rows in a nonhierarchical ListBox. Set EnableDragReorder to true and the user will be able to drag rows within the ListBox. A thin insertion bar is used to indicate the location where the row will be inserted. A DragReorderRows event is fired when the row is dropped. In a nonhierarchical ListBox, it passes an integer that represents the row into which the dragged row will be inserted. However, you really do not need to bother yourself with this in basic situations because REALbasic will handle deleting the row from the original position and inserting it into the new position for you.

Dragging and dropping using a hierarchical ListBox is more complicated. REALbasic lets you perform the drag, but the developer is responsible for inserting rows in the proper location. When a user drags on the ListBox and drops the object at a particular location, the DragReorderRows event is triggered, but now it not only passes the row where the object was dropped, but also the row that is the parent row of this row. The values that are passed are adjusted under the assumption that rows will be deleted. Thinking about this conceptually is more than just a little confusing, so a few examples should clarify things. As in my previous hierarchical ListBox example, it is best to make heavy use of CellTag if you want to implement drag and drop in a hierarchical ListBox. The reason for this relates to the problem we had earlier when inserting folders into rows in certain situations.

Class mwNodeListBox Inherits from ListBox

Listing 5.42. Sub mwNodeListBox.CollapseRow(row as Integer) Handles Event

me.RowPicture(row) = closedfolder

Listing 5.43. Sub mwNodeListBox.ExpandRow(row as Integer) Handles Event

Dim Node as mwNode Dim tmpNode as mwNode Dim x,y as Integer me.RowPicture(row) = openfolder Node = me.CellTag(row, 0) y = Node.Sequence.getChildCount - 1 For x = 0 To y tmpNode = Node.Sequence.getChild(x) If Not tmpNode.isLeaf Then me.AddFolder(tmpNode.Name) me.RowPicture(me.LastIndex) = closedfolder Else me.AddRow(tmpNode.Name) me.RowPicture(me.LastIndex) = file End If me.CellTag(me.LastIndex, 0) = tmpNode Next ExpandRow(row) 'App.expandRow(row)

Listing 5.44. Function DragReorderRows(newPosition as Integer, parentRow as Integer) as Boolean Handles Event

[View full width]

Dim d as new Date() Dim x,y as Integer Dim visibleChildren as Integer Dim parentDepth as Integer Dim siblingDepth as Integer Dim count as Integer Dim deletedRow as Integer Dim insertPosition as Integer Dim sourceNode as mwNode Dim sourceParentNode as mwNode Dim newParentNode as mwNode Dim tmpNode as mwNode // Insert position should be relative to parent object; // If parent object has children with open folders, Then those folders need to be removed from the total count. // You are dragging the selected row, // which means that it will be deleted // from its current postion. Get the row number. deletedRow = getSelectedRow // Get the object associated with the row that will be deleted sourceNode = getSelectedNode // Get a reference to the parent object (folder) // from the selected object. sourceParentNode = getSelectedNodeParent //Remove row from ListBox and Remove Child from Parent Me.removeRow(deletedRow) If Not(sourceParentNode is Nil) Then sourceParentNode.Sequence.removeChild(sourceNode) End If // Next, the object that was deleted needs to be inserted // into the proper position according to where it was dropped // I can't simply insert it into the newPosition row, because // it is a child of a folder - so it needs to be inserted // into the parent object, Then displayed. // need to account for deletion of rows; // because parentRow assumes it's already been deleted. tmpNode = getNode(parentRow) parentDepth = tmpNode.Depth siblingDepth = parentDepth + 1 count = 0 // You need to determine where to insert // the source object in the new parent object. // You cannot rely on the newposition/ParentRow // Integers, because that depicts the total // distance between the two and siblings to // the child may be Folders and they may be open. y = parentRow + 1 For x = newPosition - 1 DownTo y If getNode(x).Depth = siblingDepth Then count = count + 1 End If Next // This is the position with the sequence // of parent object's children insertPosition = count If tmpNode.isLeaf Then // An Item canNot be a parent // Put it back in its original location sourceParentNode.Sequence .insertChild(sourceNode.getPosition, sourceNode) Return True Else newParentNode = tmpNode End If If (newParentNode.isBranch) And (sourceParentNode.isBranch) Then // Update the parent information and modification date // of the object just moved sourceNode.setParent(newParentNode) // Insert the object into position within the parent If insertPosition >; -1 And insertPosition <; Me.ListCount Then newParentNode.Sequence.insertChild(insertPosition, sourceNode) Else Break End if // Select the new parent row Me.ListIndex = parentRow // Collapse and expand it to display // new children collapseSelectedNode expandSelectedNode // Select the moved object, because it was selected // when you moved it. selectNode(sourceNode) Else Return True // prevents reorder from happening End if Exception Err Break

Listing 5.45. Sub mwNodeListBox.Open() Handles Event

test me.Hierarchical = True me.EnableDrag = True me.EnableDragReorder = True

Listing 5.46. Function mwNodeListBox.EditCopy() as Boolean Menu Handler

Dim node as String Dim cb as Clipboard 's = me.List(Me.ListIndex) node = getSelectedNode().toString() cb = new Clipboard cb.AddRawData(node, "RSSReader") 'cb.SetText(s) cb.Close MsgBox "ListBox MenuHandler" Return True

Listing 5.47. Function mwNodeListBox.EditPaste() as Boolean MenuHandler

Dim cb as New Clipboard Dim s as String Dim node as mwNode If cb.RawDataAvailable("RSSReader") Then s = cb.RawData("RSSReader") node = new mwNode(s) Me.InsertRow(me.ListIndex, node.getName) Me.CellTag(me.LastIndex, 0) = node End If

Listing 5.48. Sub mwNodeListBox.view(aNode as mwNode)

Dim Node as mwNode me.Hierarchical = True Node = aNode me.AddFolder(Node.Name) me.CellTag(me.LastIndex, 0) = Node me.RowPicture(me.LastIndex) = closedfolder

Listing 5.49. Sub mwNodeListBox.collapseSelectedNode()

// Programmatically collapse a group If Me.ListIndex >; -1 Then Me.expanded(Me.ListIndex) = False End If Exception err #If DebugBuild System.Log(System.LogLevelError, str(err.ErrorNumber) + ": " + err.Message) #endif

Listing 5.50. Sub mwNodeListBox.expandSelectedNode()

// Programmatically expand a group row If Me.ListIndex >; -1 Then Me.expanded(Me.listIndex) = True End If Exception err #If DebugBuild System.Log(System.LogLevelError, str(err.ErrorNumber) + ": " + err.Message) #endif

Listing 5.51. Function mwNodeListBox.getNode(row as Integer) As mwNode

// Get meme from the row number passed as an argument Return Me.CellTag(row, 0) Exception err #If DebugBuild System.Log(System.LogLevelError, str(err.ErrorNumber) + ": " + err.Message) #endif

Listing 5.52. Function mwNodeListBox.getSelectedNode() as mwNode

Dim m as mwNode // Return the currently selected Node m = mwNode(CellTag(ListIndex, 0)) Return m Exception Err break // Ignore OutofBoundsException If err Isa OutOfBoundsException Then Return Nil End If

Listing 5.53. Function mwNodeListBox.getSelectedNodeParent() as mwNode

Dim m as mwNode Dim p as mwNode Dim x, y as Integer m = mwNode(CellTag(Me.ListIndex, 0)) Return m.getParent Exception err #If DebugBuild System.Log(System.LogLevelError, str(err.ErrorNumber) + ": " + err.Message) #endif

Listing 5.54. Function mwNodeListBox.getSelectedNodeType() as Integer

Dim m as mwNode // Return the meme type (as an Integer) If Me.ListIndex = -1 Then Return 0 Else m = Me.CellTag(Me.ListIndex, 0) Try Return m.getTypeID Catch Return 0 End End If Exception err break #If DebugBuild System.Log(System.LogLevelError, str(err.ErrorNumber) + ": " + err.Message) #endif

Listing 5.55. Function mwNodeListBox.getSelectedRow() as Integer

// Get the currently selected row number Return Me.ListIndex Exception err #If DebugBuild System.Log(System.LogLevelError, str(err.ErrorNumber) + ": " + err.Message) #endif

Listing 5.56. Sub mwNodeListBox.selectNode(aNode as mwNode)

Dim x,y as Integer Dim pos as Integer Dim m as mwNode // Select the row in the listbox // that contains a reference to // meme passed in the parameters. // get currently selected meme position pos = Me.ListIndex If pos = -1 Then pos = 0 End If // get list length y = Me.ListCount - 1 //search for visible children of currently selected meme only For x = pos To y m = getNode(x) // Test against Guids, because actual content may be // different and they may be different instances Next // If you can't find the meme, select Nothing Me.ListIndex = -1 Exception err #If DebugBuild System.Log(System.LogLevelError, str(err.ErrorNumber) + ": " + err.Message) #endif

Listing 5.57. mwNodeListBox.Sub test()

Dim aNode as mwNode aNode = new mwNode(mwNode.kBranch, "Root") aNode.Sequence.appendChild(new mwNode(mwNode.kBranch, "First Child")) aNode.Sequence.appendChild(new mwNode(mwNode.kBranch, "Second Child")) aNode.Sequence.appendChild(new mwNode(mwNode.kBranch, "Third Child")) aNode.Sequence.appendChild(new mwNode(mwNode.kLeaf, "Fourth Child")) aNode.Sequence.appendChild(new mwNode(mwNode.kLeaf, "Fifth Child")) aNode.Sequence.getChild(1).Sequence.AppendChild(new mwNode(mwNode.kLeaf, "Sixth Child")) view(aNode)

HTMLViewer Control

The HTMLViewer Control has already been encountered in Chapter 2, and I will be using it again in the RSSReader application. It was simple enough to use in the first example, and it's going to be even simpler now because I will not write any code for itI will just use the LoadPage method to view the HTML page because I will use the HTTPSocket control to actually download the page. I do this because in the next chapter, I will be processing the RSS XML file prior to viewing it in the HTMLViewer. Because XML isn't covered until the next chapter, I'll defer further discussion of that step until then.

HTMLViewer is an asynchronous control. This means that when you execute the LoadPage URL, your program doesn't have to wait until the page is returned before it can move on to other things. After the function is called, it cycles through the other events and does whatever it's supposed to be doing. When it begins to download the document, the DocumentBegin event is triggered, and when the page is returned and ready to be displayed, the DocumentComplete event is fired.

The DocumentProgressChanged event tracks the progress of the page download. You provide the URL and an integer that represents the percentage downloaded (as an Integer, so 50% is expressed "50"). An example of using this event with a ProgressBar is given in the forthcoming section named "ProgressBar."

HTTPSocket Control

HTTPSocket is a networking Control. I devote a chapter to networking, so you can expect to see more detail there. Still, the RSSReader needs to make an HTTP connection, so I will introduce it now. As I said earlier, I use the HTTPSocket control to grab an RSS XML file. In the sample code I'll provide now, the HTTPSocket control grabs HTML and displays it directly.

HTTPSocket works much like HTMLView because it is asynchronous as well.

You cannot drag an HTTPSocket directly to a window, because it is not listed in the list of Controls. Instead, drag the TCPSocket superclass to the Window and there, select it and manually change the Super value in the Properties pane to HTTPSocket.

Calling it is as easy as this:

Dim f as FolderItem f = getFolderItem("MyRSS.tmp") TCPSocket1.Get(me.Cell(row,column),f)

As you can see, I passed a FolderItem to the socket when calling the Get method. This is a FolderItem that I just created using the getFolderItem method and which will be created in the same folder as my application. I pass the FolderItem now because it will be that FolderItem that I access after the page download is completed.

When the page has been returned, the DownloadComplete event is fired:

[View full width]

Sub DownloadComplete(url as String,HTTPStatus as Integer, headers as InternetHeaders, file as FolderItem) Dim tis as TextInputStream // Load the page in the HTML Viewer HTMLViewer1.LoadPage(file) // Load the source text in the EditField. tis = file.OpenAsTextFile EditField2.Text = tis.ReadAll End Sub

The FolderItem you originally sent with the request is now returned to you, and it contains the data that was downloaded. Because HTMLViewers can load pages directly from FolderItems, I pass a reference to the Folder to the HTMLViewer, extract the text of the file, and display it in an EditField. This is the only way (that I can think of, at least) to get access to the text of a page that is loaded into HTMLViewer. The HTMLViewer does not offer access to the document that it is displaying, so if I want a copy, I have to retrieve it myself. Note that if the user follows a link on the HTML page displayed in the HTMLViewer, the new page will load, but you won't be able to get that text. Because this application is dealing with RSS files, that's okay. The only thing we need a reference to is the original RSS file so that we can cache it. If the user follows links from the page that is displayed, the program doesn't need to have a copy of the text of what is returned.

ProgressBar

The ProgressBar has two values to be concerned with. The first is Maximum and the second is Value. Value is the number that represents how much of the processing task has been accomplished. When Value is equal to Maximum, the task is completed. See an example in Figure 5.21.

Figure 5.21. ProgressBar.

Some classes provide data expressly for the purpose of making it easier to work with ProgressBars. The challenge is that you need to know how much work need to be done to have a ProgressBar that shows a smooth progression from start to finish. The HTMLView control and the HTTPSocket both have events that can be used to update a ProgressBar.

First, I'll show you how it works in an HTMLView:

Sub DocumentProgressChanged(URL as String, percentageComplete as Integer) ProgressBar1.Maximum = 100 ProgressBar1.value = percentageComplete End Sub

This event passes the URL of the document in question, as well as a number that represents the percentage complete. Note that the data type is an integer and the value that it passes is not a percentage, but a number between 0 and 100. I set the Maximum value on the ProgressBar to 100 and then update the Value property of the ProgressBar each time the event is called. In this example, I'm being quite inefficient because I am setting the Maximum value each time the event is triggered. This makes for a simpler example, but ideally you would set the Maximum value only once.

The HTTPSocket takes a different approach:

Sub ReceiveProgress(bytesReceived as Integer, totalBytes as Integer, newData as String) ProgressBar1.Maximum = totalBytes ProgressBar1.Value = bytesReceived End Sub

Instead of passing a percentage, it passes the number of bytes received plus the total number of bytes that are expected. The ProgressBar1.Maximum value is limited to 64K, so you cannot place extremely large numbers as the Maximum value. The simple solution is to set the Maximum value to 100 and then calculate the percentage of bytes received; then multiply by 100 and use this for the Value. This is not a perfect solution because it is dependent on the HTTP server returning a valid number for totalBytes, which it sometimes doesn't, sending a value of "0" instead. If that's the case, set Maximum to "0" and you will get an indicator on ProgressBar1 that indicates it is a process of indeterminate length. The actual appearance of the ProgressBar1 at this point is determined by the platform.

Категории