вторник, 30 марта 2010 г.

Как мне перекрыть панель задач окном во весь экран?

Это перевод How do I cover the taskbar with a fullscreen window? Автор: Реймонд Чен.

Почему-то, люди стараются идти сложным путём. Если вы хотите создать окно во весь экран, которое закроет панель задач - то просто создайте такое окно и панель задач сама уйдёт с вашего пути. Не надо её искать и скрывать; просто позвольте ей делать свою работу.

четверг, 11 марта 2010 г.

Где-то там есть куча разогнанных машин...

Это перевод There's an awful lot of overclocking out there. Автор: Реймонд Чен.

Часть нашей команды просматривает отчёты о проблемах Windows, которые люди отправляют нам, нажимая кнопку "Отправить отчёт" в диалоге вылета программы. И оказывается, что огромное количество отчётов вообще не имеют смысла.

среда, 10 марта 2010 г.

Отладка телепатией: почему ваша система с дорогим четырёхядерником игнорирует три ядра

Это перевод Psychic debugging: Why your expensive four-processor machine is ignoring three of its processors. Автор: Реймонд Чен.

В одном из наших внутренних списков рассылки кто-то спросил, почему их дорогая четырёх-процессорная машина, кажется, использует только один из своих процессоров. Во вкладке "Производительность" Диспетчера Задач график показывал, что первый процессор выполняет всю работу, а остальные три практически всегда стоят. Используя способность Диспетчера Задач по установке привязки к процессорам для каждого процесса, конечно же, удалось заставить машину работать значительно быстрее. Но что же случилось, что привело к такой путанице привязок процессоров?

вторник, 9 марта 2010 г.

Ваш обработчик исключения тоже может возбудить исключение

Это перевод Your exception handler can encounter an exception. Автор: Реймонд Чен.

Рассмотрите такой код:
procedure ObliterateDocument;
begin
try
try
document.DestroyAll;
finally
document.Close;
document.DestroyExtensions;
document.DestroyPlugins;
end;
finally
document.Destroy;
end;
end;
А чуть позже у вас начинает ругаться Assert в document.Destroy, говоря, что в документе всё ещё есть активные плагины. Но у нас же есть вызов document.DestroyPlugins, и он находится в блоке finally, а весь смысл блока finally в том, что вы никак не можете избежать его выполнения.

Так почему же document.DestroyPlugins не выполняется?

Потому что ваш обработчик исключений тоже может возбудить исключение.

Обработчик исключения не активен во время обработки своего собственного блока обработки исключения (как finally, так и except). В результате, если document.Close возбудит исключение, то поиск обработчика для выполнения начнётся с блока вне текущего блока finally.

(То, что обработчик исключения не активен во время прогона своего собственного блока обработки, должно быть очевидно. Потому что в противном случае программа вошла бы в бесконечный цикл, возникни в блоке обработки исключение. Кроме того, вы не смогли бы повторно возбудить исключение, т.к. raise был бы пойман вами же!)

В нашем случае, исключение ловится внешним кодом, что приводит к пропуску остатка finally-блока. Прочие внешние finally блоки выполняются - потому что они содержат в себе finally-блок, который умер.

(Эта ошибка также существует в предлагаемой анонимом альтернативе к коду на кодах ошибок).

понедельник, 8 марта 2010 г.

Окно - это не дёшево

Это перевод Windows are not cheap objects. Автор: Реймонд Чен.

Хотя Windows основана на, хм, окнах (windows), само окно не является таким уж дешёвым объектом. Даже более того: жёсткие ограничения памяти в системах 1985-го года диктуют различные решения в дизайне системы.

воскресенье, 7 марта 2010 г.

Подводные камни в восстановлении предыдущего положения окна

Это перевод A subtlety in restoring previous window position. Автор: Реймонд Чен.

Частой возможностью многих приложений является сохранение своего положения на экране и размещение себя на том же самом месте в следующий запуск. Будучи реализованной наивно, программа просто восстанавливает себя по сохранённым координатам без каких-либо условий.

Но с такой реализацией вы встретитесь с проблемами в юзабилити. Если пользователь запустит две копии вашей программы, то их окна разместятся в точности друг под другом на экране. Если только пользователь не следит внимательно за панелью задач, ему покажется, что второй запуск не произошёл. Теперь вещи становятся интересными.

В зависимости от того, что делает программа, вторая копия может встретить отказ в доступе (sharing violation), или она может просто открыть вторую копию документа для правки, или же будет играть две копии песни, приводя к странному эху из-за несинхронизированности двух запусков. Ещё более забавно, что когда пользователь щёлкнет по кнопке "Стоп", музыка продолжит играть! Почему? Потому что только вторая копия остановила воспроизведение. Первая копия продолжает играть.

Я знаю одного пользователя, который не так уж редко запускает четыре копии мультимедийного проигрывателя, что приводит к ужасной какофонии звуков с последующим смятением пользователя, когда он пытается исправить проблему, что обычно выражается в постоянном нажатии на кнопку "Стоп" самого верхнего окна. Это останавливает последний экземпляр, но остальные продолжают играть...

Если вторая копия открывает документ, то пользователь может переключиться с редактора, а потом переключиться к первой копии - и подумать, что все изменения были потеряны. Или пользователь может не заметить этого и в результате сделать конфликтующие изменения в первой копию. Когда эти документы будут сохранены - произойдёт множество плохих вещей.

Мораль истории: если ваша программа сохраняет и восстанавливает положение на экране, вам лучше бы проверить, не расположено ли уже окно от предыдущей копии по этим же координатам. Если так - то переместите своё окно куда-нибудь ещё, так что оно не займёт ровно то же самое место.

суббота, 6 марта 2010 г.

Выигрыш в производительности за счёт других компонентов

Это перевод Performance gains at the cost of other components. Автор: Реймонд Чен.

В группе операционных систем, мы должны принять целостный взгляд на производительность. Цель состоит в том, чтобы заставить всю систему работать быстрее, балансируя приложения между друг другом для общего блага.

Приложения, с другой стороны, как правило, имеют эгоистичную точку зрения на производительность: "Я сделаю всё возможное, чтобы заставить себя работать быстрее. Влияние на остальную часть системы - не моя забота".

Некоторые приложения заносят себя в папку Автозагрузка, так что они смогут запуститься быстрее. На самом деле, это не делает систему быстрее; просто смещает приоритеты. Откусывая от стоимости запуска приложения и приплетая это время к запуску ОС, время от запуска приложения пользователем до готового приложения действительно уменьшается. Но общее время остаётся неизменным.

Например, рассмотрим следующую диаграмму. "*" отмечает момент включения машины, "+" означает время, когда пользователь щёлкнул по ярлычку программы, а "!" указывает на точку, когда приложение полностью готово.
* Запуск ОС + Запуск приложения !
Разработчик приложения говорит: "дьявол, вы только посмотрите на эту огромную розовую полоску! Чтобы нам сделать, чтобы уменьшить её? О, я знаю: давайте разобьём запуск нашей программы на две части....
* Запуск ОС + Запуск приложения 1 Запуск приложения 2 !
... и поместим часть её в Автозагрузку"
* Запуск ОС Запуск приложения 1 + Запуск приложения 2 !
"Вау, вы только посмотрите на розовую часть теперь (что отображает время для полного запуска нашего приложения после команды пользователя) - насколько же она стала короче!"

Потом команда разработчиков указывает это, более короткое, значение в своих отчётах производительности и, может быть, они устраивают ужин, чтобы это отпраздновать.

Конечно же, если вы взглянете на картину целиком, от звёздочки до восклицательного знака, то увидите, что ничего не изменилось. Время полной готовности вашего приложения с холодного старта не изменилось. Всё, что это "улучшение производительности" сделало - rob Peter to pay Paul. Время проводимое в "Запуске приложения 1" теперь снимается с запуска всей системы, а не с приложения. Вы переставили местами числа, но пользователь ничего не выиграл.

Фактически, пользователь даже что-то потерял. В диаграммах выше мы предполагали, что пользователь хочет запустить вашу программу! Но если он не хочет этого делать, а просто хочет проверить свою почту, то он всё равно будет платить за "Запуск приложения 1", даже хотя он не собирается запускать вас.

Другой пример приложения с эгоистичным взглядом на производительность пришёл от компании, которая разрабатывала обработчик оверлея иконок. Оболочка рассматривает оверлеи иконок как низко-приоритетные вещи, поскольку гораздо более важным является вообще показать значок на экране, чтобы пользователь скорее начал делать то, что он там собирался делать. Декорации могут подождать. Так вот, эта компания хотела знать, как они могут улучшить свою производительность, чтобы показать свои оверлеи на экране ещё раньше, чем даже появятся сами значки, показывая этим своё феноменально эгоистичное представление "производительности".

Производительность - это сделать так, чтобы пользователь быстрее решал свои задачи. Если задача не включает в себя запуск/использование вашей программы, то ваше "улучшение производительности", на самом деле, является деградацией производительности. Нет, я уверен, что ваша программа прекрасна и полезна, но будет несколько самоуверенно полагать, что каждый пользователь, который установит её, будет считать её запуск самым важным делом в своей жизни.

При.пер.: упаковщики исполняемых файлов - ещё один пример не самой дальновидной "оптимизации".

пятница, 5 марта 2010 г.

В чём разница между PostMessage и SendNotifyMessage?

Это перевод C++ Q&A: Sending Messages in Windows. Автор: Paul DiLascia.

Есть несколько тонких различий в способах, которыми вы можете отправлять сообщения в Windows®, но основным различием между PostMessage и SendNotifyMessage является то, что функции семейства SendMessage отправляют (send) сообщение в другое окно немедленно, с помощью прямого вызова их оконной процедуры и ожидания ответа, в то время как PostMessage заносит сообщение в очередь в виде записи TMessage и возвращает управление немедленно, без ожидания.

четверг, 4 марта 2010 г.

Почему SystemParametersInfo повисает, когда я передаю флаг SPIF_SENDCHANGE?

Это перевод Why does SystemParametersInfo hang when I pass the SPIF_SENDCHANGE flag? Автор: Реймонд Чен.

Если вы передадите флаг SPIF_SENDCHANGE в функцию SystemParametersInfo, она сделает широковещательную отправку (broadcast) сообщения WM_SETTINGCHANGE с wParam равному коду, что вы указали.

Например, если вы вызовете:
SystemParametersInfo(SPI_SETDOUBLECLICKTIME, 500, 0, SPIF_UPDATEINIFILE or SPIF_SENDCHANGE);
то система разошлёт сообщение:
SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, SPI_SETDOUBLECLICKTIME, 0);
И если у нас есть где-то окно, которое не отвечает на сообщения (зависло), то эта рассылка тоже подвиснет до того момента, как это окно не проснётся и не обработает это сообщение, либо же пока зависшее приложение не убьют.

Если вы не хотели бы стать жертвой зависшего окна, то у вас есть несколько вариантов, но они, однако, могут повлиять на ожидания вашей программы.

Во-первых, вы можете вызвать SystemParametersInfo в фоновом рабочем потоке. Тогда заблокированным будет ваш рабочий поток вместо вашего UI-потока.

С этим способом, фоновый поток может уведомить основной поток, когда рассылка завершена, так что ваша программа будет знать, что к этому моменту все окна в системе получили уведомление и применили новые настройки.

Вы также можете вызвать SystemParametersInfo без указания флага SPIF_SENDCHANGE, а затем вручную разослать уведомление через SendMessageTimeout:
var
dwResult: DWORD;
...
SendMessageTimeout(HWND_BROADCAST, WM_SETTINGCHANGE, SPI_SETDOUBLECLICKTIME, 0, SMTO_ABORTIFHUNG or SMTO_NOTIMEOUTIFNOTHUNG, 5000, dwResult);
Это означает, что зависшие окна не получат уведомление о смене параметров. Это приемлемо, если вы изменяете маловажную настройку, так что пропуск уведомления программой не является такой уж большой проблемой. Другими словами, когда зависшее приложение наконец-то просыпается, оно не знает, что настройки были изменены, потому что оно проспало уведомление.

Вы можете скомбинировать эти два способа: использовать фоновый поток и отправлять сообщение вручную с таймаутом.

Вероятно, наилучшим вариантом будет использование функции SendNotifyMessage. Как мы узнали ранее, функция SendNotifyMessage похожа на SendMessage, но только она не ждёт ответа. Это позволит вашей программе продолжить работу, без влияния подвисших программ.
SendNotifyMessage(HWND_BROADCAST, WM_SETTINGCHANGE, SPI_SETDOUBLECLICKTIME, 0);
Однако недостатком будет являться то, что вы теперь не знаете, когда все окна наконец-то получат и обработают уведомление. Всё что вы знаете - что когда-нибудь они это узнают. Обычно вас не волнует этот аспект рассылки, так что отсутствие этой информации при таком способе не является критичным.

среда, 3 марта 2010 г.

Использование SystemParametersInfo для получения настроек интерфейса пользователя

Это перевод Using SystemParametersInfo to access user interface settings. Автор: Реймонд Чен.

Функция SystemParametersInfo даёт вам доступ к различным настройкам пользовательского интерфейса, и это единственный поддерживаемый официально способ получить эти настройки.

Я не буду рассматривать каждую опцию; идите и прочитайте сами. Вот несколько примеров:
  • SPI_GETICONTITLELOGFONT позволяет вам узнать шрифт, который используется для подписей к иконкам; SPI_SETICONTITLELOGFONT позволяет вам установить его.
  • SPI_GETNONCLIENTMETRICS позволяет вам запросить шрифты, которые используются в заголовках окон, меню, строках статуса и сообщениях; SPI_SETNONCLIENTMETRICS позволяет вам сменить их.
А вот некоторые настройки из Панели управления:
  • SPI_SETKEYBOARDDELAY и SPI_SETKEYBOARDSPEED позволяют вам сменить настройки авто-повтора клавиш клавиатуры.
  • SPI_SETDOUBLECLICKTIME позволяет вам сменить скорость двойного щелчка.
  • SPI_SETMENUFADE позволяет вам включить или отключить анимацию выпадения меню.
  • Есть целая серия настроек SPI_SETxxxANIMATION, которые позволяют вам контролировать анимацию различных элементов экрана.
Заметьте, что когда вы используете команды SPI_SET*, вы также должны выбрать, будут ли эти изменения временными (сбросятся после выхода пользователя из системы) или же постоянными. Исторически названный флаг SPIF_UPDATEINIFILE приводит к сохранению настроек в пользовательский профиль; если же вы не указываете его, то изменения в профиль не сохраняются и теряются при logoff-е. Вы должны также устанавливать флаг SPIF_SENDCHANGE, чтобы программы, которые хотят обновить себя при изменениях настроек, смогли бы это сделать.

Тот факт, что у нас существуют как временные, так и постоянные установки, указывает на опасности прямого чтения настроек из реестра. Если текущие настройки являются временными, то они не сохраняются в профиль пользователя (реестр). Функция SystemParametersInfo получает текущие установки, даже если они временные. Например, если вы хотите узнать, анимированы ли меню, а пользователь временно отключил анимацию, то чтение реестра скажет вам, что они являются анимированными, когда в действительности они не анимированы.

Далее, запись настроек в реестр не активирует их до следующей загрузки профиля, потому что это именно то время, когда настройки читаются из реестра. Чтобы применить настройки сразу - вы должны использовать SystemParametersInfo.

Это по-прежнему удивляет меня: почему люди используют недокументированные ключи реестра, когда есть отличный документированный способ для этого. Особенно, когда документированный способ действительно работает, а недокументированный способ - не всегда.

Я помню одно приложение, которое шло напрямую в реестр за недокументированным ключом реестра (я думаю, для получения шрифта заголовка иконки). К несчастью для этого приложения, формат ключа реестра был изменён между Windows 95 и Windows 2000, так что приложение стало вылетать (оно ожидало формат Windows 95). Если бы оно использовало документированный способ получения шрифта, то оно бы продолжило работать, и ему было бы плевать на внутренние изменения реестра. Другими словами, эта программа обошла предпочтительный способ делать вещи, только для того, чтобы загнать саму себя в ловушку.

вторник, 2 марта 2010 г.

Не спускайте глаз с кодовой страницы

Это перевод Keep your eye on the code page. Автор: Реймонд Чен.

Вспомните, что существует, как правило, две активно используемые 8-разрядные кодовые страницы: так называемая кодовая страница "ANSI" и так называемая кодовая страница "OEM". GUI программы обычно используют кодовую страницу ANSI для 8-битных файлов (хотя UTF-8 и становится все более популярным в последнее время), в то время как консольные программы обычно используют кодовую страницу OEM.

понедельник, 1 марта 2010 г.

MsgWaitForMultipleObjects и состояние очереди

Это перевод MsgWaitForMultipleObjects and the queue state. Автор: Реймонд Чен.

Одной опасностью функции MsgWaitForMultipleObjects является её вызов, когда в очереди есть сообщения для обработки - потому что MsgWaitForMultipleObjects возвращает управление, только когда у нас есть новые сообщения.

Другими словами, рассмотрим такой сценарий:
  • PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) возвращает True, говоря, что у нас есть сообщение.
  • Вместо обработки сообщения, вы игнорируете его и вызываете MsgWaitForMultipleObjects.
Это ожидание не завершится тут же, хотя у нас есть сообщение в очереди. Это потому что вызов PeekMessage сообщил вам, что сообщение готово, и вы сами проигнорировали его. Функция MsgWaitForMultipleObjects сообщает вам только о новых сообщениях; все сообщения, о которых вы уже в курсе - не считаются.

Частый вариант этих действий выглядит так:
  • MsgWaitForMultipleObjects возвращает управление, сообщая, что у нас есть сообщение.
  • Вы вызываете PeekMessage(Msg, 0, 0, 0, PM_REMOVE) и обрабатываете это сообщение.
  • Вы вызываете MsgWaitForMultipleObject, чтобы ждать следующее сообщение.
Если произойдёт так, что у вас в очереди было два сообщения, то второй вызов MsgWaitForMultipleObjects не вернёт управление немедленно, потому что у нас нет новых сообщений; однако, у вас есть старое сообщение, которое вы сами проигнорировали.

Когда MsgWaitForMultipleObjects говорит вам, что у вас есть сообщение в очереди, вы должны обрабатывать все сообщения, пока PeekMessage не вернёт False, говоря, что сообщений больше нет.

Заметьте, однако, что такая последовательность не является проблемой:
  • PeekMessage(Msg, 0, 0, 0, PM_NOREMOVE) возвращает False, указывая, что сообщений больше нет.
  • Сообщение post-тся в вашу очередь.
  • Вы вызываете MsgWaitForMultipleObjects и включаете флаг QS_ALLPOSTMESSAGE.
Это ожидание вернёт управление немедленно, потому что входящее post-сообщение устанавливает признак "в очереди есть новое сообщение, о котором никто не знает", что согласуется с флагом QS_ALLPOSTMESSAGE и приводит к тому, что MsgWaitForMultipleObjects возвращает управление немедленно.

Функция MsgWaitForMultipleObjectsEx позволяет вам указать флаг MWMO_INPUTAVAILABLE, чтобы указать, что она должна проверять ранее игнорированный ввод.

Вооружённые этим знанием, объясните, почему наблюдаемое поведение нижеследующего кода таково: "иногда моя программа застревает и сообщает на одну запись меньше, чем должна. Мне приходится подёргать мышью, чтобы обновились значения. Через некоторое время она отстаёт на два значения, а потом - на три..."

// Предположим, что у нас есть рабочий поток, который обрабатывает записи и
// отправляет сообщение WM_NEWRECORD для каждой новой записи

function WaitForNRecords(h: THandle; cRecordsExpected: Integer): Boolean;
var
Msg: TMessage;
cRecords: Integer;
begin
cRecords := 0;
while True do
begin
case MsgWaitForMultipleObjects(1, h, False, INFINITE, QS_ALLINPUT) of
WAIT_OBJECT_0:
DoSomethingWith(h); // сработало событие
WAIT_OBJECT_1:
begin
// У нас есть сообщение - обрабатываем его
if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
if SendMessage(hwndNotify, WM_GETRECORDCOUNT, 0, 0) >= cRecordsExpected then
Exit(True); // у нас достаточно записей
end;
else
Exit(False); // неожиданная ошибка
end;
end;
end;