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 #
| Userobject | u_pbt_datagrid |
| Item class | n_pbt_datagrid_column — one column, reached with of_column |
| Used for | Presenting a DataStore in a modern grid — sorting, filters, grouping, editing, very large volumes — without leaving your DataWindow |
| Demo mode limit | 100 rows displayed; CSV and Excel exports disabled — see demo mode; the copy (Ctrl+C) too |
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:
- scale — only the visible rows are actually drawn, scrolling stays smooth across tens of thousands of rows, and windowed mode holds hundreds of thousands of rows;
- rich cells — colored status chip, gauge, sparkline, star rating, avatar, multiple tags, action button, link.
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 #
| Property | Type | Default | Purpose |
|---|---|---|---|
is_selection_mode | string | SELECT_NONE | Selection mode: none, one row, several (Shift = range, Ctrl = toggle). A value outside the constants is SELECT_NONE, and reads back so. A row the search or a filter hides leaves the selection |
is_density | string | DENSITY_COMFORTABLE | Row height: comfortable or compact |
is_quick_filter | string | "" | 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_row | boolean | false | Shows the filter input row under the headers (equivalent to the Filters button) |
ib_context_menu | boolean | true | Built-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_edits | boolean | false | Asks before an edited value is kept: raises ue_cell_editing, which may refuse — the cell then keeps its old value |
ib_write_back | boolean | false | Writes 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_demand | boolean | false | Detail 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_by | string | "" | 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_size | integer | 100 | Number of rows per page, once pagination has kicked in; 0 or less gives the default back (100) |
il_page_threshold | long | 50000 | Number of rows beyond which the grid switches to pages. 0 = always paginate, whatever the volume |
is_theme_style | string | "" | Visual style of the component (THEME_STYLE_* constants); empty = the application's, followed at every change |
is_theme_mode | string | "" | Light or dark variant (THEME_MODE_* constants); empty = the application's, followed at every change |
il_theme_accent | long | -1 | Accent colour of this component (-1 = the application accent, or the theme's) |
is_tooltip | string | "" | Simple tooltip shown when hovering the component |
is_super_tooltip_title | string | "" | Title of the rich tooltip (takes precedence over is_tooltip) |
is_super_tooltip_text | string | "" | Text of the rich tooltip (rich markup accepted) |
is_super_tooltip_image | string | "" | 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
| Property | Type | Default | Purpose |
|---|---|---|---|
is_title | string | the key | Header text of the column. of_from_datastore takes the header text the DataWindow painter gave it (<column>_t); "" gives it back (the key when there is none). Reads back the title shown |
is_tones | string | "" | 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_width | integer | declared width | Width of the column, in pixels |
ib_hidden | boolean | false | Hides or shows the column again |
is_pin | string | PIN_NONE | Pins the column to the left or to the right: it stays visible while scrolling horizontally |
ib_editable | boolean | false | Allows 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. A column with a code table is picked in the library's list (search by typing), a date in its calendar — double-click, Enter or F2; a date can be TYPED too, in the order of the language (22/09/2026 14:30), a time as 9:5 or 9:30 PM |
is_summary | string | SUMMARY_NONE | Total 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_renderer | string | RENDERER_NONE | Rich 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 ([[ ]]). A renderer outside the constants is none: it reads back "" |
ii_index | integer | declaration order | Position of the column (first position = 1); a column the grid does not hold reads back 0 |
is_filter | string | "" | 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. A comparison reads the data — the code of a code table, the number of a rating — and works on a time too ("> 09:00"); "contains" reads what is shown. A selected row the filter hides leaves the selection |
is_format | string | "" | 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, else — a number — its is_mask; reads back the format in force. It is the display of the cell: its editor types the number through is_mask, or through this format stripped of its literals ($, a quoted unit) |
is_mask | string | "" | A numeric column: the input mask of its cell editor, a numeric mask in the PowerBuilder syntax (EditMask, DataWindow format): # an optional digit, 0 a required digit, , the grouping, . the decimal point, % the number shown times 100, literals ($, a quoted text, \x) and sections positive;negative ("$#,##0;($#,##0)"). The comma and the dot stand for the separators of the display language. While typing, the grouping appears as the digits come, the decimals are limited to the mask, letters are refused, the caret keeps its place among the digits and a pasted number is read in the display language; leaving the field completes it (12 becomes 12.00). The value keeps the number as the code writes it ("1234.5"), with no grouping and no symbol. ue_cell_edited still receives the bare number, and closing the editor without typing changes nothing. Empty (the default): the editor types the column's numeric format (is_format, or the DataWindow's) stripped of its literals — a cell shown $1,234.50 is typed 1,234.50, one shown 575 px is typed 575; a % stays, it is a scale; reads back the mask in force (empty for a column that is not numeric) |
il_back_color · il_text_color · il_back_color_hover · il_text_color_hover · il_accent | long | -1 | Colours of the cells of the column (background, text, on hover) and its accent; -1 gives back the theme's. A selected or hovered row keeps its own |
is_tooltip · is_super_tooltip_title · is_super_tooltip_text · is_super_tooltip_image | string | "" | Tooltip of the column, plain or rich: shown when hovering its header and each of its cells |
Constants #
Always use the constants rather than literal strings: the IDE completes them and a typo becomes impossible.
| Family | Constants | Carried by |
|---|---|---|
| Column type | TYPE_STRING, TYPE_NUMBER, TYPE_INT, TYPE_DATE, TYPE_DATETIME, TYPE_TIME, TYPE_BOOL | the component (of_add_column) |
| Rich cell | RENDERER_AVATAR, RENDERER_CHIP, RENDERER_PROGRESS, RENDERER_SPARKLINE, RENDERER_RATING, RENDERER_TAGS, RENDERER_BUTTON, RENDERER_LINK, RENDERER_BOOL, RENDERER_CUSTOM | the component and the column handle (is_renderer) |
| Rich cell: none | RENDERER_NONE | the column handle only (is_renderer) |
| Chip tone | TONE_SUCCESS, TONE_WARN, TONE_DANGER, TONE_INFO, TONE_NEUTRAL | the column handle (is_tones) |
| Selection mode | SELECT_NONE, SELECT_SINGLE, SELECT_MULTIPLE | the component (is_selection_mode) |
| Density | DENSITY_COMFORTABLE, DENSITY_COMPACT | the component (is_density) |
| Footer total | SUMMARY_NONE, SUMMARY_SUM, SUMMARY_AVG, SUMMARY_MIN, SUMMARY_MAX, SUMMARY_COUNT | the column handle (is_summary) |
| Pinning side | PIN_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_MULTIPLEfor a property of the component,n_pbt_datagrid_column.PIN_STARTfor a column property.
Methods #
Filling the grid #
| Method | Purpose | |
|---|---|---|
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. A windowed source (of_open_source) is left: the grid shows the rows of the DataStore. A CHAR value arrives without its padding, a time column shows hh:mm:ss (or its format) | |
of_from_datastore (datawindow adw) | The same load from a DataWindow: the grid reads its rows through a working DataStore that shares them (ShareData, checked) — what the DataWindow shows, the grid shows, and the write-back (ib_write_back) goes back into it. 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_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; -5 too for a type or a renderer outside the TYPE_ / RENDERER_ constants — of_get_last_error` says which |
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; -5 too for a type or a renderer outside the TYPE_ / RENDERER_ constants — of_get_last_error` says which |
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; -5 too for a type or a renderer outside the TYPE_ / RENDERER_ constants — of_get_last_error` says which |
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. A windowed source is left. A row without a key (_k or id) takes the number after the largest numeric key of the rows | |
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_get_last_error counts the rows left out. A row without a key takes the number after the largest numeric key the grid holds — after of_from_datastore, after its RowIDs | |
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. The columns it writes lose their "modified" mark: the row holds what the DataStore holds again | |
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. The columns it writes lose their "modified" mark: the row holds what the DataStore holds again | |
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. A selected row leaves the selection, and ue_selection_changed says so | |
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 #
| Method | Purpose |
|---|---|
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 #
| Method | Purpose |
|---|---|
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 #
| Method | Purpose | |
|---|---|---|
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. A row the search or a filter hides is not selected | |
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. A row whose parent is not in the grid hangs from the root; a search or a filter shows the rows found with their ancestors, open, without touching what the user had folded | |
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. A date, a date-time or a time is written as a real Excel date (sortable, filterable), in the format of its column. A workbook too large to be written (beyond about 18 MB) is refused, ue_xlsx_saved says why: export to CSV then | |
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, -2` when the component is not created (the entry is kept and goes with the next one) |
of_add_row_menu_separator ( ) | A separator between two groups of your entries. Returns 0, -2 when the component is not created | |
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 #
| Method | Purpose |
|---|---|
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_from_datastore, of_load_rows or of_clear_source leave it |
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 #
| Method | Purpose |
|---|---|
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 #
| Event | Raised 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, or Enter is pressed on a cell that is not editable |
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, as is a selected row removed by of_remove_row or hidden by the search or a filter: what the user no longer sees leaves the selection |
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. It is the one value set by the code that the grid says like a gesture (a filter set by is_filter raises nothing): the sort drives the sorting of a windowed source |
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. Also for a value picked in a list, a calendar, a check box |
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; changes made between of_set_redraw(false) and of_set_redraw(true) raise it once, on resuming. 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. Without a licence the copy is disabled, as the exports are: the event is not raised |
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:
- sort by clicking a header: ascending, descending, none — on that column alone; Shift+click adds it to the sort after the others (already in it, turns it from ascending to descending, then takes it out), and each arrow then carries its rank (1 decides, 2 breaks the ties…);
- filter quickly through the search box, or column by column through the Filters button, which shows an input row under the headers;
- choose the visible columns through the Columns button;
- resize a column by dragging the edge of its header;
- reorder the columns by dragging their header;
- pin or hide a column through the context menu of its header;
- select a row, a range (Shift) or a set (Ctrl);
- act on a row through its context menu: copy, select all, clear selection, export to CSV — and the entries you add to it.
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:
| Key | Effect |
|---|---|
| Arrows | Move the focused cell, one cell at a time |
| Home / End | First / last column of the row |
| Ctrl+Home / Ctrl+End | First / last cell of the grid |
| Page Up / Page Down | Move by one screen, following the real height of the view |
| Shift + arrows | Extend the selection from the anchor (SELECT_MULTIPLE mode) |
| Space | Selects or deselects the focused row, and drops the anchor on it; on an editable check box, flips it |
| Enter or F2 | Puts the cell into edit mode if its column is editable; a check box flips at once |
| A character | On 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+C | Copies the selection — see ue_copy (disabled without a licence) |
| Ctrl+A | Selects every row the keyboard reaches (SELECT_MULTIPLE) |
| Menu or Shift+F10 | Opens the menu of the row (on a cell) or of the column (on a header) |
| Esc (while editing) | Cancels the edit: the cell keeps its value, the keyboard goes back to the grid |
| Space, Enter, ← / → (on a group header) | Folds or opens the group; the arrows follow the screen |
| ↑ on the first row | Reaches the header of the column; ↓ goes back to the rows |
| Enter / Shift+Enter (on a header) | Sorts on the column / adds it to the sort, as a click / Shift+click does |
| Alt+↓ (on a header) | Opens the menu of the column: sort, pin, hide |
| Ctrl+Shift+← / → (on a header) | Moves the column one place, in the direction of the screen |
| Alt+← / → (on a header) | Makes the column 10 pixels narrower / wider |
| Esc (column chooser) | Closes the list of the Columns button |
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_copyhands you the text: put it there yourself withClipBoard(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
n_pbt_utils lnv_utils
long ll_row
// A detail panel for the first 20 accounts : the key of a row is its RowID, and
// a text read in the database is escaped before it goes into the markup
for ll_row = 1 to 20
uo_grid.of_fill_detail(/*key*/ String(ids.GetRowIdFromRow(ll_row)), /*markup*/ "[b]" + lnv_utils.of_escape_markup(/*text*/ ids.GetItemString(ll_row, "rep")) + "[/b] follows the " + lnv_utils.of_escape_markup(/*text*/ 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*/ String(ids.GetRowIdFromRow(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
// Local variables
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)
// Local variables
long ll_row
// The key of a row is its RowID : GetRowFromRowId gives its row in the DataStore now
ll_row = ids.GetRowFromRowId(Long(as_row_key))
// Each entry of yours, on that row
choose case as_menu_key
case "open"
wf_open_account(ll_row)
case "call"
wf_call(ids.GetItemString(ll_row, "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 #
- Wrap the construction of a grid in
of_set_redraw(false)/of_set_redraw(true): the columns and the rows appear all at once, without flickering. - Call
of_reset()before rebuilding a grid: without it, the columns already declared byof_add_columnadd up to the new ones. of_from_datastoregives every row its RowID as its key: that key travels in every event, andids.GetRowFromRowId(Long(as_key))finds the row even after a sort or a delete in the DataStore. Rows you load yourself in JSON carry a distinct_kkey.- The grid edits, your DataStore saves:
ue_cell_edited→SetItem, thenUpdate()when the user confirms. The component does neitherUpdate()nor printing. - Beyond a few tens of thousands of rows, switch to windowed mode rather than transferring everything: memory and opening time feel it immediately.
- As soon as the grid paginates, pair the page total with an
of_supply_grand_totals: without it, the user reads a partial total where they expect the total of everything. - Save rich cells for the columns that genuinely help reading: three rich columns out of twelve catch the eye, twelve out of twelve tire it.
- To analyze and cross-tabulate rather than list, crosstab is what you need.
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.
| Members | Role | Detailed in |
|---|---|---|
of_count · of_keys_at · of_has | Walk what the component holds — here, its columns | 3.2 Items |
of_reset | Put the component back to zero | 3.6 Resetting a component: of_reset() |
of_register_shortcut · of_clear_shortcuts | The component's keyboard chords | 3.5 Keyboard shortcuts |
of_is_created · of_is_ready · of_get_last_error | Whether it was born, whether it is ready, what failed | 3.7 Diagnostics |
of_save_as_png · of_save_as_jpg | Export the rendering as an image | 3.8 Exporting the rendering as an image |
of_set_redraw | Group changes into a single repaint | 3.10 Best practices |
of_preload_icons | Icons shown with no delay | Instant display: of_icon |
of_set_translation | Translate one of the component's labels | 5.2 Adapting a label: of_set_translation |
of_focus_webview | Give the component the focus | 6.4 Keyboard and focus |
of_print · of_print_to_pdf | Print, or write a PDF | 6.9 Printing |
of_set_property · of_get_property · of_component_name | Driving a property by its name | 3.1 The property engine |
is_accessible_name · is_accessible_description | The name and description a screen reader says | 3.4 Optional properties and events |
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.