Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
496 views
in Technique[技术] by (71.8m points)

c# - How to apply a fade transition effect to PictureBox Images using a Timer?

I am trying to make fade transition between two PictureBox Controls.
I used a Timer to change the opacity of two PictureBoxes using GetPixel and SetPixel every time the time elapses.

At this stage, the problem is that this code causes an exception:

System.InvalidOperationException: object is currently in use elsewhere

I tried to modify cloned Bitmaps instead of directly operate on the Bitmaps set to the Image properties of the controls, but it doesn't work anyway.
Here is my code:

public Bitmap changeOpacity(Bitmap pic, int opacity)
{
    for (int w = 0; w < pic.Width; w++)
    {
        for (int h = 0; h < pic.Height; h++)
        {
            Color c = pic.GetPixel(w, h);
            Color newC = Color.FromArgb(opacity, c);
            pic.SetPixel(w, h, newC);
        }
    }
    return pic;
}

public void CrossFade(PictureBox pictureOut, PictureBox pictureIn, int duration)
{
    int outChange = 255; // opacity of pictureOut
    int inChange = 0;    // opacity of pictureIn
    int change = 55;     // change of opacity 
    fadeTimer.Interval = 10; // this timer's type is System.Timers.Timer
    Bitmap bmp = new Bitmap(pictureIn.Image);
    // make the pictureIn transparent first
    pictureIn.Image = changeOpacity((Bitmap)bmp.Clone(), 0);
    fadeTimer.Elapsed += (sender, e) => CrossFadeEvent(sender, e, pictureOut, pictureIn, outChange, inChange, change);
    fadeTimer.Start();
}

// being called every time interval
private void CrossFadeEvent(Object source, System.Timers.ElapsedEventArgs e, PictureBox pictureOut, PictureBox pictureIn, int oChange, int iChange, int change)
{
    if (iChange <= 255)
    {
        oChange -= change;
        iChange += change;
        textBox1.Text = iChange.ToString();
        pictureOut.Image = changeOpacity((Bitmap)pictureOut.Image.Clone(), oChange);
        pictureIn.Image = changeOpacity((Bitmap)pictureIn.Image.Clone(), iChange);
    }
    else if (iChange > 255)
    {
        pictureIn.Image = changeOpacity((Bitmap)pictureOut.Image.Clone(), 255);
        fadeTimer.Stop();
    }
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

There are some problem to fix here:

fadeTimer.Interval = 10;:
You're (possibly) using the wrong Timer: the System.Timers.Timer's Elapsed is raised in a ThreadPool Thread. Unless you have set the SynchronizingObject to a Control object, which is then used to marshal the handler calls, referencing a Control in the handler will cause trouble (cross-thread violation exceptions). In this context, you can use a System.Windows.Forms.Timer instead: its Tick event is raised in the UI thread.
Also, the timer interval is to low. The standard (official) resolution of the System.Windows.Forms.Timer is 55ms (higher than the System.Timers.Timer). You end up with overlapping events.

GetPixel() / SetPixel():
cannot be used for this task. These methods are too slow when called sequentially to set multiple pixels. They're used to modify a small set of pixels (or just one), not to modify the pixels of a whole image.
Bitmap.LockBits() is the common tool used to set the color bytes of Bitmaps.

pictureOut.Image = changeOpacity((Bitmap)pictureOut.Image.Clone(), oChange);:
You're using the Image property of a Control to provide the source Bitmap, then you're setting the same property that provided the source using the same source, modified.
This will never give you fully faded Image and you're looking for trouble.

There's a simple tool that can be used to accomplish this task quite easily: the ColorMatrix class. This class handles a standard 5x5 Matrix, providing some simplified tools that allow to set the values of the Matrix components.
The 5x5 Matrix component at [3, 3] (Matrix3x3) represents the Alpha value of all the RGB components.
The ColorMatrix class is applied to a Bitmap using the SetColorMatrix() method of the ImageAttributes class, which is then passed to the Graphics.DrawImage() overload that accepts an ImageAttributes object as argument.

Since this Fade procedure could be useful in other situations, I think it's a good idea to create an Extension Method: it adds a new SetOpacity() method to the Bitmap class.
It can also change the Gamma at the same time, should the fade effect require it.

What's left is to load two Bitmaps, create a Timer, set a sensible Interval (100ms here) and paint the Bitmap on the surface of two PictureBox Controls (three here, to test a simple blending effect ).

The Opacity increment/decrement value is set to .025f, so the opacity changes 1/4 of the 0.0f-1.0f max range each second. To adjust as required.


Bitmap Fade ColorMatrix
The quality is reduced since the GIF animation can only use 256 colors

? Add the extension class to the Project.
? Setup a Form with 3 PictureBox Controls and assign the 3 event handles you find here to each of them.
? Don't assign a Bitmap to the PictureBoxes at design-time. The Bitmaps are loaded at run-time, as shown in the sample code.
? Add a Button to start the Timer.
? when the fading procedure terminates, you restart the Timer immediately, since it rewinds itself (the fading starts over, applying the inverse effect to each Bitmap and to the blended Bitmaps).

Bitmap Fade and blend ColorMatrix

Keep the Diagnostics Tools open: you'll see that you don't waste a single MB of memory, even if you repeat the operation multiple times.

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

public partial class FormBitmaFadeTest : Form
{
    Bitmap sourceBmp1 = null;
    Bitmap sourceBmp2 = null;
    Bitmap fadeBmp1 = null;
    Bitmap fadeBmp2 = null;

    float opacity1 = 0.0f;
    float opacity2 = 1.0f;
    float increment = .025f;
    Timer timer = null;

    public FormBitmaFadeTest()
    {
        InitializeComponent();

        if (components == null) components = new System.ComponentModel.Container();
        components.Add(timer);

        string image1Path = [Source Image 1 Path];
        string image2Path = [Source Image 2 Path];

        sourceBmp1 = (Bitmap)Image.FromStream(new MemoryStream(File.ReadAllBytes(image1Path)));
        sourceBmp2 = (Bitmap)Image.FromStream(new MemoryStream(File.ReadAllBytes(image2Path)));
        fadeBmp1 = sourceBmp1.Clone() as Bitmap;
        fadeBmp2 = sourceBmp2.Clone() as Bitmap;
        timer = new Timer() { Interval = 100 };
        timer.Tick += this.TimerTick;
    }

    private void TimerTick(object sender, EventArgs e)
    {
        opacity1 += increment;
        opacity2 -= increment;
        if ((opacity1 >= 1.0f || opacity1 <= .0f) || (opacity2 >= 1.0f || opacity2 <= .0f)) {
            increment *= -1;
            timer.Stop();
        }
        fadeBmp1?.Dispose();
        fadeBmp2?.Dispose();
        fadeBmp1 = sourceBmp1.SetOpacity(opacity1);
        fadeBmp2 = sourceBmp2.SetOpacity(opacity2);
        pictureBox1.Invalidate();
        pictureBox2.Invalidate();
        pictureBox3.Invalidate();
    }

    private void pictureBox1_Paint(object sender, PaintEventArgs e)
    {
        if (fadeBmp1 == null) return;
        var units = GraphicsUnit.Pixel;
        e.Graphics.DrawImage(fadeBmp1, new RectangleF(PointF.Empty, pictureBox1.ClientSize), fadeBmp1.GetBounds(ref units), units);
    }

    private void pictureBox2_Paint(object sender, PaintEventArgs e)
    {
        if (fadeBmp2 == null) return;
        var units = GraphicsUnit.Pixel;
        e.Graphics.DrawImage(fadeBmp2, new RectangleF(PointF.Empty, pictureBox2.ClientSize), fadeBmp2.GetBounds(ref units), units);
    }

    private void pictureBox3_Paint(object sender, PaintEventArgs e)
    {
        if (fadeBmp1 == null || fadeBmp2 == null) return;
        var units = GraphicsUnit.Pixel;
        e.Graphics.DrawImage(fadeBmp2, new RectangleF(PointF.Empty, pictureBox3.ClientSize), fadeBmp2.GetBounds(ref units), units);
        e.Graphics.DrawImage(fadeBmp1, new RectangleF(PointF.Empty, pictureBox3.ClientSize), fadeBmp1.GetBounds(ref units), units);
    }
}

The extension method:
Extension Methods (C# Programming Guide)

using System.Drawing;
using System.Drawing.Imaging;

public static class BitmapExtensions
{
    static float[][] fadeMatrix = {
        new float[] {1, 0, 0, 0, 0},
        new float[] {0, 1, 0, 0, 0},
        new float[] {0, 0, 1, 0, 0},
        new float[] {0, 0, 0, 1, 0},
        new float[] {0, 0, 0, 0, 1}
    };

    public static Bitmap SetOpacity(this Bitmap bitmap, float Opacity, float Gamma = 1.0f)
    {
        var mx = new ColorMatrix(fadeMatrix);
        mx.Matrix33 = Opacity;
        var bmp = new Bitmap(bitmap.Width, bitmap.Height);

        using (var g = Graphics.FromImage(bmp))
        using (var attributes = new ImageAttributes()) {
            attributes.SetGamma(Gamma, ColorAdjustType.Bitmap);
            attributes.SetColorMatrix(mx, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
            g.Clear(Color.Transparent);
            g.DrawImage(bitmap, new Rectangle(0, 0, bmp.Width, bmp.Height),
                0, 0, bitmap.Width, bitmap.Height, GraphicsUnit.Pixel, attributes);
            return bmp;
        }
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...