Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

WPF Window Allows Tabbing by Drag and Drop

Rate me:
Please Sign up or sign in to vote.
4.88/5 (23 votes)
11 Nov 2019CPOL7 min read 59.5K   2.6K   52   22
WPF Window allows tabbing by drag and drop

Introduction

This articles introduces a shell window called TabWindow embedded with TabControl that allows detaching a tab item to a new window through a drag-and-drop. It also allows a floating window being tabbed to a stationary window through the drag-and-drop.

Background

Can you imagine a WPF window that behaves like a Chrome or Internet Explorer browser? At run time, a window can be tabbed to another window through the drag-and-drop. Tabs can be re-ordered and an individual tab can be closed. The TabWindow supports those features. However it's not a mere copy-cat of the modern browser like Chrome. There are a few main differences. For instance, a tab header disappears when there is only one item left in the TabWindow. Space is a premium in GUI as you know. Also when you tab one window to another, you drag it by the title bar instead of the tab header as it's done with the Chrome. The TabWindow, however, is not a docking control. There are many commercial and open source docking controls available out there already. The TabWindow derives from WPF Window class, hence all window features are exposed at a developer's hands.

Using the Code

It's simple to use TabWindow in your code. After adding the reference to the TabWindow library to your project, first instantiate TabWindow as you would've done it for a regular WPF Window, then call AddTabItem method by passing the Control instance which will be the content of the TabWindow instance. So build your own beautiful user control, then pass it to the TabWindow.

C#
TabWindow.TabWindow tabWin = new TabWindow.TabWindow();
TextBox tb = new TextBox();
tb.Text = "Test Demo";
tabWin.AddTabItem(tb.Text, tb);
tabWin.Show();

Image 1

Depending on your need, create as many TabWindows as possible, then start tabbing windows by dragging one window over another. As one window being dragged enters the border of a stationary TabWindow, a tab drop target image will appear. Keep dragging until your mouse pointer is over the tab drop image then let the mouse go. The dragged window vanishes and the stationary window will be added with a new tab containing the content of the dragged window.

  1. Two separate TabWindows floating.

    Image 2

  2. A "Test 0" window is dragged over "Test Demo" window.

    Image 3

  3. The tab zone highlights on the "Test Demo" window. Release the mouse button pressed, then the "Test 0" window will be tabbed to the "Test Demo" window.

    Image 4Image 5

    In order to separate a tab to new window, grab the tab header and drag it out of the existing window or double-click the tab header. It will create an independent window.

Breakdown of TabWindow Library

Mainly, there are three parts in the library. Each part is responsible for its own functionality.

  • Custom TabItem with a close button
  • Derived TabControl which supports the drag and drop of custom TabItem
  • TabWindow that allows tabbing one window to another

Custom TabItem with a Close Button

There are a number of ways to accomplish this task according to a quick search on the Internet. I took an approach on creating a custom control deriving from TabItem. To draw [x] mark on a tab header, the control template style was declared in the XAML. Initially, I thought of using an image file to show the [x] mark when the tab is selected but ended up using the System.Windows.Shapes.Path object to draw the x shape. This is how the [x] button is defined in Generic.xaml.

XML
 <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>

This close button style is applied to the tab header template as shown below. The DockPanel consists of the [x] button docked to the far right and the header ContentPresenter. The default visibility of the [x] button is hidden. It becomes visible when the tab gets selected. Used Trigger to show or hide the [x] button.

XML
<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>

Now we need to wire some actions. I want the tab item to be removed when the [x] button is clicked. I also would like to raise an event when the tab header is double-clicked. This double-click notification will be consumed by TabWindow where it will generate a new TabWindow and move the content from the clicked tab item to new window. Basically, it's equivalent to dragging the tab out to a new window so double-clicking on the tab header creates a new TabWindow instance and removes the double-clicked tab item.

C#
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));
}

Derived TabControl Which Supports the Drag and Drop Among Custom Tabs

There are many drag-and-drop tutorials on the web so I won't go in detail about re-ordering the tabs by the drag-and-drop. However dragging the tab out to create a new window is not a typical drag-and-drop operation. The .NET Framework provides the QueryCotinueDrag event which is raised continuously during the dragging the mouse pointer. The dragged mouse position is kept on checked and when it goes out of the tab control border, it creates a new TabWindow. Once the new TabWindow is created, the Left and Top properties of the new window get updated by handling the QueryContinueDrag event. This event also provides the signal when the drop operation occurs. As e.KeyStates is set to DragDropKeyStates.None, it's time to remove the tab item from the tab control.

C#
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);
    }
  }
}

Unfortunately, the WPF does not provide a reliable way to retrieve the current mouse position on the desktop screen. If your mouse pointer is located within the Control, then there is a dependable way to get the accurate mouse position but it is not the case when you drag your mouse pointer out of the control or window. It was crucial for me to retrieve the mouse position whether or not the mouse pointer is within the control or out of the window. My help came from Win32 API.

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

TabWindow that Allows Tabbing One Window to Another

Allowing one window to be dragged and dropped on a different window to be tabbed was a challenging task. First of all, if the window is dragged by the window title bar, there is no drag-and-drop events raised. I had to use HwndSource class to process necessary window messages. In SourceInitialized event handler (after the TabWindow is created), get the HwndSource of the current window instance then call AddHook to include in the window procedure chain.

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

So when the window is grabbed by the title bar and dragged around, the Win32 messages are received in the hook handler. We only process the window messages that's relevant to our goal. What's our goal? I want to get notified when a TabWindow gets started with dragging by the title bar. That's WM_ENTERSIZEMOVE message. While the TabWindow gets dragged around, the coordinate of the window needs to be processed and that's WM_MOVE message. Finally the WM_EXITSIZEMOVE indicates the dragging is done. Handling these winProc messages accomplishes our goal. When a TabWindow is dragged over another TabWindow, the tab drop zone image will appear. Drop the dragged window onto the tab drop zone image, the dragged window will be added to the stationary window successfully.

C#
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;
}

How does the dragged TabWindow figures out the window underneath is the type of TabWindow or not? Well, as TabWindow gets instantiated, it registers itself to the DragWindowManger singleton instance. Whenever the TabWindow is moved, it loops through all registered windows to detect if the dragged mouse position is over one of the TabWindow instances.

C#
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();
      }
    }
  }
...
}

Once the dragged TabWindow is dropped on the tab drop zone, the content of the dragged window is transferred to a new tab created on the target TabWindow. Then the dragged TabWindow vanishes.

C#
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();
}

Summary

The TabWindow library can be very useful in a composite application where a module can be loaded directly into a TabWindow instance. Then leave the decision to user on how to merge the windows into tabs dynamically. The highlights of the TabWindow are:

  • Allows re-ordering of tab items
  • Allows a tab item to be closed
  • Tab header becomes invisible when there is only one tab item left in the window
  • A tab item can be dragged out to a new window
  • Double-clicking on a tab header creates a new window
  • A window can be dragged by the title bar and dropped over another window. A content of the source window becomes a new tab item of the target window.

History

  • 6th April, 2015: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
Senior Software Developer

Comments and Discussions

 
QuestionChange location of Minimized tabbed window on secondary screen Pin
jymitra12-Dec-19 23:53
jymitra12-Dec-19 23:53 
Questionwhat have you changed in this version ? Pin
BillWoodruff12-Nov-19 18:44
professionalBillWoodruff12-Nov-19 18:44 
Questiona great TabControl ... Pin
kad khattab4-Nov-15 4:12
kad khattab4-Nov-15 4:12 
hello

can you help by show how to to use this custom control in a XAML file.
with example how to used in TabControl and TabItem if it okay...
i try to used in the tabItem,but it doesn't work.

can you help...

thanks
AnswerRe: a great TabControl ... Pin
Suk H. Lee4-Nov-15 17:53
Suk H. Lee4-Nov-15 17:53 
GeneralRe: a great TabControl ... Pin
kad khattab4-Nov-15 20:13
kad khattab4-Nov-15 20:13 
GeneralRe: a great TabControl ... Pin
Suk H. Lee5-Nov-15 15:27
Suk H. Lee5-Nov-15 15:27 
GeneralRe: a great TabControl ... Pin
kad khattab5-Nov-15 22:23
kad khattab5-Nov-15 22:23 
GeneralMissing zip file Pin
Tomek Nizicki1-Jun-15 0:40
Tomek Nizicki1-Jun-15 0:40 
GeneralRe: Missing zip file Pin
Suk H. Lee3-Jun-15 16:10
Suk H. Lee3-Jun-15 16:10 
QuestionForce single line tab headers Pin
Bartosz Jarmuż13-Apr-15 12:27
Bartosz Jarmuż13-Apr-15 12:27 
AnswerRe: Force single line tab headers Pin
Suk H. Lee20-Apr-15 15:46
Suk H. Lee20-Apr-15 15:46 
Question2 Little bugs Pin
Bartosz Jarmuż5-Mar-15 2:16
Bartosz Jarmuż5-Mar-15 2:16 
AnswerRe: 2 Little bugs Pin
Suk H. Lee5-Mar-15 16:50
Suk H. Lee5-Mar-15 16:50 
GeneralRe: 2 Little bugs Pin
Bartosz Jarmuż5-Mar-15 23:03
Bartosz Jarmuż5-Mar-15 23:03 
GeneralRe: 2 Little bugs Pin
Suk H. Lee3-Apr-15 11:17
Suk H. Lee3-Apr-15 11:17 
GeneralRe: 2 Little bugs Pin
Bartosz Jarmuż13-Apr-15 11:40
Bartosz Jarmuż13-Apr-15 11:40 
GeneralRe: 2 Little bugs Pin
Bartosz Jarmuż13-Apr-15 12:04
Bartosz Jarmuż13-Apr-15 12:04 
GeneralRe: 2 Little bugs Pin
Suk H. Lee20-Apr-15 15:44
Suk H. Lee20-Apr-15 15:44 
AnswerRe: 2 Little bugs Pin
pdfw28-May-15 16:19
pdfw28-May-15 16:19 
GeneralRe: 2 Little bugs Pin
Suk H. Lee30-May-15 17:33
Suk H. Lee30-May-15 17:33 
BugAll great, but InvalidCast occurs when dragging tab Pin
Bartosz Jarmuż22-Feb-15 1:44
Bartosz Jarmuż22-Feb-15 1:44 
GeneralRe: All great, but InvalidCast occurs when dragging tab Pin
Suk H. Lee22-Feb-15 16:34
Suk H. Lee22-Feb-15 16:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.