Example: The WipeDlg Application

Microsoft DirectX Transform provides you with powerful tools for manipulating 2-D image data. With Microsoft DirectX Transform, you can perform operations on a single image—for example, the MotionBlur or BasicImage transform—or combine two images to form a single output image—for example, the Iris or Wheel transform.

The WipeDlg sample application shows how to host the GradientWipe transform in C++. This transform requires two input images, referred to in the code as Image A and Image B. The transform animates Image A, gradually turning it into Image B by sweeping a blended region of both images that gradually overlays Image B onto Image A. The following animation shows how this looks at different stages of the transform.

When you run WipeDlg.exe you are prompted to load Image A and Image B. You can load them from the File menu. After the images are loaded, you can use the Play buttons to either animate Image B into Image A, or Image A into Image B. This animation results from changing the values of the Wipe transform's IDXEffect::Progress property. The Properties button enables you to change two properties of the Wipe transform: GradientSize and IDXEffect::get_Duration.

The GradientSize controls the percentage of the total window size that comprises the transition region between the images, while the IDXEffect::get_Duration controls the amount of time, in seconds, that the animation process takes. A small GradientSize produces a drawn-curtain-style transition from Image A to Image B, and a large GradientSize causes a subtle fade from Image A to Image B. You can also adjust the trackbar control to set intermediate values for the IDXEffect::Progress property and to see frames of the animation process where parts of both images are visible.

This application uses the GradientWipe transform contained in the Wipe.dll sample. The GradientSize is a custom property defined only for this particular transform. Both the IDXEffect::Progress and IDXEffect::get_Duration properties are inherited from the IDXEffect interface, which is used by all transforms that support animation. If a transform supports animation, then IDXEffect is listed as a supported interface on its Transform Reference page.

The following sections examine the specific parts of the source code for the application that hosts this transform.

  • The Basic Methods
  • Initializing the Transform
  • Performing the Transform
  • Sweeping Left and Right
  • What Next?

The Basic Methods

As with all Windows applications, the program execution begins in the WinMain function. This is a standard Windows function responsible for opening the dialog box window and processing the dialog box message loop. Creation of the dialog box happens with this call.

   // Create the main window.
    hWnd = CreateDialog(hInst, MAKEINTRESOURCE(IDD_WipeDlg), NULL,
    (DLGPROC)MainWndProc);

    if(!hWnd || !g_pSurfFact)
        goto failure;

If the application is unable to create the window or unable to create an IDXSurfaceFactory interface needed for the transforms, the application quits. The following section explains how the surfaces are created by CreateDialog. If Dxtrans.dll and Wipe.dll are properly registered by Windows, and you have sufficient memory, this call should succeed.

The application enters a standard message loop, which branches to the MainWindowProc function when a message is received. The function is passed a handle to the dialog box message and the switch(uMsg) statement determines which function of WipeDlg.cpp to call. The HandleInitDialog function is executed when the window is created. HandleSize is called when a user resizes a window, and HandleTrackBar is called when the trackbar control value changes. The HandleCommand function is another switch that handles image file loading, playing the transform, and changing transform properties.

The important function to examine is HandleCommand, which draws a frame of transformed image output to the screen. Those details are examined in the Performing the Transform section. Next, this article examines how to initialize the transform and the DXSurface objects needed by the transform.

Initializing the Transform

When the message loop calls the CreateDialog function, it automatically calls the MainWndProc function, as well, with the message WM_INITDIALOG. This function, in turn, calls HandleInitDialog, which is shown in the following code example.

/////////////////////////////////////////////////////////////////
BOOL HandleInitDialog(HWND hWnd, HWND hwndFocus, LPARAM lParam)
/////////////////////////////////////////////////////////////////
{
    HRESULT     hr  = S_OK;

    // Store the dialog box handle.
    g_hDlg  = hWnd;

    // Initialize the Progress trackbar control.
    SendDlgItemMessage( hWnd, IDS_PCTCOMP, TBM_SETRANGE, TRUE,
    MAKELONGcontained( 0, 100 ) );
    SendDlgItemMessage( hWnd, IDS_PCTCOMP, TBM_SETTICFREQ, 20, 0 );

The function begins executing by storing the dialog box handle to a global variable, defined in Globals.cpp, and initializing the IDXEffect::Progress trackbar control to zero.

The next two statements use Component Object Model (COM) methods to create a DXTransformFactory (Transform Factory).

    CoInitialize(NULL);

    // Create the Transform Factory.
    hr = CoCreateInstance( CLSID_DXTransformFactory, NULL,
    CLSCTX_INPROC, IID_IDXTransformFactory,
    (void **)&g_pTransFact );

The CoInitialize method is a standard method that initializes the COM library. The second statement creates the IDXTransformFactory interface by using its class identifier (CLSID) and IID, receiving a pointer to the requested interface and an HRESULT that indicates whether the creation was successful. For more details on using these COM methods, consult a standard COM text or the COM Reference in the Windows Software Development Kit (SDK).

Now you can use the Transform Factory to manufacture transforms. These transforms operate on objects called DXSurfaces, which store all the image data. A DXSurface is created with the IDXSurfaceFactory interface, which is exposed when the Transform Factory is created.

To use the IDXSurfaceFactory object (Surface Factory), you must use the Transform Factory to retrieve a pointer to the Surface Factory it exposed, as shown in the following:

hr = g_pTransFact->QueryService( SID_SDXSurfaceFactory,
IID_IDXSurfaceFactory, (void **)&g_pSurfFact );

The IServiceProvider::QueryService method is a standard COM interface used to retrieve a pointer to a service of an interface. You pass the IServiceProvider::QueryService method a service ID, indicating to the Transform Factory to use the Surface Factory query service. You also pass IServiceProvider::QueryService an interface ID to retrieve a pointer to a Surface Factory interface. It returns an HRESULT value, indicating whether the interface exists. If it returns a value of S_OK, it also returns a pointer that you can use to access the Surface Factory's methods and to create DXSurfaces. It isn't necessary to create the surfaces until the application loads the image files, but the Surface Factory is ready to manufacture them when that happens.

The following statement uses the Transform Factory to create an instance of a Wipe transform.

hr = g_pTransFact->CreateTransform( NULL, 0, NULL, 0, NULL, NULL,
CLSID_DXTWipe, IID_IDXTransform, (void **)&g_pWipeTrans );

At this stage there are no input or output surfaces for the transform, so the first four parameters are either NULL or zero. The next NULL indicates that there is no COM property bag—a stored property set—for this interface. The fourth NULL indicates that no transform errors need to be logged. The first parameter passed to the Transform Factory is the CLSID of a transform, which in this case indicates to the Factory that a GradientWipe transform needs to be manufactured. Each transform has its own CLSID. The second parameter tells the Transform Factory that you want a pointer to an IDXTransform interface. If successful, that pointer is stored in g_pWipeTrans, and after the input and output surfaces are identified, you can start using this transform on DXSurfaces.

As a final step, you obtain a pointer to an IDXEffect interface, which will enable you to access the transform IDXEffect::Progress and IDXEffect::get_Duration properties.

hr = g_pWipeTrans->QueryInterface( IID_IDXEffect, (
void **)&g_pEffect );

Because all COM interfaces inherit from the IUnknown interface, they all have a QueryInterface method. This method is used to determine whether the requested interface is supported by its object, and if so, to retrieve a pointer to the interface. To use QueryInterface, pass it the IID_IDXEffect value. If the GradientWipe transform supports this interface, it returns an S_OK value and a pointer to the location of the IDXEffect interface.

After the dialog box initialization routine is complete, control of the application is returned to the message loop. One possible message that the loop can receive is WM_COMMAND, indicating that the user has selected one of the dialog box commands. When it gets one of these messages, program control goes to HandleCommand. This function determines which dialog box control is selected and calls the appropriate message handler. However, no image output can occur until the images are loaded into memory. This is handled by the following block of code, which is located in Commdlg.cpp.

        case IDM_IMAGEA:
        case IDM_IMAGEB:
            IDXSurface  **ppSurface     = NULL;
            TCHAR       szError[256]    = _T("");
            HResult hr;
            if (id == IDM_IMAGEA)
            {
                ppSurface = &g_pInSurfA;
                hr = StringCchCopy(szError, 256, _T("Couldn't read Image A\n"));
                // TODO: Add error handling code here.
            }
            else
            {
                ppSurface = &g_pInSurfB;
                hr = StringCchCopy(szError, 256, _T("Couldn't read Image B\n"));
                // TODO: Add error handling code here.
            }

            InitializeFileOpenDlg( g_hDlg );
            if (PopFileOpenDlg (g_hDlg, szFileName, szTitleName))
            {
                if (!ReadImageFile (ppSurface, szFileName))
                    MessageBox( g_hDlg, szError, _T("Load Error"),
                    MB_ICONERROR );
            }

            // If we have both surfaces, force send a resize
            // to force Setup and Repaint.
            HandleSize( g_hDlg, 0, 0, 0 );
            break;

First, the function initializes a DXSurface pointer and file name string. The DXSurface pointer refers to either Image A or Image B, depending on which image is being loaded. The routine then opens a dialog box so the user can select an image for loading.

When the routine has a file name for the image, it calls the ReadImageFile function located in Commdlg.cpp.

BOOL ReadImageFile (IDXSurface **lplpDXSurf, PTSTR pstrFileName)
{
    WCHAR   pwcsBuffer[256] ;
    HRESULT hr;
    
#ifndef UNICODE
    size_t cchLength;
    hr = StringCchLengthA(pstrFileName, 256, &cchLength);
    // TODO: Add error handling code here.
    mbstowcs( pwcsBuffer, pstrFileName, cchLength);
    // TODO: Add code to make sure the conversion was successful.
#endif

    // Load Input surfaces.
    hr = g_pSurfFact->LoadImage( pwcsBuffer, NULL, NULL,
    &DDPF_PMARGB32, IID_IDXSurface, (void**)lplpDXSurf );

    if (FAILED(hr))
        MessageBox(g_hDlg, _T("Couldn't load image!"), _T
        ("Error Loading"), MB_ICONERROR );

    return SUCCEEDED( hr );
}

This routine uses the Surface Factory to create a DXSurface with the IDXSurfaceFactory::LoadImage method. The first parameter is the file name of the image to load. The next two parameters are used only if the source is a Microsoft DirectDraw image, which is not the case in this example. The pixel format to use for this DXSurface is specified by the fourth parameter. In this example, the Surface Factory is set to store the image in a 32-bit, alpha-premultiplied PMARGB32 format. You can use any of 12 formats, and the Surface Factory converts the image format to the one specified. The fifth parameter specifies that the Surface Factory should return a pointer to an IDXSurface interface, and the last parameter is the location where the pointer should be stored.

The last step of the image loading process involves defining the input and output surfaces for the Wipe transform. This is initiated by a routine in the HandleSize function.

    // Create output surface.
    SAFE_RELEASE(g_pOutSurf);
    CDXDBnds bnds;
    bnds.SetXYSize(DCWidth, DCHeight);
    hr = g_pSurfFact->CreateSurface(NULL, NULL, &DDPF_PMARGB32,
    &bnds, 0, NULL, IID_IDXSurface, (void**)&g_pOutSurf );
                                        
    if (g_pInSurfA && g_pInSurfB)
    {
          if( SUCCEEDED( hr ) )
        {
            IUnknown* In[2];
            IUnknown* Out[1];
            In[0] = g_pInSurfA;
            In[1] = g_pInSurfB;
            Out[0]= g_pOutSurf;
            hr = g_pWipeTrans->Setup( In, 2, Out, 1, 0 );
        }
    }

If Image A and Image B are loaded, the routine creates an output surface based on the size of the dialog box window. The routine then initializes the arrays of input and output surface pointers to be used by the transform. For the Wipe transform, there are two inputs and one output. Finally, the IDXTransform::Setup method is called with the number of inputs and outputs, and zero for an unused parameter. After this routine has run, the GradientWipe transform has pointers to its image inputs and a location to store its output.

Performing the Transform

Each transform that supports the IDXEffect interface has two properties that are used to produce an effect: IDXEffect::Progress and IDXEffect::get_Duration. The IDXEffect::Progress property is a number from zero through one that specifies how much of the transform has been completed. In the case of the Wipe transform, setting Progress to zero shows only Image A, while setting it to 1 shows only Image B. Setting it to 0.5 shows some of Image B on the left, some of Image A on the right, and a blurry mixture of both images in the middle. This effect of setting the IDXEffect::Progress value is specific to the Wipe transform. Changing this value on a different transform has a different effect on the output. The IDXEffect::get_Duration variable stores the length of time, in seconds, that it should take for the transform to occur.

Each transform can support a custom interface that gives the transform properties that are specific to the kind of effect it is producing. For the Wipe transform, one custom property is the GradientSize. It specifies the width of the transition region between Image A and Image B as a fraction of the total window width. Setting a GradientSize of 0.01 produces a sharp transition region that sweeps across the screen, as if Image A were being peeled away to show Image B. Setting this value to 2.0 produces a gradient wider than the window, with Image A gradually fading to become Image B. Another property specific to transform type is the WipeStyle, which tells the gradient region to sweep left to right, or top to bottom. These properties can be set directly in code or changed through a dialog box created by the transform. The WipeDlg function uses the latter option in its HandleProperties function.

The Visual Filters and Transitions Reference section provides information about the custom properties of each transform. You don't have to set these properties, though. Each transform is created with reasonable default values for each property.

The following section shows how the transform is used in C++. A transform is started when the user clicks either Play button or adjusts the IDXEffect::Progress trackbar control in the dialog box. In the following example, assume that the user adjusts the trackbar control to somewhere in the middle of its range. The message handler in MainWndProc receives a WM_HSCROLL message and calls the HandleTrackBar function.

/////////////////////////////////////////////////////////////////
void HandleTrackBar( HWND hwnd, HWND hctl, UINT code, int pos)
/////////////////////////////////////////////////////////////////
{
    int     nPosition   = pos;
    TCHAR   szString[256];

    if ( (code == SB_THUMBPOSITION) && !CheckImages() )
        return;

    nPosition   = SendMessage( hctl, TBM_GETPOS, 0, 0 );
    StringCchPrintf(szString, 256, _T("Progress: %u%%"), nPosition);
    // TODO: Add error handling code here.
    
    SetDlgItemText( hwnd, IDT_PCTCOMP, szString);

    g_pEffect->put_Progress( (float)(nPosition/100.0) );

    HandleDrawItem( NULL, NULL );

    return;
}

The first statement checks that the two images are loaded, and if not, returns from the function. Otherwise, the statement uses the SendMessage function to retrieve the current trackbar control position and writes the value as text to the dialog box window. The next call uses the transform IDXEffect interface created earlier to set the IDXEffect::Progress of the transform. Because the IDXEffect::Progress property is a value from zero to one, and IDXEffect::Progress ranges from zero to 100, it is converted and passed to the put_Progress method.

The application must now redraw the screen based on the new value for the Progress property. WipeDlg has a separate function for doing this, called HandleDrawItem. This is an important function, which is explained in steps after the following sample code.

/////////////////////////////////////////////////////////////////
void HandleDrawItem( HWND hwnd, const DRAWITEMSTRUCT *lpDrawItem )
/////////////////////////////////////////////////////////////////
//
// Retrieves a display context from the owner draw button
// on the dialog box, and then executes a frame of the transform
// and blits the result over the owner draw button.
// 
// The arguments are ignored.
    HDC                 hdc         = NULL;
    HRESULT             hr          = S_OK;
    HDC                 hdcSurf     = NULL;
    IDirectDrawSurface  *pDDSurf    = NULL;
    HWND                hOwnerDraw  = NULL;
    DWORD               DCWidth, DCHeight;
    RECT                rect;

    // Get the handle and size of the owner draw window.
    hOwnerDraw  = GetDlgItem( g_hDlg, IDB_OWNERDRAW );
    GetClientRect( hOwnerDraw, &rect );
    DCWidth     = rect.right;
    DCHeight    = rect.bottom;

    // Get the display context as a place to blit.
    hdc = GetDC(hOwnerDraw);

    // Erase the output surface with a color.
    DXFillSurface( g_pOutSurf, 0xFFC0C0FF, false );

The function starts by defining some variables, including handles to the device context for image output and a DirectDrawSurface to store the transformed result. It is followed by the GetDlgItem function that retrieves a handle to the dialog box image window, and the GetClientRect function, which retrieves the window's size. This enables the function to draw an image of the correct size. The setup for the transform continues as the IDXDCLock::GetDC function returns the output device context and the DXFillSurface helper function fills the output DXSurface with a default color.

With both input surfaces defined, the output surface initialized, and the value of the IDXEffect::Progress property set, the transform is ready to execute, as shown in the following code example.

    // Execute if we have input.
    if (g_pInSurfA && g_pInSurfB)
    {
        // Execute the transform.
        DXVEC Placement = { DXBT_DISCRETE, 0 };

        hr = g_pWipeTrans->Execute( NULL, NULL, &Placement );
    }

You must specify a position for the transformed image on the output surface, as well as the variable type used to store the position for each transform. Passing a DXBT_DISCRETE from the DXBNDTYPE enumeration specifies LONG variables for the position. The zero specifies that you want no special position offset for the output image.

The important method call in the previous code example is IDXTransform::Execute. It takes the two input images and combines them to fill the output surface. The first parameter passed to the method is used for multithreaded transforms and is usually set to NULL. The second parameter specifies the portion of the output image to be blitted to the output surface. To show all the output, this is set to NULL. The last parameter is the output image position.

If the transform is successful, it returns S_OK, and the application continues.

    if( SUCCEEDED( hr ) )
    {
        // Get the DirectDrawSurface from the output DXTransform
        // surface.
        hr = g_pOutSurf->GetDirectDrawSurface(
        IID_IDirectDrawSurface, (void**)&pDDSurf );
        if( SUCCEEDED( hr ) )
        {
            // Get the DC from the DirectDraw surface.
            hr = pDDSurf->GetDC( &hdcSurf );
        }
    }

    if( SUCCEEDED( hr ) )                                            
    {
        // Blit the output surface to the device context on the screen.
        BOOL bStat = BitBlt( hdc, 0, 0, DCWidth, DCHeight,
        hdcSurf, 0, 0, SRCCOPY );
        hr = pDDSurf->ReleaseDC( hdcSurf );
    }

    // Clean up and exit.
    ReleaseDC( hOwnerDraw, hdc );
    SAFE_RELEASE( pDDSurf );

    return;

The remainder of the function takes care of rendering the output image to the screen, which uses standard DirectDraw methods. To do this, you need a pointer to a DirectDrawSurface that represents the transformed image. All DXSurface objects have the IDXSurface::GetDirectDrawSurface method, which does just this. The first parameter of the method specifies a pointer to a DirectDrawSurface, and the second parameter is a reference to the returned pointer. From there, it's a matter of getting a device context for the DirectDrawSurface and blitting the result to the window. For more information on how that is done, consult a DirectDraw reference.

Sweeping Left and Right

The previous section showed how to draw one frame of a transform, based on the value of the IDXEffect::Progress property. Sweeping the transform left to right is just a loop over IDXEffect::Progress, calling HandleCommand to update the screen for each iteration. The feature shown in the following code example is the IDXEffect::get_Duration property, which determines the length of time for the transform to complete.

/////////////////////////////////////////////////////////////////
void HandlePlayForward( void )
/////////////////////////////////////////////////////////////////
{
    float Duration, Progress = 0;
    DWORD msStartTime = timeGetTime();
    ULONG ulFrames = 0;

    if (CheckImages())
    {
        // This duration can be set with the property page for
        // the effect. It defaults to .50.
        g_pEffect->get_Duration( &Duration );

        do
        {
            // Set Progress.
            g_pEffect->put_Progress( Progress );

            // Cause repaint.
            HandleDrawItem( NULL, NULL );

            // Next
            Progress = (float)(( (timeGetTime() - msStartTime) /
            1000. ) / Duration);
            ++ulFrames;
        }
        while( Progress <= 1. );

        // Update the dialog box with the frame rate.
        TCHAR szFrames[20];
        StringCchPrintf( szFrames, 20, _T("Frames/Sec = %.2f"), (
        double)ulFrames / Duration );
        // TODO: Add error handling code here.
        SetDlgItemText( g_hDlg, IDT_FRAMES_PER_SEC, szFrames );
    }

    return;
}

The function starts by recording the start time of the transform with the Windows timeGetTime function and by initializing the IDXEffect::Progress property to zero. The transform duration is then obtained by using the IDXEffect::get_Duration method of the IDXEffect interface. The function enters the do loop by updating the most recent value for the transform's IDXEffect::Progress property with the IDXEffect::Progress method. HandleCommand updates the screen, and then IDXEffect::Progress is recalculated based on the elapsed time. When the elapsed time is greater than the IDXEffect::get_Duration, the loop ends. When the transform completes, the frame rate is calculated and displayed, showing how quickly your computer rendered the images.

To sweep the transform in the other direction, the HandlePlayBackward function is used. It is much the same as HandlePlayForward, but with the loop over IDXEffect::Progress moving from one to zero instead of from zero to one.

Much of the previous code example deals with standard function calls in a Windows application. However, the code for performing a sophisticated transform takes only a few lines. Setting things up requires calls to methods in the IDXTransformFactory, the IDXSurfaceFactory, IDXTransform, and IDXEffect interfaces. Performing the transform requires setting the IDXEffect::Progress value and calling IDXTransform::Execute. Microsoft DirectX Transform takes care of all the details for combining the images.

What Next?

For information on how the GradientWipe transform combines images to produce its output, see Example: The Wipe Transform.