Tablet PC Development Notes: Using a Timestamp on Strokes

 

Microsoft Corporation

June 2003

Applies to:
   Microsoft® Windows® XP Tablet PC Edition

Summary: This document describes a recommended practice for storing timestamp information with ink strokes when developing applications designed for the Tablet PC. Applications written using the Windows XP Tablet PC Edition SDK version 1.5 and earlier and that plan to interoperate with future versions of the Tablet PC handwriting recognizers should store timestamps indicating the creation time of each stroke. This will improve recognition accuracy for the strokes when used in future versions of the recognizers. Samples are provided in C# and C++. (8 printed pages)

Contents

Introduction
Storing a Timestamp with Your Strokes

Introduction

You should store a timestamp, in absolute time, indicating the creation time of each Stroke object if:

  • Your application is written for use with Microsoft® Windows® XP Tablet PC Edition.
  • Your application was written based on the Windows XP Tablet PC Edition SDK version 1.5 or earlier.
  • Your application currently uses stroke creation time.
  • You plan to have your application interoperate with future versions of the Tablet PC handwriting recognizers.

The timestamp is stored in an extended property with a specific GUID on each stroke.

Future versions of the Windows XP Tablet PC Edition SDK recognition system will likely use algorithms that rely on having access to the correct stroke creation order. Timestamps are the ideal mechanism for tracking the creation order of strokes. Timestamps are better than simpler integer-based indexing for two key reasons:

  • Because some applications alter the order of strokes within a single Ink object, having applications also manage a second order index becomes too cumbersome in more complex scenarios.
  • Many applications utilize multiple Ink objects. This makes integer-based indexing impractical because all Ink objects may not be in memory at any one time.

Applications that currently do not store timestamps will continue to work with future versions of the recognition system; however, these applications may not achieve the same level of recognition accuracy, particularly for free-form ink pages.

Future versions of the Tablet SDK will likely be modified to store this information automatically as each Stroke object is created.

This article discusses:

  • Reasons for storing a timestamp with ink strokes.
  • How to store timestamps with ink strokes.

This article does not discuss:

  • How to create or manipulate strokes.
  • Methods of incorporating recognizers.

Storing a Timestamp with Your Strokes

Your application should store a timestamp as an extended property on each Stroke object. The property indicates the time at which the Stroke object was created. For details on extended properties, see Extended Properties Collection in the Microsoft Windows XP Tablet PC Edition SDK.

When working in C++, the data format of the timestamp should be a FILETIME data structure, which is a 64-bit value representing the number of 100-nanosecond intervals since January 1, 1601. The FILETIME structure is declared in Winbase.h, so you must include Windows.h in your application. The timestamp should be stored in Universal Time Coordinates (UTC) format. The FILETIME structure is defined as follows:

typedef struct STRUCT tagFILETIME
{
   DWORD dwLowDateTime;
   DWORD dwHighDateTime;
} FILETIME;

The extended property for the timestamp is a byte array. The GUID for the extended property is {8A54CF58-97E6-4fc5-8F06-F8BAD2E19B22}. The FILETIME structure should be the only data stored with this GUID, and it should be no larger than two DWORDs. If you do not use this GUID value, future versions of the recognizers will not be able to make use of the timestamp value.

The FILETIME structure should be set at the time the Stroke object is created, and the FILETIME structure should not be modified thereafter. All editing operations such as pasting, splitting, copying, or cloning a Stroke object should copy forward the Stroke object's FILETIME value without modification.

For more information about the FILETIME structure, see FILETIME in the MSDN Library.

C# Code Example

The following C# code shows how to set the timestamp extended property on a Stroke object.

private void myInkCollector_Stroke(object sender,
     InkCollectorStrokeEventArgs e )
{
  //Create the GUID.
  System.Guid TIMESTAMPGuid = new Guid(
            "8A54CF58-97E6-4fc5-8F06-F8BAD2E19B22");
          
  //Check to see if the GUID already exists as an extended 
  //property on the stroke.  If it does not exist, then write.
  if(!e.Stroke.ExtendedProperties.DoesPropertyExist(TIMESTAMPGuid))
  {
    try
    {
      //Retrieve the current time, UTC format.
      DateTime dt = DateTime.Now;
      long filetime = dt.ToFileTime();
          
      //Write the time to a temporary byte array.
      MemoryStream memstr = new MemoryStream();
      BinaryWriter binwrite = new BinaryWriter(memstr);

      binwrite.Write(filetime);
      Byte[] bt = memstr.ToArray();
      
      memstr.Close();
      binwrite.Close();
      
      //Save the extended property on the stroke.
      e.Stroke.ExtendedProperties.Add(TIMESTAMPGuid, bt);
    }
    catch (Exception ex)
    {
      string msg = ex.Message;
    }
  }
}

C++ Code Example

The following C++ code shows how to set the timestamp extended property on a Stroke object.

/////////////////////////////////////////////////////////
// InkCollector event handlers 
//                                          
// CTestApp::OnStroke
//  
// The _IInkCollectorEvents's Stroke event handler.
// See the Tablet PC Automation API Reference for the 
// detailed description of the event and its parameters.
//
// Parameters:
//      IInkCursor* pIInkCursor     : [in] not used here
//      IInkStrokeDisp* pInkStroke  : [in] the new stroke's interface
//               pointer
//      VARIANT_BOOL* pbCancel      : [in,out] option to cancel the
//               gesture, default value is FALSE, not modified here
//
// Return Values (HRESULT):
//      S_OK if succeeded, E_FAIL or E_INVALIDARG otherwise
//
/////////////////////////////////////////////////////////
HRESULT CTestApp::OnStroke(
        IInkCursor* pIInkCursor, 
        IInkStrokeDisp* pIInkStroke, 
        VARIANT_BOOL* pbCancel
        )
{
  if (NULL == pIInkStroke)
         return E_INVALIDARG;

  //Define the timestamp GUID.
  #ifndef GUID_STROKE_TIMESTAMP
  #define GUID_STROKE_TIMESTAMP L"{8A54CF58-97E6-4fc5-8F06-F8BAD2E19B22}"
  #endif
     
  //A pointer to the extended properties.
  CComPtr<IInkExtendedProperties>  spIIExtendedProperties;

  //Get the collection of extended properties.
  if (SUCCEEDED(
    pIInkStroke->get_ExtendedProperties(&spIIExtendedProperties)))
  {
    //Set the correct GUID.
    CComBSTR bstrGuid(GUID_STROKE_TIMESTAMP);
            
    VARIANT_BOOL bExists;
    if (SUCCEEDED
         (spIIExtendedProperties->DoesPropertyExist
            (bstrGuid, &bExists)
         ) 
         && 
         (VARIANT_FALSE == bExists)
       )
    {
      //Get the current time, UTC format.
      FILETIME         tmFiletime;
      CoFileTimeNow (&tmFiletime);
      CComVariant     vExtendedProperty;

      //Call a helper method that converts 
      //the byte array into a variant array.
      if  (SUCCEEDED
            (BufferToUI1VariantArray 
                ((BYTE*)&tmFiletime, 
                sizeof(FILETIME),
                &vExtendedProperty)
            )
          )
      {
        //Add the extended property.
        CComPtr<IInkExtendedProperty> spInkExtendedProperty;
        spIIExtendedProperties->Add(
                bstrGuid, 
                vExtendedProperty, 
                &spInkExtendedProperty);
      }            
    }        
  }
  
  return hr;
}
///////////////////////////////////////////////////////////////////////
///    Utility function to convert a byte array to a Variant array
///////////////////////////////////////////////////////////////////////
HRESULT BufferToUI1VariantArray ( void *pvData, long cbData, 
                                  VARIANT *pvarArray) 
{
  HRESULT        hr = S_OK;
  SAFEARRAY     *psa = NULL;
  VARIANT HUGEP *pVar = NULL;

  pvarArray->parray = NULL;
  SAFEARRAY *&rpsa = pvarArray->parray;

  // Allocate the safe array.
  psa = ::SafeArrayCreateVector(
        VT_UI4, 0, 
        static_cast<unsigned long>(cbData));
  
  //Check allocation succeeded.
  if (psa == NULL)
  {
        hr = E_OUTOFMEMORY;
  }

  if (SUCCEEDED(hr))
  {
    //Lock the safe array and get the pointer to the data.
    hr = ::SafeArrayAccessData(
          psa,
          reinterpret_cast<void HUGEP **>(&pVar));
    if (SUCCEEDED(hr))
    {
      //Copy the data to the safe array.
      memcpy(pVar, pvData, cbData);
      //Unlock the safe array.
      hr = ::SafeArrayUnaccessData(psa);
    }
  }
  //Set the return result.
  if (SUCCEEDED(hr))
  {
    rpsa = psa;
    pvarArray->vt = VT_ARRAY | VT_UI1;
  }

  if (FAILED(hr) && (NULL != psa))
  {
     //Clean up the safe array in case of error.
    (void)::SafeArrayDestroy(psa);
  }

  return (hr);
}