feat(diagnostic): 完成阶段 0C 视觉与 IME 坐标探测
This commit is contained in:
@@ -0,0 +1,436 @@
|
||||
#include "VisualProbeCommon.h"
|
||||
|
||||
#include <imm.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <sstream>
|
||||
|
||||
#pragma comment(lib, "Imm32.lib")
|
||||
|
||||
namespace VisualProbe
|
||||
{
|
||||
namespace
|
||||
{
|
||||
struct Anchor
|
||||
{
|
||||
const char* id;
|
||||
POINT controlOrigin;
|
||||
POINT localCaretTopLeft;
|
||||
POINT localCaretBottom;
|
||||
RECT localLineRect;
|
||||
};
|
||||
|
||||
struct ImePositionEvent
|
||||
{
|
||||
std::uint64_t sequence = 0;
|
||||
std::string stage;
|
||||
std::string positionTrigger = "none";
|
||||
std::string anchorId;
|
||||
DWORD threadId = 0;
|
||||
std::uintptr_t hkl = 0;
|
||||
std::wstring imeDescription;
|
||||
std::wstring imeFileName;
|
||||
bool apiAttempted = false;
|
||||
bool compositionActiveAtAttempt = false;
|
||||
bool contextAvailable = false;
|
||||
bool compositionSucceeded = false;
|
||||
bool candidateSucceeded = false;
|
||||
POINT hwndRelativePoint{};
|
||||
RECT hwndRelativeLineRect{};
|
||||
POINT screenPoint{};
|
||||
};
|
||||
|
||||
struct ImePositionState
|
||||
{
|
||||
HWND hwnd = nullptr;
|
||||
WNDPROC previousWndProc = nullptr;
|
||||
std::atomic<int> activeAnchor{ 1 };
|
||||
std::atomic<bool> compositionActive{ false };
|
||||
std::atomic<bool> closeRequested{ false };
|
||||
std::atomic<std::uint64_t> nextSequence{ 0 };
|
||||
std::mutex eventsMutex;
|
||||
std::vector<ImePositionEvent> events;
|
||||
};
|
||||
|
||||
constexpr std::array<Anchor, 3> Anchors{
|
||||
Anchor{ "anchor_top_left_safe", POINT{ 80, 125 }, POINT{ 30, 35 }, POINT{ 30, 61 }, RECT{ 10, 30, 250, 66 } },
|
||||
Anchor{ "anchor_center", POINT{ 350, 305 }, POINT{ 130, 55 }, POINT{ 130, 81 }, RECT{ 10, 50, 260, 86 } },
|
||||
Anchor{ "anchor_bottom_right_safe", POINT{ 650, 520 }, POINT{ 180, 45 }, POINT{ 180, 71 }, RECT{ 10, 40, 230, 76 } }
|
||||
};
|
||||
|
||||
ImePositionState* activeState = nullptr;
|
||||
|
||||
POINT surfaceCaret(const Anchor& anchor) noexcept
|
||||
{
|
||||
return POINT{
|
||||
anchor.controlOrigin.x + anchor.localCaretBottom.x,
|
||||
anchor.controlOrigin.y + anchor.localCaretBottom.y
|
||||
};
|
||||
}
|
||||
|
||||
std::wstring imeDescription(HKL hkl)
|
||||
{
|
||||
const UINT length = ImmGetDescriptionW(hkl, nullptr, 0);
|
||||
if (!length) return {};
|
||||
std::wstring value(length + 1, L'\0');
|
||||
const UINT written = ImmGetDescriptionW(hkl, value.data(), static_cast<UINT>(value.size()));
|
||||
value.resize(written);
|
||||
return value;
|
||||
}
|
||||
|
||||
std::wstring imeFileName(HKL hkl)
|
||||
{
|
||||
const UINT length = ImmGetIMEFileNameW(hkl, nullptr, 0);
|
||||
if (!length) return {};
|
||||
std::wstring value(length + 1, L'\0');
|
||||
const UINT written = ImmGetIMEFileNameW(hkl, value.data(), static_cast<UINT>(value.size()));
|
||||
value.resize(written);
|
||||
return value;
|
||||
}
|
||||
|
||||
void appendEvent(ImePositionState& state, ImePositionEvent event)
|
||||
{
|
||||
event.sequence = state.nextSequence.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
std::lock_guard<std::mutex> lock(state.eventsMutex);
|
||||
state.events.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void recordLifecycle(ImePositionState& state, const char* stage)
|
||||
{
|
||||
const int anchorIndex = state.activeAnchor.load(std::memory_order_acquire);
|
||||
ImePositionEvent event;
|
||||
event.stage = stage;
|
||||
event.anchorId = Anchors[static_cast<std::size_t>(anchorIndex)].id;
|
||||
event.threadId = GetCurrentThreadId();
|
||||
const HKL hkl = GetKeyboardLayout(0);
|
||||
event.hkl = reinterpret_cast<std::uintptr_t>(hkl);
|
||||
event.imeDescription = imeDescription(hkl);
|
||||
event.imeFileName = imeFileName(hkl);
|
||||
appendEvent(state, std::move(event));
|
||||
}
|
||||
|
||||
void configureImePosition(ImePositionState& state, const char* stage, const char* positionTrigger)
|
||||
{
|
||||
const int anchorIndex = state.activeAnchor.load(std::memory_order_acquire);
|
||||
const Anchor& anchor = Anchors[static_cast<std::size_t>(anchorIndex)];
|
||||
ImePositionEvent event;
|
||||
event.stage = stage;
|
||||
event.positionTrigger = positionTrigger;
|
||||
event.anchorId = anchor.id;
|
||||
event.threadId = GetCurrentThreadId();
|
||||
const HKL hkl = GetKeyboardLayout(0);
|
||||
event.hkl = reinterpret_cast<std::uintptr_t>(hkl);
|
||||
event.imeDescription = imeDescription(hkl);
|
||||
event.imeFileName = imeFileName(hkl);
|
||||
event.apiAttempted = true;
|
||||
event.compositionActiveAtAttempt = state.compositionActive.load(std::memory_order_acquire);
|
||||
event.hwndRelativePoint = surfaceCaret(anchor);
|
||||
event.hwndRelativeLineRect = RECT{
|
||||
anchor.controlOrigin.x + anchor.localLineRect.left,
|
||||
anchor.controlOrigin.y + anchor.localLineRect.top,
|
||||
anchor.controlOrigin.x + anchor.localLineRect.right,
|
||||
anchor.controlOrigin.y + anchor.localLineRect.bottom
|
||||
};
|
||||
event.screenPoint = event.hwndRelativePoint;
|
||||
ClientToScreen(state.hwnd, &event.screenPoint);
|
||||
|
||||
HIMC context = ImmGetContext(state.hwnd);
|
||||
event.contextAvailable = context != 0;
|
||||
if (context)
|
||||
{
|
||||
COMPOSITIONFORM composition{};
|
||||
composition.dwStyle = CFS_POINT;
|
||||
composition.ptCurrentPos = event.hwndRelativePoint;
|
||||
event.compositionSucceeded = ImmSetCompositionWindow(context, &composition) != FALSE;
|
||||
|
||||
CANDIDATEFORM candidate{};
|
||||
candidate.dwIndex = 0;
|
||||
candidate.dwStyle = CFS_EXCLUDE;
|
||||
candidate.ptCurrentPos = event.hwndRelativePoint;
|
||||
candidate.rcArea = event.hwndRelativeLineRect;
|
||||
event.candidateSucceeded = ImmSetCandidateWindow(context, &candidate) != FALSE;
|
||||
ImmReleaseContext(state.hwnd, context);
|
||||
}
|
||||
appendEvent(state, std::move(event));
|
||||
}
|
||||
|
||||
LRESULT CALLBACK ProbeWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
ImePositionState* state = activeState;
|
||||
if (!state)
|
||||
return DefWindowProcA(hwnd, message, wParam, lParam);
|
||||
|
||||
if (message == WM_KEYDOWN && wParam >= VK_F1 && wParam <= VK_F3)
|
||||
{
|
||||
state->activeAnchor.store(static_cast<int>(wParam - VK_F1), std::memory_order_release);
|
||||
configureImePosition(*state, "anchor_preconfigure", "anchor_change");
|
||||
return 0;
|
||||
}
|
||||
else if (message == WM_IME_STARTCOMPOSITION)
|
||||
{
|
||||
state->compositionActive.store(true, std::memory_order_release);
|
||||
configureImePosition(*state, "ime_start", "ime_lifecycle");
|
||||
}
|
||||
else if (message == WM_IME_COMPOSITION)
|
||||
{
|
||||
configureImePosition(*state, "ime_composition", "ime_lifecycle");
|
||||
}
|
||||
else if (message == WM_IME_ENDCOMPOSITION)
|
||||
{
|
||||
recordLifecycle(*state, "ime_end");
|
||||
state->compositionActive.store(false, std::memory_order_release);
|
||||
}
|
||||
else if (message == WM_INPUTLANGCHANGE)
|
||||
{
|
||||
recordLifecycle(*state, "input_language_change");
|
||||
}
|
||||
else if (message == WM_CLOSE)
|
||||
{
|
||||
recordLifecycle(*state, "close_requested");
|
||||
state->closeRequested.store(true, std::memory_order_release);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return state->previousWndProc
|
||||
? CallWindowProcA(state->previousWndProc, hwnd, message, wParam, lParam)
|
||||
: DefWindowProcA(hwnd, message, wParam, lParam);
|
||||
}
|
||||
|
||||
void drawAnchorScreen(int activeAnchorIndex)
|
||||
{
|
||||
SetWorkingImage(nullptr);
|
||||
BeginBatchDraw();
|
||||
setbkcolor(RGB(238, 241, 246));
|
||||
cleardevice();
|
||||
setbkmode(TRANSPARENT);
|
||||
settextstyle(22, 0, "Microsoft YaHei UI");
|
||||
settextcolor(RGB(30, 45, 70));
|
||||
outtextxy(30, 25, "IME position probe: F1/F2/F3 select anchor; close window when done");
|
||||
outtextxy(30, 58, "Use only fixed non-sensitive input: ceshi; inspect candidate position; Esc cancels");
|
||||
for (std::size_t index = 0; index < Anchors.size(); ++index)
|
||||
{
|
||||
const Anchor& anchor = Anchors[index];
|
||||
const bool selected = static_cast<int>(index) == activeAnchorIndex;
|
||||
const COLORREF color = selected ? RGB(220, 60, 45) : RGB(45, 125, 190);
|
||||
const POINT caret = surfaceCaret(anchor);
|
||||
setlinecolor(color);
|
||||
setlinestyle(PS_SOLID, selected ? 3 : 1);
|
||||
rectangle(anchor.controlOrigin.x, anchor.controlOrigin.y,
|
||||
anchor.controlOrigin.x + anchor.localLineRect.right,
|
||||
anchor.controlOrigin.y + anchor.localLineRect.bottom + 12);
|
||||
line(caret.x, caret.y - 26, caret.x, caret.y + 4);
|
||||
line(caret.x - 5, caret.y, caret.x + 5, caret.y);
|
||||
settextcolor(color);
|
||||
const std::string label = std::string("F") + std::to_string(index + 1) + " " + anchor.id;
|
||||
outtextxy(anchor.controlOrigin.x + 8, anchor.controlOrigin.y + 5, label.c_str());
|
||||
}
|
||||
EndBatchDraw();
|
||||
}
|
||||
|
||||
std::string normalizedRelative(const std::filesystem::path& path)
|
||||
{
|
||||
std::string value = path.generic_string();
|
||||
std::replace(value.begin(), value.end(), '\\', '/');
|
||||
return value;
|
||||
}
|
||||
|
||||
void writeAnchor(std::ostream& output, const Anchor& anchor)
|
||||
{
|
||||
const POINT surfaceTopLeft{
|
||||
anchor.controlOrigin.x + anchor.localCaretTopLeft.x,
|
||||
anchor.controlOrigin.y + anchor.localCaretTopLeft.y
|
||||
};
|
||||
const POINT surfaceBottom = surfaceCaret(anchor);
|
||||
RECT surfaceLine{
|
||||
anchor.controlOrigin.x + anchor.localLineRect.left,
|
||||
anchor.controlOrigin.y + anchor.localLineRect.top,
|
||||
anchor.controlOrigin.x + anchor.localLineRect.right,
|
||||
anchor.controlOrigin.y + anchor.localLineRect.bottom
|
||||
};
|
||||
output << "{\"syntheticAnchorId\":" << jsonString(anchor.id)
|
||||
<< ",\"controlOrigin\":";
|
||||
writePointJson(output, anchor.controlOrigin);
|
||||
output << ",\"controlLocalCaretTopLeft\":";
|
||||
writePointJson(output, anchor.localCaretTopLeft);
|
||||
output << ",\"controlLocalCaretBottom\":";
|
||||
writePointJson(output, anchor.localCaretBottom);
|
||||
output << ",\"controlLocalLineRect\":";
|
||||
writeRectJson(output, anchor.localLineRect);
|
||||
output << ",\"easyxSurfaceCaretTopLeft\":";
|
||||
writePointJson(output, surfaceTopLeft);
|
||||
output << ",\"easyxSurfaceCaretBottom\":";
|
||||
writePointJson(output, surfaceBottom);
|
||||
output << ",\"easyxSurfaceLineRect\":";
|
||||
writeRectJson(output, surfaceLine);
|
||||
output << ",\"targetHwndRelativePoint\":";
|
||||
writePointJson(output, surfaceBottom);
|
||||
output << '}';
|
||||
}
|
||||
|
||||
void writeEvent(std::ostream& output, const ImePositionEvent& event)
|
||||
{
|
||||
output << "{\"sequence\":" << event.sequence
|
||||
<< ",\"stage\":" << jsonString(event.stage)
|
||||
<< ",\"positionTrigger\":" << jsonString(event.positionTrigger)
|
||||
<< ",\"syntheticAnchorId\":" << jsonString(event.anchorId)
|
||||
<< ",\"threadId\":" << event.threadId
|
||||
<< ",\"hkl\":" << jsonString(hexValue(event.hkl))
|
||||
<< ",\"imeDescriptionUtf16\":" << utf16UnitsJson(event.imeDescription)
|
||||
<< ",\"imeFileNameUtf16\":" << utf16UnitsJson(event.imeFileName)
|
||||
<< ",\"apiAttempted\":";
|
||||
writeBoolean(output, event.apiAttempted);
|
||||
output << ",\"compositionActiveAtAttempt\":";
|
||||
writeBoolean(output, event.compositionActiveAtAttempt);
|
||||
output << ",\"contextAvailable\":";
|
||||
writeBoolean(output, event.contextAvailable);
|
||||
output << ",\"compositionForm\":{\"style\":" << CFS_POINT
|
||||
<< ",\"apiResult\":";
|
||||
writeBoolean(output, event.compositionSucceeded);
|
||||
output << "},\"candidateForm\":{\"style\":" << CFS_EXCLUDE
|
||||
<< ",\"index\":0,\"apiResult\":";
|
||||
writeBoolean(output, event.candidateSucceeded);
|
||||
output << ",\"excludeRect\":";
|
||||
writeRectJson(output, event.hwndRelativeLineRect);
|
||||
output << "},\"targetHwndRelativePoint\":";
|
||||
writePointJson(output, event.hwndRelativePoint);
|
||||
output << ",\"diagnosticScreenPoint\":";
|
||||
writePointJson(output, event.screenPoint);
|
||||
output << '}';
|
||||
}
|
||||
}
|
||||
|
||||
int runImePositionProbe(const Options& options, const DpiConfiguration& dpiConfiguration)
|
||||
{
|
||||
HWND hwnd = initgraph(CanvasWidth, CanvasHeight, EX_SHOWCONSOLE);
|
||||
if (!hwnd)
|
||||
{
|
||||
std::cerr << "initgraph failed\n";
|
||||
return 3;
|
||||
}
|
||||
SetWindowTextA(hwnd, "StellarX 0C IME Position Probe");
|
||||
ShowWindow(hwnd, SW_SHOWNORMAL);
|
||||
SetForegroundWindow(hwnd);
|
||||
Sleep(80);
|
||||
const EnvironmentSnapshot environment = captureEnvironment(hwnd);
|
||||
DWORD windowProcessId = 0;
|
||||
const DWORD windowOwnerThread = GetWindowThreadProcessId(hwnd, &windowProcessId);
|
||||
|
||||
ImePositionState state;
|
||||
state.hwnd = hwnd;
|
||||
activeState = &state;
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
state.previousWndProc = reinterpret_cast<WNDPROC>(SetWindowLongPtrA(
|
||||
hwnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(&ProbeWndProc)));
|
||||
const DWORD subclassError = state.previousWndProc ? ERROR_SUCCESS : GetLastError();
|
||||
|
||||
std::vector<EvidenceFile> evidence;
|
||||
bool evidenceComplete = state.previousWndProc != nullptr;
|
||||
for (std::size_t index = 0; index < Anchors.size(); ++index)
|
||||
{
|
||||
drawAnchorScreen(static_cast<int>(index));
|
||||
Sleep(40);
|
||||
const auto relative = std::filesystem::path("evidence") /
|
||||
(std::string(Anchors[index].id) + "_client.png");
|
||||
std::string backend;
|
||||
const bool saved = captureClientImage(hwnd, options.outputDirectory / relative, &backend);
|
||||
evidenceComplete = evidenceComplete && saved;
|
||||
if (saved)
|
||||
evidence.push_back({ normalizedRelative(relative), "synthetic anchor client evidence", backend });
|
||||
}
|
||||
state.activeAnchor.store(1, std::memory_order_release);
|
||||
drawAnchorScreen(1);
|
||||
|
||||
int lastDrawnAnchor = 1;
|
||||
const ULONGLONG loopStarted = GetTickCount64();
|
||||
while (!state.closeRequested.load(std::memory_order_acquire) && IsWindow(hwnd))
|
||||
{
|
||||
if (options.autoCloseMilliseconds > 0 &&
|
||||
GetTickCount64() - loopStarted >= options.autoCloseMilliseconds)
|
||||
{
|
||||
recordLifecycle(state, "auto_close");
|
||||
state.closeRequested.store(true, std::memory_order_release);
|
||||
break;
|
||||
}
|
||||
const int currentAnchor = state.activeAnchor.load(std::memory_order_acquire);
|
||||
if (currentAnchor != lastDrawnAnchor)
|
||||
{
|
||||
drawAnchorScreen(currentAnchor);
|
||||
lastDrawnAnchor = currentAnchor;
|
||||
}
|
||||
Sleep(16);
|
||||
}
|
||||
|
||||
if (state.previousWndProc && IsWindow(hwnd))
|
||||
SetWindowLongPtrA(hwnd, GWLP_WNDPROC,
|
||||
reinterpret_cast<LONG_PTR>(state.previousWndProc));
|
||||
activeState = nullptr;
|
||||
if (IsWindow(hwnd)) closegraph();
|
||||
|
||||
std::vector<ImePositionEvent> events;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state.eventsMutex);
|
||||
events = state.events;
|
||||
}
|
||||
std::array<bool, Anchors.size()> anchorSucceeded{};
|
||||
bool imeLifecycleObserved = false;
|
||||
bool imeLifecycleSucceeded = false;
|
||||
for (const auto& event : events)
|
||||
{
|
||||
if (event.positionTrigger == "ime_lifecycle") imeLifecycleObserved = true;
|
||||
if (event.positionTrigger == "ime_lifecycle" && event.contextAvailable &&
|
||||
event.compositionSucceeded && event.candidateSucceeded)
|
||||
{
|
||||
imeLifecycleSucceeded = true;
|
||||
}
|
||||
if (!event.apiAttempted || !event.compositionSucceeded || !event.candidateSucceeded)
|
||||
continue;
|
||||
for (std::size_t index = 0; index < Anchors.size(); ++index)
|
||||
{
|
||||
if (event.anchorId == Anchors[index].id) anchorSucceeded[index] = true;
|
||||
}
|
||||
}
|
||||
bool allAnchorsSucceeded = true;
|
||||
for (const bool value : anchorSucceeded) allAnchorsSucceeded = allAnchorsSucceeded && value;
|
||||
const bool completed = evidenceComplete && allAnchorsSucceeded && imeLifecycleSucceeded;
|
||||
|
||||
std::ostringstream result;
|
||||
result << "{\"expectedImeName\":" << jsonString(options.expectedImeName)
|
||||
<< ",\"windowOwnerThreadId\":" << windowOwnerThread
|
||||
<< ",\"windowProcessId\":" << windowProcessId
|
||||
<< ",\"subclassInstalled\":";
|
||||
writeBoolean(result, state.previousWndProc != nullptr);
|
||||
result << ",\"subclassError\":" << subclassError
|
||||
<< ",\"allAnchorsApiSucceeded\":";
|
||||
writeBoolean(result, allAnchorsSucceeded);
|
||||
result << ",\"imeLifecycleObserved\":";
|
||||
writeBoolean(result, imeLifecycleObserved);
|
||||
result << ",\"imeLifecycleSucceeded\":";
|
||||
writeBoolean(result, imeLifecycleSucceeded);
|
||||
result << ",\"anchors\":[";
|
||||
for (std::size_t index = 0; index < Anchors.size(); ++index)
|
||||
{
|
||||
if (index) result << ',';
|
||||
writeAnchor(result, Anchors[index]);
|
||||
}
|
||||
result << "],\"imeEvents\":[";
|
||||
for (std::size_t index = 0; index < events.size(); ++index)
|
||||
{
|
||||
if (index) result << ',';
|
||||
writeEvent(result, events[index]);
|
||||
}
|
||||
result << "]}";
|
||||
|
||||
std::string error;
|
||||
if (!writeRawResult(options, dpiConfiguration, environment, completed,
|
||||
evidence, result.str(), error))
|
||||
{
|
||||
std::cerr << error << '\n';
|
||||
return 4;
|
||||
}
|
||||
return completed ? 0 : 5;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user