쿼리 편집기 — 한글에서 색과 글자가 어긋나던 문제, 다크 제목 표시줄, 목업 맞춤

■ 한글을 치면 색칠이 글자와 어긋났다 (가장 중요)

색칠 레이어가 글자 위치를 <b>직접 재서</b> 잡고 있었다. 앞부분을 FormattedText 로 측정해
그 폭만큼 오른쪽으로 옮겨 그리는 방식이다. ASCII 에서는 맞았지만 한글에서 무너졌다 —
편집 글꼴(Consolas)에 한글 글리프가 없어 폰트 대체가 일어나는데,
TextBox 의 내부 조판과 이쪽 측정이 같은 글꼴·같은 셰이핑을 고르리라는 보장이 없다.
조금만 달라도 뒤로 갈수록 벌어져 색과 글자가 따로 논다.

이제 <b>TextBox 에게 좌표를 묻는다</b>(GetRectFromCharacterIndex).
여백·스크롤·줄 높이·폰트 대체가 전부 TextBox 기준으로 이미 반영돼 오고,
조각마다 위치를 새로 물으므로 오차가 누적되지 않는다.
줄 높이를 손으로 계산하던 코드도 함께 사라졌다.
편집 글꼴 순서도 한글이 있는 것(D2Coding → 굴림체 → Consolas)으로 바꿔 대체 자체를 줄였다.

--query-popup 진단에 정렬 검사 2건을 넣었다: 한글이 섞인 줄에서 글자 좌표가 뒤로 밀리지 않는가,
마지막 글자 다음 좌표가 캐럿 자리와 이어지는가. 현재 9/9.

■ 다크 모드인데 제목 표시줄만 밝게 남던 문제

ThemedWindow 스타일은 창 <b>안쪽</b>만 칠한다. 제목 표시줄은 OS 가 그리고
기본값은 Windows 의 테마를 따르므로, 앱을 다크로 바꿔도 Windows 가 라이트면 흰 띠가 남는다.

DWM 속성(DWMWA_USE_IMMERSIVE_DARK_MODE)으로 창별로 지정한다. 커스텀 크롬을 직접 그리는 것보다
훨씬 싸고, 최소화·최대화·닫기의 동작과 접근성이 OS 것 그대로 남는다.
창마다 손으로 넣으면 새 대화상자에서 반드시 빠지므로 App 에서 Window 클래스 핸들러로 한 번만 걸었다 —
지금 있는 창과 앞으로 만들 창 모두 자동으로 적용된다.
테마를 바꾸는 순간 이미 떠 있는 창들도 함께 맞춘다(ThemeManager.ApplyChromeToOpenWindows).

■ 목업과 위치 맞춤

앞 배지(아이콘) · 검색칸의 돋보기와 안내 문구 · 안내 상자의 ⓘ 를 넣었다.
갈래 칩 순서는 개수순에서 <b>고정 순서</b>(환자·서식·작업자·컨트롤)로 바꿨다 —
컨트롤 수는 서식마다 달라서 개수순으로 두면 서식을 바꿀 때마다 칩이 자리를 바꿔
손이 기억하지 못한다.

회귀: 테스트 230/230, 편집 스모크 실패 0, 팝업·정렬 점검 9/9, 검증 실행 점검 10/10,
DB 왕복 1,271건 diff 0/예외 0, 종이 렌더 P062 바이트 동일.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Msystech
2026-08-13 16:05:33 +09:00
co-authored by Claude Opus 5
parent 68fc01c6ba
commit 3e26af8739
7 changed files with 224 additions and 94 deletions
@@ -43,6 +43,20 @@ public static class ThemeManager
/// <summary>라이트↔다크 토글</summary>
public static void Toggle() => Apply(!IsLight);
/// <summary>
/// 열려 있는 모든 창의 제목 표시줄을 지금 테마에 맞춘다.
///
/// 제목 표시줄은 창 안쪽 자원(B.*)이 아니라 OS 가 그리므로 사전을 바꿔도 따라오지 않는다.
/// 테마를 바꾼 순간 이미 떠 있는 창들도 함께 맞춰 줘야 위쪽만 밝은 창이 남지 않는다.
/// </summary>
public static void ApplyChromeToOpenWindows()
{
foreach (Window window in Application.Current.Windows)
{
WindowChromeTheme.Apply(window, !IsLight);
}
}
/// <summary>테마 적용 — App.Resources 병합 사전에서 토큰 dict 를 찾아 교체</summary>
public static void Apply(bool light)
{
@@ -57,11 +71,13 @@ public static class ThemeManager
{
dictionaries[i] = new ResourceDictionary { Source = tokensUri };
Save();
ApplyChromeToOpenWindows();
return;
}
}
dictionaries.Add(new ResourceDictionary { Source = tokensUri });
Save();
ApplyChromeToOpenWindows();
}
private static void Save()
@@ -0,0 +1,59 @@
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace SheetMe.Designer.Services;
/// <summary>
/// 창 제목 표시줄을 앱 테마에 맞춘다.
///
/// <b>왜 필요한가.</b> ThemedWindow 스타일은 창 <i>안쪽</i>만 칠한다. 제목 표시줄은 OS 가 그리며
/// 기본값은 <b>Windows 의 테마</b>를 따른다 — 앱을 다크로 바꿔도 Windows 가 라이트면 흰 띠가 남아
/// 창 위쪽만 밝게 뜬다. 반대 조합도 마찬가지다.
///
/// DWM 속성 하나(DWMWA_USE_IMMERSIVE_DARK_MODE)로 창별 지정이 가능하다.
/// 커스텀 크롬을 직접 그리는 것보다 훨씬 적은 비용으로 같은 결과를 얻는다 —
/// 최소화·최대화·닫기의 동작과 접근성이 OS 것 그대로 남는다.
///
/// Windows 10 1809 이전에는 이 속성이 없다. 실패해도 그냥 무시한다(예전 모습으로 남을 뿐이다).
/// </summary>
public static class WindowChromeTheme
{
#region Member Fields
// Windows 10 20H1(빌드 18985) 이상. 그 이전 빌드는 19 를 쓰던 시기가 있어 둘 다 시도한다.
private const int UseImmersiveDarkMode = 20;
private const int UseImmersiveDarkModeBefore20H1 = 19;
#endregion
#region Methods
[DllImport("dwmapi.dll", CharSet = CharSet.Unicode, SetLastError = false)]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size);
/// <summary>
/// 이 창의 제목 표시줄을 어둡게/밝게 — 창 핸들이 생긴 뒤에 불러야 한다.
/// </summary>
public static void Apply(Window window, bool dark)
{
try
{
var handle = new WindowInteropHelper(window).Handle;
if (handle == IntPtr.Zero)
{
return;
}
var value = dark ? 1 : 0;
if (DwmSetWindowAttribute(handle, UseImmersiveDarkMode, ref value, sizeof(int)) != 0)
{
DwmSetWindowAttribute(handle, UseImmersiveDarkModeBefore20H1, ref value, sizeof(int));
}
}
catch (DllNotFoundException)
{
// dwmapi 가 없는 환경 — 제목 표시줄만 OS 기본으로 남는다
}
catch (EntryPointNotFoundException)
{
}
}
#endregion
}