Fun with c# code – A disabled ProgressBar

I just noticed this post somehow made the top posts list on my recovery blog, so here is an updated version. (Links updated and code repasted; otherwise it is as is from the original.) Incidentally, the code works just fine on Windows 8 as well.

It’s been a while since I wrote a programming article, so I proudly present the latest addition to my RomyView application, an owner-drawn progress bar, that displays a nicely rendered disabled state when it is disabled. An example of a place where such a control could be used is the Firefox download window. Even if a download is paused, on that window, the progress bars continue to show the annoying progress animation, making it difficult to see at a glance what is paused and what is still downloading.

To see the code described here, you can download either the working application, or the full source code. I haven’t made a small stand-alone example, so you will need my latest upload of my RomyView application.

RomyView Release (built application – requires .NET Framework version 4.5)
RomyView (full source code – requires Visual Studio 2012)

Here is a screenshot of a disabled progress bar:

Progress

I use it in all sorts of places in my application; for example, when pausing a video conversion, or when applying image filters to all images in a directory. Actually I haven’t finished making all the methods where it is used “pausable”, which means, in those places that don’t really pause, it will just draw my grey progress bar instead of the “real” one.

I used to blame the Firefox developers for the animation even for paused downloads, assuming they were guilty of some sloppy code, but the reality is, the progress bar common control simply does not make provision for a disabled state. The Windows Forms ProgressBar control is just a managed wrapper around the common control, so how did I do this? Actually it was pretty easy.

Here is my constructor:

        public CustomProgressBar()
        {
            // The drawing code will flicker unless we enable double-buffering.
            SetStyle(ControlStyles.OptimizedDoubleBuffer | ControlStyles.SupportsTransparentBackColor, true);

            /* Make sure the control can't be sized smaller than the
             * Rectangles used in OnPaint when drawing disabled. */
            MinimumSize = new Size(100, 12);

            EnabledChanged += (sender, e) =>
            {
                /* When this ProgressBar is disabled, force it to be ownerdrawn only;
                 * otherwise the default painting paints over my hard work. */
                SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, !Enabled);
                UpdateStyles();
            };
        }

First of all, I made the control double-buffered. It will flicker otherwise. Secondly, I added an optional line of code, just to make sure that nobody could set the control size smaller than it needs to be, because doing so would result in the calculations in the painting handler getting some rather strange results.

Most importantly above, is the EnabledChanged event handler. All that is does is this… It tells the system that when the control is disabled, not to draw anything at all. This is because the wrapped control always draws enabled. If you want it to draw any other way, you have to draw it from scratch.

Eh? Draw it from scratch? That sounds difficult, right? Wrong. What is a progress bar, really? Basically, it’s two rectangles; the outer one is the bar, while the inner one shows the current progress. Since we’re only drawing it disabled, we don’t need no shitty green animation. As long as we draw it in the right place, everybody will be happy.

So here is my painting handler. (OK, so I use an extension method to draw rounded rectangles, but that is not really necessary. It would look dandy with normal rectangles also.)

        /// <summary>Paints a disabled ProgressBar.</summary>
        /// <remarks><para>
        /// Since ProgressBars don't cater for a disabled appearance, this is my idea of a
        /// disabled ProgressBar's look. It uses grey gradients for a "greyed-out" affect,
        /// drawn as close as I can get to the normal ProgressBar bounds.
        /// </para><para>
        /// For ownerdrawing to work, I had to set the UserPaint and AllPaintingInWmPaint
        /// ControlStyles (when Enabled changes to False), therefore as long as the
        /// ProgressBar is disabled, it will draw nothing at all.</para></remarks>
        protected override void OnPaint(PaintEventArgs e)
        {
            if (!Enabled)
            {
                /* It sometimes gets here with zero width clip rectangle, but this control's
                 * clip rectangle is the same as its bounds anyway, so try the bounds instead. */
                if (e.ClipRectangle.Width <= 0 || e.ClipRectangle.Height <= 0 &&
                    this.Size != Size.Empty && this.Size.Width > 0 && this.Size.Height > 0)
                    e.Graphics.SetClip(this.Bounds);

                e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
                e.Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
                e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

                /* It ends up drawing a rect with height one pixel too small. To overcome this, set
                 * the clip to a slightly inflated rect, and then don't use the whole new height. */
                var clip = e.ClipRectangle;
                clip.Inflate(0, 1); // This will increase the height by 2 pixels. We then use Height - 1.
                e.Graphics.SetClip(clip);

                var borderRect = e.ClipRectangle;
                borderRect.Inflate(-1, 0);
                borderRect = new Rectangle(borderRect.Left, borderRect.Top + 1, borderRect.Width, borderRect.Height - 1);

                if (borderRect.Width > 0 && borderRect.Height > 0)
                {
                    // First draw the rectangle border.
                    using (var borderBrush = new LinearGradientBrush(borderRect, BorderGradientTopColor, BorderGradientBottomColor, LinearGradientMode.Vertical))
                    {
                        using (var pen = new Pen(borderBrush, 1F))
                        {
                            pen.LineJoin = LineJoin.Round;
                            e.Graphics.DrawRoundedRectangle(pen, borderRect, 2);
                        }
                    }

                    /* Fill the background (the bar); then fill the foreground (the progress value).
                     *
                     * In both cases, I do this with two gradient-filled rectangles, which is why I have values for top,
                     * middle and bottom colours. (Middle colour shared by both rectangles.) The top rectangle has the
                     * top corners rounded, and the bottom rectangle has the bottom corners rounded. The corners where
                     * the two rectangles meet are not rounded, so they appear to be one rectangle. */
                    var topBarRect = new Rectangle(borderRect.Left + 1, borderRect.Top, borderRect.Width - 2, 7);
                    var bottomBarRect = new Rectangle(borderRect.Left + 1, borderRect.Top + 7, borderRect.Width - 2, borderRect.Height - 8);

                    if ((borderRect.Width - 2) > 0)
                    {
                        using (LinearGradientBrush topBrush = new LinearGradientBrush(topBarRect, BackGradientTopColor, BackGradientMiddleColor, LinearGradientMode.Vertical),
                            bottomBrush = new LinearGradientBrush(bottomBarRect, BackGradientMiddleColor, BackGradientBottomColor, LinearGradientMode.Vertical))
                        {
                            e.Graphics.FillRoundedRectangle(topBrush, topBarRect, 2, RectangleEdgeFilter.TopLeft | RectangleEdgeFilter.TopRight);
                            e.Graphics.FillRoundedRectangle(bottomBrush, bottomBarRect, 2, RectangleEdgeFilter.BottomRight | RectangleEdgeFilter.BottomLeft);
                        }

                        // Needs more contrast at the bottom.
                        using (var highlightPen = new Pen(ProgressHighlightColor, 1F))
                        {
                            highlightPen.LineJoin = LineJoin.Bevel;
                            e.Graphics.DrawLine(highlightPen, new Point(bottomBarRect.Left, bottomBarRect.Bottom - 1), new Point(bottomBarRect.Right, bottomBarRect.Bottom - 1));
                        }
                    }

                    // This fills the foreground (progress value) as explained above, but not for marquee style.
                    if (Style != ProgressBarStyle.Marquee && Value > 0 && Maximum > 0)
                    {
                        var w = (int)((double)Value / Maximum * (double)(borderRect.Width - 2)) - 1;
                        var h = borderRect.Height - 2;

                        if (w > 0 && h > 0)
                        {
                            var topValueRect = new Rectangle(borderRect.Left, borderRect.Top, w, 7);
                            var bottomValueRect = new Rectangle(borderRect.Left, borderRect.Top + 7, w, h - 6);

                            using (LinearGradientBrush topBrush = new LinearGradientBrush(topValueRect, ProgressGradientTopColor, ProgressGradientMiddleColor, LinearGradientMode.Vertical),
                                bottomBrush = new LinearGradientBrush(bottomValueRect, ProgressGradientMiddleColor, ProgressGradientBottomColor, LinearGradientMode.Vertical))
                            {
                                e.Graphics.FillRoundedRectangle(topBrush, topValueRect, 2, RectangleEdgeFilter.TopLeft | RectangleEdgeFilter.TopRight);
                                e.Graphics.FillRoundedRectangle(bottomBrush, bottomValueRect, 2, RectangleEdgeFilter.BottomRight | RectangleEdgeFilter.BottomLeft);
                            }
                        }
                    }
                }
            }
            else base.OnPaint(e);
        }

And that’s as easy as it is. It wouldn’t be much effort to use this code as a starting point for a progress bar control that always gets ownerdrawn, completely ignoring the built-in one. In fact, writing your own control for pretty-much anything in Windows Forms is quite easy.

The Romy.Controls class library in the source code zip file contains several other controls.

Enjoy.

Advertisements

About Jerome

I am a senior C# developer in Johannesburg, South Africa. I am also a recovering addict, who spent nearly eight years using methamphetamine. I write on my recovery blog about my lessons learned and sometimes give advice to others who have made similar mistakes, often from my viewpoint as an atheist, and I also write some C# programming articles on my programming blog.
This entry was posted in Programming and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s