VB: ActiveX

FTP    ::    ActiveX |- 1 -|  2  |  3  |  4  |  5  |

   
Intro

 

ActiveX was manifested by Microsoft as drop-in and interactive controls, though ActiveX didn't have to be used in that way, in Visual programming languages and so for that Visual BASIC used in Microsoft Office products. One could design a form in some Office product, drop in a text box, or combo box, or - ActiveX control - and use a VBA (acrostic for Visual BASIC for Applications) 'macro' backend to control it. Controls with similar user features were also placed in the updated .NET framework (which is to say that beyond the general purpose, some of the commands would be similar or remain the same).

A few ActiveX controls seem essential for development of sites such as this, when Office products are brought into the process. The 'incremental loading' bar is useful. The spinner control might be useful on a form; click up or down button to increment/decrement a value, or slide out for rapid change. The datagrid control is seen as useful, and now particularly as a page control using .NET, and which 'grid' is just a table of labels and values. But two others are really essential. The first handles the hierarchies of the website itself, including that to generate/compile pages, local linking, external, and all the content of each page. It's the treeview control, gone though subtle incarnations (as all Microsoft libraries are incrementally changed with new versions). It's essentially the collapsible 'directory'/hierarchy view one gets in the Windows File Explorer, when one looks through files and directories. The other is one that ships with the Internet Explorer browers. And that's an editable HTML browser page (not quite FrontPage, but pretty complete HTML editing and output, nonetheless).

 
Treeview Control

 

Of these ActiveX controls, and in the context here, the treeview control is the most essential. So it's a good place to begin. One can imagine various data, easily and intuitively, as a sort of table of contents - a hierarchy. Such would be the the organizational levels of a website itself. Such would be the order in which information is presented even on a page like this, or page by page in a book. There are a host of web pages now describing the intricacies of this treeview control. Run a google today, and you'll find nothing but treeview tutorials, going back to the late 90s versions, and up to the latest .NET incarnation. But this following will concern the ActiveX treeview controlled by the 'classic VB', the 5/6 versions of Visual BASIC, and specifically VBA found as the programming/macro language in Microsoft Office products.

In a program like Access, in something like Access 2000 (which like Access97, I'm sure is still used by many), one can create a new form. And in addition to dropping in a textbox and whatever else, one may also drop-in a treeview control. A few things. First, you need permission to drop/insert a new treeview. ActiveX controls required permissions (I suppose the idea was that third parties would produce a host of such controls, and then license and sell them). I believe Office Pro of that era came with permission, at least for this control. I'm not sure. If you try to insert one of these treeviews and get some 'permission denied' message, you might try to download another sample database from the internet which does use the control, open the database, and just copy the control and paste to your new form. It might work. Second, the source library should already be established. But just to make sure, again in Access as the example, one can open a module for programming and check, Tools, References, from the menu bar. There should be a check by an entry for - Microsoft Windows Common Controls 6.0 (SP6) - or similar, pointing to the file - C:\Windows\System\MSCOMCTL.OCX . That's generally the means of getting it, even if the actual 'com' control resides in another library altogether, with this ocx containing merely a cover/wrapper. And so back at the form's design, looking from, Insert, ActiveX Control, there should appear as one of the controls - Microsoft Treeview Control 6.0 . Click on that, and the box will appear on the form. Its class should read - MSComctlLib.TreeCtrl.2 . Ready to begin.

 
Treeview - initialization

 

I don't know if there is a standard prefix for referring to a treeview control on a form. I use the letters - tvw. Since it's the only treeview on this example form, we'll just call it - tvw. So we save, name and close the form. But to open it as just a barebones treeview, some things need to be initialized. Here's just one example of such:

 
Public Sub Form_Load()
 
    Dim rst As DAO.Recordset
    Dim objtree As TreeView, strSearch As String
 
    Set rst = fGetThisDB.OpenRecordset("tblSelfRefData", dbOpenDynaset, dbReadOnly)
 
    Set objtree = Me!tvw.Object
    With objtree
        .Font.Name = "Arial"
        .Font.Size = 8
        .Font.Weight = 600
    End With
 
    strSearch = "lngProjID=" & lngProjID & " And lngSupID=0"
    sFillTvw objtree, rst, strSearch, True
 
     If Not rst Is Nothing Then
         rst.Close
         Set rst = Nothing
     End If
 
End Sub
 

 
 

In this example, the data for the treeview is kept in a table called, tblSelfRefData. As noted above, this goes to the idea that while it isn't necessary, one may keep such hierarchical data in a self-referential table (though some might discourage the practice). It assumes, minimally, that there is a unique number for each field, an upward link from any item in any immediate sublist to that number, and also a field with a label - i.e. anID, lngSupID, and txCategory, respectively.

anIDtxCategorylngSupID
58Catalog Links0
59BUMPS58
60MAIN CATALOG58
61TEXTURES58

 

It's using the older DAO type, but that's also not essential. Once the treeview is set as an object, objtree, certain general properties can also be set, before it is first displayed. Here the text in the treeview will be Arial, 8, with a weight of 600, as you can see. Background, line types, and much else can also be specified for the entire treeview.

It's that subroutine, sFillTvw, that does all the work. Given the table/dataset to use, given an initial Where clause or criteria, it fills in the entire treeview, line by line, from the table.

In this case there is also a flag, True, to indicate that the entire tree won't be filled out, initially. It will be filled out on an as-needed basis. Instead, only two or three levels down will be filled in, whatever level you set. Then whenever you click on an item/node in the treeview, a routine is called to fill out a little more. Generally, it might be best to fill out smaller trees all at once, when first loaded. But if the meat of the database, the main tables, even start to push 8000-10000 entries (and even for smaller non-commercial databases that might not take as long as you might think), then even a fast pentium-4 can make you wait 10-15 seconds or more for the entire treeview to be filled in. That can be annoying. The idea of filling out a few levels at a time, instead of just one, and then one down, and then one down, etc., is that this ActiveX, VB6 type treeview does not allow one to set the expand/contract (plus/minus) button, directly. In order to show that the item is a folder, or contains a sublist, there have to actually be items under it. And this solves that, without forcing the complete database to be placed into the treeview, at once. [Now, it's true that one can use SendMessage from the Windows API, and an elaborate series of cover/wrapper functions, to conceivably switch on the plus/minus box even if there are no child nodes in the treeview. But it might pose problems of its own, face unexpected delays, and this method here is just as easy, frankly, particularly as it ties in with the fill-once scheme that might typically be used for treeview (on the assumption that most of the treeviews one uses will represent pretty light hierarchies).]

 
Treeview - loading it up

 

The treeview must be 'populated'. There are two routines at any rate. One fills the top level, and then passes control to a recursive routine that fills in subsequent levels. If there is a limit on levels, the recursive routine will stop at that point. If not, it fills out the entire treeview from the table specified, based on a changing criteria. And that criteria, in this case, for an 'adjacency list', or self-referential table, is simply assuming that the current record in the table has a sublist associated with it. If not, the record is just placed as a node in the treeview, whatever field is used for the name. If it does have a sublist, that is recursively read, and any under it, and so on. That's literally how the control is filled in - programatically - not simply from a SQL view or any 'flat' table. It's filled in by loop or recursion (recursion just seems easier in this case), under program control.

So the top level:

 
' Assume table/recordset has a unique numeric field, here the much maligned autonumber which is called, anID,
' and that the text to put in the treeview is in a short text field called, "txCategory".
 
Public Sub sFillTvw(objtree As Object, rst As DAO.Recordset, strSearch As String, Optional boolTopOnly As Boolean)
    On Error GoTo sft_err
    Dim bmrk As String, nodCurrent As node
 
     ' Find first top level record.
     rst.FindFirst strSearch
     objtree.Nodes.Clear
 
     ' Fill out only top level node, but call the recursive sAddBranches to fill out every subdirectory.
     Do Until rst.NoMatch
         Set nodCurrent = objtree.Nodes.Add(, , "a" & rst!anID, rst![txCategory])
         objtree.Nodes(nodCurrent.Index).Sorted = True
 
         bmrk = rst.Bookmark
         If boolTopOnly Then
             sAddLimitedBranches objtree, nodCurrent, rst, 2, 0
         Else
             sAddBranches objtree, nodCurrent, rst
         End If
         rst.Bookmark = bmrk
 
         rst.FindNext strSearch
     Loop
 
exit_out:
     Set nodCurrent = Nothing
     Exit Sub
 
sft_err:
     Resume exit_out
 
End Sub
 

 
 

As an example, even this might be improved by using a SQL view/SELECT rather than passing a reference to the entire DAO recordset. Unlike treeview, which starts to take a while to fill at about 8-10K entries, surely it would take far more records to slow things down in this way. But when the recordset gets too big, this could become a bottleneck. Whichever one prefers, then.

   
Treeview - Properties, Methods

 

You can see at the top that the treeview is emptied, cleared out. The treeview, overall, has a property - nodes. Technically - it's a collection; the Nodes collection of a treeview. One method one can use with nodes is, clear. This is a complete refresh or filling of the treeview, from scratch. What had been in the treeview - is gone (of course, that's just in the treeview, not the underlying table or data). Then the entire recordset is read, in order, by the next record with the same superior node as that which was specified on recursive entry to this routine. Again, see the notes on hierarchies. Nodes are added, using the add method of the tree's general nodes property, and here with text from the recordset's, txCategory field. And you can see that each new node is sorted. This refers to any potential sublist. If one is added, the items/nodes will be alphabetically sorted by their text property - again, the text read from txCategory in this example. The add method shows two parameters at the start which are not even used, only the commas are used to hold their places, here. The first parameter used is the "a". This is the node's key property. Each node, in turn, has certain properties; the text property just mentioned, and a key which must be unique in the entire treeview. In addition, this key is expected to started with a letter, not a number. So here "a" is prefixed to the unique number for the underlying record in the table. The assumption is that the treeview is only being filled from this one table (as this is all done programmatically, you can see it doesn't necessarily have to be so). That also means when someone clicks on this node, it's easy to find the underlying record. It's simple enough to add a letter like, "a", and then strip it off when it comes time to read the number. Even different letters, one or more, could be used, if you allowed for such in your programming, perhaps helping to identify different types of nodes. There is an additional property, however, for each node - tag. And you can put whatever you want in the tag, including such info about node type, etc.

 
Node Properties
  Child from a particular node, a reference to the first child node, if any
  Children number of child nodes - will be 0 if there are none
  Expanded true/false, true will show the sublist under the node, if any, and false shows only the node. Toggling the plus/minus button, or double clicking on the node, switches this property on/off.
  Fullpath full path to the root node, from the node in question, going step by step up the tree/hierarchy
  FirstSibling from the particular node, a reference to the first node atop that list, and the same level - i.e the first node under the parent node
  LastSibling last node in that same level/list which contains the node in question
  Index starting from 1, the treeview adds essentially an 'autonumber' to each node. Once a node is created, index can be used to read this number.
  Key case-sensitive unique value that is written/set for the node by the programmer. Must begin with a letter of the alphabet.
  Next reference to the node in the same level/list, which immediately follows the node in question
  Previous reference to the node just above the node in question, at the same level, same list
  Parent node under which the list is placed, the immediately superior node
  Parent node under which the list is placed, the immediately superior node
  Sorted true/false, and when true, the sublist for the node is sorted alphabetically, on the text property of each node. However, only a sublist, a list with a parent node, can be sorted. So it doesn't apply if you have a list at the top level.
 
And if a node is added, or if a node is just renamed, and the sort is merely alphabetic, then the parent node for that list must once again be set to, Sorted. It must be done every time. If you are using a sort field to place the list in some manually determined order, then Sorted is probably off, and you have a program to refresh the list or entire treeview when changes are made.
  Tag Text that is not displayed, but is associated with the node. Could be used to identify nodes, further, or hold parameters, or whatever else.
  Text The text that appears as the label, the very name you see for that line in the treeview. And when Sorted is true, it will sort the node's text.

 

So the treeview itself has this add method, and the clear method, seen above. And each node has at least the properties - .text, .key, .tag, .index, and .sorted, and whatever else shown in the chart. And there are a few more. These Nodes can also contain images, as well as text. That's not covered here. Now, having seen the example, just forget about the 'sorted' line. It's not needed. You'll see why in the recursive routine, below. Since Sorted must be set for any new addition, it's even just generally recommended to set the superior node's Sorted property as the last thing after all else has been completed:

   
Treeview - Recursion

 

And so all the sublists have to be filled-in. As noted above, here an option is presented to call a recursive routine that stops after so many levels down into the hierarchy, or else one that simply keeps at it until all the data is placed in the treeview as the form is first loaded. The simpler, sAddBranches, just keeps going. So it would be quite similar to that above, and looks like:

 
Public Sub sAddBranches(objtree As Object, nodSup As node, rst As DAO.Recordset)
     Dim nodCurrent As node
     Dim strSearch As String, bmrk As String
 
     On Error GoTo ErrAddBranches
 
     strSearch = "[lngSupID]=" & Mid(nodSup.Key, 2)
     rst.FindFirst strSearch
 
     Do Until rst.NoMatch
         Set nodCurrent = objtree.Nodes.Add(nodSup, tvwChild, "a" & rst!anID, rst![txCategory])
 
         bmrk = rst.Bookmark
         sAddBranches objtree, nodCurrent, rst
         rst.Bookmark = bmrk
 
         rst.FindNext strSearch
     Loop
 
     objtree.Nodes(nodSup.Index).Sorted = True
 
ExitAddBranches:
     Set nodCurrent = Nothing
     Exit Sub
 
ErrAddBranches:
 
     MsgBox Err.Number, vbCritical, "AddBranches Error:"
     Resume ExitAddBranches
 
End Sub
 

 
 

Again, it's basically just the top routine, but it calls itself, recursively. You can see the .Sorted has been moved down here. Because once a sublist is filled out, once that's completed, then the superior node is set to sorted. That alphabetically sorts the sublist by the text property of each node. And the other obvious difference is that the first two parameters of the add method have to be used. It's a child node. So the tvwChild constant is used (this doesn't have to be declared, but comes with establishing a reference to the control - see above). And the node to which this list/sublist is attached is also used, here called - nodSup. That's it.

A different scheme was noted, above, allowing that only a certain level would be reached by this recursion. Only part of the treeview would be filled in. And each click on a node would call up another routine, at that time, to attempt to fill out a few levels in, and so on each time:

 
Public Sub sAddLimitedBranches(objtree As Object, nodSup As node, rst As DAO.Recordset, _
                                             intLimit As Integer, intCtr As Integer)
     Dim nodCurrent As node
     Dim strSearch As String, bmrk As String, boolWasEmpty As Boolean
 
     On Error GoTo ErrAddBranchesL
 
     strSearch = "[lngSupID]=" & Mid(nodSup.Key, 2)
     rst.FindFirst strSearch
     boolWasEmpty = (nodSup.Children = 0)
 
     Do Until rst.NoMatch
          If boolWasEmpty Then Set nodCurrent = objtree.Nodes.Add(nodSup, tvwChild, "a" & rst!anID, rst![txCategory])
 
          If intCtr < intLimit Then
               bmrk = rst.Bookmark
               If boolWasEmpty Then
                    sAddLimitedBranches objtree, nodCurrent, rst, intLimit, intCtr + 1
               Else
                    sAddLimitedBranches objtree, objtree.Nodes("a" & rst!anID), rst, intLimit, intCtr + 1
               End If
 
               rst.Bookmark = bmrk
          End If
 
          rst.FindNext strSearch
     Loop
 
     objtree.Nodes(nodSup.Index).Sorted = True
 
ExitAddBranchesL:
     Set nodCurrent = Nothing
     Exit Sub
 
ErrAddBranchesL:
 
     MsgBox Err.Number & ": " & Err.Description, vbCritical, "AddBranchesLimited Error:"
     Resume ExitAddBranchesL
 
End Sub
 

 


' And then each time a node in this treeview is clicked - or expanded - in the module of the form containing, tvw:
 
Public Sub tvw_NodeClick(ByVal node As Object)
 
     Set rst = Me.RecordsetClone
     .
     .
     .
 
     sAddLimitedBranches Me!tvw, node, rst, 2, 0
 
     If Not rst Is Nothing Then
         rst.Close
         Set rst = Nothing
     End If
 
     .
     .
     .
 
' and:
 
Public Sub tvw_Expand(ByVal node As Object)
 
     Set rst = Me.RecordsetClone
 
     sAddLimitedBranches Me!tvw, node, rst, 2, 0
 
     rst.Close
     Set rst = Nothing
 
End Sub
 

 
 

And so even this is not all that different from the previous, which was not so different from the previous to that. Apart from having to call the limited recursion routine from the treeview's form on nodeclick and expand, here the difference is clearly that a flag is set if a sublist already exists for that node. Maybe there is no sublist for that node. Maybe it's not in the data. In that case, it proceeds normally, and makes a quick exit back out. And of course, in that case, if there is a sublist, but it hasn't been filled in yet, then it will be. But if there is one already existing, then instead of adding a new node, the correct node is only read/selected and then passed back in recursively. You can also clearly see that it is very useful that nodes is able to find a particular node by its - key. And at some point, the routine will start filling in new sublists, before hitting the limit, here previously set to 2 or 3 (see above). You can see how the recursion is used to increment the counter to compare with the limit - intLimit (which itself is simply passed in unaltered every time). So - that's that.

Interesting, or boring, as it may be, all this did was fill in the treeview. It's nice that you can see it all organized. But now what? How do you relocate sublists? which is the real feature of using treeview and such hierarchies, as opposed to the relational model which fixes hierarchies both by key assignments in the tables, and by the tables themselves. How do you then update the underlying table, using a treeview? And so on.

Continue