Answers to the questions you'll hit first, organized by category. Each answer includes a short explanation and links to the detailed docs.
- Getting started
- Controls and layout
- Events and interaction
- Data and templates
- Piping and streaming
- Visual designer
- Troubleshooting
- Advanced topics
Copy FormCast.dll into your TCC plugins directory, or load it on
demand from any path:
plugin /l C:\path\to\FormCast.dll
See the Quickstart for the full walkthrough.
Check @formversion[] — it returns the version string when loaded,
or empty when not:
set ver=%@formversion[]
iff "%ver" == "" then
echo Plugin not loaded!
endiff
Every example BTM includes this guard clause.
01-hello.btm — a label, a text
box, and an OK button in 15 lines. See also the Quickstart.
39 types covering basic input (including TOGGLE), numeric/date, lists/trees/grids, text, containers, app chrome (including SEPARATOR for toolbars), and specialized controls. See the Function Reference for the categorized list including MENUSTRIP, TOOLBAR, STATUSBAR, CONTEXTMENU, DATAGRID, TREEVIEW, WEBBROWSER, and more.
Call @FORMADD once per control, passing the control type, position,
and size. See 05-settings.btm
for a dialog with labels, text fields, radios, checkboxes, and buttons.
Use the MEMO control type instead of EDIT. It's a TextBox with
Multiline=true. See 08-memo-notepad.btm.
Set the readonly prop on a MEMO or RICHMEMO:
%@formset[%h,memo,readonly,1]
Use LISTVIEW with addcolumn (name, width, and sort type) then
additem for each row. Column types (text, number, date,
size) control how sorting works. See
04-file-picker.btm for a
complete navigable file picker.
Use the slash syntax in the control id: panel1/btn adds btn as a
child of panel1. The parent must be a PANEL type. See the
Architecture doc and the nested-layout screenshot
for the IDE-shaped pattern (toolbar + sidebar + editor + status bar).
Use the PROGRESSBAR control type with min, max, and value
props. See 03-progress.btm.
Use the COMBOBOX control type with additem for each option:
%@formadd[%h,cmb,COMBOBOX,100,12,120,24,]
%@formset[%h,cmb,style,list]
%@formset[%h,cmb,additem,Small]
%@formset[%h,cmb,additem,Medium]
%@formset[%h,cmb,additem,Large]
%@formset[%h,cmb,selectedindex,1]
Read the selection with %@formget[%h,cmb,selecteditem]. See
Function Reference: COMBOBOX.
Wrap each group in a GROUPBOX. Without GroupBox, WinForms treats
all RadioButtons on the same parent as one group — selecting one
deselects all others:
%@formadd[%h,grpTheme,GROUPBOX,12,12,180,80,Theme]
%@formadd[%h,grpTheme/rLight,RADIO,12,22,80,20,Light]
%@formadd[%h,grpTheme/rDark,RADIO,12,46,80,20,Dark]
%@formadd[%h,grpLang,GROUPBOX,200,12,180,80,Language]
%@formadd[%h,grpLang/rEN,RADIO,12,22,80,20,English]
%@formadd[%h,grpLang/rFR,RADIO,12,46,80,20,French]
Now selecting "Dark" doesn't deselect "English". See
14-tabbed-settings.btm.
Use TABCONTROL with TABPAGE children:
%@formadd[%h,tabs,TABCONTROL,8,8,400,260,]
%@formadd[%h,tabs/general,TABPAGE,0,0,0,0,General]
%@formadd[%h,tabs/general/lbl,LABEL,12,12,200,20,Some option]
%@formadd[%h,tabs/advanced,TABPAGE,0,0,0,0,Advanced]
Use NUMERICUPDOWN with min, max, and value props:
%@formadd[%h,nud,NUMERICUPDOWN,100,12,60,24,]
%@formset[%h,nud,min,1]
%@formset[%h,nud,max,100]
%@formset[%h,nud,value,4]
Use DATETIMEPICKER:
%@formadd[%h,dtp,DATETIMEPICKER,100,12,200,24,]
%@formset[%h,dtp,format,short]
Use RICHMEMO (a WPF RichTextBox) with the live-operation props:
%@formset[%h,rm,appendcolor,Error: file not found|Red]
%@formset[%h,rm,appendstyle,Build complete|bold]
%@formset[%h,rm,loadrules,ERROR|Red,WARN|Orange]
See Function Reference: RICHMEMO.
Use @FORMBIND to register a TCC command that runs when the event
fires:
%@formbind[%h,btnOK,click,gosub :on_ok]
The bound command runs on a worker thread so the script can call back
into @FORM* functions without deadlocking. See
01-hello.btm.
click, change, focus, blur, keypress, dblclick, close,
mousedown, mouseup, mouseenter, mouseleave, keydown,
keyup, resize, dragenter, dragdrop, scroll, columnclick,
cellclick, beforeexpand, afterexpand, beforecollapse,
aftercollapse. See
Function Reference: @FORMBIND.
Two patterns:
Pattern 1: do forever + bound commands (most common)
%@formbind[%h,btn,click,gosub :done]
%@formshow[%h]
do forever
delay 1
enddo
Pattern 2: on condition polling the events_pending bit
on condition %@formstate[%h] == 35 gosub :handle_event
Bit 32 = events_pending. See
07-events.btm and
Function Reference: @FORMSTATE.
Use @FORMSHOW[%h, modal] for wait-forever or @FORMSHOW[%h, modal:3000]
for a 3-second auto-dismiss. The return value is the DialogResult
integer. See 09-modal-dialog.btm.
Use @FORMTASKDIALOG — no need to build a form:
set rc=%@formtaskdialog[Delete file?,Are you sure?,yesnocancel,question]
Returns 0=yes, 1=no, 2=cancel. See
02-confirm.btm.
Use @FORMGET:
set name=%@formget[%h,txtName,text]
set picked=%@formget[%h,lst,selecteditem]
Bind dblclick on the LISTVIEW, read selecteditem, check isdir,
call cd + repopulate. See
04-file-picker.btm for the
full navigable pattern.
-
Set the
FORMCAST_DLLenvironment variable (once, in your TCC startup or system environment):set FORMCAST_DLL=C:\path\to\bin\FormCast.dll -
Add the
/apppattern at the top of your BTM (after loading the plugin):call formcast-check.btm load if "%1" == "/app" set RC=%@formconsole[hide] -
The
formcast-check.btmshared helper loads the plugin (or verifies it is already loaded), and handles unload/exit cleanup. When/appis passed,@FORMCONSOLE[hide]hides the TCC console window. Everything runs in a single TCC process.
From TCC (normal): myapp.btm -- runs in the current session.
As a standalone app: myapp.btm /app -- hides the TCC console.
The user sees only the FormCast window.
Desktop shortcut: For zero-flash launch, set the Target to:
"C:\path\to\tcc.exe" /c start /inv /pgm "C:\path\to\tcc.exe" /c "myapp.btm" /app,
Run "Minimized". The shortcut must target tcc.exe /c -- pointing
directly at a .btm file will not pass arguments correctly.
See 15-app-window.btm,
formcast-check.btm, and
Function Reference: @FORMCONSOLE.
Set the icon prop on the form before showing it:
%@formset[%h,.,icon,C:\path\to\myapp.ico]
Forms are hidden from the taskbar by default (most usage is utility dialogs under TCC). For standalone app windows, opt in:
%@formset[%h,.,showintaskbar,1]
Use MENUSTRIP with nested children via the slash-id syntax:
%@formadd[%h,menu,MENUSTRIP,0,0,0,0,]
%@formadd[%h,menu/file,LABEL,0,0,0,0,File]
%@formadd[%h,menu/file/open,LABEL,0,0,0,0,Open...]
%@formadd[%h,menu/file/exit,LABEL,0,0,0,0,Exit]
%@formbind[%h,exit,click,gosub :on_exit]
Text "-" creates a separator. See
Function Reference: MENUSTRIP.
Use TOOLBAR with button children:
%@formadd[%h,tb,TOOLBAR,0,0,0,0,]
%@formadd[%h,tb/btnNew,BUTTON,0,0,0,0,New]
%@formadd[%h,tb/sep1,LABEL,0,0,0,0,-]
%@formadd[%h,tb/btnOpen,BUTTON,0,0,0,0,Open]
Use STATUSBAR with label children:
%@formadd[%h,sb,STATUSBAR,0,0,0,0,]
%@formadd[%h,sb/msg,LABEL,0,0,0,0,Ready]
%@formset[%h,sb/msg,spring,1]
Set spring=1 on a panel to make it fill remaining space.
%@formnotify[show,My App]
%@formnotify[balloon,Alert,Build complete,info]
%@formnotify[hide]
Use CONTEXTMENU the same way as MENUSTRIP.
Use @FORMOPENDIALOG — one function call:
set file=%@formopendialog[Select a file,Text files:*.txt:All files:*.*]
iff "%file" != "" echo You picked: %file
Also available: @FORMSAVEDIALOG, @FORMFOLDERDIALOG,
@FORMCOLORDIALOG, @FORMFONTDIALOG. See
Function Reference: Common dialogs.
%@formsave[%h,myform.jsonc]
set h=%@formload[myform.jsonc]
With variable substitution:
set h=%@formload[picker.jsonc,prompt=Choose|width=500]
See 06-template.btm and
Template Reference.
JSONC (JSON with comments and trailing commas). The schema is at
schema/formcast-template.schema.json.
See Template Reference for the full spec.
%@formsaveimage[%h,screenshot.png]
Works in headless mode — no window flashes. See Tutorial section 8.
Use the FORMPIPE streaming command:
dir /s /b | FORMPIPE %h memo
type build.log | FORMPIPE %h rm Blue
For RICHMEMO, the optional third argument sets the text color. See
11-pipe-output.btm and
Function Reference: FORMPIPE.
Use the appendtext pseudo-prop on @FORMSET:
%@formset[%h,memo,appendtext,new line of text]
Works on both MEMO and RICHMEMO. Live-only (requires the form to
be realized via @FORMSHOW first).
Use the FORMEVENTS streaming command with do ... in /p:
do ev in /p formevents %h
echo got: %ev
enddo
Each event is one line: handle kind ctrl data. See
07-events.btm.
Yes. examples/designer/formcast-visual-designer.btm
is a full interactive designer with a three-window layout: Toolbox
(loaded from template), Canvas with 8-point resize handles and
snap-to-grid, and a Properties window with a PropertyGrid that
auto-updates on selection and applies edits live. Click a control
type in the Toolbox to add it to the canvas with a type-based ID
(Label1, Button1, etc.). Save the result as a JSONC template. See
the Designer Guide.
Yes. Set design_mode=1 on the form before showing it:
%@formset[%h,.,design_mode,1]
%@formshow[%h]
Click a control to select it (red dashed border). Drag to move it.
The final position is committed to the descriptor so @FORMSAVE
reflects the drag. See
10-designer-drag.btm
and the Designer Guide.
@FORMADD now works after @FORMSHOW. The new control appears
on the visible form immediately. This is what the visual designer
uses to add controls from the toolbox.
%@formset[%h,ctrl,delete,]
Removes the control from both the descriptor and the realized form.
%@formset[%h,ctrl,bringtofront,]
%@formset[%h,ctrl,sendtoback,]
Yes. Store bindings as _bind.* props:
%@formset[%h,btnOK,_bind.click,gosub :on_ok]
%@formsave[%h,myform.jsonc]
The loading BTM activates them explicitly:
set h=%@formload[myform.jsonc]
%@formapplybindings[%h]
%@formshow[%h]
The designer does NOT call @FORMAPPLYBINDINGS, so it can load
templates without having the target subroutines. See
Function Reference: Template event bindings.
%@formset[%h,ctrl,reparent,panel1] :: move into panel1
%@formset[%h,ctrl,reparent,] :: move to form root
Yes. In design mode, selecting a control shows 8-point resize handles (four corners + four edge midpoints). Drag any handle to resize. Snap-to-grid (default 8px) constrains the result. You can also resize programmatically:
%@formset[%h,btn,resizeby,20:10]
set pos=%@formget[%h,btn,position] :: returns "x:y"
%@formset[%h,btn,position,100:200] :: absolute set
%@formset[%h,btn,moveby,5:-3] :: delta move
Note the colon separator (not comma or pipe). See Function Reference: Designer primitives.
Ctrl+Click or Shift+Click additional controls after selecting the first one. The primary selection shows resize handles; secondary selections show blue dashed borders. Click the canvas background to deselect all. Right-clicking preserves the selection and opens the context menu.
Select 2+ controls (Ctrl+Click), then use:
- Edit > Align (or right-click > Align): Left, Right, Top, Bottom, Center Horizontally, Center Vertically
- Edit > Layout (or right-click > Layout): Distribute Horizontally/Vertically (3+ controls), Make Same Width/Height/Size (2+ controls)
Single-select alignment aligns to the canvas edge. Multi-select alignment aligns to the first selected control. The same operations are available from the toolbar (tb2 row), Edit menu, and context menu.
View > Grid Size in the designer menu. Options: Off (no snap), 4px (fine), 8px (default), 16px (coarse). The grid dots update immediately. Controls snap to the grid when dragged.
Select one or more controls (Ctrl+Click for multi-select), then:
- Ctrl+C (or Edit > Copy / toolbar Copy): copies to clipboard
- Ctrl+X (or Edit > Cut): copies and deletes the originals
- Ctrl+V (or Edit > Paste): pastes from clipboard
Copy/paste handles containers with children -- copying a Panel
copies everything inside it. Pasted controls get new IDs
automatically (Label1 -> Label2). Cut+paste reuses the
original ID since it was removed from the form.
Multi-select copy/cut copies all selected controls as a set. Paste adds them all at once with an offset from the originals.
Ctrl+Click to select multiple controls, then press Delete or use Edit > Delete. All selected controls are removed in one operation with a single undo snapshot.
Check that plugin /l actually succeeded — TCC may return exit=0
even when the .NET host fails silently. Look for error messages in
stderr. Common causes:
- Wrong .NET Framework version: FormCast requires net48 (4.8).
- Missing dependencies:
System.Text.Json.dlland its transitive deps must be in the same directory asFormCast.dll. The build copies them viaCopyLocalLockFileAssemblies. subst'd drive: .NET'sAssembly.LoadFromrejects paths throughsubst'd drives. Use%@truename[path]to resolve to the real path before passing toplugin /l.
Every @FORM* variable function returns a value. If you write it
as a bare statement without capturing the return, TCC expands it to
0 (success) and tries to execute 0 as a command:
:: WRONG - produces "Unknown command 0"
%@formset[%h,.,title,Hello]
:: CORRECT - capture the return value
set RC=%@formset[%h,.,title,Hello]
This applies to %@formset, %@formbind, %@formshow,
%@formnotify, and any other %@form* variable function.
Your value probably contains a comma, which ArgParser.Split treats
as an argument separator. Use colon (:) for structured values
(position 100:200, LISTVIEW addcolumn Name:280:text). Never use
pipe (|) in values that pass through set rc=%@formset[...] — TCC
eats it as a pipe operator.
You're using the wrong field separator. LISTVIEW accepts both | and
:, but : is the only one safe for BTM scripts that use
set rc=%@formset[...]. See
Function Reference: LISTVIEW.
Usually this means @FORMSHOW was called before all @FORMADD
calls. The realizer builds WinForms controls from the descriptor at
show time. Note: @FORMADD after @FORMSHOW does work -- the new
control appears on the visible form immediately (this is what the
designer uses). If controls still don't appear, check position/size
values.
- Verify the event name is correct (
click, notonclick). - Verify the control id matches exactly (case-insensitive).
- Remember that bound commands run on the worker thread, not the
script thread. The script needs a
do forever / delay 1loop to stay alive while waiting for events.
Set the FORMCAST_HEADLESS environment variable:
set FORMCAST_HEADLESS=1
All @FORMSHOW calls become no-ops, modal dialogs return immediately,
and @FORMSAVEIMAGE still works (off-screen rendering). See
Glossary: Headless mode.
%@formfocus[%h] :: focus the form
%@formfocus[TCC] :: focus the TCC console
See Function Reference: @FORMFOCUS.
set result=%@formsendmessage[%h,0x10,0,0]
:: 0x10 = WM_CLOSE
See Function Reference: @FORMSENDMESSAGE.
set id=%@formhittest[%h,150,80]
Returns the control id at that form-relative pixel position, or empty if nothing is there. Used by the designer to map mouse clicks to controls. See Function Reference: @FORMHITTEST.
Not yet in v1. The FormCast.Host.exe daemon is built and ships, but
the Global\ scope wiring that keeps forms alive across sessions is
planned for v1.1. All v1 forms are Local\ scoped and are destroyed
when the plugin unloads.
Every realized window is force-closed before Plugin.Shutdown
returns. This is the forced-shutdown contract -- a surviving window
would hold references into the unloaded assembly and crash TCC on
the next click. See Architecture: Forced shutdown.
%@formset[%h,.,theme,dark]
Theme values: system (default), dark, light. Switching is
live -- the form and all controls repaint immediately. The dark
title bar uses the DWM immersive dark mode attribute on Windows 10+.
See Function Reference: Theme.
%@formset[%h,lbl,forecolor,#00AAFF]
%@formset[%h,panel,backcolor,DarkBlue]
Use named colors (any System.Drawing.Color name or system color
like Highlight, HighlightText) or hex #RRGGBB. See
Function Reference: Appearance.
%@formset[%h,.,font,Segoe UI:11:bold]
%@formset[%h,memo,font,Consolas:10]
Format: family:size[:style]. Style is optional (bold, italic,
bold+italic). Setting the font on the form (.) propagates to
all child controls via ApplyFontRecursive.
Use the anchor property:
%@formset[%h,memo,anchor,top+bottom+left+right]
%@formset[%h,btn,anchor,bottom+right]
Anchored to opposite edges, the control stretches when the parent resizes. See Tutorial section 11.
%@formset[%h,btn,stockicon,FileNew]
Works on Button, Label, PictureBox, CheckBox, RadioButton, and
LinkLabel. List all available icons with FORMICONS. See
Function Reference: Stock icons.
%@formset[%h,.,acceptbutton,btnOK]
%@formset[%h,.,cancelbutton,btnCancel]
The shared helper that replaces formcast-load.btm. It handles
plugin load (checking if already loaded), unload, and exit cleanup.
When a BTM is launched from Explorer (%_parent contains
explorer), it performs plugin /u + exit on close for clean
teardown. All example BTMs use call formcast-check.btm load
instead of plugin /l directly.
The recommended way to handle events:
:loop
do ev in /p formevents %h
set KIND=%@word[" ",1,%ev]
set CTRL=%@word[" ",2,%ev]
iff "%KIND" == "click" .and. "%CTRL" == "btnOK" then
goto done
endiff
enddo
goto loop
This is preferred over @FORMBIND with gosub because gosub
from the CallbackWorker thread does not work reliably. All current
examples use this pattern.
%@formadd[%h,tgl,TOGGLE,12,12,50,24,]
%@formset[%h,tgl,checked,true]
TOGGLE is an on/off slider switch. It fires a change event with
true or false. Read state with %@formget[%h,tgl,checked].
%@formset[%h,memo,textfromfile,C:\data\log.txt]
This loads the file content directly, bypassing TCC's variable
expansion. Useful when the file contains % characters that would
otherwise be interpreted as variables.
%@formset[%h,lbl,textfromvar,MYVAR]
Reads the TCC variable at set-time and applies it as the control's text.