PBToolboxAI v5 ← Site

crosstab — u_pbt_crosstab #

← Component reference · Guide contents

A complete crosstab table: row / column / value areas fed from a DataStore, aggregates, filters, conditional formatting, date grouping, calculated measures, and CSV / Excel exports.

▶ See it live — Demo application, Crosstab tile: the preview, the code behind it and this page, side by side.


At a glance #

Userobjectu_pbt_crosstab
Item classn_pbt_crosstab_field (a field, through of_field) · n_pbt_crosstab_value (one value of the Values area, through of_value)
Used forGiving your users a cross-tab analysis of your data that they rearrange themselves, without writing SQL or exporting to Excel
Demo mode limit500 source rows processed — of_get_cell_value and of_get_cell_rows read those 500 rows, the same figures as on screen; CSV and Excel exports and copying (Ctrl+C) disabled — see demo mode

How it works #

You hand the component a flat data set — a DataStore, so any query already written in your application. The crosstab takes care of the rest: it derives the field list, and you distribute those fields across four areas.

AreaWhat it holdsEffect
RowsGrouping fieldsOne row level per field, collapsible
ColumnsGrouping fieldsOne column header level per field
ValuesNumeric fields and their aggregateWhat gets calculated in the cells
FiltersSelection fieldsA filter above the table, applied to everything

All the computing happens inside the component: once the data has been handed over, rearranging the table triggers no round trip to the database or to PowerBuilder.


Quick start #

// window open event
// Local variables
datastore lds
lds = create datastore
lds.dataobject = "d_sales"
lds.SetTransObject(SQLCA)
lds.Retrieve()

// 1. Hand over the data: fields are derived from the columns
uo_crosstab.of_from_datastore(/*ads*/ lds)

// 2. Distribute the fields across the areas
uo_crosstab.of_add_row_field(/*field*/ "region")
uo_crosstab.of_add_col_field(/*field*/ "year")
uo_crosstab.of_add_value_field(/*field*/ "amount", /*agg*/ uo_crosstab.AGG_SUM)

// 3. Present: amount format and grand totals
uo_crosstab.of_field(/*field*/ "amount").is_format = "$#,##0"
uo_crosstab.ib_row_grand_total = true
uo_crosstab.ib_col_grand_total = true
// ue_cell_double_clicked event of uo_crosstab: (string as_row_tuple_json, string as_col_tuple_json, integer ai_measure, double ad_value)
// The user wants the detail behind a figure: open the matching list.
of_open_detail(as_row_tuple_json, as_col_tuple_json)

Constants #

ConstantValueFor
TOTALS_BOTTOM · TOTALS_TOP"bottom" "top"is_totals_position
VALUES_COLS · VALUES_ROWS"cols" "rows"is_values_axis
THOUSANDS_LOCALE · THOUSANDS_SPACE · THOUSANDS_NONE"locale" "space" "none"is_thousands
AGG_SUM · AGG_COUNT · AGG_COUNT_ROWS · AGG_DISTINCT_COUNT"sum" "count" "countrows" "dcount"of_add_value_field · is_agg
AGG_AVG · AGG_MIN · AGG_MAX"avg" "min" "max"of_add_value_field · is_agg
VALUEFILTER_TOP · VALUEFILTER_BOTTOM"top" "bottom"of_set_value_filter
VALUEFILTER_GT · VALUEFILTER_LT · VALUEFILTER_BETWEEN"gt" "lt" "between"of_set_value_filter
LABEL_GT · LABEL_LT · LABEL_BETWEEN"gt" "lt" "between"of_set_label_filter
LABEL_CONTAINS · LABEL_BEGINS · LABEL_ENDS"contains" "begins" "ends"of_set_label_filter
LABEL_BEFORE · LABEL_AFTER"before" "after"of_set_label_filter
DATE_YEAR · DATE_QUARTER · DATE_MONTH"year" "quarter" "month"of_group_date_field

The aggregate, filter, date and separator constants are read on the component: uo_crosstab.AGG_SUM. Those of a field or a value (SHOW_*, CF_*) are read on the class of its handle: n_pbt_crosstab_field.CF_SCALE, n_pbt_crosstab_value.SHOW_PCT_GRAND. AGG_COUNT counts the non-empty values, like Excel's Count; AGG_COUNT_ROWS counts the rows, empty ones included.


Properties #

PropertyTypeDefaultPurpose
is_totals_positionstring"bottom"Where the grand total row sits: TOTALS_BOTTOM (at the foot, the default) or TOTALS_TOP (at the top, right below the headers). Only the grand total moves: the subtotal of a group stays on the group's own line
is_values_axisstring"cols"How measures are laid out when there is more than one: VALUES_COLS (side by side in columns, the default) or VALUES_ROWS (stacked in rows)
is_currency_symbolstring""The currency the Number format menu of a value chip offers, next to No symbol and %. Empty = the display language's ($ in English, € elsewhere)
is_thousandsstring"locale"Thousands separator of the values that have no format of their own (is_format empty and no Format on the DataWindow column) — the same setting as the Options menu of the grid: THOUSANDS_LOCALE (the display language's, the default), THOUSANDS_SPACE, THOUSANDS_NONE, or the separator itself (",", ".", " ")
ib_field_listbooleantrueShows the field panel, where the user rearranges the table with the mouse
ib_row_subtotalsbooleantrueShows the subtotal of each row group, written on the line of the group itself, above its members; off, that line keeps its label and shows no figure
ib_col_subtotalsbooleantrueShows a subtotal for each column group
ib_row_grand_totalbooleantrueShows the grand-total row under the table (the twin of ib_col_grand_total)
ib_col_grand_totalbooleantrueShows the grand-total column after the table (the twin of ib_row_grand_total). With no field in Columns there is no total column — the only column of values IS the total — and the property keeps its value for when a field enters. With several measures, the Total column has a "Total" header then one header per measure, sortable
ib_copy_headersbooleantrueCtrl+C copies the headers with the selected cells: the column names on a first line (the measure is named there only when the table shows it: several values, or no field in Columns) and the member of each row — its whole path, "Nord / Lille" — in a first column, whose corner carries the names of the row fields ("region / ville"). A paste in a spreadsheet thus says what the figures are. false copies the figures alone. Read live, set back to true by of_reset
ib_enabledbooleantrueGreyed out: the grid still shows its figures — an empty crosstab is a different thing from one the application has switched off — but it stops answering the pointer
is_theme_stylestring""Visual style of the component (THEME_STYLE_* constants); empty = the application's, followed at every change
is_theme_modestring""Light or dark variant (THEME_MODE_* constants); empty = the application's, followed at every change
il_theme_accentlong-1Accent colour of this component (-1 = the application accent, or the theme's)
is_tooltipstring""Simple tooltip shown when hovering the component
is_super_tooltip_titlestring""Title of the rich tooltip (takes precedence over is_tooltip)
is_super_tooltip_textstring""Text of the rich tooltip (rich markup accepted)
is_super_tooltip_imagestring""Image of the rich tooltip

Field properties #

of_field (string as_field) returns the handle of a field: you fetch it once, then you drive the field through its properties. The handle is created on the first call and reused afterwards.

PropertyTypeDefaultPurpose
is_labelstringDataWindow headerReadable caption for the field ("amount" → "Revenue"). By default the header text of the column in the DataWindow, else its name; an empty string gives it back. Set by your code, it survives of_from_datastore
is_showstring"normal"What the cell displays: "normal", "pctGrand" (% of the grand total), "pctRow" (% of the row), "pctCol" (% of the column), "running" (running total), "diff" (difference from the previous one)
is_conditional_formattingstring"none"Conditional formatting: CF_NONE, CF_SCALE (color scale), CF_BARS (bars inside the cell) or CF_ICONS (an arrow by third: up, level, down)
is_formatstringcolumn FormatDisplay format of the values of the field, a PowerBuilder display format ("#,##0.00", "$#,##0;($#,##0)", "0.0%"), written with the separators of the display language. Empty = the Format of the DataWindow column, read by of_from_datastore; with neither, is_thousands and at most two decimals. Read back: the format in force, the one set else the column's

The values of is_show and is_conditional_formatting are also available as constants on the handle (SHOW_PCT_COL, CF_SCALE…). Set on the field, is_show, is_conditional_formatting and is_format reach every value of the field in the Values area and the ones that enter it later — set before the field is in Values, they are kept, read back as set and applied when it enters. For one value alone (the same field twice: the amount, and its share of the total), use the value handle, below.

// Local variables
n_pbt_crosstab_field lnv_field

// The amount field : its caption and a colour scale
lnv_field = uo_crosstab.of_field(/*field*/ "amount")
lnv_field.is_label = "Revenue"
lnv_field.is_conditional_formatting = lnv_field.CF_SCALE

⚠️ Breaking change. This property used to be called is_cf: the abbreviation said nothing at the call site. The old name no longer exists — code that uses it does not compile. The replacement is mechanical: is_cf → is_conditional_formatting, with no change in values or behaviour.

Value properties #

of_value (integer ai_position) returns the handle of one value of the Values area, by its position (1 = the first). A field can sit there twice — the amount, and its share of the grand total — and each value has its own settings, which take precedence over those of its field. The handle designates a position: after of_remove_value, of_move_value, of_remove_field, of_remove_calc_measure, of_clear_layout or of_set_layout it is released — ask of_value again. Its SHOW_* and CF_* constants are those of n_pbt_crosstab_field.

PropertyTypeDefaultPurpose
is_aggstringthe one of of_add_value_fieldAggregate of this value: an AGG_* constant of the component (uo_crosstab.AGG_AVG). A calculated measure has none to choose: the assignment is ignored
is_showstring"normal"What the cells of this value show: SHOW_NORMAL, SHOW_PCT_GRAND, SHOW_PCT_ROW, SHOW_PCT_COL, SHOW_RUNNING, SHOW_DIFF (as on the field)
is_conditional_formattingstring"none"Conditional formatting of this value: CF_NONE, CF_SCALE, CF_BARS, CF_ICONS
is_formatstringcolumn FormatDisplay format of this value alone, a PowerBuilder display format ("#,##0.00", "$#,##0;($#,##0)"). Empty = the Format of the DataWindow column. Read back: the format in force
ib_negative_redbooleanfalseThe negative numbers of this value in red — on screen and in the Excel export. The same setting as the "Negatives in red" entry of the chip menu
is_labelstring""Caption of this value in the headers ("Share of sales"); empty gives back the original one, the aggregate and the field caption ("Sum of Amount")
MethodPurpose
of_field ( ) → stringThe field this value aggregates ("amount"); "" when the position holds no value
// Local variables
n_pbt_crosstab_value lnv_share, lnv_amount

// The amount twice in Values : the sum, then its share of the grand total
uo_crosstab.of_add_value_field(/*field*/ "amount", /*agg*/ uo_crosstab.AGG_SUM)
uo_crosstab.of_add_value_field(/*field*/ "amount", /*agg*/ uo_crosstab.AGG_SUM)

// The second value only : its share of the grand total, under its own caption
lnv_share = uo_crosstab.of_value(/*position*/ 2)
lnv_share.is_show = n_pbt_crosstab_value.SHOW_PCT_GRAND
lnv_share.is_label = "Share of sales"

// The first value : two decimals, negative amounts in red
lnv_amount = uo_crosstab.of_value(/*position*/ 1)
lnv_amount.is_format = "#,##0.00"
lnv_amount.ib_negative_red = true

Methods #

Feeding and naming #

MethodPurpose
of_from_datastore (datastore ads)Hands over the data set: fields are derived from the DataStore columns, their captions from the header text, the default format of their values from the Format of the column. A text that holds a tab or a line break stays in its row; a CHAR value loses its padding blanks; a decimal is read with the separator of the workstation; an empty value (NULL, empty text) is one (blank) member, sorted last; the captions set by is_label survive a reload. Returns 0 once the data is loaded, -5 if the DataStore is not valid or has no column, -2 if the component is not created
of_from_datastore (datawindow adw)The same load from a DataWindow: the cross table reads its rows through a working DataStore that shares them (ShareData, checked) — what the DataWindow shows, the table crosses. Returns 0, -5 when adw is not valid, has no column or cannot share its rows (of_get_last_error says which), -2 when the component is not created
of_from_datastore (datawindowchild adwc)The same load from a DataWindowChild (a drop-down DataWindow). Same returns
of_field (string as_field)Returns the handle of a field, to caption or format it (see Field properties)
of_value (integer ai_position) → n_pbt_crosstab_valueReturns the handle of one value of the Values area, by its position (1 = the first), to set its aggregate, display, format or caption (see Value properties)
of_value_count ( ) → longHow many values the Values area holds (a field there twice counts twice); 0 when the area is empty or the component is not created

Building the table #

MethodPurpose
of_clear_layout ( )Empties all four areas: the table goes back to blank, the data stays loaded. Returns 0 once applied, -2 if the component is not created
of_add_row_field (string as_field)Adds a field to the Rows area (the call order sets the level order). A field already in Values stays there: it can be in both (a distinct count per member). Returns 0 once applied, -5 for a field the data does not hold or for a calculated measure (it goes in Values only), -2 if the component is not created
of_add_col_field (string as_field)Adds a field to the Columns area. A field already in Values stays there: it can be in both (a distinct count per member). Returns 0 once applied, -5 for a field the data does not hold or for a calculated measure (it goes in Values only), -2 if the component is not created
of_add_value_field (string as_field, string as_agg)Adds a measure to the Values area, along with its aggregate (AGG_*, empty = AGG_SUM). The field keeps its place in Rows, Columns or Filters. Returns 0 once applied, -5 for a field the data does not hold or another aggregate, -2 if the component is not created
of_add_filter_field (string as_field)Adds a field to the Filters area, above the table. Returns 0 once applied, -5 for a field the data does not hold or for a calculated measure (it goes in Values only), -2 if the component is not created
of_remove_field (string as_field)Takes a field out of the table: out of every zone it sits in (Rows, Columns, Filters, and each of its values), with its filters — like "Remove" from the field list. The rest of the layout stays as it is, and ue_layout_changed tells the new one. Returns 0 once applied, -4 when the field sits in no zone, -5 for a field the data does not hold
of_remove_value (integer ai_position)Removes one value of the Values area, by its position (the 2nd "Sum of Amount" while the 1st stays); with the last value of a field placed nowhere else go its filters. Returns 0 once applied, -5 for a position that holds no value, -2 if the component is not created
of_move_value (integer ai_from, integer ai_to)Moves a value of the Values area from one position to another (1 = the first): its settings, the filters and the sort that name it go with it. Returns 0 once applied, -5 for a position that holds no value, -2 if the component is not created

The aggregates accepted by of_add_value_field are carried by the component as constants: AGG_SUM (default), AGG_COUNT (the number of non-empty values, like Excel's Count), AGG_COUNT_ROWS (the number of rows, empty ones included), AGG_DISTINCT_COUNT (distinct value count, empty cells ignored as in Excel), AGG_AVG, AGG_MIN, AGG_MAX. A sum with no value at all is an empty cell, not 0; a "difference from the previous one" where either cell is empty is empty too. Percentages are written the way the display language writes them (57.9% in English, 57,9 % in French).

Totals and subtotals #

Totals are set through properties, described above: ib_row_grand_total and ib_col_grand_total for the grand totals, ib_row_subtotals and ib_col_subtotals for the subtotals, is_totals_position for where the grand-total row sits. The Options menu of the grid (the gear) offers the same four switches, one entry per axis: Grand total row, Grand total column, Row subtotals, Column subtotals.

Filtering #

MethodPurpose
of_set_member_filter (string as_field, string as_values_tab)Keeps only the listed values of a field. Values are separated by tabs (~t) and written the way the grid keeps them (see under the table); an empty list removes the filter, like of_set_member_order. A field placed in no zone joins the Filters area, so that the filter shows. Returns 0 once applied, -5 for a field the data does not hold, -2 if the component is not created
of_clear_member_filter (string as_field)Removes that filter. Returns 0 once applied, -5 for a field the data does not hold, -2 if the component is not created
of_set_value_filter (string as_field, string as_type, double ad_a, double ad_b, integer ai_measure_index)Filters on the total of measure number ai_measure_index (the first one = 1): VALUEFILTER_TOP or VALUEFILTER_BOTTOM (the first or last ad_a), VALUEFILTER_GT, VALUEFILTER_LT (above, under ad_a), VALUEFILTER_BETWEEN (between ad_a and ad_b). On a field placed under another one of its zone, the ranking is made inside each parent (the best city of each region); at the first level, over the whole table. The members tied with the N-th one all stay. The field must sit in Rows or Columns. Returns 0 once applied, -5 for a field the data does not hold, another type, a Top or a Bottom of less than 1 member or a position with no measure in Values, -2 if the component is not created
of_clear_value_filter (string as_field)Removes that filter. Returns 0 once applied, -5 for a field the data does not hold, -2 if the component is not created
of_set_label_filter (string as_field, string as_type, double ad_a, double ad_b)Numeric filter on the value of the field itself: LABEL_GT, LABEL_LT, LABEL_BETWEEN. A date field takes days, through the text overload. The field must sit in Rows or Columns. Returns 0 once applied, -5 for a field the data does not hold, another type or a date field, -2 if the component is not created
of_set_label_filter (string as_field, string as_type, string as_a, string as_b)Text filter on the value of the field: LABEL_CONTAINS, LABEL_BEGINS, LABEL_ENDS (as_a, case ignored; as_b is unused). On a date field: LABEL_BEFORE, LABEL_AFTER (as_a, a day written yyyy-mm-dd) or LABEL_BETWEEN (as_a and as_b, both days included). Returns 0 once applied, -5 for a field the data does not hold, a type that does not fit the field (LABEL_CONTAINS on a date, LABEL_BEFORE on a text), an empty text or a day that is not one, -2 if the component is not created
of_clear_label_filter (string as_field)Removes that filter. Returns 0 once applied, -5 for a field the data does not hold, -2 if the component is not created

The values of_set_member_filter and of_set_member_order expect are those the grid keeps, not always those it shows: on a field derived from a date, a month is written "01"…"12", a quarter "1"…"4", a year "2024"; a date member that is not grouped is written yyyy-mm-dd. The user, for his part, finds the label filter in the chip menu of every field — text, number or date.

Formatting #

The format of a value is set through a property: of_field("amount").is_format = "$#,##0.00" for every value of a field, of_value(2).is_format for one alone (see Field properties and Value properties). It is a PowerBuilder display format, read by the mask engine of the library; empty, the value takes the Format of the DataWindow column. Negatives in red: of_value(n).ib_negative_red = true. The thousands separator of the values with no format stays is_thousands, the currency of the chip menu is_currency_symbol.

MethodPurpose
of_set_member_order (string as_field, string as_values_tab)Display order forced on the values of a field (separated by ~t); an empty string restores the natural order. Returns 0 once applied, -5 for a field the data does not hold, -2 if the component is not created

Dates and calculated fields #

MethodPurpose
of_group_date_field (string as_field, string as_part)Creates a field derived from a date, datetime or timestamp column (a time alone has no date to group): DATE_YEAR, DATE_QUARTER or DATE_MONTH, named <column>__<part>. It joins the field list and is used like any other. Returns 0 once applied, -5 for a field the data does not hold, another part or a field that does not hold dates (a text, a time), -2 if the component is not created
of_add_calc_field (string as_name, string as_label, string as_formula)Calculated field, evaluated row by row ("[amount] * 0.8" = the net of each sale, then summed like any column), usable in any area. Not for a ratio of totals (an average price): that is of_add_calc_measure. Returns 0 once created, -5 for an empty name or formula, a name holding / or `, a name already taken by a column or a formula that does not compile — the reason is then in of_get_last_error, and ue_calc_field_error is raised —, -2` if the component is not created
of_remove_calc_field (string as_name)Removes a calculated field. Returns 0 once applied, -5 when no calculated field has this name, -2 if the component is not created
of_add_calc_measure (string as_name, string as_label, string as_formula)Calculated measure, evaluated cell by cell, on the totals ("[marge] / [ca]" = overall margin rate). Goes in the Values area only. Its formula names data fields only: another calculated measure is refused. Same codes as of_add_calc_field: 0 once created, -5 (the reason in of_get_last_error, ue_calc_field_error raised), -2 if the component is not created
of_remove_calc_measure (string as_name)Removes a calculated measure. Returns 0 once applied, -5 when no calculated measure has this name, -2 if the component is not created

A formula accepts the + - * / ( ) operators, numbers (12, 0.5, .5, 1e3, always with a point) and field names in square brackets; ^ is not an operator. An invalid formula raises ue_calc_field_error — nothing crashes.

Expanding, saving, exporting #

MethodPurpose
of_expand_all ( ) · of_collapse_all ( )Expands or collapses every group, rows AND columns. Returns 0 once applied, -2 if the component is not created
of_expand_to_level (integer ai_level)Expands down to a given level (1 = first level only). Returns 0 once applied, -5 below 1, -2 if the component is not created
of_get_layout ( ) → stringReturns the full state of the table — store it as-is, then replay it through of_set_layout
of_set_layout (string as_state_json)Restores a state obtained earlier (the JSON of of_get_layout or of ue_layout_changed, as is). ue_layout_changed tells the new layout once, nothing when it is the one already shown; the value handles are released. Returns 0 once applied, -5 for an empty text or one that is not JSON, -2 if the component is not created
of_get_cell_value (string as_row_tuple_json, string as_col_tuple_json, integer ai_measure, ref double ad_value)Reads the value of one cell, the one the table shows (0.8 under 80.0%, the running total under a running total). Its row and its column are the tuples ue_cell_double_clicked gives — {"region":"Nord","city":"Lille"}, an empty member null, {} (or an empty string) for the total — and ai_measure is the position of the value in the Values area, from 1. A tuple names the fields of its zone from the first one down: {"region":"Nord"} is the subtotal of Nord. The answer does not depend on the display: a collapsed branch or hidden subtotals are read all the same. Returns 0 with the value in ad_value (NULL for a cell with no value), -4 when the table has no such cell (a member, or a field, it does not show there) or cannot be read now — called from an event the grid raised while an order of this script was waiting: of_get_last_error says so, read after the event —, -5 for a malformed tuple or a position with no measure, -2 if the component is not created
of_get_cell_rows (string as_row_tuple_json, string as_col_tuple_json, ref long al_rows[]) → longThe rows of the DataStore a cell aggregates (drill-through): al_rows[] receives their row numbers and the count is returned. The cell is addressed like of_get_cell_value, without a measure — the same rows feed every value of the cell. The numbers are those the rows have now in the DataStore given to of_from_datastore, even after a sort or a filter of that DataStore; only the rows that pass every filter of the table count; in demo mode, the rows past the cap are not used, so not given; a row deleted since the load is left out. Reload the grid after a Retrieve. Returns the count (0 or more), -4 when the table has no such cell or cannot be read now (see of_get_cell_value), -5 for a malformed tuple, -2 if the component is not created
of_export_csv (string as_path)Writes a CSV file of the current view (UTF-8 with a BOM, semicolon separated); ue_csv_saved confirms. The numbers take the decimal separator of the display language (a comma in French, German, Italian, Spanish, Portuguese); a label that would read as a formula (= + - @) is written with a leading apostrophe. The twin of of_export_xlsx. Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created
of_export_xlsx (string as_path)Writes an Excel file of the table as it is shown, formatting included (negatives in red too); ue_xlsx_saved confirms — or refuses with its reason past 16,384 columns or 1,048,576 rows, the limits of an Excel sheet. Returns 0 once applied, -5 on an invalid argument, -2 if the component is not created

Shared #

MethodPurpose
of_reset ( )Returns the component to its brand-new state. Returns 0 once applied, -2 if the component is not created
of_set_redraw (boolean)Batches a burst of changes into a single render. Returns 0 once applied, -2 if the component is not created
of_save_as_png (string) · of_save_as_jpg (string)Exports the rendering as an image. Returns 0, -2 if the component is not created, -4 if the capture fails, -5 on an empty path

Events #

EventRaised when
ue_layout_changed (string as_layout_json)The layout has changed — by the user (moved a field, changed an aggregate, sorted a column, collapsed a group…) or by the code: every order that changes what of_get_layout returns (of_set_layout included) raises it once, and only once at the end of a build framed by of_set_redraw(false) … of_set_redraw(true); never when nothing changes, nor for of_reset. as_layout_json is everything of_get_layout saves
ue_cell_double_clicked (string as_row_tuple_json, string as_col_tuple_json, integer ai_measure, double ad_value)Double-click on a cell: the first two arguments describe the intersection ({"region":"Nord","city":"Lille"}, an empty member is null there, {} for the total), ai_measure the position of the double-clicked value in the Values area, from 1, and ad_value the value the cell shows — 0.8 under "80.0%", the running total under a running total — or NULL for an empty cell. The two tuples and ai_measure make the address of_get_cell_value reads back. This is the entry point for a drill-down
ue_csv_saved (string as_path, boolean ab_ok, string as_error)The CSV file has been written — or not, and as_error says why
ue_xlsx_saved (string as_path, boolean ab_ok, string as_error)The Excel file has been written — or not, and as_error says why
ue_calc_field_error (string as_field, string as_message)A calculated field or measure formula is invalid
ue_copy (string as_tsv)The user has copied a cell selection (Ctrl+C): the grid has already put the text on the clipboard, the event hands you the same text — to log it or reuse it elsewhere. as_tsv is a tab-separated text that carries the headers by default — the column names on a first line, the member of each row (its whole path, "Nord / Lille") in a first column — or the figures alone with ib_copy_headers = false. In demo mode copying is an export: refused, and the grid says so
ue_ready ( )The component has finished loading; everything sent beforehand has been replayed
ue_runtime_missing ( )The WebView2 runtime is missing: the component stays empty
ue_bg_color (long al_color)The component has computed its theme background color; the userobject has already adopted it (backcolor)

What the user can do without a single line of code #

The table is alive: that is the whole point of the component. With the field panel shown (ib_field_list = true), the user can:

Each of these actions, copying aside (it raises ue_copy), comes back in ue_layout_changed: combined with of_get_layout / of_set_layout, this lets you offer "saved views" to your users.

From the keyboard. The table is built without a mouse — chips, menus, collapsing and sorting: Tab lands on a field, a collapse triangle or a sortable header, Enter or Space triggers it. On a field it opens its menu — the one carrying Add to rows / columns / values / filters and Remove: the whole build of the table goes through it. Shift+F10 or the Menu key open that menu too, and the focus follows the field just moved, even to another zone. Selecting cells and copying them (Ctrl+C) need the mouse today.


Examples #

A complete sales report #

// Rows: region, then city within each region
uo_crosstab.of_clear_layout()
uo_crosstab.of_add_row_field(/*field*/ "region")
uo_crosstab.of_add_row_field(/*field*/ "city")

// Columns: one per year
uo_crosstab.of_add_col_field(/*field*/ "year")

// Cells: the total amount
uo_crosstab.of_add_value_field(/*field*/ "amount", /*agg*/ uo_crosstab.AGG_SUM)

// Presentation: readable amounts, subtotals and grand totals
uo_crosstab.of_field(/*field*/ "amount").is_format = "$#,##0"
uo_crosstab.ib_row_subtotals = true
uo_crosstab.ib_row_grand_total = true
uo_crosstab.ib_col_grand_total = true

// Color the cells to spot the big amounts at a glance
uo_crosstab.of_field(/*field*/ "amount").is_conditional_formatting = n_pbt_crosstab_field.CF_SCALE

Readable captions #

Your columns are often named mt_ht or cd_reg. Rename them once and for all, right after of_from_datastore.

// The data first : the fields take the names of the columns
uo_crosstab.of_from_datastore(/*ads*/ lds)

// Then a readable caption for each field
uo_crosstab.of_field(/*field*/ "region").is_label    = "Region"
uo_crosstab.of_field(/*field*/ "city").is_label     = "City"
uo_crosstab.of_field(/*field*/ "category").is_label = "Category"
uo_crosstab.of_field(/*field*/ "year").is_label     = "Year"
uo_crosstab.of_field(/*field*/ "amount").is_label   = "Revenue"
uo_crosstab.of_field(/*field*/ "quantity").is_label  = "Quantity"

Analyzing shares rather than amounts #

// Local variables
n_pbt_crosstab_field lnv_amount

// Start from empty areas, then place the fields
uo_crosstab.of_clear_layout()
uo_crosstab.of_add_row_field(/*field*/ "category")
uo_crosstab.of_add_col_field(/*field*/ "year")
uo_crosstab.of_add_value_field(/*field*/ "amount", /*agg*/ uo_crosstab.AGG_SUM)

// Keep only two categories on screen (values separated by a tab)
uo_crosstab.of_set_member_filter(/*field*/ "category", /*values_tab*/ "Informatique~tMobilier")

// The amount field, to set how it is shown
lnv_amount = uo_crosstab.of_field(/*field*/ "amount")

// Show the share of each cell in the total of its column
lnv_amount.is_show = lnv_amount.SHOW_PCT_COL

// A small bar in each cell to compare the shares at a glance
lnv_amount.is_conditional_formatting = lnv_amount.CF_BARS

A measure of your own: the average price #

A calculated measure is evaluated on the totals of each cell, not row by row: that is what makes a ratio correct.

// Start from empty areas, then place the fields
uo_crosstab.of_clear_layout()
uo_crosstab.of_add_row_field(/*field*/ "region")

// The two totals the calculation will be based on
uo_crosstab.of_add_value_field(/*field*/ "amount",  /*agg*/ uo_crosstab.AGG_SUM)
uo_crosstab.of_add_value_field(/*field*/ "quantity", /*agg*/ uo_crosstab.AGG_SUM)

// Average price = total amount divided by total quantity
uo_crosstab.of_add_calc_measure(/*name*/ "avg_price", /*label*/ "Average price", /*formula*/ "[amount] / [quantity]")
uo_crosstab.of_field(/*field*/ "avg_price").is_format = "$#,##0.00"

// Then place it among the values like any field (the aggregate does not matter: a measure is computed)
uo_crosstab.of_add_value_field(/*field*/ "avg_price", /*agg*/ uo_crosstab.AGG_SUM)
// ue_calc_field_error event of uo_crosstab: (string as_field, string as_message)
// Invalid formula: warn without breaking anything, the table stays on screen.
uo_status.of_panel(/*key*/ "main").is_text = "Formula " + as_field + ": " + as_message

Analyzing by month, quarter or year #

A date column cannot be cross-tabbed as is — every single day would get its own row. Derive the level you want first.

// Create three fields derived from the sale_date column
uo_crosstab.of_group_date_field(/*field*/ "sale_date", /*part*/ uo_crosstab.DATE_YEAR)
uo_crosstab.of_group_date_field(/*field*/ "sale_date", /*part*/ uo_crosstab.DATE_QUARTER)
uo_crosstab.of_group_date_field(/*field*/ "sale_date", /*part*/ uo_crosstab.DATE_MONTH)

// Then cross-tab them like any other field: year in columns, quarter underneath
uo_crosstab.of_add_col_field(/*field*/ "sale_date__year")
uo_crosstab.of_add_col_field(/*field*/ "sale_date__quarter")

The top ten regions #

// Keep only the 10 regions with the highest total on the first measure
uo_crosstab.of_set_value_filter(/*field*/ "region", /*type*/ uo_crosstab.VALUEFILTER_TOP, /*a*/ 10, /*b*/ 0, /*measure_index*/ 1)

On a field placed under another one, the ranking is made inside each parent: the best city of each region, not the best city of the country. Ties all stay.

// The cities under the regions
uo_crosstab.of_add_row_field(/*field*/ "region")
uo_crosstab.of_add_row_field(/*field*/ "city")

// The best city of each region, on the first measure
uo_crosstab.of_set_value_filter(/*field*/ "city", /*type*/ uo_crosstab.VALUEFILTER_TOP, /*a*/ 1, /*b*/ 0, /*measure_index*/ 1)

Exporting #

The export reproduces the current view exactly: same filters, same totals, same formatting.

// To Excel: the file is written straight to the given path
uo_crosstab.of_export_xlsx(/*path*/ "C:\temp\ventes.xlsx")
// ue_xlsx_saved event of uo_crosstab: (string as_path, boolean ab_ok, string as_error)
// inv_notif = an n_pbt_toaster declared as an instance variable of the window
if ab_ok then
    inv_notif.is_title = "Export complete"
    inv_notif.is_text  = as_path
    inv_notif.is_kind  = inv_notif.KIND_SUCCESS
else
    inv_notif.is_title = "Export failed"
    inv_notif.is_text  = as_error
    inv_notif.is_kind  = inv_notif.KIND_ERROR
end if
inv_notif.of_show()
// To CSV: a file, like for Excel; ue_csv_saved confirms
uo_crosstab.of_export_csv(/*path*/ "C:\exports\ventes.csv")

Offering saved views #

// Local variables
string ls_view

// Save the current view : of_get_layout answers straight away
ls_view = uo_crosstab.of_get_layout()

// Keep the content AS IS: it replays without any transformation.
of_save_view(is_current_view, ls_view)
// Later on: replay a saved view
uo_crosstab.of_set_layout(/*state_json*/ of_read_view("Ventes par region"))

Drilling down behind a figure #

// ue_cell_double_clicked event of uo_crosstab: (string as_row_tuple_json, string as_col_tuple_json, integer ai_measure, double ad_value)
// Local variables
long ll_rows[], ll_count, ll_i

// The rows of the DataStore behind the double-clicked figure
ll_count = uo_crosstab.of_get_cell_rows(/*row_tuple_json*/ as_row_tuple_json, /*col_tuple_json*/ as_col_tuple_json, /*rows*/ ll_rows)

// Nothing to show : no row, or a cell that cannot be read now
if ll_count <= 0 then return

// Copy them into the detail list : ids_sales is the DataStore given to of_from_datastore
dw_detail.Reset()
for ll_i = 1 to ll_count
	ids_sales.RowsCopy(ll_rows[ll_i], ll_rows[ll_i], Primary!, dw_detail, dw_detail.RowCount() + 1, Primary!)
next

The two tuples and ai_measure make the address of the cell: of_get_cell_value reads it back later, as the table shows it at that moment.

// ue_cell_double_clicked event of uo_crosstab : the address of the cell, read again later
// Local variables
double ld_value

// Read the cell again from its address : 0 when it exists
if uo_crosstab.of_get_cell_value(/*row_tuple_json*/ as_row_tuple_json, /*col_tuple_json*/ as_col_tuple_json, /*measure*/ ai_measure, /*value*/ ld_value) = 0 then

	// ld_value is what the cell shows ; NULL for an empty cell
end if

A cell can also be read without a click, through its members: a partial tuple names a subtotal. And of_remove_field takes a field out of all its zones at once, filters included.

// The sales of Nord in 2025, first measure of Values
// Local variables
double ld_north

// A partial address : the region and the year, no other member
uo_crosstab.of_get_cell_value(/*row_tuple_json*/ '{"region":"Nord"}', /*col_tuple_json*/ '{"year":"2025"}', /*measure*/ 1, /*value*/ ld_north)

// Take the year out of the table, filters included
uo_crosstab.of_remove_field(/*field*/ "year")

Best practices #

Inherited from the common base #

These members exist on every visual component — they are not specific to this one. They are detailed once, in the transverse chapters; this table only says where to read them.

MembersRoleDetailed in
of_count · of_keys_at · of_hasWalk what the component holds3.2 Items
of_resetPut the component back to zero3.6 Resetting a component: of_reset()
of_set_property · of_get_property · of_component_nameDriving a property by its name3.1 The property engine
is_accessible_name · is_accessible_descriptionThe name and description a screen reader says3.4 Optional properties and events
of_register_shortcut · of_clear_shortcutsThe component's keyboard chords3.5 Keyboard shortcuts
of_is_created · of_is_ready · of_get_last_errorWhether it was born, whether it is ready, what failed3.7 Diagnostics
of_save_as_png · of_save_as_jpgExport the rendering as an image3.8 Exporting the rendering as an image
of_set_redrawGroup changes into a single repaint3.10 Best practices
of_preload_iconsIcons shown with no delayInstant display: of_icon
of_set_translationTranslate one of the component's labels5.2 Adapting a label: of_set_translation
of_focus_webviewGive the component the focus6.4 Keyboard and focus
of_print · of_print_to_pdfPrint, or write a PDF6.9 Printing

Two helpers are not inherited: of_icon and of_escape_markup live on n_pbt_utils. Declare one — n_pbt_utils lnv_utils, nothing to create — and call them on it.


← Component reference · Guide contents