Level Extreme platform
Subscription
Corporate profile
Products & Services
Support
Legal
Français
Articles
Search: 

Why would anyone want an automated total grid?
Dragan Nedeljkovich, September 1, 2006
Following a thread on the Universal Thread, Dragan Nedeljkovich wrote this article on how to make the totals below the grid scroll horizontally together with the grid. Sometimes, we only wish we wrote this much earlier. This article describes a way to calculate those totals, creating fields in some ...
Summary
Following a thread on the Universal Thread, Dragan Nedeljkovich wrote this article on how to make the totals below the grid scroll horizontally together with the grid. Sometimes, we only wish we wrote this much earlier. This article describes a way to calculate those totals, creating fields in some cursor to hold those totals, putting textboxes on the form to show those totals, rearranging them each time grid was changed and making sure the recalculation code was called when needed.
Description
It was UT thread #1142403 that got me started on this. The problem was how to make the totals below the grid scroll horizontally together with the grid.

The first obvious solution would be to have all the textboxes with the totals in a container in a container (yes, two levels of containers), where the inner container would be as wide as the total width of all of the grid's columns, and the outer one as wide as the visible columns are. That would require some neat positioning – the .left of the outer container would have to be equal to grid.left plus the width of the record mark and deletion mark (if any), and then the .left of the inner container would have to be the negative of the first visible textbox, i.e. negative total width of the invisible columns... altogether, too complicated. And the textboxes still need to be created, maintained and positioned relative to the column widths.

The other solution seemed to be much more simple: have another grid just below the first, with no scrollbars, no headers, with just one row which would show the totals, and which would scroll horizontally with the first grid. It should also reflect true totals of the first grid at all times, and be easy to implement. This should relieve the programmer of having to write any code that calculates totals, puts them into variables or form's properties, then refreshes a dozen textboxes. The textboxes would also vanish – so no more cumbersome repositioning each time column widths are edited.

So I wrote that – took me about three hours to get the initial version right. First I had to decide on vcx vs prg – and I decided to make it a prg, because it will be based on Custom class, which doesn't have a visual representation anyway. This class builds the total grid, and drives it, but doesn't show anywhere.

Build it

The first task of this object is to find the first grid. Nothing too complicated here – the .findgrid() method just looks for the first object with baseclass=”Grid” in its .parent, and sets its .o1Grid property to it. If it can't find one, its .init returns .f. and the object simply vanishes – no error, but no result either.

Then it builds the second grid from the first. I first wanted the second grid to be of the same class as the first, but it didn't work with some grid classes which mention columns in their inits – this grid is supposed to start with no columns at all. Another problem would be if the first grid's class is defined with any nonzero number of columns, for the same reason. So it uses a simple, base class grid, and builds it column by column.

First, the recordsource of the new grid. It will be a cursor with almost the same structure as the recordsource of the first grid, the only difference being the replacement of any memo, general, blob and logical fields with character fields. There will be another cursor (aliases of both these cursors are properties of this class, so it will close them in its .destroy method) which will be used to refresh the contents of the grid. That's what Marcia and Andy call “soft select”.

Build the columns

Next step is to build the columns.To do this, there's a .ObjClone(oOriginalObj, oClonedObj) method, which recursively copies any non default property from the first grid's column, to the same property of the second grid's column. So any .inputmask, dynamic* and other properties are copied, which takes care of the alignment, formatting and other cosmetic issues. This method is maybe doing a few unnecessary things, because I stole it from my column swapper builder – for instance, it's checking for any additional objects in the target column and trying to remove them, which won't happen with a blank default column we just added.

Once the column is cloned, few properties are copied from the original grid just in case they were at their default values (and so not caught by .ObjClone method): width, inputmask, alignment. Controlsource is set to the corresponding field in the cursor.

While building the columns, it also builds a script. The script will refresh the contents of the total grid, by doing a select from first grid's .recordsource into the second cursor. Any numeric fields are selected as sum(), dates as max() and other fields as a single space. This select creates one record, which is Scatter Name -d, and Gather Name -d into the first cursor. The second cursor is then closed.

At the end of building of a column, two of column events are bound to the .ColMoved() method: moved() and resize(). Since aEvents() retrieves only the object reference, and generally the grid's column doesn't know its own index, a property nIndex is added to the top grid's column.When this delegate code is triggered, this property is retrieved and used to find the corresponding column in the lower grid. The lower grid's column's .width and .columnorder are then set to be equal to the top one's.

Put it in its place and bind it

After all the columns are built, the total grid is positioned and sized: its .top is two pixels below the first grid, and its .left, .deletemark and .recordmark are copied over. HeaderHeight is set to zero, .height to .rowheight (i.e. it is a single row grid), it's readonly, no scrollbars, no tabstop.‚The width of the total grid is special – it is set to the upper grid's width, minus the width of the vertical scrollbar, which is retrieved from sysmetric(5), if the upper grid has this scrollbar.

Finally, two BindEvent()s happen: the first grid's .AfterRowColChange() is bound to .Refresh2Grd() method, and .Scrolled() event is bound to .Scroll2Grd() method. The Refresh2Grd() runs the script first, and refreshes the total grid. It then compares the .leftColumn property between the grids, and scrolls the lower grid horizontally to whichever direction it needs to. The end result is that the columns are aligned. In the first versions of the code, this sometimes got into an endless loop, because scrolling the lower grid to the right didn't change its .LeftColumn property. How could this happen? Simply, there was enough room to display the last column as it were. You can't scroll a VFP grid more to the right if there are no invisible or partially invisible columns to the right. And it so happened that the last column was partly covered with the vertical scrollbar in the upper grid, but as the lower grid had no scrollbars, it was fully visible there and it wouldn't scroll, but rather got into an endless loop while trying.

The Scroll2Grd() just echoes the scroll of the upper grid, if it was a horizontal scroll. Any such scroll of the upper grid will make the lower grid scroll the same.

Make it run and think about tweaks

Finally, we need to add this grid to the form. It takes only one line in upper grid's parent's .init:

this.NewObject("oTotal","totalgrid","dcTotalgrid.prg")
There also has to be some space below the upper grid (one row plus two pixels high, wide as the grid), but that can be easily achieved by removing all those pesky total textboxes.

The only tweak we may need here is to somehow handle the case when there are multiple grids with the same parent, and we want each of them to have its own total, or want a specific one to have a total. This could be done by passing the reference to such a grid as a parameter, and calling This.findgrid() only when absent.

Another possible extension would be to have different aggregate functions in the script – for example, in a grid where we have quantities and prices, sum(price) doesn't make sense, but sum(price*qty) does. Also, if we want some columns totaled and some not, we may want to have the latter skipped; we may want a count(*) in some column etc. This can be achieved by capturing the script in the debugger, and then editing it and adding something like this after the .newobject() line:

*-- TEXT BLOCK BEGIN
text to this.oTotal.cScript noshow textmerge
Select 	[ ] as sifra;
	,	COUNT(*) as naziv1;
	,	max(datum_nab) as datum_nab;
	,	[ ] as mtroska;
	,	sum(kolicina) as kolicina;
	,	sum(nabavna) as nabavna;
	,	sum(otpis) as otpis;
	,	sum(sadvr) as sadvr;
 from OSNSRED into cursor <>
select <>
Local loRec
Scatter name loRec
loRec.naziv1="Count "+TRANSFORM(loRec.naziv1)
select <>
gather name loRec
use in <>
endtext
*-- TEXT BLOCK END
In this example the naziv1 field (which holds the item name in the original table) is replaced with a count(*) aggregate function, and then manipulated into a string again before updating the total grid's cursor.

What good is this?

I only wish I wrote this much earlier. My usual data entry form, and all of view forms, had a grid where at least two columns needed totals. This meant writing some code to calculate those totals, creating fields in some cursor to hold those totals, putting textboxes on the form to show those totals, rearranging them each time grid was changed, making sure the recalculation code was called when needed - a lot of error-prone work. Had I had this class back then, I'd have replaced a few dozen lines of code (and PEM tweaks to the textboxes) with a single line in form's .init() and all my forms would have a consistent look.

Source code

Dragan Nedeljkovich, Now officially retired
After (and/or along with) playing with Sinclair ZX Spectrum and Atari ST, I left teaching in 1986 and went through a series of machines and operating systems: CP/M (Cobol, Turbo pascal, CB-80 Basic), PDP and VAX (Cobol, Basic Plus 2, scripts), DOS 3.1 through 7.0, Windows 3.1 through XP. Ran several (almost) forgotten networks - RPTI, Lantastic, WFWG, Novell 2.x, 3.x, 4.x, Windows peer-to-peer and eventually NT and just plain IP (Win/Lin mix). After a brief flirt with Clipper in 1988, discovered Fox and went through mFoxplus 2.1 (DOS), Foxplus (Xenix), FP 1.01, 1.02, 2.0, 2.6, VFP 3.0 (briefly), 5.0, 6.0 (SP 1 to 5), 7.0 SP1, 8SP1 and now 9. Maintained one framework, created another (in DOS Fox), helped create yet another one, then used a few more - bought or in-house - frameworks. Always too lazy to type the same sequence more than four times, I'd rather write a builder or some Intellisense. Along the way, promoted a dozen beginners into experienced programmers. IOW, been there, done that, the T-shirts are recycled already, and it's still interesting and fun way to live. Retired in 2019 and finally found time for those pet projects... in Dabo/Python.
More articles from this author
Dragan Nedeljkovich, January 31, 2007
Generally, datasessions serve this purpose - they create an isolated environment where our code can run and use tables, cursors etc as it pleases, without disturbing other code. Sometimes wee need a little more granularity - to have a routine which may create any number of cursors, open additional t...
Dragan Nedeljkovich, November 1, 2006
In this article, Dragan Nedeljkovich talks about how to rebuild from Class Browser's export functionality. Ever since Visual FoxPro 3, we have had the object browser. It has this nice feature to export a class or a form as code. Five versions later, it is still there. But, it still produces out code...
Dragan Nedeljkovich, November 27, 1999
As described in the help, padr('abc',20) will give a string with 17 trailing blanks - but try padc(12, 13), or padl(date(), 20) - you will get a string containing the string representation of number 12 or today's date, padded with blanks (or any other string you choose) on the side of your choice. ...
Dragan Nedeljkovich, November 5, 2000
There's a problem of the DataEnvironment which exists in the .scx but not in the .vcx, so when a form is createobject()ed, it has no DE. There are several ways to create one - either in the form designer, or using a session class, or simply opening the tables in code and avoiding the DE altogether (...
Dragan Nedeljkovich, December 2, 2002
This FAQ describes some considerations to take care of when using spaces in directory names when being used with macro substitution.