0

I am trying to create a custom control where I simply draw an image onto a button. The image gets resized to fill the button's area while still keeping the original scale of the image. I keep getting some very weird behavior in design and run mode.

In the designer, most of the time the transparent portions of the image are black. If I make the control small enough, the transparent areas will begin to fill with any random thing they can find on my screen. In run mode, the transparency is always filled with black (see images below).

I feel I might be using the winforms controls in the wrong way, but I don't have much experience with it. I have tried all of the suggestions found here: Using Graphics.DrawImage() to Draw Image with Transparency/Alpha Channel, and a few others I found online, to no avail.

using System.Windows.Forms;
using System.Drawing;

namespace Tools
{
    public class CustomButton : Button
    {
        public CustomButton()
        {
            Image = (Image)Properties.Resources.ResourceManager.GetObject("Custom-Logo-Horiz-RGB");
            ForeColor = BackColor = Color.FromArgb(88, 88, 88);
            DoubleBuffered = true;
        }

        protected override void OnPaint(PaintEventArgs pevent)
        {
            DrawCustomImage(pevent.Graphics);
        }

        private void DrawCustomImage(Graphics graphics)
        {
            float baseHeight = Image.Height;
            float baseWidth = Image.Width;
            float maxHeight = (Height - borderWidth * 2);
            float maxWidth = (Width - borderWidth * 2);

            float newWidth = maxWidth;
            float heightToWidth = baseHeight / baseWidth;
            float newHeight = heightToWidth * newWidth;

            if (newHeight > maxHeight)
            {
                newHeight = maxHeight;
                float widthToHeight = 1 / heightToWidth;
                newWidth = widthToHeight * newHeight;
            }

            graphics.DrawImage(Image, new RectangleF(Width / 2 - newWidth / 2, Height / 2 - newHeight / 2, newWidth, newHeight));
        }

        #region Settings

        private float borderWidth = 6.0F;
        public float BorderWidth
        {
            get { return borderWidth; }
            set { borderWidth = value; }
        }

        #endregion
    }
}

Images:

  1. In design mode: Design Mode Black Background Problem
  2. In design mode: Design Mode Weird Behavior
  3. In run mode: Run Mode Black Background
  • Since drawing bitmaps with transparency typically works fine in GDI+ (and so, in .NET Winforms), there must be something wrong in your particular scenario. The two most likely explanations are: 1) bug in the installed video driver, and 2) some problem with the bitmap file you're using. If you provide an exact copy of the bitmap as part of your question, it would be possible to investigate that possibility. But given the "design mode weird behavior" example, IMHO the first thing you should try is updating the video driver, or even trying the code on a differently-configured computer. – Peter Duniho Jun 28 '19 at 01:32
  • This is no surprise at all. You forgot to call `base.OnPaint(pevent)` in the overridden `OnPaint` method. But, it will cause the bitmap to be painted twice, if you use the default `Image` property to store the bitmap. Assign the Image to a private field: `private Image myImage = null;` then: `this.myImage = Properties.Resources.Payce-Logo-Horiz-RGB;` in the constructor and use this reference to paint the bitmap. – Jimi Jun 28 '19 at 02:34
  • There is no transparency in WinForms, because GDI+ can't do transparency. – vasily.sib Jun 28 '19 at 03:14
  • @vasily.sib ?? This is about drawing a semi-transparent bitmap. GDI+ treats transparency without problems. You're referring to WinForms controls, probably. But, for example: [Translucent control](https://stackoverflow.com/a/51435842/7444103). – Jimi Jun 28 '19 at 03:22
  • @Jimi you are right. I should probably say _"you should be aware, that transparency (or translucency?) is very hard to use with WinForms controls"_ – vasily.sib Jun 28 '19 at 03:44
  • 1
    @vasily.sib That's for sure, there's nothing (almost - Forms are the exception) built in. But drawing transparent objects inside a Control's canvas - Image objects that support transparency in particular - is *well* supported. The real problem is to make transparent controls interact consistently with each other. That's really hard and the results are not exaclty great anyway, or similar to something you can easily achieve in WPF, for example. – Jimi Jun 28 '19 at 03:57
  • What exactly do you want to see in the transparent regions? Only the parent control can shine through! – TaW Jun 28 '19 at 08:01
  • @Jimi Your answer worked perfectly thanks! You should post it as an answer so I can accept it and others can see. On the topic of disposing, are there any good articles you can recommend to get a good overview of how to properly dispose? Still pretty new to VS and Winforms, trying to learn more. – Reed Nowling Jun 28 '19 at 13:23

1 Answers1

0

I suggested to call base.OnPaint(e) in the OnPaint override because, unless the Button FlatStyle (or, better, the style of the ButtonBase class from which Button derives) is of type FlatStyle.System, the Button control is considered OwnerDrawn. As a consequence, it's created with ControlStyles.UserPaint. This has a number of consequences in the way the control is drawn by the ButtonBase class dispatchers derived from ButtonBaseAdapter which decide the rendering style and the actions of the internal PaintWorker methods.

Plus, as you can see in the ButtonBase constructor, Buttons are created with ControlStyles.Opaque style (and you can also see that ControlStyles.OptimizedDoubleBuffer style is used). This means that the Button class doesn't draw it's background, it's the PaintWorker that calls PaintThemedButtonBackground (if Application.RenderWithVisualStyles = true, otherwise the standard background), using the same PaintEventArgs generated for the Button class (you can also determine that DoubleBuffering is enabled by default).

As a consequence, you need to call base.OnPaint(e) in the override if you want the control to render properly.

The call to base.OnPaint(e) also draws the bitmap assigned to the Image property, if any.
That's why I suggested to assign your own Bitmap to a field (or another custom property), without setting the Image property. If you do, the Image will be painted twice: one on your own terms and the other by the PaintWorker.

About disposing of the unmanaged object:
If you derive a Custom Control from a .Net control, you don't really need to worry that much about the control itself. It's all handled internally. You can see in the code I posted here that protected override void Dispose(bool disposing) is used: I put it there so you can see that this method is only called when the application closes; also, it's called with the disposing parameter set to false: it's the Finalizer that's calling it, the object has already been disposed of, its resources along with it.

You may want to take care of the object you create, especially the Graphics object, when you create them: dispose of these objects right away, calling Dispose() on them or declaring these objects with a using statement, which under the hood will create a try/finally block, disposing of the object in the finally section.

You can see in the code posted here, that when a new Image is set, the old one is disposed of right away.
The OnHandledDistroyed method is overridden, to get rid of the current object assigned to Field that holds the Bitmap your Button is displaying. This because this Bitmap comes from an embedded resource, better ddispose of it as soon as it's not needed anymore.

If you instead create a class that uses unmanaged resources, which doesn't derive from another that already handles garbage collection, then implement the IDisposable interface.

Some documents on the subject:

Eric Lippert's series on Garbage collection and finalizers: When everything you know is wrong, part one
MSDN: Implementing a Dispose method (and following pages).

Here's a modified class that implements some of the suggestions:

  • Note that a private Field is used to substitute the Image property: the Image property will be null (and not painted), while the property is still accessible in the Designer and you can assign another Image without compromising the result.
  • The old Image, if any, is disposed each time is substituted with a new one.
  • The BackgroundImage property is instead hidden.

using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;

[DesignerCategory("code")]
public class PayceButton : Button
{
    private Image myImage = null;
    private float borderWidth = 6.0F;
    public PayceButton() => InitializeComponent();
    private void InitializeComponent()
    {
        this.myImage = Properties.[A Resource Image Name];
        this.BackColor = Color.FromArgb(88, 88, 88);
    }

    public float BorderWidth {
        get => borderWidth;
        set { borderWidth = value; this.Invalidate(); }
    }

    public override string Text {
        get => string.Empty;
        set => base.Text = string.Empty;
    }

    public new Image Image {
        get => this.myImage;
        set { this.myImage?.Dispose();
              this.myImage = value;
              Invalidate();
        }
    }

    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public override Image BackgroundImage {
        get => base.BackgroundImage;
        set => base.BackgroundImage = null;
    }

    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
    public override ImageLayout BackgroundImageLayout {
        get => base.BackgroundImageLayout;
        set => base.BackgroundImageLayout = ImageLayout.None;
    }

    protected override void OnPaint(PaintEventArgs e) {
        base.OnPaint(e);
        DrawPayceImage(e.Graphics);
    }

    private void DrawPayceImage(Graphics g)
    {
        float scale = (Math.Min(this.Height, this.Width) - (borderWidth * 4)) / 
                       Math.Min(myImage.Height, myImage.Width);
        var scaledImageSize = new SizeF(this.myImage.Width * scale, myImage.Height * scale);
        var imageLocation = new PointF((this.Width - scaledImageSize.Width) / 2,
                                       (this.Height - scaledImageSize.Height) /2);
        g.DrawImage(myImage,
            new RectangleF(imageLocation, scaledImageSize),
            new RectangleF(PointF.Empty, myImage.Size), GraphicsUnit.Pixel);
    }

    protected override void OnHandleDestroyed(EventArgs e) {
        this.myImage?.Dispose();
        base.OnHandleDestroyed(e);
    }

    protected override void Dispose(bool disposing) {
        if (disposing) { this.myImage?.Dispose(); }
        base.Dispose(disposing);
    }
}
Jimi
  • 29,621
  • 8
  • 43
  • 61