HowTo : Creating a custom shaped window - mcorino/wxRuby3 GitHub Wiki

This tutorial demonstrates that not only is it possible to draw your own custom widget, it is also possible to draw custom shaped (non-rectangle) windows.
The example below creates a circular window showing a clock face and updates that clock face with hour, minute and second hands accurately showing the time every second.
To be able to show a custom shaped window the window must be derived from Wx::Frame which provides the #set_shape method.
The ClockFrame
class in the example creates a clock face bitmap and uses that to paint the windows background and set
the shape of the window.
In addition the clock hands are painted on top of that background according to the time at that moment. A timer makes sure
to update the window every second.
require 'wx'
class ClockFrame < Wx::Frame
RADIUS = 180
OUTER_EDGE = 5
INNER_EDGE = 10
MARK_LENGTH = 10
CENTRE_OFFSET = RADIUS-1
class << self
# calculates the clock-wise position on a circle with given radius according to the given
# minute and adjusted for a circle centre at CENTRE_OFFSET x CENTRE_OFFSET
def minute_pos(min, radius)
rad = min * 6 * Math::PI / 180
[(radius*Math.sin(rad)).to_i+CENTRE_OFFSET,
-(radius*Math.cos(rad)).to_i+CENTRE_OFFSET]
end
# calculates the clock-wise position on a circle with given radius according to the given
# hour and minute and adjusted for a circle centre at CENTRE_OFFSET x CENTRE_OFFSET
def hour_pos(hour, min, radius)
rad = ((hour * 30) + (min * 0.5)) * Math::PI / 180
[(radius*Math.sin(rad)).to_i+CENTRE_OFFSET,
-(radius*Math.cos(rad)).to_i+CENTRE_OFFSET]
end
# centre of the clock face
def centre
@centre ||= [CENTRE_OFFSET, CENTRE_OFFSET]
end
# radius of the minute hand
def longhand_radius
@lh_radius ||= RADIUS-(OUTER_EDGE+INNER_EDGE+MARK_LENGTH+6)
end
# radius of the hour hand
def shorthand_radius
@sh_radius ||= (longhand_radius / 3) * 2
end
# radius of the seconds hand
def seconds_radius
@sec_radius ||= RADIUS-(OUTER_EDGE+INNER_EDGE+MARK_LENGTH+2)
end
# get or create the clock face bitmap
def get_clock_face
unless @bmp
@bmp = Wx::Bitmap.new(RADIUS*2, RADIUS*2)
Wx::MemoryDC.draw_on(@bmp) do |dc|
dc.with_pen(Wx::LIGHT_GREY_PEN) do
dc.with_brush(Wx::WHITE_BRUSH) do
dc.draw_circle(CENTRE_OFFSET, CENTRE_OFFSET, RADIUS)
end
end
dc.with_pen(Wx::LIGHT_GREY_PEN) do
dc.with_brush(Wx::Brush.find_or_create_brush(:DARKSLATEGREY)) do
dc.draw_circle(CENTRE_OFFSET, CENTRE_OFFSET, RADIUS-OUTER_EDGE)
end
end
dc.with_pen(Wx::WHITE_PEN) do
dc.with_brush(Wx::WHITE_BRUSH) do
dc.draw_circle(CENTRE_OFFSET, CENTRE_OFFSET, 2)
end
end
radius_outer = RADIUS-(OUTER_EDGE+INNER_EDGE)
radius_hour_inner = radius_outer - 10
radius_min_inner = radius_outer - 5
hour_pen = Wx::Pen.find_or_create_pen(:RED, 3)
min_pen = Wx::Pen.find_or_create_pen(:RED, 2)
12.times do |i|
dc.with_pen(hour_pen) do
dc.draw_line(minute_pos(i*5, radius_hour_inner), minute_pos(i*5, radius_outer))
end
dc.with_pen(min_pen) do
dc.draw_line(minute_pos(i*5+1, radius_min_inner), minute_pos(i*5+1, radius_outer))
dc.draw_line(minute_pos(i*5+2, radius_min_inner), minute_pos(i*5+2, radius_outer))
dc.draw_line(minute_pos(i*5+3, radius_min_inner), minute_pos(i*5+3, radius_outer))
dc.draw_line(minute_pos(i*5+4, radius_min_inner), minute_pos(i*5+4, radius_outer))
end
end
end
end
@bmp
end
end
def initialize(parent)
super(parent, style: Wx::FRAME_SHAPED|Wx::SIMPLE_BORDER|Wx::FRAME_NO_TASKBAR|Wx::STAY_ON_TOP)
@bmp = self.class.get_clock_face
@delta = Wx::Point.new
set_size([@bmp.width, @bmp.height])
set_tool_tip('Right-click to close')
# set the window shape to the region matching the bitmap MINUS the outer black pixels
set_shape(Wx::Region.new(@bmp, :BLACK))
@tm = Wx::Timer.new(self)
# evt_left_dclick :on_double_click
evt_left_down :on_left_down
evt_left_up :on_left_up
evt_motion :on_mouse_move
evt_right_up :on_exit
evt_paint :on_paint
evt_timer(Wx::ID_ANY, :on_timer)
@tm.start(1000)
end
# def on_double_click(_evt)
#
# end
def on_timer(_)
refresh
end
def on_left_down(evt)
capture_mouse
pos = client_to_screen(evt.position)
origin = position
@delta = pos - origin
end
def on_left_up(_evt)
release_mouse if has_capture
end
def on_mouse_move(evt)
if evt.dragging && evt.left_is_down
pos = client_to_screen(evt.position)
move(pos - @delta)
end
end
def on_exit(_evt)
close
end
def on_paint(_evt)
paint do |dc|
dc.draw_bitmap(@bmp, 0, 0, true)
t = Time.now
h = t.hour % 12
m = t.min
s = t.sec
lh_pos = self.class.minute_pos(m, self.class.longhand_radius)
sh_pos = self.class.hour_pos(h, m, self.class.shorthand_radius)
dc.with_pen(Wx::Pen.find_or_create_pen(:BLUE, 2)) do
dc.draw_line(self.class.centre, lh_pos)
dc.draw_line(self.class.centre, sh_pos)
end
sec_pos = self.class.minute_pos(s, self.class.seconds_radius)
dc.with_pen(Wx::Pen.find_or_create_pen(:YELLOW, 1)) do
dc.draw_line(self.class.centre, sec_pos)
end
end
end
end
class MyWindow < Wx::Frame
def initialize(title)
super(nil, title: title)
panel = Wx::Panel.new(self)
sizer = Wx::VBoxSizer.new
sizer.add(Wx::Button.new(panel, label: 'Show Clock'), flag: Wx::ALL|Wx::ALIGN_CENTRE, border: 20)
panel.sizer = sizer
centre
evt_button(Wx::ID_ANY, :on_show_clock)
end
def on_show_clock(_evt)
clock = ClockFrame.new(nil)
clock.show
end
end
Wx::App.run do
window = MyWindow.new("wxRuby Custom Shaped Windows Guide")
window.show
end