2010-06-17

VisualTreeHelper.HitTest và UserControl

Từ khi chuyển qua WPF nhiều thứ thay đổi, kể cả các thói quen khi code WinForms. Ví dụ trong trường hợp nào đó, mình nghĩ chức năng tương tự của WinForms nhưng WPF lại không có. Ví dụ khi click nghĩ đến bounds của control, rồi nghĩ tới 1 cái gì đó đại loại hit test thế rồi nó sẽ dẫn đến VisualTreeHelper.HitTest của WPF chẳng hạn. Nhưng mọi chuyện đâu có tuần tự tốt đẹp như vậy, sẽ có nhiều thứ để nghĩ hơn.

Ví dụ có UserControl như sau:
-->
<UserControl x:Class="TestHitTest.DemoControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Height="200">
    <Grid>
        <Label Content="This is a demo control" />
        <Rectangle Fill="#FF1E59C1" Stroke="Black" HorizontalAlignment="Left" Margin="18,18,0,0" VerticalAlignment="Top" Width="93" Height="48" Opacity="0.7"/>
        <Rectangle Fill="#FF83A3DA" Stroke="Black" Margin="71,29,80,0" VerticalAlignment="Top" Height="56" Opacity="0.7"/>
        <Rectangle Fill="#FF2D3441" Stroke="Black" HorizontalAlignment="Left" Margin="53,47,0,0" VerticalAlignment="Top" Width="48" Height="38" Opacity="0.7"/>
        <Rectangle Fill="#FF97A2B5" Stroke="Black" HorizontalAlignment="Left" Margin="18,75,0,0" VerticalAlignment="Top" Width="53" Height="53" Opacity="0.7"/>
        <Rectangle Fill="#FF7AA4EE" Stroke="Black" HorizontalAlignment="Left" Margin="32,96,0,114" Width="55" Opacity="0.7"/>
        <Rectangle Fill="#FF3E547A" Stroke="Black" HorizontalAlignment="Right" Margin="0,56,31,0" VerticalAlignment="Top" Width="102" Height="56" Opacity="0.7"/>
    Grid>
UserControl>

  Trong main win chứa 2 user control: 1 bên trái và một bên phải
-->
<Window x:Class="TestHitTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:TestHitTest" Title="Window1" Width="600" Height="300">
    <Grid x:Name="_grid">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50*" />
            <ColumnDefinition Width="50*" />
        Grid.ColumnDefinitions>
        <local:DemoControl x:Name="_leftControl" Grid.Column="0" />
        <local:DemoControl x:Name="_rightControl" Grid.Column="1" />
    Grid>
Window>


Dạng như sau:












Bây giờ đơn giản khi click chúng ta cần xác định click vào user control bên phải hay bên trái và chúng ta đang đề cập VisualTreeHelper.HitTest.

Test thử HitTest
-->
namespace TestHitTest
{
    ///
    /// Interaction logic for Window1.xaml
    ///
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
            MouseLeftButtonDown += new MouseButtonEventHandler(OnMouseLeftButtonDown);
        }

        void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            var pt = e.GetPosition(_grid);
            var area = new EllipseGeometry(pt, 1, 1);
            Debug.WriteLine("--- Demo hit test ---");
            VisualTreeHelper.HitTest(
                _grid,
                filter =>
                {                   
                    Debug.WriteLine("Pass through filter " + filter);
                    return HitTestFilterBehavior.Continue;
                },
                result =>
                {
                    Debug.WriteLine("Pass through result " + result.VisualHit);
                    return HitTestResultBehavior.Continue;
                },
                new GeometryHitTestParameters(area)
            );
        }
    }
}

OK run và click ta được 1 cái kết quả như sau
-->
--- Demo hit test ---
Pass through filter System.Windows.Controls.Grid
Pass through filter TestHitTest.DemoControl
Pass through filter System.Windows.Controls.Border
Pass through filter System.Windows.Controls.ContentPresenter
Pass through filter System.Windows.Controls.Grid
Pass through filter System.Windows.Shapes.Rectangle
Pass through result System.Windows.Shapes.Rectangle
Pass through filter System.Windows.Shapes.Rectangle
Pass through result System.Windows.Shapes.Rectangle
Pass through filter System.Windows.Shapes.Rectangle
Pass through result System.Windows.Shapes.Rectangle
Pass through filter System.Windows.Controls.Label: This is a demo control
Pass through filter System.Windows.Controls.Border
Pass through result System.Windows.Controls.Border

Bây giờ chỉnh 1 chút phần filter. Như đã biết filter sẽ lọc những thứ không cần thực hiện hit test trước khi cho ra kết quả. Theo code bên dưới khi đến DemoControl thì không cần duyệt children nữa.
-->
void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    var pt = e.GetPosition(_grid);
    var area = new EllipseGeometry(pt, 1, 1);
    Debug.WriteLine("--- Demo hit test ---");
    VisualTreeHelper.HitTest(
        _grid,
        filter =>
        {
            Debug.WriteLine("Pass through filter " + filter);

            if (filter.GetType().IsAssignableFrom(typeof(DemoControl)))
            {
                return HitTestFilterBehavior.ContinueSkipChildren;
            }
            else
            {
                return HitTestFilterBehavior.Continue;
            }
        },
        result =>
        {
            Debug.WriteLine("Pass through result " + result.VisualHit);
            return HitTestResultBehavior.Continue;
        },
        new GeometryHitTestParameters(area)
    );
}


Kết quả nghèo nàn như sau:

-->
--- Demo hit test ---
Pass through filter System.Windows.Controls.Grid
Pass through filter TestHitTest.DemoControl

Có nghĩa là sao,  phải chăng VisualTreeHelper.HitTest không nói rõ ràng cho lắm. Trong phần result không thèm đếm xỉa tới UserControl mà coi UserControl là một collection của các base elements. Như vậy filter chỉ dùng để hạn chế còn trong phần hit test result thì không có khái niệm user control. Trong trường hợp như vậy thì xử lý ntn?

Chúng ta sẽ thực hiện tìm parent cho hit test result
-->
public static T FindVisualParent<T>(DependencyObject child)
    where T : DependencyObject
{

    // get parent item
    DependencyObject parentObject = VisualTreeHelper.GetParent(child);

    // we’ve reached the end of the tree
    if (parentObject == null) return null;

    // check if the parent matches the type we’re looking for
    T parent = parentObject as T;

    if (parent != null)
    {
        return parent;
    }
    else
    {

        // use recursion to proceed with next level
        return FindVisualParent<T>(parentObject);
    }
}

OK lúc này hit test result sửa lại như sau (có vẻ tạm ổn):

 
-->
void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    var pt = e.GetPosition(_grid);
    var area = new EllipseGeometry(pt, 1, 1);
    bool isLeft = false;
    bool isRight = false;

    Debug.WriteLine("--- Demo hit test ---");
    VisualTreeHelper.HitTest(
        _grid,
        null,
        result =>
        {
            Debug.WriteLine("Pass through result " + result.VisualHit);
            IntersectionDetail detail = ((GeometryHitTestResult)result).IntersectionDetail;

            switch (detail)
            {

                case IntersectionDetail.FullyContains:
                case IntersectionDetail.Intersects:
                case IntersectionDetail.FullyInside:
                    Debug.WriteLine(result.VisualHit.ToString());
                    DemoControl control = FindVisualParent<DemoControl>(result.VisualHit);

                    if (control != null)
                    {

                        if (control.Equals(_leftControl))
                        {
                            isLeft = true;
                            Debug.WriteLine("Click on left control");
                        }
                        else if (control.Equals(_rightControl))
                        {
                            isRight = true;
                            Debug.WriteLine("Click on right control");
                        }
                        return HitTestResultBehavior.Stop;
                    }
                    return HitTestResultBehavior.Continue;
                default:
                    return HitTestResultBehavior.Stop;
            }
        },
        new GeometryHitTestParameters(area)
    );
}