Шаг 171 - Диалог - обновляем элементы интерфейса

Сегодня я предлагаю Вам научиться (почти) автоматически управлять состояние элементов интерфейса в нашем диалоге. Для нашего случая такими элементами являются пункты меню и "контролы", т.е. элементы управления. Чтобы упростить задачу и не отвлекаться на частности, используем проект из шага "Диалог - добавим акселераторы...".

Поставим себе такую задачу:

  1. Кнопка OK должна быть активной только в том случае, если поле редактирования (далее - редактор) не пусто.
  2. Пункт меню Help/About после первого выбора помечается галочкой, после последующих - становится неактивным.

Осталось немного - выполнить поставленную задачу. В принципе, для ее выполнения вовсе не обязательно знание MFC, достаточно иметь начальные знания о Win32 и вперед... Но MFC упрощает нашу работу, правда, обычно платой за простоту является размер исполняемого кода и его производительность.

В MFC для обновления элементов пользовательского интерфейса имеется специальный предназниченный для этого класс CCmdUI и обработчики ON_UPDATE_COMMAND_UI. Если не слишком углубляться в мрачные недра MFC, то алгоритм для добавления "самообновляющихся" свойств элементам управления несложен: нужно для каждого элемента определить функцию-обработчик со следующим прототипом:

afx_msg void OnUpdateConrol(CCmdUI* pCmdUI);

Имя функции не имеет никакого значения, главное, чтобы совпадали тип функции и тип ее единственного аргумента. После этого необходимо добавить в карту сообщений окна (в нашем случае - диалога) макросы со ссылками на обработчики.

ON_UPDATE_COMMAND_UI(ID_CONTROL, OnUpdateControl)

Наверное, достаточно теории, переходим к практике.

Начнем с первого пункта. Что там у нас? Сделать OK неактивным (или, как говорит один мой знакомый "засерить", от слова "серый" :-) при пустом редакторе? Нет проблем... Грузим в Visual Studio наш простенький проект, закладка "ClassView", правый тык на классе CAdvDlgDlg, в контекстном меню выбираем "Add member function...".

171_1.gif (3333 b)

Почему класс доступа "Protected"? Как я уже говорил на одном в прошлых шагов, ни к чему давать слишком много свободы...
Затем нажатием кнопки OK добавляем эту самую функцию.

Определение функции смотрите ниже:

void CAdvDlgDlg::OnUpdateOK(CCmdUI *pCmdUI)
  {
  BOOL bEnable =  GetDlgItem(IDC_EDIT)->GetWindowTextLength() != 0;
  pCmdUI->Enable( bEnable );
  }

Как видите, ничего сложного: получаем указатель на объект окна редактора, спрашиваем у редактора длину введенного текста, если длина ненулевая - делаем кнопку OK активной.

Если Вы руками работаете быстрее, чем читаете, и уже успели понажимать заветные комбинации клавиш F7, Ctrl+F5, то можете убедиться, что пока что ничего не изменилось. Так и должно быть, ведь надо внести изменения в карту сообщений. Вот так она выглядит у меня (изменения выделены):

BEGIN_MESSAGE_MAP(CAdvDlgDlg, CDialog)
  //{{AFX_MSG_MAP(CAdvDlgDlg)
  ON_WM_SYSCOMMAND()
  ON_WM_PAINT()
  ON_WM_QUERYDRAGICON()
  ON_COMMAND(ID_ABOUT, OnAbout)
  //}}AFX_MSG_MAP
  ON_UPDATE_COMMAND_UI(IDOK, OnUpdateOK)
END_MESSAGE_MAP()

Ну что, нетерпеливые, опять не работает? Ну тогда изменим еще одну функцию, уже знакомую нам по прошлому шагу...

BOOL CAdvDlgDlg::PreTranslateMessage(MSG* pMsg) 
  {
  // TODO: Add your specialized code here and/or call the base class
  if ( TranslateAccelerator( m_hWnd, hAccel, pMsg ) ) return TRUE;
  UpdateDialogControls(this, TRUE);
  return CDialog::PreTranslateMessage(pMsg);
  }

Скажу пару слов про UpdateDialogControls. Именно она для всех элементов интерфейса вызывает их обработчики, у нас пока что только OnUpdateOK. Первым аргументом в функцию UpdateDialogControls передается указатель на оконный объект, элементы которого будут обрабатываться, второй же по своему действию весьма необычен: TRUE приводит к тому, что элементы управления, не имеющие обработчиков ON_COMMAND, принудительно будут "засериваться", FALSE - никакой принудиловки не будет.

Нетерпеливые, как Вы там? Заработало? Поздравляю. Правда, нетерпеливые, но внимательные могут заметить, что при запуске программы возможна ситуация, когда редактор пуст, но кнопка OK активна. Причина проста - обновление интерфейса происходит при поступлении в окно сообщений, а их после запуска еще не поступало. Стоит при этом провести мышкой по окну, и первое же сообщение WM_MOUSEMOVE наведет порядок во внешнем виде программы. Чтобы избежать этой неоднозначности, можно пойти двумя путями: в функции CAdvDlgDlg::OnInitDialog вызвать еще раз UpdateDialogControls или послать самому себе какое-нибудь сообщение. Я выбрал второй вариант и добавил в конец CAdvDlgDlg::OnInitDialog одну строчку:

  PostMessage(WM_USER, 0, 0);

Вот теперь с первым пунктом поставленной задачи мы справились полностью. Кстати, обратите внимание на поведение кнопки 171_2.gif (152 b) ("Browse",идентификатор IDC_BROWSE), причины которого в последнем параметре TRUE функции UpdateDialogControls.

Переходим ко второй части задачи. Тут нам, как ни крути, потребуется некий счетчик. Делаем счетчик, добавляя переменную - член (гусары, молчать!:-) класса CAdvDlgDlg.

171_3.gif (2843 b)

Чтобы не тянуть резину, далее вкратце описываю свои действия: в конструкторе диалога инициализирую наш счетчик нулевым значением, после показа диалога About инкрементирую счетчик.

Теперь запускаем Class Wizard, в списке идентификаторов объектов класса CAdvDlgDlg находим ID_ABOUT, дважды кликаем на сообщении UPDATE_COMMAND_UI, тем самым соглашаясь на создание обработчика.

171_4.gif (3358 b)

Текст обработчика я написал такой, хотя можно было бы сделать его более эффективным:

void CAdvDlgDlg::OnUpdateAbout(CCmdUI *pCmdUI)
  {
  switch ( nShowAbout )
    {
    case 0:
      pCmdUI->Enable();
      break;
    case 1:
      pCmdUI->SetCheck( 1 );
      break;
    case 2:
      pCmdUI->SetCheck( 0 );
      pCmdUI->Enable( FALSE );
      break;
    default:
      break;
    }
  }

На этом подготовительные работы закончены, впереди самое интересное...

Интересность в том, что ограниченность диалога вновь не позволяет нам "автоматом" управлять состоянием элементов меню. Чтобы обойти это ограничение, надо добавить обработчик сообщения WM_INITMENUPOPUP. Но это тоже не просто, поскольку Class Wizard для диалога не предлагает такого обработчика, но мы обманем волшебника и сделаем по-своему. Запускаем Wizard (Ctrl+W), переключаемся на закладку "Class Info", в списке "Class name" выбираем CAdvDlgDlg, а вот в списке "Message filter" вместо Dialog ставим Topmost Frame, тем самым существенно расширяя список обработчиков сообщений в списке волшебника. Теперь уже спокойно добавляйте нужный нам обработчик и, если хотите, возвращайте фильтр сообщений в прежнее состояние. Идея данного обработчика заимствована (с некоторой модификацией) у John Wismar, оригинал статьи которого Вы можете прочитать здесь: "Handling OnUpdate() processing for menu items". А его текст смотрите ниже:

void CAdvDlgDlg::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu) 
  {
  CDialog::OnInitMenuPopup(pPopupMenu, nIndex, bSysMenu);
  if (!bSysMenu)
    {
    CCmdUI cmdUI;
    cmdUI.m_nIndexMax = pPopupMenu->GetMenuItemCount();
    for (UINT i = 0; i < cmdUI.m_nIndexMax; i++)
      {
      cmdUI.m_nIndex = i;
      cmdUI.m_nID = pPopupMenu->GetMenuItemID(i);
      cmdUI.m_pMenu = pPopupMenu;
      cmdUI.DoUpdate(this, TRUE);   
      }
    }
  }

В двух словах - обработчик при открывании меню перебирает его пункты и вызывает для каждого пункта функцию обновления. Собственно, на этом все. Единственное, на что хочется обратить Ваше внимание - нажатие кнопки-акселератора F12 полностью эквивалентно выбору соответствующего пункта меню. Запускайте, проверяйте, пишите о замеченных ошибках...

Шаг прислал Галицкий Игорь (ig@ntvplus.ru)


Загрузить проект | Предыдущий Шаг | Следующий Шаг | Оглавление
Автор Каев Артем - 17.04.2000