In this tutorial we will look at perhaps the most important step in creating a software-rendered application: creating a window.
Before we get into writing our code, we first need to cover some basic theory...
As Windows is a closed source operating system, there is a limit to what we can do from scratch within it. Ultimately, window creation is a process handled by the operating system, so we cannot just create a window ourselves. What we can do, though, is communicate to the operating system that we would like a window created via an application programming interface (a set of functions used by one piece of software to communicate with another). In this case, we will be using the Win32 API. It should be ready to use for most Windows computers, but if your code does not work, you may wish to look into downloading the latest version of the Windows SDK (software development kit).
First, we will need to include some headers. We will include "windows.h", which will give us access to almost all of the Win32 functions, as well as the "stdlib.h" and "stdio.h" headers for logging data to the console when debugging. Next, we will define two constants - our window width and window height. I will create a 640x480 window, but you can use whatever size you would like. Note that since we are using software rendering, you will want to choose a smaller size, since larger resolutions will take longer to render. Without the added power of hardware acceleration and the GPU, we may need to make a compromise between our program's resolution and its framerate later on. In my experience, resolutions of 800x600 and smaller run reasonably well, even when there is a lot going on on screen.
Next, we will create a global variable called "running" this will be initialised to 1. Running will have value 1 while our program is running, and will be set to 0 to mark the end of our program loop later on (we'll get to this soon). In the code below, I have also declared a function called "windowProcedure" at the top, but we will look at this function in more depth shortly.
Now we need to create our entry point. In graphical Windows apps, we use the function WinMain as our entry point, since it takes some useful additional parameters. The first parameter is of type HINSTANCE. A HINSTANCE is a "handle to an instance". A handle is simply just a numerical identifier. When using the Win32 API, the OS will create and manage objects and data strucures for us. We use handles (numerical IDs) to interact with these objects and structures. The "instance" part of HINSTANCE refers to the application instance - i.e. the running program. Therefore, the first parameter is a pointer to the application.
The second parameter of WinMain is another HINSTANCE, typically named "hPrevInstance". This is a redundant feature from 16-bit Windows, and no longer servers any purpose. It is included purely for compatibility's sake with older version of Windows. The next parameter is of type LPSTR. This is short for "long pointer to string", and is a list of command line arguments as a single string. This is named "cmd". The last parameter is an integer often called "nCmdShow". It stores a value that represents how the application should be shown. Remember, all of these parameters are passed into WinMain by the operating system, so we do not need to set them.
Note that WinMain returns an integer, like "int main". We typically need to specify the calling convention of WinMain to the WINAPI calling convention. A calling convention is simply just a way of storing data (such as function return addresses, prameters and local variables) on the system's call stack. We set the calling convention by specifying its identifier after the return type and before the function name. Our definition of WinMain should start with the line "int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstace, LPSTR cmd, int nCmdShow){".
A "window class" is a data structure, typically written as "WNDCLASS" or "WNDCLASSEX" (for the extended version) in code, that stores various pieces of data about our window. If you would like to create a window, you must first create a window class. We will create a window class structure (WNDCLASSEX) inside our entry point. Upon initialisation, we should set all the data in this window class to 0, using "WNDCLASSEX windowClass = {0};".
Once our window class structure has been initialised, we should set its properties. First, we must set the "cbSize" property to the size of a window class structure (i.e. "windowClass.cbSize = sizeof(WNDCLASSEX)). cbSize is short for "count bytes size" and should be set to the number of bytes that the structure takes up. Next we must set the property "lpszClassName", which is short for "long pointer to C-string class name". Unfortunately, I cannot tell you why "lpsz" corresponds to "long pointer to C string", but lpszClassName is definitely a string (and as we're using C, it is certainly a C string). The class name is a string identifier used to ideintify our window class. Once we have created our window class, we must send it to the OS (which stores a table of window classes for running applications), and we will need to use the class name to identify it.
The next property to set is the window style. Somewhat confusingly, style refers to how the window behaves, rather than how it looks, in this context. We will set this to two flas, combied with a bitwise OR operation: CS_HREDRAW CS_VREDRAW. CS stands for class style (I think) and H and V stand for horizontal and vertical. These flags state that all windows of this class should redraw when horizontally or vertically resized. The next property we will set is called lpfnWndProc, and stands for "long pointer to function window procedure". The window procedure is the event handler function for our window class, so we should set lpfnWndProc to the window procedure identifier. We will write our window procedure later on in this tutorial, and will call it (somewhat uncreatively) "windowProcedure", so we can write "windowClass.lpfnWndProc = windowProcedure;".
The final property to set is the background color of the window. This property is called hbrBackground, which is short for "handle to brush background". We can use the CreateSolidBrush function to get the OS to create a solid colour brush object. This brush object is simply just a set of data instructing the OS on how to draw something. CreateSolidBrush takes a colour value, which we can use the RGB macro to generate. The RGB macro takes three 8-bit numbers (a red, green and blue colour value) as inputs. CreateSolidBrush returns a handle, so we can simply set hbrBackground to the return value of CreateSolidBrush, although we will likely need to cast it to a HBRUSH first. Overall, this gives us the line: "windowClass.hbrBackground = (HBRUSH) CreateSolidBrush(RGB(100, 100, 100));". I have chosen the colour (100, 100, 100), which is a reasonably dark grey, but you may choose whichever colour you would like.
Once we have set our class properties, we must send our window class to the operating system, so that it can be added to the system's set of window classes for currently running programs. We do this with the RegisterClassEx function, which takes a pointer to our window class (giving the line: "RegisterClassEx(&windowClass)").
When we create a window, we want to set the client area size - this is the area that we can actually draw to, but unfortunately we cannot just do this. What we can do, though, is set the entire size of the window, including the client area (the area we draw to) and the window frame (the outer border of the window, including the title text at the top and any menu bars we may want). Use a function called AdjustWindowRectEx to get the total window size from a given client area size.
The first thing we want to do is create a RECT structure (a rectangle) to store the client area size. The first property of the RECT structure is the leftmost x coordinate, then the top y coordinate, then the rightmost x coordinate and finally the bottom y coordinate. Somewhat confusingly here, the top left is the point (0,0), even though when we come to drawing shapes to our window later on the point (0,0) will be at the bottom left. We can create our window/client rectangle using the line "RECT windowSize = {0, 0, WINDOW_WIDTH, WINDOW_HEIGHT};".
Now that our window rectangle has been created, and currently contains the client area, we can pass it through the AdjustWindowRectEx function to get the total size of our window. AdjustWindowRectEx takes four arguments: the first is a pointer to our window rectangle, the second is a window style, the third is whether there will be a menu bar and the fourth is an extended window style (not to be confused with a window class style). We can set the second paramter to WS_OVERLAPPEDWINDOW, the default standard window style (remember the window "style" is really just the behaviour of the window). The third parameter should be set to 0, as we do not want there to be a menu bar. We will set the fourth parameter to the value WS_EX_OVERLAPPEDWINDOW, which is the default extended style for a window (we do not need to worry about window styles that much, as just want a simple window we can draw to - i.e. the default type). This gives the line: "AdjustWindowRectEx(&windowRect, WS_OVERLAPPEDWINDOW, 0, WS_EX_OVERLAPPEDWINDOW);". Our windowRect rectangle now contains the coordinates (0, 0) and (width, height) of our outer window frame.
Now we have set up the window class and size, we can create the actual window. As previously mentioned, we cannot just create a window, but we can get the OS to create a window and have it send us back the window handle. We can create a HWND (handle to window) variable and set it to our window's handle.
We can have the OS create a window with the CreateWindowEx function. The CreateWindowEx function returns a window handle, and takes multiple parameters. The first parameter is the extended window style, we will set this to the default: WS_EX_OVERLAPPEDWINDOW.
The second parameter is the window class, we will set this to "windowClass.lpszClassName". The third parameter is the window title, this is a string and is the text that will appear in the top left corner. The fourth parameter is the window style. Again, we will set this to the default: WS_OVERLAPPEDWINDOW. Remember that the window style is simply just the basic behaviour of the window. WS_OVERLAPPEDWINDOW is simply just he default behaviour of the window (i.e. resizable, movable, etc.).
The fifth and sixth parameters are the x and y coordinates of the top left corner of the window. We can set these to the default alues, using CW_DEFAULT (where CW stands for create window). The seventh and eightth parameters are the width and height. The width is equal to windowRect.right - windowRect.left and the height is equal to windowRect.bottom - windowRect.top (remember that the top left is the point (0, 0)).
The ninth parameter is the parent window. Our window is not inside another window, so has no parent window. Therefore, NULL can be passed for the ninth parameter. The tenth parameter is the menu bar we would like for our window, we do not want one so can pass in NULL. The eleventh parameter is the application instance that the window belongs to, so we will pass in hInstance (the first WinMain parameter). The twelfth parameter is an LPARAM, and is used to send additional data. We do not need to send additional data, so can set the twelfth parameter to NULL.
We can check that the window failed to create by checking if its window handle is 0. We can do this with the line: "if(!windowHandle){". If the window handle is 0, and our window has failed to create, we can send an alert to our program using a message box.
We can create a message box using the MessageBox function. The first parameter it takes is a parent window - we should set this to NULL as our window has not created. The second is the message box's title text. The third is the text inside the box and the fourth is a set of flags. We will pass the MB_OK flag and the MB_ICONERROR flag, to indicate that the box should have an OK button and should display an error icon. These should be joined with a bitwise OR operation (e.g. "MB_OK | MB_ICONERROR"). Once our message box has finished (it will block the current thread until the user clicks a button on it), we can return -1 to exit out of WinMain.
If our window handle is valid, that will be ignored, and we can show it. We can do that with the ShowWindow function, which takes two parameters. The first is the window handle to show, and the second is an integer, the value of which instructs the OS on how to show the window. We want to pass in our nCmdShow integer (the fourth WinMain parameter) for this.
We will want our program to run until the exit button is clicked. To accomplish this, we should first start with a while loop that iterates so long as our "running" global variable is non-zero. We could write this as "while(running){".
Within the main loop, we will need to handle our events, or messages as they are called in Windows. There are thousands of different types of messages in Windows, including keyboard inputs, mouse inputs, window resize messages and many more. We want to handle any messages that pop up.
Before we can start handling messags, we have to create a message ("MSG" in code) structure to contain them. We can do this with the line "MSG message;". Next we want to start a while loop to iterate until all messages are handled. The condition of this while loop will be that the function PeekMessage returns non-zero. PeekMessage checks to see if there are any messages (events) on the OS' message queue. If there are messages, a non-zero value will be returned so we can handle said messages and check again. If there are no messages queued up, PeekMessage will return 0, exitting out of our loop.
PeekMessage takes five parameters. The first is a pointer to a message structure to be filled in with the most recent message, we can put "&message" in here. The second is a window handle. We can pass in windowHandle. The third is the minium message ID (this can be set to allow us to only process certain types of messages). We will set this to 0 to process all messages. The fourth is the maximum message ID. We will also set this to 0, as when both the minimum and maximum message IDs are set to 0, the minimum and maximum ranges will be overridden and all messages will be retrieved. The final parameter is the flags parameter. We will pass in the PM_REMOVE flag, which indicates that messages should be removed from the message queue.
Inside our PeekMessage loop, we need to call two functions. The first is TranslateMessage, and takes a pointer to our message structure. This function converts key codes from text-based messages into their respective characters. The second function is DispatchMessage. It also takes a pointer to our message structure. This sends our message to be handled by the window procedure, which we will look into writing next. This ends our WinMain function, so after our while(running) loop, we can return 0 to finish the WinMain function.
The window procedure is the message handler function for a window. A window procedure is connected to a window via our window class - you may remember that we set the window class' lpfnWndProc property to windowProcedure.
A window procedure should return an LRESULT - this is simply just a long long int that is typically used to indicate a success or error status. It has the calling convention CALLBACK. We can name our window procedure whatever we would like, so long as we specify the same name in the lpfnWndProc property of the window class. I have chosen the name windowProcedure.
The window procedure takes four parameters. The first parameter is the window handle, which we could call windowHandle. The second parameter is a UINT (unsigned integer) that holds the ID of the message (e.g. WM_DESTROY is the message ID of the window destroy message - i.e. when the X button is clicked). We could call this messageID. The third parameter is a WPARAM, which we could call wParam. This is a 32-bit integer, typically used to store integers or pointers. The fourth parameter is an LPARAM, which is a redefinition of the WPARAM type (although they used to be different on 16-bit Windows) and will hold a different value. We could call this lParam. Both WPARAM and LPARAM are used to pass in data about the event (for example a key press event will need to pass in which key was pressed).
Inside our window procedure function, we will need to check which message we are responding to. We can do this with the line "switch(messageID){". We only need to check for one message in this program, and that is the window destroy message (WM_DESTROY). If this message is received, we should call the PostQuitMessage function. This tells the program to send a WM_QUIT message to the clicked window. This message will be handled automatically and will close the window. PostQuitMessage takes one parameter, the WPARAM of the WM_QUIT message. We will pass in the value 0 here. After calling PostQuitMessage, we want to set our global variable running to 0 to stop the main loop. After this, we want to return 0 out of our window procedure.
There are thousands of different message types, and we do not want to check them all. Therefore, we can call the DefWindowProc function after the switch-case statement. This carries out the built in default window procedure. We want to carry this out for all unhandled events. DefWindowProc takes the same parameters as our window procedure, so we can just pass those parameters in. We also want to return the value returned by DefWindowProc to exit the function with the corresponding message code. This can be done with the line "return DefWindowProc(windowHandle, messageID, wParam, lParam);"
This completes our window procedure and thus our program. The code for this can be found below.
The code below will create a window. I recommend that you read through the comments thoroughly and take a while to play around with the code to improve your understanding.
I am working on Windows 10 using the MinGW compiler. I am compiling this program with the command: "gcc main.c -lgdi32 -o app". If you are on Visual Studio, this code should work, but you will need to link the gdi32 library some other way (possibly by including "#pragma comment(lib, gdi32)" at the top of your code).
main.c
Running the program should give an output similar to the window shown below. The window should close when you click the quit button.