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
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.