Recently, I stumbled across several requests in various news groups on how to embed controls within a
There are several owner-drawn ListView controls here on CP, but I wanted to try and bend the standard
ListView to my will... ;)
When you're intending to embed a control in a
, you'll have to make sure it's positioned correctly all the time. This can become difficult in several ways:
- The position and size of a
ListViewSubItem can be modified in various ways (for example, resizing the
ListView, scrolling, resizing
ColumnHeaders, and so on).
- The default implementation of
ListView doesn't have any way to tell you the size and location of
- Columns can be reordered.
ListViewItems can be sorted.
The easiest way to ensure the correct position would be right where the painting occurs, so I decided to override the
WndProc and listen for
WM_PAINT as a trigger to calculate the controls' positions.
There may be other, more efficient ways, but then it's really hard to get all the cases in which a control has to be re-positioned. Besides, I didn't find performance problems with a reasonable number of embedded controls.
Obtaining a cell's position and size
That's a little tricky, as the standard
ListView won't help you here. It does have a
GetItemRect() method, but it only gives you information on the entire
ListViewItem. No way to retrieve the bounds of a certain
Luckily, I've been confronted with the same problem in a previous article of mine (In-place editing of ListView subitems), so the necessary functions were available already.
Basically, I get the height and vertical position of the cell from
GetItemRect() and calculate its horizontal position and width from the current
To calculate the left margin of a cell, you just have to sum up the widths of all
ColumnHeaders left of your cell, i.e., with indices smaller than your
ListViewSubItem's index, right? Unfortunately, not. Columns can be reordered by the users and the
Columns collection doesn't reflect these changes :(
So, I had to resort to interop to get the current display order for the columns. There's a message
ListView understands to give you the current column order in the form of an
protected int GetColumnOrder()
IntPtr lPar = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(int)) * Columns.Count);
IntPtr res = SendMessage(Handle, LVM_GETCOLUMNORDERARRAY,
new IntPtr(Columns.Count), lPar);
if (res.ToInt32() == 0)
int  order = new int[Columns.Count];
Marshal.Copy(lPar, order, 0, Columns.Count);
Once I had this array, I could simply sum up the widths of the columns displayed left of the cell in question.
Positioning the embedded control
That's the easiest part. Once I had the correct position and size of a
ListViewSubItem, I only had to assign this information to the embedded control's
Bounds property in the
What about sorting?
My first tests didn't include sorting the
ListView. My first tests also just held the row and column number of the embedded control as a reference to where to put the control.
The problem arose when I allowed the user to sort the
ListViewItems changed their position but none of the embedded controls did. What had happened?
ListView is sorted, the
ListViewItems change their position in the
Items collection. That's OK, but after sorting, they also have their
Index property changed to reflect the current position in the collection and not the position at which they were added originally.
Luckily, this behavior could be fixed easily by adding a reference to the
ListViewItem in question to my management structure. Now, I could retrieve the right display position of the
ListViewItem as well.
Using the new ListView
To embed a given control in the new, extended
ListView, you have two new methods:
public void AddEmbeddedControl(Control c, int col, int row);
public void AddEmbeddedControl(Control c, int col, int row, DockStyle dock);
The second function allows you to specify how the control is positioned and sized in its target cell. Usually, you'd use
DockStyle.Fill to let the control use the entire SubItem rectangle (default value if you don't give the dock parameter). If you don't want your control to be resized in both directions, you can specify one of the other
DockStyles. If you specify
DockStyle.None, your control will not be resized at all and thus might overlap other parts of the
There are also methods to remove a given control or query the
ListView for the control embedded at a certain position.
About the demo
I've added a little demo project so that you can try out the new
ListView and its features.
While building the demo, I tried embedding a
RichTextBox in the
ListView and it worked quite nicely, but I was annoyed to find out that I couldn't keep the
RichTextBox from being selected, so I've also included a
ReadOnlyRichTextBox class to the demo.
I don't think this qualifies for a separate article, but you can use this
ReadOnlyRichTextBox as well with your projects, when you want a label with pretty formatting.
Please feel free to comment on the article and don't forget to vote!
- 31.12.2004 V1.0