Mosaic: Image types
The only image type we've implemented until now is the numbered tiles type. We will add a standard image type (bitmap from the resource file), and a user defined image type (where the user can select a bitmap to use on the tiles).
11.1 - Standard image
First add this to the resource file:
BMP_DEMOBITMAP BITMAP DISCARDABLE "resources\\demo.bmp"
and this to mosaic.inc:
This will add this bitmap to the resource file, with ID 802:
demo.bmp, 200x200 pixels
Add a new handler to SetBitmap:
In SetBitmap, before .ENDIF:
;--- delete old image ---
invoke DeleteObject, hImage
;--- load bitmap from resource ---
invoke LoadBitmap, hInstance, ID_DEMOBITMAP
;--- save new handle ---
mov hImage, eax
;--- select new handle in ImageDC ---
invoke SelectObject, ImageDC, eax
;--- Create the 3D effect on the bitmap
invoke CreateTiles
The old image is deleted, the bitmap is loaded from the resources (LoadBitmap), the image handle is saved and selected in ImageDC and the 3D effect is drawn on it with CreateTiles. That's all there is, it should work now.
If you run the program now and select the standard image type you can see the image above on the tiles. The color schemes only affect the background color with this image type:
11.2 - User defined image
The standard image was easy to implement because it can be loaded from the resources and you know for sure that the image is valid. When the user can choose it's own bitmap it's more complicated, because of these reasons:
- The image has to be loaded from harddisk, and there's no windows function that does that so we will have to do it by hand.
- It should be fool proof. The program shouldn't crash if the user selects a textfile as bitmap.
- The image size should be 200x200 pixels. We'll have to check the bitmap for that.
All the funtions in the following handlers will be defined below, this is just the mainframe of the process:
In SetBitmap, before .ENDIF:
invoke SelectObject, ImageDC, hImage
invoke CreateTiles
This adds the new image type so SetBitmap will respond to it. When SetBitmap is called with IMAGETYPE_BITMAP, SetBitmap assumes the bitmap is loaded in hImage. The only thing SetBitmap then does is select it in the ImageDC and make tiles of the bitmap.
In ProcessMenuItems, the MI_USEFILE handler, replace ';yet to do' with:
mov al, byte ptr [eax]
.IF al==NULL
invoke GetOpenFileName, ADDR BitmapOFN
.ENDIF
.IF eax!=NULL
invoke OpenBitmap, hWnd
invoke SetBitmap, hWnd, IMAGETYPE_BITMAP
invoke InvalidateRect, hWnd, NULL, FALSE
.ENDIF
When the user clicks on the menu item with ID MI_USEFILE, the first byte of OFN_FILE is checked for zero. OFN_FILE will be defined later, but is just a buffer for the filename for the image. This zero-check is done to check if a filename has already been chosen. If not, GetOpenFileName (explained later) will show a 'Open file' dialog that fills a OPENFILENAME structure (BitmapOFN) and OFN_file. If this function returns a non-zero value, the user has chosen a file. OpenBitmap opens this file (and checks if it's valid). SetBitmap then changes the image type, and InvalidateRect updates the main window.
Note: Even if OpenBitmap failes, SetBitmap is still called. This has no effect because if OpenBitmap fails, it sets the image type to the standard image. SetBitmap justs redraws the standard image (useless but not harmfull).
In ProcessMenuItems, in the MI_OPENBITMAP handler:
invoke GetOpenFileName, ADDR BitmapOFN
.IF eax!=NULL
invoke ProcessMenuItems, hWnd, MI_USEFILE
.ENDIF
The menu option 'Open Bitmap' in the file menu should also open a bitmap. If the user selects a file (eax!=null), ProcessMenuitems is called manually to set the file as image type.
Data used
OFN_Filter db "Bitmaps (*.bmp)",0,"*.bmp",0,"All Files (*.*)",0,"*.*",0,0
BitmapOFN OPENFILENAME <SIZEOF OPENFILENAME,\
NULL,\
NULL,\
offset OFN_Filter,\
NULL, NULL, NULL, offset OFN_file,\
270,\
NULL, NULL, NULL, NULL,\
OFN_PATHMUSTEXIST + OFN_FILEMUSTEXIST,\
NULL, NULL, NULL, NULL, NULL, NULL>
.data?]
OFN_file db 270 dup (?)
BitmapOFN is an OPENFILENAME structure. This structure is used to create a standard 'OpenFileName' dialog. The important members of this structure are:
offset OFN_Filter: This is a pointer to OFN_Filter. OFN_Filter is an array of string pairs, defining the file types the user can open. Both strings in each pair are terminated by a single 0 byte, the complete array is terminated by two 0 bytes. The first string in the pairs are the strings displayed in the 'file type' box, the second string is the search pattern for that file type.
offset OFN_file: A pointer to the buffer that receives the filename selected.
270: The maximum size of the buffer.
OFN_PATHMUSTEXISTS + OFN_FILEMUSTEXIST: Flags that ensure that the user has selected a valid path and filename.
This structure is enough for GetOpenFileName to show the dialog. It returns 0 if the user pressed cancel, and non-null if the user has chosen a filename.
OpenBitmap
;================================================================================
; OpenBitmap
;================================================================================
OpenBitmap proc uses edi esi hWnd:DWORD
LOCAL hFile:DWORD
LOCAL FileSize:DWORD
LOCAL hMem:DWORD
LOCAL pMem:DWORD
LOCAL BytesRead:DWORD
invoke DeleteObject, hImage
invoke CreateFile, ADDR OFN_file, GENERIC_READ, FILE_SHARE_READ,\
NULL, OPEN_EXISTING, NULL, NULL
.IF eax==INVALID_HANDLE_VALUE
MSGBOX "Error: Could not open bitmap file."
invoke ProcessMenuItems, hWnd, MI_USESTANDARD ;fake menu press
ret
.ENDIF
mov hFile, eax
invoke GetFileSize, hFile, NULL
mov FileSize, eax
invoke GlobalAlloc, GMEM_MOVEABLE, eax
mov hMem, eax
invoke GlobalLock, eax
mov pMem, eax
invoke ReadFile, hFile, pMem, FileSize, ADDR BytesRead, NULL
mov ecx, FileSize
.IF BytesRead!=ecx || eax==NULL
MSGBOX "Error: Failed reading from bitmap file."
invoke ProcessMenuItems, hWnd, MI_USESTANDARD ;fake menu press
jmp @ob_exit1
.ENDIF
invoke CheckIfValidBitmap, hWnd, pMem, FileSize
.IF eax==NULL
MSGBOX "ERROR: Bitmap is invalid."
invoke ProcessMenuItems, hWnd, MI_USESTANDARD ;fake menu press
.ELSEIF eax==1
mov ecx, pMem
mov edx, ecx
mov edx, dword ptr [edx + 0Ah]
add edx, ecx
add ecx, 0Eh ; Bitmapheader
invoke CreateDIBitmap, ImageDC, ecx, CBM_INIT,\
edx, ecx, DIB_RGB_COLORS
mov hImage, eax
.ELSEIF eax==2
MSGBOX "ERROR: This bitmap is not 200x200 pixels."
mov eax, offset OFN_file
mov byte ptr [eax], 0
invoke ProcessMenuItems, hWnd, MI_USESTANDARD ;fake menu press
.ENDIF
@ob_exit1:
invoke GlobalUnlock, hMem
invoke GlobalFree, hMem
invoke CloseHandle, hFile
ret
OpenBitmap endp
After a valid filename has been selected, OpenBitmap loads the bitmap in memory and processes it. We will examine this function step by step:
Delete the old image:
Open the file in the OFN_file buffer. CreateFile (which in spite of it's name can open files too :) opens the file with readonly access (GENERIC_READ), and requires that the file actually exists to open it (OPEN_EXISTING). If it returns an invalid handle, an error message is shown with the MSGBOX macro (see mosaic.inc), and the image type is reset to the standard image type:
NULL, OPEN_EXISTING, NULL, NULL
.IF eax==INVALID_HANDLE_VALUE
MSGBOX "Error: Could not open bitmap file."
invoke ProcessMenuItems, hWnd, MI_USESTANDARD ;fake menu press
ret
.ENDIF
mov hFile, eax ;save handle
Retrieve filesize:
mov FileSize, eax
Create a free memory block with the size of the file. GlobalAlloc allocates a given number of bytes (eax contains the filesize) and returns a handle to the memory block (hMem). To get a pointer to the memory block, use GlobalLock. The pointer is savid in pMem.
mov hMem, eax
invoke GlobalLock, eax
mov pMem, eax
The complete bitmap is read with ReadFile. The destination is the value of pMem, which is the pointer to the allocated memory block. The number of bytes to read is FileSize, i.e. the complete file. The start offset to read can not be given as a parameter to ReadFile, it uses the current file pointer. But as we just created the file, the file pointer is reset to 0.
If the number of bytes read is not the filesize, or if the function returns 0, the reading failed:
.IF BytesRead!=ecx || eax==NULL
MSGBOX "Error: Failed reading from bitmap file."
invoke ProcessMenuItems, hWnd, MI_USESTANDARD ;fake menu press
jmp @ob_exit1
.ENDIF
This function will be defined later, but it returns 0 if the image is invalid, 2 if the image is not 200x200 pixels (see below), and 1 if the bitmap is valid.
.IF eax==NULL
MSGBOX "ERROR: Bitmap is invalid."
invoke ProcessMenuItems, hWnd, MI_USESTANDARD ;fake menu press
If the bitmap is valid:
Here the dword at offset 0A (hex) in the file is read (This is the bmBits member of the BITMAP structure, which the BMP file format uses). This dword contains an offset to the actual image data. This offset is added to the memory pointer in ecx to get a pointer to this data.
mov edx, ecx
mov edx, dword ptr [edx + 0Ah]
add edx, ecx
add ecx, 0Eh ; Bitmapheader
CreateDIBitmap uses the pointer to the bitmapdata and the pointer to the start of the file to create a bitmap from it. The handle is saved.
edx, ecx, DIB_RGB_COLORS
mov hImage, eax
If the size is incorrect:
MSGBOX "ERROR: This bitmap is not 200x200 pixels."
mov eax, offset OFN_file
mov byte ptr [eax], 0
invoke ProcessMenuItems, hWnd, MI_USESTANDARD ;fake menu press
.ENDIF
Free the allocated memory and close the filehandle:
invoke GlobalUnlock, hMem
invoke GlobalFree, hMem
invoke CloseHandle, hFile
CheckIfValidBitmap
We can use OpenBitmap without CheckIfValidBitmap, but then it would not be fool proof. CheckIfValidBitmap uses some information of the bitmap file format to check if the file is a bitmap and has the right size.
;================================================================================
; Check If valid bitmap
;================================================================================
CheckIfValidBitmap proc uses edi hWnd:DWORD, pMem:DWORD, FileSize:DWORD
mov edi, pMem
mov ax, word ptr [edi]
.IF ax!="MB"
xor eax, eax
ret
.ENDIF
mov eax, dword ptr [edi+2]
.IF eax!=FileSize
xor eax, eax
ret
.ENDIF
mov eax, dword ptr [edi+0Eh]
.IF eax!=28h
xor eax, eax
ret
.ENDIF
mov eax, dword ptr [edi+12h]
mov ecx, dword ptr [edi+16h]
.IF eax!=200 || ecx!=200
mov eax, 2
ret
.ENDIF
xor eax,eax
inc eax
ret
CheckIfValidBitmap endp
First, the first two bytes should be
"BM"
. This is checked in the first if/endif. Then the filesize should match the dword at offset 2 in the file. At offset 14 (edi+0Eh), the dword
00000028h
should be present. Finally, the dwords at offset 18 and 22 (12h & 16h) contain the width and height of the bitmap. Both should be 200.
If one of the checks fails, 0 is returned. If all are passed, 1 is returned. If the image size is not 200x200, 2 is returned.
11.3 - Done
You can now test your program with this bitmap:
(it's a GIF, so you'll have to save it as bitmap or convert it)
It should look like this (with the green color scheme on, and a little shuffled of course :)
Current project files are here: mosaic8.zip.