MFC CListCtrl 아이템 높이 키우는 법: MeasureItem과 OWNERDRAWFIXED 완전 정리
TL;DR — MFC의 CListCtrl은 기본적으로 행 높이를 조절할 수 없습니다. MeasureItem과 ON_WM_MEASUREITEM_REFLECT 매크로를 이용해 리스트 아이템 높이를 자유롭게 키우는 방법과 함정을 정리했습니다.
문제 상황: CListCtrl의 행이 너무 좁다
MFC로 Windows 데스크톱 애플리케이션을 만들다 보면 CListCtrl(리스트 컨트롤)을 자주 쓰게 됩니다. 특히 보고서 형식(Report View)으로 표 형태의 데이터를 보여줄 때 유용하죠. 그런데 한 가지 답답한 점이 있습니다. 기본 행 높이가 폰트 크기에 맞춰 자동으로 고정되어 버린다는 것입니다.
행에 큰 아이콘을 넣고 싶거나, 가독성을 위해 행 간격을 넓히고 싶거나, 터치 환경처럼 클릭 영역을 키워야 할 때 SetItemHeight 같은 함수를 찾아보지만 CListCtrl에는 그런 직관적인 API가 없습니다. 그래서 많은 개발자들이 "리스트 컨트롤은 행 높이를 못 바꾸나?"라고 결론짓고 포기하는 경우가 많습니다.
결론부터 말하면 바꿀 수 있습니다. 다만 일반적인 setter 호출이 아니라, 윈도우 메시지 시스템을 한 단계 파고들어야 합니다. 이 글에서는 그 정석적인 방법과, 실무에서 반드시 마주치는 함정들을 단계별로 정리합니다.
왜 SetItemHeight가 없을까: 개념부터 짚기
CListCtrl의 행 높이는 컨트롤이 스스로 그릴 때(self-draw) 윈도우에게 "이 항목은 얼마나 높게 그려야 하나요?"라고 묻는 과정에서 결정됩니다. 이때 윈도우는 WM_MEASUREITEM 메시지를 보내고, 이 메시지에 응답하면 높이를 우리가 직접 지정할 수 있습니다.
핵심은 두 가지입니다.
- 컨트롤이 owner-draw 또는 변형 가능한 상태여야 한다 — 단순 텍스트 자동 그리기 상태에서는 높이 측정 메시지가 발생하지 않습니다.
WM_MEASUREITEM은 부모 윈도우로 전달된다 — 그래서 컨트롤 클래스 자신이 받으려면 "메시지 반사(Message Reflection)"를 사용해야 합니다.
이 두 가지를 코드로 풀어내는 것이 전체 작업의 전부입니다.
단계별 해결 방법
1단계: CListCtrl을 상속한 커스텀 클래스 만들기
원본 CListCtrl을 직접 건드릴 수는 없으니, 이를 상속한 파생 클래스를 하나 만듭니다. 예제에서는 CTallListCtrl이라는 이름을 쓰겠습니다.
// TallListCtrl.h
#pragma once
class CTallListCtrl : public CListCtrl
{
DECLARE_DYNAMIC(CTallListCtrl)
public:
CTallListCtrl();
virtual ~CTallListCtrl();
protected:
// 윈도우가 항목 높이를 물어볼 때 호출되는 핸들러
afx_msg void MeasureItem(LPMEASUREITEMSTRUCT lpMeasure);
DECLARE_MESSAGE_MAP()
};
afx_msg 키워드는 이 함수가 MFC 메시지 핸들러임을 표시하는 매크로입니다. 함수 이름은 관례적으로 MeasureItem을 쓰지만, 반사 매크로와 짝만 맞으면 됩니다.
2단계: 메시지 맵에 반사 매크로 등록
여기가 가장 중요한 부분입니다. WM_MEASUREITEM은 원래 부모(다이얼로그/뷰)로 가는 메시지이기 때문에, 컨트롤 자신이 처리하려면 반사(reflect) 버전 매크로를 써야 합니다.
// TallListCtrl.cpp
#include "TallListCtrl.h"
IMPLEMENT_DYNAMIC(CTallListCtrl, CListCtrl)
CTallListCtrl::CTallListCtrl() {}
CTallListCtrl::~CTallListCtrl() {}
BEGIN_MESSAGE_MAP(CTallListCtrl, CListCtrl)
// 반드시 _REFLECT 버전이어야 컨트롤 자신이 받는다
ON_WM_MEASUREITEM_REFLECT()
END_MESSAGE_MAP()
ON_WM_MEASUREITEM()(반사 없는 버전)을 잘못 쓰면 컨트롤 클래스에서는 절대 호출되지 않습니다. 반드시 ON_WM_MEASUREITEM_REFLECT() 여야 합니다. 이 한 글자 차이가 "왜 내 MeasureItem이 안 불리지?"의 90% 원인입니다.
3단계: 높이 지정 구현
마지막으로 실제 높이를 채워 넣습니다. 단위는 픽셀입니다.
void CTallListCtrl::MeasureItem(LPMEASUREITEMSTRUCT lpMeasure)
{
// 모든 행을 48픽셀 높이로 지정
lpMeasure->itemHeight = 48;
}
LPMEASUREITEMSTRUCT는 측정 정보를 담는 구조체 포인터이고, itemHeight 멤버에 원하는 높이를 넣으면 끝입니다.
4단계: owner-draw 스타일 적용 (놓치기 쉬운 핵심)
여기까지만 하면 대부분 "아무 변화가 없는데?"라는 경험을 합니다. 이유는 컨트롤이 owner-draw 상태가 아니면 WM_MEASUREITEM 자체가 발생하지 않기 때문입니다.
리소스 편집기에서 리스트 컨트롤 속성의 Owner Draw Fixed를 True로 바꾸거나, 코드로 스타일을 추가해야 합니다.
// 다이얼로그의 OnInitDialog 등에서
m_list.ModifyStyle(0, LVS_OWNERDRAWFIXED);
단, LVS_OWNERDRAWFIXED를 켜면 윈도우가 더 이상 텍스트를 자동으로 그려주지 않습니다. 즉 셀 내용을 직접 그려야 한다는 의미입니다. 이때는 DrawItem도 함께 오버라이드해야 합니다.
void CTallListCtrl::DrawItem(LPDRAWITEMSTRUCT lpDraw)
{
CDC* pDC = CDC::FromHandle(lpDraw->hDC);
int nItem = lpDraw->itemID;
// 행 배경 채우기
pDC->FillSolidRect(&lpDraw->rcItem, RGB(255, 255, 255));
// 첫 번째 컬럼 텍스트를 수직 가운데 정렬로 출력
CString strText = GetItemText(nItem, 0);
CRect rcText = lpDraw->rcItem;
rcText.left += 8; // 좌측 여백
pDC->SetBkMode(TRANSPARENT);
pDC->DrawText(strText, &rcText, DT_LEFT | DT_VCENTER | DT_SINGLELINE);
}
흔한 실수와 엣지 케이스
실제로 이 기능을 붙이다 보면 다음 함정들에 자주 걸립니다.
- 반사 매크로 누락: 앞서 말한
ON_WM_MEASUREITEM_REFLECT()대신 일반 버전을 쓴 경우. 핸들러가 영원히 안 불립니다. - owner-draw 스타일 미적용:
LVS_OWNERDRAWFIXED가 없으면MeasureItem이 호출되지 않습니다. 높이 코드를 아무리 고쳐도 변화가 없습니다. - DrawItem 미구현으로 빈 행: owner-draw로 전환하면 텍스트가 사라집니다. 셀을 직접 그려야 합니다.
- 행마다 다른 높이를 기대:
LVS_OWNERDRAWFIXED는 이름 그대로 "고정 높이"입니다.MeasureItem은 보통 한 번만 호출되어 모든 행에 같은 높이가 적용됩니다. 행별로 가변 높이가 필요하면CListCtrl로는 한계가 있고,CMFCListCtrl이나 가상 리스트, 혹은 그리드 컨트롤 계열을 검토해야 합니다. - DPI 스케일링: 고정 픽셀값(48 등)을 그대로 쓰면 고해상도 모니터에서 작아 보입니다.
GetDeviceCaps로 DPI를 읽어 보정하거나MulDiv로 스케일링하는 것이 안전합니다. - 컬럼 헤더 높이는 별개: 이 방법은 데이터 행 높이만 바꿉니다. 헤더 높이를 키우려면
CHeaderCtrl을 따로 커스터마이즈해야 합니다.
요약
CListCtrl의 행 높이 조절은 직관적인 setter가 없을 뿐, 정해진 절차를 따르면 충분히 가능합니다. 핵심 흐름은 다음과 같습니다.
CListCtrl파생 클래스를 만든다.- 메시지 맵에
ON_WM_MEASUREITEM_REFLECT()를 등록한다. MeasureItem에서lpMeasure->itemHeight로 높이를 지정한다.- 컨트롤에
LVS_OWNERDRAWFIXED스타일을 적용한다. - owner-draw로 전환됐으므로
DrawItem에서 셀을 직접 그린다.
"높이가 안 바뀐다"의 원인은 거의 항상 반사 매크로 누락 또는 owner-draw 스타일 미적용, 이 둘 중 하나입니다. 이 두 가지만 확인해도 대부분 해결됩니다.
AI에게 물어볼 때 (프롬프트 팁)
MFC처럼 자료가 흩어져 있고 윈도우 메시지 흐름까지 얽힌 주제는, AI에게 질문할 때 컨텍스트와 제약을 구체적으로 명시할수록 정확한 답을 얻습니다. 다음과 같이 물어보세요.
"MFC
CListCtrl을 Report View로 쓰는데 행 높이를 48px로 키우고 싶다.LVS_OWNERDRAWFIXED와ON_WM_MEASUREITEM_REFLECT를 사용하는 전체 예제를DrawItem구현까지 포함해서 작성해줘. Visual Studio 2022 / MFC 기준.""내
MeasureItem핸들러가 호출되지 않는다. 메시지 맵과 헤더 선언을 붙여줄 테니, 반사 매크로와 owner-draw 스타일 관점에서 원인 후보를 우선순위대로 진단해줘.""
CListCtrl에서 행마다 다른 높이를 적용하고 싶은데LVS_OWNERDRAWFIXED로는 불가능하다고 들었다. 가변 높이를 구현하기 위한 대안(가상 리스트, 커스텀 그리드, WTL 등)을 장단점과 함께 비교해줘."
이처럼 사용 환경(버전/뷰 모드) + 목표 수치 + 이미 시도한 것 + 원하는 출력 형태를 한 번에 담아 질문하면, AI가 추측 없이 바로 쓸 수 있는 코드를 돌려줍니다. 좋은 답은 좋은 프롬프트에서 나옵니다.