Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dockable Windows. Floating Window and MainWindow Menu Integration

In Visual Studio 2010, Dockable Windows seem to work like expected in every situation.
If a "Floating" document is active and some menu is selected (e.g Edit -> Paste), then the "Floating" document still has Focus and the command will be executed against that "Floating" window. Also, notice how this is clearly visible in the UI. MainWindow.xaml is still active and the Main window in Visual Studio is inactive even though the Team-menu is selected.

enter image description here

I've been trying to get the same behavior using alot of different 3rd-party docking components but they all have the same problem: once I select the menu, the MainWindow is focused and my floating window does not have focus anymore. Does anyone know of a way to get the same behavior here as in Visual Studio?

At the moment I'm using Infragistics xamDockManager and the problem can be reproduced with the following sample code.

  • Right click "Header 1" and select "Float"
  • Click the "File" menu
  • Notice how MainWindow receives focus.

xmlns:igDock="http://infragistics.com/DockManager"

<DockPanel LastChildFill="True">
    <Menu DockPanel.Dock="Top">
        <MenuItem Header="_File">
            <MenuItem Header="_New"/>
        </MenuItem>
    </Menu>
    <Grid>
        <igDock:XamDockManager x:Name="dockManager" Theme="Aero">
            <igDock:DocumentContentHost>
                <igDock:SplitPane>
                    <igDock:TabGroupPane>
                        <igDock:ContentPane Header="Header 1">
                            <TextBox Text="Some Text"/>
                        </igDock:ContentPane>
                        <igDock:ContentPane Header="Header 2">
                            <TextBox Text="Some Other Text"/>
                        </igDock:ContentPane>
                    </igDock:TabGroupPane>
                </igDock:SplitPane>
            </igDock:DocumentContentHost>
        </igDock:XamDockManager>
    </Grid>
</DockPanel>
like image 722
Fredrik Hedblad Avatar asked Jun 20 '11 10:06

Fredrik Hedblad


People also ask

What is difference between docking and floating window?

Floating windows always appear directly in front of the WinDbg window. A docked window occupies a fixed position within the WinDbg window or in a separate dock. When two or more docked windows are tabbed together, they occupy the same position within the frame.

Is an example of a dockable window?

The table of contents (TOC) in ArcMap is a good example of a dockable window.

How do you dock a floating window?

To dock a floating window, do one of the following: Double-click the window's title bar. Open the shortcut menu by selecting and holding (or right-clicking) the window's title bar or selecting the window's icon in the upper-right corner, and then select Dock.

How do I create a floating window in Windows 10?

To make the active window always on top, press Ctrl + Spacebar (or the keyboard shortcut you assigned).


2 Answers

The visual studio team has some good information on lessons they learned when making VS in WPF. One of the issues they ran into was related to Focus management. As a result, WPF 4 has some new features to help out.

Here's the info on the issue that sounds like your situation:

http://blogs.msdn.com/b/visualstudio/archive/2010/03/09/wpf-in-visual-studio-2010-part-3-focus-and-activation.aspx

Their discussion of the new "HwndSource.DefaultAcquireHwndFocusInMenuMode" property sounds very similar to what you're running into.

EDIT

After further investigation, it looks like Visual Studio might be hooking the windows message loop and returning specific values to make the floating windows work.

I'm not a win32 programmer, but it seems that when a user clicks a menu in an inactive window, windows sends the WM_MOUSEACTIVATE message to it before processing the mouse down event. This lets the main window determine whether it should be activated.

In my unmodified WPF test app, the inactive window returns MA_ACTIVATE. However, VS returns MA_NOACTIVATE. The docs indicate that this tells windows NOT to activate the main window prior to handling further input. I'm guessing that visual studio hooks the windows message loop and returns MA_NOACTIVATE when the user clicks on the menus / toolbars.

I was able to make this work in a simple, two window WPF app by adding this code to the top level window.

    protected override void OnSourceInitialized(EventArgs e)
    {
        base.OnSourceInitialized(e);

        var hook = new HwndSourceHook(this.FilterMessage);
        var source2 = HwndSource.FromVisual(this) as HwndSource;
        source2.AddHook(hook);
    }

    private IntPtr FilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        const int WM_MOUSEACTIVATE = 0x0021;
        const int MA_NOACTIVATE = 3;

        switch (msg)
        {
            case WM_MOUSEACTIVATE:
                handled = true;
                return new IntPtr(MA_NOACTIVATE);
        }
        return IntPtr.Zero;
    }

In your case, you'd probably need to add more logic that would check what the user clicked on and decide based on that whether to intercept the message and return MA_NOACTIVATE.

EDIT 2

I've attached a sample WPF application that shows how to do this with a simple WPF application. This should work pretty much the same with floating windows from a docking toolkit, but I haven't tested that specific scenario.

The sample is available at: http://blog.alner.net/downloads/floatingWindowTest.zip

The sample has code comments to explain how it works. To see it in action, run the sample, click the "open another window" button. This should put focus in the textbox of the new window. Now, click the edit menu of the main window and use the commands like "select all". These should operate on the other window without bringing the "main window" to the foreground.

You can also click on the "exit" menu item to see that it can still route commands to the main window if needed.

Key Points (Activation / Focus):

  1. Use the HwndSource.DefaultAcquireHwndFocusInMenuMode to get the menus to work stop grabbing focus.
  2. Hook the message loop and return "MA_NOACTIVATE" when the user clicks the menu.
  3. Add an event handler to the menu's PreviewGotKeyboardFocus and set e.Handled to true so that the menu wont' attempt to grab focus.

Key Points (Commands):

  1. Hook the main window's "CommandManager.PreviewCanExecute" and "CommandManager.PreviewExecuted" events.
  2. In these events, detect whether the app has an "other window" that's supposed to be the target of events.
  3. Manually invoke the original command against the "other window".

Hope it works for you. If not, let me know.

like image 134
NathanAW Avatar answered Oct 15 '22 14:10

NathanAW


I used the great answer from NathanAW and created a ResourceDictionary containing a Style for Window (which should be used by the MainWindow), contained the key pieces to solve this problem.

Update: Added support for ToolBar as well as Menu

It includes hit testing specifically for the MainMenu or ToolBar to decide if focusing should be allowed.

The reason I've used a ResourceDictionary for this is for reusability since we will be using this in many projects. Also, the code behind for the MainWindow can stay clean.

MainWindow can use this style with

<Window...>
    <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="NoFocusMenuWindowDictionary.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Window.Resources>
    <Window.Style>
        <StaticResource ResourceKey="NoFocusMenuWindow"/>
    </Window.Style>
    <!--...-->
</Window>

NoFocusMenuWindowDictionary.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    x:Class="MainWindowVS2010Mode.NoFocusMenuWindowDictionary">
    <Style x:Key="NoFocusMenuWindow" TargetType="Window">
        <EventSetter Event="Loaded" Handler="MainWindow_Loaded"/>
    </Style>
    <Style TargetType="Menu">
        <EventSetter Event="PreviewGotKeyboardFocus"
                     Handler="Menu_PreviewGotKeyboardFocus"/>
    </Style>
    <Style TargetType="ToolBar">
        <EventSetter Event="PreviewGotKeyboardFocus"
                     Handler="ToolBar_PreviewGotKeyboardFocus"/>
    </Style>
</ResourceDictionary>

NoFocusMenuWindowDictionary.xaml.cs

namespace MainWindowVS2010Mode
{
    public partial class NoFocusMenuWindowDictionary
    {
        #region Declaration

        private static Window _mainWindow;
        private static bool _mainMenuOrToolBarClicked;

        #endregion // Declaration

        void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            _mainWindow = sender as Window;
            HwndSource.DefaultAcquireHwndFocusInMenuMode = true;
            Keyboard.DefaultRestoreFocusMode = RestoreFocusMode.None;
            HwndSource hwndSource = HwndSource.FromVisual(_mainWindow) as HwndSource;
            hwndSource.AddHook(FilterMessage);
        }

        private static IntPtr FilterMessage(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            const int WM_MOUSEACTIVATE = 0x0021;
            const int MA_NOACTIVATE = 3;

            switch (msg)
            {
                case WM_MOUSEACTIVATE:

                    if (ClickedMainMenuOrToolBarItem())
                    {
                        handled = true;
                        return new IntPtr(MA_NOACTIVATE);
                    }
                    break;
            }
            return IntPtr.Zero;
        }

        #region Hit Testing

        private static bool ClickedMainMenuOrToolBarItem()
        {
            _mainMenuOrToolBarClicked = false;
            Point clickedPoint = Mouse.GetPosition(_mainWindow);
            VisualTreeHelper.HitTest(_mainWindow,
                                     null,
                                     new HitTestResultCallback(HitTestCallback),
                                     new PointHitTestParameters(clickedPoint));
            return _mainMenuOrToolBarClicked;
        }

        private static HitTestResultBehavior HitTestCallback(HitTestResult result)
        {
            DependencyObject visualHit = result.VisualHit;
            Menu parentMenu = GetVisualParent<Menu>(visualHit);
            if (parentMenu != null && parentMenu.IsMainMenu == true)
            {
                _mainMenuOrToolBarClicked = true;
                return HitTestResultBehavior.Stop;
            }
            ToolBar parentToolBar = GetVisualParent<ToolBar>(visualHit);
            if (parentToolBar != null)
            {
                _mainMenuOrToolBarClicked = true;
                return HitTestResultBehavior.Stop;
            }
            return HitTestResultBehavior.Continue;
        }

        public static T GetVisualParent<T>(object childObject) where T : Visual
        {
            DependencyObject child = childObject as DependencyObject;
            while ((child != null) && !(child is T))
            {
                child = VisualTreeHelper.GetParent(child);
            }
            return child as T;
        }
        #endregion // Hit Testing

        #region Menu

        private void Menu_PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
        {
            Menu menu = sender as Menu;
            if (menu.IsMainMenu == true)
            {
                e.Handled = true;
            }
        }

        #endregion // Menu

        #region ToolBar

        private void ToolBar_PreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
        {
            e.Handled = true;
        }

        #endregion // ToolBar
    }
}
like image 21
Fredrik Hedblad Avatar answered Oct 15 '22 15:10

Fredrik Hedblad