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);
// Get item's text, location and width
string itemText = listView.Items[hitItem.ItemIndex].SubItems[hitItem.SubItemIndex].Text;
SizeF size;
if (!editable) // display label
{
toolTip.Text = itemText;
toolTip.Location = new Point(rc.Left, rc.Top);
toolTip.Width = rc.Width;
// Get the height of the wrapped text
using(Graphics gx = listView.Parent.CreateGraphics())
{
size = this.MeasureStringExtend(gx, toolTip.Text, toolTip.Font, toolTip.Width);
}
if (size.Height > rc.Height) // Display a Tooltip only on multilined items
{
toolTip.Height = (int)size.Height;
toolTip.BringToFront();
toolTip.Show();
}
}
else // display TextBox
{
editText.Text = itemText;
editText.Width = rc.Width;
editText.Location = new Point(rc.Left, rc.Top);
editText.BringToFront();
editText.Visible = true;
}
}
}
}
In the code above we check if we received the WM_LBUTTONDOWN message. If we did, we retrieve mouse coordinates from the LParam and get the HitItem by calling created earlier GetItemAt method. After that we can fetch the item’s rectangle which is used to place the ToolTip or TextBox controls on the top of the ListView. Here are the screenshots of the test client that’s using the ListViewLabel class:


As you can see, it was not an easy road indeed.
You can download the all of the code from here:
ListViewToolTip.zip (10.06 KB)