[转]Windows CE .NET Application Development: What Are My Options?

原文地址:http://msdn.microsoft.com/en-us/library/aa459138.aspx
Mike Hall
Microsoft Corporation

Steve Maillet
Entelechy Consulting

February 7, 2003

Summary: Compares advantages of the three development options now available to application developers targeting Microsoft Windows CE .NET, namely, Win32, MFC, and the Microsoft .NET Compact Framework. Walks through the creation of an application similar to the MFC Scribble sample using each runtime. (19 printed pages)

We've just completed the Microsoft® Windows® Embedded Developers Conferences in Taiwan and South Korea (Japan and China are coming in February, and Europe in March). Whilst waiting for a meal with other speakers in the Outback Steakhouse close to our hotel in Seoul, I heard someone talking about "A conversation with the server" and the first image that came to mind was that of a large, multi-processor computer using speech recognition and text-to-speech. (Very cool, me thinks.) I discovered moments later that the server in question was actually waiting on our table. What's troubling about this was that a number of other speakers there were thinking exactly the same thing. I guess that's what happens when you get a bunch of jet-lagged Windows Embedded developers/speakers together in a restaurant.

With the recent release to the Web of the final bits for Microsoft Windows CE .NET 4.1, now would be an excellent time to examine the options available to application developers targeting Windows CE .NET devices.

There are presently three options available to application developers targeting Microsoft® Windows® CE .NET. These are Win32, Microsoft Foundation Classes (and ATL, which is primarily used to create COM components, Web Services, and Microsoft® ActiveX® controls), and the Microsoft® .NET Compact Framework. Each has its own specific benefits. You, as an application developer, need to make a decision about which to use when building your applications.

In making a decision, there are many factors you may want to take into account. This article will examine three of these that may be of concern to you: Application file sizes, runtime footprints, and rapid application development. Other items of concern may be security, robustness, working set needs, real-time support, performance, existing code base, and so forth. We will examine the development process for each of the runtimes through the development of a Scribble-like application.

In case you're wondering about the relative sizes of the runtimes, the following list shows the overall size of each:

  • Win32. Win32 is the API of the operating system, therefore there's no size hit for writing Win32 native code applications. Developing applications against the Win32 API is time consuming, since you are programming against the lowest level API exposed from the operating system (more on this later).
  • Microsoft Foundation Classes (MFC). The Microsoft Foundation Classes for Windows CE are exposed from two DLLs: MFCCE400.DLL, which is approximately 300 kilobytes (KB), and OLECE400.DLL, which is approximately 200 KB. You may not need OLECE400.DLL in your image, therefore the minimum size hit would be about 300 KB, and the total, when including OLE (COM) support, would be about 500 KB. Note that Windows CE doesn't support OLE as defined for the desktop (embedding a Microsoft® Excel spreadsheet in a Microsoft® Word document for example). We do support "O," which could stand for "Objects," as in COM objects, but linking and embedding are not supported.
  • Compact Framework. The Compact Framework is made up of a number of DLLs, such as System.drawing.dll. The Compact Framework is approximately 1.3 MB in size. All Windows CE .NET 4.1 processors are supported. The Compact Framework supports a subset of the desktop Microsoft® .NET Framework. No big surprise here, since the desktop Framework is over 30 MB in size. More on this when we get to the application.

To show the size difference when including MFC and the Compact Framework, I've built an "Internet Appliance" platform for the Windows CE .NET Emulator. Here's the size comparison for a release build.

RuntimeSize (Bytes)Delta from Base Win32 Platform
Win329,805,2310
MFC10,234,415429,184
Compact Framework11,201,4591,396,228

Adding support for MFC or the Compact Framework to a platform is as simple as right-clicking the component from the Platform Builder catalog and clicking Add to Platform. If size of your operating system is your only consideration, you can skip to the end of this article and take a peek at what we will be discussing next month. For most readers, the runtime decision isn't quite so straightforward.

To get some idea of the amount of work involved for each runtime, let's write a Scribble-style application (similar to the MFC Scribble sample) in Win32, MFC, and the .NET Compact Framework. The application will include all the usual bells and whistles (file save and restore, menu, and graphics output). Each application will need to handle much the same set of events: mouse down, mouse move, mouse up, and building an array of mouse points that can be played back in the appropriate paint/drawing function. It will also need to handle file save and restore. Sounds pretty simple, right? Okay, then it's time to crack open a bottle of Jolt Cola and get coding…

Win32

When writing code for the Windows CE .NET platform, you could be creating applications, drivers, control panel applets, or DLLs. Native Win32 development is the only option for creating some of the lower-level code, such as device drivers, real-time code, control panel applets, and so forth. When writing user applications, we could use Win32, MFC, or the Compact Framework. In some respects, you could consider a device to be tiered, the lowest tier being drivers and real-time code, which will be in Win32/native code. Above this you may have some middle-tier, data-analysis layer, perhaps a DLL or COM object, which may be written in native code, MFC, or ATL. At the topmost layer, you may have an application providing the user interface, which could be written in Win32, MFC, or the Compact Framework.

Platform Builder and Microsoft® eMbedded Visual C++® both include an Application Wizard that creates the skeleton Win32 code for our application. The skeleton code includes support for drawing (Hello World in the center of the application's client area), menu (which supports File/Exit and Help/About), and includes code for the About box.

Figure 1. The Win32 Skeleton application in all its glory

The skeleton code is a great starting point for our project, and it's also the only time the tools assists in the development of our Win32 application (apart from the excellent online help that ships with eMbedded Visual C++). From this point forward, we need to write everything by hand. Our sample application will need to support mouse events, as there aren't any wizards to assist with this process, and we will need to slot the appropriate WM_MOUSEMOVE, WM_LBUTTONDOWN, and WM_LBUTTONUP handlers into our Windows procedure (WndProc). Okay, so time for everyone's favorite switch statement.

I prefer to call out to stand-alone functions, rather than having a huge switch statement with inline code in the WNDPROC, as it makes the code easier to read and debug. Don't forget that you can use Debug Zones to assist the debug process by dynamically tracing application flow by adding debug zone information to the entry point and exit point of each function. The beauty of Debug Zones is you decide what level of debug information you want and when you want it—it would be all too easy to be overwhelmed by excessive debug information if you used OutputDebugString everywhere. So here's the core of my WNDPROC:

Copy

switch (message) 
{
   case WM_COMMAND:
      wmId    = LOWORD(wParam); 
      wmEvent = HIWORD(wParam); 
      // Parse the menu selections:
      switch (wmId)
      {
         case IDM_HELP_ABOUT:
            DialogBox(hInst, (LPCTSTR)IDD_ABOUTBOX, hWnd, DLGPROC)About);
            break;
         
         case ID_FILE_OPEN:
            OpenScribbleFile(hWnd);            
         break;
         case ID_FILE_SAVE:
            SaveScribbleFile(hWnd);
         break;
            case IDM_FILE_EXIT:
            CleanUp( );
            DestroyWindow(hWnd);
         break;
         default:
            return DefWindowProc(hWnd, message, wParam, lParam);
      }
      break;
   case WM_CREATE:
      hwndCB = CommandBar_Create(hInst, hWnd, 1);         
      CommandBar_InsertMenubar(hwndCB, hInst, IDM_MENU, 0);
      CommandBar_AddAdornments(hwndCB, 0, 0);
      Initialize( );      // setup the initial array element and mouse flags
   break;

   case WM_LBUTTONDOWN:
      HandleLButtondown(hWnd,LOWORD(lParam),HIWORD(lParam));
   break;
      case WM_LBUTTONUP:
      HandleLButtonUp(hWnd,LOWORD(lParam),HIWORD(lParam));
   break;
      case WM_MOUSEMOVE:
      HandleMouseMove(hWnd,LOWORD(lParam),HIWORD(lParam));
   break;
   case WM_PAINT:
      RECT rt;
      hdc = BeginPaint(hWnd, &ps);
      GetClientRect(hWnd, &rt);
      LoadString(hInst, IDS_HELLO, szHello, MAX_LOADSTRING);
      DrawText(hdc, szHello, _tcslen(szHello), &rt, 
         DT_SINGLELINE | DT_VCENTER | DT_CENTER);
      DrawArray(hWnd,ps);
      EndPaint(hWnd, &ps);
      break;
   case WM_DESTROY:
      CommandBar_Destroy(hwndCB);
      PostQuitMessage(0);
      break;
   default:
      return DefWindowProc(hWnd, message, wParam, lParam);
  }
  return 0;
}

We also need to build an array of points to hold the scribble data. Win32 doesn't supply any array-handling functions, so we will need to either create our own linked list (since we want to dynamically grow the number of points), or go find an appropriate array class on the Web. This is where life becomes interesting. You would think that creating a dynamic array of points would be straightforward—and you would be right. I totally goofed on my paint-handler code, and ended up walking past the end of the array and right into an access violation. This cost me around two hours of debug time to track down and fix the problem (in a 400-line application, which includes the Win32 boiler plate code—duh!). Win32 gives you complete freedom over which APIs to call, the lifetime of objects you allocate, and how you work with memory. In my case I had way too much freedom.

Here's how the POINT structure looks. I hold the x and y locations of the last mouse movement, and a pointer to the next item in the array. I also have a count of the number of points in the array. I can therefore step through the list of points and either paint the items, or write the items to a file.

Copy

typedef struct tag_ptArray
{
   POINT pt;
   LPVOID ptrNext;
} PTARRAY,*LPPTARRAY;

Below is how I'm adding a point to the array on a mouse move. Note that once we're done with the application, or if we load a scribble file, we will need to delete each element by stepping through each element of the list and deleting by hand. Otherwise we will get a memory leak.

Copy

void AddElement(int X, int Y)
{
   Current_Point->ptrNext=(LPPTARRAY)LocalAlloc(LPTR,sizeof(PTARRAY));
   Current_Point=(LPPTARRAY)Current_Point->ptrNext;
   Current_Point->pt.x=X; 
   Current_Point->pt.y=Y;
   Current_Point->ptrNext=NULL;
   iPointCount++;
}

We also need to show an open (and save) file dialog to get and save our scribble data. Since this is Win32, we need to fill out an OPENFILENAME structure with the appropriate elements, and call GetOpenFilename to show the dialog. From that point (pun totally intended) forward, we can use the CreateFile, WriteFile (or ReadFile), and (here's an API weirdness)CloseHandle (why isn't this CloseFile?).

Copy

void OpenScribbleFile(HWND hWnd)
{
   OPENFILENAME ofd;
   TCHAR tcFileName[MAX_PATH];
   TCHAR tcDefaultName[MAX_PATH];

   wcscpy(tcDefaultName,L"Scribble.scr");
   memset(&ofd,0x00,sizeof(ofd));

   ofd.lStructSize=sizeof(ofd);
   ofd.hwndOwner=hWnd;
   ofd.hInstance=hInst;
   ofd.lpstrFile=tcFileName;
   ofd.nMaxFile=MAX_PATH;
   ofd.lpstrDefExt=L"scr";
   ofd.lpstrFilter=L"Scribble Files/0*.scr/0/0";
   ofd.lpstrTitle=L"Scribble Files";
   ofd.Flags=OFN_FILEMUSTEXIST | OFN_PATHMUSTEXIST;
   BOOL bRet=GetOpenFileName(&ofd);
   if (TRUE == bRet) {
      CleanUp( );      // clean up the existing scribble array
      pt_Array=(LPPTARRAY)LocalAlloc(LPTR,sizeof(PTARRAY));
      Current_Point=pt_Array;
      HANDLE hFile=CreateFile(ofd.lpstrFile,GENERIC_READ,0,NULL,
OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
      POINT pt;
      DWORD dwRead;
      BOOL bRead=FALSE;
      iPointCount=0;
      while(TRUE) {
         bRead=ReadFile(hFile,&pt,sizeof(pt),&dwRead,0);
         if (dwRead) {
            AddElement(pt.x,pt.y);
         } else
            break;
      }
      CloseHandle(hFile);
   }
   InvalidateRect(hWnd,NULL,TRUE);
}

The total time needed to write (and debug) the Win32 application was about four hours. The end result is a 7 KB executable that doesn't need any additional runtime to load and execute.

The Win32 API can be "interesting" to work with. Some functions take multiple parameters, others take a structure, some return a pointer, others a handle that the operating system tracks against an object internally—and when we're finished with an object, depending on the object type, we either delete, close, or release it. (Failure to do so will result in a memory leak.) This may not be too much of an issue for a 4-byte handle, but leak that handle a million times and you soon start chewing up resources within your device. This does become an issue when your device is running for hours, weeks, months, or even years. Thankfully, we have tools like LMEMDEBUG, Memalyzer, and the Remote Performance Monitor to assist with tracking down leaks, as well as additional tools like the excellent CodeSnitch from Entrek. All these can be useful for pointing a big bony finger at the leaky code.

Microsoft Foundation Class (MFC)

So how does MFC assist with creating applications for Windows CE (or the desktop for that matter)? Simple. The name gives it away; the Microsoft Foundation Classes provide a set of useful classes that (for the most part) hide the complexities of Win32 development from you. There are times when you need to call into the Win32 API directly. This is also true of Microsoft® .NET Compact Framework-based applications, which is why Platform Invoke (pInvoke) is exposed.

Calling native APIs from MFC is straightforward. You just go right ahead and call the native Win32 API as if you were writing a native Win32 application. If you take a look at the MFC source, you will notice that MFC is for the most part a thin wrapper over the top of Win32 APIs. MFC ships with complete source, which is great for debugging and for figuring out what's happening under the covers. For Windows CE .NET 4.1, and a default install of eMbedded Visual C++, the MFC source can be found at C:/Program Files/Windows CE Tools/wce410/STANDARDSDK_410/Mfc/Src.

MFC supports approximately 160 classes, wrapping windows, views, documents, time, arrays, strings, sockets, and GDI objects such as pens, brushes, and so forth. You can view the list of supported classes on the Web or in the eMbedded Visual C++ product documentation.

There are some differences between the implementation of desktop MFC and Windows CE MFC. These are highlighted in Differences from Desktop.

MFC-based applications are created using eMbedded Visual C++ 4.0, which is a free download from here. MFC-based applications for Windows CE can only be created using this tool. Platform Builder can only create Win32 applications and DLLs.

eMbedded Visual C++ assists with application development through the eMbedded Visual C++ tool and the supported MFC classes. If we look back at the Win32 application, adding support for a Windows message such as WM_LBUTTONDOWN (mouse button down, which would be generated for a mouse click or a touch of a stylus on a touch screen), required modifying the WNDPROC of our application by adding an additional case statement into the WNDPROC switch. We also needed to know how to convert WPARAM and LPARAM parameters into device coordinates. With MFC we can use the MFC Class Wizard (CTRL+W, or available from the menu) to create windows message handlers (no more switch statement). The message handlers crack the message parameters and pass something more appropriate and usable to the MFC message handler. Here's how the MFC Class Wizard looks.

Figure 2. The MFC Class Wizard

Or, alternatively, you can right-click on the class and select Windows Message Handler.

Figure 3. The Windows Message Handler

So what would a typical MFC Windows message handler look like? Let's examine a Class Wizard-generated function for a WM_LBUTTONDOWN message. You can clearly see that we're passed mouse status flags (indicating which mouse buttons are down), and a CPoint that contains the x and y location of the mouse down.

Copy

void CMFCScribbleView::OnLButtonDown(UINT nFlags, CPoint point) 
{
   m_InDraw=true;
   m_CurrentPoint=point;
   
   CView::OnLButtonDown(nFlags, point);
}

void CMFCScribbleView::OnLButtonUp(UINT nFlags, CPoint point) 
{
   m_InDraw=false;
   m_CurrentPoint=CPoint(0,0);

   CView::OnLButtonUp(nFlags, point);
}

void CMFCScribbleView::OnMouseMove(UINT nFlags, CPoint point) 
{
   if (m_InDraw) {
      if (point != m_CurrentPoint) {
         CPoint line[2];
         line[0]=m_CurrentPoint;
         line[1]=point;
         m_CurrentPoint=point;
         CDC *pDC=GetDC( );
         pDC->Polyline(line,2);
         ReleaseDC(pDC);
      }
   } else
      CView::OnMouseMove(nFlags, point);
}

The code above is all that's needed to draw the mouse movements as they happen. We're getting a device context in the mouse-move handler (using GetDC), and drawing a line (using Polyline) from our previous point to the current location. This works well, but if our client area is invalidated for any reason, then we will lose the contents of the client area. Ideally, we would keep a list of points; we could use the same linked list code from the Win32 example. It would be great if MFC covered this situation for us... MFC ships with support for the Standard Template Library. This gives us classes such as CArray, which can be used to store our list of points.

Here's an example of creating a CArray and adding points to the array. The array dynamically grows as points are added. Clearing up the array when we load a scribble file is as simple as calling myArray.RemoveAll( );. This is so much easier than the Win32 application. Making use of the CArray would have saved me close to an hour when writing the Win32 application.

Copy

CArray<CPoint,CPoint> myArray;

// Add elements to the array.
for (int i=0;i < 10;i++)
    myArray.Add( CPoint(i, 2*i) );

So, we simply add a CArray <CPoint, CPoint> myArray member variable to our document class, and modify our OnMouseMove handler to add the new point into the class.

Copy

void CMFCScribbleView::OnMouseMove(UINT nFlags, CPoint point) 
{
   if (m_InDraw) {
      if (point != m_CurrentPoint) {
         CPoint line[2];
         line[0]=m_CurrentPoint;
         line[1]=point;

         // add the new point to the document.
         GetDocument( )->myArray.Add(point);
         // add the new point to the document.

         m_CurrentPoint=point;
         CDC *pDC=GetDC( );
         pDC->Polyline(line,2);
         ReleaseDC(pDC);
      }
   } else
      CView::OnMouseMove(nFlags, point);
}

We can then play back the points in our OnDraw handler, like so:

Copy

void CMFCScribbleView::OnDraw(CDC* pDC)
{
   CMFCScribbleDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   int iCount=pDoc->myArray.GetSize( );
   POINT *pt=new POINT[iCount];
   for (int x=0;x < iCount;x++) {
      pt[x].x=pDoc->myArray.GetAt(x).x;
      pt[x].y=pDoc->myArray.GetAt(x).y;
   }
   pDC->Polyline(pt,iCount);
   delete [] pt;
}

We can now scribble on the client area of the application and play back the scribble when the client area needs to be redrawn. The final step is to store and load the scribble data. A really neat side effect of using CArray to hold our array of CPoint's: CArray supports a method called Serialize, and the CDocument class for our Scribble application includes a function calledSerialize. This function is passed a CArchive object, which is used to save a complex network of objects in a permanent binary form (usually disk storage) that persists after those objects are deleted. We simply need to add two lines of code to the Serialize function as follows.

Copy

void CMFCScribbleDoc::Serialize(CArchive& ar)
{
   if (ar.IsStoring())
   {
      // Store the Array of lines
      myArray.Serialize(ar);
   }
   else
   {
      // restore the Array of lines
      myArray.Serialize(ar);
   }
}

Total time to write, test, and debug the MFC application was perhaps two hours. The release application size is approximately 18 KB. You can see that MFC provides a number of useful classes that allow you to focus on the application code rather than on the exposed API of the underlying operating system, though there are still a number of times where you will need to call the native Win32 API.

The Compact Framework

Now let's take a look at programming the Scribble application using the Microsoft .NET Compact Framework. I decided to write and test it as a desktop application and then move the code over to Windows CE. This turned out to be an interesting exercise; I will explain why as we move through the code.

The development environment for building Compact Framework applications is somewhat similar to Microsoft® Visual Basic® development. If you need a menu or a common file dialog, or other controls, you simply drag it from the toolbox and drop it onto your form. You then set the properties of the controls and write any additional code behind the controls.

Figure 4. The device controls toolbox

We need to add handlers for mouse down, move, and up. This is extremely simple. The forms properties dialog (see below) allows us to add handlers for the typical form-based messages: key down/up, mouse down/up/move, and so forth. We then write the code that lives behind the handler.

Figure 5. The properties for the applications form

Figure 6. The controls used in the Scribble application

Note that these controls have no design-time UI. However, most Compact Framework controls have a design-time rendering that allows you to arrange and size controls on your form in a close-to WYSIWYG environment.

Add three variables to the class:

Copy

...public bool bMouseDown=false;
...public ArrayList m_myAL;
...public Point m_CurrentPoint;

Below are the handlers for mouse down, mouse move, and mouse up. Notice that we're creating a System.Drawing.Graphics object in the mouse move. This is so we can draw a line from the last point to the current point without needing to invalidate the client area.

Copy

private void Form1_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
bMouseDown=false;
}

private void Form1_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
   if (true == bMouseDown) 
   {
      m_myAL.Add(new Point(e.X,e.Y));
      System.Drawing.Graphics gr=this.CreateGraphics( );
      Pen myPen=new Pen(System.Drawing.Color.Black);
         gr.DrawLine(myPen,m_CurrentPoint.X,m_CurrentPoint.Y,e.X,e.Y);
      m_CurrentPoint.X=e.X; m_CurrentPoint.Y=e.Y;
   }
}

private void Form1_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
   bMouseDown=true;
   m_CurrentPoint.X=e.X;
   m_CurrentPoint.Y=e.Y;
}

We need to "new up" (create a new instance of the object) the ArrayList. I'm handling this in the form load.

Copy

private void Form1_Load(object sender, System.EventArgs e)
{
      m_myAL = new ArrayList();
}

Now comes the code used to read back a scribble file from the file system. This is where life became interesting when writing the application. I initially used the BinaryFormatter class, which can serialize the entire ArrayList by using the BinarayFormatter.Serialize( ) function. Reading the ArrayList back from file was as simple as using the BinaryFormatter.DeSerialize( )function. The application was built, tested, and worked extremely well until I ported the code over to the Compact Framework. The build failed. BinaryFormatter is not supported in the Compact Framework. Time for a code rewrite.

In this case I decided to use BinaryReader/Writer to write the individual point items out to the stream. I initially wrote the number of items and then wrote the items themselves. Here's the code for writing an ArrayList to file. Notice that I don't need to specify the object type that I'm writing. I simply call Foo.Write(m_myAL.Count) to write the number of elements to file.

Copy

...BinaryWriter Foo=new BinaryWriter(myStream);
...Foo.Write(m_myAL.Count);
   foreach (Point p in m_myAL) 
   {
      Foo.Write(p.X); Foo.Write(p.Y);
   }
   Foo.Close( );
   myStream.Close();

That's all well and good. Now, when reading the information back from file I need to call Foo.Read<%Type%> to read the appropriate size back from the file. (It's assumed that you know what's in the file, since you probably created it in the first place.) I can use the code, Console.WriteLine ("X is a {0}",p.x.GetType); to give me the object type, which, in this case is an Int32. Therefore, my deserialize code will call Foo.ReadInt32( ); to read the count of items and the individual x and y elements.

Copy

private void FileOpen_Click(object sender, System.EventArgs e)
{
   openFileDialog1.Filter = "Scribble files (*.scr)|*.scr";
   openFileDialog1.FilterIndex = 1;
   if (openFileDialog1.ShowDialog( ) == DialogResult.OK) 
   {
      Stream myStream = File.OpenRead(openFileDialog1.FileName);
      if (myStream != null)
      {
         BinaryReader Foo=new BinaryReader(myStream);
         int iCount=Foo.ReadInt32( );
         Point p=new Point(0,0);
         for (int x=0; x < iCount;x++) 
         {
            p.X=Foo.ReadInt32( );
            p.Y=Foo.ReadInt32( );
            m_myAL.Add(p);
         }
         Foo.Close( );
         myStream.Close();
         this.Refresh();
      }
   }
}

So how long did this take to write and test? The entire application was written and running within 20 minutes. Add another 5 minutes or so to change the BinaryFormatter code over toBinaryReader, and the entire application was written in less than 30 minutes from start to finish. The application size is 28 KB on the desktop, and 7 KB on the device. (Don't forget the 1.5 MB Framework living under the application.)

Conclusion

Perhaps you were hoping for a quick answer to which runtime to use. The decision should be based on whether you have existing Win32/MFC/C#/Visual Basic code, the type of code you're writing (drivers, real-time, application), the overall size of your final operating system image once the application and runtime have been included, the speed of development of the application, and other considerations such as security and portability. The good news is that you have a choice, and can mix and match the code depending on which layer of the project you are working on: Win32 for drivers, Win32, MFC, or C#/Visual Basic for the end user application. The overall take away from this article is that application developers really can use their desktop knowledge to be immediately productive with Windows CE.

Get Embedded

Mike Hall is a Product Manager in the Microsoft Embedded and Appliance Platform Group (EAPG). Mike has been working with Windows CE since 1996—in developer support, Embedded System Engineering, and the Embedded product group. When not at the office, Mike can be found with his family, working on Skunk projects, or riding a Honda ST1100.

Steve Maillet is the Founder and Senior Consultant for Entelechy Consulting. Steve has provided training and has developed Windows CE solutions for clients since 1997, when CE was first introduced. Steve is a frequent contributor to the Microsoft Windows CE development newsgroups. When he's not at his computer burning up the keys, Steve can be found jumping out of airplanes at the nearest drop zone.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值