A Bitmap File Analyzer |
|
This little project came about because of a sequence of questions that came up on the microsoft.public.vc.mfc newsgroup. It illustrates a variety of techniques, mostly relating to bitmaps. But there are a number of other interesting programming techniques.
There are two related essays on bitmaps:
This is even more general, since it starts with the actual .bmp file. This example covers all of the following topics:
The DIB-printing code of this sample comes from the Microsoft SHOWDIB example in the MSDN. It is encapsulated in a single file, dib.cpp (and its accompanying dib.h file).
What makes this interesting is that I was able to create a scrollable bitmap image without expending a whole lot of effort to do so. Instead of creating a complex OnPaint handler and doing BitBlts, I just create a simple CStatic control and scroll it. I do this in a modeless dialog, but you could do the same thing using a generic CWnd class and make the CStatic a child of that control.
I also wanted to highlight the area selected in the ListBox and show it on the bitmap image. Now the way most people would approach this is to write a control that displayed the bitmap and handled an XOR-style highlight around the area of interest. I was extremely lazy, and simple created a child window which was painted with an alternating-black-and-white pen and hollow brush. I could make it blink just by showing it and hiding it at the user's selected caret blink rate. I was therefore able to write this in a very short period of time.
Rather than penalize those with slow modems, and because I want to put technology pictures on this page, I have put the pictures of the program running on a separate page. According to FrontPage, this page should take about two minutes to download at 28.8. If you have T1, DSL, etc., expect it will be not much time at all.
This uses one of my favorite devices, the owner-draw ListBox with a class hierarchy. I usually start with an abstract class at the top-level of the hierarchy, and build down from that. The DrawItem logic simply calls a virtual Draw method for this superclass and the class then draws itself.
This is a simplified reader; it only reads uncompressed bitmaps. I do not have any instances of compressed .bmp files and have no encountered any in my last 13 years as a Windows programmer, so I don't rate this as a serious issue. The format of compressed files is carefully documented and if you need to, you have the complete source code (and, if anyone does add this, if you send it back to me I'll include it in the distribution and give you credit for the enhancements). I also support only bottom-to-top bitmap organization, although I've added code to make pretensions of supporting the possibility of top-to-bottom organization.
Reading the bitmap is moderately straightforward. It is complicated a bit by the additions of the "Version 4" and "Version 5" bitmaps which have enhanced features. Microsoft has complicated this somewhat more by defining separate upward-compatible versions of the headers, rather than creating, say, a version 4 header whose front part was a regular header and which appended additional fields. Of course, this would have been trivial in C++, where the new versions would be derived classes of the basic bitmap header.
I handle this by assuming it is a standard bitmap header and reading it. Since all bitmaps require the same basic information, I can use the core information from all of them to create the bitmap.
I save the file position at which I read the header (although it is actually zero, I feel more comfortable asking about it), read the header, look at its length, and then read the header again into a V4 or V5 structure if necessary. I then display all the header information. I was feeling lazy, so I didn't do what I would have done with a full product and put all the strings in the STRINGTABLE.
Following the header information there may be a palette. If the "colors used" value is zero, then the palette is the maximum size required by the color depth, if the image is paletted (1-, 4-, and 8-bit sizes). If the "colors used" value is nonzero, there is a palette, independent of the color depth (this is to provide for 16-, 24- and 32-bit bitmaps an "optimal palette" if they had to be displayed on paletted devices).
There are two issues here in "reading a bitmap". One is that I want to read in each horizontal scan line and display it in expanded form in the ListBox. Another issue is that I want to read the pixels in and create a bitmap.
You would not normally do this, but the techniques are interesting. Here is the code that reads 24-bit bitmaps. Note that I use abs(info.biHeight) because for a top-to-bottom (unnatural) DIB, the height is specified as a negative value. I create a CArray<BYTE,BYTE> to hold one scan line (not including padding). I then read the tuples one at a time. You could clearly, with only minor effort, do this in a single read operation, but I was coding rapidly and chose clarity over performance.
Then, given I've read a certain number of bytes, I needed to read the remaining bytes to pad to a DWORD boundary. I append these bytes to the array.
for(int y = 0; y < abs(info.biHeight); y++) { /* read one row */ CArray<BYTE, BYTE> data; int bytesRead = 0; int line = info.biHeight > 0 ? info.biHeight - y : y; // handle top-to-bottom case! data.SetSize(3 * info.biWidth); for(int x = 0; x < info.biWidth; x++) { /* read one tuple */ f.Read(data.GetData() + 3 * x, 1); f.Read(data.GetData() + 3 * x + 1, 1); f.Read(data.GetData() + 3 * x + 2, 1); bytesRead += 3; } /* read one tuple */ int padding = PadToDWORD(f, data, bytesRead); c_Data.AddString(new BitmapRGB24(data.GetData(), data.GetSize(), info.biWidth, line, padding)); } /* read one row */
int CBitMapInfoDlg::PadToDWORD(CFile & f, CArray<BYTE, BYTE> & data, int bytesRead) { int delta = (4 - (bytesRead % 4)); if(delta != 0) { /* read last few */ int spare = data.GetSize(); data.SetSize(data.GetSize() + delta); for(int n = 0; n < delta; n++) { /* read last */ f.Read(data.GetData() + spare + n, 1); } /* read last */ } /* read last few */ return delta; } // CBitMapInfoDlg::PadToDWORD
The AddString method is an overloaded method of my subclassed owner-drawn ListBox, which I won't go into detail about here. The BitmapRGB24 class is discussed in the section about the ListBox.
In order to not intermingle the display-the-information code with the read-the-pixels code, and so anyone downloading this could readily untangle the two simply by using a single subroutine, I use a completely separate subroutine to read the DIB data and create a bitmap. This is in the dialog that is used to show the bitmap, CShow, in the file show.cpp. This code is adapted from the SHOWDIB example code in the MSDN. The CFile object passed in as the parameter is assumed to be an open file positioned at the beginning. Note that this code does not support the V4 and V5 headers! (There is a newer example on the Microsoft Web site, SeeDIB, which also handles 16-bit and 32-bit color DIBs).
// In the CShow class declare BITMAPFILEHEADER hdr; BITMAPINFOHEADER info; BITMAPINFO * bi; CBitmap TheBitmap; // These will be needed later HBITMAP CShow::ReadDibInfo(CFile & f) { CBitmap bmp; f.Read(&hdr, sizeof(hdr)); f.Read(&info, sizeof(info)); LOGPALETTE * lp; int colors = GetPaletteSize(info); lp = (LOGPALETTE *)new BYTE[sizeof(LOGPALETTE) + colors * sizeof(PALETTEENTRY)]; lp->palNumEntries = (WORD)colors; #define PALVERSION 0x0300 lp->palVersion = PALVERSION; // Fill in the palette entries if(bi != NULL) delete bi; bi = (BITMAPINFO*)new BYTE[sizeof(BITMAPINFO) + colors * sizeof(RGBQUAD)]; if((HPALETTE)palette != NULL) palette.DeleteObject(); if(colors > 0) { /* read quads */ f.Read(&bi->bmiColors, sizeof(RGBQUAD) * colors); for(int i = 0; i < colors; i++) { /* convert */ lp->palPalEntry[i].peRed = bi->bmiColors[i].rgbRed; lp->palPalEntry[i].peGreen = bi->bmiColors[i].rgbGreen; lp->palPalEntry[i].peBlue = bi->bmiColors[i].rgbBlue; lp->palPalEntry[i].peFlags = 0; } /* convert */ VERIFY(palette.CreatePalette(lp)); } /* read quads */ DWORD start = f.GetPosition(); f.SeekToEnd(); DWORD end = f.GetPosition(); if(hdr.bfOffBits != 0) { /* seek */ f.Seek(hdr.bfOffBits, CFile::begin); start = hdr.bfOffBits; } /* seek */ LPBYTE bits = new BYTE[end - start + 1]; f.Read(bits, end - start + 1); CClientDC dc(NULL); if((HPALETTE)palette != NULL) { /* select */ dc.SelectObject(&palette); } /* select */ bmp.CreateCompatibleBitmap(&dc, info.biWidth, info.biHeight); bi->bmiHeader = info; ::SetDIBits((HDC)dc, (HBITMAP)bmp, 0, info.biHeight, bits, bi, DIB_RGB_COLORS); delete bits; return (HBITMAP)bmp.Detach(); } // CShow::ReadDibInfo
I could have done this just with bit shifts but I like to test all cases.
int GetPaletteSize(DWORD n) { switch(n) { /* n */ case 1: return 2; case 4: return 16; case 8: return 256; default: return 0; } /* n */ } // GetPaletteSize
Being exceptionally lazy the day I wrote this, I did not want to be bothered by the problems of drawing and scrolling bitmaps. So I ignored them. What I did was create a modeless dialog box with scrollbars which changed the origin of an ordinary CStatic control so it appeared to "scroll". The call on ReadDibInfo contained the following code:
LRESULT CShow::OnShowFile(WPARAM wParam, LPARAM lParam) { c_Image.SetBitmap(NULL); // remove from display // If the bitmap handle we've saved has a bitmap in it, // free that bitmap. Note that if we have an error // creating the bitmap, below, the current bitmap will // be gone if((HBITMAP)TheBitmap != NULL) TheBitmap.DeleteObject(); LPCTSTR filename = (LPCTSTR)lParam; SetWindowText(filename); CFile & f = *(CFile *)wParam; // Read the DIB and create an HBITMAP object HBITMAP bm = ReadDibInfo(f); if(bm == NULL) return FALSE; // Now attach the new HBITMAP to the object TheBitmap.Attach(bm); c_Image.SetBitmap((HBITMAP)TheBitmap); SetScrollers(); return TRUE; }
The scrolling is a bit tricky. I wanted to show the "page size" on the scrollbar so the user had feedback about the position of the image. But at the boundary conditions you would normally expect to encounter, such as the window resizing to larger than the image, the scrollbar would disappear! It would do this even if the image were partially scrolled off the window, making it impossible to scroll it back. So there are some funny tests in this code that sets the scrollbar state that deal with this situation. In the interest of compacting space here, I have omitted the horizontal scrollbar code (you can get it in the download, or just copy the vertical code and make the appropriate changes for Height/Width and so on).
void CShow::SetScrollers() { CRect image; c_Image.GetWindowRect(&image); ScreenToClient(&image); CRect view; GetClientRect(&view); //**************************************************************** // Vertical Scrollbar setting //**************************************************************** SCROLLINFO info; ::ZeroMemory(&info, sizeof(info)); info.cbSize = sizeof(info); // Do not disable the scrollbar if the image is scrolled // off the top; it must be vertically entirely // scrolled on to disable the scrollbars if(image.top == 0 && image.Height() < view.Height()) { /* no vert scrolling */ info.nMin = 0; info.nMax = 0; info.fMask = SIF_RANGE; } /* no vert scrolling */ else { /* vert scrolling */ info.nMin = 0; info.nMax = image.Height(); // The code below fiddles the page size to keep // the scrollbar from disappearing when the page size // equals the scroll range, a feature of the built-in // control if(image.top < 0 && view.Height() >= image.Height()) { /* need more */ info.nPage = image.Height() + image.top + 1; } /* need more */ else { /* just fine */ info.nPage = view.Height(); } /* just fine */ info.fMask = SIF_RANGE | SIF_PAGE; } /* vert scrolling */ SetScrollInfo(SB_VERT, &info); //**************************************************************** // Horizontal Scrollbar setting //**************************************************************** // Omitted from this illustration: symmetric code for // horizontal scrollbar setting } // CShow::SetScrollers
This function would have to be called if the dialog were resized
void CShow::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); if(c_Image.GetSafeHwnd() != NULL) { /* handle scrolling */ SetScrollers(); } /* handle scrolling */ }
The responses to both horizontal and vertical scrolling are symmetric; here I show only the vertical scroll response. This is pretty much a standard scrollbar-response handler. Note that I have to recover the position and size information, so I use GetScrollInfo. I ignore the nPos value on general principles since it only handles 16-bit information (while it is declared as a UINT parameter, it is actually cast to a UINT by the framework, which receives only a short.
In order to not scroll the image off the screen, I have chosen to stop the scrolling when the bottom of the image hits the bottom of the window. Note that if you fail to set the new scroll position after computing it, you will see that no apparent scrolling takes place.
What makes this sneaky is that I have now compelled Windows to handle all the drawing for me. All I have to do is change the window origin and the image scrolls!
It is worth pointing out that if I were doing all the drawing myself with BitBlt, I would still have to do all the scrolling computations, including setting the scroller information as I do in SetScrollers and handling the OnVScroll handler. So using the CStatic has not added any complexity to my task.
void CShow::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { SCROLLINFO info; ::ZeroMemory(&info, sizeof(info)); info.cbSize = sizeof(info); info.fMask = SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS; GetScrollInfo(SB_VERT, &info); int low = info.nMin; int high = info.nMax - info.nPage; int newpos = info.nPos; CRect view; GetClientRect(&view); CRect image; c_Image.GetWindowRect(&image); ScreenToClient(&image); switch(nSBCode) { /* nSBCode */ case SB_LINEUP: newpos -= ::GetSystemMetrics(SM_CYHSCROLL); if(newpos < low) newpos = low; break; case SB_LINEDOWN: newpos += ::GetSystemMetrics(SM_CYHSCROLL); if(newpos > high) newpos = high; break; case SB_PAGEUP: newpos -= view.Height(); if(newpos < low) newpos = low; break; case SB_PAGEDOWN: newpos += view.Height(); if(newpos > high) newpos = high; break; case SB_THUMBTRACK: newpos = info.nTrackPos; break; case SB_THUMBPOSITION: newpos = info.nTrackPos; break; } /* nSBCode */ SetScrollPos(SB_VERT, newpos); c_Image.SetWindowPos(NULL, image.left, -newpos, 0, 0, SWP_NOSIZE | SWP_NOZORDER); CDialog::OnVScroll(nSBCode, nPos, pScrollBar); }
There are lots of approaches to this. One I frequently use is to create an invisible modeless dialog, make it visible when I need to see it, and hiding it when its "Close" box is clicked. This means that I don't have to reinitialize it when it is "re-created". The alternative is to create it "on demand", and allow for it to be destroyed. Modeless dialogs that retain state rarely need this form. I discuss this technique in another essay.
So I create it in the beginning, in the OnInitDialog handler of my main dialog window.
c_Viewer = new CShow; c_Viewer->Create(CShow::IDD, this);
Note that I don't have to know what the IDD_ symbol is for the dialog; I can always use Classname::IDD, and it will always be correct!
Key here is that the CShow c_Viewer variable is declared as a member variable of my main dialog class, because the object must continue to exist as long as the dialog needs to exist.
When I want to show the bitmap which is displayed within the dialog, I just make it visible. Note that if the c_Viewer pointer is NULL or does not represent a valid window, I've done something deeply wrong. If the user has iconized the window, I restore it; if it was invisible, I show it.
void CBitMapInfoDlg::OnViewBitmap() { if(c_Viewer == NULL || !::IsWindow(c_Viewer->GetSafeHwnd())) { /* creation error */ ASSERT(FALSE); } /* creation error */ else { /* show it */ if(c_Viewer->IsIconic()) { /* maximize */ c_Viewer->ShowWindow(SW_RESTORE); } /* maximize */ if(!c_Viewer->IsWindowVisible()) { /* show it */ c_Viewer->ShowWindow(SW_SHOW); } /* show it */ } /* show it */ }
Printing text is quite straightforward. You just paint it onto a DC. For simple lines of text that will fit on the page, there's nothing to it: just draw a line at the first y-coordinate, increment y by the height of the line, and repeat. Minor implementation details include doing a break at page boundaries.
First, I get a DC for the printer by setting the PD_RETURNDC flag in the CPrintDialog. I Attach the DC to a CDC.
void CBitMapInfoDlg::OnFilePrint() { CPrintDialog dlg(FALSE); dlg.m_pd.Flags |= PD_RETURNDC; // get the DC switch(dlg.DoModal()) { /* DoModal */ case 0: case IDCANCEL: return; case IDOK: break; default: ASSERT(FALSE); } /* DoModal */ CDC dc; dc.Attach(dlg.m_pd.hDC);
Before printing a bitmap, you should make sure that the printer really can print a bitmap. Most printers can, but it is good to include the test in all programs.
// Can this printer print bitmaps? DWORD caps = dc.GetDeviceCaps(RASTERCAPS); if((caps & RC_BITBLT) == 0) { /* cannot print bitmaps */ AfxMessageBox(IDS_NO_BITMAPS); return; } /* cannot print bitmaps */
To print, you must first StartDoc and StartPage. I issue an error message if either fails, and return. Note that since I have done an Attach of the DC to the variable dc, the DC will be released when the variable goes out of scope. So no cleanup is required.
CString name; GetWindowText(name); if(dc.StartDoc(name) < 0) { /* printer failed */ DWORD err = ::GetLastError(); CString msg; msg.Format(_T("Unable to start document\n%s"), ErrorString(err)); AfxMessageBox(msg, MB_ICONERROR | MB_OK); return; } /* printer failed */ if(dc.StartPage() < 0) { /* page failed */ DWORD err = ::GetLastError(); CString msg; msg.Format(_T("Unable to start page\n%s"), ErrorString(err)); AfxMessageBox(msg, MB_ICONERROR | MB_OK); return; return; } /* page failed */
Most printers can't print out to the margins (known in the printing industry as "full-bleed"); I chose 0.3 inches as the limit I will use. I compute the page size in pixels and the starting margin (relative to the top-left corner of the page). I treat the margins as symmetric but the real computation of the printable area is in the variable area.
#define PT_LEFTMARGINX 30 // in hundredths of an inch #define PT_TOPMARGINY 30 // in hundredths of an inch CSize pixels(dc.GetDeviceCaps(HORZRES), dc.GetDeviceCaps(VERTRES)); CPoint margin(inchesToPixels(PT_TOPMARGINY, dc), inchesToPixels(PT_LEFTMARGINX, dc)); CRect area(margin.x, margin.y, pixels.cx - margin.x, pixels.cy - margin.y); // First, put out all the statistics and the palette // information int y = area.top; int lineheight = dc.GetTextExtent(CString(_T("X"))).cy;
int pageno = 0; y += DoPageHeading(dc, area, pageno); BOOL anydata = FALSE;
The code below is a simple loop that retrieves one "line" from the ListBox and prints it. Because all entries in the ListBox are members of the BitmapDisplayInfo abstract class, I only have to query if they are something interesting to print or part of the image. The GetLine method of BitmapDisplayInfo returns -1 for anything that is not an actual scan line; for the scan line, it returns the scan line number (this is used to determine where to show the highlighting rectangle on the image). So as long as I have a negative line number, I have displayable data; when it becomes zero or greater, I have hit the bitmap image itself, which is the last element of the display. so I can stop.
There are many approaches to handling "page overflow"; my preference is to throw a page only if I need the space, rather than writing the line and then determining if I'm about to overflow the page. It is easier to avoid gratuitous blank pages with this approach. This code simplistically assumes that the draw routine will take up one line of output. More complex data would require that I query the data object for the height required by providing an explicit virtual GetVerticalSpaceRequired() or similar method as part of the abstract class.
for(int i =0; i < c_Data.GetCount(); i++) { /* read one line */ BitmapDisplayInfo * data = (BitmapDisplayInfo *)c_Data.GetItemDataPtr(i); if(data->GetLine() >= 0) break; // that's all, folks! // If we are at the end of the page, start a new page if(y + lineheight > area.bottom) { /* next page */ dc.EndPage(); dc.StartPage(); y = area.top; y += DoPageHeading(dc, area, pageno); } /* next page */
As I explain later, all drawing of a BitmapDisplayInfo object is accomplished by calling the virtual method Draw. Each instance, which belongs to a concrete subclass, implements the Draw method for its particular type. This draws for both the printer and for the ListBox, because all they need is a DC and a CRect. I have to synthesize the CRect.
CRect r(area.left, y, area.right, y + lineheight); data->Draw(&dc, r); anydata = TRUE; y += lineheight; } /* read one line */ if(anydata) dc.EndPage();
OK, all the text has been drawn. Now it is time to draw the bitmap. Note that the bitmap is always drawn on a new page. I could have combined the pages, but I was feeling lazy again.
CBitmap * bmp; bmp = c_Viewer->GetBitmap(); if((HBITMAP)bmp == NULL) { /* not found */ ASSERT(FALSE); // NYI: error return; } /* not found */
Now strictly speaking, this is a redundant computation. I already have a DIB, which I used to create the bitmap (although I discarded it). Now we are going the opposite direction; starting from a bitmap, I have to re-create the DIB. However, in many cases you will only have a bitmap object, so the point of this is to illustrate how to do the transformation back. Actually, the MakeDIB function is from the Microsoft SHOWDIB example.
HANDLE dib = MakeDIB(bmp); y = area.top; y += DoPageHeading(dc, area, pageno); if(dib != NULL) { /* image it */ int saved = dc.SaveDC(); BITMAPINFOHEADER bmi; DibInfo(dib, &bmi);
There are several ways to deal with the scaling problem. If we print the DIB unscaled, we get it in the printer resolution, which is rarely useful. For example, suppose you had a 1 inch × 1 inch image on your screen. The dot pitch on your display is 0.28mm or something close to it. This means you have approximately 90 pixels per inch. Now on a 600 × 600 pixels-per-inch printer, you would only see a 0.15 inch image, and on a 1200 × 1200 pixels-per-inch printer you would see an image only 0.075 inch, or slightly over 1/16 of an inch square. Not what you want to see.
If the image is larger than a page, this would take more complexity than I was willing to expend to split it up over several pages. So I simply scale it to fit. However, for small images I present them in approximately the original size. I compute a scaling factor that would spread the image horizontally to just fill the area horizontally. I then compute one that would just fill the area vertically. Then I choose the smaller of the two (if I choose the larger, then the other dimension would overflow the page). Then I limit the scaling to be no more than 1:1 based on 90 pixels-per-inch (a fancier program would allow me to set the display resolution so this would be a variable).
double scalingx = (double)(area.Width()) / (double)bmi.biWidth; double scalingy = (double)(area.Height()) / (double)abs(bmi.biHeight); double scaling = min(scalingx, scalingy); // Do not overscale. We assume the resolution of a monitor is 90dpi // Reset the scaling to not exceed 90 points per inch // Note that we could have done this by selecting the mapping mode // MM_LOENGLISH but this illustrates how to do the computations // without that scaling = min(scaling, (double)dc.GetDeviceCaps(LOGPIXELSX) / 90.0f); int width = (int)(scaling * (double)bmi.biWidth); int height = (int)(scaling * (double)abs(bmi.biHeight));
The PrintDIB function, again part of the SHOWDIB example, actually transfers the pixels from the DIB to the printer DC. Then we release the space consumed by the DIB.
PrintDIB(&dc, dib, area.left, y, width, height); FreeDIB(dib); } /* image it */ dc.EndPage(); dc.EndDoc(); }
This is from the SHOWDIB example in the MSDN.
HANDLE MakeDIB(CBitmap * bmp) { return DibFromBitmap((HBITMAP)*bmp, BI_RGB, 0, NULL); } // MakeDIB
This code is from the SHOWDIB example in the MSDN.
VOID PrintDIB (CDC * dc, HANDLE dib, INT x, INT y, INT dx, INT dy) { BITMAPINFOHEADER bi; DibInfo (dib, &bi); if (dib != NULL) { /* Stretch the DIB to printer DC */ StretchDibBlt ( dc, x, y, dx, dy, dib, 0, 0, bi.biWidth, bi.biHeight, SRCCOPY); } }
This code is from the SHOWDIB example in the MSDN.
static BOOL StretchDibBlt ( CDC * dc, // DC into which to print INT x, // Left coordinate of where to print INT y, // Top coordinate of where to print INT dx, // Target width INT dy, // Target height HANDLE hdib, // Handle to a MakeDIB DIB INT x0, // Source left INT y0, // source top INT dx0, // source width INT dy0, // source height LONG rop) // ROP code to use { LPBITMAPINFOHEADER lpbi; LPBYTE pBuf; BOOL f; // If there isn't a DIB provided, erase the area using // PatBlt to paint with the selected brush if (hdib == NULL) return dc->PatBlt(x,y,dx,dy,rop); lpbi = (LPBITMAPINFOHEADER)GlobalLock(hdib); if (!lpbi) return FALSE; pBuf = (LPBYTE)lpbi + (WORD)lpbi->biSize + PaletteSize(lpbi); f = ::StretchDIBits ( dc->m_hDC, // DC to use x, y, // Target location dx, dy, // Target size x0, y0, // Source location dx0, dy0, // Source size pBuf, // pointer to pixels (LPBITMAPINFO)lpbi, // pointer to info DIB_RGB_COLORS,// Use RGB colors rop); // Desired ROP GlobalUnlock(hdib); return f; }
Page headings can be printed at any time. This is not a dot-matrix printer where you have to put things out sequentially, top-to-bottom. So it is almost coincidental that I put the top heading out at the top. However, I do it this way because it allows me to compute the space required for the heading before I start printing. I could equally well have simply reserved a heading area from the area rectangle I computed and put the header out later.
int CBitMapInfoDlg::DoPageHeading(CDC & dc, CRect area, int & pageno) { // filename timestamp Page nn pageno++; CString leftText; leftText = path; leftText += _T(" "); leftText += timestamp.m_ctime.Format(_T("%A, %d-%b-%Y %H:%M:%S")); dc.TextOut(area.left, area.top, leftText); CString s; s.Format(_T("Page %d"), pageno); CSize sz = dc.GetTextExtent(s); int x = area.right - sz.cx; dc.TextOut(x, area.top, s); return sz.cy; } // CBitMapInfoDlg::DoPageHeading
I wanted to highlight the area on the image display to show what is being displayed in the ListBox. So while examining the magnified area in the ListBox, the actual pixels being displayed can be seen. For example, here is an excerpt of the image with the highlighted area enclosed by a dotted rectangle.
To guarantee that the image is visible no matter what the background, I chose to use a box that was alternating white-and-black sequences of pixels. But the key problem was to figure out where to display it (how to display it is covered in the next section). Determining the vertical coordinates was simple: using CListBox::GetTopLine and CWnd::GetClientRect, I could compute the first scan line shown (I stored the scan line information in the BitmapData class when the items were first created). But determining the horizontal position of the leftmost and rightmost pixels was a bit more complex. For example, there is no apparent API call to retrieve, from the ListBox, its current horizontal scrolling position.
After a bit of head-scratching, an Aha! insight and experimentation to confirm the insight, I determined that in the DrawItem handler, the DC provided for the drawing has its origin shifted by the required amount. So during the drawing, I simply detect the first time I'm drawing in the visible area (the drawing coordinate exceeds the origin shift), and the last time I'm drawing in the visible area (the drawing coordinate exceeds the origin shift plus the GetClientRect() width) and I have all the data I need for my bounding box!
Except for one minor detail: there is no event sent from a ListBox to its parent when the ListBox is scrolled, either horizontally or vertically. Unfortunately, this means I had to poll for the current position, an unpleasant approach but the only viable one. What I do is start at the CListBox::GetTopLine, and iterate through all the visible lines. Using the left/right information captured during the drawing, I can tell via the BitDisplayInfo::GetLine() method whether I have a bitmap data line or not. I do this from an OnTimer routine, and if there is any change, I post the event to the image display control.
This seems obvious and trivial: just indicate that a highlight needs to be drawn by supplying its bounding box, and then draw it in the OnPaint handler! What could be simpler?
Except, what OnPaint handler? I See No OnPaint Handler Here. Ah, that's because I chose to use a CStatic and let it draw itself. But if it is drawing itself, how can I draw on it, too? Oh, that's obvious: just override the OnPaint handler, call the superclass to render the bitmap, and then do what I want.
Except, if you've been doing this trick often enough, you may remember the little comment that comes out in a custom OnPaint handler for a CStatic:
// Do not call CStatic::OnPaint() for painting messages
Oops. So we can't really use that trick! (In case you're wondering why, it is because the built-in OnPaint handler wants to do a BeginPaint operation, but you would already have done a BeginPaint, which clears the clip region, so a second BeginPaint wouldn't have the right clipping region. Or the opposite would be true; the BeginPaint in the CStatic paint handler would mess up yours. So now what?
Well, what I did was create a custom child window, which is a child window of the CStatic, which has a simple OnPaint handler. It draws a rectangle with a dotted pen and a hollow brush. My border class is called CBorder, and here is the paint routine for it.
void CBorder::OnPaint() { CPaintDC dc(this); // device context for painting dc.SetBkColor(RGB(0,0,0)); CPen pen(PS_DOT, 0, RGB(255,255,255)); dc.SelectObject(&pen); dc.SelectStockObject(HOLLOW_BRUSH); CRect r; GetClientRect(&r); dc.Rectangle(r); // Do not call CWnd::OnPaint() for painting messages }
To set the position and size, I call SetWindowPos to resize the window. I have to resize the window a bit to allow for the fact that I want to enclose the area, so I have to move it up one pixel. However, Windows draws objects up-to-but-not-including the endpoint, so I have to add one additional pixel to the right and bottom to get the proper inclusion. Here is the method called to show the bounding box by setting its dimensions.
void CBorder::ShowBB(const CRect & r) { CRect w = r; w.top--; // move up 1 so area enclosed is selected area w.bottom++; // move down 1 so area enclosed is selected area w.right++; // encompass right end SetWindowPos(NULL, w.left, w.top, w.Width(), w.Height(), SWP_NOZORDER); } // CBorder::ShowBB
I found the static display of the image to be hard to find, so I decided I wanted it to blink. What is the ideal blink rate? The blink rate the user has selected for the caret:
SetTimer(IDT_BLINK, ::GetCaretBlinkTime(), NULL);
The timer handler then determines if the box is active, and if so, will show it or hide it (if it is made inactive it is already hidden)
void CBorder::OnTimer(UINT nIDEvent) { if(!showing) return; // do nothing if(IsWindowVisible()) ShowWindow(SW_HIDE); else ShowWindow(SW_SHOW); CWnd::OnTimer(nIDEvent); }
The method that shows or hides the blinking box is also called ShowBB, but it takes a BOOL argument, so operator overloading works properly to resolve which method. However, we don't want this to show the box if it is currently active but in its "off" phase, so a request to "show" the box is ignored if the current mode is "active" and the desired mode is "active".
void CBorder::ShowBB(BOOL show) { if(showing && show) return; showing = show; ShowWindow(show ? SW_SHOW : SW_HIDE); } // CBorder::ShowBB
This is actually fairly easy, although it does require that you do the menu item enabling yourself. For reasons that still seem obscure to me, Microsoft does not support ON_UPDATE_COMMAND_UI in a dialog. What surprises me is that when I was a straight-API programmer, I found doing this quite trivial. However, Microsoft doesn't, so we're stuck with handling it ourselves.
First, create a menu resource. Give it some name, or accept the default IDR_MENU1 name it gives you. Then bring up the Properties box for the dialog and add that resource to the dialog, as shown below.
Now you have to handle enabling/disabling the items. I believe in doing this in a centralized fashion, rather than spreading the enabling/disabling out over dozens of locations in the program. This results in code that is easier to write and maintain (ON_UPDATE_COMMAND_UI appears to distribute the logic across many functions, but in fact it "centralizes" the logic but on a per-control basis. When you can't do that, use a single site in the program. I outline how to do this in general for dialogs in my essay on Dialog Control Management).
I revert here to a technique I used back when I was doing raw API programming: put all the logic in the OnInitMenuPopup handler.
The first rule is to ignore all the parameters of this call. They don't really do much for you, since relying on them only results in code that is harder to write and maintain (for example, if you rely on the menu index, and you move the menu item, you need to rewrite your code; using my technique, your program is insensitive to the location of a menu item).
I have only one menu item in this application that needs enabling/disabling, the Print item. I can print only if there is an active bitmap. The code is:
void CBitMapInfoDlg::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu) { CDialog::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu); CBitmap * bmp = c_Viewer->GetBitmap(); GetMenu()->EnableMenuItem(IDM_FILE_PRINT, bmp == NULL ? MF_GRAYED : MF_ENABLED); }
Dialogs can be resized. To create a resizable dialog, select the Properties, and select the border style to be "Resizing". Since it is resizable, you usually want to select both the Minimize box and Maximize box options.
Now you need to resize the controls and/or limit the minimum size the user can select. To resize the controls, just handle this in the OnSize handler. You must assume that your OnSize handler will be called before the controls are even created, so you have to test to see if the control exists before resizing it. When you have several interacting controls whose size and/or position depend upon the size and/or position of another control, you must make sure all the controls in the computation exist before trying to perform the computation. I only needed to resize the ListBox, which is carefully placed at the bottom of the dialog to make this task easy. Here's the code:
void CBitMapInfoDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); if(nType == SIZE_MINIMIZED) return; // don't bother if going to icon size if(c_Data.GetSafeHwnd() != NULL) // make sure control exists! { /* resize */ CRect r; c_Data.GetWindowRect(&r); ScreenToClient(&r); c_Data.SetWindowPos(NULL, 0, 0, // no position change, ignored cx, cy - r.top, SWP_NOMOVE | SWP_NOZORDER); } /* resize */ }
The algorithm does a GetWindowRect because it needs the entire rectangle size of the ListBox, but the resize wants coordinates in client coordinates, so ScreenToClient is used to transform the coordinates. I prefer to use SetWindowPos when I only need to change one of the parameters, such as the position or the size; I find it more convenient than MoveWindow for this purpose.
If you have more complex requirements, remember that you hardly ever need a constant in these computations, with the typical exception of a 2 for computing centering or similar constants to multiply gap intervals. Most widths should be computed directly from the control information or the client area size; gaps tend to be related to the values you can conveniently obtain from ::GetSystemMetrics, or small integer multiples of them. For example 2 * ::GetSystemMetrics(SM_CXFRAME) is often a nice gap between controls. Hardwiring a pixel value is almost always a mistake. With resolutions running from 800 × 600 to 1600 × 1200, any constant you choose that is not automatically compensated by some resolution-dependent computation (such as using ::GetSystemMetrics) should be considered a fatal error.
The rest of the work is just boring geometry. There are layout management packages described in other articles http://www.codeproject.com as
Now that you have a resizable dialog, you may notice that you may want a minimum size that it can be resized to. For example, you may not want to do dynamic reorganization of every control on your dialog; in fact, you may want to leave nearly all of them fixed where you placed them. But a resize allows the user to hide the controls by dragging the dialog to a smaller size than will show everything.
To accomplish this, you need to handle the WM_GETMINMAXINFO (OnGetMinMaxInfo) event. Unfortunately, the ClassWizard "knows" (incorrectly) that a dialog never needs to handle this message, so doesn't make it available. There are two solutions to this: one, you can "add it by hand" just by typing it in, or, if you are lazy like I am, you need to lie to ClassWizard. To lie to ClassWizard, switch over to the Class Info tab, and tell ClassWizard you have a "Topmost Frame" style window (you can reset it to "Dialog" later, if you care). Now all the messages are available.
Then I place a CStatic control which I use to delimit the area I want to cover. I mark it as invisible and give it a name other than IDC_STATIC, and use ClassWizard to give it a member variable, something like c_Limit. Note that in the image below, my invisible frame keeps the user from resizing the ListBox completely out of existence and prevents hiding the selection coordinates.
Here's the handler. Note that like OnResize, this can be called before the controls are created. Note the "fudge factors" I add to allow for the caption bar, menu, and window borders (vertical) and the window borders (horizontal).
void CBitMapInfoDlg::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI) { CDialog::OnGetMinMaxInfo(lpMMI); if(c_Limit.GetSafeHwnd() != NULL) { /* use limit */ CRect r; c_Limit.GetWindowRect(&r); ScreenToClient(&r); lpMMI->ptMinTrackSize.x = r.right + 2 * ::GetSystemMetrics(SM_CXFRAME); lpMMI->ptMinTrackSize.y = r.bottom + ::GetSystemMetrics(SM_CYCAPTION) + ::GetSystemMetrics(SM_CYMENU) + 2 * ::GetSystemMetrics(SM_CYFRAME); } /* use limit */ }
Shown below is the result of the limit illustrated. The user cannot make the dialog box any smaller than this. Note that I should probably allow a bit more vertical space because the vertical scroll buttons are so tiny. I hadn't allowed for the horizontal scrollbar in my example.
This technique is directly from Paul DiLascia's article in the MSDN on how to handle this. Key here is to load the accelerator table and add the code shown below. As many readers of my regular posts are aware, I am opposed to the use of PreTranslateMessage as a catch-all for all sorts of inappropriate functionality; however, this is one of the cases where the use of PreTranslateMessage is absolutely necessary.
BOOL CBitMapInfoDlg::PreTranslateMessage(MSG* pMsg) { if (WM_KEYFIRST <= pMsg->message && pMsg->message <= WM_KEYLAST) { if (accelerators != NULL && ::TranslateAccelerator(m_hWnd, accelerators, pMsg)) return TRUE; } return CDialog::PreTranslateMessage(pMsg); }
I used the following class hierarchy for my display
BitmapDisplayInfo | Generic superclass | ||||
BitmapText | Ordinary text to display | ||||
BitmapFixedText | Text to be displayed with fixed-pitch font | ||||
BitmapPalette | Palette information | ||||
BitmapData | All bitmaps | ||||
BitmapRGB24 | 24-bit bitmaps | ||||
BitmapP | All paletted bitmaps | ||||
Bitmap8 | 8-bit color bitmaps | ||||
Bitmap4 | 4-bit color bitmaps | ||||
Bitmap1 | 1-bit color bitmaps |
I override the AddString routines for the owner-draw ListBox:
int AddString(BitmapDisplayInfo * data) { return CListBox::AddString((LPCTSTR)data); } int AddString(LPCTSTR s) { return AddString(new BitmapText(s)); }
Note that the first form takes the abstract base class reference, so any object I create can be added. The second form is a convenience since strings are so commonly added. I do not support InsertString, so for safetly I added
int InsertString(LPCTSTR s) { ASSERT(FALSE); return LB_ERR; }
This at least will catch errors if I ever did an InsertString with a string argument, and if I tried it with a BitmapDisplayInfo * the compiler would catch it. If I didn't put the definition above in, calling InsertString with a string argument would put a bogus ItemData value in, and the app would crash. Better to catch the problem early.
In addition, I compute the horizontal extent "on the fly", but when I clear the ListBox, I need to reset the horizontal extent. So I also added
void ResetContent() { CListBox::ResetContent(); SetHorizontalExtent(0); }
Using these techniques, all the ItemData components are of the class BitmapDisplayInfo *, so the DrawItem routine basically just does
BitmapDisplayInfo * info = (BitmapDisplayInfo *)dis->itemData; info->Draw(dc, dis->rcItem);
And that's it! So much more elegant than having complex decodes, and it puts the drawing logic where it belongs, in the object itself.
This made the printing very easy. I just iterated through the ListBox, picking up each ItemData value, and calling its Draw method using a printer DC and a synthesized rectangle which indicated where it went on the page.
The views expressed in these essays are those of the author, and in no way represent, nor are they endorsed by, Microsoft.