api调试工具增加支持输入请求头

接上一篇,修改下demo, 支持输入请求头, 布局文件修改如下:

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!--界面布局-->
<!-- 窗口设置 -->
<Window roundcorner="5,5" size="800,600"  mininfo="400,300"  caption="0,0,0,36"> <!--caption属性定义标题栏的矩形区域,可拖动区域-->
	<!--定义默认字体-->
  <Font id="0" name="微软雅黑" size="12" bold="false" default="true" shared="true" />
  <Font id="1" name="微软雅黑" size="12" bold="true" shared="true" />
  <Font id="2" name="Consolas" size="13" bold="false" default="false" shared="false" />
  
  <VerticalLayout bkcolor="#FFFFFFFF" bordersize="2" bordercolor="#FF0934DA" name ="root">
	<!--标题栏区域-->
    <HorizontalLayout name="title_bar" height="36" bkcolor="#FF4775CC">
      <HorizontalLayout>
        <Control width="10"/>
        <Label name="apptitle" text="API测试工具 君子之交淡如水" font="1" textcolor="#FFFFFFFF" />
      </HorizontalLayout>  
	  <HorizontalLayout height="25" width="96">
        <Button name="minbtn" tooltip="最小化" height="18" width="26" normalimage="file='minimize.png'" hotimage="file='minimize_h.png'" pushedimage="file='minimize_p.png'"/>
        <Button name="maxbtn" tooltip="最大化" height="18" width="25" normalimage="file='maximize.png'" hotimage="file='maximize_h.png'" pushedimage="file='maximize_p.png'"/>
        <Button name="restorebtn" visible="false" tooltip="恢复" height="18" width="25" normalimage="file='restore.png'" hotimage="file='restore_h.png'" pushedimage="file='restore_p.png'"/> 
	    <Button name="closebtn" tooltip="关闭" height="18" width="43" normalimage="file='close.png'" hotimage="file='close_h.png'" pushedimage="file='close_p.png'"/> 
	  </HorizontalLayout>
    </HorizontalLayout>
  
    <VerticalLayout name="body" padding="10,10,2,2" visible ="false">
		<Text name="text_str" wordbreak="true" text="安能摧眉折腰事权贵,使我不得开心颜!" fontColor="0xFF333333" textPadding="5,5,5,5"/>
		<Control height="10" />
		<Edit name="edit_str" width="120" height="26" textpadding="1,1,1,1" bkcolor="#FFFAEBD7"/>
		<Control height="10" />
		<Button name="btn_test" text="测试" height="30" width="120" normalimage="file='button.png'" hotimage="file='button_h.png'" pushedimage="file='button_p.png'"/> 
	</VerticalLayout>

	  <VerticalLayout name="main_workarea" padding="10,10,10,10" visible ="true" bkcolor="#FFF5F5F5">
		  <!-- 1. URL 行 -->
		  <HorizontalLayout height="32" padding="0,0,0,8">
			  <Label text="URL:" width="40" textalign="center" font="0" textcolor="#FF333333" />
			  <Edit name="url_edit" height="30" padding="4,0,0,0" font="0"
					textpadding="4,0,0,0" bkcolor="#FFFFFFFF" borderround="2,2"
					bordersize="1" bordercolor="#FFCCCCCC" />
		  </HorizontalLayout>

		  <!-- 2. 请求方式 + 发送按钮(发送按钮可以放右边) -->
		  <HorizontalLayout height="36" padding="0,0,0,12">
			  <Label text="请求方式:" width="65" textalign="center" font="0" textcolor="#FF333333" />
			  <Combo name="method_combo" width="100" height="28" font="0"
					 borderround="2,2" bordersize="1" bordercolor="#FFCCCCCC">
				  <ListLabelElement text="GET" selected="true"/>
				  <ListLabelElement text="POST"/>
			  </Combo>
			  <Button name="send_btn" text="🔍 发送请求" width="120" height="30"
					  font="1" textcolor="#FFFFFFFF" bkcolor="#FF4775CC"
					  hotbkcolor="#FF5A8CDE" pushedbkcolor="#FF3A5CA8"
					  borderround="4,4" padding="40,0,0,0" />
		  </HorizontalLayout>
		  
		  <!-- 请求头区域 -->
		  <VerticalLayout height="100" padding="0,0,0,8">
			 <Label text="请求头 (Headers):" height="24" font="0" textcolor="#FF333333" />
			 <RichEdit name="headers_edit" multiline="true" vscrollbar="true" 
						font="2" bkcolor="#FFFFFFFF" bordersize="1" bordercolor="#FFCCCCCC"
						textpadding="4,4,4,4" 
						tip="每行格式:key: value&#10;例如:Authorization: Bearer xyz" />
		  </VerticalLayout>

		  <!-- 3. 请求体(Body)区域 -->
		  <VerticalLayout height="140" padding="0,0,0,12">
			  <Label text="请求体 (Request Body,JSON/文本格式):" height="24" font="0" textcolor="#FF333333" />
			  <RichEdit name="body_edit" multiline="true" vscrollbar="true" wanttab="true" wantreturn="true" nowrap="false" autovscroll="false" height="100"
					font="2" bkcolor="#FFFFFFFF" bordersize="1" bordercolor="#FFCCCCCC"
					textpadding="4,4,4,4"/>
		  </VerticalLayout>

		  <!-- 4. 响应结果区域 -->
		  <VerticalLayout>
			  <Label text="响应结果 (Response):" height="24" font="0" textcolor="#FF333333" />
			  <RichEdit name="result_edit" multiline="true" vscrollbar="true" readonly="true" height ="100"
					font="2" bkcolor="#FFFFFFFF" bordersize="1" bordercolor="#FFCCCCCC"
					textpadding="4,4,4,4" />
		  </VerticalLayout>
		  
	  </VerticalLayout>
  </VerticalLayout>
</Window>

Main.cpp的HttpRequest函数增加请求头参数,修改如下:

cpp 复制代码
/**
 * @brief 通用HTTP请求封装
 * @param url 请求地址(UTF-8)
 * @param method 请求方式 GET/POST
 * @param body POST请求体JSON字符串(可为 nullptr 或空字符串)
 * @param headersStr 自定义请求头,每行格式 "key: value",多条用换行分隔(UTF-8,可为 nullptr)
 * @return 服务器完整响应文本
 */
std::string HttpRequest(const char* url, const char* method, const char* body, const char* headersStr)
{
    Log("进入 HttpRequest 函数");
    // 初始化curl会话
    CURL* curl = curl_easy_init();
    if (!curl)
    {
        return "curl 初始化失败";
    }

    // 在 curl_easy_init 之后添加
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    // 自定义调试回调,将输出重定向到 OutputDebugString
    curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION,
        [](CURL* handle, curl_infotype type, char* data, size_t size, void* userptr) -> int {
            std::string info(data, size);
            OutputDebugStringA(info.c_str());
            return 0;
        });

    std::string response;

    // 设置请求地址
    curl_easy_setopt(curl, CURLOPT_URL, url);

    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 关闭证书验证
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); // 关闭主机验证

    // 允许跟随重定向
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    // 设置接收数据的回调函数
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
    // 把response指针传给回调函数
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    // 整个 libcurl 请求允许执行的最长时间(秒数),包括连接建立、数据传输等所有阶段。
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);

    // 如果是POST请求且有请求体
    if (_stricmp(method, "POST") == 0 && body && strlen(body) > 0)
    {
        // 启用POST方式
        curl_easy_setopt(curl, CURLOPT_POST, 1L);
        // 设置POST提交的表单/JSON数据
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    }
    if (_stricmp(method, "GET") != 0 && _stricmp(method, "POST") != 0) {
        // 如果方法不是 GET/POST,可以按需要扩展
        // 这里简单返回错误
        curl_easy_cleanup(curl);
        return std::string(u8"不支持的方法: ") + method;
    }

    // 处理请求头。 注意跨平台换行符问题:DuiLib 的 RichEdit 控件在不同系统或配置下可能返回 \r 作为换行符,而 std::getline 默认以 \n 分隔,导致 \r 残留在行内,破坏 HTTP 协议格式。
    struct curl_slist* headers = nullptr;
    if (headersStr && strlen(headersStr) > 0) {
        std::string cleaned(headersStr);
        // 将所有的 '\r' 替换为 '\n'
        std::replace(cleaned.begin(), cleaned.end(), '\r', '\n');

        // 打印请求头,调试用
        Log("Raw headersStr hex:");
        for (const char* p = headersStr; *p; ++p) {
            char buf[10];
            sprintf_s(buf, "%02X ", (unsigned char)*p);
            Log(buf);
        }

        std::istringstream stream(cleaned);
        std::string line;
        while (std::getline(stream, line)) {
            // 此时 line 中可能还有残留的 '\r'(如果有 "\r\n" 的情况),再清理一次
            if (!line.empty() && line.back() == '\r') line.pop_back();
            // 去除首尾空白
            line.erase(0, line.find_first_not_of(" \t\r\n"));
            line.erase(line.find_last_not_of(" \t\r\n") + 1);
            if (line.empty()) continue;
            // 查找冒号分隔符
            size_t colon = line.find(':');
            if (colon != std::string::npos) {
                std::string key = line.substr(0, colon);
                std::string value = line.substr(colon + 1);
                // 去除 value 前导空白
                value.erase(0, value.find_first_not_of(" \t"));
                // 构造完整的头字段
                std::string header = key + ": " + value;
                headers = curl_slist_append(headers, header.c_str());
            }
            else {
                // 如果没有冒号,忽略这一行
                OutputDebugStringA("Ignored header line (no colon):   what happened? \n心口如一,犹不失为光明磊落丈夫之行也。");
            }
        }
    }
    else {
        headers = curl_slist_append(headers, "Content-Type: application/json");
    }
    if (headers) {
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    }

    // 执行同步HTTP请求
    CURLcode res = curl_easy_perform(curl);
    Log("HttpRequest: after curl_easy_perform, res=" + std::to_string(res));

    // 请求出错
    if (res != CURLE_OK)
    {
        //response = "请求失败: " + std::string(curl_easy_strerror(res));

        const char* errUtf8 = curl_easy_strerror(res);
        Log("HttpRequest error: " + std::string(errUtf8));
        response = std::string(u8"请求失败: ") + errUtf8; // 拼接 UTF-8 中文 + UTF-8 错误信息
    }
    else {
        Log("HttpRequest success, response length=" + std::to_string(response.size()));
    }

    // 清理
    if (headers) {
        curl_slist_free_all(headers);
    }
    curl_easy_cleanup(curl);
    return response;
}

点击发送时,获取请求头:

cpp 复制代码
// 4.获取请求头
CRichEditUI* headersEdit = static_cast<CRichEditUI*>(m_pm.FindControl(_T("headers_edit")));
CDuiString headersW = headersEdit ? headersEdit->GetText() : _T("");

发送请求时,把请求头也传入HttpRequest函数:

cpp 复制代码
// 执行HTTP请求
std::string respResult = HttpRequest(url.c_str()
    , method.c_str()
    , body.c_str()
    , headers.empty() ? nullptr : headers.c_str());

修改下server.js , 增加一个/api/echo接口,把获取到的请求头再返回客户端,证明服务端接受到请求头了:

javascript 复制代码
const http = require('http');

// 内存模拟数据库 
let users = [];
let likes = [];
let userIdAutoIncrement = 1;

const server = http.createServer((req, res) => {
	console.log('=== Incoming request ===');
    console.log('method:', req.method, 'url:', req.url);
    console.log('headers:', req.headers);
	
	req.on('error', (err) => {
        console.error('Request error:', err);
    });
	
    // 通用响应头
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Content-Type', 'application/json');

    // -------------------------------------------------------------------------
    // 1. POST 注册接口
    // -------------------------------------------------------------------------
    if (req.method === 'POST' && req.url === '/api/register') {
        let data = '';
        req.on('data', chunk => data += chunk);
        req.on('end', () => {
            try {
                const body = JSON.parse(data);
                const { username, password, nickname, gender, age, city } = body;

                if (!username || !password) {
                    return res.end(JSON.stringify({ code: 400, msg: '账号密码不能为空' }));
                }

                // 检查是否已注册
                const exists = users.find(u => u.username === username);
                if (exists) {
                    return res.end(JSON.stringify({ code: 400, msg: '用户名已存在' }));
                }

                // 注册
                const user = {
                    id: userIdAutoIncrement++,
                    username,
                    password,
                    nickname: nickname || '用户' + userIdAutoIncrement,
                    gender: gender || '未知',
                    age: age || 0,
                    city: city || '保密'
                };
                users.push(user);

                res.end(JSON.stringify({
                    code: 200,
                    msg: '注册成功',
                    userId: user.id
                }));
            } catch (e) {
                res.end(JSON.stringify({ code: 400, msg: '参数格式错误' }));
            }
        });
        return;
    }

    // -------------------------------------------------------------------------
    // 2. POST 登录接口
    // -------------------------------------------------------------------------
    if (req.method === 'POST' && req.url === '/api/login') {
        let data = '';
        req.on('data', chunk => data += chunk);
        req.on('end', () => {
            try {
                const body = JSON.parse(data);
                const { username, password } = body;

                const user = users.find(u => u.username === username && u.password === password);
                if (!user) {
                    return res.end(JSON.stringify({ code: 401, msg: '账号或密码错误' }));
                }

                res.end(JSON.stringify({
                    code: 200,
                    msg: '登录成功',
                    token: 'token-' + user.id,
                    userId: user.id
                }));
            } catch (e) {
                res.end(JSON.stringify({ code: 400, msg: '参数错误' }));
            }
        });
        return;
    }

    // -------------------------------------------------------------------------
    // 3. GET 获取个人资料
    // -------------------------------------------------------------------------
    if (req.method === 'GET' && req.url.startsWith('/api/profile')) {
        const url = new URL(req.url, 'http://localhost:3000');
        const userId = url.searchParams.get('userId');

        if (!userId) {
            return res.end(JSON.stringify({ code: 400, msg: '缺少userId' }));
        }

        const user = users.find(u => u.id == userId);
        if (!user) {
            return res.end(JSON.stringify({ code: 404, msg: '用户不存在' }));
        }

        res.end(JSON.stringify({
            code: 200,
            data: {
                id: user.id,
                nickname: user.nickname,
                gender: user.gender,
                age: user.age,
                city: user.city
            }
        }));
        return;
    }

    // -------------------------------------------------------------------------
    // 4. GET 查询异性推荐列表
    // -------------------------------------------------------------------------
    if (req.method === 'GET' && req.url.startsWith('/api/user/recommend')) {
        const url = new URL(req.url, 'http://localhost:3000');
        const gender = url.searchParams.get('gender'); // 想要看的性别:男/女

        if (!gender) {
            return res.end(JSON.stringify({ code: 400, msg: '请指定要查询的性别' }));
        }

        const list = users
            .filter(u => u.gender === gender)
            .map(u => ({
                id: u.id,
                nickname: u.nickname,
                age: u.age,
                city: u.city
            }));

        res.end(JSON.stringify({
            code: 200,
            count: list.length,
            list: list
        }));
        return;
    }

    // -------------------------------------------------------------------------
    // 5. POST 发送喜欢(打招呼)
    // -------------------------------------------------------------------------
    if (req.method === 'POST' && req.url === '/api/user/like') {
        let data = '';
        req.on('data', chunk => data += chunk);
        req.on('end', () => {
            try {
                const body = JSON.parse(data);
                const { fromUserId, toUserId } = body;

                if (!fromUserId || !toUserId) {
                    return res.end(JSON.stringify({ code: 400, msg: '缺少参数' }));
                }
                if (fromUserId == toUserId) {
                    return res.end(JSON.stringify({ code: 400, msg: '不能喜欢自己' }));
                }

                const fromUser = users.find(u => u.id == fromUserId);
                const toUser = users.find(u => u.id == toUserId);
                if (!fromUser || !toUser) {
                    return res.end(JSON.stringify({ code: 404, msg: '用户不存在' }));
                }

                // 记录喜欢
                likes.push({ fromUserId, toUserId, time: new Date() });
                res.end(JSON.stringify({ code: 200, msg: `你已成功喜欢 ${toUser.nickname}` }));
            } catch (e) {
                res.end(JSON.stringify({ code: 400, msg: '参数错误' }));
            }
        });
        return;
    }
	
	// 6. 测试请求头回显
	if (req.url === '/api/echo') { // 调试接口:回显请求头(用于测试)
		try {
			console.log('收到请求 /api/echo');
			let bodyData = '';
			req.on('data', chunk => bodyData += chunk);
			req.on('end', () => {
				let parsedBody = null;
				if (bodyData) {
					try {
						parsedBody = JSON.parse(bodyData);
					} catch (e) {
						parsedBody = bodyData; // 非 JSON 原样保留
					}
				}
				res.end(JSON.stringify({
					code: 200,
					msg: 'Headers Echo',
					method: req.method,
					url: req.url,
					headers: req.headers,      // 直接返回请求头对象.
					body: parsedBody || null
				}, null, 2));
			});
		}catch (err) {
			console.error('Echo 接口错误:', err);
			res.end(JSON.stringify({ code: 500, msg: 'Internal Error' }));
		}
		return;
	}

    // 404
	console.log('404');
    res.writeHead(404);
    res.end(JSON.stringify({ code: 404, msg: '接口不存在' }));
});

// 启动日志(完全规范、无歧义、不暴露变量)
server.listen(3000, () => {
    console.log('相亲交友接口服务已启动:http://localhost:3000');
    console.log('');
    console.log('POST 注册        /api/register');
    console.log('   参数:username, password, nickname, gender, age, city');
    console.log('');
    console.log(' POST 登录        /api/login');
    console.log('   参数:username, password');
    console.log('');
    console.log(' GET  个人资料    /api/profile?userId=xxx');
    console.log('');
    console.log(' GET  推荐异性    /api/user/recommend?gender=女');
    console.log('');
    console.log(' POST 发送喜欢    /api/user/like');
    console.log('   参数:fromUserId, toUserId');
	console.log('龙能大能小,能升能隐;大则兴云吐雾,小则隐介藏形;升则飞腾于宇宙之间,隐则潜伏于波涛之内。方今春深,龙乘时变化,犹人得志而纵横四海。龙之为物,可比世之英雄。');
});

测试,请求url为:http://localhost:3000/api/echo

请求头:

X-Test: i love vicky du

Authorization: Bearer abc123

Content-Type: application/json

把自己铸造成器,方才可以希望有益于社会。真实的为我,便是最有益的为人。

post请求方式

请求体:

{

"userName": "灭绝师太",

"age": 90

}

返回:

javascript 复制代码
{
  "code": 200,
  "msg": "Headers Echo",
  "method": "POST",
  "url": "/api/echo",
  "headers": {
    "host": "localhost:3000",
    "accept": "*/*",
    "x-test": "i love vicky du",
    "authorization": "Bearer abc123",
    "content-type": "application/json",
    "content-length": "46"
  },
  "body": {
    "userName": "灭绝师太",
    "age": 90
  }
}

界面如下:

ok. 看下日志打印:

bash 复制代码
"DuiLibDemo1.exe"(Win32): 已加载"E:\cpp\projects\DuiLibDemo1\DuiLibDemo1\x64\Debug\DuiLibDemo1.exe"。已加载符号。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\ntdll.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\kernel32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\KernelBase.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\user32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\win32u.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"E:\cpp\projects\DuiLibDemo1\DuiLibDemo1\x64\Debug\duilib.dll"。已加载符号。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\gdi32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\gdi32full.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\msvcp_win.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\ucrtbase.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\ole32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\WinSxS\amd64_microsoft.windows.common-controls_6595b64144ccf1df_6.0.19041.6456_none_60b8a6cb71f64256\comctl32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\rpcrt4.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\msvcrt.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\combase.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"E:\cpp\projects\DuiLibDemo1\DuiLibDemo1\x64\Debug\libcurl-d.dll"。已加载符号。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\oleaut32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\msvcp140d.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\ws2_32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\vcruntime140d.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\imm32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\bcrypt.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\shlwapi.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\vcruntime140_1d.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\advapi32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\ucrtbased.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\sechost.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"E:\cpp\projects\DuiLibDemo1\DuiLibDemo1\x64\Debug\zlibd1.dll"。已加载符号。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\crypt32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\WinSxS\amd64_microsoft.windows.gdiplus_6595b64144ccf1df_1.1.19041.6456_none_91a399b0cc88bc24\GdiPlus.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\msimg32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\uxtheme.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\msctf.dll"。包含/排除设置已禁用符号加载。
Create Control: VerticalLayout
Create Control: HorizontalLayout
Create Control: HorizontalLayout
Create Control: Control
Create Control: Label
Create Control: HorizontalLayout
Create Control: Button
Create Control: Button
Create Control: Button
Create Control: Button
Create Control: VerticalLayout
Create Control: Text
Create Control: Control
Create Control: Edit
Create Control: Control
Create Control: Button
Create Control: VerticalLayout
Create Control: HorizontalLayout
Create Control: Label
Create Control: Edit
Create Control: HorizontalLayout
Create Control: Label
Create Control: Combo
Create Control: ListLabelElement
Create Control: ListLabelElement
Create Control: Button
Create Control: VerticalLayout
Create Control: Label
Create Control: RichEdit
Create Control: VerticalLayout
Create Control: Label
Create Control: RichEdit
Create Control: VerticalLayout
Create Control: Label
Create Control: RichEdit
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\kernel.appcore.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\bcryptprimitives.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\TextInputFramework.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\CoreMessaging.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\CoreUIComponents.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\SHCore.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\ntmarta.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\WinTypes.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\WinTypes.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已卸载"C:\Windows\System32\WinTypes.dll"
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\msftedit.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\TextShaping.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\clbcatq.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\Windows.Globalization.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\BCP47mrm.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\BCP47Langs.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\globinputhost.dll"。包含/排除设置已禁用符号加载。
进入 HttpRequest 函数
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\secur32.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\sspicli.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\IPHLPAPI.DLL"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\mswsock.dll"。包含/排除设置已禁用符号加载。
HttpRequest: after curl_easy_perform, res=0
HttpRequest success, response length=209
Response raw: {
  "code": 200,
  "msg": "Headers Echo",
  "method": "GET",
  "url": "/api/echo",
  "headers": {
    "host": "localhost:3000",
    "accept": "*/*",
    "content-type": "application/json"
  },
  "body
线程 17148 已退出,返回值为 0 (0x0)。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\Windows.UI.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\WindowManagementAPI.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\InputHost.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\propsys.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\twinapi.appcore.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\twinapi.appcore.dll"。包含/排除设置已禁用符号加载。
"DuiLibDemo1.exe"(Win32): 已卸载"C:\Windows\System32\twinapi.appcore.dll"
"DuiLibDemo1.exe"(Win32): 已加载"C:\Windows\System32\edputil.dll"。包含/排除设置已禁用符号加载。
进入 HttpRequest 函数
Raw headersStr hex:
58 
2D 
54 
65 
73 
74 
3A 
20 
69 
20 
6C 
6F 
76 
65 
20 
76 
69 
63 
6B 
79 
20 
64 
75 
0D 
41 
75 
74 
68 
6F 
72 
69 
7A 
61 
74 
69 
6F 
6E 
3A 
20 
42 
65 
61 
72 
65 
72 
20 
61 
62 
63 
31 
32 
33 
0D 
43 
6F 
6E 
74 
65 
6E 
74 
2D 
54 
79 
70 
65 
3A 
20 
61 
70 
70 
6C 
69 
63 
61 
74 
69 
6F 
6E 
2F 
6A 
73 
6F 
6E 
0D 
E6 
8A 
8A 
E8 
87 
AA 
E5 
B7 
B1 
E9 
93 
B8 
E9 
80 
A0 
E6 
88 
90 
E5 
99 
A8 
EF 
BC 
8C 
E6 
96 
B9 
E6 
89 
8D 
E5 
8F 
AF 
E4 
BB 
A5 
E5 
B8 
8C 
E6 
9C 
9B 
E6 
9C 
89 
E7 
9B 
8A 
E4 
BA 
8E 
E7 
A4 
BE 
E4 
BC 
9A 
E3 
80 
82 
E7 
9C 
9F 
E5 
AE 
9E 
E7 
9A 
84 
E4 
B8 
BA 
E6 
88 
91 
EF 
BC 
8C 
E4 
BE 
BF 
E6 
98 
AF 
E6 
9C 
80 
E6 
9C 
89 
E7 
9B 
8A 
E7 
9A 
84 
E4 
B8 
BA 
E4 
BA 
BA 
E3 
80 
82 
Ignored header line (no colon):   what happened? 
心口如一,犹不失为光明磊落丈夫之行也。HttpRequest: after curl_easy_perform, res=0
HttpRequest success, response length=356
Response raw: {
  "code": 200,
  "msg": "Headers Echo",
  "method": "POST",
  "url": "/api/echo",
  "headers": {
    "host": "localhost:3000",
    "accept": "*/*",
    "x-test": "i love vicky du",
    "authorizatio
线程 8428 已退出,返回值为 0 (0x0)。
线程 16888 已退出,返回值为 0 (0x0)。
线程 15488 已退出,返回值为 0 (0x0)。
线程 16872 已退出,返回值为 0 (0x0)。
线程 14712 已退出,返回值为 0 (0x0)。
线程 4392 已退出,返回值为 0 (0x0)。
线程 15516 已退出,返回值为 0 (0x0)。
线程 9992 已退出,返回值为 0 (0x0)。
线程 10936 已退出,返回值为 0 (0x0)。

ok. 再贴一下完整代码:

cpp 复制代码
// 必须放在所有头文件包含之前. 预处理指令,编译优化,排除windows头文件(windows.h)中较少使用的组件
#define WIN32_LEAN_AND_MEAN	
// 必须放在所有头文件包含之前.  兼容性控制宏. 禁用微软对不安全函数(如scanf、strcpy等)的编译警告
#define _CRT_SECURE_NO_DEPRECATE

#include <iostream>
#include <algorithm>
#include <unordered_set>
#include <unordered_map>
#include <map>
#include <string>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <thread>
#include <atomic>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <queue>
#include <sstream>
#include <exception>
#include <format>
#include <stack>

#include <duilib/UIlib.h>
#include <windows.h>
#include <objbase.h>
#include <duilib/Core/UIContainer.h>

// libcurl 相关头文件
#include <curl/curl.h>

// JSON 解析库
#include <nlohmann/json.hpp>

using namespace DuiLib;
using json = nlohmann::json;

void Log(const std::string& msg) {
    std::string out = msg + "\n";
    OutputDebugStringA(out.c_str());
}


/**
 * @brief libcurl 数据接收回调函数
 * @param contents 服务器返回的数据块
 * @param size 单个数据块字节大小
 * @param nmemb 数据块个数
 * @param s 外部传入的string,用来拼接完整响应
 * @return 已处理的字节数
 */
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* s)
{
    size_t newLength = size * nmemb;
    try
    {
        // 把返回的数据追加到string中
        s->append((char*)contents, newLength);
    }
    catch (std::bad_alloc& e)
    {
        // 内存分配失败,返回0终止请求
        return 0;
    }
    return newLength;
}

/**
 * @brief 通用HTTP请求封装
 * @param url 请求地址(UTF-8)
 * @param method 请求方式 GET/POST
 * @param body POST请求体JSON字符串(可为 nullptr 或空字符串)
 * @param headersStr 自定义请求头,每行格式 "key: value",多条用换行分隔(UTF-8,可为 nullptr)
 * @return 服务器完整响应文本
 */
std::string HttpRequest(const char* url, const char* method, const char* body, const char* headersStr)
{
    Log("进入 HttpRequest 函数");
    // 初始化curl会话
    CURL* curl = curl_easy_init();
    if (!curl)
    {
        return "curl 初始化失败";
    }

    // 在 curl_easy_init 之后添加
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    // 自定义调试回调,将输出重定向到 OutputDebugString
    curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION,
        [](CURL* handle, curl_infotype type, char* data, size_t size, void* userptr) -> int {
            std::string info(data, size);
            OutputDebugStringA(info.c_str());
            return 0;
        });

    std::string response;

    // 设置请求地址
    curl_easy_setopt(curl, CURLOPT_URL, url);

    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 关闭证书验证
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); // 关闭主机验证

    // 允许跟随重定向
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
    // 设置接收数据的回调函数
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
    // 把response指针传给回调函数
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
    // 整个 libcurl 请求允许执行的最长时间(秒数),包括连接建立、数据传输等所有阶段。
    curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);

    // 如果是POST请求且有请求体
    if (_stricmp(method, "POST") == 0 && body && strlen(body) > 0)
    {
        // 启用POST方式
        curl_easy_setopt(curl, CURLOPT_POST, 1L);
        // 设置POST提交的表单/JSON数据
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
    }
    if (_stricmp(method, "GET") != 0 && _stricmp(method, "POST") != 0) {
        // 如果方法不是 GET/POST,可以按需要扩展
        // 这里简单返回错误
        curl_easy_cleanup(curl);
        return std::string(u8"不支持的方法: ") + method;
    }

    // 处理请求头。 注意跨平台换行符问题:DuiLib 的 RichEdit 控件在不同系统或配置下可能返回 \r 作为换行符,而 std::getline 默认以 \n 分隔,导致 \r 残留在行内,破坏 HTTP 协议格式。
    struct curl_slist* headers = nullptr;
    if (headersStr && strlen(headersStr) > 0) {
        std::string cleaned(headersStr);
        // 将所有的 '\r' 替换为 '\n'
        std::replace(cleaned.begin(), cleaned.end(), '\r', '\n');

        // 打印请求头,调试用
        Log("Raw headersStr hex:");
        for (const char* p = headersStr; *p; ++p) {
            char buf[10];
            sprintf_s(buf, "%02X ", (unsigned char)*p);
            Log(buf);
        }

        std::istringstream stream(cleaned);
        std::string line;
        while (std::getline(stream, line)) {
            // 此时 line 中可能还有残留的 '\r'(如果有 "\r\n" 的情况),再清理一次
            if (!line.empty() && line.back() == '\r') line.pop_back();
            // 去除首尾空白
            line.erase(0, line.find_first_not_of(" \t\r\n"));
            line.erase(line.find_last_not_of(" \t\r\n") + 1);
            if (line.empty()) continue;
            // 查找冒号分隔符
            size_t colon = line.find(':');
            if (colon != std::string::npos) {
                std::string key = line.substr(0, colon);
                std::string value = line.substr(colon + 1);
                // 去除 value 前导空白
                value.erase(0, value.find_first_not_of(" \t"));
                // 构造完整的头字段
                std::string header = key + ": " + value;
                headers = curl_slist_append(headers, header.c_str());
            }
            else {
                // 如果没有冒号,忽略这一行
                OutputDebugStringA("Ignored header line (no colon):   what happened? \n心口如一,犹不失为光明磊落丈夫之行也。");
            }
        }
    }
    else {
        headers = curl_slist_append(headers, "Content-Type: application/json");
    }
    if (headers) {
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    }

    // 执行同步HTTP请求
    CURLcode res = curl_easy_perform(curl);
    Log("HttpRequest: after curl_easy_perform, res=" + std::to_string(res));

    // 请求出错
    if (res != CURLE_OK)
    {
        //response = "请求失败: " + std::string(curl_easy_strerror(res));

        const char* errUtf8 = curl_easy_strerror(res);
        Log("HttpRequest error: " + std::string(errUtf8));
        response = std::string(u8"请求失败: ") + errUtf8; // 拼接 UTF-8 中文 + UTF-8 错误信息
    }
    else {
        Log("HttpRequest success, response length=" + std::to_string(response.size()));
    }

    // 清理
    if (headers) {
        curl_slist_free_all(headers);
    }
    curl_easy_cleanup(curl);
    return response;
}


// TODO 还需要设置字符集?

/*
class TestWindow : public CWindowWnd, public INotifyUI {
public:
    LPCTSTR GetWindowClassName() const override {
        return _T("TestWindowClass");
    }
    UINT GetClassStyle() const override {
        return CS_DBLCLKS;
    }
    void OnFinalMessage(HWND hWnd) override {
        delete this;
    }
    void Notify(TNotifyUI& msg) override {}
    LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override {
        if (uMsg == WM_CLOSE) {
            PostQuitMessage(0);
            return 0;
        }
        return __super::HandleMessage(uMsg, wParam, lParam);
    }
};
*/

/*
int main() {
	// 创建窗口
	TestWindow wnd;
	wnd.Create(nullptr, _T("Test"), UI_WNDSTYLE_FRAME, 0, 0, 500, 400);
	wnd.ShowModal();
}
*/

/*
LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    if (uMsg == WM_DESTROY) {
        PostQuitMessage(0);
        return 0;
    }
    return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
*/

/*
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    const wchar_t CLASS_NAME[] = L"MyWindowClass";

    WNDCLASS wc = {};
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;

    RegisterClass(&wc);

    HWND hwnd = CreateWindowEx(
        0,
        CLASS_NAME,
        L"到底是天地会还是整人会啊",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 400, 300,
        NULL, NULL, hInstance, NULL
    );

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    MSG msg = {};
    while (GetMessage(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return 0;
}
*/

// 从WindowImplBase继承创建窗口类
/*
class CTestApp : public WindowImplBase {
public:
    virtual LPCTSTR GetWindowClassName() const {
        return _T("TestAppWindow");
    }

    virtual CDuiString GetSkinFile() {
        return _T("test1.xml"); // 布局文件
    }

    virtual void Notify(TNotifyUI& msg) {
        if (msg.sType == _T("click")) {
            if (msg.pSender->GetName() == _T("closebtn")) {
                Close();
            }
        }
    }

    virtual UILIB_RESOURCETYPE GetResourceType() const {
        return UILIB_FILE;  // 或UILIB_ZIPRESOURCE等
    }

    virtual CControlUI* CreateControl(LPCTSTR pstrClass) {
        return nullptr;  // 默认返回空,需根据实际需求处理
    }

    virtual CDuiString GetSkinFolder() {
        return CPaintManagerUI::GetInstancePath() + _T(".\\res\\");
    }
};
*/

// _tWinMain是Windows GUI应用程序的入口函数
/*
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {
    CPaintManagerUI::SetInstance(hInstance);  // 设置实例句柄
    CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath() + _T(".\\res\\"));    // 设置资源路径(布局文件路径)

    // 创建并显示窗口
    CTestApp app;
    app.Create(NULL, _T("TestApp"), UI_WNDSTYLE_FRAME, 0);
    app.CenterWindow();
    app.ShowModal();
    return 0;
}
*/


//===================================================================================

//const TCHAR* const kTitleControlName = _T("apptitle");
//const TCHAR* const kCloseButtonControlName = _T("closebtn");
//const TCHAR* const kMinButtonControlName = _T("minbtn");
//const TCHAR* const kMaxButtonControlName = _T("maxbtn");
//const TCHAR* const kRestoreButtonControlName = _T("restorebtn");


// 窗口实例及消息相应部分
class CFrameWindowWnd : public CWindowWnd, public INotifyUI
{
public:
    CFrameWindowWnd() {};

    // 窗口唯一标识符
    LPCTSTR GetWindowClassName() const { return _T("UIMainFrame"); };

    // 定义窗口类样式。 UI_CLASSSTYLE_FRAME:窗口大小改变时触发重绘,通常用于带标题栏、边框的标准窗体。
    // CS_DBLCLKS‌:Windows 标准样式,启用窗口对鼠标双击消息的响应能力
    UINT GetClassStyle() const { return UI_CLASSSTYLE_FRAME | CS_DBLCLKS; };

    // 处理窗口生命周期终结
    void OnFinalMessage(HWND /*hWnd*/) { delete this; };

    // 核心功能:控件事件分发、消息传递枢纽
    void Notify(TNotifyUI& msg) override
    {
        if (msg.sType == _T("click")) {
            //if (msg.pSender->GetName() == _T("I服了You")) { // 即点击的根控件root,代码见下面
            //    Close(); // 关闭窗口
            //}

            if (msg.pSender->GetName() == _T("closebtn")) { // 点击关闭窗口
                Close();
            } else if (msg.pSender->GetName() == _T("minbtn")) { // 最小化
                //ShowWindow(m_hWnd, SW_MINIMIZE); // 不起作用
                ::ShowWindow(m_hWnd, SW_MINIMIZE); // 通过全局作用域符强制调用Win32 API函数
            }
            else if (msg.pSender->GetName() == _T("maxbtn")) { // 最大化
                ::ShowWindow(m_hWnd, SW_MAXIMIZE);
                // 隐藏最大化按钮
                CButtonUI* maxBtn = static_cast<CButtonUI*>(m_pm.FindControl(_T("maxbtn")));
                if (maxBtn) {
                    maxBtn->SetVisible(false);
                }
                // 显示恢复按钮
                CButtonUI* restoreBtn = static_cast<CButtonUI*>(m_pm.FindControl(_T("restorebtn")));
                if (restoreBtn) {
                    restoreBtn->SetVisible(true);
                }
            }
            else if (msg.pSender->GetName() == _T("restorebtn")) { // 恢复
                ::ShowWindow(m_hWnd, SW_RESTORE);
                CControlUI* restoreBtn = static_cast<CControlUI*>(m_pm.FindControl(_T("restorebtn")));
                if (restoreBtn) restoreBtn->SetVisible(false);
                CControlUI* maxBtn = static_cast<CControlUI*>(m_pm.FindControl(_T("maxbtn")));
                if (maxBtn) maxBtn->SetVisible(true);
            } 
            else if (msg.pSender->GetName() == _T("btn_test")) {
                // 测试读取输入框文本:
                CEditUI* editStr = static_cast<CEditUI*>(m_pm.FindControl(_T("edit_str")));
                if (editStr) {
                    CDuiString strText = editStr->GetText(); // 获取文本内容
                    CTextUI* textStr = static_cast<CTextUI*>(m_pm.FindControl(_T("text_str")));
                    if (textStr) textStr->SetText(strText); // 刷新文本组件

                    // 测试显示消息盒
                    MessageBox(m_hWnd, L"安能摧眉折腰事权贵,使我不得开心颜!", L"消息标题", MB_OK | MB_ICONINFORMATION);
                }
            }
            else if (msg.pSender->GetName() == _T("send_btn")) {
                // 1. 获取URL输入框控件 & 文本
                CEditUI* urlEdit = static_cast<CEditUI*>(m_pm.FindControl(_T("url_edit")));
                CDuiString urlW = urlEdit->GetText();

                // 2. 获取下拉框选中的请求方式 GET/POST
                CComboUI* combo = static_cast<CComboUI*>(m_pm.FindControl(_T("method_combo")));
                int selIdx = combo->GetCurSel();
                CListLabelElementUI* pItem = (CListLabelElementUI*)combo->GetItemAt(selIdx);
                CDuiString methodW = pItem ? pItem->GetText() : _T("GET");

                // 3. 获取请求体RichEdit输入内容
                CRichEditUI* bodyEdit = static_cast<CRichEditUI*>(m_pm.FindControl(_T("body_edit")));
                CDuiString bodyW = bodyEdit->GetText();

                // 4.获取请求头
                CRichEditUI* headersEdit = static_cast<CRichEditUI*>(m_pm.FindControl(_T("headers_edit")));
                CDuiString headersW = headersEdit ? headersEdit->GetText() : _T("");

                // 宽字符转窄字符,给libcurl使用
                auto WideToMulti = [](const std::wstring& wstr) -> std::string {
                    if (wstr.empty()) return "";
                    int len = WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), NULL, 0, NULL, NULL);
                    std::string res(len, 0);
                    WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), (int)wstr.size(), &res[0], len, NULL, NULL);
                    return res;
                    };
                std::string url = WideToMulti(urlW.GetData());
                std::string method = WideToMulti(methodW.GetData());
                std::string body = WideToMulti(bodyW.GetData());
                std::string headers = WideToMulti(headersW.GetData());

                // 4. 开子线程执行网络请求,可以避免阻塞UI主线程导致窗口卡死
                std::thread([=, this]() // 这里的等于号,意思是值捕获,以"值拷贝"的方式,捕获外部所有变量。 this是当前类的this指针
                    {
                        // 执行HTTP请求
                        std::string respResult = HttpRequest(url.c_str()
                            , method.c_str()
                            , body.c_str()
                            , headers.empty() ? nullptr : headers.c_str());

                        try
                        {
                            // 测试下解析 JSON
                            Log("Response raw: " + respResult.substr(0, 200));
                            json jsonStr = json::parse(respResult);

                            // 取值
                            int code = jsonStr["code"];
                            std::cout << "code :" << code << std::endl;
                        }
                        catch (const std::exception& e)
                        {
                            std::cerr << "解析异常: " << string(e.what()) << endl;
                        }

                        // 发送自定义消息到主线程,更新界面
                        // 不能在子线程直接操作Duilib控件
                         // PostMessage前面的:: 意思是调用全局的 Win32 API 函数,不调用该类里的同名函数
                        // 不直接传respResult, respResult是局部变量,出了作用域就销毁了。
                        ::PostMessage(m_hWnd, WM_USER + 100, 0, (LPARAM)new std::string(respResult));
                    }).detach(); // 分离线程,自动回收资源

                // 先给结果框显示 请求中 提示
                CRichEditUI* resultEdit = static_cast<CRichEditUI*>(m_pm.FindControl(_T("result_edit")));
                if (resultEdit)
                {
                    resultEdit->SetText(_T("请求中,请稍候..."));
                }
            }
        }
    }

    LRESULT HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
    {
        // 自定义消息:子线程请求完成,更新响应结果到界面
        if (uMsg == WM_USER + 100)
        {
            std::string* pResp = reinterpret_cast<std::string*>(lParam);
            CRichEditUI* resultEdit = static_cast<CRichEditUI*>(m_pm.FindControl(_T("result_edit")));
            if (resultEdit && pResp)
            {
                // 窄字符转宽字符显示
                auto UTF8ToWide = [](const std::string& str) -> std::wstring {
                    if (str.empty()) return L"";
                    int len = MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), NULL, 0);
                    std::wstring res(len, L'\0');
                    MultiByteToWideChar(CP_UTF8, 0, str.c_str(), (int)str.size(), &res[0], len);
                    return res;
                    };
                std::wstring showText = UTF8ToWide(*pResp);
                resultEdit->SetText(showText.c_str());
            }
            // 释放堆内存
            delete pResp;
            return 0;
        }

        if (uMsg == WM_CREATE) { // 成功创建窗口后, 系统会立即发送 WM_CREATE 消息, 标志着窗口句柄已有效但尚未显示
            m_pm.Init(m_hWnd); // 绑定窗口句柄,初始化绘图环境
            //CControlUI* root = new CButtonUI;
            //root->SetName(_T("I服了You"));
            //root->SetBkColor(0xFFcccccc);
            //m_pm.AttachDialog(root); // 关联控件树根节点,启动界面渲染. 根控件与绘图管理器关联. 这里根控件为root

            // 这里用xml文件作为页面布局:
            CDialogBuilder builder;
            CControlUI* root = builder.Create(_T("test2.xml"), 0, nullptr, &m_pm);
            ASSERT(root && "Failed to parse XML");
            m_pm.AttachDialog(root);

            m_pm.AddNotifier(this); // 注册事件监听器,接收控件通知(如 Notify 消息)
            return 0;
        }
        else if (uMsg == WM_DESTROY) { // 窗口销毁
            ::PostQuitMessage(0); // 0表示正常退出
        }
        // 不想使用系统的标题栏和边框这些非客户区绘制,加上下面这俩分支(WM_NCACTIVATE、WM_NCCALCSIZE、WM_NCPAINT 消息)的处理
        else if (uMsg == WM_NCACTIVATE) { // 窗口获得焦点或失去焦点时收到该消息
            if (!::IsIconic(m_hWnd)) {
                // TRUE: 允许系统将非客户区绘制为失活状态(灰色标题栏)
                // FALSE: 阻止系统默认的激活状态绘制,可能用于实现自定义标题栏样式
                return (wParam == 0) ? TRUE : FALSE;
            }
        }
        else if (uMsg == WM_NCCALCSIZE) { // 当窗口大小或位置发生变化时,系统会发送该消息
            return 0; // 返回0屏蔽系统默认的标题栏计算. 这使得开发者可以用客户区完全模拟自定义标题栏,实现更灵活的界面设计
        }
        else if (uMsg == WM_NCPAINT) { // 用于非客户区绘制的消息,在duilib中主要用于屏蔽系统默认标题栏以实现自定义界面
            return 0;
        } 
        else if (uMsg == WM_LBUTTONDOWN) {
            // 获取鼠标点击坐标
            POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
            //ScreenToClient(m_hWnd, &pt); // 已经是客户区坐标了,不需要这一步转换
            
            // 找到标题栏控件
            CControlUI* pTitleBar = m_pm.FindControl(_T("title_bar"));
            if (pTitleBar) {
                // 判断鼠标点击是否在标题栏控件范围内
                RECT rcTitle = pTitleBar->GetPos();
                if (PtInRect(&rcTitle, pt)) {
                    CControlUI* pControl = m_pm.FindControl(pt);
                    bool isClickBtn = pControl && pControl->IsVisible() && pControl->IsEnabled() 
                        && dynamic_cast<CButtonUI*>(pControl) != nullptr;
                    if (!isClickBtn) {
                        // 发送消息让系统处理窗口移动
                        ::SendMessage(m_hWnd, WM_SYSCOMMAND, SC_MOVE | HTCAPTION, 0);
                        return 0;
                    }
                }
            }
        }


        LRESULT lRes = 0;
        // 将 Windows 消息(uMsg)交由 CPaintManagerUI 处理(即交给对应控件处理),若返回 true 表示消息已被消费,直接返回处理结果
        if (m_pm.MessageHandler(uMsg, wParam, lParam, lRes)) return lRes;
        return CWindowWnd::HandleMessage(uMsg, wParam, lParam); // 基类默认处理消息的流程
    }

   /* LRESULT OnNcHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) {
    }*/

public:
    CPaintManagerUI m_pm; // 承担界面绘制、消息处理和控件管理的核心职责
};

// 程序入口及Duilib初始化部分
int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, LPSTR /*lpCmdLine*/, int nCmdShow)
{
    CPaintManagerUI::SetInstance(hInstance);
    //CPaintManagerUI::SetResourcePath(CPaintManagerUI::GetInstancePath());
    CDuiString path = CPaintManagerUI::GetInstancePath();
    // 设置资源路径:
    CPaintManagerUI::SetResourcePath(_T("skin\\")); // 相对路径

    CFrameWindowWnd* pFrame = new CFrameWindowWnd();
    if (pFrame == NULL) return 0;
    pFrame->Create(NULL, _T("别人笑我太疯癫,我笑他人看不穿"), UI_WNDSTYLE_FRAME, WS_EX_WINDOWEDGE);
    pFrame->ShowWindow(true);
    pFrame->CenterWindow(); // 居中显示
    CPaintManagerUI::MessageLoop();

    return 0;
}

ok.

相关推荐
郝学胜-神的一滴3 小时前
[简化版 GAMES 101] 计算机图形学 07:图形学投影完全推导
c++·unity·图形渲染·three.js·unreal engine
纽扣6673 小时前
【算法进阶之路】链表核心:快慢指针与反转链表专题精讲
数据结构·c++·算法·链表
lzh200409193 小时前
Linux管道(Pipe)深度指南:从原理到实战
linux·c++
eDEs OLDE3 小时前
CC++链接数据库(MySQL)超级详细指南
c语言·数据库·c++
浅念-3 小时前
吃透栈:LeetCode 栈算法题全解析
数据结构·c++·算法·leetcode·职场和发展·
NQBJT3 小时前
双轮足导盲机器人:多传感融合与全局-局部分层导航系统设计
c++·esp32·openmv·避障·导盲·轮足
lzh200409193 小时前
Linux信号(Signal)
linux·c++
承渊政道3 小时前
【动态规划算法】(两个数组的DP问题深度剖析与求解方法)
数据结构·c++·学习·算法·leetcode·动态规划·哈希算法
近津薪荼3 小时前
C++ vector容器底层深度剖析与模拟实现
开发语言·c++