Within my Updating component, Each Element is given a little Progress Bar right within the ListView. It’s drawn using a Gradient background. I’ve given passing thought to the idea of figuring out how to draw the standard Themed Progress bar within the ListView. Today I decided to delve into the seedy underbelly that is the Theme API and sort out how to do exactly that.
The Theme API
The Theme API resides in uxtheme.dll. Using a ‘theme’ component involves three steps:
- using the OpenThemeData() function to get a handle to the Theme.
- using the DrawThemeBackground() and DrawThemeText() functions to draw applicable parts of that theme element.
- closing the theme
At it’s core, Themes are really just groups of images; OpenThemeData() grabs a block of images, and you use the parameters of DrawThemeBackground() and DrawThemeText() to select which portion of the image to use. The Theme API refers to these as “parts” and “States”. The first step to using the Theme API is, of course, to declare the Functions you will be using. Unfortunately while the Functions themselves are well-documented, what is less available is the actual Constants for using the functions; so while I was able to grab some useful declarations from PInvoke.NET I had to use the Windows SDK to recreate the enumerations. Since I am only interested in the Progress Bar portions at this time, I only recreated the appropriate enumerations for them. Here is the class I came up with.
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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Text; namespace ListViewProgressBars { public class ThemePaint { [StructLayout(LayoutKind.Sequential)] private 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(System.Drawing.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 System.Drawing.Point Location { get { return new System.Drawing.Point(Left, Top); } set { X = value.X; Y = value.Y; } } public System.Drawing.Size Size { get { return new System.Drawing.Size(Width, Height); } set { Width = value.Width; Height = value.Height; } } public static implicit operator System.Drawing.Rectangle(RECT r) { return new System.Drawing.Rectangle(r.Left, r.Top, r.Width, r.Height); } public static implicit operator RECT(System.Drawing.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); else if (obj is System.Drawing.Rectangle) return Equals(new RECT((System.Drawing.Rectangle)obj)); return false; } public override int GetHashCode() { return ((System.Drawing.Rectangle)this).GetHashCode(); } public override string ToString() { return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom); } } [DllImport("uxtheme.dll", ExactSpelling = true, CharSet = CharSet.Unicode)] private static extern IntPtr OpenThemeData(IntPtr hWnd, String classList); [DllImport("uxtheme", ExactSpelling = true)] private extern static Int32 DrawThemeBackground(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, ref RECT pRect, IntPtr pClipRect); [DllImport("uxtheme.dll", ExactSpelling = true)] public extern static Int32 CloseThemeData(IntPtr hTheme); public static Brush ProgressBarEmptyBackground = new SolidBrush(SystemColors.Window); public static Brush ProgressBarFilledBackground = new SolidBrush(Color.Teal); public enum PROGRESSPARTS { PP_BAR = 1, PP_BARVERT = 2, PP_CHUNK = 3, PP_CHUNKVERT = 4, PP_FILL = 5, PP_FILLVERT = 6, PP_PULSEOVERLAY = 7, PP_MOVEOVERLAY = 8, PP_PULSEOVERLAYVERT = 9, PP_MOVEOVERLAYVERT = 10, PP_TRANSPARENTBAR = 11, PP_TRANSPARENTBARVERT = 12, }; public enum TRANSPARENTBARSTATES { PBBS_NORMAL = 1, PBBS_PARTIAL = 2, }; public enum FILLSTATES { PBFS_NORMAL = 1, PBFS_ERROR = 2, PBFS_PAUSED = 3, PBFS_PARTIAL = 4, }; public static void DrawProgress(IntPtr hWnd, Graphics Target, Rectangle Size, float Percentage, FILLSTATES FillState = FILLSTATES.PBFS_NORMAL) { if (Percentage > 1) Percentage = 1; if (Percentage < 0) Percentage = 0; Rectangle ValueRect = new Rectangle(Size.Location, new Size((int)((float)Size.Width * Percentage), Size.Height)); IntPtr Themehandle = OpenThemeData(hWnd, "PROGRESS"); if (Themehandle != IntPtr.Zero) { IntPtr hdc = Target.GetHdc(); //draw the full size. RECT FullSize = Size; RECT ValueSize = ValueRect; //draw the progressbar background... DrawThemeBackground(Themehandle, hdc, (int)(PROGRESSPARTS.PP_TRANSPARENTBAR), (int)TRANSPARENTBARSTATES.PBBS_PARTIAL, ref FullSize, IntPtr.Zero); //now draw the foreground... DrawThemeBackground(Themehandle, hdc, (int)(PROGRESSPARTS.PP_FILL), (int)(FILLSTATES.PBFS_NORMAL), ref ValueSize, IntPtr.Zero); DrawThemeBackground(Themehandle, hdc, (int)(PROGRESSPARTS.PP_PULSEOVERLAY), 0, ref FullSize, IntPtr.Zero); //DrawThemeBackground(Themehandle, hdc, 8, 6, ref FullSize, IntPtr.Zero); CloseThemeData(Themehandle); Target.ReleaseHdc(hdc); } else { Target.FillRectangle(ProgressBarEmptyBackground, Size); Target.FillRectangle(ProgressBarFilledBackground, ValueRect); } } /* * * PAINTSTRUCT ps; HDC hDC = BeginPaint(hwnd,&ps); RECT r; HTHEME theme = OpenThemeData(hwnd,L"PROGRESS"); SetRect(&r,10,10,100,25); DrawThemeBackground(theme,hDC,11, 2 ,&r,NULL); SetRect(&r,10,10,50,25); DrawThemeBackground(theme,hDC,5, 4 ,&r,NULL); CloseThemeData(theme); EndPaint(hwnd,&ps);*/ } } |
It’s worth noting there is actually a lot in common between some of the Theme Elements, so it might actually make sense for a “full API” sort of implementation that handles all the different cases in a early-bound fashion that let’s you choose the appropriate component. They all use the same functions so it would be a matter of separating each specific type into a different implementation of an abstract class of some sort. But that is for another post for sure. Here we are focused on the progressbar. This code draws the ProgressBar on the given Graphics Object by grabbing it’s DC and using the Theme API functions. Also note the “Default” action which attempts to draw a progressbar
So now the question is how do we utilize this for something like a ListView Subitem? The answer is surprisingly… (or perhaps relatively) easily- we simply set the ownerDraw property and override the appropriate functions. I created this sample project which simply advances and shrinks the progressbar values over time by different degrees in a number of List Items. To do this I created a relatively simple Wrapper class around some simple data. This is my preferred pattern when working with the ListView- I typically attach a Data Class to each ListItem through the Tag Property, which allows me to add all sorts of useful data- this can be particularly useful in cases where Delegates or actions are passed ListViewItem’s.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class ProgressItem { private float _Increment = 0.0025f; public float Increment { get { return _Increment; } set { _Increment = value; } } private int Direction = 1; private float _Amount = 0; public float Amount { get { return _Amount; } } public ListViewItem Item { get; set; } public void Act() { if (_Amount > 1 && Direction == 1) { _Amount = 1; Direction = -Direction; } if (_Amount < 0 && Direction == -1) { _Amount = 0; Direction = -Direction; } _Amount += (Increment*Direction); } public ProgressItem(float pIncrement, ListViewItem prelevantItem) { Increment = pIncrement; Item = prelevantItem; } } |
With that out of the way, I could work on the bulk of things. The Sample Program is to eliminate the surrounding faff if I was to simply release the Updater as is, which doesn’t really work well as a simple demonstration of the ProgressBar functionality. Here is the Code behind for the Form itself:
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 |
public partial class frmLvwProgress : Form { public frmLvwProgress() { InitializeComponent(); } Thread ProgressThread = null; private static Random rgen = new Random(); String[] PossibleNames = new[] { "Billy", "Thomas", "Frederick", "Sally", "Tom", "Harry", "Bill", "Bob", "Jessica", "Jennifer", "Samantha", "Crystal" }; private String SelectName() { return PossibleNames[rgen.Next(PossibleNames.Length)]; } private void frmLvwProgress_Load(object sender, EventArgs e) { lvwDisplay.Columns.Add("NAME", "Name"); lvwDisplay.Columns.Add("PROGRESS", "Progress"); for (int i = 0; i < 50; i++) { String chosenname = SelectName(); ListViewItem CreateItem = new ListViewItem(new string[] { chosenname ,"0"} ); ProgressItem p = new ProgressItem((float)(rgen.NextDouble()) * 0.1f + 0.02f, CreateItem); CreateItem.Tag = p; lvwDisplay.Items.Add(CreateItem); } ProgressThread = new Thread(ProgressRunner); ProgressThread.Start(); } private void ProgressRunner(Object state) { try { while (true) { Thread.Sleep(100); Invoke((MethodInvoker)(() => { foreach (ListViewItem iterate in lvwDisplay.Items) { ProgressItem Tagged = iterate.Tag as ProgressItem; Tagged.Act(); } lvwDisplay.Invalidate(); lvwDisplay.Refresh(); } )); } } catch (Exception exx) { return; } } private void lvwDisplay_DrawSubItem(object sender, DrawListViewSubItemEventArgs e) { if (e.ColumnIndex==1) { ListViewItem grabitem = e.Item; ProgressItem p = (ProgressItem)grabitem.Tag; float percentage = p.Amount; ThemePaint.DrawProgress(lvwDisplay.Handle, e.Graphics, e.Bounds, percentage, ThemePaint.FILLSTATES.PBFS_NORMAL); } else { e.DrawDefault = true; } } private void lvwDisplay_DrawColumnHeader(object sender, DrawListViewColumnHeaderEventArgs e) { e.DrawDefault = true; } } |
It is reasonably short. In short the Form Creates a thread that iterates over all items and advances their progress (or decrements it) each time, and then forces it to refresh. The Actual drawing logic is in lvwDisplay_DrawSubItem, which basically just draws the item with the given progress within the set bounds for Index 1.
Behold! The result! Beautiful, really. The ThemePaint Class can also paint the Error and Paused Progressbars. One thing I’ve tried was getting the “marquee” effect properly- there appears to be a way to do so but I’ve yet to work out the best way to get the appropriate effect. Perhaps in a future post I will generic-ize the ThemePaint class to a set of classes for the various Them-able things that can be drawn, though I think such a class may have reasonably dubious value, it might be a good exercise.
Have something to say about this post? Comment!