Say you're writing a Win32 application. You add a toolbar. Simple enough. Toolbar would look better with some things on it.
You want to, say, add a button to it. Like this
Flip through Petzold.
No chapters for toolbar dropdowns.
No obvious samples to use.
We're on our own, then.
Attempt 1: BTNS_DROPDOWN
You follow some of the sample code, and do the most natural thing. Use the toolbar button style 'BTNS_DROPDOWN'. (By the way, BTNS_DROPDOWN is the updated define for TBSTYLE_DROPDOWN. They mean the same thing.)
TBBUTTON tbButtons[] =
{
{ STD_CUT, 0, TBSTATE_ENABLED, BTNS_DROPDOWN, {0}, 0, (INT_PTR)L"Test" },
};
m_hwnd = CreateToolbarEx(
parent,
WS_CHILD | WS_VISIBLE | CCS_ADJUSTABLE | TBSTYLE_TOOLTIPS,
0,
sizeof(tbButtons) / sizeof(TBBUTTON), //nBitmaps
HINST_COMMCTRL,
0, // wBMID
tbButtons, //lpButtons
sizeof(tbButtons) / sizeof(TBBUTTON), // iNumButtons
90, 90, 90, 90,
sizeof(TBBUTTON)); // uStructSize
SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this);
SendMessage(m_hwnd, TB_AUTOSIZE, 0, 0);
ShowWindow(m_hwnd, TRUE);
Compile and run. STD_CUT is your standard built-in Windows scissors 'cut' icon. Result looks like this:
That visually looks fine. But wait. Let's try clicking on it.
It doesn't even show a 'button is pushed' animation. It should at least do that, right?
What gives? It's not disabled.
Attempt 2: TBSTYLE_EX_DRAWDDARROWS
Okay, so maybe our initialization of the dropdown menu was incomplete. Dropdown menus usually have an arrow at the right. Perhaps we need to add the "arrow at the right" extended style? Let's try adding the code
SendMessage(m_hwnd, TB_SETEXTENDEDSTYLE, 0, TBSTYLE_EX_DRAWDDARROWS);
So that now, it looks like
TBBUTTON tbButtons[] =
{
{ STD_CUT, 0, TBSTATE_ENABLED, BTNS_DROPDOWN, {0}, 0, (INT_PTR)L"Test" },
};
m_hwnd = CreateToolbarEx(
parent,
WS_CHILD | WS_VISIBLE | CCS_ADJUSTABLE | TBSTYLE_TOOLTIPS,
0,
sizeof(tbButtons) / sizeof(TBBUTTON), //nBitmaps
HINST_COMMCTRL,
0, // wBMID
tbButtons, //lpButtons
sizeof(tbButtons) / sizeof(TBBUTTON), // iNumButtons
90, 90, 90, 90,
sizeof(TBBUTTON)); // uStructSize
SendMessage(m_hwnd, TB_SETEXTENDEDSTYLE, 0, TBSTYLE_EX_DRAWDDARROWS);
SetWindowLongPtr(m_hwnd, GWLP_USERDATA, (LONG_PTR)this);
SendMessage(m_hwnd, TB_AUTOSIZE, 0, 0);
ShowWindow(m_hwnd, TRUE);
Let's compile and run it and see what it looks like now.
This looks better. There's an arrow on the right. That should mean something good. Let's try clicking on it.
Clicking on the button itself works.
Clicking on the arrow doesn't đ
Attempt 3: BTNS_WHOLEDROPDOWN
Maybe the ticket is WHOLEDROPDOWN. Looking it up in the header, BTNS_WHOLEDROPDOWN purports to
That sounds like it could make the whole button appear responsive, so why not let's try it.
Result looks like this:
Okay. What if we try to click on it?
Nothing happens đ
What to do?
The Answer
The answer: toolbar dropdown menus, by default, don't have any animation for clicking on them. They're not like normal buttons. That's right, the button is still working, there's just no visual feedback unless you explicitly attach some yourself.
To make the toolbar dropdown button do something, you have to just trust that it is set up ok, and attach some behavior to the dropdown notification.
Fortunately you don't have to re-invent the wheel to do that. Here's an easy way to attach a simple pop-up menu to the dropdown.
First, you need to have your WndProc pay attention to WM_NOTIFY. The handler can be something like
case WM_NOTIFY:
{
LPNMTOOLBAR lpnmtb = (LPNMTOOLBAR)lParam;
if (lpnmtb->hdr.code == TBN_DROPDOWN)
{
// Get the coordinates of the button.
RECT rc;
SendMessage(lpnmtb->hdr.hwndFrom, TB_GETRECT, (WPARAM)lpnmtb->iItem, (LPARAM)&rc);
// Convert to screen coordinates.
MapWindowPoints(lpnmtb->hdr.hwndFrom, HWND_DESKTOP, (LPPOINT)&rc, 2);
HMENU hMenuLoaded = LoadMenu(g_hInst, MAKEINTRESOURCE(IDR_MENU1));
// Get the submenu for the first menu item.
HMENU hPopupMenu = GetSubMenu(hMenuLoaded, 0);
TPMPARAMS tpm;
tpm.cbSize = sizeof(TPMPARAMS);
tpm.rcExclude = rc;
TrackPopupMenuEx(hPopupMenu, TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_VERTICAL, rc.left, rc.bottom, hWnd, &tpm);
DestroyMenu(hMenuLoaded);
}
break;
}
As for the menu IDR_MENU1, you can point it to a menu you have defined. Or, if you want a placeholder thing, put something like this in your .rc file:
IDR_MENU1 MENU
BEGIN
POPUP "TEST"
BEGIN
MENUITEM "Option 1", ID_TEST_OPTION1
MENUITEM "Option 2", ID_TEST_OPTION2
END
END
That goes along with these defines in the Resources.h coupled to the .rc file:
#define IDR_MENU1 132
#define ID_TEST_OPTION1 32777
#define ID_TEST_OPTION2 32778
Build, and you get this:
In animated form:
The dropdown works. Success!
It so happens if you re-try Attempt 2, TBSTYLE_EX_DRAWDDARROWS with a pop up menu, then it'll provide visual arrow-is-pressed feedback where it didn't before. See:
This is because BTNS_DROPDOWN, TBSTYLE_EX_DRAWDDARROWS, and BTNS_WHOLEDROPDOWN follow a common principle: anything that appeared unresponsive with no pop up menu attached is responsive once a menu is attached.
This system was not super well explained elsewhere, so maybe this will help you.
Answer: mostly, yes. Explanation below.
Part 1: Yes or No
Remember GDI? Say you're using GDI and Win32, and you want to draw some graphics to a window. What to do. You read the documentation and see what looks like the most obvious thing: "SetPixel". Sounds good. Takes an x and y and a color. What more could you want? Super easy to use.
But then, you see a bunch of cautionary notes. "It's slow." "It's inefficient." "Don't do it."
Don't do it?
Well. All these cautionary notes you see are from days of yore:
- Computers are faster now. Both CPU and GPU. Take an early CS algorithms class, experiment with solutions. Youâll see sometimes the biggest optimization you can do is to get a faster computer.
- An earlier Windows graphics driver model. Say, XPDM not WDDM. WDDM means all hardware-accelerated graphics communicate through a âDirect3D-centric driver modelâ, and yes that includes GDI. Changes in driver model can impose changes in performance characteristics.
- Different Windows presentation model. That's something this API is set up to negotiate with, so it could affect performance too. Nowadays you're probably using DWM. DWM was introduced with Windows Vista.
The date stamps give you skepticism. Is that old advice still true?
As a personal aside, I've definitely seen performance advice from people on dev forums that is super outdated and people get mis-led into following it anyway. For example for writing C++ code, to "manually turn your giant switch case into a jump table". I see jump tables in my generated code after compilation... The advice was outdated because of how much compilers have improved. I've noticed a tendency to trust performance advice "just in case", without testing to see if it matters.
Let us run some tests to see if SetPixel is still slow.
I wrote a benchmark program to compare
- SetPixel, plotting each pixel of a window sequentially one by one, against
- SetDIBits, where all pixels of a window are set from memory at once.
In each case the target is a top-level window, comparing like sizes. Each mode effectively clears the window. The window is cleared to a different color each time, so you have some confidence itâs actually working.
Timing uses good old QPC. For the sizes of timespans involved, it was not necessary to get something more accurate. The timed interval includes all the GDI commands needed to see the clear on the target, so for SetDIBits that includes one extra BitBlt from a memory bitmap to the target to keep things fair.
The source code of this benchmark is here.
Here are the results
Width | Height | Pixel Count | SetPixel | SetDIBits |
1000 | 1000 | 1000000 | 4.96194 | 0.0048658 |
950 | 950 | 902500 | 4.7488 | 0.0042761 |
900 | 900 | 810000 | 4.22436 | 0.0038637 |
850 | 850 | 722500 | 3.71547 | 0.0034435 |
800 | 800 | 640000 | 3.34327 | 0.0030824 |
750 | 750 | 562500 | 2.92991 | 0.0026711 |
700 | 700 | 490000 | 2.56865 | 0.0023415 |
650 | 650 | 422500 | 2.21742 | 0.0022196 |
600 | 600 | 360000 | 1.83416 | 0.0017374 |
550 | 550 | 302500 | 1.57133 | 0.0015125 |
500 | 500 | 250000 | 1.29894 | 0.001311 |
450 | 450 | 202500 | 1.05838 | 0.0010062 |
400 | 400 | 160000 | 0.826351 | 0.0009907 |
350 | 350 | 122500 | 0.641522 | 0.0006527 |
300 | 300 | 90000 | 0.467687 | 0.0004657 |
250 | 250 | 62500 | 0.327808 | 0.0003364 |
200 | 200 | 40000 | 0.21523 | 0.0002422 |
150 | 150 | 22500 | 0.118702 | 0.0001515 |
100 | 100 | 10000 | 0.0542065 | 9.37E-05 |
75 | 75 | 5625 | 0.0315026 | 0.000122 |
50 | 50 | 2500 | 0.0143235 | 6.17E-05 |
Viewed as a graph:
Conclusion: yeah, SetDIBits is still way faster than SetPixel in general, in all cases.
For small numbers of pixels, the difference doesn't matter as much. For setting lots of pixels, the difference is a lot.
I tested this on an Intel Core i7-10700K, with {NVIDIA GeForce 1070 and WARP} with all similar results.
So the old advice is still true. Don't use SetPixel, especially if youâre setting a lot of pixels. Use something else like SetDIBits instead.
Part 2: Why
My benchmark told me that itâs still slow, but the next question I had was âwhyâ. I took a closer look and did some more thinking about why it could be.
It's not one reason. There's multiple reasons.
1. There's no DDI for SetPixel.
You can take a look through the public documentation for display devices interfaces, and see whatâs there. Or, take a stab at it and use the Windows Driver Kit and the provided documentation to write a display driver yourself. Youâll see whatâs there. Youâll see various things. Youâll see various blit-related functions in winddi.h. For example, DrvBitBlt:
BOOL DrvBitBlt(
[in, out] SURFOBJ *psoTrg,
[in, optional] SURFOBJ *psoSrc,
[in, optional] SURFOBJ *psoMask,
[in] CLIPOBJ *pco,
[in, optional] XLATEOBJ *pxlo,
[in] RECTL *prclTrg,
[in, optional] POINTL *pptlSrc,
[in, optional] POINTL *pptlMask,
[in, optional] BRUSHOBJ *pbo,
[in, optional] POINTL *pptlBrush,
[in] ROP4 rop4
);
That said, you may also notice whatâs not there. In particular, thereâs no DDI for SetPixel. Nothing simple like that, which takes an x, y, and color. Itâs important to relate this to the diagrams on the âGraphics APIs in Windowsâ article, which shows that GDI talks to the driver for both XPDM and WDDM. It shows that every time you call SetPixel, then what the driver sees is actually far richer than that. It would get told about a brush, a mask, a clip. Itâs easy to imagine a cost to formulating all of those, since they you donât specify them at the API level and the API is structured so they can be arbitrary.
2. Cost of talking to the presentation model
Thereâs a maybe-interesting experiment you can do. Write a Win32 application with your usual WM_PAINT handler. Run the application. Hide the window behind other windows, then reveal it once again. Does your paint handler get called? To reveal the newly-revealed area? No, normally it doesnât.
So what that must logically mean is that Windows kept some kind of buffer, or copy of your window contents somewhere. Seems like a good idea if you think about it. Would you really want moving windows around to be executing everyoneâs paint handlers all the time, including yours? Probably not. Itâs the good old perf-memory tradeoff in favor of perf, and it seems worth it.
Given that youâre drawing to an intermediate buffer, then thereâs still an extra step needed in copying this intermediate buffer to the final target. Which parts should be copied, and when? It seems wasteful to be copying everything all the time. To know what needs to get re-copied, logically there has to be some notion of an âupdateâ region, or a âdirtyâ region.
If youâre an application, you might even want to aggressively optimize and only paint the update region. Can you do that? At least at one point, yes you could. The update region gets communicated to the application through WM_PAINT- see the article âRedrawing in the Update Regionâ. Thereâs a code example of clipping accordingly. Now, when I tried things out in my application I noticed that PAINTSTRUCT::rcPaint is always the full window, even in response to a small region invalidated with InvalidateRect, but the idea is at least formalized in the API there.
Still, thereâs a cost to dealing with update regions. If you change one pixel, that one-pixel area is part of the update region. Change the pixel next to it, the region needs to be updated again. And so on. Could we have gotten away with having a bigger, coarser update region? Maybe. You just never know that at the time.
If you had some way of pre-declaring which regions of the window youâre going to change, (e.g., through a different API like BitBlt), then you wouldnât have this problem.
3. Advancements in the presentation model help, but not enough
In Windows, there is DWM- the Desktop Window Manager. This went out with Windows Vista and brought about all kinds of performance improvements and opportunity for visual enhancements.
Like the documentation says, DWM makes it possible to remove level of indirection (copying) when drawing contents of Windows.
But it doesnât negate the fact that there still is tracking of update regions, and all the costs associated with that.
4. Advancements in driver model help, but not enough
DWM and Direct3D, as components that talk to the driver through the 3D stack, have a notion of âframesâ and a particular time at which work is âflushedâ to the GPU device.
By contrast, GDI doesnât have the concept of âframesâ or flushing anything. Closest thing would be the release of the GDI device context, but thatâs not strictly treated as a sign to flush. You can see it yourself in how your Win32 GDI applications are structured. You draw in response to WM_PAINT. Yes there is EndPaint, but EndPaint doesnât flush your stuff. Try it if you want- comment out EndPaint. I tried it just to check and everything still works without it.
Since there isnât a strict notion of âflushing to the deviceâ, SetPixel pixels have to be dispatched basically immediately rather than batched up.
5. 3D acceleration helps, but not enough
Nowadays, GDI blits are indeed 3D accelerated.
- They were,
- then they werenât for Vista,
- then they were again.
I noticed this firsthand, too. Very lazy way to check- in the âPerformanceâ tab in Task manager when I was testing my application, I saw little blips in the 3D queue. These coincided with activity in the SetPixel micro-benchmark.
Again, very lazy check. Good to know we are still accelerating these 2D blits, even as the graphics stack has advanced to a point of making 3D graphics a first-class citizen. Hardware acceleration is great for a lot of things, like copying large amounts of memory around at once, applying compression or decompression, or manipulating data in other ways that lend itself to data-parallelism.
Unfortunately, literally none of that helps this scenario. Parallelism? How? At a given time, the driver doesnât know if youâre done plotting or what you will plot next or where. And it canât buffer up the operations and execute them together, because it, like Windows, doesnât know when youâre done. Maybe, it could use some heuristic.
But that brings this to the punchline: even if the driver had psychic powers, it could see into the future and know exactly what the application is going to do and did an absolutely perfect job of coalescing neighboring blits together, it doesnât negate any of the above costs, especially 1. and 2.
Conclusion
Even in the current year, donât use SetPixel for more than a handful of pixels. Thereâs reasons to believe the sources of the bottlenecks to have changed over 30 years, yet even still the result is the same. Itâs slow and the old advice is still true.
Epilogue: some fantasy world
This post was about how things are. But, what could be? What would it take for SetPixel not to be slow? The most tempting way to think about this is to flatten or punch holes through the software stack. That works, even if it feels like a cop-out.
Suppose you have a Win32 program with a checkbox. You just added it. No changes to message handler.
You click the box. What happens?
Answer: the box appears checked. Click it again, the box becomes un-checked. Riveting
Now suppose you have a Win32 menu item that is checkable. Again, added with no changes to message handler.
You click the item. What happens?
Answer: Nothing. It stays checked, if it was initialized that way. Unchecked, if it was initialized that way.
In both these cases, the item was added straightforwardly through resource script with no changes to the message handler.
Why are these two things different?
Explanation: apparently it falls out of broader Win32 design. The automatic-ness that the normal checkbox has, requires features to control it. For example, those checkboxes can be grouped into radio button groups with WS_GROUP. To add that same richness to menu items, too? You could, but it'd be an increase in complexity, and the benefit would need to be clearly justified. There'd need to be an "MF_GROUP" and all the API glue that comes cascading with it. Also, automatic checkbox-ness brings with it the chance of encountering errors, and errors tends to mean modal dialogs. It's okay to launch dialogs during normal window interactions, that happens all the time. But from a menu item? It would be really jarring and unexpected. Going more broadly than that it runs the risk of encouraging of bad habits: you might use the hypothetical "MF_GROUP" glue to do something strange and expensive, and that's not what menu items are for. Since it's not clear the benefit is justified, you're on your own for check state.
In case you were wondering, I'm not really trying to "sell" this inconsistency to you. I was just as surprised as you were. I am trying to explain it based on sources though. It's not random.
Something related- this docpage, "Using Menus - Simulating Check Boxes in a Menu". The sample code leaves you asking some broader questions of "why am I doing all this?"
Raymond Chen article fills in blanks: "Why can't you use the space bar to select check box and radio button elements from a menu?"
The design is also conveyed through Petzold's "Programming Windows, 5th edition" page 445 in the code sample "MENUDEMO.C". The message handler goes like
case IDM_BKGND_WHITE: // Note: Logic below
case IDM_BKGND_LTGRAY: // assumes that IDM_WHITE
case IDM_BKGND_GRAY: // through IDM_BLACK are
case IDM_BKGND_DKGRAY: // consecutive numbers in
case IDM_BKGND_BLACK: // the order shown here.
CheckMenuItem (hMenu, iSelection, MF_UNCHECKED) ;
iSelection = LOWORD (wParam) ;
CheckMenuItem (hMenu, iSelection, MF_CHECKED) ;
The general trend in this book is to leverage automatic facilities in the Win32 API wherever it makes sense to do. But here, the radio button-ness is all programmatic for checkable menus.