Aug 01 QT Toolkit
Volume Number: 17 (2001)
Issue Number: 08
Column Tag: QuickTime Toolkit
The Skin Game
by Tim Monroe
Working with QuickTime Skins
Introduction
QuickTime 5 introduced support for displaying movies inside of arbitrarily shaped windows. These windows are called skinned movie windows, and the custom shape of one of those windows is called its skin. Up to now, our sample applications have always displayed QuickTime movies inside a standard document window, which occupies a rectangular area on the screen. Even QuickTime Player, which uses a snazzy brushed-metal window frame with rounded edges, always shows a movie inside a rectangular pane inside the frame. Skins give us a way to break out of this rectangular mold. For instance, Figure 1 shows a QuickTime movie with a skin that's shaped like the QuickTime logo.
Figure 1: A QuickTime movie with a skin
This movie contains two video tracks, one for the grainy, grayscale video showing in the center of the logo, and one for the logo image itself. (The second video track contains a single sample that extends for the entire duration of the first track.) The user can start and stop the movie by pressing the spacebar or by clicking in the visible portion of the grayscale video. And the user can move the window around on the screen by clicking anywhere on the blue logo and dragging.
Figure 2 shows another possibility. Here is our penguin sprite movie once again, but this time as a skinned movie. It's still got a tween track that changes the sprite's graphics mode from total transparency to total opacity. But now I've set the looping mode to palindrome looping so that the penguin fades in and out as the movie plays.
Figure 2: Another QuickTime movie with a skin
Figure 3 shows yet another skinned movie window. Most of what you see here, including all the buttons and draggable handles, is provided by a Flash track. The grayscale image is once again a frame of a video track, which we can start, stop, pause, and play in slow motion using the tools palette on the right side of the movie window.
Figure 3: Yet another QuickTime movie with a skin
In this article, we're going to learn how to create skinned movies. More importantly, we're going to learn how to open a skinned movie file and display the movie to the user in a window of the appropriate shape. That is to say, we're going to learn how to make our applications skin savvy. Currently there are very few applications that can open skinned movies. The only widely-available application that can do this, to my knowledge, is QuickTime Player.
Our sample application in this article, which can both create skinned movies and play them back, is called QTSkins. The Test menu of QTSkins is shown in Figure 4; as you can see, it has only one menu item, which allows us to add a skin track to a movie.
Figure 4: The Test menu of QTSkins
Skins
Perhaps the best way to think of QuickTime skins is like this: a skinned movie is just a QuickTime movie with a custom window shape. A skin provides a way of selecting some portion of an existing movie and having that portion be all that's displayed to the user when the movie is opened. Skinned movies don't have title bars or window frames, and they don't display a controller bar. As a result, if we want the user to be able to interact with the movie, we'll need to supply our own controls. We can use wired sprite tracks or Flash tracks for this, or perhaps even wired text tracks (which we encountered in the previous article).
The data that defines the custom window shape is contained in the skinned movie file itself. This fact has some very important consequences. For one thing, it means that we can select on a per-movie basis whether a movie is displayed in a normal document window or in a custom-shaped skinned window. We're not modifying the general appearance of the playback application (which is perhaps the typical use of the term ‘skins'). Rather, we're modifying the specific appearance of what's being played back. In a nutshell, we're changing the movie, not the movie player. Previously, the movie data represented some content that plays back inside a document window or pane, usually under the supervision of a movie controller and controller bar. Now the movie data can represent the content and the window and the controller. For the first time, really, the movie author has complete control over the user's playback experience.
So what kind of data do we use to construct a skinned movie? The first thing we need is some way of specifying which portion of the movie rectangle we want to appear as the content region of the skinned movie window. The content region of a window is the portion of the window in which an application displays the contents of a document; in our case, it's where the movie data and any movie controls are displayed. We specify the skinned movie's content region by providing a 1-bit (that is, black and white) mask that's the same size as the movie rectangle. If a pixel in the mask is black, then the corresponding pixel in the movie rectangle is displayed; otherwise, the corresponding pixel is not displayed. Let's call this mask the content region mask. Figure 5 shows the content region mask for the skinned movie shown in Figure 1.
Figure 5: A content region mask
We also need some way to move the skinned movie window around on the screen. Typically, of course, we move a window by grabbing its title bar or window frame and then dragging. Because skinned movie windows don't have title bars or frames, however, we need to explicitly indicate the portion of the skinned movie window that the user can grab and drag. We do this by specifying a second mask, the drag region mask. (This is also a 1-bit mask.) A user can click anywhere in this region and drag the window around. Figure 6 shows the drag region mask for the skinned movie shown in Figure 1.
Figure 6: A drag region mask
You'll notice that the drag region mask is entirely contained within the content region mask, so that the user can grab only in some visible portion of the movie window. In addition, the drag region mask should exclude any areas of the movie rectangle that you want to be interactive. It won't do any good, for instance, to have a skinned movie's drag region overlap any wired sprites, since a click in that area will be interpreted as the beginning of a drag operation.
So we need three ingredients to create a skinned movie. We need the movie data itself. We need a content region mask, to indicate the portion of the movie rectangle that is displayed to the user. And we need a drag region mask to indicate the portion of the movie rectangle that can be grabbed.
Creating Skinned Movies
The typical way to create a skinned movie is to add a skin track to an existing movie. The skin track contains data that specifies the content region and the drag region of the movie window. In this section, we'll investigate two different ways to add a skin track to a movie. First, though, we'll take a brief moment to learn about media characteristics. This will help us see that skin data can in fact be contained in other kinds of tracks as well.
Searching Media Characteristics
Let's begin by considering a utility function we'll call several times in our application, QTSkin_IsSkinnedMovie (defined in Listing 1). This function returns a Boolean value that indicates whether the specified movie contains skin data.
Listing 1: Determining whether a movie is a skinned movie
Boolean QTSkin_IsSkinnedMovie (Movie theMovie)
{
return(GetMovieIndTrackType(theMovie, 1,
FOUR_CHAR_CODE(‘skin'), movieTrackCharacteristic)
!= NULL);
}
We've worked with GetMovieIndTrackType a handful of times previously, but only using the movieTrackMediaType flag as the last parameter, to search for a track of a given index that has a specific type. Here, you'll notice, we use the movieTrackCharacteristic flag instead, which tells GetMovieIndTrackType to search for a track of a given index that has a specific media characteristic. A media characteristic is a feature that can be shared by two or more track types, such as the ability to draw data. Originally, in QuickTime version 2.0, there were two supported media characteristics, indicating whether the track has video or audio data in it:
enum {
VisualMediaCharacteristic = FOUR_CHAR_CODE(‘eyes'),
AudioMediaCharacteristic = FOUR_CHAR_CODE(‘ears')
};
Any track that displays visible data to the user has the VisualMediaCharacteristic media characteristic; some examples are video tracks, sprite tracks, text tracks, MPEG tracks, and timecode tracks. Similarly, any track that plays audible data to the user has the AudioMediaCharacteristic media characteristic; some examples are sound tracks and music tracks. QuickTime has subsequently added a few other searchable media characteristics, including kCharacteristicProvidesActions for tracks that contain wired actions.
In Listing 1, we're looking to see whether any track in the movie contains skin data. (There is as yet no publicly-defined constant for the skin media characteristic, so we've hard-coded the value FOUR_CHAR_CODE(‘skin').) Skin tracks certainly contain skin data, so they have this characteristic. But other kinds of tracks may very well contain skin data, and so they too would have this characteristic. (If we are interested in knowing whether a specific track has a given characteristic, we can call the MediaHasCharacteristic function.) By searching for the skin media characteristic instead of the skin media type, we allow our applications to work with any movie tracks that contain skin data. Right now, to be sure, there are no track types with that characteristic aside from skin tracks; but we are equipped to deal with them when they come along.
Using the QuickTime XML Importer
By far the easiest way to create a movie with a skin track is to use a QuickTime XML importer, introduced in QuickTime 5. XML (for Extensible Markup Language) is a textual description of a document that contains structured information. It's similar in flavor to HTML, but differs significantly in that XML does not have a predefined set of markup tags. Rather, XML is more of a metalanguage for describing structured information. A QuickTime XML importer is a movie importer that knows how to parse certain kinds of XML files. QuickTime provides an importer that knows how to parse XML files that contain tags describing a skinned movie. Listing 2 shows the file used to construct the skinned movie shown in Figure 1. As you can see, this XML file specifies three other files, which contain the original movie data, a mask for the content region of the window, and a mask for the drag region of the window.
Listing 2: An XML file that specifies a skinned movie
<?xml version=”1.0”?>
<?quicktime type=”application/x-qtskin”?>
<skin>
<movie src=”QTLogo.mov”/>
<contentregion src=”contentmask.pct”/>
<dragregion src=”dragmask.pct”/>
</skin>
If we open this file using QuickTime Player or any other skin-savvy application, we'll see the skinned movie shown in Figure 1. The application probably calls NewMovieFromFile or NewMovieFromDataRef to open the XML file. QuickTime will see that the file doesn't contain a movie atom and then go looking for a suitable movie importer. (See "In and Out" in MacTech, May 2000, for a more in-depth discussion of how this works.) In the present case, QuickTime will invoke the XML importer to import the movie data and return a movie to the calling application. Note that some importers, including the QuickTime XML importer, seem to ignore the newMovieActive flag passed to NewMovieFromFile. So we'll add the following line of code to the QTFrame_OpenMovieInWindow function, after we call NewMovieFromFile:
SetMovieActive(myMovie, true);
We can create a self-contained skinned movie file by calling FlattenMovieData on the open skinned movie. Our sample applications make this call when the user selects the "Save As..." menu item. The self-contained movie file is easier to move around and to transport from machine to machine. It's also preferable for web-based movie delivery.
Creating Skin Tracks Programmatically
Using the XML importer is fine and dandy, but we'd also like to be able to create skinned movies directly, using the QuickTime APIs. Once again, we'll do this by adding a skin track to an existing movie. We've created many kinds of tracks in QuickTime movies, so we've got the drill down. You'll recall that it goes basically like this:
- Create a new track and media (NewMovieTrack and NewTrackMedia).
- Create a new sample description (NewHandle).
- Start a media-editing session (BeginMediaEdits).
- Add media data to the new media (AddMediaSample).
- End the media-editing session (EndMediaEdits).
- Insert the new media data into the track (InsertMediaIntoTrack).
It turns out, however, that we need to use a slightly different method for constructing a skin track. When we build (for instance) a video track or a sprite track, we need to know the exact structure of the media sample data, and we need to fill out a sample description that describes that data (its size, its compression type, and so forth). Moreover, when we call AddMediaSample, we need to specify the duration of the media sample. But with skin media data, the notion of duration doesn't really apply. After all, we're just specifying a couple a masks for a window shape, not any time-based data.
To simplify our handling of media data that isn't time based, QuickTime 5 introduced public media information, which can be any data associated with a media that does not need to be pegged to a specific time in a track. Currently, to my knowledge, only the skin media handler supports public media information, to maintain the content and drag region masks.
QuickTime 5 includes two new functions for working with public media information, MediaSetPublicInfo and MediaGetPublicInfo. MediaSetPublicInfo is declared essentially like this:
ComponentResult MediaSetPublicInfo(MediaHandler mh,
OSType infoSelector, void *infoDataPtr, Size dataSize);
The mh parameter specifies the media handler we're giving the information to; in the present case,
it's the skin media handler. The infoSelector parameter specifies the kind of public information
we're setting. The skin media handler currently understands two selectors, ‘skcr'
(for the content region mask) and ‘skdr' (for the drag region mask). The parameters infoDataPtr
and dataSize specify the memory location and size of the public media information data. With the skin
media handler, however, dataSize should be 0 and infoDataPtr should be a picture handle (of type
PicHandle). For instance, here's how we'll set the content region mask:
myErr = MediaSetPublicInfo(myHandler, FOUR_CHAR_CODE(‘skcr'),
(void *)myContentPic, 0);
Our work really boils down to this: have the user select two pictures, one for the content region mask and another for the drag region mask; then create a new track and media (of type ‘skin'), call MediaSetPublicInfo for each of the pictures selected by the user, and finish up by calling InsertMediaIntoTrack. Once we've got the two picture handles, the 6-step sequence listed above reduces to this:
- Create a new track and media (NewMovieTrack and NewTrackMedia).
- Add media data to the new media (MediaSetPublicInfo).
- Insert the new media data into the track (InsertMediaIntoTrack).
Let's consider, then, how to get the two picture handles. Ideally, we'd like to allow the user to work with any kind of image file that QuickTime can open (just like the XML importer does). MediaSetPublicInfo expects the data we pass it to be a PicHandle, so we need some way to convert the image data in a file selected by the user into a PicHandle. Happily, there is a graphics importer function, GraphicsImportGetAsPicture, that does precisely this. Listing 3 defines the QTSkin_GetPicHandleFromFile function, which we use (in Listing 4) to prompt the user for the two images we need. (For more information about graphics importers, see "Quick on the Draw" in MacTech, April 2000.)
Listing 3: Getting a picture handle from an image file
PicHandle QTSkin_GetPicHandleFromFile (void)
{
OSType myTypeList =
kQTFileTypeQuickTimeImage;
short myNumTypes = 1;
FSSpec myPictSpec;
QTFrameFileFilterUPP myFilterUPP = NULL;
GraphicsImportComponent myImporter = NULL;
PicHandle myPicture = NULL;
OSErr myErr = noErr;
#if TARGET_OS_MAC
myNumTypes = 0;
#endif
// have the user select an image file
myFilterUPP = QTFrame_GetFileFilterUPP(
(ProcPtr)QTSkin_FileFilterFunction);
myErr = QTFrame_GetOneFileWithPreview(myNumTypes,
(QTFrameTypeListPtr)&myTypeList, &myPictSpec,
myFilterUPP);
if (myErr != noErr)
goto bail;
// get a graphics importer for the image file
myErr = GetGraphicsImporterForFile(&myPictSpec,
&myImporter);
if (myErr != noErr)
goto bail;
// convert the image into a PicHandle
myErr = GraphicsImportGetAsPicture(myImporter, &myPicture);
bail:
if (myFilterUPP != NULL)
DisposeNavObjectFilterUPP(myFilterUPP);
if (myImporter != NULL)
CloseComponent(myImporter);
return(myPicture);
}
We are finally ready to put this all together. When the user selects the "Add Skin Track..." menu item, we execute the QTSkin_AddSkinTrack function defined in Listing 4.
Listing 4: Adding a skin track to a movie
OSErr QTSkin_AddSkinTrack (Movie theMovie)
{
Track myTrack = NULL; // the movie track
Media myMedia = NULL; // the movie track's media
Rect myRect;
MediaHandler myHandler = NULL;
PicHandle myContentPic = NULL;
PicHandle myDragPic = NULL;
OSErr myErr = paramErr;
if (theMovie == NULL)
goto bail;
// elicit the two pictures we need from the user
myContentPic = QTSkin_GetPicHandleFromFile();
if (myContentPic == NULL)
goto bail;
myDragPic = QTSkin_GetPicHandleFromFile();
if (myDragPic == NULL)
goto bail;
// get the movie's dimensions
GetMovieBox(theMovie, &myRect);
MacOffsetRect(&myRect, -myRect.left, -myRect.top);
// create the skin track and media
myTrack = NewMovieTrack(theMovie,
FixRatio(myRect.right, 1),
FixRatio(myRect.bottom, 1), kNoVolume);
if (myTrack == NULL)
goto bail;
myMedia = NewTrackMedia(myTrack, FOUR_CHAR_CODE(‘skin'),
GetMovieTimeScale(theMovie), NULL, 0);
if (myMedia == NULL)
goto bail;
myHandler = GetMediaHandler(myMedia);
if (myHandler == NULL)
goto bail;
// add the skin content picture as public media information
myErr = MediaSetPublicInfo(myHandler,
FOUR_CHAR_CODE(‘skcr'),
(void *)myContentPic, 0);
if (myErr != noErr)
goto bail;
// add the skin drag picture as public media information
myErr = MediaSetPublicInfo(myHandler,
FOUR_CHAR_CODE(‘skdr'),
(void *)myDragPic, 0);
if (myErr != noErr)
goto bail;
// add the media to the track
myErr = InsertMediaIntoTrack(myTrack, 0, 0,
GetMediaDuration(myMedia), fixed1);
bail:
if (myContentPic != NULL)
KillPicture(myContentPic);
if (myDragPic != NULL)
KillPicture(myDragPic);
return(myErr);
}
As you can see, using MediaSetPublicInfo greatly simplifies creating a skin track. We don't have to create a sample description, and we don't need to call BeginMediaEdits or AddMediaSample or EndMediaEdits. The skin media handler takes care of all the details of storing the content and drag region masks in the skin media.
Skinned Movie Playback
So now we know how to build a skinned movie, using either the QuickTime XML importer or our own application code. As mentioned earlier, we also want our application to be able to open and play back skinned movies. This turns out to be significantly more complicated, however, since we need to be able to assign a custom window shape to a movie window and window shapes are handled by the application, not by QuickTime. So, we're going to have to get acquainted with some of the low-level window-handling capabilities of our host operating systems if we want to be able to open and manipulate skinned movies.
On Macintosh operating systems, we assign a custom shape to a movie window by writing a custom window definition procedure. Under Carbon, the code for a custom window definition procedure is contained in the application itself, not in a code resource of type ‘WDEF' (as in the pre-Carbon Mac world). Once we've defined our custom procedure, we can call the CreateCustomWindow function to create a skinned movie window. Whenever the Window Manager needs to draw our custom window or handle clicks on it, it calls our custom window definition procedure.
On Windows operating systems, it's even easier to assign a custom shape to a window: we can call the SetWindowRgn function when opening the movie window to assign an arbitrary region as the window shape. We'll also add a little code to our basic movie window procedure QTFrame_MovieWndProc to handle skinned window dragging.
Before we can do any of this, however, we need to get ahold of the skin data that determines the window's appearance and drag behavior. That is, we need to read the content and drag region masks out of the skinned movie file. As you might guess, we'll use GetMediaPublicInfo to get the picture data stored in the skin track. Then we'll need to convert that picture into a region, which we'll pass to the operating system window handlers.
Setting Up the Application Data
For each skinned movie file we open, we need to maintain some application-specific data. Basically, we need a place to keep track of the various regions describing the geometry of the skinned movie window. In the file ComApplication.h, we'll declare the ApplicationDataRecord data structure like this:
typedef struct ApplicationDataRecord {
RgnHandle fContentRegion; // content region of window
RgnHandle fDragRegion; // drag region of window
RgnHandle fStructRegion; // structure region of window
#if TARGET_OS_WIN32
HRGN fWinHRGN; // window region
#endif
} ApplicationDataRecord, *ApplicationDataPtr,
**ApplicationDataHdl;
The fContentRegion and fDragRegion fields hold handles to the content region and the drag region of the window, which we retrieve from the skin track as we're opening a skinned movie. The fStructRegion field holds the structure region of the skinned window. A window's structure region is the entire screen area occupied by the window, including the window's content region and its window frame. For skinned movies, the structure region is usually identical to its content region. The fWinHRGN field holds the window content region as an object of type HRGN. This is the object we'll to pass to SetWindowRgn when we set the window's shape on Windows.
Recall that the data stored in a skin track is of type PicHandle. We can retrieve that data by calling GetMediaPublicInfo, passing it a selector for the type of information we want. For instance, to retrieve the content region mask from a skin track, we can execute this code:
myPicture = (PicHandle)NewHandle(0);
if (myPicture == NULL)
goto bail;
myErr = MediaGetPublicInfo(myHandler, FOUR_CHAR_CODE(‘skcr'),
myPicture, NULL);
If GetMediaPublicInfo completes successfully, myPicture will contain a handle to the picture data. We
then need to convert this picture data into a region, since that's the kind of data we'll need to have
available in our custom window definition procedure. We make this conversion by calling the application
function QTSkin_ConvertPictureToRegion:
myErr = QTSkin_ConvertPictureToRegion(myPicture,
&(**myAppData).fContentRegion);
QTSkin_ConvertPictureToRegion creates a region that contains every non-white pixel in the specified picture. The key step is using QuickDraw's BitmapToRegion function to convert a bitmap or a pixel map into a region. So we need to create a pixel map from our picture data. But this is very easy: we simply create a new offscreen graphics world and draw the picture data into it. We can then use the GetGWorldPixMap function to get the pixel map associated with that graphics world. Listing 5 shows our definition of QTSkin_ConvertPictureToRegion.
Listing 5: Converting a picture into a region
OSErr QTSkin_ConvertPictureToRegion (PicHandle thePicture,
RgnHandle *theRegionPtr)
{
Rect myRect;
GWorldPtr myGWorld = NULL;
PixMapHandle myPixMap = NULL;
CGrafPtr mySavedPort = NULL;
GDHandle mySavedDevice = NULL;
RgnHandle myRegion = NULL;
OSErr myErr = noErr;
if ((thePicture == NULL) || (theRegionPtr == NULL))
return(paramErr);
// get the current graphics port and device
GetGWorld(&mySavedPort, &mySavedDevice);
// get the bounding box of the picture
myRect = (**thePicture).picFrame;
myRect.bottom = EndianS16_BtoN(myRect.bottom);
myRect.right = EndianS16_BtoN(myRect.right);
// create a new GWorld and draw the picture into it
myErr = QTNewGWorld(&myGWorld, k1MonochromePixelFormat,
&myRect, NULL, NULL, kICMTempThenAppMemory);
if (myGWorld == NULL)
goto bail;
SetGWorld(myGWorld, NULL);
myPixMap = GetGWorldPixMap(myGWorld);
if (myPixMap == NULL)
goto bail;
LockPixels(myPixMap);
HLock((Handle)myPixMap);
EraseRect(&myRect);
DrawPicture(thePicture, &myRect);
// create a new region and convert the pixmap into a region
myRegion = NewRgn();
myErr = MemError();
if (myErr != noErr)
goto bail;
myErr = BitMapToRegion(myRegion, (BitMap *)*myPixMap);
bail:
if (myErr != noErr) {
if (myRegion != NULL) {
DisposeRgn(myRegion);
myRegion = NULL;
}
}
if (myGWorld != NULL)
DisposeGWorld(myGWorld);
// restore the original graphics port and device
SetGWorld(mySavedPort, mySavedDevice);
*theRegionPtr = myRegion;
return(myErr);
}
For our Windows applications, we need to take one further step and convert the Macintosh region (of type RgnHandle) into a Windows region (of type HRGN). The QuickTime Media Layer provides a function that will do this for us:
(**myAppData).fWinHRGN = MacRegionToNativeRegion
((**myAppData).fContentRegion);
All of this start-up code will go into the function QTSkin_InitWindowData, which is called by QTApp_SetupWindowObject to perform any application-specific initialization of the movie window and its associated data. QTApp_SetupWindowObject contains this code to handle skinned movies:
if (QTSkin_IsSkinnedMovie(myMovie)) {
// hide the controller bar
MCSetVisible(myMC, false);
// detach the controller
MCSetControllerAttached(myMC, false);
// initialize the window data for a skins movie
(**theWindowObject).fAppData =
(Handle)QTSkin_InitWindowData(theWindowObject);
}
When QTApp_SetupWindowObject is called, the skinned movie window has already been created but it has not yet been displayed to the user. On Windows, we used our standard function QTFrame_CreateMovieWindow to create the movie window. So at this point, on Windows, we can already call SetWindowRgn to set the shape of the skinned movie window:
if ((**myAppData).fWinHRGN != NULL) {
RECT myRect;
int myResult;
GetRgnBox((**myAppData).fWinHRGN, &myRect);
OffsetRgn((**myAppData).fWinHRGN,
-myRect.left + GetSystemMetrics(SM_CXFRAME),
-myRect.top + GetSystemMetrics(SM_CYCAPTION) +
GetSystemMetrics(SM_CYFRAME));
myResult = SetWindowRgn((**theWindowObject).fWindow,
(**myAppData).fWinHRGN, true);
}
SetWindowRgn sets the visible region of a window; it expects the origin of the window region we pass it to be relative to the upper-left corner of the window, not relative to the client area of the window. So we need to offset the stored window region (**myAppData).fWinHRGN horizontally by the width of the window frame and vertically by the height of the window frame and the height of the title bar (or caption). Figure 7 shows the penguin window with these offsets.
Figure 7: The client region offsets
As far as Windows is concerned, the window frame and window controls still exist — they are just not visible on the screen. The window is a full-fledged MDI child window, just like any of our other (non-skinned) movie windows. The only difference is that the skinned movie window has a special visible region.
Listing 6 shows the full version of our skinned movie window initialization code.
Listing 6: Initializing the application data for a skinned movie
ApplicationDataHdl QTSkin_InitWindowData
(WindowObject theWindowObject)
{
ApplicationDataHdl myAppData = NULL;
Track myTrack = NULL;
MediaHandler myHandler = NULL;
PicHandle myPicture = NULL;
MatrixRecord myMatrix;
OSErr myErr = noErr;
// if we already have some window data, dump it
myAppData =
(ApplicationDataHdl)QTFrame_GetAppDataFromWindowObject
(theWindowObject);
if (myAppData != NULL)
QTSkin_DumpWindowData(theWindowObject); // see Listing 14
myAppData =
(ApplicationDataHdl)NewHandleClear
(sizeof(ApplicationDataRecord));
if (myAppData != NULL) {
myTrack = GetMovieIndTrackType
((**theWindowObject).fMovie, 1, FOUR_CHAR_CODE(‘skin'),
movieTrackCharacteristic);
if (myTrack != NULL) {
myHandler = GetMediaHandler(GetTrackMedia(myTrack));
if (myHandler != NULL) {
// get the current movie matrix
GetMovieMatrix((**theWindowObject).fMovie,
&myMatrix);
myPicture = (PicHandle)NewHandle(0);
if (myPicture == NULL)
goto bail;
// get the content region picture
myErr = MediaGetPublicInfo(myHandler,
FOUR_CHAR_CODE(‘skcr'), myPicture, NULL);
if (myErr != noErr)
goto bail;
// convert it to a region
myErr = QTSkin_ConvertPictureToRegion(myPicture,
&(**myAppData).fContentRegion);
if (myErr != noErr)
goto bail;
// scale that region so the window scales with the movie
myErr = TransformRgn(&myMatrix,
(**myAppData).fContentRegion);
if (myErr != noErr)
goto bail;
#if TARGET_OS_WIN32
(**myAppData).fWinHRGN = MacRegionToNativeRegion(
(**myAppData).fContentRegion);
if ((**myAppData).fWinHRGN != NULL) {
RECT myRect;
int myResult;
GetRgnBox((**myAppData).fWinHRGN, &myRect);
// the coordinates of a window region are relative to the upper-left corner
// of the window (not to the client area of the window)
OffsetRgn((**myAppData).fWinHRGN,
-myRect.left + GetSystemMetrics(SM_CXFRAME),
-myRect.top + GetSystemMetrics(SM_CYCAPTION) +
GetSystemMetrics(SM_CYFRAME));
myResult = SetWindowRgn(
(**theWindowObject).fWindow,
(**myAppData).fWinHRGN, true);
if (myResult == 0) {
// SetWindowRgn failed
DeleteObject((**myAppData).fWinHRGN);
(**myAppData).fWinHRGN = NULL;
goto bail;
}
}
#endif
// repeat with drag region picture
myErr = MediaGetPublicInfo(myHandler,
FOUR_CHAR_CODE(‘skdr'), myPicture, NULL);
if (myErr != noErr)
goto bail;
// convert it to a region
myErr = QTSkin_ConvertPictureToRegion(myPicture,
&(**myAppData).fDragRegion);
if (myErr != noErr)
goto bail;
// scale that region so the window scales with the movie
myErr = TransformRgn(&myMatrix,
(**myAppData).fDragRegion);
if (myErr != noErr)
goto bail;
// copy the content region into the structure region
(**myAppData).fStructRegion = NewRgn();
MacCopyRgn((**myAppData).fContentRegion,
(**myAppData).fStructRegion);
}
}
}
bail:
if (myPicture != NULL)
DisposeHandle((Handle)myPicture);
return(myAppData);
}
Specifying a Custom Window Shape
As we've seen, it's child's play on Windows operating systems to specify a custom window shape: just pass the shape (as an HRGN) to SetWindowRgn. On the Mac, it's quite a bit more complicated. We need to write a custom window definition procedure and attach it to any skinned movies that the user opens. In our framework function QTFrame_OpenMovieInWindow, we'll add a few Mac-specific lines before the existing call to QTFrame_CreateMovieWindow:
#if TARGET_OS_MAC
// create a new window to display the movie in
if (QTSkin_IsSkinnedMovie(myMovie))
myWindow = QTSkin_CreateSkinsWindow();
else
#endif
myWindow = QTFrame_CreateMovieWindow();
On Macintosh computers, QTFrame_CreateMovieWindow calls the Window Manager function NewCWindow to create a standard document window. For skinned windows, we need to call CreateCustomWindow, as shown in Listing 7.
Listing 7: Opening a window with a custom shape
WindowReference QTSkin_CreateSkinsWindow (void)
{
WindowPtr myWindow = NULL;
WindowReference myWindowRef = NULL;
Rect myRect = {10, 60, 200, 200};
// call CreateCustomWindow to create a window using our custom window defproc
CreateCustomWindow(&gDefSpec, kDocumentWindowClass,
kWindowNoAttributes, &myRect, &myWindow);
if (myWindow != NULL) {
// get the "window reference" for this window
myWindowRef =
QTFrame_GetWindowReferenceFromWindow(myWindow);
// create a new window object associated with the new window
QTFrame_CreateWindowObject(myWindowRef);
}
return(myWindowRef);
}
This call to CreateCustomWindow asks for a document window with no special attributes. (The rectangle parameter is arbitrary, since we'll change the window size later.) The window definition procedure to be used to handle the custom window is specified by the &gDefSpec parameter, which is a pointer to a window definition specification, declared like this:
struct WindowDefSpec {
WindowDefType defType;
union {
WindowDefUPP defProc;
Void *classRef;
Short procID;
} u;
};
The defType field specifies which member of the union u we want to use. In the present case, we want to use the defProc member, so we set defType to kWindowDefProcPtr. And we'll set the defProc member to a universal procedure pointer to our custom window definition procedure. We initialize the gDefSpec global variable in the application start-up code for QTSkins, by calling the QTSkin_Init function defined in Listing 8.
Listing 8: Setting up a window definition specification
void QTSkin_Init (void)
{
// set up the window definition specification structure
gDefSpec.defType = kWindowDefProcPtr;
gDefSpec.u.defProc = NewWindowDefUPP(QTSkin_SkinWindowDef);
}
Writing a Custom Window Definition Procedure
On Macintosh operating systems, the appearance and behavior of our skinned movie windows are determined by QTSkin_SkinWindowDef, our custom window definition procedure. QTSkin_SkinWindowDef is declared like this:
static PASCAL_RTN long QTSkin_SkinWindowDef
(short theVarCode, WindowRef theWindow,
short theMessage, long theParam);
Here, theMessage is a window definition message that indicates which task the window definition procedure is to perform. These are the common window definition messages:
enum {
kWindowMsgDraw = 0,
kWindowMsgHitTest = 1,
kWindowMsgCalculateShape = 2,
kWindowMsgInitialize = 3,
kWindowMsgCleanUp = 4,
kWindowMsgDrawGrowOutline = 5,
kWindowMsgDrawGrowBox = 6,
kWindowMsgGetFeatures = 7,
kWindowMsgGetRegion = 8,
kWindowMsgDragHilite = 9,
kWindowMsgModified = 10,
kWindowMsgDrawInCurrentPort = 11,
kWindowMsgSetupProxyDragImage = 12,
kWindowMsgStateChanged = 13,
kWindowMsgMeasureTitle = 14,
kWindowMsgGetGrowImageRegion = 19
};
We can ignore most of these messages in our procedure. For instance, our skinned movie windows don't have grow boxes, so we can ignore the kWindowMsgDrawGrowOutline and kWindowMsgDrawGrowBox messages. In fact, we'll need to handle only three of these messages: kWindowMsgHitTest, kWindowMsgGetFeatures, and kWindowMsgGetRegion.
When we receive the kWindowMsgGetFeatures message, we need to return (through theParam) a value that indicates the capabilities of our custom window definition procedure. Really all our custom procedure can do is return information about various window regions. So we'll set the features information like this:
case kWindowMsgGetFeatures:
if (theParam != 0L)
*(OptionBits *)theParam = kWindowCanGetWindowRegion;
return(1);
The meaning of the return value of our custom window definition procedure varies, depending on the message the procedure is handling. In this case, the documentation tells us to return 1.
When we receive the kWindowMsgHitTest message, we need to return one of these values, indicating which region of the movie (if any) was clicked in:
enum {
wNoHit = 0,
wInContent = 1,
wInDrag = 2,
wInGrow = 3,
wInGoAway = 4,
wInZoomIn = 5,
wInZoomOut = 6,
wInCollapseBox = 9,
wInProxyIcon = 10
};
With this message, theParam contains the coordinates of the mouse click, which we can extract like this:
myPoint.v = HiWord(theParam);
myPoint.h = LoWord(theParam);
This point is in global screen coordinates. Our regions, however, are stored with the upper-left corner
set to (0,Ê0). So we need to map myPoint into the window's local coordinate system, as follows:
GetPort(&myPort);
SetPortWindowPort(theWindow);
myLocal = myPoint;
GlobalToLocal(&myLocal);
MacSetPort(myPort);
The GlobalToLocal function maps the specified point into the coordinate system of the current graphics port, so we need to make sure that our custom window is the current graphics port (taking care to save and restore the previous current port).
Now that we've got a point local to the skinned movie window, we can use the PtInRgn function to do the required hit-testing:
if (PtInRgn(myLocal, (**myAppData).fDragRegion))
return(wInDrag);
if (PtInRgn(myLocal, (**myAppData).fContentRegion))
return(wInContent);
return(wNoHit);
We first look to see whether the specified point is in the drag region. If not, we look to see whether it's in the content region. If the point is in neither region, we indicate that no hit occurred.
When we receive the kWindowMsgGetRegion message, theParam is a pointer to a structure of type GetWindowRegionRec:
struct GetWindowRegionRec {
RgnHandle winRgn;
WindowRegionCode regionCode;
};
The regionCode field indicates which region we are supposed to return (through the winRgn field). Our skinned movie windows have only three interesting regions, the content region, the drag region, and the structure region (which is typically identical to the content region). So we'll respond to only three values for the regionCode field: kWindowContentRgn, kWindowDragRgn, and kWindowStructureRgn.
The region whose handle we return in the winRgn field is supposed to be specified in global screen coordinates. Our stored regions, however, are specified in coordinates local to the client region of the movie window. So we need to offset those regions before we return them from our window definition procedure. First, then, we need to figure out the global coordinates of the top-left corner of the window, like this:
GetPort(&myPort);
SetPortWindowPort(theWindow);
GetPortBounds(GetWindowPort(theWindow), &myPortBounds);
myTopLeft.h = myPortBounds.left;
myTopLeft.v = myPortBounds.top;
LocalToGlobal(&myTopLeft);
MacSetPort(myPort);
Then we need to offset any of the regions we pass back. For instance, we'll pass back the window's drag region like this:
MacCopyRgn((**myAppData).fDragRegion, myRgnRec->winRgn);
MacOffsetRgn(myRgnRec->winRgn, myTopLeft.h, myTopLeft.v);
Listing 9 shows our complete window definition procedure for skinned movie windows.
Listing 9: Handling skinned movie window messages
static PASCAL_RTN long QTSkin_SkinWindowDef
(short theVarCode, WindowRef theWindow,
short theMessage, long theParam)
{
#pragma unused(theVarCode)
switch (theMessage) {
case kWindowMsgInitialize:
case kWindowMsgCleanUp:
case kWindowMsgDrawGrowOutline:
case kWindowMsgDrawGrowBox:
case kWindowMsgDraw:
// nothing here
break;
case kWindowMsgHitTest: {
ApplicationDataHdl myAppData = NULL;
Point myPoint;
Point myLocal;
GrafPtr myPort;
myAppData =
(ApplicationDataHdl)QTFrame_GetAppDataFromWindow
(QTFrame_GetWindowReferenceFromWindow(theWindow));
if (myAppData == NULL)
return(wNoHit);
// on entry, theParam contains the mouse location in global screen coordinates
myPoint.v = HiWord(theParam);
myPoint.h = LoWord(theParam);
// the content and drag regions are offset relative to the window origin
GetPort(&myPort);
SetPortWindowPort(theWindow);
myLocal = myPoint;
GlobalToLocal(&myLocal);
MacSetPort(myPort);
// look first to see if the mouse event is in the drag region;
// it takes precedence over the content region
if (PtInRgn(myLocal, (**myAppData).fDragRegion))
return(wInDrag);
if (PtInRgn(myLocal, (**myAppData).fContentRegion))
return(wInContent);
return(wNoHit);
}
case kWindowMsgGetFeatures:
if (theParam != 0L)
*(OptionBits *)theParam = kWindowCanGetWindowRegion;
return(1);
case kWindowMsgGetRegion: {
GetWindowRegionRec *myRgnRec =
(GetWindowRegionRec *)theParam;
ApplicationDataHdl myAppData = NULL;
GrafPtr myPort;
Rect myPortBounds;
Point myTopLeft;
myAppData =
(ApplicationDataHdl)QTFrame_GetAppDataFromWindow
(QTFrame_GetWindowReferenceFromWindow(theWindow));
if (myAppData == NULL)
break;
// get the top-left corner of the window, in global coordinates
GetPort(&myPort);
SetPortWindowPort(theWindow);
#if TARGET_API_MAC_CARBON
GetPortBounds(GetWindowPort(theWindow), &myPortBounds);
#else
myPortBounds = theWindow->portRect;
#endif
myTopLeft.h = myPortBounds.left;
myTopLeft.v = myPortBounds.top;
LocalToGlobal(&myTopLeft);
MacSetPort(myPort);
switch (myRgnRec->regionCode) {
case kWindowTitleBarRgn:
case kWindowCloseBoxRgn:
break;
case kWindowDragRgn:
MacCopyRgn((**myAppData).fDragRegion,
myRgnRec->winRgn);
MacOffsetRgn(myRgnRec->winRgn, myTopLeft.h,
myTopLeft.v);
break;
case kWindowContentRgn:
MacCopyRgn((**myAppData).fContentRegion,
myRgnRec->winRgn);
MacOffsetRgn(myRgnRec->winRgn, myTopLeft.h,
myTopLeft.v);
break;
case kWindowStructureRgn:
MacCopyRgn((**myAppData).fStructRegion,
myRgnRec->winRgn);
MacOffsetRgn(myRgnRec->winRgn, myTopLeft.h,
myTopLeft.v);
break;
default:
break;
}
return(noErr);
}
default:
break;
}
return(0L);
}
Handling Dragging on Windows Computers
Earlier we saw how to assign a custom window shape to a movie on Windows operating systems, by calling SetWindowRgn. We still need to see how to handle window dragging on Windows. Let's begin by reviewing briefly how our window procedure for movie windows processes the messages it receives. Listing 10 shows a snippet from QTFrame_MovieWndProc. First of all, it fills out an MSG structure and translates the Windows message into a Macintosh event by calling WinEventToMacEvent. Then it passes the Mac event to the application function QTApp_HandleEvent. Then, if QTApp_HandleEvent did not handle the event, QTFrame_MovieWndProc passes the Mac event to MCIsPlayerEvent.
Listing 10: Sending Windows messages to the movie controller
MSG myMsg = {0};
LONG myPoints = GetMessagePos();
myMsg.hwnd = theWnd;
myMsg.message = theMessage;
myMsg.wParam = wParam;
myMsg.lParam = lParam;
myMsg.time = GetMessageTime();
myMsg.pt.x = LOWORD(myPoints);
myMsg.pt.y = HIWORD(myPoints);
// translate a Windows event to a Mac event
WinEventToMacEvent(&myMsg, &myMacEvent);
// let the application-specific code have a chance to intercept the event
myIsHandled = QTApp_HandleEvent(&myMacEvent);
// pass the Mac event to the movie controller
if (!myIsHandled)
if (myMC != NULL)
if (!IsIconic(theWnd))
myIsHandled = MCIsPlayerEvent(myMC,
(EventRecord *)&myMacEvent);
With skinned windows, the drag regions and the content regions virtually always overlap, so we need to prevent the movie controller from getting any mouse clicks that are in the drag region (since it would likely interpret them as clicks in the content region). We can do this quite easily by having QTApp_HandleEvent look to see whether the event it's passed is a mouse click in the drag region and, if it is, return true. Listing 11 shows the QTSkins version of QTApp_HandleEvent. Note that this code is conditionalized for Windows applications only, since on Macintosh the window definition procedure is responsible for finding clicks in the drag region.
Listing 11: Looking for drag region clicks (Windows)
Boolean QTApp_HandleEvent (EventRecord *theEvent)
{
#if TARGET_OS_MAC
#pragma unused(theEvent)
#endif
Boolean myIsHandled = false;
#if TARGET_OS_WIN32
ApplicationDataHdl myAppData = (ApplicationDataHdl)
QTFrame_GetAppDataFromFrontWindow();
Point myPoint;
if (theEvent == NULL)
goto bail;
if (theEvent->what == mouseDown) {
myPoint = theEvent->where;
GlobalToLocal(&myPoint);
if (myAppData != NULL)
if (PtInRgn(myPoint, (**myAppData).fDragRegion))
myIsHandled = true;
}
#endif
bail:
return(myIsHandled);
}
So far, then, we've managed to prevent the movie controller associated with a movie window from getting clicks in the window's drag region. Now we need to actually handle those clicks. On Windows, we can look for messages of type WM_LBUTTONDOWN and see if they are in the drag region. If they are, we want to trick the default window procedure into thinking that the clicks are on the title bar, so that the default window procedure will handle the dragging for us. We can do this by sending a message of type WM_NCLBUTTONDOWN to the default window procedure, like this:
SendMessage(theWnd, WM_NCLBUTTONDOWN, (WPARAM)HTCAPTION,
MAKELPARAM(5, 5));
The WM_NCLBUTTONDOWN message reports a button-down event in a non-client area of a window. The first parameter indicates which part of the window is directly under the cursor hot spot at the time of the click. In our case, we want to say that the click occurred in the title bar (indicated by the HTCAPTION constant). The second parameter indicates the location of the cursor hot spot, in coordinates that are relative to the upper-left corner of the screen. As best I can tell, the default window procedure ignores that parameter when the first parameter is set to HTCAPTION. So we'll pass an arbitrary value of (5,Ê5). Our complete left-button click handling is shown in Listing 12.
Listing 12: Handling drag region clicks (Windows)
case WM_LBUTTONDOWN:
// handle potential clicks in window drag region;
// if we get one, map it into a click on the title bar
if (QTSkin_IsSkinnedMovie(myMovie))
if (QTSkin_IsDragClick(myWindowObject, lParam)) {
SendMessage(theWnd, WM_NCLBUTTONDOWN,
(WPARAM)HTCAPTION, MAKELPARAM(5, 5));
myIsHandled = true;
}
// do any application-specific mouse-button handling,
// but only if the message hasn't already been handled
if (!myIsHandled)
QTApp_HandleContentClick(theWnd, &myMacEvent);
break;
The only thing left is to consider the definition of QTSkin_IsDragClick, which we call in Listing 12 to determine whether the specified point is in the drag region of the skinned movie window. Here we have several possibilities. We saw above that our version of QTApp_HandleEvent returns true if the specified event is a mouse-down event in the window's drag region. So we could just use that function. Alternatively, we can convert the Mac-style drag region (saved in our application data record) to a Windows region (of type HRGN) and call the Windows function PtInRegion to see whether the specified point is in that region. That's the strategy we'll use here; Listing 13 shows our definition of QTSkin_IsDragClick.
Listing 13: Finding drag region clicks (Windows)
#if TARGET_OS_WIN32
Boolean QTSkin_IsDragClick
(WindowObject theWindowObject, LONG lParam)
{
WindowObject myWindowObject = NULL;
ApplicationDataHdl myAppData = NULL;
HRGN myRegion = NULL;
POINT myPoint;
Boolean isDragClick = false;
myAppData = (ApplicationDataHdl)
QTFrame_GetAppDataFromWindowObject(theWindowObject);
if (myAppData != NULL) {
myPoint.x = LOWORD(lParam);
myPoint.y = HIWORD(lParam);
myRegion = MacRegionToNativeRegion
((**myAppData).fDragRegion);
if (PtInRegion(myRegion, myPoint.x, myPoint.y))
isDragClick = true;
DeleteObject(myRegion);
}
return(isDragClick);
}
#endif
The lParam parameter that was passed to WM_LBUTTONDOWN (which we also pass to QTSkin_IsDragClick) specifies a point in coordinates that are local to the client area of the window. As a result, we don't need to offset the drag region in Listing 13.
So now we've completely handled a click in the drag region of a skinned movie window on Windows.
Shutting Down
When the user closes a skinned movie window, we need to deallocate any memory used for displaying the movie in a skin. In particular, we need to dispose of the window regions that we're storing in the application data record. Listing 14 shows the definition of QTSkin_DumpWindowData, which is called by QTApp_RemoveWindowObject.
Listing 14: Cleaning up when a skinned window is closed
void QTSkin_DumpWindowData (WindowObject theWindowObject)
{
ApplicationDataHdl myAppData = NULL;
myAppData = (ApplicationDataHdl)
QTFrame_GetAppDataFromWindowObject(theWindowObject);
if (myAppData != NULL) {
if ((**myAppData).fContentRegion != NULL)
DisposeRgn((**myAppData).fContentRegion);
if ((**myAppData).fDragRegion != NULL)
DisposeRgn((**myAppData).fDragRegion);
if ((**myAppData).fStructRegion != NULL)
DisposeRgn((**myAppData).fStructRegion);
DisposeHandle((Handle)myAppData);
(**theWindowObject).fAppData = NULL;
}
}
You'll notice that we didn't do anything to free up the memory addressed by (**myAppData).fWinHRGN. The documentation for the SetWindowRgn function indicates that the operating system owns the region we pass it; this means that we don't need to call DeleteObject on that region.
When our application shuts down, we need to deallocate the universal procedure pointer contained inside of the gDefSpec structure. Listing 15 shows how we do this.
Listing 15: Cleaning up at application shut-down
void QTSkin_Stop (void)
{
// dispose of the window procedure UPP
if (gDefSpec.u.defProc != NULL)
DisposeWindowDefUPP(gDefSpec.u.defProc);
}
Conclusion
If you've made it this far, you deserve a pat on the back. We've had our usual dose of new QuickTime APIs, but we've also had a big gulp of low-level window management. On the Macintosh, we had to write a custom window definition procedure in order for our application to handle skinned movie windows. And on Windows, we had to tinker with our application's event-handling to support skinned movie window dragging. But the payoff for all this work is tremendous, precisely because skinned movies are such great stuff. As we've noted, the movie author now has virtually complete control over the appearance and behavior of movie windows. The movie interface has become part of the movie content. The medium is now part of the message.
Credits
Special thanks are due to Jim Batson for reviewing this article and providing some helpful comments. Thanks are also due to ici Media, Inc. (http://www.icimediainc.net) for permission to use the picture of the movie in Figure 3.
Tim Monroe is intrigued to discover that his lizards often eat their own skin after they molt. You can explain this to him at monroe@apple.com.