User Tools

Site Tools


data_grid

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

data_grid [2008/09/19 01:09] (current)
Line 1: Line 1:
 +====== Anatomy of a custom control (a simple data grid) ======
 + --- //David MacDermot 2008/09/17 19:55//
 +
 +This article describes how to create a custom control in Pelles C. The control in this case is a data grid like component using a listview and an edit box along with a number of customizations to enhance the appearance of the control.
 +
 +===== Project =====
 +{{:​data_grid:​sdkdatagridview.zip|Project Source and Demo}}
 +
 +
 +{{:​data_grid:​screenshotdemo.png|}}
 +===== Introduction =====
 +
 +The Windows SDK provides a selection of user interface controls for application development that covers most of the bases when it comes to user interface design. ​ Sooner or later however, you run into a situation where you wish there was something else in the toolbox that isn’t there. ​ A search of the web reveals a number of clever solutions but most of them use MFC or another of the modern object oriented platforms and they don’t translate well into Pelles C.  More often than not you are going to have to create your own control from scratch.
 +The good news is rolling your own control does not have to be a difficult proposition.
 +
 +===== The windows class =====
 +
 +The windows class describes common properties of all windows that will be created using that class. ​  ​We’ll use the Windows class to encapsulate the methods associated with our custom control and also to provide the public interface for our control. ​ The public interface for our control will be message based so there is only one public method that we create in order to register the windows class. ​ This method is called only once to register the class. ​ Instances of the control are created using **CreateWindow()**.
 +
 +<code c>ATOM InitDataGridView(HINSTANCE hInstance)
 +{
 +    WNDCLASSEX wcex;
 +    ATOM aReturn;
 +
 +    wcex.cbSize ​         = sizeof(WNDCLASSEX);​
 +    wcex.style ​          = CS_BYTEALIGNCLIENT;​
 +    wcex.lpfnWndProc ​    = (WNDPROC)Grid_WndProc;​
 +    wcex.cbClsExtra ​     = 0;
 +    wcex.cbWndExtra ​     = 0;
 +    wcex.hInstance ​      = hInstance;
 +    wcex.hCursor ​        = NULL;
 +    wcex.hbrBackground ​  = (HBRUSH) (GetStockObject(GRAY_BRUSH)); ​
 +    wcex.lpszMenuName ​   = NULL;
 +    wcex.lpszClassName ​  = TEXT("​DGridVwClass"​);​
 +    wcex.hIcon ​          = NULL;
 +    wcex.hIconSm ​        = NULL;
 +
 +    aReturn = RegisterClassEx(&​wcex);​
 +
 +    return aReturn;
 +}</​code> ​
 +
 +This method should be called in the application'​s entry point procedure immediately following the calls to initialize common controls.
 +
 +<code c>int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,​ LPSTR lpszCmdLine,​ int nCmdShow)
 +{
 +    INITCOMMONCONTROLSEX icc;
 +    WNDCLASSEX wcx;
 +
 +    ghInstance = hInstance;
 +
 +    /* Initialize common controls. Also needed for MANIFEST'​s */
 +
 +    icc.dwSize = sizeof(icc);​
 +    icc.dwICC = ICC_WIN95_CLASSES /​*|ICC_COOL_CLASSES|ICC_DATE_CLASSES|ICC_PAGESCROLLER_CLASS|ICC_USEREX_CLASSES*/;​
 +    InitCommonControlsEx(&​icc);​
 +
 +    /* Register our custom control*/
 +    InitDataGridView(hInstance);​
 +
 +</​code>​
 +
 +===== The callback procedure =====
 +
 +This is where we’ll support the public interface to our control as well as handle the standard windows messages that the operating system sends to our control’s window. ​
 +
 +Callback procedures can become quite lengthy and difficult to maintain in a complex control. ​ Windows provides a series of macros in **windowsx.h** and **comctrl.h** that greatly enhance code readability and help to break out code into manageable event driven method chunks. ​ These macros are referred to as message crackers and there are some tools that make implementing them fairly easy.  I used the [[http://​www.codeproject.com/​KB/​winsdk/​msgcrackwizard.aspx|Message Cracker Wizard]] to generate the macros and method prototypes for the standard windows messages, greatly reducing the size of the callback procedure.
 +
 +Most of the lines of code in this section are devoted to the control’s own custom messages and indeed mimic the property getters and setters common to C++.
 +
 +<code c>static LRESULT CALLBACK Grid_WndProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
 +{
 + // Pass along any list view specific messages transparently
 + if(LVM_FIRST <= msg && msg <= LVM_FIRST + 181)
 + {
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 + return SNDMSG(g_lpInst->​hwndList,​msg,​wParam,​lParam);​
 + }
 + switch(msg)
 + {
 + HANDLE_MSG (hwnd, WM_CTLCOLOREDIT,​ Grid_OnCtlColorEdit);​
 + HANDLE_MSG (hwnd, WM_CREATE, Grid_OnCreate);​
 + HANDLE_MSG (hwnd, WM_DESTROY, Grid_OnDestroy);​
 + HANDLE_MSG (hwnd, WM_GETDLGCODE,​ Grid_OnGetDlgCode);​
 + HANDLE_MSG (hwnd, WM_SIZE, Grid_OnSize);​
 + HANDLE_MSG (hwnd, WM_HSCROLL, Grid_OnHScroll);​
 + HANDLE_MSG (hwnd, WM_VSCROLL, Grid_OnVScroll);​
 + HANDLE_MSG (hwnd, WM_NOTIFY, Grid_OnNotify);​
 + HANDLE_MSG (hwnd, WM_SETCURSOR,​ Grid_OnSetCursor);​
 +
 + case DGVM_GETLISTVIEWCONTROL:​
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 + return (LRESULT) g_lpInst->​hwndList;​
 +
 + case DGVM_GETEDITCONTROL:​
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 + return (LRESULT) g_lpInst->​hwndEditor;​
 +
 + case DGVM_GETCOLUMNCOUNT:​
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 + return (LRESULT) Grid_GetColumnCount(g_lpInst->​hwndList);​
 +                //
 +                // Skip a bunch of properties for the sake of brevity
 +                //
 + case DGVM_SETROWHEIGHT:​
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 + Grid_SetRowHeight(g_lpInst->​hwndList,​ (INT) wParam);
 + return TRUE;
 +
 + default: return DefWindowProc (hwnd, msg, wParam, lParam);
 + }
 +}
 +</​code>​
 +
 +===== The public interface =====
 +
 +We’ll define the public interface in the control’s header file.
 +
 +Windows reserves a block of messages for custom controls beginning with the definition of  **WM_USER** so our messages will be **WM_USER** + some value we assign. ​ Messages are sent to a control via **SendMessage()** which gives us two parameters that we can use for passing values to the control’s callback procedure. ​ We can also obtain the callback return value from **SendMessage()**. ​ I find it helpful to define macros for each message as I define the messages themselves. ​ This serves as a way of documenting each message in the code of the header file during development and provides a syntactically pleasing interface for accessing the control’s properties in source code.
 +
 +In addition to the message macros I have also defined some helper macros to make it easier to add columns and rows to our control. ​ Notice that these macros do not contain references to our custom messages! ​ In the control’s callback procedure any listview specific message is forwarded to the grid control’s listview component.
 +
 +<code c>/​****************************************************************************/​
 +// Messages
 +
 +#define DGVM_GETLISTVIEWCONTROL WM_USER + 0x01
 +#define DGVM_GETEDITCONTROL WM_USER + 0x02
 +#define DGVM_GETCOLUMNCOUNT WM_USER + 0x03
 +#define DGVM_GETEDITORBACKCLR WM_USER + 0x04
 +#define DGVM_SETEDITORBACKCLR WM_USER + 0x05
 +#define DGVM_GETEDITORTEXTCLR WM_USER + 0x06
 +#define DGVM_SETEDITORTEXTCLR WM_USER + 0x07
 +#define DGVM_RESIZEABLEHEADER WM_USER + 0x08
 +#define DGVM_GETALTBACKCLR WM_USER + 0x09
 +#define DGVM_SETALTBACKCLR WM_USER + 0x0A
 +#define DGVM_GETALTTXTCLR WM_USER + 0x0B
 +#define DGVM_SETALTTXTCLR WM_USER + 0x0C
 +#define DGVM_SETDOUBLECLICKSELECT WM_USER + 0x0D
 +#define DGVM_FIRSTCOLUMNROWHEADERS WM_USER + 0x0E
 +#define DGVM_SETROWHEIGHT WM_USER + 0x0F
 +
 +/​****************************************************************************/​
 +// Macroes
 +
 +#define DataGridView_AddColumn(hGrid,​nCol,​iWidth,​szColText){ \
 + LVCOLUMN lv_c; lv_c.mask=LVCF_TEXT|LVCF_WIDTH|LVCF_SUBITEM;​ \
 + lv_c.cx=iWidth;​ lv_c.pszText=(szColText);​ \
 + ListView_InsertColumn(hGrid,​nCol,&​lv_c);​}
 +
 +#define DataGridView_AddColumns(hGrid,​aryColTxt,​iColCount) \
 + for(int Col = 0; Col<​iColCount;​Col++) \
 + DataGridView_AddColumn((hGrid),​Col,​strlen(aryColTxt[Col])*16,​aryColTxt[Col]);​
 +
 +#define DataGridView_AddRow(hGrid,​aryItemTxt,​iColCount) \
 + for(int Col = 0; Col<​iColCount;​){\
 + LV_ITEM Lv_i; \
 + Lv_i.mask=LVIF_TEXT;​ \
 + if(0==Col) { \
 + Lv_i.iItem=ListView_GetItemCount(hGrid);​ \
 + Lv_i.iSubItem=Col;​ Lv_i.pszText=aryItemTxt[Col++];​ \
 + ListView_InsertItem(hGrid,&​Lv_i);​} \
 + Lv_i.iSubItem=Col;​ \
 + Lv_i.pszText=aryItemTxt[Col++];​ \
 + ListView_SetItem(hGrid,&​Lv_i);​}
 +
 +#define DataGridView_GetEditorBkColor(hwnd) ​ (COLORREF)SNDMSG((hwnd),​DGVM_GETEDITORBACKCLR,​0,​0L)
 +#define DataGridView_SetEditorBkColor(hwnd,​clrBk) ​ (BOOL)SNDMSG((hwnd),​DGVM_SETEDITORBACKCLR,​0,​(LPARAM)(COLORREF)(clrBk))
 +#define DataGridView_GetEditorTxtColor(hwnd) ​ (COLORREF)SNDMSG((hwnd),​DGVM_GETEDITORTEXTCLR,​0,​0L)
 +#define DataGridView_SetEditorTxtColor(hwnd,​clrBk) ​ (BOOL)SNDMSG((hwnd),​DGVM_SETEDITORTEXTCLR,​0,​(LPARAM)(COLORREF)(clrBk))
 +#define DataGridView_GetListViewControl(hwnd) ​ (HWND)SNDMSG((hwnd),​DGVM_GETLISTVIEWCONTROL,​0,​0L)
 +#define DataGridView_GetEditControl(hwnd) ​ (HWND)SNDMSG((hwnd),​DGVM_GETEDITCONTROL,​0,​0L)
 +#define DataGridView_GetColumnCount(hwnd) ​ (INT)SNDMSG((hwnd),​DGVM_GETCOLUMNCOUNT,​0,​0L)
 +#define DataGridView_GetRowCount(hwnd) ​ ListView_GetItemCount((hwnd))
 +#define DataGridView_SetResizableHeader(hwnd,​fResizable) ​ (BOOL)SNDMSG((hwnd),​DGVM_RESIZEABLEHEADER,​(WPARAM)(fResizable),​(LPARAM)0L)
 +#define DataGridView_GetAltBkColor(hwnd) ​ (COLORREF)SNDMSG((hwnd),​DGVM_GETALTBACKCLR,​0,​0L)
 +#define DataGridView_SetAltBkColor(hwnd,​clrBk,​fPaintByRow) ​ (BOOL)SNDMSG((hwnd),​DGVM_SETALTBACKCLR,​(WPARAM)(fPaintByRow),​(LPARAM)(COLORREF)(clrBk))
 +#define DataGridView_GetAltTextColor(hwnd) ​ (COLORREF)SNDMSG((hwnd),​DGVM_GETALTTXTCLR,​0,​0L)
 +#define DataGridView_SetAltTextColor(hwnd,​clrBk) ​ (BOOL)SNDMSG((hwnd),​DGVM_SETALTTXTCLR,​0,​(LPARAM)(COLORREF)(clrBk))
 +#define DataGridView_SetDoubleClickSelect(hwnd,​fDblClk) ​ (BOOL)SNDMSG((hwnd),​DGVM_SETDOUBLECLICKSELECT,​(WPARAM)(fDblClk),​0L)
 +#define DataGridView_FirstColumnRowHeaders(hwnd,​fDspRwHdr) ​ (BOOL)SNDMSG((hwnd),​DGVM_FIRSTCOLUMNROWHEADERS,​(WPARAM)(fDspRwHdr),​0L)
 +#define DataGridView_SetRowHeight(hwnd,​iHeight) ​ (BOOL)SNDMSG((hwnd),​DGVM_SETROWHEIGHT,​(WPARAM)(iHeight),​0L)
 +
 +/​****************************************************************************/​
 +// Exported function prototypes
 +
 +BOOL InitDataGridView(HINSTANCE hInstance);
 +</​code>​
 +
 +===== Keeping track of multiple instances =====
 +
 +I use a struct to persist properties of each instance of the custom control.
 +
 +<code c>​typedef struct _tagINSTANCEDATA{
 + HINSTANCE hInstance;
 + HWND hwndList;
 + HWND hwndEditor;
 + LVHITTESTINFO hti;
 + SCROLLINFO hsInfo;
 + SCROLLINFO vsInfo;
 + COLORREF Editor_TxtColr;​
 + COLORREF Editor_BkColr;​
 + COLORREF Alt_TxtColr;​
 + COLORREF Alt_BkColr;
 + BOOL fPaintByRow;​
 + BOOL fRowHeaders;​
 + BOOL fResizableHeader;​
 + BOOL fDblClick;
 +}INSTANCEDATA,​ *LPINSTANCEDATA;​
 +</​code>​
 +
 +In addition to this there are some standard methods that I use to manage the creation and assignment of, access to, and destruction of this structure.
 +
 +<code c>static BOOL Control_GetInstanceData (HWND hControl, LPINSTANCEDATA *ppInstanceData)
 +{
 + *ppInstanceData=(LPINSTANCEDATA)GetProp(hControl,​(LPCSTR)"​lpInsData"​);​
 + return NULL != *ppInstanceData;​
 +}
 +
 +static BOOL Control _CreateInstanceData (HWND hControl, ​ LPINSTANCEDATA pInstanceData)
 +{
 + LPINSTANCEDATA pInst = (LPINSTANCEDATA)malloc(sizeof(INSTANCEDATA));​
 + memmove(pInst,​pInstanceData,​sizeof(INSTANCEDATA));​
 +
 + return SetProp(hControl,"​lpInsData",​pInst);​
 +}
 +
 +static BOOL Control _FreeInstanceData (HWND hControl)
 +{
 + LPINSTANCEDATA pInst;
 + if(Control _GetInstanceData(hGrid,&​pInst))
 + {
 + free ((LPINSTANCEDATA)pInst);​
 + RemoveProp(hControl,"​lpInsData"​);​
 + return TRUE;
 + }
 + return FALSE;
 +}
 +</​code>​
 +
 +Here I create and assign the structure in the WM_CREATE handler of the custom control
 +
 +<code c> static BOOL Grid_OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
 +{
 + INSTANCEDATA inst;
 +
 + // initialize hit test info and scroll info
 + memset(&​inst.hti,​ -1, sizeof(LVHITTESTINFO));​
 + memset(&​inst.hsInfo,​ 0, sizeof(SCROLLINFO));​
 + memset(&​inst.vsInfo,​ 0, sizeof(SCROLLINFO));​
 +
 + // get the hInstance
 + inst.hInstance = lpCreateStruct->​hInstance;​
 +
 +    // create the ListView control
 +    inst.hwndList = CreateListView(lpCreateStruct->​hInstance,​ hwnd);
 + if(NULL == inst.hwndList) return FALSE;
 +
 + // default ListView Colors
 + inst.fPaintByRow = TRUE;
 + inst.Alt_TxtColr = ListView_GetTextColor(inst.hwndList);​
 + inst.Alt_BkColr = ListView_GetBkColor(inst.hwndList);​
 +
 + // default ListView pseudoHeaders off
 + inst.fRowHeaders = FALSE;
 +
 +    inst.hwndEditor = CreateCellEditor(lpCreateStruct->​hInstance,​ hwnd);
 + if(NULL == inst.hwndEditor) return FALSE;
 +
 + // default Cell Editor Colors
 + inst.Editor_BkColr = RGB(0,​0,​160);​
 + inst.Editor_TxtColr = RGB(255,​255,​255);​
 +
 + // default Cell selection behavior
 + inst.fDblClick = FALSE;
 +
 + Grid_SetRowHeight(inst.hwndList,​ 20);
 +
 + // TODO: Create inializations for an array of cell edit controls
 +
 + return Grid_CreateInstanceData(hwnd,&​inst);​
 +}
 +</​code>​
 +
 +Here I set a global pointer to the struct associated with this instance of the control and then free the structure when it is no longer needed.
 +
 +<code c> static VOID Grid_OnDestroy(HWND hwnd)
 +{
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 +
 + DestroyWindow(g_lpInst->​hwndList);​
 + DestroyWindow(g_lpInst->​hwndEditor);​
 +
 + Grid_FreeInstanceData(hwnd);​
 + PostQuitMessage(0);​
 +}
 +</​code>​
 +
 +===== Points of interest =====
 +====Custom drawing the listview ====
 +
 +All of the customization and drawing, of the grid’s components, is handled in the NM_CUSTOMDRAW notification of the list view, this includes skinning the header, something that normally one cannot do, but is possible because the header is subclassed. ​ For a detailed tutorial about simple customizations using this method see [[http://​www.codeproject.com/​KB/​list/​lvcustomdraw.aspx|Neat Stuff to Do in List Controls Using Custom Draw]].
 +
 +<code c>​LRESULT Grid_OnCustomDraw (HWND hwnd, LPNMLVCUSTOMDRAW lplvcd)
 +{
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 +
 +    switch(lplvcd->​nmcd.dwDrawStage) ​
 +    {
 + //Before the paint cycle begins
 + //request notifications for individual listview items
 +        case CDDS_PREPAINT : return CDRF_NOTIFYITEMDRAW;​
 + 
 + //Before an item is drawn
 + ​ //​request notifications for individual listview subitems
 +        case CDDS_ITEMPREPAINT:​ return CDRF_NOTIFYSUBITEMDRAW;​
 +
 +        case CDDS_SUBITEM | CDDS_ITEMPREPAINT:​
 +        {
 + // Color alternating Rows
 + if(g_lpInst->​fPaintByRow&&​lplvcd->​nmcd.dwItemSpec & 1)
 + {
 + lplvcd->​clrText = g_lpInst->​Alt_TxtColr;​
 + lplvcd->​clrTextBk = g_lpInst->​Alt_BkColr;​
 + }
 + // Color alternating Cols
 + else if(!g_lpInst->​fPaintByRow&&​lplvcd->​iSubItem & 1)
 + {
 + lplvcd->​clrText ​  = g_lpInst->​Alt_TxtColr;​
 + lplvcd->​clrTextBk = g_lpInst->​Alt_BkColr;​
 + }
 + else // Color default
 + {
 + lplvcd->​clrText ​  = ListView_GetTextColor(g_lpInst->​hwndList);​
 + lplvcd->​clrTextBk = ListView_GetBkColor(g_lpInst->​hwndList);​
 + }
 + // Color active cell
 + if( lplvcd->​iSubItem == g_lpInst->​hti.iSubItem &&
 + lplvcd->​nmcd.dwItemSpec == g_lpInst->​hti.iItem)
 + {
 + lplvcd->​clrText ​  = g_lpInst->​Editor_TxtColr;​
 + lplvcd->​clrTextBk = g_lpInst->​Editor_BkColr;​
 + }
 + // Skin Column headers
 + if(g_lpInst->​fRowHeaders)
 + {
 + HWND hHdr;
 + HDITEM hdi;
 + RECT rc;
 + HDC hdc;
 +
 + hHdr = ListView_GetHeader(hwnd);​
 + Header_GetItem(hHdr,​lplvcd->​iSubItem,​ &hdi);
 + Header_GetItemRect(hHdr,​lplvcd->​iSubItem,​ &rc);
 + hdc=GetDC(hHdr);​
 +
 + if(lplvcd->​iSubItem == g_lpInst->​hti.iSubItem)
 + DrawEdge(hdc,​ &​rc,​EDGE_SUNKEN,​BF_RECT|BF_SOFT|BF_MIDDLE);​
 + else
 + DrawEdge(hdc,​ &​rc,​EDGE_RAISED,​BF_RECT|BF_SOFT|BF_MIDDLE);​
 +
 + rc.left+=6;​
 + rc.bottom-=2;​
 +
 + SetBkMode(hdc,​TRANSPARENT);​
 + SetTextColor(hdc,​GetSysColor(COLOR_BTNTEXT));​
 + SelectObject(hdc,​GetStockObject(DEFAULT_GUI_FONT));​
 + DrawText(hdc,​hdi.pszText,​lstrlen(hdi.pszText),​ &​rc,​DT_SINGLELINE|DT_BOTTOM|DT_LEFT);​
 + SetBkMode(hdc,​OPAQUE);​
 +
 + ReleaseDC(g_lpInst->​hwndList,​hdc);​
 + }
 + // Draw Row headers
 + if(lplvcd->​iSubItem == 0&&​g_lpInst->​fRowHeaders)
 + {
 + static char buf [2048];
 + RECT ​ rc;
 + HDC ​ hdc;
 +
 + ListView_GetSubItemRect(g_lpInst->​hwndList,​
 + lplvcd->​nmcd.dwItemSpec,​0,​LVIR_LABEL,&​rc);​
 +
 + rc.left-=2;​
 + rc.top-=1;​
 + rc.bottom-=1;​
 +
 + hdc=GetDC(g_lpInst->​hwndList);​
 +
 + if(lplvcd->​nmcd.dwItemSpec==g_lpInst->​hti.iItem)
 + DrawEdge(hdc,​ &​rc,​EDGE_SUNKEN,​BF_RECT|BF_SOFT|BF_MIDDLE);​
 + else
 + DrawEdge(hdc,​ &​rc,​EDGE_RAISED,​BF_RECT|BF_SOFT|BF_MIDDLE);​
 +
 + ListView_GetItemText(g_lpInst->​hwndList,​lplvcd->​nmcd.dwItemSpec,​0,​buf,​sizeof buf);
 +
 + SetBkMode(hdc,​TRANSPARENT);​
 + SetTextColor(hdc,​GetSysColor(COLOR_BTNTEXT));​
 + SelectObject(hdc,​GetStockObject(DEFAULT_GUI_FONT));​
 + DrawText(hdc,​buf,​lstrlen(buf),​ &​rc,​DT_SINGLELINE|DT_VCENTER|DT_CENTER);​
 + SetBkMode(hdc,​OPAQUE);​
 +
 + ReleaseDC(g_lpInst->​hwndList,​hdc);​
 +
 + return CDRF_SKIPDEFAULT;​
 + }
 + return CDRF_NEWFONT;​
 + }
 +    }
 +    return CDRF_DODEFAULT;​
 +}
 +</​code>​
 +
 +====Getting the column count ====
 +
 +The listview has a method for getting the item count (row count) but does not provide a method for getting column count. ​ It is therefore necessary to provide an indirect way of determining column count. ​ One method would be to get an item count from the listview’s header but this does not work if the header is not visible yet.  The following is a method that works under any circumstances.
 +
 +<code c>static INT ListView_GetColumnCount(HWND hwnd)
 +{
 + // Method: hittest low right corner of row
 + BOOL fTempRow = FALSE;
 + RECT rc;
 + LVHITTESTINFO ht = {0};
 +
 + if(0 == ListView_GetItemCount(hwnd)) // No rows yet
 + // Temporarily add a row in order to get the count
 + {
 + LV_ITEM Lv_i = {LVIF_TEXT,​0,​0,​0,​0,""​};​
 + ListView_InsertItem(hwnd,&​Lv_i);​
 + fTempRow = TRUE;
 + }
 + ListView_GetItemRect(hwnd,​
 + ListView_GetItemCount(hwnd)-1,​
 + &​rc,​LVIR_BOUNDS);​
 + ht.pt.x = rc.right-1;
 + ht.pt.y = rc.bottom-1;​
 + ListView_SubItemHitTest(hwnd,​ &ht);
 +
 + if(fTempRow) ListView_DeleteItem(hwnd,​0);​ //Delete temp row (if any)
 +
 + return ht.iSubItem;​
 +}
 +</​code>​
 +
 +====Positioning the editor on resize ====
 +
 +Resizing a column is fairly straight forward. ​ But what happens to the edit control that I am positioning over the grid during a resize? ​ It must resize and position accordingly. ​ This is hard to do real time but with some smoke and mirrors I was able to make it look like that is just what is happening.
 +
 +The listview'​s header posts an HDN_ITEMCHANGING notification that signals a resize has begun. ​ I hide the edit control when this happens but the underlying subitem is painted the same colors as the edit control so it looks like the edit control is stretching real-time. ​ All I need to do when the resize is complete is reposition the edit control and show it again. ​ Unfortunately there is no direct way to determine that a resize is complete.
 +
 +I must confess that this one had me stumped for a while. ​ I played with the list view and observed closely what was happening during a resize.
 +
 +  * Mouse down on header
 +  * Mouse move
 +  * Mouse up
 +
 +After attempting a mouse based approach with limited success I noticed something else: the cursor changes when resizing and after a resize is completed. ​ I now had an easy way to determine the completion of the resize.
 +
 +<code c>static BOOL  Grid_OnNotify(HWND hwnd, INT id, LPNMHDR pnm)
 +{
 + //
 + // If this is a resizable grid, initiate column resizing
 + //  resizing is concluded in the Grid_OnSetCursor event handler
 + //
 + if(HDN_ITEMCHANGING==pnm->​code)
 + return Grid_OnColumnResize(hwnd,​ pnm);
 +</​code>​
 +
 +<code c>BOOL Grid_OnColumnResize(HWND hwnd, LPNMHDR pnm)
 +{
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 +
 + if(!g_lpInst->​fResizableHeader) return TRUE;
 +
 + ShowWindow(g_lpInst->​hwndEditor,​FALSE);​
 + g_fsizeCol = TRUE;
 +
 + return FALSE;
 +}
 +</​code>​
 +
 +<code c>BOOL Grid_OnSetCursor(HWND hwnd, HWND hwndCursor, UINT codeHitTest,​ UINT msg)
 +{
 + //
 + // In Grid_OnColumnResize() we set g_fsizeCol when we clicked on the listview header;
 + //  now we have a means of knowing that the uncaptured cursor has
 + //  just slipped off the header. ​
 + //
 + if(g_fsizeCol)
 + {
 + RECT rc;
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 +
 + //​RePosition the editor
 + ListView_GetSubItemRect(g_lpInst->​hwndList,​g_lpInst->​hti.iItem,​g_lpInst->​hti.iSubItem,​LVIR_LABEL,&​rc);​
 + MapWindowPoints(g_lpInst->​hwndList,​ hwnd, (LPPOINT)&​rc.left,​2);​
 + MoveWindow(g_lpInst->​hwndEditor,​
 + rc.left,​rc.top,​rc.right-rc.left,​rc.bottom-rc.top,​TRUE);​
 +
 + //Show the editor
 + ShowWindow(g_lpInst->​hwndEditor,​TRUE);​
 +
 + g_fsizeCol = FALSE;
 + }
 + return FALSE; //Since this is not the standard use of this event don't handle it.
 +}
 +</​code>​
 +
 +====Scrolling====
 +
 +Getting the scrolling to work right in this control was a challenge. ​ I would resize the grid columns so that the horizontal scrollbar appeared, scroll over, then resize the columns back to the initial size.  This caused the scrollbar to disappear leaving the view shifted and some of the control not visible.
 +The solution was to persist the vertical and horizontal scroll info in the control so that it wasn't lost when the bars vanished. ​ In the case of horizontal scrolling, the control pans to the origin if the size of the listview reduces to fit within the parent window.
 +Vertical scrolling units reflect row height and horizontal units are set by the width of the first visible column. ​ I find that this gives the control the smoothest scrolling behavior while avoiding widowed or orphaned rows. Especially when navigating the control from the keyboard.
 +
 +Horizontal scrolling:
 +
 +<code c>void Grid_OnHScroll(HWND hwnd, HWND hwndCtl, UINT code, int pos)
 +{
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 + RECT rc;
 + GetClientRect(hwnd,&​rc);​
 + int line = ListView_GetFirstVisibleColWidth(g_lpInst->​hwndList,​ &rc);
 + int page = line * ((int)(rc.right-rc.left) / line);
 +
 + int oldPos = g_lpInst->​hsInfo.nPos;​
 +
 + switch (code)
 + {
 + case SB_BOTTOM:​ //​Scrolls to the lower right.
 + g_lpInst->​hsInfo.nPos = g_lpInst->​hsInfo.nMax;​
 + break;
 +
 + case SB_TOP:​ //​Scrolls to the lower left.
 + g_lpInst->​hsInfo.nPos = g_lpInst->​hsInfo.nMin;​
 + break;
 +
 + case SB_ENDSCROLL:​ //​Ends scroll.
 + break;
 +
 + case SB_LINELEFT:​ //​Scrolls left by one unit.
 + g_lpInst->​hsInfo.nPos -= line;
 + break;
 +
 + case SB_LINERIGHT:​ //​Scrolls right by one unit.
 + g_lpInst->​hsInfo.nPos += line;
 + break;
 +
 + case SB_PAGELEFT:​ //​Scrolls left by the width of the window.
 + g_lpInst->​hsInfo.nPos -= page;
 + break;
 +
 + case SB_PAGERIGHT:​ //​Scrolls right by the width of the window.
 + g_lpInst->​hsInfo.nPos += page;
 + break;
 +
 + case SB_THUMBPOSITION:​ //​Scrolls to the absolute position. The current position is specified by the Pos parameter.
 + g_lpInst->​hsInfo.nPos = pos;
 + break;
 +
 + case SB_THUMBTRACK:​ //​Drags scroll box to the specified position. The current position is specified by the Pos parameter.
 + g_lpInst->​hsInfo.nPos = pos;
 + break;​
 + }
 + // Ensure that we do not scroll beyond visible boundaries
 + g_lpInst->​hsInfo.nPos = g_lpInst->​hsInfo.nPos < g_lpInst->​hsInfo.nMin ||
 + (code == SB_LINELEFT && g_lpInst->​hsInfo.nPos < line) ?
 + g_lpInst->​hsInfo.nMin : g_lpInst->​hsInfo.nPos;​
 + g_lpInst->​hsInfo.nPos = g_lpInst->​hsInfo.nPos > g_lpInst->​hsInfo.nMax - g_lpInst->​hsInfo.nPage ?
 + g_lpInst->​hsInfo.nMax - g_lpInst->​hsInfo.nPage : g_lpInst->​hsInfo.nPos;​
 +
 + g_lpInst->​hsInfo.fMask = SIF_POS;
 + SetScrollInfo(hwnd,​SB_HORZ,&​g_lpInst->​hsInfo,​TRUE);​
 + Scroll(hwnd,​-(g_lpInst->​hsInfo.nPos-oldPos),​0);​
 +}
 +</​code>​
 +
 +Vertical scrolling:
 +
 +<code c>static VOID Grid_OnVScroll(HWND hwnd, HWND hwndCtl, UINT code, INT pos)
 +{
 + Grid_GetInstanceData(hwnd,&​g_lpInst);​
 + RECT rc;
 + GetClientRect(hwnd,&​rc);​
 + int line = ListView_GetRowHeight(g_lpInst->​hwndList,​ &rc);
 + int page = line * ((int)(rc.bottom-rc.top) / line);
 +
 + pos = line * (pos / line);//​ScrollPos in increments of line
 +
 + int oldPos = g_lpInst->​vsInfo.nPos;​
 +
 + switch (code)
 + {
 + case SB_BOTTOM:​ //​Scrolls to the lower right.
 + g_lpInst->​vsInfo.nPos = g_lpInst->​vsInfo.nMax;​
 + break;
 +
 + case SB_TOP:​ //​Scrolls to the upper left.
 + g_lpInst->​vsInfo.nPos = g_lpInst->​vsInfo.nMin;​
 + break;
 +
 + case SB_ENDSCROLL:​ //​Ends scroll.
 + break;
 +
 + case SB_LINEDOWN:​ //​Scrolls one line down.
 + g_lpInst->​vsInfo.nPos += line;
 + break;
 +
 + case SB_LINEUP:​ //​Scrolls one line up.
 + g_lpInst->​vsInfo.nPos -= line;
 + break;
 +
 + case SB_PAGEDOWN:​ //​Scrolls one page down.
 + g_lpInst->​vsInfo.nPos += page;
 + break;
 +
 + case SB_PAGEUP:​ //​Scrolls one page up.
 + g_lpInst->​vsInfo.nPos -= page;
 + break;
 +
 + case SB_THUMBPOSITION:​ //​Scrolls to the absolute position. The current position is specified by the nPos parameter.
 + g_lpInst->​vsInfo.nPos = pos;
 + break;
 +
 + case SB_THUMBTRACK:​ //​Drags scroll box to the specified position. The current position is specified by the nPos parameter.
 + g_lpInst->​vsInfo.nPos = pos;
 + break;​
 + }
 + g_lpInst->​vsInfo.nPos = g_lpInst->​vsInfo.nPos < g_lpInst->​vsInfo.nMin ? g_lpInst->​vsInfo.nMin : g_lpInst->​vsInfo.nPos;​
 + g_lpInst->​vsInfo.nPos = g_lpInst->​vsInfo.nPos > g_lpInst->​vsInfo.nMax - g_lpInst->​vsInfo.nPage ? g_lpInst->​vsInfo.nMax - g_lpInst->​vsInfo.nPage : g_lpInst->​vsInfo.nPos;​
 +
 + // In order to reduce flicker in the header we position the header
 + // before or after the scroll depending on the direction of scroll
 +
 + if(oldPos < pos) PinHeaderToTop(ListView_GetHeader(g_lpInst->​hwndList)); ​
 +
 + g_lpInst->​vsInfo.fMask = SIF_POS;
 + SetScrollInfo(hwnd,​SB_VERT,&​g_lpInst->​vsInfo,​TRUE);​
 + Scroll(hwnd,​0,​-(g_lpInst->​vsInfo.nPos-oldPos));​
 +
 + if(oldPos > pos)PinHeaderToTop(ListView_GetHeader(g_lpInst->​hwndList));​
 +}
 +</​code>​
 +
 +Calling the scroll events from the ListView_KeyProc so the edit box stays in view
 +
 +<code c>static LRESULT CALLBACK ListView_KeyProc (HWND hList, UINT msg, WPARAM wParam, LPARAM lParam)
 +{
 + // Note: Instance data is attached to ListView'​s parent
 + static RECT rc, rcParent;
 + static char buf[2048];
 +
 + if(WM_DESTROY==msg) // UnSubclass the ListView Control
 + {
 + SetWindowLong(hList,​GWL_WNDPROC,​(DWORD)GetProp(hList,"​Wprc"​));​
 + RemoveProp(hList,"​Wprc"​);​
 + return 0;
 + }
 + else if(WM_KEYDOWN==msg)
 + {
 + HWND hParent = GetParent(hList);​
 +
 + //
 + //  Snip
 + //
 +
 + //Adjust Scrolls
 + switch(wParam)
 + {
 + case VK_HOME:
 + SNDMSG(hParent,​WM_HSCROLL,​SB_TOP,​0);​
 + break;
 + case VK_END:
 + if(!PtInRect(&​rcParent,​*((LPPOINT)&​rc.right)))
 + SNDMSG(hParent,​WM_HSCROLL,​SB_BOTTOM,​0);​
 + break;
 + case VK_PRIOR:
 + SNDMSG(hParent,​WM_VSCROLL,​SB_TOP,​0);​
 + break;
 + case VK_NEXT:
 + if(!PtInRect(&​rcParent,​*((LPPOINT)&​rc.right)))
 + SNDMSG(hParent,​WM_VSCROLL,​SB_BOTTOM,​0);​
 + break;
 + case VK_UP:
 + {
 + RECT rcHeader;
 + GetClientRect(ListView_GetHeader(hList),&​rcHeader);​
 +
 + //Keep the edit box from overlaying the header bar
 + rcParent.top += (rcHeader.bottom-rcHeader.top);​
 +
 + if(!PtInRect(&​rcParent,​*((LPPOINT)&​rc.left)))
 + SNDMSG(hParent,​WM_VSCROLL,​SB_LINEUP,​0);​
 + }
 + break;
 + case VK_DOWN:
 + rcParent.bottom -= ListView_GetRowHeight(hList,​ &​rcParent);​
 + if(!PtInRect(&​rcParent,​*((LPPOINT)&​rc.left)))
 + SNDMSG(hParent,​WM_VSCROLL,​SB_LINEDOWN,​0);​
 + break;
 + case VK_LEFT:
 + rcParent.left += ListView_GetFirstVisibleColWidth(hList,​ &​rcParent);​
 + if(!PtInRect(&​rcParent,​*((LPPOINT)&​rc.right)))
 + SNDMSG(hParent,​WM_HSCROLL,​SB_LINELEFT,​0);​
 + break;
 + case VK_RIGHT:
 + rcParent.right -= ListView_GetLastVisibleColWidth(hList,​ &​rcParent);​
 + if(!PtInRect(&​rcParent,​*((LPPOINT)&​rc.left)))
 + SNDMSG(hParent,​WM_HSCROLL,​SB_LINERIGHT,​0);​
 + break;
 + }
 + return 0;
 + }
 +</​code>​
  
data_grid.txt · Last modified: 2008/09/19 01:09 by dmac