WPF 窗口允许通过拖放进行标签化






4.88/5 (22投票s)
WPF 窗口允许通过拖放进行标签化。
引言
本文介绍了一个名为 TabWindow
的外壳窗口,其中嵌入了 TabControl
,允许通过拖放操作将选项卡项分离到新窗口。它还允许通过拖放操作将浮动窗口标签化到固定窗口。
背景
你能想象一个 WPF 窗口的行为像 Chrome 或 Internet Explorer 浏览器吗?在运行时,可以通过拖放将一个窗口标签化到另一个窗口。标签可以重新排序,单个标签可以关闭。TabWindow
支持这些功能。然而,它并不仅仅是现代浏览器(如 Chrome)的复制品。有一些主要的区别。例如,当 TabWindow
中只剩一个项目时,选项卡标题会消失。如你所知,空间在 GUI 中非常宝贵。而且,当你在一个窗口上标签化另一个窗口时,你通过标题栏而不是选项卡标题进行拖动,这与 Chrome 的做法不同。然而,TabWindow
不是一个停靠控件。市面上已经有很多商业和开源的停靠控件。TabWindow
继承自 WPF Window
类,因此所有窗口功能都暴露给开发人员。
Using the Code
在代码中使用 TabWindow
非常简单。将 TabWindow
库添加到项目中后,首先像实例化普通 WPF Window 一样实例化 TabWindow
,然后调用 AddTabItem
方法,传递 Control
实例,该实例将作为 TabWindow
实例的内容。所以,构建自己的漂亮用户控件,然后将其传递给 TabWindow
。
TabWindow.TabWindow tabWin = new TabWindow.TabWindow();
TextBox tb = new TextBox();
tb.Text = "Test Demo";
tabWin.AddTabItem(tb.Text, tb);
tabWin.Show();
根据您的需求,创建尽可能多的 TabWindow
,然后通过将一个窗口拖放到另一个窗口上来开始标签化窗口。当一个被拖动的窗口进入一个固定 TabWindow
的边界时,会出现一个选项卡放置目标图像。继续拖动,直到鼠标指针悬停在选项卡放置图像上,然后释放鼠标。被拖动的窗口会消失,固定窗口将添加一个包含被拖动窗口内容的新选项卡。
- 两个独立的
TabWindows
正在浮动。 - 一个“Test 0”窗口被拖到“Test Demo”窗口上方。
- “Test Demo”窗口上的选项卡区域高亮显示。释放按下的鼠标按钮,然后“Test 0”窗口将被标签化到“Test Demo”窗口。
要将选项卡分离到新窗口,请抓取选项卡标题并将其拖出现有窗口,或者双击选项卡标题。这将创建一个独立的窗口。
TabWindow 库的细分
该库主要包含三个部分。每个部分负责其自身的功能。
- 带有关闭按钮的自定义
TabItem
- 派生的
TabControl
,支持自定义TabItem
的拖放 TabWindow
,允许将一个窗口标签化到另一个窗口
带关闭按钮的自定义 TabItem
根据互联网的快速搜索,有很多方法可以完成这项任务。我采用了一种创建派生自 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
,就会通过处理 QueryContinueDrag
事件来更新新窗口的 Left
和 Top
属性。该事件还提供了操作何时完成的信号。当 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
的亮点是:
- 允许重新排序选项卡项
- 允许关闭选项卡项
- 当窗口中只剩一个选项卡项时,选项卡标题变为不可见
- 可以将选项卡项拖出到新窗口
- 双击选项卡标题会创建一个新窗口
- 可以通过标题栏拖动一个窗口并将其拖放到另一个窗口上。源窗口的内容将成为目标窗口的新选项卡项。
历史
- 2015 年 4 月 6 日:初始版本