Сообщений 21    Оценка 445        Оценить  
Система Orphus

Message Box и немного фантазии

Как создать нестандартное окно сообщения

Автор: Paul Bludov
The RSDN Group

Источник: RSDN Magazine #1-2003
Опубликовано: 29.05.2003
Исправлено: 13.03.2005
Версия текста: 1.0
Введение
Нестандартное окно сообщения
Способ №1: диалоговое окно
Способ №2: универсальное диалоговое окно
Способ №3: Настоящий MessageBox + хук.

Демонстрационный проект MBTest (WinAPI, 14.5k)

Введение

Окна сообщения (Message Box) – это стандартные диалоговые окна, используемые в программах для информирования пользователя, предупреждения или уточнения его желаний. Типичное окно сообщения выглядит так:


Рисунок 1. Типичное окно собщения.

Для вывода окна сообщения служит функция Windows API ::MessageBox().

int MessageBox
    ( HWND hWnd,
    LPCTSTR lpText,
    LPCTSTR lpCaption,
    UINT uType
    );

Параметр 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() по каким-либо причинам не подходит, можно написать свой аналог.

Для этого понадобится заготовка – небольшой диалог со всеми кнопками, которые могут понадобиться, и двумя полями для текста и иконки, плюс немного кода, чтобы "спрятать" лишние кнопки и настроить текстовое поле и иконку.

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 окна сообщения, которое мы и будем использовать в дальнейшем.

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 не будет, например, двух иконок, или кнопок сверху.


Эта статья опубликована в журнале RSDN Magazine #1-2003. Информацию о журнале можно найти здесь
    Сообщений 21    Оценка 445        Оценить