釜底抽薪 CefSharp WPF控件不可知利用输入法输入中文的题材(代码都交由至 github)

首先,本文所有
代码已经提交至github,需要之好直接由github获取:https://github.com/starts2000/CefSharp,希望得以帮忙及出需要的朋友等。

CEF 简介

CEF is a BSD-licensed open source project founded by Marshall Greenblatt
in 2008 and based on the Google Chromium project. Unlike the Chromium
project itself, which focuses mainly on Google Chrome application
development, CEF focuses on facilitating embedded browser use cases in
third-party applications. CEF insulates the user from the underlying
Chromium and Blink code complexity by offering production-quality stable
APIs, release branches tracking specific Chromium releases, and binary
distributions. Most features in CEF have default implementations that
provide rich functionality while requiring little or no integration work
from the user. There are currently over 100 million installed instances
of CEF around the world embedded in products from a wide range of
companies and industries. A partial list of companies and products using
CEF is available on the CEF Wikipedia page. Some use cases for CEF
include:

  • Embedding an HTML5-compliant Web browser control in an existing
    native application.
  • Creating a light-weight native “shell” application that hosts a user
    interface developed primarily using Web technologies.
  • Rendering Web content “off-screen” in applications that have their
    own custom drawing frameworks.
  • Acting as a host for automated testing of existing Web properties
    and applications.

CEF supports a wide range of programming languages and operating systems
and can be easily integrated into both new and existing applications. It
was designed from the ground up with both performance and ease of use in
mind. The base framework includes C and C++ programming interfaces
exposed via native libraries that insulate the host application from
Chromium and Blink implementation details. It provides close integration
between the browser and the host application including support for
custom plugins, protocols, JavaScript objects and JavaScript extensions.
The host application can optionally control resource loading,
navigation, context menus, printing and more, while taking advantage of
the same performance and HTML5 technologies available in the Google
Chrome Web browser.
Numerous individuals and organizations contribute time and resources to
support CEF development, but more involvement from the community is
always welcome. This includes support for both the core CEF project and
external projects that integrate CEF with additional programming
languages and frameworks (see the “External Projects” section below). If
you are interested in donating time to help with CEF development please
see the “Helping Out” section below. If you are interested in donating
money to support general CEF development and infrastructure efforts
please visit the CEF Donations page.

CEF 的 .NET 开源项目重点出脚三个:

  1. CefSharp:https://github.com/chillitom/CefSharp
  2. cefglue:https://bitbucket.org/xilium/xilium.cefglue
  3. chromiumfx:https://bitbucket.org/chromiumfx/chromiumfx

CEF osr IME BUG 历史

CefSharp 和 cefglue 都使 C++/CLI  对 cef API 进行打包,都提供了 cef 的
Winform 和 WPF 控件,而 chromiumfx 使用 P/Invoke 对 cef API
进行打包,只供了cef Winform 控件。CefSharp 和 cefglue 的  cef WPF
控件都使用 cef 的 osr ( off screen  render)方式开展渲染,由于 osr
模式一直是 IME BUG,所以 CefSharp 和 cefglue 的 WPF 控件也是。

CEF osr IME bug 于 2012-11-22
就有人提出:https://bitbucket.org/chromiumembedded/cef/issues/798/out-of-focus-while-entering-ime,但是直至2016年的才解决https://bitbucket.org/chromiumembedded/cef/issues/1675/inline-ime-support-nstextinput-protocol-in,真是等之黄花菜都凉了。然而, CefSharp
和 cefglue 更是没有会与达到 CEF 的脚步,这个 BUG
直到现在也未尝解决,所有相关的 issue,回答都是建议以 WPF 中使用 Host
WinForm 控件的法门以 CEF。

CEF osr IME bug:

C++ 1

 

日前经参考 cef 的 osr 示例的源码,通过移植和修改,终于实现了 CefSharp
WPF 控件的 IME 输入,在这边享用给大家。

C++ 2

着重是当 CefSharp.Core 项目面临益了对 IME 音以及 CEF IME
相关的拍卖,还有即使是 WPF 的 ChromiumWebBrowser 控件的相干代码修改。

1.OsrImeHandler.cpp

// Copyright 2016 The Chromium Embedded Framework Authors. Portions copyright
// 2013 The Chromium Authors. All rights reserved. Use of this source code is
// governed by a BSD-style license that can be found in the LICENSE file.

// Implementation based on ui/base/ime/win/imm32_manager.cc from Chromium.

#include "Stdafx.h"
//#include <windowsx.h>
//#include <msctf.h>

#include "include/base/cef_build.h"
#include "OsrImeHandler.h"

#pragma comment(lib, "imm32.lib")

#define ColorUNDERLINE  0xFF000000    // Black SkColor value for underline,
// same as Blink.
#define ColorBKCOLOR    0x00000000    // White SkColor value for background,
// same as Blink.

namespace CefSharp {

    namespace {

        // Determines whether or not the given attribute represents a selection
        bool IsSelectionAttribute(char attribute) {
            return (attribute == ATTR_TARGET_CONVERTED ||
                attribute == ATTR_TARGET_NOTCONVERTED);
        }

        // Helper function for OsrImeHandler::GetCompositionInfo() method,
        // to get the target range that's selected by the user in the current
        // composition string.
        void GetCompositionSelectionRange(HIMC imc, int* target_start,
            int* target_end) {
            int attribute_size = ::ImmGetCompositionString(imc, GCS_COMPATTR, NULL, 0);
            if (attribute_size > 0) {
                int start = 0;
                int end = 0;
                std::vector<char> attribute_data(attribute_size);

                ::ImmGetCompositionString(imc, GCS_COMPATTR, &attribute_data[0],
                    attribute_size);
                for (start = 0; start < attribute_size; ++start) {
                    if (IsSelectionAttribute(attribute_data[start]))
                        break;
                }
                for (end = start; end < attribute_size; ++end) {
                    if (!IsSelectionAttribute(attribute_data[end]))
                        break;
                }

                *target_start = start;
                *target_end = end;
            }
        }

        // Helper function for OsrImeHandler::GetCompositionInfo() method, to get
        // underlines information of the current composition string.
        void GetCompositionUnderlines(
            HIMC imc,
            int target_start,
            int target_end,
            std::vector<CefCompositionUnderline> &underlines) {
            int clause_size = ::ImmGetCompositionString(imc, GCS_COMPCLAUSE, NULL, 0);
            int clause_length = clause_size / sizeof(uint32);
            if (clause_length) {
                std::vector<uint32> clause_data(clause_length);

                ::ImmGetCompositionString(imc, GCS_COMPCLAUSE,
                    &clause_data[0], clause_size);
                for (int i = 0; i < clause_length - 1; ++i) {
                    cef_composition_underline_t underline;
                    underline.range.from = clause_data[i];
                    underline.range.to = clause_data[i + 1];
                    underline.color = ColorUNDERLINE;
                    underline.background_color = ColorBKCOLOR;
                    underline.thick = 0;

                    // Use thick underline for the target clause.
                    if (underline.range.from >= target_start &&
                        underline.range.to <= target_end) {
                        underline.thick = 1;
                    }
                    underlines.push_back(underline);
                }
            }
        }

    }  // namespace

    OsrImeHandler::OsrImeHandler(HWND hwnd)
        : ime_status_(false),
        hwnd_(hwnd),
        input_language_id_(LANG_USER_DEFAULT),
        is_composing_(false),
        cursor_index_(-1),
        system_caret_(false) {
        ime_rect_ = { -1, -1, 0, 0 };
    }

    OsrImeHandler::~OsrImeHandler() {
        DestroyImeWindow();
    }

    void OsrImeHandler::SetInputLanguage() {
        // Retrieve the current input language from the system's keyboard layout.
        // Using GetKeyboardLayoutName instead of GetKeyboardLayout, because
        // the language from GetKeyboardLayout is the language under where the
        // keyboard layout is installed. And the language from GetKeyboardLayoutName
        // indicates the language of the keyboard layout itself.
        // See crbug.com/344834.
        WCHAR keyboard_layout[KL_NAMELENGTH];
        if (::GetKeyboardLayoutNameW(keyboard_layout)) {
            input_language_id_ =
                static_cast<LANGID>(_wtoi(&keyboard_layout[KL_NAMELENGTH >> 1]));
        } else {
            input_language_id_ = 0x0409;  // Fallback to en-US.
        }
    }

    void OsrImeHandler::CreateImeWindow() {
        // Chinese/Japanese IMEs somehow ignore function calls to
        // ::ImmSetCandidateWindow(), and use the position of the current system
        // caret instead -::GetCaretPos().
        // Therefore, we create a temporary system caret for Chinese IMEs and use
        // it during this input context.
        // Since some third-party Japanese IME also uses ::GetCaretPos() to determine
        // their window position, we also create a caret for Japanese IMEs.
        if (PRIMARYLANGID(input_language_id_) == LANG_CHINESE ||
            PRIMARYLANGID(input_language_id_) == LANG_JAPANESE) {
            if (!system_caret_) {
                if (::CreateCaret(hwnd_, NULL, 1, 1))
                    system_caret_ = true;
            }
        }
    }

    void OsrImeHandler::DestroyImeWindow() {
        // Destroy the system caret if we have created for this IME input context.
        if (system_caret_) {
            ::DestroyCaret();
            system_caret_ = false;
        }
    }

    void OsrImeHandler::MoveImeWindow() {
        // Does nothing when the target window has no input focus.
        if (GetFocus() != hwnd_)
            return;

        CefRect rc = ime_rect_;
        int location = cursor_index_;

        // If location is not specified fall back to the composition range start.
        if (location == -1)
            location = composition_range_.from;

        // Offset location by the composition range start if required.
        if (location >= composition_range_.from)
            location -= composition_range_.from;

        if (location < static_cast<int>(composition_bounds_.size()))
            rc = composition_bounds_[location];
        else
            return;

        HIMC imc = ::ImmGetContext(hwnd_);

        if (imc) {
            const int kCaretMargin = 1;
            if (PRIMARYLANGID(input_language_id_) == LANG_CHINESE) {
                // Chinese IMEs ignore function calls to ::ImmSetCandidateWindow()
                // when a user disables TSF (Text Service Framework) and CUAS (Cicero
                // Unaware Application Support).
                // On the other hand, when a user enables TSF and CUAS, Chinese IMEs
                // ignore the position of the current system caret and use the
                // parameters given to ::ImmSetCandidateWindow() with its 'dwStyle'
                // parameter CFS_CANDIDATEPOS.
                // Therefore, we do not only call ::ImmSetCandidateWindow() but also
                // set the positions of the temporary system caret if it exists.
                /*CANDIDATEFORM candidate_position = {
                    0, CFS_CANDIDATEPOS, { rc.x, rc.y }, { 0, 0, 0, 0 }
                };
                ::ImmSetCandidateWindow(imc, &candidate_position);*/

                COMPOSITIONFORM form = {
                    CFS_POINT,{ rc.x, rc.y },{ rc.x, rc.y, rc.x + rc.width, rc.y + rc.height }
                };

                auto ret = ::ImmSetCompositionWindow(imc, &form);
            }

            if (system_caret_) {
                switch (PRIMARYLANGID(input_language_id_)) {
                case LANG_JAPANESE:
                    ::SetCaretPos(rc.x, rc.y + rc.height);
                    break;
                default:
                    ::SetCaretPos(rc.x, rc.y);
                    break;
                }
            }

            if (PRIMARYLANGID(input_language_id_) == LANG_KOREAN) {
                // Korean IMEs require the lower-left corner of the caret to move their
                // candidate windows.
                rc.y += kCaretMargin;
            }

            COMPOSITIONFORM form = {
                CFS_RECT,{ rc.x, rc.y },{ rc.x, rc.y, rc.x + rc.width, rc.y + rc.height }
            };

            auto ret = ::ImmSetCompositionWindow(imc, &form);
            Debug::Assert(ret != 0);
            //Debug::WriteLine("=======ImmSetCompositionWindow:{0}, Rc:[{1},{2}]", ret != 0, rc.x, rc.y);

            // Japanese IMEs and Korean IMEs also use the rectangle given to
            // ::ImmSetCandidateWindow() with its 'dwStyle' parameter CFS_EXCLUDE
            // Therefore, we also set this parameter here.
            //CANDIDATEFORM exclude_rectangle = {
            //    0, CFS_EXCLUDE, { rc.x, rc.y },
            //    { rc.x, rc.y, rc.x + rc.width, rc.y + rc.height }
            //};

            //auto ret = ::ImmSetCandidateWindow(imc, &exclude_rectangle);
            //Debug::WriteLine("=======ImmSetCandidateWindow:{0}, Rc:[{1},{2}]", ret != 0, rc.x, rc.y);
            ::ImmReleaseContext(hwnd_, imc);
        }
    }

    void OsrImeHandler::CleanupComposition() {
        // Notify the IMM attached to the given window to complete the ongoing
        // composition (when given window is de-activated while composing and
        // re-activated) and reset the composition status.
        if (is_composing_) {
            HIMC imc = ::ImmGetContext(hwnd_);
            if (imc) {
                ::ImmNotifyIME(imc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
                ::ImmReleaseContext(hwnd_, imc);
            }
            ResetComposition();
        }
    }

    void OsrImeHandler::ResetComposition() {
        // Reset the composition status.
        is_composing_ = false;
        cursor_index_ = -1;
    }


    void OsrImeHandler::GetCompositionInfo(
        HIMC imc,
        LPARAM lparam,
        CefString &composition_text,
        std::vector<CefCompositionUnderline> &underlines,
        int& composition_start) {
        // We only care about GCS_COMPATTR, GCS_COMPCLAUSE and GCS_CURSORPOS, and
        // convert them into underlines and selection range respectively.
        underlines.clear();

        int length = static_cast<int>(composition_text.length());

        // Find out the range selected by the user.
        int target_start = length;
        int target_end = length;
        if (lparam & GCS_COMPATTR)
            GetCompositionSelectionRange(imc, &target_start, &target_end);

        // Retrieve the selection range information. If CS_NOMOVECARET is specified
        // it means the cursor should not be moved and we therefore place the caret at
        // the beginning of the composition string. Otherwise we should honour the
        // GCS_CURSORPOS value if it's available.
        // TODO(suzhe): Due to a bug in WebKit we currently can't use selection range
        // with composition string.
        // See: https://bugs.webkit.org/show_bug.cgi?id=40805
        if (!(lparam & CS_NOMOVECARET) && (lparam & GCS_CURSORPOS)) {
            // IMM32 does not support non-zero-width selection in a composition. So
            // always use the caret position as selection range.
            int cursor = ::ImmGetCompositionString(imc, GCS_CURSORPOS, NULL, 0);
            composition_start = cursor;
        } else {
            composition_start = 0;
        }

        // Retrieve the clause segmentations and convert them to underlines.
        if (lparam & GCS_COMPCLAUSE)
            GetCompositionUnderlines(imc, target_start, target_end, underlines);

        // Set default underlines in case there is no clause information.
        if (!underlines.size()) {
            CefCompositionUnderline underline;
            underline.color = ColorUNDERLINE;
            underline.background_color = ColorBKCOLOR;
            if (target_start > 0) {
                underline.range.from = 0;
                underline.range.to = target_start;
                underline.thick = 0;
                underlines.push_back(underline);
            }
            if (target_end > target_start) {
                underline.range.from = target_start;
                underline.range.to = target_end;
                underline.thick = 1;
                underlines.push_back(underline);
            }
            if (target_end < length) {
                underline.range.from = target_end;
                underline.range.to = length;
                underline.thick = 0;
                underlines.push_back(underline);
            }
        }
    }

    bool OsrImeHandler::GetString(HIMC imc, WPARAM lparam, int type,
        CefString& result) {
        if (!(lparam & type))
            return false;
        LONG string_size = ::ImmGetCompositionString(imc, type, NULL, 0);
        if (string_size <= 0)
            return false;

        // For trailing NULL - ImmGetCompositionString excludes that.
        string_size += sizeof(WCHAR);

        std::vector<wchar_t> buffer(string_size);
        ::ImmGetCompositionString(imc, type, &buffer[0], string_size);
        result.FromWString(&buffer[0]);
        return true;
    }

    bool OsrImeHandler::GetResult(LPARAM lparam, CefString& result) {
        bool ret = false;
        HIMC imc = ::ImmGetContext(hwnd_);
        if (imc) {
            ret = GetString(imc, lparam, GCS_RESULTSTR, result);
            ::ImmReleaseContext(hwnd_, imc);
        }
        return ret;
    }

    bool OsrImeHandler::GetComposition(
        LPARAM lparam,
        CefString &composition_text,
        std::vector<CefCompositionUnderline> &underlines,
        int& composition_start) {
        bool ret = false;
        HIMC imc = ::ImmGetContext(hwnd_);
        if (imc) {
            // Copy the composition string to the CompositionText object.
            ret = GetString(imc, lparam, GCS_COMPSTR, composition_text);

            if (ret) {
                // Retrieve the composition underlines and selection range information.
                GetCompositionInfo(imc, lparam, composition_text, underlines,
                    composition_start);

                // Mark that there is an ongoing composition.
                is_composing_ = true;
            }

            ::ImmReleaseContext(hwnd_, imc);
        }
        return ret;
    }

    void OsrImeHandler::DisableIME() {
        CleanupComposition();
        ::ImmAssociateContextEx(hwnd_, NULL, 0);
    }

    void OsrImeHandler::CancelIME() {
        if (is_composing_) {
            HIMC imc = ::ImmGetContext(hwnd_);
            if (imc) {
                ::ImmNotifyIME(imc, NI_COMPOSITIONSTR, CPS_CANCEL, 0);
                ::ImmReleaseContext(hwnd_, imc);
            }
            ResetComposition();
        }
    }

    void OsrImeHandler::EnableIME() {
        // Load the default IME context.
        ::ImmAssociateContextEx(hwnd_, NULL, IACE_DEFAULT);
    }

    void OsrImeHandler::UpdateCaretPosition(int index) {
        // Save the caret position.
        cursor_index_ = index;
        // Move the IME window.
        MoveImeWindow();
    }

    void OsrImeHandler::ChangeCompositionRange(
        const CefRange& selection_range,
        const std::vector<CefRect>& bounds) {
        composition_range_ = selection_range;
        composition_bounds_ = bounds;
        MoveImeWindow();
    }

}  // namespace CefSharp

2、OsrImeWin.cpp 

#include "Stdafx.h"
#include "OsrImeWin.h"
#include "Internals\CefBrowserHostWrapper.h"

namespace CefSharp {
    OsrImeWin::OsrImeWin(IntPtr ownerHWnd, IBrowser^ browser) {
        _ownerHWnd = ownerHWnd;
        _browser = browser;
        _wndProcHandler = gcnew WndProcDelegate(this, &OsrImeWin::WndProc);

        _imeHandler = new OsrImeHandler(static_cast<HWND>(_ownerHWnd.ToPointer()));
    }

    OsrImeWin::~OsrImeWin() {
        _wndProcHandler = nullptr;
        _browser = nullptr;
        if (_imeHandler) {
            delete _imeHandler;
        }
    }

    void OsrImeWin::OnIMESetContext(UINT message, WPARAM wParam, LPARAM lParam) {
        // We handle the IME Composition Window ourselves (but let the IME Candidates
        // Window be handled by IME through DefWindowProc()), so clear the
        // ISC_SHOWUICOMPOSITIONWINDOW flag:
        lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW;
        ::DefWindowProc(static_cast<HWND>(_ownerHWnd.ToPointer()), message, wParam, lParam);

        // Create Caret Window if required
        if (_imeHandler) {
            _imeHandler->CreateImeWindow();
            _imeHandler->MoveImeWindow();
        }
    }

    void OsrImeWin::OnIMEStartComposition() {
        if (_imeHandler) {
            _imeHandler->CreateImeWindow();
            _imeHandler->MoveImeWindow();
            _imeHandler->ResetComposition();
        }
    }

    void OsrImeWin::OnIMEComposition(UINT message, WPARAM wParam, LPARAM lParam) {
        if (_browser && _imeHandler) {
            CefString cTextStr;
            if (_imeHandler->GetResult(lParam, cTextStr)) {
                // Send the text to the browser. The |replacement_range| and
                // |relative_cursor_pos| params are not used on Windows, so provide
                // default invalid values.

                auto host = safe_cast<CefBrowserHostWrapper^>(_browser->GetHost());
                //host->ImeCommitText(cTextStr)
                host->ImeCommitText(cTextStr,
                    CefRange(UINT32_MAX, UINT32_MAX), 0);
                _imeHandler->ResetComposition();
                // Continue reading the composition string - Japanese IMEs send both
                // GCS_RESULTSTR and GCS_COMPSTR.
            }

            std::vector<CefCompositionUnderline> underlines;
            int composition_start = 0;

            if (_imeHandler->GetComposition(lParam, cTextStr, underlines,
                composition_start)) {
                // Send the composition string to the browser. The |replacement_range|
                // param is not used on Windows, so provide a default invalid value.

                auto host = safe_cast<CefBrowserHostWrapper^>(_browser->GetHost());
                host->ImeSetComposition(cTextStr, underlines,
                    CefRange(UINT32_MAX, UINT32_MAX),
                    CefRange(composition_start,
                    static_cast<int>(composition_start + cTextStr.length())));

                // Update the Candidate Window position. The cursor is at the end so
                // subtract 1. This is safe because IMM32 does not support non-zero-width
                // in a composition. Also,  negative values are safely ignored in
                // MoveImeWindow
                _imeHandler->UpdateCaretPosition(composition_start - 1);
            } else {
                OnIMECancelCompositionEvent();
            }
        }
    }

    void OsrImeWin::OnIMECancelCompositionEvent() {
        if (_browser && _imeHandler) {
            _browser->GetHost()->ImeCancelComposition();
            _imeHandler->ResetComposition();
            _imeHandler->DestroyImeWindow();
        }
    }

    void OsrImeWin::OnImeCompositionRangeChanged(Range selectedRange, array<Rect>^ characterBounds) {
        if (_imeHandler) {
            // Convert from view coordinates to device coordinates.
            /*RectList device_bounds;
            CefRenderHandler::RectList::const_iterator it = character_bounds.begin();
            for (; it != character_bounds.end(); ++it) {
                device_bounds.push_back(LogicalToDevice(*it, device_scale_factor_));
            }*/

            std::vector<CefRect> device_bounds;
            for each(Rect rect in characterBounds) {
                CefRect rc(rect.X, rect.Y, rect.Width, rect.Height);
                device_bounds.push_back(rc);
            }

            CefRange selection_range(selectedRange.From, selectedRange.To);
            _imeHandler->ChangeCompositionRange(selection_range, device_bounds);
        }
    }

    void OsrImeWin::OnKeyEvent(int message, int wParam, int lParam) {
        if (!_browser)
            return;

        auto host = safe_cast<CefBrowserHostWrapper^>(_browser->GetHost());
        host->SendKeyEvent(message, wParam, lParam);
    }

    IntPtr OsrImeWin::WndProc(IntPtr hWnd, int message, IntPtr wParam, IntPtr lParam) {
        WPARAM pwParam;
        switch (message) {
            case WM_IME_SETCONTEXT:
                OnIMESetContext((UINT)message,
                    reinterpret_cast<WPARAM>(wParam.ToPointer()),
                    reinterpret_cast<LPARAM>(lParam.ToPointer()));
                return IntPtr::Zero;
            case WM_IME_STARTCOMPOSITION:
                OnIMEStartComposition();
                return IntPtr::Zero;
            case WM_IME_COMPOSITION:
                OnIMEComposition((UINT)message,
                    reinterpret_cast<WPARAM>(wParam.ToPointer()),
                    reinterpret_cast<LPARAM>(lParam.ToPointer()));
                return IntPtr::Zero;
            case WM_IME_ENDCOMPOSITION:
                OnIMECancelCompositionEvent();
                // Let WTL call::DefWindowProc() and release its resources.
                break;
            case WM_SYSCHAR:
            case WM_SYSKEYDOWN:
            case WM_SYSKEYUP:
            case WM_KEYDOWN:
            case WM_KEYUP:
            case WM_CHAR:
                OnKeyEvent(message, reinterpret_cast<int>(wParam.ToPointer()), reinterpret_cast<int>(lParam.ToPointer()));
                break;
        }

        return IntPtr(1);
    }
}

3、C++ChromiumWebBrow 修改要以
github中查看。