MusicLibrary SGrid Sample
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,
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
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:
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.