Help On Locating Text by Position (Fake Treeview)

MajP

You've got your good things, and you've got mine.
Local time
Today, 11:20
Joined
May 21, 2018
Messages
8,933
In relation to this thread

I wanted to see if I could build a functional treeview using a listbox. Something that was reusable and provided real treeview capabilities
1. Multiple levels (more than 2)
2. Expand and collapse
3. Expand and collapse icons

I was surprised at how well it worked and extremely easy to implement. Like usual this is a class module that only requires a line of code for all functionality. The only effort is making a properly formatted query with proper alias columns. This allows it to be reused for any query.

Here is all the code to load two different trees and allow these features. The class does the work.
Code:
Private LTVW As New ListboxTreeview

Private Sub cmboLarge_Click()
  LTVW.Initialize Me.lstSimulated, "qryNodeLargeEmployees", "TD"
End Sub

Private Sub cmdCategories_Click()
  LTVW.Initialize Me.lstSimulated, "qryCustomers_Orders_OrderDetails"
End Sub

My issue is that on a normal treeview when you click on the [+], [-] icons to expand or collapse they work. But if you click off the icons you can simply select a node without expanding or collapsing.

Sim1.png


My question:
Any idea if there is a way to return the mouse down location and compare to the string to see approximately what is under the mouse. On a normal treeview expand/collapse only happens when you click on the icons to the left of the description. This allows you to be able to select a node by clicking on the description and not trigger and expand and collapse.

I can determine how many characters before the actual Description, if that helps. Example Order 10643 would start at 10 characters in. So if the mouse is less then 10 character width I would expand / collapse. Ideally if the user clicks to the left of the description I would trigger the expand or collapse code. They would not have to be right on the [+],[-]. If they were close to the description I wound not trigger the expand and collapse.

My other option is to simply require a double click for expand and collapse, but this is different behavior then a normal tree.

another tree
Sim2.png
 
Last edited:
As I said this is still in works. But for those interested.
FYI. The large demo loads fine because you load level by level. But if you collapse it it has to do a recursive call and lengthing search through a large (10k) records. It runs real slow and I may need to think of a better/improved method.
 

Attachments

My take for how to determine if click should expand according to mouse position is to grab the mouse X position from the MouseMove procedure of the ListBox, and compare it with the size of the indentation pattern in twips. If X is higher than the width of the indentation pattern, it should expand.

Didn't test all possible scenarios, for example, I don't know if the ListBox ever shows a horizontal scrollbar, but I guess it should work.
 

Attachments

My take for how to determine if click should expand according to mouse position is to grab the mouse X position from the MouseMove procedure of the ListBox, and compare it with the size of the indentation pattern in twips. If X is higher than the width of the indentation pattern, it should expand.

Didn't test all possible scenarios, for example, I don't know if the ListBox ever shows a horizontal scrollbar, but I guess it should work.
Thanks. That is what I am going for.

Questions.
1. How did you determine that the indent-pattern "----" is 21 twips?
2. Does this require a mono space font like courier if adding other chacters to the indent? Each level is idented a proportional amount of characters but the characters could include [+], space, |, - depending on the level and if expanded or not?
 
How did you determine that the indent-pattern "----" is 21 twips?
It's not 21 twips, but 21 pixels. I just used mspaint and zoomed in a lot so that the select tool could snap to pixels, the status bar shows the selection size. It requires to also know DPI, so, I'm afraid it would depend on the user's screen settings. I'm sure there are better references for screen things than myself, I just ran a quick google search.

Does this require a mono space font like courier if adding other chacters to the indent?
Courier new is what I like, but if you measure your symbols, I think you could hardcode the sizes using conditions and force the users to use a certain font. Either that or, you know, having to account for the size of the font as well.
Each level is idented a proportional amount of characters but the characters could include [+], space, |, - depending on the level and if expanded or not?
Maybe try to compare where the "[" character is InStr(1, m_ListBox.Column(1, m_ListBox.ListIndex), "[")
After all, the toggle occurs only if there is a plus or minus symbol. Maybe.

So, in conclusion, I think it requires some screen API stuff for this requirement to work using this approach.
 
This is the modification I made for toggling

Code:
Private Sub m_ListBox_AfterUpdate()
    ' Split the column value based on the IndentBefore delimiter
    Dim arr As Variant
    arr = Split(m_ListBox.Column(1, m_ListBox.ListIndex), IndentBefore)

    ' Calculate the indentation width in twips
    Dim indentWidth As Long
    indentWidth = UBound(arr) * PIXELS_PER_LEVEL * PIXELS_PER_TWIP

    ' Check if the X position falls within the indentation, pattern range, and padding
    If Xpos > (MARGIN + indentWidth - PADDING) And Xpos < (MARGIN + indentWidth + PATTERN_SIZE + PADDING) Then
        ToggleExpandCollapse
    End If
End Sub

I added these constants/variables:
Code:
' Constants for clicking
Const PIXELS_PER_LEVEL As Long = 22
Const PIXELS_PER_TWIP As Long = 15
Const MARGIN As Long = 5 * PIXELS_PER_TWIP
Const PATTERN_SIZE As Long = 13 * PIXELS_PER_TWIP
Const PADDING As Long = 2 * PIXELS_PER_TWIP
Private Xpos As Single

And it's capturing X with this
Code:
Private Sub m_ListBox_MouseMove(Button As Integer, Shift As Integer, X As Single, Y As Single)
    Xpos = X
End Sub

According to the measurements, there are 5 pixels of margin to the left, so I accounted for that and other measurements. It will probably offset a bit if there are too many indentations. It needs a lot of fine tune, I guess.

EDITED.
 

Attachments

Last edited:
Seems to work really well. I wanted to see if this still works with different fonts and went from 8 Times New Roman to 12 Arial and it seemed to still be spot on. I do not understand that because I thought it would fail. I expanded this to 6 levels and still seems to work.

It is good enough for a demo. I am in 1920 x 1080 so I assume this may not work at other resolutions. API is not my strength but maybe someone else has a more robust solution.
 
I think that if someone has complaints, they should calibrate it themselves. Although I suppose it could be enhanced with some sort of calibration feature like 'Click here. Now click here. Done.' In any case, it's very well done and quite scalable. I had no trouble finding where to apply the changes.
 
Haven't read this thread thoroughly, so apologies if you already have this covered or if it's irrelevant;

Access has the undocumented WizHook library that contains a TwipsFromFont function - it might be useful for calculating your sizes.

See Mike Wolfe here
 
@Edgar,
Thanks again for the assist. I did go ahead and test this with the Wizhook function using this to get the length of the string up to and including the opening and closing bracket ".....[+], or ......[-]"

Code:
Public Function GetTextLengthInTwips(pCtrl As Control, ByVal str As String, _
        Optional ByVal Height As Boolean = False) As Long
    Dim lx As Long, ly As Long
    ' Initialize WizHook
    WizHook.Key = 51488399
    ' Populate the variables lx and ly with the width and height of the
    ' string in twips, according to the font settings of the control
    WizHook.TwipsFromFont pCtrl.FontName, pCtrl.FontSize, pCtrl.FontWeight, _
                          pCtrl.FontItalic, pCtrl.FontUnderline, 0, _
                          str, 0, lx, ly
    If Not Height Then
        GetTextLengthInTwips = lx
    Else
        GetTextLengthInTwips = ly
    End If
End Function

I modified the code to the following

Code:
Private Sub m_ListBox_AfterUpdate()
   
   
    Dim displayText As String
    Dim leadingText As String
    Dim TwipsStart As Long
    Dim TwipsEnd As Long
   
    displayText = Me.ListBox.Column(1, Me.ListBox.ListIndex)
    leadingText = Left(displayText, InStr(displayText, "["))
   
    TwipsStart = GetTextLengthInTwips(Me.ListBox, leadingText)
    TwipsEnd = GetTextLengthInTwips(Me.ListBox, leadingText & "+]")
   
    If Xpos > (TwipsStart - PADDING) And Xpos < (TwipsEnd + PADDING) Then
        ToggleExpandCollapse
    End If
End Sub



I tested this with different fonts and seems to work very well, but I do not understand exactly how.
I thought Wizhook measures the length in TWIPS of the string based on the font, but I thought the mouse is measured in Points from the left edge. Where a point is 20 twips.

X, YRequired. The horizontal or vertical position, measured in points, from the left or top edge of the control
 
Have you considered when you click on a text box it sets the selstart property ?
 
Have you considered when you click on a text box it sets the selstart property ?
Not sure I understand the question since there is no textbox involved, only a single listbox.
 
Ah sorry- thought you were using a continuous form
 
I thought Wizhook measures the length in TWIPS of the string based on the font, but I thought the mouse is measured in Points from the left edge. Where a point is 20 twips.

The docs say that X and Y are in twips for MouseMove. It starts counting from the edge of the control.
1722975401554.png


I don't have a clue of how precise the Wizhook method for measuring fonts is, but after clicking on the togglers using the magnifier glass at 1600%, I think the padding is helping a lot.
 
Not trying to hijack the thread but this is my example of a fake treeview - uses a continuous form. This is used to catalogue photographs. It is managed using an ado disconnected recordset which is initially populated from a table of categories using recursion and subsequently filtered based on user selections.

'closed'
1722987917567.png

expanded on one selection by clicking on the arrow
1722987987743.png

and to a second level (currently limited to 5 levels but can be more if required)
1722988097873.png

clicking on the text 'selects' or deselects it for cataloguing (could use colour formatting/bold/etc instead of a tick)
1722988266665.png

This example would catalogue pictures of friends and family taken whilst on holiday at a villa in Cyprus.

It is still a WIP - functionality to be added include right clicking to add new categories and a filter to hide categories not relevant to the current activity (e.g. hide House and Pets whilst processing the Cyprus holiday photos)

This is a similar example showing the structure of access databases
1722989866845.png
 
@CJ_London
This one was in response to the original request using a listbox. But after I started, i figured I could probably do more and make it more visually appealing with a subform. But a subform has more potential with formatting especially with Richtext. The in memory ADO recordset is a great idea that I did not think of. I think it probably would be faster with really large tress. Since you are only filtering the view and not having to actually create and remove rows as I do in the listbox. Now I am interested in a form version too.
Questions:
1. How you do the icons? I assume this is a string concatenation and not an actual image. Is that just a specific ASCII character?
2. Why is it limited to 5 levels now? Again I assume the indenting is simply a format that you apply to the level and all records are stored in the recordset or added when needed. I would assume you could have as many levels that the formatting could support based on the width of the form.
 
1. How you do the icons? I assume this is a string concatenation and not an actual image. Is that just a specific ASCII character?
it is ascii - characters typically in the 9600 range - wchar(9658)= wchar(9698)=◢, but there are plenty of other characters that can represent furniture, people or with a database, tables, queries, forms etc

Generally these characters are pretty consistent across multiple fonts

Or as you say, you can use richtext which my last example uses - it has the benefit you can mix colours, fonts and fonts such as blockline (chr(65)-chr(70)) to provide the lines you use in your first post. However not so easy to use selStart to determine where on the control the user clicked, which may or may not matter depending on the purpose.

2. Why is it limited to 5 levels now? Again I assume the indenting is simply a format that you apply to the level and all records are stored in the recordset or added when needed. I would assume you could have as many levels that the formatting could support based on the width of the form.
For that particular cataloguer example, 5 levels is sufficient, otherwise you can get into too fine a detail for the purposes of the app (In this app, the underlying table is populated through the treeview form and a limit is used when the user adds a new category at a new level but can easily be changed for a different application). You can go to as many levels as you like but I guess it would be a max around 250 being subject to the maximum number of fields in a table (never checked but I assume ADO is the same as DAO in that respect) - and as you say what you can usefully display on a form.

It very much depends on the application and use, but my own approach would be to consider what is understandable/readable to the user in terms of number of levels - above say 10, it might be better to open a second treeview to 'continue on down'.

During development I display all the fields in the subform, but set the subform control width to only display the first column - this is a screenshot when the subform is widened to view the management fields.
1723056916191.png

There are other rows in this example - but show is set to false so they don't display.

To understand the fields, the cPK contains the PK of the specific record (so cPK=4 is Holidays) and it is a top level category so L0=4 (the cPK). Crete (cPK=7) a direct child of Holidays is displayed as level 1 (so L1=7) and it's parent is the previous level (L0) so L0=4. Whilst Hotel (PK=17) is a child of Crete so L2=17, etc

The show, ptr and selected values change depending on where the user clicks in the subform. S/O is part of the sort order definition within the parent sort order.

Once development is completed, those additional controls can be hidden or removed.

My last example comes from my Access Studio app and enables the user to open as many access databases as they like and in testing I have had perhaps 50 different db's in the list - each going down a maximum of 4 levels. The initial list does come from a table but that only contains the path and file name of the db (Level 0). The ado recordset is appended as and when the user selects or adds a db. It is populated by going through that db's tables and queries to determine the fields and their properties and that typically takes a few seconds. Users do have the option of doing this for all db's in the list (so 50 db's might take a couple of minutes to create perhaps 10,000 records). As it is part of my Access Studio app the other time it gets updated is if the user executes a DDE query via Access Studio

However once loaded, filtering the recordset takes but a moment

Since this is a form, it is a class object and it is very easy to set properties to display different characters, fonts, size, even the source from outside the form.

Longer response than I anticipated but hope is of interest:)
 
Longer response than I anticipated but hope is of interest
Thanks, that is very helpful. The in memory ADO recordset opens a lot of possibilities.

Curious why you need to track the relation to all the levels (L0 to L4) Could you not simply store a ParentID in a single field?
Why are the additional upper levels needed and how do you use that information?
For example if your know Hotel's Parent is 7, why do you need to also store grandparent of 4?

If you do not need that information and a single field suffices, you would not be limited by the amount of levels.

@Edgar_
Thanks.
The docs say that X and Y are in twips for MouseMove. It starts counting from the edge of the control.
Interesting this shows Points on the same site, but I mistakenly did not realize that is under VBA Forms section (meaning UserForms)
I be suprised if that is really true and Userforms return Points and Access Forms return twips or if that is just a documentation mistake.
 
Curious why you need to track the relation to all the levels (L0 to L4) Could you not simply store a ParentID in a single field?
Why are the additional upper levels needed and how do you use that information?
For example if your know Hotel's Parent is 7, why do you need to also store grandparent of 4?
think of it the other way round - faster and simpler processing - if user opens up or closes down a parent it is a much simpler filter rather than needing to loop through all their children and their children etc to extend the filter. The only issue (for me anyway) is if a parent has children and grandchildren showing and the parent is then closed. On reopen only the children show, not the grandchildren. It is something I would like to resolve when I get the time.

If you do not need that information and a single field suffices, you would not be limited by the amount of levels.
I have used this method, or variations thereof in perhaps 20 different apps, none of them have required more than 10 levels. It can go up to circa 250 levels if it had to - tho' I don't know what the impact would be on memory usage and I suspect I wouldn't be using Access anyway if I had such a challenge! At that point, would probably need to go the more traditional way...

Don't forget, with the exception of the Access Studio version which interrogates different dbs to populate the recordset, the underlying table has a standard PK, Description, ParentFK structure.

I've used it as a menu system and more recently as a specification selector - for example a user selects a colour, a material, a size etc. Another variation is my own version of a shortcut menu. None of these require a vast number of levels.

Incidentally, I missed your AUG presentation yesterday, unfortunately I was out but hear it has good reviews and look forward to seeing the video when it is published.
 
think of it the other way round - faster and simpler processing - if user opens up or closes down a parent it is a much simpler filter rather than needing to loop through all their children and their children etc to extend the filter.
That makes really good sense. In my demo with the listbox using a very large data set, it was fine in the expand since you only expand the next level but the collapse required recursion to close all the levels and was very slow. Since you are using an ADO recordset having lots of columns is basically free, so this makes sense.

The only issue (for me anyway) is if a parent has children and grandchildren showing and the parent is then closed. On reopen only the children show, not the grandchildren.
I got a couple of ideas. Would it be enough to add another column "PreviousState (expanded / collapsed)"? Meaning that although it is not visible because you collapsed a level above its parent but its parent was in an expanded state. Probably still require a recursive call though but you would at least know what state it was left in.
 

Users who are viewing this thread

Back
Top Bottom