CMFCListCtrl force selected item to have red color

2.2k views Asked by At

I have my own CMFCListCtrl derived class where I implemented the

virtual COLORREF OnGetCellTextColor(int nRow, int nColum)
{
    CMyClass* pMyClass = (CMyClass*)GetItemData(nRow);
    if (pMyClass && pMyClass->m_bDeleted)
        return RGB(255, 0, 0);

    return __super::OnGetCellTextColor(nRow, nColum);
}

function for marking red the deleted entries. This works except when a item is selected.

I went to the void CMFCListCtrl::OnCustomDraw(NMHDR* pNMHDR, LRESULT* pResult) function and placed a breakpoint with the condition iRow==selected item on the line

lplvcd->clrText = OnGetCellTextColor(iRow, iColumn);

executed it, then I created a new Data Breakpoint for &lplvcd->clrText.

The Data Breakpoint got hit on function

comctl32.dll!SHThemeComputeTextColors()

, as the callstack image shows:

callstack

, which is clearly overriding the variable value.

As I search for SHThemeComputeTextColors on Internet and nothing appears, can somebody help me on forcing the selected items text to be red?

1

There are 1 answers

6
MikMik On BEST ANSWER

Changing the colors and fonts of highlighted items is trickier than it seems, because highlighting selected items is a system-wide issue. In fact, it was something I had delayed for a long time, and just today I've finally tackled...

There are at least two ways to change the looks of a list control: owner draw (you have to do all the drawing yourself) and custom draw (the system tells you when it is about to do some steps of the drawing and lets you change the color, the font, etc.). This answer is about custom draw. This article covers the basics of using Custom Draw with CListCtrl.

How to change the highlight color in a CListCtrl (not a CMFCListCtrl, we'll get to that soon) is explained in this other article. These are the steps you have to do:

  1. Intercept the listview draw routine just before it is about to draw a highlighted row (item).
  2. Turn off the row highlight.
  3. Set the row colors to whatever you want.
  4. Let the listview draw the row.
  5. Intercept the listview draw routine after it has drawn the row (post-draw item).
  6. Turn this row's highlighting back on.

So, when the listview is about to draw a highlighted row, you have to telll the system the row is not highlighted so it doesn't use the system colors to paint it, tell what color to use, and set the item as highlighted again, so the selection works as usual.

Here is the code from that article:

COLORREF g_MyClrFgHi; // My foreground hilite color
COLORREF g_MyClrBgHi; // My background hilite color
HWND     g_hListView; // Window handle of listview control
void  EnableHighlighting(HWND hWnd, int row, bool bHighlight)
{
  ListView_SetItemState(hWnd, row, bHighlight? 0xff: 0, LVIS_SELECTED);
}
bool  IsRowSelected(HWND hWnd, int row)
{
  return ListView_GetItemState(hWnd, row, LVIS_SELECTED) != 0;
}
bool  IsRowHighlighted(HWND hWnd, int row)
{
  // We check if row is selected.
  // We also check if window has focus. This was because the original listview
  //  control I created did not have style LVS_SHOWSELALWAYS. So if the listview
  //  does not have focus, then there is no highlighting.
  return IsRowSelected(hWnd, row) && (::GetFocus(hWnd) == hWnd);
}
BOOL OnNotify(WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
  static bool bIsHighlighted = false;
  *pResult = 0;
  NMHDR *p = (NMHDR *)lParam;
  switch (p->code)
  { 
  ... 
  case NM_CUSTOMDRAW:
    NMLVCUSTOMDRAW *lvcd = (NMLVCUSTOMDRAW *)p;
    NMCUSTOMDRAW   &nmcd = lvcd->nmcd;
    switch (nmcd.dwDrawStage)
    {
    case CDDS_PREPAINT:
      // We want item prepaint notifications, so...
      *pResult = CDRF_NOTIFYITEMDRAW;
      break;
    case CDDS_ITEMPREPAINT:
    {
      int iRow = (int)nmcd.dwItemSpec;
      bHighlighted = IsRowHighlighted(g_hListView, iRow);
      if (bHighlighted)
      {
        lvcd->clrText   = g_MyClrFgHi; // Use my foreground hilite color
        lvcd->clrTextBk = g_MyClrBgHi; // Use my background hilite color
        // Turn off listview highlight otherwise it uses the system colors!
        EnableHighlighting(g_hListView, iRow, false);
      }
      // We want item post-paint notifications, so...
      *pResult = CDRF_DODEFAULT | CDRF_NOTIFYPOSTPAINT;
      break;
    }
    case CDDS_ITEMPOSTPAINT:
    {
      if (bHighlighted)
      {
        int  iRow = (int)nmcd.dwItemSpec;
        // Turn listview control's highlighting back on now that we have
        // drawn the row in the colors we want.
        EnableHighlighting(g_hListView, iRow, true);
      }
      *pResult = CDRF_DODEFAULT;
      break;
    }
    default:
      *pResult = CDRF_DODEFAULT;
      break;
    }
    break;
  ...
  }
}

This works fine with a CListCtrl, but you are asking about a CMFCListCtrl. The problem is that CMFCListCtrl is already asking to be notified about NM_CUSTOMDRAW notifications (that's when it calls the OnGetCellTextColor function, as you've seen). If you create a handler for that in your own CMFCListCtrl-derived class, those notifications won't get to CMFCListCtrl and you lose that functionality (it may fine, depending on your needs).

So here is what I've done: I've created a NM_CUSTOMDRAW handler in my list control and, if I'm dealing with a highlighted row, I change the colors, otherwise, I call CMFCListCtrl::OnNMCustomDraw(). As you'll see, I just use the normal highlight colors; that's because I just wanted to see the selected items even when the control doesn't have the focus:

void CMyListCtrl::OnNMCustomdraw(NMHDR* pNMHDR, LRESULT* pResult)
{
    bool callParent = true;
    static bool bHighlighted = false;
    LPNMLVCUSTOMDRAW lpLVCustomDraw = reinterpret_cast<LPNMLVCUSTOMDRAW>(pNMHDR);
    NMCUSTOMDRAW nmcd = lpLVCustomDraw->nmcd;

    *pResult = CDRF_DODEFAULT;

    switch (lpLVCustomDraw->nmcd.dwDrawStage)
    {
    case CDDS_PREPAINT:
        *pResult = CDRF_NOTIFYITEMDRAW;
        break;
    case CDDS_ITEMPREPAINT:
    {
        int row = nmcd.dwItemSpec;
        bHighlighted = IsRowHighlighted(row);
        if (bHighlighted)
        {
            lpLVCustomDraw->clrText = GetSysColor(COLOR_HIGHLIGHTTEXT);
            lpLVCustomDraw->clrTextBk = GetSysColor(COLOR_HIGHLIGHT);

            EnableHighlighting(row, false);
            *pResult = CDRF_DODEFAULT | CDRF_NOTIFYPOSTPAINT;
            callParent = false;
        }
    }
    break;
    case CDDS_ITEMPOSTPAINT:
        if (bHighlighted)
        {
            int row = nmcd.dwItemSpec;
            EnableHighlighting(row, true);
            callParent = false;
        }
        *pResult = CDRF_DODEFAULT;

        break;
    default:
        break;
    }

    if (callParent)
    {
        __super ::OnCustomDraw(pNMHDR, pResult);
    }
}

bool CMyListCtrl::IsRowHighlighted(int row)
{
    bool selected = GetItemState(row, LVIS_SELECTED) != 0;
    return selected;
}

void CMyListCtrl::EnableHighlighting(int row, bool enable)
{
    SetItemState(row, enable ? 0xff : 0, LVIS_SELECTED);
}

I haven't tested it thoroughly, but it seems to work.

UPDATE:

There is a little problem. When you call EnableHigilighting(), the dialog will get LVN_ITEMCHANGED notifications, which can trigger a redraw, which will trigger a LVN_ITEMCHANGED notification... So if you are listening to LVN_ITEMCHANGED notifications, you might need to do something like this:

void CMyListCtrl::EnableHighlighting(int row, bool enable)
{
    m_internalStateChange = true;
    SetItemState(row, enable ? 0xff : 0, LVIS_SELECTED);
    m_internalStateChange = false;
}


void CWhateverDialog::OnLvnItemchangedListaEjesPane(NMHDR *pNMHDR, LRESULT *pResult)
{
    LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);

    if (!c_List.InternalStateChange() && /* other conditions */)
    {
        // Respond to state changes
    }
}