Public Class ImageListView
    Inherits Control

    Private Const MARGIN_ITEM As Integer = 5
    Private Const MARGIN_ITEM_BOTTOM As Integer = 35
    Private Const MARGIN_SELECTION As Integer = 3
    Private Const MARGIN_SELECTION_BOTTOM As Integer = 30

    Public Event RetrieveListItem(sender As Object, e As ImageListView.RetrieveListItemEventArgs)
    Public Event SelectedIndexChanged(sender As Object, e As ImageListView.SelectedIndexChangedEventArgs)
    Public Event AfterLabelEdit(sender As Object, e As ImageListView.LabelEditEventArgs)
    
    Private WithEvents scrollBar As New VScrollBar
    Private WithEvents labelEdit As New TextBox

    Private itemIndex As Integer
    Private itemOffset As Integer
    Private itemImageSize As Size

    Private labelEditing As Boolean
    Private images As Cache

    Private _imageSize As Size
    Private _listSize As Integer
    Private _selectedIndex As Integer

    Public Property ImageSize As Size
        Set(value As Size)
            Me._imageSize = value
            Me.CalcItemImageSize()
        End Set
        Get
            Return Me._imageSize
        End Get
    End Property

    Public Property ListSize As Integer
        Set(value As Integer)
            Me._listSize = Math.Max(0, value)
            If Me._selectedIndex >= Me._listSize Then Me.SelectedIndex = -1
            Me.CalcScrollBar()
            If Me.labelEditing Then
                Me.labelEdit.Visible = False
                Me.labelEditing = False
            End If
            Me.Refresh()
        End Set
        Get
            Return Me._ListSize
        End Get
    End Property

    Public Property SelectedIndex As Integer
        Set(value As Integer)
            Me._selectedIndex = Math.Min(Math.Max(-1, value), Me._listSize - 1)
            Me.CalcScrollbarSelectionVisible()
            Me.Refresh()
            RaiseEvent SelectedIndexChanged(Me, New SelectedIndexChangedEventArgs(Me._selectedIndex))
        End Set
        Get
            Return Me._selectedIndex
        End Get
    End Property

    Public Sub New()
        'Eingenschaften des Controls setzten
        Me.BackColor = Color.White
        Me.DoubleBuffered = True

        'Eigenschaften der ScrollBar setzten
        Me.scrollBar.Parent = Me
        Me.scrollBar.Dock = DockStyle.Right
        Me.scrollBar.SmallChange = 20
        Me.scrollBar.Enabled = False

        'Eigenschaften der TextBox setzen
        Me.labelEdit.Parent = Me
        Me.labelEdit.BorderStyle = BorderStyle.FixedSingle
        Me.labelEdit.Visible = False

        'Felder initialisieren
        Me.itemIndex = 0
        Me.itemOffset = 0
        Me.labelEditing = False
        Me.images = New Cache(50)

        'Eigenschaften initialisieren
        Me.ImageSize = New Size(640, 480)
        Me._listSize = 0
        Me._selectedIndex = -1
    End Sub

    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        Try
            'Hier alle IDisposeable-Objekte disposen
            Me.scrollBar.Dispose()
            Me.labelEdit.Dispose()
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub

#Region "Control"
    Protected Overrides Sub OnPaint(ByVal e As System.Windows.Forms.PaintEventArgs)
        MyBase.OnPaint(e)

        If Me._listSize > 0 Then
            Dim g As Graphics = e.Graphics
            Dim y As Integer = -Me.itemOffset
            Dim i As Integer = Me.itemIndex

            While y < Me.Height And i < Me._listSize
                y += MARGIN_ITEM

                'Markierung für selektiertes Element zeichnen
                If i = Me._selectedIndex Then
                    g.DrawRectangle(Pens.CornflowerBlue, MARGIN_ITEM - MARGIN_SELECTION, y - MARGIN_SELECTION, Me.itemImageSize.Width + (MARGIN_SELECTION * 2) - 1, Me.itemImageSize.Height + MARGIN_SELECTION_BOTTOM)
                    g.FillRectangle(Brushes.LightBlue, MARGIN_ITEM - MARGIN_SELECTION + 1, y - MARGIN_SELECTION + 1, Me.itemImageSize.Width + (MARGIN_SELECTION * 2) - 2, Me.itemImageSize.Height + MARGIN_SELECTION_BOTTOM - 1)
                End If

                'Daten für Element holen
                Dim args As New RetrieveListItemEventArgs(i)
                RaiseEvent RetrieveListItem(Me, args)

                'Bild Zeichnen
                Dim img As Image = Me.images.GetItem(args.FileName)
                If img Is Nothing Then
                    g.FillRectangle(Brushes.WhiteSmoke, MARGIN_ITEM, y, Me.itemImageSize.Width, Me.itemImageSize.Height)
                Else
                    g.DrawImage(img, MARGIN_ITEM, y, Me.itemImageSize.Width, Me.itemImageSize.Height)
                End If

                'Label zeichnen
                y += Me.itemImageSize.Height + MARGIN_ITEM
                g.DrawString((i + 1) & " " & args.Label, New Font("Microsoft Sans Serif", 8.25), Brushes.Black, MARGIN_ITEM, y)
                y += MARGIN_ITEM_BOTTOM - MARGIN_ITEM

                i += 1
            End While
        End If
    End Sub

    Private Sub ImageListView_Resize(sender As Object, e As System.EventArgs) Handles Me.Resize
        Me.CalcItemImageSize()
        Me.CalcScrollBar()
    End Sub

    Private Sub ImageListView_MouseWheel(sender As Object, e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseWheel
        Me.scrollBar.Value = Math.Min(Math.Max(0, -e.Delta + Me.scrollBar.Value), Me.scrollBar.Maximum - Me.scrollBar.LargeChange + 1)
    End Sub

    Private Sub ImageListView_MouseDown(sender As Object, e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseDown
        Dim di As Integer = (Me.itemOffset + e.Y) \ (MARGIN_ITEM + Me.itemImageSize.Height + MARGIN_ITEM_BOTTOM)
        Me.Focus()
        Me.SelectedIndex = Me.itemIndex + di
    End Sub

    Protected Overrides Function ProcessCmdKey(ByRef msg As Message, keyData As Keys) As Boolean
        If keyData = Keys.Up Then
            Me.SelectedIndex = Math.Max(0, Me.SelectedIndex - 1)
            Return True
        ElseIf keyData = Keys.Down Then
            Me.SelectedIndex += 1
            Return True
        ElseIf keyData = Keys.PageUp Then
            Me.SelectedIndex = Math.Max(0, Me.SelectedIndex - 10)
            Return True
        ElseIf keyData = Keys.PageDown Then
            Me.SelectedIndex += 10
            Return True
        End If
        Return MyBase.ProcessCmdKey(msg, keyData)
    End Function
#End Region

#Region "ScrollBar"
    Private Sub scrollBar_ValueChanged(sender As Object, e As System.EventArgs) Handles scrollBar.ValueChanged
        If Me.scrollBar.Value > 0 Then
            Me.itemIndex = Me.scrollBar.Value \ (MARGIN_ITEM + Me.itemImageSize.Height + MARGIN_ITEM_BOTTOM)
            Me.itemOffset = Me.scrollBar.Value Mod (MARGIN_ITEM + Me.itemImageSize.Height + MARGIN_ITEM_BOTTOM)
        Else
            Me.itemIndex = 0
            Me.itemOffset = 0
        End If
        Me.EndEdit()
        Me.Refresh()
    End Sub
#End Region

#Region "LabelEdit"
    Public Sub BeginEdit()
        If Me._selectedIndex >= 0 Then
            Dim args As New RetrieveListItemEventArgs(Me._selectedIndex)
            RaiseEvent RetrieveListItem(Me, args)
            Me.labelEdit.Left = MARGIN_ITEM
            Me.labelEdit.Top = (Me._selectedIndex - Me.itemIndex) * (MARGIN_ITEM + Me.itemImageSize.Height + MARGIN_ITEM_BOTTOM) - Me.itemOffset + MARGIN_ITEM + Me.itemImageSize.Height + MARGIN_ITEM
            Me.labelEdit.Width = Me.itemImageSize.Width
            Me.labelEdit.Text = args.Label
            Me.labelEdit.Visible = True
            Me.labelEdit.Focus()
            Me.labelEdit.SelectAll()
            Me.labelEditing = True
        End If
    End Sub

    Private Sub EndEdit()
        If Me.labelEditing Then
            Me.labelEditing = False
            Me.labelEdit.Visible = False
            RaiseEvent AfterLabelEdit(Me, New LabelEditEventArgs(Me._selectedIndex, Me.labelEdit.Text))
            Me.Focus()
            Me.Refresh()
        End If
    End Sub

    Private Sub labelEdit_KeyDown(sender As Object, e As System.Windows.Forms.KeyEventArgs) Handles labelEdit.KeyDown
        If e.KeyData = Keys.Enter Then
            Me.EndEdit()
            e.Handled = True
            e.SuppressKeyPress = True
        End If
    End Sub

    Private Sub labelEdit_LostFocus(sender As Object, e As System.EventArgs) Handles labelEdit.LostFocus
        Me.EndEdit()
    End Sub
#End Region

#Region "Calc"
    Private Sub CalcItemImageSize()
        Dim w As Integer = Me.Width - (2 * MARGIN_ITEM) - Me.scrollBar.Width
        Dim h As Integer = w / (Me._imageSize.Width / Me._imageSize.Height)
        Me.itemImageSize = New Size(w, h)
    End Sub

    Private Sub CalcScrollBar()
        Me.scrollBar.LargeChange = Me.Height
        Me.scrollBar.Maximum = ((MARGIN_ITEM + Me.itemImageSize.Height + MARGIN_ITEM_BOTTOM) * Me._listSize)
        Me.scrollBar.Value = Math.Min(Math.Max(0, Me.scrollBar.Value), Me.scrollBar.Maximum - Me.scrollBar.LargeChange + 1)
        If Me.scrollBar.Maximum <= Me.scrollBar.LargeChange Then
            Me.scrollBar.Enabled = False
        Else
            Me.scrollBar.Enabled = True
        End If
    End Sub

    Private Sub CalcScrollbarSelectionVisible()
        If Me._selectedIndex >= 0 Then
            Dim h As Integer = MARGIN_ITEM + Me.itemImageSize.Height + MARGIN_ITEM_BOTTOM
            If Me.itemIndex >= Me._selectedIndex Then
                Me.scrollBar.Value = h * Me._selectedIndex
            ElseIf Me.itemIndex + ((Me.itemOffset + Me.Height) \ h) - 1 < Me._selectedIndex Then
                Me.scrollBar.Value = (h * (Me._selectedIndex + 1)) - Me.Height
            End If
        End If
    End Sub
#End Region

#Region "Cache"
    Private Class Cache
        Private c As New Dictionary(Of String, Tuple(Of Boolean, Image))
        Private cl As New List(Of String)

        Private size As Integer

        Public Sub New(size As Integer)
            Me.size = size
        End Sub

        Public Function GetItem(key As String) As Image
            Dim val As Tuple(Of Boolean, Image) = Nothing
            c.TryGetValue(key, val)
            If val Is Nothing Then
                'Miss: hinzufügen
                AddToCache(key)
                Return Me.c.Item(key).Item2
            ElseIf val.Item1 = False Then
                'Hit: noch nicht fertig geladen
                Return Nothing
            Else
                'Hit: Bild zurückgeben
                Return val.Item2
            End If
        End Function

        Private Sub AddToCache(key As String)
            If cl.Count >= size Then
                'Kein freier Platz mehr vorhanden
                Dim oldKey As String = Me.cl.First()
                Me.cl.RemoveAt(0)
                Me.c.Item(oldKey).Item2.Dispose()
                Me.c.Remove(oldKey)
            End If
            Me.cl.Add(key)
            Me.c.Add(key, New Tuple(Of Boolean, Image)(True, Bitmap.FromFile(key)))
        End Sub

    End Class
#End Region

#Region "Custom EventArgs"
    Public Class LabelEditEventArgs
        Inherits EventArgs

        Private _index As Integer
        Private _label As String

        Public ReadOnly Property Index As Integer
            Get
                Return _index
            End Get
        End Property

        Public ReadOnly Property Label As String
            Get
                Return _label
            End Get
        End Property

        Public Sub New(index As Integer, label As String)
            Me._index = index
            Me._label = label
        End Sub
    End Class

    Public Class RetrieveListItemEventArgs
        Inherits EventArgs

        Public Property FileName As String
        Public Property Label As String

        Public ReadOnly Property Index As Integer
            Get
                Return _index
            End Get
        End Property

        Private _index As Integer

        Public Sub New(index As Integer)
            Me._index = index
            Me.FileName = ""
            Me.Label = ""
        End Sub
    End Class

    Public Class SelectedIndexChangedEventArgs
        Inherits EventArgs

        Private _index As Integer

        Public ReadOnly Property Index As Integer
            Get
                Return _index
            End Get
        End Property

        Public Sub New(index As Integer)
            Me._index = index
        End Sub
    End Class
#End Region

End Class
