You weren’t missing an obvious event here - the Canvas API was somewhat inconsistent.
Canvas provided DoublePressed and the lower-level MouseDown/MouseUp events, but it did not provide a single completed activation event. That is why creating a CanvasButton subclass worked, but required more plumbing than it should have.
I’ve implemented a fix for the next release: https://feedback.objo.dev/feature/1166
- Canvas now has
Pressed(x As Double, y As Double).
- It appears in Studio’s control event picker and is wired automatically.
- It fires after a primary mouse, touch, or pen press-and-release completes over the Canvas.
x and y are logical coordinates relative to the Canvas.
- Moving far enough to turn the gesture into a drag cancels
Pressed.
ImageViewer.Pressed now follows the same completed-gesture behaviour rather than firing immediately on pointer-down.
This leaves three useful choices:
- Use a normal
Button for a conventional application command. It provides the expected native button behaviour and accessibility semantics.
- Use an
ImageViewer if the control is essentially a clickable image or SVG. SVGs remain vector-backed and stay crisp when resized.
- Use a
Canvas when you need procedural drawing, combined shapes and images, or custom normal, hover, and depressed states.
For the new Canvas event, the action handler can simply be:
# In Canvas1 Pressed() event
SaveDocument()
Put the actual action in Pressed. Use MouseDown and MouseUp only for visual feedback, so dragging away does not accidentally invoke the action.
Here is a more complete custom-drawn example:
Class MainWindow Inherits Window
Property mHovered As Boolean = False
Property mDepressed As Boolean = False
Property mSaveIcon As Picture
Sub Opening()
Canvas1.AllowFocus = True
Canvas1.MouseCursor = MouseCursor.Hand
Canvas1.ToolTip = "Save"
LoadSaveIcon()
End Sub
Sub LoadSaveIcon()
If Application.IsDarkMode Then
mSaveIcon = Picture.FromAsset("Buttons/Dark/Save", Canvas1.ScaleFactor)
Else
mSaveIcon = Picture.FromAsset("Buttons/Light/Save", Canvas1.ScaleFactor)
End If
Canvas1.Refresh()
End Sub
Sub Canvas1_Paint(g As Graphics)
If mDepressed Then
g.DrawingColour = ThemeColours.Accent
ElseIf mHovered Then
g.DrawingColour = ThemeColours.SelectionBackground
Else
g.DrawingColour = ThemeColours.ControlBackground
End If
g.FillRoundRectangle(0, 0, g.Width, g.Height, 6)
g.DrawingColour = ThemeColours.ControlBorder
g.DrawRoundRectangle(0, 0, g.Width, g.Height, 6)
Var iconSize As Double = Maths.Min(g.Width, g.Height) - 12
g.DrawPicture(mSaveIcon,
(g.Width - iconSize) / 2,
(g.Height - iconSize) / 2,
iconSize,
iconSize)
End Sub
Sub Canvas1_MouseEnter()
mHovered = True
Canvas1.Refresh()
End Sub
Sub Canvas1_MouseExit()
mHovered = False
mDepressed = False
Canvas1.Refresh()
End Sub
Sub Canvas1_MouseDown(x As Double, y As Double, button As MouseButton)
If button = MouseButton.Left Then
mDepressed = True
Canvas1.Refresh()
End If
End Sub
Sub Canvas1_MouseUp(x As Double, y As Double, button As MouseButton)
If button = MouseButton.Left Then
mDepressed = False
Canvas1.Refresh()
End If
End Sub
Sub Canvas1_Pressed(x As Double, y As Double)
ActivateSave()
End Sub
Function Canvas1_KeyDown(keyName As String) As Boolean
If keyName = Key.Space Or keyName = Key.Return Then
ActivateSave()
Return True
End If
Return False
End Function
Sub ActivateSave()
Print("Save invoked")
End Sub
Event AppearanceChanged()
LoadSaveIcon()
End Event
End Class
A couple of details are worth calling out:
- Set
MouseCursor.Hand once. You do not need to change it in every enter/exit handler.
- Set
AllowFocus = True for a Canvas acting like a button.
- Leave
AllowFocusRing = True unless you draw an equally visible custom focus indicator.
- Handle both
Key.Space and Key.Return through the same action method as Pressed.
- In a Window-owned Canvas handler,
Me is the Canvas that raised the event and Self is the containing Window.
For SVG artwork, import it as an image asset rather than treating it as an arbitrary resource file. For example:
Buttons/Light/Save.svg → Buttons/Light/Save
Buttons/Dark/Save.svg → Buttons/Dark/Save
Load the appropriate logical asset with Picture.FromAsset(...). The SVG remains vector-backed when drawn with Graphics.DrawPicture(), so it stays crisp at different sizes and display scales.
Asset folders do not automatically select light or dark artwork. Use Application.IsDarkMode, then reload cached theme-specific pictures from the Window’s AppearanceChanged event. If the same SVG works in both appearances, one theme-neutral asset is simpler.
Your current CanvasButton subclass remains a reasonable workaround for the present release. After upgrading, remove or rename its custom Pressed declaration and its MouseUp-based RaiseEvent call, then use the inherited Canvas.Pressed(x, y) event instead. You can still keep the subclass to encapsulate its custom drawing and state.