// grepWin - regex search and replace for Windows // Copyright (C) 2020-2021 - Stefan Kueng // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License // as published by the Free Software Foundation; either version 2 // of the License, or (at your option) any later version. // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software Foundation, // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. // #include "stdafx.h" #include "Theme.h" #include "SimpleIni.h" #include "PathUtils.h" #include "StringUtils.h" #include "DarkModeHelper.h" #include "DPIAware.h" #include "SmartHandle.h" #include #include #include #include #pragma warning(push) #pragma warning(disable : 4458) // declaration of 'xxx' hides class member #include #pragma warning(pop) constexpr COLORREF darkBkColor = 0x202020; constexpr COLORREF darkTextColor = 0xDDDDDD; constexpr COLORREF darkDisabledTextColor = 0x808080; constexpr auto SubclassID = 1234; static int GetStateFromBtnState(LONG_PTR dwStyle, BOOL bHot, BOOL bFocus, LRESULT dwCheckState, int iPartId, BOOL bHasMouseCapture); static void GetRoundRectPath(Gdiplus::GraphicsPath* pPath, const Gdiplus::Rect& r, int dia); static void DrawRect(LPRECT prc, HDC hdcPaint, Gdiplus::DashStyle dashStyle, Gdiplus::Color clr, Gdiplus::REAL width); static void DrawFocusRect(LPRECT prcFocus, HDC hdcPaint); static void PaintControl(HWND hWnd, HDC hdc, RECT* prc, bool bDrawBorder); static BOOL DetermineGlowSize(int* piSize, LPCWSTR pszClassIdList = nullptr); static BOOL GetEditBorderColor(HWND hWnd, COLORREF* pClr); HBRUSH CTheme::m_sBackBrush = nullptr; CTheme::CTheme() : m_bLoaded(false) , m_dark(false) , m_isHighContrastMode(false) , m_isHighContrastModeDark(false) , m_bDarkModeIsAllowed(false) , m_lastThemeChangeCallbackId(0) , m_regDarkTheme(L"Software\\grepWin\\DarkMode", 0) { } CTheme::~CTheme() { if (m_sBackBrush) DeleteObject(m_sBackBrush); } CTheme& CTheme::Instance() { static CTheme instance; if (!instance.m_bLoaded) instance.Load(); return instance; } void CTheme::Load() { IsDarkModeAllowed(); OnSysColorChanged(); auto setDarkMode = bPortable ? g_iniFile.GetBoolValue(L"global", L"darkmode", false) : !!m_regDarkTheme; m_dark = setDarkMode && IsDarkModeAllowed() && !IsHighContrastMode() && DarkModeHelper::Instance().ShouldAppsUseDarkMode(); m_bLoaded = true; } bool CTheme::IsHighContrastMode() const { return m_isHighContrastMode; } bool CTheme::IsHighContrastModeDark() const { return m_isHighContrastModeDark; } COLORREF CTheme::GetThemeColor(COLORREF clr, bool fixed /*= false*/) const { if (m_dark || (fixed && m_isHighContrastModeDark)) { float h, s, l; CTheme::RGBtoHSL(clr, h, s, l); l = 100.0f - l; if (!m_isHighContrastModeDark) { // to avoid too much contrast, prevent // too dark and too bright colors. // this is because in dark mode, contrast is // much more visible. l = std::clamp(l, 5.0f, 90.0f); } return CTheme::HSLtoRGB(h, s, l); } return clr; } int CTheme::RegisterThemeChangeCallback(ThemeChangeCallback&& cb) { ++m_lastThemeChangeCallbackId; int nextThemeCallBackId = m_lastThemeChangeCallbackId; m_themeChangeCallbacks.emplace(nextThemeCallBackId, std::move(cb)); return nextThemeCallBackId; } bool CTheme::RemoveRegisteredCallback(int id) { auto foundIt = m_themeChangeCallbacks.find(id); if (foundIt != m_themeChangeCallbacks.end()) { m_themeChangeCallbacks.erase(foundIt); return true; } return false; } void CTheme::SetDlgFontFaceName(LPCWSTR FontFaceName, int size) { (void)lstrcpyn(m_DlgFontFace, FontFaceName, _countof(m_DlgFontFace)); m_DlgFontSize = size; } LPCWSTR CTheme::GetDlgFontFaceName() { return &m_DlgFontFace[0]; } int CTheme::GetDlgFontSize() { return m_DlgFontSize; } void CTheme::SetFontForDialog(HWND hwnd, LPCWSTR FontFaceName, int FontSize) { LOGFONT lf = {0}; GetObject(GetWindowFont(hwnd), sizeof(lf), &lf); lf.lfWeight = FW_REGULAR; lf.lfHeight = (LONG)FontSize; (void)lstrcpyn(lf.lfFaceName, FontFaceName, _countof(lf.lfFaceName)); HFONT const hf = CreateFontIndirect(&lf); HDC const hdc = GetDC(hwnd); SetBkMode(hdc, OPAQUE); SendMessage(hwnd, WM_SETFONT, (WPARAM)hf, TRUE); ReleaseDC(hwnd, hdc); } bool CTheme::SetThemeForDialog(HWND hWnd, bool bDark) { if (IsDarkModeAllowed()) { DarkModeHelper::Instance().AllowDarkModeForWindow(hWnd, bDark); if (bDark) { SetWindowSubclass(hWnd, MainSubclassProc, SubclassID, reinterpret_cast(&m_sBackBrush)); } else { RemoveWindowSubclass(hWnd, MainSubclassProc, SubclassID); } EnumChildWindows(hWnd, AdjustThemeForChildrenProc, bDark ? TRUE : FALSE); EnumThreadWindows(GetCurrentThreadId(), AdjustThemeForChildrenProc, bDark ? TRUE : FALSE); ::RedrawWindow(hWnd, nullptr, nullptr, RDW_FRAME | RDW_INVALIDATE | RDW_ERASE | RDW_INTERNALPAINT | RDW_ALLCHILDREN | RDW_UPDATENOW); } DarkModeHelper::Instance().RefreshTitleBarThemeColor(hWnd, bDark); return true; } BOOL CTheme::AdjustThemeForChildrenProc(HWND hwnd, LPARAM lParam) { DarkModeHelper::Instance().AllowDarkModeForWindow(hwnd, static_cast(lParam)); wchar_t szWndClassName[MAX_PATH] = {0}; GetClassName(hwnd, szWndClassName, _countof(szWndClassName)); if (lParam) { if ((wcscmp(szWndClassName, WC_LISTVIEW) == 0) || (wcscmp(szWndClassName, WC_LISTBOX) == 0)) { // theme "Explorer" also gets the scrollbars with dark mode, but the hover color // is the blueish from the bright mode. // theme "ItemsView" has the hover color the same as the windows explorer (grayish), // but then the scrollbars are not drawn for dark mode. // theme "DarkMode_Explorer" doesn't paint a hover color at all. // // Also, the group headers are not affected in dark mode and therefore the group texts are // hardly visible. // // so use "Explorer" for now. The downside of the bluish hover color isn't that bad, // except in situations where both a treeview and a listview are on the same dialog // at the same time (e.g. repobrowser) - then the difference is unfortunately very // noticeable... SetWindowTheme(hwnd, L"Explorer", nullptr); auto header = ListView_GetHeader(hwnd); DarkModeHelper::Instance().AllowDarkModeForWindow(header, static_cast(lParam)); SetWindowTheme(header, L"Explorer", nullptr); ListView_SetTextColor(hwnd, darkTextColor); ListView_SetTextBkColor(hwnd, darkBkColor); ListView_SetBkColor(hwnd, darkBkColor); auto hTT = ListView_GetToolTips(hwnd); if (hTT) { DarkModeHelper::Instance().AllowDarkModeForWindow(hTT, static_cast(lParam)); SetWindowTheme(hTT, L"Explorer", nullptr); } SetWindowSubclass(hwnd, ListViewSubclassProc, SubclassID, reinterpret_cast(&m_sBackBrush)); } else if (wcscmp(szWndClassName, WC_HEADER) == 0) { SetWindowTheme(hwnd, L"ItemsView", nullptr); } else if (wcscmp(szWndClassName, WC_BUTTON) == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); auto style = GetWindowLongPtr(hwnd, GWL_STYLE) & 0x0F; if ((style & BS_GROUPBOX) == BS_GROUPBOX) { SetWindowSubclass(hwnd, ButtonSubclassProc, SubclassID, reinterpret_cast(&m_sBackBrush)); } else if (style == BS_CHECKBOX || style == BS_AUTOCHECKBOX || style == BS_3STATE || style == BS_AUTO3STATE || style == BS_RADIOBUTTON || style == BS_AUTORADIOBUTTON) { SetWindowSubclass(hwnd, ButtonSubclassProc, SubclassID, reinterpret_cast(&m_sBackBrush)); } } else if (wcscmp(szWndClassName, WC_STATIC) == 0) { SetWindowTheme(hwnd, L"", L""); } else if (wcscmp(szWndClassName, L"SysDateTimePick32") == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); } else if ((wcscmp(szWndClassName, WC_COMBOBOXEX) == 0) || (wcscmp(szWndClassName, L"ComboLBox") == 0) || (wcscmp(szWndClassName, WC_COMBOBOX) == 0)) { SetWindowTheme(hwnd, L"Explorer", nullptr); HWND hCombo = hwnd; if (wcscmp(szWndClassName, WC_COMBOBOXEX) == 0) { SendMessage(hwnd, CBEM_SETWINDOWTHEME, 0, reinterpret_cast(L"Explorer")); hCombo = reinterpret_cast(SendMessage(hwnd, CBEM_GETCOMBOCONTROL, 0, 0)); } if (hCombo) { SetWindowSubclass(hCombo, ComboBoxSubclassProc, SubclassID, reinterpret_cast(&m_sBackBrush)); COMBOBOXINFO info = {0}; info.cbSize = sizeof(COMBOBOXINFO); if (SendMessage(hCombo, CB_GETCOMBOBOXINFO, 0, reinterpret_cast(&info))) { DarkModeHelper::Instance().AllowDarkModeForWindow(info.hwndList, static_cast(lParam)); DarkModeHelper::Instance().AllowDarkModeForWindow(info.hwndItem, static_cast(lParam)); DarkModeHelper::Instance().AllowDarkModeForWindow(info.hwndCombo, static_cast(lParam)); SetWindowTheme(info.hwndList, L"Explorer", nullptr); SetWindowTheme(info.hwndItem, L"Explorer", nullptr); SetWindowTheme(info.hwndCombo, L"CFD", nullptr); } } } else if (wcscmp(szWndClassName, WC_TREEVIEW) == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); TreeView_SetTextColor(hwnd, darkTextColor); TreeView_SetBkColor(hwnd, darkBkColor); auto hTT = TreeView_GetToolTips(hwnd); if (hTT) { DarkModeHelper::Instance().AllowDarkModeForWindow(hTT, static_cast(lParam)); SetWindowTheme(hTT, L"Explorer", nullptr); } } else if (_wcsnicmp(szWndClassName, L"RICHEDIT", 8) == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); CHARFORMAT2 format = {0}; format.cbSize = sizeof(CHARFORMAT2); format.dwMask = CFM_COLOR | CFM_BACKCOLOR; format.crTextColor = darkTextColor; format.crBackColor = darkBkColor; SendMessage(hwnd, EM_SETCHARFORMAT, SCF_ALL, reinterpret_cast(&format)); SendMessage(hwnd, EM_SETBKGNDCOLOR, 0, static_cast(format.crBackColor)); } else if (wcscmp(szWndClassName, PROGRESS_CLASS) == 0) { SetWindowTheme(hwnd, L"", L""); SendMessage(hwnd, PBM_SETBKCOLOR, 0, static_cast(darkBkColor)); SendMessage(hwnd, PBM_SETBARCOLOR, 0, static_cast(RGB(100, 100, 0))); } else if (wcscmp(szWndClassName, L"Auto-Suggest Dropdown") == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); // note: since the list control used to show the suggest dropdown has // the style LVS_OWNERDRAWFIXED setting the theme has no effect. // that's why we don't enumerate over the children of the "Auto-Suggest Dropdown" SetWindowSubclass(hwnd, AutoSuggestSubclassProc, SubclassID, reinterpret_cast(&m_sBackBrush)); EnumChildWindows(hwnd, AdjustThemeForChildrenProc, lParam); } else if (wcscmp(szWndClassName, TOOLTIPS_CLASSW) == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); } else SetWindowTheme(hwnd, L"Explorer", nullptr); } else { if (wcscmp(szWndClassName, WC_LISTVIEW) == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); CAutoThemeData hTheme = OpenThemeData(nullptr, L"ItemsView"); if (hTheme) { COLORREF color; if (SUCCEEDED(::GetThemeColor(hTheme, 0, 0, TMT_TEXTCOLOR, &color))) { ListView_SetTextColor(hwnd, color); } if (SUCCEEDED(::GetThemeColor(hTheme, 0, 0, TMT_FILLCOLOR, &color))) { ListView_SetTextBkColor(hwnd, color); ListView_SetBkColor(hwnd, color); } } auto hTT = ListView_GetToolTips(hwnd); if (hTT) { DarkModeHelper::Instance().AllowDarkModeForWindow(hTT, static_cast(lParam)); SetWindowTheme(hTT, L"Explorer", nullptr); } RemoveWindowSubclass(hwnd, ListViewSubclassProc, SubclassID); } else if (wcscmp(szWndClassName, WC_BUTTON) == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); RemoveWindowSubclass(hwnd, ButtonSubclassProc, SubclassID); } else if ((wcscmp(szWndClassName, WC_COMBOBOXEX) == 0) || (wcscmp(szWndClassName, WC_COMBOBOX) == 0)) { SetWindowTheme(hwnd, L"DarkMode_Explorer", nullptr); HWND hCombo = hwnd; if (wcscmp(szWndClassName, WC_COMBOBOXEX) == 0) { SendMessage(hwnd, CBEM_SETWINDOWTHEME, 0, reinterpret_cast(L"DarkMode_Explorer")); hCombo = reinterpret_cast(SendMessage(hwnd, CBEM_GETCOMBOCONTROL, 0, 0)); } if (hCombo) { COMBOBOXINFO info = {0}; info.cbSize = sizeof(COMBOBOXINFO); if (SendMessage(hCombo, CB_GETCOMBOBOXINFO, 0, reinterpret_cast(&info))) { DarkModeHelper::Instance().AllowDarkModeForWindow(info.hwndList, static_cast(lParam)); DarkModeHelper::Instance().AllowDarkModeForWindow(info.hwndItem, static_cast(lParam)); DarkModeHelper::Instance().AllowDarkModeForWindow(info.hwndCombo, static_cast(lParam)); SetWindowTheme(info.hwndList, L"Explorer", nullptr); SetWindowTheme(info.hwndItem, L"Explorer", nullptr); SetWindowTheme(info.hwndCombo, L"Explorer", nullptr); CAutoThemeData hTheme = OpenThemeData(nullptr, L"ItemsView"); if (hTheme) { COLORREF color; if (SUCCEEDED(::GetThemeColor(hTheme, 0, 0, TMT_TEXTCOLOR, &color))) { ListView_SetTextColor(info.hwndList, color); } if (SUCCEEDED(::GetThemeColor(hTheme, 0, 0, TMT_FILLCOLOR, &color))) { ListView_SetTextBkColor(info.hwndList, color); ListView_SetBkColor(info.hwndList, color); } } RemoveWindowSubclass(info.hwndList, ListViewSubclassProc, SubclassID); } RemoveWindowSubclass(hCombo, ComboBoxSubclassProc, SubclassID); } } else if (wcscmp(szWndClassName, WC_TREEVIEW) == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); CAutoThemeData hTheme = OpenThemeData(nullptr, L"ItemsView"); if (hTheme) { COLORREF color; if (SUCCEEDED(::GetThemeColor(hTheme, 0, 0, TMT_TEXTCOLOR, &color))) { TreeView_SetTextColor(hwnd, color); } if (SUCCEEDED(::GetThemeColor(hTheme, 0, 0, TMT_FILLCOLOR, &color))) { TreeView_SetBkColor(hwnd, color); } } auto hTT = TreeView_GetToolTips(hwnd); if (hTT) { DarkModeHelper::Instance().AllowDarkModeForWindow(hTT, static_cast(lParam)); SetWindowTheme(hTT, L"Explorer", nullptr); } } else if (_wcsnicmp(szWndClassName, L"RICHEDIT", 8) == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); CHARFORMAT2 format = {0}; format.cbSize = sizeof(CHARFORMAT2); format.dwMask = CFM_COLOR | CFM_BACKCOLOR; format.crTextColor = CTheme::Instance().GetThemeColor(GetSysColor(COLOR_WINDOWTEXT)); format.crBackColor = CTheme::Instance().GetThemeColor(GetSysColor(COLOR_WINDOW)); SendMessage(hwnd, EM_SETCHARFORMAT, SCF_ALL, reinterpret_cast(&format)); SendMessage(hwnd, EM_SETBKGNDCOLOR, 0, static_cast(format.crBackColor)); } else if (wcscmp(szWndClassName, PROGRESS_CLASS) == 0) { SetWindowTheme(hwnd, nullptr, nullptr); } else if (wcscmp(szWndClassName, L"Auto-Suggest Dropdown") == 0) { SetWindowTheme(hwnd, L"Explorer", nullptr); RemoveWindowSubclass(hwnd, AutoSuggestSubclassProc, SubclassID); EnumChildWindows(hwnd, AdjustThemeForChildrenProc, lParam); } else SetWindowTheme(hwnd, L"Explorer", nullptr); } return TRUE; } LRESULT CTheme::ListViewSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uIdSubclass*/, DWORD_PTR /*dwRefData*/) { switch (uMsg) { case WM_NOTIFY: { if (reinterpret_cast(lParam)->code == NM_CUSTOMDRAW) { LPNMCUSTOMDRAW nmcd = reinterpret_cast(lParam); switch (nmcd->dwDrawStage) { case CDDS_PREPAINT: return CDRF_NOTIFYITEMDRAW; case CDDS_ITEMPREPAINT: { SetTextColor(nmcd->hdc, darkTextColor); return CDRF_DODEFAULT; } } } } break; case WM_DESTROY: case WM_NCDESTROY: RemoveWindowSubclass(hWnd, ListViewSubclassProc, SubclassID); break; } return DefSubclassProc(hWnd, uMsg, wParam, lParam); } LRESULT CTheme::ComboBoxSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uIdSubclass*/, DWORD_PTR dwRefData) { switch (uMsg) { case WM_CTLCOLORDLG: case WM_CTLCOLORSTATIC: case WM_CTLCOLOREDIT: case WM_CTLCOLORLISTBOX: case WM_CTLCOLORBTN: case WM_CTLCOLORSCROLLBAR: { auto hbrBkgnd = reinterpret_cast(dwRefData); HDC hdc = reinterpret_cast(wParam); SetBkMode(hdc, TRANSPARENT); SetTextColor(hdc, darkTextColor); SetBkColor(hdc, darkBkColor); if (!*hbrBkgnd) *hbrBkgnd = CreateSolidBrush(darkBkColor); return reinterpret_cast(*hbrBkgnd); } case WM_DESTROY: case WM_NCDESTROY: RemoveWindowSubclass(hWnd, ComboBoxSubclassProc, SubclassID); break; } return DefSubclassProc(hWnd, uMsg, wParam, lParam); } LRESULT CTheme::MainSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uIdSubclass*/, DWORD_PTR dwRefData) { switch (uMsg) { case WM_CTLCOLORDLG: case WM_CTLCOLORSTATIC: case WM_CTLCOLOREDIT: case WM_CTLCOLORLISTBOX: case WM_CTLCOLORBTN: case WM_CTLCOLORSCROLLBAR: { auto hbrBkgnd = reinterpret_cast(dwRefData); HDC hdc = reinterpret_cast(wParam); SetBkMode(hdc, TRANSPARENT); SetTextColor(hdc, darkTextColor); SetBkColor(hdc, darkBkColor); if (!*hbrBkgnd) *hbrBkgnd = CreateSolidBrush(darkBkColor); return reinterpret_cast(*hbrBkgnd); } case WM_DESTROY: case WM_NCDESTROY: RemoveWindowSubclass(hWnd, MainSubclassProc, SubclassID); break; } return DefSubclassProc(hWnd, uMsg, wParam, lParam); } LRESULT CTheme::AutoSuggestSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uIdSubclass*/, DWORD_PTR /*dwRefData*/) { switch (uMsg) { case WM_DRAWITEM: { LPDRAWITEMSTRUCT pDis = reinterpret_cast(lParam); HDC hDC = pDis->hDC; RECT rc = pDis->rcItem; wchar_t itemText[256]; // get the text from sub-items ListView_GetItemText(pDis->hwndItem, pDis->itemID, 0, itemText, _countof(itemText)); if (pDis->itemState & LVIS_FOCUSED) ::SetBkColor(hDC, darkDisabledTextColor); else ::SetBkColor(hDC, darkBkColor); ::ExtTextOut(hDC, 0, 0, ETO_OPAQUE, &rc, nullptr, 0, nullptr); SetTextColor(pDis->hDC, darkTextColor); SetBkMode(hDC, TRANSPARENT); DrawText(hDC, itemText, -1, &rc, DT_SINGLELINE | DT_VCENTER | DT_NOPREFIX | DT_END_ELLIPSIS); } return TRUE; case WM_DESTROY: case WM_NCDESTROY: RemoveWindowSubclass(hWnd, AutoSuggestSubclassProc, SubclassID); break; } return DefSubclassProc(hWnd, uMsg, wParam, lParam); } #ifndef RECTWIDTH # define RECTWIDTH(rc) ((rc).right - (rc).left) #endif #ifndef RECTHEIGHT # define RECTHEIGHT(rc) ((rc).bottom - (rc).top) #endif LRESULT CTheme::ButtonSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR /*uIdSubclass*/, DWORD_PTR /*dwRefData*/) { switch (uMsg) { case WM_SETTEXT: case WM_ENABLE: case WM_STYLECHANGED: { LRESULT res = DefSubclassProc(hWnd, uMsg, wParam, lParam); InvalidateRgn(hWnd, nullptr, FALSE); return res; } case WM_PAINT: { PAINTSTRUCT ps; HDC hdc = BeginPaint(hWnd, &ps); if (hdc) { LONG_PTR dwStyle = GetWindowLongPtr(hWnd, GWL_STYLE); LONG_PTR dwButtonStyle = LOWORD(dwStyle); LONG_PTR dwButtonType = dwButtonStyle & 0xF; RECT rcClient; GetClientRect(hWnd, &rcClient); if ((dwButtonType & BS_GROUPBOX) == BS_GROUPBOX) { CAutoThemeData hTheme = OpenThemeData(hWnd, L"Button"); if (hTheme) { BP_PAINTPARAMS params = {sizeof(BP_PAINTPARAMS)}; params.dwFlags = BPPF_ERASE; RECT rcExclusion = rcClient; params.prcExclude = &rcExclusion; // We have to calculate the exclusion rect and therefore // calculate the font height. We select the control's font // into the DC and fake a drawing operation: HFONT hFontOld = reinterpret_cast(SendMessage(hWnd, WM_GETFONT, 0L, NULL)); if (hFontOld) hFontOld = static_cast(SelectObject(hdc, hFontOld)); RECT rcDraw = rcClient; DWORD dwFlags = DT_SINGLELINE; // we use uppercase A to determine the height of text, so we // can draw the upper line of the groupbox: DrawTextW(hdc, L"A", -1, &rcDraw, dwFlags | DT_CALCRECT); if (hFontOld) { SelectObject(hdc, hFontOld); hFontOld = nullptr; } rcExclusion.left += 2; rcExclusion.top += RECTHEIGHT(rcDraw); rcExclusion.right -= 2; rcExclusion.bottom -= 2; HDC hdcPaint = nullptr; HPAINTBUFFER hBufferedPaint = BeginBufferedPaint(hdc, &rcClient, BPBF_TOPDOWNDIB, ¶ms, &hdcPaint); if (hdcPaint) { // now we again retrieve the font, but this time we select it into // the buffered DC: hFontOld = reinterpret_cast(SendMessage(hWnd, WM_GETFONT, 0L, NULL)); if (hFontOld) hFontOld = static_cast(SelectObject(hdcPaint, hFontOld)); ::SetBkColor(hdcPaint, darkBkColor); ::ExtTextOut(hdcPaint, 0, 0, ETO_OPAQUE, &rcClient, nullptr, 0, nullptr); BufferedPaintSetAlpha(hBufferedPaint, &ps.rcPaint, 0x00); DTTOPTS dttOpts = {sizeof(DTTOPTS)}; dttOpts.dwFlags = DTT_COMPOSITED | DTT_GLOWSIZE; dttOpts.crText = darkTextColor; dttOpts.iGlowSize = 12; // Default value DetermineGlowSize(&dttOpts.iGlowSize); COLORREF cr = darkBkColor; GetEditBorderColor(hWnd, &cr); cr |= 0xff000000; std::unique_ptr myPen(new Gdiplus::Pen(Gdiplus::Color(cr), 1)); std::unique_ptr myGraphics(new Gdiplus::Graphics(hdcPaint)); int iY = RECTHEIGHT(rcDraw) / 2; Gdiplus::Rect rr = Gdiplus::Rect(rcClient.left, rcClient.top + iY, RECTWIDTH(rcClient), RECTHEIGHT(rcClient) - iY - 1); Gdiplus::GraphicsPath path; GetRoundRectPath(&path, rr, 5); myGraphics->DrawPath(myPen.get(), &path); myGraphics.reset(); myPen.reset(); int iLen = GetWindowTextLength(hWnd); if (iLen) { iLen += 5; // 1 for terminating zero, 4 for DT_MODIFYSTRING LPWSTR szText = static_cast(LocalAlloc(LPTR, sizeof(WCHAR) * iLen)); if (szText) { iLen = GetWindowTextW(hWnd, szText, iLen); if (iLen) { int iX = RECTWIDTH(rcDraw); rcDraw = rcClient; rcDraw.left += iX; DrawTextW(hdcPaint, szText, -1, &rcDraw, dwFlags | DT_CALCRECT); ::SetBkColor(hdcPaint, darkBkColor); ::ExtTextOut(hdcPaint, 0, 0, ETO_OPAQUE, &rcDraw, nullptr, 0, nullptr); rcDraw.left++; rcDraw.right++; SetBkMode(hdcPaint, TRANSPARENT); SetTextColor(hdcPaint, darkTextColor); DrawText(hdcPaint, szText, -1, &rcDraw, dwFlags); } LocalFree(szText); } } if (hFontOld) { SelectObject(hdcPaint, hFontOld); hFontOld = nullptr; } EndBufferedPaint(hBufferedPaint, TRUE); } } } else if (dwButtonType == BS_CHECKBOX || dwButtonType == BS_AUTOCHECKBOX || dwButtonType == BS_3STATE || dwButtonType == BS_AUTO3STATE || dwButtonType == BS_RADIOBUTTON || dwButtonType == BS_AUTORADIOBUTTON) { CAutoThemeData hTheme = OpenThemeData(hWnd, L"Button"); if (hTheme) { HDC hdcPaint = nullptr; BP_PAINTPARAMS params = {sizeof(BP_PAINTPARAMS)}; params.dwFlags = BPPF_ERASE; HPAINTBUFFER hBufferedPaint = BeginBufferedPaint(hdc, &rcClient, BPBF_TOPDOWNDIB, ¶ms, &hdcPaint); if (hdcPaint) { ::SetBkColor(hdcPaint, darkBkColor); ::ExtTextOut(hdcPaint, 0, 0, ETO_OPAQUE, &rcClient, nullptr, 0, nullptr); BufferedPaintSetAlpha(hBufferedPaint, &ps.rcPaint, 0x00); LRESULT dwCheckState = SendMessage(hWnd, BM_GETCHECK, 0, NULL); POINT pt; RECT rc; GetWindowRect(hWnd, &rc); GetCursorPos(&pt); BOOL bHot = PtInRect(&rc, pt); BOOL bFocus = GetFocus() == hWnd; int iPartId = BP_CHECKBOX; if (dwButtonType == BS_RADIOBUTTON || dwButtonType == BS_AUTORADIOBUTTON) iPartId = BP_RADIOBUTTON; int iState = GetStateFromBtnState(dwStyle, bHot, bFocus, dwCheckState, iPartId, FALSE); int bmWidth = static_cast(ceil(13.0 * CDPIAware::Instance().GetDPI(hWnd) / 96.0)); UINT uiHalfWidth = (RECTWIDTH(rcClient) - bmWidth) / 2; // we have to use the whole client area, otherwise we get only partially // drawn areas: RECT rcPaint = rcClient; if (dwButtonStyle & BS_LEFTTEXT) { rcPaint.left += uiHalfWidth; rcPaint.right += uiHalfWidth; } else { rcPaint.left -= uiHalfWidth; rcPaint.right -= uiHalfWidth; } // we assume that bmWidth is both the horizontal and the vertical // dimension of the control bitmap and that it is square. bm.bmHeight // seems to be the height of a striped bitmap because it is an absurdly // high dimension value if ((dwButtonStyle & BS_VCENTER) == BS_VCENTER) /// BS_VCENTER is BS_TOP|BS_BOTTOM { int h = RECTHEIGHT(rcPaint); rcPaint.top = (h - bmWidth) / 2; rcPaint.bottom = rcPaint.top + bmWidth; } else if (dwButtonStyle & BS_TOP) { rcPaint.bottom = rcPaint.top + bmWidth; } else if (dwButtonStyle & BS_BOTTOM) { rcPaint.top = rcPaint.bottom - bmWidth; } else // default: center the checkbox/radiobutton vertically { int h = RECTHEIGHT(rcPaint); rcPaint.top = (h - bmWidth) / 2; rcPaint.bottom = rcPaint.top + bmWidth; } DrawThemeBackground(hTheme, hdcPaint, iPartId, iState, &rcPaint, nullptr); rcPaint = rcClient; GetThemeBackgroundContentRect(hTheme, hdcPaint, iPartId, iState, &rcPaint, &rc); if (dwButtonStyle & BS_LEFTTEXT) rc.right -= bmWidth + 2 * GetSystemMetrics(SM_CXEDGE); else rc.left += bmWidth + 2 * GetSystemMetrics(SM_CXEDGE); DTTOPTS dttOpts = {sizeof(DTTOPTS)}; dttOpts.dwFlags = DTT_COMPOSITED | DTT_GLOWSIZE; dttOpts.crText = darkTextColor; dttOpts.iGlowSize = 12; // Default value DetermineGlowSize(&dttOpts.iGlowSize); HFONT hFontOld = reinterpret_cast(SendMessage(hWnd, WM_GETFONT, 0L, NULL)); if (hFontOld) hFontOld = static_cast(SelectObject(hdcPaint, hFontOld)); int iLen = GetWindowTextLength(hWnd); if (iLen) { iLen += 5; // 1 for terminating zero, 4 for DT_MODIFYSTRING LPWSTR szText = static_cast(LocalAlloc(LPTR, sizeof(WCHAR) * iLen)); if (szText) { iLen = GetWindowTextW(hWnd, szText, iLen); if (iLen) { DWORD dwFlags = DT_SINGLELINE /*|DT_VCENTER*/; if (dwButtonStyle & BS_MULTILINE) { dwFlags |= DT_WORDBREAK; dwFlags &= ~(DT_SINGLELINE | DT_VCENTER); } if ((dwButtonStyle & BS_CENTER) == BS_CENTER) /// BS_CENTER is BS_LEFT|BS_RIGHT dwFlags |= DT_CENTER; else if (dwButtonStyle & BS_LEFT) dwFlags |= DT_LEFT; else if (dwButtonStyle & BS_RIGHT) dwFlags |= DT_RIGHT; if ((dwButtonStyle & BS_VCENTER) == BS_VCENTER) /// BS_VCENTER is BS_TOP|BS_BOTTOM dwFlags |= DT_VCENTER; else if (dwButtonStyle & BS_TOP) dwFlags |= DT_TOP; else if (dwButtonStyle & BS_BOTTOM) dwFlags |= DT_BOTTOM; else dwFlags |= DT_VCENTER; if ((dwButtonStyle & BS_MULTILINE) && (dwFlags & DT_VCENTER)) { // the DT_VCENTER flag only works for DT_SINGLELINE, so // we have to center the text ourselves here RECT rcDummy = rc; int height = DrawText(hdcPaint, szText, -1, &rcDummy, dwFlags | DT_WORDBREAK | DT_CALCRECT); int centerY = rc.top + (RECTHEIGHT(rc) / 2); rc.top = centerY - height / 2; rc.bottom = centerY + height / 2; } SetBkMode(hdcPaint, TRANSPARENT); if (dwStyle & WS_DISABLED) SetTextColor(hdcPaint, darkDisabledTextColor); else SetTextColor(hdcPaint, darkTextColor); DrawText(hdcPaint, szText, -1, &rc, dwFlags); // draw the focus rectangle if neccessary: if (bFocus) { RECT rcDraw = rc; DrawTextW(hdcPaint, szText, -1, &rcDraw, dwFlags | DT_CALCRECT); if (dwFlags & DT_SINGLELINE) { dwFlags &= ~DT_VCENTER; RECT rcDrawTop; DrawTextW(hdcPaint, szText, -1, &rcDrawTop, dwFlags | DT_CALCRECT); rcDraw.top = rcDraw.bottom - RECTHEIGHT(rcDrawTop); } if (dwFlags & DT_RIGHT) { int iWidth = RECTWIDTH(rcDraw); rcDraw.right = rc.right; rcDraw.left = rcDraw.right - iWidth; } RECT rcFocus; IntersectRect(&rcFocus, &rc, &rcDraw); DrawFocusRect(&rcFocus, hdcPaint); } } LocalFree(szText); } } if (hFontOld) { SelectObject(hdcPaint, hFontOld); hFontOld = nullptr; } EndBufferedPaint(hBufferedPaint, TRUE); } } } else if (BS_PUSHBUTTON == dwButtonType || BS_DEFPUSHBUTTON == dwButtonType) { // push buttons are drawn properly in dark mode without us doing anything return DefSubclassProc(hWnd, uMsg, wParam, lParam); } else PaintControl(hWnd, hdc, &ps.rcPaint, false); } EndPaint(hWnd, &ps); return 0; } case WM_DESTROY: case WM_NCDESTROY: RemoveWindowSubclass(hWnd, ButtonSubclassProc, SubclassID); break; } return DefSubclassProc(hWnd, uMsg, wParam, lParam); } void CTheme::OnSysColorChanged() { m_isHighContrastModeDark = false; m_isHighContrastMode = false; HIGHCONTRAST hc = {sizeof(HIGHCONTRAST)}; SystemParametersInfo(SPI_GETHIGHCONTRAST, sizeof(HIGHCONTRAST), &hc, FALSE); if ((hc.dwFlags & HCF_HIGHCONTRASTON) != 0) { m_isHighContrastMode = true; // check if the high contrast mode is dark float h1, h2, s1, s2, l1, l2; RGBtoHSL(::GetSysColor(COLOR_WINDOWTEXT), h1, s1, l1); RGBtoHSL(::GetSysColor(COLOR_WINDOW), h2, s2, l2); m_isHighContrastModeDark = l2 < l1; } auto setDarkMode = bPortable ? g_iniFile.GetBoolValue(L"global", L"darkmode", false) : !!m_regDarkTheme; m_dark = setDarkMode && IsDarkModeAllowed() && !IsHighContrastMode() && DarkModeHelper::Instance().ShouldAppsUseDarkMode(); } bool CTheme::IsDarkModeAllowed() { if (IsHighContrastMode()) return false; if (m_bLoaded) return m_bDarkModeIsAllowed; // we only allow the dark mode for Win10 1809 and later, // because on earlier versions it would look really, really ugly! m_bDarkModeIsAllowed = false; auto version = CPathUtils::GetVersionFromFile(L"uiribbon.dll"); std::vector tokens; stringtok(tokens, version, false, L"."); if (tokens.size() == 4) { auto major = std::stol(tokens[0]); auto minor = std::stol(tokens[1]); auto micro = std::stol(tokens[2]); //auto build = std::stol(tokens[3]); // the windows 10 update 1809 has the version // number as 10.0.17763.1 if (major > 10) m_bDarkModeIsAllowed = true; else if (major == 10) { if (minor > 0) m_bDarkModeIsAllowed = true; else if (micro > 17762) m_bDarkModeIsAllowed = true; } } return m_bDarkModeIsAllowed; } void CTheme::SetDarkTheme(bool b /*= true*/, bool force /*= false*/) { if (!m_bDarkModeIsAllowed && !force) return; if (force || m_dark != b) { bool highContrast = IsHighContrastMode(); m_dark = b && !highContrast && DarkModeHelper::Instance().ShouldAppsUseDarkMode(); if (!highContrast && DarkModeHelper::Instance().ShouldAppsUseDarkMode()) { if (bPortable) g_iniFile.SetValue(L"global", L"darkmode", b ? L"1" : L"0"); else m_regDarkTheme = b ? 1 : 0; } for (auto& [id, callback] : m_themeChangeCallbacks) callback(); } } /// returns true if dark theme is enabled. If false, then the normal theme is active. bool CTheme::IsDarkTheme() const { return m_dark; } void CTheme::RGBToHSB(COLORREF rgb, BYTE& hue, BYTE& saturation, BYTE& brightness) { BYTE r = GetRValue(rgb); BYTE g = GetGValue(rgb); BYTE b = GetBValue(rgb); BYTE minRGB = min(min(r, g), b); BYTE maxRGB = max(max(r, g), b); BYTE delta = maxRGB - minRGB; double l = maxRGB; double s = 0.0; double h = 0.0; if (maxRGB == 0) { hue = 0; saturation = 0; brightness = 0; return; } if (maxRGB) s = (255.0 * delta) / maxRGB; if (static_cast(s) != 0) { if (r == maxRGB) h = 0 + 43 * static_cast(g - b) / delta; else if (g == maxRGB) h = 85 + 43 * static_cast(b - r) / delta; else if (b == maxRGB) h = 171 + 43 * static_cast(r - g) / delta; } else h = 0.0; hue = static_cast(h); saturation = static_cast(s); brightness = static_cast(l); } void CTheme::RGBtoHSL(COLORREF color, float& h, float& s, float& l) { const float rPercent = static_cast(GetRValue(color)) / 255; const float gPercent = static_cast(GetGValue(color)) / 255; const float bPercent = static_cast(GetBValue(color)) / 255; float maxColor = 0; if ((rPercent >= gPercent) && (rPercent >= bPercent)) maxColor = rPercent; else if ((gPercent >= rPercent) && (gPercent >= bPercent)) maxColor = gPercent; else if ((bPercent >= rPercent) && (bPercent >= gPercent)) maxColor = bPercent; float minColor = 0; if ((rPercent <= gPercent) && (rPercent <= bPercent)) minColor = rPercent; else if ((gPercent <= rPercent) && (gPercent <= bPercent)) minColor = gPercent; else if ((bPercent <= rPercent) && (bPercent <= gPercent)) minColor = bPercent; float L = 0, S = 0, H = 0; L = (maxColor + minColor) / 2; if (maxColor == minColor) { S = 0; H = 0; } else { auto d = maxColor - minColor; if (L < .50) S = d / (maxColor + minColor); else S = d / ((2.0f - maxColor) - minColor); if (maxColor == rPercent) H = (gPercent - bPercent) / d; else if (maxColor == gPercent) H = 2.0f + (bPercent - rPercent) / d; else if (maxColor == bPercent) H = 4.0f + (rPercent - gPercent) / d; } H = H * 60; if (H < 0) H += 360; s = S * 100; l = L * 100; h = H; } static float hsLtoRGBSubfunction(float temp1, float temp2, float temp3) { if ((temp3 * 6) < 1) return (temp2 + (temp1 - temp2) * 6 * temp3) * 100; else if ((temp3 * 2) < 1) return temp1 * 100; else if ((temp3 * 3) < 2) return (temp2 + (temp1 - temp2) * (.66666f - temp3) * 6) * 100; else return temp2 * 100; } COLORREF CTheme::HSLtoRGB(float h, float s, float l) { if (s == 0) { BYTE t = static_cast(l / 100 * 255); return RGB(t, t, t); } const float L = l / 100; const float S = s / 100; const float H = h / 360; const float temp1 = (L < .50) ? L * (1 + S) : L + S - (L * S); const float temp2 = 2 * L - temp1; float temp3 = 0; temp3 = H + .33333f; if (temp3 > 1) temp3 -= 1; const float pcr = hsLtoRGBSubfunction(temp1, temp2, temp3); temp3 = H; const float pcg = hsLtoRGBSubfunction(temp1, temp2, temp3); temp3 = H - .33333f; if (temp3 < 0) temp3 += 1; const float pcb = hsLtoRGBSubfunction(temp1, temp2, temp3); BYTE r = static_cast(pcr / 100 * 255); BYTE g = static_cast(pcg / 100 * 255); BYTE b = static_cast(pcb / 100 * 255); return RGB(r, g, b); } int GetStateFromBtnState(LONG_PTR dwStyle, BOOL bHot, BOOL bFocus, LRESULT dwCheckState, int iPartId, BOOL bHasMouseCapture) { int iState = 0; switch (iPartId) { case BP_PUSHBUTTON: iState = PBS_NORMAL; if (dwStyle & WS_DISABLED) iState = PBS_DISABLED; else { if (dwStyle & BS_DEFPUSHBUTTON) iState = PBS_DEFAULTED; if (bHasMouseCapture && bHot) iState = PBS_PRESSED; else if (bHasMouseCapture || bHot) iState = PBS_HOT; } break; case BP_GROUPBOX: iState = (dwStyle & WS_DISABLED) ? GBS_DISABLED : GBS_NORMAL; break; case BP_RADIOBUTTON: iState = RBS_UNCHECKEDNORMAL; switch (dwCheckState) { case BST_CHECKED: if (dwStyle & WS_DISABLED) iState = RBS_CHECKEDDISABLED; else if (bFocus) iState = RBS_CHECKEDPRESSED; else if (bHot) iState = RBS_CHECKEDHOT; else iState = RBS_CHECKEDNORMAL; break; case BST_UNCHECKED: if (dwStyle & WS_DISABLED) iState = RBS_UNCHECKEDDISABLED; else if (bFocus) iState = RBS_UNCHECKEDPRESSED; else if (bHot) iState = RBS_UNCHECKEDHOT; else iState = RBS_UNCHECKEDNORMAL; break; } break; case BP_CHECKBOX: switch (dwCheckState) { case BST_CHECKED: if (dwStyle & WS_DISABLED) iState = CBS_CHECKEDDISABLED; else if (bFocus) iState = CBS_CHECKEDPRESSED; else if (bHot) iState = CBS_CHECKEDHOT; else iState = CBS_CHECKEDNORMAL; break; case BST_INDETERMINATE: if (dwStyle & WS_DISABLED) iState = CBS_MIXEDDISABLED; else if (bFocus) iState = CBS_MIXEDPRESSED; else if (bHot) iState = CBS_MIXEDHOT; else iState = CBS_MIXEDNORMAL; break; case BST_UNCHECKED: if (dwStyle & WS_DISABLED) iState = CBS_UNCHECKEDDISABLED; else if (bFocus) iState = CBS_UNCHECKEDPRESSED; else if (bHot) iState = CBS_UNCHECKEDHOT; else iState = CBS_UNCHECKEDNORMAL; break; } break; default: ASSERT(0); break; } return iState; } void GetRoundRectPath(Gdiplus::GraphicsPath* pPath, const Gdiplus::Rect& r, int dia) { // diameter can't exceed width or height if (dia > r.Width) dia = r.Width; if (dia > r.Height) dia = r.Height; // define a corner Gdiplus::Rect corner(r.X, r.Y, dia, dia); // begin path pPath->Reset(); pPath->StartFigure(); // top left pPath->AddArc(corner, 180, 90); // top right corner.X += (r.Width - dia - 1); pPath->AddArc(corner, 270, 90); // bottom right corner.Y += (r.Height - dia - 1); pPath->AddArc(corner, 0, 90); // bottom left corner.X -= (r.Width - dia - 1); pPath->AddArc(corner, 90, 90); // end path pPath->CloseFigure(); } void DrawRect(LPRECT prc, HDC hdcPaint, Gdiplus::DashStyle dashStyle, Gdiplus::Color clr, Gdiplus::REAL width) { std::unique_ptr myPen(new Gdiplus::Pen(clr, width)); myPen->SetDashStyle(dashStyle); std::unique_ptr myGraphics(new Gdiplus::Graphics(hdcPaint)); myGraphics->DrawRectangle(myPen.get(), static_cast(prc->left), static_cast(prc->top), static_cast(prc->right - 1 - prc->left), static_cast(prc->bottom - 1 - prc->top)); } void DrawFocusRect(LPRECT prcFocus, HDC hdcPaint) { DrawRect(prcFocus, hdcPaint, Gdiplus::DashStyleDot, Gdiplus::Color(0xFF, 0, 0, 0), 1.0); } void PaintControl(HWND hWnd, HDC hdc, RECT* prc, bool bDrawBorder) { HDC hdcPaint = nullptr; if (bDrawBorder) InflateRect(prc, 1, 1); HPAINTBUFFER hBufferedPaint = BeginBufferedPaint(hdc, prc, BPBF_TOPDOWNDIB, nullptr, &hdcPaint); if (hdcPaint) { RECT rc; GetWindowRect(hWnd, &rc); PatBlt(hdcPaint, 0, 0, RECTWIDTH(rc), RECTHEIGHT(rc), BLACKNESS); BufferedPaintSetAlpha(hBufferedPaint, &rc, 0x00); /// /// first blit white so list ctrls don't look ugly: /// PatBlt(hdcPaint, 0, 0, RECTWIDTH(rc), RECTHEIGHT(rc), WHITENESS); if (bDrawBorder) InflateRect(prc, -1, -1); // Tell the control to paint itself in our memory buffer SendMessage(hWnd, WM_PRINTCLIENT, reinterpret_cast(hdcPaint), PRF_CLIENT | PRF_ERASEBKGND | PRF_NONCLIENT | PRF_CHECKVISIBLE); if (bDrawBorder) { InflateRect(prc, 1, 1); FrameRect(hdcPaint, prc, static_cast(GetStockObject(BLACK_BRUSH))); } // don't make a possible border opaque, only the inner part of the control InflateRect(prc, -2, -2); // Make every pixel opaque BufferedPaintSetAlpha(hBufferedPaint, prc, 255); EndBufferedPaint(hBufferedPaint, TRUE); } } BOOL DetermineGlowSize(int* piSize, LPCWSTR pszClassIdList /*= NULL*/) { if (!piSize) { SetLastError(ERROR_INVALID_PARAMETER); return FALSE; } if (!pszClassIdList) pszClassIdList = L"CompositedWindow::Window"; CAutoThemeData hThemeWindow = OpenThemeData(nullptr, pszClassIdList); if (hThemeWindow != NULL) { SUCCEEDED(GetThemeInt(hThemeWindow, 0, 0, TMT_TEXTGLOWSIZE, piSize)); return TRUE; } SetLastError(ERROR_FILE_NOT_FOUND); return FALSE; } BOOL GetEditBorderColor(HWND hWnd, COLORREF* pClr) { ASSERT(pClr); CAutoThemeData hTheme = OpenThemeData(hWnd, L"Edit"); if (hTheme) { ::GetThemeColor(hTheme, EP_BACKGROUNDWITHBORDER, EBWBS_NORMAL, TMT_BORDERCOLOR, pClr); return TRUE; } return FALSE; }