Click here to Skip to main content
15,868,039 members
Articles / Operating Systems / Windows
Article

Remote Control for Winamp - creating a skinnable Sidebar Gadget

Rate me:
Please Sign up or sign in to vote.
4.64/5 (32 votes)
15 Jul 2007CPL14 min read 405K   7.6K   60   127
This article shows several not-so-basic tasks involved in creating a skinnable Gadget - the Remote Control for Winamp - a small front-end to the world's favourite media player.

Sample Image - WinampRemote.jpg

About the Gadget

The Remote Control for Winamp is a Windows Sidebar Gadget that displays the current song title, position and volume level and allows you to navigate the playlist, control Winamp's volume and rewind or fast forward the current track. The main advantage of this gadget over other Winamp front-ends is that users can easily create their own skins and therefore modify the gadget's looks to suit their personal taste. The default installation contains skins that resemble the default Winamp Skins - Winamp Classic, Winamp Modern and their smaller versions: Compact Classic and Compact Modern.

Using the Remote Control for Winamp

  • Download the gadget
  • Unpack the archive to a temporary folder
  • Run install32.bat or install64.bat with administrator privileges (right-click the file and choose Run as administrator) - choose install32.bat for Vista 32-bit or install64.bat for Vista 64-bit
  • Right-click the Sidebar and add the gadget
  • Clicking the title area will run and/or show Winamp, so I suggest that you uncheck the Show Winamp in taskbar option in Winamp preferences and free up some space in the task bar
  • Scroll title in Windows taskbar option in Winamp preferences affects the Gadget title scrolling, too
  • If you want to create your own skin, create a new folder in the gadget's skins folder and place your skin files in this subfolder. Look at the sample "Winamp Modern" skin to see how the skinning works, it is very simple! Just create a skin.css file in /skins/{skin name}/ and modify the left, right, width, height and/or background-image attributes of elements #main, #title, #prev, #play, #pause, #stop, #next, #volume, #pos, #volumeBase and/or #posBase, like this:

CSS
#main
{
  width: 100px;
  height: 50px;
  background: url("myback.png");
}

#play { left: 30px; top: 10px; width: 20px; height: 20px; }
#play.up { background: url("myplay_up.png"); }
#play.down { background: url("myplay_down.png"); }

Introduction

A number of good articles are already out there about writing Gadgets for the Vista's new and shiny Windows Sidebar. These articles explain the basics and help you have your Gadget up and running quickly. For two such articles, look here and here.

From now on, I will assume that you have read and understood the Gadget basics. I will assume that you know what a Gadget is, that you can write the manifest, the HTML, the stylesheets and the scripts and that you know how to read and write Gadget settings. I will not explain the localization, the docking and undocking or the flyouts, discussed in the other articles already.

In this article, I will focus on the more interesting problems (at least for some, I believe) that I had to crack in the process of creating a not-so-basic Gadget - the skinnable Remote Control for Winamp. I will also show samples of using some of the Windows Sidebar objects, because I believe that their documentation is somewhat lacking at the moment.

In the end, we will have a working, fully skinnable gadget for controlling Winamp, as you can see in the preview picture.

Contents

Calling managed code

(or How to create and use a COM-visible .NET class)

The first thing I had to deal with was the way of controlling Winamp. The calls to the Winamp API themselves are easy. You first call FindWindow() to get a handle to Winamp's main Window and then send it commands using SendMessage() with the right parameters. The process has been described in many articles already and I will not go into details here. If you are interested, check out the Winamp SDK documentation.

The problem is that gadgets cannot make API calls directly. One way to solve this would be to create an executable that the gadget would use to make the API calls. The other way, which seems to be preferred by a majority of gadget developers, involves a COM-visible class that the gadget can instantiate and use. The latter method is preferable for many reasons, but the one important for us is that there is no nice and easy way for an executable to return a value other than an integer.

So, what we will do is create a COM-visible class in C# and then have our gadget instantiate it and call its methods to control Winamp. Why use managed code instead of C++ and Windows API? Because once you can call managed code from a gadget, the full power of .NET is at your gadget's service and that is what I want to show you here.

To create a COM-visible class in C# using Microsoft Visual Studio 2005 that can later be installed with regasm.exe:

  1. create a new "Class library" project
  2. Go to project Properties -> Application -> Assembly information and check Make assembly COM-visible
  3. Go to project Properties -> Build and check Register for COM interop
  4. Go to project Properties -> Signing and sign the assembly with a strong name
  5. Add attributes Guid, ClassInterface and ProgId to your class as shown below
  6. Make sure your class has a parameterless constructor

A simple class that can call Winamp->Play would look like this:

C#
using System;
using System.Runtime.InteropServices;

namespace WinampX
{
    [Guid("C13EFECD-C2FE-4503-BB55-F3A6C63D4D54")]
    [ClassInterface(ClassInterfaceType.None)]
    [ProgId("WinampX.WinampControl")]
    public class WinampControl
    {
        // Windows API constants
        const string WINAMP = "Winamp v1.x";
        const int WM_COMMAND = 0x0111;

        // Winamp API constants
        const int WA_PLAY = 40045;

        // import extern API methods
        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern IntPtr FindWindow(
            [MarshalAs(UnmanagedType.LPTStr)]
            string lpClassName,
            [MarshalAs(UnmanagedType.LPTStr)]
            string lpWindowName);

        [DllImport("user32.dll", CharSet = CharSet.Auto)]
        public static extern int SendMessageA(IntPtr hWnd,
                                              int wMsg,
                                              int wParam,
                                              uint lParam);

        // parameterless constructor
        public WinampControl()
        {
        }

        // push Play and return a bool indicating whether
        // the Winamp window was found
        public bool Play()
        {
            IntPtr hWnd = FindWindow(WINAMP, null);
            if (hWnd.Equals(IntPtr.Zero)) return false;
            SendMessageA(hWnd, WM_COMMAND, WA_PLAY, 0);
            return true;
        }
    }
}

This should be pretty straight-forward. The other Winamp calls can be written exactly the same way, just look at the source code if you can't figure out the details.

The next step is to compile the assembly into a DLL and register it on the target machine. This is a disadvantage compared to calling an executable - you have to register the assembly on the target machine. We will deal with this in the Packaging and deployment section of this article. For now, let's register the class manually, using regasm.exe tool. The tool can be found in the .NET Framework directory: %windir%\Microsoft.NET\Framework\{version}. To register the assembly, you have to execute the following command:

regasm.exe /codebase winampX.dll

The /codebase switch tells regasm to include DLL location information in the registry. It means that Windows will always look for this assembly at the exact location where you registered it. This technique is somewhat controversial, but it is simple and works nicely for our purpose. If you want to dig deeper into this issue, one point you can start at is this blog entry.

Once the assembly is registered, we can use it in our gadget's script like this:

JavaScript
var gWinamp = new ActiveXObject("WinampX.WinampControl");
var success = gWinamp.Play();

Well, that's it. You have just called .NET framework from your Gadget. From now on, your gadget can do just about anything you can program.

Update: An excellent article on .NET interop has just appeared on The Code Project

Accessing the user's hard drive

(or System.Shell.Folder.parse() does not work)

As powerful as calling an external library is, I think we will agree that it is something we don't want to do unless we have to. One of the things you normally cannot do in JavaScript is access the user's hard drive. Fortunately, in a gadget script, we can use objects not available elsewhere. I'm talking about the Windows Sidebar object model.

A full-blown reference of the Windows Sidebar objects can be found here.

One of the things I had to do when developing the Remote Control was to let the user pick a skin to use. What I wanted was basically to enumerate subfolders of my skins folder and display them in the gadget's settings Window as a set of radio buttons.

Image 2

This is how I did it:

JavaScript
// get the skins directory object
var dir = System.Shell.itemFromPath(System.Gadget.path + '\\skins').SHFolder;

// loop through all subfolders and files
for (var i=0; i<dir.Items.count; i++)
{
  if (dir.Items.item(i).isFolder)
  {
    // add a radio button
    skins.innerHTML = skins.innerHTML
                      + '<br /><input type="radio" name="skin" value="'
                      + dir.Items.item(i).name
                      + '">'
                      + dir.Items.item(i).name
                      + '</input>';
  }
}

What I do here is obtain a System.Shell.Folder object representing the skins directory and then access its children through the Items collection. This sounds simple enough until you try to figure it out yourself.

The first catch is that you need to use System.Shell.itemFromPath(path).SHFolder instead of System.Shell.Folder.parse(path) described in the Windows Sidebar object reference. Why? Because the .parse method does not work. I don't know why, but microsoft.com forums support this statement.

The second thing to note is that the System.Shell.Folder.Items collection is not an array and you can not access it using an indexer (Items[i]). You have to use Items.item(i) instead.

And one final thought for this section - try not to abuse the ability to write to the user's hard drive by storing stuff in the gadget's folder or its subfolders. The gadget might not always have write permission to these locations and then you would have a problem.

Common dialogs

(or Letting the user pick a file or folder path)

Next on our list is the need to ask the user for the path to Winamp executable. What we want to do here is add the well-know text box with a Browse button to the settings Window.

Image 3

The Browse button will display the standard Open file dialog box and write the selected file's path to the text box.

JavaScript
function onBrowse()
{
  var f = System.Shell.chooseFile(true, "winamp.exe:winamp.exe", ".", "");
  if (f) path.value = f.path;
}

<input id="path"></input>
<input type="button" value=" &lt;&lt; " onclick="onBrowse();"></input>

The System.Shell.chooseFile() method and its arguments are described here. The one thing I will point out is that I use if (f) to check whether the user has in fact picked a file or whether he just cancelled the dialog box.

Other common dialogs are displayed by calling System.Shell.chooseFolder() and System.Shell.saveFileDialog(), both described here.

Validating gadget's settings

(or Handling the System.Gadget.onSettingsClosing event)

When the user closes the settings window by clicking the OK button, we should probably validate the settings and alert him if the adjustments he made are invalid. In our particular case, we want to check whether the path to the Winamp executable is valid.

The place to validate settings is in the gadget's onSettingsClosing handler:

JavaScript
System.Gadget.onSettingsClosing = onCloseSettings;

function onCloseSettings(e)
{
  if (e.closeAction == e.Action.commit)
  {
    // show the "path invalid" error message
    pathValid.style.display = "block";

    // check path validity
    if (!System.Shell.itemFromPath(path.value))
    {
      // cancel the event
      return;
    }

    // hide the "path invalid" error message
    pathValid.style.display = "none";

    // accept the event
    e.cancel = false;
  }
}

The onSettingsClosing handler accepts one argument - the event object described here. To accept the event, you have to set its cancel property to false.

Image 4

When you decide to cancel the event, you should let the user know what is happening. What I did here was include a hidden error message div in the settings window HTML and toggle it's visibility when validating the settings.

Gadget transparency

(or Non-rectangular gadgets)

Gadgets support transparency, but there is one catch: a semi-transparent element cannot be placed directly on top of another transparent or semi-transparent element.

Gadget's background is represented by the gbackground object. We can set gbackground's property using both HTML and JavaScript:

JavaScript
<g:background src = "image" />

gbackground.src = "image";

If you want to make the background fully transparent, just set the .src to an empty string.

Contrary to certain articles, gadget transparency works just as good "inside" the gadget as it does on the borders.

The big one: Gadget skinning framework

(or Adding and removing external stylesheets dynamically)

Here comes the fun part. One of the coolest things about Winamp is that anybody can make their own Winamp skins and change the Winamp's appearance to suit their needs. I wanted to make the same thing possible for the Remote Control Gadget - to enable the user to easily change the appearance and position of user interface elements.

The process of creating a gadget skin should consist of drawing the new UI images, writing a script to set their positions and dropping those files to the skins folder. The user should then be able to choose the active skin in the gadget's settings dialog.

The gadget's graphical user interface is defined by a set of HTML pages and cascading style sheets. The keyword is "cascading". It means that you can combine two or more style sheets and have each new style sheet complement or alter the previous ones. If we manage to define both the appearance and the position and size of all gadget's UI elements in a style sheet, than we can have the user change it just by appending his own style sheet to the document.

This means, that all images and all element positions and sizes have to be defined in the CSS, not in the HTML. All user interface elements will be DIVs with a background image, position and size set in the style sheet. For example, the Play button will be defined as follows:

HTML
<span id="play" class="up"
  onMouseDown="onDown(this.id); return true;"
  onMouseUp="if (onUp(this.id)) onPlay(); return true;" >
</span>

The class of the button tells us whether it is currently pressed or not. This naturally changes on the onMouseDown and onMouseUp:

JavaScript
function onDown(button)
{
  document.getElementById(button).className = "down";
}

function onUp(button)
{
  document.getElementById(button).className = "up";
}

The onDown() and onUp() methods will be a little more complex in a real-life situation to handle situations like moving the mouse pointer away from the button with the mouse button pressed. If you want to see such implementation, look at the source code.

The last thing we need to do is give the button a default appearance:

CSS
#play
{
  position: absolute;
  left: 31px;
  top: 50px;
  width: 23px;
  height: 18px;
  overflow: hidden;
}

#play.up
{
  background-image: url("../images/ui/play.png");
}

#play.down
{
  background-image: url("../images/ui/play_d.png");
}

Now, if someone wants to, let's say, change the button's position, all he needs to do is apply a new, very simple style sheet like this one:

CSS
#play
{
  left: 10px;
  top: 20px;
}

So now we have the default stylesheet, a skins directory containing subdirectories with custom additional stylesheets and the user has picked a skin he wants to use as described in the Accessing the hard drive section of this article. The last thing we need to do is tell the gadget (at runtime) to combine the right style sheets and link them to the gadget's HTML. Here is what needs to be done:

  1. Retrieve a handle to the document's head section
  2. Delete all style sheets linked to the document
  3. Link the default style sheet to the document
  4. Link the custom style sheet to the document
  5. Resize the gadget's body to fit the new skin

And this is how we do it:

JavaScript
function applySkin()
{
  // retrieve document's HEAD section
  var head = document.getElementsByTagName("head")[0];

  // remove all stylesheets
  var sheets = document.getElementsByTagName("link");
  for (var i = 0; i<sheets.length; i++)
  {
    head.removeChild(sheets[i]);
  }

  // link the default style sheet
  link = document.createElement('link');
  link.href = 'styles/default.css';
  link.rel = 'stylesheet';
  link.type = "text/css";
  head.appendChild(link);

  // if a skin was selected, link the custom style sheet
  if (gSkin != '')
  {
    link = document.createElement('link');
    link.href = 'skins/' + gSkin + '/skin.css';
    link.rel = 'stylesheet';
    link.type = "text/css";
    head.appendChild(link);
  }

  // resize body
  document.body.style.width = main.offsetWidth + "px";
  document.body.style.height = main.offsetHeight + "px";
}

In the first four steps, we use the Document Object Model to locate and replace the stylesheets linked to the document. In the fifth step, we use the offsetWidth and offsetHeight properties of the top-level DIV to read the new size of the gadget and adjust the size of the document's body. Please note that these two properties are only part of Internet Explorer's DHTML object model, not the W3C's standard, but they are very useful, since they always return the element's size in pixels, unlike the element.style.width and element.style.height.

The last tricky part to creating a skinnable interface is resizing the Volume and Position sliders. If we want to let the user decide the range and size of a slider, we need to calculate the slider's relative position based on it's current range and position. To calculate the current volume on a range of 1 to 255, we perform the following calculation:

JavaScript
var vol = slider.offsetLeft/sliderRange.offsetWidth*255;

This way, the creator of a skin can resize and reposition the slider however he likes and it will still work as expected.

Gadget packaging and deployment the non-standard way

(or Registering an assembly automatically)

The standard way to deploy a Gadget is to zip pack all the gadget files and change the archive's file extension to .gadget. When the user double-clicks this .gadget file, Windows unpacks it and copies it's content to the right destination directory automatically.

The problem is that before running the gadget, we need to register our winampX.dll assembly. In other articles, the authors suggest to install the gadget the standard way, then close it, register the DLL manually and run the gadget again. I don't like that too much and I have tried a different approach.

The process of installing a gadget consists of copying the gadget files to the right subfolder in the user's profile folder and, in our case, registering the DLL. Both of these tasks can be done automatically and there is no need to have the user register anything manually. The install script can be written as a simple batch file:

xcopy /E /I WinampRemote.Gadget
            "%USERPROFILE%\AppData\Local\Microsoft
            \Windows Sidebar\Gadgets\WinampRemote.Gadget"

"%WinDir%\Microsoft.NET\Framework\v2.0.50727\regasm"
           /codebase
           "%USERPROFILE%\AppData\Local\Microsoft\Windows Sidebar
           \Gadgets\WinampRemote.Gadget\winampx.dll"

Please note that I have split long lines into several short ones, which is unacceptable in a real batch file.

The script does just what I have described above. It copies the gadget's directory to the Gadget directory in the current user's profile and then registers the DLL file. The uninstall script is just as easy.

Update: Another approach to registering the assembly is described in this article.

Final words

While creating a simple Sidebar Gadget is quick and simple, there is no limit to how deep and dirty you want to get. Gadgets are as small and simple on the outside as they are powerful on the inside and that is what good user interface is all about.

Please, share your thoughts (and skins) on this Gadget with me. I am looking forward to seeing any wishes or suggestions.

Version history

  • 2007/07/15 - Mouse wheel now controls volume
  • 2007/03/08 - New skin: WinaMP11 by n4yp.formator
  • 2007/02/26 - Fixed a nasty bug that caused a crash for some users (thanks FaxedHead)
  • 2007/02/10 - New installer for 64-bit systems (thanks lalarira)
  • 2007/02/07 - Two new skins - Compact Classic and Compact Modern
  • 2007/01/31 - Some minor fixes and updates
  • 2007/01/30 - Initial release

License

This article, along with any associated source code and files, is licensed under The Common Public License Version 1.0 (CPL)


Written By
Software Developer
Czech Republic Czech Republic
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Generalsome features you could add.... Pin
The Blind Mosquito4-Aug-07 14:51
The Blind Mosquito4-Aug-07 14:51 
AnswerRe: some features you could add.... Pin
Jaroslav Klima5-Aug-07 22:19
Jaroslav Klima5-Aug-07 22:19 
GeneralRe: some features you could add.... Pin
PaXaZzz10-Aug-07 14:41
PaXaZzz10-Aug-07 14:41 
GeneralGreat Gadget! Pin
kwxl14-Jul-07 14:03
kwxl14-Jul-07 14:03 
AnswerRe: Great Gadget! Pin
Jaroslav Klima15-Jul-07 8:46
Jaroslav Klima15-Jul-07 8:46 
GeneralRe: Great Gadget! Pin
kwxl15-Jul-07 9:23
kwxl15-Jul-07 9:23 
GeneralQuestion for developer Pin
ajazzscientist5-Jun-07 13:39
ajazzscientist5-Jun-07 13:39 
AnswerRe: Question for developer Pin
Jaroslav Klima5-Jun-07 22:39
Jaroslav Klima5-Jun-07 22:39 
GeneralCool gadget Pin
marcasselin1-May-07 2:34
marcasselin1-May-07 2:34 
GeneralRe: Cool gadget Pin
Jaroslav Klima1-May-07 4:33
Jaroslav Klima1-May-07 4:33 
GeneralRe: Cool gadget Pin
marcasselin1-May-07 6:05
marcasselin1-May-07 6:05 
GeneralRun As Admin Problem Pin
Xerott18-Apr-07 16:24
Xerott18-Apr-07 16:24 
GeneralRe: Run As Admin Problem Pin
Jaroslav Klima18-Apr-07 23:00
Jaroslav Klima18-Apr-07 23:00 
GeneralRe: Run As Admin Problem Pin
Xerott19-Apr-07 3:48
Xerott19-Apr-07 3:48 
GeneralProblem Pin
Tim Kersjes9-Apr-07 2:54
Tim Kersjes9-Apr-07 2:54 
GeneralRe: Problem Pin
Tim Kersjes9-Apr-07 4:39
Tim Kersjes9-Apr-07 4:39 
GeneralRe: Problem Pin
Jaroslav Klima9-Apr-07 5:41
Jaroslav Klima9-Apr-07 5:41 
GeneralInstallation Issues Pin
Crimjob23-Mar-07 9:02
Crimjob23-Mar-07 9:02 
GeneralRe: Installation Issues Pin
Jaroslav Klima23-Mar-07 9:26
Jaroslav Klima23-Mar-07 9:26 
GeneralRe: Installation Issues Pin
Crimjob23-Mar-07 14:29
Crimjob23-Mar-07 14:29 
GeneralRe: Installation Issues Pin
Jaroslav Klima23-Mar-07 14:37
Jaroslav Klima23-Mar-07 14:37 
GeneralAlbum Cover Art Pin
DrZeusNZ12-Mar-07 7:56
DrZeusNZ12-Mar-07 7:56 
GeneralGadget Skin Pin
n4yp.formator7-Mar-07 11:52
n4yp.formator7-Mar-07 11:52 
GeneralRe: Gadget Skin Pin
Jaroslav Klima7-Mar-07 12:47
Jaroslav Klima7-Mar-07 12:47 
GeneralRe: Gadget Skin Pin
n4yp.formator8-Mar-07 2:28
n4yp.formator8-Mar-07 2:28 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.