ПРОГРАММИРОВАНИЕ НА VISUAL C++

Выпуск No. 18 от 7 октября 2000 г.

Приветствую вас!

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

И еще, хочу чтобы вы приняли к сведению: я отвечаю НЕ НА ВСЕ приходящие письма. Когда вы присылаете мне вопрос, не нужно рассчитывать на то, что я обязательно на него отвечу или помещу в рубрику "В поисках истины".

В обязательном порядке это делается только для тех читателей, кто до этого сам присылал статьи или ответы на вопросы (и которые были впоследствии опубликованы в рассылке).

/ / / / MFC / / / / / / / / / / / / / / / / / / / / / / / / / / /

Сегодня я хочу представить вашему вниманию статью - перевод из MSJ C++ Q&A, которую прислал Илья Простакишин.

Решение проблемы с OnIdle в dialog-based приложениях

Проблема в том, что OnIdle работает нормально только в приложениях document/view, что не скажешь о приложениях dialog-based. Функция CApp::InitInstance вызывает dlg.DoModal, которая в свою очередь вызывает CWnd::RunModalLoop, а та никогда не обращается к OnIdle. Кажется, что можно производить фоновую обработку посредством WM_ENTERIDLE, но это сообщение направляется только родительскому окну диалога, которого в нашем случае просто не существует. Как решить эту проблему?

Модальные диалоги на самом деле лишь имитируются в MFC. Когда вы вызываете CDialog::DoModal, MFC не вызывает ::DialogBox, как можно было ожидать; вместо этого вызывается ::CreateDialogIndirect, затем модальное поведение имитируется путем блокировки родительского окна и запуска своего собственного цикла обработки сообщений. По существу, тоже самое делает функция ::DialogBox. Тогда зачем изобретать велосипед? А дело в том, что теперь MFC имеет свой собственный цикл, в то время как раньше он "прятался" внутри функции API ::DialogBox. Это позволяет MFC обрабатывать сообщения посредством обычных потоков MFC (CWinThread::PumpMessage), что и делается с другими типами окон. В результате вы можете переопределять CWnd::PreTranslateMessage для модальных диалогов - например, для реализации "горячих" клавиш. Ранние версии MFC позволяли реализовывать свою собственную функцию PreTranslateMessage для модального диалога. Но толку от этого было мало, ведь она все равно никогда не вызывалась, т.к. CDialogDoModal напрямую обращалась к ::DialogBox. При этом управление в программу не возвращалось, пока один из обработчиков сообщений вашего диалога не вызывал EndDialog. По этой же причине была невозможна обработка интервала ожидания.

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

Хорошо, если все сообщения модального диалога проходят через стандартную очередь, почему не вызывается CWnd::OnIdle, как часть этого процесса? Проблема в том, что функция CWnd::RunModalLoop вызывает CWinThread::PumpMessage, однако OnIdle вызывается только внутри CWinThread::Run. MFC вызывает CWinThread::Run для запуска вашего приложения после вызова функции InitInstance. В сокращенном виде функция CWinThread::Run выглядит так:

// (from THRDCORE.CPP)
int CWinThread::Run()
{
  // for doing idle cycle
  BOOL bIdle = TRUE;
  LONG lIdleCount = 0;
  for (;;)
  {
     while (bIdle && !::PeekMessage(...))
     {
      // call OnIdle while in bIdle state
     
if (!OnIdle(lIdleCount++)) // assume "no idle" state
     
bIdle = FALSE;
     }
     // Get/Translate/Dispatch the message
     // (calls CWinThread::PumpMessage) 
     ...
 
}
}

Я убрал все лишнее, чтобы заострить внимание на том, как реализована обработка интервала ожидания. Если соообщений не поступает, MFC вызывает CWinThread::OnIdle, каждый раз увеличивая аргумент-счетчик. Вы можете использовать этот счетчик для установки приоритета различиных обработчиков интервала ожидания. Например, можно выполнять форматирование (диска C:, например :)), когда счетчик ожидания равен 1, затем обновлять показания часов при счетчике, равном 2 и т.п. Если OnIdle возвращает FALSE, то MFC прекращает его вызывать и ждет пока ваш поток получит какое-нибудь сообщение, просле чего обработчик будет вызываться снова.

Обратите внимание, что модальный диалог никогда не будет выполнять этот код, поскольку CWnd::RunModalLoop вызывает CWinThread::PumpMessage сразу из своего собственного цикла обработки сообщений. Он не вызывает CWinThread::Run и, следовательно, никогда не обращается к CWinThread::OnIdle. По-видимому, так было задумано разработчиками. Очевидно, что опасно вызывать OnIdle внутри модального диалога, поскольку многие обработчики сообщений создают временные объекты CWnd, которые, вполне возможно, будут существовать все время, пока существует диалог. Частью процесса обработки OnIdle является освобождение временных карт дескрипторов (handle).

Не могу удержаться от упоминания о том, что механизм временной/постоянной карты дескрипторов (handle map), используемый для связывания дескрипторов HWND с классами CWnd - это один из самых потенциально опасных элементов каркаса приложения, даже более опасный, чем карты сообщений. Проблема "временной карты" непрерывно преследует программистов, особенно в многопотоковых приложениях, делая их трудными для написания в MFC.

Итак, каким же образом обрабатывать период ожидания в приложениях, основанных на диалоге, которые не имеют родительского окна? К счастью, это довольно просто. Разработчики MFC предусмотрели возможность перехвата WM_KICKIDLE. RunModalLoop посылает это частное сообщение MFC (вы не найдете его описания в стандартной документации по Win32 API) все время, пока в очереди диалога нет сообщений, так же как CWinThread::Run вызывает OnIdle. RunModalLoop также поддерживает счетчик и увеличивает его для вас. В результате, WM_KICKIDLE является диалоговым эквивалентом OnIdle. Историческая справка: в ранних версиях MFC была реализована подмена модальных диалогов немодальными в комплексе с WM_KICKIDLE в страницах свойств (property sheets). Видимо, эта схема настоько понравилась, что в дальнейшем все немодальные диалоги стали маскироваться под модальные.

Маленькое замечание: у вас может появиться искушение вызвать функцию OnIdle основного приложения. Вот так, например:

LRESULT CMyDlg::OnKickIdle(WPARAM, LPARAM lCount)
{
  return AfxGetApp()->OnIdle(lCount);
}

Разработчики MFC утверждают, что это опасно, в связи с проблемой пресловутой временной карты. Лучше всего реализовывать обработку интервала ожидания внутри OnKickIdle. Если хотите, то можно объединить общие команды обработки в отдельную функцию, которую и вызывать из CApp::OnIdle и CMyDlg::OnKickIdle.

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

- Paul DiLascia (72400.2702@compuserve.com)
Copyright(C) 1995 by Miller Freeman, Inc.
Перевод: Илья Простакишин   (iliya@yes.ru)

Если у вас есть материал (на русском языке) по теме, которая, как вы считаете, будет интересна подписчикам рассылки, не стесняйтесь и присылайте.

/ / / / ВОПРОС-ОТВЕТ / / / / / / / / / / / / / / / / / /

Q| У меня программа с использованием MFC и Doc/View. Я вставил RichEditCtrl во вью. (2-ой версии). Установил шрифт с помощью сообщения SetCharFormat. Внимание, вопрос: почему если я ввожу текст с клавиатуры и использую ReplaceText функцию (не сообщение!) фонты различные? Вроде это сообшение не менялось у второй версии. Заранее спасибо за ответ. - Игорь

|A Думаю, всё дело в режиме IMF_AUTOFONT, который по умолчанию устанавливается для rich edit'а 2-й версии (в 1-й этого режима просто не было). В этом режиме rich edit автоматически переключает язык и фонт, когда пользователь переключает раскладку клавиатуры (в текст rtf вставляется управляющая последовательность "\langXXXX\fX"). Поэтому если язык, установленный в rich edit'е по умолчанию, отличается от выбранного при запуске программы, фонт подменяется как только кто-то начинает набирать текст с клавиатуры, что и приводит к описанному эффекту.

Для решения проблемы следует попробовать отключить режим IMF_AUTOFONT. Выглядит это так (hEdit - дескриптор rich edit'а):

::SendMessage( hEdit, 
               EM_SETLANGOPTIONS, 0, 
               ::SendMessage(hEdit, EM_GETLANGOPTIONS, 0, 0) & ~IMF_AUTOFONT);

- Alexander Shargin

Огромное спасибо Александру за ответ (уже, кстати, третий по счету).

Напоминаю, что в будущем вопросы от Ильи, Александра и всех тех, чьи ответы или материалы были опубликованы, будут рассматриваться вне очереди.


/ / / В ПОИСКАХ ИСТИНЫ / / / / / / / / / / / / / / /

Q| Как просканировать LAN на предмет создания поименного списка машин, чтобы затем можно было изпользовать результат в ListBox'e? Пробовал использовать для этой цели SHBrowseForFolder( ) и связанные ф-ции с установленным флагом CIDL_NETWORK, но открывающееся окно для выбора узла и необходимость "раскрывать плюсики" в локальных группах меня не устраивает. Если можно, в API без MFC. - DevXarT

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

До встречи!

Алекс Jenter   jenter@mail.ru
Красноярск, 2000.

Предыдущие выпуски     Статистика рассылки