Introduction
This article presents a general purpose plug-in for Nullsoft Winamp which looks and behaves like Winamp. I assume that everyone is familiar with Winamp, the MP3 player designed by the excellent Justin Frankel and his team at Nullsoft. If not, go and check it out at http://www.winamp.com.
As you may or may not be aware, Winamp contains the facility to extend its features by writing 'Plug-ins' (Win32 DLLs). I have written a few plug-ins for Winamp but they were mostly based around plain vanilla CDialog
s. I had seen one or two other plug-ins which had windows which looked like the Winamp 'skinnable' (bitmapped) window. I therefore decided my plug-ins would look far better if my windows were also bitmapped.
The Winamp user interface has loads of other cool features besides bitmapping, all of which are surprisingly simple to implement once you know how. I worked out how to do it, and now I am releasing the source code so that everyone can do it.
The plug-in presented here shows the following:-
- Bitmapped Windows
- 'Skinable' Windows
- Docking to the Winamp Main Window
- 'Tracking' the Winamp Main Window
- Restrictive Resizing
Bitmapped Windows
So, here is the hard part of the plug-in; the bitmapped windows. Bitmapped windows have been knocking about for years now but not too many people use them. The CWinampWnd
presented here uses an extension to the CBitmap
class, called CBitmapEx
, which includes the following extensions:-
- Can load a bitmap from resources or from a file on disk.
- Contains functions to report the dimensions of the bitmap.
- Can draw itself given a
CWnd
pointer and some sizing information.
The source bitmap (shown below) is loaded from resources or disk as mentioned above.
As you can see, the bitmap comprises a series of smaller bitmaps which are combined to make the window. This bitmap is a copy of the Winamp Playlist Editor bitmap with some small alterations. The layout of the bitmap is managed by the CImageMap
class which simply has an array of CRect
s which contain the co-ordinates of each of the smaller bitmaps. The following shows the constructor for the CImageMap
class where the co-ordinates are set.
CImageMap::CImageMap()
{
m_ImageMap[0].SetRect(0, 0, 25, 20);
m_ImageMap[1].SetRect(26, 0, 126, 20);
m_ImageMap[2].SetRect(127, 0, 152, 20);
m_ImageMap[3].SetRect(153, 0, 178, 20);
m_ImageMap[4].SetRect(0, 21, 25, 41);
m_ImageMap[5].SetRect(26, 21, 126, 41);
m_ImageMap[6].SetRect(127, 21, 152, 41);
m_ImageMap[7].SetRect(153, 21, 178, 41);
m_ImageMap[8].SetRect(0, 72, 125, 110);
m_ImageMap[9].SetRect(126, 72, 276, 110);
m_ImageMap[10].SetRect(179, 0, 204, 38);
m_ImageMap[11].SetRect(0, 42, 25, 71);
m_ImageMap[12].SetRect(26, 42, 51, 71);
m_ImageMap[13].SetRect(52, 42, 61, 51);
m_ImageMap[14].SetRect(62, 42, 71, 51);
m_ImageMap[15].SetRect(52, 53, 60, 71);
m_ImageMap[16].SetRect(61, 53, 69, 71);
m_ImageMap[17].SetRect(72, 42, 97, 56);
m_ImageMap[18].SetRect(99, 42, 149, 56);
m_ImageMap[19].SetRect(72, 57, 97, 71);
m_ImageMap[20].SetRect(99, 57, 149, 71);
m_ImageMap[21].SetRect(150, 42, 158, 51);
}
Each CRect
is then addressable by using the following constants:-
#define TITLEBAR_LEFT_CORNER 0
#define TITLEBAR_MIDDLE 1
#define TITLEBAR_FILL 2
#define TITLEBAR_RIGHT_CORNER 3
.... etc
So, to draw say the left corner of the titlebar onto the screen, we simply use this code:-
m_Interface.Draw(FALSE, this, 0, 0, m_map.OffSet(TITLEBAR_LEFT_CORNER));
Where m_Interface
is a CBitmapEx
and m_map
is an instance of the CImageMap
class.
All aspects of the CWinampWnd
are drawn this way in the DrawInterface()
function which is called from OnPaint()
.
'Skinnable' Windows
As an extension to the bitmapped window, instead of simply reading the window bitmap from resources, the user can optionally place a bitmap in the same directory as the plug-in. The plug-in will attempt to read the file from disk before loading the internal bitmap. This allows users to create their own designs for the window appearance based on the template bitmap used in the resources. These are called 'skins'.
Docking
Wouldn't it be cool if you could get your plug-in window to 'snap' to the Winamp Main Window like the other windows do? Well, now you can.
The trick behind this is to do a custom dragging routine. When the title bar is left clicked on, a WM_LBUTTONDOWN
message is received on the plug-in window. We then use SetCapture()
to watch where the mouse is moved to, so we get WM_MOUSEMOVE
messages as the mouse is moved around. When the edges of the window are within the Snapping distance (usually 10 pixels) of the Winamp Main Window, we move the window, using SetWindowPos
, so that the plug-in window is touching the Winamp Main Window. If we are outside all of the Snapping regions, we just move the plug-in window with the mouse.
The code is very simple, but very tedious to replicate for all four sides of the Winamp Main Window. Luckily enough, you don't have to.
Tracking the Winamp Main Window
Docking the plug-in window to the Winamp Window is a straightforward, if a little monotonous, task. The docking works fine until you move the Winamp Window, then problems occur. As the plug-in window isn't aware of where the Winamp Main Window is, it will be left behind when the Winamp Window is moved. We therefore need some way to 'track' the Winamp Window such that if we are docked to it, the plug-in window will move with it when the Main Window is dragged.
One way to do this would be to call GetParent()->GetWindowRect()
on a timer. This would allow the plug-in to keep track of where the Winamp Window is. However, this is a very CPU-intensive way of tracking Winamp. A better way to do it is to subclass the Winamp Main Window using SetWindowLong
.
pOrigProc = (WNDPROC)SetWindowLong(plugin.hwndParent,
GWL_WNDPROC, (LONG)HookWinampWnd);
The callback function now receives all of the Winamp Main Window's messages before Winamp does. This allows us to be informed when Winamp is moving by trapping the WM_MOVE
message. When this message is encountered, we call a TrackWindow
in the CWinampWnd
class to track the Main Window.
LRESULT CALLBACK HookWinampWnd(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
switch(uMsg)
{
case WM_MOVE:
m_MainWnd.TrackWindow((int)(short) LOWORD(lParam),
(int)(short) HIWORD(lParam));
break;
default:
break;
}
return CallWindowProc(pOrigProc, hwnd, uMsg, wParam, lParam);
}
Now, when Winamp is dragged, we get pulled along with it. Very cool.
Restrictive Resizing
Restrictive Resizing (I made this phrase up but it sounds technical enough), refers to allowing the window to be resized only in fixed size increments. This effect can be seen in the Winamp Playlist Editor. Grab the triangular shaped bit in the lower right corner and you will see the window can be resized but only in measured 'chunks'. The window also has a minimum size so that buttons and such are not lost off the window.
This is implemented in a very similar way to the docking code shown above. When the user clicks on the Resizing area of the window, SetCapture()
is called and the mouse is tracked. When WM_MOUSEMOVE
is called, the position of the mouse is read and compared with the position of the mouse when the resizing was initiated. If the mouse has moved more than the allocated size for the resize increment, the window is resized by this increment. If the mouse has not been moved far enough, the window remains the same size. The window is only resized if the mouse has moved beyond the next resize increment and the window is larger than its minimum size.
Again, very simple, but a neat addition to any user interface.
About the Demo
The demo shows a window derived from CWinampWnd
which simply displays the current song name and playing time. It is very simple but demonstrates how to use the CWinampWnd
class and I guess you can extend it to do whatever you wish. The Winamp IPC mechanism is very flexible so a lot more information can be gathered and displayed using it. Apologies in advance for the quality of the skin bitmap which is included with the demo. I have simply butchered the Winamp 'Playlist' skin and I have to admit that I am not much of an artist!
You will of course need Winamp to run the demo, and if you haven't got it, you should get it. It is an excellent example of original independent programming (well, it used to be independent).
Update: 6th June 2000
Fixed a memory leak spotted by Magnus Johansson. Thanks Magnus!