Mosaic: Drawing the tiles

The tile control is already made, now it's time for the drawing of the tiles. The tile image can be one of these images:

  • Numbered tiles (just a matrix of numbers)
  • The demo bitmap (a resource)
  • A bitmap the user chooses

For now we will use the numbered tiles, the bitmap stuff will be added later. Furthermore, there are 4 color schemes.

7.1 - How it works

We'll use device contexts many times. A device context is a set of graphic objects that affect the output. These graphic objects can be pens, bitmaps, brushes, palette etc. The device contexts we use are:

  • Device contexts (DCs) of windows (The bitmap object in these DCs is what you actually see of the control)
  • Back buffer DC. All the drawing will first be done on a so called back buffer. This prevents flickering as drawing directly to a control will show the process of drawing as well.
  • Image DC. This DC contains the tile image currently used (numbered tiles/demo bitmap/user bitmap).

First the bitmap the user chooses, (the numbered tiles, which are drawn at runtime, the demo bitmap or a user bitmap), is put in the ImageDC. Then the 3D effect of the tiles (highlites and shadows) are drawn on the bitmap in the DC. Now remember this: The imageDC will not change from here, only if the user selects another tile type. The drawing is done tile by tile on the back buffer. For each tile, the tile is extracted from the ImageDC, then placed at the current position of that tile. An array will hold the tile positions.

7.2 - Creating the graphic objects

A new procedure is introduced, InitBitmaps:

InitBitmaps         PROTO   STDCALL :DWORD

[in your .data?]

BackBufferDC        dd  ?
hBackBuffer         dd  ?
ImageDC             dd  ?
hImage              dd  ?
hBackgroundColor    dd  ?
hTileColor          dd  ?
hFont               dd  ?
TextColor           dd  ?

[in your .data]

FontFace            db  "Arial",0

[in your .code]

;======================================================================
;                           Init Bitmaps
;======================================================================
InitBitmaps proc hWnd:DWORD
; Create DC's for backbuffer and current image
    invoke  CreateCompatibleDC, NULL
    mov     BackBufferDC, eax

    invoke  CreateCompatibleDC, NULL
    mov     ImageDC, eax

; Create bitmap for backbuffer:
    invoke  GetDC, hWnd
    push    eax
    invoke  CreateCompatibleBitmap, eax, 200+20,200+20
    mov     hBackBuffer, eax
    pop     eax
    invoke  ReleaseDC, hWnd, eax
    invoke  SelectObject, BackBufferDC, hBackBuffer

; Create Arial font for the numbers
    invoke  CreateFont, -30, NULL, NULL, NULL, FW_EXTRABOLD, \
            FALSE, FALSE, FALSE, NULL, NULL, NULL, NULL, \
            NULL, ADDR FontFace
    mov     hFont, eax

; Select font in Image DC
   invoke   SelectObject, ImageDC, hFont

   invoke   CreateSolidBrush, 0FF8000h
   mov      hBackgroundColor, eax
   invoke   CreateSolidBrush, 0FF8080h
   mov      hTileColor, eax

   mov      TextColor, 0800000h
ret
InitBitmaps endp

Let's examine the procedure step by step:

    invoke  CreateCompatibleDC, NULL
    mov     BackBufferDC, eax

    invoke  CreateCompatibleDC, NULL
    mov     ImageDC, eax

CreateCompatibleDC creates a new DC that is compatible with a given window. If NULL is given as parameter (window handle), it is compatible with the default window. One DC is created for the backbuffer, the handle is stored in BackBufferDC. The other is for the image DC, stored in ImageDC.

    ; Create bitmap for backbuffer:
    invoke  GetDC, hWnd
    push    eax
    invoke  CreateCompatibleBitmap, eax, 200+20,200+20
    mov     hBackBuffer, eax
    pop     eax
    invoke  ReleaseDC, hWnd, eax
    invoke  SelectObject, BackBufferDC, hBackBuffer

CreateCompatibleBitmap creates a new bitmap object that is compatible with a given DC. First we get the DC of the main window with GetDC. The handle is pushed onto the stack to save it for ReleaseDC. CreateCompatibleBitmap is then called, with the main window DC as DC, and 220x220 as bitmap size. The extra 20 pixels are for the margins. The bitmap handle is saved in hBackBuffer, the window DC handle is popped of the stack again and released (ReleaseDC should always be used with GetDC). Finally, SelectObject selects a graphic object in a DC. Here, the bitmap just created is put in the backbuffer DC.

; Create Arial font for the numbers
    invoke  CreateFont, -30, NULL, NULL, NULL, FW_EXTRABOLD, \
            FALSE, FALSE, FALSE, NULL, NULL, NULL, NULL, NULL, ADDR FontFace
    mov     hFont, eax

; Select font in Image DC
   invoke   SelectObject, ImageDC, hFont

To draw the numbers on the tiles, we need a font. CreateFont creates such a font. Look it up in your reference, FontFace is the name of the font, "Arial". -30 is a size for the font. The handle for the font is saved in hFont and then the font is selected in the ImageDC so we can write with the font on the image DC.

   invoke   CreateSolidBrush, 00FFFFFFh
   mov      hBackgroundColor, eax
   invoke   CreateSolidBrush, 00FF0000h
   mov      hTileColor, eax

   mov      TextColor, 000000h

Finally, a few brush handles are made, they are not selected in any DC right now, but they will be later. CreateSolidBrush creates a brush with a certain color, you can use the handle to draw things (lines etc) with the brush. TextColor is not a brush, it's just a color value (we don't need a brush for the text color, only the color value).

Note: the image bitmap is not created yet, the DC is already available, but as the user can choose the type of bitmap, this bitmap is made when the user selects a type, then it is selected in the DC.

A function to free all these handles is necessary too:

DeleteBitmaps   PROTO STDCALL
;======================================================================
;                           Delete Bitmaps
;======================================================================
DeleteBitmaps proc
    invoke  DeleteDC, BackBufferDC
    invoke  DeleteDC, ImageDC
    invoke  DeleteObject, hImage
    invoke  DeleteObject, hBackBuffer
    invoke  DeleteObject, hFont
    invoke  DeleteObject, hBackgroundColor
    invoke  DeleteObject, hTileColor
ret
DeleteBitmaps endp

Then both functions should be called:

...
.IF     eax==WM_CREATE
        invoke  InitControls, hWnd
        invoke  InitBitmaps, hWnd ;<<< insert here
.ELSEIF eax==WM_DESTROY
        invoke  DeleteBitmaps     ;<<< other one here
        invoke  PostQuitMessage, NULL
...

On creation of the main window, the bitmaps and DCs are initialized, before destroying, the bitmaps and DCs are deleted again.

7.3 - Tile mode

Create a new procedure, SetBitmap:

[in mosaic.inc]

IMAGETYPE_STANDARD  equ     0
IMAGETYPE_NUMBERS   equ     1
IMAGETYPE_BITMAP    equ     2

[in .data?]

CurImageType        dd      ?       ;Current image type

[in .code]

SetBitmap   PROTO   STDCALL :DWORD, :DWORD
;======================================================================
;                           Set Bitmap
;======================================================================
SetBitmap   proc hWnd:DWORD, ImageType:DWORD
    mov     eax, ImageType
    .IF eax==IMAGETYPE_NUMBERS
        ;--- delete old image ---
        invoke  DeleteObject, hImage
        ;--- Get DC ---
        invoke  GetDC, hWnd
        push    eax
        ;--- Create new bitmap for the numbers bitmap ---
        invoke  CreateCompatibleBitmap, eax, 200, 200
        mov     hImage, eax
        pop     eax
        ;--- Release DC ---
        invoke  ReleaseDC, hWnd, eax
        ;--- Select new bitmap in DC ---
        invoke  SelectObject, ImageDC, hImage
        ;--- Draw numbers on the bitmap ---
        invoke  DrawNumbers
        ;--- Create the 3D effect on the bitmap ---
        invoke  CreateTiles
    .ENDIF
    ;--- Set the new image type ---
    mov     eax, ImageType
    mov     CurImageType, eax
ret
SetBitmap   endp

The SetBitmap procedure selects the type of image to use for the tiles. The procedure takes 2 parameters: hWnd, the handle of the main window, and ImageType, which can be one of these constants: IMAGETYPE_STANDARD, IMAGETYPE_NUMBERS, IMAGETYPE_BITMAP. These constants are defined in the include file. Right now, the procedure only reacts to IMAGETYPE_NUMBERS, we will implement the other two later. When the numbers image type is chosen, the old image (hImage) is deleted, and a new one is created with CreateCompatibleBitmap. Then two functions are called, DrawNumbers and CreateTiles, which are defined in the code below. DrawNumbers just draws an array of numbers on the new bitmap. CreateTiles draws the 3D effect on the bitmap. The CreateTiles procedure will be used for the other two image types too.

GetCoordinates  PROTO   STDCALL :DWORD
DrawNumbers     PROTO   STDCALL
.data
NumberFormat    db      "%lu",0
Rect200         RECT    <0,0,200,200>
.data?
Buffer          db      200 dup (?)
.code
;======================================================================
;                           Draw Numbers
;======================================================================
DrawNumbers proc uses ebx edi
LOCAL   TempRect:RECT
    ; --- Set the textcolor of ImageDC to TextColor ---
    invoke  SetTextColor, ImageDC, TextColor
    ; --- Fill the imageDC with the tile color brush ---
    invoke  FillRect, ImageDC, ADDR Rect200, hTileColor
    ; --- Set the background mode to transparent (for the text) ---
    invoke  SetBkMode, ImageDC, TRANSPARENT

    ; --- Loop through all the numbers and draw them one by one ---
    xor     ebx, ebx
    .WHILE  ebx<16
        mov     eax, ebx
        inc     eax
        invoke  GetCoordinates, eax
        mov     dx, ax      ; dx  = row
        shr     eax, 16     ; ax  = column
        and     edx, 0ffffh ; make sure that edx = dx
        imul    edx, edx, 50;} Multipy edx as well as eax with 50
        imul    eax, 50     ;}
        mov     TempRect.left, eax
        mov     TempRect.top, edx
        add     eax, 50
        add     edx, 50
        mov     TempRect.right, eax
        mov     TempRect.bottom, edx
        mov     eax, ebx
        inc     eax
        invoke  wsprintf, ADDR Buffer, ADDR NumberFormat, eax
        invoke  DrawText, ImageDC, ADDR Buffer, -1, ADDR TempRect,\
                DT_CENTER or DT_SINGLELINE or DT_VCENTER
    inc ebx
    .ENDW
ret
DrawNumbers endp

;======================================================================
;                           GetCoordinates
;======================================================================
GetCoordinates proc dwTile:DWORD
    mov     eax, dwTile
    dec     eax
    cdq
    mov     ecx, 4
    div     ecx
    ;eax=quotient = row
    ;edx=remainder = column
    shl     edx, 16
    add     eax, edx
ret
GetCoordinates endp

Two new procedures here, GetCoordinates is a little procedure that will be used several times in the program. It uses a 0-based index of the tiles (tile 0, tile 1, tile 2) etc. and returns the row of the tile (0,1,2,3) in the low word of eax, the column of the tile (0,1,2,3) in the high word of eax. The calculation is quite simple. Divide the tile index by 4, the remainder will be the column, the quotient the row of the tile. The shl instruction shifts the colomn in the high word (shift 16 bits left), and add adds the row.

The DrawNumbers procedure works like this:

  • Set textcolor to the value of the TextColor variable
  • Fill the complete bitmap with the tilecolor brush (rect200 is a RECT structure that defines the area of 200x200 pixels starting at (0,0))
  • Set the background mode to TRANSPARENT to prevent an ugly background around the text.
  • The tile loop: Loop from 0 to 15
    • GetCoordinates(currentloopvalue)
    • Extract row and column from return value
      multiply the row and the column with 50
      (this gives the image coordinates of the tile)
    • Fill a RECT structure (TempRect) with the coordinates of
      the tile.
    • Use wsprintf to convert the tile number into text.
      (NumberFormat is the format the number should be outputted
      in, buffer is a temporary buffer)
    • Use DrawText to draw the number at the right coordinates

CreateTiles draws the button-style 3D effect on the tiles by drawing black & white lines at the right places:

CreateTiles     PROTO   STDCALL
;======================================================================
;                           Create Tiles
;======================================================================
CreateTiles proc uses ebx esi edi
    invoke  GetStockObject, BLACK_PEN
    invoke  SelectObject, ImageDC, eax
; Dark lines, vertical. x = 50k - 1 (k=1,2,3,4)
; ebx = k
; esi = x
    xor     ebx, ebx
    inc     ebx
    ; ebx is 1 now

    .WHILE  ebx<5   ; (ebx= 1,2,3,4)
        mov     eax, 50
        mul     ebx
        mov     esi, eax
        dec     esi
        invoke  MoveToEx, ImageDC, esi, 0, NULL
        invoke  LineTo, ImageDC, esi, 199
    inc ebx
    .ENDW

; Dark lines, horizontal. y = 50k - 1 (k=1,2,3,4)
; ebx = k
; esi = y
    xor     ebx, ebx
    inc     ebx
    ; ebx is 1 now
    .WHILE  ebx<5   ; (ebx= 1,2,3,4)
        mov     eax, 50
        mul     ebx
        mov     esi, eax
        dec     esi
        invoke  MoveToEx, ImageDC, 0, esi, NULL
        invoke  LineTo, ImageDC, 199, esi
    inc ebx
    .ENDW
    invoke  GetStockObject, WHITE_PEN
    invoke  SelectObject, ImageDC, eax
; Light lines, vertical. x = 50k  (k=0,1,2,3)
; ebx = k
; esi = x
    xor     ebx, ebx

    .WHILE  ebx<4   ; (ebx= 0,1,2,3)
        mov     eax, 50
        mul     ebx
        mov     esi, eax
        invoke  MoveToEx, ImageDC, esi, 0, NULL
        invoke  LineTo, ImageDC, esi, 199
    inc ebx
    .ENDW

; Light lines, horizontal. y = 50k (k=0,1,2,3)
; ebx = k
; esi = y
    xor     ebx, ebx

    ; ebx is 1 now

    .WHILE  ebx<4   ; (ebx= 0,1,2,3)
        mov     eax, 50
        mul     ebx
        mov     esi, eax
        invoke  MoveToEx, ImageDC, 0, esi, NULL
        invoke  LineTo, ImageDC, 199, esi
    inc ebx
    .ENDW

ret
CreateTiles endp

This procedure is quite easy to understand. It draws 4 sets of lines. Each line is drawn by first getting a brush with GetStockObject (retrieves a standard brush color from windows). This brush is selected in the imagedc. Then the current point is moved to the startpoint of the line with MoveToEx, and the line is drawn with LineTo.

7.4 - Drawing

The procudure below will be used in the future too, but the code of it is temporary right now, it just draws the image DC on the static control to show if your code worked. Furthermore, some additional procedures are introduced to initialize everything correctly.

DrawProc    PROTO   STDCALL :DWORD, :DWORD
InitGame    PROTO   STDCALL :DWORD


[in .code]

;======================================================================
;                           Draw Numbers
;======================================================================
DrawProc proc uses ebx edi esi hWnd:DWORD, hDC:DWORD
    invoke  BitBlt, hDC, 9, 9, 220, 220, ImageDC, 0, 0, SRCCOPY
ret
DrawProc endp
;======================================================================
;                           InitGame
;======================================================================
InitGame    proc    hWnd:DWORD
    invoke  SetBitmap, hWnd, IMAGETYPE_NUMBERS
ret
InitGame    endp


[In the WM_CREATE handler of WndProc, below 'invoke InitBitmaps, hWnd']
    invoke      InitGame, hWnd
[In the messagehandler in WndProc, between the other ELSEIFs]
    .ELSEIF eax==WM_DRAWITEM
        mov     eax, wParam
        .IF     eax==CID_STATIC
            push    ebx
            mov     ebx, lParam
            assume  ebx:ptr DRAWITEMSTRUCT
            invoke  DrawProc, hWnd, [ebx].hdc
            assume  ebx:nothing
            pop     ebx
            xor     eax, eax
            inc     eax
        .ELSE
            xor     eax, eax
        .ENDIF

The DrawProc and InitGame are fairly easy to understand. BitBlt copies a part of a bitmap from one DC to another. Here the complete bitmap in ImageDC is copied to the DC of the static control window.

The WM_DRAWITEM message requires some more explanation:
WM_DRAWITEM is sent to the main window when one of the owner-drawn controls needs to be drawn. An owner-drawn control is a control that is drawn by the program instead of windows. When this message is sent, wParam contains the ID of the control that needs to be drawn. Here the .IF eax==CID_STATIC finds out if it is the static control (the tiles window) that needs to be drawn. If not (.ELSE), 0 is returned from WndProc (xor eax, eax). 0 tells windows that the message is not handled by the program. lParam is a pointer to a DRAWITEMSTRUCT.

mov     ebx, lParam
assume  ebx:ptr DRAWITEMSTRUCT
invoke  DrawProc, hWnd, [ebx].hdc
assume  ebx:nothing

First, lParam is put in ebx. Now ebx is a pointer to the DRAWITEMSTRUCT structure. The assume statement gives masm more information about the register. assume ebx:ptr DRAWITEMSTRUCT tells masm that ebx is a pointer to a DRAWITEMSTRUCT structure. Now you can use the structure members directly on ebx. hdc is one of these members, it contains the handle to the device context of the window. The control will show whatever there is on that DC. This DC is passed to DrawProc. Don't forget to tell masm to assume nothing for ebx (assume ebx:nothing), otherwise masm thinks ebx is a pointer to DRAWITEMSTRUCT throughout the whole program. Very important is that you should save ebx in your procedures (that's why the push ebx/pop ebx instructions are included). Windows assumes the values of the ebx, esi and edi registers do not change when your window procedure is called by windows (this applies to all callback functions in windows, always save ebx, esi & edi).

7.5 - Done

The current project files are here: mosaic4.zip

If you assemble the program and run it, you should see this:

Number test

As you can see, the tiles are drawn correctly.