McBasic™Get McBasic™ ↗
Build 881
Hands-on tutorial · Input & events

Build a menu-driven, dynamic Classic workbench

Rebuild a Classic application with Menu Editor actions, runtime control arrays, control dragging, Type value copies, class initialization and Sub Main.

1 Form / control designs4 complete BASIC filesOriginal web lesson

Create the layout below first. In Design → Menu Editor, add Actions as a title; indent Show details and Clear status underneath it. Name them mnuActions, mnuDetails and mnuClear; set Show details Checked True and Shortcut Ctrl+D. Parent values in the JSON are item IDs, not BASIC names. Give txtCell Index 0 and Dock None: it is the design-time template. DragMode 1 on lblToken is the automatic control-drag source; pnlDrop uses the Source As Control overload, not DragData. Project Settings selects Classic; Startup Object selects Sub Main. Add modStart, SessionCounter and WorkbenchTests before replacing their full source files below. The Boolean mLoaded prevents duplicate Load and missing Unload. Only the newly loaded member is removed; the original template remains. The record test distinguishes copied values (79) from shared identity, while two class instances keep separate counters.

Step 1Prepare a safe practice project#

Extract the archive into a new writable folder, separate from your own application. Open ModernWorkbench.mcproj in McBasic™. Keep every declared source, layout, resource and migration file together.

To reconstruct it yourself, create a DesktopApp project and add the files declared below. In the complete project, follow these steps as a guided rebuild: replace each named source file once and Check after all its procedures are present.

Exact project configuration
ModernWorkbench.mcproj
{
  "schemaVersion": 1,
  "projectId": "mcbasic-web-modern-workbench",
  "name": "ModernWorkbench",
  "kind": "DesktopApp",
  "language": {
    "dialect": "McBasic1",
    "requireExplicit": true,
    "compare": "Binary",
    "defaultArrayBase": 0
  },
  "application": {
    "bundleIdentifier": "com.mcbasic.web.modernworkbench",
    "version": "1.0.0",
    "buildNumber": "1",
    "startupObject": "SubMain",
    "shutdownMode": "OnLastFormClosed",
    "appearance": "Classic",
    "company": "Learning project",
    "copyright": "Tutorial example"
  },
  "forms": [
    {
      "id": "main",
      "typeName": "frmMain",
      "layout": "frmMain.form.json",
      "code": "frmMain.bas"
    }
  ],
  "modules": [
    {
      "name": "modStart",
      "path": "modStart.bas"
    }
  ],
  "classes": [
    {
      "name": "SessionCounter",
      "path": "SessionCounter.cls"
    }
  ],
  "tests": [
    {
      "name": "WorkbenchTests",
      "path": "WorkbenchTests.bas",
      "kind": "unit"
    }
  ],
  "references": [],
  "resources": [],
  "targets": [
    "osx-arm64"
  ],
  "capabilities": []
}

Step 2Build the Form and set its properties#

frmMain

Select the Form first and set its properties. Add containers before their children. These are initial Designer values, not statements to paste into a procedure.

Form propertyValue
Caption"Modern Workbench"
Width640
Height420
BackColor"ButtonFace"
TypeNameParent
TextBoxtxtCellForm
CommandButtoncmdAddForm
CommandButtoncmdRemoveForm
CommandButtoncmdValueForm
CommandButtoncmdPopupForm
LabellblTokenForm
PanelpnlDropForm
LabellblStatusForm
MenuItemmnuActionsComponent tray
MenuItemmnuDetailsComponent tray
MenuItemmnuClearComponent tray
txtCell · exact Inspector values
PropertyValue
Index0
Left24
Top24
Width160
Height28
Text"Template"
Dock"None"
cmdAdd · exact Inspector values
PropertyValue
Left24
Top110
Width130
Height32
Caption"Load cell 1"
cmdRemove · exact Inspector values
PropertyValue
Left164
Top110
Width130
Height32
Caption"Unload cell 1"
cmdValue · exact Inspector values
PropertyValue
Left304
Top110
Width130
Height32
Caption"Test value copy"
cmdPopup · exact Inspector values
PropertyValue
Left444
Top110
Width160
Height32
Caption"Open action menu"
lblToken · exact Inspector values
PropertyValue
Left24
Top180
Width160
Height40
Caption"Drag this label"
DragMode1
pnlDrop · exact Inspector values
PropertyValue
Left220
Top170
Width380
Height90
BackColor"WindowBackground"
lblStatus · exact Inspector values
PropertyValue
Left24
Top290
Width580
Height70
Caption"Ready"
WordWrapTrue
mnuActions · exact Inspector values
PropertyValue
Caption"&Actions"
Order0
mnuDetails · exact Inspector values
PropertyValue
Caption"&Show details"
Parent"actions"
Order0
Shortcut"Ctrl+D"
CheckedTrue
mnuClear · exact Inspector values
PropertyValue
Caption"&Clear status"
Parent"actions"
Order1

Step 3Connect the events explicitly#

frmMain

ObjectEventProcedureParameters
txtCellChangetxtCell_ChangeChange(ByVal Index As Integer)
cmdAddClickcmdAdd_ClickClick()
cmdRemoveClickcmdRemove_ClickClick()
cmdValueClickcmdValue_ClickClick()
cmdPopupClickcmdPopup_ClickClick()
pnlDropDragOverpnlDrop_DragOverDragOver(ByVal Source As Control, ByVal X As Single, ByVal Y As Single, ByVal State As Integer)
pnlDropDragDroppnlDrop_DragDropDragDrop(ByVal Source As Control, ByVal X As Single, ByVal Y As Single)
mnuDetailsClickmnuDetails_ClickClick()
mnuClearClickmnuClear_ClickClick()
FormLoadForm_Load()

Step 4Enter the complete behavior#

Open the named source file. Enter declarations first, then every procedure. Expand a procedure for its full body, or copy the complete file. Helpers are required: do not stop after entering the first Click handler.

frmMain.bas

Enter these declarations at file scope, before the procedures.

frmMain.bas · declarations
Option Explicit
Private mLoaded As Boolean
Private mSession As SessionCounter
Private Sub Form_Load()

Initialize state before enabling dependent actions. Keep the shared declarations and startup dependencies with this procedure.

frmMain.bas · Form_Load
Private Sub Form_Load()
    Set mSession = New SessionCounter
    mLoaded = False
    cmdRemove.Enabled = False
    lblStatus.Caption = "Ready; " & txtCell.Count & " cell. " & App.CompanyName
End Sub
Private Sub cmdAdd_Click()

Enter the complete action, including its input checks and calls to helper procedures.

frmMain.bas · cmdAdd_Click
Private Sub cmdAdd_Click()
    If mLoaded Then Exit Sub
    Load txtCell(1)
    mLoaded = True
    txtCell(1).Top = 64
    txtCell(1).Text = "New member"
    txtCell(1).Visible = True
    cmdAdd.Enabled = False
    cmdRemove.Enabled = True
    lblStatus.Caption = "Loaded; " & txtCell.Count & " cells."
End Sub
Private Sub cmdRemove_Click()

Enter the complete action, including its input checks and calls to helper procedures.

frmMain.bas · cmdRemove_Click
Private Sub cmdRemove_Click()
    If Not mLoaded Then Exit Sub
    Unload txtCell(1)
    mLoaded = False
    cmdAdd.Enabled = True
    cmdRemove.Enabled = False
    lblStatus.Caption = "Removed; " & txtCell.Count & " cell remains."
End Sub
Private Sub txtCell_Change(ByVal Index As Integer)

Read the current value or selection and update its dependent interface. The binding above determines which object calls this handler.

frmMain.bas · txtCell_Change
Private Sub txtCell_Change(ByVal Index As Integer)
    lblStatus.Caption = "Cell " & Index & ": " & txtCell(Index).Text
End Sub
Private Sub cmdValue_Click()

Enter the complete action, including its input checks and calls to helper procedures.

frmMain.bas · cmdValue_Click
Private Sub cmdValue_Click()
    mSession.Increment
    lblStatus.Caption = "Copy proof: " & CopyProof() & "; clicks: " & mSession.Count
End Sub
Private Sub cmdPopup_Click()

Enter the complete action, including its input checks and calls to helper procedures.

frmMain.bas · cmdPopup_Click
Private Sub cmdPopup_Click()
    PopupMenu mnuActions
End Sub
Private Sub mnuDetails_Click()

Enter the complete action, including its input checks and calls to helper procedures.

frmMain.bas · mnuDetails_Click
Private Sub mnuDetails_Click()
    mnuDetails.Checked = Not mnuDetails.Checked
    lblStatus.Visible = mnuDetails.Checked
End Sub
Private Sub mnuClear_Click()

Enter the complete action, including its input checks and calls to helper procedures.

frmMain.bas · mnuClear_Click
Private Sub mnuClear_Click()
    lblStatus.Caption = "Ready"
End Sub
Private Sub pnlDrop_DragOver(ByVal Source As Control, ByVal X As Single, ByVal Y As Single, ByVal State As Integer)

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

frmMain.bas · pnlDrop_DragOver
Private Sub pnlDrop_DragOver(ByVal Source As Control, ByVal X As Single, ByVal Y As Single, ByVal State As Integer)
    If State = vbEnter Then lblStatus.Caption = "Release to identify " & Source.Name
End Sub
Private Sub pnlDrop_DragDrop(ByVal Source As Control, ByVal X As Single, ByVal Y As Single)

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

frmMain.bas · pnlDrop_DragDrop
Private Sub pnlDrop_DragDrop(ByVal Source As Control, ByVal X As Single, ByVal Y As Single)
    lblStatus.Caption = "Dropped " & Source.Name & " at " & X & ", " & Y
End Sub
Copy the complete frmMain.bas file

Replace the file once; do not append duplicate procedures.

frmMain.bas
Option Explicit
Private mLoaded As Boolean
Private mSession As SessionCounter

Private Sub Form_Load()
    Set mSession = New SessionCounter
    mLoaded = False
    cmdRemove.Enabled = False
    lblStatus.Caption = "Ready; " & txtCell.Count & " cell. " & App.CompanyName
End Sub

Private Sub cmdAdd_Click()
    If mLoaded Then Exit Sub
    Load txtCell(1)
    mLoaded = True
    txtCell(1).Top = 64
    txtCell(1).Text = "New member"
    txtCell(1).Visible = True
    cmdAdd.Enabled = False
    cmdRemove.Enabled = True
    lblStatus.Caption = "Loaded; " & txtCell.Count & " cells."
End Sub

Private Sub cmdRemove_Click()
    If Not mLoaded Then Exit Sub
    Unload txtCell(1)
    mLoaded = False
    cmdAdd.Enabled = True
    cmdRemove.Enabled = False
    lblStatus.Caption = "Removed; " & txtCell.Count & " cell remains."
End Sub

Private Sub txtCell_Change(ByVal Index As Integer)
    lblStatus.Caption = "Cell " & Index & ": " & txtCell(Index).Text
End Sub

Private Sub cmdValue_Click()
    mSession.Increment
    lblStatus.Caption = "Copy proof: " & CopyProof() & "; clicks: " & mSession.Count
End Sub

Private Sub cmdPopup_Click()
    PopupMenu mnuActions
End Sub

Private Sub mnuDetails_Click()
    mnuDetails.Checked = Not mnuDetails.Checked
    lblStatus.Visible = mnuDetails.Checked
End Sub

Private Sub mnuClear_Click()
    lblStatus.Caption = "Ready"
End Sub

Private Sub pnlDrop_DragOver(ByVal Source As Control, ByVal X As Single, ByVal Y As Single, ByVal State As Integer)
    If State = vbEnter Then lblStatus.Caption = "Release to identify " & Source.Name
End Sub

Private Sub pnlDrop_DragDrop(ByVal Source As Control, ByVal X As Single, ByVal Y As Single)
    lblStatus.Caption = "Dropped " & Source.Name & " at " & X & ", " & Y
End Sub

modStart.bas

Enter these declarations at file scope, before the procedures.

modStart.bas · declarations
Option Explicit

Public Type Reading
    Name As String
    Value As Long
End Type
Public Sub Main()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

modStart.bas · Main
Public Sub Main()
    frmMain.Show
End Sub
Public Function CopyProof() As Long

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

modStart.bas · CopyProof
Public Function CopyProof() As Long
    Dim original As Reading
    Dim edited As Reading
    original.Name = "Sample"
    original.Value = 7
    edited = original
    With edited
        .Value = 9
    End With
    CopyProof = original.Value * 10 + edited.Value
End Function
Copy the complete modStart.bas file

Replace the file once; do not append duplicate procedures.

modStart.bas
Option Explicit

Public Type Reading
    Name As String
    Value As Long
End Type

Public Sub Main()
    frmMain.Show
End Sub

Public Function CopyProof() As Long
    Dim original As Reading
    Dim edited As Reading
    original.Name = "Sample"
    original.Value = 7
    edited = original
    With edited
        .Value = 9
    End With
    CopyProof = original.Value * 10 + edited.Value
End Function

SessionCounter.cls

Enter these declarations at file scope, before the procedures.

SessionCounter.cls · declarations
Option Explicit
Private mCount As Long
Private Sub Class_Initialize()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

SessionCounter.cls · Class_Initialize
Private Sub Class_Initialize()
    mCount = 0
End Sub
Public Sub Increment()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

SessionCounter.cls · Increment
Public Sub Increment()
    mCount = mCount + 1
End Sub
Public Property Get Count() As Long

Keep this public property accessor with its complementary accessor and the fields or controls it uses.

SessionCounter.cls · Count
Public Property Get Count() As Long
    Count = mCount
End Property
Copy the complete SessionCounter.cls file

Replace the file once; do not append duplicate procedures.

SessionCounter.cls
Option Explicit
Private mCount As Long

Private Sub Class_Initialize()
    mCount = 0
End Sub

Public Sub Increment()
    mCount = mCount + 1
End Sub

Public Property Get Count() As Long
    Count = mCount
End Property

WorkbenchTests.bas

Enter these declarations at file scope, before the procedures.

WorkbenchTests.bas · declarations
Option Explicit
Public Sub Test_ValueCopy()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

WorkbenchTests.bas · Test_ValueCopy
Public Sub Test_ValueCopy()
    Assert.AreEqual 79, CopyProof()
End Sub
Public Sub Test_IndependentClasses()

Enter this helper in the named source file, outside every other procedure. Its callers are included in the same project.

WorkbenchTests.bas · Test_IndependentClasses
Public Sub Test_IndependentClasses()
    Dim first As SessionCounter
    Dim second As SessionCounter
    Set first = New SessionCounter
    Set second = New SessionCounter
    first.Increment
    Assert.AreEqual 1, first.Count
    Assert.AreEqual 0, second.Count
End Sub
Copy the complete WorkbenchTests.bas file

Replace the file once; do not append duplicate procedures.

WorkbenchTests.bas
Option Explicit

Public Sub Test_ValueCopy()
    Assert.AreEqual 79, CopyProof()
End Sub

Public Sub Test_IndependentClasses()
    Dim first As SessionCounter
    Dim second As SessionCounter
    Set first = New SessionCounter
    Set second = New SessionCounter
    first.Increment
    Assert.AreEqual 1, first.Count
    Assert.AreEqual 0, second.Count
End Sub

Step 5Keep resources and storage reproducible#

No external resource or migration files are declared. Any inline migration, seed data or local JSON is present in the code above. Runtime application data and private preferences are not included in the archive.

Step 6Check, run and test the result#

Save all files and run Check. Resolve diagnostics before Run. Developer CLI users can work from the extracted folder:

Developer CLI
mcbasic check ModernWorkbench.mcproj
mcbasic run ModernWorkbench.mcproj
mcbasic test ModernWorkbench.mcproj

Acceptance checklist

  • Check the complete project, then run Test: both unit tests pass.
  • Run: the startup procedure opens one Classic window; the initial label reports one cell.
  • Load cell 1: a second field becomes visible below the template. Typing in it reports Cell 1. Add is disabled.
  • Unload cell 1: it disappears, Count returns to one, and Load becomes available again. Repeat twice.
  • Test value copy twice: the proof stays 79 and the class counter advances from 1 to 2.
  • Use Actions → Show details and its shortcut: the checkmark and label visibility agree. Restore visibility, then test the popup button and Clear status.
  • Drag the label onto the white panel: expect Dropped lblToken with coordinates. The label itself intentionally does not move.
  • Close the window: the Sub Main application ends. Change Appearance to macOS and repeat the interaction checks.

Step 7Make one change and repeat the tests#

Add another menu command that calls the existing copy-proof helper. Do not call an event handler directly; share the calculation and preserve the original two tests.

Save a working Git checkpoint first. Change one behavior, Check again and repeat the acceptance checklist. Compare with the downloaded solution if a regression appears.

Source baseline 20f4bfed6b · Build manifest
Sources & maintenance

Original web explanation, reviewed against these repository sources. API tables and example files follow the implementation.

  • tests/McBasic.Compiler.Tests/UserTypeChecks.cs
  • tests/McBasic.Compiler.Tests/Vb6ScreenClassChecks.cs
  • tests/McBasic.Compiler.Tests/SubMainChecks.cs
  • tests/McBasic.Compiler.Tests/ApplicationObjectChecks.cs
  • src/McBasic.Runtime/McApp.cs
  • src/McBasic.Compiler/Binder.cs
  • src/McBasic.Workspace/MenuRules.cs
  • src/McBasic.Workspace/MenuShortcuts.cs
  • macos/McBasicIDE/Sources/MenuEditorWindow.swift
  • tests/McBasic.Compiler.Tests/MenuCompilerChecks.cs
  • tests/McBasic.Compiler.Tests/PopupMenuChecks.cs
  • macos/McBasicIDE/Sources/ProjectSettingsSheet.swift
  • src/McBasic.Workspace/ProjectReader.cs
  • tests/McBasic.Compiler.Tests/ApplicationAppearanceChecks.cs
  • tests/McBasic.Compiler.Tests/AppIconChecks.cs
  • tests/McBasic.Compiler.Tests/VbControlDragChecks.cs
  • src/McBasic.Runtime/McSystemColors.cs
  • src/McBasic.Runtime.UI/McControlArray.cs
  • site/examples/ModernWorkbench/ModernWorkbench.mcproj
  • site/examples/ModernWorkbench/SessionCounter.cls
  • site/examples/ModernWorkbench/WorkbenchTests.bas
  • site/examples/ModernWorkbench/frmMain.bas
  • site/examples/ModernWorkbench/frmMain.form.json
  • site/examples/ModernWorkbench/modStart.bas