0

TLDR: I'm seeking the correct method for timing the rendering of a juxtaposed graphic for a particular control on a design surface so that the graphic always is painted ahead of the adornment glyphs when that control is selected.

This question concerns control designers for Winforms: When the user places a control on the design surface, I want to display a graphic above the client area of the control. I have succeeded to some extent doing that for a TableLayoutPanel (TLP) control by overriding its OnPaint event handler then using the e.Graphics object available to paint a peach-colored rectangle. Below is an image showing the results: a painted graphic that spans the width of the control and is 35 pixels high--remember, this is a designer instance of a control placed on a design surface (created with a BasicLoader):

enter image description here

However, within the designer, if I resize the control, the graphic always ends up below the resize glyph (the glyph that has the North/South and West/East arrows on it):

enter image description here

I've tried creating and maintaining various Boolean flags to suppress the OnPaint message under certain circumstances. For instance, I set a flag to indicate that the control was just resized (to see how I did that, see my recent question: BeginResize/EndResize Event for Control on WinForms Design Surface) in order to suppress the painting of the graphic, but that didn't work because an OnPaint event is inevitably raised after I've cleared a flag. I don't want saddle this question with details of all the flags and places I tried to use/set them but suffice it to say that I painstakingly spent hours experimenting--to no avail. I've concluded that there must be a better way.

How can I ensure that the glyphs remain on top when I paint my graphics?

Thank you!

Jazimov
  • 12,626
  • 9
  • 52
  • 59
  • My psychic debugger says that OnPaint() does not use e.Graphics to paint. – Hans Passant Jan 29 '20 at 12:58
  • Hi Hans, yes mine too. For the record, I'm using the Graphics object returned by the TLP's parent's CreateGraphics method--I am not using the e.Graphics available directly in TLP's overridden Paint method. The TLP graphic needs to know when the TLP is invalidated so that it can repaint itself too... I'm seeking a better way to time when this graphic is painted so that it fires before the adornments are displayed on a selected subclassed TLP control within the designer. – Jazimov Jan 29 '20 at 16:20
  • 1
    Don't do it. Add this.ResizeRedraw = true; to the constructor. – Hans Passant Jan 29 '20 at 17:06

1 Answers1

1

I can think of a few solutions, including the followings:

  • Using Padding of the TableLayoutPanel
  • Using Adorner and Glyph
  • Creating a custom panel, having header and editable content

I think the first solution will suit you well, however the other solutions also some points.

I can also think of a solution based on NativeWindow like what has been implemented in ErrorProvider, but It makes the post toooooo lengthy while the existing options are good enough. So I leave it to you if you like to pursue the idea.

Solution 1 - Using Padding of the TableLayoutPanel

This solution is for both design-time and run-time

TableLayoutPanel has a Padding property and its layout engine respects to the padding well. You can use the padding area to render whatever you want:

enter image description here

public class MyTLP : TableLayoutPanel
{
    public MyTLP()
    {
        Padding = new Padding(0, 30, 0, 0);
    }
    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);
        e.Graphics.FillRectangle(Brushes.Orange,
            new Rectangle(0, 0, ClientRectangle.Width, Padding.Top));
    }
}

Solution 2 - Using Adorner and Glyph

This solution is just for design-time

For design-time rendering, I'll handle it using Adorner and Glyph.

If I was creating my custom designer, all the code belong to the control designer, but since you don't want to create a new control designer for TableLayoutPanel, then the same way that I injected a new custom action in the action lists, here I'll get BehaviorService and I'll inject an adorner to the adorners of the control at design time and this will be the result:

enter image description here

The behavior is quite similar to the other glyphs, it will be resized automatically when the control resizes and you don't need to do anything specific to handle the resize at design time.

Please note: It's a design-time solution and the painting is just being done at designer. In case you need a run-time solution, you need a totally different solution.

Here is the code:

using System;
using System.ComponentModel.Design;
using System.Drawing;
using System.Windows.Forms;
using System.Windows.Forms.Design;
using System.Windows.Forms.Design.Behavior;
public class MyTLP : TableLayoutPanel
{
    private IDesignerHost designerHost;
    private BehaviorService behaviorService;
    protected override void OnHandleCreated(EventArgs e)
    {
        base.OnHandleCreated(e);
        if (DesignMode && Site != null)
        {
            designerHost = Site.GetService(typeof(IDesignerHost)) as IDesignerHost;
            behaviorService = designerHost?.GetService(typeof(BehaviorService))
                as BehaviorService;
            if (behaviorService != null)
            {
                var adorner = new Adorner();
                behaviorService.Adorners.Insert(0, adorner);
                adorner.Glyphs.Add(new MyTLPGlypg(behaviorService, this));
            }
        }
    }
}
class MyTLPGlypg : Glyph
{
    Control control;
    BehaviorService behaviorSvc;
    public MyTLPGlypg(BehaviorService behaviorSvc, Control control) :
        base(new MyBehavior())
    {
        this.behaviorSvc = behaviorSvc;
        this.control = control;
    }
    public override Rectangle Bounds
    {
        get
        {
            var edge = behaviorSvc.ControlToAdornerWindow(control);
            var h = 30;
            return new Rectangle(edge.X, edge.Y - h, control.Size.Width, h);
        }
    }
    public override Cursor GetHitTest(Point p)
    {
        //Uncomment if you want to attach a specific behavior
        //if (Bounds.Contains(p)) return Cursors.Hand;
        return null;
    }
    public override void Paint(PaintEventArgs pe)
    {
        pe.Graphics.FillRectangle(Brushes.Orange, Bounds);
    }
}
class MyBehavior : Behavior
{
    public override bool OnMouseUp(Glyph g, MouseButtons button)
    {
        //Do something and return true, meand eventhandled
        return true;
    }
}

Note:

To learn more about anchors, glyphs and behavior, take a look at the following links:

Solution 3 - Creating a custom panel, having header and editable content

You can create a custom panel having header and editable content. Then at design time disallow user from dropping content on the header part:

To do so, you need to create a new designer which enables the inner panel on design-time by calling EnableDesignMode method. Then for the inner panel, you need to create a designer which disables moving, resizing and removes some properties from designer.

I've posted a detailed answer here: UserControl with header and content - Allow dropping controls in content panel and Prevent dropping controls in header at design time

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • HI Reza, as always, profuse thanks. Comment for Solution 1: I had tried doing something similar but ran into a problem: The orange glyph started behaving strangely when I nested the control, which I need to be able to do. For example, if I placed it in another TLP's cell then the glyph spilled over into the cell above... Also, in your example, if you drag the control towards the top of the form, does the orange rectangle end up on top of the form's title bar as it did when I experimented with my own approach? – Jazimov Feb 01 '20 at 18:59
  • Command for Solution 2: Wow, this actually is an amazing idea--one I hadn't thought of!!! It might just be the brilliant addresses-all-concerns idea I needed because it offers a marquee that acts like an owner-drawn control even though I don't have access to its designer! I will experiment with this idea--wow! Thanks!!! – Jazimov Feb 01 '20 at 19:01
  • 1
    *the orange rectangle end up on top of the form's title bar* → It's an expected behavior, the same way that other glyphs appear on top. In general I shared solution 1 as an example to show how you can use glyphs at design-time. – Reza Aghaei Feb 01 '20 at 19:01
  • Exactly--and that was the dealbreaker for me. I love how the glyph paints properly but I don't like that it ends up looking like artifact if dragged over certain areas of its parent control. I know I could set up positioning rules to auto-move the control if the glyph were to end up where I don't want it to be, but that would just add to the control's rendering complexity, making it even more unmaintainable. I'm sorry that you expended effort on the glyph approach and that I did not make it clear that it would not work and the reasons why... I hope others can benefit from it, however. – Jazimov Feb 01 '20 at 19:08
  • I was completely unfamiliar with ErrorProvider but I see how it could help, thanks! What I had been working on is a way to paint the rectangle and then inspect which adorners had glyphs that intersected my graphic. Then I was going to create an inverse clipping region representing those overlapped areas so that when I repainted the graphic, the adorner glyphs would remain on top. But I think your solution with padding will work beautifully! – Jazimov Feb 01 '20 at 19:14
  • 1
    You can also take a look at my linked post, describing solution 3. – Reza Aghaei Feb 01 '20 at 20:41
  • 1
    Hi Reza, I had reviewed your linked post when I was doing research. I will leave a comment there... Thanks! – Jazimov Feb 01 '20 at 20:44