|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:
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.
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!
loPanel = THIS.AddPanel("Ready",300,.T.,0)
loPanel.Icon = "bmp\webService.gif"
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:
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:
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
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.
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
LPARAMETERS lcText, lnWidth, llSpring, lnAlign
IF ISNULL(THIS.Panels )
THIS.Panels = CREATEOBJECT("Collection")
lnAlign = 0
loPanel = CREATEOBJECT("EMPTY")
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.
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
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
loLabel.Top = 4
loLabel.BorderStyle = 0
loLabel = Evaluate("THIS.Panel" + Transform(x))
*** Inherit Font from container
loLabel.FontName = This.FontName
loLabel.FontSize = This.FontSize
loLabel.FontBold = This.FontBold
loLabel.Value = loPanel.Text
loLabel.Left = THIS.nRenderPosition
loLabel.Alignment = loPanel.Align
loLabel.Visible = .T.
lnWidth = loPanel.Width - 2
IF lnWidth < 1
loLabel.Width = 1
loLabel.Width = lnWidth
*** Store Left value so we can handle clicks
loPanel.Left = loLabel.Left
*** Draw Icon into textbox
If Type("THIS.PanelIcon" + Transform(x)) # "O"
This.AddObject("PanelIcon" + Transform(x),"Image")
loIcon = Evaluate("THIS.PanelIcon" + Transform(x))
loIcon.Picture = loPanel.Icon
loIcon.Left = THIS.nRenderPosition + 4
loIcon.Height = 16
loIcon.Width = 16
loIcon.Top = 5
loIcon.Visible = .T.
loLabel.Value = " " + lolabel.value
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")
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.
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.
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.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.
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.
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 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:
WAIT WINDOW "Here's my panelclick " + TRANSFORM(lnPanel) + CHR(13) + ;
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
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 firstname.lastname@example.org.
Windows portable executable format supports a mechanism called a Manifest file that among other things allows applications to specify specific versions of DLLs that the application is to use. The idea is to work around DLL Hell by allowing specific versions of a DLL to be specified. Manifests can contain version numbers for specific files that make this possible. You can create a manifest file for any EXE file simply by creating a file named the same name as the exe with a .manifest extension. So, wwHelp.exe becomes wwHelp.exe.manifest for example.
Manifests can be used to tell non XP applications to use XP style controls (if they follow certain rules that is) by specifying the use of version 6.0 of the Common Controls library. A manifest file that does this looks like this:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<description>West Wind Web Monitor</description>
This exact approach works and is required for .Net applications for example to be XP aware. VFP 8 is natively XP aware so it doesn't need this tweak for native controls. Unfortunately, the tweak also doesn't fix the problem with Common Controls nor does it work for VFP 7 applications as VFP controls are owner drawn controls rather than ‘windowed’ controls.
New to collections? Collections are somewhat similar to arrays, but in an objectified way. You can use simple methods like Add, Remove and Clear to manipulate collections and unlike arrays you don't have to track the size of the structure yourself. Collections also allow access using indexers which can be either numeric or via key name. It's much nicer to reference an item such loData.Columns("Company"), than using loData.Columns for example. By specifying a key in the Add() method you can greatly simplify access to the collection content. Collections lend themselves well for almost any list based structure that needs to be dynamically built and accessed.
Hooking events with BINDEVENT
BINDEVENT is a new VFP 8 function that allows you to hook events of objects and get notified by way of a method call. BindEvent takes 4 parameters: A source object and event method, and a target object and method handler. The source is the object you're listening for events for while the handler object is the event sink that's notified when the event occurs. Here's how it works: You set up BINDEVENT in your startup code, which can be anytime after the event firing and handling objects are in scope. You tell it which event you want to listen for, and then specify which method should be called in response to this event.
BindEvent is extremely useful to allow blackbox components to encapsulate processing logic that would otherwise require extra setup code in the calling application code.
Firing order for BindEvent can be important and BindEvent includes an nFlags parameter that allows you to select when BindEvents is called in the event firing sequence. The default is before the event fires, but options let you run your code after the event fires and to surpress event binding code if events are directly called as methods. If you use BINDEVENT in a reusable component be sure either to provide a way to specify when the event is hooked or a way to disable the event binding and allowing the code to be manually called.
|White Papers Home | White Papers | Message Board | Search | Products | Purchase | News ||