如何使用户控件在屏幕上像窗口一样可拖动

8
我的WPF应用程序有一个UserControl,它看起来和行为像一个弹出窗口,但它不是一个窗口。这个控件没有继承自Window类,原因是它包含一个第三方虚拟屏幕键盘,而且这个控件必须在跟它所发送输入字符的TextBox控件在同一个窗口中,当你点击其按钮时,键盘控件会将输入字符发送到TextBox控件。如果键盘控件不在同一个窗口中,它甚至看不到TextBox控件。
我的问题是,在拖动对话框时性能极差。速度足够慢,以至于鼠标离开拖动区域,它就停止跟随鼠标。我需要更好的解决方法。
以下是该控件XAML的摘录:
<Grid Name="LayoutRoot">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Border Background="{DynamicResource PopupBackground}"
            BorderBrush="{DynamicResource PopupBorder}"
            BorderThickness="5,5,5,0"
            MouseLeftButtonDown="Grid_MouseLeftButtonDown"
            MouseLeftButtonUp="Grid_MouseLeftButtonUp"
            MouseMove="Grid_MouseMove">
    . . .
    </Border>
</Grid>

这里是鼠标事件处理程序:
    private void Grid_MouseLeftButtonDown( object sender, MouseButtonEventArgs e ) {
        Canvas canvas = Parent as Canvas;
        if ( canvas == null ) {
            throw new InvalidCastException( "The parent of a KeyboardPopup control must be a Canvas." );
        }
        DraggingControl = true;
        CurrentMousePosition = e.GetPosition( canvas );
        e.Handled = true;
    }

    private void Grid_MouseLeftButtonUp( object sender, MouseButtonEventArgs e ) {
        Canvas canvas = Parent as Canvas;
        if ( canvas == null ) {
            throw new InvalidCastException( "The parent of a KeyboardPopup control must be a Canvas." );
        }

        if ( DraggingControl ) {
            Point mousePosition = e.GetPosition( canvas );

            // Correct the mouse coordinates in case they go off the edges of the control
            if ( mousePosition.X < 0.0 ) mousePosition.X = 0.0; else if ( mousePosition.X > canvas.ActualWidth ) mousePosition.X = canvas.ActualWidth;
            if ( mousePosition.Y < 0.0 ) mousePosition.Y = 0.0; else if ( mousePosition.Y > canvas.ActualHeight ) mousePosition.Y = canvas.ActualHeight;

            // Compute the new Left & Top coordinates of the control
            Canvas.SetLeft( this, Left += mousePosition.X - CurrentMousePosition.X );
            Canvas.SetTop( this, Top += mousePosition.Y - CurrentMousePosition.Y );
        }
        e.Handled = true;
    }

    private void Grid_MouseMove( object sender, MouseEventArgs e ) {
        Canvas canvas = Parent as Canvas;
        if ( canvas == null ) {
            // It is not.  Throw an exception
            throw new InvalidCastException( "The parent of a KeyboardPopup control must be a Canvas." );
        }

        if ( DraggingControl && e.LeftButton == MouseButtonState.Pressed ) {
            Point mousePosition = e.GetPosition( canvas );

            // Correct the mouse coordinates in case they go off the edges of the control
            if ( mousePosition.X < 0.0 ) mousePosition.X = 0.0; else if ( mousePosition.X > canvas.ActualWidth  ) mousePosition.X = canvas.ActualWidth;
            if ( mousePosition.Y < 0.0 ) mousePosition.Y = 0.0; else if ( mousePosition.Y > canvas.ActualHeight ) mousePosition.Y = canvas.ActualHeight;

            // Compute the new Left & Top coordinates of the control
            Canvas.SetLeft( this, Left += mousePosition.X - CurrentMousePosition.X );
            Canvas.SetTop ( this, Top  += mousePosition.Y - CurrentMousePosition.Y );

            CurrentMousePosition = mousePosition;
        }
        e.Handled = true;
    }

请注意,控件必须放置在使用它的窗口中的Canvas内。
由于该类继承自UserControl,因此无法使用Window类的DragMove方法。如何提高此控件的拖动性能?是否需要使用Win32 API?

https://dev59.com/JWzXa4cB1Zd3GeqPPg4E#38830033 - Rahul Sonone
5个回答

8

根据@DmitryMartovoi的答案,我想出了一种使这个工作的方法。我仍然给Dmitry一个+1,因为没有他的贡献,我不可能想出这个方法。

我所做的是,在我的构造函数中创建了一个TranslateTransform,并将其分配给其RenderTransform属性:

RenderTransform = new TranslateTransform();

在 XAML 中,我给用户拖动整个控件的 Border 控件命名:

<Border Background="{DynamicResource PopupBackground}"
        BorderBrush="{DynamicResource PopupBorder}"
        BorderThickness="5,5,5,0"
        MouseLeftButtonDown="Grid_MouseLeftButtonDown"
        MouseLeftButtonUp="Grid_MouseLeftButtonUp"
        MouseMove="Grid_MouseMove"
        Name="TitleBorder">

    . . .
</Border>

最后,我将各种鼠标事件处理程序修改如下:
private void Grid_MouseLeftButtonDown( object sender, MouseButtonEventArgs e ) {
    CurrentMousePosition = e.GetPosition( Parent as Window );
    TitleBorder.CaptureMouse();
}

private void Grid_MouseLeftButtonUp( object sender, MouseButtonEventArgs e ) {
    if ( TitleBorder.IsMouseCaptured ) {
        TitleBorder.ReleaseMouseCapture();
    }
}

private void Grid_MouseMove( object sender, MouseEventArgs e ) {
    Vector diff = e.GetPosition( Parent as Window ) - CurrentMousePosition;
    if ( TitleBorder.IsMouseCaptured ) {
        ( RenderTransform as TranslateTransform ).X = diff.X;
        ( RenderTransform as TranslateTransform ).Y = diff.Y;
    }
}

这个方法非常好用。当你拖动Border时,整个UserControl和它的所有内容都会平滑移动,跟着鼠标走。如果你在其表面点击其他地方,整个UserControl不会移动。

再次感谢@DmitryMartovoi提供的代码。

编辑:我编辑了这个答案,因为上面的代码虽然可以运行,但不完美。它的缺陷是,当你点击标题栏区域并开始拖动前,控件会弹回到原来的位置。这很烦人,完全错误。

我想出的实际上完美工作的方法是先把控件放在一个Canvas中。父控件必须是Canvas,否则以下代码将无法工作。我还停止使用RenderTransform。我添加了一个名为canvas的私有属性,类型为Canvas。我为弹出控件添加了一个Loaded事件处理程序,进行一些重要的初始化:

private void KeyboardPopup_Loaded( object sender, RoutedEventArgs e ) {
    canvas = Parent as Canvas;
    if ( canvas == null ) {
        throw new InvalidCastException( "The parent of a KeyboardPopup control must be a Canvas." );
    }    
}

完成所有这些操作后,以下是修改后的鼠标事件处理程序:

private void TitleBorder_MouseLeftButtonDown( object sender, MouseButtonEventArgs e ) {
    StartMousePosition = e.GetPosition( canvas );
    TitleBorder.CaptureMouse();
}

private void TitleBorder_MouseLeftButtonUp( object sender, MouseButtonEventArgs e ) {
    if ( TitleBorder.IsMouseCaptured ) {
        Point mousePosition = e.GetPosition( canvas );
        Canvas.SetLeft( this, Canvas.GetLeft( this ) + mousePosition.X - StartMousePosition.X );
        Canvas.SetTop ( this, Canvas.GetTop ( this ) + mousePosition.Y - StartMousePosition.Y );
        canvas.ReleaseMouseCapture();
    }
}

private void TitleBorder_MouseMove( object sender, MouseEventArgs e ) {
    if ( TitleBorder.IsMouseCaptured && e.LeftButton == MouseButtonState.Pressed ) {
        Point mousePosition = e.GetPosition( canvas );

        // Compute the new Left & Top coordinates of the control
        Canvas.SetLeft( this, Canvas.GetLeft( this ) + mousePosition.X - StartMousePosition.X );
        Canvas.SetTop ( this, Canvas.GetTop ( this ) + mousePosition.Y - StartMousePosition.Y );
        StartMousePosition = mousePosition;
    }
}

当你第二次点击标题栏移动它时,控件会停留在你放置的位置,只有在点击标题栏时才会移动。在控件的其他任何地方单击都没有反应,而拖动则是平稳流畅的。


7
您可以简单地使用MouseDragElementBehavior
更新: 关于MouseDragElementBehavior行为的重要事项: MouseDragElementBehavior行为不能用于处理MouseClick事件的任何控件(例如Button、TextBox和ListBox控件)。如果您需要拖动这些类型中的某个控件,则将该控件作为可以拖动的控件的子级(例如一个border)即可。然后,您可以将MouseDragElementBehavior行为应用于父元素。 您也可以像这样实现自己的拖动行为:
public class DragBehavior : Behavior<UIElement>
{
    private Point elementStartPosition;
    private Point mouseStartPosition;
    private TranslateTransform transform = new TranslateTransform();

    protected override void OnAttached()
    {
        Window parent = Application.Current.MainWindow;
        AssociatedObject.RenderTransform = transform;

        AssociatedObject.MouseLeftButtonDown += (sender, e) => 
        {
            elementStartPosition = AssociatedObject.TranslatePoint( new Point(), parent );
            mouseStartPosition = e.GetPosition(parent);
            AssociatedObject.CaptureMouse();
        };

        AssociatedObject.MouseLeftButtonUp += (sender, e) =>
        {
            AssociatedObject.ReleaseMouseCapture();
        };

        AssociatedObject.MouseMove += (sender, e) =>
        {
            Vector diff = e.GetPosition( parent ) - mouseStartPosition;
            if (AssociatedObject.IsMouseCaptured)
            {
                transform.X = diff.X;
                transform.Y = diff.Y;
            }
        };
    }
}

1
虽然这个方法可以工作,但是它存在问题。首先,它只适用于您添加行为的元素及其子元素。一开始,我将行为添加到控件上的“标题栏”背景所在的Border中。只有Border可以拖动,而控件的其余部分则保持不变。当我将该行为应用于更高级别的元素,例如UserControl本身或包含所有内容的Grid时,如果您拖动其中任何内容,包括背景和任何组成键的按钮,则该行为会起作用。这也是不可接受的。 - Tony Vitabile
1
有没有办法让拖动整个控件,而不让用户在背景或模板中拖动其他控件? - Tony Vitabile
1
你不能直接拖动处理MouseClick事件的控件。请参见更新。 - Dzmitry Martavoi
1
谢谢,但是那个行为和MouseElementDragBehavior有完全相同的问题。请注意,我没有将该行为应用于任何按钮;即使Button是应用行为的控件的子级,您也可以通过它拖动整个控件。我需要这个行为的工作方式与拖动窗口的方式完全相同:只能从标题栏拖动。我的控件上有一个应该像标题栏一样工作的区域。我只需要让整个UserControl在拖动该区域时跟随它移动。 - Tony Vitabile

2

0

我的解决方案是结合了@DmitryMartovoi和这个线程的方法:https://www.codeproject.com/Questions/1014138/Csharp-WPF-RenderTransform-resets-on-mousedown

我唯一的改变是在左键鼠标按下时,与@DmitryMartovoi的答案不同。这样可以防止在第一次单击时出现瞬移。为此,您还需要Systems.Windows.Interactivity.WPF Nuget包。

AssociatedObject.MouseLeftButtonDown += (sender, e) =>
{
    var mousePos = e.GetPosition(parent);
    mouseStartPosition = new Point(mousePos.X-transform.X, mousePos.Y-transform.Y);
    AssociatedObject.CaptureMouse();
};

-1

只需实现MouseDown事件,并在实现中添加this.DragMove();

示例:

<StackPanel MouseDown="UIElement_OnMouseDown">

</StackPanel>


void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e)
{
    this.DragMove();
}

它明确指出他不能使用DragMove,因为它是从UserControl继承的,所以这个答案是不正确的。 - Alejandro Vicaria

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接