控件边界外的自定义绘制下拉面板

Custom draw dropdown panel outside Control bounds

我似乎获得了 "unclear what I'm asking" 票。我想自定义绘制组合框样式控件。弹出打开部分需要绘制在控件本身的边界之外。我不能使用组合框 - 想想类似于 Word 功能区中的图库控件的东西。

我想到了两种方法:

后者还允许下拉菜单脱离 window 的边界,这可能有用,但不是绝对必要的。

还有其他方法吗?您认为哪种方法最好?

谢谢。


原问题:

我正在为一个小项目编写一个基于 winforms 的自定义绘图 UI 库。一切进展顺利,但我有一个轻微的结构性问题,下拉菜单超出了 Graphics 对象的范围。

大部分控件都是使用纯自定义绘制和重绘事件模型完成的,但整体界面是使用 winforms DockWidthHeight 等进行布局的。

我添加了一个下拉菜单,但很明显,当它的下拉部分的边界超出布局 Panel 的图形对象的边界时,它会被截断。

(我曾期望在 SO 上找到类似的东西,但没有找到。)

我已经通过让表单控制下拉覆盖图的绘制来解决这个问题,但是使用自定义鼠标处理程序和其他一切,表单开始感觉负担过重。

我曾尝试存储对 Graphics 对象的引用,但发现在引发 OnPaint 之外使用它们是..喜怒无常的。

当前模型的简化代码示例如下。此代码本身并没有 运行 任何有用的方式,而是展示了用于显示叠加层的方法。

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)
    {
    }
}

显然这里缺少很多位,但它至少应该显示我正在使用的方法。

有什么建议可以防止我不得不在表单之间来回传递这个吗?

谢谢


更新:

明确地说,问题在于绘制下拉菜单的 "popup" 部分,而不是下拉菜单本身。 (这里用ComboBox来演示)

从那以后我还记得一个小的 window 会强制 ComboBox 超出 window.

的范围

它上面的阴影在我看来很像 CreateParamsCS_DROPSHADOW - 我可以使用 NativeWindow 子类来处理这个问题吗?

我想我已经选择了第二个选项,即使用第二个表单来显示下拉面板。我使用了扩展窗体 class 而不是 NativeWindow。只是觉得我应该分享结果,以防其他人尝试同样的事情并发现这个。

选择下拉菜单后,我使用 PointToScreen 设置表单以获取坐标。它还设置了以下属性:

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

只是为了确保它不会出现在任何地方。我还添加了以下事件处理程序:

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

这意味着它会在失去焦点后立即关闭,并且还会调用一个方法来阻止 re-opening 的下拉列表 200 毫秒。我对此并不完全满意,但一次解决了很多问题,它可能会保留一段时间。我还通过覆盖 CreateParams 添加了阴影:

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

我的快速测试平台应用程序的最终结果:

通过这种方式,我也可以让下拉菜单超出 window 的边界:

我现在唯一的问题 - window 框架在您打开每个框架时都失去焦点,这有点不和谐。我可以通过将 ShowWithoutActivation 覆盖为 return true 来解决这个问题,但是 LostFocus 处理程序不起作用。

现在比较麻烦,但是非常欢迎任何解决问题的建议!

前几天我看了这个,因为我遇到了同样的情况,但我选择了不同的方法,也许是一个更好的解决复杂情况的方法。在下图中,TestingForm 内部有一个 SplitContainer。在右侧面板(背景橙色)中有两个控件,一个是 TextBox,另一个是 Panel(背景颜色是海蓝色)。在海蓝宝石面板内部是一个停靠的第二个拆分面板控件,左侧包含标签,右侧包含我的 "CJL_PanelCtrl"。显然,单击箭头将打开与任何其他控件重叠的控件 "down"。

我的解决方案是创建一个面板并将其放置在不受限制的父控件更改中。在此示例中,它将是第一个 SplitContainer 的右侧面板(橙色面板)。在这种情况下,如果要重新调整第一个 SplitContiner 的大小,它将正确移动面板的位置。

请注意,面板正在绘制其父颜色,即橙色。下面的代码完成了这个。

CJL_DropPanelCtrl 中有两个控件,一个按钮 _btn,其中图像根据状态而改变,还有一个文本框 _TB

public partial class CJL_DropPanelCtrl : UserControl
{
    internal bool IsDropped { get; set; }       // true if the panel is visible

    internal Panel DropPanel { get; private set; }  // the panel make the public set to set your own panel as needed


    /// <summary>
    /// The Control where the DropPanel will be in the Z-order chain
    /// </summary>
    internal Control PanelParent
    { get { return DropPanel.Parent; }
      set 
        {   
          DropPanel.Parent = value;
          SetLocation();
        }
    }

    public CJL_DropPanelCtrl()
    {
        InitializeComponent();
        IsDropped = false;

        DropPanel = new Panel();
        DropPanel.BorderStyle = BorderStyle.FixedSingle;
        DropPanel.Height = 100;
        DropPanel.Visible = false;  
    }

    private void OnBtnClick(object sender, EventArgs e)
    {
        IsDropped = !IsDropped;
        _btn.Image = IsDropped ? Properties.Resources.DnPointer : Properties.Resources.RtPointer;
        DropPanel.Visible = IsDropped;
        DropPanel.BringToFront();   
    }

    internal void SetLocation()
    {
        // here we go up the chain of controls to determine where the location of the panel is
        // to be placed.
        Control c = _TB;
        Point offset = new Point(1, _TB.Height+2);
        while (c != DropPanel.Parent)
        {
            offset.X += c.Location.X;
            offset.Y += c.Location.Y;
            c = c.Parent;
        }
        DropPanel.Location = offset;


    }

    private void OnTBSizeChanged(object sender, EventArgs e)
    {
        DropPanel.Width = _TB.Width;
    }

}

在我的 TestingForm 代码中看起来像这样。请注意,我设置了 PanelParent 属性 并为第二个 SplitContainer 拆分器移动添加了一个 EventHandler。

public partial class TestingForm : Form
{

    public TestingForm()
    {
        InitializeComponent();
    }

    private void OnLoad(object sender, EventArgs e)
    {
        cjL_DropPanelCtrl1.PanelParent = splitContainer1.Panel2;
    }

    private void OnSplitter2Moved(object sender, SplitterEventArgs e)
    {
        cjL_DropPanelCtrl1.SetLocation();
    }

}

唯一需要注意的是,我在 CJL_DropPanelCtrl 的文本框的 SizeChanged 事件中添加了一个事件处理程序,如果(在我的测试中就是这种情况)控件锚定到左右它将正确调整面板大小。

干杯