Tutorial 02 — GUI Basics

Your First Perl/Tk
Windows

Build real GUI windows on macOS: labels, buttons, text entry, frames, live data binding, and second windows — all from scratch.

perl perl_gui_01_basics.pl

What This Tutorial Covers

  • MainWindow → geometry → title → MainLoop
  • Label with -text and live -textvariable binding
  • Button with named and anonymous callbacks
  • Button -state: normal / active / disabled
  • Entry for single-line input and password (-show)
  • Frame and LabelFrame as containers
  • configure() to change options at runtime
  • cget() to read the current value of any option
  • Toplevel — opening a second window
  • Binding <Return> to move focus between fields
Perl/Tk — Basics Tutorial
Perl/Tk Basics Tutorial
Label Examples
Click the button below to update me.
0
Button Examples
Update Label Increment Counter Reset
Entry Examples
Username: alice
Password: ●●●●●●●●
Login
§22

Program Structure & the Event Loop

Every Perl/Tk program follows the same three-step pattern. Build your widgets, then hand control to MainLoop. From that point on, your callbacks drive everything.

perl
#!/usr/bin/env perl
use strict;
use warnings;
use Tk;

# Step 1 — Create the main window
my $mw = MainWindow->new;
$mw->title("Perl/Tk — Basics Tutorial");
$mw->geometry("520x620+100+50");   # WxH+X+Y
$mw->resizable(1, 1);               # (allow-x, allow-y)
$mw->configure(-background => "#f0f0f0");

# Step 2 — Create widgets (see sections below)
# ...

# Step 3 — Enter the event loop (never returns until window closes)
MainLoop;
MainLoop never returns — all code after it is unreachable. Place all widget creation before MainLoop. Your callbacks are what run afterwards.
§23

Labels — Static & Dynamic

Labels display text or images. A static label never changes. A dynamic label is bound to a Perl variable with -textvariable — when the variable changes, the display updates instantly and automatically.

perl
# Static label — text set once, never changes
$mw->Label(
    -text       => "Static label: never changes.",
    -font       => "Helvetica 11",
    -foreground => "#333333",
    -background => "#f0f0f0",
    -anchor     => "w",    # west = left-align text
)->pack(-fill => "x", -padx => 10);

# Dynamic label — linked to $dynamic_msg via -textvariable
my $dynamic_msg = "Click the button below to update me.";
$mw->Label(
    -textvariable => \$dynamic_msg,   # NOTE: pass a REFERENCE (\$var)
    -font         => "Helvetica 11 italic",
    -foreground   => "#007aff",
    -background   => "#f0f0f0",
    -wraplength   => 460,              # Wrap at 460 pixels wide
)->pack(-fill => "x", -padx => 10);

# Counter label — shows a number that increments
my $counter_val = 0;
$mw->Label(
    -textvariable => \$counter_val,
    -font         => "Helvetica 24 bold",
    -foreground   => "#34c759",
    -background   => "#f0f0f0",
)->pack(-pady => 5);

# Later, in a button callback:
$dynamic_msg = "Now I show something different!";  # Label updates automatically!
$counter_val++;                                     # Counter label updates too!
-text

Fixed string — set once at creation

-textvariable

Reference to a Perl variable — updates automatically

-wraplength

Pixel width at which text wraps to next line

-anchor

Text alignment: w=left, e=right, center

§23

Buttons & Callbacks

Every button has a -command option that points to a subroutine. You can use a reference to a named sub (\&my_sub) or an anonymous sub (sub { ... }).

perl
my $btn_row = $frame->Frame(-background => "#f0f0f0")->pack(-fill => "x");

# Named callback — defined as a sub below
$btn_row->Button(
    -text       => "Update Label",
    -font       => "Helvetica 11",
    -background => "#007aff",
    -foreground => "white",
    -relief     => "flat",
    -padx       => 12, -pady => 5,
    -command    => \&on_update_label,   # Reference to named sub
)->pack(-side => "left", -padx => 5);

# Anonymous callback — defined inline
$btn_row->Button(
    -text    => "Increment Counter",
    -command => sub {               # Anonymous sub
        $counter_val++;
        $dynamic_msg = "Counter is now $counter_val.";
    },
)->pack(-side => "left", -padx => 5);

# Disabled button
my $disabled_btn = $frame->Button(
    -text  => "Disabled Button",
    -state => "disabled",    # normal | active | disabled
)->pack;

# Toggle between enabled and disabled with cget()
$frame->Button(
    -text    => "Toggle",
    -command => sub {
        my $s = $disabled_btn->cget(-state);   # READ current state
        $disabled_btn->configure(              # WRITE new state
            -state => ($s eq "disabled" ? "normal" : "disabled")
        );
    },
)->pack;

# The named callback subroutine
sub on_update_label {
    our $update_idx //= 0;
    my @msgs = (
        "Labels update via -textvariable!",
        "Callbacks are just Perl subroutines.",
        "The event loop handles everything.",
    );
    $dynamic_msg = $msgs[$update_idx++ % @msgs];
}
§23

Entry — Single-Line Text Input

perl
my ($username, $password) = ("", "");

# Username field — linked to $username
my $name_entry = $frame->Entry(
    -textvariable => \$username,   # Variable updates as user types
    -width        => 25,
    -font         => "Helvetica 11",
    -relief       => "sunken",
)->pack;

# Password field — -show hides characters
my $pass_entry = $frame->Entry(
    -textvariable => \$password,
    -show         => "*",     # Replace each character with *
    -width        => 25,
)->pack;

# Bind <Return> to move focus, just like real forms
$name_entry->bind("<Return>", sub { $pass_entry->focus });
$pass_entry->bind("<Return>", \&on_login);

sub on_login {
    if (!$username) {
        $entry_result = "ERROR: Username required.";
        $name_entry->focus;
        return;
    }
    $entry_result = "Login successful! Welcome, $username.";
    $password = "";    # Clear password field after login
}

# Entry methods for programmatic control
my $val = $name_entry->get;            # Get current text
$name_entry->delete(0, "end");         # Clear the field
$name_entry->insert(0, "new text");    # Insert at position
§23

Frame & LabelFrame — Containers

Frames are invisible boxes that group widgets. LabelFrames add a visible border and title. Both are containers — you create child widgets inside them, not on the main window.

perl
# Plain Frame — invisible container for layout
my $toolbar = $mw->Frame(
    -background  => "#e8e8e8",
    -relief      => "flat",
    -borderwidth => 0,
)->pack(-fill => "x");

# Now pack buttons INSIDE $toolbar, not $mw
$toolbar->Button(-text => "Save")->pack(-side => "left");
$toolbar->Button(-text => "Open")->pack(-side => "left");

# LabelFrame — adds a border and title label
my $options_box = $mw->LabelFrame(
    -text        => "Options",      # Title shown on the border
    -font        => "Helvetica 11 bold",
    -foreground  => "#333333",
    -background  => "#f0f0f0",
    -relief      => "groove",
    -borderwidth => 2,
)->pack(-fill => "x", -padx => 15, -pady => 8);

# Widgets go inside the LabelFrame
$options_box->Checkbutton(-text => "Enable feature")->pack(-anchor => "w");
$options_box->Checkbutton(-text => "Debug mode"    )->pack(-anchor => "w");
§22

configure() & cget() — Runtime Changes

Every widget option set at creation can be changed later with configure(). Use cget() to read the current value of any option.

perl
my $demo_lbl = $mw->Label(
    -text       => "Watch my style change!",
    -font       => "Helvetica 13",
    -foreground => "#1a1a2e",
    -background => "#e8f4f8",
)->pack;

# Cycle through styles on each button click
my @styles = (
    [-foreground => "red",     -font => "Helvetica 13 bold"  ],
    [-foreground => "blue",    -font => "Helvetica 13 italic"],
    [-foreground => "#007a00", -font => "Helvetica 13"       ],
);
my $style_idx = 0;

$mw->Button(
    -text    => "Cycle Style",
    -command => sub {
        # configure() changes multiple options at once
        $demo_lbl->configure(@{ $styles[$style_idx++ % @styles] });
    },
)->pack;

$mw->Button(
    -text    => "Show Current Font",
    -command => sub {
        # cget() reads the current value of one option
        my $font = $demo_lbl->cget(-font);
        $mw->messageBox(-message => "Current font: $font", -type => "OK");
    },
)->pack;
§23

Toplevel — Opening a Second Window

A Toplevel widget creates a new independent window. It has its own title bar but shares the event loop with the main window.

perl
sub open_second_window {
    my $top = $mw->Toplevel;           # Create a new window
    $top->title("Second Window");
    $top->geometry("300x180+200+200");
    $top->configure(-background => "#1a1a2e");

    $top->Label(
        -text       => "I am a Toplevel window!",
        -font       => "Helvetica 13 bold",
        -foreground => "white",
        -background => "#1a1a2e",
    )->pack(-pady => 20);

    $top->Button(
        -text       => "Close Me",
        -command    => sub { $top->destroy },   # destroy() closes the window
    )->pack(-pady => 10);

    # For a MODAL dialog (blocks the parent until closed):
    # $top->transient($mw);   # Keep above parent
    # $top->grab;              # Block all input to other windows
    # $top->waitWindow;        # Pause here until $top is destroyed
}
Next: Open Tutorial 03 — Widget Showcase to learn Checkbuttons, Radiobuttons, Scales, Listboxes, the Text widget, and all three layout managers side by side.