mirror of https://github.com/tongzx/nt5src
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3679 lines
60 KiB
3679 lines
60 KiB
/*++
|
|
|
|
Copyright (c) 1994-1999 Microsoft Corporation
|
|
|
|
Module Name :
|
|
|
|
odlbox.cpp
|
|
|
|
Abstract:
|
|
|
|
Owner draw listbox/combobox base classes
|
|
|
|
Author:
|
|
|
|
Ronald Meijer (ronaldm)
|
|
|
|
Project:
|
|
|
|
Internet Services Manager (cluster edition)
|
|
|
|
Revision History:
|
|
|
|
--*/
|
|
|
|
//
|
|
// Include Files
|
|
//
|
|
#include "stdafx.h"
|
|
#include "common.h"
|
|
|
|
|
|
|
|
#ifdef _DEBUG
|
|
#undef THIS_FILE
|
|
static char BASED_CODE THIS_FILE[] = __FILE__;
|
|
#endif
|
|
|
|
|
|
|
|
#define new DEBUG_NEW
|
|
|
|
#define BMP_LEFT_OFFSET (1) // Space allotted to the left of bitmap
|
|
#define BMP_RIGHT_OFFSET (3) // Space allotted to the right of bitmap
|
|
|
|
//
|
|
// Ellipses are shown when column text doesn't fit in the display
|
|
//
|
|
const TCHAR g_szEllipses[] = _T("...");
|
|
int g_nLenEllipses = (sizeof(g_szEllipses) / sizeof(g_szEllipses[0])) - 1;
|
|
|
|
//
|
|
// Registry value for columns
|
|
//
|
|
const TCHAR g_szRegColumns[] = _T("Columns");
|
|
|
|
//
|
|
// Column Value Separator
|
|
//
|
|
const TCHAR g_szColValueSep[] = _T(" ");
|
|
|
|
|
|
|
|
void
|
|
GetDlgCtlRect(
|
|
IN HWND hWndParent,
|
|
IN HWND hWndControl,
|
|
OUT LPRECT lprcControl
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Get the control rectangle coordinates relative to its parent. This can
|
|
then be used in e.g. SetWindowPos()
|
|
|
|
Arguments:
|
|
|
|
HWND hWndParent : Parent window handle
|
|
HWND hWndControl : Control window handle
|
|
LPRECT lprcControl : Control rectangle to be filled in
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
#define MapWindowRect(hwndFrom, hwndTo, lprc)\
|
|
MapWindowPoints((hwndFrom), (hwndTo), (POINT *)(lprc), 2)
|
|
|
|
::GetWindowRect(hWndControl, lprcControl);
|
|
::MapWindowRect(NULL, hWndParent, lprcControl);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
FitPathToControl(
|
|
IN CWnd & wndControl,
|
|
IN LPCTSTR lpstrPath
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Display the given path in the given control, using ellipses
|
|
to ensure that the path fits within the control.
|
|
|
|
Arguments:
|
|
|
|
CWnd & wndControl : Control to display on
|
|
LPCTSTR lpstrPath : Path
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
CString strDisplay(lpstrPath);
|
|
UINT uLength = strDisplay.GetLength() + 4; // Account for ell.
|
|
LPTSTR lp = strDisplay.GetBuffer(uLength);
|
|
|
|
if (lp)
|
|
{
|
|
CDC * pDC = wndControl.GetDC();
|
|
ASSERT_PTR(pDC);
|
|
|
|
if (pDC != NULL)
|
|
{
|
|
CRect rc;
|
|
wndControl.GetClientRect(&rc);
|
|
pDC->DrawText(lp, uLength, &rc, DT_PATH_ELLIPSIS | DT_MODIFYSTRING);
|
|
wndControl.ReleaseDC(pDC);
|
|
}
|
|
|
|
strDisplay.ReleaseBuffer();
|
|
wndControl.SetWindowText(strDisplay);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void
|
|
ActivateControl(
|
|
IN CWnd & wndControl,
|
|
IN BOOL fShow
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Show/hide _AND_ enable/disable control window
|
|
|
|
Arguments:
|
|
|
|
CWnd & wndControl : Window in question
|
|
BOOL fShow : TRUE to show/enable,
|
|
FALSE to hide/disable
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
Notes:
|
|
|
|
Merely hiding a window does not disable it. Use this function
|
|
instead of ShowWindow() to do that.
|
|
|
|
--*/
|
|
{
|
|
wndControl.ShowWindow(fShow ? SW_SHOW : SW_HIDE);
|
|
wndControl.EnableWindow(fShow);
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
VerifyState()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Verify keyboard state
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
TRUE if keyboard is in specified state
|
|
FALSE otherwise.
|
|
|
|
--*/
|
|
{
|
|
SHORT s1, s2;
|
|
s1 = GetKeyState(VK_SHIFT);
|
|
s2 = GetKeyState(VK_CONTROL);
|
|
|
|
return (s1 & 0x8000) && (s2 & 0x8000);
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CMappedBitmapButton::LoadMappedBitmaps(
|
|
IN UINT nIDBitmapResource,
|
|
IN UINT nIDBitmapResourceSel,
|
|
IN UINT nIDBitmapResourceFocus,
|
|
IN UINT nIDBitmapResourceDisabled
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
LoadBitmaps will load one, two, three or all four bitmaps
|
|
returns TRUE if all specified images are loaded. This
|
|
will map the buttons to the default colours
|
|
|
|
Arguments:
|
|
|
|
UINT nIDBitmapResource : Standard button
|
|
UINT nIDBitmapResourceSel : Selected button
|
|
UINT nIDBitmapResourceFocus : Button with focus
|
|
UINT nIDBitmapResourceDisabled : Disabled button
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// delete old bitmaps (if present)
|
|
//
|
|
m_bitmap.DeleteObject();
|
|
m_bitmapSel.DeleteObject();
|
|
m_bitmapFocus.DeleteObject();
|
|
m_bitmapDisabled.DeleteObject();
|
|
|
|
if (!m_bitmap.LoadMappedBitmap(nIDBitmapResource))
|
|
{
|
|
TRACEEOLID("Failed to load bitmap for normal image.");
|
|
|
|
return FALSE; // need this one image
|
|
}
|
|
|
|
BOOL bAllLoaded = TRUE;
|
|
if (nIDBitmapResourceSel != 0)
|
|
{
|
|
if (!m_bitmapSel.LoadMappedBitmap(nIDBitmapResourceSel))
|
|
{
|
|
TRACEEOLID("Failed to load bitmap for selected image.");
|
|
bAllLoaded = FALSE;
|
|
}
|
|
}
|
|
if (nIDBitmapResourceFocus != 0)
|
|
{
|
|
if (!m_bitmapFocus.LoadMappedBitmap(nIDBitmapResourceFocus))
|
|
{
|
|
bAllLoaded = FALSE;
|
|
}
|
|
}
|
|
|
|
if (nIDBitmapResourceDisabled != 0)
|
|
{
|
|
if (!m_bitmapDisabled.LoadMappedBitmap(nIDBitmapResourceDisabled))
|
|
{
|
|
bAllLoaded = FALSE;
|
|
}
|
|
}
|
|
|
|
return bAllLoaded;
|
|
}
|
|
|
|
|
|
|
|
CRMCListBoxResources::CRMCListBoxResources(
|
|
IN int bmId,
|
|
IN int nBitmaps,
|
|
IN COLORREF rgbBackground
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Constructor
|
|
|
|
Arguments:
|
|
|
|
int bmId : Bitmap resource ID
|
|
int nBitmaps : Number of bitmaps
|
|
COLORREF rgbBackground : Background colour to mask out
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
: m_idBitmap(bmId),
|
|
m_rgbColorTransparent(rgbBackground),
|
|
m_nBitmaps(nBitmaps),
|
|
m_nBitmapHeight(0),
|
|
m_nBitmapWidth(-1), // Set Later
|
|
m_fInitialized(FALSE)
|
|
{
|
|
ASSERT(m_nBitmaps > 0);
|
|
GetSysColors();
|
|
PrepareBitmaps();
|
|
}
|
|
|
|
|
|
|
|
CRMCListBoxResources::~CRMCListBoxResources()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Destructor
|
|
|
|
Arguments:
|
|
|
|
N/A
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
{
|
|
UnprepareBitmaps();
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxResources::UnprepareBitmaps()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Free up bitmap resources
|
|
|
|
Arguments:
|
|
|
|
N/A
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
{
|
|
ASSERT(m_fInitialized);
|
|
|
|
if (m_fInitialized)
|
|
{
|
|
CBitmap * pBmp = (CBitmap *)CGdiObject::FromHandle(m_hOldBitmap);
|
|
ASSERT_READ_PTR(pBmp);
|
|
|
|
VERIFY(m_dcFinal.SelectObject(pBmp));
|
|
VERIFY(m_dcFinal.DeleteDC());
|
|
VERIFY(m_bmpScreen.DeleteObject());
|
|
|
|
m_fInitialized = FALSE;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxResources::PrepareBitmaps()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Prepare 2 rows of bitmaps. One with the selection colour background,
|
|
and one with the ordinary listbox background.
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
ASSERT(m_idBitmap);
|
|
|
|
//
|
|
// Clean up if we were already initialised
|
|
//
|
|
if (m_fInitialized)
|
|
{
|
|
UnprepareBitmaps();
|
|
}
|
|
|
|
//
|
|
// create device contexts compatible with screen
|
|
//
|
|
CDC dcImage;
|
|
CDC dcMasks;
|
|
|
|
VERIFY(dcImage.CreateCompatibleDC(0));
|
|
VERIFY(dcMasks.CreateCompatibleDC(0));
|
|
|
|
VERIFY(m_dcFinal.CreateCompatibleDC(0));
|
|
|
|
CBitmap bitmap;
|
|
VERIFY(bitmap.LoadBitmap(m_idBitmap));
|
|
|
|
BITMAP bm;
|
|
VERIFY(bitmap.GetObject(sizeof(BITMAP), &bm));
|
|
|
|
//
|
|
// Each bitmap is assumed to be the same size.
|
|
//
|
|
m_nBitmapWidth = bm.bmWidth / m_nBitmaps;
|
|
ASSERT(m_nBitmapWidth > 0);
|
|
|
|
const int bmWidth = bm.bmWidth;
|
|
const int bmHeight = bm.bmHeight;
|
|
m_nBitmapHeight = bmHeight;
|
|
|
|
CBitmap * pOldImageBmp = dcImage.SelectObject(&bitmap);
|
|
ASSERT_PTR(pOldImageBmp);
|
|
|
|
CBitmap bmpMasks;
|
|
VERIFY(bmpMasks.CreateBitmap(bmWidth, bmHeight * 2, 1, 1, NULL));
|
|
|
|
CBitmap * pOldMaskBmp = (CBitmap *)dcMasks.SelectObject(&bmpMasks);
|
|
ASSERT_PTR(pOldMaskBmp);
|
|
|
|
//
|
|
// create the foreground and object masks
|
|
//
|
|
COLORREF crOldBk = dcImage.SetBkColor(m_rgbColorTransparent);
|
|
dcMasks.BitBlt(0, 0, bmWidth, bmHeight, &dcImage, 0, 0, SRCCOPY);
|
|
dcMasks.BitBlt(0, 0, bmWidth, bmHeight, &dcImage, 0, bmHeight, SRCAND);
|
|
dcImage.SetBkColor(crOldBk);
|
|
dcMasks.BitBlt(0, bmHeight, bmWidth, bmHeight, &dcMasks, 0, 0, NOTSRCCOPY);
|
|
|
|
//
|
|
// create DC to hold final image
|
|
//
|
|
VERIFY(m_bmpScreen.CreateCompatibleBitmap(&dcImage, bmWidth, bmHeight * 2));
|
|
CBitmap * pOldBmp = (CBitmap*)m_dcFinal.SelectObject(&m_bmpScreen);
|
|
ASSERT_PTR(pOldBmp);
|
|
m_hOldBitmap = pOldBmp->m_hObject;
|
|
|
|
CBrush b1, b2;
|
|
VERIFY(b1.CreateSolidBrush(m_rgbColorHighlight));
|
|
VERIFY(b2.CreateSolidBrush(m_rgbColorWindow));
|
|
|
|
m_dcFinal.FillRect(CRect(0, 0, bmWidth, bmHeight), &b1);
|
|
m_dcFinal.FillRect(CRect(0, bmHeight, bmWidth, bmHeight * 2), &b2);
|
|
|
|
//
|
|
// mask out the object pixels in the destination
|
|
//
|
|
m_dcFinal.BitBlt(0, 0, bmWidth, bmHeight, &dcMasks, 0, 0, SRCAND);
|
|
|
|
//
|
|
// mask out the background pixels in the image
|
|
//
|
|
dcImage.BitBlt(0, 0, bmWidth, bmHeight, &dcMasks, 0, bmHeight, SRCAND);
|
|
|
|
//
|
|
// XOR the revised image into the destination
|
|
//
|
|
m_dcFinal.BitBlt(0, 0, bmWidth, bmHeight, &dcImage, 0, 0, SRCPAINT);
|
|
|
|
//
|
|
// mask out the object pixels in the destination
|
|
//
|
|
m_dcFinal.BitBlt(0, bmHeight, bmWidth, bmHeight, &dcMasks, 0, 0, SRCAND);
|
|
|
|
//
|
|
// XOR the revised image into the destination
|
|
//
|
|
m_dcFinal.BitBlt(0, bmHeight, bmWidth, bmHeight, &dcImage, 0, 0, SRCPAINT);
|
|
|
|
VERIFY(dcMasks.SelectObject(pOldMaskBmp));
|
|
VERIFY(dcImage.SelectObject(pOldImageBmp));
|
|
|
|
//
|
|
// The result of all of this mucking about is a bitmap identical with the
|
|
// one loaded from the resources but with the lower row of bitmaps having
|
|
// their background changed from transparent1 to the window background
|
|
// and the upper row having their background changed from transparent2 to
|
|
// the highlight colour. A derived CRMCListBox can BitBlt the relevant part
|
|
// of the image into an item's device context for a transparent bitmap
|
|
// effect which does not take any extra time over a normal BitBlt.
|
|
//
|
|
m_fInitialized = TRUE;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxResources::SysColorChanged()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Respond to change in system colours by rebuilding the resources
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Reinitialise bitmaps and syscolors. This should be called from
|
|
// the parent of the CRMCListBoxResources object from
|
|
// the OnSysColorChange() function.
|
|
//
|
|
GetSysColors();
|
|
PrepareBitmaps();
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxResources::GetSysColors()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Get sytem colours
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
m_rgbColorWindow = ::GetSysColor(COLOR_WINDOW);
|
|
m_rgbColorHighlight = ::GetSysColor(COLOR_HIGHLIGHT);
|
|
m_rgbColorWindowText = ::GetSysColor(COLOR_WINDOWTEXT);
|
|
m_rgbColorHighlightText = ::GetSysColor(COLOR_HIGHLIGHTTEXT);
|
|
}
|
|
|
|
|
|
CRMCListBoxDrawStruct::CRMCListBoxDrawStruct(
|
|
IN CDC * pDC,
|
|
IN RECT * pRect,
|
|
IN BOOL sel,
|
|
IN DWORD_PTR item,
|
|
IN int itemIndex,
|
|
IN const CRMCListBoxResources * pres
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Constructor
|
|
|
|
Arguments:
|
|
|
|
CDC * pdc : Device context
|
|
RECT * pRect : Rectange to paint into
|
|
BOOL sel : TRUE if selected
|
|
DWORD item : item
|
|
int itemIndex : item index
|
|
const CRMCListBoxResources * pres : Pointer to resources
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
: m_pDC(pDC),
|
|
m_Sel(sel),
|
|
m_ItemData(item),
|
|
m_ItemIndex(itemIndex),
|
|
m_pResources(pres)
|
|
{
|
|
m_Rect.CopyRect(pRect);
|
|
}
|
|
|
|
|
|
|
|
CODLBox::CODLBox()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Constructor for CODLBox -- abstract base class for both CRMCComboBox,
|
|
and CRMCListBox
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
: m_lfHeight(0),
|
|
m_pResources(NULL),
|
|
m_auTabs(),
|
|
m_pWnd(NULL)
|
|
{
|
|
}
|
|
|
|
|
|
|
|
CODLBox::~CODLBox()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Destructor
|
|
|
|
Arguments:
|
|
|
|
N/A
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
{
|
|
}
|
|
|
|
|
|
|
|
/* virtual */
|
|
BOOL
|
|
CODLBox::Initialize()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Listbox/combobox is being created
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Derived control must be attached at this point
|
|
//
|
|
ASSERT_PTR(m_pWnd);
|
|
|
|
//
|
|
// GetFont returns non NULL when the control is in a dialog box
|
|
//
|
|
CFont * pFont = m_pWnd->GetFont();
|
|
|
|
if(pFont == NULL)
|
|
{
|
|
LOGFONT lf;
|
|
CFont fontTmp;
|
|
|
|
::GetObject(::GetStockObject(SYSTEM_FONT), sizeof(LOGFONT), &lf);
|
|
fontTmp.CreateFontIndirect(&lf);
|
|
|
|
CalculateTextHeight(&fontTmp);
|
|
}
|
|
else
|
|
{
|
|
CalculateTextHeight(pFont);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CODLBox::ChangeFont(
|
|
IN CFont * pFont
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Change the control font the specified font
|
|
|
|
Arguments:
|
|
|
|
CFont * pFont : Pointer to the new font to be used
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure
|
|
|
|
--*/
|
|
{
|
|
ASSERT_PTR(m_pResources);
|
|
ASSERT_PTR(m_pWnd);
|
|
|
|
if( pFont == NULL || m_pResources == NULL
|
|
|| m_pWnd == NULL || m_pWnd->m_hWnd == NULL
|
|
)
|
|
{
|
|
TRACEEOLID("Invalid state of the control. Can't change font");
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Don't reflect changes immediately
|
|
//
|
|
m_pWnd->SetRedraw(FALSE);
|
|
|
|
m_pWnd->SetFont(pFont, TRUE);
|
|
CalculateTextHeight(pFont);
|
|
|
|
int nItems = __GetCount();
|
|
int bmHeight = m_pResources->BitmapHeight();
|
|
int nHeight = bmHeight > m_lfHeight ? bmHeight : m_lfHeight;
|
|
|
|
for(int i = 0; i < nItems; ++i)
|
|
{
|
|
__SetItemHeight(i, nHeight);
|
|
}
|
|
|
|
//
|
|
// Now reflect the change visually
|
|
//
|
|
m_pWnd->SetRedraw(TRUE);
|
|
m_pWnd->Invalidate();
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CODLBox::AttachResources(
|
|
IN const CRMCListBoxResources * pRes
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Attach the bitmaps
|
|
|
|
Arguments:
|
|
|
|
const CRMCListBoxResources * pRes : pointer to resources to be attached
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
if(pRes != m_pResources)
|
|
{
|
|
ASSERT_READ_PTR(pRes);
|
|
m_pResources = pRes;
|
|
|
|
if(m_pWnd != NULL && m_pWnd->m_hWnd != NULL)
|
|
{
|
|
//
|
|
// if window was created already, redraw everything.
|
|
//
|
|
m_pWnd->Invalidate();
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/* static */
|
|
int
|
|
CODLBox::GetRequiredWidth(
|
|
IN CDC * pDC,
|
|
IN const CRect & rc,
|
|
IN LPCTSTR lpstr,
|
|
IN int nLength
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Determine required display width of the string
|
|
|
|
Arguments:
|
|
|
|
CDC * pDC : Pointer to device context to use
|
|
const CRect & rc : Starting rectangle
|
|
LPCTSTR lpstr : String whose width is to be displayed
|
|
int nLength : Length (in characters of the string
|
|
|
|
Return Value:
|
|
|
|
The display width that the string would need to be displayed on the
|
|
given device context
|
|
|
|
--*/
|
|
{
|
|
#ifdef _DEBUG
|
|
|
|
pDC->AssertValid();
|
|
|
|
#endif // _DEBUG
|
|
|
|
CRect rcTmp(rc);
|
|
|
|
pDC->DrawText(
|
|
lpstr,
|
|
nLength,
|
|
&rcTmp,
|
|
DT_CALCRECT | DT_LEFT | DT_SINGLELINE | DT_NOPREFIX | DT_VCENTER
|
|
);
|
|
|
|
return rcTmp.Width();
|
|
}
|
|
|
|
|
|
|
|
/* static */
|
|
BOOL
|
|
CODLBox::ColumnText(
|
|
IN CDC * pDC,
|
|
IN int nLeft,
|
|
IN int nTop,
|
|
IN int nRight,
|
|
IN int nBottom,
|
|
IN LPCTSTR lpstr
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Display text limited by a rectangle. Use ellipses if the text is too wide
|
|
to fit inside the given dimensions.
|
|
|
|
Arguments:
|
|
|
|
CDC * pDC : Pointer to display context to use
|
|
int nLeft : Left coordinate
|
|
int nTop : Top coordinate
|
|
int nRight : Right coordinate
|
|
int nBottom : Bottom coordinate
|
|
LPCTSTR lpstr : String to be displayed
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure
|
|
|
|
--*/
|
|
{
|
|
BOOL fSuccess = TRUE;
|
|
|
|
#ifdef _DEBUG
|
|
|
|
pDC->AssertValid();
|
|
|
|
#endif // _DEBUG
|
|
|
|
CString str;
|
|
CRect rc(nLeft, nTop, nRight, nBottom);
|
|
|
|
int nAvailWidth = rc.Width();
|
|
int nLength = ::lstrlen(lpstr);
|
|
|
|
try
|
|
{
|
|
if (GetRequiredWidth(pDC, rc, lpstr, nLength) <= nAvailWidth)
|
|
{
|
|
//
|
|
// Sufficient space, display as is.
|
|
//
|
|
str = lpstr;
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Build a string with ellipses until it
|
|
// fits
|
|
//
|
|
LPTSTR lpTmp = str.GetBuffer(nLength + g_nLenEllipses);
|
|
while (nLength)
|
|
{
|
|
::lstrcpyn(lpTmp, lpstr, nLength);
|
|
::lstrcpy(lpTmp + nLength - 1, g_szEllipses);
|
|
|
|
if (GetRequiredWidth(pDC, rc, lpTmp,
|
|
nLength + g_nLenEllipses) <= nAvailWidth)
|
|
{
|
|
break;
|
|
}
|
|
|
|
--nLength;
|
|
}
|
|
|
|
str = lpTmp;
|
|
}
|
|
|
|
pDC->DrawText(
|
|
str,
|
|
&rc,
|
|
DT_LEFT | DT_SINGLELINE | DT_NOPREFIX | DT_VCENTER
|
|
);
|
|
|
|
}
|
|
catch(CMemoryException * e)
|
|
{
|
|
//
|
|
// Mem failure
|
|
//
|
|
fSuccess = FALSE;
|
|
e->ReportError();
|
|
e->Delete();
|
|
}
|
|
|
|
return fSuccess;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CODLBox::ComputeMargins(
|
|
IN CRMCListBoxDrawStruct & ds,
|
|
IN int nCol,
|
|
OUT int & nLeft,
|
|
OUT int & nRight
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Compute the left and right margins of the given column.
|
|
|
|
Arguments:
|
|
|
|
CRMCListBoxDrawStruct & ds : Drawing structure
|
|
int nCol : Column whose margins we're interested in
|
|
int & nLeft : Left column
|
|
int & nRight : Right column
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
nLeft = ds.m_Rect.left;
|
|
nRight = ds.m_Rect.right;
|
|
|
|
//
|
|
// Find tab value associated with column index (0-based),
|
|
// and adjust left and right
|
|
//
|
|
ASSERT(nCol <= NumTabs());
|
|
|
|
if (nCol > 0)
|
|
{
|
|
if (nCol <= NumTabs())
|
|
{
|
|
nLeft += m_auTabs[nCol-1];
|
|
}
|
|
}
|
|
if (nCol < NumTabs())
|
|
{
|
|
nRight = m_auTabs[nCol];
|
|
}
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CODLBox::DrawBitmap(
|
|
IN CRMCListBoxDrawStruct & ds,
|
|
IN int nCol,
|
|
IN int nID
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Draw a bitmap in the given column. Bitmap are always placed on the
|
|
leftmost side of the column if there is sufficient space.
|
|
|
|
Arguments:
|
|
|
|
CRMCListBoxDrawStruct & ds : Drawing structure
|
|
int nCol : Column to place bitmap in
|
|
int nID : Bitmap ID (offset within the bitmap resources)
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
CDC * pBmpDC = (CDC *)&ds.m_pResources->dcBitMap();
|
|
|
|
#ifdef _DEBUG
|
|
|
|
pBmpDC->AssertValid();
|
|
|
|
#endif // _DEBUG
|
|
|
|
//
|
|
// Select the bitmap with either a selection or
|
|
// a regular background
|
|
//
|
|
int bm_h = ds.m_Sel ? 0 : ds.m_pResources->BitmapHeight();
|
|
int bm_w = ds.m_pResources->BitmapWidth() * nID;
|
|
|
|
int nLeft, nRight;
|
|
ComputeMargins(ds, nCol, nLeft, nRight);
|
|
nLeft += BMP_LEFT_OFFSET;
|
|
|
|
//
|
|
// Check to make sure there's enough room before
|
|
// drawing the bitmap.
|
|
//
|
|
if (nRight - nLeft >= ds.m_pResources->BitmapWidth())
|
|
{
|
|
ds.m_pDC->BitBlt(
|
|
nLeft,
|
|
ds.m_Rect.top,
|
|
ds.m_pResources->BitmapWidth(),
|
|
ds.m_pResources->BitmapHeight(),
|
|
pBmpDC,
|
|
bm_w,
|
|
bm_h,
|
|
SRCCOPY
|
|
);
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CODLBox::ColumnText(
|
|
IN CRMCListBoxDrawStruct & ds,
|
|
IN int nCol,
|
|
IN BOOL fSkipBitmap,
|
|
IN LPCTSTR lpstr
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Draw column text.
|
|
|
|
Arguments:
|
|
|
|
CRMCListBoxDrawStruct & ds : Drawing structure
|
|
int nCol : Column to place bitmap in
|
|
BOOL fSkipBitmap : If TRUE, increment lefthand column by the width
|
|
of a bitmap
|
|
LPCTSTR lpstr : String to be displayed. May be truncated as
|
|
necessary
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure
|
|
|
|
--*/
|
|
{
|
|
int nLeft, nRight;
|
|
|
|
ComputeMargins(ds, nCol, nLeft, nRight);
|
|
|
|
//
|
|
// Optionally adjust for bitmap
|
|
//
|
|
if (fSkipBitmap)
|
|
{
|
|
nLeft += (ds.m_pResources->BitmapWidth() + BMP_RIGHT_OFFSET);
|
|
}
|
|
|
|
return CODLBox::ColumnText(
|
|
ds.m_pDC,
|
|
nLeft,
|
|
ds.m_Rect.top,
|
|
nRight,
|
|
ds.m_Rect.bottom,
|
|
lpstr
|
|
);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CODLBox::CalculateTextHeight(
|
|
IN CFont * pFont
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Calculate and set the text height of font
|
|
|
|
Arguments:
|
|
|
|
CFont * pFont : Pointer to the font to be used.
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
ASSERT_PTR(m_pWnd);
|
|
|
|
CClientDC dc(m_pWnd);
|
|
CFont * pOldFont = dc.SelectObject(pFont);
|
|
|
|
TEXTMETRIC tm;
|
|
dc.GetTextMetrics(&tm);
|
|
m_lfHeight = tm.tmHeight;
|
|
|
|
dc.SelectObject(pOldFont);
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CODLBox::AddTab(
|
|
IN UINT uTab
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Add a tab to the end of the list (e.g the right side of the header)
|
|
|
|
Arguments:
|
|
|
|
UINT uTab : Tab value to set
|
|
|
|
Return Value:
|
|
|
|
The index of the new tab
|
|
|
|
--*/
|
|
{
|
|
return (int)m_auTabs.Add(uTab);
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CODLBox::AddTabFromHeaders(
|
|
IN CWnd & wndLeft,
|
|
IN CWnd & wndRight
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Add a tab to the end of the list (e.g the right side of the header),
|
|
but compute the tab by taking the difference in left-hand coordinat of two
|
|
window controls (usually static header text)
|
|
|
|
Arguments:
|
|
|
|
CWnd & wndLeft : Left window
|
|
CWnd & wndRight : Right window
|
|
|
|
Return Value:
|
|
|
|
The index of the new tab
|
|
|
|
--*/
|
|
{
|
|
CRect rcLeft, rcRight;
|
|
|
|
wndLeft.GetWindowRect(&rcLeft);
|
|
wndRight.GetWindowRect(&rcRight);
|
|
|
|
ASSERT(rcRight.left > rcLeft.left);
|
|
|
|
return AddTab(rcRight.left - rcLeft.left - 1);
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CODLBox::AddTabFromHeaders(
|
|
IN UINT idLeft,
|
|
IN UINT idRight
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Similar to the function above, but use the control IDs. The parent
|
|
window is assumed to be the same as the parent window of the listbox
|
|
|
|
Arguments:
|
|
|
|
UINT idLeft : ID of the left control
|
|
UINT idRight : ID of the right control
|
|
|
|
Return Value:
|
|
|
|
The index of the new tab or -1 in case of failure
|
|
|
|
--*/
|
|
{
|
|
ASSERT_PTR(m_pWnd);
|
|
|
|
if (m_pWnd == NULL)
|
|
{
|
|
//
|
|
// Should have associated window handle by now
|
|
//
|
|
return -1;
|
|
}
|
|
|
|
CWnd * pLeft = m_pWnd->GetParent()->GetDlgItem(idLeft);
|
|
CWnd * pRight = m_pWnd->GetParent()->GetDlgItem(idRight);
|
|
|
|
ASSERT_READ_PTR(pLeft);
|
|
ASSERT_READ_PTR(pRight);
|
|
|
|
if (!pLeft || !pRight)
|
|
{
|
|
//
|
|
// One or both control IDs were not valid
|
|
//
|
|
return -1;
|
|
}
|
|
|
|
return AddTabFromHeaders(*pLeft, *pRight);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CODLBox::InsertTab(
|
|
IN int nIndex,
|
|
IN UINT uTab
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Insert a tab at the given index
|
|
|
|
Arguments:
|
|
|
|
int nIndex : Column index at which the tab is to be inserted
|
|
UINT uTab : Tab value to set
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
m_auTabs.InsertAt(nIndex, uTab);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CODLBox::RemoveTab(
|
|
IN int nIndex,
|
|
IN int nCount
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Remove one or more tabs
|
|
|
|
Arguments:
|
|
|
|
int nIndex : Column index at which to start removing tabs
|
|
int nCount : Number of tabs to be removed
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
m_auTabs.RemoveAt(nIndex, nCount);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CODLBox::RemoveAllTabs()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Remove all tabs
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
m_auTabs.RemoveAll();
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CODLBox::__DrawItem(
|
|
IN LPDRAWITEMSTRUCT lpDIS
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Draw an item. This will draw the focus and selection state, and then
|
|
call out to the derived class to draw the item.
|
|
|
|
Arguments:
|
|
|
|
LPDRAWITEMSTRUCT lpDIS : The drawitem structure
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Need to attach resources before creation/adding items
|
|
//
|
|
ASSERT_PTR(m_pResources);
|
|
|
|
CDC * pDC = CDC::FromHandle(lpDIS->hDC);
|
|
|
|
#ifdef _DEBUG
|
|
|
|
pDC->AssertValid();
|
|
|
|
#endif // _DEBUG
|
|
|
|
//
|
|
// Draw focus rectangle when no items in listbox
|
|
//
|
|
if(lpDIS->itemID == (UINT)-1)
|
|
{
|
|
if(lpDIS->itemAction & ODA_FOCUS)
|
|
{
|
|
//
|
|
// rcItem.bottom seems to be 0 for variable height list boxes
|
|
//
|
|
lpDIS->rcItem.bottom = m_lfHeight;
|
|
pDC->DrawFocusRect(&lpDIS->rcItem);
|
|
}
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
BOOL fSelChange = (lpDIS->itemAction & ODA_SELECT) != 0;
|
|
BOOL fFocusChange = (lpDIS->itemAction & ODA_FOCUS) != 0;
|
|
BOOL fDrawEntire = (lpDIS->itemAction & ODA_DRAWENTIRE) != 0;
|
|
|
|
if(fSelChange || fDrawEntire)
|
|
{
|
|
BOOL fSel = (lpDIS->itemState & ODS_SELECTED) != 0;
|
|
|
|
COLORREF hlite = (fSel ? (m_pResources->ColorHighlight())
|
|
: (m_pResources->ColorWindow()));
|
|
COLORREF textcol = (fSel ? (m_pResources->ColorHighlightText())
|
|
: (m_pResources->ColorWindowText()));
|
|
pDC->SetBkColor(hlite);
|
|
pDC->SetTextColor(textcol);
|
|
|
|
//
|
|
// fill the rectangle with the background colour.
|
|
//
|
|
pDC->ExtTextOut(0, 0, ETO_OPAQUE, &lpDIS->rcItem, NULL, 0, NULL);
|
|
|
|
CRMCListBoxDrawStruct ds(pDC,
|
|
(RECT *)&lpDIS->rcItem,
|
|
fSel,
|
|
(DWORD_PTR)lpDIS->itemData,
|
|
lpDIS->itemID,
|
|
m_pResources
|
|
);
|
|
|
|
//
|
|
// Now call the draw function of the derived class
|
|
//
|
|
DrawItemEx(ds);
|
|
}
|
|
|
|
if (fFocusChange || (fDrawEntire && (lpDIS->itemState & ODS_FOCUS)))
|
|
{
|
|
pDC->DrawFocusRect(&lpDIS->rcItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CODLBox::__MeasureItem(
|
|
IN OUT LPMEASUREITEMSTRUCT lpMIS
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Provide dimensions of given item
|
|
|
|
Arguments:
|
|
|
|
LPMEASUREITEMSTRUCT lpMIS : Measure item structure
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
ASSERT_PTR(m_pResources);
|
|
|
|
// int h = lpMIS->itemHeight;
|
|
int ch = TextHeight();
|
|
int bmHeight = m_pResources->BitmapHeight();
|
|
|
|
lpMIS->itemHeight = ch < bmHeight ? bmHeight : ch;
|
|
}
|
|
|
|
|
|
|
|
CRMCListBoxHeader::CRMCListBoxHeader(
|
|
IN DWORD dwStyle
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Constructor.
|
|
|
|
Arguments:
|
|
|
|
DWORD dwStyle : Style bits
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
: m_pHCtrl(NULL),
|
|
m_pListBox(NULL),
|
|
m_dwStyle(dwStyle),
|
|
m_fRespondToColumnWidthChanges(TRUE)
|
|
{
|
|
m_pHCtrl = new CHeaderCtrl;
|
|
}
|
|
|
|
|
|
|
|
CRMCListBoxHeader::~CRMCListBoxHeader()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Destructor.
|
|
|
|
Arguments:
|
|
|
|
N/A
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Kill the header control and the
|
|
// font
|
|
//
|
|
if (m_pHCtrl)
|
|
{
|
|
delete m_pHCtrl;
|
|
}
|
|
|
|
//
|
|
// Leave the listbox pointer alone, as we don't
|
|
// own it, but are merely associated with it.
|
|
//
|
|
}
|
|
|
|
//
|
|
// Message Map
|
|
//
|
|
BEGIN_MESSAGE_MAP(CRMCListBoxHeader, CStatic)
|
|
//{{AFX_MSG_MAP(CRMCListBoxHeader)
|
|
ON_WM_DESTROY()
|
|
//}}AFX_MSG_MAP
|
|
|
|
ON_NOTIFY_RANGE(HDN_ENDTRACK, 0, 0xFFFF, OnHeaderEndTrack)
|
|
ON_NOTIFY_RANGE(HDN_ITEMCHANGED, 0, 0xFFFF, OnHeaderItemChanged)
|
|
ON_NOTIFY_RANGE(HDN_ITEMCLICK, 0, 0xFFFF, OnHeaderItemClick)
|
|
|
|
END_MESSAGE_MAP()
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxHeader::OnDestroy()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
WM_DESTROY message handler. When the control is being destroyed,
|
|
also destroy the invisible static control.
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Destroy optional header control
|
|
//
|
|
if (m_pHCtrl)
|
|
{
|
|
m_pHCtrl->DestroyWindow();
|
|
}
|
|
|
|
CStatic::OnDestroy();
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CRMCListBoxHeader::Create(
|
|
IN DWORD dwStyle,
|
|
IN const RECT & rect,
|
|
IN CWnd * pParentWnd,
|
|
IN CHeaderListBox * pListBox,
|
|
IN UINT nID
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Create the control. This will first create an invisible static window,
|
|
which is to take up the entire area of the listbox. This static window
|
|
then will be the parent to the listbox as well as this header control.
|
|
|
|
Arguments:
|
|
|
|
DWORD dwStyle : Creation style bits
|
|
const RECT & rect : Rectangle in which the header is to be created
|
|
CWnd * pParentWnd : Parent window
|
|
CHeaderListBox * pListBox : Associated listbox
|
|
UINT nID : Control ID of the header
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Make sure the real header control exists by now
|
|
//
|
|
if (m_pHCtrl == NULL)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Make sure there's an associated listbox
|
|
//
|
|
m_pListBox = pListBox;
|
|
if (m_pListBox == NULL)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Create the controlling static window as do-nothing window
|
|
//
|
|
if (!CStatic::Create(NULL, WS_VISIBLE | SS_BITMAP | WS_CHILD,
|
|
rect, pParentWnd, 0xFFFF))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Now create the header control. Its parent
|
|
// window is this static control we just created
|
|
//
|
|
CRect rc(0, 0, 0 ,0);
|
|
dwStyle |= (UseButtons() ? HDS_BUTTONS : 0L);
|
|
VERIFY(m_pHCtrl->Create(dwStyle, rc, this, nID));
|
|
|
|
//
|
|
// Place header control as per style bits,
|
|
// compute the desired layout, and move it
|
|
//
|
|
HD_LAYOUT hdl;
|
|
WINDOWPOS wp;
|
|
|
|
GetClientRect(&rc);
|
|
hdl.prc = &rc;
|
|
hdl.pwpos = ℘
|
|
|
|
m_pHCtrl->Layout(&hdl);
|
|
m_pHCtrl->SetWindowPos(m_pListBox, wp.x, wp.y,
|
|
wp.cx, wp.cy, wp.flags | SWP_SHOWWINDOW);
|
|
|
|
//
|
|
// And move our associated listbox just below it
|
|
//
|
|
::GetDlgCtlRect(GetParent()->m_hWnd, m_pListBox->m_hWnd, &rc);
|
|
rc.top += wp.cy - 1;
|
|
|
|
//
|
|
// Adjust if header is bigger than the entire listbox
|
|
//
|
|
if (rc.top > rc.bottom)
|
|
{
|
|
rc.top = rc.bottom;
|
|
}
|
|
// Fix for theme support. Make listbox and header children of the same static control
|
|
m_pListBox->SetParent(this);
|
|
GetParent()->ClientToScreen(&rc);
|
|
ScreenToClient(&rc);
|
|
m_pListBox->MoveWindow(rc.left, rc.top, rc.Width(), rc.Height());
|
|
|
|
//
|
|
// Make sure the header uses the right font
|
|
//
|
|
m_pHCtrl->SetFont(
|
|
CFont::FromHandle((HFONT)::GetStockObject(DEFAULT_GUI_FONT)),
|
|
FALSE
|
|
);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxHeader::OnHeaderEndTrack(
|
|
IN UINT nId,
|
|
IN NMHDR * pnmh,
|
|
OUT LRESULT * pResult
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
User has finished dragging the column divider. If we're supposed to ensure
|
|
that the last column is a stretch column, turn off the redraw now -- it
|
|
will get turned back on after the column width changes have all been
|
|
completed. This will reduce the flash effect.
|
|
|
|
Arguments:
|
|
|
|
UINT nId : Control ID
|
|
NMHDR * pnmh : Notification header structure
|
|
LRESULT * pResult : Result. Will be set to 0 if the message was handled
|
|
|
|
Return Value:
|
|
|
|
None (handled in pResult)
|
|
|
|
--*/
|
|
{
|
|
pnmh;
|
|
nId;
|
|
|
|
if (DoesRespondToColumnWidthChanges() && UseStretch())
|
|
{
|
|
//
|
|
// This will get turned back on in OnHeaderItemChanged
|
|
//
|
|
SetRedraw(FALSE);
|
|
}
|
|
|
|
*pResult = 0;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxHeader::SetColumnWidth(
|
|
IN int nCol,
|
|
IN int nWidth
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Set the given column to the given width
|
|
|
|
Arguments:
|
|
|
|
int nCol : Column number
|
|
int nWidth : New width
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
ASSERT(nCol < QueryNumColumns());
|
|
|
|
if (nCol >= QueryNumColumns())
|
|
{
|
|
return;
|
|
}
|
|
|
|
TRACEEOLID("Setting width of column " << nCol << " to " << nWidth);
|
|
|
|
HD_ITEM hdItem;
|
|
|
|
hdItem.mask = HDI_WIDTH;
|
|
hdItem.cxy = nWidth;
|
|
VERIFY(SetItem(nCol, &hdItem));
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxHeader::OnHeaderItemChanged(
|
|
IN UINT nId,
|
|
IN NMHDR *pnmh,
|
|
OUT LRESULT *pResult
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Handle change in header column width. Note: we're actually tracking
|
|
the HDN_ITEMCHANGED notification, not the HDN_ENDDRAG one, because
|
|
the latter is sent out before the column widths in the structure have
|
|
changed.
|
|
|
|
Arguments:
|
|
|
|
UINT nId : Control ID
|
|
NMHDR * pnmh : Notification header structure
|
|
LRESULT * pResult : Result. Will be set to 0 if the message was handled
|
|
|
|
Return Value:
|
|
|
|
None (handled in pResult)
|
|
|
|
--*/
|
|
{
|
|
nId;
|
|
//
|
|
// Adjust tabs in associate listbox if
|
|
// column widths have changed
|
|
//
|
|
HD_NOTIFY * pNotify = (HD_NOTIFY *)pnmh;
|
|
if (DoesRespondToColumnWidthChanges() && pNotify->pitem->mask & HDI_WIDTH)
|
|
{
|
|
ASSERT_PTR(m_pListBox);
|
|
|
|
//
|
|
// Stretch the last column
|
|
//
|
|
if (UseStretch())
|
|
{
|
|
//
|
|
// Turn this off, as we don't want
|
|
// to get in an infinite loop
|
|
//
|
|
RespondToColumnWidthChanges(FALSE);
|
|
|
|
//
|
|
// Compute available space
|
|
//
|
|
CRect rc;
|
|
GetClientRect(&rc);
|
|
|
|
//
|
|
// See how much is taken up by preceding
|
|
// columns
|
|
//
|
|
int nTotalWidth = 0;
|
|
int cColumns = QueryNumColumns();
|
|
int nLastCol = cColumns - 1;
|
|
ASSERT(nLastCol >= 0);
|
|
|
|
for (int nCol = 0; nCol < nLastCol; ++nCol)
|
|
{
|
|
int nWidth = GetColumnWidth(nCol);
|
|
|
|
//
|
|
// Each column must be at least one pixel wide
|
|
//
|
|
int nMaxWidth = rc.Width() - nTotalWidth - (nLastCol - nCol);
|
|
if (nWidth > nMaxWidth)
|
|
{
|
|
nWidth = nMaxWidth;
|
|
SetColumnWidth(nCol, nWidth);
|
|
}
|
|
|
|
nTotalWidth += nWidth;
|
|
}
|
|
|
|
//
|
|
// Make sure the last column takes up the rest
|
|
//
|
|
if (rc.Width() > nTotalWidth)
|
|
{
|
|
SetColumnWidth(nLastCol, rc.Width() - nTotalWidth);
|
|
}
|
|
|
|
//
|
|
// Turn this back on again
|
|
//
|
|
RespondToColumnWidthChanges(TRUE);
|
|
|
|
//
|
|
// Redraw will have been turned off in
|
|
// OnHeaderEndTrack, now that all column
|
|
// movement has completed, turn it back
|
|
// on to draw the control in its current
|
|
// state.
|
|
//
|
|
SetRedraw(TRUE);
|
|
Invalidate();
|
|
}
|
|
|
|
//
|
|
// Recompute tabs on associate listbox,
|
|
// and force redraw on it.
|
|
//
|
|
m_pListBox->SetRedraw(FALSE);
|
|
SetTabsFromHeader();
|
|
m_pListBox->SetRedraw(TRUE);
|
|
m_pListBox->Invalidate();
|
|
}
|
|
|
|
*pResult = 0;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxHeader::OnHeaderItemClick(
|
|
IN UINT nId,
|
|
IN NMHDR *pnmh,
|
|
OUT LRESULT *pResult
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
A button has been clicked in the header control. Pass it on
|
|
to the real parent window.
|
|
|
|
Arguments:
|
|
|
|
UINT nId : Control ID
|
|
NMHDR * pnmh : Notification header structure
|
|
LRESULT * pResult : Result. Will be set to 0 if the message was handled
|
|
|
|
Return Value:
|
|
|
|
None (handled in pResult)
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Pass notification on to parent
|
|
//
|
|
ASSERT(GetParent());
|
|
GetParent()->SendMessage(WM_NOTIFY, (WPARAM)nId, (LPARAM)pnmh);
|
|
*pResult = 0;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBoxHeader::SetTabsFromHeader()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Set the tabs (which are cumulative) from the header control
|
|
columns (which are not)
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Must have the same number of tabs
|
|
// as header columns
|
|
//
|
|
ASSERT_PTR(m_pListBox);
|
|
ASSERT(GetItemCount() == m_pListBox->NumTabs());
|
|
|
|
int nTab = 0;
|
|
for (int n = 0; n < m_pListBox->NumTabs(); ++n)
|
|
{
|
|
m_pListBox->SetTab(n, nTab += GetColumnWidth(n));
|
|
}
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CRMCListBoxHeader::GetItemCount() const
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Get the number of items in the header
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
The number of items in the header (e.g. the number of columns)
|
|
|
|
--*/
|
|
{
|
|
ASSERT_PTR(m_pHCtrl);
|
|
return m_pHCtrl->GetItemCount();
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CRMCListBoxHeader::GetItem(
|
|
IN int nPos,
|
|
OUT HD_ITEM * pHeaderItem
|
|
) const
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Get information on specific position (column index)
|
|
|
|
Arguments:
|
|
|
|
int nPos : Column index
|
|
HD_ITEM * pHeaderItem : Header item information
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure (bad column index)
|
|
|
|
--*/
|
|
{
|
|
ASSERT_PTR(m_pHCtrl);
|
|
return m_pHCtrl->GetItem(nPos, pHeaderItem);
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CRMCListBoxHeader::GetColumnWidth(
|
|
IN int nPos
|
|
) const
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Get column width of a specific column
|
|
|
|
Arguments:
|
|
|
|
int nPos : Column index
|
|
|
|
Return Value:
|
|
|
|
The column width of the given colum, or -1 in case of failure (bad
|
|
column index)
|
|
|
|
--*/
|
|
{
|
|
HD_ITEM hi;
|
|
|
|
hi.mask = HDI_WIDTH;
|
|
if (GetItem(nPos, &hi))
|
|
{
|
|
return hi.cxy;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CRMCListBoxHeader::SetItem(
|
|
IN int nPos,
|
|
IN HD_ITEM * pHeaderItem
|
|
)
|
|
/*++***
|
|
|
|
Routine Description:
|
|
|
|
Set information on specific position (column index)
|
|
|
|
Arguments:
|
|
|
|
int nPos : Column index
|
|
HD_ITEM * pHeaderItem : Header item information
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure (bad column index)
|
|
|
|
--*/
|
|
{
|
|
ASSERT_PTR(m_pHCtrl);
|
|
ASSERT_PTR(m_pListBox);
|
|
|
|
if (!m_pHCtrl->SetItem(nPos, pHeaderItem))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
if (pHeaderItem->mask & HDI_WIDTH)
|
|
{
|
|
SetTabsFromHeader();
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CRMCListBoxHeader::InsertItem(
|
|
IN int nPos,
|
|
IN HD_ITEM * pHeaderItem
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
insert information in specific position (column index)
|
|
|
|
Arguments:
|
|
|
|
int nPos : Column index
|
|
HD_ITEM * pHeaderItem : Header item information
|
|
|
|
Return Value:
|
|
|
|
The new index, or -1 in case of failure.
|
|
|
|
--*/
|
|
{
|
|
ASSERT_PTR(m_pHCtrl);
|
|
ASSERT_PTR(m_pListBox);
|
|
|
|
int nCol = m_pHCtrl->InsertItem(nPos, pHeaderItem);
|
|
if (nCol != -1)
|
|
{
|
|
//
|
|
// Set 0-width tab, as tabs get recomputed anyway
|
|
//
|
|
m_pListBox->InsertTab(nPos, 0);
|
|
SetTabsFromHeader();
|
|
}
|
|
|
|
return nCol;
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CRMCListBoxHeader::DeleteItem(
|
|
IN int nPos
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Delete the given item (i.e. column)
|
|
|
|
Arguments:
|
|
|
|
int nPos : Column index
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure (bad column index)
|
|
|
|
--*/
|
|
{
|
|
ASSERT_PTR(m_pHCtrl);
|
|
ASSERT_PTR(m_pListBox);
|
|
|
|
if (!m_pHCtrl->DeleteItem(nPos))
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
m_pListBox->RemoveTab(nPos, 1);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
IMPLEMENT_DYNAMIC(CRMCListBoxHeader, CStatic);
|
|
|
|
|
|
|
|
CRMCListBox::CRMCListBox()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Constructor
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
: m_fInitialized(FALSE),
|
|
m_fMultiSelect(FALSE)
|
|
{
|
|
}
|
|
|
|
|
|
|
|
CRMCListBox::~CRMCListBox()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Destructor
|
|
|
|
Arguments:
|
|
|
|
N/A
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
{
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Message Map
|
|
//
|
|
BEGIN_MESSAGE_MAP(CRMCListBox, CListBox)
|
|
//{{AFX_MSG_MAP(CRMCListBox)
|
|
ON_WM_CREATE()
|
|
ON_WM_DESTROY()
|
|
//}}AFX_MSG_MAP
|
|
END_MESSAGE_MAP()
|
|
|
|
|
|
|
|
/* virtual */
|
|
BOOL
|
|
CRMCListBox::Initialize()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This function should be called directly when subclassing an existing
|
|
listbox, otherwise OnCreate will take care of it.
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Make sure we're only initialized once
|
|
//
|
|
if (m_fInitialized)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
//
|
|
// Ensure the base class knows our window
|
|
// handle
|
|
//
|
|
AttachWindow(this);
|
|
|
|
if (!CODLBox::Initialize())
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
m_fInitialized = TRUE;
|
|
|
|
DWORD dwStyle = GetStyle();
|
|
m_fMultiSelect = (dwStyle & (LBS_EXTENDEDSEL | LBS_MULTIPLESEL)) != 0;
|
|
|
|
return m_fInitialized;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBox::MeasureItem(
|
|
IN LPMEASUREITEMSTRUCT lpMIS
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
CListBox override to ODL base class
|
|
|
|
Arguments:
|
|
|
|
LPMEASUREITEMSTRUCT lpMIS : Measure item structure
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
CODLBox::__MeasureItem(lpMIS);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBox::DrawItem(
|
|
IN LPDRAWITEMSTRUCT lpDIS
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
CListBox override to ODL base class
|
|
|
|
Arguments:
|
|
|
|
LPDRAWITEMSTRUCT lpDIS : Drawing item structure
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
CODLBox::__DrawItem(lpDIS);
|
|
}
|
|
|
|
|
|
|
|
/* virtual */
|
|
void
|
|
CRMCListBox::DrawItemEx(
|
|
IN CRMCListBoxDrawStruct & dw
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Do-nothing extended draw function, which should
|
|
be provided by the derived class. This one will
|
|
ASSERT, and should never be called.
|
|
|
|
Arguments:
|
|
|
|
CRMCListBoxDrawStruct & dw : Draw Structure
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
dw;
|
|
ASSERT_MSG("Derived class did not provide DrawItemEx");
|
|
}
|
|
|
|
|
|
|
|
/* virtual */
|
|
int
|
|
CRMCListBox::__GetCount() const
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Provide GetCount() to ODL base class
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
Count of items in the listbox
|
|
|
|
--*/
|
|
{
|
|
return GetCount();
|
|
}
|
|
|
|
|
|
|
|
/* virtual */
|
|
int
|
|
CRMCListBox::__SetItemHeight(
|
|
IN int nIndex,
|
|
IN UINT cyItemHeight
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Provide SetItemHeight() to ODL base class
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
LB_ERR if the index or height is invalid.
|
|
|
|
--*/
|
|
{
|
|
return SetItemHeight(nIndex, cyItemHeight);
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CRMCListBox::OnCreate(
|
|
IN LPCREATESTRUCT lpCreateStruct
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Listbox is being created
|
|
|
|
Arguments:
|
|
|
|
LPCREATESTRUCT lpCreateStruct : Creation structure
|
|
|
|
Return Value:
|
|
|
|
-1 for failure, 0 for success
|
|
|
|
--*/
|
|
{
|
|
if (CListBox::OnCreate(lpCreateStruct) == -1)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
Initialize();
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CRMCListBox::GetCurSel() const
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Get the index of the current selected item
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
On multi-selection listbox, it will return
|
|
the index of an item, iff that is the only
|
|
item selected.
|
|
|
|
On single-selection listbox, it behaves as
|
|
normal.
|
|
|
|
--*/
|
|
{
|
|
if (IsMultiSelect())
|
|
{
|
|
//
|
|
// We only like it if one item is selected
|
|
//
|
|
int nCurSel = LB_ERR;
|
|
|
|
if (CListBox::GetSelCount() == 1)
|
|
{
|
|
if (CListBox::GetSelItems(1, &nCurSel) != 1)
|
|
{
|
|
nCurSel = LB_ERR;
|
|
}
|
|
}
|
|
|
|
return nCurSel;
|
|
}
|
|
|
|
//
|
|
// Single select listbox
|
|
//
|
|
return CListBox::GetCurSel();
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CRMCListBox::SetCurSel(
|
|
IN int nSelect
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Select an item. On a multi-select listbox,
|
|
this will deselect everything except the given
|
|
item.
|
|
|
|
Arguments:
|
|
|
|
int nSelect : Index of the item to be selected, or
|
|
-1 to reset all selections.
|
|
|
|
Return Value:
|
|
|
|
LB_ERR in case of error.
|
|
|
|
--*/
|
|
{
|
|
if (IsMultiSelect())
|
|
{
|
|
//
|
|
// Reset all selections
|
|
//
|
|
int nReturn = SelItemRange(FALSE, 0, GetCount() - 1);
|
|
|
|
if (nSelect >= 0)
|
|
{
|
|
//
|
|
// Ensure item is visible
|
|
//
|
|
nReturn = CListBox::SetSel(nSelect, TRUE);
|
|
CListBox::SetCaretIndex(nSelect, 0);
|
|
}
|
|
|
|
return nReturn;
|
|
}
|
|
|
|
return CListBox::SetCurSel(nSelect);
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CRMCListBox::GetSel(
|
|
IN int nSel
|
|
) const
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Determine if the given item is selected or not
|
|
Works for both single and multi-select listboxes
|
|
|
|
Arguments:
|
|
|
|
int nSel : Item whose state to check
|
|
|
|
Return Value:
|
|
|
|
LB_ERR in case of error, 0 if the item in question
|
|
is not selected, a positive number if it is.
|
|
|
|
--*/
|
|
{
|
|
if (IsMultiSelect())
|
|
{
|
|
return CListBox::GetSel(nSel);
|
|
}
|
|
|
|
//
|
|
// Some magic for single select
|
|
//
|
|
if (nSel < 0 || nSel >= CListBox::GetCount())
|
|
{
|
|
return LB_ERR;
|
|
}
|
|
|
|
return nSel == CListBox::GetCurSel()
|
|
? TRUE
|
|
: FALSE;
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CRMCListBox::GetSelCount() const
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Return count of selected items. Works for both
|
|
single and multi select (in the former case,
|
|
it will return zero or one only)
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
Count of selected items
|
|
|
|
--*/
|
|
{
|
|
if (IsMultiSelect())
|
|
{
|
|
return CListBox::GetSelCount();
|
|
}
|
|
|
|
return GetCurSel() != LB_ERR ? 1 : 0;
|
|
}
|
|
|
|
|
|
|
|
void *
|
|
CRMCListBox::GetSelectedListItem(
|
|
OUT int * pnSel OPTIONAL
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Return the single selected item in the list or NULL
|
|
|
|
Arguments:
|
|
|
|
int * pnSel : Optionally returns the selected index
|
|
|
|
Returns:
|
|
|
|
The currently selected (single) item, or NULL
|
|
if 0 or more than one items is selected. Works for
|
|
both multi-select and single select.
|
|
|
|
--*/
|
|
{
|
|
void * pItem = NULL;
|
|
|
|
int nCurSel = GetCurSel();
|
|
if (nCurSel >= 0)
|
|
{
|
|
//
|
|
// Get item properties
|
|
//
|
|
pItem = GetItemDataPtr(nCurSel);
|
|
if (pnSel)
|
|
{
|
|
*pnSel = nCurSel;
|
|
}
|
|
}
|
|
|
|
return pItem;
|
|
}
|
|
|
|
|
|
|
|
void *
|
|
CRMCListBox::GetNextSelectedItem(
|
|
IN OUT int * pnStartingIndex
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Return the next selected item starting at a specific
|
|
index.
|
|
|
|
Arguments:
|
|
|
|
int *pnStartingIndex : Starting index (>= 0)
|
|
|
|
Return Value:
|
|
|
|
Pointer to next selected item, or NULL if there are
|
|
none left.
|
|
|
|
The starting index will be updated to reflect the current
|
|
index, LB_ERR if no more selected items remain.
|
|
|
|
--*/
|
|
{
|
|
ASSERT_READ_WRITE_PTR(pnStartingIndex);
|
|
|
|
if (!pnStartingIndex)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
ASSERT(*pnStartingIndex >= 0);
|
|
|
|
if (*pnStartingIndex < 0)
|
|
{
|
|
*pnStartingIndex = 0;
|
|
}
|
|
|
|
if (IsMultiSelect())
|
|
{
|
|
//
|
|
// Multi-select -- loop through
|
|
// until found
|
|
//
|
|
BOOL fFoundItem = FALSE;
|
|
|
|
while (*pnStartingIndex < GetCount())
|
|
{
|
|
if (CListBox::GetSel(*pnStartingIndex) > 0)
|
|
{
|
|
++fFoundItem;
|
|
break;
|
|
}
|
|
|
|
++(*pnStartingIndex);
|
|
}
|
|
|
|
if (!fFoundItem)
|
|
{
|
|
*pnStartingIndex = LB_ERR;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//
|
|
// Single select listbox, so there's no
|
|
// looping through -- either the selected item
|
|
// (if any) is in range or it isn't.
|
|
//
|
|
int nCurSel = CListBox::GetCurSel();
|
|
*pnStartingIndex = (nCurSel >= *pnStartingIndex) ? nCurSel : LB_ERR;
|
|
}
|
|
|
|
return (*pnStartingIndex != LB_ERR)
|
|
? GetItemDataPtr(*pnStartingIndex)
|
|
: NULL;
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CRMCListBox::SelectItem(
|
|
IN void * pItemData
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Select the listbox item with the given data pointer
|
|
|
|
Arguments:
|
|
|
|
void * pItemData : Item to search for
|
|
|
|
Return Value:
|
|
|
|
TRUE if the item was found and selected, FALSE otherwise
|
|
|
|
Notes:
|
|
|
|
On a multi-select listbox, this will unselect
|
|
all other items in the listbox.
|
|
|
|
--*/
|
|
{
|
|
if (pItemData != NULL)
|
|
{
|
|
for (int n = 0; n < GetCount(); ++n)
|
|
{
|
|
if (pItemData == GetItemDataPtr(n))
|
|
{
|
|
SetCurSel(n);
|
|
|
|
return TRUE;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!IsMultiSelect())
|
|
{
|
|
//
|
|
// Set no selection
|
|
//
|
|
SetCurSel(-1);
|
|
}
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCListBox::InvalidateSelection(
|
|
IN int nSel
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Force a repaint of the given selection
|
|
|
|
Arguments:
|
|
|
|
int nSel : Index of the item to be repainted
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
CRect rc;
|
|
|
|
if (GetItemRect(nSel, &rc) != LB_ERR)
|
|
{
|
|
InvalidateRect(&rc, TRUE);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
IMPLEMENT_DYNAMIC(CRMCListBox,CListBox);
|
|
|
|
|
|
|
|
CHeaderListBox::CHeaderListBox(
|
|
IN DWORD dwStyle,
|
|
IN LPCTSTR lpRegKey OPTIONAL
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Constructor
|
|
|
|
Arguments:
|
|
|
|
DWORD dwStyle : Style bits (see HLS_*)
|
|
LPCTSTR lpRegKey : If specified, the registry key where the column
|
|
sizes will be stored.
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
: m_strRegKey(),
|
|
m_fInitialized(FALSE)
|
|
{
|
|
m_pHeader = new CRMCListBoxHeader(dwStyle);
|
|
if (lpRegKey)
|
|
{
|
|
GenerateRegistryKey(m_strRegKey, lpRegKey);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
CHeaderListBox::~CHeaderListBox()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Destructor
|
|
|
|
Arguments:
|
|
|
|
N/A
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Clean up header control
|
|
//
|
|
ASSERT_PTR(m_pHeader);
|
|
|
|
if (m_pHeader != NULL)
|
|
{
|
|
delete m_pHeader;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Message map
|
|
//
|
|
BEGIN_MESSAGE_MAP(CHeaderListBox, CRMCListBox)
|
|
//{{AFX_MSG_MAP(CHeaderListBox)
|
|
ON_WM_CREATE()
|
|
ON_WM_DESTROY()
|
|
//}}AFX_MSG_MAP
|
|
END_MESSAGE_MAP()
|
|
|
|
|
|
|
|
/* virtual */
|
|
BOOL
|
|
CHeaderListBox::Initialize()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This function should be called directly when subclassing an existing
|
|
listbox, otherwise OnCreate will take care of it, and this function
|
|
should not be called
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Make sure we're only initialized once
|
|
//
|
|
if (m_fInitialized)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
if (!CRMCListBox::Initialize())
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Create header control
|
|
//
|
|
ASSERT_PTR(m_pHeader);
|
|
|
|
if (m_pHeader)
|
|
{
|
|
TRACEEOLID("Creating Header");
|
|
|
|
//
|
|
// Create it in our location exactly
|
|
//
|
|
CRect rc;
|
|
::GetDlgCtlRect(GetParent()->m_hWnd, m_hWnd, &rc);
|
|
|
|
//
|
|
// Make sure the header control shares the same parent
|
|
// as we do,
|
|
//
|
|
ASSERT(GetParent());
|
|
|
|
#ifndef CCS_NOHILITE
|
|
#define CCS_NOHILITE 0x00000010L
|
|
#endif
|
|
|
|
DWORD dwStyle = WS_VISIBLE | CCS_TOP | CCS_NODIVIDER | WS_BORDER
|
|
| HDS_HORZ;
|
|
|
|
if (!m_pHeader->Create(dwStyle, rc, GetParent(), this, 0xFFFF))
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
m_fInitialized = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CHeaderListBox::QueryColumnWidth(
|
|
IN int nCol
|
|
) const
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Get the width of the specified column
|
|
|
|
Arguments:
|
|
|
|
int nCol : The column
|
|
|
|
Return Value:
|
|
|
|
The width of the column, or -1 if the column index was out of range
|
|
|
|
--*/
|
|
{
|
|
ASSERT(nCol < QueryNumColumns());
|
|
|
|
if (nCol >= QueryNumColumns())
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
HD_ITEM hdItem;
|
|
|
|
hdItem.mask = HDI_WIDTH;
|
|
VERIFY(GetHeaderItem(nCol, &hdItem));
|
|
|
|
return hdItem.cxy;
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CHeaderListBox::SetColumnWidth(
|
|
IN int nCol,
|
|
IN int nWidth
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Set the width of the specified column
|
|
|
|
Arguments:
|
|
|
|
int nCol : The column
|
|
int nWidth : New width
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure
|
|
|
|
--*/
|
|
{
|
|
ASSERT(nCol < QueryNumColumns());
|
|
|
|
if (nCol >= QueryNumColumns())
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
TRACEEOLID("Setting width of column " << nCol << " to " << nWidth);
|
|
|
|
HD_ITEM hdItem;
|
|
hdItem.mask = HDI_WIDTH;
|
|
hdItem.cxy = nWidth;
|
|
VERIFY(SetHeaderItem(nCol, &hdItem));
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CHeaderListBox::SetWidthsFromReg()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Attempt to set the column widths from the registry
|
|
value we were initialized with.
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
TRUE if the column widths were succesfully set from the registry,
|
|
FALSE otherwise
|
|
|
|
--*/
|
|
{
|
|
if (m_strRegKey.IsEmpty())
|
|
{
|
|
//
|
|
// No reg key specified
|
|
//
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Try to read the current column sizes from the registry
|
|
//
|
|
CRegKey rkUser;
|
|
if (ERROR_SUCCESS != rkUser.Create(HKEY_CURRENT_USER, m_strRegKey))
|
|
{
|
|
//
|
|
// Path doesn't exist -- no problem.
|
|
//
|
|
return FALSE;
|
|
}
|
|
|
|
//
|
|
// Don't auto adjust
|
|
//
|
|
m_pHeader->RespondToColumnWidthChanges(FALSE);
|
|
|
|
CRect rc;
|
|
m_pHeader->GetClientRect(&rc);
|
|
|
|
CError err;
|
|
|
|
try
|
|
{
|
|
TCHAR buf[MAX_PATH];
|
|
DWORD count = MAX_PATH;
|
|
int nTotalWidth = 0;
|
|
|
|
err = rkUser.QueryValue(buf, g_szRegColumns, &count);
|
|
|
|
if (err.Succeeded() && lstrlen(buf) > 0)
|
|
{
|
|
LPTSTR lpstrValue = buf;
|
|
LPTSTR lpWidth = _tcstok(lpstrValue, g_szColValueSep);
|
|
|
|
for (int n = 0; n < QueryNumColumns(); ++n)
|
|
{
|
|
ASSERT_PTR(lpWidth);
|
|
|
|
if (lpWidth == NULL)
|
|
{
|
|
err = ERROR_INVALID_PARAMETER;
|
|
break;
|
|
}
|
|
|
|
//
|
|
// Sanity check
|
|
//
|
|
int nWidth = _ttoi(lpWidth);
|
|
if (nWidth <= 0 || (nTotalWidth + nWidth > rc.Width()))
|
|
{
|
|
ASSERT_MSG("column width invalid");
|
|
|
|
return FALSE;
|
|
}
|
|
|
|
nTotalWidth += nWidth;
|
|
|
|
VERIFY(SetColumnWidth(n, nWidth));
|
|
|
|
lpWidth = _tcstok(NULL, g_szColValueSep);
|
|
}
|
|
}
|
|
}
|
|
catch(CMemoryException * e)
|
|
{
|
|
err = ERROR_NOT_ENOUGH_MEMORY;
|
|
e->Delete();
|
|
}
|
|
//
|
|
// Turn auto-adjust back on
|
|
//
|
|
m_pHeader->RespondToColumnWidthChanges(TRUE);
|
|
|
|
// if (err.Win32Error() == ERROR_FILE_NOT_FOUND)
|
|
// {
|
|
// // No problem, it is first run. We will set defaults.
|
|
// return err;
|
|
// }
|
|
return err;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CHeaderListBox::DistributeColumns()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Proportion the column widths of over the entire width of the
|
|
header control while maintaining relative proportions.
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Obtain available width
|
|
//
|
|
ASSERT_PTR(m_pHeader);
|
|
|
|
CRect rc;
|
|
m_pHeader->GetClientRect(&rc);
|
|
|
|
//
|
|
// Get current total width
|
|
//
|
|
int nTotalWeight = 0;
|
|
int nCol;
|
|
for (nCol = 0; nCol < QueryNumColumns(); ++nCol)
|
|
{
|
|
nTotalWeight += QueryColumnWidth(nCol);
|
|
}
|
|
|
|
//
|
|
// And spread out the width, maintaining the same
|
|
// proportions
|
|
//
|
|
|
|
//
|
|
// Temporarily ignore changes
|
|
//
|
|
m_pHeader->RespondToColumnWidthChanges(FALSE);
|
|
int cColumns = QueryNumColumns();
|
|
|
|
for (nCol = 0; nCol < cColumns; ++nCol)
|
|
{
|
|
int nWidth = QueryColumnWidth(nCol);
|
|
nWidth = rc.Width() * nWidth / nTotalWeight;
|
|
VERIFY(SetColumnWidth(nCol, nWidth));
|
|
}
|
|
|
|
//
|
|
// Turn changes back on
|
|
//
|
|
m_pHeader->RespondToColumnWidthChanges(TRUE);
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CHeaderListBox::InsertColumn(
|
|
IN int nCol,
|
|
IN int nWeight,
|
|
IN UINT nStringID,
|
|
IN HINSTANCE hResInst
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Insert column. The width of the column is actually a relative
|
|
"weight" of the column which needs to be adjusted later. The
|
|
return value is the column number or -1 if the column is not inserted.
|
|
|
|
Arguments:
|
|
|
|
int nCol : Column number
|
|
int nWeight : Relative weight of column
|
|
UINT nStringID : Resource string ID
|
|
|
|
Return Value:
|
|
|
|
Index of the column, or -1 in case of failure
|
|
|
|
--*/
|
|
{
|
|
CString strColName;
|
|
HD_ITEM hdItem;
|
|
|
|
HINSTANCE hInst = AfxGetResourceHandle();
|
|
AfxSetResourceHandle(hResInst);
|
|
VERIFY(strColName.LoadString(nStringID));
|
|
AfxSetResourceHandle(hInst);
|
|
|
|
hdItem.mask = HDI_FORMAT | HDI_WIDTH | HDI_TEXT;
|
|
hdItem.fmt = HDF_STRING | HDF_LEFT;
|
|
hdItem.pszText = (LPTSTR)(LPCTSTR)strColName;
|
|
hdItem.cchTextMax = strColName.GetLength();
|
|
hdItem.cxy = nWeight;
|
|
|
|
return InsertHeaderItem(nCol, &hdItem);
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CHeaderListBox::OnCreate(
|
|
IN LPCREATESTRUCT lpCreateStruct
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Listbox is being created
|
|
|
|
Arguments:
|
|
|
|
LPCREATESTRUCT lpCreateStruct : Creation structure
|
|
|
|
Return Value:
|
|
|
|
0 for success, -1 for failure
|
|
|
|
--*/
|
|
{
|
|
if (CRMCListBox::OnCreate(lpCreateStruct) == -1)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
Initialize();
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CHeaderListBox::EnableWindow(
|
|
IN BOOL bEnable
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Enable/disable the control.
|
|
|
|
Arguments:
|
|
|
|
BOOL bEnable : TRUE to enable the control, FALSE to disable
|
|
|
|
Return Value:
|
|
|
|
Indicates the state before the EnableWindow member function was called.
|
|
The return value is nonzero if the window was previously disabled. The
|
|
return value is 0 if the window was previously enabled or an error
|
|
occurred.
|
|
|
|
--*/
|
|
{
|
|
if (m_pHeader)
|
|
{
|
|
m_pHeader->EnableWindow(bEnable);
|
|
}
|
|
|
|
return CRMCListBox::EnableWindow(bEnable);
|
|
}
|
|
|
|
|
|
|
|
BOOL
|
|
CHeaderListBox::ShowWindow(
|
|
IN int nCmdShow
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Show/hide the window
|
|
|
|
Arguments:
|
|
|
|
int nCmdShow : SW_ flag such as SW_SHOW or SW_HIDE
|
|
|
|
Return Value:
|
|
|
|
If the window was previously visible, the return value is TRUE. If the
|
|
window was previously hidden, the return value is FALSE.
|
|
|
|
--*/
|
|
{
|
|
if (m_pHeader)
|
|
{
|
|
m_pHeader->ShowWindow(nCmdShow);
|
|
}
|
|
|
|
return CRMCListBox::ShowWindow(nCmdShow);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CHeaderListBox::OnDestroy()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Handle destruction of the control
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Destroy optional header control
|
|
//
|
|
ASSERT_PTR(m_pHeader);
|
|
|
|
if (m_pHeader)
|
|
{
|
|
if (!m_strRegKey.IsEmpty())
|
|
{
|
|
//
|
|
// Try to write the current column sizes to the registry
|
|
//
|
|
CError err;
|
|
|
|
CRegKey rkUser;
|
|
|
|
rkUser.Create(HKEY_CURRENT_USER, m_strRegKey);
|
|
|
|
int nWidth;
|
|
TCHAR szValue[32];
|
|
CString strValue;
|
|
|
|
try
|
|
{
|
|
for (int n = 0; n < GetHeaderItemCount(); ++n)
|
|
{
|
|
if (n > 0)
|
|
{
|
|
//
|
|
// Put in field separator
|
|
//
|
|
strValue += g_szColValueSep;
|
|
}
|
|
|
|
nWidth = m_pHeader->GetColumnWidth(n);
|
|
strValue += ::_itot(nWidth, szValue, 10);
|
|
}
|
|
|
|
err = rkUser.SetValue(strValue, g_szRegColumns);
|
|
}
|
|
catch(CMemoryException * e)
|
|
{
|
|
err = ERROR_NOT_ENOUGH_MEMORY;
|
|
e->Delete();
|
|
}
|
|
|
|
err.MessageBoxOnFailure();
|
|
}
|
|
|
|
m_pHeader->DestroyWindow();
|
|
}
|
|
|
|
CRMCListBox::OnDestroy();
|
|
}
|
|
|
|
|
|
|
|
IMPLEMENT_DYNAMIC(CHeaderListBox, CRMCListBox);
|
|
|
|
|
|
|
|
CRMCComboBox::CRMCComboBox()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Constructor
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
: m_fInitialized(FALSE)
|
|
{
|
|
}
|
|
|
|
|
|
|
|
CRMCComboBox::~CRMCComboBox()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Destructor
|
|
|
|
Arguments:
|
|
|
|
N/A
|
|
|
|
Return Value:
|
|
|
|
N/A
|
|
|
|
--*/
|
|
{
|
|
}
|
|
|
|
|
|
|
|
//
|
|
// Message Map
|
|
//
|
|
BEGIN_MESSAGE_MAP(CRMCComboBox, CComboBox)
|
|
//{{AFX_MSG_MAP(CRMCComboBox)
|
|
ON_WM_CREATE()
|
|
//}}AFX_MSG_MAP
|
|
END_MESSAGE_MAP()
|
|
|
|
|
|
|
|
|
|
/* virtual */
|
|
BOOL
|
|
CRMCComboBox::Initialize()
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
This function should be called directly when subclassing an existing
|
|
combobox, otherwise OnCreate will take care of it.
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
TRUE for success, FALSE for failure
|
|
|
|
--*/
|
|
{
|
|
//
|
|
// Make sure we're only initialized once
|
|
//
|
|
if (m_fInitialized)
|
|
{
|
|
return TRUE;
|
|
}
|
|
|
|
//
|
|
// Ensure the base class knows our window
|
|
// handle
|
|
//
|
|
AttachWindow(this);
|
|
|
|
if (!CODLBox::Initialize())
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
m_fInitialized = TRUE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCComboBox::MeasureItem(
|
|
IN LPMEASUREITEMSTRUCT lpMIS
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
CComboBox override to ODL base class
|
|
|
|
Arguments:
|
|
|
|
LPMEASUREITEMSTRUCT lpMIS : Measure item structure
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
CODLBox::__MeasureItem(lpMIS);
|
|
}
|
|
|
|
|
|
|
|
void
|
|
CRMCComboBox::DrawItem(
|
|
IN LPDRAWITEMSTRUCT lpDIS
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
CListBox override to ODL base class
|
|
|
|
Arguments:
|
|
|
|
LPDRAWITEMSTRUCT lpDIS : Drawing item structure
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
CODLBox::__DrawItem(lpDIS);
|
|
}
|
|
|
|
|
|
|
|
/* virtual */
|
|
void
|
|
CRMCComboBox::DrawItemEx(
|
|
IN CRMCListBoxDrawStruct & dw
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Do-nothing extended draw function, which should
|
|
be provided by the derived class. This one will
|
|
ASSERT, and should never be called.
|
|
|
|
Arguments:
|
|
|
|
CRMCListBoxDrawStruct & dw : Draw Structure
|
|
|
|
Return Value:
|
|
|
|
None
|
|
|
|
--*/
|
|
{
|
|
dw;
|
|
ASSERT_MSG("Derived class did not provide DrawItemEx");
|
|
}
|
|
|
|
|
|
|
|
int
|
|
CRMCComboBox::OnCreate(
|
|
IN LPCREATESTRUCT lpCreateStruct
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Combo box is being created
|
|
|
|
Arguments:
|
|
|
|
LPCREATESTRUCT lpCreateStruct : Creation structure
|
|
|
|
Return Value:
|
|
|
|
-1 for failure, 0 for success
|
|
|
|
--*/
|
|
{
|
|
if (CComboBox::OnCreate(lpCreateStruct) == -1)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
Initialize();
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
|
|
/* virtual */
|
|
int
|
|
CRMCComboBox::__GetCount() const
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Provide CComboBox::GetCount() functionality to base class
|
|
|
|
Arguments:
|
|
|
|
None
|
|
|
|
Return Value:
|
|
|
|
Get the count of items in the combo box
|
|
|
|
--*/
|
|
{
|
|
return GetCount();
|
|
}
|
|
|
|
|
|
|
|
/* virtual */
|
|
int
|
|
CRMCComboBox::__SetItemHeight(
|
|
IN int nIndex,
|
|
IN UINT cyItemHeight
|
|
)
|
|
/*++
|
|
|
|
Routine Description:
|
|
|
|
Provide CListBox::SetItemHeight() functionality to base class.
|
|
|
|
Arguments:
|
|
|
|
int nIndex : Index of the item
|
|
UINT cyItemHeight : Height of the item
|
|
|
|
Return Value:
|
|
|
|
SetItemHeight return value.
|
|
|
|
--*/
|
|
{
|
|
return SetItemHeight(nIndex, cyItemHeight);
|
|
}
|
|
|
|
|
|
|
|
IMPLEMENT_DYNAMIC(CRMCComboBox,CComboBox);
|