Прикрепление и отделение объектов

ОГЛАВЛЕНИЕ

Прикрепление и отделение объектов MFC (библиотека базовых классов Microsoft) к и от объектов Windows.

Введение

MFC предоставляет набор "объектов-оберток", содержащих встроенные объекты Windows. Например, CWnd оборачивает HWND, CFont оборачивает HFONT, CBrush оборачивает HBRUSH, и так далее. Они сведены в таблицу ниже. Есть значимые взаимодействия между MFC и объектами Windows, которые надо понимать.

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

Данная статья объясняет вопросы обращения с интерфейсом MFC/Windows.

Объект MFC

Объект Windows

(вариант)

CWnd

HWND

любое окно

CEdit

HWND

EDIT(редактировать)

CListBox

HWND

LISTBOX(окно списка)

CStatic

HWND

STATIC(статический)

CComboBox

HWND

COMBOBOX(комбинированный список)

CGDIObject

(gdi object)

(любой)

CBitmap

HBITMAP

 

CBrush

HBRUSH

 

CPen

HPEN

 

CFont

HFONT

 

CRegion

HRGN

 

CPalette

HPALETTE

 

 

Создание объекта Windows в два шага

Создание большинства объектов происходит в два шага. Первый шаг – создать объект C++, являющийся "оберткой" вокруг объекта Windows. Второй шаг – создать реальный объект Windows. Некоторые параметризованные конструкторы делают оба этих шага сразу. Например,

CPen pen;

Создает объект MFC CPen, но не связывает HPEN с ним. Но конструктор

CPen pen(PS_SOLID, 0, RGB(255, 0, 0));

создает объект MFC CPen, затем создает нижележащий объект Windows HPEN и прикрепляет этот объект к CPen.
Можно сделать это неявно с помощью метода Create(создать) (который иногда необоснованно переименуется, потому что разработчики MFC не были экспертами C++). Например, для создания пера можно сделать

CPen pen;
pen.CreatePen(PS_SOLID, 0, RGB(255, 0, 0));

(MFC имеет CreatePen и CreatePenIndirect, что глупо, потому что нет метода Create в CPen или в суперклассе CGDIObject).


Классы-обертки и объекты

Есть серьезное последствие прикрепления объекта Windows к объекту MFC. При уничтожении объекта MFC уничтожается прикрепленный объект Windows. Это имеет ряд серьезных последствий. Многие программисты делают следующую распространенную ошибку:

{
    CFont f;
    f.CreateFont(...);
    c_InputData.SetFont(&f);
}

Их удивляет отсутствие явного воздействия на управляющий элемент. Что поразительно, потому что раньше они делали нечто вроде:

{
    CFont f;
    f.CreateStockObject(ANSI_FIXED_FONT);
    c_DisplayData.SetFont(&f);
}

и это прекрасно работало!

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

В первом примере происходит следующее: объект CFont создается в стеке, как ожидается, затем CreateFont с длинным список параметров создает объект HFONT, обозначаемый значением его описателя, и прикрепляет HFONT к CFont. Пока все хорошо. Метод SetFont вызывается для ссылки окна c_InputData, управляющий элемент CEdit (если не знаете, как сделать это, прочитайте статью об избегании GetDlgItem). В итоге он генерирует сообщение для управляющего элемента редактирования, который можно упростить, как показано ниже (читайте код MFC, чтобы узнать реальные детали).

void CWnd::SetFont(CFont * font)
{
    ::SendMessage(m_hWnd, WM_SETFONT, font->m_hObject, TRUE);
}

Заметьте, что управляющему элементу отправляется значение HFONT. Пока все хорошо.

Теперь покидаем блок, в котором была объявлена переменная. Вызывается деструктор CFont::~CFont. Когда вызывается деструктор для обертки, связанный с ним объект Windows уничтожается. Объяснение деструктора может быть упрощено и проиллюстрировано, как в следующем коде (истина несколько сложней: читайте исходники MFC сами):

CFont::~CFont()
{
    if(m_hObject != NULL)
    {
       ::DeleteObject(m_hObject);
    }
}

Когда управляющий элемент редактирования собирается рисовать себя в своем обработчике WM_PAINT, он хочет выбрать связанный с ним шрифт в свой контекст отображения (DC). Вы можете представить код таким, как показано ниже. Это очень упрощенный код, являющийся примерным, а не точным, и в исходниках MFC его нет, потому что он является частью нижележащей реализации Windows.

edit_OnPaint(HWND hWnd)
{
    HDC dc;
    PAINTSTRUCT ps;
    HFONT font;
    dc = BeginPaint(hWnd, &ps);
    font = SendMessage(hWnd, WM_GETFONT, 0, 0);
    SelectObject(dc, font);
    TextOut(dc, ...);
}

Теперь посмотрим, что происходит. CFont был уничтожен, что в свою очередь уничтожило HFONT. Но HFONT уже был передан управляющему элементу EDIT и находится там. Когда SelectObject делается внутри обработчика редактирования, он задает недействительный описатель, следовательно, SelectObject игнорируется. Поэтому оказывается, что изменений нет.

Так почему это работало, когда был выбран ANSI_FIXED_FONT? Готовые объекты имеют специальные свойства, и одним из специальных свойств является то, что DeleteObject игнорируется для готовых объектов. Вообще-то код был неправильным и работал только потому, что готовые объекты вообще не удаляются. (Если вы слышали, что нельзя удалять готовые объекты, то вы начинали как программист Windows 3.0 (Win16) или говорили с кем-то, кто начинал с этого. Данная ошибка была исправлена с выпуском 16-битной Windows 3.1.)

Как обойти это? Продолжайте читать...


Разделение оберток и объектов: Detach()

Часто программисты используют следующее решение:

{
    CFont * f;
    f = new CFont;
    f->CreateFont(...);
    c_EditControl.SetFont(f);
}

Эмпирическое наблюдение показывает, что этот код работает, и работает правильно. Это так. Код работает, но он неаккуратный. Что именно произошло с тем объектом CFont, на который ссылались через CFont *? Ничего не произошло. Есть нестандартный CFont, недоступный и неразрушимый. Он сохраняется бесконечно. Это может быть безобидно, но не является хорошей практикой программирования.

Надо применять следующую очень важную хитрость при использовании MFC в интерфейсе MFC-к-Windows. Используется метод Detach(отделить):

{
    CFont f;
    f.CreateFont(...);
    c_InputData.SetFont(&f);
    f.Detach(); // очень важно!!!
}

Операция Detach отделяет объект Windows от его обертки и возвращает в качестве его значения описатель нижележащего объекта Windows. Он не присваивается ничему, так как не нужен. Но сейчас, когда CFont уничтожен, связанный с ним описатель m_hObject равен NULL, и нижележащий HFONT не уничтожается.

Внутри кода вы нашли бы много экземпляров Detach. Пример – функция захвата битовой карты окна, извлекающая битовую карту для заданного окна и помещающая ее в буфер обмена. Чтобы предотвратить уничтожение битовой карты при выходе из области видимости, она отделяет битовую карту от объекта CBitmap.

Когда Detach недостаточно хорош: CDialog и немодальные диалоговые окна

На разных форумах часто встречаются вопросы типа "Пытался создать немодальное диалоговое окно, и не удалось. Вообще не получил диалоговое окно. Что не так?" с приложением следующего фрагмента кода:

void CMyClass::OnLaunchDialog()
{
     CMyToolbox dlg;
     dlg.Create(dlg.IDD);
}

Сейчас вы должны понять, что произошло. Создается диалоговое окно, но как только происходит выход из области видимости, содержащей переменную, вступает в дело деструктор, и так как это окно, вызывает DestroyWindow для связанного объекта. Конечно, этот прием всегда работает для модального диалога, как, например:

{
    CMyDataEntryScreen dlg;
    dlg.DoModal();
}

потому что после выхода из модального диалога не нужна(я) переменная dlg. (Неясно, зачем надо задавать идентификатор диалогового окна для метода Create, так как он подразумевается в классе!)

Но здесь нельзя использовать Detach, потому что диалоговое окно хочет обрабатывать сообщения и хочет иметь состояние. Это не проверялось, но думается, что если действительно сделать Detach, то вы начнете получать кучу ошибок ASSERT или ошибки доступа. Нужен объект немодального диалогового окна.

В данном случае правильный метод - создать ссылку CDialog, например, добавить следующую строку в класс CWinApp (предполагается, что для всех экземпляров окон нужен лишь один инструментарий):

// в классе CWinApp
CMyToolbox * tools;

// в конструкторе CWinApp:
tools = NULL;

// в активаторе:
void CWinApp::OnOpenToolbox()
{
    if(tools != NULL)
    { /* использовать существующий */
        if(tools->IsIconic())
            tools->ShowWindow(SW_RESTORE;
        tools->SetForegroundWindow();
        return;
    } /* использовать существующий */
    tools = new CMyToolbox;
    tools->Create(CMyToolBox::IDD);
}

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

Почему бы не создать переменную CMyToolbox (не ссылку)? Потому что надо знать, когда удалять объект. Это легче, если вы полностью последовательны и никогда не выделяете объект в стеке или как член класса, а используете только указатели на объект, выделенный в куче. Надо добавить в класс обработчик PostNcDestroy, являющийся виртуальным методом, в виде:

void CMyToolbox::PostNcDestroy()
{
    CDialog::PostNcDestroy();
    // добавить эту строку
    delete this;
}

Это гарантирует, что при закрытии окна экземпляр окна будет удален. Замечание: это не меняет указатель, в данном примере tools, поэтому если явно не установить его в NULL, будут большие проблемы.

Это обрабатывается разными способами. Самый распространенный метод – передать заданное пользователем сообщение обратно родительскому окну, что немодальное диалоговое окно было уничтожено. Это говорит классу CWinApp, что он может обнулить переменную. Заметьте, что CWinApp, хоть и CCmdTarget, не является CWnd, поэтому ему нельзя передать сообщение с помощью PostMessage. Вместо этого надо сделать PostThreadMessage и сделать ON_THREAD_MESSAGE или ON_REGISTERED_THREAD_MESSAGE, чтобы обработать его. Если не знаете, о чем идет речь, прочитайте статью об управлении сообщениями.

void CMyToolbox::PostNcDestroy()
{
    CDialog::PostNcDestroy();
    // добавить эти строки
    delete this;
    AfxGetApp()->PostThreadMessage(UWM_TOOLBOX_CLOSED, 0, 0);
}

Добавьте в определение класса CWinApp:

afx_msg LRESULT OnToolboxClosed(WPARAM, LPARAM);

добавьте в карту сообщений CWinApp:

ON_THREAD_MESSAGE(UWM_TOOLBOX_CLOSED, OnToolboxClosed)

или

ON_REGISTERED_THREAD_MESSAGE(UWM_TOOLBOX_CLOSED, OnToolboxClosed)

и метод реализации

LRESULT CMyApp::OnToolboxClosed(WPARAM, LPARAM)
{
    tools = NULL;
    return 0;
}

Различие между обычными сообщениями и зарегистрированными сообщениями объясняется в статье об управлении сообщениями. С этим связан ряд рисков, потому что если приложение окажется в цикле обработки сообщений, отличном от главного конвейера сообщений (например, имеет модальное диалоговое окно или активный MessageBox), то PostThreadMessage не будет виден, и придется обрабатывать PostMessage в классе MainFrame.


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

Во времена 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();

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


Прикрепление описателей к оберткам: Attach()

Есть операция, соответствующая Detach. Это Attach. Операция Attach принимает аргумент HANDLE подходящего типа и прикрепляет этот описатель к существующему объекту MFC. К объекту MFC не должен быть уже прикреплен описатель.

При покупке сторонней DLL, сообщающей, что одна из ее функций возвращает HFONT, или HWND, или HPEN, или любой иной описатель, можно прикрепить этот объект к соответствующему уже существующему объекту MFC с помощью Attach. Допустим, есть DLL, имеющая операцию getCoolPen для операций, которые она хочет сделать. Она возвращает HPEN, который можно хранить. Но, может быть, удобно хранить его как объект MFC. Один способ сделать это - объявить, к примеру, в производном от CView классе переменную-член (вероятно, защищенную переменную-член),

CPen myCoolPen;
Затем можно сделать нечто вроде:
void CMyView::OnInitialUpdate()
{
    // ...
    myCoolPen.Attach(getCoolPen());
    // ...
}

Заметьте, что это требует понимания последствий вызова getCoolPen. Если автор DLL как следует документировал свой продукт, она четко заявит, что надо удалить HPEN при завершении работы с ним, или что нельзя удалять HPEN, потому что он общий. Часто такую полезную информацию можно узнать лишь путем тщательного изучения кода. Но допустим, известно, что надо делать.

Если надо удалить HPEN, когда он больше не нужен, не надо делать ничего особенного. При уничтожении представления вызываются деструкторы для всех его членов, а значит, вызывается деструктор CPen, удаляя нижележащий HPEN.

Если нельзя удалять HPEN, так как он общий, надо добавить в деструктор для класса, производного от CView, следующую строку:

CMyView::~CMyView()
{
    // ...
    myCoolPen.Detach();
    // ...
}

Создание объектов: FromHandle

Все классы-обертки поддерживают одну дополнительную операцию - метод FromHandle. Это статический метод класса-обертки, принимающий в качестве входного аргумента описатель нижележащего объекта Windows и возвращающий в качестве результата временный объект-обертку. Постоянный объект – тот, который не будет убран сборщиком мусора при простое.

Таким образом, при выполнении GetFont возвращается ссылка на временный объект. Этот указатель нельзя хранить, потому что в итоге занимаемое им место будет очищено. Следующий код в корне неверен:

class CMyClass : public CView {
protected:
    CFont * myFont;
};

// где угодно в реализации класса
myFont = GetFont();
// или
myFont = CFont::FromHandle(...);

Попытка использовать переменную myFont позже приведет к катастрофической ошибке. Возможно, ошибке ASSERT, или ошибке доступа, или неверному поведению, такому как отсутствие видимого изменения шрифта. Причина в том, что объект был создан с помощью GetFont, добавлен в список временных объектов, а затем удален. При удалении временного объекта нижележащий объект Windows не удаляется, так как временный объект считается лишь посредником.

Правильный способ хранения ссылки на нижележащий объект выглядит так:

CFont * f = GetFont();
if(f == NULL)
    myFont = NULL;
else
{ /* прикрепить его */
    myFont = new CFont;
    myFont->Attach(f->m_hObject);
} /* прикрепить его */

Здесь предполагается, что myFont равен NULL или его значение бессмысленно. Если он не равен NULL, наверняка он уже содержит действительную ссылку CFont. Вам надо решить, должны ли вы удалить эту ссылку, и если вы удалите эту ссылку, что должно произойти с нижележащим HFONT. Вы можете сделать это, только если переменная myFont еще не хранит ссылку на временный объект. В примере выше, поскольку каждый раз создается новый CFont, известно, что это не временный объект. Два возможных алгоритма таковы:

if(myFont != NULL)
    delete myFont; // удалить объект и HFONT
или, как вариант
if(myFont != NULL)
{
    myFont->Detach(); // не трогать HFONT
    delete myFont;
    myFont = NULL;
}

Не забудьте установить член myFont в NULL в конструкторе класса!

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

CFont  * f;
f = somewindow.GetFont();
delete f;

Если вы получите это, то почти сразу узнаете, что удалили временный объект; вскоре после возврата в главный цикл обработки сообщений вы получите ошибку подтверждения.

Окна: FromHandle и FromHandlePermanent

Для классов, производных от CWnd, можно получить временный объект CWnd * из FromHandle. Таким образом, если у вас есть класс CMyCoolWindow, и вы сделаете GetFocus, то можете получить либо не получить настоящий указатель на объект CMyCoolWindow *. Это порождает многочисленные ошибки. Например, следующий код:

CWnd * capture = GetCapture();
if(capture != NULL && capture->m_hWnd == m_hWnd)
{ /* Имеется захват */
    // ... делается что-то
} /* Имеется захват */

Если нужен описатель для реального объекта для данного HWND, надо использовать CWnd::FromHandlePermanent. Он вернет описатель из карты постоянного окна. Заметьте, что CWnd::FromHandle может вернуть описатель для постоянного окна, а может и не вернуть. Гарантии нет.

CWnd и потоки

Карта объекта является потоковой локальной. Отсюда следует, что если вы находитесь в потоке и выполняете CWnd::FromHandle, то получаете новый, временный объект окна, не являющийся тем же самым объектом C++, представлявшим ваш класс изначально. Таким образом, следующее всегда вызывает ошибку в потоке:

CMyCoolWindowClass * me = (CMyCoolWindowClass *)CWnd::FromHandle(hWnd);
me->MyCoolVariable = 17; // изменяет какое-то неизвестное место

В действительности вы получите обобщенный указатель CWnd, а если бы вы сделали:

me->IsKindOf(RUNTIME_CLASS(CMyCoolWindowClass))

то получили бы FALSE. Если бы вы сделали:

CMyCoolWindowClass * me = (CMyCoolWindowClass *)CWnd::FromHandlePermanent(hWnd);

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

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

Вывод

В то время как данная статья уделяет основное внимание объекту CFont, описанные в ней приемы применяются ко всем классам MFC, являющимся обертками для объектов Windows - CGDIObject, суперкласс CPen, CBrush, CFont, CRgn, CPalette и другие, где реализованы Attach, Detach и FromHandle. Подклассы, такие как CPen, переопределяют FromHandle, чтобы принимать HPEN и возвращать CPen *, но в действительности они просто вызывают суперкласс для выполнения всей работы и обеспечивают приведение типов, чтобы все работало правильно в среде C++. Кроме того, класс CWnd имеет Attach, Detach и FromHandle. Класс CWnd имеет еще одну операцию FromHandlePermanent, не рассмотренную в настоящей статье.

Все эти операции позволяют свободно перемещаться между областью объектов Windows, где объекты представлены экземплярами описателей, и областью объектов MFC, где объекты представлены экземплярами классов C++. Статья помогает понять отношение между этими двумя представлениями и то, как использовать их безопасно и без утечек.