Welcome!

By registering with us, you'll be able to discuss, share and private message with other members of our community.

SignUp Now!

FormCast v1.0 – GUI Framework for TCC v36 .NET Plugins

Hi all,

With the release of TCC v36 and .NET plugin support, I wanted to share something I’ve been working on.

FormCast is a .NET plugin that allows BTM batch scripts to create and interact with native Windows GUI forms.

The idea is to add a structured UI layer on top of TCC scripts so they can be used to build interactive tools and run as standalone desktop applications.

Some highlights:

  • 39 control types and 6 common dialogs
  • multiple layout systems (absolute, flow, grid, dock)
  • event-driven scripting via FORMEVENTS and @FORMBIND
  • FORMPIPE to stream command output into UI controls
  • standalone app mode (/app) that hides the console
  • a visual designer implemented in BTM

The README includes screenshots (generated headlessly) and example scripts ranging from simple dialogs to more complex layouts.

GitHub (source, docs, examples, release zip):

Quickstart:

Thanks,
Tim
 
Very impressive!

A few findings...

I run TCC as a stand-alone window.

Thus, I had to change this line in formcast-check.btm
Code:
if "%@index[%_parent,explorer]" != "-1" exit
...as it kept closing my TCC session.

Not necessary if running TCC from Windows Terminal.

Cannot get formcast-visual-designer.btm to work yet;
Code:
E:\...\designer>formcast-visual-designer.btm
ERROR: Failed to load toolbox template.

Checked to make sure file exists...
Code:
:: Toolbox loaded from template
if exist %_batchpath\templates\toolbox.jsonc echo Yes
set hTool=%@formload[%_batchpath\templates\toolbox.jsonc]

...but...

Code:
E:\...\designer>formcast-visual-designer.btm
Yes
ERROR: Failed to load toolbox template.

Other examples I have tried work as they should.

Joe
 
Thanks, Joe. I was trying to balance the different ways of starting TCC. I usually start my TCC sessions under TCMD except for one elevated session at work. But, I wanted to also be able to double-click the .btm in Windows Explorer to launch. And, I also wanted to be able to launch a 'Windows App' from a shortcut. I was testing the designer that way without another TCC nor TCMD active to verify the app icon in the taskbar was working. Like this:
1776960231109.webp


If you have the FORMCAST_DLL path env var set, that should work.

If you can, please document the issues on github so I can track them and fix them when I get a chance.

Thanks again.

Tim
 
Very cool plugin Tim! Thanks for building and sharing!

Just started playing around with it. I can definitely see some use cases. First thing that comes to mind is creating "command dialog" functionality for a couple of my .BTMs with multiple command line parameters (which I can never remember) that control script behavior.
 

I am familiar with TaskDialog and have used it for certain use cases. But in this particular situation it doesn't provide the variety of controls needed to do something like this:

1777233681869.webp
 
Here's a (not polished) sample dialog similar to yours,
but written using PowerShell 5.1

1777239546782.webp


Code:
R:\>pshell test.ps1
StartDate     : 2026-04-26 12:00:00 AM
LookAheadDays : 0
IncludeHome   : False
IncludeWork   : False
OutputTarget  : Screen

Code:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

#-------------------------
# Form
#-------------------------
$form               = New-Object System.Windows.Forms.Form
$form.Text          = 'Canvas - BDaysTest.jsonc'
$form.StartPosition = 'CenterScreen'
$form.Size          = New-Object System.Drawing.Size(520,260)
$form.FormBorderStyle = 'FixedDialog'
$form.MaximizeBox   = $false
$form.MinimizeBox   = $false

#-------------------------
# Start Date
#-------------------------
$lblStartDate              = New-Object System.Windows.Forms.Label
$lblStartDate.Text         = 'Start Date'
$lblStartDate.AutoSize     = $true
$lblStartDate.Location     = New-Object System.Drawing.Point(15,15)
$form.Controls.Add($lblStartDate)

$dtpStart                  = New-Object System.Windows.Forms.DateTimePicker
$dtpStart.Format           = 'Long'
$dtpStart.Location         = New-Object System.Drawing.Point(15,35)
$dtpStart.Width            = 200
$dtpStart.Value            = Get-Date '2026-04-26'
$form.Controls.Add($dtpStart)

# Optional: label to show day-of-week explicitly
$lblDow                    = New-Object System.Windows.Forms.Label
$lblDow.AutoSize           = $true
$lblDow.Location           = New-Object System.Drawing.Point(205,60)
$lblDow.Text               = $dtpStart.Value.ToString('dddd')
$form.Controls.Add($lblDow)

$dtpStart.Add_ValueChanged({
    $lblDow.Text = $dtpStart.Value.ToString('dddd')
})

#-------------------------
# Look Ahead Days
#-------------------------
$lblLookAhead              = New-Object System.Windows.Forms.Label
$lblLookAhead.Text         = 'Look Ahead Days'
$lblLookAhead.AutoSize     = $true
$lblLookAhead.Location     = New-Object System.Drawing.Point(15,75)
$form.Controls.Add($lblLookAhead)

$numLookAhead              = New-Object System.Windows.Forms.NumericUpDown
$numLookAhead.Location     = New-Object System.Drawing.Point(130,72)
$numLookAhead.Width        = 60
$numLookAhead.Minimum      = 0
$numLookAhead.Maximum      = 365
$numLookAhead.Value        = 0
$form.Controls.Add($numLookAhead)

#-------------------------
# Include (checkboxes)
#-------------------------
$grpInclude                = New-Object System.Windows.Forms.GroupBox
$grpInclude.Text           = 'Include'
$grpInclude.Location       = New-Object System.Drawing.Point(15,110)
$grpInclude.Size           = New-Object System.Drawing.Size(200,80)
$form.Controls.Add($grpInclude)

$chkHome                   = New-Object System.Windows.Forms.CheckBox
$chkHome.Text              = 'Home reminders'
$chkHome.AutoSize          = $true
$chkHome.Location          = New-Object System.Drawing.Point(10,25)
$grpInclude.Controls.Add($chkHome)

$chkWork                   = New-Object System.Windows.Forms.CheckBox
$chkWork.Text              = 'Work Reminders'
$chkWork.AutoSize          = $true
$chkWork.Location          = New-Object System.Drawing.Point(10,45)
$grpInclude.Controls.Add($chkWork)

#-------------------------
# Output to (radio buttons)
#-------------------------
$grpOutput                 = New-Object System.Windows.Forms.GroupBox
$grpOutput.Text            = 'Output to:'
$grpOutput.Location        = New-Object System.Drawing.Point(240,15)
$grpOutput.Size            = New-Object System.Drawing.Size(250,175)
$form.Controls.Add($grpOutput)

$rbScreen                  = New-Object System.Windows.Forms.RadioButton
$rbScreen.Text             = 'Screen'
$rbScreen.AutoSize         = $true
$rbScreen.Location         = New-Object System.Drawing.Point(10,25)
$rbScreen.Checked          = $true
$grpOutput.Controls.Add($rbScreen)

$rbSticky                  = New-Object System.Windows.Forms.RadioButton
$rbSticky.Text             = 'Sticky Note'
$rbSticky.AutoSize         = $true
$rbSticky.Location         = New-Object System.Drawing.Point(10,45)
$grpOutput.Controls.Add($rbSticky)

$rbMJG                     = New-Object System.Windows.Forms.RadioButton
$rbMJG.Text                = 'Push to MJG'
$rbMJG.AutoSize            = $true
$rbMJG.Location            = New-Object System.Drawing.Point(10,65)
$grpOutput.Controls.Add($rbMJG)

$rbJMG                     = New-Object System.Windows.Forms.RadioButton
$rbJMG.Text                = 'Push to JMG'
$rbJMG.AutoSize            = $true
$rbJMG.Location            = New-Object System.Drawing.Point(10,85)
$grpOutput.Controls.Add($rbJMG)

$rbGibFamily               = New-Object System.Windows.Forms.RadioButton
$rbGibFamily.Text          = 'Push to GibFamily'
$rbGibFamily.AutoSize      = $true
$rbGibFamily.Location      = New-Object System.Drawing.Point(10,105)
$grpOutput.Controls.Add($rbGibFamily)

#-------------------------
# OK / Cancel buttons
#-------------------------
$btnOK                     = New-Object System.Windows.Forms.Button
$btnOK.Text                = 'OK'
$btnOK.Size                = New-Object System.Drawing.Size(80,25)
$btnOK.Location            = New-Object System.Drawing.Point(310,200)
$btnOK.DialogResult        = [System.Windows.Forms.DialogResult]::OK
$form.AcceptButton         = $btnOK
$form.Controls.Add($btnOK)

$btnCancel                 = New-Object System.Windows.Forms.Button
$btnCancel.Text            = 'Cancel'
$btnCancel.Size            = New-Object System.Drawing.Size(80,25)
$btnCancel.Location        = New-Object System.Drawing.Point(400,200)
$btnCancel.DialogResult    = [System.Windows.Forms.DialogResult]::Cancel
$form.CancelButton         = $btnCancel
$form.Controls.Add($btnCancel)

#-------------------------
# Show dialog and capture result
#-------------------------
$result = $form.ShowDialog()

if ($result -eq [System.Windows.Forms.DialogResult]::OK) {
    $selectedDate   = $dtpStart.Value
    $lookAheadDays  = [int]$numLookAhead.Value
    $includeHome    = $chkHome.Checked
    $includeWork    = $chkWork.Checked

    if     ($rbScreen.Checked)    { $outputTarget = 'Screen' }
    elseif ($rbSticky.Checked)    { $outputTarget = 'Sticky Note' }
    elseif ($rbMJG.Checked)       { $outputTarget = 'Push to MJG' }
    elseif ($rbJMG.Checked)       { $outputTarget = 'Push to JMG' }
    elseif ($rbGibFamily.Checked) { $outputTarget = 'Push to GibFamily' }

    # For now just dump the selections; wire this into your pipeline as needed.
    [PSCustomObject]@{
        StartDate      = $selectedDate
        LookAheadDays  = $lookAheadDays
        IncludeHome    = $includeHome
        IncludeWork    = $includeWork
        OutputTarget   = $outputTarget
    }
}

Joe
 
Here's a (not polished) sample dialog similar to yours,
but written using PowerShell 5.1

Lots of options out there and what you created seems like a very viable alternative...especially for people already proficient in PowerShell programming.

Still playing with Tim's plugin, and I like what I've seen so far.
 
Thanks for the bug reports — v1.1.0 is up with fixes for both issues.

Unload closing your TCC session: formcast-check.btm was checking if Explorer was the parent process, which is true for any TCC window launched from the Start Menu or a shortcut. It now checks %_transient instead, so only double-click-spawned windows auto-close on unload. Persistent sessions are left alone.

Toolbox template failing to load: The designer was passing the raw %_batchpath to @FORMLOAD, which hands it to .NET's File.ReadAllText. On SUBST drives or mapped shares, TCC can resolve the path internally (so IF EXIST succeeds) but .NET can't. All @FORMLOAD calls now go through @TRUENAME first, matching the pattern already used for PLUGIN /L in formcast-check.btm.

Also flattened the release zip — DLLs are in the root now instead of a bin\ subfolder, so the install is just extract and point FORMCAST_DLL at FormCast.dll.

Download: Release v1.1.0 · Tim-Butterfield/FormCast
 
Hi @TimButterfield - I reported several issues on Github since I saw you requested that Joe log his issues there.
Were you able to look at those issues to see if they are addressable?
 
Thanks for the bug reports (GitHub issues #1–#4) — v1.1.1 is up with fixes for all of them.

Template loading (issue #1): In addition to the BTM-side @truename fix in v1.1.0, the plugin now normalizes all file paths internally via Path.GetFullPath() before calling .NET file APIs. This covers @FORMLOAD, @FORMIMPORT, @FORMSAVE, @FORMSAVEIMAGE, and @FORMSAVECOMPOSITE, so double backslashes, SUBST drives, and mapped shares work without needing %@truename[] on the BTM side.

CheckBox and Radio pre-show initialization (issues #2, #4): @FORMSET for the checked property before @FORMSHOW now works correctly. Previously, CHECKBOX and RADIO ignored the property bag during control realization — TOGGLE and NUMERICUPDOWN already handled this, and now CHECKBOX and RADIO follow the same pattern. @FORMGET and @FORMSET for checked also work on RADIO controls now (previously only CHECKBOX and TOGGLE were wired up). To set the initially selected radio button:
set RC=%@formadd[%h,grp,GROUPBOX,12,12,200,80,Pick one]
set RC=%@formadd[%h,grp/r1,RADIO,10,24,80,20,Alpha]
set RC=%@formadd[%h,grp/r2,RADIO,10,48,80,20,Beta]
set RC=%@formset[%h,grp/r1,checked,true]
And to read which one is selected after the form closes:
set sel=%@formget[%h,grp/r1,checked]
Returns true or false.

DateTimePicker value (issue #3): You can now set and read the date via the value property. It accepts ISO 8601 or common date strings for setting, and returns ISO 8601 on read:
set RC=%@formset[%h,dtp,value,2026-06-15]
set dt=%@formget[%h,dtp,value]

GroupBox Z-order note: The overlay issue MGibs reported (GROUPBOX hiding radio buttons when added first) is standard WinForms Z-order behavior. The fix is to nest radio buttons inside the GROUPBOX using the slash-id syntax (grp/r1) with coordinates relative to the GroupBox client area — not to add them as siblings at the form level.

Documentation for CHECKBOX, RADIO, and DATETIMEPICKER properties has been added to the Function Reference and Template Reference docs.

Download: Release v1.1.1 · Tim-Butterfield/FormCast

Please log any other issues you find.

Thanks,
Tim
 
Back
Top