30 Days of Windows Mobile – Day 07: Mobile FX

Screenshot of Mobile FX ApplicationMobile FX is a fun sound effects engine for Windows Mobile. It displays a series of graphical buttons which clicked produce different sound effects. The user interface is driven by an XML based configuration file that enables the user to change which sound effects are available.

This is the 7th application in the 30 days of Windows Mobile series and marks my current re-commitment to progress in converting the number of applications currently sitting on my harddrive into blog posts. My hat is off to Chris Craft who managed 30 applications in 30 days. Although I took a similar amount of time to convert his C# applications into C++, it is taking me a lot longer to write the associated blog posts…

Mobile FX is an interesting application, in order to develop it in native code we will need to cover a number of new technologies and APIs which we have not covered before.

Creating controls at runtime

Up until now all of our applications have statically placed controls within the user interface via a dialog resource located in a *.rc resource file.

This application however specifies a variable number of sound effects within an XML based configuration file. Since the number of button controls required could change, it is easier to create the controls dynamically at runtime. The CreateWindow API enables us to create a control at runtime as shown below

// Create a static (label) control at runtime
HWND hWndPicture = CreateWindow(_T("static"),
  NULL,
  WS_CHILD | WS_VISIBLE | SS_BITMAP | SS_NOTIFY, 
  left, top, width, height,
  hDlg, NULL,
  GetModuleHandle(NULL),
  NULL);

The first string parameter to CreateWindow specifies the type of control to create. A future blog post will discuss how to register your own custom controls so that they can be created via CreateWindow. The other parameters specify various properties related to the new control, such as its style, location and size.

If we want to make use of the GetDlgItem function to reference the newly created control we need to associate an ID with it. One way to do this is to use the SetWindowLong function as demonstrated below:

// Associate the id '1234' with the control 'hWnd'
SetWindowLong(hWnd, GWL_ID, 1234);

Turning relative paths into full paths

Most file based APIs within Windows CE require absolute file paths which start at the root of the filesystem (i.e. we must specify “\path\to\some\file.txt” instead of “file.txt”).

If we want to use relative file paths within our application we must manually convert them into absolute paths before passing them to system APIs. The OS provides no concept of a current working directory.

To convert a path specified relative to the directory the application is installed in we can make use of the following helper routine.

void GetFullPathToFile(LPTSTR pszFullPath, LPCTSTR pszFilename)
{
  // Find the path to the current executable
  GetModuleFileName(GetModuleHandle(NULL),
    pszFullPath, MAX_PATH);
 
  // Strip off the exe filename and replace it with
  // the path provided by our second parameter.
  wcscpy(wcsrchr(pszFullPath, '\\') + 1, pszFilename);
}

The trick is to use the GetModuleHandle and GetModuleFileName APIs to determine the path of the current executable. Once this is found we can use string manipulation functions to remove the *.exe file name and replace it with the name of the file we expect to find in the same folder.

An example use of the function is shown below.

// Place the absolute path to "test.wav" into szPath
TCHAR szPath[MAX_MATH];
GetFullPathToFile(szPath, L"test.wav");

Playing sounds

Instead of using the SndPlaySync API I decided to use an older API called PlaySound. Unlike SndPlaySync the PlaySound API is available on devices prior to Windows Mobile 6, the disadvantage however is that it only supports *.wav files (refer to my blog post about “Making use of new APIs within older applications” for one technique to support the use of new APIs while still allowing limited functionality on older devices).

To play a sound effect we simply pass the full path to the required *.wav file to PlaySound along with a couple of flags.

// Start to asyncronously play a sound
LPCTSTR pszSoundEffect = _T("\\path\\to\\some.wav");
PlaySound(pszSoundEffect, NULL,
  SND_ASYNC | SND_FILENAME | SND_NODEFAULT);

This code snippet makes use of the SND_ASYNC flag. This means that the call to PlaySound does not block until the sound effect finishes playing. Instead the call returns immediately and the sound effect plays in the background.

Playing the sound effects asynchronously enables the user to interrupt the currently playing sound by selecting another sound effect. To stop any currently playing sound effects you can pass in NULL for the sound effect filename.

// Stop any currently playing sounds
PlaySound(NULL, NULL, SND_FILENAME);

Using Common Object Model (COM) objects

The early origins of the .NET runtime can be traced to the older COM and COM+ frameworks. As an example of this the commonly referenced mscorlib assembly at one stage stood for “Microsoft COM Object Runtime (COR) library“.

The Common Object Model (COM) framework can be considered to have many of the same design goals as the Common Language Runtime (CLR), such as the ability for objects written in various languages to interoperate with each other. The first step of using COM objects within an application is to initialise the framework by calling the CoInitializeEx API.

// Initilise the COM framework
CoInitializeEx(NULL, COINIT_MULTITHREADED);

Once we have initialised the COM runtime we can request objects to be created by calling the CoCreateInstance API, passing in the class id (CLSID) of the specific class we want to create an instance of. A CLSID is a unique GUID value which uniquely identifies a class. It enables the COM framework to lookup the registry to find details about how and where the class is implemented.

// Create an instance of the 'AAA" class and return a pointer
// to the newly created object in the pThingy variable
IAAA *pThingy = NULL;
CoCreateInstance(CLSID_AAA, NULL, CLSCTX_INPROC_SERVER,
  IID_IAAA, (void**)&pThingy);
 
  // ... use the object ...
 
// Release our reference to the object
pThingy->Release();

Unlike the CLR framework the COM framework does not implement garbage collection, instead it is a reference counted API. Each object has a counter associated with it. Whenever an object is passed to another section of code its reference counter should be increased via a call to the AddRef method. Likewise when a section of code finishes using an object it should decrement the reference counter by calling the Release method. If Release causes the reference counter to decrease to zero the object’s resources are automatically freed. AddRef and Release are both part of an interface called IUnknown.

Drawing PNGs

Previous applications in this series have utilised Windows Bitmap (*.bmp) formatted images. This application is different since the application includes PNG formatted images to display on the sound effect buttons.

Prior to Windows Mobile 5.0 there was no general purpose API to manipulate images compressed in common image formats such as JPEGs, PNGs or GIFs (although there were options such as SHLoadImageFile). Windows Mobile 5.0 introduced a COM based Imaging API that offers a large number of image codecs as well as image manipulation functionality.

One benefit of using this API with PNG files is the ability for it to keep alpha transparency information in order to draw non rectangular images over top of existing content. The following subroutine can be used to draw a PNG onto an existing GDI device context.

// Draw an alpha blended image on the device context 'hdc'
// within the rectangle defined by 'prcBounds'.
static BOOL DrawAlphaImage(HDC hDC, RECT * prcBounds,
                           WCHAR * pszImageFileName)
{
  // Create an instance of the ImagingFactory class
  IImagingFactory *pFactory = NULL;
  HRESULT hr = CoCreateInstance(CLSID_ImagingFactory, NULL,
      CLSCTX_INPROC_SERVER, IID_IImagingFactory, (void**)&pFactory);
  if (hr == S_OK)
  {
    // Call the CreateImageFromFile method to load the image
    // into memory.
    IImage *pImage = NULL;
    hr = pFactory->CreateImageFromFile(pszImageFileName, &pImage);
    if (hr == S_OK)
    {
      // And finally draw the image onto the device context
      hr = pImage->Draw(hDC, prcBounds, NULL);
 
      // Free the COM objects
      pImage->Release();
      pImage = NULL;
    }
 
    pFactory->Release();
    pFactory = NULL;
  }
 
  return (hr == S_OK);
}

Parsing XML

This is the first application that has required the parsing of an XML document. For this we can use another COM based API, the Microsoft (MSXML) DOM Parser. The first step is to create an instance of the DOM Document class.

// Create an instance of the XML Document class
IXMLDOMDocument *pDoc = NULL;
HRESULT hr = CoCreateInstance(CLSID_DOMDocument, NULL,
  CLSCTX_INPROC_SERVER, IID_IXMLDOMDocument, (void**)&pDoc);
if (hr == S_OK)
{
  // ... use the DOM document ...
}

Once we have an instance of the DOM document we can load an XML document into it by specifying the path to the XML file.

// Load the XML file
VARIANT_BOOL bSuccess = VARIANT_FALSE;
VARIANT filename;
 
VariantInit(&filename);
V_BSTR(&filename) = SysAllocString(L"\\path\\to\\doc.xml");
V_VT(&filename) = VT_BSTR;
 
hr = pDoc->load(filename, &bSuccess);
if (hr == S_OK && bSuccess == VARIANT_TRUE)
{
  // ... make use of the document ...
}

There is a small amount of complexity here due to the load method making use of the VARIANT datatype (essentially a strongly typed container which can hold data of various datatypes).

Once we have loaded the XML document into memory we can query against it in a number of ways, including selecting a set of nodes via an XPATH expression.

IXMLDOMNodeList *pNodes = NULL;
LONG len = 0;
BSTR xpath = SysAllocString(L"/MobileFX/SoundPack/Buttons");
 
// Execute an XPATH query to obtain the set of nodes
// which match the predicate
if (pDoc->selectNodes(xpath, &pNodes) == S_OK
  && pNodes->get_length(&len) == S_OK)
{
  // Then iterate over each of the nodes
  // returned
  for (LONG i = 0; i < len; i++)
  {
    // Fetch the next node
    IXMLDOMNode *pNode = NULL;
    if (pNodes->get_item(i, &pNode) == S_OK)
    {
      // ... process the node ...
 
      pNode->Release();
    }
  }
}
 
pNodes->Release();

Reading the contents of the XML configuration file enables us to glue together a number of the code snippets shown previously. For each button found in the XML document we can dynamically create a button control, draw the PNG based icon onto it and load the sound effect specified by a relative path.

Sample Project

[Download mobilefx.zip - 2.34 MB]

The sample application available for download can be a lot of fun. However there are a couple of activities left as exercises for the interested developer. These would make great learning experiences, for example:

  • Add a menu item that allows the user to switch between multiple sound pack XML configuration files.
  • Add scrolling so a sound pack XML configuration file with more than 16 sound effects can be scrolled through to access additional sound effects.
  • Modify the CreateTile function so the sound effect images do not need to be “pre-buttonized”? The round edges and glossy finish should be able to be applied over top of a standard rectangular image by making multiple calls to the DrawAlphaImage function. Take a look at a tool such as Axialis IconWorkshop for some inspiration, and if you have a copy of Visual Studio 2008 you can download a special version for free! The iPhone does a similar thing for application and website shortcut icons.

5 Responses to “30 Days of Windows Mobile – Day 07: Mobile FX”

  1. Ken says:

    Christopher,
    I followed, and continue to follow Chris Craft’s blog and 30 days.
    I am an embedded C programmer, and have had a WM device for years. So, I thought I’d try some tinkering with development for the WM device.
    I got the 90 day trial of Visual Studio 2008 Pro, and have gotten a rudimentary program working, one C#, then a C++ sample.

    I was wondering about your “30 Days” program:
    1) are you using Visual Studio 2005 (it was “converted” when I opened it in VS 2008 Pro?
    2) I have WM5 SDK selected, and when I build I get the message “Project not selected to build for this solution configuration”?
    I also tried selecting Debug and Release, but I get the same message. Is this a windows mobile app?
    3) I found the MobileFXCAB.CAB file and installed it on my Dell Axim x51v and it runs OK, the icons are all in the upper left quarter of the screen (perhaps a screen size selection in the code thing, not a big deal).

    If you can let me know, then I’ll tinker with this some more.
    Thanks
    Ken

  2. Tom_in_AZ says:

    Hello,
    Is there a way to create multiple pages of sounds? I want to add onto the stock offerings. I can put my sounds into the default folder along with my icons, and they work, but I can not use any of the stock ones. My present work around makes me have to go and rename the default.xml file in order to change to the group of files that I wish to use. Even adding a 17th entry to the stock default.xml does not allow the program to load wit the “Could not parse soundpack file” error.

    I would honestly love to have 3 or 4 pages of sound effects and just switch on the fly easy using the joy pad left and right buttons as opposed to the other way. Other than that, I love this program and I really appreciate that you shared it.
    Thank you,
    Tom

  3. Tom_in_AZ says:

    One last question… What is the max size of the .WAV that can be loaded?

  4. SztupY says:

    MANY thanks for this tutorial! Without it I’ve could never open a PNG file on my phone and draw it on the screen. This tutorial is a real all-in-one, with information about how to use COM services, XML reading and drawing on the screen.

  5. Dr.Luiji says:

    Great post, thanks a lot.

Leave a Reply