Click here to Skip to main content
Click here to Skip to main content

Creating a 'Progress Cursor'

By , 1 Jul 2012
 

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

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);

 //do some work
}

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();
	
	//add text to the default drawn cursor
	e.Graphics.DrawString("Test", 
	           SystemFonts.DefaultFont, Brushes.Black, 0,0);
	
	//set Handled to true, or else nothing will happen,
	//and default painting is done
	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);

        //simulate some work
    }
}
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
    {
        //do heavy duty stuff here...
    }
    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);
}
1-3 P/Invoke methods

These methods are called in CreateCursor (1-4):

private Cursor CreateCursor(Bitmap bmp, Point hotSpot)
{
	//gets the 'icon-handle' of the bitmap
	//(~.net equivalent of bmp as Icon)
	IntPtr iconHandle = bmp.GetHicon();
	IconInfo iconInfo = new IconInfo();
	
	//fill the IconInfo structure with data from the iconHandle
	UnManagedMethodWrapper.GetIconInfo(iconHandle, ref iconInfo);
	
	//set hotspot coordinates
	iconInfo.xHotspot = hotSpot.X;
	iconInfo.yHotspot = hotSpot.Y;
	
	//indicate that this is a cursor, not an icon
	iconInfo.fIcon = false;
	
	//actually create the cursor
	iconHandle = 
	  UnManagedMethodWrapper.CreateIconIndirect(ref iconInfo);
	
	//return managed Cursor object
	return new Cursor(iconHandle);
}
1-4 Cursor magic!

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Pieter Van Parys
Software Developer SPHINX-IT
Belgium Belgium
Member
LinkedIn Profile
 
I maintain a blog at pietervp.com

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralMy vote of 5memberMichael Grünwaldt11 Jul '12 - 23:48 
like it Smile | :)
added the IDisposable and the "DestroyIcon(iconHandle);" Snippet
GeneralGDI objects aren't managed and need to be disposedmemberjeffb423 Jul '12 - 17:31 
Neat control.   One thing missing though is the use of wrapping the GDI objects in using() statements.   GDI objects (like Bitmap, Font, etc...) wrap native Win32 APIs - they are derived from IDispose and need to be placed in using() statements (or explicitly call the object's .Dispose() method), otherwise you leak non-managed GDI objects.
 
Try running your animation against PerfMon - it will report back memory leaks.
 
Jeff
GeneralRe: GDI objects aren't managed and need to be disposedmemberPieter Van Parys3 Jul '12 - 19:49 
Thank you for the feedback! I'm q bit confused though, why wouldnt the garbage collector eventually call the Finalize method on eg an Image? Don't get me wrong, its always better to have a using statement when working with IDisposables, but I don't get why not doing the using statement would cause a memory leak. All resources are declared in a 'local' scope (method) so they should be collected right?
GeneralRe: GDI objects aren't managed and need to be disposedmemberjeffb424 Jul '12 - 10:34 
Eventually, the GC will clean up the objects, but there are several caveats...
 
First, when a GDI-based object is created, not only is managed memory for the .NET object used, but a GDI resource handle is used as well.   GDI resources not only consume memory, but are finite (go try and create 1000 font object).   Fonts, Pens and other GDI resources have a limited number which can be created.   That's why one is only supposed to hold onto a GDI resource for only as long as they need it, then quickly release it.   Get in, get out.
 
When a .NET GDI object isn't disposed of, the GDI resource just sits there until the GC runs, which isn't something one can count on.   Also, it's possible to still have free memory available on your system, but to run out of GDI handles.
 
Second, this also assumes that when the GC runs, that no references to the objects remain.   We've seen with WinForm events where even after the Form goes away (and one would assume that the memory would get cleaned up by the GC), that there are some lingering weak references to some of the objects, and they don't get released or cleaned up.
 
I haven't tried it, but it would be interesting to test your cursor control with PerfMon.   Try two tests:   Have one test create a single instance and run for a few hours, and another test that consecutively creates the cursor, runs through a normal loop (the one you currently have), then repeat for a few hours.
 
Nice control & article though.   Hope this helps,
 
Jeff
QuestionnicememberCIDev2 Jul '12 - 3:31 
A well written article and a cool cursor effect. Smile | :)
Just because the code works, it doesn't mean that it is good code.

GeneralMy vote of 5mentorMd. Marufuzzaman2 Jul '12 - 2:41 
Cool, I love this post.. Thanks Smile | :)
QuestionVote of 5memberGanesanSenthilvel2 Jul '12 - 0:37 
Vote of 5
GeneralMy vote of 5membermanoj kumar choubey20 Feb '12 - 21:36 
Nice
QuestionWin32 handle passed to Cursor is not valid or is the wrong type.memberMember 40789589 Dec '11 - 5:05 
Hello! I read articles and downloaded test project to review
but
i comment line // Thread.Sleep(100);
for (int i = 0; i < 100; i++)
                                {
                                    progressCursor.IncrementTo(i);
 
                                    //simulate some work
                                   // Thread.Sleep(100);
                                }
 
after i click 10 time on button, then program throw exception in below funtion
 
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);
 

}
 
Pls help solve problem!
 
thanks!
Nguyen Thanh Xuan

AnswerRe: Win32 handle passed to Cursor is not valid or is the wrong type.membersapatag5 Jul '12 - 2:15 
For me it solved:
 
[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern bool DestroyIcon(IntPtr handle);
 
private IntPtr iconHandle;
 
private Cursor CreateCursor(Bitmap bmp)
{
    if (iconHandle != IntPtr.Zero)
        DestroyIcon(iconHandle);
    iconHandle = bmp.GetHicon();
    return new Cursor(iconHandle);
}

GeneralGreat idea!memberdanlobo13 Oct '11 - 8:16 
Great idea! Thanks for sharing!
Questionvery nicememberCIDev11 Oct '11 - 6:06 
A well written & uesful article.
Just because the code works, it doesn't mean that it is good code.

QuestionNice OnememberGandalf - The White10 Oct '11 - 2:15 
My 5! Thumbs Up | :thumbsup:
Believe Yourself™

GeneralMy vote of 5memberOshtri Deka9 Oct '11 - 0:28 
Nice!
GeneralRe: My vote of 5memberPieter Van Parys12 Oct '11 - 5:08 
Thx!
QuestionVery goodmembermarc ochsenmeier19 Sep '11 - 9:35 
thanks for sharing this!
AnswerRe: Very goodmemberPieter Van Parys20 Sep '11 - 0:23 
Thx! Thumbs Up | :thumbsup:
QuestionLove It!memberNickPace8 Sep '11 - 13:05 
Very innovative and user-friendly. Can't wait to try this out in one of my projects. Nice work!
-NP
 
Never underestimate the creativity of the end-user.

AnswerRe: Love It!memberPieter Van Parys8 Sep '11 - 22:50 
Thanks!
GeneralAwesome!memberabdurahman ibn hattab7 Sep '11 - 2:00 
This is beautiful! Great idea and implementation Thumbs Up | :thumbsup:
GeneralRe: Awesome!memberPieter Van Parys7 Sep '11 - 6:13 
Thanks dude!
QuestionGreat stuffmemberKDME7 Sep '11 - 1:23 
This will improve our .NET application exponentially.
I would definitely recommend this to my project manager!
 
Goe bezig! Wink | ;-)
AnswerRe: Great stuffmemberPieter Van Parys7 Sep '11 - 4:26 
Thank you, good luck with that project manager of yours!
QuestionMy vote of 5memberFilip D'haene6 Sep '11 - 23:36 
Just excellent!
 

Thanks for sharing. Smile | :)
AnswerRe: My vote of 5memberPieter Van Parys7 Sep '11 - 6:14 
No problem, glad you like it!
GeneralMy vote of 5memberAnkush Bansal6 Sep '11 - 23:33 
Thanks for sharing this great information.
GeneralRe: My vote of 5memberPieter Van Parys7 Sep '11 - 6:14 
You're welcome!
GeneralMy vote of 5memberScruffyDuck6 Sep '11 - 20:24 
Very Good - Thanks
GeneralRe: My vote of 5memberPieter Van Parys7 Sep '11 - 7:40 
Thanks Thumbs Up | :thumbsup:
QuestionGreatmemberWrangly6 Sep '11 - 8:36 
Great control ! thanks - proficiat uit ... Belgïe Wink | ;) !
 
Domi.

modified on Tuesday, September 6, 2011 2:43 PM

AnswerRe: GreatmemberPieter Van Parys6 Sep '11 - 20:35 
Thanks!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 2 Jul 2012
Article Copyright 2011 by Pieter Van Parys
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid