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

ОГЛАВЛЕНИЕ

Разделение оберток и объектов: 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.