切り離し可能なタブコントロール
最近全然グラフィックスっぽくないけど気にしない。。
AvalonDock だと色々とオーバースペックすぎたから、
勉強も兼ねてミニマムなかんじでつくってみた。
- ItemsTemplate と ContentTemplate がそのまま使える
- ヘッダ部分をタブコントロールの外までドラッグすると単独のウィンドウになる
- ウィンドウを閉じるとタブコントロールの中に戻る
みたいなやつ。
動作イメージ
TabControl を継承
既存の [Items|Content]Template をそのまま使えるようにしたい。
public class SeparableTabControl : TabControl
マウスイベントが置きた場所を調べる
TabControl のビジュアルツリーはちょっとややこしい。
private bool IsEventFromHeader(RoutedEventArgs e) { // TabControl のビジュアルツリー // // TabControl // ├─ TabPanel // │ └─ TabItem // ← ここが Header 部分 // └─ ContentPresenter // ← ここが Content 部分 // // Header 部分でイベントがおきたときは // e.Source.GetType() == typeof(TabItem) になる // // Content 部分でイベントが起きたときは // e.Source.GetType() は Content に応じて変わる // たとえば // <TabItem><Grid/></TabItem> のときは e.Source.GetType() == typeof(Grid) // return (e.Source is TabItem); }
「TabItem の追加と削除」をフックする
// this.Items.Add() されたときに呼ばれる protected override void PrepareContainerForItemOverride(DependencyObject element, object item) { base.PrepareContainerForItemOverride(element, item); var tabItem = (TabItem)element; tabItem.PreviewMouseLeftButtonDown += ...; tabItem.PreviewMouseLeftButtonUp += ...; } // this.Items.Remove() されたときに呼ばれる protected override void ClearContainerForItemOverride(DependencyObject element, object item) { var tabItem = (TabItem)element; tabItem.PreviewMouseLeftButtonDown -= ...; tabItem.PreviewMouseLeftButtonUp -= ...; base.ClearContainerForItemOverride(element, item); } private void tabItem_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // 「(TabItem)sender がドラッグ中」というフラグを立てる // ゴースト表示開始 } private void tabItem_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { // 「ドラッグ中」フラグを折る // ゴースト表示終了 }
TabItem のドラッグ
DragDrop.DoDragDrop() とかはややこしいから使わないで、ゴースト表示でごまかす。
SeparableTabControl/TabItemGhost.cs at master · hal1932/SeparableTabControl · GitHub
Adorner の概念ってわかりにくいんだけど、とりあえず今回の用途はこれ↓
UIElement に視覚的装飾をオーバーレイする
GetVisualChild() をオーバーライドして Rectangle を返すようにして、その Rectangle の Fill に new VisualBrush(tabItem) を仕込んでおく。そうすると、tabItem のビジュアルをオーバーレイ表示できるようになる。
SeparableTabControl 側はこんなかんじ↓
private void SeparableTabControl_Loaded(object sender, RoutedEventArgs e) { // this の修飾レイヤに _tabItemGhost を追加する AdornerLayer.GetAdornerLayer(this).Add(_tabItemGhost); } protected override void OnPreviewMouseMove(MouseEventArgs e) { // ドラッグ中だったらマウス位置にゴーストを追従させる if (!_isItemDragged) return; _tabItemGhost.Move(e.GetPosition(null)); }
TabItem の切り離し(ウィンドウ化)
SeparableTabControl で OnMouseLeave() をオーバーライドして、「マウスが領域外に出たときの処理」を設定する。
- ゴーストを非表示状態にする
- tabItem の Content を参照するウィンドウを新規作成して表示する
- this.Items から tabItem を削除する
TabItem の復帰
↑で生成したウィンドウの Closed イベントのハンドラで、TabItem を新規作成して、this.Items に追加するだけ。
ドッキングを実装しなかった理由
[Items|Content]Template を使えるようにするには、TabControl を継承するのが一番手っ取り早いんだけど、ドッキングするには UserControl か Grid(DockPanel だと GridSplitter 的な要素が使えない)を継承する必要がある。そうすると、内部で抱えた TabControl に [Items|Content]Template をパスできるようにして、更に [Column|Row]Definitions を動的にごにょごにょしなきゃいけない。正直めんどくさい。
まぁそこまでやりたかったらおとなしく AvalonDock を使いましょう、ってことで。