Search code examples
.netvb.netwinformsparent

"ShowDialog" method is not working properly (when setting the Form parent?)


I'm trying to write a Form that should draw a rectangle over the screen to select an screen region, the question and the solution(s) can be seen in other StackOverflow post, here. I've adapted the marked solution of @ZeroWorks to my needs, but the problem now is that I can't find the way to use the Form like I require.

This is how I would like to use it:

    Dim RegionRect As Rectangle = Rectangle.Empty

    Using Selector As New RegionSelector
        Selector.ShowDialog()
        RegionRect = Selector.SelectedRegion
    End Using

I just would like to use the ShowDialog method to show the Form and stop the execution until the region is selected by the user 'cause on the OnMouseUp event I close the form, but If I try to use the ShowDialog method I'm not able to draw/see the rectangle, by the way if I use the Show method it works properly but like I've said I need to use instead the ShowDialog method to wait a "response" from the Form to proceed with the next instructions, I don't understand why happens this problem, what I'm missing?, how I could fix this?.

This is the (full) code of the custom region selector:

''' <summary>
''' Selects a region on the Screen.
''' </summary>
Public Class RegionSelector : Inherits Form

#Region " Properties "

    ''' <summary>
    ''' Gets or sets the border size of the region selector.
    ''' </summary>
    ''' <value>The size of the border.</value>
    Public Property BorderSize As Integer = 2

    ''' <summary>
    ''' Gets or sets the border color of the region selector.
    ''' </summary>
    ''' <value>The color of the border.</value>
    Public Property BorderColor As Color = Color.Red

    ''' <summary>
    ''' Gets the rectangle that contains the selected region.
    ''' </summary>
    Public ReadOnly Property SelectedRegion As Rectangle
        Get
            Return Me.DrawRect
        End Get
    End Property

#End Region

#Region " Objects "

    ''' <summary>
    ''' Indicates the initial location when the mouse left button is clicked.
    ''' </summary>
    Private InitialLocation As Point = Point.Empty

    ''' <summary>
    ''' The rectangle where to draw the region.
    ''' </summary>
    Public DrawRect As Rectangle = Rectangle.Empty

    ''' <summary>
    ''' The Graphics object to draw on the screen.
    ''' </summary>
    Private ScreenGraphic As Graphics = Graphics.FromHwnd(IntPtr.Zero)

    Public IsDrawing As Boolean = False

    Dim DrawSize As Size

    Dim DrawForm As Form

#End Region

#Region " Constructors "

    ''' <summary>
    ''' Initializes a new instance of the <see cref="RegionSelector"/> class.
    ''' </summary>
    Public Sub New()
    End Sub

    ''' <summary>
    ''' Initializes a new instance of the <see cref="RegionSelector" /> class.
    ''' </summary>
    ''' <param name="BorderSize">Indicates the border size of the region selector.</param>
    ''' <param name="BorderColor">Indicates the border color of the region selector.</param>
    Public Sub New(ByVal BorderSize As Integer,
                   ByVal BorderColor As Color)

        Me.BorderSize = BorderSize
        Me.BorderColor = BorderColor

    End Sub

#End Region

#Region " Event Handlers "

    ''' <summary>
    ''' Handles the Load event of the RegionSelector.
    ''' </summary>
    ''' <param name="sender">The source of the event.</param>
    ''' <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
    Private Sub RegionSelector_Load(sender As Object, e As EventArgs) Handles Me.Load

        Me.SuspendLayout()

        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None
        Me.BackColor = System.Drawing.Color.Black
        Me.BackgroundImageLayout = System.Windows.Forms.ImageLayout.None
        Me.CausesValidation = False
        Me.ClientSize = New System.Drawing.Size(0, 0)
        Me.ControlBox = False
        Me.Cursor = System.Windows.Forms.Cursors.Cross
        Me.DoubleBuffered = True
        Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None
        Me.MaximizeBox = False
        Me.MinimizeBox = False
        Me.Name = "RegionSelector"
        Me.Opacity = 0.15R
        Me.ShowIcon = False
        Me.ShowInTaskbar = False
        Me.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide
        Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen
        Me.TopMost = False
        Me.WindowState = System.Windows.Forms.FormWindowState.Maximized

        Me.ResumeLayout(True)

        Me.DrawForm = New DrawingRegionClass(Me)
        With DrawForm
            .BackColor = Color.Tomato
            .TopLevel = True
            .TransparencyKey = Color.Tomato
            .TopMost = False
            .FormBorderStyle = Windows.Forms.FormBorderStyle.None
            .ControlBox = False
            .WindowState = FormWindowState.Maximized
        End With

        Me.AddOwnedForm(Me.DrawForm)
        Me.DrawForm.Show()

    End Sub

    ''' <summary>
    ''' Raises the <see cref="E:System.Windows.Forms.Control.MouseDown" /> event.
    ''' </summary>
    ''' <param name="e">A <see cref="T:System.Windows.Forms.MouseEventArgs" /> that contains the event data.</param>
    Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)

        If e.Button = MouseButtons.Left Then

            Me.InitialLocation = e.Location
            Me.IsDrawing = True

        End If

    End Sub

    ''' <summary>
    ''' Raises the <see cref="E:System.Windows.Forms.Control.MouseUp" /> event.
    ''' </summary>
    ''' <param name="e">A <see cref="T:System.Windows.Forms.MouseEventArgs" /> that contains the event data.</param>
    Protected Overrides Sub OnMouseUp(ByVal e As MouseEventArgs)

        Me.IsDrawing = False
        Me.Close()

    End Sub

    ''' <summary>
    ''' Raises the <see cref="E:System.Windows.Forms.Control.MouseMove" /> event.
    ''' </summary>
    ''' <param name="e">A <see cref="T:System.Windows.Forms.MouseEventArgs" /> that contains the event data.</param>
    Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs)

        If Me.IsDrawing Then

            Me.DrawSize = New Size(e.X - Me.InitialLocation.X, e.Y - Me.InitialLocation.Y)
            Me.DrawRect = New Rectangle(Me.InitialLocation, Me.DrawSize)

            If Me.DrawRect.Height < 0 Then
                Me.DrawRect.Height = Math.Abs(Me.DrawRect.Height)
                Me.DrawRect.Y -= Me.DrawRect.Height
            End If

            If Me.DrawRect.Width < 0 Then
                Me.DrawRect.Width = Math.Abs(Me.DrawRect.Width)
                Me.DrawRect.X -= Me.DrawRect.Width
            End If

            Me.DrawForm.Invalidate()

        End If

    End Sub

#End Region

End Class

Public Class DrawingRegionClass : Inherits Form

    Private DrawParent As RegionSelector

    Public Sub New(ByVal Parent As Form)

        Me.DrawParent = Parent

    End Sub

    Protected Overrides Sub OnPaintBackground(ByVal e As PaintEventArgs)

        Dim Bg As Bitmap
        Dim Canvas As Graphics

        If Me.DrawParent.IsDrawing Then

            Bg = New Bitmap(Width, Height)
            Canvas = Graphics.FromImage(Bg)
            Canvas.Clear(Color.Tomato)
            Canvas.DrawRectangle(Pens.Red, Me.DrawParent.DrawRect)
            Canvas.Dispose()
            e.Graphics.DrawImage(Bg, 0, 0, Width, Height)
            Bg.Dispose()

        Else
            MyBase.OnPaintBackground(e)
        End If

    End Sub

End Class

Solution

  • I wouldn't do it with formdialog method, you will have a lot of troubles to get it work, form dialog blocks execution and waits until its closed. But there's is a solution instead... I would use a Callback. So, first define a Delegate:

    Public Delegate Sub RegionSelectedDelegate(Region As Rectangle)
    

    Then in the calling form, in a sub or function, since using will destroy form change it:

    Dim RegionRect As Rectangle = Rectangle.Empty
    Dim Working As Boolean = False
    
    Public Sub GetRectangle()
        Dim Callback As RegionSelectedDelegate
        Dim Selector As New RegionSelector
    
        If Working Then Exit Sub 'Only one selection at once!
        Working = True
        Callback = New RegionSelectedDelegate(AddressOf RectangleDrawn)
    
        With Selector
            .Callback = Callback
            .Show()
        End With
        ' Don't do any stuff here... do it in Rectangle Drawn...
    End Sub
    

    ...

    Public Sub RectangleDrawn(Region as Rectangle)   
        Working = false 'Allow draw again.
        Me.RegionRect=Region
        MessageBox.Show("Do next steps!")
        ' Some stuff Here
    End Sub
    

    That delegate will be called from drawing form on onmouseup (when the rectangle is drawn) and will be received by RegionSelector Class. So, will define a new property Callback in RegionSelector Class, and invoke this delegate in onmouseup event handler:

    ''' <summary>
    ''' Selects a region on the Screen.
    ''' </summary>
    Public Class RegionSelector : Inherits Form
    
    #Region " Properties "
    
    ''' <summary>
    ''' Callback to be invoked when drawing is done...
    ''' </summary>
    ''' <value>Delegate of Region Selected</value>
    Public Property Callback As RegionSelectedDelegate = Nothing
    
    
    ''' <summary>
    ''' Gets or sets the border size of the region selector.
    ''' </summary>
    ''' <value>The size of the border.</value>
    Public Property BorderSize As Integer = 2
    
    ''' <summary>
    ''' Gets or sets the border color of the region selector.
    ''' </summary>
    ''' <value>The color of the border.</value>
    Public Property BorderColor As Color = Color.Red
    
    ''' <summary>
    ''' Gets the rectangle that contains the selected region.
    ''' </summary>
    Public ReadOnly Property SelectedRegion As Rectangle
        Get
            Return Me.DrawRect
        End Get
    End Property
    
    #End Region
    
    #Region " Objects "
    
    ''' <summary>
    ''' Indicates the initial location when the mouse left button is clicked.
    ''' </summary>
    Private InitialLocation As Point = Point.Empty
    
    ''' <summary>
    ''' The rectangle where to draw the region.
    ''' </summary>
    Public DrawRect As Rectangle = Rectangle.Empty
    
    ''' <summary>
    ''' The Graphics object to draw on the screen.
    ''' </summary>
    Private ScreenGraphic As Graphics = Graphics.FromHwnd(IntPtr.Zero)
    
    Public IsDrawing As Boolean = False
    
    Dim DrawSize As Size
    
    Dim DrawForm As Form
    
    #End Region
    
    #Region " Constructors "
    
    ''' <summary>
    ''' Initializes a new instance of the <see cref="RegionSelector"/> class.
    ''' </summary>
    Public Sub New()
    End Sub
    
    ''' <summary>
    ''' Initializes a new instance of the <see cref="RegionSelector" /> class.
    ''' </summary>
    ''' <param name="BorderSize">Indicates the border size of the region selector.</param>
    ''' <param name="BorderColor">Indicates the border color of the region selector.</param>
    Public Sub New(ByVal BorderSize As Integer,
                   ByVal BorderColor As Color)
    
        Me.BorderSize = BorderSize
        Me.BorderColor = BorderColor
    
    End Sub
    
    #End Region
    
    #Region " Event Handlers "
    
    ''' <summary>
    ''' Handles the Load event of the RegionSelector.
    ''' </summary>
    ''' <param name="sender">The source of the event.</param>
    ''' <param name="e">The <see cref="EventArgs"/> instance containing the event data.</param>
    Private Sub RegionSelector_Load(sender As Object, e As EventArgs) Handles Me.Load
    
        Me.SuspendLayout()
    
        Me.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None
        Me.BackColor = System.Drawing.Color.Black
        Me.BackgroundImageLayout = System.Windows.Forms.ImageLayout.None
        Me.CausesValidation = False
        Me.ClientSize = New System.Drawing.Size(0, 0)
        Me.ControlBox = False
        Me.Cursor = System.Windows.Forms.Cursors.Cross
        Me.DoubleBuffered = True
        Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None
        Me.MaximizeBox = False
        Me.MinimizeBox = False
        Me.Name = "RegionSelector"
        Me.Opacity = 0.15R
        Me.ShowIcon = False
        Me.ShowInTaskbar = False
        Me.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide
        Me.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen
        Me.TopMost = False
        Me.WindowState = System.Windows.Forms.FormWindowState.Maximized
    
        Me.ResumeLayout(True)
    
        Me.DrawForm = New DrawingRegionClass(Me)
        With DrawForm
            .BackColor = Color.Tomato
            .TopLevel = True
            .TransparencyKey = Color.Tomato
            .TopMost = False
            .FormBorderStyle = Windows.Forms.FormBorderStyle.None
            .ControlBox = False
            .WindowState = FormWindowState.Maximized
        End With
    
        Me.AddOwnedForm(Me.DrawForm)
        Me.DrawForm.Show()
    
    End Sub
    
    ''' <summary>
    ''' Raises the <see cref="E:System.Windows.Forms.Control.MouseDown" /> event.
    ''' </summary>
    ''' <param name="e">A <see cref="T:System.Windows.Forms.MouseEventArgs" /> that contains the event data.</param>
    Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
    
        If e.Button = MouseButtons.Left Then
    
            Me.InitialLocation = e.Location
            Me.IsDrawing = True
    
        End If
    
    End Sub
    
    ''' <summary>
    ''' Raises the <see cref="E:System.Windows.Forms.Control.MouseUp" /> event.
    ''' </summary>
    ''' <param name="e">A <see cref="T:System.Windows.Forms.MouseEventArgs" /> that contains the event data.</param>
    Protected Overrides Sub OnMouseUp(ByVal e As MouseEventArgs)
        'Do the callback here!
        Me.IsDrawing = False
        Callback.Invoke(SelectedRegion)
        Me.Close() 'Must be called last.
    End Sub
    
    ''' <summary>
    ''' Raises the <see cref="E:System.Windows.Forms.Control.MouseMove" /> event.
    ''' </summary>
    ''' <param name="e">A <see cref="T:System.Windows.Forms.MouseEventArgs" /> that contains the event data.</param>
    Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs)
    
        If Me.IsDrawing Then
    
            Me.DrawSize = New Size(e.X - Me.InitialLocation.X, e.Y - Me.InitialLocation.Y)
            Me.DrawRect = New Rectangle(Me.InitialLocation, Me.DrawSize)
    
            If Me.DrawRect.Height < 0 Then
                Me.DrawRect.Height = Math.Abs(Me.DrawRect.Height)
                Me.DrawRect.Y -= Me.DrawRect.Height
            End If
    
            If Me.DrawRect.Width < 0 Then
                Me.DrawRect.Width = Math.Abs(Me.DrawRect.Width)
                Me.DrawRect.X -= Me.DrawRect.Width
            End If
    
            Me.DrawForm.Invalidate()
    
        End If
    
    End Sub
    
    #End Region
    
    End Class
    
    Public Class DrawingRegionClass : Inherits Form
    
    Private DrawParent As RegionSelector
    
    Public Sub New(ByVal Parent As Form)
    
        Me.DrawParent = Parent
    
    End Sub
    
    Protected Overrides Sub OnPaintBackground(ByVal e As PaintEventArgs)
    
        Dim Bg As Bitmap
        Dim Canvas As Graphics
    
        If Me.DrawParent.IsDrawing Then
    
            Bg = New Bitmap(Width, Height)
            Canvas = Graphics.FromImage(Bg)
            Canvas.Clear(Color.Tomato)
            Canvas.DrawRectangle(Pens.Red, Me.DrawParent.DrawRect)
            Canvas.Dispose()
            e.Graphics.DrawImage(Bg, 0, 0, Width, Height)
            Bg.Dispose()
    
        Else
            MyBase.OnPaintBackground(e)
        End If
    
    End Sub
    
    End Class
    

    And that's all, no blocking modal forms are shown, the flow stills its logic and it works. Hope it helps.