ShinyMK ShinyMK - 5 months ago 97
Vb.net Question

VB.NET - Customizing Listview/Listbox

So there are a few Answered questions but I still dont really understand what i'm meant to do. Like how can I basically make my Listview very customized.

What im trying to get it to look like is:

1

As you can see it's HEAVILLY customized - I made this in Photoshop if you were wondering and I dont EXACTLY need the Scrollbar customized or that Hover-Highlight effect as of right now but in the Future a way to do it would be needed.

Any ideas how I can achieve this?

EDIT: The closest thing I can get it to is:
2
But theres a "FEW" issues - I cant get the Text alignment/position to look right as I need to put 2 spaces before the Text for it to actually look decent which "MIGHT" give issues for me in the future when trying to get actions on click. Next, There's for some reason no option to using Borders/Grids which remove the ability to do that slightly darker gray border around the Boxes in the first image. Any ideas guys?

(What I used to make it like this is: http://www.vbforums.com/showthread.php?599375-ListBox-with-custom-items-(colors-images-text-alignment))

Edit 2: Re-Edit: Ok I tought I got the Highlight Color sorted but it doesnt seem like it as im using FillRectangle but no matter how I put the Bounds (Which are correct) it seems to leave an extra like 1px border of White for about 1-2 seconds before its refreshed and gone. Anyway to fix this?

Edit 3: The http://www.vbforums.com/showthread.php?599375-ListBox-with-custom-items-(colors-images-text-alignment) ListBox I was using where I "ALMOST" got it done wont work for me due to it just not being stable - If I edit anything after making the ColorListBox in the Design mode it will just not work and give me errors which is annoying. It also has the "SelectedItem" parameter as no longer an Object which ruins half of my code. Other then that if those 2 are fixable I guess it would work but I have no idea how to fix it :(

So I reverted to a VERY basic ListBox for now with just text until you guys can help find a way to customize it like the first image above.

Answer

It is not at all clear what you want exactly. Like how can I basically make my Listview very customized is vague and very broad (and why you didn't get an answer you liked).

A picture is nice, but words describing the desired appearance would have been better. Given a picture to scrutinize, there is no way to know what is Important and what is just there...because. I assume every detail is important based on very customized. Additionally:

  • Listview/Listbox Pick one: they are very different controls.
  • The little 2 pixel margin between items would be very problematic for a ListView, so I scratched the LV1.
  • Why are all the items gray in the first image? That typically indicates disabled, so can items be disabled (such as when there is no program data for a given channel)?
  • What does the gray box on "FOX" represent? Is it the selected item? Is it the HotLight/MouseHover item? ListBox/ListView items do not normally light up when the mouse passes over (the LV can when HotTracking is true, but thats only when the mouse is over the Item not subitem(s)).
  • Is the darker gray for that box rectangle an absolute color? The HotLight and Selected coloring is usually handled by the OS (==Operating System) respecting the theme and color choices of the user. Is this gadget supposed to ignore those, or is your theme using some shade of gray for Highlight ?

I measured the elements in the first image carefully to get some metrics, then made guesses on the answers to the above.

No Code Solution

Use a Button. Since the user will presumably click one of these to select a desired channel, a Button makes more sense than a ListView (much more). You can display an image on a Button along with text, and use the FlatAppearance properties to style it how you want. Use the Tag to track the channel ID or the index of the related channel in the collection.

Finally, use the MouseHover and MouseLeave events to manipulate FlatAppearance.BorderSize and FlatAppearance.BorderColor to get very close to the first image in the question:

enter image description here

They reside in an scrolling Panel. The panel width is just a bit wider than the control to avoid the horizontal scrollbar. As for the buttons, the HotLight border (???) is around the entire control rather than just the text. Not what your picture shows you want, but on the other hand, there is nothing involved beyond some standard properties and a little event handling code (5-6 lines).


Image ListBox

An ownerdraw ListBox will get you a bit closer to what you want, but ultimately this just makes it look like a ListBox that has Buttons in it.

Place a ListBox on the form, and set these properties:
- DrawMode = OwnerDrawFixed
- IntegralHeight = False
- Itemheight = 64 (this is based on the fact that the images are 60x60 in the first image)
- Set the BackColor to ControlLight or {233,233,233} for that exact shade of gray as desired.

As mentioned, ListBox items do not normally light up when the mouse is over them, so we need some code to track that (like a Button):

Private mouseItem As Int32 = -1
Private Sub lbChannels_MouseMove(sender As Object,
                                 e As MouseEventArgs) Handles lbChannels.MouseMove

    Dim ndx = lbChannels.IndexFromPoint(e.Location)
    ' test to avoid millions of paints
    If ndx <> mouseItem Then
        If mouseItem <> -1 Then
            ' invalidate/redraw the OLD itemrect 
            lbChannels.Invalidate(lbChannels.GetItemRectangle(mouseItem))
        End If
        mouseItem = ndx
        ' invalidate/redraw the NEW itemrect 
        lbChannels.Invalidate(lbChannels.GetItemRectangle(mouseItem))
    End If

End Sub

Private Sub lbChannels_MouseLeave(sender As Object,
                                  e As EventArgs) Handles lbChannels.MouseLeave
    If mouseItem <> -1 Then
        ' get rect for what wont be the hot item in a tick
        Dim rect = lbChannels.GetItemRectangle(mouseItem)
        'lbChannels.Invalidate()
        mouseItem = -1              ' no longer Hot
        lbChannels.Invalidate(rect)
    End If
End Sub

Updated to minimize flickering by redrawing just the items which changed.

I did not use the MouseHover event because that fires later, resulting the small (but no where near a second) delay mentioned. MouseMove also makes it easier to get the mouse location.

IImageItem

Based on comments, you want to auto-generate items based on some other data in the app. Rather than assume what that class looks like and to make this easy to implement and applicable to any class, this will use an interface:

Public Interface IImageItem
    Property ID As String      ' ???
    Property ItemImage As Image
    Property Text As String
End Interface

The draw code will require that interface be implemented so it can use those properties to draw your items. Id is an extra one to allow a way to link which ListBox item is selected or clicked from the collection (only needed if you add items to the ListBox -- not recommended). It could be extended to include an Enabled property to draw those differently, if needed. You'd implement this interface on your collection items class thusly:

Public Class ChannelItem
    Implements IImageItem

    Public Property ItemImage As Image Implements IImageItem.ItemImage
    Public Property Text As String Implements IImageItem.Text
    Public Property ID As String Implements IImageItem.ID
    ' + your existing properties

    Public Sub New(txt As String, img As Image, key As String)
        Text = txt
        ItemImage = img
        ID = key
    End Sub

    Public Overrides Function ToString() As String
        Return Text
    End Function
End Class

Note: ChannelItem is my demo version of whatever class you are using to track the channels. Just type Implements IImageItem on your class, press enter and the IDE will add the properties, set them before adding items to your collection.

DrawItem Code

Private Sub lbChannels_DrawItem(sender As Object,
                                e As DrawItemEventArgs) Handles lbChannels.DrawItem
    Dim lb As ListBox = lbChannels
    If e.Index < 0 Then
        TextRenderer.DrawText(e.Graphics, "", lb.Font, e.Bounds, lb.ForeColor)
        Return
    End If

    Dim iItem As IImageItem
    If TypeOf (lb.Items(e.Index)) Is IImageItem Then
        iItem = DirectCast(lb.Items(e.Index), IImageItem)
    Else
        TextRenderer.DrawText(e.Graphics, lb.Items(e.Index).ToString,
                              lb.Font, e.Bounds, lb.ForeColor)
        Return
    End If

    Dim imgRect As Rectangle = Rectangle.Empty
    Dim txtRect As Rectangle

    ' calc 
    If iItem.ItemImage IsNot Nothing Then
        imgRect = New Rectangle(e.Bounds.X + 1, e.Bounds.Y + 1,
                                 iItem.ItemImage.Width + 2, iItem.ItemImage.Height + 2)
    End If

    ' GetTextExtent
    Dim sz = TextRenderer.MeasureText("   " & iItem.Text, lb.Font)
    txtRect = New Rectangle(iItem.ItemImage.Width + 4, e.Bounds.Y + 1,
                         (e.Bounds.Width - iItem.ItemImage.Width) - 8,
                          e.Bounds.Height - 2)

    ' Draw Big Box around the text portion
    If e.Index = mouseItem Then
        Using pR As New Pen(SystemColors.ControlDark, 2), 
                brB As New SolidBrush(SystemColors.Window)
            e.Graphics.DrawRectangle(pR, txtRect)
            txtRect.Inflate(-1, -1)
            e.Graphics.FillRectangle(brB, txtRect)
        End Using
    ElseIf (e.State.HasFlag(DrawItemState.Selected)) Then
        ' ToDo: modify for whatever is desired for the selected item
        '        this is a guess/example
        Using pR As New Pen(SystemColors.Highlight, 2), 
                       brB As New SolidBrush(SystemColors.Window)
            e.Graphics.DrawRectangle(pR, txtRect)
            txtRect.Inflate(-1, -1)
            e.Graphics.FillRectangle(brB, txtRect)
        End Using
    Else
       ' could use a channel specific color for each BG
       ' just extend IImageItem
        e.DrawBackground()
    End If

    If iItem.ItemImage IsNot Nothing Then
        e.Graphics.DrawImage(iItem.ItemImage, imgRect)
    End If

    ' recalc TR for where the text really goes
    txtRect = New Rectangle(iItem.ItemImage.Width + 4, e.Bounds.Y + 1,
                         sz.Width + 2, e.Bounds.Height - 0)
    TextRenderer.DrawText(e.Graphics, iItem.Text, lb.Font, txtRect, lb.ForeColor)
End Sub

Again, it is not clear whether the darker gray box for "FOX" represents the SelectedItem,HotLight`/Hover item or even disabled. The code shows how/where to box the text for the first 2 cases. Modify as needed.

Usage

' a collection of ChannelItem objects (which implement IImageItem)
Dim channels As New List(Of ChannelItem)

' my fake data
channels.Add(New ChannelItem("More 4", My.Resources.SO_LVImg01, "M4"))
channels.Add(New ChannelItem("Channel 4", My.Resources.SO_LVImg02, "4"))
channels.Add(New ChannelItem("RTE One", My.Resources.SO_LVImg03, "RET1"))
channels.Add(New ChannelItem("FOX", My.Resources.SO_LVImg04, "Fox"))
...

It is all pretty straightforward, just substitute your class for ChannelItem (it must implement IImageItem). Rather than copy items into the items collection, you can use that collection as the DataSource:

lbChannels.DataSource = channels

SelectedValue will be a ChannelItem object boxed as System.Object, cast it back to get at all the related data such as a URL or Now Playing text.

The result:

enter image description hereenter image description here

In this case, the selected item has a SystemColor.Hightlight box (blue in my case), and the item the mouse is over has a gray box. There even appears to be the little 2px gap between each item as in the original image. This is the result of the image height being a bit less than the ItemHeight used.

Putting a border on the things would elide the need to do mouse over highlighting.


UserControl

If you want (nearly) total control over how these pseudo buttons appear, you should probably build a UserControl. Using a Label and PictureBox and just the normal events you could get it to do almost anything you want. You can slap one together in about 20 mins:

enter image description here

The Channel UserControls are contained in an autoscrolling Panel. They are basically a custom button, but allows you a great deal of control over the layout, behavior and appearance. Best of all, each would have its own distinct Click event.


1 The reason there are no gaps between items in the LV or LB is to make it easier for users to select an item. A gutter or gap opens the chance that the user could click there and get no result. Or depending on the implementation, your code would crash using a bad index.