MFC Grid manual

How to install the MFC grid and compile the first application?

The grid is distributed in zip packages that contain all files needed to build an application. Right now we support VC++ 6.0 and VC++ 8.0 compilers. Each package consist of two libraries: Common and GUI. Common contains definition of interfaces that enable the grid to use objects of arbitrary classes, formats to convert values to strings and vice-versa, and classes for working in multi-threaded environment. GUI has the implementation of the grid and different visual controls. Both Common and GUI can be used in static or dynamic libraries. There are 8 possible configurations of statically-linked libraries (debug/release, with/without unicode, different kinds of runtime libraries) and 4 dynamically-linked configurations. Below you will find a short guide to building your first application.

1. Firstly, unzip the package. For example, to the folder C:\Program Files\Dapfor\MFCGrid_v2_0_0 We advise you to keep the version number of the library in the folder's name.

2. Create a new MFC project. It may be a Dialog, SDI or MDI based application. For the sake of simplicity, let's create an SDI application and call it MyFirstApplication.

3. Add references to include files of the unzipped package to Additional Include Directories. There are many ways to do it. One of these ways is adding a system variable DAPFOR_HOME with a value indicating the directory where you have unzipped the package:

system_var.gif

Then you put the following path to Additional Include Directories: $(DAPFOR_HOME)\include

include_dir.gif

4. Let's do the same for Additional Library directories and add the next path: $(DAPFOR_HOME)\lib

lib_dir.gif

5. If you want to use the MFC grid in static libraries, add the definition DF_STATIC_LIB as shown below to preprocessor definitions.

include_define.gif

6. Select the right version of the runtime library:

mfa_runtime_lib.gif

As shown above, there are 12 possible configurations of the MFC Grid libraries. The following table will help you to select the runtime library:

Build version Dapfor libraries Character set MFC libraries Runtime libraries
Debug static Not set static /MTd
Release static Not set static /MT
Debug static Unicode static /MTd
Release static Unicode static /MT
Debug static Not set dynamic /MDd
Release static Not set dynamic /MD
Debug static Unicode dynamic /MDd
Release static Unicode dynamic /MD
Debug dynamic Not set dynamic /MDd
Release dynamic Not set dynamic /MD
Debug dynamic Unicode dynamic /MDd
Release dynamic Unicode dynamic /MD

7. To choose the right version of the GUI & Common libraries we have added Autolink.h files to each project. You can manually choose library version, but we recommend that you add these files to StdAfx.h of your project. In this case, GUI and Common are chosen automatically.

//StdAfx.h file

#include <Dapfor/GUI/Autolink.h>
#include <Dapfor/Common/Autolink.h>
Now we have done everything to compile and link the first project.

8. Let's create the first C++ class, objects of which can be inserted into the grid. Read What is a data object? article to get more information. This class must be derived from Dapfor::Common::CDataObject. The base class adds only a possibility to use the objects of derived classes in the grid

class CTestClass : public Dapfor::Common::CDataObject
{
...

Add a constructor, destructor and some variables and methods to our class.

...
public:
    CTestClass(CString vName, long vLong, __int64 vInt64, double vDouble);
    ~CTestClass();

    //Some functions. 
    CString GetName() const;
    long    GetSomeLong() const;
    __int64 GetSomeInt64() const;
    double  GetSomeDouble() const;
    long    GetSomeDate() const;


private:
    //Some variables
    CString m_Name;
    long    m_Long;
    __int64 m_Int64;
    double  m_Double;
    long    m_Date;

...

Now we will declare a very important thing - Field map. This is the heart of the Common library. It enables the grid to find and call functions of the class to get object values. The Fildmap contains a list of fields. Each field has a unique identifier, and may also contain: a name, Get-function, Set-function and a format to convert value returned by Get- function to a string and vice-versa. The same identifiers are used in columns of the grid. This means that objects of this class correspond to rows in the grid and methods of this class correspond to columns.

...
    DF_DECLARE_FIELD_MAP();
...

Finally, we will declare identifiers for fields. Of course, you can freely use identifiers like 1, 2, 3... However, the best practice is to use the enumeration, so that the code becomes comprehensible and safe. In this case multiple verifications are done at compile time.

public:
    enum Fields
    {
        FidName = 1234,         //You can use any values. (-1 & -2 have special meaning)
        FidSomeLong = 5000,     //You don't have to follow the sequence... 
        FidSomeInt64,
        FidSomeDouble,
        FidSomeDate,
    };
...

Below you will find the full text of the TestClass.h file:

#pragma once

#include <string>
#include <Dapfor/Common/DataObject.h>

class CTestClass : public Dapfor::Common::CDataObject
{
public:
    //The functions declared below can be called by the MFC grid. To call them we use a mapping table 
    //(one per class, not per object). The Get- and Set- functions are grouped in properties. Each property
    //has a numeric identifier. The same numeric identifiers are used in columns of the grid.
    //You can freely use identifiers like 1, 2, 3... However, the best practice is to use the enumeration! In this case
    //many verifications are done at compile time, the code become more comprehensible and safe.
    enum Fields
    {
        FidName = 1234,         //You can use any values. (-1 & -2 have special meaning)
        FidSomeLong = 5000,     //You may not respect the sequence... 
        FidSomeInt64,
        FidSomeDouble,
        FidSomeDate,
    };


public:
    CTestClass(CString vName, long vLong, __int64 vInt64, double vDouble);
    ~CTestClass();

    //Some functions. The grid calls them through the mapping as declared below
    CString GetName() const;
    long    GetSomeLong() const;
    __int64 GetSomeInt64() const;
    double  GetSomeDouble() const;
    long    GetSomeDate() const;

private:
    //Some variables
    CString m_Name;
    long    m_Long;
    __int64 m_Int64;
    double  m_Double;
    

private:
    //To make the functions visible by the grid, we declare the field map - the list of properties (Get- & Set functions)
    DF_DECLARE_FIELD_MAP();
};

Now let's implement our class.

//file TestClass.cpp

#include "StdAfx.h"
#include "TestClass.h"
#include <Dapfor/Common/LongDateFormat.h>


//Field map implementation. Here you declare the properties of the class that are accessible from the grid.
//Each property has a unique identifier, and optionally may have a name, Get-function, Set-function and a format 
//to convert value, returned by the Get- function to string and vice-versa.
DF_BEGIN_FIELD_MAP(CTestClass)
    DF_MFC_STRING_ID(FidName,   _T("Name"),  &CTestClass::GetName,       0, 0)
    DF_LONG_ID(FidSomeLong,     _T("Long"),  &CTestClass::GetSomeLong,   0, 0)
    DF_INT64_ID(FidSomeInt64,   _T("Int64"), &CTestClass::GetSomeInt64,  0, 0)
    DF_DOUBLE_ID(FidSomeDouble, _T("Double"),&CTestClass::GetSomeDouble, 0, 0)
    
    //The last parameter in the field declaration - the custom format
    DF_LONG_ID(FidSomeDate,     _T("Date"),  &CTestClass::GetSomeDate,   0, new CLongDateFormat(CLongDateFormat::short_date));
DF_END_FIELD_MAP()


//The constructor
CTestClass::CTestClass(CString vName, long vLong, __int64 vInt64, double vDouble)
    : m_Name(vName)
    , m_Long(vLong)
    , m_Int64(vInt64)
    , m_Double(vDouble)
    , m_Date(CTime::GetCurrentTime().GetTime())
{
}

//The destructor
CTestClass::~CTestClass()
{
}


//Implementation of class methods
CString CTestClass::GetName() const         {return m_Name;}
long    CTestClass::GetSomeLong() const     {return m_Long;}
__int64 CTestClass::GetSomeInt64() const    {return m_Int64;}
double  CTestClass::GetSomeDouble() const   {return m_Double;}
long    CTestClass::GetSomeDate() const     {return m_Date;}

We have created and implemented the class, objects of which can be inserted to the grid. It was easy, wasn't it? How does it work? If you see a definition of the CDataObject class, you may discover methods like long GetLong(FID fid) const, double GetDouble(FID fid) const, etc. FID is a numeric identifier, that you have defined in the FieldMap. Thanks to this mapping we can call functions indirectly by their identifiers:

//In this example both calls will give the same result
CTestClass obj(_T("object 1"), 121, 1234567890, 1.32);
    
long l1 = obj.GetSomeLong();                    //Direct call
long l2 = obj.GetLong(CTestClass.FidSomeLong);  //Indirect call through the identifier

//The following call returns the formatted string "121" that represents the long value 121
//Note that the second parameter is a format that may be specified in the column of the grid's header.
//So, we can present the same object differently in different grids.
CString s1 = obj.GetFormattedMfcString(CTestClass.FidSomeLong, 0);

The grid uses indirect callling to get values from an object.

Another important thing is that the last parameter in the field declaration

    DF_LONG_ID(FidSomeLong,     _T("Long"),   &CTestClass::GetSomeLong,   0, 0)
may determine format that transforms value returned by the function to a string. If you didn't specify the format, the default format is used. This way the grid gets a formatted string that is shown in the cell.

9. To demonstrate a realistic use case, let's create a container of CTestClass objects. In the created SDI application we should have a document class CMyFirstApplicationDoc. Let's add the container to the document class:

// MyFirstApplicationDoc.h : interface of the CMyFirstApplicationDoc class

#pragma once

#include "TestClass.h"
#include <vector>

class CMyFirstApplicationDoc : public CDocument
{
public:
    //Declaration of the container. This container will keep pointers to the objects of CTestObject class
    typedef std::vector<CTestClass*> Container;

public:
    CMyFirstApplicationDoc();
    virtual ~CMyFirstApplicationDoc();

    const Container& GetContainer() const;
    ...

private:
    Container m_Container;
    ...
};

Below you will find an implementation of the document in the MyFirstApplicationDoc.cpp file:

//Constructor
CMyFirstApplicationDoc::CMyFirstApplicationDoc()
{
    // Populate our container
    m_Container.push_back(new CTestClass(_T("object 1"), 121, 1234567890, 1.32));
    m_Container.push_back(new CTestClass(_T("object 2"), 122, 1234567891, 1.33));
    m_Container.push_back(new CTestClass(_T("object 3"), 123, 1234567892, 1.34));
    m_Container.push_back(new CTestClass(_T("object 4"), 124, 1234567893, 1.35));
    m_Container.push_back(new CTestClass(_T("object 5"), 125, 1234567894, 1.36));
    m_Container.push_back(new CTestClass(_T("object 6"), 126, 1234567895, 1.37));
    m_Container.push_back(new CTestClass(_T("object 7"), 127, 1234567896, 1.38));
    m_Container.push_back(new CTestClass(_T("object 8"), 128, 1234567897, 1.39));
    m_Container.push_back(new CTestClass(_T("object 9"), 129, 1234567898, 1.40));
    m_Container.push_back(new CTestClass(_T("object 10"), 130, 1234567899, 1.41));
}

//Destructor
CMyFirstApplicationDoc::~CMyFirstApplicationDoc()
{
    for(Container::iterator it = m_Container.begin(); it != m_Container.end(); ++it)
    {
        delete *it;
    }
}

//Field accessor
const CMyFirstApplicationDoc::Container& CMyFirstApplicationDoc::GetContainer() const
{
    return m_Container;
}

10. Now we will initialize the grid. To add the grid to view you need to include the grid header to the MyFirstApplicationView.h file. Then add a member of the Dapfor::GUI::CGrid, and add OnCreate, OnSize and OnDestroy message handlers as shown below:

// MyFirstApplicationView.h

#include <Dapfor/GUI/Grid.h>

class CMyFirstApplicationView : public CView
{
...
protected:
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    afx_msg void OnDestroy();
    afx_msg void OnSize(UINT nType, int cx, int cy);
    ...

private:
    Dapfor::GUI::CGrid m_Grid;
    ...

Below you will find an implementation of view and grid initialization

// MyFirstApplicationView.cpp : implementation of the CMyFirstApplicationView class
...
#include "TestClass.h"

//Messages mapping
BEGIN_MESSAGE_MAP(CMyFirstApplicationView, CView)
    ...
    ON_WM_CREATE()
    ON_WM_DESTROY()
    ON_WM_SIZE()
    ...
END_MESSAGE_MAP()

...
int CMyFirstApplicationView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    if (CView::OnCreate(lpCreateStruct) == -1)
        return -1;

    //Initialize the grid
    m_Grid.Create(0, 0, WS_VISIBLE|WS_CHILD, CRect(), this, 1000);

    //Create a header. The 'true' value in the constructor indicates, that the header is owned by the grid and
    //the grid handles the life time of the header. This means that the grid destroys the header in its destructor.
    Dapfor::GUI::CHeader* header = new Dapfor::GUI::CHeader(true);
    
    //Add some columns. To show values, returned by functions, defined in CTestClass, 
    //we will use identifiers, declared in this class.
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidName,       _T("Name"), 70));
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidSomeLong,   _T("Long"), 60));
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidSomeInt64,  _T("Int64"), 100));
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidSomeDouble, _T("Double"), 60));
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidSomeDate,   _T("Date"),   80));

    //Set the header
    m_Grid.SetHeader(header);
    
    //Initialize background colors
    m_Grid.GetAppearance().SetBackEvenColor(RGB(255, 255, 255));
    m_Grid.GetAppearance().SetBackOddColor(RGB(240, 240, 240));

    
    //Add objects from the document to the grid...
    for(CMyFirstApplicationDoc::Container::const_iterator it = GetDocument()->GetContainer().begin(); 
                                                    it != GetDocument()->GetContainer().end(); 
                                                    ++it)
    {
        Dapfor::GUI::HITEM handle = m_Grid.Add(*it);
    }

    //Here you can set your sorting rules.
    //A small tweak: left-click the header to change sorting direction
    //Ctrl-click adds/changes multiple sorting
    //Shift-click cancels the sorting
    m_Grid.SetSort(0, Dapfor::GUI::CSortInfo(CTestClass::FidSomeLong, false));  //Sorting by the first column
    m_Grid.SetSort(1, Dapfor::GUI::CSortInfo(CTestClass::FidSomeInt64, true));  //This line adds multiple sorting by the second column

    //Select rows 3 and 4
    m_Grid.Select(3, Dapfor::GUI::ScrollableContext, true);
    m_Grid.Select(4, Dapfor::GUI::ScrollableContext, true);

    //Select a row in the grid that corresponds to the sixth element in the container.
    //Note, that you don't need to find the row in the grid. 
    //You can be sure that this is more efficient than linear search even in the grids with huge number of elements.
    m_Grid.Select(GetDocument()->GetContainer()[6], true);

    return 0;
}

void CMyFirstApplicationView::OnDestroy()
{
    //Remove all values from the grid. Note, that the grid doesn't handle the lifetime of inserted objects and doesn't 
    //destroy them.
    m_Grid.DeleteAll();

    CView::OnDestroy();
}

void CMyFirstApplicationView::OnSize(UINT nType, int cx, int cy)
{
    CView::OnSize(nType, cx, cy);

    //Below we modify a size of the grid
    CRect rc;
    GetClientRect(rc);
    m_Grid.MoveWindow(rc);
}

Please note that the grid provides a number of methods like Select(const Common::CDataObject* pDO, ...), where you can use directly your business object. You don't need your object placement anymore. The same approach is used for the columns. Instead of indexing the columns, you can use enumerations (like defined in the CTestObject). You are free forget that your object was inserted into the grid.

11. Now, we will show you how to make the grid editable. If we take a data object, it has only the Get- properties that return values by their identifiers. These values are shown in grid cells. Now, we provide a set of functions to modify data inside the data object.

class CTestClass : public Dapfor::Common::CDataObject
{
    ...
    //Some set functions. The grid will call them while edit in place operations
    void SetSomeLong(long v);
    void SetSomeInt64(__int64 v);
    void SetSomeDouble(double v);
    void SetSomeDate(long v);
    ...
};

Below you will find an implementation of these function. Note that there are declarations of Set- methods in the Field map and each Set-method of your objects sends notifications. This is a very important step. You can call the Set- methods of your objects anywhere in the application, and each grid (one or more), where the objects were inserted will automatically update, sort, filter and highlight the cells. You do not need to know, where your objects are placed.

DF_BEGIN_FIELD_MAP(CTestClass)
    DF_MFC_STRING_ID(FidName,   _T("Name"),   &CTestClass::GetName,       0, 0)
    DF_LONG_ID(FidSomeLong,     _T("Long"),   &CTestClass::GetSomeLong,   &CTestClass::SetSomeLong, 0)
    DF_INT64_ID(FidSomeInt64,   _T("Int64"),  &CTestClass::GetSomeInt64,  &CTestClass::SetSomeInt64, 0)
    DF_DOUBLE_ID(FidSomeDouble, _T("Double"), &CTestClass::GetSomeDouble, &CTestClass::SetSomeDouble, 0)
    DF_LONG_ID(FidSomeDate,     _T("Date"),   &CTestClass::GetSomeDate,   &CTestClass::SetSomeDate, new CLongDateFormat(CLongDateFormat::short_date));
DF_END_FIELD_MAP()

//Implementation of Set- methods
void CTestClass::SetSomeLong(long v)
{
    m_Long = v;

    //Send notification to the grid. Each grid automatically updates the cell, sorts and filters the row if needed.
    NotifyUpdate(FidSomeLong);
}
void CTestClass::SetSomeInt64(__int64 v)
{
    m_Int64 = v;
    NotifyUpdate(FidSomeInt64);
}
void CTestClass::SetSomeDouble(double v)
{
    m_Double = v;
    NotifyUpdate(FidSomeDouble);
}
void CTestClass::SetSomeDate(long v)
{
    m_Date = v;
    NotifyUpdate(FidSomeDate);
}

We have almost made the grid editable. To finish, we will declare the columns editable:

//file MyFirstApplicationView.cpp
    ...
    
    //The last parameter indicates that the column will be editable
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidName,       _T("Name"),   70));
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidSomeLong,   _T("Long"),   60,  Dapfor::GUI::CColumn::Auto, true));
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidSomeInt64,  _T("Int64"),  100, Dapfor::GUI::CColumn::Auto, true));
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidSomeDouble, _T("Double"), 60,  Dapfor::GUI::CColumn::Auto, true));
    header->Add(new Dapfor::GUI::CColumn(CTestClass::FidSomeDate,   _T("Date"),   80,  Dapfor::GUI::CColumn::Auto, true));
    ...

How does it work? When the user clicks on the grid, an appropriate MFC control is shown over the cell. Then the user can modify the text, select the desired item in the combobox, or date in the datetime picker. Then the grid tries to set the new value to the data object through the Set- property that we have implemented before. Note that all grids, where the data object was inserted are notified (the call of the NotifyUpdate() method) and automatically perform nested operations to update the visibility and position of this object.

Try to edit a cell in the sorted column in the demo application. The row will be resorted correctly

12. Compile and execute the application. Below you will find a snapshot of this application.

The sources of MyFirsrtApplication project are available here.

my_first_app.gif

Read Drawing workflow and Custom drawing to implement the powerful features in your application.