Tuesday, September 06, 2005

One of the developers has sent me an email asking on how to get the same list of programs that's shown in the "Running Program List" applet on Pocket PC's. I forwarded her to the EnumerateTopWindows function I'd implemented before. But she replied to me that this function enumerates all windows, including Desktop, SIP button etc... So I've added a few checks into this function such as calls to the IsWindow and IsWindowVisible API's. I've also put in a few helper functions:

private static bool CheckForDialogs(ArrayList windowList, string value)

{

      foreach(Window window in windowList)

      {

            if (window.Caption == value)

            {

                  return true;

            }

      }

      return false;

}

 

private static bool CheckExcludeList(string value)

{

      foreach(string name in excludeList)

      {

            if (name == value)

            {

                  return true;

            }

      }

      return false;

}

The first one checks if the window that has the same caption already exists and the second loops through the exclude list of some known system programs:

static string[]  excludeList = {"Desktop", "MS_SIPBUTTON", "Programs"};

So the new version of the EnumerateTopWindows looks like this:

public static Window[] EnumerateTopWindows()

{

      ArrayList windowList = new ArrayList();

      IntPtr hWnd = IntPtr.Zero;

      Window window = null;

      StringBuilder sb = null;

 

      // Get first window

      hWnd = GetActiveWindow();

      hWnd = GetWindow(hWnd, GW_HWNDFIRST);

   

      while(hWnd != IntPtr.Zero)

      {

            // Check if it is a windowm and it is visible

            if(IsWindow(hWnd) && IsWindowVisible(hWnd))

            {

                  IntPtr parentWin = GetParent(hWnd);

                  // Make sure that the window doesn't hav a parent

                  if ((parentWin == IntPtr.Zero))

                  {

                        int length = GetWindowTextLength(hWnd);

                        // Does it have the text caption

                        if (length > 0)

                        {

                              sb = new StringBuilder(length + 1);

                              GetWindowText(hWnd, sb, sb.Capacity);

                              string text = sb.ToString();

                              text = text.Substring(0, text.IndexOf('\0'));

                              // Exclude some known system programs

                              if(!CheckExcludeList(text) && !CheckForDialogs(windowList, text))

                              {

                                    window = new Window();

                                    window.Handle = hWnd;

                                    window.Caption = text;

                                    windowList.Add(window);

                              }

                        }

                  }

            }

            hWnd = GetWindow(hWnd, GW_HWNDNEXT);

      }

      return (Window[])windowList.ToArray(typeof(Window));

}

And the additional P/Invoke declarations:

[DllImport("coredll.dll")]

static extern bool IsWindow(IntPtr handle);

 

 

[DllImport("coredll.dll")]

static extern bool IsWindowVisible(IntPtr handle);

           

And as a bonus here's a way to close the program:

[DllImport("coredll.dll")]

private extern static int SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);

 

private static int WM_CLOSE  = 0x0010;

 

public static void CloseWindow(Window window)

{

      SendMessage(window.Handle, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);

}

Here's the updated project:

EnumerateWindowsProjNew.zip (6.51 KB)

9/6/2005 1:35:20 PM (GMT Daylight Time, UTC+01:00)  #     | 
 Monday, August 29, 2005

I've made a few updates to the code I'd posted last time mainly by moving the text wrapping functionality inside of the ToolTip control, therefore making it more self contained and useful for other scenarious. I optimized the MeasureStringExtend function to use StringBuilder and improving its speed by 50-70%. Here's the new version:

private SizeF MeasureStringExtend(Graphics g, string text, Font font, int desWidth)

{

      int sp_pos = 0;

      int line = 0;

      int nWidth = 0;

      StringBuilder sb = new StringBuilder();

 

      //get original size

      SizeF size = g.MeasureString(text, font);

 

      if (size.Width > desWidth)

      {

            string[] words = text.Split(' ');

            for (int i = 0; i < words.Length; i++)

            {

                  sb.Append(words[i] + " ");

                  nWidth = (int)g.MeasureString(sb.ToString(), font).Width;

 

                  if (nWidth > desWidth)

                  {

                        //try to find a space

                        sp_pos = sb.ToString().LastIndexOf(" ");

                        if (sp_pos > 0)

                        {

                              //cut out the wrap line we've found

                              sb.Remove(0, sb.Length);

                              sb.Append(words[i] + " ");

                              line++;

                        }

                        else //no space

                        {

                              sb.Remove(0, sb.Length);

                              line++;

                        }

                  }

            }

            // Check for the last line

            if (sb.Length > 0)

            {

                  line++;

            }

      }

      else

      {

            line++;

      }

 

      return new SizeF(desWidth, (line * size.Height));

}

 

I've also created a VS 2005 and CF v2 version of the ListViewExtender and ListViewLabel classes. Instead of ApplicationEx and IMessageFilter I utilized my NativeWindow implementation. Here's the ListViewSubclass class, the sole purpose of which to catch the WM_LBUTTONDOWN message and raise the MouseDown event:

public class ListViewSubclass : NativeWindow

{

      public event MouseEventHandler MouseDown;

 

      private const int WM_LBUTTONDOWN = 0x0201;

      private ListView listView;

 

      public ListViewSubclass(ListView listView)

      {

            this.listView = listView;

            this.AssignHandle(listView.Handle);

      }

 

      protected override void WndProc(ref Message m)

      {

            if (m.Msg == WM_LBUTTONDOWN)

            {

                  int y = HiWord((int)m.LParam);

                  int x = LoWord((int)m.LParam);

                  MouseEventArgs args = new MouseEventArgs(0, 1, x, y, 0);

                  if (MouseDown != null)

                  {

                        MouseDown(listView, args);

                  }

            }

            base.WndProc(ref m);

      }

 

      private static int HiWord(int number)

      {

            if ((number & 0x80000000) == 0x80000000)

                  return (number >> 16);

            else

                  return (number >> 16) & 0xffff;

      }

 

      private static int LoWord(int number)

      {

            return number & 0xffff;

      }

}

Download the updated VS 2003 version from here.

ListViewToolTip2003.zip (10.08 KB)

And here's the VS 2005 version for your pleasure:

ListViewToolTip2005.zip (14.25 KB)

 

 

8/29/2005 4:40:50 PM (GMT Daylight Time, UTC+01:00)  #     | 
 Friday, August 26, 2005

The ListView control in .NetCF has been one of the most useful for me and I think for many other developers. But it lacks a few important functions like the ability to show a long text in the wrapped mode or to edit items in the ListView. So I set myself on the road of trying to accomplish this and believe me, it was not a walk in the park, rather a rock climbing.
My first thought was that the easiest way to achieve the required functionality would be just to place either a Label or a TextBox on a top of the ListView. OK, so I need the location and the size of an item in the ListView. The Windows CE SDK docs state that it is possible to send the LVM_GETITEMRECT or LVM_GETSUBITEMRECT message to the ListView control and it will return the RECT structure with what we’re looking for. But these messages expect the indexes of item and subitem to be passed. Alright, then there is the LVM_SUBITEMHITTEST message that should provide us with the LVHITTESTINFO structure. This structure includes the item’s indexes as well as the POINT structure with the expected client coordinates. Now, it all comes down to getting the coordinates of the place where the ListView is being tapped. Unfortunatelly, ListView in .NetCF doesn’t implement mouse events, so we must resort to the extreme measures like subclassing the ListView control and catching mouse down message.  As you probably know, SDF provides the ApplicationEx and IMessageFilter classes and I had done my implementation of the pseudo window procedure as the WinProcFilter class long time ago. With all pieces of the puzzle in place I was ready finish the task.
First, I have created the ListViewExtender class that implements GetItemAt and GetItemRectangle methods:

public HitItem GetItemAt(int x, int y)

{

      HitItem hitItem = new HitItem();

 

      // Init LVHITTESTINFO structure

      LVHITTESTINFO hitTextInfo = new LVHITTESTINFO();

      hitTextInfo.x = x;

      hitTextInfo.y = y;

      hitTextInfo.flags = LVHT_ONITEM;

                 

      // Allocate native memory

      IntPtr pointer = AllocHGlobal(Marshal.SizeOf(hitTextInfo));

      // Copy LVHITTESTINFO structure to the memory pointer

      Marshal.StructureToPtr(hitTextInfo, pointer, false);

                 

      // Retreive item/subitem indexes

      SendMessage(handle, LVM_SUBITEMHITTEST, 0, pointer);

 

      hitTextInfo = (LVHITTESTINFO)Marshal.PtrToStructure(pointer, typeof(LVHITTESTINFO));

      FreeHGlobal(pointer);

 

      hitItem.ItemIndex = hitTextInfo.iItem;

      hitItem.SubItemIndex = hitTextInfo.iSubItem;

                 

      return hitItem;

 

}

 

public Rectangle GetItemRectangle(int itemIndex, int subItemIndex)

{

      RECT rect = new RECT();

      rect.left = 2;

      rect.top = subItemIndex;

      // Retreive item's rectangle

      SendMessage(handle, LVM_GETSUBITEMRECT, itemIndex, ref rect);

      // Convert to the screen coordinates

      Rectangle rc = listView.RectangleToScreen(new Rectangle(rect.left, rect.top - 26, rect.right - rect.left, rect.bottom - rect.top));

      return rc;

}

Next, I wanted to have a label control that could draw borders, so whipped up a custom control ToolTip that inherits from the Control and overrides its OnPaint event:

protected override void OnPaint(PaintEventArgs e)

{

      base.OnPaint (e);

      e.Graphics.Clear(this.BackColor);

      Rectangle textRect = this.ClientRectangle;

 

      textRect.X = 3;

      //textRect.Y = 2;

      textRect.Width += 2;

      textRect.Height += 2;

 

      e.Graphics.DrawString(this.Text, this.Font, textBrush, textRect);

 

      this.DrawBorder(e.Graphics);

}

After that, I’ve create the ListViewLabel class that is derived from ListViewExtender:

public class ListViewLabel : ListViewExtender, IDisposable

{

private ToolTip toolTip;

      private WinProcFilter winProcFilter;

      private const int WM_LBUTTONDOWN = 0x0201;

      private HitItem hitItem;

      private TextBox editText;

      private bool editable;

 

      public ListViewLabel(ListView listView, bool editable) : base(listView)

      {

            this.editable = editable;

            if (!editable)

            {

                  // Create instance of the Tooltip control

                  toolTip = new ToolTip();

                  toolTip.Visible = false;

                  // Add to the Form

                  listView.Parent.Controls.Add(toolTip);

            }

            else

            {

                  editText = new TextBox();

                  editText.Visible = false;

                  editText.LostFocus+=new EventHandler(editText_LostFocus);

                  // Add to the Form

                  listView.Parent.Controls.Add(editText);

            }

 

            // Init WinProcFiter

            winProcFilter = new WinProcFilter(this.handle);

            winProcFilter.WndProc+=new WinProc(winProcFilter_WndProc);

                 

      }

}

The constructor’s parameters are a ListView control and a Boolean value that will tell the control whether you want to edit the ListView item or just show the long content. We also create instance of the WinProcFilter class and hook up into its WndProc event:

private void winProcFilter_WndProc(ref Microsoft.WindowsCE.Forms.Message m)

{    

      if (m.Msg == WM_LBUTTONDOWN)

      {

            if (toolTip != null)

            {

                  toolTip.Hide();

            }

 

            if (editText != null)

            {

                  if (editText.Visible) // Update the item's text back

                  {

                        listView.Items[hitItem.ItemIndex].SubItems[hitItem.SubItemIndex].Text = editText.Text;

                        editText.Visible = false;

                  }

            }

 

            byte[] pos = BitConverter.GetBytes(m.LParam.ToInt32());

            int x = BitConverter.ToInt16(pos, 0); // LOWORD

            int y = BitConverter.ToInt16(pos, 2); // HIWORD

 

            Point clientPoint = listView.PointToClient(new Point(x, y));

 

            hitItem = this.GetItemAt(x, y);

            if (hitItem.ItemIndex > -1)

            {

                  // Get item's rectangle

                  Rectangle rc = GetItemRectangle(hitItem.ItemIndex, hitItem.SubItemIndex)