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 |