天天看点

WPF窗口允许通过拖放进行切换介绍背景使用代码TabWindow库的细分总结

目录

介绍

背景

使用代码

TabWindow库的细分

带有关闭按钮的自定义TabItem

派生的TabControl支持自定义选项卡之间的拖放

允许将一个窗口切换到另一个窗口的TabWindow

总结

  • 下载源代码35 KB

介绍

本文介绍了一个称为TabWindow的Shell窗口,它嵌入了TabControl,允许通过拖放将选项卡项分离到一个新窗口。它还允许通过拖放将浮动窗口选项卡固定到窗口中。

背景

您能想象WPF窗口的行为类似于Chrome或Internet Explorer浏览器吗?在运行时,可以通过拖放将一个窗口选项卡移动到另一个窗口中。选项卡可以重新排序,单个选项卡可以关闭。TabWindow支持这些功能。但是,它不仅仅是Chrome等现代浏览器的复制品。有一些主要区别。例如,当TabWindow中选项卡仅剩一项时,选项卡页头就会消失。在GUI中,空间是一项额外的功能,如您所知。同样,当您将一个窗口选项卡移动到另一个窗口时,也可以将其拖到标题栏,而不是由Chrome浏览器拖到选项卡标题的位置。TabWindow,但是,不是一个docking控制。已经有许多商业和开源docking控件可用。TabWindow派生自WPF Window 类,因此所有窗口特性都由开发人员公开。

使用代码

在您的代码中使用TabWindow很简单。在将对TabWindow库的引用添加到您的项目后,首先将TabWindow实例化为常规WPF窗口的引用,然后通过传递将成为TabWindow实例内容的Control实例来调用AddTabItem方法。因此,构建您自己的漂亮用户控件,然后将其传递给TabWindow。

TabWindow.TabWindow tabWin = new TabWindow.TabWindow();
TextBox tb = new TextBox();
tb.Text = "Test Demo";
tabWin.AddTabItem(tb.Text, tb);
tabWin.Show();
           
WPF窗口允许通过拖放进行切换介绍背景使用代码TabWindow库的细分总结

根据您的需要,创建尽可能多的TabWindow,然后通过将一个窗口拖到另一个窗口上来开始选项卡窗口。当一个窗口被拖动进入固定TabWindow对象的边界时,将出现一个选项卡放置目标图像。继续拖动,直到鼠标指针悬停在选项卡放置图像上方,然后放开鼠标。拖动的窗口消失,固定的窗口将添加一个新选项卡,其中包含拖动的窗口的内容。

1、两个独立悬浮的TabWindows。

WPF窗口允许通过拖放进行切换介绍背景使用代码TabWindow库的细分总结

2、将“Test 0”窗口拖到“Test Demo”窗口上。

WPF窗口允许通过拖放进行切换介绍背景使用代码TabWindow库的细分总结

3、选项卡区域在“测试演示”窗口中突出显示。释放按下的鼠标按钮,然后将“Test 0”窗口切换到“Test Demo”窗口。

WPF窗口允许通过拖放进行切换介绍背景使用代码TabWindow库的细分总结
WPF窗口允许通过拖放进行切换介绍背景使用代码TabWindow库的细分总结

为了将选项卡分离到新窗口,请按住选项卡标题并将其拖出现有窗口,或双击选项卡标题。它将创建一个独立的窗口。

TabWindow库的细分

库中主要包括三个部分。每个部分负责其自身的功能。

  • 使用关闭按钮的自定义TabItem
  • 派生自支持自定义TabItem拖放的TabControl
  • 允许将一个窗口切换到另一个窗口的TabWindow 

带有关闭按钮的自定义TabItem

根据Internet上的快速搜索,有多种方法可以完成此任务。我采用了一种方法来创建一个衍生自TabItem的自定义控件。为了在选项卡标题上绘制[x]标记,控件模板样式在XAML中声明。最初,我想到了在选择选项卡时使用图像文件显示[x]标记,但最终使用System.Windows.Shapes.Path对象绘制x形状。这是在Generic.xaml中定义[x]按钮的方式。

<ControlTemplate TargetType="{x:Type Button}">
  <Border x:Name="buttonBorder" CornerRadius="2" 

    Background="{TemplateBinding Background}" BorderBrush="DarkGray" BorderThickness="1">
    <Path x:Name="buttonPath" Margin="2" Stroke="DarkGray" StrokeThickness="2" 

        StrokeStartLineCap="Round" StrokeEndLineCap="Round" Stretch="Fill" >
      <Path.Data>
        <PathGeometry>
          <PathFigure StartPoint="0,0">
            <LineSegment Point="13,13"/>
          </PathFigure>
          <PathFigure StartPoint="0,13">
            <LineSegment Point="13,0"/>
          </PathFigure>
        </PathGeometry>
      </Path.Data>
    </Path>
  </Border>
  <ControlTemplate.Triggers>
    ...
  </ControlTemplate.Triggers>
</ControlTemplate>
           

如下所示,此关闭按钮样式将应用于选项卡标题模板。DockPanel由停靠在最右侧的[X]和标头ContentPresenter组成。[x]按钮的默认可见性被隐藏。选择选项卡后,它变为可见。用于Trigger显示或隐藏[x]按钮。

<Style TargetType="{x:Type local:CloseEnabledTabItem}">
...
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:CloseEnabledTabItem}">
        <Grid SnapsToDevicePixels="true" IsHitTestVisible="True" x:Name="gridHeader">
          <Border x:Name="tabItemBorder" Background="{TemplateBinding Background}" 

                    BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1,1,1,0" >
            <DockPanel x:Name="tabItemDockPanel">
              <Button x:Name="tabItemCloseButton" 

                            Style="{StaticResource tabItemCloseButtonStyle}" 

                            DockPanel.Dock="Right" Margin="3,0,3,0" Visibility="Hidden" />
              <ContentPresenter x:Name="tabItemContent" 

                            SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" 

                            RecognizesAccessKey="True" 

                            VerticalAlignment="{TemplateBinding VerticalContentAlignment}" 

                            HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 

                            ContentSource="Header" Margin="{TemplateBinding Padding}"/>
            </DockPanel>
          </Border>
        </Grid>
      ...
      </ControlTemplate>
    </Setter.Value>
  </Setter> 
</Style>
           

现在我们需要进行一些操作。我希望在单击[x]按钮时将选项卡项删除。双击选项卡标题时,我也想引发一个事件。此双击通知将由TabWindow使用,在那里它将生成一个新 TabWindow并将内容从单击的选项卡项移动到新窗口。基本上,这等效于将选项卡拖出到新窗口,因此双击选项卡标题可创建一个新TabWindow实例,并删除双击的选项卡项。

public override void OnApplyTemplate()
{
  base.OnApplyTemplate();
  Button closeButton = base.GetTemplateChild("tabItemCloseButton") as Button;
  if (closeButton != null)
    closeButton.Click += new System.Windows.RoutedEventHandler(closeButton_Click);
  Grid headerGrid = base.GetTemplateChild("gridHeader") as Grid;
  if (headerGrid != null)
    headerGrid.MouseLeftButtonDown += 
               new MouseButtonEventHandler(headerGrid_MouseLeftButtonDown);
}

void closeButton_Click(object sender, System.Windows.RoutedEventArgs e)
{
  var tabCtrl = this.Parent as TabControl;
  if (tabCtrl != null)
    tabCtrl.Items.Remove(this);
}

void headerGrid_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
  if (e.ClickCount == 2)
    this.RaiseEvent(new RoutedEventArgs(TabHeaderDoubleClickEvent, this));
}
           

派生的TabControl支持自定义选项卡之间的拖放

网上有很多拖放教程,因此我将不详细介绍通过拖放对选项卡进行重新排序。但是,将选项卡拖出以创建新窗口不是典型的拖放操作。.NET Framework提供了在拖动鼠标指针期间连续引发的QueryCotinueDrag事件。拖动的鼠标位置保持选中状态,当它离开选项卡控件边界时,它会创建一个新的TabWindow。一旦新的TabWindow被创建,新窗口的Left和Top属性通过处理QueryContinueDrag事件得到更新。当放置操作发生时,此事件还提供信号。由于e.KeyStates被设定为DragDropKeyStates.None,是时候从选项卡控件中删除选项卡项。

void DragSupportTabControl_QueryContinueDrag(object sender, QueryContinueDragEventArgs e)
{
  if (e.KeyStates == DragDropKeyStates.LeftMouseButton)
  {
    Win32Helper.Win32Point p = new Win32Helper.Win32Point();
    if (Win32Helper.GetCursorPos(ref p))
    {
      Point _tabPos = this.PointToScreen(new Point(0, 0));
      if (!((p.X >= _tabPos.X && p.X <= (_tabPos.X + this.ActualWidth) 
              && p.Y >= _tabPos.Y && p.Y <= (_tabPos.Y + this.ActualHeight))))
      {
        var item = e.Source as TabItem;
        if (item != null)
          UpdateWindowLocation(p.X - 50, p.Y - 10, item);
      }
      else
      {
        if (this._dragTornWin != null)
              UpdateWindowLocation(p.X - 50, p.Y - 10, null);
      }
    }
  }
    else if (e.KeyStates == DragDropKeyStates.None)
   {
    this.QueryContinueDrag -= DragSupportTabControl_QueryContinueDrag;
    e.Handled = true;
    if (this._dragTornWin != null)
    {
      _dragTornWin = null;
      var item = e.Source as TabItem;
      if (item != null)
        this.RemoveTabItem(item);
    }
  }
}
           

不幸的是,WPF没有提供可靠的方法来检索桌面屏幕上的当前鼠标位置。如果鼠标指针位于Control中,则有一种可靠的方法来获取准确的鼠标位置,但是将鼠标指针拖出控件或窗口时并非如此。无论鼠标指针在控件内还是窗口外,检索鼠标位置对我来说都是至关重要的。我的帮助来自Win32 API。

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool GetCursorPos(ref Win32Point pt);
           

允许将一个窗口切换到另一个窗口的TabWindow

允许将一个窗口拖放到另一个窗口上以进行选项卡显示是一项艰巨的任务。首先,如果通过窗口标题栏拖动窗口,则不会引发任何拖放事件。我必须使用HwndSource类来处理必要的窗口消息。在SourceInitialized事件处理程序中(在TabWindow创建之后),获取当前窗口实例的HwndSource,然后调用AddHook以包括在窗口过程链中。

void TabWindow_SourceInitialized(object sender, EventArgs e)
{
  HwndSource source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle);
  source.AddHook(new HwndSourceHook(WndProc));
}
           

因此,当窗口被标题栏抓住并拖动时,Win32消息将在钩子处理程序中接收。我们仅处理与目标相关的窗口消息。我们的目标是什么?我想在TabWindow标题栏开始拖动时得到通知。那是WM_ENTERSIZEMOVE信息。在TabWindow拖动这些对象时,需要处理窗口的坐标,这就是WM_MOVE消息。最后,WM_EXITSIZEMOVE指示拖动完成。处理这些winProc消息可以实现我们的目标。将一个 TabWindow拖到另一个TabWindow上时,将显示选项卡放置区图像。将拖动的窗口拖放到选项卡放置区域图像上,拖动的窗口将成功添加到固定窗口。

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
  if (msg == Win32Helper.WM_ENTERSIZEMOVE)
    _hasFocus = true;
  else if (msg == Win32Helper.WM_EXITSIZEMOVE)
  {
    _hasFocus = false;
    DragWindowManager.Instance.DragEnd(this);
  }
  else if (msg == Win32Helper.WM_MOVE)
  {
    if (_hasFocus)
      DragWindowManager.Instance.DragMove(this);
  }
  handled = false;
  return IntPtr.Zero;
}
           

拖动的TabWindow如何判断下面的窗口是否为TabWindow类型?好了,当TabWindow实例化时,它会将自己注册到DragWindowManger单例实例。每当TabWindow移动时,它就会循环访问所有注册的窗口,以检测所拖动的鼠标位置是否在这些TabWindow实例之一上。

public void DragMove(IDragDropToTabWindow dragWin)
{
  if (dragWin == null) return;

    Win32Helper.Win32Point p = new Win32Helper.Win32Point();
  if (!Win32Helper.GetCursorPos(ref p)) return;

    Point dragWinPosition = new Point(p.X, p.Y);
  foreach (IDragDropToTabWindow existWin in _allWindows)
  {
       if (dragWin.Equals(existWin)) continue;

        if (existWin.IsDragMouseOver(dragWinPosition))
    {
      if (!_dragEnteredWindows.Contains(existWin))
        _dragEnteredWindows.Add(existWin);
    }
    else
    {
      if (_dragEnteredWindows.Contains(existWin))
      {
        _dragEnteredWindows.Remove(existWin);
        existWin.OnDrageLeave();
      }
    }
  }
...
}
           

将拖动的TabWindow放到选项卡放置区域后,拖动窗口的内容将传输到在目标TabWindow上创建的新选项卡。然后拖拽的TabWindow消失了。

public void DragEnd(IDragDropToTabWindow dragWin)
{
  if (dragWin == null) return;

    Win32Helper.Win32Point p = new Win32Helper.Win32Point();
  if (!Win32Helper.GetCursorPos(ref p)) return;

    Point dragWinPosition = new Point(p.X, p.Y);
  foreach (IDragDropToTabWindow targetWin in _dragEnteredWindows)
  {
    if (targetWin.IsDragMouseOverTabZone(dragWinPosition))
    {
      System.Windows.Controls.ItemCollection items = ((ITabWindow)dragWin).TabItems;
      for (int i = 0; i < items.Count; i++)
      {
        System.Windows.Controls.TabItem item = items[i] as 
                                     System.Windows.Controls.TabItem;
        if (item != null)
          ((ITabWindow)targetWin).AddTabItem(item.Header.ToString(), 
                     (System.Windows.Controls.Control)item.Content);
      }
      for (int i = items.Count; i > 0; i--)
      {
        System.Windows.Controls.TabItem item = items[i - 1] as 
                                        System.Windows.Controls.TabItem;
        if (item != null)
          ((ITabWindow)dragWin).RemoveTabItem(item);
      }
    }
    targetWin.OnDrageLeave();
  }
  if (_dragEnteredWindows.Count > 0 && ((ITabWindow)dragWin).TabItems.Count == 0)
  {
    ((Window)dragWin).Close();
  }
  _dragEnteredWindows.Clear();
}
           

总结

TabWindow库在复合应用程序中非常有用,在复合应用程序中,模块可以直接加载到TabWindow实例中。然后,将如何将窗口动态合并到选项卡的决定权留给用户。TabWindow的亮点是:

  • 允许对选项卡项进行重新排序
  • 允许关闭选项卡项
  • 当窗口中仅剩一个选项卡项时,选项卡标题将不可见
  • 可以将选项卡项拖出到新窗口
  • 双击选项卡标题会创建一个新窗口
  • 可以通过标题栏拖动一个窗口并将其放在另一个窗口上。源窗口的内容成为目标窗口的新选项卡项。

继续阅读