How can I reposition my controls so they aren't covered by the touch keyboard?


Last week, we saw how to tell the Windows 8 touch keyboard to appear automatically in a desktop program when focus enters an edit control. But we did nothing to prevent the touch keyboard from covering the edit control.

To find out when the touch keyboard appears and disappears, you register with the framework input pane. The framework input pane tells you the current location of the keyboard and informs you when the keyboard state changes. (Note that it does not tell you about the keyboard when it is in floating mode, on the theory that when the user floats the keyboard, the user has taken responsibility for making sure it doesn't cover anything interesting.)

Take our program from last week and make these changes:

#include <strsafe.h>

using namespace Microsoft::WRL;

// Add immediately before DoLayout

ComPtr<IFrameworkInputPane> g_frameworkInputPane;
DWORD g_cookie;
RECT g_rcKeyboard;

In a real program, of course, we wouldn't use global variables, but this is a Little Program.

void Relayout(HWND hwnd,
  bool isKeyboardShowing = false);

class Handler : public RuntimeClass<
  RuntimeClassFlags<RuntimeClassType::ClassicCom>,
  IFrameworkInputPaneHandler>
{
public:
  Handler(HWND hwnd) : m_hwnd(hwnd) { }

  STDMETHODIMP Hiding(BOOL fEnsureFocusedElementInView) override
  {
    SetRectEmpty(&g_rcKeyboard);
    Relayout(m_hwnd);
    return S_OK;
  }

  STDMETHODIMP Showing(RECT *prcScreenLocation,
      BOOL fEnsureFocusedElementInView) override
  {
    g_rcKeyboard = *prcScreenLocation;
    Relayout(m_hwnd, true);
    return S_OK;
  }
private:
  HWND m_hwnd;
};

Our Handler class implements the IFramework­Input­Pane­Handler interface so it can be called when the input pane shows and hides. When this happens, we update the keyboard rectangle and ask our window to update its layout in response to the new information. We pass a special flag that indicates whether the call is in response to the keyboard showing, because we want to do extra work in that case.

void
DoLayout(HWND hwnd, int cx, int cy, bool isKeyboardShowing = false)
{
  // Just for fun, put the keyboard position in the title bar.
  if (IsRectEmpty(&g_rcKeyboard)) {
    SetWindowText(hwnd, TEXT("Keyboard is not visible"));
  } else {
    TCHAR message[256];
    StringCchPrintf(message, ARRAYSIZE(message),
      TEXT("Keyboard is at (%d,%d)-(%d,%d)"),
      g_rcKeyboard.left, g_rcKeyboard.top,
      g_rcKeyboard.right, g_rcKeyboard.bottom);
    SetWindowText(hwnd, message);
  }

  if (g_hwndChild) {
    int cyEdit = cy;
    if (!IsRectEmpty(&g_rcKeyboard)) {
      RECT rcEdit = { 0, 0, cx - 100, cy };
      RECT rcKeyboardClient = g_rcKeyboard;
      MapWindowRect(nullptr, hwnd, &rcKeyboardClient);
      RECT rc;
      if (IntersectRect(&rc, &rcEdit, &rcKeyboardClient)) {
        cyEdit = min(rcKeyboardClient.top, cy);
      }
    }
    MoveWindow(g_hwndChild, 0, 0, cx - 100, cyEdit, TRUE);
    if (isKeyboardShowing) {
      SendMessage(g_hwndChild, EM_SCROLLCARET, 0, 0);
    }
  }
  if (g_hwndButton) {
    MoveWindow(g_hwndButton, cx - 100, 0, 100, 50, TRUE);
  }
}

First, we update the title bar to show where we think the keyboard is, just so it's easier to follow what's happening. And then the actual action: If the keyboard is visible and it overlaps the edit control, then we resize the edit control to avoid it. And if the keyboard is showing, then we scroll the edit control so that the caret is visible. We don't want to force the caret visible in the general case, because that would cause the contents to scroll at unexpected times.

void
OnMove(HWND hwnd, int x, int y)
{
  Relayout(hwnd);
}

When the window moves, we want to perform relayout, because the window may have moved in such a way that the edit control is obscured by the keyboard.

void
Relayout(HWND hwnd, bool isKeyboardShowing);
{
  RECT rc;
  GetClientRect(hwnd, &rc);
  DoLayout(hwnd, rc.right, rc.bottom, isKeyboardShowing);
}

This function is kind of anticlimactic. To perform relayout, we get the client rectangle and ask DoLayout to lay out the contents inside that rectangle.

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
  ...
  EnableTouchKeyboardFocusTracking();

  CoCreateInstance(__uuidof(FrameworkInputPane), nullptr,
    CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&g_frameworkInputPane));
  g_frameworkInputPane->AdviseWithHWND(hwnd,
    Make<Handler>(hwnd).Get(),
    &g_cookie);

  return TRUE;
}

Here is how the ball gets set into motion: When we create the window, we also register our handler with the framework input pane, so that we are called back when the keyboard moves.

void
OnDestroy(HWND hwnd)
{
  if (g_cookie) {
    g_frameworkInputPane->Unadvise(g_cookie);
  }
  g_frameworkInputPane = nullptr;
  PostQuitMessage(0);
}

And some bookkeeping: When the window is destroyed, we unregister from the framework input pane. We also need to release the framework input pane before COM get uninitialized. (This wouldn't have been necessary if we had put the framework input pane in a member variable, since the member variable would be destructed at window destruction. But we were lazy and used a global variable, and now we pay the price.)

If you take this program out for a ride, you'll see that it manages to resize the edit control so that it is not covered by the touch keyboard. But there's still a problem: What if the window is near the bottom of the screen, and the user calls up the touch keyboard? The entire edit control ends up obscured! No amount of resizing will fix this. We'll look at this problem next time.

Comments (8)
  1. skSdnW says:

    MSDN does not say anything about 0 being a invalid cookie value. Is "if (g_cookie)" really safe? What if the cookie is just an array index in Win vNext?

  2. anonymouscommenter says:

    MSDN on IConnectionPoint::Advise says about pdwCookie:

    If the connection was not successfully established, this value is zero.

    So yes, 0 is not a valid cookie value.

  3. anonymouscommenter says:

    @Andre, IFrameworkInputPane does not inherit from IConnectionPoint, they're totally unrelated.  So, there's no point in your argument, you can have 0 cookie.

    [It's a general rule for the Advise pattern, in the same way that returning S_FALSE from Next is a general rule for the IEnumXxx pattern. -Raymond]
  4. anonymouscommenter says:

    The COM interface is nice, but isn't there a Win32 plain "C" API?

    If so, could you please point me into the right direction?

    Thanks!

    [From C, call CoCreateInstance, and then use the lpVtbl, e.g. g_frameworkInputPane->lpVtbl->AdviseWithHWND(g_frameworkInputPane, hwnd, handler, &g_cookie);. You can build the handler out of a vtable also. -Raymond]
  5. anonymouscommenter says:

    This looks like cross-process COM so probably not.

  6. anonymouscommenter says:

    Is there actually a set of documents for how to do COM in C or is it just tribal knowledge? I never find myself wanting any COM API where C++ is in use, but only .NET or some degenerate C context where no stdlibs are available.

    [You just do manually what C++ does natively. It's like asking for documents on how to write a for loop in assembly. Um, you just do in assembly what the C compiler does natively. -Raymond]
  7. anonymouscommenter says:

    I think you missed the point. C++ ABI does not normally work across compilers but suddenly does when its a COM class. There's no C++ ABI document. Yet somehow COM spans Compiler vendors. This was arranged to happen somehow. "Do what the compiler does" is asking for a non-true assumption that has held so far but suddenly won't in Windows next.

    [I described the vtable layout earlier. You can also see this layout if you look at the header file spit out by MIDL. -Raymond]
  8. anonymouscommenter says:

    I had the misfortune to use a PC that had been preinstalled to work with a gamepad rather than the mouse and keyboard that I had attached to it. Not only did it keep on opening the OSK over whichever control it was that I wanted to click next, but it wouldn't let me permanently dismiss it (at least, not without discovering the option buried in the Control Panel, or via the correct solution, which was to reinstall Windows from scratch as I didn't want that installation anyway).

Comments are closed.

Skip to main content