02-09-2004, 07:18 AM
Recently on this forum there have been a growing number of users moving to DirectX to fulfil their graphics and game programming needs. The children of Image Controls, Picture Boxes, PaintPicture and BitBlt are growing up, and they are looking for more power, which is delivered to them in the form of DirectDraw.
But with this great power comes great responsibility. Well, not really, however it is often a good idea to put a little more thought into our work, so as to get the most reward from it. Thatís the purpose of this article, to provide some insight into making our programs better, by removing the limit of DirectDraw7ís ability to only load BMP file types.
In the example Iíve included with this tutorial are 4 very similar images, compressed in 4 different formats: BMP, GIF, JPG and PNG. Iím sure most of you know the advantages and disadvantages of these formats, so I wonít go into it. But just as a refresher and for those who donít really know about the different formats, hereís a little list comparing each format and the resulting images file size:
BMP: 219 KB
GIF: 30 KB
JPG: 74 KB
PNG: 40 KB
As we can see, as far as file size is concerned, a BMP isnít the best option. So letís look at some ways to get around DirectDrawís lack of support.
The DirectX4VB (www.directx4vb.com) Way
If youíre anything like me, you learnt DirectX through this website. Jack Hoxley and the rest of the crew have done a great job writing up an excellent bunch of tutorials. However, I do find their Loading JPEG and GIF files (http://18.104.22.168/DirectX4VB/Tutorials/DirectX7/DD_FileTypes.asp) tutorial to be a bit below their usual standard. If youíre interested in the details follow the link provided, but a brief summation is that they load the file into a PictureBox, then use the native VB function SavePicture to convert the picture into a BMP file, load it using the CreateSurfaceFromFile method, then delete the BMP. Personally I think this is a pretty cheap hack to get around the problem; youíre not actually letting it load JPGs or GIFs at all, instead just converting it to a BMP and then loading it. That said, it does work, and it is simple. But we can do better.
The Slow Way
So we can load a picture into a picture box. We can use the GetPixel or native Point function to get pixel data from this PictureBox. We can use SetLockedPixel/SetLockedArray to put data into a DirectDraw surface. So thereís nothing to stop us looping through every pixel in our picture and drawing it in turn to our surface. This works, might even be feasible if you only have a few small images to load. But itís painfully slow. We can do better.
In the next post, we'll look at how I accomplish this.
02-09-2004, 07:20 AM
The JimCamel Way©
After spending many an hour scouring the internet looking for some kind of code or tutorial for a better way to load different image file types, I eventually came across a piece of code which was perfect. It loaded an image into a PictureBox, and then somehow the image ended up in the surface. It was fast, didnít use any special tricks, and made absolutely no sense. There were no comments, all sorts of weird calculations multiplying by Screen.TwipsPerPixelX / 0.001 and all sorts of crazy stuff. But it worked. So I tried to cut the fat out of it and figure out exactly how this piece of code. There, buried in the heart of it, was BitBlt.
Now Iím going to take a risk and assume if you know how to use DirectX, you know at least the basics of BitBlt. Just as a refresher, hereís the API Declaration for BitBlt
Declare Function BitBlt Lib "gdi32" Alias "BitBlt" (ByVal hDestDC As Long, ByVal x As Long, ByVal y As Long,_
ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long,_
ByVal xSrc As Long, ByVal ySrc As Long, ByVal dwRop As Long) As Long
Right, so we need an hDC to BitBlt from, thatís easy, PictureBoxes have a .hDC method we can use to retrieve it. X, Y, Width, Height, Source Coordinates, sure, we can figure that all out from the size of the picture box, Raster Operations, well we just want to copy it to the surface, so vbSrcCopy will do fine, but we need a destination hDC. Enter the DirectDrawSurface7ís GetDC method.
Yup, itís really that simple. We call GetDC, which returns to us a Long which references a freshly created hDC for our surface. When weíre done, we just call ReleaseDC, passing it the Long of the hDC, and it will clean it all up after us. And between these two lines of code, we can treat our Surface just like any other image based control.
So, to get this all working, we create a form. Set itís scalemode to Pixels, so we can get the pixel size of the PictureBox weíll be loading our images into. Create a PictureBox. Set itís scalemode to Pixels, AutoRedraw to true, so that our image will stay updated, AutoSize to true, so it will fit our image, BorderStyle to None and Appearance to Flat, so that the PictureBox will be exactly the size of the image when itís loaded it. Finally, set the Visible property to false, because no one really needs to see our little secret.
Now for the code. Iíll assume you can set up DirectDraw already, create surfaces and the like, (if not, start here (http://www.visualbasicforum.com/t12195.html)) so all thatís left to be done is to load our image. So something to this extent:
Private Sub LoadFromPictureBox(FileName As String)
'We'll need something to store our Device Context in
Dim DC As Long
'Load the image into our picture box
Set picLoad.Picture = LoadPicture(FileName)
'Here's where the magic happens. First we call the GetDC method, which
'locks the surface and creates a DC we can use for the surface
DC = surfImage.GetDC
'Then, it's just a matter of BitBltting the image from the hDC of the Loading
'picturebox into our newly acquired DC
BitBlt DC, 0, 0, picLoad.Width, picLoad.Height, picLoad.hDC, 0, 0, vbSrcCopy
'And once we've finished, release the DC, which unlocks the surface and we're good to go
Hrm, somethingís missing, we never actually created the surface. Well, knowing the size of the image weíre using, thatís pretty trivial. Something along these lines perhaps:
Dim ddsd As DDSURFACEDESC2
'Load the image into our picture box
Set picLoad.Picture = LoadPicture(FileName)
'Since we're creating our surface from scratch, this time
'We'll need to specify the width and height of our surface
ddsd.lFlags = DDSD_CAPS Or DDSD_WIDTH Or DDSD_HEIGHT
ddsd.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN
'When we load the image, the picturebox will resize to fit the image
'and we can find out it's size based on the size of the picture box
ddsd.lWidth = picLoad.Width
ddsd.lHeight = picLoad.Height
'Create our surface which will now be the same size as the image
Set surfImage = DD.CreateSurface(ddsd)
Nice, that will slot nicely into our previous sub, and weíre pretty much done. For the full example, check out the attached code.
The next post deals with loading file formats other than GIF, BMP and JPG (such as PNG).
02-09-2004, 07:22 AM
Extra for experts
Alright, so weíve covered loading JPGs and GIFs, but Iíve been trying to avoid loading PNGs. Thatís because the standard PictureBox doesnít actually support PNGs. But never fear, JimCamel always has a trick up his sleeve! Not to long ago I found an excellent piece of open source software known as FreeImage (http://freeimage.sourceforge.net/) FreeImage supports the loading of 23 different file types, including PNG, not to mention a whole bunch of features weíre not ever likely to need in our life. Which is why this forums very own DrunkenHyena made a cut down version called MiniImage (http://www.drunkenhyena.com/docs/MiniImage.phtml), which removes all the stuff we donít need and leaves us with PNG and MNG loading support. The choice is yours to make, but for the purposes of this article, I will be referring to the original FreeImage (Sorry DrunkenHyena!)
Kevin Wilson of The VB Zone (http://www.thevbzone.com/) was kind enough to write a Module (http://www.thevbzone.com/modFreeImage.bas) which allows us to easily access the library through Visual Basic. Included in the comments at the top is a few examples of how to use the Loading/Saving functions of the library. Thatís where this next example comes from.
So we get FreeImage to load our image, we then find out from it what the size of the image is, create the surface to that size, and then (and this is the good bit), we can use the library to load the image straight into the hDC of our surface, thereís no need for any other controls. The code behind it is a bit tricky, so Iíll break it down into (hopefully) easily digestible chunks.
'We need to check what Image Format is being used, so we can tell
'if FreeImage supports it
Dim ImageFormat As FREE_IMAGE_FORMAT
'Some holders for DIB's (Device Independent Bitmaps)
Dim DIB As Long
Dim DIB_New As Long
'A holder for our loaded BITMAP
Dim hBITMAP As Long
Right, fairly self explanatory, just a few things weíre going to need to load our image. FreeImage is a little bit of a pain in how you get it to load images, Iím not 100% sure of why itís set up the way it is, but you have to play by its rules.
'Initialise the FreeImage library
'Find out what kind of filetype we're dealing with
ImageFormat = FreeImage_GetFileType(FileName)
'We can only deal with the image if it's a known format
If ImageFormat <> FIF_UNKNOWN Then
ĎLoad the image
'And finally, deinitialise the FreeImage library. We're finished!
Alright, initialise, get the format, as long as FreeImage recognises it load it, and finally deinitialise. Now just the tricky bit, the loading
'Load the Device Independent Bitmap
DIB = FreeImage_Load(ImageFormat, FileName, 0)
'If the loading failed, the DIB would be 0
If DIB <> 0 Then
'Get a BMP from the DIB
If FreeImage_BMP_From_DIB(DIB, hBITMAP) = True Then
'Create our surface.
'Get our Device Context, as before
DC = surfImage.GetDC
'The next few steps I don't fully understand, but was written
'Following the examples given in the FreeImage module
'First we render the bitmap into our Surfaces' DC
FreeImage_RenderBitmap hBITMAP, DC
'Then we convert the Bitmap back into a DIB (why I'm not sure)
If FreeImage_DIB_From_BMP(hBITMAP, DIB_New) = True Then
'And then render the Device Independent Bitmap
FreeImage_RenderDIB DIB_New, DC
'And finally free the new Device Independent Bitmap
'Release our Surface's DC and unlock it
'Delete our Bitmap
'Unload our original DIB
Alright, into the meat of it. We tell FreeImage to load our file, and specify the Format which weíve already found out. Next, we check to see that it actually managed to load the DIB. We then try and get a BMP from the DIB. If this succeeds we create our surface (Iíve removed this code for brevity, but as far as getting the width and height is concerned, we can pass FreeImage a hBITMAP into itís FreeImage_GetBitmapWidth and FreeImage_GetBitmapHeight methods to find out). Then, we get the DC, render the bitmap to the hDC, convert the hBITMAP back into a DIB (why we do all this I donít know, but I was trying to follow the example in the modFreeImage comments). We render the DIB to our surfaces hDC, and then set about Freeing all our DIBs, Releasing our hDCís, and Deleting our hBITMAPS. Itís just that easy!
The next post is my final comments and the source code.
02-09-2004, 07:24 AM
Well, if youíve got this far, thanks for reading my first tutorial. Iím not normally one for writing huge explanations for my work Ė especially when other people can write so much better than me Ė but as I hadnít seen any attempts at a tutorial like this I thought someone had better do it. If you have any questions, or notice any errors in this tutorial, feel free to contact me. My contact details are all available in my profile.
As for the source code, itís just a simple little program which demonstrates all that Iíve covered in this tutorial (in fact, all the code snippets were stolen from it). Itís heavily commented, and Iíve tried to make it so that you can easily lift the LoadFromPictureBox and LoadFromFreeImage subs out of the code and put them in your own and it will work. All I ask is that if you make anything cool you let me know, Iím always excited to see what other people are working on in the field of amateur gaming. Of course, a thanks in the documentation is always nice too.
In trying to keep with the rules of this forum, I havenít included the FreeImage library. You can get this from the FreeImage Download Page (http://freeimage.sourceforge.net/download.html). Just put it in the same directory as the source, and it should all work. The source will work without the library, but you will get an error when trying to load PNGs.
Well, thatís all from me. Thanks for reading,
Adrian ďJimCamelĒ Clark
02-06-2005, 01:48 PM
I was looking around the old dusty DirectX7 SDK, and while there weren't a lot of VB examples I found a function that was pretty cool.
The function simply takes a file name and creates a DirectDraw surface with any image compatible with LoadPicture.
Same idea, but using the StdPicture object instead of a picturebox :)
I added to this newly found function, a halftone stretchblt option for stretching images, along with user defined width and height.
This way you can create a stretched or shrunk surface from an original image.
I'll post the project I was doing some testing with.
The original function was found in the source for 'SPACE SHOOTER 2000!
'Main programming by Adam "Gollum" Lonnberg.
I didn't see any copyright notice.. but I thought I'd at least post the original coder's name.
Anyway, here's the function:
Public Function CreateDDSFromBitmap(dd As DirectDraw7, ByVal strFile As String, _
Optional VideoMem As Boolean, Optional mWidth As Long = 0, _
Optional mHeight As Long = 0) As DirectDrawSurface7
'This function creates a direct draw surface from any valid file format that loadpicture uses, and returns
'the newly created surface.
'The function has options for video memory or new image dimensions.
Dim ddsd As DDSURFACEDESC2 'Surface description
Dim dds As DirectDrawSurface7 'Created surface
Dim hdcPicture As Long 'Device context for picture
Dim hdcSurface As Long 'Device context for surface
Dim Pic As StdPicture 'stdole2 StdPicture object
Dim picWidth As Long
Dim picHeight As Long
Set Pic = LoadPicture(strFile) 'Load the bitmap
'Convert the Picture object's HIMETRICS to PIXELS
picWidth = Screen.ActiveForm.ScaleX(Pic.Width, vbHimetric, vbPixels)
picHeight = Screen.ActiveForm.ScaleY(Pic.Height, vbHimetric, vbPixels)
'Fill the surface description
.lFlags = DDSD_CAPS Or DDSD_HEIGHT Or DDSD_WIDTH
If VideoMem Then
'Create the surface in video memory
.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN Or DDSCAPS_VIDEOMEMORY
'Create the surface in system memory
.ddsCaps.lCaps = DDSCAPS_OFFSCREENPLAIN Or DDSCAPS_SYSTEMMEMORY
If mWidth = 0 Then
.lWidth = picWidth 'Use the image dimensions
.lWidth = mWidth 'Use the user defined dimensions
If mHeight = 0 Then
.lHeight = picHeight
.lHeight = mHeight
Set dds = dd.CreateSurface(ddsd) 'Create the surface
hdcPicture = CreateCompatibleDC(ByVal 0&) 'Create a memory device context
SelectObject hdcPicture, Pic.Handle 'Select the bitmap into this memory device
dds.restore 'Restore the surface
hdcSurface = dds.GetDC 'Get the surface's DC
SetStretchBltMode hdcSurface, HALFTONE 'Set to HALFTONE for optional image stretching
'Copy from the memory device to the DirectDrawSurface
StretchBlt hdcSurface, 0, 0, ddsd.lWidth, ddsd.lHeight, hdcPicture, 0, 0, _
picWidth, picHeight, vbSrcCopy
dds.ReleaseDC hdcSurface 'Release the surface's DC
DeleteDC hdcPicture 'Release the memory device context
Set Pic = Nothing 'Release the picture object
Set CreateDDSFromBitmap = dds 'Sets the function to the newly created direct draw surface
An example of calling the function in my project:
Set BackSurf = CreateDDSFromBitmap(dd, App.Path & "\Island.jpg", False, 640, 480)
Set SpriteSurf = CreateDDSFromBitmap(dd, App.Path & "\Lolo.gif", False)
I hope this is useful enough to warrant posting ;)