MusicLibrary SGrid Sample

Music Library Demonstration Application

This sample uses SGrid 2.0 along with the MP3 Tags code to load, display and persist a Music Library. It allows library import, sorting, grouping and in the future should be upgraded to include tag editing and play functions. It has been successfully used with over 7,000 tracks.

About The Music Library

This sample was created for two reasons: firstly, during development of SGrid 2.0, I wanted to be sure that it was going to provide the necessary performance to be a useful control. Secondly, I wanted something better than the useless MusicMatch which came with my iPod, and Media Player. (The licence for the latest version of Media Player appears to require you to accept that Microsoft are in complete control of your PC, you, your family and that you specifically allow Microsoft to use you at any point in time to perform menial and/or degrading tasks for them at your expense. This is surely reasonable since in return Microsoft gracefully offer no warranty, liability or any sort of responsiblity for any problems or issues that arise through use of the software, up to and including inadvertent triggering of nuclear war, famine, the end of the universe etc, etc.1).

Anyway, as it stands this application doesn't fulfil the second point (for a start, "playing" is just enqueueing to WinAmp, and there's no Find facility nor tag editing yet) but it is fine for the first.

Inside the Music Library

The application itself is a fairly simple implementation of the grid. The first thing it does is to set up a colour theme for the grid, using the default properties. You could use the Hue, Luminance and Saturation code here to provide a neat user option to customise these colours. The grid is set up with a moss-green theme, alternate rows being slightly highlighted and group rows having a darker shade. The highlight colour is set to white, and then this is alpha-blended with the background colour of the grid. Finally, the grid is set to multi-select row mode and hot-tracking.

   With grdLib

      .BackColor = RGB(80, 120, 100)
      .AlternateRowBackColor = RGB(84, 126, 105)
      .GroupRowBackColor = RGB(56, 84, 70)
      .GroupingAreaBackColor = .BackColor
      .ForeColor = RGB(243, 247, 245)
      .GroupRowForeColor = .ForeColor
      .HighlightForeColor = vbWindowText
      .HighlightBackColor = RGB(255, 255, 255)
      .NoFocusHighlightBackColor = RGB(200, 200, 200)
      .SelectionAlphaBlend = True
      .SelectionOutline = True
      .DrawFocusRectangle = False
      .HighlightSelectedIcons = False

      .HotTrack = True
      .RowMode = True
      .MultiSelect = True
   End With

Next, the columns are added. The control is configured to sort most tags alphabetically without case-sensitivity, with track number and year set to sort numerically (if these tags do not contain a number, the item will be sorted to the end of the list). The numeric fields are also set to right-align in the cells, and the last column is stretched to fit the available grid size (note that the StretchLastColumnToFit should ideally always be set after adding the columns, to prevent columns being incorrectly sized during set up).

   With grdLib
      ' Add the columns:
      .AddColumn "Title", "Title", _
          lColumnWidth:=192, eSortType:=CCLSortStringNoCase
      .AddColumn "Artist", "Artist", _
          lColumnWidth:=128, eSortType:=CCLSortStringNoCase
      .AddColumn "Album", "Album", _
          lColumnWidth:=192, eSortType:=CCLSortStringNoCase
      .AddColumn "Track", "Track", _
          eAlign:=ecgHdrTextALignRight, eSortType:=CCLSortNumeric
      .AddColumn "Genre", "Genre", _
          bVisible:=False, eSortType:=CCLSortStringNoCase
      .AddColumn "Filename", "Filename", _
          eSortType:=CCLSortStringNoCase
      .AddColumn "TagType", "TagType", _
          eAlign:=ecgHdrTextALignRight, bVisible:=False, _
          eSortType:=CCLSortStringNoCase
      .AddColumn "Year", "Year", _
          eAlign:=ecgHdrTextALignRight, bVisible:=False, _
          eSortType:=CCLSortNumeric
      .AddColumn "LastImportFileDate", "", , , , bVisible:=False
      .AddColumn "Comments", "Comments", bRowTextColumn:=True
            
      .StretchLastColumnToFit = True
   End With

The invisible "LastImportFileDate" is used to allow the library to track the file date of each MP3 file that is imported; this allows future imports to detect whether the tags need to be rechecked.

Once the grid is configured, we can load and save the library. For performance, the grid's internal persistence mechanism (LoadGridData and SaveGridData) is used. For a 7,000 track library, the data file is typically 3-4Mb, and loads and saves take around a second on an Athlon 2000XP machine. The code also calls the SelectionChange event after loading to ensure that the menus reflect no selection:

Private Sub loadLibrary()
Dim sFile As String
   sFile = musicLibFile()
   If (fileExists(sFile)) Then
      grdLib.LoadGridData sFile
      grdLib_SelectionChange 0, 0
   End If
End Sub

Private Sub saveLibrary()
Dim sFile As String
Dim sBackFile As String
   sFile = musicLibFile()
   If (fileExists(sFile)) Then
      ' Make a backup:
      sBackFile = musicLibBackupFile()
      killFileIfExists sBackFile
      FileCopy sFile, sBackFile
      killFileIfExists sFile
   End If
   If (grdLib.Rows > 0) Then
      grdLib.SaveGridData musicLibFile()
   Else
      killFileIfExists sFile
   End If
End Sub

Now data can be added to the grid. Data is added directly from the results of the cMP3ID3v1 or cMP3ID3v2 data for each MP3 file that's imported into the library, and the cell format is set to truncate the text and either align it right- or left- depending on the column type. Note that DT_PATH_ELLIPSIS is supported, which truncates file names whilst attempting to preserve as much of the information about the file as possible (for more information, see also the tip on compacting paths to fit a given space):

Private Sub processMp3File(ByVal sFile As String)
Dim c1 As New cMP3ID3v1
Dim c2 As New cMP3ID3v2
Dim sType As String
Dim lRow As Long
Dim dFileLastImport As Date
Dim dFileDateTime As Date
   
   dFileDateTime = FileDateTime(sFile)
   lRow = findMp3Row(sFile)
   If (lRow > 0) Then
      ' Check whether need to process this file or not:
      dFileLastImport = grdLib.CellText(lRow, 9)
      If (dFileDateTime <= dFileLastImport) Then
         ' unchanged
         Exit Sub
      End If
   Else
      ' Add a new row
      grdLib.AddRow
      lRow = grdLib.Rows
   End If
   

   c1.MP3File = sFile
   If (c1.HasID3v1Tag) Then
      sType = "1"
   End If
   
   c2.MP3File = sFile
   If (c2.HasID3v2Tag) Then
      If (Len(sType) > 0) Then
         sType = sType & ","
      End If
      sType = sType & "2"
   End If
     
   If (c2.HasID3v2Tag) Then
      grdLib.CellText(lRow, 1) = c2.Title
      grdLib.CellText(lRow, 2) = c2.Artist
      grdLib.CellText(lRow, 3) = c2.Album
      grdLib.CellText(lRow, 4) = c2.Track
      grdLib.CellText(lRow, 5) = c2.GenreName(c2.Genre)
      grdLib.CellText(lRow, 8) = c2.Year
      grdLib.CellText(lRow, 10) = c2.Comment
   ElseIf (c1.HasID3v1Tag) Then
      grdLib.CellText(lRow, 1) = c1.Title
      grdLib.CellText(lRow, 2) = c1.Artist
      grdLib.CellText(lRow, 3) = c1.Album
      grdLib.CellText(lRow, 4) = c1.Track
      grdLib.CellText(lRow, 5) = c1.GenreName(c1.Genre)
      grdLib.CellText(lRow, 8) = c1.Year
      grdLib.CellText(lRow, 10) = c1.Comment
   Else
      grdLib.CellText(lRow, 1) = fileNameOf(sFile)
   End If
   grdLib.CellText(lRow, 6) = sFile
   grdLib.CellText(lRow, 7) = sType
   grdLib.CellText(lRow, 9) = dFileDateTime
   
   grdLib.CellTextAlign(lRow, 6) = DT_LEFT Or DT_SINGLELINE Or DT_PATH_ELLIPSIS
   grdLib.CellTextAlign(lRow, 4) = DT_RIGHT Or DT_SINGLELINE Or DT_END_ELLIPSIS
   grdLib.CellTextAlign(lRow, 7) = DT_RIGHT Or DT_SINGLELINE Or DT_END_ELLIPSIS
   grdLib.CellTextAlign(lRow, 8) = DT_RIGHT Or DT_SINGLELINE Or DT_END_ELLIPSIS
      
End Sub

The import function uses the filename part of the file to import to match with an existing item in the grid; if it is already there the code checks the file date time against the last import file date time to confirm whether there has been a change not.

Once some data has been loaded, you can start sorting, grouping and working with the cells as the user selects them. Firstly, sorting. The grid responds to column click events and configures the SortObject to sort the data. Any groupings that have been configured in the grid are preserved in the SortObject by using the ClearNongrouped method to initialise the sort object so it only contains sort information about the grouped rows. Once a sort has been chosen, the column is used to cache the order and a header icon set to show the sort direction:

Dim iCol As Long
Dim iSortCol As Long

   With grdLib.SortObject
      .ClearNongrouped
      iSortCol = .IndexOf(lCol)
      If (iSortCol <= 0) Then
         iSortCol = .Count + 1
      End If
      
      .SortColumn(iSortCol) = lCol
      If (grdLib.ColumnSortOrder(lCol) = CCLOrderNone) Or _
         (grdLib.ColumnSortOrder(lCol) = CCLOrderDescending) Then
         .SortOrder(iSortCol) = CCLOrderAscending
      Else
         .SortOrder(iSortCol) = CCLOrderDescending
      End If
      grdLib.ColumnSortOrder(lCol) = .SortOrder(iSortCol)
      .SortType(iSortCol) = grdLib.ColumnSortType(lCol)
      
      ' Place ascending/descending icon:
      For iCol = 1 To grdLib.Columns
         If (iCol <> lCol) Then
            If Not (grdLib.ColumnIsGrouped(iCol)) Then
               If grdLib.ColumnImage(iCol) > -1 Then
                  grdLib.ColumnImage(iCol) = -1
               End If
            End If
         ElseIf grdLib.ColumnHeader(iCol) <> "" Then
            grdLib.ColumnImageOnRight(iCol) = True
            If (.SortOrder(iSortCol) = CCLOrderAscending) Then
               grdLib.ColumnImage(iCol) = 0
            Else
               grdLib.ColumnImage(iCol) = 1
            End If
         End If
      Next iCol
      
   End With
   
   Screen.MousePointer = vbHourglass
   grdLib.Sort
   Screen.MousePointer = vbDefault

Grouping is enabled either through drag-drop or through a context menu displayed when right-clicking on a column. Header right-clicks are notified to the application by the SGrid HeaderRightClick event, which passes the coordinates of the mouse click. You then use the ColumnHeaderFromPoint function to determine which (if any) column header was clicked; this function returns zero if the user clicked on a non-column part of the header control. Once the column is determined, the context menu is set up to show allowable options, including an option at index 3 to allow the column to be grouped or removed from the grouping and the popip menu is shown at the right point by adjusting the click coordinates to the coordinates of the form:

Private Sub grdLib_HeaderRightClick( _
       ByVal x As Single, ByVal y As Single _
   )
Dim lCol As Long
   
   lCol = grdLib.ColumnHeaderFromPoint(x, y)
   
   If (lCol > 0) Then
      mnuHeaderCtx(0).Enabled = True
      mnuHeaderCtx(1).Enabled = True
      mnuHeaderCtx(3).Enabled = True
      mnuHeaderCtx(3).Caption = IIf( _
         grdLib.ColumnIsGrouped(lCol), _
            "Don't Group By This Field", _
            "Group By This Field")
      mnuHeaderCtx(6).Enabled = True
            
      mnuHeaderCtx(0).Checked = _
         (grdLib.ColumnSortOrder(lCol) = CCLOrderAscending)
      mnuHeaderCtx(1).Checked = _
         (grdLib.ColumnSortOrder(lCol) = CCLOrderDescending)
      
   Else
      mnuHeaderCtx(0).Enabled = False
      mnuHeaderCtx(1).Enabled = False
      mnuHeaderCtx(3).Enabled = False
      mnuHeaderCtx(6).Enabled = False
   
      mnuHeaderCtx(0).Checked = False
      mnuHeaderCtx(1).Checked = False
   
   End If
      
   x = (x + grdLib.ScrollOffsetX) * Screen.TwipsPerPixelX _
      + grdLib.Left
   y = y * Screen.TwipsPerPixelY + grdLib.TOp

   ' Cache column in the tag of the menu:
   mnuHeaderCtxTOP.Tag = lCol

   Me.PopupMenu mnuHeaderCtxTOP, , x, y

End Sub

Finally, the ability to act on selected items in the grid control can be added. For this application, the available commands depend upon whether the selection is a single file, or multiple files. Selection of a group row is taken to mean selection of all items within the group. The grid's SelectionChange event is used to configure the application menus for the selection (although note there is a bug with multiple selections which means Selection Change isn't always fired when an item is added to the selection, so this is augmented by responding to MouseUp events too):

Private Sub grdLib_SelectionChange(ByVal lRow As Long, ByVal lCol As Long)
   
   Dim iSelCount As Long
   iSelCount = grdLib.SelectionCount
   
   If (lRow > 0) And (lCol > 0) Then
      
      If (grdLib.RowIsGroup(lRow) Or (iSelCount > 1)) Then
         ' Multi-selection logic here
      Else
         ' Single selection logic here
      End If
   Else
      ' No selection logic here
   End If

End Sub

Private Sub grdLib_MouseUp( _
      Button As Integer, Shift As Integer, _
      x As Single, y As Single)
   '
   ' TODO: bug for multi select: doesn't always raise selection
   ' changed...
   If (Button = vbLeftButton) Or (Button = vbRightButton) Then
      Dim lRow As Long
      Dim lCol As Long
      grdLib.CellFromPoint _
         x \ Screen.TwipsPerPixelX, y \ Screen.TwipsPerPixelY, _
         lRow, lCol
      grdLib_SelectionChange lRow, lCol
   End If

End Sub

Timing Data

Here are some timings taken from importing files an AthlonXP/2000 with 512Mb RAM from a 60Gb/5400rpm Western Digital Hard Drive (Athlon64/3400/SATA timings coming soon!). All tasks shown for 4,200 MP3 files:

Task Time (ms)
Import 3,500 MP3s 236,000
Load Library 298
Save Library 748
Sort on Title 72
Sort on Artist 64
Sort on Track 150
Group By Artist 364/188*
Ungroup By Artist 90
Group By Title 950/486*
Ungroup By Title 218

* - the first time that a group-by is performed, additional rows may need to be allocated in the grid. The timing therefore depends upon how many garbage allocated rows are available when grouping is done.

Conclusion

This article provides a Music Library sample for SGrid 2.0 which although incomplete provides a good starting point and also demonstrates the grid's performance.



1 - Some of these suggestions may be untrue