65.9K
CodeProject 正在变化。 阅读更多。
Home

用户可以拖放和调整大小的停靠控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.68/5 (17投票s)

2001年10月23日

6分钟阅读

viewsIcon

239338

downloadIcon

1010

此控件允许用户调整大小并将您的停靠控件拖动到不同的窗体边缘。

Sample Image

引言

C# 最早吸引我的特性之一是能够将 Control DockForm 的边缘。现在我可以将一个 Control(或更可能是一个派生自 UserControl 的复合控件)附加到 Form 边缘,并快速构建一个看起来有用的应用程序。但是,在这种情况下,缺少了一个关键因素。用户无法自行决定此停靠控件的大小或位置。我希望用户能够将控件拖动到不同的边缘,并能够调整控件的大小,以便他们可以根据自己的偏好来自定义应用程序区域。

用于停靠的复合控件

为了解决这个问题,我们需要创建一个新的复合控件 DockingControl,它能够接收调用者提供的控件并管理其位置和大小。我们的复合控件需要一个调整大小栏;一个可用于移动其停靠位置的抓手区域;以及一个用于显示调用者提供的控件的位置。

class DockingControl : UserControl
{
    private Form _form;                
    private DockingResize _resize;        
    private DockingHandle _handle;    
    private BorderControl _wrapper;    
    
    public DockingControl(Form form, DockStyle ds, Control userControl)
    {
        // Remember the form we are hosted on    
        _form = form;

        // Create the resizing bar, gripper handle and border control
        _resize = new DockingResize(ds);
        _handle = new DockingHandle(this, ds);
        _wrapper = new BorderControl(userControl);

        // Wrapper should always fill remaining area
        _wrapper.Dock = DockStyle.Fill;
        
        // Define our own initial docking position for when we are added to 
        // host form
        this.Dock = ds;

        Controls.AddRange(new Control[]{_wrapper, _handle, _resize});
    }    

    public Form HostForm { get { return _form; } }

实例构造函数中的最后一行代码会添加三个子控件 _wrapper_handle_resize。控件在初始化列表中的顺序至关重要,因为当 DockingControl(或任何其他 Control)的 Dock 样式发生更改时,此顺序决定了子控件的位置和大小。计算从最后添加的控件(等同于初始化列表中的最后一个条目)开始,向第一个控件回溯,这与我预期的完全相反。

_resize 栏首先被定位(因此在初始化列表中最后),因为它应该始终显示以跨越停靠控件的整个长度。接下来是 _handle,因为它应该定位在调整大小栏下方。最后是 _wrapper 控件,它排在最后,因为它始终具有 FillDock 样式,并且我们希望它占据其他控件布局完成后剩余的空间。

停靠位置更改

当我们的复合控件的停靠位置发生更改时,我们需要确保我们的子控件也为新的停靠样式正确地定位。因此,我们 override 了继承的 Dock 属性,并根据需要重新计算正确的大小和位置。

    // Override the base class property to allow extra work
    public override DockStyle Dock
    {
        get { return base.Dock; }

        set
        {
            // Our size before docking position is changed
            Size size = this.ClientSize;
        
            // Remember the current docking position
            DockStyle dsOldResize = _resize.Dock;

            // New handle size is dependant on the orientation of the new 
            // docking position
            _handle.SizeToOrientation(value);

            // Modify docking position of child controls based on our new 
            // docking position
            _resize.Dock = DockingControl.ResizeStyleFromControlStyle(value);
            _handle.Dock = DockingControl.HandleStyleFromControlStyle(value);

            // Now safe to update ourselves through base class
            base.Dock = value;

            // Change in orientation occurred?
            if (dsOldResize != _resize.Dock)
            {
                // Must update our client size to ensure the correct size is 
                // used when the docking position changes.  We have to 
                // transfer the value that determines the vector of the 
                // control to the opposite dimension
                if ((this.Dock == DockStyle.Top) || 
                    (this.Dock == DockStyle.Bottom))
                    size.Height = size.Width;
                else
                    size.Width = size.Height;

                this.ClientSize = size;
            }

            // Repaint our controls 
            _handle.Invalidate();
            _resize.Invalidate();
        }
    }

两个 static 函数(ResizeStyleFromControlStyleHandleStyleFromControlStyle)用于查找 _resize_handle 控件的正确停靠样式,这取决于新的 Dock 样式。特别需要注意的是检查停靠方向变化的代码,然后更改控件的 WidthHeight。请记住,当我们的控件停靠在窗体的顶部或底部时,窗体会为我们计算控件的 Width,而 Height 决定了停靠控件向内延伸的距离。当方向移动到左侧或右侧时,我们需要更新控件的 Width 以反映我们希望控件从边缘延伸的距离。因此,新的 Width 应该等于旧的 Height,否则 Width 将保持不变,即窗体的整个宽度,因此它将填充整个客户端区域。

DockingControl 类的其余部分遵循此模式,包括用于恢复 GDI+ 对象(供子控件绘图使用)的 static 属性,以及前面提到的 static 方法,用于根据 DockingControl 的新位置计算每个子控件的新停靠位置。

    // Static variables defining colors for drawing
    private static Pen _lightPen = 
                 new Pen(Color.FromKnownColor(KnownColor.ControlLightLight));
    private static Pen _darkPen = 
                       new Pen(Color.FromKnownColor(KnownColor.ControlDark));
    private static Brush _plainBrush = Brushes.LightGray;

    // Static properties for read-only access to drawing colors
    public static Pen LightPen        { get { return _lightPen;    } }
    public static Pen DarkPen        { get { return _darkPen;    } }
    public static Brush PlainBrush    { get { return _plainBrush; } }

    public static DockStyle ResizeStyleFromControlStyle(DockStyle ds)
    {
        switch(ds)
        {
        case DockStyle.Left:
            return DockStyle.Right;
        case DockStyle.Top:
            return DockStyle.Bottom;
        case DockStyle.Right:
            return DockStyle.Left;
        case DockStyle.Bottom:
            return DockStyle.Top;
        default:
            // Should never happen!
            throw new ApplicationException("Invalid DockStyle argument");
        }
    }

    public static DockStyle HandleStyleFromControlStyle(DockStyle ds)
    {
        switch(ds)
        {
        case DockStyle.Left:
            return DockStyle.Top;
        case DockStyle.Top:
            return DockStyle.Left;
        case DockStyle.Right:
            return DockStyle.Top;
        case DockStyle.Bottom:
            return DockStyle.Left;
        default:
            // Should never happen!
            throw new ApplicationException("Invalid DockStyle argument");
        }
    }
}

调整大小

我们的第一个子控件名为 DockingResize,它提供了一个供用户拖动进行调整大小的停靠控件区域。请注意,当鼠标被单击时,OnMouseDown 会记住父 DockingControl 的当前大小以及鼠标的屏幕位置。这很有必要,以便在收到 OnMouseMove 时,它能够计算自按下鼠标以来鼠标移动的距离,从而确定 DockingControl 的新大小。还要注意,它会将光标设置为允许调整大小的指示。

// A bar used to resize the parent DockingControl
class DockingResize : UserControl
{
    // Class constants
    private const int _fixedLength = 4;

    // Instance variables
    private Point _pointStart;
    private Point _pointLast;
    private Size _size;

    public DockingResize(DockStyle ds)
    {
        this.Dock = DockingControl.ResizeStyleFromControlStyle(ds);
        this.Size = new Size(_fixedLength, _fixedLength);
    }    

    protected override void OnMouseDown(MouseEventArgs e)
    {
        // Remember the mouse position and client size when capture occurred
        _pointStart = _pointLast = PointToScreen(new Point(e.X, e.Y));
        _size = Parent.ClientSize;

        // Ensure delegates are called
        base.OnMouseDown(e);
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        // Cursor depends on if we a vertical or horizontal resize
        if ((this.Dock == DockStyle.Top) || 
            (this.Dock == DockStyle.Bottom))
            this.Cursor = Cursors.HSplit;
        else
            this.Cursor = Cursors.VSplit;

        // Can only resize if we have captured the mouse
        if (this.Capture)
        {
            // Find the new mouse position
            Point point = PointToScreen(new Point(e.X, e.Y));

            // Have we actually moved the mouse?
            if (point != _pointLast)
            {
                // Update the last processed mouse position
                _pointLast = point;

                // Find delta from original position
                int xDelta = _pointLast.X - _pointStart.X;
                int yDelta = _pointLast.Y - _pointStart.Y;

                // Resizing from bottom or right of form means inverse 
               // movements
                if ((this.Dock == DockStyle.Top) || 
                    (this.Dock == DockStyle.Left))
                {
                    xDelta = -xDelta;
                    yDelta = -yDelta;
                }

                // New size is original size plus delta
                if ((this.Dock == DockStyle.Top) || 
                    (this.Dock == DockStyle.Bottom))
                    Parent.ClientSize = new Size(_size.Width, 
                                                 _size.Height + yDelta);
                else
                    Parent.ClientSize = new Size(_size.Width + xDelta, 
                                                 _size.Height);

                // Force a repaint of parent so we can see changed appearance
                Parent.Refresh();
            }
        }

        // Ensure delegates are called
        base.OnMouseMove(e);
    }

此类中唯一其他需要的工作是重写 OnPaint,用于绘制调整大小栏本身的三维外观。它使用 DockingControl 中的 static 方法来恢复要使用的正确 GDI+ 对象。

    protected override void OnPaint(PaintEventArgs pe)
    {
        // Create objects used for drawing
        Point[] ptLight = new Point[2];
        Point[] ptDark = new Point[2];
        Rectangle rectMiddle = new Rectangle();

        // Drawing is relative to client area
        Size sizeClient = this.ClientSize;

        // Painting depends on orientation
        if ((this.Dock == DockStyle.Top) || 
            (this.Dock == DockStyle.Bottom))
        {
            // Draw as a horizontal bar
            ptDark[1].Y = ptDark[0].Y = sizeClient.Height - 1;
            ptLight[1].X = ptDark[1].X = sizeClient.Width;
            rectMiddle.Width = sizeClient.Width;
            rectMiddle.Height = sizeClient.Height - 2;
            rectMiddle.X = 0;
            rectMiddle.Y = 1;
        }
        else if ((this.Dock == DockStyle.Left) || 
                 (this.Dock == DockStyle.Right))
        {
            // Draw as a vertical bar
            ptDark[1].X = ptDark[0].X = sizeClient.Width - 1;
            ptLight[1].Y = ptDark[1].Y = sizeClient.Height;
            rectMiddle.Width = sizeClient.Width - 2;
            rectMiddle.Height = sizeClient.Height;
            rectMiddle.X = 1;
            rectMiddle.Y = 0;
        }

        // Use colors defined by docking control that is using us
        pe.Graphics.DrawLine(DockingControl.LightPen, ptLight[0], ptLight[1]);
        pe.Graphics.DrawLine(DockingControl.DarkPen, ptDark[0], ptDark[1]);
        pe.Graphics.FillRectangle(DockingControl.PlainBrush, rectMiddle);

        // Ensure delegates are called
        base.OnPaint(pe);
    }
}

拖动

我们的下一个子控件 DockingHandle 有三个任务需要执行。首先,它必须确保其大小正确,以反映父 DockingControl 的当前方向。由于我们停靠在父控件的一个边缘,因此我们的一个尺寸将总是被计算出来。但是,另一个尺寸应始终固定,以反映绘图所需的空间并允许用户抓取它。SizeToOrientation 例程执行此决策。

class DockingHandle : UserControl 
{
    // Class constants
    private const int _fixedLength = 12;
    private const int _hotLength = 20;
    private const int _offset = 3;
    private const int _inset = 3;

    // Instance variables
    private DockingControl _dockingControl = null;

    public DockingHandle(DockingControl dockingControl, DockStyle ds)
    {
        _dockingControl = dockingControl;
        this.Dock = DockingControl.HandleStyleFromControlStyle(ds);
        SizeToOrientation(ds);
    }    

    public void SizeToOrientation(DockStyle ds)
    {
        if ((ds == DockStyle.Top) || (ds == DockStyle.Bottom))
            this.ClientSize = new Size(_fixedLength, 0);
        else
            this.ClientSize = new Size(0, _fixedLength);
    }

第二个任务,也是最有趣的任务,是在 OnMouseMove 中完成的。在这里,我们需要将鼠标位置从我们自己的客户端位置转换为主机窗体中的客户端位置。通过测试光标离窗体每个边缘的距离,我们可以决定哪个边缘应该成为父 DockingControl 的新停靠位置。目前,代码使用常量值 _hotLength 来决定鼠标是否足够靠近某个边缘以便更改停靠边缘。实际上引起停靠更改非常简单,只需更改 DockingControl 上的 Dock 属性即可。

    protected override void OnMouseMove(MouseEventArgs e)
    {
        // Can only move the DockingControl is we have captured the
        // mouse otherwise the mouse is not currently pressed
        if (this.Capture)
        {
            // Must have reference to parent object
            if (null != _dockingControl)
            {
                this.Cursor = Cursors.Hand;

                // Convert from client point of DockingHandle to client of 
                // DockingControl
                Point screenPoint = PointToScreen(new Point(e.X, e.Y));
                Point parentPoint = 
                          _dockingControl.HostForm.PointToClient(screenPoint);

                // Find the client rectangle of the form
                Size parentSize = _dockingControl.HostForm.ClientSize;

                // New docking position is defaulted to current style
                DockStyle ds = _dockingControl.Dock;

                // Find new docking position
                if (parentPoint.X < _hotLength)
                {
                    ds = DockStyle.Left;
                }
                else if (parentPoint.Y < _hotLength)
                {
                    ds = DockStyle.Top;
                }
                else if (parentPoint.X >= (parentSize.Width - _hotLength))
                {
                    ds = DockStyle.Right;
                }
                else if (parentPoint.Y >= (parentSize.Height - _hotLength))
                {
                    ds = DockStyle.Bottom;
                }

                // Update docking position of DockingControl we are part of
                if (_dockingControl.Dock != ds)
                    _dockingControl.Dock = ds;
            }
        }
        else
            this.Cursor = Cursors.Default;

        // Ensure delegates are called
        base.OnMouseMove(e);
    }

最后,该控件需要绘制装饰控件区域的两条线。

    protected override void OnPaint(PaintEventArgs pe)
    {
        Size sizeClient = this.ClientSize;
        Point[] ptLight = new Point[4];
        Point[] ptDark = new Point[4];
            
        // Depends on orientation
        if ((_dockingControl.Dock == DockStyle.Top) || 
            (_dockingControl.Dock == DockStyle.Bottom))
        {
            int iBottom = sizeClient.Height - _inset - 1;
            int iRight = _offset + 2;

            ptLight[3].X = ptLight[2].X = ptLight[0].X = _offset;
            ptLight[2].Y = ptLight[1].Y = ptLight[0].Y = _inset;
            ptLight[1].X = _offset + 1;
            ptLight[3].Y = iBottom;
        
            ptDark[2].X = ptDark[1].X = ptDark[0].X = iRight;
            ptDark[3].Y = ptDark[2].Y = ptDark[1].Y = iBottom;
            ptDark[0].Y = _inset;
            ptDark[3].X = iRight - 1;
        }
        else
        {
            int iBottom = _offset + 2;
            int iRight = sizeClient.Width - _inset - 1;
            
            ptLight[3].X = ptLight[2].X = ptLight[0].X = _inset;
            ptLight[1].Y = ptLight[2].Y = ptLight[0].Y = _offset;
            ptLight[1].X = iRight;
            ptLight[3].Y = _offset + 1;
        
            ptDark[2].X = ptDark[1].X = ptDark[0].X = iRight;
            ptDark[3].Y = ptDark[2].Y = ptDark[1].Y = iBottom;
            ptDark[0].Y = _offset;
            ptDark[3].X = _inset;
        }
    
        Pen lightPen = DockingControl.LightPen;
        Pen darkPen = DockingControl.DarkPen;

         pe.Graphics.DrawLine(lightPen, ptLight[0], ptLight[1]);
         pe.Graphics.DrawLine(lightPen, ptLight[2], ptLight[3]);
         pe.Graphics.DrawLine(darkPen, ptDark[0], ptDark[1]);
         pe.Graphics.DrawLine(darkPen, ptDark[2], ptDark[3]);

        // Shift coordinates to draw section grab bar
        if ((_dockingControl.Dock == DockStyle.Top) || 
            (_dockingControl.Dock == DockStyle.Bottom))
        {
            for(int i=0; i<4; i++)
            {
                ptLight[i].X += 4;
                ptDark[i].X += 4;
            }
        }
        else
        {
            for(int i=0; i<4; i++)
            {
                ptLight[i].Y += 4;
                ptDark[i].Y += 4;
            }
        }

         pe.Graphics.DrawLine(lightPen, ptLight[0], ptLight[1]);
         pe.Graphics.DrawLine(lightPen, ptLight[2], ptLight[3]);
         pe.Graphics.DrawLine(darkPen, ptDark[0], ptDark[1]);
         pe.Graphics.DrawLine(darkPen, ptDark[2], ptDark[3]);

        // Ensure delegates are called
        base.OnPaint(pe);
    }
}

缩小用户提供的控件

在开发此代码时,我注意到将用户提供的控件放置到 DockingControl 中会将其推到调整大小栏和抓取区域的边缘。虽然这本身没有问题,但看起来不太整洁,因此我改用这个小辅助类,它在提供的控件周围放置一个边框。DockingControl 创建这个 BorderControl 的一个实例,并将用户提供的控件传递给它。然后,这个 BorderControl 被用来填充 DockingControl,而不是用户提供的控件。

// Position the provided control inside a border to give a portrait picture
// effect
class BorderControl : UserControl 
{
    // Instance variables
    private int _borderWidth = 3;
    private int _borderDoubleWidth = 6;
    private Control _userControl = null;

    public BorderControl(Control userControl)
    {
        _userControl = userControl;
        Controls.Add(_userControl);
    }    

    // Must reposition the embedded control whenever we change size
    protected override void OnResize(EventArgs e)
    {
        // Can be called before instance constructor
        if (null != _userControl)
        {
            Size sizeClient = this.Size;

            // Move the user control to enforce the border area we want    
            _userControl.Location = new Point(_borderWidth, _borderWidth);

            _userControl.Size = new Size(sizeClient.Width - _borderDoubleWidth, 
                                       sizeClient.Height - _borderDoubleWidth);
        }

        // Ensure delegates are called
        base.OnResize(e);
    }
}

结论

此代码是使用文本编辑器开发的,然后通过命令行调用 C# 编译器。因此,外观模仿了 VC6 环境(我拥有的)的外观,而不是 VC7(我没有的)新式停靠控件/窗口的外观和感觉。我认为可以很容易地在此代码基础上构建,以允许浮动控件和停靠栏中的多个停靠控件。如果您有任何想法或对代码进行了任何修改,请随时与我联系,我将非常感兴趣。

许可证

本文未附加明确的许可证,但可能在文章文本或下载文件本身中包含使用条款。如有疑问,请通过下面的讨论区联系作者。

作者可能使用的许可证列表可以在此处找到。

© . All rights reserved.