Using the InkOverlay Control to Avoid Reinventing the Wheel

As mentioned elsewhere, the InkOverlay control is a superset of the InkCollector. This control enables pen input, pen erasing, and pen selection modes for managing ink collection. For this exercise, you will be adding erase and selection functionality to the ink recognition example.

First, the InkCollector control from the example will need to be replaced with the InkOverlay control. This can be done by taking advantage of the find and replace functionality of Visual Studio. Open the project you created for the previous example and then use the Ctrl+Shift+F shortcut to find all of the instances of the inkOverlay variable. You will be replacing the declaration in Simple RecognizerDlg.h from:

      // an inkCollector control

      CComPtr<IInkCollector> inkCollector;

to

      // an inkOverlay control

      CComPtr<IInkOverlay> inkOverlay;

 

You will next be replacing the calls where the inkCollector was constructed in Simple RecognizerDlg.cpp from:

    if(inkCollector == NULL){

        try{

… … …

            hr = inkCollector.CoCreateInstance(CLSID_InkOverlay);

            if (hr != 0)::MessageBox(NULL,L"3",L"",NULL);

            // TODO: check HR

            hr = inkCollector ->put_hWnd((long)pTargetCtrl->m_hWnd);

            if (hr != 0)::MessageBox(NULL,L"2",L"",NULL);

            hr = this-> inkCollector->put_Enabled(VARIANT_TRUE);

            if (hr != 0)::MessageBox(NULL,L"4",L"",NULL);

to

    if(inkOverlay == NULL){

        try{

… … …

            hr = inkOverlay.CoCreateInstance(CLSID_InkOverlay);

            if (hr != 0)::MessageBox(NULL,L"3",L"",NULL);

            // TODO: check HR

            hr = inkOverlay->put_hWnd((long)pTargetCtrl->m_hWnd);

            if (hr != 0)::MessageBox(NULL,L"2",L"",NULL);

            hr = this->inkOverlay->put_Enabled(VARIANT_TRUE);

            if (hr != 0)::MessageBox(NULL,L"4",L"",NULL);

And finally will be replacing the call where the ink is retrieved from the inkCollector from:

        this->inkCollector->get_Ink(&ink);

to

        this->inkOverlay->get_Ink(&ink);

At this point, you should be able to build and your application will function exactly the same as before. The only difference is that you now are using an InkOverlay instead of an InkCollector control. Now that we have the InkOverlay, we can add buttons that will enable the user to change the selection mode.

First, let’s create a button that will change the input mode from Pen to Eraser. Add a new button to your dialog and change the caption to something more meaningful such as “Eraser Mode”.

The following image illustrates the dialog used for the sample:

Double-click on the “Eraser Mode” button you created and then add the following handler for the button click:

void CSimpleRecognizerDlg::OnBnClickedButton2()

{

      inkOverlay->put_EraserMode(InkOverlayEraserMode::IOERM_PointErase);

      inkOverlay->put_EditingMode(InkOverlayEditingMode::IOEM_Delete);

}

 

This will set the erasing mode to “point” for erasing (rather than stroke) and will set the Editing mode for the overlay to “delete”. Create one more button for turning the editing mode back to Pen and then set the button click handler to the following:

void CSimpleRecognizerDlg::OnBnClickedButton3()

{

      inkOverlay->put_EditingMode(InkOverlayEditingMode::IOEM_Ink);

}

 

This will set the mode back to Inking so that more strokes can be added. Create a final button for turning the editing mode to select and then set the button click handler to the following:

void CSimpleRecognizerDlg::OnBnClickedButton4()

{

      inkOverlay->put_EditingMode(InkOverlayEditingMode::IOEM_Select);

}

Now when you execute the program, you should be able to input strokes, change to eraser (delete) mode and erase them, then change back to pen mode and input more strokes. You should also be able to change the input mode to select, select strokes, and then manipulate them. But wait! There is still a problem. The background window will not be receiving messages that cause it to repaint itself and you will not see the erased / selected content become updated in the background. You can force a repaint by dragging the window off screen and then drag it back for now. We’ll get to a better solution to this in a minute.

The following screenshot shows how the example application runs at this point after adding erase / select. Note that I dragged the window off-screen to force the erased lines to be refreshed on the background window:

 

So you now have the functionality to erase but, frustratingly, the background window does not refresh. This is even worse when you are using the selection tool! How can you resolve this for now? To fix this, we’ll create a custom CWnd that will enable refreshing its parent on certain events (mousing while within the window). First, add a new class to the application. To do this, right-click on the project root, select add-> Class. This will bring up a wizard. Select C++, C++ Class. Click Add and you will see a dialog, you should just set the class name to CTabletCWnd and set the Base class to CWnd. The following image shows how the completed dialog could look:

Add the following includes and the forward class declaration for the CSimpleRecognizerDlg class to the new class header:

#include <msinkaut.h>

#include <iacom.h>

class CSimpleRecognizerDlg;

 

Next, you will be adding a few accessors and event handlers to the class. The following methods should be added to the class:

      void setParentDialogHWND(CSimpleRecognizerDlg* pDialog);

      void setEditingMode(InkOverlayEditingMode newMode);

protected:

      afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);

      afx_msg void OnLButtonDown(UINT nFlags, CPoint point);

      afx_msg void OnMouseMove(UINT nFlags, CPoint point);

      afx_msg void OnLButtonUp(UINT nFlags, CPoint point);

      afx_msg void OnMouseLeave();

      DECLARE_MESSAGE_MAP()

Note the methods under protected: . These are the message handlers. After these functions are declared, the DECLARE_MESSAGE_MAP() macro is called which will add message mapping function declarations to the header file. Now, add the following private members:

private:

      bool redraw;

      void redrawParent(bool force);

      InkOverlayEditingMode editingMode;

These members and accessors will be used to simplify redrawing the parent window.

Now, begin adding implementation code to the cpp file. First, we’ll add the following code which will add the hooks to enable message handling for the messages we are interested in:

BEGIN_MESSAGE_MAP(CTabletCWnd, CWnd)

    ON_WM_CREATE()

      ON_WM_LBUTTONDOWN()

      ON_WM_LBUTTONUP()

      ON_WM_MOUSELEAVE()

      ON_WM_MOUSEMOVE()

END_MESSAGE_MAP()

Now you will be handling the CWnd create, left mouse button down, left mouse button up, mouse leave, and mouse move messages. The constructor will initialize the dialog state variables as we assume the window starts in ink mode and will not need to redraw the window. The following code shows how you can initialize the custom CWnd in the constructor:

 

CTabletCWnd::CTabletCWnd(void)

{

      redraw = false;

      setEditingMode(InkOverlayEditingMode::IOEM_Ink);

}

 

Now we’ll add some code that will allow us to simplify redrawing the parent window. This code will redraw the parent window if we’re set to redraw it, and are either in select mode or erase mode. If force is set to true, the parent window will be redrawn regardless of state.

 

void CTabletCWnd::redrawParent(bool force){

      static int count = 0, thresh=3;

      if ((editingMode != InkOverlayEditingMode::IOEM_Ink && count > thresh) || force){

            this->GetParent()->InvalidateRect(0,1);

            count = 0;

      }else{

            count++;

      }

}

Note that you could make this more sophisticated so that the refresh rate is more frequent when you are using select versus erase. Now add a generic create window handler such as the following:

 

int CTabletCWnd::OnCreate(LPCREATESTRUCT lpCreateStruct)

{

    if ( CWnd::OnCreate(lpCreateStruct) == -1 )

        return -1;

    return 0;

}

Now that you have the create handler, you can add the handler for triggering redraws (left mouse button down):

void CTabletCWnd::OnLButtonDown(UINT nFlags, CPoint point){

      redraw = true;

}

 

Now that you will be knowing when to redraw, add the following functions to turn off the redrawing handler when you leave the CWnd or release the mouse button:

 

void CTabletCWnd::OnMouseLeave(){

      redraw = false;

      redrawParent(true);

}

void CTabletCWnd::OnLButtonUp(UINT nFlags, CPoint point){

      redraw = false;

      redrawParent(true);

}

Now that you have the start / stop redrawing triggers, you will need to have a loop somewhere that will be checking whether to redraw and will redraw the parent window when things have changed. For this exercise, the mouse move handler will be used. The following code shows how this can be done in the mouse move handler:

void CTabletCWnd::OnMouseMove(UINT nFlags, CPoint point){

      if(redraw){

            redrawParent(false);

      }

}

You can also add the following editing mode setter to simplify setting the “editing” mode of the CTabletCWnd:

void CTabletCWnd::setEditingMode(InkOverlayEditingMode newMode){

      editingMode = newMode;

}

Now that you have the custom CWnd, you should change the InkPlaceholder function in the original Simple Recognizer program to use this custom window.

 

First, add the include for the custom CWnd to the Simple RecognizerDlg.h file:

 

#include "TabletCWnd.h"

Next, add a private member to the CSimpleRecognizerDlg class (RecognizerDlg.h):

private:

CTabletCWnd* pTargetCtrl;

 

Now, replace the InkPlaceholder function to use the CTabletCWnd object instead of the CWnd object:

 

void CSimpleRecognizerDlg::InkPlaceholder(){

    if(inkOverlay == NULL){

        try{

                  // the custom window that will refresh its parent

            pTargetCtrl = (CTabletCWnd*)this->GetDlgItem(IDC_PLACEHOLDER);

            DWORD style = pTargetCtrl->GetStyle();

            DWORD styleEx = pTargetCtrl->GetExStyle();

           

            // use the control to figure out how large is the initial on-screen area for the graphics window

            CRect rect;

            pTargetCtrl->GetWindowRect(&rect);

            pTargetCtrl->DestroyWindow();// I <3 destroy!

            pTargetCtrl->m_hWnd = NULL;

            pTargetCtrl = new CTabletCWnd();

            // convert the screen rectangle to the dialog's client coordinates

            this->ScreenToClient(&rect);

            BOOL success = pTargetCtrl->CreateEx(

                styleEx,

                NULL, //CWnd default

                L"Inking", //window name

                style|WS_CHILD|WS_CLIPSIBLINGS|WS_CLIPCHILDREN|WS_VISIBLE,

                rect, // Dimension and location of the place-holder

                this,//this is the parent

                IDC_PLACEHOLDER); //Use the ID of the just destroyed static

            BringWindowToTop();

                  // now cocreate the ink control using the custom CWnd

            HRESULT hr = S_OK;

                  hr = inkOverlay.CoCreateInstance(CLSID_InkOverlay);

            if (hr != 0)::MessageBox(NULL,L"Could not CoCreate inkOverlay",L"Error while setting up inkOverlay",NULL);

            // TODO: check HR

            hr = inkOverlay->put_hWnd((long)pTargetCtrl->m_hWnd);

            if (hr != 0)::MessageBox(NULL,L"Could not put hWnd",L"Error while setting up inkOverlay",NULL);

                  // TODO: check HR

                 

            hr = this->inkOverlay->put_Enabled(VARIANT_TRUE);

            if (hr != 0)::MessageBox(NULL,L"Could not enable inkOverlay",L"Error while setting up inkOverlay",NULL);

            // TODO: check HR

        }catch(CString &message){

            ::MessageBoxEx(NULL, message, L"Error", NULL, NULL);

        }

    }

}

 

One final change to make is that you must update the button clicked handlers to set the editing mode on your custom control:

 

void CSimpleRecognizerDlg::OnBnClickedButton2()

{

      inkOverlay->put_EraserMode(InkOverlayEraserMode::IOERM_PointErase);

      inkOverlay->put_EditingMode(InkOverlayEditingMode::IOEM_Delete);

      pTargetCtrl->setEditingMode(InkOverlayEditingMode::IOEM_Delete);

}

void CSimpleRecognizerDlg::OnBnClickedButton3()

{

      inkOverlay->put_EditingMode(InkOverlayEditingMode::IOEM_Ink);

      pTargetCtrl->setEditingMode(InkOverlayEditingMode::IOEM_Ink);

}

void CSimpleRecognizerDlg::OnBnClickedButton4()

{

      inkOverlay->put_EditingMode(InkOverlayEditingMode::IOEM_Select);

      pTargetCtrl->setEditingMode(InkOverlayEditingMode::IOEM_Select);

}

 

Now when you build and run, your window will be refreshed as you click and move your cursor around on the window.

 

You can now make one more convenient change to enable a nice box around the inking area to make it easier to identify where you can ink content. To do this, go back to the dialog editor in the resource view. Select the static box that is replaced, and set the “Static Edge” field to TRUE. The following screenshot shows how this is done:

 

Now that you have made significant improvements to your application, it’s a good time to sit back, and enjoy your new found ability to easily add ink editing to unmanaged dialogs. The following screenshot shows how the application looks running on my tablet:

 

You can download my projects for Visual Studio 2005 / 2008 here.