Link to home
Start Free TrialLog in
Avatar of sternocera
sternocera

asked on

MFC: CListCtrl derived class tutorial problem

Hello,

I'm following this tutorial for MFC/C++: http://www.codeproject.com/listctrl/creportctrl.asp . This concerns implementing a CListCtrl-derived class with certain "out of the box" functionality that the MFC "report style" CListCtrl lacks. For example, the user can re-order items by clicking on a column header, and, optionally, the programmer can include a checkbox beside nodes.

I created a dialog that uses this CReporCtrl, but that didn't work -  I could see entries, but there were no headings, and no lines, even though I'd called the SetGridLines() member function.

I then called ModifyStyle(0,LVS_REPORT), which is meant to be used with the CListCtrl base class. Now, CReportCtrl was mostly fully functional. The column headers were visible. When I called SetGridLines(TRUE), gridlines appeared. However, one last important piece of functionality was still missing: I couldn't re-order column entries by clicking on a column header, in the style of windows explorer.

I'd be very greatly if someone could point me in the right direction,

Thanks a lot,
Sternocera
Avatar of mahesh1402
mahesh1402
Flag of India image

look at sort styles
http://msdn2.microsoft.com/en-us/library/ms670561.aspx

along with LVS_REPORT you can modify that to set sort order :
ModifyStyle(0,LVS_REPORT | LVS_SORTASCENDING );

-MAHESH
About grids :
you may set extended style LVS_EX_GRIDLINES using SetExtendedStyle or ModifyStyleEx

e.g
ctrl.SetExtendedStyle(LVS_EX_GRIDLINES);

-MAHESH
Avatar of AndyAinscow
The actual sorting - well you have to code that yourself.  All you get is an event that the suer has clicked the header.
Have you used the 'class wizard'  to add an event handler for you?
Is it actually being called ?     ;-)
Avatar of sternocera
sternocera

ASKER

I know that is ordinarily the case Andy, but my understanding of the tutorial was, I simply had to do this:

void CLustreView::DoDataExchange(CDataExchange* pDX)
{
      CFormView::DoDataExchange(pDX);
      DDX_Control(pDX, IDC_LIST1, SampleReportCtrl);
}

and all the message handling would be delegated to the supplied class. Isn't it so?

Regards,
Sternocera
That gives you a member variable, of type CListCtrl (I assume) that is called SampleReportCtrl.
SampleReportCtrl is a variable that I instantiated of type CReportCtrl, which is a sub-class of CListCtrl with the extra "out-of-the-box" functionality that I've described.
Programming windows with MFC, second edition.  author - Jeff Prosise

It will be money well spent.




Nope - the functionality is there no matter if you have a variable mapped to the control or not.  It would be nice that one just had to think and the program does what you think but that is going to be a few years before it happens.  Currently one has to write some code oneself.
The wizards help you in giving a foundation, but that is all it is, a foundation rather than a hole in the ground.  Not a house.
You may also look into basic classwizard reference :
http://msdn2.microsoft.com/en-us/library/aa461412.aspx

I will suggest you to look at functionx tutorials for using controls
( difference : step by step explanation along with snap shots ) : http://www.functionx.com

e.g for your list control see :
http://www.functionx.com/visualc/controls/listcontrol.htm


-MAHESH
Andy,

There is no need for sarcasm. Granted, I'm a novice programmer who asks a lot of questions, but sometimes the only way one can answer a specific question is by asking someone who knows, rather then by refering to documentation. For example, yesterday,I knew a member function of a ListCtrl set its style - I just didn't know which one, because MSDN documentation is generally just a rote discription of each function rather then an overview of how someone might use the class's functionality, like a book might give. I have a 1400 page MFC book - Ivor horton's beginning visual C++ 2005, and I've read it.

I'm surprised how scant MFC documentation is.

I know how to use class wizard.

To be honest, I am a bit of an interloper. I undertook to create a particular application, essentially a database frontend. However, I'm actually pretty close to having implemented my basic user interface. All that remains is to create a bunch of views and forms with handler functions that use a database API - figuring out the logic of the program, which won't be as challenging, as the API fairly well encapsulates everything.

I'm sorry if holding my hand hasn't been particularly personally satisfying for the last number of days. Your help has been absolutely invaluable to me.

Regards,
Sternocera
I didn't mean to be sarcastic - I was trying to give an analogy.

I agree - ask questions and learn.  However if the foundation is missing then you could be building on sand.  If you don't have a basic understanding of how MFC (and windows programs) functions then you might not ask the questions that will actually help you.

The boook I mentioned is specific to MFC.  (OK - not the newest MFC but 98% of it hasn't changed since the book was written).
Perhaps I didn't explain this particular problem very well (don't think I've ignored what you said and am once again trying to get you to do the work - I've absolutely taken what you've said to heart. In all sincerity, I was actually quite upset by it, because you were mostly right and I have quite a lot personally invested in this undertaking).

I thought that by mapping my CReportCtrl class (subclass of CListCtrl with extra bells and whistles) in the way described, I could delegate messages to the class, and use its message handling functions, which are mapped thusly in the ReportCtrl class:

ON_NOTIFY_REFLECT(LVN_COLUMNCLICK, OnColumnClick)

Is it not so?

Thanks
That ought to be the case.
Have you checked that the function is actually being called when you click on a column header?
Also - what has the data that the list control displays?  That is where one probably should be doing any sorting.
Andy,

I can confirm that the function(OnColumnClick) is called when I click on a column header. The list control is populated with filler data:

(this is from my view's OnInitialUpdate. Remember, this isn't a standard CListCtrl, so some member functions won't be familiar to you)
      SampleReportCtrl.SetGridLines(TRUE);
      SampleReportCtrl.SetCheckboxes(TRUE);
      SampleReportCtrl.ModifyStyle(0,LVS_REPORT); // In the tutorial, it isn't nessecary to set style by calling this function. I've had to do it, otherwise nothing is displayed. I don't know why.
      SampleReportCtrl.SetHeadings(_T("column1, 90; column2, 90; column3, 90"));
      SampleReportCtrl.InsertItem(0, "test1");
      SampleReportCtrl.InsertItem(1, "test2");
      SampleReportCtrl.SetColumnWidth(0, LVSCW_AUTOSIZE_USEHEADER);
      SampleReportCtrl.SetColumnWidth(1, LVSCW_AUTOSIZE_USEHEADER);
      SampleReportCtrl.SetColumnWidth(2, LVSCW_AUTOSIZE_USEHEADER);

Here is the function that is mapped to LVN_COLUMNCLICK:

void CReportCtrl::OnColumnClick(NMHDR* pNMHDR, LRESULT* pResult)
{
      NM_LISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;
      const int iColumn = pNMListView->iSubItem;

      // if it's a second click on the same column then reverse the sort order,
      // otherwise sort the new column in ascending order.
      Sort(iColumn, iColumn == m_wndHeader.GetSortedColumn() ? !m_wndHeader.IsSortAscending() : TRUE);

      *pResult = 0;
}
Here is the sort function it calls:

void CReportCtrl::Sort(int iColumn, BOOL bAscending)
{
      if (iColumn < 0 || iColumn >= GetColumnCount())
            return;
      
      m_wndHeader.SetSortedColumn(iColumn);
      m_wndHeader.SetSortAscending(bAscending);

      // show the appropriate arrow in the header control.
      m_wndHeader.UpdateSortArrow(); // my corresponding header control

      VERIFY(SortItems(_CompareFunction, reinterpret_cast<DWORD>(this)));
}

and the functions that sort calls:

void CReportCtrl::CReportHeaderCtrl::SetSortedColumn(int nCol)
{
      m_iSortColumn = nCol;
}

and

void CReportCtrl::CReportHeaderCtrl::SetSortAscending(BOOL bAscending)
{
      m_bSortAscending = bAscending;
}

m_iSortColumn is an int initialised as -1, and m_bSortAscending a BOOL initialised as TRUE.

Thanks a lot,
Sternocera
Lastly, here is the _CompateFunction function:

int CALLBACK CReportCtrl::_CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamData)
{
      CReportCtrl* pListCtrl = reinterpret_cast<CReportCtrl*>(lParamData);
      ASSERT(pListCtrl->IsKindOf(RUNTIME_CLASS(CListCtrl)));

      ItemData* pid1 = reinterpret_cast<ItemData*>(lParam1);
      ItemData* pid2 = reinterpret_cast<ItemData*>(lParam2);

      ASSERT(pid1);
      ASSERT(pid2);

      CString s1 = pid1->arrStr[ pListCtrl->m_wndHeader.GetSortedColumn()];
      CString s2 = pid2->arrStr[ pListCtrl->m_wndHeader.GetSortedColumn()];

      s1.TrimLeft();
      s1.TrimRight();
      s2.TrimLeft();
      s2.TrimRight();

      if (s1.IsEmpty() || s2.IsEmpty())
            return pListCtrl->m_wndHeader.IsSortAscending() ? _StringCompare(s1, s2) : _StringCompare(s2, s1);

      if(_IsNumber(s1))
            return pListCtrl->m_wndHeader.IsSortAscending() ? _NumberCompare(s1, s2) : _NumberCompare(s2, s1);
      else if(_IsDate(s1))
            return pListCtrl->m_wndHeader.IsSortAscending() ? _DateCompare(s1, s2) : _DateCompare(s2, s1);
      else
            // text.
            return pListCtrl->m_wndHeader.IsSortAscending() ? _StringCompare(s1, s2) : _StringCompare(s2, s1);
}

Thanks a lot,
Sternocera
I don't see what 'SortItems' is

What is the data - text, date, numeric...
Sortitems is an overloaded function:

BOOL CGridCtrl::SortItems(int nCol, BOOL bAscending, LPARAM data /* = 0 */)
{
    SetSortColumn(nCol);
    SetSortAscending(bAscending);
    ResetSelectedRange();
    SetFocusCell(-1, - 1);

      if (m_pfnCompare == NULL)
            return CGridCtrl::SortItems(pfnCellTextCompare, nCol, bAscending, data);
      else
          return CGridCtrl::SortItems(m_pfnCompare, nCol, bAscending, data);
}

// Sorts on a given column using the supplied compare function (see CListCtrl::SortItems)
BOOL CGridCtrl::SortItems(PFNLVCOMPARE pfnCompare, int nCol, BOOL bAscending,
                          LPARAM data /* = 0 */)
{
    SetSortColumn(nCol);
    SetSortAscending(bAscending);
    ResetSelectedRange();
    SetFocusCell(-1, -1);
    return SortItems(pfnCompare, nCol, bAscending, data, GetFixedRowCount(), -1);
}

The data can be text, date or numeric. The CReportCtrl class is supposed to be flexible like that.

Thanks,
Sternocera
PLease don't take offence but I see some code being busy but not actually doing anything.  You are COMPARING items.

I suspect HALF the problem is covered here.
https://www.experts-exchange.com/questions/22728297/MFC-Displaying-a-CListView.html

Sorry to be cryptic but if you look at the link it will hopefully give you an idea.



ps.  I might be bing dumb but I don't see each item in the list being checked.
Andy,

This post was for a generic CListCtrl, as apposed to for this particular tutorial.

For whatever reason, this thing doesn't work for me. I've probably failed to take account of something very trivial. I know it works, because there is a demo application. It must be something to do with compiler incompatibilities, because this code was written in visual studio 6.

I think I've found something better: http://www.codeproject.com/miscctrl/gridctrl.asp . It has a lot of additional functionality that I consider valuable too. It's a custom, Cwnd derived class. Just the kind of flexibility I value,

Thanks for everything,
Sternocera
I've been having another look at the code - I think I got distracted and made a silly comment. The SortItems is a member of the list control and it does look to be being called correctly.
Did you check that the function CReportCtrl::_CompareFunction was actually being called?  (You might need to have the properties of the list control set to allow sorting).
I've apended a messagebox function call to _CompareFunction:

int CALLBACK CReportCtrl::_CompareFunction(LPARAM lParam1, LPARAM lParam2, LPARAM lParamData)
{
      AfxMessageBox("_CompareFunction called");
      CReportCtrl* pListCtrl = reinterpret_cast<CReportCtrl*>(lParamData);
      ASSERT(pListCtrl->IsKindOf(RUNTIME_CLASS(CListCtrl)));

      ItemData* pid1 = reinterpret_cast<ItemData*>(lParam1);
      ItemData* pid2 = reinterpret_cast<ItemData*>(lParam2);

      ASSERT(pid1);
      ASSERT(pid2);

      CString s1 = pid1->arrStr[ pListCtrl->m_wndHeader.GetSortedColumn()];
      CString s2 = pid2->arrStr[ pListCtrl->m_wndHeader.GetSortedColumn()];

      s1.TrimLeft();
      s1.TrimRight();
      s2.TrimLeft();
      s2.TrimRight();

      if (s1.IsEmpty() || s2.IsEmpty())
            return pListCtrl->m_wndHeader.IsSortAscending() ? _StringCompare(s1, s2) : _StringCompare(s2, s1);

      if(_IsNumber(s1))
            return pListCtrl->m_wndHeader.IsSortAscending() ? _NumberCompare(s1, s2) : _NumberCompare(s2, s1);
      else if(_IsDate(s1))
            return pListCtrl->m_wndHeader.IsSortAscending() ? _DateCompare(s1, s2) : _DateCompare(s2, s1);
      else
            // text.
            return pListCtrl->m_wndHeader.IsSortAscending() ? _StringCompare(s1, s2) : _StringCompare(s2, s1);
}

I never see any message box, so it would seem _CompareFunction is never called. I saw a post on the bottom of that page dated 2005 (post release of visual studio 2005) from a guy who had exactly the same problem as me.  I'm going to see how this works in visual studio 6. I strongly suspect it'll work,

Thanks,
Sternocera
ASKER CERTIFIED SOLUTION
Avatar of AndyAinscow
AndyAinscow
Flag of Switzerland image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Andy,

I've tried that:
      SampleReportCtrl.Sort(0,TRUE);
      SampleReportCtrl.Sort(1,TRUE);
      SampleReportCtrl.Sort(2,TRUE);
      SampleReportCtrl.Sort(3,TRUE);
Unfortunately, it didn't make any difference. I'm going to try getting it to compile in vs6. I'll keep you posted,
Thanks,
Sternocera
No - not this special class.
In the resource editor, check the properties of the list control itself.
The plot thickens.

I changed the sort setting. Nothing happened. I decided to absolutely reconcile my my list with the list in the demo. I changed the type from icon to report, which I think was the pertinent bit.

I got this error message in debug mode: Unhandled exception at 0x7814455c in Lustre.exe: 0xC0000005: Access violation reading location 0x488e0475.

this occured here:
      for(int iColumn = 1; iColumn < GetColumnCount(); iColumn++)
      {
            LPCTSTR lpsz = va_arg(list, LPCTSTR);
            CString str = (lpsz == NULL) ? _T("") : lpsz;
            arr.Add(str); // on this line
            CListCtrl::SetItemText(iIndex, iColumn, str);
      }

This might have something to do with character encoding, I don't know.

Thanks for everything,
Sternocera
? ulp.

is this giving a valid string?
        LPCTSTR lpsz = va_arg(list, LPCTSTR);
here is the whole function:

int CReportCtrl::InsertItem(int nIndex, LPCTSTR pszText, ...)
{
      const int iIndex = CListCtrl::InsertItem(nIndex, pszText);

      if (!_IsValidIndex(iIndex))
            return iIndex;

      CStringArray arr;
      arr.Add(pszText);

       va_list list;
      va_start(list, pszText);

      for(int iColumn = 1; iColumn < GetColumnCount(); iColumn++)
      {
            LPCTSTR lpsz = va_arg(list, LPCTSTR);
            CString str = (lpsz == NULL) ? _T("") : lpsz;
            arr.Add(str);
            CListCtrl::SetItemText(iIndex, iColumn, str);
      }

      va_end(list);

      _AssignNewItemData(iIndex, arr.GetData(), arr.GetSize());

      return iIndex;
}

I've looked into it. Apparently, va_list is a standard c datatype that is passed as an argument to va_start, va_end, and, in our case, va_arg.

va_arg is a macro that retrieves the value of an argument from a variable argument list.

http://www.cplusplus.com/reference/clibrary/cstdarg/va_arg.html

(you probably already know all this, I'm aware)

I guess this means that the string ultimately comes from the argument that is padded to InsertItem.

Frankly, I'm stumped. I tried populating my listctrl with the _T() function rather then a straight string literal, but that made no difference either.

Over to you, Andy...

Thanks
That looks like you have a function that adds the main item and the sub items in one go

eg.
InsertItem(1, "col 1", "col 2", "col 3");

Are you passing in the same number of strings as there are columns?
I Insert columns using a mechanism defined by the class:
SampleReportCtrl.SetHeadings(_T("column1, 90; column2, 90; column3, 90"));

I guess there is code somewhere to parse the string.

However, I insert items in this way:
SampleReportCtrl.InsertItem(0, _T("test1"));
SampleReportCtrl.InsertItem(1, _T("test2"));
SampleReportCtrl.InsertItem(2, _T("test3"));

InsertItem is an overloaded function that is defined in the supplied header file. It would seem they were intended to be used together.
I tried changing:
SampleReportCtrl.SetHeadings(_T("column1, 90; column2, 90; column3, 90"));

to

SampleReportCtrl.SetHeadings(_T("column1, 90; column2, 90; column3, 90;"));

the extra ";" didn't make any difference.
Andy,

I've found a solution! I found that by calling a different overloaded SampleReportCtrl, everything works nicely:
SampleReportCtrl.InsertItem(0, _T("test1"),0);

However, since your help led me down a path of reasoning that led to my finding the answer, I think that you should get the 500 points.

Thanks alot