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.
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
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.
#!/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. Your callbacks are what run afterwards.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.
# 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!Fixed string — set once at creation
Reference to a Perl variable — updates automatically
Pixel width at which text wraps to next line
Text alignment: w=left, e=right, center
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 { ... }).
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];
}Entry — Single-Line Text Input
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 positionFrame & 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.
# 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");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.
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;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.
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
}