PBToolboxAI v4 ← Site

datagrid — u_pbt_datagrid #

← Component reference · Guide contents

The modern grid of a DataStore: typed columns and rich cells, sorting, filters, grouping with subtotals, in-place editing, paging, windowed source and CSV export — the DataWindow stays the master of your data.

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


At a glance #

Userobjectu_pbt_datagrid
Item classn_pbt_datagrid_column — one column, reached with of_column
Used forPresenting a DataStore in a modern grid — sorting, filters, grouping, editing, very large volumes — without leaving your DataWindow
Demo mode limit100 rows displayed; CSV and Excel exports disabled — see demo mode

Where it fits #

The datagrid is not a DataWindow replacement: it is a presentation layer that sits on top of one. You keep your SQL, your Retrieve() calls, your Update() calls, your printing — and you gain a modern grid for the display.

It targets two blind spots of the classic DataWindow grid:

Database binding and updating remain out of scope: they belong to the DataWindow.

The DataStore of the examples #

Every example on this page — like those of the demo application — starts from the same DataStore of customer accounts, one row per account: city, rep (the sales rep), status, pipeline (a percentage), trend (seven monthly figures written "3,5,2,6,7,4,8"), revenue and rating (a score out of five). The grid is uo_grid on the window, the DataStore ids. A row loaded by of_from_datastore has its RowID in the DataStore as its key: it does not move when the DataStore sorts, filters, inserts or deletes. An event hands it back, and ids.GetRowFromRowId(Long(as_key)) is the number of the row it designates now.

A DataStore only holds plain values, and that is enough for rich cells: a number is all a gauge (RENDERER_PROGRESS) and stars (RENDERER_RATING) need, a name makes an avatar, a text "3,5,2,6" makes a mini chart (RENDERER_SPARKLINE) just as "vip, b2b" makes tags (RENDERER_TAGS); the colour of a chip (RENDERER_CHIP) is given per value with is_tones.


Quick start #

// open event of the window : the accounts, retrieved the way your application already does
ids = create datastore
ids.dataobject = "d_accounts"
ids.SetTransObject(SQLCA)
ids.Retrieve()

// 1. The grid reads the columns (typed) and every row of the DataStore
uo_grid.of_from_datastore(/*ads*/ ids)

// 2. The titles are the header texts of the DataWindow ; rename one if you like
uo_grid.of_column(/*key*/ "rep").is_title = "Account manager"

// 3. Rich cells : an avatar, a coloured status, a gauge, a mini chart, stars
uo_grid.of_column(/*key*/ "rep").is_renderer = n_pbt_datagrid_column.RENDERER_AVATAR
uo_grid.of_column(/*key*/ "status").is_renderer = n_pbt_datagrid_column.RENDERER_CHIP
uo_grid.of_column(/*key*/ "status").is_tones = "Active=" + n_pbt_datagrid_column.TONE_SUCCESS + "|At risk=" + n_pbt_datagrid_column.TONE_DANGER
uo_grid.of_column(/*key*/ "pipeline").is_renderer = n_pbt_datagrid_column.RENDERER_PROGRESS
uo_grid.of_column(/*key*/ "trend").is_renderer = n_pbt_datagrid_column.RENDERER_SPARKLINE
uo_grid.of_column(/*key*/ "rating").is_renderer = n_pbt_datagrid_column.RENDERER_RATING

// 4. A total, and the city stays in view
uo_grid.of_column(/*key*/ "revenue").is_summary = n_pbt_datagrid_column.SUMMARY_SUM
uo_grid.of_column(/*key*/ "city").is_pin = n_pbt_datagrid_column.PIN_START
// ue_row_clicked event of uo_grid : (string as_key)
// The key of a row loaded by of_from_datastore is its RowID : GetRowFromRowId gives
// the row it designates now, even after a sort or a delete in the DataStore.
wf_open_account(ids.GetRowFromRowId(Long(as_key)))

Properties #

PropertyTypeDefaultPurpose
is_selection_modestringSELECT_NONESelection mode: none, one row, several (Shift = range, Ctrl = toggle)
is_densitystringDENSITY_COMFORTABLERow height: comfortable or compact
is_quick_filterstring""Global quick filter: keeps only the rows whose text — as displayed (22/09/2026, 1,234.50) or raw — contains this value. What the user types waits for the end of the typing; hidden with a windowed source
ib_filter_rowbooleanfalseShows the filter input row under the headers (equivalent to the Filters button)
ib_context_menubooleantrueBuilt-in context menu on a row: Copy, Select all, Clear selection, Export to CSV, plus your own entries (of_add_row_menu_item). On by default. A right-click outside the selection moves it onto that row, a right-click inside keeps it. Set to false to show your own menu from ue_row_rclicked
ib_veto_editsbooleanfalseAsks before an edited value is kept: raises ue_cell_editing, which may refuse — the cell then keeps its old value
ib_write_backbooleanfalseWrites every edit into the DataStore given to of_from_datastore — SetItem on the row the key designates (its RowID, found where it is), typed by the column (dates, numbers, text, codes) — before ue_cell_edited. The Update stays yours; an emptied date or number becomes NULL, and a value the DataStore cannot keep (a row deleted from it since, a value SetItem refuses, a text that is not a time) is not kept: the cell goes back to what the DataStore holds, ue_write_back_failed says why, and ue_cell_edited is not raised. SetItem does not play the Validation rule of the column: check a value in ue_cell_editing (ib_veto_edits)
ib_detail_on_demandbooleanfalseDetail on demand: every row shows its chevron, and opening one (the chevron, or of_expand_row) raises ue_detail_needed — fill the panel there with of_fill_detail, it opens when the event returns. Nothing filled: the row stays closed. The detail of 5,000 rows is never read up front
is_group_bystring""Groups the rows by one or more columns, in order, their keys joined by | ("status|city"); every group header carries the subtotals of the columns that have a summary. "" goes back to a flat list. Read back live
ii_page_sizeinteger100Number of rows per page, once pagination has kicked in
il_page_thresholdlong50000Number of rows beyond which the grid switches to pages. 0 = always paginate, whatever the volume
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

Column properties #

Every column is an object in its own right, obtained through of_column("identifier") — the handle is created on first access and stays valid afterwards.

// A column is reached by its key ; used once, it fits on one line
uo_grid.of_column(/*key*/ "amount").is_summary = n_pbt_datagrid_column.SUMMARY_SUM
uo_grid.of_column(/*key*/ "city").is_pin = n_pbt_datagrid_column.PIN_START
PropertyTypeDefaultPurpose
is_titlestringthe keyHeader text of the column; "" shows the key again. of_from_datastore leaves the DataWindow column names: name them for the user here
is_tonesstring""Colour of a chip (RENDERER_CHIP) by its value, for the text of a DataStore: value=tone pairs joined by | ("Active=success|At risk=danger"), TONE_* tones; a value not listed stays neutral
ii_widthintegerdeclared widthWidth of the column, in pixels
ib_hiddenbooleanfalseHides or shows the column again
is_pinstringPIN_NONEPins the column to the left or to the right: it stays visible while scrolling horizontally
ib_editablebooleanfalseAllows editing: double-click, type, Enter commits, Esc cancels → ue_cell_edited. A value that does not fit the column (letters in a number) is shown as refused while it is typed, and Enter keeps the editor open. A boolean column shows a real check box: a click, Space, Enter or F2 flips it at once. Typing a character on a text or number cell opens the editor with that character; during an edit, Tab and Shift+Tab keep the value and open the next (previous) editable cell
is_summarystringSUMMARY_NONETotal at the foot of the column; SUMMARY_NONE removes it. SUMMARY_COUNT counts the cells that hold a value, as count() of a DataWindow does
is_rendererstringRENDERER_NONERich cell applied after the fact; RENDERER_NONE goes back to plain text. RENDERER_BOOL shows a check mark for a true value; RENDERER_CUSTOM reads the value as rich text markup — for a column your code composes: a value typed by a user must have its brackets escaped ([[ ]])
ii_indexintegerdeclaration orderPosition of the column (first position = 1); a column the grid does not hold reads back 0
is_filterstring""Per-column filter. The text is looked for in what the cell shows (formatted date, masked number) or in its raw value; on a numeric column a leading operator is recognized ("> 1000", "<= 50", "> 1,5" with the decimal separator of the language), on a date column too ("> 22/09/2026", in the order of the language, or ISO). An empty string removes the filter
is_formatstring""Display format, in the DataWindow's own syntax: "#,##0.00", "$#,##0;($#,##0)", "0.0%", "dd/mm/yyyy", "mmmm d, yyyy", "hh:mm", "@@@-@@@@". As in a DataWindow, the comma and the dot stand for the separators of the language. An empty string brings back the DataWindow's format; reads back the format in force

Constants #

Always use the constants rather than literal strings: the IDE completes them and a typo becomes impossible.

FamilyConstantsCarried by
Column typeTYPE_STRING, TYPE_NUMBER, TYPE_INT, TYPE_DATE, TYPE_DATETIME, TYPE_BOOLthe component (of_add_column)
Rich cellRENDERER_AVATAR, RENDERER_CHIP, RENDERER_PROGRESS, RENDERER_SPARKLINE, RENDERER_RATING, RENDERER_TAGS, RENDERER_BUTTON, RENDERER_LINK, RENDERER_BOOL, RENDERER_CUSTOMthe component and the column handle (is_renderer)
Rich cell: noneRENDERER_NONEthe column handle only (is_renderer)
Chip toneTONE_SUCCESS, TONE_WARN, TONE_DANGER, TONE_INFO, TONE_NEUTRALthe column handle (is_tones)
Selection modeSELECT_NONE, SELECT_SINGLE, SELECT_MULTIPLEthe component (is_selection_mode)
DensityDENSITY_COMFORTABLE, DENSITY_COMPACTthe component (is_density)
Footer totalSUMMARY_NONE, SUMMARY_SUM, SUMMARY_AVG, SUMMARY_MIN, SUMMARY_MAX, SUMMARY_COUNTthe column handle (is_summary)
Pinning sidePIN_NONE, PIN_START, PIN_END (logical: START = the edge where reading begins)the column handle (is_pin)

Constants are read on the object that carries them: u_pbt_datagrid.SELECT_MULTIPLE for a property of the component, n_pbt_datagrid_column.PIN_START for a column property.


Methods #

Filling the grid #

MethodPurpose
of_from_datastore (datastore ads)The DataStore bridge: reads the columns (name + PowerBuilder type, automatic typing) and transfers every row in a single call, with what the DataWindow says of each column — its header text becomes the title, its display format applies, and a code table (Values, DropDown DataWindow, CheckBox) shows its display value while the row keeps the data value. The columns come in the order the DataWindow shows them (their X), and a column hidden in the painter (Visible = 0) arrives hidden: the user shows it again from the Columns button. A computed field is not a column of the DataStore: it does not come along; a DropDown DataWindow whose child holds no rows shows the codes. Call it again after a Retrieve: the columns keep what was set on them (title, rich cell, pin, width, summary) and their place, what the DataWindow says (header text, format, code table) is read again, and each value lands under its own column, whatever order the user gave them. The key of each row is its RowID: it does not move when the DataStore sorts, filters, inserts or deletes, and ids.GetRowFromRowId(Long(as_key)) is the row it designates now. Returns 0 once applied, -5 when the DataStore is not valid or has no column, -2 when the component is not created
of_add_column (string as_key, string as_title, string as_type)Adds one column with automatic width, after the others: the columns already there keep what was set on them. To change a column afterwards, use its handle (of_column). Returns 0 once applied, -5 when the key is empty, holds / or `, or names a column the grid already has, -2` when the component is not created
of_add_column (string as_key, string as_title, string as_type, long al_width)Same, with a width in pixels (0 = automatic). Returns 0 once applied, -5 when the key is empty, holds / or `, or names a column the grid already has, -2` when the component is not created
of_add_column (string as_key, string as_title, string as_type, long al_width, string as_renderer)Same, with a rich cell (RENDERER_*). Returns 0 once applied, -5 when the key is empty, holds / or `, or names a column the grid already has, -2` when the component is not created
of_set_columns (string as_columns_json)Declares every column at once, with their fine-grained options (format, pinning, editable column…). The column handles taken before (of_column) are released: take them again. Returns 0 once applied, -5 when the text is not a JSON array, -2 when the component is not created
of_load_rows (string as_rows_json)Replaces the displayed rows. The DataStore of an earlier of_from_datastore is forgotten: ib_write_back and of_reload_row no longer reach it. Returns 0 once applied, -5 when the text is not a JSON array, -2 when the component is not created
of_append_rows (string as_rows_json)Adds rows after the ones already displayed — infinite scrolling, real-time arrivals. Returns 0 once applied, -5 when the text is not a JSON array or rows of it were left out because the grid already holds their key (the others are added), -2 when the component is not created
of_update_row (string as_row_json)Replaces one row in place: the one carrying the same _k key. Returns 0 once applied, -5 when the text is not a JSON object or the grid holds no row with its key, -2 when the component is not created
of_reload_row (long al_row)Sends one row of the DataStore given to of_from_datastore again, read now: after your code changed it. al_row is its row number today; the grid finds the line by its RowID, so a sort or a delete in between does not matter. A row the grid does not hold yet (InsertRow, anywhere) is added after the others. Returns 0 once sent, -5 when no DataStore is bound or the row does not exist, -2 when the component is not created
of_reset_update ( )Call it once your Update has succeeded: the cells edited in the grid are no longer marked (a small corner in the accent). Returns 0 once sent, -2 when the component is not created
of_remove_row (string as_key)Removes one row, by its key. For a row of a DataStore the key is its RowID: take it before ids.DeleteRow(ll_row), with String(ids.GetRowIdFromRow(ll_row)). Returns 0 once applied, -5 when the grid holds no row with this key, -2 when the component is not created
of_clear_columns ( )Clears the columns accumulated by of_add_column, before rebuilding a grid. The column handles taken before (of_column) are released: take them again. Returns 0 once applied, -2 when the component is not created

Columns #

MethodPurpose
of_column (string as_key)The handle of a column (n_pbt_datagrid_column): width, pinning, hiding, editing, total, rich cell, position, filter — see Column properties

Sorting, filtering, presentation #

MethodPurpose
of_sort (string as_col, string as_dir)Sorts on one column: "asc", "desc" or "none" (back to the loaded order). A column with a code table sorts on its data, as a DataWindow does. Several columns: of_sort("city A, revenue D"). Returns 0 once sorted, -5 when the grid has no such column or the direction is not one of the three, -2 when the component is not created
of_sort (string as_sort)Sorts on several columns, in their order: the first one decides, the next ones break its ties. as_sort is written as a DataWindow's SetSort reads it — "city A, revenue D"; asc and desc are read too, a column without a direction is ascending, and "" takes the sort away (the loaded order). Returns 0 once sorted, -5 for a column the grid does not have, another direction or a column named twice, -2 when the component is not created
of_get_layout ( )What the user arranged, as JSON: the columns in their order with width, pin and visibility, the sort (an ordered array: "sort":[{"col":"city","dir":"asc"},{"col":"revenue","dir":"desc"}]), the filters as typed, the quick search, the grouping and the filter row. To store (file, registry, table) and give back to of_set_layout. Titles, rich cells and formats are yours: they are not in it
of_set_layout (string as_layout_json)Puts back a layout read by of_get_layout or received via ue_layout_changed. A column it does not know (added since) keeps its place after the ones it orders; a column it names that no longer exists is ignored; ue_layout_changed is raised, as for any change of the layout. A sort written as a single object ("sort":{"col":"city","dir":"asc"}) is read too. Returns 0 once applied, -5 when the text is empty or is not a JSON object (a truncated file: nothing is sent), -2 when the component is not created
of_clear_filters ( )Clears the quick filter and every per-column filter. Returns 0 once applied, -2 when the component is not created

Selection, detail, export #

MethodPurpose
of_select_rows (string as_keys_json)Sets the selection from your code, as a list of keys ('["1","4"]'); an empty array deselects everything, a key the grid does not hold is left out. Like a click, it raises ue_selection_changed (nothing when the same rows stay selected). Returns 0 once applied, -5 when the text is not a JSON array, -2 when the component is not created
of_selected_keys ( )Returns the selected keys, as a JSON array ('["1","4"]'), read from the grid when called: a selection set by of_select_rows, or a row removed since, is already accounted for
of_fill_detail (string as_key, string as_markup)Detail panel of a row, in rich text. Returns 0 once applied, -5 when the grid holds no row with this key, -2 when the component is not created
of_expand_row (string as_key) · of_collapse_row (string as_key)Expands / collapses the detail panel of a row; like its chevron, it raises ue_row_expanded / ue_row_collapsed (nothing when the panel already is in that state). Returns 0 once applied, -5 when the grid holds no row with this key, -2 when the component is not created
of_expand_node (string as_key) · of_collapse_node (string as_key)Tree mode: expands / collapses a node (the child rows carry _parent). Returns 0 once applied, -5 when the grid holds no row with this key, -2 when the component is not created
of_export_csv (string as_path)Writes what the grid shows (filters, sort, visible columns) to a CSV file: UTF-8 with a BOM, semicolon-separated — the twin of the crosstab export. The DLL writes the file, ue_csv_saved confirms. Refused without a licence. Returns 0 once asked, -5 when the path is empty, -2 when the component is not created
of_export_xlsx (string as_path)Writes what the grid shows (filters, sort, visible columns) to an Excel workbook (.xlsx): numbers stay numbers, with the format of their column, the header row in bold. The DLL writes the file, ue_xlsx_saved confirms; a relative path is written in the folder the application started in. Refused without a licence and with a windowed source. Returns 0 once asked, -5 when the path is empty, -2 when the component is not created
of_add_row_menu_item (string as_key, string as_label)Adds one of your entries to the row context menu, under the built-in ones; the pick comes back in ue_row_menu_clicked with this key, an empty label shows the key. Returns 0 once added, -5 when the key is empty, holds / or ``, or is already one of your entries
of_add_row_menu_separator ( )A separator between two groups of your entries. Returns 0
of_clear_row_menu ( )Drops your entries: the row menu is back to the built-in ones alone. Returns 0 once applied, -2 when the component is not created

Very large volumes #

MethodPurpose
of_open_source (long al_total)Declares a source of al_total rows without building them: the grid asks only for the ones it has to display, through ue_rows_needed. The keys are then positions in your source; the DataStore of an earlier of_from_datastore is forgotten. The quick search and the filter row are hidden (the grid only holds the rows on screen); a sort chosen by the user comes to you through ue_sort_changed: sort your source, the grid asks for its rows again. Returns 0 once applied, -2 when the component is not created
of_supply_rows (long al_from, string as_rows_json)Answer to ue_rows_needed: the batch of rows is placed from row al_from, counted from 1 like the rows of a DataStore. Returns 0 once applied, -5 when the text is not a JSON array, -2 when the component is not created
of_clear_source ( )Leaves windowed mode and goes back to the rows loaded in memory. Returns 0 once applied, -2 when the component is not created
of_go_to_page (long al_page)Displays a given page (the first one is 1). Has no effect for as long as pagination has not kicked in. Returns 0 once applied, -2 when the component is not created
of_supply_grand_totals (string as_values_json)Sets the grand total, which you compute: '{"ca":128400000,"quantity":51230}'. An empty string removes it. Returns 0 once applied, -5 when the text is not a JSON object, -2 when the component is not created

Shared #

MethodPurpose
of_reset ( )Clears columns and rows, and returns the component to its brand-new state. Returns 0 once applied, -2 when the component is not created
of_set_redraw (boolean)Groups a burst of changes into a single rendering. Returns 0
of_save_as_png (string) · of_save_as_jpg (string)Exports the rendering as an image. Returns 0 once the image is written, -2 not created, -4 capture failed, -5 empty path

Events #

EventRaised when
ue_row_clicked (string as_key)A row is clicked
ue_row_dblclicked (string as_key, string as_col)A row is double-clicked: the gesture that opens a record. as_col is the column under the pointer. On an editable cell the double-click opens the editor and does not raise this event
ue_cell_clicked (string as_key, string as_col)A cell is clicked
ue_selection_changed (string as_keys_json)The selection changes: a click selects that row alone, Ctrl+click adds or removes one, Shift+click takes a range, and of_select_rows does the same from your code; as_keys_json is the JSON array of the selected keys ('["1","4"]'). A selection your code empties (a reload, SELECT_NONE) is said too
ue_sort_changed (string as_col, string as_dir, string as_sort)The sort changed: a header click (Shift+click adds a column to the sort), the menu of a column, or of_sort — an order from the code raises the event as a gesture does; nothing when the sort stays the same. as_col / as_dir: the column the change is about and its direction now ("none" once it left the sort); as_sort: the whole sort, the columns in their order, written as SetSort reads it ("city A, revenue D", "" for none). With a windowed source (of_open_source), sort your DataStore with it — ids.SetSort(as_sort) then ids.Sort(): the grid then asks for its rows again, in the new order
ue_action_clicked (string as_key, string as_col, string as_action)A button placed in a cell (RENDERER_BUTTON) is clicked
ue_row_expanded (string as_key)A master-detail row is expanded, by its chevron or by of_expand_row — the right moment to fill its detail on the fly
ue_row_collapsed (string as_key)A master-detail row is collapsed, by its chevron or by of_collapse_row
ue_detail_needed (string as_key)With ib_detail_on_demand: a row that holds no detail yet is being opened (its chevron, or of_expand_row). Fill it here with of_fill_detail(as_key, …): the panel opens when the event returns; nothing filled, the row stays closed
ue_filter_changed (string as_filters_json)The user typed a filter, in the quick search or in the per-column filter row (the search: once the typing stops). as_filters_json says every filter as typed and the search — {"filters":{"revenue":"> 1000"},"quick":"bos"}: the texts is_filter and is_quick_filter take back as they are
ue_cell_edited (string as_key, string as_col, string as_value)An editable cell is committed with a new value; as_value is that value as text. With ib_veto_edits, only once ue_cell_editing has allowed it. With ib_write_back, a value the DataStore refuses is not kept: the cell goes back, ue_write_back_failed says why, and this event is not raised
ue_cell_editing (string as_key, string as_col, string as_value)Before a typed value is kept, only when ib_veto_edits is true. Return false to leave the old value in the cell (ue_cell_edited is then not raised); true by default
ue_write_back_failed (string as_key, string as_col, string as_value, string as_reason)With ib_write_back: the DataStore could not keep a typed value (its row deleted since the load, a value SetItem refused, a text that is not a time). The cell went back to what the DataStore holds and ue_cell_edited was not raised; as_reason says why — tell the user, or reload the row
ue_layout_changed (string as_layout_json)The layout changed, whether the user or your code changed it: a column resized, moved, pinned or hidden, the sort, the filters, the quick search, the grouping, the filter row — of_set_layout included. Nothing is raised when nothing changes. as_layout_json is the whole layout, as of_get_layout gives it: store it, then give it back to of_set_layout
ue_rows_needed (long al_from, long al_to)Windowed mode: the grid asks for rows al_from to al_to, included, counted from 1 — the row numbers of a DataStore. Answer with of_supply_rows
ue_page_changed (long al_page, long al_pages)The displayed page changes, through the pagination buttons or through of_go_to_page. al_page is the current page (the first one is 1), al_pages the total number of pages. It only informs: with a windowed source the rows of the new page are asked by ue_rows_needed — answer that one, not both
ue_csv_saved (string as_path, boolean ab_ok, string as_error)The CSV asked by of_export_csv — or by the Export to CSV entry of the row menu, which asks the user for the file — was written, or not: ab_ok, and as_error says why
ue_xlsx_saved (string as_path, boolean ab_ok, string as_error)The workbook asked by of_export_xlsx was written — or not: ab_ok, and as_error says why. as_path is the file written, a relative path resolved
ue_copy (string as_tsv)The user pressed Ctrl+C. as_tsv holds the selected rows with their header line, or the focused cell alone when nothing is selected
ue_row_rclicked (string as_key, string as_col, long al_x, long al_y)A row was right-clicked (or the Menu key / Shift+F10 pressed on it). Raised whether the built-in menu is on or off. al_x / al_y are screen pixels, not PowerBuilder units: to drop your own menu where the user aimed, use PopMenu(PointerX(), PointerY()) on your window
ue_row_menu_clicked (string as_menu_key, string as_row_key, string as_keys_json)One of your entries (of_add_row_menu_item) was picked: as_menu_key is its key, as_row_key the row the menu was opened on; as_keys_json is the whole selection, which is what a batch action should work on
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 grid ships with its own toolbar and header menus. None of this requires any code on your side:

Every layout change comes back in ue_layout_changed: widths, order, pinned or hidden columns. Keep it, and give it back in one call to of_set_layout the next time the window opens; of_get_layout reads it at any time.

Keyboard — the grid is a single tab stop. Once reached, it is walked entirely from the keyboard:

KeyEffect
ArrowsMove the focused cell, one cell at a time
Home / EndFirst / last column of the row
Ctrl+Home / Ctrl+EndFirst / last cell of the grid
Page Up / Page DownMove by one screen, following the real height of the view
Shift + arrowsExtend the selection from the anchor (SELECT_MULTIPLE mode)
SpaceSelects or deselects the focused row, and drops the anchor on it; on an editable check box, flips it
Enter or F2Puts the cell into edit mode if its column is editable; a check box flips at once
A characterOn an editable text or number cell, opens the editor with that character, as a spreadsheet does
Tab / Shift+Tab (while editing)Keeps the value and opens the next (previous) editable cell, the next row at the end of a row
Ctrl+CCopies the selection — see ue_copy

The focused cell is outlined with the accent colour, and announced to screen readers through aria-activedescendant. The focus survives scrolling: the grid being virtualized, it is held in memory and put back after every render. It does disappear, though, if its row leaves the view through a sort or a filter.

⚠️ The browser clipboard can be refused inside a hosted WebView. That is why ue_copy hands you the text: put it there yourself with ClipBoard(as_tsv) to be sure of the result.


Examples #

Customer accounts in a few lines #

// ids : the accounts DataStore (see above). One repaint for the whole setup.
uo_grid.of_set_redraw(/*on*/ false)

// 1. The grid reads the columns (typed) and every row of the DataStore
uo_grid.of_from_datastore(/*ads*/ ids)

// 2. The titles are the header texts of the DataWindow ; rename one if you like
uo_grid.of_column(/*key*/ "rep").is_title = "Account manager"

// 3. Rich cells : an avatar, a coloured status, a gauge, a mini chart, stars
uo_grid.of_column(/*key*/ "rep").is_renderer = n_pbt_datagrid_column.RENDERER_AVATAR
uo_grid.of_column(/*key*/ "status").is_renderer = n_pbt_datagrid_column.RENDERER_CHIP
uo_grid.of_column(/*key*/ "status").is_tones = "Active=" + n_pbt_datagrid_column.TONE_SUCCESS + "|At risk=" + n_pbt_datagrid_column.TONE_DANGER
uo_grid.of_column(/*key*/ "pipeline").is_renderer = n_pbt_datagrid_column.RENDERER_PROGRESS
uo_grid.of_column(/*key*/ "trend").is_renderer = n_pbt_datagrid_column.RENDERER_SPARKLINE
uo_grid.of_column(/*key*/ "rating").is_renderer = n_pbt_datagrid_column.RENDERER_RATING

// A total, a column that stays in view, several rows selectable
uo_grid.of_column(/*key*/ "revenue").is_summary = n_pbt_datagrid_column.SUMMARY_SUM
uo_grid.of_column(/*key*/ "city").is_pin = n_pbt_datagrid_column.PIN_START
uo_grid.is_selection_mode = u_pbt_datagrid.SELECT_MULTIPLE
uo_grid.of_set_redraw(/*on*/ true)

Grouping by status, then by city #

// The subtotal every group header shows
uo_grid.of_column(/*key*/ "revenue").is_summary = n_pbt_datagrid_column.SUMMARY_SUM

// Two levels : the status, then the city inside it ; "" goes back to a flat list
uo_grid.is_group_by = "status|city"

// The rating stays in view on the right
uo_grid.of_column(/*key*/ "rating").is_pin = n_pbt_datagrid_column.PIN_END

Filtering from your code #

// The filter row under the headers : the user types in it
uo_grid.ib_filter_row = true

// Only the accounts in Paris, and only those above one million
uo_grid.is_quick_filter = "Paris"
uo_grid.of_column(/*key*/ "revenue").is_filter = "> 1000000"

Sorting on several columns #

The sort is written as a DataWindow's is: the first column decides, the next ones break the ties. The user does the same with the mouse by Shift+clicking the headers; a plain click starts again from a single column.

// City first, then the largest revenue in each city
uo_grid.of_sort(/*sort*/ "city A, revenue D")

What the grid reads in the DataWindow #

of_from_datastore takes more than the data: the header text of each column (<column>_t) becomes its title, its display format (Format, or the mask of an EditMask) applies, and a code table — the Values of an Edit, a DDLB or radio buttons, a DropDown DataWindow, a CheckBox — shows the display value (Active) while the row keeps the data value (A). Sorting and searching work on what is seen; a column with a code table is edited through a list, and the data value comes back.

// The grid reads titles, formats and code tables in the DataWindow
uo_grid.of_from_datastore(/*ads*/ ids)

// A format of your own on one column, in the DataWindow's syntax
uo_grid.of_column(/*key*/ "revenue").is_format = "$#,##0.00;($#,##0.00)"

// The rating is picked in its code table (Poor to Excellent) ; the number goes into the DataStore
uo_grid.of_column(/*key*/ "rating").ib_editable = true
uo_grid.ib_write_back = true

// Your code changed a row of the DataStore : show it again
ids.SetItem(12, "rating", 5)
uo_grid.of_reload_row(/*row*/ 12)

Editing in place, saving into the DataStore #

The grid edits, the DataStore keeps the truth: with ib_write_back, every edit is written into it on the row the key designates (its RowID, found where it is), typed by the column, before ue_cell_edited — and the gauge follows. An edited cell keeps a small corner in the accent until of_reset_update.

// The pipelines can be edited : double-click, type a percentage, Enter
uo_grid.of_column(/*key*/ "pipeline").ib_editable = true

// Every edit goes into the DataStore itself, on the row the key designates
uo_grid.ib_write_back = true

A boolean column made editable (TYPE_BOOL, or a CheckBox column of a DataWindow read by of_from_datastore) shows a real check box in every cell: a click, Space, Enter or F2 flips it at once — no text editor, and a double-click does not flip it twice. The cell receives the value of the other state — true/false, or the ON/OFF values of the DataWindow's CheckBox — along the same path as a typed edit: the ib_veto_edits / ue_cell_editing veto, then ue_cell_edited, the "modified" mark and ib_write_back. Read-only, a boolean column keeps its ✓ tick.

// A yes/no column the user ticks : a check box in every cell
uo_grid.of_add_column(/*key*/ "vip", /*title*/ "VIP", /*type*/ u_pbt_datagrid.TYPE_BOOL)
uo_grid.of_column(/*key*/ "vip").ib_editable = true
// Saving stays with the DataStore : when the user confirms
if ids.Update() = 1 then
	COMMIT USING SQLCA;

	// the edited cells are no longer marked as modified
	uo_grid.of_reset_update()
else
	ROLLBACK USING SQLCA;
end if

An expandable row, written from the DataStore #

// Local variables
long ll_row

// A detail panel for the first 20 accounts, written from their DataStore row
for ll_row = 1 to 20
    uo_grid.of_fill_detail(/*key*/ String(ll_row), /*markup*/ "[b]" + ids.GetItemString(ll_row, "rep") + "[/b] follows the " + ids.GetItemString(ll_row, "city") + " account")
next

// The first one is open right away ; the chevron opens the others
uo_grid.of_expand_row(/*key*/ "1")

Pages instead of a scroll, and the total of everything #

Once paging, the grid only holds one page: its footer totals the page. The total of every account is what the DataStore has.

// Local variables
n_pbt_json lnv_totals
double ld_revenue
long ll_row

// Pages of 25 rows past 100 rows ; the footer totals the page
uo_grid.ii_page_size = 25
uo_grid.il_page_threshold = 100
uo_grid.of_column(/*key*/ "revenue").is_summary = n_pbt_datagrid_column.SUMMARY_SUM

// The grand total of ALL the accounts, from the DataStore
for ll_row = 1 to ids.RowCount()
    ld_revenue = ld_revenue + ids.GetItemDecimal(ll_row, "revenue")
next
lnv_totals.of_set_number(/*path*/ "revenue", /*value*/ ld_revenue)
uo_grid.of_supply_grand_totals(/*values_json*/ lnv_totals.of_text())

A source the grid never holds whole #

For hundreds of thousands of rows, the grid only learns how many there are and asks for the ones it shows. This is where your application reads the requested slice from the database; row numbers start at 1, like those of the DataStore.

// The grid only learns HOW MANY rows exist ; it asks for the ones on screen
uo_grid.of_open_source(/*total*/ ids.RowCount())
// ue_rows_needed event of uo_grid : (long al_from, long al_to)
// Rows al_from to al_to, both included, counted from 1 like the DataStore
n_pbt_json lnv_row
string ls_rows
long ll_row

// One JSON row per DataStore row asked for, its key being the row number
ls_rows = "["
for ll_row = al_from to Min(al_to, ids.RowCount())
    lnv_row.of_clear()
    lnv_row.of_set_string(/*path*/ "_k", /*value*/ String(ll_row))
    lnv_row.of_set_string(/*path*/ "city", /*value*/ ids.GetItemString(ll_row, "city"))
    lnv_row.of_set_string(/*path*/ "rep", /*value*/ ids.GetItemString(ll_row, "rep"))
    lnv_row.of_set_string(/*path*/ "trend", /*value*/ ids.GetItemString(ll_row, "trend"))
    lnv_row.of_set_number(/*path*/ "revenue", /*value*/ Double(ids.GetItemDecimal(ll_row, "revenue")))
    if ll_row > al_from then ls_rows = ls_rows + ","
    ls_rows = ls_rows + lnv_row.of_text()
next
uo_grid.of_supply_rows(/*from*/ al_from, /*rows_json*/ ls_rows + "]")
// ue_sort_changed event of uo_grid : (string as_col, string as_dir, string as_sort)
// as_sort is the whole sort, written as SetSort reads it ("" for none) :
// sort the source with it, the grid then asks for its rows again
ids.SetSort(as_sort)
ids.Sort()

Exporting what is shown #

// What is shown - filters, sort, visible columns - to a CSV file
uo_grid.of_export_csv(/*path*/ "C:\exports\accounts.csv")
// ue_csv_saved event of uo_grid : (string as_path, boolean ab_ok, string as_error)
if ab_ok then
    st_status.Text = "Written : " + as_path
else
    st_status.Text = as_error
end if

Your own entries in the row menu #

// The row menu, entry by entry
uo_grid.of_add_row_menu_item(/*key*/ "open", /*label*/ "Open the account")
uo_grid.of_add_row_menu_separator()
uo_grid.of_add_row_menu_item(/*key*/ "call", /*label*/ "Call the sales rep")
// ue_row_menu_clicked event of uo_grid : (string as_menu_key, string as_row_key, string as_keys_json)
choose case as_menu_key
    case "open"
        wf_open_account(Long(as_row_key))
    case "call"
        wf_call(ids.GetItemString(Long(as_row_key), "rep"))
end choose

Reacting to the selection #

// ue_selection_changed event of uo_grid : (string as_keys_json)
// '["3","7"]' : the RowIDs of the selected accounts (ids.GetRowFromRowId gives their rows)
cb_delete.Enabled = (Pos(as_keys_json, "[]") = 0)

Remembering the user's layout #

// ue_layout_changed event of uo_grid : (string as_layout_json)
// Order, widths, pins, hidden columns, sort, filters, grouping : keep it
SetProfileString(gs_ini, "grids", "accounts", as_layout_json)
// open event of the window, once the grid is filled : the layout of last time
uo_grid.of_set_layout(/*layout_json*/ ProfileString(gs_ini, "grids", "accounts", ""))

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_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
of_set_property · of_get_property · of_component_nameDriving a property by its name3.1 The property engine

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