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

Posted

技术标签:

【中文标题】如何使用户控件像窗口一样在屏幕上可拖动【英文标题】:How to make a user control draggable on screen like a window 【发布时间】:2012-12-01 16:15:45 【问题描述】:

我的 WPF 应用程序有一个 UserControl,它的外观和行为应该像一个弹出窗口,但它不是一个窗口。控件不来自Window 类的原因是它包含第三方虚拟屏幕键盘,并且该控件必须与将输入字符发送到的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 内。

我不能使用DragMove,因为它是Window 类的方法,而这个类是UserControl 的后代。如何提高此控件的拖动性能?我必须求助于 Win32 API 吗?

【问题讨论】:

***.com/a/38830033/2507782 【参考方案1】:

您可以简单地使用MouseDragElementBehavior。

UPD 关于MouseDragElementBehavior 行为的重要事项:

MouseDragElementBehavior 行为不适用于处理 MouseClick 事件的任何控件(例如,Button、TextBox 和 ListBox 控件)。如果您需要能够拖动其中一种类型的控件,请将该控件设置为可拖动控件(例如边框)的子控件。然后,您可以将 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;
            
        ;
    

【讨论】:

虽然这可行,但它有问题。第一个是它仅适用于您将行为添加到的元素及其子元素。起初,我将行为添加到Border,它是控件上“标题栏”的背景。只有Border 是可拖动的,其余控件保持不变。当我将行为应用于更高级别的东西时,例如UserControl 本身或包含所有内容的Grid,如果您在其中拖动任何内容,包括背景和构成按键的任何按钮,该行为就会起作用。这也是不能接受的。 有没有一种方法可以在不让用户拖动背景或模板中的其他控件的情况下让整个控件的拖动工作? 您不能直接拖动处理 MouseClick 事件的控件。见更新。 谢谢,但这种行为与MouseElementDragBehavior 有同样的问题。请注意,我没有将行为应用于任何按钮;只是即使Button 是应用该行为的控件的子控件,您也可以通过它拖动整个控件。我需要这种行为与拖动窗口的工作方式完全相同:您只能从标题栏中拖动。我的控件上有一个区域,应该像标题栏一样。我只需要做到这一点,以便整个 UserControl 在拖动时跟随该区域。【参考方案2】:

根据@DmitryMartovoi 的回答中的信息,我想出了一种方法来完成这项工作。我仍然给 Dmitry 一个 +1,因为如果没有他的贡献,我将无法解决这个问题。

我所做的是我在 UserControl's 构造函数中创建了一个 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;
    

当您再次单击标题栏以移动它时,控件将停留在您放置它的位置,并且仅当您单击标题栏时它才会移动。单击控件中的任何其他位置都不会执行任何操作,并且拖动非常流畅且响应迅速。

【讨论】:

【参考方案3】:

http://www.codeproject.com/Tips/442276/Drag-and-Drop-WPF-Controls 这是我花了很多时间后得到的很棒的解决方案。尽管此处显示的示例是普通控件,但经过一些更改后,您也可以使其适用于用户控件。

【讨论】:

【参考方案4】:

我对此的解决方案是@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();
;

【讨论】:

【参考方案5】:

只需实现MouseDown 事件并在实现中放入this.DragMove();

例子:

<StackPanel MouseDown="UIElement_OnMouseDown">

</StackPanel>

void UIElement_OnMouseDown(object sender, MouseButtonEventArgs e)

    this.DragMove();

【讨论】:

它特别说他不能使用 DragMove,因为它是从 UserControl 继承的,所以这个答案是不正确的。

以上是关于如何使用户控件像窗口一样在屏幕上可拖动的主要内容,如果未能解决你的问题,请参考以下文章

如何使fabricJS画布在移动设备上可水平拖动?

WPF 窗口中的可拖动和自动调整用户控件

如何允许用户在他选择的位置拖动动态创建的控件

如何相对缩放用户控件的大小?

wpf怎么实现主窗口向用户控件传值?

将UserControl显示为弹出窗口