Playing With the GUI in Visual FoxPro 8: Prototyping an MS Visio Template UI 

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.

Prototyping an MS Visio Template UI

Predrag Bosnic

While Visio is part of the Microsoft suite of applications, its user interface has some unique features, one of which is the way that groups of templates are presented to the user. In this article, Predrag Bosnic explores how to mimic this useful and intuitive interface mechanism in Visual FoxPro.

Working with Microsoft applications through the years, I became familiar with the menu system, toolbars, icons, and so on. Of course, Microsoft did a great job of standardizing its applications, introducing a common standard for GUIs, and today it's nearly impossible to imagine working with Office if every application had its own style for menus or toolbars, and different icons for the most common actions. Not such a long time ago, Microsoft bought Visio and added it to the application palette. Visio adopted all of these standards, but managed to keep its own specific look and feel. The way Visio exposes its UI is very specific, somehow different, and this gives it a specific dimension. This time out, I'll concentrate on something all Office applications have, the Task Pane—but specific to Visio, I'm interested only in the left side of the pane, which we use for choosing a template (see Figure 1). In my opinion, this is a very good example of a GUI that can make an application stand out from the masses. The users will love using it—its imaginative simplicity is probably the formula of its success.

Prototype

About two years ago, I decided to try to create an MS Visio template UI for VFP and eventually use it in one of my applications. If there's one thing I like to do, it's prototyping user controls and user interfaces. Prototypes are well known projects in any kind of engineering, and everyone probably knows about prototypes for cars and airplanes. A short and simple definition of a prototype could be: "A real model of our product, and during its development, and through it, we answer all of the technical and technological questions we had at the beginning of the project." After the development of the prototype, there shouldn't be any unknowns. Often people say a prototype is a full working model of a product. This isn't true because we don't have to achieve full functionality inside the prototype project. We use prototyping on the part of the product where we expect to apply very specific (and often unknown) technical and technological solutions.

When we decide to do prototyping of a small product, our prototype can easily achieve full functionality and eventually become a product. During prototype work we usually don't care about any rules (although we can if we want), because we want to find the solution to our problem as soon as possible and then create/make the real product applying all rules and standards.

Analysis/design/implementation

As you might have guessed, I started this project as a prototype—expecting to find the answers to all of my questions and then do the interface again, applying all the standards and rules properly. The first few drawings of the interface I created (I usually use a note pad—I mean a real paper note pad, not the computer version) have been combined into Figure 2 (I used MS Visio for this drawing). The interface is capable of showing two-level structures, with the left side listing some kind of categories and the right side showing items for the selected category. Visio's items are templates that we use to start new drawings, but it's very easy to imagine templates for letters, different kinds of reports, and so on. Every category on the left side is represented with a label showing the category name, and an icon showing whether a particular category is selected or not (Visio uses open/close folder icons).

Together, the selected category and the shape on the right side give the impression of a tab paradigm. Moving the mouse over any of the items on the right side will cause a focus rectangle to appear around the item, and Tip-Help will appear in the left corner showing a short explanation about the item. It's possible to resize the whole form and the right part (shape) will exhibit a very specific behavior. Depending on the form's width, this container rearranges the items inside—putting them in the maximum number of columns they can fit in and any number of rows (showing the vertical scrollbar if it's necessary).

Two questions arise: What is unknown for me in this project, and why do I want to make a prototype? First, the visual interpretation of the tab is something I have to create for the first time, and moving the tab from one category to another must be smooth. The second challenge is how to resize the container on the right side and how to manage a vertical scrollbar. If I can answer these two questions, then I'll have the answer for the interface.

The cItem class

The shape of the Visio tab resembles a wing, and all of the classes I use will be in a library called wbWing.vcx. The first class is a category item located on the left side; I call it cItem. Figure 3 shows the class cItem.

The class is a composite control containing a container, a label, and a picture control. The only action is in the Click method of the label control. When users click on the label, the icon is changed to "open," the previously selected item is reset, a "wing" control is moved to the specific position, and the items for the selected category are shown on the right side. The following code is from the Click method of the label control:

  DODEFAULT()
LOCAL xcnt as object

FOR each xcnt in thisform.controls
  IF xCnt.class = 'Citem'
    xCnt._label1.FontBold = .f.
    xCnt._label1.ForeColor = rgb(0,0,255)
    xCnt._image1.picture = "close.bmp"
  endif
ENDFOR 

thisform.cWing1.Top = this.Parent.Top - 3
WITH this
  .ForeColor =0
  .FontBold = .t.
  .Parent._image1.picture = .Parent._image2.picture
ENDWITH 

* reset Wing - ItemBig
thisform.cTip1.visible = .f.
myWingForm.setall('borderWidth',0,'cItemBig')

* Load details (items)
thisform.LoadDetails(this.Parent.idoption)

The control contains a user-defined property, idOption, that explicitly defines the category ID. This ID is used during the call of the LoadDetails method.

The cWing class

The next class is called cWing, and it contains controls that simulate a tab header. This control is also a composite control, which contains a container and two shape controls. The first shape control is a rectangle with rounded corners. The second shape is a rectangle without borders and the same back color as the first shape. Using the second shape control, I mask the right side of the first shape—creating an impression like the one in Figure 4. As you can see, the second shape is practically invisible, and I had to select it before I took the screen snapshot in order to show it to you.

Now imagine that this control is always at the front of the big shape control on the right side. The visible part of the cWing control is tuned to correspond to the left border of the big shape control. In this situation, the second shape control in cWing is masking the part of the left border of the big shape control, and an impression of a tab is created. When users click on another category item, the cWing control will be moved vertically to the calculated position, giving the user the impression of the other tab showing. This class doesn't contain any code.

It looks as though my first problem has been solved (if my test confirms the smooth transition).

The cItemBig class

The next class I need is an item on the right side; I called it cItemBig (see Figure 5). Once again, I use a composite control containing a container, an image, and a label control. The image control is 64x64 but could be of any reasonable size, and it contains the picture of the item it represents. The label control obviously contains the item name.

On MouseOver, this control shows a border, giving the visual notification to users that the mouse is on top, and displays any corresponding text in the Tip-Help control in the left corner of the form. On OnClick, this control will execute a user-specified action. The code is simple and isn't worth showing.

The cTip class

This class is used to show the Tip-Help text and is a composite control (what a surprise!). No code is associated with this class. The class contains a shape and two edit text controls for a title and a description (see Figure 6).

The MainForm form

It's time to create an overall container for my controls, and that will be my MainForm form. Figure 7 shows the MainForm form with all of the created controls, excluding cItemBig controls (they'll be added during runtime). Pay attention to the form caption. The form is a top-level form. I find myself using top-level forms more and more. This was a good moment to test the movement of my "wing" control, and I can tell you the result was more than satisfying.

Now I have to solve the second problem. The main question is: What is the control on the right side? My first thought was a container control, but it doesn't have a native scrollbar so I'd have to use OCX controls or make my own scroll control. No thanks! What about a grid control? It's a gorgeous control; it can do a lot of amazing things and has a native scrollbar. I don't see how to hide all the unnecessary lines, though (at least a border must be visible). It looks too complicated, if it's even possible. Please, look at Figure 1 again. It's impossible to see any controls apart from the scrollbar control. Maybe it's a good solution to place items on the shape control and then use a vertical scrollbar if not everything can fit in? Again, this is too complicated! Finally, after a few days, I hit upon a new idea—the window! The solution is so simple and obvious—that must be the reason I nearly overlooked it. I can eliminate the title bar and borders, the back color can be the same as the shape back color (meaning it could be invisible), and it can have an automatic scrollbar.

Now it's time to create a form I call WingForm.scx. The following properties are important:

  BorderStyle = No border
ScrollBars = 2 — Vertical
ShowWindow = 1 — In top level form
TitleBar = 0 — Off

The Resize method is responsible for rearranging items correctly on the form, and a scrollbar will appear automatically.

Here's the WingForm Resize method code:

  DODEFAULT()

LOCAL jnCardHeight as Integer
Local jnCardWidth as Integer, jnObject as integer
LOCAL jnOldCol as Integer, jnNoOfCols as Integer
Local jnOldRow as Integer, jnYdelta as Integer
LOCAL jnNoOfRows as Integer, jnDelta as Integer
LOCAL jnObject as integer, jc1 as String
LOCAL jn1 as Integer, jn2 as integer
Local jcCardName as string

thisform.nCardWidth  = thisform.Big1.width
thisform.nCardHeight = thisform.Big1.height

jnCardHeight = thisform.nCardHeight
jnCardWidth  = thisform.nCardWidth

*------------------------------------------
jnObject = 0
jnOldCol = thisform.nColumn
jnNoOfCols = Int(thisform.Width/jnCardWidth)
IF jnNoOfCols = 0
  jnNoOfCols = 1
ENDIF 
thisform.nColumn = jnNoOfCols
*------------------------------------------
jnOldRow = thisform.nRow
jnNoOfRows = Int(thisform.nCardMax/jnNoOfCols)+1
IF jnNoOfRows = 0
  jnNoOfRows = 1
ENDIF 
thisform.nRow = jnNoOfRows
*------------------------------------------

IF jnOldCol <> jnNoOfCols
  jnDelta = 4    && left distance from the wing shape
  jnYdelta = 4   && top distance from the wing shape

     FOR jn2 = 1 TO thisform.nRow
       FOR jn1 = 1 TO thisform.nColumn
         jnObject = jnObject + 1
         IF jnObject > thisform.nCardMax
           exit
         ENDIF 
         jc1 = Alltrim(Str(jnObject))
         jcCardName = 'Big'+jc1
         thisform.&jcCardName..Left = 4
         thisform.&jcCardName..left = (jn1-1)*;
         (jnDelta+jnCardWidth+jnDelta)
         thisform.&jcCardName..top  = (jn2-1)*;
         (jnYdelta+jnCardHeight) + jnYDelta
         NEXT 
  NEXT 
ENDIF

In order to populate WingForm with items, I create and use a metadata table called OptDetails.dbf (see Table 1). Records for this table have to be added manually.

****

Table 1. OptDetails.dbf file structure.

Field name

Type

Width

Description

ID

Integer

4

ID

IDOPTION

Integer

4

Option ID

ITEMNAME

Char

20

Item name

TIPTITLE

Char

15

Tip title

TIPTEXT

Char

64

Tip text

IMAGE

Char

64

Picture filename

PRG

Char

64

Name of the program file

The MainForm's LoadDetails method is responsible for populating WingForm and it contains the following code:

  LPARAMETERS tnIdOption

IF TYPE('myWingForm') = 'O'
  mywingform.release()
  mywingform = .f.
endif
DO FORM "WingForm.scx" NAME myWingForm WITH ;
  thisform noshow
myWingForm.height = thisform.shpWing.Height - 20
myWingForm.width  = thisform.shpWing.width  - 20
myWingForm.top  = thisform.shpWing.Top + 10 
myWingForm.left = thisform.shpWing.Left+ 10
*===============================================
Local jnCounter as Integer
jnCounter = 0
SELECT OptDetails
SCAN FOR idOption = tnIdOption
  jnCounter = jnCounter + 1
  jcBig = 'Big' + ALLTRIM(STR(jnCounter))
  myWingForm.AddObject(jcBig,'cItemBig')
  myWingForm.&jcBig..left = 20
  myWingForm.&jcBig..Top  = 10
  myWingForm.&jcBig.._label1.caption = ;
    ALLTRIM(OptDetails.ItemName)
  myWingForm.&jcBig..cTipTitle = OptDetails.TipTitle
  myWingForm.&jcBig..cTip = OptDetails.TipText
  myWingForm.&jcBig.._IMAGE1.PICTURE = ;
    OptDetails.IMAGE
  myWingForm.&jcBig..cprg = OptDetails.prg
  myWingForm.&jcBig..visible = .t.
ENDSCAN
*=====================
myWingForm.nCardMax = jnCounter
myWingForm.resize()
SHOW WINDOW wbWingForm

The Resize method on the MainForm will change a few properties and the size of the WingForm. That will trigger the Resize method on the WingForm, and items are moved to their appropriate positions.

Here's the Resize method (MainForm):

  IF TYPE('myWingForm') = 'O'
  this.shpWing.Height = thisform.Height - ;
    this.shpWing.Top - 15
  myWingform.height = this.shpWing.Height - 20

  this.shpWing.Width = thisform.Width - ;
    this.shpWing.Left - 15
  myWingform.Width   = this.shpWing.Width - 30

  thisform.cTip1.Top = thisform.Height - ;
    thisform.ctip1.Height - 0

  thisform._line2.Width = thisform.Width - ;
    thisform._line2.left - 12
ENDIF

When the MainForm activates for the first time, I need to populate the WingForm with items corresponding to the first category option. The Activate method is responsible for that:

  IF myActive = .f.
  myActive = .t.
  DODEFAULT()
  LOCAL jnIdOption as integer
  jnIdOption = 1
  thisform.LoadDetails(jnIdOption)
ELSE
  DODEFAULT()
ENDIF

Finally, Figure 8 shows my interface during runtime. The behavior of the interface is satisfactory, and using the window was a good idea.

Improvements

I can say that this prototype has full functionality, but someone can surely improve on it. Please look at the category items on the left side. MS Visio keeps these items in some kind of container that isn't usually visible. But if you extremely downsize a Visio form, a scrollbar will appear on the right side of the category items. In my case, I found this unnecessary because I can easily fit 10-15 items on the form, and—by using the setting for the minimal size of the form—I can prevent users from doing an extreme downsizing of the form.

The metadata table contains only data for subitems. The table could be restructured to contain category items as well, and then the category items could be loaded in the code. I'm sure you'll have a few more ideas.

Conclusion

I have to confess, I was proud of myself when I found the use of a window as a solution. Unfortunately, I quickly realized that I didn't come up with it all on my own. Windows are very native to VFP, and we just get used to using them. What a great tool we have! I hope you'll use this interface in your own applications.

Download 08BOSNSC.ZIP

To find out more about FoxTalk and Pinnacle Publishing, visit their website at http://www.pinpub.com/html/main.isx?sub=57

Note: This is not a Microsoft Corporation website. Microsoft is not responsible for its content.

This article is reproduced from the August 2003 issue of FoxTalk. Copyright 2003, by Pinnacle Publishing, Inc., unless otherwise noted. All rights are reserved. FoxTalk is an independently produced publication of Pinnacle Publishing, Inc. No part of this article may be used or reproduced in any fashion (except in brief quotations used in critical articles and reviews) without prior consent of Pinnacle Publishing, Inc. To contact Pinnacle Publishing, Inc., please call 1-800-493-4867 x4209.