天天看點

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的亮點是:

  • 允許對頁籤項進行重新排序
  • 允許關閉頁籤項
  • 當視窗中僅剩一個頁籤項時,頁籤标題将不可見
  • 可以将頁籤項拖出到新視窗
  • 輕按兩下頁籤标題會建立一個新視窗
  • 可以通過标題欄拖動一個視窗并将其放在另一個視窗上。源視窗的内容成為目标視窗的新頁籤項。

繼續閱讀