Home

Windowing

Distributed under the terms of the CC BY-NC-ND 4.0 License.

  1. Universal Aspects of Window Systems
  2. Creating a Window on Windows OS
  3. Source Code (external link GitHub)

Creating a Window on Windows OS

Reading time: 16 mins.

At the end of this chapter, we will have a fully functional C++ app showing you how to display an image and draw on the screen using the mouse. This will take no more than roughly 200 lines of code, which is nothing in coding terms, and will be compilable with a single command.

Again, this is not a lesson on Windows programming, so we won't delve into too many details regarding how things work, just enough to get you started. We will leave readers interested in exploring all the code provided in the sample that's specific to the API to finding resources that will delve into this specifically. The Microsoft website is relatively nicely done with respect to this and provides some good step-by-step examples.

Additionally, and more fundamentally, Scratchapixel's mission is not to teach about how to use APIs, which would typically be left to tutorials, but rather to explain the algorithms used by these APIs and how they work. This includes a general focus on algorithms and techniques used in computer graphics.

Enter the Infinite Loop

Now let's dive into it. Native Windows programs often replace the usual int main() entry point into the program with a method called WinMain. That's not necessary as we prefer to stick with a traditional app structure; we will stick to main:

int main(int argc, char** argv) {
	HINSTANCE hInstance = GetModuleHandle(NULL);
	CreateAndRegisterWindow(hInstance);
	MSG msg;
	while (1) {
		while(PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE) != 0) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
			if (msg.message == WM_QUIT) {
				break;
			}
		}
		if (msg.message == WM_QUIT)
			break;
		DoSomeWork();
	}
	return 0;
}

The only reason why you might want to use WinMain is that it directly passes an hInstance value to the method, whereas if you don't use WinMain, you have to create it yourself using GetModuleHandle(NULL). Obviously, that's so simple that this is not a good reason in itself for using WinMain. Then we call the method to create the window. We will dive into this next. Once the window is up and running, we enter our infinite loop. We check for messages, process them, and where no new messages are left in the queue, we do some work. As mentioned in the previous chapter, this is where we will be doing our rendering. Finally, note that if one of the messages is about quitting the app (WM_QUIT), then we break from the event loop and then break from the outer loop as well, effectively quitting the app. The reason why we do so is because while generally quitting is quitting, Windows happens to sometimes generate events (notably WM_TIMER event) after a WM_QUIT is received. So one might expect that more messages will be received after a WM_QUIT event.

TranslateMessage and DispatchMessage are Windows native calls. When we call these functions, Windows redispatches these messages to a method known as WinProc, which is essentially where we will add all of our logic for processing things such as mouse or keyboard events. More on this when we get to creating the window.

Now let's see what the CreateAndRegisterWindow method looks like.

Creating the window

Here is the code:

void CreateAndRegisterWindow(HINSTANCE hInstance) {
	WNDCLASSEX wc = {0};
	wc.cbSize = sizeof(WNDCLASSEX);
	wc.lpfnWndProc = WndProc;
	wc.hInstance = hInstance;
	wc.lpszClassName = CLASSNAME;
	wc.hCursor = LoadCursor(nullptr, IDC_ARROW); // Set the default arrow cursor
	wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION); // Load the default application icon
	wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszMenuName = nullptr;
	wc.hIconSm = LoadIcon(hInstance, IDI_APPLICATION); // Load the small icon for the application

	if (!RegisterClassEx(&wc)) {
		MessageBox(nullptr, L"Window Registration Failed", L"Error",
			MB_ICONEXCLAMATION | MB_OK);
	}

	hwnd = CreateWindowEx(
		WS_EX_CLIENTEDGE,
		CLASSNAME,
		L"Foo",
		WS_OVERLAPPEDWINDOW & ~WS_THICKFRAME & ~WS_MAXIMIZEBOX, // non-resizable
		CW_USEDEFAULT, CW_USEDEFAULT, win_width, win_height,
		nullptr, nullptr, hInstance, nullptr);

	if (hwnd == nullptr) {
		MessageBox(nullptr, L"Window Creation Failed", L"Error",
			MB_ICONEXCLAMATION | MB_OK);
	}

	InitializeOffScreenDC(hwnd);

	ShowWindow(hwnd, SW_SHOWDEFAULT); // or use WS_VISIBLE but more control with this option
	UpdateWindow(hwnd);
}

Creating a window is straightforward. First, we need to set some fields of the WNDCLASSEX structure, which, as the name suggests, is used to register our window's class. The concept behind window classes is to define a set of behaviors that windows of a certain class will share, and WNDCLASSEX describes the characteristics of a window class. The three most important components of the structure to focus on for now are:

After setting the member variables of the WNDCLASSEX instance, we register our class and check for success or failure. If successful, we proceed to the next step: creating the window itself. CreateWindowEx has several parameters, all of which can be found online, so we won't go into much detail here. Just note that we pass the class name registered in the previous step, set the window's title name (Foo), and pass flags that, in our example, prevent the window from being resizable. We use default x and y values for the window's position on the screen, along with the window's width and height.

That's it. Then, we call ShowWindow and UpdateWindow, both Windows API calls. The first is responsible for displaying the window on the screen, and the second forces a redraw of the window's content.

Easy breezy.

The only function we haven't yet explained is InitializeOffScreenDC(hwnd), as it isn't directly related to the window creation process. Recall that our program's goal is to display an image. This function is instrumental in achieving that, but before we delve into it, let's first review the WndProc function.

Handling The Window's Events

Here is the code:

LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) {
	switch(msg) {
		case WM_CLOSE:
			if (hBitmap != nullptr) {
				DeleteObject(hBitmap);
				hBitmap = nullptr;
			}
			CleanupOffScreenDC();
			DestroyWindow(hWnd);
			break;
		case WM_DESTROY:
			PostQuitMessage(0);
			break;
		case WM_LBUTTONDOWN:
			is_drawing = true;
			break;
		case WM_LBUTTONUP:
			is_drawing = false;
			break;
		case WM_MOUSEMOVE: {
			int xpos = GET_X_LPARAM(lParam);
			int ypos = GET_Y_LPARAM(lParam);
			if (is_drawing) {
				SetPixelColor(pBits, win_width, xpos, ypos, 255, 0, 0);
				InvalidateRect(hWnd, NULL, TRUE);
			}
			break;
		}
		case WM_ERASEBKGND:
			return 1; // Indicate that background erase is handled
		case WM_PAINT:
			{
				PAINTSTRUCT ps;
				HDC hdc = BeginPaint(hWnd, &ps);
				BitBlt(hdc, 0, 0, win_width, win_height, hdcOffscreen, 0, 0, SRCCOPY);
				EndPaint(hWnd, &ps);
			}
			break;
		default:
			return DefWindowProc(hWnd, msg, wParam, lParam);
	}
	return 0;
}

The type of event sent to our window is stored in the msg variable. The handling approach is straightforward: we use a switch-case construct to check the type of event received and associate specific code with each event type we're interested in. Windows provides dozens, if not hundreds, of event types, which you can find a list of here. The ones we'll start with are prefixed with WM, standing for Window Messages. This includes all keyboard and mouse events, as well as messages like WM_PAINT, which is sent to the window when the system requires it to repaint after being obscured by another window, for instance, or WM_CLOSE and WM_DESTROY, which are triggered when a user closes the window or when it gets terminated (such as being killed via Task Manager).

Note that our code doesn't handle the WM_SIZE event because our window is not resizable.

We won't delve deeply into this straightforward code. However, it's worth discussing how the program is structured to help us achieve our goal, which is, in addition to displaying an image, to enable drawing on the image. We want to do this to demonstrate how mouse and key events can be captured for navigation through a 3D scene, which will be the topic of our next lesson.

There's not much to say about WM_CLOSE or WM_DESTROY other than some cleanup is necessary when exiting the app. More interestingly, let's look at WM_LBUTTONDOWN and WM_LBUTTONUP, triggered when you press and release the left mouse button, respectively. Pressing the button sets a boolean variable is_drawing to true, indicating to the app that any subsequent mouse movement will be used to draw on the image. Releasing the button sets is_drawing to false, mimicking the behavior of a paint program's brush tool. The WM_MOUSEMOVE event is triggered when the mouse moves. We can obtain the x and y mouse positions using the GET_X_LPARAM macro. For more details on this, consider checking a tutorial on Windows programming; it's quite straightforward. If is_drawing is true—indicating the user is holding down the left mouse button while moving the mouse—then we draw into the buffer holding our image data (more on this in the next section). We then call the native Windows function InvalidateRect, which tells Windows to force a redraw of the window's content.

As expected, this triggers a WM_PAINT event, which looks similar and contains drawing code typically enclosed within a BeginPaint-EndPaint pair. hdc represents a handle to the window's device context, which we need to pass to drawing calls like BitBlt, used here to request the bitmap to be drawn on the screen.

If we're not processing an event ourselves, we simply reroute it to a default message handling function, DefWindowProc (provided by Windows, not our own creation).

Of course, it's up to you to now expand on this basic example. For instance, if you need to incorporate the middle and right mouse buttons to extend what your own app can do, then all you need to do is search the documentation to find out which messages these events correspond to (in this example, this would be WM_RBUTTONDOWN for the right mouse button and WM_MBUTTONDOWN for the middle mouse button). There are countless more options here, leading to a great number of possibilities. Let your imagination go wild and be creative!

That's the gist of it. How much simpler could it be? All that's left at this stage is to see how we can load an image and create a Windows-compatible bitmap object, as well as how to draw into this image, and then we will be done.

Creating a Windows Compatible Bitmap

Here is the code we will be using:

auto CreateBitmapFromRGB(char* pData, int width, int height) 
	-> std::pair<HBITMAP, void*> {
	BITMAPINFO bmi = {0};
	bmi.bmiHeader.biSize = sizeof(bmi.bmiHeader);
    bmi.bmiHeader.biWidth = width;
    bmi.bmiHeader.biHeight = -height; // Negative indicates top-down bitmap
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 24; // Assuming 24-bit RGB
    bmi.bmiHeader.biCompression = BI_RGB;

	HDC hdc = GetDC(nullptr);
    void* pBits;
    HBITMAP hbm = CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, &pBits, nullptr, 0);
    if (hbm != nullptr) {
        std::memcpy(pBits, pData, width * height * 3); // Assuming 3 bytes per pixel (RGB)
    }
    ReleaseDC(nullptr, hdc);
    return {hbm, pBits};
}

void InitializeOffScreenDC(HWND hwnd) {
	std::unique_ptr<char[]> raw_data(new char[win_width * win_height * 3]);
	
	memset(raw_data.get(), 0x0, win_width * win_height * 3);
	std::ifstream ifs("./sample.pbm", std::ios::binary);
	std::string header;
	int width, height, bpp;
	ifs >> header;
	ifs >> width >> height >> bpp;
	ifs.ignore();
	ifs.read(raw_data.get(), win_width * win_height * 3);
	for (uint32_t i = 0; i < win_width * win_height * 3; i += 3) {
		std::swap(raw_data[i], raw_data[i + 2]);
	}
	ifs.close();
    
    auto bitmap_data = CreateBitmapFromRGB(raw_data.get(), win_width, win_height);
    hBitmap = bitmap_data.first;
    pBits = bitmap_data.second;

    HDC hdc = GetDC(hwnd);
    hdcOffscreen = CreateCompatibleDC(hdc);
    SelectObject(hdcOffscreen, hBitmap);
    ReleaseDC(hwnd, hdc);
}

void CleanupOffScreenDC() {
    if (hdcOffscreen) DeleteDC(hdcOffscreen); // Delete the off-screen DC
}

void SetPixelColor(void* pBits, int width, int x, int y, uint8_t red, uint8_t green, uint8_t blue) {
    if (!pBits) return; // Ensure we have a valid pointer.

    int pixel_index = (y * width + x) * 3; // 3 bytes per pixel for RGB.
    
    uint8_t* pPixel = static_cast<uint8_t*>(pBits) + pixel_index;

    pPixel[0] = blue;
    pPixel[1] = green;
    pPixel[2] = red;
}

Let's begin with InitializeOffScreenDC(), which, if you recall, we called right after CreateAndRegisterWindow. This is where we will be loading the image data and generating our bitmap Windows object. When we mention a Windows object, we mean creating a representation of the image data that is compatible with how Windows handles image data for display on the screen, using Windows built-in native calls such as BitBlt, which we discussed in the previous section.

This process is not overly complex. We will use the .ppm, pr .pbm format, which we introduced in the lesson on images. As a quick reminder, ppm or pbm stands for Portable Pixmap or Bitmap Format (not to be confused with the BMP format, which is Windows' native bitmap format. More precisely, it's an image format used to store raster graphics, that is, images made out of pixels. It is the simplest image format in the world, and probably the entire universe. It comes in two flavors: ASCII and binary. In our case, we will use an image whose data are stored in the binary version of the file format. Again, check the lesson on images if you need a refresher or introduction to this file format. The image data are stored as char, so each pixel's values are stored as three chars for the red, green, and blue channels, respectively, with values in the range [0..255].

Then we pass the raw image data to a function called CreateBitmapFromRGB. This is where we create a Windows-compatible bitmap object using the CreateDIBSection Windows native API call. DIB stands for device-independent bitmap. It explains it all. That function returns two things: the bitmap itself, and a void pointer to the bitmap image data. We will use this pointer in our custom SetPixelColor function to write into the image at the mouse pixel positions.

The only thing you need to know about our implementation is the use of the hdcOffscreen object. It's a device-compatible graphics object associated with our device (our window, to put it simply). We tie our bitmap to that object with SelectObject(hdcOffscreen, hBitmap). The reason we do that is so we can "paint" into the bitmap off-screen and only decide to present it to the screen during a WM_PAINT event. This is sometimes referred to as double-buffering, but the use of the term here is misleading because generally, double-buffering is associated with a method used in real-time graphics whereas here, we can work with more than one image at a time, generally two, sometimes three. We draw into image 1, then present image 1 to the screen. While image 1 is on the screen, we draw into image 2, and when drawing is complete, we then present image 2 to the screen, which allows us to return to drawing in image 1, and so on. If we were drawing into an image at the same time it's being presented to the screen, this might create visual artifacts. Double-buffering eliminates this problem. However, what we are doing here is not quite the same, yet you might see it referred to in the literature as double-buffering, which, again, is incorrect. We prefer to refer to it simply as a form of off-screen rendering or drawing.

The SetPixelColor function is pretty straightforward. We call it when we receive a WM_MOUSEMOVE event. We draw a red pixel into the image upon receiving such an event at the current mouse position.

Compile

The complete source code and a sample image can be found on the GitHub repository. Note that creating your own pbm image is straightforward. You can use Gimp or Photoshop for this task.

To compile, open a GitBash terminal and enter the following command:

clang++ -std=c++23 -luser32 -lgdi32 -o window.exe window.cc

That's all there is to it. The user32 and gdi32 are two Windows libraries involved in the windowing system. We encourage you to do your own research on these libraries if you're interested.

Start the app. You should see the image of a corgi. And now you can paint on the image with the mouse left button.

Comments

Conclusion

As you can see, there's really nothing magical about Windows. It's quite straightforward to both display an image in a window and handle all kinds of events, as long as you have someone to explain it to you. Thanks, Scratchapixel.

This will constitute the foundation of our next lesson, in which we'll learn how to use mouse and keyboard events to control the motion of a 3D camera, allowing us to move through the scene. Exciting stuff! See you in the next lesson.

previous-