Tutorial 04 — Events & Canvas
Events, Canvas & Paint
A fully-working paint application teaching mouse and keyboard event binding, after() timers, menus with radio/checkbutton items, file and color dialogs, and all Canvas drawing primitives.
▶ perl perl_gui_03_events_canvas.pl
What This Tutorial Covers
- bind() — attach callbacks to mouse and keyboard events
- XEvent — read x/y coordinates from event objects
- Button-1 Button-3 Motion KeyPress event names
- Meta (⌘ Command) key bindings for macOS
- after() repeating timer — animated live clock
- Full menubar with cascade, command, separator
- Radiobutton and Checkbutton items inside a menu
- Context (right-click popup) menu
- File open dialog with filetypes filter
- chooseColor — color picker dialog
- Canvas createLine createRectangle createOval createText
- Canvas item tags for bulk operations
- Undo stack — delete canvas items by ID
- WM_DELETE_WINDOW — intercept the close button
Full Source: perl_gui_03_events_canvas.pl
perl
#!/usr/bin/env perl
# =============================================================================
# perl_gui_03_events_canvas.pl — GUI Tutorial Part 3: Events & Canvas
# Companion to: perl_mac_guide.md Sections 25–27
#
# Run with: perl perl_gui_03_events_canvas.pl
# Requires: Perl/Tk (cpanm Tk)
#
# This file teaches:
# • bind() — keyboard and mouse events
# • Modifier keys (Ctrl, Shift, Meta/Cmd on macOS)
# • after() timers — animated clock
# • Menu bar and context (right-click) menus
# • File open/save/color dialogs
# • Canvas widget — drawing shapes, text, and images
# • A mini paint application combining all of the above
# =============================================================================
use strict;
use warnings;
use Tk;
use Tk::Canvas;
use Tk::Menu;
# =============================================================================
# Application State
# =============================================================================
my $current_tool = "pencil"; # pencil | line | rect | oval | text
my $current_color = "#007aff";
my $current_width = 3;
my $status_text = "Ready — Select a tool and draw on the canvas";
my $clock_text = "";
my @undo_stack = (); # Stores canvas item IDs for undo
# Drawing state
my ($start_x, $start_y) = (0, 0);
my $current_item = undef;
my @pencil_points = ();
# =============================================================================
# Main Window
# =============================================================================
my $mw = MainWindow->new;
$mw->title("Perl/Tk Paint — Events & Canvas Tutorial");
$mw->geometry("800x650+60+30");
$mw->configure(-background => "#1c1c1e");
$mw->protocol("WM_DELETE_WINDOW", sub {
my $ans = $mw->messageBox(
-title => "Quit?",
-message => "Exit the paint application?",
-type => "YesNo",
-icon => "question",
);
exit if $ans eq "Yes";
});
# =============================================================================
# Menu Bar (Section 26)
# =============================================================================
my $menubar = $mw->Menu;
$mw->configure(-menu => $menubar);
# File menu
my $file_menu = $menubar->cascade(-label => "File", -underline => 0, -tearoff => 0);
$file_menu->command(-label => "New Canvas", -accelerator => "Cmd+N", -command => \&new_canvas);
$file_menu->command(-label => "Open Image…", -accelerator => "Cmd+O", -command => \&open_image);
$file_menu->separator;
$file_menu->command(-label => "Quit", -accelerator => "Cmd+Q", -command => sub { exit });
# Edit menu
my $edit_menu = $menubar->cascade(-label => "Edit", -underline => 0, -tearoff => 0);
$edit_menu->command(-label => "Undo", -accelerator => "Cmd+Z", -command => \&undo_last);
$edit_menu->command(-label => "Clear All",-command => \&clear_canvas);
# Tools menu
my $tools_menu = $menubar->cascade(-label => "Tools", -underline => 0, -tearoff => 0);
$tools_menu->radiobutton(-label => "Pencil", -value => "pencil", -variable => \$current_tool);
$tools_menu->radiobutton(-label => "Line", -value => "line", -variable => \$current_tool);
$tools_menu->radiobutton(-label => "Rectangle", -value => "rect", -variable => \$current_tool);
$tools_menu->radiobutton(-label => "Oval", -value => "oval", -variable => \$current_tool);
$tools_menu->radiobutton(-label => "Text", -value => "text", -variable => \$current_tool);
# Colors menu
my $colors_menu = $menubar->cascade(-label => "Colors", -tearoff => 0);
for my $pair (
["Blue", "#007aff"], ["Red", "#ff3b30"], ["Green", "#34c759"],
["Purple", "#5856d6"], ["Orange", "#ff9500"], ["Black", "#000000"],
["White", "#ffffff"], ["Pick…", "picker" ],
) {
my ($label, $val) = @$pair;
if ($val eq "picker") {
$colors_menu->command(-label => $label, -command => \&pick_color);
} else {
$colors_menu->command(-label => $label, -command => sub { $current_color = $val; update_status() });
}
}
# =============================================================================
# Toolbar
# =============================================================================
my $toolbar = $mw->Frame(
-background => "#2c2c2e",
-relief => "flat",
-borderwidth => 0,
)->pack(-fill => "x");
# Tool buttons
my @tools = (
["✏", "pencil", "Pencil (freehand)"],
["╱", "line", "Line"],
["▭", "rect", "Rectangle"],
["○", "oval", "Oval"],
["T", "text", "Text"],
);
for my $t (@tools) {
my ($icon, $tool, $tip) = @$t;
$toolbar->Button(
-text => $icon,
-font => "Helvetica 14",
-background => "#2c2c2e",
-foreground => "white",
-activebackground => "#007aff",
-activeforeground => "white",
-relief => "flat",
-padx => 12, -pady => 6,
-command => sub { $current_tool = $tool; update_status() },
)->pack(-side => "left");
}
$toolbar->Label(-text => "|", -foreground => "#555", -background => "#2c2c2e"
)->pack(-side => "left", -padx => 4);
# Color swatches in toolbar
for my $color ("#007aff", "#ff3b30", "#34c759", "#5856d6", "#ff9500", "#ffffff", "#000000") {
$toolbar->Button(
-background => $color,
-activebackground => $color,
-width => 2,
-relief => "flat",
-padx => 4, -pady => 6,
-command => sub { $current_color = $color; update_status() },
)->pack(-side => "left", -padx => 1);
}
$toolbar->Button(
-text => "⊕ Color",
-font => "Helvetica 10",
-background => "#2c2c2e",
-foreground => "#aaaaaa",
-activebackground => "#3a3a3c",
-relief => "flat",
-padx => 6, -pady => 6,
-command => \&pick_color,
)->pack(-side => "left", -padx => 4);
$toolbar->Label(-text => "|", -foreground => "#555", -background => "#2c2c2e"
)->pack(-side => "left", -padx => 4);
# Brush size
$toolbar->Label(-text => "Size:", -font => "Helvetica 10",
-foreground => "#aaaaaa", -background => "#2c2c2e"
)->pack(-side => "left");
$toolbar->Scale(
-from => 1,
-to => 20,
-orient => "horizontal",
-variable => \$current_width,
-length => 80,
-showvalue => 0,
-background => "#2c2c2e",
-foreground => "white",
-troughcolor=> "#555",
)->pack(-side => "left", -padx => 4);
# Undo and Clear
$toolbar->Label(-text => "|", -foreground => "#555", -background => "#2c2c2e"
)->pack(-side => "left", -padx => 4);
$toolbar->Button(-text => "↩ Undo", -font => "Helvetica 10",
-background => "#2c2c2e", -foreground => "#aaaaaa",
-activebackground => "#3a3a3c", -relief => "flat",
-padx => 6, -pady => 6,
-command => \&undo_last)->pack(-side => "left", -padx => 2);
$toolbar->Button(-text => "✕ Clear", -font => "Helvetica 10",
-background => "#2c2c2e", -foreground => "#ff453a",
-activebackground => "#3a3a3c", -relief => "flat",
-padx => 6, -pady => 6,
-command => \&clear_canvas)->pack(-side => "left", -padx => 2);
# Clock label (right side of toolbar)
$toolbar->Label(
-textvariable => \$clock_text,
-font => "Courier 11",
-foreground => "#aaaaaa",
-background => "#2c2c2e",
)->pack(-side => "right", -padx => 10);
# =============================================================================
# Canvas (Section 27)
# =============================================================================
my $canvas = $mw->Canvas(
-background => "white",
-cursor => "crosshair",
)->pack(-fill => "both", -expand => 1);
# =============================================================================
# Status Bar
# =============================================================================
my $status_bar = $mw->Frame(-background => "#2c2c2e", -height => 24)->pack(
-fill => "x", -side => "bottom"
);
$status_bar->Label(
-textvariable => \$status_text,
-font => "Helvetica 10",
-foreground => "#aaaaaa",
-background => "#2c2c2e",
-anchor => "w",
)->pack(-side => "left", -padx => 10);
my $coord_text = "";
$status_bar->Label(
-textvariable => \$coord_text,
-font => "Courier 10",
-foreground => "#555555",
-background => "#2c2c2e",
)->pack(-side => "right", -padx => 10);
# =============================================================================
# Canvas Event Bindings (Section 25)
# =============================================================================
# Mouse press — start drawing
$canvas->bind("<Button-1>", \&on_mouse_press);
# Mouse drag — continue drawing
$canvas->bind("<B1-Motion>", \&on_mouse_drag);
# Mouse release — finish shape
$canvas->bind("<ButtonRelease-1>", \&on_mouse_release);
# Track mouse position for status bar
$canvas->bind("<Motion>", sub {
my $e = $canvas->XEvent;
$coord_text = sprintf("x: %4d y: %4d", $e->x, $e->y);
});
# Right-click context menu
my $ctx_menu = $mw->Menu(-tearoff => 0);
$ctx_menu->command(-label => "Undo Last", -command => \&undo_last);
$ctx_menu->command(-label => "Clear All", -command => \&clear_canvas);
$ctx_menu->separator;
$ctx_menu->command(-label => "Pick Color…", -command => \&pick_color);
$canvas->bind("<Button-3>", sub {
my $e = $canvas->XEvent;
$ctx_menu->popup($e->X, $e->Y);
});
# =============================================================================
# Keyboard Shortcuts (Section 25)
# =============================================================================
# macOS Command key = Meta in Perl/Tk
$mw->bind("<Meta-z>", \&undo_last);
$mw->bind("<Meta-n>", \&new_canvas);
$mw->bind("<Meta-q>", sub { exit });
$mw->bind("<Escape>", sub { $current_tool = "pencil"; update_status() });
# Tool hotkeys
$mw->bind("<p>", sub { $current_tool = "pencil"; update_status() });
$mw->bind("<l>", sub { $current_tool = "line"; update_status() });
$mw->bind("<r>", sub { $current_tool = "rect"; update_status() });
$mw->bind("<o>", sub { $current_tool = "oval"; update_status() });
$mw->bind("<t>", sub { $current_tool = "text"; update_status() });
# =============================================================================
# after() Timer — Animated Clock (Section 25)
# =============================================================================
# after() schedules a callback. By rescheduling itself inside the callback,
# we create a repeating timer without blocking the event loop.
sub tick_clock {
$clock_text = scalar localtime();
$mw->after(1000, \&tick_clock); # Re-schedule after 1 second
}
tick_clock(); # Start the clock
# =============================================================================
# Draw a welcome message on the canvas
# =============================================================================
$mw->after(100, sub {
my $w = $canvas->width;
my $h = $canvas->height;
$canvas->createText($w/2, $h/2 - 30,
-text => "Perl/Tk Canvas",
-font => "Helvetica 28 bold",
-fill => "#e0e0e0",
-anchor => "center",
);
$canvas->createText($w/2, $h/2 + 10,
-text => "Select a tool from the toolbar and start drawing!",
-font => "Helvetica 12",
-fill => "#cccccc",
-anchor => "center",
);
$canvas->createText($w/2, $h/2 + 35,
-text => "Hotkeys: p=pencil l=line r=rect o=oval t=text Cmd+Z=undo",
-font => "Helvetica 10",
-fill => "#aaaaaa",
-anchor => "center",
);
});
# =============================================================================
# Drawing Callbacks
# =============================================================================
sub on_mouse_press {
my $e = $canvas->XEvent;
($start_x, $start_y) = ($e->x, $e->y);
@pencil_points = ($start_x, $start_y);
$current_item = undef;
if ($current_tool eq "text") {
# For text tool, ask for input
my $txt = "";
my $dlg = $mw->Toplevel;
$dlg->title("Add Text");
$dlg->geometry("280x110");
$dlg->transient($mw);
$dlg->grab;
$dlg->Label(-text => "Enter text to place at ($start_x, $start_y):"
)->pack(-padx => 10, -pady => 8);
$dlg->Entry(-textvariable => \$txt, -width => 30, -font => "Helvetica 11"
)->pack(-padx => 10);
$dlg->Frame->pack(-pady => 3);
$dlg->Button(
-text => "Place",
-command => sub {
if ($txt) {
my $id = $canvas->createText($start_x, $start_y,
-text => $txt,
-font => "Helvetica " . ($current_width * 3 + 8) . " bold",
-fill => $current_color,
-anchor => "nw",
);
push @undo_stack, $id;
}
$dlg->destroy;
},
)->pack;
$dlg->waitWindow;
}
}
sub on_mouse_drag {
my $e = $canvas->XEvent;
my ($x, $y) = ($e->x, $e->y);
if ($current_tool eq "pencil") {
# Freehand: add point to the polyline
push @pencil_points, ($x, $y);
if ($current_item) { $canvas->delete($current_item) }
$current_item = $canvas->createLine(
@pencil_points,
-fill => $current_color,
-width => $current_width,
-capstyle => "round",
-joinstyle => "round",
-smooth => 1,
);
} elsif ($current_tool eq "line") {
if ($current_item) { $canvas->delete($current_item) }
$current_item = $canvas->createLine(
$start_x, $start_y, $x, $y,
-fill => $current_color,
-width => $current_width,
-arrow => "none",
);
} elsif ($current_tool eq "rect") {
if ($current_item) { $canvas->delete($current_item) }
$current_item = $canvas->createRectangle(
$start_x, $start_y, $x, $y,
-outline => $current_color,
-width => $current_width,
-fill => "",
);
} elsif ($current_tool eq "oval") {
if ($current_item) { $canvas->delete($current_item) }
$current_item = $canvas->createOval(
$start_x, $start_y, $x, $y,
-outline => $current_color,
-width => $current_width,
-fill => "",
);
}
}
sub on_mouse_release {
push @undo_stack, $current_item if $current_item;
$current_item = undef;
@pencil_points = ();
}
# =============================================================================
# Canvas Operations
# =============================================================================
sub undo_last {
if (@undo_stack) {
my $id = pop @undo_stack;
$canvas->delete($id);
update_status("Undo: removed item $id");
} else {
update_status("Nothing to undo.");
}
}
sub clear_canvas {
my $ans = $mw->messageBox(
-title => "Clear Canvas",
-message => "Clear all drawings?",
-type => "YesNo",
-icon => "question",
);
if ($ans eq "Yes") {
$canvas->delete("all");
@undo_stack = ();
update_status("Canvas cleared.");
}
}
sub new_canvas {
clear_canvas();
}
sub open_image {
# File open dialog (Section 26)
my $file = $mw->getOpenFile(
-title => "Open Image",
-initialdir => $ENV{HOME} // ".",
-filetypes => [
["PNG Images", ".png"],
["GIF Images", ".gif"],
["All Files", "*"],
],
);
return unless $file;
eval {
my $photo = $mw->Photo(-file => $file);
my $id = $canvas->createImage(20, 20, -image => $photo, -anchor => "nw");
push @undo_stack, $id;
update_status("Opened: $file");
};
if ($@) {
$mw->messageBox(
-title => "Error",
-message => "Could not load image:\n$@",
-type => "OK",
-icon => "error",
);
}
}
sub pick_color {
# Color chooser dialog (Section 26)
my $chosen = $mw->chooseColor(
-title => "Pick Drawing Color",
-initialcolor => $current_color,
);
if ($chosen) {
$current_color = $chosen;
update_status("Color set to $current_color");
}
}
sub update_status {
my ($msg) = @_;
my $tool_names = { pencil => "Pencil ✏", line => "Line ╱", rect => "Rect ▭",
oval => "Oval ○", text => "Text T" };
my $tname = $tool_names->{$current_tool} // $current_tool;
$status_text = ($msg // "") . " Tool: $tname Color: $current_color Width: $current_width";
}
update_status();
# =============================================================================
# Main Loop
# =============================================================================
MainLoop;
Run it: Save the
.pl file and run perl perl_gui_03_events_canvas.pl on your Mac. A window will appear — experiment with it as you read the source above.