Announcement

Collapse
No announcement yet.

Listview - getting started in BP

Collapse
X
  • Filter
  • Time
  • Show
Clear All
new posts

    [TUTORIAL] Listview - getting started in BP

    I am creating this guide since I had trouble grasping the listview at first, seeing many have the same struggle and there not being any good information about them in a blueprint context.
    The listview should be pretty straight forward to use once you got the basic grasp of it.

    Note!:If a widget is generated for one instance more than once, the engine will crash. So any instance should only be in the listview once.
    Note! If the node "Is List Item Selected", part of the entrywidget class, is used, the engine will crash.
    Note! Multi Select Mode seems to be bugged, clicking an item clears the selection so it is not possible to multi-select by click. (implementing custom logic would work).

    Following bug reports has been made:
    - Is list Item Selected / Expanded crash: https://issues.unrealengine.com/issue/UE-73025
    - Multi select, click clears selection: https://issues.unrealengine.com/issue/UE-72610
    - EditorWidget listview with actor items, if an actor is deleted, trying to update the listview crash: https://issues.unrealengine.com/issue/UE-73014

    For versions: 4.21, 4.22

    Basic Setup
    Create a widget to hold the listview. For this guide, this is the widget that is added to viewport. Add a listview. You can't compile now because it wants an entry widget.
    Create a widget to be the EntryWidget. Delete the canvas panel and add a border. Then, follow the image steps.

    Click image for larger version  Name:	ListView1.png Views:	1 Size:	213.9 KB ID:	1595438
    1. Go to the Entrywidgets class settings.
    2. Add the interface "UserobjectListEntry".
    3. Note how the Entrywidget now has an interface function. This may be very useful but we need to return a value.
    4. Add the event "Event On List Item Object Set" which is also part of the interface we added. Right click on the List Item Object and promote it to variable. I renamed it to Item.
    6. Go back to the function in 3. and return the Item.
    7. Go to the listview widget, select the listview and the entrywidget you created can now be selected as the entry widget class.

    That's the basic setup done and you are now ready to use the listview.


    listview overview
    First of all, the listview works with instances. You create any object (Uobject, Actor, widget etc) to add as an item to the listview and the listview will, if needed, create an entrywidget to represent that item.
    Here's a selection of functions that the listview has. Only one for the generated widgets. It is designed to work with the items, the widgets are just there to represent the item.
    Of course, the widget instances matters too if you want to change something visually.

    Click image for larger version  Name:	ListView2.png Views:	1 Size:	265.7 KB ID:	1595439
    The Pitfall
    Now, you might be tempted to use the entrywidget you created to use as an item, changing some properties and expecting the changes to be displayed.
    Something like this: (the entrywidget has a textblock added to the border, text bound to a string variable with default text "MyText")

    Click image for larger version  Name:	Listview31.png Views:	1 Size:	57.8 KB ID:	1595440
    You might expect that this will create a listing of numbers from 0 to 10 as the displayed text. But instead the following is the actual result:

    Click image for larger version  Name:	Listview32.png Views:	1 Size:	3.0 KB ID:	1595441

    In this example a widget was created and added as an Item. The listview then generates widgets to represent those items.The Item has some properties changed (the text), the entrywidget - the widget that is displayed - remains default.
    Click image for larger version  Name:	Listview-ItemWidget.png Views:	1 Size:	78.3 KB ID:	1606577
    While widgets can be used as items, I think it's mostly confusing for learning. Lets start with something else. (the entrywidget could be used as a slot to which the widget item is added. But that's a little more advanced as of now).

    Listview generation
    So how do we set the properties of the entrywidget? The listview generates a widget for each item that needs to be displayed. When an entrywidget is generated, two events are fired. One in the listview called On Entry Generated. The other in the entrywidget which we saw earlier - "Event On List Item Object Set".
    When scrolling, entrywidgets are dynamically On Entry Released, On Entry Generated, triggering On List Item Object Set. Also On Item Selection Changed - IF that item is selected.

    Creating a proper list
    Here the item will be an UObject with a single text property which is set during a loop (1).
    To update the entrywidget, go to the entrywidget and on the event "Event On List Item Object Set" which is currently storing the item, cast the item to the Objecttype added as item and get its text (2).

    Click image for larger version  Name:	ListView4.png Views:	1 Size:	180.7 KB ID:	1595442
    Here, an UObject is used to just hold some data. But the item doesn't have to "just hold data". The ltem can be any instance and even different classes. So I could have an item list of UObjects, Actors, characters and vehicles. How you format the entrywidget visually is really up to you, with what kind of elements to display.
    Now that the basics are out of the way, lets do something more interesting. An Item can be any object, so what if we added some actors and these actors to be listed in the view?

    Going advanced - Actor representation
    For me the actor is just named "Ball" and has a static mesh sphere. These are dragged into the scene, about 10, and in the listviews construct event "get all actors(ball)". I wouldn't recommend this for practical use but it is good enough for an example.
    Now, we could loop through the array, adding each instance with the "add item". Or, we can just use the listviews "Set list items".
    The entrywidget will need to cast the item to ball class. For now, the entrywidget only has a textblock and I'm going to set its text to the objects "get display name".

    Click image for larger version  Name:	ListView5.png Views:	1 Size:	131.5 KB ID:	1595443

    We could make it more interesting, like randomizing the ball size and adding another textblock to the entrywidget, displaying the instance size. But that's just more data pulling which we have already learned.
    What if, instead, we were able to click on the listview and the corresponding ball is selected? Or Vice verse?

    Going advanced - Selection
    The listview has some inbuilt selection functionality. You can even choose between single and multiple selection in its properties. Single selection for now.
    The following image shows a simple setup. When "Event On Item Selection Changed" is called because you clicked on one of the items in the list, one entrywidget will return false and another will return true. One was deselected, the other selected. When debugging, if we only printed the boolean, how would we know which was selected and which was deselected? The impulse might be to include the widget reference as debug data, but that really provides poor information. Instead, try to work with the Items, which is done in the image.

    Click image for larger version  Name:	ListView6.png Views:	1 Size:	59.5 KB ID:	1595445

    Running the above will show that it works but we really need better representation.
    The Entrywidget gets a new variable - "Border Color", a linear Color which the border brush color is bound to.
    The ball has also gotten an event that can be called.

    Click image for larger version  Name:	ListView7.png Views:	1 Size:	221.6 KB ID:	1595444
    Running the above will change the size of the ball actor when a list item is clicked.

    -Problem 1-
    But there is a problem. If you scroll down you will eventually see a widget appear, marked as if it is selected.

    That is because the widgets are not destroyed when they are scrolled out of view, they are On Entry Released but stored and then On Entry Generated when needed again. So they still have their old properties. The display name text changed, because we do that "Event On List Item Object Set", but the border color remains untouched.
    So how do we fix that? Well, there are a many ways. There could be a "Reset Properties"-function that is run when Event On List Item Object Set? If we think about this execution logic, it would look like this:
    1. widget(s) generated, setting Item, reseting border color.
    2. Item selected, changing border color to green. and changing ball size.
    3. widget scrolled out of view, released, possibly generated again for some other item but that's fine.
    4. Selected Item is scrolled back into view - widget generated, setting Item, reseting border color!
    5. But there's another event that happens after that. The listview knows that the item is selected so it calls Event On Item Selection Changed on the widget. (only for those items that are selected, not for the ones which are not).

    -Solution 1-
    A reset function could work in this case, but would still be an issue for other types of executions. Imagine if, say a tree, would grow each time it was selected. But then it would also grow each time i was scrolled back into view...

    The reason for pointing all of this out, is that it is important to test just what all the functions do and that different solutions is needed depending on the situation.


    -Problem 2-
    If an item is selected - changing the ball size - and you scroll down and select another item, the ball size of the previous selected should reset, but it doesn't!

    Because we have implemented behaviour into the EntryWidget, and the entrywidget points at different items - items which might not be represented at the moment - the widgets may not be able to communicate with the relevant item.
    This case example should show us that the EntryWidget may not be a proper place to implement item changes to.
    So the border color stays, the ball size change goes.
    To where? To the listview to handle.

    -Solution 2-
    The listview has the event "On Item Selection Changed", which provides the item and if it is selected or not. But we also want to reset the previously selected ball size. Well, the listview only provides so many functionalities. This is a behaviour we need to implement ourselves. Current setup:

    Click image for larger version  Name:	ListView9.png Views:	1 Size:	180.2 KB ID:	1595446
    Remember that this is just for single selection. If we wanted multiple selected items the node setup would have to be modified.


    On Item Selection Changed - ListView Or EntryWidget?
    At first glance the Selection Changed events of the treeview and the entry widget may appear very similar, but they are in fact very different.

    Click image for larger version  Name:	ListView-OnItemSelection.png Views:	1 Size:	63.1 KB ID:	1604412
    The EntryWidgets event should be used for setting up visual representation of the entrywidget.
    The ListViews event should be used for logic such as informing the Item about its selection state. If the deselected item is relevant, you need to track it manually using a variable, which we have done in a previous example.


    Going advanced - World selection
    So we can select an item in the list to show that it is selected, and store the current selection in a variable for any class to use. But what about clicking on the actor itself?
    Well, how do you want to handle that? should it be with the spheres on clicked event? Or a linetrace from the camera?
    If we do it with the spheres On Clicked event, then the sphere needs to either send information to -some class- that it was clicked, or have -some class- bind to the ball being clicked.
    I'm going to go with a linetrace from the third person character, in the third person template, because that's just the easy way to do it. The third person character in my case is also the class that creates the listview widget.

    Now there is a slight issue.
    We know that by clicking on an entrywidget, that item will be selected, telling that actor to change size. To demonstrate the issue, the ball will now print the value of an int, which is incremented if selected and decremented when deselected. So deselected =0, selected =1.
    Say I do the following:

    Click image for larger version  Name:	ListView10.png Views:	1 Size:	211.1 KB ID:	1595447

    1. Detect ball through a linetrace.
    2. Tell the ball that it has been selected.
    3. Inform the listview to mark the ball as selected.
    What now happens is that the listview tells the ball to be selected, again. As you can see a 2 int.
    When it comes to selection - if the item needs to know that it is selected - in my experience it is better to go through the listview to have IT perform the selection.

    Let's follow some execution flow with some requirements.
    1. The character needs to be updated about the selection, say saving the reference in a variable.
    2. The Item needs to be informed that it has been selected, say becoming highlighted.

    First is a selection in the listview.
    Click image for larger version  Name:	ListView-SelectListView.png Views:	1 Size:	9.2 KB ID:	1604603

    Second, the item selected though the world. For example, a linetrace from the character. If the Item and character are informed of the selection after the trace, and then the Listview is told to select the item it creates unnecessary logic, which might be problematic.
    Click image for larger version  Name:	ListView-SelectCircular.png Views:	1 Size:	15.5 KB ID:	1604604

    Third, the item selected though the world. For example, a linetrace from the character. The character informs the Listview to select the Item.
    Click image for larger version  Name:	ListView-SelectusingList.png Views:	1 Size:	11.3 KB ID:	1604605
    But in the end the setup depends on what you require the selection to do.


    Going Advanced - Drag & Drop
    Drag & drop is as easy to implement for a listview as anything else. If we want to be able to reorder the items then, instead of having the listview handle the items, we need to take more control in the form of an array that we can remove and insert items into.
    The entrywidget now has a "Item dropped on" dispatcher which the listview binds to when the entry is generated

    Click image for larger version  Name:	ListView12.png Views:	1 Size:	366.2 KB ID:	1595449

    Bindings in the Entry Widget
    If your Entry Widget has a event dispatcher that the widget holding the listview binds to during the "on entry generated" event, you do not have to unbind them when the widgets are released. Seems like the entrywidget internally unbinds any bindings during the released event.
    However, if the Entry Widget binds to event dispatchers - say the item they represent - those have to be unbound manually.


    Node information

    Click image for larger version  Name:	ListView-ScrollIntoView.png Views:	1 Size:	58.8 KB ID:	1604413
    Last edited by ste1nar; 05-29-2019, 07:17 AM.

    #2
    [Reserved for treeview and Tileview]

    The treeview and gridview are very similar to listview.

    Note! The function "is List item Expanded" crashes the editor.

    Treeview - Getting Started
    You setup a treeview the same way as a listview.
    The point of a treeview is to have Items be "children" of other Items, like a tree!
    You add children to an item in the function "On BP on Get Item Children 0". This is a binding you find on the treeview events in the designer graph.
    Once that binding is created, you will get a function with an in Item, and an array of items as children.
    Again, if a entrywidget is created more than once for an instance, the engine will crash. This is very easily done when learning the treeview.
    Using the ball actor in previous examples, I have added a bunch of mesh components which will be the children.
    This is the basic setup I have created:

    Click image for larger version  Name:	Treeview1.png Views:	1 Size:	265.5 KB ID:	1595475
    **Addendum: On BP Get Item Children needs to return an empty array if the cast fails, or this setup will not work.
    Here, Ball 12 has been selected and the treeview expands it by getting all the children - children components in my case - and generating entrywidgets for them. Do note that the "On BP on Get Item Children 0" is also executed for each item - each child - that appears.
    While there's some basic formatting happening in the entrywidget - a textblock either has a > or V symbol, it could be done better.
    - Some spacing for each child to better indicate what item they are children of.
    - If an item does not have children, it really should not have either > or V symbol.

    Treeview - Spacing [Backtracking through parent]
    The spacing is set by adjusting the border left padding. The value depends on how deep in the hierarchy the item is. In this case we can find out the depth by calling a recursive function which gets the parent of each item.

    Click image for larger version  Name:	Treeview2.png Views:	1 Size:	337.4 KB ID:	1595476


    Treeview - Expandable symbol
    There are three possibilities with the expand symbol, in my case a simple > or V.
    > means it can be expanded
    V means it is expanded.
    But if it can not be expanded - if it has no children - no symbol should be displayed.
    So how do we determine that? Sure, you can cast the item to actor or scene component and get its children. But there's already a similar example of that above, and also maybe you use some other method to determine if an item has children for the treeview. It might be better to let the treeview determine this.
    Besides, while "On Item Selection Changed" updates for the entrywidget when scrolled into view, the event "On Item Expansion Changed" does not so we can not rely on that though it is needed.
    The "On BP on Get Item Children 0" can be called manually to determine if the item is expandable or not.
    The function "is List item Expanded" would have been useful but any use of it crashes the engine.
    So we need to implement a custom way of remember the expanded items. I use a TSet of UObjects.


    Click image for larger version  Name:	Treeview3.png Views:	1 Size:	336.9 KB ID:	1595477


    Treeview - Spacing [When getting the items children]

    But not all treeviews will have items that actually inherit from one another. In that case you need to track the depth of the item. The tracking can be done in the "On BP on Get Item Children" function. By using a Tmap to store the item and its depth, we can query the depth of the parent and set the depth of its children as we enter those into the map. Then, as items are generated we can get the Item depth and set it for the widget.

    Click image for larger version  Name:	Treeview-Spacing-forward.png Views:	1 Size:	355.0 KB ID:	1603229


    Treeview - I don't want the expansion changed!
    Currenty, if an item has children and is clicked on, its expansion state will change. Following are a couple of solutions so that does not happen

    Treeview - Expand only if Selected [revert expansion method]

    Currently when an item is clicked, it will expand or collapse. What if we don't want it to change its expanded state when we select it, but if we click it when it is selected, then it will change expansion state? Again, this is possible to achieve but requires our own implementation logic.
    First, we must track which item is selected but can not use the treeview's get selected item(s) function.
    For the events that we will be using, the first thing that happens is that On Item Expansion Changed is called and after that On Item Selection Changed is called. However, printing the item that the expansion event provides, shows that the clicked item has already changed.

    Click image for larger version  Name:	Treeview-ExpansionDebug.png Views:	1 Size:	119.4 KB ID:	1603247

    This means we can not use the treeview's get selected item(s) function but can track the selected item and query the previous selected item during the expansion event.
    The clicked item will change its expansion state, but we can revert the change by using the treeview function "Set Item Expansion". However, that is going to trigger "On Item Expansion Changed" event again, so we need to track the revert logic so that the second event does not call the "Set Item Expansion" again.

    Click image for larger version  Name:	Treeview-Revert-expansion.png Views:	1 Size:	296.6 KB ID:	1603248
    ** After some more experience, you may want to use an Item variable Exclusively to track the expanded state, instead of the "selected Item" reference which would be used for other things.


    Treeview - Don't change expansion [Override click method]

    The entrywidget will internally tell the treeview that it was clicked on. Whether it was a border, an image or another user widget, it will detect the click.
    Setting the entrywidget to hit test invisible is no good - we need to be able to click on the widget.
    There is one widget that does not let the userwidget know it was clicked on: the button.
    So below is an image of the entrywidget hierarchy. Logic will be that when the
    -"Expand Button" is clicked, the expansion of the item is changed- collapses if expanded, expands if collapsed.
    -"Select Button" is clicked, the item is selected.
    -The border inside the "Select Button" is there for double clicks. That is done by selecting the border and under its events, create binding.
    These click events calls event dispatchers.

    Click image for larger version  Name:	Treeview-CustomExpansion.png Views:	1 Size:	178.9 KB ID:	1625304

    Inside the widget that holds the treeview, we simply bind to the dispatchers during the "on entry generated" event.

    Click image for larger version  Name:	Treeview-CustomExpansion2.png Views:	1 Size:	214.7 KB ID:	1625305
    The result is that the entry widget do not receive the clicked events, thus does not change the expanded state.
    We manually change the expanded state depending on our needs.
    In this case, the border with the text (which is held by a button) can be clicked on to only select the item, no change in expanded state.
    The visible button can be clicked to change the expanded state only, without selecting the item.
    Last edited by ste1nar; 05-29-2019, 07:16 AM.

    Comment


      #3
      Wow, great job writing this up!

      I struggled when working with the TreeView about a month ago. I eventually figured it out, but it became clear to me that there was insufficient documentation out there to make things easy for first-time users of these widgets.

      If I had only waited a month, I would have had a much easier time, I think, thanks to your excellent tutorial here.

      So, thanks for this, even if I missed the chance to maximally benefit from it!

      Comment


        #4
        Hey, thank you.

        While this is about getting introduced to working with the listview, if anyone wonders how to do anything specific, feel free to ask. For example, if the listview listed actors based on distance. Sure that can be done, but it is not a listview thing to handle, rather you would implement your own algorithm to do the sorting of items and then update the listview.

        Feedback is also welcome, if some parts were unclear or if something specific should be mentioned. A large part of this guide tries to explain why some things -don't- work rather than just showing a setup of a listview that works. I feel that that wouldn't actually teach anything.
        I'd be especially interested to hear from someone who had never used a listview prior.
        Last edited by ste1nar; 03-22-2019, 08:03 PM.

        Comment


          #5
          ste1nar

          This tutorial was really great. It helped me figure out the last bit of opacity on the side of how-do-list-views-work.

          I have a specific problem in a EditorUtilityWidget I am making.

          I create a list view of certain actors in the level. Everything is great about it unless I delete one of the list-represented actors from the level the manual way-- selecting it and hitting the DEL key.

          This causes the whole editor to barf with an error about :

          LogSlate: Warning: WidgetMapToItem length (6) does not match ItemsWithGeneratedWidgets length (5). This is often because the same item is in the list more than once in ListViewT<ItemType>[ListViewBase.h(219)]. Diagnostics follow.

          The diagnostics show what would be expected from the error description... all the Items and Widget Map entries match up, but there is that one less object in the scene.

          I can't figure out what Unreal wants me to do to intercept this event.

          I tried clearing, setting and regenerating the list bound to the various item release events on the listview and listitem widgets, and tried to bind to OnDestroy and OnEndPlay on the object. But I can't figure out what event, at design time, will prevent slate from discovering my listview is out of sync.

          This particular functionality is a sanity check deliberately meant to detect when the user tries deleting an object that shouldn't be manually deleted from a set of objects.

          Any idea on that?

          Thanks!

          Comment


            #6
            @JDStrawesome

            I haven't used EditorWidgets yet, I created one just now to see if I could get it working. unfortunately, I could not. No matter what, when the listview needs updating (whether that is by: clear list items, Set List Items (with an updated array), regenerating, scrolling with the deleted item represented), it crashes. The only thing I noticed was that if the Item was not a displayed item, it could be deleted and when scrolling to it, there would be an "empty" entrywidget (without telling the listview to update).
            I now also noticed that if the Item is NOT currently displayed, it can be deleted and the listview updated. But the real issue is of course the displayed items.


            I did have better luck with the treeview though, if that could be a temporary solution while the listview is not working.
            The idea behind the treeview is that the widget itself is the only manually added Item. When the Treeview runs the "On Get Item Children", you just provide the array of the items you are manually tracking.
            I haven't noticed any glaring problems using the treeview, but there certainly could be.

            Click image for larger version  Name:	EditorTreeview.png Views:	1 Size:	344.4 KB ID:	1608926
            Last edited by ste1nar; 04-17-2019, 10:49 AM.

            Comment


              #7
              ste1nar

              I solved this.

              I combed through source code, and it just seems like when you delete an actor from the level, it's presence is still referenced... probably for the sake of the undo stack.

              Because of that, actors-as-items in a utility widget running at design time will crash the editor every time because you end up with a duplicate item entry in ItemToWidgetMap but not in the WidgetToItemMap and GeneratedWidgetsblahblah list.

              To solve this, I made a data object that only holds a reference of the actor it represents... I instantiate and pass these objects to the listview as items. During EventOnListItemObjectSet, the object passes its reference of the actor to the list item widget so that it can populate all the informational widgets.

              The actor handles all its own original data.

              The proxy object simply acts as a stand-in or handshake.

              Works like a charm... and exposes that the listview widget seems to work in utility widgets as intended if it weren't for this special behavior in level actors.
              Last edited by JDStrawesome; 04-18-2019, 02:54 PM.

              Comment


                #8
                Interesting, well done. I did submit a bug report about the issue, https://issues.unrealengine.com/issue/UE-73014 so perhaps next update it will just work.

                Comment


                  #9

                  Great tutorial, it would be better if there was a small example´╝ü

                  Comment

                  Working...
                  X