Док-станция Windows. Интеграция с плавающим окном и меню MainWindow

В Visual Studio 2010 Dockable Windows работает, как и ожидалось, в любой ситуации.
Если "плавающий" документ активен и выбрано какое-то меню (например, "Правка" -> "Вставить"), то "плавающий" документ все еще имеет фокус, и команда будет выполнена для этого "плавающего" окна. Также обратите внимание, как это хорошо видно в пользовательском интерфейсе. MainWindow.xaml по-прежнему активен, а главное окно в Visual Studio неактивно, даже если выбрано Team-menu.

Я пытался получить то же поведение, используя множество различных сторонних стыковочных компонентов, но у них у всех одна и та же проблема: как только я выбираю меню, основное окно фокусируется, а мое плавающее окно больше не фокусируется. Кто-нибудь знает способ получить такое же поведение здесь, как в Visual Studio?

В данный момент я использую Infragistics xamDockManager, и проблему можно воспроизвести с помощью следующего примера кода.

  • Щелкните правой кнопкой мыши "Заголовок 1" и выберите "Поплавок"
  • Нажмите меню "Файл"
  • Обратите внимание, как MainWindow получает фокус.

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>

5 ответов

Решение

Команда Visual Studio обладает хорошей информацией об уроках, которые они извлекли при создании VS в WPF. Одна из проблем, с которой они столкнулись, была связана с управлением фокусом. В результате в WPF 4 появились новые функции, которые могут помочь.

Вот информация о проблеме, которая звучит как ваша ситуация:

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

Их обсуждение нового свойства "HwndSource.DefaultAcquireHwndFocusInMenuMode" звучит очень похоже на то, с чем вы сталкиваетесь.

РЕДАКТИРОВАТЬ

После дальнейшего изучения, похоже, что Visual Studio может перехватывать цикл сообщений Windows и возвращать определенные значения, чтобы заставить работать плавающие окна.

Я не программист на win32, но кажется, что когда пользователь щелкает меню в неактивном окне, Windows отправляет ему сообщение WM_MOUSEACTIVATE перед обработкой события мыши. Это позволяет главному окну определить, следует ли его активировать.

В моем неизмененном тестовом приложении WPF неактивное окно возвращает MA_ACTIVATE. Однако VS возвращает MA_NOACTIVATE. Документы указывают, что это говорит окнам НЕ активировать главное окно перед обработкой дальнейшего ввода. Я предполагаю, что Visual Studio перехватывает цикл сообщений Windows и возвращает MA_NOACTIVATE, когда пользователь нажимает на меню / панели инструментов.

Я смог сделать это в простом приложении WPF с двумя окнами, добавив этот код в окно верхнего уровня.

    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;
    }

В вашем случае вам, вероятно, потребуется добавить дополнительную логику, которая будет проверять, на что нажал пользователь, и на основе этого решать, следует ли перехватить сообщение и вернуть MA_NOACTIVATE.

РЕДАКТИРОВАТЬ 2

Я приложил пример приложения WPF, в котором показано, как это сделать с помощью простого приложения WPF. Это должно работать почти так же, как с плавающими окнами из набора инструментов для стыковки, но я не тестировал этот конкретный сценарий.

Образец доступен по адресу: http://blog.alner.net/downloads/floatingWindowTest.zip

В примере есть комментарии к коду, чтобы объяснить, как это работает. Чтобы увидеть его в действии, запустите образец, нажмите кнопку "открыть другое окно". Это должно поместить фокус в текстовое поле нового окна. Теперь щелкните меню редактирования в главном окне и используйте команды, такие как "выбрать все". Они должны работать в другом окне, не выводя "главное окно" на передний план.

Вы также можете нажать на пункт меню "Выход", чтобы увидеть, что он по-прежнему может направлять команды в главное окно, если это необходимо.

Ключевые моменты (Активация / Фокус):

  1. Используйте HwndSource.DefaultAcquireHwndFocusInMenuMode, чтобы заставить работающие меню перестать захватывать фокус.
  2. Подцепите цикл сообщений и верните "MA_NOACTIVATE", когда пользователь щелкает меню.
  3. Добавьте обработчик событий в PreviewGotKeyboardFocus и установите для e.Handled значение true, чтобы меню не пыталось захватить фокус.

Ключевые моменты (команды):

  1. Перехватите события главного окна "CommandManager.PreviewCanExecute" и "CommandManager.PreviewExecuted".
  2. В этих событиях определите, есть ли в приложении "другое окно", которое должно быть целью событий.
  3. Вручную вызовите исходную команду против "другого окна".

Надеюсь, это работает для вас. Если нет, дайте мне знать.

Я использовал отличный ответ от NathanAW и создал ResourceDictionary содержащий Style за Window (который должен использоваться MainWindow), содержала ключевые части для решения этой проблемы.

Обновление: добавлена ​​поддержка ToolBar так же как Menu

Он включает в себя тестирование попаданий специально для MainMenu или ToolBar, чтобы решить, следует ли разрешить фокусировку.

Причина, по которой я использовал ResourceDictionary для этого, заключается в возможности повторного использования, поскольку мы будем использовать это во многих проектах. Кроме того, код для MainWindow может оставаться чистым.

MainWindow можно использовать этот стиль с

<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
    }
}

Просто из любопытства, вы пытались связать MenuItem.CommandTarget к XamDockManager.ActivePane?

Глядя на документацию XamDockManager, я также вижу CurrentFlyoutPane свойство, которое возвращает "Infragistics.Windows.DockManager.ContentPane в настоящее время в UnpinnedTabFlyout или ноль, если всплывающее окно не отображается". Я не уверен, какое свойство будет уместным в вашем сценарии, но стоит попробовать.

Я знаю, что это старый пост, но Prism может сделать вашу жизнь намного проще. Используя RegionAdapter, созданный здесь:

http://brianlagunas.com/2012/09/12/xamdockmanagera-prism-regionadapter/

Вы можете легко отслеживать, какое окно является активным, плавающим или нет, используя интерфейс IActiveAware. Команды Prism также учитывают это и могут выполнять команды только в активном представлении. В блоге есть пример приложения, с которым вы можете поиграть.

Я не уверен, как это сделать, но я знаю, что у Infragistics есть отличный форум поддержки, поэтому, возможно, стоит задать там вопрос.

http://forums.infragistics.com/

Другие вопросы по тегам