Как создать нестандартное окно сообщения
Оформил: DeeCo
Автор: Paul Bludov
Введение
Окна сообщения (Message Box) – это стандартные диалоговые окна, используемые
в программах для информирования пользователя, предупреждения или уточнения его
желаний. Типичное окно сообщения выглядит так:
Рисунок 1. Типичное окно собщения.
Для вывода окна сообщения служит функция Windows API
::MessageBox().
intMessageBox
(HWNDhWnd,
LPCTSTRlpText,
LPCTSTRlpCaption,
UINTuType
);
Параметр hWnd – это родительское окно. Как правило, это
главное окно приложения. Если приложение не имеет окон (например, консольное
приложение), этот параметр может быть равен NULL.
Параметр lpText – это собственно текст сообщения.
Параметр lpCaption – это заголовок окна сообщения. Если он
равен NULL, используется строка "Ошибка".
Параметр uType задает количество кнопок и другие параметры
окна сообщения. С его помощью можно задать иконку слева от текста и такие
свойства окна, как модальность (modality).
К сожалению, этого иногда оказывается недостаточно. Например, нужна
возможность подавления сообщения в будущем, что-то вроде:
Рисунок 2. Окно сообщения с 'галочкой'.
Как же расширить возможности этой функции?
Нестандартное окно сообщения
Способ №1: диалоговое окно
Первое, что приходит на ум – создать диалоговое окно, и расставить на нем все
нужные кнопки. Это наиболее простой способ.
INT_PTR CALLBACK _CustomDialogProc
(HWND hwndDlg,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
if (WM_COMMAND == uMsg)
::EndDialog(hwndDlg, LOWORD(wParam));
return FALSE;
}
int nRet = : : DialogBoxParam(hInstance, MAKEINTRESOURCE(ID_CUSTOMDIALOG),
NULL, _CustomDialogProc, 0);
Но, к сожалению, это и наиболее трудоемкий способ. Все эти диалоги нужно
сначала нарисовать. Кроме того, каждое из таких "неуниверсальных" диалоговых
окон увеличивает размер программы.
Способ №2: универсальное диалоговое окно
Если программе нужно выводить большое количество сообщений, и ::MessageBox()
по каким-либо причинам не подходит, можно написать свой аналог.
Для этого понадобится заготовка – небольшой диалог со всеми кнопками, которые
могут понадобиться, и двумя полями для текста и иконки, плюс немного кода, чтобы
"спрятать" лишние кнопки и настроить текстовое поле и иконку. Листинг 1. Код инициализации диалога
LRESULT _CustomMessageBoxInit(HWND hwndDlg, _SCustomMessageBoxParam * pInit)
{
// Расстояние между кнопками, а также бордюр
const int nBorder = 11;
UINT uType = pInit->m_uType;
RECT rect;
RECT rectButton;
int nVisibleButtons = 0;
int nVisibleButtonsWidth = 0;
HDC hdcDlg;
HWND hwndText = ::GetDlgItem(hwndDlg, ID_MSGBOXTEXT);
// Заголовок окна
if (pInit->m_lpCaption)
::SetWindowText(hwndDlg, pInit->m_lpCaption);
// Текст окна
::SetWindowText(hwndText, pInit->m_lpText);
// Включаем нужные кнопки
nVisibleButtons = _CustomMessageBoxShowButtons(hwndDlg, uType);
// Устанавливаем иконку
_CustomMessageBoxSetIcon(hwndDlg, uType);
// Подсчитываем размер текста
::GetClientRect(hwndText, &rect);
rect.top = rect.left = nBorder;
rect.right += nBorder;
rect.bottom = 0;
hdcDlg = ::GetWindowDC(hwndDlg);
::DrawText(hdcDlg, pInit->m_lpText, -1, &rect,
DT_LEFT | DT_EXPANDTABS | DT_WORDBREAK | DT_CALCRECT);
::ReleaseDC(hwndDlg, hdcDlg);
::SetWindowPos(hwndText, NULL, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
((MB_ICONMASK & uType) ? SWP_NOMOVE : 0 )
| SWP_NOZORDER | SWP_NOREDRAW | SWP_NOACTIVATE);
if (MB_ICONMASK & uType)
{
int nIconHeight = ::GetSystemMetrics(SM_CYICON);
if (rect.bottom - rect.top < nIconHeight)
rect.bottom = rect.top + nIconHeight;
}
// Расставляем кнопки
: : GetClientRect(: : GetDlgItem(hwndDlg, IDOK), & rectButton);
nVisibleButtonsWidth = (nVisibleButtons * (rectButton.right + nBorder));
if (rect.right < nVisibleButtonsWidth)
{
rect.right = nVisibleButtonsWidth;
_CustomMessageBoxInitPositionButtons(hwndDlg, nBorder, rect.bottom,
nBorder + rectButton.right, (uType & MB_DEFMASK) >> 8);
}
else
{
_CustomMessageBoxInitPositionButtons(hwndDlg,
(rect.right - nVisibleButtonsWidth) / 2, rect.bottom,
nBorder + rectButton.right, (uType & MB_DEFMASK) >> 8);
}
// Пересчитываем размеры самого диалога
rect.right + = nBorder * 2;
rect.bottom + = (rectButton.bottom + nBorder * 2);
: : AdjustWindowRectEx(& rect, : : GetWindowLong(hwndDlg, GWL_STYLE)
, FALSE, : : GetWindowLong(hwndDlg, GWL_EXSTYLE));
_CenterWindow(hwndDlg, & rect);
return 0;
}
Способ №3: Настоящий MessageBox + хук.
Оба предыдущих способа имеют ряд недостатков: Во-первых, никто не знает, как
будут выглядеть окна сообщений в следующей версии Windows. Возможно, у них будут
четыре дополнительных кнопки в заголовке или кнопки зеленого цвета. Наши же
диалоги будут выглядеть нормально – как и положено диалогам. Во-вторых, эти
способы не содержат кода для поддержки таких режимов стандартных окон сообщений,
как MB_TASKMODAL. В этом случае, можно воспользоваться хуками Windows.
СОВЕТ
Подробнее о хуках можно прочитать на http://www.rsdn.ru/article/?baseserv/winhooks.xml
Все, что нужно – это установить локальный хук, вызвать ::MessageBox(),
выполнить в обработчике хука все необходимые действия и снять хук по завершении
::MessageBox().
Тут имеется небольшая проблема: стандартное окно сообщения использует
локальный цикл обработки сообщений (message pump), и окон, появившихся в
результате вызова ::MessageBox(), может быть несколько. На самом деле все не так
плохо: первое оповещение типа HCBT_CREATEWND, пришедшее в наш обработчик, даст
нам HWND окна сообщения, которое мы и будем использовать в дальнейшем. Листинг 2. Код, добавляющий 'галочку' в стандартное окно
сообщения
class CMessageBoxPatcher
: public CThunk<
CMessageBoxPatcher, HOOKPROC>
{
BOOL CalcCheckBoxRect
( RECT *prectCheckBox
, int *nGap
)
{
HWND hwndTextOrIcon;
RECT rectTmp;
// Ищем иконку или текст, если иконки нет
hwndTextOrIcon = ::FindWindowEx(m_hwndMessageBox, NULL,
_T("STATIC"), NULL);
if (!hwndTextOrIcon)
return FALSE;
if (!::GetWindowRect(hwndTextOrIcon, &rectTmp))
return FALSE;
// Тут мы получили .left, отступ по вертикали, и, возможно, .bottom
prectCheckBox->left = rectTmp.left;
::MapWindowPoints(NULL, m_hwndMessageBox, (LPPOINT)&rectTmp, 1);
*nGap = rectTmp.top;
prectCheckBox->bottom = rectTmp.bottom;
// Ищем текст (если до этого нашли иконку)
hwndTextOrIcon = ::FindWindowEx(m_hwndMessageBox, hwndTextOrIcon
, _T("STATIC"), NULL);
if (hwndTextOrIcon && !::GetWindowRect(hwndTextOrIcon, &rectTmp))
return FALSE;
// получили .right && .bottom
prectCheckBox->right = rectTmp.right;
if (rectTmp.bottom > prectCheckBox->bottom)
prectCheckBox->bottom = rectTmp.bottom;
// Теперь нужно рассчитать размер текста и галочки
HDC hdcMessageBox = ::GetWindowDC(m_hwndMessageBox);
if (!hdcMessageBox)
return FALSE;
rectTmp.left = ::GetSystemMetrics(SM_CXMENUCHECK);
rectTmp.right -= prectCheckBox->left;
rectTmp.top = 0;
rectTmp.bottom = 0x4000;
::DrawText(hdcMessageBox, m_lpCheckBoxString, -1, &rectTmp,
DT_CALCRECT | DT_WORDBREAK | DT_NOPREFIX);
::ReleaseDC(m_hwndMessageBox, hdcMessageBox);
// Получили .top
prectCheckBox->top = prectCheckBox->bottom - rectTmp.bottom;
return ::MapWindowPoints(NULL, m_hwndMessageBox,
(LPPOINT)prectCheckBox, 2);
}
HWND InsetCheckBox()
{
RECT rectCheckBox;
RECT rectWindow;
int nHeightGrow;
HWND hwndCheckBox = NULL;
if (!CalcCheckBoxRect(&rectCheckBox, &nHeightGrow))
return NULL;
// Создаем галочку
hwndCheckBox = ::CreateWindowEx(WS_EX_NOPARENTNOTIFY, _T("BUTTON"),
m_lpCheckBoxString, BS_LEFT | BS_AUTOCHECKBOX | BS_MULTILINE
| WS_TABSTOP | WS_CHILD | WS_VISIBLE,
rectCheckBox.left, rectCheckBox.top,
rectCheckBox.right - rectCheckBox.left,
rectCheckBox.bottom - rectCheckBox.top,
m_hwndMessageBox, NULL, NULL, 0);
if (hwndCheckBox)
{
// Устанавливаем нужный шрифт
::SendMessage(hwndCheckBox, WM_SETFONT,
::SendMessage(m_hwndMessageBox, WM_GETFONT, 0, 0), FALSE);
// Выставляем начальное состояние
if (m_bNoMore)
::SendMessage(hwndCheckBox, BM_SETCHECK, BST_CHECKED, 0);
}
// Увеличиваем окно и сдвигаем все кнопки вниз
if (: : GetWindowRect(m_hwndMessageBox, & rectWindow))
{
nHeightGrow += (rectCheckBox.bottom - rectCheckBox.top);
::SetWindowPos(m_hwndMessageBox, NULL, 0, 0,
rectWindow.right - rectWindow.left,
rectWindow.bottom - rectWindow.top + nHeightGrow,
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW);
MoveButtonsDown(nHeightGrow);
}
return m_hwndCheckBox = hwndCheckBox;
}
void MoveButtonsDown
(int nDistance
)
{
HWND hwndButton = NULL;
RECT rectButton;
while (hwndButton = ::FindWindowEx(m_hwndMessageBox, hwndButton,
_T("BUTTON"), NULL), hwndButton)
{
::GetWindowRect(hwndButton, &rectButton);
::MapWindowPoints(NULL, m_hwndMessageBox, (LPPOINT)&rectButton, 2);
::SetWindowPos(hwndButton, NULL, rectButton.left,
rectButton.top + nDistance, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW);
}
}
bool IsOurWindow
(HWND hwnd
)const
{
ATLASSERT(m_hwndMessageBox);
return m_hwndMessageBox == hwnd;
}
LRESULT CBTProc
(int nCode,
WPARAM wParam,
LPARAM lParam
)
{
HWND hwnd = (HWND)wParam;
if (HCBT_CREATEWND == nCode && !m_hwndMessageBox)
m_hwndMessageBox = hwnd;
else if (HCBT_ACTIVATE == nCode && !m_hwndCheckBox && IsOurWindow(hwnd))
InsetCheckBox();
else if (HCBT_DESTROYWND == nCode && IsOurWindow(hwnd))
m_bNoMore = (BST_CHECKED == ::SendMessage(m_hwndCheckBox,
BM_GETCHECK, 0, 0));
return ::CallNextHookEx(m_hHook, nCode, wParam, lParam);
}
public:
CMessageBoxPatcher
(LPCTSTR lpCheckBoxString,
bool bNoMoreByDefault = false
)
: CThunk<
CMessageBoxPatcher, HOOKPROC>
((TMFP)CBTProc, this),
m_bNoMore(bNoMoreByDefault),
m_lpCheckBoxString(lpCheckBoxString),
m_hwndCheckBox(NULL),
m_hwndMessageBox(NULL)
{
m_hHook = ::SetWindowsHookEx(WH_CBT, GetThunk(), NULL,
::GetCurrentThreadId());
}
~CMessageBoxPatcher()
{
if (m_hHook)
::UnhookWindowsHookEx(m_hHook);
}
bool GetBoxState()const
{
return m_bNoMore;
}
private:
HHOOK m_hHook;
HWND m_hwndCheckBox;
HWND m_hwndMessageBox;
bool m_bNoMore;
LPCTSTR m_lpCheckBoxString;
};
inline int WINAPI MessageBox
(in HWND hwnd,
in LPCTSTR lpText,
in LPCTSTR lpCaption,
in UINT uType,
in LPCTSTR lpCheckBoxString,
in out PBOOL pbNoMore
)
{
CMessageBoxPatcher patcher(lpCheckBoxString, !!*pbNoMore);
int nRet;
nRet = ::MessageBox(hwnd, lpText, lpCaption, uType);
*pbNoMore = patcher.GetBoxState();
return nRet;
}
ПРИМЕЧАНИЕ
Чтобы "превратить" обработчик хука в функцию-член класса, в данном
примере используется механизм переходников, thunks.
100% гарантии не дает и этот способ: он рассчитан на то, что у окна сообщения
в следующей версии Windows не будет, например, двух иконок, или кнопок
сверху.
|