/***********************************************************************/ /* Copyright (C) 2002 Definitive Solutions, Inc. All Rights Reserved. */ /* THIS COMPUTER PROGRAM IS PROPRIETARY AND CONFIDENTIAL TO DEFINITIVE */ /* SOLUTIONS, INC. AND ITS LICENSORS AND CONTAINS TRADE SECRETS OF */ /* DEFINITIVE SOLUTIONS, INC. THAT ARE PROVIDED PURSUANT TO A WRITTEN */ /* AGREEMENT CONTAINING RESTRICTIONS ON USE AND DISCLOSURE. ANY USE, */ /* REPRODUCTION, OR TRANSFER EXCEPT AS PROVIDED IN SUCH AGREEMENT */ /* IS STRICTLY PROHIBITED. */ /***********************************************************************/ #include "stdafx.h" #include "MyListCtrl.h" #include "MyListCtrlResource.h" #include "MyRichEditCtrl.h" #include "MyRegistry.h" #include "Generic.h" #include "MyLog.h" #include "MyApp.h" #include "MyMenu.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // MyHeaderCtrl // Constructor. MyHeaderCtrl::MyHeaderCtrl() { } // Destructor. /* virtual */ MyHeaderCtrl::~MyHeaderCtrl() { } BEGIN_MESSAGE_MAP(MyHeaderCtrl, CHeaderCtrl) //{{AFX_MSG_MAP(MyHeaderCtrl) ON_WM_LBUTTONDBLCLK() //}}AFX_MSG_MAP END_MESSAGE_MAP() ///////////////////////////////////////////////////////////////////////////// // MyListCtrl // Constructor. MyListCtrl::MyListCtrl() : m_pFont(NULL) , m_nPoints(0) , m_nColumnSort(-1) , m_bSortAscending(true) , m_bNumericSort(false) { } // Destructor. /* virtual */ MyListCtrl::~MyListCtrl() { delete m_pFont; m_pFont = NULL; } IMPLEMENT_DYNAMIC(MyListCtrl, CListCtrl) BEGIN_MESSAGE_MAP(MyListCtrl, CListCtrl) //{{AFX_MSG_MAP(MyListCtrl) ON_NOTIFY_REFLECT(NM_RCLICK, OnRclick) ON_COMMAND(ID_MYLISTCTRL_COPY, OnMylistctrlCopy) ON_COMMAND(ID_MYLISTCTRL_SELECTALL, OnMylistctrlSelectAll) ON_COMMAND(ID_MYLISTCTRL_FONT_UP, OnMylistctrlFontUp) ON_COMMAND(ID_MYLISTCTRL_FONT_DOWN, OnMylistctrlFontDown) ON_COMMAND(ID_MYLISTCTRL_GRIDLINES, OnMylistctrlGridlines) ON_COMMAND(ID_MYLISTCTRL_COLUMNS, OnMylistctrlColumns) ON_WM_INITMENUPOPUP() ON_NOTIFY_REFLECT(LVN_COLUMNCLICK, OnColumnclick) //}}AFX_MSG_MAP ON_COMMAND_RANGE(MYLISTCTRL_FIRST_COLUMN_MENUID, MYLISTCTRL_LAST_COLUMN_MENUID, OnColumnMenuSelect) END_MESSAGE_MAP() // User wants to resize the column width. void MyHeaderCtrl::OnLButtonDblClk(UINT nFlags, CPoint point) { VALIDATE; HRESULT hr(S_OK); MyListCtrl* pParent = dynamic_cast (GetParent()); ASSERT_VALID(pParent); if (pParent) { CRect rcCell; int nCol(-1); pParent->GetCellRectFromPoint(point, rcCell, nCol); if (-1 < nCol && nCol < GetItemCount()) { EC_B(pParent->SetColumnWidth(nCol, LVSCW_AUTOSIZE_USEHEADER)); } } // Don't call the base method - it will undo all our work! // CHeaderCtrl::OnLButtonDblClk(nFlags, point); } ///////////////////////////////////////////////////////////////////////////// // MyListCtrl message handlers // Does the sent item exist? bool MyListCtrl::IsItem(int nItem) const { _ASSERTE(0 <= nItem); return -1 < nItem && GetItemCount() > nItem; } // Does the sent column exist? bool MyListCtrl::IsColumn(int nCol) const { VALIDATE; _ASSERTE(0 <= nCol); _ASSERTE(nCol < MYLISTCTRL_LAST_COLUMN_MENUID - MYLISTCTRL_FIRST_COLUMN_MENUID && "Increase MYLISTCTRL_LAST_COLUMN_MENUID as needed"); // One of the few examples of when this cast is okay. (Microsoft forgot // to supply a const CListCtrl::GetHeaderCtrl() - doh!) return 0 <= nCol && nCol < const_cast(this)->GetHeaderCtrl()->GetItemCount(); } // Returns the first item that is selected by default; if arg is passed, // returns the first one after that arg. Returns -1 if no selected item // found. Does not wrap. int MyListCtrl::GetSelectedItem(int nStartItem /* = -1 */ ) const { _ASSERTE(-1 <= nStartItem); return GetNextItem(nStartItem, LVNI_SELECTED); } // Selects the sent item. bool MyListCtrl::SelectItem(int nItem, bool bSelect /* = true */ ) { VALIDATE; _ASSERTE(0 <= nItem); return SetItemState(nItem, bSelect ? LVIS_SELECTED : 0, LVIS_SELECTED); } // Is this item selected? bool MyListCtrl::IsSelected(int nItem) const { VALIDATE; _ASSERTE(0 <= nItem); UINT uiState(GetItemState(nItem, LVIS_SELECTED)); return LVIS_SELECTED == uiState; } // Selects/deselects all the items in the control. bool MyListCtrl::SelectAll(bool bSelect /* = true */ ) { VALIDATE; HRESULT hr(S_OK); int nItem(-1); while (-1 != (nItem = GetNextItem(nItem, bSelect ? 0 : LVNI_SELECTED))) { EC_B(SetItemState(nItem, bSelect ? LVIS_SELECTED : 0, LVIS_SELECTED)); if (FAILED(hr)) break; } return SUCCEEDED(hr); } // Return an array filled with the selected item numbers. Returns the // number of items actually placed in the array, or 0 on failure. int MyListCtrl::GetSelItems(int nMaxItems, int* pnItemArray) const { _ASSERTE(0 < nMaxItems && "This is the size of the array pointed to by pnItemArray"); _ASSERTE(pnItemArray && "Caller must 'new' an array of int's and sent us that pointer"); int nReturn(0); if (pnItemArray) { ::ZeroMemory(pnItemArray, sizeof(int) * nMaxItems); POSITION pos(GetFirstSelectedItemPosition()); while (pos) { _ASSERTE(-1 < nReturn && nReturn < nMaxItems && "Out of array bounds"); int nItem(GetNextSelectedItem(pos)); pnItemArray[nReturn] = nItem; ++nReturn; } } return nReturn; } // Returns the item that is focused. int MyListCtrl::GetFocusedItem() const { return GetNextItem(-1, LVNI_FOCUSED); } // Focuses the sent item. bool MyListCtrl::FocusItem(int nItem) { _ASSERTE(0 <= nItem); return SetItemState(nItem, LVIS_FOCUSED, LVIS_FOCUSED); } // Is this item focused? bool MyListCtrl::IsFocused(int nItem) const { _ASSERTE(0 <= nItem); UINT uiState(GetItemState(nItem, LVIS_FOCUSED)); return LVIS_FOCUSED == uiState; } // Is this item visible. bool MyListCtrl::IsItemVisible(int nItem) const { _ASSERTE(0 <= nItem); int nTop(GetTopIndex()); int nCount(GetCountPerPage()); return nItem >= nTop && nItem <= nTop + nCount; } // Copy the selected items (or all, if none are selected) to the clipboard. bool MyListCtrl::CopySelectedToClipboard(CWnd* pwndCount /* = NULL */ ) { VALIDATE; HRESULT hr(S_OK); bool bHadToSelectAll(false); // If none selected, select them all. if (0 == GetSelectedCount()) { bHadToSelectAll = true; SelectAll(true); } Generic::SpinTheMessageLoop(); // Write the header out. CString sItem; GetHeadersText(sItem); // Only process selected items. CWaitCursor* pwc = new CWaitCursor; CString sItems(sItem + "\r\n"); int nCols(GetHeaderCtrl()->GetItemCount()); DWORD dwStart(::GetTickCount()); POSITION pos(GetFirstSelectedItemPosition()); for (int nSelectedItem = 0; pos; ++nSelectedItem) { int nItem(GetNextSelectedItem(pos)); sItem.Empty(); for (int nSubItem = 0; nSubItem < nCols; ++nSubItem) { CString sSubItem(GetItemText(nItem, nSubItem)); sItem += sSubItem; if (nSubItem < nCols - 1) { sItem += "\t"; } } sItems += sItem + "\r\n"; // Update the UI. if (pwndCount && 500 < ::GetTickCount() - dwStart) { dwStart = ::GetTickCount(); CString sItem; sItem.Format("%d", nSelectedItem); pwndCount->SetWindowText(sItem); delete pwc; pwc = NULL; Generic::SpinTheMessageLoop( /* bNoDrawing */ false, /* bOnlyDrawing */ true); pwc = new CWaitCursor; } } // Update the UI to reflect the total record count. if (pwndCount) { CString sItem; sItem.Format("%d", nSelectedItem); pwndCount->SetWindowText(sItem); } // Copy it to the clipboard. EC_B(OpenClipboard()); EC_B(EmptyClipboard()); if (SUCCEEDED(hr)) { HANDLE hMem = GlobalAlloc(GMEM_FIXED, sItems.GetLength() + 1); char* pStr = reinterpret_cast (GlobalLock(hMem)); lstrcpy(pStr, sItems); EC_B(GlobalUnlock(hMem)); EC_B(::SetClipboardData(CF_TEXT, hMem)); EC_B_(CloseClipboard()); } // Deselect them all it we selected them all. if (bHadToSelectAll) { SelectAll(false); } // Done. delete pwc; pwc = NULL; return SUCCEEDED(hr); } // Return the text of the column headers for output. void MyListCtrl::GetHeadersText(CString& sHeader) { VALIDATE; _ASSERTE(sHeader.IsEmpty() && "Don't want to send this populated"); sHeader.Empty(); int nCols(GetHeaderCtrl()->GetItemCount()); for (int nSubItem = 0; nSubItem < nCols; ++nSubItem) { CString sSubItem; HDITEM hdi = { 0 }; hdi.mask = HDI_TEXT; hdi.cchTextMax = 256; hdi.pszText = sSubItem.GetBuffer(hdi.cchTextMax); VERIFY(GetHeaderCtrl()->GetItem(nSubItem, &hdi)); sSubItem.ReleaseBuffer(); sHeader += sSubItem; if (nSubItem < nCols - 1) { sHeader += "\t"; } } } // NM_RCLICK void MyListCtrl::OnRclick(NMHDR* pNMHDR, LRESULT* pResult) { VALIDATE; UNUSED_ALWAYS(pNMHDR); HRESULT hr(S_OK); // Create the context menu. MyMenu menuContext; VERIFY(menuContext.LoadMenu(IDR_MYLISTCTRL)); menuContext.LoadToolbar(IDR_MYLISTCTRL); // Load the menu resource. CMenu* pSubMenu = menuContext.GetSubMenu(0); ASSERT_VALID(pSubMenu); CPoint point; VERIFY(::GetCursorPos(&point)); // Display menu and track selection of items. EC_B(pSubMenu); EC_B(pSubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RIGHTBUTTON, point.x, point.y, this)); *pResult = 0; } // WM_INITMENUPOPUP. void MyListCtrl::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu) { VALIDATE; CListCtrl::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu); // See if there are any items selected. HRESULT hr(S_OK); bool bSelected(0 < GetSelectedCount()); // We have to be sure this isn't the "Columns" submenu that is popping. MENUITEMINFO miiUnused = { sizeof (MENUITEMINFO) }; if (pPopupMenu->GetMenuItemInfo(ID_MYLISTCTRL_COPY, &miiUnused)) { EC_B(-1 != pPopupMenu->EnableMenuItem(ID_MYLISTCTRL_COPY, bSelected ? MF_ENABLED : MF_GRAYED)); } if (pPopupMenu->GetMenuItemInfo(ID_MYLISTCTRL_SELECTALL, &miiUnused)) { EC_B(-1 != pPopupMenu->EnableMenuItem(ID_MYLISTCTRL_SELECTALL, true ? MF_ENABLED : MF_GRAYED)); } if (pPopupMenu->GetMenuItemInfo(ID_MYLISTCTRL_FONT_UP, &miiUnused)) { EC_B(-1 != pPopupMenu->EnableMenuItem(ID_MYLISTCTRL_FONT_UP, true ? MF_ENABLED : MF_GRAYED)); } if (pPopupMenu->GetMenuItemInfo(ID_MYLISTCTRL_FONT_DOWN, &miiUnused)) { EC_B(-1 != pPopupMenu->EnableMenuItem(ID_MYLISTCTRL_FONT_DOWN, true ? MF_ENABLED : MF_GRAYED)); } if (pPopupMenu->GetMenuItemInfo(ID_MYLISTCTRL_COLUMNS, &miiUnused)) { EC_B(-1 != pPopupMenu->EnableMenuItem(ID_MYLISTCTRL_COLUMNS, true ? MF_ENABLED : MF_GRAYED)); } if (pPopupMenu->GetMenuItemInfo(ID_MYLISTCTRL_GRIDLINES, &miiUnused)) { EC_B(-1 != pPopupMenu->EnableMenuItem(ID_MYLISTCTRL_GRIDLINES, true ? MF_ENABLED : MF_GRAYED)); } } // User chose context menu item. void MyListCtrl::OnMylistctrlCopy() { VALIDATE; VERIFY(CopySelectedToClipboard()); } // User chose context menu item. void MyListCtrl::OnMylistctrlSelectAll() { VALIDATE; SelectAll(true); } // Append the text from the selected rows (or all of them if no rows are // selected) to the rich edit control. // // 'bCancel' holds a flag the caller will set if the user wants to cancel; this // class will check that variable once in a while. bool MyListCtrl::AppendSelectedToRichEdit(MyRichEditCtrl& richedit, bool& bCancel, CWnd* pwndCount, bool bIncludeZerothColumn, int nTwips) { VALIDATE; ASSERT_VALID(&richedit); ASSERT_NULL_OR_POINTER(pwndCount, CWnd); _ASSERTE(0 < nTwips); HRESULT hr(S_OK); bool bReturn(false); CWaitCursor* pwc = new CWaitCursor; // No point in starting if we've been cancelled already. if (! bCancel) { // Write the header out. CString sItem; int nCols(GetHeaderCtrl()->GetItemCount()); int nColFirst(bIncludeZerothColumn ? 0 : 1); CHARFORMAT cf = { sizeof(CHARFORMAT) }; cf.dwMask = CFM_UNDERLINE | CFM_SIZE; cf.dwEffects = CFE_UNDERLINE; cf.yHeight = nTwips; for (int nSubItem = nColFirst; nSubItem < nCols; ++nSubItem) { CString sSubItem; HDITEM hdi = { 0 }; hdi.mask = HDI_TEXT; hdi.pszText = sSubItem.GetBuffer(256); hdi.cchTextMax = 256; VERIFY(GetHeaderCtrl()->GetItem(nSubItem, &hdi)); sSubItem.ReleaseBuffer(); sItem += sSubItem; if (nSubItem < nCols - 1) { sItem += "\t"; } } EC_B(bReturn = richedit.AppendFormattedText(sItem + "\r\n", cf)); // Only process selected items. DOMYLOGA ("Appending <%d> selected results to output window...\n", GetSelectedCount()); cf.dwEffects = 0; POSITION pos(GetFirstSelectedItemPosition()); for (int nSelectedItem = 0; pos; ++nSelectedItem) { int nItem(GetNextSelectedItem(pos)); sItem.Empty(); for (nSubItem = nColFirst; nSubItem < nCols; ++nSubItem) { CString sSubItem(GetItemText(nItem, nSubItem)); sItem += sSubItem; if (nSubItem < nCols - 1) { sItem += "\t"; } } EC_B(bReturn = richedit.AppendFormattedText(sItem + "\r\n", cf)); if (FAILED(hr)) break; if (bCancel) { cf.dwMask = CFM_COLOR | CFM_SIZE; cf.crTextColor = RGB_RED; VERIFY(richedit.AppendFormattedText("< CANCELLED >\r\n", cf)); DOMYLOGA ("User cancelled.\n"); break; } // Allow the cancel button to get clicked and update the UI if (0 == nSelectedItem % 1000) { if (pwndCount) { CString sItem; sItem.Format("%d", nSelectedItem); pwndCount->SetWindowText(sItem); } delete pwc; pwc = NULL; Generic::SpinTheMessageLoop(); pwc = new CWaitCursor; } } // Update the UI to reflect the total record count. if (pwndCount) { CString sItem; sItem.Format("%d", nSelectedItem); pwndCount->SetWindowText(sItem); } } else { // Not an error. bReturn = true; } // Done. delete pwc; pwc = NULL; return bReturn; } // Is there an item that has these values in each column? Returns -1 // if no item found. You can send too few columns, or the exact number, // but you can't of course send too many. int MyListCtrl::FindExactMatch(const CStringList& slCells, int nItemStart /* = -1 */ , int nColStart /* = -1 */ ) const { _ASSERTE(-1 <= nItemStart && "Bad starting item sent"); _ASSERTE(-1 <= nColStart && "Bad starting col sent"); #ifdef _DEBUG int nSize(slCells.GetCount()); _ASSERTE(IsColumn(nSize - 1) && "Too many CStrings in list for number of Columns in list control"); #endif bool bFound(false); for (int nItem = nItemStart + 1; nItem < GetItemCount(); nItem++) { bFound = true; int nCol(0); POSITION pos(NULL); for (nCol = nColStart + 1, pos = slCells.GetHeadPosition(); IsColumn(nCol) && pos; nCol++) { CString sValue(GetItemText(nItem, nCol)); CString sCell(slCells.GetNext(pos)); if (0 != sCell.Compare(sValue)) { bFound = false; break; } } if (bFound) { break; } } return bFound ? nItem : -1; } // Find the sent text in any column. This is NOT case-sensitive. Returns -1 // if not found. int MyListCtrl::FindInAnyColumn(const CString& sText, int nItemStart /* = -1 */ , int nColStart /* = -1 */ ) const { VALIDATE; _ASSERTE(! sText.IsEmpty()); bool bFound(false); for (int nItem = nItemStart + 1; nItem < GetItemCount(); nItem++) { POSITION pos(NULL); for (int nCol = nColStart + 1; IsColumn(nCol); ++nCol) { CString sValue(GetItemText(nItem, nCol)); if (-1 != Generic::FindNoCase(sValue, sText)) { bFound = true; break; } } if (bFound) { break; } } return bFound ? nItem : -1; } // This is a reflected message handler, which means that this control // handles this message instead of the parent window. Any handler // for this message in the parent window will override this handler // (unless it calls this handler first, of course). void MyListCtrl::OnColumnclick(NMHDR* pNMHDR, LRESULT* pResult) { NM_LISTVIEW* pNMListView = (NM_LISTVIEW*) pNMHDR; SortColumn(pNMListView->iSubItem); *pResult = 0; } // Split this code out so that it can be called from outside this class. void MyListCtrl::SortColumn(int nColumn, bool bToggle /* = true */ ) { // If a different column is being sorted from the last time we // sorted, we always start off ascending. if (nColumn != m_nColumnSort) { m_bSortAscending = true; } else if (bToggle) { m_bSortAscending = ! m_bSortAscending; } m_nColumnSort = nColumn; _ASSERTE(IsColumn(m_nColumnSort)); // Now, the only way the CListCtrl can know how to sort is by each // item's LPARAM, so let's see if the first item has one. If so, // we assume they all do, and use them; otherwise, we use the item number! bool bHasLparams(IsItem(0) && GetItemData(0)); if (! bHasLparams) { for (int nItem = 0; IsItem(nItem); nItem++) { VERIFY(SetItemData(nItem, nItem)); } } // Call the sort routine the first time. (Make sure we do an alphabetic sort.) m_bNumericSort = false; VERIFY(SortItems(CompareFunc, reinterpret_cast (this))); // If the first and last item *begin with* only numeric strings, it's a // safe bet that this is a numeric column. if (0 < GetItemCount()) { CString sNumericChars("$.%,-0123456789"); CString sFirstItemText(GetItemText(0, m_nColumnSort)); sFirstItemText.TrimLeft(); CString sLastItemText(GetItemText(GetItemCount() - 1, m_nColumnSort)); if ((sLastItemText.IsEmpty() || 0 < sLastItemText.SpanIncluding(sNumericChars).GetLength()) && (sFirstItemText.IsEmpty() || 0 < sFirstItemText.SpanIncluding(sNumericChars).GetLength())) { m_bNumericSort = true; } // Call the sort routine the second time for a numeric sort. if (m_bNumericSort) { VERIFY(SortItems(CompareFunc, reinterpret_cast (this))); } } // I set the lParams back to zero so that stale data isn't hanging around // (because the sort has changed the item numbers). if (! bHasLparams) { for (int nItem = 0; IsItem(nItem); nItem++) { VERIFY(SetItemData(nItem, 0)); } } } // This is the function that the base CListCtrl code calls whenever it // needs to compare two items. /* static */ int CALLBACK MyListCtrl::CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort) { // Retrieve the "this" we packaged in OnColumnclick(). MyListCtrl* pListCtrl = reinterpret_cast (lParamSort); ASSERT_VALID(pListCtrl); // Retrieve the two items to compare. LV_FINDINFO lvi; ::ZeroMemory(&lvi, sizeof(lvi)); lvi.flags = LVFI_PARAM; lvi.lParam = lParam1; int nItem1(pListCtrl->FindItem(&lvi)); _ASSERTE(-1 != nItem1); lvi.lParam = lParam2; int nItem2(pListCtrl->FindItem(&lvi)); _ASSERTE(-1 != nItem2); // Now get the two strings to compare. CString s1(pListCtrl->GetItemText(nItem1, pListCtrl->m_nColumnSort)); CString s2(pListCtrl->GetItemText(nItem2, pListCtrl->m_nColumnSort)); // Now compare them, remembering to take into account which direction // we are sorting in at the moment. "The comparison function must return // a negative value if the first item should precede the second, a // positive value if the first item should follow the second, or zero if // the two items are equivalent." int nReturn(0); // Numeric sort. if (pListCtrl->m_bNumericSort) { nReturn = atoi(s1) - atoi(s2); if (! pListCtrl->m_bSortAscending) { nReturn = -nReturn; } } else { // Alphabetic sort. nReturn = s1.CompareNoCase(s2); if (! pListCtrl->m_bSortAscending) { nReturn = -nReturn; } } return nReturn; } // From context menu, user wants to increase point size. void MyListCtrl::OnMylistctrlFontUp() { VALIDATE; // If we haven't created a font yet (that is, we just using the // default font of the parent dialog). if (! m_pFont) { CreateTheFont(); } // Create a new font one point bigger. if (144 > m_nPoints) { SelectTheFont(m_sFaceName, m_nPoints + 1); } else { ::MessageBeep((UINT) -1); } // Non true-type fonts don't grow at each point-size change. if (0 == m_sFaceName.CompareNoCase("MS Sans Serif") || 0 == m_sFaceName.CompareNoCase("MS Shell Dlg")) { SelectTheFont(m_sFaceName, m_nPoints + 1); } } // From context menu, user wants to decrease point size. void MyListCtrl::OnMylistctrlFontDown() { VALIDATE; // If we haven't created a font yet (that is, we just using the // default font of the parent dialog). if (! m_pFont) { CreateTheFont(); } // Create a new font one point bigger. if (2 < m_nPoints) { SelectTheFont(m_sFaceName, m_nPoints - 1); } else { ::MessageBeep((UINT) -1); } // Non true-type fonts don't grow at each point-size change. if (0 == m_sFaceName.CompareNoCase("MS Sans Serif") || 0 == m_sFaceName.CompareNoCase("MS Shell Dlg")) { SelectTheFont(m_sFaceName, m_nPoints - 1); } } // void MyListCtrl::ReadGridLinesFromRegistry(const CString& sKey, const CString& sValue) { VALIDATE; _ASSERTE(! sKey.IsEmpty()); DWORD dwStyle(GetExtendedStyle()); MyRegistry reg(HKEY_CURRENT_USER, sKey); bool bGridLines(false); reg.ReadValueBool(sValue, bGridLines); if (bGridLines) { SetExtendedStyle(dwStyle | LVS_EX_GRIDLINES); } else { SetExtendedStyle(dwStyle & ~LVS_EX_GRIDLINES); } } #if 0 // ?? Not needed so far, but don't delete ever - may need someday! // Given the actual column number on the screen, return the column number that // the program uses. int MyListCtrl::GetColScreenFromColID(int* pnOrder, int nColID) { VALIDATE; _ASSERTE(pnOrder); bool bFound(false); int nColScreen(-1); // if (pnOrder) { int nColumns(GetHeaderCtrl() ? GetHeaderCtrl()->GetItemCount() : 0); for (nColScreen = 0; nColScreen < nColumns; ++nColScreen) { if (pnOrder[nColScreen] == nColID) { bFound = true; break; } } } // if (! bFound) { _ASSERTE(! "Couldn't find the column in the array"); nColScreen = -1; } return nColScreen; } #endif // Read the column order from the Registry, and set the column order. void MyListCtrl::ReadColumnOrderFromRegistry(const CString& sKey, const CString& sValue) { VALIDATE; _ASSERTE(! sKey.IsEmpty()); _ASSERTE(! sValue.IsEmpty()); HRESULT hr(S_OK); if (GetHeaderCtrl()) { int nArrayCount(GetHeaderCtrl()->GetItemCount()); if (0 < nArrayCount) { int* pnOrder = new int[nArrayCount]; int nBytesInRegistry(0); MyRegistry reg(HKEY_CURRENT_USER, sKey); if (reg.ReadValueInt(sValue + "Bytes", nBytesInRegistry)) { int nArraySizeBytes(nArrayCount * sizeof(int)); EC_B(nBytesInRegistry == nArraySizeBytes); if (reg.ReadValueBinary(sValue, reinterpret_cast (pnOrder), reinterpret_cast (nArraySizeBytes))) { _ASSERTE(static_cast (nArrayCount * sizeof(int)) == nArraySizeBytes && "Didn't read exactly as much as we should have"); EC_B(SetColumnOrderArray(nArrayCount, pnOrder)); DOMYLOGD ("Read list control column order array from key <%s>, " "value <%s>, columns <%d>.\n", sKey, sValue + "Bytes", nArrayCount); } } delete [] pnOrder; pnOrder = NULL; } else { _ASSERTE(! "Error"); DOMYLOGW ("ReadColumnOrderFromRegistry() failed because " "GetHeaderCtrl()->GetItemCount() returned LE 0.\n"); } } else { _ASSERTE(! "Error"); DOMYLOGW ("ReadColumnOrderFromRegistry() failed because GetHeaderCtrl() " "returned NULL.\n"); } } // void MyListCtrl::SaveGridLinesToRegistry(const CString& sKey, const CString& sValue) { VALIDATE; _ASSERTE(! sKey.IsEmpty()); DWORD dwStyle(GetExtendedStyle()); bool bGridLines(dwStyle & LVS_EX_GRIDLINES ? true : false); MyRegistry reg(HKEY_CURRENT_USER, sKey); VERIFY(reg.SaveValueBool(sValue, bGridLines)); } // Read the column widths from the Registry, and set the column widths. If // it's not in the registry yet, autosize the columns. void MyListCtrl::ReadColumnWidthsFromRegistry(const CString& sKey, const CString& sValue) { VALIDATE; _ASSERTE(! sKey.IsEmpty()); _ASSERTE(! sValue.IsEmpty()); HRESULT hr(S_OK); MyRegistry reg(HKEY_CURRENT_USER, sKey); for (int nCol = 0; IsColumn(nCol); ++nCol) { CString sCol; sCol.Format("%s%d", sValue, nCol); int nWidth(50); if (! reg.ReadValueInt(sCol, nWidth)) { nWidth = LVSCW_AUTOSIZE_USEHEADER; } EC_B(SetColumnWidth(nCol, nWidth)); } } // Write the column widths out to the Registry. void MyListCtrl::SaveColumnWidthsToRegistry(const CString& sKey, const CString& sValue) { VALIDATE; _ASSERTE(! sKey.IsEmpty()); _ASSERTE(! sValue.IsEmpty()); MyRegistry reg(HKEY_CURRENT_USER, sKey); for (int nCol = 0; IsColumn(nCol); ++nCol) { CString sCol; sCol.Format("%s%d", sValue, nCol); reg.SaveValueInt(sCol, GetColumnWidth(nCol)); } } // Write the column order out to the Registry. void MyListCtrl::SaveColumnOrderToRegistry(const CString& sKey, const CString& sValue) { VALIDATE; _ASSERTE(! sKey.IsEmpty()); _ASSERTE(! sValue.IsEmpty()); HRESULT hr(S_OK); MyRegistry reg(HKEY_CURRENT_USER, sKey); if (GetHeaderCtrl()) { int nColumns(GetHeaderCtrl()->GetItemCount()); if (0 < nColumns) { int* pnOrder = new int[nColumns]; EC_B(GetColumnOrderArray(pnOrder, nColumns)); EC_B(reg.SaveValueInt(sValue + "Bytes", nColumns * sizeof(int))); EC_B(reg.SaveValueBinary(sValue, reinterpret_cast (pnOrder), nColumns * sizeof(int))); delete [] pnOrder; pnOrder = NULL; DOMYLOGD ("Saved list control column order array to key <%s>, " "value <%s>, columns <%d>.\n", sKey, sValue + "Bytes", nColumns); } else { _ASSERTE(! "Error"); DOMYLOGW ("SaveColumnOrderToRegistry() failed because " "GetHeaderCtrl()->GetItemCount() returned LE 0.\n"); } } else { _ASSERTE(! "Error"); DOMYLOGW ("SaveColumnOrderToRegistry() failed because GetHeaderCtrl() " "returned NULL.\n"); } } // Read the selected items from the Registry, and set them. void MyListCtrl::ReadSelItemsFromRegistry(const CString& sKey, const CString& sValue) { VALIDATE; _ASSERTE(! sKey.IsEmpty()); _ASSERTE(! sValue.IsEmpty()); HRESULT hr(S_OK); DWORD dwSize(0L); // Read the items size. This will fail if they're never been saved. MyRegistry reg(HKEY_CURRENT_USER, sKey); if (reg.ReadValueDWord(sValue + "Bytes", dwSize) && dwSize) { int* pnItems = new int[dwSize]; EC_B(reg.ReadValueBinary(sValue, reinterpret_cast (pnItems), dwSize)); EC_FAIL(s, sKey); EC_FAIL(s, sValue); int nItems(dwSize / sizeof(DWORD)); for (int nIndex = 0; nIndex < nItems; ++nIndex) { int nItem(pnItems[nIndex]); if (IsItem(nItem)) { SelectItem(nItem, true); EnsureVisible(nItem, /* bPartialOk */ false); } } // This is just a trick to try to make them all visible, if // that's possible. After we've done them all, we go back and do // the first one. if (0 < nItems && IsItem(0)) { EnsureVisible(pnItems[0], /* bPartialOk */ false); } delete [] pnItems; pnItems = NULL; } } // Write the column widths out to the Registry. void MyListCtrl::SaveSelItemsToRegistry(const CString& sKey, const CString& sValue) { VALIDATE; _ASSERTE(! sKey.IsEmpty()); _ASSERTE(! sValue.IsEmpty()); HRESULT hr(S_OK); MyRegistry reg(HKEY_CURRENT_USER, sKey); int nItems(GetSelectedCount()); int* pnItems = new int[nItems]; POSITION pos(GetFirstSelectedItemPosition()); for (int nIndex = 0; pos; ++nIndex) { pnItems[nIndex] = GetNextSelectedItem(pos); } EC_B(reg.SaveValueDWord(sValue + "Bytes", nItems * sizeof(int))); EC_B(reg.SaveValueBinary(sValue, reinterpret_cast (pnItems), nItems * sizeof(int))); delete [] pnItems; pnItems = NULL; } // How many columns are there? int MyListCtrl::GetColumnCount() const { // One of the few examples of when this cast is okay. (Microsoft forgot // to supply a const CListCtrl::GetHeaderCtrl() - doh!) CHeaderCtrl* pHeader = const_cast (this)->GetHeaderCtrl(); ASSERT_VALID(pHeader); return pHeader ? pHeader->GetItemCount() : -1; } // CListCtrl::GetItemText() has a bug in Win95 when callback items are // used. This works around that bug. CString MyListCtrl::GetItemTextWorks(int nItem, int nSubItem) const { _ASSERTE(IsItem(nItem) && "Bad item sent"); _ASSERTE(IsColumn(nSubItem) && "Bad subitem sent"); CString sText; if (-1 != nItem) { char* pszText = NULL; LV_ITEM lvi; ::ZeroMemory(&lvi, sizeof(lvi)); lvi.mask = LVIF_TEXT; lvi.iItem = nItem; lvi.iSubItem = nSubItem; lvi.cchTextMax = 32; do { lvi.cchTextMax *= 2; pszText = sText.GetBufferSetLength(lvi.cchTextMax); lvi.pszText = pszText; VERIFY(GetItem(&lvi)); sText.ReleaseBuffer(); // If the GetDispInfo handler provided its own buffer // (and ignored ours), we detect that here. if (lvi.pszText != pszText) { sText = lvi.pszText; break; } // On some Win95 machines, I'm getting an infinite loop. ?? if (32000 < lvi.cchTextMax) break; } // Sometimes, lvi.pszText comes back exactly full, and sometimes // it leaves room for a null. Another CListCrl bug? while (lvi.cchTextMax - lstrlen(lvi.pszText) < 2); } return sText; } // Determine the row, col and bounding rect of a cell. Returns either: // : item number, if point is inside the control. // : -1, if the point is to the left of right of the control. // : -2, if the point is above the control. // : -3, if the point is below the control. int MyListCtrl::GetCellRectFromPoint(const CPoint& point, CRect& rcCell, int& nCol) const { bool bFoundCell(false); int nRow(-4); // Initialize variables. rcCell.SetRectEmpty(); nCol = -1; // Make sure that the ListView is in LVS_REPORT mode. if (LVS_REPORT == (::GetWindowLong(m_hWnd, GWL_STYLE) & LVS_TYPEMASK)) { CRect rcBounding; // Get the bottom row (fully) visible. There is a bug here: the correct // cell for a point in the last row isn't found correctly if the last // row is only partially-visible, because GetCountPerPage() returns only // *fully* visible rows, not partial rows. int nBottomRow(GetTopIndex() + GetCountPerPage() - 1); nBottomRow = min(nBottomRow, GetItemCount() - 1); // Loop through the visible rows. for (nRow = GetTopIndex(); nRow <= nBottomRow; nRow++) { // Get bounding rect of item and check whether point falls in it. // This rect, and the passed in point, are in the control's window // coords. GetItemRect(nRow, rcBounding, LVIR_BOUNDS); if (rcBounding.PtInRect(point)) { // Now find the column. for (nCol = 0; IsColumn(nCol); nCol++) { int nColWidth(GetColumnWidth(nCol)); if (point.x >= rcBounding.left && point.x <= (rcBounding.left + nColWidth)) { bFoundCell = true; rcBounding.right = rcBounding.left + nColWidth; // Make sure right extent does not exceed client area. CRect rcClient; GetClientRect(rcClient); if (rcBounding.right > rcClient.right) { rcBounding.right = rcClient.right; } rcCell = rcBounding; break; } rcBounding.left += nColWidth; } // End col for. if (bFoundCell) { break; } } } // End row for. // Row for loop is done. If not found, set to indicate to the left or // right, for the moment. if (! bFoundCell) { nRow = -1; } // Is the point before the first row? GetItemRect(GetTopIndex(), rcBounding, LVIR_BOUNDS); if (point.y < rcBounding.top) { nRow = -2; // Let's pretend it was actually above the first row, so that we // can get at least the column. Note that the rcCell value so // obtained is bogus. CPoint ptNew; ptNew.x = point.x; ptNew.y = rcBounding.top + 1; GetCellRectFromPoint(ptNew, rcCell, nCol); rcCell.SetRectEmpty(); } // Or, is the point after the last row? GetItemRect(nBottomRow, rcBounding, LVIR_BOUNDS); if (point.y > rcBounding.bottom) { nRow = -3; } } _ASSERTE(-4 != nRow && "Row never got set!"); return nRow; } // Get the text of the header. CString MyListCtrl::GetHeaderText(int nCol) const { _ASSERTE(IsColumn(nCol) && "Bad column sent"); CString s; HD_ITEM hdi; ::ZeroMemory(&hdi, sizeof(hdi)); hdi.mask = HDI_TEXT; hdi.pszText = s.GetBuffer(128); hdi.cchTextMax = 127; VERIFY(m_Header.GetItem(nCol, &hdi)); s.ReleaseBuffer(); return s; } // Is this column aligned right, left, or center? int MyListCtrl::GetColumnAlignment(int nCol) const { _ASSERTE(-1 < nCol); LVCOLUMN lvc; ::ZeroMemory(&lvc, sizeof lvc); lvc.mask = LVCF_FMT; VERIFY(GetColumn(nCol, &lvc)); // Strip out everything except what we want. int nReturn((lvc.fmt & LVCFMT_LEFT) + (lvc.fmt & LVCFMT_CENTER) + (lvc.fmt & LVCFMT_RIGHT)); return nReturn; } // What is the sum of the column widths? int MyListCtrl::GetTotalColumnWidth() const { int nReturn(0); for (int nCol = 0; IsColumn(nCol); ++nCol) { nReturn += GetColumnWidth(nCol); } return nReturn; } // User clicked on a column in the column context menu. We toggle its width. void MyListCtrl::OnColumnMenuSelect(UINT uiID) { VALIDATE; HRESULT hr(S_OK); int nCol(uiID - MYLISTCTRL_FIRST_COLUMN_MENUID); if (0 < GetColumnWidth(nCol)) { SetColumnWidth(nCol, 0); } else { EC_B(SetColumnWidth(nCol, LVSCW_AUTOSIZE_USEHEADER)); } } // Context menu. void MyListCtrl::OnMylistctrlColumns() { VALIDATE; // Create the dummy menu. CMenu menuDummy; VERIFY(menuDummy.CreateMenu()); CMenu menuColumns; VERIFY(menuColumns.CreatePopupMenu()); VERIFY(menuDummy.AppendMenu(MF_POPUP | MF_STRING, (UINT) menuColumns.GetSafeHmenu(), "Dummy")); // Populate the columns menu. for (int nCol = 0; IsColumn(nCol); ++nCol) { VERIFY(menuColumns.AppendMenu(MF_STRING | (0 < GetColumnWidth(nCol)) ? MF_CHECKED : MF_UNCHECKED, MYLISTCTRL_FIRST_COLUMN_MENUID + nCol, GetHeaderText(nCol))); } // Pop the menu. CPoint pt; VERIFY(::GetCursorPos(&pt)); VERIFY(menuColumns.TrackPopupMenu( TPM_LEFTALIGN | TPM_LEFTBUTTON | TPM_RIGHTBUTTON, pt.x, pt.y, this)); } // Context menu - toggle gridlines. void MyListCtrl::OnMylistctrlGridlines() { VALIDATE; DWORD dwStyle(GetExtendedStyle()); if (dwStyle & LVS_EX_GRIDLINES) { dwStyle &= ~LVS_EX_GRIDLINES; } else { dwStyle |= LVS_EX_GRIDLINES; } SetExtendedStyle(dwStyle); } // void MyListCtrl::SelectTheFont(const CString& sFace, int nPoints) { VALIDATE; _ASSERTE(! sFace.IsEmpty()); _ASSERTE(2 <= nPoints && 144 >= nPoints); // First delete any old font. delete m_pFont; m_pFont = NULL; // Create the new font. m_pFont = new CFont; m_nPoints = nPoints; m_sFaceName = sFace; m_pFont->CreatePointFont(nPoints * 10, sFace); SetFont(m_pFont); DOMYLOGD ("Font set to <%d> point <%s>.\n", m_nPoints, m_sFaceName); } // The font has never been created yet, and the user wants to change the point // size. All this function does is read the current font being used by this // control, and use that info to populate the m_nPoints and m_sFaceName members. // Once that is done, any other function can build and SelectObj a real font into // this control. (The CFont that is incidentally created here is just for temp // use and must not be stored.) void MyListCtrl::CreateTheFont() { VALIDATE; HRESULT hr(S_OK); CFont* pFont = NULL; EC_B(pFont = GetFont()); ASSERT_VALID(pFont); LOGFONT lf = { 0 }; EC_B(pFont->GetLogFont(&lf)); CDC* pDC = NULL; EC_B(pDC = GetDC()); ASSERT_VALID(pDC); // Store the face and point size. m_nPoints = -MulDiv(lf.lfHeight, 72, pDC->GetDeviceCaps(LOGPIXELSY)); m_sFaceName = lf.lfFaceName; // Done! EC_B_(ReleaseDC(pDC)); } // This is called by MFC after the window's HWND has been bound to the // MyListCtrl object, but before the subclassing occurs. Note that this class // never gets WM_CREATE, because that comes before the subclassing occurs. void MyListCtrl::PreSubclassWindow() { VALIDATE; HRESULT hr(S_OK); // We set the max width to allow multiline tips. EC_B(m_ToolTip.Create(GetParent(), TTS_ALWAYSTIP | 0x40 /* TTS_BALLOON */ )); EC_V(m_ToolTip.SetMaxTipWidth(SHRT_MAX)); EC_V(m_ToolTip.SetDelayTime(333)); EC_B(m_ToolTip.AddTool(this, GetDlgCtrlID())); EC_V(m_ToolTip.Activate(true)); // Since I want to capture all messages passed to the header, I am // subclassing the header. Note: 0 is the ID for the header. EC_B_(m_Header.SubclassDlgItem(0, this)); // Call base class method. CListCtrl::PreSubclassWindow(); } // This is needed to pass the mouse messages to the tool tip control, so that // it can turn on and off. BOOL MyListCtrl::PreTranslateMessage(MSG* pMsg) { VALIDATE; if (GetSafeHwnd()) { m_ToolTip.Activate(true); m_ToolTip.RelayEvent(pMsg); return CListCtrl::PreTranslateMessage(pMsg); } return false; }