graphics.hatenablog.com

技術系テクニカルアーティストのあれこれ

切り離し可能なタブコントロール

最近全然グラフィックスっぽくないけど気にしない。。

AvalonDock だと色々とオーバースペックすぎたから、
勉強も兼ねてミニマムなかんじでつくってみた。

  • ItemsTemplate と ContentTemplate がそのまま使える
  • ヘッダ部分をタブコントロールの外までドラッグすると単独のウィンドウになる
  • ウィンドウを閉じるとタブコントロールの中に戻る

みたいなやつ。

動作イメージ

f:id:hal1932:20141014012512j:plain

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() をオーバーライドして、「マウスが領域外に出たときの処理」を設定する。

  1. ゴーストを非表示状態にする
  2. tabItem の Content を参照するウィンドウを新規作成して表示する
  3. this.Items から tabItem を削除する

TabItem の復帰

↑で生成したウィンドウの Closed イベントのハンドラで、TabItem を新規作成して、this.Items に追加するだけ。

ドッキングを実装しなかった理由

[Items|Content]Template を使えるようにするには、TabControl を継承するのが一番手っ取り早いんだけど、ドッキングするには UserControl か Grid(DockPanel だと GridSplitter 的な要素が使えない)を継承する必要がある。そうすると、内部で抱えた TabControl に [Items|Content]Template をパスできるようにして、更に [Column|Row]Definitions を動的にごにょごにょしなきゃいけない。正直めんどくさい。

まぁそこまでやりたかったらおとなしく AvalonDock を使いましょう、ってことで。