1

I am attempting to create a "dark mode" for my Windows C++ app partially for fun, partially to try and fully comprehend the message passing in MFC, but I'm running into some really odd issues that I can't find explained anywhere.

I've spent the better part of today attempting to figure this out and will try my best to cite the many sources I've looked at and attempted to implement.

I believe I've successfully written message handlers for both WM_CTLCOLOR and WM_ERASEBKGND based on example code from this answer, but they don't seem to have any effect on my dialogs. I've cut the code down here, but am hoping that I've provided enough to expose my issue. If that still isn't enough, I can (reluctantly) share the entire repo.

SoftwareDlg.h

#ifndef _SOFTWAREDLG_H_INCLUDED_
#define _SOFTWAREDLG_H_INCLUDED_

#if _MSC_VER > 1000
#pragma once
#endif // _MSC_VER > 1000

class SoftwareDlg : public CDialog
{
// Construction
public:
    SoftwareDlg(CWnd* pParent = NULL);  // standard constructor

// Dialog Data
    //{{AFX_DATA(SoftwareDlg)
    enum { IDD = IDD_SOFTWARE_DIALOG };
    //}}AFX_DATA

    // ClassWizard generated virtual function overrides
    //{{AFX_VIRTUAL(SoftwareDlg)
    public:
    protected:
    virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
    //}}AFX_VIRTUAL

// Implementation
protected:
    BOOL                        PreTranslateMessage(MSG* pMsg);
    CFont                       m_font;
    CRichEditCtrl               m_richEditCtrl;

    // Generated message map functions
    //{{AFX_MSG(SoftwareDlg)
    virtual BOOL OnInitDialog();
    afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
    afx_msg void OnPaint();
    afx_msg HCURSOR OnQueryDragIcon();
    afx_msg void OnTimer(UINT nIDEvent);
    afx_msg void OnDestroy();
    //}}AFX_MSG
    DECLARE_MESSAGE_MAP()
public:
    afx_msg BOOL OnEraseBkgnd(CDC* pDC);
    afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
    afx_msg HBRUSH CtlColor(CDC* pDC, UINT nCtlColor);
};


/////////////////////////////////////////////////////////////////////////////
//{{AFX_INSERT_LOCATION}}
// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
/////////////////////////////////////////////////////////////////////////////
#endif
/////////////////////////////////////////////////////////////////////////////

SoftwareDlg.cpp

#include "stdafx.h"
#include <Windows.h>
#include "AboutDlg.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

//Windows Dialog inherited function overrides
/////////////////////////////////////////////////////////////////////////////
// SoftwareDlg dialog
/////////////////////////////////////////////////////////////////////////////
SoftwareDlg::SoftwareDlg(CWnd* pParent /*=NULL*/)
    : CDialog(SoftwareDlg::IDD, pParent)
{
    //{{AFX_DATA_INIT(SoftwareDlg)
    //}}AFX_DATA_INIT
    // Note that LoadIcon does not require a subsequent DestroyIcon in Win32

    m_hIcon                     = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void SoftwareDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialog::DoDataExchange(pDX);
    //{{AFX_DATA_MAP(SoftwareDlg)
    //}}AFX_DATA_MAP
}
BEGIN_MESSAGE_MAP(SoftwareDlg, CDialog)
    //{{AFX_MSG_MAP(SoftwareDlg)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_WM_TIMER()
    ON_WM_DESTROY()
    //}}AFX_MSG_MAP
    ON_WM_ERASEBKGND()
    ON_WM_CTLCOLOR()
END_MESSAGE_MAP()
/////////////////////////////////////////////////////////////////////////////
// SoftwareDlg message handlers
/////////////////////////////////////////////////////////////////////////////
BOOL SoftwareDlg::OnInitDialog()
{
    CDialog::OnInitDialog();


    // Add "About..." menu item to system menu.

    // IDM_ABOUTBOX must be in the system command range.
    ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
    ASSERT(IDM_ABOUTBOX < 0xF000);

    CMenu* pSysMenu = GetSystemMenu(FALSE);
    if (pSysMenu != NULL)
    {
        CString strAboutMenu;
        strAboutMenu.LoadString(IDS_ABOUTBOX);
        if (!strAboutMenu.IsEmpty())
        {
            pSysMenu->AppendMenu(MF_SEPARATOR);
            pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
        }
    }

    // Set the icon for this dialog.  The framework does this automatically
    //  when the application's main window is not a dialog
    SetIcon(m_hIcon, TRUE);         // Set big icon


    CWnd* pPlaceholder = GetDlgItem(IDC_PLACEHOLDER);

    if (pPlaceholder)
    {
        CRect rect;
        pPlaceholder->GetClientRect(&rect);

        if (!m_richEditCtrl.Create(WS_VISIBLE | ES_READONLY | ES_MULTILINE | ES_AUTOHSCROLL | WS_HSCROLL | ES_AUTOVSCROLL | WS_VSCROLL, rect, this, 0))
            return FALSE;

        m_font.CreateFont(-11, 0, 0, 0, FW_REGULAR, 0, 0, 0, BALTIC_CHARSET, 0, 0, 0, 0, "Courier New");
        m_richEditCtrl.SetFont(&m_font);
    }

    m_nTimerID = SetTimer(0x1234, 1000, NULL);  //Used by OnTimer function to refresh dialog box & OSD

    return TRUE;  // return TRUE  unless you set the focus to a control
}
void SoftwareDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
    if ((nID & 0xFFF0) == IDM_ABOUTBOX)
    {
        CAboutDlg dlgAbout;
        dlgAbout.DoModal();
    }
    else
    {
        CDialog::OnSysCommand(nID, lParam);
    }
}
/////////////////////////////////////////////////////////////////////////////
// If you add a minimize button to your dialog, you will need the code below
//  to draw the icon.  For MFC applications using the document/view model,
//  this is automatically done for you by the framework.
/////////////////////////////////////////////////////////////////////////////
void SoftwareDlg::OnPaint()
{
    if (IsIconic())
    {
        CPaintDC dc(this); // device context for painting

        SendMessage(WM_ICONERASEBKGND, (WPARAM)dc.GetSafeHdc(), 0);

        // Center icon in client rectangle
        int cxIcon = GetSystemMetrics(SM_CXICON);
        int cyIcon = GetSystemMetrics(SM_CYICON);
        CRect rect;
        GetClientRect(&rect);
        int x = (rect.Width() - cxIcon + 1) / 2;
        int y = (rect.Height() - cyIcon + 1) / 2;

        dc.DrawIcon(x, y, m_hIcon);
    }
    else
    {
        CDialog::OnPaint();
    }
}
HCURSOR SoftwareDlg::OnQueryDragIcon()
{
    return (HCURSOR)m_hIcon;
}
void SoftwareDlg::OnTimer(UINT nIDEvent)
{
    CDialog::OnTimer(nIDEvent);
}
void SoftwareDlg::OnDestroy()
{
    if (m_nTimerID)
        KillTimer(m_nTimerID);

    m_nTimerID = NULL;

    MSG msg;
    while (PeekMessage(&msg, m_hWnd, WM_TIMER, WM_TIMER, PM_REMOVE));

    CDialog::OnDestroy();
}
BOOL SoftwareDlg::PreTranslateMessage(MSG* pMsg)
{
    if (pMsg->message == WM_KEYDOWN)
    {
        switch (pMsg->wParam)
        {
        case ' ':
            Sleep(1000);
        }
    }

    return CDialog::PreTranslateMessage(pMsg);
}
BOOL SoftwareDlg::OnEraseBkgnd(CDC* pDC)
{
    CRect rect;
    GetClientRect(&rect);
    CBrush myBrush(RGB(255, 0, 0));    // dialog background color
    CBrush* pOld = pDC->SelectObject(&myBrush);
    BOOL bRes = pDC->PatBlt(0, 0, rect.Width(), rect.Height(), PATCOPY);
    pDC->SelectObject(pOld);    // restore old brush
    return bRes;                       // CDialog::OnEraseBkgnd(pDC);
}
HBRUSH SoftwareDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
    HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
    // Are we painting the IDC_MYSTATIC control? We can use
    m_brush.CreateSolidBrush(RGB(136, 217, 242));
    //if (pWnd->GetDlgCtrlID() == IDD_SOFTWARE_DIALOG)

    // Set the text color to red
    pDC->SetTextColor(RGB(255, 0, 0));

    // Set the background mode for text to transparent  so background will show thru.
    pDC->SetBkMode(TRANSPARENT);

    // Return handle to our CBrush object
    hbr = m_brush;

    return hbr;

}
HBRUSH SoftwareDlg::CtlColor(CDC* pDC, UINT nCtlColor)
{
    HBRUSH myBrush = CreateSolidBrush(RGB(136, 217, 242));

    return myBrush;
}

Resource.h

//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by Software.rc
//
#define IDM_ABOUTBOX                    0x0010
#define IDD_ABOUTBOX                    100
#define IDS_ABOUTBOX                    101
#define IDD_SOFTWARE_DIALOG      102

// Next default values for new objects
// 
#ifdef APSTUDIO_INVOKED
#ifndef APSTUDIO_READONLY_SYMBOLS
#define _APS_NEXT_RESOURCE_VALUE        141
#define _APS_NEXT_COMMAND_VALUE         32792
#define _APS_NEXT_CONTROL_VALUE         1026
#define _APS_NEXT_SYMED_VALUE           101
#endif
#endif

Another question, posted by the same user ~6 months earlier, was answered with somewhat similar code, but using the type of framework that contains a "WinMain" function(sorry, I can't differentiate the 2+ types yet). My program does not contain a WinMain function so I wasn't able to use the sample code directly... but another difference in this answer was that David was told to catch a WM_CTLCOLORDLG message type instead the WM_CTLCOLOR message type. I tried to catch this new message type, but IntelliSense told me it was undefined and that particular message property was completely absent from the resource view of the dialog box: WM_CTLCOLORDLG missing

I've also attempted defining "WM_CTLCOLORDLG" myself as described on the Microsoft Docs page, but continued to get error messages when I tried handling it through "ON_MESSAGE".

My code was not an original project, but was taken from an open source sample provided with RTSS. As such, it doesn't use the standard(?) "pch.h", but "stdafx.h"(which is older I guess?). I'm not certain if that's relevant, but I feel like it may be.

I think this issue may also be causing me a lot of other growing pains as well, so any help is GREATLY appreciated.

Skewjo
  • 379
  • 3
  • 12
  • 1
    "*written message handlers for ... WM_CTLCOLOR*" Make sure you got [`OnCtlColor`](https://learn.microsoft.com/en-us/cpp/mfc/reference/cwnd-class?view=msvc-160#onctlcolor) right, and that it was added into the message map. If it still doesn't work, please [edit](https://stackoverflow.com/posts/65176153/edit) the question and post the minimal code that reproduces the problem. – dxiv Dec 07 '20 at 04:52
  • Well, I'm fairly sure I've overridden the OnCtlColor function without issue, but I've finally had the bright idea of setting a breakpoint inside of that function... and execution doesn't seem to be reaching that point at all. – Skewjo Dec 07 '20 at 05:04
  • 1
    It's anybody's guess why if you don't post the actual code. – dxiv Dec 07 '20 at 05:48
  • Good point @dxiv. I was holding off on that for several reasons: 1. Hoping the problem would be immediately obvious to some lurking expert, but no such luck this time. 2. I was under the assumption that the issue wasn't necessarily my code, but some odd framework I'm using because I built this from someone else's initial project. 3. I was also afraid that cutting the code out would make me cut out a relevant piece of information, but hopefully that will not be the case. If that still isn't enough I will (reluctantly) post the entire code base. – Skewjo Dec 07 '20 at 12:10

2 Answers2

4

Simply use the CDialogEx class it supports CDialogEx::SetBackgroundColor and it does all the stuff for static and button controls.

xMRi
  • 14,982
  • 3
  • 26
  • 59
  • Thanks for the answer, but this didn't seem to resolve my issue either. I've changed the base class and all instances of ```CDialog``` to ```CDialogEx```, included the ```"afxdialogex.h"``` header file, and added the function ```CDialogEx::SetBackgroundColor((255, 0, 0), 1);``` to my ```OnInitDialog()``` function, but still get no color change. – Skewjo Dec 07 '20 at 12:54
  • Nevermind this does work! My zero margin placeholder that was holding a RichEditCtrl was blocking the background. Does CDialogEx allow me to change the border color and RichEditCtrl background as well? – Skewjo Dec 07 '20 at 13:02
  • Mark my answer as solution. – xMRi Dec 07 '20 at 13:24
  • Going to keep it open for a bit longer because I spent a lot of time putting this question together, and while this is a good work-around, I don't think it solved my original issue. – Skewjo Dec 07 '20 at 13:34
  • 1
    @Skewjo One less obvious takeout from this answer (+1) is that `CDialogEx` implements (most of) the functionality you are after. Even if you decide *not* to use `CDialogEx` in the end, you can still peek into the MFC sources and see how *they* do it. You'll notice that it's quite similar to your solution, with an added `CBrush CDialogEx::m_brBkgr;` member that gets used in `OnCtlColor` and `OnEraseBkgnd` etc. – dxiv Dec 07 '20 at 18:17
  • 1
    @Skewjo If you are implementing more than one dialog, then you're going to want to create a baseclass for all your dialogs and derive your dialogs from your base class. This allows you to have a centralized place where you can change or implement so that all the dialogs have the same behavior. Implement your special behavior once and inherit the behavior in your derived classes. – Joseph Willcoxson Dec 07 '20 at 21:06
  • @JosephWillcoxson, that's a very good point, thank you. I assumed (probably incorrectly) that all my other dialogs would automatically inherit from my main dialog. If you're following the discussion in the other thread, please feel free to have a crack at figuring out what I've messed up: https://github.com/Skewjo/SoftwareOnCtlColor I can't get my program to hit the ```OnCtlColor``` function. – Skewjo Dec 07 '20 at 21:14
4

Main problem with OP's code is that the brush is re-created every time in OnCtlColor and will keep leaking GDI handles (the debug build of MFC raises an ASSERT about this). Complete step-by-step fix below.

  • Declare the color and brush as members of the dialog.

    class SoftwareDlg : public CDialog
    {
    //...
    protected:
        COLORREF m_color;
        CBrush m_brush;
    //...
    
  • Initialize the color and brush in OnInitDialog.

    BOOL SoftwareDlg::OnInitDialog()
    {
        m_color = RGB(136, 217, 242);
        m_brush.CreateSolidBrush(m_color);
    
        CDialog::OnInitDialog();
    //...
    
  • Return the brush from OnCtlColor.

    HBRUSH SoftwareDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
    {
    //...
        return m_brush;
    }
    
  • Use the color in OnEraseBkgnd to repaint any visible area.

    afx_msg BOOL SoftwareDlg::OnEraseBkgnd(CDC* pDC)
    {
        CRect rc;
        pDC->GetClipBox(&rc);
        pDC->FillSolidRect(rc, m_color);
        return TRUE;
    }
    
  • Set the background color to the rich edit control in OnInitDialog explicitly, since rich edit controls do not use the WM_CTLCOLOR*** messages.

    BOOL SoftwareDlg::OnInitDialog()
    {
    //...
        if (pPlaceholder)
        {
        //...
    
            if (!m_richEditCtrl.Create(WS_VISIBLE | ES_READONLY | ES_MULTILINE | ES_AUTOHSCROLL | WS_HSCROLL | ES_AUTOVSCROLL | WS_VSCROLL, rect, this, 0))
                return FALSE;
    
            m_richEditCtrl.SetBackgroundColor(FALSE, m_color);
    //...
    

Note: if using the CDialogEx::SetBackgroundColor approach proposed in the other answer, the OnCtlColor and OnEraseBkgnd parts are covered by the CDialogEx implementation. The last step is still necessary because the WM_CTLCOLOR mechanism only covers the basic controls (static, edit, button etc) and the dialog itself. Controls other than those (rich edit control, list-view control etc) need to be handled each separately.

Andrew Truckle
  • 17,769
  • 16
  • 66
  • 164
dxiv
  • 16,984
  • 2
  • 27
  • 49
  • It's beautiful, thank you. Can I ask why you chose to override OnEraseBkgnd from ```CmfcDlg16Dlg``` though? I don't have seem to have class available with my current includes, but was able to get this working by overwriting in my class internally as I did with OnCtlColor. – Skewjo Dec 07 '20 at 17:33
  • 1
    @Skewjo That was a typo, thanks for catching. Fixed now. – dxiv Dec 07 '20 at 17:35
  • @dvix, sorry to keep bothering you, but I am still having an issue... ```OnEraseBkgnd``` appears to be getting called correctly, but ```OnCtlColor``` is still never called. I believe this is disallowing me from changing the font color, and following most Window manipulation tutorials. Is it possible that my ```PreTranslateMessage``` function is messing me up? I'm super lost as to why that function is NEVER getting hit. – Skewjo Dec 07 '20 at 18:25
  • @Skewjo `SoftwareDlg::OnCtlColor` certainly gets called. Make sure you don't confuse it with `CtlColor` which you have also defined, though that one is unused. – dxiv Dec 07 '20 at 18:42
  • I feel like this program is gaslighting me rofl. I have breakpoints set on both lines of the ```SoftwareDlg::OnCtlColor``` function and they are never hit. I'm going to make a copy of my program and rip EVERYTHING out and see if the issue continues. – Skewjo Dec 07 '20 at 20:03
  • @Skewjo If you create a new MFC dialog-based project in VS and make *just* the changes listed above then it will work as expected. Use that as a baseline to compare and see where the breaking difference is in your actual code. What you posted is not complete so it's hard to guess, though some parts sound a bit odd, for example having to resort to "*defining `WM_CTLCOLORDLG`*", or "*handling it through `ON_MESSAGE`*". – dxiv Dec 07 '20 at 20:17
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/225639/discussion-between-skewjo-and-dxiv). – Skewjo Dec 07 '20 at 21:16