Scrolling Property Page in PocketPC 2002
Add a scrollbar to a property page when the SIP is displayed.
Pre-Introduction
I'm using Embedded VC 3.0, and am writing code for PocketPC 2002 as it is implemented on the Compaq Ipaq. Your mileage may vary.
Introduction
You know how it goes. You're screamin' along, coding like a crazed maniac (okay, so this rarely happens with CE), and all is right with the world. Then it happens. A sales nazi (or worse, an otherwise un-involved management type) sticks its head in the door and says something that will affect not only your day, but the schedule as well. In my case it went something like this:
The Problem
"Hey John, on the such-and-such dialog, when I call up the keyboard, it obscures the field I'm entering data for. Can you do something about it?"
Being the good-natured and even-tempered soul that I am, I happily reply "Why yes, you freakin' moron. I can drop what I'm doing right now and get right on it." So there I sat. Mulling it over. After a week of trial-and-error, I finally came up with something. It's still a bit quirky (quirks described at the end of this article), but it pretty much works the way I want it to, and I thought I'd share the wealth, such as it is.
How To Do It
If you haven't already, create a class derived from CPropertyPage
. This is essential if you expect to get anything done in a reasonable amount of time. After that, here's what we'll be doing in a nutshell:
- Creating the scrollbar on the fly. This prevents us from having to physically place an actual scrollbar on the desired property page template, and it lets us make the scrollbar thinner than the app studio allows.
- Override
OnInitDialog
. - Override
OnVScroll
. - Detect when the SIP is displayed and dismissed, and react appropriately.
We'll start with modifications to the base class header file. First, we need to define a resource ID for the scrollbar. In the Resource editor, I have the habit of assigning control IDs in the 1000-10000 range, and when I need something manually defined (like now), I place it in the 64000-64100 range. This avoids collisions with other resource IDs.
#define CE_DLGSCROLLBAR 64000
Next, we need to make sure, our class definition includes the following lines:
class CMyBasePropertyPage: public CPropertyPage { ... protected: // Generated message map functions //{{AFX_MSG(CPtcPropertyPage) virtual BOOL OnInitDialog(); afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); //}}AFX_MSG afx_msg LRESULT OnSettingChange(WPARAM wParam, LPARAM lParam); DECLARE_MESSAGE_MAP() // manually added for the scrollbar protected: CScrollBar m_ctrlScrollBar; // our hero CRect m_DlgClientRect; // the propertypage client rectangle CRect m_ScrollBarRect; // the scrollbar rectangle int m_nScrollPos; // the current scrollbar position int m_nCurHeight; // the height of the scrollbar area BOOL m_bNeedScrollBar; // Default=FALSE - used by derived classes // to tell this base class that // we'll need a scrollbar BOOL m_bAutoScroll; // Default=FALSE - set by a derived class // according to whatever criteria you // might happen to specify. void initScrollBarSupport(); // Call this function from derived // class to setup the scrollbar. ... };
Next, we need to add some code to the base class CPP file. First, we want to set some default values telling the base class that we don't need a scrollbar, and that it won't autoscroll. By the way, if m_bAutoScroll
is TRUE
, the propertypage will automatically be scrolled to the bottom of the page. The way I use this is described near the end of the article.
CMyBasePropertyPage::CMyBasePropertyPage() { m_bNeedScrollBar = FALSE; m_bAutoScroll = FALSE; }
Since we're going to be using the Create()
function to build the scrollbar, we need to call the scrollbar's DestroyWindow()
method in the destructor of the propertypage.
CMyBasePropertyPage::~CMyBasePropertyPage() { if (IsWindow(m_ctrlScrollBar)) { m_ctrlScrollBar.DestroyWindow(); } }
Now that we're done with the preparation, here's the meat of the code. You should have the following message handlers implemented. For the WM_SETTINGCHANGE
handler, you'll have to manually add that to your code because the Class Wizard doesn't know how to add it for you. Notice also that it lies outside the AFX_MSG_MAP
group.
BEGIN_MESSAGE_MAP(CPtcPropertyPage, CPropertyPage) //{{AFX_MSG_MAP(CPtcPropertyPage) ON_WM_VSCROLL() //}}AFX_MSG_MAP ON_MESSAGE(WM_SETTINGCHANGE, OnSettingChange) END_MESSAGE_MAP()
Now, let's add the function that initializes the scrollbar. Here, we simply create the scrollbar, set the scroll ranges and other scrollbar stuff, and then hide it.
//--------------------------------------------------------------------------- // If you want your property page to be able to use the scrollbar, call this // function from your derived class' OnInitDialog() function. //--------------------------------------------------------------------------- void CMyBasePropertyPage::initScrollBarSupport() { m_bNeedScrollBar = TRUE; GetClientRect(&m_DlgClientRect); // create the scrollbar - the initial size is up to you, // but when everything is said and done, you want it // to be as narrow as possible. The "142" is // how big the scrollbar would actually be if // you didn't dynamically size it. DWORD dwStyle = SBS_VERT | WS_VISIBLE | WS_CHILD | WS_CLIPSIBLINGS; m_ctrlScrollBar.Create(dwStype, CRect(0, 0, 10, 142), this, CE_DLGSCROLLBAR); m_ctrlScrollBar.GetWindowRect(&m_ScrollBarRect); m_ctrlScrollBar.MoveWindow(m_DlgClientRect.Width()-m_ScrollBarRect.Width(), 0, m_ScrollBarRect.Width(), m_ScrollBarRect.Height()); m_ctrlScrollBar.BringWindowToTop(); m_ctrlScrollBar.ShowScrollBar(); m_ctrlScrollBar.GetWindowRect(&m_ScrollBarRect); m_nScrollPos = 0; m_nCurHeight = m_ScrollBarRect.Height() + 30; SCROLLINFO si; si.cbSize = sizeof(SCROLLINFO); si.fMask = SIF_ALL; si.nMin = 0; si.nMax = m_DlgClientRect.Height(); si.nPage = (int)(m_DlgClientRect.Height()* 0.75); si.nPos = 0; m_ctrlScrollBar.SetScrollInfo(&si, TRUE); // we don't need it right away, so hide it from the user m_ctrlScrollBar.ShowWindow(SW_HIDE); }
If you want the SIP to go away when they change to a different property page, use Class Wizard to include a handler for OnKillActive
, and like so:
BOOL CPtcPropertyPage::OnKillActive() { SHSipPreference(m_hWnd, SIP_FORCEDOWN); return CPropertyPage::OnKillActive(); }
Next, we have to be able to handle the scrolling itself when the user clicks the scrollbar control. Since we're taking about such a small scroll range, I decided to combine the LINEUP (and DOWN with the PAGEUP (and DOWN). One item of note is that in order to get the NEW scroll position, you must also handle the SB_ENDSCROLL
message (this little tidbit is NOT mentioned anywhere on MSDN that I found).
Another interesting item is that despite information to the contrary on MSDN, calling ScrollWindowEx
with the SW_SCROLLCHILDREN
flag set does appear to work in PocketPC 2002. Admittedly, MSDN does say that it doesn't work in CE, which is, as we all know, not exactly the same as PocketPC 2002.
//------------------------------------------------------------------------- // Handle the WM_VSCROLL message //------------------------------------------------------------------------- void CMyBasePropertyPage::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { if (pScrollBar == &m_ctrlScrollBar) { int nDelta = 0; int nMaxPos = m_DlgClientRect.Height() - m_nCurHeight; switch (nSBCode) { case SB_LINEDOWN: case SB_PAGEDOWN: if (m_nScrollPos >= nMaxPos) { return; } nDelta = min(max(nMaxPos / 2, 30), nMaxPos - m_nScrollPos); break; case SB_LINEUP: case SB_PAGEUP: if (m_nScrollPos <= 0) { return; } nDelta = -min(max(nMaxPos / 2, 30), m_nScrollPos); break; case SB_THUMBTRACK: case SB_THUMBPOSITION: nDelta = (int)nPos - m_nScrollPos; break; case SB_BOTTOM: nDelta = 50; break; case SB_TOP: if (m_nScrollPos <= 0) { return; } nDelta = -m_nScrollPos; break;; case SB_ENDSCROLL: return; default: return; } m_nScrollPos += nDelta; pScrollBar->SetScrollPos(m_nScrollPos, TRUE); ScrollWindowEx(0, -nDelta, NULL, &m_DlgClientRect, NULL, NULL, SW_SCROLLCHILDREN); pScrollBar->MoveWindow(m_DlgClientRect.Width()-m_ScrollBarRect.Width(), 0, m_ScrollBarRect.Width(), m_ScrollBarRect.Height()); } }
Finally, we have to handle the WM_SETTINGCHANGE
message. When the SIP state changes, this message is sent to the system. This function merely reacts to the calling up and putting down of the SIP. When the SIP is displayed, the scrollbar is also displayed, and if necessary, the propertypage is scrolled to the bottom. When the SIP is dismissed, the scrollbar is hidden.
//------------------------------------------------------------------------ // We have to set up a custom handler for the // WM_SETTINGCHANGE message because // the CDialog class doesn't handle it by default. //------------------------------------------------------------------------ LRESULT CMyBasePropertyPage::OnSettingChange(WPARAM wParam, LPARAM lParam) { if (m_bNeedScrollBar) { SIPINFO si; memset(&si, 0, sizeof(si)); si.cbSize = sizeof(si); if (SHSipInfo(SPI_GETSIPINFO, 0, &si, 0)) { BOOL bShowScrollBar = ((si.fdwFlags & SIPF_ON) == SIPF_ON); if (!bShowScrollBar) { if (m_nScrollPos > 0) { OnVScroll(SB_TOP, 0, &m_ctrlScrollBar); } m_ctrlScrollBar.ShowWindow(SW_HIDE); } else { // During testing, I discovered that the // standard CE SIP is always the same // height, no matter which SIP mode you're using. // This means that if you're // truly interested in performance // (albeit a negligible gain), you can comment // out this code so that the height of the // scrollbar is always 142 pixels (as // originally set when you called Create on the scrollbar). CRect sipRect(si.rcSipRect); CRect clientRect; GetClientRect(&clientRect); m_ctrlScrollBar.GetWindowRect(&m_ScrollBarRect); m_ScrollBarRect.bottom = m_ScrollBarRect.top + clientRect.Height(); m_ctrlScrollBar.MoveWindow(m_DlgClientRect.Width()-m_ScrollBarRect.Width(), 0, m_ScrollBarRect.Width(), m_ScrollBarRect.Height()); // show the scrollbar m_ctrlScrollBar.ShowWindow(SW_SHOW); if (m_bAutoScroll) { OnVScroll(SB_BOTTOM, 0, &m_ctrlScrollBar); } } } } return 1L; }
Using From a Derived Class
Now that we have our base class, we can make use of the scrollbar in a derived class. The following code is needed to activate the scrollbar handling code in the base class:
CMyDerivedPage::OnInitDialog() { CMyBasePropertyPage::OnInitDialog(); initScrollBarSupport(); }
For my needs, I wanted to have the scrollbar automatically scroll when one of a particular group of controls was focused. To implement this functionality, I needed to be able to detect when ANY control on the page gained focus, so I added the following code to the derived class:
In the header file:
protected: // Generated message map functions //{{AFX_MSG(CMyDerivedPage) ... ... //}}AFX_MSG afx_msg void OnEnSetFocus(UINT nID);
In the CPP file:
BEGIN_MESSAGE_MAP(CMyDerivedPage, CMyBasePropertyPage) //{{AFX_MSG_MAP(CMyDerivedPage) ... ... //}}AFX_MSG_MAP ON_CONTROL_RANGE(EN_SETFOCUS, 0, 65535, OnEnSetFocus) END_MESSAGE_MAP() void CMyDerivedPage::OnEnSetFocus(UINT nID) { switch (nID) { case IDC_CONTROL1 : case IDC_CONTROL2 : case IDC_CONTROL3 : m_bAutoScroll = TRUE; break; default : m_bAutoScroll = FALSE; break; } }
Quirks
The only weirdness that I haven't figured out yet is when scrolling to the bottom of the page. If I use the tab to scroll, it doesn't quite go all the way to the bottom of the page. If I click on the down-arrow scroll button, it scrolls the page the rest of the way. If you'd like to take a stab at finding out why that happens, have a ball, but let me know. :)