скрыть

скрыть

  Форум  

Delphi FAQ - Часто задаваемые вопросы

| Базы данных | Графика и Игры | Интернет и Сети | Компоненты и Классы | Мультимедиа |
| ОС и Железо | Программа и Интерфейс | Рабочий стол | Синтаксис | Технологии | Файловая система |



Google  
 

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



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






Copyright © 2004-2016 "Delphi Sources". Delphi World FAQ




Группа ВКонтакте   Ссылка на Twitter   Группа на Facebook