How do I make a WPF window movable by dragging the extended window frame?
Asked Answered
W

4

55

In applications like Windows Explorer and Internet Explorer, one can grab the extended frame areas beneath the title bar and drag windows around.

For WinForms applications, forms and controls are as close to native Win32 APIs as they can get; one would simply override the WndProc() handler in their form, process the WM_NCHITTEST window message and trick the system into thinking a click on the frame area was really a click on the title bar by returning HTCAPTION. I've done that in my own WinForms apps to delightful effect.

In WPF, I can also implement a similar WndProc() method and hook it to my WPF window's handle while extending the window frame into the client area, like this:

// In MainWindow
// For use with window frame extensions
private IntPtr hwnd;
private HwndSource hsource;

private void Window_SourceInitialized(object sender, EventArgs e)
{
    try
    {
        if ((hwnd = new WindowInteropHelper(this).Handle) == IntPtr.Zero)
        {
            throw new InvalidOperationException("Could not get window handle for the main window.");
        }

        hsource = HwndSource.FromHwnd(hwnd);
        hsource.AddHook(WndProc);

        AdjustWindowFrame();
    }
    catch (InvalidOperationException)
    {
        FallbackPaint();
    }
}

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    switch (msg)
    {
        case DwmApiInterop.WM_NCHITTEST:
            handled = true;
            return new IntPtr(DwmApiInterop.HTCAPTION);

        default:
            return IntPtr.Zero;
    }
}

The problem is that, since I'm blindly setting handled = true and returning HTCAPTION, clicking anywhere but the window icon or the control buttons causes the window to be dragged. That is, everything highlighted in red below causes dragging. This even includes the resize handles at the sides of the window (the non-client area). My WPF controls, namely the text boxes and the tab control, also stop receiving clicks as a result:

What I want is for only

  1. the title bar, and
  2. the regions of the client area...
  3. ... that aren't occupied by my controls

to be draggable. That is, I only want these red regions to be draggable (client area + title bar):

How do I modify my WndProc() method and the rest of my window's XAML/code-behind, to determine which areas should return HTCAPTION and which shouldn't? I'm thinking something along the lines of using Points to check the location of the click against the locations of my controls, but I'm not sure how to go about it in WPF land.

EDIT [4/24]: one simple way about it is to have an invisible control, or even the window itself, respond to MouseLeftButtonDown by invoking DragMove() on the window (see Ross's answer). The problem is that for some reason DragMove() doesn't work if the window is maximized, so it doesn't play nice with Windows 7 Aero Snap. Since I'm going for Windows 7 integration, it's not an acceptable solution in my case.

Woodruff answered 30/3, 2011 at 22:18 Comment(2)
This question includes a brief tutorial on handling windows messages in C#, it has well-crafted illustrations that indicate exactly what is being asked, and there are no obvious typos. +1, baby!Endaendall
@Jeffrey L Whitledge: Gee, thanks (+1 to you)! The one thing I did have to edit in the end was the question title... I swear the previous title wasn't what I wrote before posting.Woodruff
W
32

Sample code

Thanks to an email I got this morning, I was prompted to make a working sample app demonstrating this very functionality. I've done that now; you can find it on GitHub (or in the now-archived CodePlex). Just clone the repository or download and extract an archive, then open it in Visual Studio, and build and run it.

The complete application in its entirety is MIT-licensed, but you'll probably be taking it apart and putting bits of its code around your own rather than using the app code in full — not that the license stops you from doing that either. Also, while I know the design of the application's main window isn't anywhere near similar to the wireframes above, the idea is the same as posed in the question.

Hope this helps somebody!

Step-by-step solution

I finally solved it. Thanks to Jeffrey L Whitledge for pointing me in the right direction! His answer was accepted because if not for it I wouldn't have managed to work out a solution. EDIT [9/8]: this answer is now accepted as it's more complete; I'm giving Jeffrey a nice big bounty instead for his help.

For posterity's sake, here's how I did it (quoting Jeffrey's answer where relevant as I go):

Get the location of the mouse click (from the wParam, lParam maybe?), and use it to create a Point (possibly with some kind of coordinate transformation?).

This information can be obtained from the lParam of the WM_NCHITTEST message. The x-coordinate of the cursor is its low-order word and the y-coordinate of the cursor is its high-order word, as MSDN describes.

Since the coordinates are relative to the entire screen, I need to call Visual.PointFromScreen() on my window to convert the coordinates to be relative to the window space.

Then call the static method VisualTreeHelper.HitTest(Visual,Point) passing it this and the Point that you just made. The return value will indicate the control with the highest Z-Order.

I had to pass in the top-level Grid control instead of this as the visual to test against the point. Likewise I had to check whether the result was null instead of checking if it was the window. If it's null, the cursor didn't hit any of the grid's child controls — in other words, it hit the unoccupied window frame region. Anyway, the key was to use the VisualTreeHelper.HitTest() method.

Now, having said that, there are two caveats which may apply to you if you're following my steps:

  1. If you don't cover the entire window, and instead only partially extend the window frame, you have to place a control over the rectangle that's not filled by window frame as a client area filler.

    In my case, the content area of my tab control fits that rectangular area just fine, as shown in the diagrams. In your application, you may need to place a Rectangle shape or a Panel control and paint it the appropriate color. This way the control will be hit.

    This issue about client area fillers leads to the next:

  2. If your grid or other top-level control has a background texture or gradient over the extended window frame, the entire grid area will respond to the hit, even on any fully transparent regions of the background (see Hit Testing in the Visual Layer). In that case, you'll want to ignore hits against the grid itself, and only pay attention to the controls within it.

Hence:

// In MainWindow
private bool IsOnExtendedFrame(int lParam)
{
    int x = lParam << 16 >> 16, y = lParam >> 16;
    var point = PointFromScreen(new Point(x, y));

    // In XAML: <Grid x:Name="windowGrid">...</Grid>
    var result = VisualTreeHelper.HitTest(windowGrid, point);

    if (result != null)
    {
        // A control was hit - it may be the grid if it has a background
        // texture or gradient over the extended window frame
        return result.VisualHit == windowGrid;
    }

    // Nothing was hit - assume that this area is covered by frame extensions anyway
    return true;
}

The window is now movable by clicking and dragging only the unoccupied areas of the window.

But that's not all. Recall in the first illustration that the non-client area comprising the borders of the window was also affected by HTCAPTION so the window was no longer resizable.

To fix this I had to check whether the cursor was hitting the client area or the non-client area. In order to check this I needed to use the DefWindowProc() function and see if it returned HTCLIENT:

// In my managed DWM API wrapper class, DwmApiInterop
public static bool IsOnClientArea(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam)
{
    if (uMsg == WM_NCHITTEST)
    {
        if (DefWindowProc(hWnd, uMsg, wParam, lParam).ToInt32() == HTCLIENT)
        {
            return true;
        }
    }

    return false;
}

// In NativeMethods
[DllImport("user32.dll")]
private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam);

Finally, here's my final window procedure method:

// In MainWindow
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    switch (msg)
    {
        case DwmApiInterop.WM_NCHITTEST:
            if (DwmApiInterop.IsOnClientArea(hwnd, msg, wParam, lParam)
                && IsOnExtendedFrame(lParam.ToInt32()))
            {
                handled = true;
                return new IntPtr(DwmApiInterop.HTCAPTION);
            }

            return IntPtr.Zero;

        default:
            return IntPtr.Zero;
    }
}
Woodruff answered 31/3, 2011 at 2:38 Comment(2)
brain fried, core dumped. What a great solution you came up with. I beg the Lord to never deal with a similar requirement. +1.Pasteurize
@Markust: You made my afternoon with the core-dump pun.Woodruff
E
16

Here's something you could try:

Get the location of the mouse click (from the wParam, lParam maybe?), and use it to create a Point (possibly with some kind of coordinate transformation?).

Then call the static method VisualTreeHelper.HitTest(Visual,Point) passing it this and the Point that you just made. The return value will indicate the control with the highest Z-Order. If that's your window, then do your HTCAPTION voodoo. If it's some other control, then...don't.

Good luck!

Endaendall answered 30/3, 2011 at 22:53 Comment(2)
Here's the tricky part: WinForms' Control class has a PointToClient() method that performs the transformation needed. But WPF's Visual class doesn't have that method. Presumably because it's, well, WPF.Woodruff
I'm making my answer the accepted one for completeness. As a way of thanks, have a bounty!Woodruff
C
6

Looking to do the same thing (make my extended Aero glass draggable in my WPF app), I just came across this post via Google. I read through your answer, but decided to keep searching to see if there was anything simpler.

I found a much less code-intensive solution.

Simply create a transparent item behind your controls, and give it a left mouse button down event handler which calls the window's DragMove() method.

Here's the section of my XAML which appears over my extended Aero glass:

<Grid DockPanel.Dock="Top">
    <Border MouseLeftButtonDown="Border_MouseLeftButtonDown" Background="Transparent" />
    <Grid><!-- My controls are in here --></Grid>
</Grid>

And the code-behind (this is within a Window class, and so DragMove() is available to call directly):

private void Border_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    DragMove();
}

And that's it! For your solution, you would have to add more than one of these to achieve your non-rectangular draggable area.

Chaves answered 24/4, 2011 at 21:59 Comment(5)
I considered DragMove() too, but the problem with it is that it doesn't work when the window is maximized, so it doesn't really play nice with Aero Snap.Woodruff
You're right. I just tested Aero Snap with my solution and while it works no problem for maximizing and docking to left/right, it does not work for "unsnapping" from a maximized state.Chaves
Then again, Aero Snap is a Windows 7 feature, and if it doesn't matter that Aero Snap doesn't work, DragMove() is alright.Woodruff
Out of interest - does your solution work for "double-click to maximize"?Chaves
Yup, it does. Since I'm having my window procedure send fake HTCAPTIONs on clicking the glass area, the glass area behaves in every way like the title/caption bar.Woodruff
D
1

simple way is create stackpanel or every thing you want for your titlebar XAML

 <StackPanel Name="titleBar" Background="Gray" MouseLeftButtonDown="titleBar_MouseLeftButtonDown" Grid.ColumnSpan="2"></StackPanel>

code

  private void titleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
     {
         DragMove();
     }
Deputation answered 17/11, 2013 at 11:51 Comment(1)
How is this different from Ross's answer that was posted 2 and a half years ago (other than the use of a StackPanel instead of a Border)?Woodruff

© 2022 - 2024 — McMap. All rights reserved.