Showing posts with label GUI. Show all posts
Showing posts with label GUI. Show all posts

Thursday, 13 October 2011

What to do when sending message to some window fails or crashes the application

You want to send a message from one window to another by calling CWnd::SendMessage(...) or CWnd::PostMessage(...) and application crashes during these calls. How to find the cause of the crash?

We first need to determine the nature of the crash. Open crash dump, read message in Windows Debugger Dialog Box, read log file (if any). Functions above are called on the window through its pointer so check whether pointer is valid (not NULL - in which case you'll have access violation exception). Get window's handle and check whether it's valid (IsWindow(...)). If message is supposed to be sent to window's parent/owner, make sure that sending and receiving window are of proper type and in desired relation (parent/child or owner/owned).

It is very important to understand types of windows and relationships between them. They are all set at the time of window creation.

When creating some window with Create(...)/CreateEx(...) we need to pass two important arguments to these functions:
1) window styles
2) parent/owner window

Window styles define the nature (type) of the window and its appearance. There are three types of windows: overlapped, pop-up and child. For example, main application window is an overlapped window, which has some embedded controls (e.g. buttons) and these controls are child windows. When we press some of those buttons, a dialog box can appear and dialog box is a pop-up window. Each of these three types has its own flag within Window styles and they are defined in WinUser.hWS_OVERLAPPED (0x00000000L), WS_POPUP (0x80000000L), WS_CHILD (0x40000000L). Window style can have only one of these flags set (e.g. window cannot be pop-up and child at the same time). If you omit setting style, your window will be overlapped by default and no matter the fact you passed the pointer/handler of some window as its e.g. parent, you might not get expected behaviour as this window will not actually be of a child type!

When passing pointer or handler to parent/owner window, make sure that pointer is not NULL, and that handler is a valid handler of parent/owner at the time of the creation of child/owned window (use IsWindow()).

Do the same for window you're calling SendMessage/PostMessage on.

You can use this helper function which analyses some arbitrary window:

WndHelper.h:

#ifndef _WNDHELPER_H
#define _WNDHELPER_H

#include <afxwin.h>

// Usage:
// CWnd* pWnd = ...
// AnalyzeWnd(pWnd);
void AnalyzeWnd(const CWnd* pWnd);

#endif // _WNDHELPER_H

WndHelper.cpp:


#include "Include\WndHelper.h"

void AnalyzeWnd(const CWnd* pWnd)
{
TRACE(_T("AnalyzeWnd()"));

if(!pWnd)
{
TRACE(_T("pWnd is NULL!"));
return;
}

//
// Get Desktop
//

CWnd* pWndDesktop = pWnd->GetDesktopWindow();

if(pWndDesktop)
{
TRACE(_T("GetDesktopWindow() returned valid window pointer. pWndDesktop = %p"), pWndDesktop);

HWND hWndDesktop = pWndDesktop->GetSafeHwnd();
TRACE(_T("hWndDesktop = %p"), hWndDesktop);
}
else
{
TRACE(_T("GetDesktopWindow() returned NULL!"));
}

//
// Analyze Window
//

TRACE(_T("pWnd = %p"), pWnd);

HWND hWnd = pWnd->GetSafeHwnd();

if(::IsWindow(hWnd))
{
TRACE(_T("GetSafeHwnd() returned valid window handle. hWnd = %p"), hWnd);
}
else
{
TRACE(_T("GetSafeHwnd() returned INVALID window handle!"));
}

if(::IsWindow(pWnd->m_hWnd))
{
TRACE(_T("m_hWnd is a valid window handle. pWnd->m_hWnd = %p"), pWnd->m_hWnd);

TRACE(_T("Window styles: "));
LONG lStyle = ::GetWindowLong(pWnd->m_hWnd, GWL_STYLE);

if(lStyle == WS_OVERLAPPED) // 0
{
TRACE(_T("\t\tWS_OVERLAPPED (0)"));
}
else
{
if((lStyle & WS_POPUP) == WS_POPUP)
TRACE(_T("\t\tWS_POPUP"));

if((lStyle & WS_CHILD) == WS_CHILD)
TRACE(_T("\t\tWS_CHILD"));

if((lStyle & WS_VISIBLE) == WS_VISIBLE)
TRACE(_T("\t\tWS_VISIBLE"));

if((lStyle & WS_DISABLED) == WS_DISABLED)
TRACE(_T("\t\tWS_DISABLED"));

if((lStyle & WS_CAPTION) == WS_CAPTION)
TRACE(_T("\t\tWS_CAPTION"));

if((lStyle & WS_BORDER) == WS_BORDER)
TRACE(_T("\t\tWS_BORDER"));

if((lStyle & WS_DLGFRAME) == WS_DLGFRAME)
LOGGER_PRINTF(Debug, _T("\t\tWS_DLGFRAME"));

if((lStyle & WS_SYSMENU) == WS_SYSMENU)
TRACE(_T("\t\tWS_SYSMENU"));

// todo: add checks for other styles...
}

// todo: check extended styles
// LONG lStyleEx = GetWindowLong(GetSafeHwnd(), GWL_EXSTYLE);
}
else
{
TRACE(_T("pWnd->m_hWnd is an INVALID window handle!"));
}

hWnd = pWnd->m_hWndOwner;

if(::IsWindow(hWnd))
{
TRACE(_T("m_hWndOwner is a valid window handle. m_hWndOwner = %p"), hWnd);
}
else
{
TRACE(_T("m_hWndOwner is an INVALID window handle!"));
}

//
// Analyze Parent
//

CWnd* pWndParent = pWnd->GetParent();

if(pWndParent)
{
TRACE(_T("GetParent() returned valid pointer. pWndParent = %p"), pWndParent);

HWND hWnd = pWndParent->GetSafeHwnd();

if(::IsWindow(hWnd))
{
TRACE(_T("GetSafeHwnd() returned valid window handle. hWnd = %p"), hWnd);
}
else
{
TRACE(_T("GetSafeHwnd() returned INVALID window handle!"));
}
}
else
{
TRACE(_T("GetParent() returned NULL!"));
}

CWnd* pWndParentOwner = pWnd->GetParentOwner();

if(pWndParentOwner)
{
TRACE(_T("GetParentOwner() returned valid pointer. pWndParentOwner = %p"), pWndParentOwner);

HWND hWnd = pWndParentOwner->GetSafeHwnd();

if(::IsWindow(hWnd))
{
TRACE(_T("GetSafeHwnd() returned valid window handle. hWnd = %p"), hWnd);
}
else
{
TRACE(_T("GetSafeHwnd() returned INVALID window handle!"));
}
}
else
{
TRACE(_T("GetParentOwner() returned NULL!"));
}

//
// Analyze Owner
//

CWnd* pWndOwner = pWnd->GetOwner();

if(pWndOwner)
{
TRACE(_T("GetOwner() returned valid pointer. pWndOwner = %p"), pWndOwner);

HWND hWnd = pWndOwner->GetSafeHwnd();

if(::IsWindow(hWnd))
{
TRACE(_T("GetSafeHwnd() returned valid window handle. hWnd = %p"), hWnd);
}
else
{
TRACE(_T("GetSafeHwnd() returned INVALID window handle!"));
}
}
else
{
TRACE(_T("GetOwner() returned NULL!"));
}

CWnd* pWndSafeOwner = pWnd->GetSafeOwner();

if(pWndSafeOwner)
{
TRACE(_T("GetSafeOwner() returned valid pointer. pWndSafeOwner = %p"), pWndSafeOwner);

HWND hWnd = pWndSafeOwner->GetSafeHwnd();

if(::IsWindow(hWnd))
{
TRACE(_T("GetSafeHwnd() returned valid window handle. hWnd = %p"), hWnd);
}
else
{
TRACE(_T("GetSafeHwnd() returned INVALID window handle!"));
}
}
else
{
TRACE(_T("GetSafeOwner() returned NULL!"));
}
}

Links and references:
CWnd::SendMessage (MSDN)
CWnd::PostMessage (MSDN)

Monday, 19 September 2011

Message only window (MFC)

As expected: message only window cannot post messages to its "parent". CWnd::PostMessage() invoked on HWND_MESSAGE crashes with access violation:

class CMsgOnlyWnd : public CWnd
{
   ...
public:
   Initialize();
   SendMsgToParent();
   ...
}

CMsgOnlyWnd::Initialize()
{
   ...
   CreateEx(0, ::AfxRegisterWndClass(NULL), "MsgOnlyWnd", 0, 0, 0, 0, 0, HWND_MESSAGE, 0, NULL);
   ...
}

CMsgOnlyWnd::SendMsgToParent()
{
   GetParent()->PostMessage(...);
}

Links and references:
Message-Only Windows (MSDN)
How to make a Message Only Window
How to create a hidden window in C++

MFC File dialog

GUI applications use File dialog to allow user to select file in order to perform some operation on it. MFC offers ready to use CFileDialog class (declared in afxdlgs.h).

It is possible to define a filter for file types that will appear in Files list box. File types are listed in combo box which is located at the bottom of the dialog. Filter is defined with a string made of string couples. First string in a couple is the one that will appear in the dialog's combo box and the second one is the extension itself. All string items are separated with vertical tab and the filter string must end with two vertical tabs.

This code will add two items in file type filter combo box:
 EXE Files (*.exe)
 CAB Files (*.cab)

char pszFilter[] = {"EXE Files (*.exe)|*.exe|CAB Files (*.cab)|*.cab||"};

CFileDialog FileDlg(TRUE, ".exe", NULL, 0, pszFilter);

if(FileDlg.DoModal() == IDOK)
{
   CString strSelectedFileFullPath(FileDlg.GetPathName());
   CString strSelectedFileName(FileDlg.GetFileName());
   ...
}
else
{
   ...
}

If we define filter as:
char pszFilter[] = {"MyApp Setup Files (*.exe;*.cab)|*.exe;*.cab||"};
only exe and cab files will appear in file list box and combo box will have a single item:
   MyApp Setup Files (*.exe;*.cab)

Filter defined as:
char pszFilter[] = {"EXE Files (*.exe)|*.exe|All Files (*.*)|*.*||"};
adds two items to combo box:
   EXE Files (*.exe)
   All Files (*.*)

Two useful methods of this class are:
  • GetPathName() - returns full path to the selected file (including file name)
  • GetFileName() - returns a name of the selected file

CFileDialog allows selecting file either on a local machine or from a shared directory on a network. In the latter case path returned by GetPathName() is a network path (starting with \\remote_host_name\shared_dir\...).

Thursday, 18 August 2011

How to handle event when user clicks on a main menu item

When you create SDI or MDI application project in Visual Studio, Wizard automatically adds a menu bar to the main window. This default, statically created menu has three items "File", "Edit" and "Help". "File"'s submenu has only one item - "Exit", but we can add our custom items. Submenu is basically a popup window and framework notifies main window when this popup is about to become active by sending WM_INITMENUPOPUP message. We can capture this event, identify popup and change default processing of this message by overloading CWnd::OnInitMenuPopup in our class.

Let's say we have added (in the resource editor) "Foo" item in a "File" submenu. All items in a main menu are identified by their indexes. Index of the first item, "File", is 0, "Edit" - 1, etc. These numbers identify these items' submenus (popups) as well and they are passed as the second argument of OnInitMenuPopup method. Now we want to enable/disable menu item named "Foo", depending on the value of some flag. Following code shows how to achieve this:


Links and references:
MFC Dialog: How to enhance a dialog-based application with Menu, Toolbar...?

How to restrict resizing a window

We want to allow user to resize dialog up to certain minimum/maximum dimensions. This can be done by overloading CWnd::OnGetMinMaxInfo in your class. This method is WM_GETMINMAXINFO message handler in which you can set window's maximum and minimum size. This applies to any class derived from CWnd (so including classes derived from CDialog, CFrameWnd, CFormView...).

Links and references:

How to set the Minimum and Maximum window size while Resizing?
Control client area minimum size with MFC
MFC General: How to prevent a resizable window to be smaller than...?

Simple application with two panes and a splitter

Applications of this type are very common: we have some objects, or items, listed in a left pane and when we click on some item its attributes are displayed in the right pane. Items can be listed within a tree control, and right click context menu possibly provides actions we can perform on them. But let us focus now only on basic: a splitter window with two panes.

The quickest way to make application with such layout in MFC is by creating SDI (Single Document Interface) project. Document/View support is not required. Wizard creates CMainFrame class which is the main window of SDI application. Wizard also adds to it a View class - a child window that occupies the client area of the main frame:

Application looks like this:

SplitterTest1_5

We want to embed Splitter window into the main frame instead of this default View so we need to replace CChildView with instance of CSplitterWnd:

The right place to put code that creates controls in the main client area of the frame window is overloaded CFrameWnd::OnCreateClient.

Now how to overload parent's implementation of this method? The easiest way to insert various message handlers and overloads into MFC class is via class properties window:

  • go to class declaration in its header file
  • click on the class name
  • in the main menu in Visual Studio go to View->Other Windows->Properties Window
  • click Properties, Events, Messages or Overrides button in order to change properties or add event/messages handlers or to override base class methods

SplitterTest1_8

In this case, we click on Overrides, find and select OnCreateClient in the left side of the properties window, click in the value field and select Add OnCreateClient. This will automatically insert all necessary code in our class. This is default implementation of this overload:

We want to have static layout - left and right pane and a splitter bar all the time so will use CSplitterWnd::CreateStatic. Creating static splitter requires creating its panes within OnCreateClient.

A pane within a splitter window is usually a window derived from CView class. As we want to embed various controls (e.g. CTreeCtrl) in it, we will use CFormView, a class very similar to CDilaog. We could embed controls in a CView-derived class but using CFormView makes life simpler as allows using Visual Studio resource editor to design a form (properties, children controls) which is associated to CFormView object.

So how to add CFormView windows to our project? Just add a new Dialog in the resource editor and make sure its Style in Properties is set to Child. Optionally, remove Border and System menu. By default, this dialog has OK and Cancel button controls and we can leave them.

SplitterTest1_7

Right click on the dialog and pick "Add Class..." in order to associate a class with this resource. Select CFormView for the base class. I picked CLeftPaneFormView and CRightPaneFormView as names for my panes' classes.

SplitterTest1_6

Now when having panes ready, we can instruct our splitter to create them as its views:

If we run this application, we will see our dialogs with their children controls (OK and cancel buttons) embedded as the left and right pane in the splitter window - just what we wanted!

SplitterTest1_9

Throughout the code in main frame, panes can be accessed with this code:



Links and references:

TN029: Splitter Windows
MFC Tutorial
Visual C++ tutorial

Wednesday, 17 August 2011

Message Box with custom commit buttons

Windows API and MFC framework offer ready to use Message Box dialog which can display, depending on a provided flag, one or group of more commit buttons with captions from a predefined set: OK, Yes, No, Cancel, Abort, Retry and Ignore. If dialog contains a question, it is a good practice to make buttons displaying specific responses, e.g. "Run" - "Don't run", "Apply" - "Don't apply" etc. The easiest and quickest way to achieve this is to create a new, custom dialog and add onto it buttons with desired text. Resource editor in Visual Studio creates by default dialog with two buttons, OK and Cancel and you can rename them, providing that new captions will match OK and Cancel functionality. For other buttons, add BN_CLICKED handlers (ON_BN_CLICKED entry in message map). The purpose of this dialog is just to pick the user's answer on a question and not to do any hard work. Caller should read return value of (custom) Message Box and take further actions depending on it.

Return value of the standard message box (one from a predefined set IDOK, IDYES, ...) identifies user's choice and we can return these predefined values or our custom return codes, e.g. ID_RUN, ID_APPLY...We can also reuse resource ID values of our custom buttons for this purpose. Standard Message Box is a modal dialog so our dialog needs to be created as modal (CDialog::DoModal). DoModal returns value that was passed to CDialog::EndDialog. This function should be called in our custom button BN_CLICKED handlers.

In the following example, our custom Message Box has button "Apply". Its BN_CLICKED handler is OnBnClickedButtonApply. Caller identifies user's choice with DoModal return value:

Our Message Box which has "Apply" button:

Caller: