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

     About      FAQ      User Guide      Reference documentation

Creating a custom shaped Window

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.

Display Window with custom shape

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
⚠️ **GitHub.com Fallback** ⚠️