How To Write A Desktop Program In C++ Using Win32 And Com APIs - C++ Programming Tutorial
C++ Course / C++ Programs / How To Write A Desktop Program In C++ Using Win32 And Com APIs

How To Write A Desktop Program In C++ Using Win32 And Com APIs

BLUF: Mastering How To Write A Desktop Program In C++ Using Win32 And Com APIs is a critical step in becoming a proficient C++ developer. This lesson provides a deep dive into the syntax, performance considerations, and real-world applications of this concept.
Key Performance Insight: How To Write A Desktop Program In C++ Using Win32 And Com APIs

C++ is renowned for its efficiency. Learn how How To Write A Desktop Program In C++ Using Win32 And Com APIs enables low-level control and high-performance computing in the tutorial below.

By the conclusion of this series, you will possess the abilities to develop your own desktop applications starting from the beginning. Let's commence this fascinating voyage towards building a desktop application using C++.

Intro to Win32 Programming:

Creating Windows applications in C++ through Win32 programming involves utilizing the Win32 API, which consists of a fundamental collection of functions and structures offered by the Microsoft Windows OS. Below is a simple overview to help kickstart your journey:

Setting Up the Development Environment:

It would help if you have an IDE that supports C++ (i.e., Visual Studio) and the Windows SDK. Visual Studio provides a comprehensive environment for Win32 development, including project templates, debugging tools and several other features.

  • Download the Visual Studio Community 2019 or 2022:
  • If you already have Visual Studio, you can update it with the latest version.
  • During installation, select the "Desktop development with C++" and ".Net desktop development" options.
  • After that, from the optional list in the Installation section, select the option C++/CLI support for v143 build tools.
  • Then, choose Download all, then install from the dropdown menu.
  • Next, install them and launch the Visual Studio.
  • Understanding the Win32 API:

The Win32 API offers features for generating windows, managing messages, rendering graphics, and interacting with system components. It is crucial to acquaint yourself with the official documentation available on the Microsoft Developer Network (MSDN) platform.

Windows Coding Conventions

Most Windows APIs are comprised of functions or Component Object Model (COM) components.

Design (COM) interfaces. There are limited Windows APIs available in the form of C++ classes. (An important instance being GDI+, which is a 2-D graphics API.)

Typedefs

Integer types

Data Type Size Signed
BYTE 8 bits Unsigned
DWORD 32 bits Unsigned
INT32 32 bits Signed
INT64 64 bits Signed
LONG 32 bits Signed
LONGLONG 64 bits Signed
UINT32 32 bits Unsigned
UINT64 64 bits Unsigned
ULONG 32 bits Unsigned
ULONGLONG 64 bits Unsigned
WORD 16 bits Unsigned

What are Signed and Unsigned Keywords?

Signed variables can hold both positive and negative integers, including zero. By default, integers in C++ are signed. So, instead of explicitly using signed int, you can directly use int. For example:

  • signed int a = 14;
  • signed int b = -5;
  • signed int c = 0;
  • Boolean Type

BOOL serves as a type alias for integer, distinct from C++'s bool type and from various other types that signify a Boolean value. Within the header file WinDef.h, two constants are also defined for utilization with BOOL:

  • FALSE with a value of 0 and
  • TRUE with a value of 1.

Despite this explanation of TRUE, the majority of functions that yield a BOOL type have the capability to return any non-zero value to signify Boolean truth. Hence, it is advisable to consistently compose:

Example

Example

// Right way.
if (SomeFunctionThatReturnsBoolean()) { ... } // or if (SomeFunctionThatReturnsBoolean() != FALSE) { ... }

Pointer Types

Windows introduces various data types in the form of pointer-to-X. These are commonly named with prefixes like P- or LP-. An SQ represents a structure defining a square, where an LPSQ, for example, points to an SQ. The declarations of variables that ensue can be used interchangeably.

On 16-bit systems, there are distinctions between the P denoting "pointer" and LP denoting "long pointer." Long pointers, commonly known as far pointers, were essential for reaching memory areas beyond the current segment. Retaining the LP prefix aids in transitioning 16-bit code to 32-bit Windows. Currently, both pointer types are functionally equivalent. When selecting a prefix, opt for P over the alternate options.

Example

Example

SQ* sq; // Pointer to a SQ structure.
LPSQ sq; // The same
PSQ sq; // Also the same.

Explanation

In the previous instance, three distinct pointers have been defined, all indicating the same 'SQ' type. The 'SQ' type is implied to be either a struct or a type defined elsewhere in the code. These declarations exhibit varied syntaxes for the identical type: 'SQ*', 'LPSQ', and 'PSQ'. Usually, 'LPSQ' and 'PSQ' are employed as typedefs for pointers in Windows development, with 'LP' representing "Long Pointer" and 'P' representing "Pointer".

Pointer Precision Types

The following data types are always the same size as a pointer: in 32-bit programs, they are 32-bit wide, and in 64-bit applications, they are 64-bit large. The size is decided at the time of compilation. These data types remain 4 bytes wide while running 32-bit applications on 64-bit Windows.

  • INT_PTR
  • ULONG_PTR
  • DWORD_PTR
  • LONG_PTR
  • UINT_PTR

The aforementioned data types are employed in scenarios where an integer could potentially be converted to a pointer. They are also utilized for declaring variables for pointer arithmetic and for establishing loop counters that traverse the complete byte range in memory buffers. In a broader sense, they are present in cases where a current 32-bit value has been extended to 64 bits on a 64-bit Windows system.

What is Hungarian Notation?

Hungarian notation, a naming convention in programming, was first developed by Charles Simonyi during the 1970s. The term "Hungarian" in its name is a nod to the Hungarian heritage of its creator. This programming convention has undergone changes over the years, receiving both acclaim and scrutiny from the programming community.

In Hungarian notation, variable names include a prefix that indicates the data type of the variable. This prefix is typically a group of lowercase letters followed by an uppercase letter. Here are some examples of common prefixes:

  • int: Integer
  • str: String
  • bool: Boolean
  • flt: Floating point number
  • arr: Array
  • ptr: Pointer
  • hwnd: Handle to a window
  • hdc: Handle to a device context
  • lp: Long pointer (mostly used in WinAPI)

Example

  • intAge: An integer variable representing age.
  • strName: A string variable representing a name.
  • boolIsLoggedIn: A boolean variable indicating whether a user is logged in.

Hungarian notation offers clear insight into a variable's data type through its naming convention, enhancing code comprehension and reducing the likelihood of data type-related errors. Nevertheless, detractors contend that contemporary Integrated Development Environments (IDEs), syntax highlighting features, and robust type systems in programming languages have diminished the relevance of Hungarian notation, occasionally impeding code clarity.

Let's consider another example: the abbreviations i, cb, rw, and col represent index, row, and column values, respectively, and signify a byte size ("byte count"). The main objective of using these prefixes is to avoid accidental misuse of variables in inappropriate situations. For instance, if you were to come across the expression rwPosition + cbTable, it would indicate that a row number is being added to a byte size, highlighting a likely issue within the code.

Strings

Windows effortlessly accommodates Unicode strings for user interface components, filenames, and various other purposes. Unicode, encompassing all character sets and languages, stands out as the preferred character encoding method. In Windows, UTF-16 encoding is employed to portray Unicode characters, with each character being represented by one or two 16-bit integers. These UTF-16 characters are termed as wide characters to distinguish them from 8-bit ANSI characters. The Visual C++ compiler integrates support for wide character data types through the inherent wchar_t. Additionally, the following typedef is outlined in the WinNT.h header file.

Example

typedef wchar_t WCHAR;
wchar_t a = L'a';
wchar_t *str = L"hello";

Add an L before the literal to declare a wide-character or wide-character string literal.

Explanation

The code snippet above introduces an alias for a wide character data type called 'WCHAR', initializes a wide character variable 'a' with the value 'a', and creates a pointer 'str' that references the wide-character string "hello".

Unicode and ANSI Functions

To aid in the migration process, Microsoft provided two simultaneous collections of APIs when integrating Unicode compatibility into Windows: one for ANSI strings and the other for Unicode utilizing the wchart typedef as WCHAR; for example, wchart a = L'a'; wchar_t *str = L"hello"; Unicode and ANSI Functions strings. For example, there are dual methods available to personalize the text shown in the title bar of a window:

  • Inputting an ANSI string into SetWindowTextA.
  • Utilizing SetWindowTextW necessitates a Unicode string.

The ANSI variant internally converts the string to Unicode format. Moreover, a macro defined in the Windows headers points to the ANSI version if the Unicode option is not available, or to the Unicode version when the preprocessor symbol UNICODE is active. The ANSI variation converts the string internally to Unicode encoding. Furthermore, a macro defined in the Windows headers maps to the Unicode variant when the preprocessor symbol UNICODE is set, and to the ANSI variant otherwise.

Example

Example

#ifdef UNICODE
#define SetWindowText SetWindowTextW
#else
#define SetWindowText SetWindowTextA
#endif

Explanation

The code snippet above enables conditional compilation depending on the presence of the 'UNICODE' symbol. When 'UNICODE' is specified, it assigns an alias from 'SetWindowText' to 'SetWindowTextW', which represents the Unicode variant of the function designed for wide character strings. In the absence of 'UNICODE', it assigns an alias from 'SetWindowText' to 'SetWindowTextA', which is the ANSI version of the function tailored for narrow character strings. This mechanism facilitates the adjustment of the code to various character encoding specifications.

Even though it's the macro's name and not the actual method, SetWindowText is the documented function in MSDN. It's highly recommended for new software to always make use of the Unicode variants. Unicode support is crucial for accommodating diverse global languages. Localization efforts for your software may face obstacles if you rely on ANSI strings. Additionally, the ANSI counterparts are less efficient due to the runtime translation required by the operating system from ANSI to Unicode. You have the option to either employ the macros or directly utilize Unicode functions such as SetWindowTextW based on your preferences. Despite their equivalence, the sample code in MSDN typically demonstrates the use of macros. Most of the latest Windows APIs exclusively offer Unicode versions with the absence of ANSI counterparts.

TCHARS

When software needed to be compatible with Windows NT alongside Windows 95, Windows 98, and Windows Me, developers found it advantageous to write code that could handle both Unicode and ANSI strings depending on the target platform. In order to achieve this, the Windows SDK provides macros that can convert strings to either ANSI or Unicode format, as required by the specific platform.

Macro Unicode ANSI
TCHAR wchar_t char
TEXT("x") or _T("x") L"x" "x"
Example

SetWindowText(TEXT("My Application"));

Explanation

The code line mentioned above assigns the text on the window to "My Application". The 'TEXT' macro enables the code to adjust to various character encodings based on the presence or absence of the 'UNICODE' definition, utilizing either wide or narrow character strings.

Resolves to a single option:

Example

SetWindowTextW(L"My Application"); // Unicode function with wide-character
string.
SetWindowTextA("My Application"); // ANSI function.

Explanation

The code snippets above both assign the text on the window to "My Application." The initial line employs the 'SetWindowTextW' function, designed for wide-character strings (Unicode), whereas the subsequent line utilizes the 'SetWindowTextA' function, tailored for narrow-character strings (ANSI).

In modern programming, it is recommended to make use of Unicode in all software applications, thereby reducing the necessity of the TEXT and TCHAR macros. However, in legacy programs and certain MSDN code snippets, these macros may still be present. Within the headers of the Microsoft C run-time libraries, there exists a set of related macros. For example, when UNICODE is not defined, tcslen functions as strlen; when defined, it functions as wcslen, which is the wide-character version of strlen.

Example

#ifdef _UNICODE
#define _tcslen wcslen
#else
#define _tcslen strlen
#endif

Explanation

The provided code establishes 'tcslen' as a substitute for either 'wcslen' when dealing with Unicode or 'strlen' for non-Unicode scenarios, depending on the presence of the 'UNICODE' definition during the compilation process. This functionality facilitates the development of code that can effectively manage both Unicode and non-Unicode character encodings.

Note: While some headers use _UNICODE with an underscore prefix, others use the preprocessor symbol UNICODE. Make sure to declare both symbols at all times. When you start a new project in Visual C++, both are configured by default.

What is a Window?

Typically, a user interface window includes various visual components like buttons, input fields, images, and other interactive features that enable user engagement with the software. Windows operate autonomously, enabling users to manipulate them individually by moving, resizing, minimizing, maximizing, or closing them individually. This adaptability facilitates multitasking, allowing users to operate several applications concurrently. Each window commonly features a title bar positioned at the top, presenting the window's title, along with options to minimize, maximize, and close the window.

Windows generally come with boundaries that establish their limits and additional features like scrollbars, which are useful for displaying content that exceeds the window's visible area. Windows are often structured in a hierarchical manner in graphical user interface environments, establishing parent-child connections. For instance, a dialog box can be viewed as a subordinate window to the primary application window. Windows have the capability to accept and react to diverse forms of input, including mouse clicks, keystrokes, and system notifications. These interactions are commonly managed through an application's event or message loop mechanism.

What are Parent and Owner Windows?

A main window includes additional windows known as child windows. These child windows appear within the client area of the main window. A typical illustration of a main window is a dialog box. The client typically handles the creation and management of its child windows, governing their visual presentation, functionality, and state.

Master windows are essentially the idea of dialog boxes. Whenever a dialog box appears, it requires a master window, typically the primary application window or another top-level window. This master window functions as the display container and retains the ability to communicate with the application even while the dialog box remains open.

Window Handles

Windows is not a C++ class, but instead, it functions as an object containing both code and data. In contrast, a handle is a specific value utilized by a computer to point to a window. The window handle employs an opaque data type, essentially functioning as a numerical identifier assigned by the operating system to each individual window. Conceptualize Windows as maintaining a comprehensive table that stores information about every window ever created, allowing it to retrieve windows based on their respective handles. Window handles are represented using the HWND data type, commonly pronounced as "aitch-wind." The functions CreateWindow and CreateWindowEx generate windows and provide window handles as return values.

To perform an action on a window, it is typically necessary to invoke a function that requires an HWND parameter. For example, utilize the MoveWindow function to reposition a window on the display:

Example

Example

BOOL MoveWindow(HWND hwnd, int X int Y, int nWidth, int nHeight, BOOL bRepaint);

Explanation

The code snippet above relocates and adjusts the dimensions of a window identified by its handle (hwnd) to a designated position (X, Y) and size (nWidth, nHeight), providing the choice to refresh the window (bRepaint) if needed.

Screen and Window Coordinates

Screen coordinates pertain to the location and dimensions of elements (like windows, icons, or text) on the screen in relation to the complete display area. These coordinates are commonly denoted by a set of values (X, Y), where (0, 0) commonly denotes the upper-left corner of the screen.

Window coordinates, conversely, are in relation to the window's client region. They define the location and dimensions of components inside a particular window, usually denoted by a set of values (X, Y), where (0, 0) commonly denotes the upper-left corner of the window's client area.

The WinMain Application Entry Point

The WinMain function serves as the starting point for a Windows program developed in either C or C++. It acts as the counterpart to the main function in applications running in a console environment. Upon launching a Windows application, the operating system initiates the WinMain function and provides essential parameters like handles and command-line arguments.

Usually, the signature of the WinMain function appears as follows:

Example

Example

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

Explanation

In the provided code snippet, hInstance is responsible for managing the current instance of the application, while hPrevInstance is utilized for managing the previous instance. It's important to note that in Win32 programming, hPrevInstance is always NULL. lpCmdLine serves as a pointer to a null-terminated string that holds the command-line arguments passed to the program, excluding the program name. The nCmdShow parameter dictates how the window should be displayed, whether maximized, minimized, or shown in a normal state.

The function mentioned above will provide an integer value as its output. This resulting value can be used to communicate a status code to a different application, although it is not employed by the operating system.

A calling convention, such as WINAPI, defines the protocol for passing arguments from the calling function to another function. For example, it determines the order in which parameters are placed on the stack. Ensure to define your wWinMain function precisely as demonstrated in the example above.

The WinMain function, as opposed to wWinMain, accepts command-line arguments in ANSI format. While Unicode strings are the preferred choice, it is possible to employ the ANSI WinMain function even when compiling your program as Unicode. To access Unicode command-line arguments, you can make use of the GetCommandLine function, which provides all arguments in a unified string format. For an array structured like argv, you can convert this string into CommandLineToArgvW.

How does the compiler determine the suitability of using wWinMain instead of the standard main function? In practice, WinMain or wWinMain are invoked by a version of main that is included in the Microsoft C runtime library (CRT). Within this main function, the CRT carries out various operations. For example, it invokes wWinMain prior to any static initializers. It is recommended to stick with the default entry-point function when linking to the CRT, even if it is possible to specify an alternative one to the linker. Failure to include the CRT initialization code may lead to unexpected results, like improper initialization of global objects.

Here is the empty WinMain function:

Example

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
 PSTR lpCmdLine, int nCmdShow)
{
 return 0;
}

Explanation

The code snippet above specifies the WinMain function, which serves as the starting point for a Windows application. It accepts instance handles and command-line arguments, then exits without executing any specific tasks.

Creating the Window

To generate a Window, various procedures need to be followed, as outlined below in a sequential order. Take into account each of these actions to showcase the Window.

1. Registering the Window

In this part, you are required to specify a Window class WNDCLASS structure, encompassing all details regarding the Window Class and the Window Procedure.

2. Creating the Window

To generate the window, employ the CreateWindowEx function. Then, input all the details including the dimensions, position, parent window, menu handle, instance handle, class name, title, style, and any other applicable properties.

3. Handling the Messages

  • To manage the messages sent to th Window, create a window procedure (WindowProc).
  • In the window procedure, manage other messages, such as WMDESTROY, WMPAINT, etc.
  • To handle messages that the window procedure does not specifically process, utilize DefWindowProc.
  • 4. Run the Message Loop

Retrieve messages from the application's message queue by employing the GetMessage method. Subsequently, apply TranslateMessage and DispatchMessage to interpret and dispatch these messages. Finally, persist in the loop until a quit message (WM_QUIT) is not detected.

Example

#include

// Window Procedure
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_DESTROY:
            PostQuitMessage(0);
            return 0;
        case WM_PAINT:
            {
                PAINTSTRUCT ps;
                HDC hdc = BeginPaint(hwnd, &ps);
                FillRect(hdc, &ps.rcPaint, (HBRUSH)(COLOR_WINDOW + 1));
                EndPaint(hwnd, &ps);
            }
            return 0;
        default:
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    // Register Window Class
    const char* CLASS_NAME = "Sample Window Class";

    WNDCLASS wc = {};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClass(&wc);

    // Create Window
    HWND hwnd = CreateWindowEx(
        0,
        CLASS_NAME,
        "My Window",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        NULL,
        NULL,
        hInstance,
        NULL
    );

    if (hwnd == NULL) {
        return 0;
    }

    // Show Window
    ShowWindow(hwnd, nCmdShow);

    // Run Message Loop
    MSG msg = {};
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}

Explanation

In the previous code snippet, we've demonstrated a Windows application developed in C++ by leveraging the Win32 API. Following that, we have established a window program (WindowProc) responsible for managing tasks such as window scrolling and handling closure actions. The 'WinMain' function acts as the starting point, where it registers a window class within the operating system, generates a window associated with this class, showcases it, initiates a message loop to handle window-related activities. Subsequently, the message loop retrieves messages from the request queue, translates them, and directs the appropriate window to respond to the event.

Posted Messages and Sent Messages

  • When a message is posted, it enters the message queue and is dispatched through the message loop via functions like GetMessage and DispatchMessage.
  • Conversely, when a message is sent, it bypasses the queue entirely, and the operating system directly invokes the window procedure associated with the target window.
  • Writing Window Procedure

To create a window procedure in C++ with the Win32 API, you usually implement a function with the following format: LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam). Below is a demonstration illustrating the construction of a simple window procedure:

Example

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
        case WM_CREATE:
            // Handle window creation
            break;

        case WM_PAINT:
            // Handle painting the window
            break;

        case WM_DESTROY:
            // Handle window destruction
            PostQuitMessage(0);
            break;

        default:
            // Default message handling
            return DefWindowProc(hwnd, uMsg, wParam, lParam);
    }
    return 0;
}

Explanation

  • hwnd is the handle to the window.
  • uMsg is the message to be processed.
  • wParam and lParam are additional
  • Inside the switch statement, you handle different messages, such as WMCREATE for window creation, WMPAINT for painting the window, and WM_DESTROY for window destruction. For messages not explicitly handled, you typically call DefWindowProc to perform default message processing.

LRESULT denotes an integer value sent back by your program to Windows, indicating its reaction to a particular message. The significance of this value changes depending on the message code. CALLBACK defines the calling convention for the function. A prevalent window procedure consists of a substantial switch statement that shifts according to the message code. Every case in this statement caters to messages that you intend to manage.

A typical window procedure consists of a significant switch statement that assesses the message code. Within this statement, make sure to incorporate relevant cases for every type of message you plan to handle.

Example

switch (uMsg)
{
 case WM_SIZE: // Handle window resizing
 // etc
}

Default Message Handling:

Default message handling prioritizes establishing a method to address messages that are not explicitly managed within a window procedure. This is usually achieved by invoking the DefWindowProc function, which executes default operations for the unmanaged message. This guarantees that the window upholds its standard behavior for messages that are not individually handled by the application.

Example

return DefWindowProc(hwnd, uMsg, wParam, lParam);

Painting the Window

We've established the window; next, we aim to display content within it. In window context, content shown inside the window is referred to as Painting. Occasionally, your application will initiate painting to modify the window's look. Alternatively, the system may notify you to repaint a specific part of the window. In such cases, the system dispatches the WM_PAINT message to the window. This specific area of the window that requires repainting is identified as the update region.

When the Window becomes visible, it triggers the necessity to paint the Window's client area. As a result, each time a Window is shown, a single WMPAINT message is guaranteed. Once the client area is painted, the update region should be cleared. This action informs the operating system to refrain from dispatching another WMPAINT message unless modifications occur.

You are tasked with painting the client section exclusively, while the operating system takes care of painting the remaining frame and title bar components. Once the client area has been painted, clear the update region to signal the operating system to refrain from sending another WM_PAINT message unless there are any alterations.

Now, if the user switches to a different window, causing a portion of your window to be covered, when that opaque portion becomes visible again, it is included in the update region. Subsequently, the window will receive another WM_PAINT message.

If the user resizes the window, it will impact the update region as well. For instance, in the illustration provided, we demonstrate this by expanding the window towards the right side. Consequently, you will observe a corresponding adjustment in the update region.

Example

switch (uMsg)
{
 case WM_PAINT:

 {
 PAINTSTRUCT ps;
 HDC hdc = BeginPaint(hwnd, &ps);
 
 FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));

EndPaint(hwnd, &ps);
 }
 return 0;
}

Explanation

The provided code manages the Windows message 'WM_PAINT'. It initiates the painting process by acquiring a device context ('HDC') through 'BeginPaint', executes painting tasks (such as filling a rectangle with the background color of the window), and concludes the painting by utilizing 'EndPaint'. Ultimately, it returns '0' to signal successful processing of the message.

Initiate the painting process by invoking the BeginPaint function. This particular function populates the PAINSTRUCT structure with details regarding the repaint request. Within the PAINTSTRUCT, the rcPaint element specifies the existing update region, positioned in relation to the client area.

You have two choices for integrating into your paint code.

  • Rendering the entire client area disregarding the update region's size. Any content outside the update region will be clipped by the operating system.
  • Enhance performance by painting only a segment of the window contained within the update region.

The code snippet presented below populates the update section with a uniform color: the predefined background color of the system. The color dictated by COLOR_WINDOW is derived from the user's existing color palette.

Example

FillRect(hdc, &ps.rcPaint, (HBRUSH) (COLOR_WINDOW+1));

Explanation

The line of code above paints a rectangle defined by the 'rcPaint' field within the 'PAINTSTRUCT' structure ('ps') with the window background color. The '(HBRUSH) (COLOR_WINDOW+1)' segment specifies that the brush employed to fill the rectangle is the system's predefined window background color incremented by one.

Closing the Window

When a user decides to shut down the window, a series of window messages is activated in a specific order. The closure can be initiated either by selecting the close button or utilizing a shortcut method such as ALT+F4. By utilizing the WMCLOSE message, you can prompt the user for confirmation before proceeding with the window closure. If the decision is to indeed close the window, the DestroyWindow function should be employed. Conversely, if the intention is to maintain the window open, returning zero from the WMCLOSE message will result in the operating system ignoring the message, thereby preserving the window in its current state.

Here is an illustration demonstrating how a program can manage the WM_CLOSE function.

Example

case WM_CLOSE:
 if (MessageBox(hwnd, L"Really quit?", L"My application", MB_OKCANCEL) ==
IDOK)
 {
 DestroyWindow(hwnd);
 }
 // Else: User canceled. Do nothing.
 return 0;

Explanation

The provided code manages the WM_CLOSE message by triggering a message box that prompts the user to confirm if they intend to exit the application. Upon clicking OK, the window is terminated, while clicking Cancel results in no action. Subsequently, it returns 0 to signify that the message has been dealt with.

Usually, when managing the WM_DESTROY message within the primary application window, it is common practice to invoke the PostQuitMessage function.

Example

case WM_DESTROY:
 PostQuitMessage(0);
 return 0;

Explanation

The code snippet above manages the WM_DESTROY message by sending a quit message to the message queue with a parameter of 0, signifying a standard application shutdown. Afterward, it returns 0 to signal that the message has been dealt with. Typically, this action results in the conclusion of the application's message loop and ultimately the application shutdown.

Managing Application State

A window procedure acts as a callback function triggered for each message, inherently lacking persistent state. To address this limitation, a mechanism to track the application's state across function calls becomes essential. While using global variables is a straightforward approach suitable for smaller programs, it results in extensive use of global variables in larger applications. This can complicate management, especially when dealing with multiple windows and their respective procedures. Alternatively, the CreateWindowEx function provides a solution by enabling the passing of any data structure to a window. This is achieved by sending two messages,

  • WM_NCCREATE and
  • WM_CREATE, to the window procedure upon its invocation.

The WMNCCREATE and WMCREATE messages are sent out prior to the window being visible, making them ideal for handling UI setup operations like establishing the initial window design.

The last argument of CreateWindowEx is a void pointer, enabling the transfer of any pointer value. When processing the WMNCCREATE or WMCREATE message, the window procedure can retrieve this value from the message data.

Example

case WM_DESTROY:
 PostQuitMessage(0);
 return 0;

When invoking CreateWindowEx, provide a reference to this configuration in the last void* argument.

Example

StateInfo *pState = new (std::nothrow) StateInfo;
if (pState == NULL)
{
 return 0;
}

HWND hwnd = CreateWindowEx(
 0,
 CLASS_NAME,
 L"First Windows Program",
 WS_OVERLAPPEDWINDOW,
 
 CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
 NULL,
 NULL,
 hInstance,
 pState
 );

Explanation

Memory is assigned to the StateInfo structure in the code snippet above. If the allocation process is unsuccessful, the function should return 0. Subsequently, generate a window with defined attributes such as window class, text, style, dimensions, location, parent window, menu, instance handle, and extra application information.

After the WMNCCREATE and WMCREATE messages are received, the lParam argument in both messages contains a reference to a CREATESTRUCT data structure. Within this structure is the pointer that was initially provided to the CreateWindowEx function.

To retrieve the reference to your data structure, initiate the process by converting the lParam parameter to access the CREATESTRUCT construct.

Example

CREATESTRUCT *pCreate = reinterpret_cast<CREATESTRUCT*>(lParam);

Explanation

The provided code employs a reinterpret_cast to transform the lParam argument, representing a pointer, into a pointer of type CREATESTRUCT. This action allows access to the data contained within the CREATESTRUCT structure.

The lpCreateParams found within the CREATESTRUCT construct contains the initial void pointer provided in CreateWindowEx. To access a pointer to your personalized data structure, you can effortlessly perform a cast on lpCreateParams.

Example

pState = reinterpret_cast(pCreate->lpCreateParams);

Next, call the SetWindowLongPtr function and pass your data structure pointer as an argument.

Example

SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)pState);

Explanation

The code line above assigns a personalized pointer ('pState') to a window ('hwnd') in a Windows program. This enables the program to save and access its specific data associated with that window at a later time.

After completing this task, you can obtain the pointer from the window by utilizing the GetWindowLongPtr function.

Example

LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);

Explanation

The provided code snippet retrieves a custom pointer associated with a window by utilizing the 'GetWindowLongPtr' function, and then casts it as a 'StateInfo*' pointer for subsequent operations.

You have the ability to generate unique instance data for individual windows, allowing you to manage multiple windows, each with its own instance of a data structure. This approach proves beneficial when working with window classes like custom control classes, especially when you require multiple windows of identical types.

Example

inline StateInfo* GetAppState(HWND hwnd)
{
 LONG_PTR ptr = GetWindowLongPtr(hwnd, GWLP_USERDATA);
 StateInfo *pState = reinterpret_cast<StateInfo*>(ptr);
 return pState;
}

Explanation

In the provided code snippet, the 'GetAppState' function fetches a specific pointer linked to a window ('hwnd') by utilizing the 'GetWindowLongPtr' function. Subsequently, this pointer is transformed into a 'StateInfo*' pointer through the 'reinterpret_cast' operation before being returned as the result.

In the following section, we will explore the initial Windows application, where we will design a basic form to grasp the fundamentals of C++ Windows programming utilizing the Win32 API.

Module 1: First Windows Program (A Simple Form):

Here, we'll guide you through creating a simple Windows form program using the powerful Win32 APIs.

  • Open Visual Studio Community and click Create New Project.
  • In the Search box, search for clr, select CLR Empty Project (.NET Framework) and click Next.
  • Give your Project a name, select C++ language from the options and click on the Create button.
  • Select your project, go to the Project menu, and select the Properties option.
  • Expand the Linker menu, click System >> SubSystem and select Windows (/SUBSYSTEM:WINDOWS).
  • Next, click Advanced, type main in the Entry Point section, and click OK.
  • Click the Project menu again, select Add New Item >> UI >> Windows Form, and close the open section.
  • Next, right-click on the .h file and click on the View Designer option.
  • Modify or add functionality to the form according to your needs here.
  • Then, write and save the C++ code in a .cpp file.
  • Finally, click Local Windows Debugger to run the file and see the results.

Code (MyForm.cpp):

Example

#include "MyForm.h"
using namespace System;
using namespace System::Windows::Forms;
[STAThreadAttribute]

void main(array<String^>^ args) {
    Application::SetCompatibleTextRenderingDefault(false);
    Application::EnableVisualStyles();
    Project3::MyForm frm;
    Application::Run(% frm);
}

Explanation

The provided code initiates a Windows Forms program in C++, configuring essential properties and namespaces. This involves importing a file named "MyForm.h" into the form class. Afterwards, it assigns the [STAThread] attribute to the primary task, establishing a single-threaded build mode to ensure compatibility with Component Object Model (COM). By invoking the Application::EnableVisualStyles method, the application adopts visual styles, enhancing its appearance with a contemporary design.

Utilizing Application::SetCompatibleTextRenderingDefault(false) turns off compatible text rendering to ensure optimal font display quality. This function instantiates a form named MyForm and proceeds to execute the application, presenting the form to the user.

Code (MyForm.h):

Example

#pragma once

namespace Project3 {

    using namespace System;
    using namespace System::ComponentModel;
    using namespace System::Collections;
    using namespace System::Windows::Forms;
    using namespace System::Data;
    using namespace System::Drawing;

    public ref class MyForm : public System::Windows::Forms::Form
    {
    public:
        MyForm(void)
        {
            InitializeComponent();
           
        }

    protected:
       
        ~MyForm()
        {
            if (components)
            {
                delete components;
            }
        }
    private: System::Windows::Forms::Label^ label1;
    private: System::Windows::Forms::TextBox^ FN;

    protected:

    private: System::Windows::Forms::Label^ label2;
    private: System::Windows::Forms::TextBox^ MN;

    private: System::Windows::Forms::Label^ label3;
    private: System::Windows::Forms::TextBox^ LN;
    private: System::Windows::Forms::TableLayoutPanel^ tableLayoutPanel1;
    private: System::Windows::Forms::Button^ btOK;
    private: System::Windows::Forms::Button^ btClear;
    private: System::Windows::Forms::Label^ Welcome;

    private:
   
        System::ComponentModel::Container ^components;

#pragma region Windows Form Designer generated code
       
        void InitializeComponent(void)
        {
            this->label1 = (gcnew System::Windows::Forms::Label());
            this->FN = (gcnew System::Windows::Forms::TextBox());
            this->label2 = (gcnew System::Windows::Forms::Label());
            this->MN = (gcnew System::Windows::Forms::TextBox());
            this->label3 = (gcnew System::Windows::Forms::Label());
            this->LN = (gcnew System::Windows::Forms::TextBox());
            this->tableLayoutPanel1 = (gcnew System::Windows::Forms::TableLayoutPanel());
            this->btOK = (gcnew System::Windows::Forms::Button());
            this->btClear = (gcnew System::Windows::Forms::Button());
            this->Welcome = (gcnew System::Windows::Forms::Label());
            this->tableLayoutPanel1->SuspendLayout();
            this->SuspendLayout();
           
            this->label1->AutoSize = true;
            this->label1->Location = System::Drawing::Point(3, 9);
            this->label1->Name = L"label1";
            this->label1->Size = System::Drawing::Size(57, 13);
            this->label1->TabIndex = 0;
            this->label1->Text = L"First Name";
           
            this->FN->Anchor = static_cast<System::Windows::Forms::AnchorStyles>(((System::Windows::Forms::AnchorStyles::Top | System::Windows::Forms::AnchorStyles::Left)
                | System::Windows::Forms::AnchorStyles::Right));
            this->FN->Location = System::Drawing::Point(6, 25);
            this->FN->Name = L"FN";
            this->FN->Size = System::Drawing::Size(291, 20);
            this->FN->TabIndex = 1;
           
            this->label2->AutoSize = true;
            this->label2->Location = System::Drawing::Point(3, 58);
            this->label2->Name = L"label2";
            this->label2->Size = System::Drawing::Size(69, 13);
            this->label2->TabIndex = 2;
            this->label2->Text = L"Middle Name";
           
            this->MN->Anchor = static_cast<System::Windows::Forms::AnchorStyles>(((System::Windows::Forms::AnchorStyles::Top | System::Windows::Forms::AnchorStyles::Left)
                | System::Windows::Forms::AnchorStyles::Right));
            this->MN->Location = System::Drawing::Point(6, 74);
            this->MN->Name = L"MN";
            this->MN->Size = System::Drawing::Size(291, 20);
            this->MN->TabIndex = 3;
           
            this->label3->AutoSize = true;
            this->label3->Location = System::Drawing::Point(3, 113);
            this->label3->Name = L"label3";
            this->label3->Size = System::Drawing::Size(58, 13);
            this->label3->TabIndex = 4;
            this->label3->Text = L"Last Name";
           
            this->LN->Anchor = static_cast<System::Windows::Forms::AnchorStyles>(((System::Windows::Forms::AnchorStyles::Top | System::Windows::Forms::AnchorStyles::Left)
                | System::Windows::Forms::AnchorStyles::Right));
            this->LN->Location = System::Drawing::Point(6, 129);
            this->LN->Name = L"LN";
            this->LN->Size = System::Drawing::Size(291, 20);
            this->LN->TabIndex = 5;
           
            this->tableLayoutPanel1->Anchor = static_cast<System::Windows::Forms::AnchorStyles>(((System::Windows::Forms::AnchorStyles::Bottom | System::Windows::Forms::AnchorStyles::Left)
                | System::Windows::Forms::AnchorStyles::Right));
            this->tableLayoutPanel1->ColumnCount = 2;
            this->tableLayoutPanel1->ColumnStyles->Add((gcnew System::Windows::Forms::ColumnStyle(System::Windows::Forms::SizeType::Percent,
                49.21466F)));
            this->tableLayoutPanel1->ColumnStyles->Add((gcnew System::Windows::Forms::ColumnStyle(System::Windows::Forms::SizeType::Percent,
                50.78534F)));
            this->tableLayoutPanel1->Controls->Add(this->btOK, 0, 0);
            this->tableLayoutPanel1->Controls->Add(this->btClear, 1, 0);
            this->tableLayoutPanel1->Location = System::Drawing::Point(12, 207);
            this->tableLayoutPanel1->Name = L"tableLayoutPanel1";
            this->tableLayoutPanel1->RowCount = 1;
            this->tableLayoutPanel1->RowStyles->Add((gcnew System::Windows::Forms::RowStyle(System::Windows::Forms::SizeType::Percent, 50)));
            this->tableLayoutPanel1->Size = System::Drawing::Size(285, 42);
            this->tableLayoutPanel1->TabIndex = 6;
           
            this->btOK->Anchor = static_cast<System::Windows::Forms::AnchorStyles>(((System::Windows::Forms::AnchorStyles::Top | System::Windows::Forms::AnchorStyles::Left)
                | System::Windows::Forms::AnchorStyles::Right));
            this->btOK->Location = System::Drawing::Point(3, 3);
            this->btOK->Name = L"btOK";
            this->btOK->Size = System::Drawing::Size(134, 36);
            this->btOK->TabIndex = 0;
            this->btOK->Text = L"OK";
            this->btOK->UseVisualStyleBackColor = true;
           
            this->btClear->Anchor = static_cast<System::Windows::Forms::AnchorStyles>(((System::Windows::Forms::AnchorStyles::Top | System::Windows::Forms::AnchorStyles::Left)
                | System::Windows::Forms::AnchorStyles::Right));
            this->btClear->Location = System::Drawing::Point(143, 3);
            this->btClear->Name = L"btClear";
            this->btClear->Size = System::Drawing::Size(139, 36);
            this->btClear->TabIndex = 1;
            this->btClear->Text = L"Clear";
            this->btClear->UseVisualStyleBackColor = true;
           
            this->Welcome->AutoSize = true;
            this->Welcome->Location = System::Drawing::Point(9, 166);
            this->Welcome->Name = L"Welcome";
            this->Welcome->Size = System::Drawing::Size(52, 13);
            this->Welcome->TabIndex = 7;
            this->Welcome->Text = L"Welcome";
           
            this->AutoScaleDimensions = System::Drawing::SizeF(6, 13);
            this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font;
            this->BackColor = System::Drawing::Color::Silver;
            this->ClientSize = System::Drawing::Size(309, 261);
            this->Controls->Add(this->Welcome);
            this->Controls->Add(this->tableLayoutPanel1);
            this->Controls->Add(this->LN);
            this->Controls->Add(this->label3);
            this->Controls->Add(this->MN);
            this->Controls->Add(this->label2);
            this->Controls->Add(this->FN);
            this->Controls->Add(this->label1);
            this->MinimumSize = System::Drawing::Size(325, 300);
            this->Name = L"MyForm";
            this->Text = L"MyForm";
            this->Load += gcnew System::EventHandler(this, &MyForm::MyForm_Load);
            this->tableLayoutPanel1->ResumeLayout(false);
            this->ResumeLayout(false);
            this->PerformLayout();

        }
#pragma endregion
    private: System::Void MyForm_Load(System::Object^ sender, System::EventArgs^ e) {
        String^ firstName = this->FN->Text;
        String^ middleName = this->MN->Text;
        String^ lastName = this->LN->Text;
        this->Welcome->Text = "Hello " + firstName + " " + middleName + "" + lastName;
    }
    };
}

Output (MyForm.h):

Explanation

The provided code snippet illustrates a Windows Forms program in C++/CLI within Visual Studio. A form named MyForm was generated, incorporating text boxes for the first name, middle name, and last name, along with buttons for executing OK and Delete actions. Utilizing a table layout panel facilitated the arrangement of these elements. Upon loading the form, a designated function retrieves data from the input text boxes, combines it, and showcases a greeting message on a label within the form. Furthermore, the code encompasses setup and event management logic tailored for the controls within the form, encompassing tasks like configuring their attributes and specifying their functionality.

In the upcoming section, we will explore the utilization of COM (Component Object Model) within Windows applications to develop reusable software elements.

Module 2: Using COM in Your Windows Program:

COM stands for Component Object Model, a standard for developing reusable software elements. Various functionalities in modern Windows applications heavily rely on COM, such as:

-

  • Graphics (e.g., Direct2D): By utilizing interfaces like ID2D1Factory, ID2D1RenderTarget, and ID2D1PathGeometry, you can generate shapes, render images, and implement transformations.

-

  • Text Rendering (with DirectWrite): DirectWrite, an API based on COM, is essential for text rendering tasks in your software. Employ the IDWriteFactory interface to craft text layouts, format textual content, and manage font-related operations.

Here are the terms related to User Interface design and development:

  • Windows Shell: The Windows Shell is the user interface for the Microsoft Windows operating system. It encompasses various elements such as the taskbar, desktop, Start menu, and file explorer. Essentially, it's what users interact with when using Windows.
  • Ribbon Control: The Ribbon Control is a user interface element commonly used in software applications, particularly in productivity software like Microsoft Office. It's characterized by a horizontal strip that contains tabs, each with groups of related commands. Ribbon Control aims to provide a more organized and visually appealing way to access application features and functions.
  • UI Animation: UI Animation refers to the use of motion and animation within user interfaces to enhance usability, provide feedback, and create engaging interactions. UI animations can range from simple transitions between screens or elements to more complex animated effects that guide users' attention or simulate real-world interactions. They are an integral part of modern user interface design, contributing to both aesthetics and functionality.
  • Note: It must recognize that COM is a binary standard, not tied to any specific programming language. The COM defines the binary interface between an application and a software component.

    Key Goals of COM include:

Some of the main key aspects of COM are furnishing here:

  • Separating an object's implementation from its interface.
  • Managing object lifetimes.
  • Discovering object capabilities at runtime.
  • Note: In order to facilitate Object Linking and Embedding (OLE) 2.0, COM was first created in 1993. OLE 2.0 is built on COM, but you don't need to know OLE to understand COM.

For instance, consider if you wish to generate an Open Dialog Box within your application. COM provides a seamless way to accomplish this task.

Understanding the COM Interface

An interface defines a set of functions that an object can offer, concealing specifics about the inner workings of these functions. It acts as a clear boundary between the code calling a function and the code handling its execution. This principle in computer science is known as decoupling, which ensures that the user of a function is isolated from its specific implementation logic.

In C++, a pure abstract class functions as the nearest equivalent to an interface. This type of class consists solely of pure virtual functions without any additional members. Here is a demonstration of a theoretical interface in C++:

Example

// The following is not actual COM.
// pseudo-C++:
interface IDrawable
{
 void Draw();
};

Explanation

The code snippet presented above illustrates a theoretical depiction of an interface called 'IDrawable' using pseudo-C++ syntax. It outlines a solitary function, 'Draw', lacking details on how it should be executed. This interface enables objects to be handled uniformly if they possess the shared capability of being drawn; however, it's crucial to understand that this code is not genuine COM.

Since all interfaces are abstract in nature, it is impossible for a program to directly create an object of type IDrawable. Consequently, the provided code excerpt would encounter a compilation error.

Example

IDrawable draw;
draw.Draw();

Instead, the graphics library provides entities that adhere to the IDrawable interface. For example, the library might supply a shape entity designed for sketching geometric figures and a bitmap entity focused on displaying images. In C++, this is achieved by inheriting from a common abstract base class:

Example

class Shape : public IDrawable
{

public:
 virtual void Draw();
};
class Bitmap : public IDrawable
{
public:
 virtual void Draw();
};
Explanation
In the above code, two classes, 'Shape' and 'Bitmap', inherited from the 'IDrawable' interface, each providing its implementation for the 'Draw()' function.
A program utilizing this graphics library would manipulate 'Shape' and 'Bitmap' objects via 'IDrawable' pointers rather than directly using pointers to 'Shape' or 'Bitmap'.
Code
IDrawable *pDrawable = CreateTriangleShape();
if (pDrawable)
{
 pDrawable->Draw();
}

Explanation

In the provided code snippet, a pointer named 'pDrawable' with the data type 'IDrawable' is initialized with the memory address of an instance of a triangle shape generated by invoking the 'CreateTriangleShape' method. When the 'pDrawable' pointer is not equal to null, the 'Draw' method of the triangle shape instance is executed.

Explaining the process of looping through an array of pointers to objects that implement the 'IDrawable' interface. This array has the flexibility to contain different types of graphical elements such as shapes, images, or other visual entities, as long as they are derived from the 'IDrawable' interface.

Example

void DrawSomeShapes(IDrawable **drawableArray, size_t count)
{
for (size_t i = 0; i < count; i++)
 {
 drawableArray[i]->Draw();
 }
}

Explanation

The function 'DrawSomeShapes' loops over a collection of 'IDrawable' pointers and invokes the 'Draw' method on each individual object.

Note: The code provided in this document is not a real-world example. It illustrates general concepts. Creating new COM (Component Object Model) interfaces extends beyond the scope of this series. Typically, a COM interface isn't directly defined in a header file. Instead, it's defined using Interface Definition Language (IDL). Subsequently, the IDL file is processed by an IDL compiler, which generates a C++ header file.

Example

Example

class IDrawable
{
public:
 virtual void Draw() = 0;
};

When dealing with COM, it's crucial to understand that interfaces are not standalone objects; instead, they consist of a collection of methods that objects need to follow. It's possible for several objects to conform to the same interface, illustrated by the Shape and Bitmap instances. Moreover, a single object can conform to multiple interfaces. For example, a graphics library could define an interface like ISerializable to aid in the storage and retrieval of graphic elements. Below are the class declarations:

Example

// An interface for serialization.
class ISerializable
{
public:
virtual void Load(PCWSTR filename) = 0;
 virtual void Save(PCWSTR filename) = 0;
};
// Declarations of drawable object types.
class Shape : public IDrawable
{
 ...
};
class Bitmap : public IDrawable, public ISerializable
{
 ...
};

Explanation

The preceding code establishes an interface named 'ISerializable' that contains functions for loading and storing objects in files. It also introduces two types of drawable objects: 'Shape', which is a subclass of 'IDrawable', and 'Bitmap', which is a subclass of both 'IDrawable' and 'ISerializable'.

In this segment, we have provided a comprehensive overview of the interfaces. Moving forward, we will proceed to the following section, focusing on the COM library.

Initializing the COM Library

Starting up the COM (Component Object Model) library is commonly achieved by invoking the CoInitializeEx function. This particular function kicks off the COM library within the current apartment and thread. Below is a basic illustration of how you can initialize the COM library:

Example

#include <Windows.h>

  int main() {
      // Initialize the COM library
      HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
      if (FAILED(hr)) {
          // Handle initialization failure
          return 1;
      }
 
      //The COM library has been initialized, so continue with your program...
 
      // Uninitialize the COM library when done
      CoUninitialize();
 
      return 0;
  }

Explanation

In the provided code snippet, the CoInitializeEx function is invoked with a NULL value as the initial argument, indicating the initialization of the COM library for the ongoing apartment and thread. The subsequent parameter defines the concurrency model intended for this specific thread. Typically, COINIT_APARTMENTTHREADED is employed in desktop software applications.

When opting for apartment threading in COM, you commit to the following assurances:

  • Each COM object access will be confined to a singular thread, ensuring that COM interface pointers remain consistent across threads.
  • Moreover, the thread will encompass a message loop for handling communication.

If none of the conditions mentioned above are met, consider selecting a multithreaded framework. To specify a threading model, utilize one of the flags available in the dwColnit parameter.

Flag Description
COINIT_APARTMENTTHREADED Apartment threaded
COINIT_MUTITHREADED Multithreaded

Make sure you place one of these flags correctly. Normally, threads that control the window should use the 'COINITAPARTMENTTHREADED' flag; other threads should use 'COINITMULTITHREADED'. However, some COM components may require a specific threading model. Check the MSDN documentation for such requirements.

Note: In fact, even if indoor wiring is specified, it is still possible to share wiring interfaces using a technique called marshalling. However, the complexities of marshalling are not covered in this module. What is important to understand is that when using apartment threading, it is important to never simply copy the interface pointer to another thread.

Incorporating the flags mentioned earlier, it is advisable to include the COINITDISABLEOLE1DDE flag within the dwCoInit parameter. This particular flag assists in mitigating certain inefficiencies linked to the outdated Object Linking and Embedding (OLE) 1.0 technology. Here is the initial setup of COM with apartment threading:

Example

CoInitializeEx(NULL, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE);

Uninitialize the COM Library

To deinitialize a Component Object Model (COM) library in C++, you have the option to employ the CoUninitialize function. This particular function is responsible for releasing resources associated with COM objects and should be invoked in correspondence with each instance of a successful CoInitialize or CoInitializeEx invocation. Below outlines the process for executing this task:

Example

// Uninitialize the COM library
CoUninitialize();

Error Codes in COM

In Component Object Model (COM), an error code is symbolized by an HRESULT value, which is a 32-bit value encompassing the error code alongside additional details regarding the error. Normally, a COM function will provide an HRESULT value as a response to signify either the success or failure of an internal operation.

The crucial aspect of the HRESULT is responsible for indicating the success or failure of an operation. If the high-order bits contain zero (0), it signifies success, whereas a value of one (1) indicates failure.

It produces the subsequent numerical intervals:

  • Successful codes: 0x0-0x7FFFFFFF.
  • Failure codes: 0x80000000-0xFFFFFFFF.

Most Component Object Model (COM) functions typically use an HRESULT value to signal whether the operation was successful or not. However, certain functions like AddRef and Release may return uninitialised long values instead. To ascertain the success of a COM method, one can analyze the most significant bit of the returned HRESULT value. Fortunately, the Windows Software Development Kit header offers two macros - SUCCEEDED and FAILED - for this purpose. The SUCCEEDED macro evaluates to TRUE if the HRESULT signifies a successful outcome and FALSE if it indicates an error state. The following example demonstrates the application of the SUCCEEDED macro to validate the success of the CoInitializeEx function.

Example

HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
 COINIT_DISABLE_OLE1DDE);
if (SUCCEEDED(hr))
{
 // The function succeeded.
}
else
{
 // Handle the error.
}

Explanation

The preceding code snippet initializes the Component Object Model (COM) library with apartment threading and activates the Object Linking and Embedding (OLE) 1.0 Dynamic Data Exchange (DDE). Subsequently, it employs the SUCCEEDED macro to verify the success of the initialization. In the event of a successful initialization, the code proceeds with the subsequent steps; otherwise, it addresses any encountered errors.

At times, it can be more straightforward to verify the negative scenario. The FAILED macro functions as the opposite of SUCCEEDED: it evaluates as TRUE for an error code and as FALSE for a successful code.

Example

HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
 COINIT_DISABLE_OLE1DDE);

if (FAILED(hr))
{
 // Handle the error.
}
else
{
 // The function succeeded.
}

Explanation

The aforementioned code sets up the Component Object Model (COM) library with specific configurations. Subsequently, it verifies the success of the initialization using the FAILED macro. In case of failure, it manages the error; otherwise, it continues executing the function.

Creating an Object in COM

In COM (Component Object Model), objects are commonly instantiated using the CoCreateInstance function. This method is responsible for generating an instance of a designated COM class and providing a reference to its interface. Below is a fundamental illustration demonstrating the process of object creation in COM:

Generally, you have two approaches to instantiate a COM object; let's explore them:

  • One method involves the module providing a specific creation function for the object.
  • Alternatively, COM provides a universal creation function known as CoCreateInstance.

Let's consider a scenario involving the fictional Shape entity. In this instance, the Shape entity incorporates an interface called IDrawable. The graphical library that integrates the Shape entity could provide a function with the subsequent declaration.

Example

// the below code is not an actual Windows function.
HRESULT CreateShape(IDrawable** ppShape);

By the way, a new Shape object can be instantiated in the following manner:

Example

IDrawable *pShape;
HRESULT hr = CreateShape(&pShape);
if (SUCCEEDED(hr))
{
 // Use the Shape object.
}
else
{
 // An error occurred.
}

Explanation

The code snippet above generates an instance of a Shape object by invoking the CreateShape function, which yields an HRESULT upon execution. In case the object creation is successful, the subsequent step involves employing the Shape object; otherwise, it manages the encountered error gracefully.

The ppShape parameter is a double pointer to an object of type IDrawable. The function is required to provide an IDrawable pointer to the caller. However, the function's return value is reserved for error/success indication. Therefore, the pointer needs to be returned via a function argument. The caller will supply a variable of type IDrawable to the function, which will be updated with a new IDrawable pointer. In C++, functions can modify parameter values using pass-by-reference or pass-by-address. In COM, the latter approach, pass-by-address, is employed. As the address of the pointer is required, the parameter type should be IDrawable*.

Here is a visual representation to grasp the process:

The CreateShape method employs the address-of operator (&) on pShape (&pShape) to modify the pShape value with a fresh pointer.

Creating Objects Using CoCreateInstance

The CoCreateInstance function provides a universal approach to creating objects. To grasp CoCreateInstance, it is essential to acknowledge that two COM objects can support the same interface, and a single object can support multiple interfaces. Hence, for a general object creation function, two key details are essential:

  • The specific object to instantiate.
  • The particular interface to retrieve from the object.

In Component Object Model (COM), objects and interfaces are differentiated through the allocation of a 128-bit identifier termed as a globally unique identifier (GUID). GUIDs are created to guarantee their distinctiveness without the need for a central registering body. They are also known as universally unique identifiers (UUIDs) and were initially employed in DCE/RPC (Distributed Computing Environment/Remote Procedure Call) prior to their integration into COM. There are multiple algorithms accessible for the creation of fresh GUIDs.

While there is no guarantee of absolute uniqueness with all algorithms, the likelihood of generating the same GUID value twice is extremely minimal and practically insignificant. GUIDs have the capability to uniquely identify a range of entities other than just objects and interfaces; however, our current emphasis within this section is solely on their application in this particular scenario. To illustrate, the Shapes library might define a pair of GUID constants.

Example

Example

extern const GUID CLSID_Shape;
extern const GUID IID_IDrawable;

The CLSIDShape constant symbolizes the Shape object, whereas the IIDIDrawable constant symbolizes the IDrawable interface. The prefixes "CLSID" and "IID" are indicative of class identifiers and interface identifiers within the COM naming conventions. When creating a new instance of a Shape object, you would follow these steps:

Example

IDrawable *pShape;
hr = CoCreateInstance(CLSID_Shape, NULL, CLSCTX_INPROC_SERVER,
IID_IDrawable,
 reinterpret_cast<void**>(&pShape));
if (SUCCEEDED(hr))
{
 // Use the Shape object.
}
else
{
 // An error occurred.
}

Explanation

In the provided code snippet, a fresh instance of a Shape object was instantiated, and its IDrawable interface pointer was acquired. Whenever feasible, utilize the Shape object; otherwise, rectify the issue.

The coCreateInstance method includes five arguments. The initial and fourth arguments denote the identity of the class and the interface, correspondingly. These arguments guide the user to "instantiate a Shape object and supply a reference to the IDrawable interface."

Specify a value of NULL for the second argument. The third parameter serves as a collection of indicators that define the operational environment of the entity. It determines whether the entity operates within the application's current process, within a designated process on the local machine, or on a remote computer. The subsequent table outlines the predominant options for this parameter, illustrating the various scenarios.

Flag Parameter
CLSCTXINPROCSERVER Same process
CLSCTXLOCALSERVER Different process, same computer
CLSCTXREMOTESERVER Different computer
CLSCTX_ALL/ Use the most efficient option that the object supports.

The fifth CoCreateInstance parameter accepts a pointer to the interface. Due to the generic nature of CoCreateInstance, it does not have a strong parameter. As a result, its data type is void, and the caller must set the address of the pointer to the void type. This explains how reinterpret_cast was used in the previous example.

It is crucial to verify the return value of CoCreateInstance. When the function returns an error code, the COM interface pointer becomes invalid, and attempting to dereference it can lead to a program crash.

CoCreateInstance employs a range of internal techniques for object creation. In its most basic implementation, it checks the class identifier in the registry to determine the DLL or EXE associated with the object. Moreover, CoCreateInstance can leverage details from a COM+ catalog or a side-by-side (SxS) display. Nevertheless, this operational data remains abstract to the requester. For further understanding of the internal mechanisms of CoCreateInstance, refer to the documentation on COM Clients and Servers.

Let's proceed to the following section to delve into a practical scenario, where we elaborate on Component Object Model (COM) extensively along with a real-world issue.

The Open Dialogue Box: A Real-World Example

To showcase the Open dialog window, the application can employ a Component Object Model (COM) entity known as the Common Item Dialog. This component acknowledges the designated interface IFileOpenDialog, which is defined in the Shobjidl.h header file.

Below is a code snippet illustrating how to display the Open dialog box to the user. Upon choosing a file, the program prompts a dialog box showcasing the selected file's name.

Example

#include <windows.h>
  #include <shobjidl.h>
  int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int
  nCmdShow)
  {
   HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
   COINIT_DISABLE_OLE1DDE);
   if (SUCCEEDED(hr))
   {
   IFileOpenDialog *pFileOpen;
  // Create the FileOpenDialog object.
   hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL,
   IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));
   if (SUCCEEDED(hr))
   {
   // Show the Open dialog box.
   hr = pFileOpen->Show(NULL);
 
   if (SUCCEEDED(hr))
   {
   IShellItem *pItem;
   hr = pFileOpen->GetResult(&pItem);
   if (SUCCEEDED(hr))
   {
   PWSTR pszFilePath;
   hr = pItem->GetDisplayName(SIGDN_FILESYSPATH,
  &pszFilePath);
   // Display the file name to the user.
   if (SUCCEEDED(hr))
   {
   MessageBoxW(NULL, pszFilePath, L"File Path", MB_OK);
   CoTaskMemFree(pszFilePath);
   }
   pItem->Release();
   }
   }
   pFileOpen->Release();
   }
   CoUninitialize();
   }
   return 0;
  }

Explanation

The provided code sets up the Component Object Model (COM) and creates an instance of the IFileOpenDialog interface to showcase the Open dialog box within a Windows program. It acquires the chosen file path from the dialog, presents a message box to the user, and then frees up the utilized resources. The wWinMain method serves as the starting point for the application, guaranteeing correct setup and management of COM objects. This example illustrates the integration of Windows API functions with COM interfaces to enable file selection and user engagement within a graphical user interface (GUI) application.

Handling the Lifetime of an Object

Each COM interface must inherit directly or indirectly from an IUnknown interface. This interface establishes the basic capabilities that all COM objects must have. In particular, the IUnknown interface contains three methods:

  • QueryInterface
  • AddRef
  • Release

The QueryInterface function allows a software to inquire about an object's functionalities dynamically. Additional information will be presented in the next section, "Requesting an Interface from an Object." Currently, the AddRef and Release functions are employed to manage the lifespan of the object, which is the main subject of this conversation.

What is Reference Counting?

Each Component Object Model (COM) instance manages an internal tally known as the reference count, governing the quantity of live references to the instance. Once the reference count hits zero, signaling the absence of active references, the instance releases itself. It is crucial to highlight that the instance manages its own destruction, and the application does not directly delete the instance.

Below are the rules for reference counting:

  • After creation, the object's reference number starts at 1, which refers to its orientation.
  • New references can be created by creating (copying) the pointer.
  • When duplicating a pointer, the object needs to call the AddRef method, which increments the reference count by one.
  • It is necessary to call the Release method after the objeccpp tutorialer has been used. This action reduces the number of references by one and invalidates the pointer. Avoid using a pointer when calling Release. (If there are other signs for the same thing, they remain valid.)
  • By calling Release for any pointer, the object's reference count reaches zero and automatically deletes itself.

The below shows a simple but complex case:

The software generates an instance and retains its pointer (p). At the beginning, the count of references is established at 1. Subsequently, when the pointer is utilized, the software triggers the Release function. As a result, the reference count is reduced to zero, leading to the automatic deletion of the instance. Following this, the p pointer becomes invalid. Any attempt to invoke another function using p will lead to an error.

The diagram below illustrates a more intricate scenario:

Here, the software generates an instance and assigns the pointer p, as explained previously. The software then duplicates p to the fresh variable q. At this point, the software needs to invoke AddRef to boost the reference count. As a result, the count of references increases to 2, denoting the two pertinent factors. Subsequently, upon the software's conclusion by executing p, Release is triggered, decrementing the reference count to 1 and rendering p unusable. Nevertheless, q remains functional. Subsequently, when q is utilized, the software invokes Release once more. This process diminishes the reference count to zero, leading to the object's self-deletion.

You may wonder why the program would duplicate the pointer p. There exist two main rationales for this behavior:

  • The intention might be to retain the pointer in a data structure like a list.
  • Another reason could be to preserve the pointer beyond the existing scope of the initial variable.

By doing this, you are essentially copying it to a different variable that has a broader reach.

One benefit of reference counting is that developers can share pointers in various sections of the code without the need to synchronize multiple code paths for object deletion. Instead, each path can directly invoke Release once done with the object. As a result, the object manages its own destruction promptly.

Example

Example

HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
 COINIT_DISABLE_OLE1DDE);
if (SUCCEEDED(hr))
{
 IFileOpenDialog *pFileOpen;
 hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL,
 IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));
 if (SUCCEEDED(hr))
 {
 hr = pFileOpen->Show(NULL);
 if (SUCCEEDED(hr))
 {
 IShellItem *pItem;
 hr = pFileOpen->GetResult(&pItem);
 if (SUCCEEDED(hr))
 {
 PWSTR pszFilePath;
 hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
 if (SUCCEEDED(hr))
 {
 MessageBox(NULL, pszFilePath, L"File Path",
MB_OK);
 CoTaskMemFree(pszFilePath);
 }
 pItem->Release();
 }
 }
 pFileOpen->Release();
 }
 CoUninitialize();
}

Explanation

The provided code initiates the COM process, launches the file dialog window, fetches the file path of the chosen file, showcases it, and then performs necessary cleanup tasks.

Reference counting is employed twice within this code snippet. Initially, in the event that the function effectively instantiates a Common Item Dialog object, it is necessary to invoke the Release method on the pFileOpen pointer.

Example

hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_ALL,
 IID_IFileOpenDialog, reinterpret_cast<void**>(&pFileOpen));
if (SUCCEEDED(hr))
{
 // ...
 pFileOpen->Release();
}

Explanation

The aforementioned code snippet showcases the File Open Dialog COM object. Upon successful completion of the build process, it proceeds to carry out a task and subsequently frees up resources by invoking the Release method to optimize memory usage.

Moreover, when the GetResult function returns a reference to the IShellItem interface, it is essential for the program to invoke the Release method on the pItem pointer.

Example

hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
 // ...
 pItem->Release();
}

Explanation

The code snippet mentioned above fetches the item that is currently selected in the file dialog. Upon successful retrieval, it proceeds to execute operations on the item before deallocating its resources.

Note: In both scenarios, calling Release is the last action before the pointer is no longer in scope. Furthermore, it is important to note that the Release is called only after HRESULT confirms its success. For example, if the CoCreateInstance call fails, the pFileOpen pointer remains active. As a result, trying to call Release on the pointer will result in an error.

Asking an Object for an Interface

A solitary object has the capability to employ several interfaces, like the Common Item Dialog object. To provide basic functionality, the system leverages the IFileOpenDialog interface, encompassing functions to showcase a dialog box and gather details on the chosen file. Furthermore, the system accommodates the IFileDialogCustomize interface for more sophisticated customization features. This interface empowers users to modify the visual appearance of the dialog box by integrating extra UI controls. It's important to note that any COM interface has the potential to derive from an IUnknown interface either directly or indirectly. The diagram below illustrates the property hierarchy of the Common Item Dialog object.

As illustrated in the diagram, the direct antecedent of IFileOpenDialog was the IFileDialog interface, which in turn is an extension of the IModalWindow interface. While moving from IFileOpenDialog to IModalWindow, each interface progressively outlines all window functions that are produced. Ultimately, the IModalWindow interface derives from IUnknown. Moreover, the Common Item Dialog object is compatible with the IFileDialogCustomize interface, which belongs to a distinct property collection.

Now, assuming you possess a pointer referencing the IFileOpenDialog interface, how would you acquire a pointer to the IFileDialogCustomize interface?

Converting the IFileOpenDialog pointer directly into the IFileDialogCustomize pointer is not feasible on its own. If Runtime Type Information (RTTI) does not traverse the hierarchy of sequences, it is not viable in a specific programming language.

Instead, the COM technique requests the object to supply an IFileDialogCustomize pointer that serves as a pathway for the initial interface. This involves invoking the IUnknown::QueryInterface function using the initial interface pointer. In essence, QueryInterface functions as the language-agnostic counterpart to the dynamic_cast keyword in C++.

The QueryInterface function is specified with the subsequent signature:

Syntax

Example

HRESULT QueryInterface(REFIID riid, void **ppvObject);

How Does QueryInterface Work?

  • The riid parameter represents the GUID of the requested interface.
  • The REFIID data type is a typedef for the const GUID&, which eliminates the need for a Class Identifier (CLSID) when the object is already instantiated.
  • Only interface identification is required. The ppvObject parameter accepts a pointer to the interface, which is declared void** to match CoCreateInstance. This QueryInterface allows you to query any COM interface without typing it hard.

Here is the method to call QueryInterface in order to acquire an IFileDialogCustomize pointer:

Example

hr = pFileOpen->QueryInterface(IID_IFileDialogCustomize,
 reinterpret_cast<void**>(&pCustom));
if (SUCCEEDED(hr))
{
 // Use the interface. (Not shown.)
 // ...
 pCustom->Release();
}
else
{
 // Handle the error.
}

Explanation

The provided code snippet above retrieves an IFileDialogCustomize interface pointer from the pFileOpen object by utilizing a QueryInterface method. Upon a successful retrieval, the interface is utilized, followed by the release of its resources. In case of a failure, error handling procedures are executed.

Memory Allocation in COM

Memory allocation within the Component Object Model (COM) holds significant importance due to the frequent utilization of COM components across process boundaries. Establishing clear and precise memory management protocols is crucial to uphold system stability and operational efficiency.

Here is a concise summary of memory assignment in COM:

At times, a method might share the buffer's memory address with the caller through the allocation of a memory buffer on the heap. COM offers various functions tailored for the allocation and deallocation of memory on the heap.

An occurrence of this pattern was noted in the instance related to the Open dialog window.

Example

PWSTR pszFilePath;
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
if (SUCCEEDED(hr))
{
 // ...
 CoTaskMemFree(pszFilePath);
}

Explanation

The above code initializes the pointer 'pszFilePath' of type 'PWSTR` and calls the 'GetDisplayName' method on the 'pItem' object, passing 'SIGDN_FI'LESYSPATH' as a parameter and the address of 'pszFilePath' It checks as an action is successful, if any, maybe using the data stored in 'pszFilePath', execute more code. Finally, it frees the memory allocated to 'pszFilePath' using the 'CoTaskMemFree' task.

Why Does COM Define its Own Memory Allocation Functions?

COM defines its memory allocation functions to ensure consistent and reliable memory management across programming languages, platforms, and components. These tasks, such as 'CoTaskMemAlloc', 'CoTaskMemRealloc', and 'CoTaskMemFree', are specifically designed to handle memory allocation and deallocation in a manner consistent with COM rules and requirements.

This helps avoid memory leaks, fragmentation, and other memory-related problems that can occur when using standard memory allocation functions in a COM environment.

COM Coding Practices

In this topic, we will ensure the creation of robust, maintainable and efficient code within the COM framework. It is important to follow a few basic principles. Below are some recommended best practices.

You may encounter something like these linker errors while running your program:

Error Code

_PRESERVE49__

The error message above indicates that a GUID constant was created with an external link, but the linker could not find the definition of this constant. Normally, the value of the GUID constant is exported from a static library file. You can avoid the need to link to a static library by using the uuidof function in Microsoft Visual C++, which is a Microsoft language extension. This function retrieves a GUID value from an expression, which can be an interface type name, a class name, or an interface pointer. Using uuidof, you can instantiate the Common Item Dialog object as shown below:

_PRESERVE50__

Explanation

The above code initializes a pointer to a file-open dialog interface (IFileOpenDialog) and creates an instance of the FileOpenDialog class, enabling interaction with file-open dialog functionality in Windows.

Note: You do not need to export the libraries. The compiler retrieves the GUID value from the header. The type name is associated with the GUID value using __declspec(uuid(...)) in the header file.

The IID_PPV_ARGS Macro

We noticed that both CoCreateInstance and QueryInterface require that the last parameter be cast to a void** type, which risks type inconsistencies. Consider the following code snippet:

_PRESERVE51__

Explanation

The above code requests the IFileDialogCustomize interface but provides the IFileOpenDialog pointer. Using the reinterpret_cast expression bypasses the C++ type system, allowing the compiler to ignore this error. In the best-case scenario, if the object does not support the requested interface, the call simply fails. However, in a worst-case scenario, the project can succeed, resulting in inconsistent symbols. In particular, the pointer type does not match the actual vtable in memory. As you might expect, this situation comes with unintended consequences.

Note: vtable (virtual table) is a tool used in resource management to implement polymorphism. It is basically an array of function pointers associated with a class or object that enables dynamic dispatch of virtual tasks. If a class contains virtual functions, the compiler typically creates a vtable for that class. Each class object then passes a pointer to its corresponding vtable, which can call the correct virtual function at runtime based on the actual type of the object rather than its static type.

The IIDPPVARGS macro helps mitigate this error. To use this macro, substitute the following code:

_PRESERVE52__

With the following code:

_PRESERVE53__

The macro actually adds __uuidof(IFileOpenDialog) to the interface identifier, making sure it matches the pointer type. The correct code given below is:

_PRESERVE54__

Additionally, you can go through the same macro with QueryInterface:

_PRESERVE55__

What is The SafeRelease Pattern?

The SafeRelease policy is a common method used in COM design (Component Object Model) to safely release interfaces and manage memory. This includes ensuring that an interface pointer is not NULL before release and then setting the pointer to NULL after release to prevent possible duplication or incorrect memory access.

Example:

_PRESERVE56__

Explanation

The SafeRelease template function in the above code is a utility in C++ designed to safely release objects assigned to the COM interface. It takes the reference to the pointer as its argument and checks if the pointer is not NULL. If it is not NULL, it releases the interface resource with the Release method and sets the pointer to NULL, thus preventing memory leaks by ensuring that the objects are cleaned properly.

This function accepts a COM interface pointer as an argument and performs the following actions:

  • Checks whether the pointer is NULL.
  • Calls Release if the pointer is not NULL.
  • Sets the pointer to NULL value.

See the below example on how to use SafeRelease:

Example

_PRESERVE57__

Explanation

If CoCreateInstance succeeds, the pointer is released by calling SafeRelease. In the event of a failure, if pFileOpen remains NULL, the SafeRelease function detects this and avoids the Release call.

Multiple calls to SafeRelease on the same pointer are safe, as shown below:

_PRESERVE58__

Explanation

The above code snippet indicates that the second line releasing pFileOpen is unnecessary because it's already been released once.

COM Smart Pointers

The SafeRelease function proves to be beneficial, with two key considerations to keep in mind:

  • Ensure that each interface pointer is initialized to NULL.
  • For effective memory handling, remember to call SafeRelease before a pointer exits its scope.

C++ includes both a constructor and a destructor, making it beneficial to encapsulate the main interface pointer within a class. This approach ensures automatic initialization and release of the pointer, as demonstrated in the following code snippet:

Example

Example

template <class T>
  class Smarcpptutorialer
  {
   T* ptr;
  public:
   Smarcpptutorialer(T *p) : ptr(p) { }
   ~Smarcpptutorialer()
   {
   if (ptr) { ptr->Release(); }
   }
  };

Explanation

The provided class definition is incomplete and therefore cannot be used effectively. To ensure its functionality, it is necessary to include a copy constructor, an assignment operator, and a function for accessing the underlying COM pointer. It is worth noting that Microsoft Visual Studio provides a smart pointer class in the Active Template Library (ATL), which simplifies this process.

The ATL smarcpp tutorialer class is referred to as CComPtr. An illustration showcasing the utilization of the CComQIPtr is provided below.

Example

#include <atlbase.h> // Include ATL headers

  // Define a COM interface
  interface IFoo : public IUnknown
  {
      virtual void SomeMethod() = 0;
  };
 
  // Implement a COM object that supports the IFoo interface
  class CFoo : public CComObjectRootEx<CComSingleThreadModel>,
               public CComCoClass<CFoo, &CLSID_Foo>,
               public IFoo
  {
  public:
      DECLARE_REGISTRY_RESOURCEID(IDR_FOO)
 
      BEGIN_COM_MAP(CFoo)
          COM_INTERFACE_ENTRY(IFoo)
      END_COM_MAP()
 
      // Implement IFoo methods
      void SomeMethod() override { /* Method implementation */ }
  };
 
  // Define the class that uses CComQIPtr to manage COM interface pointers
  class SomeClass
  {
  private:
      CComQIPtr<IFoo> m_pFoo;
 
  public:
      void Initialize()
      {
          // Create an instance of the COM object
          CComObject<CFoo>* pFooObj;
          HRESULT hr = CComObject<CFoo>::CreateInstance(&pFooObj);
          if (SUCCEEDED(hr))
          {
              // Query for the IFoo interface and assign it to CComQIPtr
              m_pFoo = pFooObj;
          }
      }
 
      void DoSomething()
      {
          if (m_pFoo)
          {
              // Use the IFoo interface methods
              m_pFoo->SomeMethod();
          }
      }
  };
 
  int main()
  {
  // To Initialize COM
      CoInitialize(nullptr);
 
      SomeClass obj;
      obj.Initialize();
      obj.DoSomething();
 
  // To Uninitialize COM
      CoUninitialize();
 
      return 0;
  }

Explanation

The preceding code showcases the usage of 'CComQIPtr' from the Active Template Library in a Component Object Model (COM) setting. It defines a COM interface named 'IFoo' containing a method named 'SomeMethod' and implements a COM object named 'CFoo' that provides support for this interface. Within the 'SomeClass' class, the 'CComQIPtr<IFoo>' is employed to handle the interface pointer, instantiating an object of 'CFoo', querying for the 'IFoo' interface, and invoking its method. The 'main' function starts COM, generates an instance of 'SomeClass', calls its methods, and finally shuts down COM.

CComPtr represents a class template designed to store a pointer to a designated COM interface type. It is responsible for handling this pointer internally and customizing the 'operator->' and 'operator&' operators to replicate the functionality of the original pointer. This functionality facilitates smooth communication with the COM interface. For example, by utilizing CComPtr, the execution of the 'IFileOpenDialog::Show' method can be accomplished as if it were being directly invoked.

Example

hr = pFileOpen->Show(NULL);

CComPtr additionally offers a function that invokes the CoCreateInstance function with predefined parameter values. This function only needs the class identifier as an input, illustrated in the example below:

Example

hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));

The utilization of the 'CComPtr::CoCreateInstance' method is merely for ease of use; nevertheless, you still have the choice to directly call the COM 'CoCreateInstance' function.

Error Handling in COM

In Component Object Model (COM), HRESULT is employed to indicate the success or failure of a method or function call. Various SDK headers contain definitions for HRESULT constants. One such header, WinError.h, provides a comprehensive list of detailed error codes. Refer to the table below for an overview of the return codes associated with these systems.

Constant Numeric Value Message
E_ACCESSDENIED 0x80070005 Access denied
E_FAIL 0x80004005 Unspecified error
E_INVALIDARG 0x80070057 Invalid parameter value
E_OUTOFMEMORY 0x8007000E Out of Memory
E_POINTER NULL 0x80004003 was passed incorrectly for a pointer value
E_UNEXPECTED 0x8000FFFF Unexpected Condition
S_OK 0x0 Success
S_FALSE 0x1 Success

In the above table, all constants prefixed with "E" indicate an error code, while SOK and SFALSE indicate a success code. Although COM methods return SOK at about a 99% success rate, it is important not to be misled by these statistics. Some methods may return arbitrary success codes, and a SUCCEEDED or FAILED macro will need to be used for error checking. The example code below shows the incorrect and correct way to check the success of a function call.

Example

// Wrong way to implement
HRESULT hr = SomeFunction();
if (hr != S_OK)
{
 printf("Error!\n"); // Bad. hr might be another success code.
}
// Right way to implement
HRESULT hr = SomeFunction();
if (FAILED(hr))
{
 printf("Error!\n");
}

Explanation

The SFALSE success guideline is notable. Certain procedures employ SFALSE to signify a negative non-failure state or to denote a "no-op" scenario - where the procedure was successful but did not result in any changes. For instance, the CoInitializeEx function yields SFALSE if it is invoked for a second time within the same thread. To differentiate between SOK and S_FALSE within your code, it is advisable to directly evaluate the value while continuing to utilize FAILED or SUCCEEDED for managing subsequent statements. Below is a sample code excerpt:

Example

if (hr == S_FALSE)
{
 // Handle special case.
}
else if (SUCCEEDED(hr))
{
 // Handle general success case.
}
else
{
 // Handle errors.
 printf("Error!\n");
}

Explanation

The code snippet above evaluates the outcome of a function call stored in the 'hr' variable. When 'hr' is equal to 'S_FALSE', it manages this specific successful scenario. In the case where the function call is typically successful (as denoted by the 'SUCCEEDED' macro), it meets the standard success criteria. It will display an error message if an error occurs.

Utilize the 'SUCCEEDED' and 'FAILED' macros when assessing the result. When verifying a specific error code, make sure to also incorporate a default case.

Example

if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
 // Address the particular scenario of an unsupported pixel format.
}
else if (FAILED(hr))
{
 // Handle other errors.
}

Explanation

The code above verifies the occurrence of a particular error associated with pixel format. In case it is detected, it resolves that situation. If not, it manages other errors accordingly.

Patterns for Error Handling

In this section, we will examine some patterns for organizing the handling of COM errors. Each method has its benefits and drawbacks. If you go with the existing project, it might already have some guidelines that restrict a particular style. However, which pattern you use the robust code will consider the rules that are following:

  • For every function or method that returns HRESULT, check the return value before moving further.
  • Release the resources after use.
  • Avoid trying to access resources that are invalid or uninitialized, such as NULL pointers.
  • Do not use the resources after release.

With these rules, there are four patterns for handling errors.

  • Nested ifs
  • Cascading ifs
  • Jump on Fail
  • Throw on Fail
  • Nested ifs

After each function call that returns an HRESULT, use an if statement to check for success. Then, wrap the next method call inside the if statement. This nesting of if statements can go to any required level. Although this approach has been shown in earlier code samples in this section, it is restated here for emphasis.

Example

HRESULT ShowDialog()
{
 IFileOpenDialog *pFileOpen;
 HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
 CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
 if (SUCCEEDED(hr))
 {
 hr = pFileOpen->Show(NULL);
 if (SUCCEEDED(hr))
 {
 IShellItem *pItem;
hr = pFileOpen->GetResult(&pItem);
 if (SUCCEEDED(hr))
 {
 // Use pItem (not shown).
 pItem->Release();
 }
 }
 pFileOpen->Release();
 }
 return hr;
}

Explanation

The provided code outlines a function called ShowDialog, which aims to present a file open dialog. Initially, it sets up a pointer to IFileOpenDialog and then endeavors to instantiate it through CoCreateInstance. Upon successful creation, it moves on to display the dialog and fetch the chosen item. Subsequently, each action involves verifying the HRESULT for success through conditional if statements. Upon successful completion of all tasks, it deallocates resources and outputs the ultimate HRESULT.

Advantages

  • Variables can have their scope minimized, as demonstrated by the declaration of pItem only when it's needed.
  • Within each if statement, it's ensured that all previous calls have succeeded and all acquired resources remain valid.
  • For instance, in the previous example, by the time the innermost if statement is reached, both pItem and pFileOpen are confirmed to be valid.
  • The release of interface pointers and other resources is straightforward: it's done at the end of the if statement immediately following the acquisition of the resource.

Disadvantages

  • Complex code structures with multiple levels of nesting may pose difficulties for certain individuals to understand fully.
  • Integrating error management into branching and looping constructs can add another layer of complexity to the program's logic, potentially making it harder to follow.
  • Cascading ifs

After executing each method, validate its success using an if statement. If the method is successful, proceed with the next method call within the if block. Avoid nesting if statements further; instead, place each subsequent successful test after the preceding if block. In case any method fails, all following successful tests will also fail until the end of the function is reached.

Example

HRESULT ShowDialog()
{
 IFileOpenDialog *pFileOpen = NULL;
 IShellItem *pItem = NULL;
 HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
 if (SUCCEEDED(hr))
 {
 hr = pFileOpen->Show(NULL);
 }
 if (SUCCEEDED(hr))
 {
 hr = pFileOpen->GetResult(&pItem);
 }
 if (SUCCEEDED(hr))
 {
 // Use pItem.
 }
 // Clean up.
 SafeRelease(&pItem);
 SafeRelease(&pFileOpen);
 return hr;
}

Explanation

The ShowDialog method initializes pointers to the IFileOpenDialog and IShellItem interfaces by assigning them a value of NULL. It then tries to instantiate a FileOpenDialog object using CoCreateInstance. Upon successful creation, it proceeds with showing the dialog and fetching the chosen item. If both actions are successful, the variable pItem is now prepared for further operations. To conclude, it deallocates resources using SafeRelease and returns the HRESULT.

By following this approach, the resource is only freed at the conclusion of the task. Nevertheless, in the event of a bug, the pointer could become invalid upon exiting the project. Trying to dereference an invalid pointer may lead to a program failure or crash. To prevent this, it is crucial to set all pointers to NULL initially and check that they are non-NULL before deallocating them. The SafeRelease function is demonstrated below, with smart pointers providing a dependable substitute. Exercise caution with loop constructs while employing this technique; promptly exit the loop if a call fails.

Advantages

  • Compared to the "nested ifs" model, this approach significantly reduces the nesting.
  • It increases the clarity of the overall control flow.
  • Additionally, releases are made in a well-defined area in the code.

Disadvantages

  • All changes must be declared and initialized at the beginning of the process.
  • Instead of immediately exiting the project upon failure, it checks for an error unnecessarily frequently.
  • Care is needed to avoid having all the elements that do not have the function due to the continuous flow of authority in the function after a failure
  • Handling errors in the loop requires special consideration.
  • Jump on Fail

Follow each function invocation to check for any failures. In case of a failure, go directly to the indicated line towards the conclusion of the process. Once you reach this marker, make sure to release any resources before exiting the project.

Example

Example

HRESULT ShowDialog()
{
 IFileOpenDialog *pFileOpen = NULL;
 IShellItem *pItem = NULL;
 HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
 CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
 if (FAILED(hr))
 {
 goto done;
 }
 hr = pFileOpen->Show(NULL);
 if (FAILED(hr))
 {
 goto done;
 }
 hr = pFileOpen->GetResult(&pItem);
 if (FAILED(hr))
 {
 goto done;
 }
 // Use pItem (not shown).
done:
 // Clean up.
 SafeRelease(&pItem);
 SafeRelease(&pFileOpen);
 return hr;
}

Explanation

The provided code snippet outlines a function named 'ShowDialog' designed to present an open file dialog. Initially, it endeavors to instantiate the file open dialog by employing the COM function 'CoCreateInstance'. Upon successful creation, the dialog is displayed via 'Show'. Subsequently, the selected item is acquired through 'GetResult'. In the event of any of these processes encountering an error (identified by the 'FAILED(hr)' validation), the program transitions to the 'done' label. At this point, it deallocates the objects ('pItem' and 'pFileOpen') by invoking the 'SafeRelease' method. Finally, the function concludes by returning HRESULT, signaling the outcome of the operation.

Advantages

  • The control flow of the code is simple and clear.
  • After each FAILED check, if the code does not jump to the specified label, it ensures that all previous calls were successful.
  • Additionally, the releases are centralized in the code.

Disadvantages

  • All variables in a process must be declared and initialized at the beginning.
  • Some programmers prefer to avoid using go to in their code.
  • Goto comments around initializers.
  • Throw on Fail

Instead of employing a label to navigate to it, you have the option to trigger an exception in case a method fails. This method can result in a more symbolic approach to C++ programming, particularly if you are accustomed to crafting exception-safe code.

Example

#include <comdef.h>
  // Declares _com_error
  inline void throw_if_fail(HRESULT hr)
  {
   if (FAILED(hr))
   {
   throw _com_error(hr);
   }
  }
  void ShowDialog()
  {
   try
   {
   CComPtr<IFileOpenDialog> pFileOpen;
   throw_if_fail(CoCreateInstance(__uuidof(FileOpenDialog), NULL,
   CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen)));
   throw_if_fail(pFileOpen->Show(NULL));
   CComPtr<IShellItem> pItem;
   throw_if_fail(pFileOpen->GetResult(&pItem));
   // Use pItem (not shown).
   }
   catch (_com_error err)
   {
   // Handle error.
   }
  }

Explanation

The code snippet above demonstrates the utilization of the Windows COM API to generate a dialog box for opening files. The 'throwiffail' function verifies the HRESULT response from the COM function and raises a 'comerror' exception in case of a failure. Within the 'ShowDialog' function, an 'IFileOpenDialog' interface instance is initially created through 'CoCreateInstance', followed by the presentation of the dialog via the 'Show' method, and ultimately the retrieval of the selected file object using 'GetResult' to obtain the file path. Any encountered errors during this sequence are captured by a try-catch block, where handling of 'comerror' exceptions is implemented.

This demonstration illustrates the utilization of the 'CComPtr' class for managing interface pointers in C++. It highlights the significance of adhering to the RAII (Resource Acquisition is Initialization) principle, particularly when dealing with exclusive guidelines. RAII guarantees the correct handling of resources by tying their release to the lifecycle of the destructor. Implementing RAII helps in averting resource leaks by ensuring that the destructor is invoked, even in scenarios where exceptions interrupt resource allocation or utilization.

Advantages

  • This particular approach is crafted to smoothly incorporate with current codebases that implement exception handling.
  • Furthermore, it verifies compatibility with C++ libraries that rely on exceptions, like the STL (Standard Template Library).

Disadvantages

  • This method necessitates the utilization of C++ objects for handling resources like memory or file handles.
  • Proficiency in writing exception-safe code is essential to guarantee effective resource management when dealing with exceptions.

After an in-depth conversation about the Component Object Model (COM), the subsequent part will delve into Windows graphics, focusing on generating a blank window and manipulating visual elements.

Module 3: Windows Graphics:

In the initial section, we covered the process of generating a window without any content. Moving on to the second section, there was a brief overview of the Component Object Model (COM), which serves as the foundation for numerous contemporary Windows Application Programming Interfaces (APIs). Now, the task at hand is to incorporate images into the empty window established in the preceding section from Module 1. This endeavor will primarily center on Direct2D, alongside exploring the manipulation of images and visual elements.

The Windows Graphics Architecture

The Windows operating system provides a range of C++/COM APIs for graphical tasks, as illustrated in the diagram below.

It serves as the fundamental graphical user interface for the Windows operating system. Initially designed for 16-bit Windows, it was subsequently enhanced to support 32-bit and 64-bit iterations.

GDI+, which was first introduced in Windows XP, serves as the successor to GDI. It can be interacted with using various C++ classes and is accessible within the .NET Framework.

Direct3D

Direct3D serves as an interface for 3D modeling, empowering developers to craft engaging 3D landscapes, games, and simulations by leveraging the high-speed capabilities of the GPU.

Direct2D

Direct2D serves as a contemporary interface for 2D graphics that goes beyond GDI and GDI+. It offers accelerated rendering using hardware, enhanced clarity, and anti-aliasing features.

DirectWrite

DirectWrite serves as a text processing and rasterization mechanism responsible for managing top-notch text quality, font configurations, and text arrangement. You have the option to render rasterized text through GDI or Direct2D.

It represents DirectX Graphics Infrastructure, serving as a middleman between Direct3D and the graphics driver. DXGI is commonly accessed by applications through Direct3D in an indirect manner.

Note: Direct2D and DirectWrite were introduced with Windows 7 but were also made available for Windows Vista and Windows Server 2008 from the platform update.

Benefits of Direct2D:

Here are some key benefits of utilizing the Direct2D API:

Hardware Acceleration

Utilizing hardware acceleration involves leveraging a graphics processing unit (GPU) in place of a central processing unit (CPU) for graphic computations. Contemporary GPUs are specifically designed for graphical displays, enhancing efficiency in performing these operations. Shifting a larger portion of this computational burden from the CPU to the GPU typically enhances system performance.

While GDI leverages hardware acceleration for certain functions, a significant part of its operation relies on the CPU. On the other hand, Direct2D is built upon Direct3D and effectively harnesses the GPU's hardware acceleration. When the GPU lacks the required resources for Direct2D, it defaults to software rendering. Overall, Direct2D typically surpasses GDI and GDI+ in performance across various scenarios.

Transparency and Anti-aliasing

Direct2D offers comprehensive hardware-accelerated alpha blending capabilities and offers versatile anti-aliasing for edges. On the other hand, GDI has restricted support for alpha blending. While most GDI applications do not incorporate alpha blending, GDI does provide support for alpha blending in bitblt applications. In contrast, GDI+ does support transparency, but the CPU handles alpha blending, hence it does not leverage hardware acceleration. The utilization of hardware-accelerated alpha blending also facilitates anti-aliasing.

Alias occurs as an artifact that emerges during the instantiation of consecutive tasks. For instance, in the process of transforming curved lines into pixels, aliasing has the potential to lead to visual distortion. Any approach aimed at minimizing these artifacts without altering the core characteristics of the image is deemed unsuitable. In the context of digital art, antialiasing involves the technique of smoothly blending the edges of shapes with the background. For instance, consider a comparison between a circle rendered using GDI and the identical circle rendered using Direct2D.

The image below offers a detailed look at every circle.

In the images above, the circle drawn using GDI on the left displays black pixels near the curve, whereas the circle rendered with Direct2D on the right leverages blending to achieve a seamless curve.

GDI does not provide anti-aliasing support for rendering geometric shapes like lines and curves, leading to rough edges. While ClearType can anti-alias text in GDI, other text rendered in GDI lacks this feature, impacting legibility due to inconsistent font rendering. Even though GDI+ offers anti-aliasing capabilities, it relies on the CPU, leading to decreased performance when compared to Direct2D.

Interoperability

For fresh software projects, it is recommended to utilize Direct2D; however, it is also compatible with GDI when the need arises.

Vector Graphics

Direct2D employs vector illustrations, which rely on mathematical equations to define lines and curves. In contrast to raster images, vector graphics are independent of dimensions, enabling effortless adjustment without compromising clarity. This adaptability renders vector graphics indispensable for supporting diverse monitor dimensions and screen resolutions.

The Desktop Windows Manager

Before the release of Windows Vista, a Windows program would present its data directly on the display without going through intermediary layers. It achieved this by directly writing to a memory buffer controlled by the video card. However, failing to reset the window correctly using this method could lead to visual glitches. For instance, when moving one window over another, if the window at the bottom doesn't refresh quickly, it may result in a delay caused by the window on top.

The trailing phenomenon arises due to the shared memory reference in both scenarios. As the upper windows are dragged down, it is essential to repaint the lower windows. If this refreshing process is sluggish, it can lead to visual artifacts similar to those observed in the previous image. The integration of the Desktop Window Manager (DWM) in Windows Vista brought a significant shift in the handling of Windows. With DWM active, Windows no longer directly render onto the screen buffer. Instead, each window is represented in a separate offscreen memory buffer known as an offscreen surface. Subsequently, DWM merges these surfaces to compose the final display visible on the monitor.

Advantages of Desktop Windows Manager

The Desktop Window Manager (DWM) offers numerous benefits compared to the old graphics architecture.

  • Visual Effects
  • Desktop Composition
  • Improved Stability
  • High DPI Support
  • Accessibility Features
  • Desktop Composition API
  • Note: However, it is important to note that DWM is not always enabled. Some graphics cards do not meet the system requirements for DWM support, and users can activate them through the System Properties control panel. Therefore, your project should not depend solely on the DWM's repaint behavior. It is best to test your program with DWM enabled to ensure that the repaint works properly.

    Retained Mode VS Immediate Mode

There are two primary categories of Graphics APIs: immediate-mode APIs and retained-mode APIs. Direct2D belongs to the immediate-mode group, whereas Windows Presentation Foundation (WPF) serves as an illustration of a retained-mode API.

In a persistent-mode API, the function serves as a specification. The software utilizes basic graphical elements like shapes and strokes to construct a visual representation. The graphic repository retains this graphical representation in storage. When rendering a frame, the graphical toolkit translates this representation into a sequence of drawing directives. In successive frames, the toolkit maintains the state internally. To modify the existing definition, the software sends directives to modify the representation, like inserting or deleting shapes. Subsequently, the toolkit is tasked with redrawing the modified design appropriately.

On the other hand, an immediate-mode API functions procedurally. During each frame rendering, the application sends drawing commands directly without the graphics library retaining a scene model across frames. It is then up to the application to manage the state of the scene.

Retained-mode interfaces are commonly favored for their user-friendly nature, taking charge of duties such as setup, maintaining status, and tidying up. Nevertheless, they can be somewhat inflexible because of their pre-set scenario structure and may demand increased memory for a universal scenario model. On the other hand, immediate-mode interfaces provide enhanced adaptability, enabling specific optimizations to be executed.

Our First Direct2D Program

Now, let's initiate our initial Direct2D application. This software isn't overly complex; its main purpose is to render a circular shape that occupies the entire client area of the window. Nevertheless, this basic task introduces us to various essential Direct2D principles.

Below is the code snippet for the Circle program. This software makes use of the BaseWindow class, which was presented in the Managing Application State section. Future topics will explore the intricacies of the code further.

Example

#include <windows.h>
  #include <d2d1.h>
 
  #pragma comment(lib, "d2d1.lib")
 
  // Forward declarations
  LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
 
  // Global variables
  ID2D1Factory* pFactory = nullptr;
  ID2D1HwndRenderTarget* pRenderTarget = nullptr;
 
  // Function to initialize Direct2D
  HRESULT CreateGraphicsResources(HWND hWnd)
  {
      HRESULT hr = S_OK;
      if (!pRenderTarget)
      {
          // Create Direct2D factory
          hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory);
          if (SUCCEEDED(hr))
          {
              RECT rc;
              GetClientRect(hWnd, &rc);
 
              // Create a Direct2D render target
              hr = pFactory->CreateHwndRenderTarget(
                  D2D1::RenderTargetProperties(),
                  D2D1::HwndRenderTargetProperties(hWnd, D2D1::SizeU(rc.right - rc.left, rc.bottom - rc.top)),
                  &pRenderTarget
              );
          }
      }
      return hr;
  }
 
  // Function to release Direct2D resources
  void DiscardGraphicsResources()
  {
      if (pRenderTarget)
      {
          pRenderTarget->Release();
          pRenderTarget = nullptr;
      }
      if (pFactory)
      {
          pFactory->Release();
          pFactory = nullptr;
      }
  }
 
  // Function to draw Direct2D content
  void OnPaint(HWND hWnd)
  {
      HRESULT hr = CreateGraphicsResources(hWnd);
      if (SUCCEEDED(hr))
      {
          if (pRenderTarget)
          {
              pRenderTarget->BeginDraw();
              pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
 
              // Draw a circle
              D2D1_ELLIPSE ellipse = D2D1::Ellipse(D2D1::Point2F(200, 200), 100, 100);
              pRenderTarget->FillEllipse(&ellipse, D2D1::ColorF(D2D1::ColorF::Green));
 
              hr = pRenderTarget->EndDraw();
              if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)
              {
                  DiscardGraphicsResources();
              }
          }
      }
  }
 
  // Main function
  int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
  {
      // Register window class
      const wchar_t CLASS_NAME[] = L"Direct2DWindowClass";
 
      WNDCLASS wc = {};
      wc.lpfnWndProc = WndProc;
      wc.hInstance = hInstance;
      wc.lpszClassName = CLASS_NAME;
      wc.style = CS_HREDRAW | CS_VREDRAW;
 
      RegisterClass(&wc);
 
      // Create window
      HWND hWnd = CreateWindowEx(
          0,                          // Optional window styles
 
          CLASS_NAME,
          L"Direct2D Program",
          WS_OVERLAPPEDWINDOW,
          CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
          nullptr,  
          nullptr,
          hInstance,
          nullptr
      );
 
      if (hWnd == nullptr)
      {
          return 0;
      }
 
      // Show window
      ShowWindow(hWnd, nCmdShow);
      UpdateWindow(hWnd);
 
      // Message loop
      MSG msg = {};
      while (GetMessage(&msg, nullptr, 0, 0))
      {
          TranslateMessage(&msg);
          DispatchMessage(&msg);
      }
 
      DiscardGraphicsResources();
      return 0;
  }
 
  // Window procedure
  LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
  {
      switch (message)
      {
      case WM_CREATE:
          return 0;
 
      case WM_PAINT:
          {
              PAINTSTRUCT ps;
              HDC hdc = BeginPaint(hWnd, &ps);
              OnPaint(hWnd);
              EndPaint(hWnd, &ps);
          }
          return 0;
 
      case WM_DESTROY:
          PostQuitMessage(0);
          return 0;
         
      default:
          return DefWindowProc(hWnd, message, wParam, lParam);
      }
      return 0;
  }

Explanation

The provided code represents a C++ Windows program that leverages Direct2D, a graphics API, to render a green circle within a window. It sets up Direct2D resources within the 'CreateGraphicsResources' function, establishes a window in the 'WinMain' function, and manages window messages in the 'WndProc' function. The logic for drawing the circle and managing resources is contained within the 'OnPaint' function, triggered when window painting is required. The program enters a message loop to manage user interactions and window-related actions until the window is shut down.

The D2D1 Namespace

The D2D1 namespace serves as a namespace employed within the Direct2D API, which is a part of the Microsoft Windows SDK. Within this namespace, there are various classes, interfaces, enumerations, and structures designed for the purpose of rendering 2D graphics. Direct2D API offers a fast and efficient interface, accelerated by hardware, specifically for the rendering of 2D graphics like geometric shapes, text, and images on Windows operating systems.

Within the D2D1 namespace, there are a range of classes and structures that depict essential graphical components, rendering environments, and matrices for transformations, along with other capabilities. By leveraging these elements, programmers can design visually engaging and dynamic 2D graphics applications tailored for the Windows platform.

Render Targets, Devices, and Resources

In Direct2D, Render Targets are entities where graphics are painted. They act as the endpoint for drawing actions. Direct2D offers various kinds of render targets, like window render targets for drawing directly onto a window, bitmap render targets for drawing off-screen, and printer render targets for printing purposes.

Device objects within Direct2D symbolize the hardware or software elements accountable for displaying graphics. These objects oversee the rendering procedure and communicate with the underlying graphics software or hardware. They offer a level of abstraction that enables applications to display graphics in a way that is not tied to a specific platform.

Objects in Direct2D, known as resources, play a crucial role in the rendering process. These resources encompass elements like bitmaps, brushes, geometries, and effects, carrying the essential data required for rendering graphics. Typically, these resources are generated and overseen by the Direct2D device. Effective management of these resources is key to ensuring optimal and dependable graphics rendering within Direct2D applications.

Specific elements within Direct2D gain advantages from hardware acceleration, indicating they are fine-tuned to utilize the potential of the underlying hardware, whether it's the GPU or CPU. These elements, known as device-dependent resources, encompass brushes and meshes. In the event that the device linked to these resources is no longer accessible, they need to be reconstructed for a different device.

On the flip side, certain assets are saved in the CPU memory and are not tied to the specific device in use. Instances of these are stroke patterns and shapes. In contrast to device-specific assets, there is no need to regenerate device-agnostic resources when the device undergoes a change.

The MSDN documentation clarifies whether a functionality relies on a specific device or is universal across devices. Every resource category corresponds to an interface generated from ID2D1Resource; for instance, brushes are denoted by the ID2D1Brush interface.

The Direct2D Factory Object

The Direct2D Factory Object serves as the primary interface for generating Direct2D entities and components. Its role involves initializing the Direct2D environment and overseeing tasks such as render targets, brushes, geometries, and effects. Within the Direct2D workspace, there are two distinct entities:

  • Rendering targets.
  • Device-independent assets including stroke styles and geometric shapes.

The render target object is in charge of generating device-specific assets such as brushes and bitmaps.

The Factory Object is commonly generated using the 'D2D1CreateFactory' function and serves as a centralized hub for setting up Direct2D functionalities. This includes debugging preferences and guidelines for resource creation. Moreover, it guarantees the proper initialization of Direct2D based on the existing conditions, utilizing the configuration to manage essential services effectively.

Utilize the D2D1CreateFactory function to create an instance of the Direct2D factory object.

Example

ID2D1Factory *pFactory = NULL;
HRESULT hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,
&pFactory);

Explanation

The initial argument in the provided code indicates the necessity for creation. If the D2D1FACTORYTYPESINGLETHREADED flag is employed, it ensures that Direct2D functions are not accessed from various threads simultaneously. In cases where multiple threads interact with Direct2D, opt for D2D1FACTORYTYPEMULTITHREADED instead. Opting for a single-threaded approach is more efficient when your program exclusively interfaces with Direct2D functions through one thread. The second parameter of the D2D1CreateFactory function requires a reference to the ID2D1Factory interface.

It is advisable to initialize the Direct2D workspace instance prior to handling the initial WMPAINT message. The appropriate location for workspace instantiation is within the WMCREATE message handler function.

Example

case WM_CREATE:
 if (FAILED(D2D1CreateFactory(
 D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))
 {
 return -1; // Fail CreateWindowEx.
 }
 return 0;

Explanation

In the provided code snippet, within the WMCREATE message handler, there is an effort to instantiate a Direct2D factory object by invoking the D2D1CreateFactory function with the D2D1FACTORYTYPESINGLE_THREADED flag. If the factory creation process is unsuccessful, the function returns -1 to signify the failure; otherwise, it returns 0 upon successful creation.

Creating Direct2D Resources

The Circle software depends on certain device-specific assets:

  • A rendering surface connected to the program window.
  • A monochrome brush used for painting circles.

Both of these assets are represented using a Component Object Model (COM) interface.

  • The ID2D1HwndRenderTarget interface represents the render target.
  • The ID2D1SolidColorBrush interface represents the brush.

In the Circle software, the MainWindow class maintains references to these interfaces within its instance variables.

Example

ID2D1HwndRenderTarget   *pRenderTarget;
ID2D1SolidColorBrush    *pBrush;

In the subsequent code snippet, we have instantiated two resources:

Example

HRESULT MainWindow::CreateGraphicsResources()
{
    HRESULT hr = S_OK;
    if (pRenderTarget == NULL)
    {
        RECT rc;
        GetClientRect(m_hwnd, &rc);

        D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom);

        hr = pFactory->CreateHwndRenderTarget(
            D2D1::RenderTargetProperties(),
            D2D1::HwndRenderTargetProperties(m_hwnd, size),
            &pRenderTarget);

        if (SUCCEEDED(hr))
        {
            const D2D1_COLOR_F color = D2D1::ColorF(1.0f, 1.0f, 0);
            hr = pRenderTarget->CreateSolidColorBrush(color, &pBrush);

            if (SUCCEEDED(hr))
            {
                CalculateLayout();
            }
        }
    }
    return hr;
}

Explanation

The 'CreateGraphicsResources' function is responsible for setting up the necessary graphics resources for the rendering process. Initially, it verifies whether the render target is not already established. If not, it retrieves the dimensions of the program window, a Direct2D render target linked to the window, and a solid-color brush for rendering purposes. Upon successful completion, it computes the outcome and provides the final result.

To generate a render target for a window, utilize the ID2D1Factory::CreateHwndRenderTarget method from the Direct2D factory.

  • The first parameter sets the general options for each render target, with default options provided by the D2D1::RenderTargetProperties helper function.
  • The second parameter combines the window handle and render target size in pixels.
  • The third parameter gets the ID2D1HwndRenderTargecpp tutorialer.

If the destination for rendering is already present, the CreateGraphicsResources function will promptly return S_OK without initiating any additional steps.

Drawing with Direct2D

Now, you are prepared to sketch following the development of graphic assets.

Drawing an Ellipse

The Circle software carries out fundamental drawing operations:

  • Paints the backdrop with a uniform color.
  • Displays a solid circle shape.

Rendering takes place on a window as a result of receiving a WM_PAINT message. Below is the window pathway for the Circle application.

Example

LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_PAINT:
            OnPaint();
            return 0;

         // Other messages not shown...
    }
    return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}

Explanation

The 'HandleMessage' function within the code snippet above is responsible for managing window messages. Upon receiving the WM_PAINT message, it triggers the OnPaint function to manage the painting task. Additionally, it dispatches extra messages to the default window structure for additional processing.

Here, the code is to draw the circle:

Example

void MainWindow::OnPaint()
{
    HRESULT hr = CreateGraphicsResources();
    if (SUCCEEDED(hr))
    {
        PAINTSTRUCT ps;
        BeginPaint(m_hwnd, &ps);
     
        pRenderTarget->BeginDraw();

        pRenderTarget->Clear( D2D1::ColorF(D2D1::ColorF::SkyBlue) );
        pRenderTarget->FillEllipse(ellipse, pBrush);

        hr = pRenderTarget->EndDraw();
        if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)
        {
            DiscardGraphicsResources();
        }
        EndPaint(m_hwnd, &ps);
    }
}

Explanation

In the given code snippet, the 'OnPaint' function is responsible for managing the painting process. Initially, it creates a drawing object and, upon successful creation, initiates the drawing procedure. The function clears the render target using a sky blue color and proceeds to fill the ellipse using the specified brush. Following the drawing process, it verifies for any potential errors or the necessity for render target adjustments. In case any such requirements arise, it halts the graphical processing. Ultimately, the painting operation concludes.

The ID2D1RenderTarget interface handles all drawing tasks using the program's OnPaint method as follows:

  • The ID2D1RenderTarget::BeginDraw method begins the drawing process.
  • The ID2D1RenderTarget::Clear method fills the entire render target with the specified solid color using the D2D1COLORF setting. The D2D1::ColorF class can be used to configure the setting.
  • The ID2D1RenderTarget::fillEllipse method draws an ellipse filled with the specified brush to fill it. An ellipse is defined by its focal point and x- y-radius. If both radii are equal, it forms a circle.
  • The ID2D1RenderTarget::EndDraw method specifies the end of the drawing for the current frame. All draw operations must be placed between BeginDraw and EndDraw calls.

The BeginDraw, Clear, and FillEllipse functions do not have return values. In case an error occurs during their execution, the return result of the EndDraw function will indicate the issue. The procedure for generating Direct2D Resources, as illustrated in Generating Direct2D Resources, involves the utilization of the CreateGraphicsResources function, which handles the creation of render targets and solid color brushes.

The draw function has the capability to be stored in the system and carried out at a later time through the EndDraw function. If you wish to guarantee that all pending rendering tasks are completed right away, you have the option to utilize the ID2D1RenderTarget::Flush function. Nevertheless, it should be kept in mind that performing a flush operation may have an effect on the overall performance.

The graphics hardware in use might become inactive during program execution due to several factors, like alterations in screen resolution or removal of the display adapter by the user. In case a device becomes unavailable, the render target and related device-specific resources are no longer valid. Upon detection of this issue, the Direct2D EndDraw function signals the error code D2DERRRECREATETARGET, alerting about the absent device. Should you face this specific error code, it is crucial to reconstruct the render target along with any device-specific resources.

To dispose of a resource, just relinquish the interface linked to that particular resource.

Example

void MainWindow::DiscardGraphicsResources()
{
    SafeRelease(&pRenderTarget);
    SafeRelease(&pBrush);
}

Explanation

In the previous example, the 'DiscardGraphicsResources' method within the 'MainWindow' class deallocates the memory assigned to the Direct2D resources related to the render target and brush. This is achieved through a secure release technique to prevent memory leaks.

Note: Resource creation can be costly, so it is best not to recreate resources for each WM_PAINT message. Instead, create a resource once and store its pointer until it becomes invalid due to device loss or when it is no longer needed.

The Direct2D Render Loop

Regardless of what you download, your program should create a loop similar to the one below.

  • Initialize device-independent resources.
  • Render the scene: Check if you have a valid render target. If none exists, create both a render target and a device-dependent resource. Start drawing by calling ID2D1RenderTarget::BeginDraw. Issue drawing commands to display the view. End the drawing with ID2D1RenderTarget::EndDraw. If EndDraw returns D2DERRRECREATETARGET, discard the render target and its associated device-dependent targets.
  • Repeat step 2 whenever you need to update or redraw the view.
  • Check if you have a valid render target.
  • Start drawing by calling ID2D1RenderTarget::BeginDraw.
  • Issue drawing commands to display the view.
  • End the drawing with ID2D1RenderTarget::EndDraw.
  • If EndDraw returns D2DERRRECREATETARGET, discard the render target and its associated device-dependent targets.

Step 2 takes place when the render target is a window, with each window receiving a WM_PAINT message. This process manages device loss by discarding device-specific objects and then restarting at the loop's inception (step 2a).

DPI and Device-independent Pixels

To effectively create Windows graphical applications, it is essential to grasp two interconnected ideas:

  • Pixels per inch (PPI)
  • Device-independent pixels (DIPs)

Let's delve into DPI, which provides a convenient method for referencing. In the realm of calligraphy, font size is quantified in dots, where a single dot equates to 1/72 of an inch. Therefore, 1 point is equivalent to 1/72 inch.

For instance, consider a 24-point typeface intended to be contained within a 1/3" (24/72) space. Nonetheless, not all characters within the font will precisely measure 1/3" in height in practice; certain characters like Å may exceed this standard height. To ensure accuracy, a supplementary area known as the 'leading' is necessary for accommodating such variations.

Consider the 72-point line illustrated in the figure below. The continuous line encloses a 1-inch boundary box surrounding the text, whereas the dotted line marks the origin point, where the majority of alphabet lines intersect. The total height of the text comprises both the area above (ascending) and below (descending) the original text baseline.

In real-world scenarios on computer screens, determining text dimensions can be challenging as pixel sizes can vary. The size of a pixel is influenced by the resolution of the display and the physical size of the monitor. This makes utilizing physical inches as a measurement unit unfeasible since there isn't a fixed correlation between inches and pixels. Instead, measurements are made logically, with a 72-point line being established as the equivalent of one logical inch. These logical inches are then converted into pixels. Historically, Windows employed a system where one logical inch equated to 96 pixels, resulting in a 72-point font representing 96 pixels and a 12-point font being 16 pixels tall.

This conversion ratio is commonly referred to as 96 dots per inch (DPI), although it is more precisely defined as 96 pixels per logical inch. Due to variations in pixel dimensions, content that is legible on one screen might appear too small on another, prompting users to opt for larger text and images. To address this issue, Windows provides a feature that enables users to adjust the DPI settings. For instance, a 72-point character's height at 144 DPI corresponds to 144 pixels. The standard DPI options encompass 100% (96 DPI), 125% (120 DPI), and 150% (144 DPI), granting users the ability to choose from these presets. Since Windows 7, individual users can configure their DPI settings as per their preferences.

DWM Scaling

When a program neglects DPI considerations, several issues may arise when using high DPI settings:

  • UI elements may get clipped.
  • Layouts may appear incorrect.
  • Bitmaps and icons may become pixelated.
  • Mouse coordinates might be inaccurate, affecting functions like hit testing and drag-and-drop operations.

The Desktop Window Manager (DWM) implements a useful fallback mechanism to maintain compatibility with legacy configurations on high DPI displays. In cases where a specific process lacks DPI awareness, DWM intervenes by mandating uniform DPI adherence across the entire user interface. This means that at 144 DPI, the UI undergoes a 150% transformation affecting text, graphics, controls, and window sizes. To illustrate, if an application generates a 500 × 500 window, it will be displayed as 750 × 750 pixels, with all content scaling proportionately.

While this method guarantees optimal performance of the previous system at increased DPI configurations, it does lead to some blurriness caused by the scaling that occurs post window rendering.

DPI-aware Applications

To avoid DWM scaling, a software can identify itself as DPI-aware, directing DWM to refrain from automatic DPI adjustments. Every fresh application should possess DPI awareness to enhance user interface sharpness on higher DPI configurations.

DPI information is determined by an application manifest, which can be found in a DLL or XML file outlining the application's attributes. Typically located within the executable file, the manifest may also be present as an independent file. It includes crucial details like DLL dependencies, requested privilege levels, and compatibility with different versions of Windows.

If you intend to specify DPI awareness for your system, ensure that the manifest contains the subsequent details.

Syntax

Example

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" 
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" > 
  <asmv3:application> 
    <asmv3:windowsSettings 
xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings"> 
      <dpiAware>true</dpiAware>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

The list displayed only represents a partial display; the Visual Studio linker automatically generates the rest. Follow these steps in Visual Studio to add a manifest component to your project:

  • Navigate to the Project menu and select Properties.
  • In the left pane, expand Configuration Properties, then Manifest Tool, and finally click on Input and Output.
  • Within the Additional Manifest Files text box, input the name of the manifest file, and then confirm with OK.

When you mark your application as DPI-aware, you are directing the Desktop Window Manager (DWM) to adjust your application window size according to DPI configurations. Consequently, if you create a window with dimensions of 500 × 500 pixels, it will maintain that size irrespective of the user's DPI preference.

GDI and DPI

GDI graphics are raster-based, indicating that when your software is designated as DPI-aware and you direct GDI to render a 200 × 100 pixel rectangle, the displayed rectangle will appear unchanged on the screen, however, the text within the GDI will enlarge in accordance with the prevailing DPI settings. For instance, a 72-point font will measure 96 pixels at 96 DPI, but will expand to 144 pixels at 144 DPI. Here's a demonstration of a 72-point font rendered at 144 DPI utilizing GDI.

When your software is DPI-aware and uses GDI for rendering, make sure to adjust all drawing coordinates to match the DPI configuration.

Direct2D and DPI

In Direct2D, scaling is automatically adjusted based on the DPI configuration. Dimensions are specified in device-independent pixels (DIPs), where each DIP equals 1/96th of a logical inch. Every drawing operation in Direct2D is initially described in DIPs and then converted to conform with the standard DPI values.

DPI setting DIP size
96 1 pixel
120 1.25 pixels
144 1.50 pixels

Let us understand by example. For example, if the user's DPI setting stays at 144 DPI and you force Direct2D to create a rectangle of 200 × 100, the resulting rectangle will occupy 300 × 150 physical pixels. Also, DirectWrite measured font sizes in DIPs in than points. To create a 12-point font, you must specify a DIP of 16 (because 12 points are 1/6 logical inches, which is equivalent to a DIP of 96/6). Direct2D automatically converts the DIPs into physical pixels as text is rendered on the screen. The adoption of this method ensures accuracy in measurement data and imaging practices regardless of the current DPI setting.

Nonetheless, it is crucial to highlight that the mouse and window coordinates are provided in physical pixels rather than Device Independent Pixels (DIP). When handling the WM_LBUTTONDOWN message, the mouse-down event's state is presented in physical pixels. To plot a point in that specific location, you need to initially transform the pixel coordinates into DIPs.

How to Convert Physical Pixels to DIPs?

When converting physical pixels to DIP, the pixel scaling is fine-tuned to align with the display's DPI settings. This adaptability guarantees a uniform conversion process regardless of DPI variations.

The equation for transforming physical pixels into Density-Independent Pixels (DIPs) is:

Example

DIPs = Physical Pixels / DPI

Where:

  • DIPs are the measurement in device-independent pixels.
  • Physical Pixels
  • Physical Pixels are the measurement of physical pixels.
  • DPI is the dots per inch setting of the display.

For instance, in a scenario where the display has a DPI configuration of 144 and there is a need to transform a measurement of 288 physical pixels into DIPs:

Example

DIPs = 288 Pixels / 144 DPI = 2 DIPs

So, 288 physical pixels are equal to 2 density-independent pixels on a screen with a DPI configuration of 144.

The standard DPI setting, known as USERDEFAULTSCREENDPI, equals 96. To determine the scaling ratio, divide the current DPI value by USERDEFAULTSCREENDPI.

Invoke the GetDpiForWindow function to retrieve the DPI configurations. The DPI value will be provided as a decimal number. Determine the appropriate scaling ratio for both dimensions accordingly.

Example

ffloat g_DPIScale = 1.0f;
void InitializeDPIScale(HWND hwnd)
{
    float dpi = GetDpiForWindow(hwnd);
    g_DPIScale = dpi / USER_DEFAULT_SCREEN_DPI;
}
template <typename T>
float PixelsToDipsX(T x)
{
    return static_cast<float>(x) / g_DPIScale;
}
template <typename T>
float PixelsToDips(T y)
{
    return static_cast<float>(y) / g_DPIScale;
}

Explanation

The code snippet above sets the initial value of the global scaling variable 'gDPIScale' by extracting the DPI value from the window. It includes utility functions, 'PixelsToDipsX' and 'PixelsToDips', that transform pixel coordinates into device-independent pixels (DIPs) through the 'gDPIScale' scaling factor.

Note: It is recommended to use GetDpiForWindow for desktop apps and DisplayInformation::LogicalDpi for Universal Windows Platform (UWP) apps. Although the default DPI awareness can be configured using SetProcessDpiAwarenessContext, this is not encouraged. Once the window (HWND) is created in your project, changing the DPI awareness mode will not be helpful. If you need to configure the process-default DPI awareness mode programmatically, be sure to call the corresponding API before creating any HWND.

Resizing the Render Target

When the size of the window is altered, it is essential to adjust the rendering value correspondingly. In many cases, there is a necessity to modify the layout and refresh the windows. The guidelines provided below demonstrate these processes.

Example

void MainWindow::Resize()
{
    if (pRenderTarget != NULL)
    {
        RECT rc;
        GetClientRect(m_hwnd, &rc);
        D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom);
        pRenderTarget->Resize(size);
        CalculateLayout();
        InvalidateRect(m_hwnd, NULL, FALSE);
    }
}

Explanation

The provided code snippet explains a function called 'Resize' within the 'MainWindow' class. It verifies the activation status of the render target 'pRenderTarget', obtains the dimensions of the window's client area, adjusts the render target size accordingly, recalculates the layout, and triggers a repaint of the window's client area. This function guarantees that the output image remains consistent even as the window dimensions change appropriately.

The GetClientRect function fetches the current dimensions of the client area in physical pixels rather than DIPs. Subsequently, the ID2D1HwndRenderTarget::Resize method adjusts the render target size, also indicated in pixels. To initiate a redraw, the InvalidateRect function flags the complete client area for refreshing. When the window's shape alters, it's typical for the positions of rendered elements to be recomputed. In a scenario like a circular layout, modifying the circle's radius and center point becomes essential.

Example

void MainWindow::CalculateLayout()
{
if (pRenderTarget != NULL)
    {
        D2D1_SIZE_F size = pRenderTarget->GetSize();
const float x = size.width / 2;
const float y = size.height / 2;
const float radius = min(x, y);
        ellipse = D2D1::Ellipse(D2D1::Point2F(x, y), radius, radius);
    }
}

Explanation

The provided code determines the position of the ellipse rendered on the Direct2D render target. It analyzes the dimensions of the target, computes the center point and radii, and utilizes this information to generate an ellipse.

The ID2D1RenderTarget::GetSize function fetches the dimensions of the render target in DIPs (device-independent pixels), which are ideal for performing configuration computations.

On the other hand, ID2D1RenderTarget::GetPixelSize returns dimensions in actual pixels. While this aligns with the logic behind the HWND render value obtained from GetClientRect, it's important to remember that drawing operations occur in DIPs, not physical pixels.

Using Color in Direct2D

Direct2D utilizes the RGB color model, generating a variety of colors by blending varying levels of red, green, and blue intensities.

Moreover, a fourth attribute called alpha plays a crucial role in defining the transparency level of the pixel. In Direct2D, these attributes are represented by floating-point numbers ranging from 0.0 to. In the context of color components, this scale signifies the intensity of the color, with 0.0 representing complete transparency and 1.0 denoting full opacity. Refer to the table provided for a visual representation of colors achieved through different combinations at maximum intensity.

Red Green Blue Color
0 0 0 Black
1 0 0 Red
0 1 0 Green
0 0 1 Blue
0 1 1 Cyan
1 0 1 Magenta
1 1 0 Yellow
1 1 1 White

Values ranging from 0 to 1 yield a variety of shades within these primary colors. Direct2D employs the D2D1COLORF structure to depict colors. For instance, the code below defines the color magenta.

Example

// To initialize a magenta color.
    D2D1_COLOR_F clr;
    clr.r = 1;
    clr.g = 0;
clr.b = 1;
    clr.a = 1;  
// Opaque.

Explanation

The code excerpt above generates a D2D1COLORF structure that defines the color magenta, characterized by maximum intensity values for red and blue, and minimum intensity for green.

You have the option to specify a color by utilizing the D2D1::ColorF class, which is derived from the D2D1COLORF structure.

Example

D2D1::ColorF clr(1, 0, 1, 1)

Alpha Blending

Alpha blending merges transparent areas by mixing the foreground hue with the background hue, utilizing a particular mathematical equation:

Example

color = af * Cf + (1 - af) * Cb

In the provided equation, Cb represents the background color, Cf indicates the foreground color, and af signifies the alpha value of the foreground color. This computation is implemented separately for each color channel. For example, if the foreground color is (R = 1.0, G = 0.4, B = 0.0) with an alpha of 0.6, and the background color is (R = 0.0, G = 0.5, B = 1.0), the resultant alpha-mixed color will be:

Example

R = (1.0 * 0.6 + 0 * 0.4) = .6 
G = (0.4 * 0.6 + 0.5 * 0.4) = .44 
B = (0 * 0.6 + 1.0 * 0.4) = .40

Pixel Formats

The D2D1COLORF structure does not define the storage of a pixel in memory, a detail usually handled internally by Direct2D. Familiarity with pixel formats is crucial for tasks like manipulating memory bitmaps or combining Direct2D with Direct3D or GDI. The DXGI_FORMAT enumeration showcases different pixel formats, yet only a portion applies to Direct2D, with the rest being more commonly associated with Direct3D.

Pixel Format Description
DXGIFORMATB8G8R8A8_UNORM This format is widely used and considered the standard pixel layout. It comprises 8-bit unsigned integers for each of the four-pixel components: red, green, blue, and alpha. These components are arranged in memory using the BGRA sequence.
DXGIFORMATR8G8B8A8_UNORM This format's pixel components consist of 8-bit unsigned integers arranged in the RGBA sequence. In contrast to DXGIFORMATB8G8R8A8_UNORM, the red and blue components are swapped. Additionally, this format is exclusively supported for hardware devices.
DXGIFORMATA8_UNORM This format exclusively contains an 8-bit alpha component without any RGB components. It is useful for generating opacity masks.

The illustration below depicts the BGRA pixel arrangement.

Retrieve the pixel format of the render target by utilizing the ID2D1RenderTarget::GetPixelFormat method. Keep in mind that the pixel format used by the render target might not match the display resolution. For instance, the display could be set to 16-bit color depth whereas the render target operates with 32-bit color depth.

Alpha Mode

A render target also includes an alpha mode, which dictates how alpha values are handled.

Alpha Mode Description
D2D1ALPHAMODE_IGNORE This property specifies that no alpha mixing is performed, and alpha values are ignored.
D2D1ALPHAMODE_STRAIGHT This means that alpha mixing is done without any gamma correction.
D2D1ALPHAMODE_PREMULTIPLIED Indicates that alpha mixtures are being generated with preformed alpha values. This means that the colors are already multiplied by the alpha value, resulting in smooth blending and avoiding coloring the edges of transparent objects.

Use the following example to consider the difference between a straight alpha and a pre-conjugated alpha: Let's say we aim for pure red at full intensity (100%) of 50%, which appears in Direct2D; this column is shown as (1, 0, 0, 0.5). When using direct alpha and capturing 8-bit color components, the red portion of the pixel remains at 0xFF. However, if alpha is multiplied first, the blood component decreases by 50% to 0x80.

Notably, the D2D1COLORF data type employs a fixed direct alpha to depict colors, whereas Direct2D transforms pixels to a pre-multiplied alpha format when required. If alpha blending is unnecessary in your application, opting for it is advisable; contemplate establishing a render target with the D2D1ALPHAMODE_IGNORE mode, as this action can potentially enhance performance by enabling Direct2D to bypass alpha computations.

How to Apply Transforms in Direct2D?

In the earlier part, we delved into Drawing with Direct2D. It was discussed that the ID2D1RenderTarget::FillEllipse function is employed to render ellipses aligned with the x and y axes. Nonetheless, envision a scenario where you need to draw an ellipse at an inclination.

Utilizing transforms, you can modify a shape through various operations, including:

  • Rotation around a point
  • Scaling
  • translation (movement along the X or Y axis)
  • skew (also referred to as shear).

A transformation is a mathematical process that rearranges a group of points into a different configuration. For instance, take into account the illustration underneath, displaying a deformed rectangle surrounding point P3. Upon reversal, point P1 stays unaltered, point P1' shifts, point P2 remains the same, point P2' changes, and point P3 retains its position.

Transformations are made through matrices in Direct2D, although their use does not require a deep understanding of matrix computation. If you are interested in diving into the mathematical aspect, see the "Appendix: Shape Conversion" section. To apply the transform in Direct2D, pass the ID2D1RenderTarget::SetTransform method, which accepts the D2D1MATRIX3X2_F structure that defines the transform. Methods in the D2D1::Matrix3x2F class can be initialized to this framework, which provides static methods for constructing matrices for variables.

  • Matrix3x2F::Rotation
  • Matrix3x2F::Scale
  • Matrix3x2F::Translation
  • Matrix3x2F::Skew

As an example, the following code performs a rotation of 20 degrees around the point (100, 100).

Example

pRenderTarget->SetTransform( D2D1::Matrix3x2F::Rotation(20, D2D1::Point2F(100,100)));

The change will be applied to all future drawing actions until a different SetTransform function is called. To reset the current transformation, utilize SetTransform with a unit matrix, which is obtainable through the Matrix3x2F::Identity method.

Example

pRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());

Drawing Clock Hands

Let's apply the conversion by changing our Circle program into a traditional clock. This involves incorporating the alphabets that symbolize the hour hand.

Instead of computing coordinates directly for lines, we have the option to calculate an angle first and then implement a rotation transformation. The following code snippet illustrates how to render a single clock hand. The variable fAngle denotes the angle of the hand, specified in degrees.

Example

void Scene::DrawClockHand(float fHandLength, float fAngle, float
fStrokeWidth)
{
    m_pRenderTarget->SetTransform(
        D2D1::Matrix3x2F::Rotation(fAngle, m_ellipse.point)
            );
    // endPoint defines one end of the hand.
    D2D_POINT_2F endPoint = D2D1::Point2F(
        m_ellipse.point.x,
        m_ellipse.point.y - (m_ellipse.radiusY * fHandLength)
        );

    m_pRenderTarget->DrawLine(
        m_ellipse.point, endPoint, m_pStroke, fStrokeWidth);
}

Explanation

The code snippet above illustrates a vertical line extending from the clock face to its edge. This rotation is achieved through a transformation around the ellipse's midpoint, symbolizing the center of the clock.

The following code exemplifies the process of rendering the complete clock interface.

Example

void Scene::RenderScene()
{
    m_pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::SkyBlue));
    m_pRenderTarget->FillEllipse(m_ellipse, m_pFill);
    m_pRenderTarget->DrawEllipse(m_ellipse, m_pStroke);
    // Draw hands
    SYSTEMTIME time;
    GetLocalTime(&time);

    const float fHourAngle = (360.0f / 12) * (time.wHour) + (time.wMinute *
0.5f);
    const float fMinuteAngle =(360.0f / 60) * (time.wMinute);
    DrawClockHand(0.6f,  fHourAngle,   6);
    DrawClockHand(0.85f, fMinuteAngle, 4);
    // Restore the identity transformation.
    m_pRenderTarget->SetTransform( D2D1::Matrix3x2F::Identity() );
}

Explanation

The program above demonstrates the clock display. It commences by clearing the render target with a sky-blue hue. Subsequently, it populates and captures the circular face of the timepiece. It then showcases the hour and minute hand visuals according to the present time. Lastly, it employs the DrawClockHand function to illustrate the hour and minute pointers and retrieve the identity variable.

Combining Transforms

Generating multiple matrices enables the consolidation of various fundamental variables. In this instance, the following code merges rotation and translation.

Example

const D2D1::Matrix3x2F rot = D2D1::Matrix3x2F::Rotation(20);
const D2D1::Matrix3x2F trans = D2D1::Matrix3x2F::Translation(40, 10);
pRenderTarget->SetTransform(rot * trans);

Explanation

In the provided code snippet, the Matrix3x2F class provides the operator* function specifically for performing matrix multiplication, with a crucial emphasis on the order of operations. When applying a transformation (M × N), it signifies that "M is applied first, followed by N." An illustrative example involves rotation succeeded by translation:

See the below program for this transformation.

Example

const D2D1::Matrix3x2F rot = D2D1::Matrix3x2F::Rotation(45, center);
const D2D1::Matrix3x2F trans = D2D1::Matrix3x2F::Translation(x, 0);
pRenderTarget->SetTransform(rot * trans);

Now, let's compare this with a reverse sequence transformation: first translating and then rotating.

The rotation is centered around the midpoint of the initial rectangle. Presented below is the code snippet for executing this transformation.

Example

D2D1::Matrix3x2F rot = D2D1::Matrix3x2F::Rotation(45, center);
D2D1::Matrix3x2F trans = D2D1::Matrix3x2F::Translation(x, 0);
pRenderTarget->SetTransform(trans * rot);

You may observe that the matrices stay the same, but the order of operations has changed. This difference arises from the non-commutative property of matrix multiplication: M × N ≠ N × M.

Module 4: User Input:

This part will detail the process of managing mouse and keyboard input. Integrate user engagement functionalities into your application. The following terms will be highlighted for discussion:

Mouse Input

A mouse is compatible with Windows operating system. It comes with a total of five buttons: the usual three (left, middle, and right) and two extra buttons identified as XBUTTON1 and XBUTTON2.

In Windows operating system, most computer mice come equipped with both the left and right buttons. The primary function of the left mouse button includes tasks such as dragging items, selecting text or objects, and pointing to specific locations on the screen. On the other hand, the right mouse button is commonly utilized to bring up contextual menus for additional options. Positioned between the left and right buttons, some mice feature a scroll wheel that enables users to navigate through content seamlessly. The scroll wheel, which serves as the center button, may also be clickable, providing an extra input option based on the specific mouse model.

Responding to Mouse Clicks

If a user clicks a mouse button within the client area, the window will display one of the messages listed below.

Message Meaning
WM_RBUTTONDOWN Right button down
WM_RBUTTONUP Right button up
WM_LBUTTONDOWN Left button down
WM_LBUTTONUP Left button up
WM_MBUTTONDOWN Middle button down
WM_MBUTTONUP Middle button up
WM_XBUTTONDOWN XBUTTON1 or XBUTTON2 down
WM_XBUTTONUP XBUTTON1 or XBUTTON2 up

Coordinates

Each notification contains the x and y coordinates of the mouse pointer in the lParam parameter. The x-coordinate is stored in the least significant 16 bits of lParam, and the y-coordinate is stored in the subsequent 16 bits. To retrieve these coordinates from lParam, the GETXLPARAM and GETYLPARAM macros should be employed.

Example

int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);

Explanation

In this code snippet, the GETXLPARAM and GETYLPARAM macros are commonly employed in Windows development to retrieve the x and y coordinates from the LPARAM value. LPARAM is a 32-bit data structure that holds the position of the mouse cursor when the message was triggered.

Double Clicks

By default, double-click notifications are not directed to a window. To enable receiving double-click alerts, ensure to include the CS_DBLCLKS flag in the WNDCLASS structure when registering the window class.

Example

WNDCLASS wc = { };
 wc.style = CS_DBLCLKS;
 /* Set other structure members. */
 RegisterClass(&wc);

Explanation

The preceding code snippet initializes a 'WNDCLASS' structure labeled as 'wc' with preset values and assigns the 'style' attribute to 'CS_DBLCLKS', signaling the window class to produce double-click messages. Following the configuration of additional structure components, the class is then registered utilizing the 'RegisterClass' function.

Double-click notifications will be displayed in the window if the CS_DBLCLKS flag is set as indicated. When you double-click, a window message with the name "DBLCLK" appears. For instance, the following messages appear when the left mouse button is twice clicked:

  • WM_LBUTTONDOWN
  • WM_LBUTTONUP
  • WM_LBUTTONDBLCLK
  • WM_LBUTTONUP

Essentially, a WMLBUTTONDBLCLK message supersedes the second WMLBUTTONDOWN message that is usually generated. Corresponding messages are also designated for the middle, right, and XBUTTON buttons.

Non-Client Mouse Messages

When mouse interactions happen in the nonclient area of the window, a distinct group of notifications is specified. The abbreviations "NC" are included in the titles of these messages. For example, the non-client equivalent of WMLBUTTONDOWN is denoted as WMNCLBUTTONDOWN.

Mouse Movement

Windows generates a WM_MOUSEMOVE message each time the mouse is moved. By default, this message is directed to the window under the cursor. Subsequent content will elaborate on how intercepting mouse input can alter this default action.

The arguments of the WMMOUSEMOVE message mirror those of the mouse-click messages. The x-coordinate resides in the least significant 16 bits of lParam, whereas the y-coordinate is stored in the subsequent 16 bits. To retrieve the coordinates from lParam, leverage the GETXLPARAM and GETY_LPARAM macros. The wParam parameter encompasses a bitwise OR of indicators denoting the statuses of the SHIFT, CTRL keys, and additional mouse buttons. Subsequently, the provided code utilizes lParam to fetch the mouse coordinates.

Example

int xPos = GET_X_LPARAM(lParam);
int yPos = GET_Y_LPARAM(lParam);

Mouse Movement Outside the Window

WM_MOUSEMOVE messages are automatically prevented from being dispatched to a window once the mouse moves outside the client area boundaries. Nonetheless, monitoring the mouse's position could be essential for executing subsequent operations. For example, in the illustration provided, a graphic design tool might enable users to extend the selection rectangle beyond the window's confines.

Left Button Down

Follow these steps to address the left-button-down message:

  • To start capturing the mouse, call SetCapture.
  • Save the mouse click location in the ptMouse variable. This point defines the top left corner of the ellipse's bounding box.
  • Restore the ellipse framework.
  • Use InvalidateRect. This feature necessitates repainting the window.
Example

void MainWindow::OnLButtonDown(int pixelX, int pixelY, DWORD flags)
{
 SetCapture(m_hwnd);
 ellipse.point = ptMouse = DPIScale::PixelsToDips(pixelX, pixelY);
 ellipse.radiusX = ellipse.radiusY = 1.0f;
 InvalidateRect(m_hwnd, NULL, FALSE);
}

Explanation

The preceding code functions as an event handler triggered by pressing the left mouse button within a window. It seizes control of the mouse, adjusts the position of an ellipse to match the mouse cursor's location, sets the ellipse's radius to 1, and prompts a window redraw by invalidating it.

Mouse Move

Check if the primary mouse button is pressed to trigger the mouse movement notification. If affirmative, refresh the window display and recompute the ellipse. In Direct2D, an ellipse is characterized by its x-radius, y-radius, and center coordinates. Our objective is to render an ellipse that perfectly encloses the bounding rectangle.

The width, height, and position of the ellipse are established based on the initial mouse-down point (ptMouse) and the current cursor coordinates (x, y). This necessitates performing certain mathematical calculations to determine these properties. The code below triggers the use of InvalidateRect to refresh the window following the recalculation of the ellipse.

Example

void MainWindow::OnMouseMove(int pixelX, int pixelY, DWORD flags)
{
if (flags & MK_LBUTTON)
{
const D2D1_POINT_2F dips = DPIScale::PixelsToDips(pixelX, pixelY); const float width = (dips.x - ptMouse.x) / 2;
const float height = (dips.y - ptMouse.y) / 2;
const float x1 = ptMouse.x + width;
const float y1 = ptMouse.y + height;
ellipse = D2D1::Ellipse(D2D1::Point2F(x1, y1), width, height);
InvalidateRect(m_hwnd, NULL, FALSE);
}
}

Explanation

The provided code functions as an event handler specifically designed for tracking mouse movements. It verifies whether the left mouse button is being clicked. Once confirmed, the code proceeds to determine the dimensions of an ellipse by analyzing the variance between the current mouse coordinates ('pixelX', 'pixelY') and a prior position ('ptMouse'). Subsequently, it generates an ellipse using the computed values and triggers a window refresh for updating the display.

Left Button Up

To receive the message for the left button being released, invoke ReleaseCapture to release the mouse capture.

Example

void MainWindow::OnLButtonUp()
{
ReleaseCapture();
}

Explanation

The preceding code snippet introduces a method named 'OnLButtonUp' inside the 'MainWindow' class. Upon invocation, this function relinquishes the mouse capture, enabling other windows or controls to take control of mouse input events.

Mouse Operations

In this part, we will explore additional crucial mouse functions that are executable using the mouse device.

Dragging UI Elements

You ought to incorporate DragDetect as an extra technique within your mouse-down message handler if your interface permits users to move UI elements. When a user executes a mouse gesture indicating a drag action, DragDetect will yield a TRUE result. The subsequent code snippet illustrates the utilization of this function.

Example

case WM_LBUTTONDOWN:
 {
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
if (DragDetect(m_hwnd, pt))
 {

 // Start dragging.

 }
 }

 return 0;

Explanation

The provided code manages the event when the left mouse button is pressed down. It identifies whether a dragging action should commence by analyzing the mouse's movement following the button press. When a drag operation is recognized, it triggers the dragging process.

Confining the Cursor

There may be scenarios where restricting the cursor's movement to a specific area is necessary. The ClipCursor function confines the cursor within a defined rectangular region. As this region is specified in screen coordinates, with the top-left corner represented by the point (0, 0), the ClientToScreen method is employed to convert client coordinates into screen coordinates. By implementing the code below, the cursor's movement is constrained to the client area of the window.

Example

// to get the window client area.
 RECT rc;
 GetClientRect(m_hwnd, &rc);

 // Convert the client area to screen coordinates.
 POINT pt = { rc.left, rc.top };
 POINT pt2 = { rc.right, rc.bottom };
 ClientToScreen(m_hwnd, &pt);
 ClientToScreen(m_hwnd, &pt2);
 SetRect(&rc, pt.x, pt.y, pt2.x, pt2.y);

 // Confine the cursor.
 ClipCursor(&rc);

//Use the value NULL when using ClipCursor to remove the restriction:

ClipCursor(NULL);

Explanation

The code snippet above retrieves the size of the client area in the window, transforms it into screen coordinates, and subsequently limits the cursor's movement within that region.

Mouse Tracking Functions: Hover & Leave

Although not activated as a default setting, two additional mouse messages could be advantageous in particular scenarios:

  • WM_MOUSEHOVER indicates that the cursor has remained over the client area for a set duration.
  • WM_MOUSELEAVE signifies that the cursor is no longer within the client area.

Utilize the TrackMouseEvent function to enable these messages.

Example

TRACKMOUSEEVENT tme;
 tme.cbSize = sizeof(tme);
 tme.hwndTrack = hwnd;
 tme.dwFlags = TME_HOVER | TME_LEAVE;
 tme.dwHoverTime = HOVER_DEFAULT;
 TrackMouseEvent(&tme);

Explanation

The provided code configures monitoring for mouse hover and leave actions on a designated window ('hwnd'). It establishes a 'TRACKMOUSEEVENT' framework with essential settings like the window handle, indicators for hover and leave actions, and the default hover duration. Subsequently, the function 'TrackMouseEvent' is invoked to commence tracking mouse events.

You can employ this small utility class to manage mouse-tracking events.

Example

TRACKMOUSEEVENT tme;
 tme.cbSize = sizeof(tme);
 tme.hwndTrack = hwnd;
 tme.dwFlags = TME_HOVER | TME_LEAVE;
 tme.dwHoverTime = HOVER_DEFAULT;
 TrackMouseEvent(&tme);
class MouseTrackEvents
{
 bool m_bMouseTracking;
public:
 MouseTrackEvents() : m_bMouseTracking(false)
 {
 }

void OnMouseMove(HWND hwnd)
 {
 if (!m_bMouseTracking)
 {
 // Enable mouse tracking.
 TRACKMOUSEEVENT tme;
 tme.cbSize = sizeof(tme);
 tme.hwndTrack = hwnd;
 tme.dwFlags = TME_HOVER | TME_LEAVE;
 tme.dwHoverTime = HOVER_DEFAULT;
 TrackMouseEvent(&tme);
 m_bMouseTracking = true;
 }
 }
 void Reset(HWND hwnd)
 {
 m_bMouseTracking = false;
 }
};

Explanation

The provided code facilitates monitoring mouse hover and exit events on a window. Upon invoking the 'OnMouseMove' function, mouse tracking is configured if it's not yet active. To revert the tracking state, the 'Reset' function is utilized. The 'TRACKMOUSEEVENT' construct defines the tracking criteria.

Implementing this class within your window process is illustrated in the subsequent example.

Example

LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
 switch (uMsg)
 {
 case WM_MOUSEMOVE:
 mouseTrack.OnMouseMove(m_hwnd); // Start tracking.
 // TODO: Handle the mouse-move message.
 return 0;
 case WM_MOUSELEAVE:
 // TODO: Handle the mouse-leave message.
 mouseTrack.Reset(m_hwnd);
 return 0;
 case WM_MOUSEHOVER:
 // TODO: Handle the mouse-hover message.
 mouseTrack.Reset(m_hwnd);
 return 0;
 }
 return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}

Explanation

The provided code serves as an illustration of a message processor designed for a window category. It manages messages associated with mouse actions like movement ('WMMOUSEMOVE'), mouse exiting the window ('WMMOUSELEAVE'), and mouse hovering on the window ('WM_MOUSEHOVER'). The handler initiates or restarts mouse tracking as needed, signaling the successful handling of the message by returning a value of 0. In cases where the message does not pertain to the specified mouse events, it delegates the message processing task to the default window procedure 'DefWindowProc'.

If mouse tracking events are unnecessary, it is advisable to keep them disabled to avoid additional system processing. Below is a technique that queries the system for the default hover timeout duration as a comprehensive approach.

Example

UINT GetMouseHoverTime()
{
 UINT msec;
 if (SystemParametersInfo(SPI_GETMOUSEHOVERTIME, 0, &msec, 0))
 {
 return msec;
 }
 else
 {
 return 0;
 }
}

Explanation

The code mentioned above fetches the length of time in milliseconds that the mouse cursor needs to stay still on a user interface component before activating a hover action. It employs the 'SystemParametersInfo' function alongside the 'SPI_GETMOUSEHOVERTIME' parameter to fetch this information from the operating system configurations. Upon a successful operation, it provides the hover duration; otherwise, it outputs 0.

Mouse Wheel

The function provided below verifies the presence of a mouse wheel.

Example

BOOL IsMouseWheelPresent()
{
 return (GetSystemMetrics(SM_MOUSEWHEELPRESENT) != 0);

}

Explanation

The provided code verifies the presence of a mouse wheel in the system and outputs true if it is available; otherwise, it outputs false. Whenever the user rotates the mouse wheel, a WM_MOUSEWHEEL message is dispatched to the currently focused window. The wParam parameter of this message carries an integer value known as the delta, representing the distance the wheel has been moved. The delta is measured in arbitrary units, with 120 units equivalent to the completion of a single "action."

There are two delta symbols indicating the rotation's direction.

  • Positive: Rotating in the forward direction
  • Negative: Rotating in the reverse direction

The delta value is stored in wParam along with extra flags. To retrieve the delta value, you can utilize the GETWHEELDELTA_WPARAM macro.

Example

int delta = GET_WHEEL_DELTA_WPARAM(wParam);

Explanation

This code snippet retrieves the mouse wheel scrolling distance and direction by accessing the 'wParam' parameter with the 'GETWHEELDELTA_WPARAM' macro. The variable 'delta' signifies the amount of movement made by the mouse wheel, indicating both the distance and the scroll direction.

Keyboard Input

The keyboard is used for several types of commands, which are:

  • Character Input
  • Keyboard Shortcuts
  • System Commands
  • Note: It's worth considering that pressing the letter A from your keyboard doesn't just count as A; For example, it can be a, a, or a. Holding down the ALT key makes the keystroke ALT+A, which the system interprets as a command rather than a character.

    Key Codes

Key codes are numerical values assigned to individual keys on a keyboard. These codes play a crucial role in identifying keystrokes within computer systems. They are essential for interpreting user input in a wide range of software, games, operating systems, and driver programs. The specific key codes assigned to keys can differ based on the platform, operating system, and programming language being utilized.

Virtual-Key Codes:

In Windows development, virtual key codes are symbols that correspond to specific keys on the keyboard. These codes are established within the Windows API and are frequently employed in functions that manage messages like WMKEYDOWN and WMKEYUP. For instance, the virtual key code for the left ARROW KEY is VKLEFT (0x25). Similarly, VKRETURN is used for the Enter key, VK_SPACE for the Spacebar, and many more.

ASCII Codes:

It encodes characters and symbols with either 7 or 8 bits. Most of the visible characters on the keyboard are associated with an ASCII value. For instance, the ASCII value for 'A' is 65, 'B' is 66, '1' is 49, '2' is 50, and so forth.

Scan Codes:

Scan codes are unique codes produced by the keyboard controller upon pressing or releasing a key. These codes are transmitted to the computer's processor and subsequently converted into key codes by the operating system. Unlike virtual-key codes, scan codes operate at a lower level and are obtained directly from the keyboard's hardware. It's important to note that scan codes can differ depending on the keyboard type and configurations.

Unicode:

Unicode serves as a character encoding standard that assigns distinct numerical values to characters and symbols utilized in various writing systems globally. Although it is not directly tied to keyboard input, Unicode frequently finds application in software development for the purpose of encoding and managing textual input derived from keyboards.

Key Down and Key Up Messages

One of the messages displayed in the active window upon pressing a key is

  • WM_SYSKEYDOWN, while another is
  • WM_KEYDOWN.

A system key is a key press that triggers a system command, demonstrated by the WM_SYSKEYDOWN message. There are two classifications of system keys:

  • ALT + Any Key

Pressing the F10 key triggers the menu bar in an active window. Various alternative ALT keys execute system commands. For instance, pressing ALT + TAB allows you to switch to a different window. Nevertheless, certain ALT key combinations have no function.

Character Messages

The TranslateMessage function transforms keystrokes into characters by scanning key-down messages and converting them. Each generated character is represented by a WMCHAR or WMSYSCHAR message added to the message queue of the window. The UTF-16 character can be found in the message's wParam field.

Explore different keyboard combinations to observe the primary keyboard messages displayed by the debugger in the following code snippet. Try varying the keys pressed to uncover the different messages that are triggered.

Example

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM
lParam)
{
 wchar_t msg[32];
 switch (uMsg)
 {
 case WM_SYSKEYDOWN:
 swprintf_s(msg, L"WM_SYSKEYDOWN: 0x%x\n", wParam);
 OutputDebugString(msg);
 break;

 case WM_SYSCHAR:
 swprintf_s(msg, L"WM_SYSCHAR: %c\n", (wchar_t)wParam);
 OutputDebugString(msg);
 break;

 case WM_SYSKEYUP:
 swprintf_s(msg, L"WM_SYSKEYUP: 0x%x\n", wParam);
OutputDebugString(msg);
 break;

 case WM_KEYDOWN:
 swprintf_s(msg, L"WM_KEYDOWN: 0x%x\n", wParam);
 OutputDebugString(msg);
 break;
 
case WM_KEYUP:
 swprintf_s(msg, L"WM_KEYUP: 0x%x\n", wParam);
 OutputDebugString(msg);
 break;

 case WM_CHAR:
 swprintf_s(msg, L"WM_CHAR: %c\n", (wchar_t)wParam);
 OutputDebugString(msg);
 break;
 
/* Handle other messages (not shown) */
 }
 return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}

Explanation

The provided code establishes a window procedure named 'WindowProc' within a Windows program. This procedure is responsible for handling messages ('uMsg') directed at the window associated with 'hwnd'. Depending on the nature of the message, distinct actions are executed. For instance, upon detecting a key press ('WMKEYDOWN'), the program records a message in the debug output detailing the specific key pressed. Similarly, other message types such as 'WMSYSKEYDOWN', 'WM_CHAR', etc., trigger relevant responses. In cases where the message does not match any predefined scenarios, the default window procedure ('DefWindowProc') is invoked to manage it.

Keyboard Messages

Keyboard prompts are initiated by events. Essentially, whenever a significant action takes place, such as key press, a notification is displayed to inform you about the event.

However, it is also possible to verify the state of a key by employing the GetKeyState function whenever needed. The GetKeyState function is supplied with a virtual key code and in return provides an array of bit flags. The specific bit flag indicating if the key is currently pressed can be found at the position 0x8000.

Example

if (GetKeyState(VK_MENU) & 0x8000)
{
 // ALT key is down.
}

Explanation

The code snippet above verifies the status of the ALT key to determine if it is currently being pressed. If the key is indeed pressed, the expression 'GetKeyState(VK_MENU) & 0x8000' will result in a true value.

Most keyboards are equipped with both a left and a right ALT key. The preceding example verifies whether either of these keys has been pressed. Furthermore, the GetKeyState function can be employed to distinguish between the left and right CTRL, SHIFT, and ALT keys. Subsequently, the provided code examines if the appropriate ALT key has been activated.

Example

if (GetKeyState(VK_RMENU) & 0x8000)
{
 // Right ALT key is down.
}

Explanation

The line of code provided above verifies the current status of the right Alt key being pressed. When the right Alt key is indeed pressed, the expression 'GetKeyState(VK_RMENU) & 0x8000' results in true, prompting the execution of the code enclosed within the curly brackets to signify that the right Alt key is in a pressed state.

GetKeyState captures a snapshot of the keyboard at the moment each message is placed in the queue. This function offers the keyboard's status precisely when the user activates a mouse button, such as when the previous message in the queue was WM_LBUTTONDOWN. Furthermore, GetKeyState excludes keyboard input that was directed to a different application because it relies on your message queue. It also dismisses any key inputs intended for another application if the user switches to it.

Accelerator Tables

A collection of keyboard shortcuts is called an accelerator table. Every shortcut has a definition that is provided by:

  • A number identifier. This number indicates the application command that the shortcut will execute.
  • The shortcut's ASCII character or virtual key code.
  • The modifier keys CTRL, SHIFT, and ALT are optional.

The accelerator table is distinctively recognized within the application resources list through a numerical identifier displayed on the table. We will now create an accelerator table for a simple drawing application. This application will offer two primary modes: drawing and selection. In drawing mode, users can sketch various shapes, while in selection mode, they can pick shapes. Let's define the keyboard shortcuts for this program.

Initially, we will establish numerical labels for both the table and the commands within the application. These designations are random. Following that, we will map out the symbolic variables for these labels by formally introducing them in a header file. As an illustration:

Example

#define IDR_ACCEL1 101
#define ID_TOGGLE_MODE 40002
#define ID_DRAW_MODE 40003
#define ID_SELECT_MODE 40004

Explanation

The code provided above establishes fixed values for an accelerator table and menu items within a Windows program. In particular:

The constant

  • 'IDR_ACCEL1' is set to 101, presumably indicating the resource ID assigned to an accelerator table.

Similarly, the constants 'IDTOGGLEMODE', 'IDDRAWMODE', and 'IDSELECTMODE' are assigned values 40002, 40003, and 40004. These values likely correspond to unique identifiers for menu items or actions associated with switching modes, drawing, and selecting functionalities within the application.

Example

#include "resource.h"
IDR_ACCEL1 ACCELERATORS
{
 0x4D, ID_TOGGLE_MODE, VIRTKEY, CONTROL // ctrl-M
 0x70, ID_DRAW_MODE, VIRTKEY // F1
 0x71, ID_SELECT_MODE, VIRTKEY // F2
}

Explanation

The above code defines an accelerator table named IDR_ACCEL1, which associates keyboard shortcuts with specific commands or actions:

  • Ctrl+M is associated with the IDTOGGLEMODE command.
  • F1 key press is associated with the IDDRAWMODE command.
  • F2 key press is associated with the IDSELECTMODE command.

The curly brackets specify the accelerator shortcuts. Each shortcut has the entries shown below.

  • The ASCII character or virtual key triggers the shortcut.
  • The command for the program. You'll see that the example makes use of symbolic constants.
  • These constants are specified in the resource.h, which is included in the resource definition file.
  • The first element is a virtual key code, as VIRTKEY indicates. Alternatively, you may use ASCII characters.
  • Optional modifiers: ALT, CONTROL, or SHIFT.
  • Note: A lowercase character will have a different shortcut than an uppercase character if you utilize ASCII characters for shortcuts. For example, entering 'a' could result in a different command than entering 'A'. Utilizing virtual key codes for shortcuts rather than ASCII characters is typically preferable because doing so might mislead consumers.

    Load the Accelerator Table

The LoadAccelerators function in Windows development enables the loading of an accelerator table. You provide the accelerator table's name or ID as well as the handle to the application instance (usually obtained using GetModuleHandle(NULL)) as parameters to this function. Below is a demonstration of how it can be used:

Example

HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCEL1));
if (hAccelTable == NULL) {
    // Handle error loading accelerator table
}

Explanation

The preceding code snippet loads an accelerator table resource by employing the 'LoadAccelerators' function. Upon successful execution, it provides a reference to the accelerator table ('hAccelTable'). In case the loading process fails (resulting in 'hAccelTable' being NULL), it is essential to incorporate an error management procedure.

Converting Keystrokes into Command

WM_COMMAND messages are produced from key inputs by utilizing an accelerator table.

The numerical representation of the command is contained within the wParam parameter of the WMCOMMAND message. For example, pressing CTRL+M triggers a WMCOMMAND message carrying the IDTOGGLEMODE value when referring to the earlier displayed table. To implement this, adjust your message loop as follows:

Example

MSG msg;
 while (GetMessage(&msg, NULL, 0, 0))
 {
 if (!TranslateAccelerator(win.Window(), hAccel, &msg))
 {
 TranslateMessage(&msg);
 DispatchMessage(&msg);
 }
 }

Explanation

The code above monitors for messages, converts keyboard shortcuts, and sends messages to their respective window procedure until all messages have been processed.

The sketching program could react to the WM_COMMAND message in various manners:

Example

case WM_COMMAND:
 switch (LOWORD(wParam))
 {
 case ID_DRAW_MODE:
 SetMode(DrawMode);
 break;
 case ID_SELECT_MODE:
 SetMode(SelectMode);
 break;
 case ID_TOGGLE_MODE:
 if (mode == DrawMode)
 {
 SetMode(SelectMode);
 }
 else
 {
 SetMode(DrawMode);
 }
break;
 }
 return 0;
break;
 }
 return 0;

Explanation

The code snippet above manages commands originating from a menu within a window. It validates the command's ID and adjusts the drawing mode correspondingly. When the toggle mode command is activated, it alternates between draw and select modes.

Setting the Cursor Image

The cursor is a small visual representation that shows the position of the mouse or another pointing device. Various programs are employed to alter the cursor image, offering users visual feedback. While not mandatory, this feature enhances the application's functionality.

System cursors are a set of preloaded cursor images provided by Windows. These include the spinning circle (formerly the hourglass), the hand, the arrow, the I-beam, and various additional shapes. The utilization of system cursors is discussed in this segment.

By modifying the hCursor attribute within the WNDCLASS or WNDCLASSEX framework, you can associate a cursor with a window class. Otherwise, the arrow will be used as the default cursor.

The window receives a WM_SETCURSOR message as the mouse moves over it (unless another window has captured the mouse). At this juncture, one of the following actions occurs:

  • The window procedure confirms TRUE, allowing the program to set the cursor.
  • The program forwards WM_SETCURSOR to DefWindowProc without any additional actions.

A program executes the following steps to position the cursor:

  • The LoadCursor function is employed to load the cursor into memory, providing a handle to the cursor upon completion.
  • Subsequently, insert the cursor handle after utilizing the SetCursor function.

The DefWindowProc method uses the following procedure to set the cursor image if the application sends WM_SETCURSOR to it:

  • Forward the WM_SETCURSOR message to the window's parent for handling if it has one.
  • If not, move the pointer to the class cursor if the window has one.
  • Use the arrow cursor if there isn't a class cursor.

One can utilize the LoadCursor function to load either a system cursor or a custom cursor from a resource. The following example illustrates the process of assigning the cursor to the pre-defined system cursor for link selection.

Example

LPCTSTR cursor = IDC_HAND;
 hCursor = LoadCursor(NULL, cursor);
 SetCursor(hCursor);

Explanation

The code above loads a cursor source named "IDC_HAND", links it to a handle variable 'hCursor', and designates it as the active cursor.

Without intercepting the WMSETCURSOR message and redefining the cursor, the cursor's appearance will revert back after every mouse movement. Below is a demonstration of how to manage the WMSETCURSOR message in code.

Example

case WM_SETCURSOR:
 if (LOWORD(lParam) == HTCLIENT)
 {
 SetCursor(hCursor);
 return TRUE;
 }
 break;

Explanation

The code above verifies whether the message pertains to adjusting the cursor within the client area, proceeds to set the cursor with the handle 'hCursor', and then returns 'TRUE'.

User Input: Extended Example

Users are able to select, relocate, and delete ellipses, as well as draw them in a range of different colors. Nonetheless, the software restricts users from manually selecting ellipse colors in order to uphold a simplistic user interface. Instead, the system automatically rotates through a set assortment of colors. Apart from ellipses, the application does not accommodate any other shapes. Despite this limitation, it serves as a valuable template for reference. This segment will solely focus on the key functionalities.

In the software, ellipses are symbolized by a construct containing the hue (D2D1COLORF) and the elliptical information (D2D1_ELLIPSE). This construct additionally specifies two functions: one for illustrating the ellipse and another for verifying hits.

Example

struct MyEllipse
{
 D2D1_ELLIPSE ellipse;
 D2D1_COLOR_F color;
 void Draw(ID2D1RenderTarget *pRT, ID2D1SolidColorBrush *pBrush)
 {
 pBrush->SetColor(color);
 pRT->FillEllipse(ellipse, pBrush);
 pBrush->SetColor(D2D1::ColorF(D2D1::ColorF::Black));
 pRT->DrawEllipse(ellipse, pBrush, 1.0f);
 }
BOOL HitTest(float x, float y)
 {
 const float a = ellipse.radiusX;
 const float b = ellipse.radiusY;
 const float x1 = x - ellipse.point.x;
 const float y1 = y - ellipse.point.y;
 const float d = ((x1 * x1) / (a * a)) + ((y1 * y1) / (b * b));
 return d <= 1.0f;
 }
};

Explanation

The code snippet provided establishes a class named 'MyEllipse' that embodies an ellipse object characterized by attributes such as location, dimensions, and color. Within this class, there exists a 'Draw' function responsible for rendering the ellipse onto a Direct2D rendering surface. This process involves filling the ellipse with a designated color and outlining its shape. Additionally, the 'HitTest' function is implemented to verify whether a specified coordinate point '(x, y)' falls inside or on the perimeter of the ellipse. Upon evaluation, this method returns 'TRUE' if the point lies within or on the boundary, and 'FALSE' otherwise.

Conclusion

Creating a desktop application in C++ with Win32 and COM APIs provides extensive functionality for Windows software. By utilizing these APIs, programmers have the ability to manage system resources at a granular level and interact with advanced COM objects. Although this method requires knowledge of intricate nuances, it allows for the development of tailored software solutions that improve user interaction and feature sets.

Input Required

This code uses input(). Please provide values below:

Logic Practice
Install Logic Practice
Add to home screen for a faster app-like experience