Search code examples
c#winformsgraphicsgdi+gdi

Custom draw dropdown panel outside Control bounds


I seem to have picked up an "unclear what I'm asking" vote. I want to custom draw a combo box style control. The pop open section needs to draw outside the bounds of the control itself. I cannot use combo box - think something similar to the gallery control in the Word ribbon.

I have thought of 2 approaches:

  • Passing the pop open panel back up to the form to render.
  • Using a borderless, frameless Form or NativeWindow.

The latter also allows the drop down to escape the bounds of the window, which may be of use, but is not absolutely essential.

Are there any other approaches to this, and which do you think is best?

Thanks.


ORIGINAL QUESTION:

I'm writing a winforms based custom draw UI library for a small project. All is largely going well, but I have a slight structural issue with drop downs leaving the bounds of the Graphics object.

Most of the controls are being done with pure custom draw and redraw event model but the overall interface is being laid out using winforms Dock, Width, Height etc.

I've added a dropdown, but obviously when the bounds of the drop down section of it exceed the bounds of the graphics object for the layout Panel, it is cut off.

(I had expected to find something similar to this on SO already, but haven't managed.)

I've worked around this by having the form control the drawing of the drop down overlay, but what with the custom mouse handler and everything else, the form is starting to feel rather overburdened.

I have tried to store references to Graphics objects but found that using them outside of OnPaint being raised is.. temperamental.

Simplified code example of current model follows. This code doesn't run in any useful way alone, but shows the approach used to display the overlays.

public interface IDropDownOverlay
{
    DropDown DropDown { get; }

    /// <summary>can only link to a single form at once - not a problem.</summary>
    DropDownDrawForm Form { get; set; }

    void MouseUpdate(MouseEventArgs e);

    void Render(Graphics gfx);

    void Show();
}

public class DropDown
{
    private DropDownOverlay overlay;
}

public class DropDownDrawForm : Form
{
    /* lots of other things... */

    private List<IDropDownOverlay> overlays;

    public void HideOverlay(IDropDownOverlay overlay)
    {
        if (this.overlays.Contains(overlay))
        {
            this.overlays.Remove(overlay);
            this.Invalidate();
        }
    }

    public void ShowOverlay(IDropDownOverlay overlay)
    {
        if (!this.overlays.Contains(overlay))
        {
            overlay.Form = this;
            this.overlays.Add(overlay);
            this.Invalidate();
        }
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        base.OnPaint(e);

        foreach (IDropDownOverlay overlay in this.overlays)
        {
            overlay.Render(e.Graphics);
        }
    }

    private void MouseUpdate(MouseEventArgs e)
    {
        foreach (IDropDownOverlay overlay in this.overlays)
        {
            overlay.MouseUpdate(e);
        }
    }
}

public class DropDownOverlay : IDropDownOverlay
{
    public DropDown DropDown { get; }

    public DropDownDrawForm Form { get; set; }

    public void Hide()
    {
        this.Form.HideOverlay(this);
    }

    public void MouseUpdate(MouseEventArgs e)
    {
        // Informs the form to redraw the area of the control.

        if (stateChanged)
        {
            this.Invalidate(); // (method to invalidate just this area)
        }
    }

    public void Show()
    {
        this.Form.ShowOverlay(this);
    }

    public void Render(Graphics gfx)
    {
    }
}

Obviously there are lots of bits missing from this, but it should show the approach I'm using at least.

Any suggestions to prevent me having to pass this back and forth between the form?

Thanks


UPDATE:

Just to be absolutely clear, the problem is drawing the "popup" section of the dropdown not the dropdown itself. (ComboBox used here to demonstrate)

DropDown Control Areas

I've also since remembered that a small window forces a ComboBox outside the bounds of the window.

ComboBox outside window bounds

The drop shadow on it looks suspiciously like CS_DROPSHADOW from CreateParams to me - could I use a NativeWindow subclass to handle this instead?


Solution

  • I think I've settled on the second of the options, which is using a second Form to display the dropdown panel. I've used an extended Form class rather than NativeWindow. Just thought I should share the results in case anyone else is attempting the same thing and finds this.

    When the dropdown is selected, I set up the form using PointToScreen to get the coordinates. It also has the following properties set:

                this.ShowIcon = false;
                this.ControlBox = false;
                this.MinimizeBox = false;
                this.MaximizeBox = false;
                this.ShowInTaskbar = false;
                this.FormBorderStyle = FormBorderStyle.None;
    

    Just to ensure it doesn't show up anywhere. I also add the following event handler:

                this.LostFocus += delegate
                {
                    this.dropdown.BlockReopen(200);
                    this.dropdown.Close();
                };
    

    This means that it closes as soon as focus is lost, and also calls a method to block the dropdown from re-opening for 200 milliseconds. This I'm not entirely happy with, but solves so many issues in one it'll probably stay for a while. I'm also adding a drop shadow by overriding CreateParams:

            protected override CreateParams CreateParams
            {
                get
                {
                    CreateParams createParams = base.CreateParams;
                    createParams.ClassStyle |= Win32Message.CS_DROPSHADOW;
                    return createParams;
                }
            }
    

    The end result in my quick testbed application:

    Ribbon Dropdown Prototype

    By approaching it in this way, I can make the dropdown escape the bounds of the window as well:

    Ribbon Dropdown Prototype

    My only issue now - the window frame loses focus when you open each one, which is a little jarring. I can get around that by overriding ShowWithoutActivation to return true, but then the LostFocus handler doesn't work.

    More of an annoyance now, but any suggestions for fixing that very welcome!