Monday, July 23, 2007

Displaying the Calendar view on a DateTimePicker Control in .NETCF

I've recently made a solution where the customer requested to be able to bring up the calendar view in a DateTimePicker control by pressing on a specific button on the screen. The solution to that was really simple: Create a control that inherits from System.Windows.Forms.DateTimePicker and add a method called ShowCalendar() which I call to bring up the Calendar view.


public class DateTimePickerEx : DateTimePicker
{
  [DllImport("coredll.dll")]
  static extern int SendMessage(
    IntPtr hWnd, uint uMsg, int wParam, int lParam);

  const int WM_LBUTTONDOWN = 0x0201;

  public void ShowCalendar() {
    int x = Width - 10;
    int y = Height / 2;
    int lParam = x + y * 0x00010000;

    SendMessage(Handle, WM_LBUTTONDOWN, 1, lParam);
  }
}

Monday, July 16, 2007

Generic Multiple CAB File Installer for the Desktop

In this article I would like to share a nice and simple multiple CAB file installer application for the desktop that I have been using for a few years now. The tool can install up to 10 CAB files and is written in C and has less than a 100 lines of code.

Before we get started with installing multiple CAB files with a generic installer, let's try to go through the process of installing a CAB file from the desktop.

Installing a CAB file from the desktop to the mobile device is pretty simple. All you need to do is create a .INI file that defines the CAB files you wish to install and launch the Application Manager (CeAppMgr.exe) passing the .INI file (full path) as the arguments. Application Manager is included when installing ActiveSync or the Windows Mobile Device Center (Vista)

An Application Manager .INI file contains information that registers an application with the Application Manager. The .INI file uses the following format:

[CEAppManager]
Version = 1.0
Component = component_name

[component_name]
Description = descriptive_name
CabFiles = cab_filename [,cab_filename]

Here's an MSDN link for more details on Creating an .ini File for the Application Manager

Before launching the Application Manager, we should first make sure that it's installed. Once we know that then we get the location of the file. The easiest way to do this programmatically is to look into the registry key SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\CEAppMgr.exe. If this key doesn't exist, then the Application Manager is not installed.

The next step is quite interesting yet very simple. In this installer application, I use a configuration file called Setup.ini which defines which CAB files I want to install. Setup.ini will look like this:

[CabFiles]
CABFILE1 = MyApp1.ini
CABFILE2 = SQLCE.ini
CABFILE3 = NETCF.ini
CABFILE4 =
CABFILE5 =
CABFILE6 =
CABFILE7 =
CABFILE8 =
CABFILE9 =
CABFILE10 =

You can easily modifiy the code to install more than 10 CAB files if you think it's appropriate. I used GetPrivateProfileString() to read the values from my configuration file.

Setup.ini, the .INI files, and the actual CAB files are required to be in the same directory as the generic installer.

Ok, now we have Application Manager command-line arguments ready, we now just have to launch it. I used CreateProcess() to launch the application manager and used WaitForSingleObject() to wait for the process to end.

Here's the full source code:

[C CODE]

#include "windows.h"
#include "tchar.h"

#define CE_APP_MGR TEXT("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\CEAppMgr.exe")

LPTSTR GetParameters()
{
  TCHAR szParams[2048];
  szParams[0] = TCHAR(0);

  TCHAR szCurrDir[MAX_PATH];
  GetCurrentDirectory(MAX_PATH, szCurrDir);

  for (int i = 1; i < 11; i++) {
    TCHAR buffer[16];
    TCHAR szKey[16];

    strcpy(szKey, TEXT("CABFILE"));
    itoa(i, buffer, 16);
    strcat(szKey, buffer);

    TCHAR szSetupIni[MAX_PATH];
    strcpy(szSetupIni, szCurrDir);
    strcat(szSetupIni, TEXT("\\"));
    strcat(szSetupIni, TEXT("Setup.ini"));

    TCHAR szCabFile[MAX_PATH];
    ::GetPrivateProfileString(TEXT("CabFiles"), szKey,
      (TCHAR*)"", szCabFile, sizeof(szCabFile), szSetupIni);

    if (0 != strcmp(szCabFile, (TCHAR*)"")) {
      strcat(szParams, TEXT(" \""));
      strcat(szParams, szCurrDir);
      strcat(szParams, TEXT("\\"));
      strcat(szParams, szCabFile);
      strcat(szParams, TEXT(" \""));
    }
  }

  return szParams;
}

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
  HKEY hkey;
  DWORD dwDataSize = MAX_PATH;
  DWORD dwType = REG_SZ;
  TCHAR szCEAppMgrPath[MAX_PATH];

  if(ERROR_SUCCESS == RegOpenKeyEx(HKEY_LOCAL_MACHINE, CE_APP_MGR, 0, KEY_READ, &hkey)) {
    if(ERROR_SUCCESS != RegQueryValueEx(hkey, NULL, NULL,
      &dwType, (PBYTE)szCEAppMgrPath, &dwDataSize))
    {
      MessageBox(NULL, TEXT("Unable to find Application Manager for Pocket PC Applications"),
        TEXT("Error"), MB_ICONEXCLAMATION | MB_OK);
      return 1;
    }
  } else {
    MessageBox(NULL, TEXT("Unable to find Application Manager for Pocket PC Applications"),
      TEXT("Error"), MB_ICONEXCLAMATION | MB_OK);
    return 1;
  }


  RegCloseKey(hkey);

  STARTUPINFO startup_info = {0};
  PROCESS_INFORMATION pi = {0};
  startup_info.cb = sizeof(startup_info);

  if (CreateProcess(szCEAppMgrPath, GetParameters(), NULL,
    NULL, FALSE, 0, NULL, NULL, &startup_info, &pi))
  {
    WaitForSingleObject(pi.hProcess, INFINITE);
  } else {
    MessageBox(NULL, TEXT("Unable to Launch Application Manager for Pocket PC Applications"),
      TEXT("Error"), MB_ICONEXCLAMATION | MB_OK);
    return 2;
  }

  return 0;
}


When deplying my applications in this manner, I like packaging them in a self-extracting zip file that is configured for software installation. I'm still holding to an old version of Winzip Self-Extractor to accomplish this task.

Another reason I use WaitForSingleObject is because a self-extracting zip installer file deletes the all the temporary files it has extracted once the installer process ends. This means that your .INI and CAB files will be deleted even before they get copied to the device.

Monday, July 9, 2007

Accessing Windows Mobile 6.0 Sound API's through .NETCF

A new set of APIs were introduced in Windows Mobile 6 to make it easier to manage and play sound. The new API's support playing sounds in a variety of formats that Windows Media Player supports

These API's are really easy to use. You can play a sound file with a single function call. Let's try to do that through .NETCF by P/Invoking SndPlaySync(LPCTSTR pszSoundFile, DWORD dwFlags).


[DllImport("aygshell.dll")]
static extern uint SndPlaySync(string pszSoundFile, uint dwFlags);

void PlaySound() {
  SndPlaySync("\\Storage Card\\Intro.mp3", 0);
}


In the previous sample, we are playing a sound file synchronously. Now, this is interesting in a way that its very very easy to play an audio file. But what really gets interesting is that the new Sound API provides methods for playing sound files asynchronously.

To play audio files asynchronously, we will need to call 4 methods from the Sound API.

  SndOpen(LPCTSTR pszSoundFile, HSOUND* phSound)
  SndPlayAsync(HSOUND hSound, DWORD dwFlags)
  SndClose(HSOUND hSound)
  SndStop(SND_SCOPE SoundScope, HSOUND hSound)

Let's start by declare our P/Invokes


[DllImport("aygshell.dll")]
static extern uint SndOpen(string pszSoundFile, ref IntPtr phSound);

[DllImport("aygshell.dll")]
static extern uint SndPlayAsync(IntPtr hSound, uint dwFlags);

[DllImport("aygshell.dll")]
static extern uint SndClose(IntPtr hSound);

[DllImport("aygshell.dll")]
static extern uint SndStop(int SoundScope, IntPtr hSound);


Now that we have our P/Invokes ready. Let's start playing with the Sound API in .NETCF. In the sample below, the application will play the audio file Intro.mp3 located in the Storage Card. To play an Audio file asynchronously, we will first need a handle to the audio file. We use SndOpen(string, IntPtr) to accomplish that. Once we have the handle to the audio file, we can call SndPlayAsync(IntPtr, int) to start playing the audio file. To stop playing the audio we just have to close the handle and call SndStop(SND_SCOPE_PROCESS, IntPtr.Zero) to stop the playback of the sound.


IntPtr hSound = IntPtr.Zero;
const string AUDIO_FILE = "\\Storage Card\\Intro.mp3";
const int SND_SCOPE_PROCESS = 0x1;

void Play() {
  SndOpen(AUDIO_FILE, ref hSound);
  SndPlayAsync(hSound, 0);
}

void Stop() {
  SndClose(hSound);
  SndStop(SND_SCOPE_PROCESS, IntPtr.Zero);
}


How cool is that? You can now easily add some cool sound effects to your application. Maybe even use the Sound API for one of those annoying startup sounds!