Прикрепление и отделение объектов - Оптимизация и правильность

ОГЛАВЛЕНИЕ

Оптимизация и правильность

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

Это была замечательная оптимизация, необходимая тогда. В современных системах Win32 она не годится. Причина в том, что она нарушает абстракцию. Если кто-то создает управляющий элемент, ожидающий специальный шрифт, он должен создать указанный шрифт для этого управляющего элемента, а не требовать, чтобы программист создавал шрифт для него. Поэтому объект CFont входит в подкласс управляющего элемента, а не является глобальной переменной или переменной класса CDialog или класса CWinApp. Если всем экземплярам управляющего элемента требуется один и тот же шрифт, можно использовать член статического класса для хранения CFont, хотя придется подсчитывать число ссылок на него, чтобы знать, когда удалить его.

Оптимизация, делающая программу неправильной или потенциально неправильной, не есть оптимизация. Берегитесь! Потому что следствием оптимизации для "экономии занимаемой памяти" может стать утечка памяти. Это плохо.

Изменение шрифтов в управляющем элементе

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

void CMyControl::ChangeFont()
{
    CFont f;
    f.CreateFont(...);
    CFont * oldfont = GetFont();
    if(oldfont != NULL)
        oldfont->DeleteObject(); // Будьте внимательны! Смотрите обсуждение ниже!
    SetFont(&f);
    f.Detach();
}

Преимущество данного кода в том, что он не вызывает утечку HFONT при каждом изменении шрифта. В качестве упражнения для читателя оставлено выяснение того, как задаются параметры для CreateFont; обычно в качестве параметров для метода ChangeFont указываются размер, название гарнитуры шрифта и флаг жирности, что охватывает 99% случаев изменения шрифтов в управляющих элементах. Обобщая вышеуказанный пример, вам придется выяснить, каковы ваши нужды.

GetFont возвращает ссылку на объект CFont; если с управляющим элементом не был связан ни один шрифт, то результат равен NULL. Но если результат – не NULL, HFONT удаляется путем вызова DeleteObject. Пользовательский шрифт удаляется (если он был общим, другие пользователи шрифта пострадают, но скоро дойдем до этого). На стандартный шрифт DeleteObject никак не влияет. Затем SetFont устанавливает шрифт, а Detach предотвращает уничтожение HFONT вслед за CFont.

Что насчет полученного CFont *? Не происходит ли утечка там? Нет, потому что CFont *, созданный GetFont, является временным объектом MFC, то есть он добавляется в список объектов, удаляемых сборщиком мусора. В следующее время простоя стандартный метод OnIdle обходит и убирает все временные объекты, удаляя их. По сути, если вы скажете "delete oldfont", то в итоге получите ошибку ASSERT от сборщика временных объектов, говорящую, что вы сделали что-то не так с его данными.

Пояснение вышеприведенного кода

В вышеприведенном коде есть ошибка. Она не очевидна, но читатель статьи обнаружил и указал на нее. Объясним точнее. Вы должны выполнять DeleteObject только для созданных вами шрифтов. Была такая ситуация: читатель точно проделал то, что было описано выше, и пожаловался, что хотя его кнопки «Как» имели желаемый шрифт, все остальные кнопки «Сейчас» имели неверный шрифт. Это была ошибка. В диалоговом окне шрифт создается для его управляющих элементов, и SetFont выполняется для каждого управляющего элемента при его создании.

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

Что если вы меняете шрифт позже и не знаете, создали вы шрифт в прошлый раз, или нет? Храните булеву переменную, сообщающую, изменили ли вы шрифт. Типичный случай – когда вы используете CListBox как записывающий управляющий элемент и хотите, что пользователь мог изменить шрифт. Это сложно. Был выполнен GetFont для существующего управляющего элемента. Затем был создан новый шрифт, идентичный этому, и созданный шрифт был установлен в управляющем элементе. Делающий это код показан ниже:

// Создается новый шрифт, чтобы его можно было изменить позже
CFont * f = c_MyLogListBox.GetFont();
CFont newfont;
LOGFONT lf;
if(f != NULL)
{ /* Устанавливается копия шрифта */
    f->GetObject(sizeof(LOGFONT), &lf);
    newfont.CreateFontIndirect(&lf);
} /* Устанавливается копия шрифта */
else
{ /* Используется спецификация стандартного шрифта */
    newfont.CreateStockObject(ANSI_VAR_FONT);
} /* Используется спецификация стандартного шрифта */
c_MyLogListBox.SetFont(newfont);
newfont.Detach();

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