/******************************************************************************\ * This is a part of the Microsoft Source Code Samples. * Copyright (C) 1993-1997 Microsoft Corporation. * All rights reserved. * This source code is only intended as a supplement to * Microsoft Development Tools and/or WinHelp documentation. * See these sources for detailed information regarding the * Microsoft samples programs. \******************************************************************************/ /****************************** Module Header ******************************* * Module Name: TABLE.C * * Standard table class and main interface functions. * * Functions: * * gtab_init() * gtab_deltools() * gtab_sendtq() * gtab_freelinedata() * gtab_wndproc() * gtab_createtools() * gtab_deltable() * gtab_buildtable() * gtab_setsize() * gtab_newsize() * gtab_calcwidths() * gtab_alloclinedata() * gtab_invallines() * gtab_append() * * Comments: * * The table class communicates with its 'owner' window to * get the layout info and the data to display. The owner window handle * can be sent as the lParam in CreateWindow - if not, the parent window will * be used. * * After creating the window, send it a TM_NEWID message, with a 'data id' * as the lParam. This is any non-zero 32-bit value. The table will then call * back to its owner window to find out how many rows/columns, then to fetch * the name/properties of each column, and finally to get the data to display. * * Send TM_NEWID of 0 to close (or destroy the window) - wait for TQ_CLOSE * (in either case) before discarding data. Send * TM_REFRESH if data or row-count changes; send TM_NEWLAYOUT if column * properties or nr cols change etc - this is the same as sending TM_NEWID * except that no TQ_CLOSE happens on TM_NEWLAYOUT. * * TQ_SELECT is sent whenever the current selection changes. TQ_ENTER is sent * when enter or double-click occurs. * ****************************************************************************/ #include #include #include "gutils.h" #include "table.h" #include "tpriv.h" /* global tools etc */ extern HANDLE hLibInst; HANDLE hVertCurs; HANDLE hNormCurs; HPEN hpenDotted; UINT gtab_msgcode; /* function prototypes */ long FAR PASCAL gtab_wndproc(HWND, UINT, UINT, long); void gtab_createtools(void); void gtab_deltable(HWND hwnd, lpTable ptab); lpTable gtab_buildtable(HWND hwnd, DWORD id); void gtab_setsize(HWND hwnd, lpTable ptab); void gtab_newsize(HWND hwnd, lpTable ptab); void gtab_calcwidths(HWND hwnd, lpTable ptab); BOOL gtab_alloclinedata(HWND hwnd, HANDLE heap, lpTable ptab); void gtab_invallines(HWND hwnd, lpTable ptab, int start, int count); void gtab_append(HWND hwnd, lpTable ptab, int rows, DWORD id); /*************************************************************************** * Function: gtab_init * * Purpose: * * Initialise window class - called from DLL main init */ void gtab_init(void) { WNDCLASS wc; gtab_createtools(); gtab_msgcode = RegisterWindowMessage(TableMessage); wc.style = CS_GLOBALCLASS | CS_DBLCLKS; wc.lpfnWndProc = gtab_wndproc; wc.cbClsExtra = 0; wc.cbWndExtra = WLTOTAL; wc.hInstance = hLibInst; wc.hIcon = NULL; wc.hCursor = NULL; wc.hbrBackground = GetStockObject(WHITE_BRUSH); wc.lpszClassName = TableClassName; wc.lpszMenuName = NULL; RegisterClass(&wc); } /*************************************************************************** * Function: gtab_createtools * * Purpose: * * Load cursors and pens. */ void gtab_createtools(void) { hVertCurs = LoadCursor(hLibInst, "VertLine"); hNormCurs = LoadCursor(NULL, IDC_ARROW); hpenDotted = CreatePen(PS_DOT, 1, RGB(0, 0, 0)); } /*************************************************************************** * Function: gtab_deltools * * Purpose: * * Delete pen */ void gtab_deltools(void) { DeleteObject(hpenDotted); } /*************************************************************************** * Function: gtab_wndproc * * Purpose: * * Window procedure for table */ long FAR PASCAL gtab_wndproc(HWND hwnd, UINT msg, UINT wParam, long lParam) { CREATESTRUCT FAR * csp; HWND hOwner; lpTable ptab; HANDLE hHeap; PAINTSTRUCT ps; int y, y2, i; HDC hDC; lpTableSelection pselect; long oldtop; long change; switch(msg) { case WM_CREATE: /* create window. set the wnd extra bytes to * contain the owner window, a heap and a null table. * Owner window is either in lParam or the parent. * Then wait for TM_NEWID. */ csp = (CREATESTRUCT FAR *) lParam; if (csp->lpCreateParams == NULL) { hOwner = GetParent(hwnd); } else { hOwner = (HWND) (long) csp->lpCreateParams; } ptab = NULL; hHeap = gmem_init(); SetWindowLong(hwnd, WL_TABLE, (LONG) ptab); SetWindowLong(hwnd, WW_OWNER, (LONG) hOwner); SetWindowLong(hwnd, WW_HEAP, (LONG) hHeap); SetScrollRange(hwnd, SB_VERT, 0, 0, TRUE); SetScrollRange(hwnd, SB_HORZ, 0, 0, TRUE); break; case TM_NEWID: /* complete change of table. * close old table, discard memory and * build new table */ ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_sendtq(hwnd, TQ_CLOSE, ptab->hdr.id); gtab_deltable(hwnd, ptab); SetCursor(hNormCurs); SetWindowLong(hwnd, WL_TABLE, 0); } if ( (ptab = gtab_buildtable(hwnd, lParam)) != NULL) { SetWindowLong(hwnd, WL_TABLE, (long) (LPSTR) ptab); gtab_setsize(hwnd, ptab); } else { SetScrollRange(hwnd, SB_VERT, 0, 0, TRUE); SetScrollRange(hwnd, SB_HORZ, 0, 0, TRUE); } InvalidateRect(hwnd, NULL, TRUE); break; case TM_NEWLAYOUT: /* change of layout but for same id. no TQ_CLOSE, * but otherwise same as TM_NEWID */ ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_deltable(hwnd, ptab); SetCursor(hNormCurs); SetWindowLong(hwnd, WL_TABLE, 0); } if ( (ptab = gtab_buildtable(hwnd, lParam)) != NULL) { SetWindowLong(hwnd, WL_TABLE, (long) (LPSTR) ptab); gtab_setsize(hwnd, ptab); } else { SetScrollRange(hwnd, SB_VERT, 0, 0, TRUE); SetScrollRange(hwnd, SB_HORZ, 0, 0, TRUE); } InvalidateRect(hwnd, NULL, TRUE); break; case TM_REFRESH: /* data in table has changed. nrows may have * changed. ncols and col types have not changed */ ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_newsize(hwnd, ptab); } InvalidateRect(hwnd, NULL, TRUE); break; case TM_SELECT: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { pselect = (lpTableSelection) lParam; /* * we only support TM_SINGLE - so force the * selection to a single row or cell. */ gtab_select(hwnd, ptab, pselect->startrow, pselect->startcell, 1, (ptab->hdr.selectmode & TM_ROW) ? ptab->hdr.ncols : 1, TRUE); gtab_showsel_middle(hwnd, ptab); } break; case TM_PRINT: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); hHeap = (HANDLE) GetWindowLong(hwnd, WW_HEAP); if (ptab != NULL) { gtab_print(hwnd, ptab, hHeap, (lpPrintContext) lParam); return(TRUE); } case TM_TOPROW: /* return top row. if wParam is TRUE, set lParam * as the new toprow */ ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab == NULL) { return(0); } oldtop = ptab->toprow; if ((wParam) && (lParam < ptab->hdr.nrows)) { change = lParam - ptab->toprow; change -= ptab->hdr.fixedrows; gtab_dovscroll(hwnd, ptab, change); } return(oldtop); case TM_ENDROW: /* return the last visible row in the window */ ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab == NULL) { return(0); } return(ptab->nlines + ptab->toprow - 1); case TM_APPEND: /* new rows have been added to the end of the * table, but the rest of the table has no * been change. Update without forcing redraw of * everything. * lParam contains the new total nr of rows */ ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_append(hwnd, ptab, wParam, lParam); return(TRUE); } break; case WM_SIZE: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_setsize(hwnd, ptab); } break; case WM_DESTROY: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_sendtq(hwnd, TQ_CLOSE, ptab->hdr.id); gtab_deltable(hwnd, ptab); } hHeap = (HANDLE) GetWindowLong(hwnd, WW_HEAP); gmem_freeall(hHeap); break; case WM_PAINT: hDC = BeginPaint(hwnd, &ps); ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { /* separator lines between fixed rows/columns * (ie headers) and the rest - if enabled */ /* paint here first for good impression, * and again after to clean up!! */ if (ptab->hdr.vseparator) { gtab_vsep(hwnd, ptab, hDC); } if (ptab->hdr.hseparator) { gtab_hsep(hwnd, ptab, hDC); } /* paint only the rows that need painting */ for (i = 0; i < ptab->nlines; i++) { y = ptab->pdata[i].linepos.start; y2 = y + ptab->pdata[i].linepos.size; if ( (y <= ps.rcPaint.bottom) && (y2 >= ps.rcPaint.top)) { gtab_paint(hwnd, hDC, ptab, i); } } if (ptab->hdr.vseparator) { gtab_vsep(hwnd, ptab, hDC); } if (ptab->hdr.hseparator) { gtab_hsep(hwnd, ptab, hDC); } if (ptab->selvisible) { gtab_invertsel(hwnd, ptab, hDC); } } EndPaint(hwnd, &ps); break; case WM_HSCROLL: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_msg_hscroll(hwnd, ptab, GET_SCROLL_OPCODE(wParam, lParam), GET_SCROLL_POS(wParam, lParam)); } break; case WM_VSCROLL: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_msg_vscroll(hwnd, ptab, GET_SCROLL_OPCODE(wParam, lParam), GET_SCROLL_POS(wParam, lParam)); } break; case WM_MOUSEMOVE: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_move(hwnd, ptab, LOWORD(lParam), HIWORD(lParam)); } else { SetCursor(hNormCurs); } break; case WM_LBUTTONDOWN: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_press(hwnd, ptab, LOWORD(lParam), HIWORD(lParam)); } break; case WM_LBUTTONUP: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_release(hwnd, ptab, LOWORD(lParam), HIWORD(lParam)); } break; case WM_LBUTTONDBLCLK: ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { gtab_dblclick(hwnd, ptab, LOWORD(lParam), HIWORD(lParam)); } break; case WM_KEYDOWN: /* handle key presses for cursor movement about * the table, and return/space for selection. * Any key we don't handle is passed to the owner window * for him to handle. * The table window should have the focus */ ptab = (lpTable) GetWindowLong(hwnd, WL_TABLE); if (ptab != NULL) { if (gtab_key(hwnd, ptab, wParam) != 0) { /* pass key to owner since * we don't know what to do with it */ hOwner = (HANDLE) GetWindowLong(hwnd, WW_OWNER); return(SendMessage(hOwner, WM_KEYDOWN, wParam, lParam)); } else { return(0); } } break; default: return(DefWindowProc(hwnd, msg, wParam, lParam)); } return(TRUE); } /*************************************************************************** * Function: gtab_sendtq * * Purpose: * * Send a table-query message to the owner window. Returns message * value. */ long gtab_sendtq(HWND hwnd, UINT cmd, long lParam) { HWND hOwner; hOwner = (HANDLE) GetWindowLong(hwnd, WW_OWNER); return (SendMessage(hOwner, gtab_msgcode, cmd, lParam)); } /*************************************************************************** * Function: gtab_freelinedata * * Purpose: * * Free the memory allocated for the array of lines (each containing * an array of Cells, each containing an array of chars for the actual * data). Called on any occasion that would change the number of visible lines */ void gtab_freelinedata(HANDLE hHeap, lpTable ptab) { int i, j, ncols; lpCellData cd; ncols = ptab->hdr.ncols; /* for each line */ for(i = 0; i < ptab->nlines; i++) { /* for each cell */ for (j = 0; j < ncols; j++) { /* free up the actual text space */ cd = &ptab->pdata[i].pdata[j]; gmem_free(hHeap, (LPSTR) cd->ptext, cd->nchars); } /* dealloc array of CellData */ gmem_free(hHeap, (LPSTR) ptab->pdata[i].pdata, sizeof(CellData) * ncols); } /* de-alloc array of linedatas */ gmem_free(hHeap, (LPSTR) ptab->pdata, sizeof(LineData) * ptab->nlines); ptab->pdata = NULL; } /*************************************************************************** * Function: gtab_alloclinedata * * Purpose: * * Allocate and init array of linedatas (include cell array * and text for each cell) */ BOOL gtab_alloclinedata(HWND hwnd, HANDLE heap, lpTable ptab) { lpLineData pline; lpCellData cd; int i, j; ptab->pdata = (lpLineData) gmem_get(heap, sizeof(LineData) * ptab->nlines); if (ptab->pdata == NULL) { return(FALSE); } for (i = 0; i < ptab->nlines; i++) { pline = &ptab->pdata[i]; pline->linepos.size = ptab->rowheight; pline->pdata = (lpCellData) gmem_get(heap, sizeof(CellData) * ptab->hdr.ncols); if (pline->pdata == NULL) { return(FALSE); } for (j = 0; j < ptab->hdr.ncols; j++) { cd = &pline->pdata[j]; cd->props.valid = 0; cd->flags = 0; cd->nchars = ptab->pcolhdr[j].nchars; if (cd->nchars > 0) { cd->ptext = gmem_get(heap, cd->nchars); if (cd->ptext == NULL) { return(FALSE); } } } } } /*************************************************************************** * Function: gtab_deltable * * Purpose: * * Free up all table data structures. Called for new layout or new data. */ void gtab_deltable(HWND hwnd, lpTable ptab) { HANDLE hHeap; int ncols; if (ptab == NULL) { return; } hHeap = (HANDLE) GetWindowLong(hwnd, WW_HEAP); ncols = ptab->hdr.ncols; if (ptab->pcolhdr != NULL) { gmem_free(hHeap, (LPSTR) ptab->pcolhdr, sizeof(ColProps) * ncols); } if (ptab->pcellpos != NULL) { gmem_free(hHeap, (LPSTR) ptab->pcellpos, sizeof(CellPos) * ncols); } if (ptab->pdata != NULL) { gtab_freelinedata(hHeap, ptab); } gmem_free(hHeap, (LPSTR) ptab, sizeof(Table)); } /*************************************************************************** * Function: gtab_buildtable * * Purpose: * * Build up a Table struct (excluding data allocation and * anything to do with font or window size). * Return ptr to this or NULL if error */ lpTable gtab_buildtable(HWND hwnd, DWORD id) { lpTable ptab; HANDLE hHeap; int ncols, i; ColPropsList cplist; hHeap = (HANDLE) GetWindowLong(hwnd, WW_HEAP); ptab = (lpTable) gmem_get(hHeap, sizeof(Table)); if (ptab == NULL) { return(NULL); } /* get the row/column count from owner window */ ptab->hdr.id = id; ptab->hdr.props.valid = 0; ptab->hdr.sendscroll = FALSE; if (gtab_sendtq(hwnd, TQ_GETSIZE, (long) (LPSTR)&ptab->hdr) == FALSE) { return(NULL); } ncols = ptab->hdr.ncols; ptab->pcolhdr = (lpColProps) gmem_get(hHeap, sizeof(ColProps) * ncols); if (ptab->pcolhdr == NULL) { /* should prob send TQ_CLOSE at this point */ return(NULL); } /* init col properties to default */ for (i=0; i < ncols; i++) { ptab->pcolhdr[i].props.valid = 0; ptab->pcolhdr[i].nchars = 0; } /* get the column props from owner */ cplist.plist = ptab->pcolhdr; cplist.id = id; cplist.startcol = 0; cplist.ncols = ncols; gtab_sendtq(hwnd, TQ_GETCOLPROPS, (long) (LPSTR)&cplist); /* init remaining fields */ ptab->pcellpos = (lpCellPos) gmem_get(hHeap, sizeof(CellPos) * ncols); if (ptab->pcellpos == NULL) { return(NULL); } ptab->scrollscale = 1; ptab->scroll_dx = 0; ptab->toprow = 0; ptab->pdata = NULL; ptab->nlines = 0; ptab->trackmode = TRACK_NONE; /* we have to notify owner of the current selection * whenever it is changed */ ptab->select.id = id; gtab_select(hwnd, ptab, 0, 0, 0, 0, TRUE); /* calc ave height/width, cell widths and min height. * these change only when cell properties / col count changes - * ie only on rebuild-header events */ gtab_calcwidths(hwnd, ptab); return(ptab); } /*************************************************************************** * Function: gtab_setsize * * Purpose: * * Set sizes that are based on window size and scroll pos * set: * winwidth * nlines * cellpos start, clip start/end * Alloc linedata and init */ void gtab_setsize(HWND hwnd, lpTable ptab) { RECT rc; int nlines; HANDLE heap; long range, change; GetClientRect(hwnd, &rc); ptab->winwidth = rc.right - rc.left; nlines = (rc.bottom - rc.top) / ptab->rowheight; /* nlines is the number of whole lines - add one extra * for the partial line at the bottom */ nlines += 1; /* alloc space for nlines of data - if nlines has changed */ if (nlines != ptab->nlines) { heap = (HANDLE) GetWindowLong(hwnd, WW_HEAP); gtab_freelinedata(heap, ptab); ptab->nlines = nlines; if (!gtab_alloclinedata(hwnd, heap, ptab)) { ptab->nlines = 0; return; } } /* set scroll vertical range */ range = ptab->hdr.nrows - (ptab->nlines - 1); if (range < 0) { range = 0; change = -(ptab->toprow); } else if (ptab->toprow > range) { change = range - ptab->toprow; } else { change = 0; } /* the scroll range must be 16-bits for Win3 * scale until this is true */ ptab->scrollscale = 1; while (range > 32766) { ptab->scrollscale *= 16; range /= 16; } SetScrollRange(hwnd, SB_VERT, 0, (int) range, TRUE); gtab_dovscroll(hwnd, ptab, change); /* set horz scroll range */ range = ptab->rowwidth - ptab->winwidth; if (range < 0) { range = 0; change = -(ptab->scroll_dx); } else if (ptab->scroll_dx > range) { change = range - ptab->scroll_dx; } else { change = 0; } /* horz scroll range will always be < 16 bits */ SetScrollRange(hwnd, SB_HORZ, 0, (int) range, TRUE); gtab_dohscroll(hwnd, ptab, change); } /*************************************************************************** * Function: gtab_calcwidths * * Purpose: * * Set column widths/height and totals (based on column props) * - no assumption of window size (see gtab_setsize) * sets avewidth,rowheight,cellpos.size,rowwidth (total of cellpos.size) */ void gtab_calcwidths(HWND hwnd, lpTable ptab) { int i, cxtotal, cx, ave; TEXTMETRIC tm, tmcol; HDC hdc; lpProps hdrprops, cellprops; HFONT hfont; hdrprops = &ptab->hdr.props; hdc = GetDC(hwnd); if (hdrprops->valid & P_FONT) { hfont = SelectObject(hdc, hdrprops->hFont); } GetTextMetrics(hdc, &tm); if (hdrprops->valid & P_FONT) { SelectObject(hdc, hfont); } ReleaseDC(hwnd, hdc); /* get width and height of average character */ ptab->avewidth = tm.tmAveCharWidth; ptab->rowheight = tm.tmHeight + tm.tmExternalLeading; if (hdrprops->valid & P_HEIGHT) { ptab->rowheight = hdrprops->height; } /* set pixel width of each cell (and add up for row total) * based on ave width * nr chars, unless P_WIDTH set */ cxtotal = 0; for (i = 0; i < ptab->hdr.ncols; i++) { cellprops = &ptab->pcolhdr[i].props; if (cellprops->valid & P_WIDTH) { cx = cellprops->width; } else if (hdrprops->valid & P_WIDTH) { cx = hdrprops->width; } else { if (cellprops->valid & P_FONT) { hdc = GetDC(hwnd); hfont = SelectObject(hdc, cellprops->hFont); GetTextMetrics(hdc, &tmcol); SelectObject(hdc, hfont); ReleaseDC(hwnd, hdc); ave = tmcol.tmAveCharWidth; } else { ave = ptab->avewidth; } /* ave width * nchars */ cx = ptab->pcolhdr[i].nchars + 1; cx *= ave; } /* add 2 pixels for box lines */ cx += 2; ptab->pcellpos[i].size = cx; cxtotal += cx; } ptab->rowwidth = cxtotal; } /*************************************************************************** * Function: gtab_newsize * * Purpose: * * Called when row data + possible nrows changes. * other changes are ignored */ void gtab_newsize(HWND hwnd, lpTable ptab) { TableHdr hdr; /* get new row count */ hdr = ptab->hdr; gtab_sendtq(hwnd, TQ_GETSIZE, (long) (LPSTR) &hdr); if (hdr.nrows != ptab->hdr.nrows) { ptab->hdr.nrows = hdr.nrows; gtab_setsize(hwnd, ptab); } gtab_invallines(hwnd, ptab, 0, ptab->nlines); InvalidateRect(hwnd, NULL, TRUE); } void gtab_invallines(HWND hwnd, lpTable ptab, int start, int count) { int i, j; for (i = start; i < start + count; i++) { for (j = 0; j < ptab->hdr.ncols; j++) { ptab->pdata[i].pdata[j].flags = 0; } } } /*************************************************************************** * Function: gtab_append * * Purpose: * * New rows have been added to the table. Adjust the scroll range and * position, and redraw the rows if the end of the table is currently * visible. * rows = the new total row count. */ void gtab_append(HWND hwnd, lpTable ptab, int rows, DWORD id) { long range; long oldrows; int line, nupdates; RECT rc; /* change to the new id */ ptab->hdr.id = id; ptab->select.id = id; /* update the header, but remember the old nr of rows * so we know where to start updating */ oldrows = ptab->hdr.nrows; /* check that the new nr of rows is not smaller. this is * illegal at this point and should be ignored */ if (oldrows >= rows) { return; } ptab->hdr.nrows = rows; /* set the vertical scroll range */ range = rows - (ptab->nlines - 1); if (range < 0) { range = 0; } /* force the scroll range into 16-bits for win 3.1 */ ptab->scrollscale = 1; while (range > 32766) { ptab->scrollscale *= 16; range /= 16; } /* now set the scroll bar range and position */ SetScrollRange(hwnd, SB_VERT, 0, (int) range, TRUE); if (range > 0) { SetScrollPos(hwnd, SB_VERT, (int) (ptab->toprow / ptab->scrollscale), TRUE); } /* calculate which screen lines need to be updated - find what * screen line the start of the new section is at */ line = gtab_rowtoline(hwnd, ptab, oldrows); if (line == -1) { /* not visible -> no more to do */ return; } /* how many lines to update - rest of screen or nr of * new lines if less than rest of screen */ nupdates = min((ptab->nlines - line), (int)(rows - oldrows)); /* invalidate the screen line buffers to indicate data * needs to be refetch from parent window */ gtab_invallines(hwnd, ptab, line, nupdates); /* calculate the region of the screen to be repainted - * left and right are same as window. top and bottom * need to be calculated from screen line height */ GetClientRect(hwnd, &rc); rc.top += line * ptab->rowheight; rc.bottom = rc.top + (nupdates * ptab->rowheight); /* force a repaint of the updated region */ InvalidateRect(hwnd, &rc, TRUE); }