MFC 实现动态控件调整和主题切换
在开发桌面应用程序时,常常需要为不同的屏幕分辨率和用户界面需求动态调整控件的大小,并且支持不同的主题(如浅色和深色模式)。在 MFC 中,虽然有一些方法来实现这些功能,但实现起来往往比较繁琐。为了更好地管理控件的调整和主题切换,我们可以创建一个通用的基类来处理这些功能。
1. 需求分析
在一个典型的应用程序中,可能会遇到以下几种需求:
- 动态调整控件大小:当用户调整窗口大小时,控件的大小应根据窗口的变化自动调整。
- 主题切换:根据用户的偏好或系统设置,切换浅色(Light)或深色(Dark)主题。
为了满足这些需求,我们可以设计一个 CBaseDlg
基类,该类继承自 CDialogEx
,并提供以下功能:
- 动态调整控件的大小。
- 切换浅色和深色主题。
2. 设计 CBaseDlg
基类
首先,定义 CBaseDlg
类,该类提供了管理控件布局、字体、主题切换等功能。
头文件(CBaseDlg.h
)
#pragma once
#include <memory>
#include <unordered_map>
enum class ThemeType {
Light, // 浅色主题
Dark // 深色主题
};
struct Theme {
COLORREF backgroundColor;
COLORREF textColor;
COLORREF buttonColor;
COLORREF borderColor;
};
class CBaseDlg : public CDialogEx
{
DECLARE_DYNAMIC(CBaseDlg)
public:
CBaseDlg(UINT id, CWnd* pPage); // 标准构造函数
virtual ~CBaseDlg(); // 析构函数
// 字体管理
CFont* GetOrCreateFont(int nFontSize); // 获取或创建字体
void SetDefaultFont(); // 设置默认字体
// 动态控件管理
BOOL AddControl(UINT nCtrlID, CWnd* pControl); // 添加控件
BOOL RemoveControl(UINT nCtrlID); // 移除控件
BOOL UpdateControlText(UINT nCtrlID, const CString& strText); // 更新控件文本
CWnd* GetControl(UINT nCtrlID); // 获取控件
// 主题设置
void SwitchTheme(ThemeType themeType); // 切换主题
private:
void AdjustControls(float dScaleX, float dScaleY); // 调整控件大小
void AdjustControlFont(CWnd* pWnd, int nWidth, int nHeight); // 调整控件字体
private:
bool m_bResizing; // 控件是否正在调整大小
int m_nInitialWidth; // 对话框初始宽度
int m_nInitialHeight; // 对话框初始高度
std::unordered_map<int, CRect> m_mapCtrlLayouts; // 控件布局
std::map<UINT, std::unique_ptr<CWnd>> m_mapControls; // 控件集合
std::unordered_map<int, std::shared_ptr<CFont>> m_mapFonts; // 控件字体
DECLARE_MESSAGE_MAP()
public:
virtual BOOL OnInitDialog();
afx_msg void OnSize(UINT nType, int cx, int cy);
afx_msg void OnGetMinMaxInfo(MINMAXINFO* lpMMI);
afx_msg HBRUSH OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor);
};
源文件(CBaseDlg.cpp
)
#include "stdafx.h"
#include "CBaseDlg.h"
#include "GridCtrl.h"
#include <windows.h>
// 全局主题对象
Theme g_lightTheme = { RGB(255, 255, 255), RGB(0, 0, 0), RGB(240, 240, 240), RGB(200, 200, 200) };
Theme g_darkTheme = { RGB(40, 40, 40), RGB(255, 255, 255), RGB(60, 60, 60), RGB(80, 80, 80) };
CFont g_defaultFont;
Theme* g_currentTheme = &g_lightTheme;
IMPLEMENT_DYNAMIC(CBaseDlg, CDialogEx)
CBaseDlg::CBaseDlg(UINT id, CWnd* pPage) : CDialogEx(id, pPage), m_bResizing(false)
{
m_nInitialWidth = 0;
m_nInitialHeight = 0;
}
CBaseDlg::~CBaseDlg()
{
// shared_ptr会自动清理内存,不需要手动删除
m_mapFonts.clear();
m_mapCtrlLayouts.clear();
m_mapControls.clear();
}
CFont* CBaseDlg::GetOrCreateFont(int nFontSize)
{
auto it = m_mapFonts.find(nFontSize);
if (it != m_mapFonts.end()) {
return it->second.get();
}
// 使用 shared_ptr 来管理字体对象
auto font = std::make_shared<CFont>();
LOGFONT logFont = { 0 };
_tcscpy_s(logFont.lfFaceName, _T("Segoe UI"));
logFont.lfHeight = -nFontSize;
logFont.lfQuality = CLEARTYPE_QUALITY;
font->CreateFontIndirect(&logFont);
m_mapFonts[nFontSize] = font;
return font.get();
}
void CBaseDlg::SetDefaultFont()
{
CFont* defaultFont = GetOrCreateFont(12);
CWnd* pWnd = GetWindow(GW_CHILD);
while (pWnd) {
TCHAR szClassName[256];
GetClassName(pWnd->m_hWnd, szClassName, sizeof(szClassName));
if (_tcsicmp(szClassName, _T("MFCGridCtrl")) == 0) {
pWnd = pWnd->GetNextWindow();
continue;
}
pWnd->SetFont(defaultFont, TRUE);
pWnd = pWnd->GetNextWindow();
}
}
BOOL CBaseDlg::AddControl(UINT nCtrlID, CWnd* pControl)
{
// 确保控件不重复添加
if (m_mapControls.find(nCtrlID) != m_mapControls.end()) {
return FALSE; // 控件已经存在
}
m_mapControls[nCtrlID] = std::unique_ptr<CWnd>(pControl);
return TRUE;
}
BOOL CBaseDlg::RemoveControl(UINT nCtrlID)
{
auto it = m_mapControls.find(nCtrlID);
if (it != m_mapControls.end()) {
m_mapControls.erase(it);
return TRUE;
}
return FALSE;
}
BOOL CBaseDlg::UpdateControlText(UINT nCtrlID, const CString& strText)
{
auto it = m_mapControls.find(nCtrlID);
if (it != m_mapControls.end()) {
CWnd* pWnd = it->second.get();
if (pWnd->GetSafeHwnd() != nullptr)
{
pWnd->SetWindowText(strText);
return TRUE;
}
}
return FALSE;
}
CWnd* CBaseDlg::GetControl(UINT nCtrlID)
{
auto it = m_mapControls.find(nCtrlID);
if (it != m_mapControls.end()) {
return it->second.get();
}
return nullptr;
}
void CBaseDlg::SwitchTheme(ThemeType themeType)
{
// 使用 map 来根据 themeType 查找主题
static const std::unordered_map<ThemeType, Theme*> themeMap = {
{ ThemeType::Light, &g_lightTheme },
{ ThemeType::Dark, &g_darkTheme }
};
// 设置当前主题
auto it = themeMap.find(themeType);
if (it != themeMap.end()) {
g_currentTheme = it->second;
}
else {
g_currentTheme = &g_lightTheme;
}
// 更新控件的外观
CWnd* pWnd = GetWindow(GW_CHILD);
while (pWnd) {
pWnd->Invalidate(); // 重绘控件
pWnd = pWnd->GetNextWindow();
}
// 更新对话框背景颜色
SetBackgroundColor(g_currentTheme->backgroundColor);
}
void CBaseDlg::AdjustControls(float dScaleX, float dScaleY)
{
if (m_bResizing) return; // 防止在调整过程中重复调整
m_bResizing = true;
CWnd* pWnd = GetWindow(GW_CHILD);
while (pWnd) {
int nCtrlID = pWnd->GetDlgCtrlID();
if (nCtrlID != -1 && m_mapCtrlLayouts.find(nCtrlID) != m_mapCtrlLayouts.end()) {
CRect originalRect = m_mapCtrlLayouts[nCtrlID];
CRect newRect(
static_cast<int>(originalRect.left * dScaleX),
static_cast<int>(originalRect.top * dScaleY),
static_cast<int>(originalRect.right * dScaleX),
static_cast<int>(originalRect.bottom * dScaleY));
TCHAR szClassName[256];
GetClassName(pWnd->m_hWnd, szClassName, sizeof(szClassName));
if (_tcsicmp(szClassName, _T("ComboBox")) == 0) {
CComboBox* pComboBox = (CComboBox*)pWnd;
pComboBox->SetItemHeight(-1, newRect.Height()); // -1 表示所有项的高度
}
if (_tcsicmp(szClassName, _T("MFCGridCtrl")) == 0) {
CGridCtrl* pGridCtrl = (CGridCtrl*)pWnd;
pGridCtrl->SetDefCellHeight(newRect.Height() / 21);
pGridCtrl->ExpandColumnsToFit(TRUE);
pGridCtrl->ExpandLastColumn();
pGridCtrl->Invalidate();
pGridCtrl->UpdateWindow();
}
pWnd->MoveWindow(&newRect);
AdjustControlFont(pWnd, newRect.Width(), newRect.Height());
}
pWnd = pWnd->GetNextWindow();
}
m_bResizing = false;
}
void CBaseDlg::AdjustControlFont(CWnd* pWnd, int nWidth, int nHeight)
{
TCHAR szClassName[256];
GetClassName(pWnd->m_hWnd, szClassName, sizeof(szClassName));
if (_tcsicmp(szClassName, _T("MFCGridCtrl")) == 0) {
return;
}
int fontSize = nHeight / 2;
if (fontSize < 8) fontSize = 8;
if (fontSize > 32) fontSize = 32;
CFont* pFont = GetOrCreateFont(fontSize);
pWnd->SetFont(pFont);
pWnd->Invalidate(); // 刷新控件显示
}
BEGIN_MESSAGE_MAP(CBaseDlg, CDialogEx)
ON_WM_SIZE()
ON_WM_GETMINMAXINFO()
ON_WM_CTLCOLOR()
END_MESSAGE_MAP()
BOOL CBaseDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 获取当前语言
LANGID langId = GetUserDefaultLangID();
if (langId == LANG_CHINESE) {
// 加载中文资源
}
else {
// 加载英文资源
}
// 获取对话框的工作区(屏幕可用区域)
CRect screenRect, dlgRect, clientRect;
SystemParametersInfo(SPI_GETWORKAREA, 0, &screenRect, 0);
GetClientRect(&clientRect);
m_nInitialWidth = clientRect.Width();
m_nInitialHeight = clientRect.Height();
// 设置默认字体
CFont* pDefaultFont = GetOrCreateFont(12);
// 遍历子窗口(控件)
CWnd* pWnd = GetWindow(GW_CHILD);
while (pWnd) {
int nCtrlID = pWnd->GetDlgCtrlID();
if (nCtrlID != -1) {
// 保存控件的初始布局
CRect ctrlRect;
pWnd->GetWindowRect(&ctrlRect);
ScreenToClient(&ctrlRect);
m_mapCtrlLayouts[nCtrlID] = ctrlRect;
// 排除不需要操作的控件(如自定义控件 GridCtrl)
TCHAR szClassName[256];
GetClassName(pWnd->m_hWnd, szClassName, sizeof(szClassName));
if (_tcsicmp(szClassName, _T("MFCGridCtrl")) == 0) {
pWnd = pWnd->GetNextWindow();
continue;
}
// 设置控件的默认字体
pWnd->SetFont(pDefaultFont);
}
pWnd = pWnd->GetNextWindow();
}
// 将对话框居中
GetWindowRect(&dlgRect);
int dlgWidth = dlgRect.Width() * 2;
int dlgHeight = dlgRect.Height() * 2;
if (dlgWidth > screenRect.Width()) {
dlgWidth = screenRect.Width();
}
if (dlgHeight > screenRect.Height()) {
dlgHeight = screenRect.Height();
}
int centerX = screenRect.left + (screenRect.Width() - dlgWidth) / 2;
int centerY = screenRect.top + (screenRect.Height() - dlgHeight) / 2;
MoveWindow(centerX, centerY, dlgWidth, dlgHeight);
return TRUE;
}
void CBaseDlg::OnSize(UINT nType, int cx, int cy)
{
CDialogEx::OnSize(nType, cx, cy);
if (nType == SIZE_MINIMIZED || m_mapCtrlLayouts.empty()) {
return;
}
// 检查尺寸变化是否足够大,避免频繁调整
//static int lastWidth = 0, lastHeight = 0;
//if (abs(cx - lastWidth) < 10 && abs(cy - lastHeight) < 10) {
// return;
//}
//lastWidth = cx;
//lastHeight = cy;
// 计算比例并调整布局
float dScaleX = static_cast<float>(cx) / m_nInitialWidth;
float dScaleY = static_cast<float>(cy) / m_nInitialHeight;
AdjustControls(dScaleX, dScaleY);
}
void CBaseDlg::OnGetMinMaxInfo(MINMAXINFO* lpMMI)
{
lpMMI->ptMinTrackSize.x = 400;
lpMMI->ptMinTrackSize.y = 300;
CDialogEx::OnGetMinMaxInfo(lpMMI);
}
HBRUSH CBaseDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor)
{
if (g_currentTheme) {
pDC->SetBkColor(g_currentTheme->backgroundColor);
pDC->SetTextColor(g_currentTheme->textColor);
// 返回背景画刷
return CreateSolidBrush(g_currentTheme->backgroundColor);
}
return CDialogEx::OnCtlColor(pDC, pWnd, nCtlColor);
}
3. 总结
通过以上的代码,实现了一个支持动态调整控件大小和主题切换的 CBaseDlg 基类。核心功能包括:
- 动态调整控件大小和字体。
- 根据系统设置或用户选择切换浅色和深色主题。
- 切换语言资源(待完成)
该类能够帮助开发者更加便捷地管理控件布局和主题切换,提升应用的用户体验。