#ifndef __CUSTOMTABCTRL_H__ #define __CUSTOMTABCTRL_H__ #pragma once ///////////////////////////////////////////////////////////////////////////// // CCustomTabCtrl - A base class to help implement // Tab Controls with different appearances // // Original work by Bjarke Viksoe (bjarke@viksoe.dk) // Revised version by Daniel Bowen (dbowen@es.com). // Copyright (c) 2001-2002 Bjarke Viksoe. // Copyright (c) 2002-2004 Daniel Bowen. // // This code may be used in compiled form in any way you desire. This // file may be redistributed by any means PROVIDING it is // not sold for profit without the authors written consent, and // providing that this notice and the authors name is included. // // This file is provided "as is" with no expressed or implied warranty. // The author accepts no liability if it causes any damage to you or your // computer whatsoever. It's free, so don't hassle me about it. // // Beware of bugs. // // History (Date/Author/Description): // ---------------------------------- // // 2005/07/13: Daniel Bowen // - Namespace qualify the use of more ATL and WTL classes. // // 2005/03/14: Daniel Bowen // - Fix warnings when compiling for 64-bit. // // 2004/06/28: Daniel Bowen // - More ATLASSERTs // - Clean up warnings on level 4 // - Fix "FindItem" so you can search on more than 1 criteria at a time // (like it was intended to work). Use #define for flags now // instead of enumeration (CTFI_TABVIEW instead of eCustomTabItem_TabView). // // 2004/06/21: Peter Carlson // - "CanClose" for items. // - HighlightItem - "SetHighlighted" on item, then InvalidateRect for the item // // 2004/05/10: Peter Carlson // - Middle mouse button notifications // // 2004/04/29: Daniel Bowen // - Suport for a new CTCS_DRAGREARRANGE style. If you set this style, // a tab item can be dragged to another position within the // same tab control. Along with this are the new notifications // CTCN_BEGINITEMDRAG // CTCN_ACCEPTITEMDRAG // CTCN_CANCELITEMDRAG // If CTCS_SCROLL is also set, then the tabs will scroll when // you get near the left or the right edge of the tab control. // The drag rearrange methods are all overrideable, so a more // derived class could do a different UI for the drag and drop. // The current implementation roughly mimics dragging // MDI tabs in Visual Studio (with the exceptions of // supporting scrolling, and not moving the tab until the // cursor is past the half-way point of an adjacent tab). // - Remove m_idDlgCtrl, and just use GetDlgCtrlID where needed. // This way, you can change the ID after the window is created. // - Move shutdown code that was in OnDestroy to "Uninitialize". // Call Uninitialize from both OnDestroy and UnsubclassWindow // (UnsubclassWindow wasn't previously being overriden like it should). // - With "Mouse Down" state flags, specify Left or Right to be // more specific. Currently, the right mouse button state flags // are not being set. // - On a left and right button down, don't take the focus when // clicking on a tab item. // - You can now change the definition of the scroll speed specified by // CTCSR_NONE // CTCSR_SLOW // CTCSR_NORMAL // CTCSR_FAST // As long as you #define these before including this header file. // - Support for highlighting items. // Add CCustomTabItem::IsHighlighted/SetHighlighted. // Uses the custom draw state CDIS_MARKED. // // 2003/06/27: Daniel Bowen // - Update comment referencing DECLARE_WND_CLASS to be DECLARE_WND_CLASS_EX instead // - If the creation of the window fails, don't try to initialize. // // 2003/06/03: Daniel Bowen // - Fix compile errors for VC 7.1 // // 2003/01/07: Daniel Bowen // - Destroy or detach tooltip control when handling WM_DESTROY // // 2002/12/05: Daniel Bowen // - Handle WM_SYSCOLORCHANGE in case its broadcast to us // from a top-level window. Call OnSettingChange. // // 2002/11/13: Daniel Bowen // - New CTCS_FLATEDGE style. Tab controls derived from // CCustomTabCtrl can use this style to determine whether // to draw the outline of the control with a flat look. // - New CalcSize_NonClient. UpdateLayout will now call // this overrideable method before calling any other // CalcSize_* methods, so that you can adjust the client // RECT to account for non-client areas. // // 2002/10/21: Daniel Bowen // - NMCTCITEM and NMCTC2ITEMS actually have "pt" in client // coordinates, not screen coordinates. Change the comment. // - Notifications using NMCTCITEM and NMCTC2ITEMS // (NM_CLICK, et. al) were incorrectly initialing "pt". // - Remove some some unnecessary ATLASSERT(::IsWindow(m_hWnd)) // (if the method doesn't depend on a valid m_hWnd) // - Add some additional casting when dealing with current selection // - DeleteItem - after sending CTCN_DELETEITEM, re-get // the count of items in case its changed // - Change a couple of ASSERTs and parameter checks that deal // with size_t variables to not needlessly check for < 0 // - Change SetCurSel to take an int instead of a size_t. // Passing an int < 0 will clear the current selection // - CCustomTabCtrl::SetImageList - Should be // CImageList imageListOld = m_imageList; // instead of // CImageList& imageListOld = m_imageList; // // 2002/07/16: Daniel Bowen // - Ensure that any place doing anything with m_tooltip // first checks if(m_tooltip.IsWindow()). // - Update DeleteAllItems to turn off redrawing while // the delete happens. // - DeleteAllItems now takes an optional boolean to // specify whether or not to redraw after the deletion // - Handle WM_SETREDRAW, but still allow the default handling. // We'll track WM_SETREDRAW in our own state variable // so that we can avoid doing UpdateLayout if // someone has called WM_SETREDRAW with FALSE. // When they call WM_SETREDRAW with TRUE to turn // it back on, we'll UpdateLayout to be ready for the caller // to do an InvalidateRect or RedrawWindow. // // 2002/06/20: Daniel Bowen // - SetCurSel - // * Added a new optional parameter "bNotify" that allows // you to specify whether or not you want the parent // to receive the notifications CTCN_SELCHANGING and // CTCN_SELCHANGE. // * Even if the newly requested selection index is // the same index as the previous selection index, // go through the whole SetCurSel process. // Even though the index is the same, the item // might be different (as in the case of // InsertItem inserting a new item where the old // selection used to be). EnsureVisible is also called, // which in the CTCS_SCROLL case, you really want // every time even if the item is the same. // // This is a change from previous versions, // where if the index was the same as the current selection, // the method returned immediately. // // - DeleteItem - // * When bUpdateSelection is true: // Now, when you delete the selected item, instead of // selecting the 0-index item, it tries to leave the index // of the selected item the same. If the selected item // was the last item, the new last item is selected. // If the selected item was the only remaining item, // the selection is cleared. // // // 2002/06/13: Daniel Bowen // - Fix small bug with scroll-repeat when scrolling right. // // 2002/06/12: Daniel Bowen // - Publish codeproject article. For history prior // to the release of the article, please see the article // and the section "Note to previous users" #ifndef __cplusplus #error ATL requires C++ compilation (use a .cpp suffix) #endif #ifndef __ATLAPP_H__ #error CustomTabCtrl.h requires atlapp.h to be included first #endif #ifndef __ATLCTRLS_H__ #error CustomTabCtrl.h requires atlctrls.h to be included first #endif #ifndef __ATLGDIX_H__ #error CustomTabCtrl.h requires atlgdix.h to be included first #endif // There are slightly different dependencies under VC 7 (ATL 7) vs. VC 6 (ATL 3) #if (_ATL_VER >= 0x0700) #if !defined(__ATLCOLL_H__) #error CustomTabCtrl.h requires atlcoll.h (under VC 7). #endif #if !defined(__ATLSTR_H__) #error CustomTabCtrl.h requires CString. In VC 7, include atlstr.h #endif #if defined(__ATLMISC_H__) && !defined(_WTL_NO_CSTRING) #error In VC 7, please define _WTL_NO_CSTRING if you include atlmisc.h #endif #else #if !defined(_WTL_USE_CSTRING) #error CustomTabCtrl.h requires CString. In VC 6, be sure to include atlmisc.h. #endif #endif #if (_WIN32_IE < 0x0400) #error CustomTabCtrl.h requires _WIN32_IE >= 0x0400 #endif // Window styles: // NOTE: "CTCS" stands for "Custom tab control style" #define CTCS_SCROLL 0x0001 // TCS_SCROLLOPPOSITE #define CTCS_BOTTOM 0x0002 // TCS_BOTTOM //#define CTCS_RIGHT 0x0002 // TCS_RIGHT //#define CTCS_MULTISELECT 0x0004 // TCS_MULTISELECT #define CTCS_CLOSEBUTTON 0x0008 // TCS_FLATBUTTONS //#define CTCS_FORCEICONLEFT 0x0010 // TCS_FORCEICONLEFT //#define CTCS_FORCELABELLEFT 0x0020 // TCS_FORCELABELLEFT #define CTCS_HOTTRACK 0x0040 // TCS_HOTTRACK //#define CTCS_VERTICAL 0x0080 // TCS_VERTICAL //#define CTCS_TABS 0x0000 // TCS_TABS #define CTCS_FLATEDGE 0x0100 // TCS_BUTTONS //#define CTCS_SINGLELINE 0x0000 // TCS_SINGLELINE //#define CTCS_MULTILINE 0x0200 // TCS_MULTILINE //#define CTCS_RIGHTJUSTIFY 0x0000 // TCS_RIGHTJUSTIFY #define CTCS_DRAGREARRANGE 0x0400 // TCS_FIXEDWIDTH //#define CTCS_OLEDRAGDROP 0x0800 // TCS_RAGGEDRIGHT //#define CTCS_FOCUSONBUTTONDOWN 0x1000 // TCS_FOCUSONBUTTONDOWN #define CTCS_BOLDSELECTEDTAB 0x2000 // TCS_OWNERDRAWFIXED #define CTCS_TOOLTIPS 0x4000 // TCS_TOOLTIPS //#define CTCS_FOCUSNEVER 0x8000 // TCS_FOCUSNEVER // Notifications: #define CTCN_FIRST (0U-550U) // TCN_FIRST #define CTCN_LAST (0U-580U) // TCN_LAST #define CTCN_SELCHANGE (TCN_FIRST - 1) // TCN_SELCHANGE #define CTCN_SELCHANGING (TCN_FIRST - 2) // TCN_SELCHANGING //#define CTCN_GETOBJECT (TCN_FIRST - 3) // TCN_GETOBJECT //#define CTCN_FOCUSCHANGE (TCN_FIRST - 4) // TCN_FOCUSCHANGE //#define CTCN_INITIALIZE (TCN_FIRST - 10) // obsolete for now #define CTCN_INSERTITEM (TCN_FIRST - 11) #define CTCN_DELETEITEM (TCN_FIRST - 12) #define CTCN_MOVEITEM (TCN_FIRST - 13) #define CTCN_SWAPITEMPOSITIONS (TCN_FIRST - 14) #define CTCN_CLOSE (TCN_FIRST - 15) #define CTCN_BEGINITEMDRAG (TCN_FIRST - 21) #define CTCN_ACCEPTITEMDRAG (TCN_FIRST - 22) #define CTCN_CANCELITEMDRAG (TCN_FIRST - 23) #define CTCN_MCLICK (TCN_FIRST - 24) #define CTCN_MDBLCLK (TCN_FIRST - 25) // Hit Test codes #define CTCHT_NOWHERE 0x0001 // TCHT_NOWHERE #define CTCHT_ONITEMICON 0x0002 // TCHT_ONITEMICON #define CTCHT_ONITEMLABEL 0x0004 // TCHT_ONITEMLABEL #define CTCHT_ONITEM (CTCHT_ONITEMICON | CTCHT_ONITEMLABEL) #define CTCHT_ONCLOSEBTN 0x0010 #define CTCHT_ONSCROLLRIGHTBTN 0x0020 #define CTCHT_ONSCROLLLEFTBTN 0x0040 // Find Item flags #define CTFI_NONE 0x0000 #define CTFI_RECT 0x0001 #define CTFI_IMAGE 0x0002 #define CTFI_TEXT 0x0004 #define CTFI_TOOLTIP 0x0008 #define CTFI_TABVIEW 0x0010 #define CTFI_HIGHLIGHTED 0x0020 #define CTFI_CANCLOSE 0x0040 #define CTFI_LAST CTFI_CANCLOSE #define CTFI_ALL 0xFFFF // Number of milliseconds for scroll repeat #ifndef CTCSR_NONE #define CTCSR_NONE 0 #endif #ifndef CTCSR_SLOW #define CTCSR_SLOW 100 #endif #ifndef CTCSR_NORMAL #define CTCSR_NORMAL 25 #endif #ifndef CTCSR_FAST #define CTCSR_FAST 10 #endif // Drag and drop related constant #ifndef CTCD_SCROLLZONEWIDTH #define CTCD_SCROLLZONEWIDTH 20 #endif // Structures typedef struct tagNMCTCITEM { NMHDR hdr; int iItem; // Item Index POINT pt; // Client Coordinates } NMCTCITEM, *LPNMCTCITEM; typedef struct tagNMCTC2ITEMS { NMHDR hdr; int iItem1; // First Item Index int iItem2; // Second Item Index POINT pt; // Client Coordinates } NMCTC2ITEMS, *LPNMCTC2ITEMS; typedef struct tagCTCHITTESTINFO { POINT pt; // Client Coordinates of point to test UINT flags; } CTCHITTESTINFO, *LPCTCHITTESTINFO; typedef struct tagNMCTCCUSTOMDRAW { NMCUSTOMDRAW nmcd; HFONT hFontInactive; HFONT hFontSelected; HBRUSH hBrushBackground; COLORREF clrTextInactive; COLORREF clrTextSelected; COLORREF clrSelectedTab; COLORREF clrBtnFace; COLORREF clrBtnShadow; COLORREF clrBtnHighlight; COLORREF clrBtnText; COLORREF clrHighlight; COLORREF clrHighlightHotTrack; COLORREF clrHighlightText; } NMCTCCUSTOMDRAW, FAR * LPNMCTCCUSTOMDRAW; typedef struct tagCTCSETTINGS { signed char iPadding; signed char iMargin; signed char iSelMargin; signed char iIndent; } CTCSETTINGS; // Tab Item classes class CCustomTabItem { // Member variables protected: RECT m_rcItem; int m_nImage; _CSTRING_NS::CString m_sText; _CSTRING_NS::CString m_sToolTip; bool m_bHighlighted; bool m_bCanClose; public: // NOTE: These are here for backwards compatibility. // Use the new CTFI_NONE, CTFI_RECT, etc. typedef enum FieldFlags { eCustomTabItem_None = CTFI_NONE, eCustomTabItem_Rect = CTFI_RECT, eCustomTabItem_Image = CTFI_IMAGE, eCustomTabItem_Text = CTFI_TEXT, eCustomTabItem_ToolTip = CTFI_TOOLTIP, eCustomTabItem_All = CTFI_ALL, }; #if (_MSC_VER >= 1300) #pragma deprecated(eCustomTabItem_None) #pragma deprecated(eCustomTabItem_Rect) #pragma deprecated(eCustomTabItem_Image) #pragma deprecated(eCustomTabItem_Text) #pragma deprecated(eCustomTabItem_ToolTip) #pragma deprecated(eCustomTabItem_All) #endif // Constructors/Destructors public: CCustomTabItem() : m_nImage(-1), m_bHighlighted(false), m_bCanClose(true) { ::SetRectEmpty(&m_rcItem); } CCustomTabItem(const CCustomTabItem& rhs) { *this = rhs; } virtual ~CCustomTabItem() { } const CCustomTabItem& operator=(const CCustomTabItem& rhs) { if(&rhs != this) { m_rcItem = rhs.m_rcItem; m_nImage = rhs.m_nImage; m_sText = rhs.m_sText; m_sToolTip = rhs.m_sToolTip; m_bHighlighted = rhs.m_bHighlighted; m_bCanClose = rhs.m_bCanClose; } return *this; } // Accessors public: RECT GetRect() const { return m_rcItem; } LPCRECT GetRectRef() const { return &m_rcItem; } bool SetRect(RECT rcItem) { m_rcItem = rcItem; return true; } int GetImageIndex() const { return m_nImage; } bool SetImageIndex(int nImage = -1) { m_nImage = nImage; return true; } _CSTRING_NS::CString GetText() const { return m_sText; } LPCTSTR GetTextRef() const { return (LPCTSTR)m_sText; } bool SetText(LPCTSTR sNewText) { m_sText = sNewText; return true; } _CSTRING_NS::CString GetToolTip() const { return m_sToolTip; } LPCTSTR GetToolTipRef() const { return (LPCTSTR)m_sToolTip; } bool SetToolTip(LPCTSTR sNewText) { m_sToolTip = sNewText; return true; } bool IsHighlighted() const { return m_bHighlighted; } bool SetHighlighted(bool bHighlighted) { m_bHighlighted = bHighlighted; return true; } bool CanClose() const { return m_bCanClose; } bool SetCanClose(bool bCanClose) { m_bCanClose = bCanClose; return true; } // Methods: public: bool UsingImage() const { return (m_nImage >= 0); } bool UsingText() const { return (m_sText.GetLength() > 0); } bool UsingToolTip() const { return (m_sToolTip.GetLength() > 0); } BOOL InflateRect(int dx, int dy) { return ::InflateRect(&m_rcItem, dx, dy); } bool MatchItem(CCustomTabItem* pItem, DWORD eFlags) const { bool bMatch = true; if(bMatch && (eFlags & CTFI_RECT) == CTFI_RECT) { bMatch = (TRUE == ::EqualRect(&m_rcItem, &pItem->m_rcItem)); } if(bMatch && (eFlags & CTFI_IMAGE) == CTFI_IMAGE) { bMatch = (m_nImage == pItem->m_nImage); } if(bMatch && (eFlags & CTFI_TEXT) == CTFI_TEXT) { bMatch = (m_sText == pItem->m_sText); } if(bMatch && (eFlags & CTFI_TOOLTIP) == CTFI_TOOLTIP) { bMatch = (m_sToolTip == pItem->m_sToolTip); } if(bMatch && (eFlags & CTFI_HIGHLIGHTED) == CTFI_HIGHLIGHTED) { bMatch = (m_bHighlighted == pItem->m_bHighlighted); } if(bMatch && (eFlags & CTFI_CANCLOSE) == CTFI_CANCLOSE) { bMatch = (m_bCanClose == pItem->m_bCanClose); } if(bMatch) { *pItem = *this; } return bMatch; } }; // Derived Tab Item class that supports an HWND identifying a "tab view" class CTabViewTabItem : public CCustomTabItem { protected: typedef CCustomTabItem baseClass; // Member variables (in addition to CCustomTabItem ones) protected: HWND m_hWndTabView; public: // NOTE: This is here for backwards compatibility. // Use the new CTFI_TABVIEW instead typedef enum FieldFlags { eCustomTabItem_TabView = CTFI_TABVIEW, }; // Use CTFI_TABVIEW instead #if (_MSC_VER >= 1300) #pragma deprecated(eCustomTabItem_TabView) #endif // Constructors/Destructors public: CTabViewTabItem() : m_hWndTabView(NULL) { } CTabViewTabItem(const CTabViewTabItem& rhs) { *this = rhs; } virtual ~CTabViewTabItem() { } const CTabViewTabItem& operator=(const CTabViewTabItem& rhs) { if(&rhs != this) { m_rcItem = rhs.m_rcItem; m_nImage = rhs.m_nImage; m_sText = rhs.m_sText; m_sToolTip = rhs.m_sToolTip; m_bHighlighted = rhs.m_bHighlighted; m_bCanClose = rhs.m_bCanClose; m_hWndTabView = rhs.m_hWndTabView; } return *this; } // Accessors public: HWND GetTabView() const { return m_hWndTabView; } bool SetTabView(HWND hWnd = NULL) { m_hWndTabView = hWnd; return true; } // Methods: public: bool UsingTabView() const { return (m_hWndTabView != NULL); } bool MatchItem(CTabViewTabItem* pItem, DWORD eFlags) const { bool bMatch = true; if(eFlags == CTFI_TABVIEW) { // Make the common case a little faster // (searching only for a match to the "tab view" HWND) bMatch = (m_hWndTabView == pItem->m_hWndTabView); } else { // Do an extensive comparison bMatch = baseClass::MatchItem(pItem, eFlags); if(bMatch && (eFlags & CTFI_TABVIEW) == CTFI_TABVIEW) { bMatch = (m_hWndTabView == pItem->m_hWndTabView); } } if(bMatch) { *pItem = *this; } return bMatch; } }; #if (_ATL_VER < 0x0700) // With ATL 7, CAtlArray was introduced which is better than // CSimpleArray. Among other things, it supports inserting // items in any place. If this code is compiled under ATL 7, // we'll use the real CAtlArray. If this is compiled under ATL 3, // we'll use a "fake" CAtlArray where we implement the // functionality we're using that the real CAtlArray provides. // // Important! This isn't the real ATL 7 CAtlArray. // We inherit from CSimpleArray as "protected", so that you // can't call its versions of functions (so you have // to use the CAtlArray style of functions) namespace ATL { template class CAtlArray : protected ATL::CSimpleArray { protected: typedef CAtlArray thisClass; typedef ATL::CSimpleArray baseClass; public: //Real CAtlArray: size_t GetCount() const throw(); size_t GetCount() const { return m_nSize; } //Real CAtlArray: void InsertAt( size_t iElement, INARGTYPE element, size_t nCount = 1 ); void InsertAt( size_t nIndex, E& element ) { if(m_nSize == m_nAllocSize) { E* aT; int nNewAllocSize = (m_nAllocSize == 0) ? 1 : (m_nSize * 2); aT = (E*)realloc(m_aT, nNewAllocSize * sizeof(E)); if(aT == NULL) return; // FALSE; m_nAllocSize = nNewAllocSize; m_aT = aT; } memmove((void*)&m_aT[nIndex+1], (void*)&m_aT[nIndex], (m_nSize - nIndex ) * sizeof(E)); m_nSize++; SetAtIndex(nIndex, element); //return TRUE; } //Real CAtlArray: void RemoveAt( size_t iElement, size_t nCount = 1 ); void RemoveAt( size_t nIndex ) { // This is an improvement over CSimpleArray::RemoveAt suggested // by Jim Springfield on the ATL discussion list m_aT[nIndex].~E(); if((int)nIndex != (m_nSize - 1)) { memmove((void*)&m_aT[nIndex], (void*)&m_aT[nIndex + 1], (m_nSize - (nIndex + 1)) * sizeof(E)); } m_nSize--; //return TRUE; } //Real CAtlArray: const E& operator[]( size_t iElement ) const throw(); const E& operator[]( size_t iElement ) const { ATLASSERT( iElement < (size_t)m_nSize ); return( m_aT[iElement] ); } //Real CAtlArray: E& operator[]( size_t iElement ) throw(); E& operator[]( size_t iElement ) { ATLASSERT( iElement < (size_t)m_nSize ); return( m_aT[iElement] ); } }; }; // namespace ATL #endif // (_ATL_VER < 0x0700) typedef ATL::CWinTraits CCustomTabCtrlWinTraits; template class ATL_NO_VTABLE CCustomTabCtrl : public ATL::CWindowImpl< T, TBase, TWinTraits >, public COffscreenDrawRect< T > { public: // Expose the item type (that's a template parameter to this base class) typedef typename TItem TItem; protected: typedef ATL::CWindowImpl< T, TBase, TWinTraits > baseClass; typedef COffscreenDrawRect< T > offscreenDrawClass; // Member variables protected: int m_iCurSel; int m_iHotItem; CTCSETTINGS m_settings; ATL::CAtlArray< TItem* > m_Items; WTL::CFont m_font; WTL::CFont m_fontSel; WTL::CImageList m_imageList; WTL::CToolTipCtrl m_tooltip; RECT m_rcTabItemArea; RECT m_rcScrollLeft; RECT m_rcScrollRight; RECT m_rcCloseButton; int m_iDragItem; int m_iDragItemOriginal; POINT m_ptDragOrigin; HCURSOR m_hCursorMove; HCURSOR m_hCursorNoDrop; int m_iScrollOffset; // Flags, internal state, etc. // // 3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1 // 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 // +-------+-------+-------+-------+---+-----------+---------------+ // | FUT | MO | MD | HT |SR | SD | FLAGS | // +-------+-------+-------+-------+---+-----------+---------------+ // // FLAGS - boolean flags // SD - Scroll delta. The number of pixels to move in a single scroll. // Valid values are 0-63 (the value is bit shifted to/from position). // SR - Scroll repeat speed. Valid values are no-repeat, // slow repeat, normal repeat and fast repeat // HT - Current hot tracked item (if its a tab, then m_iHotItem is the hot tab item) // MD - Item under mouse when mouse button down message was sent // but before mouse button up message is sent // MO - Item current under mouse cursor // FUT - Not used at this time, but reserved for the future. DWORD m_dwState; enum StateBits { // Flags // bits = 0x000000ff ectcMouseInWindow = 0x00000001, ectcOverflowLeft = 0x00000002, ectcOverflowRight = 0x00000004, //ectcOverflowBottom = 0x00000002, // alias for vertical mode //ectcOverflowTop = 0x00000004, // alias for vertical mode ectcEnableRedraw = 0x00000008, ectcDraggingItem = 0x00000010, //ectcFlag20 = 0x00000020, //ectcFlag40 = 0x00000040, //ectcFlag80 = 0x00000080, // Scroll // bits = 0x0000ff00 ectcScrollDeltaMask = 0x00003f00, //0011 1111 ectcScrollDeltaShift = 8, // We have to publicly expose these: ectcScrollRepeat = 0x0000c000, //1100 0000 //ectcScrollRepeat_None = 0x00000000, //ectcScrollRepeat_Slow = 0x00004000, //0100 0000 //ectcScrollRepeat_Normal = 0x00008000, //1000 0000 //ectcScrollRepeat_Fast = 0x0000c000, //1100 0000 // Hot Tracking // bits = 0x000f0000 ectcHotTrack = 0x000f0000, ectcHotTrack_CloseButton = 0x00010000, ectcHotTrack_ScrollRight = 0x00020000, ectcHotTrack_ScrollLeft = 0x00030000, ectcHotTrack_TabItem = 0x00040000, // Mouse Down // bits = 0x00f00000 ectcMouseDown = 0x00f00000, ectcMouseDownL_CloseButton = 0x00100000, ectcMouseDownL_ScrollRight = 0x00200000, ectcMouseDownL_ScrollLeft = 0x00300000, ectcMouseDownL_TabItem = 0x00400000, ectcMouseDownR_CloseButton = 0x00900000, ectcMouseDownR_ScrollRight = 0x00a00000, ectcMouseDownR_ScrollLeft = 0x00b00000, ectcMouseDownR_TabItem = 0x00c00000, // Mouse Over // bits = 0x0f000000 ectcMouseOver = 0x0f000000, ectcMouseOver_CloseButton = 0x01000000, ectcMouseOver_ScrollRight = 0x02000000, ectcMouseOver_ScrollLeft = 0x03000000, ectcMouseOver_TabItem = 0x04000000, }; enum ButtonToolTipIDs { ectcToolTip_Close = 0xFFFFFFF0, ectcToolTip_ScrollRight = 0xFFFFFFF1, ectcToolTip_ScrollLeft = 0xFFFFFFF2, }; enum TimerIDs { ectcTimer_ScrollLeft = 0x00000010, ectcTimer_ScrollRight = 0x00000020, }; // Public enumerations public: enum ScrollRepeat { ectcScrollRepeat_None = 0x00000000, ectcScrollRepeat_Slow = 0x00004000, ectcScrollRepeat_Normal = 0x00008000, ectcScrollRepeat_Fast = 0x0000c000, }; // Constructors public: CCustomTabCtrl() : m_iCurSel(-1), m_iHotItem(-1), m_dwState(0), m_iDragItem(-1), m_iDragItemOriginal(-1), m_hCursorMove(NULL), m_hCursorNoDrop(NULL), m_iScrollOffset(0) { ::ZeroMemory(&m_settings, sizeof(CTCSETTINGS)); ::ZeroMemory(&m_ptDragOrigin, sizeof(POINT)); ::SetRectEmpty(&m_rcTabItemArea); ::SetRectEmpty(&m_rcCloseButton); ::SetRectEmpty(&m_rcScrollLeft); ::SetRectEmpty(&m_rcScrollRight); m_dwState |= ((40 << ectcScrollDeltaShift) & ectcScrollDeltaMask); m_dwState |= ectcScrollRepeat_Normal; m_dwState |= ectcEnableRedraw; } // Implementation protected: void InitializeTooltips(void) { ATLASSERT(!m_tooltip.IsWindow()); if(!m_tooltip.IsWindow()) { // Be sure InitCommonControlsEx is called before this, // with one of the flags that includes the tooltip control m_tooltip.Create(m_hWnd, NULL, NULL, WS_POPUP | TTS_NOPREFIX | TTS_ALWAYSTIP /* | TTS_BALLOON */, WS_EX_TOOLWINDOW); if(m_tooltip.IsWindow()) { m_tooltip.SetWindowPos(HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); m_tooltip.SetDelayTime(TTDT_INITIAL, ::GetDoubleClickTime()); m_tooltip.SetDelayTime(TTDT_AUTOPOP, ::GetDoubleClickTime() * 20); m_tooltip.SetDelayTime(TTDT_RESHOW, ::GetDoubleClickTime() / 5); } } } void ActivateTooltips(BOOL bActivate = TRUE) { ATLASSERT(m_tooltip.IsWindow()); if(m_tooltip.IsWindow()) { m_tooltip.Activate(bActivate); } } void ClearCurrentHotTracking(bool bRedrawEffectedArea = true) { switch(m_dwState & ectcHotTrack) { case ectcHotTrack_CloseButton: m_dwState &= ~ectcHotTrack; if(bRedrawEffectedArea) { this->InvalidateRect(&m_rcCloseButton); } break; case ectcHotTrack_ScrollRight: m_dwState &= ~ectcHotTrack; if(bRedrawEffectedArea) { this->InvalidateRect(&m_rcScrollRight); } break; case ectcHotTrack_ScrollLeft: m_dwState &= ~ectcHotTrack; if(bRedrawEffectedArea) { this->InvalidateRect(&m_rcScrollLeft); } break; case ectcHotTrack_TabItem: m_dwState &= ~ectcHotTrack; m_iHotItem = -1; if(bRedrawEffectedArea) { // In case the derived class actually changes the width // of a hot tracked tab, invalidate the whole tab window this->Invalidate(); } break; default: m_dwState &= ~ectcHotTrack; m_iHotItem = -1; if(bRedrawEffectedArea) { this->Invalidate(); } break; } } void ClearCurrentMouseDownTracking(bool bRedrawEffectedArea = true) { switch(m_dwState & ectcMouseDown) { case ectcMouseDownL_CloseButton: case ectcMouseDownR_CloseButton: m_dwState &= ~ectcMouseDown; if(bRedrawEffectedArea) { this->InvalidateRect(&m_rcCloseButton); } break; case ectcMouseDownL_ScrollRight: case ectcMouseDownR_ScrollRight: m_dwState &= ~ectcMouseDown; if(bRedrawEffectedArea) { this->InvalidateRect(&m_rcScrollRight); } break; case ectcMouseDownL_ScrollLeft: case ectcMouseDownR_ScrollLeft: m_dwState &= ~ectcMouseDown; if(bRedrawEffectedArea) { this->InvalidateRect(&m_rcScrollLeft); } break; case ectcMouseDownL_TabItem: case ectcMouseDownR_TabItem: m_dwState &= ~ectcMouseDown; if(bRedrawEffectedArea) { // In case the derived class actually changes the width // of a hot tracked tab, invalidate the whole tab window this->Invalidate(); //if(m_iActionItem >= 0 && m_iActionItem < m_Items.GetCount()) //{ // RECT rcItemDP; // this->GetItemRect(m_iActionItem, &rcItemDP); // this->InvalidateRect(rcItemDP); //} } break; default: m_dwState &= ~ectcMouseDown; if(bRedrawEffectedArea) { this->Invalidate(); } break; } } void ClearCurrentMouseOverTracking(bool bRedrawEffectedArea = true) { switch(m_dwState & ectcMouseOver) { case ectcMouseOver_CloseButton: m_dwState &= ~ectcMouseOver; if(bRedrawEffectedArea) { this->InvalidateRect(&m_rcCloseButton); } break; case ectcMouseOver_ScrollRight: m_dwState &= ~ectcMouseOver; if(bRedrawEffectedArea) { this->InvalidateRect(&m_rcScrollRight); } break; case ectcMouseOver_ScrollLeft: m_dwState &= ~ectcMouseOver; if(bRedrawEffectedArea) { this->InvalidateRect(&m_rcScrollLeft); } break; case ectcMouseOver_TabItem: m_dwState &= ~ectcMouseOver; if(bRedrawEffectedArea) { // In case the derived class actually changes the width // of a hot tracked tab, invalidate the whole tab window this->Invalidate(); //if(m_iActionItem >= 0 && m_iActionItem < m_Items.GetCount()) //{ // RECT rcItemDP = {0}; // this->GetItemRect(m_iActionItem, &rcItemDP); // this->InvalidateRect(rcItemDP); //} } break; default: m_dwState &= ~ectcMouseOver; if(bRedrawEffectedArea) { this->Invalidate(); } break; } } // Drag and drop methods (overrideable) // In this base class implementation, we mimic the drag rearrange // of MDI tabs in VS.NET (except, we add scroll support) public: void AcceptItemDrag(bool bRedrawEffectedArea = true) { T* pT = static_cast(this); NMCTC2ITEMS nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_ACCEPTITEMDRAG }, m_iDragItemOriginal, m_iDragItem, {-1,-1}}; ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh); // In this implementation, the tab is moved as they drag. pT->StopItemDrag(bRedrawEffectedArea); } void CancelItemDrag(bool bRedrawEffectedArea = true) { T* pT = static_cast(this); // In this implementation, the tab is moved as they drag. // To cancel the drag, we move the item back to its original place. if( m_iDragItemOriginal >= 0 && m_iDragItem >= 0 && m_iDragItemOriginal != m_iDragItem) { pT->MoveItem(m_iDragItem, m_iDragItemOriginal, true, false); pT->EnsureVisible(m_iDragItemOriginal); } NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_CANCELITEMDRAG }, m_iDragItemOriginal, {-1,-1}}; ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh); pT->StopItemDrag(bRedrawEffectedArea); } void StopItemDrag(bool bRedrawEffectedArea = true) { if(ectcDraggingItem == (m_dwState & ectcDraggingItem)) { m_dwState &= ~ectcDraggingItem; // Restore the default cursor // need conditional code because types don't match in winuser.h #ifdef _WIN64 ::SetCursor((HCURSOR)::GetClassLongPtr(m_hWnd, GCLP_HCURSOR)); #else ::SetCursor((HCURSOR)LongToHandle(::GetClassLongPtr(m_hWnd, GCLP_HCURSOR))); #endif if(m_hCursorMove != NULL) { ::DestroyCursor(m_hCursorMove); m_hCursorMove = NULL; } if(m_hCursorNoDrop != NULL) { ::DestroyCursor(m_hCursorNoDrop); m_hCursorNoDrop = NULL; } m_iDragItem = -1; m_iDragItemOriginal = -1; ::ZeroMemory(&m_ptDragOrigin, sizeof(POINT)); if(bRedrawEffectedArea) { this->Invalidate(); } } } void BeginItemDrag(int index, POINT ptDragOrigin) { T* pT = static_cast(this); if(index >= 0) { DWORD dwStyle = this->GetStyle(); if(CTCS_DRAGREARRANGE == (dwStyle & CTCS_DRAGREARRANGE)) { NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_BEGINITEMDRAG }, index, {ptDragOrigin.x, ptDragOrigin.y} }; if(FALSE != ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh)) { // Returning non-zero prevents our default handling. // We've possibly already set a couple things that we // need to cleanup, so call StopItemDrag pT->StopItemDrag(false); } else { // returning FALSE let's us do our default handling // Mark the beginning of a drag operation. // We should have already done SetCapture, but just // in case, we'll set it again. this->SetCapture(); // Set focus so that we get an ESC key press pT->SetFocus(); // This call to DoDragDrop is just to ensure a dependency on OLE32.dll. // In the future, if we support true OLE drag and drop, // we'll really use DoDragDrop. ::DoDragDrop(NULL, NULL, 0, 0); // To save on resources, we'll load the drag cursor // only when we need it, and destroy it when the drag is done HMODULE hOle32 = ::GetModuleHandle(_T("OLE32.dll")); if(hOle32 != NULL) { // Cursor ID identified using resource editor in Visual Studio int dragCursor = 2; int noDropCursor = 1; m_hCursorMove = ::LoadCursor(hOle32, MAKEINTRESOURCE(dragCursor)); m_hCursorNoDrop = ::LoadCursor(hOle32, MAKEINTRESOURCE(noDropCursor)); } m_dwState |= ectcDraggingItem; m_iDragItem = index; m_iDragItemOriginal = index; m_ptDragOrigin = ptDragOrigin; } } } } // Update the drag operation. // ptCursor should be in client coordinates void ContinueItemDrag(POINT ptCursor) { // We're dragging an item T* pT = static_cast(this); RECT rcClient = {0}; this->GetClientRect(&rcClient); if(::PtInRect(&rcClient, ptCursor)) { ::SetCursor(m_hCursorMove); } else { ::SetCursor(m_hCursorNoDrop); } CTCHITTESTINFO tchti = { 0 }; tchti.pt = ptCursor; int index = pT->HitTest(&tchti); if((index != m_iDragItem) && (index >= 0)) { RECT rcItem = {0}; this->GetItemRect(index, &rcItem); int itemMiddlePointX = rcItem.left + ((rcItem.right - rcItem.left) / 2); if((m_iDragItem < index) && (ptCursor.x > itemMiddlePointX)) { // They're dragging an item from left to right, // and have dragged it over the half way mark to the right pT->MoveItem(m_iDragItem, index, true, false); m_iDragItem = index; } else if((m_iDragItem > index) && (ptCursor.x < itemMiddlePointX)) { // They're dragging an item from right to left, // and have dragged it over the half way mark to the left pT->MoveItem(m_iDragItem, index, true, false); m_iDragItem = index; } } // Now scroll if necessary DWORD dwStyle = this->GetStyle(); if(CTCS_SCROLL == (dwStyle & CTCS_SCROLL)) { RECT rcLeftScrollZone = {rcClient.left, rcClient.top, (m_rcTabItemArea.left + CTCD_SCROLLZONEWIDTH), rcClient.bottom}; RECT rcRightScrollZone = {(m_rcTabItemArea.right - CTCD_SCROLLZONEWIDTH), rcClient.top, rcClient.right, rcClient.bottom}; if( ::PtInRect(&rcLeftScrollZone, ptCursor) && (ectcOverflowLeft == (m_dwState & ectcOverflowLeft))) { pT->ScrollLeft(true); this->SetTimer(ectcTimer_ScrollLeft, CTCSR_SLOW); } else if(::PtInRect(&rcRightScrollZone, ptCursor) && (ectcOverflowRight == (m_dwState & ectcOverflowRight))) { pT->ScrollRight(true); this->SetTimer(ectcTimer_ScrollRight, CTCSR_SLOW); } } } // Message Handling public: // Your derived class should use DECLARE_WND_CLASS or DECLARE_WND_SUPERCLASS, etc. //DECLARE_WND_CLASS_EX(_T("WTL_CustomTabCtrl"), CS_DBLCLKS, COLOR_WINDOW) BEGIN_MSG_MAP(CCustomTabCtrl) CHAIN_MSG_MAP(offscreenDrawClass) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) MESSAGE_HANDLER(WM_SIZE, OnSize) MESSAGE_RANGE_HANDLER(WM_MOUSEFIRST, WM_MOUSELAST, OnMouseMessage) MESSAGE_HANDLER(WM_MOUSEMOVE, OnMouseMove) MESSAGE_HANDLER(WM_MOUSELEAVE, OnMouseLeave) MESSAGE_HANDLER(WM_CAPTURECHANGED, OnCaptureChanged) MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown) MESSAGE_HANDLER(WM_LBUTTONUP, OnLButtonUp) MESSAGE_HANDLER(WM_LBUTTONDBLCLK, OnLButtonDoubleClick) MESSAGE_HANDLER(WM_RBUTTONDOWN, OnRButtonDown) MESSAGE_HANDLER(WM_RBUTTONUP, OnRButtonUp) MESSAGE_HANDLER(WM_RBUTTONDBLCLK, OnRButtonDoubleClick) MESSAGE_HANDLER(WM_MBUTTONDOWN, OnMButtonDown) MESSAGE_HANDLER(WM_MBUTTONUP, OnMButtonUp) MESSAGE_HANDLER(WM_MBUTTONDBLCLK, OnMButtonDoubleClick) MESSAGE_HANDLER(WM_SETTINGCHANGE, OnSettingChange) MESSAGE_HANDLER(WM_THEMECHANGED, OnSettingChange) MESSAGE_HANDLER(WM_SYSCOLORCHANGE, OnSettingChange) MESSAGE_HANDLER(WM_GETDLGCODE, OnGetDlgCode) MESSAGE_HANDLER(WM_KEYDOWN, OnKeyDown) MESSAGE_HANDLER(WM_GETFONT, OnGetFont) MESSAGE_HANDLER(WM_SETFONT, OnSetFont) MESSAGE_HANDLER(WM_STYLECHANGED, OnStyleChanged) MESSAGE_HANDLER(WM_TIMER, OnTimer) MESSAGE_HANDLER(WM_SETREDRAW, OnSetRedraw) NOTIFY_CODE_HANDLER(TTN_GETDISPINFO, OnGetToolTipInfo) END_MSG_MAP() LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { LRESULT lRes = DefWindowProc(); if(lRes == -1) { return -1; } T* pT = static_cast(this); pT->Initialize(); return lRes; } LRESULT OnDestroy(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { T* pT = static_cast(this); pT->Uninitialize(); bHandled = FALSE; return 0; } LRESULT OnSize(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { T* pT = static_cast(this); pT->UpdateLayout(); this->Invalidate(); bHandled = FALSE; return 0; } LRESULT OnSettingChange(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { T* pT = static_cast(this); pT->UpdateLayout(); this->Invalidate(); return 0; } LRESULT OnGetDlgCode(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { return DefWindowProc(uMsg, wParam, lParam) | DLGC_WANTARROWS; } LRESULT OnMouseMessage(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { bHandled = FALSE; if(m_tooltip.IsWindow()) { MSG msg = { m_hWnd, uMsg, wParam, lParam }; m_tooltip.RelayEvent(&msg); } return 1; } LRESULT OnMouseMove(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& bHandled) { bHandled = FALSE; DWORD dwStyle = this->GetStyle(); POINT ptCursor = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; T* pT = static_cast(this); if(ectcMouseInWindow != (m_dwState & ectcMouseInWindow)) { TRACKMOUSEEVENT tme = { 0 }; tme.cbSize = sizeof(tme); tme.dwFlags = TME_LEAVE; tme.hwndTrack = m_hWnd; if( _TrackMouseEvent(&tme) ) { m_dwState |= ectcMouseInWindow; pT->UpdateLayout(); this->Invalidate(); // "OnMouseEnter" //... } } if( (m_iDragItem >= 0) && (ectcMouseInWindow == (m_dwState & ectcMouseInWindow)) && (ectcDraggingItem != (m_dwState & ectcDraggingItem)) && (ectcMouseDownL_TabItem == (m_dwState & ectcMouseDown)) && (m_ptDragOrigin.x != ptCursor.x)) { pT->BeginItemDrag(m_iDragItem, m_ptDragOrigin); } else if(ectcDraggingItem == (m_dwState & ectcDraggingItem)) { pT->ContinueItemDrag(ptCursor); } else if(ectcMouseInWindow == (m_dwState & ectcMouseInWindow)) { // hit test if(::PtInRect(&m_rcCloseButton, ptCursor)) { if( ectcMouseOver_CloseButton != (m_dwState & ectcMouseOver)) { this->ClearCurrentMouseOverTracking(true); m_dwState |= ectcMouseOver_CloseButton; if(ectcMouseDownL_CloseButton == (m_dwState & ectcMouseDown)) { this->InvalidateRect(&m_rcCloseButton); } } else if( 0 == (m_dwState & ectcMouseDown) && ectcHotTrack_CloseButton != (m_dwState & ectcHotTrack)) { this->ClearCurrentHotTracking(true); m_dwState |= ectcHotTrack_CloseButton; this->InvalidateRect(&m_rcCloseButton); } } else { if(ectcMouseOver_CloseButton == (m_dwState & ectcMouseOver)) { this->ClearCurrentMouseOverTracking(true); } if(ectcHotTrack_CloseButton == (m_dwState & ectcHotTrack)) { this->ClearCurrentHotTracking(true); } } if(::PtInRect(&m_rcScrollRight, ptCursor)) { if( ectcMouseOver_ScrollRight != (m_dwState & ectcMouseOver)) { this->ClearCurrentMouseOverTracking(true); m_dwState |= ectcMouseOver_ScrollRight; if(ectcMouseDownL_ScrollRight == (m_dwState & ectcMouseDown)) { this->InvalidateRect(&m_rcScrollRight); } } else if(0 == (m_dwState & ectcMouseDown) && ectcHotTrack_ScrollRight != (m_dwState & ectcHotTrack)) { this->ClearCurrentHotTracking(true); m_dwState |= ectcHotTrack_ScrollRight; this->InvalidateRect(&m_rcScrollRight); } } else { if(ectcMouseOver_ScrollRight == (m_dwState & ectcMouseOver)) { this->ClearCurrentMouseOverTracking(true); } if(ectcHotTrack_ScrollRight == (m_dwState & ectcHotTrack)) { this->ClearCurrentHotTracking(true); } } if(::PtInRect(&m_rcScrollLeft, ptCursor)) { if( ectcMouseOver_ScrollLeft != (m_dwState & ectcMouseOver)) { this->ClearCurrentMouseOverTracking(true); m_dwState |= ectcMouseOver_ScrollLeft; if(ectcMouseDownL_ScrollLeft == (m_dwState & ectcMouseDown)) { this->InvalidateRect(&m_rcScrollLeft); } } else if(0 == (m_dwState & ectcMouseDown) && ectcHotTrack_ScrollLeft != (m_dwState & ectcHotTrack)) { this->ClearCurrentHotTracking(true); m_dwState |= ectcHotTrack_ScrollLeft; this->InvalidateRect(&m_rcScrollLeft); } } else { if(ectcMouseOver_ScrollLeft == (m_dwState & ectcMouseOver)) { this->ClearCurrentMouseOverTracking(true); } if(ectcHotTrack_ScrollLeft == (m_dwState & ectcHotTrack)) { this->ClearCurrentHotTracking(true); } } if(::PtInRect(&m_rcTabItemArea, ptCursor)) { if( ectcMouseOver_TabItem != (m_dwState & ectcMouseOver)) { this->ClearCurrentMouseOverTracking(true); m_dwState |= ectcMouseOver_TabItem; // Not needed for simple hot tracking: //if(ectcMouseDownL_TabItem == (m_dwState & ectcMouseDown)) //{ // this->InvalidateRect(&m_rcTabItemArea); //} } else if( CTCS_HOTTRACK == (dwStyle & CTCS_HOTTRACK) ) // && ectcHotTrack_TabItem != (m_dwState & ectcHotTrack)) { this->ClearCurrentHotTracking(true); CTCHITTESTINFO tchti = { 0 }; tchti.pt = ptCursor; int nIndex = pT->HitTest(&tchti); if(nIndex >= 0) { m_iHotItem = nIndex; m_dwState |= ectcHotTrack_TabItem; RECT rcItem = {0}; this->GetItemRect(nIndex, &rcItem); this->InvalidateRect(&rcItem); } } } else { if(ectcMouseOver_TabItem == (m_dwState & ectcMouseOver)) { this->ClearCurrentMouseOverTracking(true); } if( CTCS_HOTTRACK == (dwStyle & CTCS_HOTTRACK) && ectcHotTrack_TabItem == (m_dwState & ectcHotTrack)) { this->ClearCurrentHotTracking(true); } } } return 1; } LRESULT OnMouseLeave(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { T* pT = static_cast(this); bHandled = FALSE; m_dwState &= ~ectcMouseInWindow; this->ClearCurrentHotTracking(false); this->ClearCurrentMouseOverTracking(false); pT->UpdateLayout(); this->Invalidate(); return 0; } LRESULT OnCaptureChanged(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { T* pT = static_cast(this); bHandled = FALSE; this->ClearCurrentMouseDownTracking(false); if(ectcDraggingItem == (m_dwState & ectcDraggingItem)) { pT->CancelItemDrag(false); } return 0; } LRESULT OnLButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) { T* pT = static_cast(this); POINT ptCursor = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; if(::PtInRect(&m_rcCloseButton, ptCursor)) { m_dwState |= (ectcMouseDownL_CloseButton | ectcMouseOver_CloseButton); this->InvalidateRect(&m_rcCloseButton); this->SetCapture(); } else if(::PtInRect(&m_rcScrollRight, ptCursor)) { m_dwState |= (ectcMouseDownL_ScrollRight | ectcMouseOver_ScrollRight); if(ectcOverflowRight == (m_dwState & ectcOverflowRight)) { int nScrollSpeed = 0; switch(m_dwState & ectcScrollRepeat) { case ectcScrollRepeat_Slow: nScrollSpeed = CTCSR_SLOW; break; case ectcScrollRepeat_Normal: nScrollSpeed = CTCSR_NORMAL; break; case ectcScrollRepeat_Fast: nScrollSpeed = CTCSR_FAST; break; case ectcScrollRepeat_None: default: nScrollSpeed = CTCSR_NONE; break; } pT->ScrollRight(true); this->SetTimer(ectcTimer_ScrollRight, nScrollSpeed); } this->SetCapture(); } else if(::PtInRect(&m_rcScrollLeft, ptCursor)) { m_dwState |= (ectcMouseDownL_ScrollLeft | ectcMouseOver_ScrollLeft); if(ectcOverflowLeft == (m_dwState & ectcOverflowLeft)) { int nScrollSpeed = 0; switch(m_dwState & ectcScrollRepeat) { case ectcScrollRepeat_Slow: nScrollSpeed = CTCSR_SLOW; break; case ectcScrollRepeat_Normal: nScrollSpeed = CTCSR_NORMAL; break; case ectcScrollRepeat_Fast: nScrollSpeed = CTCSR_FAST; break; case ectcScrollRepeat_None: default: nScrollSpeed = CTCSR_NONE; break; } pT->ScrollLeft(true); this->SetTimer(ectcTimer_ScrollLeft, nScrollSpeed); } this->SetCapture(); } else { // Search for a tab CTCHITTESTINFO tchti = { 0 }; tchti.pt = ptCursor; int nIndex = pT->HitTest(&tchti); NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), NM_CLICK }, nIndex, {ptCursor.x, ptCursor.y} }; if(FALSE == ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh)) { // returning FALSE let's us do our default handling if(nIndex >= 0) { // NOTE: If they click on a tab, only grab the focus // if a drag operation is started. //pT->SetFocus(); pT->SetCurSel(nIndex); m_iDragItem = nIndex; m_ptDragOrigin = ptCursor; m_dwState |= ectcMouseDownL_TabItem; // This could be a drag operation. We'll start the actual drag // operation in OnMouseMove if the mouse moves while the left mouse // button is still pressed. OnLButtonUp will ReleaseCapture. this->SetCapture(); } } } return 0; } LRESULT OnLButtonUp(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) { if(m_hWnd == ::GetCapture()) { T* pT = static_cast(this); POINT ptCursor = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; if(ectcDraggingItem == (m_dwState & ectcDraggingItem)) { pT->AcceptItemDrag(); } // Before we release the capture, remember what the state was // (in WM_CAPTURECHANGED we ClearCurrentMouseDownTracking) DWORD dwState = m_dwState; ::ReleaseCapture(); if(ectcMouseDownL_CloseButton == (dwState & ectcMouseDown) && ectcMouseOver_CloseButton == (dwState & ectcMouseOver)) { // Close Button NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_CLOSE }, m_iCurSel, {ptCursor.x, ptCursor.y}}; ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh); } pT->UpdateLayout(); this->Invalidate(); } return 0; } LRESULT OnLButtonDoubleClick(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) { POINT ptCursor = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; // Search for a tab T* pT = static_cast(this); CTCHITTESTINFO tchti = { 0 }; tchti.pt = ptCursor; int nIndex = pT->HitTest(&tchti); // returning TRUE tells us not to do our default handling NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), NM_DBLCLK }, nIndex, {ptCursor.x, ptCursor.y}}; if(FALSE == ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh)) { // returning FALSE let's us do our default handling if(nIndex >= 0) { //pT->SetFocus(); //pT->SetCurSel(nIndex); } } return 0; } LRESULT OnRButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) { POINT ptCursor = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; // Search for a tab T* pT = static_cast(this); CTCHITTESTINFO tchti = { 0 }; tchti.pt = ptCursor; int nIndex = pT->HitTest(&tchti); // returning TRUE tells us not to do our default handling NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), NM_RCLICK }, nIndex, {ptCursor.x, ptCursor.y}}; if(FALSE == ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh)) { // returning FALSE let's us do our default handling if(nIndex >= 0) { // NOTE: If they click on a tab, only grab the focus // if a drag operation is started. //pT->SetFocus(); pT->SetCurSel(nIndex); } } return 0; } LRESULT OnRButtonUp(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { bHandled = FALSE; return 0; } LRESULT OnRButtonDoubleClick(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) { POINT ptCursor = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; // Search for a tab T* pT = static_cast(this); CTCHITTESTINFO tchti = { 0 }; tchti.pt = ptCursor; int nIndex = pT->HitTest(&tchti); // returning TRUE tells us not to do our default handling NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), NM_RDBLCLK }, nIndex, {ptCursor.x, ptCursor.y}}; if(FALSE == ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh)) { // returning FALSE let's us do our default handling if(nIndex >= 0) { //pT->SetFocus(); //pT->SetCurSel(nIndex); } } return 0; } LRESULT OnMButtonDown(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) { POINT ptCursor = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; // Search for a tab T* pT = static_cast(this); CTCHITTESTINFO tchti = { 0 }; tchti.pt = ptCursor; int nIndex = pT->HitTest(&tchti); // returning TRUE tells us not to do our default handling NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_MCLICK }, nIndex, {ptCursor.x, ptCursor.y}}; if(FALSE == ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh)) { // returning FALSE let's us do our default handling if( nIndex!=-1 ) { //pT->SetFocus(); //pT->SetCurSel(nIndex); } } return 0; } LRESULT OnMButtonUp(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& bHandled) { bHandled = FALSE; return 0; } LRESULT OnMButtonDoubleClick(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM lParam, BOOL& /*bHandled*/) { POINT ptCursor = {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; // Search for a tab T* pT = static_cast(this); CTCHITTESTINFO tchti = { 0 }; tchti.pt = ptCursor; int nIndex = pT->HitTest(&tchti); // returning TRUE tells us not to do our default handling NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_MDBLCLK }, nIndex, {ptCursor.x, ptCursor.y}}; if(FALSE == ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh)) { // returning FALSE let's us do our default handling if( nIndex!=-1 ) { //pT->SetFocus(); //pT->SetCurSel(nIndex); } } return 0; } LRESULT OnKeyDown(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) { T* pT = static_cast(this); switch( wParam ) { case VK_LEFT: if( m_iCurSel > 0 ) { pT->SetCurSel(m_iCurSel-1); } return 0; case VK_RIGHT: if( m_iCurSel < (int)m_Items.GetCount()-1 ) { pT->SetCurSel(m_iCurSel+1); } return 0; case VK_ESCAPE: if(ectcDraggingItem == (m_dwState & ectcDraggingItem)) { pT->CancelItemDrag(true); return 0; } break; } bHandled = FALSE; return 0; } LRESULT OnGetFont(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, BOOL& /*bHandled*/) { // DDB: 2002/04/22 // The code was doing GetFont and SetFont, but wasn't actually // properly dealing with implementing it if the window // was not a subclassed static control. return (LRESULT)(HFONT)m_font; } LRESULT OnSetFont(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { // DDB: 2002/04/22 // The code was doing GetFont and SetFont, but wasn't actually // properly dealing with implementing it if the window // was not a subclassed static control. // // Also, we're managing the lifetime of our font // (i.e., we're going to DeleteObject it in our destructor), // so if someone calls SetFont, keep a copy of the // font instead of just "pointing" to it LOGFONT lfCopy = {0}; ::GetObject((HFONT)wParam, sizeof(LOGFONT), &lfCopy); if(!m_font.IsNull()) m_font.DeleteObject(); m_font.CreateFontIndirect(&lfCopy); if(LOWORD(lParam)) { this->Invalidate(); } return 0; } LRESULT OnStyleChanged(UINT /*uMsg*/, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { if(wParam == GWL_STYLE) { LPSTYLESTRUCT pStyles = (LPSTYLESTRUCT)lParam; if(pStyles) { T* pT = static_cast(this); // Tooltips if((((pStyles->styleOld) & CTCS_TOOLTIPS) != CTCS_TOOLTIPS) && (((pStyles->styleNew) & CTCS_TOOLTIPS) == CTCS_TOOLTIPS)) { this->ActivateTooltips(TRUE); } else if((((pStyles->styleOld) & CTCS_TOOLTIPS) == CTCS_TOOLTIPS) && (((pStyles->styleNew) & CTCS_TOOLTIPS) != CTCS_TOOLTIPS)) { this->ActivateTooltips(FALSE); } // Scroll to fit if((((pStyles->styleOld) & CTCS_SCROLL) != CTCS_SCROLL) && (((pStyles->styleNew) & CTCS_SCROLL) == CTCS_SCROLL)) { if(m_tooltip.IsWindow()) { m_tooltip.AddTool(m_hWnd, _T("Scroll Right"), &rcDefault, (UINT)ectcToolTip_ScrollRight); m_tooltip.AddTool(m_hWnd, _T("Scroll Left"), &rcDefault, (UINT)ectcToolTip_ScrollLeft); } //pT->UpdateLayout(); //this->Invalidate(); } else if((((pStyles->styleOld) & CTCS_SCROLL) == CTCS_SCROLL) && (((pStyles->styleNew) & CTCS_SCROLL) != CTCS_SCROLL)) { if(m_tooltip.IsWindow()) { m_tooltip.DelTool(m_hWnd, (UINT)ectcToolTip_ScrollRight); m_tooltip.DelTool(m_hWnd, (UINT)ectcToolTip_ScrollLeft); } m_iScrollOffset = 0; //pT->UpdateLayout(); //this->Invalidate(); } // Close Button if((((pStyles->styleOld) & CTCS_CLOSEBUTTON) != CTCS_CLOSEBUTTON) && (((pStyles->styleNew) & CTCS_CLOSEBUTTON) == CTCS_CLOSEBUTTON)) { if(m_tooltip.IsWindow()) { m_tooltip.AddTool(m_hWnd, _T("Close"), &rcDefault, (UINT)ectcToolTip_Close); } //pT->UpdateLayout(); //this->Invalidate(); } else if((((pStyles->styleOld) & CTCS_CLOSEBUTTON) == CTCS_CLOSEBUTTON) && (((pStyles->styleNew) & CTCS_CLOSEBUTTON) != CTCS_CLOSEBUTTON)) { if(m_tooltip.IsWindow()) { m_tooltip.DelTool(m_hWnd, (UINT)ectcToolTip_Close); } //pT->UpdateLayout(); //this->Invalidate(); } pT->UpdateLayout(); this->Invalidate(); } } bHandled = FALSE; return 0; } LRESULT OnTimer(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) { T* pT = static_cast(this); switch(wParam) { case ectcTimer_ScrollRight: if(ectcOverflowRight != (m_dwState & ectcOverflowRight)) { this->KillTimer(ectcTimer_ScrollRight); } else // ectcOverflowRight == (m_dwState & ectcOverflowRight) { if(ectcDraggingItem == (m_dwState & ectcDraggingItem)) { // We're scrolling because they're dragging a tab near the edge. // First kill the timer, then update the drag // (which might set the timer again) this->KillTimer(ectcTimer_ScrollRight); POINT ptCursor = {0}; ::GetCursorPos(&ptCursor); this->ScreenToClient(&ptCursor); pT->ContinueItemDrag(ptCursor); } else if(ectcMouseDownL_ScrollRight == (m_dwState & ectcMouseDown) && ectcMouseOver_ScrollRight == (m_dwState & ectcMouseOver)) { // We're scrolling because they're holding down the scroll button pT->ScrollRight(true); if(ectcScrollRepeat_None == (m_dwState & ectcScrollRepeat)) { this->KillTimer(ectcTimer_ScrollRight); } } } break; case ectcTimer_ScrollLeft: if(ectcOverflowLeft != (m_dwState & ectcOverflowLeft)) { this->KillTimer(ectcTimer_ScrollLeft); } else // ectcOverflowLeft == (m_dwState & ectcOverflowLeft) { if(ectcDraggingItem == (m_dwState & ectcDraggingItem)) { // We're scrolling because they're dragging a tab near the edge. // First kill the timer, then update the drag // (which might set the timer again) this->KillTimer(ectcTimer_ScrollLeft); POINT ptCursor = {0}; ::GetCursorPos(&ptCursor); this->ScreenToClient(&ptCursor); pT->ContinueItemDrag(ptCursor); } else if(ectcMouseDownL_ScrollLeft == (m_dwState & ectcMouseDown) && ectcMouseOver_ScrollLeft == (m_dwState & ectcMouseOver)) { // We're scrolling because they're holding down the scroll button pT->ScrollLeft(true); if(ectcScrollRepeat_None == (m_dwState & ectcScrollRepeat)) { this->KillTimer(ectcTimer_ScrollLeft); } } } break; default: bHandled = FALSE; break; } return 0; } LRESULT OnSetRedraw(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) { // If someone sends us WM_SETREDRAW with FALSE, we can avoid // doing an update layout until they set it back to TRUE. if(wParam) { if(ectcEnableRedraw != (m_dwState & ectcEnableRedraw)) { // Redrawing was turned off, but now its being // turned back on again m_dwState |= ectcEnableRedraw; T* pT = static_cast(this); pT->UpdateLayout(); // The caller will typically call InvalidateRect // or RedrawWindow after sending WM_SETREDRAW with TRUE, // so we won't do that here (but we will UpdateLayout, // so that we'll be ready to redraw) } } else { if(ectcEnableRedraw == (m_dwState & ectcEnableRedraw)) { // Redrawing was turned on, but now its being turned off m_dwState &= ~ectcEnableRedraw; } } bHandled = FALSE; return 0; } LRESULT OnGetToolTipInfo(int /*idCtrl*/, LPNMHDR pnmh, BOOL& /*bHandled*/) { LPNMTTDISPINFO pToolTipInfo = (LPNMTTDISPINFO)pnmh; if(pToolTipInfo) { // The way we implement tooltips for tab items // is to have as many "tools" as there are tabs. // The relationship of tool ID => tab index is: // tool ID = tab index + 1 (to avoid 0 as an ID) // // We supply the RECT elsewhere and the text here UINT_PTR id = pToolTipInfo->hdr.idFrom; if(id > 0 && id <= m_Items.GetCount()) { TItem* pItem = m_Items[id-1]; ATLASSERT(pItem != NULL); if(pItem) { if(pItem->UsingToolTip()) { pToolTipInfo->lpszText = const_cast(pItem->GetToolTipRef()); } else if(pItem->UsingText()) { pToolTipInfo->lpszText = const_cast(pItem->GetTextRef()); } } } } return 0; } // Overridables public: void Initialize(void) { ATLASSERT(::IsWindow(m_hWnd)); this->SendMessage(WM_SETTINGCHANGE, 0, 0); this->InitializeTooltips(); // NOTE: you can change the style at any time // for a number of the cool tab control styles // (tool tips, close button, scroll buttons, etc.) DWORD dwStyle = this->GetStyle(); this->ActivateTooltips(CTCS_TOOLTIPS == (dwStyle & CTCS_TOOLTIPS)); if(CTCS_SCROLL == (dwStyle & CTCS_SCROLL)) { if(m_tooltip.IsWindow()) { m_tooltip.AddTool(m_hWnd, _T("Scroll Right"), &rcDefault, (UINT)ectcToolTip_ScrollRight); m_tooltip.AddTool(m_hWnd, _T("Scroll Left"), &rcDefault, (UINT)ectcToolTip_ScrollLeft); } } if(CTCS_CLOSEBUTTON == (dwStyle & CTCS_CLOSEBUTTON)) { if(m_tooltip.IsWindow()) { m_tooltip.AddTool(m_hWnd, _T("Close"), &rcDefault, (UINT)ectcToolTip_Close); } } } void Uninitialize(void) { T* pT = static_cast(this); DWORD dwStyle = this->GetStyle(); if(m_tooltip.IsWindow()) { if(CTCS_SCROLL == (dwStyle & CTCS_SCROLL)) { m_tooltip.DelTool(m_hWnd, (UINT)ectcToolTip_ScrollRight); m_tooltip.DelTool(m_hWnd, (UINT)ectcToolTip_ScrollLeft); } if(CTCS_CLOSEBUTTON == (dwStyle & CTCS_CLOSEBUTTON)) { m_tooltip.DelTool(m_hWnd, (UINT)ectcToolTip_Close); } } pT->DeleteAllItems(); if(m_tooltip.IsWindow()) { // Also sets the contained m_hWnd to NULL m_tooltip.DestroyWindow(); } else { m_tooltip = NULL; } } TItem* CreateNewItem(void* pInitData = NULL) { pInitData; // avoid level 4 warning #if defined (_CPPUNWIND) & (defined(_ATL_EXCEPTIONS) | defined(_AFX)) TItem* pNewItem = NULL; try { pNewItem = new TItem; } catch (...) { ATLTRACE(_T("!! Exception thrown in CCustomTabCtrl::CreateNewItem\r\n")); } #else TItem* pNewItem = new TItem; #endif return pNewItem; } void DeleteItem(TItem* pOldItem) { #if defined (_CPPUNWIND) & (defined(_ATL_EXCEPTIONS) | defined(_AFX)) try { delete pOldItem; } catch (...) { ATLTRACE(_T("!! Exception thrown in CCustomTabCtrl::DeleteItem\r\n")); } #else delete pOldItem; #endif } void UpdateLayout(void) { if( !m_hWnd || !::IsWindow(m_hWnd) || (ectcEnableRedraw != (m_dwState & ectcEnableRedraw))) { return; } this->GetClientRect(&m_rcTabItemArea); T* pT = static_cast(this); DWORD dwStyle = this->GetStyle(); pT->CalcSize_NonClient(&m_rcTabItemArea); if(CTCS_CLOSEBUTTON == (dwStyle & CTCS_CLOSEBUTTON)) { if( (m_iCurSel >= 0) && ((size_t)m_iCurSel < m_Items.GetCount()) ) { TItem* pItem = m_Items[m_iCurSel]; ATLASSERT(pItem != NULL); if((pItem != NULL) && pItem->CanClose()) { pT->CalcSize_CloseButton(&m_rcTabItemArea); } } } if(CTCS_SCROLL == (dwStyle & CTCS_SCROLL)) { pT->CalcSize_ScrollButtons(&m_rcTabItemArea); pT->UpdateLayout_ScrollToFit(m_rcTabItemArea); pT->UpdateScrollOverflowStatus(); } else { pT->UpdateLayout_Default(m_rcTabItemArea); } pT->UpdateTabItemTooltipRects(); } void CalcSize_NonClient(LPRECT prcTabItemArea) { } void CalcSize_CloseButton(LPRECT prcTabItemArea) { } void CalcSize_ScrollButtons(LPRECT prcTabItemArea) { } void UpdateLayout_Default(RECT rcTabItemArea) { UpdateLayout_ScrollToFit(rcTabItemArea); } void UpdateLayout_ScrollToFit(RECT rcTabItemArea) { WTL::CClientDC dc(m_hWnd); HFONT hOldFont = dc.SelectFont(this->GetFont()); int height = rcTabItemArea.bottom-rcTabItemArea.top; // Reposition tabs size_t nCount = m_Items.GetCount(); int xpos = m_settings.iIndent; for( size_t i=0; iUsingText() ) { RECT rcText = { 0 }; _CSTRING_NS::CString sText = pItem->GetText(); dc.DrawText(sText, sText.GetLength(), &rcText, DT_SINGLELINE | DT_CALCRECT); rc.right += (rcText.right-rcText.left) + (m_settings.iPadding*2); } pItem->SetRect(rc); xpos += (rc.right-rc.left) + m_settings.iMargin; } } if( (m_iCurSel >= 0) && ((size_t)m_iCurSel < nCount) ) { TItem* pItem = m_Items[m_iCurSel]; ATLASSERT(pItem != NULL); if(pItem) { pItem->InflateRect(m_settings.iSelMargin, 0); } } dc.SelectFont(hOldFont); } void UpdateTabItemTooltipRects(void) { // The way we implement tooltips for tab items // is to have as many "tools" as there are tabs. // The relationship of tool ID => tab index is: // tool ID = tab index + 1 (to avoid 0 as an ID) // // We supply the RECT here and text elsewhere. if(m_tooltip.IsWindow()) { size_t i = 0; size_t nCount = m_Items.GetCount(); RECT rcIntersect = {0}; for(i=0; iGetItemRect(i, &rcItemDP); ::IntersectRect(&rcIntersect, &rcItemDP, &m_rcTabItemArea); // NOTE: Even if IntersectRect determines the rectangles // don't intersect at all, we still need // to update the tool rect, or we'll get the wrong // tooltip in some cases. m_tooltip.SetToolRect(m_hWnd, (UINT)i+1, &rcIntersect); } } } void ScrollLeft(bool bForceRedraw = true) { m_iScrollOffset += ((m_dwState & ectcScrollDeltaMask) >> ectcScrollDeltaShift); T* pT = static_cast(this); pT->UpdateLayout(); pT->UpdateScrollOverflowStatus(); pT->UpdateTabItemTooltipRects(); if(bForceRedraw) { this->Invalidate(); // If something a little more forceful is needed: //::RedrawWindow(m_hWnd, NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW); } } void ScrollRight(bool bForceRedraw = true) { m_iScrollOffset -= ((m_dwState & ectcScrollDeltaMask) >> ectcScrollDeltaShift); T* pT = static_cast(this); pT->UpdateLayout(); pT->UpdateScrollOverflowStatus(); pT->UpdateTabItemTooltipRects(); if(bForceRedraw) { this->Invalidate(); // If something a little more forceful is needed: //::RedrawWindow(m_hWnd, NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW); } } void UpdateScrollOverflowStatus(void) { // Check for overflow left if(m_iScrollOffset >= 0) { m_iScrollOffset = 0; m_dwState &= ~ectcOverflowLeft; } else { m_dwState |= ectcOverflowLeft; } // Check for overflow right m_dwState &= ~ectcOverflowRight; size_t nCount = m_Items.GetCount(); if(nCount > 0) { // Check last item RECT rcItemDP = {0}; this->GetItemRect(nCount-1, &rcItemDP); if(rcItemDP.right > m_rcTabItemArea.right) { m_dwState |= ectcOverflowRight; } } } void DoPaint(CDCHandle dc, RECT &rcClip) { // Save current DC selections int nSave = dc.SaveDC(); ATLASSERT(nSave != 0); DWORD dwStyle = this->GetStyle(); // Make sure we don't paint outside client area (possible with paint dc) RECT rcClient = {0}; GetClientRect(&rcClient); dc.IntersectClipRect(&rcClient); // Prepare DC dc.SelectFont(this->GetFont()); T* pT = static_cast(this); LRESULT lResCustom; NMCTCCUSTOMDRAW nmc = { 0 }; LPNMCUSTOMDRAW pnmcd= &(nmc.nmcd); pnmcd->hdr.hwndFrom = m_hWnd; pnmcd->hdr.idFrom = this->GetDlgCtrlID(); pnmcd->hdr.code = NM_CUSTOMDRAW; pnmcd->hdc = dc; pnmcd->uItemState = 0; pT->InitializeDrawStruct(&nmc); pnmcd->dwDrawStage = CDDS_PREPAINT; lResCustom = ::SendMessage(GetParent(), WM_NOTIFY, pnmcd->hdr.idFrom, (LPARAM)&nmc); if( CDRF_SKIPDEFAULT != (lResCustom & CDRF_SKIPDEFAULT) ) { pT->DoPrePaint(rcClient, &nmc); } if( lResCustom==CDRF_DODEFAULT || CDRF_NOTIFYITEMDRAW==(lResCustom & CDRF_NOTIFYITEMDRAW) ) { POINT ptPreviousViewport = {0}; HRGN hRgnClip = NULL; bool bScrollStyle = (CTCS_SCROLL == (dwStyle & CTCS_SCROLL)); bool bHotTrackStyle = (CTCS_HOTTRACK == (dwStyle & CTCS_HOTTRACK)); if(bScrollStyle) { // Remember clip region before we modify it // to only include the tab item area // NOTE: GetClipRgn expects an already created // region, so we create one with an empty rectangle. hRgnClip = ::CreateRectRgn(0,0,0,0); ::GetClipRgn(dc, hRgnClip); dc.IntersectClipRect(&m_rcTabItemArea); dc.SetViewportOrg(m_iScrollOffset, 0, &ptPreviousViewport); } size_t nCount = m_Items.GetCount(); // Draw the list items, except the selected one. It is drawn last // so it can cover the tabs below it. RECT rcIntersect = {0}; for( size_t i=0; iGetRect(); ::CopyRect(&rcItemDP, &rcItemLP); ::OffsetRect(&rcItemDP, m_iScrollOffset, 0); if( ::IntersectRect(&rcIntersect, &rcItemDP, &rcClip) ) { pnmcd->dwItemSpec = i; pnmcd->uItemState = 0; if(bHotTrackStyle && ((DWORD)m_iHotItem == i)) { pnmcd->uItemState |= CDIS_HOT; } if(pItem->IsHighlighted()) { pnmcd->uItemState |= CDIS_MARKED; } pnmcd->rc = rcItemLP; pT->ProcessItem(lResCustom, &nmc); } } } } if( m_iCurSel >=0 && (size_t)m_iCurSel < nCount ) { TItem* pItem = m_Items[m_iCurSel]; ATLASSERT(pItem != NULL); if(pItem) { RECT rcItemLP = {0}, rcItemDP = {0}; rcItemLP = pItem->GetRect(); ::CopyRect(&rcItemDP, &rcItemLP); ::OffsetRect(&rcItemDP, m_iScrollOffset, 0); if( ::IntersectRect(&rcIntersect, &rcItemDP, &rcClip) ) { pnmcd->dwItemSpec = m_iCurSel; pnmcd->uItemState = CDIS_SELECTED; if(bHotTrackStyle && (m_iHotItem == m_iCurSel)) { pnmcd->uItemState |= CDIS_HOT; } if(pItem->IsHighlighted()) { pnmcd->uItemState |= CDIS_MARKED; } pnmcd->rc = rcItemLP; pT->ProcessItem(lResCustom, &nmc); } } } pnmcd->uItemState = 0; pnmcd->dwItemSpec = 0; if(bScrollStyle) { dc.SetViewportOrg(ptPreviousViewport); dc.SelectClipRgn(hRgnClip); ::DeleteObject(hRgnClip); } } if( CDRF_SKIPDEFAULT != (lResCustom & CDRF_SKIPDEFAULT) ) { pT->DoPostPaint(rcClient, &nmc); } if( CDRF_NOTIFYPOSTPAINT == (lResCustom & CDRF_NOTIFYPOSTPAINT) ) { pnmcd->dwDrawStage = CDDS_POSTPAINT; pnmcd->dwItemSpec = 0; pnmcd->uItemState = 0; pnmcd->rc = rcClient; ::SendMessage(GetParent(), WM_NOTIFY, pnmcd->hdr.idFrom, (LPARAM)&nmc); } dc.RestoreDC(nSave); } void ProcessItem(LRESULT lResCustom, LPNMCTCCUSTOMDRAW lpNMCustomDraw) { LRESULT lResItem = CDRF_DODEFAULT; if( CDRF_NOTIFYITEMDRAW == (lResCustom & CDRF_NOTIFYITEMDRAW) ) { lpNMCustomDraw->nmcd.dwDrawStage = CDDS_ITEMPREPAINT; lResItem = ::SendMessage(GetParent(), WM_NOTIFY, lpNMCustomDraw->nmcd.hdr.idFrom, (LPARAM)lpNMCustomDraw); } if( CDRF_SKIPDEFAULT != (lResCustom & CDRF_SKIPDEFAULT) ) { // Do default item-drawing T* pT = static_cast(this); pT->DoItemPaint(lpNMCustomDraw); } if( CDRF_NOTIFYITEMDRAW == (lResCustom & CDRF_NOTIFYITEMDRAW) && CDRF_NOTIFYPOSTPAINT == (lResItem & CDRF_NOTIFYPOSTPAINT)) { lpNMCustomDraw->nmcd.dwDrawStage = CDDS_ITEMPOSTPAINT; ::SendMessage(GetParent(), WM_NOTIFY, lpNMCustomDraw->nmcd.hdr.idFrom, (LPARAM)lpNMCustomDraw); } } void InitializeDrawStruct(LPNMCTCCUSTOMDRAW lpNMCustomDraw) { DWORD dwStyle = this->GetStyle(); lpNMCustomDraw->hFontInactive = m_font; lpNMCustomDraw->hFontSelected = m_fontSel; lpNMCustomDraw->hBrushBackground = ::GetSysColorBrush(COLOR_BTNFACE); lpNMCustomDraw->clrTextSelected = ::GetSysColor(COLOR_BTNTEXT); lpNMCustomDraw->clrTextInactive = ::GetSysColor(COLOR_BTNTEXT); lpNMCustomDraw->clrSelectedTab = ::GetSysColor(COLOR_BTNFACE); lpNMCustomDraw->clrBtnFace = ::GetSysColor(COLOR_BTNFACE); lpNMCustomDraw->clrBtnShadow = ::GetSysColor(COLOR_BTNSHADOW); lpNMCustomDraw->clrBtnHighlight = ::GetSysColor(COLOR_BTNHIGHLIGHT); lpNMCustomDraw->clrBtnText = ::GetSysColor(COLOR_BTNTEXT); lpNMCustomDraw->clrHighlight = ::GetSysColor(COLOR_HIGHLIGHT); #if WINVER >= 0x0500 || _WIN32_WINNT >= 0x0500 lpNMCustomDraw->clrHighlightHotTrack = ::GetSysColor(COLOR_HOTLIGHT); #else lpNMCustomDraw->clrHighlightHotTrack = ::GetSysColor(COLOR_HIGHLIGHT); #endif lpNMCustomDraw->clrHighlightText = ::GetSysColor(COLOR_HIGHLIGHTTEXT); } void DoPrePaint(RECT rcClient, LPNMCTCCUSTOMDRAW lpNMCustomDraw) { // "Erase Background" // NOTE: Your derived class might be able to do a // better job of erasing only the necessary area // (using the clip box, etc.) WTL::CDCHandle dc(lpNMCustomDraw->nmcd.hdc); HBRUSH hOldBrush = dc.SelectBrush(lpNMCustomDraw->hBrushBackground); dc.PatBlt(rcClient.left, rcClient.top, rcClient.right-rcClient.left, rcClient.bottom-rcClient.top, PATCOPY); dc.SelectBrush(hOldBrush); dc.SetTextColor(lpNMCustomDraw->clrTextInactive); dc.SetBkMode(TRANSPARENT); } void DoItemPaint(LPNMCTCCUSTOMDRAW /*lpNMCustomDraw*/) { } void DoPostPaint(RECT /*rcClient*/, LPNMCTCCUSTOMDRAW /*lpNMCustomDraw*/) { // In your derived verion, paint the scroll buttons if CTCS_SCROLL // is set and the close button if CTCS_CLOSEBUTTON is set // (if you want to support these) } // Operations public: BOOL SubclassWindow(HWND hWnd) { ATLASSERT(m_hWnd == NULL); ATLASSERT(::IsWindow(hWnd)); BOOL bRet = baseClass::SubclassWindow(hWnd); if( bRet ) { T* pT = static_cast(this); pT->Initialize(); } return bRet; } HWND UnsubclassWindow(BOOL bForce = FALSE) { T* pT = static_cast(this); pT->Uninitialize(); return baseClass::UnsubclassWindow(bForce); } WTL::CImageList SetImageList(HIMAGELIST hImageList) { CImageList imageListOld = m_imageList; m_imageList = hImageList; return imageListOld; } WTL::CImageList& GetImageList() const { return m_imageList; } WTL::CToolTipCtrl GetTooltips() const { return m_tooltip; } //void SetTooltips(HWND hWndToolTip) //{ // ATLASSERT(::IsWindow(m_hWnd)); // ::SendMessage(m_hWnd, TCM_SETTOOLTIPS, (WPARAM)hWndToolTip, 0L); //} bool SetScrollDelta(UINT nDelta) { if(nDelta > (ectcScrollDeltaMask >> ectcScrollDeltaShift)) { return false; } m_dwState |= ((nDelta << ectcScrollDeltaShift) & ectcScrollDeltaMask); return true; } UINT GetScrollDelta(void) { return ((m_dwState & ectcScrollDeltaMask) >> ectcScrollDeltaShift); } bool SetScrollRepeat(ScrollRepeat ectcNewScrollRepeat = ectcScrollRepeat_Normal) { m_dwState &= ~ectcScrollRepeat; m_dwState |= (ectcNewScrollRepeat & ectcScrollRepeat); return true; } ScrollRepeat GetScrollRepeat(void) { return (m_dwState & ectcScrollRepeat); } int InsertItem(int nItem, LPCTSTR sText = NULL, int nImage = -1, LPCTSTR sToolTip = NULL, bool bSelectItem = false) { T* pT = static_cast(this); TItem* pItem = pT->CreateNewItem(); if(pItem) { pItem->SetText(sText); pItem->SetImageIndex(nImage); pItem->SetToolTip(sToolTip); return InsertItem(nItem, pItem, bSelectItem); } return -1; } int InsertItem(int nItem, TItem* pItem, bool bSelectItem = false) { T* pT = static_cast(this); ATLASSERT(::IsWindow(m_hWnd)); ATLASSERT(pItem); ATLASSERT(nItem >= 0 && nItem <= (int)m_Items.GetCount()); if(!::IsWindow(m_hWnd) || pItem == NULL) { return -1; } size_t nOldCount = m_Items.GetCount(); if( nItem < 0 || nItem > (int)nOldCount ) { nItem = (int)nOldCount; } m_Items.InsertAt((size_t)nItem, pItem); size_t nNewCount = m_Items.GetCount(); // Send notification NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_INSERTITEM }, nItem, {-1,-1}}; ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh); // Select if first tab if( nNewCount==1 ) { pT->SetCurSel(0); } else if(bSelectItem) { pT->SetCurSel(nItem); } // The way we implement tooltips is to have as many // "tools" as there are tabs. The relationship of // tool ID => tab index is: // tool ID = tab index + 1 (to avoid 0 as an ID) // // We supply the RECT and text elsewhere. if(m_tooltip.IsWindow()) { m_tooltip.AddTool(m_hWnd, LPSTR_TEXTCALLBACK, &rcDefault, (UINT)nNewCount); } pT->UpdateLayout(); this->Invalidate(); return nItem; } BOOL MoveItem(size_t nFromIndex, size_t nToIndex, bool bUpdateSelection = true, bool bNotify = true) { T* pT = static_cast(this); ATLASSERT(::IsWindow(m_hWnd)); ATLASSERT(nFromIndex < m_Items.GetCount()); ATLASSERT(nToIndex < m_Items.GetCount()); if(!::IsWindow(m_hWnd) || nFromIndex >= m_Items.GetCount() || nToIndex >= m_Items.GetCount()) { return FALSE; } TItem* pFromItem = m_Items[nFromIndex]; ATLASSERT(pFromItem != NULL); m_Items.RemoveAt(nFromIndex); m_Items.InsertAt(nToIndex, pFromItem); // The number of items is staying the same, so m_iCurSel // won't be invalid whether it gets updated or not if(bUpdateSelection) { if(nFromIndex == (size_t)m_iCurSel) { pT->SetCurSel((int)nToIndex); } } if(bNotify) { NMCTC2ITEMS nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_MOVEITEM }, (int)nFromIndex, (int)nToIndex, {-1,-1}}; ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh); } return TRUE; } BOOL SwapItemPositions(size_t nFromIndex, size_t nToIndex, bool bUpdateSelection = true, bool bNotify = true) { T* pT = static_cast(this); ATLASSERT(::IsWindow(m_hWnd)); ATLASSERT(nFromIndex < m_Items.GetCount()); ATLASSERT(nToIndex < m_Items.GetCount()); if(!::IsWindow(m_hWnd) || nFromIndex >= m_Items.GetCount() || nToIndex >= m_Items.GetCount()) { return FALSE; } TItem* pFromItem = m_Items[nFromIndex]; TItem* pToItem = m_Items[nToIndex]; ATLASSERT(pFromItem != NULL); ATLASSERT(pToItem != NULL); m_Items[nFromIndex] = pToItem; m_Items[nToIndex] = pFromItem; // The number of items is staying the same, so m_iCurSel // won't be invalid whether it gets updated or not if(bUpdateSelection) { if(nFromIndex == (size_t)m_iCurSel) { pT->SetCurSel((int)nToIndex); } else if(nToIndex == (size_t)m_iCurSel) { pT->SetCurSel((int)nFromIndex); } } if(bNotify) { NMCTC2ITEMS nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_SWAPITEMPOSITIONS }, (int)nFromIndex, (int)nToIndex, {-1,-1}}; ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh); } return TRUE; } BOOL DeleteItem(size_t nItem, bool bUpdateSelection = true, bool bNotify = true) { T* pT = static_cast(this); ATLASSERT(::IsWindow(m_hWnd)); size_t nOldCount = m_Items.GetCount(); if( nItem >= nOldCount ) { return FALSE; } // Send notification if(bNotify) { // Returning TRUE tells us not to delete the item NMCTCITEM nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_DELETEITEM }, (int)nItem, {-1,-1}}; if( TRUE == ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh) ) { // Cancel the attempt return FALSE; } else { // Just in case handler of the notification // changed the count somehow, get it again nOldCount = m_Items.GetCount(); if( nItem >= nOldCount ) { return FALSE; } } } int nPostDeleteSelection = -1; // The number of items is changing, so m_iCurSel // might be invalid if we don't change it. if(nOldCount <= 1) { // There's only one item left, // and its being deleted. m_iCurSel = -1; } else if(bUpdateSelection) { if( (int)nItem < m_iCurSel ) { // The item being removed is before the current selection. // We still want the same item to be selected, but // the index needs to be adjusted to account for the missing item m_iCurSel--; pT->EnsureVisible(m_iCurSel); // NOTE: We don't call SetCurSel because we don't want // any of the notifications to go to the parent - because // the selection didn't change, the index just had to be adjusted. } else if( (int)nItem == m_iCurSel ) { // If the item to be deleted is currently selected, // select something else. // NOTE: We've already handled the "we're deleting // the only remaining item" case. if(nItem >= (nOldCount-1)) { // The selected item was the last item, // and there will still be at least one // item after this deletion occurs. // Select the item that will be the // new last item. // We need to do this before the actual // deletion so that m_iCurSel // will still be valid during the SetCurSel // call and the notification handlers of // CTCN_SELCHANGING and CTCN_SELCHANGE pT->SetCurSel(m_iCurSel-1); } else { // The selected item was NOT the last item, // and there will still be at least one // item after this deletion occurs. // Force a selection of the item that will // have the same index as the selected item being // deleted, but do it after the actual deletion. //pT->SetCurSel(m_iCurSel); nPostDeleteSelection = m_iCurSel; } } } else { if(((int)nItem == m_iCurSel) && (nItem >= (nOldCount-1))) { // If bUpdateSelection is false, // the item being deleted is selected, // and the item being deleted is the last // item, we need to clear the current selection // (setting m_iCurSel to -1) // so that our call to UpdateLayout // and our paint message handling don't // crash and burn with an invalid selected // item index. Its likely that the // caller is going to SetCurSel right // after this call to DeleteItem. m_iCurSel = -1; } } // Remove tooltip and item // The way we implement tooltips is to have as many // "tools" as there are tabs. The relationship of // tool ID => tab index is: // tool ID = tab index + 1 (to avoid 0 as an ID) // // We supply the RECT and text elsewhere. if(m_tooltip.IsWindow()) { m_tooltip.DelTool(m_hWnd, (UINT)m_Items.GetCount()); } TItem* pItem = m_Items[nItem]; ATLASSERT(pItem != NULL); m_Items.RemoveAt(nItem); pT->DeleteItem(pItem); if(nPostDeleteSelection >= 0) { pT->SetCurSel(nPostDeleteSelection); } // Repaint pT->UpdateLayout(); this->Invalidate(); return TRUE; } BOOL DeleteAllItems(bool bForceRedraw = false) { ATLASSERT(::IsWindow(m_hWnd)); m_iCurSel = -1; this->SendMessage(WM_SETREDRAW, FALSE, 0); while( GetItemCount()>0 ) DeleteItem(0U, false, true); this->SendMessage(WM_SETREDRAW, TRUE, 0); // UpdateLayout will already have been called // when sending WM_SETREDRAW with TRUE after // having sent it with FALSE // (or in DeleteItem if we weren't doing WM_SETREDRAW stuff) if(bForceRedraw) { this->Invalidate(); // If something a little more forceful is needed: //::RedrawWindow(m_hWnd, NULL, NULL, RDW_INVALIDATE | RDW_UPDATENOW); } return TRUE; } // NOTE: Now, instead of calling SetItem, call // GetItem to get the pointer to the TItem, update // what you want, then call UpdateLayout and Invalidate // if appropriate. /* BOOL SetItem(int nItem, TItem* pItem) { ATLASSERT(::IsWindow(m_hWnd)); ATLASSERT(!::IsBadReadPtr(pItem,sizeof(TItem))); CHECK_ITEM(nItem); // Copy caller's data to the internal structure TItem* pItemT = m_Items[nItem]; UINT mask = pItem->mask; if( mask & TCIF_TEXT ) { pItemT->SetText(pItem->pszText); } // PBI: Added support for ImageList. if( mask & TCIF_IMAGE ) { pItemT->iImage = pItem->iImage; pItemT->mask |= TCIF_IMAGE; } if( mask & TCIF_PARAM ) { pItemT->lParam = pItem->lParam; pItemT->mask |= TCIF_PARAM; } // Repaint control T* pT = static_cast(this); pT->UpdateLayout(); this->Invalidate(); return TRUE; } */ TItem* GetItem(size_t nItem) const { ATLASSERT(nItem < m_Items.GetCount()); if( nItem >= m_Items.GetCount() ) { return NULL; } return m_Items[nItem]; } int SetCurSel(int nItem, bool bNotify = true) { T* pT = static_cast(this); ATLASSERT(::IsWindow(m_hWnd)); // NEW (DDB): // Even if the newly requested selection index is // the same index as the previous selection index, // the item might be different (as in the case of // InsertItem inserting a new item where the old // selection used to be). We also call EnsureVisible // and UpdateLayout in this method, which we will want // called even if it is the same item // // OLD: // // Selecting same tab? If so, we won't go through all the notifications // if( (int)nItem==m_iCurSel ) return m_iCurSel; if( nItem >= (int)m_Items.GetCount() ) { nItem = m_iCurSel; } int iOldSel = m_iCurSel; // Send notification NMCTC2ITEMS nmh = {{ m_hWnd, this->GetDlgCtrlID(), CTCN_SELCHANGING }, iOldSel, nItem, {-1,-1}}; if(bNotify) { if( TRUE == ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh) ) { // Cancel the attempt return -1; } } // Change tab m_iCurSel = nItem; if(m_iCurSel >= 0) { pT->EnsureVisible(m_iCurSel); } // Recalc new layout and redraw pT->UpdateLayout(); this->Invalidate(); if(bNotify) { // Send notification nmh.hdr.code = CTCN_SELCHANGE; ::SendMessage(GetParent(), WM_NOTIFY, nmh.hdr.idFrom, (LPARAM)&nmh); } return iOldSel; } int GetCurSel() const { return m_iCurSel; } int GetItemCount() const { return (int)m_Items.GetCount(); } int HitTest(LPCTCHITTESTINFO pHitTestInfo) const { ATLASSERT(!::IsBadWritePtr(pHitTestInfo,sizeof(CTCHITTESTINFO))); pHitTestInfo->flags = CTCHT_NOWHERE; if(::PtInRect(&m_rcTabItemArea, pHitTestInfo->pt)) { // TODO: Do a smarter search. Currently, // the tabs are always going to be left to right. // Use this knowledge to do a better spacial search. RECT rcItemDP = {0}; size_t nCount = m_Items.GetCount(); for( size_t i=0; iGetItemRect(i, &rcItemDP); if( ::PtInRect(&rcItemDP, pHitTestInfo->pt) ) { // TODO: check for ONITEMLABEL, ONITEMICON pHitTestInfo->flags = CTCHT_ONITEM; return (int)i; } } } else { if(::PtInRect(&m_rcCloseButton, pHitTestInfo->pt)) { pHitTestInfo->flags = CTCHT_ONCLOSEBTN; } else if(::PtInRect(&m_rcScrollRight, pHitTestInfo->pt)) { pHitTestInfo->flags = CTCHT_ONSCROLLRIGHTBTN; } else if(::PtInRect(&m_rcScrollLeft, pHitTestInfo->pt)) { pHitTestInfo->flags = CTCHT_ONSCROLLLEFTBTN; } } return -1; } bool EnsureVisible(int nItem, bool bPartialOK = false, bool bRecalcAndRedraw = false) { bool bAdjusted = false; // Adjust scroll offset so that item is visible if(0 != (m_dwState & (ectcOverflowLeft|ectcOverflowRight))) { if(nItem < 0 || nItem >= (int)m_Items.GetCount()) { return false; } // TODO: Depend on some system metric for this value int nScrollToViewPadding = 20; RECT rcItemDP = {0}; this->GetItemRect(nItem, &rcItemDP); if(rcItemDP.left < m_rcTabItemArea.left) { if(!bPartialOK || (rcItemDP.right < m_rcTabItemArea.left)) { m_iScrollOffset += (m_rcTabItemArea.left-rcItemDP.left) + nScrollToViewPadding; bAdjusted = true; } } else if(rcItemDP.right > m_rcTabItemArea.right) { if(!bPartialOK || (rcItemDP.left > m_rcTabItemArea.right)) { m_iScrollOffset -= (rcItemDP.right-m_rcTabItemArea.right) + nScrollToViewPadding; bAdjusted = true; } } // Note: UpdateLayout should call UpdateScrollOverflowStatus which // will catch m_iScrollOffset being scrolled too far either direction } if(bAdjusted && bRecalcAndRedraw) { T* pT = static_cast(this); if(bRecalcAndRedraw) { pT->UpdateLayout(); this->Invalidate(); } else { pT->UpdateScrollOverflowStatus(); pT->UpdateTabItemTooltipRects(); } } return true; } int GetRowCount() const { return 1; } DWORD SetItemSize(size_t nItem, int cx, int cy) { ATLASSERT(::IsWindow(m_hWnd)); ATLASSERT(nItem < m_Items.GetCount()); if( nItem >= m_Items.GetCount() ) { return 0; } // TODO: Review this method. It seems that all the tabs // after the one being set would have the wrong RECT. // (unless the caller is iterating through all of the items) TItem* pItem = m_Items[nItem]; ATLASSERT(pItem != NULL); RECT rcOld = pItem->GetRect(); RECT rcNew = { rcOld.left, rcOld.top, rcOld.left + cx, rcOld.top cy }; pItem->SetRect(rcNew); T* pT = static_cast(this); pT->UpdateLayout(); this->Invalidate(); return MAKELONG(rcOld.right-rcOld.left, rcOld.bottom-rcOld.top); } BOOL GetItemRect(size_t nItem, RECT *prcItem) const { ATLASSERT(prcItem); if( prcItem == NULL ) return FALSE; if( nItem >= m_Items.GetCount() ) { ::SetRectEmpty(prcItem); return FALSE; } TItem* pItem = m_Items[nItem]; ATLASSERT(pItem != NULL); if(pItem) { *prcItem = pItem->GetRect(); } // Adjust for any scroll, so that the caller // gets the RECT in device coordinates // instead of logical coordinates ::OffsetRect(prcItem, m_iScrollOffset, 0); return TRUE; } BOOL HighlightItem(size_t nItem, bool bHighlight = true) { ATLASSERT(nItem < m_Items.GetCount()); if(nItem >= m_Items.GetCount()) { return FALSE; } TItem* pItem = m_Items[nItem]; ATLASSERT(pItem != NULL); if(pItem) { bool bCurrentHighlight = pItem->IsHighlighted(); if(bCurrentHighlight != bHighlight) { pItem->SetHighlighted(bHighlight); RECT rcItem = {0}; this->GetItemRect(nItem, &rcItem); this->InvalidateRect(&rcItem); } } return TRUE; } void SetPadding(int iPadding) { m_iPadding = iPadding; T* pT = static_cast(this); pT->UpdateLayout(); this->Invalidate(); }; // FindItem: Find the next tab item matching the search criteria // The functions is meant to mimic how // CListViewCtrl::FindItem and LVM_FINDITEM work, // since there are no comparable messages or functions // for a tab control // // eFlags should specify a mask of things to check for, // and have the corresponding fields set in pFindItem. // For example, set the flags to eCustomTabItem_TabView and set the // tab view on pFindItem to search for a tab with a particular HWND. // If nStart is -1, the search begins from the beginning. // If nStart is not -1, the search begins with the item // just after nStart (like with LVM_FINDITEM). // If a matching item is found, its index is returned. // Otherwise -1 is returned. int FindItem(TItem* pFindItem, DWORD eFlags, int nStart = -1) const { if(nStart < 0) { nStart = -1; } // Find the next item matching the criteria specified size_t nCount = m_Items.GetCount(); for( size_t i=(nStart+1); i < nCount; ++i ) { if(m_Items[i]->MatchItem(pFindItem, eFlags)) { return (int)i; } } return -1; } }; #endif // __CUSTOMTABCTRL_H__