WebBrowser控件快捷键不起作用

时间:2014-11-26 04:23:49

标签: c# .net wpf com webbrowser-control

我有一个WPF项目,它加载一个包含WebBrowser控件的窗口。 打开窗口的类可用于COM互操作性。

当作为Windows应用程序运行项目时,窗口打开并且WebBrowser控件正常工作,但是当编译为类库并且从外部应用程序使用COM打开窗口时,WebBrowser都没有shortcut keys工作。 (例如CTRL + A,DELETE,CTRL + X,TAB等)

This SO Question似乎解释了问题的原因,但是由于PreProcessMessage或ProcessCmdKey永远不会被调用,因此对我不起作用的建议。 (除非作为Windows应用程序运行)

我还阅读了讨论调用TranslateAccelerator方法的链接herehere。但我无法尝试这个,因为我订阅的KeyDown事件都没有被解雇。 我尝试过WebBrowser.KeyDown,WebBrowser.PreviewKeyDown以及与WebBrowser.Document和WebBrowser.Document.Body相关的各种onkeydown事件。这些都没有触发给我。 (除非作为Windows应用程序运行)

COM可见类

[ProgId("My.Project")]
[ComVisible(true)]
public class MyComVisibleClass : IMyComVisibleInterface
{
    private BrowserWindow myWpfWindow;
    public void OpenWpfWindow()
    {
        ...
        myWpfWindow = new myWpfWindow();
        ...
        myWpfWindow.Show();
    }
}

XAML

<WebBrowser x:Name="EmbeddedBrowser" Focusable="True"    />
<!--I tried using forms host too-->
<!--<WindowsFormsHost Name="wfHost" Focusable="True" >
    <common:WebBrowser x:Name="EmbeddedBrowser" WebBrowserShortcutsEnabled="True"     ObjectForScripting="True"  />
</WindowsFormsHost>-->

WPF浏览器窗口

public partial class BrowserWindow : Window
    {
        public BrowserWindow(Uri uri)
        {
            InitializeComponent();
            ...
            EmbeddedBrowser.Focus();
            EmbeddedBrowser.Navigate(uri); 
            ...  
        }
    }
}

通过COM互操作打开时,如何启用快捷键?

4 个答案:

答案 0 :(得分:1)

在其他开发人员的帮助下,我们可以根据需要运行浏览器。

  1. 为了解决不为webBrowser控件触发的键事件,我们使用SetWindowsHookEx来处理WH_GETMESSAGE钩子。
  2. 由于钩子是针对当前线程的每个窗口,我们需要检查活动窗口,然后将转换后的struct传递给webBrowser控件的TranslateAccelerator方法。
  3. 这里的问题是webBrowser控件已经正确处理了特定键,这导致键命令被复制。添加了一个条件来处理这些关键事件。
  4. 还有一些测试要做,但到目前为止它正确处理所有输入,剪贴板,全选,删除退格,箭头键命令。

    我仍然愿意接受改进建议,但现在我很高兴它正在发挥作用。

    public partial class BrowserWindow : Window
        {
        public BrowserWindow(Uri uri)
        {
            InitializeComponent();
            ...
            EmbeddedBrowser.Focus();
            EmbeddedBrowser.Navigate(uri); 
            ...  
            EmbeddedBrowser.LoadCompleted+= (sender, args) =>
            {
                ...
                InstallHook();
            }
        }
    
        ...
    
        DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, IntPtr windowTitle);
    
        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern IntPtr SetWindowsHookEx(int idHook, HookHandlerDelegate lpfn, IntPtr hInstance, int threadId);
    
        [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
        public static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, IntPtr wParam, IntPtr lParam);
    
        [DllImport("kernel32.dll")]
        public static extern int GetCurrentThreadId();
    
        [DllImport("user32.dll")]
        static extern bool UnhookWindowsHookEx(IntPtr hInstance);
    
        public delegate IntPtr HookHandlerDelegate(int nCode, IntPtr wParam, IntPtr lParam);
    
        //Keyboard API constants
        private const int WH_GETMESSAGE = 3;
        private const int WM_KEYUP = 0x101;
        private const int WM_KEYDOWN = 0x0100;
        private const int WM_SYSKEYUP = 0x0105;
        private const int WM_SYSKEYDOWN = 0x0104;
    
    
        private const uint VK_BACK = 0x08;
        private const uint VK_LEFT = 0x25;
        private const uint VK_UP = 0x26;
        private const uint VK_RIGHT = 0x27;
        private const uint VK_DOWN = 0x28;
    
        private List<uint> ignoreKeys = new List<uint>()
        {
            VK_BACK,
            VK_LEFT,
            VK_UP,
            VK_RIGHT,
            VK_DOWN,
        };
    
        //Remove message constants
        private const int PM_NOREMOVE = 0x0000;
    
        //Variables used in the call to SetWindowsHookEx
        private IntPtr hHook = IntPtr.Zero;
    
        private IntPtr HookCallBack(int nCode, IntPtr wParam, IntPtr lParam)
        {
            if (nCode >= 0 || wParam.ToInt32() == PM_NOREMOVE)
            {
                MSG msg = (MSG)Marshal.PtrToStructure(lParam, typeof(MSG));
                if (msg.message == WM_KEYDOWN || msg.message == WM_SYSKEYDOWN || msg.message == WM_KEYUP || msg.message == WM_SYSKEYUP)
                {
                    if (!ignoreKeys.Contains((uint)msg.wParam))
                        if (this.IsLoaded && this.IsActive)
                        {
    ((IKeyboardInputSink)EmbeddedBrowser).TranslateAccelerator(ref msg, ModifierKeys.None);
                            return (IntPtr)1;
                        }
                }
            }
            return CallNextHookEx(hHook, nCode, wParam, lParam);
        }
    
        private void InstallHook()
        { 
            IntPtr wnd = EmbeddedBrowser.Handle;
            if (wnd != IntPtr.Zero)
            {
                wnd = FindWindowEx(wnd, IntPtr.Zero, "Shell DocObject View", IntPtr.Zero);
                if (wnd != IntPtr.Zero)
                {
                    wnd = FindWindowEx(wnd, IntPtr.Zero, "Internet Explorer_Server", IntPtr.Zero);
                    if (wnd != IntPtr.Zero)
                    {
                        hHook = SetWindowsHookEx(WH_GETMESSAGE, new HookHandlerDelegate(HookCallBack), (IntPtr)0, GetCurrentThreadId());
                    }
                }
            }
        }
    }
    

答案 1 :(得分:1)

your solution中的一个真正的错误是:

hHook = SetWindowsHookEx(WH_GETMESSAGE, new HookHandlerDelegate(HookCallBack), (IntPtr)0, GetCurrentThreadId());

新分配的委托new HookHandlerDelegate(HookCallBack)在某个时刻被垃圾收集,后来导致AccessViolationException。在调用UnhookWindowsHookEx

之前,您应该对此委托保持强烈的引用
this._hookCallBack = new HookHandlerDelegate(HookCallBack);
this.hHook = SetWindowsHookEx(WH_GETMESSAGE, _hookCallBack, (IntPtr)0, GetCurrentThreadId());

尽管如此,我仍然认为这不是解决问题的正确方法。从评论到问题:

  

那么,myWpfWindow的行为就像一个无模式,独立的顶级   那个遗留应用中的窗口?或者它与其他人以某种方式相关联   遗留应用程序的GUI?   

<小时/>   独立的顶级窗口。

WPF and Win32 Interoperation(特别是,Sharing Message Loops Between Win32 and WPF)假设您可以控制Win32旧应用程序的代码。

显然,这不是这种情况,因此我建议您使用WPF调度程序(以及自己的消息循环)在单独的UI线程上打开此WPF窗口。这可以解决WebBrowser快捷方式问题,也可能解决其他一些问题。

您可以使用AttachThreadInput将原始STA线程(您的COM对象所在的位置)的用户输入队列附加到新WPF线程的用户输入队列。还有其他方面,如编组COM事件和方法调用正确的线程。以下代码说明了这一概念。它是一个完整的WinForms测试应用程序,它使用COM对象,然后在专用线程上创建一个带有WebBrowser的WPF窗口。

using System;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Windows.Threading;

namespace LegacyWinApp
{
    // by noseratio - https://stackoverflow.com/a/28573841/1768303

    /// <summary>
    /// Form1 - testing MyComVisibleClass from a client app
    /// </summary>
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.Load += Form1_Load;
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            var comObject = new MyComVisibleClass();

            var status = new Label { Left = 10, Top = 10, Width = 50, Height = 25, BorderStyle = BorderStyle.Fixed3D };
            this.Controls.Add(status);

            comObject.Loaded += () =>
                status.Text = "Loaded!";

            comObject.Closed += () =>
                status.Text = "Closed!";

            var buttonOpen = new Button { Left = 10, Top = 60, Width = 50, Height = 50, Text = "Open" };
            this.Controls.Add(buttonOpen);
            buttonOpen.Click += (_, __) =>
            {
                comObject.Open();
                status.Text = "Opened!";
                comObject.Load("http://example.com");
            };

            var buttonClose = new Button { Left = 10, Top = 110, Width = 50, Height = 50, Text = "Close" };
            this.Controls.Add(buttonClose);
            buttonClose.Click += (_, __) =>
                comObject.Close();
        }
    }

    /// <summary>
    /// MyComVisibleClass
    /// </summary>

    [ComVisible(true), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IComObject
    {
        void Open();
        void Load(string url);
        void Close();
    }

    [ComVisible(true), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
    public interface IComObjectEvents
    {
        void Loaded();
        void Closed();
    }

    /// <summary>
    /// MyComVisibleClass
    /// </summary>
    [ComVisible(true)]
    [ClassInterface(ClassInterfaceType.None)]
    [ComDefaultInterface(typeof(IComObject))]
    [ComSourceInterfaces(typeof(IComObjectEvents))]
    public class MyComVisibleClass : IComObject
    {
        internal class EventHelper
        {
            MyComVisibleClass _parent;
            System.Windows.Threading.Dispatcher _clientThreadDispatcher;

            internal EventHelper(MyComVisibleClass parent)
            {
                _parent = parent;
                _clientThreadDispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;
            }

            public void FireLoaded()
            {
                _clientThreadDispatcher.InvokeAsync(() =>
                    _parent.FireLoaded());
            }

            public void FireClosed()
            {
                _clientThreadDispatcher.InvokeAsync(() =>
                    _parent.FireClosed());
            }
        }

        WpfApartment _wpfApartment;
        BrowserWindow _browserWindow;
        readonly EventHelper _eventHelper;

        public MyComVisibleClass()
        {
            _eventHelper = new EventHelper(this);
        }

        // IComObject methods

        public void Open()
        {
            if (_wpfApartment != null)
                throw new InvalidOperationException();

            // start a new thread with WPF Dispatcher
            _wpfApartment = new WpfApartment();

            // attach the input queue of the current thread to that of c
            var thisThreadId = NativeMethods.GetCurrentThreadId();
            _wpfApartment.Invoke(() =>
                NativeMethods.AttachThreadInput(thisThreadId, NativeMethods.GetCurrentThreadId(), true));

            // create an instance of BrowserWindow on the WpfApartment's thread
            _browserWindow = _wpfApartment.Invoke(() => new BrowserWindow(_eventHelper) { 
                Left = 200, Top = 200, Width = 640, Height = 480 });
            _wpfApartment.Invoke(() => _browserWindow.Initialize());
        }

        public void Load(string url)
        {
            if (_wpfApartment == null)
                throw new InvalidOperationException();

            _wpfApartment.Run(async () =>
            {
                try
                {
                    await _browserWindow.LoadAsync(url);
                    _eventHelper.FireLoaded();
                }
                catch (Exception ex)
                {
                    System.Windows.MessageBox.Show(ex.Message);
                    throw;
                }
            });
        }

        public void Close()
        {
            if (_wpfApartment == null)
                return;

            if (_browserWindow != null)
                _wpfApartment.Invoke(() => 
                    _browserWindow.Close());

            CloseWpfApartment();
        }

        void CloseWpfApartment()
        {
            if (_wpfApartment != null)
            {
                _wpfApartment.Dispose();
                _wpfApartment = null;
            }
        }

        // IComObjectEvents events

        public event Action Loaded = EmptyEventHandler;

        public event Action Closed = EmptyEventHandler;

        // fire events, to be called by EventHelper

        static void EmptyEventHandler() { }

        internal void FireLoaded()
        {
            this.Loaded();
        }

        internal void FireClosed()
        {
            _browserWindow = null;
            CloseWpfApartment();            
            this.Closed();
        }
    }

    /// <summary>
    /// BrowserWindow
    /// </summary>
    class BrowserWindow: System.Windows.Window
    {
        System.Windows.Controls.WebBrowser _browser;
        MyComVisibleClass.EventHelper _events;

        public BrowserWindow(MyComVisibleClass.EventHelper events)
        {
            _events = events;
            this.Visibility = System.Windows.Visibility.Hidden;
            this.ShowActivated = true;
            this.ShowInTaskbar = false;
        }

        bool IsReady()
        {
            return (this.Visibility != System.Windows.Visibility.Hidden && _browser != null);
        }

        public void Initialize()
        {
            if (IsReady())
                throw new InvalidOperationException();

            this.Show();
            _browser = new System.Windows.Controls.WebBrowser();
            this.Content = _browser;
        }

        public async Task LoadAsync(string url)
        {
            if (!IsReady())
                throw new InvalidOperationException();

            // navigate and handle LoadCompleted
            var navigationTcs = new TaskCompletionSource<bool>();

            System.Windows.Navigation.LoadCompletedEventHandler handler = (s, e) =>
                navigationTcs.TrySetResult(true);

            _browser.LoadCompleted += handler;
            try
            {
                _browser.Navigate(url);
                await navigationTcs.Task;
            }
            finally
            {
                _browser.LoadCompleted -= handler;
            }

            // make the content editable to check if WebBrowser shortcuts work well
            dynamic doc = _browser.Document;
            doc.body.firstChild.contentEditable = true;
            _events.FireLoaded();
        }

        protected override void OnClosed(EventArgs e)
        {
            base.OnClosed(e);
            _browser.Dispose();
            _browser = null;
            _events.FireClosed();
        }
    }

    /// <summary>
    /// WpfApartment
    /// </summary>
    internal class WpfApartment : IDisposable
    {
        Thread _thread; // the STA thread

        TaskScheduler _taskScheduler; // the STA thread's task scheduler

        public TaskScheduler TaskScheduler { get { return _taskScheduler; } }

        // start the STA thread with WPF Dispatcher
        public WpfApartment()
        {
            var tcs = new TaskCompletionSource<TaskScheduler>();

            // start an STA thread and gets a task scheduler
            _thread = new Thread(_ =>
            {
                // post the startup callback,
                // it will be invoked when the message loop stars pumping
                Dispatcher.CurrentDispatcher.InvokeAsync(
                    () => tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext()), 
                    DispatcherPriority.ApplicationIdle);

                // run the WPF Dispatcher message loop
                Dispatcher.Run();
            });

            _thread.SetApartmentState(ApartmentState.STA);
            _thread.IsBackground = true;
            _thread.Start();
            _taskScheduler = tcs.Task.Result;
        }

        // shutdown the STA thread
        public void Dispose()
        {
            if (_taskScheduler != null)
            {
                var taskScheduler = _taskScheduler;
                _taskScheduler = null;

                if (_thread != null && _thread.IsAlive)
                {
                    // execute Dispatcher.ExitAllFrames() on the STA thread
                    Task.Factory.StartNew(
                        () => Dispatcher.ExitAllFrames(),
                        CancellationToken.None,
                        TaskCreationOptions.None,
                        taskScheduler).Wait();

                    _thread.Join();
                }
                _thread = null;
            }
        }

        // Task.Factory.StartNew wrappers
        public void Invoke(Action action)
        {
            Task.Factory.StartNew(action,
                CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait();
        }

        public TResult Invoke<TResult>(Func<TResult> func)
        {
            return Task.Factory.StartNew(func,
                CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result;
        }

        public Task Run(Action action, CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
        }

        public Task<TResult> Run<TResult>(Func<TResult> func, CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew(func, token, TaskCreationOptions.None, _taskScheduler);
        }

        public Task Run(Func<Task> func, CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew(func, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
        }

        public Task<TResult> Run<TResult>(Func<Task<TResult>> func, CancellationToken token = default(CancellationToken))
        {
            return Task.Factory.StartNew(func, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
        }
    }

    /// <summary>
    /// NativeMethods
    /// </summary>
    internal class NativeMethods
    {
        [DllImport("kernel32.dll", PreserveSig = true)]
        public static extern uint GetCurrentThreadId();

        [DllImport("user32.dll", PreserveSig = true)]
        public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
    }
}

答案 2 :(得分:0)

使用Excel WebBrowser控件而不是System.Windows.Forms WebBrowser可能更简单;它处理特殊的密钥转发,如TAB,DEL,CTRL + V等

更改WebBrowser构造函数
new System.Windows.Forms.WebBrowser();

new Microsoft.Office.Tools.Excel.Controls.WebBrowser();  

您需要添加对项目的引用: Project / Add Referernce / Extensions选择  Microsoft.Tools.Outlook&amp; Microsoft.Tools.Outlook.v4.0.Utilities

参考:https://msdn.microsoft.com/en-us/library/microsoft.office.tools.excel.controls.webbrowser.aspx

答案 3 :(得分:-1)

我不知道你是否有时间从Delphi翻译代码。我有一个项目,我不能依赖.NET安装,并且必须启用和禁用WebBrowser com对象的钩子。以下是Delphi中适合我的代码。

unit Browser.Hooks;

interface

uses
    Winapi.Windows, Winapi.Messages, Vcl.Forms;
type
  PKBDLLHOOKSTRUCT = ^TKBDLLHOOKSTRUCT;
  TKBDLLHOOKSTRUCT = packed record
    vkCode: DWORD;
    scanCode: DWORD;
    flags: DWORD;
    time: DWORD;
    dwExtraInfo: DWORD;
  end;

  function DisableWindowsKeys: Boolean;
  function EnableWindowsKeys: Boolean;
  function WindowsKeysDisabled: Boolean;

  function EnableWindowsClicks : Boolean;
  function DisableWindowsClicks : Boolean;
  function WindowsClicksDisabled: Boolean;


const
  WH_KEYBOARD_LL = 13;
  LLKHF_ALTDOWN = $0020;

function MouseProc(nCode: Integer; wParam, lParam: Longint): LongInt; stdcall;
function KeyboardProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): HRESULT; stdcall;


var
  MouseHook: HHook=0;
  KeyboardHook: HHook=0;

implementation
 //-----------------------------------------------------------------------------
function MouseProc(nCode: Integer; wParam, lParam: Longint): LongInt; stdcall;
var
    classbuf: array[0..255] of Char;
const
  ie = 'Internet Explorer_Server';
begin
  Result:=0;
  if(nCode=HC_ACTION)then
  begin
    if((wParam=WM_RBUTTONDOWN) or (wParam=WM_RBUTTONUP))then begin
      //GetClassName(PMOUSEHOOKSTRUCT(lParam)^.HWND, classbuf, SizeOf(classbuf)) ;
      //if lstrcmp(@classbuf[0], @ie[1]) = 0 then
      Result:=HC_SKIP;
    end;
  end;
  if(Result=0) then
    Result := CallNextHookEx(MouseHook, nCode, wParam, lParam) ;
 end; (*MouseProc*)
//------------------------------------------------------------------------------
function KeyboardProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): HRESULT; stdcall;
var
  pkbhs: PKBDLLHOOKSTRUCT;
  isALTDown:boolean;
  isCTLDown:boolean;
  keyCode:Cardinal;
begin
  pkbhs := PKBDLLHOOKSTRUCT(lParam);
  Result:=0;
  if (nCode = HC_ACTION) then
  begin
    isALTDown:=LongBool(pkbhs^.flags and  LLKHF_ALTDOWN);
    isCTLDown:=WordBool(GetAsyncKeyState(VK_CONTROL) and $8000);
    keyCode:=pkbhs^.vkCode;
    case keyCode of
        //VK_ESCAPE : if(isCTLDown or isALTDown) then Result:=HC_SKIP;
        VK_ESCAPE : begin
          if(isALTDown or isCTLDown) then Result:=HC_SKIP;
          if(isCTLDown)then
            Application.Terminate;
        end;

        VK_TAB    : if(isALTDown) then Result:=HC_SKIP;
        VK_SNAPSHOT,
        VK_LWIN,
        VK_RWIN,
        VK_APPS   : Result:=HC_SKIP;
    end;
  end;
  if(Result=0)then
    Result := CallNextHookEx(KeyboardHook, nCode, wParam, lParam);
end;
//------------------------------------------------------------------------------
function DisableWindowsKeys: Boolean;
begin
  if KeyboardHook = 0 then
    KeyboardHook := SetWindowsHookEx(WH_KEYBOARD_LL, @KeyboardProc, HInstance, 0);
  Result := (KeyboardHook <> 0)
end;
//------------------------------------------------------------------------------
function EnableWindowsKeys: Boolean;
begin
  Result := False;
  if (KeyboardHook <> 0) and UnhookWindowsHookEx(KeyboardHook) then
  begin
    KeyboardHook := 0;
    Result := True;
  end;
end;
//------------------------------------------------------------------------------
function WindowsKeysDisabled: Boolean;
begin
  Result := (KeyboardHook <> 0)
end;

//------------------------------------------------------------------------------
function DisableWindowsClicks: Boolean;
begin
  if MouseHook = 0 then
    MouseHook := SetWindowsHookEx(WH_MOUSE_LL, @MouseProc, HInstance, 0);
  Result := (MouseHook <> 0)
end;
//------------------------------------------------------------------------------
function EnableWindowsClicks: Boolean;
begin
  Result := False;
  if (MouseHook <> 0) and UnhookWindowsHookEx(MouseHook) then
  begin
    MouseHook := 0;
    Result := True;
  end;
end;
//------------------------------------------------------------------------------
function WindowsClicksDisabled: Boolean;
begin
  Result := (MouseHook <> 0)
end;
//------------------------------------------------------------------------------
end.