MFC for Microsoft Windows CE: Using the Object Store

Kevin Marzec
MSDN Content Development Group

January 1998

Click to copy the sample files for this technical article.

In this article I will discuss some of the issues involved with developing an application using Microsoft® Foundation Classes (MFC) for the Microsoft Windows® CE platform and I'll show how to create a very general database application for Windows CE.

This article assumes a rudimentary knowledge of the MFC framework and a basic understanding of the C++ language. To work with the sample files, or to create the sample yourself, you will need Microsoft Visual C++® version 5.0 and the Microsoft Windows CE Toolkit for Visual C++.

MFC for Windows CE

There are a couple of development limitations in Windows CE that are relevant to the sample application. Windows CE version 2.0 provides a limited subset of the standard MFC classes. Missing most notably from MFC are the control bar classes, such as CToolBar, CStatusBar, and CDialogBar. In their place is a set of command bar functions, which combine menu and toolbar functionality. ToolTips are supported, but only through command bars.

The Data Access Objects (DAO) and Open Database Connectivity (ODBC) MFC classes have been replaced by the new Windows CE database and object store classes. In addition, many existing MFC classes have been slightly modified. Check the Visual C++ for Windows CE documentation (MSDN Library, Tools and Technologies) for details regarding modifications to the MFC classes. For hardware-specific issues, check the Windows CE SDK documentation. (Download the Microsoft Windows CE SDK from the "Free Downloads" area of the Windows CE developer site at http://www.microsoft.com/windowsce/developer/.)

The Object Store and the Database API

The Windows CE Object Store refers to the persistent storage that is available to applications. The object store uses an object identifier (OID) to identify a specific object in the store. To obtain a handle to a particular object, one need only supply the OID. An object may be one of four types: file, directory, record, or database.

Generally, a portion of available RAM is available for user data. This user data is available through three groups of application programming interfaces (APIs): the System Registry API, the File System API, and the Database System API. For the purposes of this article and the sample, I'll focus on the Database System API.

A Windows CE database is a collection of records, each with any number of associated properties. Properties can exist in any of four data types: integer, string, time, and byte array (blob). There are a few new Windows CE–specific MFC classes designed to make handling and manipulating databases and records simpler. I used the following three in developing the sample.

CCeDBDatabase

This class encapsulates all of the required functionality for creating and deleting databases, adding and removing records from a database, and searching through the database for a particular record or records.

Table 1. CCeDBDatabase Methods

Method Explanation
Create Creates a new database
Open Opens an existing database
GetNumRecords Returns the number of records in the database
AddRecord Adds a new record to the database
DeleteCurrRecord Deletes the currently selected record
ReadCurrRecord Retrieves the currently selected record
SeekToRecord Positions the record pointer to the record specified by a certain OID
SeekFirst Positions the record pointer to the first record in the database
SeekNext Advances the record pointer by one record

There are also several seek methods for finding records and numerous utility methods for tracking various properties, such as modification times, names, sizes, identifiers, and sort orders.

CCeDBRecord

The CCeDBRecord class is used to add, remove, retrieve, and count record properties. A single record is represented by the set of properties that it contains. Each property contained within the CCeDBRecord object is an object of type CCeDBProp. A record can contain any number of properties.

Table 1. CCeDBRecord Methods

Method Explanation
GetNumProps Returns the number of properties associated with the record
GetPropFromIndex Retrieves a property based on its zero-based index location
DeleteProp Removes a property from the record
AddProp Adds a new property to the record

In the sample, the GetNumProps() method determines whether text or a sketch (.bmp file) is associated with a record. The first property of a record is the text data, and the second optional property is the byte array of the sketch bitmap. If there are two properties, the sketchpad is initialized with the contents of the second property to display it for editing or viewing. The sample application also uses GetPropFromIndex(), AddProp(), and DeleteProp().

CCeDBProp

The CCeDBProp class is used to create and manipulate record properties. A record property is defined in the Windows CE documentation as:

. . . a data item that consists of an application-defined property identifier, a data type identifier, and the data value. A CCeDBProp object can also be a "sort" property. A sort property contains an application-defined identifier, a data type, and a set of flags that specify sorting information.

The sample application uses the CCeDBProp methods GetString() and SetString() for the text data, and the GetBlob() and SetBlob() methods for the sketch data.

Table 3. CCeDBProp Methods

Method Explanation
GetString Retrieves the string value from a string property
SetString Assigns a string value to a string property
GetBlob Retrieves a byte-array from a blob property
SetBlob Assigns a byte-array to a blob property

The following sample code illustrates the process of opening or creating a database and populating it with a single record that contains a piece of text data:

CCeDBDatabase  myDatabase;
CCeDBRecord    myRecord;
CCeDBProp      myProperty;

// Open the database.
if (myDatabase.Open(_T("SampleDB")) == FALSE)
{
   // Database doesn't exist, attempt to create it
   if (myDatabase.Create(_T("SampleDB")) == FALSE)
   {
      // Handle gracefully.
   }
}

myProperty.SetString(_T("This is a sample text record property."));

// First add the property to the record object. . . 
if (myRecord.AddProp(&myProperty) == FALSE)
{
   // handle gracefully
}

//. . . then add the record to the database.
if (myDatabase.AddRecord(&myRecord) == FALSE)
{
   // Handle gracefully.
}

myDatabase.Close();

A Simple Sample

Each record of the Personal Information Manager (CEPIM) sample can contain up to 4KB of text data (a self-imposed limitation, not a limitation of the Windows CE database), and an optional bitmap sketch. The user can add new records, edit existing records, and delete records. To search, the user enters a text substring (case insensitive). All records that contain that substring as any part of their text data are listed in a results list box. If a user selects a particular record, the associated text data is displayed in a quick-view edit box. If the user selects a record, and then enters edit-mode, he or she is able to modify the text data and view and modify any associated sketch.

The first step in building an MFC-based Windows CE application is to use the AppWizard to generate a project skeleton. Click New on the File menu and select MFC for Windows CE from the Projects tab. Make the application a dialog-based executable.

Designing the Main Dialog

Next, use the Resource Editor to design the initial dialog box (Figure 1). After dragging each control onto the dialog box, use the ClassWizard to attach member variables to each control. Also hook up event handlers for each of the buttons (leaving them empty for now), thus providing an application skeleton.

Figure 1. Main application dialog box

Initializing the Main Dialog Box

First, include wcedb.h in the stdafx.h header file. Next, attempt to open the database in the InitDialog() handler, and if no database exists, create a new one. Call GetNumRecords() on the database and display the number of records read in the status window. Finally, call DisplayAllContaining(), which will populate the list box with records that contain the search string.

Displaying Records

The DisplayAllContaining(char * SearchString) function iterates through each record in the database, retrieving the contents of the first record property (which is the text data). A search is then performed on this text data, attempting to locate the input string. If this substring is found, then the record is displayed in the list box. There are really only two details to pay attention to here: 1) since the search is to be case insensitive, convert both the substring and the text data to lowercase before calling Find(), and 2) verify that the property being retrieved is in fact a string property. Attempting to retrieve a string property (through GetString) on a nonstring property will result in a system error (the utterly enigmatic "An unsupported operation was attempted"); for safety, throw in a check. The DisplayAllContaining() method should look something like this:

// Make the search string lowercase.
// Empty the results box.
// Rewind the database.
// Loop through each record.
   // Read the record.
// Grab the text property.
pProp = pRecord->GetPropFromIndex(0);

// Make sure that it is indeed a string property.
if (pProp && LOWORD (pProp->m_CePropVal.propid) == CEVT_LPWSTR)
{ 
   // Check for match.
   // If found, add the first line of text to the results box.
}
// Seek to the next record.
// End loop
// Display selected record 

If you examine the sample code that accompanies this article, you will notice that I call a function named DisplayTextData() to display the currently selected record. This method looks up the selected record by OID (the OID is stored as the item data in the results box) and retrieves the text. It assigns the text to the member variable that is associated with the QuickView edit control (done through ClassWizard earlier), and then calls UpdateData() to place the text in the box. The DisplayTextData() method is also called by the OnSelChangeResultsBox() handler, which is triggered each time the user selects a record from the list box.

If you look closely at the sample code, you'll also see the OnChangeSearch() handler,  which calls the DisplayAllContaining() function. This handler is called each time the text in the search box changes so that the results box will dynamically display all matching records as the user types in a search string.

Making the Buttons Useful

Now that most of the functionality of the main dialog box has been implemented, it is time to fill in the event handlers for the Add, Edit, and Remove buttons. First write the OnAdd() and OnEdit() methods (for the simple reason that if you wrote OnRemove() first, you'd have no way to test it because you wouldn’t have any records to remove!) Then build a new dialog box to display the record properties (Figure 2). Include an edit control to contain the text data for the record, and a Sketch button to open yet another dialog box that allows the user to edit or create a sketch. As in the previous dialog box, hook up member variables to each of the controls.

Figure 2. Record property dialog box

This same dialog box is used for both editing records and for adding new records. However, if the user is editing a record, the dialog box is initialized with the record's text and sketch data. This is done via the SetSketchData() and SetTextData() methods of our RecordDialog class. When the user finishes filling in or modifying data, the entire dialog object is passed to AddRecord(), removing the old record first, if necessary.

Add another event handler for the Sketch button. I'll come back to implementing the sketch portion of the application; for now, there is enough functionality to add text properties to the records in the database.

Now, implement the OnRemove() handler from the main dialog class, which is a very straightforward task. Since OIDs are stored as item data in the results box, simply grab the OID from the currently selected record, seek to the record, and call DeleteCurrRecord():

mDatabase.SeekToRecord (mResults.GetItemData (mResults.GetCurSel()));
mDatabase.DeleteCurrRecord();

Now that the required functionality has been implemented for adding, editing, and removing records, do a build and test the application. Once you are confident that everything is working the way that it should, take a short break to stretch your legs, and maybe go get a fresh cup of coffee.

Getting Sketchy

The SketchPad is another dialog box object, and it is initialized with a byte array (blob) of sketch data when a sketch is being edited. For new records, no data is passed in, so the window is empty. There is a series of toolbar buttons at the top of the window for basic drawing functionality, such as changing the pen size, erasing, clearing the screen, and saving (Figure 3).

Figure 3. SketchPad dialog box

Command bars

First, add the command bar and hook-up event handlers for each button. Next add drawing capability by intercepting WM_MOUSEMOVE and WM_LBUTTONDOWN messages. Finally, fill in the code for each of the toolbar buttons.

The easiest way to see how to implement the command bar is to look directly at the InitDialog() method of the CSketchPad class (located in SketchPad.cpp). Basically, there are four simple steps. First, call CommandBar_Create() to obtain a handle to a newly created command bar. Next, call CommandBar_AddBitmap() for each bitmap that you have. Then call CommandBar_AddButtons() to place the buttons on the command bar with the bitmaps on them. Finally, call CommandBar_Show() to display the command bar. Each of the buttons is a TBBUTTON struct, and through the ClassWizard you can hook each of these button IDs up to an event handler, which for now should be left empty. Initialize the two pens (one for drawing, one for erasing) and set up the bitmap header information. At this point, hook up the Record Properties dialog box Sketch button to the newly created SketchPad dialog box, making sure to pass across any sketch data before calling DoModal().

Sketch functionality

Like I've said before, the data structure for the sketch data is a byte array, or blob. Blobs are easy to work with in the scheme of the CE object store—they are handled as CEBLOB objects. A CEBLOB object contains two fields: CEBLOB.lpb, which points to the data itself, and CEBLOB.dwCount, which identifies the size of the array. CCeDBProp objects contain two methods for dealing with blobs, GetBlob() and SetBlob(). To store the user-drawn sketch into a standard byte array, first save the sketch as a standard CBitmap. Then use memcpy() to transfer the bytes from the bitmap's buffer to the BYTE array. If the dialog box is being used to display an existing sketch, copy the byte array from the current record into a CBitmap object, and then display it in the dialog box. Otherwise, the sketch data will be initially empty.

Next, implement the drawing functionality by intercepting MouseDown (WM_LBUTTONDOWN) and MouseMove (WM_MOUSEMOSE) events. When the mouse button is clicked, the mouse location is stored. Then, on each MouseMove(), draw a line from the stored point to the new point, and then store the new point where the old point was. That's all there is to it. See the code in SketchData.cpp if you are at a loss. Now hook up a SaveSketch() method to the dialog-box close event, build the application, and create and edit records with (almost) full sketch capability!

Completion . . .

All that is left to do now is add the functionality of the command bar buttons. There should be six handlers (all currently empty), one for each button. Since you've already created a SaveSketch() function, implementing the first button is very easy—save the sketch and close the dialog box. The next two are very simple also. Delete the current pen objects and then recreate them a bit larger or smaller, depending on which action the user selected. Likewise, the draw and erase toggle buttons are a snap to add. Keep a Boolean flag member set to the current drawing state (TRUE for drawing, FALSE for erasing). The last one, Clear Sketch, is no harder than the rest. Call FillSolidRect() on the entire client area of the dialog box. Once all these handlers are filled in, the easy part is over and you get to move on to the next step of application development—testing and debugging! I will bow out now, for having gotten this far (and with a bit of luck) you should have no problem from here.