分类目录归档:The Old New Thing

伟大的产品-windows进化启示录

当我尝试解码HttpContent对象返回的字节数据时,为什么会有乱码?

原文:Why is there trailing garbage when I try to decode the bytes of a HttpContent object? – The Old New Thing (microsoft.com)September 23rd, 2021

一个客户无法从HTTP响应中提取正确的文本:

-

winrt::HttpRequest request = ...;

auto result = co_await request.Content().ReadAsStringAsync();

-
-

{"name":"ðAPCDCS±meow"}

-

这个版本生成的字符串看起来基本正常,但有些部分损坏了。

从检查来看,很明显这里有mojibake(🐱),其中一个UTF-8字符串被错误的解读为其它8位字符集。

根据RFC 2616第3.7.1节,如果文本媒体子类型没有显式指定字符集,则默认字符集为ISO-8859-1(Latin-1)。显然,该服务器返回一个编码为UTF-8的字符串,但在报告其Content-Type时未包含显式字符集解释。因此,该字符串默认为ISO-8859-1。

哦~~

现在,RFC 2616的3.4.1节承认,HTTP客户端通常会对缺乏显式字符集解释的返回进行最佳可能性猜测。如果没有提供字符集,Windows在运行时确实会对字符集进行一些猜测:

  • 如果缓冲区以UTF-8 BOM、UTF-16LE BOM、UTF-16BE BOM或GB18030 BOM开头,则根据该字符集对缓冲区进行解码。
  • 如果内容类型是application/json或者是*+json的形式,那么它被解码为UTF-8。

以上实际情况中,服务端既没有显示指定UTF-8 BOM,也没有将内返回容类型设置为application/json。所以,即便他最后返回的是一个json对象,也不满足上面猜测的每一步,而被解释为IOS-8859-1编码。

哦~~

好吧,让我们来尝试在获取服务器响应数据时,将它们显式解码为UTF-8来解决这个问题(明显非常坏的)。

std::wstring Utf8ToUtf16(char const* str)
{
    std::wstring result;
    if (str) {
        auto resultLen = MultiByteToWideChar(
            CP_UTF8, MB_ERR_INVALID_CHARS, str, -1, nullptr, 0);
        if (resultLen) {
            result.resize(resultLen);
            MultiByteToWideChar(
                CP_UTF8, MB_ERR_INVALID_CHARS, str, -1,
                result.data(), resultLen);
        }
    }
    return result;
}

winrt::HttpRequest request = ...;

auto buffer = co_await request.Content().ReadAsBufferAsync();
auto result = Utf8ToUtf16((char const*)buffer.data());

这个版本运行得更好,但却在尾部出现了一些乱码:

-

{"name":"🐱meow"}SOH

-

在这种情况下,问题不是缓冲区数据,而是如何将缓冲区转换为字符串。data()方法返回一个指向缓冲区开头的指针,代码将其作为源字符串传递给Multi-Byte-To-Wide-Char,字符串长度为-1。

这个特殊值-1意味着该指针应被视为以空结束的字符串的开始位置。但是Read-As-Buffer-Async生成的Buffer只是从服务器返回的原始字节,服务器不会在末尾放一个空结束符。服务器说,“响应是19个字节长”,它发送19个字节,就这样。

因此,这个尾部的乱码是读缓冲区溢出,代码只是读过缓冲区的真实末端后,并“一直继续解码,直到达到空结束符”为止。

当您希望解码缓冲区中的字节数据时,您需要指定缓冲区中的长度,而不是说“一直继续解码,直到达到空结束符”。

std::wstring Utf8ToUtf16(char const* str, int32_t inputLen)
{
    std::wstring result;
    if (str) {
        auto resultLen = MultiByteToWideChar(
            CP_UTF8, MB_ERR_INVALID_CHARS, str, inputLen, nullptr, 0);
        if (resultLen) {
            result.resize(resultLen);
            MultiByteToWideChar(
                CP_UTF8, MB_ERR_INVALID_CHARS, str, inputLen,
                result.data(), resultLen);
        }
    }
    return result;
}

winrt::HttpRequest request = ...;

auto buffer = co_await request.Content().ReadAsBufferAsync();
auto result = Utf8ToUtf16(
    (char const*)buffer.data(),
    static_cast<int32_t>(buffer.Length()));

以上代码将生成实际所需的字符串解码为UTF-8,没有乱码。

现在,您不需要编写代码来获取包含UTF-8编码字符串的Buffer并将其转换为UTF-16字符串。Windows运行时已经提供了一个帮助函数:

-
winrt::HttpRequest request = ...;

auto buffer = co_await request.Content().ReadAsBufferAsync();
auto result = CryptographicBuffer::ConvertBinaryToString(
    BinaryStringEncoding::Utf8, buffer);
-

但由于我们使用的是c++ /WinRT,我们完全可以使用 the conversion built into C++/WinRT we learned last time去避免这些问题。其中难点部分是从缓冲区中获取std::string_view 。

winrt::HttpRequest request = ...;

auto buffer = co_await request.Content().ReadAsBufferAsync();
auto result = winrt::to_hstring(
    std::string_view{
        static_cast<char const*>(buffer.data()),
        buffer.Length() });

好了,你可以这样,读取原始缓冲区并将其从UTF-8转换为UTF-16字符串。

与此同时,去修复你的服务器。

尾声:客户最终发现他们的服务器确实有错误。响应是通过回调服务器生成的,它将Content-Type报头放在ResponseHeaders中而不是contenttheaders。

如何在对话框中设置控件的焦点

在对话框中设置焦点时,只调用SetFocus是不够的.

在MSDN介绍DM_SETDEFID的文档中指出, 对默认ID的不当操作可能导致奇怪的情况, 例如在对护框中包含连个默认按钮. 幸运的是, 你很少改变对护框的默认按钮.

一个更大的问题是使用SetFocus函数来在对护框中设置焦点. 如果这么做, 那就是直接进入到窗口管理器, 而绕过了对话框管理器. 这就意味着将可能出现一些“不合理”的情况, 例如吧焦点放到一个按钮上, 而这个按钮不是默认按钮.

为了避免这个问题, 不要使用SetFocus来改变对话框上的焦点,而是使用WM_NEXTDLGCTL消息.

void SetDialogFocus(HWND HDlg, HWND hwndControl)
{
  SendMessage(Hdlg, WM_NEXTDLGCTRL, (WPARAM)hwndControl,  TRUE);
}

为什么SetFocus不会管理默认的ID

windows对话框管理器是建立在窗口管理器之上的. 由于SetFocus是窗口管理器中的函数. 因此, 它不知道接收焦点的窗口是否是在某个对话框内.如果只是检查父窗口的窗口类是不是对话框, 还是不够的. 同样, 在前面已经看到了, 在应用程序中可以使用IsDialogMessage函数在窗口中支持键盘导航(使用键盘控制窗口), 而这些窗口看你根本就不是对话框. 窗口管理器不能只是发送DM_GETDEFID消息到拥有焦点窗口的父窗口, 因为DM_GETDEFID在数值上等于WM_USER, 而WM_USER在窗口类内部定义的消息范围之内. 如果父窗口不是对话框, 那么当我们向窗口发送WM_USER消息时, 结果是不可预知的.

关于自己实现模态对话框时,禁止窗口和激活窗口的正确顺序

显示模态到取消模态,所有者窗口显示会“闪烁”的原因:

当销毁模态对话框时,这个对话框刚好是拥有前台焦点的窗口。现在,窗口管理器需要找到其它的窗口并把前台焦点交给这个窗口。窗口管理器会首先试着把前台焦点交给对话框的所有者窗口,但此时这个窗口却是“禁用”的,因此窗口管理器跳过所有者窗口,并继续查找其他没有被禁用的窗口。

因此,销毁模态对话框的正确顺序是:

1、重新激活所有者窗口

2、销毁模态对话框

关于Windows TextOut输出文字时的一些难看的“方块”

什么是绘制实心矩形最好的方式

老一辈的程序员可能会告诉你”ExtTextOut”,性能最优。

TextOut在输出文字时,为什么有些难看的方块?

IMLangFontLink2::GetStrCodePages方法将字符串分解成许多”块“(chunk),并且在每个块中的所有字符都可以用相同的字体来显示,此外还可以通过IMLangFontLink2::MapFont来创建字体。

#include <mlang.h>

HRESULT TextOutFL(HDC hdc, int x, int y, LPCWSTR psz, int cch)
{
  ...
  while (cch > 0) {
    DWORD dwActualCodePages;
    long cchActual;
    pfl->GetStrCodePages(psz, cch, 0, &dwActualCodePages, &cchActual);
    HFONT hfLinked;
    pfl->MapFont(hdc, dwActualCodePages, 0, &hfLinked);
    HFONT hfOrig = SelectFont(hdc, hfLinked);
    TextOut(hdc, ?, ?, psz, cchActual);
    SelectFont(hdc, hfOrig);
    pfl->ReleaseFont(hfLinked);
    psz += cchActual;
    cch -= cchActual;
  }
  ...
}

下一步,应该确定每次TextOut的位置

SetTextAlign(hdc, GetTextAlign(hdc) | TA_UPDATECP);
MoveToEx(hdc, x, y, NULL);

最后,在处理从右到左(right-to-left)的语言时,IMLangFontLink2接口来将字符串分解为块,将会失败。

以下代码应该是windows文本输出的终极方案

#inlucde <usp10.h>

HRESULT TextOutUniscribe(HDC hdc, int x, int y, LPCWSTR psz, int cch)
{
  if (cch == 0) return S_OK;
  SCRIPT_STRING_ANALYSIS ssa;
  HRESULT hr = ScriptStringAnalyse(hdc, psz, cch, 0, -1, SSA_FALLBACK | SSA_GLYPHS, MAXLONG, NULL, NULL, NULL, NULL, NULL, &ssa);
  if (SUCCESS(hr))
  {
    hr = ScriptString(ssa, x, y, 0, NULL, 0, 0, FALSE);
    ScriptStringFree(&ssa);
  }
  return hr;
}

界面初探

  • 为什么要单击”开始”按钮来关机
    • 总揽”开始”和”结束”的入口
  • 为什么没有”专家模式”
    • 无法量化,没有办法区分哪些选项属于”专家”
  • 对话框的默认按钮是取消
    • 用户总是尽可能的去忽略这些对话框
  • 最好的设置是:及时你没有意识到这些设置的存在,但它们依然安装你所期望的方式在工作
  • 问用户无法回答的问题,是为了显示你的超群智慧吗?
  • 为什么安装程序不会问你是否希望保留操作系统文件的新版本?
    • 用户的专业度不足以回答此类问题
  • 功能设计的思考
    • ?
  • 什么时候应该禁止选项,而什么时候又该删除选项
    • 实验已经证明:当一个选项被显示出来并且处于禁止状态时,那么用户就会觉得只要他进行足够多的操作,这个选项就会被激活
  • 什么时候应该将“…”放在按或者菜单项的后面
    • 只有当某个命令需要额外的信息来执行时,才应该使用省略号。如(About)虽然是弹出对话框,但其本身就是结果,是不需要显示省略号的。
    • 参考:《Windows User Interface Design Specification And Guidelines》
  • 自动售货机的用户界面设计