MFC 컨트롤 활용 | CButton·CEdit·CListCtrl 완벽 가이드
이 글의 핵심
MFC는 Win32 컨트롤을 C++ 클래스로 감쌌습니다. CListCtrl로 리스트뷰, CTreeCtrl로 트리뷰를 구현하고, DDX_Control로 다이얼로그 컨트롤과 연결합니다. 알림 메시지는 ON_NOTIFY로 처리합니다.
들어가며
MFC 컨트롤 클래스는 Win32 API 컨트롤을 객체 지향 방식으로 감싼 것입니다. C++ 멤버 함수로 컨트롤을 제어하며, 메시지 맵으로 이벤트를 처리합니다. 2000년대 Windows 애플리케이션 UI의 핵심이었습니다.
// MFC 컨트롤 사용 예
CListCtrl m_list;
// 초기화
m_list.InsertColumn(0, _T("이름"), LVCFMT_LEFT, 150);
m_list.InsertColumn(1, _T("나이"), LVCFMT_LEFT, 80);
// 항목 추가
int index = m_list.InsertItem(0, _T("홍길동"));
m_list.SetItemText(index, 1, _T("30"));
// 선택 항목 얻기
POSITION pos = m_list.GetFirstSelectedItemPosition();
int sel = m_list.GetNextSelectedItem(pos);
1. 기본 컨트롤
1.1 CButton
class CMyDialog : public CDialog
{
public:
CButton m_btnStart;
CButton m_btnStop;
CButton m_checkAuto;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
// 버튼 활성화/비활성화
m_btnStart.EnableWindow(TRUE);
m_btnStop.EnableWindow(FALSE);
// 체크박스 상태 설정
m_checkAuto.SetCheck(BST_CHECKED);
// 버튼 텍스트 변경
m_btnStart.SetWindowText(_T("시작"));
return TRUE;
}
DECLARE_MESSAGE_MAP()
afx_msg void OnBnClickedStart()
{
m_btnStart.EnableWindow(FALSE);
m_btnStop.EnableWindow(TRUE);
AfxMessageBox(_T("시작!"));
}
afx_msg void OnBnClickedCheck()
{
int state = m_checkAuto.GetCheck();
if (state == BST_CHECKED) {
AfxMessageBox(_T("자동 모드 ON"));
} else {
AfxMessageBox(_T("자동 모드 OFF"));
}
}
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_BN_CLICKED(IDC_BTN_START, &CMyDialog::OnBnClickedStart)
ON_BN_CLICKED(IDC_CHECK_AUTO, &CMyDialog::OnBnClickedCheck)
END_MESSAGE_MAP()
1.2 CEdit
class CMyDialog : public CDialog
{
public:
CEdit m_edit;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
// 최대 글자 수 제한
m_edit.SetLimitText(100);
// 초기 텍스트
m_edit.SetWindowText(_T("초기값"));
// 선택 영역 설정
m_edit.SetSel(0, -1); // 전체 선택
// 읽기 전용
m_edit.SetReadOnly(TRUE);
return TRUE;
}
afx_msg void OnEnChangeEdit()
{
CString text;
m_edit.GetWindowText(text);
// 실시간 글자 수 표시
CString status;
status.Format(_T("%d 자"), text.GetLength());
SetDlgItemText(IDC_STATIC_COUNT, status);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_EN_CHANGE(IDC_EDIT, &CMyDialog::OnEnChangeEdit)
END_MESSAGE_MAP()
1.3 CStatic
class CMyDialog : public CDialog
{
public:
CStatic m_staticLabel;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
// 텍스트 변경
m_staticLabel.SetWindowText(_T("새 레이블"));
// 색상 변경 (서브클래싱 필요)
// 또는 OnCtlColor로 처리
return TRUE;
}
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
if (pWnd->GetDlgCtrlID() == IDC_STATIC_LABEL) {
pDC->SetTextColor(RGB(255, 0, 0)); // 빨간색 텍스트
pDC->SetBkMode(TRANSPARENT);
}
return hbr;
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_WM_CTLCOLOR()
END_MESSAGE_MAP()
2. 리스트 컨트롤 (CListBox, CComboBox)
2.1 CListBox
class CMyDialog : public CDialog
{
public:
CListBox m_listBox;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
// 항목 추가
m_listBox.AddString(_T("항목 1"));
m_listBox.AddString(_T("항목 2"));
m_listBox.AddString(_T("항목 3"));
// 특정 위치에 삽입
m_listBox.InsertString(0, _T("맨 앞 항목"));
// 첫 번째 항목 선택
m_listBox.SetCurSel(0);
return TRUE;
}
afx_msg void OnLbnSelchangeList()
{
int sel = m_listBox.GetCurSel();
if (sel != LB_ERR) {
CString text;
m_listBox.GetText(sel, text);
CString msg;
msg.Format(_T("선택: %s (인덱스: %d)"), text, sel);
AfxMessageBox(msg);
}
}
afx_msg void OnLbnDblclkList()
{
int sel = m_listBox.GetCurSel();
if (sel != LB_ERR) {
CString text;
m_listBox.GetText(sel, text);
AfxMessageBox(_T("더블클릭: ") + text);
}
}
afx_msg void OnBnClickedAdd()
{
CString text;
GetDlgItemText(IDC_EDIT_ITEM, text);
if (!text.IsEmpty()) {
m_listBox.AddString(text);
SetDlgItemText(IDC_EDIT_ITEM, _T(""));
}
}
afx_msg void OnBnClickedDelete()
{
int sel = m_listBox.GetCurSel();
if (sel != LB_ERR) {
m_listBox.DeleteString(sel);
// 다음 항목 선택
int count = m_listBox.GetCount();
if (count > 0) {
if (sel >= count) sel = count - 1;
m_listBox.SetCurSel(sel);
}
}
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_LBN_SELCHANGE(IDC_LIST, &CMyDialog::OnLbnSelchangeList)
ON_LBN_DBLCLK(IDC_LIST, &CMyDialog::OnLbnDblclkList)
ON_BN_CLICKED(IDC_BTN_ADD, &CMyDialog::OnBnClickedAdd)
ON_BN_CLICKED(IDC_BTN_DELETE, &CMyDialog::OnBnClickedDelete)
END_MESSAGE_MAP()
2.2 CComboBox
class CMyDialog : public CDialog
{
public:
CComboBox m_combo;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
// 항목 추가
m_combo.AddString(_T("서울"));
m_combo.AddString(_T("부산"));
m_combo.AddString(_T("대구"));
m_combo.AddString(_T("인천"));
// 첫 번째 항목 선택
m_combo.SetCurSel(0);
return TRUE;
}
afx_msg void OnCbnSelchangeCombo()
{
int sel = m_combo.GetCurSel();
if (sel != CB_ERR) {
CString text;
m_combo.GetLBText(sel, text);
AfxMessageBox(_T("선택: ") + text);
}
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_CBN_SELCHANGE(IDC_COMBO, &CMyDialog::OnCbnSelchangeCombo)
END_MESSAGE_MAP()
3. CListCtrl (리스트뷰)
3.1 기본 사용법
class CMyDialog : public CDialog
{
public:
CListCtrl m_list;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
// 확장 스타일
m_list.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
// 컬럼 추가
m_list.InsertColumn(0, _T("이름"), LVCFMT_LEFT, 150);
m_list.InsertColumn(1, _T("나이"), LVCFMT_LEFT, 80);
m_list.InsertColumn(2, _T("직업"), LVCFMT_LEFT, 150);
// 항목 추가
AddUser(_T("홍길동"), 30, _T("개발자"));
AddUser(_T("김철수"), 25, _T("디자이너"));
AddUser(_T("이영희"), 28, _T("기획자"));
return TRUE;
}
void AddUser(const CString& name, int age, const CString& job)
{
int index = m_list.GetItemCount();
// 첫 번째 컬럼 (항목 추가)
m_list.InsertItem(index, name);
// 나머지 컬럼
CString strAge;
strAge.Format(_T("%d"), age);
m_list.SetItemText(index, 1, strAge);
m_list.SetItemText(index, 2, job);
// 항목 데이터 (사용자 정의 데이터)
m_list.SetItemData(index, (DWORD_PTR)age);
}
DECLARE_MESSAGE_MAP()
afx_msg void OnLvnItemchangedList(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMLISTVIEW pNMLV = reinterpret_cast<LPNMLISTVIEW>(pNMHDR);
if (pNMLV->uNewState & LVIS_SELECTED) {
int index = pNMLV->iItem;
CString name = m_list.GetItemText(index, 0);
CString age = m_list.GetItemText(index, 1);
CString job = m_list.GetItemText(index, 2);
CString msg;
msg.Format(_T("%s, %s세, %s"), name, age, job);
SetDlgItemText(IDC_STATIC_INFO, msg);
}
*pResult = 0;
}
afx_msg void OnNMDblclkList(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMITEMACTIVATE pNMIA = reinterpret_cast<LPNMITEMACTIVATE>(pNMHDR);
int index = pNMIA->iItem;
if (index != -1) {
CString name = m_list.GetItemText(index, 0);
AfxMessageBox(_T("더블클릭: ") + name);
}
*pResult = 0;
}
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST, &CMyDialog::OnLvnItemchangedList)
ON_NOTIFY(NM_DBLCLK, IDC_LIST, &CMyDialog::OnNMDblclkList)
END_MESSAGE_MAP()
3.2 정렬과 검색
// 정렬 콜백
static int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
// lParam1, lParam2: 항목의 ItemData
// lParamSort: 정렬 기준 (컬럼 인덱스 등)
int age1 = (int)lParam1;
int age2 = (int)lParam2;
return age1 - age2; // 오름차순
}
afx_msg void OnBnClickedSort()
{
m_list.SortItems(CompareFunc, 0);
}
afx_msg void OnEnChangeSearch()
{
CString keyword;
GetDlgItemText(IDC_EDIT_SEARCH, keyword);
// 모든 항목 탐색
for (int i = 0; i < m_list.GetItemCount(); i++) {
CString text = m_list.GetItemText(i, 0);
if (text.Find(keyword) != -1) {
// 검색어 포함 → 선택
m_list.SetItemState(i, LVIS_SELECTED, LVIS_SELECTED);
m_list.EnsureVisible(i, FALSE);
break;
}
}
}
4. CTreeCtrl (트리뷰)
4.1 기본 사용법
class CMyDialog : public CDialog
{
public:
CTreeCtrl m_tree;
CImageList m_imageList;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
// 이미지 리스트 생성
m_imageList.Create(16, 16, ILC_COLOR32 | ILC_MASK, 0, 1);
// 아이콘 추가...
m_tree.SetImageList(&m_imageList, TVSIL_NORMAL);
// 루트 항목
HTREEITEM hRoot = m_tree.InsertItem(_T("내 컴퓨터"));
// 자식 항목
HTREEITEM hC = m_tree.InsertItem(_T("C: 드라이브"), hRoot);
HTREEITEM hD = m_tree.InsertItem(_T("D: 드라이브"), hRoot);
// C 드라이브 하위
m_tree.InsertItem(_T("Program Files"), hC);
m_tree.InsertItem(_T("Windows"), hC);
m_tree.InsertItem(_T("Users"), hC);
// 루트 확장
m_tree.Expand(hRoot, TVE_EXPAND);
return TRUE;
}
DECLARE_MESSAGE_MAP()
afx_msg void OnTvnSelchangedTree(NMHDR* pNMHDR, LRESULT* pResult)
{
LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);
HTREEITEM hItem = pNMTreeView->itemNew.hItem;
CString text = m_tree.GetItemText(hItem);
SetDlgItemText(IDC_STATIC_INFO, _T("선택: ") + text);
*pResult = 0;
}
afx_msg void OnNMDblclkTree(NMHDR* pNMHDR, LRESULT* pResult)
{
// 더블클릭 위치
CPoint pt;
GetCursorPos(&pt);
m_tree.ScreenToClient(&pt);
UINT flags;
HTREEITEM hItem = m_tree.HitTest(pt, &flags);
if (hItem != NULL) {
CString text = m_tree.GetItemText(hItem);
AfxMessageBox(_T("더블클릭: ") + text);
}
*pResult = 0;
}
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_NOTIFY(TVN_SELCHANGED, IDC_TREE, &CMyDialog::OnTvnSelchangedTree)
ON_NOTIFY(NM_DBLCLK, IDC_TREE, &CMyDialog::OnNMDblclkTree)
END_MESSAGE_MAP()
5. 프로그레스바와 슬라이더
5.1 CProgressCtrl
class CMyDialog : public CDialog
{
public:
CProgressCtrl m_progress;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
// 범위 설정
m_progress.SetRange(0, 100);
m_progress.SetPos(0);
return TRUE;
}
afx_msg void OnBnClickedStart()
{
// 타이머로 진행률 시뮬레이션
SetTimer(1, 100, NULL);
}
afx_msg void OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == 1) {
int pos = m_progress.GetPos();
if (pos >= 100) {
KillTimer(1);
AfxMessageBox(_T("완료!"));
m_progress.SetPos(0);
} else {
m_progress.SetPos(pos + 1);
CString text;
text.Format(_T("%d%%"), pos + 1);
SetDlgItemText(IDC_STATIC_PERCENT, text);
}
}
CDialog::OnTimer(nIDEvent);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_BN_CLICKED(IDC_BTN_START, &CMyDialog::OnBnClickedStart)
ON_WM_TIMER()
END_MESSAGE_MAP()
5.2 CSliderCtrl
class CMyDialog : public CDialog
{
public:
CSliderCtrl m_slider;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
// 범위 설정
m_slider.SetRange(0, 100);
m_slider.SetPos(50);
// 틱 표시
m_slider.SetTicFreq(10);
UpdateVolumeDisplay();
return TRUE;
}
void UpdateVolumeDisplay()
{
int pos = m_slider.GetPos();
CString text;
text.Format(_T("볼륨: %d"), pos);
SetDlgItemText(IDC_STATIC_VOLUME, text);
}
afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
if (pScrollBar == (CScrollBar*)&m_slider) {
UpdateVolumeDisplay();
}
CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_WM_HSCROLL()
END_MESSAGE_MAP()
6. 컨트롤 서브클래싱
6.1 숫자만 입력 가능한 에디트
// NumericEdit.h
class CNumericEdit : public CEdit
{
public:
DECLARE_MESSAGE_MAP()
afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags)
{
// 숫자, 백스페이스, 딜리트만 허용
if (isdigit(nChar) || nChar == VK_BACK || nChar == VK_DELETE) {
CEdit::OnChar(nChar, nRepCnt, nFlags);
} else {
// 다른 키는 무시 (삐 소리)
MessageBeep(MB_ICONHAND);
}
}
};
BEGIN_MESSAGE_MAP(CNumericEdit, CEdit)
ON_WM_CHAR()
END_MESSAGE_MAP()
// 사용
class CMyDialog : public CDialog
{
public:
CNumericEdit m_editAge; // CEdit 대신 CNumericEdit
protected:
virtual void DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT_AGE, m_editAge);
}
};
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- MFC 기초 | Microsoft Foundation Class 시작 가이드
- MFC 다이얼로그 | CDialog·DDX/DDV 완벽 가이드
- Windows API 컨트롤 | 버튼·에디트·리스트박스 완벽 가이드
이 글이 도움이 되셨나요? MFC 컨트롤 활용을 마스터하는 데 도움이 되었기를 바랍니다!
다음 글에서는 MFC GDI+ 그래픽 처리를 다루겠습니다. 🎨