46

I know this question had been asked more than a few times, but so far I haven't been able to find a good solution for it.

I've got a panel with other control on it.
I want to draw a line on it and on top of all the controls in the panel

I came across 3 types of solutions (non of them worked the way I wanted) :

  1. Get the desktop DC and Draw on the screen.
    This will draw on other applications if they overlap the form.

  2. Overriding the panel's "CreateParams":

=

protected override CreateParams CreateParams {  
  get {  
    CreateParams cp;  
    cp = base.CreateParams;  
    cp.Style &= ~0x04000000; //WS_CLIPSIBLINGS
    cp.Style &= ~0x02000000; //WS_CLIPCHILDREN
    return cp;  
  }  
}           

//NOTE I've also tried disabling WS_CLIPSIBLINGS

and then drawing the line OnPaint(). But... Since the panel's OnPaint is called before the OnPaint of the controls in it, the drawing of the controls inside simply paints on top of the line.
I've seen someone suggest using a message filter to listen to WM_PAINT mesages, and use a timer, but I don't think this solution is either "good practice" or effective.
What would you do ? Decide that the controls inside have finished drawing after X ms, and set the timer to X ms ?


This screen shot shows the panel with WS_CLIPSIBLINGS and WS_CLIPCHILDREN turned off.
The Blue line is painted at the Panel's OnPaint, and simply being painted on by the textboxes and label.
The Red line is painted on top only because it's not being painted from the panel's OnPaint (It's actually painted as a result of a Button being clicked)
alt text


3rd: Creating a transparent layer and drawing on top of that layer.
I've created a transparent control using:

protected override CreateParams CreateParams {  
  get {  
    CreateParams cp = base.CreateParams;  
    cp.ExStyle |= 0x00000020; //WS_EX_TRANSPARENT  
    return cp;  
  }  
}

The problem is still, putting the transparent control on top of the Panel and all its controls.
I've tried bringing it to the front using: "BringToFront()" , but it didn't seem to help.
I've put it in the Line control's OnPaint() handler.
Should I try putting it somewhere else ??
- This also creates issue with having another control on top of the panel. (catching the mouse clicks etc..)

Any help would be greatly appreciated!

**EDIT: The black line is a sample of what I was trying to do. (used windows paint to paint it)

alt text

Community
  • 1
  • 1
dtroy
  • 1,187
  • 3
  • 14
  • 21
  • I have to ask: why? (sorry, I hate when people ask that - I'm not saying this is a bad idea, but I'm curious) – MusiGenesis Nov 12 '08 at 02:54
  • Alright, I'm going to guess: you have one control that users drag around the form, and the line here is drawn between the dragging control and some other control? – MusiGenesis Nov 12 '08 at 02:56
  • I've added a photo to show what I am trying to do. I'm trying your label solution now. It is a hack, but might be enough for now. For the sake of the discussion, what if the line wasn't horizontal or vertical ?? any ideas? – dtroy Nov 12 '08 at 03:14
  • This article might help with this subject, but not sure if it's the solution: [http://www.codeproject.com/KB/GDI-plus/WindowGraphics.aspx](http://www.codeproject.com/KB/GDI-plus/WindowGraphics.aspx) I thought I'll add it in case it helps others. – dtroy Jul 08 '09 at 06:55
  • [Here](https://stackoverflow.com/questions/57397327/draw-line-on-a-component/57415540#57415540) is another solution.. – TaW Aug 08 '19 at 15:04

10 Answers10

21

Turns out this is a whole lot easier than I thought. Thanks for not accepting any of my other answers. Here is the two-step process for creating a Fline (floating line - sorry, it's late):

alt text

Step 1: Add a UserControl to your project and name it "Fline". Add the following to the using statements:

using System.Drawing.Drawing2D;

Step 2: Add the following to the Fline's Resize event:

int wfactor = 4; // half the line width, kinda
// create 6 points for path
Point[] pts = {
    new Point(0, 0), 
    new Point(wfactor, 0), 
    new Point(Width, Height - wfactor),
    new Point(Width, Height) ,
    new Point(Width - wfactor, Height),
    new Point(0, wfactor) };
// magic numbers! 
byte[] types = {
    0, // start point
    1, // line
    1, // line
    1, // line
    1, // line
    1 }; // line 
GraphicsPath path = new GraphicsPath(pts, types);
this.Region = new Region(path);

Compile, and then drag a Fline onto your form or panel. Important: the default BackColor is the same as the form, so change the Fline's BackColor to Red or something obvious (in the designer). One weird quirk about this is that when you drag it around in the designer it shows as a solid block until you release it - not a huge deal.

This control can appear in front of or behind any other control. If you set Enabled to false, it will still be visible but will not interfere with mouse events on the controls underneath.

You'll want to enhance this for your purposes, of course, but this shows the basic principle. You can use the same technique for creating a control of whatever shape you like (my initial test of this made a triangle).

Update: this makes a nice dense one-liner, too. Just put this in your UserControl's Resize event:

this.Region=new Region(new System.Drawing.Drawing2D.GraphicsPath(new Point[]{new Point(0,0),new Point(4,0),new Point(Width,Height-4),new Point(Width,Height),new Point(Width-4,Height),new Point(0,4)},new byte[]{0,1,1,1,1,1}));
Glorfindel
  • 21,988
  • 13
  • 81
  • 109
MusiGenesis
  • 74,184
  • 40
  • 190
  • 334
  • MusiGenesis, thanks for this answer. It helped a lot. But.. I was playing with it again today and I was trying to create a 1 pixel wide dashed line similar to the red line in your answer above. Since I couldn't have an open GraphicsPath, I was trying to make the control transparent – dtroy Apr 02 '09 at 04:55
  • cont... and then draw a line on it. But I couldn't find a way that worked with all the controls under it etc.. Have you got any ideas ?? THANKS! – dtroy Apr 02 '09 at 04:55
  • 7
    "Nice dense one-liners" are always a bad thing. – Glenn Maynard May 12 '14 at 00:44
10

If you want the line to be just a simple horizontal or vertical line, put another panel (disabled so it doesn't pick up any mouse events) on the main panel, set its height (or width) to 3 or 4 pixels (or whatever you want), and bring it to front. If you need to change where the line is during runtime, you can just move the panel around and make it visible and invisible. Here is how it looks:

alt text

You can even click anywhere you like, and the lines don't interfere at all. The line is drawn over any kind of control at all (although the dropdown part of a ComboBox or a DatePicker is still shown above the line, which is good anyway). The blue line is just the same thing but sent to back.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
MusiGenesis
  • 74,184
  • 40
  • 190
  • 334
  • It seems the Label has to be added to the panel first to be painted last. Playing with BringToFront() didn't seem to help much – dtroy Nov 18 '08 at 00:06
  • Instead of a label, you could use another panel (1 or 2 pixels wide). If you bring this to front, it will be in front of everything else on the panel no matter what. – MusiGenesis Nov 21 '08 at 00:03
  • Something I've noticed while playing with it: If the line is over a control that has "WS_CLIPSIBLINGS" disabled, the line won't be drawn on top of it. Otherwise, seems OK. But what will we do if we want a diagonal line ? – dtroy Nov 25 '08 at 00:40
  • BTW, why didn't my other answer work? (the one with the diagonal red line over the buttons) – MusiGenesis Nov 25 '08 at 01:50
  • It's not that it didn't work, it's just that I didn't want to hook to the control's event handler. I don't know how many controls there will be on the surface I am drawing on. What if we draw something more complicated than a line, like moving shapes etc.. You know what I mean? – dtroy Nov 25 '08 at 04:25
  • @dtroy: please see my new answer. I was halfway through a comment like "this is fundamentally impossible" when I decided to give it one more go. – MusiGenesis Nov 25 '08 at 06:52
  • Well... you just indirectly answered a question that'd been bugging me all afternoon: I was looking for some way, any way, of drawing a simple *label* on top of all the other controls inside a panel - without the label interfering with mouse input on the controls under it. Lacking your input, I thought I'd need to draw the text myself, which is how I ended up here. Nope, turns out I just needed to disable the label. Thanks! – neminem Jul 28 '11 at 21:36
7

Yes, this can be done. The problem is that the panel and the controls on it are all separate windows (in the API sense), and thus all separate drawing surfaces. There is no one drawing surface to draw on to get this effect (other than the top-level screen surface, and it's considered impolite to draw all over that).

The (cough-hack-cough) trick is to draw the line on the panel underneath the controls, and also draw it on each of the controls themselves, resulting in this (which will persist even when you click the buttons and move the mouse around):

alt text

Create a winforms project (which should come with Form1 by default). Add a panel (named "panel1") and two buttons ("button1" and "button2") on the panel as shown. Add this code in the form's constructor:

panel1.Paint += PaintPanelOrButton;
button1.Paint += PaintPanelOrButton;
button2.Paint += PaintPanelOrButton;

and then add this method to the form's code:

private void PaintPanelOrButton(object sender, PaintEventArgs e)
{
    // center the line endpoints on each button
    Point pt1 = new Point(button1.Left + (button1.Width / 2),
            button1.Top + (button1.Height / 2));
    Point pt2 = new Point(button2.Left + (button2.Width / 2),
            button2.Top + (button2.Height / 2));

    if (sender is Button)
    {
        // offset line so it's drawn over the button where
        // the line on the panel is drawn
        Button btn = (Button)sender;
        pt1.X -= btn.Left;
        pt1.Y -= btn.Top;
        pt2.X -= btn.Left;
        pt2.Y -= btn.Top;
    }

    e.Graphics.DrawLine(new Pen(Color.Red, 4.0F), pt1, pt2);
}

Something like this needs to be drawn in each control's Paint event in order for the line to persist. It's easy to draw directly on controls in .NET, but whatever you draw is wiped away when someone clicks the button or moves the mouse over it (unless it's perpetually redrawn in the Paint events, as here).

Note that for this to work, any control drawn over has to have a Paint event. I'm sure you will have to modify this sample to achieve what you need. If you come up with a good generalized function for this, please post it.

Update: this method will not work for scrollbars, textboxes, comboboxes, listviews, or basically anything with a textbox-type thing as part of it (and not because it only offsets for buttons in the example above - you just can't draw on top of a textbox at all, at least not from its Paint event, at least not if you're me). Hopefully that won't be a problem.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
MusiGenesis
  • 74,184
  • 40
  • 190
  • 334
5

A windows forms panel is a container for controls. If you want to draw something on top of other controls within a panel, then what you need is another control ( at the top of the z order ).

Luckily, you can create windows forms controls which have non-rectangular borders. Look at this technique: http://msdn.microsoft.com/en-us/library/aa289517(VS.71).aspx

To just draw something on the screen, use a label control, and turn AutoSize off. Then attach to the Paint event and set the Size and Region Properties.

Here's a code sample:

private void label1_Paint(object sender, PaintEventArgs e)
{
    System.Drawing.Drawing2D.GraphicsPath myGraphicsPath = new  System.Drawing.Drawing2D.GraphicsPath();
    myGraphicsPath.AddEllipse(new Rectangle(0, 0, 125, 125));
    myGraphicsPath.AddEllipse(new Rectangle(75, 75, 20, 20));
    myGraphicsPath.AddEllipse(new Rectangle(120, 0, 125, 125));
    myGraphicsPath.AddEllipse(new Rectangle(145, 75, 20, 20));
    //Change the button's background color so that it is easy
    //to see.
    label1.BackColor = Color.ForestGreen;
    label1.Size = new System.Drawing.Size(256, 256);
    label1.Region = new Region(myGraphicsPath);
}
Matt Brunell
  • 10,141
  • 3
  • 34
  • 46
4

The only simple solution I can think of is to create Paint event handlers for each control you want to paint on top of. Then coordinate the line drawing between these handlers. This is not the most convenient solution, however this will give you the ability to paint on top of the controls.

Assuming button is a child control of panel:

panel.Paint += new PaintEventHandler(panel_Paint);
button.Paint += new PaintEventHandler(button_Paint);

protected void panel_Paint(object sender, PaintEventArgs e)
{
    //draw the full line which will then be partially obscured by child controls
}

protected void button_Paint(object sender, PaintEventArgs e)
{
    //draw the obscured line portions on the button
}
asponge
  • 636
  • 5
  • 8
4

Make a new LineControl : Control like this:

then call BringToFront() after the InitializeComponent

public partial class MainForm : Form
    {
        public MainForm()
        {
            InitializeComponent();
            this.simpleLine1.BringToFront();
        }
    }



using System;
using System.Windows.Forms;
using System.Drawing;
using System.Collections.Generic;

public class SimpleLine : Control
{
    private Control parentHooked;   
    private List<Control> controlsHooked;

    public enum LineType
    {
        Horizontal,
        Vertical,
        ForwardsDiagonal,
        BackwardsDiagonal
    }

    public event EventHandler AppearanceChanged;
    private LineType appearance;
    public virtual LineType Appearance
    {
        get
        {
            return appearance;
        }
        set
        {
            if (appearance != value)
            {
                this.SuspendLayout();
                switch (appearance)
                {
                    case LineType.Horizontal:
                        if (value == LineType.Vertical)
                        {
                            this.Height = this.Width;
                        }

                        break;
                    case LineType.Vertical:
                        if (value == LineType.Horizontal)
                        {
                            this.Width = this.Height;
                        }
                        break;
                }
                this.ResumeLayout(false);

                appearance = value;
                this.PerformLayout();
                this.Invalidate();
            }
        }
    }
    protected virtual void OnAppearanceChanged(EventArgs e)
    {
        if (AppearanceChanged != null) AppearanceChanged(this, e);
    }

    public event EventHandler LineColorChanged;
    private Color lineColor;
    public virtual Color LineColor
    {
        get
        {
            return lineColor;
        }
        set
        {
            if (lineColor != value)
            {
                lineColor = value;
                this.Invalidate();
            }
        }
    }
    protected virtual void OnLineColorChanged(EventArgs e)
    {
        if (LineColorChanged != null) LineColorChanged(this, e);
    }

    public event EventHandler LineWidthChanged;
    private float lineWidth;
    public virtual float LineWidth
    {
        get
        {
            return lineWidth;
        }
        set
        {
            if (lineWidth != value)
            {
                if (0 >= value)
                {
                    lineWidth = 1;
                }
                lineWidth = value;
                this.PerformLayout();
            }
        }
    }
    protected virtual void OnLineWidthChanged(EventArgs e)
    {
        if (LineWidthChanged != null) LineWidthChanged(this, e);
    }

    public SimpleLine()
    {
        base.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.Selectable, false);
        base.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
        base.BackColor = Color.Transparent;

        InitializeComponent();

        appearance = LineType.Vertical;
        LineColor = Color.Black;
        LineWidth = 1;
        controlsHooked = new List<Control>();

        this.ParentChanged += new EventHandler(OnSimpleLineParentChanged);
    }

    private void RemoveControl(Control control)
    {
        if (controlsHooked.Contains(control))
        {
            control.Paint -= new PaintEventHandler(OnControlPaint);
            if (control is TextboxX)
            {
                TextboxX text = (TextboxX)control;
                text.DoingAPaint -= new EventHandler(text_DoingAPaint);
            }
            controlsHooked.Remove(control);
        }
    }

    void text_DoingAPaint(object sender, EventArgs e)
    {
        this.Invalidate();
    }

    private void AddControl(Control control)
    {
        if (!controlsHooked.Contains(control))
        {
            control.Paint += new PaintEventHandler(OnControlPaint);
            if (control is TextboxX)
            {
                TextboxX text = (TextboxX)control;
                text.DoingAPaint += new EventHandler(text_DoingAPaint);
            }
            controlsHooked.Add(control);
        }
    }

    private void OnSimpleLineParentChanged(object sender, EventArgs e)
    {
        UnhookParent();

        if (Parent != null)
        {

            foreach (Control c in Parent.Controls)
            {
                AddControl(c);
            }
            Parent.ControlAdded += new ControlEventHandler(OnParentControlAdded);
            Parent.ControlRemoved += new ControlEventHandler(OnParentControlRemoved);
            parentHooked = this.Parent;
        }
    }

    private void UnhookParent()
    {
            if (parentHooked != null)
            {
                foreach (Control c in parentHooked.Controls)
                {
                    RemoveControl(c);
                }
                parentHooked.ControlAdded -= new ControlEventHandler(OnParentControlAdded);
                parentHooked.ControlRemoved -= new ControlEventHandler(OnParentControlRemoved);
                parentHooked = null;
            }
    }

    private void OnParentControlRemoved(object sender, ControlEventArgs e)
    {
        RemoveControl(e.Control);
    }   

    private void OnControlPaint(object sender, PaintEventArgs e)
    {
        int indexa =Parent.Controls.IndexOf(this) , indexb = Parent.Controls.IndexOf((Control)sender);
        //if above invalidate on paint
        if(indexa < indexb)
        {
            Invalidate();
        }
    }

    private void OnParentControlAdded(object sender, ControlEventArgs e)
    {
        AddControl(e.Control);
    }

    private System.ComponentModel.IContainer components = null;
    private void InitializeComponent()
    {
        components = new System.ComponentModel.Container();
    }
    protected override void Dispose(bool disposing)
    {
        if (disposing && (components != null))
        {
            components.Dispose();
        }
        base.Dispose(disposing);
    }

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp = base.CreateParams;
            cp.ExStyle |= 0x20;  // Turn on WS_EX_TRANSPARENT
            return cp;
        }
    }

    protected override void OnLayout(LayoutEventArgs levent)
    {
        switch (this.Appearance)
        {
            case LineType.Horizontal:
                this.Height = (int)LineWidth;
                this.Invalidate();
                break;
            case LineType.Vertical:
                this.Width = (int)LineWidth;
                this.Invalidate();
                break;
        }

        base.OnLayout(levent);
    }

    protected override void OnPaintBackground(PaintEventArgs pevent)
    {
        //disable background paint
    }

    protected override void OnPaint(PaintEventArgs pe)
    {
        switch (Appearance)
        {
            case LineType.Horizontal:
                DrawHorizontalLine(pe);
                break;
            case LineType.Vertical:
                DrawVerticalLine(pe);
                break;
            case LineType.ForwardsDiagonal:
                DrawFDiagonalLine(pe);
                break;
            case LineType.BackwardsDiagonal:
                DrawBDiagonalLine(pe);
                break;
        }
    }

    private void DrawFDiagonalLine(PaintEventArgs pe)
    {
        using (Pen p = new Pen(this.LineColor, this.LineWidth))
        {
            pe.Graphics.DrawLine(p, this.ClientRectangle.X, this.ClientRectangle.Bottom,
                                    this.ClientRectangle.Right, this.ClientRectangle.Y);
        }
    }

    private void DrawBDiagonalLine(PaintEventArgs pe)
    {
        using (Pen p = new Pen(this.LineColor, this.LineWidth))
        {
            pe.Graphics.DrawLine(p, this.ClientRectangle.X, this.ClientRectangle.Y,
                                    this.ClientRectangle.Right, this.ClientRectangle.Bottom);
        }
    }

    private void DrawHorizontalLine(PaintEventArgs pe)
    {
        int  y = this.ClientRectangle.Height / 2;
        using (Pen p = new Pen(this.LineColor, this.LineWidth))
        {
            pe.Graphics.DrawLine(p, this.ClientRectangle.X, y,
                                    this.ClientRectangle.Width, y);
        }
    }

    private void DrawVerticalLine(PaintEventArgs pe)
    {
        int x = this.ClientRectangle.Width / 2;
        using (Pen p = new Pen(this.LineColor, this.LineWidth))
        {
            pe.Graphics.DrawLine(p,x, this.ClientRectangle.Y,
                                   x, this.ClientRectangle.Height);
        }
    }
}

Edit: Added diagonal support

I've added some support for controls that repaint when they get the focus.

textboxes and comboboxs wont work as is you will need to make your own and hook there paintish commands like so:

public class TextboxX : TextBox
{
    public event EventHandler DoingAPaint;
    protected override void WndProc(ref Message m)
    {
        switch ((int)m.Msg)
        {
            case (int)NativeMethods.WindowMessages.WM_PAINT:
            case (int)NativeMethods.WindowMessages.WM_ERASEBKGND:
            case (int)NativeMethods.WindowMessages.WM_NCPAINT:
            case 8465: //not sure what this is WM_COMMAND?
                if(DoingAPaint!=null)DoingAPaint(this,EventArgs.Empty);
                break;
        }           
        base.WndProc(ref m);
    }
}

Its not tested and i'm sure you can improve on it

Hath
  • 12,606
  • 7
  • 36
  • 38
  • Hath, This could have been a nice solution, but.. look (see the image below) what happens when you hover to mouse under a button under the line , or a control under the line gets the focus. http://i73.photobucket.com/albums/i201/sdjc1/temp/screen4.png – dtroy Nov 18 '08 at 23:19
  • @dtroy - i've added code that will invalidate when another control on in the parents.controls collection paints.. it won't work as well for textbox or comboboxs but you can see what i've done to cover them. not sure if that will do for your needs but you could make it work for what your discribing. – Hath Nov 19 '08 at 11:31
  • Just a quick comment about the code (haven't had the chance to run it yet): 8465=0x02111 which is OCM_COMMAND. is that what you're trying to get ? Also, I've noticed that in "OnSimpleLineParentChanged" you might want to detach the event from parentHooked when (Parent!=null)&&(parentHooked!=null) 10x – dtroy Nov 19 '08 at 23:30
  • @dtroy - added the detach for parentHooked. Just noticed this has a few problems if you do a diagonal.. say if you want to select a control thats been covered by the line you can't because the line control is covering it.. i suppose you could relay the mouse messages somehow but i dunno..bit dirty. – Hath Nov 20 '08 at 10:10
  • This might be the most code ever written for drawing a single line. :) – MusiGenesis Nov 21 '08 at 00:01
3

How about this take on solution #1 (Get the desktop DC and Draw on the screen):

  1. Get the desktop DC and the graphics object for that DC [Graphics.fromHDC(...)]
  2. Set the Clip property of the resulting Graphics object to be the currently visible region of your form. (I have not researched yet how to find the visible region of a form)
  3. Do your graphics rendering.
Ski
  • 1,182
  • 1
  • 9
  • 20
2

EDIT Found a way to get rid of the recursive painting issue I had. So, now, to me, this looks very, very, very close to what you want to achieve.

Here's what I could come up with. It uses approach #3 outlined in the original question. The code is somewhat lengthy because three classes are involved:

  1. A private class called DecorationCanvas. This derives from Panel and uses WS_EX_TRANSPARENT to provide a transparent canvas to draw our stuff on
  2. The panel class itself, I called it DecoratedPanel, it derives from Panel
  3. A designer class called DecoratedPanelDesigner for the panel, to make sure that the ZOrder can be preserved during Design time.

The basic approach is:

  • In the constructor of the DecoratedPanel, create an instance of DecorationCanvas and add it to the DecoratedPanel Controls collection.
  • Override OnControlAdded and OnControlRemoved, to automatically hook/unhook paint events for child controls, and to make sure the DecorationCanvas stays on top of the ZOrder.
  • Whenever a contained control paints, invalidate the corresponding DecorationCanvas rectangle.
  • Override OnResize and OnSizeChanged to make sure the DecorationCanvas has the same size as the DecoratedPanel. (I tried to accomplish this using the Anchor property, but it failed somehow).
  • Provide an internal method to reset the DecorationCanvas ZOrder from within the DecoratedPanelDesigner.

Works fine on my system (VS2010 / .net4 / Windows XP SP3). Here's the code:

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace WindowsFormsApplication3
{
  [Designer("WindowsFormsApplication3.DecoratedPanelDesigner")]
  public class DecoratedPanel : Panel
  {
    #region decorationcanvas

    // this is an internal transparent panel.
    // This is our canvas we'll draw the lines on ...
    private class DecorationCanvas : Panel
    {
      public DecorationCanvas()
      {
        // don't paint the background
        SetStyle(ControlStyles.Opaque, true);
      }

      protected override CreateParams CreateParams
      {
        get
        {
          // use transparency
          CreateParams cp = base.CreateParams;
          cp.ExStyle |= 0x00000020; //WS_EX_TRANSPARENT
          return cp;
        }
      }
    }

    #endregion

    private DecorationCanvas _decorationCanvas;

    public DecoratedPanel()
    {
      // add our DecorationCanvas to our panel control
      _decorationCanvas = new DecorationCanvas();
      _decorationCanvas.Name = "myInternalOverlayPanel";
      _decorationCanvas.Size = ClientSize;
      _decorationCanvas.Location = new Point(0, 0);
      // this prevents the DecorationCanvas to catch clicks and the like
      _decorationCanvas.Enabled = false;
      _decorationCanvas.Paint += new PaintEventHandler(decoration_Paint);
      Controls.Add(_decorationCanvas);
    }

    protected override void Dispose(bool disposing)
    {
      if (disposing && _decorationCanvas != null)
      {
        // be a good citizen and clean up after yourself

        _decorationCanvas.Paint -= new PaintEventHandler(decoration_Paint);
        Controls.Remove(_decorationCanvas);
        _decorationCanvas = null;
      }

      base.Dispose(disposing);
    }

    void decoration_Paint(object sender, PaintEventArgs e)
    {
      // --- PAINT HERE ---
      e.Graphics.DrawLine(Pens.Red, 0, 0, ClientSize.Width, ClientSize.Height);
    }

    protected override void OnControlAdded(ControlEventArgs e)
    {
      base.OnControlAdded(e);

      if (IsInDesignMode)
        return;

      // Hook paint event and make sure we stay on top
      if (!_decorationCanvas.Equals(e.Control))
        e.Control.Paint += new PaintEventHandler(containedControl_Paint);

      ResetDecorationZOrder();
    }

    protected override void OnControlRemoved(ControlEventArgs e)
    {
      base.OnControlRemoved(e);

      if (IsInDesignMode)
        return;

      // Unhook paint event
      if (!_decorationCanvas.Equals(e.Control))
        e.Control.Paint -= new PaintEventHandler(containedControl_Paint);
    }

    /// <summary>
    /// If contained controls are updated, invalidate the corresponding DecorationCanvas area
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void containedControl_Paint(object sender, PaintEventArgs e)
    {
      Control c = sender as Control;

      if (c == null)
        return;

      _decorationCanvas.Invalidate(new Rectangle(c.Left, c.Top, c.Width, c.Height));
    }

    protected override void OnResize(EventArgs eventargs)
    {
      base.OnResize(eventargs);
      // make sure we're covering the panel control
      _decorationCanvas.Size = ClientSize;
    }

    protected override void OnSizeChanged(EventArgs e)
    {
      base.OnSizeChanged(e);
      // make sure we're covering the panel control
      _decorationCanvas.Size = ClientSize;
    }

    /// <summary>
    /// This is marked internal because it gets called from the designer
    /// to make sure our DecorationCanvas stays on top of the ZOrder.
    /// </summary>
    internal void ResetDecorationZOrder()
    {
      if (Controls.GetChildIndex(_decorationCanvas) != 0)
        Controls.SetChildIndex(_decorationCanvas, 0);
    }

    private bool IsInDesignMode
    {
      get
      {
        return DesignMode || LicenseManager.UsageMode == LicenseUsageMode.Designtime;
      }
    }
  }

  /// <summary>
  /// Unfortunately, the default designer of the standard panel is not a public class
  /// So we'll have to build a new designer out of another one. Since Panel inherits from
  /// ScrollableControl, let's try a ScrollableControlDesigner ...
  /// </summary>
  public class DecoratedPanelDesigner : ScrollableControlDesigner
  {
    private IComponentChangeService _changeService;

    public override void Initialize(IComponent component)
    {
      base.Initialize(component);

      // Acquire a reference to IComponentChangeService.
      this._changeService = GetService(typeof(IComponentChangeService)) as IComponentChangeService;

      // Hook the IComponentChangeService event
      if (this._changeService != null)
        this._changeService.ComponentChanged += new ComponentChangedEventHandler(_changeService_ComponentChanged);
    }

    /// <summary>
    /// Try and handle ZOrder changes at design time
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void _changeService_ComponentChanged(object sender, ComponentChangedEventArgs e)
    {
      Control changedControl = e.Component as Control;
      if (changedControl == null)
        return;

      DecoratedPanel panelPaint = Control as DecoratedPanel;

      if (panelPaint == null)
        return;

      // if the ZOrder of controls contained within our panel changes, the
      // changed control is our control
      if (Control.Equals(panelPaint))
        panelPaint.ResetDecorationZOrder();
    }

    protected override void Dispose(bool disposing)
    {
      if (disposing)
      {
        if (this._changeService != null)
        {
          // Unhook the event handler
          this._changeService.ComponentChanged -= new ComponentChangedEventHandler(_changeService_ComponentChanged);
          this._changeService = null;
        }
      }

      base.Dispose(disposing);
    }

    /// <summary>
    /// If the panel has BorderStyle.None, a dashed border needs to be drawn around it
    /// </summary>
    /// <param name="pe"></param>
    protected override void OnPaintAdornments(PaintEventArgs pe)
    {
      base.OnPaintAdornments(pe);

      Panel panel = Control as Panel;
      if (panel == null)
        return;

      if (panel.BorderStyle == BorderStyle.None)
      {
        using (Pen p = new Pen(SystemColors.ControlDark))
        {
          p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash;
          pe.Graphics.DrawRectangle(p, 0, 0, Control.Width - 1, Control.Height - 1);
        }
      }
    }
  }
}

Let me know what you think ...

takrl
  • 6,356
  • 3
  • 60
  • 69
1

I think the best way is to inherit the control of which you want to draw a line on. Override the OnPaint method, call base.Paint() from within, after that draw the line using the same graphic instance. At the same time, you can also have a parameter which specific at which point the line should be draw, so that you can control the line directly from your main form.

faulty
  • 8,117
  • 12
  • 44
  • 61
  • Done that. No good. As I mentioned in the question, The controls on top of it paint after the panel's "OnPaint()" has returned. – dtroy Nov 12 '08 at 23:24
  • You're overriding OnPaint of panel, but I'm referring to the OnPaint of the control itself (the one displaying the "wave"). If that still doesn't work out, then what control was it that you use to display the "wave"? – faulty Nov 13 '08 at 08:03
  • The control in the picture is inherited from UserControl, but the same goes for a Label, TextBox etc.. The problem is.. there are 8 controls in the picuture, 4 of mine + 4 Splitter. Are you suggesting I'll draw a fragment of the line on each of them ? – dtroy Nov 17 '08 at 23:42
  • what if there are other controls (different types) on the panel ? Override all of their OnPaint()s ? While I know this is a possibility, I was hoping for a better solution. BTW, thanks for your help – dtroy Nov 17 '08 at 23:43
  • Sorry for the late reply. Yes I was referring to drawing fragment for each of them, and add a properly to each of them so you can easily set when you want the line to be from your usercontrol. Like: ControlA.LinePosition = new Point(100, 0); ControlB.LinePosition = new Point(100, 0); – faulty Nov 21 '08 at 07:53
1

Original code should be :

    protected override CreateParams CreateParams
    {
        get
        {
            CreateParams cp;
            cp = base.CreateParams;
            cp.Style &= 0x7DFFFFFF; //WS_CLIPCHILDREN
            return cp;
        }
    }

This works !!

  • John, thanks for your reply. You are right about the mistake in the question and I've fixed it. This WILL let you draw on top of other controls, but since the controls within the panel are drawn after the Panel's OnPaint, they will simply be drawn on top of what you've drawn. – dtroy Nov 17 '08 at 23:35