views:

381

answers:

4

I want to create an array of 256 colored buttons with the owner draw extended style to a dialog box created with the visual studio dialog design tool. I added a loop to the WM_INITDIALOG message handler in the dialog procedure to do this:

for (i=0; i<=255; i++)

{

int xp, yp;
HWND status;

xp = rect_pos.left+16*(i%16);
yp = rect_pos.top+16*(i>>4);
status = CreateWindow (
 TEXT("button"),
 "\0",
 WS_CHILD|WS_VISIBLE|BS_OWNERDRAW|BS_PUSHBUTTON,
 xp,
 yp,
 15,
 15,
 hDlg,
 (HMENU) 5000+i,  // id used to report events
 hInst,
 NULL
);

if (status == NULL)
 xp =7;

}

I added a message handler for the WM_CTLCOLORBTN message.

case WM_CTLCOLORBTN:
{ 
 int  zz;

 zz = GetWindowLong ((HWND) lParam, GWL_ID); // window identifier
 zz -= 5000;
 if ((zz >= 0) && (zz <= 255))
 {
  HBRUSH BS;

  SetTextColor ((HDC) wParam, Collector.Color);
  SetBkColor ((HDC) wParam,   Collector.Color); 

  return ((LRESULT) Collector.Brush);       

 }
 break;
}

It more or less works but only the first 64 buttons are displayed. I intend to use a different brush to color each button but for debug puproses, I substituted a single well defined brush. I've debugged the code and satisfied myself the x/y coordinates are proper for each button and that the ID provided in the hMenu createwindow call is proper. I watched all 256 buttons get colored in the WM_CTLCOLORBTN handler. I included a check to make sure the createwindow call does not return failure (NULL). I can get either 4 rows of 16 buttons or 4 columns of 16 buttons by interchanging the x/y parameters on the createwindow call.

If I remove the BS_OWNERDRAW bit from the createwindow call, all 256 buttons are drawn.

It's as if there a limit of 64 buttons with BS_OWNERDRAW :-(

Any help would be greatly appreciated!

TIA, Mike

+1  A: 

Are you handling the WM_DRAWITEM message in conjunction with the BS_OWNERDRAW style?

In your case, it seems surprising to me that any buttons are displayed while using the BS_OWNERDRAW style, while BS_PUSHBUTTON is set.

As mentioned in the following link to the documentation for BS_OWNERDRAW, you need to handle WM_DRAWITEM and avoid specifying any other BS_ button styles.

Button Styles from MSDN

Also curious is that the WM_CTLCOLORBUTTON message may be received and then ignored for buttons containing the BS_PUSHBUTTON style. Check out the following link for the documentation on that window message.

WM_CTLCOLORBUTTON from MSDN

From what I can see in your code snippet, most likely you will want to do the following:

  1. Set BS_OWNERDRAW when creating the child buttons.
  2. Handle WM_DRAWITEM on the dialog and draw the button in its correct state. Note that you don't have to handle WM_CTLCOLORBUTTON, just use the Brushes and Fonts and modify the DC as you wish inside your WM_DRAWITEM handler.

Also, depending on your application, you might benefit from making your own window class to represent a grid of buttons on your own, and just drawing the items to taste. This is preferable if you're just displaying internal state and not really looking for the user to manage or interact with a grid of buttons.

meklarian
A: 

Meklarian:

THANK YOU!

I returned false from the WM_CTLCOLORBUTTON and added the following

case WM_DRAWITEM:
 {
  LPDRAWITEMSTRUCT lpDrawItem;
  HBRUSH BS;
  HPEN    PN;

  lpDrawItem = (LPDRAWITEMSTRUCT) lParam;

  if (lpDrawItem->CtlType != ODT_BUTTON)
   return FALSE;

  if (lpDrawItem->itemAction != ODA_DRAWENTIRE)
   return TRUE;

  PN = (HPEN) SelectObject(lpDrawItem->hDC, GetStockObject(NULL_PEN));
  BS = (HBRUSH) SelectObject(lpDrawItem->hDC, Collector.Brush);
  Rectangle (lpDrawItem->hDC, 0, 0, 15, 15);
  SelectObject(lpDrawItem->hDC, PN);
  SelectObject(lpDrawItem->hDC, BS);

  return true;


 }

and I now all the buttons are drawn. :-)

BUT, now I have no idea how to detect if the button has been clicked! I added some code and see I get a Action focus change with State != ODS_FOCUS but don't know if that's how to detect the click or what to do to process it. :-(

As you have probably noticed, I'm quite a novice at this. Somethings still bothering me are

1) Do I have to any more in the WM_DRAWITEM code? 2) I still left the createwindow call with the BS_BUTTON and BS_OWNERDRAW. Am I supposed to remove the BS_BUTTON also? If so, how would it know its a button? 3) Your last comment about window class is beyond me. I guess it means I could create almost anything anywhere even in a dialog box. Is that correct?

BTW, I did some other experiments when I had the WM_CTLCOLORBUTTON code in place and found that any buttons with owner draw among the first 64 I created worked fine but any beyond that would not work. For example, if I make buttons 5000 - 5048 non owner draw, only the first 16 owner draw buttons worked. If I tried to create a single owner draw button after 5063, it would not work.

Thanks, Mike

You're obviously new to StackOverflow, too. Give Meklarian an upvote and click the check next to his answer to accept it.
Mark Ransom
When a button is clicked, you'll get a WM_COMMAND message with BN_CLICKED in the high part of wParam.
Mark Ransom
Drawing all the different button states in an owner-draw button is difficult to get perfectly right. It deserves its own question.
Mark Ransom
Hi Mike. 1. You should call DefWindowProc() when you aren't going to perform drawing operations with WM_DRAWITEM. 2. The documentation suggests that you should not use any other BS_ styles in conjunction with BS_OWNERDRAW. 3. A window class is basically a registered handler for a given type of window. In your CreateWindow() statement, the 2nd argument => TEXT("button") is the window class name. And finally, as Mark Ransom states- it is a lot of work to correctly draw all of the states for a button.
meklarian
A: 

Well, much progress. Some of my problem was not appreciating the precedence of the cast operator. Thank you all.

First on accepting the answer, is that the funny looking X to the left of the name?

Things are almost as I want them with one exception. When I click a normal push button, the button changes and gets and keep the input focus. I wanted my buttons to act similarly so I added code to the WM_DRAWITEM to draw the rectangle with either a null pen or a black pen if the button is either selected or has the focus. When I press one of the owner drawn buttons the focus is killed wherever it was and the button is drawn with the black border as long as I hold the mouse button. As soon as I release the mouse button, the black border (my code re-draws the button with the null pen border?) is removed and the focus shifts to the default OK button in the dialog.

I want my owner drawn button to keep the focus.

As far as the DefWindowProc(), I was assumming that the system applied default processing whenever I returned false from the message loop.

Here are the code snippets. Watch for some experiments // disabled.

In the WM_INITDIALOG handling

  for (i=0; i<=255; i++)
  {
   int xp, yp;
   HWND status;
   HBRUSH BS;

   xp = rect_pos.left+16*(i&0x0F);
   yp = rect_pos.top+16*(i>>4);

   if ((i > 16))
   status = CreateWindow 
    (
    TEXT("button"),
    "\0",
    WS_CHILD|WS_VISIBLE|BS_OWNERDRAW,
    xp,
    yp,
    15,
    15,
    hDlg,
    (HMENU) (5000+i),      // id used to report events
    hInst,
    NULL
    );
   else
   status = CreateWindow 
    (
    TEXT("button"),
    "\0",
    WS_CHILD|WS_VISIBLE|BS_PUSHBUTTON,
    xp,
    yp,
    15,
    15,
    hDlg,
    (HMENU) (5000+i),      // id used to report events
    hInst,
    NULL
    );


   if (status == NULL)
    xp =7;   // just so there's someplace to put a breakpoint



  }

In the WM_DRAWITEM handler

case WM_DRAWITEM:
 {
  LPDRAWITEMSTRUCT lpDrawItem;
  HBRUSH BS;
  HPEN    PN;

  lpDrawItem = (LPDRAWITEMSTRUCT) lParam;

  if (lpDrawItem->CtlType != ODT_BUTTON)
   return FALSE;

// if (lpDrawItem->itemAction & ODA_DRAWENTIRE)

  {
   int sz=15;
   int zz;

   zz =lpDrawItem->itemState;

   if (lpDrawItem->itemState & (ODS_SELECTED || ODS_FOCUS))
   {
    sz = 14;
    PN = (HPEN) SelectObject(lpDrawItem->hDC, GetStockObject(BLACK_PEN));
   }
   else
    PN = (HPEN) SelectObject(lpDrawItem->hDC, GetStockObject(NULL_PEN));
   BS = (HBRUSH) SelectObject(lpDrawItem->hDC, Collector.Brush);
   Rectangle (lpDrawItem->hDC, 0, 0, sz, sz);
   SelectObject(lpDrawItem->hDC, PN);
   SelectObject(lpDrawItem->hDC, BS);

   return true;
  }

 }

And in the WM_COMMAND handler

    if (HIWORD(wParam) == BN_CLICKED) 
    { 
  if ((LOWORD(wParam) >= 5000) && (LOWORD(wParam) <=5255))
        {

// SetFocus (GetDlgItem(hDlg,LOWORD(wParam)));

   return true;

   break; 
        } 
     }
Mike D
Hmmm a tricky situation. I presume you aren't calling SetFocus() anywhere when the mouse button is released. Try handling WM_KILLFOCUS on your pushbuttons and setting focus back to the pushbutton when the wParam is NULL.http://msdn.microsoft.com/en-us/library/ms646282(VS.85).aspxAlso, since you will have a lot of buttons that need to be handled, try specifying a subclass at creation time to handle the WM_KILLFOCUS message.http://msdn.microsoft.com/en-us/library/ms633570(VS.85).aspx#subclassing_window
meklarian
I tried adding WM_KILLFOCUS but get no msgs with NULL wParam either with or without the notify flag. I have not tried the subclass business yet. I tried one interesting experiment adding the TAB STOPs to the buttons. I can tab through the buttons and push them with the space bar but they do not get highlighted. itemstate is 0x100 in the draw code. 0x100 is not among the items defined for itemstate.
Mike D
A: 

Thanks to all who gave advice and help. I now have this working to my satisfaction. I have successfully colored the buttons and surrounded them with a black outline if selected or if they have the input focus. I'll add some code snippets below to show the final state of things but here is synopsis. First, I believe there is some legacy code in my system which makes owner drawn buttons respond to the WM_CTLCOLORBTN for the first child 64 buttons created. Second, I believe the only thing one needs to do is create the buttons, respond properly to the WM_DRAWITEM and WM_COMMAND/BN_CLICKED messages.

Here are the code snippets from my dialog box handler.

In the WM_INITDIALOG code -- create the buttons

  for (i=0; i<=255; i++)
  {
   int xp, yp;
   HWND status;

   xp = rect_pos.left+16*(i&0x0F);
   yp = rect_pos.top+16*(i>>4);

   status = CreateWindow 
    (
    TEXT("button"),
    "\0",
    WS_CHILD|WS_VISIBLE|WS_TABSTOP|BS_OWNERDRAW,
    xp,
    yp,
    15,
    15,
    hDlg,
    (HMENU) (5000+i),      // id used to report events
    hInst,
    NULL
    );

   if (status == NULL)
    xp =7;


   SetFocus (status);

  }

Respond to the WM_DRAWITEM message

case WM_DRAWITEM:       // Owner drawn botton
 {
  LPDRAWITEMSTRUCT lpDrawItem;
  HBRUSH BS, BS_Old;
  HPEN    PN_Old;
  int  sz=15;
  int  cntl;

  cntl = LOWORD (wParam) - 5000;

  lpDrawItem = (LPDRAWITEMSTRUCT) lParam;

  if (lpDrawItem->CtlType != ODT_BUTTON)
   return FALSE;

  BS = CreateSolidBrush (ColorRef[cntl]);

  if (lpDrawItem->itemState & (ODS_SELECTED | ODS_FOCUS))
  {
   sz = 14;
   PN_Old = (HPEN) SelectObject(lpDrawItem->hDC, GetStockObject(BLACK_PEN));
  }
  else
   PN_Old = (HPEN) SelectObject(lpDrawItem->hDC, GetStockObject(NULL_PEN));

  BS_Old = (HBRUSH) SelectObject(lpDrawItem->hDC, BS);
  Rectangle (lpDrawItem->hDC, 0, 0, sz, sz);
  SelectObject(lpDrawItem->hDC, PN_Old);
  SelectObject(lpDrawItem->hDC, BS_Old);
  DeleteObject (BS);

  return true;
 }

and finally in the WM_COMMAND code

    if (HIWORD(wParam) == BN_CLICKED) 
    { 
  if ((LOWORD(wParam) >= 5000) && (LOWORD(wParam) <=5255))
        {

   Color[0] = ColorRef[LOWORD(wParam)-5000] & 0xFF;
   Color[1] = (ColorRef[LOWORD(wParam)-5000] >> 16) & 0xFF;
   Color[2] = (ColorRef[LOWORD(wParam)-5000] >> 8 ) & 0xFF;

   InvalidateRect (hDlg, NULL, TRUE);

   goto Set_Color;
        } 
     }
Mike D