quickwin 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.en.md +21 -26
  2. package/README.md +3 -6
  3. package/examples/exec_server.js +139 -31
  4. package/examples/exec_server.tsx +7 -8
  5. package/examples/exec_server_worker.js +1 -1
  6. package/examples/exec_server_worker.ts +2 -2
  7. package/examples/pdf_preview2.js +1 -1
  8. package/examples/pdf_preview2.ts +1 -1
  9. package/examples/pdf_viewer.js +132 -28
  10. package/examples/pdf_viewer.tsx +1 -1
  11. package/examples/test_gallery.js +131 -27
  12. package/examples/test_react_lazy.js +131 -27
  13. package/examples/test_react_listbox.js +131 -27
  14. package/examples/test_root_window.js +131 -27
  15. package/examples/test_tab.js +131 -27
  16. package/lib/certs.js +13 -8
  17. package/lib/certs.ts +13 -8
  18. package/lib/fetch.js +51 -3
  19. package/lib/fetch.ts +38 -3
  20. package/lib/ffi/bind.js +94 -5
  21. package/lib/ffi/bind.ts +107 -10
  22. package/lib/ffi/ctype.js +33 -7
  23. package/lib/ffi/ctype.ts +48 -10
  24. package/lib/ffi/struct.js +92 -14
  25. package/lib/ffi/struct.ts +108 -19
  26. package/lib/react-qw/components/ListView.js +1 -0
  27. package/lib/react-qw/components/PathPicker.js +2 -2
  28. package/lib/react-qw/index.js +1 -1
  29. package/lib/react-qw/index.ts +4 -1
  30. package/lib/text-measure.js +3 -1
  31. package/lib/text-measure.ts +4 -3
  32. package/lib/websocket.js +32 -2
  33. package/lib/websocket.ts +25 -2
  34. package/lib/windows/advapi32.js +53 -0
  35. package/lib/windows/advapi32.ts +64 -0
  36. package/lib/windows/comctl32.js +15 -15
  37. package/lib/windows/comctl32.ts +15 -16
  38. package/lib/windows/comdlg32.js +3 -2
  39. package/lib/windows/comdlg32.ts +3 -2
  40. package/lib/windows/gdi32.js +328 -26
  41. package/lib/windows/gdi32.ts +349 -26
  42. package/lib/windows/kernel32.js +206 -10
  43. package/lib/windows/kernel32.ts +225 -10
  44. package/lib/windows/msimg32.js +38 -0
  45. package/lib/windows/msimg32.ts +45 -0
  46. package/lib/windows/ntdll.js +3 -2
  47. package/lib/windows/ntdll.ts +3 -2
  48. package/lib/windows/ole32.js +1 -1
  49. package/lib/windows/ole32.ts +1 -1
  50. package/lib/windows/psapi.js +32 -0
  51. package/lib/windows/psapi.ts +37 -0
  52. package/lib/windows/shell32.js +5 -3
  53. package/lib/windows/shell32.ts +5 -3
  54. package/lib/windows/structs.js +135 -0
  55. package/lib/windows/structs.ts +144 -0
  56. package/lib/windows/user32.js +202 -99
  57. package/lib/windows/user32.ts +204 -99
  58. package/lib/windows/version.js +29 -0
  59. package/lib/windows/version.ts +35 -0
  60. package/lib/windows/winspool.js +71 -0
  61. package/lib/windows/winspool.ts +80 -0
  62. package/package.json +1 -1
  63. package/quickwin.d.ts +11 -5
  64. package/quickwin_const.d.ts +136 -0
  65. package/qwin-nowasm-x86.exe +0 -0
  66. package/qwin-nowasm.exe +0 -0
  67. package/qwin-x86.exe +0 -0
  68. package/qwin.exe +0 -0
  69. package/test/run.js +7 -0
  70. package/test/run.ts +7 -0
  71. package/test/test_basic_fetch.js +21 -0
  72. package/test/test_basic_fetch.ts +17 -0
  73. package/test/test_ffi.js +6 -6
  74. package/test/test_ffi.ts +6 -6
  75. package/test/test_ffi_abi.js +55 -0
  76. package/test/test_ffi_abi.ts +56 -0
  77. package/test/test_ffi_array.js +116 -0
  78. package/test/test_ffi_array.ts +141 -0
  79. package/test/test_ffi_bind.js +82 -0
  80. package/test/test_ffi_bind.ts +71 -0
  81. package/test/test_files.js +102 -0
  82. package/test/test_files.ts +121 -0
  83. package/test/test_gdi_draw.js +681 -0
  84. package/test/test_gdi_draw.ts +725 -0
  85. package/test/test_net_fetch.js +4 -3
  86. package/test/test_net_fetch.ts +4 -3
  87. package/test/test_printer.js +137 -0
  88. package/test/test_printer.ts +148 -0
  89. package/test/test_proc.js +141 -0
  90. package/test/test_proc.ts +163 -0
  91. package/test/test_react_complex.js +131 -27
  92. package/test/test_react_counter.js +131 -27
  93. package/test/test_react_flex.js +131 -27
  94. package/test/test_react_hidden.js +131 -27
  95. package/test/test_react_listview_link.js +131 -27
  96. package/test/test_react_notify.js +131 -27
  97. package/test/test_react_render.js +131 -27
  98. package/test/test_registry.js +74 -0
  99. package/test/test_registry.ts +95 -0
  100. package/test/test_sysinfo.js +122 -0
  101. package/test/test_sysinfo.ts +141 -0
  102. package/test/test_wolfssl_read.js +45 -44
  103. package/test/test_wolfssl_read.ts +42 -42
@@ -0,0 +1,725 @@
1
+ import { Tester } from './test_helper.js'
2
+ import * as gui from 'gui'
3
+ import { NULL, PtrArrayBuffer, type Ptr } from '../lib/ffi/ctype.js'
4
+ import { FrameRect, GetDC, InvertRect, ReleaseDC } from '../lib/windows/user32.js'
5
+ import {
6
+ Arc, Chord, CreateBitmap, CreateCompatibleBitmap, CreateCompatibleDC, CreateFontIndirect,
7
+ CreateHatchBrush, CreatePatternBrush, CreatePen, CreateSolidBrush, DeleteDC,
8
+ DeleteObject, Ellipse, ExtTextOut, GetArcDirection, GetDIBits, GetStockObject,
9
+ GetStretchBltMode, GetTextExtentPoint32, GetTextMetrics, LineTo, MaskBlt, MoveToEx,
10
+ PatBlt, Pie, PlgBlt, Polygon, Polyline, Rectangle, RestoreDC, RoundRect, SaveDC,
11
+ SelectObject, SetArcDirection, SetBkColor, SetBkMode, SetDIBitsToDevice,
12
+ SetPolyFillMode, SetROP2,
13
+ SetStretchBltMode, SetTextAlign, SetTextColor, TextOut, StretchBlt, StretchDIBits,
14
+ } from '../lib/windows/gdi32.js'
15
+ import { AlphaBlend, TransparentBlt } from '../lib/windows/msimg32.js'
16
+ import { BITMAPINFOHEADER, POINT, SIZE, TEXTMETRICW } from '../lib/windows/structs.js'
17
+
18
+ // 像素比较统一用 COLORREF 形(0x00BBGGRR)—— 与画图入参同形,读回直接对比。
19
+ const RED = 0x000000ff
20
+ const GREEN = 0x0000ff00
21
+ const BLUE = 0x00ff0000
22
+ const WHITE = 0x00ffffff
23
+ const BLACK = 0x00000000
24
+ const W = 320
25
+ const H = 240
26
+
27
+ export const suite = {
28
+ name: 'gdi-draw',
29
+ run: (t: Tester) => {
30
+ // —— 基座:屏幕 DC → 内存 DC + 32bpp 位图(与 exec_server_worker 截屏同型)——
31
+ t.section('setup: memory DC + 32bpp bitmap')
32
+ const screen = GetDC(NULL)
33
+ const mem = CreateCompatibleDC(screen)
34
+ const bmp = screen ? CreateCompatibleBitmap(screen, W, H) : 0
35
+ if (!screen || !mem || !bmp) {
36
+ t.checkTrue('GDI setup (GetDC / CreateCompatibleDC / CreateCompatibleBitmap)', false)
37
+ return
38
+ }
39
+ const oldBmp = SelectObject(mem, bmp)
40
+ const pxB = new PtrArrayBuffer(W * H * 4)
41
+ const pxU8 = new Uint8Array(pxB)
42
+ const bmi = BITMAPINFOHEADER.encode({
43
+ biSize: BITMAPINFOHEADER.size,
44
+ biWidth: W,
45
+ biHeight: -H, // 负 = 顶向下
46
+ biPlanes: 1,
47
+ biBitCount: 32,
48
+ biSizeImage: W * H * 4,
49
+ })
50
+ // 读回当前位图像素(emitDib 同模式:32bpp BI_RGB = [B,G,R,x] 字节序)。
51
+ // 失败置哨兵(px() 返回 -1)—— 像素断言 FAIL 出可诊断值,不炸整个 suite。
52
+ let pxOk = true
53
+ const readPx = (): void => {
54
+ pxOk = GetDIBits(mem, bmp, 0, H, pxB, bmi.ptr, 0) === H
55
+ }
56
+ const px = (x: number, y: number): number => {
57
+ if (!pxOk) return -1
58
+ const i = (y * W + x) * 4
59
+ return (pxU8[i]! << 16) | (pxU8[i + 1]! << 8) | pxU8[i + 2]! // B,G,R → COLORREF
60
+ }
61
+ const clearWhite = (): void => {
62
+ PatBlt(mem, 0, 0, W, H, gui.RasterOp.WHITENESS) // 与画刷无关的整白
63
+ readPx()
64
+ }
65
+ clearWhite()
66
+ t.check('baseline = white', WHITE, px(5, 5))
67
+ t.checkTrue('GetDIBits full read', pxOk)
68
+
69
+ t.section('state: return-previous semantics')
70
+ t.check('fresh DC default bkMode = OPAQUE',
71
+ gui.BackgroundMode.OPAQUE, SetBkMode(mem, gui.BackgroundMode.OPAQUE))
72
+ t.check('SetBkMode returns previous', gui.BackgroundMode.OPAQUE,
73
+ SetBkMode(mem, gui.BackgroundMode.TRANSPARENT))
74
+ SetBkMode(mem, gui.BackgroundMode.OPAQUE)
75
+ SetTextColor(mem, RED) // 返回默认色(不假设)
76
+ t.check('SetTextColor returns previous', RED, SetTextColor(mem, BLUE))
77
+ SetTextColor(mem, 0)
78
+ SetBkColor(mem, RED)
79
+ t.check('SetBkColor returns previous', RED, SetBkColor(mem, WHITE))
80
+
81
+ t.section('type layer: enum nominal + position forms')
82
+ // 类型专用块(只过 tsc、不执行)
83
+ const typeOnly: unknown = () => {
84
+ // @ts-expect-error 跨枚举 nominal:BackgroundMode 成员进不了 PenStyle 位
85
+ CreatePen(gui.BackgroundMode.TRANSPARENT, 1, 0)
86
+ // @ts-expect-error '<HDC>ptr!' 禁空位不收 NULL
87
+ Rectangle(NULL, 0, 0, 1, 1)
88
+ // @ts-expect-error encoder 数组位不收裸对象(单对象对 C 是越界基址)
89
+ Polyline(0 as Ptr<'HDC'>, { x: 1, y: 2 }, 1)
90
+ // @ts-expect-error MoveToEx 出参位刻意不注册 encoder:只收 .ptr / NULL,不收对象形
91
+ MoveToEx(0 as Ptr<'HDC'>, 0, 0, { x: 1, y: 2 })
92
+ // @ts-expect-error <WCHAR>ptr 文本位收 string / NULL,不收裸数字
93
+ TextOut(0 as Ptr<'HDC'>, 0, 0, 123, 1)
94
+ // @ts-expect-error 跨枚举 nominal:PenStyle 成员进不了 StockObject 位
95
+ GetStockObject(gui.PenStyle.SOLID)
96
+ // @ts-expect-error 跨枚举 nominal:HatchStyle 成员进不了 PolyFillMode 位
97
+ SetPolyFillMode(0 as Ptr<'HDC'>, gui.HatchStyle.CROSS)
98
+ // @ts-expect-error u32@RasterOp 标注拦枚举外光栅码(字面量不匹配成员值)
99
+ PatBlt(0 as Ptr<'HDC'>, 0, 0, 1, 1, 0x123456)
100
+ // @ts-expect-error '<SIZE>ptr!' 出参位不收 NULL
101
+ GetTextExtentPoint32(0 as Ptr<'HDC'>, 'x', 1, NULL)
102
+ // @ts-expect-error 跨枚举 nominal:StretchBltMode 成员进不了 ArcDirection 位
103
+ SetArcDirection(0 as Ptr<'HDC'>, gui.StretchBltMode.HALFTONE)
104
+ }
105
+ t.checkTrue('type-only block wired', typeof typeOnly === 'function')
106
+
107
+ t.section('SaveDC/RestoreDC: state stack rollback')
108
+ // 先定基态(断言不依赖前面 section 的收尾值)
109
+ SetTextColor(mem, 0)
110
+ SetBkMode(mem, gui.BackgroundMode.OPAQUE)
111
+ SetBkColor(mem, WHITE)
112
+ const saved = SaveDC(mem)
113
+ t.checkTrue(`SaveDC state id ≠ 0 (${saved})`, saved !== 0)
114
+ // 打乱:三项绘图状态 + 换一支笔
115
+ SetTextColor(mem, RED)
116
+ SetBkMode(mem, gui.BackgroundMode.TRANSPARENT)
117
+ SetBkColor(mem, BLUE)
118
+ const junkPen = CreatePen(gui.PenStyle.DASH, 1, GREEN)
119
+ SelectObject(mem, junkPen)
120
+ t.checkTrue('RestoreDC ok', RestoreDC(mem, saved) !== 0)
121
+ DeleteObject(junkPen) // 恢复后已卸下,可安全删
122
+ // 逐项回滚断言:各 Set 返回的就是回滚后的当前值
123
+ t.check('text color rolled back', 0, SetTextColor(mem, RED))
124
+ SetTextColor(mem, 0)
125
+ t.check('bkMode rolled back', gui.BackgroundMode.OPAQUE,
126
+ SetBkMode(mem, gui.BackgroundMode.TRANSPARENT))
127
+ SetBkMode(mem, gui.BackgroundMode.OPAQUE)
128
+ t.check('bkColor rolled back', WHITE, SetBkColor(mem, RED))
129
+ SetBkColor(mem, WHITE)
130
+
131
+ t.section('Rectangle: brush fill (inside/outside pixels)')
132
+ clearWhite()
133
+ const brush = CreateSolidBrush(RED)
134
+ t.checkTrue('brush created', !!brush)
135
+ const oldBrush = SelectObject(mem, brush)
136
+ Rectangle(mem, 10, 10, 50, 30)
137
+ readPx()
138
+ t.check('inside = red', RED, px(20, 20))
139
+ t.check('outside left-top = white', WHITE, px(9, 9))
140
+ t.check('outside right-bottom = white', WHITE, px(60, 40))
141
+ SelectObject(mem, oldBrush)
142
+ DeleteObject(brush)
143
+
144
+ t.section('Polyline: encoder array position + NULL passthrough')
145
+ clearWhite()
146
+ const pen = CreatePen(gui.PenStyle.SOLID, 1, GREEN)
147
+ t.checkTrue('pen created', !!pen)
148
+ const oldPen = SelectObject(mem, pen)
149
+ t.checkTrue('Polyline success',
150
+ Polyline(mem, [{ x: 0, y: 100 }, { x: 100, y: 100 }], 2) !== 0)
151
+ readPx()
152
+ t.check('on-line = green', GREEN, px(50, 100))
153
+ t.check('row above = white', WHITE, px(50, 99))
154
+ t.check('row below = white', WHITE, px(50, 101))
155
+ // 空位语义:encoder 对 [] fail-loud(PAB(0) 永不产出),NULL 由调用方显式传
156
+ // —— C 拒收 NULL 返回 0。
157
+ let arrErr = ''
158
+ try { Polyline(mem, [], 0) } catch (e) { arrErr = String(e) }
159
+ t.checkTrue('[] fail-loud at encoder (never PAB(0))', arrErr.includes('non-empty'))
160
+ t.check('NULL position (C returns 0)', 0, Polyline(mem, NULL, 0))
161
+
162
+ t.section('MoveToEx/LineTo: out-param + line pixels')
163
+ t.checkTrue('MoveToEx NULL lppt ok', MoveToEx(mem, 10, 10, NULL) !== 0)
164
+ t.checkTrue('LineTo ok', LineTo(mem, 10, 50) !== 0) // 竖线,不含端点 → 行 10..49
165
+ readPx()
166
+ t.check('on-line = green', GREEN, px(10, 30))
167
+ t.check('col-1 = white', WHITE, px(9, 30))
168
+ t.check('col+1 = white', WHITE, px(11, 30))
169
+ // 当前位置现为 (10,50):MoveToEx 落新位 (77,88),旧位置写进出参(.ptr 直通通路)
170
+ const prev = POINT.alloc()
171
+ t.checkTrue('MoveToEx out slot ok', MoveToEx(mem, 77, 88, prev.ptr) !== 0)
172
+ const oldPos = POINT.decode(prev.ptr)
173
+ t.check('MoveToEx out.x', 10, oldPos.x)
174
+ t.check('MoveToEx out.y', 50, oldPos.y)
175
+ LineTo(mem, 77, 120) // 从 (77,88) 起画 → 列 77
176
+ readPx()
177
+ t.check('current position moved', GREEN, px(77, 100))
178
+ SelectObject(mem, oldPen)
179
+ DeleteObject(pen)
180
+
181
+ t.section('SetROP2: R2_XORPEN reversible')
182
+ PatBlt(mem, 0, 0, W, H, gui.RasterOp.BLACKNESS) // 黑底:黑^绿=绿,绿^绿=黑
183
+ t.check('default raster mode = R2_COPYPEN', gui.RasterMode.R2_COPYPEN,
184
+ SetROP2(mem, gui.RasterMode.R2_XORPEN)) // 返回旧值 = 默认,同时设为 XOR
185
+ const xorPen = CreatePen(gui.PenStyle.SOLID, 1, GREEN)
186
+ const xorOld = SelectObject(mem, xorPen)
187
+ MoveToEx(mem, 10, 200, NULL)
188
+ LineTo(mem, 100, 200)
189
+ readPx()
190
+ t.check('first pass = green (black XOR green)', GREEN, px(50, 200))
191
+ MoveToEx(mem, 10, 200, NULL)
192
+ LineTo(mem, 100, 200) // 同路径重画 → XOR 抵消
193
+ readPx()
194
+ t.check('second pass cancels back to black', BLACK, px(50, 200))
195
+ SelectObject(mem, xorOld)
196
+ DeleteObject(xorPen)
197
+ SetROP2(mem, gui.RasterMode.R2_COPYPEN) // 恢复
198
+
199
+ t.section('Polygon: fill + outline (auto-close)')
200
+ clearWhite()
201
+ const pBrush = CreateSolidBrush(BLUE)
202
+ const oldPB = SelectObject(mem, pBrush)
203
+ t.checkTrue('Polygon success',
204
+ Polygon(mem, [{ x: 200, y: 20 }, { x: 260, y: 20 }, { x: 230, y: 70 }], 3) !== 0)
205
+ readPx()
206
+ t.check('inside triangle = blue', BLUE, px(230, 40))
207
+ t.check('above triangle = white', WHITE, px(230, 10))
208
+ t.check('left of triangle = white', WHITE, px(180, 40))
209
+ SelectObject(mem, oldPB)
210
+ DeleteObject(pBrush)
211
+
212
+ t.section('Ellipse / RoundRect: fill')
213
+ clearWhite()
214
+ const sBrush = CreateSolidBrush(RED)
215
+ const oldSB = SelectObject(mem, sBrush)
216
+ Ellipse(mem, 100, 100, 140, 140)
217
+ readPx()
218
+ t.check('ellipse center = red', RED, px(120, 120))
219
+ t.check('ellipse corner = white', WHITE, px(101, 101))
220
+ RoundRect(mem, 200, 100, 240, 140, 10, 10)
221
+ readPx()
222
+ t.check('roundrect center = red', RED, px(220, 120))
223
+ t.check('rounded corner cut = white', WHITE, px(201, 101))
224
+ SelectObject(mem, oldSB)
225
+ DeleteObject(sBrush)
226
+
227
+ t.section('Arc/Chord/Pie: angular shapes')
228
+ // 圆 (100,100)-(200,200) 心 (150,150) r=50;起 12 点 (150,100) → 终 9 点 (100,150):
229
+ // GDI 默认逆时针 12→11→10→9 = 左上 90° 弧(SetArcDirection 可改,见 MSDN Arc)
230
+ clearWhite()
231
+ const arcPen = CreatePen(gui.PenStyle.SOLID, 1, RED)
232
+ const arcOldPen = SelectObject(mem, arcPen)
233
+ Arc(mem, 100, 100, 200, 200, 150, 100, 100, 150)
234
+ readPx()
235
+ let arcRed = 0 // 扫圆环带(圆心距 48..52)数红 = 弧存在
236
+ for (let y = 98; y < 203; y++) {
237
+ for (let x = 98; x < 203; x++) {
238
+ const dx = x - 150
239
+ const dy = y - 150
240
+ const d2 = dx * dx + dy * dy
241
+ if (d2 >= 48 * 48 && d2 <= 52 * 52 && px(x, y) === RED) arcRed++
242
+ }
243
+ }
244
+ t.checkTrue(`arc ring pixels (${arcRed})`, arcRed > 20)
245
+ t.check('arc interior untouched', WHITE, px(150, 150))
246
+ SelectObject(mem, arcOldPen)
247
+ DeleteObject(arcPen)
248
+
249
+ clearWhite()
250
+ const pieBrush = CreateSolidBrush(RED)
251
+ const pieOldB = SelectObject(mem, pieBrush)
252
+ Pie(mem, 100, 100, 200, 200, 150, 100, 100, 150)
253
+ readPx()
254
+ t.check('pie sector filled near apex', RED, px(140, 140)) // 45° 夹角内部(顶点 (150,150)
255
+ t.check('pie swept quadrant filled', RED, px(118, 128)) // 恰在半径边界行上不填——挪开)
256
+ t.check('pie opposite side empty', WHITE, px(180, 180))
257
+ SelectObject(mem, pieOldB)
258
+ DeleteObject(pieBrush)
259
+
260
+ clearWhite()
261
+ const chBrush = CreateSolidBrush(RED)
262
+ const chOldB = SelectObject(mem, chBrush)
263
+ Chord(mem, 100, 100, 200, 200, 150, 100, 100, 150)
264
+ readPx()
265
+ // 弦 (150,100)-(100,150):直线 x+y=250;左上小弧侧 = x+y < 250 一侧
266
+ t.check('chord band filled (arc side)', RED, px(118, 128)) // 246 < 250、圆内
267
+ t.check('chord excludes center side', WHITE, px(150, 150)) // 300 > 250 → 弦另一侧
268
+ SelectObject(mem, chOldB)
269
+ DeleteObject(chBrush)
270
+
271
+ t.section('SetArcDirection/GetArcDirection: sweep flip')
272
+ clearWhite()
273
+ const dirBrush = CreateSolidBrush(RED)
274
+ const dirOldB = SelectObject(mem, dirBrush)
275
+ t.check('default arc direction = counterclockwise', gui.ArcDirection.COUNTERCLOCKWISE,
276
+ GetArcDirection(mem))
277
+ t.check('SetArcDirection returns previous', gui.ArcDirection.COUNTERCLOCKWISE,
278
+ SetArcDirection(mem, gui.ArcDirection.CLOCKWISE))
279
+ Pie(mem, 100, 100, 200, 200, 150, 100, 100, 150) // 起 12 点终 9 点:顺时针 → 右下 270°
280
+ readPx()
281
+ t.check('clockwise sweep fills opposite side', RED, px(180, 180))
282
+ t.check('clockwise sweep skips upper-left', WHITE, px(118, 128))
283
+ SetArcDirection(mem, gui.ArcDirection.COUNTERCLOCKWISE) // 恢复默认
284
+ SelectObject(mem, dirOldB)
285
+ DeleteObject(dirBrush)
286
+
287
+ t.section('SetPolyFillMode: ALTERNATE vs WINDING (pentagram)')
288
+ clearWhite()
289
+ // 5 外顶点按 {5/2} 跳连(P0→P2→P4→P1→P3)= 真自相交五角星
290
+ const V: { x: number, y: number }[] = []
291
+ for (let i = 0; i < 5; i++) {
292
+ const a = -Math.PI / 2 + (i * 2 * Math.PI) / 5
293
+ V.push({ x: Math.round(150 + 55 * Math.cos(a)), y: Math.round(150 + 55 * Math.sin(a)) })
294
+ }
295
+ const star = [V[0]!, V[2]!, V[4]!, V[1]!, V[3]!]
296
+ t.check('default fill mode = ALTERNATE', gui.PolyFillMode.ALTERNATE,
297
+ SetPolyFillMode(mem, gui.PolyFillMode.ALTERNATE))
298
+ const starBrush = CreateSolidBrush(RED)
299
+ const starOldB = SelectObject(mem, starBrush)
300
+ Polygon(mem, star, 5)
301
+ readPx()
302
+ let starRed = 0
303
+ for (let y = 90; y < 210; y++) {
304
+ for (let x = 90; x < 210; x++) {
305
+ if (px(x, y) === RED) starRed++
306
+ }
307
+ }
308
+ t.checkTrue(`ALTERNATE: star drawn (${starRed}px)`, starRed > 200)
309
+ t.check('ALTERNATE: center hollow (even-odd)', WHITE, px(150, 150))
310
+ SetPolyFillMode(mem, gui.PolyFillMode.WINDING)
311
+ clearWhite()
312
+ Polygon(mem, star, 5)
313
+ readPx()
314
+ t.check('WINDING: center filled (nonzero)', RED, px(150, 150))
315
+ SelectObject(mem, starOldB)
316
+ DeleteObject(starBrush)
317
+ SetPolyFillMode(mem, gui.PolyFillMode.ALTERNATE) // 恢复默认
318
+
319
+ t.section('GetStockObject: stock objects, outline-only shape')
320
+ clearWhite()
321
+ const hollow = GetStockObject(gui.StockObject.NULL_BRUSH)
322
+ const stkPen = GetStockObject(gui.StockObject.BLACK_PEN)
323
+ t.checkTrue(`stock handles non-zero (0x${hollow.toString(16)}, 0x${stkPen.toString(16)})`,
324
+ hollow !== 0 && stkPen !== 0)
325
+ const stockOldBrush = SelectObject(mem, hollow)
326
+ const stockOldPen = SelectObject(mem, stkPen)
327
+ Rectangle(mem, 100, 100, 150, 140) // NULL_BRUSH → 只描边不填充
328
+ readPx()
329
+ let innerNz = 0 // 内部远离边线,应全白
330
+ for (let y = 105; y < 135; y++) {
331
+ for (let x = 105; x < 145; x++) {
332
+ if (px(x, y) !== WHITE) innerNz++
333
+ }
334
+ }
335
+ t.check('outline-only: interior untouched', 0, innerNz)
336
+ // 边界带扫描黑像素(不猜具体哪列——右/下边的 off-by-one 交给扫描容错)
337
+ let edgeBlack = 0
338
+ for (let y = 100; y < 140; y++) {
339
+ for (let x = 100; x < 150; x++) {
340
+ if ((x === 100 || y === 100 || x === 149 || y === 139) && px(x, y) === BLACK) edgeBlack++
341
+ }
342
+ }
343
+ t.checkTrue(`stock pen outline drawn (${edgeBlack}px)`, edgeBlack > 20)
344
+ SelectObject(mem, stockOldBrush)
345
+ SelectObject(mem, stockOldPen)
346
+ // stock 对象是系统共享的,不 DeleteObject
347
+
348
+ t.section('CreateHatchBrush: textured fill')
349
+ clearWhite()
350
+ SetBkMode(mem, gui.BackgroundMode.TRANSPARENT) // 间隙透底 = 白
351
+ const hatch = CreateHatchBrush(gui.HatchStyle.HORIZONTAL, RED)
352
+ t.checkTrue('hatch brush created', hatch !== 0)
353
+ const hatchOldB = SelectObject(mem, hatch)
354
+ Rectangle(mem, 20, 20, 120, 60)
355
+ readPx()
356
+ let hRed = 0
357
+ let hWhite = 0
358
+ let hOther = 0
359
+ for (let y = 30; y < 50; y++) {
360
+ for (let x = 30; x < 110; x++) {
361
+ const c = px(x, y)
362
+ if (c === RED) hRed++
363
+ else if (c === WHITE) hWhite++
364
+ else hOther++
365
+ }
366
+ }
367
+ t.checkTrue(`hatch lines present (red=${hRed})`, hRed > 50)
368
+ t.checkTrue(`transparent gaps (white=${hWhite})`, hWhite > 50)
369
+ t.check('no other colors in hatch', 0, hOther)
370
+ SelectObject(mem, hatchOldB)
371
+ DeleteObject(hatch)
372
+ SetBkMode(mem, gui.BackgroundMode.OPAQUE)
373
+
374
+ t.section('FrameRect/InvertRect: border and invert (user32)')
375
+ clearWhite()
376
+ const frBrush = CreateSolidBrush(RED)
377
+ FrameRect(mem, { left: 10, top: 10, right: 50, bottom: 40 }, frBrush)
378
+ readPx()
379
+ t.check('frame top edge red', RED, px(30, 10))
380
+ t.check('frame left edge red', RED, px(10, 25))
381
+ t.check('frame interior white', WHITE, px(30, 25))
382
+ DeleteObject(frBrush)
383
+
384
+ clearWhite()
385
+ InvertRect(mem, { left: 20, top: 20, right: 60, bottom: 50 })
386
+ readPx()
387
+ t.check('invert white to black', BLACK, px(40, 35))
388
+ t.check('outside rect untouched', WHITE, px(5, 5))
389
+ InvertRect(mem, { left: 20, top: 20, right: 60, bottom: 50 }) // 再反一次可逆
390
+ readPx()
391
+ t.check('second invert restores white', WHITE, px(40, 35))
392
+
393
+ t.section('StretchBlt: scaled copy + mode roundtrip')
394
+ clearWhite()
395
+ const stTmp = CreateCompatibleDC(mem)
396
+ if (!stTmp) {
397
+ t.checkTrue('CreateCompatibleDC (stretch source)', false)
398
+ return
399
+ }
400
+ const stBmp = CreateCompatibleBitmap(mem, 4, 4)
401
+ const stOldBmp = SelectObject(stTmp, stBmp)
402
+ const stFill = CreateSolidBrush(RED)
403
+ SelectObject(stTmp, stFill)
404
+ PatBlt(stTmp, 0, 0, 4, 4, gui.RasterOp.PATCOPY) // 源 4×4 全红
405
+ t.checkTrue('StretchBlt ok',
406
+ StretchBlt(mem, 10, 10, 40, 40, stTmp, 0, 0, 4, 4, gui.RasterOp.SRCCOPY) !== 0)
407
+ readPx()
408
+ t.check('scaled center red', RED, px(30, 30))
409
+ t.check('scaled near-origin red', RED, px(11, 11))
410
+ t.check('outside target untouched', WHITE, px(60, 30))
411
+ const stMode0 = GetStretchBltMode(mem)
412
+ t.check('SetStretchBltMode returns current', stMode0,
413
+ SetStretchBltMode(mem, gui.StretchBltMode.HALFTONE))
414
+ t.check('GetStretchBltMode = HALFTONE', gui.StretchBltMode.HALFTONE, GetStretchBltMode(mem))
415
+ SelectObject(stTmp, stOldBmp)
416
+ DeleteObject(stFill)
417
+ DeleteObject(stBmp)
418
+ DeleteDC(stTmp)
419
+
420
+ t.section('CreatePatternBrush: tiled bitmap fill')
421
+ clearWhite()
422
+ const pkTmp = CreateCompatibleDC(mem)
423
+ if (!pkTmp) {
424
+ t.checkTrue('CreateCompatibleDC (pattern source)', false)
425
+ return
426
+ }
427
+ const pkBmp = CreateCompatibleBitmap(mem, 4, 4)
428
+ const pkOldBmp = SelectObject(pkTmp, pkBmp)
429
+ const pkRed = CreateSolidBrush(RED)
430
+ SelectObject(pkTmp, pkRed)
431
+ PatBlt(pkTmp, 0, 0, 4, 4, gui.RasterOp.PATCOPY) // 红底
432
+ const pkWhite = CreateSolidBrush(WHITE)
433
+ SelectObject(pkTmp, pkWhite)
434
+ PatBlt(pkTmp, 2, 0, 2, 2, gui.RasterOp.PATCOPY) // 挖两白角 = 2×2 棋盘
435
+ PatBlt(pkTmp, 0, 2, 2, 2, gui.RasterOp.PATCOPY)
436
+ SelectObject(pkTmp, pkOldBmp) // 位图退出选入状态
437
+ const pkPat = CreatePatternBrush(pkBmp)
438
+ t.checkTrue('pattern brush created', pkPat !== 0)
439
+ const pkPatOld = SelectObject(mem, pkPat)
440
+ Rectangle(mem, 20, 20, 120, 120)
441
+ readPx()
442
+ t.check('tile (0,0) cell red', RED, px(24, 24)) // 平铺按 (x%4, y%4) 棋盘
443
+ t.check('tile (2,0) cell white', WHITE, px(26, 24))
444
+ t.check('tile (0,2) cell white', WHITE, px(24, 26))
445
+ t.check('tile (2,2) cell red', RED, px(26, 26))
446
+ t.check('tiled far cell red (25 cycles)', RED, px(100, 100)) // 100%4=0
447
+ SelectObject(mem, pkPatOld)
448
+ DeleteObject(pkPat)
449
+ DeleteObject(pkBmp)
450
+ DeleteObject(pkRed)
451
+ DeleteObject(pkWhite)
452
+ DeleteDC(pkTmp)
453
+
454
+ t.section('SetDIBitsToDevice/StretchDIBits: raw DIB buffer blit')
455
+ clearWhite()
456
+ // 4×4 RGBA 缓冲:上 2 行红、下 2 行蓝(biHeight<0 顶向下——专测行序不颠倒)
457
+ const dibBits = new PtrArrayBuffer(4 * 4 * 4)
458
+ const dibU8 = new Uint8Array(dibBits)
459
+ for (let y = 0; y < 4; y++) {
460
+ for (let x = 0; x < 4; x++) {
461
+ const i = (y * 4 + x) * 4 // 32bpp BI_RGB = [B,G,R,x]
462
+ dibU8[i] = y < 2 ? 0 : 255 // B:下半蓝
463
+ dibU8[i + 1] = 0
464
+ dibU8[i + 2] = y < 2 ? 255 : 0 // R:上半红
465
+ }
466
+ }
467
+ const dibBmi = BITMAPINFOHEADER.encode({
468
+ biSize: BITMAPINFOHEADER.size, biWidth: 4, biHeight: -4,
469
+ biPlanes: 1, biBitCount: 32, biSizeImage: 4 * 4 * 4,
470
+ })
471
+ t.checkTrue('SetDIBitsToDevice ok',
472
+ SetDIBitsToDevice(mem, 20, 20, 4, 4, 0, 0, 0, 4, dibBits, dibBmi, 0) === 4)
473
+ readPx()
474
+ t.check('dib top rows red (top-down)', RED, px(21, 21))
475
+ t.check('dib bottom rows blue', BLUE, px(21, 23))
476
+ t.check('dib outside untouched', WHITE, px(30, 21))
477
+ clearWhite()
478
+ t.checkTrue('StretchDIBits ok',
479
+ StretchDIBits(mem, 20, 20, 40, 40, 0, 0, 4, 4, dibBits, dibBmi, 0,
480
+ gui.RasterOp.SRCCOPY) === 4)
481
+ readPx()
482
+ t.check('stretched top half red', RED, px(40, 30))
483
+ t.check('stretched bottom half blue', BLUE, px(40, 50))
484
+ t.check('stretched outside untouched', WHITE, px(65, 30))
485
+
486
+ t.section('PlgBlt: parallelogram mapping')
487
+ clearWhite()
488
+ const plgTmp = CreateCompatibleDC(mem)
489
+ if (!plgTmp) { t.checkTrue('CreateCompatibleDC (plg src)', false); return }
490
+ const plgBmp = CreateCompatibleBitmap(mem, 40, 40)
491
+ const plgOldB = SelectObject(plgTmp, plgBmp)
492
+ const plgRed = CreateSolidBrush(RED)
493
+ SelectObject(plgTmp, plgRed)
494
+ PatBlt(plgTmp, 0, 0, 40, 40, gui.RasterOp.PATCOPY) // 源 40×40 全红
495
+ // 三点 TL(40,60) TR(160,60) BL(70,160) → 方向 s(120,0)+t(30,100),中心 (115,110)
496
+ t.checkTrue('PlgBlt ok',
497
+ PlgBlt(mem, [{ x: 40, y: 60 }, { x: 160, y: 60 }, { x: 70, y: 160 }],
498
+ plgTmp, 0, 0, 40, 40, NULL, 0, 0) !== 0)
499
+ readPx()
500
+ t.check('parallelogram center red', RED, px(115, 110))
501
+ t.check('left of slanted edge white', WHITE, px(40, 160))
502
+ SelectObject(plgTmp, plgOldB)
503
+ DeleteObject(plgRed)
504
+ DeleteObject(plgBmp)
505
+ DeleteDC(plgTmp)
506
+
507
+ t.section('CreateBitmap + MaskBlt: monochrome row mask')
508
+ clearWhite()
509
+ const mkTmp = CreateCompatibleDC(mem)
510
+ if (!mkTmp) { t.checkTrue('CreateCompatibleDC (mask src)', false); return }
511
+ const mkBmp = CreateCompatibleBitmap(mem, 4, 4)
512
+ const mkOldB = SelectObject(mkTmp, mkBmp)
513
+ const mkRed = CreateSolidBrush(RED)
514
+ SelectObject(mkTmp, mkRed)
515
+ PatBlt(mkTmp, 0, 0, 4, 4, gui.RasterOp.PATCOPY) // 源 4×4 全红
516
+ // 1bpp 掩码 4×4:行按 word(2B) 对齐(CreateBitmap 惯例,非 DIB 的 4B)——
517
+ // 行 0/2 = 0xFF(白)、行 1/3 = 0x00(黑)
518
+ const mkBits = new PtrArrayBuffer(8)
519
+ const mkU8 = new Uint8Array(mkBits)
520
+ mkU8[0] = 0xFF
521
+ mkU8[4] = 0xFF
522
+ const mask = CreateBitmap(4, 4, 1, 1, mkBits)
523
+ t.checkTrue('CreateBitmap(1bpp) ok', mask !== 0)
524
+ PatBlt(mem, 30, 30, 4, 4, gui.RasterOp.BLACKNESS) // 目标底黑 → back 白可辨
525
+ // MAKEROP4(fore=SRCCOPY 取源, back=WHITENESS 画白):back 码在 bit31..24、fore 在 bit23..16
526
+ const rop4 = (((gui.RasterOp.WHITENESS << 8) & 0xFF000000) | gui.RasterOp.SRCCOPY) >>> 0
527
+ t.checkTrue('MaskBlt ok',
528
+ MaskBlt(mem, 30, 30, 4, 4, mkTmp, 0, 0, mask, 0, 0, rop4) !== 0)
529
+ readPx()
530
+ t.check('mask row0 = fore (red)', RED, px(31, 30))
531
+ t.check('mask row1 = back (white)', WHITE, px(31, 31))
532
+ t.check('mask row2 = fore (red)', RED, px(31, 32))
533
+ t.check('mask row3 = back (white)', WHITE, px(31, 33))
534
+ SelectObject(mkTmp, mkOldB)
535
+ DeleteObject(mask)
536
+ DeleteObject(mkRed)
537
+ DeleteObject(mkBmp)
538
+ DeleteDC(mkTmp)
539
+
540
+ t.section('TransparentBlt: color-key transparency (msimg32)')
541
+ clearWhite()
542
+ const trTmp = CreateCompatibleDC(mem)
543
+ if (!trTmp) { t.checkTrue('CreateCompatibleDC (tr src)', false); return }
544
+ const trBmp = CreateCompatibleBitmap(mem, 40, 16)
545
+ const trOldB = SelectObject(trTmp, trBmp)
546
+ const trRed = CreateSolidBrush(RED)
547
+ SelectObject(trTmp, trRed)
548
+ PatBlt(trTmp, 0, 0, 20, 16, gui.RasterOp.PATCOPY) // 左半红
549
+ const trBlue = CreateSolidBrush(BLUE)
550
+ SelectObject(trTmp, trBlue)
551
+ PatBlt(trTmp, 20, 0, 20, 16, gui.RasterOp.PATCOPY) // 右半蓝 = 键色
552
+ t.checkTrue('TransparentBlt ok',
553
+ TransparentBlt(mem, 20, 20, 40, 16, trTmp, 0, 0, 40, 16, BLUE) !== 0)
554
+ readPx()
555
+ t.check('non-key pixels copied (red)', RED, px(25, 25))
556
+ t.check('key pixels transparent (white shows through)', WHITE, px(55, 25))
557
+ SelectObject(trTmp, trOldB)
558
+ DeleteObject(trRed)
559
+ DeleteObject(trBlue)
560
+ DeleteObject(trBmp)
561
+ DeleteDC(trTmp)
562
+
563
+ t.section('AlphaBlend: constant alpha (by-value BLENDFUNCTION)')
564
+ // BLENDFUNCTION 按值传(<BLENDFUNCTION> 位,4B 聚合体)——错绑成 <N>ptr 会把
565
+ // 地址当 4 字节位图参数,GDI 读到垃圾,本段必然失败(真实消费者回归网)。
566
+ clearWhite()
567
+ const abTmp = CreateCompatibleDC(mem)
568
+ if (!abTmp) { t.checkTrue('CreateCompatibleDC (ab src)', false); return }
569
+ const abBmp = CreateCompatibleBitmap(mem, 40, 16)
570
+ const abOldB = SelectObject(abTmp, abBmp)
571
+ const abGreen = CreateSolidBrush(GREEN)
572
+ SelectObject(abTmp, abGreen)
573
+ PatBlt(abTmp, 0, 0, 40, 16, gui.RasterOp.PATCOPY) // 源 = 纯绿(不透明)
574
+ // 完全覆盖:SourceConstantAlpha=255、无调制(BlendFlags=0)、BlendOp=AC_SRC_OVER(0)
575
+ t.checkTrue('AlphaBlend full alpha ok',
576
+ AlphaBlend(mem, 20, 20, 40, 16, abTmp, 0, 0, 40, 16,
577
+ { BlendOp: 0, BlendFlags: 0, SourceConstantAlpha: 255, AlphaFormat: 0 }) !== 0)
578
+ readPx()
579
+ t.check('dest = src green (alpha 255 exact)', GREEN, px(25, 25))
580
+ // 半透明 ca=128:GDI 实测(AlphaFormat=0)按 premultiplied 公式
581
+ // out = src + dst*(255-ca)/255 —— 源值保留、目标衰减(MS Answers 已知行为,
582
+ // 与文档的非预乘公式不同):R/B = 255*127/255 ≈ 127、G = 255,±3 盖取整差异
583
+ t.checkTrue('AlphaBlend half alpha ok',
584
+ AlphaBlend(mem, 80, 20, 40, 16, abTmp, 0, 0, 40, 16,
585
+ { BlendOp: 0, BlendFlags: 0, SourceConstantAlpha: 128, AlphaFormat: 0 }) !== 0)
586
+ readPx()
587
+ const abHalf = px(85, 25)
588
+ const abR = abHalf & 0xff, abG = (abHalf >> 8) & 0xff, abB = (abHalf >> 16) & 0xff
589
+ t.checkTrue(`half-alpha R ≈127 (${abR})`, Math.abs(abR - 127) <= 3)
590
+ t.checkTrue(`half-alpha G ≈255 (src kept) (${abG})`, Math.abs(abG - 255) <= 3)
591
+ t.checkTrue(`half-alpha B ≈127 (${abB})`, Math.abs(abB - 127) <= 3)
592
+ // ca=0:文档「image is transparent」→ 目标不变;premultiplied 公式下
593
+ // saturate(src+dst) 也落在白 —— 两种解读殊途同归,锚点确定
594
+ t.checkTrue('AlphaBlend zero alpha ok',
595
+ AlphaBlend(mem, 140, 20, 40, 16, abTmp, 0, 0, 40, 16,
596
+ { BlendOp: 0, BlendFlags: 0, SourceConstantAlpha: 0, AlphaFormat: 0 }) !== 0)
597
+ readPx()
598
+ t.check('dest unchanged (alpha 0)', WHITE, px(145, 25))
599
+ SelectObject(abTmp, abOldB)
600
+ DeleteObject(abGreen)
601
+ DeleteObject(abBmp)
602
+ DeleteDC(abTmp)
603
+
604
+ t.section('TextOut: transparent background + text color')
605
+ clearWhite()
606
+ SetBkColor(mem, BLUE) // 衬底设蓝:若 TRANSPARENT 未生效,字底必现蓝块
607
+ SetBkMode(mem, gui.BackgroundMode.TRANSPARENT)
608
+ SetTextColor(mem, RED)
609
+ t.checkTrue('TextOut success', TextOut(mem, 20, 100, 'Hello GDI', 9) !== 0)
610
+ readPx()
611
+ let redPx = 0
612
+ let bluePx = 0
613
+ // 扫描框放宽:对齐默认形(TA_TOP 或 TA_BASELINE)都能罩住字形
614
+ for (let y = 84; y < 124; y++) {
615
+ for (let x = 16; x < 150; x++) {
616
+ const c = px(x, y)
617
+ if (c === RED) redPx++
618
+ else if (c === BLUE) bluePx++
619
+ }
620
+ }
621
+ t.checkTrue(`glyph pixels rendered (red=${redPx})`, redPx > 20)
622
+ t.checkTrue(`transparent bg: zero blue backdrop (blue=${bluePx})`, bluePx === 0)
623
+ SetBkMode(mem, gui.BackgroundMode.OPAQUE)
624
+ SetBkColor(mem, WHITE)
625
+ SetTextColor(mem, 0)
626
+
627
+ t.section('text metrics/align: extent + SetTextAlign + ExtTextOut')
628
+ clearWhite()
629
+ // (a) 量字:出参 SIZE(alloc().ptr 进、decode 出)
630
+ const sz = SIZE.alloc()
631
+ t.checkTrue('GetTextExtentPoint32 ok',
632
+ GetTextExtentPoint32(mem, 'Hello GDI', 9, sz.ptr) !== 0)
633
+ const ext = SIZE.decode(sz.ptr)
634
+ t.checkTrue(`extent cx sane (${ext.cx})`, (ext.cx ?? 0) >= 30 && (ext.cx ?? 0) <= 300)
635
+ t.checkTrue(`extent cy sane (${ext.cy})`, (ext.cy ?? 0) >= 8 && (ext.cy ?? 0) <= 64)
636
+
637
+ // (b) 行高度量(出参 TEXTMETRICW,与 (a) 同一默认字体)
638
+ const tmSlot = TEXTMETRICW.alloc()
639
+ t.checkTrue('GetTextMetrics ok', GetTextMetrics(mem, tmSlot.ptr) !== 0)
640
+ const tmv = TEXTMETRICW.decode(tmSlot.ptr)
641
+ t.checkTrue(`tmHeight sane (${tmv.tmHeight})`,
642
+ (tmv.tmHeight ?? 0) >= 8 && (tmv.tmHeight ?? 0) <= 64)
643
+ t.checkTrue(`tmAscent/descent sane (${tmv.tmAscent}/${tmv.tmDescent})`,
644
+ (tmv.tmAscent ?? 0) > 0 && (tmv.tmDescent ?? 0) >= 0)
645
+ t.checkTrue(`tmAveCharWidth sane (${tmv.tmAveCharWidth})`,
646
+ (tmv.tmAveCharWidth ?? 0) >= 4 && (tmv.tmAveCharWidth ?? 0) <= 48)
647
+
648
+ // (c) SetTextAlign 右对齐:字形全部落在锚点左侧
649
+ SetTextColor(mem, RED)
650
+ t.check('default align = 0 (LEFT|TOP|NOUPDATECP)', 0,
651
+ SetTextAlign(mem, gui.TextAlign.TA_RIGHT))
652
+ TextOut(mem, 200, 100, 'Right', 5)
653
+ readPx()
654
+ let rRight = 0
655
+ let rWrong = 0
656
+ for (let y = 96; y < 116; y++) {
657
+ for (let x = 140; x < 260; x++) {
658
+ const c = px(x, y)
659
+ if (c === RED) {
660
+ if (x < 200) rRight++
661
+ else if (x > 200) rWrong++
662
+ }
663
+ }
664
+ }
665
+ t.checkTrue(`right-aligned glyphs left of anchor (red=${rRight})`, rRight > 10)
666
+ t.check('no glyphs right of anchor', 0, rWrong)
667
+ SetTextAlign(mem, gui.TextAlign.TA_LEFT) // 恢复 0
668
+ SetTextColor(mem, 0)
669
+
670
+ // (d) ExtTextOut ETO_OPAQUE 衬底(lprc 对象形直传 = encoder 入参位)
671
+ SetTextColor(mem, RED)
672
+ SetBkColor(mem, BLUE)
673
+ t.checkTrue('ExtTextOut ETO_OPAQUE ok',
674
+ ExtTextOut(mem, 20, 100, gui.TextOutOptions.ETO_OPAQUE,
675
+ { left: 18, top: 98, right: 100, bottom: 118 }, 'Opaque', 6, NULL) !== 0)
676
+ readPx()
677
+ let oBlue = 0
678
+ let oRed = 0
679
+ for (let y = 99; y < 117; y++) {
680
+ for (let x = 19; x < 99; x++) {
681
+ const c = px(x, y)
682
+ if (c === BLUE) oBlue++
683
+ else if (c === RED) oRed++
684
+ }
685
+ }
686
+ t.checkTrue(`opaque backdrop painted (blue=${oBlue})`, oBlue > 500)
687
+ t.checkTrue(`glyphs over backdrop (red=${oRed})`, oRed > 10)
688
+ SetBkColor(mem, WHITE)
689
+ SetTextColor(mem, 0)
690
+
691
+ t.section('CreateFontIndirect: scaled glyphs')
692
+ clearWhite()
693
+ SetTextColor(mem, RED)
694
+ TextOut(mem, 20, 40, 'Big', 3)
695
+ readPx()
696
+ let smPx = 0
697
+ for (let y = 35; y < 95; y++) {
698
+ for (let x = 15; x < 150; x++) {
699
+ if (px(x, y) === RED) smPx++
700
+ }
701
+ }
702
+ const bigFont = CreateFontIndirect({ lfHeight: -48, lfFaceName: 'Arial' })
703
+ t.checkTrue('scaled font created', bigFont !== 0)
704
+ const bigOld = SelectObject(mem, bigFont)
705
+ clearWhite()
706
+ TextOut(mem, 20, 40, 'Big', 3)
707
+ readPx()
708
+ let bgPx = 0
709
+ for (let y = 35; y < 95; y++) {
710
+ for (let x = 15; x < 150; x++) {
711
+ if (px(x, y) === RED) bgPx++
712
+ }
713
+ }
714
+ t.checkTrue(`scaled glyphs bigger (${bgPx} vs ${smPx})`, bgPx > smPx * 2)
715
+ SelectObject(mem, bigOld)
716
+ DeleteObject(bigFont)
717
+ SetTextColor(mem, 0)
718
+
719
+ t.section('cleanup')
720
+ SelectObject(mem, oldBmp)
721
+ t.checkTrue('DeleteObject(bitmap)', DeleteObject(bmp) !== 0)
722
+ t.checkTrue('DeleteDC', DeleteDC(mem) !== 0)
723
+ t.checkTrue('ReleaseDC', ReleaseDC(NULL, screen) !== 0)
724
+ }
725
+ }