Related
I was getting weird results with DrawFocusRect() in a TreeView custom draw routine. The outline was somewhat different, some were almost a solid line and others were dashed. I found out that it's the HDC SetTextColor() value that is affecting it. Even though the selection bars fill color is exactly the same, as I changed various text colors, indeed the drawn outline was different.
I ended up with setting the text color to match the fill color of the highlight bar which gives the same outline the default tree drawing routine gives.
Is this not documented anywhere? Is there even more to it?
Thanks?
DrawFocusRect draws the focus rectangle with a bitwise XOR operation. This is hinted at in the SDK documentation, under the "Remarks" section, where it says:
Because DrawFocusRect is an XOR function, calling it a second time with the same rectangle removes the rectangle from the screen.
This is the whole advantage of DrawFocusRect. It means that you (or the window manager) can draw the focus rectangle and erase it whenever needed without redrawing the entire underlying contents. (This was a big performance win in the era before double-buffering, gobs of memory, fast graphic cards, etc.) The focus rectangle is drawn wherever it needs to be drawn, then, when it needs to be erased, it is just drawn again on top of where it was drawn the first time. The only thing that you (or the window manager) need to keep track of is the coordinates of the rectangle.
You can achieve exactly the same effect as DrawFocusRect by using code like the following to draw an XOR rectangle manually:
void DrawXorRect(HDC hDC, const RECT* prc)
{
// (1)
static const WORD pattern[] = { 0x5555, 0xAAAA,
0x5555, 0xAAAA,
0x5555, 0xAAAA,
0x5555, 0xAAAA,
};
HBITMAP hbmpPattern = CreateBitmap(8, 8, 1, 1, pattern);
// (2)
HBRUSH hbrPattern = CreatePatternBrush(hbmpPattern);
// (3)
HBRUSH hbrOriginal = (HBRUSH)SelectObject(hDC, hbrPattern);
// (4)
UINT cx;
UINT cy;
SystemParametersInfo(SPI_GETFOCUSBORDERWIDTH , 0, &cx, 0);
SystemParametersInfo(SPI_GETFOCUSBORDERHEIGHT, 0, &cy, 0);
// (5)
PatBlt(hDC, prc->left , prc->top , prc->right - prc->left, cy , PATINVERT); // top
PatBlt(hDC, prc->left , prc->bottom - cy, prc->right - prc->left, cy , PATINVERT); // bottom
PatBlt(hDC, prc->left , prc->top + cy, cx , prc->bottom - prc->top - (cy * 2), PATINVERT); // left
PatBlt(hDC, prc->right - cx, prc->top + cy, cx , prc->bottom - prc->top - (cy * 2), PATINVERT); // right
// (6)
SelectObject(hDC, hbrOriginal);
DeleteObject(hbrPattern);
DeleteObject(hbmpPattern);
}
Error-checking has been elided for brevity, but this code does work, and it does mimic DrawFocusRect exactly. (Yes, I even tested it.)
Let's go through the code, step by step, to see what it does, how it works, and what that means:
First, it creates a monochrome 8×8 bitmap (CreateBitmap()) consisting of an alternating pattern of bits (pattern[]): on, off, on, off, etc. (If you don't know why, open up the Windows calculator, switch it to hex mode, and paste in the hex values that comprise the pattern. Look at the bits: see how they alternate between 0s and 1s? Now, imagine creating a bitmap from that.)
Then, it uses that bitmap to create a brush (CreatePatternBrush()).
Next, it selects the newly-created brush into the DC (SelectObject()). It saves the return value, which is the brush that was originally selected in the DC because it needs to restore that later.
With all the drawing objects created, it calls SystemParametersInfo() to retrieve the width (SPI_GETFOCUSBORDER_WIDTH) and height (SPI_GETFOCUSBORDERHEIGHT) of the focus rectangle.
Armed with all the objects and information needed to do the drawing, it blits each of the 4 sides of the rectangle using the PATINVERT raster operation. What is PATINVERT?
Combines the colors of the specified pattern with the colors of the destination rectangle by using the Boolean XOR operator.
Finally, it cleans up after itself by restoring the originally-selected brush and deleting the drawing objects that it created.
So, basically, the focus rectangle is drawn by XORing an alternating pattern of on-and-off pixels (a checkerboard pattern) with the contents of the device context (i.e., whatever is on the screen). That's what PATINVERT does. But wait—we know what the colors of the device context are, but what are the colors of the brush? Well, remember that the brush was created from a monochrome bitmap. The SDK documentation for CreateBitmap says:
If the bitmap is monochrome, zeros represent the foreground color and ones represent the background color for the destination device context.
Aha! So when the brush's bitmap pattern is 0, the brush's color is your device context's foreground color (SetTextColor); when the brush's bitmap pattern is 1, the brush's color is your device context's background color (SetBackColor). This is how the foreground and background color of the device context come into play. These colors are merged together, via an XOR operation, with the color of the original pixel in the device context.
When using DrawFocusRect, your DC's foreground and background colors should be black and white, respectively. This is the default foreground and background colors for a device context, so if you haven't called the SetTextColor or SetBackColor functions, these will be its colors. If you have called either of those functions, you either need to:
Save the value returned by each and restore it later, once you are finished drawing, but before you call DrawFocusRect (just as the code above did with the return value of the first call to SelectObject), or
Call the SaveDC function at the beginning of your drawing code to save its state (by pushing it onto a stack internally), and then call the RestoreDC function at the end of your drawing code to restore it to its original state.
When the DC's foreground and background colors are black and white, respectively, then the DrawFocusRect function works as it is expected to. This is what you need to do if you want to match how the window manager draws focus rectangles around controls. (And you do, obviously, want to do this!)
If, for some reason, you don't want to reset the DC's colors, then you won't be able to use DrawFocusRect. You'll need to take another tack where you control the colors yourself. For example, you could create a color (non-monochrome) bitmap, with its colors fixed as white and black, and then create a brush from that. Alternatively, you could consider drawing with a pen and the R2_NOT ROP code; e.g.:
void DrawXorRect2(HDC hDC, const RECT* prc)
{
LOGBRUSH lb;
lb.lbStyle = BS_SOLID;
lb.lbColor = RGB(0, 0, 0); // black
HPEN hpenNew = ExtCreatePen(PS_COSMETIC | PS_ALTERNATE, 1, &lb, 0, NULL);
HPEN hpenOld = (HPEN)SelectObject(hDC, hpenNew);
int ropOld = SetROP2(hDC, R2_NOT);
int modeOld = SetBkMode(hDC, TRANSPARENT);
HBRUSH hbrOriginal = (HBRUSH)SelectObject(hDC, GetStockObject(NULL_BRUSH));
Rectangle(hDC, prc->left, prc->top, prc->right, prc->bottom);
SelectObject(hDC, hbrOriginal);
SetBkMode(hDC, modeOld);
SetROP2(hDC, ropOld);
SelectObject(hDC, hpenOld);
DeleteObject(hpenNew);
}
I'm not sure this is actually much better, as it still requires that you set attributes of the DC—namely, the ROP and the background mode. If you're going to set and restore attributes of the DC, why not just set and restore the foreground and background colors? Plus, this pen-based implementation has the additional disadvantage of not respecting the user's preference for the thickness of the focus rectangle, which is an accessibility bug. Also, the pixel grid is slightly different here compared to the built-in DrawFocusRect, but that is, admittedly, an extremely minor drawback.
Another approach would be to create a PS_GEOMETRIC pen with a custom pattern (PS_USERSTYLE, to make it alternating), and setting the ROP style to R2_XORPEN/R2_NOTXORPEN:
void DrawXorRect3(HDC hDC, const RECT* prc)
{
UINT cx;
UINT cy;
SystemParametersInfo(SPI_GETFOCUSBORDERWIDTH , 0, &cx, 0);
SystemParametersInfo(SPI_GETFOCUSBORDERHEIGHT, 0, &cy, 0);
LOGBRUSH lb;
lb.lbStyle = BS_SOLID;
lb.lbColor = RGB(0, 0, 0);
static const DWORD pattern[] = { 0, 2 };
HPEN hpenWidth = ExtCreatePen(PS_GEOMETRIC | PS_USERSTYLE, cx, &lb, 2, pattern);
HPEN hpenHeight = ExtCreatePen(PS_GEOMETRIC | PS_USERSTYLE, cy, &lb, 2, pattern);
int ropOriginal = SetROP2(hDC, R2_NOTXORPEN);
HPEN hpenOriginal = (HPEN)SelectObject(hDC, hpenHeight);
MoveToEx(hDC, prc->left , prc->top , NULL); // \ top
LineTo (hDC, prc->right , prc->top ); // / edge
MoveToEx(hDC, prc->left + cx, prc->bottom - cy , NULL); // \ bottom
LineTo (hDC, prc->right - cx, prc->bottom - cy ); // / edge
SelectObject(hDC, hpenWidth);
MoveToEx(hDC, prc->left , prc->top + (cy * 2), NULL); // \ left
LineTo (hDC, prc->left , prc->bottom - cy ); // / edge
MoveToEx(hDC, prc->right - cx, prc->top + cy , NULL); // \ right
LineTo (hDC, prc->right - cx, prc->bottom ); // / edge
SelectObject(hDC, hpenOriginal);
SetROP2(hDC, ropOriginal);
DeleteObject(hpenHeight);
DeleteObject(hpenWidth);
}
This addresses the accessibility bug and makes the pixel pattern match that produced by DrawFocusRect, at the expense of creating an additional pen. It also, like the second attempt, works as expected regardless of what the DC's foreground or background colors are set to.
Note that all of these implementations of DrawXorRect become more efficient if you create the required drawing objects once and cache them, instead of creating and destroying them each time. This is surely a big part of the reason (along with convenience—who wants to write all this ugly code?) why the window manager provides the DrawFocusRect function for applications to use.
I am getting a very poor peformance drawing with Win32. It takes too much time and needs improving. Please advise.
Here is what I do.
HDC dc = GetDC(wnd);
HDC memoryDc = CreateCompatibleDC(dc);
HBITMAP memoryMapBitmap = CreateCompatibleBitmap(dc, 400, 400);
HGDIOBJ originalBitmap = SelectObject(memoryDc, memoryMapBitmap);
Then, I draw in a for-loop as follows.
HBRUSH brush = (HBRUSH)GetStockObject(DC_BRUSH);
SetDCBrushColor(memoryDc, colorRef);
FillRect(memoryDc, &rect, brush);
And finally, I do a cleanup
SelectObject(memoryDc, originalBitmap);
DeleteDC(memoryDc);
ReleaseDC(wnd, dc);
Drawing takes a lot of time (several seconds). Is there a way to draw faster with Win32?
Thanks in advance!
It looks like I have solved it. Below is the solution with some comments.
I have a dialog defined in RC-file. There is a control to display a bitmap image in the dialog.
CONTROL "", IDC_MEMORY_MAP, WC_STATIC, SS_BITMAP | SS_CENTERIMAGE | SS_SUNKEN, 9, 21, 271, 338, WS_EX_LEFT
In the run-time I need to create, draw and display a bitmap:
HWND map = GetDlgItem(dlg, IDC_MEMORY_MAP);
HBITMAP bitmap = createMemoryMapBitmap(map);
bitmap = (HBITMAP)SendMessage(map, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)bitmap);
DeleteObject(bitmap); // (!) this is a very important line, otherwise old bitmap leaks
Code that finds out the size of the bitmap to create:
HBITMAP createMemoryMapBitmap(HWND map) {
RECT rect = {0, 0, 0, 0};
GetClientRect(map, &rect);
SIZE size = {rect.right - rect.left, rect.bottom - rect.top};
HDC dc = GetDC(map);
HBITMAP bitmap = doCreateMemoryMapBitmap(dc, &size);
ReleaseDC(map, dc);
return bitmap;
}
Finally, we actually create the bitmap and draw on it:
HBITMAP doCreateMemoryMapBitmap(HDC dc, LPSIZE bitmapSize) {
// create 24bpp bitmap in memory in order to draw fast
BITMAPINFO info;
memset(&info, 0, sizeof(info));
info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
info.bmiHeader.biWidth = bitmapSize->cx;
info.bmiHeader.biHeight = bitmapSize->cy;
info.bmiHeader.biPlanes = 1;
info.bmiHeader.biBitCount = 24;
info.bmiHeader.biCompression = BI_RGB;
void *pixels = NULL;
HBITMAP memoryBitmap = CreateDIBSection(dc, &info, DIB_RGB_COLORS, &pixels, NULL, 0);
HDC memoryDc = CreateCompatibleDC(dc); // (!) memoryDc is attached to current thread
HGDIOBJ originalDcBitmap = SelectObject(memoryDc, memoryBitmap);
// drawing code here
// perform windows gdi cleanup
SelectObject(memoryDc, originalDcBitmap); // restore original bitmap in memoryDC (optional step)
DeleteDC(memoryDc); // this releases memoryBitmap from memoryDC
return memoryBitmap;
}
The idea above is to create a 24bpp bitmap in the memory and draw on it. This way drawing is fast, as #IInspectable pointed out.
If display is in the indexed color mode, e.g. 16 or 256 colors, it seems Windows native control is smart enough to convert the color depth automatically displaying the bitmap.
I need to make an interactive graph using MFC
It will be sort of like an equalizer control, where the user should be able to click on a point on the equalizer drag it to change it's y axis value
I am also just starting to learn MFC
Upto this point I have used CPaintDC in the OnPaint() function to draw the graph in a dialog box. For now the graph is very simple, with rectangle boarder, filled with white colour, and 4 points on the graph. I use OnMouseMove() function to know if the cursor is inside the graph area and OnLButtonDown() function to know where the user has clicked. If the user has clicked at a position which implies I want to change the y axis value of the graph point at that location, I repaint the figure using Invalidate() and calling OnPaint() inside OnLButtonDown(). However, every time the graph has to update, I can see a flicker. it is not a problem now, but I will need to extend this graph so that it has at least 64 changeable points, with the ability to change the y axis value for a point by dragging instead of just clicking where I want it to go. Will the flickering problem increase as I increase the number of points and the complexity of the appearance of the graph? The graph will need to have axes, gridlines, labels, etc later on. Is the flickering something I should be concerned about? Is there any way I can prevent it?
----UPDATE ----
This is how I updated my OnPaint() function according to how I understood CodeDreamer's suggestion
void Cgraph_on_dlgboxDlg::OnPaint()
{
CPaintDC dc_blt(this);
CDC dc;
CBitmap bmpDC;
CRect rcClient;
GetClientRect(rcClient);
if (IsIconic())
{
// CPaintDC dc(this); // device context for painting
SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
// Center icon in client rectangle
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// Draw the icon
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialogEx::OnPaint();
}
dc.CreateCompatibleDC(&dc);
bmpDC.CreateCompatibleBitmap(&dc, theGraph.width,theGraph.height );
dc.SelectObject(&bmpDC);
CPen pen;
COLORREF pencolour = RGB(0, 0, 0);
COLORREF brushcolour = RGB(0, 0, 255);
COLORREF graphColour = RGB(0, 0, 150);
// Draw boarder
pen.CreatePen(PS_SOLID, 3, pencolour);
// CBrush brush(HS_CROSS, brushcolour);
dc.SetBkMode(TRANSPARENT);
dc.SetMapMode(MM_TEXT);
dc.SetViewportOrg(theGraph.x1, theGraph.y1);
dc.SelectObject(&pen);
// Draw graph boundary
CPoint point1(0,0);
point1.x = 0;
point1.y = 0;
CPoint point2(0,0);
point2.x = point1.x + theGraph.width;
point2.y = point1.y + theGraph.height;
dc.Rectangle(CRect(point1, point2));
pen.DeleteObject();
// Draw Horizontal at 0
pen.CreatePen(PS_SOLID, 1, pencolour);
dc.SelectObject(&pen);
dc.MoveTo(0, theGraph.height - ORG_DIST_FROM_BOTTOM);
dc.LineTo(theGraph.width, theGraph.height - ORG_DIST_FROM_BOTTOM);
pen.DeleteObject();
dc.SetViewportOrg(theGraph.x1, theGraph.y1 + theGraph.height - ORG_DIST_FROM_BOTTOM); // dc.SetViewportOrg() always works relative to the clinet origin
// Draw graph line
pen.CreatePen(PS_SOLID, 2, graphColour);
dc.SelectObject(&pen);
for(int i = 0; i<NUM_OF_SECTIONS_IN_GRAPH; i++){
dc.MoveTo(graphSamplePoints[i].x, graphSamplePoints[i].y);
dc.LineTo(graphSamplePoints[i+1].x, graphSamplePoints[i+1].y);
}
// draw circles at graph sample points
for(int i = 0; i<NUM_OF_POINTS_IN_GRAPH; i++){
CIRCLE(dc, graphSamplePoints[i].x, graphSamplePoints[i].y, GRP_SMP_RAD);
}
// dc_blt.BitBlt(0,0,rcClient.Width(), rcClient.Height(), &dc, 0, 0, SRCCOPY);
dc_blt.BitBlt(theGraph.x1,theGraph.y1,theGraph.width, theGraph.height, &dc, 0, 0, SRCCOPY);
}
I will need to change the origins of the viewport a number of times and my guess is that this could be one of the reasons for the error. Any suggestions will be welcome.
This is what my output look like without double buffering
This is what it looks like with my attempt at double buffering
In this case, a general solution is 'double buffering'.
The principle is that it creates a compatible memory dc for drawing in advance, and when drawing is ended, it outputs on screen dc.
A code sample is below.
//in OnPaint() function
CPaintDC dc(this);
CDC dcMem;
CBitmap bmpDC;
CRect rcClient;
GetClientRect(&rcClient);
dcMem.CreateCompatibleDC(pDC);
bmpDC.CreateCompatibleBitmap(pDC, rcClient.Width(), rcClient.Height());
dcMem.SelectObject(&bmpDC);
CRect rect(0, 0, 100, 200);
dcMem.Rectangle(rect);
dc.BitBlt(0, 0, rcClient.Width(), rcClient.Height(), &dcMem, 0, 0, SRCCOPY);
A couple of references are below.
introduction
another reference
I hope this will help you a little.
Try the CMemDC MFC class.
In your OnPaintFunction you would have this:
CPaintDC DC(this);
CMemDC mDC(&DC, this);
// now use mDC instead of DC
Also look here fore more samples and explanations.
I'm studying Drawing Shapes by WinAPI C++
I tried to draw 2 ellipse with some codes on WM_PAINT:
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
HPEN hPen = CreatePen(PS_DASHDOTDOT, 2, NULL);SelectObject(hdc, hPen);
Ellipse(hdc, 100, 200, 400, 400);
Ellipse(hdc, 300, 300, 500, 510);
DeleteObject(hPen);
EndPaint(hWnd, &ps);
But the output is:
The result I expect is neither shapes is front of the other. And the border is dash dot dot.
Can anyone show me my mistake? I appreciate for your help.
Ellipse() (like all GDI shape functions) fills the shape using the current brush, which is why your output looks like that. For details on that see setting pen and brush colors.
If you want just the ellipse with no fill, first select a null brush:
SelectObject( hdc, GetStockObject( NULL_BRUSH ) );
One appears on top of the other because you're not just drawing the outline, but filling it as well. To stop filling it, you can select a "hollow brush", sometimes called a "null brush".
HBRUSH hbrOld = SelectObject(hdc, GetStockObject(HOLLOW_BRUSH));
// draw your ellipses here
You can only create dotted or dashed pens with widths of 1 or 0. You used 2, so the command failed.
Also, you should select the pen back out of the DC before you delete it.
I am running this following code,
HDC hdc;
HDC hdcMem;
HBITMAP bitmap;
RECT c;
GetClientRect(viewHandle, &c);
// instead of BeginPaint use GetDC or GetWindowDC
hdc = GetDC(viewHandle);
hdcMem = CreateCompatibleDC(hdc);
// always create the bitmap for the memdc from the window dc
bitmap = CreateCompatibleBitmap(hdc,c.right-c.left,200);
SelectObject(hdcMem, bitmap);
// only execute the code up to this point one time
// that is, you only need to create the back buffer once
// you can reuse it over and over again after that
// draw on hdcMem
// for example ...
Rectangle(hdcMem, 126, 0, 624, 400);
// when finished drawing blit the hdcMem to the hdc
BitBlt(hdc, 0, 0, c.right-c.left,200, hdcMem, 0, 0, SRCCOPY);
// note, height is not spelled i before e
// Clean up - only need to do this one time as well
DeleteDC(hdcMem);
DeleteObject(bitmap);
ReleaseDC(viewHandle, hdc);
The code is just fine. But I am seeing black color around this rectangle. Why is that? Here is an example image.
The bitmap is most likely initialized to be all black. You are then drawing a white rectangle that between x-coordinates 126 and 624. Hence, everything to the left of x=126 and to the right of x=624 stays black.
Edit: The documentation for CreateCompatibleBitmap doesn't state how the bitmap will be initialized, so you should explicitly initialize the bitmap with a specific colour, as Goz suggests, using FillRect:
RECT rc;
rc.left=0;
rc.top=0;
rc.right=c.right-c.left;
rc.bottom=200;
FillRect(hdcMem, &rc, (HBRUSH)GetStockObject(GRAY_BRUSH));
This example fills the bitmap in gray -- you may need to CreateSolidBrush your own brush if you need a different colour. (Don't forget to call DeleteObject when you're done.)
As a side note, I find it a bit strange that your bitmap is being set to a constant height of 200 -- the normal thing would be to make the height of the bitmap equal to the height of the window (as is done for the width).
Might it be because you haven't initialised the memory bitmap area to a given colour? Try FillRect'ing the background to a different colour then draw your white rectangle over it and see what happens.
Per MSDN http://msdn.microsoft.com/en-us/library/dd162898.aspx:
The rectangle is outlined by using the current pen and filled by using the current brush.
Consider calling FillRect instead, or select an appropriate pen prior to calling Rectangle'.
I used:
// Fill the background
hdcMem->FillSolidRect(c, hdcMem->GetBkColor());
Just as a note.