Table of contents
Introduction
I have received so much help from The Code Project community. I felt a contribution from me is a way of saying thanks to all. Here's a modified version of the project to cater for FormView apps as well. Many thanks to Joe for pointing it out.
Visual Basic has the facility to handle Control Arrays.
The advantages of using them when you have multiple child windows of the same control are as follows:
- Less lines of code when you want to modify or inspect properties of the controls (i.e. loops).
- You wish to do (1. above) to some of them (i.e. the first or last five, every other or third).
- When you wish to know which slider or spin control was scrolled when Windows traps it.
- The ease to directly modify other controls by index (without using
CWnd::UpdateData()
).
- Pointer arithmetic and comparison becomes available (cutting the use of
CWnd::GetDlgItem()
).
As I said it can be done in VB and there's no reason why you can't in VC++. You can! It's just done differently. This tutorial shows an example of how to make use of it.
Getting started
Let's start at the most appropriate place, the beginning. Create an MFC EXE app. For simplicity I'll use a Dialog based one (if you select a single or multiple document, follow the AppWizard through to step 6 of 6 Base Class (lower left) and select CFormView
).
Having done that, we now have the resource editor.
We are in a position to place the controls on the dialog (or FormView).
The controls that we require for this demo are each of the following:
- Edit Box
- Spin Button control
- Slider control
Select them one by one and place them accordingly. Having placed them on the dialog, we have to set the sliders to have tick marks. So select the slider only and then right click the mouse. Then select "Properties".
On the Slider Properties select the "Styles" tab. Check both "Tick marks" and "Auto Ticks". Also for point, select "Top/Left". Then close. This is done so you don't have to edit the properties for the other two.
Then holding down the Shift key select the other two controls. Copy (CTRL-C) and paste (CTRL-V). Move the pasted controls somewhere else and repeat the paste. Align them so that they look presentable.
The completed dialog:
With that done, now we advance onto the trickier part of the task.
Class Wizard (Next step)
This part can prove the most problematic. Because the wizard has either lost its coned hat or the wand has failed to deal with control arrays. Hence we have to do our own sorcery to work around it.
Now let's call up the Class Wizard leaving the Message Map. We want to select the following messages:
We want to map messages to all the spin controls and edit controls. The sliders we'll deal with later.
Make sure that Tab is on Message Maps. The class name is CControlArrayDlg
. Select the message WM_VSCROLL
. This automatically calls the function OnVScroll
. This caters for all the vertical scrollers (spin button). Note FormView apps also select the WM_HSCROLL
. This automatically calls the function OnHScroll
to handle the sliders.
Select the control IDC_EDIT1
. Select the message EN_UPDATE
. Accept the suggested function name. Since EN
messages only work on per edit control, repeat this for IDC_EDIT2
and IDC_EDIT3
. Skip the edit change.
Select the Member Variable tab. Map variables to IDC_EDIT1
, IDC_SLIDER1
& IDC_SPIN1
only (see note 1). The others will be dealt with in the next section. Set the slider and edit control of the category control. This is the default for the spin control.
Variable List
Now we're done with the Class Wizard. Click OK.
Coding the project
Sub sections
- Message Mapping the dialog based sliders
- Declaring the control arrays, connecting the variables to the controls
- Coding the methods
Now onto the sexy part.
Message Mapping the sliders (For Dialog based apps ONLY)
Now firstly we'll map the sliders (see note 2) which requires quite a bit of coding. Select the Class View tab in the ControlArrayDlg
and select the "Add member function" on the popup menu. Right click the function return of type LRESULT
and declare it as OnHScrollSlider(WPARAM wParam, LPARAM lParam)
. Finally, make its accessibility protected
.
Visual description of how to add a function:
ControlArrayDemo1Dlg.cpp to find the text below:
BEGIN_MESSAGE_MAP(CControlArraysDemo1Dlg, CDialog)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_WM_VSCROLL()
ON_EN_CHANGE(IDC_EDIT1, OnChangeEdit1)
ON_EN_CHANGE(IDC_EDIT2, OnChangeEdit2)
ON_EN_CHANGE(IDC_EDIT3, OnChangeEdit3)
END_MESSAGE_MAP()
Between
and END_MESSAGE_MAP()
insert ON_MESSAGE(WM_HSCROLL, OnHScrollSlider)
to look like this:
BEGIN_MESSAGE_MAP(CControlArraysDemo1Dlg, CDialog)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_WM_VSCROLL()
ON_EN_CHANGE(IDC_EDIT1, OnChangeEdit1)
ON_EN_CHANGE(IDC_EDIT2, OnChangeEdit2)
ON_EN_CHANGE(IDC_EDIT3, OnChangeEdit3)
ON_MESSAGE(WM_HSCROLL, OnHScrollSlider)
END_MESSAGE_MAP()
afx_msg LRESULT OnHScrollSlider(WPARAM wParam, LPARAM lParam);
Now go to the file ControlArraysDemo1Dlg.h and find the text below:
protected:
LRESULT OnHScrollSlider(WPARAM wParam, LPARAM lParam);
HICON m_hIcon;
Either drag and drop or cut and paste LRESULT OnHScrollSlider(WPARAM wParam, LPARAM lParam);
between
and DECLARE_MESSAGE_MAP()
to look something like this:
protected:
HICON m_hIcon;
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar);
afx_msg void OnChangeEdit1();
afx_msg void OnChangeEdit2();
afx_msg void OnChangeEdit3();
LRESULT OnHScrollSlider(WPARAM wParam, LPARAM lParam);
DECLARE_MESSAGE_MAP()
Staying in the header file we're going to work on some of the properties.
Declaring the control arrays, connecting the variables to the controls
In ControlArraysDemo1Dlg.h or ControlArraysDemo2View.h locate the snippet of code below:
enum { IDD = IDD_CONTROLARRAYS_DIALOG };
CSpinButtonCtrl m_spin;
CSliderCtrl m_slider;
CEdit m_edit;
Declare m_spin
as an array of type CSpinButtonCtrl
, m_slider
as an array of type CSliderCtrl
and m_edit
as an array of type CEdit
each having 3 elements, so that it looks like this:
enum { IDD = IDD_CONTROLARRAYS_DIALOG };
CSpinButtonCtrl m_spin[3];
CSliderCtrl m_slider[3];
CEdit m_edit[3];
protected:
virtual void DoDataExchange(CDataExchange* pDX);
That finished, we can get down to the "serious" coding.
Last part, coding the methods
Table of how the variables are connected to the controls:
Control ID |
Connecting variable |
IDC_EDIT1 |
m_edit[0] |
IDC_EDIT2 |
m_edit[1] |
IDC_EDIT3 |
m_edit[2] |
IDC_SLIDER1 |
m_slider[0] |
IDC_SLIDER2 |
m_slider[1] |
3IDC_SLIDER1 |
m_slider[2] |
IDC_SPIN1 |
m_spin[0] |
IDC_SPIN2 |
m_spin[1] |
IDC_SPIN3 |
m_spin[2] |
Let's go to the file ControlArrayDlg.cpp and go to the function DoDataExchange
using the table above. Modify the code below:
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_SPIN1, m_spin);
DDX_Control(pDX, IDC_SLIDER1, m_slider);
DDX_Control(pDX, IDC_EDIT1, m_edit);
to
CDialog::DoDataExchange(pDX);
DDX_Control(pDX, IDC_EDIT1, m_edit[0]);
DDX_Control(pDX, IDC_EDIT2, m_edit[1]);
DDX_Control(pDX, IDC_EDIT3, m_edit[2]);
DDX_Control(pDX, IDC_SLIDER1, m_slider[0]);
DDX_Control(pDX, IDC_SLIDER2, m_slider[1]);
DDX_Control(pDX, IDC_SLIDER3, m_slider[2]);
DDX_Control(pDX, IDC_SPIN1, m_spin[0]);
DDX_Control(pDX, IDC_SPIN2, m_spin[1]);
DDX_Control(pDX, IDC_SPIN3, m_spin[2]);
The coding now starts:
Create a protected function declared as: BOOL StringToNumber(LPCTSTR szText, int &nValue)
to convert the CString
to a number below the code:
BOOL CControlArraysDemo1Dlg::StringToNumber(LPCTSTR pszText, int &nValue)
{
if(!pszText)
return FALSE;
while(*pszText)
{
nValue *= 10;
if((*pszText < 48)||( *pszText > 57))
{
nValue = 0;
return FALSE;
}
nValue += *pszText - 48;
pszText++;
}
return TRUE;
}
Code for handling the spin controls:
void CControlArraysDemo1Dlg::OnVScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar)
{
CSpinButtonCtrl *pSpin = reinterpret_cast<CSPINBUTTONCTRL *>(pScrollBar);
int nIndex = pSpin - &m_spin[0];
CString szValue;
szValue.Format("%d",nPos);
m_bChangedByCtrl = TRUE;
m_edit[nIndex].SetWindowText(szValue);
m_slider[nIndex].SetPos(nPos);
CDialog::OnVScroll(nSBCode, nPos, pScrollBar);
}
The work around for handling the slider control (Dialog based apps only):
LRESULT CControlArraysDemo1Dlg::OnHScrollSlider(WPARAM wParam, LPARAM lParam)
{
HWND hwnd;
hwnd = (HWND)lParam;
CString szValue;
int nIndex;
int nPos;
if(m_slider[0].m_hWnd == hwnd)
{
nIndex = 0;
}
else if(m_slider[1].m_hWnd == hwnd)
{
nIndex = 1;
}
else
{
nIndex = 2;
}
nPos = m_slider[nIndex].GetPos();
szValue.Format("%d",nPos);
m_edit[nIndex].SetWindowText(szValue);
m_spin[nIndex].SetPos(nPos);
return 0;
}
Alternatively in the FormView (CControlArrayDemo2View
):
void CControlArraysDemo2View::OnHScroll(UINT nSBCode,
UINT nPos, CScrollBar* pScrollBar)
{
if(pScrollBar)
{
CSliderCtrl *pSlider = reinterpret_cast<CSLIDERCTRL *>(pScrollBar);
int nIndex = pSlider - &m_slider[0];
int nValue = m_slider[nIndex].GetPos();
CString szValue;
szValue.Format("%d",nValue);
m_spin[nIndex].SetPos(nValue);
m_edit[nIndex].SetWindowText(szValue);
}
else
{
}
CFormView::OnHScroll(nSBCode, nPos, pScrollBar);
}
Handling the updates to IDC_EDIT1
:
void CControlArraysDemo1Dlg::OnUpdateEdit1()
{
if(GetFocus() == &m_edit[0])
{
CString szText;
int nValue = 0;
m_edit[0].GetWindowText(szText);
StringToNumber(szText,nValue);
m_slider[0].SetPos(nValue);
m_spin[0].SetPos(nValue);
}
}
Code for OnUpdateEdit2 (IDC_EDIT2)
and OnUpdateEdit3 (IDC_EDIT3)
are very similar. The differences are in the indexes of the other controls.
Finally code that does the intialising which is called from OnInitDialog
:
void CControlArraysDemo1Dlg::Initialisation()
{
int nCounter;
for(nCounter = 0; nCounter < 3; nCounter++)
{
m_edit[nCounter].ModifyStyle(0,ES_NUMBER);
m_edit[nCounter].SetWindowText(_T("0"));
m_edit[nCounter].SetLimitText(2);
m_slider[nCounter].SetRange(0,99);
m_slider[nCounter].SetTicFreq(10);
m_slider[nCounter].SetPos(0);
m_spin[nCounter].SetRange(0,99);
m_spin[nCounter].SetPos(0);
}
}
Hey presto! There you have it, a working dialog based app that uses control arrays on the dialogue.
Points of note
Here are the problems that you will encounter when using control arrays:
- When Class Wizard is called it issues the message: Parsing error, ";". Input line CSpinButtonCtrl m_spin[3] (in this case). A possible workaround is to substitute [for a double-underscore and] for the gobbledegook characters using Replace all, commenting out some of the code.
- I don't know the reason behind it, is it the call to parent class the culprit? The message
WM_HSCROLL
with slides tend to reset the control. Although, it's being slid to a different position.
Conclusion
I don't know if you agree with me. I find arrays can make coding a heck of a lot easier. Although this demo only uses three controls, this can be extended to other controls. You may see the reasons I stated in the introduction.
History
- Version 1. (Feb 2005) Original app.
- Version 2. (August 2005) Bug fix, problems running the code on a FormView dealt with.
EN_UPDATE
is used as opposed to EN_CHANGE
.