|
I used some of your FooButton code to add a check-button group to my button class. I put some traces in DrawItem and noticed that it was being called up to 4 times for a check-button-group-style button.
I may be wrong, but at first blush it looks like the Invalidate() call in OnLButtonUp is superfluous, at least for check-buttons, because Invalidate() is already being called in check().
Take care,
Dan.
|
|
|
|
|
It's possible - I'll look into it. Thanks!
/ravi
My new year's resolution: 2048 x 1536
Home | Articles | Freeware | Music
ravib@ravib.com
|
|
|
|
|
Let me just say that this is an excellent class and I the quality of your code is top notch. Specifically, the commenting--especially for a class that you would expect people to download and try to learn--is second to none and perfect for this kind of project. I work with some programmers who seem like they have taken a vow of commenting silence, and I'm going to show them your comments as an example. Thanks very much for posting this.
(I was actually going to post a question about how the group-type buttons work, but I think I just figured it out. Thanks again.)
|
|
|
|
|
Thanks for your kind words, Dan.
I try to follow Ravi's Really Simple Rule for Comments, which is:- Remove all code from a method, except the start and end of blocks and your comments.
- Replace all conditional expressions with
(...) .
- If the remaining psuedo-code isn't obvious, you need to add more (or better) comments.
Here's a bad example of commenting:
<code>
Iterator itFoo = fooCollection.getIterator();
while (itFoo.hasMore()) {
Foo* pFoo = itFoo.getNext();
<code>
if (pFoo->isBad())
doSomethingTo (pFoo);
}
which translates to:
while (...) {
}
which doesn't make much sense. Here's the same code with better commenting:
<code>
Iterator itFoo = fooCollection.getIterator();
while (itFoo.hasMore()) {
<code>
Foo* pFoo = itFoo.getNext();
if (pFoo->isBad())
doSomethingTo (pFoo);
}
which translates to:
while (...) {
}
which leaves very little room for doubt.
/ravi
My new year's resolution: 2048 x 1536
Home | Articles | Freeware | Music
ravib@ravib.com
|
|
|
|
|
I was using this as an "Exit" button and ran into an ASSERT in this code. Here's the fix.
void FooButton::OnLButtonUp (UINT nFlags, CPoint point)<br />
{<br />
<br />
CButton::OnLButtonUp (nFlags, point);<br />
<br />
if (!IsWindow(GetSafeHwnd()))<br />
return;<br />
<br />
m_bMultiClicked = false;<br />
Invalidate();<br />
<br />
}
Overall, this is a great class. Good work!
Peteman_R
Boise, Idaho
|
|
|
|
|
Thanks! I'll be updating the article shortly.
/ravi
My new year's resolution: 2048 x 1536
Home | Articles | Freeware | Music
ravib@ravib.com
|
|
|
|
|
Hi,
Thanks for your efforts and this great control!
I tried to use bitmaps where I defined the color RGB( 0, 128, 128) for transparency. If the button will be disabled, it loses its transparency and the background changes to dark grey.
Recreating the bitmaps using the default transparent color RGB(255,0,255) solves the problem, but anyway, I think that this behaviour is a bug.
Michael
|
|
|
|
|
|
Hi. First of all, I'd like to say thank you for contributing your class! It's the only one I could find that had an easy way to hang a menu off the right side, and have an icon.
I was running boundschecker http://www.compuware.com/products/devpartner/bounds.htm[^]
on our application (we run it on all our applications - highly recommended to everyone), and it was reporting some errors in FooButton::DisabledBlt
1) line 1048 --> SelectObject (bwDC, hBitmapBW)
This bitmap is still selected into bwDC when bwDC is deleted. The return value from the the SelectObject call should be replaced before the next } (i.e. between the current lines 1069 and 1070)
2) from the online help for CreateDIBSection:
"Windows NT/ 2000: You need to guarantee that the GDI subsystem has completed any drawing to a bitmap created by CreateDIBSection before you draw to the bitmap yourself. Access to the bitmap must be synchronized. Do this by calling the GdiFlush function. This applies to any use of the pointer to the bitmap's bit values, including passing the pointer in calls to functions such as SetDIBits"
Perhaps a call to GDIFlush should be added?
Warren
|
|
|
|
|
Thanks for your comments, Warren. I stole the DisabledBlt() code from elsewhere. Will update the article when I get back (I'm currently travelling). Thanks again!
/ravi
My new year's resolution: 2048 x 1536
Home | Articles | Freeware | Music
ravib@ravib.com
|
|
|
|
|
I accidentally created a FooButton of a static control. To my surprise the code worked. However, the behavior wasn’t what I expected . The following code in PreSubclassWindow will make sure the control is a button:
#ifdef _DEBUG
TCHAR buffer[255];
GetClassName(m_hWnd, buffer, sizeof(buffer)/sizeof(TCHAR));
ASSERT( CString(buffer) == _T("Button"));
#endif
|
|
|
|
|
|
If the button is a hyperlink and the mouse pointer is over it when the button is destroyed (like when the user press escape) DestroyCursor fails in the destructor.
My suggestion is to have this code in OnSetCursor:
BOOL FooButton::OnSetCursor (CWnd* , UINT , UINT )
{
if(m_bHyperlink)
{
HCURSOR cursor = LoadCursor(NULL, MAKEINTRESOURCE(32649) );
if(cursor)
::SetCursor( cursor );
}
return FALSE;
}
This will try to load the standard hand cursor instead of using the cursor in "winhlp32.exe". When doing this I don't think it's necessary to destroy the cursor (but I'm not totally sure). The good thing with this is that the user could decide which cursor to use. The bad thing is that in some operating systems will the arrow be used instead. However, this will only happens in Windows 95 and NT, which I think acceptable. The hand cursor will be used in Windows 98.
I have tried this code in Window 95, 98 and NT and it works as expected.
|
|
|
|
|
Thanks, as always! Will update the article soon.
/ravi
My new year's resolution: 2048 x 1536
Home | Articles | Freeware | Music
ravib@ravib.com
|
|
|
|
|
I have successfully added support for Windows XP themes to FooButton. It wasn't so hard that I expected it to be . The code I will show here will compile in VC6, and programs should run without problems in other operating systems than Window XP. I have tried on Win98 and had no problems at all. "FOO_USEXPTHEMES" must be defined, otherwise will the code be compiled without support for themes.
The first thing to do is to have some kind of theme manager that loads "uxtheme.dll". There is such class in the article "XP Style CBitmapButton (CHoverBitmapButton)". However, I found some bugs in it but have posted a bugfix in the forum. Apply it, and copy the files "theme.cpp", "theme.h" and "ThemeLib.h" to an project that using FooButton. I will assume that the project is the demo project on this article.
Add the following in "FooButtonDemoDlg.cpp" (the main window):
#ifdef FOO_USEXPTHEMES
#include "theme.h"
CTheme gThemeManager;
#endif
I will use the global variable "gThemeManager". I would prefer with another solution, but this was the easiest I come up to . Anyway, init this in CFooButtonDemoDlg::OnInitDialog() by calling:
#ifdef FOO_USEXPTHEMES
gThemeManager.Init(m_hWnd);
#endif
It is nice if the theme could be changed when the program is running. Add this member function in the class CFooButtonDemoDlg:
#ifdef FOO_USEXPTHEMES
LRESULT CFooButtonDemoDlg::OnThemeChanged(WPARAM, LPARAM)
{
gThemeManager.ThemeChanged(m_hWnd);
return 1;
}
#endif
Add this the message map:
#ifdef FOO_USEXPTHEMES
ON_MESSAGE(0x031A, OnThemeChanged)
#endif
Now it's only some changes to do in the FooButton class . Add the following in the header:
#ifdef FOO_USEXPTHEMES
protected:
CRect GetInternalRect(LPDRAWITEMSTRUCT lpDrawItemStruct);
int GetXPMode(UINT state) const;
#endif
Add this in the file foobutton.cpp. This will give the class access to the global theme manager:
#ifdef FOO_USEXPTHEMES
#include "theme.h"
extern CTheme gThemeManager;
#endif
I declared two new member functions. The code for these is:
#ifdef FOO_USEXPTHEMES
CRect FooButton::GetInternalRect(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CRect rectButton;
GetClientRect (&rectButton);
if(gThemeManager.m_bXPTheme)
{
CRect newRectButton = rectButton;
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
gThemeManager.GetThemeBackgroundContentRect( *pDC,
BP_PUSHBUTTON,
GetXPMode( lpDrawItemStruct->itemState ),
&rectButton,
&newRectButton
);
rectButton = newRectButton;
}
return rectButton;
}
int FooButton::GetXPMode(UINT state) const
{
BOOL bPressed = state & ODS_SELECTED;
BOOL bGotFocus = state & ODS_FOCUS;
BOOL bDisabled = state & ODS_DISABLED;
int iMode=0;
if (m_bTracking)
{
if (bPressed)
{
iMode = PBS_PRESSED;
}
else
{
iMode = PBS_HOT;
}
}
else
{
if (!bGotFocus && !bDisabled)
{
iMode = PBS_NORMAL;
}
if (bDisabled)
{
iMode = PBS_DISABLED;
}
if(IsDefault() && !bDisabled)
iMode |= PBS_DEFAULTED;
}
return iMode;
}
#endif
Button should always be hot tracked if themes are supported. Replace FooButton::OnMouseMove() with the following:
void FooButton::OnMouseMove(UINT nFlags, CPoint point)
{
#ifndef FOO_USEXPTHEMES
if (m_bHot)
#else
{
if (!m_bTracking)
{
TRACKMOUSEEVENT tme;
tme.cbSize = sizeof(tme);
tme.hwndTrack = m_hWnd;
tme.dwFlags = TME_LEAVE;
tme.dwHoverTime = HOVER_DEFAULT;
m_bTracking = _TrackMouseEvent(&tme);
Invalidate();
}
}
#endif
COddButton::OnMouseMove(nFlags, point);
}
Replace FooButton::drawFrame with this:
void FooButton::drawFrame
(CDC* pDC,
LPDRAWITEMSTRUCT lpDrawItemStruct)
{
ASSERT (pDC != NULL);
ASSERT (lpDrawItemStruct != NULL);
if (!m_bStatic && !m_bHyperlink) {
if (m_bHot && !m_bChecked)
drawHotButtonFrame (lpDrawItemStruct);
else
{
#ifdef FOO_USEXPTHEMES
if(gThemeManager.m_bXPTheme)
{
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
int iMode = GetXPMode( lpDrawItemStruct->itemState );
gThemeManager.DrawThemeBackground( *pDC,
&(lpDrawItemStruct->rcItem),
BP_PUSHBUTTON,
iMode);
}
else
#endif
{
UINT uFrameCtrl = DFCS_BUTTONPUSH;
if (m_bChecked)
uFrameCtrl |= DFCS_CHECKED;
else
if (((lpDrawItemStruct->itemState & ODS_SELECTED) == ODS_SELECTED) && !m_bMulti)
uFrameCtrl |= DFCS_PUSHED;
if ((lpDrawItemStruct->itemState & ODS_DISABLED) == ODS_DISABLED)
uFrameCtrl |= DFCS_INACTIVE;
pDC->DrawFrameControl (&(lpDrawItemStruct->rcItem), DFC_BUTTON , uFrameCtrl);
if (m_bMulti && m_bMultiClicked)
drawMultiDropDownRegion (pDC, lpDrawItemStruct);
}
}
}
}
In the function FooButton::drawCaption() I was forced to remove code that calculates the height of the text (couldn't figure out how to do this with themes). However, sense the text a centered vertically I don't think this is necessary. The new function looks like this:
void FooButton::drawCaption
(CDC* pDC,
LPDRAWITEMSTRUCT lpDrawItemStruct,
int nLeftEdge,
int& nRightEdge)
{
nRightEdge = 0;
if (!m_bText)
return;
if (!m_bCenter)
nLeftEdge += C_BitmapBorder_X;
CRect rectButton;
GetClientRect (&rectButton);
nRightEdge = rectButton.right;
if (m_bDropDown)
nRightEdge -= 6;
else
if (m_bMulti) {
nRightEdge -= 12;
}
int nAvailableWidth = nRightEdge - nLeftEdge;
if (nAvailableWidth <= 0)
return;
CString strCaption;
GetWindowText (strCaption);
DWORD dwFormat = DT_SINGLELINE | DT_VCENTER | DT_END_ELLIPSIS;
dwFormat |= (m_bCenter ? DT_CENTER : DT_LEFT);
if (m_bMultiLine)
dwFormat = DT_WORDBREAK | DT_LEFT | DT_WORD_ELLIPSIS | DT_VCENTER;
CRect rectCaption;
GetClientRect (&rectCaption);
#ifdef FOO_USEXPTHEMES
rectCaption = GetInternalRect(lpDrawItemStruct);
#endif
strCaption.ReleaseBuffer();
int nWidth = rectCaption.Width();
rectCaption.left = nLeftEdge;
rectCaption.right = nRightEdge;
if (m_bCenter && (nAvailableWidth > nWidth)) {
int nOffsetX = (nAvailableWidth - nWidth) / 2;
rectCaption.left += nOffsetX;
rectCaption.right -= nOffsetX;
}
if ((m_type != FooButton::Type::checkBox) && (m_type != FooButton::Type::radio))
if (!m_bStatic && !m_bHyperlink && !m_bMultiClicked)
if (lpDrawItemStruct->itemState & ODS_SELECTED)
rectCaption.OffsetRect (1, 1);
pDC->SetBkMode (TRANSPARENT);
pDC->SetBkColor (::GetSysColor (COLOR_BTNFACE));
if (m_bHyperlink)
pDC->SetTextColor (C_HyperlinkColor);
else
pDC->SetTextColor (::GetSysColor(COLOR_BTNTEXT));
CFont* pOldFont = NULL;
CFont underlineFont;
if (m_bHyperlink) {
LOGFONT logFont;
VERIFY (GetFont()->GetLogFont (&logFont));
logFont.lfUnderline = TRUE;
VERIFY (underlineFont.CreateFontIndirect (&logFont));
pOldFont = pDC->SelectObject (&underlineFont);
}
#ifdef FOO_USEXPTHEMES
if(gThemeManager.m_bXPTheme && !m_bHyperlink)
{
gThemeManager.DrawThemeText( pDC->m_hDC,
BP_PUSHBUTTON,
GetXPMode(lpDrawItemStruct->itemState),
strCaption,
strCaption.GetLength(),
dwFormat,
NULL,
&rectCaption
);
}
else
#endif
{
if ((lpDrawItemStruct->itemState & ODS_DISABLED) == ODS_DISABLED)
{
::OffsetRect (&rectCaption, 1, 1);
::SetTextColor (pDC->m_hDC, ::GetSysColor (COLOR_3DHILIGHT));
::DrawTextEx ( pDC->m_hDC,
strCaption.GetBuffer(0),
strCaption.GetLength(),
&rectCaption,
dwFormat,
NULL);
strCaption.ReleaseBuffer();
::OffsetRect (&rectCaption, -1, -1);
::SetTextColor (pDC->m_hDC, ::GetSysColor (COLOR_GRAYTEXT));
::DrawTextEx (pDC->m_hDC, strCaption.GetBuffer(0), strCaption.GetLength(),
&rectCaption,
dwFormat,
NULL);
}
else
::DrawTextEx (pDC->m_hDC, strCaption.GetBuffer(0), strCaption.GetLength(),
&rectCaption,
dwFormat,
NULL);
}
strCaption.ReleaseBuffer();
if (pOldFont != NULL)
pDC->SelectObject (pOldFont);
}
I did a minor changing in FooButton::drawFocus():
void FooButton::drawFocus
(CDC* pDC,
LPDRAWITEMSTRUCT lpDrawItemStruct,
int nLeftEdge,
int nRightEdge)
{
ASSERT (pDC != NULL);
ASSERT (lpDrawItemStruct != NULL);
if (!m_bStatic && !m_bHot && (getFocusStyle() != FooButton::Focus::noFocus))
if ((lpDrawItemStruct->itemState & ODS_FOCUS) == ODS_FOCUS)
{
CRect rectButton;
#ifdef FOO_USEXPTHEMES
if(gThemeManager.m_bXPTheme)
{
rectButton = GetInternalRect(lpDrawItemStruct);
}
else
#endif
{
GetClientRect (&rectButton);
rectButton.left = nLeftEdge + 4;
rectButton.right = nRightEdge - 4;
rectButton.top += 4;
rectButton.bottom -= 4;
}
::DrawFocusRect (pDC->m_hDC, &rectButton);
}
}
It's no need to draw a default border if themes are activated (this is already done in drawFrame()). So FooButton::drawDefaultBorder() could be replaced with:
void FooButton::drawDefaultBorder
(CDC* pDC,
LPDRAWITEMSTRUCT lpDrawItemStruct)
{
ASSERT (pDC != NULL);
ASSERT (lpDrawItemStruct != NULL);
FooButton::Type btnType = getType();
if ((btnType != FooButton::Type::pushButton) &&
(btnType != FooButton::Type::pushButtonDropDown) &&
(btnType != FooButton::Type::pushButtonMulti))
return;
#ifdef FOO_USEXPTHEMES
if(gThemeManager.m_bXPTheme)
return;
#endif
if (getFocusStyle() != FooButton::Focus::defaultFocus)
return;
if (!IsDefault())
return;
COLORREF rgbBorder = GetSysColor (COLOR_3DDKSHADOW);
CBrush borderBrush (rgbBorder);
CRect innerRect = lpDrawItemStruct->rcItem;
if ((lpDrawItemStruct->itemState & ODS_SELECTED) == 0) {
COLORREF rgbBorder = GetSysColor (COLOR_3DDKSHADOW);
CPen borderGreyPen (PS_SOLID, 1, rgbBorder);
CPen* pOldPen = pDC->SelectObject (&borderGreyPen );
pDC->MoveTo (innerRect.right - 2, innerRect.top + 1);
pDC->LineTo (innerRect.right - 2, innerRect.bottom - 2);
pDC->LineTo (innerRect.left, innerRect.bottom - 2);
COLORREF rgbBrGrey = GetSysColor (COLOR_3DHILIGHT);
CPen brightGreyPen (PS_SOLID, 1, rgbBrGrey);
pOldPen = pDC->SelectObject (&brightGreyPen);
pDC->MoveTo (innerRect.right - 3, innerRect.top + 1);
pDC->LineTo (innerRect.left + 1, innerRect.top + 1);
pDC->LineTo (innerRect.left + 1, innerRect.bottom - 2);
COLORREF rgbDkGrey = GetSysColor (COLOR_BTNSHADOW);
CPen darkGreyPen (PS_SOLID, 1, rgbDkGrey);
pOldPen = pDC->SelectObject (&darkGreyPen);
pDC->MoveTo (innerRect.right - 3, innerRect.top + 2);
pDC->LineTo (innerRect.right - 3, innerRect.bottom - 3);
pDC->LineTo (innerRect.left + 1, innerRect.bottom - 3);
pDC->SelectObject (pOldPen);
} else {
innerRect.InflateRect (1, 1, -1, -1);
pDC->FrameRect (&innerRect, &borderBrush);
}
pDC->FrameRect(&lpDrawItemStruct->rcItem, &borderBrush);
}
I think that is all! However, themes are not fully supported. Radio buttons and check boxes are drawn in the old way, but it shouldn't be hard to fix this. I also think that GetInternalRect() should be used more often. This returns the inner area inside the frame. This could probably be used to place the bitmap more correctly.
And I'm sure that a few minor bugs are added, but the code seems to do what I want it to do .
|
|
|
|
|
I had the function FooButton::OnThemeChanged in my code yesterday, but that function isn't necessary. I have edited the previous message and removed this code.
|
|
|
|
|
Thanks for the feedback! Am looking into your mods.
/ravi
My new year's resolution: 2048 x 1536
Home | Articles | Freeware | Music
ravib@ravib.com
|
|
|
|
|
I accidentally draw the default frame on buttons that where disabled, but that is not how normal buttons act. I have updated the code (I simply check that bDisabled is not true before setting default frame in GetXPMode).
Another bug was that hyperlinks wasn't correctly drawn when themes was used. I solved this by drawing hyperlinks in the traditional way (that is, not using DrawThemeText).
|
|
|
|
|
The system color wasn't used to draw text caption. The code is updated.
|
|
|
|
|
Nice!
Some improvement:
void FooButton::DisabledBlt
(HDC hdcDest,
int nXDest,
int nYDest,
int nWidth,
int nHeight,
HBITMAP hBitmap,
int nXSrc,
int nYSrc)
{
ASSERT (hdcDest && hBitmap);
ASSERT (nWidth > 0 && nHeight > 0);
HDC hDC = CreateCompatibleDC (hdcDest);
ASSERT (hDC);
if (hDC) {
HDC bwDC = CreateCompatibleDC(hDC);
ASSERT (bwDC);
if (bwDC) {
struct
{
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[2];
} RGBBWBITMAPINFO = {
{
sizeof(BITMAPINFOHEADER),
nWidth,
nHeight,
1,
1,
BI_RGB,
0,
0,
0,
0,
0
},
{
{ 0x00, 0x00, 0x00, 0x00 },
{ 0xFF, 0xFF, 0xFF, 0x00 }
}
};
void* pbitsBW;
HBITMAP hBitmapBW = CreateDIBSection (bwDC,
(LPBITMAPINFO) &RGBBWBITMAPINFO,
DIB_RGB_COLORS, &pbitsBW, NULL, 0);
ASSERT (hBitmapBW);
if (hBitmapBW) {
SelectObject (bwDC, hBitmapBW);
SelectObject (hDC, hBitmap);
BitBlt (bwDC, 0, 0, nWidth, nHeight, hDC, nXSrc, nYSrc, SRCCOPY);
#ifdef FOO_USEXPTHEMES
if (!gThemeManager.m_bXPTheme)
#endif
FillRect (hdcDest,
CRect (nXDest, nYDest, nXDest + nWidth, nYDest + nHeight),
GetSysColorBrush (COLOR_3DFACE));
HBRUSH hb = CreateSolidBrush (GetSysColor (COLOR_3DHILIGHT));
HBRUSH oldBrush = (HBRUSH) SelectObject (hdcDest, hb);
BitBlt (hdcDest, nXDest + 1, nYDest + 1, nWidth, nHeight, bwDC, 0, 0, 0xB8074A);
hb = CreateSolidBrush (GetSysColor (COLOR_3DSHADOW));
DeleteObject (SelectObject(hdcDest, hb));
BitBlt (hdcDest, nXDest, nYDest, nWidth, nHeight, bwDC, 0, 0, 0xB8074A);
DeleteObject (SelectObject (hdcDest, oldBrush));
}
VERIFY (DeleteDC (bwDC));
}
VERIFY (DeleteDC(hDC));
}
}
You don't need to draw gray phone when XP themes are used.
|
|
|
|
|
These are the last three lines in OnLButtonUp:
CButton::OnLButtonUp (nFlags, point);
m_bMultiClicked = false;
Invalidate();
In one of my programs the button is destroyed when it is clicked. This means that the button exist when CButton::OnLButtonUp (nFlags, point) executes, but it's destroyed when next line run. Crash!
Now I have changed the order of the lines, and it seems works fine for me .
|
|
|
|
|
Good point - thanks! I'll update the code. Will also add a call to force a repaint immediately, eg:
m_bMultiClicked = false;
Invalidate();
UpdateWindow();
CButton::OnLButtonUp (nFlags, point);
/ravi
My new year's resolution: 2048 x 1536
Home | Articles | Freeware | Music
ravib@ravib.com
|
|
|
|
|
How to enable XP themes support for this class?
If you add manifest file it does not help
|
|
|
|
|
Unfortunately, it currently doesn't support XP themes.
/ravi
My new year's resolution: 2048 x 1536
Home | Articles | Freeware | Music
ravib@ravib.com
|
|
|
|
|
So... good project, but need some improvement.
|
|
|
|
|