GDI+ 의 메모리 릭 디텍트 방법
음.. 오늘 GDI+ 에서 memory leak 이 발생할 경우 비주얼스튜디오가 전혀 감지를 못하는걸 보고 미친듯이 뷁뷁을 외쳐주다가 찾은자료.
http://www.codeproject.com/vcpp/gdiplus/gdiplush.asp
일단 읽어보니 GDI+ 의 경우 자체적인 메모리 할당 연산자를 사용하는데 이것이 Gdiplus.dll 내부에서 어찌어찌 쌓여서.. 비주얼 스튜디오에서는 감지를 못하는 상황인듯 하다.
위에서 사용한 방법은 특정 메크로가 정의 되어있지 않을때(!)는 GdiplusBase 객체를 자체적으로 다시 정의해서 내부의 메모리 할당 연산을 일반적인 new/delete (윈도우에서 가장 기본적으로 제공함으로써 비주얼스튜디오에서 감지가능한것)을 사용하게 하는것이다.
위의 URL에서 다운을 받으면 헤더 파일이 나오는데 그걸 Gdiplus.h 대신 인클루드 하고 사용하면 된다.
MFC에서 new 를 DEBUG_NEW로 재정의 하는 것과 비슷한… 거시기.
아래는 위의 URL의 소스를 기억용으로 한글로 주석 달아놓은것
아직 깔끔하게 적용하지 못해서 좀 찝찝하다.
예를 들어 메모리 릭이 났을 경우 소스파일의 라인을 출력해야 되는데 이건 그냥 저 헤더파일을 출력한다. 메크로로 잘 정의하면 될것 같긴 한데 느므느므 귀찮다.
#ifndef _GDIPLUSH_H_INCLUDED_
#define _GDIPLUSH_H_INCLUDED_
/// @file AntGdiplusHelper.h
/// @brief GDI+ 메모리 연산자 재정의 헤더파일. 메모리 누수시 감지를 위함.
/// @warning Gdiplus.h 대신 해당 파일을 인클루드 해야 함.
/// @warning Debug 에서는 GDIPLUS_USE_GDIPLUS_MEM 정의 하지 않음 -> Gdiplus 메모리 연산자를 사용하지 않고 릭 감지 가능
/// @warning Release 에서는 GDIPLUS_USE_GDIPLUS_MEM를 정의함 -> Gdiplus 메모리 연산자를 사용하지 않고 릭 감지 못함
//
// GDI+ helper file v1.0
// Written by Zoltan Csizmadia (zoltan_csizmadia@yahoo.com)
//
// GDIPLUS_USE_GDIPLUS_MEM:
// 위의 메크로가 정의 되어있을때 GDI+ 에서 제공하는 메모리 할당 함수를 사용한다.
// 이 경우 _CrtXXXXX에 해당하는 메모리 디버깅용 함수가 일반적으로 작동하지 않는다.
// (원래 GDI+는 _CrtXXXX 가 작동하지 못했다. 즉 해당 메크로를 설정하는게 기본설정.)
//#define GDIPLUS_USE_GDIPLUS_MEM
#ifdef _GDIPLUS_H
#error Gdiplus.h is already included. You have to include this file instead.
#endif
#define _GDIPLUSBASE_H
namespace Gdiplus
{
namespace DllExports
{
#include “GdiplusMem.h”
};
class GdiplusBase
{
public:
#ifdef _DEBUG
static void* __cdecl GdiplusAlloc( size_t nSize, LPCSTR szFileName, int nLine )
{
#ifdef GDIPLUS_USE_GDIPLUS_MEM
UNREFERENCED_PARAMETER(szFileName);
UNREFERENCED_PARAMETER(nLine);
return DllExports::GdipAlloc(nSize);
#else
return ::operator new( nSize, szFileName, nLine );
#endif // GDIPLUS_USE_GDIPLUS_MEM
}
static void GdiplusFree( void* pVoid, LPCSTR szFileName, int nLine )
{
#ifdef GDIPLUS_USE_GDIPLUS_MEM
UNREFERENCED_PARAMETER(szFileName);
UNREFERENCED_PARAMETER(nLine);
DllExports::GdipFree(pVoid);
#else
::operator delete( pVoid, szFileName, nLine );
#endif // GDIPLUS_USE_GDIPLUS_MEM
}
void* (operator new)(size_t nSize)
{
return GdiplusAlloc( nSize, __FILE__, __LINE__ );
}
void* (operator new[])(size_t nSize)
{
return GdiplusAlloc( nSize, __FILE__, __LINE__ );
}
void * (operator new)(size_t nSize, LPCSTR lpszFileName, int nLine)
{
return GdiplusAlloc( nSize, lpszFileName, nLine );
}
void (operator delete)(void* pVoid)
{
GdiplusFree( pVoid, __FILE__, __LINE__ );
}
void (operator delete[])(void* pVoid)
{
GdiplusFree( pVoid, __FILE__, __LINE__ );
}
void operator delete(void* pVoid, LPCSTR lpszFileName, int nLine)
{
GdiplusFree( pVoid, lpszFileName, nLine);
}
#else // _DEBUG
static void* __cdecl GdiplusAlloc( size_t nSize )
{
#ifdef GDIPLUS_USE_GDIPLUS_MEM
return DllExports::GdipAlloc(nSize);
#else
return ::operator new(nSize);
#endif // GDIPLUS_USE_GDIPLUS_MEM
}
static void GdiplusFree( void* pVoid )
{
#ifdef GDIPLUS_USE_GDIPLUS_MEM
DllExports::GdipFree(pVoid);
#else
::operator delete( pVoid );
#endif // GDIPLUS_USE_GDIPLUS_MEM
}
void* (operator new)(size_t nSize)
{
return GdiplusAlloc( nSize );
}
void* (operator new[])(size_t nSize)
{
return GdiplusAlloc( nSize );
}
void (operator delete)(void* pVoid)
{
GdiplusFree( pVoid );
}
void (operator delete[])(void* pVoid)
{
GdiplusFree( pVoid );
}
#endif // _DEBUG
};
};
#include <Gdiplus.h>
#endif
크아 GDI+는 메모리 릭이나도 출력창에 정보가 안나와!!
GDI+는 메모리릭이 나도 디버거에서 아예 감지를 못한다!
뷁!
왜 날 브레끼!!
난 지금 까지 계~~~~속
– 훗 메모리 안새는군. 하하하
이러면서 작업했는데!
GDI+에서 투명처리 및 특정 색을 원하는 색으로 변환
위에가 원본 bitmap 밑이 투명처리와 색 변환을 적용한 CachedBitmap.
자주색은 투명처리, 파란색은 노란색으로 변환.
투명처리 및 색 변화 모두 ImageAttributes 라는 객체에 값을 설정해서 Graphics객체의 DrawImage 등을 호출할때 인자로 넣어주면 된다.
{
Bitmap tmpBit( markTp_.GetWidth(), markTp_.GetHeight(), PixelFormat32bppARGB );
ColorMap tplToinner;
tplToinner.oldColor = tplColor_;
tplToinner.newColor = innerColor;
ImageAttributes imgAttr;
imgAttr.SetRemapTable(1, &tplToinner,ColorAdjustTypeBitmap);
imgAttr.SetColorKey(transColor_, transColor_,ColorAdjustTypeBitmap );
pMemGraphics->DrawImage(&markTp_, Rect(0,0,tmpBit.GetWidth(),tmpBit.GetHeight()),
0.0,0.0,markTp_.GetWidth(), markTp_.GetHeight(), UnitPixel, &imgAttr, NULL, NULL);
delete pMemGraphics;
CachedBitmap* ccBit = new CachedBitmap(&tmpBit, g);
return ccBit;
}
코드는 대략 이런 느낌.
주의할 점은 Bitmap->CachedBitmap으로 바꾸면서 위의 내용을 적용할수가 없기때문에 Bitmap -> 변환된 Bitmap -> CachedBitmap 으로 간다. Bitmap의 변환에는 Graphics객체를 따로 생성해서 그려주는 수밖에 없는듯하다.
GDI+ 메소드별 출력속도(사각형 그릴때)
요즘 회사에서 또다시 차트 라이브러리를 제작중.
(프로젝트명은 내마음대로 “Ant chart”. 사실 난 오리콘 차트 같은걸로 하고 싶었는데 왜색이 짙다는 의견을 수렴해서 더 짧은걸로 바꿨다. 빌보드 차트역시 선상에 몰랐지만 너무 길어서…. 역시 네이밍을 위해 짧은게 좋다.)
목표는 1000만건의 데이타를 광속으로 찍어주는 라이브러리다. 뭐 사실 그림을 그리리는 퍼포먼스에 영향을 미치는 종류는 대략 3개정도 밖에 되지 않는다. 스캐터플롯과 라인플롯, 바 플롯
나머지 히스토그램이니 파이니 박스 플롯이니 하는것들은 실제 데이타를 가공해서 출력(일반적으로 그룹핑)하니 출력을 위한 데이터는 줄어들어 버려서 상대적으로 차이가 덜하다.
뭐 어쨋거나 이런짓 저런짓 하면서 좀더 빨리 찍어줄 방법이 없을까 테스트를 해본 결과를 올린다.
일단 GDI+의 경우는 매트릭스에 의한 변환(이동, 회전, 확대)을 사용할수 있는데 그건 안썼다.
일단 차트라는건 데이타를 정확하게 표현하는게 중요한데 데이타를 표현하는 점(이하 마크)은 사람들이 명확하게 보기 위해서 데이타의 범위에 관계 없이 일정한 크기를 갖는다.(5*5 크기의 원이거나 사각형이거나) 문제는 매트릭스 변환중에 하나의 확대/축소 변환으로 데이타 범위에 대한 계산을 생략해 버리면 이 마크의 크기도 변해 버린다.
게다가 일반적인 좌표축과 윈도우 좌표체계는 y축의 방향이 반대다. 이걸 역시 확대변환(y축에 -1)로 변환하면…. 심지어 문자열 출력할때도 뒤집혀 버린다.
뭐 하여간 해결하지 못한 이런저런 이유로 GDI+에서 제공하는 변환은 사용하지 못하고 위치계산 범위계산 등등은 어쩔수없이 직접 코딩을 해야 할것 같다.(뭐.. 그리 복잡하진 않지만)
그리고 마크를 출력할때(자꾸 마크의 출력을 강조하는 이유는 1000만건의 데이타를 출력한다는건 1000만개의 마크를 찍는다는 의미라서) 처음에는 DrawRectanlge 등의 백터이미지에 해당하는 메소드를 호출하는 방법을 생각했었는데 나중에 아예 마크자체를 비트맵으로 만들어 두고 그걸 찍는게 생각났다. 그래서 테스트!
위의 그림이 결관데 일단 테스트는 대략 4만건 정도의 X,Y 두개의 필드가 있는 데이타를 사용했다.
위에서 부터
GDI+ 의 CachedBitmap 이란 객체로 마크를 만들어서 찍은 경우
GDI+ 의 Bitmap 객체로 마크를 만들어두고 찍은 경우
Graphics 객체의 DrawRactangle, FillRactangle 을 사용한 경우
GraphicsPath 객체를 사용해서 백터이미지를 버퍼에 보아서 일정 개수 만큼씩(만개) 찍어준 경우.
이다.
가장 속도가 빠른건 CachedBitmap을 사용한 경우.
GDI+의 문서를 읽어보니 Bitmap 객체와의 차이점은 현재 출력을 위해 사용하는 윈도우 디바이스 객체에 종속되서 생성되는 객체라고 한다. Bitmap은 독립적인 객체라 실제 찍어줄때는 다시 변환하는 과정을 거쳐서 CachedBitmap 보다 느리다고 한다.
실제로 CachedBitmap 의 경우 Bitmap 객체를 생성자의 인자로 받아 생성하게 된다.
그리고 뒤의 두개는 백터 이미지 출력방식.
그냥 하나 하나 그려주는 방법보단 역시 버퍼에 모았다가 한번에 그려주는 방식이 빠르긴 하다.
근데 GraphicsPath를 사용할때의 문제점은… 마크 하나하나가 독립적으로 그려져야 하는데 GDI+에서 제공하는 인터페이스상 그렇게 그릴수가 없다.
GraphicsPath 의 사용법은 AddRectangle을 호출해서 사각형을 마구 버퍼에 집어넣고 Graphics객체의 DrawPath(맞나?), FillPath를 이용해서 그리게 된다. Draw 함수는 선만을 그리고 FillPath는 안의 색을 칠하는데 이 두과정이 번갈아 가면서 일어나야 독립된 사각형(마크)를 그리는데 그게 안된다. 즉 모든 사각형의 면을 칠하고 나서 다시 처음부터 모든 사각형의 선을 칠한다. 그러면? 서로 겹쳐져 있는 마크들은 선이 겹쳐진다. 가장 마지막 그림의 데이타들의 면부분이 없는 이유다.
흠 이런 저런 테스트를 해서 일단은 마크는 CachedBitmap을 사용해서 그릴려고 생각하는데 지금 막히는 문제는… CachedBitmap은 복사생성자나 operator= 가 private로 되어있다. 뭔 이윤지는 정확히 모르겠지만… 이 이유땜시 클래스로 랩핑하는데 좀 에로 사항이 꽃피고 있다.
음… 내일은 이부분하고 RTTI좀 생각해 봐야겠다.
아 데이타클래스도 만들어야 되는데. 콘란하군.
Gdiplus::GraphicsPath::Transform 에 대해서
gdi+ 에서 Graphics 객체의 world coordinate는 설정해 놓은 이후에 draw 되는 데이터들이 적용되지만 GraphicsPath 가 Transform 을 이용해서 좌표 데이타를 변환하는 것은 Transform 이 호출되기 이전에 GraphicsPath에 넣어둔 데이터에만 적용돤다.
흠…어찌보면 당연한거 같기도 하고.
어쨋거나 메모.
삽질 – 007 :: GDI+를 쓸때는 WIN32_LEAN_AND_MEAN 을 정의하지 마라
#define WIN32_LEAN_AND_MEAN
정의하면 GDI+의 헤더만 인클루드 해도 에러사항이 꽃핌.
아 놔 이런건 GDI+ 소개하는 페이지에 대문작 만하게 써놔야지 말야.
GDI+를 이용한 문자열 회전 출력
일반 GDI를 쓰면 폰트 생성할때 각도 설정해주고 위치 정해준다음 찍으면 된다.
GDI+를 쓰면 더 복잡하다.
그런데 폰트를 회전시키면 글자가 찌그러지는 현상이 발생하는데 내가 아는바로는 출력할때 안티얼라이싱 이외에는 방법이 없다.
문제는 GDI의 경우는 만족할만한 안티얼라이싱 효과를 주기 위해서는 윈도우 버젼이 XP이상이 되야 하는 단점이 있다.
이 경우는 DLL을 더 배포하더라도 GDI+를 쓰는것이 좋다.
그럼 복잡한 GDI+의 코드를 보면.
Gdiplus::PointF pointF( pos.x, pos.y);
Gdiplus::SolidBrush solidBrush(Gdiplus::Color(255, 0, 0, 0));
Gdiplus::Font labelFont( hdc , &m_lfLabel);
graphics.TranslateTransform(pointF.X,pointF.Y);
graphics.RotateTransform(-(dAngle));
graphics.TranslateTransform(-pointF.X,-pointF.Y);
graphics.SetTextRenderingHint(Gdiplus::TextRenderingHintAntiAlias);
graphics.DrawString(lpszW, -1, &labelFont, pointF, &solidBrush);
우선 lpszW 가 wchar* 로 문자열을 담고있다. 일반적으로 윈도우 프로그래밍에서 많이 사용하는 CString을 wchar로 변환하는 방법은 밑의 글을 참조하자.
1. DC핸들로 부터 Graphics 객체를 생성한뒤 문자열을 찍을 위치를 설정
2. 문자열을 찍을 위치를 PointF 구조체에 넣는다.
3. 브러시 생성
4. 여기서는 미리 가지고 있던 LOGFONT(m_lfLabel) 을 이용해서 폰트를 생성하지만 gdi+에서 폰트를 생성하는 방법은 많다. 적당히 쓰자. LOGFONT로 부터 생성하는 방법은 LOGFONT의 특정 값만들 사용한다고 한다.
5. TranslateTransform 을 이용해서 찍을 려는 위치를 Graphis 객체의 중심으로 설정한다.
6. RotateTransform 을 이용해서 Graphics 객체를 회전시킨다.
7. TranslateTransform 를 이용해서 찍을려는 위치를 회전된 위치로 옮긴다.
8. DrawString 을 이용해서 문자열을 찍는다.
….
음 무슨 3D Programming 하는것도 아니고….(3D Programming 하는 사람이 보면 웃겠지만) 문자열 찍는데 이렇게 복잡한 인터페이스로 해놓다니…
하긴 가장 기본적인 것만 제공하는게 효과적으로 사용할수 있고 응용범위가 넓어지는건 사실이다.(쓰는 사람이 알아야 될것도 많고 연구해야 할것도 많아서 그렇지)
MS는 gdi+를 이용한 간단한 3D관련 응용프로그램의 활발한 개발을 원하는게 아닐까 (예제들 보면 꽤나 그럴듯 한것도 많고)
자동 더블버퍼링 DC
훌륭하다.
void CRangeSlider::OnPaint()
{
CPaintDC RawDC(this); // device context for painting
CMemDC dc(&RawDC);
dc.SetMapMode(MM_TEXT);
if (m_bHorizontal) {
OnPaintHorizontal(dc);
} else {
// Vertical Mode
OnPaintVertical(dc);
}
}
요 코드에서 CMemDC 란 녀석이 더블버퍼링을 해주는 DC 클래스이다. CDC를 상속받은 객체의 포인터를 넘겨주면 생성자에 자동으로 메모리 DC와 비트맵 객체를 생성해서 가지고 있는다.
이후에 다른 DrawCode를 이 CMemDC에 그려주고 나서 위의 코드처럼 Paint 함수가 끝나면서 이 변수의 소멸자가 호출될때 소멸자에서 자동으로 Bitblt 함수를 호출해서 원래 DC에 그려주는 구조다.
더블버퍼링 코드를 일일이 써주기 귀찮을때 사용해주면 좋을듯.
게다가 h 파일 하나로 구성되어 있으니 컴팩트!
간단하지만 유용한 클래스인듯. 근데 요즘은 GDI+를 많이 써서.. GDI+용으로 포팅해서 써도 괜찮을듯 하다.
126572.h