This code creates a simple spreadsheet, using the CwMatrix widget, a contributed widget created at Cogent Real-Time Systems, Inc. The program uses a dynamic library, which requires that Gamma be called from it's own directory. For example, if Gamma is in your /usr/cogent/bin/ directory, you would start this program with the following command:
$ /user/cogent/bin/phgamma matrix.g
There are only three spreadsheet functions implemented in this example, though more could be easily added. They are:
sum (range) produces the sum of a given range. The range is of the form A4:B7, and can be entered by typing =sum( followed by highlighting the range, followed by typing ) into a spreadsheet cell, or by just typing in the range.
Example: =sum(A4:B7)
db (name) reflects the value of a Cascade DataHub point. This allows the example to display data from any live process that has a driver to the Cascade DataHub.
Example: =db(valve_1)
dbout (name,range) writes a value to the Cascade DataHub. The name is the Cascade DataHub point name, and the range indicates the cell whose value will be transmitted, which must be entered as a range from the cell to itself. This command will transmit its value whenever the sheet is recomputed.
Example: =dbout(valve_1_alarm, B5:B5)
The code starts here:
#!/usr/cogent/bin/phgamma /* * This is a simple spreadsheet application written for QNX Photon(TM) with * the Gamma (TM) programming language. It is not intended to be either * complete or correct, but merely to act as a demonstration of how to use * the CwMatrix widget. * * This code is provided free of charge or license by Cogent Real-Time * Systems Inc. You are hereby given permission to use and modify this * code without restriction of any kind. */ /* * If we are running a Gamma executable that does not have the CwMatrix * widget defined, load the dynamic object libraries necessary to support * it. if (undefined_p (CwMatrix)) { if (!dyna_add_lib ("/usr/cogent/lib/photon_s.dlb")) { princ ("Could not load dynamic library 'photon_s.dlb'\n"); exit_program (1); } if (!dyna_add_lib ("/usr/cogent/lib/phwidgets.dlb")) { princ ("Could not load dynamic library 'phwidgets.dlb'\n"); exit_program (1); } } */ /* * Include the necessary utility libraries to enhance the Photon widget * set. These files are found in /usr/cogent/lib */ require_lisp ("PhotonWidgets"); require ("PopupMenu"); /* * Declare some global variables. The := syntax tells Gamma that only * the first attempt to set the variable will be honored. This is useful * if you decide to re-read the source file while the application is * running. It also allows you to set alternate values for these * variables through another program or from the command line prior to * loading this file. */ MENUHEIGHT := 34; YOFFSET := 20; XOFFSET := 20; FormulaCells := make_array(0); /* * Declare a sub-class of PtButton that has an extra resource called * sequence. This sequence indicates the column or row number for which * this button is the header. We will add DivButton widgets to dividers * for the row/col headers. */ class DivButton PtButton { sequence; } /* * Declare a callback function for PtDivider to be called when the user * resizes a row. We only need to resize the rows on either side of the * divider boundary, and then resize the last row in case the entire * divider has changed size. */ function cbDividerSetHeights (mat) { local left, right, sizes = widget.divider_sizes, n = length(sizes); left = cbinfo.divider.left; right = cbinfo.divider.right; mat.SetRowHeight (left.sequence, left.dim.h + left.border_width * 2, 0); mat.SetRowHeight (right.sequence, right.dim.h + right.border_width * 2, 0); mat.SetRowHeight (n - 1, sizes[n - 1].y - sizes[n - 1].x, 0); mat.FlushDamage (); } /* * Declare a callback function for PtDivider to be called when the user * resizes a column. */ function cbDividerSetWidths (mat) { local left, right, sizes = widget.divider_sizes, n = length(sizes); left = cbinfo.divider.left; right = cbinfo.divider.right; mat.SetColumnWidth (left.sequence, left.dim.w + left.border_width * 2, 0); mat.SetColumnWidth (right.sequence, right.dim.w + right.border_width * 2, 0); mat.SetColumnWidth (n - 1, sizes[n - 1].y - sizes[n - 1].x, 0); mat.FlushDamage (); } /* * Create the row label divider and buttons. This is only called at * startup. */ function RowLabels (mat) { local i, divider, button; divider = new (PtDivider); divider.SetPos (0,YOFFSET + MENUHEIGHT); divider.SetDim (XOFFSET,mat.dim.h); divider.group_orientation = Pt_GROUP_VERTICAL; divider.divider_flags = cons (Pt_DIVIDER_RESIZE_BOTH, nil); PtAttachCallback (divider, Pt_CB_DIVIDER_DRAG, `cbDividerSetHeights (@mat)); for (i=0; i<mat.matrix_rows; i++) { button = new (DivButton); button.sequence = i; button.text_string = string (i); button.border_width = 0; button.margin_height = 0; button.margin_width = 0; button.text_font = "helv10"; button.border_width = 2; button.SetDim (XOFFSET - 2 * button.border_width, mat.RowHeight (i) - 2 * button.border_width); } PtRealizeWidget (divider); /* Since we cannot predict the size of the final button, we must adjust the final matrix row to match the button size. */ mat.SetRowHeight (mat.matrix_rows - 1, button.dim.h + 2 * button.border_width, 1); } /* * Create the column label divider and buttons. This is only called at * startup. */ function ColumnLabels (mat) { local i, divider, button; divider = new (PtDivider); divider.SetPos (XOFFSET,MENUHEIGHT); divider.SetDim (mat.dim.w,YOFFSET); divider.group_orientation = Pt_GROUP_HORIZONTAL; divider.divider_flags = cons (Pt_DIVIDER_RESIZE_BOTH, nil); PtAttachCallback (divider, Pt_CB_DIVIDER_DRAG, `cbDividerSetWidths (@mat)); for (i=0; i<mat.matrix_cols; i++) { button = new (DivButton); button.sequence = i; button.text_string = i < 26 ? char('A' + i) : string ("A", char('A' + i % 26)); button.border_width = 0; button.margin_height = 0; button.margin_width = 0; button.text_font = "helv10"; button.border_width = 2; button.SetDim (mat.ColumnWidth (i) - 2 * button.border_width, YOFFSET - 2 * button.border_width); } PtRealizeWidget (divider); /* Since we cannot predict the size of the final button, we must adjust the final matrix column to match the button size. */ mat.SetColumnWidth (mat.matrix_cols - 1, button.dim.w + 2 * button.border_width, 1); } /* * Compute the position of a menu button. This is handy since we need * this position in order to determine where to put a pop_up menu. */ function MenuButtonPos (widget) { local pos; pos = PtGetAbsPosition (widget); pos.y += widget.dim.h + widget.border_width * 2; pos; } /* * Construct the template for the "Effects" menu, accessible through the * Effects button in the upper left corner. This template does create an * actual PtMenu until you call its Instantiate(...) member function. */ function CreateEffectMenu (mat) { local emenu, textcolormenu, fillcolormenu; emenu = new (PopupMenu); emenu.AddItem ("Border On", nil, `cbBorder(@mat,t), nil, nil); emenu.AddItem ("Border Off", nil, `cbBorder(@mat,nil), nil, nil); emenu.AddItem ("-", nil, nil, nil, nil); emenu.AddItem ("Justify Left", nil, `cbJustify(@mat,Cw_CELL_LEFT), nil, nil); emenu.AddItem ("Justify Right", nil, `cbJustify(@mat,Cw_CELL_RIGHT), nil, nil); emenu.AddItem ("Justify Center", nil, `cbJustify(@mat,Cw_CELL_CENTER), nil, nil); emenu.AddItem ("-", nil, nil, nil, nil); emenu.AddItem ("Invert", nil, `cbInvert(@mat, 1), nil, nil); emenu.AddItem ("Un-Invert", nil, `cbInvert(@mat, 0), nil, nil); emenu.AddItem ("-", nil, nil, nil, nil); fillcolormenu = new (PopupMenu); fillcolormenu.AddItem ("red", nil, `cbFillColor(@mat,0xff0000), nil, nil); fillcolormenu.AddItem ("green", nil, `cbFillColor(@mat,0x00ff00), nil, nil); fillcolormenu.AddItem ("blue", nil, `cbFillColor(@mat,0x0000ff), nil, nil); fillcolormenu.AddItem ("white", nil, `cbFillColor(@mat,0xffffff), nil, nil); fillcolormenu.AddItem ("black", nil, `cbFillColor(@mat,0x000000), nil, nil); textcolormenu = new (PopupMenu); textcolormenu.AddItem ("red", nil, `cbTextColor(@mat,0xff0000), nil, nil); textcolormenu.AddItem ("green", nil, `cbTextColor(@mat,0x00ff00), nil, nil); textcolormenu.AddItem ("blue", nil, `cbTextColor(@mat,0x0000ff), nil, nil); textcolormenu.AddItem ("white", nil, `cbTextColor(@mat,0xffffff), nil, nil); textcolormenu.AddItem ("black", nil, `cbTextColor(@mat,0x000000), nil, nil); emenu.AddItem ("Text Color", nil, textcolormenu, nil, nil); emenu.AddItem ("Fill Color", nil, fillcolormenu, nil, nil); emenu; } /* * Various functions to be called when the menu buttons are selected. */ /* Turn on/off borders around the currently selected region */ function cbBorder (mat, on) { local flags = on ? Cw_CELL_ALL_BORDERS_THICK : 0; mat.OutlineRange (mat.matrix_range, flags, Cw_CELL_ALL_BORDERS_THICK); mat.FlushDamage(); } /* Justify left/right/center in the currently selected region */ function cbJustify (mat, flag) { mat.JustifyRange (mat.matrix_range, flag); mat.FlushDamage(); } /* Invert text/background colors in the currently selected region */ function cbInvert (mat, yesno) { mat.InvertRange (mat.matrix_range, yesno); mat.FlushDamage(); } /* Set the background color in the currently selected region */ function cbFillColor (mat, color) { local row, col, range, cell; range=mat.matrix_range; for (row=range.ul.y; row<=range.lr.y; row++) { for (col=range.ul.x; col<=range.lr.x; col++) { if (cell = mat.Cell (row, col)) cell.fill_color = color; } } } /* Set the text color in the currently selected region */ function cbTextColor (mat, color) { local row, col, range, cell; range=mat.matrix_range; for (row=range.ul.y; row<=range.lr.y; row++) { for (col=range.ul.x; col<=range.lr.x; col++) { if (cell = mat.Cell (row, col)) cell.text_color = color; } } } /* * This is a callback function that is called when the user begins to * edit the contents of a cell. This gives us an opportunity to insert * a different text string from the one shown for editing purposes. In * this case, if the cell has a formula, edit the formula instead of the * visible text. */ function cbBeginEdit () { if (cbinfo.matrix.cell.formula) cbinfo.matrix.text_string = string ("=",cbinfo.matrix.cell.formula); } /* * This is a callback function that is called when the text in a cell * has changed. We check to see whether this is a formula, and if so * attempt to parse it and evaluate it. * In any case, we must re-compute the contents of the sheet in case * this edit has affected something elsewhere. */ function cbTextChange () { local answer, str, _parser_throws_error_ = t; str = cbinfo.matrix.text_string; if (str[0] == '=') { str = substr(str,1,-1); SetFormula (cbinfo.matrix.cell, str); EvaluateCell (widget, cbinfo.matrix.cell.row, cbinfo.matrix.cell.col); } else SetFormula (cbinfo.matrix.cell, nil); Recompute (widget); } /* * This is a bsearch comparison function that orders the cells in row/column * order. If we wanted to evaluate in column/row order, we would modify * this function to suit. */ function CmpCellPos (!c1, !c2) { if (c1.row < c2.row) -1; else if (c1.row == c2.row) c1.col - c2.col; else 1; } /* * Add a cell containing a formula to the global formula array. We do this * so that when we recompute the sheet, we only have to visit those cells * that are known to have a formula. Since a sheet is usually sparse, this * can save a lot of time. */ function AddFormulaCell (cell) { local found; found = bsearch (FormulaCells, cell, CmpCellPos); if (undefined_p (car(found))) insert (FormulaCells, cdr(found), cell); } /* * Remove a cell from the global formulas array. */ function RemoveFormulaCell (cell) { local found; found = bsearch (FormulaCells, cell, CmpCellPos); if (!undefined_p(car(found))) delete (FormulaCells, cdr(found)); } /* * Set the formula for a cell. By offering this single entry point for * modifying the formula, we can ensure that we know which cells to * look at when we recompute the sheet. */ function SetFormula (cell, str) { if (!str || str == "") RemoveFormulaCell (cell); else AddFormulaCell (cell); cell.formula = str; } /* * Evaluate the formula of a cell, and set the cell's visible text string * to the result. We attempt this once using the exact formula text. If * it fails, we append a semicolon to the formula and try again. If there * is an error, we do something cheesy to get rid of some of the error * message, since it could be much too long for the cell on-screen. */ function EvaluateCell (mat, row, col) { local answer, str, _parser_throws_error_ = t; if (cell = mat.ExistingCell (row, col)) { str = cell.formula; if (str && str != "") { try { answer = eval_string (str,t); } catch { try { answer = eval_string (string (str, ";"),t); } catch { answer = string_split(_last_error_, ":", 3); if (length(answer) == 4) answer = car(reverse(answer)); else answer = _last_error_; } } cell.text_string = string (answer); } } } /* * This is an exhaustive recomputation function that looks to see if a * cell has any data, and then attempts to evaluate it. It is not very * efficient, so we don't use it. It is just here for demonstration. */ function Recompute (mat) { local row, col, cell; for (row = 0; row < mat.matrix_rows; row ++) { for (col = 0; col < mat.matrix_cols; col ++) { if (cell = mat.ExistingCell (row, col)) { EvaluateCell (mat, row, col); } } } } /* * This is the real recomputation function. We keep track of which cells * contain a formula so we only have to deal with those. It cannot help * but be faster. The FormulaCells are sorted in row/col order, so we * do not need to do anything fancy to get the evaluation order correct. */ function Recompute (mat) { with cell in FormulaCells do { EvaluateCell (mat, cell.row, cell.col); } } /* * A little Gamma cheat. : (colon) is a function taking two arguments. We * override it here, and just have it generate a rectangle from its args. * Now we can express ranges as A2:B5 without breaking the Gamma syntax. */ function \: (!x,!y) { local rect = new (PhRect); SpreadsheetToPoint (string(x), rect.ul); SpreadsheetToPoint (string(y), rect.lr); rect; } /* * Convert a number to an alpha number for the column designation. */ function ConvertToAlpha (num) { num < 26 ? char('A' + num) : string ("A", char('A' + num % 26)); } /* * Convert a range rectangle into a A2:B4 style designation. */ function cbRangeConvert () { local str, rect = cbinfo.matrix.cur_range; str = string (ConvertToAlpha(rect.ul.x), rect.ul.y, ":", ConvertToAlpha(rect.lr.x), rect.lr.y); cbinfo.matrix.text_string = str; } /* * The mainline. Here we set up interprocess communication and build the * main screen. */ function main () { local w, mat, menubar, mbutton, emenu; init_ipc ("matrix","matrix"); PtInit (nil); w = new (PtWindow); w.SetDim (400, 300); w.resize_flags = cons (Pt_RESIZE_X_INITIAL | Pt_RESIZE_Y_INITIAL, -1); menubar = new (PtMenuBar); menubar.SetDim (menubar.dim.w, MENUHEIGHT - 2 * menubar.border_width); mbutton = new (PtMenuButton); mbutton.text_string = "Effects"; PtRealizeWidget (w); PtSetParentWidget (w); mat = new (CwMatrix); mat.matrix_rows = 10; mat.matrix_cols = 10; mat.SetArea (XOFFSET, YOFFSET + MENUHEIGHT, 400 - XOFFSET, 300 - (YOFFSET + MENUHEIGHT)); mat.anchor_flags = cons (Pt_LEFT_ANCHORED_LEFT | Pt_RIGHT_ANCHORED_RIGHT | Pt_TOP_ANCHORED_TOP | Pt_BOTTOM_ANCHORED_BOTTOM, -1); PtAttachCallback (mat, Cw_CB_MATRIX_BEGIN_EDIT, `cbBeginEdit()); PtAttachCallback (mat, Cw_CB_MATRIX_TEXT_CHANGE, `cbTextChange()); PtAttachCallback (mat, Cw_CB_MATRIX_RANGE_CONVERT, `cbRangeConvert()); PtRealizeWidget (mat); PtSetParentWidget (w); RowLabels(mat); PtSetParentWidget (w); ColumnLabels(mat); emenu = CreateEffectMenu(mat); PtAttachCallback (mbutton, Pt_CB_ARM, `(@emenu).Instantiate (@mbutton, MenuButtonPos(@mbutton))); PtMainLoop (); } /* ---------------------- Spreadsheet functions ---------------- */ /* * This section contains functions that are callable from a spreadsheet * cell. We need to specify these specially, as they must expect to * take a range as input. Unfortunately, this means that we cannot * simply use the functions built into Gamma. */ function SpreadsheetToPoint (str, point) { local i, row, col, colstr; for (i=0; str[i] >= 'A' && str[i] <= 'Z'; i++); colstr = substr(str,0,i); row = number (substr(str,i,-1)); if (i==1) col = str[0] - 'A'; else col = (str[0] - 'A') * 26 + str[1] - 'A'; point.x = col; point.y = row; point; } /* * Functions usable in the spreadsheet. There are global variables * cell and mat defined during these function. * * A range is a PhRect representing the upper left and lower right * corners of the range. * * In a spreadsheet cell, you would see: =sum(A5:B6) */ function sum (range) { local rect = range, row, col, cell, result=0; for (row = rect.ul.y; row <= rect.lr.y; row++) { for (col = rect.ul.x; col <= rect.lr.x; col++) { if (cell = mat.ExistingCell(row,col)) { result += number(cell.text_string); } } } result; } /* * Exception function that updates a cell when a &datahub; point * change occurs, and then recomputes the spreadsheet. */ function exCellException(cell) { cell.text_string = string (value); Recompute(mat); } /* * A spreadsheet function that causes a cell to be attached to a * &datahub; point. * In the cell you would see: =db(tank_level) * Notice that the point name is unevaluated, so you do not need to * use the # or ` syntax to protect the point name. */ function db (!name) { local sym = symbol(name); if (!getprop (sym,#registered)) { setprop (sym, #registered, t); register_exception (sym, `exCellException(@cell)); } eval(sym); } /* * A spreadsheet function that writes a value to the &datahub;. * This function transmits a value from the top-left corner of the given * range to the named point every time it is evaluated. This will not * transmit the value of the current cell, since this would be a bit of * a chicken-and-egg problem. The result in the cell will be the point * name on success, or an error message on failure. */ function dbout (!name,range) { local sym = symbol(name); local rect = range, outcell; if (outcell = mat.ExistingCell (rect.ul.y, rect.ul.x)) { write_point (sym, number(outcell.text_string)); name; } else "No value"; }
Copyright © 1995-2010 by Cogent Real-Time Systems, Inc. All rights reserved.