Windows CE Graphics Programming

Bob Powell

Windows CE has revitalized hand-held computing by providing a clear standard for program design based upon the Windows API. There are a few pitfalls into which the unsuspecting developer can stumble. In this article, Bob explores and explains some of the more common problems in converting existing software from the standard Windows GDI to the Windows CE GDI.

Because I'm very used to Windows programming and do a lot of GDI graphics, my first intention was to do a quick conversion of some of my existing applications to a hand-held format. After all, the CE version of MFC handles most of the differences between the platforms, right? This is when the differences between the Windows GDI and Windows CE Multi-platform Graphic Device Interface (MGDI, the CE GDI) started to show up.

Mapping modes

The most important difference is the way that Windows CE GDIs handle logical mapping modes. They don't! A CE GDI only uses MM_TEXT, has a fixed origin, and has no way of changing the viewport or window extents for conversion between logical and device coordinates. This makes the CScrollView type of window a little difficult to use-an important consideration when you look at the screen real estate available to you on a CE machine like the HP 620.

Under the standard Windows GDI, using a CScrollView is relatively simple. All you do is scroll the bars, and the CScrollView-based class moves the origin of the logical coordinates for you. When the repaint is performed, all graphical output is sent to the same logical position onscreen whether unscrolled or scrolled. The output is modified by the origin offset so that the pixels are in a different place on the device surface.

For example, to create a simple scrolling application with the standard Windows GDI, follow these steps:

1.Create a new MFC application using the App Wizard. Call it "SVdemoGDI" and click OK.

2.Click Next on every step except the last dialog box in the wizard. Change the base class of the CSVdemoGDIView class to "CScrollView."

3.Click Finish.

4.Change the view's OnDraw() code to look like this:

void
CSVdemoGDIView::OnDraw(CDC* pDC)

{

  CSVdemoGDIDoc* pDoc = GetDocument();

  ASSERT_VALID(pDoc);

// Draw an absolute rectangle

  pDC->Rectangle(CRect(20,20,120,70)); 

}

5.Change the view's OnInitialUpdate() to look like this:

void
CSVdemoGDIView::OnInitialUpdate()

{

  CScrollView::OnInitialUpdate();

// Give the scrollbars something
to bite on.

  CSize sizeTotal;

  sizeTotal.cx = sizeTotal.cy = 1000;

  SetScrollSizes(MM_TEXT, sizeTotal);

}

If you build and run this first example as a standard Windows NT or 98 application, it gives you a simple document in which the whole client area may be scrolled using the scroll bars.

Now compare that same program written for the CE 2.0 platform. There is a CE App Wizard, and I created an application called SVdemoCEGDI. Just as in the first example, I clicked Next on every pane until the last one and changed the view's base class to CScrollView. The code for OnInitialUpdate() is just like the first example, but here's the OnDraw() code:

void
CSVdemoCEGDIView::OnDraw(CDC* pDC)

{

  CSVdemoCEGDIDoc* pDoc = GetDocument();

  ASSERT_VALID(pDoc);

  // Windows CE doesn't make much use of 

  // OnEraseBackground, so we need to do it ourselves.

  CRect rectClient;

  GetClientRect(&rectClient);

  CBrush b(RGB(255,255,255));

  pDC->FillRect(rectClient,&b);

  b.DeleteObject();

  // Under CE we need to find the scroll position...

  CPoint pSPos=GetScrollPosition(); 

  // note that this function isn't in the 

  // Standard Win32 API

 // and offset ALL of our drawing by that amount...

  pDC->Rectangle(CRect(20-pSPos.x,20-pSPos.y,

                       120-pSPos.x,70-pSPos.y));

}

Note that the Windows CE CScrollView class has a function for retrieving the amount by which the scroll bars are offset. This function isn't available in the standard MFC API.

You can clearly see that without the origin-offset capability, it becomes your responsibility to add the desired scroll amount to every operation that references a screen coordinate. This would include the line drawing functions, the text placement functions, and indeed anything that referenced a particular position onscreen. The conversion process from GDI to MGDI could be laborious if you make extensive use of mapping modes for zooming and scrolling. If you ever contemplate creating any cross-platform graphics software from scratch, it might be simpler to forget the mapping modes on the standard Windows system altogether. It would be easier to devise a graphics strategy that always assumes the output will be to an MM_TEXT device context and handle the scrollbar offsets yourself on both CE and NT/95 machines.

Missing functions: Text output and stroke paths

Windows CE doesn't support TextOut()-use DrawText() instead. Also, the Windows CE GDI doesn't support the CDC::BeginPath() or CDC::EndPath() methods.

Regions

I hope that the decision to implement regions so incompletely in Windows CE will be reconsidered soon. There are few graphical functions more important than the ability to clip to or combine polygonal regions. I admit that at the moment, the processing power of Windows CE devices is a little limited, so there aren't many programs that use this feature, but it won't be long before that improves and more competent graphics are required.

The difference between the ordinary Windows GDI region object and the MGDI region seems to be only in the way that they're created. The CE region object only has the CreateRectRgn() and CreateRectRgnIndirect() functions. Regions appear to be stored in exactly the same way on both platforms. (You can check with GetRgnData(), available in both APIs.) Unfortunately, the CE GDI has no corresponding CreateFromData() function; otherwise, a simple workaround could be made that would allow complex regions to be created.

A region is stored as a list of one or more rectangles that tessellate to cover a given area. Examples include a simple rectangular region, containing only one rectangle, or a region that combines a few rectangles, perhaps two overlapping ones XORed together. When Windows NT or 98 creates a polygonal region, the GDI code does a scan-line analysis of the polygon, similar to the way a television image is constructed from horizontal lines. This can create a list of hundreds of rectangles, one for each scan-line element. It's this functionality that's missing from the MGDI. Figure 1 shows how the rectangles are typically arranged. On the left, the outlines of the basic shapes are shown. They're a simple rectangle, a complex region created by the exclusive-or of two rectangles, and a polygon. In the center are the tessellations of rectangles that make up regions in GDI. On the right, the same is shown for MGDI regions. The polygonal shape to the right is crossed out because these can't be created on a CE device.

To illustrate the use of rectangular regions, I've modified the OnDraw() functions of both the Windows and Windows CE applications that we generated for the scrolling demo. A clipping region is selected into the DC, and the whole client area is filled with a color. The shape left on the screen is the "inside" of the region. The output from this program is shown in Figure 2. Here's the new OnDraw(), which is identical on the CE and Windows 98 or NT machines:

void
CSVdemoGDIView::OnDraw(CDC* pDC)

// or CSVdemoCEGDIView

{

  CSVdemoGDIDoc* pDoc = GetDocument();

  ASSERT_VALID(pDoc);

  CRgn ra,rb,rc;

  CRect rect(20,20,120,70);

  // create a region

  ra.CreateRectRgnIndirect(&rect); 

  

  // move the rectangle

  rect.OffsetRect(50,25);

  // create a second region

  rb.CreateRectRgnIndirect(&rect); 

  // create a valid but temporary region

  rc.CreateRectRgn(0,0,0,0); 

  // combine the regions with xor

  rc.CombineRgn(&ra,&rb,RGN_XOR); 

  // select the resulting clipping region into the DC

  pDC->SelectClipRgn(&rc,RGN_COPY); 

  // make a red brush

  CBrush b(RGB(255,0,0));

  // find the client rectangle

  GetClientRect(&rect);

  // and fill the whole screen with red

  pDC->FillRect(&rect,&b); 

  // clean up

  ra.DeleteObject();

  rb.DeleteObject();

  rc.DeleteObject();

  b.DeleteObject();

}

With standard Windows, I can redo this code to use polygonal regions, like this:

void
CSVdemoGDIView::OnDraw(CDC* pDC)

{

  CPoint p[5];

  // this is a polygonal diamond shape

  p[0]=CPoint(20,35);

  p[1]=CPoint(60,20);

  p[2]=CPoint(120,35);

  p[3]=CPoint(60,70);

  p[4]=CPoint(20,35); 

  ra.CreatePolygonRgn(&p[0],5,ALTERNATE);

  

  // move the polygon

  for(int i=0;i<5;i++) 

     p[i].y+=30;

  

  rb.CreatePolygonRgn(&p[0],5,ALTERNATE);

  // create a valid but temporary region

  rc.CreateRectRgn(0,0,0,0);     

  // combine the regions with xor

  rc.CombineRgn(&ra,&rb,RGN_XOR);

  // select the resulting clipping region into the DC

  pDC->SelectClipRgn(&rc,RGN_COPY); 

  // make a red brush

  CBrush b(RGB(255,0,0));          

  // find the client rectangle

  GetClientRect(&rect);

  // and fill the whole screen with red

  pDC->FillRect(&rect,&b);

  // clean up

  ra.DeleteObject();

  rb.DeleteObject();

  rc.DeleteObject();

  b.DeleteObject();

}

This code won't work on Windows CE. Interestingly enough, it seems to be possible to accumulate rectangles in an MGDI region so that the final result is a correctly formed polygonal region. Take a look:

void
CSVdemoCEGDIView::OnDraw(CDC* pDC)

{

  CRgn ra,rb,rc;

  CRect rect;

  ra.CreateRectRgn(0,0,0,0);

  rb.CreateRectRgn(0,0,0,0);

  rc.CreateRectRgn(0,0,0,0);

  // These for loops create a diamond shape...

  for(int i=0;i<35;i++)

  {

     rb.DeleteObject();

     rb.CreateRectRgn(20+(35-i),20+i,55+i,21+i);

     rc.CombineRgn(&ra,&rb,RGN_OR);

     ra.CopyRgn(&rc);

  }

  for(i;i<70;i++)

  {

     rb.DeleteObject();

     rb.CreateRectRgn(20+(i-35),20+i,90-(i-35),21+i);

     rc.CombineRgn(&ra,&rb,RGN_OR);

     ra.CopyRgn(&rc);

  }

  // which is copied.

  rb.CopyRgn(&ra);

  // The original is shifted down...

  ra.OffsetRgn(0,35);

  // and the regions are XORed together.

  rc.DeleteObject();
/p>
  rc.CreateRectRgn(0,0,0,0);

  rc.CombineRgn(&ra,&rb,RGN_XOR);

  pDC->SelectClipRgn(&rc);

  // Make a red brush...

  CBrush b(RGB(255,0,0)); 

  // find the client rectangle...

  GetClientRect(&rect);  

  // and fill the whole screen with red.

  pDC->FillRect(&rect,&b);

  // clean up

  ra.DeleteObject();

  rb.DeleteObject();

  rc.DeleteObject();

  b.DeleteObject();       

}

The proof that Windows CE regions aren't completely brain-dead lies in this devious method for creating polygonal regions. Admittedly, this isn't an efficient way of performing this task, but it proves that the region functions are more capable than the CE SDK documentation would have us believe. The output from the code in this example is shown in Figure 3.

Line drawing commands

This discrepancy is an odd one. The Windows CE API doesn't have the good old standard MoveTo(x,y) and LineTo(x,y) functions, and the recommended alternative is an unwieldy use of a POINT array and the PolyLine function. The MFC for CE CDC class, however, has MoveTo and LineTo built in. It seems that someone at Microsoft just couldn't live without them.

Here's another version of OnDraw(), showing how the functions will run quite happily on a Windows machine but not on a CE device:

void
CSVdemoCEGDIView::OnDraw(CDC* pDC)

{

  pDC->MoveTo(20,20);

  pDC->LineTo(50,50); 

  // works happily on CDC on both platforms

  CPoint p[2];

  p[0]=CPoint(20,30);

  p[1]=CPoint(50,60); 

  HDC hdc=pDC->GetSafeHdc();

  // now we can use the actual DC handle

  ::Polyline(hdc,(LPPOINT)&p[0],2); 

  // this works on both platforms too.

  

#ifndef UNDER_CE

  // This only works on the Windows platforms.

  // If you remove the conditional compile, 

  // you'll get a compile time error.

  MoveTo(hdc,20,40);

/* if moveto won't compile,
use...

  CPoint lastPoint(0,0);

  MoveToEx(hdc,20,40,&lastPoint);

*/

LineTo(hdc,50,70); 

#endif

}

Note that later versions of Windows might use MoveToEx() instead of MoveTo(). This function isn't available on Windows CE either.

Multi-platform programming tip

As you can see, the differences between the SDKs can make it difficult to maintain an application for both platforms. A common preprocessor command that can be used for conditional compilation is UNDER_CE:

#ifndefUNDER_CE

  standard windows code here...

#else

  Windows CE code here

#endif //UNDER_CE

Bob Powell is a senior developer with Rogue Wave software, where he manages the Objective Chart project. He currently lives and works in North Carolina. www.stingray.com, www.roguewave.com, bobpowell@stingray.com.