| In search of the perfect dock |
The ability to grab part of a window and drag it somewhere else has never been easy. WPF does not offer this out of the box, and much effort has been expended by a number of companies and individuals to mimic the docking offered by Visual Studio. Personally, I think Visual Studio's docking is rather cumbersome. The amount of code required to do it is nothing short of excessive. If you want this kind of docking, have a look at Avalon Dock, which provides all you need for free. Some commercial packages offer the same type of docking, but who am I to suggest that they are selling you what you can get for nothing?
I prefer the docking offered by programs such as Adobe Bridge, where objects are dragged into the space between two other items, or dropped directly over them to give a tabbed offering. In reality, this is little different to the Visual Studio method, but it seems a little more natural to me.
Neither method allows you to drag and drop a detached pane into the space indicated by the dotted line in the diagram below.
This is not a major failing, but it does point towards a limitation in current implementations.
Having had the luxury of being able to see other people's struggles - which always makes the problem easier - I believe docking should work like this:
I have omitted all of the other options for clarity.
The challenges are as follow:
I shall break the remaining discussion into a series of steps as follows:
| 1. Detaching a control |
The goal here is to pick up and drag a pane either within the application, or outside of the main application window to create a new window. To implement dragging and dropping solely within the main application window would not be difficult, but the goal here is to be able to detach a window and put it on another screen in a multi-screen setup, so that (for example) code can be edited on one window, and the results can be watched on another.
This part of the problem is not too onerous. On the mousedown, using hit-testing or some other mechanism, determine the control to be moved into the flyout window, detatch it, and add it to the flyout. The actual window creation and switching of focus is done in the mousemove. Here is the mouse move code, where lastElementSelected is set in the MouseDown event.
private void Window_MouseMove(object sender, MouseEventArgs e)
{
// set default height and width
double height = 200;
double width = 150;
Point start=new Point(0,0);
if (lastElementSelected != null && e.LeftButton==MouseButtonState.Pressed) // put it in a window
{
UIElement el = lastElementSelected;
lastElementSelected = null;
if (el is Canvas)
{
// if the control is a Canvas, alter the size of the window accordingly
// note: at present, I only use the CAnvas and TabControl classes
Canvas canvas = (Canvas)el;
width = canvas.ActualWidth;
height = canvas.ActualHeight;
Point pt = new Point(Canvas.GetLeft(canvas), Canvas.GetTop(canvas));
start=canvasRoot.PointToScreen(pt);
}
// create a new window
DetachableWindow window = new DetachableWindow();
window.Height = height;
window.Width = width;
window.WindowStyle = WindowStyle.None;
// get the content and put it in the new window
canvasRoot.Children.Remove(el);
window.AddContent(el);
window.Left = start.X - 4; // assuming the border is 4 pixels
window.Top = start.Y - 4;
window.Show();
// create handlers for later dropping
window.LocationChanged += new EventHandler(window_LocationChanged);
window.DockMe += new DockWindow(window_DockMe); // Note 1
window.MouseLeftButtonUp += new MouseButtonEventHandler(window_MouseLeftButtonUp);
// get a handle to the new window, and send it a Windows message equivalent to
// clicking the window's caption
IntPtr handle = new WindowInteropHelper(window).Handle; // handle=hwnd
Win32.ReleaseCapture();
window.Focus();
Win32.SendMessage(handle, Win32.WM_NCLBUTTONDOWN, new IntPtr(Win32.HTCAPTION), new IntPtr(0));
}
}
The last line of this code snippet, using the Windows API SendMessage function, switches the focus to the flyout window and prepares it to be moved with the mouse. When the mouse is released, the control will either be re-docked into the existing window, or will remain in the separated window. To allow the new window to be dragged around, the WindowsStyle is set so a normal border is visible.
I had hoped to handle the windows message WM_NCLBUTTONUP, but this message is filtered out somewhere by WPF, so I made do in the end with handling the WM_EXITSIZEMOVE message, and setting the border using:
this.WindowStyle = WindowStyle.SingleBorderWindow;
I appreciate this piece of code on its own is not a full treatment, but I shall hold off posting any full solution until I am happy with all of the methods I have used.
| 2. Reattaching a control |
In the same piece of code which handles the windows message WM_EXITSIZEMOVE, an event is raised - see Note 1 in the code snippet above - to dock the window being dragged. The relevant code to hook the windows message processing, and the code to handle the windows messages is this:
bool primed = false; private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { short x = (short)((lParam.ToInt32() & 0xFFFF)); short y = (short)((lParam.ToInt32() >> 16)); switch (msg) { case Win32.WM_NCLBUTTONDOWN: //case WM_NCLBUTTONDOWN: if (wParam.ToInt32() == Win32.HTCAPTION) { primed = true; } break; case Win32.WM_EXITSIZEMOVE: if (Mouse.LeftButton == MouseButtonState.Released && primed) { primed = false; // raise try and dock event bool success = false; DockMe(this, new Point(this.Left, this.Top), ref success); if (success) { IntPtr handle = new WindowInteropHelper(this).Handle; // handle=hwnd Win32.PostMessage(handle, Win32.WM_CLOSE, IntPtr.Zero, IntPtr.Zero); } else { this.WindowStyle = WindowStyle.SingleBorderWindow; Console.WriteLine("Setting border style"); } } break; } return IntPtr.Zero; } private void Window_Loaded(object sender, RoutedEventArgs e) { HwndSource source = HwndSource.FromHwnd(new WindowInteropHelper(this).Handle); source.AddHook(new HwndSourceHook(WndProc)); }
Reattaching the code is a matter of reversing the actions in the first code snippet, providing the window is being dropped onto a relevant dockable area. To decide which dockable area is the right one, first I need to decide how the dockable window is laid out, and as soon as I have done, I shall post it here.
I have two options:
The benefit of the first method is that is supports splitters with little extra work. It also allows the controls to be docked, and isn't too hard to code. The drawback is that the delineations between the controls are either vertical or horizontal and the page must be subdivided into ever smaller regions.
| 3. Layout |
I have decided to base the design upon nested grids, similar to the AvalonDock method, but not quite the same, as I shall use grids with 1 column and 2 or more rows, or 1 row and 2 or more columns. This will simplify grids which become overcomplex using only 2 panes per splitter.
| 4. Hovering |
When a detatched window is picked up and dragged over the docking grid, a highlight appears showing where the window will be docked if it is dropped. I had originally used a Popup() for this, but the Popup was always on top, so I now use a Window() instead. It is semi transparent and has an animated green glowing background.