This article describes how to add separator lines (single line or double line) to a list control derived from CListCtrl. This technique requires that the list control is owner drawn. This article also demonstrates how to prevent row selection in a list control.
How to do it?
Let’s assume that we have a class TVisualListCtrl derived from CListCtrl. Objects of this class correspond to list control resources (each list control must be ownerdrawn). In order to implement support for separator lines, we have to add several features to this class.
Step 1.
Add a public enum type containing the separator line identifiers and 2 public functions for inserting a separator line and checking whether the item is separator or not.
class TVisualCtrlList : public
CListCtrl {
public:
enum ESeparator {
SEPARATOR_SINGLE,
SEPARATOR_DOUBLE
};
……
public:
…..
BOOL IsSeparator(int row);
int InsertSeparator(int row, ESeparator separator = SEPARATOR_SINGLE);
};
Also create 2 constant strings (within a class or as local variables in a .cpp implementation file) which are inserted into the list control. These strings have a special meaning to the class implementation since they are actually aliases to separator lines.
const CString SingleSeparator = _T(“—“);
const CString DoubleSeparator = _T(“===”);
Now, the implementation of the above 2 public functions become the following:
BOOL TVisualListCtrl::IsSeparator(int index)
{
CString label = GetItemText(index,0);
if ((label == SingleSeparator) || (label == DoubleSeparator))
return TRUE;
return FALSE;
}
int TVisualListCtrl::InsertSeparator(int index, ESeparator separator)
{
switch (separator) {
case SEPARATOR_SINGLE: return InsertItem(index,SingleSeparator);
case SEPARATOR_DOUBLE: return InsertItem(index,DoubleSeparator);
}
return -1;
}
This step finalizes the interface of the TVisualListCtrl class regarding the separator lines. Now, let’s see how to implement the inner functionality.
Step 2.
Every list control derived class (for ownerdrawn list controls) has a member function:
void TVisualListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
This function is called each time an item in a list control needs to be drawn. This is were major modifications has to be performed. First, based on the argument, we have to find out the row number of the item being drawn, then we have to get the label of the item and check whether it is a separator or not.
int nItem = lpDrawItemStruct->itemID;
CString sLabel = GetItemText(nItem, 0);
BOOL bSeparator = IsSeparator(nItem);
So far so good. Now, if the item is not a separator, we have to draw it. This is discussed in details in several articles on the Codeguru site. However, if the item is a separator (bSeparator is TRUE), then we have to draw it (instead of drawing the item). Here is the code for drawing the separator.
CClientDC dc(this);
CRect rect;
GetClientRect(&rect);
int height = rcItem.bottom – rcItem.top;
height >>= 1;
CPen *oldpen, graypen(PS_SOLID,1,RGB(192,192,192));
CPen blackpen(PS_SOLID,1,RGB(0,0,0));
if (sLabel == SingleSeparator) {
oldpen = dc.SelectObject(&blackpen);
dc.MoveTo(rect.left,rcItem.top + height+1);
dc.LineTo(rect.right,rcItem.top + height+1);
dc.SelectObject(&graypen);
dc.MoveTo(rect.left,rcItem.top + height);
dc.LineTo(rect.right,rcItem.top + height);
} else {
oldpen = dc.SelectObject(&blackpen);
dc.MoveTo(rect.left,rcItem.top + height+2);
dc.LineTo(rect.right,rcItem.top + height+2);
dc.MoveTo(rect.left,rcItem.top + height-1);
dc.LineTo(rect.right,rcItem.top + height-1);
dc.SelectObject(&graypen);
dc.MoveTo(rect.left,rcItem.top + height+1);
dc.LineTo(rect.right,rcItem.top + height+1);
dc.MoveTo(rect.left,rcItem.top + height-2);
dc.LineTo(rect.right,rcItem.top + height-2);
}
dc.SelectObject(oldpen);
Be sure not to draw the highlight rectangle if the item to draw is a separator. This is important and will be expalined in the next step.
Step 3.
In the final step, we have to prevent the user to select the separator. In order to do this, we have to override the following messages sent to the list control.
- WM_KEYDOWN
- WM_KEYUP
- WM_LBUTONDOWN
Handle for WM_KEYDOWN handles the selection when the user pressed a character (default list control behaviour is to find the item whose label begins with the typed character). Here is the implementation:
void TVisualListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if ((nChar != SingleSeparator[0]) && (nChar != DoubleSeparator[0]))
CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}
Handle for WM_KEYUP handles the cursor movements (up, down, page up etc.). We have to prevent the user to select the separator and select previous or next item instead. Here is the implementation:
void TVisualListCtrl::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CListCtrl::OnKeyUp(nChar, nRepCnt, nFlags);
int index = GetNextItem(-1, LVNI_ALL | LVNI_SELECTED);
int newindex = index;
if (IsSeparator(index)) {
switch (nChar) {
case VK_DOWN:
case VK_END:
case VK_NEXT:
while (newindex < GetItemCount()-1) { newindex++; if (!IsSeparator(newindex))
break;
};
if (IsSeparator(newindex)) {
newindex = index;
while
(newindex > 0) {
newindex–;
if (!IsSeparator(newindex))
break;
};
}
SetCurSel(newindex);
EnsureVisible(newindex,TRUE);
break;
case VK_UP:
case VK_HOME:
case VK_PRIOR:
while (newindex > 0)
{
newindex–;
if (!IsSeparator(newindex))
break;
};
if (IsSeparator(newindex)) {
newindex = index;
while
(newindex < GetItemCount()-1) { newindex++; if (!IsSeparator(newindex))
break;
};
}
SetCurSel(newindex);
EnsureVisible(newindex,TRUE);
break;
};
}
}
Handle for WM_LBUTTONDOWN handles the selection via the mouse (click with the left mouse button). In this case, we have to prevent the user to select the separator (default handler is executed only if mouse is clicked on item other then separator). Here is the implementation:
void TVisualListCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
CPoint pt = point;
pt.x = 3;
int index = HitTest(pt);
if ((index != -1) && IsSeparator(index))
return;
CListCtrl::OnLButtonDown(nFlags, point);
}
Conclusion
Separator line in a list control offers a lot of possibilities since it is a visual separator between different sections of items. The only drawback is the height of the separator item – it has the same height as the regular item.
Full source code
Here is the complete source code of the TVisualListCtrl. This code is partly based on articles from the great Codeguru site. I am listing all the code here for convenience.
Include file:
class AFX_EXT_CLASS TVisualListCtrl : public CListCtrl {
public:
enum ESeparator {
SEPARATOR_SINGLE,
SEPARATOR_DOUBLE
};
enum EHighlight {
HIGHLIGHT_NORMAL,
HIGHLIGHT_ALLCOLUMNS,
HIGHLIGHT_ROW
};
protected:
int HighlightType; // One of EHighligh enums
BOOL VerticalGrid;
BOOL HorizontalGrid;
COLORREF ForegroundColor;
public:
TVisualListCtrl();
virtual ~TVisualListCtrl();
BOOL SetCurSel(int row);
void RepaintSelectedItems();
int SetHighlightType(EHighlight hilite);
void SetForegroundColor(COLORREF foreColor) { ForegroundColor = foreColor; }
void DrawVerticalGrid(BOOL onoff) { VerticalGrid = onoff; }
void DrawHorizontalGrid(BOOL onoff) { HorizontalGrid = onoff; }
BOOL IsSeparator(int index);
int InsertSeparator(int index, ESeparator separator);
public:
//{{AFX_VIRTUAL(TVisualListCtrl)
protected:
virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);
virtual BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult);
//}}AFX_VIRTUAL
protected:
//{{AFX_MSG(TVisualListCtrl)
afx_msg void OnPaint();
afx_msg void OnKillFocus(CWnd* pNewWnd);
afx_msg void OnSetFocus(CWnd* pOldWnd);
afx_msg void OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
Source code:
const CString SingleSeparator(_T(‘-‘),3);
const CString DoubleSeparator(_T(‘=’),3);
BEGIN_MESSAGE_MAP(TVisualListCtrl, CListCtrl)
//{{AFX_MSG_MAP(TVisualListCtrl)
ON_WM_PAINT()
ON_WM_KILLFOCUS()
ON_WM_SETFOCUS()
ON_WM_KEYUP()
ON_WM_LBUTTONDOWN()
ON_WM_KEYDOWN()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
TVisualListCtrl::TVisualListCtrl()
{
HighlightType = HIGHLIGHT_ALLCOLUMNS;
VerticalGrid = FALSE;
HorizontalGrid = FALSE;
ForegroundColor = 0;
}
TVisualListCtrl::~TVisualListCtrl()
{
}
BOOL TVisualListCtrl::IsSeparator(int index)
{
CString label = GetItemText(index,0);
if ((label == SingleSeparator) || (label == DoubleSeparator))
return TRUE;
return FALSE;
}
int TVisualListCtrl::InsertSeparator(int index, ESeparator separator)
{
switch (separator) {
case SEPARATOR_SINGLE: return InsertItem(index,SingleSeparator);
case SEPARATOR_DOUBLE: return InsertItem(index,DoubleSeparator);
}
return -1;
}
// Unless LVS_SHOWSELALWAYS is set for list control, new selection is visible
// only when the list control receives a focus
BOOL TVisualListCtrl::SetCurSel(int row)
{
return SetItemState(row,
LVIS_SELECTED|LVIS_FOCUSED,LVIS_SELECTED|LVIS_FOCUSED);
}
void TVisualListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
CDC* pDC = CDC::FromHandle(lpDrawItemStruct->hDC);
CRect rcItem(lpDrawItemStruct->rcItem);
int nItem = lpDrawItemStruct->itemID;
CImageList* pImageList;
// Save dc state
int nSavedDC = pDC->SaveDC();
// Get item image and state info
LV_ITEM lvi;
lvi.mask = LVIF_IMAGE | LVIF_STATE;
lvi.iItem = nItem;
lvi.iSubItem = 0;
lvi.stateMask = 0xFFFF;
// get all state flags
GetItem(&lvi);
// Should the item be highlighted
BOOL bHighlight =
((lvi.state & LVIS_DROPHILITED) ||
((lvi.state & LVIS_SELECTED)
&& ((GetFocus()
== this) || (GetStyle() & LVS_SHOWSELALWAYS))));
// Get rectangles for drawing
CRect rcBounds, rcLabel, rcIcon;
GetItemRect(nItem, rcBounds, LVIR_BOUNDS);
GetItemRect(nItem, rcLabel, LVIR_LABEL);
GetItemRect(nItem, rcIcon, LVIR_ICON);
CRect rcCol(rcBounds);
CString sLabel = GetItemText(nItem, 0);
BOOL bSeparator = IsSeparator(nItem);
// Labels are offset by a certain amount
// This offset is related to the width of a space character
int offset = pDC->GetTextExtent(_T(" "), 1).cx*2;
CRect rcHighlight;
CRect rcWnd;
int nExt;
switch (HighlightType) {
case HIGHLIGHT_NORMAL:
nExt = pDC->GetOutputTextExtent(sLabel).cx + offset;
rcHighlight = rcLabel;
if( rcLabel.left + nExt < rcLabel.right ) rcHighlight.right = rcLabel.left + nExt; break;
case HIGHLIGHT_ALLCOLUMNS:
rcHighlight = rcBounds;
rcHighlight.left = rcLabel.left;
break;
case HIGHLIGHT_ROW:
GetClientRect(&rcWnd);
rcHighlight = rcBounds;
rcHighlight.left = rcLabel.left;
rcHighlight.right = rcWnd.right;
break;
default:
rcHighlight = rcLabel;
}
// Draw the background color
if (bHighlight) {
if (!bSeparator) {
pDC->SetTextColor(::GetSysColor(COLOR_HIGHLIGHTTEXT));
pDC->SetBkColor(::GetSysColor(COLOR_HIGHLIGHT));
pDC->FillRect(rcHighlight,
&CBrush(::GetSysColor(COLOR_HIGHLIGHT)));
}
} else {
pDC->FillRect(rcHighlight,
&CBrush(::GetSysColor(COLOR_WINDOW)));
if (ForegroundColor != 0)
pDC->SetTextColor(ForegroundColor);
}
// Set clip region
rcCol.right = rcCol.left + GetColumnWidth(0);
CRgn rgn;
rgn.CreateRectRgnIndirect(&rcCol);
pDC->SelectClipRgn(&rgn);
rgn.DeleteObject();
if (!bSeparator) {
// Draw state icon
if (lvi.state & LVIS_STATEIMAGEMASK) {
int nImage =
((lvi.state & LVIS_STATEIMAGEMASK)>>12) – 1;
pImageList = GetImageList(LVSIL_STATE);
if (pImageList) {
pImageList->Draw(pDC,
nImage,CPoint(rcCol.left, rcCol.top), ILD_TRANSPARENT);
}
}
// Draw normal and overlay icon
pImageList = GetImageList(LVSIL_SMALL);
if (pImageList) {
UINT nOvlImageMask = lvi.state & LVIS_OVERLAYMASK;
pImageList->Draw(pDC,
lvi.iImage, CPoint(rcIcon.left, rcIcon.top),
(bHighlight?ILD_BLEND50:0)
| ILD_TRANSPARENT | nOvlImageMask );
}
// Draw item label – Column 0
rcLabel.left += offset/2;
rcLabel.right -= offset;
pDC->DrawText(sLabel,-1,rcLabel,DT_LEFT |
DT_SINGLELINE | DT_NOPREFIX | DT_NOCLIP |
DT_VCENTER | DT_END_ELLIPSIS);
// Draw labels for remaining columns
LV_COLUMN lvc;
lvc.mask = LVCF_FMT | LVCF_WIDTH;
if (HighlightType == 0) {
// Highlight only first column
pDC->SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
pDC->SetBkColor(::GetSysColor(COLOR_WINDOW));
}
rcBounds.right = rcHighlight.right >
rcBounds.right ? rcHighlight.right : rcBounds.right;
rgn.CreateRectRgnIndirect(&rcBounds);
pDC->SelectClipRgn(&rgn);
for (int nColumn = 1; GetColumn(nColumn, &lvc); nColumn++) {
rcCol.left = rcCol.right;
rcCol.right += lvc.cx;
// Draw the background if needed
if (HighlightType == HIGHLIGHT_NORMAL )
pDC->FillRect(rcCol,
&CBrush(::GetSysColor(COLOR_WINDOW)));
sLabel = GetItemText(nItem, nColumn);
if (sLabel.GetLength() == 0)
continue;
// Get the text justification
UINT nJustify = DT_LEFT;
switch (lvc.fmt & LVCFMT_JUSTIFYMASK) {
case LVCFMT_RIGHT:
nJustify = DT_RIGHT;
break;
case LVCFMT_CENTER:
nJustify = DT_CENTER;
break;
default:
break;
}
rcLabel = rcCol;
rcLabel.left += offset;
rcLabel.right -= offset;
pDC->DrawText(sLabel,
-1, rcLabel, nJustify | DT_SINGLELINE |
DT_NOPREFIX | DT_VCENTER | DT_END_ELLIPSIS);
}
// Draw focus rectangle if item has focus
if (lvi.state & LVIS_FOCUSED && (GetFocus() == this))
pDC->DrawFocusRect(rcHighlight);
} else {
CClientDC dc(this);
CRect rect;
GetClientRect(&rect);
int height = rcItem.bottom – rcItem.top;
height >>= 1;
CPen *oldpen, graypen(PS_SOLID,1,RGB(192,192,192));
CPen blackpen(PS_SOLID,1,RGB(0,0,0));
if (sLabel == SingleSeparator) {
oldpen = dc.SelectObject(&blackpen);
dc.MoveTo(rect.left,rcItem.top + height+1);
dc.LineTo(rect.right,rcItem.top + height+1);
dc.SelectObject(&graypen);
dc.MoveTo(rect.left,rcItem.top + height);
dc.LineTo(rect.right,rcItem.top + height);
} else {
oldpen = dc.SelectObject(&blackpen);
dc.MoveTo(rect.left,rcItem.top + height+2);
dc.LineTo(rect.right,rcItem.top + height+2);
dc.MoveTo(rect.left,rcItem.top + height-1);
dc.LineTo(rect.right,rcItem.top + height-1);
dc.SelectObject(&graypen);
dc.MoveTo(rect.left,rcItem.top + height+1);
dc.LineTo(rect.right,rcItem.top + height+1);
dc.MoveTo(rect.left,rcItem.top + height-2);
dc.LineTo(rect.right,rcItem.top + height-2);
}
dc.SelectObject(oldpen);
}
// Restore dc
pDC->RestoreDC( nSavedDC );
}
void TVisualListCtrl::RepaintSelectedItems()
{
CRect rcBounds, rcLabel;
// Invalidate focused item so it can repaint
int nItem = GetNextItem(-1, LVNI_FOCUSED);
if (nItem != -1) {
GetItemRect(nItem, rcBounds, LVIR_BOUNDS);
GetItemRect(nItem, rcLabel, LVIR_LABEL);
rcBounds.left = rcLabel.left;
InvalidateRect(rcBounds, FALSE);
}
// Invalidate selected items depending on LVS_SHOWSELALWAYS
if (!(GetStyle() & LVS_SHOWSELALWAYS)) {
for (nItem = GetNextItem(-1, LVNI_SELECTED); nItem != -1;
nItem = GetNextItem(nItem, LVNI_SELECTED))
{
GetItemRect(nItem, rcBounds, LVIR_BOUNDS);
GetItemRect(nItem, rcLabel, LVIR_LABEL);
rcBounds.left = rcLabel.left;
InvalidateRect(rcBounds, FALSE);
}
}
UpdateWindow();
}
void TVisualListCtrl::OnPaint()
{
CListCtrl::OnPaint();
// in full row select mode, we need to extend the clipping region
// so we can paint a selection all the way to the right
if (HighlightType == HIGHLIGHT_ROW && (GetStyle() &
LVS_TYPEMASK) == LVS_REPORT ) {
CRect rcBounds;
GetItemRect(0, rcBounds, LVIR_BOUNDS);
CRect rcClient;
GetClientRect(&rcClient);
if (rcBounds.right < rcClient.right) { CPaintDC dc(this);
CRect rcClip;
dc.GetClipBox(rcClip);
rcClip.left = min(rcBounds.right-1, rcClip.left);
rcClip.right = rcClient.right;
InvalidateRect(rcClip, FALSE);
}
}
// Paint vertical and horizontal lines
// Draw the lines only for LVS_REPORT mode
if( (GetStyle() & LVS_TYPEMASK) == LVS_REPORT ) {
// Get the number of columns
CClientDC dc(this );
CPen pen(PS_SOLID,1,RGB(192,192,192));
CPen *oldpen = dc.SelectObject(&pen);
RECT rect;
CHeaderCtrl* pHeader = (CHeaderCtrl*)GetDlgItem(0);
int nColumnCount = pHeader->GetItemCount();
// The bottom of the header corresponds to the top of the line
pHeader->GetClientRect(&rect);
int top = rect.bottom;
// Now get the client rect so we know the line length and when to stop
GetClientRect(&rect);
if (VerticalGrid) {
// The border of the column is offset by the horz scroll
int borderx = 0 – GetScrollPos(SB_HORZ);
for (int i = 0; i < nColumnCount; i++) { // Get the next border
borderx += GetColumnWidth(i);
//if next border is outside client area, break out
if ( borderx >= rect.right )
break;
// Draw the line
dc.MoveTo(borderx-1,top);
dc.LineTo(borderx-1,
rect.bottom );
}
}
// Draw the horizontal grid lines
if (HorizontalGrid) {
// First get the height
if (GetItemRect( 0,
&rect, LVIR_BOUNDS)) {
int height = rect.bottom – rect.top;
GetClientRect(&rect);
int width = rect.right;
for (int i = 1; i <= GetCountPerPage(); i++) { dc.MoveTo(0, top + height*i); dc.LineTo(width, top + height*i); } } } dc.SelectObject(oldpen); } //CPaintDC dc(this); // device context for painting
}
void TVisualListCtrl::OnKillFocus(CWnd* pNewWnd)
{
CListCtrl::OnKillFocus(pNewWnd);
// check if we are losing focus to label edit box
if (pNewWnd != NULL && pNewWnd->GetParent() == this)
return;
// repaint items that should change appearance
if ((GetStyle() & LVS_TYPEMASK) == LVS_REPORT)
RepaintSelectedItems();
}
void TVisualListCtrl::OnSetFocus(CWnd* pOldWnd)
{
CListCtrl::OnSetFocus(pOldWnd);
// check if we are getting focus from label edit box
if (pOldWnd!=NULL && pOldWnd->GetParent()==this)
return;
// repaint items that should change appearance
if ((GetStyle() & LVS_TYPEMASK)==LVS_REPORT)
RepaintSelectedItems();
}
int TVisualListCtrl::SetHighlightType(EHighlight hilite)
{
int oldhilite = HighlightType;
if (hilite <= HIGHLIGHT_ROW ) { HighlightType = hilite; Invalidate(); } return oldhilite;
}
BOOL TVisualListCtrl::OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
HD_NOTIFY *pHD = (HD_NOTIFY*)lParam;
if ((pHD->hdr.code == HDN_ITEMCHANGINGA || pHD->hdr.code ==
HDN_ITEMCHANGINGW) &&
(GetStyle() & LVS_TYPEMASK) == LVS_REPORT)
{
// Invalidate empty bottom part of control to force erase the previous
// position of column
int nBottom, nLastItem = GetItemCount()-1;
if (nLastItem < 0) { // List is empty : invalidate whole client rect
nBottom = 0;
} else {
// Get Y position of bottom of list (last item)
RECT ItemRect;
GetItemRect(nLastItem,&ItemRect,LVIR_BOUNDS);
nBottom = ItemRect.bottom;
}
RECT rect;
GetClientRect(&rect);
if (nBottom < rect.bottom) { // Set top of rect as bottom of list (last item) : rect = empty part of list
rect.top = nBottom;
InvalidateRect(&rect);
}
// NB: We must go on with default processing.
}
*pResult = 0;
return CListCtrl::OnNotify(wParam, lParam, pResult);
}
void TVisualListCtrl::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
if ((nChar != SingleSeparator[0]) && (nChar != DoubleSeparator[0]))
CListCtrl::OnKeyDown(nChar, nRepCnt, nFlags);
}
void TVisualListCtrl::OnKeyUp(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CListCtrl::OnKeyUp(nChar, nRepCnt, nFlags);
int index = GetNextItem(-1, LVNI_ALL | LVNI_SELECTED);
int newindex = index;
if (IsSeparator(index)) {
switch (nChar) {
case VK_DOWN:
case VK_END:
case VK_NEXT:
while (newindex < GetItemCount()-1) { newindex++; if (!IsSeparator(newindex))
break;
};
if (IsSeparator(newindex)) {
newindex = index;
while (newindex > 0) {
newindex–;
if (!IsSeparator(newindex))
break;
};
}
SetCurSel(newindex);
EnsureVisible(newindex,TRUE);
break;
case VK_UP:
case VK_HOME:
case VK_PRIOR:
while (newindex > 0)
{
newindex–;
if (!IsSeparator(newindex))
break;
};
if (IsSeparator(newindex)) {
newindex = index;
while (newindex < GetItemCount()-1) { newindex++; if (!IsSeparator(newindex))
break;
};
}
SetCurSel(newindex);
EnsureVisible(newindex,TRUE);
break;
};
}
}
void TVisualListCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
CPoint pt = point;
pt.x = 3;
int index = HitTest(pt);
if ((index != -1) && IsSeparator(index))
return;
CListCtrl::OnLButtonDown(nFlags, point);
}
Hope you find this useful !
Date Posted: 5th October 1998