Rendering a Region Dialog From a BMP/JPEG

Environment: VC6/VC7, MS Platform Core SDK, 24/32 bit True Color Windows Desktop Environment. (Test on Win2K/XP Passed, No Guarantee on Win95/98/ME)

Key Technology Used: Bitmap/JPEG Image Processing, Advanced GUI, Shell Programming (System Tray), Waitable Timer, Multithread, Win2k User Context Programmming (Lock & Shut Down System)

Applicable Article Category in CodeGuru: GDI, Dialog, System, Miscellaneous, Sample

Summary and GUI

It seems there are quite a few Region-Based Dialog samples here already; unfortunately, none of them went one step further to permit the user to render a region from both image resource and images files such as bitmap and JPEG. So, this time I would present a much more flexible and user-friendly Region Dialog that support such things. With it, you can drag and drop to apply new images; press Ctrl+Z to go back through all the images you have used, select the background color on different files, and even zoom the images to any ratio you like. Besides, I added some accessory functionalities such as “Lock Machine,” “Shut Down Machine After n Minutes,” “Start Screen Saver,” and so on. Well, have a look at the GUI, please:

Figure 1—GUI overview of Customizable Region Dialog

The user is free to choose the background color to render region from a bitmap or jpeg file. Say, by using a Fuchsia color, we apply this color to the middle image, and get the right one.

Figure 2—System Tray Context Menu of Customizable Region Dialog

In its context menu from the system tray, the user can, just as shown in the above figure, lock the machine, start a screen saver, and shut down the machine in a different time and a different way. Besides, you can set the balloon message and pop up period so that it can remind you of something, like this one:

Figure 3—Balloon Message Box from System Tray (text from Red Alert ©Westwood Studio)

For enjoyment, I have added more than a dozen of character images with the demo. When you want to switch images, just activate the window by a mouse click, press Ctrl+Z to use the previous image, or you can drag an image file and drop it to the dialog to apply it immediately. If you want to make your own image, you have to do it pixel by pixel, replacing the background with the background color; please use a bitmap as much as possible because a JPEG image is not so “sharp.” Following are some characters used in the demo:

Figure 4—Various Images Bounded With Customizable Region Dialog Demo

Architecture

Basically, it is a dialog with a system tray icon; it reads image files from disk, makes the region rendered, and adjusts the show of the dialog according to the zoom ration and transparency ratio. Under the hood, a thread is using a waitable timer to check whether the user wants to shut down the machine at some time.

Implementation Description

Reading BMP/JPEG Image Disk Files and Rendering Its Region

The only way to do so is to scan every pixel inside the image and compare its color with the background color (or filter color, if you like this name). If they’re the same, do not add the point to the region; otherwise, add it to the region. But, region operation is so time consuming, and adding one point each time is terrible when scanning a big image file. So, Mr. David Gallardo Llopis’s article Technique to Create Dialogs from Images does a region operation only once if we could find a consecutive serial of points to form an rectangle of background color pixels. Unfortunately, his program only deals with images from bitmaps. So, I upgraded his routine to cope with both JPEG and Bitmap files: (Note: ONLY 24, 32 bit true color bitmap image is supported!!). To JPEG files, I use IOlePicture to read it from disk first, then use memory DC to transfer it to a bitmap (zoom it if applicable):

//Note: You must use true color DIB Bitmap -- 24 or 32 bit
//I just have no time to play with color table
HRGN DIBRegion(LPBITMAPINFOHEADER lpBmpInfoHead, LPVOID lpImage,
               COLORREF cTransparentColor,BOOL bIsTransparent)
{
    ASSERT(lpBmpInfoHead->biBitCount == 32 ||
           lpBmpInfoHead->biBitCount == 24);
    BYTE c_red = GetRValue(cTransparentColor);
    BYTE c_green = GetGValue(cTransparentColor);
    BYTE c_blue = GetBValue(cTransparentColor);

   // We create an empty region
    HRGN hRegion=NULL;
    //Here, please check Mr. David Gallardo Llopis's article's
    //sample code
    #define NUMRECT 100
    DWORD maxRect = NUMRECT;

   // We create the memory data
    HANDLE hData=GlobalAlloc(GMEM_MOVEABLE,sizeof(RGNDATAHEADER)+
                            (sizeof(RECT)*maxRect));
    RGNDATA *pData=(RGNDATA*) GlobalLock(hData);
    pData->rdh.dwSize=sizeof(RGNDATAHEADER);
    pData->rdh.iType=RDH_RECTANGLES;
    pData->rdh.nCount=pData->rdh.nRgnSize=0;
    SetRect(&pData->rdh.rcBound,MAXLONG,MAXLONG,0,0);
    //Handling Bitmap --- oop, it is my code now
    DWORD dwBytePerPixel = lpBmpInfoHead->biBitCount/8;
    // 3 or 4
    DWORD dwBytePerLine = (lpBmpInfoHead->biWidth *
                           dwBytePerPixel);
    while(dwBytePerLine%4)
      dwBytePerLine++;
    DWORD dwSize = dwBytePerLine * lpBmpInfoHead->biHeight;

    BYTE *Pixeles=(BYTE*)lpImage;
    Pixeles += dwBytePerLine * (lpBmpInfoHead->biHeight - 1);
    // Main loop
    for(int Row=0;Row<lpBmpInfoHead->biHeight;Row++)
    {
      // Horizontal loop
      for(int Column=0;Column<lpBmpInfoHead->biWidth;Column++)
      {
        // We optimized searching for adjacent transparent pixels!
        int Xo=Column;
        LPBYTE lpByte = (LPBYTE)Pixeles;
        lpByte += Column*dwBytePerPixel;
        //Note Little Endian in Intel
        while(Column<lpBmpInfoHead->biWidth)
        {
          BOOL bInRange=FALSE;
          if(dwBytePerPixel == 4)    //32-bit bitmap
            {
              if(c_red == lpByte[2] && c_green ==
                          lpByte[1] && c_blue == lpByte[0])
                bInRange=TRUE;
            }
            else if(dwBytePerPixel == 3)    //24-bit bitmap
            {
              if(c_red == lpByte[2] && c_green ==
                          lpByte[1] && c_blue == lpByte[0])
                bInRange=TRUE;
            }
            if((bIsTransparent)  && (bInRange)) break;
            if((!bIsTransparent) && (!bInRange)) break;

            lpByte += dwBytePerPixel;
            Column++;
         }    // end while (Column < bm.bmWidth)

        if(Column>Xo)
        {
          if (pData->rdh.nCount>=maxRect)
          {
            GlobalUnlock(hData);
            maxRect+=NUMRECT;
          hData=GlobalReAlloc(hData,sizeof(RGNDATAHEADER)+
                             (sizeof(RECT)*maxRect),
                              GMEM_MOVEABLE);
            pData=(RGNDATA *)GlobalLock(hData);
          }

          RECT *pRect=(RECT*) &pData->Buffer;
          SetRect(&pRect[pData->rdh.nCount],Xo,Row,Column,Row+1);

          if(Xo<pData->rdh.rcBound.left)
          pData->rdh.rcBound.left=Xo;
          if(Row<pData->rdh.rcBound.top)
          pData->rdh.rcBound.top=Row;
          if(Column>pData->rdh.rcBound.right)
          pData->rdh.rcBound.right=Column;
          if(Row+1>pData->rdh.rcBound.bottom)
          pData->rdh.rcBound.bottom=Row+1;
          pData->rdh.nCount++;
          if(pData->rdh.nCount==2000)
          {
            HRGN hNewRegion=ExtCreateRegion(NULL,sizeof
                            (RGNDATAHEADER) + (sizeof(RECT) *
                            maxRect),pData);
            if (hNewRegion) {
              if (hRegion) {
                CombineRgn(hRegion,hRegion,hNewRegion,RGN_OR);
                DeleteObject(hNewRegion);
              } else
                hRegion=hNewRegion;
          }
          pData->rdh.nCount=0;
          SetRect(&pData->rdh.rcBound,MAXLONG,MAXLONG,0,0);
          }
        }    // if (Column > Xo)
      }      // for (int Column ...)
      Pixeles -= dwBytePerLine;
    }        // for (int Row...)

    HRGN hNewRegion=ExtCreateRegion(NULL,sizeof(RGNDATAHEADER)+
                    (sizeof(RECT)*maxRect),pData);
    if(hNewRegion)
    {
      // If the main region does already exists,
      // we add the new one
      if(hRegion)
      {
        CombineRgn(hRegion,hRegion,hNewRegion,RGN_OR);
        DeleteObject(hNewRegion);
      }
      else
        // if not, we consider the new one to be the main region
        // at first!
        hRegion=hNewRegion;
      }

      // We free the allocated memory and the rest of used
      // resources
      GlobalFree(hData);
      return hRegion;
}

Before calling this function, I have to read from JPEG/BMP file as following (to save space, checking code omitted)

HBITMAP hBmpOriginal = NULL;
if(strFilename.IsEmpty())    //strFilename is the image disk
                             //file name, use resource
{
  hBmpOriginal = (HBITMAP)::LoadImage(::AfxGetInstanceHandle(),
                            MAKEINTRESOURCE(IDB_SS),
    IMAGE_BITMAP, 0, 0, LR_CREATEDIBSECTION);
   m_imageType = res;
}
else if(strFilename.Right(3).CompareNoCase(_T("bmp"))
     == 0)    //bitmap file
{
  hBmpOriginal = (HBITMAP)::LoadImage(::AfxGetInstanceHandle(),
                  strFilename,
    IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
  m_imageType = bmp;
}
else if(strFilename.Right(3).CompareNoCase(_T("jpg")) == 0 ||
  strFilename.Right(3).CompareNoCase(_T("jpeg")) == 0)
{
  HANDLE hFile = CreateFile((LPCTSTR)strFilename, GENERIC_READ, 0,
                 NULL, OPEN_EXISTING, 0, NULL);
  DWORD dwFileSize = GetFileSize(hFile, NULL);
  LPVOID pvData = NULL;
  HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, dwFileSize);
  pvData = GlobalLock(hGlobal);
  DWORD dwBytesRead = 0;
  BOOL bRead = ReadFile(hFile, pvData, dwFileSize,
                        &dwBytesRead, NULL);
  GlobalUnlock(hGlobal);
  CloseHandle(hFile);

  LPSTREAM pstm = NULL;
  HRESULT hr = CreateStreamOnHGlobal(hGlobal, TRUE, &pstm);

  if(m_pPicture)
                    //LPPICTURE m_pPicture; set it to NULL
                    //when dialog created
  {
    m_pPicture->Release();
    m_pPicture = NULL;
  }
  hr = ::OleLoadPicture(pstm, dwFileSize, FALSE, IID_IPicture,
                       (LPVOID *)&m_pPicture);
  pstm->Release();
  GlobalFree(hGlobal);

  long hmWidth = 0L; long hmHeight = 0L;
  m_pPicture->get_Width(&hmWidth);
  m_pPicture->get_Height(&hmHeight);
  #define HIMETRIC_INCH 2540
  hMemDC = CreateCompatibleDC(NULL);
  int nWidth = MulDiv(hmWidth, GetDeviceCaps(hMemDC, LOGPIXELSX),
               HIMETRIC_INCH);
  int nHeight = MulDiv(hmHeight, GetDeviceCaps(hMemDC,
                       LOGPIXELSY), HIMETRIC_INCH);

  HDC hSelfDC = ::GetDC(this->GetSafeHwnd());
  //m:n is Zoom ratio
  hBmp = ::CreateCompatibleBitmap(hSelfDC, (int)(1.0*m*nWidth/n),
           (int)(1.0*m*nHeight/n));
  hPrevBmp = (HBITMAP)::SelectObject(hMemDC, hBmp);
  CRect rect;
  rect.SetRect(0,0,nWidth, nHeight);
  m_pPicture->Render(hMemDC, 0, 0, (int)(1.0*m*nWidth/n),
             (int)(1.0*m*nHeight/n),
    0, hmHeight, hmWidth, -hmHeight, &rect);
  m_pPicture->Release();
  m_pPicture = NULL;
  ::ReleaseDC(this->GetSafeHwnd(), hSelfDC);
  m_imageType = jpg;
  return TRUE;
}
//Zoom Image
HDC hSelfDC = ::GetDC(this->GetSafeHwnd());
HDC hMemDC2 = CreateCompatibleDC(hSelfDC);
HBITMAP bmpTemp = (HBITMAP)::SelectObject(hMemDC2, hBmpOriginal);
hMemDC = CreateCompatibleDC(hSelfDC);
BITMAP bm;
::GetObject(hBmpOriginal, sizeof(BITMAP), &bm);
hBmp = ::CreateCompatibleBitmap(hSelfDC,
         (int)(1.0*m*bm.bmWidth/n),
         (int)(1.0*m*bm.bmHeight/n));
hPrevBmp = (HBITMAP)::SelectObject(hMemDC, hBmp);
::ReleaseDC(this->GetSafeHwnd(), hSelfDC);
StretchBlt(hMemDC, 0, 0,(int)(1.0*m*bm.bmWidth/n),(int)
                             (1.0*m*bm.bmHeight/n), hMemDC2,
  0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY );
::SelectObject(hMemDC2, bmpTemp);
::DeleteDC(hMemDC2);
DeleteObject(hBmpOriginal);

  //Create a Dib
  CBitmap bitmap;
  bitmap.Attach(hBmp);
  CDC dc;
  dc.Attach(hMemDC);
  BITMAP bm;
  // get bitmap information
  bitmap.GetObject(sizeof(bm),(LPSTR)&bm);

  int nBitCount = bm.bmBitsPixel;
  LPBITMAPINFOHEADER lpBMIH = (LPBITMAPINFOHEADER) new
  char[sizeof(BITMAPINFOHEADER) + sizeof(RGBQUAD) * 0];
  lpBMIH->biSize = sizeof(BITMAPINFOHEADER);
  lpBMIH->biWidth = bm.bmWidth;
  lpBMIH->biHeight = bm.bmHeight;
  lpBMIH->biPlanes = 1;
  lpBMIH->biBitCount = nBitCount;
  lpBMIH->biCompression = BI_RGB;
  lpBMIH->biSizeImage = 0;
  lpBMIH->biXPelsPerMeter = 0;
  lpBMIH->biYPelsPerMeter = 0;
  lpBMIH->biClrUsed = 0;
  lpBMIH->biClrImportant = 0;
  LPVOID lpImage = NULL;    // no data yet

  DWORD dwCount = ((DWORD) lpBMIH->biWidth
                * lpBMIH->biBitCount) / 32;
  if(((DWORD)lpBMIH->biWidth * lpBMIH->biBitCount) % 32) {
  dwCount++;
  }
  dwCount *= 4;
  dwCount = dwCount * lpBMIH->biHeight;
  lpImage = (LPBYTE)(LPVOID)::VirtualAlloc(NULL, dwCount,
  MEM_COMMIT, PAGE_READWRITE);
  ::ZeroMemory((LPVOID)lpImage,dwCount);

  // finally get the dib
  BOOL result = GetDIBits(dc.GetSafeHdc(),
                (HBITMAP)bitmap.GetSafeHandle(), 0L,
    (DWORD)bm.bmHeight, (LPBYTE)lpImage, (LPBITMAPINFO)lpBMIH,
                        (DWORD)DIB_RGB_COLORS);
  //Make Region
  hRegion= DIBRegion(lpBMIH, lpImage,m_clrBack, TRUE);
  delete lpBMIH;
  dc.Detach();
  bitmap.Detach();
}
  // If there was no problem getting the region, we make it
  // the current clipping region of the dialog's window
  if(hRegion)
 SetWindowRgn(hRegion,TRUE);

How to Use a Waitable Timer

To be frank, even after I read the MSDN and “Programming Application for MS Windows 2000” (1999, MS Press), it still took me quite some time to get used to the troublesome kernel object—waitable timer. Please Note: If you set an earlier time (than now) as the due time, the timer will be signaled immediately! In other words: if now is 11:00:00am, and you set a time from 10:59:00 (Due time) and signal every 5 minutes, the timer will be signaled immediately after the API call. Though it sounds reasonable, it means you have to be careful when setting the Due Time parameter in SetWaitableTimer API. Always comparing the due time with the current time will be a good habit. Following is a routine you may need to displace (move) time, it is useful to calculate a timer later or earlier than the given time:

BOOL MoveTime(CONST SYSTEMTIME *lpSystemTime1,
    // first system time [in]
                    SYSTEMTIME *lpSystemTime2,
    // second system time [out]
                    BOOL bPositive,
    // TRUE: Later Time; FALSE : Earlier Time
                    DWORD nDay, DWORD nHour, DWORD nMinute,
                                DWORD nSecond, DWORD nMilliSecond)
    //[in], Detailed Time Displacement
{
    //For your convenience: 1 second = 1,000 milliseconds
    //                               = 1,000,000 microseconds
    //                               = 1,000,000,000 nanoseconds.
    //The FILETIME structure is a 64-bit value representing the
    //number of 100-nanosecond intervals
    //since January 1, 1601 (UTC).

    LARGE_INTEGER n1, n2;
    n1.QuadPart = nDay;           n1.QuadPart *= 24;
    n1.QuadPart += nHour;         n1.QuadPart *= 60;
    n1.QuadPart += nMinute;       n1.QuadPart *= 60;
    n1.QuadPart += nSecond;       n1.QuadPart *= 1000;
    n1.QuadPart += nMilliSecond;  n1.QuadPart *= 10000;

    FILETIME ft;
    if(!::SystemTimeToFileTime(lpSystemTime1, &ft))
      return FALSE;

    n2.LowPart = ft.dwLowDateTime;
   n2.HighPart = ft.dwHighDateTime;

    if(bPositive)
      n2.QuadPart += n1.QuadPart;
    else
     n2.QuadPart -= n1.QuadPart;

    ft.dwLowDateTime = n2.LowPart;
    ft.dwHighDateTime = n2.HighPart;

    return ::FileTimeToSystemTime(&ft, lpSystemTime2);
}

Following is my background timer thread routine; the kill event will stop the thread and the refresh event will let the thread read a global structure (MMF or whatever shared data between GUI and thread) and refresh the waitable timer parameter:

DWORD WINAPI TimerThread(LPVOID lpParam)
{
  TimerPara* myPara = (TimerPara*)lpParam;
  HANDLE hKillEvent = myPara->hKillEvent;
  HANDLE hRefreshEvent = myPara->hRefreshEvent;

  LARGE_INTEGER li;
  // Create an auto-reset timer.
  HANDLE hWaitableTimer = ::CreateWaitableTimer(NULL, FALSE,
                                                NULL);
  // Timer unit is 100-nanoseconds.
  const int nTimerUnitsPerSecond = 10000000;

  HANDLE arrayEvent[3];
  arrayEvent[0] = hKillEvent;
  arrayEvent[1] = hRefreshEvent;
  arrayEvent[2] = hWaitableTimer;

  while(TRUE)
  {
    DWORD dwRet = ::WaitForMultipleObjects(3, arrayEvent, FALSE,
                                           INFINITE, FALSE);
    if(dwRet == WAIT_OBJECT_0) { ::CloseHandle(hWaitableTimer);
                                  return 0; }    //Kill Event
    else if(dwRet == WAIT_ABANDONED_0 ||
      dwRet == WAIT_ABANDONED_0 + 1 || dwRet
            == WAIT_ABANDONED_0 + 2)
    {
      ::CloseHandle(hWaitableTimer); return -1;
    }
    if(dwRet == WAIT_OBJECT_0 + 2)
    {
      //Do the thing you need to do; timer signalled
    }
    else if(dwRet == WAIT_OBJECT_0 + 1)    //Refresh Timer Setting
    {
      ::ResetEvent(hRefreshEvent);
      ::CancelWaitableTimer(hWaitableTimer);
      //Stop Possible Coming Timer

      //You can read from a global Variable or using MMF
      //Set New Due Time and Period of the timer
      SetWaitableTimer(hWaitableTimer, ......);
    }

  }
  return 0;
}

Known Limitations

Transparency on WinXP

It is strange on WinXP, when setting transparency of the dialog; the image will be badly painted like following figure, while in Win2K all is perfect. Anyone who solved this problem in WinXP, please comment. Thanx ahead :=)

Figure 5—Transparency Badly Painted on WinXP (While Win2K OK)

When Using JPEG images

Due to the nature of a JPEG, the encoding will lose some data; so, when we apply the transparent color to the image, usually in the perimeter, the pixel’s color may not be what you think. The following left red rectangle, when saved as a bitmap, every pixel keeps its color; when saved as a JPEG, please note along the perimeter, the color changed. So, when I used JPEG to render a region, instead of using a clause like “if(r1== r2 && g1==g2 && b1 ==b2”, I use “if (r1-r2)*(r1-r2) + (g1-g2)*(g1-g2) + (b1-b2)*(b1-b2) <= some threshold”. Inevitably, this leads to other problems; for example, if some pixel inside the character has a similar color, it will be counted into the region even you need it. But, there seems no way to prevent this. So, please use bitmap files as much as possible.

Figure 6—Comparison of Bitmap and JPEG Image’s Perimeter

This Program will NOT Shut Down Your Machine When a Screen Saver is Running

I have no intention of adding an complicated things, say, a NT Service, to this application to do a shutdown; it is just an accessory functionality, so just keep in mind it can NOT shut down your machine if the screen saver is running. So, if you want to shutdown your machine after three hours, disable your screen saver before you leave.

Acknowledgements

Thanks to the following article/code contributor on CodeGuru: Mr. Brent Corkum for his cool XP-style BCMenu, Mr. David Gallardo Llopis’s article Technique to Create Dialogs from Images, Mr. Sam Hobbs for Processing Keyboard Messages, Mr. Dominik Filipp for How to dynamically show/hide the Taskbar application button (BTW, only a ModifyStyleEx(WS_EX_APPWINDOW, 0); will be enough in a dialog-based application).

Downloads

Download Demo Project Source (all the source code + exe) – 709 Kb
Download Demo Exe File Only (Exe Only, MFC library static linked) – 452 Kb

Version History

Version Release Date Features
1.0 Nov 12, 2002 First Version (BMP/JPEG File support, Tray Icon)
1.1 Nov 18, 2002 Shut Down support
1.2 Dec 14, 2002 Balloon Message Added
1.3 Xmas 2002 Finish This Article

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read