views:

339

answers:

2

I have a WinForms control on which I want to display two things:

  1. An underlying image painstakingly loaded bit-by-bit from an external input device; and
  2. A series of DrawLine calls that create a visual pattern over that image.

Thing #1, for our purposes, doesn't change, and I'd rather not have to redraw it.

Thing #2 has to be redrawn relatively quickly, as it rotates when the user turns another control.

In my fantasy, I want to put each Thing in its own Graphics object, give #2 a transparent background, and simply hit #2 with a rotational transformation to match the user control setting. But I don't see a way to make a Graphics object transparent, nor a way to rotate what's already been drawn on one. So I'm probably asking Graphics to do something it wasn't designed for.

Here's my question: What's the best way to set this up? Should I attempt to overlap my Graphics objects, or is there some completely different and better way to do this that I'm not thinking of?

+1  A: 

GDI+ isn't retained-mode, so you'll need to redraw the entire control on every Paint. So unfortunately you can't just have two "things" and apply a rotation to one of them. Your best bet with GDI+ is probably:

  • Store #1 in an Image object. (You can pre-render elements to an Image by using Graphics.FromImage, or by drawing on a pre-constructed Bitmap.)
  • Store #2 as a set of coordinate pairs.

Then in the Paint handler redraw #1 quickly using Graphics.DrawImage, set a rotate transform using Graphics.RotateTransform, and draw your lines. You should be able to make this appear smooth using double-buffering (ControlStyles.DoubleBuffer).

As for "completely different" ways to do this, well, the "fantasy" you're describing is called Windows Presentation Foundation. WPF does have a retained-mode graphics system and might be able to handle the "rotate one layer while keeping the other constant" more conveniently. And you can host WPF in WinForms using the ElementHost control. The rough idea would be to use a Grid to overlay a Canvas on an Image, add Line objects to the Canvas, set the Canvas' RenderTransform to a RotateTransform, and bind the RotateTransform's Angle to the other control's value. However this does raise project considerations (target platform, learning curve) and also technical ones (initial overhead of loading WPF DLLs, interop constraints).

itowlson
Both answers were awesome. I had to pick one. :-(
catfood
+1  A: 

The Windows painting model is a good match for your requirements. It separates drawing the background (OnPaintBackground) from the foreground (OnPaint). That however doesn't mean that you can only paint the background once and be done with it. Window surface invalidation invokes both. This is above all required to make anti-aliasing effects work properly, they can only look good against a known background color.

Punt this and draw the Image in the OnPaintBackground() override. You can let Control do this automatically for you by assigning the BackgroundImage property. You'll probably need to set the DoubleBuffer property to true to avoid the flicker you'll see when the background is drawn, temporarily wiping out the foreground pixels. Call Invalidate() if you need to update the foreground.

To be complete, your fantasy is in fact possible. You'd need to overlay the image with a toplevel layered window. That is easy to get with a Form whose TransparencyKey property is set. Here is a sample implementation:

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

class OverlayedPictureBox : PictureBox {
    private Form mOverlay;
    private bool mShown;
    public event PaintEventHandler PaintOverlay;
    public OverlayedPictureBox() {
        mOverlay = new Form();
        mOverlay.FormBorderStyle = FormBorderStyle.None;
        mOverlay.TransparencyKey = mOverlay.BackColor = Color.Magenta;
        mOverlay.ShowInTaskbar = false;
    }
    protected void OnPaintOverlay(PaintEventArgs e) {
        // NOTE: override this or implement the PaintOverlay event
        PaintEventHandler handler = PaintOverlay;
        if (handler != null) handler(this, e);
    }
    public void RefreshOverlay() {
        // NOTE: call this to force the overlay to be repainted
        mOverlay.Invalidate();
    }
    protected override void Dispose(bool disposing) {
        if (disposing) mOverlay.Dispose();
        base.Dispose(disposing);
    }
    protected override void OnVisibleChanged(EventArgs e) {
        if (!mShown && !this.DesignMode) {
            Control parent = this.Parent;
            while (!(parent is Form)) parent = parent.Parent;
            parent.LocationChanged += new EventHandler(parent_LocationChanged);
            mOverlay.Paint += new PaintEventHandler(mOverlay_Paint);
            mOverlay.Show(parent);
            mShown = true;
        }
        base.OnVisibleChanged(e);
    }
    protected override void OnLocationChanged(EventArgs e) {
        mOverlay.Location = this.PointToScreen(Point.Empty);
        base.OnLocationChanged(e);
    }
    protected override void OnSizeChanged(EventArgs e) {
        mOverlay.Size = this.Size;
        base.OnSizeChanged(e);
    }
    void parent_LocationChanged(object sender, EventArgs e) {
        mOverlay.Location = this.PointToScreen(Point.Empty);
    }
    private void mOverlay_Paint(object sender, PaintEventArgs e) {
        OnPaintOverlay(e);
    }
}

One interesting artifact: minimizing the form and restoring it again looks, erm, special.

Hans Passant