Last time I showed a way to use immediate mode graphics in WPF by showing balls bouncing around in a window.

This time, we’ll add a few features.

I’ve made the program DPI (Dots Per Inch) Aware. On most machines it works correctly. However, on my brand new Surface Pro 3 (really nice!), the screen resolution is so high (2160 X 1440) that the “normal” 96 pixels per inch is scaled to be larger. This means the bouncing balls boundary calculations are incorrect. WPF applications are DPI aware, but the immediate mode graphics that write directly to the Window’s device context was not. PresentationSource. CompositionTarget.TransformToDevice has the scale factors needed in the sample below.

The prior sample used a timer to trigger when to update the screen. Today’s sample removes the timer, so we’ll just update the screen continuously, as fast at the machine can go. The updates are done on a ThreadPool thread so that the UI remains responsive.

The UI now contains a few WPF controls, like button, textbox and slider. The slider controls the number of balls displayed. Try changing the slider to control something else like the ball size. Notice that the UI remains responsive even when the balls are moving full speed and the ThreadPool thread is fully busy.

The coordinates are now double precision floating point, for more precise positioning. WPF defaults to floating point too. Even though the arithmetic is more complicated (integer math is much faster than floating point), the effects of rounding errors are minimized. The Windows GDI API functions deal with Points and Rects that are integer pixel coordinates, so both types are required: integer and double, as well as conversions from each.

Start Visual Studio

File->New->Project-> Visual Basic->Windows WPF Application

Replace the code in MainWindow.Xaml.vb with the code below

Hit F5 to watch the balls bounce around

See also

DPI and Device-Independent Pixels

Creating a DPI-Aware Application

WPF immediate mode graphics

<Code Sample>

'http://blogs.msdn.com/b/calvin_hsia/archive/2008/11/28/9155196.aspx
Option Strict On

Imports System.Runtime.InteropServices
Imports System.Threading


Class MainWindow
  Dim _MovingBalls As MovingBalls
  Sub Load() Handles MyBase.Loaded
    Try
      Me.Width = 600
      Me.Height = 600
      AddHandler Me.SizeChanged,
          Sub()
            If _MovingBalls IsNot Nothing Then
              _MovingBalls.OnSizeChanged()
            End If
          End Sub

      Me.Content = CreateUIElem("Button", "Bisque")
    Catch ex As Exception
      Me.Content = ex.ToString
    End Try
  End Sub

  Function CreateUIElem(
                       UIElemName As String,
                       BackGround As String) As UIElement
    Dim xaml = _
    <Grid
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      Name="HwndTest" Margin="5,5,5,5">
      <Grid.RowDefinitions>
        <RowDefinition Height="25"></RowDefinition>
        <RowDefinition></RowDefinition>
      </Grid.RowDefinitions>
      <DockPanel Grid.Row="0">
        <Button
          Content="_Quit"
          Name="btnQuit"
          HorizontalAlignment="Left"
          Margin="10,2,0,0"
          VerticalAlignment="Top"
          Width="75"/>
        <TextBox
          Name="tbx"
          HorizontalAlignment="Left"
          Height="23"
          Margin="10,2,0,0"
          TextWrapping="Wrap"
          Text="TextBox"
          VerticalAlignment="Top"
          Width="120"/>
        <Slider
          Name="Slider"
          HorizontalAlignment="Left"
          Margin="12,2,0,0"
          VerticalAlignment="Top"
          Width="200"/>

      </DockPanel>
      <DockPanel Grid.Row="1">
        <UserControl Name="MyUserControl"></UserControl>
      </DockPanel>

    </Grid>
    Dim grid = CType(Markup.XamlReader.Load(xaml.CreateReader), Grid)
    Dim btnQuit = CType(grid.FindName("btnQuit"), Button)
    AddHandler btnQuit.Click, Sub() End

    Dim slider = CType(grid.FindName("Slider"), Slider)
    Dim tbx = CType(grid.FindName("tbx"), TextBox)
    slider.Minimum = 0
    slider.SmallChange = 1
    slider.Maximum = 200
    slider.TickPlacement = Primitives.TickPlacement.BottomRight
    slider.TickFrequency = 5
    AddHandler slider.ValueChanged,
      Sub()
        tbx.Text = slider.Value.ToString
        _MovingBalls._nBalls = CInt(slider.Value)
        _MovingBalls._doInit = True
      End Sub

    Dim userConrtol = CType(grid.FindName("MyUserControl"), UserControl)
    _MovingBalls = New MovingBalls(Me)
    userConrtol.Content = _MovingBalls
    Return grid
  End Function

  Protected Overrides Sub OnKeyDown(e As KeyEventArgs)
    _MovingBalls.OnKey(e)
  End Sub
End Class

Public Class MovingBalls
  Inherits MyHwndHost
  Public _balls As Ball()
  Public _ballSpeed As Integer = 10
  Public _nBalls As Integer = 100
  Public _rand As New Random(1)
  Public _doErase As Boolean = False
  Public IsClosing As Boolean = False
  Dim tmr As Timers.Timer
  Dim _wParent As Window

  Dim _boundSize As Point
  Public _doInit As Boolean = True

  Public Sub New(wParent As Window)
    MyBase.New(CreateSolidBrush(CType(&HF88080, IntPtr))) ' Blue
    _wParent = wParent
    AddHandler wParent.Closing,
        Sub()
          IsClosing = True
        End Sub
  End Sub
  Dim fDidStart As Boolean = False
  Public Overrides Sub OnReady(hwnd As IntPtr)
    If Not fDidStart Then
      fDidStart = True
      ThreadPool.QueueUserWorkItem(
          Sub()
            While Not IsClosing
              If _doInit Then
                _doInit = False
                InitBalls()
              End If
              Dim hDC = GetDC(_hwnd) ' get device context
              SelectObject(hDC, _hbrBackground)
              For i = 0 To _nBalls - 1
                Dim ball = _balls(i)
                If ball Is Nothing Then
                  Exit For
                End If
                ' first we draw over the old ball with the back color to erase it
                If _doErase Then
                  ball.Erase(hDC, _hbrBackground)
                End If

                ' now we detect wall collision
                If ball._Position.X >= Me._boundSize.X OrElse
                        ball._Position.X < 0 Then
                  ball._Speed.X = -ball._Speed.X
                End If
                If ball._Position.Y > Me._boundSize.Y OrElse
                        ball._Position.Y < 0 Then
                  ball._Speed.Y = -ball._Speed.Y
                End If
                ' now we move the ball
                ball._Position.X += ball._Speed.X
                ball._Position.Y += ball._Speed.Y
                ' now we draw the ball in the new position
                ball.Draw(hDC)
              Next
              ReleaseDC(_hwnd, hDC)
            End While
          End Sub)
    End If
  End Sub

  Dim curColor As Integer = &HFFFFFF
  Public Sub InitBalls()
    If _boundSize.X = 0 Then
      _boundSize.X = CInt(Me.ActualWidth * xScale)
      _boundSize.Y = CInt(Me.ActualHeight * yScale)
    End If
    If _balls IsNot Nothing Then
      For Each b In _balls
        If b IsNot Nothing Then
          b.Dispose()
        End If
      Next
    End If
    ReDim _balls(_nBalls + 1)
    For i = 0 To _nBalls - 1
      curColor -= 100 ' // change the color some way
      _balls(i) = New Ball(curColor)
      _balls(i)._Position = New Point With {
          .x = _rand.Next(CInt(_boundSize.X)) + 1,
          .y = _rand.Next(CInt(_boundSize.Y)) + 1
      }
      _balls(i)._Speed = New Point With {
          .x = _rand.Next(_ballSpeed) + 1,
          .y = _rand.Next(_ballSpeed) + 1
      }
    Next
    EraseRect()
  End Sub
  Sub OnSizeChanged()
    Me._doInit = True
    _boundSize = New Point(CInt(Me.ActualWidth * xScale), CInt(Me.ActualHeight * yScale))
  End Sub
  Sub OnKey(e As KeyEventArgs)
    Select Case e.Key
      Case Key.Subtract
        _ballSpeed -= 1
      Case Key.Add
        _ballSpeed += 1
      Case Key.E
        _doErase = Not _doErase
      Case Key.R
        _doInit = True
      Case Key.Q, Key.Escape
        End
    End Select
  End Sub

  Public Class MyObject
    Public _rect As Rect
    Public _ForeColor As IntPtr
    Public _Position As Point
    Public _Speed As Point
  End Class

  Public Class Ball
    Inherits MyObject
    Implements IDisposable

    Public Sub New(hbrBallColor As Integer)
      _rect = New Rect(0, 0, 20, 20)
      _Speed = New Point(1, 1)
      _ForeColor = CreateSolidBrush(CType(hbrBallColor, IntPtr))
    End Sub
    Public Sub Draw(hdc As IntPtr)
      SelectObject(hdc, _ForeColor)
      Dim newPos = _rect.RectMove(_Position)
      Ellipse(hdc, CInt(newPos.Left),
              CInt(newPos.Top),
              CInt(newPos.Right),
              CInt(newPos.Bottom)
              )
    End Sub
    Public Sub [Erase](hdc As IntPtr, hBackColor As IntPtr)
      Dim rect = _rect.RectMove(_Position).ToWinRect
      FillRect(hdc, rect, hBackColor)
    End Sub

#Region "IDisposable Support"
    Private disposedValue As Boolean ' To detect redundant calls

    ' IDisposable
    Protected Overridable Sub Dispose(disposing As Boolean)
      If Not Me.disposedValue Then
        If disposing Then
          ' TODO: dispose managed state (managed objects).
        End If
        DeleteObject(_ForeColor)
        ' TODO: free unmanaged resources (unmanaged objects) and override Finalize() below.
        ' TODO: set large fields to null.
      End If
      Me.disposedValue = True
    End Sub

    Protected Overrides Sub Finalize()
      ' Do not change this code.  Put cleanup code in Dispose(ByVal disposing As Boolean) above.
      Dispose(False)
      MyBase.Finalize()
    End Sub

    ' This code added by Visual Basic to correctly implement the disposable pattern.
    Public Sub Dispose() Implements IDisposable.Dispose
      ' Do not change this code.  Put cleanup code in Dispose(disposing As Boolean) above.
      Dispose(True)
      GC.SuppressFinalize(Me)
    End Sub
#End Region
    Public Overrides Function ToString() As String
      Return String.Format("{0}", _rect.ToString())
    End Function
  End Class
End Class

Public MustInherit Class MyHwndHost
  Inherits Interop.HwndHost
  Public _hwnd As IntPtr
  Protected _hbrBackground As IntPtr
  Public xScale As Double = 1
  Public yScale As Double = 1

  Public Sub New(hbrBackground As IntPtr)
    _hbrBackground = hbrBackground
  End Sub
  Protected Overrides Function BuildWindowCore(hwndParent As HandleRef) As HandleRef
    Dim psource = PresentationSource.FromVisual(Me)
    If psource IsNot Nothing Then
      ' deal with scaling:try commenting these out
      xScale = psource.CompositionTarget.TransformToDevice.M11
      yScale = psource.CompositionTarget.TransformToDevice.M22
    End If
    _hwnd = CreateWindowEx(
        0,
        "static",
        "",
        WindowStyles.WS_CHILD Or
        WindowStyles.WS_CLIPCHILDREN Or
        WindowStyles.WS_CLIPSIBLINGS,
        0, 0, 50, 50,
        hwndParent.Handle,
        IntPtr.Zero,
        IntPtr.Zero,
        IntPtr.Zero)
    Return New HandleRef(Me, _hwnd)
  End Function
  Public MustOverride Sub OnReady(hwnd As IntPtr)

  Public Sub EraseRect()
    Dim r = New WinRect
    GetClientRect(_hwnd, r)
    Dim hDC = GetDC(_hwnd)
    FillRect(hDC, r, _hbrBackground)
    ReleaseDC(_hwnd, hDC)
  End Sub

  Protected Overrides Function WndProc(
                                      hwnd As IntPtr,
                                      msg As Integer,
                                      wParam As IntPtr,
                                      lParam As IntPtr,
                                      ByRef handled As Boolean
                                      ) As IntPtr
    'Debug.WriteLine(String.Format("{0:x8} {1:x8} {2:x8} {3:x8}",
    '                              hwnd, msg, wParam, lParam))
    Select Case msg
      Case WM_.WM_ERASEBKGND
        EraseRect()
        ValidateRect(_hwnd, Nothing)
        OnReady(_hwnd)
        handled = True
        Return IntPtr.Zero
      Case WM_.WM_PAINT
        ValidateRect(_hwnd, Nothing)
        handled = True
        Return IntPtr.Zero
    End Select
    Return MyBase.WndProc(hwnd, msg, wParam, lParam, handled)
  End Function

  Protected Overrides Sub DestroyWindowCore(hwnd As HandleRef)
    DeleteObject(_hbrBackground)
    DestroyWindow(hwnd.Handle)
  End Sub
End Class

Public Module NativeMethods
  ''' <summary>
  ''' WinPoint uses integer for WinApi
  ''' (System.Windows.Point uses Double)
  ''' </summary>
  ''' <remarks></remarks>
  <StructLayout(LayoutKind.Sequential)>
  Public Structure WinPoint
    Public Sub New(x As Integer, y As Integer)
      Me.x = x
      Me.y = y
    End Sub
    Public x As Integer
    Public y As Integer
    Public Shared Operator +(p1 As WinPoint, p2 As WinPoint) As WinPoint
      Dim pt As WinPoint
      pt.x = p1.x + p2.x
      pt.y = p1.y + p2.y
      Return pt
    End Operator
    Public Overrides Function ToString() As String
      Return String.Format("({0},{1})", x, y)
    End Function
  End Structure

  ''' <summary>
  ''' WinRect uses integers to interop with WinAPI
  ''' (System.Windows.Rect uses Double)
  ''' </summary>
  ''' <remarks></remarks>
  <StructLayout(LayoutKind.Sequential)>
  Public Structure WinRect
    Public Sub New(
                  left As Integer,
                  top As Integer,
                  right As Integer,
                  bottom As Integer)
      Me.Left = left
      Me.Top = top
      Me.Right = right
      Me.Bottom = bottom
    End Sub
    Public Left As Integer
    Public Top As Integer
    Public Right As Integer
    Public Bottom As Integer
    Public Function ToRect() As System.Windows.Rect
      Return New System.Windows.Rect(Left, Top, Right - Left, Bottom - Top)
    End Function
    Public Overrides Function ToString() As String
      Return String.Format(
          "({0},{1}), ({2},{3})",
          Left,
          Top,
          Right,
          Bottom)
    End Function
  End Structure

  <System.Runtime.CompilerServices.Extension()>
  Public Function RectMove(rect1 As Rect, pt As Point) As Rect
    rect1.X += pt.X
    rect1.Y += pt.Y
    Return rect1
  End Function

  <System.Runtime.CompilerServices.Extension()>
  Public Function ToWinRect(rect1 As Rect) As WinRect
    Return New WinRect(CInt(rect1.Left),
                       CInt(rect1.Top),
                       CInt(rect1.Left + rect1.Width),
                       CInt(rect1.Top + rect1.Height)
                       )
  End Function
#Region "WIN32 Defs"
  Public Enum COLOR
    COLOR_SCROLLBAR = 0
    COLOR_BACKGROUND = 1
    COLOR_DESKTOP = 1
    COLOR_ACTIVECAPTION = 2
    COLOR_INACTIVECAPTION = 3
    COLOR_MENU = 4
    COLOR_WINDOW = 5
    COLOR_WINDOWFRAME = 6
    COLOR_MENUTEXT = 7
    COLOR_WINDOWTEXT = 8
    COLOR_CAPTIONTEXT = 9
    COLOR_ACTIVEBORDER = 10
    COLOR_INACTIVEBORDER = 11
    COLOR_APPWORKSPACE = 12
    COLOR_HIGHLIGHT = 13
    COLOR_HIGHLIGHTTEXT = 14
    COLOR_BTNFACE = 15
    COLOR_3DFACE = 15
    COLOR_BTNSHADOW = 16
    COLOR_3DSHADOW = 16
    COLOR_GRAYTEXT = 17
    COLOR_BTNTEXT = 18
    COLOR_INACTIVECAPTIONTEXT = 19
    COLOR_BTNHIGHLIGHT = 20
    COLOR_3DHIGHLIGHT = 20
    COLOR_3DHILIGHT = 20
    COLOR_BTNHILIGHT = 20
    COLOR_3DDKSHADOW = 21
    COLOR_3DLIGHT = 22
    COLOR_INFOTEXT = 23
    COLOR_INFOBK = 24
  End Enum

  Public Enum RasterOps
    R2_BLACK = 1        '  /*  0       */
    R2_NOTMERGEPEN = 2  '  /* DPon     */
    R2_MASKNOTPEN = 3   '  /* DPna     */
    R2_NOTCOPYPEN = 4   '  /* PN       */
    R2_MASKPENNOT = 5   '  /* PDna     */
    R2_NOT = 6          '  /* Dn       */
    R2_XORPEN = 7       '  /* DPx      */
    R2_NOTMASKPEN = 8   '  /* DPan     */
    R2_MASKPEN = 9      '  /* DPa      */
    R2_NOTXORPEN = 10   '  /* DPxn     */
    R2_NOP = 11         '  /* D        */
    R2_MERGENOTPEN = 12 '  /* DPno     */
    R2_COPYPEN = 13     '  /* P        */
    R2_MERGEPENNOT = 14 '  /* PDno     */
    R2_MERGEPEN = 15    '  /* DPo      */
    R2_WHITE = 16       '  /*  1       */
    R2_LAST = 16
  End Enum

  <DllImport("user32.dll", CharSet:=CharSet.Auto)> _
  Public Function GetClientRect(ByVal hWnd As System.IntPtr, _
       ByRef lpRECT As WinRect) As Integer
  End Function

  Public Enum WM_
    WM_PAINT = &HF
    WM_ERASEBKGND = &H14
  End Enum
  <Flags()> _
  Public Enum WindowStyles
    WS_OVERLAPPED = &H0
    WS_POPUP = &H80000000
    WS_CHILD = &H40000000
    WS_MINIMIZE = &H20000000
    WS_VISIBLE = &H10000000
    WS_DISABLED = &H8000000
    WS_CLIPSIBLINGS = &H4000000
    WS_CLIPCHILDREN = &H2000000
    WS_MAXIMIZE = &H1000000
    WS_BORDER = &H800000
    WS_DLGFRAME = &H400000
    WS_VSCROLL = &H200000
    WS_HSCROLL = &H100000
    WS_SYSMENU = &H80000
    WS_THICKFRAME = &H40000
    WS_GROUP = &H20000
    WS_TABSTOP = &H10000
    WS_MINIMIZEBOX = &H20000
    WS_MAXIMIZEBOX = &H10000
    WS_CAPTION = WS_BORDER Or WS_DLGFRAME
    WS_TILED = WS_OVERLAPPED
    WS_ICONIC = WS_MINIMIZE
    WS_SIZEBOX = WS_THICKFRAME
    WS_TILEDWINDOW = WS_OVERLAPPEDWINDOW
    WS_OVERLAPPEDWINDOW = WS_OVERLAPPED Or WS_CAPTION Or WS_SYSMENU Or WS_THICKFRAME Or WS_MINIMIZEBOX Or WS_MAXIMIZEBOX
    WS_POPUPWINDOW = WS_POPUP Or WS_BORDER Or WS_SYSMENU
    WS_CHILDWINDOW = WS_CHILD
  End Enum
  <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
  Public Function ShowWindow(ByVal hwnd As IntPtr, ByVal nCmdShow As Int32) As Boolean
  End Function
  <DllImport("user32.dll")> _
  Public Function UpdateWindow( _
     ByVal hWnd As IntPtr) As <MarshalAs(UnmanagedType.Bool)> Boolean
  End Function
  <DllImport("user32.dll", CharSet:=CharSet.Auto)> _
  Public Function CreateWindowEx( _
         ByVal dwExStyle As UInteger, _
         ByVal lpClassName As String, _
         ByVal lpWindowName As String, _
         ByVal dwStyle As WindowStyles, _
         ByVal x As Integer, _
         ByVal y As Integer, _
         ByVal nWidth As Integer, _
         ByVal nHeight As Integer, _
         ByVal hWndParent As IntPtr, _
         ByVal hMenut As IntPtr, _
         ByVal hInstancet As IntPtr, _
         ByVal lpParamt As IntPtr) As IntPtr
  End Function

  <DllImport("user32.dll", SetLastError:=True, CharSet:=CharSet.Auto)> _
  Public Function DestroyWindow(ByVal hwnd As IntPtr) As Boolean
  End Function

  Public Const CW_USEDEFAULT As Int32 = &H80000000

  Public Enum Show_Window
    SW_HIDE = 0
    SW_SHOWNORMAL = 1
    SW_NORMAL = 1
    SW_SHOWMINIMIZED = 2
    SW_SHOWMAXIMIZED = 3
    SW_MAXIMIZE = 3
    SW_SHOWNOACTIVATE = 4
    SW_SHOW = 5
    SW_MINIMIZE = 6
    SW_SHOWMINNOACTIVE = 7
    SW_SHOWNA = 8
    SW_RESTORE = 9
    SW_SHOWDEFAULT = 10
    SW_FORCEMINIMIZE = 11
    SW_MAX = 11
  End Enum
  <StructLayout(LayoutKind.Sequential, Pack:=4)> _
  Public Structure PAINTSTRUCT
    Public hdc As IntPtr
    Public fErase As Integer
    Public rcPaint As WinRect
    Public fRestore As Integer
    Public fIncUpdate As Integer
    <MarshalAs(UnmanagedType.ByValArray, SizeConst:=32)> _
    Public rgbReserved As Byte()
  End Structure
  <DllImport("user32.dll")> _
  Public Function BeginPaint( _
     ByVal hWnd As IntPtr, ByRef lpPaint As PAINTSTRUCT) As IntPtr
  End Function
  <DllImport("user32.dll")> _
  Public Function EndPaint( _
     ByVal hWnd As IntPtr, ByRef lpPaint As PAINTSTRUCT) As IntPtr
  End Function
  <DllImport("user32.dll")> _
  Public Function GetDC( _
     ByVal hWnd As IntPtr) As IntPtr
  End Function
  <DllImport("user32.dll")> _
  Public Function ReleaseDC( _
     ByVal hWnd As IntPtr, hdc As IntPtr) As IntPtr
  End Function
  <DllImport("user32.dll")> _
  Public Function FillRect(ByVal hDC As IntPtr, ByRef lpRect As WinRect, ByVal hBR As IntPtr) As IntPtr
  End Function
  <DllImport("user32.dll")> _
  Public Function InvalidateRect(ByVal hWnd As IntPtr, ByRef lpRect As WinRect, ByVal bErase As Boolean) As IntPtr
  End Function
  <DllImport("user32.dll")> _
  Public Function ValidateRect(ByVal hWnd As IntPtr, ByRef lpRect As WinRect) As Boolean
  End Function
  <DllImport("user32.dll")> _
  Public Function GetUpdateRect(ByVal hWnd As IntPtr, ByRef lpRect As WinRect, ByVal bErase As Boolean) As Boolean
  End Function
  <DllImport("gdi32.dll")> _
  Public Function Ellipse(ByVal hDC As IntPtr, nLeft As Integer, nTop As Integer, nRight As Integer, nBottom As Integer) As IntPtr
  End Function
  <DllImport("gdi32.dll")> _
  Public Function CreateSolidBrush(ByVal crColor As IntPtr) As IntPtr
  End Function

  <DllImport("gdi32.dll")> _
  Public Function DeleteObject(ByVal hObject As IntPtr) As IntPtr
  End Function
  <DllImport("gdi32.dll")> _
  Public Function SelectObject(hDC As IntPtr, ByVal hObject As IntPtr) As IntPtr
  End Function
  <DllImport("gdi32.dll")> _
  Public Function MoveToEx(hDC As IntPtr, X As Integer, Y As Integer, ByRef lpPointPrev As WinPoint) As Boolean
  End Function
  <DllImport("gdi32.dll")> _
  Public Function LineTo(hDC As IntPtr, nXEnd As Integer, nYEnd As Integer) As Boolean
  End Function

  <DllImport("gdi32.dll")> _
  Public Function CreatePen(nPenStyle As Integer, nWidth As Integer, nColor As IntPtr) As IntPtr
  End Function


  <DllImport("user32.dll")> _
  Public Function SetProcessDPIAware() As Boolean
  End Function
  <DllImport("user32.dll")> _
  Public Function IsProcessDPIAware() As Boolean
  End Function
  <DllImport("gdi32.dll")> _
  Public Function SetROP2(hdc As IntPtr, fnDrawode As RasterOps) As Boolean
  End Function
#End Region

End Module

</Code Sample>