4. Themes and appearance #
← Shared foundation · Contents · Language and RTL →
4.1 Themes: two axes #
A theme is made of a style and a mode:
| Axis | Values |
|---|---|
Style (is_theme_style) | fluent · metro · office · office2007 · office2003 |
Mode (is_theme_mode) | light · dark |
That is ten themes, named <style>-<mode>: fluent-light, fluent-dark, office2007-light, metro-dark…
4.2 The application default theme (recommended) #
Set the theme once for the whole application, before the first window opens. It is injected into every component before its first render: no flash of a light style on a dark application.
// open event of the application object
PBT_SetDefaultTheme("fluent-dark")
PBT_SetDefaultThemeAccent(RGB(0, 120, 212)) // optional
You can switch on the fly at any time: every component already open is re-themed instantly.
Changing the default theme does not touch the application accent: the one set by PBT_SetDefaultThemeAccent stays, from one theme to the next, until you set another one or -1.
// Light / dark toggle from a button in the application
PBT_SetDefaultTheme("fluent-light")
| Function | Effect |
|---|---|
PBT_SetDefaultTheme (string as_name) | Default theme of the process, broadcast to every component; an unknown name is refused (-5) and the previous theme stays |
PBT_GetDefaultTheme ( ) → string | Current default theme |
PBT_SetDefaultThemeAccent (long al_accent) | Application accent, followed by every component — those with a local theme included, as long as they have none of their own; -1 removes it (each theme takes back its own accent); a PowerBuilder system colour (above 0xFFFFFF) is refused (-5) |
PBT_GetDefaultThemeAccent ( ) → long | Application accent, -1 when none is set |
PBT_SetDefaultFont (string as_family, long al_size_px) | Font of the whole application; size in pixels, 0 = the theme's (see 4.4) |
4.3 The theme of one specific component #
A component can depart from the application theme, axis by axis:
// The style alone: the mode stays the application's, and follows it
uo_editor.is_theme_style = uo_editor.THEME_STYLE_OFFICE2007
// Both axes: an entirely local theme
uo_editor.is_theme_mode = uo_editor.THEME_MODE_DARK
uo_editor.il_theme_accent = RGB(200, 60, 40) // -1 = the application accent
// Back to the application theme: both axes empty
uo_editor.is_theme_style = ""
uo_editor.is_theme_mode = ""
| Property | Type | Default | Purpose |
|---|---|---|---|
is_theme_style | string | "" | Visual style (THEME_STYLE_* constants). Empty = the application's style, followed at every change |
is_theme_mode | string | "" | Light or dark variant (THEME_MODE_* constants). Empty = the application's mode, followed at every change |
il_theme_accent | long | -1 | Accent colour of this component. -1 = the application accent, or the theme's when the application sets none |
An of_reset() hands both axes and the accent back to the application: the component follows its theme and accent again.
The two axes are independent: an axis left empty follows the application theme at every change of it, not just the one in force when the other axis was set. Spaces and case do not matter; an unknown value ("office2010", "sombre") is ignored and the axis keeps its value. Reading is_theme_style or is_theme_mode gives what the component shows — the application's style and mode for an empty axis — and il_theme_accent reads -1 as long as the component has no accent of its own. A component with a local theme also follows the application accent as long as it has none of its own.
💡 A single theme for the whole application still looks best. Save local themes for special cases (a deliberately contrasted area, a theme preview).
4.4 Recolouring a component, a group or an item #
Three scopes, the same properties. Nothing to name, nothing to guess.
// The whole component
uo_ribbon.il_theme_accent = RGB(0, 120, 90)
// A group: everything inside it follows
uo_ribbon.of_group(/*keys*/ "home/clipboard").il_accent = RGB(0, 120, 90)
// One item
uo_list.of_item(/*keys*/ "delete").il_text_color = RGB(200, 70, 70)
uo_list.of_item(/*keys*/ "delete").il_back_color = RGB(255, 235, 235)
// The same two, under the pointer
uo_list.of_item(/*keys*/ "delete").il_back_color_hover = RGB(255, 220, 220)
// Back to the component's colour
uo_list.of_item(/*keys*/ "delete").il_text_color = -1
| Property | Where | What it recolours |
|---|---|---|
il_theme_accent | the component | its accent, and everything derived from it: hover and pressed states of accent buttons, tinted selections and checked states, the readable text on top, the application background, the tab underline |
il_accent | an item, group, tab or bar handle | what that zone paints with the accent, descendants included |
il_back_color · il_text_color | same | the background and the text of the item |
il_back_color_hover · il_text_color_hover | same | the same two, under the pointer |
-1 restores the colour the component gives, which itself comes from the theme. An item colour survives the component being rebuilt: it is carried by a style rule targeting the item, not by a property set on the node of the moment. of_reset() clears everything.
il_accent only repaints what the zone paints with the accent — a selection, an active underline, a progress bar. A component that never uses it will show nothing: for "this entry in red", il_back_color and il_text_color are the right tools, read by every component with items.
The font of the whole application #
PBT_SetDefaultFont("Segoe UI", 14)
One call dresses every live component and the ones created afterwards — the font is injected before their first paint. An empty family or a size of 0 gives that half back to the theme. The size is in pixels.
4.5 The component background is reported back to PowerBuilder #
Every component paints its background according to the theme, then reports its color: the userobject adopts that color (backcolor) and raises ue_bg_color, so that the window and the neighboring PowerBuilder controls can match it.
// ue_bg_color event of a component
parent.backcolor = al_color
st_title.backcolor = al_color
This is what lets you mix PBToolboxAI components and native PowerBuilder controls with no visible seam in dark mode.
4.6 Images and icons #
Everywhere a component expects an image path (button icon, tile, [picture=…]…), four forms are accepted:
| Form | Example | Used for |
|---|---|---|
| File | img\logo.png | The image as-is (png, jpg, gif, bmp, ico, svg, webp) |
| DLL resource | img\packimages.dll:RIBBON | An image packaged in a resource DLL |
mono: | mono:img\save.svg | Flat fill in the theme color: only the shape matters |
tint: | tint:img\logo_couleur.png | Duotone: the internal shading modulates the theme color |
- Use
mono:for every monochrome glyph (white or black icons): they recolor themselves automatically, in light mode as well as dark. tint:harmonizes a color icon with the theme while preserving its gradients. Never use it on a white glyph (it would stay white).- With no prefix, a multicolor image is left untouched.
The path.dll:name form loads a resource from an image DLL (packimages.dll style), opened read-only (LOAD_LIBRARY_AS_DATAFILE, no code executed). This saves you from shipping hundreds of loose files.
Instant display: of_icon #
A small glyph passed through of_icon() is embedded in the command (no loading round trip): it shows up on the very first render, without the flicker of an icon loaded afterwards.
n_pbt_utils lnv_utils // autoinstantiate : nothing to create, nothing to destroy
uo_toolbar.of_add_button(/*keys*/ "main/save", /*text*/ "Save", /*image*/ lnv_utils.of_icon(/*path*/ "mono:img\save.svg"), /*tooltip*/ "Save")
For a batch of icons known in advance, of_preload_icons() warms the cache in one go, at startup: the first paint then waits for nothing.
Transparent in practice: beyond a certain size, of_icon returns the original path (the image is then loaded and cached as usual).
4.7 Rich text markup #
Any label of any component accepts BBCode-style markup: tab title, button caption, status bar text, toast message, panel title, tooltip text…
The entries of the built-in menus follow the same rule — a tab's context menu, the ··· list of the tabs that no longer fit, a grid's column menus: the label shown by the menu is the one on the control, markup included.
The text is rendered as text nodes and <span> elements: no HTML injection is possible.
| Tag | Effect |
|---|---|
[b] [i] [u] [s] / [strike] | Bold, italic, underline, strikethrough |
[sub] [super] | Subscript, superscript |
[red]…[/red] (named colors) | Text color (red, green, blue, orange, teal…) |
[accent]…[/accent] | Accent color of the current theme |
[color=#rrggbb] / [color=accent] | Text color |
[bk=#rrggbb] / [backcolor=accent] | Background color |
[font=Consolas] | Font |
[size=14] | Absolute size, in points (6 to 200) |
[size+=30] / [size-=20] | Relative size in % (20% by default) |
[picture=path] / [picture=path,width,height] | Inline image. Also accepts what of_icon() returns (a data URI); the dimensions are read at the end of the value. A network path is refused there (see below) |
[symbol=name] | Built-in symbol, monochrome, drawn in the colour of the text around it (it follows the theme, the hover, an [accent]) — no file to ship: clock, location, person, people, calendar, repeat, lock, bell, phone, mail, video, note, tag, link, check, star, info, warning. An unknown name is shown as is |
[br] / [linebreak] / [br:3] | Line break (or n breaks) |
[gap=N] | Line break followed by a blank of N % of a line: [gap=100] equals [br][br], [gap=50] half an empty line |
[separator] | Horizontal rule |
[hyperlink=url]…[/hyperlink] | Clickable area: the link always opens in the user's browser, in every component. The ue_hyperlink(as_url) event is raised as well, for the components that expose it |
[action=id]…[/action] | Clickable area → ue_action(as_key) event, styled as a link |
[invisibleaction=id]…[/invisibleaction] | Clickable area → ue_action, without the link styling |
[bullet]…[/bullet] | Bullet: list item whose wrapped lines align on the first one instead of running back under the marker (hanging indent). [bullet=-] changes the marker |
[foldarea:Title]…[/foldarea] | Collapsible block: a clickable header (− / +) above an indented body. The title accepts markup |
[foldarea-closed:Title]…[/foldarea] | The same block, collapsed when displayed |
[[ / ]] | Escaping: [[b]] displays [b] without interpreting it |
uo_text.is_text = "Welcome to [b][accent]PBToolboxAI[/accent][/b] [size-=20]v1.0[/size-=20]" &
+ "[br]See the [hyperlink=https://pbtoolboxai.net]documentation[/hyperlink]."
uo_tab.of_add_page(/*key*/ "clients", /*title*/ "[b]Customers[/b] [size-=20](128)[/size-=20]", /*page*/ uo_clients)
uo_st.is_text = "The [[b]] tag makes text [b]bold[/b]" // displays: The [b] tag makes text bold
Displaying data as-is. A value coming from your database may contain a known tag — [b], [red], [picture=…]: it would be interpreted (an unknown word between brackets is shown as written). of_escape_markup(), a function of n_pbt_utils, doubles them for you — wrap the data, never the markup you wrote yourself.
n_pbt_utils lnv_utils // autoinstantiate : nothing to create, nothing to destroy
// Business data may hold a KNOWN tag : without escaping it is INTERPRETED --
// the [b] vanishes and what follows turns bold.
ls_label = "Discount [b]VIP"
uo_st.is_text = "Customer : " + ls_label // shows : Customer : Discount VIP (VIP in bold)
uo_st.is_text = "Customer : " + lnv_utils.of_escape_markup(/*text*/ ls_label) // shows : Customer : Discount [b]VIP
Text without any tag carries no overhead at all (fast path). An unknown tag is shown as written (Balance [net] stays Balance [net]). A closing tag closes its own tag only, and a closing tag with no opener is ignored. A [hyperlink] opens everywhere — a label, a tab title, a status bar panel, a toast, a dialog: the core takes care of it. The ue_action event, on the other hand, is only emitted by interactive text components (statictext); elsewhere, [action] is formatting only.
Only http, https and mailto are opened, whatever their case (HTTPS:// too). A label often carries data coming from your database: handing an arbitrary scheme to the system would turn a label into a program launcher. For the same reason, a markup image never reaches a network share ([picture=\\server\share\x.png] is refused): an unescaped comment would otherwise open a network session to any machine just by being displayed. A network image inside a text goes through of_icon(), which embeds it; is_picture and the icons of the components keep network access.
A [foldarea] is a block: it takes the full width and folds on a click on its header, with no round trip to PowerBuilder. Blocks nest, and when the component follows the height of its content (ib_auto_height), that height is reported again on every fold. The title is markup too: nothing is bolded for you, [foldarea:[b]Total[/b]] does it.
← Shared foundation · Contents · Language and RTL →
4.8 Animations and the workstation setting #
Windows offers an accessibility setting — Settings > Accessibility > Visual effects > Animation effects — and the components honour it: when it is off, no keyframe and no transition plays. The chart arrives at its place, it does not travel there.
That is the right default, and it is not up for debate: someone who asked their system for less movement meant it. ib_animated = true changes nothing about it.
An application may still insist:
// Declare it once : Function long PBT_SetAnimationPolicy (long al_policy)
// Library "pbtoolboxai.dll"
PBT_SetAnimationPolicy(1) // 1 = always animate, 0 = respect the workstation (default)
The call covers the whole process and may be made at any time: live components follow at once, later ones receive it when they open.
Only set
1if your application has a real reason to override — a kiosk, a wallboard, a demonstration whose very job is to show what these animations look like. For a business application, leave the default.
4.9 Composing your application's look #
The library ships ten themes and does not let an application define an eleventh: the token vocabulary is internal, and it stays that way. What it offers instead comes down to three levers, which combine — that is how you get "our colours" without writing a theme.
// 1. THE BASE: the shipped theme closest to the target.
PBT_SetDefaultTheme("office-light")
// 2. THE ACCENT: ONE colour dresses every component, those created
// afterwards included, and everything the theme derives from it.
PBT_SetDefaultThemeAccent(RGB(0, 105, 92))
// 3. THE FONT of the whole application, in one call.
PBT_SetDefaultFont("Segoe UI Semibold", 0)
Put these three lines in the application object's open event: they reach every component before its first paint, so nothing ever flashes.
| Lever | Scope | What it changes |
|---|---|---|
PBT_SetDefaultTheme | the process | style and mode: shapes, corners, weights, the whole palette |
PBT_SetDefaultThemeAccent | the process | the accent, and what the theme derives from it — hover and pressed accent buttons, selection, readable text over it, tab underline; components with a local theme included, and it survives a change of theme |
PBT_SetDefaultFont | the process | family and size; an empty family or a size of 0 hands that half back to the theme |
il_theme_accent | one component | its own accent, when a window has to stand apart |
il_back_color · il_text_color | one item | one precise entry, in red because it deletes (see 4.4) |
What this does not allow #
Redefining the full palette — the surface greys, the borders, the corner radius — is not offered. A theme is a coherent set of some sixty values that answer one another: opening half of them would produce unreadable combinations nobody had checked. If your brand needs more than these three levers, write to us: one more theme inside the library is an option, your theme inside your code is not.
In the demonstration application: ribbon Home > Appearance > Style > Corporate (composed). The entry composes these three levers — nothing reserved to us — with two differences: the theme is
office-lightoroffice-darkdepending on the ribbon's light/dark button, and the font is set at 16 pixels. The code iswf_apply_style, in thew_demo_homewindow.
4.10 Windows high contrast #
When the user turns on a Windows High contrast theme, the engine replaces the colours of the page with the system's, whatever the library theme. The components take it into account: monochrome icons (mono:, tint:) take the system text colour — the highlighted text colour on a selected row —, focus rings and markers drawn as shadows get a real outline, and what carries a meaning through its colour (a status dot, a colour chosen by the application) keeps it. Nothing to do on the application side: no property, no call.
4.11 Third-party content: web browser and PDF viewer #
webbrowser and pdfviewer show content the library does not draw: a website, or the engine's PDF viewer. That content knows nothing of the library themes, but it reads the light or dark preference the browser announces to it, as a website reads the one of Windows. That preference follows the application default theme (PBT_SetDefaultTheme) — not the Windows mode, not a component's local theme: an application in fluent-dark shows the dark version of a site that offers one, and the PDF viewer in its dark colours. Before the third-party page has painted, the area takes the theme background instead of a white rectangle.