1

I'm trying to build a GUI similar to the one on the first screenshot: What I'm trying to achieve

Basically it's a view of tiles, displayed in a bordered grid that dynamically changes number of columns as the window is resized. The items are selectable. Those may look like pictures, but they are just Unicode characters.

I've made a List of custom objects, and bound the list to a ListBox control successfully. Naturally, this gave me a vertically stacked list of characters.

So I googled some, and found out I could set a custom ItemsPanelTemplate for the list box:

<ListBox.ItemsPanel>
    <ItemsPanelTemplate>
       <WrapPanel Margin="0"/>
    </ItemsPanelTemplate>
</ListBox.ItemsPanel>

The result was the following: Effect of using a WrapPanel

In order to set the border and size of the tiles, I've also added an ItemTemplate:

<ListBox Margin="0,0,0,0" Padding="0,0,0,0" Grid.Row="0" Grid.Column="0" x:Name="tw" 
            SelectionChanged="tw_SelectionChanged" 
            ScrollViewer.HorizontalScrollBarVisibility="Disabled" 
            ScrollViewer.VerticalScrollBarVisibility="Visible">
            <ListBox.ItemsPanel>
                <ItemsPanelTemplate>
                    <WrapPanel Margin="0,0,0,0"/>
                </ItemsPanelTemplate>
            </ListBox.ItemsPanel>
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <Label 
                        Padding="0,0,0,0"
            Margin="0,0,0,0"
                        Width="50" 
                        Height="50" 
                        FontSize="20" 
                        Background="White"
                        HorizontalAlignment="Left" 
                        HorizontalContentAlignment="Center"
                        VerticalContentAlignment="Center"
                        BorderThickness="1"
                        BorderBrush="Black"
                    Content="{Binding Path=LiteralString}"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

The result was the following: Using Labels for ItemTemplate This is as far as I got.

The first problem is, as you can see, there are spaces between the tiles that I can't get rid of. You'll notice in the markup I've tried manually setting Margin and Padding to 0 wherever possible. The spaces are still there.

The second problem is, you can't really see which item is selected. The 3rd character from the bottom and 2nd from the right is selected, but the blue color is covered by the label. You can only just see it in the space to the left of the character. I have no idea how to fix this.

Another potential problem is performance. The view will eventually have filters, but when all the filters are off, there are some 6500 items to display. When I had just a basic ListBox, the application was taking up some 28MB of RAM while it was running. When I added the ItemsPanelTemplate with the WrapPanel, memory usage jumped to about 136MB. When I added an ItemTemplate with Labels, memory usage jumped to 183MB, and what's worse, the window now takes much longer to load.

I've tried replacing Labels with TextBlocks, since I've read they're much more lightweight. The thing got faster, and memory usage dropped back to around 130MB, but TextBlocks are no good. I can't center text in them, or give them borders. And as you can see in the screenshot, the selected item just gets messed up: Using TextBlocks in ItemTemplate

Granted, I have a slow computer, and 183MB is not life-ending, but still, it's a bit inefficient. I intend to put a lot more functionality in this application, and if this one thing takes up so much RAM, the resulting application will likely be an unusable resource vampire. Also, I believe the application I'm mimicking is also written in .NET, (although I'm not sure if it's WPF) and that one only uses 17.5MB of RAM while displaying the view.

Is there another way to do this, one that's both more efficient and more easily customizable?

Thank you.

EDIT:

I have found a slightly better way. I removed the ItemTemplate altogether, and instead I'm using ItemContainerStyle:

 <ListBox.ItemContainerStyle>
                <Style TargetType="ListBoxItem">
                    <Style.Setters>
                        <Setter Property="BorderBrush" Value="Black"/>
                        <Setter Property="BorderThickness" Value="1"/>
                        <Setter Property="Width" Value="50"/>
                        <Setter Property="Height" Value="50"/>
                        <Setter Property="FontSize" Value="20"/>
                        <Setter Property="HorizontalContentAlignment" Value="Center" />
                        <Setter Property="VerticalContentAlignment" Value="Center" />
                    </Style.Setters>
                    <Style.Triggers>
                        <Trigger Property="IsSelected" Value="True" >
                            <Setter Property="FontWeight" Value="Bold" />
                            <Setter Property="Background" Value="SteelBlue" />
                        </Trigger>
                    </Style.Triggers>

                </Style>
            </ListBox.ItemContainerStyle>

This produces much better results. There are no visible spaces between the tiles, and I can customize the look of both selected and deselected tiles. Using ItemContainerStyle

The only thing I don't know how to do is have a border line with the thickness of 1 pixel. I get two lines with thickness of 1 touching, resulting in a thickness of 2. I've tried setting margins and padding to 0 and -1. No luck. Curiously, setting BorderBrush to Black and BorderThickness to 0.5 doesn't produce a combined black line with thickness of 1, but a grey line with thickness of 2.

Memory usage dropped somewhat to about 150MB compared to when I was using labels, but load times are still atrocious, so I'm still looking for a better solution.

EDIT 2:

I played with the margins for a while, and I finally nailed it. I got the correct border thickness by setting the margin of the ListBoxItem to -0.5, squishing the tiles ever so slightly together. I would like to keep the question open, so that hopefully somebody will present a more efficient solution. The UI is responsive once it's running, but the load time is terrible. During the use of my application the ListBox will be repeatedly re-filled, as filters are adjusted and queries are made. I would like that to happen quickly. Also, memory usage is unnecessarily high in my opinion.

Shaggydog
  • 3,456
  • 7
  • 33
  • 50
  • You can also remove Margin/Padding from ListBoxItem that generates for all items. Your Label is in ListBoxItem. In Internet you can find VirtualizingWrapPanel. I'm not sure if free versions of them are so great, but it can help however. – Spawn Nov 28 '15 at 20:55
  • Thanks for the comment. I've edited the question with my latest progress. I've tried messing with an ItemContainer, I'll try ListBoxItem next. I'll look at the VirtualizingWrapPanel too. – Shaggydog Nov 28 '15 at 21:33

1 Answers1

0

As i do not have enough rep to post a comment, will try to answer, or at least point to correct direction. 1) The reason you get Thickness of 2 is that adjacent borders join their respective thicknesses. And -0.5 Margin you use makes them intersect making resultant thickness of 1. Instead you can try to set border thickness of Right and Bottom side, like:

<Setter Property="BorderThickness" Value="0,0,1,1"/>

Which means LeftSide=0, Top=0, RightSide=1, Bottom=1 thick. So leftmost items won't have left border, and topmost items won't have top border. And as a result adjacent borders will not marge, doubling their thickness.

2) And for excessive memory usage and overall slow performance. When you use WrapPanel for ItemsPanel you are discarding UI Virtualization. Because usually non UI computation doesn't take much time first consideration is UI performance. Try to load your data and make computations without UI, I think difference will be obvious. I suggest you use some sort of VirtualizingWrapPanel for ListBox ItemsPanel. Personally I use this one with corrections suggested by more experienced coders:

Original implementation

First correction

Second correction

Community
  • 1
  • 1
bamanow
  • 93
  • 7
  • Thanks, I'll look into the VirtualizingWrapPanel. I'm not sure what you mean by loading data and making computations without UI. I load my data from an XML file and put it into a list. This process is instantaneous and I involve the GUI only after the data is loaded - when I call ListBox.ItemsSource = MyList; However, I'll confess I don't know what UI Virtualization means. Tomorrow I'll study the links you provided, try to use the solution, and if it works I'll mark your answer as correct. – Shaggydog Nov 28 '15 at 23:24
  • UI virtualization is a technique when only visible items are rendered, and invisible items are "virtualized". Greatly simplified meaning: if you have 1000 items in your listbox and only 25 fit the screen, only they will be computed and rendered, others are rendered when you scroll or change window size. Result is a LOT less RAM usage and lighting fast UI responsiveness. Because framework doesn't have to keep all of the rendered items in memory. – bamanow Nov 28 '15 at 23:27
  • Pardon for misleading last sentence. It should have been: Because framework doesn't have to render and keep all of the UI representations of your items in memory. – bamanow Nov 28 '15 at 23:34
  • I've tried the VirtualizingWrapPanel. The application now loads instantly, and memory usage is back down to ~30MB. Thanks. – Shaggydog Nov 29 '15 at 19:42
  • No problem. Glad it helped. – bamanow Nov 29 '15 at 19:46