MFC 로 만든 업무용 프로그램은 생각보다 오래 산다. 십 년 넘게 쓰인 프로그램에 기능 하나를 더하려고 대화 상자를 열면, 거의 빠짐없이 목록 상자 CListBox 가 있다. 쓸 일이 자주 있는 컨트롤인데 쓸 때마다 "선택이 없으면 뭐가 돌아왔더라", "항목에 ID 를 붙이는 함수가 뭐였더라"를 다시 찾게 된다. 그래서 실제로 자주 쓰는 것만 골라 한 번에 정리했다.
아래 코드는 대화 상자에 목록 상자를 하나 놓고 컨트롤 변수 m_list(CListBox)를 연결했다고 가정한다. 리소스 ID 는 IDC_LIST1 이다.
항목 넣기: AddString 과 정렬 스타일의 함정
항목은 AddString 으로 넣는다. 반환값이 방금 넣은 항목의 위치(0부터)다.
BOOL CMyDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
m_list.AddString(_T("서울"));
m_list.AddString(_T("부산"));
int index = m_list.AddString(_T("대구")); // 넣은 위치
m_list.InsertString(0, _T("전체")); // 맨 앞에 끼워 넣기
return TRUE;
}
여기서 가장 많이 걸리는 함정이 정렬 스타일이다. 리소스 편집기에서 목록 상자를 놓으면 속성의 Sort(LBS_SORT)가 기본으로 켜져 있다. 이 상태에서 AddString 은 넣은 순서가 아니라 가나다순 위치에 항목을 넣는다. 그래서 "세 번째로 넣은 항목이 2번일 것"이라고 가정한 코드가 엉뚱한 항목을 가리킨다. 넣은 순서대로 보여야 한다면 Sort 속성을 끄고, 정렬을 켠 채로 쓴다면 위치는 반드시 AddString 의 반환값으로 받는다. InsertString 은 정렬 스타일과 상관없이 지정한 위치에 넣는다.
모두 지울 때는 ResetContent(), 하나만 지울 때는 DeleteString(index) 를 쓴다.
선택 읽기: GetCurSel 과 LB_ERR
단일 선택 목록에서 선택된 항목은 GetCurSel 로 읽는다. 아무것도 선택되지 않았으면 LB_ERR(-1)이 돌아온다. 이 확인을 빼먹으면 GetText(-1, ...) 같은 호출이 그대로 실행된다.
void CMyDlg::OnBnClickedOk()
{
int sel = m_list.GetCurSel();
if (sel == LB_ERR)
{
AfxMessageBox(_T("항목을 선택하세요."));
return;
}
CString text;
m_list.GetText(sel, text);
// text 사용
}
코드에서 선택을 바꿀 때는 SetCurSel(index), 선택을 없앨 때는 SetCurSel(-1) 이다. 문자열로 항목을 찾을 때는 FindStringExact(-1, _T("부산")) 이 정확히 같은 항목의 위치를 돌려준다. FindString 은 앞부분만 같아도 찾는다는 점이 다르다.
문자열 대신 ID 로: SetItemData
화면에는 이름을 보여 주고 프로그램은 DB 의 ID 로 일하고 싶을 때가 대부분이다. 이름 문자열로 다시 ID 를 찾는 코드를 짜기 쉬운데, 이름이 겹치는 순간 깨진다. 항목마다 숫자 하나를 붙여 두는 SetItemData 를 쓰면 된다.
struct City { int id; LPCTSTR name; };
const City cities[] = { { 11, _T("서울") }, { 26, _T("부산") }, { 27, _T("대구") } };
for (const City& c : cities)
{
int i = m_list.AddString(c.name); // 정렬 스타일이어도 반환값이 실제 위치
m_list.SetItemData(i, c.id);
}
// 선택 항목의 ID
int sel = m_list.GetCurSel();
if (sel != LB_ERR)
{
int id = static_cast<int>(m_list.GetItemData(sel));
}
SetItemData 에는 포인터 크기의 정수(DWORD_PTR)가 들어가므로 객체 포인터를 넣을 수도 있다. 그 경우 목록을 비울 때 그 객체를 누가 지울지 정해 두어야 한다. 목록 상자는 붙여 둔 값을 지워 주지 않는다.
다중 선택, 선택 변경 이벤트, 가로 스크롤
여러 개를 고르게 하려면 속성의 Selection 을 Extended(LBS_EXTENDEDSEL, Ctrl·Shift 로 고르기)나 Multiple(LBS_MULTIPLESEL, 클릭할 때마다 켜고 끄기)로 바꾼다. 이때는 GetCurSel 이 아니라 GetSelCount 와 GetSelItems 로 읽는다.
int count = m_list.GetSelCount();
if (count > 0)
{
CArray<int, int> items;
items.SetSize(count);
m_list.GetSelItems(count, items.GetData());
for (int i = 0; i < count; i++)
{
CString text;
m_list.GetText(items[i], text);
}
}
선택이 바뀔 때마다 무언가를 하려면 LBN_SELCHANGE 알림을 받는다. 이 알림은 속성의 Notify(LBS_NOTIFY)가 켜져 있어야 온다. 리소스 편집기로 놓은 목록 상자는 기본으로 켜져 있지만, 코드로 Create 한 목록 상자에서 알림이 안 온다면 이것부터 본다. 더블클릭은 LBN_DBLCLK 이다.
BEGIN_MESSAGE_MAP(CMyDlg, CDialogEx)
ON_LBN_SELCHANGE(IDC_LIST1, &CMyDlg::OnLbnSelchangeList1)
ON_LBN_DBLCLK(IDC_LIST1, &CMyDlg::OnLbnDblclkList1)
END_MESSAGE_MAP()
긴 문자열이 잘려 보일 때는 Horizontal Scroll 속성을 켜는 것만으로는 부족하다. 목록 상자는 내용의 폭을 스스로 계산하지 않기 때문에 SetHorizontalExtent 로 가장 긴 항목의 픽셀 폭을 직접 알려 줘야 가로 스크롤 막대가 생긴다.
CClientDC dc(&m_list);
CFont* oldFont = dc.SelectObject(m_list.GetFont());
int maxWidth = 0;
for (int i = 0; i < m_list.GetCount(); i++)
{
CString text;
m_list.GetText(i, text);
maxWidth = max(maxWidth, dc.GetTextExtent(text).cx);
}
dc.SelectObject(oldFont);
m_list.SetHorizontalExtent(maxWidth + 4);
항목 수천 개를 넣을 때
항목을 수천 개 넣으면 넣을 때마다 다시 그리느라 눈에 띄게 느려진다. 넣기 전에 SetRedraw(FALSE) 로 그리기를 멈추고, 끝나면 다시 켜고 한 번만 그린다. 개수를 미리 알면 InitStorage 로 메모리를 한 번에 잡아 두는 것도 도움이 된다.
m_list.SetRedraw(FALSE);
m_list.ResetContent();
m_list.InitStorage(rows.GetCount(), rows.GetCount() * 32 * sizeof(TCHAR)); // 항목 수, 문자열 전체 예상 바이트
for (int i = 0; i < rows.GetCount(); i++)
{
int idx = m_list.AddString(rows[i].name);
m_list.SetItemData(idx, rows[i].id);
}
m_list.SetRedraw(TRUE);
m_list.Invalidate();
항목이 만 개를 넘어가거나 열이 여러 개 필요해지면 목록 상자보다 리스트 컨트롤이 맞다. 열 머리글과 정렬, 수십만 행도 버티는 가상 목록까지 되는 CListCtrl 의 리포트 보기는 MFC CListCtrl 리포트 보기 정리에 이어서 정리했다.