White Papers                  Home White Papers  |  Message Board Search Products Purchase | News
 
     

Creating a Statusbar control with VFP 8

 

Last Update: April 20, 2003

 

Code for this article:

http://www.west-wind.com/presentations/wwStatusBar/wwStatusBar.zip

 


Amazon Honor System Click Here to Pay Learn More

 

 

 

 

 

Visual FoxPro 8 offers many new features and opportunities to make life easier. In this article Rick describes how to build a native VFP based status bar that fixes some of the problems found in the Windows Common Control OCX version (MSCOMCTL.OCX) that ships with VFP and other development tools. This article introduces several new VFP 8 features in passing: Collections, the Emtpy object, AddProperty() and BindEvents and provides good view of how to utilize and integrate some of these new features in a useful component.

 

One of the really cool features of VFP 8 is its ability to work with Windows Themes and provide fully themed user interfaces. Some people say that XP themes is nothing more than fancy Window dressing that sucks up CPU cycles and screen real estate, but once you start using themes it's hard to look back on the classic Windows 2000/98 interface and not have it feel archaic. Visual FoxPro 8 now supports fully themed controls for all of its own native controls. Unfortunately, the same is not true of the Common Controls ActiveX controls (MSCOMCTL.OCX) that many of us use to build enhanced user interfaces for our users. Even when using a Manifest file (see sidebar), the various controls like the treeview, listview, statusbar, progressbar and others do not inherit the Windows XP look and feel and instead render in the 'classic' style, which looks a little bit funky when you run them inside of an otherwise themed application.  To me this is most noticeable with the StatusBar control, which gives away a non-XP compliant application immediately.

What's wrong with MSCOMCTL StatusBar?

The StatusBar ActiveX control has and always has had a number problems. The most obvious problem you'll find is that the StatusBar does not properly show the sizing grip even when you enable the sizing grip in the control. Well, it does – sometimes. If you define the control in code and add it to the form and run it in an MDI form inside of the main VFP or another Fox application window, then it works. But as a Top Level Form the sizing grip never shows. Many of us have gotten around this by utilizing an image and embedding it on the statusbar (Figure 1).

 

I use the StatusBar control in almost all of my applications and in many of them it has serious timing problems with form rendering. The result is that the StatusBar often doesn't show up correctly – missing altogether or missing the panels – when the form first loads only to show up correctly after the form is resized for the first time. To work around this funky behavior (which is very inconsistent!) you need to insert several DoEvents and refresh the StatusBar from the Form's Activate event. And even then it sometimes doesn't behave correctly.

Figure 1 – A nice themed VFP application with a StatusBar control that's stuck in Windows Classic mode

 

Now with VFP 8 supporting Windows Themes, the most visible problem is that the StatusBar isn't themed. Figure 1 shows a VFP 8 application running under XP with the nice themed user interface, but a status bar that is stuck in Windows Classic mode, which looks funky and rather unprofessional.

 

It’s too bad we are stuck with a nice XP style interface for our forms and statusbars, treeviews and listviews that are stuck in the ‘classic’ era.

.

Using a VFP based wwStatusBar class

To work around this problem I decided to ditch the ActiveX control and write a new VFP class that simulates a StatusBar using VFP code. The control renders in XP style, in 'classic' style and a modified classic style that mixes XP and classic styles. It doesn't implement all of the functionality of the ActiveX control, but implements most of the important ones in an easy to use container class. Figure 2 shows a VFP application running with the wwStatusBar control in XP themes mode.

 

Figure 2 – A Themes enabled VFP application using the wwStatusBar control for an XP compliant look.

 

Here's how the end result works: The class is implemented as a VFP Container class, which essentially builds a panel collection and then dynamically renders this collection's content into various dynamically added form controls.

 

To use the control you first you drop the wwStatusBar control onto a form. The next step is to define the panels to add which you can add either to the form's or the wwStatusBar Init method. Listing 1 uses the latter.

 

Listing 1 – Adding panels in the wwStatusBar::Init method

*** Must call back to the default Init!

DODEFAULT()

 

loPanel = THIS.AddPanel("Ready",300,.T.,0)

loPanel.Icon = "bmp\webService.gif"

THIS.AddPanel("150 Topic(s)",135,.F.,2)

 

The AddPanel method receives 4 parameters the last two of which are optional: The text, the width, whether you want the width to stretch and the text alignment. The method then returns an instance of a panel object which you can further customize. Note that the third parameter – the stretch value – can only be assigned to a single panel and causes that panel to take up all the remaining space of the status bar. In Figure 2 the first panel springs and resizes to the width of the form while the second panel remains a fixed size.

 

You can also configure the StatusBar overall, by setting the backcolor, font and fontsize which is passed down to the individual objects. In addition, you can set the nStyle property to determine how the bar renders:

 

Style

Description

0

Automatic – Automatically adjusts the style depending on whether themes are active

1

XP Style with Themes enabled

2

Classic Windows Style.

3

Modified Classic Windows Style. Automatic uses this for classic Windows.

 

Figure 3 – Three different modes are available for the wwStatusBar: 1 - XP Style, 2  - Classic Style and 3- Modified Classic.

 

To modify a panel you have two options. You can use the UpdatePanel method:

 

THISFORM.oStatus.Updatepanel(2,"New Text","bmp\classheader.gif")

 

which automatically updates the text and icon (optional) based on the parameters passed. You can also access the Panels collection directly. The Panels collection consists of custom Panel objects which contain the following properties: Text, Width, Spring, Align (same as VFP's Alignment property), Icon. You can manipulate the panel like this:

 

loPanel = THISFORM.oStatus.Panels(2)

loPanel.Text = "New Text"

loPanel.Icon = "bmp\ClassMethod.gif"

loPanel.Width = 300

THISFORM.oStatus.RenderPanels()

 

If you don't modify the size of the Panel you can use the RenderPanel(lnPanel) method which is more efficient. Anytime the size of a panel changes however, the entire status bar must be redrawn by calling RenderPanels().

 

By default the StatusBar can automatically resize itself and stay anchored to the bottom of the form. Note that at Design time the status bar just sits anywhere on the form, but at runtime the Resize() method knows how to automatically resize the StatusBar. If lAutoResize is .T. wwStatusBar uses BindEvent() to hook the parent container's Resize event and automatically resizes when the form is resized. You can override this auto resizing behavior by setting the flag to .F. and manually calling wwStatusBar::Resize() when needed. This may be important if your form's Resize() handles many different items on a form and the when the order of the various resize operations is important.

 

Finally you can implement PanelClick() events that fire when you click on any of the panels. An event will fire with the number of the panel passed to you at which point you can change the text or otherwise manipulate the panel.

 

Collections make all sorts of list based operations much easier to use by providing an intuitive way to manipulate the list.

How does it work?

The wwStatusBar class is based on a container control that hosts several controls that simulate Statusbar operation. It's made of disabled, non-themed, transparent textbox controls and a few images that make up the sizing grips and separators that are placed onto the container in just the right order. To manage the panels the new VFP Collection class is used. The process begins with the AddPanel method which creates a new object to add to the Panels Collection:

 

Listing 2 – The AddPanel method uses dynamic objects to add to the collection

*** wwStatusBar::AddPanel

LPARAMETERS lcText, lnWidth, llSpring, lnAlign

 

IF ISNULL(THIS.Panels )

   THIS.Panels = CREATEOBJECT("Collection")

ENDIF

 

IF EMPTY(lnAlign)

  lnAlign = 0

ENDIF

 

loPanel = CREATEOBJECT("EMPTY")

ADDPROPERTY(loPanel,"Text",lcText)

ADDPROPERTY(loPanel,"Width",lnWidth)

ADDPROPERTY(loPanel,"Spring",llSpring)

ADDPROPERTY(loPanel,"Align",lnALign)

ADDPROPERTY(loPanel,"Icon","")

ADDPROPERTY(loPanel,"Left",0)

 

THIS.Panels.Add(loPanel)

 

RETURN loPanel

 

AddPanel only adds a new item to the Panels Collection, but it doesn't render anything yet. Collections make life much easier when building lists like the Panels here. In VFP 7.0 I might have used an array, which is more work to size and then parse and retrieve values from. With collection the process of adding and retrieving items from the list is much easier.

 

The new ‘Empty’ object and ADDPROPERTY command make it possible to dynamically build clean objects from scratch at runtime.

 

Notice also the new Empty object which creates an object with no properties or methods. It's similar to a SCATTER NAME object we could create in VFP 7 from a table record, except now we can create it directly and with no properties on it at all. The new AddProperty() function then allows you to dynamically add properties to this object. AddProperty() works on any object, but it's specifically designed for Empty and SCATTER NAME objects which don't have AddProperty methods. To match there's a RemoveProperty() function in VFP 8 as well.

 

To actually display the panels in the container the RenderPanels method is called. RenderPanels() walks through the Collection, figures out the total width of the bar and then fits the panels into the available space. RenderPanels() only figures the size and walks through the collection and it delegates the actual drawing to the RenderPanel() method.

 

Listing 3 – The RenderPanel method is the wwStatusBar workhorse method

* wwStatusBar::RenderPanel

LPARAMETERS x, llFirstRender

LOCAL loLabel as TextBox

 

*** Create the panel textbox

loPanel = This.Panels(x)

If Type("THIS.Panel" + Transform(x)) # "O"

   This.AddObject("Panel" + Transform(x),"TextBox")

   loLabel = Evaluate("THIS.Panel" + Transform(x))

   loLabel.Themes = .f.

   loLabel.BackStyle = 0

   loLabel.ReadOnly = .F.

   loLabel.TabStop = .f.

   loLabel.DisabledForeColor = this.Parent.ForeColor

   loLabel.Enabled = .F.

   loLabel.Height = this.Height - 2

 

   IF this.nDisplayMode = 2

      *** 3D Box no shadow must be closer to top

      loLabel.Top = 1

      loLabel.BorderStyle = 1

   ELSE

      loLabel.Top = 4

      loLabel.BorderStyle = 0

   ENDIF

ELSE

   loLabel = Evaluate("THIS.Panel" + Transform(x))

Endif

 

*** Inherit Font from container

loLabel.FontName = This.FontName

loLabel.FontSize = This.FontSize

loLabel.FontBold = This.FontBold

 

loLabel.Value = loPanel.Text

 

IF llFirstRender

  loLabel.Left = THIS.nRenderPosition

ENDIF

 

loLabel.Alignment = loPanel.Align

loLabel.Visible = .T.

lnWidth = loPanel.Width - 2

IF lnWidth < 1

   loLabel.Width = 1

ELSE  

   loLabel.Width = lnWidth

ENDIF

 

*** Store Left value so we can handle clicks

loPanel.Left = loLabel.Left

 

*** Draw Icon into textbox

If !Empty(loPanel.Icon)

   If Type("THIS.PanelIcon" + Transform(x)) # "O"

      This.AddObject("PanelIcon" + Transform(x),"Image")

   Endif

   loIcon = Evaluate("THIS.PanelIcon" + Transform(x))

   loIcon.Picture = loPanel.Icon

   IF llFirstRender

      loIcon.Left = THIS.nRenderPosition + 4

   ENDIF

   loIcon.Height = 16

   loIcon.Width = 16

   loIcon.Top = 5

  

   loIcon.Visible = .T.

   loLabel.Value = "       " + lolabel.value

Endif

 

THIS.nRenderPosition = THIS.nRenderPosition + loLabel.Width + 1

 

*** Paint XP style separator after all but last panel

If llFirstRender AND this.nDisplayMode # 2 AND ;

   x < This.Panels.Count

   If Type("THIS.PanelSep" + Transform(x)) # "O"

      This.AddObject("PanelSep" + Transform(x),"Image")

   Endif

   loImage = Evaluate("THIS.PanelSep" + Transform(x))

   loImage.Left = THIS.nRenderPosition

   loImage.Top = 5

   THIS.nRenderPosition = THIS.nRenderPosition + 2

   loImage.Picture = this.cXPSeparatorPicture

   loImage.Visible = .T.

Endif

 

There's nothing tricky about this method, which only serves to dynamically throw controls on the container and format and size them correctly and in the correct order. First a textbox is put down. Then if an icon is specified it's created at the beginning of the panel and overlaid and the text adjusted to skirt the icon. Finally a panel separator is laid down.

 

When the form or container is resized the status bar should resize with it and the code that handles this resizes the statusbar to fit its container's width and locate itself on the bottom of it:

 

Listing 4 – Resizing and positioning of the wwStatusBar is accomplished automatically with its Resize event.

LOCAL lnOldLockScreen

 

lnOldLockscreen = THISFORM.LockScreen

THISFORM.LockScreen = .T.

 

THIS.Left = 0

THIS.Width = THIS.Parent.Width

THIS.Top = THIS.Parent.Height - THIS.Height

 

IF VARTYPE(THIS.Panels) = "O"

   THIS.RenderPanels()

ENDIF

 

THIS.oThumb.Left = this.Width - THIS.oThumb.Width

THIS.oThumb.Top = this.Height - THIS.oThumb.Height

THIS.oShadow.Width = THIS.Width

 

THISFORM.LockScreen = lnOldLockScreen

 

You can manually call this Resize() method, but by default the StatusBar automatically resizes itself based on the Parent container's resize. I used the new VFP 8 BindEvent() function in the Init() of the control.

 

IF this.lAutoResize

   BINDEVENT(THIS.Parent,"Resize",THIS,"Resize")

ENDIF

 

This simple command tells VFP to monitor the Resize event of the parent container and call the Resize method on the wwStatusBar (THIS). BindEvent() is a very powerful tool to allow user controls to handle events fired by parent controls and Resize is a good example of an event that can be trapped and put to use in child controls without having to write code in the parent container.

 

The new BINDEVENT function makes it possible to build components that can encapsulate parent object manipulation right into the component without requiring the parent to take action.

 

wwStatusBar can figure out automatically whether it's running under Themes or not and render the appropriate view instead. To do so it uses the new SYS(2700) function which returns .T. if Themes are active. The nStyle_Assign method deals with changes to the nStyle value and internally delegates to an nDisplayMode property that contains the real display mode. It uses XP Style for Themes and the modified Classic mode for 'classic' apps which displays the thumb but doesn't use the block panels.

 

wwStatusBar also implements PanelClick events which require a little more work. To do this I originally figured I just use the Click event on the textboxes I used as panels, but unfortunately Click() doesn't fire on disabled controls which I used to keep the controls transparent and inactive. Instead I had to use the control's own MouseDown event which fires and bubbles up from the text controls to the container as shown in Listing 5.

 

Listing 5 – Handling the PanelClick by way of the MouseDown event

* wwStatusBar :: MouseDown

LPARAMETERS nButton, nShift, nXCoord, nYCoord

LOCAL x, loPanel

 

FOR x=1 TO this.Panels.Count

    loPanel = this.Panels(x)

      *** Calculate the offset and compare against panels

    IF loPanel.Left <= nXCoord AND loPanel.Left + loPanel.Width >= nXCoord

       THIS.PanelClick(x)

    ENDIF

ENDFOR

 

This code simply looks through the Panels collection and tries to find the panel that the click occurred in based on the coordinates. If found it passes on the event to the PanelClick method. You can handle the 'event' by overriding the PanelClick method on the control:

 

LPARAMETERS lnPanel

WAIT WINDOW "Here's my panelclick " + TRANSFORM(lnPanel) + CHR(13) + ;

            THIS.Panels(lnPanel).Text

Some limitations

Keep in mind that this is a minimalist implementation that isn't completely event enabled. If you change certain properties of the wwStatusBar object make sure that you always call RenderPanel() or RenderPanels() to refresh the status bar display properly. Anytime sizes of panels change RenderPanels() is required.

 

In design mode the status bar displays as a gray container and it doesn't automatically resize and attach itself to the bottom of a form like the ActiveX control does. The display mode for the control is set to Opaque by default which guarantees that the sizing grips look properly regardless of color scheme or theme used by the user. In fact, most apps I checked (including IE) do not have the status bar follow the Windows color scheme. To get the best look with standard color schemes however Transparent will likely work better, or you can explicitly choose a color for your form that works with any mode. I suggest you play with the different modes and wwStatusBar BackStyle property to see what works best for you.

 

Finally, realize that wwStatusBar has a dependency on the images that are used to render the sizing grips and separators. Figure 4 shows the wwStatusBar with the embedded invisible image controls that hold the 3 required images. I chose to include them in the container to force these images to build into the project so you don't have to ship external files. The default path for these images is in a relative .\BMPS folder of the build directory. Make sure you include these images or you'll end up with broken images.

 

Figure 4 – The Statusbar control in the class designer. Make sure the three images are found and included.

VFP 8 makes it easy

When I set out to create a proper status bar I wasn't planning on making it VFP 8 only. But after getting started I realized right away that collections the new Empty object would make this job a lot easier even though this implementation barely pushes the collection functionality. This really paid off in code reduction and time saved. Some things would have not been easily done or impossible in VFP 7 especially the event binding and the transparent image display of Gifs.

 

I hope you find this status bar control useful. It's minimal and simple and provides the most common functionality in a small package. And best of all it does it with VFP code that you can easily adjust to your needs if necessary.

 

As always, if you have any questions or comments about this article please post them on our message board at: http://www.west-wind.com/wwThreads/default.asp?forum=Code+Magazine.

 

© Rick Strahl, 2003


Amazon Honor System Click Here to Pay Learn More

 

 

 

By Rick Strahl

 

Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Windows Server Products, Visual FoxPro, .NET and Visual Studio. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro and West Wind HTML Help Builder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/ or contact Rick at rstrahl@west-wind.com.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>

<assembly xmlns="urn:schemas-microsoft-com:asm.v1"

  manifestVersion="1.0">

<assemblyIdentity

   version="1.0.0.0"

   processorArchitecture="X86"

   name="Microsoft.Winweb.WebMonitor"

   type="win32"

/>

<description>West Wind Web Monitor</description>

<dependency>

   <dependentAssembly>

     <assemblyIdentity

       type="win32"

       name="Microsoft.Windows.Common-Controls"

       version="6.0.0.0"

       processorArchitecture="X86"

       publicKeyToken="6595b64144ccf1df"

       language="*"

     />

   </dependentAssembly>

</dependency>

</assembly>

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Collections

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Hooking events with BINDEVENT

 

 

 

 

 

 

 

 

 

 

 

 

 

  White Papers                  Home White Papers  |  Message Board Search Products Purchase | News