Monday, May 03, 2004

 

A revised version of the Outlook98 bar-like control

An Outlook98 bar-like control
Iuri Apollonio
December 5, 1998
http://www.codeguru.com/Cpp/controls/controls/article.php/c2155/

OutBar98 is a well known MFC Control, which was published in CodeGuru on December 5, 1998. However, Iuri Apollonio disappeared from CodeGuru soon after that. Here are some modifications that I adopted. Thanks for Iuri Apollonio and all the guys that gave comments.

1. Use 256 color Icon

m_imagelist.Create(64, 64, ILC_COLOR32, 0, 6);
CBitmap bitmap;
bitmap.LoadBitmap(IDB_BITMAP);
m_imagelistCaptureType.Add(&bitmap, RGB(0, 0, 0));

2. Adding a simple mouse over message for items

When using the Outbar control for a program i wanted a way to know if the user is hovering over a item in one of the folders. To solve this i came up with a very simple solution (probable not the best one).

In GfxOutBarCtrl.h add:
#define NM_OB_ITEMHOVER 6

And then in GfxOutBarCtrl.cpp at the end of HighlightItem add:
GetOwner()->SendMessage(WM_OUTBAR_NOTIFY,NB_OB_ITEMHOVER,index);
Then in your program add to the WM_OUTBAR_NOTIFY handler:
case NM_OB_ITEMHOVER
// Cast lParam to get item which mouse is hovering over
{
// Do your stuff here
}
return 0;

I hope this is of help to people. It is probably not the best way to
do it but it works for me.

Cheers

Submitted By: Richard

3. Adding Right-click notification

To add right-click notification, so I can use my own menu
instead of getting the control to do its own context menu,
I have modified the code as below:

Add the code to GfxOutBarCtrl.h:

#define NM_OB_ITEMRCLICK 8

Add a new member (bFolder) to OUTBAR_INFO, so it looks like:
typedef struct OUTBAR_INFO
{
int index;
const char * cText;
int iDragFrom;
int iDragTo;
bool bFolder;
}*LPOUTBARINFO;

then add a handler for WM_RBUTTONUP:

void CGfxOutBarCtrl::OnRButtonUp(UINT nFlags, CPoint point)
{
OUTBAR_INFO obi = { 0 };
int ht = HitTestEx(point, obi.index);
if (ht != htItem)
{
obi.bFolder = TRUE;
if (ht != htFolder)
{
obi.index = -1;
}
}
LRESULT lResult = GetOwner()->SendMessage(WM_OUTBAR_NOTIFY, NM_OB_ITEMRCLICK, (LPARAM)&obi);

if (lResult == 0)
{
// add the code from RButtonDown here
}

CWnd::OnRButtonUp(nFlags, point);
}

The OnRButtonDown handler can now be removed.

The owner window can now add a handler for the notification
and show its own context menu.

To prevent the bar using its own menu, the handler should return non-zero.

Submitted By: Paul S. Vickery

4. Adding tooltips

If you want to display tooltips for items in CGfxOutBarCtrl,
you only need to derive your class and add 2 functions:

1) Create your own class COutBarCtrlEx, derived from CGfxOutBarCtr;
2) Add next 2 declarations:
virtual int OnToolHitTest(CPoint point, TOOLINFO * pTI)const;
afx_msg BOOL OnToolTipText( UINT id, NMHDR * pNMHDR, RESULT * pResult );

3)Add two lines into Message map of your class
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnToolTipText)
ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnToolTipText)

4)Add 2 definitions:
/* OnToolTipText() well described in codeguru article by Stuart Carter & Kory Becker about CFileDropListCtrl. I simply copy & paste it. */
BOOL COutBarCtrlEx::OnToolTipText(UINT id, NMHDR *pNMHDR, LRESULT *pResult)
{
// need to handle both ANSI and UNICODE versions of the message
TOOLTIPTEXTA* pTTTA = (TOOLTIPTEXTA*)pNMHDR;
TOOLTIPTEXTW* pTTTW = (TOOLTIPTEXTW*)pNMHDR;
CString strTipText;
UINT nID = pNMHDR->idFrom;

if( nID-- == 0 ) // Notification in NT from automatically
return FALSE; // created tooltip

//int row = ((nID-1) >> 10) & 0x3fffff ;
//int col = (nID-1) & 0x3ff;
int folder = nID >> 8;
int index = nID & 0xFF;


// Use Item's name as the tool tip. Change this for something different.
// Like use its file size, etc.
strTipText = GetItemText( index );

#ifndef _UNICODE
if (pNMHDR->code == TTN_NEEDTEXTA)
lstrcpyn(pTTTA->szText, strTipText, 80);
else
_mbstowcsz(pTTTW->szText, strTipText, 80);
#else
if (pNMHDR->code == TTN_NEEDTEXTA)
_wcstombsz(pTTTA->szText, strTipText, 80);
else
lstrcpyn(pTTTW->szText, strTipText, 80);
#endif
*pResult = 0;

return TRUE; // message was handled
}

/* What about OnToolHitTest(), you may define it like this: */
int COutBarCtrlEx::OnToolHitTest(CPoint point, TOOLINFO *pTI) const
{
int ht, index =0;

COutBarCtrlEx *pBar = const_cast(this);
ht = pBar->HitTestEx(point,index);

if (ht != htItem)
{
return -1;
}

int folder = iSelFolder;

CRect rect;
pBar->GetItemRect(folder,index,rect);

pTI->hwnd = m_hWnd;
pTI->uId = (UINT)(folder <<8) | (index&0xFF) + 1;
pTI->lpszText = LPSTR_TEXTCALLBACK;
pTI->rect = rect;

return pTI->uId;
}

5) Now you can use your own class with tooltips instead of CGfxOutBarCtrl. For this just replace declaration of CGfxOutBarCtrl object with COutBarCtrlEx in your app.

Submitted By: Kirill Klementiev

5. Getting notification of items/folders being deleted

In order to simplify knowing what action to perform when
the user clicks on an item, I add item data to each item,
which is then retrieved when I need to carry out, or query,
the action.

The data is new'd/malloc'd, so needs freeing when an item
or folder is deleted, and also when the control is destroyed.

I have added notification of deletion to the OutBar class
so I can free any allocated data when needed.

To add this functionality, add the following notification
codes to GfxOutBarCtrl.h (use different numbers if you have
already added some codes here):

#define NM_OB_DELETEITEM 6
#define NM_OB_DELETEFOLDER 7

To get the notifications sent, add the following code to
CGfxOutBarCtrl::RemoveFolder:

CBarFolder * pbf = (CBarFolder*)arFolder.GetAt(index);
ASSERT(pbf);

// new code start
// PSV: tell owner of deletion of each item
int nSelFolderCur = iSelFolder; // save current selected folder
iSelFolder = index; // temporarily set sel folder to folder being deleted
int nItems = pbf->arItems.GetSize();
for (int item = 0; item < nItems; item++)
GetOwner()->SendMessage(WM_OUTBAR_NOTIFY, NM_OB_DELETEITEM, (LPARAM)item);
iSelFolder = nSelFolderCur; // restore selected folder
// new code end
if (pbf->pChild != NULL)
...

Invalidate();
// new code start
// PSV: tell owner of deletion
GetOwner()->SendMessage(WM_OUTBAR_NOTIFY, NM_OB_DELETEFOLDER, (LPARAM)index);
// new code end

and add to CGfxOutBarCtrl::RemoveItem:

if (IsValidItem(index))
{
// new code start
// PSV: tell owner of impending item deletion
GetOwner()->SendMessage(WM_OUTBAR_NOTIFY, NM_OB_DELETEITEM, (LPARAM)index);
// new code end
CBarItem * i = (CBarItem *) pbf->arItems.GetAt(index);
delete i;

That caters for getting a notication when an item/folder is
deleted. Add the next code to make sure it happens when the
control is destroyed. Add a new message handler for WM_DESTROY as shown below:

void CGfxOutBarCtrl::OnDestroy()
{
// tell the owner that each folder, and each of its items is deing destroyed
for (int t = 0; t < arFolder.GetSize(); t++)
{
iSelFolder = t;
CBarFolder* pbf = (CBarFolder*) arFolder.GetAt(t);
if (pbf != NULL)
{
int nItems = pbf->arItems.GetSize();
for (int index = 0; index < nItems; index++)
GetOwner()->SendMessage(WM_OUTBAR_NOTIFY, NM_OB_DELETEITEM, (LPARAM)index);
GetOwner()->SendMessage(WM_OUTBAR_NOTIFY, NM_OB_DELETEFOLDER, (LPARAM)t);
}
}

CWnd::OnDestroy();
}

Submitted By: Paul S. Vickery

6. How to open a modal dialog from the OnOutbarNotify funtion

Since may have written to me reporting trouble when trying to execute a modal dialog box from within the OnOutbarNotify function, here's the trick;
- add ReleaseCapture(); before opening your dialog

The trouble is related to how the bar control capture the mouse to get the hoover look; if the control has capture (as is always the case in this function), no modal will be correctly executed until you release the mouse capture, with the function above.

Enjoy!

Submitted By: Iuri

7. Removing Folder Error

First, we have

///////////////////////////////////////////
void CGfxOutBarCtrl::RemoveFolder(const int index)
{
ASSERT(index >= 0 && index < GetFolderCount());

CBarFolder * pbf = (CBarFolder *)
arFolder.GetAt(index);
if (pbf->pChild != NULL)
{
pbf->pChild->DestroyWindow() ;
pbf->pChild = NULL;
} delete pbf;
arFolder.RemoveAt(index);
if (iSelFolder >= index) iSelFolder = index - 1;
if (iSelFolder < 0 && GetFolderCount() > 0)
iSelFolder = 0;
if (iSelFolder >= 0)
SetSelFolder(iSelFolder);
Invalidate();
}
////////////////////////////////////////////

Submitted By: BokSoo Yun

Then we have the following change

Still a bug when deleting last folder

If the last folder is deleted, by right-clicking and
selecting 'Remove', then the control asserts when trying
to redraw. This is because the painting code is trying to
draw the last highlighted folder, as set by the class's
member variable iLastFolderHighLighted. This is set to the
last folder, which has just been deleted, and is therefore
invalid.

The solution to this is to add the line to BokSoo Yun's
code just before the Invalidate(); thusly:

iLastFolderHighlighted = -1;

and to add, to CGfxOutBarCtrl::OnPaint(), the following test to see if there any any folders to draw:

DrawFolder(pDC, t, frc, false);
}
if (!GetFolderChild() && max > 0) // <-- modified code
{
int f,l;

Submitted By: Paul S. Vickery

8. Removing Folder Error

///////////////////////////////////////////
void CGfxOutBarCtrl::RemoveFolder(const int index)
{
ASSERT(index >= 0 && index < GetFolderCount());

CBarFolder * pbf = (CBarFolder *)
arFolder.GetAt(index);
if (pbf->pChild != NULL)
{
pbf->pChild->DestroyWindow() ;
pbf->pChild = NULL;
} delete pbf;
arFolder.RemoveAt(index);
if (iSelFolder >= index) iSelFolder = index - 1;
if (iSelFolder < 0 && GetFolderCount() > 0)
iSelFolder = 0;
if (iSelFolder >= 0)
SetSelFolder(iSelFolder);
Invalidate();
}
////////////////////////////////////////////

Submitted By: BokSoo Yun

9. Remove Flicker

// non flickering painting fix
IMAGEINFO ii;
ima->GetImageInfo(pi->iImageIndex, &ii);
CSize szImage = CRect(ii.rcImage).Size();
CPoint pt;

if (IsSmallIconView())
{
pt.x = rc.left + 2;
pt.y = rc.top + (rc.Height() - szImage.cy) / 2;
}
else
{
pt.x = rc.left + (rc.Width() - szImage.cx) / 2;
pt.y = rc.top;
}

CRect rcBck(pt.x-1, pt.y-1, pt.x + szImage.cx+2, pt.y + szImage.cy+2);

/* PREPARE THE MEMORY DC */
CClientDC dc(this);
HDC hMemDC = CreateCompatibleDC(dc);
HBITMAP hBmp = CreateCompatibleBitmap(dc, rcBck.right - rcBck.left, rcBck.bottom - rcBck.top );
SelectObject(hMemDC, GetStockObject(DEFAULT_GUI_FONT));
SelectObject(hMemDC, hBmp);
SetViewportOrgEx(hMemDC, rcBck.left * -1, rcBck.top * -1, NULL);

/* FILL THE DC WITH THE BACKGROUND COLOUR TO REMOVE THE OLD ICON POSITION */
CBrush brushFill( crBackGroundColor );
FillRect(hMemDC, &rcBck, (HBRUSH)brushFill.m_hObject);

/* DRAW THE ICON AT THE NEW POSITION */
pt.x += xoffset;
pt.y += yoffset;
ima->Draw(CDC::FromHandle(hMemDC), pi->iImageIndex, pt, ILD_NORMAL);


/* APPLY THE MEMORY DC TO THE ORIGINAL DC */
BitBlt(dc, rcBck.left, rcBck.top, rcBck.right - rcBck.left,
rcBck.bottom - rcBck.top, hMemDC, rcBck.left, rcBck.top, SRCCOPY);

(按:I forget the name of the author who proposed this modification)

10. Forbid a Folder

int CCuteFolderBarCtrl::HitTestEx(const CPoint & point, int &index)
{
if (bUpArrow && rcUpArrow.PtInRect(point)) return htUpScroll;
if (bDownArrow && rcDownArrow.PtInRect(point)) return htDownScroll;

int max = arFolder.GetSize(), t;

if (max <= 0) // prevent assertion failure on deleting last folder
return htNothing;

// Add this to enable / disable
CBarFolder * pbf = (CBarFolder *) arFolder.GetAt(iSelFolder);
if (pbf->bEnabled == false) // if disabled, then silently return
return htNothing;

11. Order of drawing items

you need to draw the scollbutton finally. otherwise, it may be covered by the labels.

12. Some other comments

While this is a great control, it differs from Outlook in various ways:

1. Outlook displays folder labels centrally, but uses an ellipsis
on the end of the line if the label is too wide.

This can be acheived by adding DT_END_ELLIPSIS to the flags
specified to DrawText in CGfxOutBarCtrl::DrawFolder thusly:
pDC->DrawText(CString(pbf->cName), rect, DT_CENTER|DT_VCENTER|DT_SINGLELINE|DT_END_ELLIPSIS);

2. Editing the folder labels in Outlook, the text is left-justified,
and the edit control is single-line, and scrolls horizontally.
This can be acheived by altering the line where the in-place
edit control is created to read:
pEdit->Create(WS_CHILD|WS_VISIBLE|ES_AUTOHSCROLL,rc,this,1);

3. Outlook draws its item labels (in large icon view) no more
than two lines high. I'm still working on a fix for this - watch this space!

4. Switching between large and small icons is only on the
current folder, so one folder can show as small icons,
while another shows as large icons. This can be acheived by
the following changes:

Add a new bSmallIcons member to class CBarFolder:

CWnd * pChild;
bool bSmallIcons; // <-- new line
};

this holds whether the folder is showing in small icon view

Replace the CGfxOutBarCtrl::IsSmallIconView/SetSmallIconView
function definitions with the functions below:

bool CGfxOutBarCtrl::IsSmallIconView(const int iFolder/*=-1*/) const
{
int folder = iFolder;
if (GetFolderCount() <= 0 || folder == -1)
return dwFlags&fSmallIcon;
CBarFolder* pbf = (CBarFolder*)arFolder.GetAt(folder);
return pbf->bSmallIcons;
}

void CGfxOutBarCtrl::SetSmallIconView(const bool bSet, const int iFolder/*=-1*/)
{
iFirstItem = 0;

if (iFolder != -1)
{
if (iFolder >= 0 && iFolder < GetFolderCount())
{
CBarFolder* pbf = (CBarFolder*)arFolder.GetAt(iFolder);
pbf->bSmallIcons = bSet;
}
}
else
{
// do all current folders, and set flag so new folders
// have the chosen style
int nFolders = GetFolderCount();
for (int i = 0; i < nFolders; i++)
{
CBarFolder* pbf = (CBarFolder*)arFolder.GetAt(i);
pbf->bSmallIcons = bSet;
}
if (bSet && ! IsSmallIconView())
dwFlags |= fSmallIcon;
else if (! bSet && IsSmallIconView())
dwFlags &= ~fSmallIcon;
}
CRect rc;
GetInsideRect(rc);
InvalidateRect(rc, false);
}

and replace the same functions' declarations with:
void SetSmallIconView(const bool bSet, const int iFolder = -1);
bool IsSmallIconView(const int iFolder = -1) const;

Insert a line into CGfxOutBarCtrl::AddFolder, to set the new
folder's view style based on the current global setting:

arFolder.Add((void *)pbf);
pbf->bSmallIcons = dwFlags & fSmallIcon; // <-- new line

return arFolder.GetSize() - 1;

Modify both calls to SetSmallIconView (found in OnGfxLargeIcon
and OnGfxSmallIcon) to take an extra argument of iSelFolder.

Now the tricky bit! - modify all calls to IsSmallIconView()
in GfxOutBarCtrl.cpp to take an argument of the folder to
check. This will be the folder variable used near the function
call (iSelFolder or iFolder). The only calls to the function
which take no arguments should be inside SetSmallIconView() (2 occurances)

Calls to Is/SetSmallIconView outside of GfxOutBarCtrl will
continue to behave exactly as previously, unless the app is
modified to pass in a folder index.

Submitted By: Paul S. Vickery

List of bugs I found
1) When I tried to scroll the iconic view with mouse wheel the program crashed.(My mouse has 4 button and 2 wheels.)

2) The old name stays at the background while editing the labels in small icon mode.(I fixed it and added some code to make the editbox window as a overlapped child of main frame to prevent the clipping at right of outbar and to defaultly select the whole text wgich is the most usual case).

3) Removing the actual folder causes Displayal error.But removing inactive bars correctly works So this problem can be solved by activating the next folder (or the previous if the active folder is the last one) and then removing the folder.

4) The label edit boxes aren't multiline. Or maybe the multiline code doesn't work. I couldn't examine it.

5) Item dragging isn't smart. It's possible to drag an item to its own place. And there are some more mistakes that I can't remember.

I will try to correct these bugs and then send it to the site.

The code is very readable but I think can be shortened without reducing the readability. And It's so strange to see bugs at top level while other detailed levels are completely mistakeless.

Thanks very much to the programmer whom I don't know his name for sharing his work with us.

Submitted By: Aykut KILIÇ

OutBar98 inspired several other controls including

CXTOutlookBar
http://www.codejock.com/developer/article03.asp

The CXTOutlookBar class is a CListBox derived class that implements a Outlook style control. The CXTPagerCtrl class is used to contain and scroll the CXTOutlookBar window. This class wraps the windows pager API. Both classes are fairly easy to use and can be used the same way you would any standard MFC control.

Themed Windows XP style Explorer Bar
By Mathew Hall
http://www.codeproject.com/cs/miscctrl/XPTaskBar.asp (I really prefer this one most)

Windows XP style Collapsible Panel Bar
By Derek Lakin
http://www.codeproject.com/cs/miscctrl/collapsiblepanelbar.asp

Just another C# Collapsing Group Control
By Daren May
http://www.codeproject.com/cs/miscctrl/xpgroupbox.asp

Full-featured XP Style Collapsible Panel
By Tom Guinther
http://www.codeproject.com/cs/miscctrl/TgXPPanel.asp



<< Home

This page is powered by Blogger. Isn't yours?