listbar — u_pbt_listbar #
← Component reference · Guide contents
Side navigation bar: collapsible accordion sections holding entries with icons, an accent band on the current entry, and collapsing into an icon rail.
▶ See it live — Demo application, Listbar tile: the preview, the code behind it and this page, side by side.
At a glance #
| Userobject | u_pbt_listbar |
| Item classes | n_pbt_listbar_section (section) → n_pbt_listbar_item (entry) |
| Used for | Replacing a side menu improvised out of buttons with structured, themed, collapsible navigation |
| Opt-in options | ib_auto_width, ib_reorderable |
This is the only component in the library that publishes ib_auto_width: its natural width actually means something, since the collapsed rail is far narrower than the expanded bar. The common case is in fact already covered without enabling anything — ib_collapsed = true shrinks the bar down to the rail, and gives its width back when it expands.
Quick start #
// window open event
// A section, then its entries : each entry is addressed by section/entry
uo_nav.of_add_section(/*key*/ "nav", /*text*/ "Navigation")
uo_nav.of_add_item(/*keys*/ "nav/home", /*text*/ "Home", /*image*/ "mono:img\home.svg")
uo_nav.of_add_item(/*keys*/ "nav/docs", /*text*/ "Documents", /*image*/ "mono:img\doc.svg")
uo_nav.of_add_item(/*keys*/ "nav/search", /*text*/ "Search", /*image*/ "mono:img\find.svg")
// A second section, with a single entry
uo_nav.of_add_section(/*key*/ "settings", /*text*/ "Settings")
uo_nav.of_add_item(/*keys*/ "settings/prefs", /*text*/ "Preferences", /*image*/ "mono:img\gear.svg")
// The selection is set on the ENTRY, by its address : the section, then the entry
uo_nav.of_select_item(/*keys*/ "nav/home")
// of_select_item raises ue_selection_changed, like a click : the first page opens there
// ue_selection_changed event of uo_nav : (string as_from_keys, string as_keys)
choose case as_keys
case "nav/home" ; of_open_home()
case "nav/docs" ; of_open_documents()
case "settings/prefs" ; of_open_preferences()
end choose
Two levels, one address #
An entry identifier is only unique within its section, so an entry is named by its address: the section, then the entry, joined by / — "nav/docs". The component is what takes it (of_item, of_select_item, of_remove_item…); a section is named by its key alone (of_section("nav")). See Hierarchies.
// A section by its key, an entry by its address
uo_nav.of_section(/*key*/ "nav").is_title = "Navigation"
uo_nav.of_item(/*keys*/ "nav/docs").is_text = "Documents"
The events also carry the full path, and the entry left with it: ue_selection_changed(as_from_keys, as_keys).
Properties #
| Property | Type | Default | Purpose |
|---|---|---|---|
ib_collapsed | boolean | false | true collapses the bar into an icon rail: section titles and labels give way to a thin rule between sections, and every entry stays visible, even in a folded section (the fold comes back on the way out). An entry with no icon shows its initial, and each entry tells its label on hover |
ib_auto_width | boolean | false | Opt-in: same for the width, including when expanded (the bar sizes itself to the longest label). Collapsing into the rail already shrinks by itself; ue_auto_width follows in both cases |
ib_reorderable | boolean | false | Opt-in: the user can drag an entry to another position. The move stays inside its section — an entry id is only unique there, so crossing would risk two identical keys (raises ue_item_reordered) |
ib_veto_selection | boolean | false | Opt-in: ask before the selection moves — a gesture of the user and of_select_item / of_clear_selection (raises ue_selection_changing, which can refuse; a refused order returns -4). Off by default, like every cancelable event of the library: set it to true when you need to refuse a move (unsaved changes). Each question of a gesture costs a round trip to PowerBuilder (~35 ms) |
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 |
Properties of a section — n_pbt_listbar_section #
| Property | Type | Default | Purpose |
|---|---|---|---|
is_title | string | "" | Section title. Accepts rich text markup. An empty title shows no header row at all: the section becomes a plain invisible grouping; a gap separates it from the section above (a thin rule in the rail), and it can no longer be folded — nobody could open it again |
ib_collapsed | boolean | false | Accordion: true collapses the entries of this section. The header stays visible and its chevron rotates; ue_section_toggled follows, as for a click on the header |
ib_pinned | boolean | false | true pins the section at the bottom of the bar (Settings, Help): a footer that stays in view while the rest of the bar scrolls, pushed to the bottom when there is room to spare, and at the bottom of the rail too. Several pinned sections stack in their order. Only the drawing changes: the layout (of_get_layout) keeps the order of the sections, and the Down arrow goes from the last entry at the top to the first one of the footer |
is_image | string | "" | Icon before the section title (accepted forms: path, mono:, tint:, DLL resource). An empty string removes it |
il_accent | long | -1 | Accent of this section, i.e. of its title and nothing else: its entries keep their own (-1 = the component's) |
il_back_color | long | -1 | Background of the section title, as RGB(r,g,b) (-1 = the component's) |
il_text_color | long | -1 | Text colour of the section title (-1 = the component's) |
il_back_color_hover | long | -1 | Background of the title under the pointer (-1 = the component's) |
il_text_color_hover | long | -1 | Text colour of the title under the pointer (-1 = the component's) |
Properties of an entry — n_pbt_listbar_item #
| Property | Type | Default | Purpose |
|---|---|---|---|
is_text | string | "" | Entry label, which can be changed on the fly without rebuilding the bar. Accepts rich text markup |
is_image | string | "" | Icon, changeable on the fly (accepted forms: file path, mono:, tint:, DLL resource) |
ib_enabled | boolean | true | false greys the entry: it stays in place and answers nothing — no click, no Enter, no right click, and the arrows skip it |
ib_visible | boolean | true | false hides the entry without removing it from the bar. A hidden entry cannot become the selection (of_select_item returns -5); hiding the one that is selected keeps it selected, and showing it again gives it back as it was |
is_badge | string | "" | Count pill at the end of the entry ("12" unread), in the theme's badge colours; in the rail, on the corner of the icon. Any short text, markup accepted. An empty string removes it |
of_is_selected ( ) → boolean | — | — | Is this entry the selected one? Read only, and deliberately: the selection is one state of the whole bar, not a flag per entry. To set it, of_select_item on the component |
is_tooltip | string | "" | Simple tooltip shown when hovering the item |
is_super_tooltip_title | string | "" | Title of the item rich tooltip (takes precedence over is_tooltip) |
is_super_tooltip_text | string | "" | Text of the item rich tooltip (rich markup accepted) |
is_super_tooltip_image | string | "" | Image of the item rich tooltip |
il_accent | long | -1 | Accent of this entry: the bar that marks it when it is selected takes it (-1 = the component's). For "this entry in red", il_back_color / il_text_color |
il_back_color | long | -1 | The entry's own background, as RGB(r,g,b) (-1 = what the theme gives) |
il_text_color | long | -1 | Text colour of the entry (-1 = what the theme gives) |
il_back_color_hover | long | -1 | Background of the entry under the pointer (-1 = what the theme gives) |
il_text_color_hover | long | -1 | Text colour of the entry under the pointer (-1 = what the theme gives) |
Methods #
On the component #
| Method | Purpose | |
|---|---|---|
of_add_section (string as_key, string as_text) | Adds a section at the end of the bar. An empty text draws no title line. The handle comes from of_section("nav") when you want to set a property. Returns 0 once applied, -5 when the key is empty, already taken or holds a / or a ` | , -2` when the component is not created |
of_insert_section (string as_key, string as_text, integer ai_index) | Adds a section at the rank you choose (first position = 1) rather than at the end. The index counts sections, not rows. A name already taken is refused: two sections under the same name would make the address of each of their entries ambiguous. Returns 0 once applied, -5 when the key is empty, already taken or holds a / or a ` | , -2` when the component is not created |
of_move_section (string as_key, integer ai_index) | Moves a section to rank ai_index, carrying its entries with it. Moving the header alone would drop its entries into whatever section then stands above them. Returns 0 once applied, -5 for a section this bar never added, -2 when the component is not created | |
of_remove_section (string as_key) | Removes a section and everything it holds. Emptying it while keeping its entries would orphan them: they would carry a section identifier that names nothing. If the selection was in it, ue_selection_changed says there is none any more. Returns 0 once applied, -5 for a section this bar never added, -2 when the component is not created | |
of_section (string as_key) | Handle of one section, by its key: its title, icon, fold and colours are set through it (created on first access). An entry is reached from the component instead, by its address (of_item) | |
of_add_item (string as_keys, string as_text, string as_image) | Adds an entry at its address, "nav/home": the section it lands in, then its own identifier. An overload omits the icon. Returns 0 once added, -5 when the section was not added by this bar, when the address is already taken or when the key holds a ` | , -2` when the component is not created |
of_item (string as_keys) | Handle of one entry, by its address (created on first access) | |
of_insert_item (string as_keys, string as_text, integer ai_index) | Inserts an entry at position ai_index within its section (first position = 1). Returns 0 once added, -5 when the section was not added by this bar, when the address is already taken or when the key holds a ` | , -2` when the component is not created |
of_insert_item (string as_keys, string as_text, string as_image, integer ai_index) | Same, with the entry's icon: of_add_item takes one, so inserting must too. Returns 0 once added, -5 when the section was not added by this bar, when the address is already taken or when the key holds a ` | , -2` when the component is not created |
of_move_item (string as_keys, integer ai_index) | Moves an existing entry within its section, preserving its state. Returns 0 once applied, -5 for an address this bar never added, -2 when the component is not created | |
of_remove_item (string as_keys) | Removes one entry, designated by its address. If it was the selection, ue_selection_changed says there is none any more. Returns 0 once applied, -5 for an address this bar never added, -2 when the component is not created | |
of_clear ( ) | Empties the bar: every section and every entry. If an entry was selected, ue_selection_changed says there is none any more (an empty address); ue_layout_changed is not raised, you are rebuilding. Returns 0 once applied, -2 when the component is not created | |
of_select_item (string as_keys) | Selects an entry, like a click on it: with ib_veto_selection on, ue_selection_changing is asked first; then ue_selection_changed follows, as for a click (right after your script, from the event queue), and of_selected_key() read right after already answers the new address. The entry already selected: nothing is asked nor raised. Returns 0 once selected (or already selected), -4 when your ue_selection_changing refused (nothing moves), -5 for an address this bar never added, a hidden entry or an entry the demo cap does not draw, -2 when the component is not created | |
of_get_layout ( ) | Reads the current arrangement back as JSON: the sections in order, each with its entries in order and whether it is folded. Store it (file, database, registry) and hand it back with of_set_layout at the next start. The same pair carries the same names on every component that can be rearranged | |
of_set_layout (string as_layout_json) | Restores an arrangement read with of_get_layout or received with ue_layout_changed. What the layout does not name keeps its place at the end: a layout saved yesterday must not make what was added since disappear. Applying it raises no event — you supplied it. Returns 0 once applied, -5 for an empty text or one that is not a JSON object (a truncated file): nothing is sent; the fold of a section that is not there is ignored, -2 when the component is not created | |
of_clear_selection ( ) | Leaves no entry selected, like any move: with ib_veto_selection on, ue_selection_changing is asked first, then ue_selection_changed follows with an empty address. Nothing selected already: nothing is asked nor raised. Returns 0 once applied, -4 when your ue_selection_changing refused (the entry stays selected), -2 when the component is not created | |
of_selected_key ( ) | Address of the selected entry — "nav/docs", "" if there is none. It is exactly what ue_selection_changed hands you: a comparison is a comparison, not a reassembly. Always the current one, read from the component: after a click, and right after of_select_item too | |
of_reset ( ) | Empties the bar, then brings the component back to its brand-new state. If an entry was selected, ue_selection_changed goes with an empty address; ue_layout_changed is not raised. Returns 0 once applied, -2 when the component is not created | |
of_set_redraw (boolean) | Groups a burst of changes into a single render. Returns 0 once applied, -2 when the component is not created | |
of_save_as_png (string) · of_save_as_jpg (string) | Exports the rendering as an image. Returns 0 once the image is written, -4 when writing fails, -2 when the component is not created |
On a section — n_pbt_listbar_section #
| Method | Purpose |
|---|---|
of_count ( ) | Number of entries in the section, hidden ones included |
of_keys_at (long al_index) | Address of the entry at rank al_index (from 1) — "nav/docs", ready to pass back to of_item |
of_has (string as_key) | Does the section hold this entry? |
Events #
| Event | Raised when |
|---|---|
ue_selection_changed (string as_from_keys, string as_keys) | The selection has moved: the user picked an entry (click, Enter, Space), your code moved it (of_select_item, of_clear_selection — an order raises it too, like SelectItem raises SelectionChanged), or the selected entry left (of_remove_item, of_remove_section, of_clear, of_reset — as_keys is then empty). Same arguments as ue_selection_changing: the question and its outcome read the same way, and the as_from_* pair designates the entry left (empty if there is none) |
ue_section_toggled (string as_key, boolean ab_collapsed) | The user folds or opens a section: a click on its title, Enter on the title, or the Left / Right arrows (the other way round right to left), or your code setting a section's ib_collapsed |
ue_item_reordered (string as_keys, integer ai_index) | The user finished dragging an entry. ai_index is its new rank inside its section, starting from 1, like of_move_item. Keep that order to give the user their bar back as they left it |
ue_layout_changed (string as_layout_json) | The arrangement to store has changed: an entry or a section moved, a section was folded or opened, something was removed — by the user or by your code. Carries the whole layout, not just what moved: persisting it is one assignment. Adds, the rail, of_set_layout, of_clear and of_reset do not raise it: they are construction, not arrangement |
ue_item_rclicked (string as_keys) | The user right-clicked an entry: its address, "nav/docs". One event per right click, and the entry is not selected. Open your own menu there, at the pointer: m_menu.PopMenu(PointerX(), PointerY()). A greyed entry raises nothing |
ue_selection_changing (string as_from_keys, string as_keys) → boolean | Cancelable, asked before the selection moves — a gesture of the user, of_select_item or of_clear_selection — and only when ib_veto_selection = true (off by default). Return false to stay where you are: nothing moves, and the order of your code returns -4 |
ue_auto_width (long al_width) | The component has recomputed its ideal width — requires ib_auto_width = true |
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) |
Examples #
A complete side menu #
// The whole bar in a single render
uo_nav.of_set_redraw(/*on*/ false)
// First section and its three entries
uo_nav.of_add_section(/*key*/ "folders", /*text*/ "Folders")
uo_nav.of_add_item(/*keys*/ "folders/recent", /*text*/ "Recent", /*image*/ "mono:img\clock.svg")
uo_nav.of_add_item(/*keys*/ "folders/customers", /*text*/ "Customers", /*image*/ "mono:img\user.svg")
uo_nav.of_add_item(/*keys*/ "folders/archives", /*text*/ "Archives", /*image*/ "mono:img\box.svg")
// Second section and its two entries
uo_nav.of_add_section(/*key*/ "tools", /*text*/ "Tools")
uo_nav.of_add_item(/*keys*/ "tools/import", /*text*/ "Import", /*image*/ "mono:img\import.svg")
uo_nav.of_add_item(/*keys*/ "tools/export", /*text*/ "Export", /*image*/ "mono:img\export.svg")
// One render for everything, then the first entry is selected
uo_nav.of_set_redraw(/*on*/ true)
uo_nav.of_select_item(/*keys*/ "folders/recent")
A collapsible rail that frees up space #
// Collapse into an icon rail : the bar shrinks by itself (and gets its width
// back when you expand it).
uo_nav.ib_collapsed = true
// ue_auto_width event of uo_nav : (long al_width)
// The bar has just adopted its ideal width : realign whatever sits to its right.
uo_content.x = uo_nav.x + al_width
uo_content.width = parent.width - uo_content.x
Accordion: collapsing a section #
// Collapse the archives section, which is rarely used
uo_nav.of_section(/*key*/ "folders").ib_collapsed = true
// ue_section_toggled event of uo_nav : (string as_key, boolean ab_collapsed)
// Remember the fold the user chose
of_save_preference("section_" + as_key, String(ab_collapsed))
Settings and Help pinned at the bottom #
A section without a title, pinned: no header line, two entries that stay at the foot of the bar even when the navigation, longer than the window, scrolls. A click there selects as anywhere else: ue_selection_changed receives foot/settings.
// A section WITHOUT a title (no header line), pinned at the bottom of the bar :
// it stays in view while the entries above it scroll
uo_nav.of_add_section(/*key*/ "foot", /*text*/ "")
uo_nav.of_add_item(/*keys*/ "foot/settings", /*text*/ "Settings", /*image*/ "mono:img\\gear.svg")
uo_nav.of_add_item(/*keys*/ "foot/help", /*text*/ "Help", /*image*/ "mono:img\\help.svg")
uo_nav.of_section(/*key*/ "foot").ib_pinned = true
Responding to navigation #
// ue_selection_changed event of uo_nav : (string as_from_keys, string as_keys)
// The address carries both levels : two sections may both have a "list" entry.
// Local variables
n_pbt_utils lnv_utils // autoinstantiate : nothing to create, nothing to destroy
string ls_keys[]
// Split the address : [1] = the section, [2] = the entry
if lnv_utils.of_split_path(/*keys*/ as_keys, /*out*/ ls_keys) < 2 then return
choose case ls_keys[1]
case "folders" ; of_open_folder(ls_keys[2])
case "tools" ; of_run_tool(ls_keys[2])
end choose
Refusing a change of selection #
// The question is off by DEFAULT : switch it on to be able to refuse a move
uo_nav.ib_veto_selection = true
// ue_selection_changing event of uo_nav :
// (string as_from_keys, string as_keys)
// Returning FALSE keeps the user on the entry being left.
if of_has_unsaved_changes(as_from_keys) then
MessageBox("Entry", "Finish the current record before navigating away.")
return false
end if
return true
Updating an entry on the fly #
// Local variables
n_pbt_listbar_item lnv_entry
// The entry is named by its address : the section, then the entry
lnv_entry = uo_nav.of_item(/*keys*/ "folders/recent")
lnv_entry.is_text = "Recent (12)"
lnv_entry.is_image = "mono:img\clock_full.svg"
// Gray out or hide according to permissions, without rebuilding the bar
uo_nav.of_item(/*keys*/ "tools/import").ib_enabled = of_is_allowed("import")
uo_nav.of_item(/*keys*/ "tools/export").ib_visible = gb_expert_mode
Selection driven from code #
// Move the selection elsewhere : the accent band follows, and
// ue_selection_changed goes as for a click
uo_nav.of_select_item(/*keys*/ "folders/customers")
// Or clear it completely
uo_nav.of_clear_selection()
Rebuilding the bar #
// of_clear empties sections and entries ; of_reset also restores the component defaults
uo_nav.of_clear()
of_build_menu_for_profile()
Best practices #
- An entry is added by its address:
of_add_item("nav/home", ...). The section handle is now only for setting a property, andof_section("nav")hands it over on first access. - Use
mono:for icons: they recolor with the theme, light and dark alike, and stay legible once the bar is collapsed into a rail. - An empty section title works as a discreet grouping: a gap separates its entries from the ones above (a thin rule in the rail) without any header row appearing. Such a section does not fold.
- With the keyboard: the Up / Down arrows walk the titles and entries, Left / Right fold and open a section (the other way round right to left), Enter or Space picks, a letter jumps to the next line that starts with it.
ib_collapsedshrinks the userobject down to the rail, but it does not move your other controls: handleue_auto_widthto take up the freed space, otherwise it just stays empty.- In rail mode only the icons remain: each entry tells its label on hover, and an entry without an icon shows its initial. Give each one an icon anyway — one initial is hard to tell from another.
- The question is only asked once you switch it on (
ib_veto_selection = true): do so only where you need to refuse a move, since every question costs a round trip to PowerBuilder.of_select_itemnever asks it. - Prefer
ib_enabled = falseoverib_visible = falsewhen the entry will become available again: the menu does not change shape under the user's eyes.
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 | 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 |
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.