VSTO WPF modal dialogs cursor doesn't blink in TextBox

感情迁移 提交于 2020-01-15 04:03:19

问题


I have a VSTO (Excel or Word) addin with a WPF dialog window, shown modally. The dialog has a TextBox that I need to be initially focused. I can get it to focus using various methods like the FocusManager or Keyboard classes, or by requesting a Traversal. Or I can simulate a TAB key press via keybd_event() (user32.dll).

The problem is with any of these methods, the field doesn't seem to be "fully" focused. The cursor shows in the TextBox, but it isn't blinking and typing won't work! To solve the problem, I can either:

  1. press TAB once (not programatically) and the cursor will start blinking and I can type; or

  2. Press Alt-Tab to switch to another application, then back. Again, the cursor will start blinking and I can type.

EDIT: SOLUTION

ErrCode's basic approach works reliably, with some modifications. First, instead of doing his idea just once, I do it on all windows. Second, I open the dummy window after my own window loads, not before. Finally, I introduce some delays, without which the approach doesn't work:

// Window or UserControl, works for both or any FrameworkElement technically.
public static class FocusExtensions
    {
        #region FocusElementOnLoaded Attached Behavior

        public static IInputElement GetFocusElementOnLoaded(FrameworkElement obj)
        {
            return (IInputElement)obj.GetValue(FocusElementOnLoadedProperty);
        }

        public static void SetFocusElementOnLoaded(FrameworkElement obj, IInputElement value)
        {
            obj.SetValue(FocusElementOnLoadedProperty, value);
        }

        public static readonly DependencyProperty FocusElementOnLoadedProperty =
        DependencyProperty.RegisterAttached("FocusElementOnLoaded", typeof(IInputElement), typeof(FocusExtensions), new PropertyMetadata(null, FocusElementOnLoadedChangedCallback));

        private static async void FocusElementOnLoadedChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            // This cast always succeeds.
            var c = (FrameworkElement) d;
            var element = (IInputElement) e.NewValue;
            if (c != null && element != null)
            {
                if (c.IsLoaded)
                    await FocusFirst(element);
                else
                    c.Loaded += async (sender, args) =>
                        await FocusFirst(element);
            }
        }

        private static async Task FocusFirst(IInputElement firstInputElement)
        {
            var tmpWindow = new Window
            {
                Width = 0,
                Height = 0,
                Visibility = Visibility.Collapsed
            };

            tmpWindow.Loaded += async (s, e) =>
            {
                await Task.Delay(1);
                Application.Current?.Dispatcher?.Invoke(() =>
                {
                    tmpWindow.Close();
                    firstInputElement.Focus();
                });
            };

            await Task.Delay(1);
            Application.Current?.Dispatcher?.Invoke(() =>
            {
                tmpWindow.ShowDialog(); 
            });
        }

        #endregion FocusElementOnLoaded Attached Behavior
    }

To apply, in your XAML's UserControl or Window, add:

FocusExtensions.FocusElementOnLoaded="{Binding ElementName=MyTextBox}"

The MyTextBox must of course be derived from IInputElement.


回答1:


Noticed that none of the focus methods you mentioned seem to be the simplest case of Control.Focus(). So for example:

// Constructor of your window
public MyWindow()
{
    // ...
    // set any DataContext before InitializeComponent() if needed
    // ...
    InitializeComponent();
    // ...
    // add anything else you need to do within the constructor of the window
    // ...
    textbox1.Focus();   // Set focus on your text box
}

This was tested in an Excel VSTO add-in project and it immediately allows typing to go to the focused textbox as soon as the dialog is shown. Use with your window Owner hack (had to do similar in my project).

Difference between Control.Focus() and FocusManager.SetFocusedElement()

EDIT

Here's what I found after starting a VSTO add-in project from scratch. It appears to be one of many quirks with using WPF within an Excel VSTO add-in, where you can't reliably get Control.Focus() to work if it is used with the first WPF window ever created.

Workaround: Create a dummy WPF window. Close it. And then create the real one that will use Control.Focus().

Never noticed the problem before since my project always had a short "Loading" window open and close itself before presenting the real window.

bool isFirstTime = true;
private void button1_Click(object sender, RibbonControlEventArgs e)
{
    if (isFirstTime)
    {
        // For some reason the Control.Focus() is unreliable for the first WPF window ever shown within the VSTO addin. :( 
        // So make a dummy one once before we open the real window...
        // NOTE: The reason why I never noticed such a problem before in my own project, is since my project always showed a short loading window that closed itself
        // before the main window is shown. So I could use Control.Focus() in the main window without issues
        var pretendLoadWindow = new Window();
        pretendLoadWindow.SizeToContent = SizeToContent.WidthAndHeight;
        pretendLoadWindow.Visibility = Visibility.Collapsed;
        pretendLoadWindow.Loaded += (_sender, _e) => pretendLoadWindow.Close();
        pretendLoadWindow.ShowDialog();
        isFirstTime = false;
    }
    var window = new Window();
    var excelHwnd = m_ExcelApplication != null ? new IntPtr(m_ExcelApplication.Hwnd) : Process.GetCurrentProcess().MainWindowHandle;
    WindowInteropHelper interopHelper = new WindowInteropHelper(window)
    {
        Owner = excelHwnd
    };
    window.Content = new UserControl1();
    window.SizeToContent = SizeToContent.WidthAndHeight;
    // FYI: just in case you have any breakpoints that switch the focus away from the Excel (to your VS IDE), then when this window is shown it won't typically get focus. Below should fix this...
    window.Loaded += (_sender, _e) => window.Focus();       
    window.ShowDialog();
}

Full test code accessible from here




回答2:


The field doesn't seem to be "fully" focused. The cursor shows in the TextBox, but it isn't blinking and typing won't work!

You're seeing the effects of message pumps misbehaving:

Excel CustomTaskPane with WebBrowser control - keyboard/focus issues

BUG: Cant choose dates on a DatePicker that fall outside a floating VSTO Add-In

VSTO WPF ContextMenu.MenuItem Click outside a TaskPane not raised

The bug is to do with controls that respond to input and the void WndProc(ref Message m) messages being incorrectly filtered or redirected in the dispatch loop. To solve it you have to tap into the Message Loop, eg:

protected override void WndProc(ref Message m)
{
  const int WM_PARENTNOTIFY = 528;
  if(m.Msg == WM_PARENTNOTIFY && !this.Focused)
  {
    this.Focus();
  }
  base.WndProc(ref m);
}

This is similar to what @Ian answered in the link you referenced in your other question ElementHost blocks mouse events.


Here is a summary of the behaviour by Hans Passant:

What's never not a problem (ie can often be problematic) is that you rely on the message pump in Excel to dispatch Windows messages, the messages that make these controls respond to input. This goes wrong in WPF as much as Winforms, they have their own dispatch loop that filters messages before they are delivered to the window. Key things that go wrong when their respective dispatcher isn't used are stuff like tabbing and short-cut keystrokes.

And then some, this kind of problem would be induced by Excel doing its own filtering before dispatching messages. I'd guess at an anti-malware feature, Microsoft is forever worried about programs messing with Office apps.




回答3:


It seems you just need to set the owner of your WPF window. To get the job done you need to initialize the WindowInteropHelper with a WPF window object for the dialog box. You can then get the WPF window's handle (HWND) from the Handle property and specify the owner for the WPF window with the Owner property. The following code example shows how to use WindowInteropHelper when hosting a WPF dialog box in a Win32 application.

  WindowInteropHelper wih = new WindowInteropHelper(myDialog);
  wih.Owner = ownerHwnd;
  myDialog.ShowDialog();



回答4:


Instead of:

var hwndOwner = (IntPtr)ExcelInterop.App.Hwnd;

try to use:

new WindowInteropHelper(window) { Owner = Process.GetCurrentProcess().MainWindowHandle };
window.ShowDialog();



回答5:


Have you tried the following

1. Try setting the focus inside the Loaded Event of the dialogue. Which will make it focus after the dialogue window is fully loaded.

            private void MyWindow_Loaded(object sender, RoutedEventArgs e)
            {
                myTextBox.Focus();
            }

2. Try setting a keyboard focus for your control.

           Keyboard.Focus(myTextBox);


来源:https://stackoverflow.com/questions/56820763/vsto-wpf-modal-dialogs-cursor-doesnt-blink-in-textbox

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!