0

So I'm attempting to make some animations with Winforms, and more specifically, a left to right animation. However, I've ran into multiple problems. Firstly, the System.Windows.Forms.Timer and even the other Timer classes like System.Threading.Timer are not nearly fast enough for the animation I want. To compensate, I could increase the amount of pixels by which I add to the left right animation. However, this results in a choppy-animation, which is not what I'm going for. To fix this, I'm using my own timer (on another thread), which is much more accurate:

long frequency = Stopwatch.Frequency;
long prevTicks = 0;
while (true)
{
    double interval = ((double)frequency) / Interval;
    long ticks = Stopwatch.GetTimestamp();
    if (ticks >= prevTicks + interval)
    {
        prevTicks = ticks;
        Tick?.Invoke(this, EventArgs.Empty);
    }
}

This however has its own drawbacks. First, this puts a heavy load on the CPU. Secondly, I cannot redraw fast enough if I want to increase the left-right animation by 1 pixel at a time for a smooth animation. The solution to this is to directly draw on the graphics provided by CreateGraphics, and it works fairly well, except when we go to transparent brushes. Then, things slow down. The solution to all of this is to just increase the amount of pixels I draw at a time on the left-right animation, but this would result in a lack of smoothness for the animation. Here is some test code:

private int index;
private Graphics g;
private Brush brush;
private void FastTimer_Tick(object sender, EventArgs e)
{
    index++;
    if (g == null)
    {
        g = CreateGraphics();
    }
    if (brush == null)
    {
        brush = new SolidBrush(Color.FromArgb(120, Color.Black));
    }
    g.FillRectangle(brush, index, 0, 1, Height);
}

I've heard the GDI is much faster as it's hardware accelerated, but I'm not sure how to use it. Does anybody have a solution to this while sticking to winforms? Thanks.

EDIT: Here's an example video: https://www.youtube.com/watch?v=gcOttFFCUz8&feature=youtu.be When the form is minimized, it's very smooth. However, when the form is maximized, I have to compensate smoothness for speed. I'm looking to know how to redraw faster (possibly using GDI) so that I can still use +1px animations, for a smooth experience.

VS-ux
  • 41
  • 1
  • 7
  • Use a double-buffered Control, as a PictureBox and draw your shape in its Paint event (or `OnPaint` method). Don't use `CreateGraphics()` (also, checking for `null` is irrelevant). Anti-aliasing can hide the single step motion, but you should use float values to increment the horizontal shift. It looks like you want to draw a moving line (as in a sound wave meter/analyzer?). You can then use a StopWatch or a [higher resolution timer](https://stackoverflow.com/q/7137121/7444103) to generate those values. You'll have to test the UI reaction to this. – Jimi Jul 02 '20 at 17:23
  • Thank you for your reply Jimi! I draw using `CreateGraphics` because if I don't, then calling Refresh or Invalidate is too slow. Also, I'm attempting to draw a moving rectangle the width and height of the control (which will be about the size of the screen). – VS-ux Jul 02 '20 at 17:44
  • Not if you Invalidate() your Control in its Paint event, then draw only when your Timer says it's time. To draw faster than that, you need another *Engine*. -- `CreateGraphics()` is not faster: you actually create that object twice, since it's created anyway when the Control receive a `WM_PAINT` message. If you try to reuse a store object, you'll have very *weird* results (or completely wrong, because of clipping), or exceptions. -- This: `FillRectangle(brush, index, 0, 1, Height);` does not draw what you described. – Jimi Jul 02 '20 at 17:55
  • Unless you think you can make that line *stick*. You'll be disappointed. – Jimi Jul 02 '20 at 18:03
  • I've tried your solution, however, it actually isn't as fast as the code I posted up there. Also, the code above does produce what I want (just not fast enough), because drawing on the object provided by `CreateGraphics` does not invalidate the control, so everything there stays as is. – VS-ux Jul 02 '20 at 18:12
  • ... unless something else invalidates the DC without your intervention, then your stored object is invalid (which may/will generate the weird effects I mentioned). I don't know what this is for, so I also cannot say what other stuff you have in your Form, but I don't think the purpose of this app is to draw a half-transparent black block on a canvas. But, if this is actually all that's to it (and nobody moves, *obscures*, minimizes/maximizes or otherwise causes your Form to repaint), then draw your block. Or describe what this is actually for, maybe you get better advice. – Jimi Jul 02 '20 at 18:19
  • Yes. Sorry. I'm making a docking type-application similar to VSCode. (Like how you can move tabs around in VSCode) – VS-ux Jul 02 '20 at 18:21
  • You can use a Layered Form for that (alpha-blended). Or DirectX. But I don't see why you would draw a semi-transparent block line by line. Why don't you paint the whole thing in one go? Anyway, you're quite far from it at this point. – Jimi Jul 02 '20 at 18:26
  • I draw a semi-transparent block line by line so that it produces a nice sliding animation effect, similar to VSCode. Also, I've actually gotten quite far, and the draft is already complete. It's just I'm looking for a way to make it smoother for larger displays. – VS-ux Jul 02 '20 at 18:28
  • As mentioned, you need an alpha-blended Form, drawing a semi-transparent Bitmap onto its DC, to get close to something similar. If you *draw lines* on a static Graphics context that WILL be invalidated, you may get close to a static look-alike, not more than that. The *speed* of it is the last thing to worry about. – Jimi Jul 02 '20 at 19:05
  • OK! Thank you! I will try this. – VS-ux Jul 02 '20 at 19:09

2 Answers2

0

There is a lot of stuff to improve/change in your code,
Here is a suggestion, please see comments inside the code:

private void button1_Click(object sender, EventArgs e)
{
    // optional: most animations are made in another thread to keep the GUI free, please see Invoke() to update GUI in StartAnimation()
    System.Threading.Thread t1 = new System.Threading.Thread(StartAnimation);
    t1.Start();
}
    
private void StartAnimation()
{
    // Use a stop watch
    if (st == null)
    {
        st = new Stopwatch();
        st.Start();
    }

    long prevTicks = 0;
    // a bool field to control the start and stop of the animation is better practice the While(true)
    while (IsAnimationActive)
    {
        double interval = 100000;
        long ticks = st.ElapsedTicks;

        if (ticks >= prevTicks + interval)
        {
            prevTicks = ticks;
            // Execture animation using invoke to prevent cross threading exception while updating the gui 
            pictureBox1.Invoke(new MethodInvoker(() =>
            {
                pictureBox1.Refresh();
            }));
        }
    }

    st.Stop();
}

private int index;
private int Height = 5;
// Use the Paint event of the control and not CreateGraphics()
// note: picture box is a control that is most suitable for animations and graphics
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
    if (index >= pictureBox1.Width)
        index = 0;

    index++;
    Graphics g = e.Graphics;
    // set HighQuality  fot the SmoothingMode property
    g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
    g.FillRectangle(Brushes.Black, 0, 0, index, Height);
}
Jonathan Applebaum
  • 5,738
  • 4
  • 33
  • 52
  • _// It is better to declare the brish locally_ Why? And why do you leak it? – TaW Jul 02 '20 at 18:05
  • @TaW You are right, I meant something else and forgot to change - please see edits (passing the static built-in black brush to `FillRectangle()`). – Jonathan Applebaum Jul 02 '20 at 18:15
  • Thank you for your advice. However, most if not all of the things in my original code (not all of it is shown) do the things shown here. Also, the line isn't supposed to be moving, the rect is supposed to stay at X = 0 and have the width expand. – VS-ux Jul 02 '20 at 18:23
  • But the second parameter of this overload is X point of the rectangle and you are passing and advancing `index`. anyway, pay attention to this line `g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;`also I have moved the index to the width parameter – Jonathan Applebaum Jul 02 '20 at 18:29
  • Yes. However, in the original code I'm using `CreateGraphics`, and since I do not invalidate, the drawing stays as is. Index advances, and I paint 1 pixel at a time for a smooth effect. The only problem is that I cannot draw fast enough with a transparent brush. – VS-ux Jul 02 '20 at 18:31
  • consider adding .gif file for a better understanding of what you are trying to do. – Jonathan Applebaum Jul 02 '20 at 18:34
  • Please note: The `Graphics` object does not __contain__ any graphics; it is a **tool** that lets you draw onto a related bitmap, including a control's surface. The system needs to draw all the controls' surfaces at times you can't control; therefore all you want to add to those surfaces must be created from the one event that the system will call, which is the `Paint` event. Only __non-persistent__ graphics operation like displaying a dynamic rubber-band rectangle are ok with a `Graphics` object you get from `control.CreateGraphics()`. And measurements without drawing... – TaW Jul 02 '20 at 18:39
  • 1
    Never use `control.CreateGraphics`! Never try to cache a `Graphics` object! Either draw into a `Bitmap bmp` using a `Graphics g = Graphics.FromImage(bmp)` or in the `Paint` event of a control, using the `e.Graphics` parameter.. – TaW Jul 02 '20 at 18:39
  • _since I do not invalidate_ You have no control over when the system will invalidate you application. – TaW Jul 02 '20 at 18:40
0

If you want it to go as fast as possible, the pop that code into a loop wrapped in an async/await method?

private void button1_Click(object sender, EventArgs e)
{
    button1.Enabled = false;
    SweepRight(Color.Black);
    button1.Enabled = true;
}

private async void SweepRight(Color c)
{
    using (Graphics g = CreateGraphics())
    {
        using (SolidBrush brush = new SolidBrush(Color.FromArgb(120, c)))
        {
            await Task.Run(() =>
            {
                for (int i = 0; i <= Width; i++)
                {
                    g.FillRectangle(brush, i, 0, 1, Height);
                }
            });
        }
    }
}
Idle_Mind
  • 38,363
  • 3
  • 29
  • 40
  • This works like the original code, but it's still limited by the graphics filling speed. When there is no transparency, it works great, but with it, it's slowed down dramatically. – VS-ux Jul 02 '20 at 19:00
  • Yeah, I think you'll have to try a completely different approach then. – Idle_Mind Jul 02 '20 at 19:12