![progresscursor/cursor.png](/KB/progress/progresscursor/cursor.png)
Introduction
This article explains how we can customize the cursor to display a circular progress bar.
Because I often get questions about extending functionality of this utility, it has now entered the world of OSS at github. You can fork the repo here.
Class diagram
![progresscursor/classdiagram.png](data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==)
Using the code
Using the code is pretty simple, as you can see in 1-1.
var progressCursor = Van.Parys.Windows.Forms.CursorHelper.StartProgressCursor(100);
for (int i = 0; i < 100; i++)
{
progressCursor.IncrementTo(i);
}
progressCursor.End();
1-1 Basic usage of ProgressCursor
The library also has some points of extensibility, by handling the 'EventHandler<CursorPaintEventArgs> CustomDrawCursor
' event.
By handling this event, the developer can choose to extend the default behaviour by running the DrawDefault
method on the CursorPaintEventArgs
instance (1-2).
...
progressCursor.CustomDrawCursor += progressCursor_CustomDrawCursor;
...
void progressCursor_CustomDrawCursor(object sender,
ProgressCursor.CursorPaintEventArgs e)
{
e.DrawDefault();
e.Graphics.DrawString("Test",
SystemFonts.DefaultFont, Brushes.Black, 0,0);
e.Handled = true;
}
1-2 ProgressCursor extension using events
IProgressCursor
also implements IDisposable
, which makes the 'using
' statement valid on this interface. The advantage is that no custom
exception handling has to be done to ensure the End()
method is called on the ProgressCursor
. An example of the usage is found in 1-3.
using (var progressCursor = CursorHelper.StartProgressCursor(100))
{
for (int i = 0; i < 100; i++)
{
progressCursor.IncrementTo(i);
}
}
1-3 ProgressCursor implements IDisposable
Why implement IDisposable
A classic usage of the default cursor classes would be like this:
private void DoStuff()
{
Cursor.Current = Cursors.WaitCursor;
try
{
}
finally
{
Cursor.Current = Cursors.Default;
}
}
If one wouldn't implement the cursor change like this, the cursor could 'hang' and stay 'WaitCursor'. To avoid this Try Finally coding style,
I implemented IDisposable
on the IProgressCursor
like this (2-2):
public ProgressCursor(Cursor originalCursor)
{
OriginalCursor = originalCursor;
}
~ProgressCursor()
{
Dispose();
}
public void Dispose()
{
End();
}
public void End()
{
Cursor.Current = OriginalCursor;
}
2-2 Classic sample of Cursor usage
How it works
Creating a custom cursor
Basically, all the 'heavy lifting' is done by two imported user32.dll methods (1-3). These can be found in the class UnManagedMethodWrapper
(what would
be the right name for this class?).
public sealed class UnManagedMethodWrapper
{
[DllImport("user32.dll")]
public static extern IntPtr CreateIconIndirect(ref IconInfo iconInfo);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetIconInfo(IntPtr iconHandle, ref IconInfo iconInfo);
}
These methods are called in CreateCursor
(1-4):
private Cursor CreateCursor(Bitmap bmp, Point hotSpot)
{
IntPtr iconHandle = bmp.GetHicon();
IconInfo iconInfo = new IconInfo();
UnManagedMethodWrapper.GetIconInfo(iconHandle, ref iconInfo);
iconInfo.xHotspot = hotSpot.X;
iconInfo.yHotspot = hotSpot.Y;
iconInfo.fIcon = false;
iconHandle =
UnManagedMethodWrapper.CreateIconIndirect(ref iconInfo);
return new Cursor(iconHandle);
}
MSDN documentation:
Circular progress cursor drawing
int fontEmSize = 7;
var totalWidth = (int) Graphics.VisibleClipBounds.Width;
var totalHeight = (int) Graphics.VisibleClipBounds.Height;
int margin_all = 2;
var band_width = (int) (totalWidth*0.1887);
int workspaceWidth = totalWidth - (margin_all*2);
int workspaceHeight = totalHeight - (margin_all*2);
var workspaceSize = new Size(workspaceWidth, workspaceHeight);
var upperLeftWorkspacePoint = new Point(margin_all, margin_all);
var upperLeftInnerEllipsePoint = new Point(upperLeftWorkspacePoint.X + band_width,
upperLeftWorkspacePoint.Y + band_width);
var innerEllipseSize = new Size(((totalWidth/2) - upperLeftInnerEllipsePoint.X)*2,
((totalWidth/2) - upperLeftInnerEllipsePoint.Y)*2);
var outerEllipseRectangle =
new Rectangle(upperLeftWorkspacePoint, workspaceSize);
var innerEllipseRectangle =
new Rectangle(upperLeftInnerEllipsePoint, innerEllipseSize);
double valueMaxRatio = (Value/Max);
var sweepAngle = (int) (valueMaxRatio*360);
var defaultFont = new Font(SystemFonts.DefaultFont.FontFamily,
fontEmSize, FontStyle.Regular);
string format = string.Format("{0:00}", (int) (valueMaxRatio*100));
SizeF measureString = Graphics.MeasureString(format, defaultFont);
var textPoint = new PointF(upperLeftInnerEllipsePoint.X +
((innerEllipseSize.Width - measureString.Width)/2),
upperLeftInnerEllipsePoint.Y +
((innerEllipseSize.Height - measureString.Height)/2));
Graphics.Clear(Color.Transparent);
Graphics.DrawEllipse(BorderPen, outerEllipseRectangle);
Graphics.FillPie(FillPen, outerEllipseRectangle, 0, sweepAngle);
Graphics.FillEllipse(new SolidBrush(Color.White), innerEllipseRectangle);
Graphics.DrawEllipse(BorderPen, innerEllipseRectangle);
Graphics.DrawString(format, defaultFont, FillPen, textPoint);
What does it (try to) solve
End users tend to have the impression to be waiting longer on a process with no progress visualization, then a process with progress indication.
History
- 2011-08-30: Initial version.