Question

I'm tring to build a Control derived class which supports an Opcacity property.
This control could host both text and image and will beable to fade them out and in.
Here is my code:

internal class FadeControl : Control
{
    private int opacity = 100;

    public FadeControl()
    {
        SetStyle(ControlStyles.SupportsTransparentBackColor, true);
    }

    public int Opacity
    {
        get
        {
            return opacity;
        }
        set
        {
            if (value > 100) opacity = 100;
            else if (value < 1) opacity = 1;
            else opacity = value;

            if (Parent != null)
                Parent.Invalidate(Bounds, true);
        }
    }

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ExStyle = cp.ExStyle | 0x20;
            return cp;
        }
    }

    protected override void OnPaintBackground(PaintEventArgs e)
    {
        //do nothing
    }

    protected override void OnMove(EventArgs e)
    {
        RecreateHandle();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        using (Graphics g = e.Graphics)
        {
            Rectangle bounds = new Rectangle(0, 0, Width - 1, Height - 1);
            int alpha = (opacity * 255) / 100;

            using (Brush bckColor = new SolidBrush(Color.FromArgb(alpha, BackColor)))
            {
                if (BackColor != Color.Transparent)
                    g.FillRectangle(bckColor, bounds);
            }

            ColorMatrix colorMatrix = new ColorMatrix();
            colorMatrix.Matrix33 = (float)alpha / 255;
            ImageAttributes imageAttr = new ImageAttributes();
            imageAttr.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

            if (BackgroundImage != null)
                g.DrawImage(BackgroundImage, bounds, 0, 0, Width, Height, GraphicsUnit.Pixel, imageAttr);

            if (Text != string.Empty)
            {
                using (Brush txtBrush = new SolidBrush(Color.FromArgb(alpha, ForeColor)))
                {
                    g.DrawString(Text, Font, txtBrush, 5, 5);
                }
            }
        }
    }

    protected override void OnBackColorChanged(EventArgs e)
    {
        if (Parent != null)
            Parent.Invalidate(Bounds, true);

        base.OnBackColorChanged(e);
    }

    protected override void OnParentBackColorChanged(EventArgs e)
    {
        Invalidate();

        base.OnParentBackColorChanged(e);
    }
}

I've putted the control on a form which has a timer on it.
The timer set the control's opacity from 0 to 100 and back and its working well.
The problem I'm trying to solved is that the control flickers while changing its opacity.
Setting the control toControlStyles.DoubleBuffer will make the control invisible on the form.

Any advice will be welcome.

Was it helpful?

Solution

I was unable to use both a double buffer and WS_EX_TRANSPARENT (0x20) for the transparent background. So I decided to implement the transparent background by copying the content of the parent control and use double buffer to prevent flicker.

The following is the final source code, tested and working:

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

internal class FadeControl : Control
{
    private int opacity = 100;
    private Bitmap backgroundBuffer;
    private bool skipPaint;

    public FadeControl()
    {
        SetStyle(ControlStyles.SupportsTransparentBackColor, true);
        SetStyle(ControlStyles.ResizeRedraw, true);
        SetStyle(ControlStyles.DoubleBuffer |
                 ControlStyles.AllPaintingInWmPaint |
                 ControlStyles.UserPaint, true);
    }

    public int Opacity
    {
        get
        {
            return opacity;
        }
        set
        {
            if (value > 100) opacity = 100;
            else if (value < 1) opacity = 1;
            else opacity = value;

            if (Parent != null)
                Parent.Invalidate(Bounds, true);
        }
    }

    protected override void OnPaintBackground(PaintEventArgs e)
    {
        //do nothig
    }

    protected override void OnMove(EventArgs e)
    {
        RecreateHandle();
    }

    private void CreateBackgroundBuffer(Control parent)
    {
        int offsetX;
        int offsetY;
        GetOffsets(out offsetX, out offsetY, parent);
        backgroundBuffer = new Bitmap(Width + offsetX, Height + offsetY);
    }

    protected override void OnResize(EventArgs e)
    {
        var parent = Parent;
        if (parent != null)
        {
            CreateBackgroundBuffer(parent);
        }
        base.OnResize(e);
    }

    private void GetOffsets(out int offsetX, out int offsetY, Control parent)
    {
        var parentPosition = parent.PointToScreen(Point.Empty);
        offsetY = Top + parentPosition.Y - parent.Top;
        offsetX = Left + parentPosition.X - parent.Left;
    }

    private void UpdateBackgroundBuffer(int offsetX, int offsetY, Control parent)
    {
        if (backgroundBuffer == null)
        {
            CreateBackgroundBuffer(parent);
        }
        Rectangle parentBounds = new Rectangle(0, 0, Width + offsetX, Height + offsetY);
        skipPaint = true;
        parent.DrawToBitmap(backgroundBuffer, parentBounds);
        skipPaint = false;
    }

    private void DrawBackground(Graphics graphics, Rectangle bounds)
    {
        int offsetX;
        int offsetY;
        var parent = Parent;
        GetOffsets(out offsetX, out offsetY, parent);
        UpdateBackgroundBuffer(offsetX, offsetY, parent);
        graphics.DrawImage(backgroundBuffer, bounds, offsetX, offsetY, Width, Height, GraphicsUnit.Pixel);
    }

    private void Draw(Graphics graphics)
    {
        Rectangle bounds = new Rectangle(0, 0, Width, Height);
        DrawBackground(graphics, bounds);

        int alpha = (opacity * 255) / 100;

        using (Brush bckColor = new SolidBrush(Color.FromArgb(alpha, BackColor)))
        {
            if (BackColor != Color.Transparent)
            {
                graphics.FillRectangle(bckColor, bounds);
            }
        }

        ColorMatrix colorMatrix = new ColorMatrix();
        colorMatrix.Matrix33 = (float)alpha / 255;
        ImageAttributes imageAttr = new ImageAttributes();
        imageAttr.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);

        if (BackgroundImage != null)
        {
            graphics.DrawImage(BackgroundImage, bounds, 0, 0, Width, Height, GraphicsUnit.Pixel, imageAttr);
        }

        if (Text != string.Empty)
        {
            using (Brush txtBrush = new SolidBrush(Color.FromArgb(alpha, ForeColor)))
            {
                graphics.DrawString(Text, Font, txtBrush, 5, 5);
            }
        }
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        if (!skipPaint)
        {
            Graphics graphics = e.Graphics;
            Draw(graphics);
        }
    }

    protected override void OnBackColorChanged(EventArgs e)
    {
        if (Parent != null)
        {
            Parent.Invalidate(Bounds, true);
        }
        base.OnBackColorChanged(e);
    }

    protected override void OnParentBackColorChanged(EventArgs e)
    {
        Invalidate();
        base.OnParentBackColorChanged(e);
    }
}

Note that the method CreateParams is no longer present, also I've changed the contructor.

The field skipPaint is to know when not to paint in order to be able to able to tell the parent to draw itself to a bitmap during OnPaint without having infinite recursion.

backgroundBuffer is not to implement double buffering, but to keep a copy of the contents of the parent without the control rendered. It is updated each paint, I know there are more efficient solutions...* yet this approach keeps it simple and shouldn't be a bottleneck unless you have too many of these controls on the same container.

*: A better solution would be to update it each time the parent invalidates. Futhermore shared it among all the FadeControls in the same parent.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top