Delphi World - Как создать нестандартное окно сообщения
Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Как создать нестандартное окно сообщения

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

Проект Delphi World © Выпуск 2002 - 2017
Автор проекта: Эксклюзивные курсы программирования