Creating transparent CStatic control in MFC
For a side project, I am working on in my spare time since college, I needed to create a splash screen. Finally. Eleven years after version 1.0 and we are thinking splash screen. Anyway, the splash screen must contain a custom image as a background, a progress bar to display how the initialization is progressing and something that will show every step of execution. Steps completed must be stacked on top of current step. Needless to say, application is written entirely in C++ using MFC framework and said splash screen, naturally, must be part of the application and not a separate project.
I started by creating a simple dialog and put on progress bar and two static controls. If you built all that, it looked weird as MFC does not reposition controls relative to the dialog size but keeps them at static positions. So, my first task was to reposition every control to desired place. You want that to happen at exact moment that dialog window is resized or your users will actually see controls moving from one place to another. To do this, you need to handle WM_ON_SIZE message.
void CSplashDialog::OnSize(UINT nType, int cx, int cy) { UNREFERENCED_PARAMETER(nType); UNREFERENCED_PARAMETER(cx); UNREFERENCED_PARAMETER(cy); this->GetWindowRect(m_rectWindow); this->SetBackground(); this->SetProgresBarAndLabel(); CDialog::OnSize(nType, cx, cy); }
First, all that UNREFERENCED_PARAMETER stuff is just to notify compiler that we are not using specified variable. Method GetWindowRect returns rectangle that represents entire dialog window. We are calling SetBackground method and after that SetProgresBarAndLabel method. For dialog to reposition properly, we need to call OnSize method of base CDialog class.
void CSplashDialog::SetBackground() { CDC MemDC; CBitmap bmp; CPaintDC dc(this); CRect rct; this->GetClientRect(&rct); MemDC.CreateCompatibleDC(&dc); m_imgOzadje.Load(_T("res\\splash_logo.png")); m_sizeBackground.cx = m_imgBackground.GetWidth(); m_sizeBackground.cy = m_imgBackground.GetHeight(); this->CalculateOffset(); bmp.Attach(m_imgBackground.Detach()); MemDC.SelectObject(&bmp); dc.BitBlt(m_sizeOffset.cx, m_sizeOffset.cy, rct.Width(), rct.Height(), &MemDC, 0, 0, SRCCOPY); } void CSplashDialog::CalculateOffset() { m_sizeOffset.cx = (m_rectWindow.Width() - m_sizeBackground.cx) / 2; m_sizeOffset.cy = (m_rectWindow.Height() - m_sizeBackground.cy) / 2; }
SetBackground method is simple. First, we need to get rectangle that represents current window. We then load PNG image. As we want to set image to the center of the screen we must call CalculateOffset method that calculates needed offset left and top of the image. Next we detach PNG image into CBitmap class and paint it on dialog window.
With Background done, we are now seeing difficulties. CStatic controls have gray background and CProgressCtrl looks like something that was made in 90s. But first, we still need to move all controls to desire window position relative to background image. This is where SetProgressBarAndLabel method steps in.
void CSplashDialog::SetProgresBarAndLabel() { CProgressCtrl* pCtrlProgress = (CProgressCtrl*)GetDlgItem(IDC_PROGRESS1); pCtrlProgress->SetRange(0, 100); pCtrlProgress->MoveWindow(m_sizeOffset.cx + 35, m_sizeOffset.cy + 740, 954, 15, FALSE); pCtrlProgress->RedrawWindow(); m_staticStep.MoveWindow(m_sizeOffset.cx + 35, m_sizeOffset.cy + 535, 200, 200, FALSE); m_staticStep.SetWindowTextW(_T("")); m_staticStepStatus.SetTextColor(RGB(0, 120, 0)); m_staticStepStatus.MoveWindow(m_sizeOffset.cx+235, m_sizeOffset.cy + 535, 50, 200, FALSE); m_staticStepStatus.SetWindowTextW(_T("")); }
Progress bar control gets sorted first, so we obtain it from the dialog using GetDlgItem, control’s ID and some casting. Next, we set progress bar range. Range can be totally custom set. So, if you like, you can set it from 3 to 29 for instance. Zero to 100 makes it easier to understand, so I am using that one. We position the window using MoveWindow with last parameter set to FALSE. If you set last parameter to TRUE, you will be left with nice gray rectangle at control’s original position. You need to RedrawWindow, so that progress bar control appears on the screen at get go.
Next, we move both static controls. Nothing fancy there. You may have noticed you cannot use SetTextColor method in your application. This method is overriden in CTransparentStatic class (more about that later), for the purpose of setting different than default color to CStatic control. I want status to have shade of green that can be seen on the background, so I set it to RGB(0, 120, 0).
Still, this only positioned controls. Progress bar still looks like it is from 90s and CStatic control still has that lovely gray background. I want my Progress bar to look much like it does in Windows 7 and according to World-wide web there is only one way to go about that. You must insert code below into stdafx.h file.
#ifdef _UNICODE #if defined _M_IX86 #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"") #elif defined _M_IA64 #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='ia64' publicKeyToken='6595b64144ccf1df' language='*'\"") #elif defined _M_X64 #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='amd64' publicKeyToken='6595b64144ccf1df' language='*'\"") #else #pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"") #endif #endif
This, however causes a bit of a glitch. You see, normal progress bar works from 0 value onward. This is not true for Windows 7 style progress bar. If you want your progress bar to work properly, you have to start with value higher than 0 and end at value higher than 100. It is a known glitch and it could be it is fixed in newer MFC versions. In this one, it most certainly is not.
So, with progress bar looking a tad more 2010-ish, we are ready to sort out that pesky CStatic control background. To do this, we need to create our own class that inherits from CStatic and make dialog believe we are using CTransparentStatic control in dialog and not plain old CStatic. This class will be called CTransparentStatic, due to it’s main feature. Transparent background. In dialog class, we must override DoDataExchange method and do the following:
void CSplashDialog::DoDataExchange(CDataExchange* pDX) { CDialog::DoDataExchange(pDX); DDX_Control(pDX, IDC_PROGRESSTEXT, m_staticStep); DDX_Control(pDX, IDC_PROGRESSTEXTSTATUS, m_staticStepStatus); }
By doing this, we tell to our dialog that controls with IDs IDC_PROGRESSTEXT and IDC_PROGRESSTEXTSTATUS will use sepcified member variables which are of type CTransparentStatic. This leads us on the right path, but we did nothing yet. To create transparent background, we must handle three messages sent to our control: WM_ERASEBKGND, WM_CTLCOLOR_REFLECT and WM_SETTEXT.
Method OnEraseBkgnd is used to handle WM_ERASEBKGND message and is triggered when background of control needs to be erased due to a redraw.
BOOL CTransparentStatic::OnEraseBkgnd(CDC* pDC) { if (m_Bmp.GetSafeHandle() == NULL) { CRect Rect; GetWindowRect(&Rect); CWnd *pParent = GetParent(); pParent->ScreenToClient(&Rect); CDC *pDC = pParent->GetDC(); CDC MemDC; MemDC.CreateCompatibleDC(pDC); m_Bmp.CreateCompatibleBitmap(pDC, Rect.Width(), Rect.Height()); CBitmap *pOldBmp = MemDC.SelectObject(&m_Bmp); MemDC.BitBlt(0, 0, Rect.Width(), Rect.Height(), pDC, Rect.left, Rect.top, SRCCOPY); MemDC.SelectObject(pOldBmp); pParent->ReleaseDC(pDC); } else { CRect Rect; GetClientRect(Rect); CDC MemDC; MemDC.CreateCompatibleDC(pDC); CBitmap *pOldBmp = MemDC.SelectObject(&m_Bmp); pDC->BitBlt(0, 0, Rect.Width(), Rect.Height(), &MemDC, 0, 0, SRCCOPY); MemDC.SelectObject(pOldBmp); } return TRUE; }
We handle things differently the first time around and every next time, as the first time around, we must get background of our parent windows e.g. dialog and we store it into member CBitmap object m_Bmp. Everything else is the same. We load Bitmap and paint background with the exact part of dialog background where our control is positioned. This will result in smooth image.
Next, we handle message WM_CTLCOLOR_REFLECT in method CtlColor. This method setsĀ background to transparent value and uses empty brush for paint.
HBRUSH CTransparentStatic::CtlColor(CDC* pDC, UINT nCtlColor) { UNREFERENCED_PARAMETER(nCtlColor); pDC->SetBkMode(TRANSPARENT); return (HBRUSH)GetStockObject(NULL_BRUSH); }
Final magic happens in OnSetText method that is used to handle WM_SETTEXT message, where we invalidate our control, update it and redraw the entire thing.
LRESULT CTransparentStatic::OnSetText(WPARAM wParam,LPARAM lParam) { UNREFERENCED_PARAMETER(wParam); UNREFERENCED_PARAMETER(lParam); LRESULT Result = Default(); Invalidate(); UpdateWindow(); RedrawWindow(); return Result; }
Nice. Now we have a transparent CStatic control. This is all you need to do, if you are thinking of putting only one line of text in your static control. However, when we print multiple lines to our static control, we notice that result gets re-printed each and every time at different position, which leads to smudged, unreadable and ugly result.
To fix this, you need to handle another message WM_PAINT. This is done in OnPaint method.
void CTransparentStatic::OnPaint() { CPaintDC dc(this); CRect rect; GetClientRect(rect); dc.SelectObject(GetParent()->GetFont()); dc.SetBkMode(TRANSPARENT); dc.SetTextColor(m_colorBesedilo); CString strOutput; GetWindowText(strOutput); CRect rectText(rect); rectText.bottom = 0; dc.DrawText(strOutput, strOutput.GetLength(), rectText, DT_WORDBREAK | DT_CALCRECT); rectText.OffsetRect(0, rect.Height() - rectText.Height()); dc.DrawText(strOutput, strOutput.GetLength(), rectText, DT_WORDBREAK); }
Idea here is to obtain text rectangle from control before it is printed on screen (DrawText method with DT_CALCRECT flag set). Then we offset text to desired position and draw text back to our control. We must avoid calling CStatic::OnPaint method here, as that would corrupt our attempts to reposition our text. This done, we now have exactly what we wanted. A multiline transparent CStatic control which fills text from bottom to top.
Figuring out this solution took me quite few hours of what would be my spare time. I hope it helps any of you out there still coding in C++ and using MFC. Dialog class code and CTransparentStatic class code can be found at GitHub.
Leave a Reply