CodeSnippet: Lets make us a Longhorn

15 November 2007 , ,    1 Comment

longhorn.jpg

Introduction

When Vista’s codename was still Longhorn, there were grand plans which never eventuated such as WinFS, the global notifications system, and NGSCB, which would have all been rather significant features to the operating system. However, some of the more disappointing features that were dropped (that the general public would have noticed) were to do with the UI. Many elements of the system (such as Explorer) utilised WinFX (which became WPF) – thats right, Explorer was coded in .NET (which in itself is very interesting, such as why was it .NET? why is Vista’s Explorer not .NET? What was the realistic performance of it?)

If you compare the animations between Vista’s Explorer and those we’re able to see in the Longhorn preview video above (and this isn’t necessarily the best Longhorn preview video out there), it is easy to see that WPF is capable of some very cool thing, but again, it is disappointing to see that Microsoft isn’t “dog-fooding” such capabilities (except ironically in XAML tools such as Expression Blend and Design). This is emphasised when you look at some of the new applications coming out from Microsoft, such as those under the “Live” brand. Live Photo could have very well been Phodeo, which would have actually been a great application to demonstrate some “wow” with Vista.

Now that my rant is out of the way, this CodeSnippet will be about recreating the “Music” view from Explorer in the video above (1:22 to 1:25, so not much to really go on, but enough to get something pretty). This will be done using WPF, both its 2D and 3D elements, and API calls to Windows Media Player (so, if you don’t already use WMP, you’ll need to add one or two items to its library otherwise you wont’ see anything appear).

mediahorn

Download

Source
Exe

Requirements

The Interface

Because Blend is particularly good at prototyping interfaces, I started with piecing together roughly what I wanted it to look like. If you’ve never used Blend before, it is much like Visual Studio’s visual interface editor, but with a dark (by default) theme, and somewhat more powerful for creating custom interfaces. If you are unfamiliar with Expression Blend, the included help file is actually pretty useful – its what has taught me all I know about Blend!

For the time being, lets just use a boring 2D element for the large album art view. Later on we’ll replace this with a 3D object created using Zam3D, exported to XAML.

mediahorn_stage

The Logic

Switching over to Visual Studio, open up the project. Since we’re working with WMP’s library, we need a reference to wmp.dll, its under COM under the Add Reference Dialog, Windows Media Player (make sure you select wmp.dll, not msdxm.dll). Any time we need to access WMP, don’t forget to add a “Using WMPLib;

The general idea is to let WPF’s databinding power do as much work as possible, so we’ll need to arrange our data in a particular way. This isn’t entirely necessary to get the concept working, but in the long run its better practice and allows further flexibility in the program.

classdiagram

public class Albums
{

    private List<String> TempAlbumList = new List<String>();

    private ObservableCollection<Album> albumList;

    public ObservableCollection<Album> AlbumList
    {
        get { return albumList; }
    }

    public Albums()
    {

        albumList = new ObservableCollection<Album>();

        WindowsMediaPlayer wmp = new WindowsMediaPlayer();
        IWMPPlaylist playlist = wmp.mediaCollection.getByAttribute("MediaType", "Audio");
        for (int i = 0; i < playlist.count; i++)
        {

            IWMPMedia tempPl = playlist.get_Item(i);
            String artist = tempPl.getItemInfo("AlbumArtist");
            String title = tempPl.getItemInfo("Title");
            String album = tempPl.getItemInfo("Album");
            String sourceurl = tempPl.getItemInfo("SourceUrl");

            String alba = album + artist;

            if (!TempAlbumList.Contains(alba) && alba != "") {
                try 
                {
                    if (artist == "")
                        artist = tempPl.getItemInfo("Artist");
                    albumList.Add(new Album(album, artist, Path.GetDirectoryName(sourceurl)));
                    TempAlbumList.Add(alba);
                }
                catch (Exception e)
                {
                   // MessageBox.Show(e.Message + " on " + album +title + sourceurl);
                }
            }

            for (int j = 0; j < albumList.Count; j++)
            {
                if (albumList[j].Title == album)
                {
                    albumList[j].AddTrack(new Track(title, sourceurl));
                    break;
                }
            }
        }
    }
}

ObservableCollections are much like any other generic collections, except WPF laps these up like there is no tomorrow. ObservableCollections don’t reside in System.Collections.Generic, however, they are in System.Collections.ObjectModel, so don’t forget to add a using statement. The reason ObservableCollections is loved by WPF so much is because it implements INotifyCollectionChanged, which allows for dynamic binding to UI elements (such as our ListBoxes) so that when we add, remove or modify anything in our collection, the UI is updated automagically.

public class Album
{
    private String title;
    public String dir;
    private String coverart;
    private String artist;
    private ObservableCollection<Track> trackList;
    public Album(String title, String artist, string dir)
    {
        this.title = title;
        this.dir = dir;
        this.artist = artist;

        this.coverart = GetArtwork();
        trackList = new ObservableCollection<Track>();
    }
    public String Title
    {
        get { return title; }
        set
        {
            title = value;

        }
    }
    public String Artist
    {
        get { return artist; }
        set
        {
            artist = value;
        }
    }
    public String CoverArt
    {
        get { return coverart; }
        set
        {
            coverart = value;

        }
    }
    public void AddTrack(Track t)
    {
        trackList.Add(t);
    }
    public ObservableCollection<Track> TrackList
    {
        get { return trackList; }
    }
    private string GetArtwork()
    {
        string filename = null;
        string[] filenames = Directory.GetFiles(this.dir, "AlbumArt*Large.jpg");

        if (filenames.Length > 0)
            filename = filenames[0];
        else
        {
            FileInfo file = new FileInfo(Path.Combine(dir, "Folder.jpg"));

            if (file.Exists)
            {
                filename = file.FullName;
            }
        }
        return filename;
    }
}
public class Track
{
    public String Title;
    public String path;

    public Track(String title, String path)
    {
        this.Title = title;
        this.path = path;
    }

    public override string ToString()
    {
        return Title;
    }
}

If this was production code, I’d implement INotifyPropertyChanged on both the Track and Album classes, so that the UI would automatically update when changes were made to any of the properties.

Now we switch over to XAML (Window1.xaml) – this can be done via Blend or Visual Studio (or even Notepad), but I find VS a bit more powerful/useful.

<Window.Resources>
    <local:Albums x:Key="Albums" />

    <!-- this style makes the format gridlike rather than just a list -->
    <Style x:Key="AlbumListStyle" TargetType="{x:Type ListBox}">
        <Setter Property="ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <WrapPanel />
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
    </Style>

    <!-- Change the artwork from text to actual images -->
    <DataTemplate x:Key="AlbumListTemplate">
        <StackPanel>
            <Border Margin="10,10,10,10">
            <Image Source="{Binding Path=CoverArt}" Width="50" Height="50"/>
            </Border>
        </StackPanel>
    </DataTemplate>

    <DataTemplate x:Key="TrackListTemplate">
            <TextBlock Text="{Binding Path=Title}" />
    </DataTemplate>

</Window.Resources>

By making local:Albums (note, to get the local prefix, add xmlns:local=”clr-namespace:MediaHorn” to the Window tag) available through the Albums key, we’re able to bind all our elements in XAML markup rather than in C#. In Albums constructor, it populates its albumList, because when the program launches, it immediately calls the class.

<ListBox
    x:Name="lbAlbums"
    Style="{StaticResource AlbumListStyle}"
    ItemsSource="{Binding Source={StaticResource Albums}, Path=AlbumList}"
    IsSynchronizedWithCurrentItem="True"
    ItemTemplate="{DynamicResource AlbumListTemplate}"
    SelectedIndex="0"
    MouseDoubleClick="lbAlbums_MouseDoubleClick" />

<ListBox
    x:Name="lbTracks"
    DataContext="{Binding ElementName=lbAlbums, Path=Items}"
    ItemsSource="{Binding Path=TrackList}"
    IsSynchronizedWithCurrentItem="True"
    MouseDoubleClick="lbTracks_MouseDoubleClick" />

<TextBlock
    x:Name="tbTitle"
    Text="{Binding Path=Title}"
    DataContext="{Binding ElementName=lbAlbums, Path=Items}" />
<TextBlock
    DataContext="{Binding ElementName=lbAlbums, Path=Items}"
    x:Name="tbArtist"
    Text="{Binding Path=Artist}"/>
<Image DataContext="{Binding ElementName=lbAlbums, Path=Items}"
       Source="{Binding Path=CoverArt}" />

The Dimensions of Three

Zam3D is a pretty cool little application by Electric Rain. It won’t win awards for sheer power in high end, bump mapped, megatextures; but then again…neither will WPF. If you are into higher end 3D modelling and animation, Zam3D will import from 3DS files (3D Studio Max), and Electric Rain have plugins for other 3D applications.

They’ve released four training/introductory videos, which are a little lengthy but are nice at getting your way around their program.

I’m not going to go into lengths about creating the 3D Model we need using Zam3D because that’s an article in itself. All we want is a box primitive, resized, slightly skewed, and animated such that it does 1.25 revolutions, and then at a slower rate rotates -.25 revolutions. Switch into the advanced editor, hit ‘edit mesh’, select ‘faces selection’ mode, and select the two triangles that make up the front of the box, and apply a bitmap texture. Save, export to XAML, and we’re done with Zam3D.

For simplicity sake, I cut and paste the entirety of the exported XAML file to inside the grid (below the listboxes), then moved it around in Blend.

We’re almost done with our WPF 3D, but there are a few things we need to adjust. First, find the texture you applied, and rename it to “AlbumArtTextureMR2” (making sure you rename all instances of it), as well as deleting the contents of that material group. Next, any time we add our own material, you might notice it is upside down, so find the object (it will be a GeometryModel3D, the name will be something meaningful like Box01OR10GR12, and add to it:

<GeometryModel3D.Transform>
    <Transform3DGroup>
        <RotateTransform3D >
            <RotateTransform3D.Rotation>
                <AxisAngleRotation3D Angle="180" Axis="0 0 1"/>
            </RotateTransform3D.Rotation>
        </RotateTransform3D>
        <ScaleTransform3D ScaleX="-1" ScaleY="1" ScaleZ="1"/>
    </Transform3DGroup>
</GeometryModel3D.Transform>

The placeholder we had for the 3D element had its source databound to the selected album. Unfortunately, MaterialGroup/ImageBrushes can’t be databound, so we’ll need to handle that in code.

public void onSelectAlbum(Object sender, EventArgs e)
{
    if (lbAlbums.SelectedIndex > 0)
    {
        Album temp = ((Album)lbAlbums.SelectedItem);

        if (temp.CoverArt != null)
        {
            BitmapImage img = new BitmapImage(new Uri(temp.CoverArt, UriKind.Relative));
            ImageBrush iB = new ImageBrush(img);

            MaterialGroup AlbumArtMaterial = (MaterialGroup)ZAM3DViewport3D.FindResource("AlbumArtTextureMR2");
            AlbumArtMaterial.Children.Clear();
            AlbumArtMaterial.Children.Add(new DiffuseMaterial(iB));
        }
((Storyboard)ZAM3DViewport3D.FindResource("OnLoaded")).Begin(this);

    }
}
 The line with the storyboard starts our animation every time a new album is selected.

Finishing Touches

To make it a proper media player/viewer, we need to actually allow audio playback. Again we can make use of WMPLib.

public void lbAlbums_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    if (lbAlbums.SelectedItem != null)
    {
        Album tempAlb = (Album)lbAlbums.SelectedItem;
        IWMPPlaylist tempPL = wmp.playlistCollection.newPlaylist(tempAlb.Title);
        foreach(Track t in tempAlb.TrackList)
        {
            tempPL.appendItem(wmp.newMedia(t.path));
        }
        wmp.currentPlaylist = tempPL;
    }
}
private void lbTracks_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    if (lbTracks.SelectedItem != null)
    {
        wmp.URL = ((Track)lbTracks.SelectedItem).path;
    }
}

These two functions provide playback for albums and individual tracks respectively. In the first function, we need to create a temporary playlist so that we can play more than one item, which is achieved through simple iteration.

In the second function, we only need to set the URL of the current file to play back a single track.

private void Window_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
{

    switch (e.Key)
    {
        case System.Windows.Input.Key.MediaStop:
            wmp.controls.stop();
            break;
        case System.Windows.Input.Key.MediaNextTrack:
            if (wmp.playState == WMPPlayState.wmppsPaused)
                wmp.controls.play();
            wmp.controls.next();

            break;
        case System.Windows.Input.Key.MediaPlayPause:
            if (wmp.playState == WMPPlayState.wmppsPaused || wmp.playState == WMPPlayState.wmppsStopped)
                wmp.controls.play();
            else
                wmp.controls.pause();
            break;
        case System.Windows.Input.Key.MediaPreviousTrack:
            wmp.controls.previous();
            break;

    }
}

Since we don’t have any media controls, I added play/pause/skip/stop through ‘media keys’ on keyboards (or some laptops that have ‘special’ function keys). This works for that, but the only problem is that it only works when the application has focus.

Conclusion

There are still bugs in this, but remember, it is more of a ‘proof of concept’ application that production ready. Several of the errors are derived from the WMP library which seems to keep a hold of albums that no longer exist, and thus while processing generate a few exceptions (so I’ve just chucked a few try/catch blocks around the offenders).

If you would like to further extend this, I’ve got a couple of ideas

 

References


Comments

One Comment

Trackbacks / Pingbacks

Leave a Reply