Desktop screen overlay - new form flicker issue

2019-05-04 22:07发布

问题:

As part of my open source DeskPins clone (two separate links, one for original, one for clone on GitHub), I need to allow user to interact with desktop. I figured the easiest way would be opening a new form and paint desktop contents on top of it. Then I should be able to easily set mouse cursor and give visual cues to user about windows that is currently in focus.

A more complicated alternative would be using p/invoke to SetSystemCursor and injecting custom code in other window's WM_PAINT event queue (and potentially other WinApi related work, for example, cursor cleaning would be an issue, if my program terminates abnormally). I'd prefer not to go this way.

The code I have below is working, the only issue is screen flicker. It got better after I set DoubleBuffered = true (instead of screen flicker it became parent form flicker), but still noticeable. So right now my form flickers every time an overlay form is opened.

Is there anything I can do to make it a smooth transition, i.e. as if a new window did not open? It's okay to have a "freeze" effect = any animation would be suspended.

public sealed partial class DesktopOverlayForm : Form
{
  public DesktopOverlayForm()
  {
    InitializeComponent();

    //make full screen
    //http://stackoverflow.com/a/2176683/897326
    Rectangle bounds = Screen.AllScreens
                      .Select(x => x.Bounds)
                      .Aggregate(Rectangle.Union);
    this.Bounds = bounds;

    //set desktop overlay image
    this.BackgroundImage = MakeScreenshot();
  }

  /// <remarks>
  ///  Based on this answer on StackOverflow:
  ///  http://stackoverflow.com/questions/1163761/capture-screenshot-of-active-window
  /// </remarks>
  private static Bitmap MakeScreenshot()
  {
    Rectangle bounds = Screen.GetBounds(Point.Empty);
    Bitmap image = new Bitmap(bounds.Width, bounds.Height);

    using (Graphics g = Graphics.FromImage(image))
    {
      g.CopyFromScreen(Point.Empty, Point.Empty, bounds.Size);
    }

    return image;
  }

  private void DesktopOverlayForm_KeyDown(object sender, KeyEventArgs e)
  {
    if (e.KeyCode == Keys.Escape)
    {
      this.Close();
    }
  }
}

回答1:

DoubleBuffered mode is needed for sure. The reason for parent form flicker is because the overlay form is activated (thus the parent form is deactivated and need to visually indicate that) before the overlay form is painted.

In order to resolve that, the overlay form needs to be shown without being activated, and then to be activated just after the first paint. The first is achieved by overriding a rarely known virtual protected property Form.ShowWithoutActivation, and the second by hooking into OnPaint method and a form level flag. Something like this

public sealed partial class DesktopOverlayForm : Form
{
    public DesktopOverlayForm()
    {
        // ...
        this.DoubleBuffered = true;
    }

    protected override bool ShowWithoutActivation { get { return true; } }

    bool activated;

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        if (!activated)
        {
            activated = true;
            BeginInvoke(new Action(Activate));
        }
    }
}


回答2:

I did this to my flickering Form in the past. For my case, double buffer didn't really work well.

//this.DoubleBuffered = true; //doesn't work
protected override CreateParams CreateParams { //Very important to cancel flickering effect!!
  get {
    CreateParams cp = base.CreateParams;
    cp.ExStyle |= 0x02000000;  // Turn on WS_EX_COMPOSITED
    //cp.Style &= ~0x02000000;  // Turn off WS_CLIPCHILDREN not a good idea when combined with above. Not tested alone
    return cp;
  }
}

The idea is to replace the CreateParams argument. Also see:

  • Winforms Double Buffering