Owner Draw in VB (Matt Gertz)

Owner Draw in VB (Matt Gertz)

  • Comments 6

I hate going so long without blogging, but it’s getting really busy around here as we get closer to the endgame for Visual Studio 2008.  That, combined with “review time,” creates a bit of a problem for writing apps for the blog, and I wasn’t able to give it much attention until last night.  Realizing I really needed to get an article out, I was wracking my brains for something to write about. 

I happened to notice that my wife was working on some puzzles in her “Games” magazine – she was on one of her favorites, which is called Paint-by-Numbers.   I thought that it might be cool to write her a small app in which I could generate some of those puzzles for her so she wouldn’t have to wait until the next issue arrived.  Then I realized, “Ah, what a perfect blog topic!  There are so many things I could cover in such an app that I haven’t covered before.”  So, here goes – I’ll be writing up this app over the next couple of weeks (posting the final product at the end of the series), and will document the approach for the following topics:

·         Owner draw and coordinate mapping on events (shown in this blog post)

·         Supporting undo/redo using a double list of action classes, leveraging generics for this.

·         Loading  & Saving puzzles

·         Printing the puzzle and the solution

Paint-by-Numbers background

Paint-by-numbers is a puzzle wherein you’re given a grid, and on the outside of the grid you’re given clues about which parts of the grid should be filled in.  For example, here’s a simple 8x8 PBN puzzle which has been solved already:

 

5

1

1

1

5

 

8

1

1

1

2

2

1 1 2

 

 

 

 

 

 

 

 

 

1 1 1 1

 

 

 

 

 

 

 

 

 

1 1 1 1

 

 

 

 

 

 

 

 

 

1 1 2

 

 

 

 

 

 

 

 

 

1 1 1 1

 

 

 

 

 

 

 

 

 

1 1 1 1

 

 

 

 

 

 

 

 

 

1 2

 

 

 

 

 

 

 

 

 

 

The numbers indicate how many consecutive gridboxes are filled in, and the order in which they are filled in for a given row/column, but not their precise location.  For example, if you look at the first row, you’ll see one block filled in, then white space, then another block filled in, then white space, then two blocks filled in – thus, “1 1 2” for the clue for that row.  The amount of whitespace is unknown to the puzzle-solver.

For our application, we’ll create a grid just like the one above.  The puzzle-maker will launch the application and fill in the grid to make a mystery picture – the application will automatically calculate the row clues.  Users should be able to save their work, re-load it, undo/redo any drawing, and then finally print out the grid (with just the clues, not the picture, of course) to let someone else enjoy solving the puzzle.

Starting out

The first thing we need is a Windows Application, so I'll go ahead & fire up Visual Studio 2005 and create a VB Windows application called “VBPaintByNumbers.”  I’ll set “Test” to “VB Paint-by-Numbers”  and  the form background color to be white. 

Now, I need some way to represent the grid internally.  Right-click the form and choose “View Code," and we'll start adding code to handle the various aspects of the grid.  I’ll want an easy way to remember all of the sizes, so:

    Const HorizontalOffsetAs Integer = 10

    Const VerticalOffsetAs Integer = 10

    Const CellSize As Integer = 16

    Const Columns As Integer = 24

    Const Rows As Integer = 24

    Const FontSize As Integer = 8

 

I’ll use bytes to represent on/off for a given cell (in fact, a bit is all I need for each cell – a byte is overkill – but for the small amount of memory we’re talking about, I’m willing to live with an 8x inflation of memory for the sake of code clarity).  Let’s add the following member to the class:

    Dim Grid(Rows, Columns) As Byte

 

I need to figure out the proper size of the form.  As implied above, I’m going to work with a fixed grid of 24x24 just for expediency, but of course this application could be readily designed to take values from the author to decide the grid size, so my code will be designed in such a way that I never hardcode numbers outside of the constants, making it easily adaptable to arbitrary sizes in the future.  I want there to be an margin offset so that the form is nicely centered, I need space for the grid itself, and I need space for the clues.  Those are the obvious ones – however, if I’m going to change the size of the form programmatically, I also need to account for the size of the window’s borders, title bar, etc.  Fortunately, I can get the latter very easily by just determining the difference between the form’s initial size and the form’s initial client size (i.e., the area within the window).  So, I’ll double-click on the background of the form and fill in the following code:

    Private Sub Form1_Load(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles MyBase.Load

 

        ' Figure out the difference between form size and client size

        Dim HorizontalDiff As Integer = Me.Width - Me.ClientSize.Width

        Dim VerticalDiff As Integer = Me.Height - Me.ClientSize.Height

 

        Me.Width = HorizontalOffset + (Columns * FontSize) _

+ (Columns * CellSize) + HorizontalOffset + HorizontalDiff

        Me.Height = VerticalOffset + (Rows * FontSize) _

 + (Rows * CellSize) + VerticalOffset + VerticalDiff

 

    End Sub

Finally, when I print a given puzzle, I may want to print it without the grid being filled in (the puzzle), or with the grid filled in (the solution), so I’m also going to add a boolean to determine whether or not to show the cells in their filled-in state:

    Dim ShowSolution As Boolean = True

 

By default, we will always show the grid as appropriately filled in.

Now our form is ready to go, and we can concentrate on painting.

Painting

I like modular code, and for this app, modular code is really required to do proper painting.  I’m going to break up the code as follows:

1.       Paint the gridlines (PaintGridlines)

2.       Paint the cells (PaintCells)

3.       Paint the clues (PaintClues)

Let’s start with PaintGridLines.  First, I’m going to import System.Drawing to make the typing a little easier, so I’ll add this to the top of the code:

Imports System.Drawing

 

Now, let’s draw.  I’m going to take a shortcut here and use the line drawing snippet the ships with VB and modify it slightly.  (You can get to the snippet by typing ? + TAB and then navigating to Windows Forms Application, then Drawing, then “Draw a Line on a Windows Form.”)  That snippet shows me how to use the Graphics class (which defines a unique graphics object containing drawing methods), as well as the Pen class.  The canvas I'll be drawing on is just the form's background -- there are no controls involved!  After modifying the result a bit, I end up with:

    Private Sub PaintGridlines()

        Dim LineWidth As Integer = (Rows * FontSize) + (Rows * CellSize)

        Dim LineHeight As Integer = (Columns * FontSize) _

+ (Columns * CellSize)

        Using myPen As New Pen(Color.Black), _

            formGraphics As Graphics = Me.CreateGraphics()

            For i As Integer = 0 To Rows

                Dim LineTop As Integer = VerticalOffset + _

(Rows * FontSize) + i * CellSize

                formGraphics.DrawLine(myPen, HorizontalOffset, LineTop, _

HorizontalOffset + LineWidth, LineTop)

            Next

            For i As Integer = 0 To Columns

                Dim LineLeft As Integer = HorizontalOffset + _

(Columns * FontSize) + i * CellSize

                formGraphics.DrawLine(myPen, LineLeft, VerticalOffset, _

                     LineLeft, VerticalOffset + LineHeight)

            Next

        End Using

    End Sub

 

Let’s see if it worked.  In the left dropdown at the top of the editor, select “Form1 Events, ” and then in the right dropdown, select “Paint.”  In the resulting method that gets generated, insert the call to  PaintGridlines() and press F5.  Voila!   Now we’ve got a grid painting itself on the form. 

Now, I need to fill in the grid cells based on the array.  This is going to be very similar to the line painting, except that we’ll paint a rectangle, and do that only if the corresponding cell is supposed to be filled in.  There’s also a “fill rectangle” snippet for this which again uses the Graphics class, which I’ll insert and modify as so:

    Private Sub PaintCells()

        If ShowSolution = True Then

            Using myBrush As New SolidBrush(Color.Black), _

                formGraphics As Graphics = Me.CreateGraphics()

                For i As Integer = 0 To Rows - 1

                    For j As Integer = 0 To Columns - 1

                        If Grid(i, j) = 1 Then

                            formGraphics.FillRectangle(myBrush, _

                                HorizontalOffset + (Columns * FontSize) + j * CellSize, _

                                VerticalOffset + (Rows * FontSize) + i * CellSize, _

                                CellSize, _

                                CellSize)

                        End If

                    Next

                Next

            End Using

        End If

    End Sub

 

Note that I’m explicitly checking to see if “ShowSolution” is true before I draw cells.  This will come handy later when I want to print out the puzzle.  Having created this method, I can now call it in the Paint handler as well.

Finally, we need to draw in the clues.  As you might have guessed, there is also a “Draw test on a form” snippet that I’ll insert and modify here for the actual drawing -- that snippet gives an example of the StringFormat Drawing class which defines how strings should be drawn.  I’ll have to construct the string first, and I’ll want to draw it right-justified/bottom justified to the grid.  Fortunately, the .NET Drawing classes makes it easy to to the positioning without having to calculate the string length, etc. – I can just use the Alignment and LineAlignment properties of the StringFormat class, setting them to “Far.”  (It’s called “Far” instead of “Right” or Bottom” because it also supports Right-to-Left languages.)  For constructing the string, I'll just iterate through the row or column and keep track of how many contiguous cells are filled -- when I get to a whitespace (or the end of the row/column), I'll update the string with the last contiguous count and then reset the counter to zero, then keep looking down the rol/column for the next contiguous set.  Here’s the code, which I'll call from the Paint event handler just like the previous two methods I wrote:

    Private Sub PaintClues()

        Dim drawFormat As New StringFormat()

        drawFormat.Alignment = StringAlignment.Far ' Horizontal right-justify

        drawFormat.LineAlignment = StringAlignment.Far ' Vertical bottom-justify

 

        Using formGraphics As Graphics = Me.CreateGraphics(), _

            drawFont As New System.Drawing.Font("Arial", FontSize), _

            drawBrush As New SolidBrush(Color.Black)

 

            ' First, do the row clues:

            For i As Integer = 0 To Rows - 1

                Dim rowText As String = ""

                Dim counter As Integer = 0

                For j As Integer = 0 To Columns - 1

                    If Grid(i, j) = 0 Then

                        ' Whitespace.  Is counter non-zero? 

  ' If so, update the strong and clear the counter

                        If counter > 0 Then

                            rowText = rowText & " " & counter.ToString

                            counter = 0

                        End If

                    Else

                        counter += 1

                    End If

                Next

                'Add the leftovers

                If counter > 0 Then

                    rowText = rowText & " " & counter.ToString

                End If

                ' Now we have the string.  Let's draw it (make sure it's

 ' offset from the line by a pixel)

                If rowText.Length() > 1 Then

                    Dim drawRect As New RectangleF(HorizontalOffset, VerticalOffset + _

(Rows * FontSize) + (i * CellSize) + 1, Columns * FontSize, CellSize)

                    formGraphics.DrawString(rowText, drawFont, drawBrush, drawRect, drawFormat)

                End If

            Next

 

            ' Next, do the column clues:

            For i As Integer = 0 To Columns - 1

                Dim colText As String = ""

                Dim counter As Integer = 0

                For j As Integer = 0 To Rows - 1

                    If Grid(j, i) = 0 Then

                        ' Whitespace.  Is counter non-zero? 

  ' If so, update the strong and clear the counter

                        If counter > 0 Then

                            colText = colText & " " & counter.ToString

                            counter = 0

                        End If

                    Else

                        counter += 1

                    End If

                Next

                'Add the leftovers

                If counter > 0 Then

                    colText = colText & " " & counter.ToString

                End If

                ' Now we have the string.  Let's draw it (make sure it's

 ' offset from the line by a pixel)

                If colText.Length() > 1 Then

                    Dim drawRect As New RectangleF(HorizontalOffset + (Columns * FontSize) + _

(i * CellSize) + 1, VerticalOffset, CellSize, Rows * FontSize)

                    formGraphics.DrawString(colText, drawFont, drawBrush, drawRect, drawFormat)

                End If

            Next

 

        End Using

 

    End Sub

 

And that’s it for the paint routine – it's pretty easy when you’ve got snippets to get you pointed in the right direction. 

Drawing

The last thing we’ll do today is enable the actual drawing.  The plan here is to track the movement of the mouse, and if the left-button is down over a cell, we’ll set the cell to “1” and invaliidate that area of the grid if the value changed; if the shift key is also down, we’ll instead set the cell to 0 and again invalidate that area of the grid if there was a change.  (I thought about using the right-mouse button for erasing, but I’m going to reserve that for the context menu work in a a later post in this series.)

I need to handle the MouseDown, MouseUp, and MouseMove events, and in that handler I’ll figure out which cell (if any) I’m over when the left mouse button is down.  Note that I am using the “\” division operator when doing the location math, since it correctly truncates the arithmetical result  – if you use the “/” division operator, you’ll get rounding errors which means that (for example) clicking in the bottom of one cell will count as a click in the next cell down.  Here's the code:

    Private Sub Form1_MouseEvent(ByVal sender As Object, _

ByVal e As System.Windows.Forms.MouseEventArgs) Handles _

Me.MouseMove, Me.MouseDown, Me.MouseUp

        If e.Button = Windows.Forms.MouseButtons.Left Then

            ' Get cell (if any) from position

            Dim row As Integer = (e.Y - (VerticalOffset + Rows * FontSize)) \ CellSize

            Dim col As Integer = (e.X - (HorizontalOffset + Columns * FontSize)) \ CellSize

            ToggleCell(row, col)

        End If

    End Sub

 

ToggleCell does the hard work –if (and only if) the state of a cell has changed, it figures out the rectangles of the cell, the affected row clue, and the affected column clue, and then invalidates just those areas so that they’ll repaint.  (I could just invalidate the whole form, but then I’d get a lot of flickering – invalidating per-region way is much more elegant.)  Once I have the three areas to invalidate stored in Rectangles, I create a Region object and use its Union method to merge the areas into one Region which I can act on.  (That’s right – the areas represented by a region don’t have to be contiguous -- very useful for updating far-flung areas at the same time.)

    Private Sub ToggleCell(ByVal row As Integer, ByVal col As Integer)

        If row >= 0 AndAlso row < Rows AndAlso col >= 0 AndAlso col < Columns Then

            ' Which areas would be affected?  The cell, the row clue, and the column clue

            Dim invalCell As New Rectangle( _

                    HorizontalOffset + (Columns * FontSize) + col * CellSize, _

                    VerticalOffset + (Rows * FontSize) + row * CellSize, _

                    CellSize, _

                    CellSize)

            Dim invalRowClue As New Rectangle(HorizontalOffset, VerticalOffset + _

(Rows * FontSize) + (row * CellSize) + 1, Columns * FontSize, CellSize)

            Dim invalColClue As New Rectangle(HorizontalOffset + (Columns * FontSize) + _

(col * CellSize) + 1, VerticalOffset, CellSize, Rows * FontSize)

            Dim invalRegion As New Region(invalCell)

            invalRegion.Union(invalRowClue)

            invalRegion.Union(invalColClue)

 

            Dim drawErase As Boolean = My.Computer.Keyboard.ShiftKeyDown

            If drawErase = True Then

                If Grid(row, col) = 1 Then

                    Grid(row, col) = 0

                    Invalidate(invalRegion)

                End If

            Else

                If Grid(row, col) = 0 Then

                    Grid(row, col) = 1

                    Invalidate(invalRegion)

                End If

            End If

        End If

    End Sub

And we’re done – we can now draw a puzzle on the screen, and all of the clues will update in real time!  That’s pretty cool for a couple of hours’ work.  However, we still have a lot of work to do to make this a really useful app.  In my next post, I’ll show an easy way to support “infinite” undo & redo for this application.  ‘Til then...

--Matt--*

Leave a Comment
  • Please add 6 and 6 and type the answer here:
  • Post
  • My wife also really likes these puzzles.  I'm looking forward to a more completed app.

  • This is a nice sample. I am working on an app right now that requires a similar grid and ended up using a TableLayoutPanel to create roughly the same effect you have here. Whenever I use the drawing methods in the Paint event I always end up with a slow refresh when I drag the form. If I take your code exactly, run it, and then drag the form around my desktop, I can see it leave a trail of invalidated objects trying to redraw. What is a good way to minimize this? I have looked into double buffering the main form but I guess I haven't learned enough about that yet.

    Thanks,

    Aaron

  • Aaron -- that's interesting. I don't get any drawing artifacts or other invalidation issues when dragging the form around, even between monitors; it's all very smooth.  In the past (when writing native code), I used to do the "double-buffering" that you mention to deal with flickering; that is, I would draw to a buffer in the background and then bit-blast the whole thing onto the window once I'd done all of the individual rendering, but that's just not really an issue anymore as the OS and graphics cards have gotten much better about rendering.  (In fact, five years from now, my invalidation of regions instead of the whole form that I demonstrated would probably be unnecessary on higher-end machines, given the rapid improvements in handling graphics on cards -- I probably wouldn't even noticed any flickering.)  I also don't think that double-buffering wouldn't help you with leaving artifacts behind on the screen -- it's really more to deal with flickering and performance, and if what you're drawing impacts anything outside of your form (such as when dragging), that sounds like a totally different problem than what double-buffering it meant to solve.  (I should caveat this with the warning that I'm no graphics expert, but this sounds like a context issue to me.)

    If you change the Invalidate(...) calls to take no arguments (i.e., invalidate the whole form), do you still get the artifacts?  (Ignore any flickering on the form itself; I'd expect that.)  If so, then that would seem to indicate some problem with graphics context going on, either in and of itself or something in the communication with the graphics card, although I'm out of my league at that point.)

    --Matt--*

  • If I take your code exactly, run it, and then drag the form around my desktop, I can see it leave a trail of invalidated objects trying to redraw. What is a good way to minimize this?

    ----------------

    Aaron, I encountered similar problem, fortunately, I solve the porblem,  just for your reference.

    you should modify every Paint*** sub like this,

       Private Sub PaintGridlines(e As PaintEventArgs)

           Dim LineWidth As Integer = (Rows * FontSize) + (Rows * CellSize)

           Dim LineHeight As Integer = (Columns * FontSize) _

    + (Columns * CellSize)

           Using myPen As New Pen(Color.Black), _

               formGraphics As Graphics = e.Graphics

               For i As Integer = 0 To Rows

                   Dim LineTop As Integer = VerticalOffset + _

    (Rows * FontSize) + i * CellSize

                   formGraphics.DrawLine(myPen, HorizontalOffset, LineTop, _

    HorizontalOffset + LineWidth, LineTop)

               Next

               For i As Integer = 0 To Columns

                   Dim LineLeft As Integer = HorizontalOffset + _

    (Columns * FontSize) + i * CellSize

                   formGraphics.DrawLine(myPen, LineLeft, VerticalOffset, _

                        LineLeft, VerticalOffset + LineHeight)

               Next

           End Using

       End Sub

    then in the form_paint event, call this procedure:

    PaintGridlines(e)

    by this modification , the drawing will refresh more smoothly.

    please try ,if your problem still exist?

  • OK here&#39;s the deal. If you are teaching Visual Basic, programming in Visual Basic for fun or profit

  • OK here's the deal. If you are teaching Visual Basic, programming in Visual Basic for fun or profit,

Page 1 of 1 (6 items)