ReactOS 图形系统分析(17):区域填充 — EngPaint / IntEngPaint(paint.c)

ReactOS 图形系统分析(17):区域填充 --- EngPaint / IntEngPaint(paint.c)

1. 概述

paint.c 位于 win32ss/gdi/eng/paint.c,实现 GDI 引擎的区域填充(Paint/区域绘制) 。它向上服务内核 GDI 的 FillRgn/PaintRgn 请求,向下把填充动作统一转化为一次无源的 BitBlt(图案画刷填充),从而复用引擎既有的 BitBlt 裁剪/ROP 管线。

文件很小(104 行),由三部分构成:

构成 作用
gajRop2ToRop3[16] ROP2(1~16)→ ROP3 查表,MIX 转换的基础
FillSolid 纯色矩形逐扫描线填充(DIB_HLine),被 EngEraseSurface 复用
EngPaint / IntEngPaint 双层入口:EngPaint(MIX→ROP4 并转发 BitBlt),IntEngPaint(DrvPaint 挂钩决策)
复制代码
FillRgn(hdc, hrgn, hbrush) / PaintRgn(hdc, hrgn)
    ↓ gdi32.dll → NtGdiFillRgn
ntgdi/bitblt.c: IntGdiFillRgn (DC 裁剪合成 / 画刷准备 / MIX 构造)
    ↓ IntEngPaint
引擎: 目标表面挂钩了 DrvPaint? (HOOK_PAINT)
    ├─ 是 → GDIDEVFUNCS(pso).Paint  (vga 等硬件驱动)
    └─ 否 → EngPaint
                ↓ MIX_TO_ROP4 转 ROP4,断言无源
            IntEngBitBlt(pso, NULL, NULL, pco, NULL, ..., pbo, ..., rop4)
                ↓ 无源 → 直接软件路径
            EngBitBlt → BltPatCopy/纯色 → DIB 引擎写像素

2. 调用链路(从 API 到像素)

#mermaid-svg-DeleXL4zcDOlqTz4{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DeleXL4zcDOlqTz4 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DeleXL4zcDOlqTz4 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DeleXL4zcDOlqTz4 .error-icon{fill:#552222;}#mermaid-svg-DeleXL4zcDOlqTz4 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DeleXL4zcDOlqTz4 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DeleXL4zcDOlqTz4 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DeleXL4zcDOlqTz4 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DeleXL4zcDOlqTz4 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DeleXL4zcDOlqTz4 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DeleXL4zcDOlqTz4 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DeleXL4zcDOlqTz4 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DeleXL4zcDOlqTz4 .marker.cross{stroke:#333333;}#mermaid-svg-DeleXL4zcDOlqTz4 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DeleXL4zcDOlqTz4 p{margin:0;}#mermaid-svg-DeleXL4zcDOlqTz4 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DeleXL4zcDOlqTz4 .cluster-label text{fill:#333;}#mermaid-svg-DeleXL4zcDOlqTz4 .cluster-label span{color:#333;}#mermaid-svg-DeleXL4zcDOlqTz4 .cluster-label span p{background-color:transparent;}#mermaid-svg-DeleXL4zcDOlqTz4 .label text,#mermaid-svg-DeleXL4zcDOlqTz4 span{fill:#333;color:#333;}#mermaid-svg-DeleXL4zcDOlqTz4 .node rect,#mermaid-svg-DeleXL4zcDOlqTz4 .node circle,#mermaid-svg-DeleXL4zcDOlqTz4 .node ellipse,#mermaid-svg-DeleXL4zcDOlqTz4 .node polygon,#mermaid-svg-DeleXL4zcDOlqTz4 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DeleXL4zcDOlqTz4 .rough-node .label text,#mermaid-svg-DeleXL4zcDOlqTz4 .node .label text,#mermaid-svg-DeleXL4zcDOlqTz4 .image-shape .label,#mermaid-svg-DeleXL4zcDOlqTz4 .icon-shape .label{text-anchor:middle;}#mermaid-svg-DeleXL4zcDOlqTz4 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DeleXL4zcDOlqTz4 .rough-node .label,#mermaid-svg-DeleXL4zcDOlqTz4 .node .label,#mermaid-svg-DeleXL4zcDOlqTz4 .image-shape .label,#mermaid-svg-DeleXL4zcDOlqTz4 .icon-shape .label{text-align:center;}#mermaid-svg-DeleXL4zcDOlqTz4 .node.clickable{cursor:pointer;}#mermaid-svg-DeleXL4zcDOlqTz4 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DeleXL4zcDOlqTz4 .arrowheadPath{fill:#333333;}#mermaid-svg-DeleXL4zcDOlqTz4 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DeleXL4zcDOlqTz4 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DeleXL4zcDOlqTz4 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DeleXL4zcDOlqTz4 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DeleXL4zcDOlqTz4 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DeleXL4zcDOlqTz4 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DeleXL4zcDOlqTz4 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DeleXL4zcDOlqTz4 .cluster text{fill:#333;}#mermaid-svg-DeleXL4zcDOlqTz4 .cluster span{color:#333;}#mermaid-svg-DeleXL4zcDOlqTz4 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DeleXL4zcDOlqTz4 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DeleXL4zcDOlqTz4 rect.text{fill:none;stroke-width:0;}#mermaid-svg-DeleXL4zcDOlqTz4 .icon-shape,#mermaid-svg-DeleXL4zcDOlqTz4 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DeleXL4zcDOlqTz4 .icon-shape p,#mermaid-svg-DeleXL4zcDOlqTz4 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DeleXL4zcDOlqTz4 .icon-shape .label rect,#mermaid-svg-DeleXL4zcDOlqTz4 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DeleXL4zcDOlqTz4 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DeleXL4zcDOlqTz4 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DeleXL4zcDOlqTz4 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是



应用程序: FillRgn / PaintRgn / FrameRgn
gdi32.dll

NtGdiFillRgn
ntgdi/bitblt.c: NtGdiFillRgn

(锁 DC / 锁区域 / 锁画刷)
ntgdi: IntGdiFillRgn
合成裁剪区域 prgnClip

= DC 可见区 ∩ RAO/Vis 区域 + DC 原点偏移
构造 MIX

mix = rop2Fg | (BkMode==OPAQUE ? rop2Fg : R2_NOP)<<8
EBRUSHOBJ_vInit 初始化画刷对象
IntEngPaint(psurf, &xcoClip, pbo, ptlFillOrigin, mix)
表面非 STYPE_BITMAP

且 HOOK_PAINT?
DrvPaint

(vga: VGADDIPaintRgn)
EngPaint
MIX_TO_ROP4(mix)

gajRop2ToRop3 查表

低字节=前景ROP3 高字节=背景ROP3
NT_ASSERT(!ROP4_USES_SOURCE)
IntEngBitBlt(pso, NULL src, NULL mask,

pco, rclBounds, NULL, NULL, pbo,

pptlBrushOrg, rop4)
目标表面 HOOK_BITBLT?
驱动 DrvBitBlt
EngBitBlt 软件路径

(BltPatCopy / 纯色 BltPatCopy)
DibFunctionsForBitmapFormat

DIB_*_BitBlt / DIB_HLine
显存/帧缓冲

2.1 ntgdi 层(IntGdiFillRgn)关键决策

bitblt.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitblt.c#L1228-L1333) 的 IntGdiFillRgn 在调用引擎前完成四件事:

c 复制代码
/* 1. 合成裁剪区域:DC 可见区与系统/RAO 区域求交,并平移到 DC 原点 */
IntGdiCombineRgn(prgnClip, prgnClip, pdc->prgnRao, RGN_AND);
REGION_bOffsetRgn(prgnClip, pdc->ptlDCOrig.x, pdc->ptlDCOrig.y);

/* 2. 构造 MIX:前景=当前 ROP2;背景= OPAQUE 时同 ROP2,TRANSPARENT 时为 R2_NOP */
rop2Fg = FIXUP_ROP2(pdc->pdcattr->jROP2);
mix = rop2Fg | (pdc->pdcattr->jBkMode == OPAQUE ? rop2Fg : R2_NOP) << 8;

/* 3. 初始化画刷:显式画刷或 DC 填充画刷(含 DIRTY 更新) */
EBRUSHOBJ_vInit(&eboFill, pbrFill, pdc->dclevel.pSurface, 0x00FFFFFF, 0, NULL);
/* ... 或 DC_vUpdateFillBrush + &pdc->eboFill.BrushObject */

/* 4. 调用引擎 */
bRet = IntEngPaint(&pdc->dclevel.pSurface->SurfObj,
                   (CLIPOBJ *)&xcoClip, pbo, &pdc->ptlFillOrigin, mix);

入口 API 对照: FillRgn(指定画刷,NtGdiFillRgn)、PaintRgn(用 DC 当前画刷,IntGdiPaintRgnIntGdiFillRgn(pdc, prgn, NULL))、FrameRgn(先 GreCreateFrameRgn 生成边框区域再 FillRgn)。


3. 核心机制

3.1 gajRop2ToRop3 查表 + MIX_TO_ROP4(eng.h(file:///d:/reactos/win32ss/gdi/eng/eng.h#L3-L7))

填充的"笔"语义由 ROP2(16 种)描述;引擎内部 BitBlt 统一使用 ROP3/ROP4。paint.c 开头提供一张 ROP2 → ROP3 映射表

c 复制代码
const BYTE gajRop2ToRop3[16] =
{
    0x00, //  1: R2_BLACK        0
    0x05, //  2: R2_NOTMERGEPEN  DPon
    0x0A, //  3: R2_MASKNOTPEN   DPna
    0x0F, //  4: R2_NOTCOPYPEN   Pn
    0x50, //  5: R2_MASKPENNOT   PDna
    0x55, //  6: R2_NOT          Dn
    0x5A, //  7: R2_XORPEN       DPx
    0x5F, //  8: R2_NOTMASKPEN   DPan
    0xA0, //  9: R2_MASKPEN      DPa
    0xA5, // 10: R2_NOTXORPEN    PDxn
    0xAA, // 11: R2_NOP          D
    0xAF, // 12: R2_MERGENOTPEN  DPno
    0xF0, // 13: R2_COPYPEN      P
    0xF5, // 14: R2_MERGEPENNOT  PDno
    0xFA, // 15: R2_MERGEPEN     DPo
    0xFF, // 16: R2_WHITE        1
};

#define MIX_TO_ROP4(mix) \
    (((ULONG)gajRop2ToRop3[((mix) - 1) & 0xF]) | \
     ((ULONG)gajRop2ToRop3[(((mix) >> 8) - 1) & 0xF] << 8))

MIX 编码(Windows 惯例): 低 8 位 = 前景 ROP2,高 8 位 = 背景 ROP2(R2_BLACK=1 ~ R2_WHITE=16)。MIX_TO_ROP4(mix-1)&0xF 索引(-1 对齐 1~16 → 0~15),把前后景分别映射成 ROP3(低字节 = 前景,高字节 = 背景),恰好构成 ROP4。例:默认 R2_COPYPEN(13)0xF0,OPAQUE 背景 → ROP4 = 0xF0F0

3.2 FillSolid --- 纯色矩形逐扫描线填充

c 复制代码
BOOL APIENTRY FillSolid(SURFOBJ *pso, PRECTL pRect, ULONG iColor)
{
  LONG y;
  ULONG LineWidth;
  LineWidth = pRect->right - pRect->left;
  for (y = pRect->top; y < pRect->bottom; y++)
  {
    DibFunctionsForBitmapFormat[pso->iBitmapFormat].DIB_HLine(
      pso, pRect->left, pRect->right, y, iColor);
  }
  return TRUE;
}

实现要点:

  • 逐行调用按位深分发的 DIB_HLine(函数签名 PFN_DIB_HLine(SURFOBJ*, LONG left, LONG right, LONG y, ULONG iColor),见 dib.h(file:///d:/reactos/win32ss/gdi/dib/dib.h#L36)),与 lineto.c 的水平线快路径同源。
  • 不做 ROP/掩码处理,只做"整行覆盖写"------专用于纯色直接擦除场景。
  • 唯一调用者是 EngEraseSurfacesurface.c(file:///d:/reactos/win32ss/gdi/eng/surface.c#L583-L593)),用于设备表面初始擦除(显示驱动 DrvEnableSurface 之后)和 EngEraseSurface DDI 服务。

3.3 EngPaint --- MIX→ROP4 并转发 BitBlt

c 复制代码
BOOL APIENTRY EngPaint(SURFOBJ *pso, CLIPOBJ *pco, BRUSHOBJ *pbo,
                       POINTL *pptlBrushOrg, MIX mix)
{
    ROP4 rop4;
    rop4 = MIX_TO_ROP4(mix);                       /* MIX(2×ROP2) → ROP4 */
    NT_ASSERT(!ROP4_USES_SOURCE(rop4));            /* 填充永不使用源表面 */
    return IntEngBitBlt(pso, NULL, NULL, pco, NULL,
                        &pco->rclBounds, NULL, NULL,
                        pbo, pptlBrushOrg, rop4);  /* 无源/无掩码的图案填充 */
}

设计本质: "区域填充"在引擎层被规约为一次特化 BitBlt ------目标表面 + 画刷(图案),源和掩码均为 NULL。由于 ROP4 必然不依赖源,ROP4_USES_SOURCE 断言保证安全。之后完全复用《分析_4》第 3.1 节的 IntEngBitBlt 管线(目标 HOOK_BITBLT → 驱动 DrvBitBlt,否则 EngBitBlt 软件路径,纯色走 BltPatCopy 快路径)。这也是引擎复用哲学的又一体现:不写专门的填充渲染器。

3.4 IntEngPaint --- DrvPaint 挂钩决策

c 复制代码
BOOL APIENTRY IntEngPaint(SURFOBJ *pso, CLIPOBJ *pco, BRUSHOBJ *pbo,
                          POINTL *pptlBrushOrg, MIX mix)
{
    SURFACE *psurf = CONTAINING_RECORD(pso, SURFACE, SurfObj);

    /* 设备表面且挂钩了 DrvPaint → 调驱动;否则引擎软件实现 */
    if ((pso->iType != STYPE_BITMAP) && (psurf->flags & HOOK_PAINT))
    {
        return GDIDEVFUNCS(pso).Paint(pso, pco, pbo, pptlBrushOrg, mix);
    }
    return EngPaint(pso, pco, pbo, pptlBrushOrg, mix);
}

挂钩条件两层:

  1. pso->iType != STYPE_BITMAP --- 只有设备表面(屏幕)才可能让硬件接管;内存位图一律引擎实现。
  2. psurf->flags & HOOK_PAINT --- 表面是否在 EngAssociateSurface 时声明 HOOK_PAINT

4. DDI 交互:DrvPaint 与 HOOK_PAINT

4.1 标志与函数表

  • HOOK_PAINT = 0x00000010 定义于 winddi.h(file:///d:/reactos/sdk/include/psdk/winddi.h#L1424)(注意:eng/surface.h 中 HOOK_* 宏被注释掉了,实际编译用的是 winddi.h 的定义)。
  • DDI 函数表 DRIVER_FUNCTIONS.Paintntgdityp.h(file:///d:/reactos/win32ss/include/ntgdityp.h#L586)),vga 驱动以 {INDEX_DrvPaint, (PFN) DrvPaint} 注册(vga/enable.c(file:///d:/reactos/win32ss/drivers/displays/vga/main/enable.c#L25)),并在关联表面时声明 HOOK_BITBLT | HOOK_PAINT | HOOK_LINETO | HOOK_COPYBITS | ...enable.c(file:///d:/reactos/win32ss/drivers/displays/vga/main/enable.c#L491))。

4.2 vga 驱动 DrvPaint 示例(vga/objects/paint.c(file:///d:/reactos/win32ss/drivers/displays/vga/objects/paint.c#L183-L201))

复制代码
DrvPaint(Surface, ClipRegion, Brush, BrushOrigin, Mix)
    ↓ 取 iSolidColor(当前只支持纯色画刷)
VGADDIPaintRgn(...)
    ├─ DC_RECT → 直接 VGADDIFillSolid(整矩形)
    └─ 其他   → CLIPOBJ_cEnumStart + CLIPOBJ_bEnum 枚举矩形,
                逐矩形 VGADDIFillSolid
                    ↓
VGADDIFillSolid: 按字节对齐拆分 左掩码/中间整字节/右掩码,
                 通过 VGA 图形控制器端口(GRA_I/GRA_D 写掩码)
                 直接写显存,规避 CPU 不可寻址字节

注意 vga 的 DrvPaint 绕过引擎直接操作端口,体现了 DDI 挂钩的意义:硬件驱动用更高效的方式完成 GDI 兜底要做的同一件事


5. 服务导出与调用方

函数 导出 调用方
EngPaint win32k.spec(file:///d:/reactos/win32ss/win32k.spec#L103) 直接导出(DDI 服务) 驱动可选调用;内核经 IntEngPaint 调用
NtGdiEngPaint gdi32.spec(file:///d:/reactos/win32ss/gdi/gdi32/gdi32.spec#L187) → win32u → umpdstubs.c(file:///d:/reactos/win32ss/gdi/eng/umpdstubs.c#L319-L331) UMPD 用户模式驱动路径(当前 UNIMPLEMENTED 桩)
FillSolid 引擎内部(paint.h(file:///d:/reactos/win32ss/gdi/ntgdi/paint.h#L3) 声明) EngEraseSurface
IntEngPaint 内部 IntGdiFillRgn
复制代码
内核驱动路径:  DrvPaint ← GDIDEVFUNCS(pso).Paint ← IntEngPaint ← IntGdiFillRgn
UMPD 驱动路径: EngPaint ← NtGdiEngPaint(umpdstubs) ← gdi32.EngPaint ← UMPD
软件兜底路径:  EngPaint → IntEngBitBlt → EngBitBlt → DIB

6. 与《分析_4》标注对照

标注 核对
《_4》第 37 行:paint.c =「区域填充」 准确。补充:实现主体是 EngPaint/IntEngPaint/FillSolid,其中 EngPaint 是 win32k 导出的 DDI 服务(NtGdiEngPaint UMPD 路径为桩)

7. 源码索引

文件 关键内容
paint.c(file:///d:/reactos/win32ss/gdi/eng/paint.c) gajRop2ToRop3 表、FillSolid、EngPaint、IntEngPaint
eng.h(file:///d:/reactos/win32ss/gdi/eng/eng.h#L5-L7) MIX_TO_ROP4 宏、gajRop2ToRop3 外部声明
inteng.h(file:///d:/reactos/win32ss/gdi/eng/inteng.h#L143) IntEngPaint 声明
ntgdi/bitblt.c(file:///d:/reactos/win32ss/gdi/ntgdi/bitblt.c#L1228-L1333) IntGdiFillRgn / IntGdiPaintRgn / NtGdiFillRgn / NtGdiFrameRgn
ntgdi/paint.h(file:///d:/reactos/win32ss/gdi/ntgdi/paint.h) FillSolid 声明
surface.c(file:///d:/reactos/win32ss/gdi/eng/surface.c#L583-L593) EngEraseSurface → FillSolid
bitblt.c(file:///d:/reactos/win32ss/gdi/eng/bitblt.c#L1104) IntEngBitBlt(EngPaint 转发目标)
winddi.h(file:///d:/reactos/sdk/include/psdk/winddi.h#L1424) HOOK_PAINT 定义
vga/objects/paint.c(file:///d:/reactos/win32ss/drivers/displays/vga/objects/paint.c) DrvPaint / VGADDIPaintRgn / VGADDIFillSolid(硬件填充示例)
umpdstubs.c(file:///d:/reactos/win32ss/gdi/eng/umpdstubs.c#L319-L331) NtGdiEngPaint(UMPD 桩)

关联模块:

  • 上层调用者:ntgdi/(file:///d:/reactos/win32ss/gdi/ntgdi/bitblt.c)(FillRgn/PaintRgn/FrameRgn 系统调用)
  • 软件渲染后端:gdi/dib/(file:///d:/reactos/win32ss/gdi/dib/)(DIB_HLine / DIB_BitBlt 函数表)
  • BitBlt 管线:eng/bitblt.c(file:///d:/reactos/win32ss/gdi/eng/bitblt.c)(详见《分析_4》3.1 节)
  • 硬件填充示例:vga 驱动(file:///d:/reactos/win32ss/drivers/displays/vga/objects/paint.c)

本文档基于 ReactOS 源代码 win32ss/gdi/eng/paint.c 及关联模块分析(2026 年 8 月)

相关推荐
生态学者1 小时前
Ecological Indicators | 机场鸟调的新方法:用 AWM-PC1 读懂干扰梯度下的鸟类群落
人工智能·算法·r语言·微信公众平台
北风toto1 小时前
研发效能与后端核心技术全景指南:从CI/CD到共性组件实战
java·开发语言·ci/cd
Billy121381 小时前
Day12-C++20 Coroutines(上):协程原理与Promise/Awaitable机制
开发语言·c++进阶学习
01二进制代码漫游日记1 小时前
C++基础入门速通
java·开发语言·c++
Herbert_hwt1 小时前
第六章 Java深入理解接口、函数式接口与lambda表达式
java·开发语言·算法
橙橙笔记1 小时前
C++的学习第三部分
开发语言·c++·学习
君君思密达1 小时前
LeetCode Hot 100 题目详解-滑动窗口
算法·leetcode·职场和发展
GreenTea10 小时前
深度解读 Anthropic 多智能体报告:更强的模型 ≠ 更好的协调
前端·后端·算法
2603_9651481110 小时前
如何解析JSON数据?API返回的商品信息处理教程
开发语言·数据库·python·自动化·json·api