WPF DataGridで列選択できるようにする

WPFのDataGridはSelectionUnitプロパティをCellOrRowHeaderにすると行ヘッダのクリックで行選択ができる。一方で列選択は用意されていない。必要になったので自前で再現してみたが、案外面倒くさかったので残しておく。

XAML

<DataGrid x:Name="MyGrid" SelectionMode="Extended" SelectionUnit="CellOrRowHeader"
            CanUserSortColumns="False" CanUserReorderColumns="False"
            CanUserResizeColumns="False" CanUserResizeRows="False">
    <DataGrid.ColumnHeaderStyle>
        <Style TargetType="DataGridColumnHeader">
            <EventSetter Event="PreviewMouseDown" Handler="Column_MouseDown" />
            <EventSetter Event="PreviewMouseMove" Handler="Column_MouseMove" />
            <EventSetter Event="PreviewMouseUp" Handler="Column_MouseUp" />
        </Style>
    </DataGrid.ColumnHeaderStyle>
</DataGrid>

セルの複数選択が必要なので、SelectionModeをExtended、SelectionUnitをCellまたはCellOrRowHeaderにする。また列ヘッダでのマウス操作で競合する、ソート、列の入れ替え、幅リサイズの機能は無効にする。

さらに列ヘッダにEventSetterでマウス操作のイベントハンドラを設定する。ColumnHeaderStyleを定義して、PreviewMouseDown、PreviewMouseMove、PreviewMouseUpのイベントハンドラを設定する。なおPreviewではないMouseDownイベントなどは発火しなかった。

コードビハインド

//列ヘッダドラッグ中
private bool IsColumnDrag = false;

//列ヘッダドラッグ開始列番号
private int ColumnDragStartOn = 0;

/// <summary>
/// 列ヘッダドラッグ開始
/// </summary>
private void Column_MouseDown(object sender, MouseButtonEventArgs e) {
    var grid = MyGrid;
    var clickOn = GetColumnNumberOfPoint(grid, e.GetPosition(grid).X);
    
    grid.Focus(); //フォーカス
    grid.SelectedCells.Clear();

    //列選択
    foreach(var x in grid.Items) {
        var info = new DataGridCellInfo(x, grid.Columns[clickOn]);
        grid.SelectedCells.Add(info);
    }
    IsColumnDrag = true;
    ColumnDragStartOn = clickOn;
}

/// <summary>
/// 列ヘッダドラッグ中
/// </summary>
private void Column_MouseMove(object sender, MouseEventArgs e) {
    if(!IsColumnDrag) return;

    var grid = MyGrid;
    var dragOn = GetColumnNumberOfPoint(grid, e.GetPosition(grid).X);
    
    grid.SelectedCells.Clear();
    
    //ドラッグ開始~現在地の列まで選択
    var from = Math.Min(ColumnDragStartOn, dragOn);
    var to = Math.Max(ColumnDragStartOn, dragOn);

    foreach (var x in grid.Items) {
        for (int i = from; i <= to; i++) {
            var info = new DataGridCellInfo(x, grid.Columns[i]);
            grid.SelectedCells.Add(info);
        }
    }
}

/// <summary>
/// 列ヘッダドラッグ解除
/// </summary>
private void Column_MouseUp(object sender, MouseButtonEventArgs e) {
    IsColumnDrag = false;
}

/// <summary>
/// グリッドの相対位置にある列を取得
/// </summary>
/// <param name="grid">対象グリッド</param>
/// <param name="pointX">相対位置</param>
/// <returns>0から始まる列番号</returns>
private int GetColumnNumberOfPoint(DataGrid grid, double pointX) {
    //行ヘッダサイズ
    double p = grid.RowHeaderActualWidth;

    //RowHeaderStyleの設定から取得
    //double p = (double)grid.RowHeaderStyle.Setters
    //    .OfType<Setter>()
    //    .First(x=>x.Property.Name=="Width")
    //    .Value;
    
    //左端以前
    if(p >= pointX) return 0;

    //ポインタ位置の列
    for(int i=0; i<grid.Columns.Count; i++) {
        p += grid.Columns[i].ActualWidth;
        if(p >= pointX) return i;
    }

    //右端以降
    return grid.Columns.Count -1;
}

マウスイベントが発生したときに、ポインタのX位置と重なる列のセルを選択状態にする。一度全てのセル選択をクリアした後に、Itemsをなめて各行と該当列のDataGridCellInfoを作りSelectedCellsに追加していく。

ポインタ位置の列の取得

マウスイベントの引数であるMouseEventArgsのGetPositionメソッドで、グリッドコントロールの原点からの相対位置を取得する。

グリッドの行ヘッダおよび各列の幅から、ポインタ位置と重なる列を判断する。なお実際の環境ではRowHeaderActualWidthがいい値にならなかったため、コメントのようにRowHeaderStyleの設定値から幅を取得した。

範囲選択の対応

行ヘッダと同じくドラッグで範囲選択するためには、MouseDownイベント発生時の列を保存しておき、MouseMoveイベントのたびに間のセルを選択しなおす。

なおWPFのコントロールではマウスを押し込んだまま動かすと、マウスポインタがコントロールの外(あるいはウィンドウの外)に出ても、イベントは元のコントロールで発火する。そのためMouseMoveのsender引数から対象の列ヘッダを取得できないが、マウスポインタが外に出た場合の処理は用意しないで済む。

できないこと

このサンプルではShift+クリックやCtrl+クリックでの複数選択に対応していない。恐らく作る機会は無い。

コメント