To display precise numeric progress values directly on a standard MFC progress control, derive a new class from CProgressCtrl and intercept its paint routine. The default control lacks native support for overlaying formatted text, so custom GDI drawing is required within the overridden OnPaint handler.
Drawing onto the control ivnolves acquiring a device context manually to bypass default frame buffering, which improves redraw performance. The BeginPaint function initializes the painting state and returns a valid HDC paired with a PAINTSTRUCT. After rendering operations complete, EndPaint finalizes the update sequence. Calling the base class OnPaint should be avoided here to prevent double-drawing.
Below is a complete implementation structure. The custom control manages three distinct color properties for the filled area, empty space, and overlaid text.
// PercProgressCtrl.h
#pragma once
#include <afxcmn.h>
class CPercProgressCtrl : public CProgressCtrl
{
public:
CPercProgressCtrl() = default;
~CPercProgressCtrl() = default;
COLORREF m_crFill = RGB(45, 120, 200); // Active fill color
COLORREF m_crEmpty = RGB(240, 240, 240); // Unfilled background
COLORREF m_crLabel = RGB(255, 255, 255); // Percentage text color
protected:
afx_msg void OnPaint();
DECLARE_MESSAGE_MAP()
};
The corresponding implementation handles coordinate calculations, rectangle splitting, and text centering.
// PercProgressCtrl.cpp
#include "stdafx.h"
#include "PercProgressCtrl.h"
BEGIN_MESSAGE_MAP(CPercProgressCtrl, CProgressCtrl)
ON_WM_PAINT()
END_MESSAGE_MAP()
void CPercProgressCtrl::OnPaint()
{
PAINTSTRUCT ps;
HDC hdc = ::BeginPaint(GetSafeHwnd(), &ps);
CDC* pDC = CDC::FromHandle(hdc);
int currentPos = GetPos();
CString renderText;
renderText.Format(L"%d%%", currentPos);
CRect clientArea;
GetClientRect(clientArea);
SIZE textSize = pDC->GetTextExtent(renderText);
// Center text coordinates
int textX = (clientArea.Width() - textSize.cx) / 2;
int textY = (clientArea.Height() - textSize.cy) / 2;
pDC->SetBkMode(TRANSPARENT);
pDC->SetTextColor(m_crLabel);
// Calculate progress width ratio
int rangeMin, rangeMax;
GetRange(rangeMin, rangeMax);
double pixelsPerUnit = (double)clientArea.Width() / (rangeMax - rangeMin);
int fillWidth = static_cast<int>(currentPos * pixelsPerUnit);
CRect fillRect = clientArea;
fillRect.right = fillWidth + clientArea.left;
CRect emptyRect = clientArea;
emptyRect.left = fillWidth;
// Draw segments
pDC->FillRect(fillRect, &CBrush(m_crFill));
pDC->FillRect(emptyRect, &CBrush(m_crEmpty));
// Render overlay text
pDC->TextOutW(textX, textY, renderText);
delete pDC;
::EndPaint(GetSafeHwnd(), &ps);
}
Integrating this control into a dialog requires mapping the control resource to the derived class instance and initializing the progress bounds. A system timer can drive simulated updates.
// DemoDialog.cpp (OnInitDialog excerpt)
BOOL CDemoDialog::OnInitDialog()
{
CDialogEx::OnInitDialog();
m_ProgressCtrl.SetRange(0, 100);
m_ProgressCtrl.SetPos(0);
SetTimer(101, 500, nullptr); // Simulate task advancement
return TRUE;
}
Timer callbacks manage state transitions and trigger UI refreshes automatically.
void CDemoDialog::OnTimer(UINT_PTR nIDEvent)
{
if (nIDEvent == 101)
{
int nextVal = m_ProgressCtrl.GetPos() + 2;
if (nextVal > 100)
{
KillTimer(101);
m_ProgressCtrl.SetPos(100);
return;
}
m_ProgressCtrl.SetPos(nextVal);
}
CDialogEx::OnTimer(nIDEvent);
}
For scenarios avoiding custom control inheritance, percentage labels can be rendered externally using standard static controls paired with data exchange mappings. Bind a CString variable to a STATIC element via DDX_Text, then calculate the formatted string based on the progress bar's range and position. Invoking UpdateData(FALSE) forces the label to reflect the latest calculated value without modifying the control's internal painting pipeline.
This approach maintains visual synchronization through explicit state checks rather than overriding window messages. Both techniques ensure accurate numeric feedback during long-running asynchronous operations.