Events & binding
onClick and friends
Activatable widgets take an onClick option — the simplest way to respond to a
press:
app.TButton({ text: 'Go', onClick: () => console.log('clicked') });
The handler receives the widget itself as its argument. CheckButton,
RadioButton, scales, and similar widgets follow the same pattern.
bind — any event
For anything beyond a plain click — keys, mouse, hover, focus, resize — use
widget.bind(sequence, handler):
entry.bind('<Return>', () => submit());
list.bind('<Double-1>', () => open(list.curselection()));
btn.bind('<Enter>', () => (btn.style = 'Accent.TButton'));
A sequence is a Tk event spec in angle brackets — for example <Button-1>
(left click), <Double-1> (double click), <Button-3> (right click), <Key>,
<Return>, <Escape>, <Enter> / <Leave> (pointer in/out), <Configure>
(resize/move), <FocusIn> / <FocusOut>.
The event object
Your handler is called with an event object whose fields depend on the event kind — ScriptWeaver fills in only the relevant ones:
| Event kind | Fields |
|---|---|
Mouse (Button, Motion, Enter, Leave, Wheel) |
x, y (within the widget), screenX, screenY, button, state |
Keyboard (Key…) |
keyCode, key, char, state |
Configure (resize / move) |
width, height, x, y |
| Focus | target, detail |
Every event object also carries widget (the target's path) and type (the
sequence).
canvas.bind('<Button-1>', (e) => {
console.log(`clicked at ${e.x}, ${e.y}`);
});
app.bind('<Configure>', (e) => {
console.log(`resized to ${e.width}×${e.height}`);
});
Mouse coordinates: widget-relative vs screen
A mouse event carries two coordinate pairs, and the difference matters the moment a widget moves:
x,yare measured relative to the event's widget, as it sat when Tk generated the event. If that widget then moves, the origin they're measured from moves with it.screenX,screenYare absolute screen coordinates — independent of any widget's position, so they stay correct across moves.
This is the classic trap in drag handlers. The tempting-but-wrong approach is
to move the widget and then reconstruct the pointer as
widgetAbsolutePosition + e.x — but e.x was measured against the widget's
old spot, so the drag distance gets counted twice and the cursor races ahead of
what it's dragging. (This is ordinary Tk geometry, not a ScriptWeaver quirk — the
same bug exists in plain Tcl/Tk.)
The robust pattern: snapshot a reference point once on press, then drive the move
from the screen-space delta since then — never from x / y:
let drag = null;
box.bind('<ButtonPress-1>', (e) => {
// Where the pointer started, and where the box started.
drag = { sx: e.screenX, sy: e.screenY, x0: boxX, y0: boxY };
});
box.bind('<B1-Motion>', (e) => {
if (!drag) return;
// New position = start position + how far the pointer moved on screen.
// Immune to the box having already moved, and to handler-dispatch latency.
boxX = drag.x0 + (e.screenX - drag.sx);
boxY = drag.y0 + (e.screenY - drag.sy);
box.place.configure({ x: boxX, y: boxY });
});
box.bind('<ButtonRelease-1>', () => (drag = null));
Rule of thumb: if your math has to stay correct while the thing under the pointer
is moving, use screenX / screenY, not x / y.
Handlers run asynchronously
Event handlers are dispatched to your JavaScript without blocking the UI — the window keeps repainting and stays responsive even while a handler runs. Two practical consequences:
- There is no
preventDefault/stopPropagation. A handler observes an event; it can't cancel the widget's built-in behaviour or stop the event from reaching other bindings. - For input validation (rejecting keystrokes as they're typed), use an entry's
onValidateoption rather than a<Key>binding.
Related
- Reactive values — to react to a value changing rather than a UI event,
bind a variable and use its
onChange. - Window close — handle the window-manager close button with
win.wm.protocol('WM_DELETE_WINDOW', () => …).