3

I have a user control with custom painting. Constructor sets styles correctly, from what I can tell. Basic code:

public partial class LineChart2 : UserControl
{
    public LineChart2()
    {
        InitializeComponent();

        //Set control styles to eliminate flicker on redraw and to redraw on resize
        this.SetStyle(
            ControlStyles.ResizeRedraw |
            ControlStyles.UserPaint |
            ControlStyles.AllPaintingInWmPaint |
            ControlStyles.DoubleBuffer,
            true);

        SetDefaultValues();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
    // breakpoint set here for verification
        Paint~misc stuff(e.Graphics);

        base.OnPaint(e);
    }

    private void UpdateGraph()
    {
    // this is called when the data that the control depends on changes
        ~update stuff();

        this.Invalidate();
        //this.Refresh();
    }
}

The control is contained within a Panel on a standard WinForm.

I've tried both Invalidate and Refresh.

When using Invalidate(), the control will redraw properly as long as the form it is contained in has focus. Drawing is smooth. When I switch focus to another form, drawing ceases even though the events are still firing, and this.Invalidate() is still being called. The form is still fully visible on screen.

When using Refresh(), the control will redraw regardless of whether the form has focus, but the drawing constantly flickers, as if bypassing the double-buffering mechanism.

So how do I get the Invalidate message to properly invoke the OnPaint method regardless of focus?

Teoman Soygul
  • 25,584
  • 6
  • 69
  • 80
dsmith
  • 646
  • 7
  • 14

3 Answers3

2

You should not force the control do redraw (Update or Refresh) so often. The UI may get not responsive, others controls may not update, because you are giving all UI attention to the forced sync Refresh. The right way is to draw only when UI is ready to do it. For that you need a render loop. The ApplicationLoopDoWork will be fired every time the UI is ready to draw something. The period depends on the machine speed and what is being redrawn.

The class is based on this post on Tom Miller's Blog.

Here is the class that I use to control that. Make updates only on the ApplicationLoopDoWork call.

    using System;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.Windows.Forms;

    namespace Utilities.UI
    {
    /// <summary>
    /// WinFormsAppIdleHandler implements a WinForms Render Loop (max FPS possible).
    /// Reference: http://blogs.msdn.com/b/tmiller/archive/2005/05/05/415008.aspx
    /// </summary>
    public sealed class WinFormsAppIdleHandler
    {
        private readonly object _completedEventLock = new object();
        private event EventHandler _applicationLoopDoWork;

        //PRIVATE Constructor
        private WinFormsAppIdleHandler()
        {
            Enabled = false;
            SleepTime = 10;

        }

        /// <summary>
        /// Singleton from:
        /// http://csharpindepth.com/Articles/General/Singleton.aspx
        /// </summary>
        private static readonly Lazy<WinFormsAppIdleHandler> lazy = new Lazy<WinFormsAppIdleHandler>(() => new WinFormsAppIdleHandler());
        public static WinFormsAppIdleHandler Instance { get { return lazy.Value; } }

        private bool _enabled = false;

        /// <summary>
        /// Gets or sets if must fire ApplicationLoopDoWork event.
        /// </summary>
        public bool Enabled
        {
            get { return _enabled; }
            set {
                if (value)
                    Application.Idle += Application_Idle;
                else
                    Application.Idle -= Application_Idle;

                _enabled = value;
            }
        }

        /// <summary>
        /// Gets or sets the minimum time betwen ApplicationLoopDoWork fires.
        /// </summary>
        public int SleepTime { get; set; }

        /// <summary>
        /// Fires while the UI is free to work. Sleeps for "SleepTime" ms.
        /// </summary>
        public event EventHandler ApplicationLoopDoWork
        {
            //Reason of using locks:
            //http://stackoverflow.com/questions/1037811/c-thread-safe-events
            add
            {
                lock (_completedEventLock)
                    _applicationLoopDoWork += value;
            }

            remove
            {
                lock (_completedEventLock)
                    _applicationLoopDoWork -= value;
            }
        }

        /// <summary>
        ///Application idle loop.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Application_Idle(object sender, EventArgs e)
        {
            //Try to update interface
            while (Enabled && IsAppIdle())
            {
                OnApplicationIdleDoWork(EventArgs.Empty);
                //Give a break to the processor... :)
                //8 ms -> 125 Hz
                //10 ms -> 100 Hz
                Thread.Sleep(SleepTime);
            }
        }

        private void OnApplicationIdleDoWork(EventArgs e)
        {
            var handler = _applicationLoopDoWork;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// Gets if the app is idle.
        /// </summary>
        /// <returns></returns>
        public static bool IsAppIdle()
        {
            bool isIdle = false;
            try
            {
                Message msg;
                isIdle = !PeekMessage(out msg, IntPtr.Zero, 0, 0, 0);
            }
            catch (Exception e)
            {
                //Should never get here... I hope...
                MessageBox.Show("IsAppStillIdle() Exception. Message: " + e.Message);
            }
            return isIdle;
        }

        #region  Unmanaged Get PeekMessage
        // http://blogs.msdn.com/b/tmiller/archive/2005/05/05/415008.aspx
        [System.Security.SuppressUnmanagedCodeSecurity] // We won't use this maliciously
        [DllImport("User32.dll", CharSet = CharSet.Auto)]
        public static extern bool PeekMessage(out Message msg, IntPtr hWnd, uint messageFilterMin, uint messageFilterMax, uint flags);

        #endregion
    }
}
Pedro77
  • 5,176
  • 7
  • 61
  • 91
  • AT LAST!!! This is the answer to many, many SO questions & all the mysterious delays that occur with invalidate/update when you do all the heavy graphics composition on a different thread and just need the UI thread to handle your rendering. Just hook up your method that calls Invalidate as an event handler for WinFormsAppIdleHandler.Instance.ApplicationLoopDoWork, and screen refresh will be as fast as it can possibly be, without all the problems that come from other methods. This is essentially the technique used by several game development environments. Great!! – Craig.Feied Mar 13 '18 at 17:04
  • I'm glad I could help. I had a lot of trouble displaying live camera images and solved with this class. :) – Pedro77 Mar 13 '18 at 23:56
2

Documentation says:

Calling the Invalidate method does not force a synchronous paint; to force a synchronous paint, call the Update method after calling the Invalidate method.

Have you tried calling Update after Invalidate?

Jim Mischel
  • 131,090
  • 20
  • 188
  • 351
  • Calling Update after Invalidate makes it behave like calling Refresh - constant flicker. – dsmith May 27 '11 at 22:00
  • Using that also means that I can't trap things like mouse events, since (I assume) the synchronous call preempts them (the graph is being updated 10-20 times per second, and the mouse manipulation of the control just flat out does not work if using Update). – dsmith May 27 '11 at 22:16
  • Have found the problem with the flickering. It's because there were two calls to Invalidate per refresh, one to clear old data out and another to add new data in. Update() apparently forces both of those draws to occur rather than collapsing them into a single call as the normal event queue would. Adjusting so that there's only one Invalidate call fixed things, and mouse events are manageable (though not as smooth as I'd like). I'm still not happy with the event queue's non-handling, but it works well enough for now. Marking accepted. – dsmith Jun 07 '11 at 21:17
1

You also might try Invalidate(true) to trigger child controls to repaint as well.

Michael Kennedy
  • 3,202
  • 2
  • 25
  • 34
  • I've tried that. Had no effect. Also tried Invalidate(this.ClientRectangle) in case it was optimizing out some 0-sized rectangle or something, but it didn't work either. – dsmith May 27 '11 at 22:04
  • Is it possible you have another modal dialog shown at the time? This would prohibit the paint message from getting processed. You can always do the less than great Application.DoEvents() right after Invalidate to force it - but this is a little risky. – Michael Kennedy May 27 '11 at 22:11
  • There are multiple forms shown at the same time (part of an MDI app), but they're not modal. And yeah, Application.DoEvents() at that point completely locks up the program. – dsmith May 27 '11 at 22:42
  • Sorry, that's all I can think of without debugging it directly. – Michael Kennedy May 27 '11 at 22:44