在 Visual Studio 2019(使用 v142 工具集)中,用 **纯 C++ + GDI+** 实现自定义 UI 控件(如按钮、文本框、滑块等),**不依赖 MFC、WinForms、WPF 或任何第三方 GUI 框架**,是完全可行的——但需手动处理窗口过程(`WndProc`)、GDI+ 绘图、鼠标/键盘消息响应、控件状态管理、重绘逻辑、焦点/捕获/悬停等底层细节。下面我将为你提供一个**可运行的最小完整示例**:一个带悬停/按下效果的自绘制按钮控件(`GdiPlusButton`),并详细解释实现要点。
---
### ✅ 目标
- 使用 `GDI+` 进行抗锯齿、渐变、圆角等现代视觉绘制;
- 完全自主管理控件生命周期(创建、布局、消息分发、销毁);
- 支持:鼠标进入/离开/按下/释放、点击回调、启用/禁用状态;
- 零外部依赖(仅 Windows SDK + GDI+);
- 符合 VS2019 v142(C++17 兼容)标准。
---
### 📦 前置准备(VS2019 项目配置)
1. 新建 **空 Win32 项目**(非控制台,选择“Windows 应用程序”);
2. 在 **项目属性 → 配置属性 → 常规 → Windows SDK 版本**:≥ 10.0(推荐 10.0.19041.0+);
3. **配置属性 → 常规 → C++ 语言标准**:ISO C++17 标准(或 `/std:c++17`);
4. **配置属性 → 链接器 → 输入 → 附加依赖项**:`gdiplus.lib`;
5. **包含头文件**:
```cpp
#include <windows.h>
#include <gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
```
> ⚠️ 注意:GDI+ 必须显式初始化/反初始化(线程安全需注意,此处单线程 UI 主线程调用)。
---
### 🧩 核心实现:`GdiPlusButton` 类(纯 C++,无继承 MFC/ATL)
```cpp
// GdiPlusButton.h
#pragma once
#include <windows.h>
#include <gdiplus.h>
#include <functional>
#include <memory>
class GdiPlusButton {
public:
using ClickCallback = std::function<void()>;
GdiPlusButton(HWND parent, int x, int y, int w, int h, LPCWSTR text);
~GdiPlusButton();
void Show();
void Hide();
void Enable(bool enable = true) { m_enabled = enable; }
void SetText(LPCWSTR text);
void SetClickCallback(ClickCallback cb) { m_on_click = std::move(cb); }
// 消息处理(由父窗口 WndProc 转发)
LRESULT HandleMessage(UINT msg, WPARAM wParam, LPARAM lParam);
private:
HWND m_hwnd = nullptr;
RECT m_rect{};
std::wstring m_text;
bool m_enabled = true;
bool m_hovered = false;
bool m_pressed = false;
ClickCallback m_on_click;
// GDI+ 资源(延迟初始化,避免构造时 GDI+ 未就绪)
mutable Gdiplus::Graphics* m_g = nullptr;
mutable Gdiplus::SolidBrush* m_brush_normal = nullptr;
mutable Gdiplus::SolidBrush* m_brush_hover = nullptr;
mutable Gdiplus::SolidBrush* m_brush_pressed = nullptr;
mutable Gdiplus::SolidBrush* m_brush_disabled = nullptr;
mutable Gdiplus::Pen* m_pen_border = nullptr;
mutable Gdiplus::Font* m_font = nullptr;
void EnsureGdiPlusResources() const;
void ReleaseGdiPlusResources();
void DrawButton(Gdiplus::Graphics& g) const;
};
```
```cpp
// GdiPlusButton.cpp
#include "GdiPlusButton.h"
#include <dwmapi.h>
GdiPlusButton::GdiPlusButton(HWND parent, int x, int y, int w, int h, LPCWSTR text)
: m_text(text), m_rect{ x, y, x + w, y + h } {
// 创建无窗口句柄的“逻辑控件”——我们不创建子窗口,而是绘制在父窗口客户区
// 所有交互通过父窗口捕获鼠标消息实现(即:父窗口需将 WM_MOUSEMOVE/UP/DOWN 等转发给此对象)
// 因此本类不调用 CreateWindow,而是纯绘制+状态管理
}
GdiPlusButton::~GdiPlusButton() {
ReleaseGdiPlusResources();
}
void GdiPlusButton::Show() {
// 通知父窗口需要重绘该区域(实际由父窗口负责刷新)
if (m_hwnd) InvalidateRect(m_hwnd, &m_rect, TRUE);
}
void GdiPlusButton::Hide() {
if (m_hwnd) InvalidateRect(m_hwnd, &m_rect, TRUE);
}
void GdiPlusButton::SetText(LPCWSTR text) {
m_text = text ? text : L"";
}
LRESULT GdiPlusButton::HandleMessage(UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_MOUSEMOVE: {
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
bool in = PtInRect(&m_rect, pt);
if (in && !m_hovered && m_enabled) {
m_hovered = true;
if (m_hwnd) InvalidateRect(m_hwnd, &m_rect, FALSE);
}
else if (!in && m_hovered) {
m_hovered = false;
m_pressed = false;
if (m_hwnd) InvalidateRect(m_hwnd, &m_rect, FALSE);
}
return 0;
}
case WM_LBUTTONDOWN: {
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
if (PtInRect(&m_rect, pt) && m_enabled) {
m_pressed = true;
if (m_hwnd) InvalidateRect(m_hwnd, &m_rect, FALSE);
SetCapture(m_hwnd); // 捕获鼠标,防止移出后丢失 UP
}
return 0;
}
case WM_LBUTTONUP: {
ReleaseCapture();
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
if (m_pressed && PtInRect(&m_rect, pt) && m_enabled) {
m_pressed = false;
if (m_on_click) m_on_click();
}
else {
m_pressed = false;
}
if (m_hwnd) InvalidateRect(m_hwnd, &m_rect, FALSE);
return 0;
}
case WM_CAPTURECHANGED:
if (m_pressed) {
m_pressed = false;
if (m_hwnd) InvalidateRect(m_hwnd, &m_rect, FALSE);
}
return 0;
case WM_SETFOCUS:
case WM_KILLFOCUS:
// 可扩展支持键盘焦点(如空格触发点击)
break;
}
return 0;
}
void GdiPlusButton::EnsureGdiPlusResources() const {
if (m_g) return;
// 获取父窗口 DC(仅用于首次创建 Graphics 对象;后续重绘由父窗口提供 HDC)
HDC hdc = GetDC(m_hwnd);
m_g = new Gdiplus::Graphics(hdc);
m_g->SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
m_g->SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic);
m_g->SetTextRenderingHint(Gdiplus::TextRenderingHintClearTypeGridFit);
// 创建画刷和笔(可根据主题/状态动态生成)
m_brush_normal = new Gdiplus::SolidBrush(Gdiplus::Color(240, 240, 240));
m_brush_hover = new Gdiplus::SolidBrush(Gdiplus::Color(220, 240, 255));
m_brush_pressed = new Gdiplus::SolidBrush(Gdiplus::Color(180, 210, 255));
m_brush_disabled = new Gdiplus::SolidBrush(Gdiplus::Color(220, 220, 220));
m_pen_border = new Gdiplus::Pen(Gdiplus::Color(120, 120, 120), 1.0f);
m_font = new Gdiplus::Font(L"Segoe UI", 12.0f, Gdiplus::FontStyleRegular, Gdiplus::UnitPixel);
ReleaseDC(m_hwnd, hdc);
}
void GdiPlusButton::ReleaseGdiPlusResources() {
delete m_g; m_g = nullptr;
delete m_brush_normal; m_brush_normal = nullptr;
delete m_brush_hover; m_brush_hover = nullptr;
delete m_brush_pressed; m_brush_pressed = nullptr;
delete m_brush_disabled; m_brush_disabled = nullptr;
delete m_pen_border; m_pen_border = nullptr;
delete m_font; m_font = nullptr;
}
void GdiPlusButton::DrawButton(Gdiplus::Graphics& g) const {
Gdiplus::RectF rc(m_rect.left, m_rect.top,
m_rect.right - m_rect.left,
m_rect.bottom - m_rect.top);
// 绘制背景(圆角矩形)
Gdiplus::GraphicsPath path;
const float radius = 4.0f;
path.AddArc(rc.X, rc.Y, radius * 2, radius * 2, 180, 90); // top-left
path.AddArc(rc.X + rc.Width - radius * 2, rc.Y, radius * 2, radius * 2, 270, 90); // top-right
path.AddArc(rc.X + rc.Width - radius * 2, rc.Y + rc.Height - radius * 2, radius * 2, radius * 2, 0, 90); // bottom-right
path.AddArc(rc.X, rc.Y + rc.Height - radius * 2, radius * 2, radius * 2, 90, 90); // bottom-left
path.CloseFigure();
Gdiplus::Brush* brush = m_brush_normal;
if (!m_enabled) brush = m_brush_disabled;
else if (m_pressed) brush = m_brush_pressed;
else if (m_hovered) brush = m_brush_hover;
g.FillPath(brush, &path);
g.DrawPath(m_pen_border, &path);
// 绘制文字(居中)
if (!m_text.empty()) {
Gdiplus::StringFormat format;
format.SetAlignment(Gdiplus::StringAlignmentCenter);
format.SetLineAlignment(Gdiplus::StringAlignmentCenter);
Gdiplus::RectF text_rc = rc;
text_rc.Inflate(-8, -4); // 内边距
g.DrawString(m_text.c_str(), -1, m_font, &text_rc, &format,
new Gdiplus::SolidBrush(Gdiplus::Color(0, 0, 0)));
delete static_cast<Gdiplus::SolidBrush*>(brush); // 临时刷子,不删主刷
}
}
```
---
### 🖼️ 主窗口集成(WinMain + WndProc)
```cpp
// 全局变量(生产环境建议封装进类)
HINSTANCE g_hInst = nullptr;
std::unique_ptr<GdiPlusButton> g_pButton;
// GDI+ 启动/退出(全局)
ULONG_PTR g_gdiplusToken = 0;
Gdiplus::GdiplusStartupInput g_gdiplusStartupInput;
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CREATE:
// 初始化 GDI+
Gdiplus::GdiplusStartup(&g_gdiplusToken, &g_gdiplusStartupInput, nullptr);
// 创建按钮(绑定到当前窗口)
g_pButton = std::make_unique<GdiPlusButton>(
hwnd, 50, 50, 160, 40, L"Click Me!");
g_pButton->SetClickCallback([]() {
MessageBox(nullptr, L"Button clicked!", L"Info", MB_OK);
});
break;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
Gdiplus::Graphics g(hdc);
// 绘制背景(可选)
g.Clear(Gdiplus::Color(255, 255, 255));
// 绘制按钮(关键!)
if (g_pButton) {
g_pButton->EnsureGdiPlusResources();
g_pButton->DrawButton(g);
}
EndPaint(hwnd, &ps);
return 0;
}
case WM_MOUSEMOVE:
case WM_LBUTTONDOWN:
case WM_LBUTTONUP:
case WM_CAPTURECHANGED:
if (g_pButton) {
g_pButton->HandleMessage(msg, wParam, lParam);
}
break;
case WM_DESTROY:
g_pButton.reset(); // 析构
Gdiplus::GdiplusShutdown(g_gdiplusToken);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPWSTR lpCmdLine, int nCmdShow) {
g_hInst = hInstance;
// 注册窗口类
WNDCLASSEX wc = { sizeof(WNDCLASSEX) };
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"GdiPlusApp";
wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
RegisterClassEx(&wc);
// 创建窗口
HWND hwnd = CreateWindowEx(
0, L"GdiPlusApp", L"GDI+ Custom Button Demo",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 400, 300,
nullptr, nullptr, hInstance, nullptr);
if (!hwnd) return 1;
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
// 消息循环
MSG msg = {};
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
```
---
### 🔍 关键技术点解释
| 技术点 | 说明 |
|--------|------|
| **无子窗口设计** | 不调用 `CreateWindow` 创建子控件句柄,所有绘制/交互由父窗口统一管理(更轻量、更可控),避免 Z-order / tab order 等复杂性。 |
| **GDI+ 资源管理** | 所有 `Graphics`/`Brush`/`Font` 均手动 `new/delete`,确保跨重绘复用;`EnsureGdiPlusResources()` 延迟初始化防 crash。 |
| **鼠标捕获(`SetCapture`)** | 保证鼠标左键按下后即使移出按钮区域,松开时仍能触发点击(否则会漏事件)。 |
| **圆角矩形绘制** | 使用 `GraphicsPath` + `AddArc` 构造平滑圆角路径,比 `RoundRect` 更灵活(支持任意半径)。 |
| **状态驱动绘制** | `m_hovered`/`m_pressed`/`m_enabled` 三态组合决定颜色与样式,支持视觉反馈。 |
| **文本抗锯齿** | `SetTextRenderingHint(Gdiplus::TextRenderingHintClearTypeGridFit)` 启用 ClearType 渲染,文字更锐利。 |
> 💡 提示:若需多个控件,可维护 `std::vector<std::unique_ptr<GdiPlusButton>> controls`,并在 `WM_PAINT` 和消息循环中批量绘制/转发。
---
### 🚀 进阶方向(供你后续探索)
- ✅ 添加 `TextBox`:监听 `WM_CHAR`/`WM_KEYDOWN`,维护字符串+光标+选区;
- ✅ 添加 `CheckBox`/`RadioButton`:用 `DrawEllipse` + `DrawString` + 状态位;
- ✅ 添加动画:用 `SetTimer` + `InvalidateRect` 实现 hover 淡入/按压缩放;
- ✅ 支持 DPI 缩放:用 `GetDpiForWindow` 动态调整字体大小/边距;
- ✅ 自定义消息路由机制:实现类似 `SendMessage(hWnd, WM_COMMAND, MAKEWPARAM(IDC_BTN, BN_CLICKED), 0)` 的语义。
---