A Demo Project covering the content of this post can be downloaded Here.
Sometimes, a standard Windows.Forms.ListView just doesn’t cut it. Sometimes you just want to present more information, or customize the way the information is presented. With Windows Presentation Foundation, you typically accomplish this with Data Templates. Unlike Windows Forms adding a Progress bar to a ListView, for example, is fairly straightforward, since WPF effectively allows elements to be used as content anywhere.
One feature that sometimes finds itself to be useful is the addition of a Standard button within a ListView subitem. Generally speaking, the preferred approach is to use a right-click menu, but depending on the context of the application and the intended userbase, there could be a different set of expectations. Furthermore, a button in a subitem is much more clear with regards to the possible functionality then hiding a capability behind a otherwise invisible right-click context menu.
In the particular case that I found purpose for this ability, was for a invoice/finance related task. In particular, when voiding/nullifying an invoice, users had to provide a reason for the invoice to be voided. In the application that lists invoices, it made sense to make this reason available, but the maximum length of such reasons made it untenable to simply add a new column, and as I mentioned, placing it in a right-click menu was rejected because users simply wouldn’t know to try that to find new features. As a result, the consensus was to try to add a button within the subitem to allow viewing the Reason, and only make that button visible where a reason was present.
For obvious reasons I cannot share the actual code involved, but since it was specifically tailored to that application, it wouldn’t be particularly useful anyway. It makes more sense when sharing code such as this to provide a more generic solution, so I set about creating a more generic implementation.
The Windows Forms ListView
As I mentioned in the introductory paragraph, the Windows Forms ListView is not as flexible as the Windows Presentation Foundation ListView. The reason for this is largely a result of Windows Forms being a Framework built in .NET that abstracts the standard Windows API. There are some advantages to this approach, in particular Windows will be more easily inspectable by accessibility programs. (without additional work). One of the disadvantages is that Windows Forms inherits the limitations of those original controls. One of these limitations is that, by default, the ListView itself really only knows about drawing small images and text for various subitems. If you want to do something fancier, then you have to do it yourself.
Owner-draw
One common extensibility approach that is taken throughout the Win32 API is the concept of “owner-draw”. This is a capability whereby the control effectively defers to the Owner (eg. your code) to perform the task of drawing. This provides a rather copious amount of flexibility in terms of the presentation, without making the control logic itself too complicated, so from a design perspective it made sense.
Taking the above into account, we can see a clearer picture of how we can have a button in a ListView’s subitem. Clearly, the solution is that we take over the drawing of that subitem, and we draw a button. But now we have a new problem- How do we draw the button?
We could mimic a button control by drawing a square with some text in the middle, but that seems like a cop-out- If we draw a button, we want it to look like all the other buttons in the User Interface. But we also aren’t about to commit to a study of the particulars of the buttons on various Windows Platforms and their appearance. Thankfully, the Win32 API does in fact provide us a solution- well, two solutions, really. We can use functions in uxtheme to draw themed components, and- if theming is off, for some reason- we can fallback to using DrawEdge.
The Native Methods
As per standard .NET Convention, we’ll slap out Native methods into an appropriate “NativeMethods” class. I’ve opted to have the class as a private class within the larger class (ListViewButton) such that a single source file can be included in projects to use the capability. But of course different classes can easily be separated into their own files. Here is the NativeMethods class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
private static class NativeMethods { [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; public RECT(int left, int top, int right, int bottom) { Left = left; Top = top; Right = right; Bottom = bottom; } public RECT(Rectangle r) : this(r.Left, r.Top, r.Right, r.Bottom) { } public int X { get { return Left; } set { Right -= (Left - value); Left = value; } } public int Y { get { return Top; } set { Bottom -= (Top - value); Top = value; } } public int Height { get { return Bottom - Top; } set { Bottom = value + Top; } } public int Width { get { return Right - Left; } set { Right = value + Left; } } public Point Location { get { return new Point(Left, Top); } set { X = value.X; Y = value.Y; } } public Size Size { get { return new Size(Width, Height); } set { Width = value.Width; Height = value.Height; } } public static implicit operator Rectangle(RECT r) { return new Rectangle(r.Left, r.Top, r.Width, r.Height); } public static implicit operator RECT(Rectangle r) { return new RECT(r); } public static bool operator ==(RECT r1, RECT r2) { return r1.Equals(r2); } public static bool operator !=(RECT r1, RECT r2) { return !r1.Equals(r2); } public bool Equals(RECT r) { return r.Left == Left && r.Top == Top && r.Right == Right && r.Bottom == Bottom; } public override bool Equals(object obj) { if (obj is RECT) return Equals((RECT)obj); if (obj is Rectangle) return Equals(new RECT((Rectangle)obj)); return false; } public override int GetHashCode() { return ((Rectangle)this).GetHashCode(); } public override string ToString() { return string.Format(CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom); } } //UxTheme Natives [DllImport("uxtheme.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] public static extern IntPtr OpenThemeData(IntPtr hWnd, String classList); [DllImport("uxtheme.dll", ExactSpelling = true)] public static extern Int32 CloseThemeData(IntPtr hTheme); [DllImport("uxtheme.dll", ExactSpelling = true)] public static extern Int32 DrawThemeBackground(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, ref RECT pRect, IntPtr pClipRect); [DllImport("uxtheme", ExactSpelling = true, CharSet = CharSet.Unicode)] public extern static Int32 DrawThemeText(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, String text, int textLength, UInt32 textFlags, UInt32 textFlags2, ref RECT pRect); [DllImport("user32.dll")] public static extern int DrawEdge(IntPtr hdc, ref RECT qrc, int edge, int grfFlags); [DllImport("gdi32.dll")] public static extern IntPtr SelectObject(IntPtr hdc, IntPtr hObject); [DllImport("gdi32.dll")] public static extern int DeleteObject(IntPtr hObject); } |
Additionally, we will want to define some enumerations and constants:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
public enum PUSHBUTTONSTATES { PBS_NORMAL = 1, PBS_HOT = 2, PBS_PRESSED = 3, PBS_DISABLED = 4, PBS_DEFAULTED = 5 } private const int BDR_SUNKENOUTER = 0x2; private const int BDR_SUNKENINNER = 0x8; private const int BDR_SUNKEN = 0xA; private const int BDR_RAISEDOUTER = 0x1; private const int BDR_RAISEDINNER = 0x4; private const int BDR_RAISED = 0x5; private const int BDR_OUTER = 0x3; private const int BDR_INNER = 0xC; private const int EDGE_BUMP = (BDR_RAISEDOUTER | BDR_SUNKENINNER); private const int EDGE_ETCHED = (BDR_SUNKENOUTER | BDR_RAISEDINNER); private const int EDGE_RAISED = (BDR_RAISEDOUTER | BDR_RAISEDINNER); private const int EDGE_SUNKEN = (BDR_SUNKENOUTER | BDR_SUNKENINNER); private const int BF_TOP = 0x2; private const int BF_RIGHT = 0x4; private const int BF_BOTTOM = 0x8; private const int BF_LEFT = 0x1; private const int DT_CENTER = 0x1; private const int DT_VCENTER = 0x4; private const int DT_SINGLELINE = 0x20; |
And now, we can write the code to Draw the button. Naturally, we make it a separate routine, rather then embedding it into, say, the ListView Ownerdraw method like some sort of uncultured savage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
public static void DrawButton(IntPtr hWnd, Graphics Target, Rectangle Size, String pText, Font DesiredFont, PUSHBUTTONSTATES buttonstate) { if (DesiredFont == null && !String.IsNullOrEmpty(pText)) { throw new ArgumentException("DesiredFont must be provided if a Text String is to be drawn."); } bool Depressed = buttonstate == PUSHBUTTONSTATES.PBS_PRESSED; //used for DrawEdge fallback. //crack open the uxtheme Data. IntPtr ThemeHandle = NativeMethods.OpenThemeData(hWnd, "BUTTON"); try { //draw the button itself, first. if (ThemeHandle != IntPtr.Zero) { //good, uxtheme is active. That's something. IntPtr hdc = Target.GetHdc(); try { NativeMethods.RECT FullSize = Size; //Draw the button itself. NativeMethods.DrawThemeBackground(ThemeHandle, hdc, BP_PUSHBUTTON, (int)buttonstate, ref FullSize, IntPtr.Zero); if (!String.IsNullOrEmpty(pText)) { //if text was provided- let's draw it. For consistency we'll use DrawThemeText- may as well go full hog, right? //convert Font into HFont, so we can select it into the DC... IntPtr useFont = DesiredFont.ToHfont(); try { //Select into DC, storing the currently selected HFont. IntPtr Previous = NativeMethods.SelectObject(hdc, useFont); //now, draw the theme text with that Font selected. NativeMethods.DrawThemeText(ThemeHandle, hdc, BP_PUSHBUTTON, (int)buttonstate, pText, pText.Length, DT_CENTER | DT_VCENTER | DT_SINGLELINE, 0, ref FullSize); //finally, select the previous object back... NativeMethods.SelectObject(hdc, Previous); //and delete the GDI object we created with ToHfont. } finally { NativeMethods.DeleteObject(useFont); } } } finally { Target.ReleaseHdc(); } } else { //no Theme API. I've actually not tested this part at all... oh well. The idea is to draw the button using DrawEdge. //if support is truly needed it would make more sense to do something for states other then pressed, Disabled, and normal. IntPtr hdc = Target.GetHdc(); NativeMethods.RECT FullSize = Size; int useEdge = (Depressed ? EDGE_SUNKEN : EDGE_RAISED); NativeMethods.DrawEdge(hdc, ref FullSize, useEdge, BF_TOP | BF_BOTTOM | BF_LEFT | BF_RIGHT); Target.ReleaseHdc(); //Calculate position of the text. if (!String.IsNullOrEmpty(pText)) { var measured = Target.MeasureString(pText, DesiredFont); PointF StringPos = new PointF((FullSize.Left + ((FullSize.Width / 2) - measured.Width / 2)), (FullSize.Top + ((FullSize.Height / 2) - measured.Height / 2))); if (Depressed) StringPos = new PointF(StringPos.X + 2, StringPos.Y + 2); var chosenbrush = buttonstate == PUSHBUTTONSTATES.PBS_DISABLED ? SystemBrushes.GrayText : SystemBrushes.ControlText; Target.DrawString(pText, DesiredFont, chosenbrush, StringPos); } } } finally { if(ThemeHandle!=IntPtr.Zero) { NativeMethods.CloseThemeData(ThemeHandle); } } } |
Alright, so now we have some logic to draw the button. For convenience, we’ll plop the functionality into a class, that can be used at run-time to basically say “hey, Class instance, draw a button for this column alright? In the code this is accomplished by Storing state information in a Dictionary which indexes Column information by column index, and the column information class has it’s own item information which maps a ListView Item to the appropriate button state information for the button, which includes information such as whether to draw the button at all. We can map an X-coordinate in the ListView’s client area to an appropriate column by taking the columns in display order and finding the column where it “crosses over” width wise. This can be done by ordering the columns by their display index. I took a rather straightforward/basic approach and just go through them and add them to an int=>int Dictionary, then return the sorted values:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
private int[] GetDisplayColumnOrder(ListView lvw) { //the key is the Display index. value is the actual index. Dictionary<int ,int> useColumns = new Dictionary</int><int ,int>(); for(int i=0;i<lvw .Columns.Count;i++) { ColumnHeader ch = lvw.Columns[i]; useColumns.Add(ch.DisplayIndex,ch.Index); } return (from u in useColumns orderby u.Key select u.Value).ToArray(); } <p>The reason we need ot deal with the DisplayIndex is because as a helper class we can't be sure that the client code's ListView isn't going to allow column reordering; if it does, we don't want to start acting weird and interpreting mouse actions in the wrong location. Speaking of- that is the point of this, unlike my previous post about adding a Progress Bar, we want to react to mouse actions and associate it with the appropriate "button". With the display column order function, we can now create a function that is able to accept an X coordinate and return the column information:</lvw></int> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public ButtonStateColumnInformation GetColumnState(int X) { //step: //retrieve set of columns ordered by Display Index. int[] ColumnOrder = GetDisplayColumnOrder(_HandleList); int WidthSum = 0; int OverColumn = 0; //add the width of each one until we exceed the X position. for (int i = 0; i < ColumnOrder.Length; i++) { WidthSum += _HandleList.Columns[ColumnOrder[i]].Width; if (WidthSum > X) { OverColumn = ColumnOrder[i]; break; } } return GetColumnStateByIndex(OverColumn); } |
The column information has it’s own dictionary associating ListView Items, which is used to retrieve the appropriate state information. We can determine the “Row” position fairly easily by using the ListView’s HitTest and then using that ListItem (if any) as the indexer into this dictionary paired with the determined column. This allows us to take a mouse position and determine what “button” it is over and react accordingly.
For mousing over a button, we want to have it show the “Hot” button image. Furthermore, we of course also want to have any other “Hot” button stop being hot. Since a Windows system can only have one mouse, we can presume there can only ever be one hot button and one pressed button, so we’ll store the button information for the pressed and hot buttons into an instance variable. On Mouse Over, if we detect the mouse is not over the hot button, we’ll change the current hot button back to normal; if it is over a button, we then change that to the hot button. For added effect, we also unpress the pressed button while the mouse is not over the button, to emulate a typical Windows Button control. Mouse Up will only activate the event for the button- fired by the helper class, and passing along information about the listview item and the column index clicked – if the mouseup event occurs over the pressed button. Lastly, there is some helper logic where the MouseLeave event is hooked to make sure there are no buttons pressed or hot when the mouse leaves the listview.
One interesting caveat I found is that if FullRowSelect and MultiSelect are off, the ListView appears to fire a MouseUp event immediately after the MouseDown event; It’s unclear if this can be fixed through code, but I wasn’t able to do so; it’s something to keep in mind. Of course, with Multiselect on, the selection rectangle looks a tad wonky with the buttons there, too.
Have something to say about this post? Comment!