PBToolboxAI v5 ← Site

3. Shared foundation u_pbt_base #

← Getting started · Contents · Themes →


Every visual component inherits from u_pbt_base, which provides the life cycle, the property engine, the transport to the web component and error handling. The properties themselves — theme and tooltips included — are published by each component: its own page lists them all. You never use u_pbt_base directly — you drop a concrete component onto your window — but everything described below is available everywhere.


3.1 The property engine #

Assigning #

Every value you can drive is a public instance variable, assigned directly:

uo_progress.id_value   = 42.5
uo_progress.is_label   = "Import in progress…"
uo_progress.ib_animated = true

The Hungarian prefix tells you the type: is_ string, ib_ boolean, ii_ integer, il_ long (often an RGB() color), id_ double.

There is no scalar of_set_xxx: a property is set by assignment. What remains methods are additions, removals and actions (of_add_*, of_remove_*, of_select_*, of_reset, of_set_layout…).

Reading back #

Reading returns the current value (a cache on the PowerBuilder side):

if uo_progress.id_value >= 100 then …

A web component cannot be queried synchronously, so that cache is refreshed by the events. Whenever the component moves a property by itself — the user follows a link, zooms with the wheel, folds the ribbon, types — the event that tells you about it updates the property on the way. Reading it back then gives the real state, and the new value is already in place when your event code runs.

The same holds for items: after a user click, of_item(...) reads back what is on screen — the selected entry, the folded section, the checked button.

A property that no event accompanies stays on the last value you set.

Batching changes #

A burst of assignments triggers just as many renders. of_set_redraw merges them into a single one:

uo_grid.of_set_redraw(/*on*/ false)
… vingt affectations et of_add_* …
uo_grid.of_set_redraw(/*on*/ true)     // ONE single repaint

Always call both (the closing true is not optional). A read made during the freeze — of_path, an item property, "is that address free?" — sees what the burst has just posed: what is pending is applied to the model before the answer, without redrawing anything; the drawing stays single, at the resume. And an event the component raises during a read is delivered after it, never in the middle of the call that was reading.

By their name. Three public functions of every visual component double the typed properties: of_set_property(name, value) sets any property (stretch or is_stretch, in any case). The value, as text, is typed and checked — by the prefix of the name when it has one, otherwise by what the component holds under that key: a boolean accepts true, TRUE, 1, yes, on (and their opposites), a number takes the comma as well as the point whatever the regional settings ("2,5" is 2.5), a colour the RGB long, -1 or #rrggbb; an empty value is the neutral of the type (false, 0, -1 for a colour). It returns 0 once applied, -5 on an empty name, on a name the component does not know or on a value its type cannot hold — nothing is sent then, and of_get_last_error() says why. of_get_property(name) reads it live, as text (an unknown name returns "", and of_get_last_error() says so), and of_component_name() returns the name of the embedded component (video, ribbon…) whatever the class of your object. What a saved configuration, or a generic tool, replays without knowing the component.


3.2 Items #

A component with content (tabs, buttons, panels, tiles, sections…) exposes its elements through typed handles, obtained from the component itself or from their parent.

Adding #

Adding is done on the component and returns a code — 0 once added, -5 on a refused address (empty, already taken, unknown parent). The handle of the new element then comes from the getter of its level:

// Add the page : 0 = added, -5 = refused address
uo_tab.of_add_page(/*key*/ "clients", /*title*/ "Customers", /*page*/ uo_page_clients)

// The getter of the level returns its handle : set the icon
uo_tab.of_page(/*key*/ "clients").is_icon = "img\clients.png"

Retrieving and modifying #

of_item(id) — or the factory for the level concerned — returns the handle of an existing element; its properties are set exactly like a component's:

uo_toolbar.of_item(/*keys*/ "main/save").ib_enabled = false
uo_tab.of_page(/*key*/ "clients").is_title = "Customers (128)"

Counting, walking, checking #

Three questions come up over and over, and they have the same answer at every level — on the component as on any handle:

FunctionAnswers
of_count ( ) → longHow many elements at this level right now — what the component shows, not what was last sent to it
of_keys_at ( long al_index ) → stringThe address of the element at rank al_index (1 based), or "" past either end. The full address, not the bare key: it goes straight back into any method that takes one, which is the whole point of walking a level you did not build
of_has ( string as_key ) → booleanDoes this element exist? A handle is always returned, even for an unknown key: this is the only way to ask
// Local variables
long ll_groups, ll_tiles, li
string ls_group

// How many groups, and how many tiles in the first one
ll_groups = uo_tilesbox.of_count()
ls_group  = uo_tilesbox.of_keys_at(/*index*/ 1)
ll_tiles  = uo_tilesbox.of_count(/*keys*/ ls_group)

// Walk what you did not build yourself
for li = 1 to uo_tilesbox.of_count()
	ls_group = uo_tilesbox.of_keys_at(/*index*/ li)
	if uo_tilesbox.of_has(/*keys*/ ls_group + "/report") then
		uo_tilesbox.of_tile(/*keys*/ ls_group + "/report").ib_enabled = false
	end if
next

A leaf answers 0, "" and false: "I hold nothing" is a true answer, not an empty one. You can therefore walk down a tree without testing at every floor.

On a handle, of_child_keys(ref string as_keys[]) returns in a single read the addresses of everything it holds — the same as of_keys_at(1..n) — and their count: walking a level of a thousand elements costs one read instead of a thousand.

Hierarchies: an identifier is only unique within its parent #

A multi-level component provides no shortcut down to the leaf: the full path is required, which guarantees that no identifier is ambiguous.

// Ribbon: tab > group > control > menu entry
uo_ribbon.of_item(/*keys*/ "home/clipboard/paste").ib_enabled = false

The events carry the full path as well:

// ue_clicked event of uo_toolbar: (string as_keys)
choose case as_keys
    case "main/save" ; of_save()
end choose

Item events #

There are no generic item events on the ancestor: a leaf identifier alone would be ambiguous as soon as items are nested (a toolbar has several bars, a tilesbox several groups…). Each component therefore declares its own item events, carrying the full path: ue_item_invoked (as_keys) for the listbar, ue_tile_clicked (as_keys) for the tilesbox, ue_clicked (as_keys) for the toolbar…

See the component page: that is where the exact list lives.

A double click raises two clicks first. On an element that opens on a double click, it arrives as two click events followed by the open event — ue_item_clicked, ue_item_clicked, ue_item_opened on the carousel; ue_card_clicked twice then ue_card_opened on the kanban — where a native PowerBuilder control raises Clicked once then DoubleClicked. Waiting to know whether a second click comes would delay every single click: in the click event, do only what can bear being done twice (select, show a detail), and keep for the open event what must happen once.

A cancelable event — a question, ue_…_changing, asked before acting — always gets its answer: if its script stops on a run-time error (a database unavailable, a null object), the component is not left stuck waiting; the default answer leaves anyway, and the error goes its usual way.


3.3 Events common to every component #

EventRaised when
ue_ready ( )The component has finished loading; everything sent beforehand has been replayed
ue_runtime_missing ( )The WebView2 runtime is missing — see Installation
ue_bg_color (long al_color)The component has computed its theme background color; the userobject has already adopted it (backcolor), it is up to you to match the window if needed

Commands sent before ue_ready are not lost: they are queued and replayed in order. You can therefore configure everything as early as the constructor or the open.

// ue_bg_color event: match the window to the component background
parent.backcolor = al_color

3.4 Optional properties and events (opt-in) #

Some features are not enabled by default: they are only published by the components where they make sense, and you have to ask for them.

Automatic height — ib_auto_height #

The component measures its ideal height and resizes the userobject; the ue_auto_height(al_height) event lets you reposition the neighboring controls.

uo_header.ib_auto_height = true
// ue_auto_height event of uo_header
il_header_height = al_height
of_relayout()          // reposition the content below

Published by: picture and statictext.

Bands do not publish this property — their height is intrinsic. ribbon and toolbar do not scroll vertically: a fixed height can only produce empty space under the band or truncated content (a collapsed ribbon, a toolbar wrapped onto two rows, and so on). They therefore adjust at all times, with nothing to enable, and still publish ue_auto_height so that you can reposition whatever sits below.

Automatic width — ib_auto_width #

Same principle for the width. Published only by listbar, the one component whose natural width means something.

A collapsed listbar shrinks to the rail on its own and gives the width back when it expands: ib_auto_width is only useful if you also want to follow the expanded width (the bar then sizes itself to the longest label).

Ambient mouse events — ib_track_mouse #

High-frequency mouse events are cut off at the source: without a subscription, the component does not emit them at all (nothing crosses the bridge to PowerBuilder).

uo_button.ib_track_mouse = true    // enables ue_mouse_enter / ue_mouse_leave / ue_rclicked

Published by: button, picture, statictext.

Discrete events (click, selection, menu, drop…) are always emitted, with no subscription needed.

Accessible name — is_accessible_name, is_accessible_description #

What a screen reader (Narrator, NVDA) says of the whole component, set by the application — like the AccessibleName and AccessibleDescription of a PowerBuilder control. is_accessible_name is the name (plain text; a rich markup is read without its tags), is_accessible_description the longer description read after it. Empty (the default), the component keeps the name it gives itself. Both read back, go through of_set_property, and of_reset empties them. Present on every visual component.

// what a screen reader says when the focus enters the chart
uo_chart.is_accessible_name = "Sales by month"
uo_chart.is_accessible_description = "Twelve bars, January to December, in euros"

3.5 Keyboard shortcuts #

A shortcut triggers a component wherever the focus sits in the window — the user does not have to go back to the button to fire it. Every visual component accepts them, with nothing to enable.

uo_save.of_register_shortcut(/*chord*/ "Ctrl+S")
uo_refresh.of_register_shortcut(/*chord*/ "F5")

Writing a key chord #

The chord is a free-form string, normalized by the library: case, spaces and modifier order are irrelevant. "Ctrl+Shift+S", "ctrl + shift + s" and "SHIFT+CTRL+S" all mean the same shortcut — you cannot register two variants of it by mistake.

ElementAccepted forms
ModifiersCtrl (or Control), Alt, Shift — combinable, in any order
Keya letter A–Z, a digit 0–9, F1 … F24, Enter (or Return), Escape (or Esc), Delete (or Del), Insert, Home, End, PageUp, PageDown; with Ctrl or Alt only: + (or Plus, zoom Ctrl++), -, ,, ., Left Right Up Down, Space, Tab, Backspace, the keys of the numeric keypad Num 0 … Num 9, Num +, Num -, Num *, Num /, Num . (as a PowerBuilder menu writes them: they answer the keypad only, while Ctrl+7 answers both 7 keys)

This list is exhaustive: a key that is not in it (Tab, Space, a numeric keypad key, a punctuation character) fires no shortcut.

A single key is a valid chord ("F5"). An empty string removes the shortcut from the component.

"Enter" and "Escape" on their own cannot be registered as shortcuts: those two keys stay reserved for the default button and the cancel button (ib_default / ib_cancel of the button). Combined with a modifier, they become ordinary chords again ("Ctrl+Enter").

Who wins in case of conflict #

Two components may ask for the same chord — a common situation when a window hosts several areas that each have their own "Save". The arbitration goes like this:

  1. the component closest to the keyboard focus wins: the one that holds the focus itself, otherwise the one in the same MDI sheet, the same panel, the same tab page as the focused control — the shortcut of an active area is never eclipsed by a neighbour;
  2. on a tie between two components (neither is closer than the other), none receives it: the key stays with the window;
  3. between two shortcuts of the same component, the first one registered wins;
  4. the Alt+letters of labels (a button's mnemonics) are consulted only after every explicit shortcut.

The same rule applies to Enter and Escape: with two MDI sheets that each have a default button, Enter goes to the one of the sheet you are typing in. Enter, however, stays with a PB CommandButton holding the focus, and Enter or Escape with an open DropDownListBox.

Only the visible and enabled components of the foreground window take part. Re-registering a chord on a component that already had one replaces it without changing its rank: reconfiguring a window does not reshuffle the priorities.

Item shortcuts #

The two-argument overload attaches the chord to an item of the component rather than to the whole component — the second argument is the item identifier; on a component with several levels, its address (a toolbar takes "main/save", or "main/export/pdf" for a menu entry):

uo_bar.of_register_shortcut(/*chord*/ "Ctrl+N", /*keys*/ "new")
uo_bar.of_register_shortcut(/*chord*/ "Ctrl+P", /*keys*/ "print")

Removing shortcuts #

uo_bar.of_clear_shortcuts()      // component AND items

of_reset() and destroying the component call of_clear_shortcuts() for you: a component that is gone never keeps a chord reserved.

The Alt key #

Alt on its own is not captured: it gives the focus to the ribbon, which raises its keytips (see ribbon). The library therefore does not intercept the keystrokes that follow — the ribbon reads them, as if the user had clicked it. Escape or a second Alt return the focus to the control that was left. If no ribbon in the window declares a keytip, Alt keeps its usual Windows behaviour.

MemberEffect
of_register_shortcut (string as_chord)Declares a shortcut for the component; an empty string removes it
of_register_shortcut (string as_chord, string as_key)Declares a shortcut for an item, designated by its identifier
of_clear_shortcuts ( )Removes every shortcut from the component, items included

3.6 Resetting a component: of_reset() #

of_reset() returns the component to its as-new state, as if it had just been loaded:

uo_grid.of_reset()          // start again from an empty grid
// ... then rebuild

⚠️ Reusing an instance to display something else without calling of_reset() keeps the previous state (a color, a mode, an automatic height). This is the single most common cause of an unexplained "leftover" on screen.


3.7 Diagnostics #

MemberEffect
of_is_created ( ) → booleanThe native component exists (runtime present, host valid)
of_is_ready ( ) → booleanThe web content is loaded (ue_ready already raised)
of_get_last_error ( ) → stringWhy the last call of this component failed, after a < 0 return code: a name refused by of_set_property, or a call of this component to the DLL — its reason is read at the moment it failed. Never the message another component left in the DLL; empty after a call that succeeded

Return codes of the of_* methods:

ReturnMeaning
≥ 0OK (applied, or queued)
-2Component not created (runtime missing, host invalid)
-4Operation failed (capture, file write…)
-5Invalid argument (empty identifier, value out of range)
-6WebView2 runtime too old for the requested feature (printing)

3.8 Exporting the rendering as an image #

Every component can export itself as an image, exactly as displayed:

uo_tiles.of_save_as_png(/*path*/ "C:\temp\accueil.png")
uo_tiles.of_save_as_jpg(/*path*/ "C:\temp\accueil.jpg")

To print it rather than export it, see Printing.

Handy for a report, an e-mail attachment or an incident trace. The component must be created and its content loaded.


3.9 Life cycle #

  1. Construction: the webview is created as soon as the userobject is constructed — essential for hosting (tabs, dockable panels): a webview created after its HWND has been reparented does not display.
  2. Queue: your commands are queued for as long as ue_ready has not been raised.
  3. Ready: ue_ready; the queue is replayed in order.
  4. Resizing: automatic, the component follows the size of the userobject.
  5. Destruction: when the window closes; the webview is released, with no orphan process left behind.

Call PBT_Warmup() once when the application starts so that this cycle goes unnoticed (Installation).


3.10 Best practices #


← Getting started · Contents · Themes →