使WPF窗口可拖动,无论点击哪个元素

136

我的问题有两个方面,我希望在WPF中提供比WinForms标准解决方案更简单的解决方案(在我做出这个澄清之前,Christophe Geers提供了标准解决方案)。

首先,是否有一种方法可以使窗口可拖动而无需捕获和处理鼠标单击+拖动事件?我的意思是窗口可通过标题栏进行拖动,但如果我设置一个窗口不具有标题栏并仍然想能够拖动它,是否有一种方法只是将事件重定向到处理标题栏拖动的内容?

其次,是否有一种方法可以将事件处理程序应用于窗口中的所有元素?也就是说,使窗口可以被拖动,无论用户点击+拖动哪个元素。显然,不需要手动向每个元素添加处理程序。只需要在某个地方执行一次即可?

10个回答

347

可以使用以下代码将 MouseDown 事件应用于你的 Window

private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
    if (e.ChangedButton == MouseButton.Left)
        this.DragMove();
}

用户可以在任何控件上单击/拖动并拖动窗口,但不能在吃掉MouseDown事件的控件上拖动窗口 (e.Handled = true)。

您可以使用PreviewMouseDown代替MouseDown,但是拖动事件会吃掉Click事件,因此您的窗口将停止响应左键单击事件。如果您真的想从任何控件中单击并拖动窗体,那么您可以使用PreviewMouseDown,启动一个计时器以开始拖动操作,并在X毫秒内取消操作,如果MouseUp事件触发,则取消操作。


2
@Drowin 你可能可以使用那个事件,但一定要先测试它,因为MouseLeftButtonDown具有直接路由策略,而MouseDown具有冒泡路由策略。请参阅MSDN页面上关于MouseLeftButtonDown的备注部分以获取更多信息,并了解如果您将使用MouseLeftButtonDown而不是MouseDown时需要注意的一些额外事项。 - Rachel
2
@Rahul 拖动一个用户控件要困难得多... 你需要将其放置在像 Canvas 这样的父面板中,并手动设置 X/Y(或 Canvas.Top 和 Canvas.Left)属性,当用户移动鼠标时进行更新。上次我使用了鼠标事件,在 OnMouseDown 中捕获位置并注册移动事件,在 OnMouseMove 中更改 X/Y,在 OnMouseUp 中移除移动事件。这就是基本的思路 :) - Rachel
是的,@Rachel,我已经使用stackoverflow.com/questions/6284056/dragging-a-wpf-user-control链接的帮助完成了这个任务。无论如何,感谢您的建议。 - Rahul
我有一个自定义的模态消息框,可以位于父窗口的左上角、顶部中心、右上角、中间左侧、中间中心、中间右侧、底部左侧、底部中心和底部右侧,但我也希望让用户自由移动这个模态消息框,你的解决方案对我来说非常完美!正是我一直在寻找的。谢谢! - Willy
在这种情况下,触摸屏上的手指算作左键点击吗? - undefined
显示剩余5条评论

10

如果需要使WPF窗体可以在任何地方拖动,一个简单的解决方案是使用委托来触发DragMove()方法,可以在Windows的OnLoad事件或Grid的load事件中实现。

private void Grid_Loaded(object sender, RoutedEventArgs 
{
      this.MouseDown += delegate{DragMove();};
}

2
我将这个添加到构造函数中。非常好用。 - Joe Johnston
3
如果您在窗体上的任何位置右键单击,这将抛出异常,因为只有在按下主鼠标按钮时才能调用DragMove - Stjepan Bakrac
1
更好的检查ChangedButton this.MouseDown += delegate (object sender, MouseButtonEventArgs e) { if (e.ChangedButton == MouseButton.Left) DragMove(); }; - Charles Chou

8
有时候,我们无法访问Window,例如,如果我们正在使用DevExpress,那么只有一个UIElement是可用的。
步骤1:添加附加属性
解决方案是:
1. 钩入MouseMove事件; 2. 向上搜索可视树,直到找到第一个父级Window; 3. 在我们新发现的Window上调用.DragMove()
代码:
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;

namespace DXApplication1.AttachedProperty
{
    public class EnableDragHelper
    {
        public static readonly DependencyProperty EnableDragProperty = DependencyProperty.RegisterAttached(
            "EnableDrag",
            typeof (bool),
            typeof (EnableDragHelper),
            new PropertyMetadata(default(bool), OnLoaded));

        private static void OnLoaded(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            var uiElement = dependencyObject as UIElement;
            if (uiElement == null || (dependencyPropertyChangedEventArgs.NewValue is bool) == false)
            {
                return;
            }
            if ((bool)dependencyPropertyChangedEventArgs.NewValue  == true)
            {
                uiElement.MouseMove += UIElementOnMouseMove;
            }
            else
            {
                uiElement.MouseMove -= UIElementOnMouseMove;
            }

        }

        private static void UIElementOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
        {
            var uiElement = sender as UIElement;
            if (uiElement != null)
            {
                if (mouseEventArgs.LeftButton == MouseButtonState.Pressed)
                {
                    DependencyObject parent = uiElement;
                    int avoidInfiniteLoop = 0;
                    // Search up the visual tree to find the first parent window.
                    while ((parent is Window) == false)
                    {
                        parent = VisualTreeHelper.GetParent(parent);
                        avoidInfiniteLoop++;
                        if (avoidInfiniteLoop == 1000)
                        {
                            // Something is wrong - we could not find the parent window.
                            return;
                        }
                    }
                    var window = parent as Window;
                    window.DragMove();
                }
            }
        }

        public static void SetEnableDrag(DependencyObject element, bool value)
        {
            element.SetValue(EnableDragProperty, value);
        }

        public static bool GetEnableDrag(DependencyObject element)
        {
            return (bool)element.GetValue(EnableDragProperty);
        }
    }
}

步骤二:为任何元素添加附加属性,使其可以拖动窗口

如果我们添加此附加属性,则用户可以通过单击特定元素来拖动整个窗口:

<Border local:EnableDragHelper.EnableDrag="True">
    <TextBlock Text="Click me to drag this entire window"/>
</Border>

附录A:可选高级示例

在这个DevExpress的示例中,我们用自己的灰色矩形替换了停靠窗口的标题栏,然后确保如果用户单击并拖动该灰色矩形,则窗口将正常拖动:

<dx:DXWindow x:Class="DXApplication1.MainWindow" Title="MainWindow" Height="464" Width="765" 
    xmlns:dx="http://schemas.devexpress.com/winfx/2008/xaml/core" 
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    xmlns:dxdo="http://schemas.devexpress.com/winfx/2008/xaml/docking" 
    xmlns:local="clr-namespace:DXApplication1.AttachedProperty"
    xmlns:dxdove="http://schemas.devexpress.com/winfx/2008/xaml/docking/visualelements"
    xmlns:themeKeys="http://schemas.devexpress.com/winfx/2008/xaml/docking/themekeys">

    <dxdo:DockLayoutManager FloatingMode="Desktop">
        <dxdo:DockLayoutManager.FloatGroups>
            <dxdo:FloatGroup FloatLocation="0, 0" FloatSize="179,204" MaxHeight="300" MaxWidth="400" 
                             local:TopmostFloatingGroupHelper.IsTopmostFloatingGroup="True"                             
                             >
                <dxdo:LayoutPanel ShowBorder="True" ShowMaximizeButton="False" ShowCaption="False" ShowCaptionImage="True" 
                                  ShowControlBox="True" ShowExpandButton="True" ShowInDocumentSelector="True" Caption="TradePad General" 
                                  AllowDock="False" AllowHide="False" AllowDrag="True" AllowClose="False"
                                  >
                    <Grid Margin="0">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto"/>
                            <RowDefinition Height="*"/>
                        </Grid.RowDefinitions>
                        <Border Grid.Row="0" MinHeight="15" Background="#FF515151" Margin="0 0 0 0"
                                                                  local:EnableDragHelper.EnableDrag="True">
                            <TextBlock Margin="4" Text="General" FontWeight="Bold"/>
                        </Border>
                        <TextBlock Margin="5" Grid.Row="1" Text="Hello, world!" />
                    </Grid>
                </dxdo:LayoutPanel>
            </dxdo:FloatGroup>
        </dxdo:DockLayoutManager.FloatGroups>
    </dxdo:DockLayoutManager>
</dx:DXWindow>

免责声明:我与DevExpress无关。这种技术适用于任何用户元素,包括标准WPFTelerik(另一个出色的WPF库提供者)。

1
这正是我想要的。在我看来,所有的WPF代码后台都应该编写为附加行为。 - fjch1997
1
@Contango,你的解决方案是最好的。 - Lucy82
1
真不敢相信居然成功了。 - bzmind
有时候在window.DragMove();这一行上会随机抛出异常(尤其是在应用刚加载时),异常信息是System.InvalidOperationException: 'Dispatcher processing has been suspended, but messages are still being processed.'我尝试使用了从另一个答案中复制粘贴的修复方法Dispatcher.CurrentDispatcher.Invoke(DispatcherPriority.Normal, () => { window?.DragMove(); });但是拖动变得非常卡顿,无法使用。你能帮忙看一下吗?因为我的应用程序在这个功能上有很大依赖。谢谢。 - undefined

6
private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
    this.DragMove();
}

在某些情况下抛出异常更加安全(例如,如果在窗口上还有一个可点击的图像,当单击时会打开一个消息框。当从消息框退出时,您将收到错误信息)。

更安全的做法是使用:

private void Window_MouseDown(object sender, MouseButtonEventArgs e)
{
if (Mouse.LeftButton == MouseButtonState.Pressed)
            this.DragMove();
}

所以你确定那个时刻左键被按下了。


我正在使用 e.LeftButton 而不是 Mouse.LeftButton,以便特别使用与事件参数相关联的按钮,尽管这可能永远不会有影响。 - Fls'Zen

4

正如@fjch1997所提到的那样,实现一种行为是很方便的。这里是实现的核心逻辑,与@loi.efy的答案相同:

public class DragMoveBehavior : Behavior<Window>
{
    protected override void OnAttached()
    {
        AssociatedObject.MouseMove += AssociatedObject_MouseMove;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
    }

    private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed && sender is Window window)
        {
            // In maximum window state case, window will return normal state and
            // continue moving follow cursor
            if (window.WindowState == WindowState.Maximized)
            {
                window.WindowState = WindowState.Normal;

                // 3 or any where you want to set window location after
                // return from maximum state
                Application.Current.MainWindow.Top = 3;
            }

            window.DragMove();
        }
    }
}

使用方法:

<Window ...
        xmlns:h="clr-namespace:A.Namespace.Of.DragMoveBehavior"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity">
    <i:Interaction.Behaviors>
        <h:DragMoveBehavior />
    </i:Interaction.Behaviors>
    ...
</Window>

3
这是所有需要的内容!
private void UiElement_MouseMove(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed)
        {
            if (this.WindowState == WindowState.Maximized) // In maximum window state case, window will return normal state and continue moving follow cursor
            {
                this.WindowState = WindowState.Normal;
                Application.Current.MainWindow.Top = 3;// 3 or any where you want to set window location affter return from maximum state
            }
            this.DragMove();
        }
    }

2
可以通过单击表单的任何位置来拖放表单,而不仅仅是标题栏。如果您有一个无边框的表单,这很方便。这篇 CodeProject 上的文章演示了一种可能的解决方案:http://www.codeproject.com/KB/cs/DraggableForm.aspx。基本上创建了 Form 类型的子类,在其中处理鼠标按下、抬起和移动事件。
- 鼠标按下:记住位置 - 鼠标移动:存储新位置 - 鼠标抬起:将表单定位到新位置
这里有一个类似的解决方案在视频教程中解释:http://www.youtube.com/watch?v=tJlY9aX73Vs。当用户单击表单中的控件时,我不会允许拖动表单。用户单击不同的控件时期望不同的结果。当我的表单因为我单击列表框、按钮、标签等而突然开始移动时,那将是令人困惑的。

当然,它不会通过单击任何控件而移动,但是如果您单击并拖动,您是否期望表单移动。我的意思是,例如,如果您单击+拖动按钮或列表框,您不会期望它们移动,但如果您尝试在表单中单击和拖动按钮,则表单的运动是一种自然的期望,我认为。 - Alex K
猜想,那只是个人口味。无论如何...控件需要处理相同的鼠标事件。由于它们不会冒泡,您必须通知父窗体这些事件。 - Christophe Geers
此外,虽然我知道WinForms解决方案,但我希望在WPF中存在更简单的方法,我想我应该在问题中更清楚地表达这一点(现在只是一个标记)。 - Alex K
抱歉,我的错。没有注意到WPF标签。原问题中没有提到。我默认认为是WinForms,忽略了标签。 - Christophe Geers

1
<Window
...
WindowStyle="None" MouseLeftButtonDown="WindowMouseLeftButtonDown"/>
<x:Code>
    <![CDATA[            
        private void WindowMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            DragMove();
        }
    ]]>
</x:Code>

源代码


0

将此添加到您的窗口样式中(我认为属性是不言自明的)

<Setter Property="WindowChrome.WindowChrome">
  <Setter.Value>
    <WindowChrome GlassFrameThickness="0" ResizeBorderThickness="3" CornerRadius="0" CaptionHeight="40" />
  </Setter.Value>
</Setter>

0

最有用的方法,无论是针对WPF还是Windows Form,以下是WPF示例:

    [DllImport("user32.dll")]
    public static extern IntPtr SendMessage(IntPtr hWnd, int wMsg, int wParam, int lParam);

    public static void StartDrag(Window window)
    {
        WindowInteropHelper helper = new WindowInteropHelper(window);
        SendMessage(helper.Handle, 161, 2, 0);
    }

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