Contributed works of Danny A. del Pilar
How to build menu like old ACHOICE function?
KPI (Key Performance Indicator) Dashboard
How to build menu like old ACHOICE function?
KPI (Key Performance Indicator) Dashboard
Consider a table for customers records with two character fields : Customer ID and customer name:
Cust_ID | Cust_Name |
CC001 | Pierce Firth |
CC002 | Stellan Taylor |
CC003 | Chris Cherry |
CC004 | Amanda Baranski |
It’s known all possible and necessary operations on a table: APPEND, DELETE, SEEK and so on; by the way, for SEEK we need an index file also.
Listing this table is quite simple:
USE CUSTOMER
WHILE .NOT. EOF()
? CUST_ID, CUST_NAME
DBSKIP()
ENDDO
If our table is sufficiently small, we can find a customer record without index and SEEK :
LOCATE FOR CUST_ID = “CC003”
? CUST_ID, CUST_NAME
If we want all our data will stand in memory and we could manage it more simple and quick way, we would use an array ( with some considerations about size of table; if it is too big, this method will be problematic ) :
aCustomer := {} // Declare / define an empty array
USE CUSTOMER
WHILE .NOT. EOF()
AADD(aCustomer, { CUST_ID, CUST_NAME } )
DBSKIP()
ENDDO
Traversing this array is quite simple :
FOR nRecord := 1 TO LEN( aCustomer )
? aCustomer[ nRecord, 1 ], aCustomer[ nRecord, 2 ]
NEXT
or :
a1Record := {}
FOR EACH a1Record IN aCustomer
? a1Record[ 1 ], a1Record[ 2 ]
NEXT
And locating a specific record too:
nRecord := ASCAN( aCustomer, { | a1Record | a1Record[ 1 ] == “CC003” } )
? aCustomer[ nRecord, 1 ], aCustomer[ nRecord, 2 ]
A lot of array functions are ready to use for maintain this array : ADEL(), AADD(), AINS() etc …
Now, let’s see how we could use a hash for achieve this job :
hCustomer := { => } // Declare / define an empty hash
USE CUSTOMER
WHILE .NOT. EOF()
hCustomer[ CUST_ID ] := CUST_NAME
DBSKIP()
ENDDO
Let’s traversing :
h1Record := NIL
FOR EACH h1Record IN hCustomer
? h1Record: __ENUMKEY(),h1Record:__ENUMVALUE()
NEXT
Now, we have a bit complicate our job; a few field addition to the table :
No: | Field Name | Type | Width | Dec | Decription |
1 |
CUST_ID |
C |
5 |
0 |
Id ( Code ) |
2 |
CUST_NAME |
C |
10 |
0 |
Name |
3 |
CUST_SNAM |
C |
10 |
0 |
Surname |
4 |
CUST_FDAT |
D |
8 |
0 |
First date |
5 |
CUST_ACTV |
L |
1 |
0 |
Is active ? |
6 |
CUST_BLNCE |
N |
11 |
2 |
Balance |
While <key> part of an element of a hash may be C / D / N / L type; <xValue> part of hash too may be ANY type of data, exactly same as arrays.
So, we can make fields values other than first ( ID) elements of an array:
hCustomer := { => } // Declare / define an empty hash
USE CUSTOMER
WHILE .NOT. EOF()
a1Data:= { CUST_NAME, CUST_SNAM, CUST_FDAT, CUST_ACTV, CUST_BLNCE }
hCustomer[ CUST_ID ] := a1Data
DBSKIP()
ENDDO
Let’s traversing :
h1Record := NIL
FOR EACH h1Record IN hCustomer
a1Key := h1Record:__ENUMKEY()
a1Data := h1Record:__ENUMVALUE()
? a1Key
AEVAL( a1Data, { | x1 | QQOUT( x1 ) } )
NEXT *-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._ /* Hash vs Tables */ #define NTrim( n ) LTRIM( STR( n ) ) #define cLMarj SPACE( 3 ) PROCEDURE Main() SET DATE GERM SET CENT ON SET COLO TO "W/B" SetMode( 40, 120 ) CLS hCustomers := { => } // Declare / define an empty PRIVATE hash IF MakUseTable() Table2Hash() * Here the hash hCustomers may be altered in any way ZAP Hash2Table() ELSE ? "Couldn't make / USE table" ENDIF ? @ MAXROW(), 0 WAIT "EOF HashVsTable.prg" RETURN // HashVsTable.Main() *-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._. PROCEDURE Table2Hash() hCustomers := { => } WHILE .NOT. EOF() hCustomers[ CUST_ID ] := CUST_SNAM DBSKIP() ENDDO ListHash( hCustomers, "A hash transferred from a table (single value)" ) hCustomers := { => } // Declare / define an empty hash DBGOTOP() WHILE .NOT. EOF() hCustomers[ CUST_ID ] := { CUST_NAME, CUST_SNAM, CUST_FDAT, CUST_ACTV, CUST_BLNCE } DBSKIP() ENDDO ListHash( hCustomers, "A hash transferred from a table (multiple values)" ) RETURN // Table2Hash() *-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._. PROCEDURE Hash2Table() LOCAL h1Record,; c1Key,; a1Record,; n1Field FOR EACH h1Record IN hCustomers c1Key := h1Record:__ENUMKEY() a1Record := h1Record:__ENUMVALUE() DBAPPEND() FIELDPUT( 1, c1Key ) AEVAL( a1Record, { | x1, n1 | FIELDPUT( n1 + 1 , x1 ) } ) NEXT h1Record DBGOTOP() ? ? "Data trasferred from hash to table :" ? WHILE ! EOF() ? STR( RECN(), 5), '' FOR n1Field := 1 TO FCOUNT() ?? FIELDGET( n1Field ), '' NEXT n1Field DBSKIP() ENDDO RETURN // Hash2Table() *-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._. PROCEDURE ListHash( hHash, cComment ) LOCAL x1Pair cComment := IF( HB_ISNIL( cComment ), '', cComment ) ? ? cComment // , "-- Type :", VALTYPE( hHash ), "size:", LEN( hHash ) ? IF HB_ISHASH( hHash ) FOR EACH x1Pair IN hHash nIndex := x1Pair:__ENUMINDEX() x1Key := x1Pair:__ENUMKEY() x1Value := x1Pair:__ENUMVALUE() ? cLMarj, NTrim( nIndex ) * ?? '', VALTYPE( x1Pair ) ?? '', x1Key, "=>" * ?? '', VALTYPE( x1Key ) * ?? VALTYPE( x1Value ) IF HB_ISARRAY( x1Value ) AEVAL( x1Value, { | x1 | QQOUT( '', x1 ) } ) ELSE ?? '', x1Value ENDIF NEXT ELSE ? "Data type error; Expected hash, came", VALTYPE( hHash ) ENDIF HB_ISHASH( hHash )
RETURN // ListHash() *-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._. FUNCTION MakUseTable() // Make / USE table LOCAL cTablName := "CUSTOMER.DBF" LOCAL lRetval, aStru, aData, a1Record IF FILE( cTablName ) USE (cTablName) ELSE aStru := { { "CUST_ID", "C", 5, 0 },; { "CUST_NAME", "C", 10, 0 },; { "CUST_SNAM", "C", 10, 0 },; { "CUST_FDAT", "D", 8, 0 },; { "CUST_ACTV", "L", 1, 0 },; { "CUST_BLNCE", "N", 11, 2 } } * * 5-th parameter of DBCREATE() is alias - * if not given then WA is open without alias * ^^^^^^^^^^^^^ DBCREATE( cTablName, aStru, , .F., "CUSTOMER" ) aData := { { "CC001", "Pierce", "Firth", 0d20120131, .T., 150.00 },; { "CC002", "Stellan", "Taylor", 0d20050505, .T., 0.15 },; { "CC003", "Chris", "Cherry", 0d19950302, .F., 0 },; { "CC004", "Amanda", "Baranski", 0d20011112, .T., 12345.00 } } FOR EACH a1Record IN aData CUSTOMER->(DBAPPEND()) AEVAL( a1Record, { | x1, nI1 | FIELDPUT( nI1, X1 ) } ) NEXT a1Record DBGOTOP() ENDIF lRetval := ( ALIAS() == "CUSTOMER" ) RETURN lRetval // MakUseTable() *-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._
Once upon a time ..
PCs has a 640 KB memory limit and Clipper programmers was must struggled with errors such as Memory overflow, Conventional memory exhausted, Stack overflow and so on…
This article mainly focused on this subject.
In our modern era we haven’t such problems, because we have gigabytes of memory installed in our computers. Morever OS’s offers to us immense memory management possibilities transparent to us. Like we have unlimited memory to use.
But nothing can be unlimited …
This article of Roger Donnay has very useful info about efficient ways to using memory; and especially variable handling.
EDITBOX control doesn’t have a WORD-WRAP property but has this feature via HSCROLLBAR property.
Look at the sample :
/*
* HMG – Harbour Win32 GUI library Demo
*
* Copyright 2010 Roberto Lopez
*
HMG official EDITBOX demo slightly modified to demonstrate effect of HSCROLLBAR property.
*/
#include "hmg.ch" Function Main LOCAL cTestText := STRTRAN( TestText(), CRLF, '' ) DEFINE WINDOW Form_1 ; AT 0,0 ; WIDTH 640 HEIGHT 480 ; TITLE 'HMG EditBox!! Demo' ; MAIN ON KEY ESCAPE ACTION Form_1.Release DEFINE STATUSBAR STATUSITEM 'HMG Power Ready!' END STATUSBAR @ 30,10 EDITBOX Edit_1 ; WIDTH 600 ; HEIGHT 170 ; VALUE cTestText ; TOOLTIP 'EDITBOX with HSCROLLBAR is .T.' @ 230,10 EDITBOX Edit_2 ; WIDTH 600 ; HEIGHT 170 ; VALUE cTestText ; TOOLTIP 'EDITBOX with HSCROLLBAR is .F.' ; NOHSCROLL END WINDOW Form_1.Center() Form_1.Activate() Return Nil FUNCTION TestText() RETURN "HMG 2.0 ALPHA BUILD 004 Changelog: "+; "- New: Cell ( nRow , nCol ) property for GRID control (read(write). "+; "- New: Edit routines for GRID control. Editing is now 'in-cell'. "+; "- New: 'DynamicForeColor' / 'DynamicBackColor' properties for GRID "+; "control. Codeblock array (one element per column) evaluated "+; "for each cell at any grid change. "+; "This.CellRowIndex, This.CellColIndex and This.CellValue variables are "+; "available at codeblock evaluation. "+; "Sample: "+; "bColor := { || if ( This.CellRowIndex/2 == int(This.CellRowIndex/2) , ; "+; "RGB (128,128,128) , RGB (192,192,192) ) } "+; "DYNAMICBACKCOLOR { bColor , bColor, bColor, bColor, bColor, bColor } "+; "See \hmg\samples\grid\grid_10 "+; "\hmg\samples\grid\grid_11 "+; "\hmg\samples\grid\grid_12 "
( Status Bar, Check Box )
We are continuing with Viva_HMG.hbp, Main.prg and Main.fmg.
While using a table and while navigating between its records, we need some extra info to show to user: Name of table, current record and record count in the table. So user always feels comfortable by knowing where is he / she; which table, which record?
The status bar control is convenient for this purpose and though this is a quite simple control, IDE has a builder for it: Status bar builder.
When you choose this builder ( after open the .fmg file by IDE of course ), a dialog box will open:
By using this dialog box we can define a status bar. We can prefer define status bar manually too:
DEFINE STATUSBAR FONT "Tahoma" SIZE 9 STATUSITEM "" WIDTH 300 STATUSITEM "" WIDTH 40 DATE WIDTH 90 CLOCK WIDTH 90 END STATUSBAR
After define status bar, we need assign values to its items. We don’t need assign values to DATE and CLOCK items, because these items will be updated by system (HMG) automatically.
First a little procedure :
PROCEDURE InitEdit()
EditReco.StatusBar.Item( 1 ) := cTableFNam
ReadData()
RETURN // InitEdit()
Change ON INIT event of EditReco form from ReadData() to InitEdit(.
And add this line at end of ReadData() procedure.
EditReco.StatusBar.Item( 2 ) := LTRIM( STR( RECN() ) ) + "\" + ; LTRIM( STR( LASTREC() ) )
Let’s look at the result :
Whenever active record change, Item( 2 ) of Status Bar will be updated ( 5/25 ) in above picture.
In this step, user must use “Save” button every time current record edited. Whereas “Read” process is different; whenever current record changed, values of text boxes automatically updated. What about automatic save? May be, we can do this; but user may don’t want such automation. Asking a question like “Do you want save?” every change doesn’t a good way.
The better way may be: put a control to form such “Auto save” with On / Off option.
Yes, fortunately we have such control: Check Box.
We can replace a Check Box control to EditReco form with chbAutoSave name and Auto Save caption:
Now, how we will implement Auto Save process?.
By adding a little IF clause to ACTION events of navigation buttons:
Top : (IF(EditReco. chbAutoSave.Value , SaveData(), ), DBGOTOP(), ReadData() )
Next : (IF(EditReco. chbAutoSave.Value , SaveData(), ), DBSKIP(), ReadData() )
Previous : ( IF(EditReco. chbAutoSave.Value , SaveData(), ), DBSKIP( -1 ), ReadData() )
Last : (IF(EditReco. chbAutoSave.Value , SaveData(), ), DBGOBOTTOM(), ReadData() )
To be continued …
This is a mini application developed primarily to denote some wonderful features of HMG.
Mini Agenda uses:
– A text (csv) file for data source and Grid as browse of this file
– DEFINE ACTIVEX for “About” page from a .html file
– HFCL library ( Thanks to S. Rathinagiri for GridPrint )
Features :
– File operations: New, Open, ReOpen, Close, Save, Save as, Print
– Record operations: Append, Delete, Insert
– Three way ( Natural, Ascending, Descending) sorting columns
– Preserve current item after sort
– In_place editing
– Incremental (and “soft” ) Search
– .ini File for keeping record of last used data file
Download : source, executable.
An alternate way to get numeric data is the SPINNER control. It consist of a text box with two arrows that allows to change control’s value using the mouse.
@ 10,10 SPINNER Spinner_1 ; RANGE 0,10 ; VALUE 5 ; WIDTH 100 #include "hmg.ch" Function Main DEFINE WINDOW Win_1 ; AT 0,0 ; WIDTH 400 ; HEIGHT 200 ; TITLE 'Tutor 16 Spinner Test' ; MAIN DEFINE MAIN MENU POPUP "First Popup" ITEM 'Change Spinner Value' ACTION ; Win_1.Spinner_1.Value := 8 ITEM 'Retrieve Spinner Value' ACTION ; MsgInfo ( Str(Win_1.Spinner_1.Value)) END POPUP END MENU @ 10,10 SPINNER Spinner_1 ; RANGE 0,10 ; VALUE 5 ; WIDTH 100 END WINDOW ACTIVATE WINDOW Win_1 Return
The easiest way to get dates from user is the datepicker control.
@ 10,10 DATEPICKER Date_1 #include "hmg.ch" Function Main DEFINE WINDOW Win_1 ; AT 0,0 ; WIDTH 400 ; HEIGHT 200 ; TITLE 'Tutor 12 - DatePicker Test' ; MAIN DEFINE MAIN MENU POPUP "First Popup" ITEM 'Change DatePicker Value' ACTION ; Win_1.date_1.Value := Date() ITEM 'Retrieve DatePicker Value' ACTION ; MsgInfo ( dtoc(Win_1.Date_1.Value)) END POPUP END MENU @ 10,10 DATEPICKER Date_1 END WINDOW ACTIVATE WINDOW Win_1 Return