Skip to content

PsImage

Paul Squires edited this page Oct 19, 2026 · 1 revision

Home › All Categories › PsControls › PsImage

PsImage

A small support class for loading a raster image — .ico, .png, .bmp, .jpg, or an existing HBITMAP/HICON — so an owner-drawn control can show a real picture in an icon slot instead of (or as well as) a Segoe Fluent Icons glyph.

It is not a control. It is a TYPE, PsImageFile, you construct, point at a file once, and keep alongside a control for as long as that control needs the image. It does exactly three things — load (decode once, at natural size), measure (report natural width/height), and hand out the decoded image — and it does not draw. The drawing is PsPaintBuffer.PaintImage, which owns the paint surface's GDI+ graphics object. Load here, draw there.

PsImage is the loader that pairs with PsBufferPaint, in the same way PsBufferPaint is the surface every Ps* control paints through.


Requirements

The module is vendor/PsControls/PsImage, comes with the toolchain, and brings in PsBufferPaint — the surface whose PaintImage actually draws the loaded image — itself. PsImage decodes into sys/win/gdiplus objects (CGpBitmap). Import PsBufferPaint yourself as well to name the buffer and PS_IMGFIT_* at the draw site:

#import "sys/win"
#import "sys/win/gui"
#import "sys/win/gdiplus"
#import "vendor/PsControls/PsBufferPaint"
#import "vendor/PsControls/PsImage"

using gui
using gdiplus
using PsBufferPaint
using PsImage

Build with tiko build in a folder whose tiko.toml defines UNICODE, as the demo's does:

[target.main]
main      = "main.tiko"
output    = "main.exe"

[target.main.defines]
UNICODE = "1"

UNICODE has to come from the manifest (or -d UNICODE on a tiko command line): a source file cannot #define it ahead of #import "sys/win", and without it every bare Win32 name — DrawText, SetWindowText, CreateFontIndirect — binds its ANSI form.

tiko build (debug profile, -g -exx) or tiko build --profile release from the toolchain's examples/PsControls/PsImage/ writes the demo's main.exe there.

GDI+ must be running before the first load and must outlive the last image. Decoding builds GDI+ objects, so bracket your message loop — the same bracket PsBufferPaint already asks for:

dim as ULONG_PTR gdipToken = gdiplus.Init()
' ... create windows, load images, run the message loop ...
gdiplus.Uninit( gdipToken )      ' AFTER every PsImageFile has been destroyed

There is no pump obligation. PsImage owns no window and no timer — there is nothing to add to your GetMessage loop, and no FilterMessage to call.


Usage

Load once (e.g. when you create the control), then hand the decoded image to the buffer inside your paint routine:

' --- once ---
dim as PsImageFile img
if img.Load( "C:\icons\save.png" ) then
    ' loaded
end if

' --- in WM_PAINT, into an icon-cell RECT you computed ---
if img.IsValid() then
    b.PaintImage( img.Image(), @rcIcon )        ' PS_IMGFIT_ASPECT by default
end if

An image loaded through the icon-bearing controls (PsButton, PsIconPanel, PsToolbar) is handled for you — see their own docs for PsButton_SetImageLeft, PsIconPanel_SetImage, PsToolbar_SetItemImage and friends. You only touch PsImage directly when you paint your own surface.


API

Member Returns Description
Load( wszPath as ustring ) long Decode a file. .ico / .png / .bmp / .jpg / .jpeg / .gif / .tif. Non-zero on success; 0 on failure (bad path, unsupported/corrupt file). A failed load clears any image previously held.
LoadFromBitmap( hbm as HBITMAP ) long Wrap an existing HBITMAP (GDI+ takes a copy; you keep ownership of hbm). Non-zero on success.
LoadFromIcon( hIcon as HICON ) long Wrap an existing HICON (GDI+ takes a copy; you still DestroyIcon it). Non-zero on success.
IsValid() long Non-zero once a load has succeeded and nothing has failed since.
NaturalWidth() long Decoded pixel width, or 0 when not valid.
NaturalHeight() long Decoded pixel height, or 0 when not valid.
Image() CGpImage ptr The decoded image, for PsPaintBuffer.PaintImage. NULL when not valid. The PsImageFile keeps ownership — do not delete it.

Notes:

  • Decode happens once, at natural size. Scaling to a cell is done at draw time by PaintImage, with high-quality bicubic interpolation, so one PsImageFile can be drawn into cells of different sizes without reloading.
  • .ico uses a fast path (LoadImageW → the HICON constructor) so the system picks the best-matching icon frame, then falls back to the generic file decoder if that fails.
  • Alpha is preserved. A transparent .png/.ico composites over whatever the control has already painted into the cell, so fill the cell background first if you want one.

Drawing — PsPaintBuffer.PaintImage

b.PaintImage( pImage as CGpImage ptr, rc as RECT ptr, nFit as long = PS_IMGFIT_ASPECT )
nFit Placement
PS_IMGFIT_ASPECT (default) Scale to fit inside rc, aspect preserved, then centre (letterbox).
PS_IMGFIT_STRETCH Fill rc exactly, aspect ignored.
PS_IMGFIT_CENTER Natural size, centred, clipped by rc if larger. No scaling.

PaintImage lives on PsPaintBuffer rather than on PsImageFile because it must draw through the buffer's own GDI+ graphics — a control building a second graphics object on the same device context would put two independently-batching GDI+ objects on one surface with nothing coordinating them.

Licence

Mozilla Public License 2.0.

MPL-2.0 is file-level copyleft, chosen deliberately for a drop-in control:

  • You may use this in closed-source software, commercial or otherwise. §3.2 permits static linking with no additional conditions.
  • If you modify these files, publish those files' changes. The obligation is per-file — your own sources are unaffected however tightly they are combined with these.
  • The Exhibit B "Incompatible With Secondary Licenses" notice is not applied, which keeps this GPL-compatible.

Clone this wiki locally