На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
! Правила раздела FAQ в группе разделов С++.
1. Раздел FAQ предназначен для публикации готовых статей.
2. Здесь нельзя задавать вопросы, для этого существуют соответствующие разделы:
Чистый С++
Visual C++ / MFC / WTL / WinApi
Borland C++ Builder
COM / DCOM / ActiveX / ATL
Сопутствующие вопросы
3. Внимание, все темы и сообщения в разделе премодерируются. Любое сообщение или тема будут видны остальным участникам только после одобрения модератора.
Модераторы: B.V., Qraizer
  
> Рисование в DC , Как избежать ошибок
    Рисование в DC. Как избежать ошибок.

    Предисловие

    Данная статья не может претендовать на полноту раскрытия принципов рисования в среде Microsoft Windows, она создана в помощь тем, кто делает свои первые шаги в этой области, с целью обратить внимание на основные моменты и предотвратить возможные (наиболее часто встречающиеся) ошибки. Примеры кода будут представлены в двух вариантах: с использованием WinAPI и MFC.


    Рассматриваемые вопросы:

    1. Получение DC (Контекста устройства)
    2. Ресурсы GDI. Выбор ресурсов в DC.



    1. Получение DC (Контекста устройства)

    Рисование в Windows осуществляется в контексте устройства (DC). Существуют 4 типа DC: Display, Printer, Memory (Compatible DC) и Information. Первые 3 используются для рисования, Information DC – для получения информации об устройстве. В данной статье будет рассматриваться Display DC (обращение с Memory DC будет рассмотрено в следующей статье, которая сейчас в подготовке), а обращение с Printer DC – большая отдельная тема, которую может быть кто-нибудь из участников осветит здесь на форуме. :)

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

    WinAPI
    Цитата
    // Получить DC клиентской области окна по его хэндлу
    HDC GetDC(HWND hWnd);

    // Получить DC всего окна (включая его заголовок, меню, скроллбары и т.д.) по его хэндлу
    HDC GetWindowDC (HWND hWnd);

    // Освободить DC, ранее полученный по GetDC или GetWindowDC
    int ReleaseDC (HWND hWnd, HDC hDC);


    В MFC классом «оберткой» для DC служит класс CDC. Для получения объекта CDC какого либо окна, в классе CWnd существуют следующие методы:

    MFC
    Цитата
    // Получить DC клиентской области окна
    CDC *CWnd:: GetDC ();

    // Получить DC всего окна (включая его заголовок, меню, скроллбары и т.д.)
    CDC *CWnd:: GetWindowDC ();

    // Освободить DC, ранее полученный по GetDC или GetWindowDC
    int  CWnd:: ReleaseDC (HWND hWnd, HDC hDC);


    Из приведенных методов получения DC наиболее часто используется GetDC, т.к. обычно рисование происходит в клиентской части.


    ВАЖНО: Любой DC, полученный по GetDC или GetWindowDC должен быть потом освобожден через вызов ReleaseDC. В противном случае происходят утечки ресурсов GDI, что при долгом времени работы програмы неизбежно приведет к глюкам при рисовании. Также, надо отметить, что ReleaseDC должно вызываться только для DC, полученных по GetDC или GetWindowDC.



    Схема получения – освобождения DC

    WinAPI

    ExpandedWrap disabled
      // hWnd - хэндл окна, DC которого нам необходим
       
      // Получаем DC
      HDC hDC = ::GetDC (hWnd);
       
      // Здесь рисуем, используя полученный DC
      // …
       
      // Освобождаем DC
      ::ReleaseDC (hWnd, hDC);


    MFC
    ExpandedWrap disabled
      // m_Button1 - член класса, объект типа CButton
      CDC *pDC = m_Button1.GetDC();
       
      // Здесь рисуем, используя полученный DC
      // …
       
      // Освобождаем DC
      m_Button1.ReleaseDC(pDC);


    2. Ресурсы GDI. Выбор ресурсов в DC.

    Рисование в DC происходит посредством ресурсов GDI, таких как Pen, Brush, Font и Bitmap. Остальные типы ресурсов являются специфическими и не будут рассмотрены в данной статье.

    Под каждый из этих типов в Windows заведен соответствующий тип данных: HPEN, HBRUSH, HFONT и HBITMAP. В WinAPI для универсализации каждый из приведенных типов приводится к типу HGDIOBJ как в качестве параметра функций, так и в качестве возвращаемого значения.

    В MFC каждый тип представлен соответствующим классом: CPen, CBrush, CFont и CBitmap. Базовым для всех этих классов является класс CGdiObject.

    Большинство функций рисования в Windows использует выбранные в DC объекты, так например LineTo использует выбранный в DC Pen, ExtFloodFill – Brush, а TextOut – Font. Соответственно, чтобы рисовать нужными нам цветами и стилями, требуется выбрать в нужном нам DC наши собственные объекты, которые естественно, перед этим необходимо создать.

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

    Несмотря на все разнообразие способов создания объектов GDI, существует лишь один метод их удаления. Все объекты, созданные по методам CreateXXXX (CreatePen, CreateBrushIndirect, …) должны быть удалены методом DeleteObject. Для объектов, полученных по GetStockObject или CGdiObject::CreateStockObject вызывать DeleteObject необязательно, хотя ошибкой это не является.

    WinAPI
    Цитата
    BOOL DeleteObject (HGDIOBJ hgdiObj);


    MFC
    Цитата
    BOOL CGdiObject::DeleteObject();


    Замечание: для объектов MFC CGdiObject::DeleteObject вызовется автоматически в деструкторе. То есть этот метод можно не вызывать для объектов, создающихся однократно.

    Для выбора объекта в DC используются следующий методы:

    WinAPI
    Цитата
    //Выбрать объект в DC
    HGDIOBJ SelectObject (HDC hdc, HGDIOBJ hgdiobj);


    MFC
    Цитата
    //Выбрать Pen в DC
    CPen * CDC::SelectObject (CPen *pPen);

    //Выбрать Brush в DC
    CBrush * CDC::SelectObject (CBrush *pBrush);

    //Выбрать Font в DC
    CFont* CDC::SelectObject (CFont* pFont);

    //Выбрать Bitmap в DC
    CBitmap* CDC::SelectObject (CBitmap* pBitmap);


    Все методы SelectObject возвращают объект GDI, который был выбран в DC перед этим.


    ВАЖНО
    1) Основное правило здесь – «забрал - отдай». То есть, последовательность должна быть такая:
    • Выбрал свой объект в DC;
    • Запомнил объект, который вернул метод SelectObject;
    • Использовал свой объект при рисовании;
    • Выбрал в DC объект, который запомнил (тем самым освободил свой объект из DC).
    2) Вызов DeleteObject для объекта, который в данный момент выбран в DC к успеху не приведет. Перед удалением объект обязательно должен быть освобожден из DC.



    Примеры:

    WinAPI
    ExpandedWrap disabled
      // Получаем DC для рисования
      HDC hDC = GetDC (hWnd);
       
      // Получаем размер клиентской области окна
      RECT rc;
      GetClientRect(hWnd, &rc);
       
      // Создаем Pen
      HPEN hPen = CreatePen (PS_SOLID, 1, RGB(255, 0, 0));
       
      // Выбираем свой Pen в DC, запоминаем старый Pen
      HPEN hOldPen = (HPEN)SelectObject (hDC, hPen);
       
      // Перемещаем точку рисования в левый верхний угол окна
      MoveToEx(hDC, rc.left, rc.top, NULL);
      // Рисуем линию в правый нижний угол
      LineTo(hDC, rc.right, rc.bottom);
       
      // Выбираем старый Pen в DC (освобождаем свой Pen из DC)
      SelectObject(hDC, hOldPen);
       
      // Удаляем Pen
      DeleteObject (hPen);
       
      // Освобождаем DC
      ReleaseDC (hWnd, hDC);


    MFC
    ExpandedWrap disabled
      // Получаем DC для рисования
      CDC *pDC = m_Buton1.GetDC();
       
      // Получаем размер клиентской области окна
      RECT rc;
      m_Button1.GetClientRect(&rc);
       
      // Создаем Pen
      CPen Pen;
      Pen.CreatePen(PS_SOLID, 1, RGB(255, 0, 0));
       
      // Выбираем свой Pen в DC, запоминаем старый Pen
      CPen *pOldPen = pDC->SelectObject (&Pen);
       
      // Перемещаем точку рисования в левый верхний угол окна
      pDC->MoveTo(rc.left, rc.top);
      // Рисуем линию в правый нижний угол
      pDC->LineTo(rc.right, rc.bottom);
       
      // Выбираем старый Pen в DC (освобождаем свой Pen из DC)
      pDC->SelectObject(pOldPen);
       
      // Удалять Pen в данном случае необязательно, но это не повредит
      Pen.DeleteObject ();
       
      // Освобождаем DC
      m_Button1.ReleaseDC (pDC);



    ВАЖНО
    Что не надо делать в WinAPI:
    • Удалять (DeleteObject) объект, полученный по SelectObject.
    Что не надо делать в MFC:
    • Удалять DC, полученный через GetDC с помощью delete (delete pDC;);
    • Удалять DC, полученный через GetDC с помощью CDC::DeleteDC (pDC->DeleteDC(););
    • Удалять объект, полученный по SelectObject (delete pOldPen; или pOldPen->DeleteObject(););
    Все это является примерами часто встречающихся ошибок.

    Отдельное внимание хочется уделить методу CGdiObject::Detach(), очень часто ошибочно используемого для удаления объекта GDI (вместо CGdiObject::DeleteObject();)

    Цитата
    HGDIOBJ CGdiObject::Detach();

    Этот метод используется для «отсоединения» от объекта класса CGdiObject(CPen, CBrush и т.д.) хэндла объекта GDI. При этом сам хэндл(а значит, и объект GDI) не удаляется.

    Пример использования метода Detach:
    ExpandedWrap disabled
      // Функция создаст и вернет хэндл GDI объекта Font
      // После использования фонта, созданного этой функцией,
      // требуется его удалить с использованием [b]DeleteObject[/b]
      HFONT CreateMyFont ()
      {
        // Создаем фонт
        CFont Font;
        Font.CreateFont(
         12,                        // nHeight
         0,                         // nWidth
         0,                         // nEscapement
         0,                         // nOrientation
         FW_NORMAL,                 // nWeight
         FALSE,                     // bItalic
         FALSE,                     // bUnderline
         0,                         // cStrikeOut
         ANSI_CHARSET,              // nCharSet
         OUT_DEFAULT_PRECIS,        // nOutPrecision
         CLIP_DEFAULT_PRECIS,       // nClipPrecision
         DEFAULT_QUALITY,           // nQuality
         DEFAULT_PITCH | FF_SWISS,  // nPitchAndFamily
         "Arial"));                 // lpszFacename
       
        // Возвращаем хэндл HFONT созданного шрифта
        // По выходу из функции объект Font удалится,
        // в то время как хэндл созданного фонта уже будет
        // отсоединен от него, и с успехом будет возвращен
        // из функции
        return (HFONT)Font.Detach();
      }




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

    В следующей статье будут описаны методы работы с Memory(Compatible) DC, так же будут предложены на скачивание несколько проектов (под MS Visual C++ 6, 7) для демонстрации материала. Все это я хотел поместить здесь же, но сейчас у меня возникли сови запарки, и когда я допишу раздел про Memory DC, пока еще не знаю.

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

    PS. Спасибо Flex Ferrum’у за любезное предоставление исходника своей статьи.
      Цитата Uncle_Bob @
      Основное правило здесь – «забрал - отдай». То есть, последовательность должна быть такая:
      Выбрал свой объект в DC;
      Запомнил объект, который вернул метод SelectObject;
      Использовал свой объект при рисовании;
      Выбрал в DC объект, который запомнил (тем самым освободил свой объект из DC).

      ExpandedWrap disabled
        // Создаем Pen
        CPen Pen;
        Pen.CreatePen(PS_SOLID, 1, RGB(255, 0, 0));
         
        // Выбираем свой Pen в DC, запоминаем старый Pen
        CPen *pOldPen = pDC->SelectObject (&Pen);
         
        // Перемещаем точку рисования в левый верхний угол окна
        pDC->MoveTo(rc.left, rc.top);
        // Рисуем линию в правый нижний угол
        pDC->LineTo(rc.right, rc.bottom);
         
        // Выбираем старый Pen в DC (освобождаем свой Pen из DC)
        pDC->SelectObject(pOldPen);




      я и раньше встречал такой порядок, но не совсем понимаю зачем это делается..

      Подскажите, пожалуйста, чем это отличается от просто:

      ExpandedWrap disabled
        CPen Pen(PS_SOLID,1,RGB(255,0,0));
        pDC->SelectObject (&Pen);
         
        // Перемещаем точку рисования в левый верхний угол окна
        pDC->MoveTo(rc.left, rc.top);
        // Рисуем линию в правый нижний угол
        pDC->LineTo(rc.right, rc.bottom);


      Без последующего
      ExpandedWrap disabled
        // Выбираем старый Pen в DC (освобождаем свой Pen из DC)
        pDC->SelectObject(pOldPen);
      Сообщение отредактировано: Sergey K -
        Sergey K
        Вообще в цитате указано зачем...
        Цитата Uncle_Bob @
        Выбрал в DC объект, который запомнил (тем самым освободил свой объект из DC).

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

        Цитата "MSDN::DeleteObject"
        Do not delete a drawing object (pen or brush) while it is still selected into a DC.


        Так что ответ на вопрос такой : потому что такие правила работы с объектами GDI.

        Собственно это и написано в факе...
        Цитата Uncle_Bob @
        Вызов DeleteObject для объекта, который в данный момент выбран в DC к успеху не приведет. Перед удалением объект обязательно должен быть освобожден из DC.
          Ещё следует добавить, что при обработке сообщения WM_PAINT контекст следует получать с помощью BeginPaint(), а освобождать с помощью EndPaint(). В остальном всё то же самое.
            Uncle_Bob
            еще 1 вопрос,
            если я последовательно выбираю разные объекты т.е.:

            ExpandedWrap disabled
              CPen Pen;
              CBrush Brush1,Brush2;
              ...
              pDC->SelectObject (&Pen);  ...
              pDC->SelectObject (&Brush1);  ...
              pDC->SelectObject (&Brush2);  ...


            то тогда, мне надо запоминать только последний использованный объект и освобождать только его?
            ExpandedWrap disabled
              ...
              CBrush *pOldBr =pDC->SelectObject (&Brush2);
              pDC->SelectObject(pOldBr);
            Сообщение отредактировано: Sergey K -
              Цитата Uncle_Bob @
              Рисование в DC происходит посредством ресурсов GDI, таких как Pen, Brush, Font и Bitmap.

              Под каждый из типов ресурса надо запоминать оригинальный ресурс. В твоем примере надо запомнить оригинальный Pen и Brush. После выбора последовательно нескольких Brush, восстановить надо исходный:

              ExpandedWrap disabled
                // выбираем в DC Brush1, освобождаем pOrigBrush
                CBrush *pOrigBrush = pDC->SelectObject (&Brush1);
                ...
                // выбираем в DC Brush2, освобождаем Brush1
                pDC->SelectObject (&Brush2);
                ...
                // выбираем в DC pOrigBrush, освобождаем Brush2
                pDC->SelectObject (pOrigBrush);
              0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
              0 пользователей:


              Рейтинг@Mail.ru
              [ Script execution time: 0,0287 ]   [ 15 queries used ]   [ Generated: 22.05.24, 00:00 GMT ]