Showing posts with label Complex Controls. Show all posts
Showing posts with label Complex Controls. Show all posts

A Native TreeView (Part V)

Version: 1.01.05 - Last Update: Tuesday the 12th, April 2011 - 11:25:00
Previous ChapterComplex Controls Home (TOC)Next Chapter


FoxPro Rocks! This is all about how to create complex controls using native VFP only!

Intro

After all, the TreeShow must go on now…

200px-CC-logo.svgLast year I decided to publish some VFP demo application without source code you still can find OffSiteLink here.  The only reason for not supplying you with the VFP source code was, I dropped those fully functional sources somewhere (on an old memory stick) and didn’t remember exactly where I dropped that one… As I started working on a new (improved!) version of my native VFP-TreeView, I only was able to recover the compiled demo app which is using an outdated driver table structure and many more odd things. Well, in between I decided not to create a PRO-version (available only for a fee) but publish my final release under a Creative Commons License. I hope you will enjoy it!

The long and winding road…

During the last years I worked on some more or less challenging projects. One of those was (and still is) my implementation of the (one and only?) Native VFP-TreeView. I’m sorry but I have to tell you (again) that there is no “final thing” at the end of this post waiting for you – nope, still no final download is available! Instead, we have to talk about some features and (interesting?) aspects of my final implementation, first. Believe me, I’m going to show you some really cool stuff in the next chapters. I am sure you will benefit from some tips!

Back to school!

After I went back to the drawing board I started thinking about how to approach the pending performance optimization issues. As I told you, those days the number one reason to rethink my first approach was the lack of speed when working with large datasets; the bigger the record count of the driving cursor, the slower most of the collapse/expand actions became. Another bottleneck was the complexity of the tree item class. The more features I implemented there, the more time was wasted during refresh/repaint for jumping through endless IF…ELSE…ENDIF and other DO CASE constructs just to find out which part of the object had to be made visible or hidden or re-colored and so on. Finally, I got stuck with a wonderful implementation of a cool looking tree item class (with dozens of features) that ate up too much refresh-cycle time.
More than ten visible TreeView items rendered my good looking tree useless. At that point in time I knew two things: 1st., It was possible to build a native VFP TreeView and 2nd.: it was abundantly clear that some extraordinary speed optimizations had to be applied to the key parts of the design.

Let’s examine the “roots”!

My initial idea to use a VFP GRID class as the basic container vehicle, I didn’t want to discard. Thus, I couldn’t discard using a driving cursor, either. The driving table/cursor was the logical starting point of my speed optimization efforts, then. If you read OffSiteLink Part IV-b of this thread, you should have seen my old driving table structure which was pretty complex. The tree cursor got more and more columns over times, but the first 5 columns of my driving table’s structure listing in Part IV-b dealt with the hierarchical aspects of tree display.
Well, we all know how to setup and store parent-child relationships of hierarchical related data in a dbf table, don’t we? I’m sure, you recognize the fields of my driving cursor immediately: pNodeID and pParentID hold the primary and foreign key values implementing that parent-child aspect.
DYNA TreeView Driving Table  
The entries shown in the browse window above correspond to the screenshot of my demo TreeView in Part VI-b below.
DYNA TreeView display
As long as we are using a Microsoft ActiveX TreeView control storing the relationship within two columns (one holding the primary key and the other holding the grouping/parental foreign key) is sufficient. There are several ways to reload the parent-child information of such a cursor back into an ActiveX TreeView: you may want to use a couple of SQL-Select statements within a recursive SCAN loop to select all sets of child entries to fill up your TreeView. Again, there are other ways to refill an OLE-TreeView, it is up to you to decide what approach fits best within your environment!
Now(!) Try to create a native VFP index that can be employed with SET ORDER TO to display the node records (shown above in the browse window screenshot) in hierarchical sorted sequence like in the screenshot below (where I SET OREDER TO ‘piorder’)!
Hierarchical sorted items
This should be the resulting browse window after your “very special” - index is active
Any ideas? I’m waiting!
I’m waiting!
I’m still waiting!
Again, I’m still waiting!
Right, you are! Without having a single sort order column containing sorting expressions in the correct hierarchical sequence there is no easy trick to solve our dilemma! The following source code fragment shows my old implementation to rebuild the Piorder column values of my driver table:
*\\ Rebuild_SortOrder() gets called from .RebuildMetaData() or recursively(!)
*\\
LPARAMETERS tcParentID As String, tiOrderNo As Integer @
   *\\      tcParentID :== id of parent node for which all children are processed now
   *\\      tiOrderNo :== next Piorder value to be set (passed in by reference!)
IF this.lReadOnlyMetadata
   RETURN
ENDIF
*//
*[BS]: NOTE>> No parameter checking any more coz we'll have done that
*\\ already in interface method "this.RebuildMetaData()"!
*\\
LOCAL lnRecNo AS Integer ,;
      lnOldRecNo AS Integer ,;
      liIndent AS Integer
*//
*\\ remember from where we started
lnOldRecNo = RECNO()
*//
*\\ Set OrderNo for current record
REPLACE PiOrder WITH m.tiOrderNo
*//
IF THIS.lSort && sub-sort children on PCCAPTION string
   SET ORDER TO PCCAPTION
ENDIF
*\\ process all children on the same level (siblings)
SCAN ALL FOR Pparentid == m.tcParentID
   *\\ increment sort order value
   tiOrderNo = m.tiOrderNo + 1
   REPLACE Piorder WITH m.tiOrderNo
   *//
   *\\ remember record number
   lnRecNo = RECNO()
   IF Pichildcnt > 0
      *\\ drill down recursively
THIS.Rebuild_SortOrder(Pnodeid, @m.tiOrderNo) *// ENDIF GOTO RECORD (m.lnRecNo) *// ENDSCAN IF THIS.lSort SET ORDER TO ENDIF *// GOTO RECORD (m.lnOldRecNo) RETURN

Those days I did not change the cursor structure, but tried to solve the hierarchical-sorting problem using a more or less pragmatic approach. Sure you already can see the weakness of my solution:
Adding new items to the TreeView (into the cursor) results in a, at least partial, rewrite of the Piorder column. The closer to the root element an item is inserted, the more Piorder value REPLACEments have to be done. Typical “CRUD” ( Create, Replace, Update, Delete) operations can get really sluggish when applied to large TreeViews (with a lot of nodes > 10.000). On the other hand, my Rebuild_SortOrder() method is the perfect place to implement any additional sub-sorting (optional, I’m sorting all sibling child-nodes based on their caption).

 

All for One, One for All

If we, at least, want to dynamically retrieved each node caption from our driving cursor, then our three musketeers (Pnodeid, Pparentid and Piorder) finally meet D’Artagnan (Pccaption). Thus, my first approach started with a “four column minimum” driving table. Many more supporting columns were added to (1)speed up the TreeView refresh. Other column were added to (2)enhance flexibility at runtime. Columns such as Piindent and Pichildcnt belong to the first group; if we always instantly know how many levels deep a given node has to be “indented” before it can be displayed, this can be a big timesaver compared to always re-compute the indentation level for all tree nodes. Same is true for knowing about the number of children a tree node has. If we know the node’s child count during refresh in advance, this speeds up things a lot, especially, if we know that a given node doesn’t have any children (Pichildcnt == 0).
In contrast to  Microsoft’s ActiveX TreeView control we cannot store additional  information inside each node object itself, because each VFP node object is a single class instance that is shared inside a grid column between all rows (roughly speaking:-). To get closer, let’s eye the checkbox implementation. If we have our TreeView displaying checkboxes before each of it’s nodes, we can query the checked-state of each node object. This true/false value is stored in a property of each node object of an ActiveX-TreeView directly. An ActiveX-TreeView with, let’s say, 1000 nodes has 1000 node objects each capable of storing the checked/unchecked information on its own.
In contrast, our native VFP TreeView has only one real node class instance. The other 1000 nodes of our TreeView grid are virtual class instances only. They only get painted during refresh sharing the one and only real node object as some kind of punching tool for that. Since all node objects are more or less virtual, they must hold their “property” values stored in the fields of the corresponding (1000) rows of the driving cursor to make them persistent. That’s exactly what a VFP grid originally was designed for: display data stored in a cursor (an not property values of object collections :-)
The screenshot below shows some of the additional columns that were added later in the game. BTW: the Pnmark column just stores the checkbox values just discussed. As my native VFP TreeView can have OptionGroups (the famous radio button groups) the Pnmark column accepts values greater then 2 (to be able to hold radio button groups of any member-count).

Extended node properties
Finally I’m going to explain another column “Pllastitem” you can see in the above screenshot. This flag (true/false) is used to simplify the implementation of the tree-lines algorithm a little bit. Below, you see a part of the screenshot my TreeView-Editor TreeView. The so called “Last items” are marked with red circles. The grid rows containing those definitions (see above) show the flag value 1 in their Pllastitem field.
TreeView's "Last items"
A “last item“ is the last node on it’s indentation level. Either, the following node (the one with Piorder+1) is a child node (then that node’s Piindent value is higher), or it is a sibling of the “last item’s node” parent node (then that node’s Piindent value is lower). There are two other extremes: either there is no next node, then our “last item” node is the last node of the whole TreeView (with the highest Piorder value), or it is the first node in the TreeView, a so called root node (with the lowest Piorder value). If the node in question is a real “last item” node, then the vertical tree line of the indentation level ends on that grid row and must only be drawn to the mid of the row. Otherwise, for all other items, the grid line on the level has to be continued down to the next row. Watch the red circled spots to get the clue. Well, how would you implement fast refreshing vertical and horizontal tree lines up to over 30 levels of indentation? Either you drop all these lines like Vista explorer does (see below :-)
Vista explorer without tree lines
… or you have to store the needed information (is the current node the last on it’s own/current indentation level) in a separate column of your driving table. Next you have to figure out when and how to update that information. I leave it to you to check my implementation shown below.
*\\ Rebuild_LastNodeFlag()
*\\
*\\ Note: any ORDER that was SET gets released on exit!
*\\
IF this.lReadOnlyMetadata
   RETURN
ENDIF
*//
LOCAL lcParent AS String ,;
      liIndent As Integer ,;
      lnRecNo  AS Integer

*\\ Important: BLANK ALL PlLastItem metadata column entries first!
*\\          BITTEST(pbit_state,4) := exclude root node(s)
BLANK FIELDS Pllastitem ALL FOR NOT BITTEST(pbit_state,4)
*//
*\\ This gets a little bit tricky now:


SET
ORDER TO PIORDER DESCENDING && TreeView definition now upside-down
GO TOP && we are on the last TreeView node now *\\ Scan from bottom to top!
*\\ ISBLANK() serves as a "virtual" flag field value: “not visited” SCAN ALL FOR ISBLANK(Pllastitem) lnRecNo = RECNO() lcParent = Pparentid liIndent = Piindent *\\ Two simple rules apply here:
*\\ 1st: The last node (the one with the highest Piorder value) of each level of indentation must *\\ be flagged TRUE in Pllastitem field! *\\ 2nd: Each trailing sibling node (all nodes with the same Piindent value and the same Pparentid *\\ value that have a Piorder value less than the current node) can NOT be the "last-node"
*\\ Thus: *\\ Running from bottom to top traversing our metadata cursor processing only BLANKed fields *\\ the first node record we'll find has to be the "last-node" of his *\\ indentation level/(child)group. REPLACE Pllastitem WITH .T. *\\ Now skip from the bottom of the TreeView cursor "up" one line due to DESCENDING ordering SKIP *\\ from there walk up the TreeView definition and replace all sibling nodes’ Pllastitem *\\ field with .F. (coz they all belong to the same group of siblings of our just flagged *\\ “last node” in the code above). Writing .T. or .F. to the cursor clears the BLANK state. *\\ This will exclude the group of sibling nodes just processed from the next SCAN loop REPLACE REST Pllastitem WITH .F. FOR Pparentid == m.lcParent AND Piindent = m.liIndent *\\ finally reposition record pointer (set back to initial position >> last TreeView node) GOTO RECORD (m.lnRecNo) *// That's it. The SCAN now again goes "up" the treedef cursor until it finds another *// BLANK record (group). Otherwise we're done. ENDSCAN *\\ SET ORDER TO

Wherever there is light, there are shadows

After all those days and nights I’ve tried to create an (almost) perfect native VFP TreeView, I can say, that both types (the ActiveX AND the native VFP one) have their pros & cons! I don’t want to repeat all OLE-control related facts I already talked about in the past in detail. Today I want to concentrate on the downsides a VFP Grid-based solution has. To make a long story short:
Displaying & manipulating hierarchical ordered records (based on a one-to-many parent-child relationship) with good performance using a VFP GRID definitely is no straight forward task! Below you see a screenshot of VFP’s document view window with my so called ‘DynaGridController’ class opened in VFP’s class designer. The dynaGridController is the workhorse class “behind the scenes“ that implements (and encapsulate) all basic TreeView engine behavior.

DynaGridController methods
A lot of functionality only is necessary to collapse/expand tree nodes, to indent items correctly, to navigate the TreeView using keystrokes and other stuff. I am sure, the implementation of most of all these ‘odd things’ was much easier for Microsoft while creating their ActiveX version!

 

What comes next

Next time I will present you my own “hierarchical indexing” technique. You can utilize a slightly varied version of it when working with SQL Server 2008. Yep, they got it there, too! But you can only see there how to use it. I will show you how to implement it! The tricky and (copyright) protected part is, that you do not have to use multiple fields to create a TreeView like hierarchical sequence!  Be prepared to learn something new ;-)


<To be continued…>


Previous ChapterComplex Controls Home (TOC)Next Chapter

A Native TreeView (Part IV-b)

Version: 2.00.00 - Last Update: Sunday the 01th, May 2011 - 10:25:00 (I added a more obvious download link:-)

Previous ChapterComplex Controls Home (TOC)Next Chapter


FoxPro Rocks! This is all about how to create complex controls using native VFP only!

Intro

Start download of RightClickMenu_B08.zip here!

Wow, finally! I found the time to finish part IV-b of my "Native TreeView" series - gosh...

I also added a link to the latest downloadable ZIP file containing all beta version sources and resources >>> A new (BETA) version of the right-click menu is available (I spent some time on debugging an enhancing it)

<< Download from there the right-click menu thing

 

Yep! Here it comes: a first (downloadable!)

impression of the all-native VFP TreeView thing :-)

Just click image below!

Click to download gridtree_alpha_01.zip

 

What you get within the gridtree_alpha_01.zip file are some icons that are/can be used to decorate the nodes, the driving table files treeviews.dbf/fpt/cdx and the demo (a VFP 9.0 executable) Sorry, I dropped the sources for that old demo a long time ago – but you’re welcome to recreate them if you have a de-compiler (like ReFox).

The driving table’s structure looks like:

Column Name
Description
pNodeid primary key
pParentid parent key
pCcaption node caption
pIorder sort order (to achieve hierarchical sequence)
pIindent indentation level
pIchildcnt number of child nodes on next level of indentation
pLvisible internal collapsed/expanded marker
pLexpanded controls +/- symbols
pLlastitem <.T.> if the current node is the last one (child node) in a branch
pNmark <1> if node is marked (if option group this is the #pointer# to the selected option node)
pBit_Objs bit field: node layout (not implemented yet)
pBit_Visib bit field: visible node items
pBit_State bit field: node items states
pBit_Lines bit field: tree lines states
pBit_Back bit field: ??
pCimg_Expd expanded node image
pCimg_Coll ?? check it out yourself ;-)
pCicon_Def default node image
pCicon_Sel selected node image
pCicon_Foc focused node image
pCicon_Grp grouped node(s) image
pCicon_Dis disabled node image
pIcaptfore caption fore color
pIcaptback caption back color
pIcaptbold caption bold flag
pCtext sub-caption text
pItextfore sub-caption fore color
pItextback sub-caption back color
pLtextbold sub-caption bold flag
pMhelp help text (not implemented)
pMerror error text (not implemented)

Some of these settings are not fully implemented in this alpha version. Thus, don’t get confused if not all features are working. You may try to right click on the tree nodes to get some additional (re-)actions :-) Please keep in mind that my latest version doesn’t use a VFP TreeView grid with driving cursor complex like the given one! This solution was one of the first to test if it is possible to create a VFP-native TreeView, at all! To prove that I’m using nothing but a regular VFP GRID internally, try to locate the grid’s row-height resizing area (in the upper left corner of the grid area) and then resize the grid’s row height >>>

 

Start download of gridtree_alpha_01.zip here!

<< Download from there the native VFP TreeView thing!

 

 

 

For those of you who want even more background information – here is the listing of a little helper routine I used during initial development to recreate the diverse indices of the driving table. As you can see there are many of them to speed things up (for large tables) using VFP’s rushmore technology. You may read my short remarks in the code fragment to get an idea for what the indices/fields are used, then.

 

WAIT WINDOW NOWAIT "Deleting all CDX tags"
DELETE TAG ALL
*\\
*\\ Transferring the flag bits from pBit_Visib and pBit_State to pBit_Objs
*//
*\\ NODE_IS_ENABLED
WAIT WINDOW NOWAIT "Processing : NODE_IS_ENABLED"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 0) FOR BITTEST(pBit_State,0)
*//
*\\ NODE_IS_FOCUSED (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_IS_FOCUSED"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 1)
*//
*\\ NODE_IS_MARKED
WAIT WINDOW NOWAIT "Processing : NODE_IS_MARKED"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 2) FOR BITTEST(pBit_State,1)
*//
*\\ NODE_IS_ROOTNODE
WAIT WINDOW NOWAIT "Processing : NODE_IS_ROOTNODE"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 3) FOR BITTEST(pBit_State,4)
*//
*\\ NODE_IS_OPTIONGROUP
WAIT WINDOW NOWAIT "Processing : NODE_IS_OPTIONGROUP"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 4) FOR BITTEST(pBit_State,2)
*//
*\\ NODE_HAS_ROOTLINES (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_ROOTLINES "
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 5)
*//
*\\ NODE_HAS_LINES
WAIT WINDOW NOWAIT "Processing : NODE_HAS_LINES"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 6) FOR BITTEST(pBit_Visib,0)
*//
*\\ NODE_HAS_EXPANDER
WAIT WINDOW NOWAIT "Processing : NODE_HAS_EXPANDER"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 7) FOR BITTEST(pBit_Visib,1)
*//
*\\ NODE_HAS_ICON
WAIT WINDOW NOWAIT "Processing : NODE_HAS_ICON"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 8) FOR BITTEST(pBit_Visib,2)
*//
*\\ NODE_HAS_DYNAMIC_ICON (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_DYNAMIC_ICON"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 9)
*//
*\\ NODE_HAS_CHECKBOX
WAIT WINDOW NOWAIT "Processing : NODE_HAS_CHECKBOX"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 10) FOR BITTEST(pBit_Visib,3)
*//
*\\ NODE_HAS_OPTIONBUTTON
WAIT WINDOW NOWAIT "Processing : NODE_HAS_OPTIONBUTTON"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 11) FOR BITTEST(pBit_Visib,4)
*//
*\\ NODE_HAS_CAPTION
WAIT WINDOW NOWAIT "Processing : NODE_HAS_CAPTION"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 12) FOR BITTEST(pBit_Visib,5)
*//
*\\ NODE_HAS_DYNAMIC_CAPTION (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_DYNAMIC_CAPTION"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 13)
*//
*\\ NODE_HAS_SUBTEXT
WAIT WINDOW NOWAIT "Processing : NODE_HAS_SUBTEXT"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 14) FOR BITTEST(pBit_Visib,6)
*//
*\\ NODE_HAS_DYNAMIC_SUBTEXT (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_DYNAMIC_SUBTEXT"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 15)
*//
*\\ NODE_HAS_OVERLAY
WAIT WINDOW NOWAIT "Processing : NODE_HAS_OVERLAY"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 16) FOR BITTEST(pBit_Visib,7)
*//
*\\ NODE_HAS_DYNAMIC_OVERLAY (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_DYNAMIC_OVERLAY"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 17)
*//
*\\ NODE_HAS_HELPICON
WAIT WINDOW NOWAIT "Processing : NODE_HAS_HELPICON"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 18) FOR BITTEST(pBit_Visib,8)
*//
*\\ NODE_HAS_DYNAMIC_HELPICON (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_DYNAMIC_HELPICON"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 19)
*//
*\\ NODE_HAS_ERRORICON
WAIT WINDOW NOWAIT "Processing : NODE_HAS_ERRORICON"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 20) FOR BITTEST(pBit_Visib,9)
*//
*\\ NODE_HAS_DYNAMIC_ERRORICON (nur löschen)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_DYNAMIC_ERRORICON"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 21)
*//
*\\ NODE_HAS_BACKGROUND
WAIT WINDOW NOWAIT "Processing : NODE_HAS_BACKGROUND"
REPLACE ALL pbit_objs WITH BITSET(pbit_objs, 22) FOR BITTEST(pBit_Visib,10)
*//
*\\ NODE_HAS_DYNAMIC_BACKGROUND (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_DYNAMIC_BACKGROUND"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 23)
*//
*\\ NODE_HAS_EXTENSION (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_EXTENSION"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 24)
*//
*\\ NODE_HAS_DYNAMIC_EXTENSION (nur löschen - only deleting)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_DYNAMIC_EXTENSION"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 25)
*//
*\\ NODE_HAS_USER (nur löschen)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_USER"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 26)
*//
*\\ NODE_HAS_DYNAMIC_USER (nur löschen)
WAIT WINDOW NOWAIT "Processing : NODE_HAS_DYNAMIC_USER"
REPLACE ALL pbit_objs WITH BITCLEAR(pbit_objs, 27)
*//
*\\ (re)creating indices
WAIT WINDOW NOWAIT "Deleting all CDX tags"
DELETE TAG ALL
WAIT WINDOW NOWAIT "Flushing dbf to disk"
FLUSH force
*//
*\\ master indices
WAIT WINDOW NOWAIT "Indexing on : PNODEID"
INDEX ON PNODEID	TAG "PNODEID"
*//
WAIT WINDOW NOWAIT "Indexing on : PPARENTID"
INDEX ON PPARENTID	TAG "PPARENTID"
*//
WAIT WINDOW NOWAIT "Indexing on : PCCAPTION"
INDEX ON PCCAPTION	TAG "PCCAPTION"
*//
WAIT WINDOW NOWAIT "Indexing on : PIORDER"
INDEX ON PIORDER	TAG "PIORDER"
*//
WAIT WINDOW NOWAIT "Indexing on : PIORDER"
INDEX ON PIORDER	TAG "PIORDERVIS" FOR PLVISIBLE
*//
WAIT WINDOW NOWAIT "Indexing on : PIINDENT"
INDEX ON PIINDENT	TAG "PIINDENT"
*//
WAIT WINDOW NOWAIT "Indexing on : PICHILDCNT"
INDEX ON PICHILDCNT	TAG "PICHILDCNT"
*//
WAIT WINDOW NOWAIT "Indexing on : PLVISIBLE"
INDEX ON PLVISIBLE	TAG "PLVISIBLE"
*//
WAIT WINDOW NOWAIT "Indexing on : PLEXPANDED"
INDEX ON PLEXPANDED	TAG "PLEXPANDED"
*//
WAIT WINDOW NOWAIT "Indexing on : PLLASTITEM"
INDEX ON PLLASTITEM	TAG "PLLASTITEM"
*//
WAIT WINDOW NOWAIT "Indexing on : PNMARK"
INDEX ON PNMARK		TAG "PNMARK"
*//
WAIT WINDOW NOWAIT "Indexing on : PBIT_OBJS"
INDEX ON PBIT_OBJS	TAG "PBIT_OBJS"
*//
*\\ --------------------------------------------------
*\\ lookup indices (rushmore tags)
WAIT WINDOW NOWAIT "Indexing on : DELETED()"
INDEX ON DELETED() TAG "_DEL" BINARY
*\\
SET ESCAPE ON
*//
LOCAL lnLoop, lcIdxText
FOR m.lnLoop = 0 TO 27
	lcIdxText = "BITTEST(pbit_objs, " + ;
				TRANSFORM(m.lnLoop) + ") TAG 'PBITOBJS" + ;
				PADL(m.lnLoop,2,'0')+"' BINARY"
	WAIT WINDOW NOWAIT m.lcIdxText
	INDEX ON &lcIdxText
	WAIT clear
NEXT

FOR m.lnLoop = 0 TO 7
	lcIdxText = "BITTEST(pbit_visib, " + ;
				TRANSFORM(m.lnLoop) + ") TAG 'PBITVISI" + ;
				PADL(m.lnLoop,2,'0')+"' BINARY"
	WAIT WINDOW NOWAIT m.lcIdxText
	INDEX ON &lcIdxText
	WAIT clear
NEXT

FOR m.lnLoop = 0 TO 7
	lcIdxText = "BITTEST(pbit_state, " + ;
	TRANSFORM(m.lnLoop) + ") TAG 'PBITSTAT" + ;
	PADL(m.lnLoop,2,'0')+"' BINARY"
	WAIT WINDOW NOWAIT m.lcIdxText
	INDEX ON &lcIdxText
	WAIT clear
NEXT

WAIT WINDOW NOWAIT "Flushing DBF to disk"
FLUSH force
RETURN


*\\ Just to remember: this is how we can compute the different node layouts
*\\		work in progress: still has to be refined!!!
SELECT DISTINCT pbit_objs, CAST(NULL AS Integer) FROM DBF(ALIAS()) INTO ARRAY atest
ACTIVATE SCREEN
clear
FOR lnLoop = 1 TO ALEN(atest,1)
	? atest[m.lnloop,1],atest[m.lnloop,2]
NEXT
WAIT clear

Note:

My reason for introducing a field called “pBit_Objs” in the driving table and transferring/merging the values of the “pBit_Visib” and “pBit_State” fields into it was to get a list of possible value-combinations (my so-called “layouts”) each representing a special kind of node appearance. This is how my latest VFP TreeView version works: These nodes are prefabricated and stored in classes, then. Thus, no time-consuming code-based re-constructions are necessary during grid refresh-cycles at runtime, coz all different/distinct node objects are loaded once at grid-startup and are referenced then using the DynamicCurrentControl property of the grid’s Column object. Only the most variant entries (like node captions and maybe sub-texts and icons) still are flexibly stored in the underlying driver table. This makes a lot of sense and speeds up VFP’s grid-based TreeView performance substantially!

The only drawback of that approach is that we have to construct our TreeView nodes in advance. Luckily, for most of our scenarios this is no big deal. I’m just working on some kind of node-designer tool, that can be used within VFP’s IDE or during runtime to construct the more ‘outlandish’ looking node types :-) But this will be the topic of another (future) part of this never-ending thread :-)


Previous ChapterComplex Controls Home (TOC)Next Chapter

A Native TreeView (Part IV-a)

Version: 1.00.10 - Last Update: Tuesday the 29th, September 2009 - 11:20:00

Previous ChapterComplex Controls Home (TOC)Next Chapter


FoxPro Rocks! This is all about how to create complex controls using native VFP only!

Intro

In part three we explored a couple of Grid related oddities and created some workarounds. We talked about the different types of item objects our Node will be made of and the static and dynamic aspects of the underlying item classes. We talked about the states a Node can have and setup a list of these. Finally we realized a working font selection Grid list with almost no coding!

Today we will extend our driver table and create our Node's item classes. After that I will show you how to create some cool looking resources like gif images with alpha transparency and how to use them the right way. Finally we will start assembling our shortcut menu. Figure #1 below shows the result (not that bad!:-).

Figure#1: Let’s go!

The little demo above (without audio, sorry) shows a first prototype implementation running our shortcut menu hooked onto VFP's _SCREEN. I created a CUSTOM class-based "click monitor" class that you can instantiate like so:

oClickmoitor1 = NEWOBJECT("clickmonitor", ;
                "dynashortcut.vcx")

After that, right-clicking VFP's desktop will launch the default menu.

To get this demo up and running you have to download the sources from here: Alpha-Version of ShortcutMenu.Zip first. Then unzip the archive file preserving the directory structures! Next step is very important: Within VFP go to your Task Pane Manager and create a new environment (via the "Manage Environments" link on top of the pane). It is important that you set the default directory and (at least) two search paths to point to the proper locations on your hard drive.
The default directory should point to the ShortCutMenu's ROOT directory (that's where the STYLES.DBF and SHORTCUT.DBF and the PJX are located). One search path should point to the RESOURCE sub-directory and the other should point to the SOURCE sub directory like shown in the screen shot below.

Environment_Setup_Alpha

Figure #2: Don't get confused by my wacky "AAAAA"-naming convention :-) Its just my own "special" way to get important projects sorted starting at the top of the list.

Of course you can add other search paths if you need them later on. I like to include one to my VFP's home directory in almost every environment I'm setting up. In addition to that it is a good idea to also include the PJX reference (on the "Associated Projects" tab), and a one-liner on the "Environment Settings" tab in the "Run script after env... is loaded:" textbox like
       _INCLUDE =  "<your path>\dynashortcut.h".
New classes you're adding during development always will have the right H-file reference then.

Well, now you should be able to run the "ShortCutHost" Form from the DynaShortCut project. Try the different right-clicks like Simple-RightClick, Shift-RightClick and Ctrl-RightClick. If you get an error, you should re-check your search path settings! If everything went fine, you can try to launch the desktop right click monitor from your command window like so:
oCMon = NEWOBJ("clickmonitor","dynashortcut.vcx").

But hey, WAIT !!

As I've always told you: this is an Alpha-Version - maybe even more: some kind of pre-alpha version :-) There are a lot of things still under construction and the rest may change very soon. So, if you are looking through my code, always keep in mind that most of the things that look bizarre are there just for testing! Never forget: <This is work in progress to be continued...>

In the last part (Part V) of this Shortcut Menu series I like to talk about all the little oddities I found while trying to get the whole thing up and running and behave like the real thing ;-)

Be prepared - and let me know what you think! Any ideas, comments and suggestions are welcome!


Previous ChapterComplex Controls Home (TOC)Next Chapter

A Native TreeView (Part III)

Version: 1.00.10 - Last Update: Tuesday the 29th, September 2009 - 11:30:00

Previous ChapterComplex Controls Home (TOC)Next Chapter


FoxPro Rocks! This is all about how to create complex controls using native VFP only!

Intro

In part two we talked about some basics and how to apply them; we setup our first Grid using our brand new Node container (showing a Label that was updated dynamically from the underlying cursor). We finally ran our Test-Form and watched the RECNO() output echoed to the screen: Some strange things were going on – something we have to explore in more detail today…

Okay, this is what I found out while playing with our DynaView-Grid (IMHO that’s a pretty well-suited name for it) so far:

  1. Our Node container’s BackStyle property is accessed by VFP always TWICE in a row! Hm, sounds strange but this is something one can find all over the place when assigning _ACCESS() and/or _ASSIGN() methods to native VFP properties. So this seems to be more “by design” than a faulty behavior! Keep in mind that it is a good idea always to check what’s going on internally when applying access/assign hooks to native VFP properties!
  2. VFP re-renders all visible Nodes when changing the active Grid row. More precisely: VFP re-renders all visible Grid cells every time a Grid gets refreshed (if column's sparse = .T.) Now, this is VERY IMPORTANT (we have to remember this when implementing our TreeView solution): There are two directions changing the current Grid row: UP and DOWN. In other words: you can select (move to) a new row “above” or “below” your current active row. Go and try this on your own and carefully watch the record numbers echoed out. You will find that as long as you are moving down (selecting below) VFP will finish its re-rendering stopping on the newly selected record. As long as you are moving up (selecting above) VFP will finish its re-rendering stopping on the old record, which was selected before you started moving the record pointer upwards!

Why are these findings so important for us?

Well, I think the first point doesn’t need any further detailed explanation. There is no need to call our internal refresh hook more than once regardless of how often VFP needs to query the container’s BackStyle value.

The second finding implies two things:

  • Because VFP does not only refresh the two nodes apparently involved while changing the current record, but ALL VISIBLE ones, we have to keep our own refreshing code as compact as possible! Otherwise we’ll get hit by serious performance degradation one fine day.
  • Seen from an end-user point of view, there is a more or less comprehensive list of “living” Nodes displayed within our DynaView-Grid. But this is not a true picture of reality! In fact we only see a nicely painted picture of a Grid! There is only one “living” object inside our DynaView-Grid at a time! This object resides within the current/active Grid row, of course. All other Grid rows(cells) are represented by a picture only!!

Got the key? What do you think will happen if we change the horizontal positions of our embedded Node items within our BackStyle_Access() hook? This will become an issue, when rendering our TreeView, where we definitely have to indent the child items inside our Node container to reflect the hierarchical parent-child relations between our Nodes. Now think about an end-user selecting a new Node above! Got it? YES! We’ll end up with the “living” node object (under our mouse cursor) which was used by VFP to render the image of the old/previous node last time. This "old" Node almost always has a different indentation/layout than the new/current one! In such a scenario the last time our rendering code ran, it repositioned all internal Node items to reflect the outlook of the old Node. And this still is the actual internal state of our container that now is our active Node! You'd have to be mad to cook up an idea like that!

Let’s implement a workaround to fix the doubled BackStyle access. It's a piece of cake:
  • Add a property “nRecNo = 0” to the node container class.
  • Modify your BackStyle_Access() method like this:
WITH THIS
   IF RECNO() # .nRecNo
      .nRecNo = RECNO()
   ELSE
      ? "BackStyle_Access RecNo# =" + TRANSFORM(RECNO())
      .Refresh(.T.)
   ENDIF
ENDWITH
RETURN THIS.BackStyle

That’s all we have to do. Re-run your Test-Form and check the output. Well done! ;-)

Next let’s move the refresh code of our Label’s caption out of the Node container. I’ve put it there to get some fast results in part two of this thread. I think each child item of our Node container should refresh itself self-dependently (in terms of OOP encapsulation). Our Node is responsible for triggering the refresh only, telling its child objects to do so without telling them how to do it!

Modify the Node container’s refresh() method like so:

LPARAMETERS tlBackStyle AS Boolean
THIS.oCaption.Refresh()

Note that this is an intermediate redefinition. We will refine the refreshing behavior of our Node container later in the game again. Okay, now open your Label class and put in the following line of code into the refresh() method:

THIS.Caption = ALLTRIM(pCaption)

That’s it. Save all and re-run your Test-Form to check it and then time has come to release breaks: If you want to comment out the code that echoes out record numbers to VFP’s screen – just do it!

There is another Grid-related oddity we have to talk about next. To understand it, follow my instructions below and add programmatic centering to the Label class inside your Node container.

Open your Label class and update the refresh() method code as follows:

WITH THIS
   .Caption = ALLTRIM(pCaption)
   NOTE: .AutoSize should be set to TRUE
   .Top = INT((.Parent.Height - .Height)/2)
   .Left = INT((.Parent.Width - .Width)/2)
ENDWITH

To visualize the effect, change the label’s BackStyle property to opaque (1) and apply some funny backcolor (like yellow). Then save and close it. Next, open up your Node container class. Apply the same opaque BackStyle to it and set the background color to something complementary like green or red. Save your modifications and re-run the Test-Form. You should see something like shown in figure #1 below.

Figure #1: Our Label isn't centered! WHY?

Even better! Now go and resize the Grid’s column! Change the column width and the row height as well. Nothing happens as you can see in figure #2 shown below:

Figure #2: Column Resizing doesn't get reflected! WHY?

But wait! There is one thing we can say for sure: Our Node container definitely gets resized correctly. Otherwise we wouldn’t see each Grid cell completely flooded with our container’s red background color!

Okay, don’t drive yourself crazy! IMHO THIS IS A BUG! I played with it for a long time. I changed all Column properties that effect orientation without success. Even VFP’s debugger always shows exactly the same WIDTH- and HEIGHT- values for our Node container! They never change! It seems that they are deep-frozen right after object initialization.

Let’s change our Label’s repositioning so we do not have to use it's PARENT dimensions any longer. Modify the refresh() method of your Label class as follows:

WITH THIS
   .Caption = ALLTRIM(pCaption)
   *\\ this won't work
   *** .Top = INT((.Parent.Height - .Height)/2)
   *** .Left = INT((.Parent.Width - .Width)/2)
   *//
   *\\ let's try to get the correct dimensions
   *\\ right from our Grid/Column
   .Top = INT((.Parent.Parent.Parent.RowHeight - .Height)/2)
   .Left = INT((.Parent.Parent.Width - .Width)/2)
ENDWITH

Save this modification and re-run your Test-Form. WOW! Now it works as expected, like shown in figure #3

Figure #3: Gotcha!

Well, I must admit, I’m no big “PARENT” fan. I mean, referencing something outside my object’s boundaries at runtime using “THIS.PARENT.PARENT.PARENT.PARENT…” like above not only is hard to follow, but breaks encapsulation completely! We’ll have to discuss/find a better, more “OOP-ish” solution. One way to accomplish this is implementing an Access() method for both, the HEIGHT and the WIDTH property of our Node container. In this case we can compute the correct Node’s height and width (never touching its frozen internal values) on the fly. BUT, remember what I’ve said about assigning such hooks to NATIVE VFP properties!

  • You should be aware of all the side-effects that can hit you when doing so, like recurring (multiple) VFP-internal queries/assignments.
  • Before you finally decide to use Access/Assign hook methods, you better think twice if there isn’t a leaner solution! Keep in mind: adding an access hook turns your static class field (that’s what a property also is called) into a dynamic/computed one (also called a non parameterized function). If you are in “need for speed” (as we are in our case) you have to search for the perfect balance between code complexity (maintainability) and execution performance. Fortunately there is a pretty straight way that can help you to solve this: Count the calls to your access/assign hooks. Or even better: think first! How often do you need the information and how often does it change between your queries?

For example, if your data changes very often in the background but you only need it every now and then, you’ll better implement the dynamic-query way using an access method. Otherwise there would be an unnecessary overhead updating the static class field (the property) every time the background data changes. On the other hand, if you have to access some (almost static) data frequently, it would be of no use to re-compute the same value over and over again. This is the perfect scenario for querying a property directly.

Let’s apply those considerations:

  • There is no need to compute our Node’s width an height every time we have to know its dimensions coz they won’t change that often, if at all. That’s the best reason for using some static properties to hold the node’s height and width.
  • To bypass any problems that might arise when using access() hooks, let’s use some properties of our own (distinct from the native ones). Let’s call them “nNodeWidth” and “nNodeHeight”. If the Grid’s RowHeight and/or our Column’s Width will change, we can change our new property values on the fly accordingly. Our Node’s child objects (our “Node Items”) will query these two properties exclusively if they have to reposition themselves.

Open your Node container class and add the following properties:


nNodeWidth = 0
nNodeHeight = 0
nLeftOffset = 0

The latter will be used to hold an initial indentation/offset and to initialize a private variable we are going to use for lining up our Node items (more on that in part four).

Finally let’s extend our test cursor.

Up to now there isn’t much we’re able to display within our Node. It is time to change this. Before hacking something in, let’s rest and think about WHAT we would like to display. Figure #4 shows some RightClick menu of my German Visual Studio 2005 version. Don’t get confused by the “Bavarian” captions coz they are irrelevant ;-) Let’s use the screen shot to determine how many different kinds of Node items we should create instead.

Figure #4: Some RightClick Menu

Here comes our Node-Items wish list:

  • Icon
  • Caption
  • Divider line
  • Checkbox (Check mark)
  • Cool looking background on left side
  • Highlighting

Next we have to think about what information can be implemented statically (as a fixed property value of a Node item) and what information should be dynamically read from our cursor at runtime to keep our RightClick menu system as flexible as possible without any remarkable performance loss!

This is the moment we have to think about the different STATEs a Node can have. Fortunately these considerations apply to all kind of DynaView-Grids: Flat Lists (like or menu) as well as Hierarchical TreeViews!

Enumeration of Node States:

  • Disabled
  • Default (enabled)
  • Marked (checked)
  • Selected (hovered)
  • Focused

Wow! This seems to get complicated more and more! Yep, Right! But don’t get lost right now. I promise to you: There will be much more (even better) reasons to feel like so ;-)

I don’t want this post to get much longer. That's why I decided to defer the new complete table structure listing. Today let’s end with some pretty cool thing which also looks very nice. Follow the steps outlined below:

Run the following code fragment after having opened your test table exclusively:

ZAP
ALTER TABLE test ALTER COLUMN pCaption V(100)
= AFONT(allfonts)
FOR lnLoop = 1 TO ALEN(allfonts)
    INSERT INTO test (pCaption) VALUES (allfonts[m.lnLoop])
NEXT

Before running your Test-Form again, remove the ugly color assignments and make all backstyles transparent again. Comment out the code line that centers your caption item horizontally. And now let’s apply our first dynamic formatting.

Modify your Label’s refresh() method until it looks like shown below:

WITH THIS
   *\\ let's see what happens next :-)
   *\\ coz we've chanced the field type to "V" we don't
   *\\ need the ALLTRIM() any longer!
   STORE pCaption TO .FontName, .Caption
   *\\ let's try to get the correct dimensions
   *\\ right from our Grid/Column
   .Top = INT((.Parent.Parent.Parent.RowHeight - .Height)/2)
   *\\ do not center horizontally ATM
   *** .Left = INT((.Parent.Parent.Width - .Width)/2)
ENDWITH

Save it and run your Test-Form! Cool? Yep! No additional lines of code were needed! Show this to some of your neighbor VB.Net developers! ;-))

Figure #5: FoxPro Really Rocks!


Previous ChapterComplex Controls Home (TOC)Next Chapter