Gigi Labs

Please follow Gigi Labs for the latest articles. Programmer's Ranch no longer has its domain, so please update your bookmarks and links to programmersranch.blogspot.com.

Sunday, November 17, 2013

SDL2: Displaying an Image in the Window

Hello! :)

[Update 2015-11-14: This article is out of date. Check out the latestversion at Gigi Labs.]

In this article we're going to learn how to load a bitmap image from disk and display it in an SDL2 window. In order to follow along, you'll need to set up a project to work with SDL (see "SDL2: Setting up SDL2 in Visual Studio 2010"), and start off with the basic empty window code from SDL2: Empty Window, which is this:

    #include <SDL2/SDL.h>        
            
    int main(int argc, char ** argv)        
    {        
        bool quit = false;        
        SDL_Event event;        
            
        SDL_Init(SDL_INIT_VIDEO);        
            
        SDL_Window * screen = SDL_CreateWindow("My SDL Empty Window",
            SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480,
            0);        
            
        while (!quit)        
        {        
           SDL_WaitEvent(&event);        
            
           switch(event.type)        
           {        
           case SDL_QUIT:        
               quit = true;        
               break;        
           }        
        }        
            
        SDL_Quit();        
            
        return 0;        
    }

Build the application. Remember to place SDL2.dll into your bin\Debug folder. The empty window should appear if you run the application, and you should be able to close it by clicking on the "X" at the top-right corner.

Let's first change the window declaration a little bit (I changed the name of the variable and the window title):

    SDL_Window * window = SDL_CreateWindow("SDL2 Displaying Image",
        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);

After that, let's create an SDL_Renderer:

    SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, 0);

We use SDL_CreateRenderer() to get an instance of SDL_Renderer. This renderer represents the output device (usually your graphics card) to which your code will be drawing. In fact it's the final destination of our image, because we'll be following the steps below:


SDL_CreateRenderer() takes three parameters. The first is the window where we are drawing. The second allows us to select different rendering drivers; in our case we don't care, and we can set it to -1 and get the default one. The last parameter allows us to set SDL_RendererFlags to control how the rendering occurs. We can set it to zero to default to hardware rendering. If any of this sounds confusing, don't worry about it and just use SDL_CreateRender() as it is here.

We can now load the image from disk using the SDL_LoadBMP() function, which takes a path to a bitmap and loads it into an SDL_Surface:

    SDL_Surface * image = SDL_LoadBMP("image.bmp");

I'm using the following photo of the Grand Harbour in Valletta, Malta, which I took back in 2005, and which has a width of 320 pixels and a height of 240 pixels:


Place the image in your bin\Debug folder, where your executable is located.

Next, we need to create a texture.

    SDL_Texture * texture = SDL_CreateTextureFromSurface(renderer,
        image);

A texture is memory close to the graphics card (see image below), and we use SDL_CreateTextureFromSurface() to directly map our surface (which contains the image we loaded) to a texture.


Now that we have a texture, let's display it in the window. At the end of the while loop, just after the switch statement, add the following:

        SDL_RenderCopy(renderer, texture, NULL, NULL);
        SDL_RenderPresent(renderer);

We use SDL_RenderCopy() to copy the texture to the output device. There are also a couple of other parameters that we're setting to NULL - more on these in a minute. Finally, SDL_RenderPresent() is what commits the texture to the video memory, displaying the image.

Before we run the program, we must always remember to clean up every resource that we initialise. In our case, that means adding the following just before the call to SDL_Quit():

    SDL_DestroyTexture(texture);  
    SDL_FreeSurface(image);  
    SDL_DestroyRenderer(renderer);  
    SDL_DestroyWindow(window);

If you now run the program from within Visual Studio, surprisingly enough, you get a blank window:


...whereas if you run the program directly from the executable in your solution's bin\Debug folder (where image.bmp is also located), it works:


We'd know more about what's going on if we had proper error handling in place. With every call to the SDL2 API, we should be checking the return values, and displaying an error if any of them fail. SDL_ShowSimpleMessageBox() is great for displaying simple error messages, while SDL_GetError() gives us the actual error text of the last thing that failed. Let's refactor our initialisation code to check for errors and display them:

    SDL_Window * window = SDL_CreateWindow("SDL2 Displaying Image",
        SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 480, 0);
    if (window == NULL)
        SDL_ShowSimpleMessageBox(0, "Window init error", SDL_GetError(),
            window);
    SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, 0);
    if (renderer == NULL)
        SDL_ShowSimpleMessageBox(0, "Renderer init error",
            SDL_GetError(), window);
    SDL_Surface * image = SDL_LoadBMP("image.bmp");
    if (image == NULL)
        SDL_ShowSimpleMessageBox(0, "Image init error", SDL_GetError(),
            window);
    SDL_Texture * texture = SDL_CreateTextureFromSurface(renderer,
        image);
    if (texture == NULL)
        SDL_ShowSimpleMessageBox(0, "Texture init error",
            SDL_GetError(), window);

When we run this code, the first error we get is the following:


That's it - the program can't find the image. Remember how it worked when running from the executable, but not from Visual Studio? That's because Visual Studio starts at a different directory. As a quick workaround, just copy image.bmp into your source folder (where main.cpp is located). Hit F5, and it now works:


Fantastic! The image is right there in the window! But... can you see what happened? The image, which has dimensions 320x240, has been stretched to fill the window, which has dimensions 640x480. That's sometimes convenient, because we get scaling for free. However, it makes photos look messy. Let's change our call to SDL_RenderCopy() to fix this:

        SDL_Rect dstrect = { 5, 5, 320, 240 };
        SDL_RenderCopy(renderer, texture, NULL, &dstrect);

Remember those two parameters at the end of SDL_RenderCopy() that we were setting to NULL? If you look at the documentation for SDL_RenderCopy(), you'll see that the last one defines a destination region (which part of the texture will the image be copied to). If it's set to NULL, then the image is stretched to fill the texture - which is what happened above.

By setting dstrect (the last parameter), we can render to only a portion of the window. We set it to an SDL_Rect, with x and y set to 5, and width and height corresponding to the dimensions of the image. When we run the program, we get this:


Excellent! :) In this article we looked at how to create renderers, surfaces and textures. We used these to load bitmaps and display them on the screen. A glimpse into error handling allowed us to troubleshoot issues with loading the image from Visual Studio. Finally, we saw the difference between copying the image to a region of the window, and making it fill the entire window.

I hope you enjoyed this. Come back for more! :)

No comments:

Post a Comment