Fundamentals of Windows Controls

 

Controls Fundamentals

A Windows control, in this book called a control, is an object that displays or is part of an application and allows the user to interact with the computer. There are various types of controls as we will see in this book:

  • a text-based control is an object whose main function is to display to, or request text from, the user
  • A list-based control displays a list of items
  • A progress control is used to show the evolution of an action
  • a static control can be used to show colors, a picture or something that does not regularly fit in the above categories

To make your application as efficient as possible, you will add controls as you judge them necessary. Some and most controls are already available so you can simply customize their behavior and appearance. Sometimes you will need a control for a specific task or for a better look. If such a control is not available, you may have to create your own.

There are two main ways a control is made part of your application. At design time, which is referred to the time you are visually creating an application, you will select controls and place them on a host. Another technique, referred to as run time, consists of creating control programmatically. In this case you must write all the code that specifies the control's appearance and behavior.

The Parent-Child Window Relationship

There are two types of windows or controls you will deal with in your applications. The type is defined by the relationship a window has with regards to other windows that are part of an application:

  • Parent: a window is referred to as a parent when there are or there can be other windows that depend on it. For example, if you look at the toolbar of Visual C++, it is equipped with the buttons. The toolbar is a parent to the buttons.
    When a parent is created, it makes it possible to "give life" to other windows that can depend on it. The most regular parents you will use are forms and dialog boxes.
  • Child: A window is referred to as child when its existence and especially its visibility depend on another window called its parent. When a parent is created, made active, or made visible, it gives existence and visibility to its children. When a parent gets hidden, it also hides its children. If a parent moves, it moves with its children, the children keep their positions and dimensions inside the parent. When a parent is destroyed, it also destroys its children (sometimes it does not happen; a parent may make its children unavailable but they may still be occupying memory after the parent has been destroyed). Child controls depend on a parent because the parent "carries", "holds", or hosts them. All of the Windows controls you will use in your applications are child controls. A child window can also be a parent of another control. For example, the Standard toolbar of Visual Studio is the parent of the buttons on it. If you close or hide the toolbar, its children disappear. At the same time, the toolbar is a child of the application's frame. If you close the application, the toolbar disappears, along with its own children. In this example, the toolbar is a child of the frame but is a parent for its buttons.
Author Note It is important to understand that this discussion refers to parents and children as windows, not as classes:
  • The CButton class is based on CWnd. Therefore, CWnd is the parent class of CButton
  • When a button is placed on a form, the form, which is based on CView (indirectly) is the window parent of the button and not its class parent

If you are creating an independent window, as we have done with the frames so far, you can simply call its Create method in its constructor and define its characteristics. Such a class or window is a prime candidate for parenthood.

 

Parent Controls

An object is referred to as a parent or container when its main job is to host other controls. This is done by adding controls to it. The most commonly used parent control is the dialog box. In the previous lesson, we learn how to easily create a dialog-based application and how to add a dialog box to an application.

Besides the WNDCLASS structure, the Win32 library also offers the WNDCLASEX structure used to create an application. Besides all of the characteristics of WNDCLASS, the WNDCLASSEX structure requires that you specify the size of the structure and it allows you to set a small icon for your application. This can be done as follows:

const char *WndName = "Windows Fundamentals";

LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
				   LPSTR lpCmdLine, int nCmdShow)
{
    MSG		Msg;
    WNDCLASSEX	WndClsEx;

    WndClsEx.cbSize        = sizeof(WNDCLASSEX);
    WndClsEx.style         = CS_HREDRAW | CS_VREDRAW;
    WndClsEx.lpfnWndProc   = WndProc;
    WndClsEx.cbClsExtra    = 0;
    WndClsEx.cbWndExtra    = 0;
    WndClsEx.hInstance     = hInstance;
    WndClsEx.hIcon         = LoadIcon(NULL, IDI_ASTERISK);
    WndClsEx.hCursor       = LoadCursor(NULL, IDC_ARROW);
    WndClsEx.hbrBackground = (HBRUSH)COLOR_ACTIVECAPTION;
    WndClsEx.lpszMenuName  = NULL;
    WndClsEx.lpszClassName = ClsName;
    WndClsEx.hIconSm       = LoadIcon(NULL, IDI_ASTERISK);

    RegisterClassEx(&WndClsEx);

    while( GetMessage(&Msg, NULL, 0, 0) )
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }

    return Msg.wParam;
}

Besides the CreateWindow() function, the Win32 library provides the CreateWindowEx() function you can use to create a  window. The CreateWindowEx() function has the followsing syntax:

HWND CreateWindowEx(
  DWORD dwExStyle,
  LPCTSTR lpClassName,
  LPCTSTR lpWindowName,
  DWORD dwStyle, 
  int x,
  int y, 
  int nWidth, 
  int nHeight, 
  HWND hWndParent, 
  HMENU hMenu,
  HINSTANCE hInstance,
  LPVOID lpParam
);

Parent Windows Styles

The role of a parent object is very important to the other controls it is hosting. Therefore the parent window must possess some valuable characteristics. For example, you must provide a way to close the parent window and/or to move it. This is usually done by the user dragging the title bar of a frame or a dialog box. If the parent is a toolbar, you can make it dockable, in which case it can be moved and positioned on various parts of the frame window. You can also allow the user to move a parent such as a dialog box by dragging its body. If you create the window using the CreateWindow() function and specify the style as WS_OVERLAPPEDWINDOW, the window would be equipped with a title that has all three system buttons and the system menu. The CreateWindowEx() function allows you to specify further features for a parent window.

Sunken Edges: A window appears with a sunken when its body is sunk with regards to its borders. To create such as window, applying the WS_EX_CLIENTEDGE style. To get this effect on a dialog box, while designing it, set its Client Edge to True.

Raised Edges: A window can have an inside body that appears to be raised with regards to its borders. Such a window can be created by adding the WS_EX_WINDOWEDGE style.

Overlapped Windows: A window is referred to as overlapped when it presents a combination of a sunk and a raised edge. Such a window can be created by either combining the WS_EX_CLIENTEDGE and the WS_EX_WINDOWEDGE styles or by using the WS_EX_OVERLAPPEDWINDOW. To get this effect of a dialog box, set its Style to Overlapped.

Tool Windows: A tool window is a parent object mostly used to float, like a toolbar,  on a frame for another window. It has a short title bar that displays its caption. It cannot be mini or maximized but it can be equipped with the system menu:

To create a tool window, set or add the WS_EX_TOOLWINDOW extended style to the CreateWindowEx() function:

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
				   LPSTR lpCmdLine, int nCmdShow)
{
    HWND		hWnd;
    MSG		Msg;
    WNDCLASSEX	WndClsEx;

    . . .

    RegisterClassEx(&WndClsEx);

    hWnd = CreateWindowEx(WS_EX_TOOLWINDOW, 
		       ClsName,
                         WndName,
                         WS_OVERLAPPEDWINDOW,
                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         CW_USEDEFAULT,
                         NULL,
                         NULL,
                         hInstance,
                         NULL);

    return Msg.wParam;
}

The above tool window allows the user to resize it by dragging its borders or corners. If you do not want the user to be able to resize the tool window, set its style to WS_CAPTION and WS_SYSMENU:

INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
				   LPSTR lpCmdLine, int nCmdShow)
{
    HWND		hWnd;
    MSG		Msg;
    WNDCLASSEX	WndClsEx;

    . . .

    RegisterClassEx(&WndClsEx);

    hWnd = CreateWindowEx(WS_EX_TOOLWINDOW, 
		       ClsName,
                         WndName,
                         WS_CAPTION | WS_SYSMENU,
                         . . .,
                         );
}

If you are working on an MFC application and designing your dialog box, to make it a tool window, set the Tool Window property to True. If you want the user to be able to resize the window, set its Border property to Resizing. If you do not want the user to resize the window, set its Border property to Dialog Frame.

Windows Taskbar Button: By default, a tool window does not display display a button on the taskbar. If you are creating a window that will depend on a main window such as a frame, you should keep that default. If for any reason you want to display a button for the tool window, add the WS_EX_APPWINDOW extended style.

Window on Top: If you want your window to stay on top of another or other windows, you have two choices. If the window you are creating, such as a dialog box, is being added to a frame-based or a form-based application and you want that window to always be on top of its parent window, add the WS_VISIBLE style to it and display it using ShowWindow(SW_SHOW). For a dialog box you are designing, set its Visible property to True. If you want the window to always be on top of other windows, add the WS_EX_TOPWINDOW extended style to it.

 

Control Creation Options

To allow the user to interact with the computer, you equip your computer with the necessary controls. The controls are positioned on the client area of the parent window. The easiest technique used to add a control to your application consists of picking it up from the Controls toolbox and positioning it on the parent. To help with this, Visual C++ provides a series or ready-made objects on the Controls toolbox:

Visual C++ 6 Controls Visual C++ 7 Controls

The second option used to add a control to an application consists of programmatically creating it. All visible controls of the MFC library are based on the CWnd class. This class is equipped with all the primary functionality that a regular window object needs. As the parent of all visible window classes, it provides the other controls with properties and methods to implement their appearance and behavior. To use the CWnd class:

  • You can directly derive a class from CWnd
  • You can use one of the CWnd-derived classes (CView, CFrameWnd, etc)
  • You can derive a class from one of the CWnd-derived classes; for example you can create a class based on CStatic
  • You can directly declare a CWnd variable in your application and initialize it the class of your choice

To create a control using a known class, you must declare a variable of the class on which the control is based. Every control is based on a specific class, and that class is directly or indirectly derived from CWnd. An example of such a declaration would be:

void CParentLoader::TodaysMainEvent()
{
	CStatic Label;
}

You can also declare the variable as a pointer. In this case, make sure that you initialize the class using the new operator. An example would:

void CParentLoader::TodaysMainEvent()
{
	CStatic *Label = new CStatic;
}

Based on the rules of inheritance and polymorphism, you can also declare the pointer as CWnd but initialize it with the desired class. Here is an example:

void CParentLoader::TodaysMainEvent()
{
	CWnd *Panel = new CStatic;
}

You can also use the CWnd::Create() method to create a control. The syntax of this method is:

virtual BOOL Create(LPCTSTR lpszClassName, LPCTSTR lpszWindowName,
		  DWORD dwStyle, const RECT& rect, CWnd* pParentWnd,
		  UINT nID, CCreateContext* pContext = NULL);

To create a fancier control, you can use the CWnd::CreateEx() method.

The availability of a child window to other parts of a program is defined by the scope in which the control is created. If you create a control in an event or a method of a parent class, it can be accessed only inside of that event. Trying to access it outside would produce an error. If you want to access a child or dependent window from more than one event or method, you must create it globally. This is usually done by declaring the variable or pointer in the class of the parent.

After declaring the variable, to initialize it, call its Create() method. The syntax of this method depends on the window you are trying to create. After initializing the window, it may become alive, depending on the style or properties you assigned to it.

The other technique you can use is to derive a class from the control whose behavior and appearance can serve as a foundation for your control. For example, you can derive a control as follows:

class CColoredLabel : public CStatic
{
public:
	CColoredLabel();
	virtual ~CColoredLabel();

	DECLARE_MESSAGE_MAP()
};

Such a class as the above CColoredLabel gives you access to the public and protected properties and methods of the base class. To use the new class in your application, declare a variable from and call the Create() method.

The Control's Class Name

If you are programmatically creating a control, you must specify its name. To do this, you have various options.

Visual C++ ships with already defined names of classes you can use to create a control. These classes are:

 

Class Name Why Use It?
STATIC A static control can be used to display text, a drawing, or a picture
EDIT As it name suggests, an edit control is used to display text to the user, to request text from the user, or both
RichEdit Like an edit box, a rich edit control displays text, request it, or does both. Besides all the capabilities of the edit control, this control can display formatted text with characters or words that use different colors or weight. The paragraphs can also be individually aligned.
The RichEdit class name is used to create a rich edit control using the features of Release 1.0 of its class
  RICHEDIT_CLASS This control is used for the same reason as for the RichEdit class name except that it provides a few more text and paragraph formatting features based on Release 2.0
  LISTBOX A list box is a control that displays of items such as text arranged so each item, considered individually displays on its own line
  COMBOBOX A combo box is a combination of an edit control and a list box. It holds a list of item so the current selection displays in the edit part of the control
  SCROLLBAR A scroll bar is a rectangular object equipped with a bar terminated by an arrow of each end. It is used to navigate left and right or up and down on a document
  BUTTON A button is an object that the user clicks to initiate an action
  MDICLIENT This class name is used to create a child window frame for an MDI application

To use one of these classes, pass its name as string to the lpszClassName of the Create() or the CreateEx() methods. Here is an example that would create an edit box:

void CSecondDlg::OnThirdControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *Memo = new CWnd;

	Memo->Create("EDIT", );
}

The second option you have is to either declare a WNDCLASS variable and initialize it, or call the AfxRegisterWndClass() function and pass it the desired values. As we saw in the previous lesson, this function returns a string. You can pass that string to the CWnd::Create() method as the class name. Here is an example:

void CSecondDlg::OnFirstControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *First = new CWnd;
	CString StrClsName = AfxRegisterWndClass(CS_VREDRAW | CS_HREDRAW, LoadCursor(NULL, IDC_CROSS),
					    (HBRUSH)GetStockObject(BLACK_BRUSH), LoadIcon(NULL, IDI_WARNING));

	First->Create(StrClsName, );
}

The other option you have is to specify the class name as NULL. In this case, the compiler would use a default name:

void CSecondDlg::OnSecondControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *Second = new CWnd;

	Second->Create(NULL, );
}

The Control's Window Name

The window name is a default string that displays on the control when the control comes up. In the previous lessons, we saw that this string displays as caption on the title bar of a window frame or a dialog box. This property is different for different controls.

When designing a static control using the Static Text button , to set the window name, change the text of the Caption field.

To set the default text of a control, you can either do this when creating the control or change its text at any time. Again, this can depend on the control. To set the default text when programmatically creating the control, pass a string value as the lpszWindowName argument of the Create() or CreateEx() method. Here is an example:

void CSecondDlg::OnThirdControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *Memo = new CWnd;

	Memo->Create("EDIT", "Voice Recorder", );
}

Many (even most) controls do not use a window name. Therefore, you can pass it as NULL

Introduction to Controls Styles and Common Properties

A style is a characteristic that defines the appearance and can set the behavior of a control. The styles are varied from one control to another although they share some of the characteristics common to most Windows controls.

After placing the control on the host, if you are using MSVC 6, you can right click the control and click Properties. This would display the Properties window that allows you to visually set the characteristics of the control. Some of the styles are available from the General tab and some others can be accessed from the Styles tab:

In Visual C++ 7, the Properties window displays, by default, to the lower right side of the IDE.

 

Childhood

All of the controls you will create need to be hosted by another control. During design, once you position a control on a dialog box or a form, it automatically gets the status of child. If you are programmatically creating your control, to specify that it is a child, add the WS_CHILD to the dwStyle argument. Here are examples:

void CSecondDlg::OnFirstControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *First = new CWnd;
	CString StrClsName = AfxRegisterWndClass(CS_VREDRAW | CS_HREDRAW, LoadCursor(NULL, IDC_CROSS),
	(                                        HBRUSH)GetStockObject(BLACK_BRUSH),
	                                         LoadIcon(NULL, IDI_WARNING));

	First->Create(StrClsName, NULL, WS_CHILD, );
}

void CSecondDlg::OnSecondControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *Second = new CWnd;

	Second->Create(NULL, NULL, WS_CHILD, );
}

void CSecondDlg::OnThirdControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *Memo = new CWnd;

	Memo->Create("EDIT", "Voice Recorder", WS_CHILD, );
}

void CSecondDlg::OnFourthControl() 
{
	// TODO: Add your control notification handler code here
	CStatic *Label = new CStatic;

	Label->Create("United Nations", WS_CHILD, );
}

Visibility

If you want to display a control to the user when the parent window comes up, at design time, set the Visible property to True. If you are programmatically creating the control, add the WS_VISIBLE property. Here is an example:

void CSecondDlg::OnSecondControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *Second = new CWnd;

	Second->Create(NULL, NULL, WS_CHILD | WS_VISIBLE, );
}

If you do not set the Visible property to True or do not add the WS_VISIBLE property, the control would be hidden (but possibly available) when its parent window comes up. Later on, you can display the control by calling the CWnd::ShowWindow() method. Remember that this method is used to either hide or to reveal a control by passing the appropriate constant, SW_HIDE to hide and SW_SHOW to display it. Here is an example that displays a control that missed the WS_VISIBLE property when it was created:

void CSecondDlg::OnFirstControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *First = new CWnd;
	CString StrClsName = AfxRegisterWndClass(CS_VREDRAW | CS_HREDRAW,
                                             LoadCursor(NULL, IDC_CROSS),
					(HBRUSH)GetStockObject(BLACK_BRUSH),
					LoadIcon(NULL, IDI_WARNING));

	First->Create(StrClsName, NULL, WS_CHILD);
	First->ShowWindow(SW_SHOW);
}

When the ShowWindow() method is called with the SW_SHOW value, if the control was hidden, it would become visible; if the control was already visible, nothing would happen. In the same way, when this method is called with the SW_HIDE argument, the control would be hidden, whether it was already hidden or not. If you want to check the visibility of a control before calling this method, you can call the CWnd::IsWindowVisible() method. Its syntax is:

BOOL IsWindowVisible() const;

This method returns TRUE if the control that called it is already visible. If the control is hidden, the method returns FALSE.

Availability

By default, after a control has been created, it is available to the user. If it is a non-static control, the user can possibly change select or change its value. If you do not want the control to be immediately usable but do not want to hide it, you can disable it. If you are designing the control, set its Disable property to True or checked. If you are programmatically creating the control, add the WS_DISABLED style as follows:

void CSecondDlg::OnSecondControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *Second = new CWnd;

	Second->Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_DISABLED, );
}

When a control is disabled, the user can see it but cannot change its value. If for any reason a control is disabled, to enable it, you can call the CWnd::EnableWindow() method. In fact, the EnableWindow() method is used either to enable or to disable a window. Its syntax is:

BOOL EnableWindow(BOOL bEnable = TRUE);

Here is an example that disables a control called Memo:

void CSecondDlg::OnDisableMemo() 
{
	// TODO: Add your control notification handler code here
	Memo->EnableWindow(FALSE);
}

When calling the the EnableWindow() method, if you pass the TRUE value, the control is disabled, whether it was already disabled or not. If you pass the FALSE constant, it gets enabled, even it was already enabled. Sometimes you may want to check first whether the control is already enabled or disabled. This can be accomplished by calling the CWnd::IsWindowEnabled(). Its syntax is:

BOOL IsWindowEnabled( ) const;

This method checks the control that called it. If the control is enabled, the member function returns TRUE. If the control is disabled, this method returns FALSE. Here is an example:

void CSecondDlg::OnDisableMemo() 
{
	// TODO: Add your control notification handler code here
	if( Memo->IsWindowEnabled() == TRUE )
		Memo->EnableWindow(FALSE);
	else // if( !Memo->IsWindowEnabled() )
		Memo->EnableWindow();
}

Here is a simplified version of the above code:

void CSecondDlg::OnDisableMemo() 
{
	// TODO: Add your control notification handler code here

	Memo->EnableWindow(!Memo->IsWindowEnabled());
}

Borders

One of the visible features you can give a control is to draw its borders. Fortunately, most Microsoft Windows controls have a 3-D look by default. Some other controls must explicitly be given a border. To add a border to a control, at design time, set its Border property to True. If you are programmatically creating the control, add the WS_BORDER style to it:

void CSecondDlg::OnSecondControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *Second = new CWnd;

	Second->Create(NULL, NULL, WS_CHILD | WS_VISIBLE | WS_BORDER, );
}

To raised the borders of such a control, add the WS_THICKFRAME to its styles:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CtrlBorders->Create(NULL, NULL,
	WS_CHILD | WS_VISIBLE | WS_THICKFRAME, );

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

Tab Sequence

When using the controls of a dialog, the user may choose to press Tab to navigate from one control to another. This process follows a sequence of controls that can receive input in an incremental order. To include a control in this sequence, at design time, set its Tab Stop property to True.

If you are creating your control with code, to make sure that it can fit in the tab sequence, add the WS_TABTOP. Here is an example:

void CSecondDlg::OnThirdControl() 
{
	// TODO: Add your control notification handler code here

	Memo->Create("EDIT", "Voice Recorder",
                 WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, );

	CWnd *CanTab = new CWnd;

	CanTab->Create("BUTTON", "&Apply", WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, );
}
 

Introduction to Extended Styles

Besides the above regular styles and properties used on controls, if you to add a more features to a window, you can create it using the CWnd::CreateEx() method. It comes in two versions as follows:

BOOL CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName, LPCTSTR lpszWindowName,
              DWORD dwStyle, int x, int y, int nWidth, int nHeight,
              HWND hwndParent, HMENU nIDorHMenu, LPVOID lpParam = NULL );
BOOL CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName, LPCTSTR lpszWindowName,
              DWORD dwStyle, const RECT& rect, CWnd* pParentWnd,
              UINT nID, LPVOID lpParam = NULL);

In Visual C++ 6, some of the extended styles are on the Styles tab of the Properties window and some others are in the Extended Styles tab:

In Visual C++ 7, all styles are listed in the Properties window's vertical list:

Left Text Alignment

Text-based controls (such as the static label, the edit box, or the rich edit control) align their text to the left by default. This means that the control displays, its text starts on the left side of its area. To align text to the left on a control that allows it, at design time, select the Left value in the Align Text combo box.

The default text alignment of a text-based control is to the left. This is because the WS_EX_LEFT extended style is applied to it. If you want to reinforce this, you can add that style as the dwExStyle argument of the CreateEx() method. This can be done as follows:

void CSecondDlg::OnThirdControl() 
{
	// TODO: Add your control notification handler code here

	Memo->CreateEx(WS_EX_LEFT, "EDIT", "Voice Recorder",
                 WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, );
}

Right Text Alignment

Many text-based controls, including the button, allow you to set their text close to the right border of their confined rectangle. To do this at design time, select the Right value in the Align Text combo box.

If you are programmatically creating the control, to align its text to the right, set or add the WS_EX_RIGHT extended style. Here is an example:

void CSecondDlg::OnThirdControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *CanTab = new CWnd;

	CanTab->CreateEx(WS_EX_RIGHT, "BUTTON", "&Apply",
                     WS_CHILD | WS_VISIBLE | WS_BORDER | WS_TABSTOP, );
}

Extended Borders

Besides the border features of the dwStyle argument of the Create() member function, the CreateEx() method provides other border drawings to apply to a control.

Static Borders: To give a 3-D appearance to a control, especially one that does not receive input from the user (such as a static control), you can apply a static edge to it. To do this, at design time, set the Static Edge property to True. At run time, you can set or add the WS_EX_STATICEDGE extended style. Here is an example:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CWnd* Panel = new CStatic;
	
	Panel->CreateEx(WS_EX_STATICEDGE, "STATIC", NULL,
                         WS_CHILD | WS_VISIBLE, );

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

Sunken Borders: You can further sink the borders of a control, give them an advanced 3-D appearance. To apply this effect at design time, set the Client Edge property to True. If you are programmatically creating a control, add the WS_EX_CLIENTEDGE extended style. Here is an example:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CWnd* Panel = new CStatic;
	
	Panel->CreateEx(WS_EX_CLIENTEDGE, "STATIC", NULL,
                         WS_CHILD | WS_VISIBLE, );

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

Raised Borders: A control appears raised when it borders come out of the client area. This creates the effect of light left and top borders while the right and bottom borders appear darker. To apply this property, at design time, set the Modal Frame property to True or, at run time, add the WS_EX_DLGMODALFRAME extended style. Here is an example:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CWnd* Panel = new CStatic;
	
	Panel->CreateEx(WS_EX_DLGMODALFRAME, "STATIC", NULL,
                         WS_CHILD | WS_VISIBLE, );

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

To raise only the borders of a control without including the body of the control, you can combine WS_EX_CLIENTEDGE and the WS_EX_DLGMODALFRAME extended styles. Here is an example:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CWnd* Panel = new CStatic;
	
	Panel->CreateEx(WS_EX_CLIENTEDGE | WS_EX_DLGMODALFRAME,
	                "STATIC", NULL,
                         WS_CHILD | WS_VISIBLE, );

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

 

Ownership and Identification: Parent Windows Logistics

One of the main roles of a parent window is its ability to host other controls. Such controls are called its children because the parent carries them when it is moved and it controls their availability. These controls are also called its clients because they request the parental service from it.

As mentioned already, at design time, you can pick up a control from the Controls toolbox and position it on the parent window. The child controls are added in an area referred to as the body of the parent window. The body of a window is a rectangular shape also called the client area:

A control can be added only in the client area and cannot exceed that area. To find out how much room a parent window is making available to its children, that is, to get the dimensions (and screen location) of the client area, you can call the CWnd::GetClientRect() function. Its syntax is:

void GetClientRect(LPRECT lpRect) const;

This function takes as argument a RECT or CRect variable and stores the location and dimension of the client rectangular area in it. In the following example, the GetClientRect() function is called to get the dimensions of the client area of a dialog box and use the resulting rectangular to paint that area:

void CSecondDlg::OnPaint() 
{
	CPaintDC dc(this); // device context for painting
	
	// TODO: Add your message handler code here
	CRect Recto;
	CBrush BlueBrush(RGB(0, 128, 255));
	CPen   BluePen(PS_SOLID, 1, RGB(0, 128, 255));

	GetClientRect(&Recto);

	CPen *OldPen = dc.SelectObject(&BluePen);
	CBrush *OldBrush = dc.SelectObject(&BlueBrush);

	dc.Rectangle(Recto);

	dc.SelectObject(OldBrush);
	dc.SelectObject(OldPen);
	// Do not call CDialog::OnPaint() for painting messages
}

To manage the area available to the control you are adding at design time, Visual C++ presents two options. If you display the grids, which is done by clicking the Toggle Grid button , the control can be positioned and resized anywhere in the area inside the borders of the parent window, as seen on the above red rectangle. No control can be positioned outside of the client area.

If you hide the grids, which is done by clicking the Toggle Guides button , a (blue dotted) rectangle appears in the client area:

This (blue dotted) rectangle allows you to effectively control the area available to your controls. To set the location and dimensions of the available area, click one of the (blue dotted) rectangle borders or corners and drag in the desired direction. If a control is positioned on a border that is moving, the control would be repositioned accordingly:

After setting the (blue dotted) rectangular location and area, you can add but cannot move a control outside of that (blue dotted) rectangle. The idea is to allow you to design a control or a group of controls in a (temporary) confined area. The (blue dotted) rectangle provides an effective means of aligning controls. After using it, if you want to add and manipulate controls outside of it, you should display the grids.

As mentioned already, the controls are confined to the client area offered by the parent window. After visually adding a control to a parent window, it assumes a location and takes some dimensions in the client area. The origin of the rectangular client area is on the upper-left corner of the parent window. The horizontal measurements move from the origin to the right. The vertical measurements move from the origin to the bottom:

At design time, to set the location and dimension of a control, after placing it on the parent, click and hold your mouse on the control, then drag left, top, right, and down. While doing that, refer to the right section of the status bar for the current location and dimensions of the control:

While moving and resizing the control, if you need help with this exercise, click the Toggle Grid button . When the grids are displaying, the control can only be located on grid indicators and when resizing the control, it can only fit on grid indicators. If you want to be more precise and ignore the grids, you have various options:

  • If you want to keep the grids but modify their distances, open the Guide Settings dialog box available from the Layout (MSVC 6) or Format (MSVC 7) menu then modify the Width and/or Height values in the Grid Spacing section:
     
  • If you want to keep the grids, press and hold Alt while you are dragging. While alt is down, you can position and dimension the control anyhow regardless of the grids settings, as long as you stay confined to the client area
  • You can hide the grids by clicking the Toggle Guides button . When this button is down, a control can moved and dimensioned anyhow as long as it stays within the confinements of the (blue dotted) rectangle
  • To use neither the grids nor the rectangular guide, click the button that is down to remove it.
    Whether the grids or the rectangular guide are displaying, to position a control with more precision, click it to select, then press one of the arrow keys to move the control by one pixel

If you are programmatically creating the control, to set its location, if you are using the first version of the CreateEx() method, specify the x value for the distance from the left border of the client area to the left border of the control, and pass the desired y value for the upper distance from the top border of the client area to the top border of the control. Here is an example:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CWnd* Panel = new CStatic;
	
	Panel->CreateEx(NULL, "STATIC", NULL,
                    WS_CHILD | WS_VISIBLE | WS_BORDER,
                    32, 15, );

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

If you specify negative values for the left and top distances, either the left or the top borders, respectively, will be hidden.

To set the dimensions of the control, if you are using the first version of the CreateEx() member function, specify the nWidth and the width and the nHeight for the height of the control. If you specify measures that are higher than the width of the client area - x or the height of the client area - y, the right border or the bottom border, respectively, of the control will be hidden.

To specify the location and the dimensions of the control at the same time, pass a RECT or a CRect variable as the rect argument of the Create() or the second version of the CreateEx() methods. Here are examples:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CWnd* btnApply = new CWnd;
	RECT Recto = {12, 16, 128, 64};
	btnApply->Create("BUTTON", "&Apply", WS_CHILD | WS_VISIBLE,
                     Recto, );

	CWnd *btnDismiss = new CWnd;
	btnDismiss->Create("BUTTON", "&Dismiss", WS_CHILD | WS_VISIBLE,
                       CRect(12, 68, 128, 120), );

	CWnd* Panel = new CStatic;
	Panel->CreateEx(WS_EX_DLGMODALFRAME, "STATIC", NULL,
                    WS_CHILD | WS_VISIBLE | WS_BORDER,
                    132, 16, 220, 120, );

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

It is important to understand that there is a tremendous difference on the way the dimensions of a control are calculated. Although we have not mentioned it, and we will not spend time on it because we are not going to treat controls differently, Common Controls are objects that started to be released with Windows 95. They calculate their width from the left border of the control to the right and the height of the control is calculated from the top border of the control down. Other (earlier) controls calculate their width by subtracting the specified nWidth value from the x value. This means that the nWidth starts on the left border of the client area. Based on this, for example if you are creating a button (class name = "BUTTON"), if you provide a value that <= x, the control would not be displayed because its width would be either nil or negative. Observe the following code:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CWnd* btnApply = new CWnd;
	RECT Recto = {12, 16, 12, 64};
	btnApply->Create("BUTTON", "&Apply", WS_CHILD | WS_VISIBLE,
                     Recto, );

	CWnd* Panel = new CStatic;
	Panel->CreateEx(WS_EX_DLGMODALFRAME, "STATIC", NULL,
                    WS_CHILD | WS_VISIBLE | WS_BORDER,
                    132, 16, 132, 120, );

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

The x and nWidth values of the button have the same value. In the same way, the x and the nWidth values of the panel object have the same value. When this program is executed, the button would not display because its width = 0. The panel object would display with a width of 132.

Once the control is already positioned on the client area, to get its location and dimensions, you can call the CWnd::GetWindowRect() method. Here is an example:

void CClientAreaDlg::OnBtnCalculate() 
{
	// TODO: Add your control notification handler code here
	CRect Recto;
	char LocDim[80];
	
	m_Rectangle.GetWindowRect(&Recto);

	sprintf(LocDim, "Left: %d, Top: %d, Width: %d, Height: %d",
	        Recto.left, Recto.top, Recto.Width(), Recto.Height());

	SetWindowText(LocDim);
}

 

The Control Location and its Origin

When calling either the GetClientRect() or the GetWindowRect() methods to get the location and the dimensions of a control or another object, it is important to know the origin of the produced rectangle. By default, the rectangle returned by the GetWindowRect() method called by a control has its origin on the top left corner of the monitor and not on the top-left corner of the parent window. Consider the following button event. It gets the location and dimensions of a static control and stores them in a CRect variable. Then it paints a rectangle (it is supposed to paint the static control) located on, and equal to the dimensions, of the static control. Finally, it displays the properties of the rectangle that holds a static control:

void CClientAreaDlg::OnBtnCalculate() 
{
	// TODO: Add your control notification handler code here
	CRect Recto;
	char LocDim[80];
	
	m_Rectangle.GetWindowRect(&Recto);

	CClientDC dc(this);
	CBrush BlueBrush(RGB(0, 128, 192));

	dc.SelectObject(&BlueBrush);
	dc.Rectangle(Recto);

	sprintf(LocDim, "Left: %d, Top: %d, Width: %d, Height: %d",
		       Recto.left, Recto.top, Recto.Width(), Recto.Height());

	SetWindowText(LocDim);
}

After executing the program and moving the dialog box somewhere to the middle center of the screen and clicking the button, the result is as follows:

After moving the dialog box close to the top-left section of the screen and clicking the button again, the result is the following:

 

This clearly demonstrates that, although the static control is a child of the dialog box, the rectangle returned by the GetWindowRect() method is based on the screen and not the client area of the parent window. This is not an anomaly. It is purposely done so you can specify what origin you want to consider.

As seen in previous lessons, the origin of the screen is positioned on the top-left corner of the monitor. The origin of a client area is placed on its top-left corner. For example, the origin used by the above GetWindowRect() method is based on the screen. If you want the rectangle resulting from a call to either the GetClientRect() or the GetWindowRect() methods to be based on the client area of the control that called it, you can transfer the origin from the screen to the client. This is conveniently done with a simple call to the CWnd::ClientToScreen() method. It is overloaded as follows:

void ClientToScreen(LPPOINT lpPoint) const;
void ClientToScreen(LPRECT lpRect) const;

If the location you had requested is a point, pass its POINT or CPoint variable to the ClientToScreen() method. If the value you requested is a rectangle, pass its RECT or CRect variable. Here is an example:

void CClientAreaDlg::OnBtnCalculate() 
{
	// TODO: Add your control notification handler code here
	CRect Recto;
	char LocDim[80];
	
	m_Rectangle.GetWindowRect(&Recto);

	CClientDC dc(this);
	CBrush BlueBrush(RGB(0, 128, 192));

	ScreenToClient(Recto);

	dc.SelectObject(&BlueBrush);
	dc.Rectangle(Recto);

	sprintf(LocDim, "Left: %d, Top: %d, Width: %d, Height: %d",
	        Recto.left, Recto.top, Recto.Width(), Recto.Height());

	SetWindowText(LocDim);
}

This time, even if the dialog box moves, the GetWindowRect() method returns the same rectangle.

If the location and/or dimension are given in client coordinates, to convert them to screen coordinates, call the ScreenToClient() method. It is overloaded as follows:

void ScreenToClient(LPPOINT lpPoint) const;
void ScreenToClient(LPRECT lpRect) const;

 

Parenthood

After specifying that the control you are creating is a child, which is done by placing the control on a form or a dialog box or by using or adding the WS_CHILD style, you must specify what window is the parent of your control. The parent of a control is automatically set when the control is placed on a host: the host becomes the parent.

If you are programmatically creating the control, you can specify its parent by passing a window handle as the pParentWnd argument of the Create() or or the second version of the CreateEx() member functions. If you are creating the control in a member function or an event of the parent window that is a descendent of CWnd, you can pass this argument as the this pointer. Here is an example:

void CSecondDlg::OnFirstControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *First = new CWnd;
	CString StrClsName = AfxRegisterWndClass(CS_VREDRAW | CS_HREDRAW,
                                                  LoadCursor(NULL, IDC_CROSS),
                                                  (HBRUSH)GetStockObject(BLACK_BRUSH)
                                                  LoadIcon(NULL, IDI_WARNING));

	First->Create(StrClsName, NULL,WS_CHILD | WS_VISIBLE | WS_BORDER,
	              CRect(20, 20, 120, 60), this, );
}

Specifying the parent as the this pointer indicates that when the parent is destroyed, it will also make sure the child control is destroyed. If you want the application to be the parent and owner of the control, use the first version of the CreateEx() method and pass the handle of your application as the hwndParent argument. Here is an example:

BOOL CClientAreaDlg::OnInitDialog()
{
	CDialog::OnInitDialog();

	SetIcon(m_hIcon, TRUE);	// Set big icon
	SetIcon(m_hIcon, FALSE);	// Set small icon
	
	// TODO: Add extra initialization here
	
	CWnd* stcLogo = new CWnd;

	stcLogo->CreateEx(WS_EX_DLGMODALFRAME, "STATIC", NULL,
		         WS_CHILD | WS_VISIBLE | WS_BORDER,
                           240, 90, 90, 40, this->m_hWnd, );

	return TRUE;  // return TRUE  unless you set the focus to a control
}

If, during the lifetime of your program you want to change a control's parent, you can call the CWnd::SetParent() method. Its syntax is:

CWnd* SetParent(CWnd* pWndNewParent);

Here is a rough example:

class CSecondDlg : public CDialog
{
// Construction
public:
	CSecondDlg(CWnd* pParent = NULL);   // standard constructor

	. . .

private:
	CWnd *lblFirstName;
	CWnd *AnotherParent;
};
CSecondDlg::CSecondDlg(CWnd* pParent /*=NULL*/)
	: CDialog(CSecondDlg::IDD, pParent)
{
	//{{AFX_DATA_INIT(CSecondDlg)
		// NOTE: the ClassWizard will add member initialization here
	//}}AFX_DATA_INIT
	lblFirstName = new CWnd;
	AnotherParent = new CWnd;
}
void CSecondDlg::OnThirdControl() 
{
	// TODO: Add your control notification handler code here
	CWnd *Memo = new CWnd;

	Memo->Create("EDIT", "Voice Recorder",
	             WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_BORDER |
	             ES_MULTILINE | ES_WANTRETURN,
	             CRect(20, 80, 185, 224), this, );
	Memo->SetParent(AnotherParent);
}

To find out what window is the parent of a certain control, call the CWnd::GetParent() method. Its syntax is:

CWnd* GetParent() const;

This method returns a pointer to the parent of the control.

Control Identification

After adding a control to your your application, you must identify it. An identifier is not a string, it is a constant integer that is used to identify a control. Therefore, it is not the name of the control. 

If you have just added the control, Visual C++ would assign a default identifier. If the control is static, it would have the IDC_STATIC identifier. Every other control gets an identifier that mimics its class name followed by a number. An example would be IDC_EDIT1. If you add another control of the same kind, it would  receive an identifier with an incremental number. To have a better idea of each control, you should change their IDs to make them more intuitive. To do this, if you are visually adding the control, first display the Properties window:

The identifier can be a word or a hexadecimal number. Here are the rules you must follow when assigning identifiers:

It you decide to use a word If you decide to use a hexadecimal number
  • The identifier must start with an underscore or a letter
  • The identifier must be in one word
  • After the first character, the identifier can have letters, underscores, and digits in any combination
  • The identifier must not have non-alphanumeric characters
  • The identifier cannot have space(s)
  • The identifier must start with a digit or one of the following letters: a, b, c, d, e, f, A, B, C, D, E, or F
  • The identifier can have only digits and the above letters except this: if the identifier starts with 0, the second character can be x or X followed by the above letters and digits so that, when converted to decimal, it must have a value between -32768 and 65535

Here are suggestions you should follow:

  • The identifier should be in all uppercase
  • The identifier should have a maximum of 30 characters
  • If the object is a form or dialog box, its identifier should start with IDD_ followed by a valid name. Examples: IDD_EMPLOYEES or IDD_EMPL_RECORS
  • If you are creating a control, the identifier should start with IDC_ followed by a name. Examples: IDC_ADDRESS or IDC_FIRST_NAME
  • If you are creating a static control but plans to use it in your code, (you must) change its identifier to something more meaningful. If the static control will hold text and you plan to change that text, you can identify it with IDC_LABEL. For a static control used to display a picture of employees, you can identify it as IDC_EMPL_PICTURE

The identifiers of the controls used in your application must be listed in a header file called Resource.h

If you visually add controls to your application, their identifiers are automatically added to this file. Even if you change the identifier of the control, Visual C++ updates this file. You can also manually add identifiers to this file, provided you know why you are doing that.

If you are programmatically creating a control, you can locally set an identifier by specifying a (randomly selected) decimal or hexadecimal number as the nID argument of the Create() or the second version of the CreateEx() methods. Here are examples:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CWnd* btnApply = new CWnd;
	RECT Recto = {12, 16, 128, 64};
	btnApply->Create("BUTTON", "&Apply", WS_CHILD | WS_VISIBLE,
                     Recto, this, 0x10);

	CWnd *btnDismiss = new CWnd;
	btnDismiss->Create("BUTTON", "&Dismiss", WS_CHILD | WS_VISIBLE,
                       CRect(12, 68, 128, 120), this, 258);

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}

Alternatively, you can first create an identifier in the String Table. We saw how to do this in Lesson 3. You can also create an identifier using the Resource Symbols dialog box. Then use that identifier for your control. Here is an example:

BOOL CBordersDlg::OnInitDialog() 
{
	CDialog::OnInitDialog();
	
	// TODO: Add extra initialization here
	CWnd *btnDismiss = new CWnd;
	btnDismiss->Create("BUTTON", "&Dismiss", WS_CHILD | WS_VISIBLE,
                       CRect(12, 68, 128, 120), this, IDN_DISMISS);

	return TRUE;  // return TRUE unless you set the focus to a control
	              // EXCEPTION: OCX Property Pages should return FALSE
}
 

Control Functionality: Variables

There are two main ways you can refer to a control in your code. The first technique consists of using its identifier. This can help you find out what type of control the identifier refers to. In this case, you usually must cast the result to the right control's class. There are two prerequisites. First, you must know the ID or nID of the control you want to refer to. Then, you can call the CWnd::GetGldItem() method. It comes in two versions as follows:

CWnd* GetDlgItem(int nID) const;
void CWnd::GetDlgItem(int nID, HWND* phWnd) const;

To refer to a control in your code, you can also declare a variable for it. There are two types of variable you can use to refer to a control. If you are more interested in the value held by a control, then you can declare a value variable for it. The variable must be a valid type of value that the control can hold. For example, the text in a text-based control (such as an edit box or a combo box) is a CString. Therefore, you can declare a CString variable for the control. If you want to refer to the object as a control and get access to its properties, you should declare the variable as Control.

To help with making that decision, you should use either ClassWizard of MSVC 6 or the Add Member Variable Wizard of MSVC 7. Because the wizard also inserts the appropriate code in the header and source files, you should rely on it instead of manually declaring the variable(s).

If you want to refer to the object as a control and separately refer to its value, you can declare both the Value and the Control variables.


Previous Copyright © 2003-2004 FunctionX, Inc. Next