Commentor Blog

When Quality Matters

Commentor A/S

When Quality Matters

Contact usSend mail

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2010

ListView Custom Drawing in .NETCF

In this article I would like to demonstrate how to do custom drawing in the ListView control that the .NET Compact Framework provides. I'll be extending the code I published previously in the article entitled ListView Extended Styles in .NETCF

This is normally a very tedious and frustrating task to do and to accomplish this task we'll have to take advantage of the custom drawing service Windows CE provides for certain controls. A very good reference for custom drawing is an MSDN article called Customizing a Control's Appearance using Custom Draw. Before going any further, I may have to warn you about the extensive interop code involved in this task.

We'll have to handle the ListView windows messages ourselves, and we accomplish this by subclassing this ListView. Subclassing a window means that we assign a new window procedure for messages that are meant for the ListView. This can be done through the SetWindowLong() method with the GWL_WNDPROC parameter. When subclassing, the developer is responsible for choosing which messages they want to handle, which to ignore, and which they let operating system handle. To have the operating system handle the message, a call to CallWindowProc() is done using a pointer to original window procedure.

Before setting the new window procedure its important to get a pointer to the original one in case the developer wishes to let the operating system handle the message. This is done through GetWindowLong()Let's get started...First we need to define the interop structures for custom drawing

 

    struct RECT
    {
        public int left;
        public int top;
        public int right;
        public int bottom;
    }
 
    struct NMHDR
    {
        public IntPtr hwndFrom;
        public IntPtr idFrom;
        public int code;
    }
 
    struct NMCUSTOMDRAW
    {
        public NMHDR nmcd;
        public int dwDrawStage;
        public IntPtr hdc;
        public RECT rc;
        public int dwItemSpec;
        public int uItemState;
        public IntPtr lItemlParam;
    }
 
    struct NMLVCUSTOMDRAW
    {
        public NMCUSTOMDRAW nmcd;
        public int clrText;
        public int clrTextBk;
        public int iSubItem;
        public int dwItemType;
        public int clrFace;
        public int iIconEffect;
        public int iIconPhase;
        public int iPartId;
        public int iStateId;
        public RECT rcText;
        public uint uAlign;
    }
 

Note: In C# (and VB and C++), the StructLayout is Sequencial by default, hence I didn't state itThe P/Invoke declarations we need are the following: 

 

    [DllImport("coredll.dll")]
    static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
 
    [DllImport("coredll")]
    static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, ref RECT lParam);
 
    [DllImport("coredll.dll")]
    static extern uint SendMessage(IntPtr hwnd, uint msg, uint wparam, uint lparam);
 
    [DllImport("coredll.dll", SetLastError = true)]
    static extern int SetWindowLong(IntPtr hWnd, int nIndex, WndProcDelegate newProc);
 
    [DllImport("coredll.dll", SetLastError = true)]
    static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
 

And to make life a bit easier, I created some extension methods to the RECT struct we just defined.

 

     static class RectangleExtensions

    {
        public static Rectangle ToRectangle(this RECT rectangle)
        {
            return Rectangle.FromLTRB(rectangle.left, rectangle.top, rectangle.right, rectangle.bottom);
        }
 
        public static RectangleF ToRectangleF(this RECT rectangle)
        {
            return new RectangleF(rectangle.left, rectangle.top, rectangle.right, rectangle.bottom);
        }
    }
 
 

We'll need the following constants defined in the Platform SDK 

 

    const int GWL_WNDPROC = -4;
    const int WM_NOTIFY = 0x4E;
    const int NM_CUSTOMDRAW = (-12);
    const int CDRF_NOTIFYITEMDRAW = 0x00000020;
    const int CDRF_NOTIFYSUBITEMDRAW = CDRF_NOTIFYITEMDRAW;
    const int CDRF_NOTIFYPOSTPAINT = 0x00000010;
    const int CDRF_SKIPDEFAULT = 0x00000004;
    const int CDRF_DODEFAULT = 0x00000000;
    const int CDDS_PREPAINT = 0x00000001;
    const int CDDS_POSTPAINT = 0x00000002;
    const int CDDS_ITEM = 0x00010000;
    const int CDDS_ITEMPREPAINT = (CDDS_ITEM | CDDS_PREPAINT);
    const int CDDS_SUBITEM = 0x00020000;
    const int CDIS_SELECTED = 0x0001;
    const int LVM_GETSUBITEMRECT = (0x1000 + 56);
 
 

Custom drawing in the ListView will only work in the Details view mode. To ensure this, I set the View to View.Details in the constructor method. Since I'm extending my old ListViewEx (Enables ListView Extended Styles) I'm gonna enable Double buffering, Grid lines, and the Gradient background. I'm gonna enable subclassing on the ListView only when the parent is changed, this is because I need to receive messages sent to the parent control of the ListView. We also need a delegate for the new window procedure and a pointer to the original window procedure. And last but not the least we need the actual window procedure method. 

 

    delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);
    IntPtr lpPrevWndFunc;
 
    public ListViewCustomDraw()
    {
        View = View.Details;
        DoubleBuffering = true;
        GridLines = true;
        Gradient = true;
 
        ParentChanged += delegate
        {
            lpPrevWndFunc = GetWindowLong(Parent.Handle, GWL_WNDPROC);
            SetWindowLong(Parent.Handle, GWL_WNDPROC, WndProc);
        };
    }
 
    private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
    {
        if (msg == WM_NOTIFY)
        {
            var nmhdr = (NMHDR)Marshal.PtrToStructure(lParam, typeof(NMHDR));
            if (nmhdr.hwndFrom == Handle && nmhdr.code == NM_CUSTOMDRAW)
                return CustomDraw(hWnd, msg, wParam, lParam);
 
        }
 
        return CallWindowProc(lpPrevWndFunc, hWnd, msg, wParam, lParam);
    }
 
 

In the new window procedure, we are only really interested in the WM_NOTIFY message, because this is what the NM_CUSTOMDRAW message is sent through. The LPARAM parameter of the message will contain the NMHDR which then contains the NM_CUSTOMDRAW message. The LPARAM also contains the NMLVCUSTOMDRAW which provide state and information about the ListView.

The trickiest part in performing custom drawing in the ListView is handling the drawing stage. We create a method called CustomDraw to handle the different drawing stages of the ListView 

 

    private IntPtr CustomDraw(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
    {
        int result;
        var nmlvcd = (NMLVCUSTOMDRAW)Marshal.PtrToStructure(lParam, typeof(NMLVCUSTOMDRAW));
        switch (nmlvcd.nmcd.dwDrawStage)
        {
            case CDDS_PREPAINT:
                result = CDRF_NOTIFYITEMDRAW;
                break;
 
            case CDDS_ITEMPREPAINT:
                var itemBounds = nmlvcd.nmcd.rc.ToRectangle();
                if ((nmlvcd.nmcd.uItemState & CDIS_SELECTED) != 0)
                {
                    using (var brush = new SolidBrush(SystemColors.Highlight))
                    using (var graphics = Graphics.FromHdc(nmlvcd.nmcd.hdc))
                        graphics.FillRectangle(brush, itemBounds);
                }
 
                result = CDRF_NOTIFYSUBITEMDRAW;
                break;
 
            case CDDS_SUBITEM | CDDS_ITEMPREPAINT:
                var index = nmlvcd.nmcd.dwItemSpec;
                var rect = new RECT();
                rect.top = nmlvcd.iSubItem;
                SendMessage(Handle, LVM_GETSUBITEMRECT, index, ref rect);
                rect.left += 2;
 
                Color textColor;
                if ((nmlvcd.nmcd.uItemState & CDIS_SELECTED) != 0)
                    textColor = SystemColors.HighlightText;
                else
                    textColor = SystemColors.ControlText;
 
                using (var brush = new SolidBrush(textColor))
                using (var graphics = Graphics.FromHdc(nmlvcd.nmcd.hdc))
                    graphics.DrawString(Items[index].SubItems[nmlvcd.iSubItem].Text,
                                        Font,
                                        brush,
                                        rect.ToRectangleF());
 
                result = CDRF_SKIPDEFAULT | CDRF_NOTIFYSUBITEMDRAW;
                break;
 
            default:
                result = CDRF_DODEFAULT;
                break;
        }
 
        return (IntPtr)result;
    }
 
 

In the first stage we handle is the CDDS_PREPAINT. Here we return CDRF_NOTIFYITEMDRAW to tell that we want to handle drawing of the row ourselves. After this we receive the CDDS_ITEMPREPAINT where we can draw the entire row.

We check if the row is selected through the uItemState field of NMCUSTOMDRAW, if this field has the CDIS_SELECTED flag then it means the item is selected, hence we draw a fill rectangle. After handling the CDDS_ITEMPREPAINT, we return CDRF_NOTIFYSUBITEMDRAW to tell that we want to draw the sub items ourselves.

For drawing the sub items we need to handle CDDS_SUBITEM | CDDS_ITEMPREPAINT. We can get the position index of the item through the dwItemSpec field of NMCUSTOMDRAW. To get the bounds of the current sub item we send the LVM_GETSUBITEMRECT message to the ListView and pass a pointer to RECT as the LPARAM. Before sending this message, set the "top" field of the RECT to the index of the sub item (retrieved from iSubItem field of NMLVCUSTOMDRAW. After drawing the sub item we return CDRF_SKIPDEFAULT | CDRF_NOTIFYSUBITEMDRAW to tell that we only care about handling the next sub item.

Well I hope you guys find this interesting and helpful. To keep things simple, I only demonstrated displaying plan text and a plain rectangle for the selection.

If you're interested in the full source code then you can grab it here.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by christian.resma.helle on Tuesday, July 27, 2010 2:54 AM
Permalink | Comments (1) | Post RSSRSS comment feed

ListView Background Image in .NETCF

In this short entry I'd like to demonstrate how to display a background image in the ListView control. For this we will send the LVM_SETBKIMAGE or the LVM_GETBKIMAGE message to the ListView control with the LVBKIMAGE struct as the LPARAM. Unfortunately, the Windows CE version of LVBKIMAGE does not support LVBKIF_SOURCE_URL flag which allows using an image file on the file system for the background image of the ListView.

The layout of the background image can be either tiled or specified by an offset percentage. The background image is not affected by custom drawing, unless of course you decide to fill each sub item rectangle. For setting the background image we use the LVBKIF_SOURCE_HBITMAP flag together with the layout which is either LVBKIF_STYLE_TILE or LVBKIF_STYLE_NORMAL. If we set the layout to LVBKIF_STYLE_NORMAL, then we have the option of setting where the image will be drawn by setting the value of xOffsetPercentage and yOffsetPercentage.

In this example I'd like to make use of extension methods to add the SetBackgroundImage() and GetBackgroundImage() methods to ListView. This can of course be easily used to in a property to an inherited ListView. 

public static class ListViewExtensions
{
    [DllImport("coredll")]
    static extern int SendMessage(IntPtr hWnd, int Msg, int wParam, ref LVBKIMAGE lParam);
 
    const int LVM_FIRST = 0x1000;
    const int LVM_SETBKIMAGE = (LVM_FIRST + 138);
    const int LVM_GETBKIMAGE = (LVM_FIRST + 139);
    const int LVBKIF_SOURCE_NONE = 0x00000000;
    const int LVBKIF_SOURCE_HBITMAP = 0x00000001;
    const int LVBKIF_STYLE_TILE = 0x00000010;
    const int LVBKIF_STYLE_NORMAL = 0x00000000;
 
    struct LVBKIMAGE
    {
        public int ulFlags;
        public IntPtr hbm;
        public IntPtr pszImage; // not supported
        public int cchImageMax;
        public int xOffsetPercent;
        public int yOffsetPercent;
    }
 
    public static void SetBackgroundImage(this ListView listView, Bitmap bitmap)
    {
        SetBackgroundImage(listView, bitmap, false);
    }
 
    public static void SetBackgroundImage(this ListView listView, Bitmap bitmap, bool tileLayout)
    {
        SetBackgroundImage(listView, bitmap, tileLayout, 0, 0);
    }
 
    public static void SetBackgroundImage(
        this ListView listView,
        Bitmap bitmap,
        bool tileLayout,
        int xOffsetPercent,
        int yOffsetPercent)
    {
        LVBKIMAGE lvBkImage = new LVBKIMAGE();
        if (bitmap == null)
            lvBkImage.ulFlags = LVBKIF_SOURCE_NONE;
        else
        {
            lvBkImage.ulFlags = LVBKIF_SOURCE_HBITMAP | (tileLayout ? LVBKIF_STYLE_TILE : LVBKIF_STYLE_NORMAL);
            lvBkImage.hbm = bitmap.GetHbitmap();
            lvBkImage.xOffsetPercent = xOffsetPercent;
            lvBkImage.yOffsetPercent = yOffsetPercent;
        }
 
        SendMessage(listView.Handle, LVM_SETBKIMAGE, 0, ref lvBkImage);
    }
 
    public static Bitmap GetBackgroundImage(this ListView listView)
    {
        LVBKIMAGE lvBkImage = new LVBKIMAGE();
        lvBkImage.ulFlags = LVBKIF_SOURCE_HBITMAP;
 
        SendMessage(listView.Handle, LVM_GETBKIMAGE, 0, ref lvBkImage);
 
        if (lvBkImage.hbm == IntPtr.Zero)
            return null;
        else
            return Bitmap.FromHbitmap(lvBkImage.hbm);
    }
}
 
Here's an example of exposing the background image as a property in an inherited ListView by using the extension methods above.
 
class ListViewEx : ListView
{
    public Bitmap BackgroundImage
    {
        get { return this.GetBackgroundImage(); }
        set { this.SetBackgroundImage(value, BackgroundLayout == BackgroundImageLayout.Tile); }
    }
 
    public BackgroundImageLayout BackgroundLayout { get; set; }
 
    public enum BackgroundImageLayout
    {
        Tile,
        Center
    }
}
 
A small catch with the ListView background image is that it is only supported in Windows CE 5.0 and later. Hope you found this information useful.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by christian.resma.helle on Tuesday, July 27, 2010 2:47 AM
Permalink | Comments (1) | Post RSSRSS comment feed

Unit Testing for Smart Devices Webcast

On the 28th of February 2008, Microsoft Denmark will have the first and largest online launch for Windows Server 2008, SQL Server 2008, and Visual Studio 2008. We made a few webcasts related to the products and technologies to be released. Here's one that I made entitled "Unit Testing for Smart Devices"

http://blogs.commentor.dk/downloads/smart_device_unit_testing.wmv

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by christian.resma.helle on Thursday, February 21, 2008 2:44 AM
Permalink | Comments (2) | Post RSSRSS comment feed

Transparent Controls in .NETCF

I work a lot with a graphic artist for developing solutions. The better the graphic artist you work with is, the harder it is to implement their designs to your application. One thing that my solutions have in common is that they all require transparent controls. My graphic artist loves having a image buttons on top of fancy background.

Here's some screen shots of what I've made with my graphic artist:

 

 

 

 

 

 

 

 



 

 

 

 

 

 

 

 

 

In these screen shots I heavily use a transparent label control over a Form that has a background image. I normally set my controls to be designer visible so I can drag and drop while designing. Visual Studio 2005 and 2008 will automatically load all Custom Controls and UserControls

Implementing Transparent Controls

For creating transparent controls, I need the following:

1) IControlBackground interface - contains BackgroundImage { get; }
2) TransparentControlBase - draws the BackgroundImage of an IControlBackground form
3) Transparent Control - Inherits from TransparentControlBase
4) FormBase Form - implements IControlBackground and draws the background image to the form

Let's start off with the IControlBackground interface. Like I mentioned above, it only contains a property called BackgroundImage.

public interface IControlBackground
{
  Image BackgroundImage { get; }
}

Next we will need to create the TransparentControlBase. Let's create a class that inherits from Control. We then need to override the OnPaintBackground() event to draw the IControlBackground.BackgroundImage of the Parent control. To do this, we create an instance of IControlBackground from the Parent. Once we have the BackgroundImage, we draw part of the BackgroundImage where the transparent control is lying on.

We also override the OnTextChanged() and OnParentChanged() events to force a re-draw whenever the text or parent of the control is changed.

public class TransparentControlBase : Control
{
  protected bool HasBackground = false;

  protected override void OnPaintBackground(PaintEventArgs e)
  {
    IControlBackground form = Parent as IControlBackground;
    if (form == null) {
      base.OnPaintBackground(e);
      return;
    } else {
      HasBackground = true;
    }

    e.Graphics.DrawImage(
      form.BackgroundImage,
      0,
      0,
      Bounds,
      GraphicsUnit.Pixel);
  }

  protected override void OnTextChanged(EventArgs e)
  {
    base.OnTextChanged(e);
    Invalidate();
  }

  protected override void OnParentChanged(EventArgs e)
  {
    base.OnParentChanged(e);
    Invalidate();
  }
}

Now we need to create a control that inherits from TransparentControlBase. I'll create a simple TransparentLabel control for this example. The control will have the same behavior as the standard Label control, except that it can be transparent when used over a form or control that implements IControlBackground.

public class TransparentLabel : TransparentControlBase
{
  ContentAlignment alignment = ContentAlignment.TopLeft;
  StringFormat format = null;
  Bitmap off_screen = null;

  public TransparentLabel()
  {
    format = new StringFormat();
  }

  public ContentAlignment TextAlign
  {
    get { return alignment; }
    set
    {
      alignment = value;
      switch (alignment) {
        case ContentAlignment.TopCenter:
          format.Alignment = StringAlignment.Center;
          format.LineAlignment = StringAlignment.Center;
          break;
        case ContentAlignment.TopLeft:
          format.Alignment = StringAlignment.Near;
          format.LineAlignment = StringAlignment.Near;
          break;
        case ContentAlignment.TopRight:
          format.Alignment = StringAlignment.Far;
          format.LineAlignment = StringAlignment.Far;
          break;
      }
    }
  }

  protected override void OnPaint(PaintEventArgs e)
  {
    if (!base.HasBackground) {
      if (off_screen == null) {
        off_screen = new Bitmap(ClientSize.Width, ClientSize.Height);
      }
      using (Graphics g = Graphics.FromImage(off_screen)) {
        using (SolidBrush brush = new SolidBrush(Parent.BackColor)) {
          g.Clear(BackColor);
          g.FillRectangle(brush, ClientRectangle);
        }
      }
    } else {
      using (SolidBrush brush = new SolidBrush(ForeColor)) {
        e.Graphics.DrawString(
          Text,
          Font,
          brush,
          new Rectangle(0, 0, Width, Height),
          format);
      }
    }
  }
}

Now that we have our transparent controls, we need to create a Form that will contain these controls. First we need to create a base class that will implement IControlBackground and inherit from Form.

In this example, I added a background image to the solution and as an embedded resource. My default namespace is called TransparentSample and my background image is located at the root folder with the filename background.jpg

public class FormBase : Form, IControlBackground
{
  Bitmap background;

  public FormBase()
  {
    background = new Bitmap(
      Assembly.GetExecutingAssembly().GetManifestResourceStream(
      "TransparentSample.background.jpg"));
  }

  protected override void OnPaint(PaintEventArgs e)
  {
    e.Graphics.DrawImage(background, 0, 0);
  }

  public Image BackgroundImage
  {
    get { return background; }
  }
}

For the last step, we need to create a Form that will contain these transparent controls. To start, let's add a new Form to our project and let it inherit from FormBase instead of Form.

Now we can add our transparent controls to the main form.

public class MainForm : FormBase
{
  TransparentLabel label;

  public MainForm()
  {
    label = new TransparentLabel();
    label.Font = new Font("Arial", 16f, FontStyle.Bold);
    label.ForeColor = Color.White;
    label.Text = "Transparent Label";
    label.Bounds = new Rectangle(20, 60, 200, 50);
    Controls.Add(label);
  }
}

That wasn't very complicated, was it? Having a nice and intuitive UI offers a very good user experience. Being creative, imaginative, and learning to work with a graphic artist can really pay off.

Currently rated 5.0 by 5 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by christian.resma.helle on Sunday, January 13, 2008 9:30 AM
Permalink | Comments (14) | Post RSSRSS comment feed

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.

Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Categories: Windows Mobile
Posted by christian.resma.helle on Monday, July 16, 2007 9:26 AM
Permalink | Comments (0) | Post RSSRSS comment feed

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() {
  SndPlaySnyc("\\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!

Currently rated 2.5 by 2 people

  • Currently 2.5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by christian.resma.helle on Monday, July 09, 2007 4:08 PM
Permalink | Comments (1) | Post RSSRSS comment feed

Retrieving the Icon Image within the System Image List in .NETCF

Here's a nice trick for retrieving the icon image of a file or folder from the system image list. All we actually need is to P/Invoke SHGetFileInfo and use Icon.FromHandle() to get the Icon.

First, we need to declare our P/Invokes.

[StructLayout(LayoutKind.Sequential)]
struct SHFILEINFO
{
  public IntPtr hIcon;
  public IntPtr iIcon;
  public uint dwAttributes;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
  public string szDisplayName;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
  public string szTypeName;
}

const uint SHGFI_ICON = 0x000000100;
const uint SHGFI_LARGEICON = 0x000000000;
const uint SHGFI_SMALLICON = 0x000000001;
const uint SHGFI_SELECTICON = 0x000040000;

[DllImport("coredll.dll")]
static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes,
  ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags);


To get an instance of System.Drawing.Icon for the small icon of a file

Icon GetSystemIconSmall(string file)
{
  SHFILEINFO shinfo = new SHFILEINFO();
  IntPtr i = SHGetFileInfo(file, 0, ref shinfo,
    (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_SMALLICON);

  return Icon.FromHandle(shinfo.hIcon);
}


For the large icon of a file

Icon GetSystemIconLarge(string file)
{
  SHFILEINFO shinfo = new SHFILEINFO();
  IntPtr i = SHGetFileInfo(file, 0, ref shinfo,
    (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_LARGEICON);

  return Icon.FromHandle(shinfo.hIcon);
}


For the small icon of a file when it is selected

Icon GetSystemIconSmallSelected(string file)
{
  SHFILEINFO shinfo = new SHFILEINFO();
  IntPtr i = SHGetFileInfo(file, 0, ref shinfo,
    (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_SMALLICON | SHGFI_SELECTICON);

  return Icon.FromHandle(shinfo.hIcon);
}


And last for the large icon of a file when it is selected

Icon GetSystemIconLargeSelected(string file)
{
  SHFILEINFO shinfo = new SHFILEINFO();
  IntPtr i = SHGetFileInfo(file, 0, ref shinfo,
    (uint)Marshal.SizeOf(shinfo), SHGFI_ICON | SHGFI_LARGEICON | SHGFI_SELECTICON);

  return Icon.FromHandle(shinfo.hIcon);
}

Ok, now how is this helpful? Well if you want to implement a File Explorer-ish control, then wouldn't have to include Icons and other images in your application. You can just use the icons in the system image list

Currently rated 5.0 by 3 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by christian.resma.helle on Tuesday, June 12, 2007 4:06 PM
Permalink | Comments (1) | Post RSSRSS comment feed

Integrating with TomTom Navigator

PDA's are used for pretty much everything these days. From the bunch of devices the I work (play) with, I took a great liking to devices that have built in GPS receivers. These devices are usually bundled with really cool navigation software from various vendors. Some of these navigation software have SDK's that you can buy separately. By using these SDK's, you can fully integrate navigation features to your mobile solutions.

In this article, I would like to discuss how to integrate a .NET Compact Framework application with TomTom Navigator. I will also demonstrate an example of making a generic navigator wrapper so your application is not just bound to one kind of navigation software.

Before we get started, we need to have the TomTom Navigator SDK. Unfortunately this is not free, but can be easily purchased from the TomTom Pro website.

Before we dig into more detail, let's go through our software requirements. We need the following:

    1. Visual Studio 2005
    2. Windows Mobile 5.0 SDK for Pocket PC
    3. A device running Windows Mobile 5.0 with TomTom Navigator 5 installed
    4. The TomTom Navigator SDK
    5. ActiveSync 4.2 or higher (for Vista, the Mobile Device Center)


Now, Lets get started...


Here is what we need to make:

    1. native wrapper for the TomTom SDK (native dll)
    2. generic navigator wrapper in .NET CF
    3. managed TomTom wrapper
    4. device application that will call TomTom SDK wrapper methods

Sounds pretty simple doesn't it?


1. Native Wrapper for the TomTom SDK

We will first need a little help from native code to access the TomTom SDK. We cannot access the TomTom SDK directly from .NET due to the architecture of the SDK. We have to wrap around the TomTom SDK C++ classes and methods and expose them as C type functions.

In your native wrapper, lets say we want to wrap the following TomTom SDK functions:
  - GetApplicationVersion(TError* err, TVersion* ver)
  - FlashMessage(TError* err, char* msg, int ms)
  - NavigateToAddress(TError* aError, char* aCity, char* aStreet, char* aHouseNr, char* aPostcode)


[C++ CODE]

#include "sdkconstants.h"
#include "TomTomAPI.h"
#include "TomTomGoFileLayer.h"

#define CLIENT_NAME "client"

CTomTomAPI::TError err;
int res = 0;

BOOL APIENTRY DllMain(
  HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved )
{
  switch (ul_reason_for_call)
  {
  case DLL_PROCESS_ATTACH:
  case DLL_THREAD_ATTACH:
  case DLL_THREAD_DETACH:
  case DLL_PROCESS_DETACH:
    break;
  }
  return TRUE;
}

extern "C" __declspec(dllexport) int TTN_GetApplicationVersion(
  int* iError, LPTSTR szVersion, int *iBuildNumber )
{
  MTomTomCommunicationLayerInterface *comms =
    DEFAULT_TRANSPORTATION_LAYER(CLIENT_NAME,2005,TOMTOM_TCPIP_PORT);

  CTomTomAPI api(*comms);
  CTomTomAPI::TVersion version;
  res = api.GetApplicationVersion(&err, &version);
  *iError = err.iError;

  TCHAR str[16];
  _stprintf(str, TEXT("%S"), version.iVersion);
  lstrcpy( szVersion, (LPTSTR)str );
  *iBuildNumber = version.iBuildNumber;

  delete comms;
  return res;
}

extern "C" __declspec(dllexport) int TTN_FlashMessage(
   int* iError, char* aMessage, int aMilliSeconds )
{
  char message[256];
  sprintf(message, "%S", aMessage);

  MTomTomCommunicationLayerInterface *comms =
    DEFAULT_TRANSPORTATION_LAYER(CLIENT_NAME,2005,TOMTOM_TCPIP_PORT);

  CTomTomAPI api(*comms);
  res = api.FlashMessage(&err, message, aMilliSeconds);
  *iError = err.iError;

  delete comms;
  return res;
}

extern "C" __declspec(dllexport) int TTN_NavigateToAddress(
   int* iError, char* aCity, char* aStreet, char* aHouseNr, char* aPostcode )
{
  char city[256];
  char street[256];
  char houseNr[16];
  char postcode[32];

  sprintf(city, "%S", aCity);
  sprintf(street, "%S", aStreet);
  sprintf(houseNr, "%S", aHouseNr);
  sprintf(postcode, "%S", aPostcode);

  MTomTomCommunicationLayerInterface *comms =
    DEFAULT_TRANSPORTATION_LAYER(CLIENT_NAME,2005,TOMTOM_TCPIP_PORT);

  CTomTomAPI api(*comms);
  res = api.NavigateToAddress(&err, city, street, houseNr, postcode);
  *iError = err.iError;

  delete comms;
  return res;
}

Let's set the output of the project to be called TTSDK.dll

2. Generic Navigator Wrapper in .NET CF

Once we've gotten our native wrapper up and running, we create a generic navigator wrapper. We start off by creating a smart device class library project. Once the project is created, add the following classes: INavigator.cs, Navigator.cs, and Common.cs

Lets go and define the common objects we want to use in Common.cs

[C# CODE]

public struct NVersion {
  string Version;
  int BuildNumber;
}

INavigator.cs will be an interface defining the how the wrapper will look like. Lets add methods for the 3 TomTom SDK methods we want to use.

[C# CODE]

public interface INavigator
{
  NVersion GetApplicationVersion();
  void FlashMessage(string text, int duration);
  void NavigateToAddress(string city, string street, string houseno, string zipcode);
}

Navigator.cs will be the class your application will call. This will load the managed TomTom wrapper as an instance of INavigator. Navigator itself will implement INavigator and will return calls from the TomTom wrapper.

[C# CODE]

public class Navigator : INavigator
{
  private INavigator instance;

  public Navigator(string typeName)
  {
    Type type = Type.GetType(typeName);
    if (type == null) {
      throw new TypeLoadException();
    } else {
      instance = (INavigator)Activator.CreateInstance(type);
      if (instance == null) {
        throw new TypeLoadException();
      }
    }
  }

  public TVersion GetApplicationVersion()
  {
    return instance.GetApplicationVersion();
  }

  public void FlashMessage(string text, int duration)
  {
    instance.FlashMessage(text, duration);
  }

  public void NavigateToAddress(string city, string street, string houseno, string zipcode)
  {
    instance.NavigateToAddress(city, street, houseno, zipcode);
  }
}

The default constructor for Navigator accepts a type name. The format for type name is "[Namespace].[ClassName], [AssemblyName]"

3. Managed TomTom Wrapper

Here we create a new smart device class library. Once the project is created, add a reference to the generic navigator wrapper since we will implement the INavigator interface and add a class called TomTom.cs

Lets implement TomTom.cs as INavigator

[C# CODE]

[DllImport("TTSDK.dll", EntryPoint="TTN_GetApplicationVersion")]
internal static extern int TTN_GetApplicationVersion(
  ref int iError, StringBuilder szVersion, ref int iBuildNumber);

[DllImport("TTSDK.dll", EntryPoint="TTN_FlashMessage")]
internal static extern int TTN_FlashMessage(
  ref int iError, string aMessage, int aMilliseconds);

[DllImport("TTSDK.dll", EntryPoint="TTN_NavigateToAddress")]
internal static extern int TTN_NavigateToAddress(
  ref int iError, string aCity, string aStreet, string aHouseNo, string aPostcode);

public void FlashMessage(string aMessage, int aMilliseconds)
{
  if (0 != TTN_FlashMessage(ref iError, aMessage, aMilliseconds)) {
    throw new InvalidOperationException();
  }
}

public NVersion GetApplicationVersion()
{
  NVersion version = new TVersion();
  StringBuilder szVersion = new StringBuilder();
  int iBuildNumber = 0;
  int iError = 0;

  if (0 != TTN_GetApplicationVersion(ref iError, szVersion, ref iBuildNumber)) {
    throw new InvalidOperationException();
  } else {
    version.iVersion = szVersion.ToString();
    version.iBuildNumber = iBuildNumber;
  }
}

public void NavigateToAddress(string sCity, string sStreet, string sHouseNo, string sPostcode)
{
  int iError = 0;

  if (0 != TTN_NavigateToAddress(ref iError, sCity, sStreet, sHouseNo, sPostcode)) {
    throw new InvalidOperationException();
  }
}

Now our TomTom wrapper is pretty much ready

4. Device Application

In our application, we want to integrate with TomTom Navigator for navigating to a specific address. The address can could be retrieved from a web service, or stored in your pocket outlook. For this article, we're going to retrieve address information of a customer from Pocket Outlook.

In order to do this, we will need the Windows Mobile 5.0 SDK for Pocket PC to be installed. Let's start off by creating a Windows Mobile 5.0 device application project. Once the project is created, add a reference to the Navigator wrapper and the TomTom wrapper. Next we have to build the Native wrapper project, and add the output file TTSDK.dll to our project. Set TTSDK.dll to be "Copied if Newer". To retrieve address information from contacts, we must add a reference to Microsoft.WindowsMobile.PocketOutlook.dll.

Once the references and files are in place, we can start adding some code to Form1.cs. No need to change the name of the main form since this is only a small demo. We need to have a control that can contain the contacts, lets use the ComboBox control for now. Add a ComboBox control to the form and call it cbContacts. Lets add a "Navigate to" button to the form as well and call it btnNavigate.

To retrieve a list of contacts we need to create a global instance of Microsoft.WindowsMobile.PocketOutlook.OutlookSession and Microsoft.WindowsMobile.PocketOutlook.ContactsCollection, once we instantiate our OutlookSession, we can then retrieve a list of Contacts through OutlookSession.Contacts.Items.

To communicate with TomTom, we create an instance of Navigator(). The default constructor for Navigator will need a typeName for loading the TomTom wrapper as INavigator. It would be a smart idea to store the typeName in a seperate file, text or xml would be perfect. Once again, we do this so that if we want our application to integrate with different navigation software, we don't have to re-write everything. In this demo, the typeName will just be a hard coded string constant.

[C# CODE]

private Microsoft.WindowsMobile.PocketOutlook.OutlookSession session;
private Microsoft.WindowsMobile.PocketOutlook.ContactsCollection contacts;

private const string TYPENAME="[The namespace].[The class name], [The assembly name]";
private Navigator navigator;

public Form1()
{
  InitializeComponent();

  btnNavigate.Click += new EventHandler(btnNavigate_Click);
  Closing += new CancelEventHandler(Form1_Closing);

  navigator = new Navigator(TYPENAME);

  string restriction = "[BusinessAddressStreet] <> \" \" OR [HomeAddressStreet] <> \" \"";
  session = new OutlookSession();
  contacts = session.Contacts.Items.Restrict(restriction);
  cbContacts.DataSource = contacts;
}

private void Form1_Closing(object sender, CancelEventArgs e)
{
  contacts.Dispose();
  contacts = null;

  session.Dispose();
  session = null;
}

private void btnNavigate_Click(object sender, EventArgs e)
{
  Contact contact = cbContacts.SelectedItem as Contact;
  navigator.FlashMessage("Navigating...", 1500);
  navigator.NavigateToAddress(contact.BusinessAddressCity,
    contact.BusinessAddressStreet,
    "PARSE THE HOUSE NUMBER...",
    contact.BusinessAddressPostalCode);
}

When the application launches, your pocket outlook contacts that have a valid address will be loaded into our ComboBox control. Let's select an item in the ComboBox. Once you click on the Navigate button it will launch TomTom Navigator and display the message "Navigating" for 1.5 seconds, after that it will start calculating the route from your current location to your destination (in this case, the selected contact).

That wasn't too hard was it?

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by christian.resma.helle on Thursday, June 07, 2007 9:20 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Programmatically Refreshing the Today Screen

A simple trick for forcing the today screen to re-read from the registry (or refresh) is by sending the message WM_WININICHANGE with the parameter 0x000000F2 to a window called the DesktopExplorerWindow.

Here's a small code snippet on how to accomplish this programmatically:

void RefreshTodayScreen() {
  HWND hWnd = FindWindow(_T("DesktopExplorerWindow"), _T("Desktop"));
  SendMessage(hWnd, WM_WININICHANGE, 0x000000F2, 0);
}

and in managed code...

[DllImport("coredll.dll")]
static extern IntPtr FindWindow(string class_name, string caption);

[DllImport("coredll.dll")]
static extern int SendMessage(IntPtr hWnd, uint Msg, int wParam, int lParam);

const uint WM_WININICHANGE = 0x1a;

void RefreshTodayScreen() {
  IntPtr hWnd = FindWindow("DesktopExplorerWindow", "Desktop");
  SendMessage(hWnd, WM_WININICHANGE, 0x000000F2, 0);
}

Currently rated 4.5 by 2 people

  • Currently 4.5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by christian.resma.helle on Wednesday, June 06, 2007 7:25 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Programmatically Minimize an Application in .NET CF 2.0

I once made a solution that runs on full screen. The solution was written completely in managed code (except for the CE Setup and other small stuff..). Since I took over the screen completely, I don't have access to the (X) button in the upper right corner of the screen. I wanted the application running at all times, but I also wanted the user to be able to get in and out of the application. Since the user won't be able to access the Start button, I added a "Close" button to my application. This "Close" button won't exit the application, instead it will just Minimize the application.

In the .NET Compact Framework 2.0, you can't just set the form's WindowState to WindowState.Minimized since the WindowState enum only contains Normal and Maximized. Currently, the only way you can programmatically minimize an application is by doing a P/Invoke to ShowWindow and passing SW_MINIMIZE to specify how the window will be displayed. It is also required that your Form has the Taskbar visible, this is done by setting the following properties:

  FormBorderStyle = FormBorderStyle.FixedDialog;
  WindowState = FormWindowState.Normal;
  ControlBox = true;
  MinimizeBox = true;
  MaximizeBox = true;

Here's a small code snippet of how to minimize your application

[DllImport("coredll.dll")]
static extern int ShowWindow(IntPtr hWnd, int nCmdShow);

const int SW_MINIMIZED = 6;

void Minimize() {
  // The Taskbar must be enabled to be able to do a Smart Minimize
  this.FormBorderStyle = FormBorderStyle.FixedDialog;
  this.WindowState = FormWindowState.Normal;
  this.ControlBox = true;
  this.MinimizeBox = true;
  this.MaximizeBox = true;

  // Since there is no WindowState.Minimize, we have to P/Invoke ShowWindow
  ShowWindow(this.Handle, SW_MINIMIZED);
}

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by christian.resma.helle on Wednesday, June 06, 2007 7:24 AM
Permalink | Comments (6) | Post RSSRSS comment feed