拖放后 TabPage 标题对齐错误

TabPage title alignment being wrong after drag'n'dropping

我有扩展 System.Windows.Forms.TabControl 的 class 并为其 TabPage 实现了拖放机制,如下所示:

#region Overriden base methods

protected override void OnDragOver(DragEventArgs e)
{
    if (PointedTabPage == null) return;

    e.Effect = DragDropEffects.Move;

    var dragTab = e.Data.GetData(typeof (ManagedTabPage)) as ManagedTabPage;

    if (dragTab == null) return;

    int dropIndex = TabPages.IndexOf(PointedTabPage);
    int dragIndex = TabPages.IndexOf(dragTab);

    if (dragIndex == dropIndex) return;

    var modifiedTabPages = new List<ManagedTabPage>(from ManagedTabPage tabPage in TabPages
                                                    where TabPages.IndexOf(tabPage) != dragIndex
                                                    select TabPages[TabPages.IndexOf(tabPage)] as ManagedTabPage);

    modifiedTabPages.Insert(dropIndex, dragTab);

    for (byte i = 0; i < TabPages.Count; ++i)
    {
        var managedTabPage = TabPages[i] as ManagedTabPage;

        if (managedTabPage != null && managedTabPage.Uid == modifiedTabPages[i].Uid)
            continue;

        TabPages[i] = modifiedTabPages[i];
    }

    SelectedTab = dragTab;
}

protected override void OnMouseDown(MouseEventArgs e)
{
    try
    {
        switch (e.Button)
        {
            case MouseButtons.Left:
                DoDragDrop(PointedTabPage, DragDropEffects.Move);

                break;
            case MouseButtons.Middle:
                CloseTab(PointedTabPage);

                break;
        }
    }
    catch (InvalidOperationException)
    {
    }
    finally
    {
        TabPages.Insert(0, String.Empty);
        TabPages.RemoveAt(0);
    }
}

#endregion

请注意,在 OnMouseDown 方法的 finally 子句中,有 2 行用于解决
问题:出于某种原因 w/o 这些行在拖放任何 TabPage 标题对齐后是错误的:

如果没有这种令人讨厌的解决方法,我应该怎么做才能纠正这种行为?也许发送一些 Windows 消息可以解决问题? 非常感谢任何建议。

编辑 1. ManagedTabPage 的代码 100% 不重要(它只是扩展了 TabPage 的一些特定属性)。

PointedTabPage 也不重要,但就是这样:

return (from ManagedTabPage tabPage in TabPages
        let tabPageIndex = TabPages.IndexOf(tabPage)
        where GetTabRect(tabPageIndex).Contains(PointToClient(Cursor.Position))
        select TabPages[tabPageIndex]).Single() as ManagedTabPage;

我正在尝试实现标签的 fully-centered 对齐。你看,屏幕截图上的标签没有水平居中?

因为你没有分享 ManagedTabPage 代码,我使用默认 TabPage 控制

在方法中进行了更改OnDragOver

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

namespace Demo
{
    public class MyTabControl : TabControl
    {
        public MyTabControl()
        {
            SizeMode = TabSizeMode.Fixed; 
            ItemSize = new Size(224, 20);
        }
        #region Overriden base methods

        protected override void OnDragOver(DragEventArgs e)
        {
            if (DesignMode)
                return;
            if (PointedTabPage == null) return;

            e.Effect = DragDropEffects.Move;

            var dragTab = e.Data.GetData(typeof(TabPage)) as TabPage;

            if (dragTab == null) return;

            int dropIndex = TabPages.IndexOf(PointedTabPage);
            int dragIndex = TabPages.IndexOf(dragTab);

            if (dragIndex == dropIndex) return;

            // change position of tab
            TabPages.Remove(dragTab);
            TabPages.Insert(dropIndex, dragTab);

            SelectedTab = dragTab;

            base.OnDragOver(e);
        }
        protected override void OnMouseDown(MouseEventArgs e)
        {
            if (DesignMode)
                return;

            switch (e.Button)
            {
                case MouseButtons.Left:
                    DoDragDrop(PointedTabPage, DragDropEffects.Move);                    

                    break;
                case MouseButtons.Middle:
                    TabPages.Remove(PointedTabPage);
                    break;
            }
        }

        #endregion
        TabPage PointedTabPage
        {
            get
            {
                return TabPages.OfType<TabPage>()
                    .Where((p, tabPageIndex) => GetTabRect(tabPageIndex).Contains(PointToClient(Cursor.Position)))
                    .FirstOrDefault();
            }
        }
    }
}

我对发布的代码无能为力。让我们采取完全不同的策略,创建一个支持用鼠标拖动标签页的标签控件。向您的项目添加一个新的 class 并粘贴此代码:

using System;
using System.Drawing;
using System.Windows.Forms;

class TabControlEx : TabControl {
    private Point downPos;
    private Form draggingHost;
    private Rectangle draggingBounds;
    private Point draggingPos;

    public TabControlEx() {
        this.SetStyle(ControlStyles.UserMouse, true);
    }
}

后面变量的用法就清楚了。我们需要做的第一件事是从 TabControl 获取鼠标事件,这样我们就可以看到用户试图拖动一个选项卡。这需要打开 UserMouse 控件样式,对于内置 Windows 控件并使用自己的鼠标处理的控件(如 TabControl),默认情况下它是关闭的。

使用“构建”>“构建”并将新控件从工具箱顶部拖到窗体上。一切看起来和行为都像常规的 TabControl,但请注意,单击选项卡不再更改活动选项卡。打开 UserMouse 样式的副作用。让我们先解决这个问题,粘贴:

protected override void OnMouseDown(MouseEventArgs e) {
    for (int ix = 0; ix < this.TabCount; ++ix) {
        if (this.GetTabRect(ix).Contains(e.Location)) {
            this.SelectedIndex = ix;
            break;
        }
    }
    downPos = e.Location;
    base.OnMouseDown(e);
}

我们正在存储点击位置,稍后需要用到它来检测用户拖动选项卡。这需要MouseMove事件,我们需要在用户将鼠标移动到离原始点击位置足够远时开始拖动:

protected override void OnMouseMove(MouseEventArgs e) {
    if (e.Button == MouseButtons.Left && this.TabCount > 1) {
        var delta = SystemInformation.DoubleClickSize;
        if (Math.Abs(e.X - downPos.X) >= delta.Width ||
            Math.Abs(e.Y - downPos.Y) >= delta.Height) {
            startDragging();
        }
    }
    base.OnMouseMove(e);
}

startDragging 方法需要创建一个可以用鼠标四处移动的顶层 window,其中包含我们要四处拖动的选项卡的复制品。我们将其显示为自有的 window,因此它始终位于顶部,与选项卡控件的大小完全相同:

private void startDragging() {
    draggingBounds = this.RectangleToScreen(new Rectangle(Point.Empty, this.Size));
    draggingHost = createDraggingHost(draggingBounds);
    draggingPos = Cursor.Position;
    draggingHost.Show(this.FindForm());
}

createDraggingHost 需要完成繁重的工作并创建一个看起来像选项卡的 window。我们将用鼠标四处移动的无边界窗体。我们将使用 TransparencyKey 属性 使其看起来类似于拖动的 TabPage,顶部有一个选项卡。并通过简单地让它显示标签页的屏幕截图使其看起来相同:

private Form createDraggingHost(Rectangle bounds) {
    var host = new Form() { FormBorderStyle = FormBorderStyle.None, ControlBox = false, AutoScaleMode = AutoScaleMode.None, Bounds = this.draggingBounds, StartPosition = FormStartPosition.Manual };
    host.BackColor = host.TransparencyKey = Color.Fuchsia;
    var tabRect = this.GetTabRect(this.SelectedIndex);
    var tabImage = new Bitmap(bounds.Width, bounds.Height);
    using (var gr = Graphics.FromImage(tabImage)) {
        gr.CopyFromScreen(bounds.Location, Point.Empty, bounds.Size);
        gr.FillRectangle(Brushes.Fuchsia, new Rectangle(0, 0, tabRect.Left, tabRect.Height));
        gr.FillRectangle(Brushes.Fuchsia, new Rectangle(tabRect.Right, 0, bounds.Width - tabRect.Right, tabRect.Height));
    }
    host.Capture = true;
    host.MouseCaptureChanged += host_MouseCaptureChanged;
    host.MouseUp += host_MouseCaptureChanged;
    host.MouseMove += host_MouseMove;
    host.Paint += (s, pe) => pe.Graphics.DrawImage(tabImage, 0, 0);
    host.Disposed += delegate { tabImage.Dispose(); };
    return host;
}

注意 Capture 属性 的使用,这是我们检测用户释放鼠标按钮或通过任何其他方式中断操作的方式。我们将使用 MouseMove 事件将 window 左右移动:

private void host_MouseMove(object sender, MouseEventArgs e) {
    draggingHost.Location = new Point(draggingBounds.Left + Cursor.Position.X - draggingPos.X, 
                                      draggingBounds.Top + Cursor.Position.Y - draggingPos.Y);

}

最后我们需要处理拖动的完成。我们将交换标签,将拖动的标签插入鼠标位置并销毁 window:

private void host_MouseCaptureChanged(object sender, EventArgs e) {
    if (draggingHost.Capture) return;
    var pos = this.PointToClient(Cursor.Position);
    for (int ix = 0; ix < this.TabCount; ++ix) {
        if (this.GetTabRect(ix).Contains(pos)) {
            if (ix != this.SelectedIndex) {
                var page = this.SelectedTab;
                this.TabPages.RemoveAt(this.SelectedIndex);
                this.TabPages.Insert(ix, page);
                this.SelectedIndex = ix;
            }
            break;
        }
    }
    draggingHost.Dispose();
    draggingHost = null;
}

看起来不错