Smoothing the Pen

k3nnt0ter0

Member
Joined
Aug 21, 2012
Messages
22
Programming Experience
Beginner
I can't make the Pen smooth. If I adjusted the Size of the pen, it'll produce crack/gap. I don't have any idea to fix it. Check the image.
20syjux.png

Here's the code.
VB.NET:
Public Class PaintFormDim PenWidth As Single = 1.0F
    Dim PenPoint As Pen
VB.NET:
Sub ReloadPen(ByVal PenWd As Single, ByVal CurColor As Color)       PenPoint = New Pen(CurColor, PenWd)


   End Sub
VB.NET:
Private Sub PictureBox1_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles PictureBox1.MouseDown
        If e.Button = Windows.Forms.MouseButtons.Left Then
            If drawing = False Then


                startLocation = e.Location


                drawing = True


                If MultiAngleRadioButton.Checked Then


                    If TempLocation.X = -1 Then
                        TempLocation = startLocation
                        TempLocation2 = startLocation
                    End If


                    endLocation = e.Location
                    g.DrawLine(PenPoint, TempLocation, endLocation)
                    TempLocation = endLocation


                ElseIf TriangleRadioButton.Checked Then
                    If TempLocation.X = -1 Then
                        TempLocation = startLocation
                        TempLocation2 = startLocation
                    End If


                    If NumberOfAngle <= 2 Then


                        endLocation = e.Location
                        g.DrawLine(PenPoint, TempLocation, endLocation)
                        TempLocation = endLocation


                        If NumberOfAngle = 2 Then
                            g.DrawLine(PenPoint, TempLocation, TempLocation2)
                            TempLocation = New Point(-1, -1)
                            NumberOfAngle = 0
                        Else
                            NumberOfAngle += 1
                        End If




                    End If




                End If




            End If


        ElseIf e.Button = Windows.Forms.MouseButtons.Right Then


            If MultiAngleRadioButton.Checked Then
                If TempLocation.X <> -1 Then
                    endLocation = e.Location
                    g.DrawLine(PenPoint, TempLocation, TempLocation2)
                    TempLocation = New Point(-1, -1)
                End If


            End If


        End If


    End Sub
VB.NET:
Private Sub PictureBox1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles PictureBox1.MouseMove
        If drawing = True Then
            If LineRadioButton.Checked Then
                g.DrawLine(PenPoint, startLocation.X, startLocation.Y, e.X, e.Y)
                startLocation = e.Location
                UpdateImage()


            ElseIf EraserRadioButton.Checked Then


                Dim p As New Pen(Color.White, PenWidth)


                g.DrawLine(p, startLocation.X, startLocation.Y, e.X, e.Y)
                startLocation = e.Location
                UpdateImage()


            End If


        End If
    End Sub
VB.NET:
Private Sub PictureBox1_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles PictureBox1.MouseUp


        If drawing Then


            If RectangleRadioButton.Checked Then


                endLocation = e.Location


                Dim s As Point


                s.X = endLocation.X - startLocation.X
                If s.X < 0 Then
                    startLocation.X = endLocation.X


                End If


                s.Y = endLocation.Y - startLocation.Y
                If s.Y < 0 Then
                    startLocation.Y = endLocation.Y


                End If


                s.X = Math.Abs(s.X)
                s.Y = Math.Abs(s.Y)
                g.DrawRectangle(PenPoint, New Rectangle(startLocation, s))


            ElseIf GradientRectAngleRadioButton.Checked Then


                endLocation = e.Location


                Dim s As Point




                If s.X < 0 Then
                    startLocation.X = endLocation.X
                ElseIf s.X = 0 Then
                    s.X = 1
                End If


                s.Y = endLocation.Y - startLocation.Y


                If s.Y < 0 Then
                    startLocation.Y = endLocation.Y
                ElseIf s.Y = 0 Then
                    s.Y = 1
                End If


                s.X = Math.Abs(s.X)
                s.Y = Math.Abs(s.Y)


                Dim b As Brush
                b = New Drawing2D.LinearGradientBrush(New Rectangle(startLocation, s), CurrentColor, CurrentColor2, Drawing2D.LinearGradientMode.BackwardDiagonal)
                g.FillRectangle(b, New Rectangle(startLocation, s))


            ElseIf CircleRadioButton.Checked Then


                endLocation = e.Location
                Dim s As Point


                s.X = endLocation.X - startLocation.X


                If s.X < 0 Then
                    startLocation.X = endLocation.X


                End If


                s.Y = endLocation.Y - startLocation.Y


                If s.Y < 0 Then
                    startLocation.Y = endLocation.Y


                End If


                s.X = Math.Abs(s.X)
                s.Y = Math.Abs(s.Y)


                If s.X > s.Y Then
                    s.Y = s.X
                Else
                    s.X = s.Y
                End If


                g.DrawEllipse(PenPoint, New Rectangle(startLocation, s))


            ElseIf ArcRadioButton.Checked Then
                endLocation = e.Location


                Dim s As Point


                s.X = endLocation.X - startLocation.X


                If s.X < 0 Then
                    startLocation.X = endLocation.X


                End If


                s.Y = endLocation.Y - startLocation.Y


                If s.Y < 0 Then
                    startLocation.Y = endLocation.Y


                End If


                s.X = Math.Abs(s.X)
                s.Y = Math.Abs(s.Y)


                If s.X > s.Y Then
                    s.Y = s.X
                Else
                    s.X = s.Y
                End If


                g.DrawArc(PenPoint, New Rectangle(startLocation, s), 0, -180)


            ElseIf ParallelepipedRadioButton.Checked Then


                endLocation = e.Location


                Dim s As Point


                s.X = endLocation.X - startLocation.X


                If s.X < 0 Then
                    Dim tmp As Integer = startLocation.X
                    startLocation.X = endLocation.X
                    endLocation.X = tmp
                End If


                s.Y = endLocation.Y - startLocation.Y


                If s.Y < 0 Then
                    Dim tmp As Integer = startLocation.Y
                    startLocation.Y = endLocation.Y
                    endLocation.Y = tmp
                End If


                s.X = Math.Abs(s.X)
                s.Y = Math.Abs(s.Y)






                Dim p(3) As Point


                p(0) = New Point(startLocation.X + s.X / 5, startLocation.Y)
                p(1) = New Point(startLocation.X + s.X, startLocation.Y)


                p(2) = New Point(endLocation.X - s.X / 5, endLocation.Y)
                p(3) = New Point(endLocation.X - s.X, endLocation.Y)


                g.DrawPolygon(PenPoint, p)


            ElseIf FillRadioButton.Checked Then
                FillRegion(e.X, e.Y, CurrentColor)


            ElseIf TextRadioButton.Checked Then
                Dim txt As String = Me.TextDrawTextBox.Text
                g.DrawString(txt, CurrentFont, New Drawing2D.HatchBrush(Drawing2D.HatchStyle.BackwardDiagonal, CurrentColor), e.X, e.Y)


            End If


        End If


        drawing = False


        UpdateImage()


    End Sub
VB.NET:
Private Sub PaintForm_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load


        g = Graphics.FromImage(LastImage)
        g.Clear(Color.White)
        UpdateImage()
        ReloadPen(PenWidth, CurrentColor)


    End Sub
 
Hi k3etc.,
Your line is breaking up because you are drawing it by adding a tiny short line each time the MouseMove event fires. By default, a GDI+ pen draws a line with flat ends, and the pen is angled to match the angle of the line. So when you use a wide brush each segments shows up as a little block turned to the direction of mouse movement. A quick fix would be to give the pen round end caps, for example in your ReloadPen sub:
VB.NET:
        PenPoint = New Pen(CurColor, PenWd)
        [B]PenPoint.StartCap = Drawing2D.LineCap.Round
          [/B][B]PenPoint.EndCap = Drawing2D.LineCap.Round   
[/B]

You could further improve the appearance of the line by setting the graphics SmoothingMode, for example in your Form Load sub:
VB.NET:
g = Graphics.FromImage(LastImage)
[B]g.SmoothingMode = Drawing2D.SmoothingMode.HighQuality[/B]
That will give anti-aliased edges to drawn lines and the edges of filled shapes.

All the same, I think a better way of drawing freehand lines is to save all the points in a line. For example you could add each mouse position to a List(Of Point) in the MouseMove sub. Then draw the whole line in one go with g.DrawLine or g.DrawCurve (using List.ToArray to turn the list into an array), for example in your UpdateImage sub or in the Paint event of the picture box.

That's because a pen is never smooth. Use a brush.
I disagree with those statements. Can you point to an example that backs them up?

VicJ
 
@VicJ, I am almost near giving up about this problem yet you came and helped me! THANK YOU VERY MUCH!!!

Anyway, i still have a problem. Can I get rid of the Circles? I am using the same pen (PenPoint) with that and I have a button that is for Highlighting that reduces its Alpha. The circles are visible as I move the mouse fast and the larger Pen I use. Can it be possible to only use that light color and no circles?
24q64yf.png
 
Last edited:
That's one of the disadvantages of drawing a lot of short lines instead of the whole line: when you use a transparent pen you can see where they overlap. So I suggest it's time to change to keeping a list of the points as you move the mouse, and then drawing them as a single, continuous line.

At the moment, you draw all the other elements such as rectangles, ellipses etc. in the MouseUp sub. That won't do in the case of a freehand line because you need to see what you are drawing as the mouse moves. So I suggest drawing the line in the picture box's Paint sub while you are drawing it. Then add it to the picture box image (LastImage) in the MouseUp sub. Here's an outline of how you can do it.

VB.NET:
[COLOR=#008000]'at top level:[/COLOR]
[COLOR=#a52a2a][B]Dim freehandLine As New List(Of Point)[/B]
[/COLOR]
[COLOR=#008000]'in the picture box MouseDown sub, start a new line:[/COLOR]
    startLocation = e.Location
    drawing = True
 [COLOR=#a52a2a]  [B]freehandLine = New List(Of Point) [/B][/COLOR][B][COLOR=#a52a2a]
    freehandLine.Add(startLocatio[/COLOR][COLOR=#a52a2a]n)
    [/COLOR][COLOR=#0000ff]If LineRadioButton.Checked Then
            ReloadPen(PenWidth, CurrentColor)
    ElseIf EraserRadioButton.Checked Then
           ReloadPen(PenWidth, Color.White)
   EndIf

[/COLOR]     [/B][COLOR=#008000]'etc.[/COLOR]
[COLOR=#008000]
'in the picture box MouseMove sub, add e.Location to the line and invalidate the picture box. This replaces the present code:[/COLOR][B]
   [COLOR=#a52a2a]     If drawing = True Then
            If LineRadioButton.Checked OrElse EraserRadioButton.Checked Then
                freehandLine.Add(e.Location)
                PictureBox1.Invalidate()
            End If
        End If[/COLOR][/B]
        
[COLOR=#008000] 'in the picture box MouseUp sub, add the completed lines to LastImage :[/COLOR]
    If drawing Then[B]
         [COLOR=#0000ff] If (LineRadioButton.Checked OrElse EraserRadioButton.Checked) _
          AndAlso freehandLine.Count > 1 Then
              g.DrawCurve(PenPoint, freehandLine.ToArray)          [/COLOR][/B]
        ElseIf RectangleRadioButton.Checked Then
         'etc.    

[COLOR=#008000]'in the picture box Paint sub, draw the current line only while [B]drawing[/B] is true:[/COLOR]
[COLOR=#a52a2a]    [B]If drawing Then
          [/B][/COLOR][COLOR=#0000ff][B]e.Graphics.SmoothingMode = HighQuality[/B][/COLOR][COLOR=#a52a2a][/COLOR][COLOR=#0000ff][B]If (LineRadioButton.Checked OrElse EraserRadioButton.Checked) _ 
          AndAlso freehandLine.Count > 1 Then 
              e.Graphics.DrawCurve(PenPoint, freehandLine.ToArray)
          End If
[/B][/COLOR][COLOR=#a52a2a][B]    End If[/B]
 
[/COLOR]
The code in the Paint sub is practically the same as in the MouseUp sub, except it uses the picture box's own Graphics object (e.Graphics).

I haven't been able to test this out in your code, so there could be errors. If you have trouble implementing it, please zip up the entire project (excluding the bin and obj folders) and post it as an attachment. There are too many things missing from the code in your first post.

Edit : I made some improvements to the suggested code to make the eraser work better. See blue. You could also improve thick lines a bit more by adding
VB.NET:
    [B]PenPoint.LineJoin = Drawing2D.LineJoin.Round
[/B]
to the ReloadPen sub.

VicJ
 
Last edited:
Thank you very much sir for the help! I tried your codes and I can see improvements on this program. I have some errors on the last part but I corrected it somehow. Correct me if I'm wrong, the Paint Event looks like this?
VB.NET:
Private Sub PictureBox1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint        If drawing Then
            e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.HighQuality
            If (LineRadioButton.Checked OrElse EraserRadioButton.Checked) _
          AndAlso freehandLine.Count > 1 Then
                e.Graphics.DrawCurve(PenPoint, freehandLine.ToArray)
            End If
        End If
    End Sub

Anyway, are there any example on how to draw a line as single? I really need to make a Highligher Pen.
 
Last edited:
Thank you very much sir for the help! I tried your codes and I can see improvements on this program. I have some errors on the last part but I corrected it somehow. Correct me if I'm wrong, the Paint Event looks like this?
VB.NET:
Private Sub PictureBox1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint       
     If drawing Then
            e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.HighQuality
            If (LineRadioButton.Checked OrElse EraserRadioButton.Checked) _
          AndAlso freehandLine.Count > 1 Then
                e.Graphics.DrawCurve(PenPoint, freehandLine.ToArray)
            End If
        End If
    End Sub
That's correct.
Anyway, are there any example on how to draw a line as single? I really need to make a Highligher Pen.
By "line as single", do you mean a straight line. That's not hard to do. Declare a point variable (call it endLocation) at form level. Then, when you are using the Straight Line tool, test for the tool and set endLocation = e.Location in the MouseMove sub and Invalidate the picture box. In the picture box Paint sub, draw the line like this:
VB.NET:
e.Graphics.DrawLine(PenPoint, StartLocation, endLocation)
In the MouseUp sub, add the line to the final bitmap in a similar way.

By the way, it's a chore duplicating things in the Paint sub and the MouseUp sub, but the way your original program is structured makes it necessary. I don't plan to change that for you.

Vic
 
I tried what you have taught to me but I have problem regarding its PenWidth. I can't change its size.
VB.NET:
Imports SystemImports System.Drawing
Imports System.Windows.Forms


Public Class Form1


    Inherits System.Windows.Forms.Form
    Dim mousePath As New System.Drawing.Drawing2D.GraphicsPath()
    Dim myAlpha As Integer = 255
    Dim myUserColor As New Color()
    Dim myPenWidth As Single = 20
    'Dim PenPoint As New Pen(Color.FromArgb(myAlpha, myUserColor), myPenWidth)


    Private Sub PictureBox1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles PictureBox1.MouseDown


        If e.Button = MouseButtons.Left Then
            mousePath.StartFigure()
        End If
    End Sub




    Private Sub PictureBox1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles PictureBox1.MouseMove


        If e.Button = MouseButtons.Left Then
            Try
                mousePath.AddLine(e.X, e.Y, e.X, e.Y)
            Catch
                MsgBox("No way, Hose!")
            End Try
        End If
        PictureBox1.Invalidate()


    End Sub


    Private Sub PictureBox1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint


        Try
            myUserColor = (System.Drawing.Color.Black)
            myAlpha = 100
            myPenWidth = Slider1.Value
            e.Graphics.DrawPath(PenPoint, mousePath)
        Catch
        End Try


    End Sub
    
    Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load


        PenPoint.StartCap = Drawing2D.LineCap.Round
        PenPoint.EndCap = Drawing2D.LineCap.Round
        PenPoint.LineJoin = Drawing2D.LineJoin.Round
    End Sub
End Class
I tried also without using the PenPoint.StartCap etc. but the pen is not stacking. As I Increase or Decrease its pensize, what I wrote previously increases/decreases also.
VB.NET:
Imports System
Imports System.Drawing
Imports System.Windows.Forms


Public Class Form1


    Inherits System.Windows.Forms.Form
    Dim mousePath As New System.Drawing.Drawing2D.GraphicsPath()
    Dim myAlpha As Integer = 255
    Dim myUserColor As New Color()
    Dim myPenWidth As Single = 20
    'Dim PenPoint As New Pen(Color.FromArgb(myAlpha, myUserColor), myPenWidth)

     Private Sub PictureBox1_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) Handles PictureBox1.MouseDown


        If e.Button = MouseButtons.Left Then
            mousePath.StartFigure()
        End If
    End Sub




    Private Sub PictureBox1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles PictureBox1.MouseMove


        If e.Button = MouseButtons.Left Then
            Try
                mousePath.AddLine(e.X, e.Y, e.X, e.Y)
            Catch
                MsgBox("No way, Hose!")
            End Try
        End If
        PictureBox1.Invalidate()


    End Sub


    Private Sub PictureBox1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles PictureBox1.Paint


        Try
            myUserColor = (System.Drawing.Color.Black)
            myAlpha = 100
            Dim PenPoint As New Pen(Color.FromArgb(myAlpha, myUserColor), myPenWidth)
            e.Graphics.DrawPath(PenPoint, mousePath)
        Catch
        End Try


    End Sub


    Private Sub Slider1_ValueChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Slider1.ValueChanged


        myPenWidth = Slider1.Value


    End Sub


    'Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load


        'PenPoint.StartCap = Drawing2D.LineCap.Round
        'PenPoint.EndCap = Drawing2D.LineCap.Round
        'PenPoint.LineJoin = Drawing2D.LineJoin.Round
   ' End Sub
End Class
 
Last edited:
In order to have a full semi transparent line without seeing the pen shape, the pen tip would need to be dithered. Instead of having just one full circle you would need a percentage of the outside circumference to be half colored pixels half transparent pixels.
 
Hi k3nn, it was a good idea to make a fresh start if you don't need all the code for drawing rectangles, ellipses etc. The reason your line strokes were not "stacking" is that you were drawing the whole graphics path with the same pen. Just putting in StartFigure won't help. Figures are a way of dividing a path into subpaths, but using them is a bit complicated. I think it's easier to store each stroke as a separate path.

The graphics path stores the route of the line stroke but it doesn't store the colour or width. That information is in the pen. One way of dealing this would be to define a structure for pairing a pen with each path. Then make list for storing the structures:
VB.NET:
Structure Stroke
   Public Path As Drawing2D.GraphicsPath
   Public Pen as Pen
End Structure

Dim StrokeList As New List(Of Stroke)
Then, in the Paint event sub, you can draw all the strokes in their individual widths and colours:
VB.NET:
For Each strk As Stroke in StrokeList
    e.Graphics.DrawPath(strk.Pen, strk.Path)
Next
I hope you get the principle of this. If you are interested in going this way, I can help you get it working.

VicJ
 
Back
Top